From fd0ab6e6dfb3971e6c207c0cf9a16b0a81113717 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Fri, 27 Feb 2026 13:06:52 -0800 Subject: [PATCH] fix(audit): validate non-negative drift thresholds Reject negative phase0 drift gate thresholds with explicit parameter names and add regression tests. No architecture/protocol flow changes; diagram files reviewed and no updates were needed. --- docs/plans/state.json | 12 +++++++ src/audit/phase0BaselineDrift.test.ts | 45 +++++++++++++++++++++++++++ src/audit/phase0BaselineDrift.ts | 21 +++++++------ 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/docs/plans/state.json b/docs/plans/state.json index 2e5f429..a6de620 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -451,6 +451,18 @@ ], "test_status": "pnpm test:run src/audit/phase0BaselineArtifactRetention.test.ts + pnpm typecheck passing" }, + "phase0-live-baseline-drift-threshold-validation-hardening": { + "status": "completed", + "date": "2026-02-27", + "updated": "2026-02-27", + "summary": "Hardened phase-0 drift gate threshold handling by rejecting negative threshold values with explicit errors, preventing nonsensical gate configurations from silently producing misleading pass/fail outcomes.", + "files_modified": [ + "src/audit/phase0BaselineDrift.ts", + "src/audit/phase0BaselineDrift.test.ts", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/audit/phase0BaselineDrift.test.ts + pnpm typecheck passing" + }, "phase0-instrumentation-ticket-checklist": { "status": "completed", "date": "2026-02-25", diff --git a/src/audit/phase0BaselineDrift.test.ts b/src/audit/phase0BaselineDrift.test.ts index c85b827..d24e45d 100644 --- a/src/audit/phase0BaselineDrift.test.ts +++ b/src/audit/phase0BaselineDrift.test.ts @@ -337,4 +337,49 @@ describe('phase0BaselineDrift', () => { 'sampled_events_drop_pct', ]); }); + + it('rejects negative threshold values', () => { + const comparison = comparePhase0BaselineDrift({ + sampled_event_count: 10, + summary: { + event_counts: { + run_state: 0, + run_cancel: 0, + reaction_match: 0, + reaction_skip: 0, + }, + run_outcomes: { + overall: { + total_outcomes: 5, + complete: 4, + cancelled: 1, + error: 0, + cancel_requested: 0, + start: 5, + completion_rate_pct: 80, + cancel_rate_pct: 20, + error_rate_pct: 0, + }, + by_channel: [], + by_session: [], + }, + cancel_latency_ms: null, + reactions: { + matched: 0, + skipped: 0, + total: 0, + match_rate_pct: null, + skip_rate_pct: null, + skip_reasons: [], + }, + }, + }); + + expect(() => evaluatePhase0BaselineDriftGate(comparison, { + maxSampledEventsDropPct: -1, + })).toThrow('maxSampledEventsDropPct'); + expect(() => evaluatePhase0BaselineDriftGate(comparison, { + minCandidateSampledEvents: -5, + })).toThrow('minCandidateSampledEvents'); + }); }); diff --git a/src/audit/phase0BaselineDrift.ts b/src/audit/phase0BaselineDrift.ts index e1035a0..94f19b1 100644 --- a/src/audit/phase0BaselineDrift.ts +++ b/src/audit/phase0BaselineDrift.ts @@ -69,11 +69,14 @@ function readFiniteNumberOrNull(value: unknown): number | null { return typeof parsed === 'number' ? parsed : null; } -function readThreshold(value: unknown): number | undefined { +function readThreshold(value: unknown, thresholdName: string): number | undefined { const parsed = readFiniteNumber(value); if (typeof parsed !== 'number') { return undefined; } + if (parsed < 0) { + throw new Error(`${thresholdName} must be greater than or equal to 0.`); + } return parsed; } @@ -163,7 +166,7 @@ export function evaluatePhase0BaselineDriftGate( }); } - const minCandidateSampledEvents = readThreshold(thresholds.minCandidateSampledEvents); + const minCandidateSampledEvents = readThreshold(thresholds.minCandidateSampledEvents, 'minCandidateSampledEvents'); if (typeof minCandidateSampledEvents === 'number') { criteria.push({ criterion: 'candidate_sampled_events', @@ -173,7 +176,7 @@ export function evaluatePhase0BaselineDriftGate( }); } - const minBaselineSampledEvents = readThreshold(thresholds.minBaselineSampledEvents); + const minBaselineSampledEvents = readThreshold(thresholds.minBaselineSampledEvents, 'minBaselineSampledEvents'); if (typeof minBaselineSampledEvents === 'number') { if (!baseline) { criteria.push({ @@ -192,7 +195,7 @@ export function evaluatePhase0BaselineDriftGate( } } - const maxSampledEventsDropPct = readThreshold(thresholds.maxSampledEventsDropPct); + const maxSampledEventsDropPct = readThreshold(thresholds.maxSampledEventsDropPct, 'maxSampledEventsDropPct'); if (typeof maxSampledEventsDropPct === 'number') { const delta = comparison.deltas.sampled_event_count_pct; if (delta === null) { @@ -213,7 +216,7 @@ export function evaluatePhase0BaselineDriftGate( } } - const maxRunOutcomesDropPct = readThreshold(thresholds.maxRunOutcomesDropPct); + const maxRunOutcomesDropPct = readThreshold(thresholds.maxRunOutcomesDropPct, 'maxRunOutcomesDropPct'); if (typeof maxRunOutcomesDropPct === 'number') { const delta = comparison.deltas.run_total_outcomes_pct; if (delta === null) { @@ -234,7 +237,7 @@ export function evaluatePhase0BaselineDriftGate( } } - const maxCompletionRateDropPp = readThreshold(thresholds.maxCompletionRateDropPp); + const maxCompletionRateDropPp = readThreshold(thresholds.maxCompletionRateDropPp, 'maxCompletionRateDropPp'); if (typeof maxCompletionRateDropPp === 'number') { const delta = comparison.deltas.completion_rate_pp; if (delta === null) { @@ -255,7 +258,7 @@ export function evaluatePhase0BaselineDriftGate( } } - const maxCancelRateIncreasePp = readThreshold(thresholds.maxCancelRateIncreasePp); + const maxCancelRateIncreasePp = readThreshold(thresholds.maxCancelRateIncreasePp, 'maxCancelRateIncreasePp'); if (typeof maxCancelRateIncreasePp === 'number') { const delta = comparison.deltas.cancel_rate_pp; if (delta === null) { @@ -276,7 +279,7 @@ export function evaluatePhase0BaselineDriftGate( } } - const maxErrorRateIncreasePp = readThreshold(thresholds.maxErrorRateIncreasePp); + const maxErrorRateIncreasePp = readThreshold(thresholds.maxErrorRateIncreasePp, 'maxErrorRateIncreasePp'); if (typeof maxErrorRateIncreasePp === 'number') { const delta = comparison.deltas.error_rate_pp; if (delta === null) { @@ -297,7 +300,7 @@ export function evaluatePhase0BaselineDriftGate( } } - const maxCancelLatencyP95IncreaseMs = readThreshold(thresholds.maxCancelLatencyP95IncreaseMs); + const maxCancelLatencyP95IncreaseMs = readThreshold(thresholds.maxCancelLatencyP95IncreaseMs, 'maxCancelLatencyP95IncreaseMs'); if (typeof maxCancelLatencyP95IncreaseMs === 'number') { const delta = comparison.deltas.cancel_latency_p95_ms; if (delta === null) {