First R3 (Theater) piece. Reworks the stale artifact-keyed ReplayRoute into
the turn-keyed hero over the #716 sealed-replay backend, and fully retires
the dead W-026 artifact-keyed machinery on both sides (the NOT_YET_MIRRORED
comment anticipated this).
The hero makes determinism *felt*: pick a journaled turn, CORE re-executes
its prompt in a sealed fresh runtime, and the result renders as a hash
verdict (≡ equivalent / ≠ diverged) + the original/replay DigestBadges + a
leaf diff (critical weighted above informational, each with a ≠ glyph). The
'What this proves' card surfaces comparison_basis + origin_state and states
the honest limit: a divergence means nondeterminism OR origin-state
influence, never rendered on its own as a determinism failure.
Retirement (verified zero serving uses):
- Python schemas.py: removed ReplayComparison/ReplayDivergence/ReplayStatus/
ReplayDivergenceSeverity; scripts/dump-enums.py drops the two replay enums;
both snapshots regenerated (deterministic: dump == committed)
- TS: removed ReplayComparison/ReplayDivergence/ReplayDivergenceSeverity;
added TurnReplayComparison/TurnReplayDivergence (+ basis/origin/severity
unions); NOT_YET_MIRRORED now empty (every engine schema mirrored)
- badges: removed ReplayStatusBadge + ReplayDivergenceSeverityBadge +
meta/enums + their enumCoverage cases (hero renders severity inline)
- api: fetchReplayComparison/useReplayComparison -> fetchTurnReplay/
useTurnReplay (/replay/<turnId>)
- deleted ArtifactList, ReplayComparisonPanel, ReplayDiffViewer,
ReplayMetadataTable, old replay.test.tsx
- App route /replay/:artifactId? -> /replay/:turnId?; conformance row
turn-keyed (loading 'Loading turns...', empty 'No turns recorded yet.')
Tests: ReplayRoute.test.tsx (equivalence hero + honesty card, diverged
critical leaf, informational-only label, replace-mode select, j/k spine).
Full vitest 358 green; workbench Python 34 green; both snapshots deterministic.
Follow-up flagged (not in this PR to keep it focused): the artifact query
hooks (useArtifacts/useArtifactDetail/fetchArtifacts*) are now orphaned but
entangled with the still-live artifact EvidenceSubject kind — a separate
cleanup once an Artifacts route or that subject's fate is decided.
Brief: docs/handoff/wave-R3-briefs-2026-06-13.md (all four R3 pieces).
- EvidenceChainRail: the spine's seven stages rendered per subject with
the honesty contract — lit/hollow/dim derive ONLY from carried fields
(a trace hash lights 'replay' as RECORDED, never as verified); pure
deriveStages() with meaningfully-fail tests (remove one field -> exactly
its stage hollows).
- Hash display standard: DigestBadge default 12-char + copy everywhere;
CopyableHash consolidated away (2 call sites swapped, file deleted).
- Typography: tabular-nums on MetadataTable values + eval metrics;
text-wrap balance on Panel titles + EmptyState statements.
- Selection unification: new --color-selected-bg/-border tokens; selected
(persistent) vs focused (transient) now visually distinct across
Proposals/Replay/Evals lists; focus-ring no longer doubles as selection.
- EmptyState gains a deterministic monochrome inline-SVG glyph.
- Panel adopted in Evals lane list + Proposals queue.
- Doctrine-as-tests: hexScan (no palette literals outside tokens.css/
generated tokens.ts) + schemaDrift gate (scripts/dump-schemas.py AST
walk -> schema-snapshot.json -> every dataclass field mirrored in
types/api.ts; explicit shrink-only NOT_YET_MIRRORED debt list).
The drift gate caught real drift on first run: ChatTurnResult.turn_id
existed in Python, missing from TS — now mirrored.
Verified: build green; vitest 36 files / 279 tests EXIT=0; playwright
12/12; dump-schemas.py runs clean.
URL = subject: one codec (evidenceAddress.ts) owns the address grammar for
every EvidenceSubject kind, including kinds whose routes are placeholders.
urlToSubject is a total inverse — malformed input yields null, never throws.
- Route params: /trace/:turnId?, /proposals/:proposalId?, /evals/:laneId?,
/replay/:artifactId? (placeholders keep flat paths)
- ?inspect= carries inspector subject + open state; EvidenceUrlSync restores
deep links and write-through syncs with replace (selection churn never
pollutes history); malformed values are dropped from the URL
- Proposals/Evals/Replay restore selection from their param on load, write it
on selection change (replace), and publish the selected subject to the
evidence context (identity at once, detail when the query resolves)
- EvidenceSubject.data now optional; inspectors render an honest
'detail not loaded' state for identity-only subjects
- Cmd+Shift+C copies origin + subjectToUrl(subject); no-op without subject;
input-focus guard; transient inline 'Copied' on the inspector header
- Fix pre-existing unbounded re-render loop in RightInspector.test.tsx
(render-phase setSubject); subjects now set from effects
Full 'pnpm test' previously hung indefinitely (killed at CI walls,
misdiagnosed as teardown slowness). Two root causes found and fixed:
1. RightInspector.test.tsx called setSubject() during render with a fresh
object every pass -> infinite synchronous render loop (100% CPU spin,
blocks the event loop so no timeout can fire). Moved into useEffect.
2. Test QueryClients used retry:false but default gcTime (5 min) -> a live
GC timer per cached query kept workers from exiting. New shared
createTestQueryClient() with gcTime:0 adopted across all 7 test files.
Hardening so any future leak fails loudly instead of hanging:
- vite.config.ts: testTimeout/hookTimeout 10s, teardownTimeout 5s
- useManagedTimeout hook owns previously-bare setTimeouts in DigestBadge,
MetadataTable, RatificationCommandPanel (4 sites), CommandPalette focus
First-ever frontend CI lane (.github/workflows/workbench-ui.yml):
path-filtered to workbench-ui/** AND the workflow file itself,
pnpm install --frozen-lockfile + build + vitest, timeout-minutes 15.
Honesty fix: KeyboardHelp no longer advertises j/k, /, Enter (unbuilt
until R0d restores them registry-backed).
Route conformance test (ADR-0162 §6, executable): empty/error/loading
contracts asserted for Chat/Proposals/Evals/Replay; fixed the two gaps
it caught (ReplayRoute bare-div error branch -> ErrorState contract;
ArtifactList empty state gained a next action).
Result: 27 files / 181 tests pass, suite EXITS in 4.9s wall-clock
(was: indefinite hang).