chore(repo): 建仓,落成协作规范与文档骨架
协作方式以 CHSAnalyzer 为蓝本,按「库」这个身份改写: - CLAUDE.md §0 的权威表换成公共 API 契约、公共类型、下游迁移三条主线, 数据库 schema / HTTP 契约 / alembic 迁移在本项目不存在,整体删去。 - §1 新增四条库特有的硬约束:字段只增不删不改名、持久化 schema 走显式版本、 不反向 import 下游、发布必须走完整流程(PolyGateway 有两个版本只 bump 没上传,registry 长期停在旧版且无人发现)。 - §3 的四类 Codex 对抗审查按同一判据重定:公共签名、停止判定与预算结算、 取消传播与 Session 隔离、schema 演进。共同点是错了不会当场炸。 - research-wiki 在常青层加第四类 migrations/,因为本项目的核心验收标准就是 能否搬回 dissect、能否替代 GovDoc-SaaS 的 docagent-core/,塞进 guides/ 会让它看起来像可选工序。 reference/ 不入库:五个仓库各带一个 .git、合计约 96MB,提交进来会变成一堆 不可用的嵌套仓库。它也不是任何事实的权威,agent-core.md 同理。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
# research-wiki 是什么,各个目录收什么
|
||||
|
||||
这里是本项目除代码之外的绝大部分文档。这一份说明它怎么组织、你要写的东西该放哪。
|
||||
|
||||
两个例外留在仓库根目录,因为它们要在克隆仓库的第一眼就被看到:`README.md`(项目概览、消费者与
|
||||
当前进度)和 `CLAUDE.md`(协作约定)。除这两份之外,新写的文档都进 `research-wiki/`。
|
||||
|
||||
先说结论,赶时间的话看完这张表就够:
|
||||
|
||||
| 目录 | 收什么 | 会不会被改写 |
|
||||
|---|---|---|
|
||||
| `explanation/` | 为什么这样设计 | 会 |
|
||||
| `migrations/` | 每个下游项目迁走什么、迁完算不算数 | 会 |
|
||||
| `reference/` | 查得到的事实:日志字段、遥测口径 | 会 |
|
||||
| `guides/` | 怎么做某件事:发布、排障、本地环境 | 会 |
|
||||
| `design/` | 动工前的方案与权衡,写完冻结 | **不会** |
|
||||
| `scratch/` | 一次性草稿,由人在每轮工作会话结束前清理 | 不适用 |
|
||||
|
||||
`reference/`(仓库根目录那个,不是本目录下的)不在表里:那是参考资料,不是本项目的文档,
|
||||
理由见 `../CLAUDE.md` §0。
|
||||
|
||||
下面解释这个划分是怎么来的,以及为什么值得遵守。
|
||||
|
||||
---
|
||||
|
||||
## 1. 两层,判据是「发现它不对了,你会改它还是留着它」
|
||||
|
||||
前五个目录分成两层,界线是这一句:
|
||||
|
||||
> **如果这份文档和事实对不上了,你会去把它改对,还是原样留着?**
|
||||
|
||||
会去改对的是**常青层**(`explanation/`、`migrations/`、`reference/`、`guides/`)。它的职责是描述
|
||||
当前的真实情况,一旦不符就是在说谎,必须修。「常青」(evergreen)是文档领域的常用说法,
|
||||
指一份需要长期保持有效的文档,与之相对的是写完就归档的一次性文档。
|
||||
|
||||
原样留着的是**记录层**(`design/`)。它记录的是某个时刻我们知道什么、决定了什么。就算后来
|
||||
证明当初判断错了也不改——因为它的价值正在于保存「当初的判断是什么」。写完就冻结。
|
||||
|
||||
**为什么判据是这一句,而不是「描述现在还是描述过去」。** 后者听起来更直观,但切不动一类
|
||||
很常见的文档:一个外部工具的固有行为(比如某个命令的输出会被缓冲),它既是现在的事实
|
||||
也是当时的事实,按时间根本分不开。而用上面这句一问就清楚了——如果那个工具升级后行为变了,
|
||||
你当然会去把文档改对,所以它属于常青层。
|
||||
|
||||
同理,一次设计决策做完的当天,它的理由既是「当时」也是「现在」,时间判据同样失效。但如果
|
||||
三个月后这个决策被推翻,你不会回去改那份 design doc,而是新写一份——所以它属于记录层。
|
||||
|
||||
### 为什么必须分开
|
||||
|
||||
因为两层的维护规则是互相排斥的,混在一起没法同时成立:
|
||||
|
||||
- 常青层的规则是「改代码时必须同步改它」。
|
||||
- 记录层的规则是「写完就冻结」。一份三个月后被人改过的决策记录,已经没法回答
|
||||
「我们当初为什么这么定」了——它变成了「我们现在觉得当初应该这么定」,而这两件事在
|
||||
排查历史问题时差别很大。
|
||||
|
||||
同一个目录挂不上这两条规则。不分开的实际后果,CHSAnalyzer 的上一版演示过:因为没人区分
|
||||
哪份该更新、哪份不该更新,结果是全都不更新。那一版的 `API.md` 落后代码 210 个提交,
|
||||
`CLAUDE.md` 落后 676 个提交,而两份文档当时都写着要保持同步。
|
||||
|
||||
## 2. 常青层:四类,各自要有更新触发点
|
||||
|
||||
前三类的划分借自 Google 的工程文档实践,以及 Diátaxis——一个把文档按「读者此刻想干什么」
|
||||
分成教程 / 操作指南 / 参考 / 说明四类的文档组织框架,读作「迪亚塔克西斯」。两者对这几类的
|
||||
切法基本一致。第四类 `migrations/` 是本项目自己加的,理由在它自己那一节。
|
||||
|
||||
本项目**故意没有「教程」那一类**。教程是带零基础的人走完一遍完整流程的入门材料,它的成本
|
||||
很高(每次流程变动都要重走一遍验证),而本项目目前的协作者都已经在项目里了,没有真正的
|
||||
新手入门场景。等真的需要时再建 `tutorial/`。
|
||||
|
||||
划分的意义在于**一份文档不要同时追求两个目标**——参考手册里插一段设计动机,查参数的人会
|
||||
被打断;设计说明里塞满字段表格,想理解全局的人会被淹没。
|
||||
|
||||
### `explanation/` —— 为什么是这样
|
||||
|
||||
回答「能讲讲 X 吗」。分层为什么这么切、某个约束为什么存在、某个坑背后的机制是什么。
|
||||
这类文档允许有观点,也应该写清楚考虑过哪些替代方案。
|
||||
|
||||
本项目最重要的一份常青文档 `explanation/architecture.md` 会在这里,它说明分层、依赖方向和
|
||||
抽象接缝。它的触发点是第 1 档(由 `pyproject.toml` 的 import-linter 契约断言,文档和代码
|
||||
对不上 `make lint` 直接失败)。
|
||||
|
||||
**这份文档会先于代码存在。** 第 ③ 阶段的正题就是把架构理清楚,而理清楚的产物只能是文档;
|
||||
如果要求文档必须落后于代码,那这个阶段就没有产物,只能并进实现阶段,变成「边写边想」。
|
||||
代价是那段时间它没有机器兜底——契约要等 `src/` 落地才写得出来。对策有两条:文档开头放一段
|
||||
醒目的状态说明,写清楚它描述的是目标而不是现状;实现时**在同一个提交里**把守着这块代码的
|
||||
那条契约加上,不是同一个 PR,是同一个提交,因为提交是能被单独回退的最小单位。
|
||||
|
||||
### `migrations/` —— 每个下游迁什么、迁完算不算数
|
||||
|
||||
一个下游项目一份。内容是删除清单(它那边哪些代码由本库继任)、组件映射(旧的哪个类对应
|
||||
新的哪个接缝)、以及验收口径(迁完之后跑什么算通过)。
|
||||
|
||||
**这一类单独设,而不是塞进 `guides/`**,因为它的读者和用途都不一样。`guides/` 的读者手上
|
||||
有一件确定的活要干,看完就照做;`migrations/` 的读者在判断「本库现在够不够用」——它同时是
|
||||
本库的验收标准和边界证据。本项目的核心验收标准就是能不能搬回 dissect、能不能替代掉
|
||||
GovDoc-SaaS 的 `docagent-core/`,把这件事藏在操作指南里会让它看起来像可选的工序。
|
||||
|
||||
触发点是第 2 档:改公共 API 时同一个提交里改它。它不设长度上限——删除清单本来就该写全,
|
||||
砍长度只会让它变得不可信。
|
||||
|
||||
**真正的验收不是这份文档,是把下游迁过来跑它的测试。** 文档只是清单和审计记录;一份写着
|
||||
「已完成迁移」而没有人真跑过下游测试的迁移文档,说明的只是我们相信自己做完了。
|
||||
|
||||
### `reference/` —— 查得到的事实
|
||||
|
||||
回答「X 的取值是什么」。日志字段契约、遥测口径。特点是读者已经知道自己要找什么,只是来
|
||||
核对,所以它要准确、完整、好检索,不需要循循善诱。
|
||||
|
||||
**有一类看起来该收在这里、但故意没有收的事实**:公共类型的字段、不变量与枚举取值。它们的
|
||||
权威是代码本身,不另写一份文档复述——那份文档不重复代码的内容太少,而它腐烂的速度和代码
|
||||
一样快。哪类事实的权威在哪,`../CLAUDE.md` §0 的表格是总索引。
|
||||
|
||||
### `guides/` —— 怎么做某件事
|
||||
|
||||
回答「我要做 X,步骤是什么」。发布流程、本地环境搭建、排障。读者手上有活要干,所以只给
|
||||
能达成目的的路径,不展开讲原理——原理放 `explanation/`,需要时链过去。
|
||||
|
||||
发布流程是这一类里最重要的一份,因为它的每一步都是欠账换来的:PolyGateway 有两个版本
|
||||
完成了版本号 bump 与 CHANGELOG 却从未上传,registry 长期停在旧版,下游 `pip install` 拿不到
|
||||
任何修复且无人发现。
|
||||
|
||||
### 每一类都必须写明更新触发点
|
||||
|
||||
**「更新触发点」指的是:什么事情发生时,这份文档一定会被改。** 这是常青文档不腐烂的唯一
|
||||
可靠机制。没有触发点的常青文档,靠的就是「大家记得更新」,而这件事在 CHSAnalyzer 上一版
|
||||
已经失败过一次。
|
||||
|
||||
触发点的强度分三档,能用强的就不用弱的:
|
||||
|
||||
1. **机器断言**(最强)。文档说的和代码不符,CI 直接失败。例如架构文档由 import-linter 契约
|
||||
断言分层,日志字段契约由结构化日志的测试断言。
|
||||
2. **同 PR 同改**(次强)。改某块代码时,改文档是同一个提交的一部分。这是 Google 的做法,
|
||||
好处是不依赖任何人事后记得。
|
||||
3. **定期复审**(最弱)。**每三个月**看一眼,并在文档头部更新「最后复审」日期。周期必须写死,
|
||||
否则读者拿到那个日期也算不出文档过没过期,这一档就等于没有。三个月这个值取自 Google 的
|
||||
做法,本身没有特别的道理,重点是它是个确定的数。只在前两档都做不到时才用这一档。
|
||||
|
||||
外部工具的固有行为(例如某个命令的缓冲方式)也属于常青层,它的触发点通常是第 2 档:
|
||||
我们依赖它的那条规则改了、或者那个工具升级后行为变了,就在同一个提交里改这份文档。
|
||||
|
||||
**写一份新的常青文档时,要在文档开头写明它属于哪一档、触发点具体是什么。** 写在开头而不是
|
||||
集中在一张索引表里,是因为索引表本身也会腐烂——它会漏掉新加的文档,而写在文档自己头上的
|
||||
东西,改这份文档的人一定会看到。
|
||||
|
||||
如果三档都想不出来,那说明这份文档不该写成常青的——要么它其实是记录层的内容,要么它不该存在。
|
||||
|
||||
### 第一节必须从一个看得见的东西起步,词表不许挡在正文前面
|
||||
|
||||
这条是 CHSAnalyzer 踩出来的,它量了自己五份 `explanation/` 文档,规律很干净:好读的三份都
|
||||
从一个具体的东西开场(一个临床问题、一张处理线的图、一个「命令跑完了没有输出」的现象),
|
||||
难读的两份都从一个抽象的定位开场(「队列入口那层代码非常薄」、整节在讲「我和另一份文档的
|
||||
分界线在哪」)。**而且和长度无关**:五百多行那份没人说难,六百行那份读不动。差别不在长短,
|
||||
在第一页给了读者什么。
|
||||
|
||||
所以定两条:
|
||||
|
||||
**一、第一节必须从一个读者已经能看见的东西起步**——一个真实的问题、一张图、一个会出错的
|
||||
场景。让读者先站稳,再开始学词。**不要用「本文和某某文档怎么分工」当第一节**:那种内容
|
||||
对已经读过别的文档、正在纠结该翻哪份的人有用,对第一次打开的人是纯负担,把它放到词表后面去。
|
||||
|
||||
**二、词表不许挡在正文前面。** 具体是两件事:正文开始前不要列一串「这些词请先去别处看」;
|
||||
词表本身如果超过十条,要在开头说清「哪几个现在就得记住、其余读到再回来查」。
|
||||
|
||||
**还要写明假设了什么背景知识**,而不只是「要先读哪几份文档」。读者读不懂的时候,得能判断是
|
||||
自己缺背景还是文档写得烂——不写清楚,他只会怪自己。
|
||||
|
||||
这条没有机器能查,靠 `../CLAUDE.md` §3 那轮「硕士生阅读」评审时专门看一眼第一节。
|
||||
**评审时要额外问一句:这份文档假设的读者是谁,项目负责人算不算在内。** CHSAnalyzer 那边
|
||||
四轮评审都是以「相关领域的硕士生」的角色做的,它们能读懂那份并发文档,而项目负责人读不懂——
|
||||
这说明评审的读者假设定窄了,而文档自己也没把这个假设写出来。
|
||||
|
||||
## 3. 记录层:`design/`
|
||||
|
||||
一份 design doc 记录一次决策:当时的处境、做了什么选择、否决了什么、代价是什么。
|
||||
|
||||
**必须在动工前写。** 不是因为流程要求,而是因为事后补写的方案文档会被已经知道的结果污染:
|
||||
你会不自觉地把当初没想清楚的地方写得很笃定,把真正纠结过的备选方案一笔带过。那样写出来的
|
||||
东西读起来像是一路顺理成章,也就失去了它唯一的用途——让后来的人看清当初在什么信息条件下
|
||||
做的判断。
|
||||
|
||||
**命名**:`NNNN-短标题.md`,四位编号递增,例如 `0001-xxx.md`。用递增编号而不是日期前缀,
|
||||
是为了让 `supersedes: 0001` 这样的引用能指向一个短而稳定的名字;日期前缀在文档之间互相引用时
|
||||
又长又难记。
|
||||
|
||||
**目录为什么叫 `design/` 而不是 `adr/`。** 机制继承自 ADR 传统(见下一段),但这里装的不只是
|
||||
架构决策——接缝取舍、公共类型的形状、验收口径的定法都放这里,而 `adr/` 这个名字会让人以为
|
||||
只收架构类的东西。`design doc` 是 Google 工程实践里的叫法,覆盖面更宽。
|
||||
|
||||
**`design/` 是中间产物,不是最终产物——这一条最容易漏,而且漏了不会有任何提示。** 后续开发
|
||||
对着的是**常青层**:写代码的人读 `explanation/`,不会为了写一行代码去翻 `design/`。所以一份
|
||||
design doc 冻结的时候,它的结论必须已经落到两处之一:
|
||||
|
||||
- **代码**(含它的测试),或者
|
||||
- **常青文档**——如果代码还轮不到写。
|
||||
|
||||
**两处都没有,这份 design doc 就是死的**:它自己说决策已接受,而权威处(`../CLAUDE.md` §0
|
||||
那张表)还写着老样子甚至写着「还没定」,于是照权威处读,这件事至今没定。**这不是「以后补」,
|
||||
是当场就已经错了**——两层直接矛盾,而 §0 明令冲突时禁止自行调和。
|
||||
|
||||
**代码还没到写的时候,常青文档照样能写。** 常青层的职责是描述当前的真实情况,而「这个机制的
|
||||
形状已经定了、代码还不存在」本身就是一种真实情况——写清楚形状,再写明它还没有代码、缺口在哪,
|
||||
就够了。
|
||||
|
||||
**冻结与取代**:写完不改。决策变了就新写一份,在新文档开头标 `supersedes: 0001`,旧的原样留着。
|
||||
这是从 ADR(Architecture Decision Record,架构决策记录,一种把每次架构决策单独存成一份不可修改
|
||||
文件的做法)里保留下来的唯一一条机制。成本很低,但记录层的价值全靠它——只有旧文档还在,
|
||||
你才能看出决策是怎么演变的。
|
||||
|
||||
**和 `explanation/architecture.md` 的分工**(这两份最容易搞混):改一次架构,两份都要动,但写的
|
||||
东西不同。design doc 写「我们当时面对什么问题、比较了哪几个方案、为什么选了这个、放弃了什么」,
|
||||
写完冻结。`architecture.md` 写「现在的分层长什么样、每层的职责和依赖方向是什么」,它永远只描述
|
||||
当前状态,上一版的样子不在里面。简单说:想知道**为什么变成今天这样**去翻 `design/`,想知道
|
||||
**今天到底是什么样**去读 `architecture.md`。
|
||||
|
||||
**什么算需要写 design doc**:改公共类型的字段、改 Protocol 签名、改主循环的停止语义、改分层与
|
||||
接缝,以及任何「选错了要花很大代价才能改回来」的决定。判断标准是**能不能写出「否决的方案」**——
|
||||
如果这件事只有一种做法,那它不是决策,不用写。
|
||||
|
||||
**只有事实、没有决策的东西不属于这里。** 比如「conda run 的输出会被缓冲两层」,它不是我们选的,
|
||||
是 conda 本来就这样。这类内容属于 `explanation/`,因为它描述的是一个不会变的机制,而不是我们
|
||||
某个时刻的选择。
|
||||
|
||||
**`reference/agent-core.md` 不属于这里,也不属于任何一层。** 它是别人写的架构提案,不是我们的
|
||||
决策记录——放进 `design/` 会让后来的人以为它被接受了。它留在仓库根目录的 `reference/` 里,
|
||||
和五个参考仓库同级,理由见 `../CLAUDE.md` §0。
|
||||
|
||||
## 4. `scratch/` —— 一次性文档
|
||||
|
||||
计划、草稿、调研笔记。进 git(方便协作时互相看见),但**由项目负责人在每轮工作会话结束前
|
||||
手动清理**。
|
||||
|
||||
两点说明,因为这条规则完全靠人执行,含糊就等于没有:
|
||||
|
||||
- **「每轮工作会话」**指一次连续的开发工作从开始到告一段落,通常就是和 AI 协作者的一次对话。
|
||||
不是按 PR 算,也不是按天算。
|
||||
- **清理的是人,不是 AI。** AI 协作者不要自动删 `scratch/` 下的任何东西——哪份草稿已经没用了、
|
||||
哪份还要接着写,只有正在做这件事的人知道。
|
||||
|
||||
这里必须把风险讲明白:**这条规则没有任何机器兜底。** CHSAnalyzer 上一版的 `plans/` 目录攒到
|
||||
49,222 行、2,757 个复选框其中 87% 永远没有被勾上,靠的也是同一句「记得定期清理」。之所以还是
|
||||
这么定,是因为自动删除别人正在用的草稿风险更大。选择这条路就是接受了这个风险。
|
||||
|
||||
## 5. 复述规则:论证可以复述,参数不许复述
|
||||
|
||||
文档写作里有两个常被混为一谈的目标,本项目明确区分它们:
|
||||
|
||||
- **DRY(每个事实只写一次)**——这是**代码**的原则。用在文档上会伤害可读性,因为人类读者
|
||||
希望在一份文档里把事情读懂,而不是在几份文档之间反复跳转。
|
||||
- **权威来源(每个事实有唯一权威版本)**——这是**文档**的原则。它规定的是冲突时信谁,
|
||||
**并不禁止复述**。
|
||||
|
||||
Google 在他们的工程实践里对这一点说得很直接:「这常常导致一些信息的重复,但这种重复是有目的的:
|
||||
为了清晰。」
|
||||
|
||||
所以本项目的规则是:
|
||||
|
||||
> **论证可以复述,参数不许复述。**
|
||||
|
||||
区别在于会不会漂移。同一个道理在两处各讲一遍,两处不会互相矛盾——最多其中一处写得不如另一处好,
|
||||
而读者少跳转一次是实打实的收益。但同一个数字写在两处,迟早有一处被改、另一处没改。
|
||||
|
||||
「参数」指的是**当前生效的取值**:阈值数字、路径、文件名、类型名、枚举取值、命令行的具体形状。
|
||||
这些只在权威来源里出现一次,别处引用它。哪些事实的权威来源在哪,见 `../CLAUDE.md` §0 的表格。
|
||||
|
||||
**历史数字不算参数**,不受这条约束。比如「CHSAnalyzer 上一版的 `API.md` 落后代码 210 个提交」,
|
||||
这个 210 是已经发生的事实,不会再变,也就不会漂移;它是论证的一部分,哪里需要就可以在哪里写。
|
||||
会漂移的只有「当前生效」的那类值——因为它们将来会被改。
|
||||
|
||||
**代价是:** 允许复述论证,就等于接受了一种新的漂移——两处的**道理**打架。比如一处写「为了
|
||||
可复现所以整份冻结」,另一处写「为了省内存所以只留引用」,两处都没有数字错误,但结论已经
|
||||
矛盾了。这类冲突机器查不出来,只能靠 `../CLAUDE.md` §3 那轮独立评审。这是接受重复必须付的账。
|
||||
|
||||
## 6. 文档质量为什么不进 CI
|
||||
|
||||
CHSAnalyzer 曾经有过一个 `check_doc_consistency.py`,用正则断言文档之间的一致性,已经删除。
|
||||
本项目不再造一个。
|
||||
|
||||
原因是它能查的(文件行数、目录树标注对不对)用一条 shell 命令就能看,而它查不了的
|
||||
(这段话读不读得懂、这个决策有没有写理由)才是文档真正的腐烂形态。
|
||||
|
||||
这不是工程没做到位,而是原理上做不到。Google 在同一件事上的自我评价是:「测试可以自动化,
|
||||
而文档自动化的方案往往是缺失的」,以及「文档必然是主观的;文档的质量不由作者衡量,而由读者
|
||||
衡量,而且往往是异步地衡量」。质量的度量发生在读者脑子里,而且延迟发生,CI 拿不到这个信号。
|
||||
|
||||
所以文档质量走 `../CLAUDE.md` §3 的独立评审:一个没参与过讨论的 subagent,只拿到改后的文档,
|
||||
纯靠文档读懂。先问它读的时候发生了什么,再问三类具体问题——哪句话读不懂、哪个决策只写了
|
||||
结论没写理由、同一个参数在两处取值不同。
|
||||
Reference in New Issue
Block a user