dsh-vibe-math 2.3.0 → 2.3.1

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 (30) hide show
  1. package/AUDIT-CHECKLIST.md +33 -3
  2. package/README.md +21 -0
  3. package/RELEASE-NOTES-2.3.1.md +134 -0
  4. package/audit-formal-sensitivity.mjs +125 -39
  5. package/audit-v5-integrity.mjs +5 -3
  6. package/docs/formal-verification.md +92 -12
  7. package/docs/test-timing.md +79 -0
  8. package/formal-verify-v2.test.mjs +286 -7
  9. package/formal-verify-v3.test.mjs +215 -8
  10. package/formal-verify-v4.test.mjs +282 -3
  11. package/formal-verify-v5.test.mjs +72 -0
  12. package/package.json +9 -2
  13. package/prompt-corpus-v2/formal-verify-v2.json +394 -0
  14. package/prompt-corpus-v2/formal-verify-v2.md +4250 -0
  15. package/prompt-corpus-v3/formal-verify-v3.json +159 -57
  16. package/prompt-corpus-v3/formal-verify-v3.md +1302 -285
  17. package/prompt-corpus-v4/formal-verify-v4.json +84 -0
  18. package/prompt-corpus-v4/formal-verify-v4.md +255 -0
  19. package/prompt-corpus-v5/prompt-corpus-v5.json +54 -16
  20. package/prompt-corpus-v5/prompt-corpus-v5.md +378 -153
  21. package/prompt-v5-integrity.test.mjs +1158 -1085
  22. package/run-tests.mjs +99 -0
  23. package/vibe-math-v2/vibe-math-v2.js +204 -22
  24. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +77 -4
  25. package/vibe-math-v3/vibe-math-v3.js +82 -21
  26. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +17 -0
  27. package/vibe-math-v4/vibe-math-v4.js +114 -22
  28. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +29 -0
  29. package/vibe-math-v5/vibe-math-v5.js +81 -22
  30. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +27 -4
@@ -100,9 +100,26 @@
100
100
  而 v2/v3/v4 的 persona 从未列出它们(只有 v5 列了),v4 的 `vibe_v4_set` 参数表也漏了
101
101
  `formalVerify`/`leanCommand`/`leanArgs`/`leanTimeoutMs`——当时**所有既有套件全绿**。
102
102
 
103
- ---
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` 档的门禁措辞从未进入任何语料。
104
121
 
105
- ## 2. 第二优先:把上面每一条变成**会变红**的测试
122
+ ---
106
123
 
107
124
  ### 2.1 逐条断言,而不是抽查
108
125
 
@@ -129,6 +146,11 @@
129
146
  - 机器可读(JSON)+ 人可读(Markdown);
130
147
  - 覆盖全部交互类型(入职、重建、常规轮、心跳、表决初评/辩论、会议、提议、各类框头、框架提示、失败);
131
148
  - 把工作区路径归一化(如 `<WS>`),使语料**确定性、可 diff**;
149
+ - **归一化必须大小写与分隔符无关**:Windows 下 `os.tmpdir()` 可能给出与插件渲染路径**不同大小写**
150
+ 的同一目录,`split(WS)` 会漏掉全局根(如 `<VibeMath 根>/Formal/Lib`)的绝对路径——语料因此
151
+ **每次运行都变**(临时目录名一变就 diff 一大片)并**泄露本机路径**。真实事故,已修;
152
+ - **时间戳也要归一化**:提示词表头自带 `### YYYY-MM-DD hh:mm:ss|<成员>`,不归一化就**逐次都变**,
153
+ diff 完全失去意义(真实事故:v5 语料里 54 处时间戳)。判据很简单——**连续跑两次,哈希必须相同**;
132
154
  - **随包发布**,作为人工复核提示词正确性的入口——复核者不必去翻会话日志。
133
155
 
134
156
  ### 2.5 每个不变式都要有灵敏度探针
@@ -144,6 +166,10 @@
144
166
  这类变异不能做探针——它会让审计误报"盲点"。
145
167
  - [ ] 探针**不得引入语法错误**:语法错误导致的非零退出同样是"假红"。
146
168
  每条变异都应能被 `node --check` 通过。
169
+ - [ ] 变异必须替换锚点的**全部**出现:`String.prototype.replace` 只替换第一处。同一份契约常在
170
+ 两处发出(例如 v5 的回执契约同时出现在 `replySpec` 与表决提示词里),只改一处时套件
171
+ **合理地保持绿色**——那是**假盲点**,比漏测更误导(会让人去"修"一个本来就是对的不变式)。
172
+ 锚点声明了几次出现,就要替换几次;脚本应对此显式断言。
147
173
  - [ ] 守 **persona 静态面**的套件也要有探针,且该套件必须支持"指向副本"的环境变量
148
174
  (`audit-persona-surface.test.mjs` 的 `PERSONA_ROOT`):探针变异的是**副本**,
149
175
  套件不读这个变量就永远在跑原始文件、恒为绿(同 §2.5 第二条)。
@@ -178,7 +204,11 @@
178
204
  - [ ] 需求可追溯:方案里列出的工具名、参数名、理念条目,代码里是否都存在。
179
205
  - [ ] 失败路径:看门狗、幂等、崩溃恢复、降级后端、配额、越权。
180
206
  - [ ] 全量回归:**所有**历史套件,且逐个检查退出码(不要用管道截断输出,
181
- 管道会吞掉退出码或造成 EPIPE)。
207
+ 管道会吞掉退出码或造成 EPIPE)。**用 `node run-tests.mjs` 并行跑**(并发 = min(4, 核数)),
208
+ 它会打印每项耗时、wall/sum、加速比与最慢几项——**先看时间再决定策略**,基线见
209
+ [`docs/test-timing.md`](docs/test-timing.md)。若某个套件远慢于基线,先查它是否在等一个
210
+ **永远不会发生的条件**(真实事故:v2 套件 186 s,主因是一次 `tick(1100)` 嵌在
211
+ "从不提前退出"的 16 次循环里)。
182
212
  - [ ] 发布产物自证:不是"publish 退出 0"就算完成——要从 registry 取回 tarball,
183
213
  核对 shasum、逐字节比对插件、确认修复标记存在、并在**已发布包内**跑一遍套件。
184
214
 
package/README.md CHANGED
@@ -255,6 +255,25 @@ flowchart TB
255
255
  > 但决定必须说出来、可审计**,不允许静默跳过。相关参数还有 `leanCommand`(默认 `lean`)、
256
256
  > `leanArgs`(配合 `lake env lean`)、`leanTimeoutMs`(默认 120s)。
257
257
 
258
+ ### ⚠️ 忠实性缺陷 ≠ 命题为假(重要)
259
+
260
+ Lean 通过只保证"这段代码过了内核",**不保证它说的就是命题想说的**。所以当表决者逐条核对后
261
+ 发现 Lean 代码与命题原文不一致(写窄了 / 写宽了 / 换了对象 / 漏了条件)时:
262
+
263
+ - **不得投 0**。投 0 的含义是"**该命题为假**";一个写错的形式化会让框架把"形式化不合格"
264
+ 记成"命题被证伪",在 v5 的全 0 一致规则下甚至会把命题写进 `Verified/` 标注**假**——
265
+ 用来求真的机制反而**伪造出一个错误的否定结论**。
266
+ - 正确做法:给一个严格介于 0 与 1 之间的值(记为弃权)+ 用回执
267
+ `formal:{decision:'defect', note:'<具体偏差>'}` 记录偏差。框架随即**撤回这条证明的「已通过」状态**
268
+ (降级为 `attempted`、撤回 `Verified/Lean/<id>.lean`、写入「形式化待办」),`require` 档下
269
+ **本次裁定不定论**;修正形式化并重新跑通后再投票。
270
+ - 只有表决者**独立于这份 Lean 代码**也能确定命题为假(并能给出独立理由)时才投 0。
271
+
272
+ > 注入提示词的另外三条硬要求(契约 §6):工具名一律**全称**(`<prefix>lean_archive` 不是
273
+ > `lean_archive`——缩写不是注册名,代理照抄会调用一个不存在的工具);归档可复用定义/引理**前先跑通**,
274
+ > 跑不通不许进库;**工具链缺失**(`LEAN_NOT_FOUND`)时把代码写下来归档并在 `note` 写明
275
+ > "宿主无 Lean 工具链"——这算显式阻塞原因,门禁据此放行,不会因为装不了 Lean 而卡死。
276
+
258
277
  ### 归档:形式化代码放哪里
259
278
 
260
279
  ```
@@ -784,8 +803,10 @@ v5 的完整架构(含成员生命周期、一轮时序、共识状态机、
784
803
  - **v4(常驻自组织)**:[`vibe-math-v4/实现方案.md`](vibe-math-v4/实现方案.md)
785
804
  - **v5(研究所体系)**:[`vibe-math-v5/实现方案.md`](vibe-math-v5/实现方案.md)(文字规格)· [`vibe-math-v5/架构图.md`](vibe-math-v5/架构图.md)(全套架构图)
786
805
  - **v5 提示词与交互语料**:[`prompt-corpus-v5/prompt-corpus-v5.md`](prompt-corpus-v5/prompt-corpus-v5.md)(框架真正发出的每一条提示词原文,可直接人工复核身份/编制/交互署名是否正确)
806
+ - **四套 Lean 提示词语料**:[`prompt-corpus-v2/formal-verify-v2.md`](prompt-corpus-v2/formal-verify-v2.md) · [`prompt-corpus-v3/formal-verify-v3.md`](prompt-corpus-v3/formal-verify-v3.md) · [`prompt-corpus-v4/formal-verify-v4.md`](prompt-corpus-v4/formal-verify-v4.md)(各自覆盖 off / encourage / **require** / 忠实性分支 / 工作轮 / 回执契约;工作区归一化为 `<WS>`、VibeMath 根为 `<VIBEMATH>`)
787
807
  - **四个预设的 persona 原文**:[`prompt-corpus-persona/persona-corpus.md`](prompt-corpus-persona/persona-corpus.md)(主代理实际收到的提示词:有哪些工具、哪些参数、哪些斜杠子命令;由 `audit-persona-surface.test.mjs` 生成,随包发布)
788
808
  - **Lean 形式化验证(四架构共用契约)**:[`docs/formal-verification.md`](docs/formal-verification.md)
809
+ - **测试耗时基线与并行跑法**:[`docs/test-timing.md`](docs/test-timing.md)(`node run-tests.mjs` 并行跑全部套件 ≈1.9 min;探针脚本 ≈2.6 min;每个 runner 都会打印耗时/加速比供下次选策略)
789
810
  - **静态提示词面一致性(persona ↔ 工具注册表 ↔ 斜杠命令 hint/usage)**:[`audit-persona-surface.test.mjs`](audit-persona-surface.test.mjs)(197 条断言,并生成 [`prompt-corpus-persona/persona-corpus.md`](prompt-corpus-persona/persona-corpus.md) 供人工复核)+ [`audit-persona-sensitivity.mjs`](audit-persona-sensitivity.mjs)(11 条灵敏度探针)——守"注册的工具必须在 persona 里出现 / persona 里的名字必须真的注册 / `prefix` 与 `text` 两块逐行一致 / hint、usage、实际分支三处必须一致"
790
811
  - **全面检查必查清单**:[`AUDIT-CHECKLIST.md`](AUDIT-CHECKLIST.md)(本仓库的强制审计流程)
791
812
 
@@ -0,0 +1,134 @@
1
+ # dsh-vibe-math 2.3.1 — 提示词与交互修复:忠实性缺陷不再被记成"命题为假"
2
+
3
+ > 上一版:2.3.0(四个架构新增 Lean 形式化验证)。本版是**审计驱动的修复版**:
4
+ > 2.3.0 的功能是对的,但**代理读到的文字**与**回执通道**里有几处会造成错误结论的缺陷。
5
+ > 发布前对"运行时注入的 Lean 提示词 + 回执通道 + 语料"做了一次专项审计,本版修掉全部发现。
6
+
7
+ ---
8
+
9
+ ## 0. 最严重的一条:形式化写错了 ≠ 命题为假
10
+
11
+ 2.3.0 的忠实性分支写的是:
12
+
13
+ ```
14
+ ▸ 因此请把 verdict 用在**忠实性**上:一致 → 1;发现任何偏离 → 0(或按不确定度给中间值并说明)。
15
+ ```
16
+
17
+ 当 Lean 代码写的不是命题想说的(把条件写窄了 / 写宽了 / 换了对象 / 漏了条件)时,这条指令让表决者
18
+ 投 **0**——而 0 的含义是"**该命题为假**"。于是:
19
+
20
+ - 框架把"形式化不合格"记成"命题被证伪";
21
+ - 在 v5 的**全 0 一致**规则(或 v4 的全组一致规则)下,一个写错的形式化会把命题
22
+ **写进 `Verified/` 并标注「假」**;
23
+ - 也就是说,**为了求真更严格而引入的机制,反而伪造出一个错误的否定结论**。
24
+
25
+ 本版把它改成独立的一档 `defect`(契约 §4.1):
26
+
27
+ | | 2.3.0 | 2.3.1 |
28
+ |---|---|---|
29
+ | 发现忠实性偏差 | 投 0(= 命题为假) | **不得投 0**:给严格介于 0 与 1 之间的值(记为弃权)+ 回执 `formal:{decision:'defect', note:'<具体偏差>'}` |
30
+ | 框架动作 | 按"假"参与定论 | **撤回「已通过」**:降级 `attempted`、清空 `proof`、撤回归档证明、写入「形式化待办」、公告 |
31
+ | `require` 档 | 门禁已放行(`passed`) | 门禁**重新拦住**:本次裁定**不定论**,修正形式化并重新跑通后再投票 |
32
+ | 什么时候才能投 0 | 发现偏差就投 | **只有独立于这份 Lean 代码也能确定命题为假**(并给出独立理由)时 |
33
+
34
+ > `encourage` 档没有门禁:框架仍然撤回证明并记入待办,但**不在提示词里承诺一个它无法强制的
35
+ > "不定论"**——那里靠表决者按指令给出的弃权使表决得不出布尔一致结论。(这条区分本身也是审计发现的:
36
+ > 首版草稿在两种档位下都声称"本次裁定不定论",而 `encourage` 档根本没有门禁。)
37
+
38
+ ---
39
+
40
+ ## 1. 本版修掉的缺陷(按严重度)
41
+
42
+ | # | 缺陷 | 影响 | 落点 |
43
+ |---|---|---|---|
44
+ | 1 | **v2 的 `formal` 回执通道是死代码**:提示词让代理"在回执的 formal 字段写明难度判断",但 v2 的两份验证回执契约里**没有**这个字段,框架也**从不解析**它 | 代理写下的难度判断**静默消失**;`require` 档下代理以为记录了阻塞,门禁却一直拦着 | 补齐 `formalJsonField` / `formalReplyNote` / `absorbFormalFromReply`,并接进初评 + 辩论两条路径 |
45
+ | 2 | **忠实性缺陷被判成"命题为假"**(四个架构) | 可能伪造出错误的否定结论(见 §0) | 新增 `defect` 档(四套) |
46
+ | 3 | **字段名写错**:v2/v3 的 Lean 段说"并据此给出 verdict",但这两套的回执字段是 `Result` | 代理回 `{"verdict":…}` 时**那一票被静默丢弃**(默认 0.5) | 忠实性分支改写为 `Result`(v3 还补上了缺失的"偏差怎么映射"指引) |
47
+ | 4 | **缩写工具名**:注入文本里出现 `lean_lib` / `lean_archive`(v2/v3/v5,**包括工具自己返回的 `hint`**) | 照抄的代理调用一个**不存在的工具** | 全部改为注册名全称 `<prefix>lean_*` |
48
+ | 5 | **没有"归档前先跑通"的要求** | 不编译的定义/引理会污染跨项目复用库 | 提示词明确:`run=true` 或先 `lean_run`,跑不通不许入库 |
49
+ | 6 | **工具链缺失没有出路** | 宿主没装 Lean 时,`require` 档代理可能反复空转 | 提示词明确:`LEAN_NOT_FOUND` 时把代码归档并在 `note` 写明"宿主无 Lean 工具链",算显式阻塞原因 |
50
+ | 7 | **语料不是确定性产物**:v5 的路径归一化在 Windows 下因**大小写**差异(`split(WS)` 匹配不上)漏掉了 VibeMath 根的绝对路径,且提示词表头自带 `### YYYY-MM-DD hh:mm:ss|<成员>` **时间戳** | 随包语料每跑一次都变(上一次提交里的语料改动就纯粹来自临时目录改名)、泄露本机路径、diff 失去意义 | `scrub` 改为大小写/分隔符无关,并把时间戳归一化为 `<TIME>`——现在**逐字节稳定**(已实测两次运行的 SHA256 相同) |
51
+ | 8 | **v2/v4 没有 Lean 提示词语料;`require` 档文本不在任何语料里** | 复核者只能翻源码;门禁那段话从没被人读过 | 四套各自新增 `prompt-corpus-vN/`,覆盖 off/encourage/**require**/忠实性/工作轮/回执契约 |
52
+ | 9 | **v2 套件的回执断言只查措辞**("提示词里有那句话") | 177 条断言全绿却守着一个**死通道** | 改为**行为断言**:把带 `formal` 的回执真的喂给框架,断言记录落库 |
53
+ | 10 | 套件里"占位垃圾"扫描用了裸 `\bundefined\b` | 把 v2 explorer 提示词里合法的英文 "no undefined symbols" 误报为垃圾 | 改为只匹配**数据位**的垃圾(`: undefined` / `"undefined"` / `undefined,` …) |
54
+
55
+ > 第 1、9 条是同一件事的两面:**提示词说了、框架没做、测试只查措辞**——这类缺陷在任何只断言
56
+ > "包含某些关键词"的套件里都是隐形的(`AUDIT-CHECKLIST.md` §2.2 早就点过名)。
57
+
58
+ ---
59
+
60
+ ## 2. 契约与文档同步
61
+
62
+ - `docs/formal-verification.md`:新增 **§4.1 `defect`**(含状态迁移与"唯一可以投 0 的情形")、
63
+ §6 顶部新增 **5 条提示词硬要求**、§6.1 忠实性分支改写、§6.3 回执契约加 `defect` 与"必须真的被解析",
64
+ §10 新增测试要求 8–11(行为断言 / 忠实性语义断言 / 每架构语料 + 归一化 / 提示词探针)。
65
+ - `README.md`:Lean 章节新增「⚠️ 忠实性缺陷 ≠ 命题为假」,并把三条硬要求写进正文。
66
+ - 各 `实现方案.md`:v2 / v3 / v4 / v5 都补了 `defect` 语义、工具名全称、归档前跑通、工具链缺失出路,
67
+ 以及各自的断言数与语料说明。
68
+ - `AUDIT-CHECKLIST.md` §1.6 静态提示词面 + §2.5 探针纪律:本版新增的 16 条**提示词探针**正是按它执行。
69
+
70
+ ---
71
+
72
+ ## 3. 测试与探针(本版实测)
73
+
74
+ | 套件 | 2.3.0 | 2.3.1 |
75
+ |---|---|---|
76
+ | `formal-verify-v2.test.mjs` | 177 | **261**(含 `defect` 双向 id 降级、回执通道行为断言、语料 89 条) |
77
+ | `formal-verify-v3.test.mjs` | 189 | **247** |
78
+ | `formal-verify-v4.test.mjs` | 144 | **226** |
79
+ | `formal-verify-v5.test.mjs` | 88 | **120** |
80
+ | `prompt-v5-integrity.test.mjs` | 563 | **588**(语料 70 条 / 27 类,新增 `lean-require`、`lean-after-defect`) |
81
+ | `audit-formal-sensitivity.mjs` | 33 探针 | **49 探针**(新增 16 条提示词探针:删掉"不要投 0"、把工具名换成缩写、删掉 `require` 门禁措辞、把回执契约里的 `defect` 去掉——四套各 4 条) |
82
+ | `audit-persona-sensitivity.mjs` | 11 探针 | 11 探针(不变,全红) |
83
+ | `audit-persona-surface.test.mjs` | 197 断言 | 197 断言(persona 未改,仍全绿) |
84
+
85
+ 四套现在都**自带**随包发布的人工复核语料:
86
+
87
+ ```
88
+ prompt-corpus-v2/formal-verify-v2.{json,md} # 89 条
89
+ prompt-corpus-v3/formal-verify-v3.{json,md}
90
+ prompt-corpus-v4/formal-verify-v4.{json,md}
91
+ prompt-corpus-v5/prompt-corpus-v5.md # 70 条 / 27 类
92
+ prompt-corpus-persona/persona-corpus.md # 四个预设的 persona 原文
93
+ ```
94
+
95
+ 语料里的工作区路径归一化为 `<WS>`、VibeMath 根归一化为 `<VIBEMATH>`,可 diff、不含本机路径。
96
+
97
+ ---
98
+
99
+ ## 3.5 测试执行提速:并行 runner + 耗时反馈(本版新增)
100
+
101
+ 这轮顺带做了一次"测试脚本自己有多慢"的审计,结论是**时间几乎全花在等待上,而不是在测东西**:
102
+
103
+ | | 优化前 | 优化后 | 手段 |
104
+ |---|---|---|---|
105
+ | 探针脚本 `audit-formal-sensitivity.mjs`(49 条) | **38 min**(串行) | **2.6 min**(wall 154.6 s,sum 612 s,**x3.96**) | ① 探针并行(并发 4,每个探针独立变异副本 + 独立语料目录,互不干扰);② 修掉 v2 套件的 186 s(见下) |
106
+ | 全量回归(23 个套件) | ≈ 5.5 min(串行) | **1.9 min**(wall 114.4 s,sum 219.4 s,**x1.92**) | 新增 **`run-tests.mjs`**:并行跑全部套件并打印每项耗时、wall/sum、加速比、最慢几项 |
107
+ | `formal-verify-v2.test.mjs` 单套件 | **186 s** | **≈32 s** | 两处根因:① 插件用 `setInterval(...,1000)` 轮询调度器,套件每次 `tick()` 都得等满 1 秒——套件现在**只把 `setInterval` 快进到 25 ms**(自己的 `sleep` 用 `setTimeout`,不受影响;插件内部"该不该 tick"仍按真实 200 ms 下限判断,**生产代码零改动**);② `verifyWithDebate` 的 16 次循环**从不提前退出**(判据 `autoDone` 之类的条件永远不会发生),每次调用白烧 ~21 s——现在按"连续 3 轮没有新 followup"提前退出。断言数不变(261) |
108
+ | `e2e-v4-fixes.test.mjs` | 105 s | 101 s | 12 个轮询循环加了"安静即停"(原判据 `autoDone‖running===false` 对活着的 run 永不成立)。**再往下压就要砍采样深度**:9 个慢用例(T13/T19/T22/T23/T25/T27/T2/T9/T20)是在观察"多轮之后某指令**没有**泄漏/重复",轮数就是它们的不变式本体 |
109
+
110
+ **耗时基线写进了 [`docs/test-timing.md`](docs/test-timing.md)**:单套件耗时表、并行安全性(语料目录必须
111
+ 逐实例隔离)、以及"按目的选最小代价组合"的策略表。两个 runner 每次都会打印耗时与加速比——
112
+ **下次跑之前先看这几行**,再决定并发数、要不要 `--only`、以及某个变慢的套件是不是又在等一个
113
+ 永远不会发生的条件。
114
+
115
+ ---
116
+
117
+ ## 4. 兼容性与迁移
118
+
119
+ 无破坏性变更,**不需要迁移**:
120
+
121
+ - 默认仍是 `formalVerify: 'off'`(真正的无操作),行为与 2.3.0 逐字节一致;
122
+ - `defect` 是**新增**的回执取值;已有代理若仍回 `"decision":"used"|"blocked"` 一切照旧;
123
+ - 提示词里的工具名由缩写改为全称,只会让**照抄的调用**从失败变为成功;
124
+ - 归档路径、门禁位置、三个工具、四个参数**都没有变**。
125
+
126
+ ---
127
+
128
+ ## 5. 升级
129
+
130
+ ```
131
+ npm i dsh-vibe-math@latest
132
+ ```
133
+
134
+ 升级后重启 DSH:未被手动改过的 preset 文件会自动更新;语言/提示词层面的变化在新会话生效。
@@ -22,20 +22,23 @@
22
22
  //
23
23
  // Run: node audit-formal-sensitivity.mjs
24
24
  // ============================================================
25
- import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
25
+ import { readFileSync, writeFileSync, mkdtempSync, rmSync, mkdirSync } from 'node:fs'
26
26
  import { tmpdir } from 'node:os'
27
27
  import { join } from 'node:path'
28
- import { spawnSync } from 'node:child_process'
28
+ import { spawn, spawnSync } from 'node:child_process'
29
+ import { cpus } from 'node:os'
29
30
  import { fileURLToPath } from 'node:url'
30
31
 
31
32
  const REPO = fileURLToPath(new URL('.', import.meta.url))
32
33
  const dir = mkdtempSync(join(tmpdir(), 'v5-formal-sens-'))
33
34
 
34
35
  const PLUGINS = {
35
- v2: { file: join(REPO, 'vibe-math-v2', 'vibe-math-v2.js'), suite: 'formal-verify-v2.test.mjs', env: 'V2_PLUGIN' },
36
- v3: { file: join(REPO, 'vibe-math-v3', 'vibe-math-v3.js'), suite: 'formal-verify-v3.test.mjs', env: 'V3_PLUGIN' },
37
- v4: { file: join(REPO, 'vibe-math-v4', 'vibe-math-v4.js'), suite: 'formal-verify-v4.test.mjs', env: 'V4_PLUGIN' },
38
- v5: { file: join(REPO, 'vibe-math-v5', 'vibe-math-v5.js'), suite: 'formal-verify-v5.test.mjs', env: 'V5_PLUGIN' },
36
+ // corpusEnv: each suite writes a human-reviewable prompt corpus; concurrent probes of the SAME
37
+ // suite must not race on it, so every probe gets its own corpus dir.
38
+ v2: { file: join(REPO, 'vibe-math-v2', 'vibe-math-v2.js'), suite: 'formal-verify-v2.test.mjs', env: 'V2_PLUGIN', corpusEnv: 'V2_CORPUS_DIR' },
39
+ v3: { file: join(REPO, 'vibe-math-v3', 'vibe-math-v3.js'), suite: 'formal-verify-v3.test.mjs', env: 'V3_PLUGIN', corpusEnv: 'V3_CORPUS_DIR' },
40
+ v4: { file: join(REPO, 'vibe-math-v4', 'vibe-math-v4.js'), suite: 'formal-verify-v4.test.mjs', env: 'V4_PLUGIN', corpusEnv: 'V4_CORPUS_DIR' },
41
+ v5: { file: join(REPO, 'vibe-math-v5', 'vibe-math-v5.js'), suite: 'formal-verify-v5.test.mjs', env: 'V5_PLUGIN', corpusEnv: 'V5_CORPUS_DIR' },
39
42
  }
40
43
  const ORIGINAL = {}
41
44
  for (const [k, v] of Object.entries(PLUGINS)) ORIGINAL[k] = readFileSync(v.file, 'utf8')
@@ -191,56 +194,139 @@ const probes = [
191
194
  to: ", 'vibe_math_lean_run_DISABLED')" },
192
195
  ]
193
196
 
197
+ // ── prompt-surface probes (2.3.1) ────────────────────────────────────────────────────────
198
+ // "成员读到的文字就是产品" (AUDIT-CHECKLIST §0): the injected text is a contract, so breaking it
199
+ // must turn the preset's suite RED. Each probe mutates ONE piece of the text agents actually read.
200
+ // `expect` is the number of times that anchor legitimately occurs (v3 declares each reply
201
+ // contract twice; v4/v5 document the formal contract in several prompt paths). The probe runner
202
+ // reports SETUP-FAIL when a count drifts, so this table is self-checking.
203
+ const DEFECT_CONTRACT_COUNT = { v2: 2, v3: 2, v4: 3, v5: 2 }
204
+ for (const [tag, pfx] of [['v2', 'vibe_math_'], ['v3', 'vibe_math_'], ['v4', 'vibe_v4_'], ['v5', 'vibe_v5_']]) {
205
+ probes.push(
206
+ { name: tag + '-fidelity-defect-rule-removed', preset: tag, expect: 1,
207
+ guarantee: 'a faithfulness defect must NEVER be expressed as a vote of 0 (= 命题为假); it is a formalisation defect, not a refutation',
208
+ from: '发现任何偏差,不要投 0', to: '发现任何偏离一律投 0' },
209
+ { name: tag + '-abbreviated-tool-name-injected', preset: tag, expect: 1,
210
+ guarantee: 'injected text must use the REGISTERED tool name (an abbreviated lean_archive is not a tool: the agent calls nothing)',
211
+ from: '· ' + pfx + 'lean_archive(归档)', to: '· lean_archive(归档)' },
212
+ { name: tag + '-require-wording-removed', preset: tag, expect: 1,
213
+ guarantee: "the require gate's own wording (mandatory formalisation + the formal-required reason code) must reach the voter",
214
+ from: '**本模式要求**:必须产出 Lean 形式化', to: '**本模式要求**:可以不做形式化' },
215
+ { name: tag + '-defect-decision-not-offered', preset: tag, expect: DEFECT_CONTRACT_COUNT[tag],
216
+ guarantee: 'the reply contract must offer decision=defect (without it a faithfulness defect cannot be recorded at all)',
217
+ from: '"decision":"used|blocked|defect"', to: '"decision":"used|blocked"' },
218
+ )
219
+ }
220
+
194
221
  let ok = 0, bad = 0
195
222
  console.log('-- Lean formal-verification sensitivity probes --')
196
223
  console.log('(a probe passes when breaking the guarantee turns that preset\'s suite RED)')
197
224
  console.log('')
198
225
 
199
- for (const p of probes) {
226
+ // ── timing feedback: per-probe durations so the next run's strategy comes from data ──────
227
+ const CONCURRENCY = (() => {
228
+ const arg = process.argv.find((a) => a.startsWith('--concurrency='))
229
+ const env = process.env.PROBE_CONCURRENCY
230
+ const v = Number((arg && arg.split('=')[1]) || env || Math.min(4, cpus().length))
231
+ return Math.max(1, Number.isFinite(v) ? v : 1)
232
+ })()
233
+ const ONLY = (() => {
234
+ const arg = process.argv.find((a) => a.startsWith('--only='))
235
+ return arg ? arg.split('=')[1] : ''
236
+ })()
237
+ const selected = probes.filter((p) => !ONLY || p.name.includes(ONLY) || p.preset === ONLY)
238
+ if (process.argv.includes('--list')) {
239
+ for (const p of selected) console.log(p.preset + ' ' + p.name)
240
+ process.exit(0)
241
+ }
242
+
243
+ function runAsync(cmd, args, opts) {
244
+ return new Promise((resolve) => {
245
+ const child = spawn(cmd, args, opts)
246
+ let out = '', err = ''
247
+ child.stdout.on('data', (d) => { out += d.toString() })
248
+ child.stderr.on('data', (d) => { err += d.toString() })
249
+ child.on('error', (e) => resolve({ status: null, error: e, stdout: out, stderr: err }))
250
+ child.on('close', (status) => resolve({ status, stdout: out, stderr: err }))
251
+ })
252
+ }
253
+
254
+ // One probe = one targeted mutation + one run of that preset's suite. Returns a verdict object;
255
+ // never throws, so a single bad probe cannot take the pool down.
256
+ async function runProbe(p) {
257
+ const t0 = Date.now()
200
258
  const preset = PLUGINS[p.preset]
201
259
  const original = ORIGINAL[p.preset]
202
- if (p.from.startsWith('PLACEHOLDER_')) {
203
- console.error(' SETUP-FAIL - ' + p.name + ': anchor not filled in yet')
204
- bad++
205
- continue
206
- }
260
+ const done = (kind, detail) => ({ p, kind, detail, ms: Date.now() - t0 })
261
+ if (p.from.startsWith('PLACEHOLDER_')) return done('setup', 'anchor not filled in yet')
262
+ const want = p.expect === undefined ? 1 : p.expect
207
263
  const occurrences = original.split(p.from).length - 1
208
- if (occurrences !== 1) {
209
- console.error(' SETUP-FAIL - ' + p.name + ' [' + p.preset + ']: anchor matched ' + occurrences + ' times (need exactly 1)')
210
- bad++
211
- continue
212
- }
213
- const mutated = original.replace(p.from, p.to)
214
- const file = join(dir, p.name + '.js')
264
+ if (occurrences !== want) return done('setup', 'anchor matched ' + occurrences + ' times (need exactly ' + want + ')')
265
+ // Mutate EVERY occurrence the anchor was asserted to have. `replace()` would only hit the first
266
+ // one, which silently produced a fake blind spot: the v5 reply contract is emitted in two places
267
+ // (replySpec + the voting prompt), so replacing just one left the other intact and the suite —
268
+ // correctly — stayed green.
269
+ const mutated = want > 1 ? original.split(p.from).join(p.to) : original.replace(p.from, p.to)
270
+ // One directory per probe: the mutated copy AND its corpus output (several suites write a
271
+ // corpus, and concurrent runs of the same suite must not race on that file).
272
+ const pdir = join(dir, p.name)
273
+ mkdirSync(pdir, { recursive: true })
274
+ const file = join(pdir, 'plugin.js')
215
275
  writeFileSync(file, mutated, 'utf8')
216
-
217
276
  // A mutation that does not even parse is red for the WRONG reason.
218
277
  const chk = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' })
219
- if (chk.status !== 0) {
220
- console.error(' SETUP-FAIL - ' + p.name + ': the mutated copy has a syntax error:\n' + String(chk.stderr || '').split('\n').slice(0, 4).join('\n'))
221
- bad++
222
- continue
223
- }
224
-
278
+ if (chk.status !== 0) return done('setup', 'the mutated copy has a syntax error: ' + String(chk.stderr || '').split('\n').slice(0, 4).join(' '))
225
279
  const env = Object.assign({}, process.env)
226
280
  env[preset.env] = file
227
- const r = spawnSync(process.execPath, [join(REPO, preset.suite)], { env, encoding: 'utf8', cwd: REPO })
228
- if (r.status === null) {
229
- console.error(' SETUP-FAIL - ' + p.name + ': the suite could not be started (' + String(r.error && r.error.message) + ')')
230
- bad++
231
- continue
281
+ if (preset.corpusEnv) env[preset.corpusEnv] = join(pdir, 'corpus')
282
+ const r = await runAsync(process.execPath, [join(REPO, preset.suite)], { env, encoding: 'utf8', cwd: REPO })
283
+ if (r.status === null) return done('setup', 'the suite could not be started (' + String(r.error && r.error.message) + ')')
284
+ if (r.status !== 0) return done('ok', '')
285
+ return done('blind', '')
286
+ }
287
+
288
+ const results = new Array(selected.length)
289
+ let cursor = 0
290
+ let finished = 0
291
+ const wall0 = Date.now()
292
+ async function worker() {
293
+ for (;;) {
294
+ const i = cursor++
295
+ if (i >= selected.length) return
296
+ const res = await runProbe(selected[i])
297
+ results[i] = res
298
+ finished++
299
+ const tag = '[' + String(finished).padStart(2) + '/' + selected.length + ']'
300
+ const secs = (res.ms / 1000).toFixed(1) + 's'
301
+ if (res.kind === 'ok') console.log(' ok - ' + res.p.name + ' [' + res.p.preset + '] => suite went RED as required (' + secs + ') [' + res.p.guarantee + ']')
302
+ else if (res.kind === 'blind') console.error(' BLIND SPOT ' + tag + ' - ' + res.p.name + ' [' + res.p.preset + '] (' + secs + ') => suite stayed GREEN, so it does NOT detect: ' + res.p.guarantee)
303
+ else console.error(' SETUP-FAIL ' + tag + ' - ' + res.p.name + ' [' + res.p.preset + '] (' + secs + '): ' + res.detail)
232
304
  }
233
- if (r.status !== 0) {
234
- ok++
235
- console.log(' ok - ' + p.name + ' [' + p.preset + '] => suite went RED as required [' + p.guarantee + ']')
236
- } else {
237
- bad++
238
- console.error(' BLIND SPOT - ' + p.name + ' [' + p.preset + '] => suite stayed GREEN, so it does NOT detect: ' + p.guarantee)
305
+ }
306
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, selected.length) }, () => worker()))
307
+
308
+ // ── timing summary: this is the feedback that decides the next run's strategy ────────────
309
+ {
310
+ const wall = (Date.now() - wall0) / 1000
311
+ const sum = results.reduce((a, r) => a + (r ? r.ms : 0), 0) / 1000
312
+ const perPreset = {}
313
+ for (const r of results) {
314
+ if (!r) continue
315
+ perPreset[r.p.preset] = perPreset[r.p.preset] || { n: 0, s: 0 }
316
+ perPreset[r.p.preset].n++
317
+ perPreset[r.p.preset].s += r.ms / 1000
239
318
  }
319
+ const slow = results.filter(Boolean).slice().sort((a, b) => b.ms - a.ms).slice(0, 5)
320
+ console.log('')
321
+ console.log('-- timing --')
322
+ console.log(' concurrency ' + CONCURRENCY + ' · wall ' + wall.toFixed(1) + 's · sum of probe times ' + sum.toFixed(1) + 's'
323
+ + ' · speed-up x' + (sum / Math.max(wall, 0.001)).toFixed(2))
324
+ console.log(' per preset: ' + Object.keys(perPreset).sort().map((k) => k + ' ' + perPreset[k].n + ' probes/' + perPreset[k].s.toFixed(0) + 's').join(' · '))
325
+ console.log(' slowest: ' + slow.map((r) => r.p.name + ' ' + (r.ms / 1000).toFixed(1) + 's').join(' · '))
326
+ ok = results.filter((r) => r && r.kind === 'ok').length
327
+ bad = results.filter((r) => r && r.kind !== 'ok').length
240
328
  }
241
329
 
242
- rmSync(dir, { recursive: true, force: true })
243
- console.log('')
244
330
  console.log('formal sensitivity: ' + ok + ' probes detected the break, ' + bad + ' problems')
245
331
  if (bad) process.exit(1)
246
332
  console.log('ALL FORMAL PROBES RED AS REQUIRED')
@@ -217,7 +217,7 @@ notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5
217
217
  const codeTools = new Set()
218
218
  for (const m of raw.matchAll(/registerTool\(\s*'(vibe_v5_[a-z_]+)'/g)) codeTools.add(m[1])
219
219
  // documented-but-wildcarded placeholders are not real tools
220
- const IGNORE = new Set(['vibe_v5_', 'vibe_v5_record_'])
220
+ const IGNORE = new Set(['vibe_v5_', 'vibe_v5_record_', 'vibe_v5_lean_', 'vibe_v5_task_'])
221
221
  for (const t of planTools) {
222
222
  if (IGNORE.has(t)) continue
223
223
  if (!codeTools.has(t)) findings.push('plan names tool ' + t + ' but the plugin never registers it')
@@ -335,8 +335,10 @@ notes.push('composition rows: ' + v5rows.length + '; non-v4 package rows: ' + v5
335
335
  'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
336
336
  'inbox-office', 'inbox-assign', 'inbox-nudge', 'inbox-office-assign', 'inbox-office-nudge',
337
337
  'notice', 'notice-claim', 'after-failure',
338
- // the Lean formal-verification interaction must be reviewable by a human too
339
- 'lean-work', 'lean-verify', 'lean-fidelity']
338
+ // the Lean formal-verification interaction must be reviewable by a human too — including
339
+ // the `require` gate wording and the state a member sees AFTER a fidelity defect withdrew
340
+ // a proof (both were missing from the first corpus, so nobody could read them).
341
+ 'lean-work', 'lean-verify', 'lean-fidelity', 'lean-require', 'lean-after-defect']
340
342
  for (const k of need) if (!kinds.has(k)) findings.push('the prompt corpus is missing a ' + k + ' prompt')
341
343
  const all = (c.prompts || []).map(p => p.prompt + '\n' + (p.charter || '')).join('\n')
342
344
  if (/你是 \?/.test(all)) findings.push('the prompt corpus contains a wrong-identity "你是 ?" brief')