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:
@@ -451,6 +451,18 @@
|
|||||||
],
|
],
|
||||||
"test_status": "pnpm test:run src/audit/phase0BaselineArtifactRetention.test.ts + pnpm typecheck passing"
|
"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": {
|
"phase0-instrumentation-ticket-checklist": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-25",
|
"date": "2026-02-25",
|
||||||
|
|||||||
@@ -337,4 +337,49 @@ describe('phase0BaselineDrift', () => {
|
|||||||
'sampled_events_drop_pct',
|
'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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,11 +69,14 @@ function readFiniteNumberOrNull(value: unknown): number | null {
|
|||||||
return typeof parsed === 'number' ? parsed : 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);
|
const parsed = readFiniteNumber(value);
|
||||||
if (typeof parsed !== 'number') {
|
if (typeof parsed !== 'number') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
if (parsed < 0) {
|
||||||
|
throw new Error(`${thresholdName} must be greater than or equal to 0.`);
|
||||||
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +166,7 @@ export function evaluatePhase0BaselineDriftGate(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const minCandidateSampledEvents = readThreshold(thresholds.minCandidateSampledEvents);
|
const minCandidateSampledEvents = readThreshold(thresholds.minCandidateSampledEvents, 'minCandidateSampledEvents');
|
||||||
if (typeof minCandidateSampledEvents === 'number') {
|
if (typeof minCandidateSampledEvents === 'number') {
|
||||||
criteria.push({
|
criteria.push({
|
||||||
criterion: 'candidate_sampled_events',
|
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 (typeof minBaselineSampledEvents === 'number') {
|
||||||
if (!baseline) {
|
if (!baseline) {
|
||||||
criteria.push({
|
criteria.push({
|
||||||
@@ -192,7 +195,7 @@ export function evaluatePhase0BaselineDriftGate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxSampledEventsDropPct = readThreshold(thresholds.maxSampledEventsDropPct);
|
const maxSampledEventsDropPct = readThreshold(thresholds.maxSampledEventsDropPct, 'maxSampledEventsDropPct');
|
||||||
if (typeof maxSampledEventsDropPct === 'number') {
|
if (typeof maxSampledEventsDropPct === 'number') {
|
||||||
const delta = comparison.deltas.sampled_event_count_pct;
|
const delta = comparison.deltas.sampled_event_count_pct;
|
||||||
if (delta === null) {
|
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') {
|
if (typeof maxRunOutcomesDropPct === 'number') {
|
||||||
const delta = comparison.deltas.run_total_outcomes_pct;
|
const delta = comparison.deltas.run_total_outcomes_pct;
|
||||||
if (delta === null) {
|
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') {
|
if (typeof maxCompletionRateDropPp === 'number') {
|
||||||
const delta = comparison.deltas.completion_rate_pp;
|
const delta = comparison.deltas.completion_rate_pp;
|
||||||
if (delta === null) {
|
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') {
|
if (typeof maxCancelRateIncreasePp === 'number') {
|
||||||
const delta = comparison.deltas.cancel_rate_pp;
|
const delta = comparison.deltas.cancel_rate_pp;
|
||||||
if (delta === null) {
|
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') {
|
if (typeof maxErrorRateIncreasePp === 'number') {
|
||||||
const delta = comparison.deltas.error_rate_pp;
|
const delta = comparison.deltas.error_rate_pp;
|
||||||
if (delta === null) {
|
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') {
|
if (typeof maxCancelLatencyP95IncreaseMs === 'number') {
|
||||||
const delta = comparison.deltas.cancel_latency_p95_ms;
|
const delta = comparison.deltas.cancel_latency_p95_ms;
|
||||||
if (delta === null) {
|
if (delta === null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user