> 进 git 的判据只有一句:明天这个仓库能不能把它重算出来

Systems & Infrastructure Intermediate 2026-10-12 04:23 2026-10-12
#git #gitignore #data-provenance #backup #artifacts #failure-modes

一条每天早上七点跑的流水线,产出证据快照、模型摘要、收据、周报。 spool/ 本来是被 git 跟踪的,但最后一次提交停在某一天, 之后 23 天的产出全都只在工作树里:管线照写不误, 而没有任何一步会去提交它。

// TABLE_OF_CONTENTS

结论先行

一条每天早上七点跑的流水线,产出证据快照、模型摘要、收据、周报。 spool/ 本来是被 git 跟踪的,但最后一次提交停在某一天, 之后 23 天的产出全都只在工作树里:管线照写不误, 而没有任何一步会去提交它。

.gitignore 顶上现在只写了一句判据:

如果这个文件今晚被删掉,明天这个仓库能不能把它重算出来? 能,就忽略;不能,就跟踪 —— 哪怕它是"产物",哪怕产物通常不进 git。

照这句话重新分一遍,结果和"产物不进 git"的直觉正好相反:

文件 进 git 理由
spool/*.evidence.json ✅ 当天 07:00 各工作树的快照,含从未提交进任何仓库的工作
spool/*.digest.json ✅ 每天一次模型调用;明年重跑读到的是另一棵树,答的是另一个问题
archive/week-*.json ✅ 周模型,除 brief 外确定性
spool/*.receipt.txt ✅ 纸上当时到底写了什么
archive/*.detail.txt ❌ 从上面两个确定性重算
spool/*.escpos ❌ 渲染中间产物

代价:42 天 2.3 MB,一年约 15 MB,私有仓库。

而我一开始判错了最不可重算的是哪个。 我以为是周模型 week-*.json, 错了 —— 它是派生的,analyze.py 就是从 spool/ 的 evidence + digest 重建它的,真正不可再生的只有里面 brief 那一小段。 链条最上游、也最补不回来的是 evidence.json:它记的是当天工作树的状态, 包括那些从没变成 commit 的工作(digest 里就有 "all uncommitted, no commits" 这样的句子)。

git history 里没有它,因为"不进 git"正是那些工作的定义。

为什么

"产物不进版本控制"这条规矩是对的,但它的真正依据不是"产物"这个分类, 而是可重算性。build 产物不进 git,因为源码加工具链能把它造出来; 一旦某个产物的输入里有不会被保留的东西,它就不再可重算, 而分类名没有变。

不可重算的输入有三种,都容易被忽略:

  1. 时点状态 —— 某一刻的工作树、某一刻的磁盘占用、某一刻外部 API 的回答。 明天重跑得到的是另一个时点的答案。
  2. 一次模型调用 —— 同样的 prompt 明年跑,模型不同、输入树不同, 答的是另一个问题。它是观测,不是函数求值。
  3. 当时呈现给人的那个版本 —— 收据和周报严格说可以重渲染, 但只能用今天这版渲染器重渲染。留着它们是因为以后要吵的是 "当时纸上写的是什么",不是"今天的代码会印出什么"。

反过来,也要防判据被滥用:*.detail.txt 确实能从 spool 文件确定性重算, 所以它该被忽略 —— 判据的价值在于它两个方向都给答案,而不是 "重要的东西都存起来"。

异盘备份不能替代这一条。那份每晚 rsync 的备份 --exclude='.git',保住文件,丢掉历史,而且两块盘在同一台机器里。 进 git 补的正是它补不了的两样:一份离机副本,和一段能 diff 的历史 (见 --exclude='.git/objects' 的备份恢复出来的是尸体,不是仓库)。

怎么用

把判据写进 .gitignore 的第一行,而不是记在脑子里。 那个文件是每次有人犹豫"这个要不要忽略"时唯一会被打开的地方。 写成一个问句比写成一份清单好:清单会过期,问句不会。

给未来的自己留一条反向警告。 这个仓库里现在有这么一段:

⚠️ 在 git 里看到几 MB 的 JSON,不要"顺手"把它 gitignore 掉。 那不是构建垃圾,那是这个项目唯一的底稿。

值得照抄。"仓库里有大 JSON" 在几乎所有项目里都是坏味道, 所以一个按常识清理仓库的人(或会话)会正好做错事。 凡是违反通行直觉的决定,都得把理由留在它旁边。

不要让 cron 去提交,但要让它报数。 run_weekly.sh 末尾现在数一次 spool/ 和 archive/ 下未提交的文件数,写进 cron log。 一次提交里该放什么不是 cron 该做的决定 —— 但这个失败模式静默了 23 天,不能再静默第二次。 这是"自动化只负责观测、不负责决定"的一个便宜实例, 同 优雅降级设计好了,却没人被告知——退化必须在产物上留痕。

把不可重算的那一段隔离出来,别让重跑覆盖它。 同一个项目里的 配套改动:确定性字段任何时候都能从证据重算,brief 不能。 --no-brief 重跑(或模型不可达的那天)以前会连同 brief 一起覆盖掉 week-*.json;现在沿用已存的那份并打印 carried_from。 一个产物里混着可重算和不可重算的部分时,重算路径必须对后者是只读的。

进 git 之前扫一遍 key / token / .env。 这批文件是扫描产物, 很容易夹带路径、主机名、凭据。(这个项目扫出的唯一命中是一个任务名 里的 task-yaml-pre-slo-calibration-1911 被切成了 sk-... 的假阳性。)

出处

~/Engineering/worklog-printer:.gitignore 顶部的判据与 TRACKED/IGNORED 分类,README.md §2「归档进 git:能重算的不留, 不能重算的必须留(2026-09-01)」,weekly.py:797-802(carried_from), run_weekly.sh 末尾的未提交文件计数。