feat(audit): add backend-scoped phase0 live baseline capture
This commit is contained in:
@@ -75,5 +75,34 @@ describe('capturePhase0LiveBaselineEvents', () => {
|
||||
expect(first.request_id).not.toBe(second.request_id);
|
||||
expect(first.lane_id).not.toBe(second.lane_id);
|
||||
});
|
||||
});
|
||||
|
||||
it('filters phase-0 events by backend route timelines when backend targets are provided', () => {
|
||||
const events: AuditEvent[] = [
|
||||
event(15, 'run.state', { session_id: 's1', channel: 'gmail', sender: 'u1', source: 'channel', state: 'start' }),
|
||||
event(20, 'reaction.skip', { session_id: 's1', channel: 'gmail', sender: 'u1', source: 'channel', reason: 'no_rules', candidate_count: 0 }),
|
||||
event(35, 'run.state', { session_id: 's1', channel: 'gmail', sender: 'u1', source: 'channel', state: 'complete' }),
|
||||
event(45, 'run.state', { session_id: 's2', channel: 'gmail', sender: 'u2', source: 'channel', state: 'complete' }),
|
||||
event(55, 'run.state', { session_id: 's3', channel: 'gmail', sender: 'u3', source: 'channel', state: 'start' }),
|
||||
];
|
||||
|
||||
const backendRouteEvents: AuditEvent[] = [
|
||||
event(10, 'backend.route', { session_id: 's1', selected_backend: 'pi_embedded' }),
|
||||
event(30, 'backend.route', { session_id: 's1', selected_backend: 'native' }),
|
||||
event(40, 'backend.route', { session_id: 's2', selected_backend: 'pi_embedded' }),
|
||||
];
|
||||
|
||||
const piOnly = capturePhase0LiveBaselineEvents(events, {
|
||||
backendTargets: ['pi_embedded'],
|
||||
backendRouteEvents,
|
||||
anonymizeIdentifiers: false,
|
||||
});
|
||||
expect(piOnly.map((entry) => entry.timestamp)).toEqual([15, 20, 45]);
|
||||
|
||||
const nativeOnly = capturePhase0LiveBaselineEvents(events, {
|
||||
backendTargets: ['native'],
|
||||
backendRouteEvents,
|
||||
anonymizeIdentifiers: false,
|
||||
});
|
||||
expect(nativeOnly.map((entry) => entry.timestamp)).toEqual([35]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ import { createHash } from 'node:crypto';
|
||||
import type { AuditEvent, AuditEventType } from './types.js';
|
||||
import type { AuditSource } from './phase0BaselineSummary.js';
|
||||
|
||||
export type Phase0BackendTarget = 'native' | 'claude_code' | 'opencode' | 'codex' | 'gemini' | 'pi_embedded';
|
||||
|
||||
const PHASE0_BASELINE_EVENT_TYPES: readonly AuditEventType[] = [
|
||||
'run.state',
|
||||
'run.cancel',
|
||||
@@ -9,9 +11,20 @@ const PHASE0_BASELINE_EVENT_TYPES: readonly AuditEventType[] = [
|
||||
'reaction.skip',
|
||||
];
|
||||
|
||||
const BACKEND_TARGETS: readonly Phase0BackendTarget[] = [
|
||||
'native',
|
||||
'claude_code',
|
||||
'opencode',
|
||||
'codex',
|
||||
'gemini',
|
||||
'pi_embedded',
|
||||
];
|
||||
|
||||
export interface CapturePhase0LiveBaselineOptions {
|
||||
channels?: string[];
|
||||
sources?: AuditSource[];
|
||||
backendTargets?: Phase0BackendTarget[];
|
||||
backendRouteEvents?: AuditEvent[];
|
||||
excludeSessionSubstrings?: string[];
|
||||
anonymizeIdentifiers?: boolean;
|
||||
}
|
||||
@@ -27,6 +40,57 @@ function toPayload(value: unknown): Record<string, unknown> {
|
||||
: {};
|
||||
}
|
||||
|
||||
function isBackendTarget(value: string): value is Phase0BackendTarget {
|
||||
return BACKEND_TARGETS.includes(value as Phase0BackendTarget);
|
||||
}
|
||||
|
||||
function buildBackendRouteTimeline(
|
||||
events: AuditEvent[],
|
||||
): Map<string, Array<{ at: number; backend: Phase0BackendTarget }>> {
|
||||
const bySession = new Map<string, Array<{ at: number; backend: Phase0BackendTarget }>>();
|
||||
|
||||
for (const event of events) {
|
||||
if (event.event_type !== 'backend.route') {
|
||||
continue;
|
||||
}
|
||||
const payload = toPayload(event.event);
|
||||
const sessionId = readStringField(payload, 'session_id');
|
||||
const selectedBackend = readStringField(payload, 'selected_backend');
|
||||
if (!sessionId || !selectedBackend || !isBackendTarget(selectedBackend)) {
|
||||
continue;
|
||||
}
|
||||
const rows = bySession.get(sessionId) ?? [];
|
||||
rows.push({ at: event.timestamp, backend: selectedBackend });
|
||||
bySession.set(sessionId, rows);
|
||||
}
|
||||
|
||||
for (const rows of bySession.values()) {
|
||||
rows.sort((a, b) => a.at - b.at);
|
||||
}
|
||||
|
||||
return bySession;
|
||||
}
|
||||
|
||||
function resolveBackendForEvent(
|
||||
timelineBySession: Map<string, Array<{ at: number; backend: Phase0BackendTarget }>>,
|
||||
sessionId: string,
|
||||
timestamp: number,
|
||||
): Phase0BackendTarget | undefined {
|
||||
const timeline = timelineBySession.get(sessionId);
|
||||
if (!timeline || timeline.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let selected: Phase0BackendTarget | undefined;
|
||||
for (const row of timeline) {
|
||||
if (row.at > timestamp) {
|
||||
break;
|
||||
}
|
||||
selected = row.backend;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
function hashIdentifier(prefix: string, value: string): string {
|
||||
const digest = createHash('sha256').update(value).digest('hex').slice(0, 12);
|
||||
return `${prefix}_${digest}`;
|
||||
@@ -61,6 +125,10 @@ export function capturePhase0LiveBaselineEvents(
|
||||
): AuditEvent[] {
|
||||
const channelFilter = new Set((options.channels ?? []).filter((value) => value.length > 0));
|
||||
const sourceFilter = new Set(options.sources ?? []);
|
||||
const backendFilter = new Set((options.backendTargets ?? []).filter((value) => value.length > 0));
|
||||
const backendTimelineBySession = backendFilter.size > 0
|
||||
? buildBackendRouteTimeline(options.backendRouteEvents ?? [])
|
||||
: new Map<string, Array<{ at: number; backend: Phase0BackendTarget }>>();
|
||||
const excludeSessionSubstrings = (options.excludeSessionSubstrings ?? [])
|
||||
.map((value) => value.trim().toLowerCase())
|
||||
.filter((value) => value.length > 0);
|
||||
@@ -90,6 +158,15 @@ export function capturePhase0LiveBaselineEvents(
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (backendFilter.size > 0) {
|
||||
if (!sessionId) {
|
||||
continue;
|
||||
}
|
||||
const selectedBackend = resolveBackendForEvent(backendTimelineBySession, sessionId, event.timestamp);
|
||||
if (!selectedBackend || !backendFilter.has(selectedBackend)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const nextPayload = anonymizeIdentifiers
|
||||
? anonymizePayloadIdentifiers(payload)
|
||||
@@ -103,4 +180,3 @@ export function capturePhase0LiveBaselineEvents(
|
||||
|
||||
return filtered.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user