UNPKG

agentfootprint

Version:

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

731 lines • 37 kB
"use strict"; /** * 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' } } * ``` */ Object.defineProperty(exports, "__esModule", { value: true }); exports.widestMsOf = exports.partlyBeyondRetention = exports.granularityMsOf = exports.lookbackAsBounds = exports.readBack = exports.readBackLacksZone = exports.wallGapArgument = exports.convertForTool = exports.spansDaysForDayOnly = exports.convertWidened = exports.convertExact = exports.toolFacts = exports.TIME_REFUSALS = exports.sugarForms = exports.sugarForm = exports.primaryArgument = exports.periodFactProblem = exports.PERIOD_SPELLINGS = exports.parsesUnderForm = exports.needsZone = exports.isBoundValue = exports.formMaxRange = exports.formIssue = exports.formFacts = exports.formArguments = exports.FORM_KINDS = exports.FACT_UNITS = exports.DEFAULT_GRANULARITY_MS = exports.BOUND_AS = exports.sameRange = void 0; const duration_js_1 = require("./duration.js"); const instant_js_1 = require("./instant.js"); const range_js_1 = require("./range.js"); Object.defineProperty(exports, "sameRange", { enumerable: true, get: function () { return range_js_1.sameRange; } }); const zone_js_1 = require("./zone.js"); const periodForm_js_1 = require("./periodForm.js"); var periodForm_js_2 = require("./periodForm.js"); Object.defineProperty(exports, "BOUND_AS", { enumerable: true, get: function () { return periodForm_js_2.BOUND_AS; } }); Object.defineProperty(exports, "DEFAULT_GRANULARITY_MS", { enumerable: true, get: function () { return periodForm_js_2.DEFAULT_GRANULARITY_MS; } }); Object.defineProperty(exports, "FACT_UNITS", { enumerable: true, get: function () { return periodForm_js_2.FACT_UNITS; } }); Object.defineProperty(exports, "FORM_KINDS", { enumerable: true, get: function () { return periodForm_js_2.FORM_KINDS; } }); Object.defineProperty(exports, "formArguments", { enumerable: true, get: function () { return periodForm_js_2.formArguments; } }); Object.defineProperty(exports, "formFacts", { enumerable: true, get: function () { return periodForm_js_2.formFacts; } }); Object.defineProperty(exports, "formIssue", { enumerable: true, get: function () { return periodForm_js_2.formIssue; } }); Object.defineProperty(exports, "formMaxRange", { enumerable: true, get: function () { return periodForm_js_2.formMaxRange; } }); Object.defineProperty(exports, "isBoundValue", { enumerable: true, get: function () { return periodForm_js_2.isBoundValue; } }); Object.defineProperty(exports, "needsZone", { enumerable: true, get: function () { return periodForm_js_2.needsZone; } }); Object.defineProperty(exports, "parsesUnderForm", { enumerable: true, get: function () { return periodForm_js_2.parsesUnderForm; } }); Object.defineProperty(exports, "PERIOD_SPELLINGS", { enumerable: true, get: function () { return periodForm_js_2.PERIOD_SPELLINGS; } }); Object.defineProperty(exports, "periodFactProblem", { enumerable: true, get: function () { return periodForm_js_2.periodFactProblem; } }); Object.defineProperty(exports, "primaryArgument", { enumerable: true, get: function () { return periodForm_js_2.primaryArgument; } }); Object.defineProperty(exports, "sugarForm", { enumerable: true, get: function () { return periodForm_js_2.sugarForm; } }); Object.defineProperty(exports, "sugarForms", { enumerable: true, get: function () { return periodForm_js_2.sugarForms; } }); Object.defineProperty(exports, "TIME_REFUSALS", { enumerable: true, get: function () { return periodForm_js_2.TIME_REFUSALS; } }); Object.defineProperty(exports, "toolFacts", { enumerable: true, get: function () { return periodForm_js_2.toolFacts; } }); // ─── Wall times and dates as text (the parsers are periodForm.ts') ───── const pad = (n, width) => String(n).padStart(width, '0'); function spellWall(wall, withMs) { const base = `${pad(wall.year, 4)}-${pad(wall.month, 2)}-${pad(wall.day, 2)}` + `T${pad(wall.hour, 2)}:${pad(wall.minute, 2)}:${pad(wall.second ?? 0, 2)}`; return withMs ? `${base}.${pad(wall.millisecond ?? 0, 3)}` : base; } const spellDate = (d) => `${pad(d.year, 4)}-${pad(d.month, 2)}-${pad(d.day, 2)}`; /** The calendar day after `d` (proleptic Gregorian). */ function nextDay(d) { const ms = (0, instant_js_1.utcWallMs)(d.year, d.month, d.day, 0, 0, 0) + 86_400_000; const t = new Date(ms); return { year: t.getUTCFullYear(), month: t.getUTCMonth() + 1, day: t.getUTCDate() }; } /** The calendar day before `d`. */ function previousDay(d) { const ms = (0, instant_js_1.utcWallMs)(d.year, d.month, d.day, 0, 0, 0) - 86_400_000; const t = new Date(ms); return { year: t.getUTCFullYear(), month: t.getUTCMonth() + 1, day: t.getUTCDate() }; } /** The one instant a wall time names in `zone` — `undefined` in a doubled or skipped hour. */ function uniqueInstant(wall, zone) { const reading = (0, zone_js_1.readWall)(wall, zone); return reading.kind === 'unique' ? reading.ms : undefined; } /** The first instant of a calendar day in `zone`, when exactly one instant is it. */ const midnightOf = (d, zone) => uniqueInstant({ ...d, hour: 0, minute: 0 }, zone); /** An instant a range bound names, exactly to the millisecond — `undefined` for a sub-millisecond one. */ function msOf(text) { const at = (0, instant_js_1.instantOf)(text, 'strict'); if (at === undefined || at.nanos !== 0) return undefined; return { ms: at.ms, offsetMinutes: at.offsetMinutes }; } /** The zone a wall or date form is written in: its zone argument's (the window's), else the app's under `wallZone`. */ function formZone(form, ctx) { return 'zone' in form && form.zone !== undefined ? ctx.zone : ctx.appZone; } /** One bound written in `as`: the value, and whether writing it moved the instant (epoch seconds, outward). */ function writeBound(at, which, as, edge, zone) { switch (as) { case 'iso': { const step = at.ms % 1000 === 0 ? 1_000 : 1; const ms = which === 'to' && edge === 'inclusive' ? at.ms - step : at.ms; const text = (0, instant_js_1.spellInstant)({ ms, nanos: 0 }, at.offsetMinutes); return text === undefined ? undefined : { value: text, rounded: false }; } case 'epoch-ms': return { value: which === 'to' && edge === 'inclusive' ? at.ms - 1 : at.ms, rounded: false }; case 'epoch-s': { const whole = which === 'from' ? Math.floor(at.ms / 1000) * 1000 : Math.ceil(at.ms / 1000) * 1000; const seconds = whole / 1000 - (which === 'to' && edge === 'inclusive' ? 1 : 0); return { value: seconds, rounded: whole !== at.ms }; } case 'wall': { if (zone === undefined) return undefined; const step = at.ms % 1000 === 0 ? 1_000 : 1; const ms = which === 'to' && edge === 'inclusive' ? at.ms - step : at.ms; const wall = (0, zone_js_1.wallAt)(zone, ms); // Exact only when the tool reads the wall time back as this instant. if (uniqueInstant(wall, zone) !== ms) return undefined; return { value: spellWall(wall, step === 1), rounded: false }; } case 'date': { if (zone === undefined) return undefined; const wall = (0, zone_js_1.wallAt)(zone, at.ms); const date = { year: wall.year, month: wall.month, day: wall.day }; if (midnightOf(date, zone) !== at.ms) return undefined; const day = which === 'to' && edge === 'inclusive' ? previousDay(date) : date; return { value: spellDate(day), rounded: false }; } } } /** The range a form's bounds hold, for a window: a look-back's is `[now − L, now)`. */ function absoluteOf(window, now) { if (window.lookback !== undefined) { const n = msOf(now); const length = (0, duration_js_1.durationMs)(window.lookback, duration_js_1.AXIS_UNITS); if (n === undefined || length === undefined) return undefined; return [ { ms: n.ms - length, offsetMinutes: n.offsetMinutes }, { ms: n.ms, offsetMinutes: n.offsetMinutes }, ]; } const from = msOf(window.range.from); const to = msOf(window.range.to); return from === undefined || to === undefined || from.ms >= to.ms ? undefined : [from, to]; } function lookbackInto(window, form, ctx) { const units = form.units ?? duration_js_1.LOOKBACK_UNITS; const sign = form.signed ? '-' : ''; if (window.lookback !== undefined) { const ms = (0, duration_js_1.durationMs)(window.lookback, duration_js_1.AXIS_UNITS); const spelled = ms === undefined ? undefined : (0, duration_js_1.spellDuration)(ms, units); return spelled === undefined ? undefined : { value: `${sign}${spelled}`, rounded: false }; } // A range ending at now (within the tool's step): the smallest spelling that covers `from`. const now = msOf(ctx.now); const from = msOf(window.range.from); const to = msOf(window.range.to); if (now === undefined || from === undefined || to === undefined) return undefined; if (Math.abs(to.ms - now.ms) > ctx.granularityMs) return undefined; const need = now.ms - from.ms; if (need <= 0) return undefined; const finest = finestUnitMs(units); const length = Math.ceil(need / finest) * finest; const spelled = (0, duration_js_1.spellDuration)(length, units); // Rounded when the length grew, or the end moved to now (a look-back ends at now, both ends inside). const endMoved = to.ms !== now.ms && to.ms !== now.ms + 1; return spelled === undefined ? undefined : { value: `${sign}${spelled}`, rounded: length !== need || endMoved }; } const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000, }; function finestUnitMs(units) { return Math.min(...[...units].map((u) => UNIT_MS[u])); } /** One form's exact values for a window, or `undefined` when the form does not hold it exactly. */ function convertOne(window, form, ctx) { if (form.kind === 'lookback') { const sent = lookbackInto(window, form, ctx); return sent === undefined ? undefined : { values: { [form.argument]: sent.value }, rounded: sent.rounded }; } const zone = formZone(form, ctx); if ((0, periodForm_js_1.needsZone)(form) && zone === undefined) return undefined; const zoneValue = 'zone' in form && form.zone !== undefined ? { [form.zone.argument]: ctx.zone } : {}; const span = absoluteOf(window, ctx.now); if (span === undefined) return undefined; const [from, to] = span; if (form.kind === 'day') { const z = zone; const wall = (0, zone_js_1.wallAt)(z, from.ms); const date = { year: wall.year, month: wall.month, day: wall.day }; if (midnightOf(date, z) !== from.ms || midnightOf(nextDay(date), z) !== to.ms) return undefined; return { values: { [form.argument]: spellDate(date), ...zoneValue }, rounded: false }; } const fromAs = form.kind === 'bounds' ? form.from.as : form.as; const toAs = form.kind === 'bounds' ? form.to.as : form.as; // `bounds` / `object` declare their edge (TQ18); only a `joined` form (the sugar's) defaults. const edge = form.kind === 'joined' ? form.edge ?? 'inclusive' : form.kind === 'bounds' ? form.to.edge : form.edge; const a = writeBound(from, 'from', fromAs, 'inclusive', zone); const b = writeBound(to, 'to', toAs, edge, zone); if (a === undefined || b === undefined) return undefined; const rounded = a.rounded || b.rounded; switch (form.kind) { case 'bounds': return { values: { [form.from.argument]: a.value, [form.to.argument]: b.value, ...zoneValue }, rounded, }; case 'joined': return { values: { [form.argument]: `${a.value}${form.joiner}${b.value}`, ...zoneValue }, rounded, }; case 'object': return { values: { [form.argument]: { [form.keys.from]: a.value, [form.keys.to]: b.value }, ...zoneValue, }, rounded, }; } } /** * 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. */ function convertExact(window, forms, ctx) { for (let i = 0; i < forms.length; i++) { const done = convertOne(window, forms[i], ctx); if (done !== undefined) { return { form: i, values: done.values, ...(done.rounded && { rounded: true }) }; } } return undefined; } exports.convertExact = convertExact; /** The parts of `[sentFrom, sentTo)` outside `[from, to)`, in time order. */ function extraOf(sentFrom, sentTo, from, to) { const parts = []; const before = from > sentFrom ? spanRange(sentFrom, from) : undefined; const after = sentTo > to ? spanRange(to, sentTo) : undefined; if (before !== undefined) parts.push(before); if (after !== undefined) parts.push(after); return parts; } /** The calendar date an instant falls on in `zone`. */ function dateAt(zone, ms) { const wall = (0, zone_js_1.wallAt)(zone, ms); return { year: wall.year, month: wall.month, day: wall.day }; } /** A `day` form widened: the one calendar day that holds the whole window. */ function dayWidened(window, form, ctx) { const zone = formZone(form, ctx); const span = absoluteOf(window, ctx.now); if (zone === undefined || span === undefined) return undefined; const [from, to] = span; const date = dateAt(zone, from.ms); if (spellDate(dateAt(zone, to.ms - 1)) !== spellDate(date)) return undefined; const start = midnightOf(date, zone); const end = midnightOf(nextDay(date), zone); if (start === undefined || end === undefined) return undefined; const sent = spanRange(start, end); if (sent === undefined) return undefined; const zoneValue = form.zone !== undefined ? { [form.zone.argument]: ctx.zone } : {}; return { values: { [form.argument]: spellDate(date), ...zoneValue }, sent, extra: extraOf(start, end, from.ms, to.ms), }; } /** * A `lookback` form widened: a range that ENDS BEFORE now (past the tool's * step) is covered by the look-back from now that reaches its `from` — the * smallest length in the form's units. A range ending at now is the exact * row's; one ending after it no look-back can hold. */ function lookbackWidened(window, form, ctx) { if (window.lookback !== undefined) return undefined; const now = msOf(ctx.now); // `absoluteOf` refuses an empty or inverted range, as every other form's reading does — a // look-back from now "covering" a range that ends before it starts would read a confident // wrong window (its `extra` outside its own `sent`). const span = absoluteOf(window, ctx.now); if (now === undefined || span === undefined) return undefined; const [from, to] = span; if (to.ms >= now.ms - ctx.granularityMs) return undefined; const units = form.units ?? duration_js_1.LOOKBACK_UNITS; const finest = finestUnitMs(units); const length = Math.ceil((now.ms - from.ms) / finest) * finest; const spelled = (0, duration_js_1.spellDuration)(length, units); if (spelled === undefined) return undefined; // The look-back reads `[now − L, now]`, both ends inside (`range.ts` · `lookbackRange`). const sent = (0, range_js_1.lookbackRange)(ctx.now, spelled, units); const sentTo = msOf(sent.to).ms; return { values: { [form.argument]: `${form.signed ? '-' : ''}${spelled}` }, sent, extra: extraOf(now.ms - length, sentTo, from.ms, to.ms), }; } /** * 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. */ function convertWidened(window, forms, ctx, widestMs) { for (let i = 0; i < forms.length; i++) { const form = forms[i]; const done = form.kind === 'day' ? dayWidened(window, form, ctx) : form.kind === 'lookback' ? lookbackWidened(window, form, ctx) : undefined; if (done === undefined || done.extra.length === 0) continue; const sentFrom = msOf(done.sent.from); const sentTo = msOf(done.sent.to); // The read's reach, judged as `periodFactProblem` judges a window (`range.ts` · `reachMs`): // a look-back `[now − L, now]` reaches `L`, so one exactly `maxRange` long is read at once. if (widestMs !== undefined && (0, range_js_1.reachMs)(sentFrom.ms, sentTo.ms) > widestMs) continue; return { form: i, ...done }; } return undefined; } exports.convertWidened = convertWidened; /** * 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. */ function spansDaysForDayOnly(window, forms, ctx) { if (forms.length === 0 || !forms.every((f) => f.kind === 'day')) return false; const span = absoluteOf(window, ctx.now); if (span === undefined) return false; return forms.every((f) => { const zone = formZone(f, ctx); if (zone === undefined) return false; return spellDate(dateAt(zone, span[0].ms)) !== spellDate(dateAt(zone, span[1].ms - 1)); }); } exports.spansDaysForDayOnly = spansDaysForDayOnly; /** * 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 * ``` */ function convertForTool(window, forms, facts, ctx) { // The tool's facts before a form is chosen: `maxRange` is the widest any form reads, so a window // is over it only when NO form can read it (`periodForm.ts` · `toolFacts`; the period's own // facts when no form declares its own `maxRange`). const judged = facts === undefined ? undefined : (0, periodForm_js_1.toolFacts)(forms, facts); const problem = judged === undefined ? undefined : (0, periodForm_js_1.periodFactProblem)(window.range, judged, ctx.now); if (problem !== undefined) return { refused: problem }; const partly = facts !== undefined && partlyBeyondRetention(window.range, facts, ctx.now) ? { partlyBeyondRetention: true } : {}; const read = forms.some((f) => f.maxRange !== undefined) ? convertWithinFormCaps(window, forms, facts, ctx) : convertExact(window, forms, ctx) ?? convertWidened(window, forms, ctx, widestMsOf(facts)); if (read !== undefined) return { conversion: read, ...partly }; if (spansDaysForDayOnly(window, forms, ctx)) return { refused: 'multi-day' }; return { refused: 'no-form-holds' }; } exports.convertForTool = convertForTool; /** * {@link convertForTool}'s exact-then-wider search when a form declares its * own `maxRange`: the same order (the first form that holds the window * exactly, then the first that holds it by reading more), each form held to * ITS cap ({@link formMaxRange}) — an exact form the window reaches past, or a * wider read past it, is skipped, and the next form is asked. */ function convertWithinFormCaps(window, forms, facts, ctx) { const capMs = (form) => { const text = (0, periodForm_js_1.formMaxRange)(form, facts); return text === undefined ? undefined : (0, duration_js_1.durationMs)(text, periodForm_js_1.FACT_UNITS); }; const from = msOf(window.range.from); const to = msOf(window.range.to); const reach = from === undefined || to === undefined ? undefined : (0, range_js_1.reachMs)(from.ms, to.ms); for (let i = 0; i < forms.length; i++) { const form = forms[i]; const cap = capMs(form); if (cap !== undefined && (reach === undefined || reach > cap)) continue; const exact = convertExact(window, [form], ctx); if (exact !== undefined) return { ...exact, form: i }; } for (let i = 0; i < forms.length; i++) { const form = forms[i]; const widened = convertWidened(window, [form], ctx, capMs(form)); if (widened !== undefined) return { ...widened, form: i }; } return undefined; } /** * 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. */ function wallGapArgument(args, form, ctx) { if (form.kind === 'day' || form.kind === 'lookback') return undefined; const sentZone = 'zone' in form && form.zone !== undefined ? args[form.zone.argument] : undefined; const zone = formZone(form, { zone: (0, zone_js_1.isZoneName)(sentZone) ? sentZone : undefined, ...(ctx.appZone !== undefined && { appZone: ctx.appZone }), }); if (zone === undefined) return undefined; const inGap = (value) => { const read = (0, periodForm_js_1.wallOf)(value); return read !== undefined && (0, zone_js_1.readWall)(read.wall, zone).kind === 'gap'; }; if (form.kind === 'bounds') { for (const bound of [form.from, form.to]) { if (bound.as === 'wall' && inGap(args[bound.argument])) return bound.argument; } return undefined; } if (form.as !== 'wall') return undefined; const value = args[form.argument]; if (form.kind === 'joined') { if (typeof value !== 'string') return undefined; return value.split(form.joiner).some(inGap) ? form.argument : undefined; } if (!(0, periodForm_js_1.isPlain)(value)) return undefined; return inGap(value[form.keys.from]) || inGap(value[form.keys.to]) ? form.argument : undefined; } exports.wallGapArgument = wallGapArgument; // ─── Reading a sent value back ─────────────────────────────────────────── /** A bound read back: the instant it names as a half-open end, in milliseconds. */ function readBound(value, which, as, edge, zone) { const inclusiveTo = which === 'to' && edge === 'inclusive'; switch (as) { case 'iso': { const at = (0, instant_js_1.instantOf)(value, 'strict'); if (at === undefined || at.nanos !== 0) return undefined; const step = at.precision === 'minute' ? 60_000 : at.precision === 'second' ? 1_000 : 1; return inclusiveTo ? at.ms + step : at.ms; } case 'epoch-ms': if (typeof value !== 'number' || !Number.isSafeInteger(value)) return undefined; return inclusiveTo ? value + 1 : value; case 'epoch-s': if (typeof value !== 'number' || !Number.isSafeInteger(value)) return undefined; return (inclusiveTo ? value + 1 : value) * 1000; case 'wall': { if (zone === undefined) return undefined; const read = (0, periodForm_js_1.wallOf)(value); if (read === undefined) return undefined; const ms = uniqueInstant(read.wall, zone); return ms === undefined ? undefined : inclusiveTo ? ms + read.stepMs : ms; } case 'date': { if (zone === undefined) return undefined; const date = (0, periodForm_js_1.dateOf)(value); if (date === undefined) return undefined; return midnightOf(inclusiveTo ? nextDay(date) : date, zone); } } } const epochOrText = (as, text) => as === 'epoch-ms' || as === 'epoch-s' ? (0, periodForm_js_1.epochFromText)(text) : text; function spanRange(fromMs, toMs) { if (!(fromMs < toMs)) return undefined; const from = (0, instant_js_1.spellInstant)({ ms: fromMs, nanos: 0 }, 0); const to = (0, instant_js_1.spellInstant)({ ms: toMs, nanos: 0 }, 0); return from === undefined || to === undefined ? undefined : { from, to }; } /** The zone `form`'s arguments are read in: the zone argument's value when the form has one, else the app's. */ function readZone(args, form, ctx) { const sentZone = 'zone' in form && form.zone !== undefined ? args[form.zone.argument] : undefined; return formZone(form, { zone: (0, zone_js_1.isZoneName)(sentZone) ? sentZone : undefined, ...(ctx.appZone !== undefined && { appZone: ctx.appZone }), }); } /** * 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`). */ function readBackLacksZone(args, form, ctx) { return form.kind !== 'lookback' && (0, periodForm_js_1.needsZone)(form) && readZone(args, form, ctx) === undefined; } exports.readBackLacksZone = readBackLacksZone; /** * 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}). */ function readBack(args, form, ctx) { if (form.kind === 'lookback') { const value = args[form.argument]; if (typeof value !== 'string') return undefined; const units = form.units ?? duration_js_1.LOOKBACK_UNITS; if (form.signed && !value.startsWith('-')) return undefined; const length = form.signed ? value.slice(1) : value; if (!(0, duration_js_1.isDuration)(length, units) || (0, duration_js_1.durationMs)(length, units) === undefined) return undefined; return (0, range_js_1.lookbackRange)(ctx.now, length, units); } const zone = readZone(args, form, ctx); if ((0, periodForm_js_1.needsZone)(form) && zone === undefined) return undefined; if (form.kind === 'day') { if (zone === undefined) return undefined; const date = (0, periodForm_js_1.dateOf)(args[form.argument]); if (date === undefined) return undefined; const from = midnightOf(date, zone); const to = midnightOf(nextDay(date), zone); return from === undefined || to === undefined ? undefined : spanRange(from, to); } let fromValue; let toValue; let fromAs; let toAs; let edge; if (form.kind === 'bounds') { fromValue = args[form.from.argument]; toValue = args[form.to.argument]; fromAs = form.from.as; toAs = form.to.as; edge = form.to.edge; } else if (form.kind === 'joined') { const text = args[form.argument]; if (typeof text !== 'string') return undefined; const halves = text.split(form.joiner); if (halves.length !== 2) return undefined; fromValue = epochOrText(form.as, halves[0]); toValue = epochOrText(form.as, halves[1]); fromAs = toAs = form.as; edge = form.edge ?? 'inclusive'; } else { const object = args[form.argument]; if (!(0, periodForm_js_1.isPlain)(object)) return undefined; fromValue = object[form.keys.from]; toValue = object[form.keys.to]; fromAs = toAs = form.as; edge = form.edge; } const from = readBound(fromValue, 'from', fromAs, 'inclusive', zone); const to = readBound(toValue, 'to', toAs, edge, zone); return from === undefined || to === undefined ? undefined : spanRange(from, to); } exports.readBack = readBack; /** * 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. */ function lookbackAsBounds(now, lookback) { const n = msOf(now); const length = (0, duration_js_1.durationMs)(lookback, duration_js_1.AXIS_UNITS); if (n === undefined || length === undefined) return undefined; return spanRange(n.ms - length, n.ms); } exports.lookbackAsBounds = lookbackAsBounds; /** The tool's step in milliseconds — its `granularity`, else one minute (§ 7.4). */ function granularityMsOf(facts) { const ms = facts?.granularity === undefined ? undefined : (0, duration_js_1.durationMs)(facts.granularity, periodForm_js_1.FACT_UNITS); return ms ?? periodForm_js_1.DEFAULT_GRANULARITY_MS; } exports.granularityMsOf = granularityMsOf; // ─── The tool's facts against a range (the checks live in periodForm.ts) ── /** * 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`. */ function partlyBeyondRetention(range, facts, now) { const retention = facts.retention === undefined ? undefined : (0, duration_js_1.durationMs)(facts.retention, periodForm_js_1.FACT_UNITS); const from = (0, instant_js_1.instantOf)(range.from, 'strict'); const to = (0, instant_js_1.instantOf)(range.to, 'strict'); const at = (0, instant_js_1.instantOf)(now, 'strict'); if (retention === undefined || from === undefined || to === undefined || at === undefined) { return false; } const oldest = at.ms - retention; return from.ms < oldest && to.ms > oldest; } exports.partlyBeyondRetention = partlyBeyondRetention; /** The widest window the tool reads at once (`maxRange`) in milliseconds, when it declares one. */ function widestMsOf(facts) { return facts?.maxRange === undefined ? undefined : (0, duration_js_1.durationMs)(facts.maxRange, periodForm_js_1.FACT_UNITS); } exports.widestMsOf = widestMsOf; //# sourceMappingURL=convert.js.map