dsh-vibe-math 2.7.2 → 2.7.3

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 (112) hide show
  1. package/README.en.md +11 -10
  2. package/README.md +13 -12
  3. package/cordis.patch.yml +112 -16
  4. package/docs/AUDIT-CHECKLIST.md +229 -3
  5. package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
  6. package/docs/final-paper.md +2 -2
  7. package/docs/math-computation.md +13 -6
  8. package/docs/parameter-schema.md +37 -0
  9. package/docs/release-notes/README.en.md +1 -0
  10. package/docs/release-notes/README.md +1 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  12. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  13. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  14. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  15. package/docs/release-notes/TEMPLATE.md +6 -0
  16. package/docs/status-report-fields.md +167 -0
  17. package/docs/test-timing.md +8 -5
  18. package/installer.js +27 -17
  19. package/package.json +250 -203
  20. package/prompt-corpus-persona/persona-corpus.json +9 -9
  21. package/prompt-corpus-persona/persona-corpus.md +225 -33
  22. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  23. package/prompt-corpus-v2/formal-verify-v2.md +2287 -283
  24. package/prompt-corpus-v3/formal-verify-v3.json +105 -105
  25. package/prompt-corpus-v3/formal-verify-v3.md +213 -143
  26. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  27. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  28. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  29. package/prompt-corpus-v5/prompt-corpus-v5.md +451 -207
  30. package/scripts/update-doc-counts.mjs +71 -0
  31. package/tests/audit-artifact-docs.mjs +72 -0
  32. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  33. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  34. package/tests/audit-installer-compat.test.mjs +52 -4
  35. package/tests/audit-installer-policy.test.mjs +41 -8
  36. package/tests/audit-math-computation-contract.mjs +64 -8
  37. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  38. package/tests/audit-math-computation-parity.mjs +164 -3
  39. package/tests/audit-math-computation-sensitivity.mjs +4 -2
  40. package/tests/audit-package-membership.mjs +57 -0
  41. package/tests/audit-package-membership.mutants.mjs +28 -0
  42. package/tests/audit-participant-set-parity.mjs +160 -0
  43. package/tests/audit-path-discipline.mjs +83 -0
  44. package/tests/audit-path-discipline.mutants.mjs +51 -0
  45. package/tests/audit-persona-sensitivity.mjs +14 -0
  46. package/tests/audit-persona-surface.mutants.mjs +69 -0
  47. package/tests/audit-persona-surface.test.mjs +63 -22
  48. package/tests/audit-prompt-duplication.mjs +143 -0
  49. package/tests/audit-prompt-duplication.mutants.mjs +50 -0
  50. package/tests/audit-prompt-invariants.mjs +43 -5
  51. package/tests/audit-readme-bilingual.test.mjs +152 -0
  52. package/tests/audit-readme-counts.mjs +95 -0
  53. package/tests/audit-readme-counts.mutants.mjs +52 -0
  54. package/tests/audit-registration.mjs +78 -0
  55. package/tests/audit-status-report-fields.mjs +172 -0
  56. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  57. package/tests/audit-v3-registration-parity.mjs +156 -0
  58. package/tests/audit-v5-integrity.mjs +300 -13
  59. package/tests/audit-v5-lean-abstention.mjs +392 -0
  60. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  61. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  62. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  63. package/tests/audit-v5-sensitivity.mjs +12 -7
  64. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  65. package/tests/e2e-v5-round2.test.mjs +458 -13
  66. package/tests/formal-verify-v2.test.mjs +91 -43
  67. package/tests/formal-verify-v3.mutants.mjs +117 -0
  68. package/tests/formal-verify-v3.test.mjs +85 -39
  69. package/tests/formal-verify-v4.mutants.mjs +152 -0
  70. package/tests/formal-verify-v4.test.mjs +541 -9
  71. package/tests/formal-verify-v5.test.mjs +68 -9
  72. package/tests/helpers/doc-counts-format.mjs +41 -0
  73. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  74. package/tests/math-computation-a1.mutants.mjs +72 -0
  75. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  76. package/tests/math-computation-discovery.mutants.mjs +76 -0
  77. package/tests/math-computation-shared.test.mjs +274 -5
  78. package/tests/math-computation-v2.test.mjs +767 -0
  79. package/tests/math-computation-v3.test.mjs +759 -0
  80. package/tests/math-computation-v4.mutants.mjs +133 -0
  81. package/tests/math-computation-v4.test.mjs +739 -0
  82. package/tests/prompt-v5-integrity.test.mjs +13 -2
  83. package/tests/run-tests.mjs +139 -9
  84. package/tests/run-tests.mutants.mjs +70 -0
  85. package/tests/selfdrive-v5.mjs +53 -1
  86. package/tests/selfdrive-v5.mutants.mjs +132 -0
  87. package/tests/v2-fix-probes.mutants.mjs +100 -0
  88. package/tests/v2-fix-probes.test.mjs +1039 -0
  89. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  90. package/tests/v3-fix-probes.mutants.mjs +95 -0
  91. package/tests/v3-fix-probes.test.mjs +891 -0
  92. package/tests/v5-institute-fixes.mutants.mjs +190 -0
  93. package/vibe-math-v2/agent.cordis.yml +62 -6
  94. package/vibe-math-v2/math-computation.js +130 -41
  95. package/vibe-math-v2/math-engines.js +12 -12
  96. package/vibe-math-v2/vibe-math-v2.js +388 -113
  97. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
  98. package/vibe-math-v3/agent.cordis.yml +72 -6
  99. package/vibe-math-v3/math-computation.js +130 -41
  100. package/vibe-math-v3/math-engines.js +12 -12
  101. package/vibe-math-v3/vibe-math-v3.js +422 -126
  102. package/vibe-math-v4/agent.cordis.yml +44 -8
  103. package/vibe-math-v4/math-computation.js +130 -41
  104. package/vibe-math-v4/math-engines.js +12 -12
  105. package/vibe-math-v4/vibe-math-v4.js +196 -43
  106. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
  107. package/vibe-math-v5/agent.cordis.yml +46 -12
  108. package/vibe-math-v5/math-computation.js +130 -41
  109. package/vibe-math-v5/math-engines.js +12 -12
  110. package/vibe-math-v5/vibe-math-v5.js +1173 -218
  111. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +80 -12
  112. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +2 -2
@@ -1,4 +1,96 @@
1
1
  // ============================================================
2
+ // Defect class: F-6 (durability surfacing). A swallowed durable write / index rebuild / compaction
3
+ // failure must be NAMED once, with its consequence, instead of disappearing silently.
4
+ // Run: node tests/formal-verify-v4.test.mjs
5
+ //
6
+ // RECORDED BOUNDARY - all four product warnings EXIST (commit d02bdbf: warnedWriteFormalTodo,
7
+ // warnedDeferFormalTodo, warnedIndexRebuild, warnedCompaction; each once-per-process and each stating
8
+ // its consequence) but are NOT captured automatically by this suite. NOT REPRODUCED != NOT
9
+ // IMPLEMENTED: the reasons below are measured on the mock host, not assumed.
10
+ // site 1 closeVerify - guarded by `if(formalOn() && formalTodos.some(id===target))` (unreachable
11
+ // in off mode); the close is finalised by finalizeVerify's ASYNC chain
12
+ // AFTER proposeAndVote() returns, so a deterministic assertion would need
13
+ // a product-side fault hook (rejected: it would become a new
14
+ // agent-settable parameter) or dedicated verify orchestration.
15
+ // site 2 deferForFormal - KNOWN COVERAGE HOLE (measured). Payload-anchored injection (fail the
16
+ // Formal/TODO.md write whose payload mentions the target) is CONSUMED by an
17
+ // earlier TODO write in require mode (recording the proposition also writes
18
+ // that file and names the target), so the induced failure lands OUTSIDE
19
+ // deferForFormal's try: the run is derailed (Formal/TODO.md never created,
20
+ // section 9's own TODO assertions fail) and NO warning is emitted
21
+ // (`matched=0 of 0`). Anchoring by State/formal.json as a precursor was also
22
+ // measured imprecise (armed=9, failed=1, no warning). The warning itself
23
+ // exists and is exercised by the F-2-style unit surface at
24
+ // tests/audit-f1-compact-fix.test.mjs; only the in-suite capture is missing.
25
+ // site 2 (old note) - same async-finalisation problem (require mode only).
26
+ // site 3 settleLeanJob - the injected index-write failure is consumed by whichever
27
+ // rebuildLeanLibIndexes() caller fires first (measured: armed=3,
28
+ // failed=1, warning not emitted), so the mock cannot discriminate the
29
+ // caller that this site guards. Caller-discriminating injection was
30
+ // attempted (run every other rebuild caller first, then arm): the settle
31
+ // path's index write was still never reached (measured failed=0) -
32
+ // caller-discriminating injection attempted, still not reached.
33
+ // AUTHOR-MEASURED, NOT INDEPENDENTLY REPRODUCED: R17 could not reproduce the
34
+ // `armed=3 failed=1` / `failed=0` instrumentation numbers (it marked them
35
+ // still-unsure). It DID verify the STRUCTURE this boundary rests on: the
36
+ // single-line best-effort wrapper, all six callers (settleLeanJob,
37
+ // writeLeanLibIndexesSafe, the three leanArchive paths, the lean_lib tool)
38
+ // now covered by the one-shot naming inside rebuildLeanLibIndexes, and every
39
+ // later rebuild rewriting the index => self-healing. The boundary stands on
40
+ // that structural evidence; the two raw numbers above are author-measured only.
41
+ // site 4 realCompact - SURFACED (section N15): the mock resolves the child agent and exposes a
42
+ // THROWING compaction service only under an injection switch, so a completed
43
+ // resident turn reaches realCompact and its catch is asserted to NAME the
44
+ // failure exactly once with its consequence (mutant: the warning guard
45
+ // disabled => `460/2`, red on N15/4).
46
+ // site 4 (old note) - realCompact returns early unless liveAgentOf(r.childId) resolves; this
47
+ // mock's agents.get() maps only sess-A, so a resident turn never reaches
48
+ // the compaction call (probe: the service is never invoked).
49
+ // F1 COVERAGE HOLE (window) - measured, not assumed. The consensus-membership fix (predicate + roster
50
+ // guard) is covered by the SOURCE-LEVEL guards in section N13 (both single-site mutants redden by name:
51
+ // count predicate => 457/2; `_inRoster=true` => 458/1). The behavioural WINDOW the v2/v3 owner proposed
52
+ // (a removed, still-in-flight frozen voter's late ballot) was driven inside section D4 and measured
53
+ // unobservable through the public API: fixed => `before=0/2 after=0/1 card=false`; guard deleted (M2) =>
54
+ // the SAME `0/1`; count predicate (M1) => the SAME `0/1`. The late ballot therefore never reaches the
55
+ // recording site in this composition, so a windowed assertion could not be reddened by M2 and would be
56
+ // vacuous - it was NOT landed. Regression coverage for F1 is N13 (source level).
57
+
58
+ // F-6 (:786) FIXED, not merely bounded: a failed build-context comparison now records
59
+ // `contextChanged=null` (unknown != unchanged) and emits a one-time named warning (warnedContextProbe,
60
+ // wording states the consequence). It carries NO automated assertion here: the branch is UNREACHABLE
61
+ // from this host mock - `leanBuildContext()` is non-throwing by construction (its only host call,
62
+ // resolveExecutable, is wrapped and degrades to the configured name) and every other step is a
63
+ // defensive coercion over agent-settable params (leanSearchPaths/leanArgs are Array.isArray-guarded and
64
+ // String()-coerced). A mutant without an assertion would prove nothing, so none is claimed.
65
+ // Consumer check (repo-wide grep of `contextChanged`): only 4 references, all in v4's settle paths -
66
+ // the field is write-only for the JSON reader, so `null` cannot be mis-read as `false`.
67
+ // Sites 3/4 carry test-side injection switches in the F-6 work branch (_oneoff/tw-patch.mjs) which
68
+ // can be re-landed together with a host-side caller discriminator.
69
+ // ---------------------------------------------------------------
70
+ // SHIPPED ASSERTION RULES (the rules behind N13-N17; the authoring ledger lives outside the package,
71
+ // so the essentials ship HERE, next to the guards they govern).
72
+ // (A) LIVENESS BEFORE SILENCE (governs N17/F-5, especially case 1c): every "no warning / no write /
73
+ // no re-anchor" assertion must ALSO prove that the code under test RAN, and the proof must be
74
+ // taken at the step CLOSEST to that branch. Measured counter-examples on this mock: "the policy
75
+ // service was queried" is NOT proof (`sandboxPolicyOf()` is read by BOTH workspaceRoot() and
76
+ // getPolicy()), and "the write succeeded" is NOT proof either (with an unknown cwd the same call
77
+ // returns ok:false for an UNRELATED path reason). The workable instrument is the resolve() call
78
+ // count - `LIVENESS: resolve calls=11` - because resolve({session}) runs immediately before
79
+ // fenceRootDriftNote(). Mutant "unknown cwd => warn" reddens `F-5/1c ... stays SILENT (drift=1)`.
80
+ // (B) A SYMBOL'S EXISTENCE IS NOT ITS USE (governs the F-3/F-4c verdicts recorded in-product at
81
+ // vibe-math-v4.js:3553): a count is a LEAD, never a verdict - the verdict must be read at the
82
+ // SEMANTIC UNIT. Measured: `registerTool(` appears 36 times but there is exactly ONE registration
83
+ // unit (the ctx.effect inside the registerTool helper), so no call site needs its own wrapper;
84
+ // and a failed mkdir is named ONCE INSIDE ensureDirs (warnedMkdir), which covers every caller -
85
+ // including the bare `await ensureDirs()` - so no caller-side return check is required.
86
+ // (C) G-6 HOST CONCURRENCY CAP, assertion level: when the host's live-child cap refuses a spawn, the
87
+ // assertion must require the cap to be NAMED together with the knob that raises it, the durable
88
+ // marker to survive (hostChildLimit / failReason), and the freed capacity to be spent on exactly
89
+ // ONE deferred member. Field-level semantics: docs/status-report-fields.md (pendingSpawns).
90
+ // (D) A2 WAKE/WATCHDOG, assertion level: ack only AFTER a successful delivery, and advance the round
91
+ // counter only AFTER a successful send - asserted under INJECTED send failures, never on the
92
+ // happy path (where build == send == success hides the defect). Shipped families: v5 G1/G2.
93
+ // ---------------------------------------------------------------
2
94
  // V4 LEAN FORMAL VERIFICATION SUITE (docs/formal-verification.md)
3
95
  //
4
96
  // Asserts the whole contract of the `formalVerify` knob for the v4 preset:
@@ -43,6 +135,7 @@ function pluginUrl() {
43
135
  return pathToFileURL(fileURLToPath(new URL('file:///' + s.replace(/\\/g, '/'))))
44
136
  }
45
137
  const PLUGIN = pluginUrl()
138
+ function PLUGIN_SRC() { return readFileSync(process.env.V4_PLUGIN ? String(process.env.V4_PLUGIN) : fileURLToPath(new URL('../vibe-math-v4/vibe-math-v4.js', import.meta.url)), 'utf8') } // protocol 搂9: ONE seam-aware resolution for source-text guards
46
139
  // docs §10 item 10: the Lean prompt corpus is shipped under prompt-corpus-v4/ so a HUMAN can read
47
140
  // the exact text the framework sends. V4_CORPUS_DIR overrides the destination (same convention v3
48
141
  // and v5 use for V3_CORPUS_DIR / V5_CORPUS_DIR).
@@ -70,6 +163,13 @@ let subprocessAvailable = true
70
163
  // Used by §13 to prove the withdrawal is not a best-effort delete: the framework must confirm the
71
164
  // file is gone through the fs service and fall back to overwriting it with a withdrawal notice.
72
165
  let shellDeletesFiles = true
166
+ let shellMkdirExit = 0
167
+ let fsWriteFailPred = null // F-6/N18: (targetKey) => boolean; the first matching write FAILS (payload/path anchored)
168
+ let fsWriteFailOnce = false
169
+ let sandboxDriftRoot = null // F-5: when set, the mock policy resolves THIS root (drift/containment harness)
170
+ let sandboxNoRoot = false // F-5: when set, the mock policy exposes NO interpretable root (no-op path)
171
+ let sandboxUnknownCwd = false // F-5/1c: the captured root agent has NO cwd (unknown => `.` guard)
172
+ let compactionFails = false // F-6 site 4: expose a THROWING compaction service + resolve child agents
73
173
  const leanRuns = [] // every spawn the framework made, for cwd/argv assertions
74
174
  const terminated = [] // files whose handle the framework actively terminate()d (docs §7)
75
175
  const shellCalls = [] // every platform-shell script (mkdir at mount, Remove-Item on defect)
@@ -95,7 +195,7 @@ function makeSubprocess() {
95
195
  if (shellDeletesFiles && /Remove-Item/.test(script) && m) rmSync(m[1].replace(/''/g, "'"), { force: true })
96
196
  if (shellDeletesFiles && /^rm -f /.test(script)) for (const q of script.slice(6).match(/'[^']*'/g) || []) rmSync(q.slice(1, -1), { force: true })
97
197
  return {
98
- done: Promise.resolve({ exitCode: 0, signal: null }),
198
+ done: Promise.resolve({ exitCode: (/New-Item|^mkdir -p /.test(script) ? shellMkdirExit : 0), signal: null }),
99
199
  collected: { stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) }, stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) } },
100
200
  terminate() {},
101
201
  }
@@ -135,9 +235,21 @@ function makeHost() {
135
235
  const WS = mkdtempSync(join(tmpdir(), 'vibe-v4-lean-'))
136
236
  const listeners = {}, toolRegs = [], spawns = [], followups = [], cmdRegs = []
137
237
  const subprocess = makeSubprocess()
238
+ let sandboxPolicyQueries = 0 // F-5/1c: how many times the policy service was queried
239
+ let sandboxResolveCalls = 0 // F-5/1c: resolve({session}) calls = fenceRootDriftNote invocations
138
240
  let ROOT
139
241
  const ctx = {
140
- get(name) { return name === 'subprocess' && subprocessAvailable ? subprocess : undefined },
242
+ get(name) {
243
+ if (name === 'subprocess' && subprocessAvailable) return subprocess
244
+ if (name === 'sandboxPolicy') sandboxPolicyQueries += 1 // F-5/1c liveness: the policy path really ran
245
+ if (name === 'sandboxPolicy' && sandboxUnknownCwd) return { resolve: () => { sandboxResolveCalls += 1; return { workspaceRoot: WS + '/fenced-elsewhere' } } } // NO top-level root: workspaceRoot() falls back to '.'
246
+ if (name === 'sandboxPolicy' && (sandboxDriftRoot || sandboxNoRoot)) return sandboxNoRoot ? { allow: ['x'], deny: [], resolve: () => ({ allow: ['x'], deny: [] }) } : { workspaceRoot: sandboxDriftRoot, resolve: () => ({ workspaceRoot: sandboxDriftRoot }) }
247
+ if (name === 'compaction' && compactionFails) return {
248
+ async compactIfNeeded() { throw new Error('COMPACTION_FAIL (injected)') },
249
+ async compactNow() { throw new Error('COMPACTION_FAIL (injected)') },
250
+ }
251
+ return undefined
252
+ },
141
253
  on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
142
254
  effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
143
255
  logger: { info() {}, warn() {}, error() {} },
@@ -151,21 +263,28 @@ function makeHost() {
151
263
  async sendMessage(parent, childId, blocks) { followups.push({ childId, blocks }) },
152
264
  interrupt() {},
153
265
  },
154
- agents: { roots() { return [] }, get(id) { return id === 'sess-A' ? ROOT : undefined } },
266
+ agents: { roots() { return [] }, get(id) {
267
+ if (compactionFails && id && id !== 'sess-A') return { id, session: { id, header: { cwd: WS, parentSession: 'sess-A' } } }
268
+ return id === 'sess-A' ? ROOT : undefined
269
+ } },
155
270
  fs: {
156
271
  async resolve(rel, opts) { const b = (opts && opts.cwd) || WS; const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/')); return { targetKey: p, displayPath: p } },
157
272
  async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
158
273
  async readText(t) { return readFileSync(t.targetKey, 'utf8') },
159
- async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
274
+ async writeText(t, c) { if (fsWriteFailOnce && fsWriteFailPred && fsWriteFailPred(String(t.targetKey), String(c))) { fsWriteFailOnce = false; throw new Error('FS_WRITE_FAIL (injected): ' + t.targetKey) } mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
160
275
  async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
161
276
  },
162
277
  }
163
278
  ROOT = { id: 'sess-A', options: { provider: 'mock', model: 'm' }, session: { id: 'sess-A', header: { cwd: WS, parentSession: undefined } }, followup() {}, ctx: undefined }
279
+ if (sandboxUnknownCwd) ROOT.session.header.cwd = '' // F-5/1c: unknown session cwd
164
280
  const projectRoot = join(WS, 'VibeMath', 'Projects', 'default')
165
281
  const vibeRoot = join(WS, 'VibeMath')
166
282
  let consumed = 0
167
283
  const h = {
168
284
  WS, ctx, toolRegs, spawns, followups, ROOT, projectRoot, vibeRoot, cmdRegs,
285
+ policyQueries: () => sandboxPolicyQueries,
286
+ resolveCalls: () => sandboxResolveCalls,
287
+ resetResolveCalls: () => { sandboxResolveCalls = 0 },
169
288
  /** Drive the registered `/v4` slash command — the OTHER parameter surface the spec §B names. */
170
289
  async cmd(rawInput) { const c = cmdRegs.find(x => x.name === 'v4'); if (!c) throw new Error('no /v4 command'); return await c.handler({ agent: ROOT, rawInput }) },
171
290
  async callTool(n, a, agent) {
@@ -590,8 +709,8 @@ assert(existsSync(join(provedPath, 'sq_odd.lean')), 'the lemma exists under Vibe
590
709
  assert(/ZMod5/.test(readIf(join(libPath, 'Index.md'))), 'Lib/Index.md lists the new definition')
591
710
  assert(/sq_odd/.test(readIf(join(provedPath, 'Index.md'))), 'Proved/Index.md lists the new lemma')
592
711
  // `from` copies an existing workspace .lean file instead of inline content
593
- writeFileSync(join(D.projectRoot, 'Formal', 'src.lean'), 'def copied := 7\n', 'utf8')
594
- const fromRes = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'Copied', from: 'Formal/src.lean' }, r1)
712
+ writeFileSync(join(D.projectRoot, 'Formal', 'PLUGIN_SRC().lean'), 'def copied := 7\n', 'utf8')
713
+ const fromRes = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'Copied', from: 'Formal/PLUGIN_SRC().lean' }, r1)
595
714
  assert(fromRes.ok === true && readIf(join(libPath, 'Copied.lean')) === 'def copied := 7\n', "kind='def' accepts from=<existing .lean file>")
596
715
  const fromEsc = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'Evil', from: '../../../../etc/passwd' }, r1)
597
716
  assert(fromEsc.ok === false && fromEsc.code === 'V4_INVALID_ARGUMENT', "★ kind='def' refuses a `from` outside the VibeMath root")
@@ -1176,7 +1295,7 @@ const K = await establish()
1176
1295
  const later = byLabel('meeting/later speaker (others relayed)')
1177
1296
  assert(!!first && !!later, '★ the corpus carries the MEETING prompt (two perspectives: no prior speech / others already relayed)')
1178
1297
  assert(/meeting is in progress/.test(first.prompt) && /meeting is in progress/.test(later.prompt), 'the meeting entries are built by the real meeting prompt builder')
1179
- assert(/目前还没人发言/.test(first.prompt), 'the first-speaker entry shows the "nobody has spoken yet" branch')
1298
+ assert(/并行独立发言/.test(first.prompt), 'the first-speaker entry states the parallel-statements semantics (nobody has spoken yet)')
1180
1299
  assert(/已有发言(他人 input,已转发给你)/.test(later.prompt) && /我建议先验证 p-corpus/.test(later.prompt), '★ the later-speaker entry carries a teammate contribution VERBATIM (the group-chat relay is reviewable)')
1181
1300
  assert(/\[r-1\]/.test(later.prompt) && !/\[r-2\]/.test(later.prompt), 'the relayed-speech list excludes the reader and names the real sender id')
1182
1301
  assert(/voteSolved/.test(first.prompt) && /停止表决必须是绝对票/.test(first.prompt), '★ the meeting prompt states the stop-vote contract the code enforces (every speaker must answer true)')
@@ -1324,12 +1443,22 @@ section('20 crash recovery: queued re-driven, changed content never verified, se
1324
1443
  await N.callTool('vibe_v4_start', { problem: '恢复扫描', residentCount: 1 })
1325
1444
  const a = await N.callTool('vibe_v4_lean_job', { jobId: 'p-rec-aaaaaaaaaaaa' })
1326
1445
  const b = await N.callTool('vibe_v4_lean_job', { jobId: 'p-stale-bbbbbbbbbbbb' })
1327
- assert(a.state === 'queued' || a.state === 'running' || a.state === 'settled', '★ recovery re-drives a `running` job whose content still matches (state=' + a.state + ')')
1446
+ assert(a.attempts >= 2 && (a.state === 'queued' || a.state === 'running' || a.state === 'settled'), '* a hash-matching interrupted job is RE-DRIVEN (never adopted): attempts 1 -> ' + a.attempts + ', state=' + a.state)
1447
+ // (cold-state liveness, code path recoverLeanJobs:911): with NO explicit followup the armed heartbeat
1448
+ // must drain the re-queued job on a later tick - poll with the diagnostic triple in the message.
1449
+ {
1450
+ const t0 = Date.now()
1451
+ let cur = a, tries = 0
1452
+ while (tries < 60 && cur.state === 'queued') { tries++; await new Promise((r) => setTimeout(r, 100)); cur = await N.callTool('vibe_v4_lean_job', { jobId: 'p-rec-aaaaaaaaaaaa' }) }
1453
+ assert(cur.state !== 'queued', '* the next tick DRAINS the re-queued job without any explicit followup (state=' + cur.state + ', window=6000ms, waited=' + (Date.now() - t0) + 'ms, attempts=' + tries + ')')
1454
+ const stLive = await N.callTool('vibe_v4_status', {})
1455
+ assert(stLive.running === true, '* the institute clock keeps running after a stale-only recovery: recoverLeanJobs:911 arms ONE idle pass even when nothing is drainable (documented behaviour - if a future change stops arming on stale-only, THIS reddens)')
1456
+ }
1328
1457
  assert(b.state === 'interrupted', '★ a `running` job whose file CHANGED is interrupted, not verified (state=' + b.state + ')')
1329
1458
  const st = await N.callTool('vibe_v4_status', {})
1330
1459
  assert(!st.formal.passed.includes('p-stale'), '★ the changed-content job never reaches `passed`')
1331
1460
  assert(!st.formal.passed.includes('p-done') && !existsSync(join(N.projectRoot, 'Verified', 'Lean', 'p-done.lean')), '★ a fresh run does NOT adopt a previous run’s `settled` record (no stale passed, no minted proof)')
1332
- assert(N.followups.map(promptOf).join('\n').length >= 0, 'recovery ran without throwing')
1461
+ assert(N.followups.length === 0, '* the stale-job recovery path wakes NO new resident (followups=' + N.followups.length + ') - the wake decision belongs to the watchdog; this line is falsifiable, so a future change of that contract reddens HERE')
1333
1462
  }
1334
1463
 
1335
1464
  // ===============================================================
@@ -1549,6 +1678,7 @@ section('D4 cross-view: one frozen participant set, seen identically by every vi
1549
1678
  assert(st2.rosterVersion > c0.rosterVersion, 'hiring mid-verify bumps rosterVersion (' + c0.rosterVersion + ' -> ' + st2.rosterVersion + ')')
1550
1679
  assert(st2.consensus.expected === st2.frozenParticipants.length && st2.consensus.expected === c0.expected,
1551
1680
  '★ after a mid-verify HIRE the frozen set is UNCHANGED and both views still agree (newcomer did not join)')
1681
+ assert(typeof rv2.rosterVersion === 'number' && typeof st2.consensus.rosterVersion === 'number', 'non-vacuous: both frozen views EXPOSE a numeric rosterVersion (a relative === passes when both are absent)')
1552
1682
  assert(rv2.rosterVersion === st2.consensus.rosterVersion,
1553
1683
  '★ after the HIRE the two FROZEN views (status().consensus + report().verify) still report the SAME rosterVersion')
1554
1684
  assert(st2.rosterVersion > rv2.rosterVersion,
@@ -1566,8 +1696,410 @@ section('D4 cross-view: one frozen participant set, seen identically by every vi
1566
1696
  assert(st4.verifyInProgress === false, '★ the mid-verify hire/fire did NOT prevent the verification from concluding')
1567
1697
  }
1568
1698
 
1699
+
1700
+
1701
+ // ===============================================================
1702
+ // 24. round A F2 (design-preserving): a HIRE never extends the frozen set, so the live counter may
1703
+ // drift ahead of it (that drift IS the staleness signal). A FIRE prunes the frozen set, so the
1704
+ // snapshot must be re-stamped — otherwise the same version would denote two different sets.
1705
+ // ===============================================================
1706
+ section('24 round A F2: hire keeps the snapshot stale-able; fire re-stamps it (version ≡ set)')
1707
+ {
1708
+ const N = await establish()
1709
+ await N.callTool('vibe_v4_meeting', { agenda: 'round A 快照一致性' })
1710
+ const s1 = await N.callTool('vibe_v4_status', {})
1711
+ assert(!!s1.frozen && Array.isArray(s1.frozen.participants) && s1.frozen.participants.length >= 1, '★ an active consensus exposes frozen{version,participants} (additive)')
1712
+ assert(s1.frozen.version === s1.rosterVersion, 'at freeze time the snapshot version equals the live counter')
1713
+ assert(typeof s1.consensus.rosterVersion === 'number' && Array.isArray(s1.consensus.participants) && s1.consensus.rosterVersion === s1.frozen.version, '\u2605 F3 (absolute first, unguarded): status().consensus EXPOSES rosterVersion + participants and it equals the frozen snapshot version')
1714
+ const v1 = s1.rosterVersion
1715
+ await N.callTool('vibe_v4_add_member', { direction: 'round A 新增成员' })
1716
+ const s2 = await N.callTool('vibe_v4_status', {})
1717
+ assert(s2.rosterVersion > v1, 'hiring bumps the numeric rosterVersion (monotone)')
1718
+ assert(s2.frozen.version === v1, '★ a HIRE leaves the frozen snapshot version untouched (its set did not change)')
1719
+ assert(s2.frozen.participants.length === s1.frozen.participants.length, '★ …and the frozen requirement set is unchanged (frozen means frozen)')
1720
+ assert(s2.rosterVersion > s2.frozen.version, '★ the live-vs-frozen drift remains the visible staleness signal')
1721
+ const removed = s1.frozen.participants[0]
1722
+ await N.callTool('vibe_v4_remove_member', { id: removed })
1723
+ const s3 = await N.callTool('vibe_v4_status', {})
1724
+ assert(s3.frozen.participants.indexOf(removed) === -1, '★ a FIRE prunes the frozen set (the wait is released)')
1725
+ assert(s3.frozen.version === s3.rosterVersion && s3.frozen.version > v1, '★ F2: the pruned set carries a NEW version — no window where a changed set keeps the old version')
1726
+ }
1727
+ // ===============================================================
1728
+ // ===============================================================
1729
+ // N4. F-PROMPT: a documented 'true no-op' must be asserted on the DELIVERED prompt, not on the
1730
+ // config path. Under formalVerify:'off' the verify prompt must carry NO formalization clause
1731
+ // and NO forward reference to a segment that is not delivered.
1732
+ // ===============================================================
1733
+ section('N4 off: the DELIVERED verify prompt has no formalization content or dangling pointer')
1734
+ {
1735
+ const P = await establish()
1736
+ const off = await P.prompts('verify', 'r-1', { target: 'p-fprompt', stage: 'independent' })
1737
+ assert(off.length > 100 && /\u9a8c\u8bc1/.test(off), '* the delivered verify prompt is non-trivial (non-vacuity: a missing/empty prompt must not pass the negations below)')
1738
+ assert(!/\u5f62\u5f0f\u5316\u6bb5/.test(off), '* the off-mode verify prompt contains no forward reference to the formalization segment (\u5f62\u5f0f\u5316\u6bb5)')
1739
+ assert(!/formal/.test(off), '* and no formalization content at all (no `formal` receipt clause) - the documented true no-op')
1740
+ await P.callTool('vibe_v4_set', { formalVerify: 'encourage' })
1741
+ const enc = await P.prompts('verify', 'r-1', { target: 'p-fprompt', stage: 'independent' })
1742
+ assert(/Lean|formal/.test(enc), '* under encourage the delivered prompt DOES carry the formalization block (the gate is a gate, not a deletion)')
1743
+ }
1569
1744
  // ===============================================================
1745
+ // N5. v4 path basis - CORRECTED CONTRACT (the earlier comment here pinned the OLD, WRONG shape:
1746
+ // it demanded a member root. v4's writers/readers live at the PROJECT ROOT: publishProgress writes
1747
+ // Progress/<rId>/progress.md, the record tools write Propos|Methods|Subproblems/<rId>/<id>.md, and
1748
+ // countArtifacts() scans exactly those bases under frameworkRoot(). A Members/<placeholder>/... path
1749
+ // is therefore a path nothing scans - the realistic documented example is base + '/Propos/r-1/p-1.md'.
1750
+ // (Named in the report as the "comment pinning old behaviour" case.)
1751
+ // ===============================================================
1752
+ section('N5 v4 member-facing paths are PROJECT-ROOT (the shape the framework scans), and no Members/ shape')
1753
+ {
1754
+ const P = await establish()
1755
+ const text = (await P.prompts('brainstorm', 'r-1')) + '\n' + (await P.prompts('normal', 'r-1'))
1756
+ // INVERTED (critical correction): the framework's writer/reader live at the PROJECT ROOT
1757
+ // (publishProgress writes Progress/<rId>/progress.md; countArtifacts scans Propos|Methods|Subproblems
1758
+ // under frameworkRoot()). The delivered text must document THOSE paths, never Members/<x>/...
1759
+ const decls = (text.match(/(?:^|[^A-Za-z0-9/_.-])(?:Progress|Propos|Methods|Subproblems)\/<[^>]+>\//g) || [])
1760
+ assert(decls.length >= 4, '* every member-visible library declaration is PROJECT-ROOT relative (Progress/<you>/, Propos/<you>/, ...) - the shape the framework scans (found ' + decls.length + ')')
1761
+ const wrongMembers = (text.match(/Members\/<[^>]+>\//g) || [])
1762
+ assert(wrongMembers.length === 0, '* NO member-facing declaration uses the Members/<x>/ shape (the framework never scans it) - found ' + JSON.stringify(wrongMembers))
1763
+ assert(/Progress\/<你>\/progress\.md/.test(text), '* the documented progress path matches publishProgress (Progress/<rId>/progress.md)')
1764
+ assert(/Propos\/<你>\//.test(text) && /Methods\/<你>\//.test(text) && /Subproblems\/<你>\//.test(text), '* the documented card paths match the record tools (Propos|Methods|Subproblems/<rId>/<id>.md)')
1765
+ // CLASS GUARD (one level above v3's): what the prompt documents must be what the code reads/scans
1766
+ const src4 = readFileSync(fileURLToPath(new URL('../vibe-math-v4/vibe-math-v4.js', import.meta.url)), 'utf8')
1767
+ for (const base of ['Progress', 'Propos', 'Methods', 'Subproblems']) {
1768
+ assert(new RegExp("'" + base + "'").test(src4), '* class guard: the framework reads/scans ' + base + ' - every documented write path must map onto a scanned base')
1769
+ }
1770
+ assert(/['"]Members\/['"]\s*\+/.test(src4) === false, '* class guard: no code path (writer) builds a Members/<id>/... path in v4')
1771
+ }
1772
+ section('N6 four-preset path discipline (member root / prefix-note)')
1773
+ {
1774
+ const { readFileSync } = await import('node:fs')
1775
+ const { fileURLToPath } = await import('node:url')
1776
+ const pick = (env, rel) => process.env[env] ? process.env[env] : fileURLToPath(new URL(rel, import.meta.url))
1777
+ const files = {
1778
+ v2: pick('V2_PLUGIN', '../vibe-math-v2/vibe-math-v2.js'),
1779
+ v3: pick('V3_PLUGIN', '../vibe-math-v3/vibe-math-v3.js'),
1780
+ v4: pick('V4_PLUGIN', '../vibe-math-v4/vibe-math-v4.js'),
1781
+ v5: pick('V5_PLUGIN', '../vibe-math-v5/vibe-math-v5.js'),
1782
+ }
1783
+ const ROOT = /Members\/<[^>]+>\/(?:Progress|Propos|Methods|Subproblems)\//g
1784
+ const LEGACY = /(?:^|[^/>])(?:Progress|Propos|Methods|Subproblems)\/<[^>]+>\//g
1785
+ for (const k of ['v5']) { // v5 pending vm-v5's re-verification; v4 handled explicitly below
1786
+ const src = readFileSync(files[k], 'utf8')
1787
+ const root = (src.match(ROOT) || []).length
1788
+ const legacy = (src.match(LEGACY) || []).length
1789
+ assert(root >= 4, '* ' + k + ': every member-visible library declaration carries the member root (found ' + root + ')')
1790
+ assert(legacy === 0, '* ' + k + ': no declaration keeps the legacy project-root-relative shape (found ' + legacy + ')')
1791
+ }
1792
+ // v4 (critical correction): the framework writes/reads at the PROJECT ROOT - publishProgress writes
1793
+ // Progress/<rId>/progress.md and countArtifacts scans Propos|Methods|Subproblems under frameworkRoot(),
1794
+ // so a Members/<x>/... declaration is a path NOTHING scans.
1795
+ {
1796
+ const src4 = files.v4 ? readFileSync(files.v4, 'utf8') : ''
1797
+ const decl4 = (src4.match(/(?:^|[^A-Za-z0-9/_.-])(?:Progress|Propos|Methods|Subproblems)\/<[^>]+>\//g) || [])
1798
+ const bad4 = (src4.match(/Members\/<[^>]+>\//g) || [])
1799
+ assert(!/Members\/<[^>]+>\//.test(src4), '* v4: NO member-facing declaration/code claim uses the Members/<x>/ shape (found ' + bad4.length + ')')
1800
+ assert(/Progress|<rId>|progress\.md/.test(src4), '* v4: the writer/reader bases are the project-root Progress/Propos/Methods/Subproblems')
1801
+ }
1802
+ // v3 (v2/v3 deep review D1): the writer `applyAgentWrites` accepts ONLY Problems|Progress|Propos|Methods|Notes
1803
+ // at the PROJECT ROOT, so a `Members/<x>/...` declaration is a write it SILENTLY DISCARDS. v3's member-facing
1804
+ // text must therefore stay root-relative - the opposite of v4/v5, which really do have a member subtree.
1805
+ const v3 = readFileSync(files.v3, 'utf8')
1806
+ assert(!/Members\/<[^>]+>\//.test(v3), '* v3: no member-facing declaration keeps the Members/<x>/ shape (the writer would discard that write)')
1807
+ const v3decl = [...v3.matchAll(/(?:^|[^/>])([A-Za-z]+)\/<[^>]+>\//g)].map((m) => m[1])
1808
+ const WL = ['Problems', 'Progress', 'Propos', 'Methods', 'Notes']
1809
+ const v3ok = v3decl.filter((x) => WL.indexOf(x) !== -1)
1810
+ assert(v3ok.length >= 1, '* v3: its declarations use the project-root-relative sections the writer accepts (found ' + v3ok.length + ' of ' + JSON.stringify(v3decl) + ')')
1811
+ // CLASS GUARD (covers v2/v3/v4/v5): whatever sections the persona/prompts tell an agent to write must be
1812
+ // accepted by that preset's writer - this is the invariant that would have caught both D1 and the v4/v5 cases.
1813
+ assert(/applyAgentWrites[\s\S]{0,600}?(Problems|Progress|Propos|Methods|Notes)/.test(v3), '* class guard: applyAgentWrites whitelists the sections the member-facing text names (v3)')
1814
+ const v2 = readFileSync(files.v2, 'utf8')
1815
+ assert(!/Members\/<[^>]+>\//.test(v2), '* v2: no Members/<x>/ shape either (anti-vacuity for the class, without over-claiming v2 has no path vocabulary)')
1816
+ assert(/PAPER_PATH_NOTE/.test(v2) && /\u4f1a\u8bdd cwd/.test(v2) && /\u7edd\u5bf9\u524d\u7f00/.test(v2), '* v2 uses the OTHER mechanism: one PAPER_PATH_NOTE stating the session-cwd + absolute-prefix rule')
1817
+ const uses = (v2.match(/PAPER_PATH_NOTE/g) || []).length
1818
+ // (D3 fixed the duplicate: the note no longer belongs in the evidence ENTRY list.) Assert the
1819
+ // STRUCTURE, not an occurrence count: defined once, appended by each member-facing material text,
1820
+ // and never embedded in a bare data entry.
1821
+ const noteDef = (v2.match(/const PAPER_PATH_NOTE/g) || []).length
1822
+ assert(noteDef === 1, '* v2 defines PAPER_PATH_NOTE exactly once (found ' + noteDef + ')')
1823
+ const noteLines = v2.split(/\r?\n/).filter((l) => l.includes('PAPER_PATH_NOTE'))
1824
+ const embeds = noteLines.filter((l) => !/const PAPER_PATH_NOTE/.test(l) && !/[+]|push\(/.test(l))
1825
+ assert(embeds.length === 0, '* v2 never embeds the prose rule in a bare data entry (' + JSON.stringify(embeds).slice(0, 80) + ')')
1826
+ const bodyOf = (src, fn) => {
1827
+ const a = src.indexOf('function ' + fn)
1828
+ if (a < 0) return null
1829
+ const b = src.slice(a + 10).search(/\n {2}(?:async )?function /)
1830
+ if (b < 0) return null // STRICT bound: no fixed-length fallback (window bleed lesson)
1831
+ return src.slice(a, a + 10 + b)
1832
+ }
1833
+ // RULING (B): the index is a DATA LIST (prose-free); the delivered member-facing material that carries
1834
+ // the paths is the digest, and it must append the note. (The other consumer, paperEmitArtifacts ->
1835
+ // paperBuildMarkdown, produces the FINAL PAPER ARTIFACT for humans, not a member prompt.)
1836
+ assert(!/PAPER_PATH_NOTE/.test(bodyOf(v2, 'paperEvidenceIndex') || ''), '* v2: the evidence-index DATA LIST embeds no prose rule')
1837
+ assert(/PAPER_PATH_NOTE/.test(bodyOf(v2, 'buildPaperDigest') || ''), '* v2: the delivered digest material appends PAPER_PATH_NOTE')
1838
+ assert(bodyOf(v2, 'buildPaperDigest') !== null && bodyOf(v2, 'paperEvidenceIndex') !== null, '* v2: both windows are bounded strictly (no fixed-length fallback)')
1839
+ }
1840
+ section('N7 delivered prompts carry no labelled-but-empty slot (proposer/agenda/target)')
1841
+ {
1842
+ const P = await establish()
1843
+ const txt = await P.prompts('verify', 'r-1', { target: 'p-nopowner', stage: 'independent' })
1844
+ assert(txt.length > 100 && /verifying object p-nopowner/.test(txt), '* the delivered verify prompt is non-trivial and names the target (non-vacuity: an empty prompt must not pass below)')
1845
+ assert(!/\u63d0\u51fa\u8005\s*\uff09/.test(txt), '* no labelled-but-empty proposer slot in the delivered frame (\u63d0\u51fa\u8005 + ) )')
1846
+ assert(!/agenda:\s*[)\uff09]/.test(txt), '* no labelled-but-empty agenda slot in the delivered frame')
1847
+ // protocol 搂9 (path-alias family): resolve the plugin ONCE from the loader's own URL, read THAT, and
1848
+ // assert the read path equals the V4_PLUGIN seam when it is set - a guard must never print one path
1849
+ // and read another.
1850
+ const _pu = pluginUrl() // the loader's own resolution (seam-aware)
1851
+ const _href = typeof _pu === 'string' ? _pu : String(_pu && _pu.href ? _pu.href : _pu)
1852
+ const _loaderPath = _href.indexOf('file:') === 0 ? fileURLToPath(new URL(_href.split('?')[0])) : String(_href).split('?')[0]
1853
+ if (process.env.V4_PLUGIN) {
1854
+ const _a = _loaderPath.replace(/\\/g, '/').toLowerCase()
1855
+ const _b = String(process.env.V4_PLUGIN).replace(/\\/g, '/').toLowerCase()
1856
+ assert(_a === _b, '* N10 reads the SAME file the loader imports (readPath === V4_PLUGIN seam): ' + _a + ' vs ' + _b)
1857
+ }
1858
+ assert(/String\(vs\.targetOwner\|\|''\)\.trim\(\)\?/.test(PLUGIN_SRC()), '* the proposer segment is assembled ONLY when a value exists (source-level: the office is never invented)')
1859
+ }
1860
+ section('N8 the meeting frame\'s relay claim matches reality (no promise of speeches that do not exist)')
1861
+ {
1862
+ const P = await establish()
1863
+ const noPrior = await P.prompts('meeting', 'r-1')
1864
+ assert(noPrior.length > 100 && /A meeting is in progress/.test(noPrior), '* the meeting frame is non-trivial (non-vacuity)')
1865
+ assert(!/\u4e0b\u9762\u5df2\u6709\u4eba\u53d1\u8a00/.test(noPrior), '* with NO prior statements the frame does not claim that others have spoken (\u4e0b\u9762\u5df2\u6709\u4eba\u53d1\u8a00)')
1866
+ assert(/\u5e76\u884c\u72ec\u7acb\u53d1\u8a00/.test(noPrior), '* …instead it states the real semantics: this round is parallel independent statements')
1867
+ assert(/\(prior\?'\\n\u8fd9\u662f\u4e00\u573a\u771f\u5b9e\u8ba8\u8bba/.test(PLUGIN_SRC()), '* the relay claim is gated on prior existing (source-level)')
1868
+ assert(/\(prior\?\(\'\\n\\n### \u5df2\u6709\u53d1\u8a00/.test(PLUGIN_SRC()), '* the forwarded-speech block is still delivered whenever prior exists')
1869
+ }
1870
+ section('N9 behavioural: a write to the DOCUMENTED path is counted by countArtifacts (auto-meeting basis)')
1871
+ {
1872
+ const P = await establish()
1873
+ const st0 = await P.callTool('vibe_v4_status', {})
1874
+ assert(typeof st0.artifactCount === 'number', '* status exposes artifactCount (the auto-meeting basis) - keys: ' + JSON.stringify(Object.keys(st0).slice(0, 14)))
1875
+ const rec = await P.callTool('vibe_v4_record_proposition', { id: 'p-docpath', statement: '文档路径行为证明', value: 0.5, motive: 'm', p: 0.5 }, P.resAgent(P.childOf('r-1')))
1876
+ assert(rec.ok === true, '* a member card is recorded through the tool (returned ' + JSON.stringify(rec).slice(0, 120) + ')')
1877
+ const st1 = await P.callTool('vibe_v4_status', {})
1878
+ assert(st1.artifactCount > st0.artifactCount, '* the card written to the DOCUMENTED path (Propos/<id>/<id>.md at the project root) IS counted by countArtifacts (' + st0.artifactCount + ' -> ' + st1.artifactCount + ')')
1879
+ const files = P.host && P.host.fsWrites ? P.host.fsWrites.map((w) => String(w.path || '')).join(',') : ''
1880
+ assert(files.indexOf('Members/') === -1, '* no write landed under Members/ (the shape nothing scans) - writes: ' + files.slice(0, 160))
1881
+ }
1882
+ section('N10 G-7 class guard: agent-facing compaction takes the AGENT-LOCAL instance')
1883
+ {
1884
+ const { readFileSync } = await import('node:fs')
1885
+ const { fileURLToPath } = await import('node:url')
1886
+ assert(/function compactionForAgent\(/.test(PLUGIN_SRC()), '* v4 defines compactionForAgent (the agent-local accessor)')
1887
+ // F2 (HIGH): a named, BOUNDED anchor. The old window ran to the next two-space function (109,191 chars)
1888
+ // and swallowed unrelated code, which is why a line-located mutant escaped unnoticed.
1889
+ const CALL = 'const compaction = compactionForAgent(agent)'
1890
+ const at = PLUGIN_SRC().indexOf(CALL)
1891
+ assert(at > 0, '* the agent-facing compaction call exists verbatim (anchor: ' + CALL + ')')
1892
+ const _ls = PLUGIN_SRC().lastIndexOf('\n', at)
1893
+ const _le = PLUGIN_SRC().indexOf('\n', at)
1894
+ const win = PLUGIN_SRC().slice(_ls, _le > 0 ? _le : at + 200)
1895
+ assert(win.length <= 400, '* the inspected window is BOUNDED (the call line only; got ' + win.length + ')')
1896
+ assert(win.indexOf(CALL) !== -1, '* the window really contains the agent-facing call')
1897
+ if (process.env.V4_DEBUG) { const _lp = (function(){ try { const u = pluginUrl(); const h = typeof u === 'string' ? u : String(u && u.href ? u.href : u); return h.indexOf('file:') === 0 ? fileURLToPath(new URL(h.split('?')[0])) : h.split('?')[0] } catch (e) { return 'ERR:' + e.message } })(); console.log(' [N10-diag] read=' + _lp + ' | seam=' + String(process.env.V4_PLUGIN || '(none)') + ' | srcHasAnchor=' + (PLUGIN_SRC().indexOf(CALL) !== -1) + ' | win=' + win.length) }
1898
+ assert(!/compactionOf\(\)/.test(win), '* the agent-facing path does NOT call the host-root compactionOf() (a realm never falls back)')
1899
+ }
1570
1900
  console.log('')
1901
+ section('N11 F-4c: a failed mkdir is NAMED exactly once (deferred/distorted diagnosis fixed)')
1902
+ {
1903
+ const h = await mount()
1904
+ const seen = []
1905
+ const realErr = console.error
1906
+ console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) } // protocol 搂6.4: capture AND still name a red
1907
+ shellMkdirExit = 1
1908
+ try {
1909
+ await h.callTool('vibe_v4_start', { problem: 'F-4c mkdir 失败可见', residentCount: 1 })
1910
+ const mk = shellCalls.filter(s => /New-Item|^mkdir -p /.test(String(s)))
1911
+ assert(mk.length >= 1, '* N11: the mkdir script was actually issued (shellCalls=' + shellCalls.length + ', mkdir=' + mk.length + ')')
1912
+ const warned = seen.filter(l => /ensureDirs: mkdir failed \(exit 1\) under/.test(l))
1913
+ assert(warned.length === 1, '* N11: the mkdir failure is NAMED exactly once (matched=' + warned.length + ' of ' + seen.length + ' captured stderr lines)')
1914
+ } finally { console.error = realErr; shellMkdirExit = 0 }
1915
+ }
1916
+
1917
+ section('N13 F1 invariant guards: membership predicates + roster-only ballots (source level)')
1918
+ {
1919
+ const srcF1 = PLUGIN_SRC()
1920
+ const bodyOfF1 = (name) => { const a = srcF1.indexOf('function ' + name + '('); if (a < 0) return null; let d = 0; for (let i = a; i < srcF1.length; i++) { const ch = srcF1[i]; if (ch === '{') d++; else if (ch === '}') { d--; if (d === 0) return srcF1.slice(a, i + 1) } } return null }
1921
+ const cvrF1 = bodyOfF1('continueVerifyRound'), fvF1 = bodyOfF1('finalizeVerify')
1922
+ assert(!!cvrF1 && !!fvF1, '* F1/1 both consensus functions are found (bounded, brace-balanced windows)')
1923
+ assert(cvrF1.length < 20000 && fvF1.length < 20000, '* F1/1 the inspected windows are BOUNDED (cvr=' + cvrF1.length + ', fv=' + fvF1.length + ') - a runaway window is itself a failure')
1924
+ const MEMF1 = 'ids.every(id=>vs.verdicts[id]!==undefined)'
1925
+ assert(cvrF1.indexOf(MEMF1) !== -1, '* F1/1 continueVerifyRound requires MEMBERSHIP of the frozen ids')
1926
+ assert(fvF1.indexOf(MEMF1) !== -1, '* F1/1 finalizeVerify uses the SAME membership predicate (the two views cannot disagree)')
1927
+ assert(!/Object\.keys\(vs\.verdicts\)\.length>=/.test(fvF1), '* F1/1 finalizeVerify does NOT use the count-only predicate (that is the F1 regression: a non-roster ballot could satisfy it)')
1928
+ assert(/const vals=ids\.filter\(id=>vs\.verdicts\[id\]!==undefined\)\.map\(id=>vs\.verdicts\[id\]\)/.test(fvF1), '* F1/1 finalizeVerify takes `vals` ONLY from the frozen ids (never Object.values of the live map)')
1929
+ assert(/_inRoster\s*=\s*!Array\.isArray\(verifyState\.rosterSnapshot\)\s*\|\|\s*verifyState\.rosterSnapshot\.indexOf\(r\.rId\)!==-1/.test(srcF1), '* F1/2 the ballot recording site is guarded by the FROZEN roster (verdicts keys are a subset of rosterSnapshot)')
1930
+ const gF1 = srcF1.indexOf('if(!_inRoster)'), aF1 = srcF1.indexOf('verifyState.verdicts[r.rId]={prob:p')
1931
+ assert(gF1 !== -1 && aF1 > gF1 && (aF1 - gF1) < 400, '* F1/2 the verdict assignment sits INSIDE that guard (distance=' + (aF1 - gF1) + ' chars)')
1932
+ }
1933
+
1934
+ section('N15 F-6 site 4: a failed compaction is NAMED exactly once (realCompact -> onResidentEnd)')
1935
+ {
1936
+ const h = await establish()
1937
+ const seen = []
1938
+ const realErr = console.error
1939
+ console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) }
1940
+ try {
1941
+ compactionFails = true
1942
+ // the turn end is only processed for a BUSY resident (`if(!busy.delete(r.rId)) return`), so
1943
+ // wake it first, exactly like the suite's workWake() helper does.
1944
+ const before = h.followups.length
1945
+ await h.callTool('vibe_v4_message', { to: 'r-1', content: '请继续推进。' })
1946
+ for (let i = 0; i < 300 && h.followups.length === before; i++) await sleep(10)
1947
+ const cid = h.followups[h.followups.length - 1].childId
1948
+ assert(!!cid, '* N15/4 the resident was woken so the turn end reaches onResidentEnd (childId=' + cid + ')')
1949
+ h.fireEnd(cid, { summary: '推进本轮。', solved: false, contextPct: 20 })
1950
+ for (let i = 0; i < 80 && !seen.some(l => /realCompact: the compaction call failed/.test(l)); i++) await sleep(10)
1951
+ const w = seen.filter(l => /realCompact: the compaction call failed/.test(l))
1952
+ assert(w.length === 1, '* N15/4 realCompact: a failed compaction is NAMED exactly once (matched=' + w.length + ' of ' + seen.length + ' stderr within 800ms)')
1953
+ assert(/NOT compacted/.test(w[0] || ''), '* N15/4 the warning STATES THE CONSEQUENCE (context pressure was NOT compacted)')
1954
+ } finally { console.error = realErr; compactionFails = false }
1955
+ }
1956
+
1957
+ section('N14 F2 compaction policy: policy verb only, re-anchor only after a REAL compaction (source level)')
1958
+ {
1959
+ const srcC = PLUGIN_SRC()
1960
+ const aC = srcC.indexOf('async function realCompact(')
1961
+ assert(aC > 0, '* F2/a realCompact is found through the seam')
1962
+ let dC = 0, eC = aC
1963
+ for (let i = aC; i < srcC.length; i++) { const ch = srcC[i]; if (ch === '{') dC++; else if (ch === '}') { dC--; if (dC === 0) { eC = i; break } } }
1964
+ const bodyC = srcC.slice(aC, eC + 1)
1965
+ assert(bodyC.length > 100 && bodyC.length < 4000, '* F2/a the inspected window is BOUNDED (' + bodyC.length + ' chars) - a runaway window is itself a failure')
1966
+ assert(bodyC.indexOf('compactIfNeeded(agent, ') !== -1, '* F2/a realCompact calls the POLICY verb compactIfNeeded (threshold-driven)')
1967
+ assert(bodyC.indexOf('compactNow(') === -1, '* F2/a realCompact contains NO compactNow( call: a host offering the forcing verb must NOT be forced (user ruling)')
1968
+ assert(!/const force\s*=/.test(bodyC), '* F2/a the capability-based forcing branch is GONE, not merely deprioritised')
1969
+ assert(/const didCompact\s*=/.test(bodyC), '* F2/a the re-anchor is gated on didCompact (evidence that the policy call ACTUALLY compacted)')
1970
+ assert(/if\(didCompact\)\{/.test(bodyC), '* F2/a and the gate is the if(didCompact){ branch (mutant: if(true){ => this reddens)')
1971
+ }
1972
+
1973
+ section('N17 F-5 fence-root drift: containment, no-op boundary, one-shot naming both roots')
1974
+ {
1975
+ const h = await establish()
1976
+ const seen = []
1977
+ const realErr = console.error
1978
+ console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) }
1979
+ const drift = () => seen.filter(l => /fence root does not CONTAIN this session workspace/.test(l))
1980
+ // NOTE: case (0) is 'no sandboxPolicy SERVICE'; case 1b is 'service present, root uninterpretable'.
1981
+ const ws = () => String(h.WS).replace(/\\/g, '/')
1982
+ try {
1983
+ const r1 = await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 1' }, h.resAgent(h.childOf('r-1')))
1984
+ await sleep(50)
1985
+ assert(drift().length === 0, '* F-5/i NO sandboxPolicy SERVICE at all => the policy path never runs and NO warning appears (drift=' + drift().length + '; the uninterpretable-root no-op is case 1b)')
1986
+ assert(r1 && r1.ok !== false, '* F-5/i and getPolicy() still returns a usable policy (the write went through)')
1987
+ // F-5/1b: a policy SERVICE that resolves to a shape with NO interpretable root ({allow,deny}) -
1988
+ // the helper IS invoked and must stay silent (uninterpretable => no-op, today's behaviour).
1989
+ seen.length = 0 // F-5: observe ONLY this case's window (no cascade from earlier cases)
1990
+ sandboxNoRoot = true
1991
+ const r1b = await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 1b' }, h.resAgent(h.childOf('r-1')))
1992
+ await sleep(50)
1993
+ assert(drift().length === 0, '* F-5/1b an UNINTERPRETABLE resolved policy ({allow,deny}) produces NO warning (documented no-op, not a dead branch) (' + drift().length + ')')
1994
+ const w1b = await h.callTool('vibe_v4_status', {})
1995
+ assert(r1b && r1b.ok !== false && !!w1b, '* F-5/1b getPolicy() still RESOLVES for that shape (the write succeeds, no throw path)')
1996
+ // F-5/1c: the session cwd is UNKNOWN (no cwd on the captured root agent) and the policy exposes no
1997
+ // top-level root, so workspaceRoot() is `'.'`; resolve({session}) still yields a root. The `.` clause
1998
+ // of the product guard is the ONLY reason this stays silent - a mutant that warns on an unknown cwd
1999
+ // reddens here by name.
2000
+ sandboxNoRoot = false
2001
+ sandboxDriftRoot = null
2002
+ sandboxUnknownCwd = true
2003
+ const seenC = []
2004
+ const realErrC = console.error
2005
+ console.error = (...a) => { seenC.push(a.map(String).join(' ')); realErrC(...a) }
2006
+ let hc = null
2007
+ try {
2008
+ // FOUNDING under an unknown cwd: the founding writes go through writeTextAbs -> getPolicy(),
2009
+ // so the window below both reaches the helper and proves it stayed silent
2010
+ hc = await mount()
2011
+ hc.resetResolveCalls()
2012
+ await hc.callTool('vibe_v4_start', { problem: 'F-5 case 1c', residentCount: 1 })
2013
+ await sleep(80)
2014
+ } finally { console.error = realErrC; sandboxUnknownCwd = false }
2015
+ const driftC = seenC.filter(l => /fence root does not CONTAIN this session workspace/.test(l))
2016
+ assert(driftC.length === 0, '* F-5/1c an UNKNOWN session cwd stays SILENT (the `.` guard: drift=' + driftC.length + ')')
2017
+ assert(hc.resolveCalls() >= 1, '* F-5/1c LIVENESS: the helper really ran with an unknown cwd (resolve calls=' + hc.resolveCalls() + ', policy queries=' + hc.policyQueries() + '), so the silence is not an unreached branch')
2018
+ sandboxNoRoot = false
2019
+ // (iv) agreeing roots => silence (anti-vacuity)
2020
+ seen.length = 0 // F-5: observe ONLY this case's window
2021
+ sandboxDriftRoot = ws()
2022
+ await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 4' }, h.resAgent(h.childOf('r-1')))
2023
+ await sleep(50)
2024
+ assert(drift().length === 0, '* F-5/iv an AGREEING resolved root produces NO warning (not vacuous)')
2025
+ // (iii) containment: an ANCESTOR root is legitimate => silence
2026
+ seen.length = 0 // F-5: observe ONLY this case's window
2027
+ sandboxDriftRoot = ws().replace(/\/[^/]+$/, '')
2028
+ await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 3' }, h.resAgent(h.childOf('r-1')))
2029
+ await sleep(50)
2030
+ assert(drift().length === 0, '* F-5/iii a root that CONTAINS the session workspace is accepted (no per-session false alarm)')
2031
+ // (ii) real drift => exactly one named warning naming BOTH sides
2032
+ seen.length = 0 // F-5: observe ONLY this case's window
2033
+ sandboxDriftRoot = ws() + '/somewhere-else'
2034
+ await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 2' }, h.resAgent(h.childOf('r-1')))
2035
+ await sleep(50)
2036
+ // a SECOND drift-triggering resolution: the one-shot must keep the warning at exactly one
2037
+ await h.callTool('vibe_v4_publish_progress', { content: 'F-5 case 2b' }, h.resAgent(h.childOf('r-1')))
2038
+ await sleep(50)
2039
+ const w = drift()
2040
+ assert(w.length === 1, '* F-5/ii the drift is NAMED exactly once (matched=' + w.length + ' of ' + seen.length + ' stderr lines)')
2041
+ assert(/resolved=/.test(w[0] || '') && /session=/.test(w[0] || ''), '* F-5/ii the warning names BOTH the resolved root and the session workspace')
2042
+ } finally { console.error = realErr; sandboxDriftRoot = null; sandboxNoRoot = false }
2043
+ }
2044
+
2045
+
2046
+ section('N18 F-6: a failed current-project marker write is NAMED exactly once')
2047
+ {
2048
+ // PRE-START: configure() refuses while a run is live and returns BEFORE writeCurrentProject
2049
+ // (measured with establish(): the injection never fired), so this section mounts without starting.
2050
+ const h = await mount()
2051
+ const seen = []
2052
+ const realErr = console.error
2053
+ console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) }
2054
+ const hits = () => seen.filter((l) => /current-project marker was NOT persisted/.test(l))
2055
+ try {
2056
+ // ONE injected failure, only for the `.current` marker write (path-anchored, existing hook)
2057
+ fsWriteFailPred = (p) => /[\\/]\.current$/.test(String(p))
2058
+ fsWriteFailOnce = true
2059
+ await h.callTool('vibe_v4_configure', { project: 'n18-proj' })
2060
+ await sleep(50)
2061
+ assert(hits().length === 1, '* N18 the failed `.current` write is NAMED exactly once (matched=' + hits().length + ' of ' + seen.length + ' stderr lines)')
2062
+ assert(/next session may load the previous project/.test(hits()[0] || ''), '* N18 the warning STATES THE CONSEQUENCE (the next session may load the previous project)')
2063
+ // anti-vacuity: a SUCCESSFUL marker write stays silent
2064
+ fsWriteFailPred = null
2065
+ fsWriteFailOnce = false
2066
+ seen.length = 0
2067
+ await h.callTool('vibe_v4_configure', { project: 'n18-proj-2' })
2068
+ await sleep(50)
2069
+ assert(hits().length === 0, '* N18 a SUCCESSFUL marker write is silent (the assertion is not vacuous)')
2070
+ } finally { console.error = realErr; fsWriteFailPred = null; fsWriteFailOnce = false }
2071
+ }
2072
+
2073
+
2074
+ section('N20-3b rebuildLeanLibIndexes: a failed INDEX WRITE is NAMED once (covers all six callers)')
2075
+ {
2076
+ // ③ callsite enumeration (measured at this revision) and verdicts:
2077
+ // · settleLeanJob (:893) - own try/catch naming (in-suite capture = measured boundary)
2078
+ // · writeLeanLibIndexesSafe (:1006) - boundary: cold-recovery convenience, rebuilt again later
2079
+ // · leanArchive def/proof/blocked (:1394/:1438/:1452) - silent because the FUNCTION swallowed
2080
+ // · the lean_lib tool (:4139) - silent for the same reason (measured: ok:true + counts)
2081
+ // => ONE fix inside the function covers all six; this asserts the naming.
2082
+ const h = await mount()
2083
+ const seen = []
2084
+ const realErr = console.error
2085
+ console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) }
2086
+ const hits = () => seen.filter(l => /rebuildLeanLibIndexes: the Lean library index could NOT be written/.test(l))
2087
+ try {
2088
+ // (a) anti-vacuity: a healthy rebuild is SILENT
2089
+ await h.callTool('vibe_v4_lean_lib', {})
2090
+ await sleep(30)
2091
+ assert(hits().length === 0, '* N20-3b a healthy index rebuild is silent (the case is not vacuous)')
2092
+ // (b) one injected index-write failure => exactly one NAMED warning WITH its consequence
2093
+ fsWriteFailPred = (p) => /(?:Lib|Proved)[\\/]Index\.md$/i.test(String(p))
2094
+ fsWriteFailOnce = true
2095
+ const r = await h.callTool('vibe_v4_lean_lib', {})
2096
+ await sleep(30)
2097
+ assert(hits().length === 1, '* N20-3b a failed index write is NAMED exactly once (matched=' + hits().length + ' of ' + seen.length + ' stderr lines)')
2098
+ assert(/STALE until the next successful rebuild/.test(hits()[0] || ''), '* N20-3b the warning STATES THE CONSEQUENCE (the index is stale until the next successful rebuild)')
2099
+ assert(!!r && r.ok === true, '* N20-3b and the tool still answers with the scanned counts (the naming is additive, not a new failure mode)')
2100
+ } finally { console.error = realErr; fsWriteFailPred = null; fsWriteFailOnce = false }
2101
+ }
2102
+
1571
2103
  console.log('passed=' + passed + ' failed=' + failed)
1572
2104
  if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
1573
2105
  console.log('ALL GREEN')