dsh-vibe-math 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.en.md +161 -389
  2. package/README.md +161 -390
  3. package/cordis.patch.yml +62 -14
  4. package/docs/AUDIT-CHECKLIST.md +2 -2
  5. package/docs/final-paper.md +1 -0
  6. package/docs/formal-verification.md +184 -29
  7. package/docs/math-computation.md +163 -0
  8. package/docs/release-notes/RELEASE-NOTES-2.7.0.en.md +151 -0
  9. package/docs/release-notes/RELEASE-NOTES-2.7.0.md +115 -0
  10. package/docs/release-notes/RELEASE-NOTES-2.7.1.en.md +86 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.7.1.md +86 -0
  12. package/docs/test-timing.md +24 -12
  13. package/installer.js +7 -4
  14. package/package.json +21 -3
  15. package/prompt-corpus-persona/persona-corpus.json +12 -12
  16. package/prompt-corpus-persona/persona-corpus.md +128 -32
  17. package/prompt-corpus-v2/formal-verify-v2.json +182 -92
  18. package/prompt-corpus-v2/formal-verify-v2.md +2379 -55
  19. package/prompt-corpus-v3/formal-verify-v3.json +215 -53
  20. package/prompt-corpus-v3/formal-verify-v3.md +2147 -22
  21. package/prompt-corpus-v4/formal-verify-v4.json +14 -14
  22. package/prompt-corpus-v4/formal-verify-v4.md +267 -14
  23. package/prompt-corpus-v5/prompt-corpus-v5.json +62 -62
  24. package/prompt-corpus-v5/prompt-corpus-v5.md +631 -3
  25. package/tests/audit-math-computation-contract.mjs +204 -0
  26. package/tests/audit-math-computation-parity.mjs +198 -0
  27. package/tests/audit-math-computation-sensitivity.mjs +231 -0
  28. package/tests/audit-persona-sensitivity.mjs +4 -4
  29. package/tests/audit-persona-surface.test.mjs +38 -10
  30. package/tests/audit-v5-integrity.mjs +14 -0
  31. package/tests/e2e-v5-round2.test.mjs +459 -2
  32. package/tests/formal-verify-v2.test.mjs +362 -8
  33. package/tests/formal-verify-v3.test.mjs +355 -6
  34. package/tests/formal-verify-v4.test.mjs +295 -6
  35. package/tests/formal-verify-v5.test.mjs +9 -1
  36. package/tests/helpers/math-computation-fake-seam.mjs +163 -0
  37. package/tests/math-computation-shared.test.mjs +806 -0
  38. package/tests/prompt-v5-integrity.test.mjs +5 -5
  39. package/vibe-math-v2/agent.cordis.yml +22 -8
  40. package/vibe-math-v2/math-computation.js +1279 -0
  41. package/vibe-math-v2/math-engines.js +197 -0
  42. package/vibe-math-v2/vibe-math-v2.js +853 -40
  43. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  44. package/vibe-math-v3/agent.cordis.yml +22 -8
  45. package/vibe-math-v3/math-computation.js +1279 -0
  46. package/vibe-math-v3/math-engines.js +197 -0
  47. package/vibe-math-v3/vibe-math-v3.js +844 -47
  48. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  49. package/vibe-math-v4/agent.cordis.yml +38 -6
  50. package/vibe-math-v4/math-computation.js +1279 -0
  51. package/vibe-math-v4/math-engines.js +197 -0
  52. package/vibe-math-v4/vibe-math-v4.js +816 -31
  53. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  54. package/vibe-math-v5/agent.cordis.yml +42 -6
  55. package/vibe-math-v5/math-computation.js +1279 -0
  56. package/vibe-math-v5/math-engines.js +197 -0
  57. package/vibe-math-v5/vibe-math-v5.js +1132 -37
  58. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +83 -3
  59. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
@@ -22,10 +22,14 @@
22
22
  import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs'
23
23
  import { tmpdir } from 'node:os'
24
24
  import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
25
- import { fileURLToPath } from 'node:url'
25
+ import { fileURLToPath, pathToFileURL } from 'node:url'
26
26
 
27
+ // V3_PLUGIN may be a REPO-RELATIVE path or an ABSOLUTE one (a mutant copy outside the repo, e.g. a
28
+ // temp dir). The previous `new URL('file:///' + path)` form only worked for drive-qualified Windows
29
+ // paths and made out-of-repo variants impossible; this is the same isAbsolute-aware resolution the
30
+ // `math-computation-v*` suites use. Default behaviour (env var unset) is unchanged.
27
31
  const PLUGIN = process.env.V3_PLUGIN
28
- ? new URL('file:///' + String(process.env.V3_PLUGIN).replace(/\\/g, '/'))
32
+ ? pathToFileURL(isAbsolute(process.env.V3_PLUGIN) ? process.env.V3_PLUGIN : join(dirname(fileURLToPath(import.meta.url)), '..', process.env.V3_PLUGIN))
29
33
  : new URL('../vibe-math-v3/vibe-math-v3.js', import.meta.url)
30
34
  const HERE = dirname(fileURLToPath(import.meta.url))
31
35
  // AUDIT-CHECKLIST §2.4: the suite must also keep the INTERACTION TEXT it drove, so a human can
@@ -55,6 +59,7 @@ const wakes = [] // { childId, rootId, prompt }
55
59
  const interrupts = []
56
60
  const leanRuns = [] // { argv, file, cwd, graceMs }
57
61
  const terminated = []
62
+ const disposers = []
58
63
  const roots = []
59
64
  let rootSeq = 0
60
65
  let subprocessAvailable = true
@@ -187,7 +192,7 @@ const ctx = {
187
192
  return undefined
188
193
  },
189
194
  on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
190
- effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
195
+ effect(fn) { const d = fn(); const off = () => { if (typeof d === 'function') d() }; disposers.push(off); return off },
191
196
  logger: { info() {}, warn() {}, error() {} },
192
197
  tools: { register(spec) { toolRegs.push(spec); return () => {} } },
193
198
  commands: { register(spec) { cmdRegs.push(spec); return () => {} } },
@@ -229,6 +234,12 @@ const ctx = {
229
234
 
230
235
  const mod = await import(PLUGIN.href + '?t=' + Date.now())
231
236
  ;(mod.default || mod).apply(ctx)
237
+ // leanAsync defaults to TRUE (background queue). The sections above guard the **synchronous** Lean
238
+ // path verbatim, so pin leanAsync=false through the workspace-level settings file (every
239
+ // setProject → loadSettings() picks it up). Section 9b flips it back on explicitly for the async
240
+ // state machine. Nothing else in the suite depends on the async default.
241
+ mkdirSync(VIBE, { recursive: true })
242
+ writeFileSync(join(VIBE, 'vibe_math_setting.json'), '{ "leanAsync": false }\n', 'utf8')
232
243
 
233
244
  // ---------------------------------------------------------------
234
245
  // driving helpers
@@ -518,7 +529,7 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
518
529
  assert(/ok/.test(runGood.stdout), 'the compiler stdout is returned')
519
530
  const last = leanRuns[leanRuns.length - 1]
520
531
  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 just the executable (got ' + last.argv.join(' ') + ')')
532
+ 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
533
  assert(last.graceMs === 120000, 'the default leanTimeoutMs is passed down as graceMs (' + last.graceMs + ')')
523
534
  }
524
535
  {
@@ -592,7 +603,7 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
592
603
  await callTool('vibe_math_set_params', { leanCommand: 'lake', leanArgs: ['env', 'lean'] }, RE)
593
604
  const runLake = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
594
605
  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, file] (got ' + leanRuns[leanRuns.length - 1].argv.join(' ') + ')')
606
+ 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
607
  await callTool('vibe_math_set_params', { leanCommand: 'lean', leanArgs: [] }, RE)
597
608
  }
598
609
 
@@ -609,7 +620,7 @@ const provedPath = join(VIBE, 'Formal', 'Proved')
609
620
  assert(!existsSync(join(toolProj, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the project tree')
610
621
  assert(defRes.run && defRes.run.ok === true, 'the reusable definition is executed on archive (run defaults to true)')
611
622
  const libIdx = readIf(join(libPath, 'Index.md'))
612
- assert(/\| 名称 \| 文件 \| 类别 \| 摘要 \| 最近运行 \|/.test(libIdx), 'Lib/Index.md uses the contract columns')
623
+ assert(/\| 名称 \| 文件 \| 类别 \| 依赖(import) \| 摘要 \| 最近运行 \|/.test(libIdx), 'Lib/Index.md uses the contract columns (incl. the import-dependency column, spec §4.4)')
613
624
  assert(/ZMod5/.test(libIdx) && /Lib\/ZMod5\.lean/.test(libIdx) && /\| ok \|/.test(libIdx), 'Lib/Index.md lists the new definition with its run result')
614
625
  assert(!/ZMod5/.test(readIf(join(toolProj, 'Formal', 'Index.md'))), 'the global definition does not pollute the project formal index')
615
626
  }
@@ -1224,10 +1235,348 @@ section('9 the office can audit formal strength')
1224
1235
  assert(existsSync(join(VIBE, 'Formal', 'Proved', 'Index.md')) && existsSync(join(libPath, 'Index.md')), 'both global indexes are framework-maintained')
1225
1236
  }
1226
1237
 
1238
+ // ===============================================================
1239
+ // 9b. Lean 增量 + 异步(spec §1–§4 + 修订 §1–§5):与 v2 同一套断言强度
1240
+ // ===============================================================
1241
+ section('9b Lean 增量 + 异步:参数/argv/队列/去重/只读面/主动性/恢复/卸载')
1242
+ {
1243
+ const H = mod.__testHelpers
1244
+ const slash = (s) => String(s == null ? '' : s).replace(/\\/g, '/')
1245
+ const SAME = (a, b) => slash(a) === slash(b)
1246
+ const formalOf2 = (proj) => JSON.parse(readIf(join(projRoot(proj), 'State', 'formal.json')) || '{}')
1247
+ const jobFile = (proj, id) => join(projRoot(proj), 'Formal', 'Jobs', id + '.json')
1248
+ const readJob = (proj, id) => JSON.parse(readIf(jobFile(proj, id)) || '{}')
1249
+ const vibeCmd = async (rawInput, root) => JSON.parse(await cmdRegs[0].handler({ rawInput, agent: root }))
1250
+ const findJob = async (proj, id, states, tries = 100, ms = 60) => {
1251
+ 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) }
1252
+ return undefined
1253
+ }
1254
+
1255
+ // ── 参数:默认值/归一化(字符串 'false' 绝不能滑过去)────────────────────────────
1256
+ const rp = makeRoot()
1257
+ await callTool('vibe_math_new_project', { name: 'lean-async-params' }, rp)
1258
+ await callTool('vibe_math_set_params', { leanAsync: true }, rp)
1259
+ const bad = await callTool('vibe_math_set_params', { leanAsync: 'false' }, rp)
1260
+ 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')
1261
+ const wp = await callTool('vibe_math_set_params', { leanInitiative: 'banana', leanJobsMaxParallel: 0, leanSearchPaths: 'not-an-array' }, rp)
1262
+ assert(wp.params.leanInitiative === 'normal', 'leanInitiative rejects an unknown mode back to normal')
1263
+ assert(wp.params.leanJobsMaxParallel === 1, 'leanJobsMaxParallel floors at 1 (never 0)')
1264
+ assert(Array.isArray(wp.params.leanSearchPaths) && wp.params.leanSearchPaths.length === 0, 'leanSearchPaths rejects a non-array back to []')
1265
+ const dd = await callTool('vibe_math_set_params', { leanSearchPaths: [' /opt/lean-lib ', '/opt/lean-lib', ''] }, rp)
1266
+ assert(JSON.stringify(dd.params.leanSearchPaths) === JSON.stringify(['/opt/lean-lib']), 'leanSearchPaths trims, drops empties and de-duplicates')
1267
+
1268
+ // ── 纯函数:搜索路径计划(修订 §2)与指纹(修订 §4)──────────────────────────────
1269
+ const planDefault = H.leanSearchPathPlan([], [], '/root/vm')
1270
+ assert(JSON.stringify(planDefault.paths) === JSON.stringify(['/root/vm']) && planDefault.inject.length === 2, '★ the default plan injects exactly the automatic root')
1271
+ const planExtra = H.leanSearchPathPlan(['env', 'lean'], ['/a', '/b'], '/root/vm')
1272
+ assert(JSON.stringify(planExtra.paths) === JSON.stringify(['/a', '/b', '/root/vm']), '★★ user leanSearchPaths come FIRST, then the automatic root')
1273
+ 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)')
1274
+ const ctxA = H.leanBuildContext('lean', ['a'], ['/p'])
1275
+ const ctxB = H.leanBuildContext('lean', ['b'], ['/p'])
1276
+ assert(H.leanJobFingerprint('x\n', ctxA) !== H.leanJobFingerprint('x\n', ctxB), '★ the job fingerprint covers the build context (engine/args/search paths)')
1277
+
1278
+ // ── argv:注入在用户参数之后、文件名之前 ─────────────────────────────────────────
1279
+ const ra = makeRoot()
1280
+ await callTool('vibe_math_new_project', { name: 'lean-async-argv' }, ra)
1281
+ await callTool('vibe_math_set_params', { leanAsync: false, leanCommand: 'lake', leanArgs: ['env', 'lean'] }, ra)
1282
+ const projA = projRoot('lean-async-argv')
1283
+ mkdirSync(join(projA, 'Formal'), { recursive: true })
1284
+ writeFileSync(join(projA, 'Formal', 'srch.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
1285
+ const runs0 = leanRuns.length
1286
+ await callTool('vibe_math_lean_run', { file: 'Formal/srch.lean' }, ra)
1287
+ const argv1 = leanRuns[leanRuns.length - 1].argv
1288
+ 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)')
1289
+ 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) + ')')
1290
+ await callTool('vibe_math_set_params', { leanArgs: ['env', 'lean', '--search-path', '/explicit'] }, ra)
1291
+ await callTool('vibe_math_lean_run', { file: 'Formal/srch.lean' }, ra)
1292
+ const argv2 = leanRuns[leanRuns.length - 1].argv
1293
+ 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))
1294
+ await callTool('vibe_math_set_params', { leanArgs: ['env', 'lean'], leanSearchPaths: ['/extra1', '/extra2'] }, ra)
1295
+ await callTool('vibe_math_lean_run', { file: 'Formal/srch.lean' }, ra)
1296
+ const argv3 = leanRuns[leanRuns.length - 1].argv
1297
+ 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)) + ')')
1298
+
1299
+ // ── 异步状态机:入队立即返回 → 心跳排空 → 只有 settled(ok) 才是通过 ───────────────
1300
+ const rq = makeRoot()
1301
+ await callTool('vibe_math_new_project', { name: 'lean-async-q' }, rq)
1302
+ await callTool('vibe_math_set_params', { leanAsync: true }, rq)
1303
+ const projQ = projRoot('lean-async-q')
1304
+ mkdirSync(join(projQ, 'Formal'), { recursive: true })
1305
+ writeFileSync(join(projQ, 'Formal', 'async-good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
1306
+ writeFileSync(join(projQ, 'Formal', 'async-bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
1307
+ const nBefore = leanRuns.length
1308
+ const q1 = await callTool('vibe_math_lean_run', { file: 'Formal/async-good.lean', target: 'pAsync' }, rq)
1309
+ 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) + ')')
1310
+ assert(leanRuns.length === nBefore, '★★ the tool call itself did NOT block on the compiler (enqueued, not run)')
1311
+ assert(q1.async.jobId.indexOf('pAsync-') === 0, 'the jobId is <target>-<fingerprint[0:12]> (got ' + q1.async.jobId + ')')
1312
+ assert(existsSync(jobFile('lean-async-q', q1.async.jobId)), 'the job record is mirrored to <project>/Formal/Jobs/<jobId>.json')
1313
+ const rec0 = readJob('lean-async-q', q1.async.jobId)
1314
+ 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')
1315
+ assert((formalOf2('lean-async-q').records || {}).pAsync === undefined || formalOf2('lean-async-q').records.pAsync.status !== 'passed', '★★ a queued job never marks the object passed')
1316
+ const done1 = await findJob('lean-async-q', q1.async.jobId, ['settled'])
1317
+ assert(!!done1, 'the apply-level heartbeat drained the queue and settled the job (got ' + readJob('lean-async-q', q1.async.jobId).state + ')')
1318
+ assert(done1 && done1.exitCode === 0, 'the settled record keeps exitCode 0')
1319
+ const recAfter = (formalOf2('lean-async-q').records || {}).pAsync
1320
+ assert(!!recAfter && recAfter.status === 'attempted' && recAfter.run && recAfter.run.ok === true, 'a green RUN lands as attempted with run.ok=true')
1321
+ assert(!!recAfter.async && recAfter.async.state === 'settled' && recAfter.async.jobId === q1.async.jobId, 'the object record carries the async job pointer/state')
1322
+ const q2 = await callTool('vibe_math_lean_run', { file: 'Formal/async-bad.lean', target: 'pAsyncBad' }, rq)
1323
+ const done2 = await findJob('lean-async-q', q2.async.jobId, ['failed'])
1324
+ assert(!!done2 && done2.exitCode === 1, '★ a red async run settles as failed with exitCode 1')
1325
+ const recBad = (formalOf2('lean-async-q').records || {}).pAsyncBad
1326
+ 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)')
1327
+ // 归档证明:落地前不是 passed,落地 ok 才写 Verified/Lean/
1328
+ const q3 = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'pAsymProof', content: 'theorem pAsymProof : 1 = 1 := rfl\n' }, rq)
1329
+ assert(q3.ok === true && q3.async && q3.async.state === 'queued' && q3.status === 'attempted', '★★ kind=proof in async档 returns queued + attempted (NOT passed)')
1330
+ assert(!existsSync(join(projQ, 'Verified', 'Lean', 'pAsymProof.lean')), '★★ the archived proof file does NOT exist while the job is still queued')
1331
+ const done3 = await findJob('lean-async-q', q3.async.jobId, ['settled', 'failed'])
1332
+ assert(!!done3 && done3.state === 'settled', 'the proof job settled green (got ' + (done3 && done3.state) + ')')
1333
+ assert(existsSync(join(projQ, 'Verified', 'Lean', 'pAsymProof.lean')), '★★ only after settled(ok) is Verified/Lean/<id>.lean written')
1334
+ assert(((formalOf2('lean-async-q').records || {}).pAsymProof || {}).status === 'passed', '★★ and only then does the object become passed')
1335
+ assert(/pAsymProof/.test(readIf(join(projQ, 'Formal', 'Index.md'))), 'the index reflects the passed object')
1336
+ // ── lean_job(修订 §3):清单/单查/waitMs ────────────────────────────────────────
1337
+ const listJobs = await callTool('vibe_math_lean_job', {}, rq)
1338
+ assert(listJobs.ok === true && Array.isArray(listJobs.jobs) && listJobs.jobs.length >= 3, '★ lean_job without jobId returns the session job list')
1339
+ assert(listJobs.jobs.every((j) => !!j.paths && typeof j.paths.receipt === 'string'), 'every listed job carries its receipt path')
1340
+ const one = await callTool('vibe_math_lean_job', { jobId: q3.async.jobId }, rq)
1341
+ 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')
1342
+ assert((await callTool('vibe_math_lean_job', { jobId: 'nope-000000000000' }, rq)).code === 'V3_NOT_FOUND', 'an unknown jobId is refused with a typed code')
1343
+ const q4 = await callTool('vibe_math_lean_run', { file: 'Formal/async-bad.lean', target: 'pAsyncBad' }, rq)
1344
+ const waited = await callTool('vibe_math_lean_job', { jobId: q4.async.jobId, waitMs: 6000 }, rq)
1345
+ 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 + ')')
1346
+ assert((await callTool('vibe_math_lean_lib', {}, rq)).paths.searchPath === VIBE.replace(/\\/g, '/'), '★ lean_lib exposes paths.searchPath = the VibeMath root')
1347
+ assert(Array.isArray((await callTool('vibe_math_lean_lib', {}, rq)).jobs), '★ lean_lib exposes the job list')
1348
+
1349
+ // ── 超时:leanTimeoutMs 是每个作业的预算,主动 terminate,绝不 passed ─────────────
1350
+ const rt = makeRoot()
1351
+ await callTool('vibe_math_new_project', { name: 'lean-async-timeout' }, rt)
1352
+ await callTool('vibe_math_set_params', { leanAsync: true, leanTimeoutMs: 1000 }, rt)
1353
+ const projT = projRoot('lean-async-timeout')
1354
+ mkdirSync(join(projT, 'Formal'), { recursive: true })
1355
+ writeFileSync(join(projT, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1 := rfl\n', 'utf8')
1356
+ const termBefore = terminated.length
1357
+ const q5 = await callTool('vibe_math_lean_run', { file: 'Formal/hang.lean', target: 'pHang' }, rt)
1358
+ const done5 = await findJob('lean-async-timeout', q5.async.jobId, ['timeout'], 150)
1359
+ assert(!!done5 && done5.timedOut === true, '★★ a hanging job times out (state=timeout) with leanTimeoutMs as its budget')
1360
+ assert(terminated.length > termBefore, '★★ the timeout ACTIVELY called handle.terminate()')
1361
+ assert(((formalOf2('lean-async-timeout').records || {}).pHang || {}).status !== 'passed', '★ a timed-out job never sets passed')
1362
+
1363
+ // ── 并发参数(修订 §5):leanJobsMaxParallel>1 时两个作业同时进入 running ──────────
1364
+ const rc = makeRoot()
1365
+ await callTool('vibe_math_new_project', { name: 'lean-async-par' }, rc)
1366
+ await callTool('vibe_math_set_params', { leanAsync: true, leanJobsMaxParallel: 2, leanTimeoutMs: 2500 }, rc)
1367
+ const projC = projRoot('lean-async-par')
1368
+ mkdirSync(join(projC, 'Formal'), { recursive: true })
1369
+ writeFileSync(join(projC, 'Formal', 'h1.lean'), '-- HANG\ntheorem h1 : 1 = 1 := rfl\n', 'utf8')
1370
+ writeFileSync(join(projC, 'Formal', 'h2.lean'), '-- HANG\ntheorem h2 : 1 = 1 := rfl\n', 'utf8')
1371
+ const j1 = await callTool('vibe_math_lean_run', { file: 'Formal/h1.lean', target: 'pH1' }, rc)
1372
+ const j2 = await callTool('vibe_math_lean_run', { file: 'Formal/h2.lean', target: 'pH2' }, rc)
1373
+ let both = false
1374
+ for (let i = 0; i < 120 && !both; i++) {
1375
+ const a = readJob('lean-async-par', j1.async.jobId)
1376
+ const b = readJob('lean-async-par', j2.async.jobId)
1377
+ both = (a.state === 'running' && b.state === 'running')
1378
+ if (!both) await sleep(50)
1379
+ }
1380
+ assert(both, '★★ with leanJobsMaxParallel=2 BOTH queued jobs are in running at the same time (concurrency is a parameter, not a constant)')
1381
+ for (const k of [j1, j2]) { try { await callTool('vibe_math_lean_job', { jobId: k.async.jobId, waitMs: 3000 }, rc) } catch (e) { /* ignore */ } }
1382
+
1383
+ // ── 去重(§4.3)+ 构建上下文进指纹(修订 §4)─────────────────────────────────────
1384
+ const rd = makeRoot()
1385
+ await callTool('vibe_math_new_project', { name: 'lean-async-dedupe' }, rd)
1386
+ await callTool('vibe_math_set_params', { leanAsync: true, leanCommand: 'lake', leanArgs: ['env', 'lean'] }, rd)
1387
+ const defBody = 'def reusableOne : Nat := 1\n'
1388
+ const r1 = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: defBody }, rd)
1389
+ assert(r1.ok === true && r1.async && r1.deduped === false, 'the first archive of a definition is enqueued (not deduped)')
1390
+ await findJob('lean-async-dedupe', r1.async.jobId, ['settled'])
1391
+ const runsAfter1 = leanRuns.length
1392
+ const r2 = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: defBody }, rd)
1393
+ assert(r2.ok === true && r2.deduped === true && r2.async === undefined, '★★★ re-archiving identical content is DEDUPED (no job, no recompile)')
1394
+ assert(leanRuns.length === runsAfter1, '★★ the deduped archive did NOT recompile')
1395
+ await callTool('vibe_math_set_params', { leanArgs: ['env', 'lean', '--threads=2'] }, rd)
1396
+ const r5 = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: defBody }, rd)
1397
+ 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)')
1398
+ if (r5.async) await findJob('lean-async-dedupe', r5.async.jobId, ['settled', 'failed'])
1399
+ await callTool('vibe_math_set_params', { leanArgs: ['env', 'lean'] }, rd)
1400
+ const changed = defBody + '\n-- 内容变了\n'
1401
+ const r3 = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: changed }, rd)
1402
+ assert(r3.ok === true && r3.deduped === false && !!r3.async, 'a changed body is NOT deduped (content hash differs)')
1403
+ if (r3.async) await findJob('lean-async-dedupe', r3.async.jobId, ['settled', 'failed'])
1404
+ const r6 = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: changed }, rd)
1405
+ assert(r6.ok === true && r6.deduped === true, 'and the changed body dedupes on its own (content-based, not "first wins")')
1406
+
1407
+ // ── 崩溃恢复(§2.6 + 修订 §4):绝不置 passed ────────────────────────────────────
1408
+ const rr = makeRoot()
1409
+ await callTool('vibe_math_new_project', { name: 'lean-async-recover' }, rr)
1410
+ await callTool('vibe_math_set_params', { leanAsync: true }, rr)
1411
+ const projR = projRoot('lean-async-recover')
1412
+ mkdirSync(join(projR, 'Formal', 'Jobs'), { recursive: true })
1413
+ const goodBody = 'theorem recGood : 1 = 1 := rfl\n'
1414
+ writeFileSync(join(projR, 'Formal', 'recGood.lean'), goodBody, 'utf8')
1415
+ const ctxG = H.leanBuildContext('lean', [], [slash(VIBE)])
1416
+ const fpG = H.leanJobFingerprint(goodBody, ctxG)
1417
+ const jobIdG = H.leanJobId('pRecGood', fpG)
1418
+ writeFileSync(jobFile('lean-async-recover', jobIdG), JSON.stringify({
1419
+ jobId: jobIdG, kind: 'archive', state: 'running', attempts: 1, target: 'pRecGood', name: 'pRecGood',
1420
+ rel: 'Formal/recGood.lean', scope: 'project', sha256: H.leanContentSha(goodBody), buildSha256: fpG, buildContext: ctxG,
1421
+ engine: 'lean', argv: [], searchPaths: [slash(VIBE)], archiveKind: 'proof', enqueuedAt: 1, startedAt: 2, settledAt: 0, exitCode: null,
1422
+ }), 'utf8')
1423
+ const otherJob = H.leanJobId('pRecOther', H.leanJobFingerprint('theorem other : 1 = 1 := rfl\n', ctxG))
1424
+ writeFileSync(jobFile('lean-async-recover', otherJob), JSON.stringify({
1425
+ jobId: otherJob, kind: 'archive', state: 'running', attempts: 3, target: 'pRecOther', name: 'pRecOther',
1426
+ rel: 'Formal/recMissing.lean', scope: 'project', sha256: H.leanContentSha('theorem other : 1 = 1 := rfl\n'), buildSha256: 'ffffffffffff', buildContext: ctxG,
1427
+ engine: 'lean', argv: [], searchPaths: [slash(VIBE)], archiveKind: 'proof', enqueuedAt: 1, startedAt: 2, settledAt: 0, exitCode: null,
1428
+ }), 'utf8')
1429
+ await callTool('vibe_math_new_project', { name: 'lean-async-recover' }, rr) // setProject ⇒ recoverLeanJobs()
1430
+ const recsR = formalOf2('lean-async-recover').records || {}
1431
+ assert((recsR.pRecGood || {}).status === 'attempted', '★★ recovery marks a crashed running job as attempted (never passed)')
1432
+ assert((recsR.pRecOther || {}).status === 'attempted', 'recovery marks the hash-mismatched job object as attempted too')
1433
+ assert(!existsSync(join(projR, 'Verified', 'Lean', 'pRecGood.lean')), '★ recovery never writes an archived proof for a crashed job')
1434
+ const afterG = readJob('lean-async-recover', jobIdG)
1435
+ 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 }) + ')')
1436
+ const afterO = readJob('lean-async-recover', otherJob)
1437
+ assert(afterO.state === 'interrupted' && Number(afterO.attempts) === 3, '★★ the mismatched job is only marked interrupted (NOT re-driven, attempts unchanged)')
1438
+ assert(!!(await findJob('lean-async-recover', jobIdG, ['settled'])), 'the re-enqueued job then settles normally on the heartbeat')
1439
+
1440
+ // ── lean_read(§1.2):只读、路径守卫、64KB 截断 ─────────────────────────────────
1441
+ mkdirSync(join(VIBE, 'Formal', 'Lib'), { recursive: true })
1442
+ writeFileSync(join(VIBE, 'Formal', 'Lib', 'recGood.lean'), goodBody, 'utf8')
1443
+ const rd1 = await callTool('vibe_math_lean_read', { name: 'recGood' }, rr)
1444
+ 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')
1445
+ 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') + ')')
1446
+ assert((await callTool('vibe_math_lean_read', { name: 'recGood', kind: 'proved' }, rr)).code === 'V3_NOT_FOUND', 'kind=proved looks only in Formal/Proved')
1447
+ for (const badName of ['../etc/passwd', 'a/b', 'C:\\x\\y', '..']) {
1448
+ const rbad = await callTool('vibe_math_lean_read', { name: badName }, rr)
1449
+ assert(rbad.ok === false && rbad.code === 'V3_INVALID_ARGUMENT', '★ lean_read rejects the path-escape attempt ' + JSON.stringify(badName))
1450
+ }
1451
+ writeFileSync(join(VIBE, 'Formal', 'Lib', 'bigOne.lean'), '-- ' + 'x'.repeat(70 * 1024) + '\n', 'utf8')
1452
+ const rbig = await callTool('vibe_math_lean_read', { name: 'bigOne' }, rr)
1453
+ assert(rbig.ok === true && rbig.truncated === true && Buffer.byteLength(rbig.text, 'utf8') <= 64 * 1024, '★ lean_read truncates at 64KB and says so (truncated=true)')
1454
+ writeFileSync(join(VIBE, 'Formal', 'Lib', 'depUser.lean'), 'import Formal.Lib.bigOne\n\ndef depUser := 1\n', 'utf8')
1455
+ await callTool('vibe_math_lean_lib', {}, rr)
1456
+ const libIdx = readIf(join(VIBE, 'Formal', 'Lib', 'Index.md'))
1457
+ 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, ' | ') + ')')
1458
+
1459
+ // ── 主动性档位(修订 §1 + v5 统一):提示词读 leanInitiative,不是 formalVerify ─────────
1460
+ // 第 4 项为 null = "日线完全不出现";第 6 项是该行的 formalVerify 档(默认 encourage)。
1461
+ for (const [mode, rootName, projName, want, notWant, fv] of [
1462
+ ['normal', 'lean-init-normal', 'lean-init-normal-proj', /主动性 normal/, null, 'encourage'],
1463
+ ['eager', 'lean-init-eager', 'lean-init-eager-proj', /主动档(leanInitiative=eager)/, null, 'encourage'],
1464
+ ['off', 'lean-init-off', 'lean-init-off-proj', null, /顺手形式化/, 'encourage'],
1465
+ // formalVerify:'off' + eager:日线仍要注入(主动性是独立轴),验证阶段文本仍关闭。
1466
+ ['eager', 'lean-init-off-eager', 'lean-init-off-eager-proj', /主动档(leanInitiative=eager)/, /【Lean 形式化验证(/, 'off'],
1467
+ ]) {
1468
+ const rw = makeRoot()
1469
+ await callTool('vibe_math_new_project', { name: projName }, rw)
1470
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: fv || 'encourage', leanInitiative: mode }), rw)
1471
+ await callTool('vibe_math_add_problem', { id: 'qInit' + mode + '-' + (fv || 'e'), 陈述: '主动性提示词 ' + mode }, rw)
1472
+ await callTool('vibe_math_start', {}, rw)
1473
+ const got = await drive(rw, () => spawnOf(rw, 'explorer:').length >= 1 || spawnOf(rw, 'solver:').length >= 1, 'work prompt for ' + mode, 12000)
1474
+ const text = promptText(rw)
1475
+ const ok4 = want ? want.test(text) : !/顺手形式化/.test(text)
1476
+ assert(got && ok4, '★★ leanInitiative=' + mode + (fv === 'off' ? ' (formalVerify:off)' : '') + ' is reflected in the work prompt' + (want ? '' : ':off ⇒ 连"顺手形式化"日线都不出现'))
1477
+ if (notWant) assert(!notWant.test(text), 'and the ' + mode + (fv === 'off' ? '/off' : '') + ' work prompt does not carry the verification-phase Lean block (验证要求仍由 formalVerify 表达)')
1478
+ if (fv === 'off') assert(text.indexOf('三条筛选判据') !== -1, '★★★ formalVerify:off + eager 仍带完整日线(三条筛选判据在),不是被 off 静默吞掉')
1479
+ await callTool('vibe_math_pause', {}, rw)
1480
+ }
1481
+
1482
+ // ── 卸载(spec §2.5):terminate + interrupted,绝不 passed ──────────────────────
1483
+ const rz = makeRoot()
1484
+ await callTool('vibe_math_new_project', { name: 'lean-async-dispose' }, rz)
1485
+ await callTool('vibe_math_set_params', { leanAsync: true, leanTimeoutMs: 60000 }, rz)
1486
+ const projZ = projRoot('lean-async-dispose')
1487
+ mkdirSync(join(projZ, 'Formal'), { recursive: true })
1488
+ writeFileSync(join(projZ, 'Formal', 'orphan.lean'), '-- HANG\ntheorem orphan : 1 = 1 := rfl\n', 'utf8')
1489
+ const termZ = terminated.length
1490
+ const qz = await callTool('vibe_math_lean_run', { file: 'Formal/orphan.lean', target: 'pOrphan' }, rz)
1491
+ const startedZ = await findJob('lean-async-dispose', qz.async.jobId, ['running'])
1492
+ assert(!!startedZ, 'the dispose case got a job into running')
1493
+ for (const off of disposers) { try { off() } catch (e) { /* ignore */ } }
1494
+ assert(terminated.length > termZ, '★★ unmounting the plugin terminated the in-flight compile (no orphan process)')
1495
+ assert(((formalOf2('lean-async-dispose').records || {}).pOrphan || {}).status !== 'passed', '★ dispose never marks the object passed')
1496
+ }
1497
+
1227
1498
  // ===============================================================
1228
1499
  // 10. interaction corpus (AUDIT-CHECKLIST §2.4) — a HUMAN must be able to re-read
1229
1500
  // every prompt the framework emitted, not just the assertions about them.
1230
1501
  // ===============================================================
1502
+ // ===============================================================
1503
+ // 5b. round-2 lens-1 RUNTIME: the initiative axis at EVERY v3 site
1504
+ // ===============================================================
1505
+ // Four sites: the three formalDailySection() call sites (explorer / rederive / solver) plus the
1506
+ // INLINE formalWorkLine() site inside methodKeeperPrompt. (1) formalVerify:'off' + leanInitiative:
1507
+ // 'eager' must carry the daily line at that site; (2) leanInitiative:'off' must drop it there while
1508
+ // the verification phase still follows formalVerify. Names carry the site so a single-site
1509
+ // regression names itself.
1510
+ const SITE_DAILY = /三条筛选判据/
1511
+ const SITE_VERIFY = /【Lean 形式化验证(/
1512
+ async function driveSiteV3(rw, opts, qid) {
1513
+ const q = qid
1514
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { methodKeepEvery: 1, maxExplorerRetries: 5 }, opts), rw)
1515
+ await callTool('vibe_math_add_problem', { id: q, 陈述: '站点级运行时用例' }, rw)
1516
+ await callTool('vibe_math_start', {}, rw)
1517
+ const exs = () => spawnOf(rw, 'explorer:' + q)
1518
+ await drive(rw, () => exs().length >= 1, 'explorer site ' + q)
1519
+ const out = { explorer: (exs()[0] || {}).prompt || '' }
1520
+ // answer explorers with directions until the solver appears (v3's fallback scheduler re-derives)
1521
+ fireEnd(exs()[0].childId, { meta: { kind: 'directions', qid: q, directions: [{ id: 'd1', title: 'D', method: 'm', core_assumption: '', feasibility: 0.8 }] } })
1522
+ for (let guard = 0; guard < 30 && !lastSpawn(rw, 'solver:' + q + ':d1'); guard++) {
1523
+ const pend = exs().filter((s) => !firedChildren.has(s.childId)).slice(-1)[0]
1524
+ if (pend) { firedChildren.add(pend.childId); fireEnd(pend.childId, { meta: { kind: 'directions', qid: q, directions: [{ id: 'd1', title: 'D', method: 'm', core_assumption: '', feasibility: 0.8 }] } }) }
1525
+ await pump(rw); await sleep(80)
1526
+ }
1527
+ const so = lastSpawn(rw, 'solver:' + q + ':d1')
1528
+ out.solver = (so && so.prompt) || ''
1529
+ // dead-end + a pending invention: the direction is exhausted (⇒ re-derive) AND the method keeper fires
1530
+ if (so) fireEnd(so.childId, { status: 'dead-end', survival_probability: 0.2, dead_end_reason: '站点用例:不可行', new_inventions: [{ 类型: '工具', 标题: '站点工具', 内容描述: 'x', 是否已入库: false }] })
1531
+ await drive(rw, () => exs().length >= 2, 'rederive site ' + q)
1532
+ out.rederive = (exs()[1] || {}).prompt || ''
1533
+ out.distinct = !!out.explorer && !!out.rederive && out.explorer !== out.rederive
1534
+ await drive(rw, () => !!lastSpawn(rw, 'method-keeper'), 'method-keeper site ' + q)
1535
+ out.methodKeeper = (lastSpawn(rw, 'method-keeper') || {}).prompt || ''
1536
+ await callTool('vibe_math_abort', {}, rw)
1537
+ return out
1538
+ }
1539
+ {
1540
+ const re = makeRoot()
1541
+ await callTool('vibe_math_new_project', { name: 'site-eager-off' }, re)
1542
+ const A = await driveSiteV3(re, { formalVerify: 'off', leanInitiative: 'eager' }, 'qS')
1543
+ assert(SITE_DAILY.test(A.explorer), '★ [explorer site] formalVerify:off + leanInitiative:eager injects the daily line')
1544
+ assert(SITE_DAILY.test(A.solver), '★ [solver site] formalVerify:off + leanInitiative:eager injects the daily line')
1545
+ assert(SITE_DAILY.test(A.rederive), '★ [rederive site] formalVerify:off + leanInitiative:eager injects the daily line')
1546
+ assert(SITE_DAILY.test(A.methodKeeper), '★ [methodKeeper site] formalVerify:off + leanInitiative:eager injects the daily line (inline site)')
1547
+ assert(A.explorer.indexOf('【Lean 形式化验证(') === -1 && A.solver.indexOf('【Lean 形式化验证(') === -1 && A.rederive.indexOf('【Lean 形式化验证(') === -1 && A.methodKeeper.indexOf('【Lean 形式化验证(') === -1, '★ [all sites] formalVerify:off keeps the verification-phase block closed while eager still injects the daily line')
1548
+
1549
+ const ro = makeRoot()
1550
+ await callTool('vibe_math_new_project', { name: 'site-init-off' }, ro)
1551
+ const B = await driveSiteV3(ro, { formalVerify: 'encourage', leanInitiative: 'off' }, 'qO')
1552
+ assert(!SITE_DAILY.test(B.explorer) && !/【顺手形式化/.test(B.explorer), '★ [explorer site] leanInitiative:off drops the daily line')
1553
+ assert(!SITE_DAILY.test(B.solver) && !/【顺手形式化/.test(B.solver), '★ [solver site] leanInitiative:off drops the daily line')
1554
+ assert(!SITE_DAILY.test(B.rederive) && !/【顺手形式化/.test(B.rederive), '★ [rederive site] leanInitiative:off drops the daily line')
1555
+ assert(!SITE_DAILY.test(B.methodKeeper) && !/【顺手形式化/.test(B.methodKeeper), '★ [methodKeeper site] leanInitiative:off drops the daily line (inline site)')
1556
+
1557
+ // verification phase still follows formalVerify under leanInitiative:'off'
1558
+ const rvv = makeRoot()
1559
+ await callTool('vibe_math_new_project', { name: 'site-init-off-verify' }, rvv)
1560
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'encourage', leanInitiative: 'off' }), rvv)
1561
+ await callTool('vibe_math_add_problem', { id: 'qV', 陈述: '验证段仍在' }, rvv)
1562
+ await callTool('vibe_math_start', {}, rvv)
1563
+ await drive(rvv, () => !!lastSpawn(rvv, 'explorer:qV'), 'explorer qV')
1564
+ const exv = lastSpawn(rvv, 'explorer:qV')
1565
+ assert(!!exv && !SITE_DAILY.test(exv.prompt || ''), '★ [explorer site] 第二个 root:initiative off 时 explorer 仍无日线')
1566
+ if (exv) fireEnd(exv.childId, { meta: { kind: 'directions', qid: 'qV', directions: [{ id: 'd1', title: 'D', method: 'm', core_assumption: '', feasibility: 0.8 }] } })
1567
+ for (let guard = 0; guard < 30 && !lastSpawn(rvv, 'solver:qV:d1'); guard++) {
1568
+ const pend = spawnOf(rvv, 'explorer:qV').filter((s) => !firedChildren.has(s.childId)).slice(-1)[0]
1569
+ if (pend) { firedChildren.add(pend.childId); fireEnd(pend.childId, { meta: { kind: 'directions', qid: 'qV', directions: [{ id: 'd1', title: 'D', method: 'm', core_assumption: '', feasibility: 0.8 }] } }) }
1570
+ await pump(rvv); await sleep(80)
1571
+ }
1572
+ const sovv = lastSpawn(rvv, 'solver:qV:d1')
1573
+ if (sovv) fireEnd(sovv.childId, { status: 'success', solution: 'complete solution', survival_probability: 0.9 })
1574
+ await drive(rvv, () => spawnOf(rvv, 'verifier:').length >= 1, 'verifier qV')
1575
+ const vsp = spawnOf(rvv, 'verifier:')[0]
1576
+ assert(!!vsp && SITE_VERIFY.test(vsp.prompt || ''), '★ leanInitiative:off 时验证段仍按 formalVerify 注入(verifier 提示词含【Lean 形式化验证()')
1577
+ await callTool('vibe_math_abort', {}, rvv)
1578
+ }
1579
+
1231
1580
  section('10 the captured prompt corpus is written for human review')
1232
1581
  {
1233
1582
  mkdirSync(CORPUS_DIR, { recursive: true })