dsh-vibe-math 2.3.12 → 2.3.13

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 (47) hide show
  1. package/README.md +17 -13
  2. package/{AUDIT-CHECKLIST.md → docs/AUDIT-CHECKLIST.md} +323 -304
  3. package/docs/COMPAT-AUDIT-ROUND2.md +325 -0
  4. package/docs/generate_framework_diagram_v2.py +114 -0
  5. package/docs/generate_framework_diagram_v3.py +127 -0
  6. package/{RELEASE-NOTES-2.1.0.md → docs/release-notes/RELEASE-NOTES-2.1.0.md} +143 -143
  7. package/{RELEASE-NOTES-2.2.0.md → docs/release-notes/RELEASE-NOTES-2.2.0.md} +266 -266
  8. package/{RELEASE-NOTES-2.2.1.md → docs/release-notes/RELEASE-NOTES-2.2.1.md} +43 -43
  9. package/{RELEASE-NOTES-2.2.2.md → docs/release-notes/RELEASE-NOTES-2.2.2.md} +88 -88
  10. package/{RELEASE-NOTES-2.3.0.md → docs/release-notes/RELEASE-NOTES-2.3.0.md} +207 -207
  11. package/{RELEASE-NOTES-2.3.1.md → docs/release-notes/RELEASE-NOTES-2.3.1.md} +134 -134
  12. package/{RELEASE-NOTES-2.3.10.md → docs/release-notes/RELEASE-NOTES-2.3.10.md} +105 -105
  13. package/{RELEASE-NOTES-2.3.11.md → docs/release-notes/RELEASE-NOTES-2.3.11.md} +57 -57
  14. package/{RELEASE-NOTES-2.3.12.md → docs/release-notes/RELEASE-NOTES-2.3.12.md} +80 -80
  15. package/docs/release-notes/RELEASE-NOTES-2.3.13.md +137 -0
  16. package/{RELEASE-NOTES-2.3.2.md → docs/release-notes/RELEASE-NOTES-2.3.2.md} +145 -145
  17. package/{RELEASE-NOTES-2.3.3.md → docs/release-notes/RELEASE-NOTES-2.3.3.md} +115 -115
  18. package/{RELEASE-NOTES-2.3.4.md → docs/release-notes/RELEASE-NOTES-2.3.4.md} +69 -69
  19. package/{RELEASE-NOTES-2.3.5.md → docs/release-notes/RELEASE-NOTES-2.3.5.md} +63 -63
  20. package/{RELEASE-NOTES-2.3.6.md → docs/release-notes/RELEASE-NOTES-2.3.6.md} +66 -66
  21. package/{RELEASE-NOTES-2.3.7.md → docs/release-notes/RELEASE-NOTES-2.3.7.md} +59 -59
  22. package/{RELEASE-NOTES-2.3.8.md → docs/release-notes/RELEASE-NOTES-2.3.8.md} +45 -45
  23. package/{RELEASE-NOTES-2.3.9.md → docs/release-notes/RELEASE-NOTES-2.3.9.md} +70 -70
  24. package/docs/test-timing.md +18 -17
  25. package/installer.js +105 -41
  26. package/package.json +42 -37
  27. package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
  28. package/{audit-installer-compat.test.mjs → tests/audit-installer-compat.test.mjs} +136 -136
  29. package/tests/audit-installer-policy.test.mjs +245 -0
  30. package/{audit-persona-sensitivity.mjs → tests/audit-persona-sensitivity.mjs} +249 -249
  31. package/{audit-persona-surface.test.mjs → tests/audit-persona-surface.test.mjs} +349 -349
  32. package/{audit-prompt-invariants.mjs → tests/audit-prompt-invariants.mjs} +508 -508
  33. package/{audit-spec-traceability.mjs → tests/audit-spec-traceability.mjs} +193 -193
  34. package/{audit-v5-integrity.mjs → tests/audit-v5-integrity.mjs} +448 -448
  35. package/{audit-v5-sensitivity.mjs → tests/audit-v5-sensitivity.mjs} +384 -384
  36. package/{e2e-v5-round2.test.mjs → tests/e2e-v5-round2.test.mjs} +521 -521
  37. package/{formal-verify-v2.test.mjs → tests/formal-verify-v2.test.mjs} +1315 -1315
  38. package/{formal-verify-v3.test.mjs → tests/formal-verify-v3.test.mjs} +1257 -1257
  39. package/{formal-verify-v4.test.mjs → tests/formal-verify-v4.test.mjs} +1082 -1082
  40. package/{formal-verify-v5.test.mjs → tests/formal-verify-v5.test.mjs} +708 -708
  41. package/{prompt-v5-integrity.test.mjs → tests/prompt-v5-integrity.test.mjs} +3 -3
  42. package/{run-tests.mjs → tests/run-tests.mjs} +121 -118
  43. package/{selfdrive-v5.mjs → tests/selfdrive-v5.mjs} +470 -470
  44. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
  45. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
  46. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +1 -1
  47. /package/{RELEASE-NOTES-2.0.22.md → docs/release-notes/RELEASE-NOTES-2.0.22.md} +0 -0
@@ -1,384 +1,384 @@
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 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
- // Each probe: { name, ref, guarantee, from, to }
35
- // `from` must occur exactly once, so a mutation can never quietly hit the wrong site.
36
- const probes = [
37
- {
38
- // The m floor is enforced by THREE cooperating checks (an early `bTrue + bFalse < m`
39
- // return, the conflict check, and a final `bTrue >= m`), so a single weakened check is
40
- // masked by the others and is a SEMANTICALLY INERT mutation — it must never be used as
41
- // a probe (it would look like a "blind spot"). Forcing the quorum itself to 1 breaks
42
- // the rule on every path at once, which is exactly the guarantee under test.
43
- name: 'quorum-forced-to-one',
44
- ref: 'selfdrive-v5.mjs',
45
- guarantee: '④ only >= m boolean votes may verify (m must be min(quorumCap, |voters|))',
46
- from: " return Math.max(1, Math.min(cap, voterCount()))",
47
- to: " return 1",
48
- },
49
- {
50
- name: 'abstention-counts-as-true',
51
- ref: 'selfdrive-v5.mjs',
52
- guarantee: '⑤ an abstention must NOT count toward the quorum',
53
- from: " else if (p === 0) bFalse += 1\n else abstain += 1",
54
- to: " else if (p === 0) bFalse += 1\n else { abstain += 1; bTrue += 1 }",
55
- },
56
- {
57
- // `judgeVerdict` checks the m floor, then the conflict, then the floor again, and each
58
- // guard masks the next — so deleting a guard is inert. The behaviour-changing break is
59
- // letting a CONFLICT pass as a verdict, which is what this mutation does.
60
- name: 'conflict-allowed',
61
- ref: 'selfdrive-v5.mjs',
62
- guarantee: '④ a conflicting 1 vs 0 must BLOCK the verdict',
63
- from: " return Object.assign(base, { outcome: 'undecided', reason: 'conflicting assertions (true=' + bTrue + ', false=' + bFalse + ')' })",
64
- to: " return Object.assign(base, { outcome: 'true', reason: 'conflicting assertions (true=' + bTrue + ', false=' + bFalse + ')' })",
65
- },
66
- {
67
- name: 'temp-worker-can-vote',
68
- ref: 'selfdrive-v5.mjs',
69
- guarantee: '⑦ a temp worker must NOT be able to vote',
70
- from: " if (member.kind === 'temp') {\n // Temp workers have no vote",
71
- to: " if (false) {\n // Temp workers have no vote",
72
- },
73
- {
74
- name: 'academician-gate-removed',
75
- ref: 'selfdrive-v5.mjs',
76
- guarantee: '⑬ only the academician may assign tasks',
77
- from: " return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can assign tasks' }",
78
- to: " return { ok: false, code: 'NO_GATE', message: 'x' }",
79
- },
80
- {
81
- name: 'spawn-not-registered-inflight',
82
- ref: 'selfdrive-v5.mjs',
83
- guarantee: '⑯ the founding turn must be registered in-flight or its end is dropped',
84
- from: " inflight.set(started.childId, shortId())",
85
- to: "",
86
- },
87
- {
88
- name: 'group-chat-not-fanned-out',
89
- ref: 'selfdrive-v5.mjs',
90
- guarantee: '③ group chat must reach every other member',
91
- from: " if (to === 'all' || to === '') targets = activeMembers().filter((m) => m.id !== from)",
92
- to: " if (to === 'all' || to === '') targets = activeMembers().filter((m) => m.id !== from).slice(0, 1)",
93
- },
94
- {
95
- name: 'verify-round-advances-anyway',
96
- ref: 'selfdrive-v5.mjs',
97
- guarantee: '④ a round must not advance without every voter answering',
98
- from: " const missing = need.filter((id) => !vs.votes[id])\n if (missing.length) {",
99
- to: " const missing = need.filter((id) => !vs.votes[id])\n if (false) {",
100
- },
101
- {
102
- name: 'fire-does-not-release',
103
- ref: 'selfdrive-v5.mjs',
104
- guarantee: '⑨ firing must really release the resident child',
105
- from: " if (typeof subagents.drainContinuableChildren === 'function') await subagents.drainContinuableChildren(rootAgent, [target.childId])",
106
- to: " if (false) await subagents.drainContinuableChildren(rootAgent, [target.childId])",
107
- },
108
- {
109
- name: 'solve-vote-not-unanimous',
110
- ref: 'selfdrive-v5.mjs',
111
- guarantee: '⑥ the institute must not stop unless EVERY voter agrees',
112
- from: " if (!vs.every((id) => solveVotes.get(id) === true)) return false",
113
- to: " if (false) return false",
114
- },
115
- // ── probes added after the round-2 audit found these defects ──────────────
116
- {
117
- name: 'fallback-state-never-loaded',
118
- ref: 'e2e-v5-round2.test.mjs',
119
- guarantee: '⑰ the file fallback must LOAD persisted state before any read (restart safety)',
120
- from: " if (backend.kind === 'file' && typeof backend.load === 'function') await backend.load()",
121
- to: " if (false) await backend.load()",
122
- },
123
- {
124
- name: 'solve-vote-never-re-evaluated',
125
- ref: 'prompt-v5-integrity.test.mjs',
126
- guarantee: '⑥ a solve vote landing OUTSIDE a meeting must still stop the institute',
127
- // The anchor must include the CALL. An `if (false)` inserted before the comment would
128
- // leave the real `await checkSolved()` below it untouched — an inert mutation that
129
- // would masquerade as a blind spot.
130
- 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()",
131
- to: " solveVotes.set(memberId, val === true)",
132
- },
133
- {
134
- name: 'proposal-only-kicks-scheduler',
135
- ref: 'selfdrive-v5.mjs',
136
- guarantee: '④ a proposal must actually START, not just ask the scheduler to try',
137
- from: " await armNextVerify()\n if (!hasVerifyInFlight()) await scheduleNext()",
138
- to: " await scheduleNext()",
139
- },
140
- {
141
- name: 'begin-not-exclusive',
142
- ref: 'prompt-v5-integrity.test.mjs',
143
- guarantee: '⑱ a second proposal must QUEUE, never start a concurrent ballot',
144
- from: " if (beginLock) return\n if (currentVerify()) return\n beginLock = true",
145
- to: " beginLock = true",
146
- },
147
- // NOTE — a probe for `continueMeetingRound`'s `if (finalizeLock) { armHeartbeat(); return }`
148
- // re-arm was REMOVED, not because the re-arm is unnecessary but because the state it
149
- // guards is UNREACHABLE, so no black-box probe can detect its removal:
150
- // · `schedulePass` handles a live meeting BEFORE it looks at a verification, and
151
- // · `startMeeting` parks any meeting while `hasVerifyInFlight()`,
152
- // so a meeting and a running verify settlement can never overlap. The re-arm stays in
153
- // the code as cheap insurance against a future ordering change; leaving a probe that can
154
- // never go red would be a false "detection" and is worse than no probe.
155
- {
156
- name: 'meeting-never-finalized',
157
- ref: 'prompt-v5-integrity.test.mjs',
158
- guarantee: '⑲ a meeting that collected every input must actually finalize and write its minutes',
159
- from: " finalizeLock = 'meeting'\n try {\n await finalizeMeeting(meeting)",
160
- to: " finalizeLock = 'meeting'\n try {\n meeting = null",
161
- },
162
-
163
- // ── PROMPT / INTERACTION INTEGRITY PROBES ────────────────────────────────
164
- // These are the probes that would have caught the 2026-09 field-test bug. Every one
165
- // of them breaks something a member READS (identity, roster, framing, persona), and
166
- // prompt-v5-integrity.test.mjs must go RED for each.
167
- {
168
- name: 'brief-names-the-last-woken-member',
169
- ref: 'prompt-v5-integrity.test.mjs',
170
- guarantee: '⑳ a prompt\'s [状态] block must name the member it is sent to (the exact field bug: briefs named the previous member)',
171
- from: " function stateBlock(member) {\n return briefBlock(member)\n }",
172
- to: " function stateBlock(member) {\n return briefBlock(memberById(currentMember) || activeMembers()[0] || member)\n }",
173
- },
174
- {
175
- name: 'joiner-absent-from-own-roster',
176
- ref: 'prompt-v5-integrity.test.mjs',
177
- guarantee: '⑳ a founding brief must show the roster INCLUDING its reader (roster committed before the prompt is built)',
178
- from: " member.phase = 'active'\n member.childId = ''\n await putMember(member)",
179
- to: " member.childId = ''\n await putMember(member)",
180
- },
181
- {
182
- name: 'inbox-message-delivered-twice',
183
- ref: 'prompt-v5-integrity.test.mjs',
184
- guarantee: '㉑ one prompt must not deliver the same message twice',
185
- from: " if (pending.length) await ackPending(pending)\n const base = typeof baseFn === 'function' ? baseFn() : baseFn",
186
- to: " const base = typeof baseFn === 'function' ? baseFn() : baseFn\n if (pending.length) await ackPending(pending)",
187
- },
188
- {
189
- name: 'charter-rewritten-on-resume',
190
- ref: 'prompt-v5-integrity.test.mjs',
191
- guarantee: '㉒ the charter is frozen at hire; a resume must not rewrite the induction snapshot',
192
- from: " const persona = member.persona || memberPersona(member)",
193
- to: " const persona = memberPersona(member)",
194
- },
195
- {
196
- name: 'resume-framed-as-induction',
197
- ref: 'prompt-v5-integrity.test.mjs',
198
- guarantee: '㉓ a rebuilt session must not be told it just joined the institute',
199
- from: " const resume = mode === 'resume'",
200
- to: " const resume = false",
201
- },
202
- {
203
- name: 'meeting-proposal-misattributed',
204
- ref: 'prompt-v5-integrity.test.mjs',
205
- guarantee: '㉔ a relayed message must carry its TRUE sender, never the last-woken member',
206
- from: " await say(callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })",
207
- to: " await say(academicianId() || callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })",
208
- },
209
- {
210
- name: 'office-impersonates-a-member',
211
- ref: 'prompt-v5-integrity.test.mjs',
212
- guarantee: '㉕ the office/host caller must resolve to the office, not to a guessed member',
213
- // The FAITHFUL regression mutation: restore the old "answer with whoever this session
214
- // woke last" fallback, which made the office's own assignments resolve to a random
215
- // researcher and be refused as V5_NOT_ACADEMICIAN.
216
- from: " try { if (rootOf(agent) === agent) return 'office' } catch (e) { /* fall through */ }",
217
- to: " if (currentMember && memberById(currentMember)) return currentMember",
218
- },
219
- {
220
- name: 'office-assignment-framed-as-academician',
221
- ref: 'prompt-v5-integrity.test.mjs',
222
- guarantee: '㉖ an assignment must be framed by its true origin (office vs academician)',
223
- from: " if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text",
224
- to: " if (m.kind === 'assign') return '【院士分派】' + m.text",
225
- },
226
- {
227
- name: 'nudge-mislabelled-as-assignment',
228
- ref: 'prompt-v5-integrity.test.mjs',
229
- guarantee: '㉗ a nudge is supervision, not an assignment, and must say so',
230
- from: " to, kind: 'nudge',",
231
- to: " to, kind: 'assign',",
232
- },
233
- {
234
- name: 'framework-notice-sent-as-self-message',
235
- ref: 'prompt-v5-integrity.test.mjs',
236
- guarantee: '㉘ framework feedback must actually reach the member (not be refused as a self-message)',
237
- from: " return await say('framework', { to: memberId, kind: 'notice', text: String(text) })",
238
- to: " return await say(memberId, { to: memberId, kind: 'notice', text: String(text) })",
239
- },
240
- {
241
- name: 'failed-member-hidden',
242
- ref: 'prompt-v5-integrity.test.mjs',
243
- guarantee: '㉙ a member that failed to provision must be visible in [未就位]',
244
- from: " if (absent.length) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))",
245
- to: " if (false) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))",
246
- },
247
- {
248
- name: 'leaderless-charter-invents-a-leader',
249
- ref: 'prompt-v5-integrity.test.mjs',
250
- guarantee: '㉚ with academician:false no charter may name a leader who does not exist',
251
- from: " const a = academicianId()\n const L = [",
252
- to: " const a = 'acad'\n const L = [",
253
- },
254
- {
255
- name: 'reply-spec-hides-the-objection-channel',
256
- ref: 'prompt-v5-integrity.test.mjs',
257
- guarantee: '㉛ every field the framework honours must be documented in the reply spec',
258
- from: " L.push(' \"reject_assign\": {\"task_id\":\"t-3\",\"why\":\"你对这项分派的异议理由\"}",
259
- to: " if (false) L.push(' \"reject_assign\": {\"task_id\":\"t-3\",\"why\":\"你对这项分派的异议理由\"}",
260
- },
261
- {
262
- name: 'task-owner-rewoken-unpaced',
263
- ref: 'prompt-v5-integrity.test.mjs',
264
- guarantee: '㉜ a task owner must be pushed on a paced cadence, not in an unbounded tight loop',
265
- from: " if ((now() - (lastActiveAt.get(m.id) || 0)) < idleMs) continue",
266
- to: " if (false) continue",
267
- },
268
- {
269
- name: 'verify-preempts-a-live-meeting',
270
- ref: 'prompt-v5-integrity.test.mjs',
271
- guarantee: '㉝ a verification proposed during a meeting must QUEUE, never start concurrently (the two are mutually exclusive)',
272
- from: " if (meeting) return\n // Only one begin may be in flight.",
273
- to: " // Only one begin may be in flight.",
274
- },
275
-
276
- // ── LEAN FORMAL VERIFICATION PROBES (docs/formal-verification.md) ─────────
277
- {
278
- name: 'formal-off-is-not-a-no-op',
279
- ref: 'formal-verify-v5.test.mjs',
280
- guarantee: '㉞ the default mode must be a TRUE no-op (no Lean text, no gate)',
281
- from: " const formalOn = () => formalMode() !== 'off'",
282
- to: " const formalOn = () => true",
283
- },
284
- {
285
- name: 'unknown-formal-mode-upgrades',
286
- ref: 'formal-verify-v5.test.mjs',
287
- guarantee: '㉟ an unknown mode must degrade to off, never to a STRONGER mode (a typo must not force formalization)',
288
- from: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'",
289
- to: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'require'",
290
- },
291
- {
292
- name: 'fidelity-switch-removed',
293
- ref: 'formal-verify-v5.test.mjs',
294
- guarantee: '㊱ a passing Lean run must switch the voting prompt to a FIDELITY review (the whole point of the feature)',
295
- from: " if (rec.status === 'passed') {\n // The whole point of the feature: the review subject CHANGES.",
296
- to: " if (false) {\n // The whole point of the feature: the review subject CHANGES.",
297
- },
298
- {
299
- name: 'require-gate-removed',
300
- ref: 'formal-verify-v5.test.mjs',
301
- guarantee: '㊲ require mode must withhold a true/false verdict until the object is Lean-passed or explicitly blocked',
302
- from: " if (formalMode() === 'require' && !formalGateOk(rec)) {",
303
- to: " if (false) {",
304
- },
305
- {
306
- name: 'blocked-note-not-required',
307
- ref: 'formal-verify-v5.test.mjs',
308
- guarantee: '㊳ a "we judged it infeasible" record must carry a reason (the difficulty decision must be auditable, not silent)',
309
- from: " if (!note) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
310
- to: " if (false) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
311
- },
312
- {
313
- name: 'proof-not-archived-under-verified',
314
- ref: 'formal-verify-v5.test.mjs',
315
- guarantee: '㊴ a passing proof must be archived as that object\'s proof (Verified/Lean/<id>.lean)',
316
- from: " if (passed) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
317
- to: " if (false) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
318
- },
319
- {
320
- name: 'reusable-lib-written-inside-the-institute',
321
- ref: 'formal-verify-v5.test.mjs',
322
- guarantee: '㊵ reuse must be CROSS-PROJECT: reusable definitions go to the global Formal/Lib, not inside one institute',
323
- from: " const okWrite = await writeTextAbs(instRootless(rel), body)",
324
- to: " const okWrite = await writeTextRel(rel, body)",
325
- },
326
- {
327
- name: 'lean-path-guard-naive',
328
- ref: 'formal-verify-v5.test.mjs',
329
- guarantee: '㊶ the Lean path guard must normalise .. (a string prefix check lets a traversal through)',
330
- from: " const abs = leanAbsPath(rel)\n if (abs === null) {",
331
- to: " const abs = (rel.charAt(0) === '/' || /^[a-z]:/i.test(rel)) ? rel.replace(/\\\\/g, '/') : instRoot() + '/' + rel\n if (abs.indexOf(vibeRoot() + '/') !== 0) {",
332
- },
333
- {
334
- name: 'lean-tools-not-registered',
335
- ref: 'formal-verify-v5.test.mjs',
336
- guarantee: '㊷ the three Lean tools must be registered (agents can only formalize if the tools exist)',
337
- from: " registerTool('vibe_v5_lean_run',",
338
- to: " if (false) registerTool('vibe_v5_lean_run',",
339
- },
340
- ]
341
-
342
- let probesPassed = 0
343
- let probesFailed = 0
344
- console.log('-- V5 sensitivity probes --')
345
- console.log('(a probe passes when breaking the guarantee turns the suite RED)')
346
- console.log('')
347
-
348
- for (const p of probes) {
349
- const occurrences = original.split(p.from).length - 1
350
- if (occurrences !== 1) {
351
- console.error(' SETUP-FAIL - ' + p.name + ': anchor matched ' + occurrences + ' times (need exactly 1)')
352
- probesFailed++
353
- continue
354
- }
355
- const mutated = original.replace(p.from, p.to)
356
- const file = join(dir, p.name + '.js')
357
- writeFileSync(file, mutated, 'utf8')
358
- const testPath = TESTS[p.ref]
359
- if (testPath === undefined) {
360
- console.error(' SETUP-FAIL - ' + p.name + ': ref "' + p.ref + '" is not a known suite')
361
- probesFailed++
362
- continue
363
- }
364
- const r = spawnSync(process.execPath, [testPath], {
365
- env: Object.assign({}, process.env, { V5_PLUGIN: file }),
366
- encoding: 'utf8',
367
- cwd: REPO,
368
- })
369
- const red = r.status !== 0
370
- if (red) {
371
- probesPassed++
372
- console.log(' ok - ' + p.name + ' [' + p.ref + '] => suite went RED as required [' + p.guarantee + ']')
373
- } else {
374
- probesFailed++
375
- console.error(' BLIND SPOT - ' + p.name + ' [' + p.ref + '] => suite stayed GREEN, so it does NOT detect: ' + p.guarantee)
376
- }
377
- }
378
-
379
- rmSync(dir, { recursive: true, force: true })
380
- console.log('')
381
- console.log('sensitivity: ' + probesPassed + ' probes detected the break, ' + probesFailed + ' blind spots')
382
- if (probesFailed) process.exit(1)
383
- console.log('ALL PROBES RED AS REQUIRED')
384
- 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')
31
+ const REPO = fileURLToPath(new URL('..', import.meta.url))
32
+ const dir = mkdtempSync(join(tmpdir(), 'v5-sens-'))
33
+
34
+ // Each probe: { name, ref, guarantee, from, to }
35
+ // `from` must occur exactly once, so a mutation can never quietly hit the wrong site.
36
+ const probes = [
37
+ {
38
+ // The m floor is enforced by THREE cooperating checks (an early `bTrue + bFalse < m`
39
+ // return, the conflict check, and a final `bTrue >= m`), so a single weakened check is
40
+ // masked by the others and is a SEMANTICALLY INERT mutation — it must never be used as
41
+ // a probe (it would look like a "blind spot"). Forcing the quorum itself to 1 breaks
42
+ // the rule on every path at once, which is exactly the guarantee under test.
43
+ name: 'quorum-forced-to-one',
44
+ ref: 'selfdrive-v5.mjs',
45
+ guarantee: '④ only >= m boolean votes may verify (m must be min(quorumCap, |voters|))',
46
+ from: " return Math.max(1, Math.min(cap, voterCount()))",
47
+ to: " return 1",
48
+ },
49
+ {
50
+ name: 'abstention-counts-as-true',
51
+ ref: 'selfdrive-v5.mjs',
52
+ guarantee: '⑤ an abstention must NOT count toward the quorum',
53
+ from: " else if (p === 0) bFalse += 1\n else abstain += 1",
54
+ to: " else if (p === 0) bFalse += 1\n else { abstain += 1; bTrue += 1 }",
55
+ },
56
+ {
57
+ // `judgeVerdict` checks the m floor, then the conflict, then the floor again, and each
58
+ // guard masks the next — so deleting a guard is inert. The behaviour-changing break is
59
+ // letting a CONFLICT pass as a verdict, which is what this mutation does.
60
+ name: 'conflict-allowed',
61
+ ref: 'selfdrive-v5.mjs',
62
+ guarantee: '④ a conflicting 1 vs 0 must BLOCK the verdict',
63
+ from: " return Object.assign(base, { outcome: 'undecided', reason: 'conflicting assertions (true=' + bTrue + ', false=' + bFalse + ')' })",
64
+ to: " return Object.assign(base, { outcome: 'true', reason: 'conflicting assertions (true=' + bTrue + ', false=' + bFalse + ')' })",
65
+ },
66
+ {
67
+ name: 'temp-worker-can-vote',
68
+ ref: 'selfdrive-v5.mjs',
69
+ guarantee: '⑦ a temp worker must NOT be able to vote',
70
+ from: " if (member.kind === 'temp') {\n // Temp workers have no vote",
71
+ to: " if (false) {\n // Temp workers have no vote",
72
+ },
73
+ {
74
+ name: 'academician-gate-removed',
75
+ ref: 'selfdrive-v5.mjs',
76
+ guarantee: '⑬ only the academician may assign tasks',
77
+ from: " return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can assign tasks' }",
78
+ to: " return { ok: false, code: 'NO_GATE', message: 'x' }",
79
+ },
80
+ {
81
+ name: 'spawn-not-registered-inflight',
82
+ ref: 'selfdrive-v5.mjs',
83
+ guarantee: '⑯ the founding turn must be registered in-flight or its end is dropped',
84
+ from: " inflight.set(started.childId, shortId())",
85
+ to: "",
86
+ },
87
+ {
88
+ name: 'group-chat-not-fanned-out',
89
+ ref: 'selfdrive-v5.mjs',
90
+ guarantee: '③ group chat must reach every other member',
91
+ from: " if (to === 'all' || to === '') targets = activeMembers().filter((m) => m.id !== from)",
92
+ to: " if (to === 'all' || to === '') targets = activeMembers().filter((m) => m.id !== from).slice(0, 1)",
93
+ },
94
+ {
95
+ name: 'verify-round-advances-anyway',
96
+ ref: 'selfdrive-v5.mjs',
97
+ guarantee: '④ a round must not advance without every voter answering',
98
+ from: " const missing = need.filter((id) => !vs.votes[id])\n if (missing.length) {",
99
+ to: " const missing = need.filter((id) => !vs.votes[id])\n if (false) {",
100
+ },
101
+ {
102
+ name: 'fire-does-not-release',
103
+ ref: 'selfdrive-v5.mjs',
104
+ guarantee: '⑨ firing must really release the resident child',
105
+ from: " if (typeof subagents.drainContinuableChildren === 'function') await subagents.drainContinuableChildren(rootAgent, [target.childId])",
106
+ to: " if (false) await subagents.drainContinuableChildren(rootAgent, [target.childId])",
107
+ },
108
+ {
109
+ name: 'solve-vote-not-unanimous',
110
+ ref: 'selfdrive-v5.mjs',
111
+ guarantee: '⑥ the institute must not stop unless EVERY voter agrees',
112
+ from: " if (!vs.every((id) => solveVotes.get(id) === true)) return false",
113
+ to: " if (false) return false",
114
+ },
115
+ // ── probes added after the round-2 audit found these defects ──────────────
116
+ {
117
+ name: 'fallback-state-never-loaded',
118
+ ref: 'e2e-v5-round2.test.mjs',
119
+ guarantee: '⑰ the file fallback must LOAD persisted state before any read (restart safety)',
120
+ from: " if (backend.kind === 'file' && typeof backend.load === 'function') await backend.load()",
121
+ to: " if (false) await backend.load()",
122
+ },
123
+ {
124
+ name: 'solve-vote-never-re-evaluated',
125
+ ref: 'prompt-v5-integrity.test.mjs',
126
+ guarantee: '⑥ a solve vote landing OUTSIDE a meeting must still stop the institute',
127
+ // The anchor must include the CALL. An `if (false)` inserted before the comment would
128
+ // leave the real `await checkSolved()` below it untouched — an inert mutation that
129
+ // would masquerade as a blind spot.
130
+ 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()",
131
+ to: " solveVotes.set(memberId, val === true)",
132
+ },
133
+ {
134
+ name: 'proposal-only-kicks-scheduler',
135
+ ref: 'selfdrive-v5.mjs',
136
+ guarantee: '④ a proposal must actually START, not just ask the scheduler to try',
137
+ from: " await armNextVerify()\n if (!hasVerifyInFlight()) await scheduleNext()",
138
+ to: " await scheduleNext()",
139
+ },
140
+ {
141
+ name: 'begin-not-exclusive',
142
+ ref: 'prompt-v5-integrity.test.mjs',
143
+ guarantee: '⑱ a second proposal must QUEUE, never start a concurrent ballot',
144
+ from: " if (beginLock) return\n if (currentVerify()) return\n beginLock = true",
145
+ to: " beginLock = true",
146
+ },
147
+ // NOTE — a probe for `continueMeetingRound`'s `if (finalizeLock) { armHeartbeat(); return }`
148
+ // re-arm was REMOVED, not because the re-arm is unnecessary but because the state it
149
+ // guards is UNREACHABLE, so no black-box probe can detect its removal:
150
+ // · `schedulePass` handles a live meeting BEFORE it looks at a verification, and
151
+ // · `startMeeting` parks any meeting while `hasVerifyInFlight()`,
152
+ // so a meeting and a running verify settlement can never overlap. The re-arm stays in
153
+ // the code as cheap insurance against a future ordering change; leaving a probe that can
154
+ // never go red would be a false "detection" and is worse than no probe.
155
+ {
156
+ name: 'meeting-never-finalized',
157
+ ref: 'prompt-v5-integrity.test.mjs',
158
+ guarantee: '⑲ a meeting that collected every input must actually finalize and write its minutes',
159
+ from: " finalizeLock = 'meeting'\n try {\n await finalizeMeeting(meeting)",
160
+ to: " finalizeLock = 'meeting'\n try {\n meeting = null",
161
+ },
162
+
163
+ // ── PROMPT / INTERACTION INTEGRITY PROBES ────────────────────────────────
164
+ // These are the probes that would have caught the 2026-09 field-test bug. Every one
165
+ // of them breaks something a member READS (identity, roster, framing, persona), and
166
+ // prompt-v5-integrity.test.mjs must go RED for each.
167
+ {
168
+ name: 'brief-names-the-last-woken-member',
169
+ ref: 'prompt-v5-integrity.test.mjs',
170
+ guarantee: '⑳ a prompt\'s [状态] block must name the member it is sent to (the exact field bug: briefs named the previous member)',
171
+ from: " function stateBlock(member) {\n return briefBlock(member)\n }",
172
+ to: " function stateBlock(member) {\n return briefBlock(memberById(currentMember) || activeMembers()[0] || member)\n }",
173
+ },
174
+ {
175
+ name: 'joiner-absent-from-own-roster',
176
+ ref: 'prompt-v5-integrity.test.mjs',
177
+ guarantee: '⑳ a founding brief must show the roster INCLUDING its reader (roster committed before the prompt is built)',
178
+ from: " member.phase = 'active'\n member.childId = ''\n await putMember(member)",
179
+ to: " member.childId = ''\n await putMember(member)",
180
+ },
181
+ {
182
+ name: 'inbox-message-delivered-twice',
183
+ ref: 'prompt-v5-integrity.test.mjs',
184
+ guarantee: '㉑ one prompt must not deliver the same message twice',
185
+ from: " if (pending.length) await ackPending(pending)\n const base = typeof baseFn === 'function' ? baseFn() : baseFn",
186
+ to: " const base = typeof baseFn === 'function' ? baseFn() : baseFn\n if (pending.length) await ackPending(pending)",
187
+ },
188
+ {
189
+ name: 'charter-rewritten-on-resume',
190
+ ref: 'prompt-v5-integrity.test.mjs',
191
+ guarantee: '㉒ the charter is frozen at hire; a resume must not rewrite the induction snapshot',
192
+ from: " const persona = member.persona || memberPersona(member)",
193
+ to: " const persona = memberPersona(member)",
194
+ },
195
+ {
196
+ name: 'resume-framed-as-induction',
197
+ ref: 'prompt-v5-integrity.test.mjs',
198
+ guarantee: '㉓ a rebuilt session must not be told it just joined the institute',
199
+ from: " const resume = mode === 'resume'",
200
+ to: " const resume = false",
201
+ },
202
+ {
203
+ name: 'meeting-proposal-misattributed',
204
+ ref: 'prompt-v5-integrity.test.mjs',
205
+ guarantee: '㉔ a relayed message must carry its TRUE sender, never the last-woken member',
206
+ from: " await say(callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })",
207
+ to: " await say(academicianId() || callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })",
208
+ },
209
+ {
210
+ name: 'office-impersonates-a-member',
211
+ ref: 'prompt-v5-integrity.test.mjs',
212
+ guarantee: '㉕ the office/host caller must resolve to the office, not to a guessed member',
213
+ // The FAITHFUL regression mutation: restore the old "answer with whoever this session
214
+ // woke last" fallback, which made the office's own assignments resolve to a random
215
+ // researcher and be refused as V5_NOT_ACADEMICIAN.
216
+ from: " try { if (rootOf(agent) === agent) return 'office' } catch (e) { /* fall through */ }",
217
+ to: " if (currentMember && memberById(currentMember)) return currentMember",
218
+ },
219
+ {
220
+ name: 'office-assignment-framed-as-academician',
221
+ ref: 'prompt-v5-integrity.test.mjs',
222
+ guarantee: '㉖ an assignment must be framed by its true origin (office vs academician)',
223
+ from: " if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text",
224
+ to: " if (m.kind === 'assign') return '【院士分派】' + m.text",
225
+ },
226
+ {
227
+ name: 'nudge-mislabelled-as-assignment',
228
+ ref: 'prompt-v5-integrity.test.mjs',
229
+ guarantee: '㉗ a nudge is supervision, not an assignment, and must say so',
230
+ from: " to, kind: 'nudge',",
231
+ to: " to, kind: 'assign',",
232
+ },
233
+ {
234
+ name: 'framework-notice-sent-as-self-message',
235
+ ref: 'prompt-v5-integrity.test.mjs',
236
+ guarantee: '㉘ framework feedback must actually reach the member (not be refused as a self-message)',
237
+ from: " return await say('framework', { to: memberId, kind: 'notice', text: String(text) })",
238
+ to: " return await say(memberId, { to: memberId, kind: 'notice', text: String(text) })",
239
+ },
240
+ {
241
+ name: 'failed-member-hidden',
242
+ ref: 'prompt-v5-integrity.test.mjs',
243
+ guarantee: '㉙ a member that failed to provision must be visible in [未就位]',
244
+ from: " if (absent.length) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))",
245
+ to: " if (false) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))",
246
+ },
247
+ {
248
+ name: 'leaderless-charter-invents-a-leader',
249
+ ref: 'prompt-v5-integrity.test.mjs',
250
+ guarantee: '㉚ with academician:false no charter may name a leader who does not exist',
251
+ from: " const a = academicianId()\n const L = [",
252
+ to: " const a = 'acad'\n const L = [",
253
+ },
254
+ {
255
+ name: 'reply-spec-hides-the-objection-channel',
256
+ ref: 'prompt-v5-integrity.test.mjs',
257
+ guarantee: '㉛ every field the framework honours must be documented in the reply spec',
258
+ from: " L.push(' \"reject_assign\": {\"task_id\":\"t-3\",\"why\":\"你对这项分派的异议理由\"}",
259
+ to: " if (false) L.push(' \"reject_assign\": {\"task_id\":\"t-3\",\"why\":\"你对这项分派的异议理由\"}",
260
+ },
261
+ {
262
+ name: 'task-owner-rewoken-unpaced',
263
+ ref: 'prompt-v5-integrity.test.mjs',
264
+ guarantee: '㉜ a task owner must be pushed on a paced cadence, not in an unbounded tight loop',
265
+ from: " if ((now() - (lastActiveAt.get(m.id) || 0)) < idleMs) continue",
266
+ to: " if (false) continue",
267
+ },
268
+ {
269
+ name: 'verify-preempts-a-live-meeting',
270
+ ref: 'prompt-v5-integrity.test.mjs',
271
+ guarantee: '㉝ a verification proposed during a meeting must QUEUE, never start concurrently (the two are mutually exclusive)',
272
+ from: " if (meeting) return\n // Only one begin may be in flight.",
273
+ to: " // Only one begin may be in flight.",
274
+ },
275
+
276
+ // ── LEAN FORMAL VERIFICATION PROBES (docs/formal-verification.md) ─────────
277
+ {
278
+ name: 'formal-off-is-not-a-no-op',
279
+ ref: 'formal-verify-v5.test.mjs',
280
+ guarantee: '㉞ the default mode must be a TRUE no-op (no Lean text, no gate)',
281
+ from: " const formalOn = () => formalMode() !== 'off'",
282
+ to: " const formalOn = () => true",
283
+ },
284
+ {
285
+ name: 'unknown-formal-mode-upgrades',
286
+ ref: 'formal-verify-v5.test.mjs',
287
+ guarantee: '㉟ an unknown mode must degrade to off, never to a STRONGER mode (a typo must not force formalization)',
288
+ from: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'off'",
289
+ to: " out.formalVerify = ['off', 'encourage', 'require'].indexOf(out.formalVerify) !== -1 ? out.formalVerify : 'require'",
290
+ },
291
+ {
292
+ name: 'fidelity-switch-removed',
293
+ ref: 'formal-verify-v5.test.mjs',
294
+ guarantee: '㊱ a passing Lean run must switch the voting prompt to a FIDELITY review (the whole point of the feature)',
295
+ from: " if (rec.status === 'passed') {\n // The whole point of the feature: the review subject CHANGES.",
296
+ to: " if (false) {\n // The whole point of the feature: the review subject CHANGES.",
297
+ },
298
+ {
299
+ name: 'require-gate-removed',
300
+ ref: 'formal-verify-v5.test.mjs',
301
+ guarantee: '㊲ require mode must withhold a true/false verdict until the object is Lean-passed or explicitly blocked',
302
+ from: " if (formalMode() === 'require' && !formalGateOk(rec)) {",
303
+ to: " if (false) {",
304
+ },
305
+ {
306
+ name: 'blocked-note-not-required',
307
+ ref: 'formal-verify-v5.test.mjs',
308
+ guarantee: '㊳ a "we judged it infeasible" record must carry a reason (the difficulty decision must be auditable, not silent)',
309
+ from: " if (!note) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
310
+ to: " if (false) return { ok: false, code: 'V5_INVALID_ARGUMENT', message: '阻塞记录必须写明原因(note)",
311
+ },
312
+ {
313
+ name: 'proof-not-archived-under-verified',
314
+ ref: 'formal-verify-v5.test.mjs',
315
+ guarantee: '㊴ a passing proof must be archived as that object\'s proof (Verified/Lean/<id>.lean)',
316
+ from: " if (passed) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
317
+ to: " if (false) await writeTextRel('Verified/Lean/' + target + '.lean', body)",
318
+ },
319
+ {
320
+ name: 'reusable-lib-written-inside-the-institute',
321
+ ref: 'formal-verify-v5.test.mjs',
322
+ guarantee: '㊵ reuse must be CROSS-PROJECT: reusable definitions go to the global Formal/Lib, not inside one institute',
323
+ from: " const okWrite = await writeTextAbs(instRootless(rel), body)",
324
+ to: " const okWrite = await writeTextRel(rel, body)",
325
+ },
326
+ {
327
+ name: 'lean-path-guard-naive',
328
+ ref: 'formal-verify-v5.test.mjs',
329
+ guarantee: '㊶ the Lean path guard must normalise .. (a string prefix check lets a traversal through)',
330
+ from: " const abs = leanAbsPath(rel)\n if (abs === null) {",
331
+ to: " const abs = (rel.charAt(0) === '/' || /^[a-z]:/i.test(rel)) ? rel.replace(/\\\\/g, '/') : instRoot() + '/' + rel\n if (abs.indexOf(vibeRoot() + '/') !== 0) {",
332
+ },
333
+ {
334
+ name: 'lean-tools-not-registered',
335
+ ref: 'formal-verify-v5.test.mjs',
336
+ guarantee: '㊷ the three Lean tools must be registered (agents can only formalize if the tools exist)',
337
+ from: " registerTool('vibe_v5_lean_run',",
338
+ to: " if (false) registerTool('vibe_v5_lean_run',",
339
+ },
340
+ ]
341
+
342
+ let probesPassed = 0
343
+ let probesFailed = 0
344
+ console.log('-- V5 sensitivity probes --')
345
+ console.log('(a probe passes when breaking the guarantee turns the suite RED)')
346
+ console.log('')
347
+
348
+ for (const p of probes) {
349
+ const occurrences = original.split(p.from).length - 1
350
+ if (occurrences !== 1) {
351
+ console.error(' SETUP-FAIL - ' + p.name + ': anchor matched ' + occurrences + ' times (need exactly 1)')
352
+ probesFailed++
353
+ continue
354
+ }
355
+ const mutated = original.replace(p.from, p.to)
356
+ const file = join(dir, p.name + '.js')
357
+ writeFileSync(file, mutated, 'utf8')
358
+ const testPath = TESTS[p.ref]
359
+ if (testPath === undefined) {
360
+ console.error(' SETUP-FAIL - ' + p.name + ': ref "' + p.ref + '" is not a known suite')
361
+ probesFailed++
362
+ continue
363
+ }
364
+ const r = spawnSync(process.execPath, [testPath], {
365
+ env: Object.assign({}, process.env, { V5_PLUGIN: file }),
366
+ encoding: 'utf8',
367
+ cwd: REPO,
368
+ })
369
+ const red = r.status !== 0
370
+ if (red) {
371
+ probesPassed++
372
+ console.log(' ok - ' + p.name + ' [' + p.ref + '] => suite went RED as required [' + p.guarantee + ']')
373
+ } else {
374
+ probesFailed++
375
+ console.error(' BLIND SPOT - ' + p.name + ' [' + p.ref + '] => suite stayed GREEN, so it does NOT detect: ' + p.guarantee)
376
+ }
377
+ }
378
+
379
+ rmSync(dir, { recursive: true, force: true })
380
+ console.log('')
381
+ console.log('sensitivity: ' + probesPassed + ' probes detected the break, ' + probesFailed + ' blind spots')
382
+ if (probesFailed) process.exit(1)
383
+ console.log('ALL PROBES RED AS REQUIRED')
384
+ process.exit(0)