dsh-vibe-math 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AUDIT-CHECKLIST.md +78 -3
  2. package/README.md +33 -2
  3. package/RELEASE-NOTES-2.3.1.md +134 -0
  4. package/RELEASE-NOTES-2.3.2.md +145 -0
  5. package/audit-formal-sensitivity.mjs +134 -39
  6. package/audit-prompt-invariants.mjs +414 -0
  7. package/audit-spec-traceability.mjs +173 -0
  8. package/audit-v5-integrity.mjs +5 -3
  9. package/docs/formal-verification.md +122 -19
  10. package/docs/generate_framework_diagram_v5.mjs +2 -1
  11. package/docs/test-timing.md +101 -0
  12. package/formal-verify-v2.test.mjs +526 -7
  13. package/formal-verify-v3.test.mjs +389 -10
  14. package/formal-verify-v4.test.mjs +462 -4
  15. package/formal-verify-v5.test.mjs +163 -4
  16. package/installer.js +3 -1
  17. package/package.json +12 -2
  18. package/prompt-corpus-persona/persona-corpus.json +2 -2
  19. package/prompt-corpus-persona/persona-corpus.md +6 -2
  20. package/prompt-corpus-v2/formal-verify-v2.json +484 -0
  21. package/prompt-corpus-v2/formal-verify-v2.md +5239 -0
  22. package/prompt-corpus-v3/formal-verify-v3.json +274 -100
  23. package/prompt-corpus-v3/formal-verify-v3.md +2057 -335
  24. package/prompt-corpus-v4/formal-verify-v4.json +89 -0
  25. package/prompt-corpus-v4/formal-verify-v4.md +283 -0
  26. package/prompt-corpus-v5/prompt-corpus-v5.json +186 -219
  27. package/prompt-corpus-v5/prompt-corpus-v5.md +485 -700
  28. package/prompt-v5-integrity.test.mjs +1272 -1085
  29. package/run-tests.mjs +118 -0
  30. package/vibe-math-v2/vibe-math-v2.js +341 -45
  31. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +129 -8
  32. package/vibe-math-v3/vibe-math-v3.js +162 -36
  33. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +21 -3
  34. package/vibe-math-v4/vibe-math-v4.js +201 -30
  35. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +54 -2
  36. package/vibe-math-v5/agent.cordis.yml +6 -2
  37. package/vibe-math-v5/vibe-math-v5.js +133 -28
  38. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +55 -5
  39. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +16 -2
  40. package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +6 -5
@@ -263,12 +263,18 @@ v2 **没有会话投影**,所以记录与待办一起持久化在 v2 自己的
263
263
  门禁与提示词取记录时两个方向都认(`formalGateRecord`),避免"代理确实形式化了,门禁却仍
264
264
  按未尝试处理"的假阴性。
265
265
 
266
+ > **必须注意**:`syncVerificationTarget` 只更新**已经存在**的别名键,所以"归档写了对象 id、
267
+ > rId 侧还没有记录"是首次形式化后的**真实状态**。因此**门禁的判定**也必须走
268
+ > `formalGateRecord`(`deferForFormal`),不能只看 `formalOf(rId)`:否则一个已 `passed`
269
+ > 的对象会被判为未形式化,而且这次搁置本身会写下 `r-<id> = status:'none'`,此后每一轮都
270
+ > 继续搁置并重开一次完整辩论——对象**永远无法定论**(真实缺陷,已由套件第 13 节守住)。
271
+
266
272
  ### 五、三个工具
267
273
 
268
274
  | 工具 | 作用 |
269
275
  |---|---|
270
- | `vibe_math_lean_run` | 在 `<VibeMath 根>` 内的一个 `.lean` 文件上执行工具链并原样回报:`{ok, exitCode, signal, ms, command, stdout, stderr, file}`;`target=<对象 id>` 时同时记录该对象的运行结果。**绝不向调度循环抛异常**:工具链缺失 → `LEAN_NOT_FOUND`;宿主无 `subprocess` 服务 → `NO_SUBPROCESS`;spawn 失败 → `LEAN_SPAWN_FAILED`;超时 → `LEAN_TIMEOUT` |
271
- | `vibe_math_lean_archive` | `kind='def'` → 全局 `Formal/Lib/<name>.lean`;`kind='lemma'` → 全局 `Formal/Proved/<name>.lean`(两者都会重建对应 `Index.md`;`run:false` 可跳过执行)。`kind='proof'` → `Formal/<target>.lean`,且**该文件最近一次运行通过**时再写 `Verified/Lean/<target>.lean` 并把对象标为 `passed`,否则标为 `attempted`。`kind='blocked'` → 记录显式难度判断/阻塞原因(`note` 空 → 拒绝)并把对象标为 `blocked` |
276
+ | `vibe_math_lean_run` | 在 `<VibeMath 根>` 内的一个 `.lean` 文件上执行工具链并原样回报:`{ok, exitCode, signal, ms, command, stdout, stderr, file, timedOut}`;`target=<对象 id>` 时同时记录该对象的运行结果。**绝不向调度循环抛异常**:工具链缺失 → `LEAN_NOT_FOUND`;宿主无 `subprocess` 服务 → `NO_SUBPROCESS`;spawn 失败 → `LEAN_SPAWN_FAILED`;超时 → `LEAN_TIMEOUT`。**超时是主动的**(契约 §7):`handle.done` 与一个 `cap` 毫秒的计时器竞速,计时器到点时尽力调用 `handle.terminate()`,再给出可读的超时结果;`done` 先到就清掉计时器(不留悬挂 timer)。 |
277
+ | `vibe_math_lean_archive` | `kind='def'` → 全局 `Formal/Lib/<name>.lean`;`kind='lemma'` → 全局 `Formal/Proved/<name>.lean`(两者都会重建对应 `Index.md`;`run:false` 可跳过执行)。`kind='proof'` → `Formal/<target>.lean`,且**该文件最近一次运行通过**时再写 `Verified/Lean/<target>.lean` 并把对象标为 `passed`,否则标为 `attempted`。`kind='blocked'` → 记录显式难度判断/阻塞原因(`note` 空 → 拒绝)并把对象标为 `blocked`。`kind='def'/'lemma'` 的返回值在**运行未通过**时不得声称"可直接 import 复用"(工具返回值同样是代理读到的文字)。 |
272
278
  | `vibe_math_lean_lib` | 扫描并重建三处索引(项目 `Formal/Index.md`、全局 `Lib/Index.md`、全局 `Proved/Index.md`),返回可复用库清单,供代理写新定义前查重;`refresh:false` 时只读不重建 |
273
279
 
274
280
  参数 schema:
@@ -298,12 +304,18 @@ v2 **没有会话投影**,所以记录与待办一起持久化在 v2 自己的
298
304
  |---|---|
299
305
  | `verifierReviewPrompt(r)` | `formalPromptBlock(r.rId)`(非 off 档)+ 判定指引 |
300
306
  | `verifierDebatePrompt(r, transcript)` | 同上——**辩论轮也必须带**,否则辩论阶段的代理会丢掉"审查对象已变成忠实性"这一变化 |
301
- | `solverPrompt` / `explorerPrompt` / `rederivePrompt` | `formalWorkLine()`("顺手形式化",`off` 档为空串) |
307
+ | `solverPrompt` / `explorerPrompt` / `rederivePrompt` | `formalWorkLine()`("顺手形式化",`off` 档为空串)。可复用库的路径写 **`<VibeMath 根>/Formal/Proved/`**(`Formal/Proved/` 在项目根下并不存在,只写相对路径会让代理去项目里找一个永远找不到的目录) |
302
308
  | `vibe_math_status` / `vibe_math_report` / 卡片 / 索引 | 可读参数表、形式化记录概览、`- 形式化: <状态>` |
303
309
 
304
310
  对象已 `passed` 时,验证提示词改为强调**忠实性审查**("你不需要重新检查推导");
305
311
  已 `blocked` 时改为要求复核"这个阻塞判断是否成立"。
306
312
 
313
+ **忠实性分支里的"撤回"措辞必须与档位一致**(契约 §4.1 第 3 条 / §6.1):
314
+ `require` 档才有门禁,所以那里写"本次裁定**不定论**";
315
+ `encourage` 档**没有**门禁,撤回证明并不等于搁置裁定,那里必须写明
316
+ "**本档没有门禁**:框架不会强制搁置本次裁定——请务必给出弃权值,靠它阻止本轮得出布尔一致结论"。
317
+ 承诺一个框架无法强制的"不定论",会让表决者以为不必自己弃权(审计清单 §1.7)。
318
+
307
319
  ### 八、门禁的实现位置
308
320
 
309
321
  - **`writeVerifiedCardIfNeeded(p)`**(命题卡片)与 **`writeVerifiedProblemCardIfNeeded(q)`**
@@ -316,13 +328,122 @@ v2 **没有会话投影**,所以记录与待办一起持久化在 v2 自己的
316
328
  - **`processStatusUpdates`**——概率=1 的**自动收口**路径。先申请卡片写入许可
317
329
  (`writeVerifiedCardIfNeeded(p, target)` / `writeVerifiedProblemCardIfNeeded(q, true)`),
318
330
  拿到许可才写入 `布尔估计` / `已解决` / `优先级`;被搁置则整条跳过。
319
-
320
- ### 九、测试
331
+ - **门禁的判定记录一律取合并后的 `formalGateRecord`**(两套 id 都认,见 §四):
332
+ `deferForFormal` 内部即如此。只看 `formalOf(rId)` 会让"代理用对象 id 归档 passed、rId 侧
333
+ 还没有记录"这种首次形式化的真实状态被判成未形式化。
334
+ - **`settleVerdict` 的「判断命题」转移路径**(`'problem-solution'` 且 `q.判断命题 && !sol.来源列表`):
335
+ 它把**源命题**的 `布尔估计` 直接写成 0/1、置 `优先级 = 'never'` 并标记 `已验证`
336
+ (此后不再入选验证),这是一次对源命题的布尔裁定;因此对 `v === 1 || v === 0` 必须先
337
+ `deferForFormal(ap.id, …)`,被搁置时源命题的概率/优先级/已验证字段**分毫不动**。
338
+ (v=1 会被上面那道门禁拦下,v=0 不会——只拦卡片而忽略返回值,会让一个未形式化的命题
339
+ 被侧面判为"假"并永久退出调度。真实缺陷,已由套件第 15 节守住。)
340
+
341
+ ### 九、回执通道(`formal` 字段)与 `defect` 语义
342
+
343
+ > 对应契约 §4、§4.1、§6.3 与 §10 的第 8–11 条。**教训**:本特性首版只在提示词里写了
344
+ > "请在回执的 formal 字段写明难度判断",框架**从不解析**它——难度判断静默消失,而套件只断言
345
+ > "那句话存在",于是 177 条断言全绿却守着一条**死通道**。措辞断言不等于行为断言。
346
+
347
+ #### 9.1 回执真的会被解析并落库
348
+
349
+ - 回执契约里出现 `formal` 字段(`formalJsonField` 注入 `verifierReviewPrompt` 与
350
+ `verifierDebatePrompt` 的 JSON 示例;`formalReplyNote()` 注入 solver/explorer 工作轮的
351
+ "顺手形式化"段落):`{"formal":{"target":"p-x","decision":"used|blocked|defect","file":"Formal/p-x.lean","note":"难度判断/阻塞原因/具体偏差"}}`。
352
+ - `absorbFormalFromReply(parsed, memberId)` 接受**顶层 `formal`** 与 **`meta.formal`** 两种位置,
353
+ 在三条解析回执的路径上都被调用:`handleVerifier`(验证者)、`handleSolver`、`handleExplorer`
354
+ (工作轮)。验证者路径里的调用**先于**裁定:`defect` 必须先把记录降级,紧随其后的
355
+ `settleVerdict` 才会在 `require` 档把本次裁定正确地记为未定论。
356
+ - `decision` 的落库:`blocked` → 记录 `blocked` + `note`;`defect` → 见 9.2;`used` → `attempted` + `file`。
357
+ - **拒绝规则**(返回 `V2_INVALID_ARGUMENT`,并在活动日志里公告,**不留下任何记录**):
358
+ `blocked`/`defect` 的 `note` 为空;缺 `target`(`safeId('')` 会返回 `anon`,绝不允许凭空造记录);
359
+ `decision` 不在三值之内。回执通道**绝不抛异常**进调度循环:一次记账失败不该吞掉一次表决。
360
+
361
+ #### 9.2 `defect`:忠实性缺陷**不是**"命题为假"
362
+
363
+ `passed` 只保证"这段 Lean 代码通过了内核检查",不保证它说的就是命题想说的。表决者逐条核对后
364
+ 发现形式化写窄了/写宽了/换了对象/漏了条件时,那是**形式化不合格**,不是命题被证伪:
365
+
366
+ - 若让表决者"发现偏差 → 投 0",框架记下的是"**该命题为假**"——用求真更严格的机制,
367
+ 反而伪造出一个错误的否定结论。
368
+ - 若只记成 `blocked`,门禁会**放行**(`blocked` 本就允许定论),等于带着一个坏形式化去定论。
369
+
370
+ 因此 `defect` 是独立的一档,**语义固定**为:
371
+
372
+ 1. **表决者**:不得投 `1` 或 `0`;给一个严格介于 0 与 1 之间的值(记为弃权),在 `Reason` 里写清偏差,
373
+ 并用回执 `formal:{decision:'defect', note:'<具体偏差>'}` 记录(`note` 必填)。
374
+ 2. **框架**(`formalDefectDowngrade`):把形式化记录**降级为 `attempted`**(无论此前是 `passed`
375
+ 还是 `blocked`)、清空 `proof`、**撤回**归档证明(工作文件 `Formal/<id>.lean` 保留,代码不丢)、
376
+ `note` 记入记录与 `Formal/TODO.md`、在活动日志里公告。
377
+
378
+ 「撤回」不是"尽力删除"(`withdrawArchivedProof`):
379
+ 1. 先尽力删除(有 `subprocess` 时走 shell),并用 fs **复核真的没了**;
380
+ 2. 删不掉(宿主无 `subprocess`、权限、`rm -f` 静默失败)时,把撤回通知**覆写进那个路径**
381
+ (`-- 已撤回(<时间>):该形式化被认定与命题原文不一致。` + 指向保留的工作文件),
382
+ 使一份已撤回的证明**不可能**再被读成该对象的证明;
383
+ 3. 活动日志如实说明发生了哪一种(删除 / 覆写成撤回通知 / ⚠ 两者都失败)。
384
+ 候选路径 = **同一对象的全部 id 别名**(对象 id、`r-<id>`、`r-<id>-s0|pf<n>|rf<n>`)在记录里
385
+ 引用的 `proof`,加上各 id 直接对应的 `Verified/Lean/<id>.lean`;只处理确实存在(或被记录引用)
386
+ 的路径——宿主不能删除时"覆写"会把不存在的文件名**凭空造出来**。
387
+ 3. **`require` 档**:降级后 `formalGateOk` 为假 ⇒ 本次裁定**不定论**(`formal-required`),
388
+ 对象进入「形式化待办」。修正形式化并重新跑通后再投票。
389
+ 4. **唯一可以投 0 的情形**:表决者**独立于这份 Lean 代码**也能确定命题为假(`Reason` 必须写清独立理由)。
390
+ 提示词里因此明确写着"发现任何偏差,不要投 0",并且**不再出现**"偏离 → 0"这类指令。
391
+
392
+ #### 9.3 两套 id 必须同时同步
393
+
394
+ 验证记录以 **rId**(`r-pGate`、`r-q1-s0`)为键,而归档/索引/卡片读的可能是 **对象 id**(`pGate`)。
395
+ 回执通道(`blocked` / `defect` / `used`)通过 `putFormalBothIds` 一次写全两侧:回执点名的那个 id、
396
+ 它对应的另一侧、以及同一对象的其它验证别名(`r-<id>`、`r-<id>-s0`、`r-<id>-pf1`、`r-<id>-rf2`,
397
+ 复用 `syncVerificationTarget` 的既有扫描)。id 解析只有一处 `formalObjectIdOf`(门禁合并查询
398
+ `formalGateRecord` 也用它),**不新造第三套映射**。只写一侧的后果是静默错位:`defect` 只写到验证 id 上,
399
+ 对象记录仍是 `passed`,`formalGateRecord` 从对象侧读回 `passed`、卡片照旧写"Lean 通过"。
400
+
401
+ #### 9.4 提示词的三条硬要求
402
+
403
+ - **工具名一律写全称**:注入文本里只出现注册名 `vibe_math_lean_run` / `vibe_math_lean_archive` /
404
+ `vibe_math_lean_lib`;**不得**出现 `lean_run` / `lean_archive` / `lean_lib` 这类缩写——
405
+ 那不是注册名,代理照抄会调用一个不存在的工具(工具自己返回的 `hint` 同样算注入文本)。
406
+ - **归档可复用定义/引理前必须先跑通**:`vibe_math_lean_archive run=true` 或先 `vibe_math_lean_run`;
407
+ 跑不通的代码不得进入 `Formal/Lib` / `Formal/Proved`,否则"可复用库"会被不编译的定义污染。
408
+ - **工具链缺失时的出路必须写出来**:`LEAN_NOT_FOUND` 时把代码写下来归档,并在回执的 `note` 里写明
409
+ "宿主无 Lean 工具链"——这算显式阻塞原因,`require` 档可以据此放行,代理不会因为装不了 Lean 而卡死。
410
+
411
+ #### 9.5 人可读语料
412
+
413
+ `prompt-corpus-v2/formal-verify-v2.json` 与 `.md` 由套件落盘(契约 §10.10):框架**真正发出**的每条提示词
414
+ 原文,工作区路径归一化为 `<WS>`、VibeMath 根归一化为 `<VIBEMATH>`,因此可 diff、不泄露本机路径。
415
+ 覆盖 off 档(无任何 Lean 文字)、encourage/require 的表决初评与辩论、`passed` 后的忠实性分支、
416
+ 工作轮的「顺手形式化」段落与 formal 回执契约。套件带 `V2_CORPUS_DIR` 覆盖;当 `V2_PLUGIN` 指向
417
+ 变异副本(灵敏度探针)时语料写到临时目录,避免把变异文本写进仓库。
418
+
419
+ ### 十、测试
321
420
 
322
421
  `formal-verify-v2.test.mjs`(仓库根):用 **mock `subprocess` 服务**(假 Lean:文件中含
323
422
  `sorry` 或 `-- FAIL` 时 exit 1,否则 exit 0)跑通真实代码路径,覆盖 `off` 无操作、
324
423
  参数校验与运行中切换、`encourage` 对 review/debate/work 三类提示词的注入、
325
- `passed` 后切忠实性措辞、三个工具(绿灯/红灯/缺文件/越界/非 .lean/缺工具链/四种归档/索引重建、
326
- `blocked` 必须有 note)、`require` 门禁(命题与问题两条收口点、搁置→形式化→放行、
327
- 卡片记录形式化状态、`resume` 后不遗忘、`encourage` 不设门禁)。
424
+ `passed` 后切忠实性措辞(并断言**不含**"偏离 → 0"、不含缩写工具名)、三个工具(绿灯/红灯/缺文件/
425
+ 越界/非 .lean/缺工具链/四种归档/索引重建、`blocked` 必须有 note)、`require` 门禁(命题与问题两条
426
+ 收口点、搁置→形式化→放行、卡片记录形式化状态、`resume` 后不遗忘、`encourage` 不设门禁),
427
+ 以及**回执通道的行为断言**(契约 §10.8/§10.9):把带 `formal:{decision:'blocked'|'defect', note}`
428
+ 的真实回执喂给框架,断言记录真的落库(`blocked` → `blocked` 且两套 id 同步;`defect` → `attempted`
429
+ + `proof` 清空 + `Verified/Lean/<id>.lean` 被删 + `Formal/TODO.md` 出现条目 + 公告),断言缺 `note`
430
+ 被 `V2_INVALID_ARGUMENT` 拒绝且不落记录,断言 `defect` 之后 `require` 档**不得定论**(无 Verified 卡片、
431
+ 命题布尔估计不变),并落盘人可读语料(§9.5)。
432
+
433
+ 后续补强(同一套件):
434
+
435
+ - 第 4b 节:超时必须**主动**终止——mock 记录 `terminate()` 调用(`-- HANG` 文件模拟一个不会自己
436
+ 结束的运行),断言返回 `LEAN_TIMEOUT` 且真的调用了 `terminate()`;并断言 `done` 先到时计时器
437
+ 被清掉(套件用 test-only 的悬挂 `setTimeout` 计数;mock 自身的兜底用真实计时器,不污染计数)。
438
+ - 第 13 节:归档写在**对象 id**、`rId` 侧还没有记录时,`require` 档仍必须能定论(门禁读合并记录),
439
+ 且该对象**不得**被写进「形式化待办」。
440
+ - 第 14 节:`defect` 的撤回覆盖**全部 id 别名**(归档写 `rId`、回执写对象 id 也能删干净);
441
+ 宿主**不能删除**时(`{ noSubprocess: true }` 的宿主)归档路径必须不再包含原证明文本、并且带
442
+ 撤回通知;活动日志必须说明"覆写"而不是"删除"。
443
+ - 第 15 节:`require` 档必须拦住「判断命题」问题解法到**源命题**的转移(`布尔估计` / `优先级` /
444
+ `已验证` / 概率=1 条目 / Verified 卡片全部不动,源命题进待办)。
445
+ - 模式相关的忠实性措辞:`encourage` 档**不得**出现"本次裁定**不定论**"(改写为"本档没有门禁"),
446
+ `require` 档**必须**出现(那里门禁真的会搁置裁定)。
447
+ - `kind='def'/'lemma'` 运行未通过时,工具返回值**不得**声称"可直接 import 复用"。
448
+
328
449
  套件支持 `V2_PLUGIN` 环境变量指向变异副本,供灵敏度探针使用。
@@ -298,7 +298,6 @@ export function apply(ctx) {
298
298
  { name: 'plannerEnabled', type: 'boolean', description: 'false = 完全走内置启发式调度(规划代理禁用)', suggestion: true },
299
299
  { name: 'plannerProvider', type: 'string', description: '规划代理模型 provider(空 = 继承根代理)', suggestion: '' },
300
300
  { name: 'plannerModel', type: 'string', description: '规划代理模型 id(空 = 继承根代理)', suggestion: '' },
301
- { name: 'plannerPersona', type: 'string', description: '注入规划代理提示词开头的人格/要求', suggestion: '' },
302
301
  { name: 'planMinIntervalMs', type: 'integer', description: '两次规划调用的最小间隔(毫秒);系统空闲且有工作时忽略', suggestion: 30000 },
303
302
  { name: 'plannerMaxFails', type: 'integer', description: '规划代理连续失败达此值 → 自动降级启发式', suggestion: 3 },
304
303
  { name: 'methodKeepIntervalMs', type: 'integer', description: 'Method Keeper 定时整理间隔(0 = 事件驱动)', suggestion: 0 },
@@ -1577,9 +1576,9 @@ export function apply(ctx) {
1577
1576
  }
1578
1577
  async function writeVerifiedPropositionCardIfNeeded(p) {
1579
1578
  if (p.概率 !== 1 && p.概率 !== 0) return false
1580
- // 写 Verified 卡片是"宣告定论"的收口点:require 模式下没过门就绝不写(防御性双保险,
1581
- // 正常路径已在 settleVerdict / processStatusUpdates 拦住)。
1582
- if (formalBlocksConclusion(p.id)) return false
1579
+ // 门禁只在**唯一的收口点**(writeVerifiedCardIfChanged)判定:那里的语义是"新卡要过门、
1580
+ // 已存在的卡只做刷新(可能要把被 defect 撤回的形式化状态如实改掉)"。在这里提前 return false
1581
+ // 会把两种情形一起挡掉,于是 require 档下一张已存在的卡片会永久宣称「形式化: Lean 通过」。
1583
1582
  const proofs1 = (p.proofs || []).filter(function (x) { return x.prob === 1 })
1584
1583
  const refutes1 = (p.refutes || []).filter(function (x) { return x.prob === 1 })
1585
1584
  const parts = []
@@ -1590,7 +1589,7 @@ export function apply(ctx) {
1590
1589
  }
1591
1590
  async function writeVerifiedProblemCardIfNeeded(q) {
1592
1591
  if (q.状态 !== '已解决') return false
1593
- if (formalBlocksConclusion(q.id)) return false
1592
+ // 同 writeVerifiedPropositionCardIfNeeded:门禁统一在 writeVerifiedCardIfChanged 里判定。
1594
1593
  const sols1 = (q.solutions || []).filter(function (s) { return s.prob === 1 })
1595
1594
  const parts = []
1596
1595
  for (let i = 0; i < sols1.length; i++) parts.push('【解法 #' + (i + 1) + '】' + (sols1[i].text || ''))
@@ -1603,8 +1602,13 @@ export function apply(ctx) {
1603
1602
  const existing = await readText(rel)
1604
1603
  // 最后一道闸门:require 模式下没有 passed/blocked 记录就不允许**新写** Verified 卡片。
1605
1604
  // 卡片已经存在(切到 require 之前就已定论)只做刷新,不算"新的定论",因此不记待办。
1606
- if (formalBlocksConclusion(card.id)) {
1607
- if (existing === undefined) await deferForFormal(card.id, formalRequiredWhy(card.id), card.结论 === true)
1605
+ //
1606
+ // 但"只做刷新"必须**真的刷新**:`defect` 会撤回形式化(降级 attempted、proof 清空、归档证明
1607
+ // 删除/覆盖),若在这里连刷新也一起 return false,那张已存在的卡片会永久宣称
1608
+ // 「形式化: Lean 通过(Verified/Lean/<id>.lean)」——指向一份已经不存在的证明。门禁管的是
1609
+ // "能不能宣告新结论",不是"能不能说实话"。所以:不存在 → 记待办并拒绝新写;已存在 → 照常刷新。
1610
+ if (formalBlocksConclusion(card.id) && existing === undefined) {
1611
+ await deferForFormal(card.id, formalRequiredWhy(card.id), card.结论 === true)
1608
1612
  return false
1609
1613
  }
1610
1614
  const md = composeVerifiedMd(card)
@@ -2588,7 +2592,7 @@ export function apply(ctx) {
2588
2592
  }
2589
2593
  /**
2590
2594
  * 把一次运行记到对象上(契约 §4 状态迁移)。普通运行最多把对象推进到 `attempted`;
2591
- * `passed` 只能由 lean_archive{kinds:'proof'} 且该文件最近一次运行 ok 产生——否则一次
2595
+ * `passed` 只能由 vibe_math_lean_archive{kind:'proof'} 且该文件最近一次运行 ok 产生——否则一次
2592
2596
  * 顺手跑个无关文件就能把对象"洗白"成已形式化。passed/blocked 不会被普通运行降级。
2593
2597
  */
2594
2598
  async function formalSetRun(target, run) {
@@ -2612,28 +2616,41 @@ export function apply(ctx) {
2612
2616
  const L = []
2613
2617
  L.push('【Lean 形式化验证(' + (mode === 'require' ? '强制' : '鼓励') + '模式)】')
2614
2618
  if (rec.status === 'passed') {
2615
- // 整套机制的要害:审查对象**变了**。
2619
+ // 整套机制的要害:审查对象**变了**——不是"推导对不对",而是"这段代码说的是不是这个命题"。
2620
+ // §4.1:忠实性缺陷**不是**"命题为假",所以这一支必须同时给出 defect 出口(不得把偏差记成 0)。
2616
2621
  L.push(' · 该对象已有**通过的 Lean 形式化证明**(' + (rec.proof || rec.file || '') + ',最近一次运行 exit 0)。')
2617
2622
  L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')
2618
- L.push(' 定义 / 对象 / 条件 / 假设 / 结论是否与命题原文**完全一致**(有偏差就指出偏差),')
2619
- L.push(' 并据此给出 verdict。')
2623
+ L.push(' 定义 / 对象 / 条件 / 假设 / 结论是否与命题原文**完全一致**。')
2624
+ L.push(' ▸ 一致 → Result = 1。')
2625
+ L.push(' ▸ **发现任何偏差,不要投 0**:偏差只说明**形式化不合格**,不代表命题为假。此时请:')
2626
+ L.push(' ① Result 给一个严格介于 0 与 1 之间的值(记为弃权),并在 Reason 里写清偏差;')
2627
+ L.push(" ② 用回执 formal:{decision:'defect', note:'<具体偏差>'} 记录它。框架会撤回这条证明的")
2628
+ // §4.1 第 3 条:只有 `require` 真的有门禁。`encourage` 档框架**仍然**撤回证明并记入待办,
2629
+ // 但**不得**承诺一个它无法强制的"不定论"——那里靠表决者自己的弃权票使表决无法得出一致结论。
2630
+ L.push(' 「已通过」状态(降级为 attempted、删除或就地覆盖归档证明、写入形式化待办)'
2631
+ + (mode === 'require'
2632
+ ? ',本次裁定**不定论**;'
2633
+ : '。**本档没有门禁**:请务必给弃权值,以保证本轮无法得出一致结论;'))
2634
+ L.push(' 修正形式化并重新跑通后再投票。')
2635
+ L.push(' ▸ 只有当你**独立于这份 Lean 代码**也能确定命题为假时,才投 0,并在 Reason 里写清独立理由。')
2620
2636
  } else if (rec.status === 'blocked') {
2621
2637
  L.push(' · 该对象已被记录为**形式化阻塞**:' + (rec.note || '未说明') + '。')
2622
2638
  L.push(' 请复核这个判断是否成立;若你认为其实可以形式化,请指出来并动手做。')
2623
2639
  } else {
2624
2640
  L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
2625
2641
  L.push(' · 工具:vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库)')
2626
- L.push(' · 工作目录:Formal/(相对项目根);可复用定义放 ' + (vibeRoot() + '/Formal/Lib/').replace(/\\/g, '/'))
2627
- L.push(' 已证引理放 ' + (vibeRoot() + '/Formal/Proved/').replace(/\\/g, '/') + ';写之前先 lean_lib 查重。')
2628
- L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义 / 对象 / 条件 / 假设 / 结论是否与')
2629
- L.push(' 命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。')
2642
+ L.push(' · 工作目录:Formal/(相对项目根);可复用定义放 ' + (vibeRoot() + '/Formal/Lib/').replace(/\\/g, '/')
2643
+ + ',已证引理放 ' + (vibeRoot() + '/Formal/Proved/').replace(/\\/g, '/') + ';写之前先 vibe_math_lean_lib 查重。')
2644
+ L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。')
2645
+ // 把"这么做的收益"说出来:本轮通过,下一轮的审查对象就整体换掉了(不是再加一道苦役)。
2646
+ L.push(' ▸ 若你在本轮把它形式化并跑通(vibe_math_lean_archive kind=\'proof\'),后续轮次的审查对象就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。')
2630
2647
  if (mode === 'require') {
2631
- L.push(' · **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因(vibe_math_lean_archive')
2632
- L.push(' kind=\'blocked\' note=… 或回执 formal.note)。若两者都没有,本次裁定不会生效,')
2633
- L.push(' 会被记为未定论(原因 formal-required)并进入「形式化待办」。')
2648
+ L.push(' · **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因(vibe_math_lean_archive kind=\'blocked\' note=… 或回执 formal.note)。若两者都没有,本次裁定不会生效,会被记为未定论(原因 formal-required)并进入「形式化待办」。')
2634
2649
  } else {
2635
- L.push(' · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断。')
2650
+ L.push(' · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision=\'blocked\' 时必须写明 note)。')
2636
2651
  }
2652
+ L.push(' · 归档可复用定义/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run);跑不通不要入库。')
2653
+ L.push(' · 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或宿主不提供 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并在回执的 note 里写明"宿主无 Lean 工具链"——这算显式阻塞原因,定论门禁可以据此放行。')
2637
2654
  }
2638
2655
  return L.join('\n')
2639
2656
  }
@@ -2646,12 +2663,14 @@ export function apply(ctx) {
2646
2663
  + (formalMode() === 'require'
2647
2664
  ? '本模式下,任何要定论为真/假的对象都必须先有 Lean 通过或显式阻塞记录。'
2648
2665
  : '这会让后续的验证与证明省掉大量重复工作。')
2666
+ // 硬要求 3(契约 §6 顶部):可复用库只收**跑通过**的代码,否则它会被不编译的定义污染。
2667
+ + '归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。'
2649
2668
  }
2650
2669
  /** 回执契约里的 formal 字段(契约 §6.3):非 off 模式必须出现在回执契约里,否则这条通道不可发现。 */
2651
2670
  function formalJsonField(target) {
2652
2671
  if (!formalOn()) return ''
2653
2672
  const id = String(target == null ? '' : target) || '<对象id>'
2654
- return ',"formal":{"target":"' + id + '","decision":"used|blocked","file":"Formal/' + id + '.lean","note":"难度判断/阻塞原因"}'
2673
+ return ',"formal":{"target":"' + id + '","decision":"used|blocked|defect","file":"Formal/' + id + '.lean","note":"难度判断/阻塞原因/具体偏差"}'
2655
2674
  }
2656
2675
  /**
2657
2676
  * 工作轮(solver / explorer)回执契约里的 formal 字段。这些角色的回执本身就是一段 JSON 模板,
@@ -2661,15 +2680,17 @@ export function apply(ctx) {
2661
2680
  function formalReplyNote() {
2662
2681
  if (!formalOn()) return ''
2663
2682
  return '\n形式化回执(本模式):若你本轮对某个对象做了形式化难度判断,请在回执里加上 '
2664
- + '"formal":{"target":"<对象id>","decision":"used|blocked","file":"Formal/<对象id>.lean","note":"难度判断/阻塞原因"}'
2665
- + '(decision=\'blocked\' 时必须写明 note,否则拒绝记录)。'
2683
+ + '"formal":{"target":"<对象id>","decision":"used|blocked|defect","file":"Formal/<对象id>.lean","note":"难度判断/阻塞原因/具体偏差"}'
2684
+ + '(decision=\'blocked\' 与 decision=\'defect\' 时必须写明 note,否则拒绝记录;'
2685
+ + 'decision=\'defect\' 表示你认定这条已通过的 Lean 形式化**不忠实于命题原文**——'
2686
+ + '那不是"命题为假",框架会撤回其已通过状态并把对象放回形式化待办)。'
2666
2687
  }
2667
2688
  /** 从一条代理回执里取出 formal 判断并落库(顶层 formal 或 meta.formal 都接受)。 */
2668
2689
  async function absorbFormalFromReply(parsed, memberId) {
2669
2690
  if (!formalOn() || !parsed || typeof parsed !== 'object') return
2670
2691
  const f = (parsed.formal && typeof parsed.formal === 'object') ? parsed.formal
2671
2692
  : ((parsed.meta && typeof parsed.meta.formal === 'object') ? parsed.meta.formal : null)
2672
- if (f) await absorbFormalReply(f, memberId)
2693
+ if (f) return await absorbFormalReply(f, memberId)
2673
2694
  }
2674
2695
  /** 对象 id → 关联可验证对象(命题/问题)。用于把表决对象映射到形式化记录。 */
2675
2696
  function verifyTargetId(r) { return String((r && (r.pId || r.qid)) || '') }
@@ -2752,7 +2773,10 @@ export function apply(ctx) {
2752
2773
  }
2753
2774
  L.push('')
2754
2775
  if (formalTodo().length) {
2755
- L.push('## 形式化待办(require 模式:定论被搁置)')
2776
+ // §4.1 第 3 条:只有 `require` 真的搁置定论;`encourage` 档的待办只是"这份形式化要重做"的记录。
2777
+ L.push(formalMode() === 'require'
2778
+ ? '## 形式化待办(require 模式:定论被搁置)'
2779
+ : '## 形式化待办(encourage 档:框架不搁置定论,靠表决者弃权)')
2756
2780
  for (const t of formalTodo()) L.push('- ' + t.id + ' —— ' + (t.why || 'formal-required') + '(' + fmtTime(t.at) + ')')
2757
2781
  L.push('')
2758
2782
  }
@@ -2761,7 +2785,10 @@ export function apply(ctx) {
2761
2785
  async function writeFormalTodo() {
2762
2786
  const list = formalTodo()
2763
2787
  const L = ['# 形式化待办|' + currentProject + '|' + fmtTime(), '',
2764
- '> 这些对象在 `require` 模式下尚不具备「Lean 已通过」或「显式阻塞记录」,因此**定论被搁置**。',
2788
+ // 说清这一档**实际**会发生什么:只有 require 有门禁会把定论记为未定论。
2789
+ formalMode() === 'require'
2790
+ ? '> 这些对象在 `require` 模式下尚不具备「Lean 已通过」或「显式阻塞记录」,因此**定论被搁置**。'
2791
+ : '> 这些对象尚未取得「Lean 已通过」或「显式阻塞记录」。本档(encourage)**没有定论门禁**,框架不会搁置裁定——请在投票时给出严格介于 0 与 1 之间的弃权值,并尽快修正形式化。',
2765
2792
  '> 完成形式化(vibe_math_lean_archive kind=\'proof\')或记录阻塞原因(kind=\'blocked\')后,重新提议验证即可。', '']
2766
2793
  if (!list.length) L.push('(暂无)')
2767
2794
  for (const t of list) L.push('- ' + t.id + '|' + (t.why || 'formal-required') + '|' + fmtTime(t.at))
@@ -2858,10 +2885,16 @@ export function apply(ctx) {
2858
2885
  await writeFormalIndex()
2859
2886
  }
2860
2887
  if (run.ok) await formalAnnounce('【形式化】' + (memberId || 'scheduler') + ' 运行 Lean 通过:' + run.file + '(' + ((run.ms || 0) / 1000).toFixed(1) + 's)' + (target ? '|对象 ' + target : ''))
2888
+ // 失败提示必须与失败原因一致:工具链缺失 / 宿主没有 subprocess 服务时**没有任何编译器输出**
2889
+ // 可以"按它修复",把它当成普通编译错误会让代理反复重试而不是走"写下代码 + 记录显式阻塞"
2890
+ // 这条出路(契约 §6 硬要求 4)。
2891
+ const noHost = run.code === 'LEAN_NOT_FOUND' || run.code === 'NO_SUBPROCESS' || run.code === 'LEAN_SPAWN_FAILED'
2861
2892
  return Object.assign({ ok: !!run.ok }, run, {
2862
2893
  hint: run.ok
2863
- ? '通过。若是某个对象的证明,请用 lean_archive kind=\'proof\' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind=\'def\'/\'lemma\' 归档到全局库。'
2864
- : '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 lean_archive kind=\'blocked\' 记录原因。',
2894
+ ? '通过。若是某个对象的证明,请用 vibe_math_lean_archive kind=\'proof\' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind=\'def\'/\'lemma\' 归档到全局库。'
2895
+ : (noHost
2896
+ ? '本宿主无法执行 Lean(' + run.code + '),没有编译器输出可以修:把形式化代码写下来并用 vibe_math_lean_archive 归档,并在回执的 note 里写明原因——这算显式阻塞原因,定论门禁可以据此放行。'
2897
+ : '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 vibe_math_lean_archive kind=\'blocked\' 记录原因。'),
2865
2898
  })
2866
2899
  }
2867
2900
  async function leanArchive(memberId, o) {
@@ -2904,20 +2937,28 @@ export function apply(ctx) {
2904
2937
  if (body === undefined && from) { const r = await readFrom(); if (r.err) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: r.err }; body = r.body }
2905
2938
  if (body === undefined) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
2906
2939
  const workRel = 'Formal/' + target + '.lean'
2907
- if (!await writeText(workRel, body)) return { ok: false, code: 'V3_WRITE_FAILED', message: 'could not write ' + workRel }
2940
+ // 用绝对路径写:`writeText` 写失败时只会抛(由工具包装层变成通用 error),
2941
+ // 这一行的 V3_WRITE_FAILED 分支就永远不可能触发;writeTextAbs 会如实返回 false。
2942
+ if (!await writeTextAbs(frameworkRoot() + '/' + workRel, body)) return { ok: false, code: 'V3_WRITE_FAILED', message: 'could not write ' + workRel }
2908
2943
  const run = await leanRunFile(workRel)
2909
2944
  const prev = formalOf(target)
2910
2945
  const passed = !!run.ok
2946
+ // A RED re-archive invalidates the previous proof: the work file it proved was just
2947
+ // overwritten by code that does not compile. Keeping the pointer (or the archived file)
2948
+ // would produce "attempted + 归档证明 X.lean" in the index and let the fidelity prompt print
2949
+ // a proof path for code that no longer exists — `proof` is for `passed` only (contract §4).
2950
+ const stalePrev = passed ? '' : String(prev.proof || ('Verified/Lean/' + target + '.lean'))
2911
2951
  const rec = Object.assign({}, prev, {
2912
2952
  status: passed ? 'passed' : 'attempted',
2913
2953
  file: workRel,
2914
- proof: passed ? 'Verified/Lean/' + target + '.lean' : (prev.proof || ''),
2954
+ proof: passed ? 'Verified/Lean/' + target + '.lean' : '',
2915
2955
  decision: 'used',
2916
2956
  note: String(args.note || prev.note || ''),
2917
2957
  run: { at: now(), ok: !!run.ok, exitCode: run.exitCode === undefined ? null : run.exitCode, ms: run.ms || 0, stdoutTail: formalTail(run.stdout, 800), stderrTail: formalTail(run.stderr, 800) },
2918
2958
  updatedAt: now(),
2919
2959
  })
2920
2960
  if (passed) await writeText('Verified/Lean/' + target + '.lean', body)
2961
+ else if (stalePrev) { try { await withdrawArchivedProof(stalePrev) } catch (e) { /* 撤回失败已在公告里如实说明 */ } }
2921
2962
  await putFormal(target, rec)
2922
2963
  await upsertFormalAnchor(target)
2923
2964
  await rebuildLeanLibIndexes()
@@ -2941,9 +2982,80 @@ export function apply(ctx) {
2941
2982
  }
2942
2983
  return { ok: false, code: 'V3_INVALID_ARGUMENT', message: "kind must be 'def' | 'lemma' | 'proof' | 'blocked'" }
2943
2984
  }
2985
+ /**
2986
+ * 撤回归档证明:**删除**既不是唯一手段,也不是想当然就能成功的手段。
2987
+ *
2988
+ * fs 服务没有 unlink,`subprocess` 服务是**可选**的(`runShell` 在没有它时只返回 no-subprocess),
2989
+ * 删除命令本身也可能静默失败(桩宿主、权限、宿主不提供 shell)。而归档证明就躺在
2990
+ * `Verified/Lean/<id>.lean`——所有人都去那个路径找"这条结论的证明"——所以"删掉了"必须被
2991
+ * **回读验证**:删不掉就用撤回声明**就地覆盖**,使它不可能再被读成一份通过的证明。
2992
+ * 返回:'deleted'(确认已不在)| 'overwritten'(已覆盖为撤回声明)| 'failed'(两者都没成功)。
2993
+ */
2994
+ async function withdrawArchivedProof(rel) {
2995
+ const abs = leanAbsPath(rel)
2996
+ if (abs === null) return 'failed'
2997
+ const sub = subprocessOf()
2998
+ if (sub !== undefined && typeof sub.spawn === 'function') {
2999
+ try { await removeFile(rel) } catch (e) { /* 落到覆盖兜底 */ }
3000
+ // 退出码 0 不等于文件真的没了(桩宿主 / 权限怪癖 / 删除被静默忽略):必须回读确认。
3001
+ if (await readTextAbs(abs) === undefined) return 'deleted'
3002
+ }
3003
+ const notice = '-- 已撤回(' + fmtTime() + '):该形式化被认定与命题原文不一致。\n'
3004
+ + '-- 原代码保留在工作文件 Formal/' + String(rel).split('/').pop() + ';修正并重新跑通后重新归档。\n'
3005
+ return (await writeTextAbs(abs, notice)) !== false ? 'overwritten' : 'failed'
3006
+ }
3007
+ /**
3008
+ * §4.1 的落地点:表决者认定这条**已通过的** Lean 形式化不忠实于命题原文。
3009
+ *
3010
+ * 偏差不是"命题为假",而恰好是"这次形式化不合格",因此这里的动作与 blocked 不同:
3011
+ * ① 无论此前是 `passed` 还是 `blocked`,一律**降级**为 `attempted`(都让位于"需重做");
3012
+ * ② 清空 `proof` 并**撤下** `Verified/Lean/<id>.lean`(工作文件 `Formal/<id>.lean` 保留,代码不丢);
3013
+ * ③ 把具体偏差写进记录与 `Formal/TODO.md`,并公告。
3014
+ * 之后 `formalGateOk` 为假:require 档本次裁定**不定论**,对象进入「形式化待办」——修正形式化
3015
+ * 并重新跑通后再投票。encourage 档没有门禁,公告里**不得**声称框架搁置了裁定(§4.1 第 3 条)。
3016
+ * 绝不把这条路径写成 `0`(那会让框架记下"命题为假")。
3017
+ */
3018
+ async function formalRecordDefect(target, note, who) {
3019
+ const id = formalId(target)
3020
+ if (!id) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'defect 记录必须写明 target' }
3021
+ const prev = formalOf(id)
3022
+ const archived = String(prev.proof || '').trim() || ('Verified/Lean/' + id + '.lean')
3023
+ await putFormal(id, Object.assign({}, prev, {
3024
+ status: 'attempted',
3025
+ decision: 'defect',
3026
+ note: note,
3027
+ proof: '',
3028
+ updatedAt: now(),
3029
+ }))
3030
+ // 归档证明必须消失,否则 Verified/Lean/ 里会留下一份"看起来已通过"的不忠实代码。
3031
+ const withdrawn = await withdrawArchivedProof(archived)
3032
+ // 待办条目按 id 去重、就地刷新:note 进入 TODO.md 的 why 列(require 档据此搁置定论)。
3033
+ const list = formalTodo()
3034
+ const i = list.findIndex(function (x) { return x && x.id === id })
3035
+ const why = 'formal-defect:' + note
3036
+ if (i === -1) list.push({ id: id, at: now(), why: why, verdict: null, project: currentProject })
3037
+ else list[i] = Object.assign({}, list[i], { why: why, at: now() })
3038
+ await writeFormalTodo()
3039
+ await upsertFormalAnchor(id)
3040
+ await rebuildLeanLibIndexes()
3041
+ await saveAll()
3042
+ // 如实说出**哪一种**撤回发生了:删除成功 / 就地覆盖 / 两者都失败(后者必须让人手动处理,
3043
+ // 否则一份不忠实的代码会静静留在"证明"的路径上而无人知晓)。
3044
+ await formalAnnounce('【形式化】' + who + ' 认定 ' + id + ' 的 Lean 形式化存在**忠实性缺陷**:' + note
3045
+ + '。这不是"命题为假",而是**形式化不合格**:已撤回其「已通过」状态(降级为 attempted)、'
3046
+ + (withdrawn === 'deleted' ? '删除归档证明 ' + archived
3047
+ : withdrawn === 'overwritten' ? '归档证明 ' + archived + ' 无法删除(宿主不支持删除),已**就地覆盖为撤回声明**'
3048
+ : '归档证明 ' + archived + ' **未能撤回**(宿主删除与覆盖均失败,请手动删除,不要把它当作该对象的证明)')
3049
+ + '、写入 Formal/TODO.md;'
3050
+ + (formalMode() === 'require'
3051
+ ? '本次裁定**不定论**,修正形式化并重新跑通(vibe_math_lean_archive kind=\'proof\')后再投票。'
3052
+ : '**本档没有门禁**:框架不会替你搁置裁定,请给弃权值以避免得出真/假一致结论;修正形式化并重新跑通(vibe_math_lean_archive kind=\'proof\')后再投票。'))
3053
+ return { ok: true, kind: 'defect', target: id, status: 'attempted', proof: '', note: note, archived: archived }
3054
+ }
2944
3055
  /**
2945
3056
  * 回执通道(契约 §3/§6.3):代理即使一次 Lean 工具都没调用,也必须能留下"实现难度判断"。
2946
- * decision='blocked' 时 note 必填(否则拒绝记录并公告);'used' 记录草稿文件 → attempted。
3057
+ * decision='blocked' 时 note 必填(否则拒绝记录并公告);'used' 记录草稿文件 → attempted;
3058
+ * decision='defect' 时 note 必填(具体偏差),落库语义见 §4.1 与 `formalRecordDefect`。
2947
3059
  */
2948
3060
  async function absorbFormalReply(f, memberId) {
2949
3061
  if (!formalOn() || !f || typeof f !== 'object') return
@@ -2960,6 +3072,16 @@ export function apply(ctx) {
2960
3072
  await formalAnnounce('【形式化】' + who + ' 通过回执记录 ' + target + ' 形式化阻塞:' + note)
2961
3073
  return
2962
3074
  }
3075
+ if (decision === 'defect') {
3076
+ const note = String(f.note || '').trim()
3077
+ if (!note) {
3078
+ // 与 blocked 同样"显式、可审计":没写出具体偏差就无法复核,整条记录拒绝(返回该架构的错误码)。
3079
+ await formalAnnounce('【形式化】' + who + ' 的 formal.decision=defect 未写明 note,已**拒绝**记录'
3080
+ + '(忠实性缺陷必须写出具体偏差,否则无从复核)。该对象的形式化记录与归档证明**保持不变**。')
3081
+ return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'defect 记录必须写明具体偏差(note)——"形式化与命题不一致"必须显式、可复核' }
3082
+ }
3083
+ return await formalRecordDefect(target, note, who)
3084
+ }
2963
3085
  if (decision === 'used') {
2964
3086
  const prev = formalOf(target)
2965
3087
  const file = String(f.file || ('Formal/' + target + '.lean'))
@@ -2969,7 +3091,7 @@ export function apply(ctx) {
2969
3091
  await formalAnnounce('【形式化】' + who + ' 通过回执记录 ' + target + ' 形式化草稿:' + file)
2970
3092
  return
2971
3093
  }
2972
- await formalAnnounce('【形式化】' + who + ' 的 formal.decision 只能是 \'used\' 或 \'blocked\'(收到 ' + String(f.decision) + '),已忽略。')
3094
+ await formalAnnounce('【形式化】' + who + ' 的 formal.decision 只能是 \'used\'、\'blocked\' 或 \'defect\'(收到 ' + String(f.decision) + '),已忽略。')
2973
3095
  }
2974
3096
 
2975
3097
  // ================= verification (验证器) =================
@@ -3330,7 +3452,7 @@ export function apply(ctx) {
3330
3452
  registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), async function () { await refreshParams(); return await getStatus() })
3331
3453
  registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json + Logs/报告.md.', objParams({}), async function () { await refreshParams(); await maybeWriteReport(true); return await buildReport() })
3332
3454
  registerTool('vibe_math_set_mode', 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.', objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), async function (args) { params.mode = args.mode; await saveAll(); await saveSettings(); if (params.mode === 'auto') await autoResolvePending(); return { ok: true, mode: params.mode } })
3333
- registerTool('vibe_math_set_params', 'Update scheduler parameters (partial).', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
3455
+ registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象未达到 Lean 已通过或已记录显式阻塞原因之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
3334
3456
  registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), async function () { await refreshParams(); const list = PARAM_SCHEMA.map(function (p) { const out = Object.assign({}, p); out.current = params[p.name]; out.default = DEFAULT_PARAMS[p.name]; return out }); return { ok: true, parameters: list, saveTo: frameworkRoot() + '/vibe_math_setting.json' } })
3335
3457
  registerTool('vibe_math_save_settings', 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.', objParams({}), async function () { return await saveSettings() })
3336
3458
  registerTool('vibe_math_template', 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.', objParams({ where: { type: 'string', enum: ['global', 'project'] } }), async function (args) { return await createTemplate((args && args.where) || 'global') })
@@ -3376,7 +3498,7 @@ export function apply(ctx) {
3376
3498
  return { target: k, status: rec.status, file: rec.file, proof: rec.proof, note: rec.note }
3377
3499
  }),
3378
3500
  paths: { project: 'Formal/(相对项目根)', lib: 'VibeMath/Formal/Lib/', proved: 'VibeMath/Formal/Proved/', proofs: 'Verified/Lean/' },
3379
- hint: "复用优先:先在 Lib/ 里找现成定义;新定义用 lean_archive kind='def' 归档,已证引理用 kind='lemma'。",
3501
+ hint: "复用优先:先在 Lib/ 里找现成定义;新定义用 vibe_math_lean_archive kind='def' 归档,已证引理用 kind='lemma';归档前先跑通(run=true 或先 vibe_math_lean_run),跑不通不要入库。",
3380
3502
  verify: (Object.keys(tasks).length ? String(tasks[Object.keys(tasks)[0]].rId || '') : null),
3381
3503
  }
3382
3504
  })
@@ -3570,8 +3692,12 @@ export function apply(ctx) {
3570
3692
  // ================= session surface =================
3571
3693
  return {
3572
3694
  sessionId: sessionId,
3573
- scheduler: scheduler,
3574
- tickInFlight: tickInFlight,
3695
+ // 这两个必须是**取值器**而不是快照:`scheduler` 会在 loadState/setProject 里被整体重新赋值,
3696
+ // 而 `tickInFlight` 每次 tick 都会翻转。快照会让 apply 级的定时器守卫(见文件末尾
3697
+ // `!s.tickInFlight && s.scheduler.gate === null`)永远读到最初的值——那个守卫就再也拦不住
3698
+ // 任何东西(tick() 内部还有一道实时守卫,所以此前没有可观测后果,但那是巧合而非设计)。
3699
+ get scheduler() { return scheduler },
3700
+ get tickInFlight() { return tickInFlight },
3575
3701
  scheduleTick: scheduleTick,
3576
3702
  onChildEnd: onChildEnd,
3577
3703
  dispatchVibeCommand: dispatchVibeCommand,
@@ -3605,7 +3731,7 @@ export function apply(ctx) {
3605
3731
  registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), 'vibe_math_status')
3606
3732
  registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json + Logs/报告.md.', objParams({}), 'vibe_math_report')
3607
3733
  registerTool('vibe_math_set_mode', 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.', objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
3608
- registerTool('vibe_math_set_params', 'Update scheduler parameters (partial).', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' } }), 'vibe_math_set_params')
3734
+ registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象未达到 Lean 已通过或已记录显式阻塞原因之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), 'vibe_math_set_params')
3609
3735
  registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), 'vibe_math_setup')
3610
3736
  registerTool('vibe_math_save_settings', 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.', objParams({}), 'vibe_math_save_settings')
3611
3737
  registerTool('vibe_math_template', 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.', objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')