dsh-vibe-math 2.6.0 → 2.7.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 (59) hide show
  1. package/README.en.md +161 -389
  2. package/README.md +161 -390
  3. package/cordis.patch.yml +62 -14
  4. package/docs/AUDIT-CHECKLIST.md +2 -2
  5. package/docs/final-paper.md +1 -0
  6. package/docs/formal-verification.md +184 -29
  7. package/docs/math-computation.md +163 -0
  8. package/docs/release-notes/RELEASE-NOTES-2.7.0.en.md +151 -0
  9. package/docs/release-notes/RELEASE-NOTES-2.7.0.md +115 -0
  10. package/docs/release-notes/RELEASE-NOTES-2.7.1.en.md +86 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.7.1.md +86 -0
  12. package/docs/test-timing.md +24 -12
  13. package/installer.js +7 -4
  14. package/package.json +21 -3
  15. package/prompt-corpus-persona/persona-corpus.json +12 -12
  16. package/prompt-corpus-persona/persona-corpus.md +128 -32
  17. package/prompt-corpus-v2/formal-verify-v2.json +182 -92
  18. package/prompt-corpus-v2/formal-verify-v2.md +2379 -55
  19. package/prompt-corpus-v3/formal-verify-v3.json +215 -53
  20. package/prompt-corpus-v3/formal-verify-v3.md +2147 -22
  21. package/prompt-corpus-v4/formal-verify-v4.json +14 -14
  22. package/prompt-corpus-v4/formal-verify-v4.md +267 -14
  23. package/prompt-corpus-v5/prompt-corpus-v5.json +62 -62
  24. package/prompt-corpus-v5/prompt-corpus-v5.md +631 -3
  25. package/tests/audit-math-computation-contract.mjs +204 -0
  26. package/tests/audit-math-computation-parity.mjs +198 -0
  27. package/tests/audit-math-computation-sensitivity.mjs +231 -0
  28. package/tests/audit-persona-sensitivity.mjs +4 -4
  29. package/tests/audit-persona-surface.test.mjs +38 -10
  30. package/tests/audit-v5-integrity.mjs +14 -0
  31. package/tests/e2e-v5-round2.test.mjs +459 -2
  32. package/tests/formal-verify-v2.test.mjs +362 -8
  33. package/tests/formal-verify-v3.test.mjs +355 -6
  34. package/tests/formal-verify-v4.test.mjs +295 -6
  35. package/tests/formal-verify-v5.test.mjs +9 -1
  36. package/tests/helpers/math-computation-fake-seam.mjs +163 -0
  37. package/tests/math-computation-shared.test.mjs +806 -0
  38. package/tests/prompt-v5-integrity.test.mjs +5 -5
  39. package/vibe-math-v2/agent.cordis.yml +22 -8
  40. package/vibe-math-v2/math-computation.js +1279 -0
  41. package/vibe-math-v2/math-engines.js +197 -0
  42. package/vibe-math-v2/vibe-math-v2.js +853 -40
  43. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  44. package/vibe-math-v3/agent.cordis.yml +22 -8
  45. package/vibe-math-v3/math-computation.js +1279 -0
  46. package/vibe-math-v3/math-engines.js +197 -0
  47. package/vibe-math-v3/vibe-math-v3.js +844 -47
  48. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  49. package/vibe-math-v4/agent.cordis.yml +38 -6
  50. package/vibe-math-v4/math-computation.js +1279 -0
  51. package/vibe-math-v4/math-engines.js +197 -0
  52. package/vibe-math-v4/vibe-math-v4.js +816 -31
  53. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  54. package/vibe-math-v5/agent.cordis.yml +42 -6
  55. package/vibe-math-v5/math-computation.js +1279 -0
  56. package/vibe-math-v5/math-engines.js +197 -0
  57. package/vibe-math-v5/vibe-math-v5.js +1132 -37
  58. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +83 -3
  59. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
@@ -18,6 +18,25 @@
18
18
  // VibeMath_State/ — scheduler private state (checkpoint/resume)
19
19
  export const name = 'vibe-math-v2'
20
20
  export const inject = ['subagents', 'agents', 'fs', 'tools', 'commands']
21
+ // Lean 增量/异步(规格:docs/formal-verification.md §1):内容哈希用于归档去重与作业幂等。
22
+ // `node:crypto` 与 v4/v5 同一写法(只读的内置模块,不执行任何动态代码)。
23
+ import { createHash } from 'node:crypto'
24
+ // 数学计算工具(共享模块,四套字节一致;由 integration owner 维护,本文件只做接线)。
25
+ // 模块内部自己 import './math-engines.js',接线方不直接引用引擎表。
26
+ import {
27
+ registerMathComputation,
28
+ MATH_PARAM_NAMES,
29
+ MATH_PARAM_DEFAULTS,
30
+ MATH_TOOL_DESCRIPTION,
31
+ MATH_TOOL_SCHEMA,
32
+ MATH_ARCHIVE_WORKFLOW_LINE,
33
+ MATH_SUBSTITUTION_RULE_LINE,
34
+ MATH_PERSONA_TOOL_LINE,
35
+ MATH_RULE_LINES,
36
+ normalizeMathParams,
37
+ mathAvailabilityLine,
38
+ probeMathEngines,
39
+ } from './math-computation.js'
21
40
 
22
41
  // Standing mount: DSH mounts each agent preset ONCE per preset and joins every
23
42
  // session that names it to that SAME plugin instance (see @deepseek-ai/dsh-agent-presets).
@@ -164,12 +183,24 @@ export function apply(ctx) {
164
183
  leanCommand: 'lean', // 要执行的 Lean 可执行文件(例:'lake')
165
184
  leanArgs: [], // 插在文件名之前的附加参数(例:['env','lean'] 配合 leanCommand='lake')
166
185
  leanTimeoutMs: 120000, // 单次 Lean 运行的超时上限(毫秒)
186
+ leanAsync: true, // true = Lean 编译走后台队列(入队即返回)| false = 同步 await(今天的语义)
187
+ leanJobsMaxParallel: 1, // 后台编译并发上限(默认 1 = 串行;可调大以并行编译)
188
+ leanInitiative: 'normal', // 日常流程中的形式化主动性:off | normal | eager(与 formalVerify 的"验证要求强度"是两件事)
189
+ leanSearchPaths: [], // 额外搜索路径(默认空 = 只用自动注入的 VibeMath 根);非空时先注入它们、再注入自动根
167
190
  // ---- 最终论文(规格:docs/final-paper.md;v2/v3 为单作者变体)----
168
191
  finalPaper: true, // 收口时是否自动撰写最终论文(false 只关自动触发;/vibe paper 仍可用)
169
192
  paperFormat: 'both', // both = md + tex | md = 只写 markdown | tex = 只写 latex
170
193
  paperLanguage: 'zh', // zh = 中文(ctexart/xelatex 优先)| en = 英文(article/pdflatex 优先)
171
194
  paperCompilePdf: true, // 检测到 LaTeX 时是否编译 paper.pdf
172
195
  paperLatexCommand: '', // 指定 LaTeX 引擎(空 = 按语言探测 xelatex/latexmk/pdflatex/lualatex/tectonic)
196
+ // ---- 数学计算 math_computation(规格:docs/math-computation.md;六参数已冻结,拼写不得改)----
197
+ // 值取共享模块的 MATH_PARAM_DEFAULTS(四套逐字一致);数组必须**拷贝**,否则四套会共享同一个默认数组。
198
+ mathComputation: MATH_PARAM_DEFAULTS.mathComputation, // off | auto | on(auto = 探测到任一允许引擎才工作)
199
+ mathMode: MATH_PARAM_DEFAULTS.mathMode, // typed | typed+shell(后者才允许提示词里的 shell 兜底段)
200
+ mathEngines: MATH_PARAM_DEFAULTS.mathEngines.slice(), // 允许的引擎(含 cli;cli 默认开启,受 mathMode/mathEngines 双闸)
201
+ mathTimeoutMs: MATH_PARAM_DEFAULTS.mathTimeoutMs, // 单次计算超时上限(毫秒,下界 1000)
202
+ mathPackages: MATH_PARAM_DEFAULTS.mathPackages.slice(), // 需要预检的包(缺包只报告 + 给安装计划)
203
+ mathInstallScope: MATH_PARAM_DEFAULTS.mathInstallScope, // user | system(system 只对当次显式调用生效,永不记忆)
173
204
  }
174
205
  let params = Object.assign({}, DEFAULT_PARAMS)
175
206
  let scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null } // activeCount 由 activeCount() 从 agentRegistry 推导,不再作为字段
@@ -289,12 +320,23 @@ export function apply(ctx) {
289
320
  { 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' },
290
321
  { name: 'leanCommand', type: 'string', description: '要执行的 Lean 可执行文件(默认 lean;用 lake 时配合 leanArgs=["env","lean"])', suggestion: 'lean' },
291
322
  { name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加命令行参数(默认空)', suggestion: [] },
292
- { name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,默认 120000,最小 1000)', suggestion: 120000 },
323
+ { name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,默认 120000,最小 1000);异步档下它同时是**每个后台编译作业**的预算(到时主动 terminate,作业记 timeout、对象留在 attempted)', suggestion: 120000 },
324
+ { name: 'leanAsync', type: 'boolean', description: 'Lean 编译模式:true(默认)= 后台队列,vibe_math_lean_run / vibe_math_lean_archive{run:true} 立即返回 async.jobId 入队,成员不阻塞,结果由下一轮提示的【形式化结果】行与 vibe_math_lean_lib / vibe_math_lean_job 公告(**只有作业落地为 ok 才会置 passed 并写归档证明**);false = 完全同步 await(与旧行为逐字一致)', suggestion: true },
325
+ { name: 'leanJobsMaxParallel', type: 'integer', description: '后台 Lean 编译的并发上限(默认 1 = 串行,保持可预测的资源占用;调大可并行编译多个作业)', suggestion: 1 },
326
+ { name: 'leanInitiative', type: 'string', enum: LEAN_INITIATIVE_MODES.slice(), description: '日常流程中的形式化主动性:off(不主动,只在验证提示词按 formalVerify 的要求做)| normal(默认:顺手把有价值且可能复用的东西形式化)| eager(更主动:日常就主动把有价值的小引理/命题/定义形式化)。注意它与 formalVerify(验证时的要求强度:off|encourage|require)是**两件事**', suggestion: 'normal' },
327
+ { name: 'leanSearchPaths', type: 'array', items: { type: 'string' }, description: '额外 Lean 搜索路径(默认空数组 = 只用框架自动注入的 VibeMath 根)。非空时按顺序先注入这里给的路径、再注入自动根(去重);若 leanArgs 里已显式给了 --search-path/-R/--root,则完全尊重用户配置、不注入任何东西', suggestion: [] },
293
328
  { name: 'finalPaper', type: 'boolean', description: '收口(严格终止)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已检验通过的命题/解法/成果整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
294
329
  { name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md;tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
295
330
  { name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
296
331
  { name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志', suggestion: true },
297
332
  { name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。该命令解析不到时按"未检测到"降级(只留 tex+md)', suggestion: '' },
333
+ // ---- 数学计算 math_computation(六参数冻结;描述与 prompts.md §1/§6 口径一致)----
334
+ { name: 'mathComputation', type: 'enum', options: ['off', 'auto', 'on'], description: '数学计算总开关:off = 真无操作(提示词零提及);auto = 探测到 mathEngines 里任一允许引擎才工作;on = 同上(探测失败时工具仍返回可执行的安装指引,而不是假装可用)', suggestion: 'auto' },
335
+ { name: 'mathMode', type: 'enum', options: ['typed', 'typed+shell'], description: '计算策略:typed+shell(默认)= 工具不可用时允许宿主 shell 兜底,但结论必须标注"未经工具归档(shell 路径)";typed = 只用工具路径(提示词里不出现 shell 兜底段,engine:"cli" 返回 REFUSED{reason:policy})', suggestion: 'typed+shell' },
336
+ { name: 'mathEngines', type: 'string[]', description: '允许的引擎列表(默认 python|r|octave|julia|matlab|maple|wolfram|cli)。cli 默认开启且走同一套超时/输出上限/回执;从列表里移除某引擎即禁用(商业引擎只探测+许可,永不安装)', suggestion: MATH_PARAM_DEFAULTS.mathEngines.slice() },
337
+ { name: 'mathTimeoutMs', type: 'integer', description: '单次数学计算的超时上限(毫秒,默认 60000,最小 1000);到时主动 terminate 并把回执标为 MATH_TIMEOUT', suggestion: 60000 },
338
+ { name: 'mathPackages', type: 'string[]', description: '需要预检的包(默认空)。缺包只报告 + 给"用户自装指引"或"代理代装计划",不会执行脚本,也永不自动安装', suggestion: [] },
339
+ { name: 'mathInstallScope', type: 'string', enum: ['user', 'system'], description: '安装作用域:user(默认,用户级目录);system 只对当次显式调用生效、永不记忆(不会写进状态文件)', suggestion: 'user' },
298
340
  ]
299
341
 
300
342
  // ================= fs =================
@@ -359,6 +401,19 @@ export function apply(ctx) {
359
401
  if (isWindows()) return 'Remove-Item -Force -LiteralPath ' + psQuote(path) + ' -ErrorAction SilentlyContinue'
360
402
  return 'rm -f ' + shQuote(path)
361
403
  }
404
+ /**
405
+ * `subprocess.spawn` 的契约返回**句柄**(带 `done`),但桩宿主/测试替身常常写成 `async spawn(...)`
406
+ * ——那样返回的是 Promise,`handle.done` 是 undefined,`await handle.done` 得到 undefined,随后
407
+ * `outcome.exitCode` 抛 "Cannot read properties of undefined (reading 'exitCode')",被 catch 成一条
408
+ * 误导性的 "mkdir 失败"(实测:第二个会话建项目目录时刷出这条)。
409
+ * 这里统一兼容一次:Promise 就先 await,句柄缺 `done` 才如实报错。
410
+ */
411
+ async function spawnHandle(subprocess, spec) {
412
+ const raw = subprocess.spawn(spec)
413
+ const handle = (raw && typeof raw.then === 'function') ? await raw : raw
414
+ if (!handle || handle.done === undefined) throw new Error('subprocess.spawn returned no handle.done')
415
+ return handle
416
+ }
362
417
  async function runShell(script, cwd) {
363
418
  const subprocess = subprocessOf()
364
419
  if (subprocess === undefined) return { ok: false, error: 'no-subprocess' }
@@ -366,9 +421,9 @@ export function apply(ctx) {
366
421
  const argv = isWindows()
367
422
  ? ['powershell', '-NoProfile', '-NonInteractive', '-Command', script]
368
423
  : ['/bin/sh', '-c', script]
369
- const handle = subprocess.spawn({ argv: argv, cwd: cwd || workspaceRoot(), stdio: { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' }, graceMs: 20000 })
424
+ const handle = await spawnHandle(subprocess, { argv: argv, cwd: cwd || workspaceRoot(), stdio: { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' }, graceMs: 20000 })
370
425
  const outcome = await handle.done
371
- return { ok: outcome.exitCode === 0, exitCode: outcome.exitCode }
426
+ return { ok: !!outcome && outcome.exitCode === 0, exitCode: (outcome && outcome.exitCode !== undefined) ? outcome.exitCode : null }
372
427
  } catch (e) { return { ok: false, error: String((e && e.message) || e) } }
373
428
  }
374
429
  /**
@@ -385,12 +440,13 @@ export function apply(ctx) {
385
440
  shellWarned[key] = true
386
441
  console.error('vibe-math-v2: ' + where + ' failed (' + why + ')')
387
442
  }
388
- // 目录布局(docs/formal-verification.md §3):
389
- // 项目内:Formal/(对象形式化工作文件 + Index.md + TODO.md)、Verified/Lean/(归档证明)
443
+ // 目录布局(docs/formal-verification.md §3 / docs/math-computation.md §路径):
444
+ // 项目内:Formal/(对象形式化工作文件 + Index.md + TODO.md)、Verified/Lean/(归档证明)、
445
+ // Computation/(math_computation 的可复核回执:Computation/<runId>/ 与 Computation/installs/)
390
446
  // 全局(**不在项目内**,跨项目复用):<VibeMath 根>/Formal/Lib/(可复用定义)、Formal/Proved/(已证引理)
391
447
  async function ensureDirs() {
392
448
  const base = frameworkRoot()
393
- const dirs = ['qs', 'Propos', 'Reliable', 'Verified', 'Verified/Lean', 'Verification_logs', 'Progress_Logs', 'VibeMath_State', 'Formal']
449
+ const dirs = ['qs', 'Propos', 'Reliable', 'Verified', 'Verified/Lean', 'Verification_logs', 'Progress_Logs', 'VibeMath_State', 'Formal', 'Formal/Jobs', 'Computation']
394
450
  const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d }))
395
451
  const r = await runShell(mkdirCmd(paths))
396
452
  if (!r || !r.ok) warnShellOnce('ensureDirs (mkdir ' + base + ')', r)
@@ -403,6 +459,134 @@ export function apply(ctx) {
403
459
  return r
404
460
  }
405
461
 
462
+ // ================= 数学计算 math_computation:会话侧接线(共享模块,FREEZE §4/§5) =================
463
+ // 探测量(是否装了 python/r/…)是异步的,而提示词是同步构造的 ⇒ 这里维护一个会话级缓存,
464
+ // 在 init / 参数变化 / TTL 到期时刷新,提示词侧只读缓存现算(与 formalVerify 的动态纪律一致)。
465
+ const MATH_PROBE_TTL_MS = 15 * 60 * 1000
466
+ let mathProbe = null
467
+ let mathProbeAt = 0
468
+ /**
469
+ * `host.spawn` 适配:模块只给 `{argv, cwd, timeoutMs, stdoutCap, stderrCap}`,超时/终止由**接线方**负责
470
+ * (契约:超时必须 handle.terminate())。返回**完整** stdout/stderr——模块自己落盘完整版,只在返回体裁 64KB,
471
+ * 所以 stdio 上限要宽(默认 4MB),否则"超大输出"永远测不出完整落盘。
472
+ */
473
+ async function mathSpawn(spec) {
474
+ const sub = subprocessOf()
475
+ if (sub === undefined || typeof sub.spawn !== 'function') return null // 模块映射为 MATH_NO_SUBPROCESS
476
+ const s = spec || {}
477
+ const cap = Math.max(1000, Number(s.timeoutMs) || Number(params.mathTimeoutMs) || 60000)
478
+ const outCap = Math.max(4096, Number(s.stdoutCap) || 4 * 1024 * 1024)
479
+ const errCap = Math.max(4096, Number(s.stderrCap) || 4 * 1024 * 1024)
480
+ const started = now()
481
+ let handle
482
+ try {
483
+ handle = await spawnHandle(sub, {
484
+ argv: s.argv,
485
+ cwd: s.cwd || frameworkRoot(),
486
+ stdio: { stdin: 'ignore', stdout: { maxBytes: outCap }, stderr: { maxBytes: errCap } },
487
+ graceMs: cap,
488
+ })
489
+ } catch (e) { return null }
490
+ let timedOut = false, killed = false, timer = null, outcome
491
+ try {
492
+ outcome = await Promise.race([
493
+ handle.done,
494
+ new Promise(function (resolve) {
495
+ timer = setTimeout(function () {
496
+ timedOut = true
497
+ killed = true
498
+ try { if (handle && typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* best effort */ }
499
+ resolve({ exitCode: null })
500
+ }, cap)
501
+ }),
502
+ ])
503
+ } catch (e) { if (timer !== null) clearTimeout(timer); return null }
504
+ if (timer !== null) clearTimeout(timer)
505
+ let out = '', err = ''
506
+ try { if (handle.collected && handle.collected.stdout) out = handle.collected.stdout.readFrom(0).text } catch (e) { /* best effort */ }
507
+ try { if (handle.collected && handle.collected.stderr) err = handle.collected.stderr.readFrom(0).text } catch (e) { /* best effort */ }
508
+ return {
509
+ exit: (outcome && outcome.exitCode !== undefined) ? outcome.exitCode : null,
510
+ timedOut: timedOut, killed: killed, ms: now() - started,
511
+ stdout: String(out == null ? '' : out), stderr: String(err == null ? '' : err),
512
+ }
513
+ }
514
+ /** host.exists:项目根相对路径是否存在。 */
515
+ async function mathExists(rel) {
516
+ try { const t = await fsTarget(rel); const s = await fs.stat(t); return s !== undefined } catch (e) { return false }
517
+ }
518
+ /**
519
+ * host.listDir(**可选**,FREEZE §4):模块用它数 `Computation/` 下的 run 目录数(>200 只告警、永不删除)。
520
+ * 返回 DSH `fs.listDir` 的原样条目({name,type})——模块按 `type === 'directory'` 计数,
521
+ * 所以**不要**在这里把条目降级成字符串数组。宿主 fs 没有该能力时整个回调都不声明(见下面的条件字段),
522
+ * 否则模块每次运行都会去问一个不存在的服务。
523
+ */
524
+ async function mathListDir(rel) {
525
+ try { const t = await fsTarget(rel); const ents = await fs.listDir(t); return Array.isArray(ents) ? ents : [] } catch (e) { return [] }
526
+ }
527
+ /** 会话级 host:参数是**活引用**(函数),回执里写 designator='vibe-math-v2'。 */
528
+ const mathHost = {
529
+ // 模块注册时只**交回** handler/description/parameters;真正的挂载是 mathTool 之后的**字面量**两行。
530
+ // (会话层写 handlers 表、apply 层挂宿主工具面;两条路径名字/描述/schema 完全一致。)
531
+ register: function (name, description, parameters, handler) { /* 挂载见 mathTool 之后的字面量两行 */ },
532
+ params: function () { return params },
533
+ projectRoot: function () { return frameworkRoot() },
534
+ designator: 'vibe-math-v2',
535
+ // 宿主明知没有 subprocess 服务时直接说 false ⇒ 模块立即返回 MATH_NO_SUBPROCESS(不再逐个探引擎)。
536
+ hasSubprocess: function () { const sub = subprocessOf(); return !!(sub && typeof sub.spawn === 'function') },
537
+ // **可选能力,按宿主实际情况声明**:宿主 fs 没有 listDir 就不给这个回调(模块据此跳过
538
+ // "每项目 run 目录数 >200" 的保留检查,而不是每次运行都白问一次)。
539
+ listDir: (typeof fs.listDir === 'function') ? mathListDir : undefined,
540
+ // round-7 (fix 2): DSH's own bundled runtimes (`<home>/.dsh/dsh-runtimes/*/dependencies/<engine>/`)
541
+ // as a LAST resort after PATH. Only generic roots are supplied here; the module globs the tree
542
+ // name (never `dsh-primary-runtime`) and only accepts the descriptor's own candidate names.
543
+ runtimeRoots: () => { const env = process.env.DSH_HOME; if (env) return [String(env)]; const home = String(process.env.HOME || process.env.USERPROFILE || ''); return home ? [home.replace(/[\\/]+$/, '') + '/.dsh'] : [] },
544
+ listDirAbs: async function (abs) { try { const t = await fsTarget(abs); const st = await fs.stat(t); if (!st) return []; return (await fs.listDir(t)) || [] } catch (e) { return [] } },
545
+ writeText: async function (rel, text) { return await writeText(rel, text) },
546
+ readText: async function (rel) { return await readText(rel) },
547
+ exists: mathExists,
548
+ resolveExecutable: async function (cmd) {
549
+ const sub = subprocessOf()
550
+ if (sub === undefined || typeof sub.resolveExecutable !== 'function') throw new Error('no subprocess service')
551
+ const p = await sub.resolveExecutable(String(cmd))
552
+ if (typeof p !== 'string' || !p) throw new Error('not found: ' + String(cmd))
553
+ return p
554
+ },
555
+ spawn: mathSpawn,
556
+ log: function (kind, msg) { logActivity('math', String(kind) + ': ' + String(msg)) },
557
+ }
558
+ // 模块只调用一次 host.register(...)(FREEZE §4)⇒ 上面那个回调就是会话层注册。
559
+ // 注意:真实注册必须等到 `const handlers = {}` 与 `function registerTool(...)` 就绪之后
560
+ // (`handlers` 是 const,提前调用会撞 TDZ)⇒ 这里只留槽位,注册放在会话工具表旁。
561
+ let mathTool = null
562
+ /** 刷新探测缓存。`mathComputation:'off'` ⇒ 不探测、不缓存、不注入(真 no-op)。 */
563
+ async function refreshMathProbe(force) {
564
+ if (params.mathComputation === 'off') { mathProbe = null; mathProbeAt = 0; return null }
565
+ if (!mathTool) return null
566
+ try {
567
+ mathProbe = await mathTool.probe(force ? { refresh: true } : undefined)
568
+ mathProbeAt = now()
569
+ // 规则漂移自检(A 项):可用性行必须带"替代必须声明(诚实性)"这条规则。这里引用**常量**
570
+ // 而不是比对文本——模块以后改措辞不会误报,只有整条规则从 MATH_RULE_LINES 里消失才会响。
571
+ if (mathProbe && mathAvailabilityLine(mathProbe, 'zh', params.mathMode).indexOf(MATH_SUBSTITUTION_RULE_LINE) === -1) {
572
+ console.error('vibe-math-v2: 可用性行缺少替代声明规则(MATH_SUBSTITUTION_RULE_LINE)')
573
+ }
574
+ } catch (e) { mathProbe = null }
575
+ return mathProbe
576
+ }
577
+ /** 心跳里的 TTL 刷新(每拍一次布尔判断,成本可忽略)。 */
578
+ function mathProbeDue() { return params.mathComputation !== 'off' && (!mathProbe || (now() - mathProbeAt) >= MATH_PROBE_TTL_MS) }
579
+ /**
580
+ * 每轮可用性行(中文)。由模块的 `mathAvailabilityLine(probe,'zh',mathMode)` 按档位拼装:
581
+ * 只有 `typed+shell` 才含 shell 兜底句(`mathMode:'typed'` 天然不含,守卫据此断言)。
582
+ * `off` 档返回空串——提示词零提及(对齐 formalVerify:'off' 的真 no-op 纪律)。
583
+ */
584
+ function mathWorkLine() {
585
+ if (params.mathComputation === 'off') return ''
586
+ if (!mathProbe) return '' // 还没探测过 ⇒ 不注入(不撒谎;init 会先探一次)
587
+ return '\n' + mathAvailabilityLine(mathProbe, 'zh', params.mathMode) + '\n'
588
+ }
589
+
406
590
  // ================= settings =================
407
591
  function sanitizeParams(obj) {
408
592
  const out = {}
@@ -417,9 +601,14 @@ export function apply(ctx) {
417
601
  maxParallelThreshold: 1, solverMaxRounds: 1, verifierCount: 2, debateMaxRounds: 1,
418
602
  directionsPerSolver: 1, tickIntervalMs: 200, reportIntervalMs: 1000, maxExplorerRetries: 1,
419
603
  }
604
+ // 数学计算六参数的显式强制(FREEZE §5.3):交给共享模块的 normalizeMathParams,它按
605
+ // enum/array/integer 逐键判型——`'false'`/字符串/错误枚举一律回退默认值,**绝不**落到
606
+ // 下面 `else { out[k] = v }` 的尾巴(那会把任意字符串当合法值放行)。
607
+ const mathNorm = normalizeMathParams(obj)
420
608
  for (const k of Object.keys(DEFAULT_PARAMS)) {
421
609
  if (!(k in obj)) continue
422
610
  const v = obj[k]
611
+ if (MATH_PARAM_NAMES.indexOf(k) !== -1) { out[k] = Object.prototype.hasOwnProperty.call(mathNorm, k) ? mathNorm[k] : DEFAULT_PARAMS[k]; continue }
423
612
  if (intFields.indexOf(k) !== -1) {
424
613
  const n = Number(v)
425
614
  if (!Number.isFinite(n)) { out[k] = DEFAULT_PARAMS[k]; continue }
@@ -437,6 +626,19 @@ export function apply(ctx) {
437
626
  else if (k === 'proposPriorityAdjust') { out[k] = (v === 'none' || v === 'progress-graded') ? v : DEFAULT_PARAMS[k] }
438
627
  else if (k === 'solverAllowNetwork' || k === 'verifierAllowNetwork' || k === 'solverAllowScripts' || k === 'verifierAllowScripts') { out[k] = (v === true || v === false || v === '') ? v : DEFAULT_PARAMS[k] }
439
628
  else if (k === 'finalPaper' || k === 'paperCompilePdf') { out[k] = (v === true || v === false) ? v : DEFAULT_PARAMS[k] }
629
+ // leanAsync 必须**显式**归一化:`else { out[k] = v }` 会把字符串 'false' 当真理放行(spec §1.1)。
630
+ else if (k === 'leanAsync') { out[k] = (v === true || v === false) ? v : DEFAULT_PARAMS[k] }
631
+ else if (k === 'leanInitiative') { out[k] = LEAN_INITIATIVE_MODES.indexOf(v) !== -1 ? v : DEFAULT_PARAMS[k] }
632
+ else if (k === 'leanJobsMaxParallel') {
633
+ const n = Number(v)
634
+ out[k] = Number.isFinite(n) ? Math.max(1, Math.min(8, Math.floor(n))) : DEFAULT_PARAMS[k]
635
+ }
636
+ // leanSearchPaths:显式数组分支(字符串/非数组一律回退默认空数组),并去掉空串与重复项。
637
+ else if (k === 'leanSearchPaths') {
638
+ if (!Array.isArray(v)) { out[k] = DEFAULT_PARAMS[k]; continue }
639
+ const seen = {}
640
+ out[k] = v.map(function (x) { return String(x == null ? '' : x).trim() }).filter(function (x) { if (!x || seen[x]) return false; seen[x] = true; return true })
641
+ }
440
642
  else if (k === 'paperFormat') { out[k] = (v === 'both' || v === 'md' || v === 'tex') ? v : DEFAULT_PARAMS[k] }
441
643
  else if (k === 'paperLanguage') { out[k] = (v === 'zh' || v === 'en') ? v : DEFAULT_PARAMS[k] }
442
644
  else { out[k] = v }
@@ -649,7 +851,6 @@ export function apply(ctx) {
649
851
  * (无服务 / 无可执行文件 / spawn 失败 / 超时 / 非零退出)都变成可读结果。
650
852
  */
651
853
  async function leanRunFile(relPath, timeoutMs) {
652
- const started = now()
653
854
  const rel = String(relPath || '').trim()
654
855
  if (!rel) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'file is required' }
655
856
  // 路径守卫:只允许执行 VibeMath 树内的文件,构造出来的路径不能让我们跑工作区之外的东西。
@@ -657,21 +858,35 @@ export function apply(ctx) {
657
858
  if (abs === null) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'Lean 文件必须位于 ' + vibeRoot() + '/ 之内(收到 ' + rel + ')' }
658
859
  if (!/\.lean$/.test(abs)) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '只有 .lean 文件可以执行' }
659
860
  if (await readTextAbs(abs) === undefined) return { ok: false, code: 'V2_NOT_FOUND', message: 'no such file: ' + rel }
861
+ return await leanExecFile(rel, abs, timeoutMs)
862
+ }
863
+ /**
864
+ * 已解析路径的**实际执行**(同步档与后台作业共用同一段:argv 注入、超时 terminate、输出截断)。
865
+ * 搜索路径注入(spec §3):`[exe, ...用户 leanArgs, --search-path <VibeMath 根>, <file>]`;
866
+ * 用户已显式给过 `--search-path`/`-R`/`--root` 就不再注入(显式覆盖优先)。
867
+ */
868
+ async function leanExecFile(rel, abs, timeoutMs, plan, onHandle) {
869
+ const started = now()
660
870
  const sub = subprocessOf()
661
871
  if (sub === undefined || typeof sub.spawn !== 'function') {
662
872
  return { ok: false, code: 'NO_SUBPROCESS', message: 'the host exposes no subprocess service; Lean cannot be executed here', file: rel, ms: 0 }
663
873
  }
664
874
  const cap = Math.max(1000, Number(timeoutMs) || Number(params.leanTimeoutMs) || 120000)
875
+ // 构建计划:异步作业用**入队时冻结**的计划(修订 §4:结果只对那个上下文有效);同步档现算。
876
+ const p = plan || leanBuildPlan()
665
877
  let exe
666
878
  try {
667
- exe = await sub.resolveExecutable(String(params.leanCommand || 'lean'))
879
+ exe = await sub.resolveExecutable(String(p.engine || 'lean'))
668
880
  } catch (e) {
669
- return { ok: false, code: 'LEAN_NOT_FOUND', message: 'cannot resolve "' + String(params.leanCommand || 'lean') + '": ' + String((e && e.message) || e) + ' —— 仍可把形式化代码写下来归档,但无法在此宿主上执行', file: rel, ms: now() - started }
881
+ return { ok: false, code: 'LEAN_NOT_FOUND', message: 'cannot resolve "' + String(p.engine || 'lean') + '": ' + String((e && e.message) || e) + ' —— 仍可把形式化代码写下来归档,但无法在此宿主上执行', file: rel, ms: now() - started }
670
882
  }
671
- const argv = [exe].concat((Array.isArray(params.leanArgs) ? params.leanArgs : []).map(String)).concat([abs])
883
+ const inject = Array.isArray(p.inject) ? p.inject : leanSearchPathPlan(p.args, p.searchPaths, vibeRoot()).inject
884
+ const argv = [exe].concat((Array.isArray(p.args) ? p.args : []).map(String)).concat(inject).concat([abs])
885
+ const searchPath = (Array.isArray(p.searchPaths) && p.searchPaths.length) ? p.searchPaths.join(',') : vibeRoot()
886
+ logActivity('formal', 'Lean 运行 ' + rel + '(search-path=' + searchPath + ')')
672
887
  let handle
673
888
  try {
674
- handle = sub.spawn({
889
+ handle = await spawnHandle(sub, {
675
890
  argv: argv,
676
891
  cwd: frameworkRoot(),
677
892
  stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } },
@@ -680,6 +895,8 @@ export function apply(ctx) {
680
895
  } catch (e) {
681
896
  return { ok: false, code: 'LEAN_SPAWN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
682
897
  }
898
+ leanSyncHandle = handle
899
+ if (typeof onHandle === 'function') { try { onHandle(handle) } catch (e) { /* best effort */ } }
683
900
  // 超时必须有**主动**兜底:`graceMs` 只是宿主侧的宽限,契约 §7 明确要求"对超时调用
684
901
  // handle.terminate()"。此前 v2 只依赖 graceMs,从不终止进程:一个卡住的 Lean 会继续占着
685
902
  // 资源,而框架已经报了超时——它与自己的契约不一致。这里与 `handle.done` 竞速:计时器到点
@@ -703,6 +920,8 @@ export function apply(ctx) {
703
920
  return { ok: false, code: 'LEAN_RUN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
704
921
  }
705
922
  if (timer !== null) clearTimeout(timer)
923
+ leanSyncHandle = null
924
+ if (typeof onHandle === 'function') { try { onHandle(null) } catch (e) { /* best effort */ } }
706
925
  let out = '', err = ''
707
926
  try { if (handle.collected && handle.collected.stdout) out = handle.collected.stdout.readFrom(0).text } catch (e) { /* best effort */ }
708
927
  try { if (handle.collected && handle.collected.stderr) err = handle.collected.stderr.readFrom(0).text } catch (e) { /* best effort */ }
@@ -715,14 +934,15 @@ export function apply(ctx) {
715
934
  // 输出截断到 ~4KB 再入库(避免把巨大的编译器输出写进状态)。
716
935
  return {
717
936
  ok: ok, exitCode: exitCode, signal: (outcome && outcome.signal) || null, ms: ms,
718
- command: argv.join(' '), file: rel,
937
+ command: argv.join(' '), file: rel, searchPath: searchPath,
719
938
  stdout: tailText(out, 4000), stderr: tailText(err, 4000),
720
939
  timedOut: isTimeout,
721
940
  code: ok ? undefined : (isTimeout ? 'LEAN_TIMEOUT' : 'LEAN_FAILED'),
722
941
  }
723
942
  }
724
943
  // 把一次运行结果写进对象的形式化记录(不提升 status,状态迁移见契约 §4)。
725
- async function formalSetRun(target, run) {
944
+ // `asyncInfo`(可选):异步档把作业 id/state/attempts/落地时刻贴在记录上,供人对照(§1.4)。
945
+ async function formalSetRun(target, run, asyncInfo) {
726
946
  const t = safeId(String(target || ''))
727
947
  if (!t) return
728
948
  const prev = formalOf(t)
@@ -730,6 +950,7 @@ export function apply(ctx) {
730
950
  status: prev.status === 'passed' ? 'passed' : (prev.status === 'blocked' ? 'blocked' : 'attempted'),
731
951
  file: run.file || prev.file || '',
732
952
  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) },
953
+ async: asyncInfo || prev.async || null,
733
954
  updatedAt: now(),
734
955
  }))
735
956
  }
@@ -898,7 +1119,7 @@ export function apply(ctx) {
898
1119
  L.push(' 请复核这个判断是否成立;若你认为其实可以形式化,请指出来并动手做。')
899
1120
  } else {
900
1121
  L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
901
- L.push(' · 工具:vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库)')
1122
+ L.push(' · 工具:vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库/jobs)· vibe_math_lean_read(取回归档原文)')
902
1123
  L.push(' · 工作目录:Formal/(相对项目根);可复用定义放 ' + vroot + '/Formal/Lib/,已证引理放 '
903
1124
  + vroot + '/Formal/Proved/;写之前先 vibe_math_lean_lib 查重。')
904
1125
  L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与命题原文逐条一致。'
@@ -911,19 +1132,42 @@ export function apply(ctx) {
911
1132
  L.push(' · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision=\'blocked\' 时必须写明 note)。')
912
1133
  }
913
1134
  L.push(' · 归档可复用定义/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run);跑不通不要入库。')
1135
+ // —— §5 B(逐字):三条筛选判据 + 先查再写 + 异步"落地前不得当已通过" ——
1136
+ L.push(' · 三条筛选判据:① 有价值或可能复用;② 较为关键或必要;③ 你对该陈述有把握(置信度高)。')
1137
+ L.push(' · 写新定义/证明前**先 vibe_math_lean_lib 查已有库**(vibe_math_lean_read 可取回归档原文逐字复用),查不到再写;'
1138
+ + '复用已归档内容用 `import Formal.Lib.<name>` / `import Formal.Proved.<name>`。')
1139
+ L.push(' · **没把握就记 blocked** 并写清难点,别用形式化掩盖不确定。')
1140
+ L.push(' · 该对象若已有后台编译在队列中(leanAsync 默认开启),**不得**在作业落地为通过之前声称已通过或转忠实性审查;'
1141
+ + '等 vibe_math_lean_lib 的 jobs 显示 settled 再审。')
914
1142
  L.push(' · 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或宿主不提供 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并在回执的 note 里写明'
915
1143
  + '"宿主无 Lean 工具链"——这算显式阻塞原因,定论门禁可以据此放行。')
916
1144
  }
917
- return L.join('\n')
1145
+ // 后台作业的一次性公告(spec §2.4):注入到本轮提示里,取走即清空。
1146
+ // 数学计算可用性行(prompts.md §2):验证轮同样告知"先 probe 再 run / 回执即引用"。
1147
+ return L.join('\n') + leanNoticeSection() + mathWorkLine()
918
1148
  }
919
1149
  function formalWorkLine() {
920
- if (!formalOn()) return ''
921
- // `Formal/Proved/` 在项目根下**并不存在**(可复用库故意在项目树之外),只写相对路径会让代理
922
- // 去项目里找一个永远找不到的目录;这里与验证段落一样给出 VibeMath 根的绝对路径(契约 §6.2)。
923
- return '【顺手形式化(' + (formalMode() === 'require' ? '强制' : '鼓励') + ')】把你工作中常用或可能复用的对象、假设、'
1150
+ // 修订 §1 + v5 统一口径:**日常主动性(leanInitiative)与验证时的要求强度(formalVerify)是两根
1151
+ // 独立的轴**。所以这里不能用 `!formalOn()` 一票否决——那会让 `leanInitiative:'eager'` 在
1152
+ // `formalVerify:'off'` 下静默失效("定论不额外要求,但日常鼓励形式化"组合必须真的成立)。
1153
+ // `formalVerify:'off'` 只关掉**验证阶段**的 Lean 文本:那里仍是 `formalPromptBlock` 的
1154
+ // `!formalOn() ⇒ ''`。`leanInitiative:'off'` 则连日线也不注入(真无操作)。
1155
+ const initiative = LEAN_INITIATIVE_MODES.indexOf(params.leanInitiative) !== -1 ? params.leanInitiative : 'normal'
1156
+ if (initiative === 'off' || !(formalOn() || initiative === 'eager')) return ''
1157
+ return '【顺手形式化(' + (formalOn() ? (formalMode() === 'require' ? '强制' : '鼓励') : '仅主动性') + '·主动性 ' + initiative + ')】把你工作中常用或可能复用的对象、假设、'
924
1158
  + '新定义用 Lean 形式化定义并归档到全局可复用库(vibe_math_lean_archive kind=\'def\'),已成立的引理归到 '
925
1159
  + vibeRoot().replace(/\\/g, '/') + '/Formal/Proved/(kind=\'lemma\');写之前先 vibe_math_lean_lib 查重,避免重复定义。'
926
1160
  + '归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。'
1161
+ + (initiative === 'eager'
1162
+ ? '\n · **主动档(leanInitiative=eager)**:日常就主动把有价值的小引理/命题/定义形式化——每轮工作结束时审视一次"这轮有什么值得进库",值得就顺手归档。'
1163
+ : '\n · 主动性 normal:顺手把明显有价值且可能复用的东西形式化;不必刻意扩大范围。')
1164
+ // —— §5 A(逐字):三条筛选判据 + 先查再写 + 「没把握就记 blocked」 + 异步"落地前不得当已通过" ——
1165
+ + '\n · 三条筛选判据:① 有价值或可能复用;② 较为关键或必要;③ 你对该陈述有把握(置信度高)。'
1166
+ + '\n · **先 vibe_math_lean_lib 查再决定是否重写**:vibe_math_lean_lib 列出现成定义/引理,vibe_math_lean_read 可取回归档原文逐字复用;'
1167
+ + '复用已归档内容用 `import Formal.Lib.<name>` / `import Formal.Proved.<name>`(模块根 = ' + vibeRoot().replace(/\\/g, '/') + ',框架已把它加进编译搜索路径)。'
1168
+ + '\n · **没把握就记 blocked**(vibe_math_lean_archive kind=\'blocked\' note=…,或回执 formal 的 blocked):把难点写清楚,别用形式化掩盖不确定。'
1169
+ + '\n · 编译默认走后台队列(leanAsync=true):入队后你可以继续工作;用 vibe_math_lean_job(可 waitMs 等结果)或下一轮提示里的'
1170
+ + '【形式化结果】行看结果。**在作业落地为通过之前,不得把该对象当成已通过。**'
927
1171
  }
928
1172
  /**
929
1173
  * 回执契约里的 `formal` 字段(契约 §6.3)。**必须真的出现在回执契约里**:契约写了字段而框架
@@ -1095,6 +1339,298 @@ export function apply(ctx) {
1095
1339
  return await absorbFormalReply(f, memberId)
1096
1340
  }
1097
1341
 
1342
+ // ================= Lean 增量 + 异步:作业队列(spec §2) =================
1343
+ // 并发上限 = `leanJobsMaxParallel`(默认 1 = 串行)。队列是**内存**态,作业记录镜像到
1344
+ // <项目>/Formal/Jobs/<jobId>.json —— 崩溃恢复、lean_lib.jobs 与 lean_job 都读它。
1345
+ const LEAN_QUEUE_CONCURRENCY_MAX = 8
1346
+ const leanQueue = [] // FIFO:待跑作业
1347
+ const leanJobs = new Map() // jobId -> job(内存视图,含 state)
1348
+ const leanRunningHandles = new Map() // jobId -> 子进程 handle(dispose 时 terminate)
1349
+ let leanSyncHandle = null // 同步档在跑的 handle(同样能被 terminate)
1350
+ const leanPendingNotices = [] // 一次性公告行(注入下一次 round prompt,spec §2.4)
1351
+ /** 当前会话的构建计划(修订 §2/§4):冻结 engine/args/搜索路径,作业与指纹都用它。 */
1352
+ function leanBuildPlan() {
1353
+ const userArgs = (Array.isArray(params.leanArgs) ? params.leanArgs : []).map(String)
1354
+ const plan = leanSearchPathPlan(userArgs, params.leanSearchPaths, vibeRoot())
1355
+ return { engine: String(params.leanCommand || 'lean'), args: userArgs, searchPaths: plan.paths, inject: plan.inject, explicit: plan.explicit }
1356
+ }
1357
+ function leanPlanContext(plan) { return leanBuildContext(plan.engine, plan.args, plan.searchPaths) }
1358
+ /** 归档作业当前应绑定的构建计划(def/lemma 与 proof 都是本项目计划)。 */
1359
+ function leanContextNow() { return leanPlanContext(leanBuildPlan()) }
1360
+ function leanJobsMaxParallel() { return Math.max(1, Math.min(LEAN_QUEUE_CONCURRENCY_MAX, Number(params.leanJobsMaxParallel) || 1)) }
1361
+ function leanActiveCount() { let n = 0; for (const j of leanJobs.values()) if (j.state === 'running') n++ ; return n }
1362
+ function leanSleep(ms) { return new Promise(function (r) { setTimeout(r, Math.max(0, Number(ms) || 0)) }) }
1363
+ function leanJobAbs(job) {
1364
+ const rel = String((job && job.rel) || '')
1365
+ if (!rel) return null
1366
+ return job.scope === 'root' ? normalizeAbsPath(vibeAbs(rel)) : normalizeAbsPath(frameworkRoot() + '/' + rel)
1367
+ }
1368
+ /** 作业的回执路径与归档路径(lean_job 返回它们,修订 §3)。 */
1369
+ function leanJobPaths(job) {
1370
+ const rec = { receipt: 'Formal/Jobs/' + job.jobId + '.json' }
1371
+ if (job.kind === 'archive' && job.archive && job.archive.kind === 'proof' && job.target) {
1372
+ rec.work = job.rel
1373
+ rec.archive = job.state === 'settled' ? ('Verified/Lean/' + job.target + '.lean') : ''
1374
+ } else if (job.kind === 'archive') rec.archive = job.rel
1375
+ else rec.file = job.rel
1376
+ return rec
1377
+ }
1378
+ function leanJobRecord(job) {
1379
+ return {
1380
+ jobId: job.jobId, kind: job.kind, state: job.state, attempts: Number(job.attempts || 1),
1381
+ target: job.target || '', name: job.name || '', rel: job.rel || '', scope: job.scope || 'project',
1382
+ sha256: job.sha || '', buildSha256: job.buildSha || '', buildContext: job.buildCtx || '',
1383
+ engine: (job.plan && job.plan.engine) || '', argv: (job.plan && job.plan.args) || [], searchPaths: (job.plan && job.plan.searchPaths) || [],
1384
+ archiveKind: (job.archive && job.archive.kind) || '',
1385
+ enqueuedAt: job.enqueuedAt || 0, startedAt: job.startedAt || 0, settledAt: job.settledAt || 0,
1386
+ exitCode: job.exitCode === undefined ? null : job.exitCode,
1387
+ timedOut: !!job.timedOut, interrupted: !!job.interrupted, note: job.note || '',
1388
+ project: currentProject,
1389
+ }
1390
+ }
1391
+ async function writeLeanJobFile(job) { await writeJson('Formal/Jobs/' + job.jobId + '.json', leanJobRecord(job)) }
1392
+ function hydrateLeanJob(rec) {
1393
+ return {
1394
+ jobId: String(rec.jobId), kind: rec.kind || 'run', target: rec.target || '', name: rec.name || '',
1395
+ rel: rec.rel || '', scope: rec.scope || 'project', sha: rec.sha256 || '',
1396
+ buildSha: rec.buildSha256 || '', buildCtx: rec.buildContext || '',
1397
+ plan: rec.engine !== undefined ? { engine: rec.engine || '', args: rec.argv || [], searchPaths: rec.searchPaths || [] } : null,
1398
+ archive: rec.archiveKind ? { kind: rec.archiveKind, target: rec.target || '', name: rec.name || '' } : null,
1399
+ state: rec.state || 'queued', attempts: Number(rec.attempts || 1), enqueuedAt: Number(rec.enqueuedAt || 0),
1400
+ startedAt: Number(rec.startedAt || 0), settledAt: Number(rec.settledAt || 0),
1401
+ exitCode: rec.exitCode === undefined ? null : rec.exitCode, timedOut: !!rec.timedOut,
1402
+ interrupted: !!rec.interrupted, run: rec.run || null, note: rec.note || '',
1403
+ }
1404
+ }
1405
+ /** 入队:同 jobId 已在队列/在跑 ⇒ 复用(幂等);返回作业对象。 */
1406
+ async function enqueueLeanJob(spec) {
1407
+ const existing = leanJobs.get(spec.jobId)
1408
+ if (existing && (existing.state === 'queued' || existing.state === 'running')) return existing
1409
+ const job = Object.assign({}, spec, {
1410
+ state: 'queued', attempts: Number(spec.attempts || ((existing && existing.attempts) || 0) + 1),
1411
+ enqueuedAt: spec.enqueuedAt || now(), startedAt: 0, settledAt: 0, exitCode: null,
1412
+ timedOut: false, interrupted: false, run: null, note: spec.note || '',
1413
+ })
1414
+ leanJobs.set(job.jobId, job)
1415
+ leanQueue.push(job)
1416
+ await writeLeanJobFile(job)
1417
+ logActivity('formal', 'Lean 作业入队 ' + job.jobId + '(' + job.kind + (job.target ? '|对象 ' + job.target : (job.name ? '|' + job.name : '')) + ',state=queued)')
1418
+ return job
1419
+ }
1420
+ function pushLeanNotice(line) { if (leanPendingNotices.length >= 20) leanPendingNotices.shift(); leanPendingNotices.push(String(line)) }
1421
+ /** 一次性公告(spec §2.4):取走即清空,保证同一条结果只注入一次。 */
1422
+ function leanNoticeSection() {
1423
+ if (leanPendingNotices.length === 0) return ''
1424
+ const lines = leanPendingNotices.splice(0, leanPendingNotices.length)
1425
+ return '\n' + lines.join('\n') + '\n'
1426
+ }
1427
+ function leanJobPublic(job) { return { jobId: job.jobId, state: job.state, rel: job.rel || '', target: job.target || '', attempts: Number(job.attempts || 1), settledAt: Number(job.settledAt || 0) } }
1428
+ /**
1429
+ * 心跳里推进队列:**不 await 编译本身**(调用方不 await 本函数)。
1430
+ * 并发上限 = `leanJobsMaxParallel`(默认 1,串行);串行时本函数等待该作业跑完再返回。
1431
+ */
1432
+ async function runLeanQueue() {
1433
+ const max = leanJobsMaxParallel()
1434
+ let started = 0
1435
+ while (leanQueue.length > 0 && leanActiveCount() + started < max) {
1436
+ const job = leanQueue.shift()
1437
+ if (!job || job.state !== 'queued') continue
1438
+ started++
1439
+ if (max <= 1) { await runLeanJob(job); return }
1440
+ runLeanJob(job).catch(function (e) { console.error('vibe-math-v2: lean job failed: ' + String((e && e.message) || e)) })
1441
+ }
1442
+ }
1443
+ async function runLeanJob(job) {
1444
+ if (job.state !== 'queued') return
1445
+ job.state = 'running'; job.startedAt = now(); job.interrupted = false
1446
+ await writeLeanJobFile(job)
1447
+ let run
1448
+ try { run = await leanExecFile(job.rel, leanJobAbs(job), job.timeoutMs, job.plan, function (h) { if (h) leanRunningHandles.set(job.jobId, h); else leanRunningHandles.delete(job.jobId) }) }
1449
+ catch (e) { run = { ok: false, exitCode: null, code: 'LEAN_RUN_FAILED', message: String((e && e.message) || e), ms: 0 } }
1450
+ try { await settleLeanJob(job, run) } catch (e) { console.error('vibe-math-v2: lean job settle failed: ' + String((e && e.stack) || e)) }
1451
+ }
1452
+ /** 结果公告行(spec §2.4 的四种结局)。 */
1453
+ function leanSettleNotice(job, settledOk, capMs) {
1454
+ const ms = (job.run && job.run.ms) || 0
1455
+ if (job.interrupted) return '【形式化结果】' + job.jobId + ':中断(会话卸载或崩溃,已标记 attempted,可重跑)'
1456
+ if (job.timedOut) return '【形式化结果】' + job.jobId + ':超时(leanTimeoutMs=' + capMs + ' 已 terminate;对象留在 attempted)'
1457
+ if (settledOk) return '【形式化结果】' + job.jobId + ':通过(exit 0,' + (ms / 1000).toFixed(1) + 's' + ((job.kind === 'archive' && job.archive && job.archive.kind === 'proof') ? (',已归档 Verified/Lean/' + job.target + '.lean') : '') + ')'
1458
+ if (job.hashMismatch) return '【形式化结果】' + job.jobId + ':失败(编译期间文件内容已变,本次结果已丢弃;请重跑)'
1459
+ if (job.contextMismatch) return '【形式化结果】' + job.jobId + ':失败(编译期间构建上下文已变:引擎/参数/搜索路径,本次结果已丢弃;请在新上下文下重跑)'
1460
+ return '【形式化结果】' + job.jobId + ':失败(exit ' + String(job.exitCode) + ',见 stderr 尾部)'
1461
+ }
1462
+ /**
1463
+ * 落地一个作业(**唯一**的 passed 入口,spec §2.1):只有 exit 0 **且** 内容哈希仍匹配才算 ok;
1464
+ * 其余一律 attempted;中断/超时都不是 passed;内容变了只标记不贴结果。
1465
+ */
1466
+ async function settleLeanJob(job, run) {
1467
+ const capMs = Math.max(1000, Number(job.timeoutMs) || Number(params.leanTimeoutMs) || 120000)
1468
+ let currentSha = null
1469
+ try { const t = await readTextAbs(leanJobAbs(job)); if (t !== undefined) currentSha = leanContentSha(t) } catch (e) { /* 读不到 ⇒ 视为不匹配 */ }
1470
+ const hashOk = currentSha !== null && currentSha === job.sha
1471
+ // 修订 §4:结果只对**产生它的那个构建上下文**有效。参数在编译途中被改(引擎/参数/搜索路径)
1472
+ // ⇒ 这次 ok 不能贴到"现在的配置"上,宁可判为未通过并明确公告。
1473
+ const ctxNow = leanContextNow()
1474
+ const ctxOk = !job.buildCtx || job.buildCtx === ctxNow
1475
+ job.hashMismatch = !hashOk
1476
+ job.contextMismatch = !ctxOk
1477
+ job.exitCode = run.exitCode === undefined ? null : run.exitCode
1478
+ job.timedOut = !!run.timedOut
1479
+ job.settledAt = now()
1480
+ const settledOk = !job.interrupted && !job.timedOut && run.exitCode === 0 && hashOk && ctxOk
1481
+ job.state = job.interrupted ? 'interrupted' : (job.timedOut ? 'timeout' : (settledOk ? 'settled' : 'failed'))
1482
+ job.run = { at: now(), ok: settledOk, exitCode: job.exitCode, ms: run.ms || 0, timedOut: !!job.timedOut, interrupted: !!job.interrupted, stdoutTail: tailText(run.stdout, 800), stderrTail: tailText(run.stderr, 800), searchPath: run.searchPath || vibeRoot(), buildContext: job.buildCtx || '' }
1483
+ if (job.hashMismatch && !job.interrupted && !job.timedOut) job.note = 'file changed while compiling — result discarded (hash mismatch)'
1484
+ else if (job.contextMismatch && !job.interrupted && !job.timedOut) job.note = 'build context changed while compiling (engine/args/search paths) — result discarded; re-run under the new context'
1485
+ const asyncInfo = { jobId: job.jobId, state: job.state, attempts: job.attempts, enqueuedAt: job.enqueuedAt, startedAt: job.startedAt, settledAt: job.settledAt, exitCode: job.exitCode, buildSha256: job.buildSha || '' }
1486
+ if (job.kind === 'archive') await settleLeanArchiveJob(job, settledOk, asyncInfo)
1487
+ else if (job.target) await formalSetRun(job.target, Object.assign({}, run, { ok: settledOk, file: job.rel }), asyncInfo)
1488
+ if (!job.interrupted) await writeFormalIndex()
1489
+ await writeLeanJobFile(job)
1490
+ pushLeanNotice(leanSettleNotice(job, settledOk, capMs))
1491
+ logActivity('formal', 'Lean 作业 ' + job.jobId + ' ' + job.state + '(' + job.kind + ')' + (job.target ? '|对象 ' + job.target : ''))
1492
+ reportDirty = true
1493
+ }
1494
+ /** 归档类作业的落地:**只有 ok 才落库/置 passed**(def/lemma 的文件在入队前已写好)。 */
1495
+ async function settleLeanArchiveJob(job, ok, asyncInfo) {
1496
+ const a = job.archive || {}
1497
+ if (a.kind !== 'proof') {
1498
+ await rebuildLeanLibIndexes()
1499
+ logActivity('formal', '归档作业 ' + job.jobId + '(' + a.kind + ' ' + job.rel + ')' + (ok ? '运行通过' : '运行未通过(见 stderr 尾部;该文件不应被当作可复用定义)'))
1500
+ return
1501
+ }
1502
+ const target = job.target
1503
+ const prev = formalOf(target)
1504
+ const passed = !!ok
1505
+ const stalePrev = passed ? '' : String(prev.proof || ('Verified/Lean/' + target + '.lean'))
1506
+ const rec = Object.assign({}, prev, {
1507
+ status: passed ? 'passed' : 'attempted',
1508
+ file: job.rel,
1509
+ proof: passed ? 'Verified/Lean/' + target + '.lean' : '',
1510
+ decision: 'used',
1511
+ note: String(prev.note || ''),
1512
+ run: job.run,
1513
+ async: asyncInfo,
1514
+ updatedAt: now(),
1515
+ })
1516
+ let withdrawn = null
1517
+ if (passed) {
1518
+ const body = await readTextAbs(leanJobAbs(job))
1519
+ if (body !== undefined) await writeText('Verified/Lean/' + target + '.lean', body)
1520
+ } else if (stalePrev) { try { withdrawn = await withdrawArchivedProof(stalePrev) } catch (e) { withdrawn = { rel: stalePrev, outcome: 'failed' } } }
1521
+ await putFormal(target, rec)
1522
+ await syncVerificationTarget(target, rec.status, rec)
1523
+ await rebuildLeanLibIndexes()
1524
+ logActivity('formal', '归档作业 ' + job.jobId + ' 为 ' + target + ' ' + (passed ? ('通过,已归档 ' + rec.proof + ',验证转为忠实性审查')
1525
+ : ('未通过:' + tailText(job.run && job.run.stderrTail, 160) + ';已撤回上一份已通过状态与归档证明' + (withdrawn && withdrawn.outcome !== 'failed' ? '(' + withdrawn.outcome + ')' : '(⚠ 撤回失败,请不要把 ' + stalePrev + ' 当作该对象的证明)'))))
1526
+ }
1527
+ /** 去重(spec §4.3):同内容且已成功过 ⇒ 跳过写盘与重编译。 */
1528
+ async function leanDedupeLookup(jobId, abs, sha, proofTarget) {
1529
+ if (proofTarget) {
1530
+ const rec = formalOf(proofTarget)
1531
+ if (rec && rec.status === 'passed') {
1532
+ const cur = await readTextAbs(vibeAbs('Verified/Lean/' + proofTarget + '.lean'))
1533
+ if (cur !== undefined && leanContentSha(cur) === sha) return { deduped: true, file: 'Verified/Lean/' + proofTarget + '.lean' }
1534
+ }
1535
+ return null
1536
+ }
1537
+ const prev = await readJson('Formal/Jobs/' + jobId + '.json')
1538
+ if (leanJobSettledOk(prev)) {
1539
+ const cur = await readTextAbs(abs)
1540
+ if (cur !== undefined && leanContentSha(cur) === sha) return { deduped: true, file: String(prev.rel || '') }
1541
+ }
1542
+ return null
1543
+ }
1544
+ /** 后台作业的公共返回形状(§1.2)。 */
1545
+ function leanAsyncReturn(job, extra) {
1546
+ return Object.assign({
1547
+ ok: true, async: { jobId: job.jobId, state: job.state }, jobId: job.jobId,
1548
+ message: '已入队后台编译(并发上限 1);你可以继续工作。结果会写入 Formal/ 与索引,并在下一轮提示里公告;也可用 vibe_math_lean_lib 的 jobs 字段随时查看。**在该作业落地为通过之前,不得把相关对象当成已通过。**',
1549
+ }, extra || {})
1550
+ }
1551
+ /** 崩溃恢复(spec §2.6):queued 重入队;running 哈希匹配 ⇒ 标记 interrupted + 重入队(attempts+1),不匹配 ⇒ 只标记;恢复**绝不**置 passed。 */
1552
+ async function recoverLeanJobs() {
1553
+ let files = []
1554
+ try { files = (await listFiles('Formal/Jobs')) || [] } catch (e) { return { recovered: 0 } }
1555
+ let recovered = 0
1556
+ for (let i = 0; i < files.length; i++) {
1557
+ const f = String(files[i])
1558
+ if (!/\.json$/.test(f)) continue
1559
+ const rec = await readJson('Formal/Jobs/' + f)
1560
+ if (!rec || !rec.jobId) continue
1561
+ // 本进程内存里已知的作业(queued/running 或已落地)不再恢复:同一进程内切换项目时,
1562
+ // 内存队列还在正常跑,按文件状态"恢复"会把一个在跑的作业误判成崩溃(重复入队/误置 interrupted)。
1563
+ if (leanJobs.has(String(rec.jobId))) continue
1564
+ const job = hydrateLeanJob(rec)
1565
+ leanJobs.set(job.jobId, job)
1566
+ const abs = leanJobAbs(job)
1567
+ const txt = abs ? await readTextAbs(abs) : undefined
1568
+ const curSha = txt === undefined ? null : leanContentSha(txt)
1569
+ const hashMatch = curSha !== null && curSha === job.sha
1570
+ if (job.state === 'queued') {
1571
+ await enqueueLeanJob(job)
1572
+ pushLeanNotice('【形式化结果】' + job.jobId + ':上次会话未执行的排队作业已重新入队')
1573
+ recovered++
1574
+ } else if (job.state === 'running') {
1575
+ if (hashMatch) {
1576
+ if (job.target) await putFormal(job.target, Object.assign({}, formalOf(job.target), { status: 'attempted', async: { jobId: job.jobId, state: 'interrupted', attempts: job.attempts }, updatedAt: now() }))
1577
+ job.interrupted = true
1578
+ // 先落到 interrupted,再重入队:否则 enqueueLeanJob 的幂等守卫会把这条 "running" 记录
1579
+ // 当成"仍在跑"而早退,attempts 也不会 +1(恢复就静默失效了)。
1580
+ job.state = 'interrupted'
1581
+ await enqueueLeanJob(Object.assign({}, job, { attempts: job.attempts + 1 }))
1582
+ pushLeanNotice('【形式化结果】' + job.jobId + ':中断(会话崩溃,已标记 attempted,已用新 attempts 重新入队)')
1583
+ } else {
1584
+ if (job.target) await putFormal(job.target, Object.assign({}, formalOf(job.target), { status: 'attempted', async: { jobId: job.jobId, state: 'interrupted', attempts: job.attempts, note: 'file changed' }, updatedAt: now() }))
1585
+ job.state = 'interrupted'; job.interrupted = true
1586
+ job.note = 'file changed while the previous session was compiling — not auto-re-driven'
1587
+ await writeLeanJobFile(job)
1588
+ pushLeanNotice('【形式化结果】' + job.jobId + ':中断(文件已变,未自动重驱;请手动重跑)')
1589
+ }
1590
+ recovered++
1591
+ } else if (job.state === 'settled') {
1592
+ // 修订 §4:只有**同一构建上下文**下的 settled(ok) 才允许恢复成 passed。
1593
+ const ctxNow = leanContextNow()
1594
+ const ctxOk = !job.buildCtx || job.buildCtx === ctxNow
1595
+ if (leanJobSettledOk(rec) && hashMatch && ctxOk) {
1596
+ job.run = { at: now(), ok: true, exitCode: 0, ms: 0, timedOut: false, interrupted: false, stdoutTail: '', stderrTail: '', searchPath: vibeRoot(), buildContext: job.buildCtx || '' }
1597
+ if (job.kind === 'archive') await settleLeanArchiveJob(job, true, { jobId: job.jobId, state: 'settled', attempts: job.attempts, settledAt: job.settledAt, exitCode: 0, buildSha256: job.buildSha || '' })
1598
+ else if (job.target) await formalSetRun(job.target, job.run, { jobId: job.jobId, state: 'settled', buildSha256: job.buildSha || '' })
1599
+ pushLeanNotice('【形式化结果】' + job.jobId + ':已按作业记录补写归档与 passed(恢复时校验内容哈希与构建上下文一致)')
1600
+ } else if (job.target) {
1601
+ await putFormal(job.target, Object.assign({}, formalOf(job.target), { status: 'attempted', async: { jobId: job.jobId, state: 'interrupted', attempts: job.attempts, note: ctxOk ? 'recovered-incomplete' : 'build context changed' }, updatedAt: now() }))
1602
+ pushLeanNotice('【形式化结果】' + job.jobId + ':按作业记录标记为未通过/已变(恢复绝不置 passed)')
1603
+ }
1604
+ recovered++
1605
+ }
1606
+ }
1607
+ if (recovered > 0) logActivity('formal', 'Lean 作业恢复:处理了 ' + recovered + ' 条遗留作业记录(queued 重入队 / running 标记中断 / settled 补写;绝不置 passed)')
1608
+ return { recovered: recovered }
1609
+ }
1610
+ /** 会话卸载/销毁:终止在跑的编译并标记 interrupted(不留孤儿进程,spec §2.5)。 */
1611
+ function disposeLeanJobs() {
1612
+ let n = 0
1613
+ for (const job of leanJobs.values()) {
1614
+ if (job.state === 'running') { job.interrupted = true; n++ }
1615
+ }
1616
+ for (const job of leanQueue) { job.state = 'interrupted'; job.interrupted = true; n++ }
1617
+ for (const h of leanRunningHandles.values()) {
1618
+ try { if (h && typeof h.terminate === 'function') h.terminate() } catch (e) { /* best effort */ }
1619
+ }
1620
+ leanRunningHandles.clear()
1621
+ if (leanSyncHandle && typeof leanSyncHandle.terminate === 'function') { try { leanSyncHandle.terminate() } catch (e) { /* best effort */ } }
1622
+ leanSyncHandle = null
1623
+ leanQueue.length = 0
1624
+ if (n > 0) {
1625
+ pushLeanNotice('【形式化结果】干预:会话卸载,' + n + ' 个 Lean 作业已标记 interrupted(未落地为 passed)')
1626
+ // 落盘尽力而为(disposer 不能 await):作业记录留着,供下次 init 恢复时标记。
1627
+ try {
1628
+ for (const job of leanJobs.values()) { if (job.state === 'running' || job.interrupted) writeLeanJobFile(job).catch(function () { }) }
1629
+ } catch (e) { /* best effort */ }
1630
+ }
1631
+ return n
1632
+ }
1633
+
1098
1634
  // ---- 三份索引(框架维护;契约 §9)----
1099
1635
  async function writeFormalIndex() {
1100
1636
  const recs = formalRecords()
@@ -1129,6 +1665,7 @@ export function apply(ctx) {
1129
1665
  async function rebuildLeanLibIndexes() {
1130
1666
  // 扫描**不执行**工具链:每次问"有什么可复用"就跑一遍 lean 既慢又出人意料。
1131
1667
  // 每个对象的运行结果存在对象记录里,显示在 Formal/Index.md。
1668
+ // 依赖列(spec §4.4):把 `import` 行扫出来写进索引,"有什么 / 叫什么 / 怎么导入"三问一次答完。
1132
1669
  const scan = async (dirAbs, dirRel, kindLabel) => {
1133
1670
  const rows = []
1134
1671
  try {
@@ -1141,21 +1678,23 @@ export function apply(ctx) {
1141
1678
  const txt = (await readTextAbs(dirAbs + '/' + e.name)) || ''
1142
1679
  const name = String(e.name).replace(/\.lean$/, '')
1143
1680
  const first = (txt.split('\n').filter(function (l) { return l.trim() && !/^\s*(\/\/|--|import)/.test(l) })[0] || '').trim().slice(0, 110)
1144
- rows.push('| ' + name + ' | ' + rel + ' | ' + kindLabel + ' | ' + first.replace(/\|/g, '/') + ' |')
1681
+ const deps = txt.split('\n').filter(function (l) { return /^\s*import\s+/.test(l) })
1682
+ .map(function (l) { return l.trim().replace(/^import\s+/, '').trim() }).filter(Boolean).slice(0, 4)
1683
+ rows.push('| ' + name + ' | ' + rel + ' | ' + kindLabel + ' | ' + (deps.length ? deps.join(', ') : '—').replace(/\|/g, '/') + ' | ' + first.replace(/\|/g, '/') + ' |')
1145
1684
  }
1146
1685
  } catch (e) { /* 列表尽力而为 */ }
1147
1686
  return rows
1148
1687
  }
1149
1688
  const libRows = await scan(vibeRoot() + '/Formal/Lib', 'Formal/Lib', 'def')
1150
1689
  await writeTextAbs(vibeRoot() + '/Formal/Lib/Index.md', ['# 可复用 Lean 定义库(跨项目)|' + currentProject, '',
1151
- '> 写新定义之前先查这里:能复用就不要重新定义。', '',
1152
- '| 名称 | 文件 | 类别 | 摘要 |', '|---|---|---|---|']
1153
- .concat(libRows.length ? libRows : ['| (暂无) | | | |']).join('\n') + '\n')
1690
+ '> 写新定义之前先查这里:能复用就不要重新定义。复用方式:`import Formal.Lib.<名称>`(模块根 = VibeMath 根)。', '',
1691
+ '| 名称 | 文件 | 类别 | 依赖(import) | 摘要 |', '|---|---|---|---|---|']
1692
+ .concat(libRows.length ? libRows : ['| (暂无) | | | | |']).join('\n') + '\n')
1154
1693
  const provedRows = await scan(vibeRoot() + '/Formal/Proved', 'Formal/Proved', 'lemma')
1155
1694
  await writeTextAbs(vibeRoot() + '/Formal/Proved/Index.md', ['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|' + currentProject, '',
1156
- '> 这些文件是通过内核检查的引理,可直接 import 复用。', '',
1157
- '| 名称 | 文件 | 类别 | 陈述 |', '|---|---|---|---|']
1158
- .concat(provedRows.length ? provedRows : ['| (暂无) | | | |']).join('\n') + '\n')
1695
+ '> 这些文件是通过内核检查的引理,可直接 import 复用:`import Formal.Proved.<名称>`。', '',
1696
+ '| 名称 | 文件 | 类别 | 依赖(import) | 陈述 |', '|---|---|---|---|---|']
1697
+ .concat(provedRows.length ? provedRows : ['| (暂无) | | | | |']).join('\n') + '\n')
1159
1698
  await writeFormalIndex()
1160
1699
  await writeFormalTodo()
1161
1700
  return { lib: libRows.length, proved: provedRows.length, objects: Object.keys(formalRecords()).length }
@@ -1163,20 +1702,110 @@ export function apply(ctx) {
1163
1702
 
1164
1703
  // 执行一个 Lean 文件,记录运行结果(可按 target 归属到对象),刷新索引,并**原样**回报结果。
1165
1704
  // 刻意在 `off` 档也照常工作:人要调试自己的工具链时仍然可用。
1705
+ // leanAsync=true(默认):入队后立即返回(成员不阻塞);false:逐字保留今天的同步路径。
1166
1706
  async function leanRunTool(memberId, o) {
1167
1707
  const args = o || {}
1168
- const run = await leanRunFile(String(args.file || ''), args.timeout_ms)
1708
+ const rel = String(args.file || '').trim()
1709
+ const target = String(args.target || '').trim()
1710
+ if (params.leanAsync !== false) {
1711
+ const guard = await leanResolveRunTarget(rel)
1712
+ if (!guard.ok) return guard
1713
+ const plan = leanBuildPlan()
1714
+ const ctx = leanPlanContext(plan)
1715
+ const fp = leanJobFingerprint(guard.text, ctx)
1716
+ const jobId = leanJobId(target || guard.rel, fp)
1717
+ const job = await enqueueLeanJob({ jobId: jobId, kind: 'run', key: target || guard.rel, rel: guard.rel, scope: 'project', target: target, sha: guard.sha, buildSha: fp, buildCtx: ctx, plan: plan, memberId: memberId, timeoutMs: args.timeout_ms })
1718
+ return leanAsyncReturn(job, { file: guard.rel, state: job.state, hint: '已入队。**在它落地为通过之前,不得把该对象当成已通过**;结果会在下一轮提示的【形式化结果】行里公告,或用 vibe_math_lean_job / vibe_math_lean_lib 的 jobs 字段查看(lean_job 可 waitMs=… 等待)。' })
1719
+ }
1720
+ const run = await leanRunFile(rel, args.timeout_ms)
1169
1721
  if (run.ok || run.file) {
1170
- if (String(args.target || '').trim()) await formalSetRun(String(args.target), run)
1722
+ if (target) await formalSetRun(target, run)
1171
1723
  await writeFormalIndex()
1172
1724
  }
1173
1725
  if (run.ok) logActivity('formal', (memberId || 'office') + ' 运行 Lean 通过:' + run.file + '(' + (run.ms / 1000).toFixed(1) + 's)' + (args.target ? '|对象 ' + args.target : ''))
1174
1726
  return Object.assign({ ok: !!run.ok }, run, {
1727
+ async: null,
1175
1728
  hint: run.ok
1176
1729
  ? '通过。若是某个对象的证明,请用 vibe_math_lean_archive kind=\'proof\' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind=\'def\'/\'lemma\' 归档到全局库(归档时会先跑一次,跑不通不要入库)。'
1177
1730
  : '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 vibe_math_lean_archive kind=\'blocked\' 记录原因。',
1178
1731
  })
1179
1732
  }
1733
+ /** 异步档的路径/存在性校验(与 leanRunFile 同一套守卫与错误码)。 */
1734
+ async function leanResolveRunTarget(relPath) {
1735
+ const rel = String(relPath || '').trim()
1736
+ if (!rel) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'file is required' }
1737
+ const abs = leanAbsPath(rel)
1738
+ if (abs === null) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'Lean 文件必须位于 ' + vibeRoot() + '/ 之内(收到 ' + rel + ')' }
1739
+ if (!/\.lean$/.test(abs)) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '只有 .lean 文件可以执行' }
1740
+ const txt = await readTextAbs(abs)
1741
+ if (txt === undefined) return { ok: false, code: 'V2_NOT_FOUND', message: 'no such file: ' + rel }
1742
+ return { ok: true, rel: rel, abs: abs, sha: leanContentSha(txt), text: txt }
1743
+ }
1744
+ /**
1745
+ * `lean_job`(修订 §3,只读):不带 jobId ⇒ 本会话作业清单;带 ⇒ 该作业的 state/exitCode/
1746
+ * 回执路径/归档路径。`waitMs>0` 时最多等这么久(内部轮询,**不阻塞心跳**——心跳是独立 timer),
1747
+ * 超时就返回当前 state。
1748
+ */
1749
+ async function leanJobTool(o) {
1750
+ const args = o || {}
1751
+ const wantId = String(args.jobId || '').trim()
1752
+ const waitMs = Math.max(0, Math.min(600000, Number(args.waitMs) || 0))
1753
+ if (!wantId) {
1754
+ const jobs = []
1755
+ for (const j of leanJobs.values()) jobs.push(Object.assign(leanJobPublic(j), { exitCode: j.exitCode === undefined ? null : j.exitCode, paths: leanJobPaths(j), buildSha256: j.buildSha || '' }))
1756
+ jobs.sort(function (a, b) { return (b.settledAt || 0) - (a.settledAt || 0) || String(a.jobId).localeCompare(String(b.jobId)) })
1757
+ return { ok: true, async: params.leanAsync !== false, maxParallel: leanJobsMaxParallel(), queued: leanQueue.length, running: leanActiveCount(), jobs: jobs, paths: { receipts: 'Formal/Jobs/', proofs: 'Verified/Lean/', lib: 'VibeMath/Formal/Lib/', proved: 'VibeMath/Formal/Proved/' }, hint: 'state=settled 且 exitCode=0(且内容哈希/构建上下文一致)才算通过;queued/running 一律还不算。' }
1758
+ }
1759
+ let job = leanJobs.get(wantId)
1760
+ if (!job) {
1761
+ const rec = await readJson('Formal/Jobs/' + wantId + '.json')
1762
+ if (!rec || !rec.jobId) return { ok: false, code: 'V2_NOT_FOUND', message: 'no such Lean job: ' + wantId + '(用不带 jobId 的 vibe_math_lean_job 列清单)' }
1763
+ job = hydrateLeanJob(rec)
1764
+ if (!leanJobs.has(job.jobId)) leanJobs.set(job.jobId, job)
1765
+ }
1766
+ const startedAt = now()
1767
+ while (waitMs > 0 && (job.state === 'queued' || job.state === 'running') && (now() - startedAt) < waitMs) {
1768
+ await leanSleep(50)
1769
+ const cur = leanJobs.get(wantId)
1770
+ if (cur) job = cur
1771
+ }
1772
+ return {
1773
+ ok: true, jobId: job.jobId, state: job.state, exitCode: job.exitCode === undefined ? null : job.exitCode,
1774
+ attempt: Number(job.attempts || 1), timedOut: !!job.timedOut, interrupted: !!job.interrupted, note: job.note || '',
1775
+ buildSha256: job.buildSha || '', buildContext: job.buildCtx || '',
1776
+ waitedMs: now() - startedAt, stillRunning: (job.state === 'queued' || job.state === 'running'),
1777
+ passed: leanJobSettledOk(leanJobRecord(job)),
1778
+ paths: leanJobPaths(job),
1779
+ run: job.run || null,
1780
+ }
1781
+ }
1782
+ /** `lean_read`:只读地取回归档的 Lean 原文(verbatim 复用;§1.2)。 */
1783
+ async function leanReadTool(o) {
1784
+ const args = o || {}
1785
+ const rawName = String(args.name || '').trim()
1786
+ if (!rawName) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'name is required' }
1787
+ if (rawName.indexOf('..') !== -1 || /[\\/]/.test(rawName) || /^[a-z]:/i.test(rawName)) {
1788
+ return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'name must be a bare library name (no path separators, no "..", no absolute path)' }
1789
+ }
1790
+ const name = safeId(rawName)
1791
+ const kind = (args.kind === 'lib' || args.kind === 'proved') ? args.kind : 'auto'
1792
+ const order = kind === 'auto' ? ['lib', 'proved'] : [kind]
1793
+ for (let i = 0; i < order.length; i++) {
1794
+ const k = order[i]
1795
+ const rel = 'Formal/' + (k === 'lib' ? 'Lib' : 'Proved') + '/' + name + '.lean'
1796
+ const dir = normalizeAbsPath(vibeRoot() + '/Formal/' + (k === 'lib' ? 'Lib' : 'Proved'))
1797
+ const abs = normalizeAbsPath(vibeAbs(rel))
1798
+ // 双保险:规范化后必须真的落在那两个目录之内(name 已过 safeId,这里是路径守卫)
1799
+ if (abs.indexOf(dir + '/') !== 0) continue
1800
+ const txt = await readTextAbs(abs)
1801
+ if (txt === undefined) continue
1802
+ const LIMIT = 64 * 1024
1803
+ const bytes = Buffer.byteLength(txt, 'utf8')
1804
+ const truncated = bytes > LIMIT
1805
+ return { ok: true, name: name, file: rel, kind: k, sha256: leanContentSha(txt), bytes: bytes, text: truncated ? Buffer.from(txt, 'utf8').slice(0, LIMIT).toString('utf8') : txt, truncated: truncated }
1806
+ }
1807
+ return { ok: false, code: 'V2_NOT_FOUND', message: 'no archived Lean file named ' + name + '(查过 Formal/Lib 与 Formal/Proved);可先用 vibe_math_lean_lib 看清单' }
1808
+ }
1180
1809
  async function leanArchive(memberId, o) {
1181
1810
  const args = o || {}
1182
1811
  const kind = String(args.kind || '')
@@ -1197,6 +1826,22 @@ export function apply(ctx) {
1197
1826
  if (body === undefined) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
1198
1827
  const rel = 'Formal/' + (kind === 'def' ? 'Lib' : 'Proved') + '/' + name + '.lean'
1199
1828
  const abs = vibeAbs(rel)
1829
+ const sha = leanContentSha(body)
1830
+ const plan = leanBuildPlan()
1831
+ const ctx = leanPlanContext(plan)
1832
+ const fp = leanJobFingerprint(body, ctx) // 修订 §4:指纹含内容 + 构建上下文
1833
+ const jobId = leanJobId(name, fp)
1834
+ // 去重(§4.3):同内容 + 同构建上下文且已成功归档过 ⇒ 跳过重写与重编译。
1835
+ const dedupe = await leanDedupeLookup(jobId, abs, sha, null)
1836
+ if (dedupe) {
1837
+ logActivity('formal', (memberId || 'office') + ' 归档 ' + kind + ' `' + name + '` 命中去重(同内容与构建上下文,跳过重写与重编译)')
1838
+ return { ok: true, kind: kind, name: name, file: rel, deduped: true, sha256: sha, buildSha256: fp, note: '内容与构建上下文都与已归档并编译通过的版本一致:跳过重写与重编译(去重)。' }
1839
+ }
1840
+ if (params.leanAsync !== false && args.run !== false) {
1841
+ if (!await writeTextAbs(abs, body)) return { ok: false, code: 'V2_WRITE_FAILED', message: 'could not write ' + rel }
1842
+ const job = await enqueueLeanJob({ jobId: jobId, kind: 'archive', key: name, rel: rel, scope: 'root', name: name, target: '', sha: sha, buildSha: fp, buildCtx: ctx, plan: plan, memberId: memberId, archive: { kind: kind } })
1843
+ return leanAsyncReturn(job, { kind: kind, name: name, file: rel, sha256: sha, buildSha256: fp, deduped: false, note: '文件已写入 ' + rel + ';编译已入队(并发上限 leanJobsMaxParallel)。**落地为通过之前,请不要把它当作可复用定义**;结果会在下一轮提示的【形式化结果】行公告,也可用 vibe_math_lean_job 查/等。' })
1844
+ }
1200
1845
  if (!await writeTextAbs(abs, body)) return { ok: false, code: 'V2_WRITE_FAILED', message: 'could not write ' + rel }
1201
1846
  // 全局库在项目树之外,必须用**绝对路径**执行(相对形式会被解析到项目根之内)。
1202
1847
  const run = args.run === false ? null : await leanRunFile(abs)
@@ -1223,6 +1868,35 @@ export function apply(ctx) {
1223
1868
  }
1224
1869
  if (body === undefined) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
1225
1870
  const workRel = 'Formal/' + target + '.lean'
1871
+ const sha = leanContentSha(body)
1872
+ const plan = leanBuildPlan()
1873
+ const ctx = leanPlanContext(plan)
1874
+ const fp = leanJobFingerprint(body, ctx)
1875
+ const jobId = leanJobId(target, fp)
1876
+ // 去重(§4.3):对象已 passed 且归档证明与新提交内容一致 ⇒ 跳过。
1877
+ const dedupe = await leanDedupeLookup(jobId, null, sha, target)
1878
+ if (dedupe) {
1879
+ logActivity('formal', (memberId || 'office') + ' 归档证明 ' + target + ' 命中去重(已 passed 且内容一致)')
1880
+ return { ok: true, kind: kind, target: target, file: workRel, proof: dedupe.file, passed: true, deduped: true, sha256: sha, buildSha256: fp, status: 'passed', note: '该对象已是 passed,且归档证明与新提交内容一致:跳过重写与重编译(去重)。' }
1881
+ }
1882
+ if (params.leanAsync !== false) {
1883
+ if (!await writeText(workRel, body)) return { ok: false, code: 'V2_WRITE_FAILED', message: 'could not write ' + workRel }
1884
+ const prevA = formalOf(target)
1885
+ await putFormal(target, Object.assign({}, prevA, {
1886
+ status: prevA.status === 'passed' ? 'passed' : 'attempted',
1887
+ file: workRel,
1888
+ decision: 'used',
1889
+ note: String(args.note || prevA.note || ''),
1890
+ async: { jobId: jobId, state: 'queued', attempts: 0, enqueuedAt: now(), startedAt: 0, settledAt: 0, exitCode: null, buildSha256: fp },
1891
+ updatedAt: now(),
1892
+ }))
1893
+ await writeFormalIndex()
1894
+ const job = await enqueueLeanJob({ jobId: jobId, kind: 'archive', key: target, rel: workRel, scope: 'project', target: target, sha: sha, buildSha: fp, buildCtx: ctx, plan: plan, memberId: memberId, archive: { kind: 'proof', target: target } })
1895
+ return leanAsyncReturn(job, {
1896
+ kind: kind, target: target, file: workRel, status: 'attempted', sha256: sha, buildSha256: fp,
1897
+ note: '工作文件已写入 ' + workRel + ';编译已入队。**只有该作业在同一个构建上下文下落地为通过,才会写入 Verified/Lean/' + target + '.lean 并把状态变为 passed**——在它落地之前,这个对象不是"已通过形式化"。',
1898
+ })
1899
+ }
1226
1900
  if (!await writeText(workRel, body)) return { ok: false, code: 'V2_WRITE_FAILED', message: 'could not write ' + workRel }
1227
1901
  const run = await leanRunFile(workRel)
1228
1902
  const prev = formalOf(target)
@@ -1530,10 +2204,13 @@ export function apply(ctx) {
1530
2204
  // 提示词里不能出现任何 Lean 字样(contract §2 / §10.1)。
1531
2205
  function formalWorkSection() {
1532
2206
  const t = formalWorkLine()
1533
- if (!t) return ''
2207
+ const notice = leanNoticeSection()
2208
+ const math = mathWorkLine()
2209
+ if (!t && !notice && !math) return ''
1534
2210
  // 回执契约(契约 §6.3):工作轮也必须被告知 formal 字段,否则"顺手形式化"里做出的难度判断
1535
2211
  // 无处可写,代理只能沉默——那正是 v2 首版死通道的成因。
1536
- return '\n' + t + formalReplyNote() + '\n'
2212
+ // 数学计算可用性行(prompts.md §2+§4):与"顺手形式化"并列注入;off 档为空串。
2213
+ return '\n' + t + formalReplyNote() + notice + math + '\n'
1537
2214
  }
1538
2215
  function capabilitiesText(role) {
1539
2216
  const maxCalls = role === 'solver' ? params.solverMaxToolCalls : params.verifierMaxToolCalls
@@ -2883,7 +3560,7 @@ export function apply(ctx) {
2883
3560
  const sub = subprocessOf()
2884
3561
  if (sub === undefined || typeof sub.spawn !== 'function') return { ok: false, exitCode: null, code: 'NO_SUBPROCESS' }
2885
3562
  let handle
2886
- try { handle = sub.spawn({ argv: paperArgvFor(engine), cwd: dirAbs, stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } }, graceMs: capMs }) }
3563
+ try { handle = await spawnHandle(sub, { argv: paperArgvFor(engine), cwd: dirAbs, stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } }, graceMs: capMs }) }
2887
3564
  catch (e) { return { ok: false, exitCode: null, code: 'SPAWN_FAILED', message: String((e && e.message) || e) } }
2888
3565
  let timedOut = false, timer = null, outcome
2889
3566
  try {
@@ -3216,6 +3893,12 @@ export function apply(ctx) {
3216
3893
  // 并发计数由 agentRegistry 推导:清空 registry 后自然归零,无需显式赋值。
3217
3894
  }
3218
3895
  await writeJson('VibeMath_State/process_epoch.json', processEpoch)
3896
+ // Lean 异步作业的崩溃恢复(spec §2.6):**同一进程 continue 不重驱**(作业还在内存队列里正常跑),
3897
+ // 只有"新进程/新会话接手这棵树"(fresh 或 stale epoch)才按 Formal/Jobs/*.json 恢复;
3898
+ // 恢复绝不置 passed(settle 的唯一判据仍然要重新校验内容哈希与 exit code)。
3899
+ if (fresh || stale) { try { await recoverLeanJobs() } catch (e) { console.error('vibe-math-v2: lean job recovery failed: ' + String((e && e.message) || e)) } }
3900
+ // 数学计算的引擎探测(异步)在 init 时先跑一次:提示词侧只读缓存现算(mathWorkLine)。
3901
+ try { await refreshMathProbe(true) } catch (e) { /* 探测失败不影响 init:mathWorkLine 会因此为空 */ }
3219
3902
  await saveAll()
3220
3903
  return { ok: true }
3221
3904
  }
@@ -3278,7 +3961,13 @@ export function apply(ctx) {
3278
3961
  if ((await readJson('qs/qs.json')) === undefined) await writeJson('qs/qs.json', [])
3279
3962
  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; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
3280
3963
  formalState = { records: {}, todo: [] } // 形式化记录随项目切换(loadState 会读新项目的 formal.json)
3281
- await loadSettings(); await migrateLegacyParams(); await loadState(); await saveAll()
3964
+ await loadSettings(); await migrateLegacyParams(); await loadState()
3965
+ // 切到/新建一个项目 = "接手这棵树的遗留作业":Formal/Jobs/*.json 里本进程不认识的记录按 §2.6 恢复
3966
+ // (queued 重入队 / running 标记 interrupted / settled 只在哈希与构建上下文都一致时补写)。
3967
+ try { await recoverLeanJobs() } catch (e) { console.error('vibe-math-v2: lean job recovery on project switch failed: ' + String((e && e.message) || e)) }
3968
+ // 切项目后引擎探测缓存过期(mathEngines/路径可能不同):重探一次再让提示词读它。
3969
+ try { await refreshMathProbe(true) } catch (e) { /* 探测失败不影响切换 */ }
3970
+ await saveAll()
3282
3971
  return { ok: true, project: slug, frameworkRoot: frameworkRoot() }
3283
3972
  }
3284
3973
 
@@ -3291,6 +3980,13 @@ export function apply(ctx) {
3291
3980
  function objParams(props, required) { return { type: 'object', properties: props, additionalProperties: false, required: required || [] } }
3292
3981
  const handlers = {}
3293
3982
  function registerTool(name, description, parameters, executeFn) { handlers[name] = executeFn }
3983
+ // math_computation 的**会话层**注册(共享模块的 host 在上面构造,绑定本会话的 params/Paths/fs)。
3984
+ // 模块调用一次 host.register(...) ⇒ handlers['math_computation'] = 该会话的 handler;
3985
+ // apply 层(工具面)在 apply 作用域单独注册一次,按名路由回这里(FREEZE §4 要求两层都在)。
3986
+ mathTool = registerMathComputation(mathHost)
3987
+ // 会话层挂载(**字面量形态**):handler 是模块为**本会话**构造的那一个(闭包住本会话的 params/Paths/fs);
3988
+ // apply 层的同名词由宿主按 handlerName 路由回这里。两条路径的名字/描述/schema 三处逐字一致。
3989
+ registerTool('math_computation', MATH_TOOL_DESCRIPTION, MATH_TOOL_SCHEMA, async function (args, agent) { return await mathTool.handler(args || {}, agent) })
3294
3990
  // Lean 工具需要知道调用者是谁(记进活动日志与索引署名)。调用者身份**显式传递**,
3295
3991
  // 不从任何全局可变状态推断(审计清单 §1.1)。无法识别时退回 'office'。
3296
3992
  function memberIdOf(agent) { try { const id = agent && agent.id ? String(agent.id) : ''; return id || 'office' } catch (e) { return 'office' } }
@@ -3301,7 +3997,7 @@ export function apply(ctx) {
3301
3997
  registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), async function () { await refreshParams(); return await getStatus() })
3302
3998
  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() })
3303
3999
  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 } })
3304
- 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 工具链的调用方式。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测),产物在 Paper/<项目>/。', 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: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, 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' }, finalPaper: { type: 'boolean' }, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] }, paperLanguage: { type: 'string', enum: ['zh', 'en'] }, paperCompilePdf: { type: 'boolean' }, paperLatexCommand: { type: 'string' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
4000
+ 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 工具链的调用方式(框架会在用户 leanArgs 之后、文件名之前自动追加 `--search-path <VibeMath 根>`,用户已显式给出就不注入);leanAsync = true(默认,后台队列:入队即返回,只有作业落地 ok 才置 passed 并写归档证明)| false(同步 await 的旧语义)。leanJobsMaxParallel = 后台编译并发上限(默认 1 = 串行);leanInitiative = off|normal|eager 控制**日常的**形式化主动性(与 formalVerify 的验证要求强度是两件事:formalVerify=off 只关验证阶段,eager 仍注入日线);leanSearchPaths = 额外 Lean 搜索路径(默认空 = 只用自动注入的 VibeMath 根;非空时先注入它们、再注入自动根)。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测),产物在 Paper/<项目>/。数学计算(工具 math_computation,回执落在 Computation/<id>/):mathComputation = off|auto|on(默认 auto;off 时提示词零提及)· mathMode = typed|typed+shell(默认 typed+shell;typed 时提示词不含 shell 兜底段且 cli 返回 REFUSED{reason:policy})· mathEngines = 允许的引擎列表(默认含 cli,cli 默认开启)· mathTimeoutMs(默认 60000,最小 1000;到时主动 terminate)· mathPackages(需预检的包,缺包只报告+给安装计划)· mathInstallScope = user|system(默认 user;system 只对当次显式调用生效、永不记忆)。', 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: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, 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' }, leanAsync: { type: 'boolean' }, leanJobsMaxParallel: { type: 'integer' }, leanInitiative: { type: 'string', enum: ['off', 'normal', 'eager'] }, leanSearchPaths: { type: 'array', items: { type: 'string' } }, mathComputation: { type: 'string', enum: ['off', 'auto', 'on'] }, mathMode: { type: 'string', enum: ['typed', 'typed+shell'] }, mathEngines: { type: 'array', items: { type: 'string' } }, mathTimeoutMs: { type: 'integer' }, mathPackages: { type: 'array', items: { type: 'string' } }, mathInstallScope: { type: 'string', enum: ['user', 'system'] }, finalPaper: { type: 'boolean' }, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] }, paperLanguage: { type: 'string', enum: ['zh', 'en'] }, paperCompilePdf: { type: 'boolean' }, paperLatexCommand: { type: 'string' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); if (args && MATH_PARAM_NAMES.some(function (k) { return k in args })) { try { await refreshMathProbe(true) } catch (e) { /* 探测失败不影响参数保存 */ } } return { ok: true, params: params } })
3305
4001
  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' } })
3306
4002
  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() })
3307
4003
  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') })
@@ -3322,17 +4018,24 @@ export function apply(ctx) {
3322
4018
  // ---- Lean 形式化验证(契约 §5)----
3323
4019
  registerTool('vibe_math_lean_run', '(member) 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) })
3324
4020
  registerTool('vibe_math_lean_archive', '(member) 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) })
3325
- 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) {
4021
+ 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. Also reports the background compile jobs (jobs[]) when leanAsync=true.', objParams({ refresh: { type: 'boolean' } }), async function (args) {
3326
4022
  const noRefresh = !!(args && args.refresh === false)
3327
4023
  const r = noRefresh ? { lib: null, proved: null, objects: Object.keys(formalRecords()).length } : await rebuildLeanLibIndexes()
4024
+ const jobs = []
4025
+ for (const j of leanJobs.values()) jobs.push(Object.assign(leanJobPublic(j), { exitCode: j.exitCode === undefined ? null : j.exitCode, paths: leanJobPaths(j), buildSha256: j.buildSha || '' }))
4026
+ jobs.sort(function (a, b) { return (b.settledAt || 0) - (a.settledAt || 0) || String(a.jobId).localeCompare(String(b.jobId)) })
3328
4027
  return {
3329
4028
  ok: true, mode: formalMode(), rebuilt: !noRefresh,
3330
4029
  counts: r, todo: formalTodo(),
3331
- 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 } }),
3332
- paths: { project: 'Formal/(相对项目根)', lib: 'VibeMath/Formal/Lib/', proved: 'VibeMath/Formal/Proved/', proofs: 'Verified/Lean/' },
3333
- hint: '复用优先:先在 Lib/ 里找现成定义;新定义用 vibe_math_lean_archive kind=\'def\' 归档,已证引理用 kind=\'lemma\'(归档前先跑通,跑不通不要入库)。',
4030
+ async: params.leanAsync !== false,
4031
+ jobs: jobs,
4032
+ 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, async: rec.async || null } }),
4033
+ paths: { project: 'Formal/(相对项目根)', lib: 'VibeMath/Formal/Lib/', proved: 'VibeMath/Formal/Proved/', proofs: 'Verified/Lean/', searchPath: vibeRoot(), jobs: 'Formal/Jobs/' },
4034
+ hint: '复用优先:先在 Lib/ 里找现成定义(vibe_math_lean_read 可看原文);新定义用 vibe_math_lean_archive kind=\'def\' 归档,已证引理用 kind=\'lemma\'(归档前先跑通,跑不通不要入库)。异步档用 jobs 字段查后台编译:**只有 state=settled 才是通过**。',
3334
4035
  }
3335
4036
  })
4037
+ registerTool('vibe_math_lean_read', '(member) Read back the original text of one archived Lean file (verbatim reuse). Only files under Formal/Lib and Formal/Proved are readable; name is id-sanitised and path escapes are rejected. Returns {ok,name,file,kind,sha256,bytes,text,truncated} (text capped at 64KB).', objParams({ name: { type: 'string' }, kind: { type: 'string', enum: ['auto', 'lib', 'proved'] } }, ['name']), async function (args) { return await leanReadTool(args) })
4038
+ registerTool('vibe_math_lean_job', '(member) Read-only view of the background Lean compile jobs. Without jobId: the session job list (state/rel/target/attempts/paths). With jobId: that job state/exitCode/receipt + archive paths. waitMs>0 waits up to that many ms for a queued/running job to settle (polling; it does not block the heartbeat) and returns the current state on timeout. Only state=settled with exitCode=0 (same content hash AND same build context) counts as passed.', objParams({ jobId: { type: 'string' }, waitMs: { type: 'integer' } }), async function (args) { return await leanJobTool(args) })
3336
4039
 
3337
4040
  // ================= slash command /vibe =================
3338
4041
  async function dispatchVibeCommand(cmd, args) {
@@ -3376,6 +4079,19 @@ export function apply(ctx) {
3376
4079
  // (激活上限)与 paper 锁续租必须由**独立于调度器**的心跳驱动。
3377
4080
  paperRetryDue: paperRetryDue,
3378
4081
  runPaperRetry: runPaperRetry,
4082
+ // Lean 异步队列(spec §2):心跳推进队列;dispose 时由 apply 级 disposer 终止在跑的编译。
4083
+ runLeanQueue: runLeanQueue,
4084
+ disposeLeanJobs: disposeLeanJobs,
4085
+ // 数学计算(FREEZE §4):心跳用 TTL 刷新探测缓存;off 档不探测(真 no-op)。
4086
+ mathProbeDue: mathProbeDue,
4087
+ refreshMathProbe: refreshMathProbe,
4088
+ mathProbe: function () { return mathProbe },
4089
+ // P2a:persona 两个文本块必须含的「归档→编辑→重跑」规则(文本取自共享模块常量,persona 不手抄)。
4090
+ mathArchiveWorkflowLine: MATH_ARCHIVE_WORKFLOW_LINE,
4091
+ // A 项:替代声明规则(文本由共享模块给出;persona 两块与每轮可用性行都必须带它)。
4092
+ mathSubstitutionRuleLine: MATH_SUBSTITUTION_RULE_LINE,
4093
+ leanQueueSize: function () { return leanQueue.length },
4094
+ leanJobsPublic: function () { const out = []; for (const j of leanJobs.values()) out.push(leanJobPublic(j)); return out },
3379
4095
  // childOwner 裁剪用:这个会话当前仍"可能再发 subagent/end"的 child(在册的 + 任务正在等的)。
3380
4096
  referencedChildIds: function () {
3381
4097
  const out = Object.keys(agentRegistry)
@@ -3421,7 +4137,7 @@ export function apply(ctx) {
3421
4137
  registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), 'vibe_math_status')
3422
4138
  registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json.', objParams({}), 'vibe_math_report')
3423
4139
  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')
3424
- 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 工具链的调用方式。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测),产物在 Paper/<项目>/。', 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: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, 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' }, finalPaper: { type: 'boolean' }, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] }, paperLanguage: { type: 'string', enum: ['zh', 'en'] }, paperCompilePdf: { type: 'boolean' }, paperLatexCommand: { type: 'string' } }), 'vibe_math_set_params')
4140
+ 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 工具链的调用方式(框架会在用户 leanArgs 之后、文件名之前自动追加 `--search-path <VibeMath 根>`,用户已显式给出就不注入);leanAsync = true(默认,后台队列:入队即返回,只有作业落地 ok 才置 passed 并写归档证明)| false(同步 await 的旧语义)。leanJobsMaxParallel = 后台编译并发上限(默认 1 = 串行);leanInitiative = off|normal|eager 控制**日常的**形式化主动性(与 formalVerify 的验证要求强度是两件事:formalVerify=off 只关验证阶段,eager 仍注入日线);leanSearchPaths = 额外 Lean 搜索路径(默认空 = 只用自动注入的 VibeMath 根;非空时先注入它们、再注入自动根)。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测),产物在 Paper/<项目>/。数学计算(工具 math_computation,回执落在 Computation/<id>/):mathComputation = off|auto|on(默认 auto;off 时提示词零提及)· mathMode = typed|typed+shell(默认 typed+shell;typed 时提示词不含 shell 兜底段且 cli 返回 REFUSED{reason:policy})· mathEngines = 允许的引擎列表(默认含 cli,cli 默认开启)· mathTimeoutMs(默认 60000,最小 1000;到时主动 terminate)· mathPackages(需预检的包,缺包只报告+给安装计划)· mathInstallScope = user|system(默认 user;system 只对当次显式调用生效、永不记忆)。', 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: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowNetwork: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, solverAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, verifierAllowScripts: { oneOf: [{ type: 'boolean' }, { type: 'string', enum: [''] }] }, 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' }, leanAsync: { type: 'boolean' }, leanJobsMaxParallel: { type: 'integer' }, leanInitiative: { type: 'string', enum: ['off', 'normal', 'eager'] }, leanSearchPaths: { type: 'array', items: { type: 'string' } }, mathComputation: { type: 'string', enum: ['off', 'auto', 'on'] }, mathMode: { type: 'string', enum: ['typed', 'typed+shell'] }, mathEngines: { type: 'array', items: { type: 'string' } }, mathTimeoutMs: { type: 'integer' }, mathPackages: { type: 'array', items: { type: 'string' } }, mathInstallScope: { type: 'string', enum: ['user', 'system'] }, finalPaper: { type: 'boolean' }, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] }, paperLanguage: { type: 'string', enum: ['zh', 'en'] }, paperCompilePdf: { type: 'boolean' }, paperLatexCommand: { type: 'string' } }), 'vibe_math_set_params')
3425
4141
  registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), 'vibe_math_setup')
3426
4142
  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')
3427
4143
  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')
@@ -3443,7 +4159,17 @@ export function apply(ctx) {
3443
4159
  // 人/代理主动调用时一样可用。
3444
4160
  registerTool('vibe_math_lean_run', '(member) 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')
3445
4161
  registerTool('vibe_math_lean_archive', '(member) 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')
3446
- 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')
4162
+ 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. Also reports the background compile jobs (jobs[]) when leanAsync=true.', objParams({ refresh: { type: 'boolean' } }), 'vibe_math_lean_lib')
4163
+ registerTool('vibe_math_lean_read', '(member) Read back the original text of one archived Lean file (verbatim reuse). Only files under Formal/Lib and Formal/Proved are readable; name is id-sanitised and path escapes are rejected. Returns {ok,name,file,kind,sha256,bytes,text,truncated} (text capped at 64KB).', objParams({ name: { type: 'string' }, kind: { type: 'string', enum: ['auto', 'lib', 'proved'] } }, ['name']), 'vibe_math_lean_read')
4164
+ registerTool('vibe_math_lean_job', '(member) Read-only view of the background Lean compile jobs. Without jobId: the session job list (state/rel/target/attempts/paths). With jobId: that job state/exitCode/receipt + archive paths. waitMs>0 waits up to that many ms for a queued/running job to settle (polling; it does not block the heartbeat) and returns the current state on timeout. Only state=settled with exitCode=0 (same content hash AND same build context) counts as passed.', objParams({ jobId: { type: 'string' }, waitMs: { type: 'integer' } }), 'vibe_math_lean_job')
4165
+
4166
+ // ── math_computation:apply 层(工具面)────────────────────────────────────────────────────
4167
+ // 共享模块的 handler 在**会话层**构造(它闭包住该会话的 params/Paths/fs);这一层只把
4168
+ // 工具名/描述/schema 挂到宿主工具面,执行时由 apply 级包装按 handlerName 路由回
4169
+ // s.handlers['math_computation'](即上面那个会话级实例)。两层都注册是 v2/v3 的硬要求。
4170
+ // **字面量形态**(名字字面量 + 描述/schema 常量):与 v3 的两层声明保持同一形状,
4171
+ // 静态扫描器/注册面审计(audit-registration、audit-v3-registration-parity)都能看见。
4172
+ registerTool('math_computation', MATH_TOOL_DESCRIPTION, MATH_TOOL_SCHEMA, 'math_computation')
3447
4173
 
3448
4174
  // /vibe slash command (registered once; routed per session)
3449
4175
  ctx.effect(() => commands.register({
@@ -3488,6 +4214,11 @@ export function apply(ctx) {
3488
4214
  // 拒绝的撰写派遣仍要重试,在途时还要续租 paper 锁(否则别的会话 2 分钟后可接管同一棵树)。
3489
4215
  // runPaperRetry() 在两种情形都不成立时立即返回(一次布尔判断)。
3490
4216
  if (typeof s.runPaperRetry === 'function') s.runPaperRetry().catch(function (e) { console.error('vibe-math-v2: paper retry failed: ' + String((e && e.message) || e)) })
4217
+ // Lean 异步队列(spec §2.2):同样独立于 scheduler.running —— 作业在收口后仍要落地。
4218
+ // runLeanQueue() 内部**不 await 编译**,所以这条心跳不会被一个编译占住。
4219
+ if (typeof s.runLeanQueue === 'function') s.runLeanQueue().catch(function (e) { console.error('vibe-math-v2: lean queue failed: ' + String((e && e.message) || e)) })
4220
+ // 数学计算的探测缓存 TTL 刷新(15 分钟或从未探测过):mathWorkLine 只读缓存,不能自己 await。
4221
+ if (typeof s.mathProbeDue === 'function' && s.mathProbeDue()) s.refreshMathProbe(false).catch(function (e) { console.error('vibe-math-v2: math probe failed: ' + String((e && e.message) || e)) })
3491
4222
  }
3492
4223
  // childOwner / sessions 裁剪:每 30 拍(约 30s)一次,成本是"会话数 × 映射数"的一次扫描。
3493
4224
  if ((++beat % 30) === 0) {
@@ -3495,7 +4226,13 @@ export function apply(ctx) {
3495
4226
  try { pruneSessions() } catch (e) { console.error('vibe-math-v2: pruneSessions failed: ' + String((e && e.message) || e)) }
3496
4227
  }
3497
4228
  }, 1000)
3498
- return () => clearInterval(t)
4229
+ return () => {
4230
+ clearInterval(t)
4231
+ // 卸载/销毁:终止在跑的编译并标记 interrupted(不留孤儿进程,spec §2.5)。
4232
+ for (const s of sessions.values()) {
4233
+ try { if (typeof s.disposeLeanJobs === 'function') s.disposeLeanJobs() } catch (e) { console.error('vibe-math-v2: lean dispose failed: ' + String((e && e.message) || e)) }
4234
+ }
4235
+ }
3499
4236
  })
3500
4237
  }
3501
4238
 
@@ -3767,6 +4504,71 @@ function paperTitleFromMd(md) {
3767
4504
  return m ? m[1].trim() : ''
3768
4505
  }
3769
4506
 
4507
+ // ============================================================================================
4508
+ // Lean 增量 + 异步(规格:docs/formal-verification.md;设计:_oneoff/lean-incremental-async-spec.md)
4509
+ // **纯函数部分**(module scope,v2/v3 逐字同构)。
4510
+ // ============================================================================================
4511
+ /** 搜索路径 flag:收敛成一个常量(不同 Lean 版本拼写可能不同,改这里即可,接口不变)。 */
4512
+ const LEAN_SEARCH_PATH_FLAG = '--search-path'
4513
+ /** 用户已显式给出搜索路径就不再注入(显式覆盖优先)。 */
4514
+ function hasLeanSearchFlag(args) {
4515
+ return (Array.isArray(args) ? args : []).some(function (a) {
4516
+ const s = String(a)
4517
+ return s === LEAN_SEARCH_PATH_FLAG || s.indexOf(LEAN_SEARCH_PATH_FLAG + '=') === 0 || s === '-R' || s === '--root'
4518
+ })
4519
+ }
4520
+ /**
4521
+ * 搜索路径注入计划(修订 §2):用户 leanArgs 已显式给过 ⇒ 什么都不注入(显式覆盖优先);
4522
+ * 否则**先注入用户给的 leanSearchPaths,再注入自动根**,去重且保持顺序。
4523
+ */
4524
+ function leanSearchPathPlan(userArgs, extraPaths, autoRoot) {
4525
+ const args = Array.isArray(userArgs) ? userArgs.map(String) : []
4526
+ if (hasLeanSearchFlag(args)) return { inject: [], paths: [], explicit: true }
4527
+ const out = []
4528
+ const seen = {}
4529
+ const add = function (p) {
4530
+ const s = String(p == null ? '' : p).trim()
4531
+ if (!s || seen[s]) return
4532
+ seen[s] = true
4533
+ out.push(s)
4534
+ }
4535
+ for (const p of (Array.isArray(extraPaths) ? extraPaths : [])) add(p)
4536
+ add(autoRoot)
4537
+ const inject = []
4538
+ for (const p of out) { inject.push(LEAN_SEARCH_PATH_FLAG, p) }
4539
+ return { inject: inject, paths: out, explicit: false }
4540
+ }
4541
+ /** 规范化文本:换行统一 \n、去行尾空白、去尾部空行——内容哈希只反映语义内容。 */
4542
+ function normalizeLeanText(t) {
4543
+ return String(t == null ? '' : t).replace(/\r\n?/g, '\n').replace(/[ \t]+$/gm, '').replace(/\n+$/, '') + '\n'
4544
+ }
4545
+ /** 内容 sha256(归档去重与作业幂等的键)。 */
4546
+ function leanContentSha(t) { return createHash('sha256').update(normalizeLeanText(t), 'utf8').digest('hex') }
4547
+ /**
4548
+ * 构建上下文(修订 §4):同一段代码在不同引擎/参数/搜索路径下结果可能不同,
4549
+ * 所以"已通过"的判定必须绑定**构建上下文**,不能只看内容。
4550
+ */
4551
+ function leanBuildContext(engine, args, searchPaths) {
4552
+ return JSON.stringify({
4553
+ engine: String(engine == null ? '' : engine),
4554
+ args: (Array.isArray(args) ? args : []).map(String),
4555
+ searchPaths: (Array.isArray(searchPaths) ? searchPaths : []).map(String),
4556
+ })
4557
+ }
4558
+ /** 作业指纹 = sha256(内容 + 构建上下文)(修订 §4:jobId 取它的前 12 位)。 */
4559
+ function leanJobFingerprint(contentText, buildCtx) {
4560
+ return createHash('sha256').update(normalizeLeanText(contentText) + '\n' + String(buildCtx || ''), 'utf8').digest('hex')
4561
+ }
4562
+ /** jobId = `<target|name>-<指纹[0:12]>`:同一内容 + 同一构建上下文天然只有一个作业。 */
4563
+ function leanJobId(key, fingerprint) {
4564
+ const k = String(key == null ? '' : key).replace(/[^A-Za-z0-9_.-]/g, '_').replace(/^_+|_+$/g, '').slice(0, 60) || 'job'
4565
+ return k + '-' + String(fingerprint == null ? '' : fingerprint).slice(0, 12)
4566
+ }
4567
+ /** "已落地且通过"的**唯一**判据:state=settled 且 exit 0 且未超时未中断(内容哈希由 settle 校验)。 */
4568
+ function leanJobSettledOk(rec) { return !!rec && rec.state === 'settled' && rec.exitCode === 0 && !rec.timedOut && !rec.interrupted }
4569
+ /** 主动性档位(修订 §1):与"验证时的要求强度"(formalVerify)是两件事。 */
4570
+ const LEAN_INITIATIVE_MODES = ['off', 'normal', 'eager']
4571
+
3770
4572
  export const __testHelpers = {
3771
4573
  uuid,
3772
4574
  shortId,
@@ -3781,6 +4583,17 @@ export const __testHelpers = {
3781
4583
  parseProgress,
3782
4584
  sanitizeToolFilter,
3783
4585
  registeredToolsFromError,
4586
+ // Lean 增量/异步(§7 的纯函数守卫面)
4587
+ LEAN_SEARCH_PATH_FLAG,
4588
+ hasLeanSearchFlag,
4589
+ leanSearchPathPlan,
4590
+ normalizeLeanText,
4591
+ leanContentSha,
4592
+ leanBuildContext,
4593
+ leanJobFingerprint,
4594
+ leanJobId,
4595
+ leanJobSettledOk,
4596
+ LEAN_INITIATIVE_MODES,
3784
4597
  // final paper(spec §6 的纯函数守卫面)
3785
4598
  PAPER_SKELETON,
3786
4599
  PAPER_NO_EVIDENCE,