dsh-vibe-math 2.3.2 → 2.3.4
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.
- package/AUDIT-CHECKLIST.md +14 -0
- package/README.md +2 -2
- package/RELEASE-NOTES-2.3.3.md +115 -0
- package/RELEASE-NOTES-2.3.4.md +69 -0
- package/audit-prompt-invariants.mjs +101 -7
- package/audit-spec-traceability.mjs +20 -0
- package/docs/formal-verification.md +4 -3
- package/docs/test-timing.md +5 -5
- package/formal-verify-v2.test.mjs +46 -0
- package/formal-verify-v3.test.mjs +54 -0
- package/formal-verify-v4.test.mjs +21 -0
- package/formal-verify-v5.test.mjs +23 -0
- package/package.json +4 -2
- package/prompt-corpus-v2/formal-verify-v2.json +30 -0
- package/prompt-corpus-v2/formal-verify-v2.md +341 -0
- package/prompt-corpus-v3/formal-verify-v3.json +36 -0
- package/prompt-corpus-v3/formal-verify-v3.md +404 -0
- package/vibe-math-v2/vibe-math-v2.js +14 -3
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
- package/vibe-math-v3/vibe-math-v3.js +7 -2
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +3 -3
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
package/AUDIT-CHECKLIST.md
CHANGED
|
@@ -130,6 +130,10 @@ v2/v3/v4/v5 是**同构实现**(同一份契约、四份独立代码,刻意
|
|
|
130
130
|
(两套静默吞掉失败、一套不校验)——宿主删不掉时,已撤回的证明仍留在 `Verified/Lean/<id>.lean`
|
|
131
131
|
这个"大家找证明"的位置;
|
|
132
132
|
- `formal` 回执通道在 v2/v3 有 `formalOn()` 守卫,v4/v5 没有——`off` 档可被残留回执写入状态。
|
|
133
|
+
- **「随手一次动作不得撤销已成立的证明」这条规则在四套里各写了一遍,却只在一套里被断言过**(v4 的
|
|
134
|
+
`formalSetRun`):确认轮实测发现 v2 的 `used` **回执**通道无条件降级 `passed`(同一条规则的另一条
|
|
135
|
+
路径),v3 只保留 `passed` 而把 `blocked` 打回 `attempted`(门禁被重新关上)。同一语义在**多条路径**
|
|
136
|
+
上要逐条核对:工具路径、回执路径、归档路径各自独立。
|
|
133
137
|
|
|
134
138
|
所以:
|
|
135
139
|
|
|
@@ -254,8 +258,18 @@ provider **直接拒绝**。于是"提示词/规格/状态表都写着这个参
|
|
|
254
258
|
[`docs/test-timing.md`](docs/test-timing.md)。若某个套件远慢于基线,先查它是否在等一个
|
|
255
259
|
**永远不会发生的条件**(真实事故:v2 套件 186 s,主因是一次 `tick(1100)` 嵌在
|
|
256
260
|
"从不提前退出"的 16 次循环里)。
|
|
261
|
+
- [ ] **产物是异步写出来的**:驱动循环("安静即停")可能在框架把卡片/状态文件落盘之前就退出,
|
|
262
|
+
而 `assert(existsSync(f))` **不会抛**、紧随其后的 `readFileSync(f)` 会抛 `ENOENT` —— 一次抖动
|
|
263
|
+
就变成**未捕获异常**,整个套件中止、后面所有断言全部丢失(真实事故:并行跑时
|
|
264
|
+
`e2e-v4-fixes` 崩在 `Verified/命题/m-meth.md`,12 个用例只跑了 2 个)。写法:
|
|
265
|
+
先 `await waitFor(()=>existsSync(f), 3000)`,再用**防御性读**(`try{…}catch{return ''}`)
|
|
266
|
+
——缺文件必须是**一条干净的断言失败**,绝不能是崩溃。参见该套件里的 `readArtifact`。
|
|
257
267
|
- [ ] 发布产物自证:不是"publish 退出 0"就算完成——要从 registry 取回 tarball,
|
|
258
268
|
核对 shasum、逐字节比对插件、确认修复标记存在、并在**已发布包内**跑一遍套件。
|
|
269
|
+
- [ ] **发布元数据交给序列化器**:不要用字符串替换手写 `package.json` 的字段(`compatNote` 里一个
|
|
270
|
+
ASCII 双引号就能产出非法 JSON;`\\u0000` 之类转义在单引号字符串里会变成真控制字符)。
|
|
271
|
+
正确写法是 `JSON.parse` → 改属性 → `JSON.stringify(pkg, null, 2)`,写完再 `JSON.parse` 复核
|
|
272
|
+
并扫描控制字符。
|
|
259
273
|
|
|
260
274
|
---
|
|
261
275
|
|
package/README.md
CHANGED
|
@@ -817,8 +817,8 @@ v5 的完整架构(含成员生命周期、一轮时序、共识状态机、
|
|
|
817
817
|
- **测试耗时基线与并行跑法**:[`docs/test-timing.md`](docs/test-timing.md)(`node run-tests.mjs` 并行跑全部套件 ≈1.9 min;探针脚本 ≈2.6 min;每个 runner 都会打印耗时/加速比供下次选策略)
|
|
818
818
|
- **静态提示词面一致性(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、实际分支三处必须一致"
|
|
819
819
|
- **全面检查必查清单**:[`AUDIT-CHECKLIST.md`](AUDIT-CHECKLIST.md)(本仓库的强制审计流程;§1.9 专门查"工具参数 schema 收不收得下")
|
|
820
|
-
- **提示词/交互不变式(四套一起,可一键复核)**:[`audit-prompt-invariants.mjs`](audit-prompt-invariants.mjs)(
|
|
821
|
-
- **规格 ↔ 代码可追溯(四套一起)**:[`audit-spec-traceability.mjs`](audit-spec-traceability.mjs)(
|
|
820
|
+
- **提示词/交互不变式(四套一起,可一键复核)**:[`audit-prompt-invariants.mjs`](audit-prompt-invariants.mjs)(157 条断言)——把"历史上真实发生过的提示词/工具面缺陷类别"逐条编码成静态不变式(缩写工具名、把忠实性缺陷投成 0、`defect` 只写在提示词里没实现、回执契约缺 `defect`、无 note 放行、字段名错、`off` 档回执仍能写状态、语料不确定、探针缺失、**工具的封闭 schema 收不下它自己文档里的参数**、**schema 声明了参数层却静默丢弃的键**)。加 `--self-probe` 会在内存里注入这些缺陷形状,要求对应不变式**变红**、未变异的对照跑**仍为绿**(5/5);脚本自身另带 X5–X8b 六条自检(注释扫描器必须认正则字面量——包括 `return /…/ ` 这种**关键字后面**的正则——字符串里的 `//` 必须保留、抹注释不改变行结构,以及"四套源码抹掉注释后仍必须能被 `node --check` 解析"这条解析级判据)
|
|
821
|
+
- **规格 ↔ 代码可追溯(四套一起)**:[`audit-spec-traceability.mjs`](audit-spec-traceability.mjs)(94 条断言)——`实现方案.md`/README 里承诺的工具必须真的注册;四个 Lean 参数必须同时被文档与代码接受
|
|
822
822
|
|
|
823
823
|
---
|
|
824
824
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# dsh-vibe-math 2.3.3 — 确认轮审计:`used` 回执曾撤销已成立的证明(v2)+ 契约字面纠错 + 守卫自身加固
|
|
2
|
+
|
|
3
|
+
> 上一版:2.3.2。本版是 2.3.2 之后的**确认轮**(round 3)结果:重新审计本轮改动过的每一处
|
|
4
|
+
> (四套插件、四套套件、共享契约、审计脚本自身),又抓到一处**真实缺陷**、一处**契约与实现字面
|
|
5
|
+
> 相反**、以及一处**守卫自身的脆弱点**。无破坏性变更,默认仍为 `formalVerify: 'off'`。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 真实缺陷:一次 `used` 回执会**撤销已成立的证明**(只有 v2)
|
|
10
|
+
|
|
11
|
+
`formal.decision='used'` 的语义是"这一轮碰了形式化 / 写了一份草稿"。契约 §4 里**只有 `defect`
|
|
12
|
+
能撤销证明**,因为撤销意味着审查对象从"忠实性"退回"推导是否正确"、`require` 档对一份**已经跑通的
|
|
13
|
+
归档证明重新关门**。
|
|
14
|
+
|
|
15
|
+
| 架构 | `used` 回执对已有 `passed`/`blocked` 的处理(修复前) |
|
|
16
|
+
|---|---|
|
|
17
|
+
| v3 / v4 / v5 | 保留 `passed`(v3 只保留 passed,`blocked` 会被打回 attempted——见 §2) |
|
|
18
|
+
| **v2** | **无条件写 `status:'attempted'`** —— 而降级发生在**两个 id 空间**上,`proof` 指针还留着,记录自相矛盾 |
|
|
19
|
+
|
|
20
|
+
后果链(实测):对象已 `passed` → 表决者回一句"我这轮又写了遍草稿" → 记录降级为 `attempted`、
|
|
21
|
+
`status.formal.objects` 里不再是 `passed` → 后续审查提示词**丢掉忠实性分支** → `require` 档对一份
|
|
22
|
+
跑通的证明**重新关门**、对象被丢进「形式化待办」。
|
|
23
|
+
|
|
24
|
+
> 这正是 2.3.2 修过的 v4 `formalSetRun` 缺陷的**同一类**("随手一次动作把 passed 抹掉"),
|
|
25
|
+
> 只是长在另一条路径(回执通道)上,而且**只长在一套里**——`AUDIT-CHECKLIST §1.8`(四套同构)
|
|
26
|
+
> 记的就是这种形态。
|
|
27
|
+
|
|
28
|
+
**修复**:v2 的 `used` 分支改为先看**两套 id 空间**的状态(对象侧自己的记录 + `formalGateRecord` 的合并
|
|
29
|
+
视图),取其中**最强**的一个(`passed` > `blocked` > `attempted`):只有两侧都还没尝试过才写 `attempted`,
|
|
30
|
+
否则保留原状态并在活动日志里说明保留了哪一个。四套现在**同构**(v3 另外补上 `blocked` 的保留,见 §2)。
|
|
31
|
+
|
|
32
|
+
## 2. 同一类的第二处:v3 的 `used` 只保留 `passed`,会把 `blocked` 打回 attempted
|
|
33
|
+
|
|
34
|
+
`blocked` 是**门禁已放行**的记录(契约 §4/§8)。v3 的条件写成 `prev.status === 'passed' ? 'passed' : 'attempted'`:
|
|
35
|
+
一个已经用"显式阻塞原因"打开门禁的对象,被一句 `used` 回执打回 `attempted`,门禁**重新关上**。
|
|
36
|
+
已改为 `passed`/`blocked` 都保留(与 v2/v4/v5 一致)。
|
|
37
|
+
|
|
38
|
+
## 3. 契约字面与实现相反(三处)
|
|
39
|
+
|
|
40
|
+
`docs/formal-verification.md` §4 的迁移表是四套的规范来源,但它有两行写错了(代码是对的):
|
|
41
|
+
|
|
42
|
+
| 行 | 原字面 | 现字面 |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `formal:{decision:'used'}` | → `attempted` | → `attempted`;**若已是 `passed`/`blocked` 则保持原状**(撤销只有 `defect` 一条路) |
|
|
45
|
+
| `lean_run` 成功/失败 | → `attempted` | 同样**不降级**已成立的 `passed`/`blocked` |
|
|
46
|
+
| (新增)`lean_archive{kind:'proof'}` + **运行失败** | 未写 | → `attempted`、清空 `proof`、并**撤回**旧的归档证明(工作文件刚被覆盖,旧证明不再对应任何代码) |
|
|
47
|
+
|
|
48
|
+
四套的 `实现方案.md` 与 v5 规格里的对应句子同步改正。规范文档写反最危险的地方在于:**下一次修改是
|
|
49
|
+
照它写的**(2.3.2 修好 `used` 之后,如果只读契约就会把修复改回去)。
|
|
50
|
+
|
|
51
|
+
## 4. 守卫自身的脆弱点:注释扫描器不认正则字面量
|
|
52
|
+
|
|
53
|
+
`audit-prompt-invariants.mjs` 的 I1/I13/I14 都建立在"先把注释抹掉、再检查剩下的代码"之上。四个预设里
|
|
54
|
+
都有 `/[\\/:*?"<>|\u0000-\u001f]+/` —— **字符类里含一个双引号**。原扫描器把那个引号当成字符串开头,
|
|
55
|
+
此后整个文件的词法状态就是错的。实测差异:
|
|
56
|
+
|
|
57
|
+
| 预设 | 两种扫描器**逐行不同**的行数 | 典型后果 |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| v2 | 190 行 | 真实代码行(如 `const abs = …`)被当成注释**抹掉** → 真缺陷可能被漏看 |
|
|
60
|
+
| v3 | 159 行 | 同上 |
|
|
61
|
+
| v4 | 14 行 | 注释被当成代码 → 反模式写进注释就会**误报** |
|
|
62
|
+
| v5 | 121 行 | 同上 |
|
|
63
|
+
|
|
64
|
+
本轮给扫描器加了**正则字面量**支持(含字符类 `[...]`、转义、结尾 flags),并把"守卫的守卫"写成
|
|
65
|
+
**X5–X7 三条自检**(放进 `audit-prompt-invariants.mjs`,任何一次运行都会执行):正则里的引号不得吞掉
|
|
66
|
+
后面的注释、字符串里的 `//`(URL)不得被当成注释、转义斜杠不得提前结束正则,且三者都必须保持行结构。
|
|
67
|
+
实测修复前 X5 变红、修复后全绿。
|
|
68
|
+
|
|
69
|
+
## 5. 本轮新增的行为断言(四套同构,+42 条;另 +6 不变式 / +3 可追溯)
|
|
70
|
+
|
|
71
|
+
| 断言(四套各一份) | 说明 |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `used` 回执**不得**降级已 `passed` 对象 | 记录与 `proof` 指针都保留;v2 修复前**实测变红**(`got attempted`),修复后全绿 |
|
|
74
|
+
| `used` 回执**不得**把 `blocked` 打回 `attempted`(门禁不得被重新关上) | v3 修复前变红 |
|
|
75
|
+
| 一次普通 `lean_run`(带 `target`)**不得**降级 `passed` | 此前只有 v4 套件有这条断言,现四套齐备 |
|
|
76
|
+
| `used` 之后 `require` 门禁**仍然放行** | v2:把该轮驱动到一致为真,断言**没有**产生 `formal-required` 待办 |
|
|
77
|
+
| 真实注册的 schema 必须仍是封闭的、且四个 Lean 参数齐全(2.3.2 的守卫) | 四套各 7 条 |
|
|
78
|
+
|
|
79
|
+
## 5.1 并行回归暴露的**测试脆弱性**(也一并修掉)
|
|
80
|
+
|
|
81
|
+
4 路并行回归第二次跑时 `e2e-v4-fixes` **崩了**(exit 1,只跑了 2 个用例就中止);单独跑 3 次全绿
|
|
82
|
+
(120/120)。形态:驱动循环"安静即停"在框架把卡片落盘**之前**退出 → `assert(existsSync(card))`
|
|
83
|
+
只记录失败**不抛** → 紧随其后的 `readFileSync(card)` 抛 `ENOENT` → **未捕获异常让整个套件中止,
|
|
84
|
+
后面 12 个用例的断言全部丢失**。
|
|
85
|
+
|
|
86
|
+
修法(两条一起):该套件新增 `readArtifact(p, ms=3000)`(先等产物、再防御性读),并把六处"断言后立刻
|
|
87
|
+
读产物"改成"先等产物 → 再断言 → 防御性读"。效果:慢机器上产物晚到 3 秒也能通过;真没写出来则是
|
|
88
|
+
**一条干净的断言失败**,不再吞掉后续用例。这条已写进 `AUDIT-CHECKLIST` §4(连同"发布元数据交给
|
|
89
|
+
序列化器、不要用字符串替换手写 `package.json`"——本轮就这么写出过一次非法 JSON)。
|
|
90
|
+
|
|
91
|
+
## 6. 验收(实测)
|
|
92
|
+
|
|
93
|
+
| 套件 / 脚本 | 2.3.2 | 2.3.3 |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `formal-verify-v2.test.mjs` | 319 | **334** |
|
|
96
|
+
| `formal-verify-v3.test.mjs` | 283 | **294** |
|
|
97
|
+
| `formal-verify-v4.test.mjs` | 269 | **277** |
|
|
98
|
+
| `formal-verify-v5.test.mjs` | 145 | **153** |
|
|
99
|
+
| `audit-prompt-invariants.mjs` | 145 | **151**(+ X5–X7 扫描器自检及其行结构断言) |
|
|
100
|
+
| `audit-spec-traceability.mjs` | 91 | **94**(契约 §4 三行的字面校验) |
|
|
101
|
+
| `audit-prompt-invariants.mjs --self-probe` | 5/5 | **5/5** |
|
|
102
|
+
| `prompt-v5-integrity.test.mjs` | 506 | 506(语料字节稳定) |
|
|
103
|
+
| `audit-persona-surface.test.mjs` | 197 | 197 |
|
|
104
|
+
|
|
105
|
+
- 全量并行回归 23/23 全绿;49 条 formal 探针全红;四套语料(v2/v3/v4/v5 + persona)连续两次哈希一致;
|
|
106
|
+
- closing verification:**18/18 阶段通过**。
|
|
107
|
+
|
|
108
|
+
## 7. 升级
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
npm i dsh-vibe-math@latest
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
无迁移。新参数默认 `off`,四套在 `off` 档的行为与 2.3.2 完全一致;本版改动只影响
|
|
115
|
+
`formalVerify` 非 `off` 时的记账语义(且都是"少撤销、不误关门"方向)。
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# dsh-vibe-math 2.3.4 — 审计守卫的最后一处盲区:正则字面量的**关键字规则** + 解析级扫描器判据
|
|
2
|
+
|
|
3
|
+
> 上一版:2.3.3。本版是 round 4:**只审"上一轮改动过的东西"**——2.3.3 修好的注释扫描器(它是
|
|
4
|
+
> I1/I13/I14 三条不变式的地基)与它新增的 X5–X7 自检。无破坏性变更,默认仍为 `formalVerify: 'off'`。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. 扫描器还剩一个盲区:`return /…/ ` 被当成除法
|
|
9
|
+
|
|
10
|
+
2.3.3 给扫描器加了正则字面量支持,但"这个 `/` 是正则还是除法"的判据**只看前一个字符**:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
if (c === '/' && /[(,=:[!&|?{};+\-*%~^<>]|^$/.test(prev)) → 正则
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
于是**关键字后面**的正则会被读成除法——而 v3 源码里真有这种写法:
|
|
17
|
+
|
|
18
|
+
| 位置 | 代码 |
|
|
19
|
+
|---|---|
|
|
20
|
+
| v3 L2813 | `return /^\s*import\s+/.test(l)` |
|
|
21
|
+
| v3 L2856 | `return /\n$/.test(out) ? out : out + '\n'` |
|
|
22
|
+
|
|
23
|
+
这两处恰好字符类里没有引号,所以**今天**还没坏;但只要有人在 `return /…/` 的正则里写一个引号
|
|
24
|
+
(例如 `return /["']/.test(s)`),扫描器就会把那个引号当成字符串开头,此后整个文件的词法状态都是错的
|
|
25
|
+
——**又回到 2.3.2 修掉的那个坑**(真代码被当注释抹掉 → 真缺陷可能被漏看;注释被当代码 → 误报)。
|
|
26
|
+
这是"守卫自己的守卫"里最后一块没盖住的地方。
|
|
27
|
+
|
|
28
|
+
**修复**:判据从"前一个字符"升级为"前一个**记号**":除运算符/开括号之外,`return` / `typeof` /
|
|
29
|
+
`case` / `delete` / `void` / `instanceof` / `in` / `of` / `yield` / `await` / `new` / `do` / `else`
|
|
30
|
+
之后也按正则解析;标识符、`)`、`]` 之后仍按除法。
|
|
31
|
+
|
|
32
|
+
## 2. 新增**解析级**判据 X8 / X8b(并实测其灵敏度)
|
|
33
|
+
|
|
34
|
+
静态自检(X5–X7)只覆盖我想到的构造;这一版加了一条**不依赖我列举**的强判据:
|
|
35
|
+
|
|
36
|
+
- **X8**:把四套源码各自"抹掉注释"之后,输出**必须仍能被 `node --check` 解析**。扫描器一旦把代码
|
|
37
|
+
读错(正则当除法、引号当字符串开头),产物往往直接语法错误。
|
|
38
|
+
- **X8b**:一个专门针对关键字规则的夹具(`return /["']/.test(l)` + 紧随其后的注释)。
|
|
39
|
+
|
|
40
|
+
灵敏度是**实测**的,不是假设的:
|
|
41
|
+
|
|
42
|
+
| 扫描器版本 | X8(四套源码可解析) | X8b 夹具 |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| 2.3.2(完全不认正则) | **四个预设全部 SyntaxError**(红) | 红 |
|
|
45
|
+
| 2.3.3(认正则、无关键字规则) | 绿(那两处 `return /…/ ` 里没有引号) | **红**:紧随其后的注释没被抹掉(`comment-blanked=false`) |
|
|
46
|
+
| 2.3.4(当前) | 绿 | 绿 |
|
|
47
|
+
|
|
48
|
+
——即:X8 守住大回退,X8b 守住这个窄口子;**2.3.3 的扫描器会被 X8b 判红**,说明这条判据不是摆设。
|
|
49
|
+
不变式 151 → **157**。
|
|
50
|
+
|
|
51
|
+
## 3. 验收(实测)
|
|
52
|
+
|
|
53
|
+
| 套件 / 脚本 | 2.3.3 | 2.3.4 |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `audit-prompt-invariants.mjs` | 151 | **157**(+X8 ×4、+X8b ×2) |
|
|
56
|
+
| `audit-prompt-invariants.mjs --self-probe` | 5/5 | **5/5** |
|
|
57
|
+
| `audit-spec-traceability.mjs` | 94 | 94 |
|
|
58
|
+
| `formal-verify-v2/v3/v4/v5` | 334 / 294 / 277 / 153 | 不变(本版未动插件) |
|
|
59
|
+
| `prompt-v5-integrity.test.mjs` | 506 | 不变 |
|
|
60
|
+
| 全量并行回归 | 23/23 | **23/23** |
|
|
61
|
+
| 49 条 formal 探针 | 全红 | 全红 |
|
|
62
|
+
|
|
63
|
+
## 4. 升级
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
npm i dsh-vibe-math@latest
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
无迁移,无行为变更(本版只改随包发布的审计脚本与文档;四套预设的字节与 2.3.3 相同)。
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
* prove the guard is a guard: re-run itself on mutated sources and require the matching
|
|
27
27
|
* invariant to go RED (control run must stay green)
|
|
28
28
|
*/
|
|
29
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
29
|
+
import { readFileSync, existsSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
30
30
|
import { spawnSync } from 'node:child_process'
|
|
31
|
+
import { tmpdir } from 'node:os'
|
|
31
32
|
import { fileURLToPath } from 'node:url'
|
|
32
33
|
import { join } from 'node:path'
|
|
33
34
|
|
|
@@ -118,24 +119,41 @@ if (process.argv.includes('--self-probe')) {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
/**
|
|
121
|
-
* Blank out comments (line + block) while preserving string/template literals, so invariants
|
|
122
|
-
* "text shown to an agent" do not fire on a COMMENT that quotes an anti-pattern. A naive regex
|
|
122
|
+
* Blank out comments (line + block) while preserving string/template REGEX literals, so invariants
|
|
123
|
+
* about "text shown to an agent" do not fire on a COMMENT that quotes an anti-pattern. A naive regex
|
|
123
124
|
* would both miss block comments and mangle strings containing `//` (URLs), so this walks the
|
|
124
125
|
* source as a tiny scanner. Newlines are preserved to keep any line-based diagnostics aligned.
|
|
126
|
+
*
|
|
127
|
+
* Regex literals matter: all four presets contain `/[\\/:*?"<>|\u0000-\u001f]+/` — a regex whose
|
|
128
|
+
* character class contains a DOUBLE QUOTE. Without regex handling the scanner treats that quote as
|
|
129
|
+
* the start of a string, keeps the state machine wrong for the rest of the file, and then either
|
|
130
|
+
* leaves a comment in the "code" stream (a spurious invariant failure) or blanks real code (a
|
|
131
|
+
* missed defect). X5/X6 below are self-checks for exactly that.
|
|
125
132
|
*/
|
|
126
133
|
function stripComments(src) {
|
|
127
134
|
const out = []
|
|
128
135
|
let i = 0
|
|
129
|
-
let state = 'code' // code | line | block | sq | dq | tpl
|
|
136
|
+
let state = 'code' // code | line | block | sq | dq | tpl | regex
|
|
137
|
+
let prev = '' // last significant code token (a single char, or a whole word such as `return`)
|
|
138
|
+
let word = '' // identifier/keyword accumulator, so `return /re/` is not read as division
|
|
139
|
+
// A `/` starts a REGEX LITERAL after an operator/keyword, and DIVISION after a value. Tracking only
|
|
140
|
+
// the previous CHARACTER is not enough: `return /^\s*import/.test(l)` (v3 really contains it) puts
|
|
141
|
+
// an identifier before the slash. Getting this wrong is exactly the bug X5–X8 guard against.
|
|
142
|
+
const REGEX_AFTER_KEYWORD = /^(?:return|typeof|case|delete|void|instanceof|in|of|yield|await|new|do|else)$/
|
|
130
143
|
while (i < src.length) {
|
|
131
144
|
const c = src[i]
|
|
132
145
|
const c2 = src[i + 1]
|
|
133
146
|
if (state === 'code') {
|
|
134
147
|
if (c === '/' && c2 === '/') { state = 'line'; out.push(' '); i += 2; continue }
|
|
135
148
|
if (c === '/' && c2 === '*') { state = 'block'; out.push(' '); i += 2; continue }
|
|
149
|
+
if (c === '/' && (prev === '' || REGEX_AFTER_KEYWORD.test(prev) || /[(,=:[!&|?{};+\-*%~^<>]/.test(prev))) {
|
|
150
|
+
state = 'regex'; out.push(c); i++; continue
|
|
151
|
+
}
|
|
136
152
|
if (c === "'") state = 'sq'
|
|
137
153
|
else if (c === '"') state = 'dq'
|
|
138
154
|
else if (c === '`') state = 'tpl'
|
|
155
|
+
if (/[A-Za-z0-9_$]/.test(c)) word += c
|
|
156
|
+
else { if (word) { prev = word; word = '' } if (!/\s/.test(c)) prev = c }
|
|
139
157
|
out.push(c); i++; continue
|
|
140
158
|
}
|
|
141
159
|
if (state === 'line') {
|
|
@@ -146,6 +164,22 @@ function stripComments(src) {
|
|
|
146
164
|
if (c === '*' && c2 === '/') { state = 'code'; out.push(' '); i += 2; continue }
|
|
147
165
|
out.push(c === '\n' ? c : ' '); i++; continue
|
|
148
166
|
}
|
|
167
|
+
if (state === 'regex') {
|
|
168
|
+
if (c === '\\') { out.push(c, c2 === undefined ? '' : c2); i += 2; continue }
|
|
169
|
+
if (c === '[') { state = 'regexClass'; out.push(c); i++; continue }
|
|
170
|
+
if (c === '/') { // end of the literal: copy it plus any flags
|
|
171
|
+
out.push(c); i++
|
|
172
|
+
while (i < src.length && /[a-z]/i.test(src[i])) { out.push(src[i]); i++ }
|
|
173
|
+
state = 'code'; prev = ')'
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
out.push(c); i++; continue
|
|
177
|
+
}
|
|
178
|
+
if (state === 'regexClass') {
|
|
179
|
+
if (c === '\\') { out.push(c, c2 === undefined ? '' : c2); i += 2; continue }
|
|
180
|
+
if (c === ']') state = 'regex'
|
|
181
|
+
out.push(c); i++; continue
|
|
182
|
+
}
|
|
149
183
|
// inside a string/template: copy verbatim, honouring escapes and the closing quote
|
|
150
184
|
if (c === '\\') { out.push(c, c2 === undefined ? '' : c2); i += 2; continue }
|
|
151
185
|
if ((state === 'sq' && c === "'") || (state === 'dq' && c === '"') || (state === 'tpl' && c === '`')) state = 'code'
|
|
@@ -363,18 +397,18 @@ for (const P of PRESETS) {
|
|
|
363
397
|
// normalizeParams for v5 (only those keys are copied through).
|
|
364
398
|
let accepts = null
|
|
365
399
|
if (P.tag === 'v5') {
|
|
366
|
-
const fn = code.
|
|
400
|
+
const fn = code.search(/function\s+normalizeParams\s*\(/)
|
|
367
401
|
const region = fn < 0 ? '' : balanced(code, code.indexOf('{', fn))
|
|
368
402
|
const names = []
|
|
369
403
|
for (const kind of ['ints', 'bools', 'strs', 'arrs']) {
|
|
370
|
-
const ai = region.
|
|
404
|
+
const ai = region.search(new RegExp('const\\s+' + kind + '\\s*=\\s*\\['))
|
|
371
405
|
if (ai < 0) continue
|
|
372
406
|
const arr = balanced(region, region.indexOf('[', ai))
|
|
373
407
|
for (const lit of arr.match(/'[A-Za-z_$][\w$]*'/g) || []) names.push(lit.slice(1, -1))
|
|
374
408
|
}
|
|
375
409
|
accepts = names.length ? [...new Set(names)] : null
|
|
376
410
|
} else {
|
|
377
|
-
const di = code.
|
|
411
|
+
const di = code.search(/DEFAULT_PARAMS\s*=\s*\{/)
|
|
378
412
|
accepts = di < 0 ? null : objectKeys(code, di)
|
|
379
413
|
}
|
|
380
414
|
check(accepts !== null && accepts.length > 0, P.tag + ' I14: the parameter accept-set is readable',
|
|
@@ -400,6 +434,66 @@ for (const P of PRESETS) {
|
|
|
400
434
|
check(/argv\[i \+ 1\]/.test(runner) && /argv\[\+\+i\]/.test(runner), 'X3: the suite runner accepts both --flag=x and --flag x')
|
|
401
435
|
}
|
|
402
436
|
|
|
437
|
+
// X5–X7: guard the guard. I1/I13/I14 trust stripComments(), and all four presets contain a regex
|
|
438
|
+
// literal whose character class holds a double quote (`/[\\/:*?"<>|…]+/`). If the scanner mistook
|
|
439
|
+
// that quote for a string start, the rest of the file would be misread — a comment quoting an
|
|
440
|
+
// anti-pattern would look like code (spurious failure) or real code would be blanked (missed bug).
|
|
441
|
+
{
|
|
442
|
+
const fixtureRegex = 'const t = s.replace(/[\\\\/:*?"<>|]+/g, "-")\n// lean_run is NOT a tool name\nconst b = 1'
|
|
443
|
+
check(!stripComments(fixtureRegex).includes('lean_run'),
|
|
444
|
+
'X5: the comment scanner survives a regex literal containing a quote (a following comment stays blanked)')
|
|
445
|
+
const fixtureUrl = "L.push('see https://example.com/a')\nconst c = 2"
|
|
446
|
+
check(stripComments(fixtureUrl).includes('https://example.com/a'),
|
|
447
|
+
'X6: a // inside a STRING is not treated as a comment start (URLs must survive)')
|
|
448
|
+
const fixtureBlock = "const d = /a\\/b/g\n/* lean_lib quoted in a block comment */\nconst e = 3"
|
|
449
|
+
check(!stripComments(fixtureBlock).includes('lean_lib'),
|
|
450
|
+
'X7: an escaped slash inside a regex does not end it early (a block comment after it stays blanked)')
|
|
451
|
+
for (const [label, src] of [['X5', fixtureRegex], ['X6', fixtureUrl], ['X7', fixtureBlock]]) {
|
|
452
|
+
check(stripComments(src).split('\n').length === src.split('\n').length,
|
|
453
|
+
label + ': comment blanking preserves line structure (diagnostics stay aligned)')
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// X8 — the strongest scanner oracle there is: the comment-stripped source of EVERY preset must still
|
|
458
|
+
// PARSE. A scanner that mis-lexes (regex read as division, or a quote inside a regex read as a string
|
|
459
|
+
// start) corrupts real code, and `node --check` sees it. Sensitivity is MEASURED, not assumed:
|
|
460
|
+
// · the 2.3.2 scanner (no regex support at all) → SyntaxError on all four presets → X8 red;
|
|
461
|
+
// · the 2.3.3 scanner (regex-aware but no keyword rule) → X8 still green, which is exactly why
|
|
462
|
+
// X8b below exists (it covers that narrower gap).
|
|
463
|
+
{
|
|
464
|
+
const dir = mkdtempSync(join(tmpdir(), 'prompt-invariants-strip-'))
|
|
465
|
+
try {
|
|
466
|
+
for (const P of PRESETS) {
|
|
467
|
+
const src = read(P.js)
|
|
468
|
+
if (!src) continue
|
|
469
|
+
const f = join(dir, P.tag + '.mjs')
|
|
470
|
+
writeFileSync(f, stripComments(src))
|
|
471
|
+
const r = spawnSync(process.execPath, ['--check', f], { encoding: 'utf8' })
|
|
472
|
+
check(r.status === 0,
|
|
473
|
+
'X8: the comment-stripped source of ' + P.tag + ' is still valid JS (the scanner must not corrupt code)',
|
|
474
|
+
String(r.stderr || '').split('\n').filter((l) => l.trim()).slice(-2).join(' ').slice(0, 160))
|
|
475
|
+
}
|
|
476
|
+
// X8b: a fixture that needs the KEYWORD rule — a regex AFTER `return` whose character class holds
|
|
477
|
+
// a quote (v3 really has `return /^\s*…/.test()`, so the rule is not hypothetical). A scanner that
|
|
478
|
+
// reads that `/` as division enters "string" state at the quote and then leaves the NEXT comment
|
|
479
|
+
// unblanked. Sensitivity MEASURED: the 2.3.3 scanner fails the second assertion below
|
|
480
|
+
// (comment-blanked=false) while passing the first; the current scanner passes both.
|
|
481
|
+
const fixtureKeyword = [
|
|
482
|
+
'function f(l) { return /["\']/.test(l) }',
|
|
483
|
+
'// lean_run must stay blanked',
|
|
484
|
+
'const h = 4 / 2',
|
|
485
|
+
].join('\n')
|
|
486
|
+
const f2 = join(dir, 'fixture.mjs')
|
|
487
|
+
const stripped = stripComments(fixtureKeyword)
|
|
488
|
+
writeFileSync(f2, stripped)
|
|
489
|
+
const r2 = spawnSync(process.execPath, ['--check', f2], { encoding: 'utf8' })
|
|
490
|
+
check(r2.status === 0, 'X8b: a regex after a KEYWORD (return /…/) does not corrupt the scan', String(r2.stderr || '').slice(0, 120))
|
|
491
|
+
check(!stripped.includes('lean_run'), 'X8b: and the comment after it is still blanked (the keyword rule is what decides this)')
|
|
492
|
+
} finally {
|
|
493
|
+
rmSync(dir, { recursive: true, force: true })
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
403
497
|
const out = { passed, failed: failures.length, failures, notes }
|
|
404
498
|
if (process.argv.includes('--json')) {
|
|
405
499
|
console.log(JSON.stringify(out, null, 2))
|
|
@@ -160,6 +160,26 @@ for (const P of PRESETS) {
|
|
|
160
160
|
const contract = read('docs/formal-verification.md') || ''
|
|
161
161
|
for (const prm of LEAN_PARAMS) ok(contract.includes(prm), 'contract documents ' + prm)
|
|
162
162
|
|
|
163
|
+
// The §4 transition table is the normative statement every preset implements. Two rows were WRONG
|
|
164
|
+
// here (the code was right, the contract was not), and a wrong contract is how the next edit gets
|
|
165
|
+
// written:
|
|
166
|
+
// · `used` must NOT withdraw an established proof (only a fidelity `defect` does). v2 actually
|
|
167
|
+
// shipped the downgrade, so the contract literal and reality disagreed in opposite directions.
|
|
168
|
+
// · a RED re-archive of kind='proof' overwrites the work file, so the old archived proof no longer
|
|
169
|
+
// corresponds to any code — it must be retracted, not left at the "everyone looks here" path.
|
|
170
|
+
{
|
|
171
|
+
const usedRow = (contract.split(/\r?\n/).find((l) => /decision:'used'/.test(l)) || '')
|
|
172
|
+
ok(/不得/.test(usedRow) && /passed/.test(usedRow) && /blocked/.test(usedRow),
|
|
173
|
+
"contract §4: the `used` row says an existing passed/blocked is PRESERVED (only `defect` retracts)",
|
|
174
|
+
'row: ' + usedRow.slice(0, 120))
|
|
175
|
+
const runRows = contract.split(/\r?\n/).filter((l) => /^\|\s*`lean_run`/.test(l))
|
|
176
|
+
ok(runRows.length >= 2 && runRows.every((l) => /不降级|保持原状/.test(l)),
|
|
177
|
+
'contract §4: both `lean_run` rows say they do not downgrade an existing passed/blocked',
|
|
178
|
+
'rows: ' + runRows.map((l) => l.slice(0, 60)).join(' || '))
|
|
179
|
+
ok(/该文件最近一次运行\*\*失败\*\*/.test(contract) && /撤回/.test(contract),
|
|
180
|
+
'contract §4: a FAILED proof re-archive is documented as retracting the previous archived proof')
|
|
181
|
+
}
|
|
182
|
+
|
|
163
183
|
const out = { passed, failed: findings.length, findings, notes }
|
|
164
184
|
if (process.argv.includes('--json')) console.log(JSON.stringify(out, null, 2))
|
|
165
185
|
else {
|
|
@@ -141,13 +141,14 @@
|
|
|
141
141
|
|
|
142
142
|
| 事件 | 迁移 |
|
|
143
143
|
|---|---|
|
|
144
|
-
| `lean_run` 成功 | `none`/`attempted` → `attempted
|
|
145
|
-
| `lean_run` 失败 | `none` → `attempted
|
|
144
|
+
| `lean_run` 成功 | `none`/`attempted` → `attempted`(记录运行结果);**已是 `passed`/`blocked` 的保持原状**——一次随手运行不得撤销已成立的证明 |
|
|
145
|
+
| `lean_run` 失败 | `none` → `attempted`(记录失败输出,供代理修复);同样**不降级** `passed`/`blocked` |
|
|
146
146
|
| `lean_archive{kinds:'proof', target, from|content}` + 该文件最近一次运行 `ok` | → `passed`,写 `Verified/Lean/<id>.lean` |
|
|
147
|
+
| `lean_archive{kinds:'proof', …}` + 该文件最近一次运行**失败** | → `attempted`,`proof` 清空,并**撤回**旧的 `Verified/Lean/<id>.lean`(工作文件刚被新代码覆盖,旧证明已不对应任何代码;§4.1 的删除→复核→覆写同一条路) |
|
|
147
148
|
| `lean_archive{kinds:'blocked', target, note}` | → `blocked`(`note` 必填) |
|
|
148
149
|
| 回执里 `formal:{target, decision:'blocked', note}` | → `blocked`(`note` 必填) |
|
|
149
150
|
| **回执里 `formal:{target, decision:'defect', note}`** | **撤回 `passed`:→ `attempted`,清空 `proof`、删除 `Verified/Lean/<id>.lean`、把 `note` 写入记录与 `Formal/TODO.md`、公告**(`note` 必填) |
|
|
150
|
-
| 回执里 `formal:{target, decision:'used', file}` | → `attempted
|
|
151
|
+
| 回执里 `formal:{target, decision:'used', file}` | → `attempted`(记录文件);**若该对象已是 `passed`/`blocked` 则保持原状**——一次"这一轮碰了形式化"的 `used` 回执**不得**撤销已成立的证明(`proof` 指针与归档文件都不变)。撤销只有 `defect` 一条路(§4.1)。 |
|
|
151
152
|
|
|
152
153
|
### 4.1 `defect`:忠实性缺陷**不是**"命题为假"
|
|
153
154
|
|
package/docs/test-timing.md
CHANGED
|
@@ -13,8 +13,8 @@ node run-tests.mjs --only formal # 只跑名字含 formal 的套件
|
|
|
13
13
|
node run-tests.mjs --concurrency=6 # 手动指定并发
|
|
14
14
|
node audit-formal-sensitivity.mjs # 49 条不变式探针,并行(--concurrency=N / --only=<preset> / --list)
|
|
15
15
|
node audit-persona-sensitivity.mjs # 11 条提示词面探针(串行,本身只要几秒)
|
|
16
|
-
node audit-prompt-invariants.mjs #
|
|
17
|
-
node audit-prompt-invariants.mjs --self-probe # 证明上面那
|
|
16
|
+
node audit-prompt-invariants.mjs # 静态:四套的提示词/工具面不变式 + 扫描器自检(< 0.1s)
|
|
17
|
+
node audit-prompt-invariants.mjs --self-probe # 证明上面那 157 条不变式真的会变红(5 个自探针)
|
|
18
18
|
node audit-spec-traceability.mjs # 静态:规格/README ↔ 代码可追溯(< 0.1s)
|
|
19
19
|
node audit-v5-integrity.mjs # 静态:v5 完整性/理念门禁(≈3 s)
|
|
20
20
|
```
|
|
@@ -28,9 +28,9 @@ node audit-v5-integrity.mjs # 静态:v5 完整性/理念门禁(
|
|
|
28
28
|
| `run-tests.mjs`(23 个套件) | 221.5 s | **111.5 s**(并发 4,speed-up x1.99) | 关键路径 = `e2e-v4-fixes` 98.1 s |
|
|
29
29
|
| `audit-formal-sensitivity.mjs`(49 探针) | 612.0 s | **154.6 s**(并发 4,speed-up x3.96) | 关键路径 = 12 个 v2 探针(每个 ≈32 s) |
|
|
30
30
|
| `audit-persona-sensitivity.mjs`(11 探针) | ≈ 5 s | — | 本身很快,不需要并行 |
|
|
31
|
-
| `audit-prompt-invariants.mjs`(
|
|
31
|
+
| `audit-prompt-invariants.mjs`(157 条,含 X5–X8b 扫描器自检) | 0.4 s | — | 静态 |
|
|
32
32
|
| `audit-prompt-invariants.mjs --self-probe`(5 探针) | 1.5 s | — | 每个探针 = 一次自我重跑(0.3 s) |
|
|
33
|
-
| `audit-spec-traceability.mjs`(
|
|
33
|
+
| `audit-spec-traceability.mjs`(94 条) | 0.3 s | — | 静态 |
|
|
34
34
|
| `audit-v5-integrity.mjs` | ≈ 3 s | — | 静态审计 |
|
|
35
35
|
| `prompt-v5-integrity.test.mjs` | 1.6 s | — | 虚拟时钟下生成 v5 语料(语料字节稳定) |
|
|
36
36
|
|
|
@@ -43,7 +43,7 @@ node audit-v5-integrity.mjs # 静态:v5 完整性/理念门禁(
|
|
|
43
43
|
| 套件 | 耗时 | 备注 |
|
|
44
44
|
|---|---|---|
|
|
45
45
|
| `e2e-v4-fixes.test.mjs` | **≈ 98 s** | 9 个用例是**轮次采样**型(如 T13 采样 400 轮、T19/T25 多轮);时间 ≈ 轮数 × 框架自身的 40 ms 计时粒度 |
|
|
46
|
-
| `formal-verify-v2.test.mjs` | **≈
|
|
46
|
+
| `formal-verify-v2.test.mjs` | **≈ 38 s** | 曾为 186 s:见 §3 |
|
|
47
47
|
| `e2e-regression.test.mjs` | ≈ 14 s | |
|
|
48
48
|
| `e2e-business.test.mjs` | ≈ 13 s | |
|
|
49
49
|
| `e2e-d9-d13.test.mjs` | ≈ 13 s | |
|
|
@@ -1116,6 +1116,52 @@ section('15 the require gate also covers the judge-problem transfer')
|
|
|
1116
1116
|
// ---------- 16. the prompt corpus (contract §10.10) ----------
|
|
1117
1117
|
// A HUMAN must be able to re-read every prompt the framework emitted, not just the assertions
|
|
1118
1118
|
// about them. Paths are normalised so the dump is deterministic, diffable and machine-free.
|
|
1119
|
+
section("15b a \`used\` reply must NOT downgrade an already-passed object")
|
|
1120
|
+
{
|
|
1121
|
+
// Contract §4: a fidelity defect WITHDRAWS a proof; a plain `used` judgement merely says "this round
|
|
1122
|
+
// touched the formalization". Letting `used` downgrade an established proof would silently remove the
|
|
1123
|
+
// fidelity branch from every later review prompt and re-close the require gate — the exact class that
|
|
1124
|
+
// was already fixed once for v4's formalSetRun. v3/v4/v5 preserve passed/blocked here; v2 did not.
|
|
1125
|
+
const h = await makeCase('used-keep')
|
|
1126
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1127
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1128
|
+
await startScheduler(h)
|
|
1129
|
+
const proj = projRoot(h)
|
|
1130
|
+
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pUsedKeep', content: 'theorem p_used_keep : 2 + 2 = 4 := by decide\n' })
|
|
1131
|
+
assert(proof.ok === true && proof.passed === true, 'used-keep: the object starts Lean-passed')
|
|
1132
|
+
const proofFile = join(proj, 'Verified', 'Lean', 'pUsedKeep.lean')
|
|
1133
|
+
assert(existsSync(proofFile), 'used-keep: the archived proof exists before the used reply')
|
|
1134
|
+
await h.call('vibe_math_add_proposition', { id: 'pUsedKeep', 概述: '已有通过证明后再写一次 used 回执', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1135
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pUsedKeep'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1136
|
+
assert(!!vs, 'used-keep: verifiers were spawned for the Lean-passed object')
|
|
1137
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pUsedKeep:0') || {}).prompt || ''
|
|
1138
|
+
assert(/忠实性审查/.test(vp), 'used-keep: the reviewers were told to audit FIDELITY (the Lean code is the subject)')
|
|
1139
|
+
if (vs) {
|
|
1140
|
+
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '这一轮只是又写了一遍草稿', formal: { target: 'pUsedKeep', decision: 'used', file: 'Formal/pUsedKeep.lean' } })
|
|
1141
|
+
// The reply is what materialises the canonical verification alias r-<objectId>, so waiting for
|
|
1142
|
+
// THAT is a signal only the reply can produce (the archive already wrote decision:'used').
|
|
1143
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['r-pUsedKeep'] && r['pUsedKeep']) ? r : undefined }, 40, 150)
|
|
1144
|
+
assert(!!rec, 'used-keep: the `used` reply is absorbed (the canonical verification alias was materialised)')
|
|
1145
|
+
assert(!!rec && rec['pUsedKeep'].status === 'passed', '★★ a `used` reply does NOT downgrade an already-passed object (got ' + (rec && rec['pUsedKeep'] ? rec['pUsedKeep'].status : 'no record') + ')')
|
|
1146
|
+
assert(!!rec && rec['pUsedKeep'].proof === 'Verified/Lean/pUsedKeep.lean', '★★ and its proof pointer survives the reply')
|
|
1147
|
+
assert(!!rec && !!rec['r-pUsedKeep'] && rec['r-pUsedKeep'].status === 'passed', '★★ the verification-id alias keeps passed too (both id spaces)')
|
|
1148
|
+
assert(existsSync(proofFile), '★★ the archived proof is still on disk')
|
|
1149
|
+
const st = await h.call('vibe_math_status', {})
|
|
1150
|
+
const obj = (st.formal.objects || []).find((o) => o.target === 'pUsedKeep')
|
|
1151
|
+
assert(!!obj && obj.status === 'passed', '★★ status still reports the object as Lean-passed (v2 exposes status.formal.objects, not a "passed" array)')
|
|
1152
|
+
// The consequence that actually matters: with 'passed' preserved the require gate stays OPEN, so
|
|
1153
|
+
// driving the round to a unanimous "true" still concludes (a wrongly downgraded object is deferred).
|
|
1154
|
+
const settled = await verifyWithDebate(h, 'r-pUsedKeep', 1, 0)
|
|
1155
|
+
assert(!!settled.first, 'used-keep: the round can still be driven to a verdict')
|
|
1156
|
+
assert(!/pUsedKeep/.test(readIf(join(proj, 'Formal', 'TODO.md')) || ''), '★★ no formal-required TODO was created: the surviving passed status still opens the require gate')
|
|
1157
|
+
// The same rule covers the RUN path (docs §4): a run attributed to the object may not downgrade it.
|
|
1158
|
+
const rerun = await h.call('vibe_math_lean_run', { file: 'Formal/pUsedKeep.lean', target: 'pUsedKeep' })
|
|
1159
|
+
assert(rerun.ok === true, 'used-keep: the object work file runs green (the record assertion below is the point)')
|
|
1160
|
+
const obj2 = ((await h.call('vibe_math_status', {})).formal.objects || []).find((o) => o.target === 'pUsedKeep')
|
|
1161
|
+
assert(!!obj2 && obj2.status === 'passed', '★★ and the record is still passed (a scratch run may not withdraw a proof)')
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1119
1165
|
section('16 the captured prompt corpus is written for human review')
|
|
1120
1166
|
{
|
|
1121
1167
|
// Freeze the scheduler in every case FIRST: a still-running tick loop could emit one more
|
|
@@ -1110,6 +1110,60 @@ await callTool('vibe_math_abort', {}, RK)
|
|
|
1110
1110
|
// ===============================================================
|
|
1111
1111
|
// 9. reporting + persistence
|
|
1112
1112
|
// ===============================================================
|
|
1113
|
+
// (d) a plain `used` judgement must NOT withdraw an ESTABLISHED proof (contract §4). Only a
|
|
1114
|
+
// fidelity defect retracts a proof; `used` just reports "this round touched the formalization".
|
|
1115
|
+
// v2 shipped the unconditional downgrade here and silently re-closed the require gate.
|
|
1116
|
+
section('8d a \`used\` reply must NOT downgrade an already-passed object')
|
|
1117
|
+
await restart(RG)
|
|
1118
|
+
await callTool('vibe_math_add_proposition', { id: 'p-usedkeep', 概述: '已有通过证明后再写一次 used 回执', 概率: 0.6, 分类: '数论' }, RG)
|
|
1119
|
+
{
|
|
1120
|
+
const pass = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-usedkeep', content: 'theorem p_usedkeep : 2 + 2 = 4 := by decide\n' }, RG)
|
|
1121
|
+
assert(pass.ok === true && pass.passed === true, 'used-keep: the object starts out Lean-passed')
|
|
1122
|
+
const re = verifyRe('p-usedkeep')
|
|
1123
|
+
assert(await drive(RG, () => unfiredVerifiers(RG, re).length >= 2, 'verifiers for r-p-usedkeep'), 'used-keep: the Lean-passed object is put to a fidelity review')
|
|
1124
|
+
const vs = unfiredVerifiers(RG, re).slice(0, 2)
|
|
1125
|
+
if (vs.length === 2) {
|
|
1126
|
+
for (const v of vs) firedChildren.add(v.childId)
|
|
1127
|
+
fireEnd(vs[0].childId, { Result: 0.5, Reason: '这一轮只是又写了一遍草稿', formal: { target: 'p-usedkeep', decision: 'used', file: 'Formal/p-usedkeep.lean' } })
|
|
1128
|
+
await sleep(150)
|
|
1129
|
+
const st = await callTool('vibe_math_status', {}, RG)
|
|
1130
|
+
const o = st.formal.objects.find((x) => x.target === 'p-usedkeep')
|
|
1131
|
+
assert(!!o && o.status === 'passed', '★ a `used` reply does NOT downgrade an already-passed object (got ' + JSON.stringify(o) + ')')
|
|
1132
|
+
assert(st.formal.passed.indexOf('p-usedkeep') !== -1, '★ and status still reports it as Lean-passed (the fidelity branch stays the reviewers\' subject)')
|
|
1133
|
+
assert(existsSync(join(replyProj, 'Verified', 'Lean', 'p-usedkeep.lean')), '★ and the archived proof is still on disk by that name')
|
|
1134
|
+
fireEnd(vs[1].childId, { Result: 0.5, Reason: '同样只是又写了一遍草稿' })
|
|
1135
|
+
await sleep(250)
|
|
1136
|
+
assert(!/p-usedkeep/.test(readIf(join(replyProj, 'Formal', 'TODO.md')) || ''), '★ no formal-required TODO was created: the surviving passed still opens the require gate')
|
|
1137
|
+
// The same rule covers the RUN path (contract §4): a run attributed to the object may not downgrade it.
|
|
1138
|
+
const rerun = await callTool('vibe_math_lean_run', { file: 'Formal/p-usedkeep.lean', target: 'p-usedkeep' }, RG)
|
|
1139
|
+
assert(rerun.ok === true, 'used-keep: the object work file runs green (the record assertion below is the point)')
|
|
1140
|
+
const o2 = (await callTool('vibe_math_status', {}, RG)).formal.objects.find((x) => x.target === 'p-usedkeep')
|
|
1141
|
+
assert(!!o2 && o2.status === 'passed', '★ and the record is still passed')
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// (e) the same rule for `blocked`: a blocker is a gate-opening record, so a later `used` reply
|
|
1146
|
+
// must not push the object back to `attempted` and silently re-close the gate (v3 used to preserve
|
|
1147
|
+
// only `passed` here — v2/v4/v5 preserve both).
|
|
1148
|
+
await restart(RG)
|
|
1149
|
+
await callTool('vibe_math_add_proposition', { id: 'p-usedblocked', 概述: '已记录阻塞后再写一次 used 回执', 概率: 0.6, 分类: '数论' }, RG)
|
|
1150
|
+
{
|
|
1151
|
+
const blk = await callTool('vibe_math_lean_archive', { kind: 'blocked', target: 'p-usedblocked', note: '需要大量未形式化的实分析前置知识' }, RG)
|
|
1152
|
+
assert(blk.ok === true && blk.status === 'blocked', 'used-blocked: the object starts with an explicit blocker (the gate is open)')
|
|
1153
|
+
const re = verifyRe('p-usedblocked')
|
|
1154
|
+
assert(await drive(RG, () => unfiredVerifiers(RG, re).length >= 2, 'verifiers for r-p-usedblocked'), 'used-blocked: the object is put to a vote')
|
|
1155
|
+
const vs = unfiredVerifiers(RG, re).slice(0, 2)
|
|
1156
|
+
if (vs.length === 2) {
|
|
1157
|
+
for (const v of vs) firedChildren.add(v.childId)
|
|
1158
|
+
fireEnd(vs[0].childId, { Result: 0.5, Reason: '这一轮只是又写了一遍草稿', formal: { target: 'p-usedblocked', decision: 'used', file: 'Formal/p-usedblocked.lean' } })
|
|
1159
|
+
await sleep(150)
|
|
1160
|
+
const o = (await callTool('vibe_math_status', {}, RG)).formal.objects.find((x) => x.target === 'p-usedblocked')
|
|
1161
|
+
assert(!!o && o.status === 'blocked', '★ a `used` reply does not re-close a gate an explicit blocker opened (got ' + JSON.stringify(o) + ')')
|
|
1162
|
+
fireEnd(vs[1].childId, { Result: 0.5, Reason: '同上' })
|
|
1163
|
+
await sleep(200)
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1113
1167
|
section('9 the office can audit formal strength')
|
|
1114
1168
|
{
|
|
1115
1169
|
const rep = await callTool('vibe_math_report', {}, RF)
|