agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
144 lines • 6.88 kB
JavaScript
/** Exact evidence selection and eligibility, before the shared pair evaluator. */
import { copyJson, ownData } from '../../lib/boundedJson.js';
import { participates, comparableValueOf } from '../assertion/types.js';
import { periodVerdict } from '../../semantics/coverage/period.js';
import { resultIdentityOf } from './capture.js';
import { BINDING_LIMITS, boundedText, boundedResultId, queriedPeriodMatches, scopesMatch, ownConfigArray, } from './scope.js';
import { bindingRuleOf, readEvidenceSnapshot } from './admit.js';
const unverified = (reason) => ({ kind: 'unverified', reason });
const isDoubt = (value) => value === null || value === 'unknown' || value === undefined;
/**
* The model supplies only result/value/rule. Snapshot lineage, rule registry
* and intendedScope are runtime/app inputs. A bound pair is not a verdict.
* JSON-restored snapshots must originate from the caller's verified recording;
* this module validates shape and associations, not signatures or ownership.
*/
export function bindClaim(rawClaim, snapshot, rules, intendedScope) {
let claim;
try {
claim = ownData(copyJson(rawClaim, BINDING_LIMITS.scopeBytes, true), [
'result',
'value',
'rule',
]);
if (!boundedResultId(claim.result) ||
!boundedText(claim.rule) ||
!Object.hasOwn(claim, 'value'))
return unverified('invalid-claim');
}
catch {
return unverified('invalid-claim');
}
let matchingRules;
try {
matchingRules = ownConfigArray(rules, BINDING_LIMITS.results)
.map(bindingRuleOf)
.filter((rule) => rule.id === claim.rule);
}
catch {
return unverified('invalid-rule');
}
if (matchingRules.length === 0)
return unverified('unknown-rule');
if (matchingRules.length !== 1)
return unverified('invalid-rule');
const rule = matchingRules[0];
if (rule === undefined)
return unverified('invalid-rule');
try {
const saved = readEvidenceSnapshot(snapshot);
const matches = saved.results.filter((result) => result.id === claim.result);
if (matches.length === 0)
return unverified('missing-result');
if (matches.length !== 1)
return unverified('ambiguous-result');
const result = matches[0];
if (result === undefined || result.id !== resultIdentityOf(result.identity))
return unverified('invalid-evidence');
if (result.identity.namespace !== saved.namespace ||
!saved.runIds.includes(result.identity.runId))
return unverified('foreign-result');
if (result.unavailable !== undefined)
return unverified(result.unavailable);
if (result.toolName !== rule.toolName)
return unverified('invalid-rule');
if (result.scopeDeclaration.id !== rule.scopeDeclaration.id ||
result.scopeDeclaration.version !== rule.scopeDeclaration.version)
return unverified('scope-version-mismatch');
if (result.scope === undefined)
return unverified('missing-scope');
const dimensions = rule.scopeDeclaration.dimensions.map((dimension) => dimension.name);
if (!dimensions.every(boundedText) ||
new Set(dimensions).size !== dimensions.length ||
dimensions.some((name) => !Object.hasOwn(result.scope, name)) ||
Object.keys(result.scope).some((name) => !dimensions.includes(name)))
return unverified('missing-scope');
if (result.periodDimension !== rule.scopeDeclaration.periodDimension)
return unverified('scope-version-mismatch');
if (intendedScope !== undefined && !scopesMatch(result.scope, intendedScope))
return unverified('scope-mismatch');
const entity = typeof rule.fact.entity === 'string'
? rule.fact.entity
: result.scope[rule.fact.entity.scopeDimension];
if (!boundedText(entity) || !boundedText(rule.fact.field))
return unverified('invalid-rule');
if (!Array.isArray(result.facts))
return unverified('invalid-evidence');
const facts = result.facts.filter((fact) => fact.entity === entity && fact.field === rule.fact.field);
if (facts.length === 0)
return unverified('missing-fact');
if (facts.length !== 1)
return unverified('ambiguous-fact');
const fact = facts[0];
if (fact === undefined || !participates(fact.value) || !participates(claim.value))
return unverified('unasserted-value');
// Absence/doubt needs a separately authored absence rule in a later slice.
if (isDoubt(comparableValueOf(fact.value)) || isDoubt(comparableValueOf(claim.value)))
return unverified('unasserted-value');
if (result.period !== undefined) {
const verdict = periodVerdict(result.period);
if (verdict === 'unknown')
return unverified('coverage-unknown');
if (verdict !== 'covered')
return unverified('coverage-partial');
}
if (result.periodDimension !== undefined) {
if (result.period === undefined)
return unverified('coverage-undeclared');
if (!queriedPeriodMatches(result.scope[result.periodDimension], result.period))
return unverified('scope-mismatch');
}
if (rule.coverage === undefined || result.coverage === undefined)
return unverified('coverage-undeclared');
const completeness = rule.coverage({
scope: result.scope,
coverage: result.coverage,
...(result.period !== undefined && { period: result.period }),
});
if (completeness === 'partial')
return unverified('coverage-partial');
if (completeness === 'stale')
return unverified('coverage-stale');
if (completeness !== 'complete')
return unverified('coverage-unknown');
const common = {
subject: { kind: 'entity', id: entity },
predicate: rule.fact.field,
stratum: 'asserted',
};
return copyJson({
kind: 'bound',
resultId: result.id,
rule: { id: rule.id, version: rule.version },
scope: result.scope,
coverage: result.coverage,
...(result.period !== undefined && { period: result.period }),
evidence: { ...common, value: fact.value, provenance: `result ${result.id}` },
claim: { ...common, value: claim.value, provenance: `rule ${rule.id}` },
}, BINDING_LIMITS.resultBytes, true);
}
catch {
return unverified('invalid-evidence');
}
}
//# sourceMappingURL=bindEvidence.js.map