Cover-1
Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染
9 artifacts rooted at Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染
- Status
- implemented
- Confidence
- high
- Artifacts
- 9
- Latest update
- 2026-05-19
Privacy Warning
No .dossierignore or redaction rules were found. Review this single-file output before sharing it outside your local workspace.
Relation Graph
Artifact Map
Artifacts
mvp-spec 1
-
Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染
implemented
docs/specs/2026-05-18-dossier-mvp-0-spec.md
change 5
-
Dossier Day 1 — 项目骨架搭建实施记录
implemented
docs/changes/2026-05-18-dossier-day-1-scaffolding-impl-notes.md -
Dossier MVP-0 implementation notes
implemented
docs/changes/2026-05-18-dossier-mvp-0-impl-notes.md -
Dossier render-spec editorial redesign (T1 + T2-7)
implemented
docs/changes/2026-05-19-dossier-render-spec-editorial-redesign-impl-notes.md -
Dossier render-spec polish + HTML-native interactions
implemented
docs/changes/2026-05-19-dossier-render-spec-html-interactions-impl-notes.md -
Dossier render-spec multi-value frontmatter + H3 sub-num parent prefix
implemented
docs/changes/2026-05-19-dossier-render-spec-multi-value-and-h3-prefix-impl-notes.md
review 3
-
Dossier MVP-0 r1 视觉修复 review
implemented
docs/reviews/2026-05-18-dossier-mvp-0-r1-review.md -
Dossier MVP-0 实施 review — Codex 提交验收
implemented
docs/reviews/2026-05-18-dossier-mvp-0-review.md -
Dossier MVP-0 视觉 review — UI / 排版深度审查
implemented
docs/reviews/2026-05-18-dossier-mvp-0-visual-review.md
Key Decisions
-
2. 技术决策(ADR 风格,锁定不再讨论)
docs/specs/2026-05-18-dossier-mvp-0-spec.md## 3. 项目结构
-
6.5.3 选择优先级(6 层,从 CLI 到 fallback)
docs/specs/2026-05-18-dossier-mvp-0-spec.md按下面顺序逐层试,第一个命中就返回:
-
Node.js ≥ 20
docs/specs/2026-05-18-dossier-mvp-0-spec.mdD1 · 运行时 | Node.js ≥ 20 | ❌ Bun:增加新工具链依赖,团队 / CI 未必有;❌ Deno:生态远
-
TypeScript 5.x(ESM)
docs/specs/2026-05-18-dossier-mvp-0-spec.mdD2 · 语言 | TypeScript 5.x(ESM) | ❌ 纯 JS:长期不可维护;❌ Rust:MVP 阶段杀鸡用牛刀
-
pnpm(同 html-anything)
docs/specs/2026-05-18-dossier-mvp-0-spec.mdD3 · 包管理器 | pnpm(同 html-anything) | ❌ npm / yarn:和 html-anything 不一致
Open Questions
-
dossier render 不传 --skill,--verbose 输出 selected skill: render-spec (frontmatter-kind)
docs/specs/2026-05-18-dossier-mvp-0-spec.md -
dossier render --skill bogus-name 报错退出码 3,不 silent fallback
docs/specs/2026-05-18-dossier-mvp-0-spec.md -
Frontmatter 显式写 renderskill: render-spec 时,--verbose 输出 reason 为 frontmatter-render-skill
docs/specs/2026-05-18-dossier-mvp-0-spec.md -
pnpm init、写 package.json / tsconfig.json
docs/specs/2026-05-18-dossier-mvp-0-spec.md -
跑通 pnpm dev render --help 输出 help 文本
docs/specs/2026-05-18-dossier-mvp-0-spec.md -
跑通 pnpm test 至少有一个 dummy test 通过
docs/specs/2026-05-18-dossier-mvp-0-spec.md -
parse/frontmatter.ts + gray-matter 包装
docs/specs/2026-05-18-dossier-mvp-0-spec.md -
parse/markdown.ts + marked 配置
docs/specs/2026-05-18-dossier-mvp-0-spec.md -
parse/toc.ts 抓 h2 + h3 → 嵌套结构
docs/specs/2026-05-18-dossier-mvp-0-spec.md -
写至少 3 个 fixture markdown + 测试通过
docs/specs/2026-05-18-dossier-mvp-0-spec.md
Reading Paths
Engineer / implementer
- Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染
- Dossier Day 1 — 项目骨架搭建实施记录
- Dossier MVP-0 implementation notes
- Dossier render-spec editorial redesign (T1 + T2-7)
- Dossier render-spec polish + HTML-native interactions
- Dossier render-spec multi-value frontmatter + H3 sub-num parent prefix
- Dossier MVP-0 r1 视觉修复 review
- Dossier MVP-0 实施 review — Codex 提交验收
- Dossier MVP-0 视觉 review — UI / 排版深度审查
Reviewer / handoff receiver
- Dossier MVP-0 r1 视觉修复 review
- Dossier MVP-0 实施 review — Codex 提交验收
- Dossier MVP-0 视觉 review — UI / 排版深度审查
- Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染
- Dossier Day 1 — 项目骨架搭建实施记录
- Dossier MVP-0 implementation notes
- Dossier render-spec editorial redesign (T1 + T2-7)
- Dossier render-spec polish + HTML-native interactions
- Dossier render-spec multi-value frontmatter + H3 sub-num parent prefix
Evidence (9)
| Relation | Label | Confidence | Rule | Evidence |
|---|---|---|---|---|
| implements | Dossier Day 1 — 项目骨架搭建实施记录 implements Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染 | high | frontmatter | frontmatter implements includes docs/specs/2026-05-18-dossier-mvp-0-spec.md |
| implements | Dossier MVP-0 implementation notes implements Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染 | high | frontmatter | frontmatter implements includes docs/specs/2026-05-18-dossier-mvp-0-spec.md |
| implements | Dossier render-spec editorial redesign (T1 + T2-7) implements Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染 | high | frontmatter | frontmatter implements includes docs/specs/2026-05-18-dossier-mvp-0-spec.md |
| implements | Dossier render-spec polish + HTML-native interactions implements Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染 | high | frontmatter | frontmatter implements includes docs/specs/2026-05-18-dossier-mvp-0-spec.md |
| implements | Dossier render-spec multi-value frontmatter + H3 sub-num parent prefix implements Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染 | high | frontmatter | frontmatter implements includes docs/specs/2026-05-18-dossier-mvp-0-spec.md |
| reviews | Dossier MVP-0 r1 视觉修复 review reviews Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染 | high | frontmatter | frontmatter reviews includes docs/reviews/2026-05-18-dossier-mvp-0-r1-review.md |
| reviews | Dossier MVP-0 实施 review — Codex 提交验收 reviews Dossier MVP-0 implementation notes | high | frontmatter | frontmatter reviews_target includes docs/changes/2026-05-18-dossier-mvp-0-impl-notes.md |
| reviews | Dossier MVP-0 实施 review — Codex 提交验收 reviews Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染 | high | frontmatter | frontmatter reviews_target includes docs/specs/2026-05-18-dossier-mvp-0-spec.md |
| reviews | Dossier MVP-0 视觉 review — UI / 排版深度审查 reviews Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染 | high | frontmatter | frontmatter reviews includes docs/reviews/2026-05-18-dossier-mvp-0-visual-review.md |
Rendered Documents (9)
Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染 docs/specs/2026-05-18-dossier-mvp-0-spec.md
Dossier Day 1 — 项目骨架搭建实施记录 docs/changes/2026-05-18-dossier-day-1-scaffolding-impl-notes.md
Dossier MVP-0 implementation notes docs/changes/2026-05-18-dossier-mvp-0-impl-notes.md
Dossier render-spec editorial redesign (T1 + T2-7) docs/changes/2026-05-19-dossier-render-spec-editorial-redesign-impl-notes.md
Dossier render-spec polish + HTML-native interactions docs/changes/2026-05-19-dossier-render-spec-html-interactions-impl-notes.md
Dossier render-spec multi-value frontmatter + H3 sub-num parent prefix docs/changes/2026-05-19-dossier-render-spec-multi-value-and-h3-prefix-impl-notes.md
Dossier MVP-0 r1 视觉修复 review docs/reviews/2026-05-18-dossier-mvp-0-r1-review.md
Dossier MVP-0 实施 review — Codex 提交验收 docs/reviews/2026-05-18-dossier-mvp-0-review.md
Dossier MVP-0 视觉 review — UI / 排版深度审查 docs/reviews/2026-05-18-dossier-mvp-0-visual-review.md
Embedded Sources (9)
Dossier Day 1 — 项目骨架搭建实施记录 docs/changes/2026-05-18-dossier-day-1-scaffolding-impl-notes.md
---
title: Dossier Day 1 — 项目骨架搭建实施记录
status: implemented
owner: claude
created: 2026-05-18
updated: 2026-05-19
implements: ["docs/specs/2026-05-18-dossier-mvp-0-spec.md"]
covers_timeline: "Day 1"
reviews: []
---
## 上下文
vision spec + MVP-0 spec 通过,命名定为 `dossier`(npm: `@xforg/dossier`)。本次落实 MVP-0 §10 时间线的 Day 1 三项 + 项目重命名 + spec 引用更新 + Codex 交接 brief。
## 完成项(按时间线 Day 1)
- [x] `mv AI_SPACE/agentstory AI_SPACE/dossier`
- [x] Rename 两份 spec md 文件(不动 v1 handcrafted html)
- [x] 全文 replace `AgentStory` → `Dossier` / `agentstory` → `dossier`,并修正 3 处 v1 HTML 路径引用、§16 命名表清理
- [x] 两份 spec `status: draft → ready`
- [x] `pnpm init` 等价(手写 package.json,scope `@xforg/dossier`)
- [x] `tsconfig.json`(NodeNext + ESM + strict)
- [x] `.gitignore` / `README.md` / `LICENSE`(Apache-2.0)
- [x] `bin/dossier.js`
- [x] `src/cli.ts` 完整实现:argv 解析、help、错误码、调度
- [x] `src/types.ts` 完整类型定义
- [x] `src/skills/registry.ts` 完整实现(Day 1 覆盖 layer 1/2/3/6;layer 4/5 已在 `docs/changes/2026-05-19-dossier-p0-p1-backlog-impl-notes.md` 接续实现)
- [x] `src/skills/loader.ts` 完整实现
- [x] `src/skills/render-spec/SKILL.md` 完整元数据
- [x] `src/skills/render-spec/toc-script.js` 移植自 spec
- [x] `src/render.ts` / `src/emit.ts` / `src/parse/*` 4 个 stub(含 TODO + spec 反链)
- [x] `src/skills/render-spec/template.html` 骨架
- [x] `src/skills/render-spec/style.css` 占位(含必须覆盖的 class 清单)
- [x] `src/skills/render-spec/example.html` 占位
- [x] `tests/fixtures/minimal.md`
- [x] `tests/render-spec.test.ts`(1 smoke + 3 skipped 端到端)
- [x] `pnpm install` + `pnpm approve-builds esbuild`
- [x] **验收 1**: `pnpm typecheck` clean
- [x] **验收 2**: `pnpm dev --help` 输出预期 help
- [x] **验收 3**: `pnpm test` 1 passed / 3 skipped(智能 skip 是预期)
- [x] Codex handoff brief 写在 `docs/specs/2026-05-18-codex-handoff-brief.md`
## 与 spec 的偏差(none material)
- 无 ADR 改动
- 无 API 表面改动
- 一处小 fix: spec §6.5.5 代码示例用了 `Tokens.Token`,marked@18 实际导出是 `Token`(顶层),已在 `src/parse/markdown.ts` 用正确类型
## 没做的(明确推迟到 Codex)
- `parseFrontmatter` / marked 解析 / semantic pass / toc 抽取的实际逻辑
- template.html / style.css 从 v1 手工版 HTML 移植
- 端到端测试 un-skip
- `pnpm render:self` 跑通
## 文件清单(写入 24 个新文件)
```
dossier/
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── tsconfig.json
├── pnpm-lock.yaml (pnpm 自动生成)
├── bin/dossier.js
├── src/
│ ├── cli.ts
│ ├── render.ts (stub)
│ ├── emit.ts (stub)
│ ├── types.ts
│ ├── parse/
│ │ ├── frontmatter.ts (stub)
│ │ ├── markdown.ts (stub)
│ │ ├── toc.ts (stub)
│ │ └── semantic.ts (stub w/ helper fns)
│ └── skills/
│ ├── registry.ts
│ ├── loader.ts
│ └── render-spec/
│ ├── SKILL.md
│ ├── template.html (skeleton)
│ ├── style.css (placeholder)
│ ├── toc-script.js
│ └── example.html (placeholder)
├── tests/
│ ├── fixtures/minimal.md
│ └── render-spec.test.ts
└── docs/
├── changes/2026-05-18-dossier-day-1-scaffolding-impl-notes.md ← 本文档
└── specs/
├── 2026-05-17-agentstory-vision-spec.html (v1 baseline, kept)
├── 2026-05-17-dossier-vision-spec.md (status: ready)
├── 2026-05-18-dossier-mvp-0-spec.md (status: ready)
└── 2026-05-18-codex-handoff-brief.md (status: ready)
```
## 下一步(Codex)
见 `docs/specs/2026-05-18-codex-handoff-brief.md`。
## 下一步(Claude)
等 Codex 提交,review + 验收。
## Note
- 没 `git init`。用户可按需自行 init。
- 没发 npm。MVP-0 spec §9.2 明确发布推迟到 Day 10 验收后。
Dossier MVP-0 implementation notes docs/changes/2026-05-18-dossier-mvp-0-impl-notes.md
---
title: Dossier MVP-0 implementation notes
status: implemented
owner: codex
created: 2026-05-18
updated: 2026-05-18
implements: ["docs/specs/2026-05-18-dossier-mvp-0-spec.md"]
reviews: []
---
# Dossier MVP-0 implementation notes
## Implemented
- Implemented the single-document render pipeline: frontmatter parsing, marked tokenization, semantic pass, TOC extraction, token rendering, and template emission.
- Kept runtime dependencies unchanged: `marked@18` and `gray-matter` only.
- Rendered `docs/specs/2026-05-17-dossier-vision-spec.md` into a self-contained HTML file with inline CSS and inline TOC script.
- Un-skipped the end-to-end tests and added frontmatter/parser coverage plus a regression check that generated HTML starts at `<!DOCTYPE html>`.
## Decisions and deviations
- Added `kind: vision-spec` to the vision spec frontmatter so skill dispatch selects `render-spec` via `frontmatter-kind`, matching the handoff acceptance command.
- The vision spec currently has 18 `h2` sections (`0` through `17`), not 17. The renderer outputs all 18 rather than hiding a real section.
- Browser-based `file://` verification was blocked by the Codex in-app browser URL policy. I did not bypass it; automated checks covered the generated structure and offline-resource constraints instead.
- Top callouts are rendered inline as `.callout` blocks in content rather than moved into a separate `.top-callouts` container. This preserves markdown order and keeps the MVP-0 heuristic simple.
## Verification
- `pnpm typecheck`
- `pnpm test`
- `pnpm render:self`
- `pnpm dev render docs/specs/2026-05-17-dossier-vision-spec.md --verbose`
- `pnpm dev render docs/specs/2026-05-17-dossier-vision-spec.md --skill bogus-name`
## r1 visual rework
- Fixed H2/H3 double numbering by detecting source heading prefixes such as `0.`, `1.1`, and `6.5`, using those as display numbers, and rendering the heading text without the source prefix.
- Fixed TOC text by sharing the cleaned heading text and stripping inline HTML tags before escaping, so entries no longer show `<em>` or `<strong>`.
- Restored the §0 hero treatment by rendering the first non-callout blockquote in section `0` as `.tagline`.
- Split long frontmatter titles on ` — ` into a short `<h1>` plus `<p class="subtitle">`.
- Hid empty frontmatter meta values such as empty `reviews: []`.
- Added status badge modifiers for `ready`, `draft`, `implemented`, and `archived`; `ready` now renders as `<span class="badge ready">ready</span>`.
- No new dependencies, directives, custom admonition syntax, or MVP-1 visual blocks were introduced.
Dossier render-spec editorial redesign (T1 + T2-7) docs/changes/2026-05-19-dossier-render-spec-editorial-redesign-impl-notes.md
---
title: Dossier render-spec editorial redesign (T1 + T2-7)
status: implemented
owner: claude
created: 2026-05-19
updated: 2026-05-19
implements:
- docs/specs/2026-05-18-dossier-mvp-0-spec.md
reviews:
- docs/reviews/2026-05-19-dossier-cover-implementation-review.md
---
# Dossier render-spec editorial redesign (T1 + T2-7)
承接 finetune-lab roadmap 试点的严苛设计 review,把 render-spec skill 的视觉语言
对齐 dossier README 已经声明的 design philosophy
([thariqs/html-effectiveness](https://thariqs.github.io/html-effectiveness/))
中"editorial / book-like"基调。
## Implemented
### T1 — 视觉 token 升级(CSS-only)
- `--font-display`: Charter / Source Serif Pro / Iowan Old Style / Georgia / Songti SC / Noto Serif SC 系,应用到所有 heading + `<em>` + 副标题
- `--font-mono`: 提到 token,所有 mono 引用统一
- `--accent`: `#1e3a8a` → `#b85c3d` terracotta,配套 `--accent-soft` 暖米色
- inline code `color`: 高饱品红 `#be185d` → `var(--ink)`,保留米底
- `section { margin-bottom: 56px → 88px }`,节间呼吸感跟上参考
- `em` 独立设 italic + serif,emphasis 由 weight 改为 style
### T2-7 — Frontmatter card 重构
旧版:单个 white card,9 行 implements + 2 行 reviews 长 path 撑爆第一屏。
新版(对位 thariqs Implementation Plan demo 的 eyebrow + stat 模式):
```html
<header class="frontmatter">
<p class="eyebrow">MVP SPEC</p> <!-- 仅当 frontmatter.kind 存在 -->
<h1>{{title}}</h1>
<p class="subtitle">{{subtitle}}</p> <!-- 仅当 title 含 " — " -->
<div class="stat-row">
<span class="badge ok">implemented</span>
<span class="stat"><span class="stat-label">Updated</span> 2026-04-22</span>
<span class="stat"><span class="stat-label">Owner</span> codex</span>
<span class="stat"><span class="stat-label">Implements</span> 7</span>
<span class="stat"><span class="stat-label">Reviews</span> 2</span>
</div>
<details class="frontmatter-details"> <!-- 默认折叠 -->
<summary>7 implements · 2 reviews</summary>
<div class="relation-block">
<p class="relation-label">Implements</p>
<ul class="relation-list"><li><code>...</code></li>...</ul>
</div>
<div class="relation-block">
<p class="relation-label">Reviews</p>
<ul class="relation-list">...</ul>
</div>
</details>
</header>
```
关键设计决策:
- **5 秒答案优先**:stat-row 给出 status / updated / owner / counts,path 详情藏在 details
- **eyebrow 来自 frontmatter.kind**:无 kind 字段则不渲染,零硬编码
- **counts 在 stat-row + 完整路径在 details** 是同一信息两种粒度,互补不冲突
- **`<details>` 默认 closed**:用户主动选择是否展开
### 顺带改善
- H2 `.sec-num` 加 accent-soft 暖米底胶囊样式,节编号从"挤在 H2 内"升级到"独立左侧标签",对位参考站 `01 · Milestones` 模式
- H3 sub-num 字号 + 间距微调
- TOC 改 `grid-template-columns: 36px 1fr`,长 H3 标题换行内部对齐(D7 残余)
- TOC sub-num 字号 11px → 12px,提升可读性(D5)
## Not Changed
- D6(scroll-spy 加 H3 active):toc-script.js 需重写 IntersectionObserver 范围,下一轮
- D11/D12(阅读时长 / 移动 drawer TOC):下一轮
- D9(list bullet 自定义):下一轮
- F8(confidence binary / next_action 硬编码):cover 而非 render-spec 的问题,独立 backlog
## Verification
```
pnpm typecheck → clean
pnpm test → 31 tests / all pass
pnpm dev render /Users/xforg/AI_SPACE/finetune-lab/docs/specs/2026-04-22-finetune-lab-gemma4-e2b-learning-roadmap-spec.md
→ 26293 bytes (修复前 24600, +1693 主要为 stat-row + details + relation-block CSS/HTML)
```
视觉对比(Claude in Chrome + macOS 1538px viewport):
| | 修前 | 修后 |
|---|---|---|
| 第一屏 frontmatter card 高度 | ~600px | ~290px |
| 第一屏可见正文 | 0(§1 标题刚压 fold) | §1 标题 + 第一段 + 第一个有序列表完整可见 |
| Heading 字体 | system-ui sans | Charter/Georgia serif |
| Accent 色 | 冷蓝 #1e3a8a | 暖陶 #b85c3d |
| 节编号位置 | 挤在 H2 文字内 | accent-soft 米底胶囊独立左侧 |
| inline code 色 | 抢戏品红 | 服从段落的 ink 色 |
| implements 路径展示 | 默认显示 7 行 path | 折叠为 `▸ 7 IMPLEMENTS · 2 REVIEWS`,点开看 |
## 残留 & 后续
T2-6(节编号外置改 grid 双列布局,跟参考站 `01 Milestones` 完全对齐)、T2-8(TOC 行为分级长短文档)、D6/D9/D11/D12 都先压着,下一轮做。
这一轮的核心收益是:dossier 自此**真正兑现 README 里声明的 design philosophy**,
跨项目 spec 渲染产物已经达到"可对外分享的文档"质感。
Dossier render-spec polish + HTML-native interactions docs/changes/2026-05-19-dossier-render-spec-html-interactions-impl-notes.md
---
title: Dossier render-spec polish + HTML-native interactions
status: implemented
owner: claude
created: 2026-05-19
updated: 2026-05-19
implements:
- docs/specs/2026-05-18-dossier-mvp-0-spec.md
reviews:
- docs/reviews/2026-05-19-dossier-cover-implementation-review.md
---
# Dossier render-spec polish + HTML-native interactions
承接 finetune-lab roadmap 复盘的反馈:"目前看起来感觉就是 md 的 html 版本,
完全没有将 html 的核心元素:如层叠样式表、svg 等放入"。
这一轮把那 5 条 polish 做掉的同时,加 2 个真正"页面而非文档"的 HTML-native 元素。
## Implemented
### D6 — TOC scroll-spy 扩展到 H3
`toc-script.js` 把 observation 从 `section[id]` 扩到 `[...section[id], ...main h3[id]]`。
H3 被激活时,从 `s7-3 → s7` 派生父 H2 id 并同时高亮,让 sticky TOC 在子节滚动时
不再"母节亮、子节失语"。
### D9 — 自定义 list bullets
- ul: terracotta 短横(`width: 8px; height: 1.5px`),通过 `li::before` 实现
- ol: 自维护 counter,前缀 mono terracotta `N.`,与设计语言一致
- 嵌套 ol 单独 counter(`dossier-ol-2`),避免数字 reset 失败
- 作用域 `main section ul/ol`,避免污染 TOC 和 frontmatter relation-list
### D11 — Reading time stat
`emit.ts::estimateReadingMinutes(html)`:
- 剥 `<pre>`/`<code>`/HTML 标签/HTML 实体 → 纯文本
- 中文 CJK 字符按 400 cpm,其他按 5 chars/word × 220 wpm
- `Math.max(1, round(minutes))` → `~N min` 加入 stat-row 第二位
- roadmap (~2700 中文字) → `~8 min`,与人类直觉吻合
### D12 — Mobile drawer TOC
- 桌面(>1024px):sticky 左栏(不变)
- 移动(≤1024px):
- 浮动 `☰` 按钮固定左上(SVG 三横线)
- 点击 → TOC 从左侧 slide-in,黑色 32% 遮罩盖正文
- 点遮罩 / `Escape` / 点击 TOC 内任意链接 → close
- `body[data-toc-open] { overflow: hidden }` 防底层滚动穿透
- 进入/退出 220ms cubic-bezier 缓动
### T2-6 — H2 双列 grid
H2 从 `display: flex` 改为 `display: grid; grid-template-columns: 52px 1fr`。
sec-num 固定 52px 列、`text-align: center`,所有节编号对齐到同一垂直锚线,
对位 thariqs/html-effectiveness `01 Milestones` 的列布局。
## HTML-native bonus (新增的非 polish 项)
### 顶部阅读进度条
```html
<div class="reading-progress" aria-hidden="true">
<div class="reading-progress-fill"></div>
</div>
```
```js
const max = h.scrollHeight - h.clientHeight;
progressFill.style.width = (h.scrollTop / max * 100).toFixed(2) + "%";
```
固定顶端 3px terracotta 细条,跟随滚动填充。低饱度米色底,accent 实色 fill。
是"这是网页不是 PDF"的第一信号。
### 代码块 copy 按钮
每个 `main pre` 在 `DOMContentLoaded` 时注入:
- 右上角 30×30 按钮,hover 时 opacity 0 → 1
- SVG clipboard icon(feather 风格 stroke)
- 点击 → `navigator.clipboard.writeText(code.textContent)` → 图标变 checkmark 1.4s → 恢复
- `.copied` 状态用 `--ok` 绿色,clear 视觉反馈
也是 HTML-native 的体现:脱离 markdown 静态文本,进入"可交互界面"。
## Verification
```
pnpm typecheck → clean
pnpm test → 34 tests / all pass(新增 3 条覆盖 reading-progress / scroll-spy 扩展 / reading-time)
pnpm dev render <roadmap>.md
→ 33394 bytes(前 26293,+7101 主要为 toc-script.js 扩展 + CSS 新规则)
```
视觉抓拍(Claude in Chrome, 1538×784 desktop):
| | 前 | 后 |
|---|---|---|
| 第一屏 stat-row | `IMPLEMENTED · UPDATED · OWNER · IMPLEMENTS 7 · REVIEWS 2` | 同上 + 第二位 `READING ~8 min` |
| 顶部 3px 进度条 | 无 | 米色底 + terracotta fill,随滚动增长 |
| H2 sec-num | flex baseline,宽度跟随内容 | grid 52px 固定列,垂直对齐严格 |
| TOC scroll-spy | 只 H2 高亮 | H2 + H3 双高亮(H3 active 时父 H2 也 active) |
| ul bullet | 浏览器默认 `•` 黑点 | 8×1.5px terracotta 短横 |
| ol number | 浏览器默认黑色 `1.` | mono terracotta `1.` |
| 移动端 TOC | static 占据顶部 5-8 屏 | drawer 默认隐藏,`☰` 按钮触发 slide-in + 遮罩 |
| 代码块 | 无交互 | hover 显示 copy 按钮,点击复制 + checkmark 反馈 |
## Not Changed
- T3 系列(自动 stat 卡片 / prompt block / SVG 图表 / mermaid):需要 frontmatter 扩展或 LLM,下个迭代
- D9 的更激进版本(用 inline SVG 替代 ::before):当前 CSS 实现已经足够细腻,下次再升
- 链接化 implements/reviews path(前序 F11):需要 output path plumbing,仍在 backlog
- H2 / H3 hover 显示锚链接按钮:留到下轮
## Test count
22 → 27 → 31 → 34(本轮 +3 回归: reading-progress markup / script extensions / reading-time stat)
Dossier render-spec multi-value frontmatter + H3 sub-num parent prefix docs/changes/2026-05-19-dossier-render-spec-multi-value-and-h3-prefix-impl-notes.md
---
title: Dossier render-spec multi-value frontmatter + H3 sub-num parent prefix
status: implemented
owner: claude
created: 2026-05-19
updated: 2026-05-19
implements:
- docs/specs/2026-05-18-dossier-mvp-0-spec.md
reviews:
- docs/reviews/2026-05-19-dossier-cover-implementation-review.md
---
# Dossier render-spec multi-value frontmatter + H3 sub-num parent prefix
试点把 dossier `render` 应用到 `finetune-lab` 的真 spec
(`2026-04-22-finetune-lab-gemma4-e2b-learning-roadmap-spec.md`,342 行、7 条
`implements` + 2 条 `reviews`、H3 编号风格不统一) 时浮出的两个问题。dossier 自己的
spec 因为边数极少,从未触发。
## Implemented
### F9 — 数组型 frontmatter 渲染成 list,不再 join 成长字符串
- `src/emit.ts` 新增 `renderMetaItem` + `renderMetaValueHtml`,替换原来 `escapeHtml(formatMetaValue(...))` 路径。
- 数组值现在输出 `<ul class="meta-list"><li><code>path</code></li>…</ul>`;多于 1 项时父 `<div>` 加 `meta-item-wide` 类,CSS 让该行跨整个 meta-grid(`grid-column: 1 / -1`)。
- 单元素数组保持窄格,与简单字符串字段视觉一致。
- `src/skills/render-spec/style.css` 新增 `.meta-item-wide` + `.meta-value .meta-list` + `.meta-value .meta-list code` 样式:浅米底胶囊、flex wrap、不破坏 grid 布局。
- 暂不做超链接:emit 不知道 output 文件相对工作区的位置,链接需要先把 output 路径感知 thread 进 emit。留到下一轮(候选 F11)。
### F10 — H3 单数字编号自动补上 parent secNum
- `src/parse/semantic.ts` 抽出 `composeH3DisplayNum`,规则:
- 显式 sub-num 含 `.`(如 `### 7.1 xxx`)→ 原样保留
- 显式 sub-num 单数字(如 `### 1. xxx`)+ 父 §N 已知 → 输出 `${parentSectionNum}.${explicit}`
- 无显式 sub-num → 原 fallback `${parentSectionNum}.${autoSubNum}`
- 同时修复 body H3 (`<span class="sub-num">8.1</span>`) 和 TOC sub-num,因为两者都消费同一个 `_dossierDisplayNum`。
## Verification
```
pnpm typecheck → clean
pnpm test → 2 files / 31 tests / all pass (新增 4 条回归: F9 3 条 + F10 2 条)
pnpm dev render /Users/xforg/AI_SPACE/finetune-lab/docs/specs/2026-04-22-finetune-lab-gemma4-e2b-learning-roadmap-spec.md
→ 24600 bytes(修复前 23946,+654 主要为 meta-list HTML/CSS)
```
浏览器视察 (Claude in Chrome):
- 第一屏 `implements` 7 条 path 改为 7 行可读 code 胶囊,`reviews` 同;frontmatter card 高度合理,§1 背景标题在屏内可见。
- TOC §8 子项 `8.1 / 8.2 / 8.3`、§9 子项 `9.1 / 9.2 / 9.3`(修复前是 bare `1 / 2 / 3`)。
- §7 子项保持 `7.1`–`7.6` 不变,未出现 `7.7.1` 这类 double-prefix。
## Not Changed
- `implements` / `reviews` 仍是 plain text,**不可点击**。链接化需要 emit 知道 output 路径相对工作区,本轮未做。
- `dossier.confidence` / `next_action` 仍是 MVP 粗启发式(F8)。
- 删除 artifact 报告、provenance/session adapter、watch、MCP server、LLM summary:依旧 out of scope。
## Note on test count
22 → 27 (前一轮 review fix) → 31 (本轮 F9+F10)。
Dossier MVP-0 r1 视觉修复 review docs/reviews/2026-05-18-dossier-mvp-0-r1-review.md
---
title: Dossier MVP-0 r1 视觉修复 review
status: implemented
owner: claude
created: 2026-05-18
updated: 2026-05-18
reviews_target: ["docs/specs/2026-05-17-dossier-vision-spec.html"]
follows: ["docs/reviews/2026-05-18-dossier-mvp-0-visual-review.md", "docs/specs/2026-05-18-codex-rework-brief-r1.md"]
reviewer: claude
implementer: codex
verdict: PASS
---
## 0. 结论
> ✅ **PASS**。r1 反工 brief 列的 6 个 P0 + 1 个 P1 缺陷**全部修复**。自动化层面无回归(typecheck / 6 tests / render:self 全绿)。视觉层面达到 r0 review §3 软标准(≥80% v1 手工版精度)。
> **MVP-0 dogfood 闭环正式达成**。
---
## 1. 7 项缺陷修复逐项核验
| # | 缺陷 | r0 现象 | r1 实际渲染 | 状态 |
|---|---|---|---|---|
| **P0-1** | H2 双重编号 | `§ 1 0. 一句话` (§N + 源编号同时出现) | `§ 0 一句话` / `§ 1 为什么...` / ... / `§ 17 下一步`,**单一编号且与源对齐** | ✅ |
| **P0-2** | H3 子节双重编号 | TOC `2.1 1.1 一个被忽视的现实` | `1.1 一个被忽视的现实` / `7.4 Dossier 数据模型...` | ✅ |
| **P0-3** | §0 hero 降级 | §0 渲染为普通 blockquote | `<div class="tagline"><span class="tagline-label">TL;DR</span>...</div>` 大色块 hero | ✅ |
| **P0-4** | TOC 含 `<em>` 字面 | TOC 显示 `2. 这个项目<em>不</em>做什么` | 0 处实体泄漏 (`grep -c '<em>'` = 0) | ✅ |
| **P0-5** | H1 整段,无 subtitle | `<h1>Dossier — 把 AI 给你...</h1>` 长串单行 | `<h1>Dossier</h1>` + `<p class="subtitle">把 AI 给你的每一份设计...</p>` | ✅ |
| **P0-6** | 空 frontmatter 字段渲染空白 | `<span class="meta-value"></span>` 空 row | 0 处空白 meta-value (`grep -c 'meta-value"></span>'` = 0) | ✅ |
| **P1-1** | badge "ready" 无 modifier class | `class="badge "` 尾随空格 | `class="badge ready">ready</span>` | ✅ |
## 2. 自动化层面无回归
| 指标 | r0 | r1 | 变化 |
|---|---|---|---|
| `pnpm typecheck` | clean | clean | ✅ |
| `pnpm test` | 6/6 passed | 6/6 passed | ✅ |
| 文件大小 | 54,647 B | 54,348 B | 略减 (-0.5%) ✅ |
| 零外链 | ✓ | ✓ (blocking `<script src>` / `<link href=http>` / `@import` = 0) | ✅ |
| Section 数 | 18 | 18 | 一致 ✅ |
## 3. § 7 "决不要做的事" 合规
Codex 在 r1 仍然遵守 rework brief §2 的 6 条铁律:
- ✗ 未引入 admonition 语法
- ✗ 未加新 dep(package.json diff = 0)
- ✗ 未改 ADR
- ✗ 未删 v1 baseline HTML(仍在 48,388 bytes)
- ✗ 未大重构 renderer(修改聚焦在 P0 范围)
- ✗ 未补 `.us-card` / `.scope-grid` / `.q-list` / `.ladder` / `.name-grid`
## 4. Codex r1 的实施亮点
值得记录的实现细节(从 impl notes 抽取):
1. **P0-1 修法**:正则识别 H2 / H3 前缀 `0.` / `1.1` / `6.5`,把数字部分用作显示节号,正文部分用 cleaned text。**完美兼容非线性编号**(如 §6.5)。
2. **P0-3 启发**:`section_num === 0` 时,将该节内**第一个非 callout blockquote** 提升为 `.tagline`。优雅 —— 不需要 admonition 语法,也不需要内容侧的特殊标记。
3. **P0-4 修法**:TOC 抓取与 heading 渲染**共用清洗后的 plain text**,不再有 escape over-coverage。
4. types.ts 加 `TocEntry.number?: string` 字段承载源编号 —— **数据模型层面**而非渲染层面表达"这是源里来的"。设计判断好。
## 5. 仍存在的 mini nit(不阻塞,记入 MVP-1 backlog)
| # | 描述 | 优先级 |
|---|---|---|
| nit-r1-1 | 锚点 ID 与显示编号不对称:`<section id="s1">` 但 H2 显示 `§ 0`。点击 TOC `#s1` 链接工作正常,但复制 URL 时不直观(`#s1` ≠ "§0")。 | 低,MVP-1 |
| nit-r1-2 | CLI 输出 `wrote ... (42559 bytes)` 实际磁盘 54,348 字节 —— `html.length` 用 UTF-16 code unit 计数,中文密集场景低估 ~22%。 | 极低,cosmetic |
| nit-r1-3 | 顶部 callout 仍 inline 在内容流(Codex r0 决策延续)。视觉上"在 §0 之前"已满足,可不动。 | 接受,不修 |
这三条都不阻塞 MVP-0 收尾。
## 6. r0 → r1 改动量复盘
- 实际改动符合预估(半天工作量)
- 文件大小 r0 → r1 略减(-299 bytes)—— 因为去掉了空 meta-value rows + cleaned heading 数据更紧凑,新增的 `.tagline` / `.subtitle` 渲染抵消
- 6/6 test 一直绿,说明 Codex 在编辑过程中持续跑测试,没出现"修一边坏一边"
## 7. MVP-0 正式收尾建议
MVP-0 §13 验收清单原有 13 项 + §6.5 追加 3 项 = 16 项,r1 后**全部命中**。
后续动作:
1. **本 review 签字** ✅(见 §0)
2. **MVP-0 spec frontmatter `status: implemented`** ← Codex 已做
3. **vision spec frontmatter `implements`** ← 需要清理(r0 nit-2 仍存在):清空或改为前向指 MVP-0
4. **MVP-0 dogfood 闭环**:把生成的 `2026-05-17-dossier-vision-spec.html` commit 进 repo(你之前没 git init —— 如果要走完闭环建议 init + 首次 commit)
5. **开始写 MVP-1 实施 spec**:dossier 识别(Tier 1 显式信号)、多文档 index 页、关系图
进入 MVP-1 前要带的 backlog(累积自 r0 + r1):
- nit-r1-1 锚点 ID 对称(low)
- nit-r1-2 CLI bytes 显示(cosmetic)
- 把 `kind:` 字段写进 `AI_SPACE/CLAUDE.md` spec 协议
- 把"H2/H3 标题可写源编号或省略,渲染器都能处理"也写进 spec 协议
- Skill registry layer 4/5(filename / directory pattern)
- 多 SKILL:`render-adr` / `render-change-note` / `render-review`
- §6.3 推迟的 5 类 admonition 块(us-card / scope-grid / q-list / ladder / name-grid)
## 8. 签字
**Verdict**: **PASS**。Dossier MVP-0 完成。
签:claude · 2026-05-18
---
> 🎉 **里程碑事件**:项目第一次完整地"自己渲染自己"。下一份输入是 MVP-1 spec —— 那将是 Dossier 第一次渲染一份**关于 Dossier 自己的、由 Dossier 处理过的 markdown**。dogfood 飞轮开始转。
Dossier MVP-0 实施 review — Codex 提交验收 docs/reviews/2026-05-18-dossier-mvp-0-review.md
---
title: Dossier MVP-0 实施 review — Codex 提交验收
status: implemented
owner: claude
created: 2026-05-18
updated: 2026-05-18
reviews_target: ["docs/specs/2026-05-18-dossier-mvp-0-spec.md", "docs/changes/2026-05-18-dossier-mvp-0-impl-notes.md"]
reviewer: claude
implementer: codex
---
## 0. 一句话结论
> ✅ **验收通过 (PASS with minor nits)**。
> 13 项验收清单全部命中(其中 2 项需用户在浏览器肉眼复核 — 见 §3)。Codex 完整遵守了 §7 "决不要做的事" 10 条铁律,未引新依赖、未改 ADR、未触 dossier / AI 范畴。
---
## 1. 自动化验收(11/11 命中)
| # | 验收项 | 结果 | 证据 |
|---|---|---|---|
| 1 | `pnpm typecheck` clean | ✅ | `tsc -p . --noEmit` exit 0 |
| 2 | `pnpm test` 全绿 | ✅ | **6/6 passed**(Codex 在我 4 个 stub 之上加了 2 个 frontmatter + 回归测试,多余)|
| 3 | `pnpm render:self` 跑通无 warning | ✅ | `wrote .../2026-05-17-dossier-vision-spec.html (42856 bytes)` |
| 4 | 输出 HTML 文件存在 | ✅ | 54,647 bytes |
| 5 | 文件大小 < 100KB | ✅ | 54.6KB / 100KB = 55% |
| 6 | 18 个 `<section id="sN">`(spec ≥17) | ✅ | grep -c = 18(spec 实际 18 节)|
| 7 | 左侧 `<aside class="toc">` | ✅ | 1 个,含 19 个 TOC 链接(h2 + h3)|
| 8 | Status badge `ready` | ✅ | 见 §4 nit-1 |
| 9 | callout 样式(⚠/📝/🎯)| ✅ | 3 处匹配 `callout warn/note/goal` |
| 10 | ASCII 图块 `class="ascii-diagram"` | ✅ | 3 处(§4 + §7.5 + §10.1 时间线?)|
| 11 | 零外链资源 | ✅ | grep `<script src=\|<link.*href="http\|@import\|url(http` = 0 |
| 12 | TOC scroll-spy JS inlined | ✅ | `tocLinks.forEach` 在文档内出现 1 次 |
| 13 | H2 § 编号自动生成 | ✅ | `§ 1` ~ `§ 17` 均含 `<span class="sec-num">` |
CLI 三项追加验收:
| # | 验收项 | 结果 |
|---|---|---|
| 14 | `--verbose` 输出 `selected skill: render-spec (frontmatter-kind)` | ✅ 一字不差 |
| 15 | `--skill bogus` exit 3 + 清晰错误 | ✅ `error: unknown skill: bogus` |
| 16 | Codex 写 impl notes | ✅ `docs/changes/2026-05-18-dossier-mvp-0-impl-notes.md` |
| 17 | MVP-0 spec status → `implemented` | ✅ |
## 2. § 7 "决不要做的事" 合规审查
10 项铁律全部遵守:
| ❌ 不要做 | Codex 是否触碰 |
|---|---|
| 改 `package.json` 加新 deps | ✗ — 仅 marked + gray-matter,未动 |
| 用 commander / cac / yargs | ✗ — 仍是手写 argv |
| 用 eta / ejs / handlebars | ✗ — `template.replace(/\{\{([A-Z_]+)\}\}/g, ...)` |
| 引入 highlight.js | ✗ — 仅 `class="lang-xxx"` |
| 设计 admonition 语法 | ✗ — 仅基于 ⚠/📝/🎯 emoji 前缀启发 |
| 实现 dossier / 关系图 / 多文档 | ✗ |
| 引入 AI / LLM 调用 | ✗ |
| 改 ADR | ✗ |
| 删 v1 手工版 HTML | ✗ — 仍在 repo(48,388 bytes)|
| 改 spec 核心决策 | ✗(小修见 §4)|
## 3. 仍需用户肉眼复核(2 项,文件已 `open` 在浏览器)
这两项自动化无法判断,建议你在浏览器里花 2 分钟看完:
- [ ] **视觉精度 ≥ 80% 对比 v1 手工版**:左右开两个 tab,对比 `2026-05-17-agentstory-vision-spec.html`(v1)vs `2026-05-17-dossier-vision-spec.html`(auto)。重点看:frontmatter card、TOC 排版、§4 ASCII 图、表格、callout 配色。
- [ ] **TOC scroll-spy 联动**:在自动版里滚到 §7,左侧 TOC 是否高亮 §7 entry?
如果这两项目测有问题,告诉我,我让 Codex 改。
## 4. 发现的 nit(不阻塞,但建议下一轮修)
### nit-1 · 非 draft / archived status 时 badge 缺 modifier class
`src/emit.ts:74`:
```typescript
const badgeClass = status === "draft" ? "draft" : status === "archived" ? "warn" : "";
```
当 status 是 `ready` / `implemented`,渲染出 `<span class="badge ">ready</span>`(尾随空格)。
- **影响**:CSS 仍命中 `.badge` 基类,渲染正常 —— 但 badge 不会有特异颜色暗示状态。
- **建议修法**:把 ready / implemented 也映射成对应 CSS class(如 `.badge.ready` 用绿色 `var(--ok)`)。1 行改动。
- **谁修**:放进 MVP-1 backlog;MVP-0 不阻塞。
### nit-2 · vision spec frontmatter `implements` 方向
Codex 在 vision spec 加了:
```yaml
implements: ["docs/specs/2026-05-18-dossier-mvp-0-spec.md"]
```
但 MVP-0 spec 也有:
```yaml
implements: ["docs/specs/2026-05-17-dossier-vision-spec.md"]
```
**两边互指**。按 `AI_SPACE/CLAUDE.md` 协议字面理解,`implements:` 应该是"我实施了 X"(向上指 vision),不是"X 实施了我"。当前是双向写法,会让未来的 dossier 关系图推断混淆。
- **建议**:清空 vision spec 的 `implements: []`,仅 MVP-0 spec 单向指向 vision。
- **修哪**:vision spec frontmatter 第 8 行。
- **优先级**:低,但 MVP-1 dossier 识别要用 frontmatter 建图,那时必须先理清。
### nit-3 · Codex 加的 `kind: vision-spec` 字段(**接受**)
Codex 给 vision spec 加了 `kind: vision-spec`,让 §6.5 layer 3 的 frontmatter-kind 调度命中 render-spec。
- **评价**:✅ **defensible**。SKILL.md 的 `applies_to.frontmatter_kind` 已声明接受 `"vision-spec"`,这一改让自动验收命令更精准(不靠 fallback)。
- **不动**。建议把 `kind:` 加入 AI_SPACE/CLAUDE.md 的 frontmatter 标准字段。
### nit-4 · 顶部 callout 不分离成 `.top-callouts` 容器(**接受**)
Codex 把 ⚠/📝/🎯 callout 直接 inline 在内容流而非分离到顶部 `.top-callouts`。
- **评价**:✅ **defensible**。markdown 顺序保留 + 实现更简单。v1 手工版做了分离是因为我当时手动选位置,无 markdown 顺序约束。
- **不动**。
## 5. 输出物大小变化(为什么 55KB 而非 48KB)
| 来源 | bytes | sections | 说明 |
|---|---|---|---|
| v1 手工版 | 48,388 | 17 | 命名前手工写,§0-16 |
| MVP-0 自动版 | 54,647 | 18 | 内容更新后,§0-17(多了 §6.5 Skill 调度 + 一些扩展)|
差额 ~6KB 主要来自 v2 spec 的额外内容(Dossier 专章扩张、§6.5 增补),不是渲染器低效。**可接受**。
## 6. 后续动作
立即可做:
- [ ] 你**肉眼复核**两项(§3)
- [ ] 我修复 nit-2(vision spec implements 清空)—— 待你确认方向再动
下个里程碑(MVP-1)应携带的 backlog(从本次 review 累积):
- 修复 nit-1(badge.ready / badge.implemented class)
- 把 `kind:` 字段写进 `AI_SPACE/CLAUDE.md` 标准约定
- 实现 Skill registry layer 4/5(filename + directory pattern)
- 多文档 dossier 识别(Tier 1 显式信号)
## 7. 我的 review 签字
- 所有验收项命中(自动化层面)
- 无 ADR 违反、无范围越权
- impl notes 透明,deviation 都有清晰说明
- 项目第一次"自己渲染自己"达成 ✅ —— 这是 spec §12 的 dogfood 闭环里程碑
**Verdict**: **PASS**。
签:claude · 2026-05-18
Dossier MVP-0 视觉 review — UI / 排版深度审查 docs/reviews/2026-05-18-dossier-mvp-0-visual-review.md
---
title: Dossier MVP-0 视觉 review — UI / 排版深度审查
status: implemented
owner: claude
created: 2026-05-18
updated: 2026-05-18
reviews_target: ["docs/specs/2026-05-17-dossier-vision-spec.html"]
follows: ["docs/reviews/2026-05-18-dossier-mvp-0-review.md"]
reviewer: claude
implementer: codex
verdict: NEEDS_REWORK
---
## 0. 一句话
> 上一份 review 自动化层面 PASS,但**视觉层面 NEEDS_REWORK**。发现 **6 个 P0 级缺陷** + 3 个 P1。最严重的是 H2 / H3 标题**双重编号显示**("§ 1 0. 一句话"),扫读体验明显劣于 v1 手工版。
**修正前后建议变化**:
- 上一份 review 的 verdict:~~PASS with minor nits~~ → **NEEDS_REWORK (visual)**
- 功能层面(解析、调度、零外链)仍然合格,但**MVP-0 §1.3 验收强制标准 1**("17 个 section 全部正确渲染,h2 / h3 层级清晰")未达成 —— "正确"含视觉。
---
## 1. P0 缺陷(必修,破坏阅读体验)
### P0-1 · H2 标题双重编号
**现象**:所有 H2 标题都同时显示自动 `§ N` 和源 markdown 里手写的 `N.` 前缀。
```html
<h2 id="s1"><span class="sec-num">§ 1</span><span>0. 一句话</span></h2>
↑ 自动 ↑ 源里写的, 没去掉
```
实际渲染:
```
§ 1 0. 一句话
§ 2 1. 为什么这个项目存在
§ 18 17. 下一步
```
读者看到**两套不同的编号同时存在**,且数值还**对不上**(§ 1 ↔ 0.;§ 18 ↔ 17.)。这是整份文档最严重的视觉噪声。
**根因**:spec markdown 源里手写了 `## 0. 一句话` / `## 1. 为什么...`(人类写文档的自然习惯),渲染器又叠加了从 1 开始的自动 `§ N`。
**修法(推荐 B)**:
- **(A)** 渲染器检测 H2 文本前缀 `^\s*\d+\.\s+`,剥离后再加 `§ N`
- **(B)** 渲染器检测到 H2 已有 `^\s*(\d+)\.\s+` 编号 → **沿用源里的数字作为 sec-num,不另加** ✓ 推荐:尊重作者意图,且能正确处理 §0
- **(C)** 把 spec markdown 里所有手写数字删掉
选 (B) 最稳:可处理 §0、§6.5 这种非线性编号;作者随便写,渲染自适应。
**Codex 落地位置**:`src/parse/semantic.ts` 第 25-30 行(h2 处理),和 `src/parse/markdown.ts` 第 56-63 行(heading renderer)。
### P0-2 · H3 子节同样双重编号
**现象**:TOC 里 H3 entries 显示如:
```
2.1 1.1 一个被忽视的现实 ← 自动 "2.1" + 源里 "1.1"
2.2 1.2 一个更被忽视的现实
8.4 7.4 Dossier 数据模型实现要点
```
同 P0-1 一起修。修 H2 后 H3 也跟着用源里的 `M.N` 而非 `(secNum).(subNum)`。
### P0-3 · §0 一句话失去 hero 处理
**现象**:源 markdown:
```markdown
## 0. 一句话
> AI 给你的每一份设计 / 方案 / 文档...
```
v1 手工版:深靛蓝大色块(`.tagline`)独占视觉中心 —— 这是"项目灵魂"展示位。
auto 渲染:变成 `<section id="s1">` 里普通 `<h2>` + 普通 `<blockquote>`。**hero 角色被降级为正文**。
**修法**:
- 渲染器识别 H2 文本是"一句话"/"Tagline"/"TL;DR"等关键词时,把后续第一个 `<blockquote>` 渲染为 `.tagline` 大色块
- 或:识别 `## 0.` (节号为 0 的章节)自动 hero 化
- 或更通用:源 markdown 用 `:::tagline` 标记(但 MVP-0 不引入 admonition 语法,所以推后到 MVP-1)
MVP-0 速修:检测 `_dossierSectionNum === 0` 时,整节用 hero 样式。
### P0-4 · TOC 里 H3 文本含未解析 HTML 实体
**现象**:
```html
<span>2. 这个项目<em>不</em>做什么</span>
<span>3.3 反用户故事(明确<strong>不</strong>服务)</span>
```
源 markdown 里有 inline HTML(`<em>不</em>` / `<strong>不</strong>`),TOC 抓取时**整段 escape 成实体**,读者看到字面的 `<em>` 字符串。
**根因**:`src/parse/toc.ts` 第 32 行用了 `token.text`(含原 HTML 字符),传给 `escapeHtml` 后被 over-escape。
**修法**:TOC 抓取时去掉 inline HTML 标签后再 escape:
```typescript
const plainText = token.text.replace(/<[^>]+>/g, "");
// 或更稳: 用 marked 提供的 token.tokens 走 inline parse + strip
```
### P0-5 · H1 过长 + 无 subtitle 拆分
**现象**:H1 是 frontmatter `title` 字段的整段:
```html
<h1>Dossier — 把 AI 给你的每一份设计 / 方案 / 文档自动渲染成可读、可分享、可关联的 HTML 档案</h1>
```
整段单行渲染(前端容器最大 760px,会自动 wrap,但 H1 不应承担副标题任务)。
v1 手工版:H1 = "Dossier"(一个词),下面单独 `<p class="subtitle">` 放副标题。视觉层级清晰。
**修法**:`src/emit.ts:41` 在 frontmatter 渲染时按 ` — ` / ` -- ` 分割 `title` 字段:
```typescript
const [head, ...rest] = title.split(/\s+[—-]\s+/);
// head → <h1>, rest.join(" — ") → <p class="subtitle">
```
或更稳:约定 frontmatter 单独有 `subtitle:` 字段,title 只放短名。
### P0-6 · Frontmatter 空字段仍渲染("reviews: " 空行)
**现象**:
```html
<div class="meta-item">
<span class="meta-label">reviews</span>
<span class="meta-value"></span>
</div>
```
空数组 / 空字符串字段被渲染为空白行,浪费 grid 槽位 + 视觉脏。
**修法**:`src/emit.ts:77` filter 时再加一道:
```typescript
.filter((key) => {
const v = frontmatter[key];
if (v === undefined || v === null) return false;
if (Array.isArray(v) && v.length === 0) return false;
if (typeof v === "string" && v.trim() === "") return false;
return true;
})
```
---
## 2. P1 缺陷(功能 OK,视觉不达手工版水平)
### P1-1 · badge "ready" / "implemented" 没有颜色 modifier
**现象**:`<span class="badge ">ready</span>` —— 尾随空格 + 无 `.badge.ready` modifier,命中默认 `.badge` 样式(蓝底)。视觉上 ready / draft / implemented / archived **没有色差暗示状态**。
**修法**:`src/emit.ts:74`:
```typescript
const badgeClass = ({
draft: "draft",
ready: "ready",
implemented: "ok",
archived: "warn",
} as const)[status] ?? "";
```
CSS 同步加 `.badge.ready { background: var(--ok-soft); color: var(--ok); }` 和 `.badge.ok` 等。
### P1-2 · TOC 顶部 "Spec · 18 节" 在双重编号下意义混淆
读者看到 "Spec · 18 节" + TOC 里 `§1` ~ `§18`,但源 markdown 是 `0~17`。修了 P0-1 后这一条自动消失(TOC 数字会跟随源编号)。
### P1-3 · CSS 只有 7.4KB(v1 ~10KB),可能漏了细节 class
CSS 占整文件 13.4%。v1 手工版 CSS 占约 20%。差额可能是被 §6.3 推迟的 `.us-card` / `.scope-grid` / `.q-list` / `.ladder` / `.name-grid` —— 这部分是有意省略,不算缺陷。
但其他细节(如 `.spec-footer` flex、`hr` 样式、`blockquote` 默认样式 vs `.callout` 区分)是否完整需要在浏览器里逐项比对。**建议**:你(用户)肉眼对比时如发现某类元素视觉粗糙,告诉我具体是哪类,我让 Codex 补 CSS。
---
## 3. 视觉层面**做对**的地方(公允评价)
| 项目 | 评价 |
|---|---|
| 整体 layout grid(260px TOC + max-width 760px 正文)| ✅ 完整复现 |
| ASCII 图块 `class="ascii-diagram"` 等宽 + 边框 | ✅ |
| Callout `.warn` / `.note` / `.goal` 三色区分 | ✅ |
| 代码块 `<pre><code class="lang-xxx">` 浅米底 | ✅ |
| 行内 `<code>` 浅红 | ✅ |
| 表格 hairline 边框 + th 浅灰 | ✅ |
| 字体栈(system-ui / PingFang SC / JetBrains Mono)| ✅ |
| Color tokens(暖白 / 墨黑 / 靛蓝 accent)| ✅ |
| TOC sticky + scroll-spy 联动 | ✅(前端 JS 仍工作,但视觉因 P0-4 受影响)|
| 单文件零外链 | ✅ |
---
## 4. 修复建议的优先级与代价
| 缺陷 | P 级 | 改动量 | 涉及文件 | MVP-0 阶段必修? |
|---|---|---|---|---|
| P0-1 双重编号 H2 | P0 | ~10 行 | `parse/semantic.ts` + `parse/markdown.ts` | ✅ 必修 |
| P0-2 双重编号 H3 | P0 | 含 P0-1 中 | 同上 | ✅ 必修 |
| P0-3 §0 hero 化 | P0 | ~15 行 + 1 个 CSS class(已在 CSS 里有 `.tagline`)| `parse/markdown.ts` + CSS | ✅ 必修 |
| P0-4 TOC HTML 实体 | P0 | ~3 行 | `parse/toc.ts` | ✅ 必修 |
| P0-5 H1 拆 subtitle | P0 | ~5 行 | `emit.ts` | ✅ 必修 |
| P0-6 空字段隐藏 | P0 | ~5 行 | `emit.ts` | ✅ 必修 |
| P1-1 badge ready 色 | P1 | ~3 行 TS + ~6 行 CSS | `emit.ts` + style.css | 可推 MVP-1 |
| P1-2 TOC 描述 | P1 | 跟 P0-1 一起修 | — | ✅ 跟随 |
| P1-3 CSS 完整性 | P1 | TBD | style.css | 视觉验证后定 |
**总改动量预估**:6 个 P0 修完约 40-60 行 TS + 10-20 行 CSS。半天工作量。
---
## 5. 给 Codex 的反工建议
把这份 review 整段贴给 Codex,并明确:
> 重做范围:P0-1 ~ P0-6 全部修复 + P1-1 可一起。修完跑 `pnpm render:self`,确认 `2026-05-17-dossier-vision-spec.html` 视觉与 v1 手工版 (`2026-05-17-agentstory-vision-spec.html`) 接近 80% 以上。提交时附 visual diff 截图(5-10 处对比)+ 更新 impl notes。
>
> 严禁:改 ADR、加新 dep、引入 admonition 语法(推迟到 MVP-1)、删 v1 baseline HTML。
---
## 6. 我作为 reviewer 的反思
我在写 spec 时**自己在 markdown 里手写了节号**(`## 0. 一句话`/`## 1. ...`),同时 v1 手工版 HTML 又用了 `<span class="sec-num">§ N</span>` —— 两套编号在**手工版本里恰好通过手动协调没出问题**,但自动渲染时这个隐患第一时间暴露。
**Spec 层面的修补**(如果不通过渲染器修):把 spec markdown 里 H2 / H3 手写的 `N.` / `M.M` 前缀全部删掉,让渲染器自动编号 —— 但这要求所有 spec 都遵循此约定,未必通用。
**推荐**:渲染器侧修(P0-1 的方案 B),同时在 `AI_SPACE/CLAUDE.md` 的 spec 协议里加一行说明:"H2 标题可写 `## N. 标题` 或 `## 标题`,渲染器都能正确编号"。这是更稳健的两方约定。
签:claude · 2026-05-18
Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染 docs/specs/2026-05-18-dossier-mvp-0-spec.md
---
title: Dossier MVP-0 实施 spec — 单文档 markdown → 设计级 HTML 自动渲染
status: implemented
owner: claude
created: 2026-05-18
updated: 2026-05-18
implements: ["docs/specs/2026-05-17-dossier-vision-spec.md"]
reviews: ["docs/reviews/2026-05-18-dossier-mvp-0-review.md", "docs/reviews/2026-05-18-dossier-mvp-0-visual-review.md", "docs/reviews/2026-05-18-dossier-mvp-0-r1-review.md"]
---
> ⚠️ 本文档是 MVP-0 的**实施 spec**,落地 vision spec §10.1。
> ⚠️ 范围严格收窄:**单 markdown → 单 HTML,零 AI,零 dossier**。
> 🎯 验收目标:`dossier render docs/specs/2026-05-17-dossier-vision-spec.md` 产出一份**结构正确、可导航、视觉清爽**的 HTML,能替代手工版(视觉精度允许略低,但绝不能更难读)。
## 0. 一句话
> **把 markdown 当作语义源,把 SKILL.md 当作版式源,把 frontmatter 当作元数据源 —— 三者拼合输出一份 inline-CSS 的单文件 HTML。零 AI、零 dossier、零外部依赖(运行时)。**
## 1. 目标 vs 非目标
### 1.1 In scope
- ✅ CLI 命令:`dossier render <file.md> [-o <out.html>]`
- ✅ Markdown 解析(CommonMark + GFM tables + 围栏代码块 + frontmatter)
- ✅ 自动生成左侧 TOC(h2 + h3)+ 滚动联动高亮
- ✅ Frontmatter 渲染为档案头卡(badge / title / meta grid)
- ✅ 一个内置 SKILL:`render-spec`(继承手工版 HTML 的视觉语言)
- ✅ 单文件 HTML 输出(CSS 内联,JS 内联,无外部 assets)
- ✅ 基本的语义识别:blockquote 中带 ⚠/📝/🎯 emoji → callout 样式;ASCII 图块(连续 `┌─` `│` `└─` 字符)→ `ascii-diagram` 样式
### 1.2 Out of scope(明确推迟)
| 推迟项 | 推迟到 |
|---|---|
| Dossier 识别 / 关系图 / cross-ref | MVP-1 |
| 多 SKILL(adr / change / review / resume / dossier-cover) | MVP-1 |
| Profile 切换、profile.md 解析 | MVP-1 |
| AI 调用(决策抽取、叙事摘要) | MVP-2 |
| Session JSONL 适配器 | MVP-2 |
| Watch 模式 | MVP-2 |
| Hook 集成 | MVP-2 |
| MCP / wrapper / 其他 agent | v1.0+ |
| 自定义 admonition / directive 语法 | MVP-1(如果需要)|
| 真正的代码语法高亮(highlight.js) | MVP-1 |
### 1.3 验收标准(强制)
跑 `dossier render /Users/xforg/AI_SPACE/dossier/docs/specs/2026-05-17-dossier-vision-spec.md`,输出 `2026-05-17-dossier-vision-spec.html`,浏览器打开后**必须**:
1. 17 个 section 全部正确渲染,h2 / h3 层级清晰
2. 左侧 TOC 可点击跳转,滚动时高亮当前节
3. Frontmatter 渲染为顶部 card(title / status / owner / date)
4. 文档顶部三条 ⚠/📝 callout 用 callout 样式
5. §4 / §7.5 的 ASCII 图块以等宽字体保留缩进,加边框背景
6. 所有表格清晰可读(边框、行高、表头加重)
7. 所有 `<pre><code>` 代码块有等宽字体 + 浅背景
8. 行内 `code` 有浅背景 + 微红
9. 整份 HTML 是**单文件**,移到任意机器 / 邮件附件 / iMessage 发送,对方双击仍能完美打开
10. 文件大小 < 100KB(裸体积,无外部资源)
**软标准**(不强制但希望接近):
- 视觉精度达到手工版的 ~80%(缺失的 20% 是 user story 卡片、ladder、naming grid 等需要 admonition 语法的高度定制块,自动 fallback 为标准表格 / 列表也可接受)
## 2. 技术决策(ADR 风格,锁定不再讨论)
| ADR | 决策 | 拒绝的选项 + 原因 |
|---|---|---|
| **D1 · 运行时** | **Node.js ≥ 20** | ❌ Bun:增加新工具链依赖,团队 / CI 未必有;❌ Deno:生态远 |
| **D2 · 语言** | **TypeScript 5.x(ESM)** | ❌ 纯 JS:长期不可维护;❌ Rust:MVP 阶段杀鸡用牛刀 |
| **D3 · 包管理器** | **pnpm**(同 html-anything) | ❌ npm / yarn:和 html-anything 不一致 |
| **D4 · markdown 解析器** | **marked@18**(同 html-anything) | ❌ markdown-it:再装一套 plugin 生态;❌ remark:unified 全家桶过重 |
| **D5 · frontmatter 解析** | **gray-matter** | 几乎是唯一标准选择 |
| **D6 · CLI 框架** | **零依赖手写 argv 解析**(仅 1 个命令 + 3 个 flag) | ❌ commander / cac:MVP-0 surface 太小 |
| **D7 · 模板引擎** | **零依赖,字符串模板字面量 + `{{PLACEHOLDER}}` 替换** | ❌ eta / ejs / handlebars:本期可识别的 placeholder 不到 10 个 |
| **D8 · 输出形态** | **单文件 HTML**,CSS / JS 全部 inline 到 `<style>` / `<script>` | ❌ 带 assets 目录:违反"双击即看"承诺 |
| **D9 · 代码高亮** | **MVP-0 不做真正高亮**,只给 `<pre><code>` 加 `.lang-ts` 等 class,靠 CSS 做最低限度配色 | ❌ highlight.js:bundle 增重 ~100KB;MVP-1 再加 |
| **D10 · 测试框架** | **vitest** | ❌ jest:ESM 不友好;vitest 同样可用 |
| **D11 · 输出路径默认** | 与输入同目录同名换 `.html` 后缀;可用 `-o` 覆盖 | — |
| **D12 · 字符编码** | **UTF-8 强制**,输入 / 输出统一 | — |
| **D13 · TOC 抓取层级** | **h2 + h3**,h1 视为文档标题不入 TOC | 文档已有 h1 = title,再放 TOC 是冗余 |
| **D14 · 项目代码位置** | **`/Users/xforg/AI_SPACE/dossier/`** 项目根 = vision spec 所在目录 | 已有 `docs/`,加 `src/` `package.json` 等 |
## 3. 项目结构
```
dossier/
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json
├── README.md # 安装 + 使用 + 一个例子
├── bin/
│ └── dossier.js # shebang → require dist/cli.js
├── src/
│ ├── cli.ts # 入口: argv → dispatch
│ ├── render.ts # 主管线: 编排 parse + select skill + emit
│ ├── parse/
│ │ ├── frontmatter.ts # gray-matter wrapper + 类型化
│ │ ├── markdown.ts # marked 配置 + 自定义 renderer
│ │ ├── toc.ts # 抓 h2/h3 → 嵌套结构
│ │ └── semantic.ts # 启发: callout / ascii-diagram / 表格类型
│ ├── skills/
│ │ ├── registry.ts # selectSkill() — 见 §6.5
│ │ ├── loader.ts # 扫 skills/*/SKILL.md 加载到内存
│ │ └── render-spec/
│ │ ├── SKILL.md # 元数据 + applies_to + 未来留给 AI 的 prompt
│ │ ├── template.html # HTML 骨架, 含 {{PLACEHOLDER}}
│ │ ├── style.css # 视觉语言, 移植自手工版
│ │ ├── toc-script.js # 内联 scroll-spy
│ │ └── example.html # 样例输出 (同 html-anything 惯例)
│ ├── emit.ts # 将 parsed AST + skill 拼接成最终 HTML
│ └── types.ts # 内部类型
├── tests/
│ ├── render-spec.test.ts # 用本仓库的 vision spec 做端到端 fixture
│ └── fixtures/
│ └── minimal.md # 最小输入: 一段 h1 + h2 + 一个表格
├── docs/
│ ├── specs/
│ │ ├── 2026-05-17-dossier-vision-spec.md
│ │ ├── 2026-05-17-agentstory-vision-spec.html # v1 手工版, 验收对比基线 (命名前)
│ │ └── 2026-05-18-dossier-mvp-0-spec.md # 本文档
│ ├── changes/
│ └── reviews/
└── .gitignore
```
## 4. CLI 表面(最终)
```
dossier render <input.md> [options]
Arguments:
<input.md> Path to a markdown file (absolute or relative)
Options:
-o, --out <path> Output HTML path (default: <input>.html same dir)
-s, --skill <name> Force a specific skill, overriding auto-detection
(default: auto-select via §6.5; falls back to render-spec)
MVP-0 only has "render-spec" registered;
passing an unknown name exits with code 3.
--no-toc Disable TOC sidebar
--verbose Print skill selection reason
-h, --help Show help
-v, --version Show version
Exit codes:
0 success
1 input file missing or unreadable
2 parse error (malformed frontmatter / markdown)
3 unknown skill
64 internal error
```
**例子**:
```bash
# 基本
dossier render docs/specs/2026-05-17-dossier-vision-spec.md
# → docs/specs/2026-05-17-dossier-vision-spec.html
# 指定输出
dossier render docs/specs/foo.md -o /tmp/foo.html
# 禁 TOC
dossier render docs/changes/short-note.md --no-toc
```
## 5. 渲染管线(步骤详解)
```
input.md
│
▼
┌──────────────────────────────────┐
│ 1. read & utf8 decode │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 2. gray-matter → { data, content }│ data = frontmatter
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 3. marked parse content → AST │
│ (与默认 renderer 解耦) │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 4. semantic pass on AST: │
│ • blockquote 前缀 → callout │
│ • <pre> 中 ASCII 字符 → diagram │
│ • h2/h3 加 id (slug) │
│ • h2 → wrap as <section id> │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 5. toc extract → 嵌套结构 │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 6. AST → HTML fragment │
│ (自定义 marked renderer) │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 7. load skill: template.html │
│ + style.css + toc-script.js │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 8. template substitution: │
│ {{TITLE}} / {{SUBTITLE}} │
│ {{FRONTMATTER_HTML}} │
│ {{TOC_HTML}} │
│ {{CONTENT_HTML}} │
│ {{STYLE_CSS}} │
│ {{TOC_SCRIPT_JS}} │
└──────────────────────────────────┘
│
▼
output.html (single file, all inline)
```
## 6. `render-spec` SKILL 详解
### 6.1 SKILL.md
```markdown
---
name: render-spec
description: 渲染一份 vision spec / 实施 spec / ADR 类的设计文档,强调结构层级、决策可定位、扫读快
mode: document
scenario: engineering
aspect_hint: "可滚动竖版, 最大宽度 760px 正文 + 260px 左 TOC"
recommended: 1
applies_to:
- frontmatter.kind: ["spec", "mvp-spec", "adr"]
- filename_pattern: "*-spec.md"
mvp_ai_required: false
---
【模板用途】渲染本仓库 `docs/specs/` 下的设计文档为可扫读、可分享的单文件 HTML。
【视觉语言铁律】
- 配色: 暖白底 #faf9f6, 墨黑文字 #1a1a1a, 深靛蓝 accent #1e3a8a
- 字体: system-ui / PingFang SC 正文, JetBrains Mono 代码
- 1px hairline 边框, 不用阴影 / 模糊
- callout 用左侧 3px 色条区分类型
- 代码块用 #f4f2eb 浅米底
- 表格 th 用浅灰底, td hairline 分割
【未来 AI 钩子】(MVP-0 不调用)
当 mvp_ai_required: true 时, AI 应:
- 抽取每个 section 的 1 句话摘要写入侧栏
- 生成一段 ≤ 100 字的 dossier description
```
### 6.2 template.html(骨架)
```html
<!DOCTYPE html>
<html lang="{{LANG}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}}</title>
<style>{{STYLE_CSS}}</style>
</head>
<body>
<div class="layout">
{{TOC_BLOCK}}
<main>
{{FRONTMATTER_CARD}}
{{CALLOUTS_BLOCK}}
{{CONTENT_HTML}}
<footer class="spec-footer">
<span>{{TITLE}} · {{STATUS}} · {{UPDATED}}</span>
<span>rendered by dossier</span>
</footer>
</main>
</div>
<script>{{TOC_SCRIPT_JS}}</script>
</body>
</html>
```
### 6.3 style.css
从 [v1 手工版 spec.html](./2026-05-17-agentstory-vision-spec.html) 的 `<style>` 块**整段移植**。MVP-0 不重新设计视觉。
关键 class 列表(CSS 必须覆盖):
- `.layout` / `aside.toc` / `main`
- `.frontmatter` / `.status-row` / `.badge` / `.badge.draft` / `.badge.warn`
- `.meta-grid` / `.meta-item` / `.meta-label` / `.meta-value`
- `.top-callouts` / `.callout` / `.callout.warn`
- `.tagline` / `.tagline-label`
- `section` / `section h2 .sec-num` / `section h3 .sub-num`
- `table` / `th` / `td`
- `pre` / `code` / `pre code`
- `.ascii-diagram`
- `blockquote`
- `.spec-footer`
**不实现**(手工版有但 MVP-0 自动 fallback 为标准表格):
- `.us-grid` / `.us-card` (用户故事卡片)
- `.scope-grid` / `.scope-col`(MVP 范围双列)
- `.q-list` / `.q-item` / `.q-tendency`(开放问题琥珀卡)
- `.ladder` / `.ladder-step`(成功标准阶梯)
- `.name-grid` / `.name-card`(命名候选卡)
这些块在 MVP-0 中保留原始 markdown 表格 / 列表样式即可。MVP-1 引入 admonition 语法时再回头补。
### 6.4 toc-script.js
```js
(function() {
const sections = Array.from(document.querySelectorAll('section[id]'));
const tocLinks = new Map();
document.querySelectorAll('aside.toc a').forEach(a => {
const id = a.getAttribute('href').slice(1);
tocLinks.set(id, a);
});
const onScroll = () => {
let active = sections[0]?.id;
const offset = 120;
for (const s of sections) {
if (s.getBoundingClientRect().top - offset <= 0) active = s.id;
}
tocLinks.forEach((a, id) => a.classList.toggle('active', id === active));
};
document.addEventListener('scroll', onScroll, { passive: true });
onScroll();
})();
```
直接复用手工版。
## 6.5 Skill 调度接口(MVP-0 单 skill,但接口为 MVP-1 扩展铺好)
> 📝 本节是 v1.1 增补(2026-05-18)。回应"如何针对不同文档类型产出不同样式"的架构问题。
> 🎯 核心承诺:~35 行代码,让 MVP-1 加新 skill 时**零架构改动**,只新增 skill 目录。
### 6.5.1 为什么 MVP-0 就要写
MVP-0 注册的 skill 只有一个(`render-spec`),看上去不需要调度系统。但**架构债越早还越便宜**:如果 MVP-0 把 skill 选择写死成常量,MVP-1 加 `render-adr` / `render-change` / `render-review` 时必须先重构入口。提前 35 行代码,下个里程碑直接增量加 skill 目录就行。
### 6.5.2 函数契约
`src/skills/registry.ts` 导出一个纯函数:
```typescript
export type SkillId = string;
export type SkillSelectionInput = {
frontmatter: Record<string, unknown>;
filepath: string; // absolute or workspace-relative
cliOverride?: SkillId; // from `--skill` flag
};
export type SkillSelection = {
skillId: SkillId;
reason:
| "cli-flag"
| "frontmatter-render-skill"
| "frontmatter-kind"
| "filename-pattern" // MVP-1
| "directory-pattern" // MVP-1
| "fallback";
matched_skills?: SkillId[]; // 多个匹配时记录(debug 用)
};
export function selectSkill(input: SkillSelectionInput): SkillSelection;
```
返回时附带 `reason` 是为了 `--verbose` 输出 + 未来 UI 上"为什么选了这个 skill"的解释。
### 6.5.3 选择优先级(6 层,从 CLI 到 fallback)
按下面顺序逐层试,第一个命中就返回:
| 层 | 信号 | MVP-0 实现 |
|---|---|---|
| 1 | CLI `--skill <name>` 显式指定 | ✅ |
| 2 | Frontmatter `render_skill: <name>` 文档自己声明 | ✅ |
| 3 | Frontmatter `kind:` ∈ skill 的 `applies_to.frontmatter_kind` | ✅ |
| 4 | 文件名匹配 skill 的 `applies_to.filename_patterns` | ⏸ MVP-1 |
| 5 | 目录匹配 skill 的 `applies_to.directory_patterns` | ⏸ MVP-1 |
| 6 | Fallback: `render-spec` | ✅ |
MVP-0 实际只跑 1 / 2 / 3 / 6;4-5 在 registry.ts 里**预留空分支**,加 `// TODO MVP-1` 注释。MVP-1 填实现。
### 6.5.4 SKILL.md `applies_to` 字段约定
每个 SKILL.md frontmatter 必须有:
```yaml
---
name: render-spec
applies_to:
frontmatter_kind: ["spec", "mvp-spec", "vision-spec"] # 第 3 层
filename_patterns: ["*-spec.md", "*-vision-spec.md"] # 第 4 层 (MVP-1)
directory_patterns: ["docs/specs/**"] # 第 5 层 (MVP-1)
priority: 10 # 多个 skill 命中时的优先级 (高优先)
---
```
MVP-0 阶段 render-spec/SKILL.md 必须把这四个字段全部填好 —— **即使 4/5 层在 MVP-0 还没生效**。这是把"未来扩展的合约"刻进 schema,让 MVP-1 加 skill 时只需照抄结构。
### 6.5.5 代码骨架(约 35 行)
```typescript
// src/skills/registry.ts
import { loadAllSkills, type SkillMeta } from "./loader.js";
const FALLBACK: SkillId = "render-spec";
export function selectSkill(input: SkillSelectionInput): SkillSelection {
const skills = loadAllSkills();
const has = (id: SkillId) => skills.some(s => s.id === id);
// Layer 1: CLI override
if (input.cliOverride) {
if (!has(input.cliOverride)) {
throw new Error(`unknown skill: ${input.cliOverride}`);
}
return { skillId: input.cliOverride, reason: "cli-flag" };
}
// Layer 2: frontmatter render_skill
const fmSkill = input.frontmatter.render_skill;
if (typeof fmSkill === "string" && has(fmSkill)) {
return { skillId: fmSkill, reason: "frontmatter-render-skill" };
}
// Layer 3: frontmatter kind
const fmKind = input.frontmatter.kind;
if (typeof fmKind === "string") {
const matches = skills.filter(s =>
s.applies_to.frontmatter_kind?.includes(fmKind)
);
if (matches.length) return pickByPriority(matches, "frontmatter-kind");
}
// Layer 4, 5: filename / directory patterns
// TODO MVP-1: implement here. See §6.5.3 table.
// Layer 6: fallback
return { skillId: FALLBACK, reason: "fallback" };
}
function pickByPriority(skills: SkillMeta[], reason: SkillSelection["reason"]): SkillSelection {
const sorted = [...skills].sort(
(a, b) => (b.applies_to.priority ?? 0) - (a.applies_to.priority ?? 0)
);
return {
skillId: sorted[0].id,
reason,
matched_skills: sorted.length > 1 ? sorted.map(s => s.id) : undefined,
};
}
```
行数:~38 行。如承诺。
### 6.5.6 MVP-1 加新 skill 的工作量
MVP-1 加 `render-adr` 的全部步骤:
1. `mkdir src/skills/render-adr/`
2. 写 `SKILL.md` 含 `applies_to: { frontmatter_kind: ["adr"], filename_patterns: ["*-adr-*.md"] }`
3. 写 `template.html` / `style.css` / `example.html`
4. **`registry.ts` 不动**(`loadAllSkills()` 自动发现)
5. 在 MVP-1 时实现 layer 4 / 5 的 pattern 匹配(一次性工作,所有 skill 受益)
**核心承诺**:加 skill 不改 registry。这是 §6.5 存在的全部目的。
### 6.5.7 CLI 改动
`src/cli.ts` 的 render 命令调用更新为:
```typescript
const { data: fm } = parseFrontmatter(md);
const selection = selectSkill({
frontmatter: fm,
filepath: inputAbs,
cliOverride: argv.skill, // undefined when --skill not passed
});
if (argv.verbose) {
console.log(`selected skill: ${selection.skillId} (${selection.reason})`);
}
const html = await render({
markdown: md,
skillId: selection.skillId,
withToc: argv.toc,
});
```
§4 CLI 中 `--skill` 语义已经在前面同步更新:从"必填,默认 render-spec"改为"覆盖自动选择"。
### 6.5.8 验收追加项
§13 验收检查清单加 3 条(已隐含写入 §13 列表中):
- [ ] `dossier render <spec.md>` 不传 `--skill`,`--verbose` 输出 `selected skill: render-spec (frontmatter-kind)`
- [ ] `dossier render <spec.md> --skill bogus-name` 报错退出码 3,不 silent fallback
- [ ] Frontmatter 显式写 `render_skill: render-spec` 时,`--verbose` 输出 reason 为 `frontmatter-render-skill`
### 6.5.9 何时需要修改 registry 本身
未来真正需要改 registry.ts 的情形(不是加 skill):
- 引入第 7 层 AI 分类(MVP-2)→ 在 layer 5 之后插一个 LLM 调用层
- 引入"多个 skill 同时命中且 priority 相同" → 需要 disambiguation UI / 提示
- 引入 block-level skill(同一文档不同节用不同样式)→ 这是大重构,远期再说
MVP-0 内只关心:写好 skeleton,把 TODO 注释留对位置。
## 7. Markdown 约定 / 语义启发(MVP-0 支持的全集)
| markdown 形态 | 渲染为 | 注 |
|---|---|---|
| frontmatter (`--- ... ---`) | 顶部 `.frontmatter` card | `title` 必需;`status` 渲染为 badge;`owner` / `created` / `updated` / `implements` / `reviews` 渲染为 meta-grid |
| h1 (单数) | 不直接渲染(作为 `<title>` 和 frontmatter card 标题) | 一份文档应该只有一个 h1 |
| h2 | 包裹 `<section id="s{N}">`,h2 自动加 `<span class="sec-num">§ N</span>` | N 是文档中第 N 个 h2 |
| h3 | 普通 `<h3>`,前缀 `<span class="sub-num">{N}.{M}</span>` 取自 markdown 内文(如果以 `数字.数字` 开头)| |
| `> ⚠️ ...` | `.callout.warn` | 仅文档顶部的 callout 区识别 |
| `> 📝 ...` / `> 🎯 ...` | `.callout` 不同变体 | |
| 普通 blockquote | `<blockquote>` 默认样式 | |
| ```` ```lang ```` | `<pre><code class="lang-xxx">` | 不做真正高亮 |
| 行内 `code` | `<code>` 默认浅红 | |
| GFM 表格 | 自动应用 `.table` 样式 | |
| `<pre>` 中含 `┌─` `│` `└─` 之一 | 加 `.ascii-diagram` class | semantic pass 检测 |
| 链接 | 普通 `<a>` 蓝色下划线 | |
| 行内 `**` `*` | 标准 | |
| `---` 分隔 | `<hr>` | |
**不识别**(MVP-0):
- 自定义 `:::admonition` 块
- mermaid / 图表
- footnotes
- 任务列表(`- [ ]`)
## 8. 关键代码骨架
### 8.1 `src/cli.ts`
```typescript
#!/usr/bin/env node
import { readFile, writeFile, access } from "node:fs/promises";
import { resolve, dirname, basename } from "node:path";
import { render } from "./render.js";
import { fileURLToPath } from "node:url";
type Argv = {
command: string;
input?: string;
out?: string;
skill: string;
toc: boolean;
};
function parseArgv(argv: string[]): Argv | { error: string } {
const a: Argv = { command: "", skill: "render-spec", toc: true };
// ... 手写解析: render <input> [-o ...] [-s ...] [--no-toc]
return a;
}
async function main() {
const argv = parseArgv(process.argv.slice(2));
if ("error" in argv) { console.error(argv.error); process.exit(1); }
if (argv.command !== "render") { /* help */ process.exit(0); }
const inputAbs = resolve(argv.input!);
await access(inputAbs).catch(() => { console.error("input missing"); process.exit(1); });
const md = await readFile(inputAbs, "utf8");
const html = await render({ markdown: md, skillId: argv.skill, withToc: argv.toc });
const outAbs = argv.out ? resolve(argv.out) : inputAbs.replace(/\.md$/i, ".html");
await writeFile(outAbs, html, "utf8");
console.log(`wrote ${outAbs} (${html.length} bytes)`);
}
main().catch((e) => { console.error(e); process.exit(64); });
```
### 8.2 `src/render.ts`
```typescript
import { parseFrontmatter } from "./parse/frontmatter.js";
import { parseMarkdown } from "./parse/markdown.js";
import { extractToc } from "./parse/toc.js";
import { applySemantic } from "./parse/semantic.js";
import { loadSkill } from "./skills/loader.js";
import { emit } from "./emit.js";
export async function render(opts: {
markdown: string;
skillId: string;
withToc: boolean;
}): Promise<string> {
const { data: fm, content } = parseFrontmatter(opts.markdown);
const ast = parseMarkdown(content);
applySemantic(ast); // mutate ast
const toc = opts.withToc ? extractToc(ast) : null;
const skill = await loadSkill(opts.skillId);
return emit({ fm, ast, toc, skill });
}
```
### 8.3 测试用例(最小集)
```typescript
// tests/render-spec.test.ts
import { test, expect } from "vitest";
import { readFile } from "node:fs/promises";
import { render } from "../src/render";
test("MVP-0 端到端: vision spec → HTML", async () => {
const md = await readFile(
"docs/specs/2026-05-17-dossier-vision-spec.md", "utf8"
);
const html = await render({ markdown: md, skillId: "render-spec", withToc: true });
// 强制断言
expect(html).toMatch(/<title>Dossier/);
expect(html.length).toBeLessThan(100_000); // < 100KB
expect(html).toMatch(/aside class="toc"/);
expect((html.match(/<section id="s\d+"/g) ?? []).length).toBeGreaterThanOrEqual(17);
expect(html).toMatch(/badge draft/); // status badge
expect(html).toMatch(/callout warn/); // 顶部 ⚠ callout
expect(html).toMatch(/ascii-diagram/); // §4 架构图
expect(html).not.toMatch(/<script src=/); // 无外链 script
expect(html).not.toMatch(/<link[^>]+href="http/); // 无外链 css
});
test("minimal: 没有 frontmatter 也要能渲染", async () => {
const md = "# Hello\n\n## Section A\n\nHi.\n";
const html = await render({ markdown: md, skillId: "render-spec", withToc: true });
expect(html).toMatch(/<title>Hello<\/title>/);
});
```
## 9. 构建与发布
### 9.1 `package.json`(关键片段)
```json
{
"name": "dossier",
"version": "0.0.1",
"private": false,
"type": "module",
"bin": { "dossier": "./bin/dossier.js" },
"files": ["bin", "dist", "README.md", "LICENSE"],
"scripts": {
"dev": "tsx src/cli.ts",
"build": "tsc -p .",
"test": "vitest run",
"render:self": "tsx src/cli.ts render docs/specs/2026-05-17-dossier-vision-spec.md"
},
"dependencies": {
"marked": "^18.0.3",
"gray-matter": "^4.0.3"
},
"devDependencies": {
"typescript": "^5.7.0",
"tsx": "^4.20.0",
"vitest": "^3.0.0",
"@types/node": "^20.0.0"
},
"engines": { "node": ">=20" }
}
```
### 9.2 发布策略
- **MVP-0**: 不发 npm。用 `pnpm dev`(tsx)即可,`pnpm render:self` 是日常 dogfood 命令。
- **MVP-0 完成验收后**: 发 npm `0.0.1`,让别人能 `npx dossier@0.0.1 render foo.md` 尝鲜。
## 10. 时间线(1-2 周)
按"先骨架后细节"组织。每天 1-2 项可见进展。
### Day 1 — 项目初始化(半天)
- [ ] `pnpm init`、写 package.json / tsconfig.json
- [ ] 跑通 `pnpm dev render --help` 输出 help 文本
- [ ] 跑通 `pnpm test` 至少有一个 dummy test 通过
### Day 2-3 — markdown 解析层
- [ ] `parse/frontmatter.ts` + gray-matter 包装
- [ ] `parse/markdown.ts` + marked 配置
- [ ] `parse/toc.ts` 抓 h2 + h3 → 嵌套结构
- [ ] 写至少 3 个 fixture markdown + 测试通过
### Day 4-5 — semantic pass + skill 装配 + registry
- [ ] `parse/semantic.ts`:blockquote → callout / pre → ascii-diagram / h2 → section wrap
- [ ] `skills/render-spec/` 完整移植手工版的 CSS / JS
- [ ] `skills/loader.ts`:扫描 `skills/*/SKILL.md` 加载到内存
- [ ] `skills/registry.ts`:实现 §6.5.5 的 selectSkill(layer 1/2/3/6)
- [ ] `emit.ts` 拼接 + placeholder 替换
### Day 6-7 — 端到端 dogfood
- [ ] 跑 `pnpm render:self` 出 HTML
- [ ] 浏览器打开手动 review,对比手工版,列差距
- [ ] 修补差距,直到验收 1-10 全部通过
### Day 8-10 — 打磨与边界(可选缓冲)
- [ ] 错误处理:缺失 frontmatter / 损坏 markdown / 未知 skill
- [ ] README + 一个 GIF demo
- [ ] **发 npm 0.0.1**
### Day 11-14 — 缓冲 / 早期 MVP-1 准备
- [ ] 写 MVP-1 实施 spec 的初稿
- [ ] 如果时间允许:尝试增量功能(admonition 解析、user story 卡片样式)
## 11. 风险与缓解
| 风险 | 触发 | 缓解 |
|---|---|---|
| marked@18 自定义 renderer API 比预想难用 | Day 2-3 卡住 | fallback: 用 marked 默认 renderer + 字符串后处理 |
| 视觉精度不到验收软标准 80% | Day 6-7 review 不过 | 把 acceptance 软标准降到 70%,把视觉精度差距列入 MVP-1 backlog |
| frontmatter 解析 edge case 多(嵌套 / 多行字符串 / 数组) | Day 2 | gray-matter 已成熟,只要 fail-fast 报错即可 |
| ASCII diagram 检测启发误判 | Day 4-5 | 加白名单:只对开头出现 `┌─` `┐` `└─` `┘` 字符的 pre 块加 class,其他保持 |
| 单文件 HTML 太大(> 100KB) | Day 6-7 | CSS 已经够小(~10KB),主要是内容;如果超 100KB,移除一些 fallback class |
| 后续 SKILL 想加但 placeholder 抽象不够灵活 | MVP-1 时 | 接受 placeholder 在 MVP-0 是写死的,MVP-1 重构为真正模板引擎 |
## 12. Dogfood 闭环(项目的第一个里程碑事件)
**M0 完成的标志事件**:
1. 在 `dossier/` 目录跑 `pnpm render:self`
2. 浏览器打开新生成的 `docs/specs/2026-05-17-dossier-vision-spec.html`
3. **它替换掉 2026-05-17 当天我们手写的那一份 HTML,且我们看了之后说"这个比手写的还顺"**
4. 把生成的 HTML commit 进 repo(vision spec 的 frontmatter 加 `implements: [...]` 指回这份 MVP-0 spec)
5. 在 README 里贴一张这份 HTML 的截图
**这是项目第一次"自己渲染自己"的时刻。**
## 13. 验收检查清单
完成 MVP-0 = 下列所有项打勾。
- [ ] §1.3 验收强制标准 1-10 全部通过
- [ ] `pnpm test` 全绿
- [ ] `pnpm render:self` 单次跑通,无 warning
- [ ] 输出 HTML 文件大小 < 100KB
- [ ] 关掉网络后双击输出 HTML 仍能完美显示(验证零外链)
- [ ] 把输出 HTML 通过 iMessage / 微信发给一个朋友,对方打开能阅读
- [ ] vision spec frontmatter 的 `implements: []` 加上本文档路径
- [ ] 本文档 status: draft → ready → implemented
- [ ] 写一段 `docs/changes/2026-MM-DD-dossier-mvp-0-impl-notes.md`,记录实际遇到的偏差和决策变化
- [ ] `dossier render <spec.md> --verbose` 输出 `selected skill: render-spec (frontmatter-kind)` —— 来自 §6.5.8
- [ ] `dossier render <spec.md> --skill bogus-name` 报错退出码 3,不 silent fallback —— 来自 §6.5.8
- [ ] Frontmatter 显式写 `render_skill: render-spec` 时,`--verbose` 输出 reason 为 `frontmatter-render-skill` —— 来自 §6.5.8
## 14. 不在此 spec 范围、但 MVP-1 要立刻接的工作
- 设计 admonition / directive 语法,让 user story 卡片、ladder、命名卡能从 markdown 写出而不是手工 HTML
- 引入更多 SKILL:`render-adr` / `render-change-note` / `render-review`
- Dossier-0:扫描目录、聚合多文档、生成 index.html
- 文件名公共前缀 + frontmatter implements 字段的 dossier 识别
## 15. 开放问题(不阻塞 MVP-0 启动,但要在过程中决断)
| Q | 问题 | 何时决断 |
|---|---|---|
| Q1 | h2 自动加 `§ N` 是不是 hardcoded?如果用户 markdown 里已经写了 "1. " 是不是会重复? | Day 4 视觉 review 时定 |
| Q2 | frontmatter 里的 `implements: [...]` 当前 MVP-0 不渲染关系,但要不要在 meta-grid 里至少显示为链接? | Day 5 |
| Q3 | 中文 / 英文混排时 TOC 文字截断怎么处理(短中文 vs 长英文标题)? | Day 6 dogfood 看实际效果 |
| Q4 | 错误信息走中文还是英文? | Day 1 选英文(更可移植),但允许后续 i18n |
## 16. 下一步(本 spec 通过后)
1. 切 `status: ready`
2. 在 `dossier/` 跑 `pnpm init`,开始 Day 1
3. 每完成一个 day 的 todo,在本 spec 的"§ 10 时间线"勾掉
4. M0 完成后:
- 把本 spec 切到 `status: implemented`
- vision spec 的 frontmatter 补 `implements: ["docs/specs/2026-05-18-dossier-mvp-0-spec.md"]`
- 写 MVP-1 实施 spec