用途
changelog-as-war-story 针对的是普通提交信息无法承载的内容:提交信息只说改了什么,很少说尝试过并否决了什么、为什么显而易见的替代方案是错的、以及临时方案在什么条件下才能移除。这些信息在常规仓库里无处安放,最终只能由不知道它存在的人重新推导一遍。
该实践的做法是:在仓库根目录维护一个受版本控制的 markdown 文件,用项目自己的词汇记录带日期的条目;在接触不熟悉的区域之前先读它。它与所描述的代码处于同一份评审、diff 和历史之中。
主要能力
一个有用的条目不是摘要,它承载四件事:
- 症状(Symptom):一个人看到的现象,用他会使用的措辞描述。
- 机制(Mechanism):为什么会发生,停留在修复方案能成立的层面。
- 已排除的方案(What was ruled out):尝试过但无效的合理替代方案,明确点名,让下一个读者不必再花同样的轮次。
- 退出条件(The exit condition):上游或平台需要发生什么变化,这个条目才能被删除。
记录形态
被挖掘出该实践的应用在根目录保留了这样一个文件:一个带日期条目的章节,以及一条规定何时必须写条目的规则。条目大纲示例如下(经改编):
- Bundled library takes over a system name (2026-07-31):应用自带的某个常见库副本先被加载并占用了名字,导致一个通过 dlopen 加载系统副本的平台 API 再也找不到匹配符号,并在该进程余下时间里报告整个功能不受支持。所有外部链接调用点同时失效——一条路径因为分支没有 else 而静默失败,另一条直接从点击处理器抛出。绕过方式是在原生库加载前探测平台 API,让系统副本赢得名字。真正的根治办法是停止打包该库——这需要重新构建、重新发布并重新固定该 bundle。
注意它比“修复了一个崩溃”多承载了什么:同一失败的两种形态、让绕过方案成立的顺序约束,以及带有其隐含工作量的退出条件。
规则写在同一文件里:任何架构变更、依赖替换、新模块、打包变更或平台迁移之后都必须写条目——并明确列出什么不算(bug 修复、无 API 变更的版本升级、样式调整),从而让该章节保持为战史日志。
陷阱
- 只写结论而不写已排除清单。“我们采用方案 A”会招来下一个读者提议 B,因为没有任何记录表明 B 已被尝试。已排除清单是条目中价值最高的部分,也是赶时间写作时最先被丢掉的东西。
- 省略退出条件。没有它,每个临时方案都是永久的。能够被关闭的条目以“remove this once upstream frees the resource on the error path”或“the actual cure is to stop bundling it”这类形态结尾——即第 5 步正则所匹配的形态。其余条目则会固化。
- 条目记录的是已达成一致的设计,而不一定是实际交付的东西。缓慢漂移是显而易见的风险——这里有一个条目把某个样式值写成修复方案,而该调用点此后已经改变——但真正咬人的漂移在第一天就落地:条目按计划写,而计划在工作过程中变了。对该文件的一次审计发现,某个带日期条目中的三条具体断言与该条目自己的交付提交相矛盾:说是在卡片内渲染的东西实际在页面级渲染,说是被传入的修饰符实际没有传,三个被称为“有意未使用”的字段中有两个被使用了。什么都没有漂移;该条目从来就不是真的。挖掘条目时取其机制(通常是对的),并从代码树重新推导每一条具体断言;就地更正错误断言,标注日期,说明它此前声称什么。
- 工作笔记是有生命周期的脚手架,条目才是产物。功能构建期间放在旁边设计文档没问题,代码一旦存在它就该被废弃——之后它会比条目活得更久并与之矛盾。在记录的历史中,两份笔记里有一份被它自己的交付提交删除,内容并入带日期条目;另一份比它的功能多活了三周,只是被一个关于别的事情的提交顺带清理掉——这个陷阱就坐在它自己的证据里。给每份笔记明确的生命周期;该目录现在已不存在。
- 任其增长而不修剪。没人读完的文件就是没人读的文件。退出条件已满足的条目应被删除,而不是原地归档。
- 把条目放进永远不会发生的后续提交。规则之所以存在,是因为“我之后再补文档”活不过下一个任务。在记录的历史中,条目两种方式都有——在功能提交内,或当天的一个 docs: 提交里——而那些在功能提交内的从未丢失。
- 先搜索提交信息。提交主题只有一行,写于后果已知之前,而被压缩或合并的分支会把几天的推理压成一条。先搜索受版本控制的 markdown;只有它返回空时才去看历史。
- 把它当作只有人类读的文件。在这里,同一个文件也是仓库中自动化助手的常备简报——这就是为什么它用行为语言而非行话写作,也是为什么自动更新规则是写给它们的。
验证方式
将 DOC 设为受版本控制的文件(DOC=CLAUDE.md),然后:
条目是否带日期且结构化,还是一整块散文?
grep -cE '^- \*\*.*\(20[0-9]{2}-[0-9]{2}-[0-9]{2}' "$DOC"
grep -oE '^- \*\*[^*]+\(20[0-9]{2}-[0-9]{2}-[0-9]{2}[^)]*\)\*\*' "$DOC" | head
强制写条目的规则是否真的在文件里?
grep -nE '^#+ .*(Auto-Update|MANDATORY|Update Rule)' "$DOC"
它是否被维护,是否随代码一起移动?只有 docs: 提交意味着条目是事后补写的;feat:/fix: 提交触及它意味着规则在变更点被强制执行:
git log --format='%h %ad %s' --date=short -- "$DOC" | head -20
为你即将接触的区域证明搜索顺序。先 markdown,后历史:
TERM=crossfade
git grep -n -i "$TERM" -- '*.md'
git log --all -i --grep="$TERM" --oneline | head
检查条目是否带有退出条件、是否有人审计过它们、以及旁边的工组笔记是否正在被退役:
grep -oniE 'remove [^.]{0,30}once[^.]{0,40}|the actual cure[^.]{0,40}|once upstream[^.]{0,40}' "$DOC"
grep -cE '\(Corrected [0-9]{4}|previously (claimed|described)' "$DOC"
grep -oE '[A-Za-z0-9_/]+\.kt:[0-9]+' "$DOC" | sort -u | while IFS=: read -r f l; do echo "$f:$l -> $(git ls-files "*$f" | head -1)"; done
git log --diff-filter=D --format='%h %s' --name-only -- '*docs/*.md' | head
一个满是临时方案的文件里退出条件为零,意味着每一个都是永久的。更正数为零意味着没人拿它对照过代码树——所以打开第三条命令解析出的每个引用,定位到那一行,读代码是否与条目所说一致。无法解析的路径要么已过时,要么位于某个依赖中。在最后一个列表里,把每次删除的提交主题与它移除的笔记对照阅读:被一个关于不同功能的提交顺带清理的笔记,比它自己的功能活得更久,这就是上面的陷阱。
在交付一个条目之前,以陌生人的身份回读它,确认它回答了:有人看到了什么、为什么会发生、我们已经试过什么、以及它何时可以被删除。
适用场景
适用于需要长期维护、存在临时方案与平台/依赖约束的代码仓库,尤其是团队协作、需要让新成员或自动化助手快速理解某处为何如此实现的项目。
限制
资料未提供该实践的具体工具集成、自动化实现或强制机制;其规则与验证均以手工维护的 markdown 文件和命令行检查为基础。