fix(audit): require integer drift minimum sample thresholds
Enforce non-negative integer minCandidateSampledEvents/minBaselineSampledEvents in drift gate evaluation and CLI parsing; add regression coverage. Architecture/protocol diagrams reviewed and no updates were needed for this validation-only change.
This commit is contained in:
@@ -382,4 +382,49 @@ describe('phase0BaselineDrift', () => {
|
||||
minCandidateSampledEvents: -5,
|
||||
})).toThrow('minCandidateSampledEvents');
|
||||
});
|
||||
|
||||
it('rejects non-integer sampled-event minimum thresholds', () => {
|
||||
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, {
|
||||
minCandidateSampledEvents: 10.5,
|
||||
})).toThrow('minCandidateSampledEvents');
|
||||
expect(() => evaluatePhase0BaselineDriftGate(comparison, {
|
||||
minBaselineSampledEvents: 8.2,
|
||||
})).toThrow('minBaselineSampledEvents');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,6 +80,17 @@ function readThreshold(value: unknown, thresholdName: string): number | undefine
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function readIntegerThreshold(value: unknown, thresholdName: string): number | undefined {
|
||||
const parsed = readThreshold(value, thresholdName);
|
||||
if (parsed === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (!Number.isInteger(parsed)) {
|
||||
throw new Error(`${thresholdName} must be an integer greater than or equal to 0.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function toPctDelta(baseline: number, candidate: number): number | null {
|
||||
if (!Number.isFinite(baseline) || baseline <= 0 || !Number.isFinite(candidate)) {
|
||||
return null;
|
||||
@@ -166,7 +177,7 @@ export function evaluatePhase0BaselineDriftGate(
|
||||
});
|
||||
}
|
||||
|
||||
const minCandidateSampledEvents = readThreshold(thresholds.minCandidateSampledEvents, 'minCandidateSampledEvents');
|
||||
const minCandidateSampledEvents = readIntegerThreshold(thresholds.minCandidateSampledEvents, 'minCandidateSampledEvents');
|
||||
if (typeof minCandidateSampledEvents === 'number') {
|
||||
criteria.push({
|
||||
criterion: 'candidate_sampled_events',
|
||||
@@ -176,7 +187,7 @@ export function evaluatePhase0BaselineDriftGate(
|
||||
});
|
||||
}
|
||||
|
||||
const minBaselineSampledEvents = readThreshold(thresholds.minBaselineSampledEvents, 'minBaselineSampledEvents');
|
||||
const minBaselineSampledEvents = readIntegerThreshold(thresholds.minBaselineSampledEvents, 'minBaselineSampledEvents');
|
||||
if (typeof minBaselineSampledEvents === 'number') {
|
||||
if (!baseline) {
|
||||
criteria.push({
|
||||
|
||||
Reference in New Issue
Block a user