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.
This commit is contained in:
William Valentin
2026-02-27 13:06:52 -08:00
parent ad395bbdd6
commit fd0ab6e6df
3 changed files with 69 additions and 9 deletions
+45
View File
@@ -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');
});
});
+12 -9
View File
@@ -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) {