dsh-vibe-math 2.3.1 → 2.3.3

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 (39) hide show
  1. package/AUDIT-CHECKLIST.md +59 -0
  2. package/README.md +16 -6
  3. package/RELEASE-NOTES-2.3.2.md +145 -0
  4. package/RELEASE-NOTES-2.3.3.md +115 -0
  5. package/audit-formal-sensitivity.mjs +11 -2
  6. package/audit-prompt-invariants.mjs +459 -0
  7. package/audit-spec-traceability.mjs +193 -0
  8. package/docs/formal-verification.md +37 -13
  9. package/docs/generate_framework_diagram_v5.mjs +2 -1
  10. package/docs/test-timing.md +32 -10
  11. package/formal-verify-v2.test.mjs +288 -2
  12. package/formal-verify-v3.test.mjs +230 -4
  13. package/formal-verify-v4.test.mjs +205 -5
  14. package/formal-verify-v5.test.mjs +114 -4
  15. package/installer.js +3 -1
  16. package/package.json +6 -2
  17. package/prompt-corpus-persona/persona-corpus.json +2 -2
  18. package/prompt-corpus-persona/persona-corpus.md +6 -2
  19. package/prompt-corpus-v2/formal-verify-v2.json +164 -44
  20. package/prompt-corpus-v2/formal-verify-v2.md +1374 -44
  21. package/prompt-corpus-v3/formal-verify-v3.json +236 -128
  22. package/prompt-corpus-v3/formal-verify-v3.md +1509 -400
  23. package/prompt-corpus-v4/formal-verify-v4.json +8 -3
  24. package/prompt-corpus-v4/formal-verify-v4.md +38 -10
  25. package/prompt-corpus-v5/prompt-corpus-v5.json +175 -246
  26. package/prompt-corpus-v5/prompt-corpus-v5.md +341 -781
  27. package/prompt-v5-integrity.test.mjs +136 -22
  28. package/run-tests.mjs +30 -11
  29. package/vibe-math-v2/vibe-math-v2.js +163 -38
  30. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +58 -6
  31. package/vibe-math-v3/vibe-math-v3.js +95 -25
  32. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +8 -7
  33. package/vibe-math-v4/vibe-math-v4.js +103 -24
  34. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +37 -14
  35. package/vibe-math-v5/agent.cordis.yml +6 -2
  36. package/vibe-math-v5/vibe-math-v5.js +56 -10
  37. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +45 -14
  38. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +16 -2
  39. package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +6 -5
@@ -562,25 +562,45 @@ export function apply(ctx) {
562
562
  } catch (e) {
563
563
  return { ok: false, code: 'LEAN_SPAWN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
564
564
  }
565
+ // 超时必须有**主动**兜底:`graceMs` 只是宿主侧的宽限,契约 §7 明确要求"对超时调用
566
+ // handle.terminate()"。此前 v2 只依赖 graceMs,从不终止进程:一个卡住的 Lean 会继续占着
567
+ // 资源,而框架已经报了超时——它与自己的契约不一致。这里与 `handle.done` 竞速:计时器到点
568
+ // 时尽力 terminate(),并给出一份可读的超时结果;`done` 先到就清掉计时器(不留悬挂 timer)。
569
+ let timedOut = false
570
+ let timer = null
565
571
  let outcome
566
572
  try {
567
- outcome = await handle.done
573
+ outcome = await Promise.race([
574
+ handle.done,
575
+ new Promise(function (resolve) {
576
+ timer = setTimeout(function () {
577
+ timedOut = true
578
+ try { if (handle && typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* best effort:终止失败也要给出可读结果 */ }
579
+ resolve({ exitCode: null, signal: 'SIGTERM' })
580
+ }, cap)
581
+ }),
582
+ ])
568
583
  } catch (e) {
584
+ if (timer !== null) clearTimeout(timer)
569
585
  return { ok: false, code: 'LEAN_RUN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
570
586
  }
587
+ if (timer !== null) clearTimeout(timer)
571
588
  let out = '', err = ''
572
589
  try { if (handle.collected && handle.collected.stdout) out = handle.collected.stdout.readFrom(0).text } catch (e) { /* best effort */ }
573
590
  try { if (handle.collected && handle.collected.stderr) err = handle.collected.stderr.readFrom(0).text } catch (e) { /* best effort */ }
574
591
  const exitCode = outcome ? outcome.exitCode : null
575
592
  const ms = now() - started
576
593
  const ok = exitCode === 0
594
+ // 两种超时都算超时:我们自己的计时器到点(timedOut),或耗时已越过 cap(宿主的 graceMs
595
+ // 杀掉了进程时 done 会先返回一个非零 exitCode)。
596
+ const isTimeout = timedOut || ms >= cap
577
597
  // 输出截断到 ~4KB 再入库(避免把巨大的编译器输出写进状态)。
578
598
  return {
579
599
  ok: ok, exitCode: exitCode, signal: (outcome && outcome.signal) || null, ms: ms,
580
600
  command: argv.join(' '), file: rel,
581
601
  stdout: tailText(out, 4000), stderr: tailText(err, 4000),
582
- timedOut: ms >= cap,
583
- code: ok ? undefined : (ms >= cap ? 'LEAN_TIMEOUT' : 'LEAN_FAILED'),
602
+ timedOut: isTimeout,
603
+ code: ok ? undefined : (isTimeout ? 'LEAN_TIMEOUT' : 'LEAN_FAILED'),
584
604
  }
585
605
  }
586
606
  // 把一次运行结果写进对象的形式化记录(不提升 status,状态迁移见契约 §4)。
@@ -727,16 +747,18 @@ export function apply(ctx) {
727
747
  L.push(' · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision=\'blocked\' 时必须写明 note)。')
728
748
  }
729
749
  L.push(' · 归档可复用定义/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run);跑不通不要入库。')
730
- L.push(' · 宿主没有 Lean 工具链(LEAN_NOT_FOUND)时:把代码写下来归档,并在回执的 note 里写明'
750
+ L.push(' · 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或宿主不提供 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并在回执的 note 里写明'
731
751
  + '"宿主无 Lean 工具链"——这算显式阻塞原因,定论门禁可以据此放行。')
732
752
  }
733
753
  return L.join('\n')
734
754
  }
735
755
  function formalWorkLine() {
736
756
  if (!formalOn()) return ''
757
+ // `Formal/Proved/` 在项目根下**并不存在**(可复用库故意在项目树之外),只写相对路径会让代理
758
+ // 去项目里找一个永远找不到的目录;这里与验证段落一样给出 VibeMath 根的绝对路径(契约 §6.2)。
737
759
  return '【顺手形式化(' + (formalMode() === 'require' ? '强制' : '鼓励') + ')】把你工作中常用或可能复用的对象、假设、'
738
- + '新定义用 Lean 形式化定义并归档到全局可复用库(vibe_math_lean_archive kind=\'def\'),已成立的引理归到 Formal/Proved/'
739
- + '(kind=\'lemma\');写之前先 vibe_math_lean_lib 查重,避免重复定义。'
760
+ + '新定义用 Lean 形式化定义并归档到全局可复用库(vibe_math_lean_archive kind=\'def\'),已成立的引理归到 '
761
+ + vibeRoot().replace(/\\/g, '/') + '/Formal/Proved/(kind=\'lemma\');写之前先 vibe_math_lean_lib 查重,避免重复定义。'
740
762
  + '归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。'
741
763
  }
742
764
  /**
@@ -761,13 +783,34 @@ export function apply(ctx) {
761
783
  + '"note":"难度判断/阻塞原因/具体偏差"}(decision=\'blocked\'/\'defect\' 时必须写明 note,否则整条记录被拒绝;'
762
784
  + 'decision=\'defect\' 会撤回该证明的「已通过」状态并写入「形式化待办」)。'
763
785
  }
786
+ /**
787
+ * 撤回一份归档证明。删除是**尽力而为**,撤回却不是:
788
+ * 宿主没有 `subprocess` 服务、shell 删除失败、或 shell 退出 0 却没真的删掉(stub 宿主、权限问题)时,
789
+ * 必须把撤回通知**写进那个路径**——否则一份已被撤回的证明仍留在 `Verified/Lean/`,那是"所有人找
790
+ * 这个对象的证明"的地方;记录里的 `proof` 已经清空,于是目录与记录脱节,谁也不会再发现它
791
+ * (契约 §4.1 要求撤回,而不是"试过删除")。
792
+ * 返回**实际发生了哪一种**(deleted / overwritten / failed / unreachable),由调用方如实告知读者。
793
+ */
794
+ async function withdrawArchivedProof(rel) {
795
+ const abs = leanAbsPath(rel)
796
+ if (abs === null) return { rel: String(rel), outcome: 'unreachable' }
797
+ const sub = subprocessOf()
798
+ if (sub !== undefined && typeof sub.spawn === 'function') {
799
+ try { await runShell(rmCmd(abs)) } catch (e) { /* 落到下面的覆写 */ }
800
+ // 用 fs 复核"真的没了":`Remove-Item -ErrorAction SilentlyContinue` / `rm -f` 都可能静默失败。
801
+ if (await readTextAbs(abs) === undefined) return { rel: String(rel), outcome: 'deleted' }
802
+ }
803
+ const ok = await writeTextAbs(abs, '-- 已撤回(' + fmtTime() + '):该形式化被认定与命题原文不一致。\n'
804
+ + '-- 原代码保留在工作文件 Formal/' + String(rel).split('/').pop() + ';修正并重新跑通后重新归档。\n')
805
+ return { rel: String(rel), outcome: ok ? 'overwritten' : 'failed' }
806
+ }
764
807
  /**
765
808
  * 契约 §4.1:`defect` = **形式化不合格**,不是"命题为假"。
766
809
  *
767
810
  * 表决者逐条核对后发现 Lean 代码与命题原文不一致(写窄了/写宽了/换了对象/漏了条件…),
768
811
  * 那是这条形式化写得不对,不是命题被证伪。因此:
769
812
  * ① 把形式化记录**降级为 `attempted`**(无论此前是 `passed` 还是 `blocked`)、清空 `proof`;
770
- * ② 删除归档证明 `Verified/Lean/<id>.lean`(工作文件 `Formal/<id>.lean` 保留,代码不丢);
813
+ * ② 撤回归档证明 `Verified/Lean/<id>.lean`(工作文件 `Formal/<id>.lean` 保留,代码不丢);
771
814
  * ③ `note` 记入记录与 `Formal/TODO.md`,并在活动日志里公告;
772
815
  * ④ `require` 档下 `formalGateOk` 随之为假 → 本次裁定**不定论**,修正形式化并重新跑通后再投票。
773
816
  */
@@ -777,24 +820,50 @@ export function apply(ctx) {
777
820
  const before = formalRecords()
778
821
  const proofs = []
779
822
  const pushProof = function (p) { const s = String(p || ''); if (s && proofs.indexOf(s) === -1) proofs.push(s) }
780
- pushProof((before[t] || {}).proof)
781
- pushProof((before[objectId] || {}).proof)
823
+ // **同一对象的全部 id 别名**都要看:归档可能写在对象 id(`pX`)上,也可能写在验证 id
824
+ // (`r-pX` / `r-pX-s0` / `r-pX-pf1`)上,而回执点名的可以是任意一侧。只扫 `t` 与 `objectId`
825
+ // 会漏掉"归档用 rId、回执用对象 id"这一组合,于是那份被撤回的证明永远留在 `Verified/Lean/`
826
+ // 里(记录已清空 proof,没人再指向它)——这正是"成对关系只做一半"的缺陷形态(审计清单 §3)。
827
+ for (const k of Object.keys(before)) if (formalObjectIdOf(k) === objectId) pushProof((before[k] || {}).proof)
782
828
  // 两套 id 一起降级——只降一侧会让另一侧继续"已通过",门禁/卡片就会照旧放行。
783
829
  await putFormalBothIds(t, { status: 'attempted', decision: 'defect', note: note, proof: '', updatedAt: now() })
784
- // 删除归档证明:删记录里记着的那份,也删两边 id 直接对应的文件名(覆盖"归档时用对象 id、
785
- // 回执时用验证 id"这种两侧写法不同的情形)。
830
+ // 备选的归档证明路径 = 记录里记着的那份(一定存在过)+ 各个 id 直接对应的文件名。
831
+ const candidates = []
832
+ for (let i = 0; i < proofs.length; i++) if (proofs[i].indexOf('Verified/Lean/') === 0 && candidates.indexOf(proofs[i]) === -1) candidates.push(proofs[i])
833
+ const keys = Object.keys(before).filter(function (k) { return formalObjectIdOf(k) === objectId })
834
+ for (const id of keys.concat([t, objectId])) { const r = 'Verified/Lean/' + id + '.lean'; if (candidates.indexOf(r) === -1) candidates.push(r) }
835
+ // 只处理**确实存在**(或在记录里被引用)的路径:宿主没有 subprocess 时撤回会退化成"覆写通知",
836
+ // 若把从未存在过的文件名也一并处理,就会凭空造出 `Verified/Lean/<id>.lean`。
786
837
  const rels = []
787
- for (let i = 0; i < proofs.length; i++) if (proofs[i].indexOf('Verified/Lean/') === 0 && rels.indexOf(proofs[i]) === -1) rels.push(proofs[i])
788
- for (const id of [t, objectId]) { const r = 'Verified/Lean/' + id + '.lean'; if (rels.indexOf(r) === -1) rels.push(r) }
789
- for (let i = 0; i < rels.length; i++) { try { await removeFile(rels[i]) } catch (e) { /* 删除失败不应影响记账 */ } }
838
+ for (let i = 0; i < candidates.length; i++) {
839
+ if (proofs.indexOf(candidates[i]) !== -1) { rels.push(candidates[i]); continue }
840
+ const abs = leanAbsPath(candidates[i])
841
+ if (abs !== null && (await readTextAbs(abs)) !== undefined) rels.push(candidates[i])
842
+ }
843
+ const deleted = [], overwritten = [], failed = []
844
+ for (let i = 0; i < rels.length; i++) {
845
+ let r
846
+ try { r = await withdrawArchivedProof(rels[i]) } catch (e) { r = { rel: rels[i], outcome: 'failed' } }
847
+ if (r.outcome === 'deleted') deleted.push(r.rel)
848
+ else if (r.outcome === 'overwritten') overwritten.push(r.rel)
849
+ else failed.push(r.rel)
850
+ }
790
851
  const list = formalTodo().filter(function (x) { return x.id !== objectId })
791
852
  list.push({ id: objectId, at: now(), why: 'defect:形式化与命题原文不一致,需修正后重新跑通(' + note + ')' })
792
853
  await putFormal(t, Object.assign({}, formalRecords()[t] || {}, { status: 'attempted', decision: 'defect', note: note, proof: '', updatedAt: now() }), list)
793
854
  await writeFormalTodo()
794
855
  await writeFormalIndex()
856
+ // 公告必须如实说明**撤回实际怎么完成的**:删除成功、只能覆写成撤回通知、还是两者都没做到。
857
+ // 否则读者会以为归档目录里已经干净了,而一份坏证明还躺在那里(契约 §4.1)。
795
858
  logActivity('formal', '【形式化】' + who + ' 通过回执记录 ' + objectId + ' 的忠实性缺陷(decision=defect):' + note
796
- + ' —— 已撤回「已通过」状态(降级 attempted、删除归档证明 ' + rels.join('、') + '、写入 Formal/TODO.md),本次裁定不定论。')
797
- return { ok: true, decision: 'defect', target: objectId, named: t, status: 'attempted', note: note, cleared: rels }
859
+ + ' —— 已撤回「已通过」状态(降级 attempted'
860
+ + (deleted.length ? '、删除归档证明 ' + deleted.join('、') : '')
861
+ + (overwritten.length ? '、把归档证明覆写为撤回通知 ' + overwritten.join('、') + '(宿主无法删除,但该路径已不再是一份证明)' : '')
862
+ + (failed.length ? '、⚠ 归档证明 ' + failed.join('、') + ' 既未删除也未能覆写,记录已降级——请不要把它当作该对象的证明' : '')
863
+ + '、写入 Formal/TODO.md)。'
864
+ // 强度必须与档位一致(契约 §4.1 第 3 条):encourage 没有门禁,不能声称框架会搁置裁定。
865
+ + (formalRequired() ? '本次裁定不定论。' : '本档没有门禁:本次裁定是否定论由表决结果决定(靠表决者的弃权值阻止布尔一致结论)。'))
866
+ return { ok: true, decision: 'defect', target: objectId, named: t, status: 'attempted', note: note, cleared: rels, deleted: deleted, overwritten: overwritten, failed: failed }
798
867
  }
799
868
  /**
800
869
  * 回执通道(契约 §4 / §6.3):代理**即使一次 Lean 工具都没调用**,也必须能留下"实现难度判断"或
@@ -828,10 +897,21 @@ export function apply(ctx) {
828
897
  }
829
898
  if (decision === 'used') {
830
899
  const file = String(f.file || ('Formal/' + target + '.lean'))
831
- await putFormalBothIds(target, { status: 'attempted', decision: 'used', file: file, updatedAt: now() })
900
+ // 契约 §4:`used` 只表示"这一轮碰了形式化/写了草稿",它**不得**撤销已经成立的证明。
901
+ // 无条件写 attempted 会静默抹掉 passed:投票提示词丢掉忠实性分支、require 档对一份已跑通的
902
+ // 归档证明重新关门,而 `proof` 指针还留着(记录自相矛盾)。v3/v4/v5 都保留 passed/blocked——
903
+ // 这是"四套同构"里最容易被漏掉的一处(AUDIT-CHECKLIST §1.8)。
904
+ // 两套 id 空间都可能是"更强的那一侧"(对象侧 blocked、验证侧 passed 这类历史状态),
905
+ // 所以取两者的最强状态:passed > blocked > attempted——保证 `used` 在任何一侧都不降级。
906
+ const own = formalOf(target).status
907
+ const merged = formalGateRecord(target).status
908
+ const status = (own === 'passed' || merged === 'passed') ? 'passed'
909
+ : ((own === 'blocked' || merged === 'blocked') ? 'blocked' : 'attempted')
910
+ await putFormalBothIds(target, { status: status, decision: 'used', file: file, updatedAt: now() })
832
911
  await writeFormalIndex()
833
- logActivity('formal', '【形式化】' + who + ' 通过回执记录 ' + target + ' 形式化草稿:' + file)
834
- return { ok: true, decision: 'used', target: target, status: 'attempted', file: file }
912
+ logActivity('formal', '【形式化】' + who + ' 通过回执记录 ' + target + ' 形式化草稿:' + file
913
+ + (status === 'attempted' ? '' : '(保留已有的 ' + status + ' 状态:一次 used 回执不撤销已成立的证明)'))
914
+ return { ok: true, decision: 'used', target: target, status: status, file: file }
835
915
  }
836
916
  logActivity('formal', '【形式化】' + who + ' 的 formal.decision 只能是 \'used\' | \'blocked\' | \'defect\'(收到 '
837
917
  + String(f.decision) + '),已忽略(V2_INVALID_ARGUMENT)。')
@@ -959,7 +1039,14 @@ export function apply(ctx) {
959
1039
  const run = args.run === false ? null : await leanRunFile(abs)
960
1040
  await rebuildLeanLibIndexes()
961
1041
  logActivity('formal', (memberId || 'office') + ' 归档了' + (kind === 'def' ? '可复用定义' : '已证引理') + ' `' + name + '` → ' + rel + (run ? '(运行 ' + (run.ok ? '通过' : '未通过') + ')' : ''))
962
- return { ok: true, kind: kind, name: name, file: rel, run: run || undefined, note: '已并入全局可复用库,后续项目可直接 import 复用' }
1042
+ // 工具返回值同样是代理读到的文字:`run` 为红时**不能**声称"可直接 import 复用"——
1043
+ // 那份文件没有通过编译,"可复用库"里的它是有害的(契约 §6 第 3 条 / v2 实现方案 §9.4)。
1044
+ return {
1045
+ ok: true, kind: kind, name: name, file: rel, run: run || undefined,
1046
+ note: (run && !run.ok)
1047
+ ? '⚠ 该文件**运行未通过**(见 run.stderr):它已写入 ' + rel + ',但**不合格**,请不要当作可复用定义;请修复后用 vibe_math_lean_run(或再次归档 run=true)跑通。'
1048
+ : '已并入全局可复用库,后续项目可直接 import 复用',
1049
+ }
963
1050
  }
964
1051
  if (kind === 'proof') {
965
1052
  const rawTarget = String(args.target || '').trim()
@@ -977,16 +1064,23 @@ export function apply(ctx) {
977
1064
  const run = await leanRunFile(workRel)
978
1065
  const prev = formalOf(target)
979
1066
  const passed = !!run.ok
1067
+ // A RED re-archive invalidates the previous proof: the work file it proved has just been
1068
+ // overwritten by code that does not compile. Keeping the pointer (or leaving the archived
1069
+ // file) would produce "attempted + 归档证明 X.lean" in the index and let the fidelity prompt
1070
+ // print a proof path for code that no longer exists — `proof` is for `passed` only (§4).
1071
+ const stalePrev = passed ? '' : String(prev.proof || ('Verified/Lean/' + target + '.lean'))
980
1072
  const rec = Object.assign({}, prev, {
981
1073
  status: passed ? 'passed' : 'attempted',
982
1074
  file: workRel,
983
- proof: passed ? 'Verified/Lean/' + target + '.lean' : (prev.proof || ''),
1075
+ proof: passed ? 'Verified/Lean/' + target + '.lean' : '',
984
1076
  decision: 'used',
985
1077
  note: String(args.note || prev.note || ''),
986
1078
  run: { at: now(), ok: !!run.ok, exitCode: run.exitCode === undefined ? null : run.exitCode, ms: run.ms || 0, stdoutTail: tailText(run.stdout, 800), stderrTail: tailText(run.stderr, 800) },
987
1079
  updatedAt: now(),
988
1080
  })
1081
+ let withdrawn = null
989
1082
  if (passed) await writeText('Verified/Lean/' + target + '.lean', body)
1083
+ else if (stalePrev) { try { withdrawn = await withdrawArchivedProof(stalePrev) } catch (e) { withdrawn = { rel: stalePrev, outcome: 'failed' } } }
990
1084
  await putFormal(target, rec)
991
1085
  // ★ 让"归档"与"验证对象"两套 id 对齐。
992
1086
  // 验证提示词与门禁关心的是**这一次验证**(rId,例如 r-pGate),而代理用 vibe_math_lean_archive
@@ -995,7 +1089,10 @@ export function apply(ctx) {
995
1089
  // 裁定还是被反复搁置。两套记录都写,门禁与提示词任取其一都自洽。
996
1090
  await syncVerificationTarget(target, passed ? 'passed' : 'attempted', rec)
997
1091
  await rebuildLeanLibIndexes()
998
- logActivity('formal', (memberId || 'office') + ' 为 ' + target + ' 归档形式化证明 ' + workRel + '(运行 ' + (passed ? '通过,已归档到 ' + rec.proof + ',验证转为忠实性审查' : '未通过:' + tailText(run.stderr || run.message, 160)) + ')')
1092
+ logActivity('formal', (memberId || 'office') + ' 为 ' + target + ' 归档形式化证明 ' + workRel + '(运行 ' + (passed
1093
+ ? '通过,已归档到 ' + rec.proof + ',验证转为忠实性审查'
1094
+ : '未通过:' + tailText(run.stderr || run.message, 160)
1095
+ + ';已撤回上一份已通过状态与归档证明' + (withdrawn && withdrawn.outcome !== 'failed' ? '(' + withdrawn.outcome + ')' : '(⚠ 撤回失败,请不要把 ' + stalePrev + ' 当作该对象的证明)')) + ')')
999
1096
  return { ok: true, kind: kind, target: target, file: workRel, proof: rec.proof, passed: passed, run: run, status: rec.status }
1000
1097
  }
1001
1098
  if (kind === 'blocked') {
@@ -1024,14 +1121,22 @@ export function apply(ctx) {
1024
1121
  async function deferForFormal(target, why) {
1025
1122
  const t = safeId(String(target || ''))
1026
1123
  if (!t) return false
1027
- const rec = formalOf(t)
1028
1124
  if (!formalRequired()) return false
1125
+ // ★ 判定必须用**合并后**的记录(`formalGateRecord`,两套 id 都认),不能只看自己那一侧。
1126
+ // 代理是用**对象 id** 归档的(提示词里给它的就是对象 id),而验证/门禁问的是 rId;
1127
+ // `syncVerificationTarget` 只更新**已存在**的别名键,所以"归档写了 pX、r-pX 还没有记录"是
1128
+ // 首次形式化后的真实状态。那时 `formalOf('r-pX')` 返回 `{status:'none'}`,门禁会把一个
1129
+ // **已经 passed 的对象**判为未形式化(假阴性);更糟的是这次搁置本身会写下 r-pX =
1130
+ // status:'none',于是此后**每一轮**都继续搁置、继续重开一次完整辩论——对象永远无法定论。
1131
+ // 这正是实现方案 §四写明的"门禁与提示词取记录时两个方向都认(formalGateRecord)"。
1132
+ const rec = formalGateRecord(t)
1029
1133
  if (formalGateOk(rec)) return false
1134
+ const own = formalOf(t)
1030
1135
  const reason = 'formal-required:尚未取得 Lean 形式化通过,也没有显式阻塞记录(当前状态 ' + (rec.status || 'none') + ')' + (why ? '|' + why : '')
1031
1136
  const list = formalTodo().filter(function (x) { return x.id !== t })
1032
1137
  list.push({ id: t, at: now(), why: reason })
1033
1138
  // 不改动对象的既有权重/概率字段:只补一条 formal 记录(status 保持 none/attempted)。
1034
- await putFormal(t, rec.status === 'none' ? { status: 'none', deferredAt: now() } : Object.assign({}, rec, { deferredAt: now() }), list)
1139
+ await putFormal(t, own.status === 'none' ? { status: 'none', deferredAt: now() } : Object.assign({}, own, { deferredAt: now() }), list)
1035
1140
  await writeFormalTodo()
1036
1141
  await writeFormalIndex()
1037
1142
  // v2 没有群聊文件(没有 Shared/Chat/),所以"群聊公告"落在它的可读通道上:
@@ -1411,7 +1516,16 @@ export function apply(ctx) {
1411
1516
  L.push(' ▸ **发现任何偏差,不要投 0**:偏差只说明**形式化不合格**,不代表命题为假。此时请:')
1412
1517
  L.push(" ① Result 给一个严格介于 0 与 1 之间的值(记为弃权),并在 Reason 里写清偏差;")
1413
1518
  L.push(" ② 用回执 formal:{decision:'defect', note:'<具体偏差>'} 记录它。框架会撤回这条证明的")
1414
- L.push(' 「已通过」状态(降级为 attempted、删除归档证明、写入形式化待办),本次裁定**不定论**;')
1519
+ if (formalMode() === 'require') {
1520
+ // 这一档真的有门禁,所以"本次裁定**不定论**"是框架**能兑现**的承诺(契约 §6.1)。
1521
+ L.push(' 「已通过」状态(降级为 attempted、删除归档证明、写入形式化待办),本次裁定**不定论**;')
1522
+ } else {
1523
+ // encourage 档没有门禁:撤回证明 ≠ 搁置裁定。承诺一个框架无法强制的"不定论",会让表决者
1524
+ // 以为不必自己弃权——那正是"提示词承诺的强度档位必须与实现一致"这条不变式(契约 §4.1 第 3 条
1525
+ // / §6.1,审计清单 §1.7)。这里如实说明:阻止本轮定论的是**你的弃权值**。
1526
+ L.push(' 「已通过」状态(降级为 attempted、删除归档证明、写入形式化待办)。**本档没有门禁**:')
1527
+ L.push(' 框架不会强制搁置本次裁定——请务必给出①里的弃权值,靠它阻止本轮得出布尔一致结论;')
1528
+ }
1415
1529
  L.push(' 修正形式化并重新跑通后再投票。')
1416
1530
  L.push(' ▸ 只有当你**独立于这份 Lean 代码**也能确定命题为假时,才投 0,并在 Reason 里写清独立理由。')
1417
1531
  } else if (rec.status === 'blocked') {
@@ -2085,19 +2199,30 @@ export function apply(ctx) {
2085
2199
  if (q.判断命题 && !sol.来源列表) {
2086
2200
  const ap = await findProposition(q.判断命题)
2087
2201
  if (ap) {
2088
- ap.布尔估计 = v
2089
- // 收敛闸门:本条路径只在 v=1/0 时才写入证明/证伪条目,中间裁决(flat 默认给出
2090
- // 0.5,forced 给出加权浮点)会让 ap 停留在"中间布尔估计 + 两个列表皆空"的状态——
2091
- // 而这正是 buildVerifyCandidates 认定"裸命题需要验证"的条件。若不在此标记,该命题
2092
- // 会在每个 tick 重新入选、重开一轮完整辩论;又因 processVerify 每 tick 只跑一个验证,
2093
- // 其它对象被无限饿死,终止条件(所有问题已解决)永不可达。标记后不再重复消耗验证配额,
2094
- // 裁决值仍保留在 布尔估计 中。
2095
- ap.已验证 = true
2096
- if (v === 1) { ap.证明列表 = ap.证明列表 || []; ap.证明列表.push({ 完整过程: strongestReason(t, 1) || '判断问题解法验证通过', 正确概率: 1, '支持信息/依据': '经「判断下述命题是否成立」问题解法验证', 已验: true }); ap.优先级 = 'never' }
2097
- else if (v === 0) { ap.证伪列表 = ap.证伪列表 || []; ap.证伪列表.push({ 完整过程: strongestReason(t, 0) || '判断问题解法判定不成立', 正确概率: 1, '支持信息/依据': '经「判断下述命题是否成立」问题解法验证', 已验: true }); ap.优先级 = 'never' }
2098
- await upsertProposition(ap)
2099
- await writeVerifiedCardIfNeeded(ap)
2100
- logActivity('judge-sync', 'judge problem verdict ' + v + ' synced to proposition ' + ap.id)
2202
+ // ★ require 门禁:这条路径把**源命题**的 布尔估计 直接写成 0/1、置 优先级='never'、
2203
+ // 并标记 已验证(此后不再入选验证)——这就是一次对源命题的布尔裁定,必须先过门禁。
2204
+ // 原来的写法只让 writeVerifiedCardIfNeeded 去拦卡片,而它的返回值没人检查,概率字段
2205
+ // 早已落库:一个未形式化的命题会被侧面判为"假"并永久退出调度(v=1 时上面那道门禁
2206
+ // 拦得住,v=0 拦不住)。实现方案 §八把顺序写得很清楚:先拿卡片写入许可,再改
2207
+ // 布尔估计/优先级(契约 §8:门禁不通过时"不改变对象的既有权重/概率字段")。
2208
+ const judgeBool = (v === 1 || v === 0)
2209
+ if (judgeBool && await deferForFormal(ap.id, '「判断命题」问题解法的裁定转移到源命题(' + v + ')')) {
2210
+ logActivity('gate', t.rId + ' 对源命题 ' + ap.id + ' 的裁定 ' + v + ' 被 require 模式搁置为未定论(formal-required)')
2211
+ } else {
2212
+ ap.布尔估计 = v
2213
+ // 收敛闸门:本条路径只在 v=1/0 时才写入证明/证伪条目,中间裁决(flat 默认给出
2214
+ // 0.5,forced 给出加权浮点)会让 ap 停留在"中间布尔估计 + 两个列表皆空"的状态——
2215
+ // 而这正是 buildVerifyCandidates 认定"裸命题需要验证"的条件。若不在此标记,该命题
2216
+ // 会在每个 tick 重新入选、重开一轮完整辩论;又因 processVerify 每 tick 只跑一个验证,
2217
+ // 其它对象被无限饿死,终止条件(所有问题已解决)永不可达。标记后不再重复消耗验证配额,
2218
+ // 裁决值仍保留在 布尔估计 中。
2219
+ ap.已验证 = true
2220
+ if (v === 1) { ap.证明列表 = ap.证明列表 || []; ap.证明列表.push({ 完整过程: strongestReason(t, 1) || '判断问题解法验证通过', 正确概率: 1, '支持信息/依据': '经「判断下述命题是否成立」问题解法验证', 已验: true }); ap.优先级 = 'never' }
2221
+ else if (v === 0) { ap.证伪列表 = ap.证伪列表 || []; ap.证伪列表.push({ 完整过程: strongestReason(t, 0) || '判断问题解法判定不成立', 正确概率: 1, '支持信息/依据': '经「判断下述命题是否成立」问题解法验证', 已验: true }); ap.优先级 = 'never' }
2222
+ await upsertProposition(ap)
2223
+ await writeVerifiedCardIfNeeded(ap)
2224
+ logActivity('judge-sync', 'judge problem verdict ' + v + ' synced to proposition ' + ap.id)
2225
+ }
2101
2226
  }
2102
2227
  }
2103
2228
  if (v === 1) { q.已解决 = true; q.优先级 = 'never'; await writeVerifiedProblemCardIfNeeded(q) }
@@ -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,6 +328,15 @@ v2 **没有会话投影**,所以记录与待办一起持久化在 v2 自己的
316
328
  - **`processStatusUpdates`**——概率=1 的**自动收口**路径。先申请卡片写入许可
317
329
  (`writeVerifiedCardIfNeeded(p, target)` / `writeVerifiedProblemCardIfNeeded(q, true)`),
318
330
  拿到许可才写入 `布尔估计` / `已解决` / `优先级`;被搁置则整条跳过。
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 节守住。)
319
340
 
320
341
  ### 九、回执通道(`formal` 字段)与 `defect` 语义
321
342
 
@@ -332,7 +353,11 @@ v2 **没有会话投影**,所以记录与待办一起持久化在 v2 自己的
332
353
  在三条解析回执的路径上都被调用:`handleVerifier`(验证者)、`handleSolver`、`handleExplorer`
333
354
  (工作轮)。验证者路径里的调用**先于**裁定:`defect` 必须先把记录降级,紧随其后的
334
355
  `settleVerdict` 才会在 `require` 档把本次裁定正确地记为未定论。
335
- - `decision` 的落库:`blocked` → 记录 `blocked` + `note`;`defect` → 见 9.2;`used` → `attempted` + `file`。
356
+ - `decision` 的落库:`blocked` → 记录 `blocked` + `note`;`defect` → 见 9.2;
357
+ `used` → `attempted` + `file`,**但若该对象(两个 id 空间任一)已经是 `passed`/`blocked`,则保持原状态**
358
+ ——一次"这一轮碰了形式化"的 `used` 回执**不得**撤销已成立的证明:无条件写 `attempted` 会让投票提示词
359
+ 丢掉忠实性分支、`require` 档对一份已跑通的归档证明重新关门,而 `proof` 指针还留着(记录自相矛盾)。
360
+ 撤销证明只有 `defect` 一条路(契约 §4)。
336
361
  - **拒绝规则**(返回 `V2_INVALID_ARGUMENT`,并在活动日志里公告,**不留下任何记录**):
337
362
  `blocked`/`defect` 的 `note` 为空;缺 `target`(`safeId('')` 会返回 `anon`,绝不允许凭空造记录);
338
363
  `decision` 不在三值之内。回执通道**绝不抛异常**进调度循环:一次记账失败不该吞掉一次表决。
@@ -351,8 +376,18 @@ v2 **没有会话投影**,所以记录与待办一起持久化在 v2 自己的
351
376
  1. **表决者**:不得投 `1` 或 `0`;给一个严格介于 0 与 1 之间的值(记为弃权),在 `Reason` 里写清偏差,
352
377
  并用回执 `formal:{decision:'defect', note:'<具体偏差>'}` 记录(`note` 必填)。
353
378
  2. **框架**(`formalDefectDowngrade`):把形式化记录**降级为 `attempted`**(无论此前是 `passed`
354
- 还是 `blocked`)、清空 `proof`、**删除** `Verified/Lean/<id>.lean`(工作文件 `Formal/<id>.lean`
355
- 保留,代码不丢)、`note` 记入记录与 `Formal/TODO.md`、在活动日志里公告。
379
+ 还是 `blocked`)、清空 `proof`、**撤回**归档证明(工作文件 `Formal/<id>.lean` 保留,代码不丢)、
380
+ `note` 记入记录与 `Formal/TODO.md`、在活动日志里公告。
381
+
382
+ 「撤回」不是"尽力删除"(`withdrawArchivedProof`):
383
+ 1. 先尽力删除(有 `subprocess` 时走 shell),并用 fs **复核真的没了**;
384
+ 2. 删不掉(宿主无 `subprocess`、权限、`rm -f` 静默失败)时,把撤回通知**覆写进那个路径**
385
+ (`-- 已撤回(<时间>):该形式化被认定与命题原文不一致。` + 指向保留的工作文件),
386
+ 使一份已撤回的证明**不可能**再被读成该对象的证明;
387
+ 3. 活动日志如实说明发生了哪一种(删除 / 覆写成撤回通知 / ⚠ 两者都失败)。
388
+ 候选路径 = **同一对象的全部 id 别名**(对象 id、`r-<id>`、`r-<id>-s0|pf<n>|rf<n>`)在记录里
389
+ 引用的 `proof`,加上各 id 直接对应的 `Verified/Lean/<id>.lean`;只处理确实存在(或被记录引用)
390
+ 的路径——宿主不能删除时"覆写"会把不存在的文件名**凭空造出来**。
356
391
  3. **`require` 档**:降级后 `formalGateOk` 为假 ⇒ 本次裁定**不定论**(`formal-required`),
357
392
  对象进入「形式化待办」。修正形式化并重新跑通后再投票。
358
393
  4. **唯一可以投 0 的情形**:表决者**独立于这份 Lean 代码**也能确定命题为假(`Reason` 必须写清独立理由)。
@@ -398,4 +433,21 @@ v2 **没有会话投影**,所以记录与待办一起持久化在 v2 自己的
398
433
  + `proof` 清空 + `Verified/Lean/<id>.lean` 被删 + `Formal/TODO.md` 出现条目 + 公告),断言缺 `note`
399
434
  被 `V2_INVALID_ARGUMENT` 拒绝且不落记录,断言 `defect` 之后 `require` 档**不得定论**(无 Verified 卡片、
400
435
  命题布尔估计不变),并落盘人可读语料(§9.5)。
436
+
437
+ 后续补强(同一套件):
438
+
439
+ - 第 4b 节:超时必须**主动**终止——mock 记录 `terminate()` 调用(`-- HANG` 文件模拟一个不会自己
440
+ 结束的运行),断言返回 `LEAN_TIMEOUT` 且真的调用了 `terminate()`;并断言 `done` 先到时计时器
441
+ 被清掉(套件用 test-only 的悬挂 `setTimeout` 计数;mock 自身的兜底用真实计时器,不污染计数)。
442
+ - 第 13 节:归档写在**对象 id**、`rId` 侧还没有记录时,`require` 档仍必须能定论(门禁读合并记录),
443
+ 且该对象**不得**被写进「形式化待办」。
444
+ - 第 14 节:`defect` 的撤回覆盖**全部 id 别名**(归档写 `rId`、回执写对象 id 也能删干净);
445
+ 宿主**不能删除**时(`{ noSubprocess: true }` 的宿主)归档路径必须不再包含原证明文本、并且带
446
+ 撤回通知;活动日志必须说明"覆写"而不是"删除"。
447
+ - 第 15 节:`require` 档必须拦住「判断命题」问题解法到**源命题**的转移(`布尔估计` / `优先级` /
448
+ `已验证` / 概率=1 条目 / Verified 卡片全部不动,源命题进待办)。
449
+ - 模式相关的忠实性措辞:`encourage` 档**不得**出现"本次裁定**不定论**"(改写为"本档没有门禁"),
450
+ `require` 档**必须**出现(那里门禁真的会搁置裁定)。
451
+ - `kind='def'/'lemma'` 运行未通过时,工具返回值**不得**声称"可直接 import 复用"。
452
+
401
453
  套件支持 `V2_PLUGIN` 环境变量指向变异副本,供灵敏度探针使用。