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