/** * Evaluation Harness -- orchestrates running the corpus through the ATR engine * and produces a structured EvalReport. * * Supports: * - Regex-only evaluation (Tier 2) * - Regex + Embedding evaluation (Tier 2 + 2.5) * - Full pipeline evaluation (all tiers) * - Per-sample latency measurement * - Regression check against baseline thresholds * * @module agent-threat-rules/eval/eval-harness */ import { type EnforceLaneRecall } from '../measurement/enforce-lane.js'; import type { Lane } from '../quality/rule-contract.js'; import type { CorpusSample } from './corpus.js'; import { getCorpusStats } from './corpus.js'; import type { EvalReport, BaselineThresholds, RegressionCheck } from './metrics.js'; import type { RuleQualityReport } from './rule-metrics.js'; /** * How a corpus sample is presented to the engine. * * 'legacy' — one event: `{ type: sample.eventType, content: sample.text }`. * What this harness did until 2026-08-05. Reproduces every * previously published eval / PINT number. * 'wide' — legacy PLUS the same event re-typed to mcp_exchange, which * removes ONLY the engine's source-type filter. Isolates how * much of the gap is rule ADMISSION rather than field pouring. * 'canonical' — legacy PLUS scripts' canonical shape set (src/corpus-event.ts) * PLUS scanSkill(). The presentation every FP gate in this repo * already uses. * * WHY THIS EXISTS AT ALL * * src/engine.ts derives a source type from event.type (EVENT_TYPE_TO_SOURCE) * and then skips every rule whose `agent_source.type` differs. The PINT corpus * labels all 850 samples `llm_input` -> source `llm_io`, so on main 383 of the * 773 non-draft rules — every mcp_exchange, tool_call, multi_agent_comm, * memory_access, context_window and skill_lifecycle rule — were dropped before * a single pattern was compared. They could not produce a true positive and, * more importantly, could not produce a false positive either: the published * "precision 100%" was measured over half a rulebase. * * That is the same zero-measurement defect src/corpus-event.ts was written to * abolish; this harness simply escaped the ratchet because it lives under src/ * and the ratchet only greps scripts/. * * The mode is explicit, and recorded in the measurement notes, because flipping * it changes a PUBLISHED number. Override with ATR_EVAL_EVENT_SHAPE=legacy|wide. */ export type EvalEventShapeMode = 'legacy' | 'wide' | 'canonical'; /** Default presentation. Env override exists so an old number can be reproduced. */ export declare function defaultEventShapeMode(): EvalEventShapeMode; export interface EvalConfig { /** Directory containing ATR YAML rules */ readonly rulesDir: string; /** Path to attack embeddings JSON (optional, for Tier 2.5) */ readonly embeddingsPath?: string; /** Custom corpus (defaults to built-in EVAL_CORPUS) */ readonly corpus?: readonly CorpusSample[]; /** Baseline thresholds for regression check */ readonly thresholds?: BaselineThresholds; /** Path to save report JSON */ readonly outputPath?: string; /** Enable Tier 2.5 embedding evaluation (default: auto-detect) */ readonly enableEmbedding?: boolean; /** Event presentation (default: defaultEventShapeMode()) */ readonly eventShape?: EvalEventShapeMode; /** * Detection lane the engine is constructed with (default: the engine's own * default, `hunt`, which loads every maturity). Every published figure from * this harness is a hunt run; the runners make a second, `enforce` run over * the same corpus and record it as breakdown.enforce_lane. */ readonly lane?: Lane; } /** Cosine-similarity threshold of the evaluation harness's embedding tier (Tier 2.5). */ export declare const EVAL_EMBEDDING_SIMILARITY_THRESHOLD = 0.65; /** * Run the full evaluation harness. * Returns the EvalReport and RegressionCheck. */ export declare function runEval(config: EvalConfig): Promise<{ report: EvalReport; regression: RegressionCheck; corpusStats: ReturnType; tiersUsed: readonly string[]; ruleQuality: RuleQualityReport; eventShape: EvalEventShapeMode; /** The lane the engine ran in. */ lane: Lane; /** Rules that lane loads: status not draft/deprecated, maturity admitted by the lane. */ rulesInLane: number; }>; /** * The enforce-lane record for a measurement, from a runEval() made with * `lane: 'enforce'` over the same corpus as the headline run. Its attack * samples are the headline's (tp + fn); what may differ is the lane, and * nothing else: a second run that presented the samples differently, or lost * the embedding tier the headline had, would measure something the headline * did not, so it is refused rather than recorded. */ export declare function enforceLaneFromEval(run: { readonly report: EvalReport; readonly lane: Lane; readonly rulesInLane: number; readonly eventShape: EvalEventShapeMode; readonly tiersUsed: readonly string[]; }, headline: { readonly report: EvalReport; readonly eventShape: EvalEventShapeMode; readonly tiersUsed: readonly string[]; }): EnforceLaneRecall; export declare function runEvalCLI(): Promise; //# sourceMappingURL=eval-harness.d.ts.map