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
@@ -498,7 +498,7 @@ VibeMath/ # 工作区级
498
498
  | `vibe_math_decide` | 扩展支持 `node: 'plan'`(计划审批)与 `node: 'method-promote'` |
499
499
  | `vibe_math_lean_run` | `{file, target?, timeout_ms?}`:在一个 `.lean` 文件上执行 Lean 工具链并**如实**回报 `{ok, exitCode, signal, ms, command, stdout, stderr, file}`;给了 `target` 就把这次运行记到该对象上。**绝不抛异常到调度循环**:宿主没有 subprocess 服务 → `NO_SUBPROCESS`,工具链解析失败 → `LEAN_NOT_FOUND`,超时 → `LEAN_TIMEOUT`(并调用 `handle.terminate()`) |
500
500
  | `vibe_math_lean_archive` | `{kind, ...}` 一个工具覆盖四种归档:`kind='def'`/`'lemma'` → 全局 `<VibeMath 根>/Formal/{Lib,Proved}/<name>.lean` 并重建对应索引(`content` 或 `from` 二选一;`run:false` 跳过执行);`kind='proof'` → 写 `Formal/<target>.lean`,该文件最近一次运行 ok 时**同时**写 `Verified/Lean/<target>.lean` 并把对象标为 `passed`;`kind='blocked'` → 记录显式难度判断/阻塞原因(`note` 必填,空则拒绝) |
501
- | `vibe_math_lean_lib` | `{refresh?}`:扫描并重建三处索引(项目 `Formal/Index.md`、全局 `Lib/Index.md`、`Proved/Index.md`),返回可复用库清单(供写新定义前查重、直接复用);`refresh:false` 只读不重建 |
501
+ | `vibe_math_lean_lib` | `{refresh?}`:扫描并重建三处索引(项目 `Formal/Index.md`、全局 `Lib/Index.md`、`Proved/Index.md`),返回可复用库清单(供写新定义前查重、直接复用);`refresh:false` 只读不重建。另有 `objects`(每个对象的形式化状态)与 `verify`(当前在飞验证任务的标识,无则 `null`)两个字段,与 v5 的同名输出一致 |
502
502
 
503
503
  ### 11.1 Lean 形式化验证(v3 实现要点)
504
504
 
@@ -534,8 +534,8 @@ VibeMath/ # 工作区级
534
534
 
535
535
  **门禁落点**(契约 §8 要求唯一收口点,不散落):
536
536
  - 判定入口:`settleVerdict`(辩论/评审裁定,在**改对象之前**拦下)与 `processStatusUpdates`(自报概率 1 的第二条"宣告定论"入口);
537
- - 写卡收口点:`writeVerifiedPropositionCardIfNeeded` / `writeVerifiedProblemCardIfNeeded` → `writeVerifiedCardIfChanged`(require 模式下没有 `passed`/`blocked` 就**不写** Verified 卡片,作为防御性双保险);
538
- - 不通过时统一走 `deferForFormal`:记未定论(`formal-required`)+ 写 `Formal/TODO.md` + 公告(活动日志 + `Logs/形式化.md`),**不改变对象的既有权重/概率字段**,绝不卡死调度——对象留在原库,完成形式化或记录阻塞后重新提议验证即可;达到 `passed`/`blocked` 时自动把该对象从待办中移除(待办的含义就是"还不满足门禁")。
537
+ - 写卡收口点:`writeVerifiedPropositionCardIfNeeded` / `writeVerifiedProblemCardIfNeeded` → `writeVerifiedCardIfChanged`。门禁**只在这唯一的收口点**判定:卡片**不存在**时 require 模式下没有 `passed`/`blocked` 就**不写**(并记待办);卡片**已存在**时只做刷新——包括把被 `defect` 撤回的形式化状态如实改成 `已尝试未通过`(否则一张定论卡片会永久宣称「形式化: Lean 通过(Verified/Lean/<id>.lean)」而那份证明已被撤回。门禁管的是"能不能宣告新结论",不是"能不能说实话")。
538
+ - 不通过时统一走 `deferForFormal`:记未定论(`formal-required`)+ 写 `Formal/TODO.md` + 公告(活动日志 + `Logs/形式化.md`),**不改变对象的既有权重/概率字段**,绝不卡死调度——对象留在原库,完成形式化或记录阻塞后重新提议验证即可;达到 `passed`/`blocked` 时自动把该对象从待办中移除(待办的含义就是"还不满足门禁")。`Formal/TODO.md` 与 `Formal/Index.md` 的待办段按**当前档位**措辞:`require` 说"定论被搁置",`encourage` 说明本档没有门禁、框架不会搁置裁定(靠表决者弃权),不承诺它无法强制的行为(契约 §4.1 第 3 条)。
539
539
  - **防空转**:已经被记为「形式化待办」而形式化又没补齐的对象,`createVerifyTask` 不再为它建验证任务(否则同一个 `rId` 每个 tick 都会被重建、每轮再派一批验证器,而结果每轮都被同一道门拦下——纯烧验证预算)。一旦 `passed`/`blocked` 落库,门禁条件满足,候选自然重新出现并继续验证,无需人工干预。回退启发式在跳过这类候选时**不占用本轮名额**(`created` 才 `return`),否则排在待办对象后面的候选会被永久饿死。
540
540
 
541
541
  **错误码**(一律作为结果返回,不抛进调度循环):
@@ -553,6 +553,24 @@ VibeMath/ # 工作区级
553
553
 
554
554
  **执行与守卫**:`subprocess.resolveExecutable(leanCommand)` → `spawn({argv:[exe, ...leanArgs, abs], cwd:<项目根>, stdio:{stdin:'ignore',stdout/stderr:{maxBytes:64KB}}, graceMs:leanTimeoutMs})` → `await handle.done` → 收集 stdout/stderr(`readFrom(0).text`,截断到 4KB)。路径守卫 = `normalizeAbsPath`(纯词法折叠 `.`/`..`/重复斜杠)+ 边界为 **VibeMath 根**(不是项目根);`lean_run` 的 `file` 先按项目根解析、再按 VibeMath 根解析,两条路径都过同一道守卫。
555
555
 
556
+ ### 11.2 忠实性缺陷(`defect`)、工具名全称与归档纪律
557
+
558
+ **`defect` 是独立的一档,不是"命题为假"**(契约 §4.1)。`passed` 只保证"这段 Lean 代码通过了内核检查",**不保证它说的就是命题想说的**。表决者逐条核对后发现写窄了 / 写宽了 / 换了对象 / 漏了条件时,那是**形式化不合格**:若把它记成 0,框架记下的会是"该命题为假"——用来求真更严格的机制,反而伪造出一个错误的否定结论;若只记成 `blocked`,门禁又会**放行**(`blocked` 本就允许定论),等于带着一个坏形式化去定论。
559
+
560
+ - **回执**:`formal:{target, decision:'defect', note:'<具体偏差>'}`。`note` **必填**,为空则整条记录被**拒绝**(返回 `V3_INVALID_ARGUMENT`),形式化记录与归档证明**保持不变**。顶层 `formal` 与 `meta.formal` 两种写法都接受:验证回执走 `handleVerifier`,solver / explorer / 重派生等工作轮回执走 `absorbFormalFromReply`。
561
+ - **落库**(`formalRecordDefect`,契约 §4.1):无论此前是 `passed` 还是 `blocked`,一律**降级**为 `attempted`(都让位于"形式化不合格,需重做");清空 `proof`;**撤下**归档证明 `Verified/Lean/<id>.lean`(工作文件 `Formal/<id>.lean` 保留,代码不丢);`note` 写入形式化记录与 `Formal/TODO.md`(按 id 去重、就地刷新);对象卡锚点刷新为 `已尝试未通过`;公告到活动日志与 `Logs/形式化.md`。
562
+ - **撤回归档证明不是"尽力删除"**(`withdrawArchivedProof`):`fs` 服务没有 unlink,`subprocess` 又是可选服务,删除命令也可能静默失败(桩宿主 / 权限 / 宿主不提供 shell),而 `Verified/Lean/<id>.lean` 正是所有人找"这条结论的证明"的路径。因此删除后**必须回读确认**;确认不了就用撤回声明**就地覆盖**该文件(`-- 已撤回(<时间>):该形式化被认定与命题原文不一致。` + 指向保留的工作文件),使它不可能再被读成一份通过的证明;两种兜底都失败时公告里明确写出"**未能撤回**,请手动删除"。公告如实说明发生的是**哪一种**(删除 / 就地覆盖 / 失败)。
563
+ - **`require` 档**:降级后 `formalGateOk` 为假,本次裁定**不定论**——不改对象的既有权重/概率字段、不写 `Verified/` 卡片,对象进入「形式化待办」;修正形式化并重新跑通(`vibe_math_lean_archive kind='proof'`)后再投票。**`encourage` 档没有门禁**:框架仍然撤回证明并写入待办,但注入文本与公告都**不得**声称框架搁置了裁定(那是它无法强制的行为,契约 §4.1 第 3 条)——只有表决者自己的弃权值能阻止一致性结论。
564
+ - **唯一可以投 0 的情形**:表决者**独立于这份 Lean 代码**也能确定命题为假,且 `Reason` 写清独立理由。
565
+
566
+ **提示词硬要求**(契约 §6 顶部的五条,v3 全部落地,并由套件对**每条捕获的提示词**做正则扫描,而不是只看一两处):
567
+
568
+ 1. **工具名一律全称**:注入文本(含工具自己返回的 `hint` 字段)里只允许 `vibe_math_lean_run` / `vibe_math_lean_archive` / `vibe_math_lean_lib`。`lean_run` / `lean_archive` / `lean_lib` 这类缩写**不是注册名**,代理照抄会去调用一个不存在的工具;套件用 `/(^|[^a-z_])lean_(run|archive|lib)/` 扫描全部语料,命中即红。
569
+ 2. **回执字段名必须与真实契约一致**:v3 的表决值字段是 **`Result`**(v4/v5 才是 `verdict`)。写错字段名 = 那一票被静默丢弃(退回默认 0.5)。忠实性分支因此写的是 `一致 → Result = 1`;套件断言每条表决提示词含 `Result` 且不含 `verdict`。
570
+ 3. **归档可复用定义/引理前必须先跑通**:`vibe_math_lean_archive` 支持 `run:true`,或先 `vibe_math_lean_run`;**跑不通的代码不得进入 `Formal/Lib` / `Formal/Proved`**,否则"可复用库"会被不编译的定义污染。验证提示词与「顺手形式化」一行都写明了这条纪律(`归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。`)。
571
+ 4. **工具链缺失时的出路必须写出来**:`LEAN_NOT_FOUND` 时把代码写下来归档,并在回执的 `note` 里写明"宿主无 Lean 工具链"——这算**显式阻塞原因**,`require` 档可以据此放行,代理不会因为装不了 Lean 而卡死。v3 的注入文本**同时点名 `NO_SUBPROCESS`**(宿主不提供 subprocess 服务、因此根本无法执行 Lean 的另一种失败形态),否则代理不认识这个码,会把它当成未知故障反复重试而不是记录阻塞。`vibe_math_lean_run` 返回的失败 `hint` 也按错误码区分:工具链缺失 / 无 subprocess / spawn 失败时给出"没有编译器输出可修,写下代码并归档 + 记录阻塞原因",只有真正的编译失败才说"按上面的编译器输出修复后重跑"。
572
+ 5. **忠实性缺陷不得用 0 表达**:只有独立于 Lean 代码也能确定命题为假时才投 0(§4.1 第 4 条)。套件断言注入文本**不含** `偏离 → 0` 这类"偏差即假"的指令,且含"不要投 0 / 记为形式化不合格 / 走待办"的要求。
573
+
556
574
  ---
557
575
 
558
576
  ## 12. 提示词与知识上下文(关键)
@@ -65,7 +65,7 @@ export function apply(ctx) {
65
65
  // is persisted through v4's OWN durable State/*.json mechanism (State/formal.json) and must
66
66
  // survive `resume` — otherwise a require-mode object would lose the very record that decides
67
67
  // whether its verdict may take effect.
68
- let formal = {}, formalTodos = []
68
+ let formal = {}, formalTodos = [], formalPersisted = false
69
69
  let meetingState = null, verifyState = null, pendingVerify = [], pendingMeeting = null // pendingVerify: FIFO queue (several residents may independently propose different objects before any verify runs — a single slot silently DROPPED all but the last proposal)
70
70
  let busy = new Set(), wakeKind = new Map(), currentResident = ''
71
71
  let finalizeLock = null // 'meeting'|'verify' while a consensus finalize is running (reentry guard)
@@ -146,6 +146,17 @@ export function apply(ctx) {
146
146
  if(isWindows()) return 'New-Item -Force -ItemType Directory -Path '+paths.map(psQuote).join(',')+' | Out-Null'
147
147
  return 'mkdir -p '+paths.map(shQuote).join(' ')
148
148
  }
149
+ /**
150
+ * Delete command for the same two interpreters. DSH's `fs` service has no unlink/remove at all
151
+ * (dsh-fs FileSystem: resolve/stat/readText/writeText/editText/listDir), so the ONE place this
152
+ * preset must remove a file — withdrawing a retracted proof on `defect` (spec §4.1) — goes
153
+ * through the platform shell helper the preset already uses for mkdir. `-ErrorAction
154
+ * SilentlyContinue` / `-f` keep it idempotent: deleting an already-absent file is a success.
155
+ */
156
+ function rmCmd(paths){
157
+ if(isWindows()) return 'Remove-Item -Force -ErrorAction SilentlyContinue -LiteralPath '+paths.map(psQuote).join(',')
158
+ return 'rm -f '+paths.map(shQuote).join(' ')
159
+ }
149
160
  async function runShell(script,cwd){
150
161
  const subprocess=subprocessOf(); if(subprocess===undefined) return {ok:false,error:'no-subprocess'}
151
162
  try {
@@ -283,7 +294,13 @@ export function apply(ctx) {
283
294
  await saveFormal()
284
295
  return true
285
296
  }
286
- function saveFormal(){ return writeJson('State/formal.json',{records:formal,todo:formalTodos}) }
297
+ // Once formal state has been persisted (or restored from disk) the file must KEEP being
298
+ // written even after the mode is switched back to `off` — otherwise `start()`'s clean slate
299
+ // would never reach disk and a later `resume` would restore a stale `passed` record for a
300
+ // reused object id (the exact hazard that reset exists to prevent). A session that never
301
+ // touched the feature writes NO State/formal.json at all: `off` is a TRUE no-op (v3 guards
302
+ // its own State/formal.json the same way).
303
+ function saveFormal(){ formalPersisted=true; return writeJson('State/formal.json',{records:formal,todo:formalTodos}) }
287
304
  // `passed` requires a GREEN RUN, not merely an archived file: a proof file that has never
288
305
  // been executed proves nothing, so a hand-written file cannot buy its way past the gate.
289
306
  const formalGateOk=(rec)=>!!rec&&(rec.status==='passed'||rec.status==='blocked')
@@ -293,6 +310,9 @@ export function apply(ctx) {
293
310
  const r=formalOf(target)
294
311
  if(r.status==='passed') return 'Lean 通过('+(r.proof||r.file||'')+')'
295
312
  if(r.status==='blocked') return '阻塞('+(r.note||'未说明')+')'
313
+ // A retracted proof must not read as a plain "tried and failed": the card has to say WHY the
314
+ // archived proof disappeared (spec §4.1 — a fidelity defect is not a refutation).
315
+ if(r.status==='attempted'&&r.decision==='defect') return '忠实性缺陷('+(r.note||'未说明')+',待重做)'
296
316
  if(r.status==='attempted') return '已尝试未通过'
297
317
  return '未尝试'
298
318
  }
@@ -360,16 +380,44 @@ export function apply(ctx) {
360
380
  try {
361
381
  handle=sub.spawn({ argv, cwd:frameworkRoot(), stdio:{stdin:'ignore',stdout:{maxBytes:64*1024},stderr:{maxBytes:64*1024}}, graceMs:cap })
362
382
  } catch(e){ return {ok:false,code:'LEAN_SPAWN_FAILED',message:String((e&&e.message)||e),file:rel,ms:now()-started} }
383
+ // A timeout must be ACTIVELY enforced. `graceMs` is only the host's own grace window; the
384
+ // contract (§7) additionally requires `handle.terminate()` on timeout, and a host that
385
+ // ignores/exceeds graceMs would otherwise leave the Lean process running while the framework
386
+ // reports LEAN_TIMEOUT. Race `done` against a cap-ms timer that terminates the handle and
387
+ // resolves a synthetic outcome. Note: the plugin runtime has no global setTimeout — the
388
+ // `timer` service (ctx.timeout) is the only timer, and it returns the disposer we clear
389
+ // below when `done` wins the race.
390
+ let killedByUs=false, timerDispose=null
363
391
  let outcome
364
- try { outcome=await handle.done }
365
- catch(e){ return {ok:false,code:'LEAN_RUN_FAILED',message:String((e&&e.message)||e),file:rel,ms:now()-started} }
392
+ try {
393
+ outcome=await Promise.race([
394
+ handle.done,
395
+ new Promise(function(resolve){
396
+ if(typeof ctx.timeout!=='function') return // no timer service: the host's graceMs is all we have
397
+ timerDispose=ctx.timeout(function(){
398
+ killedByUs=true
399
+ try { if(typeof handle.terminate==='function') handle.terminate() } catch(e){ /* best effort */ }
400
+ resolve({exitCode:null,signal:'SIGTERM'})
401
+ }, cap)
402
+ }),
403
+ ])
404
+ } catch(e){
405
+ if(timerDispose){ try { timerDispose() } catch(_e){} }
406
+ return {ok:false,code:'LEAN_RUN_FAILED',message:String((e&&e.message)||e),file:rel,ms:now()-started}
407
+ }
408
+ if(timerDispose){ try { timerDispose() } catch(e){ /* already fired */ } }
366
409
  let out='',err=''
367
410
  try { if(handle.collected&&handle.collected.stdout) out=handle.collected.stdout.readFrom(0).text } catch(e){ /* best effort */ }
368
411
  try { if(handle.collected&&handle.collected.stderr) err=handle.collected.stderr.readFrom(0).text } catch(e){ /* best effort */ }
369
412
  const exitCode=outcome?outcome.exitCode:null
370
413
  const ms=now()-started
371
414
  const ok=exitCode===0
372
- const timedOut=!ok&&ms>=cap
415
+ // Two ways a timeout is observed: OUR timer won (we terminated the handle), or the HOST's own
416
+ // grace window killed the process first and its `done` beat our timer to the race — the latter
417
+ // is recognised by "non-zero exit after at least cap ms", which is what the spec's
418
+ // LEAN_TIMEOUT row describes. Keeping both means a real hang is never reported as a plain
419
+ // compile failure just because the host's kill resolved first.
420
+ const timedOut=killedByUs||(!ok&&ms>=cap)
373
421
  return {
374
422
  ok, exitCode, signal:(outcome&&outcome.signal)||null, ms,
375
423
  command:argv.join(' '), file:rel,
@@ -379,11 +427,17 @@ export function apply(ctx) {
379
427
  }
380
428
  // Record one run against an object. `passed`/`blocked` are NEVER downgraded by a later red
381
429
  // run (only an explicit re-archive decides those); everything else becomes `attempted`.
430
+ // The comment used to describe a transition the body did not implement: `status:'attempted'`
431
+ // was hardcoded, so ANY `lean_run {target}` on a passed object silently stripped `passed`
432
+ // (and kept the now-stale `proof` pointer). That is not a cosmetic slip: it removes the
433
+ // fidelity branch from the next voting prompt AND, in `require` mode, re-closes the gate on an
434
+ // object that already has a green archived proof. docs/formal-verification.md §4 only maps
435
+ // `none`/`attempted` → `attempted`, and v2/v5 preserve passed/blocked here.
382
436
  async function formalSetRun(target,run){
383
437
  const key=formalKey(target); if(!key) return
384
438
  const prev=formalOf(key)
385
439
  await putFormal(key,Object.assign({},prev,{
386
- status:'attempted',
440
+ status:prev.status==='passed'?'passed':(prev.status==='blocked'?'blocked':'attempted'),
387
441
  file:run.file||prev.file||'',
388
442
  decision:prev.decision||'used',
389
443
  run:{at:now(),ok:!!run.ok,exitCode:run.exitCode===undefined?null:run.exitCode,ms:run.ms||0,stdoutTail:tail(run.stdout,800),stderrTail:tail(run.stderr,800)},
@@ -405,9 +459,21 @@ export function apply(ctx) {
405
459
  if(rec.status==='passed'){
406
460
  L.push(' · 该对象已有**通过的 Lean 形式化证明**('+(rec.proof||rec.file||'')+',最近运行 exit 0)。')
407
461
  L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')
408
- L.push(' 定义 / 对象 / 条件 / 假设 / 结论是否与命题原文**完全一致**(有偏差就指出偏差),')
409
- L.push(' 并据此给出 verdict。')
410
- L.push(' ▸ 因此请把 verdict 用在**忠实性**上:一致 → 1;发现任何偏离 → 0(或按不确定度给中间值并说明)。')
462
+ L.push(' 定义 / 对象 / 条件 / 假设 / 结论是否与命题原文**完全一致**。')
463
+ L.push(' ▸ 一致 → verdict = 1。')
464
+ L.push(' ▸ **发现任何偏差,不要投 0**:偏差只说明**形式化不合格**,不代表命题为假。此时请:')
465
+ L.push(" ① verdict 给一个严格介于 0 与 1 之间的值(记为弃权),并在 reason 里写清偏差;")
466
+ L.push(" ② 用回执 formal:{decision:'defect', note:'<具体偏差>'} 记录它。框架会撤回这条证明的")
467
+ // Only `require` actually GATES the conclusion. In `encourage` the framework still
468
+ // withdraws the proof (and records the defect + TODO), but it CANNOT hold the ballot —
469
+ // promising "本次裁定不定论" there would promise behaviour the framework does not have
470
+ // (docs §4.1-3/§6.1); the voter's own abstention is what keeps the ballot from concluding.
471
+ L.push(' 「已通过」状态(降级为 attempted、删除归档证明、写入形式化待办)'
472
+ +(formalMode()==='require'
473
+ ?',本次裁定**不定论**;'
474
+ :';本档没有门禁:请务必给弃权值,以保证本轮无法得出一致结论;'))
475
+ L.push(' 修正形式化并重新跑通后再投票。')
476
+ L.push(' ▸ 只有当你**独立于这份 Lean 代码**也能确定命题为假时,才投 0,并在 reason 里写清独立理由。')
411
477
  } else if(rec.status==='blocked'){
412
478
  L.push(' · 该对象已被记录为**形式化阻塞**:'+(rec.note||'未说明')+'。')
413
479
  L.push(' 请复核这个判断是否成立;若你认为其实可以形式化,请指出来并动手做。')
@@ -415,18 +481,15 @@ export function apply(ctx) {
415
481
  } else {
416
482
  L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
417
483
  L.push(' · 工具:vibe_v4_lean_run(执行)· vibe_v4_lean_archive(归档)· vibe_v4_lean_lib(查已有可复用库)')
418
- L.push(' · 工作目录:Formal/(相对项目根 '+frameworkRoot().replace(/\\/g,'/')+'/);')
419
- L.push(' 可复用定义放 '+formalLibRoot().replace(/\\/g,'/')+'/,已证引理放 '+formalProvedRoot().replace(/\\/g,'/')+'/;')
420
- L.push(' 写之前先 vibe_v4_lean_lib 查重。')
421
- L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与')
422
- L.push(' 命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。')
484
+ L.push(' · 工作目录:Formal/(相对项目根);可复用定义放 '+formalLibRoot().replace(/\\/g,'/')+'/,已证引理放 '+formalProvedRoot().replace(/\\/g,'/')+'/;写之前先 vibe_v4_lean_lib 查重。')
485
+ L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。')
423
486
  if(formalMode()==='require'){
424
- L.push(' · **本模式要求**:**必须产出 Lean 形式化**,或**必须**给出显式的阻塞原因')
425
- L.push(" (vibe_v4_lean_archive kind='blocked' note=… 或回执 formal.note)。若两者都没有,")
426
- L.push(' 本次裁定不会生效,会被记为未定论(原因 formal-required)并进入「形式化待办」。')
487
+ L.push(" · **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因(vibe_v4_lean_archive kind='blocked' note=… 或回执 formal.note)。若两者都没有,本次裁定不会生效,会被记为未定论(原因 formal-required)并进入「形式化待办」。")
427
488
  } else {
428
- L.push(' · 若判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断。')
489
+ L.push(" · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision='blocked' 时必须写明 note)。")
429
490
  }
491
+ L.push(' · 归档可复用定义/引理前先跑通(vibe_v4_lean_archive run=true 或先 vibe_v4_lean_run);跑不通不要入库。')
492
+ L.push(' · 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并在回执的 note 里写明"宿主无 Lean 工具链"——这两种都算显式阻塞原因,定论门禁可以据此放行。')
430
493
  if(rec.status==='attempted'){
431
494
  L.push(' ▸ 该对象已有形式化尝试但尚未通过(最近一次 '+(rec.run?(rec.run.ok?'通过':'未通过'):'无运行记录')+')。')
432
495
  L.push(" 请修复后重跑(vibe_v4_lean_run),跑通后用 kind='proof' 归档。")
@@ -446,10 +509,11 @@ export function apply(ctx) {
446
509
  +(formalMode()==='require'
447
510
  ? '本模式下,任何要定论为真/假的对象都必须先有 Lean 通过或显式阻塞记录。'
448
511
  : '这会让后续的验证与证明省掉大量重复工作。')
512
+ +'归档前先跑通(vibe_v4_lean_run 或 run=true);跑不通的定义不要进可复用库。'
449
513
  }
450
514
  /** The `formal` object every non-off prompt documents in its JSON reply contract. */
451
515
  function formalReplyField(target){
452
- return '{"formal":{"target":"'+String(target||'p-x')+'","decision":"used|blocked","file":"Formal/'+String(target||'p-x')+'.lean","note":"难度判断/阻塞原因"}}'
516
+ return '{"formal":{"target":"'+String(target||'p-x')+'","decision":"used|blocked|defect","file":"Formal/'+String(target||'p-x')+'.lean","note":"难度判断/阻塞原因/具体偏差"}}'
453
517
  }
454
518
 
455
519
  // ---- the three indexes (framework-maintained) ---------------------------
@@ -536,11 +600,14 @@ export function apply(ctx) {
536
600
  await writeFormalIndex()
537
601
  }
538
602
  } catch(e){ /* the index is best-effort; a run result must always come back */ }
539
- logActivity('formal',(memberId||'host')+' lean_run '+(run.file||String(args.file||''))+' → '+(run.ok?'通过':(run.code||'未通过')))
603
+ // The activity log is part of the injected-text surface (a host reads it out of
604
+ // vibe_v4_report, and residents may be quoted it): use the REGISTERED tool name, never the
605
+ // bare `lean_run` abbreviation (docs §6 hard requirement 1).
606
+ logActivity('formal',(memberId||'host')+' vibe_v4_lean_run '+(run.file||String(args.file||''))+' → '+(run.ok?'通过':(run.code||'未通过')))
540
607
  return Object.assign({ok:!!run.ok},run,{
541
608
  mode:formalMode(),
542
609
  hint: run.ok
543
- ? "通过。若是某个对象的证明,请用 vibe_v4_lean_archive kind='proof' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind='def'/'lemma' 归档到全局库。"
610
+ ? "通过。若是某个对象的证明,请用 vibe_v4_lean_archive kind='proof' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind='def'/'lemma' 归档到全局库——归档前先跑通(run=true 或先 vibe_v4_lean_run):跑不通的定义不要进可复用库。"
544
611
  : '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 vibe_v4_lean_archive kind=\'blocked\' 记录原因。',
545
612
  })
546
613
  }
@@ -623,23 +690,118 @@ export function apply(ctx) {
623
690
  }
624
691
  return {ok:false,code:'V4_INVALID_ARGUMENT',message:"kind must be 'def' | 'lemma' | 'proof' | 'blocked'"}
625
692
  }
693
+ /**
694
+ * Withdraw an archived proof (spec §4.1). The RECORD is authoritative, but the FILE must not
695
+ * survive at the exact path everyone looks for "this object's proof": a host whose shell
696
+ * cannot delete (no `subprocess`, a stub shell that exits 0 without removing anything, a
697
+ * permission quirk) would otherwise leave the retracted proof readable as the object's proof
698
+ * while the record already says `attempted`. So the withdrawal is: delete → CONFIRM through
699
+ * the fs service that it is really gone → if it still exists, OVERWRITE it with an explicit
700
+ * withdrawal notice. Returns WHICH path it took ({ok,how:'deleted'|'overwritten'|'failed'})
701
+ * so the caller/activity log can say so out loud instead of reporting a best-effort delete as
702
+ * done. Every path goes through `leanAbsPath`, so this can only ever touch a `.lean` file
703
+ * inside the VibeMath root (a hand-edited State/formal.json must not become an arbitrary-file
704
+ * delete).
705
+ */
706
+ async function withdrawProof(rel){
707
+ const raw=String(rel==null?'':rel)
708
+ if(!raw) return {ok:false,how:'failed',skipped:true}
709
+ const abs=leanAbsPath(raw)
710
+ if(abs===null||!/\.lean$/.test(abs)){
711
+ logActivity('formal','拒绝删除越界的归档证明路径:'+raw)
712
+ return {ok:false,how:'failed',skipped:true}
713
+ }
714
+ const sub=subprocessOf()
715
+ if(sub!==undefined&&typeof sub.spawn==='function'){
716
+ const r=await runShell(rmCmd([abs]),vibeRoot())
717
+ // Exit 0 is NOT proof of deletion: confirm through the fs service before believing it.
718
+ if(r&&r.ok&&await readTextAbs(abs)===undefined) return {ok:true,how:'deleted',abs}
719
+ }
720
+ // Fallback: make the file impossible to read as this object's proof any more. The withdrawn
721
+ // code itself is NOT lost — it stays in the working file Formal/<id>.lean.
722
+ const notice='-- 已撤回('+fmtTime()+'):该形式化被认定与命题原文不一致。\n'
723
+ +'-- 原代码保留在工作文件 Formal/'+String(raw).split('/').pop()+';修正并重新跑通后重新归档。\n'
724
+ if(await writeTextAbs(abs,notice)) return {ok:true,how:'overwritten',abs}
725
+ return {ok:false,how:'failed',abs}
726
+ }
727
+ /**
728
+ * §4.1 `defect`: a voter checked the Lean code against the proposition and found a FIDELITY
729
+ * defect (written too narrow/wide, wrong object, missing hypothesis). That is a statement about
730
+ * the FORMALIZATION, not about the proposition, so it must never be absorbed as "the
731
+ * proposition is false". The record is therefore ALWAYS downgraded to `attempted` (even from
732
+ * `blocked`), `proof` is cleared, `Verified/Lean/<id>.lean` is withdrawn, the deviation goes
733
+ * into the record + Formal/Index.md + Formal/TODO.md, and the group is told in the activity
734
+ * log. The working file `Formal/<id>.lean` is deliberately KEPT — the code is not lost, only
735
+ * its "passed" claim. In `require` mode the downgrade also makes `formalGateOk` false, so the
736
+ * verdict defers through the existing `deferForFormal` path (no Verified card, TODO entry).
737
+ */
738
+ async function recordFormalDefect(rId,key,note){
739
+ const prev=formalOf(key)
740
+ const proofRel=String(prev.proof||'')||('Verified/Lean/'+key+'.lean')
741
+ const rec=Object.assign({},prev,{status:'attempted',proof:'',decision:'defect',note,updatedAt:now()})
742
+ const why='formal-defect:形式化与命题原文不一致——'+String(note).slice(0,160)
743
+ const todo=formalTodo().slice()
744
+ const i=todo.findIndex(t=>t&&t.id===key)
745
+ // The defect reason must survive the later `deferForFormal`, which keeps an EXISTING entry
746
+ // rather than overwriting it — so the TODO file explains "formalization不合格", not merely
747
+ // "formal-required". An already-deferred object keeps its original 真/假 tally here.
748
+ if(i>=0) todo[i]=Object.assign({},todo[i],{why,at:now()})
749
+ else todo.push({id:key,at:now(),why,verdict:null})
750
+ // Durable write FIRST (record + todo together), then withdraw the file: a crash in between
751
+ // leaves an orphaned file with a truthful record, never a record still claiming `passed`.
752
+ await putFormal(key,rec,todo)
753
+ const del=await withdrawProof(proofRel)
754
+ const how=del&&del.how==='deleted'?'删除归档证明 '+proofRel
755
+ :del&&del.how==='overwritten'?'覆盖归档证明 '+proofRel+'(宿主无法删除,已写入撤回说明,原证明内容不再可读)'
756
+ :'⚠ 归档证明 '+proofRel+' 未能撤回(删除与覆盖均失败)——它仍停留在"该对象的证明"的位置,请不要把它当作该对象的证明'
757
+ try { await writeFormalTodo(); await writeFormalIndex() } catch(e){ /* best-effort */ }
758
+ // The activity log is agent/host-readable, so the same §4.1-3 rule applies here as in the
759
+ // prompt: only `require` actually GATES the verdict, so only there may this say 不定论.
760
+ logActivity('formal',(rId||'host')+' 报告 '+key+' 存在**忠实性缺陷**(formal.decision=defect):'+note
761
+ +' ——已撤回「已通过」状态(→ attempted)、'+how
762
+ +'、写入 Formal/TODO.md;'+(formalMode()==='require'
763
+ ?'require 门禁使本次裁定**不定论**,修正形式化并重新跑通后再投票'
764
+ :'本档没有门禁:本轮能否定论取决于表决者是否给出弃权值,修正形式化并重新跑通后再投票'))
765
+ return {ok:true,target:key,status:'attempted',proof:'',decision:'defect',removedProof:!!(del&&del.ok),withdrawal:del?del.how:'failed'}
766
+ }
626
767
  /**
627
768
  * The per-round `formal` reply channel. This is the path that fires IN PRACTICE: a resident
628
769
  * that never calls a Lean tool still has to state its difficulty judgement. Every failure is
629
- * swallowed into the activity log — an end handler must never throw into the scheduler.
770
+ * swallowed into the activity log — an end handler must never throw into the scheduler — but
771
+ * each rejection still returns the preset's typed error so the caller/audit can see WHY a
772
+ * judgement was dropped instead of silently losing it.
630
773
  */
631
774
  async function applyFormalReply(rId,formalReply){
632
775
  try {
776
+ // `off` is a TRUE no-op: the reply contract only offers the `formal` field in non-off modes,
777
+ // so a stray / stale / hallucinated one must NOT create Lean state (v2/v3 guard here too;
778
+ // without this, off mode could still be made to write Formal/ records, TODO entries and
779
+ // announcements). The TOOLS stay usable in off mode on purpose — a tool call is deliberate.
780
+ if(!formalOn()) return {ok:false,ignored:true}
633
781
  const key=formalKey(String(formalReply.target||''))
634
- if(!key) return
782
+ if(!key){
783
+ logActivity('formal',(rId||'host')+' 的 formal 回执缺少 target(对象 id)——本次未记录(V4_INVALID_ARGUMENT)')
784
+ return {ok:false,code:'V4_INVALID_ARGUMENT',message:'formal.target(对象 id)是必填的'}
785
+ }
635
786
  const decision=String(formalReply.decision||'').trim()
636
787
  if(decision==='blocked'){
637
788
  const note=String(formalReply.note||'').trim()
638
789
  if(!note){
639
- logActivity('formal',(rId||'host')+" 的 formal.decision='blocked' 缺少 note(难度判断/阻塞原因)——本次未记录")
640
- return
790
+ logActivity('formal',(rId||'host')+" 的 formal.decision='blocked' 缺少 note(难度判断/阻塞原因)——本次未记录(V4_INVALID_ARGUMENT)")
791
+ return {ok:false,code:'V4_INVALID_ARGUMENT',message:"formal.decision='blocked' 必须写明 note(难度判断/阻塞原因)"}
641
792
  }
642
793
  await leanArchive(rId,{kind:'blocked',target:key,note})
794
+ return {ok:true,target:key,decision:'blocked',status:'blocked'}
795
+ } else if(decision==='defect'){
796
+ // §4.1: a fidelity defect is NOT a refutation. Accepting it as "0 / false" would make the
797
+ // framework fabricate a negative conclusion out of a broken formalization, so the ONLY
798
+ // thing this branch may do is RETRACT the passing proof and defer the verdict.
799
+ const note=String(formalReply.note||'').trim()
800
+ if(!note){
801
+ logActivity('formal',(rId||'host')+" 的 formal.decision='defect' 缺少 note(具体偏差)——本次未记录(V4_INVALID_ARGUMENT)")
802
+ return {ok:false,code:'V4_INVALID_ARGUMENT',message:"formal.decision='defect' 必须写明 note(具体偏差:写窄了/写宽了/换了对象/漏了条件…)"}
803
+ }
804
+ return await recordFormalDefect(rId,key,note)
643
805
  } else if(decision==='used'){
644
806
  const file=String(formalReply.file||('Formal/'+key+'.lean'))
645
807
  const prev=formalOf(key)
@@ -648,10 +810,16 @@ export function apply(ctx) {
648
810
  file,decision:'used',note:String(formalReply.note||prev.note||''),updatedAt:now(),
649
811
  }))
650
812
  try { await writeFormalIndex() } catch(e){ /* best-effort */ }
813
+ return {ok:true,target:key,decision:'used'}
651
814
  } else if(decision){
652
- logActivity('formal',(rId||'host')+" 的 formal.decision 只能是 'used' 或 'blocked'(收到 "+decision+")")
815
+ logActivity('formal',(rId||'host')+" 的 formal.decision 只能是 'used'、'blocked' 或 'defect'(收到 "+decision+")——本次未记录(V4_INVALID_ARGUMENT)")
816
+ return {ok:false,code:'V4_INVALID_ARGUMENT',message:"formal.decision 只能是 'used' | 'blocked' | 'defect'(收到 "+decision+")"}
653
817
  }
654
- } catch(e){ logActivity('formal','formal 回执处理失败:'+String((e&&e.message)||e)) }
818
+ return {ok:false,code:'V4_INVALID_ARGUMENT',message:'formal.decision 是必填的'}
819
+ } catch(e){
820
+ logActivity('formal','formal 回执处理失败:'+String((e&&e.message)||e))
821
+ return {ok:false,code:'V4_INVALID_ARGUMENT',message:String((e&&e.message)||e)}
822
+ }
655
823
  }
656
824
  /** The {mode, on, objects, todo} view the host tools report (state-storage transparency). */
657
825
  function formalView(){
@@ -672,7 +840,9 @@ export function apply(ctx) {
672
840
  await writeJson('State/mailboxes.json', Object.fromEntries(mailboxes))
673
841
  await writeJson('State/taskboard.json', taskboard)
674
842
  await writeJson('State/decisions.json', decisions)
675
- await writeJson('State/formal.json', {records:formal,todo:formalTodos})
843
+ // `off` mode must not CREATE Lean state: a run that never used the feature leaves no
844
+ // State/formal.json behind. Seeded/live records (or a mode that is on) still persist.
845
+ if(formalOn()||formalPersisted||Object.keys(formal).length||formalTodos.length) await writeJson('State/formal.json', {records:formal,todo:formalTodos})
676
846
  await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,lastProgressAt,activityLog,processEpoch,artifactCount})
677
847
  }
678
848
  async function loadAll(){
@@ -687,6 +857,7 @@ export function apply(ctx) {
687
857
  if(fm&&typeof fm==='object'){
688
858
  formal=(fm.records&&typeof fm.records==='object')?fm.records:{}
689
859
  formalTodos=Array.isArray(fm.todo)?fm.todo:[]
860
+ if(Object.keys(formal).length||formalTodos.length) formalPersisted=true
690
861
  }
691
862
  }
692
863
 
@@ -782,7 +953,7 @@ export function apply(ctx) {
782
953
  +(formalOn()?('\n'+formalWorkLine()+'\n'):'')
783
954
  +'Reply with ONLY a JSON object:\n'
784
955
  +'{"summary":"<what you did / decided this round, 1-3 sentences>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<task id|null>","task_done":"<task id|null>","contextPct":40'
785
- +(formalOn()?(',"formal":{"target":"<对象 id>","decision":"used|blocked","file":"Formal/<对象 id>.lean","note":"难度判断/阻塞原因"}'):'')
956
+ +(formalOn()?(',"formal":{"target":"<对象 id>","decision":"used|blocked|defect","file":"Formal/<对象 id>.lean","note":"难度判断/阻塞原因/具体偏差"}'):'')
786
957
  +'}'
787
958
  }
788
959
  function meetingPrompt(r, st){
@@ -1290,7 +1461,7 @@ export function apply(ctx) {
1290
1461
  +(formalOn()?(formalWorkLine()+'\n'):'')
1291
1462
  +'Reply with ONLY a JSON object:\n'
1292
1463
  +'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<id|null>","contextPct":40'
1293
- +(formalOn()?(',"formal":{"target":"<对象 id>","decision":"used|blocked","file":"Formal/<对象 id>.lean","note":"难度判断/阻塞原因"}'):'')
1464
+ +(formalOn()?(',"formal":{"target":"<对象 id>","decision":"used|blocked|defect","file":"Formal/<对象 id>.lean","note":"难度判断/阻塞原因/具体偏差"}'):'')
1294
1465
  +'}'
1295
1466
  }
1296
1467
  function clearHeartbeat(){ if(heartbeatDisposer!==null){ try{ heartbeatDisposer() }catch(e){} heartbeatDisposer=null } }
@@ -121,7 +121,7 @@ VibeMath/Projects/<project>/
121
121
  | **会议** | `vibe_v4_meeting(agenda)` → 向全体发会议 prompt → 收齐发言 → 写 `Shared/meetings/<id>.md` → 广播结论 | 会议用于分工/方向/任务分配/提出验证/表决"是否已解决" |
122
122
  | **任务板** | 常驻在会议/留言提议任务 → 框架记 `Shared/taskboard.md`;认领后被唤醒 | 框架只搬运,不决定谁做什么 |
123
123
  | **产物沉淀** | `vibe_v4_publish_progress` / `record_proposition` / `record_method` / `record_subproblem` | **必填**:价值程度 / 动机用途计划 / 自身概率估计(框架校验,缺则提示) |
124
- | **共识验证** | `vibe_v4_propose_verify(targetId)` → 常驻们同意后开辩论 | 独立初评→公开辩论→**全票真/假才入 Verified/**;否则留库附概率 |
124
+ | **共识验证** | 回执字段 `propose_verify: "<id>"`(常规/心跳轮与会议发言都会读取;v4 **没有** `vibe_v4_propose_verify` 这个工具)→ 框架按 FIFO 排队 → 全体独立初评→公开辩论 | 独立初评→公开辩论→**全票真/假才入 Verified/**;否则留库附概率 |
125
125
  | **Lean 形式化验证** | `vibe_v4_lean_run` / `vibe_v4_lean_archive` / `vibe_v4_lean_lib` + `formalVerify` 三档开关 | 见 §31;`passed` 后表决对象从"推导是否正确"变成**忠实性**;`require` 档在收口点加门禁 |
126
126
  | **停止条件** | 会议中全体常驻对"原问题已解决"投票,**全部同意** → 停止唤醒 | 人工 `abort` 始终可用 |
127
127
  | **上下文/compact** | 监测每常驻上下文占比 ≥ 阈值 → 触发 DSH `/compact` | `compactThreshold` 默认 66,可调 |
@@ -793,12 +793,17 @@ else await closeVerify(vs,allTrue)
793
793
  | `LEAN_NOT_FOUND` | `resolveExecutable(leanCommand)` 失败(工具链未安装 → 优雅降级) |
794
794
  | `LEAN_SPAWN_FAILED` | `spawn` 本身抛错 |
795
795
  | `LEAN_RUN_FAILED` | `await handle.done` 抛错 |
796
- | `LEAN_TIMEOUT` | 非零退出且耗时达到 `graceMs`(超时上限) |
796
+ | `LEAN_TIMEOUT` | 到 `graceMs` 还没跑完:框架自己起一个 `cap` 计时器,到点**主动调用 `handle.terminate()`** 并返回合成结果(契约 §7;不再只依赖宿主的 `graceMs`)。宿主先于本计时器杀进程时,用"非零退出且耗时达到 `cap`"兜底 |
797
797
  | `LEAN_FAILED` | 其它非零退出(编译器输出原样返回 ~4KB 尾巴) |
798
798
  | `V4_INVALID_ARGUMENT` | 缺 `file`/`kind`/`name`/`target`/`note`;非 `.lean`;越界路径;未知 `kind` |
799
799
  | `V4_NOT_FOUND` | `.lean` 文件不存在(越界被拒 ≠ 不存在:两者码不同) |
800
800
  | `V4_WRITE_FAILED` | 目标路径写不进去 |
801
801
 
802
+ **超时是主动的**:`leanRunFile` 用 `Promise.race([handle.done, ctx.timeout(cap)])`,计时器一响就
803
+ `terminate()`;`done` 先赢则清掉该计时器(插件 runtime 没有全局 `setTimeout`,只能用 `timer` 服务)。
804
+ **唯一可删文件的路径**(§4.1 撤回归档证明):删除 → **用 `fs` 复核文件真的没了** → 还在就**覆盖为
805
+ 撤回说明**(`-- 已撤回(…)`),并把实际走的那条路(`deleted`/`overwritten`/`failed`)写进活动日志。
806
+
802
807
  **绝不抛异常到调度循环**:`leanRunFile` 把每一种失败都变成可读结果;`onResidentEnd` 里的
803
808
  `applyFormalReply` 整体包在 try/catch 内(回执处理失败只记 `activityLog`,不影响本轮结算)。
804
809
 
@@ -867,6 +872,53 @@ Lean 这一批改动同时做了一次**静态提示词面**审计(`audit-pers
867
872
  三处已补入 persona 的 `prefix` 与 `text` **两个块**,并由 `audit-persona-surface.test.mjs`
868
873
  (双向一致性 + 未文档化工具显式快照 + `prefix`/`text` 逐行一致)长期守护。
869
874
 
875
+ ### 31.13 忠实性缺陷 `defect`:形式化写错了 ≠ 命题为假(2.3.1)
876
+
877
+ 首版提示词写的是"一致 → 1;发现任何偏离 → 0"。这条指令有一个致命后果:Lean 代码写的不是命题
878
+ 想说的(写窄了/写宽了/换了对象/漏了条件)时,投票 0 的含义是"**该命题为假**"——框架会把
879
+ "形式化不合格"记成"命题被证伪",甚至在 v4 的**全组一致**规则下把命题写进 `Verified/` 标注**假**。
880
+ 用来求真的机制反而**伪造出一个错误的否定结论**。
881
+
882
+ 按契约 §4.1 改为独立的 `defect` 档:
883
+
884
+ - **表决者**:不得投 1 或 0;给一个严格介于 0 与 1 之间的值(记为弃权)并在 `reason` 里写清偏差,
885
+ 同时用回执 `formal:{decision:'defect', note:'<具体偏差>'}` 记录(`note` 必填,缺则 `V4_INVALID_ARGUMENT`)。
886
+ - **框架**:把该对象**降级为 `attempted`**(无论此前是 `passed` 还是 `blocked`)、清空 `proof`、
887
+ 撤回归档证明 `Verified/Lean/<id>.lean`(**删除 → 用 `fs` 复核真的没了 → 还在就覆盖为"已撤回"说明**,
888
+ 见 §31.14)、`note` 记入记录与 `Formal/TODO.md`、活动日志公告。
889
+ - **`require` 档**:降级后 `formalGateOk` 为假 → 本次裁定**不定论**(`deferForFormal`),
890
+ 对象进「形式化待办」,修正形式化并重新跑通后再投票。**`encourage` 档不得声称这一点**(没有门禁,
891
+ 框架撤不回表决):注入文本与活动日志都按档位分岔,说明"本档没有门禁:请务必给弃权值"。
892
+ - **只有独立于这份 Lean 代码也能确定命题为假**时才投 0,并在 `reason` 里写清独立理由。
893
+
894
+ 同批补齐的提示词硬要求(契约 §6):注入文本里的工具名**一律全称**(`vibe_v4_lean_*`;缩写
895
+ `lean_archive` 不是注册名,连活动日志行也不例外);归档可复用定义/引理**前先跑通**(`run=true` 或先 `vibe_v4_lean_run`),
896
+ 跑不通不许进 `Formal/Lib`/`Proved`;**工具链不可用**时把代码归档并在 `note`
897
+ 写明"宿主无 Lean 工具链"——**两个错误码都写出来**(`LEAN_NOT_FOUND`:没有 Lean 可执行文件;
898
+ `NO_SUBPROCESS`:宿主根本不提供 `subprocess` 服务),两者都算显式阻塞原因,门禁据此放行,
899
+ 不会因为装不了 Lean 而卡死。
900
+
901
+ **测试**:`formal-verify-v4.test.mjs` 增至 **262 条断言**(含 `defect` 端到端:降级 + 撤回归档证明 +
902
+ 进待办 + `require` 档不定论 + 缺 `note` 被拒;注入文本不得出现缩写工具名、不得出现"偏离 → 0"),
903
+ 并新增随包发布的人工复核语料 `prompt-corpus-v4/formal-verify-v4.{json,md}`
904
+ (覆盖 off 无 Lean 文字、encourage、**require**、passed 忠实性分支(require/encourage 两种措辞各一份)、
905
+ 平时工作轮、回执契约;工作区路径归一化为 `<WS>`、VibeMath 根为 `<VIBEMATH>`,可 diff、无本机路径)。
906
+
907
+ ### 31.14 深审计轮修复(本轮)
908
+
909
+ 逐条对着共同契约复审后修掉的问题(每条都有断言,见 §31.10 的测试表):
910
+
911
+ | 问题 | 症状 | 修复 |
912
+ |---|---|---|
913
+ | **`lean_run` 把 `passed` 降级** | `formalSetRun` 硬编码 `status:'attempted'`(注释却写着"`passed`/`blocked` 不被红运行降级",v2/v5 也是保留语义):运行中任何一次 `vibe_v4_lean_run {target}` 都会把已通过的记录打回 `attempted` 而 `proof` 指针留着——下一轮表决提示词因此丢掉**忠实性分支**,`require` 档还会对一份已跑绿的归档证明**重新关门禁** | `status: prev.status==='passed'?'passed':(prev.status==='blocked'?'blocked':'attempted')`;断言"红重跑不降级" |
914
+ | **`off` 模式仍写 `State/formal.json`** | `saveAll()` 无条件落这份状态文件:一个从未用过该功能的 run 也会留下 Lean 状态文件(v3 的守卫是 `formalOn()||有记录`) | 仅当 `formalOn()||formalPersisted||有记录/待办` 才写;`formalPersisted` 保证 `start()` 的清零**真的落盘**(否则 resume 会恢复陈旧 `passed`),纯 `off` 会话不产生该文件 |
915
+ | **超时不主动终止** | 只靠宿主的 `graceMs`,从不调 `handle.terminate()`(契约 §7 明写要求),宿主的宽限失效时 Lean 进程会残留 | `Promise.race([handle.done, ctx.timeout(cap)])`,到点 `terminate()` 并返回 `LEAN_TIMEOUT`;`done` 先赢则清掉计时器(插件 runtime 无全局 `setTimeout`,只能用 `timer` 服务) |
916
+ | **撤回归档证明只是"尽力删除"** | 删除失败(无 `subprocess`/shell 假成功/权限)时,被撤回的证明仍停在 `Verified/Lean/<id>.lean`——所有人找"这条结论的证明"的地方;记录却已降级 | 删除 → **用 `fs` 复核文件真的没了** → 还在就**覆盖为 `-- 已撤回(…)` 说明**;返回并播报实际走的那条路(`deleted`/`overwritten`/`failed`) |
917
+ | **`encourage` 档承诺"不定论"** | 忠实性分支与活动日志都无条件写"本次裁定**不定论**",但只有 `require` 有门禁——这是在承诺框架没有的行为(契约 §4.1-3/§6.1;v5 已分岔) | 按档位分岔:`require` 保留"不定论";`encourage` 改为"本档没有门禁:请务必给弃权值,以保证本轮无法得出一致结论" |
918
+ | **注入文本只写 `LEAN_NOT_FOUND`** | `NO_SUBPROCESS`(宿主没有 `subprocess` 服务)这条出路没写进提示词,代理会反复重试而不是记阻塞(契约 §6 硬要求 4;v3/v5 都写了两个码) | 同一句话里补上两个码,并断言该句在插件里只出现一份(不按档位复制) |
919
+ | **活动日志里用缩写工具名** | `'… lean_run …'`(不是注册名;host 会把活动日志念给团队) | 改为 `vibe_v4_lean_run`;注入文本扫描面扩到 `State/session.json` 的 `activityLog` |
920
+ | **`实现方案.md` 写了不存在的工具** | §5 表格里的 `vibe_v4_propose_verify(targetId)` 从未注册(验证对象靠回执字段 `propose_verify`),照着文档找工具会扑空 | 表格改回真实机制(回执字段 → FIFO 排队) |
921
+
870
922
 
871
923
 
872
924
 
@@ -109,7 +109,9 @@
109
109
  reusable definitions VibeMath/Formal/Lib/; proved lemmas VibeMath/Formal/Proved/.
110
110
  - The toolchain knobs leanCommand / leanArgs / leanTimeoutMs are tunable as well
111
111
  (e.g. leanCommand='lake' with leanArgs=['env','lean']); a missing Lean binary is
112
- reported as LEAN_NOT_FOUND and still lets the code be written and archived.
112
+ reported as LEAN_NOT_FOUND, a host with no subprocess service as NO_SUBPROCESS
113
+ (on a timeout the process is terminated first), and in every one of those cases the
114
+ code can still be written down and archived.
113
115
  - vibe_v5_status / vibe_v5_report show the mode, per-object formal status and the
114
116
  formalization TODO. The framework never installs Lean and never judges fidelity.
115
117
 
@@ -201,7 +203,9 @@
201
203
  reusable definitions VibeMath/Formal/Lib/; proved lemmas VibeMath/Formal/Proved/.
202
204
  - The toolchain knobs leanCommand / leanArgs / leanTimeoutMs are tunable as well
203
205
  (e.g. leanCommand='lake' with leanArgs=['env','lean']); a missing Lean binary is
204
- reported as LEAN_NOT_FOUND and still lets the code be written and archived.
206
+ reported as LEAN_NOT_FOUND, a host with no subprocess service as NO_SUBPROCESS
207
+ (on a timeout the process is terminated first), and in every one of those cases the
208
+ code can still be written down and archived.
205
209
  - vibe_v5_status / vibe_v5_report show the mode, per-object formal status and the
206
210
  formalization TODO. The framework never installs Lean and never judges fidelity.
207
211