agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
204 lines • 9.34 kB
JavaScript
/** Closed restored-data schemas; shared JSON/coverage/period owners do validation. */
import { copyJson, ownData } from '../../lib/boundedJson.js';
import { normalizeCoverageList } from '../../semantics/coverage/items.js';
import { periodProblem } from '../../semantics/coverage/period.js';
import { BINDING_LIMITS, boundedText, boundedResultId, scopeDeclarationOf } from './scope.js';
import { resultIdentityOf } from './capture.js';
import { BINDING_REASONS } from './types.js';
// Only this owner's detached, deeply frozen outputs carry this ephemeral proof.
// A frozen input, JSON restore or proxy of an output is NOT an admitted identity.
const admittedSnapshots = new WeakMap();
const encoder = new TextEncoder();
const jsonBytes = (value) => encoder.encode(JSON.stringify(value)).byteLength;
function rememberSnapshot(snapshot, bytes = jsonBytes(snapshot)) {
admittedSnapshots.set(snapshot, Object.freeze({ bytes, emptyBytes: jsonBytes({ ...snapshot, results: [] }) }));
return snapshot;
}
export function bindingRuleOf(raw) {
const rule = ownData(raw, ['id', 'version', 'toolName', 'scopeDeclaration', 'fact', 'coverage']);
const fact = ownData(rule.fact, ['entity', 'field']);
if (!boundedText(rule.id) ||
!boundedText(rule.version) ||
!boundedText(rule.toolName) ||
!boundedText(fact.field) ||
(Object.hasOwn(rule, 'coverage') && typeof rule.coverage !== 'function'))
throw new Error('Invalid claim binding rule.');
const scopeDeclaration = scopeDeclarationOf(rule.scopeDeclaration);
let entity;
if (typeof fact.entity === 'string') {
if (!boundedText(fact.entity))
throw new Error('Invalid fact entity.');
entity = fact.entity;
}
else {
const selector = ownData(fact.entity, ['scopeDimension']);
if (!boundedText(selector.scopeDimension) ||
!scopeDeclaration.dimensions.some((dimension) => dimension.name === selector.scopeDimension))
throw new Error('Invalid entity scope selector.');
entity = Object.freeze({ scopeDimension: selector.scopeDimension });
}
return Object.freeze({
id: rule.id,
version: rule.version,
toolName: rule.toolName,
scopeDeclaration,
fact: Object.freeze({ entity, field: fact.field }),
...(typeof rule.coverage === 'function' && {
coverage: rule.coverage,
}),
});
}
/** Closed coverage admission shared by receipts and saved verification rows. */
export function validateCoverage(raw) {
const coverage = ownData(raw, ['checked', 'notChecked', 'cannotCover']);
for (const section of ['checked', 'notChecked', 'cannotCover']) {
const list = coverage[section];
if (!Array.isArray(list) || list.length > BINDING_LIMITS.results)
throw new Error('Invalid coverage list.');
for (const rawItem of list) {
const item = ownData(rawItem, ['what', 'why', 'short', 'kind']);
if (!boundedText(item.what) || (Object.hasOwn(item, 'why') && !boundedText(item.why)))
throw new Error('Invalid coverage item.');
}
// Ask the one existing rule owner, but never repair the saved shape.
normalizeCoverageList(section, list, section === 'cannotCover');
}
}
function resultOf(raw) {
raw = copyJson(raw, BINDING_LIMITS.resultBytes, true);
const result = ownData(raw, [
'identity',
'id',
'toolName',
'toolCallId',
'iteration',
'scopeDeclaration',
'scope',
'periodDimension',
'unavailable',
'facts',
'coverage',
'period',
]);
const identity = ownData(result.identity, ['namespace', 'runId', 'ordinal']);
const declaration = ownData(result.scopeDeclaration, ['id', 'version']);
if (!boundedText(identity.namespace) ||
!boundedText(identity.runId) ||
typeof identity.ordinal !== 'number' ||
!Number.isSafeInteger(identity.ordinal) ||
identity.ordinal < 1 ||
!boundedResultId(result.id) ||
result.id !==
resultIdentityOf({
namespace: identity.namespace,
runId: identity.runId,
ordinal: identity.ordinal,
}) ||
!boundedText(result.toolName) ||
!boundedText(result.toolCallId) ||
typeof result.iteration !== 'number' ||
!Number.isSafeInteger(result.iteration) ||
result.iteration < 0 ||
!boundedText(declaration.id) ||
!boundedText(declaration.version))
throw new Error('Invalid result receipt identity.');
if (Object.hasOwn(result, 'unavailable')) {
if (typeof result.unavailable !== 'string' ||
!BINDING_REASONS.includes(result.unavailable) ||
['scope', 'periodDimension', 'facts', 'coverage', 'period'].some((key) => Object.hasOwn(result, key)))
throw new Error('Invalid unavailable receipt.');
}
else {
const scope = ownData(copyJson(result.scope, BINDING_LIMITS.scopeBytes, true));
if (Object.keys(scope).length === 0 ||
Object.keys(scope).length > BINDING_LIMITS.dimensions ||
!Object.keys(scope).every(boundedText) ||
!Array.isArray(result.facts) ||
result.facts.length > BINDING_LIMITS.results)
throw new Error('Invalid receipt scope or facts.');
if (Object.hasOwn(result, 'periodDimension') && !boundedText(result.periodDimension))
throw new Error('Invalid period dimension.');
for (const rawFact of result.facts) {
const fact = ownData(rawFact, [
'entity',
'field',
'value',
'toolName',
'toolCallId',
'iteration',
'measuredAt',
]);
if (!boundedText(fact.entity) ||
!boundedText(fact.field) ||
!Object.hasOwn(fact, 'value') ||
fact.toolName !== result.toolName ||
fact.toolCallId !== result.toolCallId ||
fact.iteration !== result.iteration ||
(Object.hasOwn(fact, 'measuredAt') &&
!boundedText(fact.measuredAt) &&
typeof fact.measuredAt !== 'number'))
throw new Error('Invalid fact receipt.');
}
if (Object.hasOwn(result, 'coverage'))
validateCoverage(result.coverage);
if (Object.hasOwn(result, 'period') && periodProblem(result.period, 'camel') !== undefined)
throw new Error('Invalid period receipt.');
}
return raw;
}
/** Validate a detached saved snapshot without evaluating or reconstructing evidence. */
export function readEvidenceSnapshot(raw) {
if (raw !== null && typeof raw === 'object' && admittedSnapshots.has(raw))
return raw;
const source = ownData(copyJson(raw, BINDING_LIMITS.snapshotBytes, true), [
'version',
'namespace',
'runIds',
'results',
]);
if (source.version !== 1 ||
!boundedText(source.namespace) ||
!Array.isArray(source.runIds) ||
source.runIds.length > BINDING_LIMITS.results ||
!source.runIds.every(boundedText) ||
new Set(source.runIds).size !== source.runIds.length ||
!Array.isArray(source.results) ||
source.results.length > BINDING_LIMITS.results)
throw new Error('Invalid evidence snapshot.');
for (const result of source.results)
resultOf(result);
return rememberSnapshot(Object.freeze(source));
}
/**
* Append a singleton admitted at the SAME snapshot root depth. Reuse only
* owner-proven immutable history; unknown inputs still pass the strict reader.
* The carrier owner separately checks reservation/lineage/ordinal coherence.
*/
export function appendEvidenceSnapshot(snapshot, singleton) {
const previous = readEvidenceSnapshot(snapshot);
const addition = readEvidenceSnapshot(singleton);
if (addition.results.length !== 1 ||
previous.namespace !== addition.namespace ||
previous.runIds.length !== addition.runIds.length ||
previous.runIds.some((runId, index) => runId !== addition.runIds[index]))
throw new Error('Evidence append requires the same snapshot header and one receipt.');
if (previous.results.length >= BINDING_LIMITS.results)
throw new Error('Invalid evidence snapshot.');
const priorSize = admittedSnapshots.get(previous);
const additionSize = admittedSnapshots.get(addition);
if (priorSize === undefined || additionSize === undefined)
throw new Error('Missing internal evidence admission proof.');
// Exact UTF-8 JSON bytes, including escaping and the new separating comma.
// No old receipt is re-serialized, re-validated or deep-copied here.
const bytes = priorSize.bytes +
additionSize.bytes -
additionSize.emptyBytes +
(previous.results.length ? 1 : 0);
if (bytes > BINDING_LIMITS.snapshotBytes)
throw new Error('JSON byte limit.');
return rememberSnapshot(Object.freeze({
...previous,
results: Object.freeze([...previous.results, addition.results[0]]),
}), bytes);
}
//# sourceMappingURL=admit.js.map