agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
631 lines • 30.6 kB
JavaScript
;
/**
* coverage/period — the ONE period shape a result declares, the ONE rule set
* every door asks, and the ONE verdict.
*
* Pattern: one rule set, every door (the `items.ts` law for `short` and
* `kind`): the three mints — `absent()`, `coverage()`,
* `describedResult()` — REFUSE a malformed period at the line the
* author wrote, and the recognizers READ a period an envelope minted
* elsewhere (a Python helper, an older or newer process) without
* repairing it. Both ask {@link periodProblem}, so a mint and a read
* cannot disagree about what a well-formed period is.
* Role: core/ layer leaf of the result doors (honesty layer 3). Imports only
* `refusal.ts`, the dev-mode flag and the time layer's one instant
* parser (`time/instant.ts` · `instantOf`, lenient profile) and its
* person-facing renderer (`time/present.ts`). The results layer
* (`../../core/agent/results/subflow.ts`) files one verdict per call from
* {@link periodVerdict}; the limits block prints one line per
* declaring call from {@link periodLine}.
* Emits: N/A.
*
* ## The shape — anchored on the READ
*
* ```ts
* period: {
* queried: { from: '2026-09-26T09:00:00Z', to: '2026-09-26T10:00:00Z' }, // what the read asked for
* held: { from: '2026-08-27T02:00:00Z', to: '2026-09-26T02:00:00Z' }, // what the store holds — or 'unknown'
* readAt: '2026-09-26T10:00:03Z', // when the read ran (optional)
* }
* // on the wire: "period": { "queried": {…}, "held": {…} | "unknown", "read_at"? }
* ```
*
* Every value is an ISO 8601 instant WITH a zone (`Z` or `±HH:MM`) — the
* library compares instants, and an instant without a zone is ambiguous, so it
* is refused rather than guessed. `held: 'unknown'` is the store saying out
* loud that it cannot vouch for what it holds. Nothing here ever parses a
* duration: the library compares instants the tool declared and never reads
* "2h" (the inputs layer's `ToolPeriod` says which ARGUMENT set the period;
* this shape says what the READ covered).
*
* ## Why not the word `window`, and why not a coverage item
*
* `window` names the context window (`core/agent/window/`, `.window()`). A
* coverage item's `kind` is a closed, record-only word: it cannot hold two
* instants to compare.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.clockLines = exports.periodCheckLine = exports.periodRowIsWellFormed = exports.PERIOD_ROW_VERDICTS = exports.periodLine = exports.noteWithClause = exports.noteWithoutClause = exports.unservedPeriod = exports.servedPeriod = exports.PERIOD_VERDICT_CLAUSES = exports.SERVED_VERDICT_KEY = exports.leastHeld = exports.periodVerdict = exports._resetPeriodWarnings = exports.warnDroppedDeclaration = exports.copyPeriod = exports.readPeriod = exports.mintPeriod = exports.periodProblem = exports.PERIOD_WIRE = void 0;
const footprintjs_1 = require("footprintjs");
const instant_js_1 = require("../../time/instant.js");
const checkRecord_js_1 = require("../../time/checkRecord.js");
const refusal_js_1 = require("./refusal.js");
/**
* The period's wire spelling as DATA — every reserved key and the one literal
* (`'unknown'`) — so a tool written in another language mints it byte for
* byte. `canonical-notes.json` publishes it, generated from this constant
* (`scripts/gen-canonical-notes.mjs`): the JSON cannot disagree with the code.
*
* @example
* ```ts
* PERIOD_WIRE.readAt; // 'read_at' — the one key whose wire spelling differs
* ```
*/
exports.PERIOD_WIRE = Object.freeze({
/** The key a period sits under on `af_absent`, `af_coverage` and `af_semantics`. */
key: 'period',
queried: 'queried',
held: 'held',
from: 'from',
to: 'to',
readAt: 'read_at',
/** The literal `held` carries when the store cannot vouch for what it holds. */
heldUnknown: 'unknown',
});
const isPlainObject = (value) => {
if (typeof value !== 'object' || value === null || Array.isArray(value))
return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
};
/** The shown form of a value in a message — JSON, clipped: a message is never a copy of a document. */
function shown(value) {
let text;
try {
text = JSON.stringify(value) ?? String(value);
}
catch {
text = String(value);
}
return text.length > 60 ? `${text.slice(0, 57)}…` : text;
}
const readAtKey = (s) => (s === 'camel' ? 'readAt' : 'read_at');
/** The first unknown key of `value`, as a problem — naming the spelling meant when it is a slip. */
function unknownKey(value, known, at) {
for (const key of Object.keys(value)) {
if (known.includes(key))
continue;
const meant = (0, refusal_js_1.spellingMeant)(key, known);
return {
field: `${at}.${key}`,
message: `'${at}.${key}' is not a field this vocabulary has` +
(meant === undefined ? '.' : ` — did you mean \`${meant}\`?`) +
` The fields of \`${at}\` are: ${known.join(', ')}.`,
};
}
return undefined;
}
/** The problem with one instant field, or `undefined` when it names an instant. */
function instantProblem(value, at) {
if ((0, instant_js_1.instantOf)(value, 'lenient') !== undefined)
return undefined;
return {
field: at,
message: `\`${at}\` must be an ISO 8601 instant with a zone — e.g. '2026-09-26T08:00:00Z' or ` +
`'2026-09-26T10:00:00+02:00' — got ${shown(value)}. The library compares instants, and ` +
'one with no zone could be any of 24 hours, so it is refused rather than guessed.',
};
}
/** The problem with one `{ from, to }` span, or `undefined` when it is one. */
function spanProblem(value, at) {
if (!isPlainObject(value)) {
return {
field: at,
message: `\`${at}\` must be { from, to } — two instants, got ${shown(value)}.`,
};
}
const extra = unknownKey(value, ['from', 'to'], at);
if (extra !== undefined)
return extra;
const bad = instantProblem(value.from, `${at}.from`) ?? instantProblem(value.to, `${at}.to`);
if (bad !== undefined)
return bad;
// Both parse — checked just above.
if ((0, instant_js_1.compareInstants)((0, instant_js_1.instantOf)(value.from, 'lenient'), (0, instant_js_1.instantOf)(value.to, 'lenient')) > 0) {
return {
field: at,
message: `\`${at}.from\` (${String(value.from)}) is after \`${at}.to\` (${String(value.to)}) — ` +
'a span runs forward.',
};
}
return undefined;
}
/**
* THE rule set: the first fault in a period, or `undefined` when it is well
* formed — asked by every mint (which refuses it) and every recognizer (which
* drops it from the record and serves the envelope as the tool wrote it).
*
* In order: a plain object; no key it does not have (a spelling slip names the
* key meant — `read_at` at a camelCase door names `readAt`); `queried` a
* `{ from, to }` span of instants with `from` not after `to`; `held` the
* literal `'unknown'` or a span by the same rule; `readAt` an instant.
*
* @example
* ```ts
* periodProblem({ queried: { from: 'yesterday', to: '2026-09-26T10:00:00Z' }, held: 'unknown' }, 'camel');
* // → { field: 'period.queried.from', message: '`period.queried.from` must be an ISO 8601 instant with a zone — …' }
* ```
*/
function periodProblem(value, spelling) {
const readAt = readAtKey(spelling);
if (!isPlainObject(value)) {
return {
field: 'period',
message: `\`period\` must be { queried, held, ${readAt}? } — got ${shown(value)}.`,
};
}
const extra = unknownKey(value, ['queried', 'held', readAt], 'period');
if (extra !== undefined)
return extra;
if (value.queried === undefined || value.queried === null) {
return {
field: 'period.queried',
message: '`period.queried` must say what the read asked for — { from, to }, two instants of the ' +
'READ that produced this result.',
};
}
const queried = spanProblem(value.queried, 'period.queried');
if (queried !== undefined)
return queried;
if (value.held !== 'unknown') {
if (value.held === undefined || value.held === null) {
return {
field: 'period.held',
message: "`period.held` must say what the store holds — { from, to }, or 'unknown' said out " +
'loud when the tool cannot vouch for it.',
};
}
const held = spanProblem(value.held, 'period.held');
if (held !== undefined) {
return typeof value.held === 'string'
? {
field: 'period.held',
message: `\`period.held\` is ${shown(value.held)} — it must be { from, to } or the literal ` +
"'unknown'.",
}
: held;
}
}
const at = value[readAt];
if (at !== undefined && at !== null) {
const bad = instantProblem(at, `period.${readAt}`);
if (bad !== undefined)
return bad;
}
return undefined;
}
exports.periodProblem = periodProblem;
// ─── The doors ───────────────────────────────────────────────────────────
/**
* Mint the wire form from an author's (camelCase) declaration — THROWS a
* refusal (`refused: …`) on the first fault, at the line the author wrote (the
* `absent()` law). `undefined` or `null` → `undefined`: not declared. A fresh
* object, keys in wire order; nothing of the author's is held.
*/
function mintPeriod(declared) {
if (declared === undefined || declared === null)
return undefined;
const problem = periodProblem(declared, 'camel');
if (problem !== undefined)
throw (0, refusal_js_1.refusal)(problem.message);
const period = declared;
return {
queried: { from: period.queried.from, to: period.queried.to },
held: period.held === 'unknown' ? 'unknown' : { from: period.held.from, to: period.held.to },
...(period.readAt != null && { read_at: period.readAt }),
};
}
exports.mintPeriod = mintPeriod;
/**
* Read a wire period off a recognized envelope — never repaired. `{}` when
* the envelope declares none (`undefined` or `null`, a JSON producer's
* `None`); `{ period }` in the record's camelCase form when it is well formed;
* `{ problem }` when it is not, so the caller can drop it from the record and
* say so — the model still reads what the tool wrote.
*/
function readPeriod(wire) {
if (wire === undefined || wire === null)
return {};
const problem = periodProblem(wire, 'wire');
if (problem !== undefined)
return { problem };
const onWire = wire;
return {
period: {
queried: { from: onWire.queried.from, to: onWire.queried.to },
held: onWire.held === 'unknown' ? 'unknown' : { from: onWire.held.from, to: onWire.held.to },
...(onWire.read_at != null && { readAt: onWire.read_at }),
},
};
}
exports.readPeriod = readPeriod;
/** A declared period as detached plain data — the record's copy. */
function copyPeriod(period) {
return {
queried: { from: period.queried.from, to: period.queried.to },
held: period.held === 'unknown' ? 'unknown' : { from: period.held.from, to: period.held.to },
...(period.readAt !== undefined && { readAt: period.readAt }),
};
}
exports.copyPeriod = copyPeriod;
/** (declaration, tool) pairs already warned about — one warning per tool per PROCESS for each (dev mode only). */
const warnedTools = new Set();
const MAX_WARNED = 500;
/**
* Say, once per tool per process and in dev mode only, that a recognized
* envelope's `period` — or an absence's `provenance` — could not be read and
* was left off the record. The envelope itself is still recognized and served
* as the tool wrote it — read, never repaired (the `items.ts` ·
* `readItemExtras` precedent).
*/
function warnDroppedDeclaration(toolName, declaration, problem) {
const key = `${declaration} ${toolName ?? ''}`;
if (warnedTools.has(key) || !(0, footprintjs_1.isDevMode)())
return;
if (warnedTools.size < MAX_WARNED)
warnedTools.add(key);
// eslint-disable-next-line no-console
console.warn(`agentfootprint coverage: tool '${toolName ?? '(unknown)'}' returned a ${declaration} the ` +
`record cannot carry, so the ${declaration} was left off the record (the result itself is ` +
`kept and served as written): ${problem.message} This warning fires once per tool per ` +
`process.`);
}
exports.warnDroppedDeclaration = warnDroppedDeclaration;
/**
* Forget every dropped-declaration warning issued so far.
* @internal test seam — the ledger is process-wide and warn-once.
*/
function _resetPeriodWarnings() {
warnedTools.clear();
}
exports._resetPeriodWarnings = _resetPeriodWarnings;
// ─── The verdict ─────────────────────────────────────────────────────────
/**
* How far a declared period's READ covered what it asked for — ONE pure rule
* over the instants the tool declared; bounds are inclusive:
*
* ```text
* held === 'unknown' → 'unknown'
* held.from ≤ queried.from and queried.to ≤ held.to → 'covered'
* queried.to < held.from or queried.from > held.to → 'not-held'
* otherwise → 'partly-held'
* ```
*
* No clock is read and no duration is parsed: the tool declared both spans.
* Throws a `TypeError` naming the fault when handed a period that is not well
* formed — a caller error; every period the library files was checked first.
*
* @example
* ```ts
* periodVerdict({
* queried: { from: '2026-09-26T09:00:00Z', to: '2026-09-26T10:00:00Z' },
* held: { from: '2026-08-27T02:00:00Z', to: '2026-09-26T02:00:00Z' },
* }); // 'not-held' — the data ends at 02:00; the hour asked about is after it
* ```
*/
function periodVerdict(period) {
const problem = periodProblem(period, 'camel');
if (problem !== undefined)
throw new TypeError(`periodVerdict: ${problem.message}`);
if (period.held === 'unknown')
return 'unknown';
const qFrom = (0, instant_js_1.instantOf)(period.queried.from, 'lenient');
const qTo = (0, instant_js_1.instantOf)(period.queried.to, 'lenient');
const hFrom = (0, instant_js_1.instantOf)(period.held.from, 'lenient');
const hTo = (0, instant_js_1.instantOf)(period.held.to, 'lenient');
if ((0, instant_js_1.compareInstants)(hFrom, qFrom) <= 0 && (0, instant_js_1.compareInstants)(qTo, hTo) <= 0)
return 'covered';
if ((0, instant_js_1.compareInstants)(qTo, hFrom) < 0 || (0, instant_js_1.compareInstants)(qFrom, hTo) > 0)
return 'not-held';
return 'partly-held';
}
exports.periodVerdict = periodVerdict;
/**
* The verdict a CALL gets when its result declared more than one period (a
* `coverage()` around an `absent()`, each with its own): the LEAST held of
* them, so a reader is never told more than the weakest declaration covers —
* `not-held`, then `partly-held`, then `unknown`, then `covered`.
*/
function leastHeld(verdicts) {
for (const verdict of ['not-held', 'partly-held', 'unknown', 'covered']) {
if (verdicts.includes(verdict))
return verdict;
}
return undefined;
}
exports.leastHeld = leastHeld;
/** The key the served verdict word sits under, inside the served `period`. */
exports.SERVED_VERDICT_KEY = 'verdict';
/**
* The ONE note clause per served verdict (honesty step 7b, bench round 1).
* Static library text: no instant, no tool value, nothing interpolated — the
* `ABSENCE_NOTE` law. Each states the CONSEQUENCE of its word in the terms the
* notes already teach ("ground this result does NOT cover"), because the bench
* showed a model repeating `period.queried` as the ground a result covered
* without ever comparing it with `period.held`.
*/
exports.PERIOD_VERDICT_CLAUSES = Object.freeze({
'not-held': '`period.verdict` is `not-held`: the store holds none of the time this read asked about ' +
'(`period.queried`), so all of that time is ground this result does NOT cover — this ' +
'result settles nothing about it, and the time the store does hold is `period.held`.',
'partly-held': '`period.verdict` is `partly-held`: the store holds only part of the time this read asked ' +
'about (`period.queried`), so the part outside `period.held` is ground this result does ' +
'NOT cover — what this result says holds for `period.held` only.',
unknown: '`period.verdict` is `unknown`: the tool cannot say what time its store holds, so this ' +
'result cannot vouch that it covers the time this read asked about (`period.queried`).',
});
/**
* What the model is served for one declared wire period — `undefined` when
* there is nothing to add: no period, a period the rule set refuses (served
* as the tool wrote it — read, never repaired), or a `covered` one.
*
* The verdict is {@link periodVerdict} over the instants the tool declared —
* the one rule the record's `period` row is filed by, so the word the model
* reads and the row the fold reads cannot disagree. Computed at the SERVE
* door, never minted: the tool's own output keeps the bytes it declared, so a
* helper in another language mints nothing new.
*
* @example
* ```ts
* servedPeriod({
* queried: { from: '2026-09-26T09:00:00Z', to: '2026-09-26T10:00:00Z' },
* held: { from: '2026-08-27T02:00:00Z', to: '2026-09-26T02:00:00Z' },
* })?.period; // { queried: {…}, held: {…}, verdict: 'not-held' }
* ```
*/
function servedPeriod(wire) {
const { period } = readPeriod(wire);
if (period === undefined)
return undefined;
const verdict = periodVerdict(period);
if (verdict === 'covered')
return undefined;
const onWire = mintPeriod(period);
return {
period: { ...onWire, [exports.SERVED_VERDICT_KEY]: verdict },
verdict,
clause: exports.PERIOD_VERDICT_CLAUSES[verdict],
};
}
exports.servedPeriod = servedPeriod;
/**
* The inverse, for a reader of what the model was SERVED: a wire period with
* its served verdict word removed — when that word is exactly the one
* {@link servedPeriod} computes from the rest. Anything else (no word, a word
* that disagrees with the instants, a period the rule set refuses) comes back
* as the same reference, so the rule set still names it: a verdict is derived
* by the library, never declared by a tool.
*/
function unservedPeriod(wire) {
if (!isPlainObject(wire) || !(exports.SERVED_VERDICT_KEY in wire))
return wire;
const { [exports.SERVED_VERDICT_KEY]: word, ...declared } = wire;
return servedPeriod(declared)?.verdict === word ? declared : wire;
}
exports.unservedPeriod = unservedPeriod;
/**
* A served note with its period clause removed (the inverse of appending
* {@link PERIOD_VERDICT_CLAUSES}`[verdict]`), or the note as found.
*/
function noteWithoutClause(note, verdict) {
if (typeof note !== 'string')
return note;
const clause = exports.PERIOD_VERDICT_CLAUSES[verdict];
if (note === clause)
return undefined;
return note.endsWith(` ${clause}`) ? note.slice(0, -(clause.length + 1)) : note;
}
exports.noteWithoutClause = noteWithoutClause;
/** A served note: the note as found, then the period clause. */
function noteWithClause(note, clause) {
return typeof note === 'string' && note !== '' ? `${note} ${clause}` : clause;
}
exports.noteWithClause = noteWithClause;
// ─── The person's line ───────────────────────────────────────────────────
/**
* The `Period:` line the limits block prints for one declaring call — the
* period AS THE TOOL DECLARED IT, instants verbatim (no reformatting, no zone
* conversion, no verdict word: the answer's standing owns the verdict's
* sentence). Static words around the tool's own values. With a
* `presentation` (the run's clock zone, only under `.time()`), the instants
* are rendered in that zone instead — see {@link periodLineInZone}.
*
* @example
* ```ts
* periodLine('backup_runs', {
* queried: { from: '2026-09-26T09:00:00Z', to: '2026-09-26T10:00:00Z' },
* held: { from: '2026-08-27T02:00:00Z', to: '2026-09-26T02:00:00Z' },
* });
* // 'backup_runs queried 2026-09-26T09:00:00Z to 2026-09-26T10:00:00Z; the store holds
* // 2026-08-27T02:00:00Z to 2026-09-26T02:00:00Z'
* ```
*/
function periodLine(toolName, period, presentation) {
if (presentation !== undefined)
return periodLineInZone(toolName, period, presentation);
const queried = `${toolName} queried ${period.queried.from} to ${period.queried.to}`;
const held = period.held === 'unknown'
? 'what the store holds is unknown'
: `the store holds ${period.held.from} to ${period.held.to}`;
const readAt = period.readAt !== undefined ? ` (read at ${period.readAt})` : '';
return `${queried}; ${held}${readAt}`;
}
exports.periodLine = periodLine;
/**
* The same line under `.time()` (time design § 10.2): every instant rendered
* in the presentation zone by the time layer's one renderer
* (`time/present.ts`), the zone named, each end AS DECLARED (a declared
* period is inclusive, so no end moves). The raw instants stay in the typed
* record (`coverageDeclared`, `answerCoverage`); a label is never parsed back.
*
* @example
* ```ts
* periodLine('backup_runs', {
* queried: { from: '2026-09-26T09:00:00Z', to: '2026-09-26T10:00:00Z' },
* held: 'unknown',
* }, bindPresentation({ zone: 'America/Los_Angeles' })); // time/present.ts
* // 'backup_runs queried 2026-09-26 02:00–03:00 America/Los_Angeles (UTC-07:00);
* // what the store holds is unknown'
* ```
*/
function periodLineInZone(toolName, period, presentation) {
const queried = `${toolName} queried ${presentation.span(period.queried.from, period.queried.to)}`;
const held = period.held === 'unknown'
? 'what the store holds is unknown'
: `the store holds ${presentation.span(period.held.from, period.held.to)}`;
const readAt = period.readAt !== undefined ? `; read at ${presentation.instant(period.readAt)}` : '';
return `${queried}; ${held}${readAt}`;
}
/** Whether a value is a {@link HeldSpan} the rule set accepts — the checkpoint door's test. */
function isHeldSpan(value) {
if (!isPlainObject(value))
return false;
return (spanProblem(value.queried, 'held.queried') === undefined &&
spanProblem(value.held, 'held.held') === undefined &&
Object.keys(value).every((k) => k === 'queried' || k === 'held'));
}
/** Every verdict a period row may carry, in the order the fold reports them. */
exports.PERIOD_ROW_VERDICTS = Object.freeze([
'covered',
'partly-held',
'not-held',
'unknown',
'undeclared',
]);
/**
* The checkpoint door's test for a `period` row (`core/runCheckpoint.ts` ·
* `ledgerRowIsWellFormed`) — the one owner of the row's shape, so the door
* refuses exactly what this module never files.
*/
function periodRowIsWellFormed(row) {
return (row.kind === 'period' &&
typeof row.turn === 'number' &&
typeof row.toolCallId === 'string' &&
typeof row.toolName === 'string' &&
typeof row.iteration === 'number' &&
typeof row.verdict === 'string' &&
exports.PERIOD_ROW_VERDICTS.includes(row.verdict) &&
(row.argument === undefined || (typeof row.argument === 'string' && row.argument !== '')) &&
(row.differs === undefined || (0, checkRecord_js_1.isPeriodDiffers)(row.differs)) &&
(row.shifted === undefined || (0, checkRecord_js_1.isShifted)(row.shifted)) &&
(row.beyondRetention === undefined || row.beyondRetention === true) &&
(row.partlyBeyondRetention === undefined || row.partlyBeyondRetention === true) &&
(row.held === undefined || isHeldSpan(row.held)));
}
exports.periodRowIsWellFormed = periodRowIsWellFormed;
// ─── The time layer's limits lines (step T8) ─────────────────────────────
/**
* The one limits line for a call whose `period` row carries a time check
* (time design § 10.2, step T8) — `undefined` when it carries none. Static
* words around the two ranges, rendered in the presentation zone by the time
* layer's one renderer, HANDED in bound (`time/present.ts` ·
* `bindPresentation` → `range`, to the second, the zone named — coverage never
* imports the renderer); the raw instants stay on the row.
*
* @example
* ```ts
* periodCheckLine(row, bindPresentation({ zone: 'America/Los_Angeles' })); // time/present.ts
* // 'search_logs read more than was asked — asked: 2026-10-08 00:00:00–23:59:59 America/Los_Angeles
* // (UTC-07:00); read: 2026-10-08 00:00:00–2026-10-09 08:40:00 America/Los_Angeles (UTC-07:00)'
* ```
*
* `audience` names who reads the line: the PERSON (the limits block, the
* default — their window is "your window") or the MODEL (the served time line,
* `agent/arguments/serve.ts` · `timeLimitsSentence` — the same window is "the
* person's window"). That reference differs, and a SHIFTED read: the model is
* handed the library's conclusion ({@link shiftedConclusion}) — which part of
* the window the result does not cover, in the person's zone, and that the
* answer says so — not the two ranges to compare (T8 bench: after a pause the
* two-range line did not stop "1 error in the last 30 minutes", 11/20). The
* person's line keeps its bytes.
*/
function periodCheckLine(row, presentation, audience = 'person') {
const parts = [];
const d = row.differs;
if (d !== undefined &&
audience === 'model' &&
row.shifted !== undefined &&
d.missing.length > 0 &&
d.extra.length > 0) {
parts.push(shiftedConclusion(row.toolName, d, presentation));
}
else if (d !== undefined) {
const person = d.against === 'person';
const theirs = audience === 'model' ? "the person's window" : 'your window';
const reference = person ? theirs : 'was asked';
const what = d.missing.length > 0 && d.extra.length > 0
? row.shifted !== undefined
? 'a shifted window'
: 'a different window'
: `${d.missing.length > 0 ? 'less' : 'more'} than ${reference}`;
const reads = d.read.map((r) => presentation.range(r, 'second')).join('; ');
parts.push(`${row.toolName} read ${what} — ${person ? theirs : 'asked'}: ` +
`${presentation.range(d.asked, 'second')}; read: ${reads}`);
}
else if (row.shifted !== undefined) {
parts.push(`${row.toolName}'s look-back ran after the clock moved on`);
}
if (row.beyondRetention === true) {
parts.push(`${row.toolName}: the time asked about is older than the oldest data the tool declares its source keeps`);
}
return parts.length === 0 ? undefined : parts.join('. ');
}
exports.periodCheckLine = periodCheckLine;
// LENS · late-line · request-ephemeral
// reads: one `period` row's `differs` (asked, missing, extra) and `shifted` — a look-back that ran
// after the turn's clock moved on — rendered in the person's zone by the bound renderer
// law: the library's CONCLUSION for the MODEL: the part of the window the result does not cover,
// and that the answer says so and claims nothing about it; never two ranges left to compare.
/**
* The model's line for a SHIFTED read (`period-shifted` with both `missing`
* and `extra`): the part of the asked window (or the person's) the result
* does not cover and the time it read outside it, in the presentation zone,
* then what the answer does with that — says it, and claims nothing about it
* from this tool.
*
* @example
* ```ts
* shiftedConclusion('search_logs', differs, presentation);
* // "search_logs's look-back ran after the clock moved on, so its result does not cover
* // 2026-10-09 09:30:00–09:59:59 America/Los_Angeles (UTC-07:00) of the person's window (…),
* // and covers …, outside it. So the answer says that search_logs's result does not cover
* // 2026-10-09 09:30:00–09:59:59 America/Los_Angeles (UTC-07:00), and claims nothing about that
* // time from it"
* ```
*/
function shiftedConclusion(toolName, d, presentation) {
const ranges = (rs) => rs.map((r) => presentation.range(r, 'second')).join(' and ');
const reference = d.against === 'person' ? "the person's window" : 'the window asked';
const missing = ranges(d.missing);
const asked = ranges([d.asked]);
const none = missing === asked;
const gap = none ? `any of ${reference}, ${asked}` : `${missing} of ${reference} (${asked})`;
return (`${toolName}'s look-back ran after the clock moved on, so its result does not cover ${gap}, ` +
`and covers ${ranges(d.extra)}, outside it. So the answer says that ${toolName}'s result ` +
`does not cover ${missing}, and claims nothing about that time from it`);
}
/**
* The `Clocks` lines (time design § 9.6, § 10.2, step T8): one per wall-clock
* source — a dataset whose declared axis names a zone — and, when two sources
* declare different zones, the label that they differ. Every comparison is
* made on instants; the lines only name the clocks.
*/
function clockLines(sources, differ) {
const lines = sources.map((s) => `${s.toolName}'s rows are wall times in ${s.zone} (declared) — compared as instants`);
if (differ !== undefined) {
lines.push(`the sources' clocks differ (${differ.zones.join(', ')}) — compared as instants`);
}
return lines;
}
exports.clockLines = clockLines;
//# sourceMappingURL=period.js.map