agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
176 lines • 8.1 kB
JavaScript
;
/**
* unsupported-claim — the answer's typed claims against the run's settled
* facts, at the CLAIM seam.
*
* Pattern: pure function over (declared contract, ledger rows, parsed
* answer); same algebra, last seam.
* Role: the check the evidence gate states it cannot do — "a FALSE
* CLAIM ASSEMBLED FROM REAL VALUES" passes token grounding. Typed
* facts (semantics envelopes) make the contradiction
* representable; the DECLARED contract (`.claims()`, the
* `argumentsFrom` precedent: the author names the edge, the
* checker joins and never infers) makes it decidable.
*
* THE FENCES, because they are the check's honesty:
* - TYPED STRATUM ONLY. The check reads the schema-validated answer
* object; prose files nothing, ever.
* - LATEST SETTLES, EARLIER QUOTES. Only the last ledger row for a fact
* asserts; earlier rows ride as `quoted` witnesses — history is
* quotation, so claiming a superseded value is a contradiction WITH the
* history on the record.
* - UNCOLLECTED IS UNREACHABLE. A declared claim whose fact no envelope
* ever carried is incomparable — never an accusation. Same for a ledger
* value that is itself an unknown/not-applicable Claim: honest absence
* propagates (`participates`).
* - DOUBT IS AN ADVISORY, NOT A DEFECT. An answer of `null` or the literal
* string 'unknown' where the ledger holds a verified value files a
* finding marked `advisory: true` and counted APART — the model
* declining to claim is a softer fact than the model claiming wrong.
* - ONE STATED LENIENCY: a numeric fact answered as its exact string
* ('30' for 30) is not a contradiction — models stringify numbers, and
* on an accusation boundary a representational difference is not a
* difference. One direction only, string-of-number toward number.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.unsupportedClaimsOf = exports.compareClaim = void 0;
const types_js_1 = require("../assertion/types.js");
const evaluate_js_1 = require("./evaluate.js");
/** Minimal dot-path read; `undefined` for any miss along the way. */
function readPath(answer, path) {
let cursor = answer;
for (const key of path.split('.')) {
if (cursor === null || typeof cursor !== 'object')
return undefined;
cursor = cursor[key];
}
return cursor;
}
/**
* Compare ONE declared claim of the parsed answer against the ledger. Pure.
* Throws exactly what the pair evaluator throws (a value JSON cannot render).
*/
function compareClaim(claim, ledger, answer, epoch) {
const claimed = readPath(answer, claim.answerField);
if (claimed === undefined)
return { kind: 'omitted' };
const rows = ledger.filter((r) => r.entity === claim.entity && r.field === claim.field);
const settledRow = rows[rows.length - 1];
if (settledRow === undefined)
return { kind: 'uncollected' };
const subject = { kind: 'entity', id: claim.entity };
const ledgerProvenance = `${settledRow.toolName} (call ${settledRow.toolCallId}, iteration ` +
`${String(settledRow.iteration)})` +
(settledRow.measuredAt !== undefined ? `, measured_at ${String(settledRow.measuredAt)}` : '');
const evidence = {
subject,
predicate: claim.field,
value: settledRow.value,
epoch,
stratum: 'asserted',
provenance: ledgerProvenance,
};
const assertion = {
subject,
predicate: claim.field,
value: claimed,
epoch,
stratum: 'asserted',
provenance: `answer field '${claim.answerField}'`,
};
return {
kind: 'compared',
evaluation: (0, evaluate_js_1.evaluateClaimPair)(evidence, assertion),
claimed,
rows,
settledRow,
subject,
ledgerProvenance,
evidence,
assertion,
};
}
exports.compareClaim = compareClaim;
/**
* Compare the parsed answer's declared claims against the ledger.
* Pure; the caller files findings and dispositions.
*/
function unsupportedClaimsOf(declared, ledger, answer, epoch) {
const findings = [];
const dispositions = [];
for (const claim of declared) {
const comparison = compareClaim(claim, ledger, answer, epoch);
if (comparison.kind === 'omitted') {
// The schema allowed the absence: the model claimed nothing here.
dispositions.push({ claim: claim.answerField, disposition: 'not-applicable' });
continue;
}
if (comparison.kind === 'uncollected') {
// Never collected — incomparable, never an accusation.
dispositions.push({ claim: claim.answerField, disposition: 'unreachable' });
continue;
}
const { evaluation, claimed, rows, settledRow, subject, ledgerProvenance, evidence, assertion, } = comparison;
if (evaluation === 'evidence-unavailable') {
dispositions.push({ claim: claim.answerField, disposition: 'unreachable' });
continue;
}
const settledValue = (0, types_js_1.comparableValueOf)(settledRow.value);
if (evaluation === 'doubt-against-value') {
// The model declined to claim a fact the run verified — an advisory.
findings.push({
kind: 'unsupported-claim',
seam: 'claim',
advisory: true,
subjects: [subject],
// Two fields of one entity are two disagreements: without the
// predicate their identities collide and the second is deduplicated
// away, leaving the event channel disagreeing with the ledger.
predicate: claim.field,
witnesses: [evidence],
epoch,
message: `The answer's '${claim.answerField}' says ${JSON.stringify(claimed)} while the run ` +
`verified ${claim.field}(${claim.entity}) = ${JSON.stringify(settledValue)} ` +
`(${ledgerProvenance}). Declining to claim a verified fact is doubt, not ` +
`contradiction — filed as an advisory, counted apart.`,
});
dispositions.push({ claim: claim.answerField, disposition: 'checked-pass' });
continue;
}
if (evaluation !== 'conflict') {
// Preserve the existing diagnostic mapping, including both-doubt and
// claim-nonparticipating. checked-pass here is NOT a support certificate.
dispositions.push({ claim: claim.answerField, disposition: 'checked-pass' });
continue;
}
const witnesses = [
evidence,
assertion,
// Earlier values ride as QUOTED history: legitimately superseded, on
// the record so a reader sees the fact's whole life, never firing.
...rows.slice(0, -1).map((r) => ({
subject,
predicate: claim.field,
value: r.value,
stratum: 'quoted',
provenance: `${r.toolName} (call ${r.toolCallId}, iteration ${String(r.iteration)})`,
})),
];
findings.push({
kind: 'unsupported-claim',
seam: 'claim',
subjects: [subject],
predicate: claim.field,
witnesses,
epoch,
message: `The answer's '${claim.answerField}' claims ${JSON.stringify(claimed)} but the run's ` +
`settled fact is ${claim.field}(${claim.entity}) = ${JSON.stringify(settledValue)} ` +
`(${ledgerProvenance}). The record supports the fact, not the claim — nothing here ` +
`rewrites the answer; it makes the disagreement attributable.`,
});
dispositions.push({ claim: claim.answerField, disposition: 'checked-fail' });
}
return { findings, dispositions };
}
exports.unsupportedClaimsOf = unsupportedClaimsOf;
//# sourceMappingURL=check.js.map