agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
132 lines • 6.89 kB
TypeScript
/**
* skillNeverRoutes — the NEGATIVE routing row: a phrase this graph must claim
* NOWHERE.
*
* ## Why the negative form is the valuable one
*
* `skillExamples.ts` pins the positive assertion — this rule claims this
* phrasing — and it cannot state the opposite. Field use says the opposite is
* the one that catches the expensive failure. An UNDER-triggering skill costs a
* turn: the deterministic layer declines, the model tier picks, and the answer
* is usually still reachable. An OVER-triggering skill costs the whole answer:
* the wrong body enters the system prompt, the wrong tools enter the tools
* slot, and everything the model then says is shaped by a skill that had no
* business in the turn. One reviewer of a production graph had written his own
* harness for exactly this, outside the library, because the library could only
* express the cheap half.
*
* ## Where a negative row lives, and why it is NOT on a skill
*
* `examples` sits on the rule it is about. A negative row cannot: the phrase it
* describes belongs to NO skill, and hanging it on one would make three things
* wrong at once —
*
* • it would read as that skill's business ("weather questions, filed under
* billing") when the point is that it is nobody's;
* • the assertion is satisfied only when EVERY rule declines, so a row read
* as one rule's property would be checked against one rule and pass while
* another rule swallowed the phrase;
* • deleting the skill would delete the assertion — exactly when a graph is
* re-partitioned, which is exactly when over-triggering is introduced.
*
* A negative row is a property of the PARTITION, so it is declared at the graph
* level (`.neverRoutes([...])` / `neverRoutes: [...]`) and judged against every
* rule in declaration order.
*
* ## What is asserted — precisely, and what it does NOT cover
*
* The claim this module proves is: **no declared start RULE claims the
* phrase.** It runs the compiled conditions on a cold-start context, exactly as
* `skillExamples.ts` does, and reports the rule that says yes.
*
* It does NOT prove "no routing at all occurs", and could not without becoming
* a different kind of check:
*
* • an INTENT rule compiles to no sync predicate — a classifier judges it at
* run time, with a scorer (and, for `llmClassifier`, a model call). Running
* that would make the check async, non-deterministic, and in the LLM case
* billable — `graph.checkupIntents()` is where scorer-dependent auditing
* already lives;
* • a scorer menu (`.entryBy()`) and `.entryByRead()` RANK descriptions;
* there is no claim to run;
* • the model tier can open any open skill by name with `read_skill`, at
* which point no declaration is deciding anything.
*
* That statement ships on the report as {@link NEVER_ROUTES_BOUNDARY}, next to
* the problems, for the same reason the examples boundary does: a reader who
* meets a boundary only in prose docs meets a clean report first.
*
* A predicate that THROWS on the cold context counts as a no-match here, and is
* not reported — because routing itself treats a throw as a no-match, so the
* phrase really does not route through that rule. Its own rule's `examples`
* report the throw as an error (that is a defect in the rule, not in the
* partition).
*
* Composed into `graph.checkup()` by `skillGraph.ts`, like every other domain
* check, and reporting in the shared `GraphProblem` voice.
*
* Zone: PURE CORE. Pinned by
* `test/injection-engine/skill-graph-fence.test.ts`.
*/
import type { GraphProblem } from './skillGraphCheckup.js';
import { type StartRuleDecl } from './startRuleClaim.js';
/**
* The statement the negative check makes about its own reach, carried on
* `GraphCheckup.notes` whenever a graph declared any negative row. Same voice
* as `EXAMPLES_BOUNDARY` (skillExamples.ts): silence here is evidence about the declared
* RULES, and about nothing downstream of them.
*/
export declare const NEVER_ROUTES_BOUNDARY: string;
/** The slice of an entry declaration this check reads: the shared claim shape,
* plus the POSITIVE phrases the same rule declared (a phrase asserted both
* ways is a contradiction the author wrote, and is reported as one). */
export interface NeverRoutesRuleDecl extends StartRuleDecl {
readonly examples?: readonly string[];
}
/** What {@link checkNeverRoutes} found, plus what it wants to say about its own
* reach. Both empty when the graph declared no negative row. */
export interface NeverRoutesCheckup {
readonly problems: readonly GraphProblem[];
readonly notes: readonly string[];
}
/** How two phrases are compared for "the same phrase": trimmed and
* case-folded, the same normalization `duplicate-intent-example` uses — an
* author who writes one row in two casings meant one row. Exported so the
* builder's accumulated-rows set is keyed the SAME way the refusal is; two
* spellings of one rule is how a duplicate slips through. */
export declare function neverRouteKey(phrase: string): string;
/**
* Validate a graph-level `neverRoutes` list at DECLARATION time, refusing every
* shape whose check-up answer could only mislead — each refusal naming the fix:
*
* • not an array / an EMPTY array — nothing to assert, and a check-up that
* passes in silence reads as coverage;
* • a non-string or blank entry — there is no phrase to run a matcher on;
* • a phrase already declared — a repeat asserts nothing the first one did
* not, and reads as if two different phrasings were meant.
*
* A single string is accepted and read as a one-row list: `.neverRoutes('what
* is the weather')` is the commonest call, and making the author type brackets
* for it buys nothing.
*
* Returns a frozen copy of the ROWS THIS CALL ADDS, so the stored list cannot
* drift from the validated one.
*/
export declare function validateNeverRoutes(phrases: unknown, where: string, alreadyDeclared: ReadonlySet<string>): readonly string[];
/**
* Run the negative rows against the declared start rules. Pure, and
* byte-identically silent when the graph declared none (one length check and
* out).
*
* Unlike the positive examples, this needs NO `orderDecides` gate: the
* assertion is "nobody claims it", which every rule has to satisfy, so the
* order they are read in cannot change the answer. That is also why it is the
* one phrase check that stays meaningful under a classifier — tier 1 is still
* read first there, and a rule that claims the phrase still takes the turn
* before any scorer runs.
*/
export declare function checkNeverRoutes(input: {
readonly entries: readonly NeverRoutesRuleDecl[];
readonly phrases: readonly string[];
}): NeverRoutesCheckup;
//# sourceMappingURL=skillNeverRoutes.d.ts.map