dsh-vibe-math 2.1.0 → 2.2.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.
@@ -0,0 +1,306 @@
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
+ const TESTS = {
24
+ selfdrive: fileURLToPath(new URL('./selfdrive-v5.mjs', import.meta.url)),
25
+ round2: fileURLToPath(new URL('./e2e-v5-round2.test.mjs', import.meta.url)),
26
+ prompt: fileURLToPath(new URL('./prompt-v5-integrity.test.mjs', import.meta.url)),
27
+ }
28
+ const original = readFileSync(SRC, 'utf8')
29
+ const REPO = fileURLToPath(new URL('.', import.meta.url))
30
+ const dir = mkdtempSync(join(tmpdir(), 'v5-sens-'))
31
+
32
+ // Each probe: { name, ref, guarantee, from, to }
33
+ // `from` must occur exactly once, so a mutation can never quietly hit the wrong site.
34
+ const probes = [
35
+ {
36
+ // The m floor is enforced by THREE cooperating checks (an early `bTrue + bFalse < m`
37
+ // return, the conflict check, and a final `bTrue >= m`), so a single weakened check is
38
+ // masked by the others and is a SEMANTICALLY INERT mutation — it must never be used as
39
+ // a probe (it would look like a "blind spot"). Forcing the quorum itself to 1 breaks
40
+ // the rule on every path at once, which is exactly the guarantee under test.
41
+ name: 'quorum-forced-to-one',
42
+ ref: 'selfdrive-v5.mjs',
43
+ guarantee: '④ only >= m boolean votes may verify (m must be min(quorumCap, |voters|))',
44
+ from: " return Math.max(1, Math.min(cap, voterCount()))",
45
+ to: " return 1",
46
+ },
47
+ {
48
+ name: 'abstention-counts-as-true',
49
+ ref: 'selfdrive-v5.mjs',
50
+ guarantee: '⑤ an abstention must NOT count toward the quorum',
51
+ from: " else if (p === 0) bFalse += 1\n else abstain += 1",
52
+ to: " else if (p === 0) bFalse += 1\n else { abstain += 1; bTrue += 1 }",
53
+ },
54
+ {
55
+ // `judgeVerdict` checks the m floor, then the conflict, then the floor again, and each
56
+ // guard masks the next — so deleting a guard is inert. The behaviour-changing break is
57
+ // letting a CONFLICT pass as a verdict, which is what this mutation does.
58
+ name: 'conflict-allowed',
59
+ ref: 'selfdrive-v5.mjs',
60
+ guarantee: '④ a conflicting 1 vs 0 must BLOCK the verdict',
61
+ from: " return Object.assign(base, { outcome: 'undecided', reason: 'conflicting assertions (true=' + bTrue + ', false=' + bFalse + ')' })",
62
+ to: " return Object.assign(base, { outcome: 'true', reason: 'conflicting assertions (true=' + bTrue + ', false=' + bFalse + ')' })",
63
+ },
64
+ {
65
+ name: 'temp-worker-can-vote',
66
+ ref: 'selfdrive-v5.mjs',
67
+ guarantee: '⑦ a temp worker must NOT be able to vote',
68
+ from: " if (member.kind === 'temp') {\n // Temp workers have no vote",
69
+ to: " if (false) {\n // Temp workers have no vote",
70
+ },
71
+ {
72
+ name: 'academician-gate-removed',
73
+ ref: 'selfdrive-v5.mjs',
74
+ guarantee: '⑬ only the academician may assign tasks',
75
+ from: " return { ok: false, code: 'V5_NOT_ACADEMICIAN', message: 'only the academician (or the office) can assign tasks' }",
76
+ to: " return { ok: false, code: 'NO_GATE', message: 'x' }",
77
+ },
78
+ {
79
+ name: 'spawn-not-registered-inflight',
80
+ ref: 'selfdrive-v5.mjs',
81
+ guarantee: '⑯ the founding turn must be registered in-flight or its end is dropped',
82
+ from: " inflight.set(started.childId, shortId())",
83
+ to: "",
84
+ },
85
+ {
86
+ name: 'group-chat-not-fanned-out',
87
+ ref: 'selfdrive-v5.mjs',
88
+ guarantee: '③ group chat must reach every other member',
89
+ from: " if (to === 'all' || to === '') targets = activeMembers().filter((m) => m.id !== from)",
90
+ to: " if (to === 'all' || to === '') targets = activeMembers().filter((m) => m.id !== from).slice(0, 1)",
91
+ },
92
+ {
93
+ name: 'verify-round-advances-anyway',
94
+ ref: 'selfdrive-v5.mjs',
95
+ guarantee: '④ a round must not advance without every voter answering',
96
+ from: " const missing = need.filter((id) => !vs.votes[id])\n if (missing.length) {",
97
+ to: " const missing = need.filter((id) => !vs.votes[id])\n if (false) {",
98
+ },
99
+ {
100
+ name: 'fire-does-not-release',
101
+ ref: 'selfdrive-v5.mjs',
102
+ guarantee: '⑨ firing must really release the resident child',
103
+ from: " if (typeof subagents.drainContinuableChildren === 'function') await subagents.drainContinuableChildren(rootAgent, [target.childId])",
104
+ to: " if (false) await subagents.drainContinuableChildren(rootAgent, [target.childId])",
105
+ },
106
+ {
107
+ name: 'solve-vote-not-unanimous',
108
+ ref: 'selfdrive-v5.mjs',
109
+ guarantee: '⑥ the institute must not stop unless EVERY voter agrees',
110
+ from: " if (!vs.every((id) => solveVotes.get(id) === true)) return false",
111
+ to: " if (false) return false",
112
+ },
113
+ // ── probes added after the round-2 audit found these defects ──────────────
114
+ {
115
+ name: 'fallback-state-never-loaded',
116
+ ref: 'e2e-v5-round2.test.mjs',
117
+ guarantee: '⑰ the file fallback must LOAD persisted state before any read (restart safety)',
118
+ from: " if (backend.kind === 'file' && typeof backend.load === 'function') await backend.load()",
119
+ to: " if (false) await backend.load()",
120
+ },
121
+ {
122
+ name: 'solve-vote-never-re-evaluated',
123
+ ref: 'prompt-v5-integrity.test.mjs',
124
+ guarantee: '⑥ a solve vote landing OUTSIDE a meeting must still stop the institute',
125
+ // The anchor must include the CALL. An `if (false)` inserted before the comment would
126
+ // leave the real `await checkSolved()` below it untouched — an inert mutation that
127
+ // would masquerade as a blind spot.
128
+ 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()",
129
+ to: " solveVotes.set(memberId, val === true)",
130
+ },
131
+ {
132
+ name: 'proposal-only-kicks-scheduler',
133
+ ref: 'selfdrive-v5.mjs',
134
+ guarantee: '④ a proposal must actually START, not just ask the scheduler to try',
135
+ from: " await armNextVerify()\n if (!hasVerifyInFlight()) await scheduleNext()",
136
+ to: " await scheduleNext()",
137
+ },
138
+ {
139
+ name: 'begin-not-exclusive',
140
+ ref: 'prompt-v5-integrity.test.mjs',
141
+ guarantee: '⑱ a second proposal must QUEUE, never start a concurrent ballot',
142
+ from: " if (beginLock) return\n if (currentVerify()) return\n beginLock = true",
143
+ to: " beginLock = true",
144
+ },
145
+ // NOTE — a probe for `continueMeetingRound`'s `if (finalizeLock) { armHeartbeat(); return }`
146
+ // re-arm was REMOVED, not because the re-arm is unnecessary but because the state it
147
+ // guards is UNREACHABLE, so no black-box probe can detect its removal:
148
+ // · `schedulePass` handles a live meeting BEFORE it looks at a verification, and
149
+ // · `startMeeting` parks any meeting while `hasVerifyInFlight()`,
150
+ // so a meeting and a running verify settlement can never overlap. The re-arm stays in
151
+ // the code as cheap insurance against a future ordering change; leaving a probe that can
152
+ // never go red would be a false "detection" and is worse than no probe.
153
+ {
154
+ name: 'meeting-never-finalized',
155
+ ref: 'prompt-v5-integrity.test.mjs',
156
+ guarantee: '⑲ a meeting that collected every input must actually finalize and write its minutes',
157
+ from: " finalizeLock = 'meeting'\n try {\n await finalizeMeeting(meeting)",
158
+ to: " finalizeLock = 'meeting'\n try {\n meeting = null",
159
+ },
160
+
161
+ // ── PROMPT / INTERACTION INTEGRITY PROBES ────────────────────────────────
162
+ // These are the probes that would have caught the 2026-09 field-test bug. Every one
163
+ // of them breaks something a member READS (identity, roster, framing, persona), and
164
+ // prompt-v5-integrity.test.mjs must go RED for each.
165
+ {
166
+ name: 'brief-names-the-last-woken-member',
167
+ ref: 'prompt-v5-integrity.test.mjs',
168
+ guarantee: '⑳ a prompt\'s [状态] block must name the member it is sent to (the exact field bug: briefs named the previous member)',
169
+ from: " function stateBlock(member) {\n return briefBlock(member)\n }",
170
+ to: " function stateBlock(member) {\n return briefBlock(memberById(currentMember) || activeMembers()[0] || member)\n }",
171
+ },
172
+ {
173
+ name: 'joiner-absent-from-own-roster',
174
+ ref: 'prompt-v5-integrity.test.mjs',
175
+ guarantee: '⑳ a founding brief must show the roster INCLUDING its reader (roster committed before the prompt is built)',
176
+ from: " member.phase = 'active'\n member.childId = ''\n await putMember(member)",
177
+ to: " member.childId = ''\n await putMember(member)",
178
+ },
179
+ {
180
+ name: 'inbox-message-delivered-twice',
181
+ ref: 'prompt-v5-integrity.test.mjs',
182
+ guarantee: '㉑ one prompt must not deliver the same message twice',
183
+ from: " if (pending.length) await ackPending(pending)\n const base = typeof baseFn === 'function' ? baseFn() : baseFn",
184
+ to: " const base = typeof baseFn === 'function' ? baseFn() : baseFn\n if (pending.length) await ackPending(pending)",
185
+ },
186
+ {
187
+ name: 'charter-rewritten-on-resume',
188
+ ref: 'prompt-v5-integrity.test.mjs',
189
+ guarantee: '㉒ the charter is frozen at hire; a resume must not rewrite the induction snapshot',
190
+ from: " const persona = member.persona || memberPersona(member)",
191
+ to: " const persona = memberPersona(member)",
192
+ },
193
+ {
194
+ name: 'resume-framed-as-induction',
195
+ ref: 'prompt-v5-integrity.test.mjs',
196
+ guarantee: '㉓ a rebuilt session must not be told it just joined the institute',
197
+ from: " const resume = mode === 'resume'",
198
+ to: " const resume = false",
199
+ },
200
+ {
201
+ name: 'meeting-proposal-misattributed',
202
+ ref: 'prompt-v5-integrity.test.mjs',
203
+ guarantee: '㉔ a relayed message must carry its TRUE sender, never the last-woken member',
204
+ from: " await say(callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })",
205
+ to: " await say(academicianId() || callerId, { to: 'voters', kind: 'voters', text: '提议开会:「' + agenda + '」(' + kind + ')' })",
206
+ },
207
+ {
208
+ name: 'office-impersonates-a-member',
209
+ ref: 'prompt-v5-integrity.test.mjs',
210
+ guarantee: '㉕ the office/host caller must resolve to the office, not to a guessed member',
211
+ // The FAITHFUL regression mutation: restore the old "answer with whoever this session
212
+ // woke last" fallback, which made the office's own assignments resolve to a random
213
+ // researcher and be refused as V5_NOT_ACADEMICIAN.
214
+ from: " try { if (rootOf(agent) === agent) return 'office' } catch (e) { /* fall through */ }",
215
+ to: " if (currentMember && memberById(currentMember)) return currentMember",
216
+ },
217
+ {
218
+ name: 'office-assignment-framed-as-academician',
219
+ ref: 'prompt-v5-integrity.test.mjs',
220
+ guarantee: '㉖ an assignment must be framed by its true origin (office vs academician)',
221
+ from: " if (m.kind === 'assign') return (m.from === 'office' ? '【所办分派】' : '【院士分派】') + m.text",
222
+ to: " if (m.kind === 'assign') return '【院士分派】' + m.text",
223
+ },
224
+ {
225
+ name: 'nudge-mislabelled-as-assignment',
226
+ ref: 'prompt-v5-integrity.test.mjs',
227
+ guarantee: '㉗ a nudge is supervision, not an assignment, and must say so',
228
+ from: " to, kind: 'nudge',",
229
+ to: " to, kind: 'assign',",
230
+ },
231
+ {
232
+ name: 'framework-notice-sent-as-self-message',
233
+ ref: 'prompt-v5-integrity.test.mjs',
234
+ guarantee: '㉘ framework feedback must actually reach the member (not be refused as a self-message)',
235
+ from: " return await say('framework', { to: memberId, kind: 'notice', text: String(text) })",
236
+ to: " return await say(memberId, { to: memberId, kind: 'notice', text: String(text) })",
237
+ },
238
+ {
239
+ name: 'failed-member-hidden',
240
+ ref: 'prompt-v5-integrity.test.mjs',
241
+ guarantee: '㉙ a member that failed to provision must be visible in [未就位]',
242
+ from: " if (absent.length) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))",
243
+ to: " if (false) b.push('[未就位] ' + absent.map((m) => m.id + '(' + m.phase + ')').join('、'))",
244
+ },
245
+ {
246
+ name: 'leaderless-charter-invents-a-leader',
247
+ ref: 'prompt-v5-integrity.test.mjs',
248
+ guarantee: '㉚ with academician:false no charter may name a leader who does not exist',
249
+ from: " const a = academicianId()\n const L = [",
250
+ to: " const a = 'acad'\n const L = [",
251
+ },
252
+ {
253
+ name: 'reply-spec-hides-the-objection-channel',
254
+ ref: 'prompt-v5-integrity.test.mjs',
255
+ guarantee: '㉛ every field the framework honours must be documented in the reply spec',
256
+ from: " L.push(' \"reject_assign\": {\"task_id\":\"t-3\",\"why\":\"你对这项分派的异议理由\"}",
257
+ to: " if (false) L.push(' \"reject_assign\": {\"task_id\":\"t-3\",\"why\":\"你对这项分派的异议理由\"}",
258
+ },
259
+ {
260
+ name: 'task-owner-rewoken-unpaced',
261
+ ref: 'prompt-v5-integrity.test.mjs',
262
+ guarantee: '㉜ a task owner must be pushed on a paced cadence, not in an unbounded tight loop',
263
+ from: " if ((now() - (lastActiveAt.get(m.id) || 0)) < idleMs) continue",
264
+ to: " if (false) continue",
265
+ },
266
+ ]
267
+
268
+ let probesPassed = 0
269
+ let probesFailed = 0
270
+ console.log('-- V5 sensitivity probes --')
271
+ console.log('(a probe passes when breaking the guarantee turns the suite RED)')
272
+ console.log('')
273
+
274
+ for (const p of probes) {
275
+ const occurrences = original.split(p.from).length - 1
276
+ if (occurrences !== 1) {
277
+ console.error(' SETUP-FAIL - ' + p.name + ': anchor matched ' + occurrences + ' times (need exactly 1)')
278
+ probesFailed++
279
+ continue
280
+ }
281
+ const mutated = original.replace(p.from, p.to)
282
+ const file = join(dir, p.name + '.js')
283
+ writeFileSync(file, mutated, 'utf8')
284
+ const testPath = TESTS[p.ref === 'e2e-v5-round2.test.mjs' ? 'round2'
285
+ : p.ref === 'prompt-v5-integrity.test.mjs' ? 'prompt' : 'selfdrive']
286
+ const r = spawnSync(process.execPath, [testPath], {
287
+ env: Object.assign({}, process.env, { V5_PLUGIN: file }),
288
+ encoding: 'utf8',
289
+ cwd: REPO,
290
+ })
291
+ const red = r.status !== 0
292
+ if (red) {
293
+ probesPassed++
294
+ console.log(' ok - ' + p.name + ' [' + p.ref + '] => suite went RED as required [' + p.guarantee + ']')
295
+ } else {
296
+ probesFailed++
297
+ console.error(' BLIND SPOT - ' + p.name + ' [' + p.ref + '] => suite stayed GREEN, so it does NOT detect: ' + p.guarantee)
298
+ }
299
+ }
300
+
301
+ rmSync(dir, { recursive: true, force: true })
302
+ console.log('')
303
+ console.log('sensitivity: ' + probesPassed + ' probes detected the break, ' + probesFailed + ' blind spots')
304
+ if (probesFailed) process.exit(1)
305
+ console.log('ALL PROBES RED AS REQUIRED')
306
+ process.exit(0)