fix(audit): enforce phase0 summary max-limit semantics
Validate maxSessions/maxChannels/maxSkipReasons as non-negative finite values, make 0 produce zero rows, and add regression coverage. No architecture/protocol flow changes; diagram files reviewed and no updates were needed.
This commit is contained in:
@@ -463,6 +463,18 @@
|
|||||||
],
|
],
|
||||||
"test_status": "pnpm test:run src/audit/phase0BaselineDrift.test.ts + pnpm typecheck passing"
|
"test_status": "pnpm test:run src/audit/phase0BaselineDrift.test.ts + pnpm typecheck passing"
|
||||||
},
|
},
|
||||||
|
"phase0-live-baseline-summary-limit-validation-hardening": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-27",
|
||||||
|
"updated": "2026-02-27",
|
||||||
|
"summary": "Hardened phase-0 baseline summary limit handling so `maxSessions`, `maxChannels`, and `maxSkipReasons` enforce non-negative finite values, treat `0` as zero rows, and reject negative limits with explicit errors.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/audit/phase0BaselineSummary.ts",
|
||||||
|
"src/audit/phase0BaselineSummary.test.ts",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/audit/phase0BaselineSummary.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",
|
||||||
|
|||||||
@@ -163,6 +163,65 @@ describe('summarizePhase0Baseline', () => {
|
|||||||
expect(summary.run_outcomes.by_channel).toHaveLength(1);
|
expect(summary.run_outcomes.by_channel).toHaveLength(1);
|
||||||
expect(summary.run_outcomes.by_channel[0]?.key).toBe('telegram');
|
expect(summary.run_outcomes.by_channel[0]?.key).toBe('telegram');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies max row limits including zero', () => {
|
||||||
|
const events: AuditEvent[] = [
|
||||||
|
makeEvent(1000, 'run.state', {
|
||||||
|
session_id: 's1',
|
||||||
|
channel: 'telegram',
|
||||||
|
sender: 'u1',
|
||||||
|
source: 'channel',
|
||||||
|
state: 'complete',
|
||||||
|
}),
|
||||||
|
makeEvent(1100, 'run.state', {
|
||||||
|
session_id: 's2',
|
||||||
|
channel: 'discord',
|
||||||
|
sender: 'u2',
|
||||||
|
source: 'gateway',
|
||||||
|
state: 'error',
|
||||||
|
}),
|
||||||
|
makeEvent(1200, 'reaction.skip', {
|
||||||
|
session_id: 's1',
|
||||||
|
channel: 'telegram',
|
||||||
|
sender: 'u1',
|
||||||
|
source: 'channel',
|
||||||
|
reason: 'no_match',
|
||||||
|
}),
|
||||||
|
makeEvent(1300, 'reaction.skip', {
|
||||||
|
session_id: 's2',
|
||||||
|
channel: 'discord',
|
||||||
|
sender: 'u2',
|
||||||
|
source: 'gateway',
|
||||||
|
reason: 'no_rules',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const none = summarizePhase0Baseline(events, {
|
||||||
|
maxChannels: 0,
|
||||||
|
maxSessions: 0,
|
||||||
|
maxSkipReasons: 0,
|
||||||
|
});
|
||||||
|
expect(none.run_outcomes.by_channel).toHaveLength(0);
|
||||||
|
expect(none.run_outcomes.by_session).toHaveLength(0);
|
||||||
|
expect(none.reactions.skip_reasons).toHaveLength(0);
|
||||||
|
|
||||||
|
const oneEach = summarizePhase0Baseline(events, {
|
||||||
|
maxChannels: 1,
|
||||||
|
maxSessions: 1,
|
||||||
|
maxSkipReasons: 1,
|
||||||
|
});
|
||||||
|
expect(oneEach.run_outcomes.by_channel).toHaveLength(1);
|
||||||
|
expect(oneEach.run_outcomes.by_session).toHaveLength(1);
|
||||||
|
expect(oneEach.reactions.skip_reasons).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects negative max limits', () => {
|
||||||
|
const events: AuditEvent[] = [];
|
||||||
|
|
||||||
|
expect(() => summarizePhase0Baseline(events, { maxSessions: -1 })).toThrow('maxSessions');
|
||||||
|
expect(() => summarizePhase0Baseline(events, { maxChannels: -1 })).toThrow('maxChannels');
|
||||||
|
expect(() => summarizePhase0Baseline(events, { maxSkipReasons: -1 })).toThrow('maxSkipReasons');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('renderPhase0BaselineMarkdown', () => {
|
describe('renderPhase0BaselineMarkdown', () => {
|
||||||
|
|||||||
@@ -119,6 +119,19 @@ function toPct(part: number, whole: number): number | null {
|
|||||||
return Math.round((part / whole) * 10000) / 100;
|
return Math.round((part / whole) * 10000) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeLimit(value: number | undefined, optionName: string): number | undefined {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
throw new Error(`${optionName} must be a finite number.`);
|
||||||
|
}
|
||||||
|
if (value < 0) {
|
||||||
|
throw new Error(`${optionName} must be greater than or equal to 0.`);
|
||||||
|
}
|
||||||
|
return Math.floor(value);
|
||||||
|
}
|
||||||
|
|
||||||
function percentile(sortedAscending: number[], pct: number): number {
|
function percentile(sortedAscending: number[], pct: number): number {
|
||||||
if (sortedAscending.length === 0) {
|
if (sortedAscending.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -183,7 +196,7 @@ function sortGroups(groups: Map<string, RunCounter>, limit?: number): RunOutcome
|
|||||||
return a.key.localeCompare(b.key);
|
return a.key.localeCompare(b.key);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof limit === 'number' && Number.isFinite(limit) && limit > 0) {
|
if (typeof limit === 'number' && Number.isFinite(limit)) {
|
||||||
return rows.slice(0, Math.floor(limit));
|
return rows.slice(0, Math.floor(limit));
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
@@ -199,7 +212,7 @@ function sortSkipReasons(reasons: Map<string, number>, limit?: number): Reaction
|
|||||||
pct: total > 0 ? Math.round((count / total) * 10000) / 100 : 0,
|
pct: total > 0 ? Math.round((count / total) * 10000) / 100 : 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (typeof limit === 'number' && Number.isFinite(limit) && limit > 0) {
|
if (typeof limit === 'number' && Number.isFinite(limit)) {
|
||||||
return rows.slice(0, Math.floor(limit));
|
return rows.slice(0, Math.floor(limit));
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
@@ -244,6 +257,10 @@ export function summarizePhase0Baseline(
|
|||||||
events: AuditEvent[],
|
events: AuditEvent[],
|
||||||
options: Phase0BaselineSummaryOptions = {},
|
options: Phase0BaselineSummaryOptions = {},
|
||||||
): Phase0BaselineSummary {
|
): Phase0BaselineSummary {
|
||||||
|
const maxSessions = normalizeLimit(options.maxSessions, 'maxSessions');
|
||||||
|
const maxChannels = normalizeLimit(options.maxChannels, 'maxChannels');
|
||||||
|
const maxSkipReasons = normalizeLimit(options.maxSkipReasons, 'maxSkipReasons');
|
||||||
|
|
||||||
const sessionFilter = new Set(options.sessionIds ?? []);
|
const sessionFilter = new Set(options.sessionIds ?? []);
|
||||||
const channelFilter = new Set(options.channels ?? []);
|
const channelFilter = new Set(options.channels ?? []);
|
||||||
const senderFilter = new Set(options.senders ?? []);
|
const senderFilter = new Set(options.senders ?? []);
|
||||||
@@ -342,8 +359,8 @@ export function summarizePhase0Baseline(
|
|||||||
event_counts: eventCounts,
|
event_counts: eventCounts,
|
||||||
run_outcomes: {
|
run_outcomes: {
|
||||||
overall: buildRunOutcomeStats(runTotals),
|
overall: buildRunOutcomeStats(runTotals),
|
||||||
by_channel: sortGroups(runByChannel, options.maxChannels),
|
by_channel: sortGroups(runByChannel, maxChannels),
|
||||||
by_session: sortGroups(runBySession, options.maxSessions),
|
by_session: sortGroups(runBySession, maxSessions),
|
||||||
},
|
},
|
||||||
cancel_latency_ms: computeLatencyStats(cancelLatencies),
|
cancel_latency_ms: computeLatencyStats(cancelLatencies),
|
||||||
reactions: {
|
reactions: {
|
||||||
@@ -352,7 +369,7 @@ export function summarizePhase0Baseline(
|
|||||||
total: totalReactions,
|
total: totalReactions,
|
||||||
match_rate_pct: toPct(reactionMatched, totalReactions),
|
match_rate_pct: toPct(reactionMatched, totalReactions),
|
||||||
skip_rate_pct: toPct(reactionSkipped, totalReactions),
|
skip_rate_pct: toPct(reactionSkipped, totalReactions),
|
||||||
skip_reasons: sortSkipReasons(skipReasons, options.maxSkipReasons),
|
skip_reasons: sortSkipReasons(skipReasons, maxSkipReasons),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user