UNPKG

agentfootprint

Version:

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

261 lines • 11.8 kB
"use strict"; /** * time/resolveRecord — what the resolver writes down: the candidate and * choice shapes, the app's policy (read at `.time()`), and the checks the * checkpoint door runs on a filed reading. * * Pattern: the record half of `resolve.ts`, split from the resolver so the * SYNCHRONOUS doors — `.time({ policy })` (`clock.ts` · * `readTimeOptions`) and `validateCheckpoint` (`rows.ts` · * `timeRowIsWellFormed`) — reach the shapes and their checks without * the resolver, which only an armed seed loads (`agent/stages/ * timeLayer.ts`, through `import()`: the optional-family law of * docs-next's site budget). `resolve.ts` re-exports everything here. * Role: L0 leaf (the time layer). Imports `range.ts` and `zone.ts` * (the window check) only. * Emits: N/A. * * @example * ```ts * readPolicy({ dateOrder: 'DMY' }); // { dateOrder: 'DMY', year: 'ask' } * readPolicy({ abbreviations: { PST: { zone: 'America/Los_Angeles', offset: '-08:00' } } }); * // { dateOrder: 'ask', year: 'ask', abbreviations: { PST: { zone: 'America/Los_Angeles', offset: '-08:00' } } } * candidateIsWellFormed({ kind: 'range', range: { from: 'x', to: 'y' } }); // false * ``` */ Object.defineProperty(exports, "__esModule", { value: true }); exports.choiceIsWellFormed = exports.candidateIsWellFormed = exports.PART_ORDER = exports.shownGrain = exports.widenedGrain = exports.readPolicy = exports.offsetOfToken = exports.DEFAULT_TIME_POLICY = void 0; const range_js_1 = require("./range.js"); const zone_js_1 = require("./zone.js"); /** No silent MDY, no guessed year. */ exports.DEFAULT_TIME_POLICY = Object.freeze({ dateOrder: 'ask', year: 'ask' }); // ─── The policy ────────────────────────────────────────────────────────── const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value); const DATE_ORDERS = ['ask', 'MDY', 'DMY', 'YMD']; const YEAR_RULES = ['ask', 'current']; const POLICY_KEYS = ['dateOrder', 'year', 'abbreviations']; /** `±HH`, `±HHMM`, `±HH:MM` — a colon only between hours and minutes, never trailing. */ const OFFSET_TOKEN = /^([+-])(\d{2})(?::?(\d{2}))?$/; /** * A numeric offset as written (`-07`, `-0700`, `-07:00`) → minutes east of * UTC and its `±HH:MM` spelling, or `undefined`. The one reader of an offset * token: `resolve.ts` · `zoneReadsOf` (a said offset) and {@link readPolicy} (an * abbreviation's literal offset) both ask it. */ function offsetOfToken(token) { const m = OFFSET_TOKEN.exec(token); if (m === null) return undefined; const hours = Number(m[2]); const minutes = m[3] === undefined ? 0 : Number(m[3]); if (hours > 23 || minutes > 59) return undefined; const total = (m[1] === '-' ? -1 : 1) * (hours * 60 + minutes); return { minutes: total === 0 ? 0 : total, spelled: `${m[1]}${m[2]}:${m[3] ?? '00'}` }; } exports.offsetOfToken = offsetOfToken; /** An abbreviation key: letters only, as the person writes it (`PST`, `CEST`). */ const ABBREVIATION_KEY = /^[A-Za-z]{2,8}$/; const MAX_ABBREVIATIONS = 256; function readAbbreviations(value) { const shape = "policy.abbreviations must map letters to { zone, offset } — { PST: { zone: 'America/Los_Angeles', offset: '-08:00' } }"; if (value === null || typeof value !== 'object' || Array.isArray(value)) return shape; const entries = Object.entries(value); if (entries.length === 0 || entries.length > MAX_ABBREVIATIONS) return shape; const out = {}; for (const [key, entry] of entries) { if (!ABBREVIATION_KEY.test(key) || !isRecord(entry)) return shape; if (Object.keys(entry).some((k) => k !== 'zone' && k !== 'offset')) return shape; if (!(0, zone_js_1.isZoneName)(entry.zone)) { return `policy.abbreviations.${key}.zone must be an IANA zone name such as 'America/Los_Angeles'`; } const offset = typeof entry.offset === 'string' ? offsetOfToken(entry.offset) : undefined; if (offset === undefined) { return `policy.abbreviations.${key}.offset must be a numeric offset such as '-08:00'`; } out[key] = Object.freeze({ zone: entry.zone, offset: offset.spelled }); } return Object.freeze(out); } /** * The app's `policy`, read with the v1 defaults filled in — or a problem in * words (an unknown key, a value outside its switch). `abbreviations` is kept * only when given, so a policy without it reads byte-identical to v1's. */ function readPolicy(value) { if (value === undefined) return exports.DEFAULT_TIME_POLICY; if (value === null || typeof value !== 'object' || Array.isArray(value)) { return 'policy must be an object — { dateOrder?, year?, abbreviations? }'; } const p = value; const extra = Object.keys(p).filter((k) => !POLICY_KEYS.includes(k)); if (extra.length > 0) { return `policy takes { dateOrder?, year?, abbreviations? } — unknown key ${extra .map((k) => `'${k}'`) .join(', ')}`; } if (p.dateOrder !== undefined && !DATE_ORDERS.includes(p.dateOrder)) { return "policy.dateOrder must be 'ask', 'MDY', 'DMY' or 'YMD'"; } if (p.year !== undefined && !YEAR_RULES.includes(p.year)) { return "policy.year must be 'ask' or 'current'"; } const abbreviations = p.abbreviations === undefined ? undefined : readAbbreviations(p.abbreviations); if (typeof abbreviations === 'string') return abbreviations; return Object.freeze({ dateOrder: p.dateOrder ?? 'ask', year: p.year ?? 'ask', ...(abbreviations !== undefined && { abbreviations }), }); } exports.readPolicy = readPolicy; /** * The grain a candidate's END was widened to, or `undefined` when its `to` * is exact — the one answer to "was the end widened?" (§ 3.3). A label * renders the end as said only then (`readingAsk.ts` · `timeAskOf`), and * the said end is `to − 1 grain` only then (`forms.ts` · `saidEndMs`). * `grain` alone is not the answer: "8 AM to 9 AM" is grain `hour` and ends * AT `09:00`; a look-back is grain `hour` and ends at the clock's now. * * @example * ```ts * widenedGrain({ grain: 'minute', notes: [{ kind: 'end-of-grain' }] }); // 'minute' — [08:00, 08:41) * widenedGrain({ grain: 'hour', notes: [] }); // undefined — [08:00, 09:00) * ``` */ function widenedGrain(candidate) { return candidate.notes.some((n) => n.kind === 'end-of-grain') ? candidate.grain : undefined; } exports.widenedGrain = widenedGrain; /** * The grain a candidate's LABEL shows its end at (`readingAsk.ts` · * `timeAskOf`), or `undefined` to show the range as it is. A widened end is * shown as said ({@link widenedGrain}). A look-back is the other kept-inside * end: `[now − L, now + 1 ms)` holds the clock's now, so its label ends AT now, * at the grain the person counted in — "last 40 minutes" is `8:00 – 8:40 AM`, * never `8:40:00.001`. Only the label asks this: a look-back's end was not * widened to a grain, so the said end (`forms.ts` · `saidEndMs`) keeps asking * {@link widenedGrain}. * * @example * ```ts * shownGrain({ window: { kind: 'lookback', duration: '40m' }, grain: 'minute', notes: [] }); // 'minute' * shownGrain({ window: { kind: 'range', range }, grain: 'hour', notes: [] }); // undefined — "8 AM to 9 AM" * ``` */ function shownGrain(candidate) { return candidate.window.kind === 'lookback' ? candidate.grain : widenedGrain(candidate); } exports.shownGrain = shownGrain; // ─── The record's checks ───────────────────────────────────────────────── /** The parts a candidate can name, in the order a record lists them. */ exports.PART_ORDER = [ 'year', 'month', 'day', 'hour', 'minute', 'second', 'meridiem', 'zone', 'duration', ]; const GRAINS = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year']; const ANCHORS = ['message', 'previous-window', 'none']; const OPEN = [ 'date-order', 'year', 'meridiem', 'dst', 'zone', 'abbreviation', 'parse', 'confirm', ]; const isIndex = (value, below) => typeof value === 'number' && Number.isInteger(value) && value >= 0 && value < below; const isPartList = (value) => Array.isArray(value) && value.every((p) => exports.PART_ORDER.includes(p)); function isNote(value) { if (!isRecord(value)) return false; switch (value.kind) { case 'end-of-grain': return true; case 'dst-overlap': case 'dst-gap': return ((value.which === 'earlier' || value.which === 'later') && (value.end === undefined || value.end === 'from' || value.end === 'to')); case 'offset-said': return typeof value.offset === 'string'; case 'century-implied': return typeof value.century === 'number'; case 'zone-read': return (typeof value.token === 'string' && (value.as === 'place' || value.as === 'abbreviation' || value.as === 'abbreviation-literal')); default: return false; } } function isWindow(value) { if (!isRecord(value)) return false; if (value.kind === 'range') return (0, range_js_1.isTimeRange)(value.range); return value.kind === 'lookback' && typeof value.duration === 'string'; } /** Whether `value` is a candidate this module would have written. */ function candidateIsWellFormed(value) { if (!isRecord(value)) return false; const reader = value.reader; return (isWindow(value.window) && (0, range_js_1.isTimeRange)(value.range) && (0, zone_js_1.isZoneName)(value.zone) && GRAINS.includes(value.grain) && isPartList(value.said) && isPartList(value.implied) && ANCHORS.includes(value.anchor) && isRecord(reader) && typeof reader.id === 'string' && (reader.kind === 'rule' || reader.kind === 'model') && Array.isArray(value.notes) && value.notes.every(isNote) && isRecord(value.reading) && isIndex(value.parse, 64)); } exports.candidateIsWellFormed = candidateIsWellFormed; function isPolicyUsed(value) { if (!isRecord(value)) return false; return Object.entries(value).every(([k, v]) => (k === 'dateOrder' && DATE_ORDERS.includes(v)) || (k === 'year' && YEAR_RULES.includes(v))); } /** Whether `value` is a choice this module would have written over `count` candidates. */ function choiceIsWellFormed(value, count) { if (!isRecord(value)) return false; switch (value.by) { case 'only': return isIndex(value.candidate, count); case 'policy': return isIndex(value.candidate, count) && isPolicyUsed(value.policy); case 'open': return (Array.isArray(value.remaining) && value.remaining.every((i) => isIndex(i, count)) && Array.isArray(value.open) && value.open.length > 0 && value.open.every((q) => OPEN.includes(q)) && (value.policy === undefined || isPolicyUsed(value.policy))); case 'none': return ['unreadable', 'unsupported', 'no-candidate', 'excluded-by-policy'].includes(value.why); default: return false; } } exports.choiceIsWellFormed = choiceIsWellFormed; //# sourceMappingURL=resolveRecord.js.map