feat(cli): Complete the check rule catalog

Add D002 (malformed attribute blocks), R004 (skipped declare rows), and V001-V005 (CSV data files). CSV checks run once per referenced file and also cover inline spark-table bodies.

Generate docs/reference.md from the known.ts catalog via npm run gen:reference, and add drift guards asserting the catalog matches the customElement call sites and the committed reference.
This commit is contained in:
2026-09-12 17:01:49 +08:00
parent ca6d8d1d14
commit 71542e7453
17 changed files with 1167 additions and 16 deletions
+10 -6
View File
@@ -1,15 +1,19 @@
# `tttk check` — implementation plan (validator-first)
Status: **implemented (partial).** Supersedes `docs/compile-plan.md`.
Status: **implemented.** Supersedes `docs/compile-plan.md`.
Landed: `S001`/`S002`, `D001`, `R001`/`R002`/`R003`/`R005`/`R006`,
`C001`–`C004`, `L001`/`L002`, `A001`/`A002`, with tests. The `check` command,
Landed: `S001`/`S002`, `D001`/`D002`, `R001`–`R006`, `C001`–`C004`,
`L001`/`L002`, `A001`/`A002`, `V001`–`V005`, with tests. The `check` command,
`--json`/`--strict`/`--quiet`, and exit codes are live; the markdown module is
the browser-safe one described in §4, and `readContentIndex` backs the walk.
The reference generator (§7.11) and both drift guards (§7.12) are in place:
`docs/reference.md` is generated from `known.ts` via `npm run gen:reference`,
and `known.test.ts` asserts the catalog matches the `customElement` call sites
and the committed reference.
Not yet implemented: CSV data checks (`V001`–`V005`), `D002`, `R004`, and the
reference generator + drift guard (§7.11–§7.12). The rule catalog in §5, the
CLI surface in §6, and the resolved decisions in §10 remain the specification.
Not implemented: `D003` (P2 — the legacy `:md-deck` directive is not flagged).
The rule catalog in §5, the CLI surface in §6, and the resolved decisions in §10
remain the specification.
## 1. Goal
+8 -1
View File
@@ -112,9 +112,11 @@ content/adventures/hollow-mill/adventure.md
| `TTTK-S001` | error | Markdown 解析失败 |
| `TTTK-S002` | error | 容器指令 `:::` 未闭合 |
| `TTTK-D001` | error | 未知的 `:md-*` 元素名 |
| `TTTK-D002` | error | 属性块畸形(例如未加引号的值中含空格) |
| `TTTK-R001` | error | 未知的代码块 `role=`(块内容会被静默丢弃) |
| `TTTK-R002` | warning | `role=spark-table` 首列表头不是骰点公式 |
| `TTTK-R003` | error | `role=declare` 解析失败(缺少 `key`/`expr` 等) |
| `TTTK-R004` | warning | `role=declare` 中 `key`/`expr` 为空的行被跳过 |
| `TTTK-R005` | error | `role=tag` 的 YAML 无法解析 |
| `TTTK-R006` | error | `role=tag` 缺少 `tag:` 或元素未知 |
| `TTTK-C001` | error | 内容引用无法解析(文件不存在,或内联 id 未定义) |
@@ -125,8 +127,13 @@ content/adventures/hollow-mill/adventure.md
| `TTTK-L002` | warning | `#锚点` 未匹配到任何标题 |
| `TTTK-A001` | warning | 图标在 `assets/` 中不存在 |
| `TTTK-A002` | warning | 图片不存在 |
| `TTTK-V001` | error | CSV 解析失败(引号未闭合等) |
| `TTTK-V002` | warning | 行的列数与表头不一致 |
| `TTTK-V003` | warning | `label` 值重复 |
| `TTTK-V004` | warning | `body` 中的 `{{prop}}` 不是列名或 front-matter 键 |
| `TTTK-V005` | warning | spark-table 行的骰点值不是数字或范围 |
**尚未实现的校验:** CSV 数据校验(解析错误、列数不一致、`label` 重复、`{{prop}}` 引用无效列)、畸形属性块、`role=declare` 被跳过的行。
完整的语法目录(元素、属性、代码块角色)由 `src/cli/validate/known.ts` 生成到 `docs/reference.md`,可用 `npm run gen:reference` 重新生成。
**实现说明:**
+286
View File
@@ -0,0 +1,286 @@
# tttk syntax reference
Generated from `src/cli/validate/known.ts` by `npm run gen:reference`.
Do not edit by hand.
Everything not listed here is standard GFM markdown. Element names are
prefixed `md-` and must match exactly.
## Directive syntax
```text
:name[content]{key="value" .class #id}
```
- `[content]` is the directive body — a path, dice formula, color, etc.
- `{key="value"}` sets attributes; `{.class}` and `{#id}` are shorthands.
- Container directives use `::: name` … `:::`; nest with `::::`, `:::::`.
## Custom elements
| Directive | Body | Key attributes |
|---|---|---|
| `:md-dice` | dice formula | `key` |
| `:md-table` | CSV path or inline id | `roll`, `remix` |
| `:md-deck` | CSV path | `size`, `grid`, `layers` |
| `:md-link` | path, optional #section | — |
| `:md-embed` | path, optional #section | `headingBase` |
| `:md-bg` | image path or CSS color | `fit` |
| `:md-border` | color or border image | `width`, `slice` |
| `:md-font` | font family | `source`, `weight`, `color` |
| `:md-pins` | image path | `pins`, `fixed`, `labelStart` |
| `:md-commander` | command template CSV | `placeholder`, `class`, `height` |
| `:md-yarn-spinner` | Yarn Spinner file | `start` |
### `:md-dice`
Clickable dice roller; `key` stores the result in the URL.
- Body: dice formula
- Attributes: `key`
```markdown
:md-dice[1d20+5]{key="attack"}
```
```markdown
:md-dice[2d6+3]
```
```markdown
:md-dice[3d6kh1]
```
### `:md-table`
Tab-separated view of a CSV with optional group tabs.
- Body: CSV path or inline id
- Attributes: `roll`, `remix`
```markdown
:md-table[./npcs.csv]
```
```markdown
:md-table[./encounters.csv]{roll=true}
```
```markdown
:md-table[./quests.csv]{roll=true remix=true}
```
### `:md-deck`
Card layout from a CSV (legacy; prefer `yaml role=tag`).
- Body: CSV path
- Attributes: `size`, `grid`, `layers`
```markdown
:md-deck[./cards.csv]{size="63x88" grid="5x5" layers="title:1,1-5,1f8 body:1,5-5,8f3"}
```
### `:md-link`
Clickable link that expands the target article inline.
- Body: path, optional #section
```markdown
:md-link[./rules.md]
```
```markdown
:md-link[./rules.md#combat]
```
### `:md-embed`
Renders the target article or section inline.
- Body: path, optional #section
- Attributes: `headingBase`
```markdown
:md-embed[./rules.md#combat]{headingBase=1}
```
### `:md-bg`
Sets the surrounding article card background.
- Body: image path or CSS color
- Attributes: `fit`
- Body may also be a CSS color (e.g. `#2a1a3a`, `crimson`).
```markdown
:md-bg[./images/dungeon-bg.jpg]{fit="cover"}
```
```markdown
:md-bg[#2a1a3a]
```
### `:md-border`
Applies a border to the article card.
- Body: color or border image
- Attributes: `width`, `slice`
- Body may also be a CSS color (e.g. `#2a1a3a`, `crimson`).
```markdown
:md-border[crimson]{.b}
```
```markdown
:md-border[./frame.png]{width="2mm" slice="10" .all.round}
```
### `:md-font`
Sets the article font.
- Body: font family
- Attributes: `source`, `weight`, `color`
```markdown
:md-font[Noto Sans SC]{source="google" weight="700" color="#1a1a2e"}
```
### `:md-pins`
Numbered markers on an image.
- Body: image path
- Attributes: `pins`, `fixed`, `labelStart`
```markdown
:md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30 C:50,80" fixed}
```
```markdown
:md-pins[./images/dungeon.png]{labelStart="1"}
```
### `:md-commander`
Interactive command history / tracker panel.
- Body: command template CSV
- Attributes: `placeholder`, `class`, `height`
```markdown
:md-commander[./commands.csv]{placeholder="输入命令…" height="400px"}
```
### `:md-yarn-spinner`
Runs a Yarn Spinner dialogue file.
- Body: Yarn Spinner file
- Attributes: `start`
```markdown
:md-yarn-spinner[./story.yarn]{start="start"}
```
## Inline directives
### Icons — `:[name]`
Inserts an icon looked up under `assets/` (searching upward from the file).
Default extension is `.png`; `.svg`, `.gif`, `.jpg`, `.jpeg`, and `.webp`
are also supported.
```markdown
:[attack] :[shield.svg] :[potion]
```
### Command links — `:cmd[command]{label=...}`
Renders a clickable span that dispatches a journal command.
```markdown
:cmd[roll 1d20+5]{label=Roll initiative}
```
## Code block roles
The `role=` attribute on the info string determines how a fenced block is
handled.
### `role=spark-table`
CSV (or markdown pipe table) rendered as `:md-table`.
````markdown
```csv role=spark-table
d6,结果
1,遭遇强盗
2,平安无事
```
````
````markdown
```markdown role=spark-table
| d6 | 结果 |
|----|------|
| 1 | 遭遇强盗 |
| 2 | 平安无事 |
```
````
### `role=declare`
Reactive variable / tag-modifier definitions.
````markdown
```csv role=declare
tag,threshold,key,expr
,,$hp,$con*5+$mod_hp
#warrior,2,$mod_hp,20
```
````
### `role=file`
Stored as inline text content and stripped.
````markdown
```text role=file
Stored as inline content.
```
````
### `role=tag`
YAML rendered as a custom element.
````markdown
```yaml role=tag
tag: md-deck
body: ./cards.csv
size: 63x88
grid: 5x5
```
````
### No role / unknown role
No role → a normal visible code block. Unknown role → warned about and
stripped.
## CSV data files
Keep CSV files clean — a header row and data rows. Put deck configuration
and templates in a `yaml role=tag` block's `data-config`.
- `label` — unique row id (number or text).
- `body` — main content, supports markdown.
- Any other columns are custom fields.
- `{{prop}}` in `body` interpolates another column (or front-matter key)
from the same row; with `{remix}` it pulls from a random row.
- Wrap values containing `,`, `"`, or newlines in double quotes; escape
`"` as `""`. Lines beginning with `#` are comments.