agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
147 lines • 6.97 kB
JavaScript
;
/**
* prose — the prose door (design § 5.2): negative phrases anchored in a dispatched read's
* declared words, sent values or resolved names are resolved and NOTED against the record.
* An anchor permits a finding, never binds an entity or establishes a claim. Observe-only:
* a note never withholds, whatever the validation mode; a claim may carry no note.
*
* Pattern: detect (`detect.ts`) → data anchor (`anchor.ts`) → resolve (`resolve.ts`) → containment notes
* (`containment.ts`), one row per claim and one per unit the grammar could not judge
* (`in-code-block`, `language-unsupported`), in text order. A record that may be missing
* a call, or keeps one out by redaction, notes every claim `record-incomplete` and
* nothing else: nothing can be said around a hole. More than MAX_NEGATIVE_CLAIMS
* returns one `too-many-claims` note for the whole text, before reading the record.
* Role: integrity/ layer (L3). The agent's negativeClaims arm observes every original
* model string within a whole-answer budget and files these rows; no prose enforcement.
*
* Rows carry spans into the text, phrase ids, the pack version, note codes and result
* identities — never a word of the text or a value of the record.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.judgeProseNegatives = void 0;
const containment_js_1 = require("./containment.js");
const anchor_js_1 = require("./anchor.js");
const noRead_js_1 = require("./noRead.js");
const readings_js_1 = require("./readings.js");
const detect_js_1 = require("./detect.js");
const types_js_1 = require("./types.js");
const limits_js_1 = require("./limits.js");
const phrases_js_1 = require("./phrases.js");
const resolve_js_1 = require("./resolve.js");
const work_js_1 = require("./work.js");
const ordered = (notes) => types_js_1.PROSE_NOTES.filter((note) => notes.includes(note));
function claimRow(field, claim, notes, results, extra = {}) {
return {
kind: 'negative-claim',
field,
claim: claim.kind,
span: claim.span,
phraseId: claim.phraseId,
packVersion: claim.packVersion,
notes: ordered(notes),
results: [...new Set(results.map((call) => call.resultId))],
...(extra.candidates === undefined || extra.candidates.length === 0
? {}
: { candidates: extra.candidates }),
...(extra.staleSince === undefined ? {} : { staleSince: extra.staleSince }),
};
}
/** Judge one claim against the record. */
function judgeClaim(input, claim, peers) {
const { field, text, record } = input;
if (!record.ledgerComplete || peers.some((call) => call.keptOut === true)) {
return claimRow(field, claim, ['record-incomplete'], []);
}
const resolution = (0, resolve_js_1.resolveProseClaim)(claim, text, record, input.personMessage, input.work);
if (resolution.status === 'entity-unresolved') {
return claimRow(field, claim, ['entity-unresolved'], [], { candidates: resolution.candidates });
}
const words = (0, containment_js_1.claimWordsOf)(claim, text, record.clock, input.work);
const notes = [];
const results = [];
let staleSince;
for (const group of resolution.groups) {
results.push(...group.calls);
if (group.status === 'stale') {
notes.push('stale-turn');
staleSince = group.staleSince;
}
else if (group.status === 'subject-unmatched') {
notes.push('subject-unmatched');
}
else {
notes.push(...(0, containment_js_1.entityNotes)(group.calls, words, record, peers));
}
}
const inOrder = record.calls.filter((call) => results.includes(call));
return claimRow(field, claim, notes, inOrder, staleSince === undefined ? {} : { staleSince });
}
/** Note each claim, or one whole-text overflow note without judging any claim. */
function judgeProseNegatives(input) {
const { field, text, record } = input;
const detection = (0, detect_js_1.detectNegativeClaims)(text, input.phrases);
if (detection.claims.length > limits_js_1.MAX_NEGATIVE_CLAIMS) {
return [
{
kind: 'negative-claim-note',
field,
code: 'too-many-claims',
span: { start: 0, end: text.length },
packVersion: input.phrases?.version ?? phrases_js_1.NEGATIVE_CLAIM_PHRASES_VERSION,
},
];
}
// Detection and its cap precede every record read. A phrase alone is not
// a data claim; anchoring only permits a note, never resolves its entity.
const work = input.work ?? (0, work_js_1.createProseWorkBudget)();
try {
work.charge(0);
const anchored = detection.claims.length === 0 ? undefined : (0, anchor_js_1.proseAnchorOf)(record, work);
const peers = record.calls.filter((call) => (0, resolve_js_1.isCurrent)(call, record));
// This describes the native record, never an assertion about external work.
// A failed or hidden dispatched read is still a read attempt; an earlier
// turn, a proposal or a write cannot stand in for one on this turn.
const noRead = detection.claims.some(noRead_js_1.eligibleNoReadClaim) &&
!peers.some((call) => call.dispatched && (0, readings_js_1.isRead)(call));
const rows = [
...(noRead
? [
{
kind: 'negative-claim-note',
field,
code: 'no-read',
span: { start: 0, end: text.length },
packVersion: input.phrases?.version ?? phrases_js_1.NEGATIVE_CLAIM_PHRASES_VERSION,
},
]
: []),
...detection.notes.map((note) => ({
kind: 'negative-claim-note',
field,
code: note.code,
span: note.span,
packVersion: note.packVersion,
...(note.phraseId === undefined ? {} : { phraseId: note.phraseId }),
})),
...detection.claims
.filter((claim) => anchored?.(claim, text) === true)
.map((claim) => judgeClaim({ ...input, work }, claim, peers)),
];
return rows.sort((a, b) => a.span.start - b.span.start);
}
catch (error) {
if (!(error instanceof work_js_1.ProseWorkLimit))
throw error;
return [
{
kind: 'negative-claim-note',
field,
code: 'input-limit',
span: { start: 0, end: text.length },
packVersion: input.phrases?.version ?? phrases_js_1.NEGATIVE_CLAIM_PHRASES_VERSION,
},
];
}
}
exports.judgeProseNegatives = judgeProseNegatives;
//# sourceMappingURL=prose.js.map