UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

252 lines (251 loc) • 11.6 kB
/** * time/resolveRecord — what the resolver writes down: the candidate and * choice shapes, the app's policy (read at `.time()`), and the checks the * checkpoint door runs on a filed reading. * * Pattern: the record half of `resolve.ts`, split from the resolver so the * SYNCHRONOUS doors — `.time({ policy })` (`clock.ts` · * `readTimeOptions`) and `validateCheckpoint` (`rows.ts` · * `timeRowIsWellFormed`) — reach the shapes and their checks without * the resolver, which only an armed seed loads (`agent/stages/ * timeLayer.ts`, through `import()`: the optional-family law of * docs-next's site budget). `resolve.ts` re-exports everything here. * Role: L0 leaf (the time layer). Imports `range.ts` and `zone.ts` * (the window check) only. * Emits: N/A. * * @example * ```ts * readPolicy({ dateOrder: 'DMY' }); // { dateOrder: 'DMY', year: 'ask' } * readPolicy({ abbreviations: { PST: { zone: 'America/Los_Angeles', offset: '-08:00' } } }); * // { dateOrder: 'ask', year: 'ask', abbreviations: { PST: { zone: 'America/Los_Angeles', offset: '-08:00' } } } * candidateIsWellFormed({ kind: 'range', range: { from: 'x', to: 'y' } }); // false * ``` */ import type { InstantText } from './instant.js'; import type { DurationText } from './duration.js'; import { type TimeRange } from './range.js'; import { type ZoneName } from './zone.js'; import type { Grain } from './present.js'; /** A part of a time a person can say. `duration`: the length of a look-back ("last 40 minutes"). */ export type TimePart = 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'meridiem' | 'zone' | 'duration'; /** What was asked, before the clock is applied. */ export type TimeWindow = { readonly kind: 'range'; readonly range: TimeRange; } /** Always "until the clock's now". */ | { readonly kind: 'lookback'; readonly duration: DurationText; }; /** A library-written note on how a window was read. */ export type TimeNote = /** * The window's END was widened to the end of its last said grain: "to 8:40" * → `08:41`, a day → the next midnight, a lone "9 AM" → `[09:00, 10:00)`. * Never on a range end said as an o'clock hour ("8 AM to 9 AM" ends AT * `09:00`, § 3.3). Ask {@link widenedGrain}, never `grain` alone. */ { readonly kind: 'end-of-grain'; } /** A wall time the clocks go back through: which of its two instants this is (`end`: which end of a range). */ | { readonly kind: 'dst-overlap'; readonly which: 'earlier' | 'later'; readonly end?: 'from' | 'to'; } /** A wall time the clocks skip: which of Temporal's two readings this is. */ | { readonly kind: 'dst-gap'; readonly which: 'earlier' | 'later'; readonly end?: 'from' | 'to'; } /** The person said a numeric offset, not a zone: the instants carry it; `zone` is the clock's. */ | { readonly kind: 'offset-said'; readonly offset: string; } /** A two-digit year, read in the clock's century. */ | { readonly kind: 'century-implied'; readonly century: number; } /** * The person named the zone in words that are no zone name (`London time`, * `PST`): `token` as written, and how the library read it — the one IANA * zone the place names (`place`), the app's abbreviation map's zone * (`abbreviation`), or the fixed offset the abbreviation spells * (`abbreviation-literal`). A proposal like every reading: the person * confirms the zone with the window. */ | { readonly kind: 'zone-read'; readonly token: string; readonly as: 'place' | 'abbreviation' | 'abbreviation-literal'; }; /** Which reading of the PARTS produced a candidate, so a policy — or an ask — can choose among them. */ export interface ReadingTags { readonly dateOrder?: 'MDY' | 'DMY' | 'YMD'; /** The meridiem this reading gave a clock time said without one (the `from` end of a range). */ readonly meridiem?: 'am' | 'pm'; /** The same, for the `to` end of a range. */ readonly endMeridiem?: 'am' | 'pm'; readonly year?: 'said' | 'current' | 'previous'; /** An abbreviation in the app's map: read as its zone (`zone`) or as its literal offset (`literal`). */ readonly abbreviation?: 'zone' | 'literal'; } /** A window read from words, resolved against the clock (§ 3.2). */ export interface ResolvedWindow { readonly window: TimeWindow; /** Always present: a look-back resolved against the clock. Spelled in the offset the person meant. */ readonly range: TimeRange; /** The zone the person meant, else the clock's. */ readonly zone: ZoneName; /** The finest part the person said. */ readonly grain: Grain; /** The parts the person said — empty for a `model` reader's reading (§ 5.5). */ readonly said: readonly TimePart[]; /** The parts this module filled: from the clock, the policy's candidates, the century. */ readonly implied: readonly TimePart[]; readonly anchor: 'message' | 'previous-window' | 'none'; readonly reader: { readonly id: string; readonly kind: 'rule' | 'model'; }; readonly notes: readonly TimeNote[]; } /** One candidate — produced here, never by a strategy. */ export interface TimeCandidate extends ResolvedWindow { readonly reading: ReadingTags; /** Which of the mention's parses it came from. */ readonly parse: number; } /** * One zone abbreviation the app's people write, as data (§ 11): the zone it * stands for, and the fixed offset it literally spells. `PST` in October is * both `America/Los_Angeles` (−07:00, what a person on the US west coast * usually means) and −08:00 (what the letters say) — so a mismatch offers * BOTH readings, never corrects one into the other. */ export interface ZoneAbbreviation { readonly zone: ZoneName; /** `±HH:MM` (or `±HH`, `±HHMM`). */ readonly offset: string; } /** The v1 policy (§ 11): the switches with careful answers. */ export interface TimePolicy { /** `'ask'`: a numeric date's readings become choices. Or the one order this app's people write. */ readonly dateOrder: 'ask' | 'MDY' | 'DMY' | 'YMD'; /** `'ask'`: a date said without a year is asked. `'current'`: the clock's year, recorded as assumed. */ readonly year: 'ask' | 'current'; /** * Absent (the default): no abbreviation maps to a zone — `PST` is ASKED as * a zone. Present: the abbreviations this app's people write, each read as * its zone AND as its literal offset when the two disagree at the instant * (both offered in the one confirmation, noted `zone-read`). An * abbreviation missing from the map is still asked. No map ships. */ readonly abbreviations?: Readonly<Record<string, ZoneAbbreviation>>; } /** No silent MDY, no guessed year. */ export declare const DEFAULT_TIME_POLICY: TimePolicy; /** What the clock gives the resolver. */ export interface ResolveClock { readonly now: InstantText; readonly zone: ZoneName; /** * The clock's `zoneSource` (`clock.ts` · `TimeClock`). `'unknown'` (G15): * `zone` is only the UTC spelling, so a mention that names no zone of its * own is not resolved — the person's zone is asked first. */ readonly zoneSource?: string; } /** Every candidate of one mention, and what kept a parse from any. */ export interface MentionResolution { readonly candidates: readonly TimeCandidate[]; /** A zone the person said that names no zone this layer can read (`PST`): the zone must be asked. */ readonly needsZone: boolean; /** Parts v1 does not resolve, named. */ readonly unsupported: readonly string[]; } /** A question only the person can settle. `abbreviation`: the app's map's zone, or the letters' offset. */ export type OpenQuestion = 'date-order' | 'year' | 'meridiem' | 'dst' | 'zone' | 'abbreviation' | 'parse' | 'confirm'; /** How a mention's reading settled — recorded on its `time-reading` row. */ export type ReadingChoice = /** Every candidate left names the same window. */ { readonly by: 'only'; readonly candidate: number; } /** The policy removed a reading and one window is left — assumed, recorded. */ | { readonly by: 'policy'; readonly candidate: number; readonly policy: Partial<TimePolicy>; } /** An ask must settle it (a later step raises it): the candidates left and the questions. */ | { readonly by: 'open'; readonly remaining: readonly number[]; readonly open: readonly OpenQuestion[]; readonly policy?: Partial<TimePolicy>; } | { readonly by: 'none'; readonly why: 'unreadable' | 'unsupported' | 'no-candidate' | 'excluded-by-policy'; }; /** * A numeric offset as written (`-07`, `-0700`, `-07:00`) → minutes east of * UTC and its `±HH:MM` spelling, or `undefined`. The one reader of an offset * token: `resolve.ts` · `zoneReadsOf` (a said offset) and {@link readPolicy} (an * abbreviation's literal offset) both ask it. */ export declare function offsetOfToken(token: string): { readonly minutes: number; readonly spelled: string; } | undefined; /** * The app's `policy`, read with the v1 defaults filled in — or a problem in * words (an unknown key, a value outside its switch). `abbreviations` is kept * only when given, so a policy without it reads byte-identical to v1's. */ export declare function readPolicy(value: unknown): TimePolicy | string; /** * The grain a candidate's END was widened to, or `undefined` when its `to` * is exact — the one answer to "was the end widened?" (§ 3.3). A label * renders the end as said only then (`readingAsk.ts` · `timeAskOf`), and * the said end is `to − 1 grain` only then (`forms.ts` · `saidEndMs`). * `grain` alone is not the answer: "8 AM to 9 AM" is grain `hour` and ends * AT `09:00`; a look-back is grain `hour` and ends at the clock's now. * * @example * ```ts * widenedGrain({ grain: 'minute', notes: [{ kind: 'end-of-grain' }] }); // 'minute' — [08:00, 08:41) * widenedGrain({ grain: 'hour', notes: [] }); // undefined — [08:00, 09:00) * ``` */ export declare function widenedGrain(candidate: Pick<TimeCandidate, 'notes' | 'grain'>): Grain | undefined; /** * The grain a candidate's LABEL shows its end at (`readingAsk.ts` · * `timeAskOf`), or `undefined` to show the range as it is. A widened end is * shown as said ({@link widenedGrain}). A look-back is the other kept-inside * end: `[now − L, now + 1 ms)` holds the clock's now, so its label ends AT now, * at the grain the person counted in — "last 40 minutes" is `8:00 – 8:40 AM`, * never `8:40:00.001`. Only the label asks this: a look-back's end was not * widened to a grain, so the said end (`forms.ts` · `saidEndMs`) keeps asking * {@link widenedGrain}. * * @example * ```ts * shownGrain({ window: { kind: 'lookback', duration: '40m' }, grain: 'minute', notes: [] }); // 'minute' * shownGrain({ window: { kind: 'range', range }, grain: 'hour', notes: [] }); // undefined — "8 AM to 9 AM" * ``` */ export declare function shownGrain(candidate: Pick<TimeCandidate, 'notes' | 'grain' | 'window'>): Grain | undefined; /** The parts a candidate can name, in the order a record lists them. */ export declare const PART_ORDER: readonly TimePart[]; /** Whether `value` is a candidate this module would have written. */ export declare function candidateIsWellFormed(value: unknown): boolean; /** Whether `value` is a choice this module would have written over `count` candidates. */ export declare function choiceIsWellFormed(value: unknown, count: number): boolean;