/** * 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/lib/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): 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;