import type { CompiledRule } from '../compiler-schema.js'; import type { RuleEngineContext } from '../rule-engine.js'; import type { RuleFiring } from './windtunnel-scorer.js'; /** * One resolved PR's diff plus its role in the wind-tunnel. The certifying run * (5c-ii) builds these from each resolved-PR diff + the controls; 5c-i is the * deterministic engine that turns them into `RuleFiring[]`. * * `controlKind` is the role of the PR/corpus item being scanned, NOT a property * of any individual rule — every rule that fires on a `negative` item is culled * (fold-H: neg-control firings pass through as `controlKind:'negative'`, never * dropped pre-score). `targetRuleId` is the rule a `positive` control MUST fire * to prove non-vacuousness (the positive-control contract the scorer checks). */ export interface ResolvedPrDiff { pr: number; /** Unified diff text for the PR (post-image additions are extracted from it). */ diff: string; controlKind: 'corpus' | 'positive' | 'negative'; /** For positive controls: the rule expected to fire (proves non-vacuousness). */ targetRuleId?: string; } /** * C1 (fold-B's data prerequisite) — a PER-RULE control result over the * surviving (active, non-culled) rules. `positiveControl`/`negativeControl` are * derived from THIS rule's actual firings, NEVER from the run-level * `nonVacuity` (which is a global AND across all positive-control targets and * would over-stamp a rule that never exercised a control). 5c-ii consumes this * to stamp legitimacy per-rule, survivor-only. */ export interface PerRuleControlResult { /** True iff this rule's positive control is proven (per-rule, not global): a target firing (mined) or a §4 differential held at emission (authored). */ positiveControl: boolean; /** True iff this rule did NOT fire on any negative control (clean = passed). */ negativeControl: boolean; /** * Evidence for the positive result: MINED — the establishing firingLabelIds; * AUTHORED — `§6-emission:`-prefixed locus refs (option (i), #2291; see * `computeAuthoredPerRuleControlResults`). Never both shapes in one map. */ evidenceRefs: string[]; } /** * A1 (fold-D) collision detail — surfaced when two firings normalize to the * same `labelId` (the `labelId→evidenceRef`/ground-truth join would silently * overwrite). Emitted into the thrown error so the cert-run report can name the * colliding labelIds + their evidence refs. */ export interface FiringLabelCollision { labelId: string; /** The colliding firings' evidence (ruleId/pr/filePath/matchedLine). */ evidenceRefs: Array<{ ruleId: string; pr: number; filePath: string; matchedLine: string; }>; } /** Thrown by `assertUniqueFiringLabels` (A1 hard-gate floor, Tenet 4). */ export declare class FiringLabelCollisionError extends Error { readonly collisions: FiringLabelCollision[]; constructor(collisions: FiringLabelCollision[]); } /** Thrown when an archived rule reaches the scored set (fold-F, Tenet 4). */ export declare class ArchivedRuleInScopeError extends Error { readonly archivedRuleIds: string[]; constructor(archivedRuleIds: string[]); } /** * Normalize a matched line for the content-based labelId (A2/A3). Trailing * whitespace is dropped so cosmetic EOL drift between the post-image and the * diff does not split a firing into a new labelId; interior content is * preserved (the label must still distinguish genuinely different lines). * * Exported so the 5d-iii label-deriver's span-join normalizes a disposition's * added hunk rows with the EXACT same rule the firing's `matchedLine` uses — * the content bind keys on the same bytes the labelId keys on, so the two can * never silently drift apart (codex hard fold + the panel anti-drift mandate). */ export declare function normalizeMatchedLine(line: string): string; /** * fold-F — assert no `status:'archived'` rule is in the scored set (Tenet 4). * Called at the scorer input boundary BEFORE the engine runs, so * `applyAstRulesToAdditions` is never invoked on an archived rule and zero * archived refs can reach `RuleFiring[]`. Archived ≠ wind-tunnel FP: an * archived rule that "fires" is not evidence of imprecision, it is a corpus * contamination that must fail loud. */ export declare function assertNoArchivedRules(rules: CompiledRule[]): void; /** * A1 (fold-D) — assert `firings.length === unique(labelIds).size` BEFORE * `scoreWindtunnel` (Tenet 4). Originally specced as a hard-gate FLOOR that * threw on any collision; the strategy ruling (2026-06-20) DEMOTED it to a * post-dedup invariant once `buildFirings` learned to collapse same-labelId * matches (`dedupeFirings`). It still THROWS on a collision — but a collision * here now means a dedup BUG (or a caller that bypassed `buildFirings`), not an * honest multi-match line, so failing loud is correct. We deliberately do NOT * add an occurrence discriminator / ordinal (it would regress `firingLabelId`'s * line-drift resistance); a diff-hunk-span discriminator stays reserved. The * invariant guards the `labelId→evidenceRef` contract the ground-truth join * depends on: a 1:1 labelId→firing map. */ export declare function assertUniqueFiringLabels(firings: RuleFiring[]): void; export interface BuildFiringsInput { /** Active compiled rules (already loaded; archived must be pre-filtered). */ rules: CompiledRule[]; /** Resolved-PR diffs + controls (corpus / positive / negative). */ prDiffs: ResolvedPrDiff[]; /** Repo root for AST file resolution (NOT process.cwd() — #1304). */ cwd: string; /** Post-image content seam (S1/C1 — same content for regex astContext + AST). */ readStrategy: (file: string) => Promise; /** Per-invocation rule-engine context (logger + per-ctx state). */ ruleEngineCtx: RuleEngineContext; onWarn?: (msg: string) => void; } export interface BuildFiringsResult { firings: RuleFiring[]; /** Distinct files touched across all PR diffs (C2 — real exposure). */ filesTouchedInWindow: number; /** Positive-control targets, derived from the prDiffs (for the scorer). */ positiveControlTargets: Array<{ pr: number; targetRuleId: string; }>; } /** * The real-engine firing path (replaces `runMockEngine` for the certifying * phase). For each resolved-PR diff: extract additions → `enrichWithAstContext` * (regex astContext via the shared post-image readStrategy) → regex engine + * `applyAstRulesToAdditions` (AST/ast-grep, same readStrategy) → map every * `Violation` to a `RuleFiring` with `labelId = firingLabelId(...)`. * * Invariants honored here: * - **fold-F**: throws if any archived rule is present (assertNoArchivedRules), * so the engine is never invoked on an archived rule. * - **fold-H**: a firing on a `negative` PR is emitted as `controlKind:'negative'` * (the scorer culls + ledgers it — never dropped pre-score). Unlabeled firings * stay in `firings` (no ground-truth) so the scorer routes them to * needsAdjudication. * - **C2**: `filesTouchedInWindow` is the count of distinct post-image files * across all diffs — the real third exposure leg. * * fold-D: before returning, same-`labelId` matches are collapsed to ONE logical * firing (`dedupeFirings`), retaining every raw match as `evidence`. The caller's * `assertUniqueFiringLabels` is therefore a post-dedup INVARIANT (it can no * longer fire on an honest multi-match line), not a pre-score floor. */ export declare function buildFirings(input: BuildFiringsInput): Promise; /** * C1 (fold-B's data prerequisite) — build a `Map` * over the SURVIVING rules (active rules that were NOT culled by a negative * control). `positiveControl` is true ONLY when THIS rule fired its declared * positive-control target — derived from the rule's own firings, never from the * global `nonVacuity`. `negativeControl` is true when the rule fired on NO * negative control. 5c-ii reads this to stamp legitimacy survivor-only. * * Survivors = mintedRuleIds minus rules that fired on any negative control * (those are culled; the scorer records them in the cullLedger and we do not * stamp them). */ export declare function computePerRuleControlResults(input: { firings: RuleFiring[]; mintedRuleIds: string[]; positiveControlTargets: Array<{ pr: number; targetRuleId: string; }>; }): Map; //# sourceMappingURL=windtunnel-firing.d.ts.map