import type { EvidenceSubject } from "./evidenceContext"; // Evidence address codec: URL = subject. This file owns the entire grammar — // every EvidenceSubject kind is addressable here, including kinds whose routes // are still placeholders (the grammar is fixed once; routes grow into it). // // turn -> /trace/ // proposal -> /proposals/ // eval_result -> /evals/ // artifact -> /replay/ // run -> /runs/ // pack -> /packs/ // vault_entry -> /vault?inspect=vault: // audit_event -> /audit?inspect=audit: // // The `inspect` query param carries the inspector's subject as `:`; // its presence means the inspector is open on that subject. export type AddressableSubject = Exclude; export const INSPECT_PARAM = "inspect"; export function isAddressable( subject: EvidenceSubject, ): subject is AddressableSubject { return subject.kind !== "none"; } export function sameIdentity(a: EvidenceSubject, b: EvidenceSubject): boolean { switch (a.kind) { case "turn": return b.kind === "turn" && b.turnId === a.turnId; case "proposal": return ( b.kind === "proposal" && b.proposalId === a.proposalId && (b.domain ?? "cognition") === (a.domain ?? "cognition") ); case "eval_result": return b.kind === "eval_result" && b.lane === a.lane; case "artifact": return b.kind === "artifact" && b.artifactId === a.artifactId; case "run": return b.kind === "run" && b.sessionId === a.sessionId; case "pack": return b.kind === "pack" && b.packId === a.packId; case "vault_entry": return b.kind === "vault_entry" && b.entryIndex === a.entryIndex; case "audit_event": return b.kind === "audit_event" && b.eventId === a.eventId; case "none": return b.kind === "none"; } } interface SubjectAddress { path: string; params: URLSearchParams; } function emptyParams(): URLSearchParams { return new URLSearchParams(); } function subjectAddress(subject: AddressableSubject): SubjectAddress { switch (subject.kind) { case "turn": return { path: `/trace/${subject.turnId}`, params: emptyParams() }; case "proposal": { const params = emptyParams(); if (subject.domain === "math") params.set("domain", "math"); return { path: `/proposals/${encodeURIComponent(subject.proposalId)}`, params, }; } case "eval_result": return { path: `/evals/${encodeURIComponent(subject.lane)}`, params: emptyParams() }; case "artifact": return { path: `/replay/${encodeURIComponent(subject.artifactId)}`, params: emptyParams() }; case "run": return { path: `/runs/${encodeURIComponent(subject.sessionId)}`, params: emptyParams() }; case "pack": return { path: `/packs/${encodeURIComponent(subject.packId)}`, params: emptyParams() }; case "vault_entry": { const params = emptyParams(); params.set(INSPECT_PARAM, subjectToInspectValue(subject)); return { path: "/vault", params }; } case "audit_event": { const params = emptyParams(); params.set(INSPECT_PARAM, subjectToInspectValue(subject)); return { path: "/audit", params }; } } } export function subjectToInspectValue(subject: AddressableSubject): string { switch (subject.kind) { case "turn": return `turn:${subject.turnId}`; case "proposal": return `proposal:${subject.proposalId}`; case "eval_result": return `eval_result:${subject.lane}`; case "artifact": return `artifact:${subject.artifactId}`; case "run": return `run:${subject.sessionId}`; case "pack": return `pack:${subject.packId}`; case "vault_entry": return `vault:${subject.entryIndex}`; case "audit_event": return `audit:${subject.eventId}`; } } export function subjectToUrl( subject: AddressableSubject, inspect?: EvidenceSubject | null, ): string { const address = subjectAddress(subject); const params = new URLSearchParams(address.params); if (inspect && isAddressable(inspect) && !sameIdentity(subject, inspect)) { params.set(INSPECT_PARAM, subjectToInspectValue(inspect)); } const query = params.toString(); return query ? `${address.path}?${query}` : address.path; } function parseTurnId(raw: string): number | null { if (!/^\d+$/.test(raw)) return null; const value = Number(raw); return Number.isSafeInteger(value) ? value : null; } export function inspectValueToSubject( value: string | null, ): AddressableSubject | null { if (value === null) return null; const sep = value.indexOf(":"); if (sep <= 0 || sep === value.length - 1) return null; const kind = value.slice(0, sep); const id = value.slice(sep + 1); switch (kind) { case "turn": { const turnId = parseTurnId(id); return turnId === null ? null : { kind: "turn", turnId }; } case "proposal": return { kind: "proposal", proposalId: id }; case "eval_result": return { kind: "eval_result", lane: id }; case "artifact": return { kind: "artifact", artifactId: id }; case "run": return { kind: "run", sessionId: id }; case "pack": return { kind: "pack", packId: id }; case "vault": { const entryIndex = parseTurnId(id); return entryIndex === null ? null : { kind: "vault_entry", entryIndex }; } case "audit": return { kind: "audit_event", eventId: id }; default: return null; } } function routeParamsToSubject( params: Readonly>, searchParams: URLSearchParams, ): AddressableSubject | null { // React Router populates exactly one of these keys per matched route; // precedence below only matters for hand-built (malformed) inputs. if (params.turnId !== undefined) { const turnId = parseTurnId(params.turnId); return turnId === null ? null : { kind: "turn", turnId }; } if (params.proposalId !== undefined) { if (params.proposalId === "") return null; return searchParams.get("domain") === "math" ? { kind: "proposal", proposalId: params.proposalId, domain: "math" } : { kind: "proposal", proposalId: params.proposalId }; } if (params.sessionId !== undefined) { return params.sessionId === "" ? null : { kind: "run", sessionId: params.sessionId }; } if (params.packId !== undefined) { return params.packId === "" ? null : { kind: "pack", packId: params.packId }; } if (params.laneId !== undefined) { return params.laneId === "" ? null : { kind: "eval_result", lane: params.laneId }; } if (params.artifactId !== undefined) { return params.artifactId === "" ? null : { kind: "artifact", artifactId: params.artifactId }; } return null; } export interface UrlSubjects { route: AddressableSubject | null; inspect: AddressableSubject | null; } // Total inverse of subjectToUrl: malformed input yields null, never throws. // Returned subjects carry identity only — `data` loads via the route's query. export function urlToSubject( params: Readonly>, searchParams: URLSearchParams, ): UrlSubjects { return { route: routeParamsToSubject(params, searchParams), inspect: inspectValueToSubject(searchParams.get(INSPECT_PARAM)), }; }