构建产物的脏改动不是事故 · 复发是归因错了的信号
入档:2026-07-28 来源:蛛网之上(above-the-web)源代码管理面板里
kb-content反复出现 305 个未提交改动,作者问「为什么又出现了,这是第二次了」 状态:已定位并处置(提交1ead950,作者重载窗口后确认消失);本篇同时是对 2026-07-27 一次错误归因的推翻——那次把同一现象归到 Obsidian 头上,并据此写进了记忆与归档分支
事实记录(不可修改区)
现象
E:\above-the-web\kb-content 稳定呈现 305 个 M(modified)。作者明确指出「第二次了」。
我的初判(后被证伪)
初判为「Obsidian 属性面板批量重写 frontmatter」。依据两条,都不是直接证据:
- 我的记忆文件
repo-sweep-loose-ends里写着「kb-content 有损格式化 = Obsidian,已归档到archive/obsidian-lossy-reformat-20260727」; - knowledge-base 仓库当天有一条提交
df79c76 配置:Obsidian 属性显示改为源码模式,阻断 frontmatter 批量重写。
diff 形态也「很像」:tags: [类型/档案] 展开成带引号的 YAML 块列表、BOM 被删。我当时说了「确诊了」。
逐步收集的事实
- 不是行尾问题:
git diff --ignore-cr-at-eol/-w后改动全在(+926 / −414),是真实内容改动。 - 改动范围极干净:305 个 hunk 的起始行分别是 1(56 个)、2(248 个)、3(1 个),起始行 >12 的 hunk 为 0,正文零改动。
- 唯一一行”非 frontmatter 新增”是假象:
+# Stay alive Case Notes实为-# …→+# …,即 BOM 被删。 song-caption-mv-workflow/SKILL.md的 frontmatter 被清空:name与description两行消失,只剩---\n---。我当时说「这个 skill 已经废了」——也是错的,见下。- 时间戳比对(决定性):305 个文件的 mtime 全部落在 2026-07-27 18:00,而
.obsidian/app.json那次”修复”改于 16:44。改动发生在修复之后——上次的归因与修复被同时证伪。 - 名册排除(决定性):
%APPDATA%\obsidian\obsidian.json里 Obsidian 只注册了一个库E:\AIGC工作站\年轮平台测试\docs,kb-content 根本不在其中。Obsidian 不会自动扫描磁盘上的目录。
真凶:自家的同步脚本,且是设计行为
E:\above-the-web\scripts\sync-content.mjs 的 normalizeFrontmatter(),三个动作正好对应三种 diff:
| 脚本行为 | 观察到的 diff |
|---|---|
stripBom() | ---- → +--- |
重建 tags:\n - "x" | 行内数组展开成带引号块列表 |
只保留 tags / title,丢弃其余键 | SKILL.md 的 name / description 消失 |
源码注释写明了意图:「knowledge-base 用 Obsidian 风味 frontmatter(值里可能含 [[]]、未引号冒号等非法 YAML)。平台只消费 tags / title,故重建为最小安全 YAML,丢弃其余专有键,杜绝解析崩溃。」
为什么必然复发
kb-content/在 above-the-web 的.gitignore第 11 行,注释即「内容源:构建时从 knowledge-base 仓库克隆,不入库」;package.json里dev与build都以npm run sync开头;sync先git reset --hard FETCH_HEAD,再遍历所有 md 就地sanitize()写回。
即:每次开发或构建都必然产生这批脏改动,且下一次 sync 会把它们冲掉重来。 它是流水线的稳定产物,不是事故。
派生物损坏 ≠ 源损坏
核实 SKILL.md:E:\knowledge-base 源仓库那份、以及本机 C:\Users\Administrator\.claude\skills\ 装载的那份,name / description 均完好,源仓库 git status 干净。kb-content 里那份只是会被反复重建的副本。
处置
问题不在数据层——文件被改是设计内的;问题在展示层:VSCode 自动检测嵌套 git 仓库,把这个构建产物单列进源代码管理面板,逼人每次重新判断一遍「我是不是漏提交了」。
加 E:\above-the-web\.vscode\settings.json:
{ "git.ignoredRepositories": ["kb-content", "E:\\above-the-web\\kb-content"] }
提交 1ead950。作者 Developer: Reload Window 后确认面板里不再出现。
一句话总结
同一个现象第二次出现,而上次”修复”之后它照样发生——这不是”又坏了一次”,这是上次归因错了的证据。最快的证伪手段是比时间戳:改动发生在修复之后,归因就不成立。
五条可复用 insight
1. 复发 + 上次已修 = 上次归因错了 ⚠️首次
人的默认反应是「又犯了,再修一次同样的地方」——这个反应默认了上次归因是对的。但「修了却照犯」本身就是对那次归因最强的反证。
最快的证伪动作是比时间戳,两个数字就够:
改动发生:2026-07-27 18:00
上次修复:2026-07-27 16:44 ← 修复在前,改动在后 → 归因不成立
这一步花不到一分钟,却直接推翻了一整条被写进记忆的结论。遇到”第二次”,先做这个减法,再谈修。
2. 自己写下的错误归因,会成为下一次的加速陷阱 ⚠️首次
我不是慢慢误判的,是很快误判的——因为记忆文件里明明白白写着「kb-content 有损格式化 = Obsidian」。它让我跳过了取证,直接说出「确诊了」。
错误的结论一旦被写进记忆 / 文档 / commit message,就不再是一个待检验的假设,而变成了下次的默认前提。 它的危害不是”存了个错东西”,而是它会主动加速你第二次走错,且比第一次更自信(“上次就是这个原因”)。
有意改动不是故障_v1 已记过「归因被推翻要连经验一起改」;本次补上更前一步的判据:任何被写下的归因,都要带着”它可能是错的”这个标签被读取。尤其当它来自你自己——自己写的东西最不会被质疑。
推论:归因类的记忆条目,最好连”证据是什么”一起写。我那条记忆只写了结论(“是 Obsidian”),没写”凭什么”,于是无从复核。写上”证据:obsidian.json 里注册了该 vault”,这次一查就露馅了。
3. 查”谁干的”要点名册,不要从形态反推 ⚠️首次
diff 的形态(tags 重排 + 去 BOM)确实非常像 Obsidian 的手笔——这正是它骗人的地方。形态相似只能提出嫌疑人,不能定罪。
定罪靠的是名册:obsidian.json 里 Obsidian 到底注册了哪些库。一查,kb-content 不在其中,嫌疑当场排除。
判据:怀疑某个程序改了文件,先去查那个程序的”作用域清单”(它管哪些目录 / 打开了哪些项目),而不是从改动长什么样往回猜。 作用域是二元的、可证伪的;形态相似是连续的、可自圆其说的。
同族反面:有意改动不是故障_v1 那次是「用一个真实的模式去套了一个不属于它的现象」(formatter 确实重排过表格,于是把删图也归给它)。本次一模一样——Obsidian 确实会重写 frontmatter,于是把这批也归给它。真实存在的模式,是最容易被过度套用的模式。
4. 派生物损坏 ≠ 源损坏,喊”废了”之前先查源 ⚠️首次
看到 SKILL.md 的 name / description 被删,我脱口而出「这个 skill 已经废了」。实际上那是个每次构建都会被重建的副本,源仓库和本机装载的那份都完好。
判据:在一个可重建的目录里发现”数据损坏”,第一件事是确认它是源还是派生物。 派生物的损坏不是数据事故,只是产物形态——它甚至可能是设计要的(本例中丢弃 name/description 正是脚本刻意为之,因为站点只消费 tags/title)。
识别派生物的三个信号,本例全中:在 .gitignore 里 / 由脚本自动克隆 / 同步时会 reset --hard。
5. 问题在展示层时,别去修数据层
文件被改是对的、不该拦;git status 显示脏也是对的、没说谎。唯一错的是 VSCode 把一个构建产物摆进了”等你提交”的位置,让人每次都要重新做一遍判断。
如果只盯着数据层,能做的只有”每次手动 checkout 一遍”——治标且徒劳(下次 build 又来)。真正的解法在展示层:git.ignoredRepositories 一行配置,从此不再出现在待办位。
判据:当”数据是正确的,但它出现在错误的位置上”时,改的是位置,不是数据。 反复出现的、需要人做同一个判断的东西,都该问一句:能不能让它压根别出现在需要判断的地方。
顺手教训
- 构建产物目录同时是 git clone 时,会被 VSCode 当独立仓库单列。父仓库的
.gitignore拦不住它——.gitignore管的是”父仓库不跟踪它”,而 VSCode 的嵌套仓库检测是另一套逻辑。要用git.ignoredRepositories(配在项目.vscode/settings.json里,只影响该工作区)。 git diff --name-only的中文路径默认被转义成\345\275\225形式,直接喂给 PowerShell 的Test-Path会报 “Illegal characters in path”。加git -c core.quotepath=false即可。- 判断”是不是纯格式化”的快法:
git diff -U0 | grep '^@@'取所有 hunk 起始行,看是否全部落在文件头几行。本次 305 个 hunk 全在第 1–3 行,正文零改动,一眼定性。比逐个文件看 diff 快一个量级。 git diff --ignore-cr-at-eol/-w是排除行尾/空白噪声的第一道筛。本次一开始满屏 “LF will be replaced by CRLF” 警告,很容易误以为是行尾问题——加这两个开关一比,改动数一点没少,当场排除。- PowerShell 的
ConvertFrom-Json读obsidian.json会因编码报错(中文被按 GBK 读成乱码 + 反斜杠转义非法),但报错信息里把原始 JSON 整段打出来了,反而直接给出了答案。歪打正着不可复制,正规做法是[IO.File]::ReadAllText($p, [Text.Encoding]::UTF8)再解析。
下次改进
- 遇到”第二次发生”,第一个动作固定为比时间戳(现象 mtime vs 上次修复时间),而不是复用上次结论。
- 写归因类记忆时,连证据一起写(“凭什么这么判”),让它可被复核;只写结论的归因等于给未来的自己埋雷。
- 怀疑某程序改了文件,先查该程序的作用域清单(vault 列表 / 打开的项目 / 监听的路径),再看 diff 形态。
- 在可重建目录里看到”损坏”,先确认它是源还是派生物;三个信号:在
.gitignore里、脚本自动克隆、同步时reset --hard。 - 反复要求人做同一个判断的界面元素,优先考虑”让它别出现”,而不是”每次判断得更快”。
关联文档
- ⭐ 有意改动不是故障_v1 —— 直系母本,本篇是它的再验与扩容:那篇的三类归因(用户有意改 / 协作方越界 / 无害格式化)没覆盖本次这类——自动化流水线的设计产物,已回补为第四类;同时那篇「用一个真实的模式去套了一个不属于它的现象」在本次原样复现(Obsidian 确实会重写 frontmatter,于是这批也被归给它)
- ⭐ 全站文字截断体检_检测工具本身要先被证伪_v1 —— 同族第三次现形:那篇是「检测工具的输出要先证伪」,ping通不等于路通_fake-ip假信号与节点带宽实测选型_v1 是「诊断信号要打在真实链路上」,本篇是「你自己写下的历史归因要先证伪」——三条合起来覆盖了工具、信号、记忆三个信息源
- ⚠️ ping通不等于路通_fake-ip假信号与节点带宽实测选型_v1 —— 前一天的同族案例:那次是 ping 永远绿导致误判链路健康,本次是记忆永远在导致误判故障来源;共同点是最方便取用的那个信号最不可信
- 生成物不入git_v1 —— 相邻的 git 资产纪律:那条管「可重建的生成物不进版本库」,本篇管「可重建的生成物出现在版本控制界面里时,别把它当待办」;本例的 kb-content 正是被正确 gitignore 了,却仍被编辑器摆进提交位
- 变通方案不等于故障点_v1 —— 同族错误归因:那条管「别从产物形态反推故障位置」,与本篇 insight 3「别从 diff 形态反推是谁改的」是同一条在不同场景的投影
- 复盘事实先行原则 —— 本篇顶部事实冻结区按它执行;本次尤其重要,因为要冻结的事实里包含「我自己上一条结论是错的」
- 09_平台工程索引 —— 平台工程区入口