本层判断标准

把仓库交给一个陌生人,他能不能只凭仓库完成启动和一次验收?

答「否」,他做不到的那一步就是缺失的那一份前置——多半是账号来源、服务地址,或无依据的那句原文。要补的是这一句,不是整篇重写。

README 模板

README 只回答一件事:怎样把它跑起来。不写验收、不写目录树、不写字段表——这三样各有归属,抄过来就多出三处会过期的地方。启动方式变了,只改这一份。

README.md

命令清单与 Makefile 里的目标一一对应;端口与变量名与 .env.example 一致。把字数控制在 80 行以内。

# 笔记服务

一句话说明这个服务做什么、给谁用。

## 环境要求

- Go 1.22 或更高(以 go.mod 为准)
- Docker 与 Docker Compose

## 启动

1. cp .env.example .env
2. 生成会话密钥并填进 .env:openssl rand -base64 32
3. docker compose up --build -d

端口默认 8080,改 .env 里的 WEB_PORT。
打开 http://localhost:8080/healthz 应返回 200。

## 常用命令

- make check              本地与持续集成跑同一条检查
- make fmt                排版,会改文件
- make layout             顶层目录白名单检查
- make migrations         迁移文件命名与成对检查
- make migrate-roundtrip  迁移上、全部下、再上(只对测试库跑)

## 文档在哪

- 决议:docs/decisions/
- 验收:docs/notes-check.md
- 接口:docs/api-notes.md

目录结构不在这里重复,见 docs/decisions/0035-layout.md。

可以省的两样:「环境要求」里的版本下限(go.mod 里已写着),以及架构图与性能指标——后两样没有真实数据时写出来只会过期。

验收文档示例

每一条验收都写成三部分:前置、操作、可判断的期望。期望写成状态码、某句原文、某个日志字段——「体验良好」「基本可用」不能作为通过条件,因为它们无法被两个人独立判成同一个结果。

docs/notes-check.md

读者是不认识这个项目的人,他只使用仓库和本篇,不依赖口头补充。状态码与日志字段与接口、日志两页保持一致。

# 笔记读取验收

读者:核对接口和页面的人。只使用仓库和本篇,不依赖口头补充。

前置:
- 已按 README 启动。
- 所属范围 A、B 各有一个账号,各有一条标题不同的笔记。
- 无依据的页面原句以本篇「没有依据」一节为准。

## 本范围可以读到

1. 用所属范围 A 的账号登录。
2. 打开该范围的一条笔记。
3. 期望:状态码 200,响应或页面中有这条标题。

## 其他范围读不到

1. 保持所属范围 A 的登录。
2. 请求一条已知属于所属范围 B 的 id。
3. 期望:状态码 404,正文与「id 不存在」相同,不出现所属范围 B 的标题。
4. 日志中有 cross-scope access denied,并带有两个不同的 scope_id。

## 没有依据

1. 用一段材料中不存在的问题做检索。
2. 期望:结果为无依据的原句,不出现材料里没有的讲解,
   本轮不调用对话模型。

## 留档

命令、退出码、端口、字符集记在本节下方,与本次验收的日期一起提交。

界面验收怎么写期望:把期望写成页面上应出现的原句,比写成截图更可靠——截图会随样式改变而过期,原句不会。需要核对布局时,把截图放在改动差异里,不进 docs/。

助手交出来的样子

## 验收标准
- 登录体验良好
- 数据安全
- 性能满足要求

认它的办法:把仓库交给一个陌生人,他能不能只凭仓库完成启动和一次验收。三条都判不了——没有状态码、没有页面上该出现的原句,两个评审会给出相反结论。助手写验收很自然,因为「体验良好」这种写法在方案讨论里是合格的;验收文档要的恰恰相反:一段能逐字比对的原句。

文档与代码同步:谁在什么时候更新

「文档要跟着代码更新」不构成机制,因为它没说清改了什么必须更新哪份、谁检查。下面这张表的每一行都能用一条命令核对。

代码改动触及必须同一次更新的文档谁检查
.env.exampleREADME.md 的启动步骤与环境要求脚本(自动)
migrations/验收文档里的相关前置与期望;docs/decisions/ 里数据决议的字段清单脚本 + 评审人
检查阈值、工作流、规则集docs/decisions/ 里对应决议的「怎么查」一节脚本 + CODEOWNERS 指定的必审人
scripts/check-layout.sh 的白名单docs/decisions/0035-layout.md脚本
internal/http/ 或路由注册docs/api-<资源>.md 的四句话脚本 + 契约测试
发布步骤、回滚点、版本号规则docs/release.md 与本次发布说明发布前由发布人按清单核对,见发布
界面上的「无依据」原句docs/notes-check.md 与界面核对用例评审人
Makefile 的命令入口README.md 的常用命令清单;AGENTS.md 里提到的命令评审人

scripts/check-doc-sync.sh

前五行自动检查。它在合并请求的检出目录里运行,BASE 默认 main。规则写成「触及左边就必须出现右边」,所以要放宽时改的是这个文件,评审会看到。

#!/bin/sh
# 文档同步检查:改动触及左边的路径时,右边必须出现在同一次改动里。
changed=$(git diff --name-only "origin/${BASE:-main}...HEAD")
status=0

check() {
    if echo "$changed" | grep -qE "$1"; then
        if ! echo "$changed" | grep -qE "$2"; then
            echo "文档未同步:改动命中 /$1/,但 /$2/ 未更新"
            status=1
        fi
    fi
}

check '^\.env\.example$'                                  '^README\.md$'
check '^migrations/'                                      '^(docs/|internal/.*_test\.go)'
check '^(pyproject\.toml|\.golangci\.yml|\.github/workflows/)' '^docs/decisions/'
check '^scripts/check-layout\.sh$'                        '^docs/decisions/0034-layout\.md$'
check '^internal/http/'                                   '^docs/'

exit $status

Makefile(并进 check)

本地跑时基线取 main,所以先 git fetch origin main 再跑。BASE 可以在持续集成里改成合并目标分支。make check 的聚合入口只在质量门禁里定义一次,这一页只加子目标。

.PHONY: docsync docs-check

docsync:
	sh scripts/check-doc-sync.sh

# 挂进 check:check: fmt-check lint type docs-check secret complexity test
docs-check: docsync
	ruff check --output-format=concise .

脚本管不到的地方:它只在改动差异的文件名上做判断,它不知道文档内容有没有真改。给 README.md 加一个空行也能让它变绿。所以第六到第八行仍要评审来看——脚本的作用是把「忘了更新」这类问题从人身上挪走,不是替代人。

术语对照与链接检查

同一个东西在文档、代码、界面上叫三个名字,会让人以为它们是三件事。下面五个词出现过歧义,各留一个说法。代码与 SQL 里的英文名保持原样,在文档里第一次出现时写「所属范围(scope_id)」。

只用这个说法不要写成英文名(代码里保持)
所属范围租户、班级、组织、部门scope、scope_id
笔记文档、条目、卡片note
无依据未命中、拒答、没有答案(界面原句单独维护一份)
决议ADR、决定、规约文档docs/decisions/
检查门禁、校验、质量关卡make check

仓库内链接检查

它只检查仓库内的相对链接。不检查外网链接——外网会因为网络和限流误报,每次都红会让人开始忽略这条检查。

lychee --offline --no-progress README.md 'docs/**/*.md'

外网链接怎么处理:外网链接过期要人处理,做法是把它列进「每季度检查一次」的清单,由一个人在一个时间点跑一次带网络的检查。工具名与参数以官方文档为准。

必须死守的五条

同一条内容只维护一份判据:一条步骤出现在三个文件里时,留在最接近执行的那一份,另外两处改成文件名。搜关键词,命中数应为 1。违反后果:改一处忘另一处,读者按过期的那份操作后失败。
期望必须可判断判据:每条验收里不出现「良好」「可用」「流畅」「正常」这类词,只出现状态码、原句或日志字段。违反后果:两个评审对同一套结果给出相反结论,验收变成表态。
术语各留一个说法判据:上一节那张表的左列在 docs/ 里搜得到,右列搜不到。违反后果:读者以为「租户」和「所属范围」是两个不同的概念。
把文档和导致它变化的代码放进同一次评审判据:sh scripts/check-doc-sync.sh 退出码 0。违反后果:文档落后一个版本,且没有任何东西会在下次提醒。
README 里不放目录树与字段表判据:README.md 中不含以 ├── 或 | 字段 开头的行。违反后果:目录一变,README 就成了第三份会过期的描述。

第四条不触发的情况:纯文档改动(只改 docs/ 或只改 README.md)时它因为「没有代码改动」而不触发任何检查,这是正常的。此时靠评审来看术语与期望是否仍然可判断。