UNPKG

agentfootprint

Version:

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

312 lines • 12.7 kB
"use strict"; /** * time/present — time rendered for a PERSON, in a named zone. * * Pattern: one renderer, every person-facing line (time design § 3.1, § 10.2): * the limits block's `Period:` line (`coverage/period.ts` · * `periodLine`) and, later, every time label the layer shows. A * label is never parsed back; the typed record keeps the instants. * Role: L0 leaf (the time layer). Imports `instant.ts`, `zone.ts` and * `range.ts` only. * Emits: N/A. * * ## The form — locale-neutral, the zone always named * * With no locale (no reader armed) there is no language to speak: the * form is ISO-like and the same in every language — a date, a 24-hour wall * time in the presentation zone, then the zone's name and its offset at that * moment: * * ``` * 2026-10-09 08:00–08:40 America/Los_Angeles (UTC-07:00) * 2026-10-09 23:00 – 2026-10-10 01:00 Asia/Kolkata (UTC+05:30) * 2026-11-01 01:00 (UTC-07:00) – 2026-11-01 01:30 (UTC-08:00) America/Los_Angeles * 2026-10-09 15:00–15:40 UTC * ``` * * The time is written as finely as either end needs it (minutes, seconds, or * milliseconds), never finer. When the two ends sit on different offsets (the * span crosses a DST change) each end names its own; otherwise the offset is * named once. * * ## With a locale (the reader's, § 11) * * A {@link Presentation} with a `locale` — the armed reader's, the language * the person wrote in — is rendered through `Intl.DateTimeFormat` in that * locale, at the same precision, the zone named by its short name * (`Fri, Oct 9, 2026, 8:00 – 8:40 AM PDT` in `en-US`). The words are the * runtime's ICU data; the instants behind the label are the record. * * ## The said end (§ 3.3) * * Inside the library a person's range is half-open: "to 8:40" read to the end * of its grain is `[08:00, 08:41)`. {@link presentRange} with that grain shows * the end AS SAID — the last instant inside the range at the grain, * `08:40` — never `08:41`. A declared period's `queried` / `held` is * inclusive already, so {@link presentSpan} shows its ends as declared. * * @example * ```ts * const la = { zone: 'America/Los_Angeles' }; * presentRange({ from: '2026-10-09T08:00:00-07:00', to: '2026-10-09T08:41:00-07:00' }, la, 'minute'); * // '2026-10-09 08:00–08:40 America/Los_Angeles (UTC-07:00)' * presentSpan('2026-10-09T15:00:00Z', '2026-10-09T15:40:00Z', la); * // '2026-10-09 08:00–08:40 America/Los_Angeles (UTC-07:00)' * presentInstant('2026-10-09T15:40:03Z', la); * // '2026-10-09 08:40:03 America/Los_Angeles (UTC-07:00)' * ``` */ Object.defineProperty(exports, "__esModule", { value: true }); exports.presentRange = exports.bindPresentation = exports.presentSpan = exports.presentInstant = void 0; const instant_js_1 = require("./instant.js"); const zone_js_1 = require("./zone.js"); const ORDER = [ 'year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond', ]; /** A week is shown as its days: a calendar week has no one ISO-like spelling people share. */ const PRECISION_OF = { second: 'second', minute: 'minute', hour: 'hour', day: 'day', week: 'day', month: 'month', year: 'year', }; const pad = (n, width = 2) => String(n).padStart(width, '0'); function finer(a, b) { return ORDER.indexOf(a) >= ORDER.indexOf(b) ? a : b; } /** The coarsest precision that writes this wall time exactly (never coarser than minutes). */ function naturalPrecision(wall) { if ((wall.millisecond ?? 0) !== 0) return 'millisecond'; if ((wall.second ?? 0) !== 0) return 'second'; return 'minute'; } function spellDate(wall, precision) { const year = wall.year < 0 ? `-${pad(-wall.year, 4)}` : pad(wall.year, 4); if (precision === 'year') return year; if (precision === 'month') return `${year}-${pad(wall.month)}`; return `${year}-${pad(wall.month)}-${pad(wall.day)}`; } /** The time of day at `precision`, or `''` for a date-only precision. */ function spellClock(wall, precision) { switch (precision) { case 'year': case 'month': case 'day': return ''; case 'hour': return `${pad(wall.hour)}:00`; case 'minute': return `${pad(wall.hour)}:${pad(wall.minute)}`; case 'second': return `${pad(wall.hour)}:${pad(wall.minute)}:${pad(wall.second ?? 0)}`; case 'millisecond': return `${pad(wall.hour)}:${pad(wall.minute)}:${pad(wall.second ?? 0)}.${pad(wall.millisecond ?? 0, 3)}`; } } /** `UTC-07:00`, `UTC+05:45`; a historical offset that is not whole minutes keeps its seconds. */ function spellOffset(offsetMinutes) { const totalSeconds = Math.round(offsetMinutes * 60); const sign = totalSeconds < 0 ? '-' : '+'; const abs = Math.abs(totalSeconds); const h = Math.floor(abs / 3600); const m = Math.floor((abs % 3600) / 60); const s = abs % 60; return `UTC${sign}${pad(h)}:${pad(m)}${s === 0 ? '' : `:${pad(s)}`}`; } /** Whether a zone is UTC itself, which needs no offset beside its name. */ const isUtcName = (zone) => zone === 'UTC' || zone === 'Etc/UTC'; function pointOf(instant, zone) { return { wall: (0, zone_js_1.wallAt)(zone, instant.ms), offset: spellOffset((0, zone_js_1.offsetAt)(zone, instant.ms)) }; } function assertZone(presentation, caller) { if (!(0, zone_js_1.isZoneName)(presentation?.zone)) { throw new TypeError(`${caller}: ${JSON.stringify(presentation?.zone)} is not an IANA zone name.`); } return presentation.zone; } /** The fields `Intl` writes at each precision — the date always, the zone always named. */ const LOCALE_FIELDS = { year: { year: 'numeric' }, month: { year: 'numeric', month: 'short' }, day: { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }, hour: { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }, minute: { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }, second: { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', second: '2-digit', }, millisecond: { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3, }, }; /** The locale's formatter at `precision` in `zone`; throws a `TypeError` on a tag `Intl` cannot read. */ function localeFormatter(locale, zone, precision, caller) { try { return new Intl.DateTimeFormat(locale, { ...LOCALE_FIELDS[precision], timeZone: zone, timeZoneName: 'short', }); } catch { throw new TypeError(`${caller}: ${JSON.stringify(locale)} is not a language tag.`); } } /** Two instants (`b` not before `a`) in the locale's own words: one label, or a range. */ function renderLocale(aMs, bMs, presentation, zone, precision, caller) { const formatter = localeFormatter(presentation.locale, zone, precision, caller); return aMs === bMs ? formatter.format(new Date(aMs)) : formatter.formatRange(new Date(aMs), new Date(bMs)); } function readInstant(value, caller) { const instant = (0, instant_js_1.instantOf)(value, 'lenient'); if (instant === undefined) { throw new TypeError(`${caller}: ${JSON.stringify(value)} is not an ISO 8601 instant with a zone.`); } return instant; } function spellPoint(point, precision) { const clock = spellClock(point.wall, precision); const date = spellDate(point.wall, precision); return clock === '' ? date : `${date} ${clock}`; } function zoneTag(zone, offset) { return isUtcName(zone) ? zone : `${zone} (${offset})`; } function render(a, b, zone, precision) { const left = spellPoint(a, precision); const right = spellPoint(b, precision); if (a.offset !== b.offset && !isUtcName(zone)) { return `${left} (${a.offset}) – ${right} (${b.offset}) ${zone}`; } const tag = zoneTag(zone, a.offset); if (left === right) return `${left} ${tag}`; const sameDate = spellDate(a.wall, precision) === spellDate(b.wall, precision); const clockA = spellClock(a.wall, precision); const clockB = spellClock(b.wall, precision); if (sameDate && clockA !== '' && clockB !== '') { return `${spellDate(a.wall, precision)} ${clockA}–${clockB} ${tag}`; } return `${left} – ${right} ${tag}`; } /** * One instant for a person: its wall time in the presentation zone, written * as finely as it needs (never coarser than minutes), and the zone named. */ function presentInstant(value, presentation) { const zone = assertZone(presentation, 'presentInstant'); const instant = readInstant(value, 'presentInstant'); const point = pointOf(instant, zone); if (presentation.locale !== undefined) { const precision = naturalPrecision(point.wall); return renderLocale(instant.ms, instant.ms, presentation, zone, precision, 'presentInstant'); } return `${spellPoint(point, naturalPrecision(point.wall))} ${zoneTag(zone, point.offset)}`; } exports.presentInstant = presentInstant; /** * Two INCLUSIVE ends — a declared period's `queried` or `held` — for a * person, each shown as declared (no end moved), in the presentation zone. * Accepts the lenient profile: a period a result declares. */ function presentSpan(from, to, presentation) { const zone = assertZone(presentation, 'presentSpan'); const fromInstant = readInstant(from, 'presentSpan'); const toInstant = readInstant(to, 'presentSpan'); const a = pointOf(fromInstant, zone); const b = pointOf(toInstant, zone); const precision = finer(naturalPrecision(a.wall), naturalPrecision(b.wall)); if (presentation.locale !== undefined && fromInstant.ms <= toInstant.ms) { return renderLocale(fromInstant.ms, toInstant.ms, presentation, zone, precision, 'presentSpan'); } return render(a, b, zone, precision); } exports.presentSpan = presentSpan; /** {@link presentSpan}, {@link presentInstant} and {@link presentRange}, bound to one presentation. */ function bindPresentation(presentation) { return { span: (from, to) => presentSpan(from, to, presentation), instant: (value) => presentInstant(value, presentation), range: (range, grain) => presentRange(range, presentation, grain), }; } exports.bindPresentation = bindPresentation; /** * A half-open range for a person. With the `grain` the person said, the end * is the end AS SAID (§ 3.3): the last instant inside the range, written at * that grain — `[08:00, 08:41)` at `'minute'` shows `08:00–08:40`, and a * `'day'` range `[9 Oct, 10 Oct)` shows `2026-10-09`. Without a grain (an * exact range) both ends are shown as they are. */ function presentRange(range, presentation, grain) { const zone = assertZone(presentation, 'presentRange'); const from = readInstant(range?.from, 'presentRange'); const to = readInstant(range?.to, 'presentRange'); if (from.ms > to.ms || (from.ms === to.ms && from.nanos >= to.nanos)) { throw new TypeError('presentRange: from must be before to.'); } const a = pointOf(from, zone); if (grain === undefined) { const b = pointOf(to, zone); const precision = finer(naturalPrecision(a.wall), naturalPrecision(b.wall)); if (presentation.locale !== undefined) { return renderLocale(from.ms, to.ms, presentation, zone, precision, 'presentRange'); } return render(a, b, zone, precision); } const precision = PRECISION_OF[grain]; if (precision === undefined) { throw new TypeError(`presentRange: ${JSON.stringify(grain)} is not a grain.`); } // The last instant inside the range: one millisecond (or the sub-millisecond // remainder) before `to`. Written at the grain, that is the end as said. const lastMs = to.nanos > 0 ? to.ms : to.ms - 1; if (presentation.locale !== undefined) { return renderLocale(from.ms, lastMs, presentation, zone, precision, 'presentRange'); } const b = { wall: (0, zone_js_1.wallAt)(zone, lastMs), offset: spellOffset((0, zone_js_1.offsetAt)(zone, lastMs)) }; return render(a, b, zone, precision); } exports.presentRange = presentRange; //# sourceMappingURL=present.js.map