dsh-vibe-math 2.5.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 +175 -369
- package/README.md +175 -370
- package/cordis.patch.yml +78 -22
- package/docs/AUDIT-CHECKLIST.md +3 -3
- package/docs/final-paper.md +172 -0
- package/docs/formal-verification.md +184 -29
- package/docs/math-computation.md +121 -0
- package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -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 +25 -9
- package/installer.js +7 -4
- package/package.json +21 -3
- package/prompt-corpus-persona/persona-corpus.json +16 -16
- package/prompt-corpus-persona/persona-corpus.md +164 -52
- package/prompt-corpus-v2/formal-verify-v2.json +128 -92
- package/prompt-corpus-v2/formal-verify-v2.md +1574 -108
- package/prompt-corpus-v3/formal-verify-v3.json +137 -53
- package/prompt-corpus-v3/formal-verify-v3.md +1406 -100
- 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 +64 -11
- package/tests/audit-persona-surface.test.mjs +53 -16
- package/tests/audit-v5-integrity.mjs +14 -0
- package/tests/e2e-v5-round2.test.mjs +923 -3
- package/tests/formal-verify-v2.test.mjs +295 -6
- package/tests/formal-verify-v3.test.mjs +275 -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 +14 -8
- package/tests/selfdrive-v5.mjs +55 -4
- package/vibe-math-v2/agent.cordis.yml +36 -14
- 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 +1653 -48
- 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 +30 -12
- 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 +1623 -53
- 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 +44 -8
- 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 +1592 -37
- 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 +46 -10
- 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 +2233 -42
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +153 -4
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
|
@@ -39,6 +39,22 @@
|
|
|
39
39
|
// VibeMath/current.<sessionId>.json = per-session current project.
|
|
40
40
|
export const name = 'vibe-math-v3'
|
|
41
41
|
export const inject = ['subagents', 'agents', 'fs', 'tools', 'commands']
|
|
42
|
+
import { createHash } from 'node:crypto'
|
|
43
|
+
// 数学计算工具(共享模块,四套字节一致;由 integration owner 维护,本文件只做接线)。
|
|
44
|
+
// 模块内部自己 import './math-engines.js',接线方不直接引用引擎表。
|
|
45
|
+
import {
|
|
46
|
+
registerMathComputation,
|
|
47
|
+
MATH_PARAM_NAMES,
|
|
48
|
+
MATH_PARAM_DEFAULTS,
|
|
49
|
+
MATH_TOOL_DESCRIPTION,
|
|
50
|
+
MATH_TOOL_SCHEMA,
|
|
51
|
+
MATH_ARCHIVE_WORKFLOW_LINE,
|
|
52
|
+
MATH_PERSONA_TOOL_LINE,
|
|
53
|
+
MATH_RULE_LINES,
|
|
54
|
+
normalizeMathParams,
|
|
55
|
+
mathAvailabilityLine,
|
|
56
|
+
probeMathEngines,
|
|
57
|
+
} from './math-computation.js'
|
|
42
58
|
|
|
43
59
|
// Standing mount: DSH mounts each agent preset ONCE per preset and joins every
|
|
44
60
|
// session that names it to that SAME plugin instance (see @deepseek-ai/dsh-agent-presets).
|
|
@@ -179,6 +195,24 @@ export function apply(ctx) {
|
|
|
179
195
|
leanCommand: 'lean', // 要执行的 Lean 可执行文件(例:'lake')
|
|
180
196
|
leanArgs: [], // 插在文件名之前的附加参数(例:['env','lean'] 配 leanCommand='lake')
|
|
181
197
|
leanTimeoutMs: 120000, // 单次 Lean 运行超时上限(毫秒,正整数)
|
|
198
|
+
leanAsync: true, // true = Lean 编译走后台队列(入队即返回)| false = 同步 await(旧语义)
|
|
199
|
+
leanJobsMaxParallel: 1, // 后台编译并发上限(默认 1 = 串行;可调大以并行编译)
|
|
200
|
+
leanInitiative: 'normal', // 日常流程中的形式化主动性:off | normal | eager(与 formalVerify 的"验证要求强度"是两件事)
|
|
201
|
+
leanSearchPaths: [], // 额外搜索路径(默认空 = 只用自动注入的 VibeMath 根);非空时先注入它们、再注入自动根
|
|
202
|
+
// ---- 最终论文(规格:docs/final-paper.md;v2/v3 为单作者变体)----
|
|
203
|
+
finalPaper: true, // 收口时是否自动撰写最终论文(false 只关自动触发;/vibe paper 仍可用)
|
|
204
|
+
paperFormat: 'both', // both = md + tex | md = 只写 markdown | tex = 只写 latex
|
|
205
|
+
paperLanguage: 'zh', // zh = 中文(ctexart/xelatex 优先)| en = 英文(article/pdflatex 优先)
|
|
206
|
+
paperCompilePdf: true, // 检测到 LaTeX 时是否编译 paper.pdf
|
|
207
|
+
paperLatexCommand: '', // 指定 LaTeX 引擎(空 = 按语言探测 xelatex/latexmk/pdflatex/lualatex/tectonic)
|
|
208
|
+
// ---- 数学计算 math_computation(规格:docs/math-computation.md;六参数已冻结,拼写不得改)----
|
|
209
|
+
// 值取共享模块的 MATH_PARAM_DEFAULTS(四套逐字一致);数组必须**拷贝**,否则四套会共享同一个默认数组。
|
|
210
|
+
mathComputation: MATH_PARAM_DEFAULTS.mathComputation, // off | auto | on(auto = 探测到任一允许引擎才工作)
|
|
211
|
+
mathMode: MATH_PARAM_DEFAULTS.mathMode, // typed | typed+shell(后者才允许提示词里的 shell 兜底段)
|
|
212
|
+
mathEngines: MATH_PARAM_DEFAULTS.mathEngines.slice(), // 允许的引擎(含 cli;cli 默认开启,受 mathMode/mathEngines 双闸)
|
|
213
|
+
mathTimeoutMs: MATH_PARAM_DEFAULTS.mathTimeoutMs, // 单次计算超时上限(毫秒,下界 1000)
|
|
214
|
+
mathPackages: MATH_PARAM_DEFAULTS.mathPackages.slice(), // 需要预检的包(缺包只报告 + 给安装计划)
|
|
215
|
+
mathInstallScope: MATH_PARAM_DEFAULTS.mathInstallScope, // user | system(system 只对当次显式调用生效,永不记忆)
|
|
182
216
|
}
|
|
183
217
|
let params = Object.assign({}, DEFAULT_PARAMS)
|
|
184
218
|
let scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null } // activeCount 由 activeCount() 从 agentRegistry 推导(防漂移,同 v2)
|
|
@@ -298,7 +332,23 @@ export function apply(ctx) {
|
|
|
298
332
|
{ name: 'formalVerify', type: 'enum', options: ['off', 'encourage', 'require'], description: 'Lean 形式化验证:off = 不额外要求(默认,提示词里不出现任何 Lean 内容);encourage = 鼓励按实现难度形式化,Lean 通过后验证转为忠实性审查;require = 同上并加门禁:真/假定论必须先达到 Lean 已通过 或 已记录显式阻塞原因,否则记为未定论(formal-required)并进「形式化待办」', suggestion: 'off' },
|
|
299
333
|
{ name: 'leanCommand', type: 'string', description: 'Lean 可执行文件(例:lean / lake;配合 leanArgs=[env,lean] 用 lake)', suggestion: 'lean' },
|
|
300
334
|
{ name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加参数', suggestion: [] },
|
|
301
|
-
{ name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean
|
|
335
|
+
{ name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,非正数回退默认);异步档下它同时是**每个后台作业**的预算(到时主动 terminate,作业记 timeout,对象留在 attempted)', suggestion: 120000 },
|
|
336
|
+
{ name: 'leanAsync', type: 'boolean', description: 'Lean 编译模式:true(默认)= 后台队列,vibe_math_lean_run / vibe_math_lean_archive{run:true} 立即返回 async.jobId 入队,成员不阻塞,结果由下一轮提示的【形式化结果】行与 vibe_math_lean_job 公告(**只有作业落地为 ok 才会置 passed 并写归档证明**);false = 完全同步 await(与旧行为逐字一致)', suggestion: true },
|
|
337
|
+
{ name: 'leanJobsMaxParallel', type: 'integer', description: '后台 Lean 编译的并发上限(默认 1 = 串行,保持可预测的资源占用;调大可并行编译多个作业)', suggestion: 1 },
|
|
338
|
+
{ name: 'leanInitiative', type: 'enum', options: ['off', 'normal', 'eager'], description: '日常流程中的形式化主动性:off(不主动,只在验证提示词按 formalVerify 的要求做)| normal(默认:顺手把有价值且可能复用的东西形式化)| eager(更主动:日常就主动把有价值的小引理/命题/定义形式化)。注意它与 formalVerify(验证时的要求强度:off|encourage|require)是**两件事**', suggestion: 'normal' },
|
|
339
|
+
{ name: 'leanSearchPaths', type: 'string[]', description: '额外 Lean 搜索路径(默认空数组 = 只用框架自动注入的 VibeMath 根)。非空时按顺序先注入这里给的路径、再注入自动根(去重);若 leanArgs 里已显式给了 --search-path/-R/--root,则完全尊重用户配置、不注入任何东西', suggestion: [] },
|
|
340
|
+
{ name: 'finalPaper', type: 'boolean', description: '收口(checkTermination 的完整收口分支:无未解决问题、无待验证对象、无任务/计划/门)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已定论命题/解法/方法整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
|
|
341
|
+
{ name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md(跳过编译);tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
|
|
342
|
+
{ name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
|
|
343
|
+
{ name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志(不阻塞定稿)', suggestion: true },
|
|
344
|
+
{ name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。解析不到时按"未检测到"降级', suggestion: '' },
|
|
345
|
+
// ---- 数学计算 math_computation(六参数冻结;描述与 prompts.md §1/§6 口径一致)----
|
|
346
|
+
{ name: 'mathComputation', type: 'enum', options: ['off', 'auto', 'on'], description: '数学计算总开关:off = 真无操作(提示词零提及);auto = 探测到 mathEngines 里任一允许引擎才工作;on = 同上(探测失败时工具仍返回可执行的安装指引,而不是假装可用)', suggestion: 'auto' },
|
|
347
|
+
{ name: 'mathMode', type: 'enum', options: ['typed', 'typed+shell'], description: '计算策略:typed+shell(默认)= 工具不可用时允许宿主 shell 兜底,但结论必须标注"未经工具归档(shell 路径)";typed = 只用工具路径(提示词里不出现 shell 兜底段,engine:"cli" 返回 REFUSED{reason:policy})', suggestion: 'typed+shell' },
|
|
348
|
+
{ name: 'mathEngines', type: 'string[]', description: '允许的引擎列表(默认 python|r|octave|julia|matlab|maple|wolfram|cli)。cli 默认开启且走同一套超时/输出上限/回执;从列表里移除某引擎即禁用(商业引擎只探测+许可,永不安装)', suggestion: MATH_PARAM_DEFAULTS.mathEngines.slice() },
|
|
349
|
+
{ name: 'mathTimeoutMs', type: 'integer', description: '单次数学计算的超时上限(毫秒,默认 60000,最小 1000);到时主动 terminate 并把回执标为 MATH_TIMEOUT', suggestion: 60000 },
|
|
350
|
+
{ name: 'mathPackages', type: 'string[]', description: '需要预检的包(默认空)。缺包只报告 + 给"用户自装指引"或"代理代装计划",不会执行脚本,也永不自动安装', suggestion: [] },
|
|
351
|
+
{ name: 'mathInstallScope', type: 'enum', options: ['user', 'system'], description: '安装作用域:user(默认,用户级目录);system 只对当次显式调用生效、永不记忆(不会写进状态文件)', suggestion: 'user' },
|
|
302
352
|
]
|
|
303
353
|
|
|
304
354
|
// ================= fs (adapted to DSH 0.1.1: resolve returns {targetKey, displayPath}) =================
|
|
@@ -360,6 +410,18 @@ export function apply(ctx) {
|
|
|
360
410
|
if (isWindows()) return 'Remove-Item -Force -LiteralPath ' + psQuote(path) + ' -ErrorAction SilentlyContinue'
|
|
361
411
|
return 'rm -f ' + shQuote(path)
|
|
362
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* `subprocess.spawn` 的契约返回**句柄**(带 `done`),但桩宿主/测试替身常常写成 `async spawn(...)`
|
|
415
|
+
* ——返回的是 Promise,`handle.done` 是 undefined,`await handle.done` 得到 undefined,随后
|
|
416
|
+
* `outcome.exitCode` 抛 "Cannot read properties of undefined (reading 'exitCode')",被 catch 成一条
|
|
417
|
+
* 误导性的目录创建失败。这里统一兼容一次:Promise 就先 await,句柄缺 `done` 才如实报错。
|
|
418
|
+
*/
|
|
419
|
+
async function spawnHandle(subprocess, spec) {
|
|
420
|
+
const raw = subprocess.spawn(spec)
|
|
421
|
+
const handle = (raw && typeof raw.then === 'function') ? await raw : raw
|
|
422
|
+
if (!handle || handle.done === undefined) throw new Error('subprocess.spawn returned no handle.done')
|
|
423
|
+
return handle
|
|
424
|
+
}
|
|
363
425
|
async function runShell(script, cwd) {
|
|
364
426
|
const subprocess = subprocessOf()
|
|
365
427
|
if (subprocess === undefined) return { ok: false, error: 'no-subprocess' }
|
|
@@ -367,9 +429,9 @@ export function apply(ctx) {
|
|
|
367
429
|
const argv = isWindows()
|
|
368
430
|
? ['powershell', '-NoProfile', '-NonInteractive', '-Command', script]
|
|
369
431
|
: ['/bin/sh', '-c', script]
|
|
370
|
-
const handle = subprocess
|
|
432
|
+
const handle = await spawnHandle(subprocess, { argv: argv, cwd: cwd || workspaceRoot(), stdio: { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' }, graceMs: 20000 })
|
|
371
433
|
const outcome = await handle.done
|
|
372
|
-
return { ok: outcome.exitCode === 0, exitCode: outcome.exitCode }
|
|
434
|
+
return { ok: !!outcome && outcome.exitCode === 0, exitCode: (outcome && outcome.exitCode !== undefined) ? outcome.exitCode : null }
|
|
373
435
|
} catch (e) { return { ok: false, error: String((e && e.message) || e) } }
|
|
374
436
|
}
|
|
375
437
|
/**
|
|
@@ -380,19 +442,139 @@ export function apply(ctx) {
|
|
|
380
442
|
// Progress_Logs/ 必须和别的骨架目录**一起**建出来(审计 L8):vibe_math_report 会写
|
|
381
443
|
// `Progress_Logs/report.json`,README 也把它列为布局的一部分,但此前它只靠 writeText 的隐式
|
|
382
444
|
// mkdir 兜底——在宿主不支持删除/创建的路径上(runShell 失败)报告目录就时有时无。
|
|
383
|
-
async function ensureDirs() { const base = frameworkRoot(); const dirs = ['Problems', 'Progress', 'Progress_Logs', 'Propos', 'Methods', 'Verified/命题', 'Verified/问题', 'Verified/Lean', 'Formal', 'Reliable', 'Notes', 'Logs/Verification', 'Logs/Plans', 'State']; const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Methods', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d })); return await runShell(mkdirCmd(paths)) }
|
|
445
|
+
async function ensureDirs() { const base = frameworkRoot(); const dirs = ['Problems', 'Progress', 'Progress_Logs', 'Propos', 'Methods', 'Verified/命题', 'Verified/问题', 'Verified/Lean', 'Formal', 'Formal/Jobs', 'Reliable', 'Notes', 'Logs/Verification', 'Logs/Plans', 'State', 'Computation']; const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Methods', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d })); return await runShell(mkdirCmd(paths)) }
|
|
384
446
|
async function removeFile(rel) { const base = frameworkRoot(); return await runShell(rmCmd(base + '/' + rel)) }
|
|
385
447
|
|
|
448
|
+
// ================= 数学计算 math_computation:会话侧接线(共享模块,FREEZE §4/§5) =================
|
|
449
|
+
// 探测量(是否装了 python/r/…)是异步的,而提示词是同步构造的 ⇒ 会话级缓存 + TTL 刷新,
|
|
450
|
+
// 提示词侧只读缓存现算(与 formalVerify / Lean 的动态纪律一致)。
|
|
451
|
+
const MATH_PROBE_TTL_MS = 15 * 60 * 1000
|
|
452
|
+
let mathProbe = null
|
|
453
|
+
let mathProbeAt = 0
|
|
454
|
+
/**
|
|
455
|
+
* `host.spawn` 适配:模块只给 `{argv, cwd, timeoutMs, stdoutCap, stderrCap}`,超时/终止由**接线方**负责
|
|
456
|
+
* (契约:超时必须 handle.terminate())。返回**完整** stdout/stderr——模块自己落盘完整版、只在返回体裁 64KB,
|
|
457
|
+
* 所以 stdio 上限要宽(默认 4MB),否则"超大输出"永远测不出完整落盘。
|
|
458
|
+
*/
|
|
459
|
+
async function mathSpawn(spec) {
|
|
460
|
+
const sub = subprocessOf()
|
|
461
|
+
if (sub === undefined || typeof sub.spawn !== 'function') return null // 模块映射为 MATH_NO_SUBPROCESS
|
|
462
|
+
const s = spec || {}
|
|
463
|
+
const cap = Math.max(1000, Number(s.timeoutMs) || Number(params.mathTimeoutMs) || 60000)
|
|
464
|
+
const outCap = Math.max(4096, Number(s.stdoutCap) || 4 * 1024 * 1024)
|
|
465
|
+
const errCap = Math.max(4096, Number(s.stderrCap) || 4 * 1024 * 1024)
|
|
466
|
+
const started = now()
|
|
467
|
+
let handle
|
|
468
|
+
try {
|
|
469
|
+
handle = await spawnHandle(sub, {
|
|
470
|
+
argv: s.argv,
|
|
471
|
+
cwd: s.cwd || frameworkRoot(),
|
|
472
|
+
stdio: { stdin: 'ignore', stdout: { maxBytes: outCap }, stderr: { maxBytes: errCap } },
|
|
473
|
+
graceMs: cap,
|
|
474
|
+
})
|
|
475
|
+
} catch (e) { return null }
|
|
476
|
+
let timedOut = false, killed = false, timer = null, outcome
|
|
477
|
+
try {
|
|
478
|
+
outcome = await Promise.race([
|
|
479
|
+
handle.done,
|
|
480
|
+
new Promise(function (resolve) {
|
|
481
|
+
timer = setTimeout(function () {
|
|
482
|
+
timedOut = true
|
|
483
|
+
killed = true
|
|
484
|
+
try { if (handle && typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* best effort */ }
|
|
485
|
+
resolve({ exitCode: null })
|
|
486
|
+
}, cap)
|
|
487
|
+
}),
|
|
488
|
+
])
|
|
489
|
+
} catch (e) { if (timer !== null) clearTimeout(timer); return null }
|
|
490
|
+
if (timer !== null) clearTimeout(timer)
|
|
491
|
+
let out = '', err = ''
|
|
492
|
+
try { if (handle.collected && handle.collected.stdout) out = handle.collected.stdout.readFrom(0).text } catch (e) { /* best effort */ }
|
|
493
|
+
try { if (handle.collected && handle.collected.stderr) err = handle.collected.stderr.readFrom(0).text } catch (e) { /* best effort */ }
|
|
494
|
+
return {
|
|
495
|
+
exit: (outcome && outcome.exitCode !== undefined) ? outcome.exitCode : null,
|
|
496
|
+
timedOut: timedOut, killed: killed, ms: now() - started,
|
|
497
|
+
stdout: String(out == null ? '' : out), stderr: String(err == null ? '' : err),
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/** host.exists:项目根相对路径是否存在。 */
|
|
501
|
+
async function mathExists(rel) {
|
|
502
|
+
try { const t = await fsTarget(rel); const s = await fs.stat(t); return s !== undefined } catch (e) { return false }
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* host.listDir(**可选**,FREEZE §4):模块用它数 `Computation/` 下的 run 目录数(>200 只告警、永不删除)。
|
|
506
|
+
* 必须原样返回 DSH `fs.listDir` 的条目({name,type})——模块按 `type === 'directory'` 计数。
|
|
507
|
+
* 宿主 fs 没有该能力时整个回调都不声明(见 mathHost 的条件字段)。
|
|
508
|
+
*/
|
|
509
|
+
async function mathListDir(rel) {
|
|
510
|
+
try { const t = await fsTarget(rel); const ents = await fs.listDir(t); return Array.isArray(ents) ? ents : [] } catch (e) { return [] }
|
|
511
|
+
}
|
|
512
|
+
/** 会话级 host:参数是**活引用**(函数),回执里写 designator='vibe-math-v3'。 */
|
|
513
|
+
const mathHost = {
|
|
514
|
+
// 模块注册时只**交回** handler/description/parameters;真正的挂载是下面两条**字面量** registerTool
|
|
515
|
+
// 调用(会话层 + apply 层)。原因:tests/audit-v3-registration-parity.mjs 的静态扫描器按
|
|
516
|
+
// `registerTool('<名字字面量>', TOOL_DESC.<同名>, …)` 收集两条路径,动态回调形态它看不见,
|
|
517
|
+
// 会让 `tableKeys.size === byName.size` 不成立。回调保持为空以实现"只注册一次"的显式化。
|
|
518
|
+
register: function (name, description, parameters, handler) { /* 挂载见 mathTool 之后的字面量两行 */ },
|
|
519
|
+
params: function () { return params },
|
|
520
|
+
projectRoot: function () { return frameworkRoot() },
|
|
521
|
+
designator: 'vibe-math-v3',
|
|
522
|
+
// 宿主明知没有 subprocess 服务时可以直接说 false ⇒ 模块立即返回 MATH_NO_SUBPROCESS(不再逐个探引擎)。
|
|
523
|
+
hasSubprocess: function () { const sub = subprocessOf(); return !!(sub && typeof sub.spawn === 'function') },
|
|
524
|
+
// **可选能力,按宿主实际情况声明**:宿主 fs 没有 listDir 就不给这个回调(模块据此跳过
|
|
525
|
+
// "每项目 run 目录数 >200" 的保留检查,而不是每次运行都白问一次)。
|
|
526
|
+
listDir: (typeof fs.listDir === 'function') ? mathListDir : undefined,
|
|
527
|
+
writeText: async function (rel, text) { return await writeText(rel, text) },
|
|
528
|
+
readText: async function (rel) { return await readText(rel) },
|
|
529
|
+
exists: mathExists,
|
|
530
|
+
resolveExecutable: async function (cmd) {
|
|
531
|
+
const sub = subprocessOf()
|
|
532
|
+
if (sub === undefined || typeof sub.resolveExecutable !== 'function') throw new Error('no subprocess service')
|
|
533
|
+
const p = await sub.resolveExecutable(String(cmd))
|
|
534
|
+
if (typeof p !== 'string' || !p) throw new Error('not found: ' + String(cmd))
|
|
535
|
+
return p
|
|
536
|
+
},
|
|
537
|
+
spawn: mathSpawn,
|
|
538
|
+
log: function (kind, msg) { logActivity('math', String(kind) + ': ' + String(msg)) },
|
|
539
|
+
}
|
|
540
|
+
// 真实注册必须等到 `const handlers = {}` 与 `function registerTool(...)` 就绪(handlers 是 const,
|
|
541
|
+
// 提前调用会撞 TDZ)⇒ 这里只留槽位,注册放在会话工具表旁。
|
|
542
|
+
let mathTool = null
|
|
543
|
+
/** 刷新探测缓存。`mathComputation:'off'` ⇒ 不探测、不缓存、不注入(真 no-op)。 */
|
|
544
|
+
async function refreshMathProbe(force) {
|
|
545
|
+
if (params.mathComputation === 'off') { mathProbe = null; mathProbeAt = 0; return null }
|
|
546
|
+
if (!mathTool) return null
|
|
547
|
+
try { mathProbe = await mathTool.probe(force ? { refresh: true } : undefined); mathProbeAt = now() } catch (e) { mathProbe = null }
|
|
548
|
+
return mathProbe
|
|
549
|
+
}
|
|
550
|
+
/** 心跳里的 TTL 刷新(每拍一次布尔判断)。 */
|
|
551
|
+
function mathProbeDue() { return params.mathComputation !== 'off' && (!mathProbe || (now() - mathProbeAt) >= MATH_PROBE_TTL_MS) }
|
|
552
|
+
/**
|
|
553
|
+
* 每轮可用性行(中文)。由模块的 `mathAvailabilityLine(probe,'zh',mathMode)` 按档位拼装:
|
|
554
|
+
* 只有 `typed+shell` 才含 shell 兜底句(`mathMode:'typed'` 天然不含,守卫据此断言)。
|
|
555
|
+
* `off` 档返回空串——提示词零提及(对齐 formalVerify:'off' 的真 no-op 纪律)。
|
|
556
|
+
*/
|
|
557
|
+
function mathWorkLine() {
|
|
558
|
+
if (params.mathComputation === 'off') return ''
|
|
559
|
+
if (!mathProbe) return '' // 还没探测过 ⇒ 不注入(不撒谎;init 会先探一次)
|
|
560
|
+
return '\n' + mathAvailabilityLine(mathProbe, 'zh', params.mathMode) + '\n'
|
|
561
|
+
}
|
|
562
|
+
|
|
386
563
|
// ================= settings =================
|
|
387
564
|
function sanitizeParams(obj) {
|
|
388
565
|
const out = {}
|
|
389
566
|
const intFields = ['maxParallelThreshold', 'solverMaxRounds', 'directionsPerSolver', 'verifierCount', 'debateMaxRounds', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries', 'planningHorizon', 'planMinIntervalMs', 'plannerMaxFails', 'methodKeepIntervalMs', 'methodKeepEvery', 'projectLockTimeoutMs']
|
|
390
567
|
const numFields = ['promoteValueThreshold']
|
|
391
568
|
const arrayFields = ['solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny', 'leanArgs']
|
|
392
|
-
const boolFields = ['plannerEnabled', 'methodAutoPromote', 'indexAutoRebuild']
|
|
569
|
+
const boolFields = ['plannerEnabled', 'methodAutoPromote', 'indexAutoRebuild', 'finalPaper', 'paperCompilePdf', 'leanAsync']
|
|
570
|
+
// 数学计算六参数的显式强制(FREEZE §5.3):交给共享模块的 normalizeMathParams 逐键判型
|
|
571
|
+
// (enum/array/integer)——`'false'`/错类型/错枚举一律回退默认,**绝不**落到末尾的
|
|
572
|
+
// `else { out[k] = v }`(那条尾巴会把任意字符串当合法值放行)。
|
|
573
|
+
const mathNorm = normalizeMathParams(obj)
|
|
393
574
|
for (const k of Object.keys(DEFAULT_PARAMS)) {
|
|
394
575
|
if (!(k in obj)) continue
|
|
395
576
|
const v = obj[k]
|
|
577
|
+
if (MATH_PARAM_NAMES.indexOf(k) !== -1) { out[k] = Object.prototype.hasOwnProperty.call(mathNorm, k) ? mathNorm[k] : DEFAULT_PARAMS[k]; continue }
|
|
396
578
|
if (intFields.indexOf(k) !== -1) { const n = Number(v); out[k] = Number.isFinite(n) ? Math.floor(n) : DEFAULT_PARAMS[k] }
|
|
397
579
|
else if (numFields.indexOf(k) !== -1) { const n = Number(v); out[k] = Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : DEFAULT_PARAMS[k] }
|
|
398
580
|
else if (arrayFields.indexOf(k) !== -1) { out[k] = Array.isArray(v) ? v.filter(function (x) { return typeof x === 'string' }) : DEFAULT_PARAMS[k] }
|
|
@@ -407,7 +589,20 @@ export function apply(ctx) {
|
|
|
407
589
|
else if (k === 'formalVerify') { out[k] = (v === 'off' || v === 'encourage' || v === 'require') ? v : 'off' }
|
|
408
590
|
else if (k === 'leanCommand') { const s = String(v == null ? '' : v).trim(); out[k] = s || 'lean' }
|
|
409
591
|
else if (k === 'leanTimeoutMs') { const n = Number(v); out[k] = (Number.isFinite(n) && n > 0) ? Math.floor(n) : DEFAULT_PARAMS[k] }
|
|
592
|
+
// Lean 增量/异步(spec §1.1 + 修订 §5):并行度必须有下界(0 会让队列永不推进,静默失效)。
|
|
593
|
+
else if (k === 'leanJobsMaxParallel') { const n = Number(v); out[k] = Number.isFinite(n) ? Math.max(1, Math.min(8, Math.floor(n))) : DEFAULT_PARAMS[k] }
|
|
594
|
+
else if (k === 'leanInitiative') { out[k] = (v === 'off' || v === 'normal' || v === 'eager') ? v : DEFAULT_PARAMS[k] }
|
|
595
|
+
else if (k === 'leanSearchPaths') {
|
|
596
|
+
if (!Array.isArray(v)) { out[k] = DEFAULT_PARAMS[k] } else {
|
|
597
|
+
const seen = {}
|
|
598
|
+
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 })
|
|
599
|
+
}
|
|
600
|
+
}
|
|
410
601
|
else if (k === 'solverAllowNetwork' || k === 'verifierAllowNetwork' || k === 'solverAllowScripts' || k === 'verifierAllowScripts') { out[k] = (v === true || v === false || v === '') ? v : DEFAULT_PARAMS[k] }
|
|
602
|
+
// 最终论文(spec v2 §B):布尔/枚举必须**显式归一化**,不允许未知键直通('false' 会保持真值)。
|
|
603
|
+
else if (k === 'paperFormat') { out[k] = (v === 'both' || v === 'md' || v === 'tex') ? v : DEFAULT_PARAMS[k] }
|
|
604
|
+
else if (k === 'paperLanguage') { out[k] = (v === 'zh' || v === 'en') ? v : DEFAULT_PARAMS[k] }
|
|
605
|
+
else if (k === 'paperLatexCommand') { const s = String(v == null ? '' : v).trim(); out[k] = s }
|
|
411
606
|
else { out[k] = v }
|
|
412
607
|
}
|
|
413
608
|
return out
|
|
@@ -1212,7 +1407,7 @@ export function apply(ctx) {
|
|
|
1212
1407
|
'feasibility ∈ [0,1] = your estimate of the probability this direction leads to a full solution. Respond with ONLY a single JSON object in a ```json code fence (no prose outside it). Register the directions as metadata; the scheduler writes them into the research log:\n' +
|
|
1213
1408
|
'{"meta":{"kind":"directions","qid":"<qid>","directions":[{"id":"d1","title":"...","method":"...","core_assumption":"...","feasibility":0.5}],"methods_used":[{"id":"m-...","效果":"<为何该方向借鉴它>","建议":"..."}],"new_inventions":[{"类型":"方法|工具|...","标题":"...","内容描述":"...","是否已入库":false}]}}' +
|
|
1214
1409
|
// 顺手形式化(契约 §6.2)+ 回执字段(契约 §6.3)
|
|
1215
|
-
(
|
|
1410
|
+
mathWorkLine() + formalDailySection()
|
|
1216
1411
|
}
|
|
1217
1412
|
function rederivePrompt(q, prog) {
|
|
1218
1413
|
const prior = prog.map(function (d) {
|
|
@@ -1227,7 +1422,7 @@ export function apply(ctx) {
|
|
|
1227
1422
|
'Then deeply DERIVE 1-3 BRAND-NEW directions never tried before, each with a one-line motivation. Return the UNION of high-potential leftover directions and the brand-new directions (drop dead ends).\n\n' +
|
|
1228
1423
|
'feasibility ∈ [0,1]. Respond with ONLY a single JSON object in a ```json code fence (no prose outside it). Register the directions as metadata; the scheduler writes them into the research log:\n' +
|
|
1229
1424
|
'{"meta":{"kind":"directions","qid":"<qid>","directions":[{"id":"d1","title":"...","method":"...","core_assumption":"...","feasibility":0.5}],"methods_used":[{"id":"m-...","效果":"...","建议":"..."}],"new_inventions":[{"类型":"方法|工具|...","标题":"...","内容描述":"...","是否已入库":false}]}}' +
|
|
1230
|
-
(
|
|
1425
|
+
mathWorkLine() + formalDailySection()
|
|
1231
1426
|
}
|
|
1232
1427
|
function directionSummary(d) {
|
|
1233
1428
|
return 'id ' + d.id + '「' + d.title + '」method=' + d.method + ' | round=' + d.round + ' status=' + d.status +
|
|
@@ -1275,7 +1470,7 @@ export function apply(ctx) {
|
|
|
1275
1470
|
'区分规则:methods_used 只能填**已存在的方法卡 ID**(m-…,来自 AVAILABLE METHODS 列表)——引用你自己刚想出的新方法/新技巧不属于 methods_used,请如实填入 new_inventions(它会由 Method Keeper 蒸馏建卡);不要把方法名/标题当 id 填进 methods_used。'
|
|
1276
1471
|
// 顺手形式化(契约 §6.2):把常用/可复用的对象、假设、新定义沉淀到全局 Lean 库;
|
|
1277
1472
|
// 回执里同样要带上 formal 难度判断字段(契约 §6.3)。
|
|
1278
|
-
|
|
1473
|
+
head += mathWorkLine() + formalDailySection()
|
|
1279
1474
|
return head
|
|
1280
1475
|
}
|
|
1281
1476
|
function verifierTargetText(r) {
|
|
@@ -1334,7 +1529,8 @@ export function apply(ctx) {
|
|
|
1334
1529
|
'For each pending invention decide: create a NEW method card, or fold it into an EXISTING method (as an improvement). Only list 可信断言 for claims already verified (ids from Verified/) — everything else stays 经验 (experiential). You may propose 上级体系/子方法 links to organize methods into systems.\n' +
|
|
1335
1530
|
// Method Keeper 的职责正是「沉淀可复用方法」,所以形式化的沉淀也归它:可复用的定义/假设
|
|
1336
1531
|
// 进全局 Lib/,已成立的引理进 Proved/,让后续项目的证明直接 import 复用(契约 §6.2)。
|
|
1337
|
-
(
|
|
1532
|
+
mathWorkLine() +
|
|
1533
|
+
(formalWorkLine() + (formalOn() ? '\n【方法沉淀 × Lean 形式化】除了方法卡,你沉淀的每个可复用对象 / 定义 / 假设都应当归档到全局 Lean 库(vibe_math_lean_archive kind=\'def\'),已成立的引理归档到 Proved/(kind=\'lemma\');归档时**连同定义与陈述一起写清**,方便后续直接 import。\n' : '')) +
|
|
1338
1534
|
'OUTPUT CONTRACT — pick ONE channel. Write method cards into Markdown; only the created IDs, which cards were used, and improvements cross the machine reply.\n' +
|
|
1339
1535
|
'CHANNEL A (recommended, you can write files): write each method card into `Methods/<m-id>.md` (`# 方法|标题` + `- 标题/ID/类型/状态/可信断言/适用场景` + `## 核心内容`/`## 应用记录`/`## 改进历史`), then reply ONLY this metadata:\n' +
|
|
1340
1536
|
'{"meta":{"kind":"methods","used":[{"id":"m-...","效果":"...","建议":"..."}],"created":["m-xxx"],"improvements":[{"id":"m-...","改进内容":"...","原因":"..."}]}}\n' +
|
|
@@ -2472,21 +2668,30 @@ export function apply(ctx) {
|
|
|
2472
2668
|
* 在一个 .lean 文件上跑工具链。**任何**失败模式(无 subprocess 服务、工具链不存在、
|
|
2473
2669
|
* spawn 失败、超时、非零退出)都变成可读结果:调度循环永远不会因为 Lean 而崩。
|
|
2474
2670
|
*/
|
|
2671
|
+
/**
|
|
2672
|
+
* 路径/存在性守卫(同步档与异步档共用同一套错误码)。
|
|
2673
|
+
*/
|
|
2475
2674
|
async function leanRunFile(relPath, timeoutMs) {
|
|
2476
|
-
const started = now()
|
|
2477
2675
|
const rel = String(relPath == null ? '' : relPath).trim()
|
|
2478
2676
|
if (!rel) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'file is required' }
|
|
2479
2677
|
const abs = await leanResolveRun(rel)
|
|
2480
|
-
if (abs === null) {
|
|
2481
|
-
return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'Lean 文件必须位于 ' + vibeRoot().replace(/\\/g, '/') + '/ 之内(收到 ' + rel + ')' }
|
|
2482
|
-
}
|
|
2678
|
+
if (abs === null) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'Lean 文件必须位于 ' + vibeRoot().replace(/\\/g, '/') + '/ 之内(收到 ' + rel + ')' }
|
|
2483
2679
|
if (!/\.lean$/i.test(abs)) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'only .lean files can be executed' }
|
|
2484
2680
|
if (await readTextAbs(abs) === undefined) return { ok: false, code: 'V3_NOT_FOUND', message: 'no such file: ' + rel }
|
|
2681
|
+
return await leanExecFile(rel, abs, timeoutMs)
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* 已解析路径的**实际执行**(同步档与后台作业共用):搜索路径注入(spec §3 + 修订 §2)
|
|
2685
|
+
* 在**用户 leanArgs 之后、文件名之前**;作业用入队时冻结的 plan(修订 §4)。
|
|
2686
|
+
*/
|
|
2687
|
+
async function leanExecFile(rel, abs, timeoutMs, plan, onHandle) {
|
|
2688
|
+
const started = now()
|
|
2485
2689
|
const sub = subprocessOf()
|
|
2486
2690
|
if (sub === undefined || typeof sub.spawn !== 'function') {
|
|
2487
2691
|
return { ok: false, code: 'NO_SUBPROCESS', message: 'the host exposes no subprocess service; Lean cannot be executed here', file: rel, ms: 0 }
|
|
2488
2692
|
}
|
|
2489
|
-
const
|
|
2693
|
+
const p = plan || leanBuildPlan()
|
|
2694
|
+
const cmd = String(p.engine || 'lean')
|
|
2490
2695
|
const cap = Math.max(1000, Number(timeoutMs) || Number(params.leanTimeoutMs) || 120000)
|
|
2491
2696
|
if (typeof sub.resolveExecutable !== 'function') {
|
|
2492
2697
|
return { ok: false, code: 'LEAN_NOT_FOUND', message: 'the host subprocess service exposes no resolveExecutable(); cannot resolve "' + cmd + '" —— 仍可把形式化代码写下来归档,但无法在此宿主上执行', file: rel, ms: now() - started }
|
|
@@ -2495,10 +2700,13 @@ export function apply(ctx) {
|
|
|
2495
2700
|
try { exe = await sub.resolveExecutable(cmd) } catch (e) {
|
|
2496
2701
|
return { ok: false, code: 'LEAN_NOT_FOUND', message: 'cannot resolve "' + cmd + '": ' + String((e && e.message) || e) + ' —— 仍可把形式化代码写下来归档,但无法在此宿主上执行', file: rel, ms: now() - started }
|
|
2497
2702
|
}
|
|
2498
|
-
const
|
|
2703
|
+
const inject = Array.isArray(p.inject) ? p.inject : leanSearchPathPlan(p.args, p.searchPaths, vibeRoot()).inject
|
|
2704
|
+
const argv = [exe].concat((Array.isArray(p.args) ? p.args : []).map(String)).concat(inject).concat([abs])
|
|
2705
|
+
const searchPath = (Array.isArray(p.searchPaths) && p.searchPaths.length) ? p.searchPaths.join(',') : vibeRoot()
|
|
2706
|
+
logActivity('formal', 'Lean 运行 ' + rel + '(search-path=' + searchPath + ')')
|
|
2499
2707
|
let handle
|
|
2500
2708
|
try {
|
|
2501
|
-
handle = sub
|
|
2709
|
+
handle = await spawnHandle(sub, {
|
|
2502
2710
|
argv: argv,
|
|
2503
2711
|
cwd: frameworkRoot(),
|
|
2504
2712
|
stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } },
|
|
@@ -2507,7 +2715,8 @@ export function apply(ctx) {
|
|
|
2507
2715
|
} catch (e) {
|
|
2508
2716
|
return { ok: false, code: 'LEAN_SPAWN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
|
|
2509
2717
|
}
|
|
2510
|
-
|
|
2718
|
+
leanSyncHandle = handle
|
|
2719
|
+
if (typeof onHandle === 'function') { try { onHandle(handle) } catch (e) { /* best effort */ } }
|
|
2511
2720
|
let timedOut = false
|
|
2512
2721
|
let timer = null
|
|
2513
2722
|
let outcome
|
|
@@ -2527,26 +2736,24 @@ export function apply(ctx) {
|
|
|
2527
2736
|
return { ok: false, code: 'LEAN_RUN_FAILED', message: String((e && e.message) || e), file: rel, ms: now() - started }
|
|
2528
2737
|
}
|
|
2529
2738
|
if (timer) clearTimeout(timer)
|
|
2739
|
+
leanSyncHandle = null
|
|
2740
|
+
if (typeof onHandle === 'function') { try { onHandle(null) } catch (e) { /* best effort */ } }
|
|
2530
2741
|
let out = '', err = ''
|
|
2531
2742
|
try { if (handle.collected && handle.collected.stdout) out = handle.collected.stdout.readFrom(0).text } catch (e) { /* best effort */ }
|
|
2532
2743
|
try { if (handle.collected && handle.collected.stderr) err = handle.collected.stderr.readFrom(0).text } catch (e) { /* best effort */ }
|
|
2533
2744
|
const exitCode = outcome ? outcome.exitCode : null
|
|
2534
2745
|
const ms = now() - started
|
|
2535
2746
|
const ok = exitCode === 0
|
|
2747
|
+
const isTimeout = timedOut || ms >= cap
|
|
2536
2748
|
return {
|
|
2537
2749
|
ok: ok, exitCode: exitCode, signal: (outcome && outcome.signal) || null, ms: ms,
|
|
2538
|
-
command: argv.join(' '), file: rel,
|
|
2750
|
+
command: argv.join(' '), file: rel, searchPath: searchPath,
|
|
2539
2751
|
stdout: formalTail(out, 4000), stderr: formalTail(err, 4000),
|
|
2540
|
-
timedOut:
|
|
2541
|
-
code: ok ? undefined : (
|
|
2752
|
+
timedOut: isTimeout,
|
|
2753
|
+
code: ok ? undefined : (isTimeout ? 'LEAN_TIMEOUT' : 'LEAN_FAILED'),
|
|
2542
2754
|
}
|
|
2543
2755
|
}
|
|
2544
|
-
|
|
2545
|
-
* 把一次运行记到对象上(契约 §4 状态迁移)。普通运行最多把对象推进到 `attempted`;
|
|
2546
|
-
* `passed` 只能由 vibe_math_lean_archive{kind:'proof'} 且该文件最近一次运行 ok 产生——否则一次
|
|
2547
|
-
* 顺手跑个无关文件就能把对象"洗白"成已形式化。passed/blocked 不会被普通运行降级。
|
|
2548
|
-
*/
|
|
2549
|
-
async function formalSetRun(target, run) {
|
|
2756
|
+
async function formalSetRun(target, run, asyncInfo) {
|
|
2550
2757
|
const t = formalId(target)
|
|
2551
2758
|
if (!t) return
|
|
2552
2759
|
const prev = formalOf(t)
|
|
@@ -2555,10 +2762,10 @@ export function apply(ctx) {
|
|
|
2555
2762
|
status: status,
|
|
2556
2763
|
file: (run && run.file) || prev.file || '',
|
|
2557
2764
|
run: { at: now(), ok: !!(run && run.ok), exitCode: (run && run.exitCode !== undefined) ? run.exitCode : null, ms: (run && run.ms) || 0, stdoutTail: formalTail(run && run.stdout, 800), stderrTail: formalTail(run && run.stderr, 800) },
|
|
2765
|
+
async: asyncInfo || prev.async || null,
|
|
2558
2766
|
updatedAt: now(),
|
|
2559
2767
|
}))
|
|
2560
2768
|
}
|
|
2561
|
-
|
|
2562
2769
|
// ---- 提示词注入(都在**构造提示词的那一刻**现算,故运行中切模式立刻生效)--------
|
|
2563
2770
|
function formalPromptBlock(target) {
|
|
2564
2771
|
if (!formalOn()) return ''
|
|
@@ -2589,7 +2796,7 @@ export function apply(ctx) {
|
|
|
2589
2796
|
L.push(' 请复核这个判断是否成立;若你认为其实可以形式化,请指出来并动手做。')
|
|
2590
2797
|
} else {
|
|
2591
2798
|
L.push(' · 请先判断该对象的**实现难度**:若能在可接受的工作量内形式化,优先写 Lean 代码并执行。')
|
|
2592
|
-
L.push(' · 工具:vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib
|
|
2799
|
+
L.push(' · 工具:vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库/jobs)· vibe_math_lean_read(取回归档原文)')
|
|
2593
2800
|
L.push(' · 工作目录:Formal/(相对项目根);可复用定义放 ' + (vibeRoot() + '/Formal/Lib/').replace(/\\/g, '/')
|
|
2594
2801
|
+ ',已证引理放 ' + (vibeRoot() + '/Formal/Proved/').replace(/\\/g, '/') + ';写之前先 vibe_math_lean_lib 查重。')
|
|
2595
2802
|
L.push(' · **一旦 Lean 通过,你唯一需要确认的就是忠实性**:定义/对象/条件/假设/结论是否与命题原文逐条一致。请把注意力放在这种核对上,而不是重新做一遍推导。')
|
|
@@ -2601,14 +2808,27 @@ export function apply(ctx) {
|
|
|
2601
2808
|
L.push(' · 若你判断不值得或无法形式化,可以不做,但请在回执的 formal 字段写明难度判断(decision=\'blocked\' 时必须写明 note)。')
|
|
2602
2809
|
}
|
|
2603
2810
|
L.push(' · 归档可复用定义/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run);跑不通不要入库。')
|
|
2811
|
+
// —— §5 B(逐字):三条筛选判据 + 先查再写 + 异步"落地前不得当成已通过" ——
|
|
2812
|
+
L.push(' · 三条筛选判据:① 有价值或可能复用;② 较为关键或必要;③ 你对该陈述有把握(置信度高)。')
|
|
2813
|
+
L.push(' · 写新定义/证明前**先 vibe_math_lean_lib 查已有库**(vibe_math_lean_read 可取回归档原文逐字复用),查不到再写;复用已归档内容用 `import Formal.Lib.<name>` / `import Formal.Proved.<name>`。')
|
|
2814
|
+
L.push(' · **没把握就记 blocked** 并写清难点,别用形式化掩盖不确定。')
|
|
2815
|
+
L.push(' · 该对象若已有后台编译在队列中(leanAsync 默认开启),**不得**在作业落地为通过之前声称已通过或转忠实性审查;等 vibe_math_lean_lib 的 jobs 显示 settled 再审。')
|
|
2604
2816
|
L.push(' · 宿主没有 Lean 工具链(LEAN_NOT_FOUND)或宿主不提供 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并在回执的 note 里写明"宿主无 Lean 工具链"——这算显式阻塞原因,定论门禁可以据此放行。')
|
|
2605
2817
|
}
|
|
2606
|
-
|
|
2818
|
+
// 数学计算可用性行(prompts.md §2):验证轮同样告知"先 probe 再 run / 回执即引用"。
|
|
2819
|
+
return L.join('\n') + leanNoticeSection() + mathWorkLine()
|
|
2607
2820
|
}
|
|
2608
2821
|
/** 日常提示词里的"顺手形式化"一行(off 模式返回空串 = 一个字都不多)。 */
|
|
2609
2822
|
function formalWorkLine() {
|
|
2610
|
-
|
|
2611
|
-
|
|
2823
|
+
// v5 统一口径:**日常主动性(leanInitiative)与验证要求强度(formalVerify)是两根独立的轴**。
|
|
2824
|
+
// 不能用 `!formalOn()` 一票否决——那会让 `leanInitiative:'eager'` 在 `formalVerify:'off'` 下静默
|
|
2825
|
+
// 失效。`formalVerify:'off'` 只关掉**验证阶段**的 Lean 文本(formalPromptBlock 仍 `!formalOn() ⇒ ''`);
|
|
2826
|
+
// `leanInitiative:'off'` 连日线都不注入(真无操作)。
|
|
2827
|
+
const initiative = LEAN_INITIATIVE_MODES.indexOf(params.leanInitiative) !== -1 ? params.leanInitiative : 'normal'
|
|
2828
|
+
// 后台作业的一次性公告(spec §2.4):注入到本轮提示里,取走即清空。
|
|
2829
|
+
const notice = leanNoticeSection()
|
|
2830
|
+
if (initiative === 'off' || !(formalOn() || initiative === 'eager')) return ''
|
|
2831
|
+
return '【顺手形式化(' + (formalOn() ? (formalMode() === 'require' ? '强制' : '鼓励') : '仅主动性') + ')】把你工作中常用或可能复用的对象、假设、'
|
|
2612
2832
|
+ '新定义用 Lean 形式化定义并归档到全局可复用库(vibe_math_lean_archive kind=\'def\'),已成立的引理归到 '
|
|
2613
2833
|
+ (vibeRoot() + '/Formal/Proved/').replace(/\\/g, '/') + '(kind=\'lemma\');写之前先 vibe_math_lean_lib 查重,避免重复定义。'
|
|
2614
2834
|
+ (formalMode() === 'require'
|
|
@@ -2616,6 +2836,29 @@ export function apply(ctx) {
|
|
|
2616
2836
|
: '这会让后续的验证与证明省掉大量重复工作。')
|
|
2617
2837
|
// 硬要求 3(契约 §6 顶部):可复用库只收**跑通过**的代码,否则它会被不编译的定义污染。
|
|
2618
2838
|
+ '归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。'
|
|
2839
|
+
+ (initiative === 'eager'
|
|
2840
|
+
? '\n · **主动档(leanInitiative=eager)**:日常就主动把有价值的小引理/命题/定义形式化——每轮工作结束时审视一次"这轮有什么值得进库",值得就顺手归档。'
|
|
2841
|
+
: '\n · 主动性 normal:顺手把明显有价值且可能复用的东西形式化;不必刻意扩大范围。')
|
|
2842
|
+
// —— §5 A(逐字):三条筛选判据 + 先查再写 + 「没把握就记 blocked」 + 异步"落地前不得当成已通过" ——
|
|
2843
|
+
+ '\n · 三条筛选判据:① 有价值或可能复用;② 较为关键或必要;③ 你对该陈述有把握(置信度高)。'
|
|
2844
|
+
+ '\n · **先 vibe_math_lean_lib 查再决定是否重写**:vibe_math_lean_lib 列出现成定义/引理,vibe_math_lean_read 可取回归档原文逐字复用;'
|
|
2845
|
+
+ '复用已归档内容用 `import Formal.Lib.<name>` / `import Formal.Proved.<name>`(模块根 = ' + vibeRoot().replace(/\\/g, '/') + ',框架已把它加进编译搜索路径)。'
|
|
2846
|
+
+ '\n · **没把握就记 blocked**(vibe_math_lean_archive kind=\'blocked\' note=…,或回执 formal 的 blocked):把难点写清楚,别用形式化掩盖不确定。'
|
|
2847
|
+
+ '\n · 编译默认走后台队列(leanAsync=true):入队后你可以继续工作;用 vibe_math_lean_job(可 waitMs 等结果)或下一轮提示里的'
|
|
2848
|
+
+ '【形式化结果】行看结果。**在作业落地为通过之前,不得把该对象当成已通过。**'
|
|
2849
|
+
+ notice
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* 平时工作提示词里的「顺手形式化」日线段 + 回执 note。**两根轴独立**(v5 统一口径):
|
|
2853
|
+
* 日线由 `leanInitiative` 决定(`eager` 在 `formalVerify:'off'` 下也要注入),
|
|
2854
|
+
* `formalReplyNote()` 只属于**验证阶段**(`formalOn()` 门控)。
|
|
2855
|
+
* 用一个 `formalOn()` 把两者一起关掉,正是 audit-B #2 的成因。
|
|
2856
|
+
*/
|
|
2857
|
+
function formalDailySection() {
|
|
2858
|
+
const line = formalWorkLine()
|
|
2859
|
+
const note = formalOn() ? formalReplyNote() : ''
|
|
2860
|
+
if (!line && !note) return ''
|
|
2861
|
+
return (line ? '\n' + line : '') + note
|
|
2619
2862
|
}
|
|
2620
2863
|
/** 回执契约里的 formal 字段(契约 §6.3):非 off 模式必须出现在回执契约里,否则这条通道不可发现。 */
|
|
2621
2864
|
function formalJsonField(target) {
|
|
@@ -2710,6 +2953,304 @@ export function apply(ctx) {
|
|
|
2710
2953
|
note: formalOn() ? '' : '未启用(formalVerify = off;可用 vibe_math_set_params 切到 encourage / require)',
|
|
2711
2954
|
}
|
|
2712
2955
|
}
|
|
2956
|
+
// ================= Lean 增量 + 异步:作业队列(spec §2) =================
|
|
2957
|
+
// 并发上限 = `leanJobsMaxParallel`(默认 1 = 串行)。队列是**内存**态,作业记录镜像到
|
|
2958
|
+
// <项目>/Formal/Jobs/<jobId>.json —— 崩溃恢复、lean_lib.jobs 与 lean_job 都读它。
|
|
2959
|
+
const LEAN_QUEUE_CONCURRENCY_MAX = 8
|
|
2960
|
+
const leanQueue = [] // FIFO:待跑作业
|
|
2961
|
+
const leanJobs = new Map() // jobId -> job(内存视图,含 state)
|
|
2962
|
+
const leanRunningHandles = new Map() // jobId -> 子进程 handle(dispose 时 terminate)
|
|
2963
|
+
let leanSyncHandle = null // 同步档在跑的 handle(同样能被 terminate)
|
|
2964
|
+
const leanPendingNotices = [] // 一次性公告行(注入下一次 round prompt,spec §2.4)
|
|
2965
|
+
/** 当前会话的构建计划(修订 §2/§4):冻结 engine/args/搜索路径,作业与指纹都用它。 */
|
|
2966
|
+
function leanBuildPlan() {
|
|
2967
|
+
const userArgs = (Array.isArray(params.leanArgs) ? params.leanArgs : []).map(String)
|
|
2968
|
+
const plan = leanSearchPathPlan(userArgs, params.leanSearchPaths, vibeRoot())
|
|
2969
|
+
return { engine: String(params.leanCommand || 'lean'), args: userArgs, searchPaths: plan.paths, inject: plan.inject, explicit: plan.explicit }
|
|
2970
|
+
}
|
|
2971
|
+
function leanPlanContext(plan) { return leanBuildContext(plan.engine, plan.args, plan.searchPaths) }
|
|
2972
|
+
/** 归档作业当前应绑定的构建计划(def/lemma 与 proof 都是本项目计划)。 */
|
|
2973
|
+
function leanContextNow() { return leanPlanContext(leanBuildPlan()) }
|
|
2974
|
+
function leanJobsMaxParallel() { return Math.max(1, Math.min(LEAN_QUEUE_CONCURRENCY_MAX, Number(params.leanJobsMaxParallel) || 1)) }
|
|
2975
|
+
function leanActiveCount() { let n = 0; for (const j of leanJobs.values()) if (j.state === 'running') n++ ; return n }
|
|
2976
|
+
function leanSleep(ms) { return new Promise(function (r) { setTimeout(r, Math.max(0, Number(ms) || 0)) }) }
|
|
2977
|
+
/** VibeMath 根相对 → 绝对(全局库不在项目树内)。 */
|
|
2978
|
+
function vibeAbs(rel) { return vibeRoot() + '/' + String(rel).replace(/^\.\//, '') }
|
|
2979
|
+
function leanJobAbs(job) {
|
|
2980
|
+
const rel = String((job && job.rel) || '')
|
|
2981
|
+
if (!rel) return null
|
|
2982
|
+
return job.scope === 'root' ? normalizeAbsPath(vibeAbs(rel)) : normalizeAbsPath(frameworkRoot() + '/' + rel)
|
|
2983
|
+
}
|
|
2984
|
+
/** 作业的回执路径与归档路径(lean_job 返回它们,修订 §3)。 */
|
|
2985
|
+
function leanJobPaths(job) {
|
|
2986
|
+
const rec = { receipt: 'Formal/Jobs/' + job.jobId + '.json' }
|
|
2987
|
+
if (job.kind === 'archive' && job.archive && job.archive.kind === 'proof' && job.target) {
|
|
2988
|
+
rec.work = job.rel
|
|
2989
|
+
rec.archive = job.state === 'settled' ? ('Verified/Lean/' + job.target + '.lean') : ''
|
|
2990
|
+
} else if (job.kind === 'archive') rec.archive = job.rel
|
|
2991
|
+
else rec.file = job.rel
|
|
2992
|
+
return rec
|
|
2993
|
+
}
|
|
2994
|
+
function leanJobRecord(job) {
|
|
2995
|
+
return {
|
|
2996
|
+
jobId: job.jobId, kind: job.kind, state: job.state, attempts: Number(job.attempts || 1),
|
|
2997
|
+
target: job.target || '', name: job.name || '', rel: job.rel || '', scope: job.scope || 'project',
|
|
2998
|
+
sha256: job.sha || '', buildSha256: job.buildSha || '', buildContext: job.buildCtx || '',
|
|
2999
|
+
engine: (job.plan && job.plan.engine) || '', argv: (job.plan && job.plan.args) || [], searchPaths: (job.plan && job.plan.searchPaths) || [],
|
|
3000
|
+
archiveKind: (job.archive && job.archive.kind) || '',
|
|
3001
|
+
enqueuedAt: job.enqueuedAt || 0, startedAt: job.startedAt || 0, settledAt: job.settledAt || 0,
|
|
3002
|
+
exitCode: job.exitCode === undefined ? null : job.exitCode,
|
|
3003
|
+
timedOut: !!job.timedOut, interrupted: !!job.interrupted, note: job.note || '',
|
|
3004
|
+
project: currentProject,
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
async function writeLeanJobFile(job) { await writeJson('Formal/Jobs/' + job.jobId + '.json', leanJobRecord(job)) }
|
|
3008
|
+
function hydrateLeanJob(rec) {
|
|
3009
|
+
return {
|
|
3010
|
+
jobId: String(rec.jobId), kind: rec.kind || 'run', target: rec.target || '', name: rec.name || '',
|
|
3011
|
+
rel: rec.rel || '', scope: rec.scope || 'project', sha: rec.sha256 || '',
|
|
3012
|
+
buildSha: rec.buildSha256 || '', buildCtx: rec.buildContext || '',
|
|
3013
|
+
plan: rec.engine !== undefined ? { engine: rec.engine || '', args: rec.argv || [], searchPaths: rec.searchPaths || [] } : null,
|
|
3014
|
+
archive: rec.archiveKind ? { kind: rec.archiveKind, target: rec.target || '', name: rec.name || '' } : null,
|
|
3015
|
+
state: rec.state || 'queued', attempts: Number(rec.attempts || 1), enqueuedAt: Number(rec.enqueuedAt || 0),
|
|
3016
|
+
startedAt: Number(rec.startedAt || 0), settledAt: Number(rec.settledAt || 0),
|
|
3017
|
+
exitCode: rec.exitCode === undefined ? null : rec.exitCode, timedOut: !!rec.timedOut,
|
|
3018
|
+
interrupted: !!rec.interrupted, run: rec.run || null, note: rec.note || '',
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
/** 入队:同 jobId 已在队列/在跑 ⇒ 复用(幂等);返回作业对象。 */
|
|
3022
|
+
async function enqueueLeanJob(spec) {
|
|
3023
|
+
const existing = leanJobs.get(spec.jobId)
|
|
3024
|
+
if (existing && (existing.state === 'queued' || existing.state === 'running')) return existing
|
|
3025
|
+
const job = Object.assign({}, spec, {
|
|
3026
|
+
state: 'queued', attempts: Number(spec.attempts || ((existing && existing.attempts) || 0) + 1),
|
|
3027
|
+
enqueuedAt: spec.enqueuedAt || now(), startedAt: 0, settledAt: 0, exitCode: null,
|
|
3028
|
+
timedOut: false, interrupted: false, run: null, note: spec.note || '',
|
|
3029
|
+
})
|
|
3030
|
+
leanJobs.set(job.jobId, job)
|
|
3031
|
+
leanQueue.push(job)
|
|
3032
|
+
await writeLeanJobFile(job)
|
|
3033
|
+
formalAnnounce('Lean 作业入队 ' + job.jobId + '(' + job.kind + (job.target ? '|对象 ' + job.target : (job.name ? '|' + job.name : '')) + ',state=queued)')
|
|
3034
|
+
return job
|
|
3035
|
+
}
|
|
3036
|
+
function pushLeanNotice(line) { if (leanPendingNotices.length >= 20) leanPendingNotices.shift(); leanPendingNotices.push(String(line)) }
|
|
3037
|
+
/** 一次性公告(spec §2.4):取走即清空,保证同一条结果只注入一次。 */
|
|
3038
|
+
function leanNoticeSection() {
|
|
3039
|
+
if (leanPendingNotices.length === 0) return ''
|
|
3040
|
+
const lines = leanPendingNotices.splice(0, leanPendingNotices.length)
|
|
3041
|
+
return '\n' + lines.join('\n') + '\n'
|
|
3042
|
+
}
|
|
3043
|
+
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) } }
|
|
3044
|
+
/**
|
|
3045
|
+
* 心跳里推进队列:**不 await 编译本身**(调用方不 await 本函数)。
|
|
3046
|
+
* 并发上限 = `leanJobsMaxParallel`(默认 1,串行);串行时本函数等待该作业跑完再返回。
|
|
3047
|
+
*/
|
|
3048
|
+
async function runLeanQueue() {
|
|
3049
|
+
const max = leanJobsMaxParallel()
|
|
3050
|
+
let started = 0
|
|
3051
|
+
while (leanQueue.length > 0 && leanActiveCount() + started < max) {
|
|
3052
|
+
const job = leanQueue.shift()
|
|
3053
|
+
if (!job || job.state !== 'queued') continue
|
|
3054
|
+
started++
|
|
3055
|
+
if (max <= 1) { await runLeanJob(job); return }
|
|
3056
|
+
runLeanJob(job).catch(function (e) { console.error('vibe-math-v3: lean job failed: ' + String((e && e.message) || e)) })
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
async function runLeanJob(job) {
|
|
3060
|
+
if (job.state !== 'queued') return
|
|
3061
|
+
job.state = 'running'; job.startedAt = now(); job.interrupted = false
|
|
3062
|
+
await writeLeanJobFile(job)
|
|
3063
|
+
let run
|
|
3064
|
+
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) }) }
|
|
3065
|
+
catch (e) { run = { ok: false, exitCode: null, code: 'LEAN_RUN_FAILED', message: String((e && e.message) || e), ms: 0 } }
|
|
3066
|
+
try { await settleLeanJob(job, run) } catch (e) { console.error('vibe-math-v3: lean job settle failed: ' + String((e && e.stack) || e)) }
|
|
3067
|
+
}
|
|
3068
|
+
/** 结果公告行(spec §2.4 的四种结局)。 */
|
|
3069
|
+
function leanSettleNotice(job, settledOk, capMs) {
|
|
3070
|
+
const ms = (job.run && job.run.ms) || 0
|
|
3071
|
+
if (job.interrupted) return '【形式化结果】' + job.jobId + ':中断(会话卸载或崩溃,已标记 attempted,可重跑)'
|
|
3072
|
+
if (job.timedOut) return '【形式化结果】' + job.jobId + ':超时(leanTimeoutMs=' + capMs + ' 已 terminate;对象留在 attempted)'
|
|
3073
|
+
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') : '') + ')'
|
|
3074
|
+
if (job.hashMismatch) return '【形式化结果】' + job.jobId + ':失败(编译期间文件内容已变,本次结果已丢弃;请重跑)'
|
|
3075
|
+
if (job.contextMismatch) return '【形式化结果】' + job.jobId + ':失败(编译期间构建上下文已变:引擎/参数/搜索路径,本次结果已丢弃;请在新上下文下重跑)'
|
|
3076
|
+
return '【形式化结果】' + job.jobId + ':失败(exit ' + String(job.exitCode) + ',见 stderr 尾部)'
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* 落地一个作业(**唯一**的 passed 入口,spec §2.1):只有 exit 0 **且** 内容哈希仍匹配才算 ok;
|
|
3080
|
+
* 其余一律 attempted;中断/超时都不是 passed;内容变了只标记不贴结果。
|
|
3081
|
+
*/
|
|
3082
|
+
async function settleLeanJob(job, run) {
|
|
3083
|
+
const capMs = Math.max(1000, Number(job.timeoutMs) || Number(params.leanTimeoutMs) || 120000)
|
|
3084
|
+
let currentSha = null
|
|
3085
|
+
try { const t = await readTextAbs(leanJobAbs(job)); if (t !== undefined) currentSha = leanContentSha(t) } catch (e) { /* 读不到 ⇒ 视为不匹配 */ }
|
|
3086
|
+
const hashOk = currentSha !== null && currentSha === job.sha
|
|
3087
|
+
// 修订 §4:结果只对**产生它的那个构建上下文**有效。参数在编译途中被改(引擎/参数/搜索路径)
|
|
3088
|
+
// ⇒ 这次 ok 不能贴到"现在的配置"上,宁可判为未通过并明确公告。
|
|
3089
|
+
const ctxNow = leanContextNow()
|
|
3090
|
+
const ctxOk = !job.buildCtx || job.buildCtx === ctxNow
|
|
3091
|
+
job.hashMismatch = !hashOk
|
|
3092
|
+
job.contextMismatch = !ctxOk
|
|
3093
|
+
job.exitCode = run.exitCode === undefined ? null : run.exitCode
|
|
3094
|
+
job.timedOut = !!run.timedOut
|
|
3095
|
+
job.settledAt = now()
|
|
3096
|
+
const settledOk = !job.interrupted && !job.timedOut && run.exitCode === 0 && hashOk && ctxOk
|
|
3097
|
+
job.state = job.interrupted ? 'interrupted' : (job.timedOut ? 'timeout' : (settledOk ? 'settled' : 'failed'))
|
|
3098
|
+
job.run = { at: now(), ok: settledOk, exitCode: job.exitCode, ms: run.ms || 0, timedOut: !!job.timedOut, interrupted: !!job.interrupted, stdoutTail: formalTail(run.stdout, 800), stderrTail: formalTail(run.stderr, 800), searchPath: run.searchPath || vibeRoot(), buildContext: job.buildCtx || '' }
|
|
3099
|
+
if (job.hashMismatch && !job.interrupted && !job.timedOut) job.note = 'file changed while compiling — result discarded (hash mismatch)'
|
|
3100
|
+
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'
|
|
3101
|
+
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 || '' }
|
|
3102
|
+
if (job.kind === 'archive') await settleLeanArchiveJob(job, settledOk, asyncInfo)
|
|
3103
|
+
else if (job.target) await formalSetRun(job.target, Object.assign({}, run, { ok: settledOk, file: job.rel }), asyncInfo)
|
|
3104
|
+
if (!job.interrupted) await writeFormalIndex()
|
|
3105
|
+
await writeLeanJobFile(job)
|
|
3106
|
+
pushLeanNotice(leanSettleNotice(job, settledOk, capMs))
|
|
3107
|
+
formalAnnounce('Lean 作业 ' + job.jobId + ' ' + job.state + '(' + job.kind + ')' + (job.target ? '|对象 ' + job.target : ''))
|
|
3108
|
+
reportDirty = true
|
|
3109
|
+
}
|
|
3110
|
+
/** 归档类作业的落地:**只有 ok 才落库/置 passed**(def/lemma 的文件在入队前已写好)。 */
|
|
3111
|
+
async function settleLeanArchiveJob(job, ok, asyncInfo) {
|
|
3112
|
+
const a = job.archive || {}
|
|
3113
|
+
if (a.kind !== 'proof') {
|
|
3114
|
+
// 与同步档同一份记账:全局库的运行结果进 formalState.libRuns(索引据此显示"跑通/未跑通")。
|
|
3115
|
+
const libRel = String(job.rel || '').replace(/^Formal\//, '')
|
|
3116
|
+
if (libRel) formalState.libRuns[libRel] = { ok: !!ok, exitCode: job.exitCode === undefined ? null : job.exitCode, ms: (job.run && job.run.ms) || 0, at: now() }
|
|
3117
|
+
await rebuildLeanLibIndexes()
|
|
3118
|
+
formalAnnounce('归档作业 ' + job.jobId + '(' + a.kind + ' ' + job.rel + ')' + (ok ? '运行通过' : '运行未通过(见 stderr 尾部;该文件不应被当作可复用定义)'))
|
|
3119
|
+
return
|
|
3120
|
+
}
|
|
3121
|
+
const target = job.target
|
|
3122
|
+
const prev = formalOf(target)
|
|
3123
|
+
const passed = !!ok
|
|
3124
|
+
const stalePrev = passed ? '' : String(prev.proof || ('Verified/Lean/' + target + '.lean'))
|
|
3125
|
+
const rec = Object.assign({}, prev, {
|
|
3126
|
+
status: passed ? 'passed' : 'attempted',
|
|
3127
|
+
file: job.rel,
|
|
3128
|
+
proof: passed ? 'Verified/Lean/' + target + '.lean' : '',
|
|
3129
|
+
decision: 'used',
|
|
3130
|
+
note: String(prev.note || ''),
|
|
3131
|
+
run: job.run,
|
|
3132
|
+
async: asyncInfo,
|
|
3133
|
+
updatedAt: now(),
|
|
3134
|
+
})
|
|
3135
|
+
let withdrawn = null
|
|
3136
|
+
if (passed) {
|
|
3137
|
+
const body = await readTextAbs(leanJobAbs(job))
|
|
3138
|
+
if (body !== undefined) await writeText('Verified/Lean/' + target + '.lean', body)
|
|
3139
|
+
} else if (stalePrev) { try { withdrawn = await withdrawArchivedProof(stalePrev) } catch (e) { withdrawn = { rel: stalePrev, outcome: 'failed' } } }
|
|
3140
|
+
await putFormal(target, rec)
|
|
3141
|
+
await upsertFormalAnchor(target)
|
|
3142
|
+
await rebuildLeanLibIndexes()
|
|
3143
|
+
formalAnnounce('归档作业 ' + job.jobId + ' 为 ' + target + ' ' + (passed ? ('通过,已归档 ' + rec.proof + ',验证转为忠实性审查')
|
|
3144
|
+
: ('未通过:' + formalTail(job.run && job.run.stderrTail, 160) + ';已撤回上一份已通过状态与归档证明' + (withdrawn && withdrawn.outcome !== 'failed' ? '(' + withdrawn.outcome + ')' : '(⚠ 撤回失败,请不要把 ' + stalePrev + ' 当作该对象的证明)'))))
|
|
3145
|
+
}
|
|
3146
|
+
/** 去重(spec §4.3):同内容且已成功过 ⇒ 跳过写盘与重编译。 */
|
|
3147
|
+
async function leanDedupeLookup(jobId, abs, sha, proofTarget) {
|
|
3148
|
+
if (proofTarget) {
|
|
3149
|
+
const rec = formalOf(proofTarget)
|
|
3150
|
+
if (rec && rec.status === 'passed') {
|
|
3151
|
+
const cur = await readTextAbs(vibeAbs('Verified/Lean/' + proofTarget + '.lean'))
|
|
3152
|
+
if (cur !== undefined && leanContentSha(cur) === sha) return { deduped: true, file: 'Verified/Lean/' + proofTarget + '.lean' }
|
|
3153
|
+
}
|
|
3154
|
+
return null
|
|
3155
|
+
}
|
|
3156
|
+
const prev = await readJson('Formal/Jobs/' + jobId + '.json')
|
|
3157
|
+
if (leanJobSettledOk(prev)) {
|
|
3158
|
+
const cur = await readTextAbs(abs)
|
|
3159
|
+
if (cur !== undefined && leanContentSha(cur) === sha) return { deduped: true, file: String(prev.rel || '') }
|
|
3160
|
+
}
|
|
3161
|
+
return null
|
|
3162
|
+
}
|
|
3163
|
+
/** 后台作业的公共返回形状(§1.2)。 */
|
|
3164
|
+
function leanAsyncReturn(job, extra) {
|
|
3165
|
+
return Object.assign({
|
|
3166
|
+
ok: true, async: { jobId: job.jobId, state: job.state }, jobId: job.jobId,
|
|
3167
|
+
message: '已入队后台编译(并发上限 1);你可以继续工作。结果会写入 Formal/ 与索引,并在下一轮提示里公告;也可用 vibe_math_lean_lib 的 jobs 字段随时查看。**在该作业落地为通过之前,不得把相关对象当成已通过。**',
|
|
3168
|
+
}, extra || {})
|
|
3169
|
+
}
|
|
3170
|
+
/** 崩溃恢复(spec §2.6):queued 重入队;running 哈希匹配 ⇒ 标记 interrupted + 重入队(attempts+1),不匹配 ⇒ 只标记;恢复**绝不**置 passed。 */
|
|
3171
|
+
async function recoverLeanJobs() {
|
|
3172
|
+
let files = []
|
|
3173
|
+
try { files = (await listFiles('Formal/Jobs')) || [] } catch (e) { return { recovered: 0 } }
|
|
3174
|
+
let recovered = 0
|
|
3175
|
+
for (let i = 0; i < files.length; i++) {
|
|
3176
|
+
const f = String(files[i])
|
|
3177
|
+
if (!/\.json$/.test(f)) continue
|
|
3178
|
+
const rec = await readJson('Formal/Jobs/' + f)
|
|
3179
|
+
if (!rec || !rec.jobId) continue
|
|
3180
|
+
// 本进程内存里已知的作业(queued/running 或已落地)不再恢复:同一进程内切换项目时,
|
|
3181
|
+
// 内存队列还在正常跑,按文件状态"恢复"会把一个在跑的作业误判成崩溃(重复入队/误置 interrupted)。
|
|
3182
|
+
if (leanJobs.has(String(rec.jobId))) continue
|
|
3183
|
+
const job = hydrateLeanJob(rec)
|
|
3184
|
+
leanJobs.set(job.jobId, job)
|
|
3185
|
+
const abs = leanJobAbs(job)
|
|
3186
|
+
const txt = abs ? await readTextAbs(abs) : undefined
|
|
3187
|
+
const curSha = txt === undefined ? null : leanContentSha(txt)
|
|
3188
|
+
const hashMatch = curSha !== null && curSha === job.sha
|
|
3189
|
+
if (job.state === 'queued') {
|
|
3190
|
+
await enqueueLeanJob(job)
|
|
3191
|
+
pushLeanNotice('【形式化结果】' + job.jobId + ':上次会话未执行的排队作业已重新入队')
|
|
3192
|
+
recovered++
|
|
3193
|
+
} else if (job.state === 'running') {
|
|
3194
|
+
if (hashMatch) {
|
|
3195
|
+
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() }))
|
|
3196
|
+
job.interrupted = true
|
|
3197
|
+
// 先落到 interrupted,再重入队:否则 enqueueLeanJob 的幂等守卫会把这条 "running" 记录
|
|
3198
|
+
// 当成"仍在跑"而早退,attempts 也不会 +1(恢复就静默失效了)。
|
|
3199
|
+
job.state = 'interrupted'
|
|
3200
|
+
await enqueueLeanJob(Object.assign({}, job, { attempts: job.attempts + 1 }))
|
|
3201
|
+
pushLeanNotice('【形式化结果】' + job.jobId + ':中断(会话崩溃,已标记 attempted,已用新 attempts 重新入队)')
|
|
3202
|
+
} else {
|
|
3203
|
+
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() }))
|
|
3204
|
+
job.state = 'interrupted'; job.interrupted = true
|
|
3205
|
+
job.note = 'file changed while the previous session was compiling — not auto-re-driven'
|
|
3206
|
+
await writeLeanJobFile(job)
|
|
3207
|
+
pushLeanNotice('【形式化结果】' + job.jobId + ':中断(文件已变,未自动重驱;请手动重跑)')
|
|
3208
|
+
}
|
|
3209
|
+
recovered++
|
|
3210
|
+
} else if (job.state === 'settled') {
|
|
3211
|
+
// 修订 §4:只有**同一构建上下文**下的 settled(ok) 才允许恢复成 passed。
|
|
3212
|
+
const ctxNow = leanContextNow()
|
|
3213
|
+
const ctxOk = !job.buildCtx || job.buildCtx === ctxNow
|
|
3214
|
+
if (leanJobSettledOk(rec) && hashMatch && ctxOk) {
|
|
3215
|
+
job.run = { at: now(), ok: true, exitCode: 0, ms: 0, timedOut: false, interrupted: false, stdoutTail: '', stderrTail: '', searchPath: vibeRoot(), buildContext: job.buildCtx || '' }
|
|
3216
|
+
if (job.kind === 'archive') await settleLeanArchiveJob(job, true, { jobId: job.jobId, state: 'settled', attempts: job.attempts, settledAt: job.settledAt, exitCode: 0, buildSha256: job.buildSha || '' })
|
|
3217
|
+
else if (job.target) await formalSetRun(job.target, job.run, { jobId: job.jobId, state: 'settled', buildSha256: job.buildSha || '' })
|
|
3218
|
+
pushLeanNotice('【形式化结果】' + job.jobId + ':已按作业记录补写归档与 passed(恢复时校验内容哈希与构建上下文一致)')
|
|
3219
|
+
} else if (job.target) {
|
|
3220
|
+
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() }))
|
|
3221
|
+
pushLeanNotice('【形式化结果】' + job.jobId + ':按作业记录标记为未通过/已变(恢复绝不置 passed)')
|
|
3222
|
+
}
|
|
3223
|
+
recovered++
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
if (recovered > 0) formalAnnounce('Lean 作业恢复:处理了 ' + recovered + ' 条遗留作业记录(queued 重入队 / running 标记中断 / settled 补写;绝不置 passed)')
|
|
3227
|
+
return { recovered: recovered }
|
|
3228
|
+
}
|
|
3229
|
+
/** 会话卸载/销毁:终止在跑的编译并标记 interrupted(不留孤儿进程,spec §2.5)。 */
|
|
3230
|
+
function disposeLeanJobs() {
|
|
3231
|
+
let n = 0
|
|
3232
|
+
for (const job of leanJobs.values()) {
|
|
3233
|
+
if (job.state === 'running') { job.interrupted = true; n++ }
|
|
3234
|
+
}
|
|
3235
|
+
for (const job of leanQueue) { job.state = 'interrupted'; job.interrupted = true; n++ }
|
|
3236
|
+
for (const h of leanRunningHandles.values()) {
|
|
3237
|
+
try { if (h && typeof h.terminate === 'function') h.terminate() } catch (e) { /* best effort */ }
|
|
3238
|
+
}
|
|
3239
|
+
leanRunningHandles.clear()
|
|
3240
|
+
if (leanSyncHandle && typeof leanSyncHandle.terminate === 'function') { try { leanSyncHandle.terminate() } catch (e) { /* best effort */ } }
|
|
3241
|
+
leanSyncHandle = null
|
|
3242
|
+
leanQueue.length = 0
|
|
3243
|
+
if (n > 0) {
|
|
3244
|
+
pushLeanNotice('【形式化结果】干预:会话卸载,' + n + ' 个 Lean 作业已标记 interrupted(未落地为 passed)')
|
|
3245
|
+
// 落盘尽力而为(disposer 不能 await):作业记录留着,供下次 init 恢复时标记。
|
|
3246
|
+
try {
|
|
3247
|
+
for (const job of leanJobs.values()) { if (job.state === 'running' || job.interrupted) writeLeanJobFile(job).catch(function () { }) }
|
|
3248
|
+
} catch (e) { /* best effort */ }
|
|
3249
|
+
}
|
|
3250
|
+
return n
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// ---- 三份索引(框架维护;契约 §9)----
|
|
2713
3254
|
async function writeFormalIndex() {
|
|
2714
3255
|
const recs = formalRecords()
|
|
2715
3256
|
const keys = Object.keys(recs).sort()
|
|
@@ -2769,9 +3310,9 @@ export function apply(ctx) {
|
|
|
2769
3310
|
}
|
|
2770
3311
|
const libRows = await scan(vibeRoot() + '/Formal/Lib', 'Lib')
|
|
2771
3312
|
await writeTextAbs(vibeRoot() + '/Formal/Lib/Index.md', ['# 可复用 Lean 定义库(跨项目)|' + currentProject, '',
|
|
2772
|
-
'>
|
|
2773
|
-
'| 名称 | 文件 | 类别 | 摘要 | 最近运行 |', '
|
|
2774
|
-
.concat(libRows.length ? libRows.map(function (r) { return '| ' + r.base + ' | ' + r.rel + ' | def | ' + r.first.replace(/\|/g, '/') + ' | ' + r.run + ' |' }) : ['| (暂无) | | | | |']).join('\n') + '\n')
|
|
3313
|
+
'> 写新定义之前先查这里:能复用就不要重新定义。复用方式:`import Formal.Lib.<名称>`(模块根 = VibeMath 根)。', '',
|
|
3314
|
+
'| 名称 | 文件 | 类别 | 依赖(import) | 摘要 | 最近运行 |', '|---|---|---|---|---|---|']
|
|
3315
|
+
.concat(libRows.length ? libRows.map(function (r) { return '| ' + r.base + ' | ' + r.rel + ' | def | ' + String(r.depend || '—').replace(/\|/g, '/') + ' | ' + r.first.replace(/\|/g, '/') + ' | ' + r.run + ' |' }) : ['| (暂无) | | | | | |']).join('\n') + '\n')
|
|
2775
3316
|
const provedRows = await scan(vibeRoot() + '/Formal/Proved', 'Proved')
|
|
2776
3317
|
await writeTextAbs(vibeRoot() + '/Formal/Proved/Index.md', ['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|' + currentProject, '',
|
|
2777
3318
|
'> 这些文件是通过内核检查的引理,可直接 import 复用。', '',
|
|
@@ -2829,18 +3370,27 @@ export function apply(ctx) {
|
|
|
2829
3370
|
*/
|
|
2830
3371
|
async function leanRunTool(memberId, o) {
|
|
2831
3372
|
const args = o || {}
|
|
2832
|
-
const
|
|
3373
|
+
const rel = String(args.file || '').trim()
|
|
2833
3374
|
const target = String(args.target || '').trim()
|
|
3375
|
+
if (params.leanAsync !== false) {
|
|
3376
|
+
const guard = await leanResolveRunTarget(rel)
|
|
3377
|
+
if (!guard.ok) return guard
|
|
3378
|
+
const plan = leanBuildPlan()
|
|
3379
|
+
const ctx = leanPlanContext(plan)
|
|
3380
|
+
const fp = leanJobFingerprint(guard.text, ctx)
|
|
3381
|
+
const jobId = leanJobId(target || guard.rel, fp)
|
|
3382
|
+
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 })
|
|
3383
|
+
return leanAsyncReturn(job, { file: guard.rel, state: job.state, hint: '已入队。**在它落地为通过之前,不得把该对象当成已通过**;结果会在下一轮提示的【形式化结果】行里公告,或用 vibe_math_lean_job(可 waitMs 等待)查看。' })
|
|
3384
|
+
}
|
|
3385
|
+
const run = await leanRunFile(rel, args.timeout_ms)
|
|
2834
3386
|
if (run.file || run.ok) {
|
|
2835
3387
|
if (target) { await formalSetRun(target, run); await upsertFormalAnchor(target) }
|
|
2836
3388
|
await writeFormalIndex()
|
|
2837
3389
|
}
|
|
2838
3390
|
if (run.ok) await formalAnnounce('【形式化】' + (memberId || 'scheduler') + ' 运行 Lean 通过:' + run.file + '(' + ((run.ms || 0) / 1000).toFixed(1) + 's)' + (target ? '|对象 ' + target : ''))
|
|
2839
|
-
// 失败提示必须与失败原因一致:工具链缺失 / 宿主没有 subprocess 服务时**没有任何编译器输出**
|
|
2840
|
-
// 可以"按它修复",把它当成普通编译错误会让代理反复重试而不是走"写下代码 + 记录显式阻塞"
|
|
2841
|
-
// 这条出路(契约 §6 硬要求 4)。
|
|
2842
3391
|
const noHost = run.code === 'LEAN_NOT_FOUND' || run.code === 'NO_SUBPROCESS' || run.code === 'LEAN_SPAWN_FAILED'
|
|
2843
3392
|
return Object.assign({ ok: !!run.ok }, run, {
|
|
3393
|
+
async: null,
|
|
2844
3394
|
hint: run.ok
|
|
2845
3395
|
? '通过。若是某个对象的证明,请用 vibe_math_lean_archive kind=\'proof\' 归档(会写入 Verified/Lean/ 并把审查对象变成忠实性);若是可复用定义/引理,用 kind=\'def\'/\'lemma\' 归档到全局库。'
|
|
2846
3396
|
: (noHost
|
|
@@ -2848,6 +3398,80 @@ export function apply(ctx) {
|
|
|
2848
3398
|
: '未通过。请按上面的编译器输出修复后重跑;若判断无法完成,用 vibe_math_lean_archive kind=\'blocked\' 记录原因。'),
|
|
2849
3399
|
})
|
|
2850
3400
|
}
|
|
3401
|
+
/** 异步档的路径/存在性校验(与 leanRunFile 同一套守卫与错误码)。 */
|
|
3402
|
+
async function leanResolveRunTarget(relPath) {
|
|
3403
|
+
const rel = String(relPath || '').trim()
|
|
3404
|
+
if (!rel) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'file is required' }
|
|
3405
|
+
const abs = await leanResolveRun(rel)
|
|
3406
|
+
if (abs === null) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'Lean 文件必须位于 ' + vibeRoot().replace(/\\/g, '/') + '/ 之内(收到 ' + rel + ')' }
|
|
3407
|
+
if (!/\.lean$/i.test(abs)) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'only .lean files can be executed' }
|
|
3408
|
+
const txt = await readTextAbs(abs)
|
|
3409
|
+
if (txt === undefined) return { ok: false, code: 'V3_NOT_FOUND', message: 'no such file: ' + rel }
|
|
3410
|
+
return { ok: true, rel: rel, abs: abs, sha: leanContentSha(txt), text: txt }
|
|
3411
|
+
}
|
|
3412
|
+
/** `lean_read`:只读地取回归档的 Lean 原文(verbatim 复用;§1.2)。 */
|
|
3413
|
+
async function leanReadTool(o) {
|
|
3414
|
+
const args = o || {}
|
|
3415
|
+
const rawName = String(args.name || '').trim()
|
|
3416
|
+
if (!rawName) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'name is required' }
|
|
3417
|
+
if (rawName.indexOf('..') !== -1 || /[\\/]/.test(rawName) || /^[a-z]:/i.test(rawName)) {
|
|
3418
|
+
return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'name must be a bare library name (no path separators, no "..", no absolute path)' }
|
|
3419
|
+
}
|
|
3420
|
+
const name = idSafe(rawName)
|
|
3421
|
+
const kind = (args.kind === 'lib' || args.kind === 'proved') ? args.kind : 'auto'
|
|
3422
|
+
const order = kind === 'auto' ? ['lib', 'proved'] : [kind]
|
|
3423
|
+
for (let i = 0; i < order.length; i++) {
|
|
3424
|
+
const k = order[i]
|
|
3425
|
+
const rel = 'Formal/' + (k === 'lib' ? 'Lib' : 'Proved') + '/' + name + '.lean'
|
|
3426
|
+
const dir = normalizeAbsPath(vibeRoot() + '/Formal/' + (k === 'lib' ? 'Lib' : 'Proved'))
|
|
3427
|
+
const abs = normalizeAbsPath(vibeRoot() + '/' + rel)
|
|
3428
|
+
if (abs.indexOf(dir + '/') !== 0) continue
|
|
3429
|
+
const txt = await readTextAbs(abs)
|
|
3430
|
+
if (txt === undefined) continue
|
|
3431
|
+
const LIMIT = 64 * 1024
|
|
3432
|
+
const bytes = Buffer.byteLength(txt, 'utf8')
|
|
3433
|
+
const truncated = bytes > LIMIT
|
|
3434
|
+
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 }
|
|
3435
|
+
}
|
|
3436
|
+
return { ok: false, code: 'V3_NOT_FOUND', message: 'no archived Lean file named ' + name + '(查过 Formal/Lib 与 Formal/Proved);可先用 vibe_math_lean_lib 看清单' }
|
|
3437
|
+
}
|
|
3438
|
+
/**
|
|
3439
|
+
* `lean_job`(修订 §3,只读):不带 jobId ⇒ 本会话作业清单;带 ⇒ 该作业 state/exitCode/回执路径/归档路径。
|
|
3440
|
+
* waitMs>0 最多等这么久(内部轮询;心跳是独立 timer,不会被阻塞),超时返回当前 state。
|
|
3441
|
+
*/
|
|
3442
|
+
async function leanJobTool(o) {
|
|
3443
|
+
const args = o || {}
|
|
3444
|
+
const wantId = String(args.jobId || '').trim()
|
|
3445
|
+
const waitMs = Math.max(0, Math.min(600000, Number(args.waitMs) || 0))
|
|
3446
|
+
if (!wantId) {
|
|
3447
|
+
const jobs = []
|
|
3448
|
+
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 || '' }))
|
|
3449
|
+
jobs.sort(function (a, b) { return (b.settledAt || 0) - (a.settledAt || 0) || String(a.jobId).localeCompare(String(b.jobId)) })
|
|
3450
|
+
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 一律还不算。' }
|
|
3451
|
+
}
|
|
3452
|
+
let job = leanJobs.get(wantId)
|
|
3453
|
+
if (!job) {
|
|
3454
|
+
const rec = await readJson('Formal/Jobs/' + wantId + '.json')
|
|
3455
|
+
if (!rec || !rec.jobId) return { ok: false, code: 'V3_NOT_FOUND', message: 'no such Lean job: ' + wantId + '(用不带 jobId 的 vibe_math_lean_job 列清单)' }
|
|
3456
|
+
job = hydrateLeanJob(rec)
|
|
3457
|
+
if (!leanJobs.has(job.jobId)) leanJobs.set(job.jobId, job)
|
|
3458
|
+
}
|
|
3459
|
+
const startedAt = now()
|
|
3460
|
+
while (waitMs > 0 && (job.state === 'queued' || job.state === 'running') && (now() - startedAt) < waitMs) {
|
|
3461
|
+
await leanSleep(50)
|
|
3462
|
+
const cur = leanJobs.get(wantId)
|
|
3463
|
+
if (cur) job = cur
|
|
3464
|
+
}
|
|
3465
|
+
return {
|
|
3466
|
+
ok: true, jobId: job.jobId, state: job.state, exitCode: job.exitCode === undefined ? null : job.exitCode,
|
|
3467
|
+
attempt: Number(job.attempts || 1), timedOut: !!job.timedOut, interrupted: !!job.interrupted, note: job.note || '',
|
|
3468
|
+
buildSha256: job.buildSha || '', buildContext: job.buildCtx || '',
|
|
3469
|
+
waitedMs: now() - startedAt, stillRunning: (job.state === 'queued' || job.state === 'running'),
|
|
3470
|
+
passed: leanJobSettledOk(leanJobRecord(job)),
|
|
3471
|
+
paths: leanJobPaths(job),
|
|
3472
|
+
run: job.run || null,
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
2851
3475
|
async function leanArchive(memberId, o) {
|
|
2852
3476
|
const args = o || {}
|
|
2853
3477
|
const kind = String(args.kind || '')
|
|
@@ -2872,6 +3496,22 @@ export function apply(ctx) {
|
|
|
2872
3496
|
const dirRel = kind === 'def' ? 'Lib' : 'Proved'
|
|
2873
3497
|
const rel = 'Formal/' + dirRel + '/' + name + '.lean'
|
|
2874
3498
|
const abs = vibeRoot() + '/' + rel
|
|
3499
|
+
const sha = leanContentSha(body)
|
|
3500
|
+
const plan = leanBuildPlan()
|
|
3501
|
+
const ctx = leanPlanContext(plan)
|
|
3502
|
+
const fp = leanJobFingerprint(body, ctx)
|
|
3503
|
+
const jobId = leanJobId(name, fp)
|
|
3504
|
+
// 去重(§4.3):同内容 + 同构建上下文且已成功归档过 ⇒ 跳过重写与重编译。
|
|
3505
|
+
const dedupe = await leanDedupeLookup(jobId, abs, sha, null)
|
|
3506
|
+
if (dedupe) {
|
|
3507
|
+
await formalAnnounce('【形式化】' + who + ' 归档 ' + kind + ' `' + name + '` 命中去重(同内容与构建上下文,跳过重写与重编译)')
|
|
3508
|
+
return { ok: true, kind: kind, name: name, file: rel, deduped: true, sha256: sha, buildSha256: fp, note: '内容与构建上下文都与已归档并编译通过的版本一致:跳过重写与重编译(去重)。' }
|
|
3509
|
+
}
|
|
3510
|
+
if (params.leanAsync !== false && args.run !== false) {
|
|
3511
|
+
if (!await writeTextAbs(abs, body)) return { ok: false, code: 'V3_WRITE_FAILED', message: 'could not write ' + rel }
|
|
3512
|
+
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 } })
|
|
3513
|
+
return leanAsyncReturn(job, { kind: kind, name: name, file: rel, sha256: sha, buildSha256: fp, deduped: false, note: '文件已写入 ' + rel + ';编译已入队(并发上限 leanJobsMaxParallel)。**落地为通过之前,请不要把它当作可复用定义**;结果会在下一轮提示的【形式化结果】行公告,也可用 vibe_math_lean_job 查/等。' })
|
|
3514
|
+
}
|
|
2875
3515
|
if (!await writeTextAbs(abs, body)) return { ok: false, code: 'V3_WRITE_FAILED', message: 'could not write ' + rel }
|
|
2876
3516
|
// 全局库在项目树之外,必须用**绝对路径**执行(相对形式会在项目根里找)。
|
|
2877
3517
|
const run = args.run === false ? null : await leanRunFile(abs)
|
|
@@ -2888,6 +3528,36 @@ export function apply(ctx) {
|
|
|
2888
3528
|
if (body === undefined && from) { const r = await readFrom(); if (r.err) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: r.err }; body = r.body }
|
|
2889
3529
|
if (body === undefined) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'provide content, or from=<existing .lean file>' }
|
|
2890
3530
|
const workRel = 'Formal/' + target + '.lean'
|
|
3531
|
+
const sha = leanContentSha(body)
|
|
3532
|
+
const plan = leanBuildPlan()
|
|
3533
|
+
const ctx = leanPlanContext(plan)
|
|
3534
|
+
const fp = leanJobFingerprint(body, ctx)
|
|
3535
|
+
const jobId = leanJobId(target, fp)
|
|
3536
|
+
// 去重(§4.3):对象已 passed 且归档证明与新提交内容一致 ⇒ 跳过。
|
|
3537
|
+
const dedupe = await leanDedupeLookup(jobId, null, sha, target)
|
|
3538
|
+
if (dedupe) {
|
|
3539
|
+
await formalAnnounce('【形式化】' + who + ' 归档证明 ' + target + ' 命中去重(已 passed 且内容一致)')
|
|
3540
|
+
return { ok: true, kind: kind, target: target, file: workRel, proof: dedupe.file, passed: true, deduped: true, sha256: sha, buildSha256: fp, status: 'passed', note: '该对象已是 passed,且归档证明与新提交内容一致:跳过重写与重编译(去重)。' }
|
|
3541
|
+
}
|
|
3542
|
+
if (params.leanAsync !== false) {
|
|
3543
|
+
if (!await writeTextAbs(frameworkRoot() + '/' + workRel, body)) return { ok: false, code: 'V3_WRITE_FAILED', message: 'could not write ' + workRel }
|
|
3544
|
+
const prevA = formalOf(target)
|
|
3545
|
+
await putFormal(target, Object.assign({}, prevA, {
|
|
3546
|
+
status: prevA.status === 'passed' ? 'passed' : 'attempted',
|
|
3547
|
+
file: workRel,
|
|
3548
|
+
decision: 'used',
|
|
3549
|
+
note: String(args.note || prevA.note || ''),
|
|
3550
|
+
async: { jobId: jobId, state: 'queued', attempts: 0, enqueuedAt: now(), startedAt: 0, settledAt: 0, exitCode: null, buildSha256: fp },
|
|
3551
|
+
updatedAt: now(),
|
|
3552
|
+
}))
|
|
3553
|
+
await upsertFormalAnchor(target)
|
|
3554
|
+
await writeFormalIndex()
|
|
3555
|
+
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 } })
|
|
3556
|
+
return leanAsyncReturn(job, {
|
|
3557
|
+
kind: kind, target: target, file: workRel, status: 'attempted', sha256: sha, buildSha256: fp,
|
|
3558
|
+
note: '工作文件已写入 ' + workRel + ';编译已入队。**只有该作业在同一个构建上下文下落地为通过,才会写入 Verified/Lean/' + target + '.lean 并把状态变为 passed**——在它落地之前,这个对象不是"已通过形式化"。',
|
|
3559
|
+
})
|
|
3560
|
+
}
|
|
2891
3561
|
// 用绝对路径写:`writeText` 写失败时只会抛(由工具包装层变成通用 error),
|
|
2892
3562
|
// 这一行的 V3_WRITE_FAILED 分支就永远不可能触发;writeTextAbs 会如实返回 false。
|
|
2893
3563
|
if (!await writeTextAbs(frameworkRoot() + '/' + workRel, body)) return { ok: false, code: 'V3_WRITE_FAILED', message: 'could not write ' + workRel }
|
|
@@ -3289,13 +3959,493 @@ export function apply(ctx) {
|
|
|
3289
3959
|
return best
|
|
3290
3960
|
}
|
|
3291
3961
|
|
|
3962
|
+
// ==========================================================================================
|
|
3963
|
+
// 最终论文(规格:docs/final-paper.md)—— 会话侧(v3 数据来源:md 知识库)。
|
|
3964
|
+
//
|
|
3965
|
+
// 时序(修订 §A2/§A3):v3 的收口信号是 checkTermination() 的完整收口分支(无未解决问题、
|
|
3966
|
+
// `leftoverVerify` 为空——**这才是真实完整性判据**(§A4)、无真实在途代理、无任务/计划/挂起门)。
|
|
3967
|
+
// 撰写子代理必须在 `scheduler.running=false` 与 `releaseProjectLock()` **之前**派遣
|
|
3968
|
+
// (停止后 scheduleTick() 是空操作;提前释放项目锁后另一会话可能写同一棵树)。派不出去
|
|
3969
|
+
// (宿主激活上限 ACTIVATION_LIMIT_REACHED)→ 排队 + apply 级心跳重试 + 可见告警。
|
|
3970
|
+
// 落盘(§C/§D):md/tex 是文本;**paper.pdf 只能由编译器子进程生成**、插件只 stat 且**永不删除/覆盖**;
|
|
3971
|
+
// paper.meta.json 最后写(原子提交点);幂等 = run id + finalizedAt + 逐产物存在性(不用稳定哈希)。
|
|
3972
|
+
// ==========================================================================================
|
|
3973
|
+
let paperInFlight = '' // 正在撰写的 childId
|
|
3974
|
+
let paperInFlightAt = 0 // 派遣时刻(判定卡死窗口;force 与心跳都据它降级)
|
|
3975
|
+
let paperReaps = 0 // 本 run 内已回收卡死撰写者的次数(自动重派上限)
|
|
3976
|
+
const paperAbandoned = {} // childId -> true:已放弃的撰写者;它事后返回时输出被丢弃并留痕
|
|
3977
|
+
let paperPending = null // 激活上限排队重试:{ tries, retryAt, reason, limit, trigger, opts }
|
|
3978
|
+
const PAPER_RETRY_MS = 5000
|
|
3979
|
+
const PAPER_MAX_RETRIES = 12
|
|
3980
|
+
const PAPER_STALE_MS = 10 * 60 * 1000 // 撰写者卡死窗口:超过它,force 与心跳都会回收并重派(10 分钟)
|
|
3981
|
+
const PAPER_MAX_AUTO_REAPS = 2 // 心跳自动回收+重派的次数上限,之后交给 /vibe paper force
|
|
3982
|
+
const PAPER_LOCK_STALE_MS = 120000
|
|
3983
|
+
function activationLimitFrom(message) { const m = /active child limit:\s*(\d+)/.exec(String(message == null ? '' : message)); return m ? Number(m[1]) : undefined }
|
|
3984
|
+
function paperId(opts) { return paperDirId((opts && opts.id) || currentProject) }
|
|
3985
|
+
function paperDir(id) { return 'Paper/' + paperDirId(id) }
|
|
3986
|
+
function paperAbsDir(id) { return frameworkRoot() + '/' + paperDir(id) }
|
|
3987
|
+
function paperAuthorLine() { return 'Vibe Math V3(单作者:论文撰写子代理)· 项目 ' + currentProject }
|
|
3988
|
+
function paperOpts(opts) {
|
|
3989
|
+
const o = opts || {}
|
|
3990
|
+
const fmt = (o.format === 'md' || o.format === 'tex' || o.format === 'both') ? o.format : ((params.paperFormat === 'md' || params.paperFormat === 'tex') ? params.paperFormat : 'both')
|
|
3991
|
+
const lang = (o.lang === 'en' || o.lang === 'zh') ? o.lang : (params.paperLanguage === 'en' ? 'en' : 'zh')
|
|
3992
|
+
const compilePdf = (o.compilePdf === true || o.compilePdf === false) ? o.compilePdf : (params.paperCompilePdf !== false)
|
|
3993
|
+
return { format: fmt, lang: lang, compilePdf: compilePdf, force: o.force === true, id: o.id }
|
|
3994
|
+
}
|
|
3995
|
+
async function paperReadMeta(id) { const m = await readJson(paperDir(id) + '/paper.meta.json'); return (m && typeof m === 'object' && !Array.isArray(m)) ? m : undefined }
|
|
3996
|
+
async function paperPathExists(rel) { try { const t = await fsTarget(rel); return (await fs.stat(t)) !== undefined } catch (e) { return false } }
|
|
3997
|
+
async function paperAppendLog(id, tag, text) {
|
|
3998
|
+
const rel = paperDir(id) + '/paper.log.md'
|
|
3999
|
+
const prev = await readText(rel)
|
|
4000
|
+
const head = (prev === undefined || !String(prev).trim()) ? (paperLogHead(id).join('\n') + '\n') : String(prev)
|
|
4001
|
+
await writeText(rel, head + paperLogLine(now(), tag, text) + '\n')
|
|
4002
|
+
}
|
|
4003
|
+
/** paper 作用域锁(修订 §A3):v3 在收口时已释放项目锁,用它防止两个会话同时写同一棵 Paper/ 树。 */
|
|
4004
|
+
async function paperLockAcquire(id) {
|
|
4005
|
+
const rel = paperDir(id) + '/paper.lock.json'
|
|
4006
|
+
const cur = await readJson(rel)
|
|
4007
|
+
if (cur && cur.sessionId && cur.sessionId !== sessionId && (now() - Number(cur.at || 0)) < PAPER_LOCK_STALE_MS) return { ok: false, holder: String(cur.sessionId), at: Number(cur.at || 0) }
|
|
4008
|
+
await writeJson(rel, { sessionId: sessionId, at: now(), project: currentProject })
|
|
4009
|
+
return { ok: true }
|
|
4010
|
+
}
|
|
4011
|
+
async function paperLockRelease(id) {
|
|
4012
|
+
const rel = paperDir(id) + '/paper.lock.json'
|
|
4013
|
+
const cur = await readJson(rel)
|
|
4014
|
+
if (cur && cur.sessionId === sessionId) await writeJson(rel, { sessionId: '', at: now(), released: true })
|
|
4015
|
+
}
|
|
4016
|
+
/**
|
|
4017
|
+
* 回收卡死的撰写者(修订 §1:死掉的撰写者绝不能把论文永久卡住)。清状态 + 尽力中断 +
|
|
4018
|
+
* 从 agentRegistry 摘除(否则一直被算作活跃子代理)+ 记进 paperAbandoned(事后返回的输出被明确
|
|
4019
|
+
* 丢弃并留痕)+ 可见告警。
|
|
4020
|
+
*/
|
|
4021
|
+
async function paperReapWriter(id, why) {
|
|
4022
|
+
const cid = paperInFlight
|
|
4023
|
+
paperInFlight = ''; paperInFlightAt = 0
|
|
4024
|
+
if (cid) {
|
|
4025
|
+
try { await interruptChild(cid) } catch (e) { /* best effort */ }
|
|
4026
|
+
delete agentRegistry[cid]
|
|
4027
|
+
paperAbandoned[cid] = true
|
|
4028
|
+
}
|
|
4029
|
+
await paperAppendLog(id, 'reap', '回收卡死的撰写者 ' + (cid || '(未知)') + ':' + why + '(论文不会被永久卡住;force/心跳会继续)')
|
|
4030
|
+
logActivity('paper', '回收卡死的论文撰写子代理 ' + (cid || '(未知)') + ':' + why)
|
|
4031
|
+
console.error('vibe-math-v3: reaped a stalled paper writer (' + (cid || 'unknown') + '): ' + why)
|
|
4032
|
+
reportDirty = true
|
|
4033
|
+
return cid
|
|
4034
|
+
}
|
|
4035
|
+
function paperWriterVerdictNow(force) {
|
|
4036
|
+
return paperWriterVerdict({ inFlight: !!paperInFlight, force: !!force, ageMs: now() - Number(paperInFlightAt || 0), staleMs: PAPER_STALE_MS, childInRegistry: paperInFlight ? (agentRegistry[paperInFlight] !== undefined) : true })
|
|
4037
|
+
}
|
|
4038
|
+
function paperOneLine(s, cap) { return String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, cap || 400) }
|
|
4039
|
+
/** 证据索引(spec §3.9):只列**确实存在**的文件;排除 Paper/ 自己(避免自我污染)。
|
|
4040
|
+
* formalVerify=off 必须真的是无操作(formal-verify-v3 的语料守卫断言"off 的提示词里零 Lean 文本"),
|
|
4041
|
+
* 所以 off 档不把形式化路径写进材料。 */
|
|
4042
|
+
async function paperEvidenceIndex() {
|
|
4043
|
+
const formalNow = formalOn()
|
|
4044
|
+
const out = []
|
|
4045
|
+
if (formalNow && await paperPathExists('State/formal.json')) out.push('State/formal.json')
|
|
4046
|
+
if (await paperPathExists('State/index.json')) out.push('State/index.json')
|
|
4047
|
+
for (const p of allProblems()) out.push(problemRel(p))
|
|
4048
|
+
for (const p of allPropos()) out.push(propositionRel(p))
|
|
4049
|
+
for (const m of methods.values()) out.push(methodRel(m, false))
|
|
4050
|
+
const dirs = ['Verified/命题', 'Verified/问题', 'Logs/Verification', 'Logs/Plans', 'Progress'].concat(formalNow ? ['Verified/Lean', 'Formal'] : [])
|
|
4051
|
+
for (let i = 0; i < dirs.length; i++) { const files = await listFiles(dirs[i]); for (let j = 0; j < files.length; j++) out.push(dirs[i] + '/' + files[j]) }
|
|
4052
|
+
const seen = {}, uniq = []
|
|
4053
|
+
for (let i = 0; i < out.length; i++) { const x = String(out[i]); if (seen[x] || x.indexOf('Paper/') === 0) continue; seen[x] = true; uniq.push(x) }
|
|
4054
|
+
return uniq.sort()
|
|
4055
|
+
}
|
|
4056
|
+
/** 汇总材料(spec §4 + 修订 §A4):只含既有证据;未决项(含 leftoverVerify 的待验证对象)显式标注。 */
|
|
4057
|
+
async function buildPaperDigest() {
|
|
4058
|
+
const L = []
|
|
4059
|
+
L.push('PRESET: vibe-math-v3 (single-author)')
|
|
4060
|
+
L.push('PROJECT: ' + currentProject)
|
|
4061
|
+
const qs = allProblems()
|
|
4062
|
+
L.push('')
|
|
4063
|
+
L.push('[ORIGINAL PROBLEMS] (Problems/*.md)')
|
|
4064
|
+
if (qs.length === 0) L.push('- (none)')
|
|
4065
|
+
for (let i = 0; i < qs.length; i++) {
|
|
4066
|
+
const q = qs[i]
|
|
4067
|
+
L.push('- id=' + q.id + ' | 状态=' + q.状态 + ' | 优先级=' + q.优先级 + ' | 依赖=' + JSON.stringify(q.依赖 || []) + ' | 来源=' + (q.来源 || '原始') + ' | 陈述=' + paperOneLine(q.陈述, 800))
|
|
4068
|
+
const dirs = getDirState(q.id)
|
|
4069
|
+
if (dirs.length) L.push(' · 方向:' + dirs.map(function (d) { return d.id + ':' + d.status }).join(', '))
|
|
4070
|
+
const sols = q.solutions || []
|
|
4071
|
+
if (sols.length === 0) L.push(' · 解法:无')
|
|
4072
|
+
for (let j = 0; j < sols.length; j++) L.push(' · 解法#' + j + ' 概率=' + sols[j].prob + ' 状态=' + (sols[j].status || '') + ' 证据=Verified/问题/ 与 Logs/Verification/ | ' + paperOneLine(sols[j].text, 1200))
|
|
4073
|
+
}
|
|
4074
|
+
const ps = allPropos()
|
|
4075
|
+
L.push('')
|
|
4076
|
+
L.push('[PROPOSITIONS] (Propos/<分类>/<id>.md)')
|
|
4077
|
+
if (ps.length === 0) L.push('- (none)')
|
|
4078
|
+
for (let i = 0; i < ps.length; i++) {
|
|
4079
|
+
const p = ps[i]
|
|
4080
|
+
const rec = formalOn() ? formalOf(p.id) : null // off 档不把形式化字段写进材料(off 是真无操作)
|
|
4081
|
+
L.push('- id=' + p.id + ' | 概率=' + p.概率 + ' | 状态=' + p.状态 + ' | 优先级=' + p.优先级 + ' | 价值/关键性=' + p.价值关键性 + ' | 来源问题=' + (p.来源问题 || '') + (rec ? (' | 形式化=' + ((rec && rec.status) || 'none') + (rec && rec.proof ? ('(' + rec.proof + ')') : '')) : '') + ' | 陈述=' + paperOneLine(p.陈述, 800))
|
|
4082
|
+
const sides = [['证明', p.proofs || []], ['证伪', p.refutes || []]]
|
|
4083
|
+
for (let k = 0; k < sides.length; k++) { const arr = sides[k][1]; for (let j = 0; j < arr.length; j++) if (arr[j] && arr[j].prob === 1) L.push(' · 已检验通过:' + sides[k][0] + '#' + j + ' | ' + paperOneLine(arr[j].text, 900)) }
|
|
4084
|
+
}
|
|
4085
|
+
L.push('')
|
|
4086
|
+
L.push('[METHODS / ARTEFACTS] (Methods/ + 全局 VibeMath/Methods/)')
|
|
4087
|
+
const ms = Array.from(methods.values()).concat(Array.from(globalMethods.values()))
|
|
4088
|
+
if (ms.length === 0) L.push('- (none)')
|
|
4089
|
+
for (let i = 0; i < ms.length; i++) {
|
|
4090
|
+
const m = ms[i]
|
|
4091
|
+
L.push('- id=' + m.id + ' | 标题=' + m.标题 + ' | 类型=' + m.类型 + ' | 状态=' + m.状态 + ' | 可信断言=' + JSON.stringify(m.可信断言 || []) + ' | 应用次数=' + ((m.applications || []).length) + ' | 核心内容=' + paperOneLine(m.核心内容, 900))
|
|
4092
|
+
}
|
|
4093
|
+
// §A4:v3 的真实完整性判据是 leftoverVerify(buildVerifyCandidates)——把仍未验证的对象列出来。
|
|
4094
|
+
let cands = []
|
|
4095
|
+
try { cands = await buildVerifyCandidates() } catch (e) { /* 尽力而为 */ }
|
|
4096
|
+
L.push('')
|
|
4097
|
+
L.push('[STILL UNVERIFIED — v3 的完整性判据 leftoverVerify(buildVerifyCandidates);必须标注为未决]')
|
|
4098
|
+
if (cands.length === 0) L.push('- (none — 没有待验证对象)')
|
|
4099
|
+
for (let i = 0; i < cands.length; i++) L.push('- 未决:' + cands[i].rId + '(' + cands[i].kind + ',' + paperOneLine(cands[i].概述, 200) + (cands[i].prob === undefined ? '' : (',prob=' + cands[i].prob)) + ')')
|
|
4100
|
+
L.push('')
|
|
4101
|
+
L.push('[UNRESOLVED / REFUTED — 论文里必须显式标注,不得当成已成立的结论]')
|
|
4102
|
+
let any = false
|
|
4103
|
+
for (let i = 0; i < ps.length; i++) {
|
|
4104
|
+
if (!(ps[i].概率 === 1 || ps[i].概率 === 0)) { any = true; L.push('- 未定论:命题 ' + ps[i].id + '(概率=' + ps[i].概率 + ')') }
|
|
4105
|
+
else if (ps[i].概率 === 0) L.push('- 已被否证:命题 ' + ps[i].id)
|
|
4106
|
+
}
|
|
4107
|
+
for (let i = 0; i < qs.length; i++) {
|
|
4108
|
+
const sols = qs[i].solutions || []
|
|
4109
|
+
for (let j = 0; j < sols.length; j++) if (!(sols[j].prob === 1 || sols[j].prob === 0)) { any = true; L.push('- 未定论:问题 ' + qs[i].id + ' 的解法#' + j + '(概率=' + sols[j].prob + ')') }
|
|
4110
|
+
}
|
|
4111
|
+
if (!any && cands.length === 0) L.push('- (none — 所有对象均已定论)')
|
|
4112
|
+
L.push('')
|
|
4113
|
+
L.push('[EVIDENCE INDEX] (only files that exist)')
|
|
4114
|
+
const ev = await paperEvidenceIndex()
|
|
4115
|
+
for (let i = 0; i < ev.length; i++) L.push('- ' + ev[i])
|
|
4116
|
+
return L.join('\n')
|
|
4117
|
+
}
|
|
4118
|
+
/** 「论文撰写」子代理提示词(单作者;与 v2 同一形状/同一骨架)。 */
|
|
4119
|
+
function paperWriterPrompt(digest, o) {
|
|
4120
|
+
const langName = o.lang === 'en' ? 'English' : '中文(Chinese)'
|
|
4121
|
+
return 'You are the DEDICATED PAPER WRITER (single-author mode) of a math research run that has just CONVERGED.\n' +
|
|
4122
|
+
'Write its final paper in ' + langName + ', using ONLY the evidence in the MATERIAL section below.\n\n' +
|
|
4123
|
+
'HARD RULES:\n' +
|
|
4124
|
+
'- NEVER invent content: no new proposition, no new computation, no citation that is not in the MATERIAL.\n' +
|
|
4125
|
+
'- Unresolved or refuted items MUST be explicitly labelled (「未定论」/「已被否证」, or "unresolved"/"refuted" in English); never present them as established.\n' +
|
|
4126
|
+
'- Fixed 9-section skeleton — provide bodies for these EXACT `## ` headings (the framework writes the headings, author/date and the evidence index itself):\n' +
|
|
4127
|
+
PAPER_SKELETON.map(function (s, i) { return ' ' + (i + 1) + '. ' + s.key + ' — ' + s.spec }).join('\n') + '\n' +
|
|
4128
|
+
'- A section with no evidence must be exactly 「' + PAPER_NO_EVIDENCE + '」 (do not pad it).\n' +
|
|
4129
|
+
'- Markdown subset only: `#`/`##`/`###`, `- ` lists, `**bold**`, `*em*`, `` `code` ``, and inline math as `$...$`. No tables, images, footnotes or raw HTML.\n\n' +
|
|
4130
|
+
'OUTPUT CONTRACT — respond with ONLY one ```json code fence, no prose:\n' +
|
|
4131
|
+
'{"title":"<paper title>","abstract":"<original problem + main results>","sections":[{"name":"<one of the 9 headings>","body":"<markdown>"}, ...]}\n\n' +
|
|
4132
|
+
'MATERIAL (evidence only — do not add anything beyond it):\n' + digest
|
|
4133
|
+
}
|
|
4134
|
+
/** LaTeX 检测(spec §5 + 修订 §D):paperLatexCommand 非空时只用它;否则按语言顺序探测。 */
|
|
4135
|
+
async function paperDetectLatex(lang) {
|
|
4136
|
+
const sub = subprocessOf()
|
|
4137
|
+
if (sub === undefined || typeof sub.spawn !== 'function') return { available: [], reason: 'no-subprocess' }
|
|
4138
|
+
if (typeof sub.resolveExecutable !== 'function') return { available: [], reason: 'no-resolveExecutable' }
|
|
4139
|
+
const forced = String(params.paperLatexCommand || '').trim()
|
|
4140
|
+
if (forced) {
|
|
4141
|
+
try { const p = await sub.resolveExecutable(forced); return p ? { available: [{ name: forced, path: String(p) }], reason: 'ok', forced: true } : { available: [], reason: 'paperLatexCommand not found: ' + forced, forced: true } }
|
|
4142
|
+
catch (e) { return { available: [], reason: 'paperLatexCommand not found: ' + forced, forced: true } }
|
|
4143
|
+
}
|
|
4144
|
+
const order = lang === 'en' ? PAPER_ENGINE_ORDER_EN : PAPER_ENGINE_ORDER_ZH
|
|
4145
|
+
const available = []
|
|
4146
|
+
for (let i = 0; i < order.length; i++) {
|
|
4147
|
+
try { const p = await sub.resolveExecutable(order[i]); if (p) available.push({ name: order[i], path: String(p) }) } catch (e) { /* 未安装 */ }
|
|
4148
|
+
}
|
|
4149
|
+
return { available: available, reason: available.length ? 'ok' : 'none' }
|
|
4150
|
+
}
|
|
4151
|
+
function paperArgvFor(engine) {
|
|
4152
|
+
if (engine.name === 'latexmk') return [engine.path, '-pdf', '-interaction=nonstopmode', '-halt-on-error', 'paper.tex']
|
|
4153
|
+
if (engine.name === 'tectonic') return [engine.path, '--keep-logs', 'paper.tex']
|
|
4154
|
+
return [engine.path, '-interaction=nonstopmode', '-halt-on-error', 'paper.tex']
|
|
4155
|
+
}
|
|
4156
|
+
/** 一次编译(nonstopmode),带主动超时 terminate。 */
|
|
4157
|
+
async function paperRunOnce(engine, dirAbs, capMs) {
|
|
4158
|
+
const sub = subprocessOf()
|
|
4159
|
+
if (sub === undefined || typeof sub.spawn !== 'function') return { ok: false, exitCode: null, code: 'NO_SUBPROCESS' }
|
|
4160
|
+
let handle
|
|
4161
|
+
try { handle = await spawnHandle(sub, { argv: paperArgvFor(engine), cwd: dirAbs, stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } }, graceMs: capMs }) }
|
|
4162
|
+
catch (e) { return { ok: false, exitCode: null, code: 'SPAWN_FAILED', message: String((e && e.message) || e) } }
|
|
4163
|
+
let timedOut = false, timer = null, outcome
|
|
4164
|
+
try {
|
|
4165
|
+
outcome = await Promise.race([
|
|
4166
|
+
handle.done,
|
|
4167
|
+
new Promise(function (resolve) { timer = setTimeout(function () { timedOut = true; try { if (handle && typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* best effort */ } resolve({ exitCode: null }) }, capMs) }),
|
|
4168
|
+
])
|
|
4169
|
+
} catch (e) { if (timer !== null) clearTimeout(timer); return { ok: false, exitCode: null, code: 'RUN_FAILED', message: String((e && e.message) || e) } }
|
|
4170
|
+
if (timer !== null) clearTimeout(timer)
|
|
4171
|
+
let err = ''
|
|
4172
|
+
try { if (handle.collected && handle.collected.stderr) err = String(handle.collected.stderr.readFrom(0).text || '') } catch (e) { /* best effort */ }
|
|
4173
|
+
return { ok: !timedOut && !!outcome && outcome.exitCode === 0, exitCode: outcome ? outcome.exitCode : null, timedOut: timedOut, stderr: err.slice(-1200) }
|
|
4174
|
+
}
|
|
4175
|
+
/**
|
|
4176
|
+
* 编译(spec §5 + 修订 §D/§E):主引擎(nonstopmode,两遍)→ 换引擎 → 去不支持宏包 → 最小模板再试一次;
|
|
4177
|
+
* 然后报告并降级。**永不删除/覆盖已有 pdf**:成功判据 = 本次运行 exit 0 **且** pdf 存在。
|
|
4178
|
+
*/
|
|
4179
|
+
async function paperCompile(id, o, texPrimary, bodyTex) {
|
|
4180
|
+
const relDir = paperDir(id), absDir = paperAbsDir(id)
|
|
4181
|
+
const attempts = []
|
|
4182
|
+
if (!o.compilePdf) return { compile: 'skipped', reason: 'paperCompilePdf=false', engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
4183
|
+
if (o.format === 'md') return { compile: 'skipped', reason: 'paperFormat=md(不产出 tex,跳过编译)', engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
4184
|
+
const det = await paperDetectLatex(o.lang)
|
|
4185
|
+
if (det.available.length === 0) {
|
|
4186
|
+
await paperAppendLog(id, 'latex', '未检测到任何 LaTeX 引擎(' + det.reason + ')→ 只保留 paper.tex + paper.md,不编译 pdf(spec §5:不阻塞定稿)')
|
|
4187
|
+
return { compile: 'not-detected', reason: det.reason, engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
4188
|
+
}
|
|
4189
|
+
await paperAppendLog(id, 'latex', '检测到引擎 ' + det.available.map(function (x) { return x.name }).join(', ') + (det.forced ? '(paperLatexCommand 指定)' : ('(顺序 ' + (o.lang === 'en' ? PAPER_ENGINE_ORDER_EN : PAPER_ENGINE_ORDER_ZH).join(' > ') + ')')))
|
|
4190
|
+
const plan = [{ engine: det.available[0], tex: texPrimary, mode: 'primary' }]
|
|
4191
|
+
if (det.available[1]) plan.push({ engine: det.available[1], tex: texPrimary, mode: 'engine-fallback' })
|
|
4192
|
+
plan.push({ engine: det.available[0], tex: paperSanitizeTex(texPrimary), mode: 'drop-unsupported-packages' })
|
|
4193
|
+
plan.push({ engine: det.available[0], tex: paperMinimalTexDoc({ lang: o.lang, bodyTex: bodyTex }), mode: 'minimal-template' })
|
|
4194
|
+
const cap = 120000
|
|
4195
|
+
for (let i = 0; i < plan.length; i++) {
|
|
4196
|
+
const step = plan[i]
|
|
4197
|
+
await writeText(relDir + '/paper.tex', step.tex)
|
|
4198
|
+
const runs = []
|
|
4199
|
+
for (let pass = 0; pass < 2; pass++) runs.push(await paperRunOnce(step.engine, absDir, cap))
|
|
4200
|
+
const last = runs[runs.length - 1]
|
|
4201
|
+
const pdf = !!last.ok && await paperPathExists(relDir + '/paper.pdf')
|
|
4202
|
+
attempts.push({ engine: step.engine.name, mode: step.mode, ok: pdf, exitCode: last.exitCode, timedOut: !!last.timedOut, stderr: last.stderr || '' })
|
|
4203
|
+
await paperAppendLog(id, 'compile', step.mode + ' engine=' + step.engine.name + ' → ' + (pdf ? 'ok(paper.pdf 已生成)' : ('failed(exit=' + String(last.exitCode) + (last.timedOut ? ', timeout' : '') + ')')))
|
|
4204
|
+
if (pdf) return { compile: (i === 0) ? 'ok' : 'repaired', engine: step.engine.name, mode: step.mode, attempts: attempts, pdfPreserved: false }
|
|
4205
|
+
}
|
|
4206
|
+
await writeText(relDir + '/paper.tex', texPrimary)
|
|
4207
|
+
const preserved = await paperPathExists(relDir + '/paper.pdf')
|
|
4208
|
+
await paperAppendLog(id, 'compile', '所有尝试均失败:保留 paper.tex + paper.md' + (preserved ? '(paper.pdf 是**上一次成功编译**留下的,未被覆盖)' : ',未生成 paper.pdf') + '(不阻塞定稿)')
|
|
4209
|
+
logActivity('paper', '论文 pdf 编译失败(已尝试换引擎/去不支持宏包/最小模板),已保留 tex+md:' + relDir)
|
|
4210
|
+
reportDirty = true
|
|
4211
|
+
return { compile: 'failed', engine: det.available[0].name, attempts: attempts, pdfPreserved: preserved }
|
|
4212
|
+
}
|
|
4213
|
+
/** 落盘 md/tex/pdf/meta(meta **最后**写 = 原子提交点;spec §2/§5)。 */
|
|
4214
|
+
async function paperEmitArtifacts(id, o, info) {
|
|
4215
|
+
const relDir = paperDir(id)
|
|
4216
|
+
const dateStr = fmtTime(now()).slice(0, 10)
|
|
4217
|
+
const author = paperAuthorLine()
|
|
4218
|
+
const evidence = info.evidence || (await paperEvidenceIndex())
|
|
4219
|
+
const title = String(info.title || '').trim() || ('研究报告:' + currentProject)
|
|
4220
|
+
const md = paperBuildMarkdown({ title: title, author: author, date: dateStr, abstract: info.abstract, sections: info.sections, evidence: evidence })
|
|
4221
|
+
const artifacts = { md: false, tex: false, pdf: false }
|
|
4222
|
+
let compile = 'skipped', engine = null, attempts = [], pdfPreserved = false
|
|
4223
|
+
if (o.format === 'md' || o.format === 'both') { await writeText(relDir + '/paper.md', md); artifacts.md = true }
|
|
4224
|
+
if (o.format === 'tex' || o.format === 'both') {
|
|
4225
|
+
const bodyTex = paperMdToTexBody(md)
|
|
4226
|
+
const tex = paperTexDoc({ title: title, author: author, date: dateStr, lang: o.lang, bodyTex: bodyTex })
|
|
4227
|
+
await writeText(relDir + '/paper.tex', tex)
|
|
4228
|
+
artifacts.tex = true
|
|
4229
|
+
const c = await paperCompile(id, o, tex, bodyTex)
|
|
4230
|
+
compile = c.compile; engine = c.engine || null; attempts = c.attempts || []
|
|
4231
|
+
pdfPreserved = !!c.pdfPreserved
|
|
4232
|
+
if (compile === 'ok' || compile === 'repaired') artifacts.pdf = await paperPathExists(relDir + '/paper.pdf')
|
|
4233
|
+
}
|
|
4234
|
+
const filled = PAPER_SKELETON.map(function (s) { return s.key }).filter(function (k) { return !!(info.sections && String(info.sections[k] || '').trim()) })
|
|
4235
|
+
const meta = {
|
|
4236
|
+
id: id, dir: relDir, preset: 'vibe-math-v3', singleAuthor: true, project: currentProject,
|
|
4237
|
+
title: title, author: author, date: dateStr, abstract: String(info.abstract || ''),
|
|
4238
|
+
sectionBodies: info.sections || {}, sections: PAPER_SKELETON.map(function (s) { return s.key }), filledSections: filled,
|
|
4239
|
+
raw: String(info.raw == null ? '' : info.raw), notes: Array.isArray(info.notes) ? info.notes : [],
|
|
4240
|
+
trigger: info.trigger || 'manual', runStartedAt: Number(scheduler.startedAt) || 0,
|
|
4241
|
+
params: { finalPaper: params.finalPaper !== false, paperFormat: o.format, paperLanguage: o.lang, paperCompilePdf: !!o.compilePdf, paperLatexCommand: String(params.paperLatexCommand || '') },
|
|
4242
|
+
finalizedAt: now(), finalizedAtText: fmtTime(now()),
|
|
4243
|
+
artifacts: artifacts, compile: compile, compileEngine: engine, compileAttempts: attempts, pdfPreserved: pdfPreserved,
|
|
4244
|
+
material: info.material || null, evidence: evidence,
|
|
4245
|
+
}
|
|
4246
|
+
await writeJson(relDir + '/paper.meta.json', meta)
|
|
4247
|
+
return meta
|
|
4248
|
+
}
|
|
4249
|
+
async function paperMissingArtifacts(id, o) {
|
|
4250
|
+
const relDir = paperDir(id)
|
|
4251
|
+
const need = []
|
|
4252
|
+
if (o.format === 'md' || o.format === 'both') { if (!(await paperPathExists(relDir + '/paper.md'))) need.push('paper.md') }
|
|
4253
|
+
if (o.format === 'tex' || o.format === 'both') { if (!(await paperPathExists(relDir + '/paper.tex'))) need.push('paper.tex') }
|
|
4254
|
+
return need
|
|
4255
|
+
}
|
|
4256
|
+
/** 派遣(内部):激活上限拒绝 → 排队重试(修订 §A2)。 */
|
|
4257
|
+
async function paperDispatch(trigger, o, id, prevTries) {
|
|
4258
|
+
const relDir = paperDir(id)
|
|
4259
|
+
const digest = await buildPaperDigest()
|
|
4260
|
+
let childId
|
|
4261
|
+
try {
|
|
4262
|
+
childId = await spawnChild('paper-writer:' + id, paperWriterPrompt(digest, o), { role: 'paper', paperId: id, trigger: trigger, lang: o.lang, format: o.format, compilePdf: o.compilePdf })
|
|
4263
|
+
} catch (e) {
|
|
4264
|
+
const message = String((e && e.message) || e)
|
|
4265
|
+
const limit = activationLimitFrom(message)
|
|
4266
|
+
if (limit !== undefined || /ACTIVATION_LIMIT_REACHED|active child limit/i.test(message)) {
|
|
4267
|
+
const tries = Number(prevTries || 0) + 1
|
|
4268
|
+
if (tries > PAPER_MAX_RETRIES) {
|
|
4269
|
+
paperPending = null
|
|
4270
|
+
await paperAppendLog(id, 'dispatch-failed', '宿主激活上限连续拒绝 ' + PAPER_MAX_RETRIES + ' 次,已放弃自动派遣:' + message)
|
|
4271
|
+
logActivity('paper', '论文撰写子代理被宿主激活上限连续拒绝(' + PAPER_MAX_RETRIES + ' 次),已放弃自动派遣(稍后可 /vibe paper force 重试):' + message)
|
|
4272
|
+
console.error('vibe-math-v3: paper writer refused by the host activation limit ' + PAPER_MAX_RETRIES + ' times; giving up (retry later with /vibe paper force) — ' + message)
|
|
4273
|
+
await paperLockRelease(id)
|
|
4274
|
+
return { ok: false, message: 'paper writer refused by the activation limit ' + PAPER_MAX_RETRIES + ' times: ' + message, id: id, dir: relDir, limit: limit }
|
|
4275
|
+
}
|
|
4276
|
+
paperPending = { tries: tries, retryAt: now() + PAPER_RETRY_MS, reason: message, limit: limit, trigger: trigger, opts: { lang: o.lang, format: o.format, compilePdf: o.compilePdf } }
|
|
4277
|
+
if (tries === 1 || tries % 3 === 0) {
|
|
4278
|
+
await paperAppendLog(id, 'dispatch-queued', '宿主激活上限(' + (limit === undefined ? '?' : limit) + ')拒绝派遣(第 ' + tries + ' 次):' + (PAPER_RETRY_MS / 1000) + 's 后重试')
|
|
4279
|
+
logActivity('paper', '论文撰写子代理被宿主激活上限拒绝(第 ' + tries + ' 次),已排队重试:' + message)
|
|
4280
|
+
console.error('vibe-math-v3: paper writer queued after an activation-limit refusal (try ' + tries + '): ' + message)
|
|
4281
|
+
}
|
|
4282
|
+
reportDirty = true
|
|
4283
|
+
return { ok: true, queued: true, reason: 'activation-limit-reached', tries: tries, retryAt: paperPending.retryAt, limit: limit, id: id, dir: relDir }
|
|
4284
|
+
}
|
|
4285
|
+
await paperAppendLog(id, 'dispatch-failed', message)
|
|
4286
|
+
logActivity('paper', '「论文撰写」子代理派遣失败:' + message)
|
|
4287
|
+
await paperLockRelease(id)
|
|
4288
|
+
return { ok: false, message: 'paper writer dispatch failed: ' + message, id: id, dir: relDir }
|
|
4289
|
+
}
|
|
4290
|
+
paperInFlight = childId
|
|
4291
|
+
paperInFlightAt = now()
|
|
4292
|
+
// 只有"非自动重试"的派遣才重置回收计数:auto-retry 必须保留计数,否则卡死重派会无限循环。
|
|
4293
|
+
if (trigger !== 'auto-retry') paperReaps = 0
|
|
4294
|
+
paperPending = null
|
|
4295
|
+
const material = paperMaterialSummary(digest)
|
|
4296
|
+
await paperAppendLog(id, 'dispatch', 'trigger=' + trigger + ' child=' + childId + ' format=' + o.format + ' lang=' + o.lang + ' compilePdf=' + o.compilePdf + (o.force ? ' force=true' : '') + ' run=' + (Number(scheduler.startedAt) || 0) + ' material=' + material.chars + ' chars/' + material.lines + ' lines')
|
|
4297
|
+
logActivity('paper', '已派遣「论文撰写」子代理(' + trigger + ',' + o.format + '/' + o.lang + ')→ ' + relDir)
|
|
4298
|
+
reportDirty = true
|
|
4299
|
+
return { ok: true, dispatched: true, childId: childId, id: id, dir: relDir, trigger: trigger, format: o.format, lang: o.lang, compilePdf: o.compilePdf }
|
|
4300
|
+
}
|
|
4301
|
+
/** 触发入口(spec §2/§3 + 修订 §A2/§A3):自动(收口)与手动命令共用。 */
|
|
4302
|
+
async function maybeWritePaper(trigger, opts) {
|
|
4303
|
+
const o = paperOpts(opts)
|
|
4304
|
+
const id = paperId(opts)
|
|
4305
|
+
const relDir = paperDir(id)
|
|
4306
|
+
const meta = await paperReadMeta(id)
|
|
4307
|
+
const sameRun = !!meta && Number(meta.runStartedAt || 0) === (Number(scheduler.startedAt) || 0)
|
|
4308
|
+
if (!o.force && meta && meta.finalizedAt && sameRun) {
|
|
4309
|
+
const missing = await paperMissingArtifacts(id, o)
|
|
4310
|
+
if (missing.length === 0) {
|
|
4311
|
+
await paperAppendLog(id, 'skip', '幂等:同一 run(' + (Number(scheduler.startedAt) || 0) + ')已有定稿且产物齐全 → 不重复撰写(/vibe paper force 可重写)')
|
|
4312
|
+
return { ok: true, skipped: true, reason: 'already-finalized-this-run', id: id, dir: relDir, finalizedAt: meta.finalizedAt, artifacts: meta.artifacts || {}, compile: meta.compile, note: '同一 run 只写一次;重复触发只补写缺失产物(spec §2)' }
|
|
4313
|
+
}
|
|
4314
|
+
await paperAppendLog(id, 'fill', '幂等:已有定稿但缺少 ' + missing.join(', ') + ' → 用已存的定稿内容补写(不重新派遣作者)')
|
|
4315
|
+
const filled = await paperEmitArtifacts(id, o, { title: meta.title, abstract: meta.abstract, sections: meta.sectionBodies || {}, evidence: meta.evidence, raw: meta.raw, notes: (meta.notes || []).concat(['补写缺失产物:' + missing.join(', ')]), trigger: meta.trigger || trigger, material: meta.material })
|
|
4316
|
+
return { ok: true, skipped: true, reason: 'filled-missing-artifacts', filled: missing, id: id, dir: relDir, finalizedAt: filled.finalizedAt, artifacts: filled.artifacts, compile: filled.compile }
|
|
4317
|
+
}
|
|
4318
|
+
if (paperInFlight) {
|
|
4319
|
+
const verdict = paperWriterVerdictNow(o.force)
|
|
4320
|
+
if (verdict === 'reap') {
|
|
4321
|
+
const gone = agentRegistry[paperInFlight] === undefined
|
|
4322
|
+
await paperReapWriter(id, gone ? '子代理已不在 agentRegistry(end 事件丢失 / 宿主丢弃)' : ('超过卡死窗口 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟未返回'))
|
|
4323
|
+
} else if (verdict === 'refuse') {
|
|
4324
|
+
const ageMs = now() - Number(paperInFlightAt || 0)
|
|
4325
|
+
await paperAppendLog(id, 'force-refused', '仍有在途撰写者 ' + paperInFlight + '(' + Math.round(ageMs / 1000) + 's):超过 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟才会被 force 放弃')
|
|
4326
|
+
return { ok: false, reason: 'writer-in-flight', childId: paperInFlight, ageMs: ageMs, staleMs: PAPER_STALE_MS, id: id, dir: relDir, message: '已有在途的论文撰写子代理 ' + paperInFlight + '(已 ' + Math.round(ageMs / 1000) + 's 未返回)。等它返回,或等超过 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟后 force 会自动回收并重派(心跳也会自动回收)。' }
|
|
4327
|
+
} else {
|
|
4328
|
+
return { ok: true, skipped: true, reason: 'writer-in-flight', childId: paperInFlight, ageMs: now() - Number(paperInFlightAt || 0), staleMs: PAPER_STALE_MS, id: id, dir: relDir }
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
if (paperPending) return { ok: true, queued: true, reason: 'retry-queued', retryAt: paperPending.retryAt, tries: paperPending.tries, id: id, dir: relDir }
|
|
4332
|
+
const lock = await paperLockAcquire(id)
|
|
4333
|
+
if (!lock.ok) {
|
|
4334
|
+
await paperAppendLog(id, 'skip', 'paper 锁由会话 ' + lock.holder + ' 持有(未过期)→ 本次不派遣(修订 §A3 的 paper 作用域锁)')
|
|
4335
|
+
return { ok: true, skipped: true, reason: 'locked-by-another-session', holder: lock.holder, id: id, dir: relDir }
|
|
4336
|
+
}
|
|
4337
|
+
return await paperDispatch(trigger, o, id, 0)
|
|
4338
|
+
}
|
|
4339
|
+
function paperRetryDue() { return !!(paperPending && now() >= Number(paperPending.retryAt || 0)) }
|
|
4340
|
+
/**
|
|
4341
|
+
* 心跳(修订 §1):在途 → 续租 paper 锁;**卡死则自动回收**(child 不在注册表,或超过卡死窗口),
|
|
4342
|
+
* 并自动重派(上限 PAPER_MAX_AUTO_REAPS,之后给出可执行提示交给 /vibe paper force);有排队则重试派遣。
|
|
4343
|
+
*/
|
|
4344
|
+
async function runPaperRetry() {
|
|
4345
|
+
const id = paperId()
|
|
4346
|
+
if (paperInFlight) {
|
|
4347
|
+
if (paperWriterVerdictNow(false) === 'reap') {
|
|
4348
|
+
const gone = agentRegistry[paperInFlight] === undefined
|
|
4349
|
+
await paperReapWriter(id, gone ? '子代理已不在 agentRegistry(end 事件丢失 / 宿主丢弃)' : ('超过卡死窗口 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟未返回'))
|
|
4350
|
+
const meta = await paperReadMeta(id)
|
|
4351
|
+
if (params.finalPaper !== false && !(meta && meta.finalizedAt) && Number(paperReaps || 0) < PAPER_MAX_AUTO_REAPS) {
|
|
4352
|
+
paperReaps = Number(paperReaps || 0) + 1
|
|
4353
|
+
await paperAppendLog(id, 'retry-after-reap', '第 ' + paperReaps + ' 次自动重派(上限 ' + PAPER_MAX_AUTO_REAPS + ')')
|
|
4354
|
+
await paperDispatch('auto-retry', paperOpts({}), id, 0)
|
|
4355
|
+
} else {
|
|
4356
|
+
await paperAppendLog(id, 'reap-giveup', '自动重派已达上限 ' + PAPER_MAX_AUTO_REAPS + '(或已有定稿):不再自动派遣,请用 /vibe paper force 手动重写')
|
|
4357
|
+
logActivity('paper', '论文撰写子代理连续卡死/已放弃:不再自动重派,可用 /vibe paper force 手动重写')
|
|
4358
|
+
}
|
|
4359
|
+
return
|
|
4360
|
+
}
|
|
4361
|
+
try { await paperLockAcquire(id) } catch (e) { /* 续租失败不致命 */ }
|
|
4362
|
+
return
|
|
4363
|
+
}
|
|
4364
|
+
if (!paperRetryDue()) return
|
|
4365
|
+
const p = paperPending
|
|
4366
|
+
paperPending = null
|
|
4367
|
+
const o = paperOpts(p.opts)
|
|
4368
|
+
await paperDispatch(p.trigger || 'auto-retry', o, id, Number(p.tries || 0))
|
|
4369
|
+
}
|
|
4370
|
+
/** `/vibe paper [lang=zh|en] [format=both|md|tex] [force]`(spec §2/§6)。 */
|
|
4371
|
+
async function paperCommand(args) {
|
|
4372
|
+
const rest = Array.isArray(args) ? args : []
|
|
4373
|
+
const opts = {}
|
|
4374
|
+
for (let i = 0; i < rest.length; i++) {
|
|
4375
|
+
const a = String(rest[i] || '')
|
|
4376
|
+
if (a === 'force') { opts.force = true; continue }
|
|
4377
|
+
const eq = a.indexOf('=')
|
|
4378
|
+
const k = eq > 0 ? a.slice(0, eq).toLowerCase() : ''
|
|
4379
|
+
const v = eq > 0 ? a.slice(eq + 1).toLowerCase() : ''
|
|
4380
|
+
if (k === 'lang') { if (v !== 'zh' && v !== 'en') return { ok: false, message: '/vibe paper lang= 只接受 zh|en(收到 ' + JSON.stringify(a) + ')' }; opts.lang = v; continue }
|
|
4381
|
+
if (k === 'format') { if (v !== 'both' && v !== 'md' && v !== 'tex') return { ok: false, message: '/vibe paper format= 只接受 both|md|tex(收到 ' + JSON.stringify(a) + ')' }; opts.format = v; continue }
|
|
4382
|
+
return { ok: false, message: 'unknown /vibe paper option: ' + a + '(可用 lang=zh|en、format=both|md|tex、force)' }
|
|
4383
|
+
}
|
|
4384
|
+
const r = await maybeWritePaper('manual', opts)
|
|
4385
|
+
if (r.ok === false) return r
|
|
4386
|
+
const autoNote = params.finalPaper === false ? '(自动触发已关闭 finalPaper=false;手动命令仍可用)' : ''
|
|
4387
|
+
const msg = r.dispatched ? ('已派遣「论文撰写」子代理,产物将写入 ' + r.dir + '/')
|
|
4388
|
+
: r.queued ? ('宿主激活上限已满:论文撰写已排队,' + (PAPER_RETRY_MS / 1000) + 's 后自动重试(第 ' + r.tries + ' 次)')
|
|
4389
|
+
: (r.reason === 'already-finalized-this-run' ? '同一 run 已有定稿且产物齐全:不重复撰写(需要重写请用 /vibe paper force)'
|
|
4390
|
+
: (r.reason === 'filled-missing-artifacts' ? ('已用既有定稿补写缺失产物:' + (r.filled || []).join(', ')) : ('未重新派遣:' + (r.reason || ''))))
|
|
4391
|
+
return Object.assign({}, r, { message: msg + autoNote, autoDisabled: params.finalPaper === false })
|
|
4392
|
+
}
|
|
4393
|
+
/** 论文撰写子代理结束(spec §4):解析回复 → 组装 9 节 → 落盘 → 编译 → 记日志 → 释放 paper 锁。 */
|
|
4394
|
+
async function handlePaperWriter(childId, meta, output) {
|
|
4395
|
+
const id = meta.paperId || paperId()
|
|
4396
|
+
paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0
|
|
4397
|
+
delete paperAbandoned[childId]
|
|
4398
|
+
const o = paperOpts({ lang: meta.lang, format: meta.format, compilePdf: meta.compilePdf })
|
|
4399
|
+
const parsed = parseJson(output)
|
|
4400
|
+
const info = paperSkeletonFromReply(parsed, output)
|
|
4401
|
+
if (!info.title) info.title = paperTitleFromMd(String(output || '')) || ('研究报告:' + currentProject)
|
|
4402
|
+
for (let i = 0; i < info.notes.length; i++) await paperAppendLog(id, 'note', info.notes[i])
|
|
4403
|
+
if (!String(output || '').trim()) await paperAppendLog(id, 'reply', '作者回复为空:只写骨架 + 占位说明(绝不编造内容)')
|
|
4404
|
+
let written
|
|
4405
|
+
try {
|
|
4406
|
+
written = await paperEmitArtifacts(id, o, { title: info.title, abstract: info.abstract, sections: info.sections, raw: String(output || ''), notes: info.notes, trigger: meta.trigger || 'auto', material: paperMaterialSummary(await buildPaperDigest()) })
|
|
4407
|
+
} catch (e) {
|
|
4408
|
+
await paperAppendLog(id, 'finalize-failed', String((e && e.message) || e))
|
|
4409
|
+
logActivity('paper', '论文落盘失败:' + String((e && e.message) || e))
|
|
4410
|
+
await paperLockRelease(id)
|
|
4411
|
+
return
|
|
4412
|
+
}
|
|
4413
|
+
await paperAppendLog(id, 'finalize', '产物 ' + JSON.stringify(written.artifacts) + ' compile=' + written.compile + (written.compileEngine ? (' engine=' + written.compileEngine) : '') + ' run=' + (Number(scheduler.startedAt) || 0))
|
|
4414
|
+
logActivity('paper', '论文(最终稿)已落盘:' + paperDir(id) + '/(' + Object.keys(written.artifacts).filter(function (k) { return written.artifacts[k] }).join('/') + ',编译=' + written.compile + ')')
|
|
4415
|
+
await paperLockRelease(id)
|
|
4416
|
+
reportDirty = true
|
|
4417
|
+
await saveAll()
|
|
4418
|
+
}
|
|
4419
|
+
/** status 里的论文视图(spec §2/§6)。 */
|
|
4420
|
+
async function paperStatusView() {
|
|
4421
|
+
const id = paperId()
|
|
4422
|
+
const meta = await paperReadMeta(id)
|
|
4423
|
+
return {
|
|
4424
|
+
id: id, dir: paperDir(id), inFlight: paperInFlight || null,
|
|
4425
|
+
inFlightSince: paperInFlight ? Number(paperInFlightAt || 0) : null,
|
|
4426
|
+
inFlightAgeMs: paperInFlight ? (now() - Number(paperInFlightAt || 0)) : null,
|
|
4427
|
+
staleMs: PAPER_STALE_MS, reapedThisRun: Number(paperReaps || 0),
|
|
4428
|
+
queued: paperPending ? { tries: paperPending.tries, retryAt: paperPending.retryAt, reason: paperPending.reason, limit: paperPending.limit === undefined ? null : paperPending.limit } : null,
|
|
4429
|
+
autoFinalPaper: params.finalPaper !== false, format: params.paperFormat, language: params.paperLanguage, compilePdf: params.paperCompilePdf !== false,
|
|
4430
|
+
finalizedAt: meta ? meta.finalizedAt : null, artifacts: meta ? (meta.artifacts || {}) : {}, compile: meta ? meta.compile : null,
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
|
|
3292
4434
|
// ================= child result dispatch =================
|
|
3293
4435
|
async function onChildEnd(info) {
|
|
3294
4436
|
// 子代理结束/中断:自动释放它持有的所有写锁(防锁残留导致文件被永久锁住)
|
|
3295
4437
|
const endedId = String(info.id)
|
|
3296
4438
|
for (const k of Object.keys(fileOwner)) { if (String(fileOwner[k].childId) === endedId) delete fileOwner[k] }
|
|
3297
4439
|
const meta = agentRegistry[info.id]
|
|
3298
|
-
if (meta === undefined)
|
|
4440
|
+
if (meta === undefined) {
|
|
4441
|
+
// 已回收的撰写者事后才返回:输出被丢弃,但必须**留痕**,不能静默消失(修订 §1)。
|
|
4442
|
+
if (paperAbandoned[info.id]) {
|
|
4443
|
+
delete paperAbandoned[info.id]
|
|
4444
|
+
logActivity('paper', '已回收的论文撰写子代理 ' + info.id + ' 事后返回:其输出被丢弃(已重派新的撰写者)')
|
|
4445
|
+
reportDirty = true
|
|
4446
|
+
}
|
|
4447
|
+
return
|
|
4448
|
+
}
|
|
3299
4449
|
const output = blocksToText(info.lastAssistantMessage)
|
|
3300
4450
|
try {
|
|
3301
4451
|
if (meta.role === 'explorer') await handleExplorer(info.id, meta, output)
|
|
@@ -3303,6 +4453,9 @@ export function apply(ctx) {
|
|
|
3303
4453
|
else if (meta.role === 'verifier') await handleVerifier(info.id, meta, output, info.stopReason)
|
|
3304
4454
|
else if (meta.role === 'planner') await handlePlanner(info.id, meta, output)
|
|
3305
4455
|
else if (meta.role === 'method-keeper') await handleMethodKeeper(info.id, meta, output)
|
|
4456
|
+
// 修订 §A2:论文撰写子代理必须有**自己的分支**——未知 role 的产出会被静默丢弃。
|
|
4457
|
+
else if (meta.role === 'paper') await handlePaperWriter(info.id, meta, output)
|
|
4458
|
+
else console.error('vibe-math-v3: subagent ' + info.id + ' ended with an unknown role "' + String(meta.role) + '" — its output is dropped')
|
|
3306
4459
|
} catch (e) { console.error('vibe-math-v3 onChildEnd error: ' + String((e && e.stack) || e)) }
|
|
3307
4460
|
await saveAll()
|
|
3308
4461
|
await maybePromoteMethods()
|
|
@@ -3321,9 +4474,17 @@ export function apply(ctx) {
|
|
|
3321
4474
|
if (Object.keys(agentRegistry).length > 0 || Object.keys(tasks).length > 0) {
|
|
3322
4475
|
logActivity(fresh ? 'start' : 'resume', 'cleared ' + Object.keys(agentRegistry).length + ' agent(s) and ' + Object.keys(tasks).length + ' task(s) (' + (fresh ? 'restart' : 'stale from previous process') + ')')
|
|
3323
4476
|
agentRegistry = {}; tasks = {}
|
|
4477
|
+
// 论文派遣态是本进程内存态:子代理已被中断/丢弃,排队与在途标记必须一起清掉(修订 §A2)。
|
|
4478
|
+
if (paperInFlight || paperPending) logActivity('paper', '重启/跨进程恢复:清空论文撰写态(inFlight=' + (paperInFlight || '-') + (paperPending ? ', queued' : '') + ')')
|
|
4479
|
+
paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
|
|
3324
4480
|
}
|
|
3325
4481
|
}
|
|
3326
4482
|
await writeJson('State/process_epoch.json', processEpoch)
|
|
4483
|
+
// Lean 异步作业的崩溃恢复(spec §2.6):同一进程 continue 不重驱(内存队列还在跑),
|
|
4484
|
+
// 只有新进程/新会话接手(fresh 或 stale epoch)才按 Formal/Jobs/*.json 恢复;恢复绝不置 passed。
|
|
4485
|
+
if (fresh || stale) { try { await recoverLeanJobs() } catch (e) { console.error('vibe-math-v3: lean job recovery failed: ' + String((e && e.message) || e)) } }
|
|
4486
|
+
// 数学计算的引擎探测(异步)在 init 时先跑一次:提示词侧只读缓存现算(mathWorkLine)。
|
|
4487
|
+
try { await refreshMathProbe(true) } catch (e) { /* 探测失败不影响 init:mathWorkLine 会因此为空 */ }
|
|
3327
4488
|
await saveAll()
|
|
3328
4489
|
if (params.indexAutoRebuild) await rebuildIndex()
|
|
3329
4490
|
return { ok: true }
|
|
@@ -3376,7 +4537,7 @@ export function apply(ctx) {
|
|
|
3376
4537
|
async function startScheduler(override) { const r = await init(true); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject + '(v3:md 知识库 + 规划代理调度 + 方法库)'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3377
4538
|
async function resumeScheduler(override) { const r = await init(false); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3378
4539
|
async function pauseScheduler() { scheduler.running = false; await releaseProjectLock(); logActivity('pause', 'scheduler paused'); await saveAll(); return { ok: true, message: 'scheduler paused' } }
|
|
3379
|
-
async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; planQueue = []; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
|
|
4540
|
+
async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; planQueue = []; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
|
|
3380
4541
|
async function autoResolvePending() {
|
|
3381
4542
|
const pending = decisionQueue.filter(function (d) { return d.status === 'pending' })
|
|
3382
4543
|
for (let i = 0; i < pending.length; i++) {
|
|
@@ -3413,6 +4574,8 @@ export function apply(ctx) {
|
|
|
3413
4574
|
plannerEnabled: params.plannerEnabled, plannerFails: plannerFails,
|
|
3414
4575
|
formal: formalSummary(),
|
|
3415
4576
|
recentActivity: activityLog.slice(-Math.min(10, Number(params.activityLogCap) || 100)), params: params,
|
|
4577
|
+
// 最终论文的可观测面(spec §2/§6)。
|
|
4578
|
+
paper: await paperStatusView(),
|
|
3416
4579
|
}
|
|
3417
4580
|
}
|
|
3418
4581
|
async function checkTermination() {
|
|
@@ -3431,8 +4594,14 @@ export function apply(ctx) {
|
|
|
3431
4594
|
// H1:在途的 **planner 不算"活跃工作"**。planner 不产出知识、不持有验证任务,只是"问下一步做什么";
|
|
3432
4595
|
// 旧判定把刚派出的 planner 也算成活跃代理,于是本 tick 的停机判定永远被自己派出的 planner 挡掉,
|
|
3433
4596
|
// 空闲时形成"每 tick 一个 planner"的活锁。排除它之后,空闲的那一 tick 就能正常收口停机。
|
|
3434
|
-
const realAgents = Object.keys(agentRegistry).filter(function (cid) { const m = agentRegistry[cid]; return m && m.role !== 'planner' }).length
|
|
4597
|
+
const realAgents = Object.keys(agentRegistry).filter(function (cid) { const m = agentRegistry[cid]; return m && m.role !== 'planner' && m.role !== 'paper' }).length
|
|
3435
4598
|
if (unsolved.length === 0 && !leftoverVerify && realAgents === 0 && Object.keys(tasks).length === 0 && planQueue.length === 0) {
|
|
4599
|
+
// ★ 最终论文(修订 §A2/§A3):必须在 running=false 与 releaseProjectLock() **之前**派遣——
|
|
4600
|
+
// 停止之后 scheduleTick() 是空操作;提前放锁后另一会话可能开始写同一棵树(paper 锁兜底)。
|
|
4601
|
+
// 触发点就是这个分支本身:`leftoverVerify` 为空才是 v3 的真实完整性判据(§A4)。
|
|
4602
|
+
if (params.finalPaper !== false) {
|
|
4603
|
+
try { await maybeWritePaper('auto') } catch (e) { logActivity('paper', '自动撰写最终论文失败:' + String((e && e.message) || e)) }
|
|
4604
|
+
}
|
|
3436
4605
|
scheduler.running = false
|
|
3437
4606
|
await releaseProjectLock()
|
|
3438
4607
|
logActivity('stop', 'all active problems solved (never-priority excluded) and no active agents/tasks/plans — scheduler stopped (strict termination)')
|
|
@@ -3464,8 +4633,13 @@ export function apply(ctx) {
|
|
|
3464
4633
|
if (!create && !exists) return { ok: false, message: 'project not found: ' + slug }
|
|
3465
4634
|
if (scheduler.running) await abortScheduler()
|
|
3466
4635
|
currentProject = slug; await writeCurrentProject(); await ensureDirs()
|
|
3467
|
-
params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; planQueue = []; plannerFails = 0; methodLog = { pendingInventions: [], keepCount: 0, lastKeepAt: 0 }; projectLock = { sessionId: '', at: 0 }; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; lastPlanSummary = null; archivedJ = {}; lastIndexWrite = 0; formalState = { records: {}, todo: [], libRuns: {} }
|
|
3468
|
-
await loadSettings(); await migrateLegacyParams(); await loadState()
|
|
4636
|
+
params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; planQueue = []; plannerFails = 0; methodLog = { pendingInventions: [], keepCount: 0, lastKeepAt: 0 }; projectLock = { sessionId: '', at: 0 }; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; lastPlanSummary = null; archivedJ = {}; lastIndexWrite = 0; formalState = { records: {}, todo: [], libRuns: {} }; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
|
|
4637
|
+
await loadSettings(); await migrateLegacyParams(); await loadState()
|
|
4638
|
+
// 切到/新建项目 = 接手这棵树的遗留作业(本进程不认识的 jobId 才恢复)。
|
|
4639
|
+
try { await recoverLeanJobs() } catch (e) { console.error('vibe-math-v3: lean job recovery on project switch failed: ' + String((e && e.message) || e)) }; await loadKnowledgeBase()
|
|
4640
|
+
// 切项目后引擎探测缓存过期(mathEngines/路径可能不同):重探一次再让提示词读它。
|
|
4641
|
+
try { await refreshMathProbe(true) } catch (e) { /* 探测失败不影响切换 */ }
|
|
4642
|
+
await saveAll()
|
|
3469
4643
|
if (params.indexAutoRebuild) await rebuildIndex()
|
|
3470
4644
|
return { ok: true, project: slug, frameworkRoot: frameworkRoot() }
|
|
3471
4645
|
}
|
|
@@ -3476,6 +4650,13 @@ export function apply(ctx) {
|
|
|
3476
4650
|
function objParams(props, required) { return { type: 'object', properties: props, additionalProperties: false, required: required || [] } }
|
|
3477
4651
|
const handlers = {}
|
|
3478
4652
|
function registerTool(name, description, parameters, executeFn) { handlers[name] = executeFn }
|
|
4653
|
+
// math_computation 的**会话层**注册(共享模块的 host 在上面构造,绑定本会话的 params/Paths/fs)。
|
|
4654
|
+
// 模块调用一次 host.register(...) ⇒ handlers['math_computation'] = 该会话的 handler;
|
|
4655
|
+
// apply 层(工具面)在 apply 作用域单独注册一次,按名路由回这里(FREEZE §4 要求两层都在)。
|
|
4656
|
+
mathTool = registerMathComputation(mathHost)
|
|
4657
|
+
// 会话层挂载(**字面量形态**,静态扫描器可见):handler 是模块为**本会话**构造的那一个;
|
|
4658
|
+
// apply 层的同名词由宿主按 handlerName 路由回这里。两层名字/描述/schema 三处完全一致。
|
|
4659
|
+
registerTool('math_computation', TOOL_DESC.math_computation, MATH_TOOL_SCHEMA, async function (args, agent) { return await mathTool.handler(args || {}, agent) })
|
|
3479
4660
|
registerTool('vibe_math_start', TOOL_DESC.vibe_math_start, objParams({ override: { type: 'boolean' } }), async function (args) { return await startScheduler(args && args.override) })
|
|
3480
4661
|
registerTool('vibe_math_resume', TOOL_DESC.vibe_math_resume, objParams({ override: { type: 'boolean' } }), async function (args) { return await resumeScheduler(args && args.override) })
|
|
3481
4662
|
registerTool('vibe_math_pause', TOOL_DESC.vibe_math_pause, objParams({}), async function () { return await pauseScheduler() })
|
|
@@ -3483,7 +4664,7 @@ export function apply(ctx) {
|
|
|
3483
4664
|
registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), async function () { await refreshParams(); return await getStatus() })
|
|
3484
4665
|
registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), async function () { await refreshParams(); await maybeWriteReport(true); return await buildReport() })
|
|
3485
4666
|
registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, 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 } })
|
|
3486
|
-
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, 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' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
|
|
4667
|
+
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, 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' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { 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 } })
|
|
3487
4668
|
registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, 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' } })
|
|
3488
4669
|
registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), async function () { return await saveSettings() })
|
|
3489
4670
|
registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), async function (args) { return await createTemplate((args && args.where) || 'global') })
|
|
@@ -3529,13 +4710,17 @@ export function apply(ctx) {
|
|
|
3529
4710
|
counts: r, todo: formalTodo().map(function (t) { return { id: t.id, why: t.why, at: t.at } }),
|
|
3530
4711
|
objects: Object.keys(formalRecords()).map(function (k) {
|
|
3531
4712
|
const rec = formalRecords()[k] || {}
|
|
3532
|
-
return { target: k, status: rec.status, file: rec.file, proof: rec.proof, note: rec.note }
|
|
4713
|
+
return { target: k, status: rec.status, file: rec.file, proof: rec.proof, note: rec.note, async: rec.async || null }
|
|
3533
4714
|
}),
|
|
3534
|
-
|
|
3535
|
-
|
|
4715
|
+
async: params.leanAsync !== false,
|
|
4716
|
+
jobs: (function () { const out = []; for (const j of leanJobs.values()) out.push(Object.assign(leanJobPublic(j), { exitCode: j.exitCode === undefined ? null : j.exitCode, paths: leanJobPaths(j), buildSha256: j.buildSha || '' })); out.sort(function (x, y) { return (y.settledAt || 0) - (x.settledAt || 0) || String(x.jobId).localeCompare(String(y.jobId)) }); return out })(),
|
|
4717
|
+
paths: { project: 'Formal/(相对项目根)', lib: 'VibeMath/Formal/Lib/', proved: 'VibeMath/Formal/Proved/', proofs: 'Verified/Lean/', searchPath: vibeRoot(), jobs: 'Formal/Jobs/' },
|
|
4718
|
+
hint: "复用优先:先在 Lib/ 里找现成定义(vibe_math_lean_read 可取原文);新定义用 vibe_math_lean_archive kind='def' 归档,已证引理用 kind='lemma';归档前先跑通(run=true 或先 vibe_math_lean_run),跑不通不要入库。异步档用 jobs 字段查后台编译:**只有 state=settled 才算通过**。",
|
|
3536
4719
|
verify: (Object.keys(tasks).length ? String(tasks[Object.keys(tasks)[0]].rId || '') : null),
|
|
3537
4720
|
}
|
|
3538
4721
|
})
|
|
4722
|
+
registerTool('vibe_math_lean_read', TOOL_DESC.vibe_math_lean_read, objParams({ name: { type: 'string' }, kind: { type: 'string', enum: ['auto', 'lib', 'proved'] } }, ['name']), async function (args) { return await leanReadTool(args) })
|
|
4723
|
+
registerTool('vibe_math_lean_job', TOOL_DESC.vibe_math_lean_job, objParams({ jobId: { type: 'string' }, waitMs: { type: 'integer' } }), async function (args) { return await leanJobTool(args) })
|
|
3539
4724
|
|
|
3540
4725
|
// ---- 代理直接写 md 的写锁 + 轻元数据同步(任务2:代理自组织写各自对应路径的 md,避免并发写同一文件) ----
|
|
3541
4726
|
/**
|
|
@@ -3745,7 +4930,8 @@ export function apply(ctx) {
|
|
|
3745
4930
|
}
|
|
3746
4931
|
if (cmd === 'decisions') return { ok: true, decisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }) }
|
|
3747
4932
|
if (cmd === 'agents') { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round }) } return { ok: true, agents: out } }
|
|
3748
|
-
|
|
4933
|
+
if (cmd === 'paper') return await paperCommand(args)
|
|
4934
|
+
return { ok: false, usage: 'start [override] | resume [override] | pause | abort | status | report | mode <auto|manual> | setup | save | template [global|project] | add <id> <desc> | add-proposition <id> <概述> | list-propositions | methods | index | plan | lock | project [list|new <name>|<name>] | decisions | agents | paper [lang=zh|en] [format=both|md|tex] [force]', message: 'unknown /vibe subcommand: ' + (cmd || '(empty)') }
|
|
3749
4935
|
}
|
|
3750
4936
|
|
|
3751
4937
|
// ================= session surface =================
|
|
@@ -3766,6 +4952,21 @@ export function apply(ctx) {
|
|
|
3766
4952
|
handlers: handlers,
|
|
3767
4953
|
refreshProject: async function () { if (rootAgent) currentProject = await readCurrentProject() },
|
|
3768
4954
|
getRunning: function () { return scheduler.running },
|
|
4955
|
+
// apply 级心跳用(修订 §A2):running=false 之后 scheduleTick() 是空操作,论文的排队重试
|
|
4956
|
+
// (激活上限)与 paper 锁续租必须由**独立于调度器**的心跳驱动。
|
|
4957
|
+
paperRetryDue: paperRetryDue,
|
|
4958
|
+
runPaperRetry: runPaperRetry,
|
|
4959
|
+
// Lean 异步队列(spec §2):心跳推进队列;dispose 由 apply 级 disposer 调用(不留孤儿进程)。
|
|
4960
|
+
runLeanQueue: runLeanQueue,
|
|
4961
|
+
disposeLeanJobs: disposeLeanJobs,
|
|
4962
|
+
leanQueueSize: function () { return leanQueue.length },
|
|
4963
|
+
leanJobsPublic: function () { const out = []; for (const j of leanJobs.values()) out.push(leanJobPublic(j)); return out },
|
|
4964
|
+
// 数学计算(FREEZE §4):心跳用 TTL 刷新探测缓存;off 档不探测(真 no-op)。
|
|
4965
|
+
mathProbeDue: mathProbeDue,
|
|
4966
|
+
refreshMathProbe: refreshMathProbe,
|
|
4967
|
+
mathProbe: function () { return mathProbe },
|
|
4968
|
+
// P2a:persona 两个文本块必须含的「归档→编辑→重跑」规则(文本取自共享模块常量,persona 不手抄)。
|
|
4969
|
+
mathArchiveWorkflowLine: MATH_ARCHIVE_WORKFLOW_LINE,
|
|
3769
4970
|
// childOwner 裁剪用(审计 L1):这个会话当前仍"可能再发 subagent/end"的 child
|
|
3770
4971
|
// = 在册子代理 + 任何任务正在等的那几个。
|
|
3771
4972
|
referencedChildIds: function () {
|
|
@@ -3804,7 +5005,7 @@ export function apply(ctx) {
|
|
|
3804
5005
|
registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), 'vibe_math_status')
|
|
3805
5006
|
registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), 'vibe_math_report')
|
|
3806
5007
|
registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
|
|
3807
|
-
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, 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' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), 'vibe_math_set_params')
|
|
5008
|
+
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, 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' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { 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')
|
|
3808
5009
|
registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, objParams({}), 'vibe_math_setup')
|
|
3809
5010
|
registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), 'vibe_math_save_settings')
|
|
3810
5011
|
registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
|
|
@@ -3832,12 +5033,21 @@ export function apply(ctx) {
|
|
|
3832
5033
|
registerTool('vibe_math_lean_run', TOOL_DESC.vibe_math_lean_run, objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), 'vibe_math_lean_run')
|
|
3833
5034
|
registerTool('vibe_math_lean_archive', TOOL_DESC.vibe_math_lean_archive, 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')
|
|
3834
5035
|
registerTool('vibe_math_lean_lib', TOOL_DESC.vibe_math_lean_lib, objParams({ refresh: { type: 'boolean' } }), 'vibe_math_lean_lib')
|
|
5036
|
+
registerTool('vibe_math_lean_read', TOOL_DESC.vibe_math_lean_read, objParams({ name: { type: 'string' }, kind: { type: 'string', enum: ['auto', 'lib', 'proved'] } }, ['name']), 'vibe_math_lean_read')
|
|
5037
|
+
registerTool('vibe_math_lean_job', TOOL_DESC.vibe_math_lean_job, objParams({ jobId: { type: 'string' }, waitMs: { type: 'integer' } }), 'vibe_math_lean_job')
|
|
5038
|
+
|
|
5039
|
+
// ── math_computation:apply 层(工具面)────────────────────────────────────────────────────
|
|
5040
|
+
// 共享模块的 handler 在**会话层**构造(闭包住该会话的 params/Paths/fs);这一层只把工具名/描述/schema
|
|
5041
|
+
// 挂到宿主工具面,执行时由 apply 级包装按 handlerName 路由回 s.handlers['math_computation']。
|
|
5042
|
+
// **字面量形态**(与 audit-v3-registration-parity.mjs 的扫描器约定一致):名字字面量 +
|
|
5043
|
+
// TOOL_DESC.math_computation + MATH_TOOL_SCHEMA,两条路径的描述/schema 因此逐字相同。
|
|
5044
|
+
registerTool('math_computation', TOOL_DESC.math_computation, MATH_TOOL_SCHEMA, 'math_computation')
|
|
3835
5045
|
|
|
3836
5046
|
// /vibe slash command (registered once; routed per session)
|
|
3837
5047
|
ctx.effect(() => commands.register({
|
|
3838
5048
|
name: 'vibe',
|
|
3839
5049
|
description: 'control the Vibe Math V3 solver (start/pause/projects/setup/save/decisions/agents/methods/index/plan/lock)',
|
|
3840
|
-
input: { hint: '[start [override]|resume [override]|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|methods|index|plan|lock|project [list|new <name>|<name>]|decisions|agents]' },
|
|
5050
|
+
input: { hint: '[start [override]|resume [override]|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|methods|index|plan|lock|project [list|new <name>|<name>]|decisions|agents|paper [lang=zh|en] [format=both|md|tex] [force]]' },
|
|
3841
5051
|
handler: async function (invocation) {
|
|
3842
5052
|
const s = getSession(invocation && invocation.agent)
|
|
3843
5053
|
if (!s) return { kind: 'error', text: JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' }) }
|
|
@@ -3892,14 +5102,29 @@ export function apply(ctx) {
|
|
|
3892
5102
|
function everyMs(ms, fn) {
|
|
3893
5103
|
try { if (typeof ctx.interval === 'function') return ctx.interval(fn, ms) } catch (e) { /* 回落到 setInterval */ }
|
|
3894
5104
|
const t = setInterval(fn, ms)
|
|
3895
|
-
return () =>
|
|
5105
|
+
return () => {
|
|
5106
|
+
clearInterval(t)
|
|
5107
|
+
// 卸载/销毁:终止在跑的编译并标记 interrupted(不留孤儿进程,spec §2.5)。
|
|
5108
|
+
for (const s of sessions.values()) {
|
|
5109
|
+
try { if (typeof s.disposeLeanJobs === 'function') s.disposeLeanJobs() } catch (e) { console.error('vibe-math-v3: lean dispose failed: ' + String((e && e.message) || e)) }
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
3896
5112
|
}
|
|
3897
5113
|
ctx.effect(() => everyMs(LOCK_POLL_MS, function () { for (const s of sessions.values()) { s.renewLockIfDue().catch(function (e) { console.error('vibe-math-v3 lock renew error: ' + String((e && e.message) || e)) }) } }))
|
|
3898
5114
|
// tick timer (registered once; ticks every running session at its own pace)
|
|
3899
5115
|
ctx.effect(() => {
|
|
3900
5116
|
let beat = 0
|
|
3901
5117
|
return everyMs(1000, function () {
|
|
3902
|
-
for (const s of sessions.values()) {
|
|
5118
|
+
for (const s of sessions.values()) {
|
|
5119
|
+
if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick()
|
|
5120
|
+
// 论文心跳(修订 §A2):**独立于 scheduler.running** —— 收口后 running=false,但被激活上限
|
|
5121
|
+
// 拒绝的撰写派遣仍要重试,在途时还要续租 paper 锁。runPaperRetry() 无事可做时立即返回。
|
|
5122
|
+
if (typeof s.runPaperRetry === 'function') s.runPaperRetry().catch(function (e) { console.error('vibe-math-v3: paper retry failed: ' + String((e && e.message) || e)) })
|
|
5123
|
+
// Lean 异步队列(spec §2.2):独立于 scheduler.running;runLeanQueue() 内部不 await 编译。
|
|
5124
|
+
if (typeof s.runLeanQueue === 'function') s.runLeanQueue().catch(function (e) { console.error('vibe-math-v3: lean queue failed: ' + String((e && e.message) || e)) })
|
|
5125
|
+
// 数学计算的探测缓存 TTL 刷新(15 分钟或从未探测过):mathWorkLine 只读缓存,不能自己 await。
|
|
5126
|
+
if (typeof s.mathProbeDue === 'function' && s.mathProbeDue()) s.refreshMathProbe(false).catch(function (e) { console.error('vibe-math-v3: math probe failed: ' + String((e && e.message) || e)) })
|
|
5127
|
+
}
|
|
3903
5128
|
// childOwner 裁剪(审计 L1):每 30 拍(约 30s)一次,成本是"会话数 × 映射数"的一次扫描。
|
|
3904
5129
|
if ((++beat % 30) === 0) {
|
|
3905
5130
|
try { pruneChildOwner() } catch (e) { console.error('vibe-math-v3: pruneChildOwner failed: ' + String((e && e.message) || e)) }
|
|
@@ -3921,6 +5146,316 @@ export function apply(ctx) {
|
|
|
3921
5146
|
// to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
|
|
3922
5147
|
// Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
|
|
3923
5148
|
// byte-identical to the previous in-`apply` declarations.
|
|
5149
|
+
// ============================================================================================
|
|
5150
|
+
// 最终论文(规格:docs/final-paper.md)—— **纯函数部分**(module scope;
|
|
5151
|
+
// 与 v2 逐字同构,只有数据来源不同)。会话相关的落盘/派遣/编译在 makeSession 里。
|
|
5152
|
+
// ============================================================================================
|
|
5153
|
+
/** 固定 9 节骨架(spec §3)。key 同时是 md 的 `## ` 标题与 tex 的 `\section{}`。 */
|
|
5154
|
+
const PAPER_SKELETON = [
|
|
5155
|
+
{ key: '摘要', spec: '1 标题、作者、日期、摘要(原问题 + 主要结论)' },
|
|
5156
|
+
{ key: '引言与问题背景', spec: '2 原问题的完整陈述' },
|
|
5157
|
+
{ key: '原问题的完整解法', spec: '3 最终答案 + 完整推理链' },
|
|
5158
|
+
{ key: '已检验通过的命题', spec: '4 逐条列出,含判定为真的估计值与证据来源' },
|
|
5159
|
+
{ key: '已解决的子问题与中间成果', spec: '5' },
|
|
5160
|
+
{ key: '创造或发现的有价值之物', spec: '6 方法、理论、思想、有价值经验、数学理解' },
|
|
5161
|
+
{ key: '规律总结', spec: '7 从上述条目归纳出的可复用规律' },
|
|
5162
|
+
{ key: '讨论、局限与展望', spec: '8' },
|
|
5163
|
+
{ key: '附录:证据与文件索引', spec: '9 Verified/、Logs/、关键卡片路径' },
|
|
5164
|
+
]
|
|
5165
|
+
const PAPER_NO_EVIDENCE = '(本节暂无证据支持的内容——不编造。)'
|
|
5166
|
+
const PAPER_EVIDENCE_HEADING = '证据与文件索引'
|
|
5167
|
+
/** LaTeX 引擎检测顺序(spec §5):中文优先 xelatex,英文优先 pdflatex/latexmk。 */
|
|
5168
|
+
const PAPER_ENGINE_ORDER_ZH = ['xelatex', 'latexmk', 'pdflatex', 'lualatex', 'tectonic']
|
|
5169
|
+
const PAPER_ENGINE_ORDER_EN = ['pdflatex', 'latexmk', 'xelatex', 'lualatex', 'tectonic']
|
|
5170
|
+
/** 只用常见宏包(spec §5);修复阶段会丢弃不在这个名单里的 \usepackage。 */
|
|
5171
|
+
const PAPER_ALLOWED_PACKAGES = ['amsmath', 'amssymb', 'amsthm', 'geometry', 'hyperref', 'longtable', 'booktabs']
|
|
5172
|
+
/** tex 必须转义的 10 个字符(spec §5):\ _ % & # $ { } ~ ^ ——一次替换,避免二次转义。 */
|
|
5173
|
+
const PAPER_TEX_MAP = { '\\': '\\textbackslash{}', '_': '\\_', '%': '\\%', '&': '\\&', '#': '\\#', '$': '\\$', '{': '\\{', '}': '\\}', '~': '\\textasciitilde{}', '^': '\\textasciicircum{}' }
|
|
5174
|
+
function texEscape(s) { return String(s == null ? '' : s).replace(/[\\_%&#${}~^]/g, function (c) { return PAPER_TEX_MAP[c] }) }
|
|
5175
|
+
/** 材料摘要(只作记录;幂等判定用 run id + finalizedAt + 逐产物存在性,修订 §C)。 */
|
|
5176
|
+
function paperMaterialSummary(digest) {
|
|
5177
|
+
const t = String(digest == null ? '' : digest)
|
|
5178
|
+
return { chars: t.length, lines: t.split('\n').length }
|
|
5179
|
+
}
|
|
5180
|
+
/**
|
|
5181
|
+
* 撰写者卡死判定(纯函数,便于守卫覆盖两条分支):
|
|
5182
|
+
* 'none' 正常在途(还没到卡死窗口,且 force 未要求放弃)
|
|
5183
|
+
* 'reap' 必须回收:child 已不在 agentRegistry(end 事件丢失 / 宿主丢弃,它永远不会再回来),
|
|
5184
|
+
* 或已超过卡死窗口——force 与心跳都据此自动降级,论文不能被一个死掉的撰写者永久卡住
|
|
5185
|
+
* 'refuse' force 但还没到窗口:调用方必须给出**可执行**的报错(告诉调用者还要等多久)
|
|
5186
|
+
*/
|
|
5187
|
+
function paperWriterVerdict(o) {
|
|
5188
|
+
if (!(o && o.inFlight)) return 'none'
|
|
5189
|
+
if (o.childInRegistry === false) return 'reap'
|
|
5190
|
+
const age = Number((o && o.ageMs) || 0)
|
|
5191
|
+
const stale = Math.max(1, Number((o && o.staleMs) || 0))
|
|
5192
|
+
if (age >= stale) return 'reap'
|
|
5193
|
+
return (o && o.force) ? 'refuse' : 'none'
|
|
5194
|
+
}
|
|
5195
|
+
/** 行内 md → tex:``code`` / **bold** / *em* 转成命令,其余按 spec §5 转义;`$...$` 原样保留。 */
|
|
5196
|
+
function paperInlineToTex(s) {
|
|
5197
|
+
const t = String(s == null ? '' : s)
|
|
5198
|
+
let out = ''
|
|
5199
|
+
let i = 0
|
|
5200
|
+
while (i < t.length) {
|
|
5201
|
+
const c = t[i]
|
|
5202
|
+
if (c === '$') { const j = t.indexOf('$', i + 1); if (j > i) { out += t.slice(i, j + 1); i = j + 1; continue } }
|
|
5203
|
+
if (c === '`') { const j = t.indexOf('`', i + 1); if (j > i) { out += '\\texttt{' + texEscape(t.slice(i + 1, j)) + '}'; i = j + 1; continue } }
|
|
5204
|
+
if (c === '*' && t[i + 1] === '*') { const j = t.indexOf('**', i + 2); if (j > i) { out += '\\textbf{' + paperInlineToTex(t.slice(i + 2, j)) + '}'; i = j + 2; continue } }
|
|
5205
|
+
if (c === '*') { const j = t.indexOf('*', i + 1); if (j > i) { out += '\\emph{' + paperInlineToTex(t.slice(i + 1, j)) + '}'; i = j + 1; continue } }
|
|
5206
|
+
out += PAPER_TEX_MAP[c] !== undefined ? PAPER_TEX_MAP[c] : c
|
|
5207
|
+
i++
|
|
5208
|
+
}
|
|
5209
|
+
return out
|
|
5210
|
+
}
|
|
5211
|
+
/** md 正文 → tex 正文(标题/列表/段落;只认 compose 侧约定的最小语法)。 */
|
|
5212
|
+
function paperMdToTexBody(md) {
|
|
5213
|
+
const out = []
|
|
5214
|
+
let inList = false
|
|
5215
|
+
let buf = []
|
|
5216
|
+
const flush = function () { if (buf.length) { out.push(paperInlineToTex(buf.join(' '))); buf = [] } }
|
|
5217
|
+
const closeList = function () { if (inList) { out.push('\\end{itemize}'); inList = false } }
|
|
5218
|
+
const lines = String(md == null ? '' : md).split(/\r?\n/)
|
|
5219
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5220
|
+
const line = lines[i].replace(/\s+$/, '')
|
|
5221
|
+
const h = /^(#{1,4})\s+(.*)$/.exec(line)
|
|
5222
|
+
if (h) {
|
|
5223
|
+
flush(); closeList()
|
|
5224
|
+
const txt = paperInlineToTex(h[2])
|
|
5225
|
+
const lv = h[1].length
|
|
5226
|
+
out.push(lv <= 1 ? ('\\section*{' + txt + '}') : lv === 2 ? ('\\section{' + txt + '}') : lv === 3 ? ('\\subsection{' + txt + '}') : ('\\subsubsection{' + txt + '}'))
|
|
5227
|
+
continue
|
|
5228
|
+
}
|
|
5229
|
+
const li = /^\s*[-*]\s+(.*)$/.exec(line)
|
|
5230
|
+
if (li) { flush(); if (!inList) { out.push('\\begin{itemize}'); inList = true } out.push(' \\item ' + paperInlineToTex(li[1])); continue }
|
|
5231
|
+
if (!line.trim()) { flush(); closeList(); out.push(''); continue }
|
|
5232
|
+
buf.push(line.trim())
|
|
5233
|
+
}
|
|
5234
|
+
flush(); closeList()
|
|
5235
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n').trim()
|
|
5236
|
+
}
|
|
5237
|
+
/** 主模板(spec §5):中文 ctexart / 英文 article,只加常见宏包。 */
|
|
5238
|
+
function paperTexDoc(o) {
|
|
5239
|
+
const lang = (o && o.lang === 'en') ? 'en' : 'zh'
|
|
5240
|
+
return [
|
|
5241
|
+
'\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}',
|
|
5242
|
+
'\\usepackage[margin=2.5cm]{geometry}',
|
|
5243
|
+
'\\usepackage{amsmath,amssymb,amsthm}',
|
|
5244
|
+
'\\usepackage{hyperref}',
|
|
5245
|
+
'\\usepackage{longtable,booktabs}',
|
|
5246
|
+
'\\title{' + texEscape(o && o.title) + '}',
|
|
5247
|
+
'\\author{' + texEscape(o && o.author) + '}',
|
|
5248
|
+
'\\date{' + texEscape(o && o.date) + '}',
|
|
5249
|
+
'\\begin{document}',
|
|
5250
|
+
'\\maketitle',
|
|
5251
|
+
String((o && o.bodyTex) || ''),
|
|
5252
|
+
'\\end{document}',
|
|
5253
|
+
].join('\n') + '\n'
|
|
5254
|
+
}
|
|
5255
|
+
/** 修复阶段的**最小模板**(spec §5):只留 documentclass + 正文。 */
|
|
5256
|
+
function paperMinimalTexDoc(o) {
|
|
5257
|
+
const lang = (o && o.lang === 'en') ? 'en' : 'zh'
|
|
5258
|
+
return [
|
|
5259
|
+
'\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}',
|
|
5260
|
+
'\\begin{document}',
|
|
5261
|
+
String((o && o.bodyTex) || ''),
|
|
5262
|
+
'\\end{document}',
|
|
5263
|
+
].join('\n') + '\n'
|
|
5264
|
+
}
|
|
5265
|
+
/** 修复阶段:丢弃不在常见宏包名单里的 \usepackage(其余原样保留)。 */
|
|
5266
|
+
function paperSanitizeTex(tex) {
|
|
5267
|
+
const keep = []
|
|
5268
|
+
const lines = String(tex == null ? '' : tex).split('\n')
|
|
5269
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5270
|
+
const m = /^\s*\\usepackage(\[[^\]]*\])?\{([^}]*)\}\s*$/.exec(lines[i])
|
|
5271
|
+
if (m) {
|
|
5272
|
+
const pkgs = String(m[2]).split(',').map(function (x) { return x.trim() }).filter(Boolean)
|
|
5273
|
+
const ok = pkgs.filter(function (p) { return PAPER_ALLOWED_PACKAGES.indexOf(p) >= 0 })
|
|
5274
|
+
if (ok.length === 0) continue
|
|
5275
|
+
keep.push('\\usepackage' + (m[1] || '') + '{' + ok.join(',') + '}')
|
|
5276
|
+
continue
|
|
5277
|
+
}
|
|
5278
|
+
keep.push(lines[i])
|
|
5279
|
+
}
|
|
5280
|
+
return keep.join('\n')
|
|
5281
|
+
}
|
|
5282
|
+
/** 骨架名匹配:允许带序号/前缀,或只写节名的一部分(例如「附录」)。 */
|
|
5283
|
+
function paperSkeletonKey(name) {
|
|
5284
|
+
const n = String(name == null ? '' : name).replace(/^#+\s*/, '').trim()
|
|
5285
|
+
if (!n) return undefined
|
|
5286
|
+
for (const s of PAPER_SKELETON) if (n === s.key) return s.key
|
|
5287
|
+
for (const s of PAPER_SKELETON) if (n.indexOf(s.key) !== -1 || s.key.indexOf(n) !== -1) return s.key
|
|
5288
|
+
return undefined
|
|
5289
|
+
}
|
|
5290
|
+
/** 按 `## ` 把任意 markdown 切成 [{name,text}](无标题时 name='')。 */
|
|
5291
|
+
function paperSplitByHeadings(md) {
|
|
5292
|
+
const out = []
|
|
5293
|
+
let cur = { name: '', text: [] }
|
|
5294
|
+
const lines = String(md == null ? '' : md).split(/\r?\n/)
|
|
5295
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5296
|
+
const h = /^##\s+(.*?)\s*$/.exec(lines[i])
|
|
5297
|
+
if (h) { if (cur.name || cur.text.length) out.push({ name: cur.name, text: cur.text.join('\n').trim() }); cur = { name: h[1], text: [] }; continue }
|
|
5298
|
+
cur.text.push(lines[i])
|
|
5299
|
+
}
|
|
5300
|
+
if (cur.name || cur.text.length) out.push({ name: cur.name, text: cur.text.join('\n').trim() })
|
|
5301
|
+
return out
|
|
5302
|
+
}
|
|
5303
|
+
/** 把「论文撰写」子代理的回复映射到 9 节骨架上(**绝不编造**:缺的节由调用方填占位说明)。 */
|
|
5304
|
+
function paperSkeletonFromReply(reply, fallbackText) {
|
|
5305
|
+
const sections = {}
|
|
5306
|
+
const notes = []
|
|
5307
|
+
let title = ''
|
|
5308
|
+
let abstract = ''
|
|
5309
|
+
const put = function (key, body) {
|
|
5310
|
+
const b = String(body == null ? '' : body).trim()
|
|
5311
|
+
if (!b) return
|
|
5312
|
+
sections[key] = sections[key] ? (sections[key] + '\n\n' + b) : b
|
|
5313
|
+
}
|
|
5314
|
+
const absorbUnknown = function (name, text) {
|
|
5315
|
+
if (!name && !String(text || '').trim()) return
|
|
5316
|
+
notes.push('作者写了一节「' + (name || '(无名)') + '」,不属于固定 9 节骨架,已并入「讨论、局限与展望」')
|
|
5317
|
+
put('讨论、局限与展望', (name ? ('**' + name + '**\n\n') : '') + text)
|
|
5318
|
+
}
|
|
5319
|
+
const obj = (reply && typeof reply === 'object' && !Array.isArray(reply)) ? reply : null
|
|
5320
|
+
if (obj) {
|
|
5321
|
+
title = String(obj.title || obj.标题 || '').trim()
|
|
5322
|
+
abstract = String(obj.abstract || obj.摘要 || '').trim()
|
|
5323
|
+
const list = Array.isArray(obj.sections) ? obj.sections : (Array.isArray(obj.章节) ? obj.章节 : [])
|
|
5324
|
+
for (let i = 0; i < list.length; i++) {
|
|
5325
|
+
const s = list[i]
|
|
5326
|
+
if (!s) continue
|
|
5327
|
+
const name = String(s.name || s.key || s.heading || s.标题 || '')
|
|
5328
|
+
const body = String(s.body != null ? s.body : (s.content != null ? s.content : (s.内容 != null ? s.内容 : '')))
|
|
5329
|
+
const key = paperSkeletonKey(name)
|
|
5330
|
+
if (key === undefined) absorbUnknown(name.trim(), body)
|
|
5331
|
+
else put(key, body)
|
|
5332
|
+
}
|
|
5333
|
+
const bodyField = String(obj.body || obj.content || obj.md || obj.正文 || '')
|
|
5334
|
+
if (bodyField.trim()) {
|
|
5335
|
+
const parts = paperSplitByHeadings(bodyField)
|
|
5336
|
+
for (let i = 0; i < parts.length; i++) {
|
|
5337
|
+
const key = paperSkeletonKey(parts[i].name)
|
|
5338
|
+
if (key === undefined) { if (!parts[i].name && !abstract && !sections['摘要']) abstract = parts[i].text; else if (parts[i].name) absorbUnknown(parts[i].name, parts[i].text) }
|
|
5339
|
+
else put(key, parts[i].text)
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
} else {
|
|
5343
|
+
const parts = paperSplitByHeadings(String(fallbackText == null ? '' : fallbackText))
|
|
5344
|
+
let matched = 0
|
|
5345
|
+
for (let i = 0; i < parts.length; i++) {
|
|
5346
|
+
const key = paperSkeletonKey(parts[i].name)
|
|
5347
|
+
if (key === undefined) { if (parts[i].name) absorbUnknown(parts[i].name, parts[i].text) }
|
|
5348
|
+
else { put(key, parts[i].text); matched++ }
|
|
5349
|
+
}
|
|
5350
|
+
if (matched === 0) {
|
|
5351
|
+
const raw = String(fallbackText == null ? '' : fallbackText).trim()
|
|
5352
|
+
if (raw) { notes.push('作者回复不是契约 JSON,也没有可识别的 `## ` 节标题;其原样正文已记入「讨论、局限与展望」'); put('讨论、局限与展望', raw) }
|
|
5353
|
+
else notes.push('作者回复为空:没有任何内容可写(不得编造)')
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
return { title: title, abstract: abstract, sections: sections, notes: notes }
|
|
5357
|
+
}
|
|
5358
|
+
/** 组装 md(spec §3):9 节固定骨架 + 确定性证据索引;缺证据的节写占位说明。 */
|
|
5359
|
+
function paperBuildMarkdown(o) {
|
|
5360
|
+
const sec = (o && o.sections) || {}
|
|
5361
|
+
const ev = ((o && o.evidence) || []).slice()
|
|
5362
|
+
const L = []
|
|
5363
|
+
const ttl = String((o && o.title) || '').trim() || '研究报告'
|
|
5364
|
+
L.push('# ' + ttl)
|
|
5365
|
+
L.push('')
|
|
5366
|
+
L.push('- 作者:' + String((o && o.author) || ''))
|
|
5367
|
+
L.push('- 日期:' + String((o && o.date) || ''))
|
|
5368
|
+
L.push('')
|
|
5369
|
+
for (let i = 0; i < PAPER_SKELETON.length; i++) {
|
|
5370
|
+
const key = PAPER_SKELETON[i].key
|
|
5371
|
+
L.push('## ' + key)
|
|
5372
|
+
L.push('')
|
|
5373
|
+
let body = String(sec[key] || '').trim()
|
|
5374
|
+
if (key === '摘要') { const ab = String((o && o.abstract) || '').trim(); if (ab) body = body ? (ab + '\n\n' + body) : ab }
|
|
5375
|
+
if (key === '附录:证据与文件索引' && ev.length) {
|
|
5376
|
+
const idx = '### ' + PAPER_EVIDENCE_HEADING + '\n\n' + ev.map(function (x) { return '- `' + String(x) + '`' }).join('\n')
|
|
5377
|
+
body = body ? (body + '\n\n' + idx) : idx
|
|
5378
|
+
}
|
|
5379
|
+
L.push(body || PAPER_NO_EVIDENCE)
|
|
5380
|
+
L.push('')
|
|
5381
|
+
}
|
|
5382
|
+
return L.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'
|
|
5383
|
+
}
|
|
5384
|
+
/** paper.log.md 的**唯一格式**(v2/v3 逐字一致,spec §4)。 */
|
|
5385
|
+
function paperLogHead(id) {
|
|
5386
|
+
return ['# 论文写作日志|' + String(id), '',
|
|
5387
|
+
'> 单作者流程(v2/v3 同一格式):收口触发 → 派遣「论文撰写」子代理 → 落盘 md/tex → 编译 pdf。', '']
|
|
5388
|
+
}
|
|
5389
|
+
function paperLogLine(at, tag, text) { return '- ' + fmtTime(at) + ' [' + String(tag) + '] ' + String(text == null ? '' : text) }
|
|
5390
|
+
/** 论文目录 id:只允许安全字符(同一把归一化 ⇒ 不可能写出 Paper/ 之外,spec §6 越权用例)。 */
|
|
5391
|
+
function paperDirId(raw) { return (idSafe(String(raw == null ? '' : raw).trim()) || 'run') }
|
|
5392
|
+
/** 从回复里抽出可能存在的标题行(md 非契约路径的兜底)。 */
|
|
5393
|
+
function paperTitleFromMd(md) {
|
|
5394
|
+
const m = /^#\s+(.*?)\s*$/m.exec(String(md == null ? '' : md))
|
|
5395
|
+
return m ? m[1].trim() : ''
|
|
5396
|
+
}
|
|
5397
|
+
|
|
5398
|
+
/** 搜索路径 flag:收敛成一个常量(不同 Lean 版本拼写可能不同,改这里即可,接口不变)。 */
|
|
5399
|
+
const LEAN_SEARCH_PATH_FLAG = '--search-path'
|
|
5400
|
+
/** 用户已显式给出搜索路径就不再注入(显式覆盖优先)。 */
|
|
5401
|
+
function hasLeanSearchFlag(args) {
|
|
5402
|
+
return (Array.isArray(args) ? args : []).some(function (a) {
|
|
5403
|
+
const s = String(a)
|
|
5404
|
+
return s === LEAN_SEARCH_PATH_FLAG || s.indexOf(LEAN_SEARCH_PATH_FLAG + '=') === 0 || s === '-R' || s === '--root'
|
|
5405
|
+
})
|
|
5406
|
+
}
|
|
5407
|
+
/**
|
|
5408
|
+
* 搜索路径注入计划(修订 §2):用户 leanArgs 已显式给过 ⇒ 什么都不注入(显式覆盖优先);
|
|
5409
|
+
* 否则**先注入用户给的 leanSearchPaths,再注入自动根**,去重且保持顺序。
|
|
5410
|
+
*/
|
|
5411
|
+
function leanSearchPathPlan(userArgs, extraPaths, autoRoot) {
|
|
5412
|
+
const args = Array.isArray(userArgs) ? userArgs.map(String) : []
|
|
5413
|
+
if (hasLeanSearchFlag(args)) return { inject: [], paths: [], explicit: true }
|
|
5414
|
+
const out = []
|
|
5415
|
+
const seen = {}
|
|
5416
|
+
const add = function (p) {
|
|
5417
|
+
const s = String(p == null ? '' : p).trim()
|
|
5418
|
+
if (!s || seen[s]) return
|
|
5419
|
+
seen[s] = true
|
|
5420
|
+
out.push(s)
|
|
5421
|
+
}
|
|
5422
|
+
for (const p of (Array.isArray(extraPaths) ? extraPaths : [])) add(p)
|
|
5423
|
+
add(autoRoot)
|
|
5424
|
+
const inject = []
|
|
5425
|
+
for (const p of out) { inject.push(LEAN_SEARCH_PATH_FLAG, p) }
|
|
5426
|
+
return { inject: inject, paths: out, explicit: false }
|
|
5427
|
+
}
|
|
5428
|
+
/** 规范化文本:换行统一 \n、去行尾空白、去尾部空行——内容哈希只反映语义内容。 */
|
|
5429
|
+
function normalizeLeanText(t) {
|
|
5430
|
+
return String(t == null ? '' : t).replace(/\r\n?/g, '\n').replace(/[ \t]+$/gm, '').replace(/\n+$/, '') + '\n'
|
|
5431
|
+
}
|
|
5432
|
+
/** 内容 sha256(归档去重与作业幂等的键)。 */
|
|
5433
|
+
function leanContentSha(t) { return createHash('sha256').update(normalizeLeanText(t), 'utf8').digest('hex') }
|
|
5434
|
+
/**
|
|
5435
|
+
* 构建上下文(修订 §4):同一段代码在不同引擎/参数/搜索路径下结果可能不同,
|
|
5436
|
+
* 所以"已通过"的判定必须绑定**构建上下文**,不能只看内容。
|
|
5437
|
+
*/
|
|
5438
|
+
function leanBuildContext(engine, args, searchPaths) {
|
|
5439
|
+
return JSON.stringify({
|
|
5440
|
+
engine: String(engine == null ? '' : engine),
|
|
5441
|
+
args: (Array.isArray(args) ? args : []).map(String),
|
|
5442
|
+
searchPaths: (Array.isArray(searchPaths) ? searchPaths : []).map(String),
|
|
5443
|
+
})
|
|
5444
|
+
}
|
|
5445
|
+
/** 作业指纹 = sha256(内容 + 构建上下文)(修订 §4:jobId 取它的前 12 位)。 */
|
|
5446
|
+
function leanJobFingerprint(contentText, buildCtx) {
|
|
5447
|
+
return createHash('sha256').update(normalizeLeanText(contentText) + '\n' + String(buildCtx || ''), 'utf8').digest('hex')
|
|
5448
|
+
}
|
|
5449
|
+
/** jobId = `<target|name>-<指纹[0:12]>`:同一内容 + 同一构建上下文天然只有一个作业。 */
|
|
5450
|
+
function leanJobId(key, fingerprint) {
|
|
5451
|
+
const k = String(key == null ? '' : key).replace(/[^A-Za-z0-9_.-]/g, '_').replace(/^_+|_+$/g, '').slice(0, 60) || 'job'
|
|
5452
|
+
return k + '-' + String(fingerprint == null ? '' : fingerprint).slice(0, 12)
|
|
5453
|
+
}
|
|
5454
|
+
/** "已落地且通过"的**唯一**判据:state=settled 且 exit 0 且未超时未中断(内容哈希由 settle 校验)。 */
|
|
5455
|
+
function leanJobSettledOk(rec) { return !!rec && rec.state === 'settled' && rec.exitCode === 0 && !rec.timedOut && !rec.interrupted }
|
|
5456
|
+
/** 主动性档位(修订 §1):与"验证时的要求强度"(formalVerify)是两件事。 */
|
|
5457
|
+
const LEAN_INITIATIVE_MODES = ['off', 'normal', 'eager']
|
|
5458
|
+
|
|
3924
5459
|
export const __testHelpers = {
|
|
3925
5460
|
uuid,
|
|
3926
5461
|
shortId,
|
|
@@ -3948,6 +5483,37 @@ export const __testHelpers = {
|
|
|
3948
5483
|
parseMethodMd,
|
|
3949
5484
|
sanitizeToolFilter,
|
|
3950
5485
|
registeredToolsFromError,
|
|
5486
|
+
LEAN_INITIATIVE_MODES,
|
|
5487
|
+
leanJobSettledOk,
|
|
5488
|
+
leanJobId,
|
|
5489
|
+
leanJobFingerprint,
|
|
5490
|
+
leanBuildContext,
|
|
5491
|
+
leanContentSha,
|
|
5492
|
+
normalizeLeanText,
|
|
5493
|
+
leanSearchPathPlan,
|
|
5494
|
+
hasLeanSearchFlag,
|
|
5495
|
+
LEAN_SEARCH_PATH_FLAG,
|
|
5496
|
+
// final paper(spec §6 的纯函数守卫面;与 v2 同构)
|
|
5497
|
+
PAPER_SKELETON,
|
|
5498
|
+
PAPER_NO_EVIDENCE,
|
|
5499
|
+
PAPER_ALLOWED_PACKAGES,
|
|
5500
|
+
PAPER_ENGINE_ORDER_ZH,
|
|
5501
|
+
PAPER_ENGINE_ORDER_EN,
|
|
5502
|
+
texEscape,
|
|
5503
|
+
paperMaterialSummary,
|
|
5504
|
+
paperWriterVerdict,
|
|
5505
|
+
paperInlineToTex,
|
|
5506
|
+
paperMdToTexBody,
|
|
5507
|
+
paperTexDoc,
|
|
5508
|
+
paperMinimalTexDoc,
|
|
5509
|
+
paperSanitizeTex,
|
|
5510
|
+
paperSkeletonKey,
|
|
5511
|
+
paperSkeletonFromReply,
|
|
5512
|
+
paperBuildMarkdown,
|
|
5513
|
+
paperLogHead,
|
|
5514
|
+
paperLogLine,
|
|
5515
|
+
paperDirId,
|
|
5516
|
+
paperTitleFromMd,
|
|
3951
5517
|
}
|
|
3952
5518
|
|
|
3953
5519
|
function now() { return Date.now() }
|
|
@@ -3968,7 +5534,7 @@ const TOOL_DESC = {
|
|
|
3968
5534
|
vibe_math_set_mode: 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.',
|
|
3969
5535
|
// set_params 的 schema 现在也收 mode(M10):同一能力既有专用工具 vibe_math_set_mode,也可用这个键;
|
|
3970
5536
|
// 两条注册路径共用这一份描述,`--self-probe` 会证明漂移能被抓到。
|
|
3971
|
-
vibe_math_set_params: 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象未达到 Lean 已通过或已记录显式阻塞原因之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean
|
|
5537
|
+
vibe_math_set_params: 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象未达到 Lean 已通过或已记录显式阻塞原因之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式(框架会在用户 leanArgs 之后、文件名之前自动追加 --search-path <VibeMath 根>,用户已显式给出就不注入;leanSearchPaths 可附加额外搜索路径,先注入它们再注入自动根);leanAsync = true(默认,后台队列:入队即返回,只有作业落地 ok 才置 passed 并写归档证明)| false(同步 await 的旧语义);leanJobsMaxParallel = 后台编译并发上限(默认 1 = 串行);leanInitiative = off|normal|eager 控制**日常的**形式化主动性(与 formalVerify 的验证要求强度是两件事)。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测 xelatex→latexmk→pdflatex→lualatex→tectonic),产物在 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 只对当次显式调用生效、永不记忆)。',
|
|
3972
5538
|
vibe_math_setup: 'Return the interactive parameter schema for guided configuration.',
|
|
3973
5539
|
vibe_math_save_settings: 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.',
|
|
3974
5540
|
vibe_math_template: 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.',
|
|
@@ -3994,6 +5560,10 @@ const TOOL_DESC = {
|
|
|
3994
5560
|
vibe_math_lean_run: "(member) Execute the Lean toolchain on one .lean file inside the workspace and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output. Pass target=<object id> to also record the run against that object.",
|
|
3995
5561
|
vibe_math_lean_archive: '(member) Archive Lean code. kind="def": a REUSABLE definition/object/assumption → the global cross-project library (VibeMath/Formal/Lib). kind="lemma": a machine-checked lemma → VibeMath/Formal/Proved. kind="proof": the formal proof of a project object → Formal/<target>.lean, and (when the run passes) also Verified/Lean/<target>.lean, marking the object Lean-passed. kind="blocked": record an explicit, reasoned "cannot/not worth formalizing" decision (note required).',
|
|
3996
5562
|
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.",
|
|
5563
|
+
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).',
|
|
5564
|
+
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.',
|
|
5565
|
+
// 数学计算:描述逐字来自共享模块的 MATH_TOOL_DESCRIPTION(四套一致,FREEZE §1)。
|
|
5566
|
+
math_computation: MATH_TOOL_DESCRIPTION,
|
|
3997
5567
|
}
|
|
3998
5568
|
|
|
3999
5569
|
function uuid() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 36; i++) { if (i === 8 || i === 13 || i === 18 || i === 23) s += '-'; else s += h[Math.floor(Math.random() * 16)] } return s }
|