# SPEC —— skillpress 投影规范

> **一句话**：`docs/**` 是事实的家；skill 是**给 AI 用的投影** —— 只收慢变的、不可推导的、会让人踩坑的东西，其余留指针。
>
> 状态：**v0**（2026-10-04 起草；2026-10-06 补 §0 需求）。判据见 §6，工具见 `cmd/skillpress`（门与 CLI）与 `engine/site/`（站点判据），分工见 [`SKILLS.md`](SKILLS.md)。

---

## 0. 需求（这个东西是什么、给谁）

**一句话**：把**任意一堆** `SKILL.md` 印成一个**书架式站点** —— 进门先读首页那份长文，
顶栏切「文档 / SKILL」进书架（左侧树 = skill → 它的子页）；同一份内容源还能**随包分发**（skills 跟着包走）。

| # | 需求 | 落点 |
|---|---|---|
| R1 | 一份内容源 → 多个投影（站点是第二投影） | §8 |
| R2 | **首页 = `<内容根>/skillpress/WEBSITE.md`**：H1 = 站名，首个 `##` 之前 = 首屏引言，每个 `##` = 顶栏一栏。`skillpress/SKILL.md` **只讲 skillpress 本身**（不再当主页），它**自己进忽略清单**；`--home <路径>` 可覆盖；没有 `WEBSITE.md` 时回退 `SKILL.md` 并**打印**用了哪个（D19 / D20） | §8 |
| R3 | 书架 = 文档区：进门先读首页，顶栏切过去才是书架 | §8 |
| R4 | **忽略清单 `skillpress.ignore.md`**：声明哪些 skill 不上桌（见 §0.1） | 本节 |
| R5 | **门分层**：核心只留**通用**门（G1–G4 / G7 / G8）；`.mbt` API 对账、docs 的 `§` 号、docs 事实门是**内容仓自己的**门 | §6 / §6b |
| R6 | **通用产品**：别人拿一堆 skill 也能起站 ⇒ `attach` / `pack` 是**核心需求**；没有 moobile 源码时核心门**不许死**（缺什么**明说跳过**） | §6 / PLAN 的 P6 |
| R7 | 引擎迁 MoonBit（D15）；迁移**不许让 R6 退步** | PLAN 的 P8 |
| R8 | 价值观：**一个说谎的 skill 比没有 skill 更糟**；假绿是头号敌人；新判据必配诱饵；落锁 = 人复核过；**不许有静默的后门** | §6 |
| R9 | **代码体量：新引擎每个源文件 ≤ 400 行（含注释）** —— 与 skill 那边同一个数字、同一条规矩；超了**拆成「一个文件一件事」**，**不许删注释凑数**；**由门 + 诱饵保证**（一个 401 行的样本必须被点名）。Node 版**不回改**（它冻结了，见 D15/P8） | §6 |

### 0.1 忽略清单：`skillpress.ignore.md`

- **住哪**：**内容仓根**（与 `skills/` 同级）。⚠️ **不进** `skills/` 里 —— 那是个 skill 根，
  散落的 `.md` 会被 harness 当成"缺 frontmatter 的坏 skill"。
- **作用范围**：列进去的 skill **站点不上桌，门也跳过**（用户定，2026-10-06）。
- **但它必须"响"** —— 否则这就是一条**静默的后门**，正撞在 R8 上：
  1. **每条必须带理由**，没写理由**就红**（照 `DRIFT.md` §2③"豁免必须写在文档里"那条来）；
  2. 每次跑门 / 生成，**都要把"被跳过的 N 份 + 理由"打印出来**（跳过的形状必须与通过**不一样** ——
     "总数也是读数"这条老账）；
  3. **首页不许被忽略**（写了就报错，不静默）；
  4. 清单进 git ⇒ 谁在什么时候把哪一份藏起来了，看得见、可复核；
  5. **首页不许被豁免**：`skillpress/WEBSITE.md` **永远过门**，忽略清单管不到它 ——
     忽略管的是「上不上书架 + 该 skill 的门跳不跳过」，而首页是**站点的脸**（D20）。
- **粒度**：v0 只到 **skill 级**（一行一个目录名 + 理由）；子页不单独忽略。

---

## 1. skill 的物理格式（实测，不是假设）

格式由 DSH 的 skill 注册表决定。三条实测事实（参照实现：`dsh-godot-skill` 的 `lib/index.js`）：

| 事实 | 含义 |
|---|---|
| `【目录】/SKILL.md`，YAML frontmatter **只解析 `name` / `description` / `whenToUse`** | 其余字段会被**丢掉** —— 别指望自定义字段 |
| 加载时**整份正文一次性进上下文** | 体量 = token 成本，直接决定它好不好用 |
| 本机三个 skill 实测 **14 KB / 21 KB / 34 KB**（165 / 420 / 444 行） | 这就是现实档位；**超了就该拆** |

`description` 与 `whenToUse` 决定"什么时候被加载"—— 它们是**检索键**，要写清"遇到什么任务该用我"，**不要**写成内容摘要。

---

## 2. 收录判据：进 / 不进

**进（五类）**

1. **不可推导的约定** —— 代码里看不出来、猜错要付代价的（例：本地新建必须用**负 id**；`transform` 每项只能一个键）
2. **反面清单** —— "别做 X"，尤其是"**写了不报错但没效果**"的那一类
3. **确切命令** —— 可复制、可运行，并写清在哪个目录跑
4. **判据** —— "哪条命令的输出能证明这句话"
5. **指针** —— 去哪个文件的哪一节看细节

**不进（五类）**

1. **会漂的数字** —— 版本号、门的分数、项数、"已发布"状态 → 换成"去 `docs/STATUS.md` 看"
2. **动机与历史** —— 为什么这么设计、当初试过什么 → 留 `docs/`
3. **长代码** —— 超过 ~15 行的示例 → 指向 `examples/apps/**`
4. **编译器 / 类型系统会拦的东西** —— 已经会红的事不用再写一遍
5. **一眼 grep 得到的** —— 函数签名、目录清单、包列表

> 第 4 条最省 token，也最容易被忽略：**skill 的价值在"编译器不说的那些话"**。

---

## 3. 一个 skill 的**固定结构**（一个 skill = 一个主题）

参照一个成熟 skill 的实际形状（`dev-expert 2.0.3`：`SKILL.md` 15 KB ＋ `references/` 46 份 ＋
`scripts/` ＋ `hooks/` 29 个 ＋ `FAQ.md` ＋ `_meta.json`）。**关键在于纪律，不在于目录数量**：

> **主文档只放"门禁"，细则进 `references/`，并且按步按需读** —— 它的原话是
> "门禁内联；其下细则须按步 `Read`（**未读 = 该步门禁未生效**）"。

所以我们的形状是：

```
skills/<name>/
├── SKILL.md        必填。**门禁 + 指针**：读完它就够动手；每条细则给 `references/…` 的路径
├── references/*.md       可选（**推荐位置**）。深水区：长表 / 逐条清单 / 证据链。**一个主题一份，按需读**
├── scripts/*       可选。随 skill 一起发的可跑小工具（检查器、生成器）
├── FAQ.md          可选。人看的问答（站点上作为子页）
└── _meta.json      可选。发布元数据（slug / version / author / license）
```

**四条规矩**：

1. **SKILL.md 必须自足到"能动手"**：读完主文档就该知道做什么、先跑什么、哪些线不能碰。
   细则可以外置，**判据**（"哪条命令能证明这句话"）不能外置。
2. **外置必须留指针**，且写**路径**（`references/xxx.md`）—— 读的人（AI）是按路径去取的。
3. **refs 也是 skill 的一部分，受同样的门**：体量、路径存在、API 名、禁语、行尾，
   一条都不放宽（`check.mjs` 现在会一起查）。
4. **`_meta.json` 不是事实的家**：它只管打包（版本、owner）。**事实仍然只在 `docs/**`**。
5. **子页的位置不限**：`references/` 只是**推荐**位置 —— **任何子目录里的 `.md`** 都会被收成子页
   （`docs/…`、`notes/…` 都行），并且一样过门、一样进指纹。跳过的是产物与点开头目录
   （`node_modules/`、`_build/`、`dist/`、`.mooncakes/`、`.git/`…），规矩写在 `engine/content/kids.mbt`。

> 为什么值得外置：SKILL.md 是**一次性全量进上下文**的（§1），而 refs 只在需要时读。
> 把 34 道门的逐条说明塞进主文档，等于让每次加载都付那笔 token。

| 项 | 上限 |
|---|---|
| 单个 `SKILL.md` | **400 行 / 20 KB**（先到为准） |
| 单个 `references/*.md` | **400 行 / 20 KB**（同一条线；超了就再拆一份 ref） |
| 单个 `scripts/*` | 不设上限（脚本是给人跑的，不是给上下文读的） |
| 单个代码块 | ≤ 15 行 |
| 单张表 | ≤ 12 行，超了就拆两节 |

超限的处置**不是删**，是**拆 skill**：一个 skill 只解决一类问题
（"怎么写应用"与"怎么改库"不该挤在同一个 skill 里）。

---

## 4. 写法

- **密度优先**：能写成表就不写成段，一句话能说完不写两句。
- **每条硬事实尽量带证据**：命令、文件路径，或"实测"字样。没有证据的判断写"**未验证**"。
- **状态符号**：✅ 已实测 / 🟡 部分 / ❌ 试过不行（附原因）—— 与仓库既有约定一致。
- **中文**（与仓库一致）；API 名、命令、路径保持原文。
- 开头**前 5 行**内给出"这份 skill 解决什么 + 先跑什么"。

---

## 5. 与 docs 的关系（防二次粮仓）

这是本项目最容易做坏的地方。

- **一份事实一个家**：事实住在 `docs/**`、`AGENTS.md` 与各 `README.md`。skill 是**投影**，不是新家。
- skill 里若要引入**新事实**，必须**回写到家**（docs），否则下一个人会在两个地方看到两个版本。
- skill **不复制长表格**：能用一句"见 `docs/X.md` §N"就用一句。
- **快变内容一律走指针**（§2 第 1 条）。

> 反例（本仓库刚发生的活体样本）：`npm/moobile-host/README.md:240` 还写着"事件载荷未做，受控组件用不了"，
> 而 `vendor/rabbita/html/payload.mbt` 与 `CHANGELOG.md` 表明 2026-09 就落地了。
> **这就是没有门的下场。**

---

## 6. 门（`engine/gates/`，由 `cmd/skillpress` 就是**程序根**的 CLI 调起来 —— 程序是与内容仓平级的另一个仓库）

| # | 判据 | v0 状态 |
|---|---|---|
| **G1** | frontmatter 三字段齐全；`name` == 目录名；都是单行标量 | ✅ |
| **G2** | 体量上限（§3）**＋ 行尾必须 LF**（CRLF 会让 frontmatter 静默失效） | ✅ |
| **G3** | 正文反引号里的**文件路径**必须存在（按内容仓根 / **程序根** / **该 skill 自己的目录**查） | ✅ |
| **G4** | 正文里的 `tools/*` / `scripts/*` / `moon` / `node` 命令必须解析到真实文件（同样三个根） | ✅ |
| ~~**G5**~~ | `@html.` / `@style.` / `@cmd.` / `@sub.` API 名对账 —— ⚠️ **不是核心门**：它要读 moobile 的 `.mbt` 源码，别人没有。**定了搬去内容仓自己**（D18）✅ **已落地**（2026-10-06）：搬进 `moobile/tools/skillpress-gates.mjs`（`--gate g5`），逐字节对过账 |
| ~~**G6**~~ | `docs/**.md` 的 `§` 锚点对账 —— 同上：**不是核心门**（`docs/**` 是 moobile 的布局）。**搬去内容仓自己**（D18）✅ **已落地**（2026-10-06）：搬进 `moobile/tools/skillpress-gates.mjs`（`--gate g6`）|
| **G7** | **不许把"未做"写成"已支持"**：`claims.txt` 里的禁语出现即红 | 🟡 |
| **G8** | **体量/条目数变化必须人复核**：指纹（行数/字节/表格行/代码块）对不上就红，复核后 `--update-lock`。锁**一份**（跟着程序走），但每条指纹记着**它属于哪个内容根** —— 程序可以面对多个内容根（内容仓的 `skills/` 与程序自己的 `skills/` —— **同名、不同仓库**） | ✅ |

门存在的理由只有一条：**一个说谎的 skill 比没有 skill 更糟** ——
它会让 AI 自信地写错代码，而且没人会发现。

> **G3 / G4 为什么查三个根**：多数 skill 讲 moobile，路径相对**内容仓根**；
> 但内容里也有「这套工具自己在哪儿」的指针（`cmd/skillpress`、`engine/gates/`、
> `grammars/PROVENANCE.md`），它们相对**程序根** —— 而程序是**另一个仓库**（内容仓的兄弟）；
> 还有 `scripts/**` 这种相对**那个 skill 自己的目录**的（站点实例就住在里面）。
> 只认一个根会让另两边全红，而人红了之后的处置通常是**把真引用删掉** ——
> 那正是最坏的结果（判据逼人删掉正确的东西）。三个根都认，缺失才红。
>
> ⚠️ 2026-10-06 搬家时才发现：**程序根那条原先根本不存在**（`check.mjs` 里第二个根写的是仓库根，
> 与第一个重复），而 `bin`/`lib`/`grammars` 又不在 G3 的一级目录白名单里 ⇒ 那些指针
> **一个都没被查过**（是"静默不查"，不是"查了但错了"）。现在两个都补上了，
> 证据是 `--selftest` 里新加的诱饵 `bad-program-path`（`bin/没有这个门面.mjs` 必须被点名）。

### 6b. 另一道门：查事实来源（`docfacts`）

✅ **已搬去内容仓自己**（D18，2026-10-06 落地）：它查的是 **moobile 的 `docs/**`** 有没有撒谎 ——
对别人的内容仓毫无意义。今天住在 `moobile/tools/skillpress-gates.mjs`（和 G5 / G6 一个入口：
`--gate g5|g6|facts|check`），台账 `done-claims.txt` 也跟着**搬进内容仓根**了
（它的证据路径本来就全是相对 moobile 的）。程序这边当时只留**冻结的旧实现**（`lib/docfacts.mjs`，**已随 `lib/` 退役**）
与 `facts` 子命令）：身份从"现役门"变成"搬走时的对照物"——搬的时候逐字节对过账
（真仓库 facts 整份 **diff 0 行**、真内容夹具 647 行 **diff 0 行**）。

上表查的是 **skill 自己**；skill 的事实来自 `docs/**`，所以 docs 撒谎一样致命。
它只查**可计算**的那类事实（"文档写 X、代码里是 Y"、"说未做但台账记着已落地"）。
首跑在 moobile 的 19 份当前态文档上找到 **45 处硬伤 + 34 处软警告**。
方法与政策见 [`DRIFT.md`](DRIFT.md)。

---

## 7. 漂移处置

- skill 里凡是引用**具体版本/分数/项数**的句子，**必须**带"以 `docs/STATUS.md` 为准"的指针（G7 会扫）。
- 每次 moobile 抬版：跑门；`claims.txt` 要跟着 STATUS 的"未做清单"更新。
- 门红了的处置顺序：**先改文档的家，再改投影** —— 不许只改 skill 让它变绿。

---

## 8. 站点（第二条投影）的边界

站点**不是**第三个事实的家：它和 skill 从**同一份内容源**长出来。

1. **首页 = `<内容仓>/skills/skillpress/WEBSITE.md`**（D19：它**不是**一份 skill，只当站点首页那块内容）：H1 + 引言 = 首屏，
   **每个 `##` = 顶栏的一栏** —— 加一节就多一条，站点代码里不写死任何一节 ⇒ 它的 `##` 要写短名。
2. **文档区 = `<内容仓>/skills/**`**：顶栏那条「文档 / SKILL」切过去；侧栏树 = skill → 它的 `references/`、`scripts/`。
   站点上"一个 skill 一页"与 skill 的**结构**同源 ⇒ 结构变了站点跟着变，不用动渲染。
3. **改了内容源必须重跑生成器**：`gen-content.mjs --check` 是一道门（生成物与源不一致即红）。
4. **代码块在构建期上色**：tree-sitter 解析 + 语法自带的 `highlights.scm` → 片段带**色号**
   （色号的含义在 `highlight.mjs` 的 `PALETTE`，颜色在站点应用的 `app.mbt`）。
   判据是 `highlight.mjs --audit`：**召回率与漏色比例越线就红** ——
   上色坏起来是安静的（页面照样渲染，只是颜色变少），这是唯一看得见的地方。
   这一步是这台引擎里**唯一**用 npm 依赖的地方（`web-tree-sitter`）；语法 wasm 与查询都 vendor 在
   `grammars/`（出处与 sha256 见那份 `PROVENANCE.md`）。
5. **引擎住在**程序根（`interest/skillpress/`，与内容仓平级的另一个仓库），
   **站点实例住在内容仓里那份 skill 的 `scripts/` 下**（它本来就是个 moobile 应用）。
   形状与理由见 `<内容仓>/skills/skillpress/references/layout.md`。

细则（数据类型、解析边界、两种模式、判据怎么加）见 `<内容仓>/skills/skillpress/references/site-pipeline.md`。
