dsh-vibe-math 2.3.0 → 2.3.1

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 (30) hide show
  1. package/AUDIT-CHECKLIST.md +33 -3
  2. package/README.md +21 -0
  3. package/RELEASE-NOTES-2.3.1.md +134 -0
  4. package/audit-formal-sensitivity.mjs +125 -39
  5. package/audit-v5-integrity.mjs +5 -3
  6. package/docs/formal-verification.md +92 -12
  7. package/docs/test-timing.md +79 -0
  8. package/formal-verify-v2.test.mjs +286 -7
  9. package/formal-verify-v3.test.mjs +215 -8
  10. package/formal-verify-v4.test.mjs +282 -3
  11. package/formal-verify-v5.test.mjs +72 -0
  12. package/package.json +9 -2
  13. package/prompt-corpus-v2/formal-verify-v2.json +394 -0
  14. package/prompt-corpus-v2/formal-verify-v2.md +4250 -0
  15. package/prompt-corpus-v3/formal-verify-v3.json +159 -57
  16. package/prompt-corpus-v3/formal-verify-v3.md +1302 -285
  17. package/prompt-corpus-v4/formal-verify-v4.json +84 -0
  18. package/prompt-corpus-v4/formal-verify-v4.md +255 -0
  19. package/prompt-corpus-v5/prompt-corpus-v5.json +54 -16
  20. package/prompt-corpus-v5/prompt-corpus-v5.md +378 -153
  21. package/prompt-v5-integrity.test.mjs +1158 -1085
  22. package/run-tests.mjs +99 -0
  23. package/vibe-math-v2/vibe-math-v2.js +204 -22
  24. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +77 -4
  25. package/vibe-math-v3/vibe-math-v3.js +82 -21
  26. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +17 -0
  27. package/vibe-math-v4/vibe-math-v4.js +114 -22
  28. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +29 -0
  29. package/vibe-math-v5/vibe-math-v5.js +81 -22
  30. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +27 -4
@@ -1,1085 +1,1158 @@
1
- // ============================================================
2
- // Vibe-Math-V5 PROMPT & INTERACTION INTEGRITY SUITE
3
- //
4
- // Why this suite exists
5
- // ---------------------
6
- // The 2026-09 field test (D:\wd\vibemath测试\项目测试1) exposed a fatal class of bug that
7
- // 123 pre-existing assertions could not see: the framework built every member's brief
8
- // from a mutable "currentMember" global, so each member was told it was SOMEBODY ELSE —
9
- // "[状态] 你是 r-2" appeared inside r-3's induction brief, and the academician's brief
10
- // said "你是 ?(常驻研究员)… 有表决权者 0 人". Nothing asserted the TEXT a member
11
- // actually reads, so the entire class was invisible to the test suite.
12
- //
13
- // This suite therefore treats the PROMPT as the product:
14
- // · every prompt the framework sends is captured verbatim;
15
- // · every prompt is checked for IDENTITY coherence (does it name the member that
16
- // receives it, in its header, its [状态] block and its persona?), ROSTER/quorum
17
- // coherence, and INTERACTION coherence (do framed messages name the true sender
18
- // and the true kind?);
19
- // · the FULL prompt corpus is written to prompt-corpus-v5/ so the real interaction
20
- // content is preserved for human review, not just reduced to pass/fail.
21
- //
22
- // Each case runs in its OWN session root, so one case can never leave a meeting or a
23
- // verification in flight to pollute the next one.
24
- //
25
- // Run: node prompt-v5-integrity.test.mjs
26
- // Env: V5_PLUGIN=<abs path> point the suite at a mutated copy (sensitivity probes)
27
- // V5_CORPUS_DIR=<dir> where to write the corpus (default: ./prompt-corpus-v5)
28
- // ============================================================
29
- import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
30
- import { tmpdir } from 'node:os'
31
- import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
32
- import { fileURLToPath } from 'node:url'
33
-
34
- // fileURLToPath, not URL.pathname: a Windows path with non-ASCII characters comes back
35
- // percent-encoded from `.pathname`, which would silently write the corpus into a
36
- // directory literally named "%E5%BC%80...".
37
- const HERE = dirname(fileURLToPath(import.meta.url))
38
- const PLUGIN = process.env.V5_PLUGIN
39
- ? new URL('file:///' + String(process.env.V5_PLUGIN).replace(/\\/g, '/'))
40
- : new URL('./vibe-math-v5/vibe-math-v5.js', import.meta.url)
41
- const CORPUS_DIR = process.env.V5_CORPUS_DIR ? pathResolve(process.env.V5_CORPUS_DIR) : join(HERE, 'prompt-corpus-v5')
42
- const WS = mkdtempSync(join(tmpdir(), 'vibe-v5-prompt-'))
43
-
44
- let passed = 0, failed = 0
45
- const failures = []
46
- const assert = (c, m) => {
47
- if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) }
48
- }
49
- const sleep = ms => new Promise(r => setTimeout(r, ms))
50
- const section = (t) => console.log('\n[' + t + ']')
51
-
52
- // ---------------------------------------------------------------
53
- // mock host
54
- // ---------------------------------------------------------------
55
- function makeProjectionRegistry() {
56
- const units = new Map()
57
- const cells = new Map()
58
- const cellMap = (sess) => {
59
- const id = String(sess.id)
60
- let m = cells.get(id)
61
- if (!m) { m = new Map(); cells.set(id, m) }
62
- return m
63
- }
64
- return {
65
- register(def) { units.set(def.key, def); return () => { units.delete(def.key) } },
66
- stateOf(session, key) {
67
- const def = units.get(key)
68
- if (!def) return undefined
69
- const m = cellMap(session)
70
- if (!m.has(key)) m.set(key, def.init(session.header, session.inheritedEventCount || 0))
71
- return m.get(key)
72
- },
73
- _drive(session, event) {
74
- const m = cellMap(session)
75
- for (const [k, def] of units) {
76
- const cur = m.has(k) ? m.get(k) : def.init(session.header, session.inheritedEventCount || 0)
77
- let next
78
- try { next = def.apply(cur, event) } catch (e) { next = cur }
79
- m.set(k, next)
80
- }
81
- },
82
- }
83
- }
84
-
85
- const projections = makeProjectionRegistry()
86
- const listeners = {}
87
- const toolRegs = []
88
- const liveAgents = new Map()
89
-
90
- const spawns = [] // { label, childId, rootId, persona, prompt, toolFilter }
91
- const wakes = [] // queued sends not yet handled
92
- const delivered = [] // sends that drainWakes actually handled
93
- let failNextStarts = 0
94
-
95
- function makeMockSession(id, parentSession) {
96
- const events = []
97
- const s = {
98
- id,
99
- header: { version: 1, id, createdAt: Date.now(), cwd: WS, parentSession, isSeeded: false },
100
- inheritedEventCount: 0,
101
- get seq() { return events.length },
102
- append(type, data) {
103
- const ev = { type, data, seq: events.length, time: Date.now() }
104
- events.push(ev)
105
- projections._drive(s, ev)
106
- return ev
107
- },
108
- deriveMessages() { return [] },
109
- snapshotEvents(from) { return events.slice(from || 0) },
110
- ownEvents() { return events.slice() },
111
- _events: events,
112
- }
113
- return s
114
- }
115
-
116
- const roots = new Map()
117
- let rootSeq = 0
118
- function makeRoot() {
119
- const id = 'sess-' + String.fromCharCode(65 + rootSeq++)
120
- const session = makeMockSession(id, undefined)
121
- const root = { id, options: { provider: 'mock', model: 'm' }, session, ctx: undefined }
122
- roots.set(id, root)
123
- return root
124
- }
125
-
126
- const ctx = {
127
- get(name) {
128
- if (name === 'sessionProjections') return projections
129
- if (name === 'sandboxPolicy') return undefined
130
- if (name === 'compaction') return undefined
131
- if (name === 'subprocess') {
132
- return {
133
- async resolveExecutable(cmd) { return String(cmd) },
134
- spawn({ argv }) {
135
- const last = argv[argv.length - 1] || ''
136
- // directory creation still goes through the same mock (mkdirs uses a shell)
137
- if (/New-Item/.test(last)) {
138
- const paths = []
139
- const re = /'((?:[^']|'')*)'/g
140
- let m
141
- while ((m = re.exec(last)) !== null) paths.push(m[1].replace(/''/g, "'"))
142
- for (const q of paths) if (q && !/^-/.test(q)) mkdirSync(q, { recursive: true })
143
- return { done: Promise.resolve({ exitCode: 0, signal: null }), collected: {}, terminate() {} }
144
- }
145
- // The fake Lean toolchain: GREEN unless the file still uses sorry / carries -- FAIL.
146
- // Case 12 needs a proof that really passes so the prompt switches to fidelity review.
147
- const text = existsSync(last) ? readFileSync(last, 'utf8') : ''
148
- const bad = /sorry|-- FAIL/.test(text)
149
- const ok = { text: 'ok\n', nextOffset: 3, lossy: false }
150
- const err = { text: bad ? 'error: declaration uses sorry\n' : '', nextOffset: 0, lossy: false }
151
- return {
152
- done: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null }),
153
- collected: { stdout: { readFrom: () => ok }, stderr: { readFrom: () => err } },
154
- terminate() {},
155
- }
156
- },
157
- }
158
- }
159
- return undefined
160
- },
161
- on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
162
- effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
163
- logger: { info() {}, warn() {}, error() {} },
164
- timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
165
- tools: { register(spec) { toolRegs.push(spec); return () => {} } },
166
- commands: { register() { return () => {} } },
167
- sessions: { async flush() { return true } },
168
- subagents: {
169
- list() { return ['spawn'] },
170
- async startContinuable({ label, request }) {
171
- if (failNextStarts > 0) { failNextStarts -= 1; throw new Error('mock provisioning failure') }
172
- const rootId = (request && request.parent && request.parent.id) || 'sess-A'
173
- const id = 'c' + (spawns.length + 1)
174
- liveAgents.set(id, { id, session: makeMockSession(id, rootId), options: request && request.agentOptions })
175
- spawns.push({
176
- label, childId: id, rootId,
177
- persona: request && request.persona,
178
- prompt: request && request.prompt && request.prompt[0] && request.prompt[0].text,
179
- toolFilter: request && request.toolFilter,
180
- })
181
- return { childId: id, messageId: 'm' + spawns.length }
182
- },
183
- async sendMessage(parent, childId, blocks) {
184
- wakes.push({ childId, rootId: (parent && parent.id) || 'sess-A', prompt: (blocks && blocks[0] && blocks[0].text) || '' })
185
- return 'w' + (delivered.length + wakes.length)
186
- },
187
- interrupt() {},
188
- async drainContinuableChildren(parent, ids) { for (const i of ids) liveAgents.delete(i) },
189
- },
190
- agents: {
191
- roots() { return [...roots.values()] },
192
- get(id) { return roots.get(id) || liveAgents.get(id) },
193
- list() { return [...roots.values(), ...liveAgents.values()] },
194
- },
195
- fs: {
196
- async resolve(rel, opts) {
197
- const b = (opts && opts.cwd) || WS
198
- const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/'))
199
- return { targetKey: p, displayPath: p }
200
- },
201
- async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
202
- async readText(t) { return readFileSync(t.targetKey, 'utf8') },
203
- async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
204
- async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
205
- },
206
- }
207
-
208
- const mod = await import(PLUGIN.href + '?t=' + Date.now())
209
- const plugin = mod.default || mod
210
- plugin.apply(ctx)
211
-
212
- // ---------------------------------------------------------------
213
- // driving helpers
214
- // ---------------------------------------------------------------
215
- async function callTool(name, args, agent) {
216
- const spec = toolRegs.find(x => x.name === name)
217
- if (!spec) throw new Error('no tool ' + name)
218
- return JSON.parse(await spec.execute(args || {}, { agent }))
219
- }
220
- const childAgent = (childId) => liveAgents.get(childId)
221
- function fireEnd(childId, reply, stopReason) {
222
- const blocks = reply === undefined ? [] : [{ type: 'text', text: '```json\n' + JSON.stringify(reply) + '\n```' }]
223
- for (const h of (listeners['subagent/end'] || [])) {
224
- h({ id: childId, runId: 'r', provider: 'spawn', local: true, stopReason: stopReason || 'completed', lastAssistantMessage: blocks })
225
- }
226
- }
227
- const settle = async () => { await sleep(30) }
228
- const memberOfChild = (childId) => {
229
- const s = spawns.find(x => x.childId === childId)
230
- const m = s ? /vibe5 (\S+) /.exec(s.label) : null
231
- return m ? m[1] : ''
232
- }
233
- const spawnOf = (root, memberId) => spawns.find(s => s.rootId === root.id && s.label.indexOf('vibe5 ' + memberId + ' ') !== -1)
234
- const childOf = (root, memberId) => { const s = spawnOf(root, memberId); return s ? s.childId : '' }
235
- const spawnsFor = (root) => spawns.filter(s => s.rootId === root.id)
236
-
237
- // Pull exactly the VOTING prompts for one root. A plain FIFO drain returns whatever was
238
- // queued first (work rounds, heartbeats), which is how an earlier version of this case ended
239
- // up asserting against the wrong prompt entirely.
240
- async function takeVerifyPrompts(root, n) {
241
- const got = []
242
- for (let guard = 0; guard < 400 && got.length < n; guard++) {
243
- const idx = wakes.findIndex(w => w.rootId === root.id && /【求真表决/.test(w.prompt))
244
- if (idx === -1) {
245
- const other = wakes.findIndex(w => w.rootId === root.id)
246
- if (other !== -1) {
247
- const w = wakes.splice(other, 1)[0]
248
- delivered.push({ prompt: w.prompt, owner: memberOfChild(w.childId), rootId: w.rootId })
249
- fireEnd(w.childId, { progress: '(语料采样时略过非表决轮)', contextPct: 20 })
250
- await settle()
251
- continue
252
- }
253
- await settle()
254
- continue
255
- }
256
- const w = wakes.splice(idx, 1)[0]
257
- got.push(w)
258
- delivered.push({ prompt: w.prompt, owner: memberOfChild(w.childId), rootId: w.rootId })
259
- fireEnd(w.childId, { verdict: { target: (/"target"\s*:\s*"([^"]+)"/.exec(w.prompt) || [])[1] || '', verdict: 0.5, reason: '语料采样' }, contextPct: 20 })
260
- await settle()
261
- }
262
- return got
263
- }
264
-
265
- let votePlan = new Map() // memberId -> verdict number for the next verify prompts
266
- let replyOverride = new Map() // memberId -> the exact reply its NEXT wake must produce
267
- // Roots whose MEETING prompts the driver must NOT answer, so the meeting stays in flight
268
- // (case 10b needs a live meeting to test that a verification cannot preempt it).
269
- const hushed = new Set()
270
- // Handle queued sends. `delivered` collects what was actually sent for the case under
271
- // test, because the queue is consumed here and assertions must not read it afterwards.
272
- // Wakes belonging to OTHER roots are skipped over rather than allowed to block: a case
273
- // with a short heartbeat keeps producing its own wakes, and a naive
274
- // "stop at the first foreign wake" loop would starve every later case.
275
- async function drainWakes(budget, root) {
276
- let n = 0
277
- while (n < budget) {
278
- const idx = wakes.findIndex(w => (!root || w.rootId === root.id)
279
- && !(hushed.has(w.rootId) && /【研究所会议/.test(w.prompt)))
280
- if (idx === -1) break
281
- const w = wakes.splice(idx, 1)[0]
282
- const owner = memberOfChild(w.childId)
283
- delivered.push({ prompt: w.prompt, owner, childId: w.childId, rootId: w.rootId })
284
- let reply
285
- if (replyOverride.has(owner)) { reply = replyOverride.get(owner); replyOverride.delete(owner) } else if (/【求真表决/.test(w.prompt)) {
286
- const target = (/"target"\s*:\s*"([^"]+)"/.exec(w.prompt) || [])[1] || ''
287
- const v = votePlan.has(owner) ? votePlan.get(owner) : 0.5
288
- reply = { verdict: { target, verdict: v, reason: owner + ' 的判断' }, contextPct: 20 }
289
- } else if (/【研究所会议/.test(w.prompt)) {
290
- reply = { input: owner + ':我的意见。', solved: false, contextPct: 20 }
291
- } else {
292
- reply = { progress: owner + ':继续推进。', solved: false, contextPct: 20 }
293
- }
294
- fireEnd(w.childId, reply)
295
- n++
296
- await settle()
297
- }
298
- return n
299
- }
300
- // A case is over: pause it so it can never generate a wake that would leak into the
301
- // next case, and drop anything it still had queued.
302
- async function endCase(root) {
303
- await callTool('vibe_v5_pause', {}, root)
304
- for (let i = wakes.length - 1; i >= 0; i--) if (wakes[i].rootId === root.id) wakes.splice(i, 1)
305
- }
306
- async function settleInstitute(root, rounds = 14) {
307
- for (let i = 0; i < rounds; i++) {
308
- await drainWakes(40, root)
309
- await sleep(20)
310
- const st = await callTool('vibe_v5_status', {}, root)
311
- if (!st.members.some(m => m.busy) && !st.meeting && !st.verify && wakes.length === 0) return st
312
- }
313
- await drainWakes(40, root)
314
- return await callTool('vibe_v5_status', {}, root)
315
- }
316
-
317
- // ---------------------------------------------------------------
318
- // prompt inspection
319
- // ---------------------------------------------------------------
320
- const KINDS = ['院士', '常驻研究员', '临时工']
321
- const reState = new RegExp('\\[状态\\]\\s*你是\\s+(\\S+?)((' + KINDS.join('|') + '))|轮次\\s*(\\d+)|法定票数\\s*m=(\\d+)|有表决权者\\s*(\\d+)\\s*人')
322
- const reRoster = /\[在册\]\s*(.*)/
323
- const reAbsent = /\[未就位\]\s*(.*)/
324
- const reHeader = /^【([^】]*)】/m
325
-
326
- function headerMember(prompt) {
327
- const h = (reHeader.exec(prompt) || [])[1]
328
- if (!h) return { header: '', id: '', kind: '' }
329
- for (const k of KINDS) {
330
- let m = new RegExp('——\\s*' + k + '\\s+(\\S+?)\\s*】?$').exec(h)
331
- if (m) return { header: h, id: m[1], kind: k }
332
- m = new RegExp('——\\s*' + k + '\\s+(\\S+?)\\s+就对象').exec(h)
333
- if (m) return { header: h, id: m[1], kind: k }
334
- }
335
- return { header: h, id: '', kind: '' }
336
- }
337
- function parseState(prompt) {
338
- const s = reState.exec(prompt)
339
- if (!s) return null
340
- return { id: s[1], kind: s[2], round: Number(s[3]), m: Number(s[4]), voters: Number(s[5]) }
341
- }
342
- const rosterOf = (prompt) => {
343
- const r = reRoster.exec(prompt)
344
- return r ? r[1].split(/[、,]/).map(s => s.trim()).filter(s => s && s !== '(无)') : null
345
- }
346
- const absentOf = (prompt) => {
347
- const r = reAbsent.exec(prompt)
348
- return r ? r[1].split(/[、,]/).map(s => s.trim()).filter(Boolean) : null
349
- }
350
- const stateBlockCount = (prompt) => (prompt.match(/\[状态\]/g) || []).length
351
- const GARBAGE = [/\bundefined\b/, /\bNaN\b/, /\[object Object\]/, /你是\s*\?/]
352
- const isVoter = (id) => id === 'acad' || /^r-/.test(id)
353
-
354
- // Applied to EVERY captured prompt.
355
- function checkPromptSweep(prompt, owner, where) {
356
- const problems = []
357
- if (!prompt) return ['prompt is empty']
358
- for (const g of GARBAGE) if (g.test(prompt)) problems.push('contains ' + g)
359
- if (stateBlockCount(prompt) !== 1) problems.push('expected exactly one [状态] block, found ' + stateBlockCount(prompt))
360
- const st = parseState(prompt)
361
- if (!st) { problems.push('no parseable [状态] line'); return problems }
362
- if (st.id !== owner) problems.push('[状态] names ' + st.id + ' but was sent to ' + owner)
363
- const hd = headerMember(prompt)
364
- if (hd.id && hd.id !== owner) problems.push('header names ' + hd.id + ' but was sent to ' + owner)
365
- if (hd.kind && st.kind && hd.kind !== st.kind) problems.push('header kind ' + hd.kind + ' ≠ [状态] kind ' + st.kind)
366
- const roster = rosterOf(prompt)
367
- if (!roster) problems.push('[在册] line missing')
368
- else {
369
- if (roster.indexOf(owner) === -1) problems.push('the roster omits the reader ' + owner + ' ([' + roster.join('、') + '])')
370
- if (new Set(roster).size !== roster.length) problems.push('duplicate ids in [在册]')
371
- for (const a of (absentOf(prompt) || [])) {
372
- const id = String(a).replace(/(.*$/, '')
373
- if (roster.indexOf(id) !== -1) problems.push(id + ' is on the roster AND listed as 未就位')
374
- }
375
- if (st.voters !== roster.filter(isVoter).length) {
376
- problems.push('有表决权者 ' + st.voters + ' ≠ voters in [在册] ' + roster.filter(isVoter).length)
377
- }
378
- if (st.m !== Math.min(3, st.voters)) problems.push('m=' + st.m + ' ≠ min(quorumCap 3, voters ' + st.voters + ')')
379
- }
380
- for (const other of KINDS) {
381
- const hits = prompt.match(new RegExp('你是\\s+\\S+?(' + other + ')', 'g')) || []
382
- if (hits.length > 1) problems.push('more than one identity claim: ' + hits.join(' / '))
383
- }
384
- if (problems.length) console.error(' !! ' + where + ' → ' + problems.join('; '))
385
- return problems
386
- }
387
-
388
- // ---------------------------------------------------------------
389
- // corpus recorder
390
- // ---------------------------------------------------------------
391
- const corpus = []
392
- const scrub = (s) => String(s == null ? '' : s).split(WS).join('<WS>')
393
- function record(kind, owner, prompt, persona, extra) {
394
- corpus.push({
395
- kind, owner,
396
- sentToLabel: (extra && extra.label) || '',
397
- persona: persona === undefined ? null : scrub(persona),
398
- prompt: scrub(prompt),
399
- toolFilter: (extra && extra.toolFilter) || null,
400
- })
401
- }
402
- function recordAndCheck(kind, owner, prompt, opts) {
403
- record(kind, owner, prompt, opts && opts.persona, opts)
404
- const problems = checkPromptSweep(prompt, owner, kind + ' prompt for ' + owner)
405
- assert(problems.length === 0, kind + ' prompt for ' + owner + ' is identity/roster coherent')
406
- return problems
407
- }
408
- function checkPersona(kind, owner, persona, checks) {
409
- for (const [re, label] of checks) assert(re.test(persona), kind + ': ' + owner + "'s charter " + label)
410
- }
411
-
412
- // ===============================================================
413
- console.log('-- V5 prompt & interaction integrity --')
414
-
415
- // =============== CASE 1: founding briefs =========================================
416
- section('1 founding — every induction brief describes the member that receives it')
417
- const RA = makeRoot()
418
- const started = await callTool('vibe_v5_start', { problem: '求 3N^2-2=b^2 与 3N^2+2=5a^2 的全部整数解', researcherCount: 3 }, RA)
419
- assert(started.ok === true, 'institute founded')
420
- const FOUND_ORDER = ['acad', 'r-1', 'r-2', 'r-3']
421
- const founding = spawnsFor(RA)
422
- assert(founding.length === 4, 'four founding members were started (got ' + founding.length + ')')
423
- for (let i = 0; i < founding.length; i++) {
424
- const sp = founding[i]
425
- const owner = memberOfChild(sp.childId)
426
- assert(owner === FOUND_ORDER[i], 'founding #' + i + ' started ' + owner + ' (expected ' + FOUND_ORDER[i] + ')')
427
- recordAndCheck('founding', owner, sp.prompt, sp)
428
- const st = parseState(sp.prompt) || {}
429
- const roster = rosterOf(sp.prompt) || []
430
- assert(JSON.stringify(roster) === JSON.stringify(FOUND_ORDER.slice(0, i + 1)),
431
- 'the founding brief of ' + owner + ' shows the roster INCLUDING itself: ' + JSON.stringify(roster))
432
- assert(st.round === 1, owner + "'s founding brief is round 1 (got " + st.round + ')')
433
- assert(st.voters === i + 1, owner + ' sees ' + (i + 1) + ' voter(s) (got ' + st.voters + ')')
434
- assert(st.m === Math.min(3, i + 1), owner + ' sees m=min(3,' + (i + 1) + ')=' + Math.min(3, i + 1) + ' (got ' + st.m + ')')
435
- assert(sp.prompt.indexOf('【入职首轮') === 0, owner + "'s first prompt is framed as an induction")
436
- assert(sp.prompt.indexOf('你刚刚加入本所') !== -1, owner + "'s induction asks for its own first view")
437
- }
438
- assert(founding[0].prompt.indexOf('[状态] 你是 acad(院士)') !== -1, 'the academician brief says 你是 acad(院士) — never "?"')
439
- assert(!/你是 \?/.test(founding[0].prompt), 'no "你是 ?" placeholder')
440
- checkPersona('founding', 'acad', founding[0].persona, [
441
- [/在册院士:acad/, 'lists itself as the sitting academician'],
442
- [/在册常驻研究员:(无)/, 'shows no researchers at that instant'],
443
- [/你是「institute」的\*\*院士\*\*/, 'opens by naming its office'],
444
- [/Members\/acad\//, 'points at its own library'],
445
- ])
446
- checkPersona('founding', 'r-3', founding[3].persona, [
447
- [/在册院士:acad/, 'names the sitting academician'],
448
- [/在册常驻研究员:r-1、r-2、r-3/, 'lists r-1、r-2、r-3 as the sitting researchers'],
449
- [/在册临时工:(无)/, 'shows no temps'],
450
- [/代号 r-3。/, 'states its own 代号'],
451
- [/Members\/r-3\//, 'points at its own library'],
452
- [/progress.md/, 'documents Progress/progress.md'],
453
- ])
454
- checkPersona('founding', 'r-1', founding[1].persona, [[/一名常驻研究员/, 'opens as 常驻研究员']])
455
- for (const sp of founding) {
456
- const owner = memberOfChild(sp.childId)
457
- assert(sp.persona.indexOf('Members/' + owner + '/') !== -1, owner + "'s charter points at Members/" + owner + '/')
458
- }
459
- for (const sp of founding) { sp._ended = true; fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
460
- await settleInstitute(RA)
461
- await endCase(RA)
462
-
463
- // =============== CASE 2: round prompts (normal + checkpoint) =====================
464
- section('2 rounds — normal and checkpoint prompts keep the identity straight')
465
- const RB = makeRoot()
466
- await callTool('vibe_v5_start', { problem: '无领头人情形下的组织', researcherCount: 2 }, RB)
467
- for (const sp of spawnsFor(RB)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
468
- await settleInstitute(RB)
469
- // A short idle window makes the heartbeat prompt reachable inside a test run.
470
- await callTool('vibe_v5_set', { activityTimeoutMs: 80, maxParallel: 6, chatDigestMax: 1 }, RB)
471
- // (a) an addressed message must produce a NORMAL round prompt carrying the framed inbox
472
- delivered.length = 0
473
- await callTool('vibe_v5_say', { to: 'r-2', text: '请把你手上的结论同步给我。' }, childAgent(childOf(RB, 'r-1')))
474
- await settle(); await drainWakes(10, RB)
475
- const normalWakes = delivered.slice()
476
- assert(normalWakes.length > 0, 'the addressed message produced a wake (' + normalWakes.length + ')')
477
- for (const w of normalWakes) recordAndCheck('normal', w.owner, w.prompt)
478
- assert(normalWakes.some(w => w.owner === 'r-2' && w.prompt.indexOf('【研究所·私信 from r-1】') !== -1),
479
- 'r-2 is woken with its inbox containing the DM framed from r-1')
480
- assert(normalWakes.filter(w => w.owner === 'r-2').every(w => w.prompt.indexOf('【研究所·私信 from r-2】') === -1),
481
- 'r-2 never receives the DM framed as coming from itself')
482
- // (b) the heartbeat must produce a CHECKPOINT prompt
483
- delivered.length = 0
484
- await sleep(260); await settle(); await drainWakes(10, RB)
485
- let checkpointWakes = delivered.filter(w => /【心跳检查/.test(w.prompt))
486
- if (!checkpointWakes.length) { await sleep(260); await settle(); await drainWakes(10, RB); checkpointWakes = delivered.filter(w => /【心跳检查/.test(w.prompt)) }
487
- assert(checkpointWakes.length > 0, 'the heartbeat produced a checkpoint prompt (' + checkpointWakes.length + ')')
488
- for (const w of checkpointWakes) {
489
- recordAndCheck('checkpoint', w.owner, w.prompt)
490
- // The heartbeat body may be preceded by a delivered inbox or the core-rules recap
491
- // after a real compaction, so match anywhere rather than at offset 0.
492
- assert(w.prompt.indexOf('【心跳检查 —— ') !== -1, w.owner + "'s heartbeat prompt names its own office and id")
493
- assert(new RegExp('【心跳检查 —— (院士|常驻研究员|临时工) ' + w.owner + '】').test(w.prompt),
494
- w.owner + "'s heartbeat header carries its own kind and id")
495
- }
496
- await endCase(RB)
497
-
498
- // =============== CASE 3: interaction framing ====================================
499
- section('3 interaction framing — every message names its true sender and kind')
500
- const RC = makeRoot()
501
- await callTool('vibe_v5_start', { problem: '交互框架测试', researcherCount: 2 }, RC)
502
- for (const sp of spawnsFor(RC)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
503
- await settleInstitute(RC)
504
- await callTool('vibe_v5_set', { maxParallel: 8, chatDigestMax: 1 }, RC)
505
- const r1 = childOf(RC, 'r-1'), acad = childOf(RC, 'acad')
506
- const send = async (kind, fn) => {
507
- delivered.length = 0
508
- const r = await fn()
509
- await settle(); await drainWakes(20, RC)
510
- const handled = delivered.slice()
511
- for (const w of handled) recordAndCheck(kind, w.owner, w.prompt).length
512
- return { r, prompts: handled.map(w => w.prompt).join('\n'), owners: handled.map(w => w.owner), count: handled.length }
513
- }
514
- const dm = await send('inbox-dm', () => callTool('vibe_v5_say', { to: 'r-2', text: '私下问你一下。' }, childAgent(r1)))
515
- const voters = await send('inbox-voters', () => callTool('vibe_v5_say', { to: 'voters', text: '请全体表决者注意。' }, childAgent(r1)))
516
- const chat = await send('inbox-chat', () => callTool('vibe_v5_say', { text: '各位,我建议先做最小反例归约。' }, childAgent(r1)))
517
- const office = await send('inbox-office', () => callTool('vibe_v5_message', { to: 'all', content: '所办通知:请按计划推进。' }, RC))
518
- const assign = await send('inbox-assign', () => callTool('vibe_v5_assign', { subject: '核验模 9 情形', to: 'r-2', why: '你最熟同余', acceptance: '给出模 9 全表' }, childAgent(acad)))
519
- const nudge = await send('inbox-nudge', () => callTool('vibe_v5_nudge', { to: 'r-2', why: '进度偏慢', next_step: '先交一份模 9 表' }, childAgent(acad)))
520
- assert(dm.r.ok === true && dm.r.delivered === 1, 'a DM reaches exactly its addressee')
521
- assert(voters.r.ok === true && voters.r.delivered === 2, 'a voters-only broadcast reaches every voter but the sender')
522
- assert(chat.r.ok === true && chat.r.delivered === 2, 'group chat fans out to every other member')
523
- assert(assign.r.ok === true, "the ACADEMICIAN's assignment succeeded (" + JSON.stringify(assign.r).slice(0, 90) + ')')
524
- assert(/【研究所·私信 from r-1】/.test(dm.prompts), 'a DM is framed by its true sender (【研究所·私信 from r-1】)')
525
- assert(/【研究所·致全体表决者 from r-1】/.test(voters.prompts), 'a voters-only broadcast is framed as such, not as a DM')
526
- assert(/【研究所·群聊】r-1:/.test(chat.prompts), 'group chat is framed with the true speaker')
527
- assert(/【所办通知】/.test(office.prompts), 'an office notice is framed 所办通知')
528
- assert(/【院士分派】/.test(assign.prompts), "the academician's assignment is framed 院士分派")
529
- assert(/【督办 from acad】/.test(nudge.prompts), 'a nudge is framed 督办 by its true author')
530
- assert(!/【院士分派】[^\n]*督办/.test(nudge.prompts), 'a nudge is NOT mislabelled as an assignment')
531
- // THE OFFICE ITSELF must be able to assign, and must not impersonate the academician.
532
- const officeAssign = await send('inbox-office-assign', () => callTool('vibe_v5_assign', { subject: '所办指派', to: 'r-2', why: '所办决定', acceptance: '给出结论' }, RC))
533
- assert(officeAssign.r.ok === true, 'the OFFICE (session root) can assign — it is resolved as the office, not as a random member (' + JSON.stringify(officeAssign.r).slice(0, 90) + ')')
534
- assert(/【所办分派】/.test(officeAssign.prompts), 'an OFFICE assignment is framed 所办分派, not 院士分派')
535
- assert(!/【院士分派】/.test(officeAssign.prompts), 'an office assignment does not impersonate the academician')
536
- const officeNudge = await send('inbox-office-nudge', () => callTool('vibe_v5_nudge', { to: 'r-2', why: '所办督办一下' }, RC))
537
- assert(officeNudge.r.ok === true, 'the OFFICE can nudge')
538
- assert(/【督办 from office】/.test(officeNudge.prompts) && /所办督办/.test(officeNudge.prompts),
539
- 'an office nudge is labelled 所办督办 by the office, not 院士督办')
540
- assert(await callTool('vibe_v5_prioritize', { order: [], why: 'x' }, RC).then(r => r.ok === false), 'the office still hits argument validation (resolved AS the office)')
541
- await endCase(RC)
542
-
543
- // =============== CASE 4: framework feedback delivery ============================
544
- section('4 framework feedback reaches the member (never dropped as a self-message)')
545
- const RD = makeRoot()
546
- await callTool('vibe_v5_start', { problem: '框架反馈投递测试', researcherCount: 2 }, RD)
547
- for (const sp of spawnsFor(RD)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
548
- await settleInstitute(RD)
549
- await callTool('vibe_v5_set', { maxParallel: 8 }, RD)
550
- const rd1 = childOf(RD, 'r-1'), rd2 = childOf(RD, 'r-2')
551
- // A member can only be answered while a turn of its own is in flight, so each case
552
- // below first WAKES r-1 and then lets its reply be the malformed one.
553
- const wakeR1With = async (kind, reply) => {
554
- delivered.length = 0
555
- replyOverride.set('r-1', reply)
556
- await callTool('vibe_v5_say', { to: 'r-1', text: '请就当前状态给个结论。' }, childAgent(rd2))
557
- await settle(); await drainWakes(20, RD)
558
- const handled = delivered.slice()
559
- for (const w of handled) if (/【框架提示】/.test(w.prompt)) recordAndCheck(kind, w.owner, w.prompt).length
560
- return handled.map(w => w.prompt).join('\n')
561
- }
562
- const badVerdictText = await wakeR1With('notice', { verdict: { target: 'p-ghost', verdict: 'not-a-number', reason: 'x' }, contextPct: 20 })
563
- assert(/【框架提示】/.test(badVerdictText), 'a malformed verdict produces a real 【框架提示】 delivery')
564
- assert(/verdict 必须是 0-1 的数值/.test(badVerdictText), 'the notice says what was wrong')
565
- assert(/【框架提示】[^\n]*verdict/.test(badVerdictText), 'the notice is framed by the framework, not by the member itself')
566
- const badClaimText = await wakeR1With('notice-claim', { task_claim: 't-999', contextPct: 20 })
567
- assert(/【框架提示】/.test(badClaimText) && /没有任务 t-999/.test(badClaimText), 'a refused claim is reported back to the claimer')
568
- // A CAS refusal reported through the JSON reply must also come back.
569
- const t = await callTool('vibe_v5_task_create', { subject: '一个没人认领的任务' }, childAgent(rd1))
570
- const staleText = await wakeR1With('notice-task', { task_update: { task_id: t.task.id, expected_revision: 99, action: 'claim' }, contextPct: 20 })
571
- assert(/【框架提示】/.test(staleText) && /V5_TASK_STALE_REVISION/.test(staleText), 'a stale CAS reported through the reply channel is echoed back')
572
- const selfFramed = corpus.filter(c => new RegExp('【研究所·私信 from ' + c.owner + '】').test(c.prompt))
573
- assert(selfFramed.length === 0, 'no member ever receives a message framed as coming from itself')
574
- await endCase(RD)
575
-
576
- // =============== CASE 5: temp workers ===========================================
577
- section('5 a hired temp worker is told its own name, employer and purpose')
578
- const RE = makeRoot()
579
- await callTool('vibe_v5_start', { problem: '临时工入职测试', researcherCount: 2 }, RE)
580
- for (const sp of spawnsFor(RE)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
581
- await settleInstitute(RE)
582
- const hired = await callTool('vibe_v5_hire', { purpose: '核对文献引理', initial_task: '核对第 3 节引理' }, childAgent(childOf(RE, 'r-1')))
583
- assert(hired.ok === true, 'r-1 hired a temp worker (' + JSON.stringify(hired).slice(0, 80) + ')')
584
- const tempSpawn = spawnOf(RE, hired.id)
585
- recordAndCheck('founding-temp', hired.id, tempSpawn.prompt, tempSpawn)
586
- {
587
- const st = parseState(tempSpawn.prompt) || {}
588
- assert(st.kind === '临时工', 'the temp brief calls it 临时工 (got ' + st.kind + ')')
589
- assert((rosterOf(tempSpawn.prompt) || []).indexOf(hired.id) !== -1, 'the temp brief lists the temp itself on the roster')
590
- checkPersona('founding-temp', hired.id, tempSpawn.persona, [
591
- [new RegExp('代号 ' + hired.id + ',由 r-1 雇入'), 'names itself and its true employer'],
592
- [/用途:核对文献引理/, 'states its purpose'],
593
- [new RegExp('Members/' + hired.id + '/'), 'points at its own library'],
594
- [/你的雇主:r-1/, 'names its employer'],
595
- ])
596
- assert(/【入职首轮 —— 临时工 t-\d+】/.test(tempSpawn.prompt), 'the temp brief is framed as its induction')
597
- assert(/你的初始任务\/用途:/.test(tempSpawn.prompt) && /核对第 3 节引理/.test(tempSpawn.prompt), 'the temp brief carries its initial task')
598
- assert(!/"verdict":/.test(tempSpawn.prompt) || /"verdict" 字段对你不适用/.test(tempSpawn.prompt), 'the temp brief states it has no vote')
599
- assert(!/"hire":/.test(tempSpawn.prompt), 'the temp brief does not offer hire')
600
- }
601
- for (const sp of spawnsFor(RE)) { if (!sp._ended) { sp._ended = true; fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':收到。', solved: false, contextPct: 10 }); await settle() } }
602
- await settleInstitute(RE)
603
-
604
- // =============== CASE 6: failed provisioning is visible =========================
605
- section('6 a member that failed to provision is visible, not a phantom')
606
- failNextStarts = 1
607
- const failedHire = await callTool('vibe_v5_hire', { purpose: '注定失败', initial_task: 'x' }, childAgent(childOf(RE, 'r-1')))
608
- assert(failedHire.ok === false, 'a provisioning failure is reported to the hirer (' + JSON.stringify(failedHire).slice(0, 100) + ')')
609
- const stFail = await callTool('vibe_v5_status', {}, RE)
610
- const failedMember = stFail.members.find(m => m.phase === 'failed')
611
- assert(!!failedMember, 'the member is recorded as failed rather than left active')
612
- assert(!failedMember || failedMember.busy !== true, 'a failed member is not left marked busy')
613
- delivered.length = 0
614
- await callTool('vibe_v5_say', { to: 'r-2', text: '看下编制。' }, childAgent(childOf(RE, 'r-1')))
615
- await settle(); await drainWakes(20, RE)
616
- const failPrompts = delivered.slice()
617
- for (const w of failPrompts) recordAndCheck('after-failure', w.owner, w.prompt).length
618
- const failText = failPrompts.map(w => w.prompt).join('\n')
619
- assert(failPrompts.length > 0, 'a member was woken after the failure (' + failPrompts.length + ')')
620
- assert(new RegExp('\\[未就位\\][^\\n]*' + failedMember.id).test(failText), 'the failed member appears in [未就位] with its id')
621
- assert(!new RegExp('\\[在册\\][^\\n]*' + failedMember.id).test(failText), 'the failed member is NOT listed as if it were on the roster')
622
- await endCase(RE)
623
-
624
- // =============== CASE 7: session rebuild ========================================
625
- section('7 a rebuilt session is told it was rebuilt, not that it just joined')
626
- const RF = makeRoot()
627
- await callTool('vibe_v5_start', { problem: '会话重建测试', researcherCount: 2 }, RF)
628
- for (const sp of spawnsFor(RF)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
629
- await settleInstitute(RF)
630
- const personasBefore = {}
631
- for (const sp of spawnsFor(RF)) personasBefore[memberOfChild(sp.childId)] = sp.persona
632
- await callTool('vibe_v5_stop', {}, RF)
633
- await settle()
634
- const spawnCountBefore = spawns.length
635
- const resumed = await callTool('vibe_v5_resume', {}, RF)
636
- assert(resumed.ok === true, 'the institute resumed (' + JSON.stringify(resumed).slice(0, 120) + ')')
637
- const resumeSpawns = spawns.slice(spawnCountBefore).filter(s => s.rootId === RF.id)
638
- assert(resumeSpawns.length > 0, 'resume rebuilt at least one member session (' + resumeSpawns.length + ')')
639
- for (const sp of resumeSpawns) {
640
- const owner = memberOfChild(sp.childId)
641
- recordAndCheck('resume', owner, sp.prompt, sp)
642
- assert(sp.prompt.indexOf('【会话重建 —— ') === 0, owner + "'s rebuilt session is framed 会话重建, not 入职首轮")
643
- assert(sp.prompt.indexOf('你刚刚加入本所') === -1, owner + ' is NOT told "你刚刚加入本所" on resume')
644
- assert(sp.prompt.indexOf('不要从头再来') !== -1, owner + ' is told to read back its progress instead of restarting')
645
- assert(sp.persona === personasBefore[owner], owner + "'s charter is the FROZEN hire-time one, not a resumed-time rewrite")
646
- }
647
- await endCase(RF)
648
-
649
- // =============== CASE 8: leaderless institute ===================================
650
- section('8 with academician:false no charter invents a leader')
651
- const RG = makeRoot()
652
- const l2 = await callTool('vibe_v5_start', { problem: '无院士建所', researcherCount: 2, academician: false }, RG)
653
- assert(l2.ok === true, 'a leaderless institute can be founded (' + JSON.stringify(l2).slice(0, 100) + ')')
654
- const l2spawns = spawnsFor(RG)
655
- assert(l2spawns.length === 2, 'two researchers were founded and no academician (got ' + l2spawns.length + ')')
656
- for (let i = 0; i < l2spawns.length; i++) {
657
- const sp = l2spawns[i]
658
- const owner = memberOfChild(sp.childId)
659
- recordAndCheck('founding-leaderless', owner, sp.prompt, sp)
660
- const st = parseState(sp.prompt) || {}
661
- assert(st.m === Math.min(3, i + 1), owner + ': m is computed over the leaderless roster INCLUDING itself (m=' + st.m + ')')
662
- assert(st.kind === '常驻研究员', owner + ' is a 常驻研究员 (got ' + st.kind + ')')
663
- assert(/在册院士:(无)/.test(sp.persona), owner + "'s charter records that there is no academician")
664
- assert(/本所当前\*\*没有在册院士\*\*/.test(sp.persona), owner + "'s charter says so in the organization section")
665
- assert(!/本所的领头人是\*\*院士/.test(sp.persona), owner + "'s charter does NOT claim a leader exists")
666
- assert(!/院士 acad/.test(sp.persona), owner + "'s charter never names a non-existent 院士 acad")
667
- assert(!/主动向院士汇报/.test(sp.persona), owner + "'s charter does not tell it to report to a non-existent academician")
668
- assert(!/院士也可以给你派活/.test(sp.persona), owner + "'s charter does not promise assignments from a non-existent academician")
669
- assert(!/院士可以直接分派任务/.test(sp.persona), owner + "'s charter does not promise academician assignment powers")
670
- }
671
- const l2status = await callTool('vibe_v5_status', {}, RG)
672
- assert(l2status.quorum.voters.indexOf('acad') === -1, 'the leaderless institute has no academician among its voters')
673
- await endCase(RG)
674
-
675
- // =============== CASE 8b: the JSON contract offered matches what is honoured =====
676
- section('8b the reply spec documents exactly the fields the framework honours')
677
- {
678
- const specKinds = corpus.filter(c => ['founding', 'founding-temp', 'founding-leaderless', 'normal', 'checkpoint'].indexOf(c.kind) !== -1)
679
- assert(specKinds.length >= 6, 'the corpus has round prompts to check the reply spec on (' + specKinds.length + ')')
680
- for (const c of specKinds) {
681
- const isTemp = /^t-/.test(c.owner)
682
- const isAcad = c.owner === 'acad'
683
- if (isTemp) {
684
- assert(c.prompt.indexOf('"verdict" 字段对你不适用') !== -1, c.owner + ' (temp) is told it has no vote')
685
- assert(!/"hire":/.test(c.prompt) && !/"fire":/.test(c.prompt), c.owner + ' (temp) is not offered hire/fire')
686
- } else {
687
- assert(/"verdict":/.test(c.prompt), c.owner + ' is offered the verdict field')
688
- assert(/"hire":/.test(c.prompt) && /"fire":/.test(c.prompt), c.owner + ' is offered hire/fire')
689
- }
690
- if (isAcad) {
691
- assert(/"assign":/.test(c.prompt) && /"prioritize":/.test(c.prompt) && /"nudge":/.test(c.prompt) && /"convene_meeting":/.test(c.prompt),
692
- c.owner + ' (academician) is offered its organizational fields')
693
- } else {
694
- assert(!/"assign":/.test(c.prompt) && !/"prioritize":/.test(c.prompt), c.owner + ' is not offered academician-only fields')
695
- }
696
- // A field the framework HONOURS but never documents is an unreachable channel: the
697
- // member cannot object to an assignment, close a task, or fill a meeting input.
698
- assert(/"reject_assign":/.test(c.prompt), c.owner + ' is told about reject_assign (the objection channel is reachable)')
699
- assert(/"task_done":/.test(c.prompt), c.owner + ' is told about task_done')
700
- assert(/"input":/.test(c.prompt), c.owner + ' is told about the meeting "input" field')
701
- }
702
- }
703
-
704
- // =============== CASE 9: verification prompts ===================================
705
- section('9 verification — voters are asked by name about the right object')
706
- const RH = makeRoot()
707
- await callTool('vibe_v5_start', { problem: '表决提示词测试', researcherCount: 2 }, RH)
708
- for (const sp of spawnsFor(RH)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
709
- await settleInstitute(RH)
710
- await callTool('vibe_v5_set', { maxParallel: 8 }, RH)
711
- await callTool('vibe_v5_record_proposition', { id: 'p-lemma-a', title: '引理甲', statement: '若 n>2 则不存在整数解。', value: 0.6, motive: '用于归约', p: 0.8 }, childAgent(childOf(RH, 'r-1')))
712
- delivered.length = 0
713
- const proposed = await callTool('vibe_v5_propose_verify', { target: 'p-lemma-a', kind: 'proposition', reason: '已有证明' }, childAgent(childOf(RH, 'r-1')))
714
- assert(proposed.ok === true, 'the object was proposed for verification')
715
- await settle(); await drainWakes(3, RH)
716
- const verifyPrompts = delivered.filter(w => /【求真表决/.test(w.prompt))
717
- assert(verifyPrompts.length === 3, 'exactly the three voters were asked, and no temp/non-voter (' + verifyPrompts.length + ')')
718
- for (const w of verifyPrompts) {
719
- recordAndCheck('verify', w.owner, w.prompt)
720
- assert(w.prompt.indexOf('【求真表决 —— ') === 0, w.owner + "'s voting prompt is framed as a vote")
721
- assert(w.prompt.indexOf(' ' + w.owner + ' 就对象 p-lemma-a 投票】') !== -1, w.owner + "'s voting prompt names itself and the object")
722
- assert(w.prompt.indexOf('引理甲') !== -1 || w.prompt.indexOf('若 n>2 则不存在整数解') !== -1, w.owner + "'s voting prompt shows the object statement")
723
- assert(/"target"\s*:\s*"p-lemma-a"/.test(w.prompt), w.owner + "'s voting prompt ends with the exact JSON the plugin parses")
724
- assert(w.prompt.indexOf('verdict = 1') !== -1 && w.prompt.indexOf('verdict = 0') !== -1, w.owner + ' is told the boolean rule')
725
- }
726
- // Now actually reach a debate round, to exercise the DEBATE-stage prompt.
727
- await callTool('vibe_v5_set', { verdictMaxRounds: 3 }, RH)
728
- votePlan = new Map([['acad', 0.5], ['r-1', 1], ['r-2', 0.5]])
729
- delivered.length = 0
730
- await drainWakes(3, RH) // round 1: not enough boolean votes -> debate
731
- const stillOpen = await callTool('vibe_v5_status', {}, RH)
732
- assert(!!stillOpen.verify, 'the verification is still open after abstentions')
733
- delivered.length = 0
734
- await drainWakes(3, RH) // round 2 (debate) is asked
735
- const debatePrompts = delivered.filter(w => /【求真表决/.test(w.prompt))
736
- assert(debatePrompts.length === 3, 'the debate round re-asks every voter (' + debatePrompts.length + ')')
737
- for (const w of debatePrompts) {
738
- recordAndCheck('verify-debate', w.owner, w.prompt)
739
- assert(/### 上一轮各成员的意见/.test(w.prompt), w.owner + "'s debate prompt publishes the previous round's opinions")
740
- assert(/verdict=1/.test(w.prompt) && /verdict=0\.5/.test(w.prompt), w.owner + "'s debate prompt shows the real per-member verdicts")
741
- assert(w.prompt.indexOf('- ' + w.owner + ':') !== -1, w.owner + "'s debate prompt shows its OWN previous vote so it can revise it")
742
- const hist = /### 上一轮各成员的意见[\s\S]*?(?:\n\n|$)/.exec(w.prompt)
743
- const histIds = hist ? (hist[0].match(/^- (\S+?):/gm) || []).map(s => s.slice(2, -1)) : []
744
- assert(histIds.slice().sort().join(',') === 'acad,r-1,r-2', w.owner + "'s debate prompt publishes exactly the voters' opinions (got " + histIds.join('、') + ')')
745
- }
746
- await endCase(RH)
747
-
748
- // =============== CASE 9b: only ONE verification at a time =========================
749
- section('9b a second proposal QUEUES; it never starts a concurrent verification')
750
- const RL = makeRoot()
751
- await callTool('vibe_v5_start', { problem: '并发表决测试', researcherCount: 1 }, RL)
752
- for (const sp of spawnsFor(RL)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
753
- await settleInstitute(RL)
754
- await callTool('vibe_v5_set', { maxParallel: 8 }, RL)
755
- const rl1 = childOf(RL, 'r-1')
756
- await callTool('vibe_v5_record_proposition', { id: 'p-first', statement: '第一个对象', value: 0.6, motive: 'm', p: 0.7 }, childAgent(rl1))
757
- await callTool('vibe_v5_record_proposition', { id: 'p-second', statement: '第二个对象', value: 0.6, motive: 'm', p: 0.7 }, childAgent(rl1))
758
- await callTool('vibe_v5_propose_verify', { target: 'p-first', kind: 'proposition', reason: '先做这个' }, childAgent(rl1))
759
- await settle()
760
- const stq0 = await callTool('vibe_v5_status', {}, RL)
761
- assert(!!stq0.verify && stq0.verify.target === 'p-first', 'the first object is under verification')
762
- // Proposing a second object while one is in flight must QUEUE it. The whole point of the
763
- // one-at-a-time rule is that consensus is never split across two live ballots; a
764
- // regression here would silently start a second ballot and drop the object from the queue.
765
- await callTool('vibe_v5_propose_verify', { target: 'p-second', kind: 'proposition', reason: '排后面' }, childAgent(rl1))
766
- await settle()
767
- const stq1 = await callTool('vibe_v5_status', {}, RL)
768
- assert(!!stq1.verify && stq1.verify.target === 'p-first', 'the in-flight ballot is still the first object')
769
- assert((stq1.verifyQueue || []).indexOf('p-second') !== -1,
770
- 'the second proposal is still QUEUED, not begun concurrently (queue=' + JSON.stringify(stq1.verifyQueue) + ')')
771
- assert(stq1.undecided.length === 0 && stq1.verified.length === 0, 'nothing was settled by merely proposing')
772
- // Once the first ballot settles, the queued one starts on its own.
773
- await callTool('vibe_v5_set', { verdictMaxRounds: 1 }, RL)
774
- delivered.length = 0
775
- await drainWakes(20, RL)
776
- const stq2 = await callTool('vibe_v5_status', {}, RL)
777
- assert(stq2.verify === null || stq2.verify.target === 'p-second',
778
- 'the queued object took over after the first ballot closed (now: ' + JSON.stringify(stq2.verify && stq2.verify.target) + ')')
779
- await endCase(RL)
780
-
781
- // =============== CASE 9c: a solve vote OUTSIDE a meeting ==========================
782
- section('9c a unanimous solve vote landing outside a meeting still stops the institute')
783
- const RM = makeRoot()
784
- await callTool('vibe_v5_start', { problem: '会外表决停工测试', researcherCount: 1 }, RM)
785
- for (const sp of spawnsFor(RM)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
786
- await settleInstitute(RM)
787
- await callTool('vibe_v5_set', { maxParallel: 8 }, RM)
788
- const solvedReply = { vote_solved: true, solved: true, progress: '我认为原问题已解决。', contextPct: 20 }
789
- replyOverride.set('acad', solvedReply)
790
- replyOverride.set('r-1', solvedReply)
791
- await callTool('vibe_v5_say', { to: 'acad', text: '请你就"是否已解决"表态。' }, childAgent(childOf(RM, 'r-1')))
792
- await settle(); await drainWakes(4, RM)
793
- await callTool('vibe_v5_say', { to: 'r-1', text: '请你就"是否已解决"表态。' }, childAgent(childOf(RM, 'acad')))
794
- await settle(); await drainWakes(4, RM)
795
- const stSolved = await callTool('vibe_v5_status', {}, RM)
796
- assert(stSolved.solveVotes.length >= 2, 'both voters recorded a solve vote outside any meeting (' + JSON.stringify(stSolved.solveVotes) + ')')
797
- assert(stSolved.autoDone === true,
798
- 'the institute STOPPED on a unanimous solve vote that arrived outside a meeting ' + JSON.stringify({ autoDone: stSolved.autoDone, solveVotes: stSolved.solveVotes }))
799
- await endCase(RM)
800
-
801
- // =============== CASE 10: meeting prompts =======================================
802
- section('10 meeting — real speakers, real transcript keys')
803
- const RI = makeRoot()
804
- await callTool('vibe_v5_start', { problem: '会议提示词测试', researcherCount: 1 }, RI)
805
- for (const sp of spawnsFor(RI)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
806
- await settleInstitute(RI)
807
- await callTool('vibe_v5_set', { maxParallel: 8 }, RI)
808
- delivered.length = 0
809
- const mtg = await callTool('vibe_v5_meeting', { agenda: '分工与下一步', kind: 'sync' }, childAgent(childOf(RI, 'acad')))
810
- assert(mtg.ok === true, 'the academician convened a meeting (' + JSON.stringify(mtg).slice(0, 90) + ')')
811
- await settle(); await drainWakes(20, RI)
812
- const meetingOne = delivered.filter(w => /【研究所会议/.test(w.prompt))
813
- assert(meetingOne.length >= 2, 'both members were asked to speak (' + meetingOne.length + ')')
814
- for (const w of meetingOne) {
815
- recordAndCheck('meeting', w.owner, w.prompt)
816
- assert(w.prompt.indexOf('【研究所会议 mt-1 进行中 —— ') === 0, w.owner + "'s meeting prompt is framed with the meeting id")
817
- assert(w.prompt.indexOf('分工与下一步') !== -1, w.owner + "'s meeting prompt carries the agenda")
818
- assert(w.prompt.indexOf('"input"') !== -1, w.owner + "'s meeting prompt documents the input field it must fill")
819
- }
820
- await settleInstitute(RI)
821
- const stMtg = await callTool('vibe_v5_status', {}, RI)
822
- assert(stMtg.meeting === null, 'the meeting finished instead of deadlocking')
823
- const minutes = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Shared', 'Meetings', 'mt-1.md')
824
- assert(existsSync(minutes), 'the meeting minutes were written')
825
- if (existsSync(minutes)) {
826
- const t = readFileSync(minutes, 'utf8')
827
- assert(/### acad/.test(t) && /### r-1/.test(t), 'the minutes key each speech by its real member id')
828
- assert(/有表决权者:acad、r-1/.test(t), 'the minutes list the real voting members')
829
- }
830
- // A non-academician may only PROPOSE a meeting; the relay must be signed by the proposer.
831
- delivered.length = 0
832
- const propMtg = await callTool('vibe_v5_meeting', { agenda: '我提议讨论路线', kind: 'sync' }, childAgent(childOf(RI, 'r-1')))
833
- assert(propMtg.ok === true && propMtg.proposed === true, 'a researcher can only PROPOSE a meeting (' + JSON.stringify(propMtg).slice(0, 80) + ')')
834
- await settle(); await drainWakes(20, RI)
835
- const propText = delivered.map(w => w.prompt).join('\n')
836
- for (const w of delivered) record('meeting-proposal', w.owner, w.prompt)
837
- assert(/【研究所·致全体表决者 from r-1】[^\n]*提议开会/.test(propText),
838
- 'the meeting proposal is relayed SIGNED BY ITS TRUE PROPOSER r-1, not by whoever was woken last')
839
- await endCase(RI)
840
-
841
- // =============== CASE 10b: meetings and verifications are mutually exclusive ======
842
- section('10b a verification proposed DURING a meeting must queue, never preempt it')
843
- const RN = makeRoot()
844
- await callTool('vibe_v5_start', { problem: '会议与验证互斥测试', researcherCount: 1 }, RN)
845
- for (const sp of spawnsFor(RN)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
846
- await settleInstitute(RN)
847
- await callTool('vibe_v5_set', { maxParallel: 8 }, RN)
848
- await callTool('vibe_v5_record_proposition', { id: 'p-mid', statement: '会议期间提出的对象', value: 0.6, motive: 'm', p: 0.7 }, childAgent(childOf(RN, 'r-1')))
849
- // Convene a meeting and stop before it has collected every input, so it stays in flight.
850
- hushed.add(RN.id)
851
- delivered.length = 0
852
- const convened = await callTool('vibe_v5_meeting', { agenda: '先开这个会', kind: 'sync' }, childAgent(childOf(RN, 'acad')))
853
- assert(convened.ok === true, 'a meeting was convened (' + JSON.stringify(convened).slice(0, 80) + ')')
854
- await settle(); await drainWakes(1, RN)
855
- const during = await callTool('vibe_v5_status', {}, RN)
856
- assert(!!during.meeting, 'the meeting is still in flight (not everyone has spoken)')
857
- // A member proposing a verification mid-meeting must NOT start a second, concurrent
858
- // consensus process: the design says meetings and verifications never overlap, and a
859
- // verification that preempts a meeting starves the meeting's watchdog clock.
860
- const propMid = await callTool('vibe_v5_propose_verify', { target: 'p-mid', kind: 'proposition', reason: '想在会上定' }, childAgent(childOf(RN, 'r-1')))
861
- assert(propMid.ok === true, 'the proposal is accepted (' + JSON.stringify(propMid).slice(0, 90) + ')')
862
- await settle()
863
- const afterProp = await callTool('vibe_v5_status', {}, RN)
864
- assert(!!afterProp.meeting, 'the meeting is STILL in flight after the proposal')
865
- assert(afterProp.verify === null,
866
- 'NO verification started while the meeting was in flight (got ' + JSON.stringify(afterProp.verify && afterProp.verify.target) + ')')
867
- assert((afterProp.verifyQueue || []).indexOf('p-mid') !== -1,
868
- 'the proposal is QUEUED instead (queue=' + JSON.stringify(afterProp.verifyQueue) + ')')
869
- // Once the meeting ends, the queued proposal must run — queueing must not drop it.
870
- hushed.delete(RN.id)
871
- await settleInstitute(RN)
872
- const afterMtg = await callTool('vibe_v5_status', {}, RN)
873
- assert(afterMtg.meeting === null, 'the meeting finished')
874
- assert(afterMtg.verify !== null || afterMtg.undecided.indexOf('p-mid') !== -1 || afterMtg.verified.indexOf('p-mid') !== -1,
875
- 'the queued proposal was started after the meeting ended (verify=' + JSON.stringify(afterMtg.verify && afterMtg.verify.target)
876
- + ', queue=' + JSON.stringify(afterMtg.verifyQueue) + ')')
877
- await endCase(RN)
878
-
879
- // =============== CASE 11: no unpaced re-wake loop ===============================
880
- section('11 a task owner is pushed on a PACED cadence, not in a tight loop')
881
- const RJ = makeRoot()
882
- await callTool('vibe_v5_start', { problem: '调度节奏测试', researcherCount: 1 }, RJ)
883
- for (const sp of spawnsFor(RJ)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
884
- await settleInstitute(RJ)
885
- const asg = await callTool('vibe_v5_assign', { subject: '一个长任务', to: 'r-1', why: '你最合适', acceptance: '给出结果' }, childAgent(childOf(RJ, 'acad')))
886
- assert(asg.ok === true, 'a task was assigned to r-1')
887
- await settle(); await drainWakes(6, RJ)
888
- const afterAssign = await callTool('vibe_v5_status', {}, RJ)
889
- assert(afterAssign.tasks.some(t => t.status === 'in_progress' && t.ownerId === 'r-1'), 'r-1 still owns in-progress work')
890
- assert(!afterAssign.members.some(m => m.busy), 'r-1 is idle again after answering')
891
- // A task owner must NOT be re-woken the moment its turn ends: the work push is paced by
892
- // activityTimeoutMs (120 s here). Without the pace, one unfinished task became an
893
- // unbounded wake -> turn -> wake chain that burned tokens with no backoff at all.
894
- await sleep(500); await settle()
895
- const unpaced = wakes.filter(w => w.rootId === RJ.id)
896
- assert(unpaced.length === 0, 'no unpaced re-wake of the task owner within the idle window (got ' + unpaced.length + ')')
897
- await endCase(RJ)
898
-
899
- // =============== CASE 12: Lean mode prompt text =================================
900
- section('12 Lean formal-verification text enters the prompts (and the corpus)')
901
- const RK = makeRoot()
902
- await callTool('vibe_v5_start', { problem: 'Lean 提示词测试', researcherCount: 2 }, RK)
903
- for (const sp of spawnsFor(RK)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
904
- await settleInstitute(RK)
905
- await callTool('vibe_v5_set', { maxParallel: 8, formalVerify: 'encourage' }, RK)
906
- // (a) an ordinary work round carries the "formalize reusable things as you go" request
907
- delivered.length = 0
908
- await callTool('vibe_v5_say', { to: 'r-1', text: '继续推进。' }, childAgent(childOf(RK, 'acad')))
909
- await settle(); await drainWakes(3, RK)
910
- for (const w of delivered.filter(d => d.rootId === RK.id)) recordAndCheck('lean-work', w.owner, w.prompt)
911
- {
912
- const txt = delivered.filter(d => d.rootId === RK.id).map(d => d.prompt).join('\n')
913
- assert(/\[形式化\] 鼓励 Lean/.test(txt), 'the state block announces the Lean mode with its counts')
914
- assert(/【顺手形式化(鼓励)】/.test(txt), 'the work round asks for reusable objects to be formalized as work proceeds')
915
- }
916
- // (b) a voting round on an object WITHOUT a proof carries the "decide by difficulty" block
917
- await callTool('vibe_v5_record_proposition', { id: 'p-lean-a', statement: 'Lean 语料对象甲', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RK, 'r-1')))
918
- const propA = await callTool('vibe_v5_propose_verify', { target: 'p-lean-a', kind: 'proposition', reason: '语料' }, childAgent(childOf(RK, 'r-1')))
919
- assert(propA.ok === true && propA.started === true, 'the Lean corpus ballot for object 甲 actually started (' + JSON.stringify(propA).slice(0, 90) + ')')
920
- delivered.length = 0
921
- const vwA = await takeVerifyPrompts(RK, 3)
922
- assert(vwA.length === 3, 'captured three voting prompts for object 甲 (got ' + vwA.length + ')')
923
- for (const w of vwA) recordAndCheck('lean-verify', memberOfChild(w.childId), w.prompt)
924
- {
925
- const txt = delivered.filter(d => d.rootId === RK.id).map(d => d.prompt).join('\n')
926
- assert(/【Lean 形式化验证(鼓励模式)】/.test(txt), 'the voting prompt explains the Lean mode')
927
- assert(/你唯一需要确认的就是忠实性/.test(txt), 'the voting prompt states the fidelity question')
928
- }
929
- await endCase(RK)
930
- // (c) once a proof passes, the voting prompt switches to the fidelity review. This uses its
931
- // own root: object 甲's ballot may still be in flight above, and a queued proposal would
932
- // make the drained prompts belong to the WRONG ballot (the assertion would then fail for a
933
- // reason that has nothing to do with the feature).
934
- const RL2 = makeRoot()
935
- await callTool('vibe_v5_start', { problem: 'Lean 忠实性提示词测试', researcherCount: 2 }, RL2)
936
- for (const sp of spawnsFor(RL2)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
937
- await settleInstitute(RL2)
938
- await callTool('vibe_v5_set', { maxParallel: 8, formalVerify: 'encourage' }, RL2)
939
- await callTool('vibe_v5_record_proposition', { id: 'p-lean-b', statement: 'Lean 语料对象乙', value: 0.6, motive: 'm', p: 0.9 }, childAgent(childOf(RL2, 'r-1')))
940
- const leanB = await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-lean-b', content: 'theorem p_lean_b : 1 + 1 = 2 := by decide\n' }, childAgent(childOf(RL2, 'r-1')))
941
- assert(leanB.ok === true && leanB.passed === true, 'object 乙 has a proof that really passes (' + JSON.stringify({ ok: leanB.ok, passed: leanB.passed, code: leanB.run && leanB.run.code }) + ')')
942
- const propB = await callTool('vibe_v5_propose_verify', { target: 'p-lean-b', kind: 'proposition', reason: '已有证明' }, childAgent(childOf(RL2, 'r-1')))
943
- assert(propB.ok === true && propB.started === true, 'the Lean corpus ballot for object 乙 actually started (' + JSON.stringify(propB).slice(0, 90) + ')')
944
- delivered.length = 0
945
- const vwB = await takeVerifyPrompts(RL2, 3)
946
- assert(vwB.length === 3, 'captured three voting prompts for object 乙 (got ' + vwB.length + ')')
947
- for (const w of vwB) recordAndCheck('lean-fidelity', memberOfChild(w.childId), w.prompt)
948
- {
949
- const txt = vwB.map(w => w.prompt).join('\n')
950
- assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(txt), 'the prompt announces the passing proof')
951
- assert(/你不需要重新检查推导/.test(txt), 'with a proof in hand the prompt tells voters not to re-derive')
952
- assert(/忠实性审查/.test(txt), 'and asks for a fidelity review instead')
953
- }
954
- await drainWakes(10, RL2)
955
- await endCase(RL2)
956
-
957
- // =============== PART: full-corpus sweep ========================================
958
- section('13 full-corpus sweep over every prompt ever sent')
959
- {
960
- let swept = 0
961
- for (const sp of spawns) {
962
- if (!sp.prompt) continue
963
- swept++
964
- checkPromptSweep(sp.prompt, memberOfChild(sp.childId), 'corpus spawn ' + memberOfChild(sp.childId))
965
- }
966
- assert(swept >= 12, 'the corpus inspected every founding/resume prompt in the process (' + swept + ')')
967
- const owners = new Set(corpus.map(c => c.owner))
968
- assert(owners.has('acad') && owners.has('r-1') && owners.has('r-2'), 'the corpus covers academician and researchers (' + [...owners].join('、') + ')')
969
- assert([...owners].some(o => /^t-/.test(o)), 'the corpus covers a temp worker')
970
- const kinds = new Set(corpus.map(c => c.kind))
971
- for (const need of ['founding', 'founding-temp', 'founding-leaderless', 'resume', 'normal', 'checkpoint',
972
- 'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
973
- 'inbox-office', 'inbox-assign', 'inbox-nudge', 'notice', 'notice-claim', 'after-failure',
974
- 'lean-work', 'lean-verify', 'lean-fidelity']) {
975
- assert(kinds.has(need), 'the corpus contains a ' + need + ' prompt')
976
- }
977
- assert(corpus.every(c => c.prompt && c.prompt.length > 200), 'no captured prompt is suspiciously short')
978
- assert(corpus.every(c => !GARBAGE.some(g => g.test(c.prompt + (c.persona || '')))), 'no prompt or charter contains undefined/NaN/? garbage')
979
- // A single prompt must not deliver the same message twice. The inbox used to be
980
- // prepended AND re-emitted from the [状态] block, so a member read every new message
981
- // twice in one prompt.
982
- for (const c of corpus) {
983
- const bodies = c.prompt.match(/【[^】]*】[^\n]{20,}/g) || []
984
- for (const frame of new Set(bodies)) {
985
- const n = bodies.filter(b => b === frame).length
986
- if (n > 1) { assert(false, 'message delivered ' + n + '× in one prompt (' + c.kind + '/' + c.owner + '): ' + frame.slice(0, 60)); break }
987
- }
988
- const inboxHeads = (c.prompt.match(/\[新到的消息/g) || []).length
989
- assert(inboxHeads <= 1, c.kind + '/' + c.owner + ': at most one inbox section per prompt (found ' + inboxHeads + ')')
990
- }
991
- assert(true, 'no prompt delivers the same framed message twice, and no prompt has two inbox sections')
992
- // The identity claim inside a prompt must agree with the persona shipped alongside it.
993
- for (const c of corpus) {
994
- if (!c.persona) continue
995
- const st = parseState(c.prompt)
996
- if (!st) continue
997
- assert(c.persona.indexOf('Members/' + st.id + '/') !== -1,
998
- c.kind + ': the charter shipped with ' + st.id + "'s prompt points at Members/" + st.id + '/')
999
- }
1000
- }
1001
-
1002
- // =============== corpus dump ====================================================
1003
- section('14 the full prompt corpus is preserved for human review')
1004
- mkdirSync(CORPUS_DIR, { recursive: true })
1005
- const md = []
1006
- md.push('# Vibe Math V5 — 提示词与交互语料(自动生成,请勿手改)')
1007
- md.push('')
1008
- md.push('由 `prompt-v5-integrity.test.mjs` 在每次运行时重写。这里保存的是**框架真正发给每个')
1009
- md.push('成员的提示词原文**,用于人工复核提示词分配、成员代号与交互内容的正确性。')
1010
- md.push('')
1011
- md.push('- 生成时刻的工作区路径被替换为 `<WS>`,因此内容是确定性的、可 diff 的。')
1012
- md.push('- `owner` 是这条提示词**实际发给的成员**;`kind` 是提示词类型。')
1013
- md.push('- 人设(charter/persona)按成员只完整打印一次,其余条目只记录字符数。')
1014
- md.push('- 这是提示词正确性的人工复核入口:任何“成员代号/职位/在册名单/交互署名”问题')
1015
- md.push(' 都能在这里一眼看出,而不必去翻会话日志。')
1016
- md.push('')
1017
- const seenPersona = new Set()
1018
- const order = ['founding', 'founding-temp', 'founding-leaderless', 'resume', 'normal', 'checkpoint',
1019
- 'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
1020
- 'inbox-office', 'inbox-assign', 'inbox-nudge', 'notice', 'notice-claim', 'after-failure',
1021
- 'lean-work', 'lean-verify', 'lean-fidelity']
1022
- const sorted = corpus.slice().sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind))
1023
- for (let i = 0; i < sorted.length; i++) {
1024
- const c = sorted[i]
1025
- md.push('---')
1026
- md.push('')
1027
- md.push('## [' + (i + 1) + '] kind=`' + c.kind + '` owner=`' + c.owner + '`')
1028
- md.push('')
1029
- if (c.toolFilter) md.push('- toolFilter: `' + JSON.stringify(c.toolFilter) + '`')
1030
- md.push('- charter: ' + (c.persona == null ? '(本次唤醒不带人设)' : c.persona.length + ' 字符'))
1031
- md.push('')
1032
- if (c.persona != null && !seenPersona.has(c.owner)) {
1033
- seenPersona.add(c.owner)
1034
- md.push('### 人设 / 规章(' + c.owner + ',仅首次完整打印)')
1035
- md.push('')
1036
- md.push('```text')
1037
- md.push(c.persona)
1038
- md.push('```')
1039
- md.push('')
1040
- }
1041
- md.push('### 提示词原文')
1042
- md.push('')
1043
- md.push('```text')
1044
- md.push(c.prompt)
1045
- md.push('```')
1046
- md.push('')
1047
- }
1048
- const byKind = {}
1049
- for (const c of corpus) byKind[c.kind] = (byKind[c.kind] || 0) + 1
1050
- md.push('---')
1051
- md.push('')
1052
- md.push('## 统计')
1053
- md.push('')
1054
- for (const k of Object.keys(byKind).sort()) md.push('- `' + k + '`: ' + byKind[k])
1055
- md.push('')
1056
- md.push('- 合计:' + corpus.length + ' 条提示词')
1057
- md.push('')
1058
- const mdPath = join(CORPUS_DIR, 'prompt-corpus-v5.md')
1059
- writeFileSync(mdPath, md.join('\n'), 'utf8')
1060
- writeFileSync(join(CORPUS_DIR, 'prompt-corpus-v5.json'), JSON.stringify({
1061
- note: 'Vibe Math V5 prompt/interaction corpus — generated by prompt-v5-integrity.test.mjs. <WS> = the run workspace.',
1062
- counts: byKind, total: corpus.length,
1063
- prompts: corpus.map(c => ({
1064
- kind: c.kind, owner: c.owner, sentToLabel: c.sentToLabel,
1065
- charterChars: c.persona == null ? null : c.persona.length,
1066
- charter: c.persona, toolFilter: c.toolFilter, prompt: c.prompt,
1067
- })),
1068
- }, null, 2), 'utf8')
1069
- assert(existsSync(mdPath), 'the prompt corpus Markdown was written')
1070
- assert(existsSync(join(CORPUS_DIR, 'prompt-corpus-v5.json')), 'the prompt corpus JSON was written')
1071
- const corpusMd = readFileSync(mdPath, 'utf8')
1072
- assert(corpusMd.length > 30000, 'the corpus is substantial (' + corpusMd.length + ' chars) — the real prompt text is preserved')
1073
- assert(corpusMd.indexOf('[状态] 你是 acad(院士)') !== -1, 'a human can verify the academician brief verbatim')
1074
- assert(corpusMd.indexOf('[状态] 你是 r-2') !== -1, 'a human can verify a researcher brief naming itself')
1075
- assert(corpusMd.indexOf('【框架提示】') !== -1, 'the corpus contains the framework-feedback interaction')
1076
- assert(corpusMd.indexOf('【会话重建 —— ') !== -1, 'the corpus contains a resume brief')
1077
- assert(corpusMd.indexOf('【所办分派】') !== -1, 'the corpus contains an office assignment')
1078
- assert(!/你是 \?/.test(corpusMd), 'the corpus contains NO wrong-identity "?" brief')
1079
-
1080
- console.log('')
1081
- console.log('corpus: ' + mdPath)
1082
- console.log('passed=' + passed + ' failed=' + failed)
1083
- if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
1084
- console.log('ALL GREEN')
1085
- process.exit(0)
1
+ // ============================================================
2
+ // Vibe-Math-V5 PROMPT & INTERACTION INTEGRITY SUITE
3
+ //
4
+ // Why this suite exists
5
+ // ---------------------
6
+ // The 2026-09 field test (D:\wd\vibemath测试\项目测试1) exposed a fatal class of bug that
7
+ // 123 pre-existing assertions could not see: the framework built every member's brief
8
+ // from a mutable "currentMember" global, so each member was told it was SOMEBODY ELSE —
9
+ // "[状态] 你是 r-2" appeared inside r-3's induction brief, and the academician's brief
10
+ // said "你是 ?(常驻研究员)… 有表决权者 0 人". Nothing asserted the TEXT a member
11
+ // actually reads, so the entire class was invisible to the test suite.
12
+ //
13
+ // This suite therefore treats the PROMPT as the product:
14
+ // · every prompt the framework sends is captured verbatim;
15
+ // · every prompt is checked for IDENTITY coherence (does it name the member that
16
+ // receives it, in its header, its [状态] block and its persona?), ROSTER/quorum
17
+ // coherence, and INTERACTION coherence (do framed messages name the true sender
18
+ // and the true kind?);
19
+ // · the FULL prompt corpus is written to prompt-corpus-v5/ so the real interaction
20
+ // content is preserved for human review, not just reduced to pass/fail.
21
+ //
22
+ // Each case runs in its OWN session root, so one case can never leave a meeting or a
23
+ // verification in flight to pollute the next one.
24
+ //
25
+ // Run: node prompt-v5-integrity.test.mjs
26
+ // Env: V5_PLUGIN=<abs path> point the suite at a mutated copy (sensitivity probes)
27
+ // V5_CORPUS_DIR=<dir> where to write the corpus (default: ./prompt-corpus-v5)
28
+ // ============================================================
29
+ import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
30
+ import { tmpdir } from 'node:os'
31
+ import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
32
+ import { fileURLToPath } from 'node:url'
33
+
34
+ // fileURLToPath, not URL.pathname: a Windows path with non-ASCII characters comes back
35
+ // percent-encoded from `.pathname`, which would silently write the corpus into a
36
+ // directory literally named "%E5%BC%80...".
37
+ const HERE = dirname(fileURLToPath(import.meta.url))
38
+ const PLUGIN = process.env.V5_PLUGIN
39
+ ? new URL('file:///' + String(process.env.V5_PLUGIN).replace(/\\/g, '/'))
40
+ : new URL('./vibe-math-v5/vibe-math-v5.js', import.meta.url)
41
+ const CORPUS_DIR = process.env.V5_CORPUS_DIR ? pathResolve(process.env.V5_CORPUS_DIR) : join(HERE, 'prompt-corpus-v5')
42
+ const WS = mkdtempSync(join(tmpdir(), 'vibe-v5-prompt-'))
43
+
44
+ let passed = 0, failed = 0
45
+ const failures = []
46
+ const assert = (c, m) => {
47
+ if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) }
48
+ }
49
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
50
+ const section = (t) => console.log('\n[' + t + ']')
51
+
52
+ // ---------------------------------------------------------------
53
+ // mock host
54
+ // ---------------------------------------------------------------
55
+ function makeProjectionRegistry() {
56
+ const units = new Map()
57
+ const cells = new Map()
58
+ const cellMap = (sess) => {
59
+ const id = String(sess.id)
60
+ let m = cells.get(id)
61
+ if (!m) { m = new Map(); cells.set(id, m) }
62
+ return m
63
+ }
64
+ return {
65
+ register(def) { units.set(def.key, def); return () => { units.delete(def.key) } },
66
+ stateOf(session, key) {
67
+ const def = units.get(key)
68
+ if (!def) return undefined
69
+ const m = cellMap(session)
70
+ if (!m.has(key)) m.set(key, def.init(session.header, session.inheritedEventCount || 0))
71
+ return m.get(key)
72
+ },
73
+ _drive(session, event) {
74
+ const m = cellMap(session)
75
+ for (const [k, def] of units) {
76
+ const cur = m.has(k) ? m.get(k) : def.init(session.header, session.inheritedEventCount || 0)
77
+ let next
78
+ try { next = def.apply(cur, event) } catch (e) { next = cur }
79
+ m.set(k, next)
80
+ }
81
+ },
82
+ }
83
+ }
84
+
85
+ const projections = makeProjectionRegistry()
86
+ const listeners = {}
87
+ const toolRegs = []
88
+ const liveAgents = new Map()
89
+
90
+ const spawns = [] // { label, childId, rootId, persona, prompt, toolFilter }
91
+ const wakes = [] // queued sends not yet handled
92
+ const delivered = [] // sends that drainWakes actually handled
93
+ let failNextStarts = 0
94
+
95
+ function makeMockSession(id, parentSession) {
96
+ const events = []
97
+ const s = {
98
+ id,
99
+ header: { version: 1, id, createdAt: Date.now(), cwd: WS, parentSession, isSeeded: false },
100
+ inheritedEventCount: 0,
101
+ get seq() { return events.length },
102
+ append(type, data) {
103
+ const ev = { type, data, seq: events.length, time: Date.now() }
104
+ events.push(ev)
105
+ projections._drive(s, ev)
106
+ return ev
107
+ },
108
+ deriveMessages() { return [] },
109
+ snapshotEvents(from) { return events.slice(from || 0) },
110
+ ownEvents() { return events.slice() },
111
+ _events: events,
112
+ }
113
+ return s
114
+ }
115
+
116
+ const roots = new Map()
117
+ let rootSeq = 0
118
+ function makeRoot() {
119
+ const id = 'sess-' + String.fromCharCode(65 + rootSeq++)
120
+ const session = makeMockSession(id, undefined)
121
+ const root = { id, options: { provider: 'mock', model: 'm' }, session, ctx: undefined }
122
+ roots.set(id, root)
123
+ return root
124
+ }
125
+
126
+ const ctx = {
127
+ get(name) {
128
+ if (name === 'sessionProjections') return projections
129
+ if (name === 'sandboxPolicy') return undefined
130
+ if (name === 'compaction') return undefined
131
+ if (name === 'subprocess') {
132
+ return {
133
+ async resolveExecutable(cmd) { return String(cmd) },
134
+ spawn({ argv }) {
135
+ const last = argv[argv.length - 1] || ''
136
+ // directory creation still goes through the same mock (mkdirs uses a shell)
137
+ if (/New-Item/.test(last)) {
138
+ const paths = []
139
+ const re = /'((?:[^']|'')*)'/g
140
+ let m
141
+ while ((m = re.exec(last)) !== null) paths.push(m[1].replace(/''/g, "'"))
142
+ for (const q of paths) if (q && !/^-/.test(q)) mkdirSync(q, { recursive: true })
143
+ return { done: Promise.resolve({ exitCode: 0, signal: null }), collected: {}, terminate() {} }
144
+ }
145
+ // The fake Lean toolchain: GREEN unless the file still uses sorry / carries -- FAIL.
146
+ // Case 12 needs a proof that really passes so the prompt switches to fidelity review.
147
+ const text = existsSync(last) ? readFileSync(last, 'utf8') : ''
148
+ const bad = /sorry|-- FAIL/.test(text)
149
+ const ok = { text: 'ok\n', nextOffset: 3, lossy: false }
150
+ const err = { text: bad ? 'error: declaration uses sorry\n' : '', nextOffset: 0, lossy: false }
151
+ return {
152
+ done: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null }),
153
+ collected: { stdout: { readFrom: () => ok }, stderr: { readFrom: () => err } },
154
+ terminate() {},
155
+ }
156
+ },
157
+ }
158
+ }
159
+ return undefined
160
+ },
161
+ on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
162
+ effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
163
+ logger: { info() {}, warn() {}, error() {} },
164
+ timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
165
+ tools: { register(spec) { toolRegs.push(spec); return () => {} } },
166
+ commands: { register() { return () => {} } },
167
+ sessions: { async flush() { return true } },
168
+ subagents: {
169
+ list() { return ['spawn'] },
170
+ async startContinuable({ label, request }) {
171
+ if (failNextStarts > 0) { failNextStarts -= 1; throw new Error('mock provisioning failure') }
172
+ const rootId = (request && request.parent && request.parent.id) || 'sess-A'
173
+ const id = 'c' + (spawns.length + 1)
174
+ liveAgents.set(id, { id, session: makeMockSession(id, rootId), options: request && request.agentOptions })
175
+ spawns.push({
176
+ label, childId: id, rootId,
177
+ persona: request && request.persona,
178
+ prompt: request && request.prompt && request.prompt[0] && request.prompt[0].text,
179
+ toolFilter: request && request.toolFilter,
180
+ })
181
+ return { childId: id, messageId: 'm' + spawns.length }
182
+ },
183
+ async sendMessage(parent, childId, blocks) {
184
+ wakes.push({ childId, rootId: (parent && parent.id) || 'sess-A', prompt: (blocks && blocks[0] && blocks[0].text) || '' })
185
+ return 'w' + (delivered.length + wakes.length)
186
+ },
187
+ interrupt() {},
188
+ async drainContinuableChildren(parent, ids) { for (const i of ids) liveAgents.delete(i) },
189
+ },
190
+ agents: {
191
+ roots() { return [...roots.values()] },
192
+ get(id) { return roots.get(id) || liveAgents.get(id) },
193
+ list() { return [...roots.values(), ...liveAgents.values()] },
194
+ },
195
+ fs: {
196
+ async resolve(rel, opts) {
197
+ const b = (opts && opts.cwd) || WS
198
+ const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/'))
199
+ return { targetKey: p, displayPath: p }
200
+ },
201
+ async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
202
+ async readText(t) { return readFileSync(t.targetKey, 'utf8') },
203
+ async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
204
+ async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
205
+ },
206
+ }
207
+
208
+ const mod = await import(PLUGIN.href + '?t=' + Date.now())
209
+ const plugin = mod.default || mod
210
+ plugin.apply(ctx)
211
+
212
+ // ---------------------------------------------------------------
213
+ // driving helpers
214
+ // ---------------------------------------------------------------
215
+ async function callTool(name, args, agent) {
216
+ const spec = toolRegs.find(x => x.name === name)
217
+ if (!spec) throw new Error('no tool ' + name)
218
+ return JSON.parse(await spec.execute(args || {}, { agent }))
219
+ }
220
+ const childAgent = (childId) => liveAgents.get(childId)
221
+ function fireEnd(childId, reply, stopReason) {
222
+ const blocks = reply === undefined ? [] : [{ type: 'text', text: '```json\n' + JSON.stringify(reply) + '\n```' }]
223
+ for (const h of (listeners['subagent/end'] || [])) {
224
+ h({ id: childId, runId: 'r', provider: 'spawn', local: true, stopReason: stopReason || 'completed', lastAssistantMessage: blocks })
225
+ }
226
+ }
227
+ const settle = async () => { await sleep(30) }
228
+ const memberOfChild = (childId) => {
229
+ const s = spawns.find(x => x.childId === childId)
230
+ const m = s ? /vibe5 (\S+) /.exec(s.label) : null
231
+ return m ? m[1] : ''
232
+ }
233
+ const spawnOf = (root, memberId) => spawns.find(s => s.rootId === root.id && s.label.indexOf('vibe5 ' + memberId + ' ') !== -1)
234
+ const childOf = (root, memberId) => { const s = spawnOf(root, memberId); return s ? s.childId : '' }
235
+ const spawnsFor = (root) => spawns.filter(s => s.rootId === root.id)
236
+
237
+ // Pull exactly the VOTING prompts for one root. A plain FIFO drain returns whatever was
238
+ // queued first (work rounds, heartbeats), which is how an earlier version of this case ended
239
+ // up asserting against the wrong prompt entirely.
240
+ async function takeVerifyPrompts(root, n) {
241
+ const got = []
242
+ for (let guard = 0; guard < 400 && got.length < n; guard++) {
243
+ const idx = wakes.findIndex(w => w.rootId === root.id && /【求真表决/.test(w.prompt))
244
+ if (idx === -1) {
245
+ const other = wakes.findIndex(w => w.rootId === root.id)
246
+ if (other !== -1) {
247
+ const w = wakes.splice(other, 1)[0]
248
+ delivered.push({ prompt: w.prompt, owner: memberOfChild(w.childId), rootId: w.rootId })
249
+ fireEnd(w.childId, { progress: '(语料采样时略过非表决轮)', contextPct: 20 })
250
+ await settle()
251
+ continue
252
+ }
253
+ await settle()
254
+ continue
255
+ }
256
+ const w = wakes.splice(idx, 1)[0]
257
+ got.push(w)
258
+ delivered.push({ prompt: w.prompt, owner: memberOfChild(w.childId), rootId: w.rootId })
259
+ fireEnd(w.childId, { verdict: { target: (/"target"\s*:\s*"([^"]+)"/.exec(w.prompt) || [])[1] || '', verdict: 0.5, reason: '语料采样' }, contextPct: 20 })
260
+ await settle()
261
+ }
262
+ return got
263
+ }
264
+
265
+ let votePlan = new Map() // memberId -> verdict number for the next verify prompts
266
+ let replyOverride = new Map() // memberId -> the exact reply its NEXT wake must produce
267
+ // Roots whose MEETING prompts the driver must NOT answer, so the meeting stays in flight
268
+ // (case 10b needs a live meeting to test that a verification cannot preempt it).
269
+ const hushed = new Set()
270
+ // Handle queued sends. `delivered` collects what was actually sent for the case under
271
+ // test, because the queue is consumed here and assertions must not read it afterwards.
272
+ // Wakes belonging to OTHER roots are skipped over rather than allowed to block: a case
273
+ // with a short heartbeat keeps producing its own wakes, and a naive
274
+ // "stop at the first foreign wake" loop would starve every later case.
275
+ async function drainWakes(budget, root) {
276
+ let n = 0
277
+ while (n < budget) {
278
+ const idx = wakes.findIndex(w => (!root || w.rootId === root.id)
279
+ && !(hushed.has(w.rootId) && /【研究所会议/.test(w.prompt)))
280
+ if (idx === -1) break
281
+ const w = wakes.splice(idx, 1)[0]
282
+ const owner = memberOfChild(w.childId)
283
+ delivered.push({ prompt: w.prompt, owner, childId: w.childId, rootId: w.rootId })
284
+ let reply
285
+ if (replyOverride.has(owner)) { reply = replyOverride.get(owner); replyOverride.delete(owner) } else if (/【求真表决/.test(w.prompt)) {
286
+ const target = (/"target"\s*:\s*"([^"]+)"/.exec(w.prompt) || [])[1] || ''
287
+ const v = votePlan.has(owner) ? votePlan.get(owner) : 0.5
288
+ reply = { verdict: { target, verdict: v, reason: owner + ' 的判断' }, contextPct: 20 }
289
+ } else if (/【研究所会议/.test(w.prompt)) {
290
+ reply = { input: owner + ':我的意见。', solved: false, contextPct: 20 }
291
+ } else {
292
+ reply = { progress: owner + ':继续推进。', solved: false, contextPct: 20 }
293
+ }
294
+ fireEnd(w.childId, reply)
295
+ n++
296
+ await settle()
297
+ }
298
+ return n
299
+ }
300
+ // A case is over: pause it so it can never generate a wake that would leak into the
301
+ // next case, and drop anything it still had queued.
302
+ async function endCase(root) {
303
+ await callTool('vibe_v5_pause', {}, root)
304
+ for (let i = wakes.length - 1; i >= 0; i--) if (wakes[i].rootId === root.id) wakes.splice(i, 1)
305
+ }
306
+ async function settleInstitute(root, rounds = 14) {
307
+ for (let i = 0; i < rounds; i++) {
308
+ await drainWakes(40, root)
309
+ await sleep(20)
310
+ const st = await callTool('vibe_v5_status', {}, root)
311
+ if (!st.members.some(m => m.busy) && !st.meeting && !st.verify && wakes.length === 0) return st
312
+ }
313
+ await drainWakes(40, root)
314
+ return await callTool('vibe_v5_status', {}, root)
315
+ }
316
+
317
+ // ---------------------------------------------------------------
318
+ // prompt inspection
319
+ // ---------------------------------------------------------------
320
+ const KINDS = ['院士', '常驻研究员', '临时工']
321
+ const reState = new RegExp('\\[状态\\]\\s*你是\\s+(\\S+?)((' + KINDS.join('|') + '))|轮次\\s*(\\d+)|法定票数\\s*m=(\\d+)|有表决权者\\s*(\\d+)\\s*人')
322
+ const reRoster = /\[在册\]\s*(.*)/
323
+ const reAbsent = /\[未就位\]\s*(.*)/
324
+ const reHeader = /^【([^】]*)】/m
325
+
326
+ function headerMember(prompt) {
327
+ const h = (reHeader.exec(prompt) || [])[1]
328
+ if (!h) return { header: '', id: '', kind: '' }
329
+ for (const k of KINDS) {
330
+ let m = new RegExp('——\\s*' + k + '\\s+(\\S+?)\\s*】?$').exec(h)
331
+ if (m) return { header: h, id: m[1], kind: k }
332
+ m = new RegExp('——\\s*' + k + '\\s+(\\S+?)\\s+就对象').exec(h)
333
+ if (m) return { header: h, id: m[1], kind: k }
334
+ }
335
+ return { header: h, id: '', kind: '' }
336
+ }
337
+ function parseState(prompt) {
338
+ const s = reState.exec(prompt)
339
+ if (!s) return null
340
+ return { id: s[1], kind: s[2], round: Number(s[3]), m: Number(s[4]), voters: Number(s[5]) }
341
+ }
342
+ const rosterOf = (prompt) => {
343
+ const r = reRoster.exec(prompt)
344
+ return r ? r[1].split(/[、,]/).map(s => s.trim()).filter(s => s && s !== '(无)') : null
345
+ }
346
+ const absentOf = (prompt) => {
347
+ const r = reAbsent.exec(prompt)
348
+ return r ? r[1].split(/[、,]/).map(s => s.trim()).filter(Boolean) : null
349
+ }
350
+ const stateBlockCount = (prompt) => (prompt.match(/\[状态\]/g) || []).length
351
+ // Data-position garbage only: a bare \bundefined\b also matches legitimate prose
352
+ // such as v2's "no undefined symbols" (the v2 suite caught that as a false positive).
353
+ const GARBAGE = [/:\s*undefined/, /["']undefined["']/, /undefined\s*[,}\]]/, /\bNaN\b/, /\[object Object\]/, /你是\s*\?/]
354
+ const isVoter = (id) => id === 'acad' || /^r-/.test(id)
355
+
356
+ // Applied to EVERY captured prompt.
357
+ function checkPromptSweep(prompt, owner, where) {
358
+ const problems = []
359
+ if (!prompt) return ['prompt is empty']
360
+ for (const g of GARBAGE) if (g.test(prompt)) problems.push('contains ' + g)
361
+ if (stateBlockCount(prompt) !== 1) problems.push('expected exactly one [状态] block, found ' + stateBlockCount(prompt))
362
+ const st = parseState(prompt)
363
+ if (!st) { problems.push('no parseable [状态] line'); return problems }
364
+ if (st.id !== owner) problems.push('[状态] names ' + st.id + ' but was sent to ' + owner)
365
+ const hd = headerMember(prompt)
366
+ if (hd.id && hd.id !== owner) problems.push('header names ' + hd.id + ' but was sent to ' + owner)
367
+ if (hd.kind && st.kind && hd.kind !== st.kind) problems.push('header kind ' + hd.kind + ' ≠ [状态] kind ' + st.kind)
368
+ const roster = rosterOf(prompt)
369
+ if (!roster) problems.push('[在册] line missing')
370
+ else {
371
+ if (roster.indexOf(owner) === -1) problems.push('the roster omits the reader ' + owner + ' ([' + roster.join('、') + '])')
372
+ if (new Set(roster).size !== roster.length) problems.push('duplicate ids in [在册]')
373
+ for (const a of (absentOf(prompt) || [])) {
374
+ const id = String(a).replace(/(.*$/, '')
375
+ if (roster.indexOf(id) !== -1) problems.push(id + ' is on the roster AND listed as 未就位')
376
+ }
377
+ if (st.voters !== roster.filter(isVoter).length) {
378
+ problems.push('有表决权者 ' + st.voters + ' ≠ voters in [在册] ' + roster.filter(isVoter).length)
379
+ }
380
+ if (st.m !== Math.min(3, st.voters)) problems.push('m=' + st.m + ' ≠ min(quorumCap 3, voters ' + st.voters + ')')
381
+ }
382
+ for (const other of KINDS) {
383
+ const hits = prompt.match(new RegExp('你是\\s+\\S+?(' + other + ')', 'g')) || []
384
+ if (hits.length > 1) problems.push('more than one identity claim: ' + hits.join(' / '))
385
+ }
386
+ if (problems.length) console.error(' !! ' + where + ' → ' + problems.join('; '))
387
+ return problems
388
+ }
389
+
390
+ // ---------------------------------------------------------------
391
+ // corpus recorder
392
+ // ---------------------------------------------------------------
393
+ const corpus = []
394
+ // Normalise the workspace OUT of the corpus. A plain `split(WS)` is not enough on Windows:
395
+ // the plugin renders paths with forward slashes while os.tmpdir() may hand back a different
396
+ // CASE ("...\ADMIN\..." vs ".../admin/..."), so the absolute workspace path used to survive
397
+ // into the SHIPPED corpus — non-deterministic (the temp dir changes every run) and a machine
398
+ // path leak. Match case-insensitively, on either separator.
399
+ const scrub = (s) => {
400
+ const t = String(s == null ? '' : s).replace(/\\/g, '/')
401
+ const ws = WS.replace(/\\/g, '/')
402
+ const re = new RegExp(ws.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
403
+ // Timestamps are part of the prompt a member reads, but not part of what a reviewer needs:
404
+ // normalise them too. Otherwise the shipped corpus changes on EVERY run — its headings carry
405
+ // `### YYYY-MM-DD hh:mm:ss|<member>` — and its diffs stop being meaningful.
406
+ return t.replace(re, '<WS>').replace(/\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2})?/g, '<TIME>')
407
+ }
408
+ // Every Lean tool mention in agent-facing text must be the REGISTERED name.
409
+ const noBareLeanTool = (t) => !/(^|[^a-z_])lean_(run|archive|lib)/.test(String(t || ''))
410
+ function record(kind, owner, prompt, persona, extra) {
411
+ corpus.push({
412
+ kind, owner,
413
+ sentToLabel: (extra && extra.label) || '',
414
+ persona: persona === undefined ? null : scrub(persona),
415
+ prompt: scrub(prompt),
416
+ toolFilter: (extra && extra.toolFilter) || null,
417
+ })
418
+ }
419
+ function recordAndCheck(kind, owner, prompt, opts) {
420
+ record(kind, owner, prompt, opts && opts.persona, opts)
421
+ const problems = checkPromptSweep(prompt, owner, kind + ' prompt for ' + owner)
422
+ assert(problems.length === 0, kind + ' prompt for ' + owner + ' is identity/roster coherent')
423
+ return problems
424
+ }
425
+ function checkPersona(kind, owner, persona, checks) {
426
+ for (const [re, label] of checks) assert(re.test(persona), kind + ': ' + owner + "'s charter " + label)
427
+ }
428
+
429
+ // ===============================================================
430
+ console.log('-- V5 prompt & interaction integrity --')
431
+
432
+ // =============== CASE 1: founding briefs =========================================
433
+ section('1 founding — every induction brief describes the member that receives it')
434
+ const RA = makeRoot()
435
+ const started = await callTool('vibe_v5_start', { problem: '求 3N^2-2=b^2 与 3N^2+2=5a^2 的全部整数解', researcherCount: 3 }, RA)
436
+ assert(started.ok === true, 'institute founded')
437
+ const FOUND_ORDER = ['acad', 'r-1', 'r-2', 'r-3']
438
+ const founding = spawnsFor(RA)
439
+ assert(founding.length === 4, 'four founding members were started (got ' + founding.length + ')')
440
+ for (let i = 0; i < founding.length; i++) {
441
+ const sp = founding[i]
442
+ const owner = memberOfChild(sp.childId)
443
+ assert(owner === FOUND_ORDER[i], 'founding #' + i + ' started ' + owner + ' (expected ' + FOUND_ORDER[i] + ')')
444
+ recordAndCheck('founding', owner, sp.prompt, sp)
445
+ const st = parseState(sp.prompt) || {}
446
+ const roster = rosterOf(sp.prompt) || []
447
+ assert(JSON.stringify(roster) === JSON.stringify(FOUND_ORDER.slice(0, i + 1)),
448
+ 'the founding brief of ' + owner + ' shows the roster INCLUDING itself: ' + JSON.stringify(roster))
449
+ assert(st.round === 1, owner + "'s founding brief is round 1 (got " + st.round + ')')
450
+ assert(st.voters === i + 1, owner + ' sees ' + (i + 1) + ' voter(s) (got ' + st.voters + ')')
451
+ assert(st.m === Math.min(3, i + 1), owner + ' sees m=min(3,' + (i + 1) + ')=' + Math.min(3, i + 1) + ' (got ' + st.m + ')')
452
+ assert(sp.prompt.indexOf('【入职首轮') === 0, owner + "'s first prompt is framed as an induction")
453
+ assert(sp.prompt.indexOf('你刚刚加入本所') !== -1, owner + "'s induction asks for its own first view")
454
+ }
455
+ assert(founding[0].prompt.indexOf('[状态] 你是 acad(院士)') !== -1, 'the academician brief says 你是 acad(院士) — never "?"')
456
+ assert(!/你是 \?/.test(founding[0].prompt), 'no "你是 ?" placeholder')
457
+ checkPersona('founding', 'acad', founding[0].persona, [
458
+ [/在册院士:acad/, 'lists itself as the sitting academician'],
459
+ [/在册常驻研究员:(无)/, 'shows no researchers at that instant'],
460
+ [/你是「institute」的\*\*院士\*\*/, 'opens by naming its office'],
461
+ [/Members\/acad\//, 'points at its own library'],
462
+ ])
463
+ checkPersona('founding', 'r-3', founding[3].persona, [
464
+ [/在册院士:acad/, 'names the sitting academician'],
465
+ [/在册常驻研究员:r-1、r-2、r-3/, 'lists r-1、r-2、r-3 as the sitting researchers'],
466
+ [/在册临时工:(无)/, 'shows no temps'],
467
+ [/代号 r-3。/, 'states its own 代号'],
468
+ [/Members\/r-3\//, 'points at its own library'],
469
+ [/progress.md/, 'documents Progress/progress.md'],
470
+ ])
471
+ checkPersona('founding', 'r-1', founding[1].persona, [[/一名常驻研究员/, 'opens as 常驻研究员']])
472
+ for (const sp of founding) {
473
+ const owner = memberOfChild(sp.childId)
474
+ assert(sp.persona.indexOf('Members/' + owner + '/') !== -1, owner + "'s charter points at Members/" + owner + '/')
475
+ }
476
+ for (const sp of founding) { sp._ended = true; fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
477
+ await settleInstitute(RA)
478
+ await endCase(RA)
479
+
480
+ // =============== CASE 2: round prompts (normal + checkpoint) =====================
481
+ section('2 rounds — normal and checkpoint prompts keep the identity straight')
482
+ const RB = makeRoot()
483
+ await callTool('vibe_v5_start', { problem: '无领头人情形下的组织', researcherCount: 2 }, RB)
484
+ for (const sp of spawnsFor(RB)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
485
+ await settleInstitute(RB)
486
+ // A short idle window makes the heartbeat prompt reachable inside a test run.
487
+ await callTool('vibe_v5_set', { activityTimeoutMs: 80, maxParallel: 6, chatDigestMax: 1 }, RB)
488
+ // (a) an addressed message must produce a NORMAL round prompt carrying the framed inbox
489
+ delivered.length = 0
490
+ await callTool('vibe_v5_say', { to: 'r-2', text: '请把你手上的结论同步给我。' }, childAgent(childOf(RB, 'r-1')))
491
+ await settle(); await drainWakes(10, RB)
492
+ const normalWakes = delivered.slice()
493
+ assert(normalWakes.length > 0, 'the addressed message produced a wake (' + normalWakes.length + ')')
494
+ for (const w of normalWakes) recordAndCheck('normal', w.owner, w.prompt)
495
+ assert(normalWakes.some(w => w.owner === 'r-2' && w.prompt.indexOf('【研究所·私信 from r-1】') !== -1),
496
+ 'r-2 is woken with its inbox containing the DM framed from r-1')
497
+ assert(normalWakes.filter(w => w.owner === 'r-2').every(w => w.prompt.indexOf('【研究所·私信 from r-2】') === -1),
498
+ 'r-2 never receives the DM framed as coming from itself')
499
+ // (b) the heartbeat must produce a CHECKPOINT prompt
500
+ delivered.length = 0
501
+ await sleep(260); await settle(); await drainWakes(10, RB)
502
+ let checkpointWakes = delivered.filter(w => /【心跳检查/.test(w.prompt))
503
+ if (!checkpointWakes.length) { await sleep(260); await settle(); await drainWakes(10, RB); checkpointWakes = delivered.filter(w => /【心跳检查/.test(w.prompt)) }
504
+ assert(checkpointWakes.length > 0, 'the heartbeat produced a checkpoint prompt (' + checkpointWakes.length + ')')
505
+ for (const w of checkpointWakes) {
506
+ recordAndCheck('checkpoint', w.owner, w.prompt)
507
+ // The heartbeat body may be preceded by a delivered inbox or the core-rules recap
508
+ // after a real compaction, so match anywhere rather than at offset 0.
509
+ assert(w.prompt.indexOf('【心跳检查 —— ') !== -1, w.owner + "'s heartbeat prompt names its own office and id")
510
+ assert(new RegExp('【心跳检查 —— (院士|常驻研究员|临时工) ' + w.owner + '】').test(w.prompt),
511
+ w.owner + "'s heartbeat header carries its own kind and id")
512
+ }
513
+ await endCase(RB)
514
+
515
+ // =============== CASE 3: interaction framing ====================================
516
+ section('3 interaction framing — every message names its true sender and kind')
517
+ const RC = makeRoot()
518
+ await callTool('vibe_v5_start', { problem: '交互框架测试', researcherCount: 2 }, RC)
519
+ for (const sp of spawnsFor(RC)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
520
+ await settleInstitute(RC)
521
+ await callTool('vibe_v5_set', { maxParallel: 8, chatDigestMax: 1 }, RC)
522
+ const r1 = childOf(RC, 'r-1'), acad = childOf(RC, 'acad')
523
+ const send = async (kind, fn) => {
524
+ delivered.length = 0
525
+ const r = await fn()
526
+ await settle(); await drainWakes(20, RC)
527
+ const handled = delivered.slice()
528
+ for (const w of handled) recordAndCheck(kind, w.owner, w.prompt).length
529
+ return { r, prompts: handled.map(w => w.prompt).join('\n'), owners: handled.map(w => w.owner), count: handled.length }
530
+ }
531
+ const dm = await send('inbox-dm', () => callTool('vibe_v5_say', { to: 'r-2', text: '私下问你一下。' }, childAgent(r1)))
532
+ const voters = await send('inbox-voters', () => callTool('vibe_v5_say', { to: 'voters', text: '请全体表决者注意。' }, childAgent(r1)))
533
+ const chat = await send('inbox-chat', () => callTool('vibe_v5_say', { text: '各位,我建议先做最小反例归约。' }, childAgent(r1)))
534
+ const office = await send('inbox-office', () => callTool('vibe_v5_message', { to: 'all', content: '所办通知:请按计划推进。' }, RC))
535
+ const assign = await send('inbox-assign', () => callTool('vibe_v5_assign', { subject: '核验模 9 情形', to: 'r-2', why: '你最熟同余', acceptance: '给出模 9 全表' }, childAgent(acad)))
536
+ const nudge = await send('inbox-nudge', () => callTool('vibe_v5_nudge', { to: 'r-2', why: '进度偏慢', next_step: '先交一份模 9 表' }, childAgent(acad)))
537
+ assert(dm.r.ok === true && dm.r.delivered === 1, 'a DM reaches exactly its addressee')
538
+ assert(voters.r.ok === true && voters.r.delivered === 2, 'a voters-only broadcast reaches every voter but the sender')
539
+ assert(chat.r.ok === true && chat.r.delivered === 2, 'group chat fans out to every other member')
540
+ assert(assign.r.ok === true, "the ACADEMICIAN's assignment succeeded (" + JSON.stringify(assign.r).slice(0, 90) + ')')
541
+ assert(/【研究所·私信 from r-1】/.test(dm.prompts), 'a DM is framed by its true sender (【研究所·私信 from r-1】)')
542
+ assert(/【研究所·致全体表决者 from r-1】/.test(voters.prompts), 'a voters-only broadcast is framed as such, not as a DM')
543
+ assert(/【研究所·群聊】r-1:/.test(chat.prompts), 'group chat is framed with the true speaker')
544
+ assert(/【所办通知】/.test(office.prompts), 'an office notice is framed 所办通知')
545
+ assert(/【院士分派】/.test(assign.prompts), "the academician's assignment is framed 院士分派")
546
+ assert(/【督办 from acad】/.test(nudge.prompts), 'a nudge is framed 督办 by its true author')
547
+ assert(!/【院士分派】[^\n]*督办/.test(nudge.prompts), 'a nudge is NOT mislabelled as an assignment')
548
+ // THE OFFICE ITSELF must be able to assign, and must not impersonate the academician.
549
+ const officeAssign = await send('inbox-office-assign', () => callTool('vibe_v5_assign', { subject: '所办指派', to: 'r-2', why: '所办决定', acceptance: '给出结论' }, RC))
550
+ assert(officeAssign.r.ok === true, 'the OFFICE (session root) can assign — it is resolved as the office, not as a random member (' + JSON.stringify(officeAssign.r).slice(0, 90) + ')')
551
+ assert(/【所办分派】/.test(officeAssign.prompts), 'an OFFICE assignment is framed 所办分派, not 院士分派')
552
+ assert(!/【院士分派】/.test(officeAssign.prompts), 'an office assignment does not impersonate the academician')
553
+ const officeNudge = await send('inbox-office-nudge', () => callTool('vibe_v5_nudge', { to: 'r-2', why: '所办督办一下' }, RC))
554
+ assert(officeNudge.r.ok === true, 'the OFFICE can nudge')
555
+ assert(/【督办 from office】/.test(officeNudge.prompts) && /所办督办/.test(officeNudge.prompts),
556
+ 'an office nudge is labelled 所办督办 by the office, not 院士督办')
557
+ assert(await callTool('vibe_v5_prioritize', { order: [], why: 'x' }, RC).then(r => r.ok === false), 'the office still hits argument validation (resolved AS the office)')
558
+ await endCase(RC)
559
+
560
+ // =============== CASE 4: framework feedback delivery ============================
561
+ section('4 framework feedback reaches the member (never dropped as a self-message)')
562
+ const RD = makeRoot()
563
+ await callTool('vibe_v5_start', { problem: '框架反馈投递测试', researcherCount: 2 }, RD)
564
+ for (const sp of spawnsFor(RD)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
565
+ await settleInstitute(RD)
566
+ await callTool('vibe_v5_set', { maxParallel: 8 }, RD)
567
+ const rd1 = childOf(RD, 'r-1'), rd2 = childOf(RD, 'r-2')
568
+ // A member can only be answered while a turn of its own is in flight, so each case
569
+ // below first WAKES r-1 and then lets its reply be the malformed one.
570
+ const wakeR1With = async (kind, reply) => {
571
+ delivered.length = 0
572
+ replyOverride.set('r-1', reply)
573
+ await callTool('vibe_v5_say', { to: 'r-1', text: '请就当前状态给个结论。' }, childAgent(rd2))
574
+ await settle(); await drainWakes(20, RD)
575
+ const handled = delivered.slice()
576
+ for (const w of handled) if (/【框架提示】/.test(w.prompt)) recordAndCheck(kind, w.owner, w.prompt).length
577
+ return handled.map(w => w.prompt).join('\n')
578
+ }
579
+ const badVerdictText = await wakeR1With('notice', { verdict: { target: 'p-ghost', verdict: 'not-a-number', reason: 'x' }, contextPct: 20 })
580
+ assert(/【框架提示】/.test(badVerdictText), 'a malformed verdict produces a real 【框架提示】 delivery')
581
+ assert(/verdict 必须是 0-1 的数值/.test(badVerdictText), 'the notice says what was wrong')
582
+ assert(/【框架提示】[^\n]*verdict/.test(badVerdictText), 'the notice is framed by the framework, not by the member itself')
583
+ const badClaimText = await wakeR1With('notice-claim', { task_claim: 't-999', contextPct: 20 })
584
+ assert(/【框架提示】/.test(badClaimText) && /没有任务 t-999/.test(badClaimText), 'a refused claim is reported back to the claimer')
585
+ // A CAS refusal reported through the JSON reply must also come back.
586
+ const t = await callTool('vibe_v5_task_create', { subject: '一个没人认领的任务' }, childAgent(rd1))
587
+ const staleText = await wakeR1With('notice-task', { task_update: { task_id: t.task.id, expected_revision: 99, action: 'claim' }, contextPct: 20 })
588
+ assert(/【框架提示】/.test(staleText) && /V5_TASK_STALE_REVISION/.test(staleText), 'a stale CAS reported through the reply channel is echoed back')
589
+ const selfFramed = corpus.filter(c => new RegExp('【研究所·私信 from ' + c.owner + '】').test(c.prompt))
590
+ assert(selfFramed.length === 0, 'no member ever receives a message framed as coming from itself')
591
+ await endCase(RD)
592
+
593
+ // =============== CASE 5: temp workers ===========================================
594
+ section('5 a hired temp worker is told its own name, employer and purpose')
595
+ const RE = makeRoot()
596
+ await callTool('vibe_v5_start', { problem: '临时工入职测试', researcherCount: 2 }, RE)
597
+ for (const sp of spawnsFor(RE)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
598
+ await settleInstitute(RE)
599
+ const hired = await callTool('vibe_v5_hire', { purpose: '核对文献引理', initial_task: '核对第 3 节引理' }, childAgent(childOf(RE, 'r-1')))
600
+ assert(hired.ok === true, 'r-1 hired a temp worker (' + JSON.stringify(hired).slice(0, 80) + ')')
601
+ const tempSpawn = spawnOf(RE, hired.id)
602
+ recordAndCheck('founding-temp', hired.id, tempSpawn.prompt, tempSpawn)
603
+ {
604
+ const st = parseState(tempSpawn.prompt) || {}
605
+ assert(st.kind === '临时工', 'the temp brief calls it 临时工 (got ' + st.kind + ')')
606
+ assert((rosterOf(tempSpawn.prompt) || []).indexOf(hired.id) !== -1, 'the temp brief lists the temp itself on the roster')
607
+ checkPersona('founding-temp', hired.id, tempSpawn.persona, [
608
+ [new RegExp('代号 ' + hired.id + ',由 r-1 雇入'), 'names itself and its true employer'],
609
+ [/用途:核对文献引理/, 'states its purpose'],
610
+ [new RegExp('Members/' + hired.id + '/'), 'points at its own library'],
611
+ [/你的雇主:r-1/, 'names its employer'],
612
+ ])
613
+ assert(/【入职首轮 —— 临时工 t-\d+】/.test(tempSpawn.prompt), 'the temp brief is framed as its induction')
614
+ assert(/你的初始任务\/用途:/.test(tempSpawn.prompt) && /核对第 3 节引理/.test(tempSpawn.prompt), 'the temp brief carries its initial task')
615
+ assert(!/"verdict":/.test(tempSpawn.prompt) || /"verdict" 字段对你不适用/.test(tempSpawn.prompt), 'the temp brief states it has no vote')
616
+ assert(!/"hire":/.test(tempSpawn.prompt), 'the temp brief does not offer hire')
617
+ }
618
+ for (const sp of spawnsFor(RE)) { if (!sp._ended) { sp._ended = true; fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':收到。', solved: false, contextPct: 10 }); await settle() } }
619
+ await settleInstitute(RE)
620
+
621
+ // =============== CASE 6: failed provisioning is visible =========================
622
+ section('6 a member that failed to provision is visible, not a phantom')
623
+ failNextStarts = 1
624
+ const failedHire = await callTool('vibe_v5_hire', { purpose: '注定失败', initial_task: 'x' }, childAgent(childOf(RE, 'r-1')))
625
+ assert(failedHire.ok === false, 'a provisioning failure is reported to the hirer (' + JSON.stringify(failedHire).slice(0, 100) + ')')
626
+ const stFail = await callTool('vibe_v5_status', {}, RE)
627
+ const failedMember = stFail.members.find(m => m.phase === 'failed')
628
+ assert(!!failedMember, 'the member is recorded as failed rather than left active')
629
+ assert(!failedMember || failedMember.busy !== true, 'a failed member is not left marked busy')
630
+ delivered.length = 0
631
+ await callTool('vibe_v5_say', { to: 'r-2', text: '看下编制。' }, childAgent(childOf(RE, 'r-1')))
632
+ await settle(); await drainWakes(20, RE)
633
+ const failPrompts = delivered.slice()
634
+ for (const w of failPrompts) recordAndCheck('after-failure', w.owner, w.prompt).length
635
+ const failText = failPrompts.map(w => w.prompt).join('\n')
636
+ assert(failPrompts.length > 0, 'a member was woken after the failure (' + failPrompts.length + ')')
637
+ assert(new RegExp('\\[未就位\\][^\\n]*' + failedMember.id).test(failText), 'the failed member appears in [未就位] with its id')
638
+ assert(!new RegExp('\\[在册\\][^\\n]*' + failedMember.id).test(failText), 'the failed member is NOT listed as if it were on the roster')
639
+ await endCase(RE)
640
+
641
+ // =============== CASE 7: session rebuild ========================================
642
+ section('7 a rebuilt session is told it was rebuilt, not that it just joined')
643
+ const RF = makeRoot()
644
+ await callTool('vibe_v5_start', { problem: '会话重建测试', researcherCount: 2 }, RF)
645
+ for (const sp of spawnsFor(RF)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
646
+ await settleInstitute(RF)
647
+ const personasBefore = {}
648
+ for (const sp of spawnsFor(RF)) personasBefore[memberOfChild(sp.childId)] = sp.persona
649
+ await callTool('vibe_v5_stop', {}, RF)
650
+ await settle()
651
+ const spawnCountBefore = spawns.length
652
+ const resumed = await callTool('vibe_v5_resume', {}, RF)
653
+ assert(resumed.ok === true, 'the institute resumed (' + JSON.stringify(resumed).slice(0, 120) + ')')
654
+ const resumeSpawns = spawns.slice(spawnCountBefore).filter(s => s.rootId === RF.id)
655
+ assert(resumeSpawns.length > 0, 'resume rebuilt at least one member session (' + resumeSpawns.length + ')')
656
+ for (const sp of resumeSpawns) {
657
+ const owner = memberOfChild(sp.childId)
658
+ recordAndCheck('resume', owner, sp.prompt, sp)
659
+ assert(sp.prompt.indexOf('【会话重建 —— ') === 0, owner + "'s rebuilt session is framed 会话重建, not 入职首轮")
660
+ assert(sp.prompt.indexOf('你刚刚加入本所') === -1, owner + ' is NOT told "你刚刚加入本所" on resume')
661
+ assert(sp.prompt.indexOf('不要从头再来') !== -1, owner + ' is told to read back its progress instead of restarting')
662
+ assert(sp.persona === personasBefore[owner], owner + "'s charter is the FROZEN hire-time one, not a resumed-time rewrite")
663
+ }
664
+ await endCase(RF)
665
+
666
+ // =============== CASE 8: leaderless institute ===================================
667
+ section('8 with academician:false no charter invents a leader')
668
+ const RG = makeRoot()
669
+ const l2 = await callTool('vibe_v5_start', { problem: '无院士建所', researcherCount: 2, academician: false }, RG)
670
+ assert(l2.ok === true, 'a leaderless institute can be founded (' + JSON.stringify(l2).slice(0, 100) + ')')
671
+ const l2spawns = spawnsFor(RG)
672
+ assert(l2spawns.length === 2, 'two researchers were founded and no academician (got ' + l2spawns.length + ')')
673
+ for (let i = 0; i < l2spawns.length; i++) {
674
+ const sp = l2spawns[i]
675
+ const owner = memberOfChild(sp.childId)
676
+ recordAndCheck('founding-leaderless', owner, sp.prompt, sp)
677
+ const st = parseState(sp.prompt) || {}
678
+ assert(st.m === Math.min(3, i + 1), owner + ': m is computed over the leaderless roster INCLUDING itself (m=' + st.m + ')')
679
+ assert(st.kind === '常驻研究员', owner + ' is a 常驻研究员 (got ' + st.kind + ')')
680
+ assert(/在册院士:(无)/.test(sp.persona), owner + "'s charter records that there is no academician")
681
+ assert(/本所当前\*\*没有在册院士\*\*/.test(sp.persona), owner + "'s charter says so in the organization section")
682
+ assert(!/本所的领头人是\*\*院士/.test(sp.persona), owner + "'s charter does NOT claim a leader exists")
683
+ assert(!/院士 acad/.test(sp.persona), owner + "'s charter never names a non-existent 院士 acad")
684
+ assert(!/主动向院士汇报/.test(sp.persona), owner + "'s charter does not tell it to report to a non-existent academician")
685
+ assert(!/院士也可以给你派活/.test(sp.persona), owner + "'s charter does not promise assignments from a non-existent academician")
686
+ assert(!/院士可以直接分派任务/.test(sp.persona), owner + "'s charter does not promise academician assignment powers")
687
+ }
688
+ const l2status = await callTool('vibe_v5_status', {}, RG)
689
+ assert(l2status.quorum.voters.indexOf('acad') === -1, 'the leaderless institute has no academician among its voters')
690
+ await endCase(RG)
691
+
692
+ // =============== CASE 8b: the JSON contract offered matches what is honoured =====
693
+ section('8b the reply spec documents exactly the fields the framework honours')
694
+ {
695
+ const specKinds = corpus.filter(c => ['founding', 'founding-temp', 'founding-leaderless', 'normal', 'checkpoint'].indexOf(c.kind) !== -1)
696
+ assert(specKinds.length >= 6, 'the corpus has round prompts to check the reply spec on (' + specKinds.length + ')')
697
+ for (const c of specKinds) {
698
+ const isTemp = /^t-/.test(c.owner)
699
+ const isAcad = c.owner === 'acad'
700
+ if (isTemp) {
701
+ assert(c.prompt.indexOf('"verdict" 字段对你不适用') !== -1, c.owner + ' (temp) is told it has no vote')
702
+ assert(!/"hire":/.test(c.prompt) && !/"fire":/.test(c.prompt), c.owner + ' (temp) is not offered hire/fire')
703
+ } else {
704
+ assert(/"verdict":/.test(c.prompt), c.owner + ' is offered the verdict field')
705
+ assert(/"hire":/.test(c.prompt) && /"fire":/.test(c.prompt), c.owner + ' is offered hire/fire')
706
+ }
707
+ if (isAcad) {
708
+ assert(/"assign":/.test(c.prompt) && /"prioritize":/.test(c.prompt) && /"nudge":/.test(c.prompt) && /"convene_meeting":/.test(c.prompt),
709
+ c.owner + ' (academician) is offered its organizational fields')
710
+ } else {
711
+ assert(!/"assign":/.test(c.prompt) && !/"prioritize":/.test(c.prompt), c.owner + ' is not offered academician-only fields')
712
+ }
713
+ // A field the framework HONOURS but never documents is an unreachable channel: the
714
+ // member cannot object to an assignment, close a task, or fill a meeting input.
715
+ assert(/"reject_assign":/.test(c.prompt), c.owner + ' is told about reject_assign (the objection channel is reachable)')
716
+ assert(/"task_done":/.test(c.prompt), c.owner + ' is told about task_done')
717
+ assert(/"input":/.test(c.prompt), c.owner + ' is told about the meeting "input" field')
718
+ }
719
+ }
720
+
721
+ // =============== CASE 9: verification prompts ===================================
722
+ section('9 verification — voters are asked by name about the right object')
723
+ const RH = makeRoot()
724
+ await callTool('vibe_v5_start', { problem: '表决提示词测试', researcherCount: 2 }, RH)
725
+ for (const sp of spawnsFor(RH)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
726
+ await settleInstitute(RH)
727
+ await callTool('vibe_v5_set', { maxParallel: 8 }, RH)
728
+ await callTool('vibe_v5_record_proposition', { id: 'p-lemma-a', title: '引理甲', statement: '若 n>2 则不存在整数解。', value: 0.6, motive: '用于归约', p: 0.8 }, childAgent(childOf(RH, 'r-1')))
729
+ delivered.length = 0
730
+ const proposed = await callTool('vibe_v5_propose_verify', { target: 'p-lemma-a', kind: 'proposition', reason: '已有证明' }, childAgent(childOf(RH, 'r-1')))
731
+ assert(proposed.ok === true, 'the object was proposed for verification')
732
+ await settle(); await drainWakes(3, RH)
733
+ const verifyPrompts = delivered.filter(w => /【求真表决/.test(w.prompt))
734
+ assert(verifyPrompts.length === 3, 'exactly the three voters were asked, and no temp/non-voter (' + verifyPrompts.length + ')')
735
+ for (const w of verifyPrompts) {
736
+ recordAndCheck('verify', w.owner, w.prompt)
737
+ assert(w.prompt.indexOf('【求真表决 —— ') === 0, w.owner + "'s voting prompt is framed as a vote")
738
+ assert(w.prompt.indexOf(' ' + w.owner + ' 就对象 p-lemma-a 投票】') !== -1, w.owner + "'s voting prompt names itself and the object")
739
+ assert(w.prompt.indexOf('引理甲') !== -1 || w.prompt.indexOf('若 n>2 则不存在整数解') !== -1, w.owner + "'s voting prompt shows the object statement")
740
+ assert(/"target"\s*:\s*"p-lemma-a"/.test(w.prompt), w.owner + "'s voting prompt ends with the exact JSON the plugin parses")
741
+ assert(w.prompt.indexOf('verdict = 1') !== -1 && w.prompt.indexOf('verdict = 0') !== -1, w.owner + ' is told the boolean rule')
742
+ }
743
+ // Now actually reach a debate round, to exercise the DEBATE-stage prompt.
744
+ await callTool('vibe_v5_set', { verdictMaxRounds: 3 }, RH)
745
+ votePlan = new Map([['acad', 0.5], ['r-1', 1], ['r-2', 0.5]])
746
+ delivered.length = 0
747
+ await drainWakes(3, RH) // round 1: not enough boolean votes -> debate
748
+ const stillOpen = await callTool('vibe_v5_status', {}, RH)
749
+ assert(!!stillOpen.verify, 'the verification is still open after abstentions')
750
+ delivered.length = 0
751
+ await drainWakes(3, RH) // round 2 (debate) is asked
752
+ const debatePrompts = delivered.filter(w => /【求真表决/.test(w.prompt))
753
+ assert(debatePrompts.length === 3, 'the debate round re-asks every voter (' + debatePrompts.length + ')')
754
+ for (const w of debatePrompts) {
755
+ recordAndCheck('verify-debate', w.owner, w.prompt)
756
+ assert(/### 上一轮各成员的意见/.test(w.prompt), w.owner + "'s debate prompt publishes the previous round's opinions")
757
+ assert(/verdict=1/.test(w.prompt) && /verdict=0\.5/.test(w.prompt), w.owner + "'s debate prompt shows the real per-member verdicts")
758
+ assert(w.prompt.indexOf('- ' + w.owner + ':') !== -1, w.owner + "'s debate prompt shows its OWN previous vote so it can revise it")
759
+ const hist = /### 上一轮各成员的意见[\s\S]*?(?:\n\n|$)/.exec(w.prompt)
760
+ const histIds = hist ? (hist[0].match(/^- (\S+?):/gm) || []).map(s => s.slice(2, -1)) : []
761
+ assert(histIds.slice().sort().join(',') === 'acad,r-1,r-2', w.owner + "'s debate prompt publishes exactly the voters' opinions (got " + histIds.join('、') + ')')
762
+ }
763
+ await endCase(RH)
764
+
765
+ // =============== CASE 9b: only ONE verification at a time =========================
766
+ section('9b a second proposal QUEUES; it never starts a concurrent verification')
767
+ const RL = makeRoot()
768
+ await callTool('vibe_v5_start', { problem: '并发表决测试', researcherCount: 1 }, RL)
769
+ for (const sp of spawnsFor(RL)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
770
+ await settleInstitute(RL)
771
+ await callTool('vibe_v5_set', { maxParallel: 8 }, RL)
772
+ const rl1 = childOf(RL, 'r-1')
773
+ await callTool('vibe_v5_record_proposition', { id: 'p-first', statement: '第一个对象', value: 0.6, motive: 'm', p: 0.7 }, childAgent(rl1))
774
+ await callTool('vibe_v5_record_proposition', { id: 'p-second', statement: '第二个对象', value: 0.6, motive: 'm', p: 0.7 }, childAgent(rl1))
775
+ await callTool('vibe_v5_propose_verify', { target: 'p-first', kind: 'proposition', reason: '先做这个' }, childAgent(rl1))
776
+ await settle()
777
+ const stq0 = await callTool('vibe_v5_status', {}, RL)
778
+ assert(!!stq0.verify && stq0.verify.target === 'p-first', 'the first object is under verification')
779
+ // Proposing a second object while one is in flight must QUEUE it. The whole point of the
780
+ // one-at-a-time rule is that consensus is never split across two live ballots; a
781
+ // regression here would silently start a second ballot and drop the object from the queue.
782
+ await callTool('vibe_v5_propose_verify', { target: 'p-second', kind: 'proposition', reason: '排后面' }, childAgent(rl1))
783
+ await settle()
784
+ const stq1 = await callTool('vibe_v5_status', {}, RL)
785
+ assert(!!stq1.verify && stq1.verify.target === 'p-first', 'the in-flight ballot is still the first object')
786
+ assert((stq1.verifyQueue || []).indexOf('p-second') !== -1,
787
+ 'the second proposal is still QUEUED, not begun concurrently (queue=' + JSON.stringify(stq1.verifyQueue) + ')')
788
+ assert(stq1.undecided.length === 0 && stq1.verified.length === 0, 'nothing was settled by merely proposing')
789
+ // Once the first ballot settles, the queued one starts on its own.
790
+ await callTool('vibe_v5_set', { verdictMaxRounds: 1 }, RL)
791
+ delivered.length = 0
792
+ await drainWakes(20, RL)
793
+ const stq2 = await callTool('vibe_v5_status', {}, RL)
794
+ assert(stq2.verify === null || stq2.verify.target === 'p-second',
795
+ 'the queued object took over after the first ballot closed (now: ' + JSON.stringify(stq2.verify && stq2.verify.target) + ')')
796
+ await endCase(RL)
797
+
798
+ // =============== CASE 9c: a solve vote OUTSIDE a meeting ==========================
799
+ section('9c a unanimous solve vote landing outside a meeting still stops the institute')
800
+ const RM = makeRoot()
801
+ await callTool('vibe_v5_start', { problem: '会外表决停工测试', researcherCount: 1 }, RM)
802
+ for (const sp of spawnsFor(RM)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
803
+ await settleInstitute(RM)
804
+ await callTool('vibe_v5_set', { maxParallel: 8 }, RM)
805
+ const solvedReply = { vote_solved: true, solved: true, progress: '我认为原问题已解决。', contextPct: 20 }
806
+ replyOverride.set('acad', solvedReply)
807
+ replyOverride.set('r-1', solvedReply)
808
+ await callTool('vibe_v5_say', { to: 'acad', text: '请你就"是否已解决"表态。' }, childAgent(childOf(RM, 'r-1')))
809
+ await settle(); await drainWakes(4, RM)
810
+ await callTool('vibe_v5_say', { to: 'r-1', text: '请你就"是否已解决"表态。' }, childAgent(childOf(RM, 'acad')))
811
+ await settle(); await drainWakes(4, RM)
812
+ const stSolved = await callTool('vibe_v5_status', {}, RM)
813
+ assert(stSolved.solveVotes.length >= 2, 'both voters recorded a solve vote outside any meeting (' + JSON.stringify(stSolved.solveVotes) + ')')
814
+ assert(stSolved.autoDone === true,
815
+ 'the institute STOPPED on a unanimous solve vote that arrived outside a meeting ' + JSON.stringify({ autoDone: stSolved.autoDone, solveVotes: stSolved.solveVotes }))
816
+ await endCase(RM)
817
+
818
+ // =============== CASE 10: meeting prompts =======================================
819
+ section('10 meeting — real speakers, real transcript keys')
820
+ const RI = makeRoot()
821
+ await callTool('vibe_v5_start', { problem: '会议提示词测试', researcherCount: 1 }, RI)
822
+ for (const sp of spawnsFor(RI)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
823
+ await settleInstitute(RI)
824
+ await callTool('vibe_v5_set', { maxParallel: 8 }, RI)
825
+ delivered.length = 0
826
+ const mtg = await callTool('vibe_v5_meeting', { agenda: '分工与下一步', kind: 'sync' }, childAgent(childOf(RI, 'acad')))
827
+ assert(mtg.ok === true, 'the academician convened a meeting (' + JSON.stringify(mtg).slice(0, 90) + ')')
828
+ await settle(); await drainWakes(20, RI)
829
+ const meetingOne = delivered.filter(w => /【研究所会议/.test(w.prompt))
830
+ assert(meetingOne.length >= 2, 'both members were asked to speak (' + meetingOne.length + ')')
831
+ for (const w of meetingOne) {
832
+ recordAndCheck('meeting', w.owner, w.prompt)
833
+ assert(w.prompt.indexOf('【研究所会议 mt-1 进行中 —— ') === 0, w.owner + "'s meeting prompt is framed with the meeting id")
834
+ assert(w.prompt.indexOf('分工与下一步') !== -1, w.owner + "'s meeting prompt carries the agenda")
835
+ assert(w.prompt.indexOf('"input"') !== -1, w.owner + "'s meeting prompt documents the input field it must fill")
836
+ }
837
+ await settleInstitute(RI)
838
+ const stMtg = await callTool('vibe_v5_status', {}, RI)
839
+ assert(stMtg.meeting === null, 'the meeting finished instead of deadlocking')
840
+ const minutes = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Shared', 'Meetings', 'mt-1.md')
841
+ assert(existsSync(minutes), 'the meeting minutes were written')
842
+ if (existsSync(minutes)) {
843
+ const t = readFileSync(minutes, 'utf8')
844
+ assert(/### acad/.test(t) && /### r-1/.test(t), 'the minutes key each speech by its real member id')
845
+ assert(/有表决权者:acad、r-1/.test(t), 'the minutes list the real voting members')
846
+ }
847
+ // A non-academician may only PROPOSE a meeting; the relay must be signed by the proposer.
848
+ delivered.length = 0
849
+ const propMtg = await callTool('vibe_v5_meeting', { agenda: '我提议讨论路线', kind: 'sync' }, childAgent(childOf(RI, 'r-1')))
850
+ assert(propMtg.ok === true && propMtg.proposed === true, 'a researcher can only PROPOSE a meeting (' + JSON.stringify(propMtg).slice(0, 80) + ')')
851
+ await settle(); await drainWakes(20, RI)
852
+ const propText = delivered.map(w => w.prompt).join('\n')
853
+ for (const w of delivered) record('meeting-proposal', w.owner, w.prompt)
854
+ assert(/【研究所·致全体表决者 from r-1】[^\n]*提议开会/.test(propText),
855
+ 'the meeting proposal is relayed SIGNED BY ITS TRUE PROPOSER r-1, not by whoever was woken last')
856
+ await endCase(RI)
857
+
858
+ // =============== CASE 10b: meetings and verifications are mutually exclusive ======
859
+ section('10b a verification proposed DURING a meeting must queue, never preempt it')
860
+ const RN = makeRoot()
861
+ await callTool('vibe_v5_start', { problem: '会议与验证互斥测试', researcherCount: 1 }, RN)
862
+ for (const sp of spawnsFor(RN)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
863
+ await settleInstitute(RN)
864
+ await callTool('vibe_v5_set', { maxParallel: 8 }, RN)
865
+ await callTool('vibe_v5_record_proposition', { id: 'p-mid', statement: '会议期间提出的对象', value: 0.6, motive: 'm', p: 0.7 }, childAgent(childOf(RN, 'r-1')))
866
+ // Convene a meeting and stop before it has collected every input, so it stays in flight.
867
+ hushed.add(RN.id)
868
+ delivered.length = 0
869
+ const convened = await callTool('vibe_v5_meeting', { agenda: '先开这个会', kind: 'sync' }, childAgent(childOf(RN, 'acad')))
870
+ assert(convened.ok === true, 'a meeting was convened (' + JSON.stringify(convened).slice(0, 80) + ')')
871
+ await settle(); await drainWakes(1, RN)
872
+ const during = await callTool('vibe_v5_status', {}, RN)
873
+ assert(!!during.meeting, 'the meeting is still in flight (not everyone has spoken)')
874
+ // A member proposing a verification mid-meeting must NOT start a second, concurrent
875
+ // consensus process: the design says meetings and verifications never overlap, and a
876
+ // verification that preempts a meeting starves the meeting's watchdog clock.
877
+ const propMid = await callTool('vibe_v5_propose_verify', { target: 'p-mid', kind: 'proposition', reason: '想在会上定' }, childAgent(childOf(RN, 'r-1')))
878
+ assert(propMid.ok === true, 'the proposal is accepted (' + JSON.stringify(propMid).slice(0, 90) + ')')
879
+ await settle()
880
+ const afterProp = await callTool('vibe_v5_status', {}, RN)
881
+ assert(!!afterProp.meeting, 'the meeting is STILL in flight after the proposal')
882
+ assert(afterProp.verify === null,
883
+ 'NO verification started while the meeting was in flight (got ' + JSON.stringify(afterProp.verify && afterProp.verify.target) + ')')
884
+ assert((afterProp.verifyQueue || []).indexOf('p-mid') !== -1,
885
+ 'the proposal is QUEUED instead (queue=' + JSON.stringify(afterProp.verifyQueue) + ')')
886
+ // Once the meeting ends, the queued proposal must run — queueing must not drop it.
887
+ hushed.delete(RN.id)
888
+ await settleInstitute(RN)
889
+ const afterMtg = await callTool('vibe_v5_status', {}, RN)
890
+ assert(afterMtg.meeting === null, 'the meeting finished')
891
+ assert(afterMtg.verify !== null || afterMtg.undecided.indexOf('p-mid') !== -1 || afterMtg.verified.indexOf('p-mid') !== -1,
892
+ 'the queued proposal was started after the meeting ended (verify=' + JSON.stringify(afterMtg.verify && afterMtg.verify.target)
893
+ + ', queue=' + JSON.stringify(afterMtg.verifyQueue) + ')')
894
+ await endCase(RN)
895
+
896
+ // =============== CASE 11: no unpaced re-wake loop ===============================
897
+ section('11 a task owner is pushed on a PACED cadence, not in a tight loop')
898
+ const RJ = makeRoot()
899
+ await callTool('vibe_v5_start', { problem: '调度节奏测试', researcherCount: 1 }, RJ)
900
+ for (const sp of spawnsFor(RJ)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
901
+ await settleInstitute(RJ)
902
+ const asg = await callTool('vibe_v5_assign', { subject: '一个长任务', to: 'r-1', why: '你最合适', acceptance: '给出结果' }, childAgent(childOf(RJ, 'acad')))
903
+ assert(asg.ok === true, 'a task was assigned to r-1')
904
+ await settle(); await drainWakes(6, RJ)
905
+ const afterAssign = await callTool('vibe_v5_status', {}, RJ)
906
+ assert(afterAssign.tasks.some(t => t.status === 'in_progress' && t.ownerId === 'r-1'), 'r-1 still owns in-progress work')
907
+ assert(!afterAssign.members.some(m => m.busy), 'r-1 is idle again after answering')
908
+ // A task owner must NOT be re-woken the moment its turn ends: the work push is paced by
909
+ // activityTimeoutMs (120 s here). Without the pace, one unfinished task became an
910
+ // unbounded wake -> turn -> wake chain that burned tokens with no backoff at all.
911
+ await sleep(500); await settle()
912
+ const unpaced = wakes.filter(w => w.rootId === RJ.id)
913
+ assert(unpaced.length === 0, 'no unpaced re-wake of the task owner within the idle window (got ' + unpaced.length + ')')
914
+ await endCase(RJ)
915
+
916
+ // =============== CASE 12: Lean mode prompt text =================================
917
+ section('12 Lean formal-verification text enters the prompts (and the corpus)')
918
+ const RK = makeRoot()
919
+ await callTool('vibe_v5_start', { problem: 'Lean 提示词测试', researcherCount: 2 }, RK)
920
+ for (const sp of spawnsFor(RK)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
921
+ await settleInstitute(RK)
922
+ await callTool('vibe_v5_set', { maxParallel: 8, formalVerify: 'encourage' }, RK)
923
+ // (a) an ordinary work round carries the "formalize reusable things as you go" request
924
+ delivered.length = 0
925
+ await callTool('vibe_v5_say', { to: 'r-1', text: '继续推进。' }, childAgent(childOf(RK, 'acad')))
926
+ await settle(); await drainWakes(3, RK)
927
+ for (const w of delivered.filter(d => d.rootId === RK.id)) recordAndCheck('lean-work', w.owner, w.prompt)
928
+ {
929
+ const txt = delivered.filter(d => d.rootId === RK.id).map(d => d.prompt).join('\n')
930
+ assert(/\[形式化\] 鼓励 Lean/.test(txt), 'the state block announces the Lean mode with its counts')
931
+ assert(/【顺手形式化(鼓励)】/.test(txt), 'the work round asks for reusable objects to be formalized as work proceeds')
932
+ }
933
+ // (b) a voting round on an object WITHOUT a proof carries the "decide by difficulty" block
934
+ await callTool('vibe_v5_record_proposition', { id: 'p-lean-a', statement: 'Lean 语料对象甲', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RK, 'r-1')))
935
+ const propA = await callTool('vibe_v5_propose_verify', { target: 'p-lean-a', kind: 'proposition', reason: '语料' }, childAgent(childOf(RK, 'r-1')))
936
+ assert(propA.ok === true && propA.started === true, 'the Lean corpus ballot for object 甲 actually started (' + JSON.stringify(propA).slice(0, 90) + ')')
937
+ delivered.length = 0
938
+ const vwA = await takeVerifyPrompts(RK, 3)
939
+ assert(vwA.length === 3, 'captured three voting prompts for object 甲 (got ' + vwA.length + ')')
940
+ for (const w of vwA) recordAndCheck('lean-verify', memberOfChild(w.childId), w.prompt)
941
+ {
942
+ const txt = delivered.filter(d => d.rootId === RK.id).map(d => d.prompt).join('\n')
943
+ assert(/【Lean 形式化验证(鼓励模式)】/.test(txt), 'the voting prompt explains the Lean mode')
944
+ assert(/你唯一需要确认的就是忠实性/.test(txt), 'the voting prompt states the fidelity question')
945
+ }
946
+ await endCase(RK)
947
+ // (c) once a proof passes, the voting prompt switches to the fidelity review. This uses its
948
+ // own root: object 甲's ballot may still be in flight above, and a queued proposal would
949
+ // make the drained prompts belong to the WRONG ballot (the assertion would then fail for a
950
+ // reason that has nothing to do with the feature).
951
+ const RL2 = makeRoot()
952
+ await callTool('vibe_v5_start', { problem: 'Lean 忠实性提示词测试', researcherCount: 2 }, RL2)
953
+ for (const sp of spawnsFor(RL2)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
954
+ await settleInstitute(RL2)
955
+ await callTool('vibe_v5_set', { maxParallel: 8, formalVerify: 'encourage' }, RL2)
956
+ await callTool('vibe_v5_record_proposition', { id: 'p-lean-b', statement: 'Lean 语料对象乙', value: 0.6, motive: 'm', p: 0.9 }, childAgent(childOf(RL2, 'r-1')))
957
+ const leanB = await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-lean-b', content: 'theorem p_lean_b : 1 + 1 = 2 := by decide\n' }, childAgent(childOf(RL2, 'r-1')))
958
+ assert(leanB.ok === true && leanB.passed === true, 'object 乙 has a proof that really passes (' + JSON.stringify({ ok: leanB.ok, passed: leanB.passed, code: leanB.run && leanB.run.code }) + ')')
959
+ const propB = await callTool('vibe_v5_propose_verify', { target: 'p-lean-b', kind: 'proposition', reason: '已有证明' }, childAgent(childOf(RL2, 'r-1')))
960
+ assert(propB.ok === true && propB.started === true, 'the Lean corpus ballot for object 乙 actually started (' + JSON.stringify(propB).slice(0, 90) + ')')
961
+ delivered.length = 0
962
+ const vwB = await takeVerifyPrompts(RL2, 3)
963
+ assert(vwB.length === 3, 'captured three voting prompts for object 乙 (got ' + vwB.length + ')')
964
+ for (const w of vwB) recordAndCheck('lean-fidelity', memberOfChild(w.childId), w.prompt)
965
+ {
966
+ const txt = vwB.map(w => w.prompt).join('\n')
967
+ assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(txt), 'the prompt announces the passing proof')
968
+ assert(/你不需要重新检查推导/.test(txt), 'with a proof in hand the prompt tells voters not to re-derive')
969
+ assert(/忠实性审查/.test(txt), 'and asks for a fidelity review instead')
970
+ assert(/不要投 0/.test(txt), '★ it forbids expressing a faithfulness defect as 0 (= 命题为假)')
971
+ assert(/decision:'defect'/.test(txt), 'it names the defect reply channel that withdraws the proof')
972
+ assert(!/偏离 → 0/.test(txt), '★ the old "any deviation → 0" instruction is gone')
973
+ assert(noBareLeanTool(txt), 'no abbreviated tool name appears in the fidelity prompt')
974
+ }
975
+ await drainWakes(10, RL2)
976
+ await endCase(RL2)
977
+
978
+ // (d) `require` mode adds the conclusion gate to the voting prompt (contract §10 item 10)
979
+ const R_LEANREQ = makeRoot()
980
+ await callTool('vibe_v5_start', { problem: 'Lean require 提示词测试', researcherCount: 2 }, R_LEANREQ)
981
+ for (const sp of spawnsFor(R_LEANREQ)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
982
+ await settleInstitute(R_LEANREQ)
983
+ await callTool('vibe_v5_set', { maxParallel: 8, formalVerify: 'require' }, R_LEANREQ)
984
+ await callTool('vibe_v5_record_proposition', { id: 'p-lean-req', statement: 'Lean 语料对象丙(require 档)', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(R_LEANREQ, 'r-1')))
985
+ const propR = await callTool('vibe_v5_propose_verify', { target: 'p-lean-req', kind: 'proposition', reason: '语料' }, childAgent(childOf(R_LEANREQ, 'r-1')))
986
+ assert(propR.ok === true && propR.started === true, 'the require-mode ballot started (' + JSON.stringify(propR).slice(0, 90) + ')')
987
+ delivered.length = 0
988
+ const vwR = await takeVerifyPrompts(R_LEANREQ, 3)
989
+ assert(vwR.length === 3, 'captured three require-mode voting prompts (got ' + vwR.length + ')')
990
+ for (const w of vwR) recordAndCheck('lean-require', memberOfChild(w.childId), w.prompt)
991
+ {
992
+ const txt = vwR.map(w => w.prompt).join('\n')
993
+ assert(/【Lean 形式化验证(强制模式)】/.test(txt), 'the require-mode block is labelled 强制')
994
+ assert(/本模式要求/.test(txt) && /formal-required/.test(txt), 'it states the conclusion gate and its reason code')
995
+ assert(/vibe_v5_lean_archive/.test(txt) && /kind='blocked'/.test(txt), 'it names the full archive tool for the blocker route')
996
+ assert(/宿主无 Lean 工具链/.test(txt), 'it also says what to do when the host has no Lean toolchain')
997
+ assert(noBareLeanTool(txt), 'no abbreviated tool name appears in the require-mode prompt')
998
+ }
999
+ await drainWakes(30, R_LEANREQ)
1000
+ await endCase(R_LEANREQ)
1001
+ // (e) after a fidelity DEFECT the member must see the withdrawal, never a stale "已通过"
1002
+ const R_LEANDEF = makeRoot()
1003
+ await callTool('vibe_v5_start', { problem: 'Lean 缺陷后提示词测试', researcherCount: 2 }, R_LEANDEF)
1004
+ for (const sp of spawnsFor(R_LEANDEF)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
1005
+ await settleInstitute(R_LEANDEF)
1006
+ await callTool('vibe_v5_set', { maxParallel: 8, formalVerify: 'encourage' }, R_LEANDEF)
1007
+ await callTool('vibe_v5_record_proposition', { id: 'p-lean-def', statement: 'Lean 语料对象丁', value: 0.6, motive: 'm', p: 0.9 }, childAgent(childOf(R_LEANDEF, 'r-1')))
1008
+ const leanD = await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-lean-def', content: 'theorem p_lean_d : 1 + 1 = 2 := by decide\n' }, childAgent(childOf(R_LEANDEF, 'r-1')))
1009
+ assert(leanD.ok === true && leanD.passed === true, 'object 丁 has a passing proof before the defect')
1010
+ delivered.length = 0
1011
+ await callTool('vibe_v5_say', { to: 'r-1', text: '请核对形式化的忠实性。' }, childAgent(childOf(R_LEANDEF, 'acad')))
1012
+ await settle()
1013
+ fireEnd(childOf(R_LEANDEF, 'r-1'), { progress: '核对后发现偏差。', formal: { target: 'p-lean-def', decision: 'defect', note: '条件被加强:连续写成了逐点连续' }, contextPct: 20 })
1014
+ await settle()
1015
+ await drainWakes(8, R_LEANDEF)
1016
+ const stRP = await callTool('vibe_v5_status', {}, R_LEANDEF)
1017
+ const recRP = (stRP.formal.objects || []).find(o => o.target === 'p-lean-def') || {}
1018
+ assert(recRP.status === 'attempted' && !recRP.proof, 'the defect reply withdrew the proof (status=' + recRP.status + ')')
1019
+ delivered.length = 0
1020
+ await callTool('vibe_v5_say', { to: 'r-1', text: '再继续。' }, childAgent(childOf(R_LEANDEF, 'acad')))
1021
+ await settle(); await drainWakes(3, R_LEANDEF)
1022
+ for (const w of delivered.filter(d => d.rootId === R_LEANDEF.id)) recordAndCheck('lean-after-defect', w.owner, w.prompt)
1023
+ {
1024
+ const txt = delivered.filter(d => d.rootId === R_LEANDEF.id).map(d => d.prompt).join('\n')
1025
+ assert(!/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(txt), '★ the post-defect prompt no longer claims a passing proof')
1026
+ assert(/已通过 0/.test(txt), '★ the state block reports zero Lean-passed objects after the withdrawal')
1027
+ }
1028
+ await endCase(R_LEANDEF)
1029
+
1030
+ // =============== PART: full-corpus sweep ========================================
1031
+ section('13 full-corpus sweep over every prompt ever sent')
1032
+ {
1033
+ let swept = 0
1034
+ for (const sp of spawns) {
1035
+ if (!sp.prompt) continue
1036
+ swept++
1037
+ checkPromptSweep(sp.prompt, memberOfChild(sp.childId), 'corpus spawn ' + memberOfChild(sp.childId))
1038
+ }
1039
+ assert(swept >= 12, 'the corpus inspected every founding/resume prompt in the process (' + swept + ')')
1040
+ const owners = new Set(corpus.map(c => c.owner))
1041
+ assert(owners.has('acad') && owners.has('r-1') && owners.has('r-2'), 'the corpus covers academician and researchers (' + [...owners].join('、') + ')')
1042
+ assert([...owners].some(o => /^t-/.test(o)), 'the corpus covers a temp worker')
1043
+ const kinds = new Set(corpus.map(c => c.kind))
1044
+ for (const need of ['founding', 'founding-temp', 'founding-leaderless', 'resume', 'normal', 'checkpoint',
1045
+ 'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
1046
+ 'inbox-office', 'inbox-assign', 'inbox-nudge', 'notice', 'notice-claim', 'after-failure',
1047
+ 'lean-work', 'lean-verify', 'lean-fidelity', 'lean-require', 'lean-after-defect']) {
1048
+ assert(kinds.has(need), 'the corpus contains a ' + need + ' prompt')
1049
+ }
1050
+ assert(corpus.every(c => c.prompt && c.prompt.length > 200), 'no captured prompt is suspiciously short')
1051
+ assert(corpus.every(c => !GARBAGE.some(g => g.test(c.prompt + (c.persona || '')))), 'no prompt or charter contains undefined/NaN/? garbage')
1052
+ // A single prompt must not deliver the same message twice. The inbox used to be
1053
+ // prepended AND re-emitted from the [状态] block, so a member read every new message
1054
+ // twice in one prompt.
1055
+ for (const c of corpus) {
1056
+ const bodies = c.prompt.match(/【[^】]*】[^\n]{20,}/g) || []
1057
+ for (const frame of new Set(bodies)) {
1058
+ const n = bodies.filter(b => b === frame).length
1059
+ if (n > 1) { assert(false, 'message delivered ' + n + '× in one prompt (' + c.kind + '/' + c.owner + '): ' + frame.slice(0, 60)); break }
1060
+ }
1061
+ const inboxHeads = (c.prompt.match(/\[新到的消息/g) || []).length
1062
+ assert(inboxHeads <= 1, c.kind + '/' + c.owner + ': at most one inbox section per prompt (found ' + inboxHeads + ')')
1063
+ }
1064
+ assert(true, 'no prompt delivers the same framed message twice, and no prompt has two inbox sections')
1065
+ // The identity claim inside a prompt must agree with the persona shipped alongside it.
1066
+ for (const c of corpus) {
1067
+ if (!c.persona) continue
1068
+ const st = parseState(c.prompt)
1069
+ if (!st) continue
1070
+ assert(c.persona.indexOf('Members/' + st.id + '/') !== -1,
1071
+ c.kind + ': the charter shipped with ' + st.id + "'s prompt points at Members/" + st.id + '/')
1072
+ }
1073
+ }
1074
+
1075
+ // =============== corpus dump ====================================================
1076
+ section('14 the full prompt corpus is preserved for human review')
1077
+ mkdirSync(CORPUS_DIR, { recursive: true })
1078
+ const md = []
1079
+ md.push('# Vibe Math V5 — 提示词与交互语料(自动生成,请勿手改)')
1080
+ md.push('')
1081
+ md.push('由 `prompt-v5-integrity.test.mjs` 在每次运行时重写。这里保存的是**框架真正发给每个')
1082
+ md.push('成员的提示词原文**,用于人工复核提示词分配、成员代号与交互内容的正确性。')
1083
+ md.push('')
1084
+ md.push('- 生成时刻的工作区路径被替换为 `<WS>`,因此内容是确定性的、可 diff 的。')
1085
+ md.push('- `owner` 是这条提示词**实际发给的成员**;`kind` 是提示词类型。')
1086
+ md.push('- 人设(charter/persona)按成员只完整打印一次,其余条目只记录字符数。')
1087
+ md.push('- 这是提示词正确性的人工复核入口:任何“成员代号/职位/在册名单/交互署名”问题')
1088
+ md.push(' 都能在这里一眼看出,而不必去翻会话日志。')
1089
+ md.push('')
1090
+ const seenPersona = new Set()
1091
+ const order = ['founding', 'founding-temp', 'founding-leaderless', 'resume', 'normal', 'checkpoint',
1092
+ 'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
1093
+ 'inbox-office', 'inbox-assign', 'inbox-nudge', 'notice', 'notice-claim', 'after-failure',
1094
+ 'lean-work', 'lean-verify', 'lean-fidelity']
1095
+ const sorted = corpus.slice().sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind))
1096
+ for (let i = 0; i < sorted.length; i++) {
1097
+ const c = sorted[i]
1098
+ md.push('---')
1099
+ md.push('')
1100
+ md.push('## [' + (i + 1) + '] kind=`' + c.kind + '` owner=`' + c.owner + '`')
1101
+ md.push('')
1102
+ if (c.toolFilter) md.push('- toolFilter: `' + JSON.stringify(c.toolFilter) + '`')
1103
+ md.push('- charter: ' + (c.persona == null ? '(本次唤醒不带人设)' : c.persona.length + ' 字符'))
1104
+ md.push('')
1105
+ if (c.persona != null && !seenPersona.has(c.owner)) {
1106
+ seenPersona.add(c.owner)
1107
+ md.push('### 人设 / 规章(' + c.owner + ',仅首次完整打印)')
1108
+ md.push('')
1109
+ md.push('```text')
1110
+ md.push(c.persona)
1111
+ md.push('```')
1112
+ md.push('')
1113
+ }
1114
+ md.push('### 提示词原文')
1115
+ md.push('')
1116
+ md.push('```text')
1117
+ md.push(c.prompt)
1118
+ md.push('```')
1119
+ md.push('')
1120
+ }
1121
+ const byKind = {}
1122
+ for (const c of corpus) byKind[c.kind] = (byKind[c.kind] || 0) + 1
1123
+ md.push('---')
1124
+ md.push('')
1125
+ md.push('## 统计')
1126
+ md.push('')
1127
+ for (const k of Object.keys(byKind).sort()) md.push('- `' + k + '`: ' + byKind[k])
1128
+ md.push('')
1129
+ md.push('- 合计:' + corpus.length + ' 条提示词')
1130
+ md.push('')
1131
+ const mdPath = join(CORPUS_DIR, 'prompt-corpus-v5.md')
1132
+ writeFileSync(mdPath, md.join('\n'), 'utf8')
1133
+ writeFileSync(join(CORPUS_DIR, 'prompt-corpus-v5.json'), JSON.stringify({
1134
+ note: 'Vibe Math V5 prompt/interaction corpus — generated by prompt-v5-integrity.test.mjs. <WS> = the run workspace.',
1135
+ counts: byKind, total: corpus.length,
1136
+ prompts: corpus.map(c => ({
1137
+ kind: c.kind, owner: c.owner, sentToLabel: c.sentToLabel,
1138
+ charterChars: c.persona == null ? null : c.persona.length,
1139
+ charter: c.persona, toolFilter: c.toolFilter, prompt: c.prompt,
1140
+ })),
1141
+ }, null, 2), 'utf8')
1142
+ assert(existsSync(mdPath), 'the prompt corpus Markdown was written')
1143
+ assert(existsSync(join(CORPUS_DIR, 'prompt-corpus-v5.json')), 'the prompt corpus JSON was written')
1144
+ const corpusMd = readFileSync(mdPath, 'utf8')
1145
+ assert(corpusMd.length > 30000, 'the corpus is substantial (' + corpusMd.length + ' chars) — the real prompt text is preserved')
1146
+ assert(corpusMd.indexOf('[状态] 你是 acad(院士)') !== -1, 'a human can verify the academician brief verbatim')
1147
+ assert(corpusMd.indexOf('[状态] 你是 r-2') !== -1, 'a human can verify a researcher brief naming itself')
1148
+ assert(corpusMd.indexOf('【框架提示】') !== -1, 'the corpus contains the framework-feedback interaction')
1149
+ assert(corpusMd.indexOf('【会话重建 —— ') !== -1, 'the corpus contains a resume brief')
1150
+ assert(corpusMd.indexOf('【所办分派】') !== -1, 'the corpus contains an office assignment')
1151
+ assert(!/你是 \?/.test(corpusMd), 'the corpus contains NO wrong-identity "?" brief')
1152
+
1153
+ console.log('')
1154
+ console.log('corpus: ' + mdPath)
1155
+ console.log('passed=' + passed + ' failed=' + failed)
1156
+ if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
1157
+ console.log('ALL GREEN')
1158
+ process.exit(0)