Skip to content

让 spec 与代码保持同步

spec 只有在还能描述它下面的代码时才有用。SpexCode 不去判断正文的意思是否仍和代码一致,那得靠人读。它能做的,是只凭 git 算出:spec 管辖的代码什么时候变了、而 spec 没有跟着变。整个机制就是这一件事,而且算起来足够便宜,每次提交都能跑。

spec 写明自己管辖的代码

节点的 code: 写明它管辖的那一个文件。还可以更进一步,把这个文件里具名的单元(函数、方法、类)钉成锚点:src/ingest/webhookVerifier.ts#verifyWebhook。TypeScript、Python、Go、Rust、Java、Ruby 的锚点按语法结构解析,所以锚点跟着这个单元在文件里走,而不是钉在某个行号上。related: 列的是节点引用、但不归它管的文件。

窗口与求交

spec 的每个版本,就是一次碰过它 spec.md 的 commit。窗口从最新一版开始。之后的每个 commit,git 给出它改动了哪些行,SpexCode 拿这些行去和锚点所指单元在那个 commit 时的行区间求交。

spec webhook-security v3 之后,commit cbe53ee 改了 webhookVerifier.ts 第 6 行,落在 verifyWebhook(第 5–13 行)内,重叠即 anchor-drift。

  • 某个 commit 改动的行和锚点所指单元有重叠,就是 anchor-drift,属于错误。装了钩子时,会引入它的提交会被拦下。
  • 被管辖的文件在锚点以外的地方变了,或者节点没有锚点,就是 drift,属于提醒,永远不会拦提交。
  • related: 里的文件变了,给出的是更轻的一类提醒。

不存任何东西:没有哈希,也没有「上一次正确状态」的快照。每次读取都从历史里重新算出版本、窗口和重叠。

关闭窗口的两种办法

意图变了,就把 spec 和代码一起改。碰了 spec.md 的那个 commit 就是新版本,窗口从它之后重新开始。

只是实现细节变了、契约仍然成立,就把这一点写下来。正在提交的 commit,加一个 trailer:

git commit --trailer "Spec-OK: webhook-security"

已经提交的改动:

spex spec ack webhook-security --reason "契约仍然成立,这次重构没有改变它"

ack 会记下一个空的标记 commit,理由写在里面。有理由的 ack 和别的改动一样要被 review;没有真实理由的 ack 等于白做。

spex spec lint 还检查什么

除了漂移,lint 还保证图本身是健全的。错误:code: 或 related: 里的路径或锚点解析不到(integrity);一个节点管辖了不止一个文件(one-govern);正文长出了 ## vN 这样的变更记录标题,而不是描述现状(living);[[节点]] 引用指向不存在的节点(mention)。提醒:没有任何节点认领的受 git 跟踪的源文件(coverage);一个文件被太多节点整体管辖(owners)。每条规则都写在 spex guide spec 里。

检查在哪里跑

每个 clone 都可以装一个 pre-commit 钩子,跑同样的 lint,遇到错误就拦下。它反馈快,但只在本地有效:没装钩子的新 checkout 就没有它,一次提交也可以用 SPEXCODE_SKIP_LINT=1 跳过它。

谁都跳不过的关口,是在持续集成里对每次 push 和 pull request 跑 spex spec lint。因为版本和窗口都来自历史,CI 需要完整的 git 历史,不能用浅克隆。