agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
217 lines • 11.8 kB
TypeScript
/**
* skillMatch — the DATA-matcher domain for skill-graph start rules (SG-A).
*
* One module owns everything a matcher is and does, so the three consumers can
* never drift apart:
*
* • what a matcher IS — {@link SkillMatch} (author form) and
* {@link SkillMatchData} (serializable description);
* • how it RUNS — {@link compileMatch}: ONE compilation returns the
* predicate that routes AND the data that describes it;
* • how two matchers RELATE — {@link compareMatchers}: what the check-up can
* honestly prove (`overlapping-rules` /
* `rules-shadowed-by-order`);
* • how it DRAWS — {@link mermaidMatchCaption}: the entry-edge caption
* `toMermaid()` uses when the author gave no label.
*
* Deliberately engine-type-free (the predicate context is structural —
* `{ userMessage }` — which the engine's `InjectionContext` satisfies), so
* `skillGraphCheckup.ts` can import it and keep its own "pure over strings"
* law intact. Imported by `skillGraph.ts` (compile + caption) and
* `skillGraphCheckup.ts` (data type + compare); imports nothing.
*/
/**
* A DATA matcher on a start rule — the declarative alternative to a `when`
* predicate. The two SYNC leaf members:
*
* • a `RegExp` — tested against the user's message (`ctx.userMessage`). The
* stateful `g`/`y` flags are dropped at compile time (a sticky regex would
* alternate its answer across identical messages); the stored provenance
* carries the flags that actually run.
* • `{ keywords: [...] }` — case-insensitive; the rule matches when ANY keyword
* is present. Whole-word where sensible: a `\b` anchor is applied at each edge
* of the keyword that is an ASCII word character, so `refund` does not match
* "refunds", while `visa card` matches as a phrase and `v2?` matches literally.
*
* Being DATA is the point: a predicate is opaque code the library can only run,
* while a matcher can be COMPARED by the check-up (`overlapping-rules`,
* `rules-shadowed-by-order`), CAPTIONED by `toMermaid()` on the entry edge, and
* STORED on the compiled skill's provenance (`metadata.skillGraph.match`, as
* serializable {@link SkillMatchData}).
*
* The third member (SG-C) is the INTENT matcher:
* • `{ intent, examples }` — one sentence naming the intent ("customer wants
* a refund") plus ≥1 real user phrasings. Unlike the other two it compiles
* to NO message predicate: it cannot answer synchronously, so the entry
* carrying it compiles cursor-gated and the matching happens in the
* turn-start cascade (the RouteTurn stage), judged by the graph's
* configured classifier (`.classify(scorer)` / `start.classify`). A graph
* declaring one without a classifier is refused at build.
*
* The fourth member (9.20.0) is the CONJUNCTION:
* • `{ all: [...] }` — the rule matches only when EVERY member matches ("zone
* AND audit-shaped" as data, instead of a lookahead regex nobody can read
* back). Members are the SYNC data arms only — RegExp or `{ keywords }`:
* an intent inside `all` is refused at build (intents are judged by the
* classifier at turn start; they cannot compose into a synchronous AND —
* declare a separate intent rule instead). A nested `all` is FLATTENED
* (AND is associative, so the flattened form runs identically and the
* stored data describes exactly what runs — the same normalize-to-truth
* rule that drops a regex's stateful flags). An empty `all` is refused:
* it has nothing to check, so it could only surprise.
*
* Extensible by design: each non-RegExp member is an object discriminated by its
* own required key, so a future matcher is a new arm — not a reshape. A shape
* that is none of these is refused at build time, naming the forms that ARE
* supported.
*/
export type SkillMatch = RegExp | {
readonly keywords: readonly string[];
} | {
readonly intent: string;
readonly examples: readonly string[];
} | {
readonly all: readonly SkillMatch[];
};
/**
* The serializable description of a DATA matcher on a start rule (`match:` on
* `start.rules` / `SkillEntryOptions.match`) — what the check-up compares, what
* `toMermaid()` captions, and what rides the compiled skill's provenance. Pure
* strings by design (it has to survive `structuredClone`): a `RegExp` matcher is
* stored as its `source` + the `flags` that actually run.
*
* A discriminated union so a future member (e.g. an intent matcher) is a new
* `kind` arm, not a reshape.
*
* The `'all'` arm carries the conjunction's members as `parts`. Compilation
* GUARANTEES the parts are the leaf sync arms only (`'regex'` / `'keywords'`):
* an intent member is refused, and a nested `all` is flattened before the data
* is stored — so every consumer (the check-up's part comparison, the mermaid
* caption) reads a flat AND of leaves, never a tree.
*/
export type SkillMatchData = {
readonly kind: 'regex';
readonly source: string;
readonly flags: string;
} | {
readonly kind: 'keywords';
readonly keywords: readonly string[];
} | {
readonly kind: 'intent';
readonly intent: string;
readonly examples: readonly string[];
} | {
readonly kind: 'all';
readonly parts: readonly SkillMatchData[];
};
/** The one field a compiled matcher reads. Structural on purpose — the engine's
* `InjectionContext` satisfies it, and this module never has to import it. */
export interface MatchableContext {
readonly userMessage: string;
}
/**
* The EVIDENCE a data matcher routed on (9.28.0) — the actual text out of the
* USER MESSAGE that made the rule true. Recorded on the routing verdict
* (`turn_routed.witness`, `cursorMove.witness`) so the record can say *because
* the message said "…"* instead of only *a rule matched*.
*
* Three honesty rules, all enforced at capture:
* • the text comes from `ctx.userMessage` and nothing else — never a tool
* result, never system/assistant prose (a matcher only ever reads the user
* message, so this is a property of where it is captured, not a filter);
* • it is BOUNDED to {@link WITNESS_MAX_CHARS} characters, ellipsis included
* — a `/[\s\S]+/` rule must not paste the whole message into every record;
* • runs of whitespace collapse to one space and the ends are trimmed, so a
* multi-line match stays one quotable clause. That is the ONLY edit made to
* the matched substring, and it is the reason `text` is described as the
* matched text rather than the matched bytes.
*
* `keyword` is present only for the `{ keywords }` arm: WHICH declared keyword
* hit, beside the text it hit on (they differ in case, and `text` may carry the
* phrase's real spacing).
*
* A `when` predicate produces NO witness (opaque code — the library cannot know
* what convinced it), and neither does an intent match (a scorer's evidence is
* its scores, already recorded). Absent is the honest answer there.
*/
export interface RouteWitness {
readonly text: string;
readonly keyword?: string;
}
/** The witness bound — 80 characters, ellipsis included. A record is evidence,
* not a transcript. */
export declare const WITNESS_MAX_CHARS = 80;
/**
* Compile a data matcher into its predicate + its serializable description — ONE
* compilation, so the predicate that routes and the data the check-up compares can
* never describe different matchers. Refuses a shape it cannot honor (a bare
* string, an empty keyword list, an intent without examples), naming the forms
* that ARE supported — accepting one and matching nothing would be the silent kind
* of wrong.
*
* The INTENT arm compiles to `predicate: undefined` — it cannot answer
* synchronously (a classifier judges it in the turn-start cascade), so the entry
* carrying it is cursor-gated instead of rule-gated. `skillGraph.ts` is the only
* caller that accepts a predicate-less compile, and it refuses `match: { intent }`
* when no classifier is configured.
*
* The third member of the returned triple is `witness` (9.28.0): the SAME
* compilation's evidence extractor — given a context the predicate accepted, it
* returns {@link RouteWitness} (the matched text, bounded). It is a separate
* function on purpose, so the hot path stays exactly what it was (a boolean
* `.test`) and the evidence is paid for ONCE per turn, on the rule that actually
* won. The intent arm has no witness (nothing matched synchronously).
*/
export declare function compileMatch(match: SkillMatch, where: string): {
predicate: ((ctx: MatchableContext) => boolean) | undefined;
data: SkillMatchData;
witness?: (ctx: MatchableContext) => RouteWitness | undefined;
};
/**
* Compare two DATA matchers, earlier vs later (declaration order). Returns only
* what is PROVABLE:
* • `'shadows'` — every message the later matches, the earlier matches too, so
* the later can never be chosen (identical regex; earlier keywords ⊇ later's);
* • `'overlaps'` — a witness class of messages matches both (a shared keyword),
* but each rule also has messages of its own;
* • `undefined` — nothing provable here (different regex sources, a regex vs a
* keyword list): say nothing rather than guess.
* `why` is the human clause the check-up splices into its problem message.
*
* The `'all'` arm (9.20.0) keeps the same law — claim only what part-coverage
* proves. An `all` rule matches a SUBSET of each of its parts' messages, so:
* • a plain rule EARLIER that provably covers ONE part of a later `all` rule
* shadows it (every message the `all` matches satisfies that part, which
* the earlier already matches);
* • an `all` rule EARLIER shadows a later matcher when EVERY earlier part is
* provably covered by some later part — the later only ADDS constraints
* (identical part sets are the simplest case).
* "Provably covers" is same-kind only: an identical regex, or a keyword
* superset. No `'overlaps'` is ever claimed for a pair involving `all`: a
* conjunction can be unsatisfiable (`all: [/^a/, /^b/]` matches nothing), so a
* witness message cannot be asserted from the data — and the ordinary
* specific-rule-first layout (the `all` rule above its plain fallback) is a
* DESIGN, not a problem to warn about.
*/
export declare function compareMatchers(earlier: SkillMatchData, later: SkillMatchData): {
relation: 'shadows' | 'overlaps';
why: string;
} | undefined;
/**
* Caption an entry edge with its data matcher, escaped for a mermaid `|…|` label
* (`|` would end the label early; `"` would open a string — both become mermaid
* entities). Keywords list at most three, then a count, so a wide router stays
* readable; an `all` conjunction joins its parts with ` AND ` (parts are always
* leaves — compilation flattened any nesting). Used only when the author gave no
* explicit `label` — an explicit label always wins, byte-for-byte, exactly as
* before.
*/
export declare function mermaidMatchCaption(m: SkillMatchData): string;
/**
* The UNESCAPED caption — one grammar for naming a matcher, shared by the
* mermaid label (which escapes it once, at the top) and by prose that quotes a
* matcher back to its author (`skillExamples.ts`'s messages). Recursion for the
* `all` arm happens here so the escaping stays a single outer pass.
*/
export declare function plainMatchCaption(m: SkillMatchData): string;
//# sourceMappingURL=skillMatch.d.ts.map