agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
78 lines • 3.89 kB
JavaScript
/**
* startRuleClaim — WHO CLAIMS A PHRASE. The one place that answers it.
*
* Two check-ups ask the same question of a graph's start rules and want
* opposite answers:
*
* • `skillExamples.ts` declares a phrase and asserts THIS rule claims it;
* • `skillNeverRoutes.ts` declares a phrase and asserts NO rule claims it.
*
* The question underneath is identical — run the compiled conditions over one
* concrete phrase, on the context a turn's first iteration really hands a start
* rule, and see who says yes. Two copies of that would drift the moment one of
* them learned something about the router the other did not, and the two would
* then disagree about the same graph. So it lives here, once, and the modules
* that report keep only their own sentences.
*
* Nothing here decides SEVERITY or writes a message — that is the reporting
* module's job, because the same "no" means different things to a positive and
* a negative assertion. This module only runs predicates and names conditions.
*
* Zone: PURE CORE (no engine, no loop). Pinned by
* `test/injection-engine/skill-graph-fence.test.ts`.
*/
import { plainMatchCaption } from './skillMatch.js';
/**
* Run one entry's condition over one phrase, on a COLD-START context — the
* context a turn's first iteration really hands a start rule: iteration 1, the
* phrase as `userMessage`, no history, no cursor, no tool results. A throw is
* reported as `'threw'` and treated as a no-match by every caller, which is
* exactly what the cursor resolver does with it at run time (so a claim made
* here stays true of the run).
*
* An entry with NO condition matches everything: that is the cold walk's law
* (`if (!e.when) return { to: e.id }`), not a shortcut.
*/
export function runsOn(entry, phrase) {
if (entry.when === undefined)
return 'match';
try {
return entry.when(coldContext(phrase)) ? 'match' : 'no-match';
}
catch {
return 'threw';
}
}
/** The one context every predicate in a phrase check is judged on. */
export function coldContext(userMessage) {
return { iteration: 1, userMessage, history: [], activatedInjectionIds: [] };
}
/** The one context every predicate here is judged on, spelled out for the
* reader of a message — a check that names its own inputs can be argued with;
* one that hides them can only be guessed at. Kept as ONE string so every
* message describes the SAME context {@link coldContext} really builds. */
export const COLD_CONTEXT_PHRASE = 'a COLD-START context (iteration 1, the phrase as `userMessage`, empty `history`, no ' +
'cursor and no activated injections)';
/** How a rule's condition is named in a message: the data matcher quoted back,
* the honest word for opaque code — or, for an entry that declares neither,
* what having no condition MEANS. Naming a `when` predicate an unconditional
* entry does not have is how a report describes a graph nobody wrote. */
export function describeCondition(entry) {
if (entry.match !== undefined)
return `its matcher \`${plainMatchCaption(entry.match)}\``;
return entry.when === undefined
? 'declares no `match` and no `when`, so it claims every message'
: 'its `when` predicate';
}
/** Build both laws' claim finders over one entry list, in declaration order. */
export function claimLaws(entries) {
const nonIntent = entries
.map((entry, index) => ({ entry, index }))
.filter(({ entry }) => entry.match?.kind !== 'intent');
const ruleClaimants = nonIntent.filter(({ entry }) => entry.when !== undefined);
return {
underColdWalk: (phrase) => nonIntent.find(({ entry }) => runsOn(entry, phrase) === 'match'),
underCascade: (phrase) => ruleClaimants.find(({ entry }) => runsOn(entry, phrase) === 'match'),
};
}
//# sourceMappingURL=startRuleClaim.js.map