dsh-vibe-math 2.7.1 → 2.7.2

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 (47) hide show
  1. package/README.en.md +681 -690
  2. package/README.md +8 -17
  3. package/cordis.patch.yml +6 -6
  4. package/docs/AUDIT-CHECKLIST.md +518 -518
  5. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  6. package/docs/final-paper.md +172 -172
  7. package/docs/formal-verification.md +588 -588
  8. package/docs/math-computation.md +187 -163
  9. package/docs/release-notes/README.en.md +48 -0
  10. package/docs/release-notes/README.md +47 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
  12. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
  13. package/docs/release-notes/TEMPLATE.md +120 -0
  14. package/docs/test-timing.md +154 -154
  15. package/package.json +208 -203
  16. package/prompt-corpus-persona/persona-corpus.json +8 -8
  17. package/prompt-corpus-persona/persona-corpus.md +12 -12
  18. package/prompt-corpus-v2/formal-verify-v2.json +121 -109
  19. package/prompt-corpus-v2/formal-verify-v2.md +223 -107
  20. package/prompt-corpus-v3/formal-verify-v3.json +100 -76
  21. package/prompt-corpus-v3/formal-verify-v3.md +268 -70
  22. package/prompt-corpus-v4/formal-verify-v4.json +10 -10
  23. package/prompt-corpus-v4/formal-verify-v4.md +20 -20
  24. package/prompt-corpus-v5/prompt-corpus-v5.json +81 -81
  25. package/prompt-corpus-v5/prompt-corpus-v5.md +138 -128
  26. package/tests/audit-math-computation-contract.mjs +43 -1
  27. package/tests/audit-math-computation-sensitivity.mjs +2 -2
  28. package/tests/audit-v5-sensitivity.mjs +3 -1
  29. package/tests/e2e-v5-round2.test.mjs +114 -5
  30. package/tests/formal-verify-v2.test.mjs +38 -0
  31. package/tests/formal-verify-v3.test.mjs +33 -0
  32. package/tests/formal-verify-v4.test.mjs +87 -0
  33. package/tests/math-computation-shared.test.mjs +126 -1
  34. package/vibe-math-v2/agent.cordis.yml +2 -2
  35. package/vibe-math-v2/math-computation.js +122 -11
  36. package/vibe-math-v2/vibe-math-v2.js +75 -11
  37. package/vibe-math-v3/agent.cordis.yml +2 -2
  38. package/vibe-math-v3/math-computation.js +122 -11
  39. package/vibe-math-v3/vibe-math-v3.js +60 -13
  40. package/vibe-math-v4/agent.cordis.yml +4 -4
  41. package/vibe-math-v4/math-computation.js +122 -11
  42. package/vibe-math-v4/vibe-math-v4.js +55 -15
  43. package/vibe-math-v5/agent.cordis.yml +4 -4
  44. package/vibe-math-v5/math-computation.js +122 -11
  45. package/vibe-math-v5/vibe-math-v5.js +137 -28
  46. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1660 -1655
  47. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -1,518 +1,518 @@
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` 用 16 条探针
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` **关闭**。**但"关闭"的强度取决于注册路径**(v3 审计 M5
170
- 的更正,Round B 复核扩大):只有走宿主 `defineTool` 的工具,宿主才会用该 schema 校验实参、把未列出的键
171
- **直接拒绝**;`tools.register`(裸 `ToolDefinition`)只把 `parameters` 生成给模型的 schema,宿主**从不校验实参**,
172
- `additionalProperties:false` 在那里只是给模型看的**文档**。**四套预设全部走裸注册**
173
- (`grep defineTool vibe-math-v{2,3,4,5}.js` → 0 命中;`tools.register` 各 1–3 处),所以"schema 与参数层不一致
174
- 就静默成功(fail open)"对**每一套**都成立,不只是 v3。最典型的反例是 `vibe_math_setup` 公布 `mode`、而
175
- `vibe_math_set_params` 的 schema 里一度没有它:模型看不到这个键,写给它却仍会被 `sanitizeParams` 收下
176
- (该键已补进 schema,两条注册路径的键集现在一致)。
177
-
178
- - [ ] 每个"可调参数"是否同时出现在:**设置工具的参数 schema**、发现面(`vibe_math_setup` 返回的
179
- `PARAM_SCHEMA` / v4·v5 的 status 参数表)、`<vibe root>/*setting.json` 模板、以及 persona 参数表?
180
- **真实事故(2.3.2 D1)**:v3 的四个 Lean 参数写进了 persona、规格与状态行,却**从未写进
181
- `vibe_math_set_params` 的 schema**;所有套件全绿(套件直接调 handler、绕过 schema),而用户
182
- **永远无法开启这个功能**。
183
- - [ ] schema **声明的**每个键,参数层是否**真的接收**?(v2/v3 的闸门是 `DEFAULT_PARAMS` 的键集、
184
- v4 是 `k in params`、v5 是 `normalizeParams` 的类型列表。)声明而不接收 = 调用返回 `{ok:true}`、
185
- 什么都不发生——最容易被读成"设置成功"的静默失效。
186
- - [ ] 同一工具被**注册两次**(v2/v3 各有"会话 handler 表"与"真实 `tools.register`"两份定义)时,
187
- 两份 schema 是否**逐键一致**?漂移会让其中一条路径上的功能不可达。
188
- - [ ] 新增/修改任何参数后,跑 `node tests/audit-prompt-invariants.mjs`(I13/I14)与
189
- `node tests/audit-prompt-invariants.mjs --self-probe`(证明这两条不变式**真的会变红**)。
190
-
191
- > **守卫为什么必须是"可被证伪的"**:I13/I14 用 `--self-probe` 在内存里注入真实缺陷形状
192
- > (v3 少一个 Lean 参数 / v4 多一个参数层不接收的键 / v5 的 `normalizeParams` 少一项),
193
- > 要求对应不变式**变红**,并要求**未变异的对照跑仍为绿**。没有对照的探针会把"脚本坏了"当成"守住了"。
194
-
195
- ### 1.11 测试与脚本的路径必须可移植(不许写死本机路径)
196
-
197
- 测试和脚本"在我机器上是绿的"不等于它们能跑。**真实事故(2.3.13 归档时发现)**:5 个测试/脚本把
198
- 本机绝对路径写死在源码里(`D:/wd/vibemath开发/...`、`C:/Users/admin/...`),其中
199
- `audit-fuzz-helpers.mjs` 当时**是随包发布的**——对任何用户都跑不了,而仓库里从来没人发现,
200
- 因为作者机器上它恰好是对的。(那些路径后来都改成由 `import.meta.url` 推导;Round B 复核:`tests/`
201
- 下已经没有任何真实的写死盘符路径,而且 `audit-fuzz-helpers.mjs` **现在也不再随包发布**——
202
- 它不在 `package.json` 的 `files` 里,也不在 `npm pack --dry-run --json` 的清单里。)
203
-
204
- - [ ] 每个文件路径是否都由 `import.meta.url` / `npm root -g` / 环境变量**推导**出来,
205
- 而不是写死盘符或用户名?(推导不出时应当**报出所有试过的候选路径**,而不是静默跳过。)
206
- - [ ] 文件被移动/改名后,**引用它的每一处**是否都跟着改?(包括:`package.json` 的 `files`、
207
- `run-tests.mjs` 这类 runner、守卫里按名字读套件的表、文档里的命令行、README 链接。)
208
- - [ ] 新增/改名 `tests/` 下的脚本后,确认 **`run-tests.mjs` 真的收集了它**:runner 收集 `tests/` 下
209
- **所有** `.mjs`(`*.test.mjs` 是套件,其余是探针),只有登记进 `NEEDS_ARGS` 的脚本才会被跳过,
210
- 而那个登记会在**开发检出**(存在 `.git`)里被逐项校验——条目失效就直接报错退出,所以改名不能
211
- 悄悄把一条守卫踢出发布门禁。参数变体(同一脚本跑多次,如 `audit-registration.mjs` 四个预设)走
212
- `VARIANTS`。
213
- - [ ] 命令行/相对链接是否需要**从仓库根执行才成立**?写清楚起点(如 `node tests/run-tests.mjs`),
214
- 别留一个照抄就报"找不到文件"的用法行。
215
- - [ ] **搬动之后必须有不依赖"跑绿了"的证据**:① 每个套件的断言条数与搬动前**逐项相同**;
216
- ② 建一个 `git worktree` 拿改动前的检出,同一套件在两种布局各跑一遍,归一化路径/临时目录/耗时后
217
- **逐行比对**(`_oneoff/layout-invariance.mjs`);③ 相对链接扫描 0 失效(`_oneoff/scan-links.mjs`)。
218
- - [ ] 讲"全套件 / 门禁 / 多少次全绿"时,是否区分了**随包发布面**与**仓库**?`package.json` 的 `files`
219
- 只发 `tests/` 的 **23 项**(其中 `.test.mjs` **11** 个),完整门禁(`node tests/run-tests.mjs`,
220
- 当前 **65 项 = 44 套件 + 21 探针/变体**)只在开发检出里成立。两边的清单见
221
- `docs/test-timing.md` §1.1;发布物里的 runner 会把缺失/跳过项**打印出来**(不会静默少跑),
222
- 所以"安装用户照文档跑得到全套件"这类说法必须避免。
223
- - [ ] **runner 本身也要跑一遍**:直接跑套件通过 ≠ 并行 runner 通过(2.3.13 就出现过
224
- "26 个套件直接跑全绿、`run-tests.mjs` 因为按裸文件名 spawn 而全红")。发布门禁必须包含 runner。
225
-
226
- ---
227
-
228
- ### 2.1 逐条断言,而不是抽查
229
-
230
- 对**每一条**捕获到的提示词都跑一遍通用扫描(身份一致性、编制一致性、无垃圾、无重复投递),
231
- 而不是只挑几条看。给出**精确的期望值**(例如 4 名创始成员各自的 `[在册]` / m / 表决者数),
232
- 而不是"包含某些关键词"。
233
-
234
- ### 2.2 期望值要能证伪
235
-
236
- - 断言"此时**还没有**定论"时,**必须**同时断言"这一轮真的走完了"(例如断言阶段已推进到
237
- `debate`、或恰好收齐 N 张票)。否则预算不足会让"还没有"**无条件成立**,
238
- 无论规则被破坏成什么样。
239
- - 断言顺序:先确认前置状态已达成,再断言结果。
240
-
241
- ### 2.3 用例互相隔离
242
-
243
- 每个用例用**独立的会话根/工作区**,并在结束时暂停它。
244
- 否则一个用例遗留的心跳/会议/验证会污染下一个用例(表现为"某个用例莫名其妙收不到唤醒")。
245
-
246
- ### 2.4 保留交互语料(**强制交付物**)
247
-
248
- 测试脚本除了断言,还必须把**框架真正发出的每一条提示词原文**落盘:
249
-
250
- - 机器可读(JSON)+ 人可读(Markdown);
251
- - 覆盖全部交互类型(入职、重建、常规轮、心跳、表决初评/辩论、会议、提议、各类框头、框架提示、失败);
252
- - 把工作区路径归一化(如 `<WS>`),使语料**确定性、可 diff**;
253
- - **归一化必须大小写与分隔符无关**:Windows 下 `os.tmpdir()` 可能给出与插件渲染路径**不同大小写**
254
- 的同一目录,`split(WS)` 会漏掉全局根(如 `<VibeMath 根>/Formal/Lib`)的绝对路径——语料因此
255
- **每次运行都变**(临时目录名一变就 diff 一大片)并**泄露本机路径**。真实事故,已修;
256
- - **时间戳也要归一化**:提示词表头自带 `### YYYY-MM-DD hh:mm:ss|<成员>`,不归一化就**逐次都变**,
257
- diff 完全失去意义(真实事故:v5 语料里 54 处时间戳)。判据很简单——**连续跑两次,哈希必须相同**;
258
- - **随包发布**,作为人工复核提示词正确性的入口——复核者不必去翻会话日志。
259
-
260
- ### 2.5 每个不变式都要有灵敏度探针
261
-
262
- - [ ] 每个"提示词/交互"不变式,都有一条探针**故意打破它**,并要求对应套件**变红**。
263
- - [ ] **探针必须真的启动被测套件**:检查工作目录/路径解析。
264
- 曾因工作目录被百分号转义(Windows 中文路径 + `URL.pathname`)导致子进程**全部启动失败**,
265
- "非零退出"被当成"探测成功"——整份审计是假的。
266
- - [ ] **探针必须被目标套件真正读取**:曾因某个 e2e 套件**不读** `V5_PLUGIN` 环境变量,
267
- 针对它的探针跑的是**未变异**的插件,恒为绿。
268
- - [ ] **变异必须真的改变行为**:很多守卫是互相遮蔽的,删掉其中一个其实是**语义惰性**的
269
- (例如同一规则在三处重复检查,只削弱一处仍会被另外两处挡住)。
270
- 这类变异不能做探针——它会让审计误报"盲点"。
271
- - [ ] 探针**不得引入语法错误**:语法错误导致的非零退出同样是"假红"。
272
- 每条变异都应能被 `node --check` 通过。
273
- - [ ] 变异必须替换锚点的**全部**出现:`String.prototype.replace` 只替换第一处。同一份契约常在
274
- 两处发出(例如 v5 的回执契约同时出现在 `replySpec` 与表决提示词里),只改一处时套件
275
- **合理地保持绿色**——那是**假盲点**,比漏测更误导(会让人去"修"一个本来就是对的不变式)。
276
- 锚点声明了几次出现,就要替换几次;脚本应对此显式断言。
277
- - [ ] 守 **persona 静态面**的套件也要有探针,且该套件必须支持"指向副本"的环境变量
278
- (`audit-persona-surface.test.mjs` 的 `PERSONA_ROOT`):探针变异的是**副本**,
279
- 套件不读这个变量就永远在跑原始文件、恒为绿(同 §2.5 第二条)。
280
- 探针脚本还应在开跑前先确认"**未变异**的副本是绿的",否则它探测到的可能是覆盖机制本身。
281
- - [ ] 对**不可达**的守卫(设计上互斥、永远不会进入的分支),**不要**留一个永远为绿的探针;
282
- 要么写行为可观测的等价探针,要么显式删除并在脚本里注明原因。
283
-
284
- ---
285
-
286
- ## 3. 一个高效的补充手段:**把不变式写成文档/图,再拿它去对代码**
287
-
288
- 本次修复中最后找到的一个真实缺陷(会议与验证的"互斥"只做了单向)就是这么被发现的:
289
- 为 v5 画架构图时,被迫把"会议与验证互斥"写成一句**明确的不变式**,然后拿这句话去逐条对照代码 ——
290
- 发现 `startMeeting` 有守卫、`armNextVerify` 没有。单看任何一处代码都不觉得有问题,
291
- 是"写下不变式 → 双向核对"这个动作把它逼了出来。
292
-
293
- 所以:
294
-
295
- - [ ] 画架构图/写规格时,**每一条箭头与每一个"互斥/永不/必须"的措辞都要落到代码里核对**,
296
- 特别是**成对出现的关系**(互斥、双向、唯一、幂等)——只做一半是最常见的形态;
297
- - [ ] 文档里凡是出现"二者互斥""永不同时""必然"这类断言,都要问一句**它在代码里由谁保证**;
298
- 若答案只在一个方向上成立,那就是缺陷;
299
- - [ ] 相对顺序**不可观测**的地方(因为互斥)不要写成精确的优先级列表假装精确,
300
- 而要写明"二者互斥,故顺序无关"。
301
-
302
- ---
303
-
304
- ## 4. 第三优先:其余(沿用既有做法)
305
-
306
- - [ ] 静态自检:调用了但未定义的函数、未声明的参数键、不存在的会话 API 方法、
307
- 已文档化但从未抛出的错误码、遗留的开发标记。
308
- - [ ] 需求可追溯:方案里列出的工具名、参数名、理念条目,代码里是否都存在。
309
- - [ ] 失败路径:看门狗、幂等、崩溃恢复、降级后端、配额、越权。
310
- - [ ] 全量回归:**所有**历史套件,且逐个检查退出码(不要用管道截断输出,
311
- 管道会吞掉退出码或造成 EPIPE)。**用 `node tests/run-tests.mjs` 并行跑**(并发 = min(4, 核数)),
312
- 它会打印每项耗时、wall/sum、加速比与最慢几项——**先看时间再决定策略**,基线见
313
- [`docs/test-timing.md`](test-timing.md)。若某个套件远慢于基线,先查它是否在等一个
314
- **永远不会发生的条件**(真实事故:v2 套件 186 s,主因是一次 `tick(1100)` 嵌在
315
- "从不提前退出"的 16 次循环里)。
316
- - [ ] **产物是异步写出来的**:驱动循环("安静即停")可能在框架把卡片/状态文件落盘之前就退出,
317
- 而 `assert(existsSync(f))` **不会抛**、紧随其后的 `readFileSync(f)` 会抛 `ENOENT` —— 一次抖动
318
- 就变成**未捕获异常**,整个套件中止、后面所有断言全部丢失(真实事故:并行跑时
319
- `e2e-v4-fixes` 崩在 `Verified/命题/m-meth.md`,12 个用例只跑了 2 个)。写法:
320
- 先 `await waitFor(()=>existsSync(f), 3000)`,再用**防御性读**(`try{…}catch{return ''}`)
321
- ——缺文件必须是**一条干净的断言失败**,绝不能是崩溃。参见该套件里的 `readArtifact`。
322
- - [ ] 发布产物自证:不是"publish 退出 0"就算完成——要从 registry 取回 tarball,
323
- 核对 shasum、逐字节比对插件、确认修复标记存在、并在**已发布包内**跑一遍套件。
324
- - [ ] **每个"自己动手抹注释/抹字符串"的脚本都要有解析级自检**:这类扫描器一旦读错词法
325
- (最典型:正则字面量里的引号被当成字符串开头),它脚下**所有**静态检查都会静默失明。
326
- 真实事故:`audit-prompt-invariants.mjs`(2.3.2 完全不认正则 → 四套源码抹完直接语法错误)与
327
- `audit-v5-integrity.mjs`(不认正则 → v5 有 30 行读错、抹完不能解析)。自检写法:把抹除后的
328
- 真实源码写进临时文件跑 `node --check`,再加一个"引号在字符类里"的夹具。**并且在写结论前先量准**
329
- ——用"下一个引号"估算受影响区域曾让我把 30 行误报成 402 行。
330
- - [ ] **发布元数据交给序列化器**:不要用字符串替换手写 `package.json` 的字段(`compatNote` 里一个
331
- ASCII 双引号就能产出非法 JSON;`\\u0000` 之类转义在单引号字符串里会变成真控制字符)。
332
- 正确写法是 `JSON.parse` → 改属性 → `JSON.stringify(pkg, null, 2)`,写完再 `JSON.parse` 复核
333
- 并扫描控制字符。
334
-
335
- ---
336
-
337
- ## 5. 汇报要求
338
-
339
- - 缺陷要分**类**:"这一整类此前没有审计维度"比"修了 N 个 bug"更重要。
340
- - 假绿/假红必须单独说明:**审计本身失效**是最严重的发现。
341
- - 每条结论都要给出**可复核的证据**(真实日志片段、语料原文、可重跑的命令与结果)。
342
-
343
- ---
344
-
345
- ## 6. 发布说明(Release Notes / GitHub Release)格式
346
-
347
- **中英双语、先中文后英文,只写"给用户看的信息"。** 工程过程(探针结果、断言条数、事故复盘、行号与
348
- 内部编号)属于 `_oneoff/audit-findings-*.md` 台账与提交信息,**不进**发布说明。
349
-
350
- 固定骨架(只有版本号、标题与正文随版本变,**节名不变**):
351
-
352
- ```markdown
353
- # dsh-vibe-math <版本> — 发布说明(中文)
354
-
355
- > 上一版:<版本>。<一句话:这一版是干什么的、是否影响行为。>
356
-
357
- ## 概览
358
- ## 变更
359
- ## 兼容性
360
- ## 升级
361
-
362
- ---
363
-
364
- # dsh-vibe-math <版本> — Release Notes (English)
365
-
366
- > Previous: <version>. <One sentence: what this release is and whether behaviour changes.>
367
-
368
- ## Overview
369
- ## Changes
370
- ## Compatibility
371
- ## Upgrade
372
- ```
373
-
374
- - [ ] 两个 H1 都在;**英文那份的 H1 必须含 `English`**(人工检查:本仓库**没有任何 CI 工作流**,
375
- `scripts/` 只有 `build-preset-rows.mjs`,不存在所谓的"发布门禁阶段 1c");
376
- - [ ] 中文部分 ≥ 150 个汉字(同样人工检查),英文部分不得残留未翻译段落;
377
- - [ ] 每条变更都回答"**对使用者有什么影响**"——不写内部编号、探针名、断言条数;
378
- - [ ] 「兼容性」明确回答:四套预设的字节/行为是否变化、参数与工具面是否变化;
379
- - [ ] 「升级」给出可直接复制的命令(`npm i dsh-vibe-math@latest`;若 profile 里把版本钉死了,
380
- 写出对应的 `dsh plugin --profile <name> add dsh-vibe-math@latest`);
381
- - [ ] **发布前把草稿交给用户确认**(在对话里展示全文,或用提问工具);用户同意或给出修改意见后再发布;
382
- - [ ] GitHub Release 的正文 = 这份文件本身(`gh release create --notes-file <该文件>`),标题中英并列。
383
-
384
- ---
385
-
386
- ## 7. 中英双语 README 与英文版架构图(2.3.16 起)
387
-
388
- - **默认语言**:`README.md` 为中文(默认、GitHub 首屏),`README.en.md` 为英文;两文顶部各有一行切换器
389
- (中文版 `[English](README.en.md) | 中文`,英文版 `English | [中文](README.md)`)。不建 `README.zh.md`。
390
- - **结构同构**:标题层级序列、表格行数、代码块数、图片数量、本地链接集合必须一一对应;
391
- 唯一允许的差异是四张架构图各自指向 `-en` 版本(按序号配对校验)。
392
- - **文件与产物名不翻译**:真实目录/文件名(`示例图/…`、`docs/架构图.md`、`Verified/命题/`、`Logs/报告.md`)、
393
- 真实 JSON 键(`已解决`、`正确概率`)、真实格式锚点(`- ID/类型/状态/概率/…`、`### 解法/证明/证伪 N|标题|概率X|状态Y`)、
394
- 会话重建标记 `【会话重建 —— <role> <id>】`、v2/v3 真实 md 字段名(`可信断言`、`上级体系`、`子方法`)**保持原样**,
395
- 首次出现处可加括注;除此以外的正文必须是英文(`tests/audit-readme-bilingual.test.mjs` 的 CJK 预算兜底)。
396
- - **架构图**:四张英文图 `示例图/框架图-v{2,3,4,5}-en.svg` 只被 `README.en.md` 引用,中文图不受影响;
397
- v4/v5 用现有零依赖生成器加 `--lang=en`(`docs/generate_framework_diagram_v4.mjs`),
398
- v2/v3 用独立的零依赖 Node 生成器 `docs/generate_framework_diagram_v{2,3}_en.mjs`(中文 v2/v3 海报仍由 matplotlib 脚本产出)。
399
- 英文图内只允许保留上述"真实字面量"的中文,`tests/audit-diagram-assets.test.mjs` 按图核 CJK 预算。
400
- - **历史文档必须标注适用范围**:描述旧版本布局的文档(如 v1 时期的 `docs/架构图.md`)要在开头写明它描述的是哪一版,
401
- 不能在被 README 当作"当前详解"引用时静默误导(`qs.csv`、`Pending_Verification/` 等 v1 名称在 v2 起已不存在)。
402
-
403
- ---
404
-
405
- ## 8. 宿主版本线适配与发布流程实测教训(2.4.0 起)
406
-
407
- ### 8.1 一条版本线一个机制:先定"形态",再谈"版本"
408
-
409
- DSH 的 **agent preset 交付方式**在 0.1.6 → 0.1.7 之间换过一次,且**旧机制是被彻底删除**的:
410
-
411
- | 版本线 | preset 形态 | 本文档对应做法 |
412
- |---|---|---|
413
- | ≤ 0.1.6 | 目录 `<DSH_HOME>/.agent-presets/<id>/{agent.cordis.yml,preset.yml,…}` | `installer.js` 复制(`detectPresetMechanism` 判为 `directory`) |
414
- | ≥ 0.1.7 | 组合行 `@deepseek-ai/dsh-agent-preset`(`agentPresets.register({id,plugins,…})`) | `cordis.patch.yml` 里的声明行(判为 `rows`,安装器跳过目录同步) |
415
-
416
- **判定必须用能力,不要用版本号**:`agentPresets` 服务在两条线上都存在但语义不同(旧线是目录扫描器、没有 `register`),
417
- 所以判定改成读 **loader 入口树**里有没有 `@deepseek-ai/dsh-agent-preset`(或其 registry)——这在任何插件激活之前就可见,
418
- 而"读服务"在 bundle 被单独激活(`dsh plugin add`)时可能还没就绪。**旧线上"装了但看不见"正是本版修掉的那类缺陷。**
419
-
420
- ### 8.2 组合行声明的四条硬约束(都是实测出来的)
421
-
422
- 1. **`dsh.bundle.patch` 必须是字符串**。数组形态只有 ≥ 0.1.7 接受;0.1.5/0.1.6 会把该值直接送进 `path.join`,
423
- 结果是整个 profile **起不来**(`ERR_INVALID_ARG_TYPE`)。要多个 patch 就合并成一个文件。
424
- 2. **声明行不要命名宿主包**(如 `@deepseek-ai/dsh-agent-preset`):旧线上该包不存在,行会 `failed to import`,
425
- 每次启动都报一条激活失败。改成本包自己的模块(`preset-declaration.js`),它在旧线上**什么都不注册且不报错**。
426
- 3. **声明行不能用 `disabled: !!js` 做版本门**:patch 表达式里 **`ctx.get(...)` 会抛**(实测:常量、`typeof ctx` 正常,
427
- 任何服务查询都让入口永远不初始化,报 "N entries did not activate")。要按宿主能力分流,就分到**代码里**(运行时读服务)。
428
- 4. **声明行里的 `name` 不会被改写成文件 URL**(app-boot 只递归 group 的 config),loader 会拿**profile 目录**去解析相对路径
429
- → `./vibe-math-vN.js` 必 ENOENT、整个 preset 拒绝挂载。必须用**包内子路径**(`dsh-vibe-math/vibe-math-vN/vibe-math-vN.js`)+ `exports` 放行。
430
-
431
- ### 8.3 不要在宿主的"历史事件"面上存自己的状态
432
-
433
- 会话日志是**只增不改**的,但**读它是严格的**:持久化层遇到不认识的 `type` 会**拒绝加载整个会话**,除非写入方标了
434
- `ignorable: true`——而 `Session.append` **无法**设置该字段(只有构造种子能)。所以"把插件状态写进会话日志"这条路
435
- 会让**用户自己的会话在下次恢复时打不开**(静默:写入成功、失败发生在之后的加载)。自研状态请落在自己的文件里(v5 即如此)。
436
-
437
- ### 8.4 发布流程:`npm publish` 返回 202 不是失败
438
-
439
- - `npm publish` 得到 **HTTP 202** + "Your package is being processed…" 时,registry 正在**异步处理**;此时 `npm view` /
440
- `dist-tags` / 版本 manifest / tarball 都可能是 404 或旧值,**持续数分钟**。
441
- - 期间重发会得到 **409 `Cannot publish over previously staged version`**——这句措辞会让人误以为被扣下等 2FA 批准
442
- (`npm stage list` 实测为空,并非 staged)。**正确处理:等待并轮询 packument**(`dist-tags.latest` + `/<pkg>/<ver>` + tarball HEAD),
443
- **绝不在此时改版本号或内容**。
444
- - `dsh plugin --profile <p> add` 在 Windows 上可能**在 pnpm 完成后才崩**(`0xC0000409`,报 "pnpm failed"):
445
- **不要信退出码**,查 `package.json` 的 spec、lockfile、`node_modules` 三处是否一致,不一致就只对齐 manifest。
446
-
447
- ---
448
-
449
- ## 9. 插件目录安全基线(收录进 awesome-ai-plugins 起)
450
-
451
- 本项目被 [awesome-ai-plugins](https://github.com/hashgraph-online/awesome-ai-plugins) 收录,其 CI 对**源仓库**跑
452
- [HOL Guard 扫描器](https://github.com/hashgraph-online/hol-guard):**评分 ≥80 且不得有 critical/high 发现**才允许合并。
453
- 下面这些是硬门槛,改动代码时不要踩回去(首次提交实测 73 分、2 个 high,被拦下):
454
-
455
- | 扫描规则 | 要求 | 本仓库的做法 |
456
- |---|---|---|
457
- | `DANGEROUS_DYNAMIC_EXECUTION` (high) | 仓库里**不得**出现 `eval` / `new Function` —— 包括测试与脚本 | 需要"执行真实实现"的套件改为 `await import(<preset 模块>)` + 预设导出的 `__testHelpers` 测试接缝,不再把源码文本编译成函数;`node:vm` 同样属于动态执行,不要用它绕过 |
458
- | `SECURITY_MD_MISSING` (low) | 仓库根有 `SECURITY.md`(支持版本 + 上报流程) | `SECURITY.md`(含本包攻击面说明:无网络、无凭据、无动态执行、只写工作区) |
459
- | `DEPENDABOT_MISSING` (low) | `.github/dependabot.yml` | 本仓库**不配置**(§9.1:低危可接受,实测 94 分仍通过) |
460
- | `DEPENDENCY_LOCKFILE_MISSING` (medium) | 有 `package.json` 就要有 lockfile | `package-lock.json`(本包零运行时依赖,lockfile 只有根条目) |
461
- | 未固定的 Actions | 第三方 Action 必须钉到 commit SHA | 本仓库**无工作流**(§9.2);将来若引入,必须钉 SHA 且保留版本注释,`permissions: contents: read`、`persist-credentials: false` |
462
-
463
- 另外目录要求的**包侧**条件(`CONTRIBUTING.md`):`package.json` 里要有可安装的 `dsh.bundle`,插件要导出 `apply(ctx)`,
464
- 并且**在本仓库 README 里写明包名或 `dsh plugin add` 命令**、条目里用**准确的仓库 URL**。
465
-
466
- 自查方式(无需 Python 环境也能跑:用 DSH 自带运行时里的 Python 装一次 `plugin-scanner`):
467
-
468
- ```powershell
469
- & "<python>" -m pip install plugin-scanner
470
- & "<python>\Scripts\plugin-scanner.exe" lint "D:\wd\vibemath开发\Vibe-Mathematics" --format json # 逐条规则
471
- & "<python>\Scripts\plugin-scanner.exe" scan "D:\wd\vibemath开发\Vibe-Mathematics" # 评分
472
- ```
473
-
474
- 本地 `plugin-scanner` 的评分与他们 CI 的评分**实测一致**(首次提交时两边同为 73),所以本地复现即可预判 CI;本仓库当前为 **94 分、1 个低危**(见 §9.1),达标线是 §9.2 的「≥80 且无 critical/high」。
475
- 目录侧的检查(格式、字母序、条目可解析)由他们的 `check-alphabetical.py` + `validate-contribution.py` 在 PR 上跑:
476
- 本仓库的条目必须放在 `### DeepSeek Harness Plugins` 小节里、按**显示名小写**字母序(`Vibe-Mathematics` 排在 `humanizer-ru` 之后)。
477
-
478
- ---
479
-
480
- ### 9.1 为什么本仓库**没有** Dependabot(别再顺手加回去)
481
-
482
- 扫描器有一条 `DEPENDABOT_MISSING`(低危,值 5 分),最初为凑分加过 `.github/dependabot.yml`,随后它立刻开出一批
483
- "补课" PR(`actions/checkout` 4.2.2 → 7.0.1、扫描器 action 补丁升级)。实测各档分数后决定**移除**:
484
-
485
- | 配置 | 分数 | 通过(阈值 80 + 无 high) |
486
- |---|---|---|
487
- | 全都有(含 dependabot) | 100 | ✅ |
488
- | 去掉 `dependabot.yml` | **95** | ✅ |
489
- | 再去掉 `SECURITY.md` | 91 | ✅ |
490
- | 再去掉 `package-lock.json` | 86 | ✅ |
491
-
492
- 结论:**低危项不是必需的**——阈值 80,去掉它仍有 95 分。本仓库零运行时依赖,依赖面只有 `.github/workflows` 里两个 Action,
493
- 为它们维护一个每周开 PR 的机器人,**换来的是维护者每次都要做决定**,不划算。改为:
494
-
495
- - ~~`node scripts/check-action-versions.mjs`(npm script:`npm run check:actions`)~~(当时的做法,已随工作流一并删除,见 §9.2)——它曾读出工作流里所有
496
- `uses: owner/repo@<sha> # vX.Y.Z`,用 GitHub API 比对上游最新 release,落后的打印**可直接替换的整行**,退出码 1;
497
- - ~~**例行检查与每次发布时跑一次**(发布清单里带上),需要升级时由维护者显式改这一行,而不是被动接 PR。~~(同上,已删除)
498
-
499
- 顺带记住两条与它相关的既有纪律:工作流里的第三方 Action **必须钉 commit SHA**(目录扫描器与供应链原则都要求),
500
- 且钉法要**同时保留版本注释**(否则别人无法判断钉的是哪版)。
501
-
502
- ### 9.2 本仓库**不使用** GitHub Actions(2.4.0 之后又撤掉了)
503
-
504
- 办理收录时我一度加过 `.github/workflows/plugin-security-scan.yml`(在**本仓库**每次推送时跑同一个扫描器)。
505
- 事后复核目录规则才发现**这是可选的**:`CONTRIBUTING.md` 与 `SCANNER_GUIDE.md` 都写明
506
- "Scanner CI in the source repository is optional for listing",必需的只是**他们 CI 对我们仓库的集中扫描**——
507
- 而那个扫描是在他们的 runner 上克隆本仓库后执行的,**不运行本仓库的任何工作流**,所以本仓库有没有工作流与能否收录无关。
508
-
509
- 实测代价/收益:保留工作流 95 分,删掉 94 分(阈值 80);它唯一的真实成本是"长期盯两个 Action 的版本"。
510
- 故**撤掉**:本仓库现在没有任何 Actions,也没有 dependabot;`.github/` 目录不再存在。
511
-
512
- 取而代之的纪律(**别再顺手加回 CI**):
513
-
514
- - **例行检查与每次发布前,在本地跑一次扫描器**(与目录同款,规则一致):
515
- `plugin-scanner lint <repo> --format json`(逐条规则)/ `… scan <repo>`(评分);
516
- 要求:**分数 ≥80 且无 critical/high**。命令与安装方式见 §9 顶部。
517
- - Action 版本核对脚本已随之删除(没有工作流就没有可核对的 pin)。**若将来有人(包括我)真要引入工作流**:
518
- 第三方 Action 必须钉 commit SHA 并保留版本注释,同时把本节的结论一并更新,并说明这次引入解决了什么原文中非做不可的问题。
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` 用 16 条探针
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` **关闭**。**但"关闭"的强度取决于注册路径**(v3 审计 M5
170
+ 的更正,Round B 复核扩大):只有走宿主 `defineTool` 的工具,宿主才会用该 schema 校验实参、把未列出的键
171
+ **直接拒绝**;`tools.register`(裸 `ToolDefinition`)只把 `parameters` 生成给模型的 schema,宿主**从不校验实参**,
172
+ `additionalProperties:false` 在那里只是给模型看的**文档**。**四套预设全部走裸注册**
173
+ (`grep defineTool vibe-math-v{2,3,4,5}.js` → 0 命中;`tools.register` 各 1–3 处),所以"schema 与参数层不一致
174
+ 就静默成功(fail open)"对**每一套**都成立,不只是 v3。最典型的反例是 `vibe_math_setup` 公布 `mode`、而
175
+ `vibe_math_set_params` 的 schema 里一度没有它:模型看不到这个键,写给它却仍会被 `sanitizeParams` 收下
176
+ (该键已补进 schema,两条注册路径的键集现在一致)。
177
+
178
+ - [ ] 每个"可调参数"是否同时出现在:**设置工具的参数 schema**、发现面(`vibe_math_setup` 返回的
179
+ `PARAM_SCHEMA` / v4·v5 的 status 参数表)、`<vibe root>/*setting.json` 模板、以及 persona 参数表?
180
+ **真实事故(2.3.2 D1)**:v3 的四个 Lean 参数写进了 persona、规格与状态行,却**从未写进
181
+ `vibe_math_set_params` 的 schema**;所有套件全绿(套件直接调 handler、绕过 schema),而用户
182
+ **永远无法开启这个功能**。
183
+ - [ ] schema **声明的**每个键,参数层是否**真的接收**?(v2/v3 的闸门是 `DEFAULT_PARAMS` 的键集、
184
+ v4 是 `k in params`、v5 是 `normalizeParams` 的类型列表。)声明而不接收 = 调用返回 `{ok:true}`、
185
+ 什么都不发生——最容易被读成"设置成功"的静默失效。
186
+ - [ ] 同一工具被**注册两次**(v2/v3 各有"会话 handler 表"与"真实 `tools.register`"两份定义)时,
187
+ 两份 schema 是否**逐键一致**?漂移会让其中一条路径上的功能不可达。
188
+ - [ ] 新增/修改任何参数后,跑 `node tests/audit-prompt-invariants.mjs`(I13/I14)与
189
+ `node tests/audit-prompt-invariants.mjs --self-probe`(证明这两条不变式**真的会变红**)。
190
+
191
+ > **守卫为什么必须是"可被证伪的"**:I13/I14 用 `--self-probe` 在内存里注入真实缺陷形状
192
+ > (v3 少一个 Lean 参数 / v4 多一个参数层不接收的键 / v5 的 `normalizeParams` 少一项),
193
+ > 要求对应不变式**变红**,并要求**未变异的对照跑仍为绿**。没有对照的探针会把"脚本坏了"当成"守住了"。
194
+
195
+ ### 1.11 测试与脚本的路径必须可移植(不许写死本机路径)
196
+
197
+ 测试和脚本"在我机器上是绿的"不等于它们能跑。**真实事故(2.3.13 归档时发现)**:5 个测试/脚本把
198
+ 本机绝对路径写死在源码里(`D:/wd/vibemath开发/...`、`C:/Users/admin/...`),其中
199
+ `audit-fuzz-helpers.mjs` 当时**是随包发布的**——对任何用户都跑不了,而仓库里从来没人发现,
200
+ 因为作者机器上它恰好是对的。(那些路径后来都改成由 `import.meta.url` 推导;Round B 复核:`tests/`
201
+ 下已经没有任何真实的写死盘符路径,而且 `audit-fuzz-helpers.mjs` **现在也不再随包发布**——
202
+ 它不在 `package.json` 的 `files` 里,也不在 `npm pack --dry-run --json` 的清单里。)
203
+
204
+ - [ ] 每个文件路径是否都由 `import.meta.url` / `npm root -g` / 环境变量**推导**出来,
205
+ 而不是写死盘符或用户名?(推导不出时应当**报出所有试过的候选路径**,而不是静默跳过。)
206
+ - [ ] 文件被移动/改名后,**引用它的每一处**是否都跟着改?(包括:`package.json` 的 `files`、
207
+ `run-tests.mjs` 这类 runner、守卫里按名字读套件的表、文档里的命令行、README 链接。)
208
+ - [ ] 新增/改名 `tests/` 下的脚本后,确认 **`run-tests.mjs` 真的收集了它**:runner 收集 `tests/` 下
209
+ **所有** `.mjs`(`*.test.mjs` 是套件,其余是探针),只有登记进 `NEEDS_ARGS` 的脚本才会被跳过,
210
+ 而那个登记会在**开发检出**(存在 `.git`)里被逐项校验——条目失效就直接报错退出,所以改名不能
211
+ 悄悄把一条守卫踢出发布门禁。参数变体(同一脚本跑多次,如 `audit-registration.mjs` 四个预设)走
212
+ `VARIANTS`。
213
+ - [ ] 命令行/相对链接是否需要**从仓库根执行才成立**?写清楚起点(如 `node tests/run-tests.mjs`),
214
+ 别留一个照抄就报"找不到文件"的用法行。
215
+ - [ ] **搬动之后必须有不依赖"跑绿了"的证据**:① 每个套件的断言条数与搬动前**逐项相同**;
216
+ ② 建一个 `git worktree` 拿改动前的检出,同一套件在两种布局各跑一遍,归一化路径/临时目录/耗时后
217
+ **逐行比对**(`_oneoff/layout-invariance.mjs`);③ 相对链接扫描 0 失效(`_oneoff/scan-links.mjs`)。
218
+ - [ ] 讲"全套件 / 门禁 / 多少次全绿"时,是否区分了**随包发布面**与**仓库**?`package.json` 的 `files`
219
+ 只发 `tests/` 的 **23 项**(其中 `.test.mjs` **11** 个),完整门禁(`node tests/run-tests.mjs`,
220
+ 当前 **65 项 = 44 套件 + 21 探针/变体**)只在开发检出里成立。两边的清单见
221
+ `docs/test-timing.md` §1.1;发布物里的 runner 会把缺失/跳过项**打印出来**(不会静默少跑),
222
+ 所以"安装用户照文档跑得到全套件"这类说法必须避免。
223
+ - [ ] **runner 本身也要跑一遍**:直接跑套件通过 ≠ 并行 runner 通过(2.3.13 就出现过
224
+ "26 个套件直接跑全绿、`run-tests.mjs` 因为按裸文件名 spawn 而全红")。发布门禁必须包含 runner。
225
+
226
+ ---
227
+
228
+ ### 2.1 逐条断言,而不是抽查
229
+
230
+ 对**每一条**捕获到的提示词都跑一遍通用扫描(身份一致性、编制一致性、无垃圾、无重复投递),
231
+ 而不是只挑几条看。给出**精确的期望值**(例如 4 名创始成员各自的 `[在册]` / m / 表决者数),
232
+ 而不是"包含某些关键词"。
233
+
234
+ ### 2.2 期望值要能证伪
235
+
236
+ - 断言"此时**还没有**定论"时,**必须**同时断言"这一轮真的走完了"(例如断言阶段已推进到
237
+ `debate`、或恰好收齐 N 张票)。否则预算不足会让"还没有"**无条件成立**,
238
+ 无论规则被破坏成什么样。
239
+ - 断言顺序:先确认前置状态已达成,再断言结果。
240
+
241
+ ### 2.3 用例互相隔离
242
+
243
+ 每个用例用**独立的会话根/工作区**,并在结束时暂停它。
244
+ 否则一个用例遗留的心跳/会议/验证会污染下一个用例(表现为"某个用例莫名其妙收不到唤醒")。
245
+
246
+ ### 2.4 保留交互语料(**强制交付物**)
247
+
248
+ 测试脚本除了断言,还必须把**框架真正发出的每一条提示词原文**落盘:
249
+
250
+ - 机器可读(JSON)+ 人可读(Markdown);
251
+ - 覆盖全部交互类型(入职、重建、常规轮、心跳、表决初评/辩论、会议、提议、各类框头、框架提示、失败);
252
+ - 把工作区路径归一化(如 `<WS>`),使语料**确定性、可 diff**;
253
+ - **归一化必须大小写与分隔符无关**:Windows 下 `os.tmpdir()` 可能给出与插件渲染路径**不同大小写**
254
+ 的同一目录,`split(WS)` 会漏掉全局根(如 `<VibeMath 根>/Formal/Lib`)的绝对路径——语料因此
255
+ **每次运行都变**(临时目录名一变就 diff 一大片)并**泄露本机路径**。真实事故,已修;
256
+ - **时间戳也要归一化**:提示词表头自带 `### YYYY-MM-DD hh:mm:ss|<成员>`,不归一化就**逐次都变**,
257
+ diff 完全失去意义(真实事故:v5 语料里 54 处时间戳)。判据很简单——**连续跑两次,哈希必须相同**;
258
+ - **随包发布**,作为人工复核提示词正确性的入口——复核者不必去翻会话日志。
259
+
260
+ ### 2.5 每个不变式都要有灵敏度探针
261
+
262
+ - [ ] 每个"提示词/交互"不变式,都有一条探针**故意打破它**,并要求对应套件**变红**。
263
+ - [ ] **探针必须真的启动被测套件**:检查工作目录/路径解析。
264
+ 曾因工作目录被百分号转义(Windows 中文路径 + `URL.pathname`)导致子进程**全部启动失败**,
265
+ "非零退出"被当成"探测成功"——整份审计是假的。
266
+ - [ ] **探针必须被目标套件真正读取**:曾因某个 e2e 套件**不读** `V5_PLUGIN` 环境变量,
267
+ 针对它的探针跑的是**未变异**的插件,恒为绿。
268
+ - [ ] **变异必须真的改变行为**:很多守卫是互相遮蔽的,删掉其中一个其实是**语义惰性**的
269
+ (例如同一规则在三处重复检查,只削弱一处仍会被另外两处挡住)。
270
+ 这类变异不能做探针——它会让审计误报"盲点"。
271
+ - [ ] 探针**不得引入语法错误**:语法错误导致的非零退出同样是"假红"。
272
+ 每条变异都应能被 `node --check` 通过。
273
+ - [ ] 变异必须替换锚点的**全部**出现:`String.prototype.replace` 只替换第一处。同一份契约常在
274
+ 两处发出(例如 v5 的回执契约同时出现在 `replySpec` 与表决提示词里),只改一处时套件
275
+ **合理地保持绿色**——那是**假盲点**,比漏测更误导(会让人去"修"一个本来就是对的不变式)。
276
+ 锚点声明了几次出现,就要替换几次;脚本应对此显式断言。
277
+ - [ ] 守 **persona 静态面**的套件也要有探针,且该套件必须支持"指向副本"的环境变量
278
+ (`audit-persona-surface.test.mjs` 的 `PERSONA_ROOT`):探针变异的是**副本**,
279
+ 套件不读这个变量就永远在跑原始文件、恒为绿(同 §2.5 第二条)。
280
+ 探针脚本还应在开跑前先确认"**未变异**的副本是绿的",否则它探测到的可能是覆盖机制本身。
281
+ - [ ] 对**不可达**的守卫(设计上互斥、永远不会进入的分支),**不要**留一个永远为绿的探针;
282
+ 要么写行为可观测的等价探针,要么显式删除并在脚本里注明原因。
283
+
284
+ ---
285
+
286
+ ## 3. 一个高效的补充手段:**把不变式写成文档/图,再拿它去对代码**
287
+
288
+ 本次修复中最后找到的一个真实缺陷(会议与验证的"互斥"只做了单向)就是这么被发现的:
289
+ 为 v5 画架构图时,被迫把"会议与验证互斥"写成一句**明确的不变式**,然后拿这句话去逐条对照代码 ——
290
+ 发现 `startMeeting` 有守卫、`armNextVerify` 没有。单看任何一处代码都不觉得有问题,
291
+ 是"写下不变式 → 双向核对"这个动作把它逼了出来。
292
+
293
+ 所以:
294
+
295
+ - [ ] 画架构图/写规格时,**每一条箭头与每一个"互斥/永不/必须"的措辞都要落到代码里核对**,
296
+ 特别是**成对出现的关系**(互斥、双向、唯一、幂等)——只做一半是最常见的形态;
297
+ - [ ] 文档里凡是出现"二者互斥""永不同时""必然"这类断言,都要问一句**它在代码里由谁保证**;
298
+ 若答案只在一个方向上成立,那就是缺陷;
299
+ - [ ] 相对顺序**不可观测**的地方(因为互斥)不要写成精确的优先级列表假装精确,
300
+ 而要写明"二者互斥,故顺序无关"。
301
+
302
+ ---
303
+
304
+ ## 4. 第三优先:其余(沿用既有做法)
305
+
306
+ - [ ] 静态自检:调用了但未定义的函数、未声明的参数键、不存在的会话 API 方法、
307
+ 已文档化但从未抛出的错误码、遗留的开发标记。
308
+ - [ ] 需求可追溯:方案里列出的工具名、参数名、理念条目,代码里是否都存在。
309
+ - [ ] 失败路径:看门狗、幂等、崩溃恢复、降级后端、配额、越权。
310
+ - [ ] 全量回归:**所有**历史套件,且逐个检查退出码(不要用管道截断输出,
311
+ 管道会吞掉退出码或造成 EPIPE)。**用 `node tests/run-tests.mjs` 并行跑**(并发 = min(4, 核数)),
312
+ 它会打印每项耗时、wall/sum、加速比与最慢几项——**先看时间再决定策略**,基线见
313
+ [`docs/test-timing.md`](test-timing.md)。若某个套件远慢于基线,先查它是否在等一个
314
+ **永远不会发生的条件**(真实事故:v2 套件 186 s,主因是一次 `tick(1100)` 嵌在
315
+ "从不提前退出"的 16 次循环里)。
316
+ - [ ] **产物是异步写出来的**:驱动循环("安静即停")可能在框架把卡片/状态文件落盘之前就退出,
317
+ 而 `assert(existsSync(f))` **不会抛**、紧随其后的 `readFileSync(f)` 会抛 `ENOENT` —— 一次抖动
318
+ 就变成**未捕获异常**,整个套件中止、后面所有断言全部丢失(真实事故:并行跑时
319
+ `e2e-v4-fixes` 崩在 `Verified/命题/m-meth.md`,12 个用例只跑了 2 个)。写法:
320
+ 先 `await waitFor(()=>existsSync(f), 3000)`,再用**防御性读**(`try{…}catch{return ''}`)
321
+ ——缺文件必须是**一条干净的断言失败**,绝不能是崩溃。参见该套件里的 `readArtifact`。
322
+ - [ ] 发布产物自证:不是"publish 退出 0"就算完成——要从 registry 取回 tarball,
323
+ 核对 shasum、逐字节比对插件、确认修复标记存在、并在**已发布包内**跑一遍套件。
324
+ - [ ] **每个"自己动手抹注释/抹字符串"的脚本都要有解析级自检**:这类扫描器一旦读错词法
325
+ (最典型:正则字面量里的引号被当成字符串开头),它脚下**所有**静态检查都会静默失明。
326
+ 真实事故:`audit-prompt-invariants.mjs`(2.3.2 完全不认正则 → 四套源码抹完直接语法错误)与
327
+ `audit-v5-integrity.mjs`(不认正则 → v5 有 30 行读错、抹完不能解析)。自检写法:把抹除后的
328
+ 真实源码写进临时文件跑 `node --check`,再加一个"引号在字符类里"的夹具。**并且在写结论前先量准**
329
+ ——用"下一个引号"估算受影响区域曾让我把 30 行误报成 402 行。
330
+ - [ ] **发布元数据交给序列化器**:不要用字符串替换手写 `package.json` 的字段(`compatNote` 里一个
331
+ ASCII 双引号就能产出非法 JSON;`\\u0000` 之类转义在单引号字符串里会变成真控制字符)。
332
+ 正确写法是 `JSON.parse` → 改属性 → `JSON.stringify(pkg, null, 2)`,写完再 `JSON.parse` 复核
333
+ 并扫描控制字符。
334
+
335
+ ---
336
+
337
+ ## 5. 汇报要求
338
+
339
+ - 缺陷要分**类**:"这一整类此前没有审计维度"比"修了 N 个 bug"更重要。
340
+ - 假绿/假红必须单独说明:**审计本身失效**是最严重的发现。
341
+ - 每条结论都要给出**可复核的证据**(真实日志片段、语料原文、可重跑的命令与结果)。
342
+
343
+ ---
344
+
345
+ ## 6. 发布说明(Release Notes / GitHub Release)格式
346
+
347
+ **中英双语、先中文后英文,只写"给用户看的信息"。** 工程过程(探针结果、断言条数、事故复盘、行号与
348
+ 内部编号)属于 `_oneoff/audit-findings-*.md` 台账与提交信息,**不进**发布说明。
349
+
350
+ 固定骨架(只有版本号、标题与正文随版本变,**节名不变**):
351
+
352
+ ```markdown
353
+ # dsh-vibe-math <版本> — 发布说明(中文)
354
+
355
+ > 上一版:<版本>。<一句话:这一版是干什么的、是否影响行为。>
356
+
357
+ ## 概览
358
+ ## 变更
359
+ ## 兼容性
360
+ ## 升级
361
+
362
+ ---
363
+
364
+ # dsh-vibe-math <版本> — Release Notes (English)
365
+
366
+ > Previous: <version>. <One sentence: what this release is and whether behaviour changes.>
367
+
368
+ ## Overview
369
+ ## Changes
370
+ ## Compatibility
371
+ ## Upgrade
372
+ ```
373
+
374
+ - [ ] 两个 H1 都在;**英文那份的 H1 必须含 `English`**(人工检查:本仓库**没有任何 CI 工作流**,
375
+ `scripts/` 只有 `build-preset-rows.mjs`,不存在所谓的"发布门禁阶段 1c");
376
+ - [ ] 中文部分 ≥ 150 个汉字(同样人工检查),英文部分不得残留未翻译段落;
377
+ - [ ] 每条变更都回答"**对使用者有什么影响**"——不写内部编号、探针名、断言条数;
378
+ - [ ] 「兼容性」明确回答:四套预设的字节/行为是否变化、参数与工具面是否变化;
379
+ - [ ] 「升级」给出可直接复制的命令(`npm i dsh-vibe-math@latest`;若 profile 里把版本钉死了,
380
+ 写出对应的 `dsh plugin --profile <name> add dsh-vibe-math@latest`);
381
+ - [ ] **发布前把草稿交给用户确认**(在对话里展示全文,或用提问工具);用户同意或给出修改意见后再发布;
382
+ - [ ] GitHub Release 的正文 = 这份文件本身(`gh release create --notes-file <该文件>`),标题中英并列。
383
+
384
+ ---
385
+
386
+ ## 7. 中英双语 README 与英文版架构图(2.3.16 起)
387
+
388
+ - **默认语言**:`README.md` 为中文(默认、GitHub 首屏),`README.en.md` 为英文;两文顶部各有一行切换器
389
+ (中文版 `[English](README.en.md) | 中文`,英文版 `English | [中文](README.md)`)。不建 `README.zh.md`。
390
+ - **结构同构**:标题层级序列、表格行数、代码块数、图片数量、本地链接集合必须一一对应;
391
+ 唯一允许的差异是四张架构图各自指向 `-en` 版本(按序号配对校验)。
392
+ - **文件与产物名不翻译**:真实目录/文件名(`示例图/…`、`docs/架构图.md`、`Verified/命题/`、`Logs/报告.md`)、
393
+ 真实 JSON 键(`已解决`、`正确概率`)、真实格式锚点(`- ID/类型/状态/概率/…`、`### 解法/证明/证伪 N|标题|概率X|状态Y`)、
394
+ 会话重建标记 `【会话重建 —— <role> <id>】`、v2/v3 真实 md 字段名(`可信断言`、`上级体系`、`子方法`)**保持原样**,
395
+ 首次出现处可加括注;除此以外的正文必须是英文(`tests/audit-readme-bilingual.test.mjs` 的 CJK 预算兜底)。
396
+ - **架构图**:四张英文图 `示例图/框架图-v{2,3,4,5}-en.svg` 只被 `README.en.md` 引用,中文图不受影响;
397
+ v4/v5 用现有零依赖生成器加 `--lang=en`(`docs/generate_framework_diagram_v4.mjs`),
398
+ v2/v3 用独立的零依赖 Node 生成器 `docs/generate_framework_diagram_v{2,3}_en.mjs`(中文 v2/v3 海报仍由 matplotlib 脚本产出)。
399
+ 英文图内只允许保留上述"真实字面量"的中文,`tests/audit-diagram-assets.test.mjs` 按图核 CJK 预算。
400
+ - **历史文档必须标注适用范围**:描述旧版本布局的文档(如 v1 时期的 `docs/架构图.md`)要在开头写明它描述的是哪一版,
401
+ 不能在被 README 当作"当前详解"引用时静默误导(`qs.csv`、`Pending_Verification/` 等 v1 名称在 v2 起已不存在)。
402
+
403
+ ---
404
+
405
+ ## 8. 宿主版本线适配与发布流程实测教训(2.4.0 起)
406
+
407
+ ### 8.1 一条版本线一个机制:先定"形态",再谈"版本"
408
+
409
+ DSH 的 **agent preset 交付方式**在 0.1.6 → 0.1.7 之间换过一次,且**旧机制是被彻底删除**的:
410
+
411
+ | 版本线 | preset 形态 | 本文档对应做法 |
412
+ |---|---|---|
413
+ | ≤ 0.1.6 | 目录 `<DSH_HOME>/.agent-presets/<id>/{agent.cordis.yml,preset.yml,…}` | `installer.js` 复制(`detectPresetMechanism` 判为 `directory`) |
414
+ | ≥ 0.1.7 | 组合行 `@deepseek-ai/dsh-agent-preset`(`agentPresets.register({id,plugins,…})`) | `cordis.patch.yml` 里的声明行(判为 `rows`,安装器跳过目录同步) |
415
+
416
+ **判定必须用能力,不要用版本号**:`agentPresets` 服务在两条线上都存在但语义不同(旧线是目录扫描器、没有 `register`),
417
+ 所以判定改成读 **loader 入口树**里有没有 `@deepseek-ai/dsh-agent-preset`(或其 registry)——这在任何插件激活之前就可见,
418
+ 而"读服务"在 bundle 被单独激活(`dsh plugin add`)时可能还没就绪。**旧线上"装了但看不见"正是本版修掉的那类缺陷。**
419
+
420
+ ### 8.2 组合行声明的四条硬约束(都是实测出来的)
421
+
422
+ 1. **`dsh.bundle.patch` 必须是字符串**。数组形态只有 ≥ 0.1.7 接受;0.1.5/0.1.6 会把该值直接送进 `path.join`,
423
+ 结果是整个 profile **起不来**(`ERR_INVALID_ARG_TYPE`)。要多个 patch 就合并成一个文件。
424
+ 2. **声明行不要命名宿主包**(如 `@deepseek-ai/dsh-agent-preset`):旧线上该包不存在,行会 `failed to import`,
425
+ 每次启动都报一条激活失败。改成本包自己的模块(`preset-declaration.js`),它在旧线上**什么都不注册且不报错**。
426
+ 3. **声明行不能用 `disabled: !!js` 做版本门**:patch 表达式里 **`ctx.get(...)` 会抛**(实测:常量、`typeof ctx` 正常,
427
+ 任何服务查询都让入口永远不初始化,报 "N entries did not activate")。要按宿主能力分流,就分到**代码里**(运行时读服务)。
428
+ 4. **声明行里的 `name` 不会被改写成文件 URL**(app-boot 只递归 group 的 config),loader 会拿**profile 目录**去解析相对路径
429
+ → `./vibe-math-vN.js` 必 ENOENT、整个 preset 拒绝挂载。必须用**包内子路径**(`dsh-vibe-math/vibe-math-vN/vibe-math-vN.js`)+ `exports` 放行。
430
+
431
+ ### 8.3 不要在宿主的"历史事件"面上存自己的状态
432
+
433
+ 会话日志是**只增不改**的,但**读它是严格的**:持久化层遇到不认识的 `type` 会**拒绝加载整个会话**,除非写入方标了
434
+ `ignorable: true`——而 `Session.append` **无法**设置该字段(只有构造种子能)。所以"把插件状态写进会话日志"这条路
435
+ 会让**用户自己的会话在下次恢复时打不开**(静默:写入成功、失败发生在之后的加载)。自研状态请落在自己的文件里(v5 即如此)。
436
+
437
+ ### 8.4 发布流程:`npm publish` 返回 202 不是失败
438
+
439
+ - `npm publish` 得到 **HTTP 202** + "Your package is being processed…" 时,registry 正在**异步处理**;此时 `npm view` /
440
+ `dist-tags` / 版本 manifest / tarball 都可能是 404 或旧值,**持续数分钟**。
441
+ - 期间重发会得到 **409 `Cannot publish over previously staged version`**——这句措辞会让人误以为被扣下等 2FA 批准
442
+ (`npm stage list` 实测为空,并非 staged)。**正确处理:等待并轮询 packument**(`dist-tags.latest` + `/<pkg>/<ver>` + tarball HEAD),
443
+ **绝不在此时改版本号或内容**。
444
+ - `dsh plugin --profile <p> add` 在 Windows 上可能**在 pnpm 完成后才崩**(`0xC0000409`,报 "pnpm failed"):
445
+ **不要信退出码**,查 `package.json` 的 spec、lockfile、`node_modules` 三处是否一致,不一致就只对齐 manifest。
446
+
447
+ ---
448
+
449
+ ## 9. 插件目录安全基线(收录进 awesome-ai-plugins 起)
450
+
451
+ 本项目被 [awesome-ai-plugins](https://github.com/hashgraph-online/awesome-ai-plugins) 收录,其 CI 对**源仓库**跑
452
+ [HOL Guard 扫描器](https://github.com/hashgraph-online/hol-guard):**评分 ≥80 且不得有 critical/high 发现**才允许合并。
453
+ 下面这些是硬门槛,改动代码时不要踩回去(首次提交实测 73 分、2 个 high,被拦下):
454
+
455
+ | 扫描规则 | 要求 | 本仓库的做法 |
456
+ |---|---|---|
457
+ | `DANGEROUS_DYNAMIC_EXECUTION` (high) | 仓库里**不得**出现 `eval` / `new Function` —— 包括测试与脚本 | 需要"执行真实实现"的套件改为 `await import(<preset 模块>)` + 预设导出的 `__testHelpers` 测试接缝,不再把源码文本编译成函数;`node:vm` 同样属于动态执行,不要用它绕过 |
458
+ | `SECURITY_MD_MISSING` (low) | 仓库根有 `SECURITY.md`(支持版本 + 上报流程) | `SECURITY.md`(含本包攻击面说明:无网络、无凭据、无动态执行、只写工作区) |
459
+ | `DEPENDABOT_MISSING` (low) | `.github/dependabot.yml` | 本仓库**不配置**(§9.1:低危可接受,实测 94 分仍通过) |
460
+ | `DEPENDENCY_LOCKFILE_MISSING` (medium) | 有 `package.json` 就要有 lockfile | `package-lock.json`(本包零运行时依赖,lockfile 只有根条目) |
461
+ | 未固定的 Actions | 第三方 Action 必须钉到 commit SHA | 本仓库**无工作流**(§9.2);将来若引入,必须钉 SHA 且保留版本注释,`permissions: contents: read`、`persist-credentials: false` |
462
+
463
+ 另外目录要求的**包侧**条件(`CONTRIBUTING.md`):`package.json` 里要有可安装的 `dsh.bundle`,插件要导出 `apply(ctx)`,
464
+ 并且**在本仓库 README 里写明包名或 `dsh plugin add` 命令**、条目里用**准确的仓库 URL**。
465
+
466
+ 自查方式(无需 Python 环境也能跑:用 DSH 自带运行时里的 Python 装一次 `plugin-scanner`):
467
+
468
+ ```powershell
469
+ & "<python>" -m pip install plugin-scanner
470
+ & "<python>\Scripts\plugin-scanner.exe" lint "D:\wd\vibemath开发\Vibe-Mathematics" --format json # 逐条规则
471
+ & "<python>\Scripts\plugin-scanner.exe" scan "D:\wd\vibemath开发\Vibe-Mathematics" # 评分
472
+ ```
473
+
474
+ 本地 `plugin-scanner` 的评分与他们 CI 的评分**实测一致**(首次提交时两边同为 73),所以本地复现即可预判 CI;本仓库当前为 **94 分、1 个低危**(见 §9.1),达标线是 §9.2 的「≥80 且无 critical/high」。
475
+ 目录侧的检查(格式、字母序、条目可解析)由他们的 `check-alphabetical.py` + `validate-contribution.py` 在 PR 上跑:
476
+ 本仓库的条目必须放在 `### DeepSeek Harness Plugins` 小节里、按**显示名小写**字母序(`Vibe-Mathematics` 排在 `humanizer-ru` 之后)。
477
+
478
+ ---
479
+
480
+ ### 9.1 为什么本仓库**没有** Dependabot(别再顺手加回去)
481
+
482
+ 扫描器有一条 `DEPENDABOT_MISSING`(低危,值 5 分),最初为凑分加过 `.github/dependabot.yml`,随后它立刻开出一批
483
+ "补课" PR(`actions/checkout` 4.2.2 → 7.0.1、扫描器 action 补丁升级)。实测各档分数后决定**移除**:
484
+
485
+ | 配置 | 分数 | 通过(阈值 80 + 无 high) |
486
+ |---|---|---|
487
+ | 全都有(含 dependabot) | 100 | ✅ |
488
+ | 去掉 `dependabot.yml` | **95** | ✅ |
489
+ | 再去掉 `SECURITY.md` | 91 | ✅ |
490
+ | 再去掉 `package-lock.json` | 86 | ✅ |
491
+
492
+ 结论:**低危项不是必需的**——阈值 80,去掉它仍有 95 分。本仓库零运行时依赖,依赖面只有 `.github/workflows` 里两个 Action,
493
+ 为它们维护一个每周开 PR 的机器人,**换来的是维护者每次都要做决定**,不划算。改为:
494
+
495
+ - ~~`node scripts/check-action-versions.mjs`(npm script:`npm run check:actions`)~~(当时的做法,已随工作流一并删除,见 §9.2)——它曾读出工作流里所有
496
+ `uses: owner/repo@<sha> # vX.Y.Z`,用 GitHub API 比对上游最新 release,落后的打印**可直接替换的整行**,退出码 1;
497
+ - ~~**例行检查与每次发布时跑一次**(发布清单里带上),需要升级时由维护者显式改这一行,而不是被动接 PR。~~(同上,已删除)
498
+
499
+ 顺带记住两条与它相关的既有纪律:工作流里的第三方 Action **必须钉 commit SHA**(目录扫描器与供应链原则都要求),
500
+ 且钉法要**同时保留版本注释**(否则别人无法判断钉的是哪版)。
501
+
502
+ ### 9.2 本仓库**不使用** GitHub Actions(2.4.0 之后又撤掉了)
503
+
504
+ 办理收录时我一度加过 `.github/workflows/plugin-security-scan.yml`(在**本仓库**每次推送时跑同一个扫描器)。
505
+ 事后复核目录规则才发现**这是可选的**:`CONTRIBUTING.md` 与 `SCANNER_GUIDE.md` 都写明
506
+ "Scanner CI in the source repository is optional for listing",必需的只是**他们 CI 对我们仓库的集中扫描**——
507
+ 而那个扫描是在他们的 runner 上克隆本仓库后执行的,**不运行本仓库的任何工作流**,所以本仓库有没有工作流与能否收录无关。
508
+
509
+ 实测代价/收益:保留工作流 95 分,删掉 94 分(阈值 80);它唯一的真实成本是"长期盯两个 Action 的版本"。
510
+ 故**撤掉**:本仓库现在没有任何 Actions,也没有 dependabot;`.github/` 目录不再存在。
511
+
512
+ 取而代之的纪律(**别再顺手加回 CI**):
513
+
514
+ - **例行检查与每次发布前,在本地跑一次扫描器**(与目录同款,规则一致):
515
+ `plugin-scanner lint <repo> --format json`(逐条规则)/ `… scan <repo>`(评分);
516
+ 要求:**分数 ≥80 且无 critical/high**。命令与安装方式见 §9 顶部。
517
+ - Action 版本核对脚本已随之删除(没有工作流就没有可核对的 pin)。**若将来有人(包括我)真要引入工作流**:
518
+ 第三方 Action 必须钉 commit SHA 并保留版本注释,同时把本节的结论一并更新,并说明这次引入解决了什么原文中非做不可的问题。