/** * Verdict — turn a finished report into a one-line ship/no-ship recommendation. * * Why this exists * --------------- * v0.21 makes the data trustworthy. closes the last mile: senior * engineers' #1 complaint is "the report has all the right numbers but I * still need 30 minutes to read it before I can decide". This module * aggregates the data sources omk already produces — Bootstrap CI on the * pairwise diff, three-layer CI gate, saturation curve, inter-judge ensemble * agreement, and (when available) Krippendorff α against gold — and emits one * of six verdicts: * * - **PROGRESS** diff CI shows real positive shift, no layer regressed * - **CAUTIOUS** positive shift but at least one layer broke its gate, * or saturation says we're not powered yet * - **REGRESS** diff CI clearly negative, or a layer dropped a gate * - **NOISE** diff CI contains 0 — can't separate from noise * - **UNDERPOWERED** N too small / saturation low confidence and no signal * - **SOLO** single-variant report; nothing to compare against * * The output is intentionally text-template-driven so the same rule engine * powers both `omk eval` (CLI, terse) and the HTML report's * top-of-page "verdict pill" ("verdict pill" surface). Both surfaces must agree. * * Subjectivity caveat: the ship recommendation is rule-based, not statistically * proven optimal. Each rule's source (NIST AI 800-3 / Krippendorff thresholds / * empirical) is documented inline so users can audit and override. */ import type { Lang, Report } from '../types/index.js'; /** * Below this sample count a non-significant diff is read as UNDERPOWERED * (only large effects are detectable) rather than NOISE. Matches the * pre-flight power band documented in `docs/specs/sample-design-spec.md`; * guarded by `test/scripts/doc-constants-drift.test.ts`. */ export declare const UNDERPOWERED_MIN_SAMPLES = 20; /** * Inter-judge Pearson bands, shared with the report's ensemble-agreement audit * badge (`src/renderer/summary.ts`) so the verdict gate and the UI read one scale. * - `>= ENSEMBLE_STRONG_PEARSON` (0.7): judges agree on rank order (badge green). * - `< ENSEMBLE_DISSENT_PEARSON` (0.4): strong disagreement (badge red). A * would-be PROGRESS is downgraded to CAUTIOUS — the judge-layer signal driving * the win is unreliable when the ensemble can't agree. */ export declare const ENSEMBLE_STRONG_PEARSON = 0.7; export declare const ENSEMBLE_DISSENT_PEARSON = 0.4; /** * Run-to-run instability threshold on the median coefficient of variation (CV = stddev/mean). * Once stability is **actually measured** (`--repeat ≥ 2`) and the median CV exceeds this line, * a would-be PROGRESS is downgraded to CAUTIOUS — a statistically significant but run-to-run * irreproducible "gain" is not shippable. It is the upper bound of the 5/15% stability bands in * `docs/specs/terminology-spec.md` §5; doc ↔ code parity is guarded by * `test/scripts/doc-constants-drift.test.ts`. Single-run reports (stability not measured) are * never gated by it — see `computeVerdict`. */ export declare const STABILITY_UNSTABLE_CV = 0.15; /** * Per-layer pass/fail line for the three-layer gate, on the 1-5 scale. * **Pragmatic default, not derived from an external standard**: 3.5 is a clear margin * above the 3.0 scale midpoint ("basically acceptable"), so a layer must land * comfortably in the upper half to pass. Overridable via `omk eval --threshold`. * doc ↔ code parity guarded by `test/scripts/doc-constants-drift.test.ts`. */ export declare const DEFAULT_GATE_THRESHOLD = 3.5; /** * Train − holdout composite gap (1-5 scale) above which `omk eval --holdout-ratio` * is read as **sample-set overfitting**: the gain lives on the samples the skill * was shaped around and does not carry to the held-out slice. Like the stability * gate, a would-be PROGRESS is then downgraded to CAUTIOUS — a win that does not * generalize is not shippable. * **Pragmatic default, not from an external standard**: 0.5 is 10% of the 1-5 scale * — small enough to catch a real generalization drop, wide enough to ignore the * sampling noise of a small holdout slice. Only fires when a holdout split is * present (opt-in), so it never moves a default report's verdict. * doc ↔ code parity guarded by `test/scripts/doc-constants-drift.test.ts`. */ export declare const OVERFITTING_GAP_THRESHOLD = 0.5; export type VerdictLevel = 'PROGRESS' | 'CAUTIOUS' | 'REGRESS' | 'NOISE' | 'UNDERPOWERED' | 'SOLO'; export interface VerdictResult { level: VerdictLevel; /** One-line headline shown in CLI / pill. */ headline: string; /** Per-pair verdict if multi-treatment; same shape as level for each. */ perPair?: Array<{ control: string; treatment: string; level: VerdictLevel; headline: string; }>; /** Detail bullets shown by `omk eval` verbose output. */ rationale: { significance?: string; layerWinners?: string; sampleSize?: string; /** Stability claim:CV / variance summary if --repeat ≥ 2, 否则显式说"未测量" * 让用户感受到 single-run 的盲区,而不是默默不提。 */ stability?: string; judgeAgreement?: string; /** Overfitting (train vs holdout) caveat — only present under `--holdout-ratio`. */ overfitting?: string; /** Knowledge-gap caveat — informational, watermarked, never gates (gap-spec §8). */ gapSignal?: string; shipRecommendation?: string; }; /** The pair the top-level verdict is about — the worst pair from the roll-up, NOT * variants[1]. Surfaces let the HTML pill name the right treatment in a * control-vs-many report instead of re-deriving from the first pair. Undefined for * SOLO / pairless reports. */ representative?: { control: string; treatment: string; }; /** Structured caveats (language-neutral) so HTML / other surfaces can i18n them * instead of re-parsing the zh `rationale` strings. Present only when the caveat * fires; mirrors `rationale.overfitting` / `rationale.gapSignal`. */ caveats?: { overfitting?: { variant: string; trainScore: number; holdoutScore: number; gap: number; }; gapSignal?: { variant: string; gapRatePct: number; testSetPath?: string | null; testSetHash?: string | null; }; }; /** Variants present in the report (best-vs-control framing). */ variants: string[]; } export interface VerdictOptions { /** Three-layer ci-gate threshold; defaults to DEFAULT_GATE_THRESHOLD (matches `omk eval`). */ gateThreshold?: number; /** * Magnitude (in raw score points) below which a "significant" diff is treated * as practically negligible (statistically real but too small to matter). * Defaults to 0.1 — about 2% of the 1-5 scale, the floor of practical signal. */ triviallySmallDiff?: number; } /** * Compute a verdict for a finished report. Pure function — no I/O. */ export declare function computeVerdict(report: Report, options?: VerdictOptions): VerdictResult; /** * Stability rationale. 三种状态: * - --repeat ≥ 2 + 有 variance 数据: 报告 CV (variation coefficient) 主指标 * - --repeat ≥ 2 但 variance 缺失: 异常,标 "—" 提示数据丢失 * - --repeat < 2: 显式说"未测量,需 --repeat ≥ 2",而不是默默不提 * * 单轮场景关键:不是"稳定 = 100%"(常见误读),而是"测不到稳定性"。 * Verdict 必须诚实交代这个盲区,不能让用户以为没说就是 OK。 */ /** * 跨轮稳定性的中位 CV(variation coefficient = stddev/mean)。仅在**已测**(runs≥2)且 variance 数据齐时 * 返回 { runs, cv },否则 null(单轮未测 / variance 缺失 / CV 全算不出 → 不参与门控)。formatStability 的 * 文字、computeVerdict 的稳定性门控、renderer 的 hero CV chip 共用这一处计算,口径一致、绝不漂移。 * **真·中位**:偶数个 variant 取中间两项的平均(不是上中位)—— 最常见的 A/B 报告恰好是两个 variant,取上中位 * 会退化成"较大的那个 CV",把门控口径从「中位」悄悄变成「max」、直接改变 ship/no-ship(复审 P2)。 */ export declare function medianStabilityCV(report: Report): { runs: number; cv: number; } | null; interface VerdictTextOptions { verbose?: boolean; lang?: Lang; /** Managed skill NAME from `omk list`; omitted when the formatter cannot know it safely. */ promoteTarget?: string; } /** * Plain-text formatter for the `omk eval` verdict. Stays terse for the * spec — one verdict, rationale bullets, one ship recommendation, and one next step. */ export declare function formatVerdictText(result: VerdictResult, options?: VerdictTextOptions): string; export {};