dsh-vibe-math 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/AUDIT-CHECKLIST.md +28 -0
  2. package/README.md +98 -1
  3. package/RELEASE-NOTES-2.3.0.md +207 -0
  4. package/audit-formal-sensitivity.mjs +247 -0
  5. package/audit-persona-sensitivity.mjs +249 -0
  6. package/audit-persona-surface.test.mjs +349 -0
  7. package/audit-v5-integrity.mjs +40 -1
  8. package/audit-v5-sensitivity.mjs +77 -6
  9. package/docs/formal-verification.md +321 -0
  10. package/docs/generate_framework_diagram_v5.mjs +22 -16
  11. package/formal-verify-v2.test.mjs +672 -0
  12. package/formal-verify-v3.test.mjs +824 -0
  13. package/formal-verify-v4.test.mjs +603 -0
  14. package/formal-verify-v5.test.mjs +526 -0
  15. package/package.json +15 -2
  16. package/prompt-corpus-persona/persona-corpus.json +32 -0
  17. package/prompt-corpus-persona/persona-corpus.md +674 -0
  18. package/prompt-corpus-v3/formal-verify-v3.json +280 -0
  19. package/prompt-corpus-v3/formal-verify-v3.md +2826 -0
  20. package/prompt-corpus-v5/prompt-corpus-v5.json +75 -9
  21. package/prompt-corpus-v5/prompt-corpus-v5.md +384 -65
  22. package/prompt-v5-integrity.test.mjs +111 -10
  23. package/vibe-math-v2/agent.cordis.yml +40 -2
  24. package/vibe-math-v2/vibe-math-v2.js +627 -19
  25. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +145 -1
  26. package/vibe-math-v3/agent.cordis.yml +46 -2
  27. package/vibe-math-v3/vibe-math-v3.js +749 -21
  28. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +87 -2
  29. package/vibe-math-v4/agent.cordis.yml +46 -4
  30. package/vibe-math-v4/vibe-math-v4.js +652 -15
  31. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +226 -0
  32. package/vibe-math-v5/agent.cordis.yml +41 -5
  33. package/vibe-math-v5/vibe-math-v5.js +562 -9
  34. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +108 -4
  35. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +57 -0
  36. package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +51 -46
@@ -150,6 +150,11 @@ export function apply(ctx) {
150
150
  methodAutoPromote: false, // 项目级方法自动晋升全局库(false = 人工门)
151
151
  indexAutoRebuild: true, // 每次写盘后自动重建索引(false = 手动 vibe_math_index)
152
152
  projectLockTimeoutMs: 60000, // 项目锁等待超时
153
+ // ---- Lean 形式化验证(契约:docs/formal-verification.md,四架构同名同语义)----
154
+ formalVerify: 'off', // off | encourage | require(三档开关,见 实现方案.md §10.1)
155
+ leanCommand: 'lean', // 要执行的 Lean 可执行文件(例:'lake')
156
+ leanArgs: [], // 插在文件名之前的附加参数(例:['env','lean'] 配 leanCommand='lake')
157
+ leanTimeoutMs: 120000, // 单次 Lean 运行超时上限(毫秒,正整数)
153
158
  }
154
159
  let params = Object.assign({}, DEFAULT_PARAMS)
155
160
  let scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null } // activeCount 由 activeCount() 从 agentRegistry 推导(防漂移,同 v2)
@@ -179,6 +184,16 @@ export function apply(ctx) {
179
184
  let projectLock = { sessionId: '', at: 0 }
180
185
  let lastIndexWrite = 0 // State/index.json 写入节流(每 5s 至多一次;工具/init 强制时立即)
181
186
  let archivedJ = {} // qid -> [md 段]:重派生时被替换方向的 journal 归档(论文式历史保留)
187
+ /**
188
+ * Lean 形式化记录(契约 docs/formal-verification.md §4/§9)。
189
+ *
190
+ * v3 没有会话投影,所以每个对象的形式化记录落在**自己的持久状态** `State/formal.json`
191
+ * (与其它 State/*.json 同样的 readJson/writeJson 通道),按对象 id 索引,跨 resume 存活:
192
+ * records: { <对象id>: {status, file, proof, decision, note, run, updatedAt} }
193
+ * todo: [{id, at, why, verdict, project}] —— require 模式下被搁置的定论
194
+ * libRuns: { 'Lib/<名>.lean' | 'Proved/<名>.lean': {ok, exitCode, ms, at} } —— 索引的「最近运行」列
195
+ */
196
+ let formalState = { records: {}, todo: [], libRuns: {} }
182
197
 
183
198
  // ================= helpers =================
184
199
  function textBlock(t) { return { type: 'text', text: String(t) } }
@@ -291,6 +306,10 @@ export function apply(ctx) {
291
306
  { name: 'methodAutoPromote', type: 'boolean', description: '项目级方法自动晋升全局库(false = 人工门)', suggestion: false },
292
307
  { name: 'indexAutoRebuild', type: 'boolean', description: '每次写盘后自动重建索引(false = 手动 vibe_math_index)', suggestion: true },
293
308
  { name: 'projectLockTimeoutMs', type: 'integer', description: '项目锁等待超时(毫秒)', suggestion: 60000 },
309
+ { name: 'formalVerify', type: 'enum', options: ['off', 'encourage', 'require'], description: 'Lean 形式化验证:off = 不额外要求(默认,提示词里不出现任何 Lean 内容);encourage = 鼓励按实现难度形式化,Lean 通过后验证转为忠实性审查;require = 同上并加门禁:真/假定论必须先达到 Lean 已通过 或 已记录显式阻塞原因,否则记为未定论(formal-required)并进「形式化待办」', suggestion: 'off' },
310
+ { name: 'leanCommand', type: 'string', description: 'Lean 可执行文件(例:lean / lake;配合 leanArgs=[env,lean] 用 lake)', suggestion: 'lean' },
311
+ { name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加参数', suggestion: [] },
312
+ { name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,非正数回退默认)', suggestion: 120000 },
294
313
  ]
295
314
 
296
315
  // ================= fs (adapted to DSH 0.1.1: resolve returns {targetKey, displayPath}) =================
@@ -364,7 +383,12 @@ export function apply(ctx) {
364
383
  return { ok: outcome.exitCode === 0, exitCode: outcome.exitCode }
365
384
  } catch (e) { return { ok: false, error: String((e && e.message) || e) } }
366
385
  }
367
- async function ensureDirs() { const base = frameworkRoot(); const dirs = ['Problems', 'Progress', 'Propos', 'Methods', 'Verified/命题', 'Verified/问题', 'Reliable', 'Notes', 'Logs/Verification', 'Logs/Plans', 'State']; const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Methods'].concat(dirs.map(function (d) { return base + '/' + d })); return await runShell(mkdirCmd(paths)) }
386
+ /**
387
+ * 目录骨架。Lean 形式化(契约 §3)要求项目内 `Formal/`(形式化工作文件 + Index.md/TODO.md)
388
+ * 与 `Verified/Lean/`(归档证明,与定论卡片同处 Verified/,一眼可见"这条结论的证明在哪"),
389
+ * 以及**项目树之外**的全局可复用库 `<VibeMath 根>/Formal/{Lib,Proved}`(跨项目复用是核心收益)。
390
+ */
391
+ async function ensureDirs() { const base = frameworkRoot(); const dirs = ['Problems', 'Progress', 'Propos', 'Methods', 'Verified/命题', 'Verified/问题', 'Verified/Lean', 'Formal', 'Reliable', 'Notes', 'Logs/Verification', 'Logs/Plans', 'State']; const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Methods', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d })); return await runShell(mkdirCmd(paths)) }
368
392
  async function removeFile(rel) { const base = frameworkRoot(); return await runShell(rmCmd(base + '/' + rel)) }
369
393
 
370
394
  // ================= settings =================
@@ -372,7 +396,7 @@ export function apply(ctx) {
372
396
  const out = {}
373
397
  const intFields = ['maxParallelThreshold', 'solverMaxRounds', 'directionsPerSolver', 'verifierCount', 'debateMaxRounds', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries', 'planningHorizon', 'planMinIntervalMs', 'plannerMaxFails', 'methodKeepIntervalMs', 'methodKeepEvery', 'projectLockTimeoutMs']
374
398
  const numFields = ['promoteValueThreshold']
375
- const arrayFields = ['solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny']
399
+ const arrayFields = ['solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny', 'leanArgs']
376
400
  const boolFields = ['plannerEnabled', 'methodAutoPromote', 'indexAutoRebuild']
377
401
  for (const k of Object.keys(DEFAULT_PARAMS)) {
378
402
  if (!(k in obj)) continue
@@ -386,6 +410,11 @@ export function apply(ctx) {
386
410
  else if (k === 'reportMode') { out[k] = (v === 'file' || v === 'push' || v === 'both') ? v : DEFAULT_PARAMS[k] }
387
411
  else if (k === 'priorityAdjust') { out[k] = (v === 'none' || v === 'deadend-deprioritize' || v === 'survival-map') ? v : DEFAULT_PARAMS[k] }
388
412
  else if (k === 'proposPriorityAdjust') { out[k] = (v === 'none' || v === 'progress-graded') ? v : DEFAULT_PARAMS[k] }
413
+ // Lean 形式化(契约 §1):非法值一律回退默认,且**绝不**回退到更强的档位——
414
+ // 一个拼错的值若被当成 encourage/require,会让每个对象都被形式化要求卡住。
415
+ else if (k === 'formalVerify') { out[k] = (v === 'off' || v === 'encourage' || v === 'require') ? v : 'off' }
416
+ else if (k === 'leanCommand') { const s = String(v == null ? '' : v).trim(); out[k] = s || 'lean' }
417
+ else if (k === 'leanTimeoutMs') { const n = Number(v); out[k] = (Number.isFinite(n) && n > 0) ? Math.floor(n) : DEFAULT_PARAMS[k] }
389
418
  else if (k === 'solverAllowNetwork' || k === 'verifierAllowNetwork' || k === 'solverAllowScripts' || k === 'verifierAllowScripts') { out[k] = (v === true || v === false || v === '') ? v : DEFAULT_PARAMS[k] }
390
419
  else { out[k] = v }
391
420
  }
@@ -528,6 +557,9 @@ export function apply(ctx) {
528
557
  lines.push(anchorLine('ID', p.id))
529
558
  lines.push(anchorLine('类型', '问题'))
530
559
  lines.push(anchorLine('状态', p.状态 || '求解中'))
560
+ // 形式化锚点(契约 §6/§9.1):与「状态」同行区,让读者一眼看到这条结论的形式化强度。
561
+ // off 模式或尚无形式化记录时**不写这一行**(off 必须是真正的无操作)。
562
+ if (formalAnchorLine(p.id)) lines.push(anchorLine('形式化', formalAnchorLine(p.id)))
531
563
  lines.push(anchorLine('优先级', p.优先级 == null ? 1 : p.优先级))
532
564
  lines.push(anchorLine('依赖', JSON.stringify(p.依赖 || [])))
533
565
  lines.push(anchorLine('被依赖', JSON.stringify(p.被依赖 || [])))
@@ -573,6 +605,8 @@ export function apply(ctx) {
573
605
  lines.push(anchorLine('类型', '命题'))
574
606
  lines.push(anchorLine('状态', p.状态 || '未定论'))
575
607
  lines.push(anchorLine('概率', p.概率 == null ? 0.5 : p.概率))
608
+ // 形式化锚点(契约 §6/§9.1):紧邻 状态/概率,off 模式或状态为 none 时不写(无操作)。
609
+ if (formalAnchorLine(p.id)) lines.push(anchorLine('形式化', formalAnchorLine(p.id)))
576
610
  lines.push(anchorLine('优先级', p.优先级 == null ? 1 : p.优先级))
577
611
  lines.push(anchorLine('依赖', JSON.stringify(p.依赖 || [])))
578
612
  if (p.价值关键性 != null) lines.push(anchorLine('价值/关键性', p.价值关键性))
@@ -695,6 +729,8 @@ export function apply(ctx) {
695
729
  lines.push(anchorLine('类型', card.类型 || '命题'))
696
730
  lines.push(anchorLine('结论', card.结论 === true ? '真' : (card.结论 === false ? '假' : '')))
697
731
  lines.push(anchorLine('概率', card.概率))
732
+ // 结论卡片必须随信带上形式化强度:读者要能分辨"内核已核对"与"仅共识"(契约 §8)。
733
+ if (formalAnchorLine(card.id)) lines.push(anchorLine('形式化', formalAnchorLine(card.id)))
698
734
  lines.push(anchorLine('分类', card.分类 || '未分类'))
699
735
  lines.push(anchorLine('来源', card.来源 || ''))
700
736
  lines.push(anchorLine('时间', fmtTime(card.时间)))
@@ -905,6 +941,14 @@ export function apply(ctx) {
905
941
  const pl = await readJson('State/project_lock.json'); if (pl) projectLock = Object.assign({ sessionId: '', at: 0 }, pl)
906
942
  const lp = await readJson('State/last_plan.json'); if (lp) lastPlanSummary = lp
907
943
  const aj = await readJson('State/archived_journals.json'); if (aj && typeof aj === 'object') archivedJ = aj
944
+ const fm = await readJson('State/formal.json')
945
+ if (fm && typeof fm === 'object') {
946
+ const rec = (fm.records && typeof fm.records === 'object') ? fm.records : {}
947
+ // 只接受形状正确的记录:单个坏条目不该让整份形式化状态失效(与 loadKnowledgeBase 的容错一致)。
948
+ const clean = {}
949
+ for (const k of Object.keys(rec)) { const r = rec[k]; if (r && typeof r === 'object' && typeof r.status === 'string') clean[k] = r }
950
+ formalState = { records: clean, todo: Array.isArray(fm.todo) ? fm.todo.filter(function (t) { return t && t.id }) : [], libRuns: (fm.libRuns && typeof fm.libRuns === 'object') ? fm.libRuns : {} }
951
+ }
908
952
  }
909
953
  async function saveAll() {
910
954
  await writeJson('State/scheduler_state.json', scheduler)
@@ -917,6 +961,8 @@ export function apply(ctx) {
917
961
  await writeJson('State/method_log.json', methodLog)
918
962
  await writeJson('State/project_lock.json', projectLock)
919
963
  await writeJson('State/archived_journals.json', archivedJ)
964
+ // 未启用且从未产生任何形式化记录时不落这份状态文件(off 保持真正的无操作)。
965
+ if (formalOn() || Object.keys(formalState.records).length || formalState.todo.length || Object.keys(formalState.libRuns).length) await writeJson('State/formal.json', formalState)
920
966
  if (lastPlanSummary) await writeJson('State/last_plan.json', lastPlanSummary)
921
967
  scheduler.lastCheckpoint = now()
922
968
  }
@@ -952,6 +998,8 @@ export function apply(ctx) {
952
998
  registeredAgents: Object.keys(agentRegistry).length,
953
999
  queuedPlanActions: planQueue.length,
954
1000
  recentActivity: activityLog.slice(-Math.min(30, Number(params.activityLogCap) || 100)),
1001
+ // Lean 形式化:模式 + 每个对象的状态 + 形式化待办(可读参数表在 params 里)。
1002
+ formal: formalSummary(),
955
1003
  params: params,
956
1004
  }
957
1005
  }
@@ -981,6 +1029,18 @@ export function apply(ctx) {
981
1029
  lines.push('- **' + q.id + '**:' + q.状态 + ',优先级 ' + q.优先级 + ',解法 ' + (q.solutions || []).length + ' 条' + dirInfo)
982
1030
  }
983
1031
  lines.push('')
1032
+ lines.push('## Lean 形式化')
1033
+ if (!formalOn()) {
1034
+ lines.push('- 未启用(`formalVerify` = off;可用 vibe_math_set_params 切到 encourage / require)')
1035
+ } else {
1036
+ const fs2 = formalSummary()
1037
+ lines.push('- 模式:' + fs2.mode + '(' + (fs2.mode === 'require' ? '强制:定论前必须有 Lean 通过或显式阻塞记录' : '鼓励:按实现难度自行决定') + ')')
1038
+ lines.push('- 已通过:' + (fs2.passed.join('、') || '(无)'))
1039
+ lines.push('- 已记录阻塞:' + (fs2.blocked.join('、') || '(无)'))
1040
+ lines.push('- 形式化待办:' + (fs2.todo.map(function (t) { return t.id }).join('、') || '(无)'))
1041
+ lines.push('- 可复用库:VibeMath/Formal/{Lib,Proved}/(跨项目)|本项目形式化:Formal/|归档证明:Verified/Lean/')
1042
+ }
1043
+ lines.push('')
984
1044
  lines.push('## 最近活动')
985
1045
  for (const e of activityLog.slice(-10)) lines.push('- ' + fmtTime(e.at) + ' [' + e.event + '] ' + e.detail)
986
1046
  lines.push('')
@@ -1237,7 +1297,9 @@ export function apply(ctx) {
1237
1297
  'Then propose 3-6 DIVERSE, mutually distinct solution directions (e.g. analytic method, constructive proof, contradiction, numeric approximation + limit passage, categorical abstraction, ...). ' +
1238
1298
  'Record each direction with its core assumption and an initial feasibility estimate. Every direction must be self-contained: title / method / core_assumption written completely, defining every object they mention — no 断章取义.\n\n' +
1239
1299
  'feasibility ∈ [0,1] = your estimate of the probability this direction leads to a full solution. Respond with ONLY a single JSON object in a ```json code fence (no prose outside it). Register the directions as metadata; the scheduler writes them into the research log:\n' +
1240
- '{"meta":{"kind":"directions","qid":"<qid>","directions":[{"id":"d1","title":"...","method":"...","core_assumption":"...","feasibility":0.5}],"methods_used":[{"id":"m-...","效果":"<为何该方向借鉴它>","建议":"..."}],"new_inventions":[{"类型":"方法|工具|...","标题":"...","内容描述":"...","是否已入库":false}]}}'
1300
+ '{"meta":{"kind":"directions","qid":"<qid>","directions":[{"id":"d1","title":"...","method":"...","core_assumption":"...","feasibility":0.5}],"methods_used":[{"id":"m-...","效果":"<为何该方向借鉴它>","建议":"..."}],"new_inventions":[{"类型":"方法|工具|...","标题":"...","内容描述":"...","是否已入库":false}]}}' +
1301
+ // 顺手形式化(契约 §6.2)+ 回执字段(契约 §6.3)
1302
+ (formalOn() ? '\n' + formalWorkLine() + formalReplyNote() : '')
1241
1303
  }
1242
1304
  function rederivePrompt(q, prog) {
1243
1305
  const prior = prog.map(function (d) {
@@ -1251,7 +1313,8 @@ export function apply(ctx) {
1251
1313
  '\nQuantitatively analyze the historical progress, blocker causes, and feasibility decay of each prior direction. Discard directions already proven dead ends (unless a new tool/idea changes that). ' +
1252
1314
  'Then deeply DERIVE 1-3 BRAND-NEW directions never tried before, each with a one-line motivation. Return the UNION of high-potential leftover directions and the brand-new directions (drop dead ends).\n\n' +
1253
1315
  'feasibility ∈ [0,1]. Respond with ONLY a single JSON object in a ```json code fence (no prose outside it). Register the directions as metadata; the scheduler writes them into the research log:\n' +
1254
- '{"meta":{"kind":"directions","qid":"<qid>","directions":[{"id":"d1","title":"...","method":"...","core_assumption":"...","feasibility":0.5}],"methods_used":[{"id":"m-...","效果":"...","建议":"..."}],"new_inventions":[{"类型":"方法|工具|...","标题":"...","内容描述":"...","是否已入库":false}]}}'
1316
+ '{"meta":{"kind":"directions","qid":"<qid>","directions":[{"id":"d1","title":"...","method":"...","core_assumption":"...","feasibility":0.5}],"methods_used":[{"id":"m-...","效果":"...","建议":"..."}],"new_inventions":[{"类型":"方法|工具|...","标题":"...","内容描述":"...","是否已入库":false}]}}' +
1317
+ (formalOn() ? '\n' + formalWorkLine() + formalReplyNote() : '')
1255
1318
  }
1256
1319
  function directionSummary(d) {
1257
1320
  return 'id ' + d.id + '「' + d.title + '」method=' + d.method + ' | round=' + d.round + ' status=' + d.status +
@@ -1297,6 +1360,9 @@ export function apply(ctx) {
1297
1360
  'CHANNEL B (your file tools are unavailable): put the content you would have written into __writes and carry the same meta:\n' +
1298
1361
  '{"__writes":[{"path":"Progress/' + q.id + '/' + dir.id + '.md","content":"<完整本轮叙述>"}],"meta":{"kind":"solver","qid":"' + q.id + '","dirId":"' + dir.id + '",...同上 meta 字段...}}\n' +
1299
1362
  '区分规则:methods_used 只能填**已存在的方法卡 ID**(m-…,来自 AVAILABLE METHODS 列表)——引用你自己刚想出的新方法/新技巧不属于 methods_used,请如实填入 new_inventions(它会由 Method Keeper 蒸馏建卡);不要把方法名/标题当 id 填进 methods_used。'
1363
+ // 顺手形式化(契约 §6.2):把常用/可复用的对象、假设、新定义沉淀到全局 Lean 库;
1364
+ // 回执里同样要带上 formal 难度判断字段(契约 §6.3)。
1365
+ if (formalOn()) head += '\n' + formalWorkLine() + formalReplyNote()
1300
1366
  return head
1301
1367
  }
1302
1368
  function verifierTargetText(r) {
@@ -1305,6 +1371,7 @@ export function apply(ctx) {
1305
1371
  return 'PROBLEM (id: ' + r.qid + '): ' + r.概述 + '\nSOLUTION TO CHECK:\n' + r.process
1306
1372
  }
1307
1373
  function verifierReviewPrompt(r) {
1374
+ const target = verifyTargetId(r)
1308
1375
  return personaText('verifierPersona') + 'You are a STRICT peer reviewer verifying one mathematical object. Check it multiple times.\n\nTARGET (r: ' + r.kind + '):\n' + verifierTargetText(r) + '\n' +
1309
1376
  knowledgeContextText('verifier') +
1310
1377
  capabilitiesText('verifier') +
@@ -1312,10 +1379,14 @@ export function apply(ctx) {
1312
1379
  '\nCalibration: 0.5 means "genuinely undecided — there is a real unresolved gap"; it is NOT a safe hedge, so do not default to 0.5. Give the number your honest confidence from the evidence actually supports.\n' +
1313
1380
  '\n**Reason is MANDATORY and MUST be non-empty**: name the exact step you verified, or the potential counterexample / fatal flaw, or (for 0.5) the precise gap that blocks a decision. A Result with an empty Reason is non-contributory and will be ignored; never return {"Result":0.5} with no justification.\n' +
1314
1381
  '\nCitations: facts may only be cited from Verified/ (or Propos/ 状态: 已验证·真/假). Never cite an unverified or refuted object as a fact — if you need a sub-claim of a refuted card, re-derive it yourself.\n' +
1382
+ // 形式化注入:模式与对象状态都在**构造提示词的这一刻**现算(运行中切档立刻生效)。
1383
+ // 若该对象已有通过的 Lean 证明,这一段把审查对象换成"忠实性",而不是让评审重做推导(契约 §6.1)。
1384
+ (formalOn() ? '\n' + formalPromptBlock(target) + '\n' : '') +
1315
1385
  '\nIndependently output your initial review — ONLY a single JSON object in a ```json code fence, no prose outside it:\n' +
1316
- '{"Result":0.5,"Reason":"<MANDATORY, non-empty: your detailed logic chain / potential counterexample / supporting evidence>"}'
1386
+ '{"Result":0.5,"Reason":"<MANDATORY, non-empty: your detailed logic chain / potential counterexample / supporting evidence>"' + formalJsonField(target) + '}'
1317
1387
  }
1318
1388
  function verifierDebatePrompt(r, transcript) {
1389
+ const target = verifyTargetId(r)
1319
1390
  return personaText('verifierPersona') + 'You are one reviewer in a DEBATE ("交流群") about this object.\n\nTARGET:\n' + verifierTargetText(r) + '\n' +
1320
1391
  knowledgeContextText('verifier') +
1321
1392
  capabilitiesText('verifier') +
@@ -1323,8 +1394,10 @@ export function apply(ctx) {
1323
1394
  '\nRespond to the others (agree / rebut / add new evidence, referencing earlier rounds if needed). If you changed your Result because of them, state the reason explicitly. ' +
1324
1395
  'Remember: formal/notation-level flaws in an otherwise correct proof should lower confidence only slightly — a mathematically correct argument is not "uncertain" because of typos; near-consensus is not a deadlock. Undue swing to 0.5 is discouraged: a bare review merits 0.5 ONLY if there is a genuine undecidable gap, never as a hedge.\n' +
1325
1396
  '\nReason is MANDATORY and MUST be non-empty; an empty-Reason result (esp. a bare 0.5) is ignored as non-contributory, so always justify your number.\n' +
1397
+ // 辩论轮同样现算:对象已 Lean 通过时,辩论的题目是**忠实性**,不是重新推导。
1398
+ (formalOn() ? '\n' + formalPromptBlock(target) + '\n' : '') +
1326
1399
  '\nReply with ONLY a single JSON object in a ```json code fence, no prose outside it:\n' +
1327
- '{"Result":0.5,"Reason":"<MANDATORY, non-empty: updated logic chain / counterexample / proof / refutation>","changed":"brief reason if you changed your Result, else null"}'
1400
+ '{"Result":0.5,"Reason":"<MANDATORY, non-empty: updated logic chain / counterexample / proof / refutation>","changed":"brief reason if you changed your Result, else null"' + formalJsonField(target) + '}'
1328
1401
  }
1329
1402
  function plannerPrompt(brief) {
1330
1403
  return personaText('plannerPersona') + 'You are the SCHEDULING PLANNER of a multi-agent mathematical research system. Your job: autonomously choose the OPTIMAL schedule — you may lay out the NEXT ' + params.planningHorizon + ' agent-task calls in one plan (they will be executed in order, beyond-capacity ones queued for later ticks).\n\n' +
@@ -1346,6 +1419,9 @@ export function apply(ctx) {
1346
1419
  knowledgeContextText('method-keeper') +
1347
1420
  '\nRECENT WORK DIGEST:\n' + digest + '\n\n' +
1348
1421
  'For each pending invention decide: create a NEW method card, or fold it into an EXISTING method (as an improvement). Only list 可信断言 for claims already verified (ids from Verified/) — everything else stays 经验 (experiential). You may propose 上级体系/子方法 links to organize methods into systems.\n' +
1422
+ // Method Keeper 的职责正是「沉淀可复用方法」,所以形式化的沉淀也归它:可复用的定义/假设
1423
+ // 进全局 Lib/,已成立的引理进 Proved/,让后续项目的证明直接 import 复用(契约 §6.2)。
1424
+ (formalOn() ? formalWorkLine() + '\n【方法沉淀 × Lean 形式化】除了方法卡,你沉淀的每个可复用对象 / 定义 / 假设都应当归档到全局 Lean 库(vibe_math_lean_archive kind=\'def\'),已成立的引理归档到 Proved/(kind=\'lemma\');归档时**连同定义与陈述一起写清**,方便后续直接 import。\n' : '') +
1349
1425
  'OUTPUT CONTRACT — pick ONE channel. Write method cards into Markdown; only the created IDs, which cards were used, and improvements cross the machine reply.\n' +
1350
1426
  'CHANNEL A (recommended, you can write files): write each method card into `Methods/<m-id>.md` (`# 方法|标题` + `- 标题/ID/类型/状态/可信断言/适用场景` + `## 核心内容`/`## 应用记录`/`## 改进历史`), then reply ONLY this metadata:\n' +
1351
1427
  '{"meta":{"kind":"methods","used":[{"id":"m-...","效果":"...","建议":"..."}],"created":["m-xxx"],"improvements":[{"id":"m-...","改进内容":"...","原因":"..."}]}}\n' +
@@ -1459,32 +1535,51 @@ export function apply(ctx) {
1459
1535
  for (const q of allProblems()) {
1460
1536
  if ((q.solutions || []).some(function (s) { return s.prob === 1 })) {
1461
1537
  const wasSolved = q.状态 === '已解决'
1462
- q.状态 = '已解决'; q.优先级 = 'never'
1463
- // 先置状态再写卡(writeVerifiedProblemCardIfNeeded 依赖 状态=已解决 才能生成卡)
1464
- if (!wasSolved) { changed = true; await writeVerifiedProblemCardIfNeeded(q) }
1538
+ // require 门禁(契约 §8):自报概率 1 是另一条"宣告定论"的入口,同样必须过门。
1539
+ // 不改变对象的既有权重/概率字段,只把结果记为未定论 + 形式化待办。
1540
+ if (!wasSolved && formalBlocksConclusion(q.id)) await deferForFormal(q.id, formalRequiredWhy(q.id), true)
1541
+ else {
1542
+ q.状态 = '已解决'; q.优先级 = 'never'
1543
+ // 先置状态再写卡(writeVerifiedProblemCardIfNeeded 依赖 状态=已解决 才能生成卡)
1544
+ if (!wasSolved) { changed = true; await writeVerifiedProblemCardIfNeeded(q) }
1545
+ }
1465
1546
  }
1466
1547
  }
1467
1548
  for (const p of allPropos()) {
1468
1549
  let pChanged = false
1469
1550
  const proofOne = (p.proofs || []).some(function (x) { return x.prob === 1 })
1470
1551
  const refuteOne = (p.refutes || []).some(function (x) { return x.prob === 1 })
1471
- if (proofOne && p.概率 !== 1) { p.概率 = 1; p.状态 = '已验证·真'; pChanged = true }
1472
- else if (refuteOne && p.概率 !== 0) { p.概率 = 0; p.状态 = '已验证·假'; pChanged = true }
1473
- if ((p.概率 === 1 || p.概率 === 0) && p.优先级 !== 'never') { p.优先级 = 'never'; pChanged = true }
1474
- if (p.概率 === 1 || p.概率 === 0) {
1475
- if (await writeVerifiedPropositionCardIfNeeded(p)) pChanged = true
1476
- // 关闭晋升/判断出的"僵尸"问题
1477
- for (const q of allProblems()) {
1478
- if (q.状态 === '已解决') continue
1479
- if (q.判断命题 === p.id || (q.来源命题 === p.id) || ((q.来源 === 'promote' || q.来源 === 'judge') && q.来源与动机 && q.来源与动机.indexOf(p.id) !== -1)) { q.状态 = '已解决'; q.优先级 = 'never'; changed = true }
1552
+ const wouldConclude = (proofOne && p.概率 !== 1) || (refuteOne && p.概率 !== 0)
1553
+ if (wouldConclude && formalBlocksConclusion(p.id)) {
1554
+ // 门禁不通过:不写 概率/状态,不写 Verified/ 卡片,只记未定论 + 待办(幂等,不刷屏)。
1555
+ await deferForFormal(p.id, formalRequiredWhy(p.id), proofOne)
1556
+ } else {
1557
+ if (proofOne && p.概率 !== 1) { p.概率 = 1; p.状态 = '已验证·真'; pChanged = true }
1558
+ else if (refuteOne && p.概率 !== 0) { p.概率 = 0; p.状态 = '已验证·假'; pChanged = true }
1559
+ if ((p.概率 === 1 || p.概率 === 0) && p.优先级 !== 'never') { p.优先级 = 'never'; pChanged = true }
1560
+ if (p.概率 === 1 || p.概率 === 0) {
1561
+ if (await writeVerifiedPropositionCardIfNeeded(p)) pChanged = true
1562
+ // 关闭晋升/判断出的"僵尸"问题
1563
+ for (const q of allProblems()) {
1564
+ if (q.状态 === '已解决') continue
1565
+ if (q.判断命题 === p.id || (q.来源命题 === p.id) || ((q.来源 === 'promote' || q.来源 === 'judge') && q.来源与动机 && q.来源与动机.indexOf(p.id) !== -1)) { q.状态 = '已解决'; q.优先级 = 'never'; changed = true }
1566
+ }
1480
1567
  }
1481
1568
  }
1482
1569
  if (pChanged) { await saveProposition(p); changed = true }
1483
1570
  }
1484
1571
  if (changed) { for (const q of allProblems()) await saveProblem(q); logActivity('update', 'status updates applied (probability-1 closures / verified cards)') }
1485
1572
  }
1573
+ /** require 门禁的机器可读原因(写进 Formal/TODO.md 与 Formal/Index.md)。 */
1574
+ function formalRequiredWhy(target) {
1575
+ const rec = formalOf(target)
1576
+ return 'formal-required:尚未取得 Lean 形式化通过,也没有显式阻塞记录(当前状态 ' + (rec.status || 'none') + ')'
1577
+ }
1486
1578
  async function writeVerifiedPropositionCardIfNeeded(p) {
1487
1579
  if (p.概率 !== 1 && p.概率 !== 0) return false
1580
+ // 写 Verified 卡片是"宣告定论"的收口点:require 模式下没过门就绝不写(防御性双保险,
1581
+ // 正常路径已在 settleVerdict / processStatusUpdates 拦住)。
1582
+ if (formalBlocksConclusion(p.id)) return false
1488
1583
  const proofs1 = (p.proofs || []).filter(function (x) { return x.prob === 1 })
1489
1584
  const refutes1 = (p.refutes || []).filter(function (x) { return x.prob === 1 })
1490
1585
  const parts = []
@@ -1495,6 +1590,7 @@ export function apply(ctx) {
1495
1590
  }
1496
1591
  async function writeVerifiedProblemCardIfNeeded(q) {
1497
1592
  if (q.状态 !== '已解决') return false
1593
+ if (formalBlocksConclusion(q.id)) return false
1498
1594
  const sols1 = (q.solutions || []).filter(function (s) { return s.prob === 1 })
1499
1595
  const parts = []
1500
1596
  for (let i = 0; i < sols1.length; i++) parts.push('【解法 #' + (i + 1) + '】' + (sols1[i].text || ''))
@@ -1505,6 +1601,12 @@ export function apply(ctx) {
1505
1601
  async function writeVerifiedCardIfChanged(card) {
1506
1602
  const rel = verifiedRel(card) // 必须与 saveVerified 用同一路径,否则读侧永远读不到已写出的卡
1507
1603
  const existing = await readText(rel)
1604
+ // 最后一道闸门:require 模式下没有 passed/blocked 记录就不允许**新写** Verified 卡片。
1605
+ // 卡片已经存在(切到 require 之前就已定论)只做刷新,不算"新的定论",因此不记待办。
1606
+ if (formalBlocksConclusion(card.id)) {
1607
+ if (existing === undefined) await deferForFormal(card.id, formalRequiredWhy(card.id), card.结论 === true)
1608
+ return false
1609
+ }
1508
1610
  const md = composeVerifiedMd(card)
1509
1611
  const strip = function (s) { return String(s).split('\n').filter(function (l) { return l.indexOf('- 时间:') !== 0 }).join('\n').trim() }
1510
1612
  if (existing !== undefined && strip(existing) === strip(md)) return false
@@ -1614,6 +1716,12 @@ export function apply(ctx) {
1614
1716
  async function createVerifyTask(c) {
1615
1717
  const rId = c.rId
1616
1718
  if (verifyTaskBusy(rId)) return false
1719
+ // require 门禁的**防空转**(契约 §2「对象留在原库,可形式化后再次提议」):已经被记为
1720
+ // 「形式化待办」、而形式化又还没补齐的对象不再重复表决——重复表决只会一次又一次被同一道
1721
+ // 门拦下(同一 rId 每 tick 重建任务、每轮再派验证器),白白耗尽验证预算。一旦 passed /
1722
+ // blocked 落库,门禁条件满足,候选自然重新出现并继续验证(无需人工干预)。
1723
+ const gateTarget = String(c.pId || c.qid || '')
1724
+ if (formalBlocksConclusion(gateTarget) && formalTodo().some(function (t) { return t && t.id === gateTarget })) return false
1617
1725
  tasks['verify:' + rId] = { id: 'verify:' + rId, type: 'verify', r: c, rId: rId, status: 'spawning', children: [], childResults: {}, history: [], round: 1, expectedCount: Math.max(2, params.verifierCount), createdAt: now() }
1618
1726
  logActivity('verify', 'verification task created for ' + rId)
1619
1727
  await saveAll()
@@ -1913,8 +2021,10 @@ export function apply(ctx) {
1913
2021
  if (activeCount() >= params.maxParallelThreshold) break
1914
2022
  const c = cands[i]
1915
2023
  if (verifyTaskBusy(c.rId)) continue
1916
- await createVerifyTask(c)
1917
- return // one per tick keeps scheduling simple
2024
+ // 只有**真的建了任务**才吃掉本轮的名额:require 门禁的防空转会跳过待办对象,
2025
+ // 若照旧 `return`,排在这些对象后面的候选会被永久饿死(每次都轮到同一个被跳过的对象)。
2026
+ const created = await createVerifyTask(c)
2027
+ if (created) return // one per tick keeps scheduling simple
1918
2028
  }
1919
2029
  }
1920
2030
  // 3) solve: explorer / solver spawns (manual → gate)
@@ -1961,6 +2071,9 @@ export function apply(ctx) {
1961
2071
  async function handleExplorer(childId, meta, output) {
1962
2072
  delete agentRegistry[childId]
1963
2073
  const parsed = parseJson(output)
2074
+ // 回执里的 formal 字段(契约 §6.3):即使一次 Lean 工具都没调用,代理也必须能留下显式的
2075
+ // 形式化难度判断;meta.formal 与顶层 formal 两种写法都接受。
2076
+ if (parsed) await absorbFormalFromReply(parsed, childId)
1964
2077
  // 新协议(代理直接写 md + sync_meta):__writes 落盘,meta.kind==='directions' 走元数据同步
1965
2078
  if (parsed && ((Array.isArray(parsed.__writes) && parsed.__writes.length) || (parsed.meta && parsed.meta.kind === 'directions'))) {
1966
2079
  await applyAgentWrites(parsed.__writes)
@@ -1985,6 +2098,7 @@ export function apply(ctx) {
1985
2098
  async function handleSolver(childId, meta, output, stopReason) {
1986
2099
  const qid = meta.qid; const dirId = meta.direction
1987
2100
  const parsed = parseJson(output)
2101
+ if (parsed) await absorbFormalFromReply(parsed, childId)
1988
2102
  // 新协议(代理直接写 md + sync_meta):__writes 落盘,meta.kind==='solver' 走元数据同步
1989
2103
  if (parsed && ((Array.isArray(parsed.__writes) && parsed.__writes.length) || (parsed.meta && parsed.meta.kind === 'solver'))) {
1990
2104
  delete agentRegistry[childId]
@@ -2288,6 +2402,576 @@ export function apply(ctx) {
2288
2402
  }
2289
2403
  }
2290
2404
 
2405
+ // ================= Lean 形式化验证 =================
2406
+ // 契约:docs/formal-verification.md(v2/v3/v4/v5 共用;实现方案.md §10.1/§11.1)。
2407
+ //
2408
+ // 这套机制要换掉的是**审查对象**,不是给代理加一道苦役:多代理交叉验证的本质是**共识**——
2409
+ // m 个人一致认为"这是对的"既排除不了共同误解,也排除不了共同漏掉的情形;Lean 把"我认为"
2410
+ // 换成"机器已核对",于是剩下的唯一不确定项收缩成一个人和代理都能有效审查的问题:
2411
+ //
2412
+ // Lean 代码里的定义 / 对象 / 条件 / 假设 / 结论,是否与命题原文完全一致?
2413
+ //
2414
+ // 因此一旦 Lean 运行通过,验证提示词就不再要求重做推导,而是要求**忠实性审查**;
2415
+ // require 模式把这句话变成门禁:一个对象要被判定为真(严格证明)或假(严格反驳),必须
2416
+ // 先达到 `passed`(有 Lean 产物且最近一次运行 exit 0)或 `blocked`(代理给出显式、可审计的
2417
+ // 阻塞原因)。门禁只是兜底:它把裁定记为未定论 + 形式化待办,绝不把系统卡死。
2418
+ const FORMAL_MODES = ['off', 'encourage', 'require']
2419
+ function formalMode() { const m = String(params.formalVerify); return FORMAL_MODES.indexOf(m) !== -1 ? m : 'off' }
2420
+ function formalOn() { return formalMode() !== 'off' }
2421
+ function formalRecords() { return formalState.records }
2422
+ function formalTodo() { return formalState.todo }
2423
+ /**
2424
+ * 对象 id → 形式化记录的键。**空 id 必须是空串**:`idSafe('')` 会回退成 'id',那样一个没写
2425
+ * target 的回执(或空参数)就能在对象表里凭空造出一条名为 `id` 的记录。
2426
+ */
2427
+ function formalId(raw) { const s = String(raw == null ? '' : raw).trim(); return s ? idSafe(s) : '' }
2428
+ function formalOf(target) {
2429
+ const id = formalId(target)
2430
+ if (!id) return { status: 'none' }
2431
+ const r = formalState.records[id]
2432
+ return r || { status: 'none' }
2433
+ }
2434
+ /**
2435
+ * 落一条形式化记录。达到 passed / blocked 时**同时清掉待办**:待办的含义就是"还不满足
2436
+ * require 门禁",留着已形式化完成的对象会让 Formal/TODO.md 永久说谎。
2437
+ */
2438
+ async function putFormal(target, record) {
2439
+ const id = formalId(target)
2440
+ if (!id) return false
2441
+ let todoChanged = false
2442
+ if (record === null) delete formalState.records[id]
2443
+ else {
2444
+ formalState.records[id] = record
2445
+ if (record.status === 'passed' || record.status === 'blocked') {
2446
+ const i = formalState.todo.findIndex(function (x) { return x && x.id === id })
2447
+ if (i !== -1) { formalState.todo.splice(i, 1); todoChanged = true }
2448
+ }
2449
+ }
2450
+ await saveAll()
2451
+ return todoChanged
2452
+ }
2453
+ // `passed` 需要的是**绿过的运行**,不是"归档了一个文件":从未执行过的证明文件什么也没证明。
2454
+ function formalGateOk(rec) { return !!rec && (rec.status === 'passed' || rec.status === 'blocked') }
2455
+ function formalBlocksConclusion(target) { return formalMode() === 'require' && !formalGateOk(formalOf(target)) }
2456
+ function formalStatusLine(target) {
2457
+ const r = formalOf(target)
2458
+ if (r.status === 'passed') return 'Lean 通过(' + (r.proof || r.file || '') + ')'
2459
+ if (r.status === 'blocked') return '阻塞(' + (r.note || '未说明') + ')'
2460
+ if (r.status === 'attempted') return '已尝试未通过'
2461
+ return '未尝试'
2462
+ }
2463
+ /** md 卡片锚点行内容(off 模式或状态 none 时为空 = 不写这一行)。 */
2464
+ function formalAnchorLine(target) {
2465
+ if (!formalOn()) return ''
2466
+ const r = formalOf(target)
2467
+ if (!r || r.status === 'none') return ''
2468
+ return formalStatusLine(target)
2469
+ }
2470
+ const formalTail = function (s, n) { const t = String(s == null ? '' : s); return t.length > n ? t.slice(-n) : t }
2471
+
2472
+ // ---- 路径守卫 ----------------------------------------------------------
2473
+ /**
2474
+ * 纯**词法**归一化绝对路径(折叠 '.', '..' 与重复斜杠),完全不碰文件系统。
2475
+ * 只做 `startsWith(root)` 是不够的:`…/VibeMath/Projects/../../../../etc/evil.lean`
2476
+ * 作为字符串仍然以根开头,解析后却在根外。
2477
+ */
2478
+ function normalizeAbsPath(p) {
2479
+ const parts = String(p == null ? '' : p).replace(/\\/g, '/').split('/')
2480
+ const out = []
2481
+ for (const seg of parts) {
2482
+ if (seg === '') { if (out.length === 0) out.push(''); continue }
2483
+ if (seg === '.') continue
2484
+ if (seg === '..') { if (out.length > 1) out.pop(); continue }
2485
+ out.push(seg)
2486
+ }
2487
+ return out.join('/')
2488
+ }
2489
+ /**
2490
+ * 把 Lean 路径解析成"可证明位于 <VibeMath 根> 之内"的归一化绝对路径,否则 null。
2491
+ * 边界是 **VibeMath 根**而不是项目根:全局可复用库 <VibeMath 根>/Formal/{Lib,Proved}
2492
+ * 按契约 §3 就故意放在项目树之外。越界(爬到 VibeMath 根之上、或无关绝对路径)一律拒绝。
2493
+ */
2494
+ function leanAbsPathFrom(baseAbs, rel) {
2495
+ const raw = String(rel == null ? '' : rel).trim()
2496
+ if (!raw) return null
2497
+ const abs = (raw.charAt(0) === '/' || /^[a-z]:/i.test(raw)) ? raw : baseAbs + '/' + raw.replace(/^\.\//, '')
2498
+ const norm = normalizeAbsPath(abs)
2499
+ const root = normalizeAbsPath(vibeRoot())
2500
+ if (norm !== root && norm.indexOf(root + '/') !== 0) return null
2501
+ return norm
2502
+ }
2503
+ function leanAbsPath(rel) { return leanAbsPathFrom(frameworkRoot(), rel) }
2504
+ function leanAbsPathVibe(rel) { return leanAbsPathFrom(vibeRoot(), rel) }
2505
+ /** 契约 §5.1:`file` 可相对**项目根**或 **<VibeMath 根>**——两处都过同一守卫,先项目后全局。 */
2506
+ async function leanResolveRun(rel) {
2507
+ const cand = leanAbsPath(rel)
2508
+ if (cand !== null && await readTextAbs(cand) !== undefined) return cand
2509
+ const alt = leanAbsPathVibe(rel)
2510
+ if (alt !== null && await readTextAbs(alt) !== undefined) return alt
2511
+ if (cand !== null) return cand
2512
+ return alt
2513
+ }
2514
+
2515
+ // ---- 执行(绝不抛进调度循环)------------------------------------------
2516
+ /**
2517
+ * 在一个 .lean 文件上跑工具链。**任何**失败模式(无 subprocess 服务、工具链不存在、
2518
+ * spawn 失败、超时、非零退出)都变成可读结果:调度循环永远不会因为 Lean 而崩。
2519
+ */
2520
+ async function leanRunFile(relPath, timeoutMs) {
2521
+ const started = now()
2522
+ const rel = String(relPath == null ? '' : relPath).trim()
2523
+ if (!rel) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'file is required' }
2524
+ const abs = await leanResolveRun(rel)
2525
+ if (abs === null) {
2526
+ return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'Lean 文件必须位于 ' + vibeRoot().replace(/\\/g, '/') + '/ 之内(收到 ' + rel + ')' }
2527
+ }
2528
+ if (!/\.lean$/i.test(abs)) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'only .lean files can be executed' }
2529
+ if (await readTextAbs(abs) === undefined) return { ok: false, code: 'V3_NOT_FOUND', message: 'no such file: ' + rel }
2530
+ const sub = subprocessOf()
2531
+ if (sub === undefined || typeof sub.spawn !== 'function') {
2532
+ return { ok: false, code: 'NO_SUBPROCESS', message: 'the host exposes no subprocess service; Lean cannot be executed here', file: rel, ms: 0 }
2533
+ }
2534
+ const cmd = String(params.leanCommand || 'lean')
2535
+ const cap = Math.max(1000, Number(timeoutMs) || Number(params.leanTimeoutMs) || 120000)
2536
+ if (typeof sub.resolveExecutable !== 'function') {
2537
+ return { ok: false, code: 'LEAN_NOT_FOUND', message: 'the host subprocess service exposes no resolveExecutable(); cannot resolve "' + cmd + '" —— 仍可把形式化代码写下来归档,但无法在此宿主上执行', file: rel, ms: now() - started }
2538
+ }
2539
+ let exe
2540
+ try { exe = await sub.resolveExecutable(cmd) } catch (e) {
2541
+ return { ok: false, code: 'LEAN_NOT_FOUND', message: 'cannot resolve "' + cmd + '": ' + String((e && e.message) || e) + ' —— 仍可把形式化代码写下来归档,但无法在此宿主上执行', file: rel, ms: now() - started }
2542
+ }
2543
+ const argv = [exe].concat((Array.isArray(params.leanArgs) ? params.leanArgs : []).map(String)).concat([abs])
2544
+ let handle
2545
+ try {
2546
+ handle = sub.spawn({
2547
+ argv: argv,
2548
+ cwd: frameworkRoot(),
2549
+ stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } },
2550
+ graceMs: cap,
2551
+ })
2552
+ } catch (e) {
2553
+ return { ok: false, code: 'LEAN_SPAWN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
2554
+ }
2555
+ // 超时必须有**主动**兜底:graceMs 只是宿主侧的宽限,契约 §7 要求超时后调用 handle.terminate()。
2556
+ let timedOut = false
2557
+ let timer = null
2558
+ let outcome
2559
+ try {
2560
+ outcome = await Promise.race([
2561
+ handle.done,
2562
+ new Promise(function (resolve) {
2563
+ timer = setTimeout(function () {
2564
+ timedOut = true
2565
+ try { if (typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* best effort */ }
2566
+ resolve({ exitCode: null, signal: 'SIGTERM' })
2567
+ }, cap)
2568
+ }),
2569
+ ])
2570
+ } catch (e) {
2571
+ if (timer) clearTimeout(timer)
2572
+ return { ok: false, code: 'LEAN_RUN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
2573
+ }
2574
+ if (timer) clearTimeout(timer)
2575
+ let out = '', err = ''
2576
+ try { if (handle.collected && handle.collected.stdout) out = handle.collected.stdout.readFrom(0).text } catch (e) { /* best effort */ }
2577
+ try { if (handle.collected && handle.collected.stderr) err = handle.collected.stderr.readFrom(0).text } catch (e) { /* best effort */ }
2578
+ const exitCode = outcome ? outcome.exitCode : null
2579
+ const ms = now() - started
2580
+ const ok = exitCode === 0
2581
+ return {
2582
+ ok: ok, exitCode: exitCode, signal: (outcome && outcome.signal) || null, ms: ms,
2583
+ command: argv.join(' '), file: rel,
2584
+ stdout: formalTail(out, 4000), stderr: formalTail(err, 4000),
2585
+ timedOut: timedOut,
2586
+ code: ok ? undefined : (timedOut ? 'LEAN_TIMEOUT' : 'LEAN_FAILED'),
2587
+ }
2588
+ }
2589
+ /**
2590
+ * 把一次运行记到对象上(契约 §4 状态迁移)。普通运行最多把对象推进到 `attempted`;
2591
+ * `passed` 只能由 lean_archive{kinds:'proof'} 且该文件最近一次运行 ok 产生——否则一次
2592
+ * 顺手跑个无关文件就能把对象"洗白"成已形式化。passed/blocked 不会被普通运行降级。
2593
+ */
2594
+ async function formalSetRun(target, run) {
2595
+ const t = formalId(target)
2596
+ if (!t) return
2597
+ const prev = formalOf(t)
2598
+ const status = (prev.status === 'passed' || prev.status === 'blocked') ? prev.status : 'attempted'
2599
+ await putFormal(t, Object.assign({}, prev, {
2600
+ status: status,
2601
+ file: (run && run.file) || prev.file || '',
2602
+ run: { at: now(), ok: !!(run && run.ok), exitCode: (run && run.exitCode !== undefined) ? run.exitCode : null, ms: (run && run.ms) || 0, stdoutTail: formalTail(run && run.stdout, 800), stderrTail: formalTail(run && run.stderr, 800) },
2603
+ updatedAt: now(),
2604
+ }))
2605
+ }
2606
+
2607
+ // ---- 提示词注入(都在**构造提示词的那一刻**现算,故运行中切模式立刻生效)--------
2608
+ function formalPromptBlock(target) {
2609
+ if (!formalOn()) return ''
2610
+ const mode = formalMode()
2611
+ const rec = target ? formalOf(target) : { status: 'none' }
2612
+ const L = []
2613
+ L.push('【Lean 形式化验证(' + (mode === 'require' ? '强制' : '鼓励') + '模式)】')
2614
+ if (rec.status === 'passed') {
2615
+ // 整套机制的要害:审查对象**变了**。
2616
+ L.push(' · 该对象已有**通过的 Lean 形式化证明**(' + (rec.proof || rec.file || '') + ',最近一次运行 exit 0)。')
2617
+ L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')
2618
+ L.push(' 定义 / 对象 / 条件 / 假设 / 结论是否与命题原文**完全一致**(有偏差就指出偏差),')
2619
+ L.push(' 并据此给出 verdict。')
2620
+ } else if (rec.status === 'blocked') {
2621
+ L.push(' · 该对象已被记录为**形式化阻塞**:' + (rec.note || '未说明') + '。')
2622
+ L.push(' 请复核这个判断是否成立;若你认为其实可以形式化,请指出来并动手做。')
2623
+ } else {
2624
+ L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
2625
+ 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(' 命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。')
2630
+ if (mode === 'require') {
2631
+ L.push(' · **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因(vibe_math_lean_archive')
2632
+ L.push(' kind=\'blocked\' note=… 或回执 formal.note)。若两者都没有,本次裁定不会生效,')
2633
+ L.push(' 会被记为未定论(原因 formal-required)并进入「形式化待办」。')
2634
+ } else {
2635
+ L.push(' · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断。')
2636
+ }
2637
+ }
2638
+ return L.join('\n')
2639
+ }
2640
+ /** 日常提示词里的"顺手形式化"一行(off 模式返回空串 = 一个字都不多)。 */
2641
+ function formalWorkLine() {
2642
+ if (!formalOn()) return ''
2643
+ return '【顺手形式化(' + (formalMode() === 'require' ? '强制' : '鼓励') + ')】把你工作中常用或可能复用的对象、假设、'
2644
+ + '新定义用 Lean 形式化定义并归档到全局可复用库(vibe_math_lean_archive kind=\'def\'),已成立的引理归到 '
2645
+ + (vibeRoot() + '/Formal/Proved/').replace(/\\/g, '/') + '(kind=\'lemma\');写之前先 vibe_math_lean_lib 查重,避免重复定义。'
2646
+ + (formalMode() === 'require'
2647
+ ? '本模式下,任何要定论为真/假的对象都必须先有 Lean 通过或显式阻塞记录。'
2648
+ : '这会让后续的验证与证明省掉大量重复工作。')
2649
+ }
2650
+ /** 回执契约里的 formal 字段(契约 §6.3):非 off 模式必须出现在回执契约里,否则这条通道不可发现。 */
2651
+ function formalJsonField(target) {
2652
+ if (!formalOn()) return ''
2653
+ const id = String(target == null ? '' : target) || '<对象id>'
2654
+ return ',"formal":{"target":"' + id + '","decision":"used|blocked","file":"Formal/' + id + '.lean","note":"难度判断/阻塞原因"}'
2655
+ }
2656
+ /**
2657
+ * 工作轮(solver / explorer)回执契约里的 formal 字段。这些角色的回执本身就是一段 JSON 模板,
2658
+ * 直接改模板尾部容易把示例改成非法 JSON,所以在**契约说明**里给出同样的字段(契约 §6.3),
2659
+ * 框架侧 `absorbFormalFromReply` 同时接受顶层 `formal` 与 `meta.formal`。
2660
+ */
2661
+ function formalReplyNote() {
2662
+ if (!formalOn()) return ''
2663
+ return '\n形式化回执(本模式):若你本轮对某个对象做了形式化难度判断,请在回执里加上 '
2664
+ + '"formal":{"target":"<对象id>","decision":"used|blocked","file":"Formal/<对象id>.lean","note":"难度判断/阻塞原因"}'
2665
+ + '(decision=\'blocked\' 时必须写明 note,否则拒绝记录)。'
2666
+ }
2667
+ /** 从一条代理回执里取出 formal 判断并落库(顶层 formal 或 meta.formal 都接受)。 */
2668
+ async function absorbFormalFromReply(parsed, memberId) {
2669
+ if (!formalOn() || !parsed || typeof parsed !== 'object') return
2670
+ const f = (parsed.formal && typeof parsed.formal === 'object') ? parsed.formal
2671
+ : ((parsed.meta && typeof parsed.meta.formal === 'object') ? parsed.meta.formal : null)
2672
+ if (f) await absorbFormalReply(f, memberId)
2673
+ }
2674
+ /** 对象 id → 关联可验证对象(命题/问题)。用于把表决对象映射到形式化记录。 */
2675
+ function verifyTargetId(r) { return String((r && (r.pId || r.qid)) || '') }
2676
+ /**
2677
+ * 这次裁决会不会**宣告某个对象定论**?返回被宣告对象的 id,否则空串。
2678
+ *
2679
+ * 门禁只管 boolean 裁定(真=严格证明 / 假=严格反驳):近共识的小数(例如 0.95)本来就仍留
2680
+ * 原库为未定论,不受门禁约束(契约 §2)。`prop-proof` 判 0 时并不会立刻把命题定论,它往反
2681
+ * 方向推入一条 prob=1 的证伪;下一次 processStatusUpdates 会在那里被同一道门拦住。
2682
+ */
2683
+ function formalConclusionTarget(r, v) {
2684
+ if (!r) return ''
2685
+ if (r.kind === 'proposition') return (v === 1 || v === 0) ? String(r.pId || '') : ''
2686
+ if (r.kind === 'prop-proof') return v === 1 ? String(r.pId || '') : ''
2687
+ if (r.kind === 'problem-solution') return v === 1 ? String(r.qid || '') : ''
2688
+ return ''
2689
+ }
2690
+
2691
+ // ---- 公告 / 索引 --------------------------------------------------------
2692
+ /**
2693
+ * v3 没有 v5 的群聊通道,对应的"群聊公告"落点是:①活动日志(vibe_math_status.recentActivity /
2694
+ * 报告里可见)②Logs/形式化.md 的追加式公告(人可复核、可 diff)。公告失败绝不影响调度。
2695
+ */
2696
+ async function formalAnnounce(text) {
2697
+ logActivity('formal', text)
2698
+ try {
2699
+ const rel = 'Logs/形式化.md'
2700
+ const prev = await readText(rel)
2701
+ const head = '# 形式化公告|' + currentProject + '\n\n> 由框架维护的追加式公告(形式化定论、归档、require 门禁搁置)。\n\n'
2702
+ await writeText(rel, ((prev === undefined || !String(prev).trim()) ? head : prev) + '- ' + fmtTime() + ' ' + text + '\n')
2703
+ } catch (e) { /* 公告失败不应影响调度 */ }
2704
+ }
2705
+ /** require 模式:本轮裁定**不生效**——记未定论 + 形式化待办 + 公告,而不写 Verified/ 卡片。 */
2706
+ async function deferForFormal(target, why, isTrue) {
2707
+ const t = formalId(target)
2708
+ if (!t) return { deferred: false }
2709
+ const list = formalTodo()
2710
+ const already = list.some(function (x) { return x && x.id === t })
2711
+ if (!already) {
2712
+ // 幂等:processStatusUpdates 每个 tick 都会再试一次同一对象,只有**首次**才写盘 + 公告,
2713
+ // 否则每秒都会往 Formal/TODO.md 和人读公告里追加一遍(日志刷屏)。
2714
+ list.push({ id: t, at: now(), why: why, verdict: isTrue ? 1 : 0, project: currentProject })
2715
+ await writeFormalTodo()
2716
+ await writeFormalIndex()
2717
+ await formalAnnounce('【形式化】' + t + ' 的表决结果为 ' + (isTrue ? '真' : '假') + ',但 **require 模式**要求'
2718
+ + '先有 Lean 通过或显式阻塞记录,因此本轮**不定论**(已记入 Formal/TODO.md)。请完成形式化'
2719
+ + '(vibe_math_lean_archive kind=\'proof\')或记录阻塞原因(kind=\'blocked\')后重新提议验证。')
2720
+ await saveAll()
2721
+ }
2722
+ return { deferred: true, already: already, why: why }
2723
+ }
2724
+ /** status/report 里的形式化摘要:让所办/人能审计"到底拿到了严格证明,还是只有共识"。 */
2725
+ function formalSummary() {
2726
+ const recs = formalRecords()
2727
+ const keys = Object.keys(recs)
2728
+ return {
2729
+ mode: formalMode(),
2730
+ objects: keys.map(function (k) {
2731
+ const r = recs[k] || {}
2732
+ return { target: k, status: r.status || 'none', file: r.file || '', proof: r.proof || '', note: r.note || '', run: r.run ? { ok: r.run.ok, exitCode: r.run.exitCode, ms: r.run.ms } : null }
2733
+ }),
2734
+ passed: keys.filter(function (k) { return (recs[k] || {}).status === 'passed' }),
2735
+ blocked: keys.filter(function (k) { return (recs[k] || {}).status === 'blocked' }),
2736
+ todo: formalTodo().map(function (t) { return { id: t.id, why: t.why || 'formal-required', at: t.at } }),
2737
+ paths: { project: 'Formal/(相对项目根)', lib: 'VibeMath/Formal/Lib/', proved: 'VibeMath/Formal/Proved/', proofs: 'Verified/Lean/' },
2738
+ note: formalOn() ? '' : '未启用(formalVerify = off;可用 vibe_math_set_params 切到 encourage / require)',
2739
+ }
2740
+ }
2741
+ async function writeFormalIndex() {
2742
+ const recs = formalRecords()
2743
+ const keys = Object.keys(recs).sort()
2744
+ const L = ['# Lean 形式化索引|' + currentProject + '|' + fmtTime(), '',
2745
+ '> 本文件由框架维护(工具调用时增量更新;`vibe_math_lean_lib` 会重建)。权威状态在对象记录里。', '',
2746
+ '| 对象 | 状态 | 形式化文件 | 归档证明 | 最近运行 | 难度判断 / 阻塞原因 |', '|---|---|---|---|---|---|']
2747
+ if (!keys.length) L.push('| (暂无) | | | | | |')
2748
+ for (const k of keys) {
2749
+ const r = recs[k] || {}
2750
+ const run = r.run ? (r.run.ok ? 'ok(exit 0,' + ((r.run.ms || 0) / 1000).toFixed(1) + 's)' : 'fail(exit ' + r.run.exitCode + ',' + ((r.run.ms || 0) / 1000).toFixed(1) + 's)') : '—'
2751
+ L.push('| ' + k + ' | ' + (r.status || 'none') + ' | ' + (r.file || '—') + ' | ' + (r.proof || '—') + ' | ' + run + ' | ' + String(r.note || '—').replace(/\|/g, '/').slice(0, 120) + ' |')
2752
+ }
2753
+ L.push('')
2754
+ if (formalTodo().length) {
2755
+ L.push('## 形式化待办(require 模式:定论被搁置)')
2756
+ for (const t of formalTodo()) L.push('- ' + t.id + ' —— ' + (t.why || 'formal-required') + '(' + fmtTime(t.at) + ')')
2757
+ L.push('')
2758
+ }
2759
+ await writeText('Formal/Index.md', L.join('\n'))
2760
+ }
2761
+ async function writeFormalTodo() {
2762
+ const list = formalTodo()
2763
+ const L = ['# 形式化待办|' + currentProject + '|' + fmtTime(), '',
2764
+ '> 这些对象在 `require` 模式下尚不具备「Lean 已通过」或「显式阻塞记录」,因此**定论被搁置**。',
2765
+ '> 完成形式化(vibe_math_lean_archive kind=\'proof\')或记录阻塞原因(kind=\'blocked\')后,重新提议验证即可。', '']
2766
+ if (!list.length) L.push('(暂无)')
2767
+ for (const t of list) L.push('- ' + t.id + '|' + (t.why || 'formal-required') + '|' + fmtTime(t.at))
2768
+ L.push('')
2769
+ await writeText('Formal/TODO.md', L.join('\n'))
2770
+ }
2771
+ /**
2772
+ * 扫描并重建三处索引(项目 Formal/Index.md + 全局 Lib/Index.md + Proved/Index.md)。
2773
+ * 列举**便宜且无副作用**:不跑工具链("我能复用哪些定义"不该触发编译);每次运行的结果记在
2774
+ * formalState.libRuns 里(归档 kind='def'/'lemma' 时 run:true 记录的),作为"最近运行"列。
2775
+ */
2776
+ async function rebuildLeanLibIndexes() {
2777
+ const scan = async function (dirAbs, dirRel) {
2778
+ const rows = []
2779
+ const names = await listFilesAbs(dirAbs)
2780
+ for (const name of names) {
2781
+ if (!/\.lean$/i.test(String(name))) continue
2782
+ const rel = dirRel + '/' + name
2783
+ const txt = (await readTextAbs(dirAbs + '/' + name)) || ''
2784
+ const base = String(name).replace(/\.lean$/i, '')
2785
+ const first = (String(txt).split('\n').filter(function (l) { return l.trim() && !/^\s*(\/\/|--|import)/.test(l) })[0] || '').trim().slice(0, 110)
2786
+ const depend = (String(txt).split('\n').filter(function (l) { return /^\s*import\s+/.test(l) }).map(function (l) { return l.replace(/^\s*import\s+/, '').trim() }).join('、')) || '—'
2787
+ const run = formalState.libRuns[rel]
2788
+ rows.push({ base: base, rel: rel, first: first, depend: depend, run: run ? (run.ok ? 'ok' : 'fail') : '—' })
2789
+ }
2790
+ return rows
2791
+ }
2792
+ const libRows = await scan(vibeRoot() + '/Formal/Lib', 'Lib')
2793
+ await writeTextAbs(vibeRoot() + '/Formal/Lib/Index.md', ['# 可复用 Lean 定义库(跨项目)|' + currentProject, '',
2794
+ '> 写新定义之前先查这里:能复用就不要重新定义。', '',
2795
+ '| 名称 | 文件 | 类别 | 摘要 | 最近运行 |', '|---|---|---|---|---|']
2796
+ .concat(libRows.length ? libRows.map(function (r) { return '| ' + r.base + ' | ' + r.rel + ' | def | ' + r.first.replace(/\|/g, '/') + ' | ' + r.run + ' |' }) : ['| (暂无) | | | | |']).join('\n') + '\n')
2797
+ const provedRows = await scan(vibeRoot() + '/Formal/Proved', 'Proved')
2798
+ await writeTextAbs(vibeRoot() + '/Formal/Proved/Index.md', ['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|' + currentProject, '',
2799
+ '> 这些文件是通过内核检查的引理,可直接 import 复用。', '',
2800
+ '| 名称 | 文件 | 陈述 | 依赖 | 最近运行 |', '|---|---|---|---|---|']
2801
+ .concat(provedRows.length ? provedRows.map(function (r) { return '| ' + r.base + ' | ' + r.rel + ' | ' + r.first.replace(/\|/g, '/') + ' | ' + r.depend.replace(/\|/g, '/') + ' | ' + r.run + ' |' }) : ['| (暂无) | | | | |']).join('\n') + '\n')
2802
+ await writeFormalIndex()
2803
+ await writeFormalTodo()
2804
+ return { lib: libRows.length, proved: provedRows.length, objects: Object.keys(formalRecords()).length }
2805
+ }
2806
+ /**
2807
+ * 形式化记录变化后,把对象 md 卡片上的 `- 形式化:` 锚点改成一致(任务要求)。
2808
+ *
2809
+ * 用**文本级插入/替换**而不是 saveProposition/saveProblem 整卡重写:v3 支持代理直接写 md,
2810
+ * 整卡重写会把代理在调度器最后一次解析之后写进卡里的内容抹掉(v3 自己已有"代理已直接写了
2811
+ * 该命题卡,保留其内容(不覆盖)"的纪律)。位置与 compose*Md 保持一致:紧随 `- 概率:`,
2812
+ * 无概率锚点时紧随 `- 状态:`。
2813
+ */
2814
+ function withFormalAnchor(text, line) {
2815
+ const s = String(text)
2816
+ const idx = s.search(/\n## /)
2817
+ const head = idx === -1 ? s.replace(/\s+$/, '') : s.slice(0, idx)
2818
+ const body = idx === -1 ? '' : s.slice(idx)
2819
+ const kept = head.split('\n').filter(function (l) { return !/^-\s*形式化\s*:/.test(l) })
2820
+ if (line) {
2821
+ let at = -1
2822
+ for (let i = 0; i < kept.length; i++) { if (/^-\s*概率\s*:/.test(kept[i])) { at = i; break } }
2823
+ if (at === -1) for (let i = 0; i < kept.length; i++) { if (/^-\s*状态\s*:/.test(kept[i])) { at = i; break } }
2824
+ if (at === -1) kept.push('- 形式化: ' + line)
2825
+ else kept.splice(at + 1, 0, '- 形式化: ' + line)
2826
+ }
2827
+ const out = kept.join('\n') + body
2828
+ if (out === s) return s
2829
+ return /\n$/.test(out) ? out : out + '\n'
2830
+ }
2831
+ async function upsertFormalAnchor(target) {
2832
+ if (!formalOn()) return false
2833
+ const id = formalId(target)
2834
+ if (!id) return false
2835
+ const p = propos.get(id)
2836
+ const q = problems.get(id)
2837
+ const rel = p ? propositionRel(p) : (q ? problemRel(q) : '')
2838
+ if (!rel) return false
2839
+ const txt = await readText(rel)
2840
+ if (txt === undefined) return false
2841
+ const next = withFormalAnchor(txt, formalAnchorLine(id))
2842
+ if (next === txt) return false
2843
+ await writeText(rel, next)
2844
+ return true
2845
+ }
2846
+
2847
+ // ---- 三个工具的实现 -----------------------------------------------------
2848
+ /**
2849
+ * 在一个 .lean 文件上跑工具链、把结果记到对象上(可选)、刷新索引,并如实回报。
2850
+ * off 模式下也照常工作:人/代理主动调用时它不该失效,只是框架不主动宣传它存在。
2851
+ */
2852
+ async function leanRunTool(memberId, o) {
2853
+ const args = o || {}
2854
+ const run = await leanRunFile(String(args.file || ''), args.timeout_ms)
2855
+ const target = String(args.target || '').trim()
2856
+ if (run.file || run.ok) {
2857
+ if (target) { await formalSetRun(target, run); await upsertFormalAnchor(target) }
2858
+ await writeFormalIndex()
2859
+ }
2860
+ if (run.ok) await formalAnnounce('【形式化】' + (memberId || 'scheduler') + ' 运行 Lean 通过:' + run.file + '(' + ((run.ms || 0) / 1000).toFixed(1) + 's)' + (target ? '|对象 ' + target : ''))
2861
+ return Object.assign({ ok: !!run.ok }, run, {
2862
+ hint: run.ok
2863
+ ? '通过。若是某个对象的证明,请用 lean_archive kind=\'proof\' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind=\'def\'/\'lemma\' 归档到全局库。'
2864
+ : '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 lean_archive kind=\'blocked\' 记录原因。',
2865
+ })
2866
+ }
2867
+ async function leanArchive(memberId, o) {
2868
+ const args = o || {}
2869
+ const kind = String(args.kind || '')
2870
+ const content = typeof args.content === 'string' ? args.content : undefined
2871
+ const from = args.from ? String(args.from) : ''
2872
+ const who = memberId || 'scheduler'
2873
+ // 读来源文件:只在 <VibeMath 根> 之内,且必须存在。
2874
+ const readFrom = async function () {
2875
+ const srcAbs = await leanResolveRun(from)
2876
+ if (srcAbs === null) return { err: 'from must be a .lean file inside the VibeMath root (got ' + from + ')' }
2877
+ const body = await readTextAbs(srcAbs)
2878
+ if (body === undefined) return { err: 'no such file: ' + from }
2879
+ return { body: body }
2880
+ }
2881
+ if (kind === 'def' || kind === 'lemma') {
2882
+ const rawName = String(args.name || '').trim()
2883
+ if (!rawName) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'name is required for a reusable definition/lemma' }
2884
+ const name = idSafe(rawName)
2885
+ let body = content
2886
+ if (body === undefined && from) { const r = await readFrom(); if (r.err) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: r.err }; body = r.body }
2887
+ if (body === undefined) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
2888
+ const dirRel = kind === 'def' ? 'Lib' : 'Proved'
2889
+ const rel = 'Formal/' + dirRel + '/' + name + '.lean'
2890
+ const abs = vibeRoot() + '/' + rel
2891
+ if (!await writeTextAbs(abs, body)) return { ok: false, code: 'V3_WRITE_FAILED', message: 'could not write ' + rel }
2892
+ // 全局库在项目树之外,必须用**绝对路径**执行(相对形式会在项目根里找)。
2893
+ const run = args.run === false ? null : await leanRunFile(abs)
2894
+ if (run && run.file) formalState.libRuns[dirRel + '/' + name + '.lean'] = { ok: !!run.ok, exitCode: run.exitCode === undefined ? null : run.exitCode, ms: run.ms || 0, at: now() }
2895
+ await rebuildLeanLibIndexes()
2896
+ await formalAnnounce('【形式化】' + who + ' 归档了' + (kind === 'def' ? '可复用定义' : '已证引理') + ' `' + name + '` → ' + rel + (run ? '(运行 ' + (run.ok ? '通过' : '未通过') + ')' : '(未运行)'))
2897
+ return { ok: true, kind: kind, name: name, file: rel, run: run || undefined, note: '已并入全局可复用库,后续项目可直接 import 复用' }
2898
+ }
2899
+ if (kind === 'proof') {
2900
+ const rawTarget = String(args.target || '').trim()
2901
+ if (!rawTarget) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'target is required for kind=proof' }
2902
+ const target = idSafe(rawTarget)
2903
+ let body = content
2904
+ 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
+ if (body === undefined) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
2906
+ const workRel = 'Formal/' + target + '.lean'
2907
+ if (!await writeText(workRel, body)) return { ok: false, code: 'V3_WRITE_FAILED', message: 'could not write ' + workRel }
2908
+ const run = await leanRunFile(workRel)
2909
+ const prev = formalOf(target)
2910
+ const passed = !!run.ok
2911
+ const rec = Object.assign({}, prev, {
2912
+ status: passed ? 'passed' : 'attempted',
2913
+ file: workRel,
2914
+ proof: passed ? 'Verified/Lean/' + target + '.lean' : (prev.proof || ''),
2915
+ decision: 'used',
2916
+ note: String(args.note || prev.note || ''),
2917
+ 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
+ updatedAt: now(),
2919
+ })
2920
+ if (passed) await writeText('Verified/Lean/' + target + '.lean', body)
2921
+ await putFormal(target, rec)
2922
+ await upsertFormalAnchor(target)
2923
+ await rebuildLeanLibIndexes()
2924
+ await formalAnnounce('【形式化】' + who + ' 为 ' + target + ' 归档形式化证明 ' + workRel + '(运行 '
2925
+ + (passed ? '**通过**,已归档到 ' + rec.proof + ',验证转为忠实性审查' : '**未通过**:' + formalTail(run.stderr || run.message, 160)) + ')')
2926
+ return { ok: true, kind: kind, target: target, file: workRel, proof: rec.proof, passed: passed, run: run, status: rec.status }
2927
+ }
2928
+ if (kind === 'blocked') {
2929
+ const rawTarget = String(args.target || '').trim()
2930
+ if (!rawTarget) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'target is required for kind=blocked' }
2931
+ const target = idSafe(rawTarget)
2932
+ const note = String(args.note || '').trim()
2933
+ // "因难度决定不做形式化"必须**显式、可审计**:note 空就拒绝,不允许静默跳过。
2934
+ if (!note) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)——"因难度决定不做形式化"必须显式、可审计' }
2935
+ const prev = formalOf(target)
2936
+ await putFormal(target, Object.assign({}, prev, { status: 'blocked', decision: 'blocked', note: note, updatedAt: now() }))
2937
+ await upsertFormalAnchor(target)
2938
+ await rebuildLeanLibIndexes()
2939
+ await formalAnnounce('【形式化】' + who + ' 记录 ' + target + ' 形式化阻塞:' + note)
2940
+ return { ok: true, kind: kind, target: target, status: 'blocked', note: note }
2941
+ }
2942
+ return { ok: false, code: 'V3_INVALID_ARGUMENT', message: "kind must be 'def' | 'lemma' | 'proof' | 'blocked'" }
2943
+ }
2944
+ /**
2945
+ * 回执通道(契约 §3/§6.3):代理即使一次 Lean 工具都没调用,也必须能留下"实现难度判断"。
2946
+ * decision='blocked' 时 note 必填(否则拒绝记录并公告);'used' 记录草稿文件 → attempted。
2947
+ */
2948
+ async function absorbFormalReply(f, memberId) {
2949
+ if (!formalOn() || !f || typeof f !== 'object') return
2950
+ const who = memberId || 'member'
2951
+ const target = formalId(f.target)
2952
+ if (!target) return
2953
+ const decision = String(f.decision || '')
2954
+ if (decision === 'blocked') {
2955
+ const note = String(f.note || '').trim()
2956
+ if (!note) { await formalAnnounce('【形式化】' + who + ' 的 formal.decision=blocked 未写明 note,已**拒绝**记录(难度判断必须显式、可审计)。'); return }
2957
+ await putFormal(target, Object.assign({}, formalOf(target), { status: 'blocked', decision: 'blocked', note: note, updatedAt: now() }))
2958
+ await upsertFormalAnchor(target)
2959
+ await rebuildLeanLibIndexes()
2960
+ await formalAnnounce('【形式化】' + who + ' 通过回执记录 ' + target + ' 形式化阻塞:' + note)
2961
+ return
2962
+ }
2963
+ if (decision === 'used') {
2964
+ const prev = formalOf(target)
2965
+ const file = String(f.file || ('Formal/' + target + '.lean'))
2966
+ await putFormal(target, Object.assign({}, prev, { status: prev.status === 'passed' ? 'passed' : 'attempted', decision: 'used', file: file, updatedAt: now() }))
2967
+ await upsertFormalAnchor(target)
2968
+ await rebuildLeanLibIndexes()
2969
+ await formalAnnounce('【形式化】' + who + ' 通过回执记录 ' + target + ' 形式化草稿:' + file)
2970
+ return
2971
+ }
2972
+ await formalAnnounce('【形式化】' + who + ' 的 formal.decision 只能是 \'used\' 或 \'blocked\'(收到 ' + String(f.decision) + '),已忽略。')
2973
+ }
2974
+
2291
2975
  // ================= verification (验证器) =================
2292
2976
  function consensus(t) { const vs = Object.keys(t.childResults).map(function (cid) { return t.childResults[cid].Result }); if (vs.length === 0) return false; return vs.every(function (v) { return v === 1 }) || vs.every(function (v) { return v === 0 }) }
2293
2977
  function buildTranscript(t) { const parts = []; const cids = Object.keys(t.childResults); for (let i = 0; i < cids.length; i++) { const r = t.childResults[cids[i]]; parts.push('Reviewer ' + i + ': Result=' + r.Result + ' Reason=' + r.Reason) } return parts.join('\n') }
@@ -2296,6 +2980,9 @@ export function apply(ctx) {
2296
2980
  const parsed = parseJson(output)
2297
2981
  const Result = clamp01((parsed && parsed.Result != null) ? parsed.Result : 0.5)
2298
2982
  const Reason = (parsed && parsed.Reason) || ''
2983
+ // 回执通道(契约 §4/§6.3):验证者在回执里给出的难度判断/阻塞原因也要落成正式记录,
2984
+ // 否则提示词里承诺的 formal 字段就是一条"框架收不到"的死通道。
2985
+ if (parsed && parsed.formal && typeof parsed.formal === 'object') await absorbFormalReply(parsed.formal, childId)
2299
2986
  let t = tasks['verify:' + rId]
2300
2987
  if (!t) { t = { id: 'verify:' + rId, type: 'verify', r: { kind: 'proposition', pId: rId, 概述: rId }, rId: rId, status: 'debating', children: [], childResults: {}, history: [], round: 1, expectedCount: Math.max(2, params.verifierCount), createdAt: now() }; tasks[t.id] = t }
2301
2988
  if (!parsed && !scheduler.running) {
@@ -2391,6 +3078,18 @@ export function apply(ctx) {
2391
3078
  verifierAccuracy[cids[i]] = acc
2392
3079
  }
2393
3080
  await writeJson('Logs/Verification/' + t.rId + '_' + Date.now() + '.json', { r: r, verdict: v, results: t.childResults, transcript: buildTranscript(t), history: t.history || [], at: now() })
3081
+ // ── require 门禁(契约 §8)────────────────────────────────────────────────
3082
+ // 判定为真(严格证明)或假(严格反驳)之前必须先有 `passed` 或 `blocked`。门禁放在**改
3083
+ // 对象之前**:不这样就改写不出"不改变对象的既有权重/概率字段"(一旦先把 概率 写成 1,
3084
+ // 下一 tick 的 processStatusUpdates 还会照着 prob=1 的条目再把它写回去)。
3085
+ // 记录票数(上面那份 Logs/Verification 已落盘)→ 记未定论 + 形式化待办 + 群聊公告 → 返回,
3086
+ // 对象留在原库、可形式化后再次提议,系统继续推进,绝不卡死。
3087
+ const gTarget = formalConclusionTarget(r, v)
3088
+ if (gTarget && formalBlocksConclusion(gTarget)) {
3089
+ const d = await deferForFormal(gTarget, formalRequiredWhy(gTarget), v === 1)
3090
+ logActivity('verdict', t.rId + ' = ' + v + ' 被 require 门禁搁置(formal-required;对象 ' + gTarget + ' 尚无 Lean 通过或阻塞记录)' + (d.already ? '(已在待办中)' : ''))
3091
+ return
3092
+ }
2394
3093
  if (r.kind === 'proposition') {
2395
3094
  const p = propos.get(r.pId)
2396
3095
  if (p) {
@@ -2563,6 +3262,7 @@ export function apply(ctx) {
2563
3262
  registeredAgents: Object.keys(agentRegistry).length,
2564
3263
  queuedPlanActions: planQueue.length,
2565
3264
  plannerEnabled: params.plannerEnabled, plannerFails: plannerFails,
3265
+ formal: formalSummary(),
2566
3266
  recentActivity: activityLog.slice(-Math.min(10, Number(params.activityLogCap) || 100)), params: params,
2567
3267
  }
2568
3268
  }
@@ -2611,7 +3311,7 @@ export function apply(ctx) {
2611
3311
  if (!create && !exists) return { ok: false, message: 'project not found: ' + slug }
2612
3312
  if (scheduler.running) await abortScheduler()
2613
3313
  currentProject = slug; await writeCurrentProject(); await ensureDirs()
2614
- params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; planQueue = []; plannerFails = 0; methodLog = { pendingInventions: [], keepCount: 0, lastKeepAt: 0 }; projectLock = { sessionId: '', at: 0 }; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; lastPlanSummary = null; archivedJ = {}; lastIndexWrite = 0
3314
+ params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; planQueue = []; plannerFails = 0; methodLog = { pendingInventions: [], keepCount: 0, lastKeepAt: 0 }; projectLock = { sessionId: '', at: 0 }; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; lastPlanSummary = null; archivedJ = {}; lastIndexWrite = 0; formalState = { records: {}, todo: [], libRuns: {} }
2615
3315
  await loadSettings(); await migrateLegacyParams(); await loadState(); await loadKnowledgeBase(); await saveAll()
2616
3316
  if (params.indexAutoRebuild) await rebuildIndex()
2617
3317
  return { ok: true, project: slug, frameworkRoot: frameworkRoot() }
@@ -2658,6 +3358,29 @@ export function apply(ctx) {
2658
3358
  registerTool('vibe_math_release_write', 'Release the write lock for one target file (relative to the project root). Call after you finished writing it.', objParams({ target: { type: 'string' } }, ['target']), async function (args, agent) { return await releaseWrite(String(args.target || ''), agent) })
2659
3359
  registerTool('vibe_math_sync_meta', 'Report lightweight scheduling metadata after you wrote content to Markdown files (direction status/survival, registered lemma ids, methods_used/new_inventions, new method cards). Content itself stays in the md files; this only keeps the scheduler index/state in sync.', objParams({ meta: { type: 'object' } }, ['meta']), async function (args, agent) { return await syncMeta(args.meta || {}, agent) })
2660
3360
 
3361
+ // ---- Lean 形式化验证工具(契约 docs/formal-verification.md §5)----
3362
+ // **无条件注册**:注册是静态的(与既有 ctx.effect 纪律一致),模式只决定框架是否主动告诉
3363
+ // 代理它们存在;off 模式下人/代理主动调用时照常工作。
3364
+ registerTool('vibe_math_lean_run', '(member) Execute the Lean toolchain on one .lean file inside the workspace and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output. Pass target=<object id> to also record the run against that object.', objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), async function (args, agent) { return await leanRunTool(agent && agent.id, args) })
3365
+ registerTool('vibe_math_lean_archive', '(member) Archive Lean code. kind="def": a REUSABLE definition/object/assumption → the global cross-project library (VibeMath/Formal/Lib). kind="lemma": a machine-checked lemma → VibeMath/Formal/Proved. kind="proof": the formal proof of a project object → Formal/<target>.lean, and (when the run passes) also Verified/Lean/<target>.lean, marking the object Lean-passed. kind="blocked": record an explicit, reasoned "cannot/not worth formalizing" decision (note required).', objParams({ kind: { type: 'string', enum: ['def', 'lemma', 'proof', 'blocked'] }, name: { type: 'string' }, target: { type: 'string' }, content: { type: 'string' }, from: { type: 'string' }, note: { type: 'string' }, run: { type: 'boolean' } }, ['kind']), async function (args, agent) { return await leanArchive(agent && agent.id, args) })
3366
+ registerTool('vibe_math_lean_lib', "(member) List (and by default rebuild) the Lean reuse library: this project's Formal/Index.md, plus the global cross-project Formal/Lib and Formal/Proved indexes. Look here BEFORE writing a new definition so you reuse instead of redefining.", objParams({ refresh: { type: 'boolean' } }), async function (args) {
3367
+ const a = args || {}
3368
+ const r = a.refresh === false
3369
+ ? { lib: null, proved: null, objects: Object.keys(formalRecords()).length }
3370
+ : await rebuildLeanLibIndexes()
3371
+ return {
3372
+ ok: true, mode: formalMode(), rebuilt: a.refresh !== false,
3373
+ counts: r, todo: formalTodo().map(function (t) { return { id: t.id, why: t.why, at: t.at } }),
3374
+ objects: Object.keys(formalRecords()).map(function (k) {
3375
+ const rec = formalRecords()[k] || {}
3376
+ return { target: k, status: rec.status, file: rec.file, proof: rec.proof, note: rec.note }
3377
+ }),
3378
+ paths: { project: 'Formal/(相对项目根)', lib: 'VibeMath/Formal/Lib/', proved: 'VibeMath/Formal/Proved/', proofs: 'Verified/Lean/' },
3379
+ hint: "复用优先:先在 Lib/ 里找现成定义;新定义用 lean_archive kind='def' 归档,已证引理用 kind='lemma'。",
3380
+ verify: (Object.keys(tasks).length ? String(tasks[Object.keys(tasks)[0]].rId || '') : null),
3381
+ }
3382
+ })
3383
+
2661
3384
  // ---- 代理直接写 md 的写锁 + 轻元数据同步(任务2:代理自组织写各自对应路径的 md,避免并发写同一文件) ----
2662
3385
  /**
2663
3386
  * 写锁键 = 归一化后的项目内相对路径。
@@ -2905,6 +3628,11 @@ export function apply(ctx) {
2905
3628
  registerTool('vibe_math_claim_write', 'Acquire the write lock for one target file (relative to the project root). Call before writing a Markdown file directly.', objParams({ target: { type: 'string' } }, ['target']), 'vibe_math_claim_write')
2906
3629
  registerTool('vibe_math_release_write', 'Release the write lock for one target file (relative to the project root).', objParams({ target: { type: 'string' } }, ['target']), 'vibe_math_release_write')
2907
3630
  registerTool('vibe_math_sync_meta', 'After you write content into Markdown files, report ONLY lightweight scheduling metadata to keep the scheduler state in sync (content stays in the md files). meta.kind must be one of:\n- "directions": {qid, directions:[{id,title,method,core_assumption,feasibility}], methods_used:[{id,效果,建议}], new_inventions:[{类型,标题,内容描述,是否已入库}]}\n- "solver": {qid, dirId, round, survival, status:"continue|success|dead-end", dead_end_reason, lemmas:[{id,title,statement,proof,prob,分类,优先级}], methods_used, new_inventions, solution_prob, solution_text, sub_questions:[{q_sub_title,q_sub_statement,assumption_title,assumption_statement}]}\n- "methods": {used:[{id,效果,建议}], created:[ids], improvements:[{id,改进内容,原因}]}', objParams({ meta: { type: 'object' } }, ['meta']), 'vibe_math_sync_meta')
3631
+ // Lean 形式化验证(docs/formal-verification.md):三个工具**无条件注册**——注册是静态的,
3632
+ // 模式只决定框架是否主动告诉代理它们存在。off 模式下人/代理主动调用时照常工作。
3633
+ registerTool('vibe_math_lean_run', '(member) Execute the Lean toolchain on one .lean file inside the workspace and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output. Pass target=<object id> to also record the run against that object.', objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), 'vibe_math_lean_run')
3634
+ registerTool('vibe_math_lean_archive', '(member) Archive Lean code. kind="def": a REUSABLE definition/object/assumption → the global cross-project library (VibeMath/Formal/Lib). kind="lemma": a machine-checked lemma → VibeMath/Formal/Proved. kind="proof": the formal proof of a project object → Formal/<target>.lean, and (when the run passes) also Verified/Lean/<target>.lean, marking the object Lean-passed. kind="blocked": record an explicit, reasoned "cannot/not worth formalizing" decision (note required).', objParams({ kind: { type: 'string', enum: ['def', 'lemma', 'proof', 'blocked'] }, name: { type: 'string' }, target: { type: 'string' }, content: { type: 'string' }, from: { type: 'string' }, note: { type: 'string' }, run: { type: 'boolean' } }, ['kind']), 'vibe_math_lean_archive')
3635
+ registerTool('vibe_math_lean_lib', "(member) List (and by default rebuild) the Lean reuse library: this project's Formal/Index.md, plus the global cross-project Formal/Lib and Formal/Proved indexes. Look here BEFORE writing a new definition so you reuse instead of redefining.", objParams({ refresh: { type: 'boolean' } }), 'vibe_math_lean_lib')
2908
3636
 
2909
3637
  // /vibe slash command (registered once; routed per session)
2910
3638
  ctx.effect(() => commands.register({