v1.35.0 — 写入前拦截、审计降噪、捕获复访、三个否定结论
本版有一个会让你撞到的行为变更、一次审计降噪(参考 Vault 上 finding 从 113 降到 92,去掉的全是假阳性)、一个回答「捕获之后有没有被再打开」的新 helper,以及三个被数据否掉、连同落败一起记下来的假设。
还有一个自己引入又自己修掉的回归——它值得单独说,因为它暴露的东西比它本身更重要。
写入前拦截:审计知道有缺陷,文件却已经落盘(#156)
create-note --apply 现在会拒绝仍带模板脚手架的正文,退出码 2,不落盘。
此前的形状是:写后审计在同一次调用里就知道这篇笔记有缺陷,但文件已经落盘、索引已经更新、退出码是 0,--json 顶层没有任何字段报告判定结果。唯一的信号埋在嵌套的 audit.ok: false 里。任何按退出码判断成败的调用方都被告知写入成功。
参考 Vault 上 9 篇笔记因此带着写给 Agent 的指令存活下来——<!-- 用 2–4 句话区分原文观点与自己的推论 --> 是给写笔记的人看的,现在是用户笔记的一部分。
拒绝集不是全部 defect。二十个 defect 码里多数描述的是 Vault 而非这篇笔记,broken-wikilink 更是被同期裁定为标准用法——链接一篇还没写的笔记正是 Obsidian 的图谱提示概念值得单开一篇的方式。按 defect 拦截会让「新建笔记时链接一个未来概念」变成不可能,所以有一条断言点名 broken-wikilink 不在拒绝集里。
--no-audit 跳过写后报告,不跳过这道拦截。让它绕过就等于把它变成「写一篇已知有缺陷的笔记」的开关。
那个回归,以及它暴露的东西(#163 → #179)
上面那条拦截的判据是裸正则 \{\{([^}]+)\}\},没有任何代码块豁免。于是:
```html
<span>Message: {{ msg }}</span>
一篇讲 Vue 的笔记根本写不进去。Jinja2、Handlebars、Liquid、GitHub Actions、Obsidian Templater 同理。
最难堪的是:**我在同一个 PR 里为 `broken-wikilink` 写过一模一样的论证,却没把它应用到自己收进拒绝集的那一码上。** 三条新断言全绿——因为它们测的是我想到的场景。
修复不损失任何检测:参考 Vault 上被报的 8 篇**全部是 `{{date}}` 且全部在代码块外**,忽略围栏与行内代码之后仍是 8 篇。
判据收进 `note_catalog`,因为要共享的不只是正则,还包括**「忽略代码」这个决定**——`audit-vault` 与 `process-inbox` 必须一致,否则一篇笔记能通过归档却过不了审计。而 `template_contract` 继续用裸正则:它读的是**模板文件**,那里的 `{{date}}` 正是要找的东西。
顺带:为 `note_catalog` 复制围栏遮罩时,闭合正则的转义写坏了,**围栏永远不闭合**,代码块之后的占位符会被整段吞掉。新加的「两份实现必须一致」那条断言**写完五分钟就红了**。
## 审计降噪:113 → 92,去掉的全是假阳性
- **YAML 裸日期不再被判占位符。** YAML 把 `published: 2026-08-13` 解析成 `datetime.date`,只有带引号的才是 `str`。而判据拒绝一切非 `str`,于是它实际在评判**作者有没有给日期加引号**——两篇把 `published` 与 `author` 都填对了的 web-clip 被报成缺失。**写得最规范的反而被罚。**
- **`disconnected-note` 不再命中 web-clip。** 23 条全部 ≤47 天、中位 27 天,这个状态会自己消失;而 `suggest-directed-links` 在这 23 篇上**候选数为 0**,所以也无从建议。23 篇里 20 篇已被 `review-captures` 以更强的问题覆盖,一条断言把豁免绑在那份覆盖上:把 `web-clip` 从 `CAPTURE_TYPES` 拿掉,测试就红——否则豁免的理由会静默失效。
- **日报这类编号序列不再被当成近重复。**
## review-captures:捕获之后呢(#160)
其它每一项测量问的都是捕获是否**忠实**,没有一项问它是否被用过。
参考 Vault 上 94 次捕获里 **55 次再没被打开过**,而复访率按类型相差三倍:`learning-note` 0.75、`insight-note` 0.31、`web-clip` 0.275。
证据来源是 git 历史(Vault 在仓库里时)或文件 mtime,**报告会说明用的是哪一个**——两者精确度不同,不该被读成同一回事。
同期修掉一个它自己的缺陷:`.obsidian-kb-backups/` 下的副本被算成了捕获。副本不会被任何人再打开,把它算进来只会污染这个指标。
## 三个被否掉的假设,连同落败一起记下
否定结论在树里不留痕迹:没有测试守着它,没有代码提到它。下一个看到同一条 finding 的人会从头再推一遍,**而且可能不在证据停下的地方停下**。三条都在 `docs/superpowers/specs/2026-08-21-rejected-hypotheses.md`,各自带着试过的判据、杀死它的数据、以及什么会重开它。
- **断链无法区分「概念占位」与「被删笔记」。** 判据是「概念占位会被多篇引用」——实测每一个概念占位都**只被引用一次**,和一次删除完全一样。更根本的是:单次快照里两者留下的痕迹相同,**Obsidian 自己也不区分**。
- **web-clip 里的 `unknown` 不是「正确捕获被误报」。** 两种写法**在时间上零重叠**:`unknown`/`未知` 止于 2026-07-26,`原文未标明`/`原文未署名` 起于 07-27,而分界线正是那次把 `unknown` 列为占位符、并在指令里指定替代写法的提交落地那天。规则生效后使用被弃形式的 web-clip 是 **0 篇**——裁定生效了,豁免它等于撤销它。
- **空壳笔记的结构判据不成立。** 按「空章节占比」排序,六篇日报和一篇有意留简的草稿排在真正的空壳前面。而那些措辞也不是仓内常量:`web-capture.md` 明令禁止保存占位物,代码里根本不存在这条写入路径——那两篇是 Agent 违反规则时自己编的散文。
## 评估基础设施
- **硬门禁不再中止整批。** 硬失败意味着「这次运行不算数」,而退出码已经承载了这个语义;中止额外丢掉的正是这次运行存在的目的——一次基线因此停在 36 次中的 15 次。`--stop-on-hard-failure` 保留旧行为,`stopped_after_case` 记录截断,好让部分均值不被读成整体。
- **收据门禁不再按 Agent 选了哪种内容来源判分。** 它额外要求 `--from-preflight`,而 `create-note` 同样接受 `--stdin` 与 `--content-file`,于是一次完全正确的 verified 捕获被判成硬失败。绑定关系由 helper 自己的输出证明,去掉那个要求什么都没削弱。
- **对抗集语料的平均长度对齐真实 Vault。** 两篇 76KB 笔记独占语料 **97.9%** 的字节,把均值顶到 9170 对真实的 4247——BM25 的长度惩罚因此从不触及任何日常长度的笔记,**一整类排序失败在这个集合里根本不可观测**。现在是 38KB,落在参考 Vault 第 2/3 大笔记的量级,而 `adv-dilution-06` 把那个失败冻结下来:同一来源的 2.4KB 完整捕获,在关键词密集查询上排在 0.5KB 摘要之后。
---
发版清单外还撞到三个硬编码版本号的测试文件(`test_build.py` 的 `test_project_version_reads_pyproject`、`test_doctor.py` 两处、`test_installers.py` 一处)。全套测试会抓到它们,但只会在 CHANGELOG 已经写完之后。