feat(audit): add backend-scoped phase0 live baseline capture

This commit is contained in:
William Valentin
2026-02-27 08:47:31 -08:00
parent a97cc9dc95
commit 68cdc2cf8b
20 changed files with 1055 additions and 91 deletions
+30 -1
View File
@@ -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]);
});
});
+77 -1
View File
@@ -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);
}