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
@@ -0,0 +1,392 @@
1
+ // ============================================================================
2
+ // audit-v5-lean-abstention.mjs — IN-REPO audit for L2 option A (the framework
3
+ // enforces an abstention when a reply records `formal:{decision:'defect'}` for an
4
+ // object AND votes on it in the same round).
5
+ //
6
+ // Owned by the v5 preset owner (leased by the Lead). Drives ONLY the public tool
7
+ // API + the `/v5` command handler, exactly like every other v5 suite.
8
+ // Scenarios (each with NAMED assertions):
9
+ // S1 formalVerify:'off' => the whole reply channel is a TRUE no-op
10
+ // S2 multi-defect ARRAY => per-object enforcement for every declared target
11
+ // S3 two members defect => the marker is an ARRAY with length 2
12
+ // S4 per-object enforcement => a defect on A does not abstain a vote on B
13
+ // S5 proper abstention => no false marker (no double counting)
14
+ // S6 `require` mode unaffected => the gate neither loosened nor bypassed
15
+ // S7 no-vote / no-rights (temp) => no abstention marker for a non-voter
16
+ // S8 no cross-round carryover => a later plain 1 still concludes
17
+ // S9 observability => marker visible on status() + lean_lib + report()
18
+ // Falsifiability: `tests/audit-v5-lean-abstention.mutants.mjs` (single-site mutants).
19
+ // Run: node tests/audit-v5-lean-abstention.mjs [V5_PLUGIN=… for a mutant copy]
20
+ // ============================================================================
21
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, rmSync } from 'node:fs'
22
+ import { tmpdir } from 'node:os'
23
+ import { fileURLToPath, pathToFileURL } from 'node:url'
24
+ import { join, dirname, isAbsolute } from 'node:path'
25
+
26
+ const HERE = dirname(fileURLToPath(import.meta.url))
27
+ const PRESET_DIR = process.env.V5_PRESET_DIR ? String(process.env.V5_PRESET_DIR) : join(HERE, '..', 'vibe-math-v5')
28
+ const PLUGIN_PATH = process.env.V5_PLUGIN ? String(process.env.V5_PLUGIN) : join(PRESET_DIR, 'vibe-math-v5.js')
29
+ const TARGET_A = 'p-a'
30
+ const TARGET_B = 'p-b'
31
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
32
+ const noop = new Proxy(function () { return noop }, { get: () => noop, apply: () => noop })
33
+ let passed = 0, failed = 0
34
+ const failures = []
35
+ const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
36
+ const section = (t) => console.log('\n' + t)
37
+
38
+ const mkSession = (id, parent) => ({ id, header: { version: 1, id, createdAt: 1, cwd: null, parentSession: parent, isSeeded: false }, inheritedEventCount: 0, get seq() { return 0 }, append() { return {} }, deriveMessages() { return [] }, snapshotEvents() { return [] }, ownEvents() { return [] } })
39
+ function makeHost(ws) {
40
+ const listeners = {}, toolRegs = [], commandRegs = [], spawns = [], wakes = [], liveAgents = new Map()
41
+ const base = {
42
+ get(n) {
43
+ if (n === 'sandboxPolicy') return undefined
44
+ if (n === 'compaction') return undefined
45
+ if (n === 'subprocess') return { async resolveExecutable(e) { return 'C:/fake/' + e }, spawn() { return { done: Promise.resolve({ exitCode: 0 }) } } }
46
+ return undefined
47
+ },
48
+ on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
49
+ effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
50
+ logger: { info() {}, warn() {}, error() {} },
51
+ timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
52
+ tools: { register(spec) { toolRegs.push(spec); return () => {} } },
53
+ commands: { register(spec) { commandRegs.push(spec); return () => {} } },
54
+ subagents: {
55
+ list() { return ['spawn'] },
56
+ async startContinuable({ label, request }) { const id = 'c' + (spawns.length + 1); const cs = mkSession(id, 'sess-audit'); cs.header.cwd = ws; liveAgents.set(id, { id, session: cs }); spawns.push({ label, request, childId: id }); return { childId: id, messageId: 'm' + spawns.length } },
57
+ async sendMessage(p, c, b) { wakes.push({ childId: c, blocks: b }); return 'w' + wakes.length },
58
+ interrupt() {}, async drainContinuableChildren(p, ids) { for (const i of ids) liveAgents.delete(i) },
59
+ },
60
+ agents: { roots() { return [ROOT] }, get(id) { return id === ROOT.id ? ROOT : liveAgents.get(id) }, list() { return [ROOT, ...liveAgents.values()] } },
61
+ fs: {
62
+ async resolve(rel, o2) { const b = (o2 && o2.cwd) || ws; const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/')); return { targetKey: p, displayPath: p } },
63
+ async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
64
+ async readText(t) { return readFileSync(t.targetKey, 'utf8') },
65
+ async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
66
+ async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map((e) => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
67
+ },
68
+ }
69
+ const ctx = new Proxy(base, { get(t, k) { if (k in t) return t[k]; if (k === 'then') return undefined; return noop } })
70
+ const ROOT_SESSION = mkSession('sess-audit', undefined)
71
+ ROOT_SESSION.header.cwd = ws
72
+ const ROOT = { id: 'sess-audit', options: {}, session: ROOT_SESSION }
73
+ return { ws, ctx, ROOT, toolRegs, commandRegs, listeners, spawns, wakes }
74
+ }
75
+ const instRootOf = (ws) => join(ws, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute')
76
+ const statePathOf = (ws) => join(instRootOf(ws), 'State', 'institute.v5state.json')
77
+
78
+ function writeState(ws, o) {
79
+ const members = o.members || [{ id: 'acad', kind: 'academician', childId: '', phase: 'active', direction: '', hiredBy: '', term: '', provider: 'spawn', persona: '', error: '', createdAt: 1, dismissedAt: 0, dismissReason: '' }]
80
+ const inst = {
81
+ key: 'default::institute', project: 'default', institute: 'institute', createdAt: 1, phase: 'active',
82
+ problem: { id: 'p-prime', statement: 'x' },
83
+ params: Object.assign({ researcherCount: 0, academician: true, quorumCap: 1, quorumMode: 'm-unanimous', formalVerify: 'encourage', leanAsync: false }, o.params || {}),
84
+ members, tasks: [], messages: [], delivered: [], meetings: [], debates: [],
85
+ verdicts: {}, formal: {}, todo: [], queue: [], solve: {},
86
+ counters: { academician: 1, researcher: 0, temp: 0, task: 0, meeting: 0, message: 0, verify: 0 },
87
+ runId: 'run-audit', lastProgressAt: 1, artifactCount: 0, diagnostics: [], paper: null, meetingOpen: null, officeRequestsDropped: 0,
88
+ }
89
+ const t = o.target || TARGET_A
90
+ inst.verdicts[t] = { target: t, kind: 'proposition', stage: 'vote', round: 1, votes: {}, history: {}, closed: false, proposer: 'acad', openedAt: 1, lastVoteAt: 1 }
91
+ inst.formal[t] = { status: 'passed', file: 'Formal/' + t + '.lean', proof: 'Verified/Lean/' + t + '.lean', decision: 'used', decisionSource: 'job-settle', run: { ok: true, exitCode: 0 }, updatedAt: 2 }
92
+ if (o.targetB) {
93
+ inst.verdicts[TARGET_B] = { target: TARGET_B, kind: 'proposition', stage: 'vote', round: 1, votes: {}, history: {}, closed: false, proposer: 'acad', openedAt: 1, lastVoteAt: 1 }
94
+ inst.formal[TARGET_B] = { status: 'passed', file: 'Formal/' + TARGET_B + '.lean', proof: 'Verified/Lean/' + TARGET_B + '.lean', decision: 'used', decisionSource: 'job-settle', run: { ok: true, exitCode: 0 }, updatedAt: 2 }
95
+ }
96
+ mkdirSync(dirname(statePathOf(ws)), { recursive: true })
97
+ writeFileSync(statePathOf(ws), JSON.stringify({ v: 1, institutes: { 'default::institute': inst } }, null, 2), 'utf8')
98
+ mkdirSync(join(instRootOf(ws), 'Formal'), { recursive: true })
99
+ for (const tt of [t].concat(o.targetB ? [TARGET_B] : [])) writeFileSync(join(instRootOf(ws), 'Formal', tt + '.lean'), 'theorem ' + tt.replace(/-/g, '_') + ' : True := trivial\n', 'utf8')
100
+ }
101
+
102
+ async function mkScenario(tag, o) {
103
+ const WS = mkdtempSync(join(tmpdir(), 'vibe-v5abs-'))
104
+ writeState(WS, o || {})
105
+ const mod = await import(pathToFileURL(PLUGIN_PATH).href + '?t=' + Date.now() + tag)
106
+ const h = makeHost(WS)
107
+ mod.apply(h.ctx)
108
+ const cmd = h.commandRegs.find((c) => c.name === 'v5')
109
+ const call = async (raw) => JSON.parse((await cmd.handler({ rawInput: raw, agent: h.ROOT })).text)
110
+ const toolSpec = (name) => h.toolRegs.find((t) => t.name === name)
111
+ const tool = async (name, args, agent) => JSON.parse(await toolSpec(name).execute(args || {}, { agent: agent || h.ROOT }))
112
+ const child = (id) => h.liveAgents && h.liveAgents.get && h.liveAgents.get(id) ? h.liveAgents.get(id) : { id, session: { header: { parentSession: h.ROOT.id } } }
113
+ const fire = (childId, reply) => { const blocks = [{ type: 'text', text: '```json\n' + JSON.stringify(reply) + '\n```' }]; for (const fn of (h.listeners['subagent/end'] || [])) fn({ id: childId, runId: 'r', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: blocks }) }
114
+ return { WS, h, call, tool, child, fire }
115
+ }
116
+ const stateOf = (ws) => JSON.parse(readFileSync(statePathOf(ws), 'utf8')).institutes['default::institute']
117
+ const clean = (ws) => { try { rmSync(ws, { recursive: true, force: true }) } catch (e) { /* best effort */ } }
118
+
119
+ section('[S1] formalVerify:\'off\' — the reply channel is a TRUE no-op')
120
+ {
121
+ const { WS, call, tool, fire, h } = await mkScenario('off', { params: { formalVerify: 'off' } })
122
+ await call('resume'); await sleep(40)
123
+ if (h.spawns.length) { fire(h.spawns[0].childId, { progress: 'ok', solved: false, contextPct: 10 }); await sleep(60) }
124
+ await call('set maxParallel=1'); await sleep(120)
125
+ let asked = false
126
+ for (let i = 0; i < h.wakes.length; i++) {
127
+ const t = ((h.wakes[i].blocks || [])[0] || {}).text || ''
128
+ if (t.indexOf('【求真表决') !== -1) { const w = h.wakes.splice(i, 1)[0]; fire(w.childId, { formal: { target: TARGET_A, decision: 'defect', note: '不应生效' }, verdict: { target: TARGET_A, verdict: 1, reason: 'off 模式' }, progress: 'x', solved: false, contextPct: 10 }); asked = true; break }
129
+ }
130
+ await sleep(150)
131
+ const st = await call('status')
132
+ const s = stateOf(WS)
133
+ const obj = ((st.formal || {}).objects || []).find((o) => o.target === TARGET_A) || {}
134
+ console.log(' asked=' + asked + ' formal.mode=' + st.formal.mode + ' status=' + JSON.stringify(obj.status) + ' fidelity=' + JSON.stringify(obj.fidelity) + ' closed/outcome=' + JSON.stringify([(s.verdicts[TARGET_A] || {}).closed, (s.verdicts[TARGET_A] || {}).outcome]))
135
+ assert(asked, 'S1: the voter was asked (the reply path really ran)')
136
+ assert(st.formal.mode === 'off', 'S1: the mode is off')
137
+ assert(obj.status === 'passed' && !obj.fidelity, '★ S1: a `formal:{…}` reply changes NOTHING observable in off mode (status stays passed, no fidelity marker)')
138
+ assert(!obj.abstainedCount, '★ S1: no enforced abstention is recorded in off mode')
139
+ assert((s.verdicts[TARGET_A] || {}).outcome === 'true', '★ S1: the plain 1 ballot is what counted (off mode is inert, not silently abstaining)')
140
+ clean(WS)
141
+ }
142
+
143
+ section('[S2] multi-defect ARRAY — every declared target is enforced (adv-F2)')
144
+ {
145
+ const { WS, call, tool, fire, h } = await mkScenario('arr', { targetB: true })
146
+ await call('resume'); await sleep(40)
147
+ if (h.spawns.length) { fire(h.spawns[0].childId, { progress: 'ok', solved: false, contextPct: 10 }); await sleep(60) }
148
+ await call('set maxParallel=1'); await sleep(120)
149
+ let asked = false
150
+ for (let i = 0; i < h.wakes.length; i++) {
151
+ const t = ((h.wakes[i].blocks || [])[0] || {}).text || ''
152
+ if (t.indexOf('【求真表决') !== -1) {
153
+ const w = h.wakes.splice(i, 1)[0]
154
+ fire(w.childId, { formal: [{ target: TARGET_A, decision: 'defect', note: 'A 的形式化偏离' }, { target: TARGET_B, decision: 'defect', note: 'B 的形式化偏离' }], verdict: { target: TARGET_A, verdict: 1, reason: '同回复数组' }, progress: 'x', solved: false, contextPct: 10 })
155
+ asked = true; break
156
+ }
157
+ }
158
+ await sleep(150)
159
+ const st = await call('status')
160
+ const s = stateOf(WS)
161
+ const a = ((st.formal || {}).objects || []).find((o) => o.target === TARGET_A) || {}
162
+ const b = ((st.formal || {}).objects || []).find((o) => o.target === TARGET_B) || {}
163
+ console.log(' asked=' + asked + ' A=' + JSON.stringify(a.status) + '/marker=' + JSON.stringify(a.abstainedCount) + ' B=' + JSON.stringify(b.status) + '/marker=' + JSON.stringify(b.abstainedCount) + ' A closed=' + JSON.stringify((s.verdicts[TARGET_A] || {}).closed))
164
+ assert(asked, 'S2: the voter was asked')
165
+ assert(a.status === 'attempted' && b.status === 'attempted', '★ S2: BOTH array entries were recorded (both retracted to attempted)')
166
+ assert(a.abstainedCount === 1, '★ S2: the array form enforces the abstention for the voted target (marker length 1)')
167
+ assert(!((s.verdicts[TARGET_A] || {}).closed === true && (s.verdicts[TARGET_A] || {}).outcome === 'true'), '★ S2: the same reply cannot conclude the object it declared defective')
168
+ clean(WS)
169
+ }
170
+
171
+ section('[S3] two members each recording a defect — the marker is an ARRAY of length 2 (adv-F1)')
172
+ {
173
+ const { WS, call, tool, fire, h } = await mkScenario('two', { members: [
174
+ { id: 'acad', kind: 'academician', childId: '', phase: 'active', direction: '', hiredBy: '', term: '', provider: 'spawn', persona: '', error: '', createdAt: 1, dismissedAt: 0, dismissReason: '' },
175
+ // TWO RESEARCHERS: the academician is the office and does not hold a ballot once researchers
176
+ // exist, so the two-marker scenario needs two real voters.
177
+ { id: 'r-1', kind: 'researcher', childId: '', phase: 'active', direction: '', hiredBy: '', term: '', provider: 'spawn', persona: '', error: '', createdAt: 2, dismissedAt: 0, dismissReason: '' },
178
+ { id: 'r-2', kind: 'researcher', childId: '', phase: 'active', direction: '', hiredBy: '', term: '', provider: 'spawn', persona: '', error: '', createdAt: 3, dismissedAt: 0, dismissReason: '' },
179
+ ], params: { researcherCount: 2, quorumCap: 2, maxParallel: 2 } })
180
+ await call('resume'); await sleep(60)
181
+ for (const sp of h.spawns.slice()) { fire(sp.childId, { progress: 'ok', solved: false, contextPct: 10 }); await sleep(40) }
182
+ await call('set maxParallel=2'); await sleep(200)
183
+ // Fire BOTH voters' replies directly (their ids come from the session, not from wake labels):
184
+ // wake routing is the host's business, and this scenario is about the MARKER accumulating.
185
+ const st0 = await call('status')
186
+ const voterChildren = (st0.members || []).filter((m) => m.id === 'r-1' || m.id === 'r-2').map((m) => ({ id: m.id, childId: String(m.childId || '') }))
187
+ let answers = 0
188
+ for (const v of voterChildren) {
189
+ if (!v.childId) continue
190
+ fire(v.childId, { formal: { target: TARGET_A, decision: 'defect', note: v.id + ' 认为形式化偏离' }, verdict: { target: TARGET_A, verdict: 1, reason: v.id + ' 投 1' }, progress: 'x', solved: false, contextPct: 10 })
191
+ answers += 1
192
+ await sleep(120)
193
+ }
194
+ await sleep(150)
195
+ const st = await call('status')
196
+ const s = stateOf(WS)
197
+ const a = ((st.formal || {}).objects || []).find((o) => o.target === TARGET_A) || {}
198
+ const list = (a.fidelity || {}).voteAbstainedByFramework
199
+ const len = Array.isArray(list) ? list.length : (list ? 1 : 0)
200
+ const lib = await tool('vibe_v5_lean_lib', { refresh: false })
201
+ const libObj = ((lib.objects || [])[0]) || {}
202
+ const rp = String((await call('report')).report || '')
203
+ console.log(' answers=' + answers + ' markerLen=' + len + ' abstainedCount=' + a.abstainedCount + ' libLen=' + (Array.isArray((libObj.fidelity || {}).voteAbstainedByFramework) ? libObj.fidelity.voteAbstainedByFramework.length : 0))
204
+ assert(answers === 2, 'S3: two members answered (' + answers + ')')
205
+ assert(Array.isArray(list) && list.length === 2, '★ S3: the enforced-abstention marker is an ARRAY of length 2 (both members auditable)')
206
+ assert(a.abstainedCount === 2, '★ S3: abstainedCount === 2')
207
+ assert(list.map((x) => x.by).sort().join(',') === 'r-1,r-2', '★ S3: both VOTERS are recorded (' + JSON.stringify(list.map((x) => x.by)) + ')')
208
+ assert(!((s.verdicts[TARGET_A] || {}).closed === true && (s.verdicts[TARGET_A] || {}).outcome === 'true'), '★ S3: the round still cannot conclude the defective object')
209
+ clean(WS)
210
+ }
211
+
212
+ section('[S4] per-object enforcement — a defect on A does not abstain a vote on B')
213
+ {
214
+ const { WS, call, fire, h } = await mkScenario('perobj', { targetB: true, params: { quorumCap: 1 } })
215
+ await call('resume'); await sleep(40)
216
+ if (h.spawns.length) { fire(h.spawns[0].childId, { progress: 'ok', solved: false, contextPct: 10 }); await sleep(60) }
217
+ await call('set maxParallel=1'); await sleep(120)
218
+ let answered = 0
219
+ const deadline = Date.now() + 2500
220
+ while (answered < 2 && Date.now() < deadline) {
221
+ const i = h.wakes.findIndex((w) => (((w.blocks || [])[0] || {}).text || '').indexOf('【求真表决') !== -1)
222
+ if (i === -1) { if (h.wakes.length) { const w = h.wakes.shift(); fire(w.childId, { progress: 'x', solved: false, contextPct: 10 }) } await sleep(25); continue }
223
+ const w = h.wakes.splice(i, 1)[0]
224
+ const text = (w.blocks[0] || {}).text || ''
225
+ if (text.indexOf(TARGET_A) !== -1 && answered === 0) fire(w.childId, { formal: { target: TARGET_A, decision: 'defect', note: 'A 偏离' }, verdict: { target: TARGET_A, verdict: 1, reason: 'A' }, progress: 'x', solved: false, contextPct: 10 })
226
+ else fire(w.childId, { verdict: { target: text.indexOf(TARGET_B) !== -1 ? TARGET_B : TARGET_A, verdict: 1, reason: 'B' }, progress: 'x', solved: false, contextPct: 10 })
227
+ answered += 1
228
+ await sleep(60)
229
+ }
230
+ await sleep(150)
231
+ const st = await call('status')
232
+ const s = stateOf(WS)
233
+ const a = ((st.formal || {}).objects || []).find((o) => o.target === TARGET_A) || {}
234
+ const b = ((st.formal || {}).objects || []).find((o) => o.target === TARGET_B) || {}
235
+ console.log(' answered=' + answered + ' A.marker=' + JSON.stringify(a.abstainedCount) + ' B.marker=' + JSON.stringify(b.abstainedCount) + ' B=' + JSON.stringify([(s.verdicts[TARGET_B] || {}).closed, (s.verdicts[TARGET_B] || {}).outcome]))
236
+ assert(!a.abstainedCount && !b.abstainedCount || a.abstainedCount === 1, 'S4: the marker belongs to the object that was declared defective')
237
+ assert(b.abstainedCount === 0 || b.abstainedCount === undefined, '★ S4: the OTHER object gets no abstention marker')
238
+ clean(WS)
239
+ }
240
+
241
+ section('[S5] a PROPER abstention (0.5) gets no false marker')
242
+ {
243
+ const { WS, call, fire, h } = await mkScenario('prop', {})
244
+ await call('resume'); await sleep(40)
245
+ if (h.spawns.length) { fire(h.spawns[0].childId, { progress: 'ok', solved: false, contextPct: 10 }); await sleep(60) }
246
+ await call('set maxParallel=1'); await sleep(120)
247
+ for (let i = 0; i < h.wakes.length; i++) {
248
+ const t = ((h.wakes[i].blocks || [])[0] || {}).text || ''
249
+ if (t.indexOf('【求真表决') !== -1) { const w = h.wakes.splice(i, 1)[0]; fire(w.childId, { formal: { target: TARGET_A, decision: 'defect', note: '偏离' }, verdict: { target: TARGET_A, verdict: 0.5, reason: '主动弃权' }, progress: 'x', solved: false, contextPct: 10 }); break }
250
+ }
251
+ await sleep(150)
252
+ const st = await call('status')
253
+ const s = stateOf(WS)
254
+ const a = ((st.formal || {}).objects || []).find((o) => o.target === TARGET_A) || {}
255
+ console.log(' status=' + JSON.stringify(a.status) + ' marker=' + JSON.stringify(a.fidelity && a.fidelity.voteAbstainedByFramework) + ' closed/outcome=' + JSON.stringify([(s.verdicts[TARGET_A] || {}).closed, (s.verdicts[TARGET_A] || {}).outcome]))
256
+ assert(a.status === 'attempted', 'S5: the defect still retracted the proof')
257
+ assert(!a.fidelity || !a.fidelity.voteAbstainedByFramework, '★ S5: NO false marker when the value was already an abstention (no double counting)')
258
+ assert(!((s.verdicts[TARGET_A] || {}).closed === true && (s.verdicts[TARGET_A] || {}).outcome === 'true'), '★ S5: an abstention cannot conclude the object')
259
+ clean(WS)
260
+ }
261
+
262
+ section('[S6] `require` mode: the gate is neither loosened nor bypassed')
263
+ {
264
+ const { WS, call, tool, fire, h } = await mkScenario('req', { params: { formalVerify: 'require' } })
265
+ await call('resume'); await sleep(40)
266
+ if (h.spawns.length) { fire(h.spawns[0].childId, { progress: 'ok', solved: false, contextPct: 10 }); await sleep(60) }
267
+ await call('set maxParallel=1'); await sleep(120)
268
+ for (let i = 0; i < h.wakes.length; i++) {
269
+ const t = ((h.wakes[i].blocks || [])[0] || {}).text || ''
270
+ if (t.indexOf('【求真表决') !== -1) { const w = h.wakes.splice(i, 1)[0]; fire(w.childId, { formal: { target: TARGET_A, decision: 'defect', note: '偏离' }, verdict: { target: TARGET_A, verdict: 1, reason: 'x' }, progress: 'x', solved: false, contextPct: 10 }); break }
271
+ }
272
+ await sleep(180)
273
+ const s = stateOf(WS)
274
+ const v = s.verdicts[TARGET_A] || {}
275
+ console.log(' require: closed=' + v.closed + ' outcome=' + v.outcome + ' reason=' + String(v.reason || '').slice(0, 60))
276
+ assert(!(v.closed === true && v.outcome === 'true'), '★ S6: `require` still withholds a TRUE conclusion after a defect')
277
+ assert(v.closed !== true || v.outcome === 'undecided', '★ S6: a closure in `require` mode can only be 未定论 here (the gate is not bypassed)')
278
+ clean(WS)
279
+ }
280
+
281
+ section('[S7] a temp worker (no vote rights) records a defect — no abstention marker for a non-voter')
282
+ {
283
+ const { WS, call, fire, h } = await mkScenario('temp', { members: [
284
+ { id: 'acad', kind: 'academician', childId: '', phase: 'active', direction: '', hiredBy: '', term: '', provider: 'spawn', persona: '', error: '', createdAt: 1, dismissedAt: 0, dismissReason: '' },
285
+ { id: 't-1', kind: 'temp', childId: '', phase: 'active', direction: '', hiredBy: 'acad', term: '', provider: 'spawn', persona: '', error: '', createdAt: 2, dismissedAt: 0, dismissReason: '' },
286
+ ] })
287
+ await call('resume'); await sleep(60)
288
+ for (const sp of h.spawns.slice()) { fire(sp.childId, { progress: 'ok', solved: false, contextPct: 10 }); await sleep(40) }
289
+ await call('set maxParallel=2'); await sleep(200)
290
+ const st0 = await call('status')
291
+ const childOf = (id) => { const m = (st0.members || []).find((x) => x.id === id); return m ? String(m.childId || '') : '' }
292
+ const tempChild = (st0.members || []).filter((m) => m.id === 't-1' && m.childId).map((m) => m.childId)
293
+ .concat(h.spawns.map((s) => String(s.childId).slice(0, 12)).filter((c) => c && c !== childOf('acad')))
294
+ // A temp worker has NO vote rights, so the framework never asks it to vote — the honest scenario is
295
+ // an OPINION from a non-voter while a verify is open: it must be recordable, must not be counted,
296
+ // and must not produce an "abstention" claim.
297
+ let acadChild = ''
298
+ const deadline = Date.now() + 3000
299
+ while (!acadChild && Date.now() < deadline) {
300
+ const i = h.wakes.findIndex((w) => (((w.blocks || [])[0] || {}).text || '').indexOf('【求真表决') !== -1)
301
+ if (i === -1) { if (h.wakes.length) { const w = h.wakes.shift(); fire(w.childId, { progress: 'x', solved: false, contextPct: 10 }) } await sleep(25); continue }
302
+ acadChild = h.wakes[i].childId
303
+ const w = h.wakes.splice(i, 1)[0]
304
+ fire(w.childId, { verdict: { target: TARGET_A, verdict: 1, reason: '唯一有表决权者同意' }, progress: 'x', solved: false, contextPct: 10 })
305
+ await sleep(120)
306
+ }
307
+ const stV = await call('status')
308
+ // The temp worker has NO rights: its "vote" must be relayed, never counted — and it must not
309
+ // produce an abstention marker (there was no boolean ballot to abstain from).
310
+ const tcid = tempChild.find((c) => c !== String(acadChild).slice(0, 12)) || (tempChild[0] || '')
311
+ let tempAnswered = false
312
+ if (tcid) {
313
+ fire(tcid, { verdict: { target: TARGET_A, verdict: 0, reason: '临时工认为假(无权)' }, progress: 'x', solved: false, contextPct: 10 })
314
+ tempAnswered = true
315
+ await sleep(120)
316
+ }
317
+ const st = await call('status')
318
+ const s = stateOf(WS)
319
+ const a = ((st.formal || {}).objects || []).find((o) => o.target === TARGET_A) || {}
320
+ const voted = ((stV.verify || {}).voted || [])
321
+ console.log(' tempChild=' + JSON.stringify(tcid) + ' tempAnswered=' + tempAnswered + ' votedAtBallot=' + JSON.stringify(voted) + ' marker=' + JSON.stringify(a.abstainedCount) + ' closed/outcome=' + JSON.stringify([(s.verdicts[TARGET_A] || {}).closed, (s.verdicts[TARGET_A] || {}).outcome]))
322
+ assert(!!tcid && tempAnswered, 'S7: the temp worker could still speak (a non-voter opinion is delivered)')
323
+ assert(!voted.includes('t-1'), '★ S7: the temp worker is never a voter in the verify')
324
+ assert(!a.abstainedCount, '★ S7: no abstention marker for a non-voter (there was no boolean ballot to abstain from)')
325
+ assert(!((s.verdicts[TARGET_A] || {}).closed === true && (s.verdicts[TARGET_A] || {}).outcome === 'false'), '★ S7: the non-voter\'s 0 did not decide the object')
326
+ clean(WS)
327
+ }
328
+
329
+ section('[S8] no cross-round carryover — a later plain 1 still concludes')
330
+ {
331
+ const { WS, call, fire, h } = await mkScenario('carry', {})
332
+ await call('resume'); await sleep(40)
333
+ if (h.spawns.length) { fire(h.spawns[0].childId, { progress: 'ok', solved: false, contextPct: 10 }); await sleep(60) }
334
+ await call('set maxParallel=1'); await sleep(120)
335
+ // round 1: defect + 1 (enforced abstention, no conclusion)
336
+ for (let i = 0; i < h.wakes.length; i++) {
337
+ const t = ((h.wakes[i].blocks || [])[0] || {}).text || ''
338
+ if (t.indexOf('【求真表决') !== -1) { const w = h.wakes.splice(i, 1)[0]; fire(w.childId, { formal: { target: TARGET_A, decision: 'defect', note: '偏离' }, verdict: { target: TARGET_A, verdict: 1, reason: 'r1' }, progress: 'x', solved: false, contextPct: 10 }); break }
339
+ }
340
+ await sleep(150)
341
+ const mid = stateOf(WS).verdicts[TARGET_A] || {}
342
+ // round 2: a plain 1 (no formal field) must be able to conclude
343
+ await call('set maxParallel=1'); await sleep(150)
344
+ let answered2 = false
345
+ const deadline = Date.now() + 2500
346
+ while (!answered2 && Date.now() < deadline) {
347
+ const i = h.wakes.findIndex((w) => (((w.blocks || [])[0] || {}).text || '').indexOf('【求真表决') !== -1)
348
+ if (i === -1) { if (h.wakes.length) { const w = h.wakes.shift(); fire(w.childId, { progress: 'x', solved: false, contextPct: 10 }) } await sleep(25); continue }
349
+ const w = h.wakes.splice(i, 1)[0]
350
+ fire(w.childId, { verdict: { target: TARGET_A, verdict: 1, reason: 'r2 无缺陷' }, progress: 'x', solved: false, contextPct: 10 })
351
+ answered2 = true
352
+ await sleep(120)
353
+ }
354
+ const s = stateOf(WS).verdicts[TARGET_A] || {}
355
+ console.log(' after round1 closed=' + mid.closed + '/marker kept; round2 answered=' + answered2 + ' closed=' + s.closed + ' outcome=' + s.outcome)
356
+ assert(!(mid.closed === true && mid.outcome === 'true'), 'S8: round 1 (defect) did not conclude')
357
+ assert(s.closed === true && s.outcome === 'true', '★ S8: a LATER reply with a plain 1 concludes normally (no cross-round carryover)')
358
+ clean(WS)
359
+ }
360
+
361
+ section('[S9] observability — the marker is on status(), lean_lib and report()')
362
+ {
363
+ const { WS, call, tool, fire, h } = await mkScenario('obs', {})
364
+ await call('resume'); await sleep(40)
365
+ if (h.spawns.length) { fire(h.spawns[0].childId, { progress: 'ok', solved: false, contextPct: 10 }); await sleep(60) }
366
+ await call('set maxParallel=1'); await sleep(120)
367
+ for (let i = 0; i < h.wakes.length; i++) {
368
+ const t = ((h.wakes[i].blocks || [])[0] || {}).text || ''
369
+ if (t.indexOf('【求真表决') !== -1) { const w = h.wakes.splice(i, 1)[0]; fire(w.childId, { formal: { target: TARGET_A, decision: 'defect', note: '偏离' }, verdict: { target: TARGET_A, verdict: 1, reason: 'x' }, progress: 'x', solved: false, contextPct: 10 }); break }
370
+ }
371
+ await sleep(150)
372
+ const st = await call('status')
373
+ const a = ((st.formal || {}).objects || []).find((o) => o.target === TARGET_A) || {}
374
+ const lib = await tool('vibe_v5_lean_lib', { refresh: false })
375
+ const libObj = ((lib.objects || [])[0]) || {}
376
+ const rp = String((await call('report')).report || '')
377
+ const line = (rp.split('\n').find((l) => l.includes('框架强制弃权')) || '').trim()
378
+ // fourth surface: the member-facing archive receipt (dedupe path returns the stored record)
379
+ const arch = await tool('vibe_v5_lean_archive', { kind: 'proof', target: TARGET_A, content: existsSync(join(instRootOf(WS), 'Formal', TARGET_A + '.lean')) ? readFileSync(join(instRootOf(WS), 'Formal', TARGET_A + '.lean'), 'utf8') : 'theorem p_a : True := trivial\n', run: false })
380
+ console.log(' status.fidelity=' + JSON.stringify(a.fidelity && a.fidelity.voteAbstainedByFramework) + ' lib.count=' + JSON.stringify(libObj.abstainedCount) + ' archive.count=' + JSON.stringify(arch.abstainedCount) + ' report=' + line.slice(0, 110))
381
+ assert(!!(a.fidelity && Array.isArray(a.fidelity.voteAbstainedByFramework) && a.fidelity.voteAbstainedByFramework.length === 1), '★ S9: status().formal.objects[].fidelity exposes the marker array')
382
+ assert(libObj.abstainedCount === 1, '★ S9: vibe_v5_lean_lib.objects exposes abstainedCount')
383
+ assert(arch.abstainedCount === 1 && !!(arch.fidelity && arch.fidelity.voteAbstainedByFramework), '★ S9: vibe_v5_lean_archive returns the marker on its receipt')
384
+ assert(/框架强制弃权/.test(line), '★ S9: report() has a 框架强制弃权 line naming the object and the member')
385
+ clean(WS)
386
+ }
387
+
388
+ console.log('')
389
+ console.log('passed=' + passed + ' failed=' + failed)
390
+ if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
391
+ console.log('ALL GREEN')
392
+ process.exit(0)
@@ -0,0 +1,106 @@
1
+ // ============================================================================
2
+ // audit-v5-lean-abstention.mutants.mjs — falsifiability for the L2 abstention audit.
3
+ // Each mutant is a SINGLE-SITE edit; every one must turn a NAMED assertion red in
4
+ // `tests/audit-v5-lean-abstention.mjs` (and, for the gate mutant, also the I8
5
+ // invariant in `tests/audit-prompt-invariants.mjs`).
6
+ // Run: node tests/audit-v5-lean-abstention.mutants.mjs
7
+ // ============================================================================
8
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync } from 'node:fs'
9
+ import { spawnSync } from 'node:child_process'
10
+ import { fileURLToPath } from 'node:url'
11
+ import { tmpdir } from 'node:os'
12
+ import { join, dirname } from 'node:path'
13
+
14
+ const HERE = dirname(fileURLToPath(import.meta.url))
15
+ const REPO = join(HERE, '..')
16
+ const PRESET = join(REPO, 'vibe-math-v5')
17
+ const AUDIT = join(HERE, 'audit-v5-lean-abstention.mjs')
18
+ const INVARIANTS = process.env.PROMPT_INVARIANTS_SUITE ? String(process.env.PROMPT_INVARIANTS_SUITE) : join(HERE, 'audit-prompt-invariants.mjs')
19
+ const ONLY = process.env.MUTANTS_ONLY ? String(process.env.MUTANTS_ONLY).split(',').map((x) => x.trim()).filter(Boolean) : null
20
+ const REL = 'vibe-math-v5/vibe-math-v5.js'
21
+ const SRC = readFileSync(join(PRESET, 'vibe-math-v5.js'), 'utf8')
22
+
23
+ const MUTANTS = [
24
+ {
25
+ name: 'M1 formalOn() gate removed',
26
+ from: 'if (formalOn() && p.formal) {',
27
+ to: 'if (p.formal) { // MUTANT: the off-mode gate is gone',
28
+ expectAudit: 'S1: a `formal:{…}` reply changes NOTHING observable in off mode',
29
+ // The cross-suite expectation is an INVARIANT ID, not just "something went red": removing the
30
+ // gate must redden `audit-prompt-invariants`'s I8 (guard-load-bearing F3: this field used to be
31
+ // dead — any failing invariant would have been accepted as the claimed red).
32
+ expectInvariant: 'I8',
33
+ },
34
+ {
35
+ name: 'M2 enforcement disabled',
36
+ from: 'const enforced = isVoterHere && defectTargetsThisReply.has(vTarget) && (declared === 0 || declared === 1)',
37
+ to: 'const enforced = false // MUTANT: no enforced abstention',
38
+ expectAudit: 'S2: the same reply cannot conclude the object it declared defective',
39
+ },
40
+ {
41
+ name: 'M3 marker collapses to a single value',
42
+ from: 'fid.voteAbstainedByFramework = list',
43
+ to: 'fid.voteAbstainedByFramework = list[list.length - 1] // MUTANT: single-value marker',
44
+ expectAudit: 'S3: the enforced-abstention marker is an ARRAY of length 2',
45
+ },
46
+ {
47
+ name: 'M4 defect record wipes the marker history',
48
+ from: 'fidelity: Object.assign({}, (prev0 && prev0.fidelity) || {}, { at: now(), by: String(memberId || \'\'), note: why }),',
49
+ to: 'fidelity: { at: now(), by: String(memberId || \'\'), note: why }, // MUTANT: replace, not merge',
50
+ expectAudit: 'S3: the enforced-abstention marker is an ARRAY of length 2',
51
+ },
52
+ ]
53
+
54
+ const runAudit = (plugin) => {
55
+ const r = spawnSync(process.execPath, [AUDIT], { encoding: 'utf8', timeout: 600000, env: Object.assign({}, process.env, { V5_PLUGIN: plugin }) })
56
+ return { code: r.status, out: String(r.stdout || '') + String(r.stderr || '') }
57
+ }
58
+ const reds = (out) => out.split('\n').filter((l) => l.trim().startsWith('FAIL -')).map((l) => l.replace(/^\s*FAIL - /, '').trim())
59
+
60
+ // control: the unmutated audit must be green
61
+ const control = runAudit(join(PRESET, 'vibe-math-v5.js'))
62
+ const controlGreen = control.code === 0 && /passed=\d+ failed=0/.test(control.out)
63
+ console.log('control (unmutated): exit=' + control.code + ' | ' + (control.out.split('\n').find((l) => l.startsWith('passed=')) || '').trim())
64
+ let problems = controlGreen ? 0 : 1
65
+
66
+ for (const m of MUTANTS) {
67
+ if (ONLY && !ONLY.includes(m.name.split(' ')[0])) continue
68
+ if (!SRC.includes(m.from)) { console.log(m.name + ': ANCHOR MISSING'); problems += 1; continue }
69
+ const dir = join(tmpdir(), 'v5-abs-mut-' + m.name.split(' ')[0])
70
+ rmSync(dir, { recursive: true, force: true })
71
+ mkdirSync(dir, { recursive: true })
72
+ for (const f of ['math-computation.js', 'math-engines.js']) copyFileSync(join(PRESET, f), join(dir, f))
73
+ writeFileSync(join(dir, 'vibe-math-v5.js'), SRC.replace(m.from, m.to), 'utf8')
74
+ const r = runAudit(join(dir, 'vibe-math-v5.js'))
75
+ const list = reds(r.out)
76
+ const hit = list.some((x) => x.indexOf(m.expectAudit) !== -1)
77
+ const ok = r.code !== 0 && hit
78
+ let invariantNote = ''
79
+ if (m.expectInvariant) {
80
+ const ir = spawnSync(process.execPath, [INVARIANTS, '--json'], {
81
+ cwd: REPO, encoding: 'utf8', timeout: 600000,
82
+ env: Object.assign({}, process.env, { PROMPT_INVARIANTS_MUTATE: JSON.stringify([REL, m.from, m.to]) }),
83
+ })
84
+ let parsed = null
85
+ try { parsed = JSON.parse(ir.stdout) } catch (e) { /* reported below */ }
86
+ // Assert the EXPECTED invariant, not merely "something failed": the JSON must carry a
87
+ // `failures` array and `expectInvariant` must appear in it. A missing array or a different
88
+ // invariant failing is a harness failure, loudly.
89
+ const failures = parsed && Array.isArray(parsed.failures) ? parsed.failures : null
90
+ const named = !!failures && failures.some((f) => String(f).includes(m.expectInvariant))
91
+ const invariantRed = ir.status !== 0 && parsed && parsed.failed > 0 && named
92
+ invariantNote = ' | audit-prompt-invariants: ' + (invariantRed
93
+ ? 'RED on ' + m.expectInvariant + ' (failed=' + parsed.failed + ': ' + String(failures[0]).slice(0, 70) + ')'
94
+ : (failures === null
95
+ ? 'NO failures[] ARRAY in --json output (cannot verify ' + m.expectInvariant + ')'
96
+ : 'expected ' + m.expectInvariant + ' but got ' + JSON.stringify(failures.map((f) => String(f).slice(0, 40)).slice(0, 3))))
97
+ if (!invariantRed) problems += 1
98
+ }
99
+ if (!ok) problems += 1
100
+ console.log(m.name.padEnd(42) + ': audit exit=' + r.code + (ok ? ' RED (as required)' : ' NOT RED') + invariantNote)
101
+ for (const x of list.slice(0, 2)) console.log(' → ' + x.slice(0, 140))
102
+ rmSync(dir, { recursive: true, force: true })
103
+ }
104
+ console.log('')
105
+ console.log(problems === 0 ? 'ALL MUTANTS RED AS REQUIRED' : problems + ' problem(s)')
106
+ process.exit(problems === 0 ? 0 : 1)
@@ -0,0 +1,88 @@
1
+ // ============================================================================
2
+ // audit-v5-prompt-duplication.mjs — pins the invariant that the v5 toolkit prompt
3
+ // text exists TWICE (two `agent-instructions` entries: office + members) and that
4
+ // both copies are IDENTICAL after normalisation, plus that the three review-8
5
+ // wording fixes (P1/P2/P3) landed in BOTH copies.
6
+ //
7
+ // Owned by the v5 preset owner (proposed to the shared-layer owner for an index row).
8
+ // Falsifiability: `node tests/audit-v5-prompt-duplication.mjs --self-probe` mutates ONE
9
+ // copy in memory (single site) and requires the named check to go red.
10
+ // Run: node tests/audit-v5-prompt-duplication.mjs [--json] [--self-probe]
11
+ // ============================================================================
12
+ import { readFileSync, existsSync } from 'node:fs'
13
+ import { fileURLToPath } from 'node:url'
14
+ import { dirname, join } from 'node:path'
15
+
16
+ const HERE = dirname(fileURLToPath(import.meta.url))
17
+ const REL = 'vibe-math-v5/agent.cordis.yml'
18
+ const YML = process.env.V5_PROMPT_YML ? String(process.env.V5_PROMPT_YML) : join(HERE, '..', REL)
19
+ // In-memory single-site mutation for --self-probe: `[from, to]` applied to the FIRST copy only.
20
+ const MUT = process.env.V5_PROMPT_MUTATE ? JSON.parse(process.env.V5_PROMPT_MUTATE) : null
21
+
22
+ const failures = []
23
+ let passed = 0
24
+ const check = (cond, msg) => { if (cond) { passed += 1 } else { failures.push(msg) } }
25
+ const norm = (s) => s.replace(/\s+/g, ' ').replace(/\{\{cwd\}\}|\{\{model\}\}/g, '<T>').trim()
26
+
27
+ function loadBlocks() {
28
+ if (!existsSync(YML)) return null
29
+ let text = readFileSync(YML, 'utf8')
30
+ if (MUT && Array.isArray(MUT) && MUT.length === 2) {
31
+ const i = text.indexOf(MUT[0])
32
+ if (i !== -1) text = text.slice(0, i) + MUT[1] + text.slice(i + MUT[0].length) // FIRST copy only
33
+ }
34
+ const lines = text.split('\n')
35
+ const starts = []
36
+ lines.forEach((l, i) => { if (l.indexOf('This session includes "Vibe Math V5"') !== -1) starts.push(i) })
37
+ // The prompt text is NOT ended by the `/v5 slash command …` line (that line sits in the MIDDLE:
38
+ // the LEAN FORMAL VERIFICATION and TRUST RULE sections follow it). End each copy at its last
39
+ // sentence instead, so the invariant covers the whole text (a truncated comparison would miss
40
+ // exactly the sections that drift most easily).
41
+ const endAt = (s) => {
42
+ for (let i = s; i < lines.length; i++) if (/Never present an unverified claim as established/.test(lines[i])) return i
43
+ for (let i = s; i < lines.length; i++) if (/\/v5 slash command mirrors these/.test(lines[i])) return i
44
+ return lines.length - 1
45
+ }
46
+ return starts.map((s) => ({ from: s + 1, to: endAt(s) + 1, text: norm(lines.slice(s, endAt(s) + 1).join(' ')) }))
47
+ }
48
+
49
+ const blocks = loadBlocks()
50
+ check(!!blocks, 'agent.cordis.yml is readable at ' + YML)
51
+ if (blocks) {
52
+ check(blocks.length === 2, 'exactly TWO toolkit prompt copies exist (got ' + blocks.length + ')')
53
+ if (blocks.length === 2) {
54
+ check(blocks[0].text === blocks[1].text,
55
+ 'the two prompt copies are IDENTICAL after normalisation (L' + blocks[0].from + '-' + blocks[0].to + ' vs L' + blocks[1].from + '-' + blocks[1].to
56
+ + '; lengths ' + blocks[0].text.length + '/' + blocks[1].text.length + ') — a single-site edit in one copy must be mirrored in the other')
57
+ // The review-8 wording fixes must be present in BOTH copies.
58
+ const NEEDED = [
59
+ ['P1 exception', /when a written flow requires it/],
60
+ ['P1 paraphrase clause', /PARAPHRASE of the institute/],
61
+ ['P2 disclosure', /YOUR OWN boolean vote on that object in the SAME reply is counted as an ABSTENTION/],
62
+ ['P2 scope note', /Only a voter's ballot is affected/],
63
+ ['P3 base statement', /resolved against the SESSION WORKING DIRECTORY/],
64
+ ['P3 short-form equivalence', /names the SAME file/],
65
+ ]
66
+ for (const [name, re] of NEEDED) {
67
+ blocks.forEach((b, i) => check(re.test(b.text), name + ' present in copy ' + (i + 1) + ' (L' + b.from + '-' + b.to + ')'))
68
+ }
69
+ // the disclosure must NOT contradict the enforced behaviour
70
+ check(/ABSTENTION/.test(blocks[1].text) && /retracts the proof/.test(blocks[1].text), 'the disclosure explains that the defect still retracts the proof')
71
+ }
72
+ }
73
+
74
+ if (process.argv.includes('--json')) {
75
+ console.log(JSON.stringify({ passed, failed: failures.length, failures }))
76
+ } else {
77
+ console.log('v5 prompt duplication audit — ' + REL)
78
+ for (const f of failures) console.log(' FAIL - ' + f)
79
+ console.log((failures.length ? 'FAILED' : 'ALL GREEN') + ' — passed=' + passed + ' failed=' + failures.length)
80
+ if (process.argv.includes('--self-probe')) {
81
+ console.log('\nself-probe: one copy mutated in memory ⇒ the expected check must go red')
82
+ if (!MUT) {
83
+ const needle = 'There is no forced closure.'
84
+ console.log(' (no V5_PROMPT_MUTATE set; the suite driver runs this with a single-site mutation)')
85
+ }
86
+ }
87
+ }
88
+ process.exit(failures.length ? 1 : 0)
@@ -0,0 +1,34 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+ import { spawnSync } from 'node:child_process'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+
6
+ // Falsifiability for tests/audit-v5-prompt-duplication.mjs: a SINGLE-SITE edit in ONE prompt copy
7
+ // must turn the equality check (and the wording checks) NAMED-red, while the unmutated run is green.
8
+ const SUITE = 'D:/wd/vibemath开发/Vibe-Mathematics/tests/audit-v5-prompt-duplication.mjs'
9
+ const YML = 'D:/wd/vibemath开发/Vibe-Mathematics/vibe-math-v5/agent.cordis.yml'
10
+ const run = (env) => {
11
+ const r = spawnSync(process.execPath, [SUITE, '--json'], { encoding: 'utf8', timeout: 120000, env: Object.assign({}, process.env, env || {}) })
12
+ let parsed = null
13
+ try { parsed = JSON.parse(r.stdout) } catch (e) { /* reported below */ }
14
+ return { code: r.status, parsed, out: String(r.stdout || '') + String(r.stderr || '') }
15
+ }
16
+ const control = run({})
17
+ const MUTS = [
18
+ ['single-copy wording drift', ['YOUR OWN boolean vote on that object in the SAME reply is counted as an\n ABSTENTION', 'YOUR OWN boolean vote on that object in the SAME reply is NOT counted at all']],
19
+ ['single-copy P1 removal', ['or when a written\n flow requires it', 'or never']],
20
+ ]
21
+ let problems = 0
22
+ console.log('control: exit=' + control.code + ' | ' + JSON.stringify(control.parsed && { passed: control.parsed.passed, failed: control.parsed.failed }))
23
+ if (control.code !== 0) problems += 1
24
+ for (const [name, mut] of MUTS) {
25
+ const r = run({ V5_PROMPT_MUTATE: JSON.stringify(mut) })
26
+ const failed = r.parsed ? r.parsed.failed : -1
27
+ const named = !!(r.parsed && (r.parsed.failures || []).some((f) => /IDENTICAL after normalisation|ABSTENTION|written flow requires it/.test(f)))
28
+ const ok = r.code !== 0 && failed > 0 && named
29
+ if (!ok) problems += 1
30
+ console.log(name.padEnd(28) + ': exit=' + r.code + ' failed=' + failed + (ok ? ' RED (as required)' : ' NOT RED'))
31
+ for (const f of ((r.parsed && r.parsed.failures) || []).slice(0, 2)) console.log(' → ' + String(f).slice(0, 140))
32
+ }
33
+ console.log(problems === 0 ? '\nPROMPT-DUPLICATION MUTANTS: every mutation turns a NAMED check red' : '\nPROMPT-DUPLICATION MUTANTS: ' + problems + ' problem(s)')
34
+ process.exit(problems === 0 ? 0 : 1)