write-project-docs · git:20260920.dac80a7 · 2026-09-20 · sha256 8de42abc9c0e4c35

write-project-docs git:20260920.dac80a7A

Immutable. This exact content never changes and is served at /api/v1/blob/8de42abc9c0e4c35.

---
name: write-project-docs
description: 按 NixKits 风格为任意项目编写或重新生成完整的多语言文档 — 简洁、表驱动、含语言切换器标题和模块分类 README。覆盖 README、各模块文档及技能文档;含「主仓薄封装 + 子仓完整文档」的迁出组件文档分工与子仓引用关系的显式记录。
---

# 项目文档编写(NixKits 风格)

为任意项目生成完整的多语言文档体系。

**主流程在本文档**;配套模板集按需查阅:

| 文件 | 内容 | 何时读 |
|---|---|---|
| **`SKILL.md`**(本文) | 编写规则、双语切换器、自动发现契约、技能文档同步规则 | 始终 |
| [`templates.md`](templates.md) | 完整模板集:目录结构、模块文档模板、README 模板、维护日志模板 | 第一次搭建某类文档时 |

## 何时使用

- 用户要求"编写文档"或"生成文档"
- 用户希望采用 NixKits 风格(简洁、多语言、表驱动)
- 用户有含模块/包/服务的现有项目需要文档化

## 架构:主仓薄封装 + 子仓完整文档

当主仓中的某个组件**迁出为独立仓库**时,采用这一分工——它避免同一份文档维护两遍:

| 位置 | 承载内容 |
|------|---------|
| **子仓**(独立仓库) | **完整文档**(全部语言)+ 源码。子仓是该项目的事实来源 |
| **主仓** | **短页**:说明已迁出、给出子仓链接、**只保留主仓独有的一节**(通常是声明式安装/集成方式),并链向子仓完整文档 |

### 判据:什么组件应当迁出

| 信号 | 说明 |
|------|------|
| **定位与主仓不符** | 主仓是 Nix flake 合集,而该组件是平台无关的通用软件 |
| **有独立的发布渠道** | 需发布到 npm / PyPI / crates.io 等主仓之外的 registry |
| **受众不同** | 用户群与主仓用户群不重叠(如非 NixOS 用户) |
| **代码层零耦合** | 不 import 主仓内其他包、模块或路径 |

### 短页写法

主仓短页**不得复制**子仓的完整内容(会漂移)。标准结构:

```markdown
# <项目名>

<语言切换器>

<一句话定位>

> **本项目已迁出为独立仓库**:<子仓 URL>
>
> **完整文档(多语)在新仓库内**:<子仓文档 URL>
>
> 本页只保留<主仓名>独有的一节——**<独有节名>**。

## 基本信息
(含「源码仓库」「主仓侧角色」等行)

## 安装
### 方式 A:<子仓原生方式>(如 dsh plugin add / npm install)
### 方式 B:<主仓独有方式>(如声明式)

## <主仓独有的维护说明>
```

要点:

- 基本信息表注明**主仓侧角色**(如「薄封装包」),读者才知两仓关系
- 安装节保留**两种方式**并标注**择一使用**(若二者注册同一标识,同时启用会重复)
- 保留主仓独有的更新说明(如薄封装固定的 `rev`/hash 需同步更新)
- 结尾给「功能概览」概要 + 子仓链接,**不复述完整功能文档**

### 主仓侧的其余同步

| 位置 | 处理 |
|------|------|
| `README` 展示表(全语言) | 该行标注「已迁出为[独立仓库](URL),本仓保留薄封装」 |
| 包定义 | 改为从子仓拉源码(`fetchFromGitHub` 固定 rev + hash),不再自带源码 |
| CI workflow | **通常保留**——它构建的是主仓 flake 输出(现为薄封装),可让用户继续命中二进制缓存 |
| 语言切换器 | 短页仍参与主仓的切换器体系,路径按主仓布局写 |

### 子仓引用关系必须显式记录

只要主仓**引用了同账户子仓**(不论是否「已迁出」,薄封装、flake input、
submodule 都算),文档里必须能找到这条引用关系——否则维护者会在主仓里
找不到源码,或重复实现子仓已有的东西。

| 位置 | 必须写明的信息 |
|------|---------------|
| 主仓短页「基本信息」 | **源码仓库** URL + **主仓侧角色**(薄封装 / input / submodule)+ 主仓钉住的 `rev` 或版本 |
| 子仓 README | 该项目的**完整文档**;若有下游引用方,可注明「被 <主仓> 以薄封装引用」 |
| 主仓完整文档(若该组件未迁出,仅被引用) | 说明引用方式与**同步方式**(如何升级钉住的坐标) |

**同步说明不可省**:读者需要知道「子仓发新版后,主仓这边要做什么」。
薄封装的答案通常是「改 `rev` + 重算两个 hash」,写清哪两个。

> 与维护日志的分工:文档说明**引用关系与同步方法**(长期有效的结构信息),
> 维护日志记录**每次坐标变更**(时间线上的事件)。两者都要有,内容不重复。

## 自动发现契约

1. **扫描** — 仓库根目录 `skills/translate-*/` 下查找所有翻译技能
   (**仓库未采用该约定时**——如根本没有该目录、或技能已安装到助手目录而
   当前 cwd 不是技能源仓库——改为读取仓库现有的语言清单文件或目录结构)
2. **读取** — 解析 frontmatter 中的 `language_code`、`display_name`、`base_language`
3. **注册** — 将发现的语言扩展纳入目录结构、语言切换器、列名映射等生成逻辑

## 编写规则

- **零废话** — 没有引导性修辞,没有"欢迎来到"
- **表格优先于段落** — 用 `基本信息` 表格展示元数据
- **代码块完整可运行** — 可直接复制粘贴
- **技术术语保持英文** — 即使在中文/日文文档中
- **语言切换器标签** — 统一用各语言 `display_name`(语言自称),自身不加链接
- **中文标题 2 或 4 字词** — 优先简洁对称词汇(如 `组件`、`基本信息`)
- **不生成目录** — 文档足够短,直接扫读
- **目标行数 ~40-60 行** — 补丁/模块文档以 `rcc-fix.md`(39 行)为标杆
- **标准节结构** — `基本信息` → `修正内容`/`功能` → `安装` → `注意`
- **禁止反模式**:

| 反模式 | 正确做法 |
|--------|----------|
| 硬编码语言列表 | 自动发现 `translate-*/` |
| 硬编码文档路径(如 `docs/zh/`) | 根据语言代码动态生成 |
| 链接修复只改路径不改显示文本 | 路径和显示文本同步修正 |
| README 只更新主语言 | 同步所有已发现的本地化版本 |
| 源文件修改后不跟新文档 | 每次源变更后立即校对相关文档 |
| 在已有节之前插入内容时未检查是否已存在 | 逐行 `grep` 确认目标文本不重复后再写入 |
| 已迁出组件在主仓保留完整文档副本 | 主仓改短页 + 子仓链接(见「主仓薄封装 + 子仓完整文档」) |
| 引用了子仓但不写引用关系 | 主仓短页写明源码仓库 + 主仓侧角色 + 钉住的坐标 + 同步方式 |
| 主仓维护日志复述子仓变更详情 | 主仓只记坐标变更 + 链接到子仓条目;详情留在子仓 |

## 工作流程

### 第 1 步:评估项目

读取所有可用的模块/包/服务元数据,对每个模块提取名称、描述、类型、依赖、安装命令、使用示例。

### 第 2 步:模块分类

按功能类别分组。对于 Nix flake 项目:

| 分类 | 发现方式 |
|------|---------|
| 软件 | `packages/*.nix` 中的 `callPackage` |
| 模块 | `modules/*.nix` 中的 NixOS 模块定义 |
| 覆盖层 | `overlays/*.nix` 中的 overlay 函数 |
| 技能(**仅当仓库发布 AI 代理技能时**) | `skills/*/SKILL.md` 中的 frontmatter |
| 开发 | `devShells` 在 `flake.nix` 中 |

### 第 3 步:生成 README

主语言 README 放根目录(无后缀),本地化版本放 `docs/README.<code>.md`。

### 第 4 步:生成模块文档

`docs/<lang>/<module>.md`,遵循四段式结构,每组自动包含对应分类标题。

### 第 5 步:生成技能文档

`docs/<lang>/skills/<skill>.md`,每技能包含基本信息 + 功能 + 使用。

### 第 6 步:语言切换器

按 `zh → en → ja → 扩展语言(自动发现顺序)` 生成。从源文件提取路径,用脚本逐个精确构造(**禁止 sed 模式匹配**)。

**⚠️ 自身语言的标签必须为纯文本,不加链接。** 其他语言才加 `[]()` 链接。错误示例:`[中文](home.md)`(自身可点击)→ 正确:`中文`(纯文本,不可点击)。

### 第 7 步:最终检查

- 无死链接
- 各模板满员(无缺失语言版本)
- 中文 2/4 字标题一致
- 引用块格式统一

### 第 8 步:语言切换器双向同步(⚠️ 易遗漏)

新增语言变体后,**必须双向更新**所有语言切换器:

| 方向 | 操作 |
|------|------|
| 新文件 → 自身 | 创建时已含完整切换器 |
| 现有文件 → 新语言 | ⚠️ **易遗漏!** 需追加新语言链接到所有现有文件的切换器行 |

**实施方法**:用 `sed` 匹配切换器行(以 `[中文]`/`[English]`/`[日本語]` 开头),追加新链接。**禁止用行号定位**(`sed '2s'` 可能命中空行而非切换器)。

```bash
# 正确:模式匹配切换器行
sed -i "/^\[中文\]\|^\[English\]\|^\[日本語\]/s|$| \| [新标签](path/file.md)|" docs/*/xxx.md

# 错误:硬编码行号(切换器不一定在第 2 行)
sed -i "2s|$| ...|" docs/*/xxx.md
```

**子代理陷阱**:子代理生成新语言文件时,容易只输出 **3 条目切换器**(仅含源语言),缺少其他已安装的扩展语言。父代理接收子代理输出后,必须用以下脚本验证补全:

```bash
# 验证所有语言目录下切换器完整性
# 语言集动态发现:skills/translate-*/ 的 display_name(勿写死 —— 这是本技能自己的反模式)
EXTRA=$(for s in skills/translate-*/SKILL.md; do
          [ -f "$s" ] && sed -n '/^display_name:/s/.*: *//p' "$s"
        done)
# 已知语言显示名(基准语言 + 固定三语 + 动态发现的扩展语言)
NAMES="中文 English 日本語 $EXTRA"
EXPECTED=$(printf '%s\n' $NAMES | grep -c .)

# 切换器行的精确特征:行首即某个「语言显示名 + |」——用它锚定,避免命中代码/表格
ANCHOR=$(printf '%s\n' $NAMES | paste -sd'|')
for f in $(grep -rl --include='*.md' -E "^(\[?($ANCHOR)\]?) \|" docs MAINTENANCE.md 2>/dev/null); do
  line=$(grep -m1 -E "^(\[?($ANCHOR)\]?) \|" "$f")
  s=0
  for name in $NAMES; do
    # 宽松匹配:纯文本(基准语言自身)与 [名](链接) 都算
    printf '%s' "$line" | grep -qF -- "$name" && s=$((s+1))
  done
  [ "$s" -lt "$EXPECTED" ] && echo "INCOMPLETE ($s/$EXPECTED): $f"
done
echo "切换器检查完成"
```

> ⚠️ **四个易错点**(前三个是实测踩到的):
>
> 1. **`grep '^\[中文\]'` 匹配的不是字面量**:BRE 里 `[中文]` 是**字符类**
>    (匹配单个「中」或「文」)。要字面量必须 `grep -F`。
> 2. **别把基语言名写死**:基语言由仓库自定,硬写「中文」后在 `en` 为基准的
>    仓库里永远匹配不到,检查会**静默通过**。
> 3. **必须用"行首是已知语言名"锚定**,不能只找 ` | `:否则会命中 **shell
>    管道**(`cat x | grep y`)与 **Markdown 表格行**,报出一堆假阳性;而
>    `grep -m1` 取到的还会是**出错那一行**,让人误以为切换器坏了。
> 4. **基语言自身在切换器里是纯文本**(如 `偽中国語`,无方括号),故匹配要
>    宽松——只找 `[名]` 会把基准语言的文档判成缺条目。
>
> **更可靠的做法**:若仓库已有文档检查脚本(如本仓的
> `develop/check-doc-links.py` 会同时校验链接可达性与切换器完整性),
> **直接复用并将语言集改成动态发现**,比在文档里维护一份临时脚本更稳。

### 第 8.1 步:根目录与跨层级文件检查(⚠️ 三阶遗漏)

语言切换器更新的常见遗漏呈**三阶递进**模式,每轮修复只覆盖到更深一层:

| 阶 | 遗漏范围 | 原因 |
|----|---------|------|
| 1 | 子代理生成的新文件自身 | 只生成 3 条目,未含扩展语言 |
| 3 | **根目录 + `docs/*.xx.md` 模式** | sed glob `docs/zh/*.md` 不匹配根级文件 |

**为什么顶层文档被系统性忽略——两种文件组织模式的结构性冲突:**

项目文档存在两种互不重叠的组织模式:

| 模式 | 路径格式 | 示例 | 子代理扫描方式 |
|------|---------|------|---------------|
| 模块文档(多数) | `docs/<语言>/<模块>.md` | `docs/en/home.md` | `ls docs/en/` 可发现 |
| 顶层文档(少数) | `docs/<文件名>.<语言>.md` 或根级 | `docs/README.en.md`、`README.md` | `ls docs/en/` **不可发现** |

子代理被分派任务「转换 `docs/en/` 下所有文件」时,`ls docs/en/` 只返回模块文档(如 `home.md`),**完全不可见** `docs/README.en.md` 和 `README.md`——因为这些文件不在 `docs/en/` 子目录中。


**结论:基于目录遍历的文件发现逻辑,对「后缀式」组织的顶层文档存在盲区。**

**第三阶遗漏文件清单**(极易漏检):

```
README.md                          # 项目根
MAINTENANCE.md                     # 项目根
NOTICE.md                          # 项目根
<子目录>/README.md                  # 子目录根(示例仓库用 kits/,按实际替换)
docs/README.en.md                  # docs/ 根级(非 docs/en/ 内)
docs/README.ja.md
docs/MAINTENANCE.en.md
docs/MAINTENANCE.ja.md
docs/NOTICE.en.md
docs/NOTICE.ja.md
```

> ⚠️ `<子目录>/README.md` 是**这类遗漏的典型代表**:任何"有自己 README 的
> 子目录"都属此列(示例仓库是 `kits/`,你的仓库可能是 `lib/`、`src/` 等)。
> 用 `find . -maxdepth 2 -name README.md` 按实际情况列出,**不要照抄示例路径**。

**验证命令(覆盖全部层级)**:

```bash
# 全项目切换器完整性检查(不遗漏根目录与子目录根)
# 先动态列出所有含切换器的文件,再逐个核对条目数(复用上文「切换器检查」脚本的逻辑)
for f in $(grep -rl --include='*.md' -E '^(\[?(中文|English|日本語)\]?) \|' . 2>/dev/null); do
  l=$(grep -m1 -E '^(\[?(中文|English|日本語)\]?) \|' "$f")
  [ -z "$l" ] && continue
  echo "$f: $l"      # 逐条核对:条目数应等于「基准语言 + 已发现扩展语言」
done
```

**⚠️ 先建文件再加链接**:向现有文件的切换器追加新语言链接前,必须确保目标文件已存在。否则产生死链(HTTP 404)。新增语言的顶层文档(README / MAINTENANCE / NOTICE)极易在此环节遗漏。

**正确的本地化分派方式——两种模式并行覆盖:**

向子代理分派翻译任务时,不能只给 `ls docs/en/` 的目录清单。必须同时指定两种模式的文件:

```markdown
## SCOPE (Katalish 示例)
Convert TWO sets of files:
1. Module docs: all .md files in ~/project/docs/en/
2. Top-level docs:
   - ~/project/docs/README.en.md
   - ~/project/docs/MAINTENANCE.en.md
   - ~/project/docs/NOTICE.en.md
   (Also check for any other *.en.md files at docs/ root level)
```

父代理接收子代理输出后,必须执行全文件覆盖验证:
```bash
# 源文件 × 目标文件 一一对应检查
for src in docs/en/*.md docs/README.en.md docs/MAINTENANCE.en.md docs/NOTICE.en.md; do
  name=$(basename "$src" | sed 's/\.en\.md$/.md/')
done
```

### 第 9 步:README 展示表同步

新增磁贴/模块后,**所有语言版本**的 README 展示表都需要更新。检查清单:
- [ ] zh/README.md 展示区
- [ ] en/README.md (docs/README.en.md) 展示区
- [ ] ja/README.md (docs/README.ja.md) 展示区
- [ ] 每个新磁贴有对应的三语文档

**README 节结构变更**:新增或移除 README 中的整节(如「规范」章节),必须同步所有语言版本。仅更新主语言会导致其他语言残留死节。

### 第 10 步:MAINTENANCE.md 去重

维护记录的日期节之间若出现摘要区块(表格式),应合并至时间线列表,避免信息重复。
### 第 11 步:本地化后全量验证

所有语言变体生成完毕后,执行以下一次性全量检查:

**切换器完整性** — 每个文档的切换器必须包含全部已发现语言(含扩展),自身标签为纯文本。

**死链扫描** — 解析每个文档中所有相对链接,确认目标文件存在。易遗漏:
- 顶层文档(README/MAINTENANCE/NOTICE)的 body 链接需跨深度调整
- 模块文档表格中的链接必须指向同语言版本而非源语言


**结构一致性** — 所有语言版本的 README 应有相同的节结构(新增/移除节时所有语言同步)。