dsh-vibe-math 2.8.3 → 2.8.5

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 (50) hide show
  1. package/README.en.md +19 -4
  2. package/README.md +19 -4
  3. package/cordis.patch.yml +8 -8
  4. package/docs/COMPAT-AUDIT-ROUND2.md +27 -0
  5. package/docs/final-paper.md +13 -0
  6. package/docs/formal-verification.md +6 -6
  7. package/docs/release-notes/README.en.md +2 -0
  8. package/docs/release-notes/README.md +2 -0
  9. package/docs/release-notes/RELEASE-NOTES-2.8.4.en.md +48 -0
  10. package/docs/release-notes/RELEASE-NOTES-2.8.4.md +48 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.8.5.en.md +75 -0
  12. package/docs/release-notes/RELEASE-NOTES-2.8.5.md +75 -0
  13. package/package.json +7 -3
  14. package/prompt-corpus-persona/persona-corpus.json +9 -9
  15. package/prompt-corpus-persona/persona-corpus.md +17 -17
  16. package/prompt-corpus-v2/formal-verify-v2.json +109 -109
  17. package/prompt-corpus-v2/formal-verify-v2.md +216 -216
  18. package/prompt-corpus-v3/formal-verify-v3.json +77 -77
  19. package/prompt-corpus-v3/formal-verify-v3.md +147 -147
  20. package/prompt-corpus-v4/formal-verify-v4.json +10 -10
  21. package/prompt-corpus-v4/formal-verify-v4.md +40 -40
  22. package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
  23. package/prompt-corpus-v5/prompt-corpus-v5.md +613 -186
  24. package/tests/audit-artifact-docs.mjs +38 -2
  25. package/tests/audit-artifact-docs.mutants.mjs +66 -1
  26. package/tests/audit-engine-faces.mjs +2 -2
  27. package/tests/audit-persona-surface.test.mjs +4 -1
  28. package/tests/audit-prompt-invariants.mjs +292 -2
  29. package/tests/e2e-v5-round2.test.mjs +248 -2
  30. package/tests/formal-verify-v2.test.mjs +6 -5
  31. package/tests/formal-verify-v3.test.mjs +6 -5
  32. package/tests/formal-verify-v4.test.mjs +4 -4
  33. package/tests/prompt-v5-integrity.test.mjs +6 -6
  34. package/tests/run-tests.mjs +18 -2
  35. package/tests/v4-final-paper.mutants.mjs +14 -0
  36. package/tests/v4-final-paper.test.mjs +18 -0
  37. package/tests/v5-institute-fixes.mutants.mjs +115 -0
  38. package/vibe-math-v2/agent.cordis.yml +2 -2
  39. package/vibe-math-v2/math-computation.js +5 -5
  40. package/vibe-math-v2/vibe-math-v2.js +66 -17
  41. package/vibe-math-v3/agent.cordis.yml +2 -2
  42. package/vibe-math-v3/math-computation.js +5 -5
  43. package/vibe-math-v3/vibe-math-v3.js +64 -15
  44. package/vibe-math-v4/agent.cordis.yml +6 -6
  45. package/vibe-math-v4/math-computation.js +5 -5
  46. package/vibe-math-v4/vibe-math-v4.js +71 -20
  47. package/vibe-math-v5/agent.cordis.yml +6 -6
  48. package/vibe-math-v5/math-computation.js +5 -5
  49. package/vibe-math-v5/vibe-math-v5.js +385 -34
  50. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +55 -2
@@ -32,6 +32,7 @@ process.env.DSH_HOME = EMPTY_ENGINE_ROOT
32
32
  process.env.ProgramFiles = EMPTY_ENGINE_ROOT
33
33
  process.env['ProgramFiles(x86)'] = EMPTY_ENGINE_ROOT
34
34
  process.env.LOCALAPPDATA = EMPTY_ENGINE_ROOT
35
+ process.env.V5_TEX_ROOTS_SANDBOX = EMPTY_ENGINE_ROOT // task-29: the documented TeX Live roots are sandboxed too (the product's own list now covers D:/C:/texlive)
35
36
 
36
37
  // V5_PLUGIN lets a sensitivity probe point this suite at a deliberately broken copy.
37
38
  // Without it every probe against this suite silently tested the UNMUTATED plugin and was
@@ -1495,6 +1496,40 @@ console.log('\n[27] fake LaTeX compiler: repair path and persistent-failure degr
1495
1496
  '★ [real1004-minutes] a speech arriving after the meeting closed is appended as a late note, never dropped silently')
1496
1497
  assert(after.includes('### '), 'the earlier speeches are still in the minutes (append-only, never clobbered)')
1497
1498
  }
1499
+ // real1004-stall (found on a real host with 2.8.3): the meeting watchdog measured "stall" from the last INPUT and
1500
+ // ignored whether an ASKED member was still busy. A member's turn easily outlives the budget on a real host, so
1501
+ // meetings were abandoned mid-turn and the speeches could only land as "late notes" (that run: 1067 late notes
1502
+ // and 863 refused office finalisations). A meeting must survive while an asked speaker is in flight.
1503
+ {
1504
+ const hs = makeHost({ pluginModule })
1505
+ await hs.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
1506
+ await hs.settleSpawns()
1507
+ await hs.callTool('vibe_v5_set', { activityTimeoutMs: 300 }) // soft stall = 600 ms, hard bound = 1800 ms
1508
+ const mts = await hs.callTool('vibe_v5_meeting', { agenda: 'real1004-stall:被点名的成员仍在忙', kind: 'sync' })
1509
+ const midS = mts && mts.meeting
1510
+ assert(!!midS, 'precondition: the meeting is convened (' + JSON.stringify(mts).slice(0, 110) + ')')
1511
+ // acad is asked and NOT answered, so it stays BUSY. Nobody submits an input, so `lastInputAt` keeps ageing.
1512
+ // NOTE (why this is a SOURCE-level invariant): the watchdog branch runs from a heartbeat whose armed delay is
1513
+ // not controllable from this harness (the plugin arms `ctx.timeout` with delays up to tens of seconds), so a
1514
+ // runtime assertion here cannot be made to FAIL on the unfixed code — it would be a vacuous guard. The
1515
+ // behaviour is instead pinned by (a) this source invariant and (b) the real-host re-test, which is the
1516
+ // acceptance criterion of the board task (measured there: 1067 late notes before the fix).
1517
+ const wA = await hs.peekWakeOf('acad', 3000)
1518
+ assert(!!wA, 'precondition: a member was asked to speak (so it is busy) while no input has arrived')
1519
+ await sleep(900) // past the SOFT bound, inside the hard one
1520
+ const mpathS = join(hs.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Shared', 'Meetings', midS + '.md')
1521
+ assert(!readFileSync(mpathS, 'utf8').includes('因长时间无新发言而被放弃'),
1522
+ 'no input has arrived yet, so nothing may close this meeting: ' + readFileSync(mpathS, 'utf8').slice(0, 160))
1523
+ hs.fireEnd(wA.childId, { input: 'real1004-stall:我被点名后仍在忙,现在交回我的发言。', contextPct: 20 })
1524
+ await sleep(300)
1525
+ const docS = readFileSync(mpathS, 'utf8')
1526
+ assert(docS.includes('### acad') && !docS.includes('(会后补记'),
1527
+ 'a speech from an ASKED member lands in the NORMAL section (never as a late note)')
1528
+ const v5src = readFileSync(PLUGIN, 'utf8') // PLUGIN points at the mutated COPY when a family runs this suite
1529
+ assert(v5src.includes('const inFlight = meeting.order.some((id) => busy.has(id))') &&
1530
+ v5src.includes('if (stale >= recoverStallMs() && (!inFlight || stale >= recoverStallMs() * 3)) {'),
1531
+ '★ [real1004-stall] the meeting watchdog exempts ASKED-but-busy speakers up to a hard bound (a real host abandoned meetings mid-turn: 1067 late notes, 863 refused finalisations)')
1532
+ }
1498
1533
 
1499
1534
  // ---------- 28. office-only surface + id normalisation ----------
1500
1535
  console.log('\n[28] the final-paper surface is office-only and its directory id cannot escape Paper/')
@@ -1742,9 +1777,9 @@ console.log('\n[35] async Lean: enqueue returns at once, settle(ok) is the only
1742
1777
  'the proof is archived under Verified/Lean/<id>.lean')
1743
1778
  const argv = fake.calls[0].argv
1744
1779
  const root = join(h.WS, 'VibeMath').replace(/\\/g, '/')
1745
- const si = argv.indexOf('--search-path')
1780
+ const si = argv.indexOf('-R')
1746
1781
  assert(si > 0 && argv[si + 1] === root && si + 2 === argv.length - 1 && /\.lean$/.test(argv[argv.length - 1]),
1747
- '★ argv carries `--search-path <ABSOLUTE VibeMath root>` right before the file (' + JSON.stringify(argv) + ')')
1782
+ '★ argv carries `-R <ABSOLUTE VibeMath root>` (Lean 4 root; the Lean-3 --search-path made lean fail at parsing) right before the file (' + JSON.stringify(argv) + ')')
1748
1783
  assert(fake.calls.length === 1, 'exactly one compile ran for the settle (got ' + fake.calls.length + ')')
1749
1784
  }
1750
1785
 
@@ -2503,6 +2538,217 @@ console.log('\n[53] v5 L1 leanPathContractOk: the queued note reaches report() d
2503
2538
  assert(rep.indexOf('Lean path contract broken') === -1,
2504
2539
  '★★ [v5 L1] a HEALTHY composition surfaces no Lean path contract note in report() diagnostics')
2505
2540
  }
2541
+ // task-24 (real-host R2, D5'): a statement longer than a display cap used to be cut SILENTLY — at the resolve
2542
+ // step (1200 chars) and again in the member prompt (800 chars) — and the cut text is what voters read and what
2543
+ // the verified record carries. SOURCE-LEVEL invariant: both sites must keep a generous cap AND mark any cut
2544
+ // (a runtime scenario would need a >20k-char card, which is impractical here; the cut-marking itself is pinned
2545
+ // by the single-site mutation in tests/v5-institute-fixes.mutants.mjs).
2546
+ {
2547
+ const src = readFileSync(PLUGIN, 'utf8')
2548
+ assert(src.includes('const cap = 20000') && src.includes('(注意:陈述超过 ' + "' + cap + '" + ' 字符已截断,完整文本见 '),
2549
+ '★ [real1004-visibility] the resolved statement keeps a generous cap (20000) and MARKS any cut in-band')
2550
+ assert(src.includes('(已截断;完整陈述见源卡片 ') && !src.includes("' 陈述:' + String(vs.statement).slice(0, 800)"),
2551
+ '★ [real1004-visibility] the member-prompt statement line marks its 800-char cut instead of hiding the tail')
2552
+ }
2553
+ // task-26 (real-host R2): `vibe_v5_set` used to coerce out-of-range values and drop unknown keys with NOTHING in
2554
+ // the response, and two different capacity limits shared one error code. The response must report both, and the
2555
+ // two limits must be distinguishable by a machine-readable field.
2556
+ {
2557
+ const hz = makeHost({ pluginModule })
2558
+ await hz.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
2559
+ await hz.settleSpawns()
2560
+ const set = await hz.callTool('vibe_v5_set', { quorumCap: -1, unknownKeyXyz: 5 })
2561
+ assert(set.ok === true && Array.isArray(set.dropped) && set.dropped.indexOf('unknownKeyXyz') !== -1,
2562
+ '★ [real1004-visibility] vibe_v5_set REPORTS the unknown key it dropped (dropped=' + JSON.stringify(set.dropped) + ')')
2563
+ assert(set.adjusted && set.adjusted.quorumCap && Number(set.adjusted.quorumCap.from) === -1 && Number(set.adjusted.quorumCap.to) !== -1,
2564
+ '★ [real1004-visibility] vibe_v5_set reports `adjusted` so a coerced value is visible (adjusted=' + JSON.stringify(set.adjusted) + ')')
2565
+ // The per-member vs institute-wide distinction is pinned as a SOURCE invariant instead of by driving two
2566
+ // refusals: `vibe_v5_hire` demands a full brief (`purpose` + `initial_task` + …) before it reaches the cap
2567
+ // check, and assembling that brief here would only test the brief. The mutation in
2568
+ // tests/v5-institute-fixes.mutants.mjs collapses the two scopes and reddens this assertion by name.
2569
+ const src26 = readFileSync(PLUGIN, 'utf8')
2570
+ assert(src26.includes("scope: 'per-member'") && src26.includes("scope: 'institute'"),
2571
+ '★ [real1004-visibility] the two capacity refusals carry DISTINCT machine-readable scopes (both literals present)')
2572
+ }
2573
+ // task-25 (real-host R2/R3): `status().fieldScopes` must cover the fields the payload actually carries; an
2574
+ // incomplete electorate must NAME the missing voters; and v4 must name the residents that did not vote true.
2575
+ {
2576
+ const hf = makeHost({ pluginModule })
2577
+ await hf.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
2578
+ await hf.settleSpawns()
2579
+ const stf = await hf.callTool('vibe_v5_status', {})
2580
+ const fs = stf.fieldScopes || {}
2581
+ assert(stf.diagnostics !== undefined && stf.backend !== undefined,
2582
+ 'precondition: the payload really carries diagnostics/backend (' + JSON.stringify({ d: typeof stf.diagnostics, b: typeof stf.backend }) + ')')
2583
+ assert(Array.isArray(fs.session) && fs.session.indexOf('diagnostics') !== -1 && fs.session.indexOf('backend') !== -1,
2584
+ '★ [real1004-visibility] fieldScopes.session covers the payload fields it used to omit (diagnostics/backend)')
2585
+ assert(Array.isArray(fs.durable) && fs.durable.indexOf('verdicts') !== -1 && fs.durable.indexOf('solve') !== -1,
2586
+ '★ [real1004-visibility] fieldScopes.durable declares verdicts/solve (a reader can tell durable facts apart)')
2587
+ const src25 = readFileSync(PLUGIN, 'utf8')
2588
+ assert(src25.includes('pendingVoters: need.filter((id) => !votes[id])'),
2589
+ '★ [real1004-visibility] an incomplete electorate NAMES the missing voters (pendingVoters)')
2590
+ const v4src = readFileSync(new URL('../vibe-math-v4/vibe-math-v4.js', import.meta.url), 'utf8')
2591
+ assert(v4src.includes(';未投 true:'),
2592
+ '★ [real1004-visibility] v4 names the residents that did not vote solved=true (no silent "no unanimous vote")')
2593
+ }
2594
+ // task-27 (real-host A round + local reproduction): Lean 4 accepts -R/--root (or LEAN_PATH); the Lean-3 spelling
2595
+ // `--search-path` makes `lean` fail at ARGUMENT PARSING (rc=1) before it ever reads the file — every compile failed.
2596
+ {
2597
+ const src27 = readFileSync(PLUGIN, 'utf8')
2598
+ assert(src27.includes("const LEAN_SEARCH_PATH_FLAG = '-R'"),
2599
+ '★ [real1004-lean-flag] the injected Lean search root uses -R (Lean 4), not the Lean-3 --search-path')
2600
+ assert(!src27.includes('--search-path <VibeMath root>') && !/registerTool\(\s*'[^']+'\s*,\s*'[^']*--search-path/.test(src27),
2601
+ '★ [real1004-lean-flag] no agent-facing tool description still teaches --search-path')
2602
+ }
2603
+ // Iron-rules P4: the LaTeX-missing block was pushed into EVERY wake (founding/verify/meeting included) with no
2604
+ // statement of when it applies, so an agent that has nothing to do with the paper met unexplained noise.
2605
+ {
2606
+ const srcP4 = readFileSync(PLUGIN, 'utf8')
2607
+ assert(srcP4.includes('仅在你参与论文写作或编译时适用'),
2608
+ '★ [real1004-prompt-scope] the LaTeX block says WHEN it applies instead of appearing as unexplained noise')
2609
+ assert(/function paperPushLine\(L\)[\s\S]{0,600}仅在你参与论文写作或编译时适用/.test(srcP4),
2610
+ '★ [real1004-prompt-scope] the scope note is what the shared tail actually pushes (not a dangling sentence)')
2611
+ }
2612
+ // ---------- 54. task-30: the methodology/collaboration feedback library (Shared/Feedback/) ----------
2613
+ console.log('\n[54] vibe_v5_feedback: 用途+三路由提示;add→list→状态流转;off 不注入+具名拒绝+不写盘;计数与库一致')
2614
+ {
2615
+ const INSDIR = join('VibeMath', 'Projects', 'default', 'Institutes', 'institute')
2616
+ const h = makeHost({ pluginModule })
2617
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
2618
+ await h.settleSpawns()
2619
+ const r1 = h.childAgent(h.childOf('r-1'))
2620
+ const acad = h.childAgent(h.childOf('acad'))
2621
+ const fbPath = (...p) => join(h.WS, INSDIR, 'Shared', 'Feedback', ...p)
2622
+ const wake = async (member) => {
2623
+ await h.callTool('vibe_v5_say', { to: member, text: '请继续推进。' }, acad)
2624
+ const w = await h.peekWakeOf(member, 3000)
2625
+ if (w) h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
2626
+ await sleep(25)
2627
+ return w ? w.text : ''
2628
+ }
2629
+ const countsLine = (t) => (String(t).split('\n').find((l) => l.indexOf('条目 ') !== -1 && l.indexOf('未闭环 ') !== -1) || '')
2630
+
2631
+ // (1) 每一轮都可见:为什么记/记什么/三条路由/记了之后会怎样
2632
+ const p1 = await wake('r-1')
2633
+ assert(p1.indexOf('【工作经验/流程反馈') !== -1, '★ [task-30] the per-round prompt carries the feedback hint')
2634
+ assert(p1.indexOf('为什么记') !== -1 && p1.indexOf('记什么') !== -1 && p1.indexOf('记了之后') !== -1,
2635
+ '★ [task-30] the hint explains WHY / WHAT / what happens after')
2636
+ assert(p1.indexOf('self=') !== -1 && p1.indexOf('team=') !== -1 && p1.indexOf('interpersonal=') !== -1,
2637
+ '★ [task-30] the hint distinguishes the THREE routes')
2638
+ assert(p1.indexOf('自己调整,无需谁采纳') !== -1 && p1.indexOf('同样无需审批') !== -1,
2639
+ '★★ [task-30] self/team routes are self-regulation — the hint never asks for an approval')
2640
+ assert(p1.indexOf('缜密评估') !== -1 && p1.indexOf('事后回填验证') !== -1,
2641
+ '★ [task-30] only the interpersonal route demands a careful assessment + written-back verification')
2642
+ assert(p1.indexOf('Progress/') !== -1 && p1.indexOf('Verified/') !== -1,
2643
+ '★ [task-30] the boundary against Progress//Verified/ is stated in the prompt')
2644
+ assert(p1.indexOf('向用户问一次') === -1 && p1.indexOf('ask the user once') === -1,
2645
+ 'the hint never demands an action a member cannot perform (I16)')
2646
+
2647
+ // (2) add → list 可见;self/team 的状态流转无需任何审批
2648
+ const add = await h.callTool('vibe_v5_feedback', { op: 'add', category: 'process', route: 'self', context: '每轮都在等别人回复', phenomenon: '轮次空转', impact: '进展慢', action: '我自己改成先写进度再发问' }, r1)
2649
+ assert(add.ok === true && add.entry && add.entry.id === 'fb-1' && add.entry.status === 'open',
2650
+ '★ [task-30] add records an entry (open, id=fb-1): ' + JSON.stringify(add.entry && add.entry.id))
2651
+ assert(add.mirror === true && existsSync(fbPath('process.md')),
2652
+ '★ [task-30] the human-readable mirror is really written (Shared/Feedback/process.md)')
2653
+ const list1 = await h.callTool('vibe_v5_feedback', { op: 'list' }, r1)
2654
+ assert(list1.count === 1 && list1.entries[0].id === 'fb-1' && list1.openOnly === true,
2655
+ '★ [task-30] list defaults to the NOT-yet-closed entries')
2656
+ const upd = await h.callTool('vibe_v5_feedback', { op: 'update', id: 'fb-1', status: 'adjusted', outcome: '工作流改完,空转少了' }, r1)
2657
+ assert(upd.ok === true && upd.entry.status === 'adjusted' && /空转少了/.test(upd.entry.outcome),
2658
+ '★ [task-30] status flows open→adjusted WITHOUT any approval step (self-regulation)')
2659
+ const addTeam = await h.callTool('vibe_v5_feedback', { op: 'add', category: 'cooperation', route: 'team', phenomenon: '任务分配不均', impact: '有人闲置', action: '分派前先看负载' }, acad)
2660
+ assert(addTeam.ok === true, 'the academician can record a team-route entry (organisation / workflow)')
2661
+ const forbidden = await h.callTool('vibe_v5_feedback', { op: 'update', id: addTeam.entry.id, status: 'closed' }, r1)
2662
+ assert(forbidden.ok === false && forbidden.code === 'V5_FEEDBACK_FORBIDDEN',
2663
+ '★ [task-30] a member cannot update somebody else\'s entry (named refusal)')
2664
+
2665
+ // (3) interpersonal:必须带评估;闭环必须回填 outcome(事后验证)
2666
+ const needA = await h.callTool('vibe_v5_feedback', { op: 'add', category: 'conflict', route: 'interpersonal', phenomenon: 'r-2 不回消息', impact: '我卡住', action: '等他回' }, r1)
2667
+ assert(needA.ok === false && needA.code === 'V5_FEEDBACK_NEEDS_ASSESSMENT',
2668
+ '★★ [task-30] the interpersonal route refuses to be recorded WITHOUT the assessment')
2669
+ const inter = await h.callTool('vibe_v5_feedback', { op: 'add', category: 'conflict', route: 'interpersonal', phenomenon: 'r-2 不回消息', impact: '我卡住', action: '约会议当面问', assessment: '让他改习惯是否真更优?先约会议成本更低' }, r1)
2670
+ assert(inter.ok === true && inter.entry.route === 'interpersonal', 'the interpersonal entry is accepted WITH the assessment')
2671
+ const noOut = await h.callTool('vibe_v5_feedback', { op: 'update', id: inter.entry.id, status: 'closed' }, r1)
2672
+ assert(noOut.ok === false && noOut.code === 'V5_FEEDBACK_NEEDS_OUTCOME',
2673
+ '★★ [task-30] closing an interpersonal entry WITHOUT the written-back verification is refused')
2674
+ const officeFix = await h.callTool('vibe_v5_feedback', { op: 'update', id: inter.entry.id, status: 'closed', outcome: '会上确认了回复习惯,之后未再发生' })
2675
+ assert(officeFix.ok === true && officeFix.entry.status === 'closed',
2676
+ '★ [task-30] the OFFICE may resolve ANY entry and write the result back')
2677
+
2678
+ // (4) off:提示词不注入 + 每个 op 都具名拒绝 + 一个字都不写
2679
+ const rpBefore = (await h.callTool('vibe_v5_report', {})).report
2680
+ const idxBefore = readFileSync(fbPath('index.json'), 'utf8')
2681
+ await h.callTool('vibe_v5_set', { feedback: 'off' })
2682
+ const p2 = await wake('r-1')
2683
+ assert(p2.indexOf('【工作经验/流程反馈') === -1, '★★ [task-30] feedback=off: the per-round hint is NOT injected')
2684
+ const offAdd = await h.callTool('vibe_v5_feedback', { op: 'add', category: 'process', route: 'self', phenomenon: 'x', impact: 'y', action: 'z' }, r1)
2685
+ assert(offAdd.ok === false && offAdd.code === 'V5_FEEDBACK_DISABLED' && /已关闭/.test(offAdd.message),
2686
+ '★★ [task-30] off refuses the write BY NAME (never a silent drop)')
2687
+ const offList = await h.callTool('vibe_v5_feedback', { op: 'list', all: true }, r1)
2688
+ assert(offList.ok === false && offList.code === 'V5_FEEDBACK_DISABLED',
2689
+ 'off refuses EVERY op, so nothing can look normal while the feature is disabled')
2690
+ const rpAfter = (await h.callTool('vibe_v5_report', {})).report
2691
+ assert(countsLine(rpAfter) === countsLine(rpBefore) && idxBefore === readFileSync(fbPath('index.json'), 'utf8'),
2692
+ '★★ [task-30] off wrote NOTHING (report counts and the on-disk index are unchanged)')
2693
+ await h.callTool('vibe_v5_set', { feedback: 'on' })
2694
+
2695
+ // (5) 计数与库内容一致(工具 summary / report / overview 三个面)
2696
+ const summary = (await h.callTool('vibe_v5_feedback', { op: 'summary' }, r1)).counts
2697
+ const allEntries = (await h.callTool('vibe_v5_feedback', { op: 'list', all: true }, r1)).entries
2698
+ assert(summary.total === allEntries.length && summary.open + summary.closed === summary.total,
2699
+ '★★ [task-30] summary counts match the library contents exactly (' + JSON.stringify(summary) + ')')
2700
+ const byCat = allEntries.reduce((m, e) => { m[e.category] = (m[e.category] || 0) + 1; return m }, {})
2701
+ assert(Object.keys(summary.byCategory).every((k) => summary.byCategory[k] === (byCat[k] || 0)),
2702
+ '★★ [task-30] the by-category counts match the entries')
2703
+ const rp = await h.callTool('vibe_v5_report', {})
2704
+ assert(rp.report.indexOf('## 反馈库') !== -1 && rp.report.indexOf('条目 ' + summary.total + '|未闭环 ' + summary.open) !== -1,
2705
+ '★★ [task-30] report() shows the SAME counts as the library')
2706
+ const ov = await h.callTool('vibe_v5_overview', {}, acad)
2707
+ assert(ov.overview.indexOf('## 反馈库') !== -1 && ov.overview.indexOf('条目 ' + summary.total) !== -1 && ov.overview.indexOf('未闭环 ' + summary.open) !== -1,
2708
+ '★★ [task-30] overview shows the same counts (the academician\'s periodic summary reads this)')
2709
+ }
2710
+ // ---------- 55. task-31: three prompt-review follow-ups (P1 receipt background / P3 empty engine table / P4 frame) ----
2711
+ console.log('\n[55] task-31: P1 首次提回执处给背景+字段最小注解;P3 空引擎表"可用引擎 无";P4 面向代理不再自称"框架"')
2712
+ {
2713
+ // P3/P1 at the SOURCE of the line: the shared module builds it, so call it for real with an empty probe.
2714
+ // The sibling is resolved THROUGH the same seam as the plugin, so a deliberately broken copy is the
2715
+ // module actually under test (otherwise a mutation here could never redden this guard).
2716
+ const mc = await import(new URL('./math-computation.js', PLUGIN))
2717
+ const emptyZh = mc.mathAvailabilityLine({ engines: [] }, 'zh', 'typed+shell')
2718
+ const emptyEn = mc.mathAvailabilityLine({ engines: [] }, 'en', 'typed+shell')
2719
+ // P1: the archive→edit→re-run section explains WHAT a receipt is at its FIRST mention, and the bare
2720
+ // field name carries its own minimal context (only the necessary ones - the section must stay short).
2721
+ assert(emptyZh.indexOf('回执=一次 math_computation 调用的 JSON 结果') !== -1,
2722
+ '★★ [task-31] P1: 归档→编辑→重跑 段首次提到回执处解释了"回执是什么"')
2723
+ assert(emptyZh.indexOf('(脚本内容相对上一份回执变过)') !== -1,
2724
+ '★ [task-31] P1: 裸字段名 scriptChanged 自带最小上下文')
2725
+ assert(emptyEn.indexOf('a RECEIPT is the JSON result of ONE math_computation call') !== -1
2726
+ && emptyEn.indexOf('(the script content differs from the previous receipt)') !== -1,
2727
+ '★ [task-31] P1: en 段同步给出回执背景与字段上下文')
2728
+ // P3: an empty engine table must not read as "可用的东西:无".
2729
+ assert(emptyZh.indexOf('math_computation:可用引擎 无') !== -1 && emptyZh.indexOf('本机可用') === -1,
2730
+ '★★ [task-31] P3: 空引擎表渲染为「可用引擎 无」: ' + JSON.stringify((/math_computation:[^\n]{0,30}/.exec(emptyZh) || ['<none>'])[0]))
2731
+ assert(emptyEn.indexOf('math_computation: available engines none') !== -1,
2732
+ '★ [task-31] P3: en 空表同步为「available engines none」')
2733
+ // …and the SAME text really reaches a member (end-to-end, not only at the module boundary).
2734
+ const h = makeHost({ pluginModule })
2735
+ await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
2736
+ await h.settleSpawns()
2737
+ await h.callTool('vibe_v5_say', { to: 'r-1', text: '请继续推进。' }, h.childAgent(h.childOf('acad')))
2738
+ const w = await h.peekWakeOf('r-1', 3000)
2739
+ const p = w ? w.text : ''
2740
+ if (w) h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
2741
+ assert(p.indexOf('回执=一次 math_computation 调用的 JSON 结果') !== -1,
2742
+ '★★ [task-31] P1: 成员那一轮真正读到的提示词就带这段背景')
2743
+ assert(p.indexOf('math_computation:可用引擎 无') !== -1 && p.indexOf('本机可用') === -1,
2744
+ '★★ [task-31] P3: 成员提示词里是「可用引擎 无」')
2745
+ // P4: agent-facing text must not call the plugin "框架" (ambiguous from an agent's point of view:
2746
+ // plugin? host? DSH?). The one class fixed here is the notice frame.
2747
+ const src31 = readFileSync(PLUGIN, 'utf8')
2748
+ assert(src31.indexOf('【框架提示】') === -1, '★★ [task-31] P4: 面向代理文本里不再出现【框架提示】')
2749
+ assert(src31.indexOf("return '【研究所提示】' + m.text") !== -1,
2750
+ '★ [task-31] P4: 研究所回执帧改用【研究所提示】')
2751
+ }
2506
2752
  console.log('')
2507
2753
  console.log('passed=' + passed + ' failed=' + failed)
2508
2754
  if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
@@ -1362,9 +1362,10 @@ section('15d Lean 增量 + 异步(spec §1–§4 + 修订 §1–§5):参
1362
1362
  assert(JSON.stringify(planDefault.paths) === JSON.stringify(['/root/vm']) && planDefault.inject.length === 2, '★ default plan injects exactly the automatic VibeMath root')
1363
1363
  const planExtra = H.leanSearchPathPlan(['env', 'lean'], ['/a', '/b'], '/root/vm')
1364
1364
  assert(JSON.stringify(planExtra.paths) === JSON.stringify(['/a', '/b', '/root/vm']), '★★ user leanSearchPaths come FIRST, then the automatic root')
1365
- assert(JSON.stringify(planExtra.inject) === JSON.stringify(['--search-path', '/a', '--search-path', '/b', '--search-path', '/root/vm']), 'and each path is injected with its own --search-path flag')
1366
- assert(H.leanSearchPathPlan(['--search-path', '/x'], ['/a'], '/root/vm').inject.length === 0, '★ an explicit --search-path in leanArgs suppresses all injection (user wins)')
1365
+ assert(JSON.stringify(planExtra.inject) === JSON.stringify(['-R', '/a', '-R', '/b', '-R', '/root/vm']), 'and each path is injected with its own -R (Lean 4) flag')
1366
+ assert(H.leanSearchPathPlan(['--search-path', '/x'], ['/a'], '/root/vm').inject.length === 0, '★ an explicit legacy Lean-3 --search-path in leanArgs suppresses all injection (user wins)')
1367
1367
  assert(H.leanSearchPathPlan(['-R', '/x'], ['/a'], '/root/vm').inject.length === 0 && H.leanSearchPathPlan(['--root', '/x'], ['/a'], '/root/vm').inject.length === 0, '★ -R / --root are honoured too')
1368
+ assert(H.leanSearchPathPlan(['-R=/x'], ['/a'], '/root/vm').inject.length === 0 && H.leanSearchPathPlan(['--search-path=/x'], ['/a'], '/root/vm').inject.length === 0 && H.leanSearchPathPlan(['--root=/x'], ['/a'], '/root/vm').inject.length === 0, '★ the = forms of -R / legacy --search-path / --root are honoured too')
1368
1369
 
1369
1370
  // ── argv:注入位置在用户参数之后、文件名之前 ──────────────────────────────────────
1370
1371
  const b = await makeCase('lean-argv')
@@ -1377,15 +1378,15 @@ section('15d Lean 增量 + 异步(spec §1–§4 + 修订 §1–§5):参
1377
1378
  const argv1 = leanRuns[leanRuns.length - 1].argv
1378
1379
  assert(leanRuns.length === runsBefore + 1, 'the sync run really reached the compiler')
1379
1380
  assert(JSON.stringify(argv1.slice(0, 2)) === JSON.stringify(['lean', 'env']), 'the user leanArgs are preserved in order')
1380
- assert(argv1[argv1.length - 2] === '--search-path' && SAME(argv1[argv1.length - 1], vibeRoot(b)), '★★ --search-path + the VibeMath root are injected AFTER the user args and immediately BEFORE the file (argv=' + JSON.stringify(argv1) + ')')
1381
+ assert(argv1[argv1.length - 2] === '-R' && SAME(argv1[argv1.length - 1], vibeRoot(b)), '★★ -R + the VibeMath root are injected AFTER the user args and immediately BEFORE the file (argv=' + JSON.stringify(argv1) + ')')
1381
1382
  await b.call('vibe_math_set_params', { leanArgs: ['env', 'lean', '--search-path', '/explicit'] })
1382
1383
  await b.call('vibe_math_lean_run', { file: 'Formal/srch.lean' })
1383
1384
  const argv2 = leanRuns[leanRuns.length - 1].argv
1384
- assert(argv2.indexOf('--search-path') === 3 && argv2[4] === '/explicit' && argv2.filter((x) => x === '--search-path').length === 1, '★ an explicit --search-path is respected verbatim (no second injection) — got ' + JSON.stringify(argv2))
1385
+ assert(argv2.indexOf('--search-path') === 3 && argv2[4] === '/explicit' && argv2.filter((x) => x === '--search-path').length === 1 && argv2.indexOf('-R') === -1, '★ an explicit legacy --search-path is respected verbatim: nothing is injected (no -R) — got ' + JSON.stringify(argv2))
1385
1386
  await b.call('vibe_math_set_params', { leanArgs: ['env', 'lean'], leanSearchPaths: ['/extra1', '/extra2'] })
1386
1387
  await b.call('vibe_math_lean_run', { file: 'Formal/srch.lean' })
1387
1388
  const argv3 = leanRuns[leanRuns.length - 1].argv
1388
- assert(JSON.stringify(argv3.slice(-6)) === JSON.stringify(['--search-path', '/extra1', '--search-path', '/extra2', '--search-path', slash(vibeRoot(b))]), '★★ leanSearchPaths are injected first, then the automatic root (got ' + JSON.stringify(argv3.slice(-6)) + ')')
1389
+ assert(JSON.stringify(argv3.slice(-6)) === JSON.stringify(['-R', '/extra1', '-R', '/extra2', '-R', slash(vibeRoot(b))]), '★★ leanSearchPaths are injected first, then the automatic root (got ' + JSON.stringify(argv3.slice(-6)) + ')')
1389
1390
 
1390
1391
  // ── 异步状态机:入队立即返回 → 心跳排空 → 只有 settled(ok) 才是通过 ────────────────
1391
1392
  const c = await makeCase('lean-async')
@@ -564,7 +564,7 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
564
564
  assert(/ok/.test(runGood.stdout), 'the compiler stdout is returned')
565
565
  const last = leanRuns[leanRuns.length - 1]
566
566
  assert(last.cwd.replace(/\\/g, '/') === toolProj.replace(/\\/g, '/'), 'the toolchain runs with the PROJECT root as cwd')
567
- assert(last.argv.join(' ') === 'lean --search-path ' + VIBE.replace(/\\/g, '/'), 'by default the argv is the executable + the injected search path (got ' + last.argv.join(' ') + ')')
567
+ assert(last.argv.join(' ') === 'lean -R ' + VIBE.replace(/\\/g, '/'), 'by default the argv is the executable + the injected -R (Lean 4) search root (got ' + last.argv.join(' ') + ')')
568
568
  assert(last.graceMs === 120000, 'the default leanTimeoutMs is passed down as graceMs (' + last.graceMs + ')')
569
569
  }
570
570
  {
@@ -645,7 +645,7 @@ try {
645
645
  await callTool('vibe_math_set_params', { leanCommand: 'lake', leanArgs: ['env', 'lean'] }, RE)
646
646
  const runLake = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
647
647
  assert(runLake.ok === true, 'lake env lean works through leanCommand/leanArgs')
648
- assert(leanRuns[leanRuns.length - 1].argv.join(' ') === 'lake env lean --search-path ' + VIBE.replace(/\\/g, '/'), 'the argv is [exe, ...leanArgs, injected --search-path] (got ' + leanRuns[leanRuns.length - 1].argv.join(' ') + ')')
648
+ assert(leanRuns[leanRuns.length - 1].argv.join(' ') === 'lake env lean -R ' + VIBE.replace(/\\/g, '/'), 'the argv is [exe, ...leanArgs, injected -R (Lean 4)] (got ' + leanRuns[leanRuns.length - 1].argv.join(' ') + ')')
649
649
  await callTool('vibe_math_set_params', { leanCommand: 'lean', leanArgs: [] }, RE)
650
650
  }
651
651
 
@@ -1324,6 +1324,7 @@ section('9b Lean 增量 + 异步:参数/argv/队列/去重/只读面/主动性
1324
1324
  const planExtra = H.leanSearchPathPlan(['env', 'lean'], ['/a', '/b'], '/root/vm')
1325
1325
  assert(JSON.stringify(planExtra.paths) === JSON.stringify(['/a', '/b', '/root/vm']), '★★ user leanSearchPaths come FIRST, then the automatic root')
1326
1326
  assert(H.leanSearchPathPlan(['--search-path', '/x'], ['/a'], '/root/vm').inject.length === 0 && H.leanSearchPathPlan(['-R', '/x'], ['/a'], '/root/vm').inject.length === 0 && H.leanSearchPathPlan(['--root', '/x'], ['/a'], '/root/vm').inject.length === 0, '★ an explicit --search-path/-R/--root in leanArgs suppresses all injection (user wins)')
1327
+ assert(H.leanSearchPathPlan(['-R=/x'], ['/a'], '/root/vm').inject.length === 0 && H.leanSearchPathPlan(['--search-path=/x'], ['/a'], '/root/vm').inject.length === 0 && H.leanSearchPathPlan(['--root=/x'], ['/a'], '/root/vm').inject.length === 0, '★ the = forms of -R / legacy --search-path / --root are honoured too')
1327
1328
  const ctxA = H.leanBuildContext('lean', ['a'], ['/p'])
1328
1329
  const ctxB = H.leanBuildContext('lean', ['b'], ['/p'])
1329
1330
  assert(H.leanJobFingerprint('x\n', ctxA) !== H.leanJobFingerprint('x\n', ctxB), '★ the job fingerprint covers the build context (engine/args/search paths)')
@@ -1339,15 +1340,15 @@ section('9b Lean 增量 + 异步:参数/argv/队列/去重/只读面/主动性
1339
1340
  await callTool('vibe_math_lean_run', { file: 'Formal/srch.lean' }, ra)
1340
1341
  const argv1 = leanRuns[leanRuns.length - 1].argv
1341
1342
  assert(leanRuns.length === runs0 + 1 && JSON.stringify(argv1.slice(0, 3)) === JSON.stringify(['lake', 'env', 'lean']), 'the user leanArgs are preserved in order (lake env lean)')
1342
- assert(argv1[argv1.length - 2] === '--search-path' && SAME(argv1[argv1.length - 1], VIBE), '★★ --search-path + the VibeMath root are injected AFTER the user args and immediately BEFORE the file (' + JSON.stringify(argv1) + ')')
1343
+ assert(argv1[argv1.length - 2] === '-R' && SAME(argv1[argv1.length - 1], VIBE), '★★ -R + the VibeMath root are injected AFTER the user args and immediately BEFORE the file (' + JSON.stringify(argv1) + ')')
1343
1344
  await callTool('vibe_math_set_params', { leanArgs: ['env', 'lean', '--search-path', '/explicit'] }, ra)
1344
1345
  await callTool('vibe_math_lean_run', { file: 'Formal/srch.lean' }, ra)
1345
1346
  const argv2 = leanRuns[leanRuns.length - 1].argv
1346
- assert(argv2.filter((x) => x === '--search-path').length === 1 && argv2[4] === '/explicit', '★ an explicit --search-path is respected verbatim (no second injection) — got ' + JSON.stringify(argv2))
1347
+ assert(argv2.filter((x) => x === '--search-path').length === 1 && argv2[4] === '/explicit' && argv2.indexOf('-R') === -1, '★ an explicit legacy --search-path is respected verbatim: nothing is injected (no -R) — got ' + JSON.stringify(argv2))
1347
1348
  await callTool('vibe_math_set_params', { leanArgs: ['env', 'lean'], leanSearchPaths: ['/extra1', '/extra2'] }, ra)
1348
1349
  await callTool('vibe_math_lean_run', { file: 'Formal/srch.lean' }, ra)
1349
1350
  const argv3 = leanRuns[leanRuns.length - 1].argv
1350
- assert(JSON.stringify(argv3.slice(-6)) === JSON.stringify(['--search-path', '/extra1', '--search-path', '/extra2', '--search-path', slash(VIBE)]), '★★ leanSearchPaths are injected first, then the automatic root (got ' + JSON.stringify(argv3.slice(-6)) + ')')
1351
+ assert(JSON.stringify(argv3.slice(-6)) === JSON.stringify(['-R', '/extra1', '-R', '/extra2', '-R', slash(VIBE)]), '★★ leanSearchPaths are injected first, then the automatic root (got ' + JSON.stringify(argv3.slice(-6)) + ')')
1351
1352
 
1352
1353
  // ── 异步状态机:入队立即返回 → 心跳排空 → 只有 settled(ok) 才是通过 ───────────────
1353
1354
  const rq = makeRoot()
@@ -1507,14 +1507,14 @@ section('22 leanSearchPaths/leanJobsMaxParallel/leanInitiative + build-context j
1507
1507
  writeIn(N, 'Projects/default/Formal/a-sp.lean', 'theorem a_sp : True := by trivial\n')
1508
1508
  await N.callTool('vibe_v4_lean_run', { file: 'Formal/a-sp.lean' }, N.resAgent(N.childOf('r-1')))
1509
1509
  let last = leanRuns[leanRuns.length - 1]
1510
- const spIdx = last.argv.indexOf('--search-path')
1511
- assert(last.argv.filter(a => a === '--search-path').length === 3, '★ one --search-path per DISTINCT root (user paths first, then the automatic VibeMath root)')
1510
+ const spIdx = last.argv.indexOf('-R')
1511
+ assert(last.argv.filter(a => a === '-R').length === 3, '★ one -R (Lean 4 root) per DISTINCT root (user paths first, then the automatic VibeMath root)')
1512
1512
  assert(last.argv[spIdx + 1] === '/extra/one' && last.argv[spIdx + 3] === '/extra/two' && last.argv[spIdx + 5] === N.vibeRoot.replace(/\\/g, '/'), '★ order + de-duplication: user paths in order, automatic root last')
1513
- assert(last.argv.indexOf('--search-path') < last.argv.length - 1 && /\.lean$/.test(String(last.argv[last.argv.length - 1])), 'the roots stay BEFORE the file name')
1513
+ assert(last.argv.indexOf('-R') < last.argv.length - 1 && /\.lean$/.test(String(last.argv[last.argv.length - 1])), 'the roots stay BEFORE the file name')
1514
1514
  await N.callTool('vibe_v4_set', { leanArgs: ['--search-path', '/user/own'] })
1515
1515
  await N.callTool('vibe_v4_lean_run', { file: 'Formal/a-sp.lean' }, N.resAgent(N.childOf('r-1')))
1516
1516
  last = leanRuns[leanRuns.length - 1]
1517
- assert(last.argv.filter(a => a === '--search-path').length === 1 && last.argv[last.argv.indexOf('--search-path') + 1] === '/user/own', '★ an explicit --search-path in leanArgs wins outright: nothing is injected')
1517
+ assert(last.argv.filter(a => a === '--search-path').length === 1 && last.argv[last.argv.indexOf('--search-path') + 1] === '/user/own' && last.argv.indexOf('-R') === -1, '★ an explicit legacy --search-path in leanArgs wins outright: nothing is injected (no -R)')
1518
1518
  await N.callTool('vibe_v4_set', { leanArgs: ['-R', '/user/root'] })
1519
1519
  await N.callTool('vibe_v4_lean_run', { file: 'Formal/a-sp.lean' }, N.resAgent(N.childOf('r-1')))
1520
1520
  assert(leanRuns[leanRuns.length - 1].argv.indexOf('--search-path') === -1, '★ -R/--root count as a user-stated root too')
@@ -639,19 +639,19 @@ const wakeR1With = async (kind, reply) => {
639
639
  await callTool('vibe_v5_say', { to: 'r-1', text: '请就当前状态给个结论。' }, childAgent(rd2))
640
640
  await settle(); await drainWakes(20, RD)
641
641
  const handled = delivered.slice()
642
- for (const w of handled) if (/【框架提示】/.test(w.prompt)) recordAndCheck(kind, w.owner, w.prompt).length
642
+ for (const w of handled) if (/【研究所提示】/.test(w.prompt)) recordAndCheck(kind, w.owner, w.prompt).length
643
643
  return handled.map(w => w.prompt).join('\n')
644
644
  }
645
645
  const badVerdictText = await wakeR1With('notice', { verdict: { target: 'p-ghost', verdict: 'not-a-number', reason: 'x' }, contextPct: 20 })
646
- assert(/【框架提示】/.test(badVerdictText), 'a malformed verdict produces a real 【框架提示】 delivery')
646
+ assert(/【研究所提示】/.test(badVerdictText), 'a malformed verdict produces a real 【研究所提示】 delivery')
647
647
  assert(/verdict 必须是 0-1 的数值/.test(badVerdictText), 'the notice says what was wrong')
648
- assert(/【框架提示】[^\n]*verdict/.test(badVerdictText), 'the notice is framed by the framework, not by the member itself')
648
+ assert(/【研究所提示】[^\n]*verdict/.test(badVerdictText), 'the notice is framed by the institute, not by the member itself')
649
649
  const badClaimText = await wakeR1With('notice-claim', { task_claim: 't-999', contextPct: 20 })
650
- assert(/【框架提示】/.test(badClaimText) && /没有任务 t-999/.test(badClaimText), 'a refused claim is reported back to the claimer')
650
+ assert(/【研究所提示】/.test(badClaimText) && /没有任务 t-999/.test(badClaimText), 'a refused claim is reported back to the claimer')
651
651
  // A CAS refusal reported through the JSON reply must also come back.
652
652
  const t = await callTool('vibe_v5_task_create', { subject: '一个没人认领的任务' }, childAgent(rd1))
653
653
  const staleText = await wakeR1With('notice-task', { task_update: { task_id: t.task.id, expected_revision: 99, action: 'claim' }, contextPct: 20 })
654
- assert(/【框架提示】/.test(staleText) && /V5_TASK_STALE_REVISION/.test(staleText), 'a stale CAS reported through the reply channel is echoed back')
654
+ assert(/【研究所提示】/.test(staleText) && /V5_TASK_STALE_REVISION/.test(staleText), 'a stale CAS reported through the reply channel is echoed back')
655
655
  const selfFramed = corpus.filter(c => new RegExp('【研究所·私信 from ' + c.owner + '】').test(c.prompt))
656
656
  assert(selfFramed.length === 0, 'no member ever receives a message framed as coming from itself')
657
657
  await endCase(RD)
@@ -1278,7 +1278,7 @@ const corpusMd = readFileSync(mdPath, 'utf8')
1278
1278
  assert(corpusMd.length > 30000, 'the corpus is substantial (' + corpusMd.length + ' chars) — the real prompt text is preserved')
1279
1279
  assert(corpusMd.indexOf('[状态] 你是 acad(院士)') !== -1, 'a human can verify the academician brief verbatim')
1280
1280
  assert(corpusMd.indexOf('[状态] 你是 r-2') !== -1, 'a human can verify a researcher brief naming itself')
1281
- assert(corpusMd.indexOf('【框架提示】') !== -1, 'the corpus contains the framework-feedback interaction')
1281
+ assert(corpusMd.indexOf('【研究所提示】') !== -1, 'the corpus contains the institute-feedback interaction')
1282
1282
  assert(corpusMd.indexOf('【会话重建 —— ') !== -1, 'the corpus contains a resume brief')
1283
1283
  assert(corpusMd.indexOf('【所办分派】') !== -1, 'the corpus contains an office assignment')
1284
1284
  assert(!/你是 \?/.test(corpusMd), 'the corpus contains NO wrong-identity "?" brief')
@@ -37,8 +37,8 @@
37
37
  * node tests/run-tests.mjs --no-temp-hygiene # skip the sweep and the roomier-drive temp root
38
38
  */
39
39
  import { spawn } from 'node:child_process'
40
- import { existsSync, readdirSync } from 'node:fs'
41
- import { cpus } from 'node:os'
40
+ import { existsSync, readdirSync, mkdtempSync, rmSync } from 'node:fs'
41
+ import { cpus, tmpdir } from 'node:os'
42
42
  import { join } from 'node:path'
43
43
  import { fileURLToPath } from 'node:url'
44
44
 
@@ -92,6 +92,22 @@ if (!has('no-temp-hygiene') && !has('self-check') && !has('counts')) {
92
92
  }
93
93
  } catch (e) { console.error('run-tests: temp hygiene skipped (' + ((e && e.message) || e) + ')') }
94
94
  }
95
+ // task-29: HOST-INDEPENDENT GATE. Whether THIS machine has TeX Live installed must not decide whether the
96
+ // suites that assert the "no LaTeX engine here" degradation are green: on this dev box `D:\texlive\...`
97
+ // really exists and the product now FINDS it (that is the task-28 fix), so those suites would redden for a
98
+ // reason that has nothing to do with the code under test. Point every child suite (they inherit
99
+ // `process.env` — see runSuite's spawn, which passes no `env`) at ONE empty scratch directory through the
100
+ // four documented-root sandbox gates: the product still probes every documented TeX root, one by one,
101
+ // inside that empty directory, finds nothing, and `triedPaths` still NAMES what it looked at. Real-machine
102
+ // discovery is covered by the SLV live runs, not by the gate; `audit-engine-faces` FACE 3 drives an
103
+ // explicit `paperLatexCommand`, which the sandbox never touches (only the known-install stage is rebased).
104
+ // Nothing else about the sweep changes; the scratch dir is removed when the runner exits.
105
+ const TEX_ROOTS_SANDBOX_DIR = mkdtempSync(join(tmpdir(), 'vibe-gate-tex-sandbox-'))
106
+ process.env.V2_TEX_ROOTS_SANDBOX = TEX_ROOTS_SANDBOX_DIR
107
+ process.env.V3_TEX_ROOTS_SANDBOX = TEX_ROOTS_SANDBOX_DIR
108
+ process.env.V4_TEX_ROOTS_SANDBOX = TEX_ROOTS_SANDBOX_DIR
109
+ process.env.V5_TEX_ROOTS_SANDBOX = TEX_ROOTS_SANDBOX_DIR
110
+ process.on('exit', () => { try { rmSync(TEX_ROOTS_SANDBOX_DIR, { recursive: true, force: true }) } catch (e) { /* best effort: a leftover empty scratch dir must never fail the gate */ } })
95
111
  const asJson = has('json')
96
112
  // task-13 (MEASURED): the default used to be min(4, cpus) = 4 on this 4-core box, and the heavy jobs then
97
113
  // ran 1.3x-4.5x SLOWER than standalone - the gate's own "slowest" lines recorded v2-fix-probes.mutants
@@ -103,6 +103,20 @@ const FAMILIES = [
103
103
  to: "zh?''",
104
104
  expect: /\[deliverable\/v4\]/,
105
105
  },
106
+ {
107
+ // task-23: a meeting speech missing from the end block must be recorded with a NAMED placeholder.
108
+ name: 'real1004-minutes: a silent meeting speech is stored as an empty body again',
109
+ from: "'(本轮结束块未含 input/summary:框架按空发言记录,见 docs/final-paper.md)'",
110
+ to: "''",
111
+ expect: /\[real1004-minutes\] a meeting speech missing from the end block is recorded with a NAMED placeholder/,
112
+ },
113
+ {
114
+ // task-27: v4 must inject Lean 4's -R, not the Lean-3 --search-path.
115
+ name: 'real1004-lean-flag: v4 falls back to the Lean-3 --search-path',
116
+ from: "const LEAN_SEARCH_PATH_FLAG = '-R'",
117
+ to: "const LEAN_SEARCH_PATH_FLAG = '--search-path'",
118
+ expect: /\[real1004-lean-flag\] v4 injects -R/,
119
+ },
106
120
  ]
107
121
  let red = 0
108
122
  for (const f of FAMILIES) { const t0 = Date.now(); const ok = runFamily(f); const ms = Date.now() - t0; if (ok) red++ }
@@ -621,6 +621,24 @@ section('G3 a paper whose REQUIRED artifact was not written is FAILED, never don
621
621
  await finish(m)
622
622
  }
623
623
 
624
+ // task-23 (real-host R3): all four meetings recorded correct speaker headers with EMPTY bodies, because a
625
+ // resident's end block carried no `input`/`summary` and the collector stored '' silently. A NAMED placeholder is
626
+ // required instead. SOURCE-LEVEL invariant: driving a full meeting with a deliberately silent resident would
627
+ // exercise the harness more than this one line.
628
+ {
629
+ const v4src = readFileSync(PLUGIN, 'utf8') // PLUGIN points at the mutated COPY when a family runs this suite
630
+ assert(v4src.includes('(本轮结束块未含 input/summary:框架按空发言记录') && !v4src.includes("{input:parsed.input||parsed.summary||'',voteSolved:"),
631
+ '★ [real1004-minutes] a meeting speech missing from the end block is recorded with a NAMED placeholder, never as a silent empty body')
632
+ }
633
+ // task-27: the same Lean-4 flag correction as v5 — the injected search root must be `-R`, and no agent-facing
634
+ // tool description may still teach the Lean-3 `--search-path` (which makes `lean` fail at argument parsing).
635
+ {
636
+ const v4s = readFileSync(PLUGIN, 'utf8')
637
+ assert(v4s.includes("const LEAN_SEARCH_PATH_FLAG = '-R'"),
638
+ '★ [real1004-lean-flag] v4 injects -R (Lean 4 root), not the Lean-3 --search-path')
639
+ assert(!v4s.includes('--search-path <VibeMath root>') && !/registerTool\(\s*'[^']+'\s*,\s*'[^']*--search-path/.test(v4s),
640
+ '★ [real1004-lean-flag] no v4 tool description still teaches --search-path')
641
+ }
624
642
  console.log('passed=' + passed + ' failed=' + failed)
625
643
  if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f) }
626
644
  if (failed) process.exit(1)