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
@@ -17,9 +17,17 @@
17
17
  // Run: node tests/e2e-v5-round2.test.mjs
18
18
  // ============================================================
19
19
  import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'
20
+ import { createHash } from 'node:crypto'
20
21
  import { tmpdir } from 'node:os'
21
22
  import { join, dirname, isAbsolute } from 'node:path'
22
23
 
24
+ // round-7 (fix 2) test isolation: engine discovery now also scans DSH's own runtime trees, and this
25
+ // machine HAS a bundled python. This suite injects ONE fake subprocess for everything (LaTeX + math),
26
+ // so an unexpected engine probe would be recorded as a compiler call and would even write a fake
27
+ // paper.pdf. Pinning DSH_HOME at an empty dir keeps the fixture's premise ("no math engine here")
28
+ // true and the suite deterministic; production behaviour is unaffected.
29
+ process.env.DSH_HOME = mkdtempSync(join(tmpdir(), 'vibe-v5-e2e-nodsh-'))
30
+
23
31
  // V5_PLUGIN lets a sensitivity probe point this suite at a deliberately broken copy.
24
32
  // Without it every probe against this suite silently tested the UNMUTATED plugin and was
25
33
  // reported as a "detection" only because the probe's own spawn failed — i.e. the whole
@@ -59,6 +67,9 @@ function makeHost(opts) {
59
67
  const o = opts || {}
60
68
  const WS = o.ws || mkdtempSync(join(tmpdir(), 'vibe-v5r2-'))
61
69
  const listeners = {}, toolRegs = [], commandRegs = [], spawns = [], wakes = [], interrupts = [], drains = []
70
+ // Every effect disposer, so a test can simulate a plugin UNLOAD (the Lean queue's disposer is
71
+ // registered first: it terminates in-flight compiles and marks them interrupted).
72
+ const effectDisposers = []
62
73
  const liveAgents = new Map()
63
74
  // The two services the fix removed. This host does not provide them, and records every
64
75
  // request so the suite can assert the plugin never even LOOKS for them any more.
@@ -87,7 +98,7 @@ function makeHost(opts) {
87
98
  return undefined
88
99
  },
89
100
  on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
90
- effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
101
+ effect(fn) { const d = fn(); const disp = () => { if (typeof d === 'function') d() }; effectDisposers.push(disp); return disp },
91
102
  logger: { info() {}, warn() {}, error() {} },
92
103
  timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
93
104
  tools: { register(spec) { toolRegs.push(spec); return () => {} } },
@@ -194,7 +205,25 @@ function makeHost(opts) {
194
205
  }
195
206
  return null
196
207
  }
197
- return { WS, ctx, ROOT, ROOT_SESSION, removedServiceQueries, spawns, wakes, interrupts, drains, toolRegs, commandRegs, listeners, callTool, childAgent, fireEnd, spawnOf, childOf, labelOf, kindOf, settleSpawns, drain, peekWakeOf, set plannedVotes(v) { plannedVotes = v }, get plannedVotes() { return plannedVotes }, set solvePlan(v) { solvePlan = v } }
208
+ // Any member's next prompt matching a predicate — used to capture the CHECKPOINT heartbeat,
209
+ // whose recipient is whichever member the scheduler picked.
210
+ async function peekWakeWhere(pred, maxWaitMs) {
211
+ const t0 = Date.now()
212
+ while (Date.now() - t0 < (maxWaitMs || 3000)) {
213
+ const i = wakes.findIndex(w => pred((w.blocks && w.blocks[0] && w.blocks[0].text) || ''))
214
+ if (i !== -1) {
215
+ const w = wakes.splice(i, 1)[0]
216
+ return { childId: w.childId, text: (w.blocks && w.blocks[0] && w.blocks[0].text) || '' }
217
+ }
218
+ if (wakes.length) {
219
+ const w = wakes.shift()
220
+ fireEnd(w.childId, { progress: '其他成员推进中', solved: false, contextPct: 10 })
221
+ }
222
+ await sleep(20)
223
+ }
224
+ return null
225
+ }
226
+ return { WS, ctx, ROOT, ROOT_SESSION, removedServiceQueries, spawns, wakes, interrupts, drains, toolRegs, commandRegs, listeners, effectDisposers, callTool, childAgent, fireEnd, spawnOf, childOf, labelOf, kindOf, settleSpawns, drain, peekWakeOf, peekWakeWhere, set plannedVotes(v) { plannedVotes = v }, get plannedVotes() { return plannedVotes }, set solvePlan(v) { solvePlan = v } }
198
227
  }
199
228
 
200
229
  const pluginModule = await import(PLUGIN.href + '?t=' + Date.now())
@@ -239,7 +268,59 @@ function fakeLatex(opts) {
239
268
  const paperKindOf = (p) => /【最终论文·撰写/.test(p) ? 'write'
240
269
  : /【最终论文·互审/.test(p) ? 'review'
241
270
  : /【最终论文·定稿/.test(p) ? 'final' : ''
271
+
272
+ // ── fake Lean toolchain (docs/formal-verification.md §7) ────────────────────
273
+ // Injected through the SAME `subprocess` service the plugin compiles with, so detection,
274
+ // argv assembly, the queue and the settle logic all run unchanged; only the toolchain is fake.
275
+ // exitFor(argv, file) -> exitCode | undefined terminated -> count of terminate() calls
276
+ // defer: true every spawn stays alive until releaseAll() (for timeout/dispose/build-context)
277
+ function fakeLean(opts) {
278
+ const o = opts || {}
279
+ const calls = []
280
+ return {
281
+ calls,
282
+ terminated: 0,
283
+ async resolveExecutable(cmd) {
284
+ const name = String(cmd || 'lean')
285
+ if (o.missing) throw new Error('ENOENT: ' + name)
286
+ return 'C:/fake/' + name
287
+ },
288
+ spawn(spec) {
289
+ const argv = (spec.argv || []).map(String)
290
+ // Only the LEAN engine is a compile: the plugin also uses the subprocess service for
291
+ // platform shell helpers (directory setup) AND for the math_computation engine probe
292
+ // (which resolves its own candidates through the same seam) — neither may pollute the
293
+ // Lean call log, nor be deferred by the gate.
294
+ const head = String(argv[0] || '')
295
+ if (!/(^|[\\/])lean$/i.test(head)) {
296
+ return { done: Promise.resolve({ exitCode: 0, stdout: '', stderr: '' }), terminate() { /* not a Lean compile */ } }
297
+ }
298
+ const file = argv[argv.length - 1]
299
+ calls.push({ argv, file, cwd: spec.cwd })
300
+ let exitCode = 0
301
+ if (o.alwaysFail) exitCode = 1
302
+ else if (typeof o.exitFor === 'function') { const r = o.exitFor(argv, file); if (r !== undefined && r !== null) exitCode = r }
303
+ const self = this
304
+ if (!o.defer) return { done: Promise.resolve({ exitCode }), terminate() { self.terminated += 1 } }
305
+ let settled = false
306
+ let resolveDone = null
307
+ const done = new Promise((resolve) => { resolveDone = resolve })
308
+ const finish = (v) => { if (!settled) { settled = true; resolveDone(v) } }
309
+ o._release = o._release || []
310
+ o._release.push(() => finish({ exitCode }))
311
+ return { done, terminate() { self.terminated += 1; finish({ exitCode: null, signal: 'SIGTERM' }) } }
312
+ },
313
+ releaseAll() { for (const f of (o._release || []).splice(0)) { try { f() } catch (e) { /* ignore */ } } },
314
+ }
315
+ }
242
316
  const paperDirOf = (h, id) => join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Paper', id || 'institute')
317
+ // Seed a .lean work file inside the institute tree (a run job needs an existing file).
318
+ function seedLeanFile(h, rel, text) {
319
+ const abs = join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', ...String(rel).split('/'))
320
+ mkdirSync(dirname(abs), { recursive: true })
321
+ writeFileSync(abs, text, 'utf8')
322
+ return abs
323
+ }
243
324
  function listFilesUnder(root, rel) {
244
325
  const out = []
245
326
  const base = rel === undefined ? '' : rel
@@ -1408,6 +1489,382 @@ console.log('\n[33] an existing Paper/<id>/paper.pdf is never overwritten or del
1408
1489
  assert(fake2.calls.length === 0, 'no compiler process was launched for the already-delivered pdf')
1409
1490
  }
1410
1491
 
1492
+ // ---------- 34. Lean async/initiative/search params: closed schema + coercion ----------
1493
+ console.log('\n[34] Lean async params: closed schema, coercion, visibleParams, embedded sha256')
1494
+ {
1495
+ const h = makeHost({ pluginModule })
1496
+ const setSpec = h.toolRegs.find(t => t.name === 'vibe_v5_set')
1497
+ const keys = ['leanAsync', 'leanInitiative', 'leanSearchPaths', 'leanJobsMaxParallel']
1498
+ assert(keys.every(k => Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k)),
1499
+ '★ vibe_v5_set advertises all four new Lean keys (the schema is closed, so an unlisted key is unreachable)')
1500
+ assert(JSON.stringify(setSpec.parameters.properties.leanInitiative.enum) === JSON.stringify(['off', 'normal', 'eager']),
1501
+ 'leanInitiative is a closed enum in the schema')
1502
+ const p0 = (await h.callTool('vibe_v5_status', {})).params
1503
+ assert(p0.leanAsync === true && p0.leanInitiative === 'normal' && Array.isArray(p0.leanSearchPaths) && p0.leanSearchPaths.length === 0 && p0.leanJobsMaxParallel === 1,
1504
+ '★ the documented defaults are live and visibleParams exposes all four (' + JSON.stringify({ a: p0.leanAsync, i: p0.leanInitiative, sp: p0.leanSearchPaths, mp: p0.leanJobsMaxParallel }) + ')')
1505
+ const r = await h.callTool('vibe_v5_set', { leanAsync: 'false', leanInitiative: 'bogus', leanJobsMaxParallel: 0, leanSearchPaths: '/libA, /libB' })
1506
+ assert(r.params.leanAsync === false, "★ leanAsync:'false' normalises to FALSE (a string must never stay truthy)")
1507
+ assert(r.params.leanInitiative === 'normal', 'a bogus leanInitiative falls back to the documented default')
1508
+ assert(r.params.leanJobsMaxParallel === 1, 'leanJobsMaxParallel:0 floors at 1')
1509
+ assert(JSON.stringify(r.params.leanSearchPaths) === JSON.stringify(['/libA', '/libB']),
1510
+ 'leanSearchPaths accepts a comma string and keeps the given order (' + JSON.stringify(r.params.leanSearchPaths) + ')')
1511
+ const help = String(setSpec.description || '')
1512
+ assert(/leanAsync/.test(help) && /leanInitiative/.test(help) && /leanSearchPaths/.test(help) && /leanJobsMaxParallel/.test(help),
1513
+ 'the set-tool help text names all four (the switch is discoverable)')
1514
+ const H = pluginModule.__testHelpers
1515
+ assert(H.sha256Hex('abc') === 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' &&
1516
+ H.sha256Hex('') === 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
1517
+ '★ the embedded sha256 matches the published vectors (job ids/dedupe rest on it)')
1518
+ assert(H.sha256Hex('中文abc') === createHash('sha256').update('中文abc', 'utf8').digest('hex'), 'it also matches node:crypto on non-ASCII input')
1519
+ assert(H.leanHasSearchFlag(['-R', 'x']) && H.leanHasSearchFlag(['--search-path=o']) && H.leanHasSearchFlag(['--root', 'r']) && !H.leanHasSearchFlag(['-j4']),
1520
+ 'the search-flag guard recognises -R / --search-path(=) / --root and nothing else')
1521
+ }
1522
+
1523
+ // ---------- 35. async Lean: enqueue → settle(ok) → passed ----------
1524
+ console.log('\n[35] async Lean: enqueue returns at once, settle(ok) is the only path to passed')
1525
+ {
1526
+ const fake = fakeLean({})
1527
+ const h = makeHost({ pluginModule, subprocess: fake })
1528
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1529
+ await h.settleSpawns()
1530
+ await h.callTool('vibe_v5_set', { formalVerify: 'encourage' })
1531
+ const arch = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-async', content: 'theorem p_async : 1 + 1 = 2 := by decide\n' })
1532
+ assert(arch.ok === true && arch.async && arch.async.state === 'queued' && arch.status === 'attempted',
1533
+ '★ the archive ENQUEUES and returns async:{jobId,state} with the object at attempted (' + JSON.stringify(arch).slice(0, 150) + ')')
1534
+ assert(fake.calls.length === 0, '★ the compiler has NOT been started when the tool returns')
1535
+ const st0 = await h.callTool('vibe_v5_status', {})
1536
+ assert(st0.formal.passed.indexOf('p-async') === -1, 'the queued object is not passed')
1537
+ const listed = await h.callTool('vibe_v5_lean_job', {})
1538
+ assert(listed.ok === true && listed.jobs.some(j => j.jobId === arch.jobId), 'lean_job lists the job (' + JSON.stringify(listed.jobs).slice(0, 140) + ')')
1539
+ const waited = await h.callTool('vibe_v5_lean_job', { jobId: arch.jobId, waitMs: 3000 })
1540
+ assert(waited.ok === true && waited.job.state === 'settled' && waited.job.exitCode === 0 && waited.job.proof === 'Verified/Lean/p-async.lean',
1541
+ '★ lean_job {jobId,waitMs} waits for the settle and reports the archived proof (' + JSON.stringify(waited.job).slice(0, 170) + ')')
1542
+ const st1 = await h.callTool('vibe_v5_status', {})
1543
+ assert(st1.formal.passed.indexOf('p-async') !== -1 && st1.formal.objects.find(o => o.target === 'p-async').async.jobId === arch.jobId,
1544
+ '★ ONLY the settled(ok) job set the object passed, and its receipt is on the record')
1545
+ assert(existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-async.lean')),
1546
+ 'the proof is archived under Verified/Lean/<id>.lean')
1547
+ const argv = fake.calls[0].argv
1548
+ const root = join(h.WS, 'VibeMath').replace(/\\/g, '/')
1549
+ const si = argv.indexOf('--search-path')
1550
+ assert(si > 0 && argv[si + 1] === root && si + 2 === argv.length - 1 && /\.lean$/.test(argv[argv.length - 1]),
1551
+ '★ argv carries `--search-path <ABSOLUTE VibeMath root>` right before the file (' + JSON.stringify(argv) + ')')
1552
+ assert(fake.calls.length === 1, 'exactly one compile ran for the settle (got ' + fake.calls.length + ')')
1553
+ }
1554
+
1555
+ // ---------- 36. failure / timeout / dispose never verify ----------
1556
+ console.log('\n[36] async Lean: failure, timeout and session dispose never verify')
1557
+ {
1558
+ const fakeFail = fakeLean({ alwaysFail: true })
1559
+ const h = makeHost({ pluginModule, subprocess: fakeFail })
1560
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1561
+ await h.settleSpawns()
1562
+ const a = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-fail', content: 'theorem p_fail : 1 = 2 := by decide\n' })
1563
+ const w = await h.callTool('vibe_v5_lean_job', { jobId: a.jobId, waitMs: 3000 })
1564
+ assert(w.job.state === 'failed', '★ a failed compile settles as failed (' + w.job.state + ')')
1565
+ assert(!existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-fail.lean')),
1566
+ '★ no Verified/Lean file is written for a failure')
1567
+ assert((await h.callTool('vibe_v5_status', {})).formal.passed.indexOf('p-fail') === -1, 'the failed object stays attempted')
1568
+ // (b) the budget terminates the process
1569
+ const fakeHang = fakeLean({ defer: true })
1570
+ const h2 = makeHost({ pluginModule, subprocess: fakeHang })
1571
+ await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1572
+ await h2.settleSpawns()
1573
+ seedLeanFile(h2, 'Formal/good.lean', 'theorem good : 1 + 1 = 2 := by decide\n')
1574
+ const r = await h2.callTool('vibe_v5_lean_run', { file: 'Formal/good.lean', timeout_ms: 1000 })
1575
+ assert(r.ok === true && r.async.state === 'queued', 'the run is queued')
1576
+ const w2 = await h2.callTool('vibe_v5_lean_job', { jobId: r.jobId, waitMs: 5000 })
1577
+ assert(w2.job.state === 'timeout' && fakeHang.terminated >= 1,
1578
+ '★ the per-job budget TERMINATED the process and the job is timeout (' + JSON.stringify({ s: w2.job.state, t: fakeHang.terminated }) + ')')
1579
+ // (c) session dispose (the plugin-unload disposer)
1580
+ const fakeGate = fakeLean({ defer: true })
1581
+ const h3 = makeHost({ pluginModule, subprocess: fakeGate })
1582
+ await h3.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1583
+ await h3.settleSpawns()
1584
+ const a3 = await h3.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dispose', content: 'theorem p_dispose : 1 + 1 = 2 := by decide\n' })
1585
+ await sleep(40)
1586
+ assert(fakeGate.calls.length >= 1, 'precondition: the compile is in flight')
1587
+ h3.effectDisposers[0]()
1588
+ assert(fakeGate.terminated >= 1, '★ dispose TERMINATED the in-flight compiler (no orphan process)')
1589
+ const w3 = await h3.callTool('vibe_v5_lean_job', { jobId: a3.jobId })
1590
+ assert(w3.job.state === 'interrupted', '★ the job is marked interrupted, not settled (' + w3.job.state + ')')
1591
+ assert((await h3.callTool('vibe_v5_status', {})).formal.passed.indexOf('p-dispose') === -1, '★ dispose NEVER verifies an object')
1592
+ }
1593
+
1594
+ // ---------- 37. content-hash dedupe ----------
1595
+ console.log('\n[37] identical re-archive is de-duplicated (no rewrite, no recompile)')
1596
+ {
1597
+ const fake = fakeLean({})
1598
+ const h = makeHost({ pluginModule, subprocess: fake })
1599
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1600
+ await h.settleSpawns()
1601
+ const PROOF = 'theorem p_dup : 2 + 2 = 4 := by decide\n'
1602
+ const one = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dup', content: PROOF })
1603
+ await h.callTool('vibe_v5_lean_job', { jobId: one.jobId, waitMs: 3000 })
1604
+ const before = fake.calls.length
1605
+ const again = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dup', content: PROOF })
1606
+ assert(again.deduped === true && fake.calls.length === before,
1607
+ '★ identical content is deduped: no new compile (' + JSON.stringify({ d: again.deduped, before, after: fake.calls.length }) + ')')
1608
+ const diff = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dup', content: PROOF + '-- changed\n' })
1609
+ assert(diff.deduped !== true && diff.async && diff.async.state === 'queued', 'different content is NOT deduped (a new job is queued)')
1610
+ const DEF = 'def ZDup := Fin 2\n'
1611
+ const d1 = await h.callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZDup', content: DEF })
1612
+ await h.callTool('vibe_v5_lean_job', { jobId: d1.jobId, waitMs: 3000 })
1613
+ const callsAfter = fake.calls.length
1614
+ const d2 = await h.callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZDup', content: DEF })
1615
+ assert(d2.deduped === true && fake.calls.length === callsAfter, '★ the global library dedupes on the same content hash too')
1616
+ }
1617
+
1618
+ // ---------- 38. lean_read: verbatim text + path guard ----------
1619
+ console.log('\n[38] lean_read returns archived text verbatim and refuses a path-shaped name')
1620
+ {
1621
+ const fake = fakeLean({})
1622
+ const h = makeHost({ pluginModule, subprocess: fake })
1623
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1624
+ await h.settleSpawns()
1625
+ const body = 'def ZRead : Nat := 7\n'
1626
+ const a = await h.callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZRead', content: body })
1627
+ await h.callTool('vibe_v5_lean_job', { jobId: a.jobId, waitMs: 3000 })
1628
+ const rd = await h.callTool('vibe_v5_lean_read', { name: 'ZRead' })
1629
+ assert(rd.ok === true && rd.kind === 'lib' && rd.text === body && rd.bytes === body.length && /^[0-9a-f]{64}$/.test(rd.sha256) && rd.truncated === false,
1630
+ '★ lean_read returns the archived text verbatim with sha/bytes (' + JSON.stringify({ k: rd.kind, b: rd.bytes }) + ')')
1631
+ const esc = await h.callTool('vibe_v5_lean_read', { name: '../ZRead' })
1632
+ assert(esc.ok === false && esc.code === 'V5_INVALID_ARGUMENT', 'a path-shaped name is REFUSED, never sanitised into another file (' + JSON.stringify(esc).slice(0, 90) + ')')
1633
+ assert((await h.callTool('vibe_v5_lean_read', { name: 'C:/x/ZRead' })).ok === false, 'an absolute name is refused')
1634
+ const miss = await h.callTool('vibe_v5_lean_read', { name: 'NoSuchThing' })
1635
+ assert(miss.ok === false && miss.code === 'V5_NOT_FOUND', 'an unknown name is V5_NOT_FOUND')
1636
+ assert((await h.callTool('vibe_v5_lean_read', { name: 'ZRead', kind: 'bogus' })).ok === false, 'a bogus kind is refused')
1637
+ }
1638
+
1639
+ // ---------- 39. a job only settles the BUILD CONTEXT it was queued under ----------
1640
+ console.log('\n[39] a job only settles the build context it was queued under')
1641
+ {
1642
+ const fake = fakeLean({ defer: true })
1643
+ const h = makeHost({ pluginModule, subprocess: fake })
1644
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1645
+ await h.settleSpawns()
1646
+ seedLeanFile(h, 'Formal/good.lean', 'theorem good : 1 + 1 = 2 := by decide\n')
1647
+ await h.callTool('vibe_v5_lean_run', { file: 'Formal/good.lean' })
1648
+ await sleep(40)
1649
+ const proof = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-ctx', content: 'theorem p_ctx : 3 + 3 = 6 := by decide\n' })
1650
+ await h.callTool('vibe_v5_set', { leanArgs: ['-Dctx=1'] }) // retune AFTER queueing
1651
+ for (let i = 0; i < 10; i++) { fake.releaseAll(); await sleep(25) }
1652
+ const w = await h.callTool('vibe_v5_lean_job', { jobId: proof.jobId, waitMs: 1000 })
1653
+ assert(w.job.state === 'failed' && w.job.buildMatched === false,
1654
+ '★ a job compiled under retuned args does NOT settle (' + JSON.stringify({ s: w.job.state, b: w.job.buildMatched }) + ')')
1655
+ const st = await h.callTool('vibe_v5_status', {})
1656
+ assert(st.formal.passed.indexOf('p-ctx') === -1, '★ the object stays attempted on a build-context mismatch')
1657
+ assert(!existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-ctx.lean')),
1658
+ 'no Verified/Lean file for a mismatched build')
1659
+ // (b) the CONTENT changed after the job was queued: the compile succeeds, but it compiled
1660
+ // something else — the object must stay attempted.
1661
+ const blocker2 = await h.callTool('vibe_v5_lean_run', { file: 'Formal/good.lean' })
1662
+ await sleep(40)
1663
+ const p2 = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-hash', content: 'theorem p_hash : 4 + 4 = 8 := by decide\n' })
1664
+ seedLeanFile(h, 'Formal/p-hash.lean', 'theorem p_hash : 4 + 4 = 8 := by decide\n-- edited while queued\n')
1665
+ for (let i = 0; i < 10; i++) { fake.releaseAll(); await sleep(25) }
1666
+ const w2 = await h.callTool('vibe_v5_lean_job', { jobId: p2.jobId, waitMs: 1000 })
1667
+ assert(w2.job.exitCode === 0 && w2.job.state === 'failed' && w2.job.buildMatched === true,
1668
+ '★ exit 0 with CHANGED content does not settle either (' + JSON.stringify({ s: w2.job.state, e: w2.job.exitCode, b: w2.job.buildMatched, blocker: blocker2.jobId }) + ')')
1669
+ assert(!existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-hash.lean')),
1670
+ 'a changed-content compile writes no proof either')
1671
+ }
1672
+
1673
+ // ---------- 40. leanJobsMaxParallel (default 1 = serial) ----------
1674
+ console.log('\n[40] leanJobsMaxParallel caps simultaneous compiles')
1675
+ {
1676
+ const fake = fakeLean({ defer: true })
1677
+ const h = makeHost({ pluginModule, subprocess: fake })
1678
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1679
+ await h.settleSpawns()
1680
+ seedLeanFile(h, 'Formal/a.lean', 'theorem a : 1 = 1 := rfl\n')
1681
+ seedLeanFile(h, 'Formal/b.lean', 'theorem b : 2 = 2 := rfl\n')
1682
+ await h.callTool('vibe_v5_lean_run', { file: 'Formal/a.lean' })
1683
+ await h.callTool('vibe_v5_lean_run', { file: 'Formal/b.lean' })
1684
+ await sleep(50)
1685
+ assert(fake.calls.length === 1, '★ with the default maxParallel=1 only ONE compile is in flight (got ' + fake.calls.length + ')')
1686
+ const listed = await h.callTool('vibe_v5_lean_job', {})
1687
+ assert(listed.maxParallel === 1 && listed.jobs.filter(j => j.state === 'queued').length === 1,
1688
+ 'lean_job reports maxParallel=1 with the second job queued (' + JSON.stringify(listed).slice(0, 150) + ')')
1689
+ const q = listed.jobs.find(j => j.state === 'queued')
1690
+ await h.callTool('vibe_v5_set', { leanJobsMaxParallel: 2 })
1691
+ await h.callTool('vibe_v5_lean_job', { jobId: q.jobId, waitMs: 150 })
1692
+ await sleep(30)
1693
+ assert(fake.calls.length === 2, '★ raising maxParallel to 2 starts the queued job as well (got ' + fake.calls.length + ')')
1694
+ for (let i = 0; i < 8; i++) { fake.releaseAll(); await sleep(20) }
1695
+ }
1696
+
1697
+ // ---------- 41. the daily/verify Lean text is gated by leanInitiative ----------
1698
+ console.log('\n[41] leanInitiative gates the daily Lean reminders; the async rule is in both blocks')
1699
+ {
1700
+ const h = makeHost({ pluginModule })
1701
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
1702
+ await h.settleSpawns()
1703
+ const promptFor = async (member, plan) => {
1704
+ if (plan) await h.callTool('vibe_v5_set', plan)
1705
+ await h.callTool('vibe_v5_say', { to: member, text: '请继续推进。' }, h.childAgent(h.childOf('acad')))
1706
+ const w = await h.peekWakeOf(member, 3000)
1707
+ if (!w) return ''
1708
+ h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
1709
+ await sleep(25)
1710
+ return w.text
1711
+ }
1712
+ const normal = await promptFor('r-1', { formalVerify: 'off', leanInitiative: 'normal' })
1713
+ assert(normal.indexOf('【顺手形式化') === -1, "leanInitiative=normal + formalVerify=off keeps today's behaviour (no daily Lean line)")
1714
+ const eager = await promptFor('r-1', { leanInitiative: 'eager' })
1715
+ assert(eager.indexOf('【顺手形式化') !== -1 && eager.indexOf('**主动**') !== -1,
1716
+ '★ leanInitiative=eager injects the daily line even when formalVerify is off')
1717
+ assert(eager.indexOf('判断标准:①') !== -1 && eager.indexOf('先 vibe_v5_lean_lib 查已有库') !== -1 && eager.indexOf('**查不到再新写**') !== -1,
1718
+ '★ the three criteria + reuse-first ride in the daily line')
1719
+ assert(eager.indexOf('import Formal.Lib.<name>') !== -1 && eager.indexOf('vibe_v5_lean_read') !== -1, 'the import/read reuse routes are named')
1720
+ assert(eager.indexOf('没把握就记 blocked') !== -1, 'the no-confidence→blocked rule rides in the daily line')
1721
+ assert(eager.indexOf('leanAsync=true') !== -1 && eager.indexOf('不得把该对象当成已通过') !== -1, '★ the async honesty rule rides in the daily line')
1722
+ const offInit = await promptFor('r-1', { formalVerify: 'encourage', leanInitiative: 'off' })
1723
+ assert(offInit.indexOf('【顺手形式化') === -1, '★ leanInitiative=off suppresses the daily line even when formalVerify is on')
1724
+ // The VERIFY block carries the async rule too.
1725
+ await h.callTool('vibe_v5_set', { formalVerify: 'encourage', leanInitiative: 'normal' })
1726
+ await h.callTool('vibe_v5_record_proposition', { id: 'p-leanv', statement: '异步落地前不得当已通过', value: 0.6, motive: 'm', p: 0.9 }, h.childAgent(h.childOf('r-1')))
1727
+ await h.callTool('vibe_v5_propose_verify', { target: 'p-leanv', kind: 'proposition', reason: '测试' }, h.childAgent(h.childOf('r-1')))
1728
+ const vw = await h.peekWakeOf('r-1', 3000)
1729
+ const vtxt = vw ? vw.text : ''
1730
+ assert(/形式化只写你有把握的版本/.test(vtxt) && /不得\*\*在它落地前声称已通过/.test(vtxt),
1731
+ '★ the verify block carries the confidence + async-honesty lines (' + JSON.stringify(vtxt.slice(-260)) + ')')
1732
+ }
1733
+
1734
+ // ---------- 42. per-injection-site gate: every Lean prompt site, both directions ----------
1735
+ // Lens-1 (HIGH coverage gap): the gate `leanInitiative !== 'off' && (formalOn() || eager)` is
1736
+ // correct, but a static scan found ZERO assertions naming `eager` per SITE. v5 has exactly two
1737
+ // DAILY-line sites (normalPrompt, checkpointPrompt) plus two mode-gated Lean sites (briefBlock's
1738
+ // `[形式化]` state line and verifyPrompt's formalPromptBlock). Each site below is asserted in BOTH
1739
+ // directions: (1) formalVerify=off + leanInitiative=eager ⇒ that site HAS the daily line (with the
1740
+ // `**主动**` mark) while the VERIFY block stays absent; (2) leanInitiative=off ⇒ that site has NO
1741
+ // daily line while the mode/verify text still follows formalVerify.
1742
+ console.log('\n[42] every Lean prompt site obeys the leanInitiative gate in both directions')
1743
+ {
1744
+ const DAILY = '【顺手形式化'
1745
+ const EAGER = '**主动**'
1746
+ // The verify-phase BLOCK header (its mode suffix distinguishes it from the normal prompt's
1747
+ // reply contract, which also *mentions* 【Lean 形式化验证】).
1748
+ const VERIFY_BLOCK = '【Lean 形式化验证('
1749
+ const STATE = '[形式化]'
1750
+ const h = makeHost({ pluginModule })
1751
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
1752
+ await h.settleSpawns()
1753
+ const setAndAssert = async (plan) => {
1754
+ const r = await h.callTool('vibe_v5_set', plan)
1755
+ assert(r.ok === true, 'precondition: vibe_v5_set applied ' + JSON.stringify(plan) + ' (' + JSON.stringify(r).slice(0, 120) + ')')
1756
+ return r.params
1757
+ }
1758
+ // Pending turns must be ANSWERED, never dropped: a discarded wake leaves its member busy
1759
+ // forever, so the scheduler would have no idle member left to send a heartbeat to.
1760
+ const flushWakes = async (rounds) => {
1761
+ for (let i = 0; i < (rounds || 8); i++) {
1762
+ const w = h.wakes.shift()
1763
+ if (!w) return
1764
+ h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
1765
+ await sleep(10)
1766
+ }
1767
+ }
1768
+ const normalFor = async (member) => {
1769
+ await h.callTool('vibe_v5_say', { to: member, text: '请继续推进。' }, h.childAgent(h.childOf('acad')))
1770
+ const w = await h.peekWakeOf(member, 3000)
1771
+ if (!w) return ''
1772
+ h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
1773
+ await sleep(25)
1774
+ return w.text
1775
+ }
1776
+ // ── SITE 1: normalPrompt ────────────────────────────────────────────────────
1777
+ const pA = await setAndAssert({ formalVerify: 'off', leanInitiative: 'eager' })
1778
+ assert(pA.formalVerify === 'off' && pA.leanInitiative === 'eager', 'precondition: formalVerify=off + leanInitiative=eager is live')
1779
+ await flushWakes()
1780
+ const s1eager = await normalFor('r-1')
1781
+ assert(s1eager.indexOf(DAILY) !== -1 && s1eager.indexOf(EAGER) !== -1,
1782
+ 'S1 normalPrompt: formalVerify=off + leanInitiative=eager ⇒ the daily line IS injected (with **主动**)')
1783
+ assert(s1eager.indexOf(VERIFY_BLOCK) === -1 && s1eager.indexOf(STATE) === -1,
1784
+ 'S1 normalPrompt: …while the VERIFY block and the [形式化] state line stay ABSENT (formalVerify=off)')
1785
+ const pB = await setAndAssert({ formalVerify: 'encourage', leanInitiative: 'off' })
1786
+ assert(pB.formalVerify === 'encourage' && pB.leanInitiative === 'off', 'precondition: formalVerify=encourage + leanInitiative=off is live')
1787
+ await sleep(30) // let prompts built with the previous params drain
1788
+ await flushWakes()
1789
+ const s1off = await normalFor('r-1')
1790
+ assert(s1off.indexOf(DAILY) === -1, 'S1 normalPrompt: leanInitiative=off ⇒ NO daily line')
1791
+ assert(s1off.indexOf(VERIFY_BLOCK) === -1 && s1off.indexOf(STATE) !== -1,
1792
+ 'S1 normalPrompt: …but the mode-gated [形式化] state line still follows formalVerify=encourage (stateIdx=' + s1off.indexOf(STATE) + ')')
1793
+ // ── SITE 2: checkpointPrompt (the heartbeat) ────────────────────────────────
1794
+ // Delivered to the longest-idle member that owns no in-progress task, so the queue is answered
1795
+ // FIFO (a live institute keeps working) until a 【心跳检查】 turn shows up.
1796
+ const checkpointFor = async (plan) => {
1797
+ await setAndAssert(plan)
1798
+ await flushWakes()
1799
+ const seen = []
1800
+ for (let i = 0; i < 30; i++) {
1801
+ // A tuning call drives one scheduling pass, exactly like a real institute where events keep
1802
+ // arriving; the heartbeat branch then picks the longest-idle member.
1803
+ await h.callTool('vibe_v5_set', { maxParallel: i % 2 === 0 ? 3 : 4 })
1804
+ const w = await h.peekWakeWhere(() => true, 300)
1805
+ if (!w) continue
1806
+ seen.push(w.text.slice(0, 20))
1807
+ const isCheckpoint = w.text.indexOf('【心跳检查') !== -1
1808
+ h.fireEnd(w.childId, { progress: '继续推进。', solved: false, contextPct: 10 })
1809
+ await sleep(15)
1810
+ if (isCheckpoint) return { text: w.text, seen }
1811
+ }
1812
+ return { text: '', seen }
1813
+ }
1814
+ await h.callTool('vibe_v5_set', { activityTimeoutMs: 40, chatDigestMs: 600000, stallAutoMeetingMs: 600000 })
1815
+ const s2a = await checkpointFor({ formalVerify: 'off', leanInitiative: 'eager' })
1816
+ const s2st = await h.callTool('vibe_v5_status', {})
1817
+ assert(s2a.text !== '' && s2a.text.indexOf(DAILY) !== -1 && s2a.text.indexOf(EAGER) !== -1,
1818
+ 'S2 checkpointPrompt (heartbeat): formalVerify=off + leanInitiative=eager ⇒ the daily line IS injected (seen=' + JSON.stringify(s2a.seen) + ' status=' + JSON.stringify({ verify: s2st.verify && s2st.verify.target, meeting: s2st.meeting, paper: s2st.paper && s2st.paper.stage, busy: (s2st.members || []).filter((m) => m.busy).map((m) => m.id), undecided: s2st.undecided }) + ')')
1819
+ assert(s2a.text.indexOf(VERIFY_BLOCK) === -1, 'S2 checkpointPrompt: the VERIFY block stays absent with formalVerify=off')
1820
+ const s2b = await checkpointFor({ formalVerify: 'encourage', leanInitiative: 'off' })
1821
+ assert(s2b.text !== '' && s2b.text.indexOf(DAILY) === -1,
1822
+ 'S2 checkpointPrompt: leanInitiative=off ⇒ NO daily line (seen=' + JSON.stringify(s2b.seen) + ')')
1823
+ assert(s2b.text.indexOf(STATE) !== -1, 'S2 checkpointPrompt: …while the [形式化] state line still follows formalVerify=encourage')
1824
+ // ── SITE 3: verifyPrompt's formalPromptBlock ────────────────────────────────
1825
+ // Finish any verification that is already in flight (answering every voter turn) so that the
1826
+ // NEXT verify wake is necessarily built with the params we set afterwards.
1827
+ const drainVerify = async () => {
1828
+ for (let i = 0; i < 25; i++) {
1829
+ const st = await h.callTool('vibe_v5_status', {})
1830
+ if (!st.verify) return true
1831
+ await h.callTool('vibe_v5_set', { maxParallel: i % 2 === 0 ? 3 : 4 })
1832
+ const w = await h.peekWakeWhere((p) => p.indexOf('【求真表决') !== -1, 250)
1833
+ if (w) {
1834
+ const tm = /"target"\s*:\s*"([^"]+)"/.exec(w.text)
1835
+ h.fireEnd(w.childId, { verdict: { target: tm ? tm[1] : st.verify.target, verdict: 0.5, reason: '存疑' }, contextPct: 20 })
1836
+ }
1837
+ await sleep(20)
1838
+ }
1839
+ return false
1840
+ }
1841
+ const verifyFor = async (plan) => {
1842
+ await drainVerify()
1843
+ await setAndAssert(plan)
1844
+ const id = 'p-site-' + Math.random().toString(36).slice(2, 8)
1845
+ await h.callTool('vibe_v5_record_proposition', { id, statement: '站点门控', value: 0.6, motive: 'm', p: 0.9 }, h.childAgent(h.childOf('r-1')))
1846
+ await h.callTool('vibe_v5_propose_verify', { target: id, kind: 'proposition', reason: 'x' }, h.childAgent(h.childOf('r-1')))
1847
+ for (let i = 0; i < 25; i++) {
1848
+ await h.callTool('vibe_v5_set', { maxParallel: i % 2 === 0 ? 3 : 4 })
1849
+ const w = await h.peekWakeWhere((p) => p.indexOf('【求真表决') !== -1, 300)
1850
+ if (!w) continue
1851
+ const tm = /"target"\s*:\s*"([^"]+)"/.exec(w.text)
1852
+ h.fireEnd(w.childId, { verdict: { target: tm ? tm[1] : id, verdict: 0.5, reason: '存疑' }, contextPct: 20 })
1853
+ await sleep(25)
1854
+ return w.text
1855
+ }
1856
+ return ''
1857
+ }
1858
+ const s3off = await verifyFor({ formalVerify: 'off', leanInitiative: 'eager' })
1859
+ assert(s3off.indexOf('【求真表决') !== -1 && s3off.indexOf(VERIFY_BLOCK) === -1,
1860
+ 'S3 verifyPrompt: formalVerify=off ⇒ NO verify-phase Lean block even when leanInitiative=eager')
1861
+ assert(s3off.indexOf(DAILY) === -1, 'S3 verifyPrompt: the daily line is never injected into the verify prompt')
1862
+ const s3on = await verifyFor({ formalVerify: 'encourage', leanInitiative: 'off' })
1863
+ assert(s3on.indexOf('【求真表决') !== -1 && s3on.indexOf(VERIFY_BLOCK) !== -1,
1864
+ 'S3 verifyPrompt: formalVerify=encourage ⇒ the verify-phase block appears even with leanInitiative=off')
1865
+ assert(s3on.indexOf(DAILY) === -1, 'S3 verifyPrompt: …and the daily line is still absent there')
1866
+ }
1867
+
1411
1868
  console.log('')
1412
1869
  console.log('passed=' + passed + ' failed=' + failed)
1413
1870
  if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }