UNPKG

agentfootprint

Version:

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

222 lines (221 loc) • 13.1 kB
/** * time/convert — a tool's period FORMS (time design § 7.1) and the EXACT * conversions of one range into them (§ 7.2), with the inverse a binding reads. * * Pattern: one table per direction. A tool declares every SHAPE its period * takes (`PeriodForm` — two arguments, one joined string, an object, * a day, a look-back); the library converts a resolved range into the * first form that holds it EXACTLY ({@link convertExact}) and reads a * sent value back into a range ({@link readBack}). Both cross the * argument boundary of § 3.3 through `range.ts` (`boundInto` / * `boundFrom` for a `to` bound; `lookbackRange` for a look-back) — * never inline. * Role: L0 leaf (the time layer). Imports `instant.ts`, `duration.ts`, * `range.ts`, `zone.ts` and `periodForm.ts` — the declaration half * (the shapes, the sugar, each form's own rules, a declared value's * spelling, the tool's facts), split out for the synchronous doors * (`tools/declare.ts`, `ask.ts`, `rows.ts`) and re-exported * here. Asked by `bind.ts` (the fill and the binding) and `drift.ts`, * which only an armed agent loads. * Emits: N/A. * * ## The sugar (§ 7.1) * * | Sugar | Means | * |-------|-------| * | `{ argument, spelling: 'lookback' }` | `{ kind: 'lookback', argument, signed: false }` | * | `spelling: 'signed-lookback'` | the same with `signed: true` | * | `spelling: 'iso-range'` | `{ kind: 'joined', argument, as: 'iso', joiner: '..', edge: 'inclusive' }` | * | `'wall-range'` + `zoneArgument` | `{ kind: 'joined', argument, as: 'wall', joiner: '..', edge: 'inclusive', zone: { argument } }` | * | `accepts: [a, b]` | `[sugar(a), sugar(b)]` | * * ## The exact rows (§ 7.2) — what {@link convertExact} sends * * | Asked | Form | Sent | Note | * |-------|------|------|------| * | a look-back | `lookback` | the same length, the sign added or removed, respelled in the form's `units` | none when no unit of `units` spells it | * | a look-back | `bounds` / `joined` / `object` | `[now − L, now)` in that `as` | | * | a range | `bounds` / `joined` / `object`, `as` `iso` · `epoch-ms` · `epoch-s` | the range; a `to` bound per its `edge` | `epoch-s` rounds outward to whole seconds: `rounded` | * | a range | `as: 'wall'` / `'date'` + a zone | the wall times (dates) in the zone | exact only when the tool reads back the same instants (no doubled hour; a date only at midnights) | * | a whole day | `day` | that day | any other range is not exact — see the inexact rows | * | a range ending at now (within `granularity`) | `lookback` | the smallest spelling in `units` that covers `from` | `rounded` when it is longer | * * Every other row of § 7.2 is not exact, and {@link convertExact} answers * `undefined` for it. * * ## The inexact rows (§ 7.2, step T5b) — what {@link convertWidened} sends * * | Asked | Form | Sent | Reads | * |-------|------|------|-------| * | a range inside one calendar day (in the form's zone) | `day` | that day | the whole day — the rest of it is `extra` | * | a range ending before now (past the tool's step) | `lookback` | the covering look-back from now, in the form's units | `[now − L, now]` — the gap after the range (and any rounding before it) is `extra` | * * A form whose read would be wider than the tool's `maxRange` is skipped. * The refusals are tests the binding asks: a range across days when every * form is a `day` ({@link spansDaysForDayOnly}), a sent wall time the zone * skips ({@link wallGapArgument}), and the facts ({@link periodFactProblem}; * a range only partly older than `retention` runs — * {@link partlyBeyondRetention}). * * ## Precision and the inclusive edge * * An ISO or wall bound is written to the second when the instant is a whole * second, else to the millisecond; an inclusive `to` is "the last instant * inside at that precision" (`to − 1 s`, or `to − 1 ms`). Read back, a bound's * precision is how it was written: minutes (`to + 1 min`), seconds, or a * fraction (`+ 1 ms`). An epoch bound's precision is its unit; a date's is * the calendar day in the zone. * * @example * ```ts * const forms = sugarForms({ argument: 'window', spelling: 'iso-range' }); * convertExact( * { range: { from: '2026-10-09T08:00:00-07:00', to: '2026-10-09T08:41:00-07:00' } }, * forms, * { now: '2026-10-09T15:40:00Z', zone: 'America/Los_Angeles', granularityMs: 60_000 }, * ); * // { form: 0, values: { window: '2026-10-09T08:00:00-07:00..2026-10-09T08:40:59-07:00' } } * ``` */ import { type DurationText } from './duration.js'; import { type InstantText } from './instant.js'; import { sameRange, type TimeRange } from './range.js'; export { sameRange }; import { type ZoneName } from './zone.js'; import { type PeriodFacts, type PeriodForm, type TimeRefusal } from './periodForm.js'; export { BOUND_AS, DEFAULT_GRANULARITY_MS, FACT_UNITS, FORM_KINDS, formArguments, formFacts, formIssue, formMaxRange, isBoundValue, needsZone, parsesUnderForm, PERIOD_SPELLINGS, periodFactProblem, primaryArgument, sugarForm, sugarForms, TIME_REFUSALS, toolFacts, } from './periodForm.js'; export type { Bound, BoundAs, FormArgument, PeriodDirection, PeriodFactProblem, PeriodFacts, PeriodForm, PeriodShapes, PeriodSpelling, TimeRefusal, ToBound, ZoneArgument, } from './periodForm.js'; /** What a conversion knows beyond the window: the turn's clock, the zones, the tool's step. */ export interface ConvertContext { /** The turn's clock (§ 4) — a look-back ends here. */ readonly now: InstantText; /** The zone a wall or date form is written in and its zone argument is sent: the window's. */ readonly zone: ZoneName; /** The app's `.time({ zone })` — the zone of a form that declares `wallZone: 'app'` and no zone argument. */ readonly appZone?: ZoneName; /** The tool's `granularity` in milliseconds, else {@link DEFAULT_GRANULARITY_MS}. */ readonly granularityMs: number; } /** The window to convert: its half-open range and, when it was said as one, its look-back. */ export interface WindowToConvert { readonly range: TimeRange; /** The look-back length the window was said as (`smhdw`), ending at the clock's `now`. */ readonly lookback?: DurationText; } /** One exact conversion: the form used (its index), the argument values, and whether it rounded outward. */ export interface Conversion { readonly form: number; readonly values: Readonly<Record<string, unknown>>; readonly rounded?: true; } /** * The first form (in declared order) that holds `window` EXACTLY — the exact * rows of § 7.2 — as the argument values to send, or `undefined` when no form * does. A form that would widen, shift or drop part of the window is never * chosen here. */ export declare function convertExact(window: WindowToConvert, forms: readonly PeriodForm[], ctx: ConvertContext): Conversion | undefined; /** * One conversion that holds MORE than the window: the form, the values, the * range the tool reads with them (`sent`, half-open) and the parts of it the * window did not ask for (`extra` — one or two ranges, in time order). */ export interface WidenedConversion extends Conversion { readonly sent: TimeRange; readonly extra: readonly TimeRange[]; } /** * The first form (in declared order) that holds `window` by READING MORE than * it — the inexact rows of § 7.2: a range inside one calendar day → that day * (`day`); a range ending before now → the covering look-back from now * (`lookback`). `widestMs` (the tool's `maxRange`) skips a form whose read * would be wider than the tool reads at once. Asked only after * {@link convertExact} found no exact form. */ export declare function convertWidened(window: WindowToConvert, forms: readonly PeriodForm[], ctx: ConvertContext, widestMs?: number): WidenedConversion | undefined; /** * Whether every form is a `day` and the window spans more than one calendar * day in the form's zone — the row § 7.2 refuses before dispatch: one call per * day is the model's choice, not the library's. */ export declare function spansDaysForDayOnly(window: WindowToConvert, forms: readonly PeriodForm[], ctx: ConvertContext): boolean; /** A window a tool can read — the conversion to send — or the reason it cannot, before dispatch. */ export type ToolConversion = { /** Exact, or — when no form holds the window exactly — WIDENED (it carries `sent` and `extra`). */ readonly conversion: Conversion | WidenedConversion; /** The window starts before the source's oldest data and ends after it: it dispatches, marked. */ readonly partlyBeyondRetention?: true; } | { readonly refused: TimeRefusal; }; /** * Whether one tool can read one window, and how — the ONE answer every door * that puts a person's window into a tool asks (the fill, the time ask's * answer and its choices, the served line): the tool's facts first * ({@link periodFactProblem}), then the first form that holds it exactly * ({@link convertExact}), then the first that holds it by reading MORE * ({@link convertWidened} — `maxRange` skips a read too wide), then `multi-day` * for a `day`-only tool ({@link spansDaysForDayOnly}), and otherwise * `no-form-holds`: no declared form can read it at all (a look-back ends at * now, so it cannot reach a window still running; a covering look-back wider * than `maxRange` is not read). Never the tool's own default in its place. * * @example * ```ts * const lookbackOnly = [{ kind: 'lookback', argument: 'window', signed: false }] as const; * const ctx = { now: '2026-10-09T15:40:00Z', zone: 'America/Los_Angeles', granularityMs: 60_000 }; * const yesterday = { from: '2026-10-08T00:00:00-07:00', to: '2026-10-09T00:00:00-07:00' }; * convertForTool({ range: yesterday }, lookbackOnly, {}, ctx); * // { conversion: { form: 0, values: { window: '1960m' }, sent: …, extra: [ …the gap after it… ] } } * convertForTool({ range: yesterday }, lookbackOnly, { maxRange: '24h' }, ctx); * // { refused: 'no-form-holds' } — reaching 8 Oct from now takes a look-back wider than 24h * ``` */ export declare function convertForTool(window: WindowToConvert, forms: readonly PeriodForm[], facts: PeriodFacts | undefined, ctx: ConvertContext): ToolConversion; /** * The first argument of `form` whose sent value is a wall time the zone * SKIPS (a spring-forward gap) — the tool would read a time that never * happened; refused before dispatch (§ 7.2). `undefined` when the form reads * no wall bound, the zone is unknown, or every wall value exists. */ export declare function wallGapArgument(args: Readonly<Record<string, unknown>>, form: PeriodForm, ctx: { readonly appZone?: ZoneName; }): string | undefined; /** * Whether {@link readBack} cannot read `args` for want of a zone alone — a * wall or date form whose zone argument is not in the call yet and no app * zone stands in. A caller that finds every bound present and still gets no * range tells "the zone is still to come" (this) from "the bounds name no * window" (misspelled, a wall time the zone skips, `from` not before `to`). */ export declare function readBackLacksZone(args: Readonly<Record<string, unknown>>, form: PeriodForm, ctx: { readonly appZone?: ZoneName; }): boolean; /** * The half-open range the arguments of `form` name — the § 3.3 conversion back * from each bound — or `undefined` when they do not name one (missing, * misspelled, a wall time the zone doubles or skips, `from` not before `to`). * A look-back is read at the turn's clock (`lookbackRange`). The range is * spelled in UTC: compare it as instants ({@link sameRange}). */ export declare function readBack(args: Readonly<Record<string, unknown>>, form: PeriodForm, ctx: { readonly now: InstantText; readonly appZone?: ZoneName; }): TimeRange | undefined; /** * The absolute range a look-back window was sent as to a bounds form — * `[now − L, now)`, which reads back one millisecond shorter than the * look-back's own `[now − L, now]` — so a binding recognises either. */ export declare function lookbackAsBounds(now: InstantText, lookback: DurationText): TimeRange | undefined; /** The tool's step in milliseconds — its `granularity`, else one minute (§ 7.4). */ export declare function granularityMsOf(facts: PeriodFacts | undefined): number; /** * Whether a half-open range is PARTLY older than what the source keeps — it * starts before `now − retention` and ends after it. It dispatches as asked * (§ 7.2); the result's `held` decides `partly-held`. `false` when the tool * declares no `retention`. */ export declare function partlyBeyondRetention(range: TimeRange, facts: PeriodFacts, now: InstantText): boolean; /** The widest window the tool reads at once (`maxRange`) in milliseconds, when it declares one. */ export declare function widestMsOf(facts: PeriodFacts | undefined): number | undefined;