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
TypeScript
/**
* 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;