UNPKG

agentfootprint

Version:

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

116 lines • 5.29 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.observeNegativeProse = void 0; /** Bounded observation of original model strings, before library-written statements. * The grammar only files findings. It never establishes a claim or blocks delivery. */ const boundedJson_js_1 = require("../../../lib/boundedJson.js"); const detect_js_1 = require("../../../integrity/negative-claim/detect.js"); const prose_js_1 = require("../../../integrity/negative-claim/prose.js"); const work_js_1 = require("../../../integrity/negative-claim/work.js"); const limits_js_1 = require("../../../integrity/negative-claim/limits.js"); const phrases_js_1 = require("../../../integrity/negative-claim/phrases.js"); // Whole-answer budgets, not per-field multipliers. Length is UTF-16 code units. const MAX_PROSE_CHARS = 64 * 1024; const MAX_PROSE_FIELDS = 256; /** Scan every original free-form string, including nested strings and citation labels. * Preflight all limits before looking at any evidence. If any limit is exceeded, * one whole-answer note replaces partial findings: skipped work is not success. * The caller has already admitted a bounded JSON candidate; copy again so a * parser-owned object can never supply getters or mutable evidence to this reader. */ function observeNegativeProse(candidate, record, citationField, phrases) { let chars = 0; const note = (code) => [ { kind: 'negative-claim-note', field: '/', code, span: { start: 0, end: chars }, packVersion: phrases?.version ?? phrases_js_1.NEGATIVE_CLAIM_PHRASES_VERSION, }, ]; let data; try { if (typeof candidate === 'string' && candidate.length > MAX_PROSE_CHARS) { chars = candidate.length; return note('input-limit'); } data = (0, boundedJson_js_1.copyJson)(candidate, 1024 * 1024, true); } catch { return note('input-limit'); } const fields = []; const pending = [{ value: data, path: '' }]; while (pending.length > 0) { const entry = pending.pop(); if (entry === undefined) break; const { value, path } = entry; if (typeof value === 'string') { // These closed protocol values are typed citations, not model prose. // Their free-form about label still passes through the observer. const parts = path.split('/'); if (citationField !== undefined && parts[1] === citationField && /^\d+$/.test(parts[2] ?? '') && ((parts.length === 4 && parts[3] === 'kind' && ['none', 'not-found', 'complete'].includes(value)) || (parts.length === 5 && parts[3] === 'restsOn' && /^\d+$/.test(parts[4]) && /^r[1-9][0-9]{0,3}$/.test(value)))) continue; if (path.length > 4096) return note('input-limit'); chars += value.length; if (chars > MAX_PROSE_CHARS || fields.length === MAX_PROSE_FIELDS) return note('input-limit'); fields.push({ field: path || '/', text: value }); } else if (value !== null && typeof value === 'object') { const entries = Object.entries(value); for (let i = entries.length - 1; i >= 0; i--) { const [key, child] = entries[i]; const segment = key.replace(/~/g, '~0').replace(/\//g, '~1'); pending.push({ value: child, path: `${path}/${segment}` }); } } } let claims = 0; for (const { text } of fields) { const detection = (0, detect_js_1.detectNegativeClaims)(text, phrases); // Unread units must share the budget: thousands of fenced/code snippets // must not amplify one long field path into an unbounded findings ledger. claims += detection.claims.length + detection.notes.length; if (claims > limits_js_1.MAX_NEGATIVE_CLAIMS) return note('too-many-claims'); } const work = (0, work_js_1.createProseWorkBudget)(); const rows = []; let notedNoRead = false; for (const { field, text } of fields) { const found = (0, prose_js_1.judgeProseNegatives)({ field, text, record, phrases, work }); if (found.some((row) => row.kind === 'negative-claim-note' && row.code === 'input-limit')) { return note('input-limit'); } for (const row of found) { if (row.kind === 'negative-claim-note' && row.code === 'no-read') { if (notedNoRead) continue; notedNoRead = true; rows.push(...note('no-read')); } else rows.push(row); } // The extra answer-level note shares the observation cap. No partial // findings survive any limit, including a stale claim plus this note. if (rows.length > limits_js_1.MAX_NEGATIVE_CLAIMS) return note('too-many-claims'); } return rows; } exports.observeNegativeProse = observeNegativeProse; //# sourceMappingURL=prose.js.map