dsh-vibe-math 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +156 -389
- package/README.md +156 -390
- package/cordis.patch.yml +56 -14
- package/docs/AUDIT-CHECKLIST.md +2 -2
- package/docs/final-paper.md +1 -0
- package/docs/formal-verification.md +184 -29
- package/docs/math-computation.md +121 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.0.en.md +151 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.0.md +115 -0
- package/docs/test-timing.md +24 -12
- package/installer.js +7 -4
- package/package.json +18 -2
- package/prompt-corpus-persona/persona-corpus.json +12 -12
- package/prompt-corpus-persona/persona-corpus.md +116 -32
- package/prompt-corpus-v2/formal-verify-v2.json +116 -92
- package/prompt-corpus-v2/formal-verify-v2.md +1430 -55
- package/prompt-corpus-v3/formal-verify-v3.json +101 -53
- package/prompt-corpus-v3/formal-verify-v3.md +1001 -22
- package/prompt-corpus-v4/formal-verify-v4.json +14 -14
- package/prompt-corpus-v4/formal-verify-v4.md +247 -14
- package/prompt-corpus-v5/prompt-corpus-v5.json +62 -62
- package/prompt-corpus-v5/prompt-corpus-v5.md +570 -3
- package/tests/audit-math-computation-contract.mjs +191 -0
- package/tests/audit-math-computation-parity.mjs +169 -0
- package/tests/audit-math-computation-sensitivity.mjs +231 -0
- package/tests/audit-persona-sensitivity.mjs +4 -4
- package/tests/audit-persona-surface.test.mjs +38 -10
- package/tests/audit-v5-integrity.mjs +14 -0
- package/tests/e2e-v5-round2.test.mjs +452 -2
- package/tests/formal-verify-v2.test.mjs +292 -6
- package/tests/formal-verify-v3.test.mjs +271 -4
- package/tests/formal-verify-v4.test.mjs +295 -6
- package/tests/formal-verify-v5.test.mjs +9 -1
- package/tests/helpers/math-computation-fake-seam.mjs +135 -0
- package/tests/math-computation-shared.test.mjs +504 -0
- package/tests/prompt-v5-integrity.test.mjs +5 -5
- package/vibe-math-v2/agent.cordis.yml +20 -8
- package/vibe-math-v2/math-computation.js +936 -0
- package/vibe-math-v2/math-engines.js +156 -0
- package/vibe-math-v2/vibe-math-v2.js +837 -40
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v3/agent.cordis.yml +20 -8
- package/vibe-math-v3/math-computation.js +936 -0
- package/vibe-math-v3/math-engines.js +156 -0
- package/vibe-math-v3/vibe-math-v3.js +828 -47
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v4/agent.cordis.yml +34 -6
- package/vibe-math-v4/math-computation.js +936 -0
- package/vibe-math-v4/math-engines.js +156 -0
- package/vibe-math-v4/vibe-math-v4.js +801 -31
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v5/agent.cordis.yml +38 -6
- package/vibe-math-v5/math-computation.js +936 -0
- package/vibe-math-v5/math-engines.js +156 -0
- package/vibe-math-v5/vibe-math-v5.js +1120 -37
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +83 -3
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
|
@@ -18,6 +18,24 @@
|
|
|
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_PERSONA_TOOL_LINE,
|
|
34
|
+
MATH_RULE_LINES,
|
|
35
|
+
normalizeMathParams,
|
|
36
|
+
mathAvailabilityLine,
|
|
37
|
+
probeMathEngines,
|
|
38
|
+
} from './math-computation.js'
|
|
21
39
|
|
|
22
40
|
// Standing mount: DSH mounts each agent preset ONCE per preset and joins every
|
|
23
41
|
// session that names it to that SAME plugin instance (see @deepseek-ai/dsh-agent-presets).
|
|
@@ -164,12 +182,24 @@ export function apply(ctx) {
|
|
|
164
182
|
leanCommand: 'lean', // 要执行的 Lean 可执行文件(例:'lake')
|
|
165
183
|
leanArgs: [], // 插在文件名之前的附加参数(例:['env','lean'] 配合 leanCommand='lake')
|
|
166
184
|
leanTimeoutMs: 120000, // 单次 Lean 运行的超时上限(毫秒)
|
|
185
|
+
leanAsync: true, // true = Lean 编译走后台队列(入队即返回)| false = 同步 await(今天的语义)
|
|
186
|
+
leanJobsMaxParallel: 1, // 后台编译并发上限(默认 1 = 串行;可调大以并行编译)
|
|
187
|
+
leanInitiative: 'normal', // 日常流程中的形式化主动性:off | normal | eager(与 formalVerify 的"验证要求强度"是两件事)
|
|
188
|
+
leanSearchPaths: [], // 额外搜索路径(默认空 = 只用自动注入的 VibeMath 根);非空时先注入它们、再注入自动根
|
|
167
189
|
// ---- 最终论文(规格:docs/final-paper.md;v2/v3 为单作者变体)----
|
|
168
190
|
finalPaper: true, // 收口时是否自动撰写最终论文(false 只关自动触发;/vibe paper 仍可用)
|
|
169
191
|
paperFormat: 'both', // both = md + tex | md = 只写 markdown | tex = 只写 latex
|
|
170
192
|
paperLanguage: 'zh', // zh = 中文(ctexart/xelatex 优先)| en = 英文(article/pdflatex 优先)
|
|
171
193
|
paperCompilePdf: true, // 检测到 LaTeX 时是否编译 paper.pdf
|
|
172
194
|
paperLatexCommand: '', // 指定 LaTeX 引擎(空 = 按语言探测 xelatex/latexmk/pdflatex/lualatex/tectonic)
|
|
195
|
+
// ---- 数学计算 math_computation(规格:docs/math-computation.md;六参数已冻结,拼写不得改)----
|
|
196
|
+
// 值取共享模块的 MATH_PARAM_DEFAULTS(四套逐字一致);数组必须**拷贝**,否则四套会共享同一个默认数组。
|
|
197
|
+
mathComputation: MATH_PARAM_DEFAULTS.mathComputation, // off | auto | on(auto = 探测到任一允许引擎才工作)
|
|
198
|
+
mathMode: MATH_PARAM_DEFAULTS.mathMode, // typed | typed+shell(后者才允许提示词里的 shell 兜底段)
|
|
199
|
+
mathEngines: MATH_PARAM_DEFAULTS.mathEngines.slice(), // 允许的引擎(含 cli;cli 默认开启,受 mathMode/mathEngines 双闸)
|
|
200
|
+
mathTimeoutMs: MATH_PARAM_DEFAULTS.mathTimeoutMs, // 单次计算超时上限(毫秒,下界 1000)
|
|
201
|
+
mathPackages: MATH_PARAM_DEFAULTS.mathPackages.slice(), // 需要预检的包(缺包只报告 + 给安装计划)
|
|
202
|
+
mathInstallScope: MATH_PARAM_DEFAULTS.mathInstallScope, // user | system(system 只对当次显式调用生效,永不记忆)
|
|
173
203
|
}
|
|
174
204
|
let params = Object.assign({}, DEFAULT_PARAMS)
|
|
175
205
|
let scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null } // activeCount 由 activeCount() 从 agentRegistry 推导,不再作为字段
|
|
@@ -289,12 +319,23 @@ export function apply(ctx) {
|
|
|
289
319
|
{ 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
320
|
{ name: 'leanCommand', type: 'string', description: '要执行的 Lean 可执行文件(默认 lean;用 lake 时配合 leanArgs=["env","lean"])', suggestion: 'lean' },
|
|
291
321
|
{ name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加命令行参数(默认空)', suggestion: [] },
|
|
292
|
-
{ name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,默认 120000,最小 1000)', suggestion: 120000 },
|
|
322
|
+
{ name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,默认 120000,最小 1000);异步档下它同时是**每个后台编译作业**的预算(到时主动 terminate,作业记 timeout、对象留在 attempted)', suggestion: 120000 },
|
|
323
|
+
{ 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 },
|
|
324
|
+
{ name: 'leanJobsMaxParallel', type: 'integer', description: '后台 Lean 编译的并发上限(默认 1 = 串行,保持可预测的资源占用;调大可并行编译多个作业)', suggestion: 1 },
|
|
325
|
+
{ name: 'leanInitiative', type: 'string', enum: LEAN_INITIATIVE_MODES.slice(), description: '日常流程中的形式化主动性:off(不主动,只在验证提示词按 formalVerify 的要求做)| normal(默认:顺手把有价值且可能复用的东西形式化)| eager(更主动:日常就主动把有价值的小引理/命题/定义形式化)。注意它与 formalVerify(验证时的要求强度:off|encourage|require)是**两件事**', suggestion: 'normal' },
|
|
326
|
+
{ name: 'leanSearchPaths', type: 'array', items: { type: 'string' }, description: '额外 Lean 搜索路径(默认空数组 = 只用框架自动注入的 VibeMath 根)。非空时按顺序先注入这里给的路径、再注入自动根(去重);若 leanArgs 里已显式给了 --search-path/-R/--root,则完全尊重用户配置、不注入任何东西', suggestion: [] },
|
|
293
327
|
{ name: 'finalPaper', type: 'boolean', description: '收口(严格终止)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已检验通过的命题/解法/成果整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
|
|
294
328
|
{ name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md;tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
|
|
295
329
|
{ name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
|
|
296
330
|
{ name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志', suggestion: true },
|
|
297
331
|
{ name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。该命令解析不到时按"未检测到"降级(只留 tex+md)', suggestion: '' },
|
|
332
|
+
// ---- 数学计算 math_computation(六参数冻结;描述与 prompts.md §1/§6 口径一致)----
|
|
333
|
+
{ name: 'mathComputation', type: 'enum', options: ['off', 'auto', 'on'], description: '数学计算总开关:off = 真无操作(提示词零提及);auto = 探测到 mathEngines 里任一允许引擎才工作;on = 同上(探测失败时工具仍返回可执行的安装指引,而不是假装可用)', suggestion: 'auto' },
|
|
334
|
+
{ name: 'mathMode', type: 'enum', options: ['typed', 'typed+shell'], description: '计算策略:typed+shell(默认)= 工具不可用时允许宿主 shell 兜底,但结论必须标注"未经工具归档(shell 路径)";typed = 只用工具路径(提示词里不出现 shell 兜底段,engine:"cli" 返回 REFUSED{reason:policy})', suggestion: 'typed+shell' },
|
|
335
|
+
{ name: 'mathEngines', type: 'string[]', description: '允许的引擎列表(默认 python|r|octave|julia|matlab|maple|wolfram|cli)。cli 默认开启且走同一套超时/输出上限/回执;从列表里移除某引擎即禁用(商业引擎只探测+许可,永不安装)', suggestion: MATH_PARAM_DEFAULTS.mathEngines.slice() },
|
|
336
|
+
{ name: 'mathTimeoutMs', type: 'integer', description: '单次数学计算的超时上限(毫秒,默认 60000,最小 1000);到时主动 terminate 并把回执标为 MATH_TIMEOUT', suggestion: 60000 },
|
|
337
|
+
{ name: 'mathPackages', type: 'string[]', description: '需要预检的包(默认空)。缺包只报告 + 给"用户自装指引"或"代理代装计划",不会执行脚本,也永不自动安装', suggestion: [] },
|
|
338
|
+
{ name: 'mathInstallScope', type: 'string', enum: ['user', 'system'], description: '安装作用域:user(默认,用户级目录);system 只对当次显式调用生效、永不记忆(不会写进状态文件)', suggestion: 'user' },
|
|
298
339
|
]
|
|
299
340
|
|
|
300
341
|
// ================= fs =================
|
|
@@ -359,6 +400,19 @@ export function apply(ctx) {
|
|
|
359
400
|
if (isWindows()) return 'Remove-Item -Force -LiteralPath ' + psQuote(path) + ' -ErrorAction SilentlyContinue'
|
|
360
401
|
return 'rm -f ' + shQuote(path)
|
|
361
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* `subprocess.spawn` 的契约返回**句柄**(带 `done`),但桩宿主/测试替身常常写成 `async spawn(...)`
|
|
405
|
+
* ——那样返回的是 Promise,`handle.done` 是 undefined,`await handle.done` 得到 undefined,随后
|
|
406
|
+
* `outcome.exitCode` 抛 "Cannot read properties of undefined (reading 'exitCode')",被 catch 成一条
|
|
407
|
+
* 误导性的 "mkdir 失败"(实测:第二个会话建项目目录时刷出这条)。
|
|
408
|
+
* 这里统一兼容一次:Promise 就先 await,句柄缺 `done` 才如实报错。
|
|
409
|
+
*/
|
|
410
|
+
async function spawnHandle(subprocess, spec) {
|
|
411
|
+
const raw = subprocess.spawn(spec)
|
|
412
|
+
const handle = (raw && typeof raw.then === 'function') ? await raw : raw
|
|
413
|
+
if (!handle || handle.done === undefined) throw new Error('subprocess.spawn returned no handle.done')
|
|
414
|
+
return handle
|
|
415
|
+
}
|
|
362
416
|
async function runShell(script, cwd) {
|
|
363
417
|
const subprocess = subprocessOf()
|
|
364
418
|
if (subprocess === undefined) return { ok: false, error: 'no-subprocess' }
|
|
@@ -366,9 +420,9 @@ export function apply(ctx) {
|
|
|
366
420
|
const argv = isWindows()
|
|
367
421
|
? ['powershell', '-NoProfile', '-NonInteractive', '-Command', script]
|
|
368
422
|
: ['/bin/sh', '-c', script]
|
|
369
|
-
const handle = subprocess
|
|
423
|
+
const handle = await spawnHandle(subprocess, { argv: argv, cwd: cwd || workspaceRoot(), stdio: { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' }, graceMs: 20000 })
|
|
370
424
|
const outcome = await handle.done
|
|
371
|
-
return { ok: outcome.exitCode === 0, exitCode: outcome.exitCode }
|
|
425
|
+
return { ok: !!outcome && outcome.exitCode === 0, exitCode: (outcome && outcome.exitCode !== undefined) ? outcome.exitCode : null }
|
|
372
426
|
} catch (e) { return { ok: false, error: String((e && e.message) || e) } }
|
|
373
427
|
}
|
|
374
428
|
/**
|
|
@@ -385,12 +439,13 @@ export function apply(ctx) {
|
|
|
385
439
|
shellWarned[key] = true
|
|
386
440
|
console.error('vibe-math-v2: ' + where + ' failed (' + why + ')')
|
|
387
441
|
}
|
|
388
|
-
// 目录布局(docs/formal-verification.md §3
|
|
389
|
-
// 项目内:Formal/(对象形式化工作文件 + Index.md + TODO.md)、Verified/Lean
|
|
442
|
+
// 目录布局(docs/formal-verification.md §3 / docs/math-computation.md §路径):
|
|
443
|
+
// 项目内:Formal/(对象形式化工作文件 + Index.md + TODO.md)、Verified/Lean/(归档证明)、
|
|
444
|
+
// Computation/(math_computation 的可复核回执:Computation/<runId>/ 与 Computation/installs/)
|
|
390
445
|
// 全局(**不在项目内**,跨项目复用):<VibeMath 根>/Formal/Lib/(可复用定义)、Formal/Proved/(已证引理)
|
|
391
446
|
async function ensureDirs() {
|
|
392
447
|
const base = frameworkRoot()
|
|
393
|
-
const dirs = ['qs', 'Propos', 'Reliable', 'Verified', 'Verified/Lean', 'Verification_logs', 'Progress_Logs', 'VibeMath_State', 'Formal']
|
|
448
|
+
const dirs = ['qs', 'Propos', 'Reliable', 'Verified', 'Verified/Lean', 'Verification_logs', 'Progress_Logs', 'VibeMath_State', 'Formal', 'Formal/Jobs', 'Computation']
|
|
394
449
|
const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d }))
|
|
395
450
|
const r = await runShell(mkdirCmd(paths))
|
|
396
451
|
if (!r || !r.ok) warnShellOnce('ensureDirs (mkdir ' + base + ')', r)
|
|
@@ -403,6 +458,121 @@ export function apply(ctx) {
|
|
|
403
458
|
return r
|
|
404
459
|
}
|
|
405
460
|
|
|
461
|
+
// ================= 数学计算 math_computation:会话侧接线(共享模块,FREEZE §4/§5) =================
|
|
462
|
+
// 探测量(是否装了 python/r/…)是异步的,而提示词是同步构造的 ⇒ 这里维护一个会话级缓存,
|
|
463
|
+
// 在 init / 参数变化 / TTL 到期时刷新,提示词侧只读缓存现算(与 formalVerify 的动态纪律一致)。
|
|
464
|
+
const MATH_PROBE_TTL_MS = 15 * 60 * 1000
|
|
465
|
+
let mathProbe = null
|
|
466
|
+
let mathProbeAt = 0
|
|
467
|
+
/**
|
|
468
|
+
* `host.spawn` 适配:模块只给 `{argv, cwd, timeoutMs, stdoutCap, stderrCap}`,超时/终止由**接线方**负责
|
|
469
|
+
* (契约:超时必须 handle.terminate())。返回**完整** stdout/stderr——模块自己落盘完整版,只在返回体裁 64KB,
|
|
470
|
+
* 所以 stdio 上限要宽(默认 4MB),否则"超大输出"永远测不出完整落盘。
|
|
471
|
+
*/
|
|
472
|
+
async function mathSpawn(spec) {
|
|
473
|
+
const sub = subprocessOf()
|
|
474
|
+
if (sub === undefined || typeof sub.spawn !== 'function') return null // 模块映射为 MATH_NO_SUBPROCESS
|
|
475
|
+
const s = spec || {}
|
|
476
|
+
const cap = Math.max(1000, Number(s.timeoutMs) || Number(params.mathTimeoutMs) || 60000)
|
|
477
|
+
const outCap = Math.max(4096, Number(s.stdoutCap) || 4 * 1024 * 1024)
|
|
478
|
+
const errCap = Math.max(4096, Number(s.stderrCap) || 4 * 1024 * 1024)
|
|
479
|
+
const started = now()
|
|
480
|
+
let handle
|
|
481
|
+
try {
|
|
482
|
+
handle = await spawnHandle(sub, {
|
|
483
|
+
argv: s.argv,
|
|
484
|
+
cwd: s.cwd || frameworkRoot(),
|
|
485
|
+
stdio: { stdin: 'ignore', stdout: { maxBytes: outCap }, stderr: { maxBytes: errCap } },
|
|
486
|
+
graceMs: cap,
|
|
487
|
+
})
|
|
488
|
+
} catch (e) { return null }
|
|
489
|
+
let timedOut = false, killed = false, timer = null, outcome
|
|
490
|
+
try {
|
|
491
|
+
outcome = await Promise.race([
|
|
492
|
+
handle.done,
|
|
493
|
+
new Promise(function (resolve) {
|
|
494
|
+
timer = setTimeout(function () {
|
|
495
|
+
timedOut = true
|
|
496
|
+
killed = true
|
|
497
|
+
try { if (handle && typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* best effort */ }
|
|
498
|
+
resolve({ exitCode: null })
|
|
499
|
+
}, cap)
|
|
500
|
+
}),
|
|
501
|
+
])
|
|
502
|
+
} catch (e) { if (timer !== null) clearTimeout(timer); return null }
|
|
503
|
+
if (timer !== null) clearTimeout(timer)
|
|
504
|
+
let out = '', err = ''
|
|
505
|
+
try { if (handle.collected && handle.collected.stdout) out = handle.collected.stdout.readFrom(0).text } catch (e) { /* best effort */ }
|
|
506
|
+
try { if (handle.collected && handle.collected.stderr) err = handle.collected.stderr.readFrom(0).text } catch (e) { /* best effort */ }
|
|
507
|
+
return {
|
|
508
|
+
exit: (outcome && outcome.exitCode !== undefined) ? outcome.exitCode : null,
|
|
509
|
+
timedOut: timedOut, killed: killed, ms: now() - started,
|
|
510
|
+
stdout: String(out == null ? '' : out), stderr: String(err == null ? '' : err),
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/** host.exists:项目根相对路径是否存在。 */
|
|
514
|
+
async function mathExists(rel) {
|
|
515
|
+
try { const t = await fsTarget(rel); const s = await fs.stat(t); return s !== undefined } catch (e) { return false }
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* host.listDir(**可选**,FREEZE §4):模块用它数 `Computation/` 下的 run 目录数(>200 只告警、永不删除)。
|
|
519
|
+
* 返回 DSH `fs.listDir` 的原样条目({name,type})——模块按 `type === 'directory'` 计数,
|
|
520
|
+
* 所以**不要**在这里把条目降级成字符串数组。宿主 fs 没有该能力时整个回调都不声明(见下面的条件字段),
|
|
521
|
+
* 否则模块每次运行都会去问一个不存在的服务。
|
|
522
|
+
*/
|
|
523
|
+
async function mathListDir(rel) {
|
|
524
|
+
try { const t = await fsTarget(rel); const ents = await fs.listDir(t); return Array.isArray(ents) ? ents : [] } catch (e) { return [] }
|
|
525
|
+
}
|
|
526
|
+
/** 会话级 host:参数是**活引用**(函数),回执里写 designator='vibe-math-v2'。 */
|
|
527
|
+
const mathHost = {
|
|
528
|
+
// 模块注册时只**交回** handler/description/parameters;真正的挂载是 mathTool 之后的**字面量**两行。
|
|
529
|
+
// (会话层写 handlers 表、apply 层挂宿主工具面;两条路径名字/描述/schema 完全一致。)
|
|
530
|
+
register: function (name, description, parameters, handler) { /* 挂载见 mathTool 之后的字面量两行 */ },
|
|
531
|
+
params: function () { return params },
|
|
532
|
+
projectRoot: function () { return frameworkRoot() },
|
|
533
|
+
designator: 'vibe-math-v2',
|
|
534
|
+
// 宿主明知没有 subprocess 服务时直接说 false ⇒ 模块立即返回 MATH_NO_SUBPROCESS(不再逐个探引擎)。
|
|
535
|
+
hasSubprocess: function () { const sub = subprocessOf(); return !!(sub && typeof sub.spawn === 'function') },
|
|
536
|
+
// **可选能力,按宿主实际情况声明**:宿主 fs 没有 listDir 就不给这个回调(模块据此跳过
|
|
537
|
+
// "每项目 run 目录数 >200" 的保留检查,而不是每次运行都白问一次)。
|
|
538
|
+
listDir: (typeof fs.listDir === 'function') ? mathListDir : undefined,
|
|
539
|
+
writeText: async function (rel, text) { return await writeText(rel, text) },
|
|
540
|
+
readText: async function (rel) { return await readText(rel) },
|
|
541
|
+
exists: mathExists,
|
|
542
|
+
resolveExecutable: async function (cmd) {
|
|
543
|
+
const sub = subprocessOf()
|
|
544
|
+
if (sub === undefined || typeof sub.resolveExecutable !== 'function') throw new Error('no subprocess service')
|
|
545
|
+
const p = await sub.resolveExecutable(String(cmd))
|
|
546
|
+
if (typeof p !== 'string' || !p) throw new Error('not found: ' + String(cmd))
|
|
547
|
+
return p
|
|
548
|
+
},
|
|
549
|
+
spawn: mathSpawn,
|
|
550
|
+
log: function (kind, msg) { logActivity('math', String(kind) + ': ' + String(msg)) },
|
|
551
|
+
}
|
|
552
|
+
// 模块只调用一次 host.register(...)(FREEZE §4)⇒ 上面那个回调就是会话层注册。
|
|
553
|
+
// 注意:真实注册必须等到 `const handlers = {}` 与 `function registerTool(...)` 就绪之后
|
|
554
|
+
// (`handlers` 是 const,提前调用会撞 TDZ)⇒ 这里只留槽位,注册放在会话工具表旁。
|
|
555
|
+
let mathTool = null
|
|
556
|
+
/** 刷新探测缓存。`mathComputation:'off'` ⇒ 不探测、不缓存、不注入(真 no-op)。 */
|
|
557
|
+
async function refreshMathProbe(force) {
|
|
558
|
+
if (params.mathComputation === 'off') { mathProbe = null; mathProbeAt = 0; return null }
|
|
559
|
+
if (!mathTool) return null
|
|
560
|
+
try { mathProbe = await mathTool.probe(force ? { refresh: true } : undefined); mathProbeAt = now() } catch (e) { mathProbe = null }
|
|
561
|
+
return mathProbe
|
|
562
|
+
}
|
|
563
|
+
/** 心跳里的 TTL 刷新(每拍一次布尔判断,成本可忽略)。 */
|
|
564
|
+
function mathProbeDue() { return params.mathComputation !== 'off' && (!mathProbe || (now() - mathProbeAt) >= MATH_PROBE_TTL_MS) }
|
|
565
|
+
/**
|
|
566
|
+
* 每轮可用性行(中文)。由模块的 `mathAvailabilityLine(probe,'zh',mathMode)` 按档位拼装:
|
|
567
|
+
* 只有 `typed+shell` 才含 shell 兜底句(`mathMode:'typed'` 天然不含,守卫据此断言)。
|
|
568
|
+
* `off` 档返回空串——提示词零提及(对齐 formalVerify:'off' 的真 no-op 纪律)。
|
|
569
|
+
*/
|
|
570
|
+
function mathWorkLine() {
|
|
571
|
+
if (params.mathComputation === 'off') return ''
|
|
572
|
+
if (!mathProbe) return '' // 还没探测过 ⇒ 不注入(不撒谎;init 会先探一次)
|
|
573
|
+
return '\n' + mathAvailabilityLine(mathProbe, 'zh', params.mathMode) + '\n'
|
|
574
|
+
}
|
|
575
|
+
|
|
406
576
|
// ================= settings =================
|
|
407
577
|
function sanitizeParams(obj) {
|
|
408
578
|
const out = {}
|
|
@@ -417,9 +587,14 @@ export function apply(ctx) {
|
|
|
417
587
|
maxParallelThreshold: 1, solverMaxRounds: 1, verifierCount: 2, debateMaxRounds: 1,
|
|
418
588
|
directionsPerSolver: 1, tickIntervalMs: 200, reportIntervalMs: 1000, maxExplorerRetries: 1,
|
|
419
589
|
}
|
|
590
|
+
// 数学计算六参数的显式强制(FREEZE §5.3):交给共享模块的 normalizeMathParams,它按
|
|
591
|
+
// enum/array/integer 逐键判型——`'false'`/字符串/错误枚举一律回退默认值,**绝不**落到
|
|
592
|
+
// 下面 `else { out[k] = v }` 的尾巴(那会把任意字符串当合法值放行)。
|
|
593
|
+
const mathNorm = normalizeMathParams(obj)
|
|
420
594
|
for (const k of Object.keys(DEFAULT_PARAMS)) {
|
|
421
595
|
if (!(k in obj)) continue
|
|
422
596
|
const v = obj[k]
|
|
597
|
+
if (MATH_PARAM_NAMES.indexOf(k) !== -1) { out[k] = Object.prototype.hasOwnProperty.call(mathNorm, k) ? mathNorm[k] : DEFAULT_PARAMS[k]; continue }
|
|
423
598
|
if (intFields.indexOf(k) !== -1) {
|
|
424
599
|
const n = Number(v)
|
|
425
600
|
if (!Number.isFinite(n)) { out[k] = DEFAULT_PARAMS[k]; continue }
|
|
@@ -437,6 +612,19 @@ export function apply(ctx) {
|
|
|
437
612
|
else if (k === 'proposPriorityAdjust') { out[k] = (v === 'none' || v === 'progress-graded') ? v : DEFAULT_PARAMS[k] }
|
|
438
613
|
else if (k === 'solverAllowNetwork' || k === 'verifierAllowNetwork' || k === 'solverAllowScripts' || k === 'verifierAllowScripts') { out[k] = (v === true || v === false || v === '') ? v : DEFAULT_PARAMS[k] }
|
|
439
614
|
else if (k === 'finalPaper' || k === 'paperCompilePdf') { out[k] = (v === true || v === false) ? v : DEFAULT_PARAMS[k] }
|
|
615
|
+
// leanAsync 必须**显式**归一化:`else { out[k] = v }` 会把字符串 'false' 当真理放行(spec §1.1)。
|
|
616
|
+
else if (k === 'leanAsync') { out[k] = (v === true || v === false) ? v : DEFAULT_PARAMS[k] }
|
|
617
|
+
else if (k === 'leanInitiative') { out[k] = LEAN_INITIATIVE_MODES.indexOf(v) !== -1 ? v : DEFAULT_PARAMS[k] }
|
|
618
|
+
else if (k === 'leanJobsMaxParallel') {
|
|
619
|
+
const n = Number(v)
|
|
620
|
+
out[k] = Number.isFinite(n) ? Math.max(1, Math.min(8, Math.floor(n))) : DEFAULT_PARAMS[k]
|
|
621
|
+
}
|
|
622
|
+
// leanSearchPaths:显式数组分支(字符串/非数组一律回退默认空数组),并去掉空串与重复项。
|
|
623
|
+
else if (k === 'leanSearchPaths') {
|
|
624
|
+
if (!Array.isArray(v)) { out[k] = DEFAULT_PARAMS[k]; continue }
|
|
625
|
+
const seen = {}
|
|
626
|
+
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 })
|
|
627
|
+
}
|
|
440
628
|
else if (k === 'paperFormat') { out[k] = (v === 'both' || v === 'md' || v === 'tex') ? v : DEFAULT_PARAMS[k] }
|
|
441
629
|
else if (k === 'paperLanguage') { out[k] = (v === 'zh' || v === 'en') ? v : DEFAULT_PARAMS[k] }
|
|
442
630
|
else { out[k] = v }
|
|
@@ -649,7 +837,6 @@ export function apply(ctx) {
|
|
|
649
837
|
* (无服务 / 无可执行文件 / spawn 失败 / 超时 / 非零退出)都变成可读结果。
|
|
650
838
|
*/
|
|
651
839
|
async function leanRunFile(relPath, timeoutMs) {
|
|
652
|
-
const started = now()
|
|
653
840
|
const rel = String(relPath || '').trim()
|
|
654
841
|
if (!rel) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'file is required' }
|
|
655
842
|
// 路径守卫:只允许执行 VibeMath 树内的文件,构造出来的路径不能让我们跑工作区之外的东西。
|
|
@@ -657,21 +844,35 @@ export function apply(ctx) {
|
|
|
657
844
|
if (abs === null) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'Lean 文件必须位于 ' + vibeRoot() + '/ 之内(收到 ' + rel + ')' }
|
|
658
845
|
if (!/\.lean$/.test(abs)) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '只有 .lean 文件可以执行' }
|
|
659
846
|
if (await readTextAbs(abs) === undefined) return { ok: false, code: 'V2_NOT_FOUND', message: 'no such file: ' + rel }
|
|
847
|
+
return await leanExecFile(rel, abs, timeoutMs)
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* 已解析路径的**实际执行**(同步档与后台作业共用同一段:argv 注入、超时 terminate、输出截断)。
|
|
851
|
+
* 搜索路径注入(spec §3):`[exe, ...用户 leanArgs, --search-path <VibeMath 根>, <file>]`;
|
|
852
|
+
* 用户已显式给过 `--search-path`/`-R`/`--root` 就不再注入(显式覆盖优先)。
|
|
853
|
+
*/
|
|
854
|
+
async function leanExecFile(rel, abs, timeoutMs, plan, onHandle) {
|
|
855
|
+
const started = now()
|
|
660
856
|
const sub = subprocessOf()
|
|
661
857
|
if (sub === undefined || typeof sub.spawn !== 'function') {
|
|
662
858
|
return { ok: false, code: 'NO_SUBPROCESS', message: 'the host exposes no subprocess service; Lean cannot be executed here', file: rel, ms: 0 }
|
|
663
859
|
}
|
|
664
860
|
const cap = Math.max(1000, Number(timeoutMs) || Number(params.leanTimeoutMs) || 120000)
|
|
861
|
+
// 构建计划:异步作业用**入队时冻结**的计划(修订 §4:结果只对那个上下文有效);同步档现算。
|
|
862
|
+
const p = plan || leanBuildPlan()
|
|
665
863
|
let exe
|
|
666
864
|
try {
|
|
667
|
-
exe = await sub.resolveExecutable(String(
|
|
865
|
+
exe = await sub.resolveExecutable(String(p.engine || 'lean'))
|
|
668
866
|
} catch (e) {
|
|
669
|
-
return { ok: false, code: 'LEAN_NOT_FOUND', message: 'cannot resolve "' + String(
|
|
867
|
+
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
868
|
}
|
|
671
|
-
const
|
|
869
|
+
const inject = Array.isArray(p.inject) ? p.inject : leanSearchPathPlan(p.args, p.searchPaths, vibeRoot()).inject
|
|
870
|
+
const argv = [exe].concat((Array.isArray(p.args) ? p.args : []).map(String)).concat(inject).concat([abs])
|
|
871
|
+
const searchPath = (Array.isArray(p.searchPaths) && p.searchPaths.length) ? p.searchPaths.join(',') : vibeRoot()
|
|
872
|
+
logActivity('formal', 'Lean 运行 ' + rel + '(search-path=' + searchPath + ')')
|
|
672
873
|
let handle
|
|
673
874
|
try {
|
|
674
|
-
handle = sub
|
|
875
|
+
handle = await spawnHandle(sub, {
|
|
675
876
|
argv: argv,
|
|
676
877
|
cwd: frameworkRoot(),
|
|
677
878
|
stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } },
|
|
@@ -680,6 +881,8 @@ export function apply(ctx) {
|
|
|
680
881
|
} catch (e) {
|
|
681
882
|
return { ok: false, code: 'LEAN_SPAWN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
|
|
682
883
|
}
|
|
884
|
+
leanSyncHandle = handle
|
|
885
|
+
if (typeof onHandle === 'function') { try { onHandle(handle) } catch (e) { /* best effort */ } }
|
|
683
886
|
// 超时必须有**主动**兜底:`graceMs` 只是宿主侧的宽限,契约 §7 明确要求"对超时调用
|
|
684
887
|
// handle.terminate()"。此前 v2 只依赖 graceMs,从不终止进程:一个卡住的 Lean 会继续占着
|
|
685
888
|
// 资源,而框架已经报了超时——它与自己的契约不一致。这里与 `handle.done` 竞速:计时器到点
|
|
@@ -703,6 +906,8 @@ export function apply(ctx) {
|
|
|
703
906
|
return { ok: false, code: 'LEAN_RUN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
|
|
704
907
|
}
|
|
705
908
|
if (timer !== null) clearTimeout(timer)
|
|
909
|
+
leanSyncHandle = null
|
|
910
|
+
if (typeof onHandle === 'function') { try { onHandle(null) } catch (e) { /* best effort */ } }
|
|
706
911
|
let out = '', err = ''
|
|
707
912
|
try { if (handle.collected && handle.collected.stdout) out = handle.collected.stdout.readFrom(0).text } catch (e) { /* best effort */ }
|
|
708
913
|
try { if (handle.collected && handle.collected.stderr) err = handle.collected.stderr.readFrom(0).text } catch (e) { /* best effort */ }
|
|
@@ -715,14 +920,15 @@ export function apply(ctx) {
|
|
|
715
920
|
// 输出截断到 ~4KB 再入库(避免把巨大的编译器输出写进状态)。
|
|
716
921
|
return {
|
|
717
922
|
ok: ok, exitCode: exitCode, signal: (outcome && outcome.signal) || null, ms: ms,
|
|
718
|
-
command: argv.join(' '), file: rel,
|
|
923
|
+
command: argv.join(' '), file: rel, searchPath: searchPath,
|
|
719
924
|
stdout: tailText(out, 4000), stderr: tailText(err, 4000),
|
|
720
925
|
timedOut: isTimeout,
|
|
721
926
|
code: ok ? undefined : (isTimeout ? 'LEAN_TIMEOUT' : 'LEAN_FAILED'),
|
|
722
927
|
}
|
|
723
928
|
}
|
|
724
929
|
// 把一次运行结果写进对象的形式化记录(不提升 status,状态迁移见契约 §4)。
|
|
725
|
-
|
|
930
|
+
// `asyncInfo`(可选):异步档把作业 id/state/attempts/落地时刻贴在记录上,供人对照(§1.4)。
|
|
931
|
+
async function formalSetRun(target, run, asyncInfo) {
|
|
726
932
|
const t = safeId(String(target || ''))
|
|
727
933
|
if (!t) return
|
|
728
934
|
const prev = formalOf(t)
|
|
@@ -730,6 +936,7 @@ export function apply(ctx) {
|
|
|
730
936
|
status: prev.status === 'passed' ? 'passed' : (prev.status === 'blocked' ? 'blocked' : 'attempted'),
|
|
731
937
|
file: run.file || prev.file || '',
|
|
732
938
|
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) },
|
|
939
|
+
async: asyncInfo || prev.async || null,
|
|
733
940
|
updatedAt: now(),
|
|
734
941
|
}))
|
|
735
942
|
}
|
|
@@ -898,7 +1105,7 @@ export function apply(ctx) {
|
|
|
898
1105
|
L.push(' 请复核这个判断是否成立;若你认为其实可以形式化,请指出来并动手做。')
|
|
899
1106
|
} else {
|
|
900
1107
|
L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
|
|
901
|
-
L.push(' · 工具:vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib
|
|
1108
|
+
L.push(' · 工具:vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库/jobs)· vibe_math_lean_read(取回归档原文)')
|
|
902
1109
|
L.push(' · 工作目录:Formal/(相对项目根);可复用定义放 ' + vroot + '/Formal/Lib/,已证引理放 '
|
|
903
1110
|
+ vroot + '/Formal/Proved/;写之前先 vibe_math_lean_lib 查重。')
|
|
904
1111
|
L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与命题原文逐条一致。'
|
|
@@ -911,19 +1118,42 @@ export function apply(ctx) {
|
|
|
911
1118
|
L.push(' · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision=\'blocked\' 时必须写明 note)。')
|
|
912
1119
|
}
|
|
913
1120
|
L.push(' · 归档可复用定义/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run);跑不通不要入库。')
|
|
1121
|
+
// —— §5 B(逐字):三条筛选判据 + 先查再写 + 异步"落地前不得当已通过" ——
|
|
1122
|
+
L.push(' · 三条筛选判据:① 有价值或可能复用;② 较为关键或必要;③ 你对该陈述有把握(置信度高)。')
|
|
1123
|
+
L.push(' · 写新定义/证明前**先 vibe_math_lean_lib 查已有库**(vibe_math_lean_read 可取回归档原文逐字复用),查不到再写;'
|
|
1124
|
+
+ '复用已归档内容用 `import Formal.Lib.<name>` / `import Formal.Proved.<name>`。')
|
|
1125
|
+
L.push(' · **没把握就记 blocked** 并写清难点,别用形式化掩盖不确定。')
|
|
1126
|
+
L.push(' · 该对象若已有后台编译在队列中(leanAsync 默认开启),**不得**在作业落地为通过之前声称已通过或转忠实性审查;'
|
|
1127
|
+
+ '等 vibe_math_lean_lib 的 jobs 显示 settled 再审。')
|
|
914
1128
|
L.push(' · 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或宿主不提供 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并在回执的 note 里写明'
|
|
915
1129
|
+ '"宿主无 Lean 工具链"——这算显式阻塞原因,定论门禁可以据此放行。')
|
|
916
1130
|
}
|
|
917
|
-
|
|
1131
|
+
// 后台作业的一次性公告(spec §2.4):注入到本轮提示里,取走即清空。
|
|
1132
|
+
// 数学计算可用性行(prompts.md §2):验证轮同样告知"先 probe 再 run / 回执即引用"。
|
|
1133
|
+
return L.join('\n') + leanNoticeSection() + mathWorkLine()
|
|
918
1134
|
}
|
|
919
1135
|
function formalWorkLine() {
|
|
920
|
-
|
|
921
|
-
// `
|
|
922
|
-
//
|
|
923
|
-
|
|
1136
|
+
// 修订 §1 + v5 统一口径:**日常主动性(leanInitiative)与验证时的要求强度(formalVerify)是两根
|
|
1137
|
+
// 独立的轴**。所以这里不能用 `!formalOn()` 一票否决——那会让 `leanInitiative:'eager'` 在
|
|
1138
|
+
// `formalVerify:'off'` 下静默失效("定论不额外要求,但日常鼓励形式化"组合必须真的成立)。
|
|
1139
|
+
// `formalVerify:'off'` 只关掉**验证阶段**的 Lean 文本:那里仍是 `formalPromptBlock` 的
|
|
1140
|
+
// `!formalOn() ⇒ ''`。`leanInitiative:'off'` 则连日线也不注入(真无操作)。
|
|
1141
|
+
const initiative = LEAN_INITIATIVE_MODES.indexOf(params.leanInitiative) !== -1 ? params.leanInitiative : 'normal'
|
|
1142
|
+
if (initiative === 'off' || !(formalOn() || initiative === 'eager')) return ''
|
|
1143
|
+
return '【顺手形式化(' + (formalOn() ? (formalMode() === 'require' ? '强制' : '鼓励') : '仅主动性') + '·主动性 ' + initiative + ')】把你工作中常用或可能复用的对象、假设、'
|
|
924
1144
|
+ '新定义用 Lean 形式化定义并归档到全局可复用库(vibe_math_lean_archive kind=\'def\'),已成立的引理归到 '
|
|
925
1145
|
+ vibeRoot().replace(/\\/g, '/') + '/Formal/Proved/(kind=\'lemma\');写之前先 vibe_math_lean_lib 查重,避免重复定义。'
|
|
926
1146
|
+ '归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。'
|
|
1147
|
+
+ (initiative === 'eager'
|
|
1148
|
+
? '\n · **主动档(leanInitiative=eager)**:日常就主动把有价值的小引理/命题/定义形式化——每轮工作结束时审视一次"这轮有什么值得进库",值得就顺手归档。'
|
|
1149
|
+
: '\n · 主动性 normal:顺手把明显有价值且可能复用的东西形式化;不必刻意扩大范围。')
|
|
1150
|
+
// —— §5 A(逐字):三条筛选判据 + 先查再写 + 「没把握就记 blocked」 + 异步"落地前不得当已通过" ——
|
|
1151
|
+
+ '\n · 三条筛选判据:① 有价值或可能复用;② 较为关键或必要;③ 你对该陈述有把握(置信度高)。'
|
|
1152
|
+
+ '\n · **先 vibe_math_lean_lib 查再决定是否重写**:vibe_math_lean_lib 列出现成定义/引理,vibe_math_lean_read 可取回归档原文逐字复用;'
|
|
1153
|
+
+ '复用已归档内容用 `import Formal.Lib.<name>` / `import Formal.Proved.<name>`(模块根 = ' + vibeRoot().replace(/\\/g, '/') + ',框架已把它加进编译搜索路径)。'
|
|
1154
|
+
+ '\n · **没把握就记 blocked**(vibe_math_lean_archive kind=\'blocked\' note=…,或回执 formal 的 blocked):把难点写清楚,别用形式化掩盖不确定。'
|
|
1155
|
+
+ '\n · 编译默认走后台队列(leanAsync=true):入队后你可以继续工作;用 vibe_math_lean_job(可 waitMs 等结果)或下一轮提示里的'
|
|
1156
|
+
+ '【形式化结果】行看结果。**在作业落地为通过之前,不得把该对象当成已通过。**'
|
|
927
1157
|
}
|
|
928
1158
|
/**
|
|
929
1159
|
* 回执契约里的 `formal` 字段(契约 §6.3)。**必须真的出现在回执契约里**:契约写了字段而框架
|
|
@@ -1095,6 +1325,298 @@ export function apply(ctx) {
|
|
|
1095
1325
|
return await absorbFormalReply(f, memberId)
|
|
1096
1326
|
}
|
|
1097
1327
|
|
|
1328
|
+
// ================= Lean 增量 + 异步:作业队列(spec §2) =================
|
|
1329
|
+
// 并发上限 = `leanJobsMaxParallel`(默认 1 = 串行)。队列是**内存**态,作业记录镜像到
|
|
1330
|
+
// <项目>/Formal/Jobs/<jobId>.json —— 崩溃恢复、lean_lib.jobs 与 lean_job 都读它。
|
|
1331
|
+
const LEAN_QUEUE_CONCURRENCY_MAX = 8
|
|
1332
|
+
const leanQueue = [] // FIFO:待跑作业
|
|
1333
|
+
const leanJobs = new Map() // jobId -> job(内存视图,含 state)
|
|
1334
|
+
const leanRunningHandles = new Map() // jobId -> 子进程 handle(dispose 时 terminate)
|
|
1335
|
+
let leanSyncHandle = null // 同步档在跑的 handle(同样能被 terminate)
|
|
1336
|
+
const leanPendingNotices = [] // 一次性公告行(注入下一次 round prompt,spec §2.4)
|
|
1337
|
+
/** 当前会话的构建计划(修订 §2/§4):冻结 engine/args/搜索路径,作业与指纹都用它。 */
|
|
1338
|
+
function leanBuildPlan() {
|
|
1339
|
+
const userArgs = (Array.isArray(params.leanArgs) ? params.leanArgs : []).map(String)
|
|
1340
|
+
const plan = leanSearchPathPlan(userArgs, params.leanSearchPaths, vibeRoot())
|
|
1341
|
+
return { engine: String(params.leanCommand || 'lean'), args: userArgs, searchPaths: plan.paths, inject: plan.inject, explicit: plan.explicit }
|
|
1342
|
+
}
|
|
1343
|
+
function leanPlanContext(plan) { return leanBuildContext(plan.engine, plan.args, plan.searchPaths) }
|
|
1344
|
+
/** 归档作业当前应绑定的构建计划(def/lemma 与 proof 都是本项目计划)。 */
|
|
1345
|
+
function leanContextNow() { return leanPlanContext(leanBuildPlan()) }
|
|
1346
|
+
function leanJobsMaxParallel() { return Math.max(1, Math.min(LEAN_QUEUE_CONCURRENCY_MAX, Number(params.leanJobsMaxParallel) || 1)) }
|
|
1347
|
+
function leanActiveCount() { let n = 0; for (const j of leanJobs.values()) if (j.state === 'running') n++ ; return n }
|
|
1348
|
+
function leanSleep(ms) { return new Promise(function (r) { setTimeout(r, Math.max(0, Number(ms) || 0)) }) }
|
|
1349
|
+
function leanJobAbs(job) {
|
|
1350
|
+
const rel = String((job && job.rel) || '')
|
|
1351
|
+
if (!rel) return null
|
|
1352
|
+
return job.scope === 'root' ? normalizeAbsPath(vibeAbs(rel)) : normalizeAbsPath(frameworkRoot() + '/' + rel)
|
|
1353
|
+
}
|
|
1354
|
+
/** 作业的回执路径与归档路径(lean_job 返回它们,修订 §3)。 */
|
|
1355
|
+
function leanJobPaths(job) {
|
|
1356
|
+
const rec = { receipt: 'Formal/Jobs/' + job.jobId + '.json' }
|
|
1357
|
+
if (job.kind === 'archive' && job.archive && job.archive.kind === 'proof' && job.target) {
|
|
1358
|
+
rec.work = job.rel
|
|
1359
|
+
rec.archive = job.state === 'settled' ? ('Verified/Lean/' + job.target + '.lean') : ''
|
|
1360
|
+
} else if (job.kind === 'archive') rec.archive = job.rel
|
|
1361
|
+
else rec.file = job.rel
|
|
1362
|
+
return rec
|
|
1363
|
+
}
|
|
1364
|
+
function leanJobRecord(job) {
|
|
1365
|
+
return {
|
|
1366
|
+
jobId: job.jobId, kind: job.kind, state: job.state, attempts: Number(job.attempts || 1),
|
|
1367
|
+
target: job.target || '', name: job.name || '', rel: job.rel || '', scope: job.scope || 'project',
|
|
1368
|
+
sha256: job.sha || '', buildSha256: job.buildSha || '', buildContext: job.buildCtx || '',
|
|
1369
|
+
engine: (job.plan && job.plan.engine) || '', argv: (job.plan && job.plan.args) || [], searchPaths: (job.plan && job.plan.searchPaths) || [],
|
|
1370
|
+
archiveKind: (job.archive && job.archive.kind) || '',
|
|
1371
|
+
enqueuedAt: job.enqueuedAt || 0, startedAt: job.startedAt || 0, settledAt: job.settledAt || 0,
|
|
1372
|
+
exitCode: job.exitCode === undefined ? null : job.exitCode,
|
|
1373
|
+
timedOut: !!job.timedOut, interrupted: !!job.interrupted, note: job.note || '',
|
|
1374
|
+
project: currentProject,
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
async function writeLeanJobFile(job) { await writeJson('Formal/Jobs/' + job.jobId + '.json', leanJobRecord(job)) }
|
|
1378
|
+
function hydrateLeanJob(rec) {
|
|
1379
|
+
return {
|
|
1380
|
+
jobId: String(rec.jobId), kind: rec.kind || 'run', target: rec.target || '', name: rec.name || '',
|
|
1381
|
+
rel: rec.rel || '', scope: rec.scope || 'project', sha: rec.sha256 || '',
|
|
1382
|
+
buildSha: rec.buildSha256 || '', buildCtx: rec.buildContext || '',
|
|
1383
|
+
plan: rec.engine !== undefined ? { engine: rec.engine || '', args: rec.argv || [], searchPaths: rec.searchPaths || [] } : null,
|
|
1384
|
+
archive: rec.archiveKind ? { kind: rec.archiveKind, target: rec.target || '', name: rec.name || '' } : null,
|
|
1385
|
+
state: rec.state || 'queued', attempts: Number(rec.attempts || 1), enqueuedAt: Number(rec.enqueuedAt || 0),
|
|
1386
|
+
startedAt: Number(rec.startedAt || 0), settledAt: Number(rec.settledAt || 0),
|
|
1387
|
+
exitCode: rec.exitCode === undefined ? null : rec.exitCode, timedOut: !!rec.timedOut,
|
|
1388
|
+
interrupted: !!rec.interrupted, run: rec.run || null, note: rec.note || '',
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
/** 入队:同 jobId 已在队列/在跑 ⇒ 复用(幂等);返回作业对象。 */
|
|
1392
|
+
async function enqueueLeanJob(spec) {
|
|
1393
|
+
const existing = leanJobs.get(spec.jobId)
|
|
1394
|
+
if (existing && (existing.state === 'queued' || existing.state === 'running')) return existing
|
|
1395
|
+
const job = Object.assign({}, spec, {
|
|
1396
|
+
state: 'queued', attempts: Number(spec.attempts || ((existing && existing.attempts) || 0) + 1),
|
|
1397
|
+
enqueuedAt: spec.enqueuedAt || now(), startedAt: 0, settledAt: 0, exitCode: null,
|
|
1398
|
+
timedOut: false, interrupted: false, run: null, note: spec.note || '',
|
|
1399
|
+
})
|
|
1400
|
+
leanJobs.set(job.jobId, job)
|
|
1401
|
+
leanQueue.push(job)
|
|
1402
|
+
await writeLeanJobFile(job)
|
|
1403
|
+
logActivity('formal', 'Lean 作业入队 ' + job.jobId + '(' + job.kind + (job.target ? '|对象 ' + job.target : (job.name ? '|' + job.name : '')) + ',state=queued)')
|
|
1404
|
+
return job
|
|
1405
|
+
}
|
|
1406
|
+
function pushLeanNotice(line) { if (leanPendingNotices.length >= 20) leanPendingNotices.shift(); leanPendingNotices.push(String(line)) }
|
|
1407
|
+
/** 一次性公告(spec §2.4):取走即清空,保证同一条结果只注入一次。 */
|
|
1408
|
+
function leanNoticeSection() {
|
|
1409
|
+
if (leanPendingNotices.length === 0) return ''
|
|
1410
|
+
const lines = leanPendingNotices.splice(0, leanPendingNotices.length)
|
|
1411
|
+
return '\n' + lines.join('\n') + '\n'
|
|
1412
|
+
}
|
|
1413
|
+
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) } }
|
|
1414
|
+
/**
|
|
1415
|
+
* 心跳里推进队列:**不 await 编译本身**(调用方不 await 本函数)。
|
|
1416
|
+
* 并发上限 = `leanJobsMaxParallel`(默认 1,串行);串行时本函数等待该作业跑完再返回。
|
|
1417
|
+
*/
|
|
1418
|
+
async function runLeanQueue() {
|
|
1419
|
+
const max = leanJobsMaxParallel()
|
|
1420
|
+
let started = 0
|
|
1421
|
+
while (leanQueue.length > 0 && leanActiveCount() + started < max) {
|
|
1422
|
+
const job = leanQueue.shift()
|
|
1423
|
+
if (!job || job.state !== 'queued') continue
|
|
1424
|
+
started++
|
|
1425
|
+
if (max <= 1) { await runLeanJob(job); return }
|
|
1426
|
+
runLeanJob(job).catch(function (e) { console.error('vibe-math-v2: lean job failed: ' + String((e && e.message) || e)) })
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async function runLeanJob(job) {
|
|
1430
|
+
if (job.state !== 'queued') return
|
|
1431
|
+
job.state = 'running'; job.startedAt = now(); job.interrupted = false
|
|
1432
|
+
await writeLeanJobFile(job)
|
|
1433
|
+
let run
|
|
1434
|
+
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) }) }
|
|
1435
|
+
catch (e) { run = { ok: false, exitCode: null, code: 'LEAN_RUN_FAILED', message: String((e && e.message) || e), ms: 0 } }
|
|
1436
|
+
try { await settleLeanJob(job, run) } catch (e) { console.error('vibe-math-v2: lean job settle failed: ' + String((e && e.stack) || e)) }
|
|
1437
|
+
}
|
|
1438
|
+
/** 结果公告行(spec §2.4 的四种结局)。 */
|
|
1439
|
+
function leanSettleNotice(job, settledOk, capMs) {
|
|
1440
|
+
const ms = (job.run && job.run.ms) || 0
|
|
1441
|
+
if (job.interrupted) return '【形式化结果】' + job.jobId + ':中断(会话卸载或崩溃,已标记 attempted,可重跑)'
|
|
1442
|
+
if (job.timedOut) return '【形式化结果】' + job.jobId + ':超时(leanTimeoutMs=' + capMs + ' 已 terminate;对象留在 attempted)'
|
|
1443
|
+
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') : '') + ')'
|
|
1444
|
+
if (job.hashMismatch) return '【形式化结果】' + job.jobId + ':失败(编译期间文件内容已变,本次结果已丢弃;请重跑)'
|
|
1445
|
+
if (job.contextMismatch) return '【形式化结果】' + job.jobId + ':失败(编译期间构建上下文已变:引擎/参数/搜索路径,本次结果已丢弃;请在新上下文下重跑)'
|
|
1446
|
+
return '【形式化结果】' + job.jobId + ':失败(exit ' + String(job.exitCode) + ',见 stderr 尾部)'
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* 落地一个作业(**唯一**的 passed 入口,spec §2.1):只有 exit 0 **且** 内容哈希仍匹配才算 ok;
|
|
1450
|
+
* 其余一律 attempted;中断/超时都不是 passed;内容变了只标记不贴结果。
|
|
1451
|
+
*/
|
|
1452
|
+
async function settleLeanJob(job, run) {
|
|
1453
|
+
const capMs = Math.max(1000, Number(job.timeoutMs) || Number(params.leanTimeoutMs) || 120000)
|
|
1454
|
+
let currentSha = null
|
|
1455
|
+
try { const t = await readTextAbs(leanJobAbs(job)); if (t !== undefined) currentSha = leanContentSha(t) } catch (e) { /* 读不到 ⇒ 视为不匹配 */ }
|
|
1456
|
+
const hashOk = currentSha !== null && currentSha === job.sha
|
|
1457
|
+
// 修订 §4:结果只对**产生它的那个构建上下文**有效。参数在编译途中被改(引擎/参数/搜索路径)
|
|
1458
|
+
// ⇒ 这次 ok 不能贴到"现在的配置"上,宁可判为未通过并明确公告。
|
|
1459
|
+
const ctxNow = leanContextNow()
|
|
1460
|
+
const ctxOk = !job.buildCtx || job.buildCtx === ctxNow
|
|
1461
|
+
job.hashMismatch = !hashOk
|
|
1462
|
+
job.contextMismatch = !ctxOk
|
|
1463
|
+
job.exitCode = run.exitCode === undefined ? null : run.exitCode
|
|
1464
|
+
job.timedOut = !!run.timedOut
|
|
1465
|
+
job.settledAt = now()
|
|
1466
|
+
const settledOk = !job.interrupted && !job.timedOut && run.exitCode === 0 && hashOk && ctxOk
|
|
1467
|
+
job.state = job.interrupted ? 'interrupted' : (job.timedOut ? 'timeout' : (settledOk ? 'settled' : 'failed'))
|
|
1468
|
+
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 || '' }
|
|
1469
|
+
if (job.hashMismatch && !job.interrupted && !job.timedOut) job.note = 'file changed while compiling — result discarded (hash mismatch)'
|
|
1470
|
+
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'
|
|
1471
|
+
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 || '' }
|
|
1472
|
+
if (job.kind === 'archive') await settleLeanArchiveJob(job, settledOk, asyncInfo)
|
|
1473
|
+
else if (job.target) await formalSetRun(job.target, Object.assign({}, run, { ok: settledOk, file: job.rel }), asyncInfo)
|
|
1474
|
+
if (!job.interrupted) await writeFormalIndex()
|
|
1475
|
+
await writeLeanJobFile(job)
|
|
1476
|
+
pushLeanNotice(leanSettleNotice(job, settledOk, capMs))
|
|
1477
|
+
logActivity('formal', 'Lean 作业 ' + job.jobId + ' ' + job.state + '(' + job.kind + ')' + (job.target ? '|对象 ' + job.target : ''))
|
|
1478
|
+
reportDirty = true
|
|
1479
|
+
}
|
|
1480
|
+
/** 归档类作业的落地:**只有 ok 才落库/置 passed**(def/lemma 的文件在入队前已写好)。 */
|
|
1481
|
+
async function settleLeanArchiveJob(job, ok, asyncInfo) {
|
|
1482
|
+
const a = job.archive || {}
|
|
1483
|
+
if (a.kind !== 'proof') {
|
|
1484
|
+
await rebuildLeanLibIndexes()
|
|
1485
|
+
logActivity('formal', '归档作业 ' + job.jobId + '(' + a.kind + ' ' + job.rel + ')' + (ok ? '运行通过' : '运行未通过(见 stderr 尾部;该文件不应被当作可复用定义)'))
|
|
1486
|
+
return
|
|
1487
|
+
}
|
|
1488
|
+
const target = job.target
|
|
1489
|
+
const prev = formalOf(target)
|
|
1490
|
+
const passed = !!ok
|
|
1491
|
+
const stalePrev = passed ? '' : String(prev.proof || ('Verified/Lean/' + target + '.lean'))
|
|
1492
|
+
const rec = Object.assign({}, prev, {
|
|
1493
|
+
status: passed ? 'passed' : 'attempted',
|
|
1494
|
+
file: job.rel,
|
|
1495
|
+
proof: passed ? 'Verified/Lean/' + target + '.lean' : '',
|
|
1496
|
+
decision: 'used',
|
|
1497
|
+
note: String(prev.note || ''),
|
|
1498
|
+
run: job.run,
|
|
1499
|
+
async: asyncInfo,
|
|
1500
|
+
updatedAt: now(),
|
|
1501
|
+
})
|
|
1502
|
+
let withdrawn = null
|
|
1503
|
+
if (passed) {
|
|
1504
|
+
const body = await readTextAbs(leanJobAbs(job))
|
|
1505
|
+
if (body !== undefined) await writeText('Verified/Lean/' + target + '.lean', body)
|
|
1506
|
+
} else if (stalePrev) { try { withdrawn = await withdrawArchivedProof(stalePrev) } catch (e) { withdrawn = { rel: stalePrev, outcome: 'failed' } } }
|
|
1507
|
+
await putFormal(target, rec)
|
|
1508
|
+
await syncVerificationTarget(target, rec.status, rec)
|
|
1509
|
+
await rebuildLeanLibIndexes()
|
|
1510
|
+
logActivity('formal', '归档作业 ' + job.jobId + ' 为 ' + target + ' ' + (passed ? ('通过,已归档 ' + rec.proof + ',验证转为忠实性审查')
|
|
1511
|
+
: ('未通过:' + tailText(job.run && job.run.stderrTail, 160) + ';已撤回上一份已通过状态与归档证明' + (withdrawn && withdrawn.outcome !== 'failed' ? '(' + withdrawn.outcome + ')' : '(⚠ 撤回失败,请不要把 ' + stalePrev + ' 当作该对象的证明)'))))
|
|
1512
|
+
}
|
|
1513
|
+
/** 去重(spec §4.3):同内容且已成功过 ⇒ 跳过写盘与重编译。 */
|
|
1514
|
+
async function leanDedupeLookup(jobId, abs, sha, proofTarget) {
|
|
1515
|
+
if (proofTarget) {
|
|
1516
|
+
const rec = formalOf(proofTarget)
|
|
1517
|
+
if (rec && rec.status === 'passed') {
|
|
1518
|
+
const cur = await readTextAbs(vibeAbs('Verified/Lean/' + proofTarget + '.lean'))
|
|
1519
|
+
if (cur !== undefined && leanContentSha(cur) === sha) return { deduped: true, file: 'Verified/Lean/' + proofTarget + '.lean' }
|
|
1520
|
+
}
|
|
1521
|
+
return null
|
|
1522
|
+
}
|
|
1523
|
+
const prev = await readJson('Formal/Jobs/' + jobId + '.json')
|
|
1524
|
+
if (leanJobSettledOk(prev)) {
|
|
1525
|
+
const cur = await readTextAbs(abs)
|
|
1526
|
+
if (cur !== undefined && leanContentSha(cur) === sha) return { deduped: true, file: String(prev.rel || '') }
|
|
1527
|
+
}
|
|
1528
|
+
return null
|
|
1529
|
+
}
|
|
1530
|
+
/** 后台作业的公共返回形状(§1.2)。 */
|
|
1531
|
+
function leanAsyncReturn(job, extra) {
|
|
1532
|
+
return Object.assign({
|
|
1533
|
+
ok: true, async: { jobId: job.jobId, state: job.state }, jobId: job.jobId,
|
|
1534
|
+
message: '已入队后台编译(并发上限 1);你可以继续工作。结果会写入 Formal/ 与索引,并在下一轮提示里公告;也可用 vibe_math_lean_lib 的 jobs 字段随时查看。**在该作业落地为通过之前,不得把相关对象当成已通过。**',
|
|
1535
|
+
}, extra || {})
|
|
1536
|
+
}
|
|
1537
|
+
/** 崩溃恢复(spec §2.6):queued 重入队;running 哈希匹配 ⇒ 标记 interrupted + 重入队(attempts+1),不匹配 ⇒ 只标记;恢复**绝不**置 passed。 */
|
|
1538
|
+
async function recoverLeanJobs() {
|
|
1539
|
+
let files = []
|
|
1540
|
+
try { files = (await listFiles('Formal/Jobs')) || [] } catch (e) { return { recovered: 0 } }
|
|
1541
|
+
let recovered = 0
|
|
1542
|
+
for (let i = 0; i < files.length; i++) {
|
|
1543
|
+
const f = String(files[i])
|
|
1544
|
+
if (!/\.json$/.test(f)) continue
|
|
1545
|
+
const rec = await readJson('Formal/Jobs/' + f)
|
|
1546
|
+
if (!rec || !rec.jobId) continue
|
|
1547
|
+
// 本进程内存里已知的作业(queued/running 或已落地)不再恢复:同一进程内切换项目时,
|
|
1548
|
+
// 内存队列还在正常跑,按文件状态"恢复"会把一个在跑的作业误判成崩溃(重复入队/误置 interrupted)。
|
|
1549
|
+
if (leanJobs.has(String(rec.jobId))) continue
|
|
1550
|
+
const job = hydrateLeanJob(rec)
|
|
1551
|
+
leanJobs.set(job.jobId, job)
|
|
1552
|
+
const abs = leanJobAbs(job)
|
|
1553
|
+
const txt = abs ? await readTextAbs(abs) : undefined
|
|
1554
|
+
const curSha = txt === undefined ? null : leanContentSha(txt)
|
|
1555
|
+
const hashMatch = curSha !== null && curSha === job.sha
|
|
1556
|
+
if (job.state === 'queued') {
|
|
1557
|
+
await enqueueLeanJob(job)
|
|
1558
|
+
pushLeanNotice('【形式化结果】' + job.jobId + ':上次会话未执行的排队作业已重新入队')
|
|
1559
|
+
recovered++
|
|
1560
|
+
} else if (job.state === 'running') {
|
|
1561
|
+
if (hashMatch) {
|
|
1562
|
+
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() }))
|
|
1563
|
+
job.interrupted = true
|
|
1564
|
+
// 先落到 interrupted,再重入队:否则 enqueueLeanJob 的幂等守卫会把这条 "running" 记录
|
|
1565
|
+
// 当成"仍在跑"而早退,attempts 也不会 +1(恢复就静默失效了)。
|
|
1566
|
+
job.state = 'interrupted'
|
|
1567
|
+
await enqueueLeanJob(Object.assign({}, job, { attempts: job.attempts + 1 }))
|
|
1568
|
+
pushLeanNotice('【形式化结果】' + job.jobId + ':中断(会话崩溃,已标记 attempted,已用新 attempts 重新入队)')
|
|
1569
|
+
} else {
|
|
1570
|
+
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() }))
|
|
1571
|
+
job.state = 'interrupted'; job.interrupted = true
|
|
1572
|
+
job.note = 'file changed while the previous session was compiling — not auto-re-driven'
|
|
1573
|
+
await writeLeanJobFile(job)
|
|
1574
|
+
pushLeanNotice('【形式化结果】' + job.jobId + ':中断(文件已变,未自动重驱;请手动重跑)')
|
|
1575
|
+
}
|
|
1576
|
+
recovered++
|
|
1577
|
+
} else if (job.state === 'settled') {
|
|
1578
|
+
// 修订 §4:只有**同一构建上下文**下的 settled(ok) 才允许恢复成 passed。
|
|
1579
|
+
const ctxNow = leanContextNow()
|
|
1580
|
+
const ctxOk = !job.buildCtx || job.buildCtx === ctxNow
|
|
1581
|
+
if (leanJobSettledOk(rec) && hashMatch && ctxOk) {
|
|
1582
|
+
job.run = { at: now(), ok: true, exitCode: 0, ms: 0, timedOut: false, interrupted: false, stdoutTail: '', stderrTail: '', searchPath: vibeRoot(), buildContext: job.buildCtx || '' }
|
|
1583
|
+
if (job.kind === 'archive') await settleLeanArchiveJob(job, true, { jobId: job.jobId, state: 'settled', attempts: job.attempts, settledAt: job.settledAt, exitCode: 0, buildSha256: job.buildSha || '' })
|
|
1584
|
+
else if (job.target) await formalSetRun(job.target, job.run, { jobId: job.jobId, state: 'settled', buildSha256: job.buildSha || '' })
|
|
1585
|
+
pushLeanNotice('【形式化结果】' + job.jobId + ':已按作业记录补写归档与 passed(恢复时校验内容哈希与构建上下文一致)')
|
|
1586
|
+
} else if (job.target) {
|
|
1587
|
+
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() }))
|
|
1588
|
+
pushLeanNotice('【形式化结果】' + job.jobId + ':按作业记录标记为未通过/已变(恢复绝不置 passed)')
|
|
1589
|
+
}
|
|
1590
|
+
recovered++
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (recovered > 0) logActivity('formal', 'Lean 作业恢复:处理了 ' + recovered + ' 条遗留作业记录(queued 重入队 / running 标记中断 / settled 补写;绝不置 passed)')
|
|
1594
|
+
return { recovered: recovered }
|
|
1595
|
+
}
|
|
1596
|
+
/** 会话卸载/销毁:终止在跑的编译并标记 interrupted(不留孤儿进程,spec §2.5)。 */
|
|
1597
|
+
function disposeLeanJobs() {
|
|
1598
|
+
let n = 0
|
|
1599
|
+
for (const job of leanJobs.values()) {
|
|
1600
|
+
if (job.state === 'running') { job.interrupted = true; n++ }
|
|
1601
|
+
}
|
|
1602
|
+
for (const job of leanQueue) { job.state = 'interrupted'; job.interrupted = true; n++ }
|
|
1603
|
+
for (const h of leanRunningHandles.values()) {
|
|
1604
|
+
try { if (h && typeof h.terminate === 'function') h.terminate() } catch (e) { /* best effort */ }
|
|
1605
|
+
}
|
|
1606
|
+
leanRunningHandles.clear()
|
|
1607
|
+
if (leanSyncHandle && typeof leanSyncHandle.terminate === 'function') { try { leanSyncHandle.terminate() } catch (e) { /* best effort */ } }
|
|
1608
|
+
leanSyncHandle = null
|
|
1609
|
+
leanQueue.length = 0
|
|
1610
|
+
if (n > 0) {
|
|
1611
|
+
pushLeanNotice('【形式化结果】干预:会话卸载,' + n + ' 个 Lean 作业已标记 interrupted(未落地为 passed)')
|
|
1612
|
+
// 落盘尽力而为(disposer 不能 await):作业记录留着,供下次 init 恢复时标记。
|
|
1613
|
+
try {
|
|
1614
|
+
for (const job of leanJobs.values()) { if (job.state === 'running' || job.interrupted) writeLeanJobFile(job).catch(function () { }) }
|
|
1615
|
+
} catch (e) { /* best effort */ }
|
|
1616
|
+
}
|
|
1617
|
+
return n
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1098
1620
|
// ---- 三份索引(框架维护;契约 §9)----
|
|
1099
1621
|
async function writeFormalIndex() {
|
|
1100
1622
|
const recs = formalRecords()
|
|
@@ -1129,6 +1651,7 @@ export function apply(ctx) {
|
|
|
1129
1651
|
async function rebuildLeanLibIndexes() {
|
|
1130
1652
|
// 扫描**不执行**工具链:每次问"有什么可复用"就跑一遍 lean 既慢又出人意料。
|
|
1131
1653
|
// 每个对象的运行结果存在对象记录里,显示在 Formal/Index.md。
|
|
1654
|
+
// 依赖列(spec §4.4):把 `import` 行扫出来写进索引,"有什么 / 叫什么 / 怎么导入"三问一次答完。
|
|
1132
1655
|
const scan = async (dirAbs, dirRel, kindLabel) => {
|
|
1133
1656
|
const rows = []
|
|
1134
1657
|
try {
|
|
@@ -1141,21 +1664,23 @@ export function apply(ctx) {
|
|
|
1141
1664
|
const txt = (await readTextAbs(dirAbs + '/' + e.name)) || ''
|
|
1142
1665
|
const name = String(e.name).replace(/\.lean$/, '')
|
|
1143
1666
|
const first = (txt.split('\n').filter(function (l) { return l.trim() && !/^\s*(\/\/|--|import)/.test(l) })[0] || '').trim().slice(0, 110)
|
|
1144
|
-
|
|
1667
|
+
const deps = txt.split('\n').filter(function (l) { return /^\s*import\s+/.test(l) })
|
|
1668
|
+
.map(function (l) { return l.trim().replace(/^import\s+/, '').trim() }).filter(Boolean).slice(0, 4)
|
|
1669
|
+
rows.push('| ' + name + ' | ' + rel + ' | ' + kindLabel + ' | ' + (deps.length ? deps.join(', ') : '—').replace(/\|/g, '/') + ' | ' + first.replace(/\|/g, '/') + ' |')
|
|
1145
1670
|
}
|
|
1146
1671
|
} catch (e) { /* 列表尽力而为 */ }
|
|
1147
1672
|
return rows
|
|
1148
1673
|
}
|
|
1149
1674
|
const libRows = await scan(vibeRoot() + '/Formal/Lib', 'Formal/Lib', 'def')
|
|
1150
1675
|
await writeTextAbs(vibeRoot() + '/Formal/Lib/Index.md', ['# 可复用 Lean 定义库(跨项目)|' + currentProject, '',
|
|
1151
|
-
'>
|
|
1152
|
-
'| 名称 | 文件 | 类别 | 摘要 |', '
|
|
1153
|
-
.concat(libRows.length ? libRows : ['| (暂无) | | | |']).join('\n') + '\n')
|
|
1676
|
+
'> 写新定义之前先查这里:能复用就不要重新定义。复用方式:`import Formal.Lib.<名称>`(模块根 = VibeMath 根)。', '',
|
|
1677
|
+
'| 名称 | 文件 | 类别 | 依赖(import) | 摘要 |', '|---|---|---|---|---|']
|
|
1678
|
+
.concat(libRows.length ? libRows : ['| (暂无) | | | | |']).join('\n') + '\n')
|
|
1154
1679
|
const provedRows = await scan(vibeRoot() + '/Formal/Proved', 'Formal/Proved', 'lemma')
|
|
1155
1680
|
await writeTextAbs(vibeRoot() + '/Formal/Proved/Index.md', ['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|' + currentProject, '',
|
|
1156
|
-
'> 这些文件是通过内核检查的引理,可直接 import
|
|
1157
|
-
'| 名称 | 文件 | 类别 | 陈述 |', '
|
|
1158
|
-
.concat(provedRows.length ? provedRows : ['| (暂无) | | | |']).join('\n') + '\n')
|
|
1681
|
+
'> 这些文件是通过内核检查的引理,可直接 import 复用:`import Formal.Proved.<名称>`。', '',
|
|
1682
|
+
'| 名称 | 文件 | 类别 | 依赖(import) | 陈述 |', '|---|---|---|---|---|']
|
|
1683
|
+
.concat(provedRows.length ? provedRows : ['| (暂无) | | | | |']).join('\n') + '\n')
|
|
1159
1684
|
await writeFormalIndex()
|
|
1160
1685
|
await writeFormalTodo()
|
|
1161
1686
|
return { lib: libRows.length, proved: provedRows.length, objects: Object.keys(formalRecords()).length }
|
|
@@ -1163,20 +1688,110 @@ export function apply(ctx) {
|
|
|
1163
1688
|
|
|
1164
1689
|
// 执行一个 Lean 文件,记录运行结果(可按 target 归属到对象),刷新索引,并**原样**回报结果。
|
|
1165
1690
|
// 刻意在 `off` 档也照常工作:人要调试自己的工具链时仍然可用。
|
|
1691
|
+
// leanAsync=true(默认):入队后立即返回(成员不阻塞);false:逐字保留今天的同步路径。
|
|
1166
1692
|
async function leanRunTool(memberId, o) {
|
|
1167
1693
|
const args = o || {}
|
|
1168
|
-
const
|
|
1694
|
+
const rel = String(args.file || '').trim()
|
|
1695
|
+
const target = String(args.target || '').trim()
|
|
1696
|
+
if (params.leanAsync !== false) {
|
|
1697
|
+
const guard = await leanResolveRunTarget(rel)
|
|
1698
|
+
if (!guard.ok) return guard
|
|
1699
|
+
const plan = leanBuildPlan()
|
|
1700
|
+
const ctx = leanPlanContext(plan)
|
|
1701
|
+
const fp = leanJobFingerprint(guard.text, ctx)
|
|
1702
|
+
const jobId = leanJobId(target || guard.rel, fp)
|
|
1703
|
+
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 })
|
|
1704
|
+
return leanAsyncReturn(job, { file: guard.rel, state: job.state, hint: '已入队。**在它落地为通过之前,不得把该对象当成已通过**;结果会在下一轮提示的【形式化结果】行里公告,或用 vibe_math_lean_job / vibe_math_lean_lib 的 jobs 字段查看(lean_job 可 waitMs=… 等待)。' })
|
|
1705
|
+
}
|
|
1706
|
+
const run = await leanRunFile(rel, args.timeout_ms)
|
|
1169
1707
|
if (run.ok || run.file) {
|
|
1170
|
-
if (
|
|
1708
|
+
if (target) await formalSetRun(target, run)
|
|
1171
1709
|
await writeFormalIndex()
|
|
1172
1710
|
}
|
|
1173
1711
|
if (run.ok) logActivity('formal', (memberId || 'office') + ' 运行 Lean 通过:' + run.file + '(' + (run.ms / 1000).toFixed(1) + 's)' + (args.target ? '|对象 ' + args.target : ''))
|
|
1174
1712
|
return Object.assign({ ok: !!run.ok }, run, {
|
|
1713
|
+
async: null,
|
|
1175
1714
|
hint: run.ok
|
|
1176
1715
|
? '通过。若是某个对象的证明,请用 vibe_math_lean_archive kind=\'proof\' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind=\'def\'/\'lemma\' 归档到全局库(归档时会先跑一次,跑不通不要入库)。'
|
|
1177
1716
|
: '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 vibe_math_lean_archive kind=\'blocked\' 记录原因。',
|
|
1178
1717
|
})
|
|
1179
1718
|
}
|
|
1719
|
+
/** 异步档的路径/存在性校验(与 leanRunFile 同一套守卫与错误码)。 */
|
|
1720
|
+
async function leanResolveRunTarget(relPath) {
|
|
1721
|
+
const rel = String(relPath || '').trim()
|
|
1722
|
+
if (!rel) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'file is required' }
|
|
1723
|
+
const abs = leanAbsPath(rel)
|
|
1724
|
+
if (abs === null) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'Lean 文件必须位于 ' + vibeRoot() + '/ 之内(收到 ' + rel + ')' }
|
|
1725
|
+
if (!/\.lean$/.test(abs)) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: '只有 .lean 文件可以执行' }
|
|
1726
|
+
const txt = await readTextAbs(abs)
|
|
1727
|
+
if (txt === undefined) return { ok: false, code: 'V2_NOT_FOUND', message: 'no such file: ' + rel }
|
|
1728
|
+
return { ok: true, rel: rel, abs: abs, sha: leanContentSha(txt), text: txt }
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* `lean_job`(修订 §3,只读):不带 jobId ⇒ 本会话作业清单;带 ⇒ 该作业的 state/exitCode/
|
|
1732
|
+
* 回执路径/归档路径。`waitMs>0` 时最多等这么久(内部轮询,**不阻塞心跳**——心跳是独立 timer),
|
|
1733
|
+
* 超时就返回当前 state。
|
|
1734
|
+
*/
|
|
1735
|
+
async function leanJobTool(o) {
|
|
1736
|
+
const args = o || {}
|
|
1737
|
+
const wantId = String(args.jobId || '').trim()
|
|
1738
|
+
const waitMs = Math.max(0, Math.min(600000, Number(args.waitMs) || 0))
|
|
1739
|
+
if (!wantId) {
|
|
1740
|
+
const jobs = []
|
|
1741
|
+
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 || '' }))
|
|
1742
|
+
jobs.sort(function (a, b) { return (b.settledAt || 0) - (a.settledAt || 0) || String(a.jobId).localeCompare(String(b.jobId)) })
|
|
1743
|
+
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 一律还不算。' }
|
|
1744
|
+
}
|
|
1745
|
+
let job = leanJobs.get(wantId)
|
|
1746
|
+
if (!job) {
|
|
1747
|
+
const rec = await readJson('Formal/Jobs/' + wantId + '.json')
|
|
1748
|
+
if (!rec || !rec.jobId) return { ok: false, code: 'V2_NOT_FOUND', message: 'no such Lean job: ' + wantId + '(用不带 jobId 的 vibe_math_lean_job 列清单)' }
|
|
1749
|
+
job = hydrateLeanJob(rec)
|
|
1750
|
+
if (!leanJobs.has(job.jobId)) leanJobs.set(job.jobId, job)
|
|
1751
|
+
}
|
|
1752
|
+
const startedAt = now()
|
|
1753
|
+
while (waitMs > 0 && (job.state === 'queued' || job.state === 'running') && (now() - startedAt) < waitMs) {
|
|
1754
|
+
await leanSleep(50)
|
|
1755
|
+
const cur = leanJobs.get(wantId)
|
|
1756
|
+
if (cur) job = cur
|
|
1757
|
+
}
|
|
1758
|
+
return {
|
|
1759
|
+
ok: true, jobId: job.jobId, state: job.state, exitCode: job.exitCode === undefined ? null : job.exitCode,
|
|
1760
|
+
attempt: Number(job.attempts || 1), timedOut: !!job.timedOut, interrupted: !!job.interrupted, note: job.note || '',
|
|
1761
|
+
buildSha256: job.buildSha || '', buildContext: job.buildCtx || '',
|
|
1762
|
+
waitedMs: now() - startedAt, stillRunning: (job.state === 'queued' || job.state === 'running'),
|
|
1763
|
+
passed: leanJobSettledOk(leanJobRecord(job)),
|
|
1764
|
+
paths: leanJobPaths(job),
|
|
1765
|
+
run: job.run || null,
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
/** `lean_read`:只读地取回归档的 Lean 原文(verbatim 复用;§1.2)。 */
|
|
1769
|
+
async function leanReadTool(o) {
|
|
1770
|
+
const args = o || {}
|
|
1771
|
+
const rawName = String(args.name || '').trim()
|
|
1772
|
+
if (!rawName) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'name is required' }
|
|
1773
|
+
if (rawName.indexOf('..') !== -1 || /[\\/]/.test(rawName) || /^[a-z]:/i.test(rawName)) {
|
|
1774
|
+
return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'name must be a bare library name (no path separators, no "..", no absolute path)' }
|
|
1775
|
+
}
|
|
1776
|
+
const name = safeId(rawName)
|
|
1777
|
+
const kind = (args.kind === 'lib' || args.kind === 'proved') ? args.kind : 'auto'
|
|
1778
|
+
const order = kind === 'auto' ? ['lib', 'proved'] : [kind]
|
|
1779
|
+
for (let i = 0; i < order.length; i++) {
|
|
1780
|
+
const k = order[i]
|
|
1781
|
+
const rel = 'Formal/' + (k === 'lib' ? 'Lib' : 'Proved') + '/' + name + '.lean'
|
|
1782
|
+
const dir = normalizeAbsPath(vibeRoot() + '/Formal/' + (k === 'lib' ? 'Lib' : 'Proved'))
|
|
1783
|
+
const abs = normalizeAbsPath(vibeAbs(rel))
|
|
1784
|
+
// 双保险:规范化后必须真的落在那两个目录之内(name 已过 safeId,这里是路径守卫)
|
|
1785
|
+
if (abs.indexOf(dir + '/') !== 0) continue
|
|
1786
|
+
const txt = await readTextAbs(abs)
|
|
1787
|
+
if (txt === undefined) continue
|
|
1788
|
+
const LIMIT = 64 * 1024
|
|
1789
|
+
const bytes = Buffer.byteLength(txt, 'utf8')
|
|
1790
|
+
const truncated = bytes > LIMIT
|
|
1791
|
+
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 }
|
|
1792
|
+
}
|
|
1793
|
+
return { ok: false, code: 'V2_NOT_FOUND', message: 'no archived Lean file named ' + name + '(查过 Formal/Lib 与 Formal/Proved);可先用 vibe_math_lean_lib 看清单' }
|
|
1794
|
+
}
|
|
1180
1795
|
async function leanArchive(memberId, o) {
|
|
1181
1796
|
const args = o || {}
|
|
1182
1797
|
const kind = String(args.kind || '')
|
|
@@ -1197,6 +1812,22 @@ export function apply(ctx) {
|
|
|
1197
1812
|
if (body === undefined) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
|
|
1198
1813
|
const rel = 'Formal/' + (kind === 'def' ? 'Lib' : 'Proved') + '/' + name + '.lean'
|
|
1199
1814
|
const abs = vibeAbs(rel)
|
|
1815
|
+
const sha = leanContentSha(body)
|
|
1816
|
+
const plan = leanBuildPlan()
|
|
1817
|
+
const ctx = leanPlanContext(plan)
|
|
1818
|
+
const fp = leanJobFingerprint(body, ctx) // 修订 §4:指纹含内容 + 构建上下文
|
|
1819
|
+
const jobId = leanJobId(name, fp)
|
|
1820
|
+
// 去重(§4.3):同内容 + 同构建上下文且已成功归档过 ⇒ 跳过重写与重编译。
|
|
1821
|
+
const dedupe = await leanDedupeLookup(jobId, abs, sha, null)
|
|
1822
|
+
if (dedupe) {
|
|
1823
|
+
logActivity('formal', (memberId || 'office') + ' 归档 ' + kind + ' `' + name + '` 命中去重(同内容与构建上下文,跳过重写与重编译)')
|
|
1824
|
+
return { ok: true, kind: kind, name: name, file: rel, deduped: true, sha256: sha, buildSha256: fp, note: '内容与构建上下文都与已归档并编译通过的版本一致:跳过重写与重编译(去重)。' }
|
|
1825
|
+
}
|
|
1826
|
+
if (params.leanAsync !== false && args.run !== false) {
|
|
1827
|
+
if (!await writeTextAbs(abs, body)) return { ok: false, code: 'V2_WRITE_FAILED', message: 'could not write ' + rel }
|
|
1828
|
+
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 } })
|
|
1829
|
+
return leanAsyncReturn(job, { kind: kind, name: name, file: rel, sha256: sha, buildSha256: fp, deduped: false, note: '文件已写入 ' + rel + ';编译已入队(并发上限 leanJobsMaxParallel)。**落地为通过之前,请不要把它当作可复用定义**;结果会在下一轮提示的【形式化结果】行公告,也可用 vibe_math_lean_job 查/等。' })
|
|
1830
|
+
}
|
|
1200
1831
|
if (!await writeTextAbs(abs, body)) return { ok: false, code: 'V2_WRITE_FAILED', message: 'could not write ' + rel }
|
|
1201
1832
|
// 全局库在项目树之外,必须用**绝对路径**执行(相对形式会被解析到项目根之内)。
|
|
1202
1833
|
const run = args.run === false ? null : await leanRunFile(abs)
|
|
@@ -1223,6 +1854,35 @@ export function apply(ctx) {
|
|
|
1223
1854
|
}
|
|
1224
1855
|
if (body === undefined) return { ok: false, code: 'V2_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
|
|
1225
1856
|
const workRel = 'Formal/' + target + '.lean'
|
|
1857
|
+
const sha = leanContentSha(body)
|
|
1858
|
+
const plan = leanBuildPlan()
|
|
1859
|
+
const ctx = leanPlanContext(plan)
|
|
1860
|
+
const fp = leanJobFingerprint(body, ctx)
|
|
1861
|
+
const jobId = leanJobId(target, fp)
|
|
1862
|
+
// 去重(§4.3):对象已 passed 且归档证明与新提交内容一致 ⇒ 跳过。
|
|
1863
|
+
const dedupe = await leanDedupeLookup(jobId, null, sha, target)
|
|
1864
|
+
if (dedupe) {
|
|
1865
|
+
logActivity('formal', (memberId || 'office') + ' 归档证明 ' + target + ' 命中去重(已 passed 且内容一致)')
|
|
1866
|
+
return { ok: true, kind: kind, target: target, file: workRel, proof: dedupe.file, passed: true, deduped: true, sha256: sha, buildSha256: fp, status: 'passed', note: '该对象已是 passed,且归档证明与新提交内容一致:跳过重写与重编译(去重)。' }
|
|
1867
|
+
}
|
|
1868
|
+
if (params.leanAsync !== false) {
|
|
1869
|
+
if (!await writeText(workRel, body)) return { ok: false, code: 'V2_WRITE_FAILED', message: 'could not write ' + workRel }
|
|
1870
|
+
const prevA = formalOf(target)
|
|
1871
|
+
await putFormal(target, Object.assign({}, prevA, {
|
|
1872
|
+
status: prevA.status === 'passed' ? 'passed' : 'attempted',
|
|
1873
|
+
file: workRel,
|
|
1874
|
+
decision: 'used',
|
|
1875
|
+
note: String(args.note || prevA.note || ''),
|
|
1876
|
+
async: { jobId: jobId, state: 'queued', attempts: 0, enqueuedAt: now(), startedAt: 0, settledAt: 0, exitCode: null, buildSha256: fp },
|
|
1877
|
+
updatedAt: now(),
|
|
1878
|
+
}))
|
|
1879
|
+
await writeFormalIndex()
|
|
1880
|
+
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 } })
|
|
1881
|
+
return leanAsyncReturn(job, {
|
|
1882
|
+
kind: kind, target: target, file: workRel, status: 'attempted', sha256: sha, buildSha256: fp,
|
|
1883
|
+
note: '工作文件已写入 ' + workRel + ';编译已入队。**只有该作业在同一个构建上下文下落地为通过,才会写入 Verified/Lean/' + target + '.lean 并把状态变为 passed**——在它落地之前,这个对象不是"已通过形式化"。',
|
|
1884
|
+
})
|
|
1885
|
+
}
|
|
1226
1886
|
if (!await writeText(workRel, body)) return { ok: false, code: 'V2_WRITE_FAILED', message: 'could not write ' + workRel }
|
|
1227
1887
|
const run = await leanRunFile(workRel)
|
|
1228
1888
|
const prev = formalOf(target)
|
|
@@ -1530,10 +2190,13 @@ export function apply(ctx) {
|
|
|
1530
2190
|
// 提示词里不能出现任何 Lean 字样(contract §2 / §10.1)。
|
|
1531
2191
|
function formalWorkSection() {
|
|
1532
2192
|
const t = formalWorkLine()
|
|
1533
|
-
|
|
2193
|
+
const notice = leanNoticeSection()
|
|
2194
|
+
const math = mathWorkLine()
|
|
2195
|
+
if (!t && !notice && !math) return ''
|
|
1534
2196
|
// 回执契约(契约 §6.3):工作轮也必须被告知 formal 字段,否则"顺手形式化"里做出的难度判断
|
|
1535
2197
|
// 无处可写,代理只能沉默——那正是 v2 首版死通道的成因。
|
|
1536
|
-
|
|
2198
|
+
// 数学计算可用性行(prompts.md §2+§4):与"顺手形式化"并列注入;off 档为空串。
|
|
2199
|
+
return '\n' + t + formalReplyNote() + notice + math + '\n'
|
|
1537
2200
|
}
|
|
1538
2201
|
function capabilitiesText(role) {
|
|
1539
2202
|
const maxCalls = role === 'solver' ? params.solverMaxToolCalls : params.verifierMaxToolCalls
|
|
@@ -2883,7 +3546,7 @@ export function apply(ctx) {
|
|
|
2883
3546
|
const sub = subprocessOf()
|
|
2884
3547
|
if (sub === undefined || typeof sub.spawn !== 'function') return { ok: false, exitCode: null, code: 'NO_SUBPROCESS' }
|
|
2885
3548
|
let handle
|
|
2886
|
-
try { handle = sub
|
|
3549
|
+
try { handle = await spawnHandle(sub, { argv: paperArgvFor(engine), cwd: dirAbs, stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } }, graceMs: capMs }) }
|
|
2887
3550
|
catch (e) { return { ok: false, exitCode: null, code: 'SPAWN_FAILED', message: String((e && e.message) || e) } }
|
|
2888
3551
|
let timedOut = false, timer = null, outcome
|
|
2889
3552
|
try {
|
|
@@ -3216,6 +3879,12 @@ export function apply(ctx) {
|
|
|
3216
3879
|
// 并发计数由 agentRegistry 推导:清空 registry 后自然归零,无需显式赋值。
|
|
3217
3880
|
}
|
|
3218
3881
|
await writeJson('VibeMath_State/process_epoch.json', processEpoch)
|
|
3882
|
+
// Lean 异步作业的崩溃恢复(spec §2.6):**同一进程 continue 不重驱**(作业还在内存队列里正常跑),
|
|
3883
|
+
// 只有"新进程/新会话接手这棵树"(fresh 或 stale epoch)才按 Formal/Jobs/*.json 恢复;
|
|
3884
|
+
// 恢复绝不置 passed(settle 的唯一判据仍然要重新校验内容哈希与 exit code)。
|
|
3885
|
+
if (fresh || stale) { try { await recoverLeanJobs() } catch (e) { console.error('vibe-math-v2: lean job recovery failed: ' + String((e && e.message) || e)) } }
|
|
3886
|
+
// 数学计算的引擎探测(异步)在 init 时先跑一次:提示词侧只读缓存现算(mathWorkLine)。
|
|
3887
|
+
try { await refreshMathProbe(true) } catch (e) { /* 探测失败不影响 init:mathWorkLine 会因此为空 */ }
|
|
3219
3888
|
await saveAll()
|
|
3220
3889
|
return { ok: true }
|
|
3221
3890
|
}
|
|
@@ -3278,7 +3947,13 @@ export function apply(ctx) {
|
|
|
3278
3947
|
if ((await readJson('qs/qs.json')) === undefined) await writeJson('qs/qs.json', [])
|
|
3279
3948
|
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
3949
|
formalState = { records: {}, todo: [] } // 形式化记录随项目切换(loadState 会读新项目的 formal.json)
|
|
3281
|
-
await loadSettings(); await migrateLegacyParams(); await loadState()
|
|
3950
|
+
await loadSettings(); await migrateLegacyParams(); await loadState()
|
|
3951
|
+
// 切到/新建一个项目 = "接手这棵树的遗留作业":Formal/Jobs/*.json 里本进程不认识的记录按 §2.6 恢复
|
|
3952
|
+
// (queued 重入队 / running 标记 interrupted / settled 只在哈希与构建上下文都一致时补写)。
|
|
3953
|
+
try { await recoverLeanJobs() } catch (e) { console.error('vibe-math-v2: lean job recovery on project switch failed: ' + String((e && e.message) || e)) }
|
|
3954
|
+
// 切项目后引擎探测缓存过期(mathEngines/路径可能不同):重探一次再让提示词读它。
|
|
3955
|
+
try { await refreshMathProbe(true) } catch (e) { /* 探测失败不影响切换 */ }
|
|
3956
|
+
await saveAll()
|
|
3282
3957
|
return { ok: true, project: slug, frameworkRoot: frameworkRoot() }
|
|
3283
3958
|
}
|
|
3284
3959
|
|
|
@@ -3291,6 +3966,13 @@ export function apply(ctx) {
|
|
|
3291
3966
|
function objParams(props, required) { return { type: 'object', properties: props, additionalProperties: false, required: required || [] } }
|
|
3292
3967
|
const handlers = {}
|
|
3293
3968
|
function registerTool(name, description, parameters, executeFn) { handlers[name] = executeFn }
|
|
3969
|
+
// math_computation 的**会话层**注册(共享模块的 host 在上面构造,绑定本会话的 params/Paths/fs)。
|
|
3970
|
+
// 模块调用一次 host.register(...) ⇒ handlers['math_computation'] = 该会话的 handler;
|
|
3971
|
+
// apply 层(工具面)在 apply 作用域单独注册一次,按名路由回这里(FREEZE §4 要求两层都在)。
|
|
3972
|
+
mathTool = registerMathComputation(mathHost)
|
|
3973
|
+
// 会话层挂载(**字面量形态**):handler 是模块为**本会话**构造的那一个(闭包住本会话的 params/Paths/fs);
|
|
3974
|
+
// apply 层的同名词由宿主按 handlerName 路由回这里。两条路径的名字/描述/schema 三处逐字一致。
|
|
3975
|
+
registerTool('math_computation', MATH_TOOL_DESCRIPTION, MATH_TOOL_SCHEMA, async function (args, agent) { return await mathTool.handler(args || {}, agent) })
|
|
3294
3976
|
// Lean 工具需要知道调用者是谁(记进活动日志与索引署名)。调用者身份**显式传递**,
|
|
3295
3977
|
// 不从任何全局可变状态推断(审计清单 §1.1)。无法识别时退回 'office'。
|
|
3296
3978
|
function memberIdOf(agent) { try { const id = agent && agent.id ? String(agent.id) : ''; return id || 'office' } catch (e) { return 'office' } }
|
|
@@ -3301,7 +3983,7 @@ export function apply(ctx) {
|
|
|
3301
3983
|
registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), async function () { await refreshParams(); return await getStatus() })
|
|
3302
3984
|
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
3985
|
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
|
|
3986
|
+
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
3987
|
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
3988
|
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
3989
|
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 +4004,24 @@ export function apply(ctx) {
|
|
|
3322
4004
|
// ---- Lean 形式化验证(契约 §5)----
|
|
3323
4005
|
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
4006
|
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) {
|
|
4007
|
+
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
4008
|
const noRefresh = !!(args && args.refresh === false)
|
|
3327
4009
|
const r = noRefresh ? { lib: null, proved: null, objects: Object.keys(formalRecords()).length } : await rebuildLeanLibIndexes()
|
|
4010
|
+
const jobs = []
|
|
4011
|
+
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 || '' }))
|
|
4012
|
+
jobs.sort(function (a, b) { return (b.settledAt || 0) - (a.settledAt || 0) || String(a.jobId).localeCompare(String(b.jobId)) })
|
|
3328
4013
|
return {
|
|
3329
4014
|
ok: true, mode: formalMode(), rebuilt: !noRefresh,
|
|
3330
4015
|
counts: r, todo: formalTodo(),
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
4016
|
+
async: params.leanAsync !== false,
|
|
4017
|
+
jobs: jobs,
|
|
4018
|
+
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 } }),
|
|
4019
|
+
paths: { project: 'Formal/(相对项目根)', lib: 'VibeMath/Formal/Lib/', proved: 'VibeMath/Formal/Proved/', proofs: 'Verified/Lean/', searchPath: vibeRoot(), jobs: 'Formal/Jobs/' },
|
|
4020
|
+
hint: '复用优先:先在 Lib/ 里找现成定义(vibe_math_lean_read 可看原文);新定义用 vibe_math_lean_archive kind=\'def\' 归档,已证引理用 kind=\'lemma\'(归档前先跑通,跑不通不要入库)。异步档用 jobs 字段查后台编译:**只有 state=settled 才是通过**。',
|
|
3334
4021
|
}
|
|
3335
4022
|
})
|
|
4023
|
+
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) })
|
|
4024
|
+
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
4025
|
|
|
3337
4026
|
// ================= slash command /vibe =================
|
|
3338
4027
|
async function dispatchVibeCommand(cmd, args) {
|
|
@@ -3376,6 +4065,17 @@ export function apply(ctx) {
|
|
|
3376
4065
|
// (激活上限)与 paper 锁续租必须由**独立于调度器**的心跳驱动。
|
|
3377
4066
|
paperRetryDue: paperRetryDue,
|
|
3378
4067
|
runPaperRetry: runPaperRetry,
|
|
4068
|
+
// Lean 异步队列(spec §2):心跳推进队列;dispose 时由 apply 级 disposer 终止在跑的编译。
|
|
4069
|
+
runLeanQueue: runLeanQueue,
|
|
4070
|
+
disposeLeanJobs: disposeLeanJobs,
|
|
4071
|
+
// 数学计算(FREEZE §4):心跳用 TTL 刷新探测缓存;off 档不探测(真 no-op)。
|
|
4072
|
+
mathProbeDue: mathProbeDue,
|
|
4073
|
+
refreshMathProbe: refreshMathProbe,
|
|
4074
|
+
mathProbe: function () { return mathProbe },
|
|
4075
|
+
// P2a:persona 两个文本块必须含的「归档→编辑→重跑」规则(文本取自共享模块常量,persona 不手抄)。
|
|
4076
|
+
mathArchiveWorkflowLine: MATH_ARCHIVE_WORKFLOW_LINE,
|
|
4077
|
+
leanQueueSize: function () { return leanQueue.length },
|
|
4078
|
+
leanJobsPublic: function () { const out = []; for (const j of leanJobs.values()) out.push(leanJobPublic(j)); return out },
|
|
3379
4079
|
// childOwner 裁剪用:这个会话当前仍"可能再发 subagent/end"的 child(在册的 + 任务正在等的)。
|
|
3380
4080
|
referencedChildIds: function () {
|
|
3381
4081
|
const out = Object.keys(agentRegistry)
|
|
@@ -3421,7 +4121,7 @@ export function apply(ctx) {
|
|
|
3421
4121
|
registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), 'vibe_math_status')
|
|
3422
4122
|
registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json.', objParams({}), 'vibe_math_report')
|
|
3423
4123
|
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
|
|
4124
|
+
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
4125
|
registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), 'vibe_math_setup')
|
|
3426
4126
|
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
4127
|
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 +4143,17 @@ export function apply(ctx) {
|
|
|
3443
4143
|
// 人/代理主动调用时一样可用。
|
|
3444
4144
|
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
4145
|
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')
|
|
4146
|
+
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')
|
|
4147
|
+
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')
|
|
4148
|
+
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')
|
|
4149
|
+
|
|
4150
|
+
// ── math_computation:apply 层(工具面)────────────────────────────────────────────────────
|
|
4151
|
+
// 共享模块的 handler 在**会话层**构造(它闭包住该会话的 params/Paths/fs);这一层只把
|
|
4152
|
+
// 工具名/描述/schema 挂到宿主工具面,执行时由 apply 级包装按 handlerName 路由回
|
|
4153
|
+
// s.handlers['math_computation'](即上面那个会话级实例)。两层都注册是 v2/v3 的硬要求。
|
|
4154
|
+
// **字面量形态**(名字字面量 + 描述/schema 常量):与 v3 的两层声明保持同一形状,
|
|
4155
|
+
// 静态扫描器/注册面审计(audit-registration、audit-v3-registration-parity)都能看见。
|
|
4156
|
+
registerTool('math_computation', MATH_TOOL_DESCRIPTION, MATH_TOOL_SCHEMA, 'math_computation')
|
|
3447
4157
|
|
|
3448
4158
|
// /vibe slash command (registered once; routed per session)
|
|
3449
4159
|
ctx.effect(() => commands.register({
|
|
@@ -3488,6 +4198,11 @@ export function apply(ctx) {
|
|
|
3488
4198
|
// 拒绝的撰写派遣仍要重试,在途时还要续租 paper 锁(否则别的会话 2 分钟后可接管同一棵树)。
|
|
3489
4199
|
// runPaperRetry() 在两种情形都不成立时立即返回(一次布尔判断)。
|
|
3490
4200
|
if (typeof s.runPaperRetry === 'function') s.runPaperRetry().catch(function (e) { console.error('vibe-math-v2: paper retry failed: ' + String((e && e.message) || e)) })
|
|
4201
|
+
// Lean 异步队列(spec §2.2):同样独立于 scheduler.running —— 作业在收口后仍要落地。
|
|
4202
|
+
// runLeanQueue() 内部**不 await 编译**,所以这条心跳不会被一个编译占住。
|
|
4203
|
+
if (typeof s.runLeanQueue === 'function') s.runLeanQueue().catch(function (e) { console.error('vibe-math-v2: lean queue failed: ' + String((e && e.message) || e)) })
|
|
4204
|
+
// 数学计算的探测缓存 TTL 刷新(15 分钟或从未探测过):mathWorkLine 只读缓存,不能自己 await。
|
|
4205
|
+
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
4206
|
}
|
|
3492
4207
|
// childOwner / sessions 裁剪:每 30 拍(约 30s)一次,成本是"会话数 × 映射数"的一次扫描。
|
|
3493
4208
|
if ((++beat % 30) === 0) {
|
|
@@ -3495,7 +4210,13 @@ export function apply(ctx) {
|
|
|
3495
4210
|
try { pruneSessions() } catch (e) { console.error('vibe-math-v2: pruneSessions failed: ' + String((e && e.message) || e)) }
|
|
3496
4211
|
}
|
|
3497
4212
|
}, 1000)
|
|
3498
|
-
return () =>
|
|
4213
|
+
return () => {
|
|
4214
|
+
clearInterval(t)
|
|
4215
|
+
// 卸载/销毁:终止在跑的编译并标记 interrupted(不留孤儿进程,spec §2.5)。
|
|
4216
|
+
for (const s of sessions.values()) {
|
|
4217
|
+
try { if (typeof s.disposeLeanJobs === 'function') s.disposeLeanJobs() } catch (e) { console.error('vibe-math-v2: lean dispose failed: ' + String((e && e.message) || e)) }
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
3499
4220
|
})
|
|
3500
4221
|
}
|
|
3501
4222
|
|
|
@@ -3767,6 +4488,71 @@ function paperTitleFromMd(md) {
|
|
|
3767
4488
|
return m ? m[1].trim() : ''
|
|
3768
4489
|
}
|
|
3769
4490
|
|
|
4491
|
+
// ============================================================================================
|
|
4492
|
+
// Lean 增量 + 异步(规格:docs/formal-verification.md;设计:_oneoff/lean-incremental-async-spec.md)
|
|
4493
|
+
// **纯函数部分**(module scope,v2/v3 逐字同构)。
|
|
4494
|
+
// ============================================================================================
|
|
4495
|
+
/** 搜索路径 flag:收敛成一个常量(不同 Lean 版本拼写可能不同,改这里即可,接口不变)。 */
|
|
4496
|
+
const LEAN_SEARCH_PATH_FLAG = '--search-path'
|
|
4497
|
+
/** 用户已显式给出搜索路径就不再注入(显式覆盖优先)。 */
|
|
4498
|
+
function hasLeanSearchFlag(args) {
|
|
4499
|
+
return (Array.isArray(args) ? args : []).some(function (a) {
|
|
4500
|
+
const s = String(a)
|
|
4501
|
+
return s === LEAN_SEARCH_PATH_FLAG || s.indexOf(LEAN_SEARCH_PATH_FLAG + '=') === 0 || s === '-R' || s === '--root'
|
|
4502
|
+
})
|
|
4503
|
+
}
|
|
4504
|
+
/**
|
|
4505
|
+
* 搜索路径注入计划(修订 §2):用户 leanArgs 已显式给过 ⇒ 什么都不注入(显式覆盖优先);
|
|
4506
|
+
* 否则**先注入用户给的 leanSearchPaths,再注入自动根**,去重且保持顺序。
|
|
4507
|
+
*/
|
|
4508
|
+
function leanSearchPathPlan(userArgs, extraPaths, autoRoot) {
|
|
4509
|
+
const args = Array.isArray(userArgs) ? userArgs.map(String) : []
|
|
4510
|
+
if (hasLeanSearchFlag(args)) return { inject: [], paths: [], explicit: true }
|
|
4511
|
+
const out = []
|
|
4512
|
+
const seen = {}
|
|
4513
|
+
const add = function (p) {
|
|
4514
|
+
const s = String(p == null ? '' : p).trim()
|
|
4515
|
+
if (!s || seen[s]) return
|
|
4516
|
+
seen[s] = true
|
|
4517
|
+
out.push(s)
|
|
4518
|
+
}
|
|
4519
|
+
for (const p of (Array.isArray(extraPaths) ? extraPaths : [])) add(p)
|
|
4520
|
+
add(autoRoot)
|
|
4521
|
+
const inject = []
|
|
4522
|
+
for (const p of out) { inject.push(LEAN_SEARCH_PATH_FLAG, p) }
|
|
4523
|
+
return { inject: inject, paths: out, explicit: false }
|
|
4524
|
+
}
|
|
4525
|
+
/** 规范化文本:换行统一 \n、去行尾空白、去尾部空行——内容哈希只反映语义内容。 */
|
|
4526
|
+
function normalizeLeanText(t) {
|
|
4527
|
+
return String(t == null ? '' : t).replace(/\r\n?/g, '\n').replace(/[ \t]+$/gm, '').replace(/\n+$/, '') + '\n'
|
|
4528
|
+
}
|
|
4529
|
+
/** 内容 sha256(归档去重与作业幂等的键)。 */
|
|
4530
|
+
function leanContentSha(t) { return createHash('sha256').update(normalizeLeanText(t), 'utf8').digest('hex') }
|
|
4531
|
+
/**
|
|
4532
|
+
* 构建上下文(修订 §4):同一段代码在不同引擎/参数/搜索路径下结果可能不同,
|
|
4533
|
+
* 所以"已通过"的判定必须绑定**构建上下文**,不能只看内容。
|
|
4534
|
+
*/
|
|
4535
|
+
function leanBuildContext(engine, args, searchPaths) {
|
|
4536
|
+
return JSON.stringify({
|
|
4537
|
+
engine: String(engine == null ? '' : engine),
|
|
4538
|
+
args: (Array.isArray(args) ? args : []).map(String),
|
|
4539
|
+
searchPaths: (Array.isArray(searchPaths) ? searchPaths : []).map(String),
|
|
4540
|
+
})
|
|
4541
|
+
}
|
|
4542
|
+
/** 作业指纹 = sha256(内容 + 构建上下文)(修订 §4:jobId 取它的前 12 位)。 */
|
|
4543
|
+
function leanJobFingerprint(contentText, buildCtx) {
|
|
4544
|
+
return createHash('sha256').update(normalizeLeanText(contentText) + '\n' + String(buildCtx || ''), 'utf8').digest('hex')
|
|
4545
|
+
}
|
|
4546
|
+
/** jobId = `<target|name>-<指纹[0:12]>`:同一内容 + 同一构建上下文天然只有一个作业。 */
|
|
4547
|
+
function leanJobId(key, fingerprint) {
|
|
4548
|
+
const k = String(key == null ? '' : key).replace(/[^A-Za-z0-9_.-]/g, '_').replace(/^_+|_+$/g, '').slice(0, 60) || 'job'
|
|
4549
|
+
return k + '-' + String(fingerprint == null ? '' : fingerprint).slice(0, 12)
|
|
4550
|
+
}
|
|
4551
|
+
/** "已落地且通过"的**唯一**判据:state=settled 且 exit 0 且未超时未中断(内容哈希由 settle 校验)。 */
|
|
4552
|
+
function leanJobSettledOk(rec) { return !!rec && rec.state === 'settled' && rec.exitCode === 0 && !rec.timedOut && !rec.interrupted }
|
|
4553
|
+
/** 主动性档位(修订 §1):与"验证时的要求强度"(formalVerify)是两件事。 */
|
|
4554
|
+
const LEAN_INITIATIVE_MODES = ['off', 'normal', 'eager']
|
|
4555
|
+
|
|
3770
4556
|
export const __testHelpers = {
|
|
3771
4557
|
uuid,
|
|
3772
4558
|
shortId,
|
|
@@ -3781,6 +4567,17 @@ export const __testHelpers = {
|
|
|
3781
4567
|
parseProgress,
|
|
3782
4568
|
sanitizeToolFilter,
|
|
3783
4569
|
registeredToolsFromError,
|
|
4570
|
+
// Lean 增量/异步(§7 的纯函数守卫面)
|
|
4571
|
+
LEAN_SEARCH_PATH_FLAG,
|
|
4572
|
+
hasLeanSearchFlag,
|
|
4573
|
+
leanSearchPathPlan,
|
|
4574
|
+
normalizeLeanText,
|
|
4575
|
+
leanContentSha,
|
|
4576
|
+
leanBuildContext,
|
|
4577
|
+
leanJobFingerprint,
|
|
4578
|
+
leanJobId,
|
|
4579
|
+
leanJobSettledOk,
|
|
4580
|
+
LEAN_INITIATIVE_MODES,
|
|
3784
4581
|
// final paper(spec §6 的纯函数守卫面)
|
|
3785
4582
|
PAPER_SKELETON,
|
|
3786
4583
|
PAPER_NO_EVIDENCE,
|