UNPKG

agentfootprint

Version:

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

130 lines (129 loc) • 8.69 kB
/** * time/resolve — parts + clock + policy → every candidate window, and * the one rule for which candidate a reading settles on. * * Pattern: the hard, universal half of reading time, written once (time * design § 5.1): a strategy tokenizes (`reader.ts`); this module * resolves, the same way for every strategy. It is the one place * zone arithmetic meets the person's words. * Role: L0 leaf (the time layer). Imports `instant.ts`, `zone.ts`, * `range.ts`, `present.ts` (the `Grain` word), `reader.ts` (the * parts) and `resolveRecord.ts` (the shapes, the policy and the * record's checks — split out for the synchronous doors, and * re-exported here). `rowsBuild.ts` · `timeReadingRows` calls it at the * seed, which loads it only under `.time()`. * Emits: N/A. * * ## Every candidate, then the policy (§ 5.1, § 11) * * {@link resolveMention} writes EVERY reading of the parts, each tagged with * how it read them ({@link ReadingTags}): * * | Parts | Candidates | * |-------|------------| * | a numeric date `10/09/26` | up to three: `MDY`, `DMY`, `YMD` — each checked as a calendar day | * | a numeric date of two fields `10/09` | `MDY` and `DMY`, each for the clock's year and the one before | * | a date with no year | the clock's year (`current`) and the one before (`previous`) | * | a two-digit year `26` | the clock's century, noted `century-implied` | * | a clock time with no meridiem, hour 1–12 (`8:40`) | am and pm — one when the form is a 24-hour clock (`clock: '24h'`, an ISO instant) | * | a wall time the clocks go back through (`01:30`) | both instants, each noted `dst-overlap` | * | a wall time the clocks skip (`02:30`) | both readings Temporal names, each noted `dst-gap` | * | a day word (`{ day, offset: -1 }`) | the calendar day in the zone, anchored on the clock | * | a span (`{ minute, count: 40 }`) | a look-back until the clock's `now` | * | a range | one per combination of its sides that agree on date order, year and abbreviation reading, `from` before `to`; a first side with no meridiem takes the second's (`8 to 9 PM` → 8 PM) unless that runs backwards (`11 to 1 PM` → 11 AM); two sides with none share one (`8:45 to 8:55` → AM–AM and PM–PM, never 8:45 AM – 8:55 PM) unless neither runs forward (`11 to 1` → 11 AM – 1 PM, and 11 PM – 1 AM the next day); any range that runs forward on no one day ends the next one, within the half-day (`11 PM to 1 AM`, `23:00 to 1:00`) | * | a place named with `time` (`London time`) | the ONE zone the tz database has for it (`zone.ts` · `zoneOfPlace`), noted `zone-read`; none or several → the zone is asked | * | any mention that names no zone, while the clock's zone is unknown (`zoneSource: 'unknown'`, G15) | none — the person's zone is asked first (`needsZone`) | * | an abbreviation in the app's map (`PST`) | its zone's reading AND its literal offset's, when they name different windows — tagged `abbreviation`, noted `zone-read` | * * The fixed laws: a time is read to the END OF ITS GRAIN ("to 8:40" is * `[08:00, 08:41)`, a lone "9 AM" is `[09:00, 10:00)`, a day ends at the next * midnight — each noted `end-of-grain`) EXCEPT a range end said as an * o'clock hour, which is a boundary on the clock face and ends AT that * instant ("8 AM to 9 AM" is `[08:00, 09:00)`, no note — `endOf`); whether * an end was widened is asked of `widenedGrain`, never of `grain`. A day * starts at its first instant (Temporal's `startOfDay`: a midnight the * clocks skip starts the day at the first wall time that exists); a zone the * person did not say is the clock's; a zone they said is an IANA name, a * numeric offset, a place named with `time` that the tz database names * exactly once, or an abbreviation in the APP's map (`policy.abbreviations`) * — any other token (`PST` with no map, `India time`, `Pacific time`) * resolves nothing and is ASKED (`zoneReadsOf`, the one owner). Parts v1 * does not resolve — a part of the day, a calendar span other than a day, a * window anchored on the previous one — are named `unsupported`, never * guessed. * * {@link chooseReading} then applies the app's policy (`dateOrder`, `year`) * and names what is left: one window (`only`, or `policy` when the policy * removed a reading — a choice nobody said, recorded), several (`open`, with * the questions an ask must settle), or none. A `kind: 'model'` reader's * window is never settled by the library: it stays `open` until the person * confirms it (§ 5.5), and its candidates carry `said: []`. Since the * owner's decision "Always confirm" (time design TQ29) neither is ANY * reading the record files: `rowsBuild.ts` · `timeReadingRows` passes * `confirm: true` for every reader's reading, so only the person's answer * in the time ask settles a window. The `confirm` parameter's default * (`kind === 'model'`) is the unit's, for a caller that resolves parts * outside the record. * * @example * ```ts * const clock = { now: '2026-10-09T15:40:00Z', zone: 'America/Los_Angeles' }; * const rule = { id: 'fixture', kind: 'rule' as const }; * const res = resolveMention([{ date: { kind: 'numeric', fields: [10, 9, 26] } }], clock, rule); * res.candidates.map((c) => c.reading.dateOrder); // ['MDY', 'DMY', 'YMD'] * chooseReading(res, DEFAULT_TIME_POLICY, 'rule'); * // { by: 'open', remaining: [0, 1, 2], open: ['date-order'] } * chooseReading(res, { dateOrder: 'DMY', year: 'ask' }, 'rule'); * // { by: 'policy', candidate: 1, policy: { dateOrder: 'DMY' } } * ``` */ import { type ZoneName } from './zone.js'; import type { TimeParts } from './reader.js'; import { type MentionResolution, type ReadingChoice, type ResolveClock, type TimePolicy } from './resolveRecord.js'; export { candidateIsWellFormed, choiceIsWellFormed, DEFAULT_TIME_POLICY, readPolicy, shownGrain, widenedGrain, } from './resolveRecord.js'; export type { MentionResolution, OpenQuestion, ReadingChoice, ReadingTags, ResolveClock, ResolvedWindow, TimeCandidate, TimeNote, TimePart, TimePolicy, TimeWindow, ZoneAbbreviation, } from './resolveRecord.js'; /** * `London time`, `New York time` — a place named with the word `time` (the * reader keeps it as written): `/^(.+?)\s+time$/i.exec(token)?.[1]`, in one * pass. That regex grew its lazy place one character at a time and, at each, * re-read the blank run after it looking for `time` — quadratic on a letter, a * long blank run and anything but `time`, and a token can come straight from * the message. The place is everything before the blank run that ends just * before `time` (at least one character), and holds no line break, which `.` * would not match. */ export declare function placeBeforeTime(token: string): string | undefined; /** The policy half the resolver reads: the app's abbreviation map. */ type ZonePolicy = Pick<TimePolicy, 'abbreviations'>; /** * The parts with every zone token this layer cannot read (an abbreviation * outside the app's map, `India time`) replaced by the zone the PERSON named * when asked (§ 6.3) — the whole mention's and each range side's. A token it * can read is kept. * * @example * ```ts * withZoneAnswered([{ wall: { h: 8, meridiem: 'am' }, zoneToken: 'PST' }], 'America/Los_Angeles'); * // [{ wall: { h: 8, meridiem: 'am' }, zoneToken: 'America/Los_Angeles' }] * ``` */ export declare function withZoneAnswered(parses: readonly TimeParts[], zone: ZoneName, policy?: ZonePolicy): TimeParts[]; /** * Every candidate window of one mention's parses, resolved against the * clock. A reading the person must confirm (`confirm: true` — every reading * the record files, TQ29; a `model` reader's by default) carries `said: []`. * `policy` is read for its abbreviation map only (`dateOrder` and `year` * are {@link chooseReading}'s): absent, no abbreviation names a zone. */ export declare function resolveMention(parses: readonly TimeParts[], clock: ResolveClock, reader: { readonly id: string; readonly kind: 'rule' | 'model'; }, confirm?: boolean, policy?: ZonePolicy): MentionResolution; /** * How one mention settles under the app's policy (§ 5.1, § 11). The policy * only removes readings; it never adds one. A DST choice, a meridiem, a * zone the person named that is no zone, and a reading to confirm * (`confirm` — every reading the record files, TQ29) are never settled here * — they stay `open` for the person. */ export declare function chooseReading(resolution: MentionResolution, policy: TimePolicy, kind: 'rule' | 'model', problem?: 'unreadable', confirm?: boolean): ReadingChoice;