agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
122 lines • 5.58 kB
JavaScript
/** Evidence-owned scope and absence spelling. The model selects a claim; native
* reads determine whether it holds. Several reads never become a broader union. */
import { textOf } from '../../lib/normalize.js';
import { isAmongRow, sameValue, scopeOf } from './readings.js';
import { holdsName, tokensOf } from './matcher.js';
/** Only normalize the absence family, after every cited read explicitly declares
* absence. Never turn a negative into complete, or downgrade requested completeness. */
export function deriveCitationKind(requested, reads) {
if (requested === 'complete' ||
!reads.every((call) => call.outcome === 'ok' && call.emptiness === 'declared-absent'))
return requested;
const kinds = reads.map((call) => (isAmongRow(call) ? 'not-found' : 'none'));
const first = kinds[0];
return first !== undefined && kinds.every((kind) => kind === first) ? first : undefined;
}
const SUBJECT_LIMIT = 256;
const scalar = (value) => (typeof value === 'string' && value.length > 0 && value.length <= SUBJECT_LIMIT) ||
(typeof value === 'number' && Number.isFinite(value));
/** Each recorded population value remains a separate subject. An unavailable,
* all-populations or non-scalar subject cannot be validated against model prose. */
function subjectsOf(call) {
const population = (scopeOf(call) ?? []).filter((item) => item.facet === 'population');
if (population.length === 0) {
const name = isAmongRow(call) ? call.row?.lookedFor : undefined;
return scalar(name) ? [name] : undefined;
}
if (population.length > SUBJECT_LIMIT ||
population.some((item) => item.from === 'host' || item.any === true || !scalar(item.value)))
return undefined;
return population.map((item) => item.value);
}
/** Canonical public population values only; never model text or a first-value
* shortcut. The delivery adapter may expose a scalar only when this has one value. */
export function derivedSubjects(reads) {
const result = [];
for (const call of reads) {
const values = subjectsOf(call);
if (values === undefined)
return [];
for (const value of values) {
if (!result.some((known) => sameValue(known, value)))
result.push(value);
if (result.length > SUBJECT_LIMIT)
return [];
}
}
return result;
}
/** Bounded text normalization, without deleting hidden characters or accents. */
function normalized(value) {
const raw = String(value);
if (raw.length > SUBJECT_LIMIT)
return undefined;
const text = textOf(raw);
return text.length > 0 && text.length <= SUBJECT_LIMIT * 4 && !/[\p{Cc}\p{Cf}]/u.test(text)
? text
: undefined;
}
/** A supplied label must contain EVERY recorded subject. This checks the label's
* scope only; its surrounding prose and any negative wording remain unchecked. */
export function aboutContainsSubjects(about, reads) {
const phrase = normalized(about);
const values = derivedSubjects(reads);
const tokens = phrase === undefined ? [] : tokensOf(phrase);
return (phrase !== undefined &&
values.length > 0 &&
values.every((value) => {
const subject = normalized(value);
return (subject !== undefined && (sameValue(phrase, subject) || holdsName(phrase, tokens, subject)));
}));
}
function subjects(call) {
return (scopeOf(call) ?? []).filter((item) => item.facet === 'subject').map((item) => item.what);
}
/** Cross-tool peers need the app's same declared subject. Within one tool an
* undeclared subject cannot prove a reading unrelated to its sibling. */
export function sameRecordedSubject(a, b) {
const left = subjects(a);
const right = subjects(b);
if (left.length === 0 || right.length === 0)
return a.toolName === b.toolName;
return left.some((l) => right.some((r) => sameValue(l, r)));
}
function populations(call) {
const groups = new Map();
for (const item of scopeOf(call) ?? []) {
if (item.facet !== 'population' || item.argument === undefined)
continue;
groups.set(item.argument, [...(groups.get(item.argument) ?? []), item]);
}
return groups;
}
const concrete = (call, argument, items) => call.row !== undefined &&
!call.row.gaps.some((gap) => gap.kind === 'match' && gap.argument === argument) &&
items.length > 0 &&
items.every((item) => item.any !== true && item.value !== undefined);
/** Only the runtime-admitted resolution attached to the sent value supplies
* aliases. No token, substring or model-answer match establishes overlap. */
function names(item) {
return [
item.value,
item.resolved?.id,
item.resolved?.name,
...(item.resolved?.aliases ?? []),
].filter((value) => value !== undefined);
}
/** Population values in one argument are alternatives; different arguments
* are separate dimensions. A shared concrete dimension can prove disjointness.
* Missing, differently named, or `any` dimensions cannot. */
export function populationsMayOverlap(a, b) {
const left = populations(a);
const right = populations(b);
for (const [argument, l] of left) {
const r = right.get(argument);
if (r === undefined || !concrete(a, argument, l) || !concrete(b, argument, r))
continue;
if (!l.some((li) => r.some((ri) => names(li).some((lv) => names(ri).some((rv) => sameValue(lv, rv))))))
return false;
}
return true;
}
//# sourceMappingURL=derive.js.map