dsh-vibe-math 2.7.0 → 2.7.2

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 (55) hide show
  1. package/README.en.md +681 -685
  2. package/README.md +9 -13
  3. package/cordis.patch.yml +12 -6
  4. package/docs/AUDIT-CHECKLIST.md +518 -518
  5. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  6. package/docs/final-paper.md +172 -172
  7. package/docs/formal-verification.md +588 -588
  8. package/docs/math-computation.md +187 -121
  9. package/docs/release-notes/README.en.md +48 -0
  10. package/docs/release-notes/README.md +47 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.7.1.en.md +86 -0
  12. package/docs/release-notes/RELEASE-NOTES-2.7.1.md +86 -0
  13. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
  14. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
  15. package/docs/release-notes/TEMPLATE.md +120 -0
  16. package/docs/test-timing.md +154 -154
  17. package/package.json +208 -201
  18. package/prompt-corpus-persona/persona-corpus.json +8 -8
  19. package/prompt-corpus-persona/persona-corpus.md +24 -12
  20. package/prompt-corpus-v2/formal-verify-v2.json +176 -98
  21. package/prompt-corpus-v2/formal-verify-v2.md +1161 -96
  22. package/prompt-corpus-v3/formal-verify-v3.json +201 -63
  23. package/prompt-corpus-v3/formal-verify-v3.md +1401 -57
  24. package/prompt-corpus-v4/formal-verify-v4.json +10 -10
  25. package/prompt-corpus-v4/formal-verify-v4.md +40 -20
  26. package/prompt-corpus-v5/prompt-corpus-v5.json +81 -81
  27. package/prompt-corpus-v5/prompt-corpus-v5.md +199 -128
  28. package/tests/audit-math-computation-contract.mjs +56 -1
  29. package/tests/audit-math-computation-parity.mjs +30 -1
  30. package/tests/audit-math-computation-sensitivity.mjs +2 -2
  31. package/tests/audit-v5-sensitivity.mjs +3 -1
  32. package/tests/e2e-v5-round2.test.mjs +116 -0
  33. package/tests/formal-verify-v2.test.mjs +108 -2
  34. package/tests/formal-verify-v3.test.mjs +117 -2
  35. package/tests/formal-verify-v4.test.mjs +87 -0
  36. package/tests/helpers/math-computation-fake-seam.mjs +30 -2
  37. package/tests/math-computation-shared.test.mjs +430 -3
  38. package/vibe-math-v2/agent.cordis.yml +4 -2
  39. package/vibe-math-v2/math-computation.js +509 -55
  40. package/vibe-math-v2/math-engines.js +47 -6
  41. package/vibe-math-v2/vibe-math-v2.js +92 -12
  42. package/vibe-math-v3/agent.cordis.yml +4 -2
  43. package/vibe-math-v3/math-computation.js +509 -55
  44. package/vibe-math-v3/math-engines.js +47 -6
  45. package/vibe-math-v3/vibe-math-v3.js +77 -14
  46. package/vibe-math-v4/agent.cordis.yml +8 -4
  47. package/vibe-math-v4/math-computation.js +509 -55
  48. package/vibe-math-v4/math-engines.js +47 -6
  49. package/vibe-math-v4/vibe-math-v4.js +70 -15
  50. package/vibe-math-v5/agent.cordis.yml +8 -4
  51. package/vibe-math-v5/math-computation.js +509 -55
  52. package/vibe-math-v5/math-engines.js +47 -6
  53. package/vibe-math-v5/vibe-math-v5.js +152 -31
  54. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1660 -1655
  55. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -44,6 +44,12 @@ function ok(cond, label, detail) {
44
44
  }
45
45
  const read = (rel) => readFileSync(join(ROOT, rel), 'utf8')
46
46
 
47
+ // round-8: the persona rule lines must be VERBATIM copies of the shared module constants, so a module
48
+ // wording change can never silently leave one preset behind. Import the module once (ESM top level).
49
+ const MATH_MODULE = await import(new URL('../vibe-math-v2/math-computation.js', import.meta.url).href)
50
+ const ARCHIVE_RULE_ZH = MATH_MODULE.MATH_ARCHIVE_WORKFLOW_LINE
51
+ const ARCHIVE_RULE_EN = MATH_MODULE.MATH_ARCHIVE_WORKFLOW_LINE_EN
52
+
47
53
  function block(src, startMarker, endMarker) {
48
54
  const i = src.indexOf(startMarker)
49
55
  if (i === -1) return ''
@@ -167,7 +173,48 @@ for (const P of PRESETS) {
167
173
  const wf = blocks.filter((b) => b.indexOf('scriptChanged') !== -1 && b.indexOf("mode:'file'") !== -1).length
168
174
  ok(wf >= 2, tag + 'both persona blocks carry the archive->edit->re-run rule (scriptChanged + mode:file)', 'blocks with the rule: ' + wf)
169
175
  ok(js.indexOf('MATH_ARCHIVE_WORKFLOW_LINE') !== -1, tag + 'references MATH_ARCHIVE_WORKFLOW_LINE (zh persona text source)')
176
+
177
+ // 4c. round-6 (A): both blocks must carry the substitution-honesty rule (zh, plus en on v4/v5).
178
+ const subZh = blocks.filter((b) => b.indexOf('替代必须声明') !== -1).length
179
+ ok(subZh >= 2, tag + 'both persona blocks carry the substitution-honesty rule (zh)', 'blocks with the rule: ' + subZh)
180
+ if (P.dir === 'vibe-math-v4' || P.dir === 'vibe-math-v5') {
181
+ const subEn = blocks.filter((b) => b.indexOf('Declare substitutions') !== -1).length
182
+ ok(subEn >= 2, tag + 'both persona blocks carry the substitution-honesty rule (en)', 'blocks with the rule: ' + subEn)
183
+ }
184
+ ok(js.indexOf('MATH_SUBSTITUTION_RULE_LINE') !== -1, tag + 'references MATH_SUBSTITUTION_RULE_LINE (zh persona text source)')
170
185
  if (P.dir === 'vibe-math-v4' || P.dir === 'vibe-math-v5') ok(js.indexOf('MATH_ARCHIVE_WORKFLOW_LINE_EN') !== -1, tag + 'references MATH_ARCHIVE_WORKFLOW_LINE_EN (en persona text source)')
186
+
187
+ // 4d. round-7 (fix 2): every preset must wire the bundled-runtime discovery seam, otherwise a
188
+ // machine whose only interpreter is DSH's own runtime still reports MATH_ENGINE_NOT_FOUND.
189
+ ok(js.indexOf('runtimeRoots') !== -1 && js.indexOf('listDirAbs') !== -1, tag + 'wires runtimeRoots + listDirAbs (DSH bundled-runtime discovery)')
190
+
191
+ // 4e. round-8 (P0/D3): every persona block mentioning the archived scriptPath must ALSO say how to
192
+ // open it (absolute `receipt.scriptAbs` / join with `receipt.cwd`): member file tools resolve
193
+ // against the SESSION CWD, while scriptPath is project-root relative.
194
+ const scriptBlocks = blocks.filter((b) => b.indexOf('scriptPath') !== -1)
195
+ const openableBlocks = scriptBlocks.filter((b) => /receipt\.scriptAbs/.test(b))
196
+ ok(scriptBlocks.length >= 2, tag + 'both persona blocks mention the archived scriptPath', 'blocks: ' + scriptBlocks.length)
197
+ ok(openableBlocks.length === scriptBlocks.length && openableBlocks.length >= 2, tag + '★ every scriptPath mention also gives the ABSOLUTE path receipt.scriptAbs', 'openable: ' + openableBlocks.length + '/' + scriptBlocks.length)
198
+
199
+ // 4e-ter. VERBATIM-from-constant guard: the archive-workflow rule must appear in BOTH persona
200
+ // blocks exactly as the shared module constant defines it (a wording change in the module that is
201
+ // not re-synced into a preset is a silent drift - this assertion reports the found count).
202
+ const zhRule = String(ARCHIVE_RULE_ZH).trim()
203
+ const enRule = String(ARCHIVE_RULE_EN).trim()
204
+ const zhFound = blocks.filter((b) => b.indexOf(zhRule) !== -1).length
205
+ ok(zhFound >= 2, tag + 'the zh archive-workflow rule appears VERBATIM in BOTH persona blocks', 'found ' + zhFound)
206
+ if (P.dir === 'vibe-math-v4' || P.dir === 'vibe-math-v5') {
207
+ const enFound = blocks.filter((b) => b.indexOf(enRule) !== -1).length
208
+ ok(enFound >= 2, tag + 'the en archive-workflow rule appears VERBATIM in BOTH persona blocks', 'found ' + enFound)
209
+ }
210
+ }
211
+ // 4e-bis. the SHARED module's rule text must carry the same instruction (zh + en), and the receipt
212
+ // fields it points at must exist in the module.
213
+ {
214
+ const mod = read('vibe-math-v2/math-computation.js')
215
+ ok(/receipt\.scriptAbs/.test(mod), 'the shared module rule mentions receipt.scriptAbs')
216
+ ok(/会话 cwd/.test(mod) && /SESSION CWD/.test(mod), 'the shared module rule states the session-cwd fact in BOTH languages')
217
+ ok(/scriptAbs: scriptAbs, cwd: root/.test(mod), 'the shared module WRITES scriptAbs + cwd into the receipt')
171
218
  }
172
219
 
173
220
  // 5. README (both languages) document the six parameters
@@ -181,7 +228,15 @@ for (const f of ['README.md', 'README.en.md']) {
181
228
  {
182
229
  const doc = existsSync(join(ROOT, 'docs/math-computation.md')) ? read('docs/math-computation.md') : ''
183
230
  ok(doc.indexOf('字符串级') !== -1 && doc.indexOf('不解析符号链接') !== -1, 'docs §6 states the path guard is string-level and does not resolve links/junctions')
184
- ok(doc.indexOf('只通过假 subprocess seam 验证') !== -1, 'docs §6 states engine execution is verified only through the fake subprocess seam')
231
+ // round-9 (F0): the boundary paragraph must pin the CORRECTED truth - real engines were verified on
232
+ // this machine, AND the still-unverified set is named. (The old claim "only through the fake
233
+ // subprocess seam" is stale and must not be pinned any more.)
234
+ ok(doc.indexOf('假 subprocess seam') !== -1 && doc.indexOf('不再') !== -1, 'docs §6 keeps the fake-subprocess seam but states it is no longer the only evidence')
235
+ ok(doc.indexOf('python 3.12.10') !== -1 && doc.indexOf('R 4.6.1') !== -1, 'docs §6 names the real engines/versions verified on this machine')
236
+ ok(doc.indexOf('scriptHash') !== -1 && doc.indexOf('native') !== -1, 'docs §6 states the native-vs-cli parity (same numbers + same scriptHash)')
237
+ ok(doc.indexOf('MATH_TIMEOUT') !== -1 && doc.indexOf('MATH_NONZERO_EXIT') !== -1 && doc.indexOf('MATH_MISSING_PACKAGES') !== -1, 'docs §6 states the real-engine edge codes that were confirmed')
238
+ ok(doc.indexOf('Octave / Julia 未安装') !== -1 && doc.indexOf('VERIFY') !== -1 && doc.indexOf('无法强制禁网') !== -1, 'docs §6 keeps the still-unverified set (no Octave/Julia, commercial VERIFY, no network/write enforcement)')
239
+ ok(doc.indexOf('只通过假 subprocess seam 验证') === -1, 'docs §6 no longer claims engine execution was verified ONLY through the fake seam (stale claim removed)')
185
240
  ok(doc.indexOf('串行') !== -1 && doc.indexOf('跨进程') !== -1, 'docs §6 states the in-process serialisation and the cross-process limitation')
186
241
  }
187
242
 
@@ -99,7 +99,11 @@ for (const f of MODULES) {
99
99
 
100
100
  // ---- 5. descriptor invariants ----
101
101
  {
102
- const E = await import(new URL('../vibe-math-v2/math-engines.js', import.meta.url).href)
102
+ // MATH_ENGINES_MODULE points this section at a MUTATED descriptor copy (round-6 mutants must be
103
+ // able to delete a dispatch manager or a scope reason without touching the repository).
104
+ const E = await import(process.env.MATH_ENGINES_MODULE
105
+ ? new URL('file:///' + String(process.env.MATH_ENGINES_MODULE).replace(/\\/g, '/')).href
106
+ : new URL('../vibe-math-v2/math-engines.js', import.meta.url).href)
103
107
  const order = E.MATH_ENGINE_ORDER
104
108
  ok(JSON.stringify(order) === JSON.stringify(['python', 'r', 'octave', 'julia', 'matlab', 'maple', 'wolfram', 'cli']), 'MATH_ENGINE_ORDER is exactly the P1 set')
105
109
  ok(E.MATH_ENGINES.cli.defaultOn === true, 'cli is flagged defaultOn')
@@ -114,6 +118,31 @@ for (const f of MODULES) {
114
118
  }
115
119
  ok(!!E.MATH_P2_ENGINES.sage && E.MATH_P2_ENGINES.sage.phase === 'P2', 'sage is kept as the P2 descriptor')
116
120
  ok(E.mathEngineCandidates('cli') === null, 'cli has no detection candidates')
121
+
122
+ // round-6 (B): python must ship the manager DISPATCH table, and each manager must carry both the
123
+ // install and the matching uninstall template (the audit is only honest if the uninstall matches).
124
+ const pm = E.MATH_ENGINES.python.install && E.MATH_ENGINES.python.install.managers
125
+ ok(!!pm, 'python install declares a managers{conda,mamba,uv,pip} dispatch table')
126
+ for (const mgr of ['conda', 'mamba', 'uv', 'pip']) {
127
+ ok(!!(pm && pm[mgr]), 'python dispatch has the ' + mgr + ' manager')
128
+ if (pm && pm[mgr]) {
129
+ // The user template is either the manager's own or (for the pip fallback) the engine-level one;
130
+ // that precedence is what opInstall implements (`mgrTmpl.userArgv || d.install.userArgv`).
131
+ const userTemplate = pm[mgr].userArgv || (mgr === 'pip' ? E.MATH_ENGINES.python.install.userArgv : null)
132
+ ok(Array.isArray(userTemplate) && userTemplate.length > 0, mgr + ' has a user-scope install template (own or the engine-level fallback)')
133
+ ok(Array.isArray(pm[mgr].uninstallArgv) && pm[mgr].uninstallArgv.length > 0, mgr + ' ships the MATCHING uninstall template')
134
+ }
135
+ }
136
+ ok(pm && /-c/.test(pm.conda.userArgv.join(' ')) && /conda-forge/.test(pm.conda.userArgv.join(' ')), 'conda installs from -c conda-forge')
137
+ ok(pm && pm.pip.systemArgv && pm.pip.systemArgv.length > 0, 'pip keeps a real system-scope template')
138
+ ok(!!(pm && pm.uv && pm.conda && pm.mamba) && !pm.uv.systemArgv && !pm.conda.systemArgv && !pm.mamba.systemArgv, 'uv/conda/mamba have NO system template (user-only by design)')
139
+
140
+ // round-6 (C): every user-only engine states WHY in the descriptor (the refusal message quotes it).
141
+ for (const name of ['r', 'octave', 'julia']) {
142
+ ok(!!(E.MATH_ENGINES[name].install && E.MATH_ENGINES[name].install.systemUnsupportedReason), name + ' explains why system scope is unsupported')
143
+ ok(!E.MATH_ENGINES[name].install.systemArgv, name + ' ships no system template (user-only by design)')
144
+ }
145
+ ok(!!pm.uv.systemUnsupportedReason && !!pm.conda.systemUnsupportedReason, 'conda/uv managers explain why system scope is unsupported')
117
146
  }
118
147
 
119
148
  // ── 6. round-3 item 3: the machine-readable refusal vocabulary cannot drift ──────────────────────
@@ -77,8 +77,8 @@ const PROBES = [
77
77
  {
78
78
  name: 'script-fields-dropped-from-return',
79
79
  file: 'math-computation.js',
80
- from: ' scriptPath: scriptRel, scriptHash: scriptHash,\n attempt: attempt, attemptDir: dir, baseRunDir: baseDir,\n scriptChanged: scriptChanged, scriptChangedDuringRun: scriptChangedDuringRun,\n sourceFile: fileRel || null,\n previousReceipt: receipt.previousReceipt,\n packages: pk.found,',
81
- to: ' packages: pk.found,',
80
+ from: ' engineInfo: receipt.engine,\n argv: assembled.argv.slice(),\n scriptPath: scriptRel, scriptHash: scriptHash,',
81
+ to: ' engineInfo: receipt.engine,\n argv: assembled.argv.slice(),',
82
82
  expect: 'scriptPath + scriptHash are in the return value AND the receipt',
83
83
  },
84
84
  {
@@ -68,7 +68,9 @@ const probes = [
68
68
  name: 'quorum-forced-to-one',
69
69
  ref: 'selfdrive-v5.mjs',
70
70
  guarantee: '④ only >= m boolean votes may verify (m must be min(quorumCap, |voters|))',
71
- from: " return Math.max(1, Math.min(cap, voterCount()))",
71
+ // The floor moved into `quorumMFrom(vc)` when the quorum became single-sourced (defect B of
72
+ // the 3rd self-test); the anchor follows the implementation, it is not relaxed.
73
+ from: " return Math.max(1, Math.min(cap, vc))",
72
74
  to: " return 1",
73
75
  },
74
76
  {
@@ -21,6 +21,18 @@ import { createHash } from 'node:crypto'
21
21
  import { tmpdir } from 'node:os'
22
22
  import { join, dirname, isAbsolute } from 'node:path'
23
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. round-9 adds the known per-OS INSTALL dirs, and this machine now really
26
+ // has python/R installed. This suite injects ONE fake subprocess for everything (LaTeX + math), so an
27
+ // unexpected engine probe would be recorded as a compiler call and would even write a fake paper.pdf.
28
+ // Pinning all of those roots at an empty dir keeps the fixture's premise ("no math engine here") true
29
+ // and the suite deterministic; production behaviour is unaffected.
30
+ const EMPTY_ENGINE_ROOT = mkdtempSync(join(tmpdir(), 'vibe-v5-e2e-nodsh-'))
31
+ process.env.DSH_HOME = EMPTY_ENGINE_ROOT
32
+ process.env.ProgramFiles = EMPTY_ENGINE_ROOT
33
+ process.env['ProgramFiles(x86)'] = EMPTY_ENGINE_ROOT
34
+ process.env.LOCALAPPDATA = EMPTY_ENGINE_ROOT
35
+
24
36
  // V5_PLUGIN lets a sensitivity probe point this suite at a deliberately broken copy.
25
37
  // Without it every probe against this suite silently tested the UNMUTATED plugin and was
26
38
  // reported as a "detection" only because the probe's own spawn failed — i.e. the whole
@@ -1858,6 +1870,110 @@ console.log('\n[42] every Lean prompt site obeys the leanInitiative gate in both
1858
1870
  assert(s3on.indexOf(DAILY) === -1, 'S3 verifyPrompt: …and the daily line is still absent there')
1859
1871
  }
1860
1872
 
1873
+ // ---------- 43. pre-start diagnosis + coherent quorum (architecture self-test defects 1/2) -------
1874
+ // The self-test (§18 of _oneoff/method3-scripted-driving.md) found that before `start`:
1875
+ // (1) record_progress / record_proposition / verdict answered a BARE `V5_MEMBER_NOT_FOUND`
1876
+ // with no hint about why or what to do first;
1877
+ // (2) `vibe_v5_status` reported `quorum.m=1` with `voters=0` — reading like a satisfied quorum.
1878
+ // Both are asserted here, INCLUDING the actionable `next` step, and the normal (post-start) path
1879
+ // is exercised in the same section so a follow-up self-test can see a non-error path.
1880
+ console.log('\n[43] pre-start tools explain themselves; the quorum is coherent before and after start')
1881
+ {
1882
+ const h = makeHost({ pluginModule })
1883
+ const pre = async (name, args) => await h.callTool(name, args, h.ROOT)
1884
+ const rProg = await pre('vibe_v5_record_progress', { content: '自我测试' })
1885
+ assert(rProg.ok === false && rProg.code === 'V5_MEMBER_NOT_FOUND' && /尚未启动/.test(String(rProg.message || '')) &&
1886
+ rProg.next && rProg.next.kind === 'start' && rProg.next.tool === 'vibe_v5_start',
1887
+ '★ pre-start record_progress: the code is unchanged but the answer names the state + the next step (' + JSON.stringify(rProg).slice(0, 200) + ')')
1888
+ const rProp = await pre('vibe_v5_record_proposition', { statement: 's', value: 1, motive: 'm', p: 0.5 })
1889
+ assert(rProp.ok === false && rProp.code === 'V5_MEMBER_NOT_FOUND' && /尚未启动/.test(String(rProp.message || '')) && rProp.next && rProp.next.kind === 'start',
1890
+ '★ pre-start record_proposition: same actionable diagnosis (' + JSON.stringify(rProp).slice(0, 160) + ')')
1891
+ const rVer = await pre('vibe_v5_verdict', { target: 'p-x', verdict: 1 })
1892
+ assert(rVer.ok === false && rVer.code === 'V5_MEMBER_NOT_FOUND' && /尚未启动/.test(String(rVer.message || '')) && rVer.next && rVer.next.kind === 'start',
1893
+ '★ pre-start verdict: same actionable diagnosis (' + JSON.stringify(rVer).slice(0, 160) + ')')
1894
+ const st0 = await pre('vibe_v5_status', {})
1895
+ assert(st0.quorum && st0.quorum.started === false && st0.quorum.m === 0 && st0.quorum.voterCount === 0 && st0.quorum.phase === 'not-started',
1896
+ '★ pre-start quorum is COHERENT: started=false, m=0, voters=0 (' + JSON.stringify(st0.quorum) + ')')
1897
+ assert(st0.quorum.next && st0.quorum.next.kind === 'start' && st0.quorum.next.tool === 'vibe_v5_start',
1898
+ '★ …and it says what to do first (' + JSON.stringify(st0.quorum.next) + ')')
1899
+ assert(st0.params && st0.params.m === 0 && st0.params.started === false,
1900
+ 'the params view agrees: m=0, started=false (' + JSON.stringify({ m: st0.params && st0.params.m, started: st0.params && st0.params.started }) + ')')
1901
+ assert(st0.quorum.m !== 1 || st0.quorum.voterCount !== 0,
1902
+ 'the old inconsistent `m=1 with voters=0` reading is gone')
1903
+ // ── the NORMAL path: after start, quorum is defined and the member tools work ──
1904
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
1905
+ await h.settleSpawns()
1906
+ const st1 = await h.callTool('vibe_v5_status', {})
1907
+ assert(st1.quorum && st1.quorum.started === true && st1.quorum.m >= 1 && st1.quorum.voterCount >= 1 && st1.quorum.voters.length === st1.quorum.voterCount,
1908
+ '★ after start the quorum is real: started=true, m>=1, voters listed (' + JSON.stringify(st1.quorum) + ')')
1909
+ const member = h.childAgent(h.childOf('r-1'))
1910
+ const okProg = await h.callTool('vibe_v5_record_progress', { content: '启动后写进度' }, member)
1911
+ assert(okProg.ok === true && /Members\/r-1\/Progress/.test(String(okProg.file || '')),
1912
+ '★ after start the SAME tool succeeds for a member (normal path reachable) (' + JSON.stringify(okProg).slice(0, 120) + ')')
1913
+ }
1914
+
1915
+ // ---------- 44. workspace containment + single-sourced quorum (3rd self-test defects A/B) --------
1916
+ // Defect A: member-facing text said the library root was `Members/<id>/` while a member's OWN
1917
+ // file tools resolve relative paths against the SESSION CWD ⇒ a stray `<cwd>/Members/acad/Propos/
1918
+ // p-*.md` appeared outside `VibeMath/Projects/…`. Every member-facing path is now CWD-relative,
1919
+ // and this section proves that a member card write cannot land outside the project tree.
1920
+ // Defect B: the office digest said m=2 while status/acad said m=3 in the same window (two roster
1921
+ // snapshots). Every view now reads ONE snapshot and carries `rosterVersion`, so all views at one
1922
+ // point in time must agree — including after a roster change.
1923
+ console.log('\n[44] paths stay inside the project root; every quorum view agrees')
1924
+ {
1925
+ const h = makeHost({ pluginModule })
1926
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
1927
+ // (A1) the member brief must name the CWD-relative library root (the member's real baseline).
1928
+ const brief = h.spawns.map((sp) => JSON.stringify(sp.request || {})).join('\n')
1929
+ assert(brief.indexOf('VibeMath/Projects/default/Institutes/institute/Members/') !== -1,
1930
+ '★ the member brief carries the CWD-relative library root (' + (brief.match(/Members\/[^"\\]{0,40}/) || [''])[0] + ')')
1931
+ // A bare `Members/…` NOT preceded by a path separator is an institute-relative path: a member
1932
+ // copying it into its own file tool would write it against the session cwd (defect A).
1933
+ const bareLib = (brief.match(/(^|[^A-Za-z0-9/_.-])Members\//g) || [])
1934
+ assert(bareLib.length === 0,
1935
+ 'no member-facing path is left institute-relative (bare ' + JSON.stringify(bareLib) + ')')
1936
+ await h.settleSpawns()
1937
+ // (A2) a member card write must not add anything at the workspace TOP level.
1938
+ const topBefore = readdirSync(h.WS).sort().join(',')
1939
+ const card = await h.callTool('vibe_v5_record_proposition', { id: 'p-stray-check', statement: '包层测试', value: 1, motive: 'm', p: 0.5 }, h.childAgent(h.childOf('r-1')))
1940
+ const topAfter = readdirSync(h.WS).sort().join(',')
1941
+ assert(card.ok === true && /Members\/r-1\/Propos\/p-stray-check\.md/.test(String(card.file || '')),
1942
+ 'the member card is written through the v5 tool (' + JSON.stringify(card).slice(0, 120) + ')')
1943
+ assert(existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Members', 'r-1', 'Propos', 'p-stray-check.md')),
1944
+ '…and it exists under VibeMath/Projects/<project>/…')
1945
+ assert(topAfter === topBefore && !existsSync(join(h.WS, 'Members')),
1946
+ '★ NO path outside VibeMath/Projects/… gained files (top level unchanged: ' + topBefore + ' → ' + topAfter + ')')
1947
+ // (B) every quorum view agrees at one point in time, and they move TOGETHER after a hire.
1948
+ const views = async () => {
1949
+ const st = await h.callTool('vibe_v5_status', {})
1950
+ const rp = await h.callTool('vibe_v5_report', {})
1951
+ await h.callTool('vibe_v5_say', { to: 'r-1', text: '请继续推进。' }, h.childAgent(h.childOf('acad')))
1952
+ const w = await h.peekWakeOf('r-1', 3000)
1953
+ const pm = w ? /法定票数 m=(\d+)/.exec(w.text) : null
1954
+ if (w) h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
1955
+ await sleep(20)
1956
+ return { st, rp, promptM: pm ? Number(pm[1]) : null }
1957
+ }
1958
+ const v1 = await views()
1959
+ assert(v1.st.quorum && v1.rp.quorum && v1.st.quorum.m === v1.rp.quorum.m,
1960
+ '★ status and report agree on m at one point in time (' + JSON.stringify({ status: v1.st.quorum && v1.st.quorum.m, report: v1.rp.quorum && v1.rp.quorum.m }) + ')')
1961
+ assert(v1.st.quorum.rosterVersion === v1.rp.quorum.rosterVersion,
1962
+ 'both views carry the SAME rosterVersion (' + JSON.stringify({ s: v1.st.quorum.rosterVersion, r: v1.rp.quorum.rosterVersion }) + ')')
1963
+ assert(v1.promptM === v1.st.quorum.m,
1964
+ 'the member-facing state block shows the same m (' + JSON.stringify({ prompt: v1.promptM, status: v1.st.quorum.m }) + ')')
1965
+ // A roster change must reach EVERY view (the stale-snapshot bug). Dismissing a permanent
1966
+ // researcher shrinks the voter set synchronously, so m MUST move in every view at once.
1967
+ const rm = await h.callTool('vibe_v5_remove_researcher', { id: 'r-1' })
1968
+ assert(rm.ok === true, 'precondition: the roster change succeeded (' + JSON.stringify(rm).slice(0, 140) + ')')
1969
+ await sleep(30)
1970
+ const v2 = await views()
1971
+ assert(v2.st.quorum.m === v2.rp.quorum.m,
1972
+ '★ after the roster changes, status and report STILL agree (status=' + v2.st.quorum.m + ', report=' + v2.rp.quorum.m + ')')
1973
+ assert(v2.st.quorum.rosterVersion !== v1.st.quorum.rosterVersion && v2.st.quorum.m !== v1.st.quorum.m,
1974
+ 'the roster change moved both m and rosterVersion (' + JSON.stringify({ before: v1.st.quorum.m, after: v2.st.quorum.m, v: [v1.st.quorum.rosterVersion, v2.st.quorum.rosterVersion] }) + ')')
1975
+ }
1976
+
1861
1977
  console.log('')
1862
1978
  console.log('passed=' + passed + ' failed=' + failed)
1863
1979
  if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
@@ -31,10 +31,14 @@
31
31
  import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, writeFileSync, statSync, mkdirSync } from 'node:fs'
32
32
  import { tmpdir } from 'node:os'
33
33
  import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
34
- import { fileURLToPath } from 'node:url'
34
+ import { fileURLToPath, pathToFileURL } from 'node:url'
35
35
 
36
+ // V2_PLUGIN may be a REPO-RELATIVE path or an ABSOLUTE one (a mutant copy outside the repo, e.g. a
37
+ // temp dir). The previous `new URL('file:///' + path)` form only worked for drive-qualified Windows
38
+ // paths and made out-of-repo variants impossible; this is the same isAbsolute-aware resolution the
39
+ // `math-computation-v*` suites use. Default behaviour (env var unset) is unchanged.
36
40
  const PLUGIN = process.env.V2_PLUGIN
37
- ? new URL('file:///' + String(process.env.V2_PLUGIN).replace(/\\/g, '/'))
41
+ ? pathToFileURL(isAbsolute(process.env.V2_PLUGIN) ? process.env.V2_PLUGIN : join(dirname(fileURLToPath(import.meta.url)), '..', process.env.V2_PLUGIN))
38
42
  : new URL('../vibe-math-v2/vibe-math-v2.js', import.meta.url)
39
43
  // 纯函数测试面(__testHelpers):参数归一化/搜索路径计划/哈希/指纹都从这里守卫。
40
44
  const MOD = await import(PLUGIN.href)
@@ -1568,6 +1572,108 @@ section('15d Lean 增量 + 异步(spec §1–§4 + 修订 §1–§5):参
1568
1572
  await wOffEager.call('vibe_math_pause', {})
1569
1573
  }
1570
1574
 
1575
+ // ---------- 15e. round-2 lens-1 RUNTIME: the initiative axis is injected at EVERY site ----------
1576
+ // Three injection sites with three different shapes (explorer / rederive / solver). The static [sites]
1577
+ // check can only see that the producer call is not textually gated; THIS section proves the actual
1578
+ // prompt text of each site: (1) formalVerify:'off' + leanInitiative:'eager' must carry the daily line
1579
+ // at that site (and keep the verification block closed), (2) leanInitiative:'off' must drop it there
1580
+ // while the verification phase still follows formalVerify. Assertion names carry the site name, so a
1581
+ // single-site regression names itself instead of hiding behind a sibling site's coverage.
1582
+ const SITE_DAILY = /三条筛选判据/
1583
+ const SITE_VERIFY = /【Lean 形式化验证(/
1584
+ const FENCE = '`'.repeat(3)
1585
+ const jsonReply = (obj) => FENCE + 'json\n' + JSON.stringify(obj) + '\n' + FENCE
1586
+ /** Drive one problem through explorer -> solver(dead-end) -> re-derive and collect each site's prompt. */
1587
+ async function driveSites(h, opts, qid) {
1588
+ const q = qid || 'qSite'
1589
+ await h.call('vibe_math_set_params', Object.assign({ tickIntervalMs: 200, verifierCount: 2, leanAsync: false, maxExplorerRetries: 5, maxParallelThreshold: 4 }, opts))
1590
+ await h.call('vibe_math_add_problem', { id: q, description: '站点级运行时用例' })
1591
+ await startScheduler(h)
1592
+ const label = (n) => h.spawns.filter((s) => s.label.startsWith(n + ':' + q))
1593
+ const ex1 = await waitFor(() => label('explorer')[0], 80, 200)
1594
+ const out = { explorer: (ex1 && ex1.prompt) || '' }
1595
+ if (ex1) h.fireEnd({ id: ex1.childId, runId: 'rS1', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: jsonReply({ directions: [{ id: 'd1', title: 'D', method: 'm', core_assumption: 'c', feasibility: 0.8 }] }) }] })
1596
+ const so = await waitFor(() => label('solver')[0], 80, 200)
1597
+ out.solver = (so && so.prompt) || ''
1598
+ if (so) h.fireEnd({ id: so.childId, runId: 'rS2', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: jsonReply({ status: 'dead-end', dead_end_reason: '站点用例:本方向不可行', solution: '', solution_probability: 0.1, lemmas: [], routes: [], lessons: [], survival_probability: 0.2, sub_questions: [], new_inventions: [{ 类型: '工具', 标题: '站点工具', 内容描述: 'x', 是否已入库: false }] }) }] })
1599
+ // every direction is now dead-end ⇒ the NEXT explorer for this problem is built by rederivePrompt
1600
+ await waitFor(() => label('explorer').length >= 2 && label('explorer')[1], 80, 200)
1601
+ const exs = label('explorer')
1602
+ out.rederive = (exs[1] && exs[1].prompt) || ''
1603
+ out.distinct = !!out.explorer && !!out.rederive && out.explorer !== out.rederive
1604
+ await h.call('vibe_math_abort', {})
1605
+ return out
1606
+ }
1607
+ {
1608
+ // (1) eager 在 formalVerify:'off' 下也必须在**每一个**站点注入日线
1609
+ const he = await makeCase('site-eager-off')
1610
+ const A = await driveSites(he, { formalVerify: 'off', leanInitiative: 'eager' })
1611
+ assert(SITE_DAILY.test(A.explorer), '★ [explorer site] formalVerify:off + leanInitiative:eager injects the daily line')
1612
+ assert(SITE_DAILY.test(A.solver), '★ [solver site] formalVerify:off + leanInitiative:eager injects the daily line')
1613
+ assert(SITE_DAILY.test(A.rederive), '★ [rederive site] formalVerify:off + leanInitiative:eager injects the daily line')
1614
+ assert(A.distinct, '★ [rederive site] the re-explore prompt really is the rederive builder (differs from the first explorer wave)')
1615
+ assert(A.explorer.indexOf('【Lean 形式化验证(') === -1 && A.solver.indexOf('【Lean 形式化验证(') === -1 && A.rederive.indexOf('【Lean 形式化验证(') === -1, '★ [all sites] formalVerify:off keeps the verification-phase block closed while eager still injects the daily line')
1616
+
1617
+ // (2) leanInitiative:'off' ⇒ 每个站点都没有日线(同一驱动)
1618
+ const ho = await makeCase('site-init-off')
1619
+ const B = await driveSites(ho, { formalVerify: 'encourage', leanInitiative: 'off' }, 'qSiteOff')
1620
+ assert(!SITE_DAILY.test(B.explorer) && !/【顺手形式化/.test(B.explorer), '★ [explorer site] leanInitiative:off drops the daily line')
1621
+ assert(!SITE_DAILY.test(B.solver) && !/【顺手形式化/.test(B.solver), '★ [solver site] leanInitiative:off drops the daily line')
1622
+ assert(!SITE_DAILY.test(B.rederive) && !/【顺手形式化/.test(B.rederive), '★ [rederive site] leanInitiative:off drops the daily line')
1623
+
1624
+ // (2b) …而验证段仍按 formalVerify 出现(同一参数下的另一个 root,驱动到 verifier)
1625
+ const hv = await makeCase('site-init-off-verify')
1626
+ await hv.call('vibe_math_set_params', { tickIntervalMs: 200, verifierCount: 2, leanAsync: false, formalVerify: 'encourage', leanInitiative: 'off' })
1627
+ await hv.call('vibe_math_add_problem', { id: 'qV', description: '验证段仍在' })
1628
+ await startScheduler(hv)
1629
+ const exv = await waitFor(() => hv.spawns.find((s) => s.label.startsWith('explorer:qV')), 80, 200)
1630
+ assert(!!exv && !SITE_DAILY.test(exv.prompt || ''), '★ [explorer site] 第二个 root:initiative off 时 explorer 仍无日线')
1631
+ if (exv) hv.fireEnd({ id: exv.childId, runId: 'rV1', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: jsonReply({ directions: [{ id: 'd1', title: 'D', method: 'm', core_assumption: 'c', feasibility: 0.8 }] }) }] })
1632
+ const sov = await waitFor(() => hv.spawns.find((s) => s.label.startsWith('solver:qV')), 80, 200)
1633
+ if (sov) hv.fireEnd({ id: sov.childId, runId: 'rV2', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: jsonReply({ status: 'success', solution: 'complete solution', solution_probability: 0.9, lemmas: [], routes: [], lessons: [], survival_probability: 0.9, dead_end_reason: null, sub_questions: [] }) }] })
1634
+ const vv = await waitFor(() => hv.spawns.find((s) => /^verifier:/.test(s.label)), 100, 250)
1635
+ assert(!!vv && SITE_VERIFY.test(vv.prompt || ''), '★ leanInitiative:off 时验证段仍按 formalVerify 注入(verifier 提示词含【Lean 形式化验证()')
1636
+ await hv.call('vibe_math_abort', {})
1637
+ }
1638
+
1639
+ // ---------- 15f. P2-v2 D4: ONE participant set / ONE count across the two views ----------
1640
+ // Pinned definition (must stay in sync with the plugin's voteCount() doc comment):
1641
+ // * participant set of a verify task = `t.children` of the CURRENT round (their `childResults`
1642
+ // entries are that round's votes);
1643
+ // * "how many votes are required" = the task's **creation-time snapshot** `t.expectedCount`
1644
+ // (written when processVerify creates the task) — NEVER live `params.verifierCount` /
1645
+ // `params.maxParallelThreshold`.
1646
+ // Before the convergence the answer lived in three places (`consensus()` over reported votes with
1647
+ // the MIN_REVIEWERS constant, `finalizeVerification()` counting again, and two inline `allReported`
1648
+ // checks over `t.children`), so a mid-round hire/fire could make the debate view and the verdict
1649
+ // view disagree about whether the round is complete.
1650
+ section('15f P2-v2 D4: both views read ONE participant set (snapshot + round)')
1651
+ {
1652
+ const h = await makeCase('d4-v2-midvote')
1653
+ await h.call('vibe_math_set_params', { verifierCount: 2, debateMaxRounds: 2, verdictMode: 'flat' })
1654
+ await h.call('vibe_math_add_proposition', { id: 'p-d4v2', 概述: 'D4 v2 参与集', 概率: 0.6, 分类: '数论' })
1655
+ await startScheduler(h)
1656
+ const staffed = await waitFor(() => { const x = h.spawns.filter((s) => s.label.startsWith('verifier:r-p-d4v2')); return x.length >= 2 ? x : undefined }, 60, 250)
1657
+ assert(!!staffed, 'two reviewers were staffed for the round (the creation-time snapshot)')
1658
+ const pick = (o) => ((o && o.verifyTasks) || []).filter((t) => String(t.rId).indexOf('p-d4v2') !== -1)[0]
1659
+ const st1 = pick(await h.call('vibe_math_status', {}))
1660
+ const rp1 = pick(await h.call('vibe_math_report', {}))
1661
+ assert(!!st1 && !!rp1 && st1.expected === 2 && st1.dispatched === 2 && st1.round === 1,
1662
+ '★★ [D4-v2] the projection reports the round snapshot: expected=2 dispatched=2 round=1(实测 ' + JSON.stringify({ status: st1, report: rp1 }) + ')')
1663
+ assert(JSON.stringify(st1) === JSON.stringify(rp1),
1664
+ '★★★ [D4-v2] status 与 report 两个视图对同一任务的参与集/票数**逐字段相同**(一个来源;实测 ' + JSON.stringify({ status: st1, report: rp1 }) + ')')
1665
+ // mid-vote "hire more": the office raises verifierCount. The in-flight round keeps its snapshot —
1666
+ // a view that re-read the live roster/params would now claim 4.
1667
+ await h.call('vibe_math_set_params', { verifierCount: 4 })
1668
+ const after = pick(await h.call('vibe_math_status', {}))
1669
+ const afterR = pick(await h.call('vibe_math_report', {}))
1670
+ assert(!!after && after.expected === 2 && after.dispatched === 2,
1671
+ '★★★ [D4-v2] 中途调高 verifierCount **不改变**在飞轮次的参与集/需求票数(快照仍是 2;实测 ' + JSON.stringify(after) + ')')
1672
+ assert(JSON.stringify(after) === JSON.stringify(afterR),
1673
+ '★★ [D4-v2] 调参后两个视图仍然逐字段相同(实测 ' + JSON.stringify({ status: after, report: afterR }) + ')')
1674
+ await h.call('vibe_math_abort', {})
1675
+ }
1676
+
1571
1677
  section('16 the captured prompt corpus is written for human review')
1572
1678
  {
1573
1679
  // Freeze the scheduler in every case FIRST: a still-running tick loop could emit one more
@@ -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
@@ -1495,6 +1499,117 @@ section('9b Lean 增量 + 异步:参数/argv/队列/去重/只读面/主动性
1495
1499
  // 10. interaction corpus (AUDIT-CHECKLIST §2.4) — a HUMAN must be able to re-read
1496
1500
  // every prompt the framework emitted, not just the assertions about them.
1497
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
+
1580
+ // ===============================================================
1581
+ // 5c. round-2 D4: ONE vote-count source — a mid-verification verifierCount change must not re-derive
1582
+ // the in-flight task's quorum from live params (the audit's "same count, different code" bug).
1583
+ // ===============================================================
1584
+ section('5c D4 vote count: quorum comes from the task snapshot, not from live params')
1585
+ {
1586
+ const RD4 = makeRoot()
1587
+ await callTool('vibe_math_new_project', { name: 'd4-votecount' }, RD4)
1588
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { verifierCount: 2 }), RD4)
1589
+ await callTool('vibe_math_add_proposition', { id: 'p-d4', 概述: 'D4 票数单一来源', 概率: 0.6, 分类: '数论' }, RD4)
1590
+ await callTool('vibe_math_start', {}, RD4)
1591
+ const reD4 = verifyRe('p-d4')
1592
+ assert(await drive(RD4, () => unfiredVerifiers(RD4, reD4).length >= 2, 'two verifiers for p-d4'), 'two verifiers were dispatched for p-d4 (the creation-time snapshot)')
1593
+ // Mid-verification: the office asks for MORE verifiers. Intent-wise that is a NEW requirement for
1594
+ // FUTURE objects; this task's participant set was frozen when it was created.
1595
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { verifierCount: 4 }), RD4)
1596
+ await sleep(300)
1597
+ // Count PER EXACT rId: the proposition may legitimately have several candidate rIds
1598
+ // (r-p-d4, r-p-d4-pf0, …); only the in-flight task for one rId must stay pinned.
1599
+ const labels = spawnOf(RD4, 'verifier:').map((x) => x.label)
1600
+ const exact = {}
1601
+ for (const l of labels) { const m = /^verifier:(r-p-d4):(\d+)$/.exec(l); if (m) exact[m[1]] = (exact[m[1]] || 0) + 1 }
1602
+ const inFlight = exact['r-p-d4'] || 0
1603
+ assert(inFlight === 2, '★ [D4] raising verifierCount mid-verification does NOT re-staff the in-flight task (participant set = the snapshot;实测 ' + JSON.stringify({ inFlight: inFlight, labels: labels }) + ')')
1604
+ // Both dispatched votes satisfy the quorum ⇒ the object must conclude. A view that re-derived its
1605
+ // quorum from the live params would demand 4 votes and stall here forever.
1606
+ await runVerifyRound(RD4, 'p-d4', [1, 1])
1607
+ const cardOf = () => readIf(join(projRoot('d4-votecount'), 'Propos', '数论', 'p-d4.md'))
1608
+ const concluded = await drive(RD4, () => { const c = cardOf(); return /-\s*概率:\s*1(\D|$)/.test(c) || /状态:\s*已验证/.test(c) }, 'p-d4 concludes with the two votes it has', 15000)
1609
+ assert(concluded, '★★★ [D4] the two dispatched votes still satisfy the quorum and the object concludes (no view may re-derive its quorum from live params;card=' + JSON.stringify(cardOf().slice(0, 220)) + ')')
1610
+ await callTool('vibe_math_abort', {}, RD4)
1611
+ }
1612
+
1498
1613
  section('10 the captured prompt corpus is written for human review')
1499
1614
  {
1500
1615
  mkdirSync(CORPUS_DIR, { recursive: true })