/** * Validation Harness — tests learned rules before promotion to the formal catalog. * * Every auto-generated rule must pass validation: * 1. Fires correctly on the ORIGINAL code that triggered it * 2. Fires correctly on synthetic "known-bad" variations * 3. Does NOT fire on synthetic "known-good" variations * * Only rules with precision >= 0.8 and recall >= 0.5 are promoted. */ import type { LearnedRule, ValidationResult } from './types.js'; import { type CorpusFile } from '../rule-harvester/recall-benchmark.js'; /** Reject a candidate when more than this many corpus files are noisy. */ export declare const DEFAULT_CORPUS_MAX_NOISY_FILES = 2; /** * A detection regex is sane when it compiles in this runtime and does not * match the empty string (with the same 'gi' flags executeRule uses). */ export declare function isSaneRegex(pattern: string | undefined): boolean; /** * Validate a learned rule against synthetic test cases. * * @param rule - The candidate rule to validate. * @returns The validation result with precision/recall metrics. */ export declare function validateRule(rule: LearnedRule): ValidationResult; export interface CorpusOverFiringConfig { /** * A corpus file is "noisy" when the rule fires MORE than this many times * in it. Mirrors the recall benchmark's per-file noise cap. */ readonly maxFiresPerFile: number; /** Reject the candidate when the number of noisy files EXCEEDS this. */ readonly maxNoisyFiles: number; } export interface CorpusOverFiringResult { readonly passed: boolean; /** Files where the rule fired more than maxFiresPerFile times. */ readonly noisyFiles: number; /** Highest fire count seen in any single corpus file. */ readonly worstFires: number; readonly filesChecked: number; /** Human-readable rejection reason; undefined when passed. */ readonly reason?: string; } /** * Corpus over-firing gate — an ADDITIONAL promotion check run after * validateRule, never a replacement for it. * * validateRule tests a candidate only against its own source hunks plus * synthetic variations, so a rule matching a ubiquitous code shape passes * trivially and then floods real scans (noise-bombs lp_cve_1784567380355 * and lp_cve_1784582234937 both shipped exactly this way — the per-file * noise cap only caught them AFTER promotion, at scoring time). Here the * candidate runs against a held-out multi-repo corpus and is rejected when * it exceeds the per-file noise cap in too many files. * * Deterministic and LLM-free. An empty corpus passes (gate inactive), so * harvests without a configured corpus keep working. absence_is_bad rules * pass automatically: they produce no match counts to measure. */ export declare function checkCorpusOverFiring(rule: LearnedRule, corpus: readonly CorpusFile[], config?: Partial): CorpusOverFiringResult; /** * Get the validation thresholds. */ export declare function getValidationThresholds(): { minPrecision: number; minRecall: number; minTestCases: number; };