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.
Files changed (59) hide show
  1. package/README.en.md +175 -369
  2. package/README.md +175 -370
  3. package/cordis.patch.yml +78 -22
  4. package/docs/AUDIT-CHECKLIST.md +3 -3
  5. package/docs/final-paper.md +172 -0
  6. package/docs/formal-verification.md +184 -29
  7. package/docs/math-computation.md +121 -0
  8. package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -0
  9. package/docs/release-notes/RELEASE-NOTES-2.7.0.en.md +151 -0
  10. package/docs/release-notes/RELEASE-NOTES-2.7.0.md +115 -0
  11. package/docs/test-timing.md +25 -9
  12. package/installer.js +7 -4
  13. package/package.json +21 -3
  14. package/prompt-corpus-persona/persona-corpus.json +16 -16
  15. package/prompt-corpus-persona/persona-corpus.md +164 -52
  16. package/prompt-corpus-v2/formal-verify-v2.json +128 -92
  17. package/prompt-corpus-v2/formal-verify-v2.md +1574 -108
  18. package/prompt-corpus-v3/formal-verify-v3.json +137 -53
  19. package/prompt-corpus-v3/formal-verify-v3.md +1406 -100
  20. package/prompt-corpus-v4/formal-verify-v4.json +14 -14
  21. package/prompt-corpus-v4/formal-verify-v4.md +247 -14
  22. package/prompt-corpus-v5/prompt-corpus-v5.json +62 -62
  23. package/prompt-corpus-v5/prompt-corpus-v5.md +570 -3
  24. package/tests/audit-math-computation-contract.mjs +191 -0
  25. package/tests/audit-math-computation-parity.mjs +169 -0
  26. package/tests/audit-math-computation-sensitivity.mjs +231 -0
  27. package/tests/audit-persona-sensitivity.mjs +64 -11
  28. package/tests/audit-persona-surface.test.mjs +53 -16
  29. package/tests/audit-v5-integrity.mjs +14 -0
  30. package/tests/e2e-v5-round2.test.mjs +923 -3
  31. package/tests/formal-verify-v2.test.mjs +295 -6
  32. package/tests/formal-verify-v3.test.mjs +275 -4
  33. package/tests/formal-verify-v4.test.mjs +295 -6
  34. package/tests/formal-verify-v5.test.mjs +9 -1
  35. package/tests/helpers/math-computation-fake-seam.mjs +135 -0
  36. package/tests/math-computation-shared.test.mjs +504 -0
  37. package/tests/prompt-v5-integrity.test.mjs +14 -8
  38. package/tests/selfdrive-v5.mjs +55 -4
  39. package/vibe-math-v2/agent.cordis.yml +36 -14
  40. package/vibe-math-v2/math-computation.js +936 -0
  41. package/vibe-math-v2/math-engines.js +156 -0
  42. package/vibe-math-v2/vibe-math-v2.js +1653 -48
  43. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  44. package/vibe-math-v3/agent.cordis.yml +30 -12
  45. package/vibe-math-v3/math-computation.js +936 -0
  46. package/vibe-math-v3/math-engines.js +156 -0
  47. package/vibe-math-v3/vibe-math-v3.js +1623 -53
  48. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  49. package/vibe-math-v4/agent.cordis.yml +44 -8
  50. package/vibe-math-v4/math-computation.js +936 -0
  51. package/vibe-math-v4/math-engines.js +156 -0
  52. package/vibe-math-v4/vibe-math-v4.js +1592 -37
  53. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  54. package/vibe-math-v5/agent.cordis.yml +46 -10
  55. package/vibe-math-v5/math-computation.js +936 -0
  56. package/vibe-math-v5/math-engines.js +156 -0
  57. package/vibe-math-v5/vibe-math-v5.js +2233 -42
  58. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +153 -4
  59. 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
@@ -113,6 +114,10 @@ const scrub = (s) => canonBrief(String(s == null ? '' : s)
113
114
  .replace(/("at"\s*:\s*)\d{10,16}/g, '$1"<TIME>"')
114
115
  .replace(/("childId"\s*:\s*")c\d+(")/g, '$1<CHILD>$2')
115
116
  .replace(/("free_slots"\s*:\s*)\d+/g, '$1<SLOTS>')
117
+ // Epoch-ms in a verification-log FILE NAME (`Logs/Verification/<rId>_<Date.now()>.json`) is run
118
+ // metadata too: the final-paper material embeds the evidence index verbatim, so without this the
119
+ // shipped corpus changed on every run (contract §10 item 10 demands byte determinism).
120
+ .replace(/_[0-9]{10,16}\.json/g, '_<TIME>.json')
116
121
 
117
122
  // A fake Lean: a file PASSES unless it still contains `sorry` or the marker `-- FAIL`.
118
123
  // `-- HANG` simulates a toolchain that never returns (the timeout path).
@@ -183,7 +188,7 @@ const ctx = {
183
188
  return undefined
184
189
  },
185
190
  on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
186
- effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
191
+ effect(fn) { const d = fn(); const off = () => { if (typeof d === 'function') d() }; disposers.push(off); return off },
187
192
  logger: { info() {}, warn() {}, error() {} },
188
193
  tools: { register(spec) { toolRegs.push(spec); return () => {} } },
189
194
  commands: { register(spec) { cmdRegs.push(spec); return () => {} } },
@@ -225,6 +230,12 @@ const ctx = {
225
230
 
226
231
  const mod = await import(PLUGIN.href + '?t=' + Date.now())
227
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')
228
239
 
229
240
  // ---------------------------------------------------------------
230
241
  // driving helpers
@@ -514,7 +525,7 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
514
525
  assert(/ok/.test(runGood.stdout), 'the compiler stdout is returned')
515
526
  const last = leanRuns[leanRuns.length - 1]
516
527
  assert(last.cwd.replace(/\\/g, '/') === toolProj.replace(/\\/g, '/'), 'the toolchain runs with the PROJECT root as cwd')
517
- assert(last.argv.join(' ') === 'lean', 'by default the argv is just the executable (got ' + last.argv.join(' ') + ')')
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(' ') + ')')
518
529
  assert(last.graceMs === 120000, 'the default leanTimeoutMs is passed down as graceMs (' + last.graceMs + ')')
519
530
  }
520
531
  {
@@ -588,7 +599,7 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
588
599
  await callTool('vibe_math_set_params', { leanCommand: 'lake', leanArgs: ['env', 'lean'] }, RE)
589
600
  const runLake = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
590
601
  assert(runLake.ok === true, 'lake env lean works through leanCommand/leanArgs')
591
- assert(leanRuns[leanRuns.length - 1].argv.join(' ') === 'lake env lean', 'the argv is [exe, ...leanArgs, file] (got ' + leanRuns[leanRuns.length - 1].argv.join(' ') + ')')
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(' ') + ')')
592
603
  await callTool('vibe_math_set_params', { leanCommand: 'lean', leanArgs: [] }, RE)
593
604
  }
594
605
 
@@ -605,7 +616,7 @@ const provedPath = join(VIBE, 'Formal', 'Proved')
605
616
  assert(!existsSync(join(toolProj, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the project tree')
606
617
  assert(defRes.run && defRes.run.ok === true, 'the reusable definition is executed on archive (run defaults to true)')
607
618
  const libIdx = readIf(join(libPath, 'Index.md'))
608
- 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)')
609
620
  assert(/ZMod5/.test(libIdx) && /Lib\/ZMod5\.lean/.test(libIdx) && /\| ok \|/.test(libIdx), 'Lib/Index.md lists the new definition with its run result')
610
621
  assert(!/ZMod5/.test(readIf(join(toolProj, 'Formal', 'Index.md'))), 'the global definition does not pollute the project formal index')
611
622
  }
@@ -1220,6 +1231,266 @@ section('9 the office can audit formal strength')
1220
1231
  assert(existsSync(join(VIBE, 'Formal', 'Proved', 'Index.md')) && existsSync(join(libPath, 'Index.md')), 'both global indexes are framework-maintained')
1221
1232
  }
1222
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
+
1223
1494
  // ===============================================================
1224
1495
  // 10. interaction corpus (AUDIT-CHECKLIST §2.4) — a HUMAN must be able to re-read
1225
1496
  // every prompt the framework emitted, not just the assertions about them.