/** * skillGuard — the DATA-guard domain for skill-graph route edges (9.51.0). * * This module completes the SkillWalker's third mover. The SkillMap is data * (`skillGraph()`/`defineSkillMap`), entry matchers are data (`match:` — * skillMatch.ts), tool-outcome route arms are data (`onToolStatus`) — the one * thing left opaque on a route edge was the general `when` predicate. A * `guard:` is its declarative twin: conditions over the hop's context and the * tool result's own fields, compiled ONCE into the predicate that routes AND * the serializable data that describes it, so the check-up can compare * guards, `toMermaid()` can caption them, `skill.graph_declared` can carry * them, and every evaluation leaves per-condition evidence on the record. * * One module owns everything a guard is and does (the skillMatch.ts law): * * • what a guard IS — {@link SkillGuard} (author form) and * {@link SkillGuardData} (serializable description); * • how it RUNS — {@link compileGuard}: ONE compilation returns the * predicate, the data, and the evidence evaluator; * • what it PROVES WRONG — {@link guardUnsatisfiable}: the contradictions * the check-up can honestly flag; * • how it DRAWS — {@link plainGuardCaption} / {@link mermaidGuardCaption}. * * ## The operator grammar deliberately MIRRORS footprintjs's WhereFilter * * `eq / ne / gt / gte / lt / lte / in / notIn`, every condition ANDed, with * per-condition evidence — the exact vocabulary and evidence shape of * footprintjs's `evaluateFilter` (footprintjs `src/lib/decide/evaluator.ts`). * It is a door-local TWIN, not an import: this module sits behind the * skill-graph door's no-footprintjs fence * (test/lib/injection-engine/skill-graph-fence.test.ts), so the tiny operator * set is mirrored here and a future shared extraction is mechanical. Two * deliberate divergences, both because guards are compiled at BUILD time * while footprintjs filters arrive at run time: a malformed guard is REFUSED * by name at the keystroke (footprintjs dev-warns and fails the condition), * and there is no redaction hook (this layer has no redaction registry — the * summarized `actualSummary` is the bounded record, never the raw bytes). * * ## What a guard can read (the key rule) * * A guard is judged per tool result of the previous iteration's batch — the * same evidence every route edge fires on. Six HOP KEYS read the hop context * directly: * * `toolName`, `result`, `status` — the judged tool result (`status` only * when the tool's envelope declared one); * `iteration`, `userMessage`, `currentSkillId` — the iteration context. * * Any OTHER key reads the top-level field of that name from the RESULT * parsed as JSON — the shape structured tool results already have. A result * that is not a JSON object yields `undefined` for such keys, so the * condition fails and the evidence says so. NOTE the same caveat that rides * `InjectionContext.lastToolResult`: `result` is the string the MODEL read, * which artifact placement can replace with a claim ticket — a guard over * result fields judges what the model was told. Guard on `toolName` / * `status` when you want a condition placement cannot move. * * Engine-type-free by design (the view is structural), imports only the * closed status vocabulary from its own door — `skillGraphCheckup.ts` can * import it and keep its "pure over strings" law intact. */ /** A guard threshold value — plain data only, because a guard IS data: it * rides `SkillEdge.guard`, `skill.graph_declared` and every recording, so it * must survive `structuredClone` and read back as what was declared. */ export type GuardValue = string | number | boolean | null; /** The guard operators — deliberately the footprintjs WhereFilter set. */ export type GuardOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn'; /** * The operators one guarded key may declare. All declared operators must * pass (AND), exactly as in footprintjs's `FilterOps`. * * • `eq` / `ne` — strict equality / inequality; * • `gt`/`gte`/`lt`/`lte` — ordered comparison (numbers, or strings * lexicographically — `iteration: { gte: 3 }`, `riskLevel: { gte: 'high' }`); * • `in` / `notIn` — membership in a non-empty list (≤ 1000 entries, the * footprintjs bound). */ export interface SkillGuardOps { readonly eq?: GuardValue; readonly ne?: GuardValue; readonly gt?: string | number; readonly gte?: string | number; readonly lt?: string | number; readonly lte?: string | number; readonly in?: readonly GuardValue[]; readonly notIn?: readonly GuardValue[]; } /** * The AUTHOR form of a route-edge guard (`SkillRouteOptions.guard`): keys to * operator sets, every condition ANDed. Keys resolve per the module-header * rule — six hop keys read the hop directly, any other key reads the result's * top-level JSON field. * * @example * .route(triage, escalation, { guard: { riskLevel: { gte: 'high' } } }) * .route(triage, billing, { * onToolReturn: 'lookup_order', * guard: { status: { ne: 'denied' }, iteration: { lte: 5 } }, * }) */ export type SkillGuard = { readonly [key: string]: SkillGuardOps; }; /** One compiled guard condition, as serializable data. */ export interface GuardConditionData { readonly key: string; readonly op: GuardOperator; readonly value: GuardValue | readonly GuardValue[]; } /** * The serializable description of a route-edge guard — what the check-up * compares, what `toMermaid()` captions, what `SkillEdge.guard` and the * `skill.graph_declared` payload carry. Pure data (survives * `structuredClone`); conditions in declaration order, all ANDed. */ export interface SkillGuardData { readonly conditions: readonly GuardConditionData[]; } /** The hop view a compiled guard reads. Structural on purpose — the cursor * resolver builds it from `InjectionContext` + one tool result, and this * module never has to import either. */ export interface GuardHopView { readonly toolName: string; readonly result: string; readonly status?: string; readonly iteration: number; readonly userMessage: string; readonly currentSkillId?: string; } /** * One condition's evaluation, for the record — the footprintjs * `FilterCondition` shape, door-local: which condition, what it was judged * against (bounded summary, never the raw bytes), and whether it passed. */ export interface GuardConditionEvidence { readonly key: string; readonly op: GuardOperator; readonly value: GuardValue | readonly GuardValue[]; /** The judged value, summarized to ≤ 80 chars — evidence, not a transcript. */ readonly actualSummary: string; readonly passed: boolean; } /** A full guard evaluation: the verdict plus every condition's evidence. */ export interface GuardVerdict { readonly verdict: boolean; readonly conditions: readonly GuardConditionEvidence[]; } /** * ONE compilation's three faces (the `compileMatch` pattern): the predicate * that routes, the data that describes it, and the evidence evaluator the * record quotes — all from the same conditions, so they can never describe * different guards. */ export interface CompiledGuard { /** Cheap boolean — short-circuits on the first failing condition. */ readonly predicate: (view: GuardHopView) => boolean; /** The serializable description. */ readonly data: SkillGuardData; /** The full evaluation, with per-condition evidence. Same conditions, same * order, same answers as `predicate` — one compiled list feeds both. */ readonly evaluate: (view: GuardHopView) => GuardVerdict; } /** The six hop keys a guard reads directly (every other key reads the * result's top-level JSON field). Exported for docs/tests. */ export declare const GUARD_HOP_KEYS: readonly ["toolName", "result", "status", "iteration", "userMessage", "currentSkillId"]; /** * Compile a guard into its predicate + its serializable data + its evidence * evaluator — ONE compilation, so the predicate that routes, the data the * check-up compares, and the evidence the record quotes can never describe * different guards (the `compileMatch` law). Refuses every shape it cannot * honor, by name, at the keystroke: an empty guard (asserts nothing — the * footprintjs anti-vacuous-truth law, refused at build instead of matched * never), an unknown operator (a typo like `gle` must not become an edge that * silently never fires), a non-data threshold (a guard that cannot ride a * recording is not a guard), an empty or oversized `in`/`notIn` list, and the * prototype-pollution key set. */ export declare function compileGuard(guard: SkillGuard, where: string): CompiledGuard; /** * The UNESCAPED caption — one grammar for naming a guard, shared by the * mermaid label and by prose quoting a guard back to its author (the * `plainMatchCaption` twin): `riskLevel ≥ high AND iteration ≤ 5`. */ export declare function plainGuardCaption(g: SkillGuardData): string; /** Caption a guard-only edge for a mermaid `|…|` label — the plain caption * under a leading "when", escaped once (the `mermaidMatchCaption` twin). */ export declare function mermaidGuardCaption(g: SkillGuardData): string; /** The route-edge preconditions a guard is judged AFTER — what the edge * already requires of the same result. Only the provably-comparable forms * arrive here: an exact-string `onToolReturn` (a RegExp is not decided) and * the declared `onToolStatus` set. */ export interface GuardPreconditions { readonly onToolReturnExact?: string; readonly onToolStatuses?: readonly string[]; } /** * The check-up's honesty core: is this guard PROVABLY unsatisfiable — by its * own conditions, or against the edge's declared preconditions? Returns the * human why-clause, or `undefined` when nothing is provable (say nothing * rather than guess — the `compareMatchers` law). * * What is claimed, exactly (all same-key, all decidable from the data): * • `eq` vs `ne`/`in`/`notIn`/ordered bounds on one key — a required value * the same guard excludes; * • crossed ordered bounds (`gt`/`gte` above `lt`/`lte`, same-type only); * • `in` whose every member the same key's `notIn` excludes; * • `status` values outside the CLOSED result-status vocabulary — a status * a tool can never declare (`TOOL_RESULT_STATUSES` is the whole set); * • the guard's `status`/`toolName` conditions vs the edge's own declared * `onToolStatus` set / exact-string `onToolReturn` — two declarations on * one edge, one of which must be wrong. * NOT claimed: anything across keys, anything involving result-JSON fields' * runtime values, RegExp `onToolReturn` intersection, mixed-type bounds. */ export declare function guardUnsatisfiable(g: SkillGuardData, pre?: GuardPreconditions): string | undefined;