dsh-vibe-math 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +156 -389
- package/README.md +156 -390
- package/cordis.patch.yml +56 -14
- package/docs/AUDIT-CHECKLIST.md +2 -2
- package/docs/final-paper.md +1 -0
- package/docs/formal-verification.md +184 -29
- package/docs/math-computation.md +121 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.0.en.md +151 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.0.md +115 -0
- package/docs/test-timing.md +24 -12
- package/installer.js +7 -4
- package/package.json +18 -2
- package/prompt-corpus-persona/persona-corpus.json +12 -12
- package/prompt-corpus-persona/persona-corpus.md +116 -32
- package/prompt-corpus-v2/formal-verify-v2.json +116 -92
- package/prompt-corpus-v2/formal-verify-v2.md +1430 -55
- package/prompt-corpus-v3/formal-verify-v3.json +101 -53
- package/prompt-corpus-v3/formal-verify-v3.md +1001 -22
- package/prompt-corpus-v4/formal-verify-v4.json +14 -14
- package/prompt-corpus-v4/formal-verify-v4.md +247 -14
- package/prompt-corpus-v5/prompt-corpus-v5.json +62 -62
- package/prompt-corpus-v5/prompt-corpus-v5.md +570 -3
- package/tests/audit-math-computation-contract.mjs +191 -0
- package/tests/audit-math-computation-parity.mjs +169 -0
- package/tests/audit-math-computation-sensitivity.mjs +231 -0
- package/tests/audit-persona-sensitivity.mjs +4 -4
- package/tests/audit-persona-surface.test.mjs +38 -10
- package/tests/audit-v5-integrity.mjs +14 -0
- package/tests/e2e-v5-round2.test.mjs +452 -2
- package/tests/formal-verify-v2.test.mjs +292 -6
- package/tests/formal-verify-v3.test.mjs +271 -4
- package/tests/formal-verify-v4.test.mjs +295 -6
- package/tests/formal-verify-v5.test.mjs +9 -1
- package/tests/helpers/math-computation-fake-seam.mjs +135 -0
- package/tests/math-computation-shared.test.mjs +504 -0
- package/tests/prompt-v5-integrity.test.mjs +5 -5
- package/vibe-math-v2/agent.cordis.yml +20 -8
- package/vibe-math-v2/math-computation.js +936 -0
- package/vibe-math-v2/math-engines.js +156 -0
- package/vibe-math-v2/vibe-math-v2.js +837 -40
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v3/agent.cordis.yml +20 -8
- package/vibe-math-v3/math-computation.js +936 -0
- package/vibe-math-v3/math-engines.js +156 -0
- package/vibe-math-v3/vibe-math-v3.js +828 -47
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v4/agent.cordis.yml +34 -6
- package/vibe-math-v4/math-computation.js +936 -0
- package/vibe-math-v4/math-engines.js +156 -0
- package/vibe-math-v4/vibe-math-v4.js +801 -31
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v5/agent.cordis.yml +38 -6
- package/vibe-math-v5/math-computation.js +936 -0
- package/vibe-math-v5/math-engines.js +156 -0
- package/vibe-math-v5/vibe-math-v5.js +1120 -37
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +83 -3
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
|
@@ -55,6 +55,7 @@ const wakes = [] // { childId, rootId, prompt }
|
|
|
55
55
|
const interrupts = []
|
|
56
56
|
const leanRuns = [] // { argv, file, cwd, graceMs }
|
|
57
57
|
const terminated = []
|
|
58
|
+
const disposers = []
|
|
58
59
|
const roots = []
|
|
59
60
|
let rootSeq = 0
|
|
60
61
|
let subprocessAvailable = true
|
|
@@ -187,7 +188,7 @@ const ctx = {
|
|
|
187
188
|
return undefined
|
|
188
189
|
},
|
|
189
190
|
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
190
|
-
effect(fn) { const d = fn();
|
|
191
|
+
effect(fn) { const d = fn(); const off = () => { if (typeof d === 'function') d() }; disposers.push(off); return off },
|
|
191
192
|
logger: { info() {}, warn() {}, error() {} },
|
|
192
193
|
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
193
194
|
commands: { register(spec) { cmdRegs.push(spec); return () => {} } },
|
|
@@ -229,6 +230,12 @@ const ctx = {
|
|
|
229
230
|
|
|
230
231
|
const mod = await import(PLUGIN.href + '?t=' + Date.now())
|
|
231
232
|
;(mod.default || mod).apply(ctx)
|
|
233
|
+
// leanAsync defaults to TRUE (background queue). The sections above guard the **synchronous** Lean
|
|
234
|
+
// path verbatim, so pin leanAsync=false through the workspace-level settings file (every
|
|
235
|
+
// setProject → loadSettings() picks it up). Section 9b flips it back on explicitly for the async
|
|
236
|
+
// state machine. Nothing else in the suite depends on the async default.
|
|
237
|
+
mkdirSync(VIBE, { recursive: true })
|
|
238
|
+
writeFileSync(join(VIBE, 'vibe_math_setting.json'), '{ "leanAsync": false }\n', 'utf8')
|
|
232
239
|
|
|
233
240
|
// ---------------------------------------------------------------
|
|
234
241
|
// driving helpers
|
|
@@ -518,7 +525,7 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
|
|
|
518
525
|
assert(/ok/.test(runGood.stdout), 'the compiler stdout is returned')
|
|
519
526
|
const last = leanRuns[leanRuns.length - 1]
|
|
520
527
|
assert(last.cwd.replace(/\\/g, '/') === toolProj.replace(/\\/g, '/'), 'the toolchain runs with the PROJECT root as cwd')
|
|
521
|
-
assert(last.argv.join(' ') === 'lean', 'by default the argv is
|
|
528
|
+
assert(last.argv.join(' ') === 'lean --search-path ' + VIBE.replace(/\\/g, '/'), 'by default the argv is the executable + the injected search path (got ' + last.argv.join(' ') + ')')
|
|
522
529
|
assert(last.graceMs === 120000, 'the default leanTimeoutMs is passed down as graceMs (' + last.graceMs + ')')
|
|
523
530
|
}
|
|
524
531
|
{
|
|
@@ -592,7 +599,7 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
|
|
|
592
599
|
await callTool('vibe_math_set_params', { leanCommand: 'lake', leanArgs: ['env', 'lean'] }, RE)
|
|
593
600
|
const runLake = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
|
|
594
601
|
assert(runLake.ok === true, 'lake env lean works through leanCommand/leanArgs')
|
|
595
|
-
assert(leanRuns[leanRuns.length - 1].argv.join(' ') === 'lake env lean', 'the argv is [exe, ...leanArgs,
|
|
602
|
+
assert(leanRuns[leanRuns.length - 1].argv.join(' ') === 'lake env lean --search-path ' + VIBE.replace(/\\/g, '/'), 'the argv is [exe, ...leanArgs, injected --search-path] (got ' + leanRuns[leanRuns.length - 1].argv.join(' ') + ')')
|
|
596
603
|
await callTool('vibe_math_set_params', { leanCommand: 'lean', leanArgs: [] }, RE)
|
|
597
604
|
}
|
|
598
605
|
|
|
@@ -609,7 +616,7 @@ const provedPath = join(VIBE, 'Formal', 'Proved')
|
|
|
609
616
|
assert(!existsSync(join(toolProj, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the project tree')
|
|
610
617
|
assert(defRes.run && defRes.run.ok === true, 'the reusable definition is executed on archive (run defaults to true)')
|
|
611
618
|
const libIdx = readIf(join(libPath, 'Index.md'))
|
|
612
|
-
assert(/\| 名称 \| 文件 \| 类别 \| 摘要 \| 最近运行 \|/.test(libIdx), 'Lib/Index.md uses the contract columns')
|
|
619
|
+
assert(/\| 名称 \| 文件 \| 类别 \| 依赖(import) \| 摘要 \| 最近运行 \|/.test(libIdx), 'Lib/Index.md uses the contract columns (incl. the import-dependency column, spec §4.4)')
|
|
613
620
|
assert(/ZMod5/.test(libIdx) && /Lib\/ZMod5\.lean/.test(libIdx) && /\| ok \|/.test(libIdx), 'Lib/Index.md lists the new definition with its run result')
|
|
614
621
|
assert(!/ZMod5/.test(readIf(join(toolProj, 'Formal', 'Index.md'))), 'the global definition does not pollute the project formal index')
|
|
615
622
|
}
|
|
@@ -1224,6 +1231,266 @@ section('9 the office can audit formal strength')
|
|
|
1224
1231
|
assert(existsSync(join(VIBE, 'Formal', 'Proved', 'Index.md')) && existsSync(join(libPath, 'Index.md')), 'both global indexes are framework-maintained')
|
|
1225
1232
|
}
|
|
1226
1233
|
|
|
1234
|
+
// ===============================================================
|
|
1235
|
+
// 9b. Lean 增量 + 异步(spec §1–§4 + 修订 §1–§5):与 v2 同一套断言强度
|
|
1236
|
+
// ===============================================================
|
|
1237
|
+
section('9b Lean 增量 + 异步:参数/argv/队列/去重/只读面/主动性/恢复/卸载')
|
|
1238
|
+
{
|
|
1239
|
+
const H = mod.__testHelpers
|
|
1240
|
+
const slash = (s) => String(s == null ? '' : s).replace(/\\/g, '/')
|
|
1241
|
+
const SAME = (a, b) => slash(a) === slash(b)
|
|
1242
|
+
const formalOf2 = (proj) => JSON.parse(readIf(join(projRoot(proj), 'State', 'formal.json')) || '{}')
|
|
1243
|
+
const jobFile = (proj, id) => join(projRoot(proj), 'Formal', 'Jobs', id + '.json')
|
|
1244
|
+
const readJob = (proj, id) => JSON.parse(readIf(jobFile(proj, id)) || '{}')
|
|
1245
|
+
const vibeCmd = async (rawInput, root) => JSON.parse(await cmdRegs[0].handler({ rawInput, agent: root }))
|
|
1246
|
+
const findJob = async (proj, id, states, tries = 100, ms = 60) => {
|
|
1247
|
+
for (let i = 0; i < tries; i++) { const r = readJob(proj, id); if (r.state && states.indexOf(r.state) !== -1) return r; await sleep(ms) }
|
|
1248
|
+
return undefined
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// ── 参数:默认值/归一化(字符串 'false' 绝不能滑过去)────────────────────────────
|
|
1252
|
+
const rp = makeRoot()
|
|
1253
|
+
await callTool('vibe_math_new_project', { name: 'lean-async-params' }, rp)
|
|
1254
|
+
await callTool('vibe_math_set_params', { leanAsync: true }, rp)
|
|
1255
|
+
const bad = await callTool('vibe_math_set_params', { leanAsync: 'false' }, rp)
|
|
1256
|
+
assert(bad.params.leanAsync === true, '★★★ leanAsync="false" (a STRING) does not slip through the `else out[k]=v` tail: it falls back to the boolean default')
|
|
1257
|
+
const wp = await callTool('vibe_math_set_params', { leanInitiative: 'banana', leanJobsMaxParallel: 0, leanSearchPaths: 'not-an-array' }, rp)
|
|
1258
|
+
assert(wp.params.leanInitiative === 'normal', 'leanInitiative rejects an unknown mode back to normal')
|
|
1259
|
+
assert(wp.params.leanJobsMaxParallel === 1, 'leanJobsMaxParallel floors at 1 (never 0)')
|
|
1260
|
+
assert(Array.isArray(wp.params.leanSearchPaths) && wp.params.leanSearchPaths.length === 0, 'leanSearchPaths rejects a non-array back to []')
|
|
1261
|
+
const dd = await callTool('vibe_math_set_params', { leanSearchPaths: [' /opt/lean-lib ', '/opt/lean-lib', ''] }, rp)
|
|
1262
|
+
assert(JSON.stringify(dd.params.leanSearchPaths) === JSON.stringify(['/opt/lean-lib']), 'leanSearchPaths trims, drops empties and de-duplicates')
|
|
1263
|
+
|
|
1264
|
+
// ── 纯函数:搜索路径计划(修订 §2)与指纹(修订 §4)──────────────────────────────
|
|
1265
|
+
const planDefault = H.leanSearchPathPlan([], [], '/root/vm')
|
|
1266
|
+
assert(JSON.stringify(planDefault.paths) === JSON.stringify(['/root/vm']) && planDefault.inject.length === 2, '★ the default plan injects exactly the automatic root')
|
|
1267
|
+
const planExtra = H.leanSearchPathPlan(['env', 'lean'], ['/a', '/b'], '/root/vm')
|
|
1268
|
+
assert(JSON.stringify(planExtra.paths) === JSON.stringify(['/a', '/b', '/root/vm']), '★★ user leanSearchPaths come FIRST, then the automatic root')
|
|
1269
|
+
assert(H.leanSearchPathPlan(['--search-path', '/x'], ['/a'], '/root/vm').inject.length === 0 && H.leanSearchPathPlan(['-R', '/x'], ['/a'], '/root/vm').inject.length === 0 && H.leanSearchPathPlan(['--root', '/x'], ['/a'], '/root/vm').inject.length === 0, '★ an explicit --search-path/-R/--root in leanArgs suppresses all injection (user wins)')
|
|
1270
|
+
const ctxA = H.leanBuildContext('lean', ['a'], ['/p'])
|
|
1271
|
+
const ctxB = H.leanBuildContext('lean', ['b'], ['/p'])
|
|
1272
|
+
assert(H.leanJobFingerprint('x\n', ctxA) !== H.leanJobFingerprint('x\n', ctxB), '★ the job fingerprint covers the build context (engine/args/search paths)')
|
|
1273
|
+
|
|
1274
|
+
// ── argv:注入在用户参数之后、文件名之前 ─────────────────────────────────────────
|
|
1275
|
+
const ra = makeRoot()
|
|
1276
|
+
await callTool('vibe_math_new_project', { name: 'lean-async-argv' }, ra)
|
|
1277
|
+
await callTool('vibe_math_set_params', { leanAsync: false, leanCommand: 'lake', leanArgs: ['env', 'lean'] }, ra)
|
|
1278
|
+
const projA = projRoot('lean-async-argv')
|
|
1279
|
+
mkdirSync(join(projA, 'Formal'), { recursive: true })
|
|
1280
|
+
writeFileSync(join(projA, 'Formal', 'srch.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
1281
|
+
const runs0 = leanRuns.length
|
|
1282
|
+
await callTool('vibe_math_lean_run', { file: 'Formal/srch.lean' }, ra)
|
|
1283
|
+
const argv1 = leanRuns[leanRuns.length - 1].argv
|
|
1284
|
+
assert(leanRuns.length === runs0 + 1 && JSON.stringify(argv1.slice(0, 3)) === JSON.stringify(['lake', 'env', 'lean']), 'the user leanArgs are preserved in order (lake env lean)')
|
|
1285
|
+
assert(argv1[argv1.length - 2] === '--search-path' && SAME(argv1[argv1.length - 1], VIBE), '★★ --search-path + the VibeMath root are injected AFTER the user args and immediately BEFORE the file (' + JSON.stringify(argv1) + ')')
|
|
1286
|
+
await callTool('vibe_math_set_params', { leanArgs: ['env', 'lean', '--search-path', '/explicit'] }, ra)
|
|
1287
|
+
await callTool('vibe_math_lean_run', { file: 'Formal/srch.lean' }, ra)
|
|
1288
|
+
const argv2 = leanRuns[leanRuns.length - 1].argv
|
|
1289
|
+
assert(argv2.filter((x) => x === '--search-path').length === 1 && argv2[4] === '/explicit', '★ an explicit --search-path is respected verbatim (no second injection) — got ' + JSON.stringify(argv2))
|
|
1290
|
+
await callTool('vibe_math_set_params', { leanArgs: ['env', 'lean'], leanSearchPaths: ['/extra1', '/extra2'] }, ra)
|
|
1291
|
+
await callTool('vibe_math_lean_run', { file: 'Formal/srch.lean' }, ra)
|
|
1292
|
+
const argv3 = leanRuns[leanRuns.length - 1].argv
|
|
1293
|
+
assert(JSON.stringify(argv3.slice(-6)) === JSON.stringify(['--search-path', '/extra1', '--search-path', '/extra2', '--search-path', slash(VIBE)]), '★★ leanSearchPaths are injected first, then the automatic root (got ' + JSON.stringify(argv3.slice(-6)) + ')')
|
|
1294
|
+
|
|
1295
|
+
// ── 异步状态机:入队立即返回 → 心跳排空 → 只有 settled(ok) 才是通过 ───────────────
|
|
1296
|
+
const rq = makeRoot()
|
|
1297
|
+
await callTool('vibe_math_new_project', { name: 'lean-async-q' }, rq)
|
|
1298
|
+
await callTool('vibe_math_set_params', { leanAsync: true }, rq)
|
|
1299
|
+
const projQ = projRoot('lean-async-q')
|
|
1300
|
+
mkdirSync(join(projQ, 'Formal'), { recursive: true })
|
|
1301
|
+
writeFileSync(join(projQ, 'Formal', 'async-good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
1302
|
+
writeFileSync(join(projQ, 'Formal', 'async-bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
|
|
1303
|
+
const nBefore = leanRuns.length
|
|
1304
|
+
const q1 = await callTool('vibe_math_lean_run', { file: 'Formal/async-good.lean', target: 'pAsync' }, rq)
|
|
1305
|
+
assert(q1.ok === true && !!q1.async && q1.async.state === 'queued' && typeof q1.async.jobId === 'string', '★★ leanAsync=true returns IMMEDIATELY with {async:{jobId,state:queued}} (got ' + JSON.stringify(q1.async) + ')')
|
|
1306
|
+
assert(leanRuns.length === nBefore, '★★ the tool call itself did NOT block on the compiler (enqueued, not run)')
|
|
1307
|
+
assert(q1.async.jobId.indexOf('pAsync-') === 0, 'the jobId is <target>-<fingerprint[0:12]> (got ' + q1.async.jobId + ')')
|
|
1308
|
+
assert(existsSync(jobFile('lean-async-q', q1.async.jobId)), 'the job record is mirrored to <project>/Formal/Jobs/<jobId>.json')
|
|
1309
|
+
const rec0 = readJob('lean-async-q', q1.async.jobId)
|
|
1310
|
+
assert(rec0.state === 'queued' && typeof rec0.sha256 === 'string' && typeof rec0.buildSha256 === 'string' && String(rec0.buildContext).length > 0, '★ the job record carries state + content hash + build-context hash')
|
|
1311
|
+
assert((formalOf2('lean-async-q').records || {}).pAsync === undefined || formalOf2('lean-async-q').records.pAsync.status !== 'passed', '★★ a queued job never marks the object passed')
|
|
1312
|
+
const done1 = await findJob('lean-async-q', q1.async.jobId, ['settled'])
|
|
1313
|
+
assert(!!done1, 'the apply-level heartbeat drained the queue and settled the job (got ' + readJob('lean-async-q', q1.async.jobId).state + ')')
|
|
1314
|
+
assert(done1 && done1.exitCode === 0, 'the settled record keeps exitCode 0')
|
|
1315
|
+
const recAfter = (formalOf2('lean-async-q').records || {}).pAsync
|
|
1316
|
+
assert(!!recAfter && recAfter.status === 'attempted' && recAfter.run && recAfter.run.ok === true, 'a green RUN lands as attempted with run.ok=true')
|
|
1317
|
+
assert(!!recAfter.async && recAfter.async.state === 'settled' && recAfter.async.jobId === q1.async.jobId, 'the object record carries the async job pointer/state')
|
|
1318
|
+
const q2 = await callTool('vibe_math_lean_run', { file: 'Formal/async-bad.lean', target: 'pAsyncBad' }, rq)
|
|
1319
|
+
const done2 = await findJob('lean-async-q', q2.async.jobId, ['failed'])
|
|
1320
|
+
assert(!!done2 && done2.exitCode === 1, '★ a red async run settles as failed with exitCode 1')
|
|
1321
|
+
const recBad = (formalOf2('lean-async-q').records || {}).pAsyncBad
|
|
1322
|
+
assert(!!recBad && recBad.status === 'attempted' && recBad.run.ok === false && /sorry/.test(String(recBad.run.stderrTail || '')), '★ a red async run is recorded as attempted with the compiler output kept (never passed)')
|
|
1323
|
+
// 归档证明:落地前不是 passed,落地 ok 才写 Verified/Lean/
|
|
1324
|
+
const q3 = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'pAsymProof', content: 'theorem pAsymProof : 1 = 1 := rfl\n' }, rq)
|
|
1325
|
+
assert(q3.ok === true && q3.async && q3.async.state === 'queued' && q3.status === 'attempted', '★★ kind=proof in async档 returns queued + attempted (NOT passed)')
|
|
1326
|
+
assert(!existsSync(join(projQ, 'Verified', 'Lean', 'pAsymProof.lean')), '★★ the archived proof file does NOT exist while the job is still queued')
|
|
1327
|
+
const done3 = await findJob('lean-async-q', q3.async.jobId, ['settled', 'failed'])
|
|
1328
|
+
assert(!!done3 && done3.state === 'settled', 'the proof job settled green (got ' + (done3 && done3.state) + ')')
|
|
1329
|
+
assert(existsSync(join(projQ, 'Verified', 'Lean', 'pAsymProof.lean')), '★★ only after settled(ok) is Verified/Lean/<id>.lean written')
|
|
1330
|
+
assert(((formalOf2('lean-async-q').records || {}).pAsymProof || {}).status === 'passed', '★★ and only then does the object become passed')
|
|
1331
|
+
assert(/pAsymProof/.test(readIf(join(projQ, 'Formal', 'Index.md'))), 'the index reflects the passed object')
|
|
1332
|
+
// ── lean_job(修订 §3):清单/单查/waitMs ────────────────────────────────────────
|
|
1333
|
+
const listJobs = await callTool('vibe_math_lean_job', {}, rq)
|
|
1334
|
+
assert(listJobs.ok === true && Array.isArray(listJobs.jobs) && listJobs.jobs.length >= 3, '★ lean_job without jobId returns the session job list')
|
|
1335
|
+
assert(listJobs.jobs.every((j) => !!j.paths && typeof j.paths.receipt === 'string'), 'every listed job carries its receipt path')
|
|
1336
|
+
const one = await callTool('vibe_math_lean_job', { jobId: q3.async.jobId }, rq)
|
|
1337
|
+
assert(one.ok === true && one.state === 'settled' && one.passed === true && one.paths.archive === 'Verified/Lean/pAsymProof.lean', '★ lean_job {jobId} returns state/exitCode/paths and the passed verdict')
|
|
1338
|
+
assert((await callTool('vibe_math_lean_job', { jobId: 'nope-000000000000' }, rq)).code === 'V3_NOT_FOUND', 'an unknown jobId is refused with a typed code')
|
|
1339
|
+
const q4 = await callTool('vibe_math_lean_run', { file: 'Formal/async-bad.lean', target: 'pAsyncBad' }, rq)
|
|
1340
|
+
const waited = await callTool('vibe_math_lean_job', { jobId: q4.async.jobId, waitMs: 6000 }, rq)
|
|
1341
|
+
assert(waited.ok === true && waited.state !== 'queued' && waited.stillRunning === false && waited.waitedMs > 0, '★★ lean_job waitMs>0 waits for the job to leave queued/running (got ' + waited.state + ')')
|
|
1342
|
+
assert((await callTool('vibe_math_lean_lib', {}, rq)).paths.searchPath === VIBE.replace(/\\/g, '/'), '★ lean_lib exposes paths.searchPath = the VibeMath root')
|
|
1343
|
+
assert(Array.isArray((await callTool('vibe_math_lean_lib', {}, rq)).jobs), '★ lean_lib exposes the job list')
|
|
1344
|
+
|
|
1345
|
+
// ── 超时:leanTimeoutMs 是每个作业的预算,主动 terminate,绝不 passed ─────────────
|
|
1346
|
+
const rt = makeRoot()
|
|
1347
|
+
await callTool('vibe_math_new_project', { name: 'lean-async-timeout' }, rt)
|
|
1348
|
+
await callTool('vibe_math_set_params', { leanAsync: true, leanTimeoutMs: 1000 }, rt)
|
|
1349
|
+
const projT = projRoot('lean-async-timeout')
|
|
1350
|
+
mkdirSync(join(projT, 'Formal'), { recursive: true })
|
|
1351
|
+
writeFileSync(join(projT, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1 := rfl\n', 'utf8')
|
|
1352
|
+
const termBefore = terminated.length
|
|
1353
|
+
const q5 = await callTool('vibe_math_lean_run', { file: 'Formal/hang.lean', target: 'pHang' }, rt)
|
|
1354
|
+
const done5 = await findJob('lean-async-timeout', q5.async.jobId, ['timeout'], 150)
|
|
1355
|
+
assert(!!done5 && done5.timedOut === true, '★★ a hanging job times out (state=timeout) with leanTimeoutMs as its budget')
|
|
1356
|
+
assert(terminated.length > termBefore, '★★ the timeout ACTIVELY called handle.terminate()')
|
|
1357
|
+
assert(((formalOf2('lean-async-timeout').records || {}).pHang || {}).status !== 'passed', '★ a timed-out job never sets passed')
|
|
1358
|
+
|
|
1359
|
+
// ── 并发参数(修订 §5):leanJobsMaxParallel>1 时两个作业同时进入 running ──────────
|
|
1360
|
+
const rc = makeRoot()
|
|
1361
|
+
await callTool('vibe_math_new_project', { name: 'lean-async-par' }, rc)
|
|
1362
|
+
await callTool('vibe_math_set_params', { leanAsync: true, leanJobsMaxParallel: 2, leanTimeoutMs: 2500 }, rc)
|
|
1363
|
+
const projC = projRoot('lean-async-par')
|
|
1364
|
+
mkdirSync(join(projC, 'Formal'), { recursive: true })
|
|
1365
|
+
writeFileSync(join(projC, 'Formal', 'h1.lean'), '-- HANG\ntheorem h1 : 1 = 1 := rfl\n', 'utf8')
|
|
1366
|
+
writeFileSync(join(projC, 'Formal', 'h2.lean'), '-- HANG\ntheorem h2 : 1 = 1 := rfl\n', 'utf8')
|
|
1367
|
+
const j1 = await callTool('vibe_math_lean_run', { file: 'Formal/h1.lean', target: 'pH1' }, rc)
|
|
1368
|
+
const j2 = await callTool('vibe_math_lean_run', { file: 'Formal/h2.lean', target: 'pH2' }, rc)
|
|
1369
|
+
let both = false
|
|
1370
|
+
for (let i = 0; i < 120 && !both; i++) {
|
|
1371
|
+
const a = readJob('lean-async-par', j1.async.jobId)
|
|
1372
|
+
const b = readJob('lean-async-par', j2.async.jobId)
|
|
1373
|
+
both = (a.state === 'running' && b.state === 'running')
|
|
1374
|
+
if (!both) await sleep(50)
|
|
1375
|
+
}
|
|
1376
|
+
assert(both, '★★ with leanJobsMaxParallel=2 BOTH queued jobs are in running at the same time (concurrency is a parameter, not a constant)')
|
|
1377
|
+
for (const k of [j1, j2]) { try { await callTool('vibe_math_lean_job', { jobId: k.async.jobId, waitMs: 3000 }, rc) } catch (e) { /* ignore */ } }
|
|
1378
|
+
|
|
1379
|
+
// ── 去重(§4.3)+ 构建上下文进指纹(修订 §4)─────────────────────────────────────
|
|
1380
|
+
const rd = makeRoot()
|
|
1381
|
+
await callTool('vibe_math_new_project', { name: 'lean-async-dedupe' }, rd)
|
|
1382
|
+
await callTool('vibe_math_set_params', { leanAsync: true, leanCommand: 'lake', leanArgs: ['env', 'lean'] }, rd)
|
|
1383
|
+
const defBody = 'def reusableOne : Nat := 1\n'
|
|
1384
|
+
const r1 = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: defBody }, rd)
|
|
1385
|
+
assert(r1.ok === true && r1.async && r1.deduped === false, 'the first archive of a definition is enqueued (not deduped)')
|
|
1386
|
+
await findJob('lean-async-dedupe', r1.async.jobId, ['settled'])
|
|
1387
|
+
const runsAfter1 = leanRuns.length
|
|
1388
|
+
const r2 = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: defBody }, rd)
|
|
1389
|
+
assert(r2.ok === true && r2.deduped === true && r2.async === undefined, '★★★ re-archiving identical content is DEDUPED (no job, no recompile)')
|
|
1390
|
+
assert(leanRuns.length === runsAfter1, '★★ the deduped archive did NOT recompile')
|
|
1391
|
+
await callTool('vibe_math_set_params', { leanArgs: ['env', 'lean', '--threads=2'] }, rd)
|
|
1392
|
+
const r5 = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: defBody }, rd)
|
|
1393
|
+
assert(r5.ok === true && r5.deduped === false && !!r5.async && !!r1.async && r5.async.jobId !== r1.async.jobId, '★★★ the SAME content under a DIFFERENT build context gets a different jobId (not treated as already-verified)')
|
|
1394
|
+
if (r5.async) await findJob('lean-async-dedupe', r5.async.jobId, ['settled', 'failed'])
|
|
1395
|
+
await callTool('vibe_math_set_params', { leanArgs: ['env', 'lean'] }, rd)
|
|
1396
|
+
const changed = defBody + '\n-- 内容变了\n'
|
|
1397
|
+
const r3 = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: changed }, rd)
|
|
1398
|
+
assert(r3.ok === true && r3.deduped === false && !!r3.async, 'a changed body is NOT deduped (content hash differs)')
|
|
1399
|
+
if (r3.async) await findJob('lean-async-dedupe', r3.async.jobId, ['settled', 'failed'])
|
|
1400
|
+
const r6 = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: changed }, rd)
|
|
1401
|
+
assert(r6.ok === true && r6.deduped === true, 'and the changed body dedupes on its own (content-based, not "first wins")')
|
|
1402
|
+
|
|
1403
|
+
// ── 崩溃恢复(§2.6 + 修订 §4):绝不置 passed ────────────────────────────────────
|
|
1404
|
+
const rr = makeRoot()
|
|
1405
|
+
await callTool('vibe_math_new_project', { name: 'lean-async-recover' }, rr)
|
|
1406
|
+
await callTool('vibe_math_set_params', { leanAsync: true }, rr)
|
|
1407
|
+
const projR = projRoot('lean-async-recover')
|
|
1408
|
+
mkdirSync(join(projR, 'Formal', 'Jobs'), { recursive: true })
|
|
1409
|
+
const goodBody = 'theorem recGood : 1 = 1 := rfl\n'
|
|
1410
|
+
writeFileSync(join(projR, 'Formal', 'recGood.lean'), goodBody, 'utf8')
|
|
1411
|
+
const ctxG = H.leanBuildContext('lean', [], [slash(VIBE)])
|
|
1412
|
+
const fpG = H.leanJobFingerprint(goodBody, ctxG)
|
|
1413
|
+
const jobIdG = H.leanJobId('pRecGood', fpG)
|
|
1414
|
+
writeFileSync(jobFile('lean-async-recover', jobIdG), JSON.stringify({
|
|
1415
|
+
jobId: jobIdG, kind: 'archive', state: 'running', attempts: 1, target: 'pRecGood', name: 'pRecGood',
|
|
1416
|
+
rel: 'Formal/recGood.lean', scope: 'project', sha256: H.leanContentSha(goodBody), buildSha256: fpG, buildContext: ctxG,
|
|
1417
|
+
engine: 'lean', argv: [], searchPaths: [slash(VIBE)], archiveKind: 'proof', enqueuedAt: 1, startedAt: 2, settledAt: 0, exitCode: null,
|
|
1418
|
+
}), 'utf8')
|
|
1419
|
+
const otherJob = H.leanJobId('pRecOther', H.leanJobFingerprint('theorem other : 1 = 1 := rfl\n', ctxG))
|
|
1420
|
+
writeFileSync(jobFile('lean-async-recover', otherJob), JSON.stringify({
|
|
1421
|
+
jobId: otherJob, kind: 'archive', state: 'running', attempts: 3, target: 'pRecOther', name: 'pRecOther',
|
|
1422
|
+
rel: 'Formal/recMissing.lean', scope: 'project', sha256: H.leanContentSha('theorem other : 1 = 1 := rfl\n'), buildSha256: 'ffffffffffff', buildContext: ctxG,
|
|
1423
|
+
engine: 'lean', argv: [], searchPaths: [slash(VIBE)], archiveKind: 'proof', enqueuedAt: 1, startedAt: 2, settledAt: 0, exitCode: null,
|
|
1424
|
+
}), 'utf8')
|
|
1425
|
+
await callTool('vibe_math_new_project', { name: 'lean-async-recover' }, rr) // setProject ⇒ recoverLeanJobs()
|
|
1426
|
+
const recsR = formalOf2('lean-async-recover').records || {}
|
|
1427
|
+
assert((recsR.pRecGood || {}).status === 'attempted', '★★ recovery marks a crashed running job as attempted (never passed)')
|
|
1428
|
+
assert((recsR.pRecOther || {}).status === 'attempted', 'recovery marks the hash-mismatched job object as attempted too')
|
|
1429
|
+
assert(!existsSync(join(projR, 'Verified', 'Lean', 'pRecGood.lean')), '★ recovery never writes an archived proof for a crashed job')
|
|
1430
|
+
const afterG = readJob('lean-async-recover', jobIdG)
|
|
1431
|
+
assert(Number(afterG.attempts) === 2 && ['queued', 'running', 'settled'].indexOf(afterG.state) !== -1, '★★ the matching-hash job was re-enqueued with attempts+1 (got ' + JSON.stringify({ state: afterG.state, attempts: afterG.attempts }) + ')')
|
|
1432
|
+
const afterO = readJob('lean-async-recover', otherJob)
|
|
1433
|
+
assert(afterO.state === 'interrupted' && Number(afterO.attempts) === 3, '★★ the mismatched job is only marked interrupted (NOT re-driven, attempts unchanged)')
|
|
1434
|
+
assert(!!(await findJob('lean-async-recover', jobIdG, ['settled'])), 'the re-enqueued job then settles normally on the heartbeat')
|
|
1435
|
+
|
|
1436
|
+
// ── lean_read(§1.2):只读、路径守卫、64KB 截断 ─────────────────────────────────
|
|
1437
|
+
mkdirSync(join(VIBE, 'Formal', 'Lib'), { recursive: true })
|
|
1438
|
+
writeFileSync(join(VIBE, 'Formal', 'Lib', 'recGood.lean'), goodBody, 'utf8')
|
|
1439
|
+
const rd1 = await callTool('vibe_math_lean_read', { name: 'recGood' }, rr)
|
|
1440
|
+
assert(rd1.ok === true && rd1.kind === 'lib' && rd1.sha256 === H.leanContentSha(goodBody) && rd1.truncated === false && rd1.text === goodBody, '★ lean_read returns the archived text + sha256 + bytes for a Lib file')
|
|
1441
|
+
assert(rd1.bytes === Buffer.byteLength(goodBody, 'utf8'), 'lean_read reports the byte length (keys=' + Object.keys(rd1).join(',') + ' got ' + JSON.stringify(rd1.bytes) + ' want ' + Buffer.byteLength(goodBody, 'utf8') + ')')
|
|
1442
|
+
assert((await callTool('vibe_math_lean_read', { name: 'recGood', kind: 'proved' }, rr)).code === 'V3_NOT_FOUND', 'kind=proved looks only in Formal/Proved')
|
|
1443
|
+
for (const badName of ['../etc/passwd', 'a/b', 'C:\\x\\y', '..']) {
|
|
1444
|
+
const rbad = await callTool('vibe_math_lean_read', { name: badName }, rr)
|
|
1445
|
+
assert(rbad.ok === false && rbad.code === 'V3_INVALID_ARGUMENT', '★ lean_read rejects the path-escape attempt ' + JSON.stringify(badName))
|
|
1446
|
+
}
|
|
1447
|
+
writeFileSync(join(VIBE, 'Formal', 'Lib', 'bigOne.lean'), '-- ' + 'x'.repeat(70 * 1024) + '\n', 'utf8')
|
|
1448
|
+
const rbig = await callTool('vibe_math_lean_read', { name: 'bigOne' }, rr)
|
|
1449
|
+
assert(rbig.ok === true && rbig.truncated === true && Buffer.byteLength(rbig.text, 'utf8') <= 64 * 1024, '★ lean_read truncates at 64KB and says so (truncated=true)')
|
|
1450
|
+
writeFileSync(join(VIBE, 'Formal', 'Lib', 'depUser.lean'), 'import Formal.Lib.bigOne\n\ndef depUser := 1\n', 'utf8')
|
|
1451
|
+
await callTool('vibe_math_lean_lib', {}, rr)
|
|
1452
|
+
const libIdx = readIf(join(VIBE, 'Formal', 'Lib', 'Index.md'))
|
|
1453
|
+
assert(/Formal\.Lib\.bigOne/.test(libIdx) || /bigOne/.test(libIdx), '★★ the Lib index carries the dependency scanned from import lines (idx=' + String(libIdx).slice(0, 500).replace(/\n/g, ' | ') + ')')
|
|
1454
|
+
|
|
1455
|
+
// ── 主动性档位(修订 §1 + v5 统一):提示词读 leanInitiative,不是 formalVerify ─────────
|
|
1456
|
+
// 第 4 项为 null = "日线完全不出现";第 6 项是该行的 formalVerify 档(默认 encourage)。
|
|
1457
|
+
for (const [mode, rootName, projName, want, notWant, fv] of [
|
|
1458
|
+
['normal', 'lean-init-normal', 'lean-init-normal-proj', /主动性 normal/, null, 'encourage'],
|
|
1459
|
+
['eager', 'lean-init-eager', 'lean-init-eager-proj', /主动档(leanInitiative=eager)/, null, 'encourage'],
|
|
1460
|
+
['off', 'lean-init-off', 'lean-init-off-proj', null, /顺手形式化/, 'encourage'],
|
|
1461
|
+
// formalVerify:'off' + eager:日线仍要注入(主动性是独立轴),验证阶段文本仍关闭。
|
|
1462
|
+
['eager', 'lean-init-off-eager', 'lean-init-off-eager-proj', /主动档(leanInitiative=eager)/, /【Lean 形式化验证(/, 'off'],
|
|
1463
|
+
]) {
|
|
1464
|
+
const rw = makeRoot()
|
|
1465
|
+
await callTool('vibe_math_new_project', { name: projName }, rw)
|
|
1466
|
+
await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: fv || 'encourage', leanInitiative: mode }), rw)
|
|
1467
|
+
await callTool('vibe_math_add_problem', { id: 'qInit' + mode + '-' + (fv || 'e'), 陈述: '主动性提示词 ' + mode }, rw)
|
|
1468
|
+
await callTool('vibe_math_start', {}, rw)
|
|
1469
|
+
const got = await drive(rw, () => spawnOf(rw, 'explorer:').length >= 1 || spawnOf(rw, 'solver:').length >= 1, 'work prompt for ' + mode, 12000)
|
|
1470
|
+
const text = promptText(rw)
|
|
1471
|
+
const ok4 = want ? want.test(text) : !/顺手形式化/.test(text)
|
|
1472
|
+
assert(got && ok4, '★★ leanInitiative=' + mode + (fv === 'off' ? ' (formalVerify:off)' : '') + ' is reflected in the work prompt' + (want ? '' : ':off ⇒ 连"顺手形式化"日线都不出现'))
|
|
1473
|
+
if (notWant) assert(!notWant.test(text), 'and the ' + mode + (fv === 'off' ? '/off' : '') + ' work prompt does not carry the verification-phase Lean block (验证要求仍由 formalVerify 表达)')
|
|
1474
|
+
if (fv === 'off') assert(text.indexOf('三条筛选判据') !== -1, '★★★ formalVerify:off + eager 仍带完整日线(三条筛选判据在),不是被 off 静默吞掉')
|
|
1475
|
+
await callTool('vibe_math_pause', {}, rw)
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// ── 卸载(spec §2.5):terminate + interrupted,绝不 passed ──────────────────────
|
|
1479
|
+
const rz = makeRoot()
|
|
1480
|
+
await callTool('vibe_math_new_project', { name: 'lean-async-dispose' }, rz)
|
|
1481
|
+
await callTool('vibe_math_set_params', { leanAsync: true, leanTimeoutMs: 60000 }, rz)
|
|
1482
|
+
const projZ = projRoot('lean-async-dispose')
|
|
1483
|
+
mkdirSync(join(projZ, 'Formal'), { recursive: true })
|
|
1484
|
+
writeFileSync(join(projZ, 'Formal', 'orphan.lean'), '-- HANG\ntheorem orphan : 1 = 1 := rfl\n', 'utf8')
|
|
1485
|
+
const termZ = terminated.length
|
|
1486
|
+
const qz = await callTool('vibe_math_lean_run', { file: 'Formal/orphan.lean', target: 'pOrphan' }, rz)
|
|
1487
|
+
const startedZ = await findJob('lean-async-dispose', qz.async.jobId, ['running'])
|
|
1488
|
+
assert(!!startedZ, 'the dispose case got a job into running')
|
|
1489
|
+
for (const off of disposers) { try { off() } catch (e) { /* ignore */ } }
|
|
1490
|
+
assert(terminated.length > termZ, '★★ unmounting the plugin terminated the in-flight compile (no orphan process)')
|
|
1491
|
+
assert(((formalOf2('lean-async-dispose').records || {}).pOrphan || {}).status !== 'passed', '★ dispose never marks the object passed')
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1227
1494
|
// ===============================================================
|
|
1228
1495
|
// 10. interaction corpus (AUDIT-CHECKLIST §2.4) — a HUMAN must be able to re-read
|
|
1229
1496
|
// every prompt the framework emitted, not just the assertions about them.
|