# Caseboard

> Break a topic or source material into a hierarchy and render it as an interactive 3D detective evidence board — corkboard, red threads, legal-pad detail panel. Output is a self-contained Vite + three.js project; npm run dev to view.

- Skill: `win-hao/caseboard` (Agent Skill, multi-file: 59 files)
- Install (CLI): `npx skillmds@latest add win-hao/caseboard`
- Raw SKILL.md: https://api.skillmd.com/api/skills/win-hao/caseboard/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: win-hao (https://skillmd.com/u/win-hao)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/win-hao/caseboard

---


# Caseboard

Break a topic into a hierarchy and pin it onto a WebGL corkboard.

**Output**: a self-contained Vite + three.js project. The user runs `npm run dev` and can drag, zoom, click any piece of paper for details, and search with ⌘K.

## Input

This skill is only invoked explicitly via `/caseboard <topic or material>`. The argument is the topic, an article, a book, a tech stack — whatever should become the board. If no argument was given, ask what the topic is; don't guess.

---

## Workflow

### Step 1: Decompose the topic (**the most important step — never skip it**)

Break the topic into levels. The default shape below — root, dimensions, evidence — fits most topics and stays the most legible; depth beyond it is a judgment call you make from the material, not a rule.

```
L0  root            1        the topic itself           → large central photo card, "Start here"
L1  main branches   3–7      the topic's skeleton/axes   → dossier cards, red thread to root
L2  evidence        2–5/L1   concrete facts, defs, data  → slips/notes/clippings, thread to their L1
```

**Decomposition rules**

- **L1 entries are "dimensions", not "list items".** For coffee brewing, L1 should be `grind` / `water temp` / `ratio` / `extraction yield`, not `step 1` / `step 2`.
- **Keep L1 between 3 and 7.** Fewer than 3 and there's no sense of hierarchy; more than 7 and the board gets crowded.
- **Every L2 must be verifiable, concrete content**: a formula, a number, a quoted sentence, a year, a license clause. Vague "this matters a lot" doesn't belong.
- **Nest deeper only where the material genuinely chains.** The renderer supports any depth — an evidence node can carry its own children (mechanism → sub-mechanism → measurement), each level drawn smaller and pushed further out. Use it for the one or two branches that truly nest; never pad depth for show. Past 4 levels the deepest cards stop being readable at overview (the model warns and `check` fails) — a branch that wants to go that deep is usually a case of its own.
- **Write like a person, not a spec sheet.** `summary` and `detail` get read off a corkboard. Plain words, concrete claims, verbs over bureaucratic filler: "Every variable ultimately maps back to extraction yield" reads stiff; "whatever you tweak, the yield is where it shows up" reads like a human pinned it there.
- **Total node count 12–35 per case is the sweet spot; up to 60 works.** Below 12 the board looks empty (coverage < 0.3). Past 30 nodes the renderer shrinks every card automatically (∝ √(30/n), floored at ×0.7) so one board holds more notes at the cost of smaller overview text; above 60 `npm run check` fails outright — split into cases. The project's diagnostics report the actual coverage and the applied scale.
- **Every node needs a `summary`** (one sentence shown on the card, ≤ 30 CJK chars / ≤ 60 Latin chars) **and a `detail`** (focus-panel body, 2–5 sentences). Anything that doesn't fit on the card goes into `detail`.

**When the material doesn't fit one board**: estimate scope before decomposing. Up to ~60 nodes the auto-shrink handles density for you; past that, don't cram or amputate — split into **multiple cases** (the `cases` array; each case is its own board with its own layout and accent, and the UI collapses many cases into a drawer automatically). Between 35 and 60, choose by reading style: one dense board keeps everything in a single view; splitting keeps overview text larger. Split along the material's natural top level: a book by parts/chapters, a tech stack by subsystem, history by era, a big domain by subfield. Then decompose each case independently with the same 3–7 branch rules, 12–35 nodes each. Two things to keep in mind:

- Threads never cross cases. If a concept matters in several cases, give each case its own node for it — a card repeated is better than a connection lost.
- Don't pad the other way: a topic that fits comfortably in one board should stay one board. Multi-case is for overflow, not decoration.

Include the case list (one line each) plus every per-case tree in the confirmation you show the user.

**Worked example** — topic "Coffee Extraction". The approach: find the **mutually constraining variables** first, then give each one verifiable evidence:

```
Coffee Extraction                     ← L0: one sentence on what problem this topic solves
├─ Grind Size           blueprint     ← L1 named as a variable/dimension, not "chapter 1"
│  ├─ Surface Area      excerpt       ← L2 is a mechanism: halve the size, double the area
│  ├─ Fines             note          ← L2 is a side effect
│  └─ Channeling        clipping      ← L2 is a failure mode
├─ Water Temperature    dossier
│  ├─ Dissolution Order excerpt       ← acids first, sugars next, bitters last
│  └─ Follow the Roast  note
├─ Brew Ratio           stamp         ← facts: 1:15 – 1:17
│  └─ TDS               note          ← facts: 1.15 – 1.45 %
├─ Time & Flow          dossier
│  ├─ The Bloom         excerpt
│  └─ Early, Mid, Late  note
└─ Extraction Yield     blueprint     ← the "criterion" branch that unifies the other four
   ├─ The Golden Cup    quote         ← 18–22%, with SCA source
   ├─ Under vs. Over    clipping      ← diagnosis: sour = under, astringent = over
   └─ Refractometer     note
```

Reusable moves:

- **Pick L1 as "mutually constraining variables"**, not time slices or chapters. For history: institutions / technology / population / external shocks. For a book: its main lines of argument.
- **Reserve one branch as the "criterion" or "conclusion"** (here: extraction yield). With a convergence point, the red threads stop feeling scattered.
- **Rotate L2 roles**: mechanism / number / side effect / failure mode / diagnosis / tool. Every branch using the same role reads monotonous.
- **Fill `facts` with numbers whenever possible.** "1 : 15 – 1 : 17" beats "use a proper ratio".

**Ground the content — don't recite training data.**

- If the user supplied material (a paper, notes, a URL), that material is the single source of truth: every L2 must trace back to it. Don't pad it with remembered facts.
- Decomposing from your own knowledge: verify the load-bearing specifics — numbers, dates, quotes, version numbers, standards — with web search when a search tool is available, and record the real origin in that node's `sources`. Fast-moving domains (software, prices, papers, current events) always deserve a search pass; the person asking for a board usually doesn't know the topic well enough to catch your errors.
- No search tool available? Keep to stable, well-established facts, drop any specific you can't stand behind rather than inventing it, and tell the user which parts went unverified.

**Present the decomposition for confirmation — in this fixed form.** Show the indented tree, then ask exactly one closed question (via AskUserQuestion if available). Never ask "which node would you like to change?" — someone meeting unfamiliar material has no way to answer that. Ask:

1. **Build it** — the decomposition looks right
2. **Deeper** — more evidence under each branch
3. **Broader** — add missing dimensions/branches
4. **Simpler** — fewer nodes, plainer wording

If the user names concrete changes in free text instead, treat those as authoritative and re-confirm only if the structure changed substantially.

**Board language: follow the user, don't ask.** Write all board content — `title`, `summary`, `detail`, `facts`, case labels — in the language the user typed their request in (a Chinese prompt gets a Chinese board, even about an English-named subject). Source material in another language doesn't override this — the user's own words do. Only deviate if they explicitly name a language. The runtime detects the content language automatically and switches UI labels, line-breaking, and font fallback to match; no configuration needed.

### Step 2: Pick a card type per node

Choose a `kind` for every node. Mix them — the board only gets texture if you don't use the same card everywhere.

| kind | Looks like | Use for |
|---|---|---|
| `dossier` | cream file card, binder clip, table area | L1 main branches (default) |
| `excerpt` | torn slip held by tape | passages quoted from papers/books |
| `note` | small square note, push pin | one-line points, term definitions |
| `quote` | kraft paper, torn edges, tape | quotations, verbatim lines |
| `stamp` | pale card, staple | licenses, specs, statutes, parameter tables |
| `photo` | polaroid white frame, pin, red title strip | nodes with an `image` |
| `clipping` | yellowed newspaper clipping, torn | news, events, points in time |
| `blueprint` | blue drafting paper, grid, binder clip | architecture, flows, formulas |
| `ledger` | pale-green ledger paper, ruled rows, right-aligned numbers | fact-heavy nodes: parameters, metrics, budgets |
| `index` | index card, red top rule + ruled lines + punch holes | term definitions, zettelkasten-style single notes |
| `telegram` | telegram paper, ALL CAPS + STOP breaks + feed holes | conclusions, warnings, non-negotiables |
| `chart` | **renders `facts` values as a bar chart** | comparable quantities: shares, weights, rankings |
| `timeline` | horizontal year axis, `facts` labels as time points | chronology, processes, evolution |
| `memo` | letterhead `MEMORANDUM` + RE line | positions, rules, official statements |
| `sticky` | saturated yellow sticky with curled corner, self-adhesive | open questions, TODOs, unsettled ideas |
| `ticket` | stub with perforation + vertical serial | single numberable records: one experiment, one event |

The root node uses `photo` (an image is nice; without one a procedural plate is drawn).

`chart` and `timeline` **require `facts`** — the whole layout depends on them. Missing facts produce an empty card plus a runtime warning.
`chart` only draws bars for values it can parse as numbers (`"42 %"` and `"1.5 小时"` both work); unparseable values are laid out as text — it never fakes bar lengths with random numbers.

Card sizes **auto-scale by level** (L1 ×1.16, L2 ×0.9), so even if an L2 node gets a large card kind, the main branches still dominate at overview zoom — hierarchy doesn't depend on picking the right `kind`.

### Step 3: Create the project

Output location: use the user's choice if they named one; otherwise create `<topic-slug>-board/` in the **user's current working directory**. Never create it inside the skill repo.

```bash
rsync -a --exclude node_modules --exclude dist <skill-dir>/assets/template/ <output-dir>/
cd <output-dir> && npm install
```

`<skill-dir>` is wherever this SKILL.md actually lives — the skill may be installed in `~/.claude/skills/`, a project-level `.claude/skills/`, or elsewhere; never hardcode it. Use rsync, not `cp -R`: the template dir may contain tens of MB of leftover `node_modules`, and BSD cp nests into `<output-dir>/template/` when the target exists.

Then **overwrite `data/board.json`**. The full schema is in `references/schema.md` — read it before writing. There aren't many fields, but a few constraints matter (`id` unique, `parent` must point to an existing node, `facts` max 12 — the card face draws the first ~4, the focus panel shows them all, so put the headline numbers first).

Images go into `public/`; reference them as `/filename.png` in the JSON. If there are no images, omit the `image` field — never fill in placeholder URLs.

### Step 4: Validate (**mandatory**)

**No browser needed** — run this first:

```bash
npm run check     # exit 0 = pass, 1 = problems
```

check.mjs has zero dependencies — **it does not need npm install to have finished**. Write board.json, check immediately, iterate; let npm install run in parallel.

It checks layout (coverage, overlap, out-of-bounds, quadrants), structure (duplicate ids, bad parents, node count), thread connectivity, image paths, and text length (a width heuristic — CJK counts double — that fails on over-limit `kicker`/`title`/`summary`, catching nearly all overflows) — and tells you exactly what to fix when something fails.

The browser's canvas-measured `textOverflows` remains the ground truth for edge cases. With a browser tool, read on; without one, start the dev server yourself and ask the user to open the URL — the devtools console prints one line, `[board] 排版合格` (pass) or `[board] 排版待改进: …` (needs work); them pasting that line back is enough.

With a browser, the board writes all diagnostics into the DOM after rendering — read them directly, no screenshots needed:

```js
document.querySelector('.kb-viewport').dataset
// pieceCount, coverageRatio, overlapRatio, maxPairOverlap, occupiedQuadrants,
// textOverflows, textTruncations, overflowIds, offBoardPieces,
// orphanPieces, orphanIds, imageFailures,
// boardSize, layoutAttempts, threadCount, drawCalls, triangles, fontState
```

Or call `window.__BOARD__.diagnostics()`.

`npm run check` covers everything in the table below (`textOverflows` via the length heuristic; canvas is exact).

**Pass thresholds**:

| Metric | Target | If failing |
|---|---|---|
| `coverageRatio` | 0.35 – 0.62 (check's hard floor/ceiling: 0.30 – 0.68) | too low → add L2 nodes; too high → remove nodes or raise `board.scale` |
| `maxPairOverlap` | < 0.15 | change `layout.seed` to reshuffle, or remove nodes |
| `occupiedQuadrants` | 4 | distribution uneven — check whether L1 count is too low |
| `textOverflows` | 0 | a `summary` is too long — shorten it (`overflowIds` lists the nodes) |
| `offBoardPieces` | 0 | too many nodes to fit — raise `layout.scale` |
| `orphanPieces` | 0 | a card isn't connected — check whether its `parent` points to an L2 node |
| `imageFailures` | 0 | bad `image` path — files go in `public/`, paths start with `/` |

Below 10 nodes, fewer than 3 branches, or above 38 nodes, the console prints advice directly — no need to count yourself.

If a check fails, edit the JSON and re-run. Never deliver a board with red diagnostics.

`layoutAttempts` shows which board sizes the solver tried and the max overlap of each; the one ending in `✓` is the one in use. If none has a check mark, the nodes are too crowded — cut content or raise `board.scale`.

Changing `layout.seed` (any string) reshuffles the whole layout — if it looks bad, try another seed; it's the cheapest fix. You can try without editing the file: `__BOARD__.reseed('another-seed')` in the console reshuffles immediately and returns fresh diagnostics.

### Step 5: Deliver

Do the launching yourself — don't hand the user a list of commands to type. Start the dev server in the background (`npm run dev`, port 5180; Vite auto-bumps the port if taken — read the actual URL from its output) and give the user the URL plus the controls in one line: drag to pan, scroll to zoom, click a piece for details, ⌘K to search, Esc back, 0 to fit.

Mention that `npm run build` produces a static site they can host anywhere — but only run it if they ask.

---

## Common adjustments

**Multiple topics**: put several case files in the `cases` array; folder tabs at the bottom switch between them (many cases collapse into a drawer). Good for "chapters of one book" or "subfields of one domain" — see the sizing rule in Step 1 for when to split.

**Central card artwork**: when the root has no `image`, a procedural "specimen plate" is drawn — six styles (`dial` gauge / `grid` survey grid / `constellation` star map / `strata` strata section / `orbit` orbits / `trace` waveform). Cases in the same collection automatically get different ones; you can also pin one with `"plate": "orbit"` on the root. Pick one matching the topic's character: relationships → `constellation`, stages → `strata`, change over time → `trace`.

**Real images or not**: default is none — the procedural plates carry the layout fine. Add images only when the user asks or already has files: put them in `public/`, write `"image": "/filename.png"`. **Never scrape images from the web**: unknown provenance creates copyright problems for the user, hotlinks rot, and a network dependency breaks the guarantee that the same JSON reproduces the same board. If web images are truly needed, ask the user first, use only clearly licensed sources (Wikimedia Commons, Openverse), download into `public/`, and record the origin in that node's `sources`.

**Recolor**: each case's `accent` sets the primary color (threads, titles, highlights). Default is archive red `#8c171d`. For a cool scheme try `#1f4e5f`; dark green `#2d4a34`.

**Chinese and English**: the runtime detects the content language and switches UI copy, card type labels (`档案`/`FILE`), and panel headings accordingly. Writing English content requires zero configuration. CJK line-breaking and font fallback (Courier Prime + Songti) are prewired; a `summary` within 30 CJK chars / 60 Latin chars will not overflow.

**Any depth renders.** `parent` may point to any existing node; chains nest with shrinking cards. A `parent` that doesn't resolve (missing id, cycle) gets promoted to a main branch with a warning — nothing silently disappears, but fix the structure in the JSON anyway.

**Video**: add `video: "https://www.youtube.com/embed/XXX"` to a node; the focus panel renders it as a vintage monitor.

## Reference docs

- `references/schema.md` — complete `board.json` field reference
- `references/example-board.json` — the full JSON of the coffee example above, 17 nodes, all diagnostics green; usable as a starting template
- `references/materials.md` — materials / textures / palette parameter tables; read when changing the visual style
- `references/contributing-a-card.md` — how to add a new card style (two touch points, with a checklist)

