dsh-vibe-math 2.7.2 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -681
  3. package/README.md +683 -682
  4. package/cordis.patch.yml +112 -16
  5. package/docs/AUDIT-CHECKLIST.md +755 -518
  6. package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
  7. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  8. package/docs/audit-methodology.md +93 -0
  9. package/docs/final-paper.md +172 -172
  10. package/docs/formal-verification.md +613 -588
  11. package/docs/generate_framework_diagram_v2.py +114 -114
  12. package/docs/generate_framework_diagram_v3.py +127 -127
  13. package/docs/math-computation.md +195 -187
  14. package/docs/parameter-schema.md +37 -0
  15. package/docs/release-notes/README.en.md +2 -0
  16. package/docs/release-notes/README.md +2 -0
  17. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  18. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  19. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  20. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  21. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  22. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  23. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  24. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  25. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  26. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  27. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  28. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  29. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  30. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  31. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  32. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  33. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  34. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  35. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  36. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  37. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  38. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  39. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  40. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  41. package/docs/release-notes/TEMPLATE.md +6 -0
  42. package/docs/release-playbook.md +103 -0
  43. package/docs/slv-playbook.md +117 -0
  44. package/docs/status-report-fields.md +167 -0
  45. package/docs/test-timing.md +158 -154
  46. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  47. package/installer.js +793 -783
  48. package/package.json +260 -203
  49. package/prompt-corpus-persona/persona-corpus.json +9 -9
  50. package/prompt-corpus-persona/persona-corpus.md +225 -33
  51. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  52. package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
  53. package/prompt-corpus-v3/formal-verify-v3.json +112 -112
  54. package/prompt-corpus-v3/formal-verify-v3.md +227 -143
  55. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  56. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  57. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  58. package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
  59. package/scripts/clean-temp.mjs +234 -0
  60. package/scripts/update-doc-counts.mjs +71 -0
  61. package/tests/audit-artifact-docs.mjs +72 -0
  62. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  63. package/tests/audit-engine-faces.mjs +298 -0
  64. package/tests/audit-formal-sensitivity.mjs +345 -342
  65. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  66. package/tests/audit-installer-compat.test.mjs +256 -208
  67. package/tests/audit-installer-policy.test.mjs +41 -8
  68. package/tests/audit-math-computation-contract.mjs +64 -8
  69. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  70. package/tests/audit-math-computation-parity.mjs +164 -3
  71. package/tests/audit-math-computation-sensitivity.mjs +5 -3
  72. package/tests/audit-package-membership.mjs +57 -0
  73. package/tests/audit-package-membership.mutants.mjs +28 -0
  74. package/tests/audit-participant-set-parity.mjs +160 -0
  75. package/tests/audit-path-discipline.mjs +83 -0
  76. package/tests/audit-path-discipline.mutants.mjs +51 -0
  77. package/tests/audit-persona-sensitivity.mjs +323 -309
  78. package/tests/audit-persona-surface.mutants.mjs +69 -0
  79. package/tests/audit-persona-surface.test.mjs +431 -390
  80. package/tests/audit-prompt-duplication.mjs +143 -0
  81. package/tests/audit-prompt-duplication.mutants.mjs +51 -0
  82. package/tests/audit-prompt-invariants.mjs +589 -508
  83. package/tests/audit-readme-bilingual.test.mjs +152 -0
  84. package/tests/audit-readme-counts.mjs +95 -0
  85. package/tests/audit-readme-counts.mutants.mjs +52 -0
  86. package/tests/audit-registration.mjs +78 -0
  87. package/tests/audit-spec-traceability.mjs +193 -193
  88. package/tests/audit-status-report-fields.mjs +172 -0
  89. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  90. package/tests/audit-v3-registration-parity.mjs +156 -0
  91. package/tests/audit-v5-integrity.mjs +795 -508
  92. package/tests/audit-v5-lean-abstention.mjs +392 -0
  93. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  94. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  95. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  96. package/tests/audit-v5-sensitivity.mjs +433 -427
  97. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  98. package/tests/e2e-v5-round2.test.mjs +2453 -1981
  99. package/tests/formal-verify-v2.test.mjs +1778 -1724
  100. package/tests/formal-verify-v3.mutants.mjs +117 -0
  101. package/tests/formal-verify-v3.test.mjs +1724 -1671
  102. package/tests/formal-verify-v4.mutants.mjs +152 -0
  103. package/tests/formal-verify-v4.test.mjs +2114 -1574
  104. package/tests/formal-verify-v5.test.mjs +790 -724
  105. package/tests/helpers/doc-counts-format.mjs +41 -0
  106. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  107. package/tests/math-computation-a1.mutants.mjs +72 -0
  108. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  109. package/tests/math-computation-discovery.mutants.mjs +76 -0
  110. package/tests/math-computation-shared.test.mjs +392 -6
  111. package/tests/math-computation-v2.test.mjs +779 -0
  112. package/tests/math-computation-v3.test.mjs +771 -0
  113. package/tests/math-computation-v4.mutants.mjs +133 -0
  114. package/tests/math-computation-v4.test.mjs +739 -0
  115. package/tests/prompt-v5-integrity.test.mjs +1291 -1280
  116. package/tests/run-tests.mjs +393 -202
  117. package/tests/run-tests.mutants.mjs +70 -0
  118. package/tests/selfdrive-v5.mjs +560 -508
  119. package/tests/selfdrive-v5.mutants.mjs +132 -0
  120. package/tests/temp-hygiene.mutants.mjs +80 -0
  121. package/tests/v2-fix-probes.mutants.mjs +104 -0
  122. package/tests/v2-fix-probes.test.mjs +1040 -0
  123. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  124. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  125. package/tests/v3-fix-probes.mutants.mjs +99 -0
  126. package/tests/v3-fix-probes.test.mjs +892 -0
  127. package/tests/v4-final-paper.mutants.mjs +108 -0
  128. package/tests/v5-institute-fixes.mutants.mjs +218 -0
  129. package/vibe-math-v2/agent.cordis.yml +378 -322
  130. package/vibe-math-v2/math-computation.js +144 -53
  131. package/vibe-math-v2/math-engines.js +12 -12
  132. package/vibe-math-v2/preset.yml +2 -2
  133. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  134. package/vibe-math-v2/vibe-math-v2.js +5097 -4755
  135. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
  136. package/vibe-math-v3/agent.cordis.yml +422 -356
  137. package/vibe-math-v3/math-computation.js +144 -53
  138. package/vibe-math-v3/math-engines.js +12 -12
  139. package/vibe-math-v3/preset.yml +2 -2
  140. package/vibe-math-v3/vibe-math-v3.js +6235 -5873
  141. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  142. package/vibe-math-v4/agent.cordis.yml +438 -402
  143. package/vibe-math-v4/math-computation.js +144 -53
  144. package/vibe-math-v4/math-engines.js +12 -12
  145. package/vibe-math-v4/preset.yml +2 -2
  146. package/vibe-math-v4/vibe-math-v4.js +4490 -4269
  147. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
  148. package/vibe-math-v5/agent.cordis.yml +46 -12
  149. package/vibe-math-v5/math-computation.js +144 -53
  150. package/vibe-math-v5/math-engines.js +12 -12
  151. package/vibe-math-v5/vibe-math-v5.js +1266 -229
  152. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
  153. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -1,427 +1,433 @@
1
- // ============================================================
2
- // V5 SENSITIVITY PROBES — prove the suites are not vacuous.
3
- //
4
- // Each probe copies vibe-math-v5.js, applies ONE targeted mutation that breaks a
5
- // specific guarantee, and runs the referencing suite against the mutated copy. A probe
6
- // PASSES when the suite goes RED (non-zero exit) — i.e. the assertions really do detect
7
- // that break. A probe that stays green means the suite has a blind spot.
8
- //
9
- // The prompt-integrity block below exists because the 2026-09 field test found a bug
10
- // that 123 tool-level assertions could not see: every member's brief named the WRONG
11
- // member. Any guarantee about the TEXT a member reads must have a probe that proves
12
- // prompt-v5-integrity.test.mjs detects its violation.
13
- //
14
- // Run: node tests/audit-v5-sensitivity.mjs
15
- // ============================================================
16
- import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
17
- import { tmpdir } from 'node:os'
18
- import { join } from 'node:path'
19
- import { spawnSync } from 'node:child_process'
20
- import { fileURLToPath } from 'node:url'
21
-
22
- const SRC = new URL('../vibe-math-v5/vibe-math-v5.js', import.meta.url)
23
- // Keyed by the EXACT suite filename a probe names in its `ref`. A ternary chain here once
24
- // silently routed every new probe to the wrong suite (which then stayed green because the
25
- // mutation did not touch what it tests) — that reports a blind spot that does not exist.
26
- const TESTS = {}
27
- for (const f of ['selfdrive-v5.mjs', 'e2e-v5-round2.test.mjs', 'prompt-v5-integrity.test.mjs', 'formal-verify-v5.test.mjs']) {
28
- TESTS[f] = fileURLToPath(new URL('./' + f, import.meta.url))
29
- }
30
- const original = readFileSync(SRC, 'utf8')
31
- const REPO = fileURLToPath(new URL('..', import.meta.url))
32
- const dir = mkdtempSync(join(tmpdir(), 'v5-sens-'))
33
-
34
- // `armNextVerify`'s two exclusion gates (a live MEETING and a live VERIFICATION), each written
35
- // at the function head AND as a re-check inside the `while` loop with no `await` in between.
36
- // `armGuards(true, true)` is the pristine source text; the probes switch ONE gate off in BOTH
37
- // copies, which is the minimal edit that actually breaks the guarantee. Keeping the block in
38
- // one place means the two mutations cannot drift apart from the source they mutate.
39
- const armGuards = (meetingOn, verifyOn) => [
40
- ...(meetingOn ? [' if (meeting) return'] : []),
41
- ' // Only one begin may be in flight. Without this, two callers (a scheduling pass',
42
- ' // and a fresh proposal) could both pass the `currentVerify()` check before either',
43
- ' // has published its verdict record and would start the SAME object twice.',
44
- ' if (beginLock) return',
45
- ...(verifyOn ? [' if (currentVerify()) return'] : []),
46
- ' beginLock = true',
47
- ' try {',
48
- ' while (true) {',
49
- ' // NEVER remove a proposal we are not about to run: the entry leaves the durable',
50
- ' // queue only in the same step that starts it. The old code shifted it out and then',
51
- ' // returned because a meeting was live, so the proposal existed in neither the queue',
52
- ' // nor `verdicts` until some later pass happened to re-arm it (audit M2). Re-check',
53
- ' // the exclusion guards here too: `await putQueue` can let a meeting open.',
54
- ' const q = inst().queue.slice()',
55
- ' if (!q.length) return',
56
- ' if (' + (meetingOn && verifyOn ? 'meeting || currentVerify()' : meetingOn ? 'meeting' : 'currentVerify()') + ') return',
57
- ].join('\n')
58
-
59
- // Each probe: { name, ref, guarantee, from, to }
60
- // `from` must occur exactly once, so a mutation can never quietly hit the wrong site.
61
- const probes = [
62
- {
63
- // The m floor is enforced by THREE cooperating checks (an early `bTrue + bFalse < m`
64
- // return, the conflict check, and a final `bTrue >= m`), so a single weakened check is
65
- // masked by the others and is a SEMANTICALLY INERT mutation — it must never be used as
66
- // a probe (it would look like a "blind spot"). Forcing the quorum itself to 1 breaks
67
- // the rule on every path at once, which is exactly the guarantee under test.
68
- name: 'quorum-forced-to-one',
69
- ref: 'selfdrive-v5.mjs',
70
- guarantee: '④ only >= m boolean votes may verify (m must be min(quorumCap, |voters|))',
71
- // The floor moved into `quorumMFrom(vc)` when the quorum became single-sourced (defect B of
72
- // the 3rd self-test); the anchor follows the implementation, it is not relaxed.
73
- from: " return Math.max(1, Math.min(cap, vc))",
74
- to: " return 1",
75
- },
76
- {
77
- name: 'abstention-counts-as-true',
78
- ref: 'selfdrive-v5.mjs',
79
- guarantee: '⑤ an abstention must NOT count toward the quorum',
80
- from: " else if (p === 0) bFalse += 1\n else abstain += 1",
81
- to: " else if (p === 0) bFalse += 1\n else { abstain += 1; bTrue += 1 }",
82
- },
83
- {
84
- // `judgeVerdict` checks the m floor, then the conflict, then the floor again, and each
85
- // guard masks the next — so deleting a guard is inert. The behaviour-changing break is
86
- // letting a CONFLICT pass as a verdict, which is what this mutation does.
87
- name: 'conflict-allowed',
88
- ref: 'selfdrive-v5.mjs',
89
- guarantee: '④ a conflicting 1 vs 0 must BLOCK the verdict',
90
- from: " return Object.assign(base, { outcome: 'undecided', reason: 'conflicting assertions (true=' + bTrue + ', false=' + bFalse + ')' })",
91
- to: " return Object.assign(base, { outcome: 'true', reason: 'conflicting assertions (true=' + bTrue + ', false=' + bFalse + ')' })",
92
- },
93
- {
94
- name: 'temp-worker-can-vote',
95
- ref: 'selfdrive-v5.mjs',
96
- guarantee: '⑦ a temp worker must NOT be able to vote',
97
- from: " if (member.kind === 'temp') {\n // Temp workers have no vote",
98
- to: " if (false) {\n // Temp workers have no vote",
99
- },
100
- {
101
- name: 'academician-gate-removed',
102
- ref: 'selfdrive-v5.mjs',
103
- guarantee: '⑬ only the academician may assign tasks',
104
- from: " return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can assign tasks' }",
105
- to: " return { ok: false, code: 'NO_GATE', message: 'x' }",
106
- },
107
- {
108
- name: 'spawn-not-registered-inflight',
109
- ref: 'selfdrive-v5.mjs',
110
- guarantee: '⑯ the founding turn must be registered in-flight or its end is dropped',
111
- from: " inflight.set(started.childId, shortId())",
112
- to: "",
113
- },
114
- {
115
- name: 'group-chat-not-fanned-out',
116
- ref: 'selfdrive-v5.mjs',
117
- guarantee: '③ group chat must reach every other member',
118
- from: " if (to === 'all' || to === '') targets = activeMembers().filter((m) => m.id !== from)",
119
- to: " if (to === 'all' || to === '') targets = activeMembers().filter((m) => m.id !== from).slice(0, 1)",
120
- },
121
- {
122
- name: 'verify-round-advances-anyway',
123
- ref: 'selfdrive-v5.mjs',
124
- guarantee: '④ a round must not advance without every voter answering',
125
- from: " const missing = need.filter((id) => !vs.votes[id])\n if (missing.length) {",
126
- to: " const missing = need.filter((id) => !vs.votes[id])\n if (false) {",
127
- },
128
- {
129
- name: 'fire-does-not-release',
130
- ref: 'selfdrive-v5.mjs',
131
- guarantee: '⑨ firing must really release the resident child',
132
- from: " if (typeof subagents.drainContinuableChildren === 'function') await subagents.drainContinuableChildren(rootAgent, [target.childId])",
133
- to: " if (false) await subagents.drainContinuableChildren(rootAgent, [target.childId])",
134
- },
135
- {
136
- name: 'solve-vote-not-unanimous',
137
- ref: 'selfdrive-v5.mjs',
138
- guarantee: '⑥ the institute must not stop unless EVERY voter agrees',
139
- from: " if (!vs.every((id) => solveVotes.get(id) === true)) return false",
140
- to: " if (false) return false",
141
- },
142
- // ── probes added after the round-2 audit found these defects ──────────────
143
- {
144
- name: 'fallback-state-never-loaded',
145
- ref: 'e2e-v5-round2.test.mjs',
146
- guarantee: '⑰ the file fallback must LOAD persisted state before any read (restart safety)',
147
- from: " if (backend.kind === 'file' && typeof backend.load === 'function') await backend.load()",
148
- to: " if (false) await backend.load()",
149
- },
150
- {
151
- name: 'solve-vote-never-re-evaluated',
152
- ref: 'prompt-v5-integrity.test.mjs',
153
- guarantee: '⑥ a solve vote landing OUTSIDE a meeting must still stop the institute',
154
- // The anchor must include the CALL. An `if (false)` inserted before the comment would
155
- // leave the real `await checkSolved()` below it untouched — an inert mutation that
156
- // would masquerade as a blind spot.
157
- from: " solveVotes.set(memberId, val === true)\n // Evaluate the stop condition on EVERY solve vote, not only when a meeting\n // finalizes. A vote that lands after the meeting closed — a late reply, or an\n // ordinary round carrying vote_solved — would otherwise be recorded and never\n // read, leaving a unanimously-concluded institute running forever.\n await checkSolved()",
158
- to: " solveVotes.set(memberId, val === true)",
159
- },
160
- {
161
- name: 'proposal-only-kicks-scheduler',
162
- ref: 'selfdrive-v5.mjs',
163
- guarantee: '④ a proposal must actually START, not just ask the scheduler to try',
164
- from: " await armNextVerify()\n if (!hasVerifyInFlight()) await scheduleNext()",
165
- to: " await scheduleNext()",
166
- },
167
- {
168
- name: 'begin-not-exclusive',
169
- ref: 'prompt-v5-integrity.test.mjs',
170
- guarantee: '⑱ a second proposal must QUEUE, never start a concurrent ballot',
171
- from: armGuards(true, true),
172
- to: armGuards(true, false),
173
- },
174
- // NOTE — the mutual-exclusion gates of `armNextVerify` are written TWICE: once at the
175
- // function head and once as a re-check inside the `while (true)` loop, and there is NO
176
- // `await` between them. The head copies are therefore redundant mirrors of the in-loop
177
- // ones, and a probe that deletes only a head copy is SEMANTICALLY INERT — it can never go
178
- // red, which would masquerade as a "blind spot" forever (this was the state of both of the
179
- // ⑱/㉝ probes: the in-loop re-check kept the invariant alive and
180
- // `prompt-v5-integrity.test.mjs` stayed green). The probes for ⑱ and ㉝ therefore remove
181
- // BOTH copies of the ONE gate they test, which is the minimal edit that actually lets a
182
- // second ballot start — and the suite's case 9b ("a second proposal QUEUES") and case 10b
183
- // ("a verification proposed DURING a meeting must queue") do detect exactly that.
184
- // NOTE — a probe for `continueMeetingRound`'s `if (finalizeLock) { armHeartbeat(); return }`
185
- // re-arm was REMOVED, not because the re-arm is unnecessary but because the state it
186
- // guards is UNREACHABLE, so no black-box probe can detect its removal:
187
- // · `schedulePass` handles a live meeting BEFORE it looks at a verification, and
188
- // · `startMeeting` parks any meeting while `hasVerifyInFlight()`,
189
- // so a meeting and a running verify settlement can never overlap. The re-arm stays in
190
- // the code as cheap insurance against a future ordering change; leaving a probe that can
191
- // never go red would be a false "detection" and is worse than no probe.
192
- {
193
- name: 'meeting-never-finalized',
194
- ref: 'prompt-v5-integrity.test.mjs',
195
- guarantee: '⑲ a meeting that collected every input must actually finalize and write its minutes',
196
- from: " finalizeLock = 'meeting'\n try {\n await finalizeMeeting(meeting)",
197
- to: " finalizeLock = 'meeting'\n try {\n meeting = null",
198
- },
199
-
200
- // ── PROMPT / INTERACTION INTEGRITY PROBES ────────────────────────────────
201
- // These are the probes that would have caught the 2026-09 field-test bug. Every one
202
- // of them breaks something a member READS (identity, roster, framing, persona), and
203
- // prompt-v5-integrity.test.mjs must go RED for each.
204
- {
205
- name: 'brief-names-the-last-woken-member',
206
- ref: 'prompt-v5-integrity.test.mjs',
207
- guarantee: '⑳ a prompt\'s [状态] block must name the member it is sent to (the exact field bug: briefs named the previous member)',
208
- from: " function stateBlock(member) {\n return briefBlock(member)\n }",
209
- to: " function stateBlock(member) {\n return briefBlock(memberById(currentMember) || activeMembers()[0] || member)\n }",
210
- },
211
- {
212
- name: 'joiner-absent-from-own-roster',
213
- ref: 'prompt-v5-integrity.test.mjs',
214
- guarantee: '⑳ a founding brief must show the roster INCLUDING its reader (roster committed before the prompt is built)',
215
- from: " member.phase = 'active'\n member.childId = ''\n await putMember(member)",
216
- to: " member.childId = ''\n await putMember(member)",
217
- },
218
- {
219
- name: 'inbox-message-delivered-twice',
220
- ref: 'prompt-v5-integrity.test.mjs',
221
- guarantee: '㉑ one prompt must not deliver the same message twice',
222
- from: " if (pending.length) await ackPending(pending)\n const base = typeof baseFn === 'function' ? baseFn() : baseFn",
223
- to: " const base = typeof baseFn === 'function' ? baseFn() : baseFn\n if (pending.length) await ackPending(pending)",
224
- },
225
- {
226
- name: 'charter-rewritten-on-resume',
227
- ref: 'prompt-v5-integrity.test.mjs',
228
- guarantee: '㉒ the charter is frozen at hire; a resume must not rewrite the induction snapshot',
229
- from: " const persona = member.persona || memberPersona(member)",
230
- to: " const persona = memberPersona(member)",
231
- },
232
- {
233
- name: 'resume-framed-as-induction',
234
- ref: 'prompt-v5-integrity.test.mjs',
235
- guarantee: '㉓ a rebuilt session must not be told it just joined the institute',
236
- from: " const resume = mode === 'resume'",
237
- to: " const resume = false",
238
- },
239
- {
240
- name: 'meeting-proposal-misattributed',
241
- ref: 'prompt-v5-integrity.test.mjs',
242
- guarantee: '㉔ a relayed message must carry its TRUE sender, never the last-woken member',
243
- from: " await say(callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })",
244
- to: " await say(academicianId() || callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })",
245
- },
246
- {
247
- name: 'office-impersonates-a-member',
248
- ref: 'prompt-v5-integrity.test.mjs',
249
- guarantee: '㉕ the office/host caller must resolve to the office, not to a guessed member',
250
- // The FAITHFUL regression mutation: restore the old "answer with whoever this session
251
- // woke last" fallback, which made the office's own assignments resolve to a random
252
- // researcher and be refused as V5_NOT_ACADEMICIAN.
253
- from: " try { if (rootOf(agent) === agent) return 'office' } catch (e) { /* fall through */ }",
254
- to: " if (currentMember && memberById(currentMember)) return currentMember",
255
- },
256
- {
257
- name: 'office-assignment-framed-as-academician',
258
- ref: 'prompt-v5-integrity.test.mjs',
259
- guarantee: '㉖ an assignment must be framed by its true origin (office vs academician)',
260
- from: " if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text",
261
- to: " if (m.kind === 'assign') return '【院士分派】' + m.text",
262
- },
263
- {
264
- name: 'nudge-mislabelled-as-assignment',
265
- ref: 'prompt-v5-integrity.test.mjs',
266
- guarantee: '㉗ a nudge is supervision, not an assignment, and must say so',
267
- from: " to, kind: 'nudge',",
268
- to: " to, kind: 'assign',",
269
- },
270
- {
271
- name: 'framework-notice-sent-as-self-message',
272
- ref: 'prompt-v5-integrity.test.mjs',
273
- guarantee: '㉘ framework feedback must actually reach the member (not be refused as a self-message)',
274
- from: " return await say('framework', { to: memberId, kind: 'notice', text: String(text) })",
275
- to: " return await say(memberId, { to: memberId, kind: 'notice', text: String(text) })",
276
- },
277
- {
278
- name: 'failed-member-hidden',
279
- ref: 'prompt-v5-integrity.test.mjs',
280
- guarantee: '㉙ a member that failed to provision must be visible in [未就位]',
281
- from: " if (absent.length) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))",
282
- to: " if (false) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))",
283
- },
284
- {
285
- name: 'leaderless-charter-invents-a-leader',
286
- ref: 'prompt-v5-integrity.test.mjs',
287
- guarantee: '㉚ with academician:false no charter may name a leader who does not exist',
288
- from: " const a = academicianId()\n const L = [",
289
- to: " const a = 'acad'\n const L = [",
290
- },
291
- {
292
- name: 'reply-spec-hides-the-objection-channel',
293
- ref: 'prompt-v5-integrity.test.mjs',
294
- guarantee: '㉛ every field the framework honours must be documented in the reply spec',
295
- from: " L.push(' \"reject_assign\": {\"task_id\":\"t-3\",\"why\":\"你对这项分派的异议理由\"}",
296
- to: " if (false) L.push(' \"reject_assign\": {\"task_id\":\"t-3\",\"why\":\"你对这项分派的异议理由\"}",
297
- },
298
- {
299
- name: 'task-owner-rewoken-unpaced',
300
- ref: 'prompt-v5-integrity.test.mjs',
301
- guarantee: '㉜ a task owner must be pushed on a paced cadence, not in an unbounded tight loop',
302
- from: " if ((now() - (lastActiveAt.get(m.id) || 0)) < idleMs) continue",
303
- to: " if (false) continue",
304
- },
305
- {
306
- name: 'verify-preempts-a-live-meeting',
307
- ref: 'prompt-v5-integrity.test.mjs',
308
- guarantee: '㉝ a verification proposed during a meeting must QUEUE, never start concurrently (the two are mutually exclusive)',
309
- // Removes BOTH `meeting` gates (see the note above `meeting-never-finalized`): deleting
310
- // only the head copy is inert because the in-loop re-check runs before any `await`.
311
- from: armGuards(true, true),
312
- to: armGuards(false, true),
313
- },
314
-
315
- // ── LEAN FORMAL VERIFICATION PROBES (docs/formal-verification.md) ─────────
316
- {
317
- name: 'formal-off-is-not-a-no-op',
318
- ref: 'formal-verify-v5.test.mjs',
319
- guarantee: '㉞ the default mode must be a TRUE no-op (no Lean text, no gate)',
320
- from: " const formalOn = () => formalMode() !== 'off'",
321
- to: " const formalOn = () => true",
322
- },
323
- {
324
- name: 'unknown-formal-mode-upgrades',
325
- ref: 'formal-verify-v5.test.mjs',
326
- guarantee: '㉟ an unknown mode must degrade to off, never to a STRONGER mode (a typo must not force formalization)',
327
- from: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'",
328
- to: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'require'",
329
- },
330
- {
331
- name: 'fidelity-switch-removed',
332
- ref: 'formal-verify-v5.test.mjs',
333
- guarantee: '㊱ a passing Lean run must switch the voting prompt to a FIDELITY review (the whole point of the feature)',
334
- from: " if (rec.status === 'passed') {\n // The whole point of the feature: the review subject CHANGES.",
335
- to: " if (false) {\n // The whole point of the feature: the review subject CHANGES.",
336
- },
337
- {
338
- name: 'require-gate-removed',
339
- ref: 'formal-verify-v5.test.mjs',
340
- guarantee: '㊲ require mode must withhold a true/false verdict until the object is Lean-passed or explicitly blocked',
341
- from: " if (formalMode() === 'require' && !formalGateOk(rec)) {",
342
- to: " if (false) {",
343
- },
344
- {
345
- name: 'blocked-note-not-required',
346
- ref: 'formal-verify-v5.test.mjs',
347
- guarantee: '㊳ a "we judged it infeasible" record must carry a reason (the difficulty decision must be auditable, not silent)',
348
- from: " if (!note) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
349
- to: " if (false) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
350
- },
351
- {
352
- name: 'proof-not-archived-under-verified',
353
- ref: 'formal-verify-v5.test.mjs',
354
- guarantee: '㊴ a passing proof must be archived as that object\'s proof (Verified/Lean/<id>.lean)',
355
- from: " if (passed) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
356
- to: " if (false) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
357
- },
358
- {
359
- name: 'reusable-lib-written-inside-the-institute',
360
- ref: 'formal-verify-v5.test.mjs',
361
- guarantee: '㊵ reuse must be CROSS-PROJECT: reusable definitions go to the global Formal/Lib, not inside one institute',
362
- from: " const okWrite = await writeTextAbs(instRootless(rel), body)",
363
- to: " const okWrite = await writeTextRel(rel, body)",
364
- },
365
- {
366
- name: 'lean-path-guard-naive',
367
- ref: 'formal-verify-v5.test.mjs',
368
- guarantee: '㊶ the Lean path guard must normalise .. (a string prefix check lets a traversal through)',
369
- from: " const abs = leanAbsPath(rel)\n if (abs === null) {",
370
- to: " const abs = (rel.charAt(0) === '/' || /^[a-z]:/i.test(rel)) ? rel.replace(/\\\\/g, '/') : instRoot() + '/' + rel\n if (abs.indexOf(vibeRoot() + '/') !== 0) {",
371
- },
372
- {
373
- name: 'lean-tools-not-registered',
374
- ref: 'formal-verify-v5.test.mjs',
375
- guarantee: '㊷ the three Lean tools must be registered (agents can only formalize if the tools exist)',
376
- from: " registerTool('vibe_v5_lean_run',",
377
- to: " if (false) registerTool('vibe_v5_lean_run',",
378
- },
379
- ]
380
-
381
- let probesPassed = 0
382
- let probesFailed = 0
383
- console.log('-- V5 sensitivity probes --')
384
- console.log('(a probe passes when breaking the guarantee turns the suite RED)')
385
- console.log('')
386
-
387
- for (const p of probes) {
388
- const occurrences = original.split(p.from).length - 1
389
- if (occurrences !== 1) {
390
- console.error(' SETUP-FAIL - ' + p.name + ': anchor matched ' + occurrences + ' times (need exactly 1)')
391
- probesFailed++
392
- continue
393
- }
394
- const mutated = original.replace(p.from, p.to)
395
- const file = join(dir, p.name + '.js')
396
- writeFileSync(file, mutated, 'utf8')
397
- const testPath = TESTS[p.ref]
398
- if (testPath === undefined) {
399
- console.error(' SETUP-FAIL - ' + p.name + ': ref "' + p.ref + '" is not a known suite')
400
- probesFailed++
401
- continue
402
- }
403
- const r = spawnSync(process.execPath, [testPath], {
404
- // V5_CORPUS_DIR: a MUTATED run must never rewrite the shipped `prompt-corpus-v5/`. The
405
- // corpus is the mandatory human-review artifact (AUDIT-CHECKLIST §0.3/§2.4); letting a
406
- // deliberately broken copy dump its prompts there would leave the working tree holding
407
- // mutant text until someone re-ran the healthy suite.
408
- env: Object.assign({}, process.env, { V5_PLUGIN: file, V5_CORPUS_DIR: join(dir, 'corpus') }),
409
- encoding: 'utf8',
410
- cwd: REPO,
411
- })
412
- const red = r.status !== 0
413
- if (red) {
414
- probesPassed++
415
- console.log(' ok - ' + p.name + ' [' + p.ref + '] => suite went RED as required [' + p.guarantee + ']')
416
- } else {
417
- probesFailed++
418
- console.error(' BLIND SPOT - ' + p.name + ' [' + p.ref + '] => suite stayed GREEN, so it does NOT detect: ' + p.guarantee)
419
- }
420
- }
421
-
422
- rmSync(dir, { recursive: true, force: true })
423
- console.log('')
424
- console.log('sensitivity: ' + probesPassed + ' probes detected the break, ' + probesFailed + ' blind spots')
425
- if (probesFailed) process.exit(1)
426
- console.log('ALL PROBES RED AS REQUIRED')
427
- process.exit(0)
1
+ // ============================================================
2
+ // V5 SENSITIVITY PROBES — prove the suites are not vacuous.
3
+ //
4
+ // Each probe copies vibe-math-v5.js, applies ONE targeted mutation that breaks a
5
+ // specific guarantee, and runs the referencing suite against the mutated copy. A probe
6
+ // PASSES when the suite goes RED (non-zero exit) — i.e. the assertions really do detect
7
+ // that break. A probe that stays green means the suite has a blind spot.
8
+ //
9
+ // The prompt-integrity block below exists because the 2026-09 field test found a bug
10
+ // that 123 tool-level assertions could not see: every member's brief named the WRONG
11
+ // member. Any guarantee about the TEXT a member reads must have a probe that proves
12
+ // prompt-v5-integrity.test.mjs detects its violation.
13
+ //
14
+ // Run: node tests/audit-v5-sensitivity.mjs
15
+ // ============================================================
16
+ import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { join } from 'node:path'
19
+ import { spawnSync } from 'node:child_process'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ const SRC = new URL('../vibe-math-v5/vibe-math-v5.js', import.meta.url)
23
+ // Keyed by the EXACT suite filename a probe names in its `ref`. A ternary chain here once
24
+ // silently routed every new probe to the wrong suite (which then stayed green because the
25
+ // mutation did not touch what it tests) — that reports a blind spot that does not exist.
26
+ const TESTS = {}
27
+ for (const f of ['selfdrive-v5.mjs', 'e2e-v5-round2.test.mjs', 'prompt-v5-integrity.test.mjs', 'formal-verify-v5.test.mjs']) {
28
+ TESTS[f] = fileURLToPath(new URL('./' + f, import.meta.url))
29
+ }
30
+ const original = readFileSync(SRC, 'utf8').replace(/\r\n?/g, '\n') // task-7: anchors are LF; a CRLF checkout made them match 0 times
31
+ if (/\r/.test(original)) { console.error('SETUP-FAIL - the anchor source was not EOL-normalised (CRLF leaked into anchor matching)'); process.exit(1) }
32
+ const REPO = fileURLToPath(new URL('..', import.meta.url))
33
+ const dir = mkdtempSync(join(tmpdir(), 'v5-sens-'))
34
+
35
+ // `armNextVerify`'s two exclusion gates (a live MEETING and a live VERIFICATION), each written
36
+ // at the function head AND as a re-check inside the `while` loop with no `await` in between.
37
+ // `armGuards(true, true)` is the pristine source text; the probes switch ONE gate off in BOTH
38
+ // copies, which is the minimal edit that actually breaks the guarantee. Keeping the block in
39
+ // one place means the two mutations cannot drift apart from the source they mutate.
40
+ const armGuards = (meetingOn, verifyOn) => [
41
+ ...(meetingOn ? [' if (meeting) return'] : []),
42
+ ' // Only one begin may be in flight. Without this, two callers (a scheduling pass',
43
+ ' // and a fresh proposal) could both pass the `currentVerify()` check before either',
44
+ ' // has published its verdict record and would start the SAME object twice.',
45
+ ' if (beginLock) return',
46
+ ...(verifyOn ? [' if (currentVerify()) return'] : []),
47
+ ' beginLock = true',
48
+ ' try {',
49
+ ' while (true) {',
50
+ ' // NEVER remove a proposal we are not about to run: the entry leaves the durable',
51
+ ' // queue only in the same step that starts it. The old code shifted it out and then',
52
+ ' // returned because a meeting was live, so the proposal existed in neither the queue',
53
+ ' // nor `verdicts` until some later pass happened to re-arm it (audit M2). Re-check',
54
+ ' // the exclusion guards here too: `await putQueue` can let a meeting open.',
55
+ ' const q = inst().queue.slice()',
56
+ ' if (!q.length) return',
57
+ ' if (' + (meetingOn && verifyOn ? 'meeting || currentVerify()' : meetingOn ? 'meeting' : 'currentVerify()') + ') return',
58
+ ].join('\n')
59
+
60
+ // Each probe: { name, ref, guarantee, from, to }
61
+ // `from` must occur exactly once, so a mutation can never quietly hit the wrong site.
62
+ const probes = [
63
+ {
64
+ // The m floor is enforced by THREE cooperating checks (an early `bTrue + bFalse < m`
65
+ // return, the conflict check, and a final `bTrue >= m`), so a single weakened check is
66
+ // masked by the others and is a SEMANTICALLY INERT mutation — it must never be used as
67
+ // a probe (it would look like a "blind spot"). Forcing the quorum itself to 1 breaks
68
+ // the rule on every path at once, which is exactly the guarantee under test.
69
+ name: 'quorum-forced-to-one',
70
+ ref: 'selfdrive-v5.mjs',
71
+ guarantee: '④ only >= m boolean votes may verify (m must be min(quorumCap, |voters|))',
72
+ // The floor moved into `quorumMFrom(vc)` when the quorum became single-sourced (defect B of
73
+ // the 3rd self-test); the anchor follows the implementation, it is not relaxed.
74
+ from: " return Math.max(1, Math.min(cap, vc))",
75
+ to: " return 1",
76
+ },
77
+ {
78
+ name: 'abstention-counts-as-true',
79
+ ref: 'selfdrive-v5.mjs',
80
+ guarantee: '⑤ an abstention must NOT count toward the quorum',
81
+ from: " else if (p === 0) bFalse += 1\n else abstain += 1",
82
+ to: " else if (p === 0) bFalse += 1\n else { abstain += 1; bTrue += 1 }",
83
+ },
84
+ {
85
+ // `judgeVerdict` checks the m floor, then the conflict, then the floor again, and each
86
+ // guard masks the next — so deleting a guard is inert. The behaviour-changing break is
87
+ // letting a CONFLICT pass as a verdict, which is what this mutation does.
88
+ name: 'conflict-allowed',
89
+ ref: 'selfdrive-v5.mjs',
90
+ guarantee: '④ a conflicting 1 vs 0 must BLOCK the verdict',
91
+ from: " return Object.assign(base, { outcome: 'undecided', reason: 'conflicting assertions (true=' + bTrue + ', false=' + bFalse + ')' })",
92
+ to: " return Object.assign(base, { outcome: 'true', reason: 'conflicting assertions (true=' + bTrue + ', false=' + bFalse + ')' })",
93
+ },
94
+ {
95
+ name: 'temp-worker-can-vote',
96
+ ref: 'selfdrive-v5.mjs',
97
+ guarantee: '⑦ a temp worker must NOT be able to vote',
98
+ from: " if (member.kind === 'temp') {\n // Temp workers have no vote",
99
+ to: " if (false) {\n // Temp workers have no vote",
100
+ },
101
+ {
102
+ name: 'academician-gate-removed',
103
+ ref: 'selfdrive-v5.mjs',
104
+ guarantee: '⑬ only the academician may assign tasks',
105
+ from: " return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can assign tasks' }",
106
+ to: " return { ok: false, code: 'NO_GATE', message: 'x' }",
107
+ },
108
+ {
109
+ name: 'spawn-not-registered-inflight',
110
+ ref: 'selfdrive-v5.mjs',
111
+ guarantee: '⑯ the founding turn must be registered in-flight or its end is dropped',
112
+ from: " inflight.set(started.childId, shortId())",
113
+ to: "",
114
+ },
115
+ {
116
+ name: 'group-chat-not-fanned-out',
117
+ ref: 'selfdrive-v5.mjs',
118
+ guarantee: '③ group chat must reach every other member',
119
+ from: " if (to === 'all' || to === '') targets = activeMembers().filter((m) => m.id !== from)",
120
+ to: " if (to === 'all' || to === '') targets = activeMembers().filter((m) => m.id !== from).slice(0, 1)",
121
+ },
122
+ {
123
+ name: 'verify-round-advances-anyway',
124
+ ref: 'selfdrive-v5.mjs',
125
+ guarantee: '④ a round must not advance without every voter answering',
126
+ from: " const missing = need.filter((id) => !vs.votes[id])\n if (missing.length) {",
127
+ to: " const missing = need.filter((id) => !vs.votes[id])\n if (false) {",
128
+ },
129
+ {
130
+ name: 'fire-does-not-release',
131
+ ref: 'selfdrive-v5.mjs',
132
+ guarantee: '⑨ firing must really release the resident child',
133
+ from: " if (typeof subagents.drainContinuableChildren === 'function') await subagents.drainContinuableChildren(rootAgent, [target.childId])",
134
+ to: " if (false) await subagents.drainContinuableChildren(rootAgent, [target.childId])",
135
+ },
136
+ {
137
+ name: 'solve-vote-not-unanimous',
138
+ ref: 'selfdrive-v5.mjs',
139
+ guarantee: '⑥ the institute must not stop unless EVERY voter agrees',
140
+ // Anchor follows the HIGH-3 fix (the tally moved from an in-memory Map to durable state); the
141
+ // guarantee is unchanged — the unanimity test itself is what the mutation removes.
142
+ from: " const sv = solveVotesOf()\n if (!vs.every((id) => sv[id] === true)) return false",
143
+ to: " if (false) return false",
144
+ },
145
+ // ── probes added after the round-2 audit found these defects ──────────────
146
+ {
147
+ name: 'fallback-state-never-loaded',
148
+ ref: 'e2e-v5-round2.test.mjs',
149
+ guarantee: '⑰ the file fallback must LOAD persisted state before any read (restart safety)',
150
+ from: " if (backend.kind === 'file' && typeof backend.load === 'function') await backend.load()",
151
+ to: " if (false) await backend.load()",
152
+ },
153
+ {
154
+ name: 'solve-vote-never-re-evaluated',
155
+ ref: 'prompt-v5-integrity.test.mjs',
156
+ guarantee: '⑥ a solve vote landing OUTSIDE a meeting must still stop the institute',
157
+ // The anchor must include the CALL. An `if (false)` inserted before the comment would
158
+ // leave the real `await checkSolved()` below it untouched — an inert mutation that
159
+ // would masquerade as a blind spot.
160
+ from: " await putSolve({ member: memberId, value: val === true })\n // Evaluate the stop condition on EVERY solve vote, not only when a meeting\n // finalizes. A vote that lands after the meeting closed — a late reply, or an\n // ordinary round carrying vote_solved — would otherwise be recorded and never\n // read, leaving a unanimously-concluded institute running forever.\n await checkSolved()",
161
+ to: " await putSolve({ member: memberId, value: val === true })",
162
+ },
163
+ {
164
+ name: 'proposal-only-kicks-scheduler',
165
+ ref: 'selfdrive-v5.mjs',
166
+ guarantee: '④ a proposal must actually START, not just ask the scheduler to try',
167
+ from: " await armNextVerify()\n if (!hasVerifyInFlight()) await scheduleNext()",
168
+ to: " await scheduleNext()",
169
+ },
170
+ {
171
+ name: 'begin-not-exclusive',
172
+ ref: 'prompt-v5-integrity.test.mjs',
173
+ guarantee: '⑱ a second proposal must QUEUE, never start a concurrent ballot',
174
+ from: armGuards(true, true),
175
+ to: armGuards(true, false),
176
+ },
177
+ // NOTE — the mutual-exclusion gates of `armNextVerify` are written TWICE: once at the
178
+ // function head and once as a re-check inside the `while (true)` loop, and there is NO
179
+ // `await` between them. The head copies are therefore redundant mirrors of the in-loop
180
+ // ones, and a probe that deletes only a head copy is SEMANTICALLY INERT — it can never go
181
+ // red, which would masquerade as a "blind spot" forever (this was the state of both of the
182
+ // ⑱/㉝ probes: the in-loop re-check kept the invariant alive and
183
+ // `prompt-v5-integrity.test.mjs` stayed green). The probes for ⑱ and ㉝ therefore remove
184
+ // BOTH copies of the ONE gate they test, which is the minimal edit that actually lets a
185
+ // second ballot start — and the suite's case 9b ("a second proposal QUEUES") and case 10b
186
+ // ("a verification proposed DURING a meeting must queue") do detect exactly that.
187
+ // NOTE — a probe for `continueMeetingRound`'s `if (finalizeLock) { armHeartbeat(); return }`
188
+ // re-arm was REMOVED, not because the re-arm is unnecessary but because the state it
189
+ // guards is UNREACHABLE, so no black-box probe can detect its removal:
190
+ // · `schedulePass` handles a live meeting BEFORE it looks at a verification, and
191
+ // · `startMeeting` parks any meeting while `hasVerifyInFlight()`,
192
+ // so a meeting and a running verify settlement can never overlap. The re-arm stays in
193
+ // the code as cheap insurance against a future ordering change; leaving a probe that can
194
+ // never go red would be a false "detection" and is worse than no probe.
195
+ {
196
+ name: 'meeting-never-finalized',
197
+ ref: 'prompt-v5-integrity.test.mjs',
198
+ guarantee: '⑲ a meeting that collected every input must actually finalize and write its minutes',
199
+ from: " finalizeLock = 'meeting'\n try {\n await finalizeMeeting(meeting)",
200
+ to: " finalizeLock = 'meeting'\n try {\n meeting = null",
201
+ },
202
+
203
+ // ── PROMPT / INTERACTION INTEGRITY PROBES ────────────────────────────────
204
+ // These are the probes that would have caught the 2026-09 field-test bug. Every one
205
+ // of them breaks something a member READS (identity, roster, framing, persona), and
206
+ // prompt-v5-integrity.test.mjs must go RED for each.
207
+ {
208
+ name: 'brief-names-the-last-woken-member',
209
+ ref: 'prompt-v5-integrity.test.mjs',
210
+ guarantee: '⑳ a prompt\'s [状态] block must name the member it is sent to (the exact field bug: briefs named the previous member)',
211
+ from: " function stateBlock(member, roundNo) {\n return briefBlock(member, roundNo)\n }",
212
+ to: " function stateBlock(member, roundNo) {\n return briefBlock(memberById(currentMember) || activeMembers()[0] || member, roundNo)\n }",
213
+ },
214
+ {
215
+ name: 'joiner-absent-from-own-roster',
216
+ ref: 'prompt-v5-integrity.test.mjs',
217
+ guarantee: '⑳ a founding brief must show the roster INCLUDING its reader (roster committed before the prompt is built)',
218
+ from: " member.phase = 'active'\n member.childId = ''\n await putMember(member)",
219
+ to: " member.childId = ''\n await putMember(member)",
220
+ },
221
+ {
222
+ name: 'inbox-message-delivered-twice',
223
+ ref: 'prompt-v5-integrity.test.mjs',
224
+ guarantee: '㉑ one prompt must not deliver the same message twice',
225
+ // G1 moved the ack AFTER the wake, so the duplicate can no longer be produced by the ack order:
226
+ // it now comes from prepending the inbox while `briefBlock` ALSO embeds the same pending mail.
227
+ // Suppressing the base block while a block IS prepended is what keeps exactly one inbox section.
228
+ from: " const pending = inboxSuppressed.has(member.id) ? [] : pendingFor(member.id)",
229
+ to: " const pending = pendingFor(member.id)",
230
+ },
231
+ {
232
+ name: 'charter-rewritten-on-resume',
233
+ ref: 'prompt-v5-integrity.test.mjs',
234
+ guarantee: '㉒ the charter is frozen at hire; a resume must not rewrite the induction snapshot',
235
+ from: " const persona = member.persona || memberPersona(member)",
236
+ to: " const persona = memberPersona(member)",
237
+ },
238
+ {
239
+ name: 'resume-framed-as-induction',
240
+ ref: 'prompt-v5-integrity.test.mjs',
241
+ guarantee: '㉓ a rebuilt session must not be told it just joined the institute',
242
+ from: " const resume = mode === 'resume'",
243
+ to: " const resume = false",
244
+ },
245
+ {
246
+ name: 'meeting-proposal-misattributed',
247
+ ref: 'prompt-v5-integrity.test.mjs',
248
+ guarantee: '㉔ a relayed message must carry its TRUE sender, never the last-woken member',
249
+ from: " await say(callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })",
250
+ to: " await say(academicianId() || callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })",
251
+ },
252
+ {
253
+ name: 'office-impersonates-a-member',
254
+ ref: 'prompt-v5-integrity.test.mjs',
255
+ guarantee: '㉕ the office/host caller must resolve to the office, not to a guessed member',
256
+ // The FAITHFUL regression mutation: restore the old "answer with whoever this session
257
+ // woke last" fallback, which made the office's own assignments resolve to a random
258
+ // researcher and be refused as V5_NOT_ACADEMICIAN.
259
+ from: " try { if (rootOf(agent) === agent) return 'office' } catch (e) { /* fall through */ }",
260
+ to: " if (currentMember && memberById(currentMember)) return currentMember",
261
+ },
262
+ {
263
+ name: 'office-assignment-framed-as-academician',
264
+ ref: 'prompt-v5-integrity.test.mjs',
265
+ guarantee: '㉖ an assignment must be framed by its true origin (office vs academician)',
266
+ from: " if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text",
267
+ to: " if (m.kind === 'assign') return '【院士分派】' + m.text",
268
+ },
269
+ {
270
+ name: 'nudge-mislabelled-as-assignment',
271
+ ref: 'prompt-v5-integrity.test.mjs',
272
+ guarantee: '㉗ a nudge is supervision, not an assignment, and must say so',
273
+ from: " to, kind: 'nudge',",
274
+ to: " to, kind: 'assign',",
275
+ },
276
+ {
277
+ name: 'framework-notice-sent-as-self-message',
278
+ ref: 'prompt-v5-integrity.test.mjs',
279
+ guarantee: '㉘ framework feedback must actually reach the member (not be refused as a self-message)',
280
+ from: " return await say('framework', { to: memberId, kind: 'notice', text: String(text) })",
281
+ to: " return await say(memberId, { to: memberId, kind: 'notice', text: String(text) })",
282
+ },
283
+ {
284
+ name: 'failed-member-hidden',
285
+ ref: 'prompt-v5-integrity.test.mjs',
286
+ guarantee: '㉙ a member that failed to provision must be visible in [未就位]',
287
+ from: " if (absent.length) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))",
288
+ to: " if (false) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))",
289
+ },
290
+ {
291
+ name: 'leaderless-charter-invents-a-leader',
292
+ ref: 'prompt-v5-integrity.test.mjs',
293
+ guarantee: '㉚ with academician:false no charter may name a leader who does not exist',
294
+ from: " const a = academicianId()\n const L = [",
295
+ to: " const a = 'acad'\n const L = [",
296
+ },
297
+ {
298
+ name: 'reply-spec-hides-the-objection-channel',
299
+ ref: 'prompt-v5-integrity.test.mjs',
300
+ guarantee: '㉛ every field the framework honours must be documented in the reply spec',
301
+ from: " L.push(' \"reject_assign\": {\"task_id\":\"t-3\",\"why\":\"你对这项分派的异议理由\"}",
302
+ to: " if (false) L.push(' \"reject_assign\": {\"task_id\":\"t-3\",\"why\":\"你对这项分派的异议理由\"}",
303
+ },
304
+ {
305
+ name: 'task-owner-rewoken-unpaced',
306
+ ref: 'prompt-v5-integrity.test.mjs',
307
+ guarantee: '㉜ a task owner must be pushed on a paced cadence, not in an unbounded tight loop',
308
+ from: " if ((now() - (lastActiveAt.get(m.id) || 0)) < idleMs) continue",
309
+ to: " if (false) continue",
310
+ },
311
+ {
312
+ name: 'verify-preempts-a-live-meeting',
313
+ ref: 'prompt-v5-integrity.test.mjs',
314
+ guarantee: '㉝ a verification proposed during a meeting must QUEUE, never start concurrently (the two are mutually exclusive)',
315
+ // Removes BOTH `meeting` gates (see the note above `meeting-never-finalized`): deleting
316
+ // only the head copy is inert because the in-loop re-check runs before any `await`.
317
+ from: armGuards(true, true),
318
+ to: armGuards(false, true),
319
+ },
320
+
321
+ // ── LEAN FORMAL VERIFICATION PROBES (docs/formal-verification.md) ─────────
322
+ {
323
+ name: 'formal-off-is-not-a-no-op',
324
+ ref: 'formal-verify-v5.test.mjs',
325
+ guarantee: '㉞ the default mode must be a TRUE no-op (no Lean text, no gate)',
326
+ from: " const formalOn = () => formalMode() !== 'off'",
327
+ to: " const formalOn = () => true",
328
+ },
329
+ {
330
+ name: 'unknown-formal-mode-upgrades',
331
+ ref: 'formal-verify-v5.test.mjs',
332
+ guarantee: '㉟ an unknown mode must degrade to off, never to a STRONGER mode (a typo must not force formalization)',
333
+ from: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'",
334
+ to: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'require'",
335
+ },
336
+ {
337
+ name: 'fidelity-switch-removed',
338
+ ref: 'formal-verify-v5.test.mjs',
339
+ guarantee: '㊱ a passing Lean run must switch the voting prompt to a FIDELITY review (the whole point of the feature)',
340
+ from: " if (rec.status === 'passed') {\n // The whole point of the feature: the review subject CHANGES.",
341
+ to: " if (false) {\n // The whole point of the feature: the review subject CHANGES.",
342
+ },
343
+ {
344
+ name: 'require-gate-removed',
345
+ ref: 'formal-verify-v5.test.mjs',
346
+ guarantee: '㊲ require mode must withhold a true/false verdict until the object is Lean-passed or explicitly blocked',
347
+ from: " if (formalMode() === 'require' && !formalGateOk(rec)) {",
348
+ to: " if (false) {",
349
+ },
350
+ {
351
+ name: 'blocked-note-not-required',
352
+ ref: 'formal-verify-v5.test.mjs',
353
+ guarantee: '㊳ a "we judged it infeasible" record must carry a reason (the difficulty decision must be auditable, not silent)',
354
+ from: " if (!note) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
355
+ to: " if (false) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
356
+ },
357
+ {
358
+ name: 'proof-not-archived-under-verified',
359
+ ref: 'formal-verify-v5.test.mjs',
360
+ guarantee: '㊴ a passing proof must be archived as that object\'s proof (Verified/Lean/<id>.lean)',
361
+ from: " if (passed) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
362
+ to: " if (false) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
363
+ },
364
+ {
365
+ name: 'reusable-lib-written-inside-the-institute',
366
+ ref: 'formal-verify-v5.test.mjs',
367
+ guarantee: '㊵ reuse must be CROSS-PROJECT: reusable definitions go to the global Formal/Lib, not inside one institute',
368
+ from: " const okWrite = await writeTextAbs(instRootless(rel), body)",
369
+ to: " const okWrite = await writeTextRel(rel, body)",
370
+ },
371
+ {
372
+ name: 'lean-path-guard-naive',
373
+ ref: 'formal-verify-v5.test.mjs',
374
+ guarantee: '㊶ the Lean path guard must normalise .. (a string prefix check lets a traversal through)',
375
+ from: " const abs = leanAbsPath(rel)\n if (abs === null) {",
376
+ to: " const abs = (rel.charAt(0) === '/' || /^[a-z]:/i.test(rel)) ? rel.replace(/\\\\/g, '/') : instRoot() + '/' + rel\n if (abs.indexOf(vibeRoot() + '/') !== 0) {",
377
+ },
378
+ {
379
+ name: 'lean-tools-not-registered',
380
+ ref: 'formal-verify-v5.test.mjs',
381
+ guarantee: '㊷ the three Lean tools must be registered (agents can only formalize if the tools exist)',
382
+ from: " registerTool('vibe_v5_lean_run',",
383
+ to: " if (false) registerTool('vibe_v5_lean_run',",
384
+ },
385
+ ]
386
+
387
+ let probesPassed = 0
388
+ let probesFailed = 0
389
+ console.log('-- V5 sensitivity probes --')
390
+ console.log('(a probe passes when breaking the guarantee turns the suite RED)')
391
+ console.log('')
392
+
393
+ for (const p of probes) {
394
+ const occurrences = original.split(p.from).length - 1
395
+ if (occurrences !== 1) {
396
+ console.error(' SETUP-FAIL - ' + p.name + ': anchor matched ' + occurrences + ' times (need exactly 1)')
397
+ probesFailed++
398
+ continue
399
+ }
400
+ const mutated = original.replace(p.from, p.to)
401
+ const file = join(dir, p.name + '.js')
402
+ writeFileSync(file, mutated, 'utf8')
403
+ const testPath = TESTS[p.ref]
404
+ if (testPath === undefined) {
405
+ console.error(' SETUP-FAIL - ' + p.name + ': ref "' + p.ref + '" is not a known suite')
406
+ probesFailed++
407
+ continue
408
+ }
409
+ const r = spawnSync(process.execPath, [testPath], {
410
+ // V5_CORPUS_DIR: a MUTATED run must never rewrite the shipped `prompt-corpus-v5/`. The
411
+ // corpus is the mandatory human-review artifact (AUDIT-CHECKLIST §0.3/§2.4); letting a
412
+ // deliberately broken copy dump its prompts there would leave the working tree holding
413
+ // mutant text until someone re-ran the healthy suite.
414
+ env: Object.assign({}, process.env, { V5_PLUGIN: file, V5_CORPUS_DIR: join(dir, 'corpus') }),
415
+ encoding: 'utf8',
416
+ cwd: REPO,
417
+ })
418
+ const red = r.status !== 0
419
+ if (red) {
420
+ probesPassed++
421
+ console.log(' ok - ' + p.name + ' [' + p.ref + '] => suite went RED as required [' + p.guarantee + ']')
422
+ } else {
423
+ probesFailed++
424
+ console.error(' BLIND SPOT - ' + p.name + ' [' + p.ref + '] => suite stayed GREEN, so it does NOT detect: ' + p.guarantee)
425
+ }
426
+ }
427
+
428
+ rmSync(dir, { recursive: true, force: true })
429
+ console.log('')
430
+ console.log('sensitivity: ' + probesPassed + ' probes detected the break, ' + probesFailed + ' blind spots')
431
+ if (probesFailed) process.exit(1)
432
+ console.log('ALL PROBES RED AS REQUIRED')
433
+ process.exit(0)