dsh-vibe-math 2.3.12 → 2.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +17 -13
  2. package/cordis.patch.yml +1 -1
  3. package/{AUDIT-CHECKLIST.md → docs/AUDIT-CHECKLIST.md} +323 -304
  4. package/docs/COMPAT-AUDIT-ROUND2.md +325 -0
  5. package/docs/generate_framework_diagram_v2.py +114 -0
  6. package/docs/generate_framework_diagram_v3.py +127 -0
  7. package/{RELEASE-NOTES-2.1.0.md → docs/release-notes/RELEASE-NOTES-2.1.0.md} +143 -143
  8. package/{RELEASE-NOTES-2.2.0.md → docs/release-notes/RELEASE-NOTES-2.2.0.md} +266 -266
  9. package/{RELEASE-NOTES-2.2.1.md → docs/release-notes/RELEASE-NOTES-2.2.1.md} +43 -43
  10. package/{RELEASE-NOTES-2.2.2.md → docs/release-notes/RELEASE-NOTES-2.2.2.md} +88 -88
  11. package/{RELEASE-NOTES-2.3.0.md → docs/release-notes/RELEASE-NOTES-2.3.0.md} +207 -207
  12. package/{RELEASE-NOTES-2.3.1.md → docs/release-notes/RELEASE-NOTES-2.3.1.md} +134 -134
  13. package/{RELEASE-NOTES-2.3.10.md → docs/release-notes/RELEASE-NOTES-2.3.10.md} +105 -105
  14. package/{RELEASE-NOTES-2.3.11.md → docs/release-notes/RELEASE-NOTES-2.3.11.md} +57 -57
  15. package/{RELEASE-NOTES-2.3.12.md → docs/release-notes/RELEASE-NOTES-2.3.12.md} +80 -80
  16. package/docs/release-notes/RELEASE-NOTES-2.3.13.md +137 -0
  17. package/docs/release-notes/RELEASE-NOTES-2.3.14.md +83 -0
  18. package/{RELEASE-NOTES-2.3.2.md → docs/release-notes/RELEASE-NOTES-2.3.2.md} +145 -145
  19. package/{RELEASE-NOTES-2.3.3.md → docs/release-notes/RELEASE-NOTES-2.3.3.md} +115 -115
  20. package/{RELEASE-NOTES-2.3.4.md → docs/release-notes/RELEASE-NOTES-2.3.4.md} +69 -69
  21. package/{RELEASE-NOTES-2.3.5.md → docs/release-notes/RELEASE-NOTES-2.3.5.md} +63 -63
  22. package/{RELEASE-NOTES-2.3.6.md → docs/release-notes/RELEASE-NOTES-2.3.6.md} +66 -66
  23. package/{RELEASE-NOTES-2.3.7.md → docs/release-notes/RELEASE-NOTES-2.3.7.md} +59 -59
  24. package/{RELEASE-NOTES-2.3.8.md → docs/release-notes/RELEASE-NOTES-2.3.8.md} +45 -45
  25. package/{RELEASE-NOTES-2.3.9.md → docs/release-notes/RELEASE-NOTES-2.3.9.md} +70 -70
  26. package/docs/test-timing.md +19 -18
  27. package/installer.js +115 -41
  28. package/package.json +43 -37
  29. package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
  30. package/{audit-installer-compat.test.mjs → tests/audit-installer-compat.test.mjs} +136 -136
  31. package/tests/audit-installer-policy.test.mjs +261 -0
  32. package/{audit-persona-sensitivity.mjs → tests/audit-persona-sensitivity.mjs} +249 -249
  33. package/{audit-persona-surface.test.mjs → tests/audit-persona-surface.test.mjs} +349 -349
  34. package/{audit-prompt-invariants.mjs → tests/audit-prompt-invariants.mjs} +508 -508
  35. package/{audit-spec-traceability.mjs → tests/audit-spec-traceability.mjs} +193 -193
  36. package/{audit-v5-integrity.mjs → tests/audit-v5-integrity.mjs} +448 -448
  37. package/{audit-v5-sensitivity.mjs → tests/audit-v5-sensitivity.mjs} +384 -384
  38. package/{e2e-v5-round2.test.mjs → tests/e2e-v5-round2.test.mjs} +521 -521
  39. package/{formal-verify-v2.test.mjs → tests/formal-verify-v2.test.mjs} +1315 -1315
  40. package/{formal-verify-v3.test.mjs → tests/formal-verify-v3.test.mjs} +1257 -1257
  41. package/{formal-verify-v4.test.mjs → tests/formal-verify-v4.test.mjs} +1082 -1082
  42. package/{formal-verify-v5.test.mjs → tests/formal-verify-v5.test.mjs} +708 -708
  43. package/{prompt-v5-integrity.test.mjs → tests/prompt-v5-integrity.test.mjs} +3 -3
  44. package/{run-tests.mjs → tests/run-tests.mjs} +121 -118
  45. package/{selfdrive-v5.mjs → tests/selfdrive-v5.mjs} +470 -470
  46. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
  47. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
  48. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +1 -1
  49. /package/{RELEASE-NOTES-2.0.22.md → docs/release-notes/RELEASE-NOTES-2.0.22.md} +0 -0
@@ -1,304 +1,323 @@
1
- # 全面检查(audit)必查清单
2
-
3
- > **本文件是强制流程,不是建议。** 每次对本仓库做"全面检查 / 找 bug / 优化"时,
4
- > **必须**逐项过一遍本清单。清单的来历是一次真实事故:v2.1.0 在实地测试中,
5
- > 每个成员收到的提示词都写错了身份,而当时 123 条断言 + 15 个灵敏度探针**全部通过**。
6
- > 事故的根因不是某一处代码写错,而是**审计维度本身漏了一整类**——没有人检查"成员读到的文字"。
7
-
8
- ---
9
-
10
- ## 0. 铁律
11
-
12
- 1. **成员读到的文字就是产品。** 提示词(人设 / 状态块 / 每轮问句 / 收件框头 / 回执契约)
13
- 必须像函数返回值一样被断言。任何只检查工具返回值、投影状态、文件内容的测试,
14
- 对提示词缺陷是**盲的**。
15
- 2. **身份一律显式传递,绝不猜测。** 禁止从"最近唤醒的成员""列表第一个"之类的全局状态推断
16
- "这段文字是写给谁的"。宁可显式失败(`V5_INTERNAL`),也不要生成一段身份错误的提示词。
17
- 3. **测试脚本必须完整保留交互信息。** 断言之外,还要把交互原文落盘成可人工复核的语料
18
- (见 §2.4)。"退出码 0"不是验收,"人能读到正确的原文"才是。
19
- 4. **探针必须真的能变红。** 一个灵敏度探针如果始终为绿,说明它守的那条不变式**其实没被测到**——
20
- 这比没有探针更糟(它给人虚假的安全感)。
21
-
22
- ---
23
-
24
- ## 1. 第一优先:提示词分配与交互内容
25
-
26
- **这一节是最高优先级。历史上最致命的缺陷全部出在这里。**
27
-
28
- ### 1.1 身份分配
29
-
30
- - [ ] 每条提示词里的身份(`[状态] 你是 X`)= 这条提示词**实际发给的成员** X?
31
- - [ ] 表头里的身份(`【… —— <职位> <代号>】`)= `[状态]` 里的身份?
32
- - [ ] 随行人设(persona)指向的资料库路径 = 该成员自己的(`Members/<该成员>/`)?
33
- - [ ] 人设里的代号/职位 = 实际职位?
34
- - [ ] 有没有任何地方从**全局可变状态**推断身份(`currentMember`、`list[0]`、闭包快照)?
35
- - [ ] 成员**创建/唤醒的时序**:构造提示词时,该成员是否**已经**被写入权威状态?
36
- (先落盘、再构造;顺序反了会得到"上一个成员"的身份与"加入前"的编制)
37
- - [ ] 全文有没有 `?` / `undefined` / `NaN` / `[object Object]` 之类占位垃圾?
38
- - [ ] 一条提示词里是否**只有一个**身份声明(不能出现两段互相矛盾的身份)?
39
-
40
- ### 1.2 编制 / 名额 / 门槛
41
-
42
- - [ ] `[在册]` 是否包含读者**自己**?
43
- - [ ] `[在册]` 是否与权威编制一致(不多、不少、无重复)?
44
- - [ ] "有表决权者 N 人"是否等于 `[在册]` 里真正的表决者数(临时工不算)?
45
- - [ ] `m = min(quorumCap, N)` 是否与状态块里报的一致?
46
- - [ ] 未就位/失败/已除名的成员有没有被**如实**呈现(不能静默略去,也不能混进在册)?
47
- - [ ] 轮次号在表头与状态块里是否一致?
48
- - [ ] 编制变动(雇佣/解雇/增聘常驻)后,后续提示词是否立刻反映新编制?
49
-
50
- ### 1.3 名称与领袖叙事
51
-
52
- - [ ] 章程/人设里出现的**每一个代号**是否都真实存在于当前编制?
53
- - [ ] 有没有**硬编码**的代号(例如写死 `acad`),而实际编制可能不同?
54
- - [ ] 当某种职位根本不存在时(例如 `academician:false`),章程是否仍然声称它存在、
55
- 要求成员向它汇报、或承诺它会派活?(这是"指向一个不存在的人")
56
- - [ ] 章程是**入职快照**还是每次重建都改写?如果它自称"你入职时的…",就必须冻结在入职时。
57
-
58
- ### 1.4 交互内容(消息 / 群聊 / 会议 / 辩论 / 分派 / 雇佣)
59
-
60
- - [ ] 每条送达消息的**框头署名 = 真实发送者**?(尤其:所办 ≠ 院士;不能署"最近唤醒的成员")
61
- - [ ] 框头**类型**是否正确?(私信 ≠ 致全体表决者;督办 ≠ 分派;所办分派 ≠ 院士分派)
62
- - [ ] 系统/框架自己发出的反馈,**发送者**是否是框架自己(而不是"该成员发给自己",
63
- 那会被自消息校验拒掉、静默失效)?
64
- - [ ] 一次提示词里,**同一条消息是否只出现一次**?(先 ack 再构造,否则会投递两遍)
65
- - [ ] 会议提示里"其他人的发言"是否只列**别人**、且用真实代号?
66
- - [ ] 表决提示里的对象、陈述、历史票是否与该对象真正对应?
67
- - [ ] 转述/中继的消息(提议开会、增聘、临时工意见、反对分派)是否署**真实发起人**?
68
- - [ ] 分派/督办里"由谁分派""谁督办"是否与真实调用者一致?
69
- - [ ] 人可读镜像(编制表、任务板、会议纪要、辩论录、结案)里的代号/职位/雇主是否正确?
70
- - [ ] **回执契约**:框架实际处理的每个字段,是否都在 `replySpec` 里出现且按职位裁剪?
71
- (藏起来的字段 = 不可发现的通道)
72
-
73
- ### 1.5 时序与幂等
74
-
75
- - [ ] 重放同一事件(`subagent/end` 重复、重复提议)会不会产生**重复**的交互内容?
76
- - [ ] 成员被解雇/失败后,还会不会收到后续消息?
77
- - [ ] 会话重建(resume)后的提示词,措辞是否与"新入职"区分开?
78
-
79
- ### 1.6 静态提示词面:人设 ↔ 注册表
80
-
81
- 1.1–1.5 检查的是**运行时逐条发出的**提示词;还有一层**静态**提示词面必须一起核对——
82
- `agent.cordis.yml` 的 persona 行。它是主代理收到的**唯一**一份"有哪些工具、能调哪些参数"的清单,
83
- 而所有 e2e 套件都直接 `apply(ctx)`,**从不加载 YAML**,因此对这一层完全盲。
84
-
85
- - [ ] 注册的**每一个工具**是否在 persona(`prefix` 与 `text` **两个**块)里出现?
86
- 没出现 = 代理**无法发现**该能力(连准确名字都猜不到)。
87
- - [ ] persona 里出现的**每一个** `vibe_*` 名字是否都真的注册了?
88
- 出现但未注册 = 代理会去调用一个必然失败的工具。
89
- - [ ] 新参数是否同时进了 persona 与工具 description 的参数表?
90
- (例:`formalVerify` / `leanCommand` / `leanArgs` / `leanTimeoutMs`)
91
- 参数没写进 prompt = 用户无法开启这个能力。
92
- - [ ] `prefix` 与 `text` 两个块是否**逐行一致**(只允许第 0 行不同)?
93
- 旧宿主读 `text`、新宿主读 `prefix`,两者漂移 = 不同宿主看到不同的工具面。
94
- - [ ] 工具**改名/删除**时 persona 是否同步?("persona 提到的名字 ⊆ 注册表"这条反向检查负责抓)
95
- - [ ] 人设里写的路径/模式名(`Formal/Lib`、`off|encourage|require`)是否与实现中的字符串**逐字**一致?
96
-
97
- `audit-persona-surface.test.mjs` 把上述各条变成断言("未文档化工具"用**显式快照**表示:
98
- 新增工具必须主动改快照、或在 persona 里写清);`audit-persona-sensitivity.mjs` 用 11 条探针
99
- 证明这套断言真的会变红。**v2.3.0 修的就是这一类缺陷**:三个 `*_lean_*` 工具无条件注册,
100
- 而 v2/v3/v4 的 persona 从未列出它们(只有 v5 列了),v4 的 `vibe_v4_set` 参数表也漏了
101
- `formalVerify`/`leanCommand`/`leanArgs`/`leanTimeoutMs`——当时**所有既有套件全绿**。
102
-
103
- ### 1.7 语义映射:把"事实"映射成"字段值"时不能张冠李戴
104
-
105
- §1.6 查的是"名字/路径/参数有没有写对";这一节查**语义有没有写反**。它比漏写更危险:提示词读起来
106
- 通顺、完整,而套件往往只断言"包含某句话"就能全绿。
107
-
108
- - [ ] 每一个"发现 X 就投 Y"的映射是否**语义正确**?**真实事故**:Lean 形式化与命题原文不一致时,
109
- 提示词要求"投 0"——而 0 的含义是"**命题为假**"。于是"形式化写错了"被记成"命题被证伪",
110
- 在布尔一致规则下直接被写进 `Verified/` 标注**假**:用来求真更严格的机制,反而**伪造出
111
- 一个错误的否定结论**。现在改为独立一档 `defect`(撤回证明 + 进待办 + 不定论)。
112
- - [ ] **反向情形**的指令是否也在?("只有独立于该证据也能确定时才可否决"这类边界必须写出来,
113
- 否则代理只会照字面把"证据不合格"当成"结论为假"。)
114
- - [ ] 提示词承诺的**框架行为**是否真的实现了?**真实事故**:v2 的提示词让代理"在回执的 `formal`
115
- 字段写明难度判断",但回执契约里没有这个字段、框架也从不解析它——代理的判断**静默消失**,
116
- 而套件只断言"那句话存在",177 条断言全绿却守着一个**死通道**。
117
- - [ ] 提示词承诺的**强度档位**是否与实现一致?(`encourage` 档没有门禁,就**不能**声称
118
- "框架会搁置本次裁定";只改文字不改机制 = 骗代理。)
119
- - [ ] 每个档位(`off` / `encourage` / `require`)的注入文本是否**各自**被人读过?只读一个档位等于没读
120
- ——首版 `require` 档的门禁措辞从未进入任何语料。
121
-
122
- ### 1.8 四套同构:任何语义修正必须**四套同步**
123
-
124
- v2/v3/v4/v5 是**同构实现**(同一份契约、四份独立代码,刻意零共享)。这带来一个特有的缺陷类别:
125
- **修正只落到一套**。真实事故(2.3.1 → 2.3.2):
126
-
127
- - `defect` 的"`encourage` 档不得声称框架会强制搁置"这条修正只改了 v5,另外三套仍无条件宣称
128
- "本次裁定**不定论**"——承诺了 `encourage` 档根本无法强制的行为;
129
- - "撤回归档证明"在 v5 是**删除 → 校验真的没了 → 否则覆盖撤回说明**,另外三套只做尽力删除
130
- (两套静默吞掉失败、一套不校验)——宿主删不掉时,已撤回的证明仍留在 `Verified/Lean/<id>.lean`
131
- 这个"大家找证明"的位置;
132
- - `formal` 回执通道在 v2/v3 有 `formalOn()` 守卫,v4/v5 没有——`off` 档可被残留回执写入状态。
133
- - **「随手一次动作不得撤销已成立的证明」这条规则在四套里各写了一遍,却只在一套里被断言过**(v4 的
134
- `formalSetRun`):确认轮实测发现 v2 的 `used` **回执**通道无条件降级 `passed`(同一条规则的另一条
135
- 路径),v3 只保留 `passed` 而把 `blocked` 打回 `attempted`(门禁被重新关上)。同一语义在**多条路径**
136
- 上要逐条核对:工具路径、回执路径、归档路径各自独立。
137
-
138
- 所以:
139
-
140
- - [ ] 任何语义/措辞修正,**逐一核对四套**(含各自的 `实现方案.md`、persona、语料与断言);
141
- - [ ] 参数、工具名、字段名、路径、错误码这类**表面**已有静态守卫
142
- (`audit-persona-surface` / `audit-spec-traceability` / `audit-prompt-invariants`);
143
- 但**语义**(承诺强度、失败回退、边界条件)没有静态守卫,必须靠"把四套**渲染后的提示词/行为**
144
- 并排对照"来查——随包语料就是为这个准备的;
145
- - [ ] 用**同一份输入**驱动四套,比较**可观测结果**(记录落库、文件是否撤回、门禁是否放行),
146
- 而不是比较代码长得像不像。
147
-
148
- ### 1.10 id 映射不许"猜":能从权威来源拿,就不要从名字解析
149
-
150
- 两套 id 空间(对象 id ↔ 验证 id)之间**只能有一处映射**(v2 的 `formalObjectIdOf`),但"一处"不等于"正确":
151
- 从**名字**反推归属,一旦名字本身带有会被当成后缀的部分(对象 id `p-ineq-s1` 长得就像"`p-ineq` 的第 1 个解法"),
152
- 就会指向**另一个对象**——而且后果往往是破坏性的(把别人的归档证明撤回)。
153
-
154
- - [ ] 该映射是否优先使用**权威来源**(框架生成 id 时就知道的归属)?
155
- - [ ] 权威来源不在内存时(resume 早期),记录里是否**持久化**了 `objectId` 可查?
156
- - [ ] **从用户可编辑的状态文件里读出来的"权威值"是否先做了自洽校验?** 状态文件(如
157
- `VibeMath_State/formal.json`)是可以被人改的:**真实事故(2.3.7)**——某条记录的 `objectId`
158
- 被写成 r 形(`r-pX`)时,"验证 id → 对象 id"会映射到**它自己**,于是"两套 id 一起写"退化成
159
- 只写验证侧、对象侧仍是 `passed`。修法:锚点必须自洽(不以 `r-` 开头)才可用,读与写两处都校验;
160
- 灵敏度探针见 `_oneoff/probe-v2-anchor-guard.mjs`(去掉守卫 → 用例变红)。
161
- - [ ] 字符串解析是否只作为**兜底**,而不是唯一手段?
162
- - [ ] **真实事故(2.3.6)**:`formalObjectIdOf` 无条件剥离 `-sN/-pfN/-rfN`,于是命题 `pAmb-s1` 的
163
- 验证 id `r-pAmb-s1` 被解析成对象 `pAmb`;一句 `defect` 于是降级并**撤回了 `pAmb` 的归档证明**,
164
- 而真正有问题的 `pAmb-s1` 仍然 `passed`。修前用 6 条断言复现,修后 348/0。
165
-
166
- ### 1.9 工具参数 schema:文档写了 ≠ 工具收得下
167
-
168
- §1.6 查的是"persona 里有没有写",这一节查**工具的参数 schema 收不收得下**。本仓库所有工具 schema 都由
169
- `objParams` 收口,并以 `additionalProperties:false` **关闭**:schema 没列出的键会被任何遵守 schema 的
170
- provider **直接拒绝**。于是"提示词/规格/状态表都写着这个参数"完全可以在**功能根本打不开**的同时全绿。
171
-
172
- - [ ] 每个"可调参数"是否同时出现在:**设置工具的参数 schema**、发现面(`vibe_math_setup` 返回的
173
- `PARAM_SCHEMA` / v4·v5 的 status 参数表)、`<vibe root>/*setting.json` 模板、以及 persona 参数表?
174
- **真实事故(2.3.2 D1)**:v3 的四个 Lean 参数写进了 persona、规格与状态行,却**从未写进
175
- `vibe_math_set_params` 的 schema**;所有套件全绿(套件直接调 handler、绕过 schema),而用户
176
- **永远无法开启这个功能**。
177
- - [ ] schema **声明的**每个键,参数层是否**真的接收**?(v2/v3 的闸门是 `DEFAULT_PARAMS` 的键集、
178
- v4 是 `k in params`、v5 是 `normalizeParams` 的类型列表。)声明而不接收 = 调用返回 `{ok:true}`、
179
- 什么都不发生——最容易被读成"设置成功"的静默失效。
180
- - [ ] 同一工具被**注册两次**(v2/v3 各有"会话 handler 表"与"真实 `tools.register`"两份定义)时,
181
- 两份 schema 是否**逐键一致**?漂移会让其中一条路径上的功能不可达。
182
- - [ ] 新增/修改任何参数后,跑 `node audit-prompt-invariants.mjs`(I13/I14)与
183
- `node audit-prompt-invariants.mjs --self-probe`(证明这两条不变式**真的会变红**)。
184
-
185
- > **守卫为什么必须是"可被证伪的"**:I13/I14 用 `--self-probe` 在内存里注入真实缺陷形状
186
- > (v3 少一个 Lean 参数 / v4 多一个参数层不接收的键 / v5 的 `normalizeParams` 少一项),
187
- > 要求对应不变式**变红**,并要求**未变异的对照跑仍为绿**。没有对照的探针会把"脚本坏了"当成"守住了"。
188
-
189
- ---
190
-
191
- ### 2.1 逐条断言,而不是抽查
192
-
193
- 对**每一条**捕获到的提示词都跑一遍通用扫描(身份一致性、编制一致性、无垃圾、无重复投递),
194
- 而不是只挑几条看。给出**精确的期望值**(例如 4 名创始成员各自的 `[在册]` / m / 表决者数),
195
- 而不是"包含某些关键词"。
196
-
197
- ### 2.2 期望值要能证伪
198
-
199
- - 断言"此时**还没有**定论"时,**必须**同时断言"这一轮真的走完了"(例如断言阶段已推进到
200
- `debate`、或恰好收齐 N 张票)。否则预算不足会让"还没有"**无条件成立**,
201
- 无论规则被破坏成什么样。
202
- - 断言顺序:先确认前置状态已达成,再断言结果。
203
-
204
- ### 2.3 用例互相隔离
205
-
206
- 每个用例用**独立的会话根/工作区**,并在结束时暂停它。
207
- 否则一个用例遗留的心跳/会议/验证会污染下一个用例(表现为"某个用例莫名其妙收不到唤醒")。
208
-
209
- ### 2.4 保留交互语料(**强制交付物**)
210
-
211
- 测试脚本除了断言,还必须把**框架真正发出的每一条提示词原文**落盘:
212
-
213
- - 机器可读(JSON)+ 人可读(Markdown);
214
- - 覆盖全部交互类型(入职、重建、常规轮、心跳、表决初评/辩论、会议、提议、各类框头、框架提示、失败);
215
- - 把工作区路径归一化(如 `<WS>`),使语料**确定性、可 diff**;
216
- - **归一化必须大小写与分隔符无关**:Windows 下 `os.tmpdir()` 可能给出与插件渲染路径**不同大小写**
217
- 的同一目录,`split(WS)` 会漏掉全局根(如 `<VibeMath 根>/Formal/Lib`)的绝对路径——语料因此
218
- **每次运行都变**(临时目录名一变就 diff 一大片)并**泄露本机路径**。真实事故,已修;
219
- - **时间戳也要归一化**:提示词表头自带 `### YYYY-MM-DD hh:mm:ss|<成员>`,不归一化就**逐次都变**,
220
- diff 完全失去意义(真实事故:v5 语料里 54 处时间戳)。判据很简单——**连续跑两次,哈希必须相同**;
221
- - **随包发布**,作为人工复核提示词正确性的入口——复核者不必去翻会话日志。
222
-
223
- ### 2.5 每个不变式都要有灵敏度探针
224
-
225
- - [ ] 每个"提示词/交互"不变式,都有一条探针**故意打破它**,并要求对应套件**变红**。
226
- - [ ] **探针必须真的启动被测套件**:检查工作目录/路径解析。
227
- 曾因工作目录被百分号转义(Windows 中文路径 + `URL.pathname`)导致子进程**全部启动失败**,
228
- "非零退出"被当成"探测成功"——整份审计是假的。
229
- - [ ] **探针必须被目标套件真正读取**:曾因某个 e2e 套件**不读** `V5_PLUGIN` 环境变量,
230
- 针对它的探针跑的是**未变异**的插件,恒为绿。
231
- - [ ] **变异必须真的改变行为**:很多守卫是互相遮蔽的,删掉其中一个其实是**语义惰性**的
232
- (例如同一规则在三处重复检查,只削弱一处仍会被另外两处挡住)。
233
- 这类变异不能做探针——它会让审计误报"盲点"。
234
- - [ ] 探针**不得引入语法错误**:语法错误导致的非零退出同样是"假红"。
235
- 每条变异都应能被 `node --check` 通过。
236
- - [ ] 变异必须替换锚点的**全部**出现:`String.prototype.replace` 只替换第一处。同一份契约常在
237
- 两处发出(例如 v5 的回执契约同时出现在 `replySpec` 与表决提示词里),只改一处时套件
238
- **合理地保持绿色**——那是**假盲点**,比漏测更误导(会让人去"修"一个本来就是对的不变式)。
239
- 锚点声明了几次出现,就要替换几次;脚本应对此显式断言。
240
- - [ ] 守 **persona 静态面**的套件也要有探针,且该套件必须支持"指向副本"的环境变量
241
- (`audit-persona-surface.test.mjs` 的 `PERSONA_ROOT`):探针变异的是**副本**,
242
- 套件不读这个变量就永远在跑原始文件、恒为绿(同 §2.5 第二条)。
243
- 探针脚本还应在开跑前先确认"**未变异**的副本是绿的",否则它探测到的可能是覆盖机制本身。
244
- - [ ] 对**不可达**的守卫(设计上互斥、永远不会进入的分支),**不要**留一个永远为绿的探针;
245
- 要么写行为可观测的等价探针,要么显式删除并在脚本里注明原因。
246
-
247
- ---
248
-
249
- ## 3. 一个高效的补充手段:**把不变式写成文档/图,再拿它去对代码**
250
-
251
- 本次修复中最后找到的一个真实缺陷(会议与验证的"互斥"只做了单向)就是这么被发现的:
252
- 为 v5 画架构图时,被迫把"会议与验证互斥"写成一句**明确的不变式**,然后拿这句话去逐条对照代码 ——
253
- 发现 `startMeeting` 有守卫、`armNextVerify` 没有。单看任何一处代码都不觉得有问题,
254
- 是"写下不变式 → 双向核对"这个动作把它逼了出来。
255
-
256
- 所以:
257
-
258
- - [ ] 画架构图/写规格时,**每一条箭头与每一个"互斥/永不/必须"的措辞都要落到代码里核对**,
259
- 特别是**成对出现的关系**(互斥、双向、唯一、幂等)——只做一半是最常见的形态;
260
- - [ ] 文档里凡是出现"二者互斥""永不同时""必然"这类断言,都要问一句**它在代码里由谁保证**;
261
- 若答案只在一个方向上成立,那就是缺陷;
262
- - [ ] 相对顺序**不可观测**的地方(因为互斥)不要写成精确的优先级列表假装精确,
263
- 而要写明"二者互斥,故顺序无关"。
264
-
265
- ---
266
-
267
- ## 4. 第三优先:其余(沿用既有做法)
268
-
269
- - [ ] 静态自检:调用了但未定义的函数、未声明的参数键、不存在的会话 API 方法、
270
- 已文档化但从未抛出的错误码、遗留的开发标记。
271
- - [ ] 需求可追溯:方案里列出的工具名、参数名、理念条目,代码里是否都存在。
272
- - [ ] 失败路径:看门狗、幂等、崩溃恢复、降级后端、配额、越权。
273
- - [ ] 全量回归:**所有**历史套件,且逐个检查退出码(不要用管道截断输出,
274
- 管道会吞掉退出码或造成 EPIPE)。**用 `node run-tests.mjs` 并行跑**(并发 = min(4, 核数)),
275
- 它会打印每项耗时、wall/sum、加速比与最慢几项——**先看时间再决定策略**,基线见
276
- [`docs/test-timing.md`](docs/test-timing.md)。若某个套件远慢于基线,先查它是否在等一个
277
- **永远不会发生的条件**(真实事故:v2 套件 186 s,主因是一次 `tick(1100)` 嵌在
278
- "从不提前退出"的 16 次循环里)。
279
- - [ ] **产物是异步写出来的**:驱动循环("安静即停")可能在框架把卡片/状态文件落盘之前就退出,
280
- 而 `assert(existsSync(f))` **不会抛**、紧随其后的 `readFileSync(f)` 会抛 `ENOENT` —— 一次抖动
281
- 就变成**未捕获异常**,整个套件中止、后面所有断言全部丢失(真实事故:并行跑时
282
- `e2e-v4-fixes` 崩在 `Verified/命题/m-meth.md`,12 个用例只跑了 2 个)。写法:
283
- 先 `await waitFor(()=>existsSync(f), 3000)`,再用**防御性读**(`try{…}catch{return ''}`)
284
- ——缺文件必须是**一条干净的断言失败**,绝不能是崩溃。参见该套件里的 `readArtifact`。
285
- - [ ] 发布产物自证:不是"publish 退出 0"就算完成——要从 registry 取回 tarball,
286
- 核对 shasum、逐字节比对插件、确认修复标记存在、并在**已发布包内**跑一遍套件。
287
- - [ ] **每个"自己动手抹注释/抹字符串"的脚本都要有解析级自检**:这类扫描器一旦读错词法
288
- (最典型:正则字面量里的引号被当成字符串开头),它脚下**所有**静态检查都会静默失明。
289
- 真实事故:`audit-prompt-invariants.mjs`(2.3.2 完全不认正则 → 四套源码抹完直接语法错误)与
290
- `audit-v5-integrity.mjs`(不认正则 → v5 有 30 行读错、抹完不能解析)。自检写法:把抹除后的
291
- 真实源码写进临时文件跑 `node --check`,再加一个"引号在字符类里"的夹具。**并且在写结论前先量准**
292
- ——用"下一个引号"估算受影响区域曾让我把 30 行误报成 402 行。
293
- - [ ] **发布元数据交给序列化器**:不要用字符串替换手写 `package.json` 的字段(`compatNote` 里一个
294
- ASCII 双引号就能产出非法 JSON;`\\u0000` 之类转义在单引号字符串里会变成真控制字符)。
295
- 正确写法是 `JSON.parse` → 改属性 → `JSON.stringify(pkg, null, 2)`,写完再 `JSON.parse` 复核
296
- 并扫描控制字符。
297
-
298
- ---
299
-
300
- ## 5. 汇报要求
301
-
302
- - 缺陷要分**类**:"这一整类此前没有审计维度"比"修了 N 个 bug"更重要。
303
- - 假绿/假红必须单独说明:**审计本身失效**是最严重的发现。
304
- - 每条结论都要给出**可复核的证据**(真实日志片段、语料原文、可重跑的命令与结果)。
1
+ # 全面检查(audit)必查清单
2
+
3
+ > **本文件是强制流程,不是建议。** 每次对本仓库做"全面检查 / 找 bug / 优化"时,
4
+ > **必须**逐项过一遍本清单。清单的来历是一次真实事故:v2.1.0 在实地测试中,
5
+ > 每个成员收到的提示词都写错了身份,而当时 123 条断言 + 15 个灵敏度探针**全部通过**。
6
+ > 事故的根因不是某一处代码写错,而是**审计维度本身漏了一整类**——没有人检查"成员读到的文字"。
7
+
8
+ ---
9
+
10
+ ## 0. 铁律
11
+
12
+ 1. **成员读到的文字就是产品。** 提示词(人设 / 状态块 / 每轮问句 / 收件框头 / 回执契约)
13
+ 必须像函数返回值一样被断言。任何只检查工具返回值、投影状态、文件内容的测试,
14
+ 对提示词缺陷是**盲的**。
15
+ 2. **身份一律显式传递,绝不猜测。** 禁止从"最近唤醒的成员""列表第一个"之类的全局状态推断
16
+ "这段文字是写给谁的"。宁可显式失败(`V5_INTERNAL`),也不要生成一段身份错误的提示词。
17
+ 3. **测试脚本必须完整保留交互信息。** 断言之外,还要把交互原文落盘成可人工复核的语料
18
+ (见 §2.4)。"退出码 0"不是验收,"人能读到正确的原文"才是。
19
+ 4. **探针必须真的能变红。** 一个灵敏度探针如果始终为绿,说明它守的那条不变式**其实没被测到**——
20
+ 这比没有探针更糟(它给人虚假的安全感)。
21
+
22
+ ---
23
+
24
+ ## 1. 第一优先:提示词分配与交互内容
25
+
26
+ **这一节是最高优先级。历史上最致命的缺陷全部出在这里。**
27
+
28
+ ### 1.1 身份分配
29
+
30
+ - [ ] 每条提示词里的身份(`[状态] 你是 X`)= 这条提示词**实际发给的成员** X?
31
+ - [ ] 表头里的身份(`【… —— <职位> <代号>】`)= `[状态]` 里的身份?
32
+ - [ ] 随行人设(persona)指向的资料库路径 = 该成员自己的(`Members/<该成员>/`)?
33
+ - [ ] 人设里的代号/职位 = 实际职位?
34
+ - [ ] 有没有任何地方从**全局可变状态**推断身份(`currentMember`、`list[0]`、闭包快照)?
35
+ - [ ] 成员**创建/唤醒的时序**:构造提示词时,该成员是否**已经**被写入权威状态?
36
+ (先落盘、再构造;顺序反了会得到"上一个成员"的身份与"加入前"的编制)
37
+ - [ ] 全文有没有 `?` / `undefined` / `NaN` / `[object Object]` 之类占位垃圾?
38
+ - [ ] 一条提示词里是否**只有一个**身份声明(不能出现两段互相矛盾的身份)?
39
+
40
+ ### 1.2 编制 / 名额 / 门槛
41
+
42
+ - [ ] `[在册]` 是否包含读者**自己**?
43
+ - [ ] `[在册]` 是否与权威编制一致(不多、不少、无重复)?
44
+ - [ ] "有表决权者 N 人"是否等于 `[在册]` 里真正的表决者数(临时工不算)?
45
+ - [ ] `m = min(quorumCap, N)` 是否与状态块里报的一致?
46
+ - [ ] 未就位/失败/已除名的成员有没有被**如实**呈现(不能静默略去,也不能混进在册)?
47
+ - [ ] 轮次号在表头与状态块里是否一致?
48
+ - [ ] 编制变动(雇佣/解雇/增聘常驻)后,后续提示词是否立刻反映新编制?
49
+
50
+ ### 1.3 名称与领袖叙事
51
+
52
+ - [ ] 章程/人设里出现的**每一个代号**是否都真实存在于当前编制?
53
+ - [ ] 有没有**硬编码**的代号(例如写死 `acad`),而实际编制可能不同?
54
+ - [ ] 当某种职位根本不存在时(例如 `academician:false`),章程是否仍然声称它存在、
55
+ 要求成员向它汇报、或承诺它会派活?(这是"指向一个不存在的人")
56
+ - [ ] 章程是**入职快照**还是每次重建都改写?如果它自称"你入职时的…",就必须冻结在入职时。
57
+
58
+ ### 1.4 交互内容(消息 / 群聊 / 会议 / 辩论 / 分派 / 雇佣)
59
+
60
+ - [ ] 每条送达消息的**框头署名 = 真实发送者**?(尤其:所办 ≠ 院士;不能署"最近唤醒的成员")
61
+ - [ ] 框头**类型**是否正确?(私信 ≠ 致全体表决者;督办 ≠ 分派;所办分派 ≠ 院士分派)
62
+ - [ ] 系统/框架自己发出的反馈,**发送者**是否是框架自己(而不是"该成员发给自己",
63
+ 那会被自消息校验拒掉、静默失效)?
64
+ - [ ] 一次提示词里,**同一条消息是否只出现一次**?(先 ack 再构造,否则会投递两遍)
65
+ - [ ] 会议提示里"其他人的发言"是否只列**别人**、且用真实代号?
66
+ - [ ] 表决提示里的对象、陈述、历史票是否与该对象真正对应?
67
+ - [ ] 转述/中继的消息(提议开会、增聘、临时工意见、反对分派)是否署**真实发起人**?
68
+ - [ ] 分派/督办里"由谁分派""谁督办"是否与真实调用者一致?
69
+ - [ ] 人可读镜像(编制表、任务板、会议纪要、辩论录、结案)里的代号/职位/雇主是否正确?
70
+ - [ ] **回执契约**:框架实际处理的每个字段,是否都在 `replySpec` 里出现且按职位裁剪?
71
+ (藏起来的字段 = 不可发现的通道)
72
+
73
+ ### 1.5 时序与幂等
74
+
75
+ - [ ] 重放同一事件(`subagent/end` 重复、重复提议)会不会产生**重复**的交互内容?
76
+ - [ ] 成员被解雇/失败后,还会不会收到后续消息?
77
+ - [ ] 会话重建(resume)后的提示词,措辞是否与"新入职"区分开?
78
+
79
+ ### 1.6 静态提示词面:人设 ↔ 注册表
80
+
81
+ 1.1–1.5 检查的是**运行时逐条发出的**提示词;还有一层**静态**提示词面必须一起核对——
82
+ `agent.cordis.yml` 的 persona 行。它是主代理收到的**唯一**一份"有哪些工具、能调哪些参数"的清单,
83
+ 而所有 e2e 套件都直接 `apply(ctx)`,**从不加载 YAML**,因此对这一层完全盲。
84
+
85
+ - [ ] 注册的**每一个工具**是否在 persona(`prefix` 与 `text` **两个**块)里出现?
86
+ 没出现 = 代理**无法发现**该能力(连准确名字都猜不到)。
87
+ - [ ] persona 里出现的**每一个** `vibe_*` 名字是否都真的注册了?
88
+ 出现但未注册 = 代理会去调用一个必然失败的工具。
89
+ - [ ] 新参数是否同时进了 persona 与工具 description 的参数表?
90
+ (例:`formalVerify` / `leanCommand` / `leanArgs` / `leanTimeoutMs`)
91
+ 参数没写进 prompt = 用户无法开启这个能力。
92
+ - [ ] `prefix` 与 `text` 两个块是否**逐行一致**(只允许第 0 行不同)?
93
+ 旧宿主读 `text`、新宿主读 `prefix`,两者漂移 = 不同宿主看到不同的工具面。
94
+ - [ ] 工具**改名/删除**时 persona 是否同步?("persona 提到的名字 ⊆ 注册表"这条反向检查负责抓)
95
+ - [ ] 人设里写的路径/模式名(`Formal/Lib`、`off|encourage|require`)是否与实现中的字符串**逐字**一致?
96
+
97
+ `audit-persona-surface.test.mjs` 把上述各条变成断言("未文档化工具"用**显式快照**表示:
98
+ 新增工具必须主动改快照、或在 persona 里写清);`audit-persona-sensitivity.mjs` 用 11 条探针
99
+ 证明这套断言真的会变红。**v2.3.0 修的就是这一类缺陷**:三个 `*_lean_*` 工具无条件注册,
100
+ 而 v2/v3/v4 的 persona 从未列出它们(只有 v5 列了),v4 的 `vibe_v4_set` 参数表也漏了
101
+ `formalVerify`/`leanCommand`/`leanArgs`/`leanTimeoutMs`——当时**所有既有套件全绿**。
102
+
103
+ ### 1.7 语义映射:把"事实"映射成"字段值"时不能张冠李戴
104
+
105
+ §1.6 查的是"名字/路径/参数有没有写对";这一节查**语义有没有写反**。它比漏写更危险:提示词读起来
106
+ 通顺、完整,而套件往往只断言"包含某句话"就能全绿。
107
+
108
+ - [ ] 每一个"发现 X 就投 Y"的映射是否**语义正确**?**真实事故**:Lean 形式化与命题原文不一致时,
109
+ 提示词要求"投 0"——而 0 的含义是"**命题为假**"。于是"形式化写错了"被记成"命题被证伪",
110
+ 在布尔一致规则下直接被写进 `Verified/` 标注**假**:用来求真更严格的机制,反而**伪造出
111
+ 一个错误的否定结论**。现在改为独立一档 `defect`(撤回证明 + 进待办 + 不定论)。
112
+ - [ ] **反向情形**的指令是否也在?("只有独立于该证据也能确定时才可否决"这类边界必须写出来,
113
+ 否则代理只会照字面把"证据不合格"当成"结论为假"。)
114
+ - [ ] 提示词承诺的**框架行为**是否真的实现了?**真实事故**:v2 的提示词让代理"在回执的 `formal`
115
+ 字段写明难度判断",但回执契约里没有这个字段、框架也从不解析它——代理的判断**静默消失**,
116
+ 而套件只断言"那句话存在",177 条断言全绿却守着一个**死通道**。
117
+ - [ ] 提示词承诺的**强度档位**是否与实现一致?(`encourage` 档没有门禁,就**不能**声称
118
+ "框架会搁置本次裁定";只改文字不改机制 = 骗代理。)
119
+ - [ ] 每个档位(`off` / `encourage` / `require`)的注入文本是否**各自**被人读过?只读一个档位等于没读
120
+ ——首版 `require` 档的门禁措辞从未进入任何语料。
121
+
122
+ ### 1.8 四套同构:任何语义修正必须**四套同步**
123
+
124
+ v2/v3/v4/v5 是**同构实现**(同一份契约、四份独立代码,刻意零共享)。这带来一个特有的缺陷类别:
125
+ **修正只落到一套**。真实事故(2.3.1 → 2.3.2):
126
+
127
+ - `defect` 的"`encourage` 档不得声称框架会强制搁置"这条修正只改了 v5,另外三套仍无条件宣称
128
+ "本次裁定**不定论**"——承诺了 `encourage` 档根本无法强制的行为;
129
+ - "撤回归档证明"在 v5 是**删除 → 校验真的没了 → 否则覆盖撤回说明**,另外三套只做尽力删除
130
+ (两套静默吞掉失败、一套不校验)——宿主删不掉时,已撤回的证明仍留在 `Verified/Lean/<id>.lean`
131
+ 这个"大家找证明"的位置;
132
+ - `formal` 回执通道在 v2/v3 有 `formalOn()` 守卫,v4/v5 没有——`off` 档可被残留回执写入状态。
133
+ - **「随手一次动作不得撤销已成立的证明」这条规则在四套里各写了一遍,却只在一套里被断言过**(v4 的
134
+ `formalSetRun`):确认轮实测发现 v2 的 `used` **回执**通道无条件降级 `passed`(同一条规则的另一条
135
+ 路径),v3 只保留 `passed` 而把 `blocked` 打回 `attempted`(门禁被重新关上)。同一语义在**多条路径**
136
+ 上要逐条核对:工具路径、回执路径、归档路径各自独立。
137
+
138
+ 所以:
139
+
140
+ - [ ] 任何语义/措辞修正,**逐一核对四套**(含各自的 `实现方案.md`、persona、语料与断言);
141
+ - [ ] 参数、工具名、字段名、路径、错误码这类**表面**已有静态守卫
142
+ (`audit-persona-surface` / `audit-spec-traceability` / `audit-prompt-invariants`);
143
+ 但**语义**(承诺强度、失败回退、边界条件)没有静态守卫,必须靠"把四套**渲染后的提示词/行为**
144
+ 并排对照"来查——随包语料就是为这个准备的;
145
+ - [ ] 用**同一份输入**驱动四套,比较**可观测结果**(记录落库、文件是否撤回、门禁是否放行),
146
+ 而不是比较代码长得像不像。
147
+
148
+ ### 1.10 id 映射不许"猜":能从权威来源拿,就不要从名字解析
149
+
150
+ 两套 id 空间(对象 id ↔ 验证 id)之间**只能有一处映射**(v2 的 `formalObjectIdOf`),但"一处"不等于"正确":
151
+ 从**名字**反推归属,一旦名字本身带有会被当成后缀的部分(对象 id `p-ineq-s1` 长得就像"`p-ineq` 的第 1 个解法"),
152
+ 就会指向**另一个对象**——而且后果往往是破坏性的(把别人的归档证明撤回)。
153
+
154
+ - [ ] 该映射是否优先使用**权威来源**(框架生成 id 时就知道的归属)?
155
+ - [ ] 权威来源不在内存时(resume 早期),记录里是否**持久化**了 `objectId` 可查?
156
+ - [ ] **从用户可编辑的状态文件里读出来的"权威值"是否先做了自洽校验?** 状态文件(如
157
+ `VibeMath_State/formal.json`)是可以被人改的:**真实事故(2.3.7)**——某条记录的 `objectId`
158
+ 被写成 r 形(`r-pX`)时,"验证 id → 对象 id"会映射到**它自己**,于是"两套 id 一起写"退化成
159
+ 只写验证侧、对象侧仍是 `passed`。修法:锚点必须自洽(不以 `r-` 开头)才可用,读与写两处都校验;
160
+ 灵敏度探针见 `_oneoff/probe-v2-anchor-guard.mjs`(去掉守卫 → 用例变红)。
161
+ - [ ] 字符串解析是否只作为**兜底**,而不是唯一手段?
162
+ - [ ] **真实事故(2.3.6)**:`formalObjectIdOf` 无条件剥离 `-sN/-pfN/-rfN`,于是命题 `pAmb-s1` 的
163
+ 验证 id `r-pAmb-s1` 被解析成对象 `pAmb`;一句 `defect` 于是降级并**撤回了 `pAmb` 的归档证明**,
164
+ 而真正有问题的 `pAmb-s1` 仍然 `passed`。修前用 6 条断言复现,修后 348/0。
165
+
166
+ ### 1.9 工具参数 schema:文档写了 ≠ 工具收得下
167
+
168
+ §1.6 查的是"persona 里有没有写",这一节查**工具的参数 schema 收不收得下**。本仓库所有工具 schema 都由
169
+ `objParams` 收口,并以 `additionalProperties:false` **关闭**:schema 没列出的键会被任何遵守 schema 的
170
+ provider **直接拒绝**。于是"提示词/规格/状态表都写着这个参数"完全可以在**功能根本打不开**的同时全绿。
171
+
172
+ - [ ] 每个"可调参数"是否同时出现在:**设置工具的参数 schema**、发现面(`vibe_math_setup` 返回的
173
+ `PARAM_SCHEMA` / v4·v5 的 status 参数表)、`<vibe root>/*setting.json` 模板、以及 persona 参数表?
174
+ **真实事故(2.3.2 D1)**:v3 的四个 Lean 参数写进了 persona、规格与状态行,却**从未写进
175
+ `vibe_math_set_params` 的 schema**;所有套件全绿(套件直接调 handler、绕过 schema),而用户
176
+ **永远无法开启这个功能**。
177
+ - [ ] schema **声明的**每个键,参数层是否**真的接收**?(v2/v3 的闸门是 `DEFAULT_PARAMS` 的键集、
178
+ v4 是 `k in params`、v5 是 `normalizeParams` 的类型列表。)声明而不接收 = 调用返回 `{ok:true}`、
179
+ 什么都不发生——最容易被读成"设置成功"的静默失效。
180
+ - [ ] 同一工具被**注册两次**(v2/v3 各有"会话 handler 表"与"真实 `tools.register`"两份定义)时,
181
+ 两份 schema 是否**逐键一致**?漂移会让其中一条路径上的功能不可达。
182
+ - [ ] 新增/修改任何参数后,跑 `node tests/audit-prompt-invariants.mjs`(I13/I14)与
183
+ `node tests/audit-prompt-invariants.mjs --self-probe`(证明这两条不变式**真的会变红**)。
184
+
185
+ > **守卫为什么必须是"可被证伪的"**:I13/I14 用 `--self-probe` 在内存里注入真实缺陷形状
186
+ > (v3 少一个 Lean 参数 / v4 多一个参数层不接收的键 / v5 的 `normalizeParams` 少一项),
187
+ > 要求对应不变式**变红**,并要求**未变异的对照跑仍为绿**。没有对照的探针会把"脚本坏了"当成"守住了"。
188
+
189
+ ### 1.11 测试与脚本的路径必须可移植(不许写死本机路径)
190
+
191
+ 测试和脚本"在我机器上是绿的"不等于它们能跑。**真实事故(2.3.13 归档时发现)**:5 个测试/脚本把
192
+ 本机绝对路径写死在源码里(`D:/wd/vibemath开发/...`、`C:/Users/admin/...`),其中
193
+ `audit-fuzz-helpers.mjs` **是随包发布的**——对任何用户都跑不了,而仓库里从来没人发现,
194
+ 因为作者机器上它恰好是对的。
195
+
196
+ - [ ] 每个文件路径是否都由 `import.meta.url` / `npm root -g` / 环境变量**推导**出来,
197
+ 而不是写死盘符或用户名?(推导不出时应当**报出所有试过的候选路径**,而不是静默跳过。)
198
+ - [ ] 文件被移动/改名后,**引用它的每一处**是否都跟着改?(包括:`package.json` 的 `files`、
199
+ `run-tests.mjs` 这类 runner、守卫里按名字读套件的表、文档里的命令行、README 链接。)
200
+ - [ ] 命令行/相对链接是否需要**从仓库根执行才成立**?写清楚起点(如 `node tests/run-tests.mjs`),
201
+ 别留一个照抄就报"找不到文件"的用法行。
202
+ - [ ] **搬动之后必须有不依赖"跑绿了"的证据**:① 每个套件的断言条数与搬动前**逐项相同**;
203
+ ② 建一个 `git worktree` 拿改动前的检出,同一套件在两种布局各跑一遍,归一化路径/临时目录/耗时后
204
+ **逐行比对**(`_oneoff/layout-invariance.mjs`);③ 相对链接扫描 0 失效(`_oneoff/scan-links.mjs`)。
205
+ - [ ] **runner 本身也要跑一遍**:直接跑套件通过 ≠ 并行 runner 通过(2.3.13 就出现过
206
+ "26 个套件直接跑全绿、`run-tests.mjs` 因为按裸文件名 spawn 而全红")。发布门禁必须包含 runner。
207
+
208
+ ---
209
+
210
+ ### 2.1 逐条断言,而不是抽查
211
+
212
+ 对**每一条**捕获到的提示词都跑一遍通用扫描(身份一致性、编制一致性、无垃圾、无重复投递),
213
+ 而不是只挑几条看。给出**精确的期望值**(例如 4 名创始成员各自的 `[在册]` / m / 表决者数),
214
+ 而不是"包含某些关键词"。
215
+
216
+ ### 2.2 期望值要能证伪
217
+
218
+ - 断言"此时**还没有**定论"时,**必须**同时断言"这一轮真的走完了"(例如断言阶段已推进到
219
+ `debate`、或恰好收齐 N 张票)。否则预算不足会让"还没有"**无条件成立**,
220
+ 无论规则被破坏成什么样。
221
+ - 断言顺序:先确认前置状态已达成,再断言结果。
222
+
223
+ ### 2.3 用例互相隔离
224
+
225
+ 每个用例用**独立的会话根/工作区**,并在结束时暂停它。
226
+ 否则一个用例遗留的心跳/会议/验证会污染下一个用例(表现为"某个用例莫名其妙收不到唤醒")。
227
+
228
+ ### 2.4 保留交互语料(**强制交付物**)
229
+
230
+ 测试脚本除了断言,还必须把**框架真正发出的每一条提示词原文**落盘:
231
+
232
+ - 机器可读(JSON)+ 人可读(Markdown);
233
+ - 覆盖全部交互类型(入职、重建、常规轮、心跳、表决初评/辩论、会议、提议、各类框头、框架提示、失败);
234
+ - 把工作区路径归一化(如 `<WS>`),使语料**确定性、可 diff**;
235
+ - **归一化必须大小写与分隔符无关**:Windows 下 `os.tmpdir()` 可能给出与插件渲染路径**不同大小写**
236
+ 的同一目录,`split(WS)` 会漏掉全局根(如 `<VibeMath 根>/Formal/Lib`)的绝对路径——语料因此
237
+ **每次运行都变**(临时目录名一变就 diff 一大片)并**泄露本机路径**。真实事故,已修;
238
+ - **时间戳也要归一化**:提示词表头自带 `### YYYY-MM-DD hh:mm:ss|<成员>`,不归一化就**逐次都变**,
239
+ diff 完全失去意义(真实事故:v5 语料里 54 处时间戳)。判据很简单——**连续跑两次,哈希必须相同**;
240
+ - **随包发布**,作为人工复核提示词正确性的入口——复核者不必去翻会话日志。
241
+
242
+ ### 2.5 每个不变式都要有灵敏度探针
243
+
244
+ - [ ] 每个"提示词/交互"不变式,都有一条探针**故意打破它**,并要求对应套件**变红**。
245
+ - [ ] **探针必须真的启动被测套件**:检查工作目录/路径解析。
246
+ 曾因工作目录被百分号转义(Windows 中文路径 + `URL.pathname`)导致子进程**全部启动失败**,
247
+ "非零退出"被当成"探测成功"——整份审计是假的。
248
+ - [ ] **探针必须被目标套件真正读取**:曾因某个 e2e 套件**不读** `V5_PLUGIN` 环境变量,
249
+ 针对它的探针跑的是**未变异**的插件,恒为绿。
250
+ - [ ] **变异必须真的改变行为**:很多守卫是互相遮蔽的,删掉其中一个其实是**语义惰性**的
251
+ (例如同一规则在三处重复检查,只削弱一处仍会被另外两处挡住)。
252
+ 这类变异不能做探针——它会让审计误报"盲点"。
253
+ - [ ] 探针**不得引入语法错误**:语法错误导致的非零退出同样是"假红"。
254
+ 每条变异都应能被 `node --check` 通过。
255
+ - [ ] 变异必须替换锚点的**全部**出现:`String.prototype.replace` 只替换第一处。同一份契约常在
256
+ 两处发出(例如 v5 的回执契约同时出现在 `replySpec` 与表决提示词里),只改一处时套件
257
+ **合理地保持绿色**——那是**假盲点**,比漏测更误导(会让人去"修"一个本来就是对的不变式)。
258
+ 锚点声明了几次出现,就要替换几次;脚本应对此显式断言。
259
+ - [ ] 守 **persona 静态面**的套件也要有探针,且该套件必须支持"指向副本"的环境变量
260
+ (`audit-persona-surface.test.mjs` 的 `PERSONA_ROOT`):探针变异的是**副本**,
261
+ 套件不读这个变量就永远在跑原始文件、恒为绿(同 §2.5 第二条)。
262
+ 探针脚本还应在开跑前先确认"**未变异**的副本是绿的",否则它探测到的可能是覆盖机制本身。
263
+ - [ ] 对**不可达**的守卫(设计上互斥、永远不会进入的分支),**不要**留一个永远为绿的探针;
264
+ 要么写行为可观测的等价探针,要么显式删除并在脚本里注明原因。
265
+
266
+ ---
267
+
268
+ ## 3. 一个高效的补充手段:**把不变式写成文档/图,再拿它去对代码**
269
+
270
+ 本次修复中最后找到的一个真实缺陷(会议与验证的"互斥"只做了单向)就是这么被发现的:
271
+ 为 v5 画架构图时,被迫把"会议与验证互斥"写成一句**明确的不变式**,然后拿这句话去逐条对照代码 ——
272
+ 发现 `startMeeting` 有守卫、`armNextVerify` 没有。单看任何一处代码都不觉得有问题,
273
+ 是"写下不变式 → 双向核对"这个动作把它逼了出来。
274
+
275
+ 所以:
276
+
277
+ - [ ] 画架构图/写规格时,**每一条箭头与每一个"互斥/永不/必须"的措辞都要落到代码里核对**,
278
+ 特别是**成对出现的关系**(互斥、双向、唯一、幂等)——只做一半是最常见的形态;
279
+ - [ ] 文档里凡是出现"二者互斥""永不同时""必然"这类断言,都要问一句**它在代码里由谁保证**;
280
+ 若答案只在一个方向上成立,那就是缺陷;
281
+ - [ ] 相对顺序**不可观测**的地方(因为互斥)不要写成精确的优先级列表假装精确,
282
+ 而要写明"二者互斥,故顺序无关"。
283
+
284
+ ---
285
+
286
+ ## 4. 第三优先:其余(沿用既有做法)
287
+
288
+ - [ ] 静态自检:调用了但未定义的函数、未声明的参数键、不存在的会话 API 方法、
289
+ 已文档化但从未抛出的错误码、遗留的开发标记。
290
+ - [ ] 需求可追溯:方案里列出的工具名、参数名、理念条目,代码里是否都存在。
291
+ - [ ] 失败路径:看门狗、幂等、崩溃恢复、降级后端、配额、越权。
292
+ - [ ] 全量回归:**所有**历史套件,且逐个检查退出码(不要用管道截断输出,
293
+ 管道会吞掉退出码或造成 EPIPE)。**用 `node tests/run-tests.mjs` 并行跑**(并发 = min(4, 核数)),
294
+ 它会打印每项耗时、wall/sum、加速比与最慢几项——**先看时间再决定策略**,基线见
295
+ [`docs/test-timing.md`](test-timing.md)。若某个套件远慢于基线,先查它是否在等一个
296
+ **永远不会发生的条件**(真实事故:v2 套件 186 s,主因是一次 `tick(1100)` 嵌在
297
+ "从不提前退出"的 16 次循环里)。
298
+ - [ ] **产物是异步写出来的**:驱动循环("安静即停")可能在框架把卡片/状态文件落盘之前就退出,
299
+ 而 `assert(existsSync(f))` **不会抛**、紧随其后的 `readFileSync(f)` 会抛 `ENOENT` —— 一次抖动
300
+ 就变成**未捕获异常**,整个套件中止、后面所有断言全部丢失(真实事故:并行跑时
301
+ `e2e-v4-fixes` 崩在 `Verified/命题/m-meth.md`,12 个用例只跑了 2 个)。写法:
302
+ 先 `await waitFor(()=>existsSync(f), 3000)`,再用**防御性读**(`try{…}catch{return ''}`)
303
+ ——缺文件必须是**一条干净的断言失败**,绝不能是崩溃。参见该套件里的 `readArtifact`。
304
+ - [ ] 发布产物自证:不是"publish 退出 0"就算完成——要从 registry 取回 tarball,
305
+ 核对 shasum、逐字节比对插件、确认修复标记存在、并在**已发布包内**跑一遍套件。
306
+ - [ ] **每个"自己动手抹注释/抹字符串"的脚本都要有解析级自检**:这类扫描器一旦读错词法
307
+ (最典型:正则字面量里的引号被当成字符串开头),它脚下**所有**静态检查都会静默失明。
308
+ 真实事故:`audit-prompt-invariants.mjs`(2.3.2 完全不认正则 → 四套源码抹完直接语法错误)与
309
+ `audit-v5-integrity.mjs`(不认正则 → v5 有 30 行读错、抹完不能解析)。自检写法:把抹除后的
310
+ 真实源码写进临时文件跑 `node --check`,再加一个"引号在字符类里"的夹具。**并且在写结论前先量准**
311
+ ——用"下一个引号"估算受影响区域曾让我把 30 行误报成 402 行。
312
+ - [ ] **发布元数据交给序列化器**:不要用字符串替换手写 `package.json` 的字段(`compatNote` 里一个
313
+ ASCII 双引号就能产出非法 JSON;`\\u0000` 之类转义在单引号字符串里会变成真控制字符)。
314
+ 正确写法是 `JSON.parse` → 改属性 → `JSON.stringify(pkg, null, 2)`,写完再 `JSON.parse` 复核
315
+ 并扫描控制字符。
316
+
317
+ ---
318
+
319
+ ## 5. 汇报要求
320
+
321
+ - 缺陷要分**类**:"这一整类此前没有审计维度"比"修了 N 个 bug"更重要。
322
+ - 假绿/假红必须单独说明:**审计本身失效**是最严重的发现。
323
+ - 每条结论都要给出**可复核的证据**(真实日志片段、语料原文、可重跑的命令与结果)。