dsh-vibe-math 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.en.md +156 -389
  2. package/README.md +156 -390
  3. package/cordis.patch.yml +56 -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 +121 -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/test-timing.md +24 -12
  11. package/installer.js +7 -4
  12. package/package.json +18 -2
  13. package/prompt-corpus-persona/persona-corpus.json +12 -12
  14. package/prompt-corpus-persona/persona-corpus.md +116 -32
  15. package/prompt-corpus-v2/formal-verify-v2.json +116 -92
  16. package/prompt-corpus-v2/formal-verify-v2.md +1430 -55
  17. package/prompt-corpus-v3/formal-verify-v3.json +101 -53
  18. package/prompt-corpus-v3/formal-verify-v3.md +1001 -22
  19. package/prompt-corpus-v4/formal-verify-v4.json +14 -14
  20. package/prompt-corpus-v4/formal-verify-v4.md +247 -14
  21. package/prompt-corpus-v5/prompt-corpus-v5.json +62 -62
  22. package/prompt-corpus-v5/prompt-corpus-v5.md +570 -3
  23. package/tests/audit-math-computation-contract.mjs +191 -0
  24. package/tests/audit-math-computation-parity.mjs +169 -0
  25. package/tests/audit-math-computation-sensitivity.mjs +231 -0
  26. package/tests/audit-persona-sensitivity.mjs +4 -4
  27. package/tests/audit-persona-surface.test.mjs +38 -10
  28. package/tests/audit-v5-integrity.mjs +14 -0
  29. package/tests/e2e-v5-round2.test.mjs +452 -2
  30. package/tests/formal-verify-v2.test.mjs +292 -6
  31. package/tests/formal-verify-v3.test.mjs +271 -4
  32. package/tests/formal-verify-v4.test.mjs +295 -6
  33. package/tests/formal-verify-v5.test.mjs +9 -1
  34. package/tests/helpers/math-computation-fake-seam.mjs +135 -0
  35. package/tests/math-computation-shared.test.mjs +504 -0
  36. package/tests/prompt-v5-integrity.test.mjs +5 -5
  37. package/vibe-math-v2/agent.cordis.yml +20 -8
  38. package/vibe-math-v2/math-computation.js +936 -0
  39. package/vibe-math-v2/math-engines.js +156 -0
  40. package/vibe-math-v2/vibe-math-v2.js +837 -40
  41. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  42. package/vibe-math-v3/agent.cordis.yml +20 -8
  43. package/vibe-math-v3/math-computation.js +936 -0
  44. package/vibe-math-v3/math-engines.js +156 -0
  45. package/vibe-math-v3/vibe-math-v3.js +828 -47
  46. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  47. package/vibe-math-v4/agent.cordis.yml +34 -6
  48. package/vibe-math-v4/math-computation.js +936 -0
  49. package/vibe-math-v4/math-engines.js +156 -0
  50. package/vibe-math-v4/vibe-math-v4.js +801 -31
  51. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
  52. package/vibe-math-v5/agent.cordis.yml +38 -6
  53. package/vibe-math-v5/math-computation.js +936 -0
  54. package/vibe-math-v5/math-engines.js +156 -0
  55. package/vibe-math-v5/vibe-math-v5.js +1120 -37
  56. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +83 -3
  57. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
@@ -132,29 +132,35 @@ const PRESETS = [
132
132
  dir: 'vibe-math-v2',
133
133
  js: 'vibe-math-v2.js',
134
134
  prefix: 'vibe_math_',
135
- tools: 25,
135
+ tools: 28,
136
136
  // member-facing write-lock / scheduler-metadata tools; the v2 coordinator never
137
137
  // writes Markdown itself, so they stay out of its persona.
138
138
  undocumented: [],
139
- lean: { tools: ['vibe_math_lean_run', 'vibe_math_lean_archive', 'vibe_math_lean_lib'], extra: [] },
139
+ lean: { tools: ['vibe_math_lean_run', 'vibe_math_lean_archive', 'vibe_math_lean_lib', 'vibe_math_lean_read', 'vibe_math_lean_job'], extra: [] },
140
+ // the shared math-computation tool (docs/math-computation.md) is prefix-less by
141
+ // design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
142
+ math: { tools: ['math_computation'] },
140
143
  },
141
144
  {
142
145
  dir: 'vibe-math-v3',
143
146
  js: 'vibe-math-v3.js',
144
147
  prefix: 'vibe_math_',
145
- tools: 33,
148
+ tools: 36,
146
149
  // `vibe_math_sync_meta` is no longer undocumented: the promotion-contract line of the
147
150
  // persona now names it as the place to report `lemmas[].价值/关键性` (v3 audit M1 —
148
151
  // without a prompt-side source for that field the promotion main line is unreachable).
149
152
  // The write-lock pair stays out: solver/method-keeper prompts carry it via kcWriteRules().
150
153
  undocumented: ['vibe_math_claim_write', 'vibe_math_release_write'],
151
- lean: { tools: ['vibe_math_lean_run', 'vibe_math_lean_archive', 'vibe_math_lean_lib'], extra: [] },
154
+ lean: { tools: ['vibe_math_lean_run', 'vibe_math_lean_archive', 'vibe_math_lean_lib', 'vibe_math_lean_read', 'vibe_math_lean_job'], extra: [] },
155
+ // the shared math-computation tool (docs/math-computation.md) is prefix-less by
156
+ // design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
157
+ math: { tools: ['math_computation'] },
152
158
  },
153
159
  {
154
160
  dir: 'vibe-math-v4',
155
161
  js: 'vibe-math-v4.js',
156
162
  prefix: 'vibe_v4_',
157
- tools: 32,
163
+ tools: 35,
158
164
  // resident-facing tools (mail, library cards, task board, write lock). The
159
165
  // coordinator drives residents through vibe_v4_message / _meeting / _add_member.
160
166
  undocumented: [
@@ -164,13 +170,16 @@ const PRESETS = [
164
170
  'vibe_v4_task_done', 'vibe_v4_list_tasks', 'vibe_v4_report_context',
165
171
  'vibe_v4_claim_write', 'vibe_v4_release_write',
166
172
  ],
167
- lean: { tools: ['vibe_v4_lean_run', 'vibe_v4_lean_archive', 'vibe_v4_lean_lib'], extra: ['vibe_v4_formal_report'] },
173
+ lean: { tools: ['vibe_v4_lean_run', 'vibe_v4_lean_archive', 'vibe_v4_lean_lib', 'vibe_v4_lean_read', 'vibe_v4_lean_job'], extra: ['vibe_v4_formal_report'] },
174
+ // the shared math-computation tool (docs/math-computation.md) is prefix-less by
175
+ // design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
176
+ math: { tools: ['math_computation'] },
168
177
  },
169
178
  {
170
179
  dir: 'vibe-math-v5',
171
180
  js: 'vibe-math-v5.js',
172
181
  prefix: 'vibe_v5_',
173
- tools: 37,
182
+ tools: 40,
174
183
  // member- and academician-facing tools; the office (main agent) holds only the
175
184
  // institute-level controls plus the hiring authority. The final-paper pair is an
176
185
  // OFFICE control and IS named in the persona (vibe_v5_paper / vibe_v5_finalize_paper),
@@ -182,11 +191,14 @@ const PRESETS = [
182
191
  'vibe_v5_task_list', 'vibe_v5_task_get', 'vibe_v5_task_update',
183
192
  'vibe_v5_overview', 'vibe_v5_assign', 'vibe_v5_prioritize', 'vibe_v5_nudge',
184
193
  ],
185
- lean: { tools: ['vibe_v5_lean_run', 'vibe_v5_lean_archive', 'vibe_v5_lean_lib'], extra: [] },
194
+ lean: { tools: ['vibe_v5_lean_run', 'vibe_v5_lean_archive', 'vibe_v5_lean_lib', 'vibe_v5_lean_read', 'vibe_v5_lean_job'], extra: [] },
195
+ // the shared math-computation tool (docs/math-computation.md) is prefix-less by
196
+ // design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
197
+ math: { tools: ['math_computation'] },
186
198
  },
187
199
  ]
188
200
 
189
- const LEAN_PARAMS = ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']
201
+ const LEAN_PARAMS = ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs', 'leanAsync', 'leanJobsMaxParallel', 'leanInitiative', 'leanSearchPaths']
190
202
 
191
203
  for (const P of PRESETS) {
192
204
  const ymlPath = join(BASE, P.dir, 'agent.cordis.yml')
@@ -195,9 +207,19 @@ for (const P of PRESETS) {
195
207
  const src = readFileSync(jsPath, 'utf8')
196
208
 
197
209
  // ---- registered tools -------------------------------------------------
210
+ // Two registration shapes exist now:
211
+ // 1. a literal `registerTool('<name>', …)` (most tools, including the shared math tool in
212
+ // v2/v3, which register it by its frozen prefix-less name);
213
+ // 2. the shared math tool in v4/v5, where the plugin only wires a host callback and the module
214
+ // calls `host.register(MATH_TOOL_NAME, …)`. The module's name is FROZEN
215
+ // (MATH_TOOL_NAME = 'math_computation', INTERFACE-FREEZE.md §3), so a source that wires
216
+ // `registerMathComputation(` contributes exactly that one tool. Without this the shared tool
217
+ // would be invisible to this guard on v4/v5 - a silent coverage hole.
198
218
  const registered = new Set()
199
219
  for (const m of src.matchAll(/registerTool\(\s*'([A-Za-z0-9_]+)'/g)) registered.add(m[1])
220
+ if (/registerMathComputation\s*\(/.test(src)) registered.add('math_computation')
200
221
  eq(registered.size, P.tools, `${P.dir}: registered tool count changed (update this snapshot deliberately)`)
222
+ for (const tool of (P.math ? P.math.tools : [])) ok(registered.has(tool), `${P.dir}: the shared math tool ${tool} is not registered`)
201
223
 
202
224
  // ---- A. both literal blocks exist and are non-empty -------------------
203
225
  const blocks = literalBlocks(yml)
@@ -226,7 +248,13 @@ for (const P of PRESETS) {
226
248
  // Uppercase is part of the token class on purpose: a drifted name such as
227
249
  // `vibe_v4_formal_reportX` must be captured whole and judged unregistered, instead of
228
250
  // being read as a mention of the registered `vibe_v4_formal_report`.
229
- const re = new RegExp('\\b' + P.prefix.replace(/_$/, '') + '_[A-Za-z0-9_]+', 'g')
251
+ // Names that do NOT carry this preset's prefix (the shared `math_computation`) are added to the
252
+ // alternation from the registry, so "every registered tool must be mentioned" stays true for
253
+ // them too instead of forcing them into the `undocumented` snapshot.
254
+ const prefixRoot = P.prefix.replace(/_$/, '')
255
+ const prefixless = [...registered].filter((n) => !new RegExp('^' + prefixRoot + '_').test(n))
256
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
257
+ const re = new RegExp('\\b' + prefixRoot + '_[A-Za-z0-9_]+' + (prefixless.length ? '|\\b' + prefixless.map(escapeRe).join('\\b|\\b') + '\\b' : ''), 'g')
230
258
  for (const m of body.matchAll(re)) {
231
259
  if (body[m.index + m[0].length] === '*') { wildcards.add(m[0]); continue }
232
260
  mentioned.add(m[0])
@@ -90,6 +90,20 @@ for (const m of src.matchAll(/(?:const|let|var)\s+\{([^}]+)\}\s*=/g)) {
90
90
  for (const part of m[1].split(',')) { const n = part.split(':').pop().trim(); if (n) defined.add(n) }
91
91
  }
92
92
  for (const m of src.matchAll(/catch\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g)) defined.add(m[1])
93
+ // ESM imports are DEFINITIONS too: the preset imports the shared `math_computation` module
94
+ // (INTERFACE-FREEZE §5.1), so calling an imported function is not a ReferenceError. Without
95
+ // this every imported symbol was reported as "called but never defined"; a call to a name that
96
+ // is neither declared nor imported is still flagged.
97
+ for (const m of src.matchAll(/import\s*(?:\*\s*as\s+([A-Za-z_$][\w$]*)|([A-Za-z_$][\w$]*)\s*,?\s*)?(?:\{([^}]*)\}\s*)?from\s*['"][^'"]*['"]/g)) {
98
+ if (m[1]) defined.add(m[1])
99
+ if (m[2]) defined.add(m[2])
100
+ if (m[3]) {
101
+ for (const part of m[3].split(',')) {
102
+ const name = part.split(/\s+as\s+/).pop().trim()
103
+ if (name) defined.add(name)
104
+ }
105
+ }
106
+ }
93
107
  // Function/method PARAMETERS are locally bound identifiers too, not free calls.
94
108
  for (const m of src.matchAll(/function\s*[A-Za-z_$]*\s*\(([^)]*)\)/g)) {
95
109
  for (const raw of m[1].split(',')) {
@@ -17,6 +17,7 @@
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
 
@@ -59,6 +60,9 @@ function makeHost(opts) {
59
60
  const o = opts || {}
60
61
  const WS = o.ws || mkdtempSync(join(tmpdir(), 'vibe-v5r2-'))
61
62
  const listeners = {}, toolRegs = [], commandRegs = [], spawns = [], wakes = [], interrupts = [], drains = []
63
+ // Every effect disposer, so a test can simulate a plugin UNLOAD (the Lean queue's disposer is
64
+ // registered first: it terminates in-flight compiles and marks them interrupted).
65
+ const effectDisposers = []
62
66
  const liveAgents = new Map()
63
67
  // The two services the fix removed. This host does not provide them, and records every
64
68
  // request so the suite can assert the plugin never even LOOKS for them any more.
@@ -87,7 +91,7 @@ function makeHost(opts) {
87
91
  return undefined
88
92
  },
89
93
  on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
90
- effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
94
+ effect(fn) { const d = fn(); const disp = () => { if (typeof d === 'function') d() }; effectDisposers.push(disp); return disp },
91
95
  logger: { info() {}, warn() {}, error() {} },
92
96
  timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
93
97
  tools: { register(spec) { toolRegs.push(spec); return () => {} } },
@@ -194,7 +198,25 @@ function makeHost(opts) {
194
198
  }
195
199
  return null
196
200
  }
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 } }
201
+ // Any member's next prompt matching a predicate — used to capture the CHECKPOINT heartbeat,
202
+ // whose recipient is whichever member the scheduler picked.
203
+ async function peekWakeWhere(pred, maxWaitMs) {
204
+ const t0 = Date.now()
205
+ while (Date.now() - t0 < (maxWaitMs || 3000)) {
206
+ const i = wakes.findIndex(w => pred((w.blocks && w.blocks[0] && w.blocks[0].text) || ''))
207
+ if (i !== -1) {
208
+ const w = wakes.splice(i, 1)[0]
209
+ return { childId: w.childId, text: (w.blocks && w.blocks[0] && w.blocks[0].text) || '' }
210
+ }
211
+ if (wakes.length) {
212
+ const w = wakes.shift()
213
+ fireEnd(w.childId, { progress: '其他成员推进中', solved: false, contextPct: 10 })
214
+ }
215
+ await sleep(20)
216
+ }
217
+ return null
218
+ }
219
+ 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
220
  }
199
221
 
200
222
  const pluginModule = await import(PLUGIN.href + '?t=' + Date.now())
@@ -239,7 +261,59 @@ function fakeLatex(opts) {
239
261
  const paperKindOf = (p) => /【最终论文·撰写/.test(p) ? 'write'
240
262
  : /【最终论文·互审/.test(p) ? 'review'
241
263
  : /【最终论文·定稿/.test(p) ? 'final' : ''
264
+
265
+ // ── fake Lean toolchain (docs/formal-verification.md §7) ────────────────────
266
+ // Injected through the SAME `subprocess` service the plugin compiles with, so detection,
267
+ // argv assembly, the queue and the settle logic all run unchanged; only the toolchain is fake.
268
+ // exitFor(argv, file) -> exitCode | undefined terminated -> count of terminate() calls
269
+ // defer: true every spawn stays alive until releaseAll() (for timeout/dispose/build-context)
270
+ function fakeLean(opts) {
271
+ const o = opts || {}
272
+ const calls = []
273
+ return {
274
+ calls,
275
+ terminated: 0,
276
+ async resolveExecutable(cmd) {
277
+ const name = String(cmd || 'lean')
278
+ if (o.missing) throw new Error('ENOENT: ' + name)
279
+ return 'C:/fake/' + name
280
+ },
281
+ spawn(spec) {
282
+ const argv = (spec.argv || []).map(String)
283
+ // Only the LEAN engine is a compile: the plugin also uses the subprocess service for
284
+ // platform shell helpers (directory setup) AND for the math_computation engine probe
285
+ // (which resolves its own candidates through the same seam) — neither may pollute the
286
+ // Lean call log, nor be deferred by the gate.
287
+ const head = String(argv[0] || '')
288
+ if (!/(^|[\\/])lean$/i.test(head)) {
289
+ return { done: Promise.resolve({ exitCode: 0, stdout: '', stderr: '' }), terminate() { /* not a Lean compile */ } }
290
+ }
291
+ const file = argv[argv.length - 1]
292
+ calls.push({ argv, file, cwd: spec.cwd })
293
+ let exitCode = 0
294
+ if (o.alwaysFail) exitCode = 1
295
+ else if (typeof o.exitFor === 'function') { const r = o.exitFor(argv, file); if (r !== undefined && r !== null) exitCode = r }
296
+ const self = this
297
+ if (!o.defer) return { done: Promise.resolve({ exitCode }), terminate() { self.terminated += 1 } }
298
+ let settled = false
299
+ let resolveDone = null
300
+ const done = new Promise((resolve) => { resolveDone = resolve })
301
+ const finish = (v) => { if (!settled) { settled = true; resolveDone(v) } }
302
+ o._release = o._release || []
303
+ o._release.push(() => finish({ exitCode }))
304
+ return { done, terminate() { self.terminated += 1; finish({ exitCode: null, signal: 'SIGTERM' }) } }
305
+ },
306
+ releaseAll() { for (const f of (o._release || []).splice(0)) { try { f() } catch (e) { /* ignore */ } } },
307
+ }
308
+ }
242
309
  const paperDirOf = (h, id) => join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Paper', id || 'institute')
310
+ // Seed a .lean work file inside the institute tree (a run job needs an existing file).
311
+ function seedLeanFile(h, rel, text) {
312
+ const abs = join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', ...String(rel).split('/'))
313
+ mkdirSync(dirname(abs), { recursive: true })
314
+ writeFileSync(abs, text, 'utf8')
315
+ return abs
316
+ }
243
317
  function listFilesUnder(root, rel) {
244
318
  const out = []
245
319
  const base = rel === undefined ? '' : rel
@@ -1408,6 +1482,382 @@ console.log('\n[33] an existing Paper/<id>/paper.pdf is never overwritten or del
1408
1482
  assert(fake2.calls.length === 0, 'no compiler process was launched for the already-delivered pdf')
1409
1483
  }
1410
1484
 
1485
+ // ---------- 34. Lean async/initiative/search params: closed schema + coercion ----------
1486
+ console.log('\n[34] Lean async params: closed schema, coercion, visibleParams, embedded sha256')
1487
+ {
1488
+ const h = makeHost({ pluginModule })
1489
+ const setSpec = h.toolRegs.find(t => t.name === 'vibe_v5_set')
1490
+ const keys = ['leanAsync', 'leanInitiative', 'leanSearchPaths', 'leanJobsMaxParallel']
1491
+ assert(keys.every(k => Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k)),
1492
+ '★ vibe_v5_set advertises all four new Lean keys (the schema is closed, so an unlisted key is unreachable)')
1493
+ assert(JSON.stringify(setSpec.parameters.properties.leanInitiative.enum) === JSON.stringify(['off', 'normal', 'eager']),
1494
+ 'leanInitiative is a closed enum in the schema')
1495
+ const p0 = (await h.callTool('vibe_v5_status', {})).params
1496
+ assert(p0.leanAsync === true && p0.leanInitiative === 'normal' && Array.isArray(p0.leanSearchPaths) && p0.leanSearchPaths.length === 0 && p0.leanJobsMaxParallel === 1,
1497
+ '★ the documented defaults are live and visibleParams exposes all four (' + JSON.stringify({ a: p0.leanAsync, i: p0.leanInitiative, sp: p0.leanSearchPaths, mp: p0.leanJobsMaxParallel }) + ')')
1498
+ const r = await h.callTool('vibe_v5_set', { leanAsync: 'false', leanInitiative: 'bogus', leanJobsMaxParallel: 0, leanSearchPaths: '/libA, /libB' })
1499
+ assert(r.params.leanAsync === false, "★ leanAsync:'false' normalises to FALSE (a string must never stay truthy)")
1500
+ assert(r.params.leanInitiative === 'normal', 'a bogus leanInitiative falls back to the documented default')
1501
+ assert(r.params.leanJobsMaxParallel === 1, 'leanJobsMaxParallel:0 floors at 1')
1502
+ assert(JSON.stringify(r.params.leanSearchPaths) === JSON.stringify(['/libA', '/libB']),
1503
+ 'leanSearchPaths accepts a comma string and keeps the given order (' + JSON.stringify(r.params.leanSearchPaths) + ')')
1504
+ const help = String(setSpec.description || '')
1505
+ assert(/leanAsync/.test(help) && /leanInitiative/.test(help) && /leanSearchPaths/.test(help) && /leanJobsMaxParallel/.test(help),
1506
+ 'the set-tool help text names all four (the switch is discoverable)')
1507
+ const H = pluginModule.__testHelpers
1508
+ assert(H.sha256Hex('abc') === 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' &&
1509
+ H.sha256Hex('') === 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
1510
+ '★ the embedded sha256 matches the published vectors (job ids/dedupe rest on it)')
1511
+ assert(H.sha256Hex('中文abc') === createHash('sha256').update('中文abc', 'utf8').digest('hex'), 'it also matches node:crypto on non-ASCII input')
1512
+ assert(H.leanHasSearchFlag(['-R', 'x']) && H.leanHasSearchFlag(['--search-path=o']) && H.leanHasSearchFlag(['--root', 'r']) && !H.leanHasSearchFlag(['-j4']),
1513
+ 'the search-flag guard recognises -R / --search-path(=) / --root and nothing else')
1514
+ }
1515
+
1516
+ // ---------- 35. async Lean: enqueue → settle(ok) → passed ----------
1517
+ console.log('\n[35] async Lean: enqueue returns at once, settle(ok) is the only path to passed')
1518
+ {
1519
+ const fake = fakeLean({})
1520
+ const h = makeHost({ pluginModule, subprocess: fake })
1521
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1522
+ await h.settleSpawns()
1523
+ await h.callTool('vibe_v5_set', { formalVerify: 'encourage' })
1524
+ const arch = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-async', content: 'theorem p_async : 1 + 1 = 2 := by decide\n' })
1525
+ assert(arch.ok === true && arch.async && arch.async.state === 'queued' && arch.status === 'attempted',
1526
+ '★ the archive ENQUEUES and returns async:{jobId,state} with the object at attempted (' + JSON.stringify(arch).slice(0, 150) + ')')
1527
+ assert(fake.calls.length === 0, '★ the compiler has NOT been started when the tool returns')
1528
+ const st0 = await h.callTool('vibe_v5_status', {})
1529
+ assert(st0.formal.passed.indexOf('p-async') === -1, 'the queued object is not passed')
1530
+ const listed = await h.callTool('vibe_v5_lean_job', {})
1531
+ assert(listed.ok === true && listed.jobs.some(j => j.jobId === arch.jobId), 'lean_job lists the job (' + JSON.stringify(listed.jobs).slice(0, 140) + ')')
1532
+ const waited = await h.callTool('vibe_v5_lean_job', { jobId: arch.jobId, waitMs: 3000 })
1533
+ assert(waited.ok === true && waited.job.state === 'settled' && waited.job.exitCode === 0 && waited.job.proof === 'Verified/Lean/p-async.lean',
1534
+ '★ lean_job {jobId,waitMs} waits for the settle and reports the archived proof (' + JSON.stringify(waited.job).slice(0, 170) + ')')
1535
+ const st1 = await h.callTool('vibe_v5_status', {})
1536
+ assert(st1.formal.passed.indexOf('p-async') !== -1 && st1.formal.objects.find(o => o.target === 'p-async').async.jobId === arch.jobId,
1537
+ '★ ONLY the settled(ok) job set the object passed, and its receipt is on the record')
1538
+ assert(existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-async.lean')),
1539
+ 'the proof is archived under Verified/Lean/<id>.lean')
1540
+ const argv = fake.calls[0].argv
1541
+ const root = join(h.WS, 'VibeMath').replace(/\\/g, '/')
1542
+ const si = argv.indexOf('--search-path')
1543
+ assert(si > 0 && argv[si + 1] === root && si + 2 === argv.length - 1 && /\.lean$/.test(argv[argv.length - 1]),
1544
+ '★ argv carries `--search-path <ABSOLUTE VibeMath root>` right before the file (' + JSON.stringify(argv) + ')')
1545
+ assert(fake.calls.length === 1, 'exactly one compile ran for the settle (got ' + fake.calls.length + ')')
1546
+ }
1547
+
1548
+ // ---------- 36. failure / timeout / dispose never verify ----------
1549
+ console.log('\n[36] async Lean: failure, timeout and session dispose never verify')
1550
+ {
1551
+ const fakeFail = fakeLean({ alwaysFail: true })
1552
+ const h = makeHost({ pluginModule, subprocess: fakeFail })
1553
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1554
+ await h.settleSpawns()
1555
+ const a = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-fail', content: 'theorem p_fail : 1 = 2 := by decide\n' })
1556
+ const w = await h.callTool('vibe_v5_lean_job', { jobId: a.jobId, waitMs: 3000 })
1557
+ assert(w.job.state === 'failed', '★ a failed compile settles as failed (' + w.job.state + ')')
1558
+ assert(!existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-fail.lean')),
1559
+ '★ no Verified/Lean file is written for a failure')
1560
+ assert((await h.callTool('vibe_v5_status', {})).formal.passed.indexOf('p-fail') === -1, 'the failed object stays attempted')
1561
+ // (b) the budget terminates the process
1562
+ const fakeHang = fakeLean({ defer: true })
1563
+ const h2 = makeHost({ pluginModule, subprocess: fakeHang })
1564
+ await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1565
+ await h2.settleSpawns()
1566
+ seedLeanFile(h2, 'Formal/good.lean', 'theorem good : 1 + 1 = 2 := by decide\n')
1567
+ const r = await h2.callTool('vibe_v5_lean_run', { file: 'Formal/good.lean', timeout_ms: 1000 })
1568
+ assert(r.ok === true && r.async.state === 'queued', 'the run is queued')
1569
+ const w2 = await h2.callTool('vibe_v5_lean_job', { jobId: r.jobId, waitMs: 5000 })
1570
+ assert(w2.job.state === 'timeout' && fakeHang.terminated >= 1,
1571
+ '★ the per-job budget TERMINATED the process and the job is timeout (' + JSON.stringify({ s: w2.job.state, t: fakeHang.terminated }) + ')')
1572
+ // (c) session dispose (the plugin-unload disposer)
1573
+ const fakeGate = fakeLean({ defer: true })
1574
+ const h3 = makeHost({ pluginModule, subprocess: fakeGate })
1575
+ await h3.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1576
+ await h3.settleSpawns()
1577
+ const a3 = await h3.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dispose', content: 'theorem p_dispose : 1 + 1 = 2 := by decide\n' })
1578
+ await sleep(40)
1579
+ assert(fakeGate.calls.length >= 1, 'precondition: the compile is in flight')
1580
+ h3.effectDisposers[0]()
1581
+ assert(fakeGate.terminated >= 1, '★ dispose TERMINATED the in-flight compiler (no orphan process)')
1582
+ const w3 = await h3.callTool('vibe_v5_lean_job', { jobId: a3.jobId })
1583
+ assert(w3.job.state === 'interrupted', '★ the job is marked interrupted, not settled (' + w3.job.state + ')')
1584
+ assert((await h3.callTool('vibe_v5_status', {})).formal.passed.indexOf('p-dispose') === -1, '★ dispose NEVER verifies an object')
1585
+ }
1586
+
1587
+ // ---------- 37. content-hash dedupe ----------
1588
+ console.log('\n[37] identical re-archive is de-duplicated (no rewrite, no recompile)')
1589
+ {
1590
+ const fake = fakeLean({})
1591
+ const h = makeHost({ pluginModule, subprocess: fake })
1592
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1593
+ await h.settleSpawns()
1594
+ const PROOF = 'theorem p_dup : 2 + 2 = 4 := by decide\n'
1595
+ const one = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dup', content: PROOF })
1596
+ await h.callTool('vibe_v5_lean_job', { jobId: one.jobId, waitMs: 3000 })
1597
+ const before = fake.calls.length
1598
+ const again = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dup', content: PROOF })
1599
+ assert(again.deduped === true && fake.calls.length === before,
1600
+ '★ identical content is deduped: no new compile (' + JSON.stringify({ d: again.deduped, before, after: fake.calls.length }) + ')')
1601
+ const diff = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dup', content: PROOF + '-- changed\n' })
1602
+ assert(diff.deduped !== true && diff.async && diff.async.state === 'queued', 'different content is NOT deduped (a new job is queued)')
1603
+ const DEF = 'def ZDup := Fin 2\n'
1604
+ const d1 = await h.callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZDup', content: DEF })
1605
+ await h.callTool('vibe_v5_lean_job', { jobId: d1.jobId, waitMs: 3000 })
1606
+ const callsAfter = fake.calls.length
1607
+ const d2 = await h.callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZDup', content: DEF })
1608
+ assert(d2.deduped === true && fake.calls.length === callsAfter, '★ the global library dedupes on the same content hash too')
1609
+ }
1610
+
1611
+ // ---------- 38. lean_read: verbatim text + path guard ----------
1612
+ console.log('\n[38] lean_read returns archived text verbatim and refuses a path-shaped name')
1613
+ {
1614
+ const fake = fakeLean({})
1615
+ const h = makeHost({ pluginModule, subprocess: fake })
1616
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1617
+ await h.settleSpawns()
1618
+ const body = 'def ZRead : Nat := 7\n'
1619
+ const a = await h.callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZRead', content: body })
1620
+ await h.callTool('vibe_v5_lean_job', { jobId: a.jobId, waitMs: 3000 })
1621
+ const rd = await h.callTool('vibe_v5_lean_read', { name: 'ZRead' })
1622
+ assert(rd.ok === true && rd.kind === 'lib' && rd.text === body && rd.bytes === body.length && /^[0-9a-f]{64}$/.test(rd.sha256) && rd.truncated === false,
1623
+ '★ lean_read returns the archived text verbatim with sha/bytes (' + JSON.stringify({ k: rd.kind, b: rd.bytes }) + ')')
1624
+ const esc = await h.callTool('vibe_v5_lean_read', { name: '../ZRead' })
1625
+ 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) + ')')
1626
+ assert((await h.callTool('vibe_v5_lean_read', { name: 'C:/x/ZRead' })).ok === false, 'an absolute name is refused')
1627
+ const miss = await h.callTool('vibe_v5_lean_read', { name: 'NoSuchThing' })
1628
+ assert(miss.ok === false && miss.code === 'V5_NOT_FOUND', 'an unknown name is V5_NOT_FOUND')
1629
+ assert((await h.callTool('vibe_v5_lean_read', { name: 'ZRead', kind: 'bogus' })).ok === false, 'a bogus kind is refused')
1630
+ }
1631
+
1632
+ // ---------- 39. a job only settles the BUILD CONTEXT it was queued under ----------
1633
+ console.log('\n[39] a job only settles the build context it was queued under')
1634
+ {
1635
+ const fake = fakeLean({ defer: true })
1636
+ const h = makeHost({ pluginModule, subprocess: fake })
1637
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1638
+ await h.settleSpawns()
1639
+ seedLeanFile(h, 'Formal/good.lean', 'theorem good : 1 + 1 = 2 := by decide\n')
1640
+ await h.callTool('vibe_v5_lean_run', { file: 'Formal/good.lean' })
1641
+ await sleep(40)
1642
+ const proof = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-ctx', content: 'theorem p_ctx : 3 + 3 = 6 := by decide\n' })
1643
+ await h.callTool('vibe_v5_set', { leanArgs: ['-Dctx=1'] }) // retune AFTER queueing
1644
+ for (let i = 0; i < 10; i++) { fake.releaseAll(); await sleep(25) }
1645
+ const w = await h.callTool('vibe_v5_lean_job', { jobId: proof.jobId, waitMs: 1000 })
1646
+ assert(w.job.state === 'failed' && w.job.buildMatched === false,
1647
+ '★ a job compiled under retuned args does NOT settle (' + JSON.stringify({ s: w.job.state, b: w.job.buildMatched }) + ')')
1648
+ const st = await h.callTool('vibe_v5_status', {})
1649
+ assert(st.formal.passed.indexOf('p-ctx') === -1, '★ the object stays attempted on a build-context mismatch')
1650
+ assert(!existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-ctx.lean')),
1651
+ 'no Verified/Lean file for a mismatched build')
1652
+ // (b) the CONTENT changed after the job was queued: the compile succeeds, but it compiled
1653
+ // something else — the object must stay attempted.
1654
+ const blocker2 = await h.callTool('vibe_v5_lean_run', { file: 'Formal/good.lean' })
1655
+ await sleep(40)
1656
+ const p2 = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-hash', content: 'theorem p_hash : 4 + 4 = 8 := by decide\n' })
1657
+ seedLeanFile(h, 'Formal/p-hash.lean', 'theorem p_hash : 4 + 4 = 8 := by decide\n-- edited while queued\n')
1658
+ for (let i = 0; i < 10; i++) { fake.releaseAll(); await sleep(25) }
1659
+ const w2 = await h.callTool('vibe_v5_lean_job', { jobId: p2.jobId, waitMs: 1000 })
1660
+ assert(w2.job.exitCode === 0 && w2.job.state === 'failed' && w2.job.buildMatched === true,
1661
+ '★ 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 }) + ')')
1662
+ assert(!existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-hash.lean')),
1663
+ 'a changed-content compile writes no proof either')
1664
+ }
1665
+
1666
+ // ---------- 40. leanJobsMaxParallel (default 1 = serial) ----------
1667
+ console.log('\n[40] leanJobsMaxParallel caps simultaneous compiles')
1668
+ {
1669
+ const fake = fakeLean({ defer: true })
1670
+ const h = makeHost({ pluginModule, subprocess: fake })
1671
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
1672
+ await h.settleSpawns()
1673
+ seedLeanFile(h, 'Formal/a.lean', 'theorem a : 1 = 1 := rfl\n')
1674
+ seedLeanFile(h, 'Formal/b.lean', 'theorem b : 2 = 2 := rfl\n')
1675
+ await h.callTool('vibe_v5_lean_run', { file: 'Formal/a.lean' })
1676
+ await h.callTool('vibe_v5_lean_run', { file: 'Formal/b.lean' })
1677
+ await sleep(50)
1678
+ assert(fake.calls.length === 1, '★ with the default maxParallel=1 only ONE compile is in flight (got ' + fake.calls.length + ')')
1679
+ const listed = await h.callTool('vibe_v5_lean_job', {})
1680
+ assert(listed.maxParallel === 1 && listed.jobs.filter(j => j.state === 'queued').length === 1,
1681
+ 'lean_job reports maxParallel=1 with the second job queued (' + JSON.stringify(listed).slice(0, 150) + ')')
1682
+ const q = listed.jobs.find(j => j.state === 'queued')
1683
+ await h.callTool('vibe_v5_set', { leanJobsMaxParallel: 2 })
1684
+ await h.callTool('vibe_v5_lean_job', { jobId: q.jobId, waitMs: 150 })
1685
+ await sleep(30)
1686
+ assert(fake.calls.length === 2, '★ raising maxParallel to 2 starts the queued job as well (got ' + fake.calls.length + ')')
1687
+ for (let i = 0; i < 8; i++) { fake.releaseAll(); await sleep(20) }
1688
+ }
1689
+
1690
+ // ---------- 41. the daily/verify Lean text is gated by leanInitiative ----------
1691
+ console.log('\n[41] leanInitiative gates the daily Lean reminders; the async rule is in both blocks')
1692
+ {
1693
+ const h = makeHost({ pluginModule })
1694
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
1695
+ await h.settleSpawns()
1696
+ const promptFor = async (member, plan) => {
1697
+ if (plan) await h.callTool('vibe_v5_set', plan)
1698
+ await h.callTool('vibe_v5_say', { to: member, text: '请继续推进。' }, h.childAgent(h.childOf('acad')))
1699
+ const w = await h.peekWakeOf(member, 3000)
1700
+ if (!w) return ''
1701
+ h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
1702
+ await sleep(25)
1703
+ return w.text
1704
+ }
1705
+ const normal = await promptFor('r-1', { formalVerify: 'off', leanInitiative: 'normal' })
1706
+ assert(normal.indexOf('【顺手形式化') === -1, "leanInitiative=normal + formalVerify=off keeps today's behaviour (no daily Lean line)")
1707
+ const eager = await promptFor('r-1', { leanInitiative: 'eager' })
1708
+ assert(eager.indexOf('【顺手形式化') !== -1 && eager.indexOf('**主动**') !== -1,
1709
+ '★ leanInitiative=eager injects the daily line even when formalVerify is off')
1710
+ assert(eager.indexOf('判断标准:①') !== -1 && eager.indexOf('先 vibe_v5_lean_lib 查已有库') !== -1 && eager.indexOf('**查不到再新写**') !== -1,
1711
+ '★ the three criteria + reuse-first ride in the daily line')
1712
+ assert(eager.indexOf('import Formal.Lib.<name>') !== -1 && eager.indexOf('vibe_v5_lean_read') !== -1, 'the import/read reuse routes are named')
1713
+ assert(eager.indexOf('没把握就记 blocked') !== -1, 'the no-confidence→blocked rule rides in the daily line')
1714
+ assert(eager.indexOf('leanAsync=true') !== -1 && eager.indexOf('不得把该对象当成已通过') !== -1, '★ the async honesty rule rides in the daily line')
1715
+ const offInit = await promptFor('r-1', { formalVerify: 'encourage', leanInitiative: 'off' })
1716
+ assert(offInit.indexOf('【顺手形式化') === -1, '★ leanInitiative=off suppresses the daily line even when formalVerify is on')
1717
+ // The VERIFY block carries the async rule too.
1718
+ await h.callTool('vibe_v5_set', { formalVerify: 'encourage', leanInitiative: 'normal' })
1719
+ 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')))
1720
+ await h.callTool('vibe_v5_propose_verify', { target: 'p-leanv', kind: 'proposition', reason: '测试' }, h.childAgent(h.childOf('r-1')))
1721
+ const vw = await h.peekWakeOf('r-1', 3000)
1722
+ const vtxt = vw ? vw.text : ''
1723
+ assert(/形式化只写你有把握的版本/.test(vtxt) && /不得\*\*在它落地前声称已通过/.test(vtxt),
1724
+ '★ the verify block carries the confidence + async-honesty lines (' + JSON.stringify(vtxt.slice(-260)) + ')')
1725
+ }
1726
+
1727
+ // ---------- 42. per-injection-site gate: every Lean prompt site, both directions ----------
1728
+ // Lens-1 (HIGH coverage gap): the gate `leanInitiative !== 'off' && (formalOn() || eager)` is
1729
+ // correct, but a static scan found ZERO assertions naming `eager` per SITE. v5 has exactly two
1730
+ // DAILY-line sites (normalPrompt, checkpointPrompt) plus two mode-gated Lean sites (briefBlock's
1731
+ // `[形式化]` state line and verifyPrompt's formalPromptBlock). Each site below is asserted in BOTH
1732
+ // directions: (1) formalVerify=off + leanInitiative=eager ⇒ that site HAS the daily line (with the
1733
+ // `**主动**` mark) while the VERIFY block stays absent; (2) leanInitiative=off ⇒ that site has NO
1734
+ // daily line while the mode/verify text still follows formalVerify.
1735
+ console.log('\n[42] every Lean prompt site obeys the leanInitiative gate in both directions')
1736
+ {
1737
+ const DAILY = '【顺手形式化'
1738
+ const EAGER = '**主动**'
1739
+ // The verify-phase BLOCK header (its mode suffix distinguishes it from the normal prompt's
1740
+ // reply contract, which also *mentions* 【Lean 形式化验证】).
1741
+ const VERIFY_BLOCK = '【Lean 形式化验证('
1742
+ const STATE = '[形式化]'
1743
+ const h = makeHost({ pluginModule })
1744
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
1745
+ await h.settleSpawns()
1746
+ const setAndAssert = async (plan) => {
1747
+ const r = await h.callTool('vibe_v5_set', plan)
1748
+ assert(r.ok === true, 'precondition: vibe_v5_set applied ' + JSON.stringify(plan) + ' (' + JSON.stringify(r).slice(0, 120) + ')')
1749
+ return r.params
1750
+ }
1751
+ // Pending turns must be ANSWERED, never dropped: a discarded wake leaves its member busy
1752
+ // forever, so the scheduler would have no idle member left to send a heartbeat to.
1753
+ const flushWakes = async (rounds) => {
1754
+ for (let i = 0; i < (rounds || 8); i++) {
1755
+ const w = h.wakes.shift()
1756
+ if (!w) return
1757
+ h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
1758
+ await sleep(10)
1759
+ }
1760
+ }
1761
+ const normalFor = async (member) => {
1762
+ await h.callTool('vibe_v5_say', { to: member, text: '请继续推进。' }, h.childAgent(h.childOf('acad')))
1763
+ const w = await h.peekWakeOf(member, 3000)
1764
+ if (!w) return ''
1765
+ h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
1766
+ await sleep(25)
1767
+ return w.text
1768
+ }
1769
+ // ── SITE 1: normalPrompt ────────────────────────────────────────────────────
1770
+ const pA = await setAndAssert({ formalVerify: 'off', leanInitiative: 'eager' })
1771
+ assert(pA.formalVerify === 'off' && pA.leanInitiative === 'eager', 'precondition: formalVerify=off + leanInitiative=eager is live')
1772
+ await flushWakes()
1773
+ const s1eager = await normalFor('r-1')
1774
+ assert(s1eager.indexOf(DAILY) !== -1 && s1eager.indexOf(EAGER) !== -1,
1775
+ 'S1 normalPrompt: formalVerify=off + leanInitiative=eager ⇒ the daily line IS injected (with **主动**)')
1776
+ assert(s1eager.indexOf(VERIFY_BLOCK) === -1 && s1eager.indexOf(STATE) === -1,
1777
+ 'S1 normalPrompt: …while the VERIFY block and the [形式化] state line stay ABSENT (formalVerify=off)')
1778
+ const pB = await setAndAssert({ formalVerify: 'encourage', leanInitiative: 'off' })
1779
+ assert(pB.formalVerify === 'encourage' && pB.leanInitiative === 'off', 'precondition: formalVerify=encourage + leanInitiative=off is live')
1780
+ await sleep(30) // let prompts built with the previous params drain
1781
+ await flushWakes()
1782
+ const s1off = await normalFor('r-1')
1783
+ assert(s1off.indexOf(DAILY) === -1, 'S1 normalPrompt: leanInitiative=off ⇒ NO daily line')
1784
+ assert(s1off.indexOf(VERIFY_BLOCK) === -1 && s1off.indexOf(STATE) !== -1,
1785
+ 'S1 normalPrompt: …but the mode-gated [形式化] state line still follows formalVerify=encourage (stateIdx=' + s1off.indexOf(STATE) + ')')
1786
+ // ── SITE 2: checkpointPrompt (the heartbeat) ────────────────────────────────
1787
+ // Delivered to the longest-idle member that owns no in-progress task, so the queue is answered
1788
+ // FIFO (a live institute keeps working) until a 【心跳检查】 turn shows up.
1789
+ const checkpointFor = async (plan) => {
1790
+ await setAndAssert(plan)
1791
+ await flushWakes()
1792
+ const seen = []
1793
+ for (let i = 0; i < 30; i++) {
1794
+ // A tuning call drives one scheduling pass, exactly like a real institute where events keep
1795
+ // arriving; the heartbeat branch then picks the longest-idle member.
1796
+ await h.callTool('vibe_v5_set', { maxParallel: i % 2 === 0 ? 3 : 4 })
1797
+ const w = await h.peekWakeWhere(() => true, 300)
1798
+ if (!w) continue
1799
+ seen.push(w.text.slice(0, 20))
1800
+ const isCheckpoint = w.text.indexOf('【心跳检查') !== -1
1801
+ h.fireEnd(w.childId, { progress: '继续推进。', solved: false, contextPct: 10 })
1802
+ await sleep(15)
1803
+ if (isCheckpoint) return { text: w.text, seen }
1804
+ }
1805
+ return { text: '', seen }
1806
+ }
1807
+ await h.callTool('vibe_v5_set', { activityTimeoutMs: 40, chatDigestMs: 600000, stallAutoMeetingMs: 600000 })
1808
+ const s2a = await checkpointFor({ formalVerify: 'off', leanInitiative: 'eager' })
1809
+ const s2st = await h.callTool('vibe_v5_status', {})
1810
+ assert(s2a.text !== '' && s2a.text.indexOf(DAILY) !== -1 && s2a.text.indexOf(EAGER) !== -1,
1811
+ '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 }) + ')')
1812
+ assert(s2a.text.indexOf(VERIFY_BLOCK) === -1, 'S2 checkpointPrompt: the VERIFY block stays absent with formalVerify=off')
1813
+ const s2b = await checkpointFor({ formalVerify: 'encourage', leanInitiative: 'off' })
1814
+ assert(s2b.text !== '' && s2b.text.indexOf(DAILY) === -1,
1815
+ 'S2 checkpointPrompt: leanInitiative=off ⇒ NO daily line (seen=' + JSON.stringify(s2b.seen) + ')')
1816
+ assert(s2b.text.indexOf(STATE) !== -1, 'S2 checkpointPrompt: …while the [形式化] state line still follows formalVerify=encourage')
1817
+ // ── SITE 3: verifyPrompt's formalPromptBlock ────────────────────────────────
1818
+ // Finish any verification that is already in flight (answering every voter turn) so that the
1819
+ // NEXT verify wake is necessarily built with the params we set afterwards.
1820
+ const drainVerify = async () => {
1821
+ for (let i = 0; i < 25; i++) {
1822
+ const st = await h.callTool('vibe_v5_status', {})
1823
+ if (!st.verify) return true
1824
+ await h.callTool('vibe_v5_set', { maxParallel: i % 2 === 0 ? 3 : 4 })
1825
+ const w = await h.peekWakeWhere((p) => p.indexOf('【求真表决') !== -1, 250)
1826
+ if (w) {
1827
+ const tm = /"target"\s*:\s*"([^"]+)"/.exec(w.text)
1828
+ h.fireEnd(w.childId, { verdict: { target: tm ? tm[1] : st.verify.target, verdict: 0.5, reason: '存疑' }, contextPct: 20 })
1829
+ }
1830
+ await sleep(20)
1831
+ }
1832
+ return false
1833
+ }
1834
+ const verifyFor = async (plan) => {
1835
+ await drainVerify()
1836
+ await setAndAssert(plan)
1837
+ const id = 'p-site-' + Math.random().toString(36).slice(2, 8)
1838
+ await h.callTool('vibe_v5_record_proposition', { id, statement: '站点门控', value: 0.6, motive: 'm', p: 0.9 }, h.childAgent(h.childOf('r-1')))
1839
+ await h.callTool('vibe_v5_propose_verify', { target: id, kind: 'proposition', reason: 'x' }, h.childAgent(h.childOf('r-1')))
1840
+ for (let i = 0; i < 25; i++) {
1841
+ await h.callTool('vibe_v5_set', { maxParallel: i % 2 === 0 ? 3 : 4 })
1842
+ const w = await h.peekWakeWhere((p) => p.indexOf('【求真表决') !== -1, 300)
1843
+ if (!w) continue
1844
+ const tm = /"target"\s*:\s*"([^"]+)"/.exec(w.text)
1845
+ h.fireEnd(w.childId, { verdict: { target: tm ? tm[1] : id, verdict: 0.5, reason: '存疑' }, contextPct: 20 })
1846
+ await sleep(25)
1847
+ return w.text
1848
+ }
1849
+ return ''
1850
+ }
1851
+ const s3off = await verifyFor({ formalVerify: 'off', leanInitiative: 'eager' })
1852
+ assert(s3off.indexOf('【求真表决') !== -1 && s3off.indexOf(VERIFY_BLOCK) === -1,
1853
+ 'S3 verifyPrompt: formalVerify=off ⇒ NO verify-phase Lean block even when leanInitiative=eager')
1854
+ assert(s3off.indexOf(DAILY) === -1, 'S3 verifyPrompt: the daily line is never injected into the verify prompt')
1855
+ const s3on = await verifyFor({ formalVerify: 'encourage', leanInitiative: 'off' })
1856
+ assert(s3on.indexOf('【求真表决') !== -1 && s3on.indexOf(VERIFY_BLOCK) !== -1,
1857
+ 'S3 verifyPrompt: formalVerify=encourage ⇒ the verify-phase block appears even with leanInitiative=off')
1858
+ assert(s3on.indexOf(DAILY) === -1, 'S3 verifyPrompt: …and the daily line is still absent there')
1859
+ }
1860
+
1411
1861
  console.log('')
1412
1862
  console.log('passed=' + passed + ' failed=' + failed)
1413
1863
  if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }