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 • 8.73 kB
TypeScript
/**
* 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;
//# sourceMappingURL=resolve.d.ts.map