dsh-vibe-math 2.4.1 → 2.6.0
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/README.en.md +53 -14
- package/README.md +54 -14
- package/cordis.patch.yml +54 -17
- package/docs/AUDIT-CHECKLIST.md +26 -7
- package/docs/COMPAT-AUDIT-ROUND3.md +16 -1
- package/docs/final-paper.md +171 -0
- package/docs/release-notes/RELEASE-NOTES-2.5.0.md +173 -0
- package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -0
- package/docs/test-timing.md +77 -40
- package/installer.js +225 -45
- package/package.json +6 -3
- package/preset-declaration.js +38 -7
- package/prompt-corpus-persona/persona-corpus.json +13 -13
- package/prompt-corpus-persona/persona-corpus.md +113 -39
- package/prompt-corpus-v2/formal-verify-v2.json +128 -92
- package/prompt-corpus-v2/formal-verify-v2.md +380 -142
- package/prompt-corpus-v3/formal-verify-v3.json +117 -81
- package/prompt-corpus-v3/formal-verify-v3.md +693 -1189
- package/prompt-corpus-v4/formal-verify-v4.json +38 -8
- package/prompt-corpus-v4/formal-verify-v4.md +157 -25
- package/prompt-corpus-v5/prompt-corpus-v5.json +17 -17
- package/prompt-corpus-v5/prompt-corpus-v5.md +32 -26
- package/tests/audit-installer-compat.test.mjs +70 -4
- package/tests/audit-installer-policy.test.mjs +229 -4
- package/tests/audit-persona-sensitivity.mjs +68 -8
- package/tests/audit-persona-surface.test.mjs +21 -8
- package/tests/audit-v5-sensitivity.mjs +47 -6
- package/tests/e2e-v5-round2.test.mjs +825 -1
- package/tests/formal-verify-v2.test.mjs +57 -2
- package/tests/formal-verify-v3.test.mjs +67 -2
- package/tests/formal-verify-v4.test.mjs +120 -4
- package/tests/prompt-v5-integrity.test.mjs +9 -3
- package/tests/run-tests.mjs +104 -23
- package/tests/selfdrive-v5.mjs +55 -4
- package/vibe-math-v2/agent.cordis.yml +20 -4
- package/vibe-math-v2/vibe-math-v2.js +1318 -106
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +20 -4
- package/vibe-math-v3/agent.cordis.yml +36 -14
- package/vibe-math-v3/vibe-math-v3.js +1348 -179
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +3 -3
- package/vibe-math-v4/agent.cordis.yml +36 -8
- package/vibe-math-v4/vibe-math-v4.js +1195 -57
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +91 -9
- package/vibe-math-v5/agent.cordis.yml +16 -8
- package/vibe-math-v5/vibe-math-v5.js +1628 -112
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +143 -41
|
@@ -595,7 +595,7 @@
|
|
|
595
595
|
|
|
596
596
|
【五、表决与定论(求真门槛)】
|
|
597
597
|
· 任何命题 / 论断 / 方法 / 子问题的结论,要进入 Verified/,必须满足:
|
|
598
|
-
(a) 至少有 m =
|
|
598
|
+
(a) 至少有 m = 1 名有表决权者(院士 + 常驻研究员)投出**布尔概率值**;
|
|
599
599
|
(b) 这些票**全部**是 1(绝对为真)或**全部**是 0(绝对为假);
|
|
600
600
|
(c) 若同时出现 1 和 0(分歧),或投布尔票者不足 m 人 → 不能定论。
|
|
601
601
|
· m 随在册有表决权者人数变化(m = min(所办设定的上限, 人数));本规章里的 m 是
|
|
@@ -808,7 +808,7 @@
|
|
|
808
808
|
|
|
809
809
|
【五、表决与定论(求真门槛)】
|
|
810
810
|
· 任何命题 / 论断 / 方法 / 子问题的结论,要进入 Verified/,必须满足:
|
|
811
|
-
(a) 至少有 m =
|
|
811
|
+
(a) 至少有 m = 2 名有表决权者(院士 + 常驻研究员)投出**布尔概率值**;
|
|
812
812
|
(b) 这些票**全部**是 1(绝对为真)或**全部**是 0(绝对为假);
|
|
813
813
|
(c) 若同时出现 1 和 0(分歧),或投布尔票者不足 m 人 → 不能定论。
|
|
814
814
|
· m 随在册有表决权者人数变化(m = min(所办设定的上限, 人数));本规章里的 m 是
|
|
@@ -3009,7 +3009,7 @@ r-2:初始见解。
|
|
|
3009
3009
|
### 提示词原文
|
|
3010
3010
|
|
|
3011
3011
|
```text
|
|
3012
|
-
[核心规则] 只有 Verified/(及标记"已验证·真/假"的卡片)算已确立;任何对象要进 Verified
|
|
3012
|
+
[核心规则] 只有 Verified/(及标记"已验证·真/假"的卡片)算已确立;任何对象要进 Verified/,必须至少有 m 名有表决权者投出布尔值(恰好 1 或恰好 0)**且没有任何一张反向票**,否则留库附平均概率;你只写自己的库(Members/<你>/),可只读任何人的库;组织与分派由院士负责,但判断属于你自己;退出时只输出一个 JSON 对象。
|
|
3013
3013
|
[CONTEXT COMPACT — 你的对话已接近上限。不要重新推导历史。
|
|
3014
3014
|
请把当前工作状态浓缩成一段自述(已有发现、当前方向、已记录的关键成果、下一步具体动作、未决问题),然后照常以 JSON 回答本轮。请在回复里填 "contextPct": 15 与 "compacted": true。]
|
|
3015
3015
|
|
|
@@ -3234,7 +3234,7 @@ r-2:初始见解。
|
|
|
3234
3234
|
"task_done": "t-3",
|
|
3235
3235
|
"task_update": {"task_id":"t-3","expected_revision":2,"action":"complete|release|reopen|edit|set_dependencies|delete"},
|
|
3236
3236
|
"input": "本轮会议/辩论的发言正文(会议轮用;也可直接用 say)",
|
|
3237
|
-
"formal": {"target":"p-x","decision":"used|blocked|defect","file":"Formal/p-x.lean","note":"
|
|
3237
|
+
"formal": {"target":"p-x","decision":"used|blocked|defect","file":"Formal/p-x.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}
|
|
3238
3238
|
← Lean 形式化:**鼓励**:按实现难度自行决定;做了就归档,没做就写明难度判断,详见提示词里的【Lean 形式化验证】段
|
|
3239
3239
|
"reject_assign": {"task_id":"t-3","why":"你对这项分派的异议理由"} ← 有异议时填;理由会被广播给
|
|
3240
3240
|
全体表决者(任务仍会执行,但你的理由不会被埋掉),
|
|
@@ -3277,7 +3277,7 @@ r-2:初始见解。
|
|
|
3277
3277
|
命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。
|
|
3278
3278
|
· 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision='blocked' 时必须写明 note)。
|
|
3279
3279
|
· 归档可复用定义/引理前先跑通(vibe_v5_lean_archive run=true 或先 vibe_v5_lean_run);跑不通不要入库。
|
|
3280
|
-
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS
|
|
3280
|
+
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并把"宿主无 Lean 工具链"写成**阻塞记录**(vibe_v5_lean_archive kind='blocked' note=… 或回执 formal:{decision:'blocked', note:…})——这算显式阻塞原因,定论门禁可以据此放行。
|
|
3281
3281
|
▸ 若你在本轮把它形式化并跑通(vibe_v5_lean_archive kind='proof'),后续轮次的
|
|
3282
3282
|
审查对象就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。
|
|
3283
3283
|
|
|
@@ -3292,7 +3292,7 @@ r-2:初始见解。
|
|
|
3292
3292
|
结束时请**只**输出一个 JSON 对象(```json 围栏内):
|
|
3293
3293
|
{"verdict":{"target":"p-lean-a","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}
|
|
3294
3294
|
若你本轮做了形式化或给出难度判断,请一并加上:
|
|
3295
|
-
{"formal":{"target":"p-lean-a","decision":"used|blocked|defect","file":"Formal/p-lean-a.lean","note":"
|
|
3295
|
+
{"formal":{"target":"p-lean-a","decision":"used|blocked|defect","file":"Formal/p-lean-a.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}}
|
|
3296
3296
|
```
|
|
3297
3297
|
|
|
3298
3298
|
---
|
|
@@ -3325,7 +3325,7 @@ r-2:初始见解。
|
|
|
3325
3325
|
命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。
|
|
3326
3326
|
· 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision='blocked' 时必须写明 note)。
|
|
3327
3327
|
· 归档可复用定义/引理前先跑通(vibe_v5_lean_archive run=true 或先 vibe_v5_lean_run);跑不通不要入库。
|
|
3328
|
-
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS
|
|
3328
|
+
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并把"宿主无 Lean 工具链"写成**阻塞记录**(vibe_v5_lean_archive kind='blocked' note=… 或回执 formal:{decision:'blocked', note:…})——这算显式阻塞原因,定论门禁可以据此放行。
|
|
3329
3329
|
▸ 若你在本轮把它形式化并跑通(vibe_v5_lean_archive kind='proof'),后续轮次的
|
|
3330
3330
|
审查对象就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。
|
|
3331
3331
|
|
|
@@ -3340,7 +3340,7 @@ r-2:初始见解。
|
|
|
3340
3340
|
结束时请**只**输出一个 JSON 对象(```json 围栏内):
|
|
3341
3341
|
{"verdict":{"target":"p-lean-a","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}
|
|
3342
3342
|
若你本轮做了形式化或给出难度判断,请一并加上:
|
|
3343
|
-
{"formal":{"target":"p-lean-a","decision":"used|blocked|defect","file":"Formal/p-lean-a.lean","note":"
|
|
3343
|
+
{"formal":{"target":"p-lean-a","decision":"used|blocked|defect","file":"Formal/p-lean-a.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}}
|
|
3344
3344
|
```
|
|
3345
3345
|
|
|
3346
3346
|
---
|
|
@@ -3373,7 +3373,7 @@ r-2:初始见解。
|
|
|
3373
3373
|
命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。
|
|
3374
3374
|
· 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision='blocked' 时必须写明 note)。
|
|
3375
3375
|
· 归档可复用定义/引理前先跑通(vibe_v5_lean_archive run=true 或先 vibe_v5_lean_run);跑不通不要入库。
|
|
3376
|
-
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS
|
|
3376
|
+
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并把"宿主无 Lean 工具链"写成**阻塞记录**(vibe_v5_lean_archive kind='blocked' note=… 或回执 formal:{decision:'blocked', note:…})——这算显式阻塞原因,定论门禁可以据此放行。
|
|
3377
3377
|
▸ 若你在本轮把它形式化并跑通(vibe_v5_lean_archive kind='proof'),后续轮次的
|
|
3378
3378
|
审查对象就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。
|
|
3379
3379
|
|
|
@@ -3388,7 +3388,7 @@ r-2:初始见解。
|
|
|
3388
3388
|
结束时请**只**输出一个 JSON 对象(```json 围栏内):
|
|
3389
3389
|
{"verdict":{"target":"p-lean-a","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}
|
|
3390
3390
|
若你本轮做了形式化或给出难度判断,请一并加上:
|
|
3391
|
-
{"formal":{"target":"p-lean-a","decision":"used|blocked|defect","file":"Formal/p-lean-a.lean","note":"
|
|
3391
|
+
{"formal":{"target":"p-lean-a","decision":"used|blocked|defect","file":"Formal/p-lean-a.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}}
|
|
3392
3392
|
```
|
|
3393
3393
|
|
|
3394
3394
|
---
|
|
@@ -3435,7 +3435,7 @@ r-2:初始见解。
|
|
|
3435
3435
|
结束时请**只**输出一个 JSON 对象(```json 围栏内):
|
|
3436
3436
|
{"verdict":{"target":"p-lean-b","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}
|
|
3437
3437
|
若你本轮做了形式化或给出难度判断,请一并加上:
|
|
3438
|
-
{"formal":{"target":"p-lean-b","decision":"used|blocked|defect","file":"Formal/p-lean-b.lean","note":"
|
|
3438
|
+
{"formal":{"target":"p-lean-b","decision":"used|blocked|defect","file":"Formal/p-lean-b.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}}
|
|
3439
3439
|
```
|
|
3440
3440
|
|
|
3441
3441
|
---
|
|
@@ -3482,7 +3482,7 @@ r-2:初始见解。
|
|
|
3482
3482
|
结束时请**只**输出一个 JSON 对象(```json 围栏内):
|
|
3483
3483
|
{"verdict":{"target":"p-lean-b","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}
|
|
3484
3484
|
若你本轮做了形式化或给出难度判断,请一并加上:
|
|
3485
|
-
{"formal":{"target":"p-lean-b","decision":"used|blocked|defect","file":"Formal/p-lean-b.lean","note":"
|
|
3485
|
+
{"formal":{"target":"p-lean-b","decision":"used|blocked|defect","file":"Formal/p-lean-b.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}}
|
|
3486
3486
|
```
|
|
3487
3487
|
|
|
3488
3488
|
---
|
|
@@ -3529,7 +3529,7 @@ r-2:初始见解。
|
|
|
3529
3529
|
结束时请**只**输出一个 JSON 对象(```json 围栏内):
|
|
3530
3530
|
{"verdict":{"target":"p-lean-b","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}
|
|
3531
3531
|
若你本轮做了形式化或给出难度判断,请一并加上:
|
|
3532
|
-
{"formal":{"target":"p-lean-b","decision":"used|blocked|defect","file":"Formal/p-lean-b.lean","note":"
|
|
3532
|
+
{"formal":{"target":"p-lean-b","decision":"used|blocked|defect","file":"Formal/p-lean-b.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}}
|
|
3533
3533
|
```
|
|
3534
3534
|
|
|
3535
3535
|
---
|
|
@@ -3560,11 +3560,13 @@ r-2:初始见解。
|
|
|
3560
3560
|
已证引理放 <VIBEMATH>/Formal/Proved/;写之前先 vibe_v5_lean_lib 查重。
|
|
3561
3561
|
· **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义 / 对象 / 条件 / 假设 / 结论是否与
|
|
3562
3562
|
命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。
|
|
3563
|
-
· **本模式要求**:必须产出 Lean
|
|
3564
|
-
kind='blocked' note=…
|
|
3563
|
+
· **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因——用
|
|
3564
|
+
vibe_v5_lean_archive kind='blocked' note=… 记录,或用回执 formal:{decision:'blocked', note:…}。
|
|
3565
|
+
只有 decision='blocked' 的 note 会写成阻塞记录;decision='used' 的 note 只是难度判断,
|
|
3566
|
+
**不会**打开定论门禁。两者都没有时,本次裁定不会生效,
|
|
3565
3567
|
会被记为未定论(原因 formal-required)并进入「形式化待办」。
|
|
3566
3568
|
· 归档可复用定义/引理前先跑通(vibe_v5_lean_archive run=true 或先 vibe_v5_lean_run);跑不通不要入库。
|
|
3567
|
-
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS
|
|
3569
|
+
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并把"宿主无 Lean 工具链"写成**阻塞记录**(vibe_v5_lean_archive kind='blocked' note=… 或回执 formal:{decision:'blocked', note:…})——这算显式阻塞原因,定论门禁可以据此放行。
|
|
3568
3570
|
▸ 若你在本轮把它形式化并跑通(vibe_v5_lean_archive kind='proof'),后续轮次的
|
|
3569
3571
|
审查对象就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。
|
|
3570
3572
|
|
|
@@ -3579,7 +3581,7 @@ r-2:初始见解。
|
|
|
3579
3581
|
结束时请**只**输出一个 JSON 对象(```json 围栏内):
|
|
3580
3582
|
{"verdict":{"target":"p-lean-req","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}
|
|
3581
3583
|
若你本轮做了形式化或给出难度判断,请一并加上:
|
|
3582
|
-
{"formal":{"target":"p-lean-req","decision":"used|blocked|defect","file":"Formal/p-lean-req.lean","note":"
|
|
3584
|
+
{"formal":{"target":"p-lean-req","decision":"used|blocked|defect","file":"Formal/p-lean-req.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}}
|
|
3583
3585
|
```
|
|
3584
3586
|
|
|
3585
3587
|
---
|
|
@@ -3610,11 +3612,13 @@ r-2:初始见解。
|
|
|
3610
3612
|
已证引理放 <VIBEMATH>/Formal/Proved/;写之前先 vibe_v5_lean_lib 查重。
|
|
3611
3613
|
· **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义 / 对象 / 条件 / 假设 / 结论是否与
|
|
3612
3614
|
命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。
|
|
3613
|
-
· **本模式要求**:必须产出 Lean
|
|
3614
|
-
kind='blocked' note=…
|
|
3615
|
+
· **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因——用
|
|
3616
|
+
vibe_v5_lean_archive kind='blocked' note=… 记录,或用回执 formal:{decision:'blocked', note:…}。
|
|
3617
|
+
只有 decision='blocked' 的 note 会写成阻塞记录;decision='used' 的 note 只是难度判断,
|
|
3618
|
+
**不会**打开定论门禁。两者都没有时,本次裁定不会生效,
|
|
3615
3619
|
会被记为未定论(原因 formal-required)并进入「形式化待办」。
|
|
3616
3620
|
· 归档可复用定义/引理前先跑通(vibe_v5_lean_archive run=true 或先 vibe_v5_lean_run);跑不通不要入库。
|
|
3617
|
-
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS
|
|
3621
|
+
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并把"宿主无 Lean 工具链"写成**阻塞记录**(vibe_v5_lean_archive kind='blocked' note=… 或回执 formal:{decision:'blocked', note:…})——这算显式阻塞原因,定论门禁可以据此放行。
|
|
3618
3622
|
▸ 若你在本轮把它形式化并跑通(vibe_v5_lean_archive kind='proof'),后续轮次的
|
|
3619
3623
|
审查对象就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。
|
|
3620
3624
|
|
|
@@ -3629,7 +3633,7 @@ r-2:初始见解。
|
|
|
3629
3633
|
结束时请**只**输出一个 JSON 对象(```json 围栏内):
|
|
3630
3634
|
{"verdict":{"target":"p-lean-req","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}
|
|
3631
3635
|
若你本轮做了形式化或给出难度判断,请一并加上:
|
|
3632
|
-
{"formal":{"target":"p-lean-req","decision":"used|blocked|defect","file":"Formal/p-lean-req.lean","note":"
|
|
3636
|
+
{"formal":{"target":"p-lean-req","decision":"used|blocked|defect","file":"Formal/p-lean-req.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}}
|
|
3633
3637
|
```
|
|
3634
3638
|
|
|
3635
3639
|
---
|
|
@@ -3660,11 +3664,13 @@ r-2:初始见解。
|
|
|
3660
3664
|
已证引理放 <VIBEMATH>/Formal/Proved/;写之前先 vibe_v5_lean_lib 查重。
|
|
3661
3665
|
· **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义 / 对象 / 条件 / 假设 / 结论是否与
|
|
3662
3666
|
命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。
|
|
3663
|
-
· **本模式要求**:必须产出 Lean
|
|
3664
|
-
kind='blocked' note=…
|
|
3667
|
+
· **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因——用
|
|
3668
|
+
vibe_v5_lean_archive kind='blocked' note=… 记录,或用回执 formal:{decision:'blocked', note:…}。
|
|
3669
|
+
只有 decision='blocked' 的 note 会写成阻塞记录;decision='used' 的 note 只是难度判断,
|
|
3670
|
+
**不会**打开定论门禁。两者都没有时,本次裁定不会生效,
|
|
3665
3671
|
会被记为未定论(原因 formal-required)并进入「形式化待办」。
|
|
3666
3672
|
· 归档可复用定义/引理前先跑通(vibe_v5_lean_archive run=true 或先 vibe_v5_lean_run);跑不通不要入库。
|
|
3667
|
-
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS
|
|
3673
|
+
· 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并把"宿主无 Lean 工具链"写成**阻塞记录**(vibe_v5_lean_archive kind='blocked' note=… 或回执 formal:{decision:'blocked', note:…})——这算显式阻塞原因,定论门禁可以据此放行。
|
|
3668
3674
|
▸ 若你在本轮把它形式化并跑通(vibe_v5_lean_archive kind='proof'),后续轮次的
|
|
3669
3675
|
审查对象就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。
|
|
3670
3676
|
|
|
@@ -3679,7 +3685,7 @@ r-2:初始见解。
|
|
|
3679
3685
|
结束时请**只**输出一个 JSON 对象(```json 围栏内):
|
|
3680
3686
|
{"verdict":{"target":"p-lean-req","verdict":<0-1 数值>,"reason":"<你的理由>"}, "contextPct": 40}
|
|
3681
3687
|
若你本轮做了形式化或给出难度判断,请一并加上:
|
|
3682
|
-
{"formal":{"target":"p-lean-req","decision":"used|blocked|defect","file":"Formal/p-lean-req.lean","note":"
|
|
3688
|
+
{"formal":{"target":"p-lean-req","decision":"used|blocked|defect","file":"Formal/p-lean-req.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}}
|
|
3683
3689
|
```
|
|
3684
3690
|
|
|
3685
3691
|
---
|
|
@@ -3721,7 +3727,7 @@ r-2:初始见解。
|
|
|
3721
3727
|
"task_done": "t-3",
|
|
3722
3728
|
"task_update": {"task_id":"t-3","expected_revision":2,"action":"complete|release|reopen|edit|set_dependencies|delete"},
|
|
3723
3729
|
"input": "本轮会议/辩论的发言正文(会议轮用;也可直接用 say)",
|
|
3724
|
-
"formal": {"target":"p-x","decision":"used|blocked|defect","file":"Formal/p-x.lean","note":"
|
|
3730
|
+
"formal": {"target":"p-x","decision":"used|blocked|defect","file":"Formal/p-x.lean","note":"难度判断(used)/ 阻塞原因(blocked)/ 具体偏差(defect)"}
|
|
3725
3731
|
← Lean 形式化:**鼓励**:按实现难度自行决定;做了就归档,没做就写明难度判断,详见提示词里的【Lean 形式化验证】段
|
|
3726
3732
|
"reject_assign": {"task_id":"t-3","why":"你对这项分派的异议理由"} ← 有异议时填;理由会被广播给
|
|
3727
3733
|
全体表决者(任务仍会执行,但你的理由不会被埋掉),
|
|
@@ -15,13 +15,16 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Run: node tests/audit-installer-compat.test.mjs (part of `node tests/run-tests.mjs`)
|
|
17
17
|
// ============================================================================================
|
|
18
|
-
import { readFileSync } from 'node:fs'
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
18
|
+
import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, cpSync } from 'node:fs'
|
|
19
|
+
import { tmpdir } from 'node:os'
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
21
|
+
import { join, resolve } from 'node:path'
|
|
22
|
+
import { satisfiesDshRange, dshVersionVerdict, PRESETS } from '../installer.js'
|
|
22
23
|
|
|
23
24
|
const HERE = fileURLToPath(new URL('../', import.meta.url))
|
|
24
25
|
const pkg = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'))
|
|
26
|
+
// §4 drives the real apply(); INSTALLER_JS lets the sensitivity probe run it against a reverted copy
|
|
27
|
+
const INSTALLER_SRC = process.env.INSTALLER_JS ? resolve(process.env.INSTALLER_JS) : join(HERE, 'installer.js')
|
|
25
28
|
|
|
26
29
|
let passed = 0, failed = 0
|
|
27
30
|
const failures = []
|
|
@@ -136,6 +139,69 @@ eq(satisfiesDshRange('not-a-version', pkg.engines.dsh), null, 'an unparseable ho
|
|
|
136
139
|
ok(missedByRange.length === 0, 'the declared range can be evaluated for every release in the matrix', missedByRange.join(', '))
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
// ---------------------------------------------------------------------------------------------
|
|
143
|
+
// 4. the version probe must not be able to hold the installer ROW's activation
|
|
144
|
+
//
|
|
145
|
+
// `detectDshVersion` asks `pluginManager.listBundles()` for the runtime version, and it runs at the
|
|
146
|
+
// top of apply(). The result is diagnostic only — the preset mechanism is decided by the loader tree
|
|
147
|
+
// / the service's `register`, never by the version — so a slow or stuck plugin manager must fall
|
|
148
|
+
// through to the next source instead of stalling the boot. (Round B, machinery audit.)
|
|
149
|
+
// ---------------------------------------------------------------------------------------------
|
|
150
|
+
console.log('=== 4. a pluginManager that never answers must not hold the row ===')
|
|
151
|
+
{
|
|
152
|
+
const tmp = mkdtempSync(join(tmpdir(), 'vibe-installer-timeout-'))
|
|
153
|
+
const pkg = join(tmp, 'pkg')
|
|
154
|
+
mkdirSync(pkg, { recursive: true })
|
|
155
|
+
cpSync(INSTALLER_SRC, join(pkg, 'installer.js'))
|
|
156
|
+
writeFileSync(join(pkg, 'package.json'), JSON.stringify({ name: 'dsh-vibe-math', version: '9.9.9', type: 'module' }, null, 2))
|
|
157
|
+
for (const p of PRESETS) {
|
|
158
|
+
mkdirSync(join(pkg, p.src), { recursive: true })
|
|
159
|
+
for (const f of p.files) cpSync(join(HERE, p.src, f), join(pkg, p.src, f))
|
|
160
|
+
}
|
|
161
|
+
const home = join(tmp, 'dshhome')
|
|
162
|
+
mkdirSync(home, { recursive: true })
|
|
163
|
+
process.env.DSH_HOME = home
|
|
164
|
+
delete process.env.DSH_VERSION // leave the pluginManager as the only source
|
|
165
|
+
const presetRoot = join(home, '.agent-presets')
|
|
166
|
+
// import the COPY, so `here` inside installer.js is the throwaway package (manifest + preset dirs)
|
|
167
|
+
const mod = await import(pathToFileURL(join(pkg, 'installer.js')).href + '?t=' + Date.now())
|
|
168
|
+
|
|
169
|
+
const run = (services) => {
|
|
170
|
+
const logs = []
|
|
171
|
+
const ctx = {
|
|
172
|
+
get: (n) => (n === 'pluginManager' ? services : undefined),
|
|
173
|
+
logger: { info: (m) => logs.push('info: ' + m), warn: (m) => logs.push('warn: ' + m), error: (m) => logs.push('error: ' + m) },
|
|
174
|
+
}
|
|
175
|
+
return { ctx, logs }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// a promise that never settles: without a timeout apply() would await it forever
|
|
179
|
+
const HARD_LIMIT_MS = 8000
|
|
180
|
+
const { ctx, logs } = run({ listBundles: () => new Promise(() => {}) })
|
|
181
|
+
let verdict = 'hung'
|
|
182
|
+
const t0 = Date.now()
|
|
183
|
+
let hardTimer
|
|
184
|
+
await Promise.race([
|
|
185
|
+
mod.apply(ctx).then(() => { verdict = 'returned' }, (e) => { verdict = 'threw: ' + String(e && e.message) }),
|
|
186
|
+
new Promise((resolve) => { hardTimer = setTimeout(resolve, HARD_LIMIT_MS) }),
|
|
187
|
+
])
|
|
188
|
+
clearTimeout(hardTimer) // the shipped installer wins the race in ~1.5 s; do not hold the suite open
|
|
189
|
+
const elapsed = Date.now() - t0
|
|
190
|
+
ok(verdict === 'returned', 'apply() returns instead of waiting on a pluginManager that never answers', verdict)
|
|
191
|
+
ok(elapsed < 6000, '...within a short timeout, not at the hard test limit', elapsed + 'ms')
|
|
192
|
+
const copied = PRESETS.every((p) => p.files.every((f) => existsSync(join(presetRoot, p.dst, f))))
|
|
193
|
+
ok(copied, '...and the preset copy still happens (the version probe gates nothing)')
|
|
194
|
+
ok(!logs.some((l) => l.includes('preset install/update failed')), 'the timeout is handled as a fallback, not as an error')
|
|
195
|
+
|
|
196
|
+
// the timeout must NOT replace a working probe: a prompt service still supplies the version
|
|
197
|
+
const quick = run({ listBundles: async () => [{ name: '@deepseek-ai/dsh-base', version: '0.2.0-rc.2' }] })
|
|
198
|
+
await mod.apply(quick.ctx)
|
|
199
|
+
ok(quick.logs.some((l) => l.includes('0.2.0-rc.2')),
|
|
200
|
+
'a prompt listBundles() still supplies the version (the timeout is a fallback, not a replacement)',
|
|
201
|
+
quick.logs.filter((l) => l.includes('source')).join(' | ').slice(0, 160))
|
|
202
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
203
|
+
}
|
|
204
|
+
|
|
139
205
|
console.log('')
|
|
140
206
|
console.log('=== INSTALLER COMPAT: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
141
207
|
if (failed) { for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// (_oneoff/probe-installer-policy.mjs) uses this to prove these assertions
|
|
24
24
|
// really do detect the previous, edit-preserving policy.
|
|
25
25
|
// ============================================================================================
|
|
26
|
-
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, existsSync, rmSync, cpSync, readdirSync } from 'node:fs'
|
|
26
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, existsSync, rmSync, cpSync, readdirSync, statSync, symlinkSync } from 'node:fs'
|
|
27
27
|
import { createHash } from 'node:crypto'
|
|
28
28
|
import { tmpdir } from 'node:os'
|
|
29
29
|
import { join, dirname, resolve } from 'node:path'
|
|
@@ -43,10 +43,15 @@ function ok(cond, label, detail) {
|
|
|
43
43
|
return false
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* A throwaway copy of the package at a chosen version; every managed file is version-marked.
|
|
48
|
+
* `transform` (optional) rewrites the copied installer source — used by §14 to build the package
|
|
49
|
+
* that DROPS a preset, which no shipped package can express.
|
|
50
|
+
*/
|
|
51
|
+
function buildPackage(version, dir, transform) {
|
|
48
52
|
mkdirSync(dir, { recursive: true })
|
|
49
|
-
|
|
53
|
+
if (typeof transform === 'function') writeFileSync(join(dir, 'installer.js'), transform(readFileSync(INSTALLER_SRC, 'utf8')))
|
|
54
|
+
else cpSync(INSTALLER_SRC, join(dir, 'installer.js'))
|
|
50
55
|
writeFileSync(join(dir, 'package.json'), JSON.stringify({
|
|
51
56
|
name: 'dsh-vibe-math', version, type: 'module', dsh: { bundle: { patch: './cordis.patch.yml' } },
|
|
52
57
|
}, null, 2))
|
|
@@ -253,6 +258,226 @@ console.log('=== 9. a recorded version NEWER than this package (a downgrade) say
|
|
|
253
258
|
ok(!logs.some((l) => l.includes('preset install/update failed')), 'apply() swallowed no failure')
|
|
254
259
|
}
|
|
255
260
|
|
|
261
|
+
// ============================================================================================
|
|
262
|
+
// The Round-B machinery findings. Each of these sections was RED before its fix; see
|
|
263
|
+
// _oneoff/probe-installer-policy.mjs and _oneoff/rC-probe-mechanism-sensitivity.mjs for the
|
|
264
|
+
// sensitivity runs (the shipped suite must be green while the previous logic is red ON THE NAMED
|
|
265
|
+
// ASSERTION).
|
|
266
|
+
// ============================================================================================
|
|
267
|
+
|
|
268
|
+
// fresh home + helpers for the sections below, so the edits accumulated above cannot leak in
|
|
269
|
+
// (the readers never throw: a mutant that deletes or half-writes a file must show up as a FAIL on
|
|
270
|
+
// the assertion that names it, not as a crashed suite)
|
|
271
|
+
const readText = (p) => { try { return readFileSync(p, 'utf8') } catch (e) { return null } }
|
|
272
|
+
const readJson = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')) } catch (e) { return null } }
|
|
273
|
+
const tmpHome = (n) => {
|
|
274
|
+
const home = join(tmp, 'dshhome' + n)
|
|
275
|
+
mkdirSync(home, { recursive: true })
|
|
276
|
+
const root = join(home, '.agent-presets')
|
|
277
|
+
return {
|
|
278
|
+
home, root,
|
|
279
|
+
state: join(root, '.vibe-math-installed.json'),
|
|
280
|
+
backup: join(root, '.vibe-math-backup'),
|
|
281
|
+
at: (p, f) => join(root, p.dst, f),
|
|
282
|
+
isCopyOf: (p, f, from) => {
|
|
283
|
+
try { return readFileSync(join(root, p.dst, f)).equals(readFileSync(join(from, p.src, f))) } catch (e) { return false }
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log('=== 10. preset writes are ATOMIC: a failed write cannot touch the destination ===')
|
|
289
|
+
{
|
|
290
|
+
const H = tmpHome('10')
|
|
291
|
+
await applyFrom(pkgA, H.home, [])
|
|
292
|
+
const target = { p: PRESETS[1], f: 'preset.yml' }
|
|
293
|
+
const userBytes = Buffer.from('# USER EDIT (must survive a failed write)\n')
|
|
294
|
+
writeFileSync(H.at(target.p, target.f), userBytes)
|
|
295
|
+
// `writePresetFile` writes `<dest>.<pid>.<seq>.vibe-math-tmp` and renames it over the destination,
|
|
296
|
+
// so a DIRECTORY squatting on that temp path makes the temp write fail while the destination stays
|
|
297
|
+
// untouched. An in-place `writeFileSync(dest)` would not care: it would overwrite the user's bytes
|
|
298
|
+
// and log nothing. The squat covers every sequence number a full run can use.
|
|
299
|
+
const MANY = 32
|
|
300
|
+
const squat = (n) => H.at(target.p, target.f) + '.' + process.pid + '.' + n + '.vibe-math-tmp'
|
|
301
|
+
for (let n = 0; n < MANY; n++) mkdirSync(squat(n), { recursive: true })
|
|
302
|
+
const logs = await applyFrom(pkgB, H.home, [])
|
|
303
|
+
ok(readText(H.at(target.p, target.f)) === userBytes.toString(),
|
|
304
|
+
'a failed preset write leaves the destination byte-for-byte intact (tmp+rename, never in place)')
|
|
305
|
+
ok(logs.some((l) => l.includes('preset install/update failed')),
|
|
306
|
+
'...and the failure is reported instead of being swallowed', logs.filter((l) => l.includes('failed')).join(' | ').slice(0, 200))
|
|
307
|
+
ok((readJson(H.state) || {}).version === '1.0.0',
|
|
308
|
+
'the state still describes the bytes that are actually on disk, so the next boot self-heals')
|
|
309
|
+
for (let n = 0; n < MANY; n++) rmSync(squat(n), { recursive: true, force: true })
|
|
310
|
+
await applyFrom(pkgB, H.home, [])
|
|
311
|
+
ok(H.isCopyOf(target.p, target.f, pkgB), 'with the obstruction gone the next boot replaces the file normally')
|
|
312
|
+
ok((readJson(H.state) || {}).version === '2.0.0', '...and records the new version')
|
|
313
|
+
const leftovers = []
|
|
314
|
+
const walk = (dir) => {
|
|
315
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
316
|
+
const p = join(dir, e.name)
|
|
317
|
+
if (e.isDirectory()) walk(p)
|
|
318
|
+
else if (e.name.endsWith('.vibe-math-tmp')) leftovers.push(p)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
walk(H.root)
|
|
322
|
+
ok(leftovers.length === 0, 'no `.vibe-math-tmp` file is left behind by a clean run', leftovers.join(', '))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.log('=== 11. the state file is not rewritten (or even touched) when nothing changed ===')
|
|
326
|
+
{
|
|
327
|
+
const H = tmpHome('11')
|
|
328
|
+
await applyFrom(pkgA, H.home, [])
|
|
329
|
+
const before = readText(H.state)
|
|
330
|
+
const mtime = statSync(H.state).mtimeMs
|
|
331
|
+
await new Promise((r) => setTimeout(r, 30))
|
|
332
|
+
await applyFrom(pkgA, H.home, [])
|
|
333
|
+
ok(readText(H.state) === before, 'a same-version boot leaves the state file byte-identical (no churn)')
|
|
334
|
+
ok(statSync(H.state).mtimeMs === mtime, '...and does not move its mtime either')
|
|
335
|
+
// ...but a same-version boot that LEARNS something (a file was edited) must record it
|
|
336
|
+
appendFileSync(H.at(PRESETS[0], 'preset.yml'), '\n# USER EDIT\n')
|
|
337
|
+
await applyFrom(pkgA, H.home, [])
|
|
338
|
+
const after = readJson(H.state) || {}
|
|
339
|
+
ok(readText(H.state) !== before, 'a same-version boot that learns something DOES rewrite the state')
|
|
340
|
+
ok(after.files && after.files[PRESETS[0].src + '/preset.yml'] && after.files[PRESETS[0].src + '/preset.yml'].provenance === 'user',
|
|
341
|
+
'the drift is recorded as user-owned — by the real code path (the audit found this value was never written)')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log('=== 12. a legacy hash-less record is reported as UNKNOWN, never as a proven edit ===')
|
|
345
|
+
{
|
|
346
|
+
const H = tmpHome('12')
|
|
347
|
+
await applyFrom(pkgA, H.home, [])
|
|
348
|
+
const state = readJson(H.state) || { files: {} }
|
|
349
|
+
delete state.files[PRESETS[2].src + '/preset.yml'].hash // the shape a pre-hash installer wrote
|
|
350
|
+
writeFileSync(H.state, JSON.stringify(state, null, 2) + '\n')
|
|
351
|
+
appendFileSync(H.at(PRESETS[2], 'preset.yml'), '\n# edit while the recorded hash is unknown\n')
|
|
352
|
+
const editBytes = readText(H.at(PRESETS[2], 'preset.yml'))
|
|
353
|
+
const logs = await applyFrom(pkgB, H.home, [])
|
|
354
|
+
ok(logs.some((l) => l.includes('没有哈希')),
|
|
355
|
+
'the file whose previous hash is unknown is reported as "cannot tell whether you changed it"')
|
|
356
|
+
ok(!logs.some((l) => l.includes('被改过')),
|
|
357
|
+
'...and is NOT called an edit the installer can prove', logs.filter((l) => l.includes('被改过')).join(' | '))
|
|
358
|
+
const backup = join(H.backup, '1.0.0', PRESETS[2].dst, 'preset.yml')
|
|
359
|
+
ok(existsSync(backup) && readText(backup) === editBytes,
|
|
360
|
+
'...while its bytes are still copied to the backup root before the replacement')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
console.log('=== 13. deleting a file with a dropped preset backs it up FIRST and names it ===')
|
|
364
|
+
{
|
|
365
|
+
const H = tmpHome('13')
|
|
366
|
+
await applyFrom(pkgA, H.home, [])
|
|
367
|
+
mkdirSync(join(H.root, 'vibe-math-v1'), { recursive: true })
|
|
368
|
+
writeFileSync(join(H.root, 'vibe-math-v1', 'legacy.js'), 'MY VALUABLE EDIT\n')
|
|
369
|
+
writeFileSync(join(H.root, 'vibe-math-v1', 'stock.yml'), 'package bytes\n')
|
|
370
|
+
const state = readJson(H.state) || { files: {} }
|
|
371
|
+
state.files['vibe-math-v1/legacy.js'] = { hash: sha('the bytes the installer wrote long ago\n'), provenance: 'package' }
|
|
372
|
+
state.files['vibe-math-v1/stock.yml'] = { hash: sha('package bytes\n'), provenance: 'package' }
|
|
373
|
+
writeFileSync(H.state, JSON.stringify(state, null, 2) + '\n')
|
|
374
|
+
const logs = await applyFrom(pkgA, H.home, [])
|
|
375
|
+
ok(!existsSync(join(H.root, 'vibe-math-v1', 'legacy.js')), 'the changed file of a dropped preset is removed (the preset is gone)')
|
|
376
|
+
const backup = join(H.backup, '1.0.0', 'vibe-math-v1', 'legacy.js')
|
|
377
|
+
ok(existsSync(backup) && readText(backup) === 'MY VALUABLE EDIT\n',
|
|
378
|
+
'...AFTER its bytes were copied to .vibe-math-backup/<fromVersion>/<preset>/<file>')
|
|
379
|
+
ok(logs.some((l) => l.includes('vibe-math-v1/legacy.js') && l.includes('.vibe-math-backup')),
|
|
380
|
+
'...and the log names both the file it deleted and where the copy is')
|
|
381
|
+
ok(!existsSync(join(H.backup, '1.0.0', 'vibe-math-v1', 'stock.yml')),
|
|
382
|
+
'a file that still matches its recorded hash is deleted WITHOUT a backup (it was the package copy)')
|
|
383
|
+
ok(logs.some((l) => l.includes('仍是本安装器写入的字节')), '...and that is stated in the log')
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
console.log('=== 14. a user edit inside a preset a NEWER package drops is KEPT ===')
|
|
387
|
+
{
|
|
388
|
+
const H = tmpHome('14')
|
|
389
|
+
const edited = { p: PRESETS[1], f: 'agent.cordis.yml' }
|
|
390
|
+
await applyFrom(pkgA, H.home, [])
|
|
391
|
+
appendFileSync(H.at(edited.p, edited.f), '\n# MY EDIT\n')
|
|
392
|
+
await applyFrom(pkgA, H.home, [])
|
|
393
|
+
const recorded = (readJson(H.state) || {}).files || {}
|
|
394
|
+
ok(recorded[edited.p.src + '/' + edited.f] && recorded[edited.p.src + '/' + edited.f].provenance === 'user',
|
|
395
|
+
'the same-version edit is recorded as user-owned before the preset is dropped')
|
|
396
|
+
// "a newer package no longer ships this preset": rename its src in the copied installer, so the copy
|
|
397
|
+
// loop cannot rebuild it and the old keys become stale — exactly what the cleanup must handle.
|
|
398
|
+
const dropper = buildPackage('2.0.0', join(tmp, 'pkg-dropper'),
|
|
399
|
+
(src) => src.replace("src: 'vibe-math-v3',", "src: 'vibe-math-v3-dropped',"))
|
|
400
|
+
const logs = await applyFrom(dropper, H.home, [])
|
|
401
|
+
ok(existsSync(H.at(edited.p, edited.f)), 'the edited file of the dropped preset SURVIVES')
|
|
402
|
+
ok((readText(H.at(edited.p, edited.f)) || '').includes('MY EDIT'), '...with the user\'s bytes intact')
|
|
403
|
+
ok(!existsSync(H.at(edited.p, 'preset.yml')), 'the unedited files of the dropped preset are still removed')
|
|
404
|
+
ok(logs.some((l) => l.includes('记为用户所有的文件被保留')), '...and the log says the user-owned file was kept')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
console.log('=== 15. a failed state write is reported (not swallowed), and does not abort the run ===')
|
|
408
|
+
{
|
|
409
|
+
const H = tmpHome('15')
|
|
410
|
+
await applyFrom(pkgA, H.home, [])
|
|
411
|
+
appendFileSync(H.at(PRESETS[0], 'preset.yml'), '\n# USER EDIT (the state must be updated)\n')
|
|
412
|
+
// the first state write of a fresh module instance uses sequence 0: squat that exact temp path
|
|
413
|
+
mkdirSync(H.state + '.' + process.pid + '.0.tmp', { recursive: true })
|
|
414
|
+
const logs = await applyFrom(pkgA, H.home, [])
|
|
415
|
+
ok(logs.some((l) => l.includes('状态文件写入失败')),
|
|
416
|
+
'a failed state write is REPORTED (it used to fail silently)', logs.filter((l) => l.includes('写入失败')).join(' | ').slice(0, 160))
|
|
417
|
+
ok(!logs.some((l) => l.includes('preset install/update failed')), '...without aborting the preset step')
|
|
418
|
+
ok((readJson(H.state) || {}).version === '1.0.0',
|
|
419
|
+
'the previous state file is still parseable (tmp+rename left no half-written file)')
|
|
420
|
+
rmSync(H.state + '.' + process.pid + '.0.tmp', { recursive: true, force: true })
|
|
421
|
+
await applyFrom(pkgA, H.home, [])
|
|
422
|
+
const healed = ((readJson(H.state) || {}).files || {})[PRESETS[0].src + '/preset.yml'] || {}
|
|
423
|
+
ok(healed.provenance === 'user',
|
|
424
|
+
'the next boot records what the failed write lost (self-healing)')
|
|
425
|
+
ok(!existsSync(H.state + '.' + process.pid + '.0.tmp'), 'the temp path is free again after the successful write')
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.log('=== 16. an UNREADABLE stale file is not deleted (there is no backup it could make) ===')
|
|
429
|
+
{
|
|
430
|
+
const H = tmpHome('16')
|
|
431
|
+
await applyFrom(pkgA, H.home, [])
|
|
432
|
+
const staleDir = join(H.root, 'vibe-math-v1')
|
|
433
|
+
mkdirSync(staleDir, { recursive: true })
|
|
434
|
+
// A directory junction sitting at the stale FILE path: existsSync() is true, readFileSync()
|
|
435
|
+
// throws (EISDIR), and unlinkSync() WOULD succeed — exactly the shape the policy must refuse.
|
|
436
|
+
// (POSIX ignores the 'junction' type and makes a plain symlink to the directory, which behaves
|
|
437
|
+
// identically here.) The previous code unlinked it with no backup; the rule is now
|
|
438
|
+
// "back up before deleting, or do not delete".
|
|
439
|
+
const target = join(tmp, 'unreadable-target-16')
|
|
440
|
+
mkdirSync(target, { recursive: true })
|
|
441
|
+
symlinkSync(target, join(staleDir, 'legacy.js'), 'junction')
|
|
442
|
+
const state = readJson(H.state) || { files: {} }
|
|
443
|
+
state.files['vibe-math-v1/legacy.js'] = { hash: sha('bytes this installer wrote long ago\n'), provenance: 'package' }
|
|
444
|
+
writeFileSync(H.state, JSON.stringify(state, null, 2) + '\n')
|
|
445
|
+
const logs = await applyFrom(pkgA, H.home, [])
|
|
446
|
+
ok(existsSync(join(staleDir, 'legacy.js')),
|
|
447
|
+
'a stale file whose bytes cannot be read is KEPT (the delete is skipped, not performed unbacked)')
|
|
448
|
+
ok(logs.some((l) => l.includes('无法读取')), '...and the log names it as unreadable',
|
|
449
|
+
logs.filter((l) => l.includes('无法读取')).join(' | ').slice(0, 200))
|
|
450
|
+
ok(!logs.some((l) => l.includes('被改过')),
|
|
451
|
+
'...and it is NOT called a proven edit the installer can see', logs.filter((l) => l.includes('被改过')).join(' | '))
|
|
452
|
+
ok(!logs.some((l) => l.includes('legacy.js') && l.includes('.vibe-math-backup')),
|
|
453
|
+
'...and no backup is claimed for it (there were no bytes to back up)')
|
|
454
|
+
ok(existsSync(staleDir), 'the stale preset directory is kept too, while the file inside it is kept')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
console.log('=== 16b. a stale file whose BACKUP FAILS is kept, not deleted unbacked ===')
|
|
458
|
+
{
|
|
459
|
+
const H = tmpHome('16b')
|
|
460
|
+
await applyFrom(pkgA, H.home, [])
|
|
461
|
+
const staleDir = join(H.root, 'vibe-math-v1')
|
|
462
|
+
mkdirSync(staleDir, { recursive: true })
|
|
463
|
+
writeFileSync(join(staleDir, 'legacy.js'), "the user's own bytes\n")
|
|
464
|
+
// Every backup attempt must fail: a FILE sits where the backup ROOT directory would be, so the
|
|
465
|
+
// recursive mkdir throws (ENOTDIR/EEXIST). This is the rule's hardest case — the bytes exist,
|
|
466
|
+
// they differ from the package, and preserving them is impossible — so the delete must be
|
|
467
|
+
// skipped. (Round C's fix only covered the UNREADABLE case; a failed backup still deleted.)
|
|
468
|
+
writeFileSync(join(H.root, '.vibe-math-backup'), 'not a directory\n')
|
|
469
|
+
const state = readJson(H.state) || { files: {} }
|
|
470
|
+
state.files['vibe-math-v1/legacy.js'] = { hash: sha('bytes this installer wrote long ago\n'), provenance: 'package' }
|
|
471
|
+
writeFileSync(H.state, JSON.stringify(state, null, 2) + '\n')
|
|
472
|
+
const logs = await applyFrom(pkgA, H.home, [])
|
|
473
|
+
ok(existsSync(join(staleDir, 'legacy.js')) && readFileSync(join(staleDir, 'legacy.js'), 'utf8') === "the user's own bytes\n",
|
|
474
|
+
'a stale file whose BACKUP fails is KEPT (the delete is skipped, not performed unbacked)')
|
|
475
|
+
ok(logs.some((l) => l.includes('备份失败') && l.includes('跳过删除')),
|
|
476
|
+
"...and the log reports the failed backup as a skipped delete (not as 'the original was lost')",
|
|
477
|
+
logs.filter((l) => l.includes('备份失败')).join(' | ').slice(0, 220))
|
|
478
|
+
rmSync(join(H.root, '.vibe-math-backup'), { force: true })
|
|
479
|
+
}
|
|
480
|
+
|
|
256
481
|
rmSync(tmp, { recursive: true, force: true })
|
|
257
482
|
|
|
258
483
|
console.log('')
|