Merge pull request #722 from AssetOverflow/feat/wb-r3-replay-moment

feat(workbench): the Replay Moment — turn-keyed hero + retire W-026 artifact replay (Wave R3)
This commit is contained in:
Shay 2026-06-12 22:17:18 -07:00 committed by GitHub
commit 12df5dde43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 678 additions and 1113 deletions

View file

@ -0,0 +1,112 @@
# Wave R3 Briefs — Theater (the wow, honest)
Date: 2026-06-13
Plan: `docs/workbench/wave-R-mastery-revamp.md` § Wave R3.
Predecessor: Wave R2 complete (#714#721; all six routes real). Replay
backend already merged (#716, `GET /replay/{turn_id}``TurnReplayComparison`).
Four pieces. The Replay Moment is first (its backend exists and the old
artifact-keyed `ReplayRoute` is now stale against it). The others are
independent and can be authored after.
## Standing constraints (all briefs)
Same as Wave R2: worktree off fresh `origin/main`; green-local before push
(`pnpm build && pnpm test`); **STOP after checks green, Shay merges**;
token-only styling (hexScan); shrink-only `NOT_YET_MIRRORED`; conformance
rows (ADR-0162 §6); no new mutation endpoints; no invented data; no
force-directed / nondeterministic layout — ever.
---
## Brief R3-Replay — The Replay Moment (first; reworks + retires)
The hero flow. Turn-keyed sealed replay made *felt*: same prompt, same
genesis substrate → bit-identical envelope, rendered as hash≡hash with an
honest leaf diff. Consumes the merged #716 backend.
**This PR also retires the dead W-026 artifact-keyed replay machinery**
(confirmed zero serving uses) on both sides — the `NOT_YET_MIRRORED` comment
already anticipates it.
### Retirement surface (verified)
- **Python** (`workbench/schemas.py`): delete `ReplayDivergenceSeverity`,
`ReplayStatus`, `ReplayDivergence`, `ReplayComparison` (the artifact-keyed
block). Keep the `TurnReplay*` block. Zero word-boundary uses elsewhere.
- **Snapshots**: regenerate both — `pnpm schema:snapshot` (drops
`ReplayComparison`/`ReplayDivergence`) and `pnpm enum:snapshot` (drops
`ReplayStatus`/`ReplayDivergenceSeverity`).
- **TS** (`types/api.ts`): remove `ReplayComparison`/`ReplayDivergence`/
`ReplayDivergenceSeverity`; add `TurnReplayComparison`/`TurnReplayDivergence`
(+ the `TurnReplayDivergenceSeverity`/`TurnReplayBasis`/`TurnReplayOriginState`
unions). Remove both `TurnReplay*` entries from `NOT_YET_MIRRORED`.
- **Badges** (`design/components/badges/{types,mappings,index}.ts*`): remove
`ReplayStatusBadge` + `ReplayDivergenceSeverityBadge` + their meta/types;
drop their two cases from `enumCoverage.test.ts`. The hero renders the new
severity (`critical`/`informational`) inline with a `≠` glyph — no new
enum-tracked badge.
- **API** (`client.ts`/`queries.ts`): remove `fetchReplayComparison`/
`useReplayComparison`; add `fetchTurnReplay(turnId)``/replay/<turnId>` and
`useTurnReplay(turnId)`.
- **Route dir** (`app/replay/`): delete `ArtifactList`, `ReplayComparisonPanel`,
`ReplayDiffViewer`, `ReplayMetadataTable`, and the old `replay.test.tsx`.
### New ReplayRoute (turn-keyed hero)
- List journaled turns (reuse `useTraceTurns`) in the left pane (VirtualizedList
+ useListNavigation + SearchInput), same as Trace.
- Select → navigate `/replay/<turnId>` (replace) → `useTurnReplay(turnId)`.
- Hero: `original_trace_hash` vs `replay_trace_hash` rendered big; a clear
`≡ equivalent` (when `equivalent`) or `≠ diverged` verdict. **Honesty card**
states `comparison_basis` (`sealed_fresh_runtime_single_turn`) and
`origin_state` (`unrecorded`) — a divergence means nondeterminism OR
origin-state influence, never disambiguated; never render divergence as a
determinism-failure verdict.
- Leaf diff: each `divergence` as a row with `path`, original vs replay, and a
`≠` glyph; `critical` weighted above `informational`; informational
divergences (timestamp/cost/digest) explicitly labeled as expected.
- Publishes the `turn` subject for the inspector (identity → detail), like Trace.
- App route `path="replay/:turnId?"`; conformance row turn-keyed
(loading "Loading turns...", empty "No turns recorded yet. Use Chat to
create evidence." + `core chat`).
- Tests: hero equivalence (hash≡hash), a tampered-leaf divergence renders `≠` at
the right path, informational-only divergence still reads equivalent, the
honesty fields render, j/k spine, replace-mode URL selection.
### Verification
```bash
cd workbench-ui && pnpm build && pnpm test
# plus the Python lane the snapshots feed:
uv run python scripts/dump-schemas.py | diff - workbench-ui/schema-snapshot.json
uv run python scripts/dump-enums.py | diff - workbench-ui/enum-snapshot.json
```
---
## Brief R3-DAG — Deterministic DAG viewer
One hand-rolled component (`design/components/Dag/`): longest-path layering,
lexicographic tie-break, ~150 lines, **golden-file layout tests**, no graph
dependency (force-directed = doctrine violation). Pan/zoom/click-node →
inspector. Consumers: proposal chains, the 8 PCCP proof-promotion scenarios
(`demos/proof_carrying_promotion`), entailment traces. Pure layout function;
golden tests pin node coordinates.
---
## Brief R3-Demo — Demo Theater route
`GET /demos`, `POST /demos/{id}/run` (new read + a scoped run endpoint).
Scenario results with evidence-class badges and "what this proves / what this
does not prove" honesty cards; proposer-was-wrong scenarios visually
highlighted. Backend brief first (Python), then the route.
---
## Brief R3-Ledger — wrong=0 ledger view
Evals renders the correct/refused/wrong triplet as the PRIMARY visualization;
refusal reasons inspectable; failures-first ordering. Additive to the existing
EvalsRoute — no retirement, lowest risk of the four.

View file

@ -55,8 +55,6 @@ snapshot = {
"GroundingSource": literal_values(ROOT / "core" / "epistemic_state.py", "GroundingSource"),
"NormativeClearance": enum_values(ROOT / "core" / "epistemic_state.py", "NormativeClearance"),
"ReviewState": literal_values(ROOT / "teaching" / "proposals.py", "ReviewState"),
"ReplayDivergenceSeverity": literal_values(ROOT / "workbench" / "schemas.py", "ReplayDivergenceSeverity"),
"ReplayStatus": literal_values(ROOT / "workbench" / "schemas.py", "ReplayStatus"),
}
print(json.dumps(snapshot, indent=2, sort_keys=True))

View file

@ -30,17 +30,6 @@
"unassessable",
"suppressed"
],
"ReplayDivergenceSeverity": [
"info",
"warning",
"failure"
],
"ReplayStatus": [
"equivalent",
"not_yet_replayed",
"diverged",
"evidence_unavailable"
],
"ReviewState": [
"pending",
"accepted",

View file

@ -130,19 +130,6 @@
"created_at",
"downstream_effect"
],
"ReplayComparison": [
"artifact_id",
"original_hash",
"replay_hash",
"equivalent",
"divergences"
],
"ReplayDivergence": [
"path",
"original",
"replay",
"severity"
],
"RunDetail": [
"turns",
"manifest"

View file

@ -6,7 +6,7 @@ import type {
EvalRunResult,
ArtifactRef,
ArtifactDetail,
ReplayComparison,
TurnReplayComparison,
ProposalDetail,
ProposalState,
ProposalSummary,
@ -92,8 +92,8 @@ export async function fetchArtifactDetail(artifactId: string): Promise<ArtifactD
return apiFetch<ArtifactDetail>(`/artifacts/${artifactId}`);
}
export async function fetchReplayComparison(artifactId: string): Promise<ReplayComparison> {
return apiFetch<ReplayComparison>(`/replay/${artifactId}`);
export async function fetchTurnReplay(turnId: number): Promise<TurnReplayComparison> {
return apiFetch<TurnReplayComparison>(`/replay/${encodeURIComponent(String(turnId))}`);
}
export type ProposalStateFilter = ProposalState | "all";

View file

@ -10,7 +10,7 @@ import {
runEvalLane,
fetchArtifacts,
fetchArtifactDetail,
fetchReplayComparison,
fetchTurnReplay,
fetchProposalDetail,
fetchProposals,
fetchAuditEvents,
@ -49,7 +49,7 @@ import type {
EvalRunResult,
ChatTurnResult,
EvalRunRequest,
ReplayComparison,
TurnReplayComparison,
MathProposalSummary,
MathProposalDetail,
MathRatifyResult,
@ -99,11 +99,14 @@ export function useArtifactDetail(artifactId: string) {
});
}
export function useReplayComparison(artifactId: string) {
return useQuery<ReplayComparison, WorkbenchApiError>({
queryKey: ["api", "replay", artifactId],
queryFn: () => fetchReplayComparison(artifactId),
enabled: !!artifactId,
export function useTurnReplay(turnId?: number | null) {
return useQuery<TurnReplayComparison, WorkbenchApiError>({
queryKey: ["api", "replay", "turn", turnId ?? null],
queryFn: () => fetchTurnReplay(turnId as number),
enabled: typeof turnId === "number",
retry: false,
staleTime: 30_000,
refetchOnWindowFocus: false,
});
}

View file

@ -24,7 +24,7 @@ export function App() {
<Route index element={<Navigate to={`/${getWorkbenchPrefs().landingRoute}`} replace />} />
<Route path="chat" element={<ChatRoute />} />
<Route path="trace/:turnId?" element={<TraceRoute />} />
<Route path="replay/:artifactId?" element={<ReplayRoute />} />
<Route path="replay/:turnId?" element={<ReplayRoute />} />
<Route path="proposals/:proposalId?" element={<ProposalsRoute />} />
<Route path="evals/:laneId?" element={<EvalsRoute />} />
<Route path="runs/:sessionId?" element={<RunsRoute />} />

View file

@ -1,135 +0,0 @@
import type { ArtifactRef } from "../../types/api";
import { cn } from "../../design/lib";
import { EmptyState } from "../../design/components/states/EmptyState";
import { useListNavigation } from "../../design/hooks/useListNavigation";
interface ArtifactListProps {
artifacts: ArtifactRef[];
selectedId: string | null;
onSelect: (id: string) => void;
}
const KINDS = [
"trace",
"eval_result",
"proposal",
"contemplation_report",
"telemetry",
"engine_state_manifest",
"unknown",
] as const;
type ArtifactKind = (typeof KINDS)[number];
export function ArtifactList({ artifacts, selectedId, onSelect }: ArtifactListProps) {
// Initialize grouped structure
const grouped: Record<ArtifactKind, ArtifactRef[]> = {
trace: [],
eval_result: [],
proposal: [],
contemplation_report: [],
telemetry: [],
engine_state_manifest: [],
unknown: [],
};
artifacts.forEach((art) => {
const kind = KINDS.includes(art.kind as ArtifactKind)
? (art.kind as ArtifactKind)
: "unknown";
grouped[kind].push(art);
});
// Sort each group
KINDS.forEach((kind) => {
grouped[kind].sort((a, b) => {
if (a.created_at && b.created_at) {
return b.created_at.localeCompare(a.created_at);
}
if (a.created_at) return -1;
if (b.created_at) return 1;
return a.artifact_id.localeCompare(b.artifact_id);
});
});
// Flat traversal order across groups for the keyboard spine (R0d).
const flatOrder = KINDS.flatMap((kind) => grouped[kind]);
const flatIndexById = new Map(flatOrder.map((a, i) => [a.artifact_id, i]));
const { listProps, itemProps } = useListNavigation({
itemCount: flatOrder.length,
onActivate: (index) => {
const art = flatOrder[index];
if (art) onSelect(art.artifact_id);
},
});
if (artifacts.length === 0) {
return (
<div className="p-2" data-testid="artifact-list-empty">
<EmptyState
statement="No artifacts available."
nextAction={{ kind: "cli", command: "core eval cognition" }}
/>
</div>
);
}
return (
<nav
className="flex h-full flex-col gap-4 overflow-y-auto pr-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]"
aria-label="Artifact list navigation"
data-testid="artifact-list"
{...listProps}
>
{KINDS.map((kind) => {
const items = grouped[kind];
if (items.length === 0) return null;
return (
<div key={kind} className="space-y-1" data-testid={`group-${kind}`}>
<h4 className="px-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
{kind.replace(/_/g, " ")}
</h4>
<div className="space-y-0.5">
{items.map((art) => {
const isSelected = art.artifact_id === selectedId;
const { ref: rowRef, ...rowProps } = itemProps(
flatIndexById.get(art.artifact_id) ?? 0,
);
return (
<button
key={art.artifact_id}
{...rowProps}
ref={rowRef}
onClick={() => onSelect(art.artifact_id)}
type="button"
className={cn(
"w-full rounded px-2 py-1.5 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]",
isSelected
? "bg-[var(--color-selected-bg)] text-[var(--color-text-primary)] border-l-2 border-[var(--color-selected-border)] pl-1.5"
: "text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text-primary)]"
)}
aria-current={isSelected ? "true" : undefined}
data-testid={`artifact-${art.artifact_id}`}
>
<div className="truncate text-xs font-medium font-mono">
{art.artifact_id}
</div>
{art.created_at && (
<div className="text-[10px] text-[var(--color-text-muted)] mt-0.5">
{new Date(art.created_at).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</div>
)}
</button>
);
})}
</div>
</div>
);
})}
</nav>
);
}

View file

@ -1,84 +0,0 @@
import type { ArtifactDetail, ReplayComparison } from "../../types/api";
import { ReplayStatusBadge, TraceHashBadge, ReplayStatus } from "../../design/components/badges";
import { EmptyState } from "../../design/components/states/EmptyState";
import { ReplayDiffViewer } from "./ReplayDiffViewer";
import { ReplayMetadataTable } from "./ReplayMetadataTable";
interface ReplayComparisonPanelProps {
artifact: ArtifactDetail;
comparison?: ReplayComparison | null;
status: ReplayStatus;
}
export function ReplayComparisonPanel({
artifact,
comparison,
status,
}: ReplayComparisonPanelProps) {
const finalComparison: ReplayComparison = comparison || {
artifact_id: artifact.artifact_id,
original_hash: artifact.digest,
replay_hash: null,
equivalent: false,
divergences: [],
};
return (
<div className="space-y-6" data-testid="replay-comparison-panel">
<div className="flex items-center justify-between border-b border-[var(--color-border-subtle)] pb-4">
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">Replay Evidence</h2>
<ReplayStatusBadge value={status} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-4 flex flex-col gap-2">
<span className="text-[10px] font-semibold text-[var(--color-text-muted)] uppercase tracking-wider">
Original Hash
</span>
<div>
{finalComparison.original_hash ? (
<TraceHashBadge value={finalComparison.original_hash} />
) : (
<span className="text-xs font-mono text-[var(--color-text-muted)]">not_available</span>
)}
</div>
</div>
<div className="rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-4 flex flex-col gap-2">
<span className="text-[10px] font-semibold text-[var(--color-text-muted)] uppercase tracking-wider">
Replay Hash
</span>
<div>
{finalComparison.replay_hash ? (
<TraceHashBadge value={finalComparison.replay_hash} />
) : (
<span className="text-xs font-mono text-[var(--color-text-muted)]">not_available</span>
)}
</div>
</div>
</div>
<div className="space-y-4">
{status === "evidence_unavailable" ? (
<EmptyState
statement="Replay evidence is not available on the backend for this artifact kind."
nextAction="Verify the backend replay path configuration."
/>
) : status === "equivalent" ? (
<EmptyState
statement="Replay evidence intact — no divergences."
nextAction={{ kind: "cli", command: "core test --suite runtime" }}
/>
) : status === "not_yet_replayed" ? (
<EmptyState
statement="No replay has been attempted for this artifact yet."
nextAction={{ kind: "cli", command: `core replay ${artifact.artifact_id}` }}
/>
) : (
<ReplayDiffViewer divergences={finalComparison.divergences} />
)}
</div>
<ReplayMetadataTable artifact={artifact} comparison={finalComparison} />
</div>
);
}

View file

@ -1,92 +0,0 @@
import type { ReplayDivergence } from "../../types/api";
import { ReplayDivergenceSeverityBadge, ReplayDivergenceSeverity } from "../../design/components/badges";
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
import { Copy } from "lucide-react";
import { Button } from "../../design/components/primitives/Button";
interface ReplayDiffViewerProps {
divergences: ReplayDivergence[];
}
const severityOrder: Record<"info" | "warning" | "failure", number> = {
failure: 3,
warning: 2,
info: 1,
};
function serializeValue(val: unknown): string {
if (val === undefined) return "";
try {
return JSON.stringify(val);
} catch {
return String(val);
}
}
export function ReplayDiffViewer({ divergences }: ReplayDiffViewerProps) {
if (divergences.length === 0) {
return null;
}
const sortedDivergences = [...divergences].sort((a, b) => {
const diff = severityOrder[b.severity] - severityOrder[a.severity];
if (diff !== 0) return diff;
return a.path.localeCompare(b.path);
});
const copyPath = (path: string) => {
navigator.clipboard.writeText(path).catch(() => {
// fail silently
});
};
return (
<div className="space-y-4" data-testid="replay-diff-viewer">
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">Replay Divergences</h3>
<div className="space-y-4">
{sortedDivergences.map((div) => {
const key = `${div.severity}-${div.path}`;
return (
<div
key={key}
className="rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-4 space-y-3"
>
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-[var(--color-border-subtle)] pb-2">
<div className="flex items-center gap-2">
<ReplayDivergenceSeverityBadge value={div.severity as ReplayDivergenceSeverity} />
<span className="font-mono text-xs text-[var(--color-text-secondary)] break-all select-all">
{div.path}
</span>
</div>
<Button
onClick={() => copyPath(div.path)}
variant="quiet"
type="button"
aria-label={`Copy path ${div.path}`}
>
<Copy size={12} className="mr-1" />
Copy Path
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
Original Value
</div>
<StableJsonViewer source={serializeValue(div.original)} />
</div>
<div>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
Replay Value
</div>
<StableJsonViewer source={serializeValue(div.replay)} />
</div>
</div>
</div>
);
})}
</div>
</div>
);
}

View file

@ -1,89 +0,0 @@
import type { ArtifactDetail, ReplayComparison } from "../../types/api";
import { TraceHashBadge } from "../../design/components/badges";
interface ReplayMetadataTableProps {
artifact: ArtifactDetail;
comparison: ReplayComparison;
}
export function ReplayMetadataTable({ artifact, comparison }: ReplayMetadataTableProps) {
const divergenceCounts = comparison.divergences.reduce(
(acc, div) => {
acc[div.severity] = (acc[div.severity] || 0) + 1;
return acc;
},
{ info: 0, warning: 0, failure: 0 } as Record<"info" | "warning" | "failure", number>
);
return (
<section className="rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-4">
<h3 className="mb-3 text-sm font-semibold text-[var(--color-text-primary)]">Metadata Audit Details</h3>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="border-b border-[var(--color-border-subtle)] text-[var(--color-text-muted)]">
<th className="pb-2 font-medium">Property</th>
<th className="pb-2 font-medium">Value</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--color-border-subtle)] text-[var(--color-text-secondary)]">
<tr>
<td className="py-2 font-medium">Artifact ID</td>
<td className="py-2 font-mono">{artifact.artifact_id}</td>
</tr>
<tr>
<td className="py-2 font-medium">Kind</td>
<td className="py-2">{artifact.kind}</td>
</tr>
<tr>
<td className="py-2 font-medium">Path</td>
<td className="py-2 font-mono" data-testid="artifact-path-text">
{artifact.path}
</td>
</tr>
<tr>
<td className="py-2 font-medium">Original Hash</td>
<td className="py-2">
{comparison.original_hash ? (
<TraceHashBadge value={comparison.original_hash} />
) : (
<span className="text-[var(--color-text-muted)]">None</span>
)}
</td>
</tr>
<tr>
<td className="py-2 font-medium">Replay Hash</td>
<td className="py-2">
{comparison.replay_hash ? (
<TraceHashBadge value={comparison.replay_hash} />
) : (
<span className="text-[var(--color-text-muted)]">None</span>
)}
</td>
</tr>
<tr>
<td className="py-2 font-medium">Divergences</td>
<td className="py-2">
<span className="inline-flex gap-2">
<span className="text-[var(--color-review-rejected)]">
Failure: {divergenceCounts.failure}
</span>
<span className="text-[var(--color-review-pending)]">
Warning: {divergenceCounts.warning}
</span>
<span className="text-[var(--color-grounding-vault)]">
Info: {divergenceCounts.info}
</span>
</span>
</td>
</tr>
<tr>
<td className="py-2 font-medium">Content Type</td>
<td className="py-2 font-mono">{artifact.content_type}</td>
</tr>
</tbody>
</table>
</div>
</section>
);
}

View file

@ -0,0 +1,220 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
MemoryRouter,
Route,
Routes,
useLocation,
useNavigationType,
} from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTestQueryClient } from "../../test/createTestQueryClient";
import type { TurnJournalSummary, TurnReplayComparison } from "../../types/api";
import { EvidenceProvider } from "../evidenceContext";
import { ReplayRoute } from "./ReplayRoute";
const summaries: TurnJournalSummary[] = [
{
turn_id: 1,
timestamp: "2026-06-12T18:00:00Z",
prompt_excerpt: "What is truth?",
surface_excerpt: "Truth is coherent.",
trace_hash: "sha256:111111111111aaaa",
grounding_source: "pack",
},
{
turn_id: 2,
timestamp: "2026-06-12T18:01:00Z",
prompt_excerpt: "What is beauty?",
surface_excerpt: "Beauty is form.",
trace_hash: "sha256:222222222222bbbb",
grounding_source: "teaching",
},
];
function comparisonFor(turnId: number): TurnReplayComparison {
if (turnId === 1) {
// equivalent, only an informational (wall-clock) divergence
return {
turn_id: 1,
comparison_basis: "sealed_fresh_runtime_single_turn",
origin_state: "unrecorded",
original_trace_hash: "sha256:111111111111aaaa",
replay_trace_hash: "sha256:111111111111aaaa",
equivalent: true,
replay_turn_cost_ms: 412,
divergences: [
{
path: "timestamp",
original: "2026-06-12T18:00:00Z",
replay: "2026-06-13T00:00:00Z",
severity: "informational",
},
],
};
}
// diverged: a critical surface divergence
return {
turn_id: 2,
comparison_basis: "sealed_fresh_runtime_single_turn",
origin_state: "unrecorded",
original_trace_hash: "sha256:222222222222bbbb",
replay_trace_hash: "sha256:999999999999cccc",
equivalent: false,
replay_turn_cost_ms: 401,
divergences: [
{
path: "surface",
original: "Beauty is form.",
replay: "Beauty is symmetry.",
severity: "critical",
},
],
};
}
function LocationProbe() {
const location = useLocation();
const navigationType = useNavigationType();
return (
<>
<span data-testid="location">{`${location.pathname}${location.search}`}</span>
<span data-testid="nav-type">{navigationType}</span>
</>
);
}
function renderRoute(initialEntry = "/replay") {
return render(
<QueryClientProvider client={createTestQueryClient()}>
<MemoryRouter initialEntries={[initialEntry]}>
<EvidenceProvider>
<Routes>
<Route
path="/replay/:turnId?"
element={
<>
<ReplayRoute />
<LocationProbe />
</>
}
/>
</Routes>
</EvidenceProvider>
</MemoryRouter>
</QueryClientProvider>,
);
}
function stubReplayFetch(items: TurnJournalSummary[] = summaries) {
const fetchMock = vi.fn((input: unknown) => {
const path = new URL(String(input)).pathname;
if (path === "/trace/turns") {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: { items } }),
});
}
const match = path.match(/^\/replay\/(\d+)$/);
if (match) {
return Promise.resolve({
json: () =>
Promise.resolve({ ok: true, generated_at: "now", data: comparisonFor(Number(match[1])) }),
});
}
return Promise.resolve({
json: () =>
Promise.resolve({
ok: false,
generated_at: "now",
error: { code: "not_found", message: `unexpected ${path}` },
}),
});
});
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
const offsetDescriptors = {
offsetHeight: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight"),
offsetWidth: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth"),
};
describe("ReplayRoute", () => {
beforeEach(() => {
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
configurable: true,
get: () => 560,
});
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
configurable: true,
get: () => 480,
});
});
afterEach(() => {
if (offsetDescriptors.offsetHeight) {
Object.defineProperty(HTMLElement.prototype, "offsetHeight", offsetDescriptors.offsetHeight);
}
if (offsetDescriptors.offsetWidth) {
Object.defineProperty(HTMLElement.prototype, "offsetWidth", offsetDescriptors.offsetWidth);
}
vi.restoreAllMocks();
localStorage.clear();
});
it("lists journaled turns to replay", async () => {
stubReplayFetch();
renderRoute();
expect(await screen.findByText("What is truth?")).toBeInTheDocument();
expect(screen.getByText("What is beauty?")).toBeInTheDocument();
});
it("renders the equivalence hero with the honesty card for a matching replay", async () => {
stubReplayFetch();
renderRoute("/replay/1");
expect(await screen.findByText(/Replay equivalent/)).toBeInTheDocument();
// honesty fields are surfaced, not hidden
expect(screen.getByText("sealed_fresh_runtime_single_turn")).toBeInTheDocument();
expect(screen.getByText("unrecorded")).toBeInTheDocument();
// an equivalent replay with only wall-clock drift is labeled as such
expect(screen.getByText(/all informational/)).toBeInTheDocument();
});
it("renders a diverged verdict with the critical leaf for a tampered replay", async () => {
stubReplayFetch();
renderRoute("/replay/2");
expect(await screen.findByText("Replay diverged")).toBeInTheDocument();
// the critical divergence is shown at its exact leaf path
expect(screen.getByText("surface")).toBeInTheDocument();
expect(screen.getByText("Beauty is symmetry.")).toBeInTheDocument();
expect(screen.getAllByText("critical").length).toBeGreaterThan(0);
});
it("selecting a turn writes /replay/<turnId> with replace", async () => {
stubReplayFetch();
const user = userEvent.setup();
renderRoute();
await user.click(await screen.findByText("What is beauty?"));
await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/replay/2"));
expect(screen.getByTestId("nav-type")).toHaveTextContent("REPLACE");
expect(await screen.findByText("Replay diverged")).toBeInTheDocument();
});
it("moves the turn list focus with j/k through the VirtualizedList spine", async () => {
stubReplayFetch();
const user = userEvent.setup();
renderRoute();
const list = await screen.findByRole("listbox", { name: "Replayable turns" });
list.focus();
expect(screen.getAllByRole("option")[0]).toHaveAttribute("aria-selected", "true");
await user.keyboard("j");
expect(screen.getAllByRole("option")[1]).toHaveAttribute("aria-selected", "true");
});
});

View file

@ -1,174 +1,321 @@
import { useEffect, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useEvidenceSubject } from "../evidenceContext";
import { subjectToUrl } from "../evidenceAddress";
import { useArtifacts, useArtifactDetail, useReplayComparison } from "../../api/queries";
import { ArtifactList } from "./ArtifactList";
import { ReplayComparisonPanel } from "./ReplayComparisonPanel";
import { LoadingState } from "../../design/components/states/LoadingState";
import { ErrorState } from "../../design/components/states/ErrorState";
import { EmptyState } from "../../design/components/states/EmptyState";
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { WorkbenchApiError } from "../../api/client";
import { ReplayStatus } from "../../design/components/badges";
import { useTraceTurns, useTurnReplay } from "../../api/queries";
import { DigestBadge } from "../../design/components/DigestBadge/DigestBadge";
import { Panel } from "../../design/components/Panel/Panel";
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
import { SplitPane } from "../../design/components/SplitPane/SplitPane";
import { Timestamp } from "../../design/components/Timestamp/Timestamp";
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
import { EmptyState } from "../../design/components/states/EmptyState";
import { ErrorState } from "../../design/components/states/ErrorState";
import { LoadingState } from "../../design/components/states/LoadingState";
import type {
TurnJournalSummary,
TurnReplayComparison,
TurnReplayDivergence,
} from "../../types/api";
import { pushRecentItem } from "../commandRegistry";
import { useEvidenceSubject } from "../evidenceContext";
export function ReplayRoute() {
const { artifactId } = useParams();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { setSubject } = useEvidenceSubject();
const selectedId = artifactId ?? null;
const [search, setSearch] = useState("");
function parseTurnId(raw: string | undefined): number | null {
if (!raw || !/^\d+$/.test(raw)) return null;
const value = Number(raw);
return Number.isSafeInteger(value) ? value : null;
}
const artifactsQuery = useArtifacts();
const detailQuery = useArtifactDetail(selectedId || "");
const comparisonQuery = useReplayComparison(selectedId || "");
function errorMessage(error: unknown) {
return error instanceof WorkbenchApiError ? error.message : "Replay request failed.";
}
// Publish the selected artifact as the evidence subject: identity
// immediately, detail once the query resolves.
const detailData = detailQuery.data;
useEffect(() => {
if (!selectedId) return;
setSubject({ kind: "artifact", artifactId: selectedId, data: detailData });
}, [selectedId, detailData, setSubject]);
function digestPayload(value: string | null | undefined): string | null {
if (!value) return null;
return value.replace(/^sha256:/, "");
}
function handleSelect(id: string) {
const search = searchParams.toString();
const path = subjectToUrl({ kind: "artifact", artifactId: id });
// Selection churn must not pollute history: replace, never push.
navigate(search ? `${path}?${search}` : path, { replace: true });
}
// Handle loading states
const isLoadingArtifacts = artifactsQuery.isPending;
const isLoadingDetail = selectedId ? detailQuery.isPending : false;
const isLoadingComparison = selectedId ? comparisonQuery.isPending : false;
// Determine if comparison error is the unsupported (evidence_unavailable) case
const isUnsupportedError =
comparisonQuery.error instanceof WorkbenchApiError &&
comparisonQuery.error.code === "unsupported";
// Check for genuine API errors
const artifactsError = artifactsQuery.error;
const detailError = detailQuery.error;
const comparisonError = comparisonQuery.error;
const hasGenuineError =
artifactsQuery.isError ||
detailQuery.isError ||
(comparisonQuery.isError && !isUnsupportedError);
const getGenuineErrorDetails = () => {
if (artifactsQuery.isError && artifactsError) {
return {
message:
artifactsError instanceof WorkbenchApiError
? artifactsError.message
: "Failed to load artifacts.",
reproducer: "curl -X GET /artifacts",
};
}
if (detailQuery.isError && detailError) {
return {
message:
detailError instanceof WorkbenchApiError
? detailError.message
: "Failed to load artifact details.",
reproducer: `curl -X GET /artifacts/${selectedId || ""}`,
};
}
if (comparisonQuery.isError && comparisonError && !isUnsupportedError) {
return {
message:
comparisonError instanceof WorkbenchApiError
? comparisonError.message
: "Failed to load replay comparison.",
reproducer: `curl -X GET /replay/${selectedId || ""}`,
};
}
return null;
};
const errorDetails = getGenuineErrorDetails();
function stringify(value: unknown): string {
if (typeof value === "string") return value;
return JSON.stringify(value);
}
function TurnRow({
turn,
selected,
focused,
onSelect,
}: {
turn: TurnJournalSummary;
selected: boolean;
focused: boolean;
onSelect: () => void;
}) {
return (
<div
className="grid grid-cols-[18rem_1fr] h-[calc(100vh-8rem)] gap-4 overflow-hidden"
data-testid="replay-theater-route"
role="button"
tabIndex={-1}
aria-current={selected ? "true" : undefined}
onClick={onSelect}
className={`grid w-full grid-cols-[minmax(0,1fr)_auto] items-start gap-3 border-b border-[var(--color-border-subtle)] px-3 py-2 text-left transition-colors hover:bg-[var(--color-surface-inset)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-[var(--color-focus-ring)] ${
selected ? "bg-[var(--color-selected-bg)]" : ""
} ${
selected
? "border-l-2 border-l-[var(--color-selected-border)] pl-[10px]"
: focused
? "border-l-2 border-l-[var(--color-focus-ring)] pl-[10px]"
: "border-l-2 border-l-transparent pl-[10px]"
}`}
>
{/* Left Pane: Artifact list */}
<div className="border-r border-[var(--color-border-subtle)] overflow-y-auto pr-2">
<div className="mb-2">
<SearchInput
placeholder="Filter by artifact id or kind"
value={search}
onChange={setSearch}
/>
</div>
{isLoadingArtifacts ? (
<LoadingState label="Loading artifacts..." />
) : artifactsQuery.isError ? (
<ErrorState
whatFailed={
artifactsError instanceof WorkbenchApiError
? artifactsError.message
: "Failed to load artifacts."
}
mutationStatus="No corpus mutation occurred."
reproducer="curl http://127.0.0.1:8765/artifacts"
retrySafety="Retry: safe"
/>
) : (
<ArtifactList
artifacts={(artifactsQuery.data || []).filter((a) => {
const q = search.trim().toLowerCase();
if (!q) return true;
return (
a.artifact_id.toLowerCase().includes(q) ||
a.kind.toLowerCase().includes(q)
);
})}
selectedId={selectedId}
onSelect={handleSelect}
/>
)}
</div>
{/* Right Pane: Replay detail comparison */}
<div className="overflow-y-auto pl-2 pr-4">
{hasGenuineError && errorDetails ? (
<ErrorState
whatFailed={errorDetails.message}
mutationStatus="No corpus mutation occurred."
reproducer={errorDetails.reproducer}
retrySafety="Retry: safe"
/>
) : isLoadingDetail || isLoadingComparison ? (
<LoadingState
label={isLoadingComparison ? "Comparing artifacts..." : "Comparing artifacts..."}
/>
) : !selectedId ? (
<EmptyState
statement="No artifact selected."
nextAction="Select an artifact from the list to inspect replay evidence."
/>
) : detailQuery.data ? (
<ReplayComparisonPanel
artifact={detailQuery.data}
comparison={isUnsupportedError ? null : comparisonQuery.data}
status={
isUnsupportedError
? ReplayStatus.EVIDENCE_UNAVAILABLE
: comparisonQuery.data?.equivalent
? ReplayStatus.EQUIVALENT
: comparisonQuery.data?.replay_hash === null
? ReplayStatus.NOT_YET_REPLAYED
: ReplayStatus.DIVERGED
}
/>
) : null}
</div>
<span className="min-w-0">
<span className="block text-xs text-[var(--color-text-secondary)]">
<Timestamp iso={turn.timestamp} format="relative" />
</span>
<span className="mt-1 block truncate text-sm text-[var(--color-text-primary)]">
{turn.prompt_excerpt || `Turn #${turn.turn_id}`}
</span>
</span>
<span className="justify-self-end font-mono text-xs text-[var(--color-text-muted)]">
#{turn.turn_id}
</span>
</div>
);
}
function HashPair({ comparison }: { comparison: TurnReplayComparison }) {
const original = digestPayload(comparison.original_trace_hash);
const replay = digestPayload(comparison.replay_trace_hash);
return (
<div className="grid grid-cols-[auto_1fr] items-center gap-x-3 gap-y-2 text-sm">
<span className="text-[var(--color-text-secondary)]">original</span>
<span>{original ? <DigestBadge digest={original} truncate={16} /> : "not recorded"}</span>
<span className="text-[var(--color-text-secondary)]">replay</span>
<span>{replay ? <DigestBadge digest={replay} truncate={16} /> : "not recorded"}</span>
</div>
);
}
function Verdict({ equivalent }: { equivalent: boolean }) {
return (
<div
className={`flex items-center gap-2 rounded-md border px-3 py-2 ${
equivalent
? "border-[var(--color-state-verified)] text-[var(--color-state-verified)]"
: "border-[var(--color-state-contradicted)] text-[var(--color-state-contradicted)]"
}`}
>
<span aria-hidden className="font-mono text-lg">
{equivalent ? "≡" : "≠"}
</span>
<span className="text-sm font-semibold">
{equivalent ? "Replay equivalent — bit-identical envelope" : "Replay diverged"}
</span>
</div>
);
}
function DivergenceRow({ divergence }: { divergence: TurnReplayDivergence }) {
const critical = divergence.severity === "critical";
return (
<li
className={`grid gap-1 rounded-md border px-3 py-2 ${
critical
? "border-[var(--color-state-contradicted)] bg-[var(--color-surface-inset)]"
: "border-[var(--color-border-subtle)]"
}`}
>
<span className="flex items-center gap-2">
<span aria-hidden className="font-mono text-[var(--color-text-muted)]">
</span>
<span className="font-mono text-xs text-[var(--color-text-primary)]">{divergence.path}</span>
<span
className={`text-[10px] uppercase tracking-wide ${
critical ? "text-[var(--color-state-contradicted)]" : "text-[var(--color-text-muted)]"
}`}
>
{divergence.severity}
</span>
</span>
<span className="grid grid-cols-[auto_1fr] gap-x-2 font-mono text-xs">
<span className="text-[var(--color-text-secondary)]">original</span>
<span className="break-all text-[var(--color-text-primary)]">
{stringify(divergence.original)}
</span>
<span className="text-[var(--color-text-secondary)]">replay</span>
<span className="break-all text-[var(--color-text-primary)]">
{stringify(divergence.replay)}
</span>
</span>
</li>
);
}
function ReplayHero({ comparison }: { comparison: TurnReplayComparison }) {
// critical first, then informational; stable within a severity by path.
const ordered = useMemo(() => {
const weight = (s: string) => (s === "critical" ? 0 : 1);
return [...comparison.divergences].sort(
(a, b) => weight(a.severity) - weight(b.severity) || a.path.localeCompare(b.path),
);
}, [comparison.divergences]);
const informationalOnly =
comparison.divergences.length > 0 &&
comparison.divergences.every((d) => d.severity === "informational");
return (
<Panel title={`Replay of Turn #${comparison.turn_id}`}>
<div className="grid gap-4">
<Verdict equivalent={comparison.equivalent} />
<HashPair comparison={comparison} />
<section className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] p-3">
<h3 className="m-0 text-xs font-semibold text-[var(--color-text-secondary)]">
What this proves
</h3>
<p className="mt-1 mb-0 text-xs text-[var(--color-text-muted)]">
Basis: <span className="font-mono">{comparison.comparison_basis}</span>. The prompt was
re-executed in a sealed fresh runtime. Origin state is{" "}
<span className="font-mono">{comparison.origin_state}</span> the journal does not
record whether the original turn loaded a checkpoint, so a divergence means
nondeterminism <em>or</em> origin-state influence; it is not, on its own, a determinism
failure.
</p>
</section>
{comparison.divergences.length === 0 ? (
<p className="m-0 text-sm text-[var(--color-text-secondary)]">
No divergences every leaf of the envelope matched.
</p>
) : (
<section className="grid gap-2">
<h3 className="m-0 text-xs font-semibold text-[var(--color-text-secondary)]">
Leaf divergences ({comparison.divergences.length})
{informationalOnly ? " — all informational (wall-clock; expected)" : ""}
</h3>
<ul className="m-0 grid list-none gap-2 p-0">
{ordered.map((divergence) => (
<DivergenceRow key={divergence.path} divergence={divergence} />
))}
</ul>
</section>
)}
</div>
</Panel>
);
}
export function ReplayRoute() {
const { turnId } = useParams();
const selectedTurnId = parseTurnId(turnId);
const navigate = useNavigate();
const { setSubject } = useEvidenceSubject();
const [search, setSearch] = useState("");
const turnsQuery = useTraceTurns();
const replayQuery = useTurnReplay(selectedTurnId);
const turns = turnsQuery.data ?? [];
const filteredTurns = useMemo(() => {
const q = search.trim().toLowerCase();
if (!q) return turns;
return turns.filter(
(turn) =>
turn.prompt_excerpt.toLowerCase().includes(q) || String(turn.turn_id).includes(q),
);
}, [search, turns]);
// Publish the turn subject for the inspector.
useEffect(() => {
if (selectedTurnId === null) return;
setSubject({ kind: "turn", turnId: selectedTurnId });
}, [selectedTurnId, setSubject]);
function selectTurn(turn: TurnJournalSummary) {
navigate(`/replay/${turn.turn_id}`, { replace: true });
pushRecentItem({ label: `Replay #${turn.turn_id}`, path: `/replay/${turn.turn_id}` });
}
if (turnsQuery.isLoading) {
return <LoadingState label="Loading turns..." />;
}
if (turnsQuery.isError) {
return (
<ErrorState
whatFailed={errorMessage(turnsQuery.error)}
mutationStatus="No replay mutation occurred."
reproducer="curl /trace/turns"
retrySafety="Retry: safe"
/>
);
}
if (turns.length === 0) {
return (
<EmptyState
statement="No turns recorded yet. Use Chat to create evidence."
nextAction={{ kind: "cli", command: "core chat" }}
/>
);
}
return (
<div className="h-full min-h-0">
<SplitPane direction="horizontal" id="replay" defaultSplit={36} minSize={320}>
<Panel title="Replayable turns">
<div className="grid min-h-0 gap-3">
<SearchInput
placeholder="Filter by prompt or turn id"
value={search}
onChange={setSearch}
/>
{filteredTurns.length === 0 ? (
<EmptyState
statement="No turns match this filter."
nextAction={{ kind: "cli", command: "core chat" }}
/>
) : (
<VirtualizedList
ariaLabel="Replayable turns"
estimateSize={72}
getKey={(turn) => String(turn.turn_id)}
height="calc(100vh - 14rem)"
initialRect={{ width: 480, height: 560 }}
items={filteredTurns}
onActivate={(turn) => selectTurn(turn)}
renderItem={(turn, _index, focused) => (
<TurnRow
turn={turn}
selected={turn.turn_id === selectedTurnId}
focused={focused}
onSelect={() => selectTurn(turn)}
/>
)}
/>
)}
</div>
</Panel>
<section className="h-full min-h-0 overflow-y-auto pl-3">
{selectedTurnId === null ? (
<EmptyState
statement="Select a turn to replay it deterministically and compare hashes."
nextAction={{ kind: "cli", command: "core chat" }}
/>
) : replayQuery.isLoading ? (
<LoadingState label="Replaying turn..." />
) : replayQuery.isError ? (
<ErrorState
whatFailed={errorMessage(replayQuery.error)}
mutationStatus="No replay mutation occurred."
reproducer={`curl /replay/${selectedTurnId}`}
retrySafety="Retry: safe"
/>
) : replayQuery.data ? (
<ReplayHero comparison={replayQuery.data} />
) : null}
</section>
</SplitPane>
</div>
);
}

View file

@ -1,414 +0,0 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { createTestQueryClient } from "../../test/createTestQueryClient";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { describe, expect, it, vi, afterEach } from "vitest";
import { EvidenceProvider } from "../evidenceContext";
import { ArtifactList } from "./ArtifactList";
import { ReplayStatusBadge, ReplayStatus } from "../../design/components/badges";
import { ReplayComparisonPanel } from "./ReplayComparisonPanel";
import { ReplayDiffViewer } from "./ReplayDiffViewer";
import { ReplayMetadataTable } from "./ReplayMetadataTable";
import { ReplayRoute } from "./ReplayRoute";
import type { ArtifactRef, ArtifactDetail, ReplayComparison } from "../../types/api";
import * as fs from "fs";
import * as path from "path";
// Mock globals
vi.stubGlobal("navigator", {
clipboard: {
writeText: vi.fn().mockResolvedValue(undefined),
},
});
const mockArtifacts: ArtifactRef[] = [
{
artifact_id: "art-trace-1",
kind: "trace",
path: "traces/1.json",
digest: "sha256:1",
created_at: "2026-05-26T12:00:00Z",
},
{
artifact_id: "art-eval-1",
kind: "eval_result",
path: "evals/1.json",
digest: "sha256:2",
created_at: "2026-05-26T12:01:00Z",
},
{
artifact_id: "art-prop-1",
kind: "proposal",
path: "proposals/1.json",
digest: "sha256:3",
created_at: "2026-05-26T12:02:00Z",
},
{
artifact_id: "art-rep-1",
kind: "contemplation_report",
path: "reports/1.json",
digest: "sha256:4",
created_at: "2026-05-26T12:03:00Z",
},
{
artifact_id: "art-tel-1",
kind: "telemetry",
path: "telemetry/1.jsonl",
digest: "sha256:5",
created_at: "2026-05-26T12:04:00Z",
},
{
artifact_id: "art-state-1",
kind: "engine_state_manifest",
path: "state/1.json",
digest: "sha256:6",
created_at: "2026-05-26T12:05:00Z",
},
{
artifact_id: "art-unk-1",
kind: "unknown",
path: "unknown/1.json",
digest: "sha256:7",
created_at: "2026-05-26T12:06:00Z",
},
];
const mockArtifactDetail: ArtifactDetail = {
artifact_id: "art-trace-1",
kind: "trace",
path: "traces/1.json",
digest: "sha256:1",
created_at: "2026-05-26T12:00:00Z",
content_type: "json",
content: { hello: "world" },
};
const mockReplayComparison: ReplayComparison = {
artifact_id: "art-trace-1",
original_hash: "sha256:orig-1234",
replay_hash: "sha256:repl-5678",
equivalent: false,
divergences: [
{ path: "/a", original: 1, replay: 2, severity: "warning" },
{ path: "/b", original: true, replay: false, severity: "failure" },
{ path: "/c", original: "x", replay: "y", severity: "info" },
],
};
function renderWithProviders(ui: React.ReactElement, initialEntries = ["/"]) {
const queryClient = createTestQueryClient();
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={initialEntries}>{ui}</MemoryRouter>
</QueryClientProvider>
);
}
// ReplayRoute reads its selection from the :artifactId path param and
// publishes the selected artifact as the evidence subject, so it needs the
// real route declaration and an EvidenceProvider.
function renderReplayRoute(initialEntry: string) {
return renderWithProviders(
<EvidenceProvider>
<Routes>
<Route path="/replay/:artifactId?" element={<ReplayRoute />} />
</Routes>
</EvidenceProvider>,
[initialEntry],
);
}
describe("W-031 Replay Theater Tests", () => {
afterEach(() => {
vi.restoreAllMocks();
});
describe("ArtifactList", () => {
it("renders empty state when API returns []", () => {
render(
<ArtifactList artifacts={[]} selectedId={null} onSelect={() => {}} />
);
expect(screen.getByTestId("artifact-list-empty")).toBeInTheDocument();
expect(screen.getByText("No artifacts available.")).toBeInTheDocument();
});
it("groups by kind correctly with a fixture covering all 7 kind values", () => {
render(
<ArtifactList
artifacts={mockArtifacts}
selectedId={null}
onSelect={() => {}}
/>
);
// Verify that all 7 group headings are rendered
expect(screen.getByText("trace")).toBeInTheDocument();
expect(screen.getByText("eval result")).toBeInTheDocument();
expect(screen.getByText("proposal")).toBeInTheDocument();
expect(screen.getByText("contemplation report")).toBeInTheDocument();
expect(screen.getByText("telemetry")).toBeInTheDocument();
expect(screen.getByText("engine state manifest")).toBeInTheDocument();
expect(screen.getByText("unknown")).toBeInTheDocument();
// Verify artifact IDs are present
mockArtifacts.forEach((art) => {
expect(screen.getByText(art.artifact_id)).toBeInTheDocument();
});
});
it("selecting a row invokes onSelect with the artifact id", () => {
// URL writing is ReplayRoute's job (path param, replace) — covered by
// the route tests below. ArtifactList only reports the selection.
const onSelect = vi.fn();
render(
<ArtifactList
artifacts={mockArtifacts}
selectedId={null}
onSelect={onSelect}
/>
);
fireEvent.click(screen.getByTestId("artifact-art-trace-1"));
expect(onSelect).toHaveBeenCalledWith("art-trace-1");
});
});
describe("ReplayStatusBadge", () => {
it("all four enum states render with the right label and color semantics", () => {
const states = [
{ value: ReplayStatus.EQUIVALENT, label: "equivalent" },
{ value: ReplayStatus.NOT_YET_REPLAYED, label: "not_yet_replayed" },
{ value: ReplayStatus.DIVERGED, label: "diverged" },
{ value: ReplayStatus.EVIDENCE_UNAVAILABLE, label: "evidence_unavailable" },
];
states.forEach(({ value, label }) => {
const { unmount } = render(<ReplayStatusBadge value={value} />);
expect(screen.getByRole("button", { name: label })).toBeInTheDocument();
unmount();
});
});
});
describe("ReplayComparisonPanel", () => {
it("equivalent=true renders the calm empty-state for divergences, NOT a celebratory affordance", () => {
const eqComparison: ReplayComparison = {
artifact_id: "art-trace-1",
original_hash: "sha256:1",
replay_hash: "sha256:1",
equivalent: true,
divergences: [],
};
render(
<ReplayComparisonPanel
artifact={mockArtifactDetail}
comparison={eqComparison}
status={ReplayStatus.EQUIVALENT}
/>
);
// Check for calm empty state text
expect(screen.getByText("Replay evidence intact — no divergences.")).toBeInTheDocument();
// Assert static text contains no celebratory affect
const containerText = document.body.innerHTML;
const forbiddenWords = ["success", "celebrate", "congratulations", "🎉", "✅", "🏆", "🌟"];
forbiddenWords.forEach((word) => {
expect(containerText.toLowerCase()).not.toContain(word);
});
});
it("verifies the component source code files contain no celebratory words or emojis", () => {
const dir = __dirname;
const files = [
"ReplayRoute.tsx",
"ReplayComparisonPanel.tsx",
"ReplayDiffViewer.tsx",
"ReplayMetadataTable.tsx",
"ArtifactList.tsx",
];
files.forEach((file) => {
const filePath = path.join(dir, file);
const code = fs.readFileSync(filePath, "utf-8");
const forbiddenWords = ["success", "celebrate", "congratulations", "🎉", "✅"];
forbiddenWords.forEach((word) => {
expect(
code.toLowerCase(),
`File ${file} contains forbidden positive-affect word: ${word}`
).not.toContain(word);
});
});
});
});
describe("ReplayDiffViewer", () => {
it("renders divergences ordered failure -> warning -> info", () => {
render(<ReplayDiffViewer divergences={mockReplayComparison.divergences} />);
const badges = screen.getAllByRole("button", { name: /(failure|warning|info)/i });
expect(badges).toHaveLength(3);
expect(badges[0].textContent).toBe("failure");
expect(badges[1].textContent).toBe("warning");
expect(badges[2].textContent).toBe("info");
});
it("renders nothing (null) with 0 divergences", () => {
const { container } = render(<ReplayDiffViewer divergences={[]} />);
expect(container.firstChild).toBeNull();
});
});
describe("ReplayMetadataTable", () => {
it("copyable digests work", () => {
render(
<ReplayMetadataTable
artifact={mockArtifactDetail}
comparison={mockReplayComparison}
/>
);
// Check presence of digest badges (which are copyable InfoBadges)
const origBadge = screen.getByRole("button", { name: "sha256:orig-" });
const replBadge = screen.getByRole("button", { name: "sha256:repl-" });
expect(origBadge).toBeInTheDocument();
expect(replBadge).toBeInTheDocument();
});
it("rendered path is text-only, not an anchor/link element", () => {
render(
<ReplayMetadataTable
artifact={mockArtifactDetail}
comparison={mockReplayComparison}
/>
);
const pathEl = screen.getByTestId("artifact-path-text");
expect(pathEl.tagName.toLowerCase()).not.toBe("a");
expect(pathEl.closest("a")).toBeNull();
expect(pathEl.textContent).toBe(mockArtifactDetail.path);
});
});
describe("ReplayRoute", () => {
it("full happy-path with fixture artifact + fixture comparison", async () => {
const fetchMock = vi.fn().mockImplementation((url: string) => {
if (url.endsWith("/artifacts")) {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: { items: mockArtifacts } }),
});
}
if (url.endsWith("/artifacts/art-trace-1")) {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: mockArtifactDetail }),
});
}
if (url.endsWith("/replay/art-trace-1")) {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: mockReplayComparison }),
});
}
return Promise.reject(new Error("Unknown route"));
});
vi.stubGlobal("fetch", fetchMock);
renderReplayRoute("/replay/art-trace-1");
// Should load list, detail, and comparison
expect(await screen.findByText("Replay Evidence")).toBeInTheDocument();
expect(screen.getAllByText("art-trace-1").length).toBeGreaterThanOrEqual(1);
expect(screen.getByText("Replay Divergences")).toBeInTheDocument();
});
it("evidence_unavailable state when backend returns unsupported", async () => {
const fetchMock = vi.fn().mockImplementation((url: string) => {
if (url.endsWith("/artifacts")) {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: { items: mockArtifacts } }),
});
}
if (url.endsWith("/artifacts/art-trace-1")) {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: mockArtifactDetail }),
});
}
if (url.endsWith("/replay/art-trace-1")) {
// Return unsupported error code
return Promise.resolve({
json: () => Promise.resolve({
ok: false,
generated_at: "now",
error: { code: "unsupported", message: "route is unsupported" },
}),
});
}
return Promise.reject(new Error("Unknown route"));
});
vi.stubGlobal("fetch", fetchMock);
renderReplayRoute("/replay/art-trace-1");
expect(await screen.findByText("evidence_unavailable")).toBeInTheDocument();
expect(screen.getByText("Replay evidence is not available on the backend for this artifact kind.")).toBeInTheDocument();
});
it("ErrorState only for genuine API errors (not for unsupported)", async () => {
const fetchMock = vi.fn().mockImplementation((url: string) => {
if (url.endsWith("/artifacts")) {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: { items: mockArtifacts } }),
});
}
if (url.endsWith("/artifacts/art-trace-1")) {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: mockArtifactDetail }),
});
}
if (url.endsWith("/replay/art-trace-1")) {
// Return a genuine read_error
return Promise.resolve({
json: () => Promise.resolve({
ok: false,
generated_at: "now",
error: { code: "read_error", message: "disk read error" },
}),
});
}
return Promise.reject(new Error("Unknown route"));
});
vi.stubGlobal("fetch", fetchMock);
renderReplayRoute("/replay/art-trace-1");
expect(await screen.findByText("What failed")).toBeInTheDocument();
expect(screen.getByText("disk read error")).toBeInTheDocument();
});
});
describe("Anti-motion & Animation Constraints", () => {
it("verifies the component files contain no animation/motion triggers like transition, animate, fade", () => {
const dir = __dirname;
const files = [
"ReplayRoute.tsx",
"ReplayComparisonPanel.tsx",
"ReplayDiffViewer.tsx",
"ReplayMetadataTable.tsx",
"ArtifactList.tsx",
];
files.forEach((file) => {
const filePath = path.join(dir, file);
const code = fs.readFileSync(filePath, "utf-8");
const forbiddenMotion = ["transition", "animate", "fade"];
forbiddenMotion.forEach((trigger) => {
expect(
code.toLowerCase(),
`File ${file} contains forbidden animation keyword: ${trigger}`
).not.toContain(trigger);
});
});
});
});
});

View file

@ -158,11 +158,11 @@ const MOUNT_ROUTES: MountRouteSpec[] = [
{
name: "Replay",
element: <ReplayRoute />,
path: "/replay/:artifactId?",
path: "/replay/:turnId?",
initialEntry: "/replay",
loadingLabel: "Loading artifacts...",
emptyStatement: "No artifacts available.",
emptyCommand: "core eval cognition",
loadingLabel: "Loading turns...",
emptyStatement: "No turns recorded yet. Use Chat to create evidence.",
emptyCommand: "core chat",
},
{
name: "Packs",

View file

@ -5,8 +5,6 @@ import {
groundingSourceMeta,
normativeClearanceMeta,
reviewStateMeta,
replayDivergenceSeverityMeta,
replayStatusMeta,
} from "./mappings";
function expectExactCoverage(name: string, engineValues: string[], uiValues: string[]) {
@ -45,20 +43,4 @@ describe("build-time enum coverage", () => {
Object.keys(groundingSourceMeta),
);
});
it("tracks every ratified ReplayDivergenceSeverity value exactly once", () => {
expectExactCoverage(
"ReplayDivergenceSeverity",
snapshot.ReplayDivergenceSeverity,
Object.keys(replayDivergenceSeverityMeta),
);
});
it("tracks every ratified ReplayStatus value exactly once", () => {
expectExactCoverage(
"ReplayStatus",
snapshot.ReplayStatus,
Object.keys(replayStatusMeta),
);
});
});

View file

@ -3,8 +3,6 @@ import {
groundingSourceMeta,
normativeClearanceMeta,
reviewStateMeta,
replayDivergenceSeverityMeta,
replayStatusMeta,
} from "./mappings";
import { InfoBadge } from "./Badge";
import {
@ -12,8 +10,6 @@ import {
GroundingSource,
NormativeClearance,
ReviewState,
ReplayDivergenceSeverity,
ReplayStatus,
} from "./types";
export {
@ -21,8 +17,6 @@ export {
GroundingSource,
NormativeClearance,
ReviewState,
ReplayDivergenceSeverity,
ReplayStatus,
};
export function EpistemicStateBadge({ value }: { value: EpistemicState }) {
@ -41,14 +35,6 @@ export function GroundingSourceBadge({ value }: { value: GroundingSource }) {
return <InfoBadge {...groundingSourceMeta[value]} />;
}
export function ReplayDivergenceSeverityBadge({ value }: { value: ReplayDivergenceSeverity }) {
return <InfoBadge {...replayDivergenceSeverityMeta[value]} />;
}
export function ReplayStatusBadge({ value }: { value: ReplayStatus }) {
return <InfoBadge {...replayStatusMeta[value]} />;
}
export function TraceHashBadge({
value,
truncate = 12,

View file

@ -3,8 +3,6 @@ import {
GroundingSource,
NormativeClearance,
ReviewState,
ReplayDivergenceSeverity,
ReplayStatus,
type BadgeMeta,
} from "./types";
@ -48,16 +46,3 @@ export const groundingSourceMeta = {
[GroundingSource.OOV]: { label: "OOV", colorToken: "--color-grounding-oov", meaning: "Out-of-vocabulary grounding was encountered.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is oov." },
[GroundingSource.NONE]: { label: "None", colorToken: "--color-grounding-none", meaning: "No grounding source was present.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is none." },
} satisfies BadgeMeta<GroundingSource>;
export const replayDivergenceSeverityMeta = {
[ReplayDivergenceSeverity.INFO]: { label: "info", colorToken: "--color-grounding-vault", meaning: "Informational divergence, non-critical deviation.", adr: "ADR-0160 / ADR-0162", evidence: "divergence severity is info." },
[ReplayDivergenceSeverity.WARNING]: { label: "warning", colorToken: "--color-review-pending", meaning: "Warning divergence, requires attention.", adr: "ADR-0160 / ADR-0162", evidence: "divergence severity is warning." },
[ReplayDivergenceSeverity.FAILURE]: { label: "failure", colorToken: "--color-review-rejected", meaning: "Failure divergence, critical replay violation.", adr: "ADR-0160 / ADR-0162", evidence: "divergence severity is failure." },
} satisfies BadgeMeta<ReplayDivergenceSeverity>;
export const replayStatusMeta = {
[ReplayStatus.EQUIVALENT]: { label: "equivalent", colorToken: "--color-review-accepted", meaning: "Replay evidence intact — no divergences.", adr: "ADR-0160 / ADR-0162", evidence: "equivalent is true and replay_hash is present." },
[ReplayStatus.NOT_YET_REPLAYED]: { label: "not_yet_replayed", colorToken: "--color-review-pending", meaning: "No replay has been attempted for this artifact yet.", adr: "ADR-0160 / ADR-0162", evidence: "equivalent is false and replay_hash is null." },
[ReplayStatus.DIVERGED]: { label: "diverged", colorToken: "--color-review-rejected", meaning: "Replay completed but generated different state/output.", adr: "ADR-0160 / ADR-0162", evidence: "equivalent is false and replay_hash is present." },
[ReplayStatus.EVIDENCE_UNAVAILABLE]: { label: "evidence_unavailable", colorToken: "--color-review-withdrawn", meaning: "Replay evidence is not available on the backend for this kind.", adr: "ADR-0160 / ADR-0162", evidence: "API returned unsupported error code." },
} satisfies BadgeMeta<ReplayStatus>;

View file

@ -39,19 +39,6 @@ export enum GroundingSource {
NONE = "none",
}
export enum ReplayDivergenceSeverity {
INFO = "info",
WARNING = "warning",
FAILURE = "failure",
}
export enum ReplayStatus {
EQUIVALENT = "equivalent",
NOT_YET_REPLAYED = "not_yet_replayed",
DIVERGED = "diverged",
EVIDENCE_UNAVAILABLE = "evidence_unavailable",
}
export type BadgeMeta<T extends string> = Record<
T,
{

View file

@ -19,13 +19,10 @@ import { describe, expect, it } from "vitest";
* only shrink.
*/
const NOT_YET_MIRRORED = new Set([
// Wave R3 sealed turn replay backend — TS mirrors land with the frontend
// Replay Moment PR (which also retires the W-026 artifact-keyed
// ReplayComparison/ReplayDivergence pair on both sides):
"TurnReplayComparison",
"TurnReplayDivergence",
]);
// Empty: every engine schema is now mirrored. New unmirrored schemas may be
// allowlisted here as shrink-only debt (a class gaining a mirror while listed
// fails the gate).
const NOT_YET_MIRRORED = new Set<string>([]);
const UI_ROOT = join(__dirname, "..", "..", "..");
const snapshot: Record<string, string[]> = JSON.parse(

View file

@ -247,21 +247,28 @@ export interface EvalRunResult {
source_digest: string | null;
}
export type ReplayDivergenceSeverity = "info" | "warning" | "failure";
// Wave R3 — sealed single-turn replay (turn-keyed; supersedes the W-026
// artifact-keyed ReplayComparison, now retired on both sides).
export type TurnReplayDivergenceSeverity = "critical" | "informational";
export type TurnReplayBasis = "sealed_fresh_runtime_single_turn";
export type TurnReplayOriginState = "unrecorded";
export interface ReplayDivergence {
export interface TurnReplayDivergence {
path: string;
original: unknown;
replay: unknown;
severity: ReplayDivergenceSeverity;
severity: TurnReplayDivergenceSeverity;
}
export interface ReplayComparison {
artifact_id: string;
original_hash: string | null;
replay_hash: string | null;
export interface TurnReplayComparison {
turn_id: number;
comparison_basis: TurnReplayBasis;
origin_state: TurnReplayOriginState;
original_trace_hash: string | null;
replay_trace_hash: string | null;
equivalent: boolean;
divergences: ReplayDivergence[];
replay_turn_cost_ms: number;
divergences: TurnReplayDivergence[];
}
export interface VaultSummary {

View file

@ -210,27 +210,6 @@ class EvalRunResult:
source_digest: str | None = None
ReplayDivergenceSeverity = Literal["info", "warning", "failure"]
ReplayStatus = Literal["equivalent", "not_yet_replayed", "diverged", "evidence_unavailable"]
@dataclass(frozen=True, slots=True)
class ReplayDivergence:
path: str
original: Any
replay: Any
severity: ReplayDivergenceSeverity
@dataclass(frozen=True, slots=True)
class ReplayComparison:
artifact_id: str
original_hash: str | None
replay_hash: str | None
equivalent: bool
divergences: list[ReplayDivergence] = field(default_factory=list)
# ---------------------------------------------------------------------------
# Wave R3 — sealed single-turn replay over the turn journal.
# Scoping: docs/analysis/replay-moment-backend-scoping-2026-06-12.md.