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
JavaScript
;
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