Replaces the W-026 501 stub. Re-executes a journaled prompt in a sealed
fresh runtime (ChatRuntime(no_load_state=True): no checkpoint load, no
checkpoint write, no proposal lineage) and compares the envelope
leaf-by-leaf against the recorded TurnJournalEntry.
Design correction vs the scoping doc (amended in-doc): journaled turns
each ran in a fresh ChatRuntime(), never one continuous session, so
genesis-PREFIX replay would manufacture spurious divergence; shipped
basis is sealed_fresh_runtime_single_turn (O(1)). origin_state:
"unrecorded" — the journal does not record whether the original turn's
runtime loaded a checkpoint, so divergence is reported as nondeterminism
OR origin-state influence, never disambiguated.
- workbench/replay.py: pure comparison + injected executor; every
TurnJournalEntry field classified critical/informational exactly once
(exhaustiveness enforced by test)
- api.py: route wiring under _CHAT_TURN_LOCK; runtime failure -> 500
runtime_unavailable, no comparison may be fabricated
- schemas: additive TurnReplayComparison/TurnReplayDivergence (W-026
artifact-keyed pair retires with the frontend Replay Moment PR)
- tests: 10 obligations incl. tamper-prompt, tamper-leaf precision,
no-execution-no-comparison, no-trace (journal + engine_state bytes),
wall-clock tolerance, sealed-construction proof
- snapshot regenerated; NOT_YET_MIRRORED debt entries for the two new
classes (mirrors land with the frontend PR)
- api-contract-v1.md § Replay rewritten for the turn-keyed shape
- 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.
Routes now publish selection as the evidence subject and read selection
from path params (R0c), so the ADR-0162 conformance harness mounts them
the way App.tsx declares them.
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
Reconciliation on rebase over R0a (#709): vitest's default include
pattern collects e2e/*.spec.ts and fails on Playwright imports.
e2e/ belongs to 'pnpm test:e2e'; vitest owns src/.
Verified post-rebase: vitest 27 files/181 tests EXIT=0 + playwright 12/12.
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).
Brief 4 of the Wave 1 evidence spine — wire the evidence-context architecture
so every route can project into one shared evidence manifold.
Command registry (1A):
- commandRegistry.ts: useSyncExternalStore-backed command store with a cached
snapshot (referentially stable to avoid render loops); useCommands /
useCommandRegistry; recent-items in localStorage (last 10).
- CommandPalette.tsx: reads from the registry; all ten routes are navigation
commands (was Chat/Proposals/Evals hardcoded); fuzzy filter, recent section,
shortcut badges; Router/Fallback split preserved.
- useGlobalKeyboard.ts: Cmd+K palette, Cmd+I inspector, Cmd+1-0 routes, ? help.
Evidence drawer (1B):
- evidenceContext.tsx: EvidenceSubject union (turn|proposal|artifact|
eval_result|none) + useEvidenceSubject hook + EvidenceProvider above the
router Outlet so inspector state persists across route transitions.
- RightInspector.tsx: five evidence projections (was a null stub); reuses the
Brief 2 primitives (MetadataTable, DigestBadge, Timestamp, badges).
- Shell.tsx: visibility driven by inspectorOpen (collapsed by default); no more
hardcoded collapsed=true.
- KeyboardHelp.tsx: shortcut reference dialog.
- TopBar.tsx: palette open state lifted to Shell via props.
Tests: evidenceContext + RightInspector specs added; CommandPalette keyboard
contract updated for the ten-command navigation set (clamp at both ends).
Deferred (noted in spine doc): per-route register()/setSubject call sites,
action-commands (run eval / copy hash), resizable inspector width.
Verified: tsc -b + vite build green; affected specs pass per-file.
SplitPane, TabBar, MetadataTable, DigestBadge, Timestamp, SearchInput.
All token-driven, keyboard-navigable, ARIA-accessible, dark theme,
motion-respectful. No new dependencies — TabBar uses native ARIA
semantics instead of @radix-ui/react-tabs.
PreviewPage updated with all six components in representative states.
Section 1C checked off in wave-1-evidence-spine.md.
* feat(workbench-ui): design system v1 scaffold
* fix(workbench): close R1 (GroundingSource enum coverage) + R4 (digest test)
R1 — Promote GroundingSource to a typed Literal in core/epistemic_state.py
so it has the same single-source-of-truth shape as ReviewState. The
existing epistemic_state_for_grounding_source() function already
enumerates the six labels (pack, teaching, vault, partial, oov, none);
this codifies them.
scripts/dump-enums.py now snapshots GroundingSource via the existing
literal_values helper. workbench-ui's enumCoverage.test.ts gains a
fourth assertion that the badge mapping matches the Python source
1:1. Adding a grounding-source value on the Python side without
updating the badge fails the build-time test loud — same discipline
as the other three enums.
R4 — Add an explicit DigestBadge test to StableJsonViewer.test.tsx:
asserts the badge text matches the SHA-256 prefix of the source bytes,
and clicking the badge copies the FULL digest (not the truncated
prefix). Recomputes the expected digest via crypto.subtle to avoid
hard-coding a hex string that could drift.
R2 (component-level reduced-motion enforcement), R3 (EmptyState
copy-CLI affordance), and R5 (`uv run core` packaging paper cut) are
deferred — R2/R3 become meaningful with W-027/W-029, R5 is a
packaging-layer concern outside this PR's scope.
Validation:
- pnpm test: 19 passed (was 17, +1 enum coverage, +1 digest test)
- pnpm build: clean
- pnpm test:enum-coverage: 4 passed
- core test --suite smoke -q: 67 passed