UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

187 lines • 8.38 kB
/** Pure preservation check at the dataset adapter boundary. No fact validation or row scan. */ import { ABSENCE_MARKER } from './coverage/recognize.js'; import { COVERAGE_MARKER } from './coverage/ledger.js'; import { readToolResultEnvelope } from './toolEffects.js'; import { SEMANTICS_MARKER } from './types.js'; const own = (value, key) => { const descriptor = Object.getOwnPropertyDescriptor(value, key); // Native readers also recognize inherited reserved fields. Refuse those at // this JSON boundary rather than silently treating their absence as success. if (!descriptor && key in value) throw new Error('Inherited metadata'); if (descriptor && !('value' in descriptor)) throw new Error('Accessor'); return descriptor?.value; }; const object = (value) => typeof value === 'object' && value !== null && !Array.isArray(value); const coverageKeys = ['checked', 'not_checked', 'cannot_cover', 'note']; /** Capture before calling an adapter: it might mutate the original result. * Only reserved declarations and the framework's documented wrapper spine are * inspected. Raw facts/series/rows and arbitrary nested objects are not read. */ export function snapshotProjectionSemantics(result) { const fields = new Map(); const issues = []; const visited = new Set(); let count = 0, chars = 0; const limits = (size) => { if (++count > 10_000 || (chars += size) > 65_536) throw new Error('Limit'); }; function json(value, depth = 0, ancestors = new Set()) { limits(typeof value === 'string' ? value.length : 0); if (value === null || typeof value === 'string' || typeof value === 'boolean') return value; if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value !== 'object' || depth >= 16 || ancestors.has(value)) throw new Error('Non-JSON'); const array = Array.isArray(value); const prototype = Object.getPrototypeOf(value); if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) throw new Error('Non-JSON'); ancestors.add(value); try { const keys = Reflect.ownKeys(value); if (keys.length > 10_000) throw new Error('Limit'); if (array) { const length = own(value, 'length'); if (keys.length !== length + 1) throw new Error('Sparse or extra array fields'); return Array.from({ length }, (_, index) => json(own(value, String(index)), depth + 1, ancestors)); } const copy = Object.create(null); for (const key of keys.sort((a, b) => typeof a === 'string' && typeof b === 'string' ? (a < b ? -1 : a > b ? 1 : 0) : 0)) { if (typeof key !== 'string') throw new Error('Symbol'); limits(key.length); const descriptor = Object.getOwnPropertyDescriptor(value, key); if (!descriptor.enumerable) throw new Error('Non-JSON'); copy[key] = json(own(value, key), depth + 1, ancestors); } return copy; } finally { ancestors.delete(value); } } const unreadable = (field) => issues.push({ code: 'projection-declaration-unreadable', field, message: `Dataset projection cannot preserve declaration '${field}': metadata is unreadable, non-JSON or exceeds the preservation limits.`, }); function capture(value, key, path) { const field = `${path}.${key}`; try { const original = own(value, key); if (original !== undefined) fields.set(field, JSON.stringify(json(original))); } catch { unreadable(field); } } function walk(value, path, depth) { if (!object(value)) return; if (visited.has(value) || depth >= 16) { unreadable(path); return; } visited.add(value); try { // Match the existing effect-envelope recognizer using detached metadata; // never hand it the producer's getters or walk its content as JSON. if ('content' in value && Array.isArray(own(value, 'effects'))) { own(value, 'content'); const keys = Reflect.ownKeys(value); if (keys.length > 10_000) throw new Error('Limit'); // Native recognition uses enumerable string keys; bookkeeping symbols // and non-enumerable fields must not hide an otherwise valid envelope. const envelopeKeys = keys.every((key) => typeof key !== 'string' || !Object.getOwnPropertyDescriptor(value, key)?.enumerable || key === 'content' || key === 'effects' || key === 'status'); if (envelopeKeys) { const envelope = readToolResultEnvelope({ content: undefined, effects: json(own(value, 'effects')), ...(own(value, 'status') !== undefined && { status: own(value, 'status') }), }); if (envelope) { capture(value, 'status', `${path}.envelope`); capture(value, 'effects', `${path}.envelope`); walk(own(value, 'content'), `${path}.content`, depth + 1); return; } } } if (own(value, ABSENCE_MARKER) === true) { const checked = own(value, 'checked'); if (Array.isArray(checked) && checked.length > 0) { for (const key of [ ABSENCE_MARKER, 'outcome', 'looked_for', ...coverageKeys, 'retry_returns_the_same', 'try_instead', 'try_instead_tool', ]) capture(value, key, path); return; } } if (own(value, SEMANTICS_MARKER) === true) { // This protects declarations, not semanticIssues' separate schema/data // checks. A declared marker never licenses silently dropping metadata. for (const key of [ SEMANTICS_MARKER, 'grain', 'provenance', 'coverage', 'not_covered', 'clarify', 'note', ]) capture(value, key, path); return; } const ledger = own(value, COVERAGE_MARKER); if (object(ledger) && 'result' in value) { fields.set(`${path}.${COVERAGE_MARKER}`, 'declared'); for (const key of coverageKeys) capture(ledger, key, `${path}.${COVERAGE_MARKER}`); walk(own(value, 'result'), `${path}.result`, depth + 1); } } catch { unreadable(path); } } walk(result, 'result', 0); return { fields, issues }; } /** Exact preservation of original fields; unrelated new declarations may be added. */ export function projectionSemanticsIssues(before, after) { if (before.issues.length) return before.issues; if (before.fields.size === 0) return []; const projected = snapshotProjectionSemantics(after); if (projected.issues.length) return projected.issues; const issues = []; for (const [field, value] of before.fields) if (projected.fields.get(field) !== value) issues.push({ code: 'projection-declaration-changed', field, message: `Dataset projection removed or changed declaration '${field}'. Preserve the original absence, coverage and semantic metadata while replacing its data with references.`, }); return issues; } //# sourceMappingURL=projection.js.map