agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
143 lines • 7 kB
JavaScript
/**
* Private, immutable evidence carrier for the explicit findings verifier.
* Role: reservation and bounded retention; no schema, events or Agent wiring.
* The checkpoint owner authenticates provenance and supplies the ledger's
* largest claim ordinal. Shape validation alone never authorizes a record.
*/
import { appendEvidenceSnapshot, readEvidenceSnapshot, } from '../../../integrity/claim-binding/admit.js';
import { resultIdentityOf } from '../../../integrity/claim-binding/capture.js';
import { BINDING_LIMITS, boundedText } from '../../../integrity/claim-binding/scope.js';
import { ownData } from '../../../lib/boundedJson.js';
import { claimIdentityOf } from './verificationRow.js';
const positiveOrdinal = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
/**
* Read only after the checkpoint's owner/namespace authority is established.
* The caller MUST derive largestClaimOrdinal from its validated claim rows;
* this carrier does not contain those rows and cannot infer their maximum.
* Stage commits clone changed state. At entry, admit the tracked raw value
* and publish this detached carrier before the batch; subsequent raw reads
* can reuse its immutable evidence. A live scope proxy is not that identity.
*/
export function readVerificationState(raw, largestClaimOrdinal) {
const source = ownData(raw, [
'version',
'evidence',
'nextResultOrdinal',
'nextClaimOrdinal',
'limited',
]);
if (source.version !== 1 ||
!positiveOrdinal(source.nextResultOrdinal) ||
!positiveOrdinal(source.nextClaimOrdinal) ||
!Number.isSafeInteger(largestClaimOrdinal) ||
largestClaimOrdinal < 0 ||
source.nextClaimOrdinal <= largestClaimOrdinal ||
(Object.hasOwn(source, 'limited') && source.limited !== true))
throw new Error('Invalid findings verification carrier.');
const evidence = readEvidenceSnapshot(source.evidence);
if (evidence.runIds.length === 0)
throw new Error('Verification carrier requires its recorded run lineage.');
const ordinals = new Set();
for (const result of evidence.results) {
if (result.identity.namespace !== evidence.namespace ||
!evidence.runIds.includes(result.identity.runId) ||
result.identity.ordinal >= source.nextResultOrdinal ||
ordinals.has(result.identity.ordinal))
throw new Error('Invalid verification receipt lineage or ordinal.');
ordinals.add(result.identity.ordinal);
}
return Object.freeze({
version: 1,
evidence,
nextResultOrdinal: source.nextResultOrdinal,
nextClaimOrdinal: source.nextClaimOrdinal,
...(source.limited === true && { limited: true }),
});
}
/** Called only when opted in. No randomness is consumed at module load. */
export function createVerificationState(runId, namespaceSource = () => globalThis.crypto.randomUUID()) {
const namespace = namespaceSource();
if (!boundedText(runId) || !boundedText(namespace))
throw new Error('Invalid verification recording identity.');
return readVerificationState({
version: 1,
evidence: { version: 1, namespace, runIds: [runId], results: [] },
nextResultOrdinal: 1,
nextClaimOrdinal: 1,
}, 0);
}
const limited = (state) => state.limited === true ? state : Object.freeze({ ...state, limited: true });
/** A resume keeps its namespace/counters, including when a process reuses runId. */
export function continueVerificationState(state, runId) {
if (!boundedText(runId))
throw new Error('Invalid verification run identity.');
if (state.evidence.runIds.includes(runId))
return state;
if (state.evidence.runIds.length >= BINDING_LIMITS.results)
throw new Error('Verification run lineage capacity exhausted.');
try {
const evidence = readEvidenceSnapshot({
...state.evidence,
runIds: [...state.evidence.runIds, runId],
});
return Object.freeze({ ...state, evidence });
}
catch {
// Identity cannot be reserved for an unrecorded run. The input is an
// admitted carrier; adding one bounded run can exceed only its budget.
// Refuse this continuation rather than evicting old lineage or evidence.
throw new Error('Verification run lineage capacity exhausted.');
}
}
/** Publish the returned state before awaiting execution, even if it later fails. */
export function reserveResultIdentity(state, runId) {
state = continueVerificationState(state, runId);
const ordinal = state.nextResultOrdinal;
if (!positiveOrdinal(ordinal) || ordinal === Number.MAX_SAFE_INTEGER)
throw new Error('Verification result identity capacity exhausted.');
const identity = Object.freeze({ namespace: state.evidence.namespace, runId, ordinal });
return Object.freeze({
state: Object.freeze({ ...state, nextResultOrdinal: ordinal + 1 }),
identity,
id: resultIdentityOf(identity),
});
}
/** One occurrence, not a text key. The caller publishes state before evaluation. */
export function reserveClaimIdentity(state, runId) {
state = continueVerificationState(state, runId);
const ordinal = state.nextClaimOrdinal;
if (!positiveOrdinal(ordinal) || ordinal === Number.MAX_SAFE_INTEGER)
throw new Error('Verification claim identity capacity exhausted.');
return Object.freeze({
state: Object.freeze({ ...state, nextClaimOrdinal: ordinal + 1 }),
ordinal,
id: claimIdentityOf({ namespace: state.evidence.namespace, runId, ordinal }),
});
}
/**
* Retain only a finalized, previously reserved receipt. Capacity refusal is
* sticky and never replaces an earlier result; the writer reports the limit.
*/
export function retainVerificationEvidence(state, receipt) {
if (state.limited === true)
return state;
const singleton = readEvidenceSnapshot({ ...state.evidence, results: [receipt] });
const admitted = singleton.results[0];
if (admitted.identity.namespace !== state.evidence.namespace ||
!state.evidence.runIds.includes(admitted.identity.runId) ||
admitted.identity.ordinal >= state.nextResultOrdinal ||
state.evidence.results.some((prior) => prior.identity.ordinal === admitted.identity.ordinal))
throw new Error('Verification receipt was not uniquely reserved in this recording.');
if (state.evidence.results.length >= BINDING_LIMITS.results)
return limited(state);
try {
const evidence = appendEvidenceSnapshot(state.evidence, singleton);
return Object.freeze({ ...state, evidence });
}
catch {
// Both the carrier and individual receipt were already admitted. Their
// combined bytes exceed the snapshot cap; preserve the earlier snapshot.
return limited(state);
}
}
//# sourceMappingURL=verificationState.js.map