agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
172 lines • 9.33 kB
TypeScript
/**
* skillExamples — the EXAMPLES side of a start rule: the phrasings a rule says
* it claims, and the three things a build-time check-up can then PROVE about
* them by RUNNING the compiled matchers instead of comparing them.
*
* ## Why this exists (two field failures, one evening)
*
* `compareMatchers` (skillMatch.ts) answers only what matcher-vs-matcher
* analysis can prove, and says so out loud: two DIFFERENT regex sources return
* `undefined` ("regex intersection is not decided here — only identity is
* provable"). That honesty left two real production failures unreportable:
*
* A. SHADOWING BY DIFFERENT REGEXES — an earlier rule matching one product
* name sat above an inventory rule whose matcher is a longer alternation.
* A real phrase ("what's running on <host-looking-name>") was claimed by
* the earlier rule, so the later rule could never win on its own
* phrasings. Two different regexes: the check-up was correctly silent.
* B. ABSENCE — a second phrase matched NO rule at all, fell through to the
* model tier, and the model picked the wrong skill because an array name
* looked like a hostname. Not shadowing: absence. No amount of
* matcher-vs-matcher analysis can ever catch it.
*
* A declared example turns both into arithmetic. Run the compiled predicates
* over one concrete phrase, in declaration order, exactly as the cold start
* does — and the answer is a WITNESS, not a theory: this phrase, this rule,
* this winner. No regex intersection is decided anywhere in this file.
*
* ## The tier difference (read this before writing `examples` anywhere)
*
* Two different lists spell the same author-facing sentence — "the phrasings
* this rule claims" — and they have DIFFERENT RUNTIME ROLES:
*
* • TIER 2, `match: { intent, examples }` — SCORING material. The classifier
* reads those examples AT RUN TIME to judge every new message
* (`skillIntent.ts`); they are part of how the graph routes.
* • TIER 1, a rule-level `examples: [...]` (this file) — TEST material. Read
* at BUILD time by the check-up and fed to nothing, ever. They do not
* widen, soften or otherwise touch matching: a rule with examples routes
* byte-identically to the same rule without them.
*
* One rule may not carry both lists (a teaching refusal names the difference):
* two example lists with two different jobs under one rule is exactly the
* confusion this note exists to prevent.
*
* ## The two start laws, and why one of them is only a WARNING
*
* Which rule claims a phrase is decided by one of TWO laws, and the graph does
* not know which one it will be mounted under:
*
* • the declaration-order COLD WALK (`makeResolveCursor`, the default mount)
* returns at the first entry with no condition or the first whose condition
* passes — so an UNCONDITIONAL entry claims every message from its position
* onward;
* • the turn-start CASCADE's tier 1 (`firstRuleMatch`, skillIntent.ts) reads
* the CONDITIONAL non-intent entries only — an unconditional entry is a
* default, not a rule, and never wins the turn there. That cascade is
* mounted by `.classify()` AND by `.skillGraph(g, { continuity:
* 'conversation' })`, and continuity is an AGENT-MOUNT option that does not
* exist yet when this check runs.
*
* The two laws differ in exactly one place: whether an unconditional entry
* claims. So when the earlier claimant is unconditional, the check-up cannot
* say which law will apply — it reports BOTH readings as a WARNING
* (`example-shadowed-by-default`) instead of asserting one as an ERROR. A
* check-up that disagrees with the router is worse than no check-up: the router
* really does start that turn on the later rule under a `continuity:
* 'conversation'` mount. The ERROR (`example-shadowed-by-earlier`) is kept for
* what both laws agree on.
*
* ## The context a rule is judged in, and what that makes provable
*
* Every predicate here runs on ONE context: {@link coldContext} — iteration 1,
* the phrase as `userMessage`, empty `history`, no cursor. That is a real
* context (the first iteration of a turn), but it is not the only one a start
* rule ever sees: turn 2 of a conversation also starts cold in cursor terms
* while carrying HISTORY. So:
*
* • a DATA matcher (`match:`) reads `userMessage` and nothing else, so a
* no-match here is a no-match under every context — ERROR;
* • an opaque `when` may legitimately be gated on conversation state
* (`ctx.history.length > 0`) and claim the phrase on a later turn — the
* check cannot run that turn, so a no-match is a WARNING, and the message
* names the context it judged under rather than leaving the author to guess.
*
* A THROW stays an ERROR either way: a predicate must be pure and total, and
* the context it threw on is one every turn 1 really hands it.
*
* ## The boundary, and why it is on the report itself
*
* These checks prove things about the phrases the author DECLARED and nothing
* about phrases nobody wrote. No warning is NOT proof of coverage. That
* sentence ships as {@link EXAMPLES_BOUNDARY} on `GraphCheckup.notes` —
* visible wherever the graph reports its problems — because a reader who meets
* it only in prose docs will meet a clean report first.
*
* ## The opposite assertion lives next door
*
* Everything here is POSITIVE: this rule claims this phrase. The negative form
* — a phrase that must claim NO skill, which is the assertion that catches
* over-triggering — is `skillNeverRoutes.ts`, declared on the GRAPH rather than
* on a rule (a phrase that must route nowhere belongs to no skill). Both ask
* `startRuleClaim.ts` the same question and differ only in which answer is a
* defect; a phrase declared BOTH ways is reported there as the contradiction it
* is.
*
* Composed into `graph.checkup()` by `skillGraph.ts`, exactly like
* `skillContract`/`skillIntent`/`skillVocabulary`: this module owns the rule
* AND its boundaries, and reports in the shared `GraphProblem` voice.
*/
import { type StartRuleDecl } from './startRuleClaim.js';
import type { GraphProblem } from './skillGraphCheckup.js';
/**
* The statement the check-up makes about its own reach, carried on
* `GraphCheckup.notes` whenever any rule declared examples. Same voice as the
* ingress ticket's "absence of a refusal is not consent": a clean report is
* evidence about the phrases you wrote, and about nothing else.
*/
export declare const EXAMPLES_BOUNDARY: string;
/** The note added when declaration order does not decide the turn start, so
* only the order-independent check (self-match) could run. */
export declare const EXAMPLES_ORDER_NOT_CHECKED: string;
/**
* The slice of an entry declaration this module reads — the shared claim shape
* ({@link StartRuleDecl}, which owns "who claims a phrase") plus the one field
* only this check reads. Structural, so `skillGraph.ts`'s module-private
* `EntryDecl` satisfies it unchanged (the same trick `skillIntent.ts` uses).
*/
export interface ExampleRuleDecl extends StartRuleDecl {
/** The phrasings this rule claims (build-time TEST material). */
readonly examples?: readonly string[];
}
/** What {@link checkStartRuleExamples} found, plus what it wants to say about
* its own reach. Both empty when no rule declared examples. */
export interface ExamplesCheckup {
readonly problems: readonly GraphProblem[];
readonly notes: readonly string[];
}
/**
* Validate a rule-level `examples` 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 prove, 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;
* • examples on an UNCONDITIONAL entry — it claims every message, so every
* example passes by construction and proves nothing;
* • examples beside `match: { intent, examples }` — two lists, two jobs (see
* the tier note in this file's header).
*
* Returns a frozen copy (or `undefined` when nothing was declared), so the
* stored list cannot drift from the validated one.
*/
export declare function validateStartRuleExamples(examples: unknown, where: string, rule: {
readonly hasCondition: boolean;
readonly isIntent: boolean;
}): readonly string[] | undefined;
/**
* Run the three example properties. Pure, and byte-identically silent when no
* rule declared examples (one `Array.some` and out).
*
* `orderDecides` mirrors the gate the pairwise rule checks already use
* (`!exclusiveEntries || hasClassifier`): declaration order decides the turn
* start under the default form and under a classifier's tier 1, and does NOT
* under a scorer or `.entryByRead()`. Where it does not hold, only the
* order-independent property (self-match) is claimed and a note says so.
*/
export declare function checkStartRuleExamples(input: {
readonly entries: readonly ExampleRuleDecl[];
readonly orderDecides: boolean;
readonly hasClassifier: boolean;
}): ExamplesCheckup;
//# sourceMappingURL=skillExamples.d.ts.map