/** * ADR-091 Stage 4 Verify-Against-Codebase verifier (mmnto-ai/totem#1682). * * Runs a compiled rule deterministically (zero LLM) against the consumer's * existing codebase before promoting it to Active status. Catches the class * of false positive that Layer 3 (ADR-088) cannot — Layer 3 verifies the * pattern matches the lesson's authored `badExample` (internal consistency); * Stage 4 verifies the pattern doesn't accidentally fire on legitimate code * (global false-positive safety). * * Four outcomes per ADR-091 §"Stage 4: Verify-Against-Codebase": * * - **No matches** (`outcome: 'no-matches'`) — the verifier ran, found zero * hits in the codebase. Caller sets `status: 'untested-against-codebase'`; * subsequent compile cycles in a populated repo can re-run Stage 4 and * promote. * - **Out-of-scope baseline match** (`outcome: 'out-of-scope'`) — the rule * fired on at least one file in the verification baseline (test files, * fixture directories, or files outside the rule's `fileGlobs` scope). * The pattern is over-broad. Caller archives the rule with * `reasonCode: 'stage4-out-of-scope-match'` and the offending paths. * - **In-scope `badExample`-shape match** (`outcome: 'in-scope-bad-example'`) * — the rule fired only on in-scope files AND every in-scope match line is * structurally equivalent to the rule's `badExample`. The rule fires on * real code in the exact authored shape. Caller sets `status: 'active'` * with `confidence: 'high'`. * - **Candidate Debt** (`outcome: 'candidate-debt'`) — the rule fired only * on in-scope files but at least one match line differs from the * `badExample` shape. The rule may be catching real debt, or producing * false positives the LLM-generated pattern overshoots into. Caller * accepts as `status: 'active'` and forces `severity: 'warning'` so it * never breaks CI on first run; `totem doctor` (mmnto-ai/totem#1685) * surfaces the candidate-debt sites for human confirmation. * * Bootstrap modes: T1 ships local-compile fully (the verifier runs against * the consumer's working tree before `totem lesson compile` serializes the * rule). Pack install→lint promotion lands in T3 (mmnto-ai/totem#1684). * Consumer baseline overrides land in T2 (mmnto-ai/totem#1683). Perf * optimizations (single-pass, file-tree caching, streaming short-circuit) * land in T5 (mmnto-ai/totem#1686). T1 walks per-rule, no caching. * * Architecture: callback-based filesystem. The verifier accepts `listFiles` * + `readFile` callbacks instead of touching `fs` directly so core stays * orchestration-only. CLI implementations back the callbacks with `git * ls-files` and `fs.readFile`; tests stub them with synthetic file maps. */ import type { CompiledRule } from './compiler-schema.js'; import type { RuleEngineContext } from './rule-engine.js'; export interface Stage4Baseline { /** * Glob patterns the rule MUST NOT fire on. Files matching any of these * globs are part of the verification baseline — a match on one of them * is evidence the pattern is over-broad. T1 shipped with `DEFAULT_BASELINE_GLOBS` * (test + fixture patterns); T2 (mmnto-ai/totem#1683) layers consumer * `extend` / `exclude` overrides via `review.stage4Baseline` config. * * Files outside the rule's `fileGlobs` scope are implicitly in the baseline * — no need to list them here. The verifier computes the implicit case * from `rule.fileGlobs` at evaluation time. */ readonly excludeFileGlobs: readonly string[]; /** * Provenance: globs added via `# stage4-baseline:` directives in the * consumer's `.totemignore` file. Empty when no such directives. Read * by `totem doctor` (T4) and trace events; the verifier itself only * reads `excludeFileGlobs`. */ readonly extendedFromIgnoreFile: readonly string[]; /** * Provenance: globs added via `review.stage4Baseline.extend` in * `totem.config.ts`. Empty when not configured. */ readonly extendedFromConfig: readonly string[]; /** * Provenance: globs removed from the default baseline via * `review.stage4Baseline.exclude` in `totem.config.ts`. Empty when not * configured. Useful for diagnosing why a rule fires on a path the * consumer expected to be in the baseline. */ readonly excludedFromConfig: readonly string[]; } export type Stage4Outcome = 'no-matches' | 'out-of-scope' | 'in-scope-bad-example' | 'candidate-debt'; export interface Stage4VerificationResult { outcome: Stage4Outcome; /** Repo-relative paths where the rule fired AND the file is in the baseline. */ readonly baselineMatches: readonly string[]; /** Repo-relative paths where the rule fired AND the file is in scope. */ readonly inScopeMatches: readonly string[]; /** * Match lines from in-scope hits that did NOT match the `badExample` * shape (after trimming). Empty when `outcome === 'in-scope-bad-example'`. * Populated when `outcome === 'candidate-debt'` to feed the `totem doctor` * UX surface in T4 (mmnto-ai/totem#1685). */ readonly candidateDebtLines: readonly string[]; } export interface Stage4VerifierDeps { /** * Returns repo-relative paths of all files to verify against. CLI * implementation calls `git ls-files --recurse-submodules`. Tests pass * a synthetic list. Empty array is valid input — the verifier returns * `outcome: 'no-matches'` (the zero-files case is indistinguishable from * the no-hits case at the API level; both produce `untested-against-codebase` * status downstream). */ listFiles: () => Promise; /** * Returns the file content as a string. CLI implementation reads from * the working tree (`fs.readFile`). MUST throw if the file is missing — * Stage 4 is a fail-loud contract per Tenet 4. */ readFile: (file: string) => Promise; /** * Optional working directory absolute path. Required when the verifier * encounters ast / ast-grep rules — `applyAstRulesToAdditions` resolves * file content against this root. Regex-only verification does not need * it. T1 callers always pass the repo root. */ workingDirectory?: string; /** Optional rule-engine context. Defaults to a no-op logger. */ ruleCtx?: RuleEngineContext; } /** * Glob shapes the test-contract scope classifier (mmnto-ai/totem#1626 / * mmnto-ai/totem#1652) promotes-to-test-inclusive when emitting LLM scope. * Stage 4 mirrors them as the default baseline so any rule that fires on a * test or fixture file is treated as out-of-scope by default. T2 * (mmnto-ai/totem#1683) lets consumers `exclude` from this list when their * project legitimately treats `tests/` as production. */ export declare const DEFAULT_BASELINE_GLOBS: readonly string[]; /** * Backwards-compatible shorthand for `resolveStage4Baseline({})`. Returns the * default baseline (test + fixture globs) with empty provenance arrays. Kept * because pre-T2 callers (early CLI integration sites, tests) pass no * overrides; new callers should prefer `resolveStage4Baseline` directly so * config + .totemignore overrides flow through. */ export declare function getDefaultBaseline(): Stage4Baseline; /** * Static manifest paths excluded from Stage 4 corpus to prevent rules with * a `badExample` field from self-matching against their own entry in the * compiled manifest. CLI integration (`packages/cli/src/commands/compile.ts`) * additionally computes a `totemDir`-aware path at runtime * (`path.join(config.totemDir, 'compiled-rules.json')` normalized to forward * slashes) and adds it to the exclusion set so consumers who override * `config.totemDir` are covered too. */ export declare const STAGE4_MANIFEST_EXCLUSIONS: readonly string[]; export interface ResolveStage4BaselineInput { /** Globs parsed from `# stage4-baseline:` directives in `.totemignore`. */ readonly ignoreDirectives?: readonly string[]; /** Globs from `review.stage4Baseline.extend` in `totem.config.ts`. */ readonly configExtend?: readonly string[]; /** Globs from `review.stage4Baseline.exclude` in `totem.config.ts`. */ readonly configExclude?: readonly string[]; } /** * Compute the effective Stage 4 baseline for a compile run. * * Composition: `defaults ∪ ignoreDirectives ∪ configExtend ∖ configExclude`. * `configExclude` is set-difference (LAST), so a consumer can remove a * default baseline glob like `**\/tests/**` when their project legitimately * treats `tests/` as production. Set membership uses byte-equal comparison * on the glob string, NOT path matching — `exclude: ['**\/tests/**']` * removes that exact default entry, not every glob that happens to match * a `tests/` path. * * Pure function. Does NOT read the filesystem; the CLI integration site * reads `.totemignore` and parses it via `parseStage4BaselineDirectives` * before passing the directives in. * * @param input - The three composition inputs (all optional / default to `[]`). * @returns A `Stage4Baseline` whose `excludeFileGlobs` is consumed by the * verifier and whose three provenance arrays are read by `totem doctor` * (T4 / `mmnto-ai/totem#1685`) and trace events. */ export declare function resolveStage4Baseline(input: ResolveStage4BaselineInput): Stage4Baseline; /** * Extract `# stage4-baseline: ` directives from `.totemignore` content * (or any line-oriented text). The leading `#` is REQUIRED — the directive * lives on a comment line so it doesn't interfere with the rest of * `.totemignore`'s ignore semantics. Variable whitespace around the `#`, * the colon, and the body is allowed; the regex collapses it. * * Returns the globs in source order. Empty / whitespace-only directive * bodies are skipped silently (no throw). The directive name is * case-sensitive to match `.totemignore`'s overall convention. * * Pure function (`string → string[]`) so it can be invoked from any core * consumer. CLI reads the file and hands the content to this helper; * MCP integrations may want the same surface in the future. * * @param content - Raw `.totemignore` text (or any line-oriented content). * Empty or undefined returns `[]`. CRLF and LF line endings both work. * @returns Glob strings extracted from `# stage4-baseline:` lines, in * source order, excluding empty/whitespace-only directive bodies. * * @example * ```ts * parseStage4BaselineDirectives('# stage4-baseline: build/**\nsrc/temp/**'); * // → ['build/**'] (only the directive line; 'src/temp/**' is ordinary * // .totemignore content, not a stage4 directive) * ``` */ export declare function parseStage4BaselineDirectives(content: string): string[]; /** * Run Stage 4 verification for a single compiled rule against the consumer's * codebase. Caller decides what to do with the returned outcome: * * - `'no-matches'` → set rule.status = 'untested-against-codebase' * - `'out-of-scope'` → archive rule with reasonCode 'stage4-out-of-scope-match' * - `'in-scope-bad-example'` → set rule.status = 'active', confidence = 'high' * - `'candidate-debt'` → set rule.status = 'active', force severity = 'warning' * * The verifier itself does NOT mutate the rule. Mutation happens at the * compileLesson integration site so the trace event and lifecycle field * preservation remain centralized. * * For Pipeline 1 manual rules, the integration site bypasses Stage 4 * entirely — those rules are human-authored and Stage 4 is a safety net * for LLM-generated patterns. The verifier itself is engine-agnostic and * will run on any rule it's handed. */ export declare function verifyAgainstCodebase(rule: CompiledRule, baseline: Stage4Baseline, deps: Stage4VerifierDeps): Promise; //# sourceMappingURL=stage4-verifier.d.ts.map