UNPKG

agentfootprint

Version:

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

288 lines • 14.2 kB
"use strict"; /** * time/range — the library's one range, its two spellings, and one * conversion per boundary a range crosses. * * Pattern: one internal shape, converted at every edge by a named function — * never inline. Inside the library a range is HALF-OPEN `[from, to)`; * the places a range crosses do not share that edge (the time design, * `docs/design/time/README.md` § 3.3), so each crossing is a function * here and its inverse. * Role: L0 leaf (the time layer's one owner). Imports `instant.ts` and * `duration.ts`. Asked by `tools/declare.ts` · * `parsesUnderSpelling` (an `iso-range` argument is * {@link splitRange} under `'..'` and the strict profile). No other * file splits or joins a range. * Emits: N/A. * * ## The boundaries * * | Boundary | Its edge | Into it from `[from, to)` | Back | * |----------|----------|---------------------------|------| * | a result's `DeclaredPeriod.queried` / `held` | inclusive at both ends | not converted — the tool declares it | {@link fromInclusive} with the tool's step (`granularity`, else 1 ms) | * | a tool argument bound | declared on every `bounds`/`object` end (TQ18); only the sugar defaults inclusive | {@link boundInto} | {@link boundFrom} | * | a look-back argument | `[until − L, until]` | {@link lookbackRange} | {@link lookbackOf} | * | an ask answer / an ISO 8601 interval | `from/to`, half-open | identity: {@link spellRange} | identity: {@link parseRange} | * | an inclusive interval clause (SQL `BETWEEN`, a chart brush) | inclusive at both ends | {@link toInclusive} | {@link fromInclusive} | * * `coverage/period.ts` · `periodVerdict` stays inclusive and is NOT routed * through here: it compares the two inclusive spans a result declared. * * A derived bound keeps the offset its source was written in, spelled in the * strict profile; a bound that is not derived is kept as written. * * @example * ```ts * parseRange('2026-10-09T08:00:00-07:00/2026-10-09T08:41:00-07:00'); * // { from: '2026-10-09T08:00:00-07:00', to: '2026-10-09T08:41:00-07:00' } * toInclusive({ from: '2026-10-09T08:00:00-07:00', to: '2026-10-09T08:41:00-07:00' }, 60_000); * // { from: '2026-10-09T08:00:00-07:00', to: '2026-10-09T08:40:00-07:00' } — "through 08:40" * ``` */ Object.defineProperty(exports, "__esModule", { value: true }); exports.roundOutward = exports.overlaps = exports.sameRange = exports.covers = exports.reachMs = exports.lookbackOf = exports.lookbackRange = exports.boundFrom = exports.boundInto = exports.toInclusive = exports.fromInclusive = exports.stepMsOf = exports.spellRange = exports.isTimeRange = exports.parseRange = exports.splitRange = void 0; const duration_js_1 = require("./duration.js"); const instant_js_1 = require("./instant.js"); /** * The two bounds of a joined range string — exactly one `joiner`, each half an * instant under `profile` — or `undefined`. The ORDER is not checked: a format * check, for a declaration that only promises a spelling. */ function splitRange(value, joiner, profile) { if (typeof value !== 'string') return undefined; const halves = value.split(joiner); if (halves.length !== 2) return undefined; const from = (0, instant_js_1.instantOf)(halves[0], profile); const to = (0, instant_js_1.instantOf)(halves[1], profile); return from === undefined || to === undefined ? undefined : [from, to]; } exports.splitRange = splitRange; /** * A joined string read as a {@link TimeRange} — strict instants, `from` * before `to` — or `undefined`. The bounds are kept as written. */ function parseRange(value, joiner = '/') { const bounds = splitRange(value, joiner, 'strict'); if (bounds === undefined || (0, instant_js_1.compareInstants)(bounds[0], bounds[1]) >= 0) return undefined; const [from, to] = value.split(joiner); return { from, to }; } exports.parseRange = parseRange; /** Whether `value` is a well-formed {@link TimeRange}: strict instants, `from` before `to`, no other key. */ function isTimeRange(value) { if (typeof value !== 'object' || value === null || Array.isArray(value)) return false; const keys = Object.keys(value); if (keys.length !== 2 || !keys.includes('from') || !keys.includes('to')) return false; const { from, to } = value; const a = (0, instant_js_1.instantOf)(from, 'strict'); const b = (0, instant_js_1.instantOf)(to, 'strict'); return a !== undefined && b !== undefined && (0, instant_js_1.compareInstants)(a, b) < 0; } exports.isTimeRange = isTimeRange; function assertRange(range, caller) { if (!isTimeRange(range)) { throw new TypeError(`${caller}: ${JSON.stringify(range)} is not a TimeRange — two strict ISO 8601 instants with a zone, from before to.`); } return [(0, instant_js_1.instantOf)(range.from, 'strict'), (0, instant_js_1.instantOf)(range.to, 'strict')]; } function assertStep(stepMs, caller) { if (!Number.isSafeInteger(stepMs) || stepMs <= 0) { throw new TypeError(`${caller}: the step ${String(stepMs)} is not a positive whole number of milliseconds.`); } } function respell(instant, offsetMinutes, caller) { const text = (0, instant_js_1.spellInstant)(instant, offsetMinutes); if (text === undefined) throw new RangeError(`${caller}: the bound falls outside years 0000–9999.`); return text; } /** The range spelled as one string: `from<joiner>to`, bounds as written. Throws on a range that is not well formed. */ function spellRange(range, joiner = '/') { assertRange(range, 'spellRange'); return `${range.from}${joiner}${range.to}`; } exports.spellRange = spellRange; /** The step a tool's `granularity` names (under `units`), else 1 ms — the § 3.3 rule. */ function stepMsOf(granularity, units) { if (granularity === undefined) return 1; const ms = (0, duration_js_1.durationMs)(granularity, units); if (ms === undefined) throw new TypeError(`stepMsOf: '${String(granularity)}' is not a duration in '${units}'.`); return ms; } exports.stepMsOf = stepMsOf; /** * An inclusive span read back as half-open: `[from, to + step)`. The step is * the source's smallest step (a tool's `granularity`, else 1 ms). The span is * read in the LENIENT profile — a result declares it. */ function fromInclusive(span, stepMs = 1) { assertStep(stepMs, 'fromInclusive'); const from = (0, instant_js_1.instantOf)(span.from, 'lenient'); const to = (0, instant_js_1.instantOf)(span.to, 'lenient'); if (from === undefined || to === undefined || (0, instant_js_1.compareInstants)(from, to) > 0) { throw new TypeError(`fromInclusive: ${JSON.stringify(span)} is not an inclusive span of instants.`); } return { // Kept as written when it is already strict; a lenient spelling (`t`, `z`, `:60`) is respelled. from: (0, instant_js_1.instantOf)(span.from, 'strict') !== undefined ? span.from : respell(from, from.offsetMinutes, 'fromInclusive'), to: respell((0, instant_js_1.shiftInstant)(to, stepMs), to.offsetMinutes, 'fromInclusive'), }; } exports.fromInclusive = fromInclusive; /** * A half-open range written with inclusive ends: `[from, to − step]` — the * last instant inside the range at the reader's precision. Refused when the * range is narrower than one step (it holds no instant at that precision). */ function toInclusive(range, stepMs = 1) { assertStep(stepMs, 'toInclusive'); const [from, to] = assertRange(range, 'toInclusive'); const last = (0, instant_js_1.shiftInstant)(to, -stepMs); if ((0, instant_js_1.compareInstants)(from, last) > 0) { throw new RangeError(`toInclusive: ${spellRange(range)} is narrower than one step of ${stepMs} ms.`); } return { from: range.from, to: respell(last, to.offsetMinutes, 'toInclusive') }; } exports.toInclusive = toInclusive; /** * A half-open range's end as a tool's `to` bound: `exclusive` → as is; * `inclusive` → the last instant inside at the argument's precision * (`to − step`). */ function boundInto(to, edge, stepMs) { assertStep(stepMs, 'boundInto'); const at = (0, instant_js_1.instantOf)(to, 'strict'); if (at === undefined) throw new TypeError(`boundInto: ${JSON.stringify(to)} is not a strict instant.`); return edge === 'exclusive' ? to : respell((0, instant_js_1.shiftInstant)(at, -stepMs), at.offsetMinutes, 'boundInto'); } exports.boundInto = boundInto; /** The inverse of {@link boundInto}: a tool's `to` bound as a half-open range's end. */ function boundFrom(to, edge, stepMs) { assertStep(stepMs, 'boundFrom'); const at = (0, instant_js_1.instantOf)(to, 'strict'); if (at === undefined) throw new TypeError(`boundFrom: ${JSON.stringify(to)} is not a strict instant.`); return edge === 'exclusive' ? to : respell((0, instant_js_1.shiftInstant)(at, stepMs), at.offsetMinutes, 'boundFrom'); } exports.boundFrom = boundFrom; /** * A look-back `duration` evaluated at `until` — `[until − L, until]`, both * ends inside — as a half-open range `[until − L, until + 1 ms)`. */ function lookbackRange(until, duration, units) { const at = (0, instant_js_1.instantOf)(until, 'strict'); if (at === undefined) throw new TypeError(`lookbackRange: ${JSON.stringify(until)} is not a strict instant.`); const ms = (0, duration_js_1.durationMs)(duration, units); if (ms === undefined) throw new TypeError(`lookbackRange: '${String(duration)}' is not a duration in '${units}'.`); return fromInclusive({ from: respell((0, instant_js_1.shiftInstant)(at, -ms), at.offsetMinutes, 'lookbackRange'), to: until, }); } exports.lookbackRange = lookbackRange; /** * The inverse of {@link lookbackRange}: the `until` and the smallest exact * look-back spelling a half-open range is, or `undefined` when its length is * no whole duration in `units`. */ function lookbackOf(range, units) { const [from, to] = assertRange(range, 'lookbackOf'); const until = (0, instant_js_1.shiftInstant)(to, -1); if (from.nanos !== until.nanos) return undefined; const duration = (0, duration_js_1.spellDuration)(until.ms - from.ms, units); return duration === undefined ? undefined : { until: respell(until, to.offsetMinutes, 'lookbackOf'), duration }; } exports.lookbackOf = lookbackOf; /** * How far a half-open range `[from, to)` REACHES, in milliseconds: from its * first instant to its last (`to − 1 ms − from`) — the length a tool's * `maxRange` is held against (an inclusive ceiling: a range is over it only * when its last instant lies further than `maxRange` after its first). The one * owner of that length, so a range is judged by the instants it holds, never by * how it was spelled: the look-back row `[until − L, until]` ({@link lookbackRange}, * half-open `[until − L, until + 1 ms)`) reaches `L`, exactly as the same instants * written as bounds do — a look-back exactly `maxRange` long is one the tool reads * at once, and one more step (`L + 1 min`) is not. * * @example * ```ts * reachMs(Date.parse('2026-10-08T15:40:00Z'), Date.parse('2026-10-09T15:40:00.001Z')); // 86_400_000 — 'last 24h' * reachMs(Date.parse('2026-10-08T00:00:00Z'), Date.parse('2026-10-09T00:00:00Z')); // 86_399_999 — one day * ``` */ function reachMs(fromMs, toMs) { return toMs - 1 - fromMs; } exports.reachMs = reachMs; /** Whether `outer` holds every instant of `inner` (half-open). */ function covers(outer, inner) { const [of, ot] = assertRange(outer, 'covers'); const [inf, int] = assertRange(inner, 'covers'); return (0, instant_js_1.compareInstants)(of, inf) <= 0 && (0, instant_js_1.compareInstants)(int, ot) <= 0; } exports.covers = covers; /** Whether two ranges hold the same instants (their spellings and offsets aside). */ function sameRange(a, b) { const af = (0, instant_js_1.instantOf)(a.from, 'strict'); const at = (0, instant_js_1.instantOf)(a.to, 'strict'); const bf = (0, instant_js_1.instantOf)(b.from, 'strict'); const bt = (0, instant_js_1.instantOf)(b.to, 'strict'); if (af === undefined || at === undefined || bf === undefined || bt === undefined) return false; return (0, instant_js_1.compareInstants)(af, bf) === 0 && (0, instant_js_1.compareInstants)(at, bt) === 0; } exports.sameRange = sameRange; /** Whether `a` and `b` share at least one instant (half-open: touching ends do not overlap). */ function overlaps(a, b) { const [af, at] = assertRange(a, 'overlaps'); const [bf, bt] = assertRange(b, 'overlaps'); return (0, instant_js_1.compareInstants)(af, bt) < 0 && (0, instant_js_1.compareInstants)(bf, at) < 0; } exports.overlaps = overlaps; const floorTo = (instant, stepMs) => ({ ms: instant.ms - (((instant.ms % stepMs) + stepMs) % stepMs), nanos: 0, }); /** * The range widened to whole steps — `from` down, `to` up, never inward — * with `rounded` saying whether anything moved. Steps are counted from the * epoch in UTC: right for seconds, minutes and hours; a calendar day or week * in a zone is `zone.ts`'s question, not this one. */ function roundOutward(range, stepMs) { assertStep(stepMs, 'roundOutward'); const [from, to] = assertRange(range, 'roundOutward'); const down = floorTo(from, stepMs); const toFloor = floorTo(to, stepMs); const up = (0, instant_js_1.compareInstants)(toFloor, to) === 0 ? toFloor : (0, instant_js_1.shiftInstant)(toFloor, stepMs); const fromMoved = (0, instant_js_1.compareInstants)(down, from) !== 0; const toMoved = (0, instant_js_1.compareInstants)(up, to) !== 0; return { range: { from: fromMoved ? respell(down, from.offsetMinutes, 'roundOutward') : range.from, to: toMoved ? respell(up, to.offsetMinutes, 'roundOutward') : range.to, }, rounded: fromMoved || toMoved, }; } exports.roundOutward = roundOutward; //# sourceMappingURL=range.js.map