feat(workbench-ui): design system v1 scaffold (ADR-0162 Branch 1) (#295)

* 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
This commit is contained in:
Shay 2026-05-26 11:33:27 -07:00 committed by GitHub
parent 5b4dcb17ca
commit e89463a975
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 4738 additions and 1 deletions

View file

@ -9,7 +9,17 @@ serialize stably into JSONL, metadata dictionaries, and test fixtures.
from __future__ import annotations
from enum import Enum, unique
from typing import Any
from typing import Any, Literal
GroundingSource = Literal["pack", "teaching", "vault", "partial", "oov", "none"]
"""Ratified grounding-source labels.
Single source of truth for the values ``epistemic_state_for_grounding_source``
maps, the cold-start-grounding lane validates, and the Workbench UI badges
bind to (ADR-0162 §3d). Adding a value here without adding a corresponding
badge fails the build-time enum coverage test under ``workbench-ui/``.
"""
@unique
@ -136,6 +146,7 @@ def epistemic_state_for_grounding_source(source: str | None) -> EpistemicState:
__all__ = [
"EpistemicState",
"GroundingSource",
"NormativeClearance",
"clearance_from_verdicts",
"coerce_epistemic_state",

View file

@ -0,0 +1,61 @@
# Workbench Design System v1
This document records the Branch 1 substrate for ADR-0162, cross-referenced with
[ADR-0160](../decisions/ADR-0160-core-workbench-v1.md) and
[ADR-0161](../decisions/ADR-0161-hitl-async-queue.md).
The implementation lives in `workbench-ui/`. The preview page is `/preview`:
```bash
cd workbench-ui && pnpm install && pnpm preview
```
Branch 1 is intentionally static and read-only. It adds no backend, no API
client, no route shell beyond `/preview`, and no runtime mutation surface.
## Token Substrate
Tokens are defined in `workbench-ui/src/design/tokens/tokens.css` and mirrored
to typed TypeScript by `workbench-ui/scripts/generate-tokens.ts`. The build
prehook regenerates `tokens.ts`; the token test fails if CSS and TypeScript
diverge.
The theme is dark-only. Fonts are self-hosted from `workbench-ui/public/fonts/`
and referenced through local `@font-face` rules only.
## Badge Tables
Badge components live under `workbench-ui/src/design/components/badges/`.
Each component accepts a typed enum value, never an arbitrary string.
| Badge | Values | Source |
|---|---:|---|
| `EpistemicStateBadge` | 15 | `core/epistemic_state.py` |
| `NormativeClearanceBadge` | 4 | `core/epistemic_state.py` |
| `ReviewStateBadge` | 4 | `teaching/proposals.py` |
| `GroundingSourceBadge` | 6 | ADR-0162 Branch 1 contract |
| `TraceHashBadge` | copyable digest | ADR-0153 / ADR-0160 |
`scripts/dump-enums.py` performs a read-only AST walk over the Python enum
sources. `pnpm test:enum-coverage` regenerates `workbench-ui/enum-snapshot.json`
and asserts exact UI coverage so engine enum drift fails loudly at build/test
time.
## JSON Viewer
`StableJsonViewer` preserves raw source spans for strings and numbers, renders
object keys in deterministic lexicographic order, copies JSON Pointer paths,
renders a source-byte SHA-256 badge, supports side-by-side leaf diffs, and
refuses inline rendering above 16 MiB.
## Motion And Keyboard
All motion uses tokenized durations/easing. `prefers-reduced-motion: reduce`
collapses tokenized durations to instant.
The preview page installs the Branch 1 keyboard baseline:
- `Cmd+K` / `Ctrl+K` opens the stub command palette.
- `Esc` closes overlays.
- Interactive primitives use `--color-focus-ring`.
- Preview tab order follows DOM order.

60
scripts/dump-enums.py Normal file
View file

@ -0,0 +1,60 @@
"""Dump ratified engine enum values for Workbench UI coverage tests.
Read-only helper: parses source with Python AST and writes JSON to stdout.
"""
from __future__ import annotations
import ast
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def enum_values(path: Path, class_name: str) -> list[str]:
module = ast.parse(path.read_text(encoding="utf-8"))
for node in module.body:
if isinstance(node, ast.ClassDef) and node.name == class_name:
values: list[str] = []
for statement in node.body:
if (
isinstance(statement, ast.Assign)
and isinstance(statement.value, ast.Constant)
and isinstance(statement.value.value, str)
):
values.append(statement.value.value)
return values
raise SystemExit(f"enum class not found: {class_name}")
def literal_values(path: Path, name: str) -> list[str]:
module = ast.parse(path.read_text(encoding="utf-8"))
for node in module.body:
if isinstance(node, ast.Assign):
if not any(isinstance(target, ast.Name) and target.id == name for target in node.targets):
continue
value = node.value
if (
isinstance(value, ast.Subscript)
and isinstance(value.value, ast.Name)
and value.value.id == "Literal"
):
items = value.slice.elts if isinstance(value.slice, ast.Tuple) else [value.slice]
return [
item.value
for item in items
if isinstance(item, ast.Constant) and isinstance(item.value, str)
]
raise SystemExit(f"literal alias not found: {name}")
snapshot = {
"EpistemicState": enum_values(ROOT / "core" / "epistemic_state.py", "EpistemicState"),
"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"),
}
print(json.dumps(snapshot, indent=2, sort_keys=True))

7
workbench-ui/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
node_modules/
dist/
*.tsbuildinfo
vite.config.js
vite.config.d.ts
scripts/*.js
scripts/*.d.ts

View file

@ -0,0 +1,39 @@
{
"EpistemicState": [
"perceived",
"evidenced",
"evidenced_incomplete",
"verified",
"decoded",
"decoded_unarticulated",
"inferred",
"unverified_possible",
"unverified_novel",
"contradicted",
"ambiguous",
"undetermined",
"scope_boundary",
"computationally_bounded",
"epistemic_state_needed"
],
"GroundingSource": [
"pack",
"teaching",
"vault",
"partial",
"oov",
"none"
],
"NormativeClearance": [
"cleared",
"violated",
"unassessable",
"suppressed"
],
"ReviewState": [
"pending",
"accepted",
"rejected",
"withdrawn"
]
}

12
workbench-ui/index.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CORE Workbench Preview</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

45
workbench-ui/package.json Normal file
View file

@ -0,0 +1,45 @@
{
"name": "core-workbench-ui",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"prebuild": "pnpm generate:tokens",
"build": "tsc -b && vite build",
"dev": "vite --host 127.0.0.1",
"preview": "vite preview --host 127.0.0.1",
"test": "vitest run",
"test:enum-coverage": "pnpm enum:snapshot && vitest run src/design/components/badges/enumCoverage.test.ts",
"generate:tokens": "tsx scripts/generate-tokens.ts",
"enum:snapshot": "cd .. && uv run python scripts/dump-enums.py > workbench-ui/enum-snapshot.json"
},
"dependencies": {
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-popover": "1.1.15",
"@radix-ui/react-slot": "1.2.3",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"lucide-react": "0.468.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"tailwind-merge": "2.6.0"
},
"devDependencies": {
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.1.0",
"@testing-library/user-event": "14.5.2",
"@types/node": "22.10.2",
"@types/react": "18.3.17",
"@types/react-dom": "18.3.5",
"@vitejs/plugin-react": "4.3.4",
"autoprefixer": "10.4.20",
"happy-dom": "15.11.7",
"postcss": "8.4.49",
"tailwindcss": "3.4.17",
"tsx": "4.19.2",
"typescript": "5.7.2",
"vite": "5.4.11",
"vitest": "2.1.8"
},
"packageManager": "pnpm@9.15.0"
}

3113
workbench-ui/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View file

@ -0,0 +1,8 @@
# Preview
The Vite route at `/preview` renders the Branch 1 design substrate.
```bash
pnpm build
pnpm preview
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,22 @@
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const cssPath = resolve(here, "../src/design/tokens/tokens.css");
const outPath = resolve(here, "../src/design/tokens/tokens.ts");
const css = readFileSync(cssPath, "utf8");
const tokenMatches = [...css.matchAll(/--([a-z0-9-]+):\s*([^;]+);/g)];
const tokens = Object.fromEntries(
tokenMatches.map(([, name, value]) => [name, value.trim()]),
);
const body = `// Generated by scripts/generate-tokens.ts. Do not edit by hand.
export const tokens = ${JSON.stringify(tokens, null, 2)} as const;
export type DesignTokenName = keyof typeof tokens;
export type DesignTokenValue = (typeof tokens)[DesignTokenName];
`;
writeFileSync(outPath, body);

View file

@ -0,0 +1,88 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { StableJsonViewer } from "./StableJsonViewer";
import { diffLeaves, leaves, parseJsonSource } from "./jsonModel";
async function sha256Hex(source: string): Promise<string> {
const bytes = new TextEncoder().encode(source);
const hash = await crypto.subtle.digest("SHA-256", bytes);
return [...new Uint8Array(hash)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
describe("StableJsonViewer invariants", () => {
it("renders object keys in deterministic lexicographic order", () => {
render(<StableJsonViewer source='{"z":1,"a":2}' />);
const rows = within(screen.getByTestId("json-rows")).getAllByRole("button");
expect(rows.map((row) => row.textContent)).toEqual(["/a 2", "/z 1"]);
});
it("preserves source string bytes without smart quotes or entity coercion", () => {
const source = "{\"quote\":\"a \\\"raw\\\" & <tag>\"}";
render(<StableJsonViewer source={source} />);
expect(screen.getByText(/"a \\"raw\\" & <tag>"/)).toBeInTheDocument();
});
it("preserves numeric notation and int/float distinction", () => {
render(<StableJsonViewer source='{"epsilon":1e-6,"int":1,"float":1.0}' />);
expect(screen.getByText((_, node) => node?.textContent === "/epsilon 1e-6")).toBeInTheDocument();
expect(screen.getByText((_, node) => node?.textContent === "/int 1")).toBeInTheDocument();
expect(screen.getByText((_, node) => node?.textContent === "/float 1.0")).toBeInTheDocument();
});
it("copy-path returns a JSON Pointer", async () => {
const user = userEvent.setup();
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText },
});
render(<StableJsonViewer source='{"scenes":[0,0,0,{"detail":{"object":"lens"}}]}' />);
await user.click(screen.getByText(/\/scenes\/3\/detail\/object/));
expect(writeText).toHaveBeenCalledWith("/scenes/3/detail/object");
});
it("diff mode is side-by-side and marks only changed leaves with glyphs", () => {
render(<StableJsonViewer source='{"same":1,"changed":1}' compareSource='{"same":1,"changed":2,"added":3}' />);
expect(screen.getByTestId("json-diff")).toBeInTheDocument();
expect(screen.getAllByLabelText("changed")).toHaveLength(2);
expect(screen.getAllByLabelText("added")).toHaveLength(2);
expect(screen.getAllByLabelText("same")).toHaveLength(2);
const modelDiff = diffLeaves(leaves(parseJsonSource('{"a":1}')), leaves(parseJsonSource('{"a":2}')));
expect(modelDiff).toMatchObject([{ pointer: "/a", kind: "changed" }]);
});
it("virtualizes large documents and refuses inline render above 16 MiB", () => {
const manyLeaves = `{"items":[${Array.from({ length: 1001 }, (_, i) => i).join(",")}]}`;
const { rerender } = render(<StableJsonViewer source={manyLeaves} />);
expect(screen.getByTestId("virtualized-json")).toHaveTextContent("virtualized 1000 of 1001 leaves");
rerender(<StableJsonViewer source={`{"blob":"${"x".repeat(16 * 1024 * 1024)}"}`} />);
expect(screen.getByText(/larger than 16 MiB/)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Open in external viewer/ })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Copy path/ })).toBeInTheDocument();
});
it("renders a sha256 digest badge over the source bytes and copies the full digest on click", async () => {
const user = userEvent.setup();
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText },
});
const source = '{"a":1}';
const expectedDigest = await sha256Hex(source);
render(<StableJsonViewer source={source} />);
const digestButton = await screen.findByRole("button", { name: /sha256:/ });
// The badge displays the truncated prefix.
await waitFor(() => {
expect(digestButton.textContent).toBe(`sha256:${expectedDigest.slice(0, 12)}`);
});
// Clicking the badge copies the FULL digest, not the truncated prefix.
await user.click(digestButton);
expect(writeText).toHaveBeenCalledWith(expectedDigest);
});
});

View file

@ -0,0 +1,136 @@
import { Copy, ExternalLink } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Button } from "../primitives/Button";
import { copyText, cn } from "../../lib";
import { countLeaves, diffLeaves, leaves, parseJsonSource, type DiffKind, type JsonNode } from "./jsonModel";
const MAX_INLINE_BYTES = 16 * 1024 * 1024;
const VIRTUALIZE_LEAVES = 1000;
function bytes(source: string) {
return new TextEncoder().encode(source);
}
async function sha256(source: string) {
const hash = await crypto.subtle.digest("SHA-256", bytes(source));
return [...new Uint8Array(hash)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
function nodeRows(node: JsonNode, pointer = "", depth = 0): { pointer: string; depth: number; label: string; raw?: string }[] {
if (node.kind === "literal") return [{ pointer, depth, label: pointer.split("/").at(-1) || "/", raw: node.raw }];
if (node.kind === "array") {
return node.items.flatMap((item, index) => nodeRows(item, `${pointer}/${index}`, depth + 1));
}
return node.entries.flatMap((entry) => nodeRows(entry.value, `${pointer}/${entry.key.replaceAll("~", "~0").replaceAll("/", "~1")}`, depth + 1));
}
function DigestBadge({ source }: { source: string }) {
const [digest, setDigest] = useState("");
useEffect(() => {
void sha256(source).then(setDigest);
}, [source]);
return (
<button
className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] px-2 py-1 font-mono text-xs text-[var(--color-text-secondary)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]"
onClick={() => void copyText(digest)}
type="button"
>
sha256:{digest.slice(0, 12) || "pending"}
</button>
);
}
function ChangeGlyph({ kind }: { kind: DiffKind }) {
const glyph = kind === "added" ? "+" : kind === "removed" ? "-" : kind === "changed" ? "~" : " ";
return <span aria-label={kind} className="inline-block w-4 font-mono">{glyph}</span>;
}
export function StableJsonViewer({
source,
compareSource,
}: {
source: string;
compareSource?: string;
}) {
const rawBytes = bytes(source).byteLength;
const parsed = useMemo(() => (rawBytes <= MAX_INLINE_BYTES ? parseJsonSource(source) : null), [rawBytes, source]);
const leafCount = parsed ? countLeaves(parsed) : 0;
if (rawBytes > MAX_INLINE_BYTES) {
return (
<section className="rounded-lg border border-[var(--color-border-strong)] bg-[var(--color-surface-raised)] p-4">
<div className="mb-3 flex items-center gap-2">
<DigestBadge source={source} />
</div>
<p className="text-sm text-[var(--color-text-secondary)]">JSON is larger than 16 MiB and was not rendered inline.</p>
<Button onClick={() => void copyText("/")} type="button" variant="quiet">
<ExternalLink size={14} aria-hidden />
Open in external viewer
</Button>
<Button className="ml-2" onClick={() => void copyText("/")} type="button" variant="quiet">
<Copy size={14} aria-hidden />
Copy path /
</Button>
</section>
);
}
if (compareSource) {
const right = parseJsonSource(compareSource);
const rows = diffLeaves(leaves(parsed!), leaves(right));
return (
<section className="rounded-lg border border-[var(--color-border-strong)] bg-[var(--color-surface-raised)] p-4">
<div className="mb-3 flex items-center gap-2">
<DigestBadge source={source} />
<span className="text-xs text-[var(--color-text-muted)]">diff mode</span>
</div>
<div className="grid grid-cols-[1fr_1fr] gap-3" data-testid="json-diff">
{rows.map((row) => (
<div className="contents" key={row.pointer}>
<button
className={cn("rounded border px-2 py-1 text-left font-mono text-xs", row.kind !== "same" ? "border-[var(--color-state-warning-border)]" : "border-[var(--color-border-subtle)]")}
onClick={() => void copyText(row.pointer)}
type="button"
>
<ChangeGlyph kind={row.kind} />
{row.pointer}: {row.before?.raw ?? ""}
</button>
<button
className={cn("rounded border px-2 py-1 text-left font-mono text-xs", row.kind !== "same" ? "border-[var(--color-state-warning-border)]" : "border-[var(--color-border-subtle)]")}
onClick={() => void copyText(row.pointer)}
type="button"
>
<ChangeGlyph kind={row.kind} />
{row.pointer}: {row.after?.raw ?? ""}
</button>
</div>
))}
</div>
</section>
);
}
const rows = nodeRows(parsed!);
const visibleRows = leafCount > VIRTUALIZE_LEAVES ? rows.slice(0, VIRTUALIZE_LEAVES) : rows;
return (
<section className="rounded-lg border border-[var(--color-border-strong)] bg-[var(--color-surface-raised)] p-4">
<div className="mb-3 flex items-center gap-2">
<DigestBadge source={source} />
{leafCount > VIRTUALIZE_LEAVES ? <span data-testid="virtualized-json" className="text-xs text-[var(--color-text-muted)]">virtualized {VIRTUALIZE_LEAVES} of {leafCount} leaves</span> : null}
</div>
<div className="grid gap-1" data-testid="json-rows">
{visibleRows.map((row) => (
<button
className="rounded border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] px-2 py-1 text-left font-mono text-xs text-[var(--color-text-secondary)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]"
key={row.pointer}
onClick={() => void copyText(row.pointer)}
style={{ paddingLeft: `${8 + row.depth * 12}px` }}
type="button"
>
<span className="text-[var(--color-text-muted)]">{row.pointer || "/"}</span> {row.raw}
</button>
))}
</div>
</section>
);
}

View file

@ -0,0 +1,2 @@
export { StableJsonViewer } from "./StableJsonViewer";
export { parseJsonSource, leaves, diffLeaves } from "./jsonModel";

View file

@ -0,0 +1,155 @@
export type JsonNode =
| { kind: "object"; entries: { key: string; keyRaw: string; value: JsonNode }[] }
| { kind: "array"; items: JsonNode[] }
| { kind: "literal"; raw: string; valueKind: "string" | "number" | "boolean" | "null" };
export type Leaf = { pointer: string; raw: string; valueKind: string };
class Parser {
private index = 0;
constructor(private readonly source: string) {}
parse(): JsonNode {
this.ws();
const node = this.value();
this.ws();
if (this.index !== this.source.length) throw new Error("Unexpected trailing JSON content.");
return node;
}
private value(): JsonNode {
this.ws();
const ch = this.source[this.index];
if (ch === "{") return this.object();
if (ch === "[") return this.array();
if (ch === '"') return { kind: "literal", raw: this.stringRaw(), valueKind: "string" };
if (ch === "-" || /\d/.test(ch)) return { kind: "literal", raw: this.numberRaw(), valueKind: "number" };
for (const word of ["true", "false", "null"] as const) {
if (this.source.startsWith(word, this.index)) {
this.index += word.length;
return { kind: "literal", raw: word, valueKind: word === "null" ? "null" : "boolean" };
}
}
throw new Error(`Unexpected JSON token at ${this.index}.`);
}
private object(): JsonNode {
this.index++;
this.ws();
const entries: { key: string; keyRaw: string; value: JsonNode }[] = [];
if (this.source[this.index] === "}") {
this.index++;
return { kind: "object", entries };
}
while (this.index < this.source.length) {
const keyRaw = this.stringRaw();
const key = JSON.parse(keyRaw) as string;
this.ws();
this.expect(":");
const value = this.value();
entries.push({ key, keyRaw, value });
this.ws();
if (this.source[this.index] === "}") {
this.index++;
break;
}
this.expect(",");
this.ws();
}
entries.sort((a, b) => a.key.localeCompare(b.key));
return { kind: "object", entries };
}
private array(): JsonNode {
this.index++;
this.ws();
const items: JsonNode[] = [];
if (this.source[this.index] === "]") {
this.index++;
return { kind: "array", items };
}
while (this.index < this.source.length) {
items.push(this.value());
this.ws();
if (this.source[this.index] === "]") {
this.index++;
break;
}
this.expect(",");
this.ws();
}
return { kind: "array", items };
}
private stringRaw(): string {
const start = this.index;
this.expect('"');
while (this.index < this.source.length) {
const ch = this.source[this.index++];
if (ch === "\\") {
this.index++;
} else if (ch === '"') {
return this.source.slice(start, this.index);
}
}
throw new Error("Unterminated string.");
}
private numberRaw(): string {
const start = this.index;
if (this.source[this.index] === "-") this.index++;
while (/\d/.test(this.source[this.index] ?? "")) this.index++;
if (this.source[this.index] === ".") {
this.index++;
while (/\d/.test(this.source[this.index] ?? "")) this.index++;
}
if ((this.source[this.index] ?? "").toLowerCase() === "e") {
this.index++;
if (["+", "-"].includes(this.source[this.index] ?? "")) this.index++;
while (/\d/.test(this.source[this.index] ?? "")) this.index++;
}
return this.source.slice(start, this.index);
}
private ws() {
while (/\s/.test(this.source[this.index] ?? "")) this.index++;
}
private expect(ch: string) {
if (this.source[this.index] !== ch) throw new Error(`Expected ${ch} at ${this.index}.`);
this.index++;
}
}
export function parseJsonSource(source: string): JsonNode {
return new Parser(source).parse();
}
export function pointerPart(value: string) {
return value.replaceAll("~", "~0").replaceAll("/", "~1");
}
export function leaves(node: JsonNode, pointer = ""): Leaf[] {
if (node.kind === "literal") return [{ pointer, raw: node.raw, valueKind: node.valueKind }];
if (node.kind === "array") return node.items.flatMap((item, index) => leaves(item, `${pointer}/${index}`));
return node.entries.flatMap((entry) => leaves(entry.value, `${pointer}/${pointerPart(entry.key)}`));
}
export function countLeaves(node: JsonNode) {
return leaves(node).length;
}
export type DiffKind = "added" | "removed" | "changed" | "same";
export function diffLeaves(left: Leaf[], right: Leaf[]) {
const l = new Map(left.map((leaf) => [leaf.pointer, leaf]));
const r = new Map(right.map((leaf) => [leaf.pointer, leaf]));
const pointers = [...new Set([...l.keys(), ...r.keys()])].toSorted();
return pointers.map((pointer) => {
const before = l.get(pointer);
const after = r.get(pointer);
const kind: DiffKind = before && after ? (before.raw === after.raw ? "same" : "changed") : before ? "removed" : "added";
return { pointer, before, after, kind };
});
}

View file

@ -0,0 +1,67 @@
import * as Popover from "@radix-ui/react-popover";
import { Copy } from "lucide-react";
import type { CSSProperties } from "react";
import { copyText, cn } from "../../lib";
export function InfoBadge({
label,
colorToken,
meaning,
adr,
evidence,
mono = false,
onCopy,
}: {
label: string;
colorToken: string;
meaning: string;
adr: string;
evidence: string;
mono?: boolean;
onCopy?: string;
}) {
const style = {
backgroundColor: `color-mix(in srgb, var(${colorToken}) 18%, transparent)`,
borderColor: `var(${colorToken})`,
color: `var(${colorToken})`,
} as CSSProperties;
return (
<Popover.Root>
<Popover.Trigger
className={cn(
"inline-flex h-7 items-center gap-1 rounded-md border px-2 text-xs font-medium transition-colors motion-standard focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]",
mono && "font-mono",
)}
style={style}
>
{label}
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
sideOffset={6}
className="z-50 w-72 rounded-lg border border-[var(--color-border-strong)] bg-[var(--color-surface-overlay)] p-3 text-sm text-[var(--color-text-primary)] shadow-[var(--shadow-overlay)]"
>
<div className="font-semibold">{label}</div>
<p className="my-2 text-[var(--color-text-secondary)]">{meaning}</p>
<dl className="m-0 grid gap-1 text-xs">
<dt className="text-[var(--color-text-muted)]">Pinned by</dt>
<dd className="m-0">{adr}</dd>
<dt className="text-[var(--color-text-muted)]">Evidence example</dt>
<dd className="m-0">{evidence}</dd>
</dl>
{onCopy ? (
<button
className="mt-3 inline-flex items-center gap-1 rounded border border-[var(--color-border-subtle)] px-2 py-1 text-xs focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]"
onClick={() => void copyText(onCopy)}
type="button"
>
<Copy size={12} aria-hidden />
Copy
</button>
) : null}
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}

View file

@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import snapshot from "../../../../enum-snapshot.json";
import {
epistemicStateMeta,
groundingSourceMeta,
normativeClearanceMeta,
reviewStateMeta,
} from "./mappings";
function expectExactCoverage(name: string, engineValues: string[], uiValues: string[]) {
expect(
uiValues.toSorted(),
`${name} badge mapping must exactly match engine enum values`,
).toEqual(engineValues.toSorted());
expect(new Set(uiValues).size, `${name} badge mapping has duplicate values`).toBe(uiValues.length);
}
describe("build-time enum coverage", () => {
it("tracks every ratified EpistemicState value exactly once", () => {
expectExactCoverage(
"EpistemicState",
snapshot.EpistemicState,
Object.keys(epistemicStateMeta),
);
});
it("tracks every ratified NormativeClearance value exactly once", () => {
expectExactCoverage(
"NormativeClearance",
snapshot.NormativeClearance,
Object.keys(normativeClearanceMeta),
);
});
it("tracks every ratified ReviewState value exactly once", () => {
expectExactCoverage("ReviewState", snapshot.ReviewState, Object.keys(reviewStateMeta));
});
it("tracks every ratified GroundingSource value exactly once", () => {
expectExactCoverage(
"GroundingSource",
snapshot.GroundingSource,
Object.keys(groundingSourceMeta),
);
});
});

View file

@ -0,0 +1,47 @@
import {
epistemicStateMeta,
groundingSourceMeta,
normativeClearanceMeta,
reviewStateMeta,
} from "./mappings";
import { InfoBadge } from "./Badge";
import { EpistemicState, GroundingSource, NormativeClearance, ReviewState } from "./types";
export { EpistemicState, GroundingSource, NormativeClearance, ReviewState };
export function EpistemicStateBadge({ value }: { value: EpistemicState }) {
return <InfoBadge {...epistemicStateMeta[value]} />;
}
export function NormativeClearanceBadge({ value }: { value: NormativeClearance }) {
return <InfoBadge {...normativeClearanceMeta[value]} />;
}
export function ReviewStateBadge({ value }: { value: ReviewState }) {
return <InfoBadge {...reviewStateMeta[value]} />;
}
export function GroundingSourceBadge({ value }: { value: GroundingSource }) {
return <InfoBadge {...groundingSourceMeta[value]} />;
}
export function TraceHashBadge({
value,
truncate = 12,
}: {
value: string;
truncate?: number;
}) {
const label = value.slice(0, truncate);
return (
<InfoBadge
label={label}
colorToken="--color-text-mono"
meaning="Deterministic trace hash. Click copy for the full digest."
adr="ADR-0153 / ADR-0160 / ADR-0162"
evidence="TurnEvent.trace_hash is present."
mono
onCopy={value}
/>
);
}

View file

@ -0,0 +1,48 @@
import {
EpistemicState,
GroundingSource,
NormativeClearance,
ReviewState,
type BadgeMeta,
} from "./types";
export const epistemicStateMeta = {
[EpistemicState.PERCEIVED]: { label: "Perceived", colorToken: "--color-state-perceived", meaning: "Observed at the surface but not yet independently evidenced.", adr: "ADR-0142 / ADR-0162", evidence: "A trace carries raw input evidence." },
[EpistemicState.EVIDENCED]: { label: "Evidenced", colorToken: "--color-state-evidenced", meaning: "Supported by explicit evidence.", adr: "ADR-0142 / ADR-0162", evidence: "A proposition cites a supporting artifact." },
[EpistemicState.EVIDENCED_INCOMPLETE]: { label: "Evidenced incomplete", colorToken: "--color-state-evidenced-muted", meaning: "Some evidence exists, but coverage is partial.", adr: "ADR-0142 / ADR-0162", evidence: "Grounding source is partial." },
[EpistemicState.VERIFIED]: { label: "Verified", colorToken: "--color-state-verified", meaning: "Checked against an authoritative deterministic lane.", adr: "ADR-0142 / ADR-0162", evidence: "Replay hash matches original." },
[EpistemicState.DECODED]: { label: "Decoded", colorToken: "--color-state-decoded", meaning: "Mapped through ratified semantic structure.", adr: "ADR-0142 / ADR-0162", evidence: "Grounding source is pack, teaching, or vault." },
[EpistemicState.DECODED_UNARTICULATED]: { label: "Decoded unarticulated", colorToken: "--color-state-decoded-muted", meaning: "Structured internally but not surfaced.", adr: "ADR-0142 / ADR-0162", evidence: "Graph node exists without realized sentence." },
[EpistemicState.INFERRED]: { label: "Inferred", colorToken: "--color-state-inferred", meaning: "Derived from available structure.", adr: "ADR-0142 / ADR-0162", evidence: "Planner derives a relation from linked propositions." },
[EpistemicState.UNVERIFIED_POSSIBLE]: { label: "Unverified possible", colorToken: "--color-state-unverified", meaning: "Plausible but not verified.", adr: "ADR-0142 / ADR-0162", evidence: "Candidate has no ratified support yet." },
[EpistemicState.UNVERIFIED_NOVEL]: { label: "Unverified novel", colorToken: "--color-state-unverified-warm", meaning: "New or OOV content without ratified grounding.", adr: "ADR-0142 / ADR-0162", evidence: "Grounding source is oov." },
[EpistemicState.CONTRADICTED]: { label: "Contradicted", colorToken: "--color-state-contradicted", meaning: "Evidence conflicts with the proposition.", adr: "ADR-0142 / ADR-0162", evidence: "A boundary or claim rejects the assertion." },
[EpistemicState.AMBIGUOUS]: { label: "Ambiguous", colorToken: "--color-state-ambiguous", meaning: "Multiple readings remain live.", adr: "ADR-0142 / ADR-0162", evidence: "Parser retains competing interpretations." },
[EpistemicState.UNDETERMINED]: { label: "Undetermined", colorToken: "--color-state-undetermined", meaning: "No stronger epistemic state is justified.", adr: "ADR-0142 / ADR-0162", evidence: "No grounding source is present." },
[EpistemicState.SCOPE_BOUNDARY]: { label: "Scope boundary", colorToken: "--color-state-scope", meaning: "The claim is outside the active scope.", adr: "ADR-0142 / ADR-0162", evidence: "Runtime declines to classify beyond scope." },
[EpistemicState.COMPUTATIONALLY_BOUNDED]: { label: "Computationally bounded", colorToken: "--color-state-bounded", meaning: "The answer is limited by bounded computation.", adr: "ADR-0142 / ADR-0162", evidence: "Eval lane reports a bounded operator." },
[EpistemicState.EPISTEMIC_STATE_NEEDED]: { label: "State needed", colorToken: "--color-state-needed", meaning: "A missing state must be assigned before trust display.", adr: "ADR-0142 / ADR-0162", evidence: "Unknown grounding source reached UI mapping." },
} satisfies BadgeMeta<EpistemicState>;
export const normativeClearanceMeta = {
[NormativeClearance.CLEARED]: { label: "Cleared", colorToken: "--color-clearance-cleared", meaning: "Runtime-checkable normative gates passed.", adr: "ADR-0142 / ADR-0162", evidence: "Safety and ethics verdicts upheld." },
[NormativeClearance.VIOLATED]: { label: "Violated", colorToken: "--color-clearance-violated", meaning: "A runtime-checkable boundary failed.", adr: "ADR-0142 / ADR-0162", evidence: "Boundary id appears in normative detail." },
[NormativeClearance.UNASSESSABLE]: { label: "Unassessable", colorToken: "--color-clearance-unassessable", meaning: "No positive clearance can be asserted.", adr: "ADR-0142 / ADR-0162", evidence: "No runtime-checkable verdict exists." },
[NormativeClearance.SUPPRESSED]: { label: "Suppressed", colorToken: "--color-clearance-suppressed", meaning: "A refusal surface replaced unsafe output.", adr: "ADR-0142 / ADR-0162", evidence: "refusal_emitted is true." },
} satisfies BadgeMeta<NormativeClearance>;
export const reviewStateMeta = {
[ReviewState.PENDING]: { label: "Pending", colorToken: "--color-review-pending", meaning: "Proposal awaits operator review.", adr: "ADR-0057 / ADR-0161 / ADR-0162", evidence: "No terminal transition exists." },
[ReviewState.ACCEPTED]: { label: "Accepted", colorToken: "--color-review-accepted", meaning: "Operator ratified the proposal.", adr: "ADR-0057 / ADR-0161 / ADR-0162", evidence: "transition.to is accepted." },
[ReviewState.REJECTED]: { label: "Rejected", colorToken: "--color-review-rejected", meaning: "Operator rejected the proposal.", adr: "ADR-0057 / ADR-0161 / ADR-0162", evidence: "transition.to is rejected." },
[ReviewState.WITHDRAWN]: { label: "Withdrawn", colorToken: "--color-review-withdrawn", meaning: "Proposal was withdrawn without ratification.", adr: "ADR-0057 / ADR-0161 / ADR-0162", evidence: "transition.to is withdrawn." },
} satisfies BadgeMeta<ReviewState>;
export const groundingSourceMeta = {
[GroundingSource.PACK]: { label: "Pack", colorToken: "--color-grounding-pack", meaning: "Grounded in a curated semantic pack.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is pack." },
[GroundingSource.TEACHING]: { label: "Teaching", colorToken: "--color-grounding-teaching", meaning: "Grounded in reviewed teaching memory.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is teaching." },
[GroundingSource.VAULT]: { label: "Vault", colorToken: "--color-grounding-vault", meaning: "Grounded in deterministic vault recall.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is vault." },
[GroundingSource.PARTIAL]: { label: "Partial", colorToken: "--color-grounding-partial", meaning: "Only partial grounding was available.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is partial." },
[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>;

View file

@ -0,0 +1,51 @@
export enum EpistemicState {
PERCEIVED = "perceived",
EVIDENCED = "evidenced",
EVIDENCED_INCOMPLETE = "evidenced_incomplete",
VERIFIED = "verified",
DECODED = "decoded",
DECODED_UNARTICULATED = "decoded_unarticulated",
INFERRED = "inferred",
UNVERIFIED_POSSIBLE = "unverified_possible",
UNVERIFIED_NOVEL = "unverified_novel",
CONTRADICTED = "contradicted",
AMBIGUOUS = "ambiguous",
UNDETERMINED = "undetermined",
SCOPE_BOUNDARY = "scope_boundary",
COMPUTATIONALLY_BOUNDED = "computationally_bounded",
EPISTEMIC_STATE_NEEDED = "epistemic_state_needed",
}
export enum NormativeClearance {
CLEARED = "cleared",
VIOLATED = "violated",
UNASSESSABLE = "unassessable",
SUPPRESSED = "suppressed",
}
export enum ReviewState {
PENDING = "pending",
ACCEPTED = "accepted",
REJECTED = "rejected",
WITHDRAWN = "withdrawn",
}
export enum GroundingSource {
PACK = "pack",
TEACHING = "teaching",
VAULT = "vault",
PARTIAL = "partial",
OOV = "oov",
NONE = "none",
}
export type BadgeMeta<T extends string> = Record<
T,
{
label: string;
colorToken: string;
meaning: string;
adr: string;
evidence: string;
}
>;

View file

@ -0,0 +1,30 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import type { ButtonHTMLAttributes } from "react";
import { cn } from "../../lib";
const buttonVariants = cva(
"inline-flex h-9 items-center justify-center gap-2 rounded-md border px-3 text-sm font-medium transition-colors motion-standard focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)] disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
primary:
"border-[var(--color-border-strong)] bg-[var(--color-surface-overlay)] text-[var(--color-text-primary)] hover:bg-[var(--color-surface-raised)]",
quiet:
"border-[var(--color-border-subtle)] bg-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
},
},
defaultVariants: { variant: "primary" },
},
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export function Button({ className, variant, asChild, ...props }: ButtonProps) {
const Comp = asChild ? Slot : "button";
return <Comp className={cn(buttonVariants({ variant }), className)} {...props} />;
}

View file

@ -0,0 +1,32 @@
import * as Dialog from "@radix-ui/react-dialog";
import { Search } from "lucide-react";
import { EmptyState } from "../states/EmptyState";
export function CommandPalette({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/55" data-testid="overlay" />
<Dialog.Content className="fixed left-1/2 top-[18vh] w-[min(560px,calc(100vw-32px))] -translate-x-1/2 rounded-lg border border-[var(--color-border-strong)] bg-[var(--color-surface-overlay)] p-4 shadow-[var(--shadow-overlay)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]">
<Dialog.Title className="mb-3 flex items-center gap-2 text-sm font-semibold">
<Search size={16} aria-hidden />
Command Palette
</Dialog.Title>
<Dialog.Description className="sr-only">
Empty command palette stub for the Branch 1 keyboard contract.
</Dialog.Description>
<EmptyState
statement="No commands are registered in Branch 1."
nextAction="W-027 will add command content behind this keybinding."
/>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View file

@ -0,0 +1,18 @@
import { Button } from "../primitives/Button";
export function EmptyState({
statement,
nextAction,
}: {
statement: string;
nextAction: string;
}) {
return (
<section className="rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-4">
<p className="m-0 text-sm text-[var(--color-text-primary)]">{statement}</p>
<Button className="mt-3" variant="quiet" type="button">
{nextAction}
</Button>
</section>
);
}

View file

@ -0,0 +1,34 @@
export function ErrorState({
whatFailed,
mutationStatus,
reproducer,
retrySafety,
}: {
whatFailed: string;
mutationStatus: string;
reproducer: string;
retrySafety: string;
}) {
return (
<section className="rounded-lg border border-[var(--color-state-danger-border)] bg-[var(--color-state-danger-bg)] p-4 text-sm text-[var(--color-state-danger-text)]">
<dl className="m-0 grid gap-2">
<div>
<dt className="font-semibold">What failed</dt>
<dd className="m-0">{whatFailed}</dd>
</div>
<div>
<dt className="font-semibold">Mutation status</dt>
<dd className="m-0">{mutationStatus}</dd>
</div>
<div>
<dt className="font-semibold">Reproducer</dt>
<dd className="m-0 font-mono text-xs">{reproducer}</dd>
</div>
<div>
<dt className="font-semibold">Retry safety</dt>
<dd className="m-0">{retrySafety}</dd>
</div>
</dl>
</section>
);
}

View file

@ -0,0 +1,16 @@
export function LoadingState({ label }: { label: string }) {
if (label.trim().toLowerCase() === "thinking...") {
throw new Error('LoadingState label must be specific; "Thinking..." is forbidden.');
}
return (
<section className="rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-4">
<p className="m-0 text-sm text-[var(--color-text-secondary)]">{label}</p>
<div
aria-hidden
className="mt-3 h-3 w-full rounded bg-[var(--color-surface-overlay)] after:block after:h-3 after:w-1/3 after:rounded after:bg-[var(--color-border-strong)] after:content-['']"
data-shimmer-cycles="2"
/>
</section>
);
}

View file

@ -0,0 +1,37 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { EmptyState } from "./EmptyState";
import { ErrorState } from "./ErrorState";
import { LoadingState } from "./LoadingState";
describe("state components", () => {
it("requires empty-state statement and next action", () => {
render(<EmptyState statement="No trace selected." nextAction="Select a trace." />);
expect(screen.getByText("No trace selected.")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Select a trace." })).toBeInTheDocument();
});
it("renders every error-state contract field", () => {
render(
<ErrorState
whatFailed="Preview fixture failed to parse."
mutationStatus="No mutation attempted."
reproducer="pnpm test"
retrySafety="Retry is read-only."
/>,
);
expect(screen.getByText("What failed")).toBeInTheDocument();
expect(screen.getByText("Mutation status")).toBeInTheDocument();
expect(screen.getByText("Reproducer")).toBeInTheDocument();
expect(screen.getByText("Retry safety")).toBeInTheDocument();
});
it("requires a specific loading label and caps shimmer cycles", () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
expect(() => render(<LoadingState label="Thinking..." />)).toThrow();
consoleError.mockRestore();
render(<LoadingState label="Loading trace fixture." />);
expect(screen.getByText("Loading trace fixture.")).toBeInTheDocument();
expect(document.querySelector("[data-shimmer-cycles='2']")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,10 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export async function copyText(value: string) {
await navigator.clipboard?.writeText(value);
}

View file

@ -0,0 +1,146 @@
@font-face {
font-family: "Inter";
src: url("/fonts/Inter-Regular.woff2") format("woff2");
font-weight: 400 800;
font-display: swap;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2");
font-weight: 400 800;
font-display: swap;
}
:root,
.dark {
color-scheme: dark;
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
--color-surface-base: #0d1117;
--color-surface-raised: #131923;
--color-surface-overlay: #191f2a;
--color-surface-inset: #070a0f;
--color-border-subtle: #252d3a;
--color-border-strong: #3a4556;
--color-text-primary: #eef2f7;
--color-text-secondary: #aeb8c7;
--color-text-mono: #d8e2ef;
--color-text-muted: #788395;
--color-focus-ring: #7dd3fc;
--color-link: #8bd3ff;
--color-state-decoded: #4fc3b4;
--color-state-decoded-muted: #3d8f86;
--color-state-verified: #67d391;
--color-state-evidenced: #38bfa7;
--color-state-evidenced-muted: #2f8175;
--color-state-inferred: #8d8cff;
--color-state-unverified: #8aa0b8;
--color-state-unverified-warm: #d39b4f;
--color-state-perceived: #5ec8f2;
--color-state-contradicted: #f07178;
--color-state-ambiguous: #b18cff;
--color-state-undetermined: #77808f;
--color-state-scope: #64748b;
--color-state-bounded: #e59f48;
--color-state-needed: #f3c45b;
--color-clearance-cleared: #74d99f;
--color-clearance-violated: #f25f6f;
--color-clearance-unassessable: #7f8998;
--color-clearance-suppressed: #bd5b68;
--color-review-pending: #e8b94e;
--color-review-accepted: #57c989;
--color-review-rejected: #cf6470;
--color-review-withdrawn: #858d9b;
--color-grounding-teaching: #61d394;
--color-grounding-pack: #42c7b5;
--color-grounding-vault: #7f95ff;
--color-grounding-partial: #6faea4;
--color-grounding-oov: #e9a94f;
--color-grounding-none: #6f7b8c;
--color-state-info-bg: #10253a;
--color-state-info-border: #23648e;
--color-state-info-text: #bde8ff;
--color-state-success-bg: #10291e;
--color-state-success-border: #2d7a55;
--color-state-success-text: #b9f3d3;
--color-state-warning-bg: #31240c;
--color-state-warning-border: #9c6a1b;
--color-state-warning-text: #ffd89a;
--color-state-danger-bg: #351419;
--color-state-danger-border: #9f3848;
--color-state-danger-text: #ffc3ca;
--color-state-neutral-bg: #1b212c;
--color-state-neutral-border: #465164;
--color-state-neutral-text: #d4dbe7;
--color-state-purple-bg: #241a34;
--color-state-purple-border: #7655a2;
--color-state-purple-text: #dec8ff;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-1: var(--radius-sm);
--radius-2: var(--radius-md);
--radius-3: var(--radius-lg);
--shadow-panel: 0 10px 35px rgb(0 0 0 / 0.28);
--shadow-floating: 0 18px 60px rgb(0 0 0 / 0.45);
--shadow-overlay: var(--shadow-floating);
--font-size-display: 1.75rem;
--font-size-h1: 1.375rem;
--font-size-h2: 1rem;
--font-size-body: 0.875rem;
--font-size-meta: 0.75rem;
--font-size-mono: 0.75rem;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 650;
--line-height-tight: 1.2;
--line-height-base: 1.5;
--text-xs: var(--font-size-meta);
--text-sm: var(--font-size-body);
--text-md: 1rem;
--text-lg: 1.125rem;
--text-xl: var(--font-size-h1);
--line-tight: var(--line-height-tight);
--line-normal: var(--line-height-base);
--motion-instant: 0ms;
--motion-fast: 120ms;
--motion-base: 180ms;
--motion-slow: 1200ms;
--motion-ease-out: cubic-bezier(0.2, 0, 0, 1);
--motion-ease-spring: cubic-bezier(0.16, 1, 0.3, 1);
--motion-duration-instant: var(--motion-instant);
--motion-duration-fast: var(--motion-fast);
--motion-duration-standard: var(--motion-base);
--motion-duration-slow: var(--motion-slow);
--motion-ease-standard: var(--motion-ease-out);
--motion-ease-emphasized: var(--motion-ease-spring);
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: var(--motion-duration-instant) !important;
transition-duration: var(--motion-duration-instant) !important;
animation-iteration-count: 1 !important;
}
}

View file

@ -0,0 +1,37 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { tokens } from "./tokens";
function parseCssTokens() {
const css = readFileSync(resolve(process.cwd(), "src/design/tokens/tokens.css"), "utf8");
return Object.fromEntries(
[...css.matchAll(/--([a-z0-9-]+):\s*([^;]+);/g)].map(([, name, value]) => [
name,
value.trim(),
]),
);
}
describe("design tokens", () => {
it("keeps tokens.css and generated tokens.ts synchronized", () => {
expect(tokens).toEqual(parseCssTokens());
});
it("collapses tokenized motion under reduced motion", () => {
const reduced = true;
vi.stubGlobal("matchMedia", (query: string) => ({
matches: reduced && query.includes("prefers-reduced-motion"),
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
expect(window.matchMedia("(prefers-reduced-motion: reduce)").matches).toBe(true);
expect(tokens["motion-instant"]).toBe("0ms");
});
});

View file

@ -0,0 +1,115 @@
// Generated by scripts/generate-tokens.ts. Do not edit by hand.
export const tokens = {
"font-sans": "\"Inter\", ui-sans-serif, system-ui, sans-serif",
"font-mono": "\"JetBrains Mono\", ui-monospace, SFMono-Regular, monospace",
"color-surface-base": "#0d1117",
"color-surface-raised": "#131923",
"color-surface-overlay": "#191f2a",
"color-surface-inset": "#070a0f",
"color-border-subtle": "#252d3a",
"color-border-strong": "#3a4556",
"color-text-primary": "#eef2f7",
"color-text-secondary": "#aeb8c7",
"color-text-mono": "#d8e2ef",
"color-text-muted": "#788395",
"color-focus-ring": "#7dd3fc",
"color-link": "#8bd3ff",
"color-state-decoded": "#4fc3b4",
"color-state-decoded-muted": "#3d8f86",
"color-state-verified": "#67d391",
"color-state-evidenced": "#38bfa7",
"color-state-evidenced-muted": "#2f8175",
"color-state-inferred": "#8d8cff",
"color-state-unverified": "#8aa0b8",
"color-state-unverified-warm": "#d39b4f",
"color-state-perceived": "#5ec8f2",
"color-state-contradicted": "#f07178",
"color-state-ambiguous": "#b18cff",
"color-state-undetermined": "#77808f",
"color-state-scope": "#64748b",
"color-state-bounded": "#e59f48",
"color-state-needed": "#f3c45b",
"color-clearance-cleared": "#74d99f",
"color-clearance-violated": "#f25f6f",
"color-clearance-unassessable": "#7f8998",
"color-clearance-suppressed": "#bd5b68",
"color-review-pending": "#e8b94e",
"color-review-accepted": "#57c989",
"color-review-rejected": "#cf6470",
"color-review-withdrawn": "#858d9b",
"color-grounding-teaching": "#61d394",
"color-grounding-pack": "#42c7b5",
"color-grounding-vault": "#7f95ff",
"color-grounding-partial": "#6faea4",
"color-grounding-oov": "#e9a94f",
"color-grounding-none": "#6f7b8c",
"color-state-info-bg": "#10253a",
"color-state-info-border": "#23648e",
"color-state-info-text": "#bde8ff",
"color-state-success-bg": "#10291e",
"color-state-success-border": "#2d7a55",
"color-state-success-text": "#b9f3d3",
"color-state-warning-bg": "#31240c",
"color-state-warning-border": "#9c6a1b",
"color-state-warning-text": "#ffd89a",
"color-state-danger-bg": "#351419",
"color-state-danger-border": "#9f3848",
"color-state-danger-text": "#ffc3ca",
"color-state-neutral-bg": "#1b212c",
"color-state-neutral-border": "#465164",
"color-state-neutral-text": "#d4dbe7",
"color-state-purple-bg": "#241a34",
"color-state-purple-border": "#7655a2",
"color-state-purple-text": "#dec8ff",
"space-1": "0.25rem",
"space-2": "0.5rem",
"space-3": "0.75rem",
"space-4": "1rem",
"space-5": "1.25rem",
"space-6": "1.5rem",
"space-8": "2rem",
"space-10": "2.5rem",
"space-12": "3rem",
"radius-sm": "0.25rem",
"radius-md": "0.375rem",
"radius-lg": "0.5rem",
"radius-1": "var(--radius-sm)",
"radius-2": "var(--radius-md)",
"radius-3": "var(--radius-lg)",
"shadow-panel": "0 10px 35px rgb(0 0 0 / 0.28)",
"shadow-floating": "0 18px 60px rgb(0 0 0 / 0.45)",
"shadow-overlay": "var(--shadow-floating)",
"font-size-display": "1.75rem",
"font-size-h1": "1.375rem",
"font-size-h2": "1rem",
"font-size-body": "0.875rem",
"font-size-meta": "0.75rem",
"font-size-mono": "0.75rem",
"font-weight-regular": "400",
"font-weight-medium": "500",
"font-weight-semibold": "650",
"line-height-tight": "1.2",
"line-height-base": "1.5",
"text-xs": "var(--font-size-meta)",
"text-sm": "var(--font-size-body)",
"text-md": "1rem",
"text-lg": "1.125rem",
"text-xl": "var(--font-size-h1)",
"line-tight": "var(--line-height-tight)",
"line-normal": "var(--line-height-base)",
"motion-instant": "0ms",
"motion-fast": "120ms",
"motion-base": "180ms",
"motion-slow": "1200ms",
"motion-ease-out": "cubic-bezier(0.2, 0, 0, 1)",
"motion-ease-spring": "cubic-bezier(0.16, 1, 0.3, 1)",
"motion-duration-instant": "var(--motion-instant)",
"motion-duration-fast": "var(--motion-fast)",
"motion-duration-standard": "var(--motion-base)",
"motion-duration-slow": "var(--motion-slow)",
"motion-ease-standard": "var(--motion-ease-out)",
"motion-ease-emphasized": "var(--motion-ease-spring)"
} as const;
export type DesignTokenName = keyof typeof tokens;
export type DesignTokenValue = (typeof tokens)[DesignTokenName];

View file

@ -0,0 +1,35 @@
@import "./design/tokens/tokens.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background: var(--color-surface-base);
color: var(--color-text-primary);
font-family: var(--font-sans);
line-height: var(--line-normal);
}
button,
a,
[tabindex]:not([tabindex="-1"]) {
outline: none;
}
:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.motion-standard {
transition-duration: var(--motion-duration-standard);
transition-timing-function: var(--motion-ease-standard);
}

14
workbench-ui/src/main.tsx Normal file
View file

@ -0,0 +1,14 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { PreviewPage } from "./preview/PreviewPage";
function App() {
return window.location.pathname === "/preview" ? <PreviewPage /> : <PreviewPage />;
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View file

@ -0,0 +1,35 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { PreviewPage } from "./PreviewPage";
describe("/preview", () => {
it("renders every primitive with network unavailable", () => {
vi.stubGlobal("fetch", vi.fn(() => Promise.reject(new Error("network blocked"))));
render(<PreviewPage />);
expect(screen.getByRole("heading", { name: "CORE Workbench Design System v1" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Primitives" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Badges" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "States" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Stable JSON Viewer" })).toBeInTheDocument();
expect(fetch).not.toHaveBeenCalled();
});
it("opens CommandPalette with mod+k and closes overlays with Escape", async () => {
const user = userEvent.setup();
render(<PreviewPage />);
await user.keyboard("{Control>}k{/Control}");
expect(screen.getByRole("dialog", { name: "Command Palette" })).toBeInTheDocument();
await user.keyboard("{Escape}");
expect(screen.queryByRole("dialog", { name: "Command Palette" })).not.toBeInTheDocument();
});
it("keeps tab order in DOM order for preview actions", async () => {
const user = userEvent.setup();
render(<PreviewPage />);
await user.tab();
expect(screen.getByRole("button", { name: "Primary action" })).toHaveFocus();
await user.tab();
expect(screen.getByRole("button", { name: "Quiet action" })).toHaveFocus();
});
});

View file

@ -0,0 +1,85 @@
import { useEffect, useState } from "react";
import { StableJsonViewer } from "../design/components/StableJsonViewer";
import {
EpistemicState,
EpistemicStateBadge,
GroundingSource,
GroundingSourceBadge,
NormativeClearance,
NormativeClearanceBadge,
ReviewState,
ReviewStateBadge,
TraceHashBadge,
} from "../design/components/badges";
import { Button } from "../design/components/primitives/Button";
import { CommandPalette } from "../design/components/primitives/CommandPalette";
import { EmptyState } from "../design/components/states/EmptyState";
import { ErrorState } from "../design/components/states/ErrorState";
import { LoadingState } from "../design/components/states/LoadingState";
export function PreviewPage() {
const [paletteOpen, setPaletteOpen] = useState(false);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
setPaletteOpen(true);
}
if (event.key === "Escape") setPaletteOpen(false);
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
return (
<main className="mx-auto grid max-w-6xl gap-8 px-5 py-8">
<header>
<h1 className="m-0 text-xl font-semibold">CORE Workbench Design System v1</h1>
<p className="mt-2 max-w-3xl text-sm text-[var(--color-text-secondary)]">
Static Branch 1 substrate preview. No backend, no API client, no runtime mutation surface.
</p>
</header>
<section aria-labelledby="primitive-heading" className="grid gap-3">
<h2 id="primitive-heading" className="text-sm font-semibold text-[var(--color-text-secondary)]">Primitives</h2>
<div className="flex flex-wrap gap-2">
<Button type="button">Primary action</Button>
<Button type="button" variant="quiet">Quiet action</Button>
</div>
</section>
<section aria-labelledby="badge-heading" className="grid gap-3">
<h2 id="badge-heading" className="text-sm font-semibold text-[var(--color-text-secondary)]">Badges</h2>
<div className="flex flex-wrap gap-2">
{Object.values(EpistemicState).map((value) => <EpistemicStateBadge key={value} value={value} />)}
{Object.values(NormativeClearance).map((value) => <NormativeClearanceBadge key={value} value={value} />)}
{Object.values(ReviewState).map((value) => <ReviewStateBadge key={value} value={value} />)}
{Object.values(GroundingSource).map((value) => <GroundingSourceBadge key={value} value={value} />)}
<TraceHashBadge value="4f80f7e12c7e8ca1f1a277f8ccecf2846f08bb9d8f22354e6d3f30eb7fb34c80" />
</div>
</section>
<section aria-labelledby="state-heading" className="grid gap-3">
<h2 id="state-heading" className="text-sm font-semibold text-[var(--color-text-secondary)]">States</h2>
<div className="grid gap-3 md:grid-cols-3">
<EmptyState statement="No artifact selected." nextAction="Select an artifact." />
<ErrorState
whatFailed="Preview artifact failed validation."
mutationStatus="No mutation attempted."
reproducer="cd workbench-ui && pnpm test"
retrySafety="Retry reads static fixtures only."
/>
<LoadingState label="Loading preview fixture." />
</div>
</section>
<section aria-labelledby="json-heading" className="grid gap-3">
<h2 id="json-heading" className="text-sm font-semibold text-[var(--color-text-secondary)]">Stable JSON Viewer</h2>
<StableJsonViewer source='{"trace_hash":"4f80f7e12c7e","scenes":[{"detail":{"object":"lens","epsilon":1e-6}}],"state":"decoded"}' />
</section>
<CommandPalette open={paletteOpen} onOpenChange={setPaletteOpen} />
</main>
);
}

View file

@ -0,0 +1,8 @@
import "@testing-library/jest-dom/vitest";
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: {
writeText: vi.fn().mockResolvedValue(undefined),
},
});

View file

@ -0,0 +1,15 @@
import type { Config } from "tailwindcss";
export default {
darkMode: "class",
content: ["./index.html", "./src/**/*.{ts,tsx}"],
theme: {
extend: {
fontFamily: {
sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
mono: ["JetBrains Mono", "ui-monospace", "SFMono-Regular", "monospace"],
},
},
},
plugins: [],
} satisfies Config;

View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2023",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ES2023"],
"types": ["vitest/globals"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src", "scripts"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2023",
"lib": ["ES2023"],
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts", "scripts/*.ts"]
}

View file

@ -0,0 +1,11 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [react()],
test: {
environment: "happy-dom",
setupFiles: ["./src/test/setup.ts"],
globals: true,
},
});