# 上手页重写实施计划（「跟 Claude 怎么说」）

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 把 `playground/public/onboarding.html` 从「装什么，然后怎么用」的安装手册重写成给技术小白的「跟 Claude 怎么说」指南，并把写代码那 256 行分出到新页 `for-developers.html`。

**Architecture:** 单页三起点分岔一次（clone / plugin / 浏览器），之后合成一条三类读者共用的对话主线；dev 内容移到同目录第二个静态页，两页互相指路，都随 `pnpm build:playground` 发布、免登录可读。

**Tech Stack:** 纯静态 HTML + 内联 `<style>`/`<script>`（无框架、无构建步骤，Vite 只做 copy）· `playground-dist/tvu-tokens.css` 提供 token · vitest 做机械护栏。

**判据真源：** [`docs/superpowers/specs/2026-08-12-onboarding-page-rewrite-design.md`](../specs/2026-08-12-onboarding-page-rewrite-design.md)（下称 **spec**，已 owner 确认、commit `ccb8f7c8`）。本计划不复述 spec 的内容契约，只把它拆成可执行步骤 + 可核验判据。**内容有歧义时读 spec §5/§6/§7，不要自行发挥。**

## Global Constraints

每个 Task 的要求都隐含包含本节。

- **`.html` 命中 pre-commit 视觉闸**（`.husky/pre-commit:81` 匹配 `\.(html|css|svg|vue|jsx|tsx)$`）⇒ 提交需 `VISUAL_COMMIT_APPROVED=1`，**AI 不自设**。⇒ **Task 2/3/4 不提交 `.html`**，两页的改动累积到 **Task 7** 由 owner 看过后一次提交。
- **`onboarding.html` 在 de-mirror 闸扫描面内，mode = `no-version`**（`scripts/audit-doc-de-mirror.mjs`）⇒ **页面里不得出现任何 `vX.Y` 版本字面**，除非行内 `<!-- de-mirror-ok: 理由 -->`。
- **搜索**：可以写「文档站能搜索」，**必须同时写明范围是页面/组件**名字**过滤**；⛔ 不得写成「全文检索」，⛔ 不得复述旧现状「搜索是死的」（[[INFRA-F111]] ②a 已 ship，`4c10a788`）。
- **只出中文**（owner 2026-08-11 拍：英文等真有人要再说）。
- **规则正文不内联**（间距 / 命名 / token / 组件 API 只指路，owner 2026-08-11 内联边界）。
- **`⌨️` 块全页恰好 3 个**（spec §6a：判据是「Claude 能不能代做」，不是「是不是命令」）：装 Claude Code · clone 输凭据 · plugin 报 auth 错 prime 凭据。
- **别手工往 `playground-dist/` 塞**（`emptyOutDir: true`）；改源后跑 `pnpm build:playground`。
- **取渲染证据必须用送 `charset=utf-8` 的服务器**。本轮已有可用脚本：
  `/private/tmp/claude-501/-Users-nancy-Documents-AICoding-VS-Code-tvu-design-system/5c7ab4ec-eed4-49ec-acc3-d6d0c41ecbfb/scratchpad/serve-utf8.py`
  用法 `python3 serve-utf8.py <port> <dir>`。⛔ 不许用 `python3 -m http.server`（不送 charset → 整页按 Latin-1 解码成乱码 → 毁掉 CJK 断行 → 量出不存在的布局缺陷）。
- **提交纪律**（本仓库常态多 session 并行、共享 git index）：先 `git diff --stat -- <路径…>` 核行数，再
  `git commit -F <msg 文件> -- <路径…>`（`-F` 在 `--` 之前），**紧跟 `git reset -- <路径…>`**。
  ⚠️ pre-commit 闸链单次可超过 2 分钟 —— 用 `run_in_background` 跑 commit，别用 2 分钟前台超时。
  ⚠️ 撞上 `.git/index.lock` 是**对方正在提交**，不是故障；等一下重来，改动完好留在工作树。
- **素材来源**：`playground/public/onboarding.html` 的**工作树当前版本**（含未提交的 95 增 17 删）。
  ⛔ 不要从 `HEAD` 取 —— HEAD 那版含一句**已证伪的说法**（「可以直接复制带段落锚点的链接」，实测点 CONTENTS 只滚动、地址栏不变）。

---

## File Structure

| 文件 | 责任 | 动作 |
|---|---|---|
| `playground/public/onboarding.html` | 小白主页面：三起点安装 + 共用对话主线 | 重写正文 + script |
| `playground/public/for-developers.html` | dev 面：npm 接入 / 引用 / 报错表 / 为什么这种装法 | 新建（近逐字搬运） |
| `tests/onboarding-pages.test.ts` | 两页的机械护栏（`data-aud` 枚举 / 锚点零悬空 / LEGACY 可解析 / `⌨️` 计数 / 措辞红线 / 互指） | 新建 |
| `docs/CONSUMER_ONBOARDING.md` | 收成指针（装法真源移到站上新页） | 改写 |
| `docs/ONBOARDING_NEW_MACHINE.md`:42 · `docs/PLUGIN.md`:34 · `docs/DESIGNING_WITH_TVU.md`:44 · `scripts/setup-consumer.sh`:9 | 4 处入站引用改指向 | 各改一行 |
| `docs/STATUS.md` §一 第 0 条 | 在飞项从「`#browse` 等视觉批准」换成本重写 | 改写 |
| spec §10 | 把两个「开工前必测」改成已测结论 | Task 1 落 |

---

## Task 1: 把两个开工前必测钉成结论

**Files:**
- Modify: `docs/superpowers/specs/2026-08-12-onboarding-page-rewrite-design.md` §10（改成已测结论）+ §6e/§7a（据结论补 Figma 第 0 步）
- Modify: `docs/internal/backlog.md`（仅当 Step 4 确认是缺陷时新增 entry）

**Interfaces:**
- Consumes: 无（第一个 Task）
- Produces: 「Figma 那条要不要加第 0 步」的结论 → Task 3 的段 5 内容依赖它；
  「plugin-only 能否读到规则文档」的结论 → Task 3 的段 3b 验收话强度依赖它

- [ ] **Step 1: 测 Figma 通路来自哪一层**

```bash
claude mcp list 2>&1 | grep -iE 'figma'
claude mcp list 2>&1 | grep -c '^claude.ai '
```

判据：输出行前缀 `plugin:` = **机器级 plugin**（换台机器不会有）；前缀 `claude.ai ` = **账号级连接器**（登录即有）。
第二条命令是阴性对照，确认账号级那批确实用另一种前缀（本轮实测 ≥10 行），排除「前缀无意义」。

本轮已跑，实测：`plugin:figma:figma: https://mcp.figma.com/mcp (HTTP) - ✔ Connected` ⇒ **机器级**。
⇒ 结论：**新机器装完 Claude Code + tvu plugin 不会自动有 Figma 通路。** 执行时重跑确认一遍即可。

- [ ] **Step 2: 把结论写进 spec §10-①，并在段 5 的内容契约里补第 0 步**

spec §10-① 改成「已测（2026-08-12）：Figma 走 `plugin:figma:figma`，机器级，不随账号自动就位」，
并在 §7a 那张开场表的「Figma 设计稿」一行前加一步：**先确认接上了 Figma**（形态 = 一句自检 + 没接时去装 figma plugin）。
⚠️ 这一步同时修掉现页面的一个误导：它的 Figma 自检只区分「通」和「没权限」，
而 `Looks like you don't have edit access to this file.` **不区分**「没权限 / 文件不存在 / 根本没接 Figma」。

- [ ] **Step 3: 测 plugin-only 环境能否读到 skills 引用的规则文档**

```bash
for s in tvu-design-mockup tvu-design-code role-ux consumer-product-conventions; do
  printf '%-32s CLAUDE_PLUGIN_ROOT=%s  裸docs/internal=%s\n' "$s" \
    "$(grep -c 'CLAUDE_PLUGIN_ROOT' skills/$s/SKILL.md)" \
    "$(grep -cE '(^|[^/${])docs/internal/' skills/$s/SKILL.md)"
done
```

本轮实测：`tvu-design-mockup` / `tvu-design-code` / `role-ux` 都用 `${CLAUDE_PLUGIN_ROOT}/…` 定位
（`tvu-design-code/SKILL.md:16` 还写了变量未展开时的 fallback）⇒ plugin-only 环境可解析。
**`consumer-product-conventions` 只有裸 `docs/internal/…`** ⇒ 弱点。

- [ ] **Step 4: 判定 `consumer-product-conventions` 的裸路径是不是缺陷**

读该文件那几行的上下文：若裸路径出现在**要 AI 去读的指令位置** → 是缺陷；
若只出现在**说明性表格/散文**里（讲「真源在哪」而非「现在去读它」）→ 不是缺陷，记一句即可。

- [ ] **Step 5: 缺陷则立 backlog entry（先挑号）**

```bash
grep -oE '(INFRA|CANONICAL|BRIDGE|EXTRACT|META|PLUGIN)-[A-Z]*-?F?[0-9]+' docs/internal/backlog.md | sort -u | tail -20
```

挑**最大数字 +1**（⛔ 不许凭印象选号）。entry 必含：发现时间 · 现象（实测非读码推断）· 触发查看条件 · 阻塞关系。
⚠️ **这条不在本轮修**（skill 路径形态是另一件事，属 [[INFRA-F110]] 那条线的相邻面）——
只登记，不顺手做（scope creep 红线）。

- [ ] **Step 6: 提交**

```bash
git diff --stat -- docs/superpowers/specs/2026-08-12-onboarding-page-rewrite-design.md docs/internal/backlog.md
git commit -F <msg> -- docs/superpowers/specs/2026-08-12-onboarding-page-rewrite-design.md docs/internal/backlog.md
git reset -- docs/superpowers/specs/2026-08-12-onboarding-page-rewrite-design.md docs/internal/backlog.md
```

纯 `.md`，不需要 `VISUAL_COMMIT_APPROVED`。

---

## Task 2: 新建 `for-developers.html`，把 dev 那 256 行搬出去

**Files:**
- Create: `playground/public/for-developers.html`
- Modify: `playground/public/onboarding.html`（删 `install-npm` / `develop` / `appendix` 三段）

**Interfaces:**
- Consumes: 无
- Produces: 新页存在且含 dev 全部内容 → Task 3 的段 11 指路、Task 4 的 LEGACY 跨页映射、Task 5 的 `DEV` 断言都依赖它

- [ ] **Step 1: 用同一套外壳建新页**

从 `onboarding.html` 复制到新文件：`<meta charset>` + `<meta viewport>` + `<link rel="stylesheet" href="./tvu-tokens.css">`
+ 整个 `<style>` 块 + topbar/masthead 骨架 + `<footer>` + 回到顶部按钮 + 深浅色 toggle 那段 script。
`h1` = 「在产品里用 TVU 组件（给开发）」。**不要**复制身份筛选 chips / `?view=` 那套（新页无需分身份）。

- [ ] **Step 2: 逐字搬三段正文**

从工作树版 `onboarding.html` 搬：`install-npm`（L466–652）· `develop`（L1021–1041）· `appendix`（L1081–1128）。
⛔ **只改跨页指路措辞，不动任何技术内容** —— 那些命令 / 数值 / 报错表是实测过的
（`vue ^3.5.0` peer 下界 · `read:package` · `E401` 对照 · 图标 `manifest.json` vs `svg/` 文件名 · `data-theme` 必须设在 `<html>`）。
新页顶部加一句回指：本页只讲写代码；不写代码看「跟 Claude 怎么说」（链 `./onboarding.html`）。

- [ ] **Step 3: 从 onboarding.html 删掉这三段**

连同它们的 `<hr class="sep">` 一起删。此刻页面会暂时缺一句指路 —— Task 3 的段 11 补。

- [ ] **Step 4: 起 charset 服务器，两页都开一遍**

```bash
mkdir -p /tmp/onb2 && cp playground/public/onboarding.html playground/public/for-developers.html /tmp/onb2/ \
  && cp playground-dist/tvu-tokens.css /tmp/onb2/
python3 <serve-utf8.py 路径> 8911 /tmp/onb2 &
curl -sI http://localhost:8911/for-developers.html | grep -iE 'HTTP/|content-type'
curl -sI http://localhost:8911/tvu-tokens.css   | grep -iE 'HTTP/|content-type'
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8911/no-such.css   # 阴性对照，期望 404
```

期望：两个 200 + `charset=utf-8`；阴性对照 404。⚠️ 只测首页 200 不算 —— 必须再取一个 HTML 引用的子资源。

- [ ] **Step 5: 机械核搬运完整性**

```bash
# 新页应含 11 个可粘块 + peer 下界 + 报错表关键行
grep -c 'class="you' playground/public/for-developers.html          # 期望 11
grep -c 'peerDependencies' playground/public/for-developers.html    # 期望 ≥1
grep -c 'E401' playground/public/for-developers.html                # 期望 ≥1
# 旧页这三段 id 必须消失
grep -cE 'id="(install-npm|develop|appendix|b-token|b-verify|b-style|b-import|b-first|b-theme|b-icons|b-wc|b-errors|b-docs)"' \
  playground/public/onboarding.html                                  # 期望 0
```

- [ ] **Step 6: 不提交，累积到 Task 7**

`.html` 走视觉闸（Global Constraints 第 1 条）。本 Task 只留工作树改动 + 上面这批实测输出。

---

## Task 3: 重写 `onboarding.html` 正文（段 0–11）

**Files:**
- Modify: `playground/public/onboarding.html`（`<header class="masthead">` + 全部 `<section>`）

**Interfaces:**
- Consumes: Task 1 的两个结论（Figma 第 0 步 / 段 3b 验收话强度）· Task 2 的新页存在
- Produces: 12 个 section 的 `id` 与 `data-aud` 取值 → Task 4 的 chips/LEGACY 与 Task 5 的断言按它们写。
  **约定的 id（Task 4/5 逐字依赖）**：
  `method` · `triage` · `install-cc` · `install-clone` · `install-plugin` · `design-browser` ·
  `first-words` · `say-clearly` · `correct-it` · `accept-it` · `wrap-up` · `stuck` · `appendix-browse`
  **约定的 `data-aud` 取值**：`all` · `maintain` · `use`（⛔ 只这三个）

- [ ] **Step 1: 改报头**

`h1` → 「跟 Claude 怎么说」。副标题 → 「你不用懂命令行、不用记规则。这一页给的是可以直接粘给 Claude 的话；哪一步必须你自己动手，会单独标出来。」
`.bigsteps` 那排 ①②③④ 按新主线改（不再是「你手上有什么/该装什么/装/怎么用」）。

- [ ] **Step 2: 加两类可粘块的样式与标记**

在 `<style>` 里给现有 `.you` 增加一个变体（如 `.you.cmd`），并在块内首行加可见标记：
`💬 粘给 Claude` / `⌨️ 这行你自己敲（Claude 代不了）`。视觉上必须一眼可分（不同左边框色 + 不同标签）。
⚠️ `⌨️` 只允许 3 处（Global Constraints）。

- [ ] **Step 3: 写段 0–1（一句话 + 分诊表）**

段 0 = 现 `method` 收短（保留「先问系统有什么，再动手」+ 三条方法表 + 「别去背数字」压成一句）。
段 1 = 分诊表，**先问产出形态、再问有没有内网 Gitea**，含兜底行（spec §3a + §3b）：

```
你要 Figma 设计稿吗？
  要   → Claude Code（+ 先接上 Figma + 对目标文件有编辑权 + 内网）
  不要 → 两个工具都能出稿，按手上有什么挑，也可以都装
  两样都没有（没内网 Gitea、也没 Claude 付费账号）→ 先只看文档站
```

⛔ 不得把「不要 Figma」写成「那就用 Claude Design」（spec §3a 的红线）。

- [ ] **Step 4: 写段 2–4（三个起点）**

照 spec §6c / §6d / §6e / §6f 逐条落。必须出现的硬内容，逐项自查：

| 必须出现 | 在哪段 |
|---|---|
| Mac / Win 各怎么开终端（`Cmd+空格`→Terminal ／ 开始菜单→PowerShell） | 2 |
| 「从这里往后命令都能让 Claude 代跑」 | 2 |
| Windows 三条特例写在 `💬` 话术里（`wincred` / 别用 corepack / `autocrlf`+Git Bash） | 3a |
| 用户级 `setup-tvu-consumer` 挂载（现页面漏了这一步，spec §2f） | 3a |
| **网页登录 Gitea 不算**，必须终端跑一次 | 3a + 3b |
| 输密码屏幕不显示字符是正常的 | 3a |
| 验收用 `dist 目录在吗？里面有多少文件？`（因为 Win 上构建会静默跳过、退出码 0） | 3a |
| `:3001` 那个 marketplace 地址**原样**（⛔ 不许改 https） | 3b |
| 「跟段 3 不排斥，可以都用」 | 4 |
| Claude Design 的 4 条 ⚠️（认所有者 / 没有间距卡 / 命名不一致以描述为准 / 是快照不实时） | 4 |
| 卡片张数带「实况日期」并注明是快照 | 4 |

⛔ **删掉** Claude Design 段里「早期发 zip 包」那条。

- [ ] **Step 5: 写段 5–10（对话主线）**

照 spec §7a–§7f 逐条落，含：三块开场（按产出形态）· 5 个唤醒词 + 「完整清单问 Claude」兜底 ·
「唤醒词只在 Claude Code 里有效」· 需求 4 格模板 + 一个填好的真例子 · 4 条纠偏话 ·
验收三条 + 「它说应该没问题就等于没验」· 收工分人（`收尾` 会真的推代码、署真名）· 卡住了那块兜底话 + 「报错原样贴、别转述」。
⚠️ 唤醒词表**不链** `docs/WAKE-WORDS.md`（私有仓库，外部读者打不开）。

- [ ] **Step 6: 写段 11（附录两块）**

照 spec §7g：文档站收短版（入口 + 两个开关 + 7 组分组表 + **搜索能用但只搜名字** + 删掉段落锚点细节）
+ 一句指向 `./for-developers.html`。

- [ ] **Step 7: 机械自查**

```bash
f=playground/public/onboarding.html
grep -oE '<section id="[^"]+" data-aud="[^"]+"' $f            # 逐个核 id 与取值
grep -oE 'data-aud="[^"]*"' $f | sort -u                       # 只允许 all / maintain / use 的组合
grep -c '⌨️' $f                                                # 期望 3
grep -cE 'v[0-9]+\.[0-9]+' $f                                  # de-mirror：期望 0
grep -c '全文检索' $f                                           # 期望 0
grep -c 'for-developers.html' $f                               # 期望 ≥1
grep -c '早期有过发 zip' $f                                     # 期望 0
```

- [ ] **Step 8: 不提交，累积到 Task 7**

---

## Task 4: script —— 3 个 chips / `data-aud` 过滤 / 跨页 LEGACY

**Files:**
- Modify: `playground/public/onboarding.html`（`<div class="chips">` + 页尾 `<script>`）

**Interfaces:**
- Consumes: Task 3 约定的 13 个 id 与三个 `data-aud` 取值
- Produces: `VIEWS` / `JUMP` / `STANDFIRST` / `LEGACY` 四张表 → Task 5 的断言逐字读它们

- [ ] **Step 1: chips 收成 3 个**

```html
<button class="chip" data-view="all"      aria-pressed="true">全部</button>
<button class="chip" data-view="maintain" aria-pressed="false">我维护设计系统</button>
<button class="chip" data-view="use"      aria-pressed="false">我只用它，不维护</button>
```

同步改 script 里的 `VIEWS` 为 `{ all:'全部', maintain:'我维护设计系统', use:'我只用它，不维护' }`。

- [ ] **Step 2: 按读者隐藏的只有两段**

`install-clone` 挂 `data-aud="maintain"`，`install-plugin` 挂 `data-aud="use"`，
**其余全部 `data-aud="all"`** —— 含段 4（Claude Design）：spec §5 明写它对两类读者都可见
（维护者同样可以用它出非 Figma 稿）。

- [ ] **Step 3: 改 `JUMP` 与 `STANDFIRST`**

`JUMP` 两条：`maintain` → `install-clone`；`use` → `install-plugin`。
`STANDFIRST` 两条按身份换副标题（`maintain` / `use` 各一句），`all` 用页面原文。
⚠️ `h1` 不参与切换。

- [ ] **Step 4: 扩 `LEGACY`，含跨页跳转**

同页改名的：`install`→`install-cc` · `install-source`/`install-source-2`/`install-source-3`/`install-source-4`→`install-clone` ·
`daily`/`use`/`three`→对话主线对应段 · `browse`→`appendix-browse` · `combine`/`new-project`→最近的存活段 ·
`triage`/`method`/`lookup`/`stuck` 若 id 未变则无需加。
**搬去新页的**（`install-npm` · `develop` · `appendix` · `b-*` 一批）要**跨页跳转**：

```js
// 搬去 for-developers.html 的旧 id：整页跳过去，不是改 hash
var MOVED = {
  'install-npm': 'install-npm', 'develop': 'develop', 'appendix': 'appendix',
  'b-token': 'b-token', 'b-token-check': 'b-token-check', 'b-verify': 'b-verify',
  'b-style': 'b-style', 'b-import': 'b-import', 'b-first': 'b-first',
  'b-theme': 'b-theme', 'b-icons': 'b-icons', 'b-wc': 'b-wc',
  'b-errors': 'b-errors', 'b-docs': 'b-docs'
};
if (location.hash) {
  var id = location.hash.slice(1);
  if (MOVED[id]) { location.replace('./for-developers.html#' + MOVED[id]); return; }
  if (LEGACY[id]) id = LEGACY[id];
  var t = document.getElementById(id);
  if (t) { if (t.classList.contains('hide')) apply('all'); t.scrollIntoView(); }
}
```

⚠️ `MOVED` 的每个 value 必须是 `for-developers.html` 里**真实存在**的 id（Task 5 会断言）。

- [ ] **Step 5: 浏览器实测三条深链**

在 charset 服务器上开这三个 URL，逐个记录实际行为（⛔ 不许凭代码推断）：

| URL | 期望 |
|---|---|
| `/onboarding.html?view=use` | 只剩 `use`+`all` 的段；banner 报隐藏数；副标题换成 use 那句 |
| `/onboarding.html#install-npm` | **整页跳到** `/for-developers.html#install-npm` |
| `/onboarding.html#browse` | 落到 `appendix-browse` 那一节 |

⚠️ 锚点/滚动类断言的两个标准陷阱（pickup §5，都会把好功能判成坏的）：
① 复用同一个 page 改 hash 是**同文档导航、内联脚本不重跑** ⇒ 每个 case 用全新 page；
② `scroll-behavior: smooth` 会让采样落在动画中途 ⇒ 判据要「连续 3 次 `scrollY` 不变」。

- [ ] **Step 6: 不提交，累积到 Task 7**

---

## Task 5: 机械护栏 `tests/onboarding-pages.test.ts`

> ⚠️ **这个 Task 不在 spec 里，是加项**。理由：这两页现在**只有一条闸**覆盖（`audit:doc-de-mirror` 的
> `no-version`）、**零测试**，而本轮新增了「三个 `data-aud` 取值」「跨页 LEGACY」两套**会静默坏**的机制
> （值写错一个字母 → 那段对所有人隐身；`MOVED` 指向不存在的 id → 旧链接跳过去落空）。
> owner 若不要这层保护，删掉本 Task 即可，其余 Task 不依赖它。

**Files:**
- Create: `tests/onboarding-pages.test.ts`

**Interfaces:**
- Consumes: Task 3 的 id/`data-aud` 约定 · Task 4 的 `LEGACY`/`MOVED` 两张表 · Task 2 的新页
- Produces: `pnpm vitest run tests/onboarding-pages.test.ts` 可跑的 6 组断言

- [ ] **Step 1: 写失败测试**

```ts
// tests/onboarding-pages.test.ts
//
// 两个静态上手页的机械护栏。它们不进 vite 的组件树、没有类型检查、没有视觉回归，
// 唯一的既有闸是 audit:doc-de-mirror 的 no-version ⇒ 下面这些会静默坏的东西全靠本文件钉：
//   · data-aud 取值写错一个字母 → 那段对所有读者隐身（页面照样渲染，无报错）
//   · MOVED 指向 for-developers.html 里不存在的 id → 旧链接跳过去落空
//   · 站内 href="#x" 指向不存在的 id → 点了不动
import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

const HERE = dirname(fileURLToPath(import.meta.url))
const read = (p: string) => readFileSync(resolve(HERE, '..', p), 'utf8')
const ONB = read('playground/public/onboarding.html')
const DEV = read('playground/public/for-developers.html')

const idsOf = (src: string) => new Set([...src.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]))
const ONB_IDS = idsOf(ONB)
const DEV_IDS = idsOf(DEV)

/** 抽出 `var NAME = { … };` 里的 'key': 'value' 对 */
function objectLiteral(src: string, name: string): Record<string, string> {
  const m = new RegExp(`var\\s+${name}\\s*=\\s*\\{([\\s\\S]*?)\\};`).exec(src)
  if (!m) throw new Error(`找不到 ${name} 字面量`)
  const out: Record<string, string> = {}
  for (const pair of m[1].matchAll(/'([^']+)'\s*:\s*'([^']+)'/g)) out[pair[1]] = pair[2]
  return out
}

const VIEWS = new Set(['all', 'maintain', 'use'])

describe('data-aud 取值只允许三个（写错一个字母 = 那段对所有人隐身）', () => {
  it('每个 data-aud 的每个 token 都在枚举里', () => {
    const bad: string[] = []
    for (const m of ONB.matchAll(/data-aud="([^"]*)"/g)) {
      for (const tok of m[1].trim().split(/\s+/)) if (!VIEWS.has(tok)) bad.push(tok)
    }
    expect(bad).toEqual([])
  })
})

describe('站内锚点零悬空', () => {
  it('每个 href="#x" 都有对应 id', () => {
    const dangling = [...ONB.matchAll(/href="#([^"]+)"/g)]
      .map((m) => m[1])
      .filter((id) => id !== '' && !ONB_IDS.has(id))
    expect(dangling).toEqual([])
  })
})

describe('旧链接映射可解析（转发出去的链接不许死）', () => {
  it('LEGACY 的每个 value 都是本页真实 id', () => {
    const legacy = objectLiteral(ONB, 'LEGACY')
    expect(Object.keys(legacy).length).toBeGreaterThan(0)
    const broken = Object.entries(legacy).filter(([, v]) => !ONB_IDS.has(v))
    expect(broken).toEqual([])
  })

  it('MOVED 的每个 value 都是 for-developers.html 真实 id', () => {
    const moved = objectLiteral(ONB, 'MOVED')
    expect(Object.keys(moved)).toContain('install-npm')
    const broken = Object.entries(moved).filter(([, v]) => !DEV_IDS.has(v))
    expect(broken).toEqual([])
  })
})

describe('两类可粘块的约定', () => {
  it('⌨️（Claude 代不了的）恰好 3 处', () => {
    expect((ONB.match(/⌨️/g) ?? []).length).toBe(3)
  })
})

describe('对外措辞红线', () => {
  it('不得把文档站搜索写成全文检索（INFRA-F111 ②a 只做名字过滤）', () => {
    expect(ONB).not.toMatch(/全文检索/)
  })

  it('不得复述「搜索是死的」旧现状', () => {
    expect(ONB).not.toMatch(/搜索.{0,6}(是死的|没用|点了没反应)/)
  })
})

describe('两页互指', () => {
  it('小白页指向 dev 页，dev 页指回小白页', () => {
    expect(ONB).toContain('for-developers.html')
    expect(DEV).toContain('onboarding.html')
  })
})
```

- [ ] **Step 2: 跑测试，确认它先失败**

```bash
pnpm vitest run tests/onboarding-pages.test.ts
```

期望：在 Task 2/3/4 完成前失败（`找不到 MOVED 字面量` 或 `ENOENT for-developers.html`）。
⚠️ **若此刻全绿 = 空过**，说明断言没咬住任何东西，回去检查正则。

- [ ] **Step 3: 造一次故障，确认闸真的会红（防空过）**

临时把 `onboarding.html` 里某个 `data-aud="all"` 改成 `data-aud="alll"`，重跑 → 必须红且逐字点名 `alll`；
再把 `MOVED` 里某个 value 改成 `b-does-not-exist`，重跑 → 必须红。**两处改回**。
⚠️ 探针名必须是**全仓无命中**的字符串（别用真实 id 的变体，避免自引用污染）。

- [ ] **Step 4: 修到全绿**

```bash
pnpm vitest run tests/onboarding-pages.test.ts
```

- [ ] **Step 5: 提交测试文件（`.ts` 不走视觉闸，可单独提交）**

```bash
git diff --stat -- tests/onboarding-pages.test.ts
git commit -F <msg> -- tests/onboarding-pages.test.ts
git reset -- tests/onboarding-pages.test.ts
```

---

## Task 6: 连带文档改动

**Files:**
- Modify: `docs/CONSUMER_ONBOARDING.md`（收成指针）
- Modify: `docs/ONBOARDING_NEW_MACHINE.md`:42 · `docs/PLUGIN.md`:34 · `docs/DESIGNING_WITH_TVU.md`:44 · `scripts/setup-consumer.sh`:9
- Modify: `docs/STATUS.md` §一 第 0 条

**Interfaces:**
- Consumes: Task 3 完成后的页面（收指针前必须确认内容已被吸收）
- Produces: 无下游依赖

- [ ] **Step 1: 逐条对照，确认收缩不丢内容**

把 `CONSUMER_ONBOARDING.md` 的这些逐条在新页里找到落点，**列成对照表**（找不到的先补进页面，再收缩）：
Setup A 两行命令 · A 段 auth 故障排查（`git ls-remote` prime + 「网页登录不能替代」）·
Setup B Step 1（clone / `pnpm install` / 用户级 symlink）· Windows 开发者模式 + Git Bash ·
Step 2 文字唤醒 · 验证段（`plugin list` / `ls .claude/skills`）· 故障排查表 8 行 · 「给完全不熟终端的同事」段。
⛔ 一条都不许「收缩」成丢内容。

- [ ] **Step 2: 收成指针**

沿用 `ONBOARDING_NEW_MACHINE.md` 的既有范式（保留标题 + 一段「正文在站上那一页」+ 链接），
并写明**装法真源 = 站上的新页**。

- [ ] **Step 3: 改 4 处入站引用**

```bash
grep -n 'CONSUMER_ONBOARDING' docs/ONBOARDING_NEW_MACHINE.md docs/PLUGIN.md docs/DESIGNING_WITH_TVU.md scripts/setup-consumer.sh
```

逐处改成指向站上新页（`scripts/setup-consumer.sh:9` 是注释，同改）。

- [ ] **Step 4: 改 STATUS §一 第 0 条**

把「上手页 `#browse` 段重写 —— 等视觉批准」换成本重写的在飞状态 + 指向 spec 与本计划。
⚠️ **STATUS 顶部摘要区有 3000 B 上限**（`audit:doc-shape` S1 阻塞）—— 写前先跑：

```bash
pnpm run audit:doc-shape
```

看当前占用与余量；⚠️ 若被并行 session 整段替换过，**先核 CHANGELOG 再追加补指针**，别顶掉别人的落地结论。

- [ ] **Step 5: 跑文档闸**

```bash
pnpm run audit:stale-anchors && pnpm run audit:doc-shape && pnpm run audit:doc-de-mirror && pnpm run audit:doc-sync
```

期望：四条全 PASS。⚠️ `audit:doc-sync` 校的是 `docs/internal/doc-sync-map.json` 里「改 X 复查 Y」的依赖 ——
若它报 `CONSUMER_ONBOARDING` 有未复查的关联文件，按它说的改。

- [ ] **Step 6: 提交（纯 .md/.sh，不走视觉闸）**

---

## Task 7: 视觉验收 → 提交两页 → 重建发布 → 线上核实物

**Files:**
- Commit: `playground/public/onboarding.html` + `playground/public/for-developers.html`
- Commit: `playground-dist/`（由 `pnpm build:playground` 生成）

**Interfaces:**
- Consumes: Task 2/3/4 累积的两页改动 · Task 5 全绿 · Task 6 已落
- Produces: 线上实物

- [ ] **Step 1: 起 charset 服务器，给 owner 看**

三个 URL：`/onboarding.html` · `/onboarding.html?view=maintain` · `/onboarding.html?view=use`
+ `/for-developers.html`。⛔ 不许用 `python3 -m http.server`。

- [ ] **Step 2: 等 owner 明确批准**

⛔ **`VISUAL_COMMIT_APPROVED` AI 不自设**。owner 没说「批准」之前，本 Task 停在这一步。

- [ ] **Step 3: 提交两页**

```bash
git diff --stat -- playground/public/onboarding.html playground/public/for-developers.html
VISUAL_COMMIT_APPROVED=1 git commit -F <msg> -- \
  playground/public/onboarding.html playground/public/for-developers.html
git reset -- playground/public/onboarding.html playground/public/for-developers.html
```

⚠️ 用后台跑（pre-commit 闸链可超 2 分钟）。

- [ ] **Step 4: 重建 dist**

```bash
git status --short          # 先确认工作树只剩自己的东西（别把并行线在飞的改动编译进去）
pnpm build:playground
grep -c '跟 Claude 怎么说' playground-dist/onboarding.html    # 期望 ≥1
ls -l playground-dist/for-developers.html                      # 期望存在
```

⚠️ **别开 worktree** —— 08-11 那条「不能干净重建、要开 worktree」的结论已过期（抽屉导航 `c98253a5` 已 ship）。
⚠️ 若 `git status` 显示有别的 session 在飞的源文件改动，**停下问 owner**，别编译进自己的 commit。

- [ ] **Step 5: 提交 dist 并 push**

```bash
git diff --stat -- playground-dist/
VISUAL_COMMIT_APPROVED=1 git commit -F <msg> -- playground-dist/
git reset -- playground-dist/
git push origin master
git ls-remote origin -h refs/heads/master; git rev-parse HEAD    # 两个必须相同
```

⚠️ push 落地**以 `git ls-remote` 为准**（GitHub 侧的 `remote rejected` 常是假象，报错原文会写 `is at <目标 sha>`）。

- [ ] **Step 6: 线上人眼核实物**

开 `https://product-demo.tvustream.com/tvu-design-system/playground-dist/onboarding.html`
和 `…/for-developers.html`，确认：新 `h1` · 3 个 chips 能筛 · `?view=use` 能带参进 · 两页互指能点通。
⛔ **只看 CI 绿 / 只看 200 不算**（首页 200 ≠ 服务可用：再取一个子资源，并做一次去前缀阴性对照）。

- [ ] **Step 7: 收尾**

更新 STATUS「Last updated」+ 当日摘要（旧摘要 prepend 进 `STATUS-CHANGELOG.md`）· 写 work-log ·
把 spec 状态从「未开工」改成「已实施」并回填实测数字。
⚠️ **不写 changeset** —— 纯 `playground/` + `docs/`，不改 npm 发布产物（`AGENTS.md` §写 changeset 的「何时不写」）。

---

## Task 8: 站上给这两页加入口（owner 2026-08-12 追加）

> **为什么追加**：owner 提出「怎么嵌入到设计系统网站」后实测发现 —— `playground/docs/` 下
> **0 个**文件提到 onboarding、`playground-dist/index.html` **无**入口、`navigation.ts` 的 33 个页面里
> **没有**它。⇒ 这一页是「发布了但站上找不到」的孤页，只能靠人手发 URL。
> **owner 拍板：继续做静态页 + 在站上加入口**，不迁成站内 Vue 页（站是双语且默认英文，而本页只出中文；
> `?view=` 身份筛选与 LEGACY 旧链接映射也得重做成路由 —— 那是独立一件事）。

**Files:**
- Modify: `playground/docs/pages/OverviewPage.vue`（加入口区块）
- ⛔ **本轮不碰** `playground/docs/DocsShell.vue`（顶栏入口）—— 并行 session 正在改它
  （自适应 + 搜索，实测 173 行 dirty）。顶栏那处等他们落地后单独做，记进 ledger 待办。

**Interfaces:**
- Consumes: Task 3 定稿的 `onboarding.html`（入口文案要与页面 `h1` 一致）· Task 2 的 `for-developers.html`
- Produces: 站上到两个静态页的可点入口

- [ ] **Step 1: 先读 OverviewPage.vue，摸清它的写法**

```bash
wc -l playground/docs/pages/OverviewPage.vue
grep -nE 'i18n|locale|useI18n|t\(|text\(' playground/docs/pages/OverviewPage.vue | head
```

看它的区块用什么 class、文案走不走 i18n helper。**照它现有的形态写**，别自创一套样式。

- [ ] **Step 2: 加入口区块**

两条链接：
- 「新手上手：跟 Claude 怎么说」→ `./onboarding.html`
- 「给开发：在产品里用 TVU 组件」→ `./for-developers.html`

⚠️ **入口文案要双语**（站有 locale 开关），但**落地页只有中文** ⇒ 入口上要如实标一句「（中文）」，
别让英文界面的读者点进去才发现是中文页。

- [ ] **Step 3: 确认相对路径真能解析（实测，不许推断）**

站是 hash 路由、SPA 挂在 `/playground-dist/`，所以 `./onboarding.html` 应解析到
`/playground-dist/onboarding.html`。**必须在浏览器里真点一次确认**，不是读代码推断。

- [ ] **Step 4: 浏览器实测两条入口**

起服务器（charset 规则同 Global Constraints），从站首页点两条入口，各自确认落到正确页面、
且返回站上还能回来。记录实际 URL 变化。

- [ ] **Step 5: 不单独提交**

`.vue` 同样命中视觉闸 ⇒ 并入 **Task 7** 的视觉批准批次一起提交。

- [ ] **Step 6: 把顶栏那处记成待办**

在 ledger 写：「顶栏入口（DocsShell.vue）待并行线的自适应/搜索改动落地后单独做」，
避免这条被忘掉 —— overview 页的入口只在首页可见，顶栏才是全站可见。

---

## 自审（对着 spec 逐节核）

| spec 节 | 落在哪 |
|---|---|
| §3 两类读者 / §3a 二维表 / §3b 凭据过滤 | Task 3 Step 3（分诊表）+ Task 4 Step 2（只两段按读者藏） |
| §4-1 方案① / §4-4 两个 chips / §4-5 h1 | Task 4 Step 1 · Task 3 Step 1 |
| §4-2 dev 分页 | Task 2 |
| §4-3 browse 收短 | Task 3 Step 6 |
| §5 12 段大纲 + §5a 吸收/删除表 | Task 3 Step 3–6 · Task 2 Step 3 |
| §6a 两类块 / §6b Windows 分配 | Task 3 Step 2 · Step 4 |
| §6c–§6f 三个起点 | Task 3 Step 4（含必须出现项自查表） |
| §7a–§7g 对话主线 + 附录 | Task 3 Step 5–6 |
| §8 第二页范围 | Task 2 Step 2 |
| §9-1 CONSUMER_ONBOARDING + 4 处入站 | Task 6 Step 1–3 |
| §9-2 LEGACY 跨页 | Task 4 Step 4（+ Task 5 断言） |
| §9-3 STATUS | Task 6 Step 4 |
| §9-4 DESIGNING_WITH_TVU 措辞仍成立 | Task 6 Step 3 |
| §10 两个必测 | Task 1 |
| §11-1…8 落地约束 | Global Constraints + Task 7 |
| §12 不做 | 无 Task（YAGNI 项，本计划未出现） |

**类型/命名一致性**：13 个 section id 在 Task 3 Interfaces 定义，Task 4 Step 4 与 Task 5 的断言逐字引用同一批；
`data-aud` 三个取值在 Task 3 Interfaces、Task 4 Step 2、Task 5 的 `VIEWS` 三处一致；
`LEGACY` / `MOVED` 两个变量名在 Task 4 Step 4 定义、Task 5 `objectLiteral()` 按名读取。
