/assets/`. We
+**assume no `assets/` directory exists above the check root**, which makes the
+lookup definitive rather than a guess — hence a `warning`, not `info`.
+
+### CSV data files
+
+| Code | Sev | Rule | Prio |
+|---|---|---|---|
+| `TTTK-V001` | error | CSV fails to parse (bad quoting) | P1 |
+| `TTTK-V002` | warning | Row column count ≠ header column count | P1 |
+| `TTTK-V003` | warning | Duplicate `label` values | P1 |
+| `TTTK-V004` | warning | `{{prop}}` key is not a CSV column or front-matter key | P1 |
+| `TTTK-V005` | warning | spark-table row roll value is not a number/range | P2 |
+
+`V004` mirrors `processVariables` (`src/components/utils/csv-loader.ts:86-99`):
+a `{{key}}` resolves when `key` is a CSV header or a front-matter key (front
+matter is merged into every row, `csv-loader.ts:56-61`). Unresolved keys are
+left verbatim in the output, so the check is definitive. Honor the parser's
+options: BOM stripped, `#` lines are comments, headers/cells trimmed, empty
+lines skipped (`csv-loader.ts:45-51`).
+
+## 6. CLI surface
+
+```
+tttk check [dir] [options]
+
+Arguments:
+ [dir] directory to validate (default ".")
+
+Options:
+ --json emit a JSON report (see §6.1)
+ --strict treat warnings as errors
+ --quiet report errors only
+ --no-color disable ANSI color
+ -h, --help
+```
+
+Wire in `src/cli/index.ts` alongside `serve` (commander pattern at
+`src/cli/index.ts:17-30`).
+
+### 6.1 Output
+
+Human (default), grouped by file:
+
+```
+content/adventures/hollow-mill/adventure.md
+ 12:1 error TTTK-C001 :md-table reference "./npcs.csv" does not resolve
+ hint: expected content/adventures/hollow-mill/npcs.csv
+ 40:8 warning TTTK-R002 spark-table first header "Name" is not a dice formula
+ hint: use d6, 2d6+1, …
+
+2 errors, 1 warning
+```
+
+`--json`:
+
+```json
+{
+ "root": "content",
+ "filesChecked": 12,
+ "diagnostics": [ { "code": "TTTK-C001", "severity": "error", "message": "…",
+ "file": "adventures/hollow-mill/adventure.md",
+ "line": 12, "column": 1, "hint": "…" } ],
+ "summary": { "errors": 2, "warnings": 1, "infos": 0 }
+}
+```
+
+### 6.2 Exit codes
+
+| Code | Meaning |
+|---|---|
+| `0` | No errors (warnings allowed unless `--strict`) |
+| `1` | At least one error, or any warning under `--strict` |
+| `2` | Usage / IO / internal failure |
+
+Exit code is the contract that makes `check` usable in an agent loop.
+
+## 7. Implementation steps
+
+1. **`src/cli/validate/source.ts`** — line-start table; `offsetToPosition`.
+2. **`src/cli/validate/diagnostics.ts`** — `Diagnostic`, `Sink` (collector),
+ sort, human + JSON formatters.
+3. **`src/cli/validate/known.ts`** — the catalog is the single source of
+ truth: `KNOWN_ELEMENTS` (the 11 verified names, with body/attrs/description/
+ examples), `KNOWN_ROLES = { spark-table, declare, file, tag }`, expected
+ file-type map for `C002`. `docs/reference.md` is generated from it (§7.11).
+4. **`src/cli/validate/scan.ts`** — fence-aware scanner yielding
+ `{ name, body, attrs, offset, inFence }` for directives, plus role info
+ lines. Reuse `parseBlockAttrs` (`src/markdown/block-attrs.ts`); do not
+ reimplement attribute parsing.
+5. **Checks** — `checks/roles.ts`, `checks/directives.ts`, `checks/refs.ts`,
+ `checks/links.ts`, `checks/csv.ts`, `checks/parse.ts`. Pure functions
+ `(source, registry, docPath, sink) => void`.
+6. **`src/cli/validate/index.ts`** — `runCheck(dir)`: `buildRegistry`, iterate
+ raw `.md` sorted, run checks, sort diagnostics, compute summary.
+7. **`src/cli/commands/check.ts`** — commander handler: parse options, run,
+ print, `process.exitCode = …`. Add `CheckCommandHandler` to
+ `src/cli/types.ts`.
+8. **`src/cli/index.ts`** — register `check` (alias `lint`).
+9. **Refactor `scanDoc` to report into a sink** (optional but removes drift):
+ give `scanDoc(content, docPath, sink?)` an optional sink, replace
+ `console.warn` at `content-registry.ts:175-179` and `:285-288` with
+ `sink.report(...)`, and pass the match offset through. Default no-op keeps
+ existing callers and tests unchanged. This lets the CLI share one code path
+ for role diagnostics instead of duplicating the switch.
+10. **Prereq for `S001`** — make `src/markdown/mermaid.ts` browser-safe so
+ `parseMarkdown` is importable from the CLI: **delete** `import mermaid`
+ and the top-level `mermaid.initialize` (the extension only emits
+ ``), then call `mermaid.initialize({ theme: "dark" })`
+ in `src/main.tsx` before the existing `mermaid.run()` calls
+ (`Article.tsx:89`, `md-embed.tsx:40`). Required for v1.
+11. **Reference generator** — `scripts/generate-reference.ts` renders the
+ `known.ts` catalog to markdown. Canonical output `docs/reference.md`
+ (versioned, testable); `--out` can target the skill copy at
+ `~/.agents/skills/tttk-authoring/reference.md`. Add `npm run
+ gen:reference`.
+12. **Drift guard** — a test that greps `customElement("md-*")` call sites
+ under `src/components` and asserts each appears in `known.ts` (and vice
+ versa). This is what would have caught `:md-card`.
+
+## 8. Tests
+
+`src/cli/validate/*.test.ts`, fixture-driven (temp dirs), covering:
+
+- **Positive:** a valid multi-file fixture produces zero diagnostics; exit 0.
+- **Negative, one per P0 code:** unknown role; missing declare columns;
+ invalid tag YAML; unknown tag element; unresolved `:md-table`; missing
+ `:md-link` target; unknown `:md-*` element. Assert `code`, `file`, `line`.
+- **Fence awareness:** an example directive inside a fenced block is *not*
+ reported; the same directive outside a fence is.
+- **No false positives:** a doc that round-trips cleanly, including
+ `:md-card`-free legacy content.
+- **Exit codes:** 0 / 1 / 1-under-`--strict` / 2 on a nonexistent dir.
+- **JSON shape:** stable, sorted, one entry per diagnostic.
+
+## 9. Docs and skill
+
+- Update `docs/cli.md`: replace the `compile` section with `check`; document
+ codes, `--json`, exit codes.
+- Update `README.md`.
+- Update `~/.agents/skills/tttk-authoring/SKILL.md`: the verification step
+ becomes **`npx tttk check ./content`**, and the workflow becomes
+ write → check → fix. Remove the "compile is a no-op stub" note. Cross-link
+ the code catalog (`reference.md`) with the diagnostic codes.
+- `reference.md` is generated from `known.ts` (§7.11) — do not hand-edit. The
+ skill's copy is refreshed by `npm run gen:reference`.
+
+## 10. Resolved decisions
+
+1. **`known.ts` is the source of truth; `reference.md` is generated from it**
+ (§7.11). A drift test (§7.12) keeps both honest.
+2. **`S001` is in scope.** Do the mermaid refactor (§7.10).
+3. **Icon existence is checked** (`A001`), assuming no `assets/` directory
+ above the check root, which makes the lookup definitive.
+4. **`V004` (`{{prop}}` keys) is in scope for v1.**
+5. **Report only — no `--fix`.** `check` never edits content.
+6. **`compile` stays a stub,** and its `--help` text now says so explicitly
+ (`src/cli/index.ts:27`).
+
+## 11. Risks
+
+- **Icon check assumes no parent assets.** `A001` treats `` as the top
+ of the icon search. If a project keeps shared `assets/` above the check root,
+ `A001` will false-positive; the assumption is documented in §5.
+- **False positives are fatal to adoption.** An agent that gets spurious
+ errors stops trusting the tool. Prefer P0 precision over coverage; gate
+ noisy checks behind `--strict` or `info`.
+- **Divergence from the app.** Do not reimplement attribute parsing, dice
+ detection, slugging, or role dispatch — import `parseBlockAttrs`,
+ `resolveContentEntry`, `parseDeclareCsv`, `inspectSparkTableCsv`,
+ `Slugger`. Reuse `buildRegistry` for the walk.
+- **Offsets vs. stripped content.** Validate raw source only (§3.2); mixing in
+ `pathIndex` will produce wrong line numbers.
+- **Windows paths.** Use `node:path`; normalize to posix keys via
+ `normalizePathKey` for all registry lookups. Repo targets Windows.
+- **Encoding.** Read UTF-8 explicitly.
+- **Declare parser throws.** Wrap `parseDeclareCsv`; a throw must become a
+ localized diagnostic, never a stack trace.