dsh-vibe-math 2.2.2 → 2.3.1

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 (43) hide show
  1. package/AUDIT-CHECKLIST.md +61 -3
  2. package/README.md +119 -1
  3. package/RELEASE-NOTES-2.3.0.md +207 -0
  4. package/RELEASE-NOTES-2.3.1.md +134 -0
  5. package/audit-formal-sensitivity.mjs +333 -0
  6. package/audit-persona-sensitivity.mjs +249 -0
  7. package/audit-persona-surface.test.mjs +349 -0
  8. package/audit-v5-integrity.mjs +43 -2
  9. package/audit-v5-sensitivity.mjs +77 -6
  10. package/docs/formal-verification.md +401 -0
  11. package/docs/generate_framework_diagram_v5.mjs +22 -16
  12. package/docs/test-timing.md +79 -0
  13. package/formal-verify-v2.test.mjs +951 -0
  14. package/formal-verify-v3.test.mjs +1031 -0
  15. package/formal-verify-v4.test.mjs +882 -0
  16. package/formal-verify-v5.test.mjs +598 -0
  17. package/package.json +22 -2
  18. package/prompt-corpus-persona/persona-corpus.json +32 -0
  19. package/prompt-corpus-persona/persona-corpus.md +674 -0
  20. package/prompt-corpus-v2/formal-verify-v2.json +394 -0
  21. package/prompt-corpus-v2/formal-verify-v2.md +4250 -0
  22. package/prompt-corpus-v3/formal-verify-v3.json +382 -0
  23. package/prompt-corpus-v3/formal-verify-v3.md +3843 -0
  24. package/prompt-corpus-v4/formal-verify-v4.json +84 -0
  25. package/prompt-corpus-v4/formal-verify-v4.md +255 -0
  26. package/prompt-corpus-v5/prompt-corpus-v5.json +109 -5
  27. package/prompt-corpus-v5/prompt-corpus-v5.md +653 -109
  28. package/prompt-v5-integrity.test.mjs +1158 -984
  29. package/run-tests.mjs +99 -0
  30. package/vibe-math-v2/agent.cordis.yml +40 -2
  31. package/vibe-math-v2/vibe-math-v2.js +811 -21
  32. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +218 -1
  33. package/vibe-math-v3/agent.cordis.yml +46 -2
  34. package/vibe-math-v3/vibe-math-v3.js +810 -21
  35. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +104 -2
  36. package/vibe-math-v4/agent.cordis.yml +46 -4
  37. package/vibe-math-v4/vibe-math-v4.js +744 -15
  38. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +255 -0
  39. package/vibe-math-v5/agent.cordis.yml +41 -5
  40. package/vibe-math-v5/vibe-math-v5.js +621 -9
  41. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +131 -4
  42. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +57 -0
  43. package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +51 -46
@@ -115,6 +115,11 @@ export function apply(ctx) {
115
115
  tickIntervalMs: 2000, // 调度器心跳间隔(毫秒)
116
116
  activityLogCap: 100, // 活动日志保留条数(report.recentActivity 最多显示 30 条)
117
117
  maxExplorerRetries: 3, // explorer 重派生上限(拆方向失败重试次数)
118
+ // ---- Lean 形式化验证(契约:docs/formal-verification.md §1,四架构同名同语义)----
119
+ formalVerify: 'off', // off = 无任何额外要求(真无操作)| encourage = 鼓励但不强制 | require = 强制 + 定论门禁
120
+ leanCommand: 'lean', // 要执行的 Lean 可执行文件(例:'lake')
121
+ leanArgs: [], // 插在文件名之前的附加参数(例:['env','lean'] 配合 leanCommand='lake')
122
+ leanTimeoutMs: 120000, // 单次 Lean 运行的超时上限(毫秒)
118
123
  }
119
124
  let params = Object.assign({}, DEFAULT_PARAMS)
120
125
  let scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null } // activeCount 由 activeCount() 从 agentRegistry 推导,不再作为字段
@@ -211,6 +216,10 @@ export function apply(ctx) {
211
216
  { name: 'tickIntervalMs', type: 'integer', description: '调度器心跳间隔(毫秒):多久扫描一次子代理状态并推进(越小越灵敏、越大越省资源)', suggestion: 2000 },
212
217
  { name: 'activityLogCap', type: 'integer', description: '活动日志保留条数(影响 report.recentActivity 的细节量,报告最多显示 30 条)', suggestion: 100 },
213
218
  { name: 'maxExplorerRetries', type: 'integer', description: 'explorer 拆方向失败的重派生上限(达到后该问题标记为方向耗尽)', suggestion: 3 },
219
+ { name: 'formalVerify', type: 'enum', options: ['off', 'encourage', 'require'], description: 'Lean 形式化验证档位:off=不额外要求(默认,提示词里不出现 Lean);encourage=鼓励按实现难度自行形式化,一旦 Lean 通过则验证重点转为「忠实性审查」;require=同 encourage 且加门禁——对象的 formal.status 未达到 passed/blocked 前,真/假裁定记为未定论(原因 formal-required)并写入 Formal/TODO.md', suggestion: 'off' },
220
+ { name: 'leanCommand', type: 'string', description: '要执行的 Lean 可执行文件(默认 lean;用 lake 时配合 leanArgs=["env","lean"])', suggestion: 'lean' },
221
+ { name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加命令行参数(默认空)', suggestion: [] },
222
+ { name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,默认 120000,最小 1000)', suggestion: 120000 },
214
223
  ]
215
224
 
216
225
  // ================= fs =================
@@ -287,15 +296,23 @@ export function apply(ctx) {
287
296
  return { ok: outcome.exitCode === 0, exitCode: outcome.exitCode }
288
297
  } catch (e) { return { ok: false, error: String((e && e.message) || e) } }
289
298
  }
290
- async function ensureDirs() { const base = frameworkRoot(); const dirs = ['qs', 'Propos', 'Reliable', 'Verified', 'Verification_logs', 'Progress_Logs', 'VibeMath_State']; const paths = [vibeRoot() + '/Projects'].concat(dirs.map(function (d) { return base + '/' + d })); return await runShell(mkdirCmd(paths)) }
299
+ // 目录布局(docs/formal-verification.md §3):
300
+ // 项目内:Formal/(对象形式化工作文件 + Index.md + TODO.md)、Verified/Lean/(归档证明)
301
+ // 全局(**不在项目内**,跨项目复用):<VibeMath 根>/Formal/Lib/(可复用定义)、Formal/Proved/(已证引理)
302
+ async function ensureDirs() {
303
+ const base = frameworkRoot()
304
+ const dirs = ['qs', 'Propos', 'Reliable', 'Verified', 'Verified/Lean', 'Verification_logs', 'Progress_Logs', 'VibeMath_State', 'Formal']
305
+ const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d }))
306
+ return await runShell(mkdirCmd(paths))
307
+ }
291
308
  async function removeFile(rel) { const base = frameworkRoot(); return await runShell(rmCmd(base + '/' + rel)) }
292
309
 
293
310
  // ================= settings =================
294
311
  function sanitizeParams(obj) {
295
312
  const out = {}
296
- const intFields = ['maxParallelThreshold', 'solverMaxRounds', 'directionsPerSolver', 'verifierCount', 'debateMaxRounds', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries']
313
+ const intFields = ['maxParallelThreshold', 'solverMaxRounds', 'directionsPerSolver', 'verifierCount', 'debateMaxRounds', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries', 'leanTimeoutMs']
297
314
  const numFields = ['promoteValueThreshold']
298
- const arrayFields = ['solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny']
315
+ const arrayFields = ['solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny', 'leanArgs']
299
316
  // 整数字段的下界:0 会让对应功能**静默失效**而不是报错——例如 maxParallelThreshold=0 使所有派发
300
317
  // 闸门 (activeCount >= 0) 恒真,此后永不派发任何代理,而 status 仍显示 running:true;
301
318
  // solverMaxRounds=0 让每个方向立刻判死。这里把会让调度停滞/失能的键抬到最小可用值;
@@ -325,6 +342,15 @@ export function apply(ctx) {
325
342
  else if (k === 'solverAllowNetwork' || k === 'verifierAllowNetwork' || k === 'solverAllowScripts' || k === 'verifierAllowScripts') { out[k] = (v === true || v === false || v === '') ? v : DEFAULT_PARAMS[k] }
326
343
  else { out[k] = v }
327
344
  }
345
+ // 非正的 leanTimeoutMs 会让每次 Lean 运行立即"超时",而超时被记成"未通过"——静默失效。
346
+ // 删除该键(回退默认),而不是抬到最小值,与 v5/v4 的既有处理一致。
347
+ if (out.leanTimeoutMs !== undefined && !(out.leanTimeoutMs > 0)) delete out.leanTimeoutMs
348
+ if (out.leanTimeoutMs !== undefined) out.leanTimeoutMs = Math.max(1000, out.leanTimeoutMs)
349
+ // 未知档位一律回退 'off'(无操作),绝不回退到更强的档位:一个拼写错误若静默启用强制形式化,
350
+ // 会让所有结论被门禁拦下。
351
+ if (out.formalVerify !== undefined && ['off', 'encourage', 'require'].indexOf(String(out.formalVerify)) === -1) out.formalVerify = DEFAULT_PARAMS.formalVerify
352
+ // 空白的 leanCommand 会让 resolveExecutable('') 直接失败;回退默认 'lean'。
353
+ if (out.leanCommand !== undefined && !String(out.leanCommand).trim()) out.leanCommand = DEFAULT_PARAMS.leanCommand
328
354
  return out
329
355
  }
330
356
  async function loadSettings() {
@@ -395,6 +421,13 @@ export function apply(ctx) {
395
421
  const va = await readJson('VibeMath_State/verifier_accuracy.json'); if (va) verifierAccuracy = va
396
422
  const tk = await readJson('VibeMath_State/tasks.json'); if (tk) tasks = tk
397
423
  const er = await readJson('VibeMath_State/explorer_retries.json'); if (er) explorerRetries = er
424
+ // 形式化记录(docs/formal-verification.md §4):v2 没有会话投影,这条状态必须自己持久化,
425
+ // 否则一次 resume 就让 require 门禁失忆(已 passed 的对象被再当"未尝试")。
426
+ const fm = await readJson('VibeMath_State/formal.json')
427
+ formalState = {
428
+ records: (fm && fm.records && typeof fm.records === 'object' && !Array.isArray(fm.records)) ? fm.records : {},
429
+ todo: (fm && Array.isArray(fm.todo)) ? fm.todo : [],
430
+ }
398
431
  }
399
432
  async function saveAll() {
400
433
  await writeJson('VibeMath_State/scheduler_state.json', scheduler)
@@ -403,6 +436,7 @@ export function apply(ctx) {
403
436
  await writeJson('VibeMath_State/verifier_accuracy.json', verifierAccuracy)
404
437
  await writeJson('VibeMath_State/tasks.json', tasks)
405
438
  await writeJson('VibeMath_State/explorer_retries.json', explorerRetries)
439
+ await writeJson('VibeMath_State/formal.json', { records: formalRecords(), todo: formalTodo() })
406
440
  scheduler.lastCheckpoint = now()
407
441
  }
408
442
  // 查询类工具(status/setup/report)汇报前重读设置文件:文件是唯一持久化源,可能在会话启动后被用户手改或由本进程外编辑更新。
@@ -424,6 +458,589 @@ export function apply(ctx) {
424
458
  } catch (e) { console.error('vibe-math-v2: params migration failed: ' + String((e && e.message) || e)) }
425
459
  }
426
460
 
461
+ // ================= Lean 形式化验证(契约:docs/formal-verification.md)=================
462
+ // 本节的要点不是"多一道工序",而是**审查对象发生位移**:
463
+ // 多代理共识回答"我们是否都相信它",机器核对的 Lean 开发回答"它是否为真",
464
+ // 于是悬而未决的问题收缩为一件人(和代理)确实能审计的事:
465
+ // Lean 代码里的定义 / 对象 / 条件 / 假设 / 结论,是否与命题原文一致?
466
+ // 所以一旦 Lean 通过,验证提示词不再要求重新推导,而是要求做**忠实性审查**。
467
+ // `require` 档把这句话变成门禁:真/假裁定在对象既非 `passed` 也无显式 `blocked` 记录时
468
+ // 不生效(记为未定论 + 形式化待办),——"按难度自行决定,但必须明说"。
469
+ const FORMAL_MODES = ['off', 'encourage', 'require']
470
+ // 模式是**动态**的:所有与档位相关的提示词文本都在构造提示词的那一刻现算(见 formalPromptBlock /
471
+ // formalWorkLine),绝不写进任何"入职时冻结"的快照,否则切换档位后成员读到的仍是旧指令。
472
+ function formalMode() { const m = String(params.formalVerify == null ? 'off' : params.formalVerify); return FORMAL_MODES.indexOf(m) !== -1 ? m : 'off' }
473
+ function formalOn() { return formalMode() !== 'off' }
474
+ // formal 记录:status/file/proof/decision/note/run/updatedAt(契约 §4)。
475
+ // v2 没有会话投影,这些记录与「形式化待办」一起持久化在 v2 自己的状态文件里
476
+ // (VibeMath_State/formal.json),以便 resume 后仍然可读——否则重启一次门禁就会失忆。
477
+ let formalState = { records: {}, todo: [] }
478
+ function formalRecords() { return (formalState && formalState.records) || {} }
479
+ function formalTodo() { return (formalState && Array.isArray(formalState.todo)) ? formalState.todo : [] }
480
+ function formalOf(target) { const r = formalRecords()[safeId(String(target || ''))]; return r || { status: 'none' } }
481
+ async function saveFormal() { await writeJson('VibeMath_State/formal.json', { records: formalRecords(), todo: formalTodo() }) }
482
+ // `passed` 只表示"形式化代码通过了内核检查",而不是"命题为真"——忠实性仍需 m 票审查(契约 §11)。
483
+ function formalGateOk(rec) { return !!rec && (rec.status === 'passed' || rec.status === 'blocked') }
484
+ function formalRequired() { return formalMode() === 'require' }
485
+ // 卡片/索引里的人类可读形式化状态。同样取合并后的记录(见 formalGateRecord)。
486
+ function formalStatusLine(target) {
487
+ const r = formalGateRecord(target)
488
+ if (r.status === 'passed') return 'Lean 通过(' + (r.proof || r.file || '') + ')'
489
+ if (r.status === 'blocked') return '阻塞(' + (r.note || '未说明') + ')'
490
+ if (r.status === 'attempted') return '已尝试未通过'
491
+ return '未尝试'
492
+ }
493
+ function tailText(s, n) { const t = String(s == null ? '' : s); return t.length > n ? t.slice(-n) : t }
494
+
495
+ /**
496
+ * 纯词法规范化绝对路径(折叠 '.'、'..' 与重复斜杠),**不访问文件系统**。
497
+ * 单纯的 `startsWith(root)` 判断不够:"…/VibeMath/Projects/../../../../etc/evil.lean"
498
+ * 在字符串上仍以 root 开头,解析后却在 root 之外。
499
+ */
500
+ function normalizeAbsPath(p) {
501
+ const parts = String(p == null ? '' : p).replace(/\\/g, '/').split('/')
502
+ const out = []
503
+ for (const seg of parts) {
504
+ if (seg === '') { if (out.length === 0) out.push(''); continue }
505
+ if (seg === '.') continue
506
+ if (seg === '..') { if (out.length > 1) out.pop(); continue }
507
+ out.push(seg)
508
+ }
509
+ return out.join('/')
510
+ }
511
+ /**
512
+ * 把一个 Lean 路径解析成**可证明位于 VibeMath 根之内**的规范化绝对路径,否则 null。
513
+ * 边界是 **VibeMath 根**而不是项目根:全局可复用库 <VibeMath>/Formal/{Lib,Proved} 有意
514
+ * 放在项目树之外,所以"爬出项目但仍在 VibeMath 内"必须合法;而爬出 VibeMath 根之上、
515
+ * 或一个不相干的绝对路径,一律拒绝。所有 Lean 文件访问(run / archive / read)都走这里。
516
+ */
517
+ function leanAbsPath(rel) {
518
+ const raw = String(rel == null ? '' : rel).trim()
519
+ if (!raw) return null
520
+ const abs = (raw.charAt(0) === '/' || /^[a-z]:/i.test(raw)) ? raw : (frameworkRoot() + '/' + raw.replace(/^\.\//, ''))
521
+ const norm = normalizeAbsPath(abs)
522
+ const root = normalizeAbsPath(vibeRoot())
523
+ if (norm !== root && norm.indexOf(root + '/') !== 0) return null
524
+ return norm
525
+ }
526
+ // VibeMath 根相对路径 → 绝对路径。用于**全局**库(它不在当前项目树内)。
527
+ function vibeAbs(rel) { return vibeRoot() + '/' + String(rel).replace(/^\.\//, '') }
528
+
529
+ /**
530
+ * 在一个 .lean 文件上执行工具链。**绝不向调度循环抛异常**:每一种失败模式
531
+ * (无服务 / 无可执行文件 / spawn 失败 / 超时 / 非零退出)都变成可读结果。
532
+ */
533
+ async function leanRunFile(relPath, timeoutMs) {
534
+ const started = now()
535
+ const rel = String(relPath || '').trim()
536
+ if (!rel) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'file is required' }
537
+ // 路径守卫:只允许执行 VibeMath 树内的文件,构造出来的路径不能让我们跑工作区之外的东西。
538
+ const abs = leanAbsPath(rel)
539
+ if (abs === null) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'Lean 文件必须位于 ' + vibeRoot() + '/ 之内(收到 ' + rel + ')' }
540
+ if (!/\.lean$/.test(abs)) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '只有 .lean 文件可以执行' }
541
+ if (await readTextAbs(abs) === undefined) return { ok: false, code: 'V2_NOT_FOUND', message: 'no such file: ' + rel }
542
+ const sub = subprocessOf()
543
+ if (sub === undefined || typeof sub.spawn !== 'function') {
544
+ return { ok: false, code: 'NO_SUBPROCESS', message: 'the host exposes no subprocess service; Lean cannot be executed here', file: rel, ms: 0 }
545
+ }
546
+ const cap = Math.max(1000, Number(timeoutMs) || Number(params.leanTimeoutMs) || 120000)
547
+ let exe
548
+ try {
549
+ exe = await sub.resolveExecutable(String(params.leanCommand || 'lean'))
550
+ } catch (e) {
551
+ return { ok: false, code: 'LEAN_NOT_FOUND', message: 'cannot resolve "' + String(params.leanCommand || 'lean') + '": ' + String((e && e.message) || e) + ' —— 仍可把形式化代码写下来归档,但无法在此宿主上执行', file: rel, ms: now() - started }
552
+ }
553
+ const argv = [exe].concat((Array.isArray(params.leanArgs) ? params.leanArgs : []).map(String)).concat([abs])
554
+ let handle
555
+ try {
556
+ handle = sub.spawn({
557
+ argv: argv,
558
+ cwd: frameworkRoot(),
559
+ stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } },
560
+ graceMs: cap,
561
+ })
562
+ } catch (e) {
563
+ return { ok: false, code: 'LEAN_SPAWN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
564
+ }
565
+ let outcome
566
+ try {
567
+ outcome = await handle.done
568
+ } catch (e) {
569
+ return { ok: false, code: 'LEAN_RUN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
570
+ }
571
+ let out = '', err = ''
572
+ try { if (handle.collected && handle.collected.stdout) out = handle.collected.stdout.readFrom(0).text } catch (e) { /* best effort */ }
573
+ try { if (handle.collected && handle.collected.stderr) err = handle.collected.stderr.readFrom(0).text } catch (e) { /* best effort */ }
574
+ const exitCode = outcome ? outcome.exitCode : null
575
+ const ms = now() - started
576
+ const ok = exitCode === 0
577
+ // 输出截断到 ~4KB 再入库(避免把巨大的编译器输出写进状态)。
578
+ return {
579
+ ok: ok, exitCode: exitCode, signal: (outcome && outcome.signal) || null, ms: ms,
580
+ command: argv.join(' '), file: rel,
581
+ stdout: tailText(out, 4000), stderr: tailText(err, 4000),
582
+ timedOut: ms >= cap,
583
+ code: ok ? undefined : (ms >= cap ? 'LEAN_TIMEOUT' : 'LEAN_FAILED'),
584
+ }
585
+ }
586
+ // 把一次运行结果写进对象的形式化记录(不提升 status,状态迁移见契约 §4)。
587
+ async function formalSetRun(target, run) {
588
+ const t = safeId(String(target || ''))
589
+ if (!t) return
590
+ const prev = formalOf(t)
591
+ await putFormal(t, Object.assign({}, prev, {
592
+ status: prev.status === 'passed' ? 'passed' : (prev.status === 'blocked' ? 'blocked' : 'attempted'),
593
+ file: run.file || prev.file || '',
594
+ 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) },
595
+ updatedAt: now(),
596
+ }))
597
+ }
598
+ // 写入一条 formal 记录(可选同时更新待办列表)。readJson/writeJson 走项目根,与 v2 其它状态一致。
599
+ async function putFormal(target, record, todo) {
600
+ const t = safeId(String(target || ''))
601
+ const recs = Object.assign({}, formalRecords())
602
+ if (record === null) delete recs[t]; else recs[t] = record
603
+ formalState = { records: recs, todo: Array.isArray(todo) ? todo : formalTodo() }
604
+ await saveFormal()
605
+ }
606
+ /**
607
+ * 归档(对象 id)→ 验证对象(rId)的状态同步。
608
+ *
609
+ * v2 有两个 id 空间,而它们指的是同一个东西:代理用 `vibe_math_lean_archive target=<对象 id>`
610
+ * (`r-pGate` 这种 rId 是调度器内部标识,代理在提示词里看到的是对象 id),而验证提示词
611
+ * 与 require 门禁问的是"**这一次验证对象**"的状态。只写一个键会让两边错位:
612
+ * 对象明明已 passed,门禁却认为 rId 仍未被形式化,裁定被反复搁置。
613
+ * 这里把已知的验证对象记录(`r-<id>`、`r-<id>-pf<n>`、`r-<id>-rf<n>`)一并同步。
614
+ */
615
+ async function syncVerificationTarget(objectId, status, source) {
616
+ const ids = Object.keys(formalRecords()).filter(function (k) { return k === ('r-' + objectId) || k.indexOf('r-' + objectId + '-') === 0 })
617
+ for (const k of ids) {
618
+ const prev = formalRecords()[k] || {}
619
+ await putFormal(k, Object.assign({}, prev, {
620
+ status: status,
621
+ file: source.file || prev.file || '',
622
+ proof: source.proof || prev.proof || '',
623
+ decision: source.decision || prev.decision || '',
624
+ note: source.note || prev.note || '',
625
+ syncedFrom: objectId,
626
+ updatedAt: now(),
627
+ }))
628
+ }
629
+ return ids
630
+ }
631
+ /**
632
+ * 验证 id → 它对应的对象 id(`r-pGate` / `r-pGate-s0` / `r-pGate-pf1` / `r-pGate-rf2` → `pGate`)。
633
+ * v2 有两套 id 空间,这条映射是**唯一**的一处:门禁的合并查询、提示词取记录、以及回执通道
634
+ * (`blocked` / `defect` / `used`)都从这里得到对象 id,绝不另造第二套解析规则。
635
+ */
636
+ function formalObjectIdOf(id) {
637
+ const t = safeId(String(id == null ? '' : id))
638
+ const m = /^r-(.+?)(?:-(?:s\d+|pf\d+|rf\d+))?$/.exec(t)
639
+ return m ? m[1] : t
640
+ }
641
+ /**
642
+ * 门禁/提示词看到的对象状态 = 合并后的记录,**两个方向都要认**,因为 v2 里归档与验证
643
+ * 用的是两套 id,而代理两种写法都会用:
644
+ * · 归档写了对象 id(`pGate`) → 验证对象 `r-pGate` 的查询回退到 `pGate`;
645
+ * · 归档写了验证 id(`r-pGate`)→ 对象 `pGate` 的查询回退到 `r-pGate`。
646
+ * 只做单向就会产生"代理确实形式化了,门禁/提示词却仍按未尝试处理"的假阴性——那正是
647
+ * 这条不变式最容易被写错的地方(审计清单 §3:成对关系只做一半是最常见的缺陷形态)。
648
+ */
649
+ function formalGateRecord(target) {
650
+ const t = safeId(String(target || ''))
651
+ const recs = formalRecords()
652
+ const own = recs[t]
653
+ if (formalGateOk(own)) return own
654
+ // 验证 id → 其对象 id(r-<obj> / r-<obj>-s0 / r-<obj>-pf0 / r-<obj>-rf0)
655
+ const objId = formalObjectIdOf(t)
656
+ if (objId !== t) { const obj = recs[objId]; if (formalGateOk(obj)) return obj }
657
+ // 对象 id → 其验证 id(本对象的第一个验证记录)
658
+ const vr = recs['r-' + t]
659
+ if (formalGateOk(vr)) return vr
660
+ return own || { status: 'none' }
661
+ }
662
+ /**
663
+ * 把同一条状态写进**两套 id 的全部记录**(契约 §8)。门禁、提示词、卡片三处**各读不同的一侧**,
664
+ * 所以只写一侧会造成静默错位:例如 `defect` 只写到 `r-<id>` 上,对象记录仍是 `passed`,
665
+ * 于是 `formalGateRecord` 从对象侧读回 `passed`、索引与卡片照旧写"Lean 通过"——
666
+ * 这正是"成对关系只做一半"的典型缺陷形态(AUDIT-CHECKLIST §3)。
667
+ */
668
+ async function putFormalBothIds(target, patch) {
669
+ const t = safeId(String(target == null ? '' : target))
670
+ if (!t) return []
671
+ const objectId = formalObjectIdOf(t)
672
+ const written = []
673
+ const write = async function (k) {
674
+ if (written.indexOf(k) !== -1) return
675
+ const prev = formalRecords()[k] || { status: 'none' }
676
+ await putFormal(k, Object.assign({}, prev, patch))
677
+ written.push(k)
678
+ }
679
+ await write(t) // 回执点名的那个 id(对象 id 或验证 id)
680
+ if (objectId !== t) await write(objectId) // 另一侧
681
+ // 同一对象的其它验证别名(r-<id>、r-<id>-s0、r-<id>-pf1、r-<id>-rf2):复用既有扫描,不新造映射。
682
+ const aliases = await syncVerificationTarget(objectId, patch.status, patch)
683
+ for (let i = 0; i < aliases.length; i++) await write(aliases[i])
684
+ // 实体化**规范验证别名** r-<对象id>:归档时写对象 id 是常见写法,此时验证侧可能**根本还没有**
685
+ // 记录;只降级对象侧会让"这一次验证"看起来从未被形式化过(门禁查询优先看 rId)。显式写下这条
686
+ // attempted/blocked 记录,两套 id 空间才真正一致(审计清单 §3:成对关系别只做一半)。
687
+ await write('r-' + objectId)
688
+ // syncVerificationTarget 用 `||` 合并 proof,表达不了"清空"——而 defect 恰恰必须清空 proof。
689
+ if (patch.proof === '') {
690
+ for (let i = 0; i < written.length; i++) await putFormal(written[i], Object.assign({}, formalRecords()[written[i]] || {}, { proof: '' }))
691
+ }
692
+ return written
693
+ }
694
+
695
+ // ---- 提示词注入(在构造提示词的那一刻现算;契约 §6)----
696
+ function formalPromptBlock(target) {
697
+ if (!formalOn()) return ''
698
+ const mode = formalMode()
699
+ // 与门禁取同一条记录:验证对象记录优先,其次它对应的对象记录,避免"归档了却仍按未尝试提示"。
700
+ const rec = target ? formalGateRecord(target) : { status: 'none' }
701
+ const L = []
702
+ const vroot = vibeRoot().replace(/\\/g, '/')
703
+ L.push('【Lean 形式化验证(' + (mode === 'require' ? '强制' : '鼓励') + '模式)】')
704
+ if (rec.status === 'passed') {
705
+ // 整个功能的意义所在:审查对象**变了**。证明正确性已由内核保证,剩下的唯一风险是
706
+ // "这段 Lean 代码说的不是我们想说的"。
707
+ // 忠实性的**判定指引**(一致怎么投、发现偏差怎么投、什么情况才能投 0)在 formalVerifySection
708
+ // 里紧随其后给出——两段拼在同一条提示词里,合成契约 §6.1 的完整「忠实性分支」。
709
+ L.push(' · 该对象已有**通过的 Lean 形式化证明**(' + (rec.proof || rec.file) + ',最近运行 exit 0)。')
710
+ L.push(' **你不需要重新检查推导**。你的任务是**忠实性审查**:逐条核对 Lean 代码里的')
711
+ L.push(' 定义 / 对象 / 条件 / 假设 / 结论是否与命题原文**完全一致**。')
712
+ } else if (rec.status === 'blocked') {
713
+ L.push(' · 该对象已被记录为**形式化阻塞**:' + (rec.note || '未说明') + '。')
714
+ L.push(' 请复核这个判断是否成立;若你认为其实可以形式化,请指出来并动手做。')
715
+ } else {
716
+ L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
717
+ L.push(' · 工具:vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库)')
718
+ L.push(' · 工作目录:Formal/(相对项目根);可复用定义放 ' + vroot + '/Formal/Lib/,已证引理放 '
719
+ + vroot + '/Formal/Proved/;写之前先 vibe_math_lean_lib 查重。')
720
+ L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与命题原文逐条一致。'
721
+ + '请把注意力放在这种核对上,而不是重新做一遍推导。')
722
+ if (mode === 'require') {
723
+ L.push(' · **本模式要求**:必须产出 Lean 形式化,或**必须**给出显式的阻塞原因(vibe_math_lean_archive '
724
+ + 'kind=\'blocked\' note=… 或回执 formal.note)。若两者都没有,本次裁定不会生效,会被记为未定论'
725
+ + '(原因 formal-required)并进入「形式化待办」。')
726
+ } else {
727
+ L.push(' · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision=\'blocked\' 时必须写明 note)。')
728
+ }
729
+ L.push(' · 归档可复用定义/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run);跑不通不要入库。')
730
+ L.push(' · 宿主没有 Lean 工具链(LEAN_NOT_FOUND)时:把代码写下来归档,并在回执的 note 里写明'
731
+ + '"宿主无 Lean 工具链"——这算显式阻塞原因,定论门禁可以据此放行。')
732
+ }
733
+ return L.join('\n')
734
+ }
735
+ function formalWorkLine() {
736
+ if (!formalOn()) return ''
737
+ return '【顺手形式化(' + (formalMode() === 'require' ? '强制' : '鼓励') + ')】把你工作中常用或可能复用的对象、假设、'
738
+ + '新定义用 Lean 形式化定义并归档到全局可复用库(vibe_math_lean_archive kind=\'def\'),已成立的引理归到 Formal/Proved/'
739
+ + '(kind=\'lemma\');写之前先 vibe_math_lean_lib 查重,避免重复定义。'
740
+ + '归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。'
741
+ }
742
+ /**
743
+ * 回执契约里的 `formal` 字段(契约 §6.3)。**必须真的出现在回执契约里**:契约写了字段而框架
744
+ * 不解析,就是一条"框架收不到"的死通道(审计清单 §2.2)。`off` 档返回空串,verifier 的 JSON 示例
745
+ * 因此与改动前逐字节一致。
746
+ */
747
+ function formalJsonField(target) {
748
+ if (!formalOn()) return ''
749
+ const id = String(target == null ? '' : target) || '<对象id>'
750
+ return ',"formal":{"target":"' + id + '","decision":"used|blocked|defect","file":"Formal/' + id + '.lean","note":"难度判断/阻塞原因/具体偏差"}'
751
+ }
752
+ /**
753
+ * 工作轮(solver / explorer)回执里的 formal 字段。这些角色的回执契约本身就是一段 JSON 模板,
754
+ * 直接改模板尾部容易把示例改成非法 JSON,所以在**契约说明**里给出同样的字段(与 v3 同一形态);
755
+ * 框架侧 `absorbFormalFromReply` 同时接受顶层 `formal` 与 `meta.formal`。
756
+ */
757
+ function formalReplyNote() {
758
+ if (!formalOn()) return ''
759
+ return '\n形式化回执(本模式):若你本轮对某个对象做了形式化难度判断,或发现已有 Lean 证明与命题原文不符,'
760
+ + '请在回执里加上 "formal":{"target":"<对象id>","decision":"used|blocked|defect","file":"Formal/<对象id>.lean",'
761
+ + '"note":"难度判断/阻塞原因/具体偏差"}(decision=\'blocked\'/\'defect\' 时必须写明 note,否则整条记录被拒绝;'
762
+ + 'decision=\'defect\' 会撤回该证明的「已通过」状态并写入「形式化待办」)。'
763
+ }
764
+ /**
765
+ * 契约 §4.1:`defect` = **形式化不合格**,不是"命题为假"。
766
+ *
767
+ * 表决者逐条核对后发现 Lean 代码与命题原文不一致(写窄了/写宽了/换了对象/漏了条件…),
768
+ * 那是这条形式化写得不对,不是命题被证伪。因此:
769
+ * ① 把形式化记录**降级为 `attempted`**(无论此前是 `passed` 还是 `blocked`)、清空 `proof`;
770
+ * ② 删除归档证明 `Verified/Lean/<id>.lean`(工作文件 `Formal/<id>.lean` 保留,代码不丢);
771
+ * ③ `note` 记入记录与 `Formal/TODO.md`,并在活动日志里公告;
772
+ * ④ `require` 档下 `formalGateOk` 随之为假 → 本次裁定**不定论**,修正形式化并重新跑通后再投票。
773
+ */
774
+ async function formalDefectDowngrade(target, note, who) {
775
+ const t = safeId(String(target || ''))
776
+ const objectId = formalObjectIdOf(t)
777
+ const before = formalRecords()
778
+ const proofs = []
779
+ 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)
782
+ // 两套 id 一起降级——只降一侧会让另一侧继续"已通过",门禁/卡片就会照旧放行。
783
+ await putFormalBothIds(t, { status: 'attempted', decision: 'defect', note: note, proof: '', updatedAt: now() })
784
+ // 删除归档证明:删记录里记着的那份,也删两边 id 直接对应的文件名(覆盖"归档时用对象 id、
785
+ // 回执时用验证 id"这种两侧写法不同的情形)。
786
+ 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) { /* 删除失败不应影响记账 */ } }
790
+ const list = formalTodo().filter(function (x) { return x.id !== objectId })
791
+ list.push({ id: objectId, at: now(), why: 'defect:形式化与命题原文不一致,需修正后重新跑通(' + note + ')' })
792
+ await putFormal(t, Object.assign({}, formalRecords()[t] || {}, { status: 'attempted', decision: 'defect', note: note, proof: '', updatedAt: now() }), list)
793
+ await writeFormalTodo()
794
+ await writeFormalIndex()
795
+ 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 }
798
+ }
799
+ /**
800
+ * 回执通道(契约 §4 / §6.3):代理**即使一次 Lean 工具都没调用**,也必须能留下"实现难度判断"或
801
+ * "忠实性缺陷"。`blocked` / `defect` 的 `note` 必填(决定必须显式、可审计,不允许静默跳过);
802
+ * 缺 note / 缺 target / 非法 decision 一律拒绝(`V2_INVALID_ARGUMENT`)且不留下任何记录。
803
+ */
804
+ async function absorbFormalReply(f, memberId) {
805
+ try {
806
+ if (!formalOn() || !f || typeof f !== 'object') return { ok: false, ignored: true }
807
+ const who = memberId || 'office'
808
+ const rawTarget = String(f.target == null ? '' : f.target).trim()
809
+ const decision = String(f.decision || '')
810
+ if (!rawTarget) {
811
+ // safeId('') 会返回 'anon':没有 target 的回执绝不能凭空造出一条 formal 记录。
812
+ logActivity('formal', '【形式化】' + who + ' 的 formal 回执没有 target,已拒绝(V2_INVALID_ARGUMENT)。')
813
+ return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'formal.target is required' }
814
+ }
815
+ const target = safeId(rawTarget)
816
+ const note = String(f.note || '').trim()
817
+ if (decision === 'blocked' || decision === 'defect') {
818
+ if (!note) {
819
+ logActivity('formal', '【形式化】' + who + ' 的 formal.decision=' + decision + ' 没有写明 note,已拒绝(V2_INVALID_ARGUMENT)——'
820
+ + target + ' 的难度判断/忠实性缺陷必须显式、可审计。')
821
+ return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'formal.decision=' + decision + ' requires a non-empty note (target ' + target + ')' }
822
+ }
823
+ if (decision === 'defect') return await formalDefectDowngrade(target, note, who)
824
+ await putFormalBothIds(target, { status: 'blocked', decision: 'blocked', note: note, updatedAt: now() })
825
+ await writeFormalIndex()
826
+ logActivity('formal', '【形式化】' + who + ' 通过回执记录 ' + target + ' 形式化阻塞:' + note)
827
+ return { ok: true, decision: 'blocked', target: target, status: 'blocked', note: note }
828
+ }
829
+ if (decision === 'used') {
830
+ const file = String(f.file || ('Formal/' + target + '.lean'))
831
+ await putFormalBothIds(target, { status: 'attempted', decision: 'used', file: file, updatedAt: now() })
832
+ await writeFormalIndex()
833
+ logActivity('formal', '【形式化】' + who + ' 通过回执记录 ' + target + ' 形式化草稿:' + file)
834
+ return { ok: true, decision: 'used', target: target, status: 'attempted', file: file }
835
+ }
836
+ logActivity('formal', '【形式化】' + who + ' 的 formal.decision 只能是 \'used\' | \'blocked\' | \'defect\'(收到 '
837
+ + String(f.decision) + '),已忽略(V2_INVALID_ARGUMENT)。')
838
+ return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'formal.decision must be one of used|blocked|defect' }
839
+ } catch (e) {
840
+ // 回执通道**绝不**把异常抛进调度循环:一次记账失败不该吞掉一次表决。
841
+ console.error('vibe-math-v2: absorbFormalReply failed: ' + String((e && e.message) || e))
842
+ return { ok: false, error: String((e && e.message) || e) }
843
+ }
844
+ }
845
+ /** 从一条代理回执里取出 formal 判断并落库(顶层 `formal` 或 `meta.formal` 都接受)。 */
846
+ async function absorbFormalFromReply(parsed, memberId) {
847
+ if (!formalOn() || !parsed || typeof parsed !== 'object') return { ok: false, ignored: true }
848
+ const f = (parsed.formal && typeof parsed.formal === 'object') ? parsed.formal
849
+ : ((parsed.meta && typeof parsed.meta.formal === 'object') ? parsed.meta.formal : null)
850
+ if (!f) return { ok: false, ignored: true }
851
+ return await absorbFormalReply(f, memberId)
852
+ }
853
+
854
+ // ---- 三份索引(框架维护;契约 §9)----
855
+ async function writeFormalIndex() {
856
+ const recs = formalRecords()
857
+ const L = ['# Lean 形式化索引|' + currentProject, '',
858
+ '> 本文件由框架维护(工具调用时增量更新;`vibe_math_lean_lib` 会重建)。权威状态在 VibeMath_State/formal.json 与对象记录里。', '',
859
+ '| 对象 | 状态 | 形式化文件 | 归档证明 | 最近运行 | 难度判断 / 阻塞原因 |', '|---|---|---|---|---|---|']
860
+ const keys = Object.keys(recs)
861
+ if (!keys.length) L.push('| (暂无) | | | | | |')
862
+ for (const k of keys) {
863
+ const r = recs[k] || {}
864
+ 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)') : '—'
865
+ L.push('| ' + k + ' | ' + (r.status || 'none') + ' | ' + (r.file || '—') + ' | ' + (r.proof || '—') + ' | ' + run + ' | ' + String(r.note || '—').replace(/\|/g, '/').slice(0, 120) + ' |')
866
+ }
867
+ L.push('')
868
+ if (formalTodo().length) {
869
+ L.push('## 形式化待办(require 模式)')
870
+ for (const t of formalTodo()) L.push('- ' + t.id + ' —— ' + (t.why || 'formal-required') + '(' + fmtTime(t.at) + ')')
871
+ L.push('')
872
+ }
873
+ await writeText('Formal/Index.md', L.join('\n'))
874
+ }
875
+ async function writeFormalTodo() {
876
+ const list = formalTodo()
877
+ const L = ['# 形式化待办|' + currentProject + '|' + fmtTime(), '',
878
+ '> 这些对象在 `require` 模式下尚不具备「Lean 已通过」或「显式阻塞记录」,因此**定论被搁置**。',
879
+ '> 完成形式化(vibe_math_lean_archive kind=\'proof\')或记录阻塞原因(kind=\'blocked\')后,重新提议验证即可。', '']
880
+ if (!list.length) L.push('(暂无)')
881
+ for (const t of list) L.push('- ' + t.id + '|' + (t.why || 'formal-required') + '|' + fmtTime(t.at))
882
+ L.push('')
883
+ await writeText('Formal/TODO.md', L.join('\n'))
884
+ }
885
+ function fmtTime(ms) { try { return new Date(Number(ms) || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return '' } }
886
+ async function rebuildLeanLibIndexes() {
887
+ // 扫描**不执行**工具链:每次问"有什么可复用"就跑一遍 lean 既慢又出人意料。
888
+ // 每个对象的运行结果存在对象记录里,显示在 Formal/Index.md。
889
+ const scan = async (dirAbs, dirRel, kindLabel) => {
890
+ const rows = []
891
+ try {
892
+ const t = await fs.resolve(dirAbs)
893
+ if (await fs.stat(t) === undefined) return rows
894
+ const entries = await fs.listDir(t)
895
+ for (const e of entries || []) {
896
+ if (!e || e.type !== 'file' || !/\.lean$/.test(String(e.name))) continue
897
+ const rel = dirRel + '/' + e.name
898
+ const txt = (await readTextAbs(dirAbs + '/' + e.name)) || ''
899
+ const name = String(e.name).replace(/\.lean$/, '')
900
+ const first = (txt.split('\n').filter(function (l) { return l.trim() && !/^\s*(\/\/|--|import)/.test(l) })[0] || '').trim().slice(0, 110)
901
+ rows.push('| ' + name + ' | ' + rel + ' | ' + kindLabel + ' | ' + first.replace(/\|/g, '/') + ' |')
902
+ }
903
+ } catch (e) { /* 列表尽力而为 */ }
904
+ return rows
905
+ }
906
+ const libRows = await scan(vibeRoot() + '/Formal/Lib', 'Formal/Lib', 'def')
907
+ await writeTextAbs(vibeRoot() + '/Formal/Lib/Index.md', ['# 可复用 Lean 定义库(跨项目)|' + currentProject, '',
908
+ '> 写新定义之前先查这里:能复用就不要重新定义。', '',
909
+ '| 名称 | 文件 | 类别 | 摘要 |', '|---|---|---|---|']
910
+ .concat(libRows.length ? libRows : ['| (暂无) | | | |']).join('\n') + '\n')
911
+ const provedRows = await scan(vibeRoot() + '/Formal/Proved', 'Formal/Proved', 'lemma')
912
+ await writeTextAbs(vibeRoot() + '/Formal/Proved/Index.md', ['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|' + currentProject, '',
913
+ '> 这些文件是通过内核检查的引理,可直接 import 复用。', '',
914
+ '| 名称 | 文件 | 类别 | 陈述 |', '|---|---|---|---|']
915
+ .concat(provedRows.length ? provedRows : ['| (暂无) | | | |']).join('\n') + '\n')
916
+ await writeFormalIndex()
917
+ await writeFormalTodo()
918
+ return { lib: libRows.length, proved: provedRows.length, objects: Object.keys(formalRecords()).length }
919
+ }
920
+
921
+ // 执行一个 Lean 文件,记录运行结果(可按 target 归属到对象),刷新索引,并**原样**回报结果。
922
+ // 刻意在 `off` 档也照常工作:人要调试自己的工具链时仍然可用。
923
+ async function leanRunTool(memberId, o) {
924
+ const args = o || {}
925
+ const run = await leanRunFile(String(args.file || ''), args.timeout_ms)
926
+ if (run.ok || run.file) {
927
+ if (String(args.target || '').trim()) await formalSetRun(String(args.target), run)
928
+ await writeFormalIndex()
929
+ }
930
+ if (run.ok) logActivity('formal', (memberId || 'office') + ' 运行 Lean 通过:' + run.file + '(' + (run.ms / 1000).toFixed(1) + 's)' + (args.target ? '|对象 ' + args.target : ''))
931
+ return Object.assign({ ok: !!run.ok }, run, {
932
+ hint: run.ok
933
+ ? '通过。若是某个对象的证明,请用 vibe_math_lean_archive kind=\'proof\' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind=\'def\'/\'lemma\' 归档到全局库(归档时会先跑一次,跑不通不要入库)。'
934
+ : '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 vibe_math_lean_archive kind=\'blocked\' 记录原因。',
935
+ })
936
+ }
937
+ async function leanArchive(memberId, o) {
938
+ const args = o || {}
939
+ const kind = String(args.kind || '')
940
+ const content = typeof args.content === 'string' ? args.content : undefined
941
+ const from = args.from ? String(args.from) : ''
942
+ if (kind === 'def' || kind === 'lemma') {
943
+ // 必填校验必须看**原始**输入:v2 的 id 安全化函数 safeId('') 返回 'anon'(非空),
944
+ // 直接用 safeId 的结果做 `!name` 判断就永远为假,缺 name 的归档会静默写成 anon.lean。
945
+ const rawName = String(args.name || '').trim()
946
+ if (!rawName) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'name is required for a reusable definition/lemma' }
947
+ const name = safeId(rawName)
948
+ let body = content
949
+ if (body === undefined && from) {
950
+ const srcAbs = leanAbsPath(from)
951
+ if (srcAbs === null) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'from must be a .lean file inside the VibeMath root (got ' + from + ')' }
952
+ body = await readTextAbs(srcAbs)
953
+ }
954
+ if (body === undefined) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
955
+ const rel = 'Formal/' + (kind === 'def' ? 'Lib' : 'Proved') + '/' + name + '.lean'
956
+ const abs = vibeAbs(rel)
957
+ if (!await writeTextAbs(abs, body)) return { ok: false, code: 'V2_WRITE_FAILED', message: 'could not write ' + rel }
958
+ // 全局库在项目树之外,必须用**绝对路径**执行(相对形式会被解析到项目根之内)。
959
+ const run = args.run === false ? null : await leanRunFile(abs)
960
+ await rebuildLeanLibIndexes()
961
+ 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 复用' }
963
+ }
964
+ if (kind === 'proof') {
965
+ const rawTarget = String(args.target || '').trim()
966
+ if (!rawTarget) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'target is required for kind=proof' }
967
+ const target = safeId(rawTarget)
968
+ let body = content
969
+ if (body === undefined && from) {
970
+ const srcAbs = leanAbsPath(from)
971
+ if (srcAbs === null) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'from must be a .lean file inside the VibeMath root (got ' + from + ')' }
972
+ body = await readTextAbs(srcAbs)
973
+ }
974
+ if (body === undefined) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
975
+ const workRel = 'Formal/' + target + '.lean'
976
+ if (!await writeText(workRel, body)) return { ok: false, code: 'V2_WRITE_FAILED', message: 'could not write ' + workRel }
977
+ const run = await leanRunFile(workRel)
978
+ const prev = formalOf(target)
979
+ const passed = !!run.ok
980
+ const rec = Object.assign({}, prev, {
981
+ status: passed ? 'passed' : 'attempted',
982
+ file: workRel,
983
+ proof: passed ? 'Verified/Lean/' + target + '.lean' : (prev.proof || ''),
984
+ decision: 'used',
985
+ note: String(args.note || prev.note || ''),
986
+ 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
+ updatedAt: now(),
988
+ })
989
+ if (passed) await writeText('Verified/Lean/' + target + '.lean', body)
990
+ await putFormal(target, rec)
991
+ // ★ 让"归档"与"验证对象"两套 id 对齐。
992
+ // 验证提示词与门禁关心的是**这一次验证**(rId,例如 r-pGate),而代理用 vibe_math_lean_archive
993
+ // 归档时给的是**对象 id**(例如 pGate)。若不在这里把验证对象也标成同一状态,就会
994
+ // 出现"对象已 passed、门禁却仍认为 rId 未形式化"的错位:代理明明做了形式化,
995
+ // 裁定还是被反复搁置。两套记录都写,门禁与提示词任取其一都自洽。
996
+ await syncVerificationTarget(target, passed ? 'passed' : 'attempted', rec)
997
+ await rebuildLeanLibIndexes()
998
+ logActivity('formal', (memberId || 'office') + ' 为 ' + target + ' 归档形式化证明 ' + workRel + '(运行 ' + (passed ? '通过,已归档到 ' + rec.proof + ',验证转为忠实性审查' : '未通过:' + tailText(run.stderr || run.message, 160)) + ')')
999
+ return { ok: true, kind: kind, target: target, file: workRel, proof: rec.proof, passed: passed, run: run, status: rec.status }
1000
+ }
1001
+ if (kind === 'blocked') {
1002
+ const rawTarget = String(args.target || '').trim()
1003
+ if (!rawTarget) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'target is required for kind=blocked' }
1004
+ const target = safeId(rawTarget)
1005
+ const note = String(args.note || '').trim()
1006
+ if (!note) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)——"因难度决定不做形式化"必须显式、可审计' }
1007
+ const prev = formalOf(target)
1008
+ const rec = Object.assign({}, prev, { status: 'blocked', decision: 'blocked', note: note, updatedAt: now() })
1009
+ await putFormal(target, rec)
1010
+ await syncVerificationTarget(target, 'blocked', rec)
1011
+ await rebuildLeanLibIndexes()
1012
+ logActivity('formal', (memberId || 'office') + ' 记录 ' + target + ' 形式化阻塞:' + note)
1013
+ return { ok: true, kind: kind, target: target, status: 'blocked', note: note }
1014
+ }
1015
+ return { ok: false, code: 'V2_INVALID_ARGUMENT', message: "kind must be 'def' | 'lemma' | 'proof' | 'blocked'" }
1016
+ }
1017
+ /**
1018
+ * `require` 门禁的判定 + 记账(契约 §8)。返回 true 表示"本次裁定被搁置"。
1019
+ *
1020
+ * 为什么记 `未定论` 而不是"卡住不动":卡住会让整个研究所永久停在一个对象上;记为未定论 +
1021
+ * 待办既保住"未经形式化不得称为严格结论",又保证系统继续推进(与既有"未达门槛留库附平均
1022
+ * 概率"一致)。门禁是**兜底**:正常路径下验证提示词已经先告知表决者要么形式化要么记录阻塞。
1023
+ */
1024
+ async function deferForFormal(target, why) {
1025
+ const t = safeId(String(target || ''))
1026
+ if (!t) return false
1027
+ const rec = formalOf(t)
1028
+ if (!formalRequired()) return false
1029
+ if (formalGateOk(rec)) return false
1030
+ const reason = 'formal-required:尚未取得 Lean 形式化通过,也没有显式阻塞记录(当前状态 ' + (rec.status || 'none') + ')' + (why ? '|' + why : '')
1031
+ const list = formalTodo().filter(function (x) { return x.id !== t })
1032
+ list.push({ id: t, at: now(), why: reason })
1033
+ // 不改动对象的既有权重/概率字段:只补一条 formal 记录(status 保持 none/attempted)。
1034
+ await putFormal(t, rec.status === 'none' ? { status: 'none', deferredAt: now() } : Object.assign({}, rec, { deferredAt: now() }), list)
1035
+ await writeFormalTodo()
1036
+ await writeFormalIndex()
1037
+ // v2 没有群聊文件(没有 Shared/Chat/),所以"群聊公告"落在它的可读通道上:
1038
+ // 活动日志(report.recentActivity 会写进 Progress_Logs/report.json)+ 推送汇报给的提示语。
1039
+ logActivity('formal-gate', '【形式化】' + t + ' 的裁定被 require 模式搁置:' + reason + '(已记入 Formal/TODO.md)')
1040
+ reportDirty = true
1041
+ return true
1042
+ }
1043
+
427
1044
  // ================= data layer: qs.json =================
428
1045
  async function getQs() { const a = await readJson('qs/qs.json'); return Array.isArray(a) ? a : [] }
429
1046
  async function writeQs(list) { await writeJson('qs/qs.json', list) }
@@ -500,6 +1117,14 @@ export function apply(ctx) {
500
1117
  pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }),
501
1118
  registeredAgents: Object.keys(agentRegistry).length,
502
1119
  recentActivity: activityLog.slice(-Math.min(30, Number(params.activityLogCap) || 100)),
1120
+ // Lean 形式化:可调档位与开关同处可读参数表(契约 §1),并附当前形式化记录概况。
1121
+ formal: {
1122
+ mode: formalMode(), required: formalRequired(),
1123
+ leanCommand: params.leanCommand, leanArgs: params.leanArgs, leanTimeoutMs: params.leanTimeoutMs,
1124
+ objects: Object.keys(formalRecords()).map(function (k) { const r = formalRecords()[k] || {}; return { target: k, status: r.status, file: r.file, proof: r.proof, note: r.note } }),
1125
+ todo: formalTodo(),
1126
+ paths: { project: 'Formal/', lib: vibeRoot() + '/Formal/Lib/', proved: vibeRoot() + '/Formal/Proved/', proofs: 'Verified/Lean/' },
1127
+ },
503
1128
  params: params,
504
1129
  }
505
1130
  }
@@ -668,6 +1293,15 @@ export function apply(ctx) {
668
1293
  '- 示例(完整命题 概述):"设函数 f:[0,1]→R 连续,则 f 在 [0,1] 上有界(连续性按 ε-δ 定义,有界性按标准实数分析定义)。" —— 概念与对象定义完整,不引用未定义的记号。\n'
669
1294
  }
670
1295
  function knowledgeContextText() { const k = params.knowledgeContext ? String(params.knowledgeContext) : defaultKnowledgeContext(); return k ? ('\n' + k + '\n') : '' }
1296
+ // 平时工作提示词里的"顺手形式化"段落。**off 档必须返回空串**:off 是真正的无操作,
1297
+ // 提示词里不能出现任何 Lean 字样(contract §2 / §10.1)。
1298
+ function formalWorkSection() {
1299
+ const t = formalWorkLine()
1300
+ if (!t) return ''
1301
+ // 回执契约(契约 §6.3):工作轮也必须被告知 formal 字段,否则"顺手形式化"里做出的难度判断
1302
+ // 无处可写,代理只能沉默——那正是 v2 首版死通道的成因。
1303
+ return '\n' + t + formalReplyNote() + '\n'
1304
+ }
671
1305
  function capabilitiesText(role) {
672
1306
  const maxCalls = role === 'solver' ? params.solverMaxToolCalls : params.verifierMaxToolCalls
673
1307
  const netOn = role === 'solver' ? params.solverAllowNetwork : params.verifierAllowNetwork
@@ -693,6 +1327,7 @@ export function apply(ctx) {
693
1327
  return explorerPersonaText() + 'You are a research mathematician orchestrating strategy for one problem.\n\nPROBLEM (id: ' + q.id + '): ' + q.概述 + '\n\n' +
694
1328
  knowledgeContextText() +
695
1329
  capabilitiesText('solver') +
1330
+ formalWorkSection() +
696
1331
  '\nDo a first-stage METACOGNITIVE BRAINSTORM: decompose constraints, test boundary/extreme cases, map to similar known problems. ' +
697
1332
  'Then propose 3-6 DIVERSE, mutually distinct solution directions (e.g. analytic method, constructive proof, contradiction, numeric approximation + limit passage, categorical abstraction, ...). ' +
698
1333
  'Record each direction with its core assumption and an initial feasibility estimate.\n\n' +
@@ -708,6 +1343,7 @@ export function apply(ctx) {
708
1343
  return explorerPersonaText() + 'You are a research mathematician re-deriving strategy for a problem whose prior directions stalled or failed.\n\nPROBLEM (id: ' + q.id + '): ' + q.概述 + '\n\nPRIOR DIRECTIONS (with blockers):\n' + prior + '\n' +
709
1344
  knowledgeContextText() +
710
1345
  capabilitiesText('solver') +
1346
+ formalWorkSection() +
711
1347
  '\nQuantitatively analyze the historical progress, blocker causes, and feasibility decay of each prior direction. Discard directions already proven to be dead ends (unless a new tool/idea changes that). ' +
712
1348
  'Then deeply DERIVE 1-3 BRAND-NEW directions never tried before, each with a one-line motivation. ' +
713
1349
  'Finally return the UNION of high-potential leftover directions and the brand-new directions as the new direction set M_q (drop dead ends).\n\n' +
@@ -742,6 +1378,7 @@ export function apply(ctx) {
742
1378
  if (round > 1 || (progressText && progressText.length)) head += '\nYOUR PRIOR PROGRESS / OTHER DIRECTIONS:\n' + progressText + '\n'
743
1379
  head += knowledgeContextText()
744
1380
  head += capabilitiesText('solver')
1381
+ head += formalWorkSection()
745
1382
  head += '\nStart from the last recorded node of direction ' + dir.id + ' (inherit progress, or branch a sub-route under it). Each round you MUST produce, even if incomplete:\n' +
746
1383
  '- new lemmas / intermediate conclusions WITH full proofs (these go to the Propos/ knowledge base);\n' +
747
1384
  '- each concrete sub-route tried, its progress overview, an EXPLICIT feasibility signal (e.g. "unremovable singularity", "conflicts with known theorem X"), and any blocker;\n' +
@@ -755,6 +1392,36 @@ export function apply(ctx) {
755
1392
  '{"status":"continue|success|dead-end","solution":"complete solution text, or null","solution_probability":0.85,"lemmas":[{"title":"...","statement":"...","proof":"...","细类型":{"分类名":{}},"布尔估计":0.6,"价值/关键性":0.5,"优先级":1}],"routes":[{"title":"...","progress":"...","feasibility_signal":"...","blocker":"..."}],"lessons":["..."],"survival_probability":0.5,"dead_end_reason":"... or null","sub_questions":[{"q_sub_title":"...","q_sub_statement":"完整问题陈述(含所有对象/定义)","assumption_title":"p_{q-tmp} 标题","assumption_statement":"完整假设陈述(含所有定义)"}]}'
756
1393
  return head
757
1394
  }
1395
+ // 验证提示词里的形式化段落(review 与 debate 两条路径都必须带)。**off 档返回空串**。
1396
+ // 形式化记录以「验证对象」为键(rId,例如 r-p1 或 r-q1-s0)——这正是 vibe_math_lean_archive 的 target,
1397
+ // 于是"归档了证明 → 下一条验证提示词自动切换成忠实性审查"这条因果链闭合。
1398
+ function formalVerifySection(r) {
1399
+ if (!formalOn()) return ''
1400
+ const block = formalPromptBlock(r && r.rId)
1401
+ if (!block) return ''
1402
+ const rec = formalGateRecord(r && r.rId)
1403
+ const L = [block]
1404
+ // 把"审查对象变了"这件事说透:手里已有机器核对过的证明时,重新推导是浪费,
1405
+ // 真正的风险是"这段代码说的不是我们想说的"。
1406
+ if (rec.status === 'passed') {
1407
+ // 忠实性审查的**判定指引**:形式化与命题原文不一致时,那是"形式化不合格",**不是**命题为假。
1408
+ // 让它"发现偏离就投 0"会伪造出一个错误的否定结论(契约 §4.1),所以这里明确禁止投 0,
1409
+ // 并给出 `defect` 回执——框架据此撤回证明、写入待办、本次裁定不定论。
1410
+ L.push(' ▸ 一致 → Result = 1。')
1411
+ L.push(' ▸ **发现任何偏差,不要投 0**:偏差只说明**形式化不合格**,不代表命题为假。此时请:')
1412
+ L.push(" ① Result 给一个严格介于 0 与 1 之间的值(记为弃权),并在 Reason 里写清偏差;")
1413
+ L.push(" ② 用回执 formal:{decision:'defect', note:'<具体偏差>'} 记录它。框架会撤回这条证明的")
1414
+ L.push(' 「已通过」状态(降级为 attempted、删除归档证明、写入形式化待办),本次裁定**不定论**;')
1415
+ L.push(' 修正形式化并重新跑通后再投票。')
1416
+ L.push(' ▸ 只有当你**独立于这份 Lean 代码**也能确定命题为假时,才投 0,并在 Reason 里写清独立理由。')
1417
+ } else if (rec.status === 'blocked') {
1418
+ L.push(' ▸ 因此请把 Result 用在"这个阻塞判断是否成立 / 是否仍有别的形式化路线"上,并给出理由。')
1419
+ } else {
1420
+ L.push(' ▸ 若你在本轮把它形式化并跑通(vibe_math_lean_archive kind=\'proof\'),后续轮次的审查对象')
1421
+ L.push(' 就会从"推导是否正确"变成"Lean 代码是否忠实于命题"。')
1422
+ }
1423
+ return '\n' + L.join('\n') + '\n'
1424
+ }
758
1425
  function verifierReviewPrompt(r) {
759
1426
  let target = ''
760
1427
  if (r.kind === 'proposition') target = 'PROPOSITION (id: ' + r.pId + '): ' + r.概述
@@ -763,9 +1430,10 @@ export function apply(ctx) {
763
1430
  return verifierPersonaText() + 'You are a STRICT peer reviewer verifying one mathematical object. Check it multiple times.\n\nTARGET (r: ' + r.kind + '):\n' + target + '\n' +
764
1431
  knowledgeContextText() +
765
1432
  capabilitiesText('verifier') +
1433
+ formalVerifySection(r) +
766
1434
  '\nResult ∈ [0,1] = your probability that the TARGET is CORRECT: 1 ONLY when you are fully certain (for a bare proposition: Reason must be a complete proof; for a proof/refutation/solution: you verified every step and Reason confirms the whole chain); 0 ONLY when you are certain it is wrong (Reason must be a rigorous complete refutation / pinpoint the fatal flaw); otherwise a value strictly between 0 and 1.\n' +
767
1435
  '\nIndependently output your initial review. Respond with ONLY a single JSON object wrapped in a ```json code fence — no prose:\n' +
768
- '{"Result":0.5,"Reason":"detailed logic chain, potential counterexample, or supporting evidence"}'
1436
+ '{"Result":0.5,"Reason":"detailed logic chain, potential counterexample, or supporting evidence"' + formalJsonField(r && r.rId) + '}'
769
1437
  }
770
1438
  function verifierDebatePrompt(r, transcript) {
771
1439
  let target = ''
@@ -775,10 +1443,11 @@ export function apply(ctx) {
775
1443
  return verifierPersonaText() + 'You are one reviewer in a DEBATE ("交流群") about this object.\n\nTARGET:\n' + target + '\n' +
776
1444
  knowledgeContextText() +
777
1445
  capabilitiesText('verifier') +
1446
+ formalVerifySection(r) +
778
1447
  '\nFULL DEBATE HISTORY SO FAR (每轮所有评审轮流发言的记录):\n' + transcript + '\n' +
779
1448
  '\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.\n' +
780
1449
  'Respond with ONLY a single JSON object wrapped in a ```json code fence — no prose:\n' +
781
- '{"Result":0.5,"Reason":"updated logic chain / counterexample / proof / refutation","changed":"brief reason if you changed your Result, else null"}'
1450
+ '{"Result":0.5,"Reason":"updated logic chain / counterexample / proof / refutation","changed":"brief reason if you changed your Result, else null"' + formalJsonField(r && r.rId) + '}'
782
1451
  }
783
1452
 
784
1453
  // ================= decisions (manual/auto) =================
@@ -855,12 +1524,24 @@ export function apply(ctx) {
855
1524
  } finally { tickInFlight = false }
856
1525
  }
857
1526
  // note 4: probability-1 rules
1527
+ //
1528
+ // ★ 这里同时是 require 门禁在"自动收口"路径上的落点:一个 1/0 概率的对象要先拿到
1529
+ // 卡片写入许可(writeVerifiedCardIfNeeded / writeVerifiedProblemCardIfNeeded)才允许
1530
+ // 真的把 布尔估计 / 已解决 / 优先级 提升上去。否则"搁置"就只是没写卡片,而对象已经
1531
+ // 被标成定论了——门禁形同虚设。
858
1532
  async function processStatusUpdates() {
859
1533
  const qs = await getQs()
860
1534
  let qsChanged = false
861
1535
  for (let i = 0; i < qs.length; i++) {
862
1536
  const q = qs[i]
863
- if (q.解法列表 && q.解法列表.some(function (s) { return s.正确概率 === 1 })) { if (!q.已解决) { qsChanged = true; await writeVerifiedProblemCardIfNeeded(q) } q.已解决 = true; q.优先级 = 'never' }
1537
+ if (q.解法列表 && q.解法列表.some(function (s) { return s.正确概率 === 1 })) {
1538
+ if (!q.已解决) {
1539
+ const card = await writeVerifiedProblemCardIfNeeded(q, true)
1540
+ if (card.deferred) continue // 被门禁搁置:保持未解决、优先级不变(卡片与提升都不写)
1541
+ qsChanged = true
1542
+ }
1543
+ q.已解决 = true; q.优先级 = 'never'
1544
+ }
864
1545
  }
865
1546
  if (qsChanged) { await writeQs(qs); logActivity('update', 'problems marked solved by probability-1 solutions') }
866
1547
  const propos = await getPropos()
@@ -870,11 +1551,18 @@ export function apply(ctx) {
870
1551
  let pChanged = false
871
1552
  const proofOne = (p.证明列表 || []).some(function (x) { return x.正确概率 === 1 })
872
1553
  const refuteOne = (p.证伪列表 || []).some(function (x) { return x.正确概率 === 1 })
873
- if (proofOne && p.布尔估计 !== 1) { p.布尔估计 = 1; pChanged = true }
874
- else if (refuteOne && p.布尔估计 !== 0) { p.布尔估计 = 0; pChanged = true }
1554
+ const targetBE = proofOne ? 1 : (refuteOne ? 0 : null)
1555
+ const atConclusion = targetBE !== null || p.布尔估计 === 1 || p.布尔估计 === 0
1556
+ let cardWritten = false
1557
+ if (atConclusion) {
1558
+ const card = await writeVerifiedCardIfNeeded(p, targetBE === null ? p.布尔估计 : targetBE)
1559
+ if (card.deferred) continue // 被门禁搁置:布尔估计/优先级都不提升,"僵尸"问题也不关(它并未定论)
1560
+ cardWritten = cardOk(card)
1561
+ }
1562
+ if (targetBE !== null && p.布尔估计 !== targetBE) { p.布尔估计 = targetBE; pChanged = true }
875
1563
  if ((p.布尔估计 === 1 || p.布尔估计 === 0) && p.优先级 !== 'never') { p.优先级 = 'never'; pChanged = true }
1564
+ if (cardWritten) pChanged = true
876
1565
  if (p.布尔估计 === 1 || p.布尔估计 === 0) {
877
- if (await writeVerifiedCardIfNeeded(p)) pChanged = true
878
1566
  // 源命题已定论 → 关闭其晋升出的"僵尸"问题(优先用 判断命题 字段;兼容旧文本标记数据)
879
1567
  for (let j = 0; j < qs.length; j++) {
880
1568
  const qj = qs[j]
@@ -1070,6 +1758,8 @@ export function apply(ctx) {
1070
1758
  async function handleExplorer(childId, meta, output) {
1071
1759
  delete agentRegistry[childId]
1072
1760
  const parsed = parseJson(output)
1761
+ // 回执通道(契约 §4 / §6.3):工作轮里做出的形式化难度判断也要落库。
1762
+ await absorbFormalFromReply(parsed, childId)
1073
1763
  const dirs = (parsed && parsed.directions) || []
1074
1764
  if (dirs.length === 0) { logActivity('explorer', 'problem ' + meta.qid + ' returned no directions (output head: ' + String(output || '').slice(0, 200) + ')'); await saveAll(); return }
1075
1765
  explorerRetries[meta.qid] = 0
@@ -1084,6 +1774,8 @@ export function apply(ctx) {
1084
1774
  async function handleSolver(childId, meta, output, stopReason) {
1085
1775
  const qid = meta.qid; const dirId = meta.direction
1086
1776
  const parsed = parseJson(output)
1777
+ // 回执通道(契约 §4 / §6.3):求解器/探索者的"顺手形式化"产物与难度判断都要落库。
1778
+ await absorbFormalFromReply(parsed, childId)
1087
1779
  const q = await findQ(qid); if (!q) { delete agentRegistry[childId]; return }
1088
1780
  const prog = parseProgress(q)
1089
1781
  const dir = prog.directions.find(function (d) { return d.id === dirId })
@@ -1210,6 +1902,10 @@ export function apply(ctx) {
1210
1902
  async function handleVerifier(childId, meta, output, stopReason) {
1211
1903
  const rId = meta.rId
1212
1904
  const parsed = parseJson(output)
1905
+ // 回执通道(契约 §4 / §6.3):验证者在回执里给出的难度判断 / 忠实性缺陷(`defect`)必须先于
1906
+ // 裁定落库——`defect` 会把形式化记录降级,于是紧随其后的 `settleVerdict` 会在 require 档
1907
+ // 把本次裁定正确地记为未定论。放在这里(而不是裁定之后)是有意的顺序依赖。
1908
+ await absorbFormalFromReply(parsed, childId)
1213
1909
  const Result = clamp01((parsed && parsed.Result != null) ? parsed.Result : 0.5)
1214
1910
  const Reason = (parsed && parsed.Reason) || ''
1215
1911
  let t = tasks['verify:' + rId]
@@ -1288,6 +1984,24 @@ export function apply(ctx) {
1288
1984
  }
1289
1985
  return 0.5 // flat = 均衡机制
1290
1986
  }
1987
+ /**
1988
+ * `require` 门禁对**一次具体裁定**的判定(契约 §8)。
1989
+ * 返回 true 表示本次裁定被搁置:把任务记为 `未定论`(原因 formal-required)、
1990
+ * 不写 Verified 卡片、**不改变对象的任何概率/权重字段**,并把对象写入「形式化待办」。
1991
+ * 只有布尔裁定(真/假 = 1/0)受门禁约束;中间值本来就不写卡片,照旧走原路径。
1992
+ */
1993
+ async function formalVerdictDeferred(t, v, vIsBool) {
1994
+ if (!formalRequired() || !vIsBool) return false
1995
+ const gateTarget = safeId(String(t.rId || ''))
1996
+ if (await deferForFormal(gateTarget, '裁定 ' + (v === 1 ? '真' : '假'))) {
1997
+ t.formalDeferred = true
1998
+ t.outcome = 'undecided'
1999
+ t.formalDeferReason = 'formal-required'
2000
+ logActivity('gate', t.rId + ' 的裁定 ' + v + ' 被 require 模式搁置为未定论(formal-required)')
2001
+ return true
2002
+ }
2003
+ return false
2004
+ }
1291
2005
  async function settleVerdict(t, verdict) {
1292
2006
  const v = clamp01(verdict)
1293
2007
  const r = t.r
@@ -1306,6 +2020,8 @@ export function apply(ctx) {
1306
2020
  if (r.kind === 'proposition') {
1307
2021
  const p = await findProposition(r.pId)
1308
2022
  if (p) {
2023
+ // require 门禁:裸命题的 1/0 裁定不生效(对象留在原库、布尔估计不变、无卡片)
2024
+ if (await formalVerdictDeferred(t, v, v === 1 || v === 0)) return
1309
2025
  p.布尔估计 = v
1310
2026
  if (v === 1) { p.证明列表 = p.证明列表 || []; p.证明列表.push({ 完整过程: strongestReason(t, 1), 正确概率: 1, '支持信息/依据': '', 已验: true }); p.优先级 = 'never' }
1311
2027
  else if (v === 0) { p.证伪列表 = p.证伪列表 || []; p.证伪列表.push({ 完整过程: strongestReason(t, 0), 正确概率: 1, '支持信息/依据': '', 已验: true }); p.优先级 = 'never' }
@@ -1320,6 +2036,8 @@ export function apply(ctx) {
1320
2036
  } else if (r.kind === 'prop-proof') {
1321
2037
  const p = await findProposition(r.pId)
1322
2038
  if (p) {
2039
+ // require 门禁:一条证明/证伪被判定为 1(严格成立)时同样受门禁约束
2040
+ if (await formalVerdictDeferred(t, v, v === 1)) return
1323
2041
  const list = r.side === '证明' ? (p.证明列表 = p.证明列表 || []) : (p.证伪列表 = p.证伪列表 || [])
1324
2042
  const item = list[r.idx]
1325
2043
  if (item) {
@@ -1342,6 +2060,9 @@ export function apply(ctx) {
1342
2060
  const qs = await getQs(); const q = qs.find(function (x) { return x.id === r.qid }); if (q) {
1343
2061
  const sol = (q.解法列表 || [])[r.idx]
1344
2062
  if (sol) {
2063
+ // require 门禁:v=1 意味着"这个问题的解法严格成立"(问题收口),必须被门禁拦住,
2064
+ // 否则问题会被标成已解决、优先级 never,而 formal 记录仍是 none。
2065
+ if (await formalVerdictDeferred(t, v, v === 1)) return
1345
2066
  sol.正确概率 = v
1346
2067
  sol.已验 = true
1347
2068
  sol.验证记录 = sol.验证记录 || []
@@ -1398,8 +2119,23 @@ export function apply(ctx) {
1398
2119
  }
1399
2120
  // 点4:Verified 卡片 = 每对象一卡,内容 = 该对象当前所有正确概率=1 的证明/证伪完整过程;
1400
2121
  // 新 1-概率条目出现时自动更新(内容不变则不写)。幂等键 = 对象 id。
1401
- async function writeVerifiedCardIfNeeded(p) {
1402
- if (p.布尔估计 !== 1 && p.布尔估计 !== 0) return false
2122
+ //
2123
+ // ★ require 门禁的**单一收口点**(契约 §8):一张"已验证·真/假"的卡片只在这里产生,
2124
+ // 所以门禁只加在这里(以及它的问题分支 writeVerifiedProblemCardIfNeeded)——不散落在多处。
2125
+ // 门禁不通过时**不写卡片**,把对象记为搁置(未定论 + Formal/TODO.md + 公告),
2126
+ // 并且由调用方保证"不把 1/0 提升写进概率字段"。
2127
+ //
2128
+ // 返回 { ok, deferred }:
2129
+ // ok=true → 卡片已写/已更新(或本来就无需写)
2130
+ // ok=false,deferred=true → 被 require 门禁搁置(本次裁定不生效)
2131
+ async function writeVerifiedCardIfNeeded(p, promoteTo) {
2132
+ const target = (promoteTo === 0 || promoteTo === 1) ? promoteTo : p.布尔估计
2133
+ if (target !== 1 && target !== 0) return { ok: false, deferred: false }
2134
+ // ------- require 门禁 -------
2135
+ if (formalRequired() && !formalGateOk(formalGateRecord(p.id))) {
2136
+ await deferForFormal(p.id, '布尔裁定 ' + target)
2137
+ return { ok: false, deferred: true }
2138
+ }
1403
2139
  const cat = categoryOf(p)
1404
2140
  const list = await readVerifiedCategory(cat)
1405
2141
  const idx = list.findIndex(function (c) { return c.id === p.id })
@@ -1409,17 +2145,28 @@ export function apply(ctx) {
1409
2145
  for (let i = 0; i < proofs1.length; i++) parts.push('【证明 #' + (i + 1) + '】' + (proofs1[i].完整过程 || ''))
1410
2146
  for (let i = 0; i < refutes1.length; i++) parts.push('【证伪 #' + (i + 1) + '】' + (refutes1[i].完整过程 || ''))
1411
2147
  const card = {
1412
- id: p.id, 概述: p.概述, 类型: '命题', 结论: p.布尔估计 === 1, 概率: p.布尔估计,
2148
+ id: p.id, 概述: p.概述, 类型: '命题', 结论: target === 1, 概率: target,
1413
2149
  内容: parts.join('\n'), 证明条数: proofs1.length, 证伪条数: refutes1.length,
1414
2150
  来源: p.来源问题 || '', 时间: now(), 分类: cat,
1415
2151
  }
2152
+ // 形式化状态随卡片一起落盘:读卡片的人必须能看出这条结论有多强(机器已核对 vs 仅共识)。
2153
+ // 仅在非 off 档写入,off 档保持卡片与改动前逐字节一致(off 是真无操作)。
2154
+ if (formalOn()) card['形式化'] = formalStatusLine(p.id)
1416
2155
  if (idx === -1) list.push(card)
1417
- else { if (list[idx].内容 === card.内容 && list[idx].概率 === card.概率) return false; list[idx] = card }
2156
+ else { if (list[idx].内容 === card.内容 && list[idx].概率 === card.概率 && list[idx]['形式化'] === card['形式化']) return { ok: false, deferred: false }; list[idx] = card }
1418
2157
  await writeJson('Verified/' + cat + '_Verified.json', list)
1419
- return idx === -1
1420
- }
1421
- async function writeVerifiedProblemCardIfNeeded(q) {
1422
- if (!q || !q.已解决) return false
2158
+ return { ok: true, deferred: false, created: idx === -1 }
2159
+ }
2160
+ // 问题类的对应收口点(契约 §8 的"问题收口分支")。
2161
+ async function writeVerifiedProblemCardIfNeeded(q, promoteTo) {
2162
+ if (!q) return { ok: false, deferred: false }
2163
+ const solved = (promoteTo === true) ? true : (promoteTo === false ? false : !!q.已解决)
2164
+ if (!solved) return { ok: false, deferred: false }
2165
+ // ------- require 门禁 -------
2166
+ if (formalRequired() && !formalGateOk(formalGateRecord(q.id))) {
2167
+ await deferForFormal(q.id, '问题判定为已解决')
2168
+ return { ok: false, deferred: true }
2169
+ }
1423
2170
  const cat = '问题'
1424
2171
  const list = await readVerifiedCategory(cat)
1425
2172
  const idx = list.findIndex(function (c) { return c.id === q.id })
@@ -1427,11 +2174,14 @@ export function apply(ctx) {
1427
2174
  const parts = []
1428
2175
  for (let i = 0; i < sols1.length; i++) parts.push('【解法 #' + (i + 1) + '】' + (sols1[i].完整解法 || ''))
1429
2176
  const card = { id: q.id, 概述: q.概述, 类型: '问题', 结论: true, 概率: 1, 内容: parts.join('\n'), 解法条数: sols1.length, 来源: q.id, 时间: now(), 分类: cat }
2177
+ if (formalOn()) card['形式化'] = formalStatusLine(q.id)
1430
2178
  if (idx === -1) list.push(card)
1431
- else { if (list[idx].内容 === card.内容) return false; list[idx] = card }
2179
+ else { if (list[idx].内容 === card.内容 && list[idx]['形式化'] === card['形式化']) return { ok: false, deferred: false }; list[idx] = card }
1432
2180
  await writeJson('Verified/' + cat + '_Verified.json', list)
1433
- return idx === -1
2181
+ return { ok: true, deferred: false, created: idx === -1 }
1434
2182
  }
2183
+ // 卡片写入结果 → 布尔(向后兼容既有的 if (await writeVerifiedCardIfNeeded(p)) 语义)。
2184
+ function cardOk(r) { return !!(r && r.ok) }
1435
2185
 
1436
2186
  // ================= child result dispatch =================
1437
2187
  async function onChildEnd(info) {
@@ -1506,6 +2256,13 @@ export function apply(ctx) {
1506
2256
  pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,
1507
2257
  registeredAgents: Object.keys(agentRegistry).length,
1508
2258
  recentActivity: activityLog.slice(-Math.min(10, Number(params.activityLogCap) || 100)), params: params,
2259
+ formal: {
2260
+ mode: formalMode(), required: formalRequired(),
2261
+ leanCommand: params.leanCommand, leanArgs: params.leanArgs, leanTimeoutMs: params.leanTimeoutMs,
2262
+ objects: Object.keys(formalRecords()).map(function (k) { const r = formalRecords()[k] || {}; return { target: k, status: r.status, file: r.file, proof: r.proof, note: r.note } }),
2263
+ todo: formalTodo(),
2264
+ paths: { project: 'Formal/', lib: vibeRoot() + '/Formal/Lib/', proved: vibeRoot() + '/Formal/Proved/', proofs: 'Verified/Lean/' },
2265
+ },
1509
2266
  }
1510
2267
  }
1511
2268
 
@@ -1518,6 +2275,7 @@ export function apply(ctx) {
1518
2275
  currentProject = slug; await writeCurrentProject(); await ensureDirs()
1519
2276
  if ((await readJson('qs/qs.json')) === undefined) await writeJson('qs/qs.json', [])
1520
2277
  params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; lastReportWrite = 0; lastPushReport = 0; reportDirty = false
2278
+ formalState = { records: {}, todo: [] } // 形式化记录随项目切换(loadState 会读新项目的 formal.json)
1521
2279
  await loadSettings(); await migrateLegacyParams(); await loadState(); await saveAll()
1522
2280
  return { ok: true, project: slug, frameworkRoot: frameworkRoot() }
1523
2281
  }
@@ -1531,6 +2289,9 @@ export function apply(ctx) {
1531
2289
  function objParams(props, required) { return { type: 'object', properties: props, additionalProperties: false, required: required || [] } }
1532
2290
  const handlers = {}
1533
2291
  function registerTool(name, description, parameters, executeFn) { handlers[name] = executeFn }
2292
+ // Lean 工具需要知道调用者是谁(记进活动日志与索引署名)。调用者身份**显式传递**,
2293
+ // 不从任何全局可变状态推断(审计清单 §1.1)。无法识别时退回 'office'。
2294
+ function memberIdOf(agent) { try { const id = agent && agent.id ? String(agent.id) : ''; return id || 'office' } catch (e) { return 'office' } }
1534
2295
  registerTool('vibe_math_start', 'Start (or restart) the Vibe Math V2 scheduler for the current project.', objParams({}), async function () { return await startScheduler() })
1535
2296
  registerTool('vibe_math_resume', 'Resume the Vibe Math V2 scheduler after a checkpoint/restart.', objParams({}), async function () { return await resumeScheduler() })
1536
2297
  registerTool('vibe_math_pause', 'Pause the scheduler (in-flight children finish their current turn).', objParams({}), async function () { return await pauseScheduler() })
@@ -1538,7 +2299,7 @@ export function apply(ctx) {
1538
2299
  registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), async function () { await refreshParams(); return await getStatus() })
1539
2300
  registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json.', objParams({}), async function () { await refreshParams(); await maybeWriteReport(true); return await buildReport() })
1540
2301
  registerTool('vibe_math_set_mode', 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.', objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), async function (args) { params.mode = args.mode; await saveAll(); await saveSettings(); if (params.mode === 'auto') await autoResolvePending(); return { ok: true, mode: params.mode } })
1541
- registerTool('vibe_math_set_params', 'Update scheduler parameters (partial).', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
2302
+ registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象的 formal.status 未达到 passed/blocked 之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
1542
2303
  registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), async function () { await refreshParams(); const list = PARAM_SCHEMA.map(function (p) { const out = Object.assign({}, p); out.current = params[p.name]; out.default = DEFAULT_PARAMS[p.name]; return out }); return { ok: true, parameters: list, saveTo: frameworkRoot() + '/vibe_math_setting.json' } })
1543
2304
  registerTool('vibe_math_save_settings', 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.', objParams({}), async function () { return await saveSettings() })
1544
2305
  registerTool('vibe_math_template', 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.', objParams({ where: { type: 'string', enum: ['global', 'project'] } }), async function (args) { return await createTemplate((args && args.where) || 'global') })
@@ -1556,6 +2317,20 @@ export function apply(ctx) {
1556
2317
  registerTool('vibe_math_list_agents', 'List tracked sub-agents (child sessions).', objParams({}), async function () { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round, rId: m.rId }) } return { ok: true, agents: out, count: out.length } })
1557
2318
  registerTool('vibe_math_message_agent', 'Send a message to a tracked child agent (next turn).', objParams({ childId: { type: 'string' }, message: { type: 'string' } }, ['childId', 'message']), async function (args) { if (!agentRegistry[args.childId]) return { ok: false, message: 'unknown childId' }; await followupChild(args.childId, args.message); return { ok: true, message: 'message delivered' } })
1558
2319
  registerTool('vibe_math_interrupt_agent', 'Interrupt a tracked child agent.', objParams({ childId: { type: 'string' } }, ['childId']), async function (args) { await interruptChild(args.childId); return { ok: true, message: 'interrupt requested' } })
2320
+ // ---- Lean 形式化验证(契约 §5)----
2321
+ registerTool('vibe_math_lean_run', 'Execute the Lean toolchain on one .lean file inside the VibeMath root 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(memberIdOf(agent), args) })
2322
+ registerTool('vibe_math_lean_archive', 'Archive Lean code. kind="def": a REUSABLE definition/object/assumption → the global cross-project library (Formal/Lib). kind="lemma": a machine-checked lemma → 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(memberIdOf(agent), args) })
2323
+ registerTool('vibe_math_lean_lib', '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) {
2324
+ const noRefresh = !!(args && args.refresh === false)
2325
+ const r = noRefresh ? { lib: null, proved: null, objects: Object.keys(formalRecords()).length } : await rebuildLeanLibIndexes()
2326
+ return {
2327
+ ok: true, mode: formalMode(), rebuilt: !noRefresh,
2328
+ counts: r, todo: formalTodo(),
2329
+ objects: Object.keys(formalRecords()).map(function (k) { const rec = formalRecords()[k] || {}; return { target: k, status: rec.status, file: rec.file, proof: rec.proof, note: rec.note } }),
2330
+ paths: { project: 'Formal/(相对项目根)', lib: 'VibeMath/Formal/Lib/', proved: 'VibeMath/Formal/Proved/', proofs: 'Verified/Lean/' },
2331
+ hint: '复用优先:先在 Lib/ 里找现成定义;新定义用 vibe_math_lean_archive kind=\'def\' 归档,已证引理用 kind=\'lemma\'(归档前先跑通,跑不通不要入库)。',
2332
+ }
2333
+ })
1559
2334
 
1560
2335
  // ================= slash command /vibe =================
1561
2336
  async function dispatchVibeCommand(cmd, args) {
@@ -1594,6 +2369,13 @@ export function apply(ctx) {
1594
2369
  // 每次工具调用前同步当前项目(按会话读 current.json;多会话互不干扰)
1595
2370
  refreshProject: async function () { if (rootAgent) currentProject = await readCurrentProject() },
1596
2371
  getRunning: function () { return scheduler.running },
2372
+ // Lean 形式化验证(docs/formal-verification.md):状态与工具的内部入口,
2373
+ // 供状态/报告与测试直接读取,不必绕过工具层。
2374
+ formalMode: formalMode, formalOn: formalOn, formalRequired: formalRequired, formalGateOk: formalGateOk,
2375
+ formalRecords: formalRecords, formalTodo: formalTodo, formalOf: formalOf,
2376
+ rebuildLeanLibIndexes: rebuildLeanLibIndexes, leanArchive: leanArchive, leanRunTool: leanRunTool,
2377
+ writeFormalIndex: writeFormalIndex, writeFormalTodo: writeFormalTodo,
2378
+ leanRunToolApi: async function (relPath, timeoutMs) { return await leanRunFile(relPath, timeoutMs) },
1597
2379
  // 会话自己的心跳节流:timer 每 1s 询问是否到点;tick 执行时刷新 lastTickAt
1598
2380
  tickDue: function () { const iv = Math.max(200, Number(params.tickIntervalMs) || 2000); return (now() - lastTickAt) >= iv },
1599
2381
  }
@@ -1622,7 +2404,7 @@ export function apply(ctx) {
1622
2404
  registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), 'vibe_math_status')
1623
2405
  registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json.', objParams({}), 'vibe_math_report')
1624
2406
  registerTool('vibe_math_set_mode', 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.', objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
1625
- registerTool('vibe_math_set_params', 'Update scheduler parameters (partial).', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' } }), 'vibe_math_set_params')
2407
+ registerTool('vibe_math_set_params', 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象的 formal.status 未达到 passed/blocked 之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), 'vibe_math_set_params')
1626
2408
  registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), 'vibe_math_setup')
1627
2409
  registerTool('vibe_math_save_settings', 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.', objParams({}), 'vibe_math_save_settings')
1628
2410
  registerTool('vibe_math_template', 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.', objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
@@ -1638,6 +2420,14 @@ export function apply(ctx) {
1638
2420
  registerTool('vibe_math_message_agent', 'Send a message to a tracked child agent (next turn).', objParams({ childId: { type: 'string' }, message: { type: 'string' } }, ['childId', 'message']), 'vibe_math_message_agent')
1639
2421
  registerTool('vibe_math_interrupt_agent', 'Interrupt a tracked child agent.', objParams({ childId: { type: 'string' } }, ['childId']), 'vibe_math_interrupt_agent')
1640
2422
 
2423
+ // ── Lean 形式化验证(docs/formal-verification.md §5)─────────────────────────
2424
+ // 三个工具**无条件注册**:工具注册是静态的(动态注册会依赖运行时开关,破坏既有
2425
+ // `ctx.effect` 纪律),而档位只决定"框架是否告诉成员它们存在"。off 档下它们照常工作,
2426
+ // 人/代理主动调用时一样可用。
2427
+ registerTool('vibe_math_lean_run', 'Execute the Lean toolchain on one .lean file inside the VibeMath root 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')
2428
+ registerTool('vibe_math_lean_archive', 'Archive Lean code. kind="def": a REUSABLE definition/object/assumption → the global cross-project library (Formal/Lib). kind="lemma": a machine-checked lemma → 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')
2429
+ registerTool('vibe_math_lean_lib', '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')
2430
+
1641
2431
  // /vibe slash command (registered once; routed per session)
1642
2432
  ctx.effect(() => commands.register({
1643
2433
  name: 'vibe',