dsh-vibe-math 2.3.12 → 2.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -13
- package/{AUDIT-CHECKLIST.md → docs/AUDIT-CHECKLIST.md} +323 -304
- package/docs/COMPAT-AUDIT-ROUND2.md +325 -0
- package/docs/generate_framework_diagram_v2.py +114 -0
- package/docs/generate_framework_diagram_v3.py +127 -0
- package/{RELEASE-NOTES-2.1.0.md → docs/release-notes/RELEASE-NOTES-2.1.0.md} +143 -143
- package/{RELEASE-NOTES-2.2.0.md → docs/release-notes/RELEASE-NOTES-2.2.0.md} +266 -266
- package/{RELEASE-NOTES-2.2.1.md → docs/release-notes/RELEASE-NOTES-2.2.1.md} +43 -43
- package/{RELEASE-NOTES-2.2.2.md → docs/release-notes/RELEASE-NOTES-2.2.2.md} +88 -88
- package/{RELEASE-NOTES-2.3.0.md → docs/release-notes/RELEASE-NOTES-2.3.0.md} +207 -207
- package/{RELEASE-NOTES-2.3.1.md → docs/release-notes/RELEASE-NOTES-2.3.1.md} +134 -134
- package/{RELEASE-NOTES-2.3.10.md → docs/release-notes/RELEASE-NOTES-2.3.10.md} +105 -105
- package/{RELEASE-NOTES-2.3.11.md → docs/release-notes/RELEASE-NOTES-2.3.11.md} +57 -57
- package/{RELEASE-NOTES-2.3.12.md → docs/release-notes/RELEASE-NOTES-2.3.12.md} +80 -80
- package/docs/release-notes/RELEASE-NOTES-2.3.13.md +137 -0
- package/{RELEASE-NOTES-2.3.2.md → docs/release-notes/RELEASE-NOTES-2.3.2.md} +145 -145
- package/{RELEASE-NOTES-2.3.3.md → docs/release-notes/RELEASE-NOTES-2.3.3.md} +115 -115
- package/{RELEASE-NOTES-2.3.4.md → docs/release-notes/RELEASE-NOTES-2.3.4.md} +69 -69
- package/{RELEASE-NOTES-2.3.5.md → docs/release-notes/RELEASE-NOTES-2.3.5.md} +63 -63
- package/{RELEASE-NOTES-2.3.6.md → docs/release-notes/RELEASE-NOTES-2.3.6.md} +66 -66
- package/{RELEASE-NOTES-2.3.7.md → docs/release-notes/RELEASE-NOTES-2.3.7.md} +59 -59
- package/{RELEASE-NOTES-2.3.8.md → docs/release-notes/RELEASE-NOTES-2.3.8.md} +45 -45
- package/{RELEASE-NOTES-2.3.9.md → docs/release-notes/RELEASE-NOTES-2.3.9.md} +70 -70
- package/docs/test-timing.md +18 -17
- package/installer.js +105 -41
- package/package.json +42 -37
- package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
- package/{audit-installer-compat.test.mjs → tests/audit-installer-compat.test.mjs} +136 -136
- package/tests/audit-installer-policy.test.mjs +245 -0
- package/{audit-persona-sensitivity.mjs → tests/audit-persona-sensitivity.mjs} +249 -249
- package/{audit-persona-surface.test.mjs → tests/audit-persona-surface.test.mjs} +349 -349
- package/{audit-prompt-invariants.mjs → tests/audit-prompt-invariants.mjs} +508 -508
- package/{audit-spec-traceability.mjs → tests/audit-spec-traceability.mjs} +193 -193
- package/{audit-v5-integrity.mjs → tests/audit-v5-integrity.mjs} +448 -448
- package/{audit-v5-sensitivity.mjs → tests/audit-v5-sensitivity.mjs} +384 -384
- package/{e2e-v5-round2.test.mjs → tests/e2e-v5-round2.test.mjs} +521 -521
- package/{formal-verify-v2.test.mjs → tests/formal-verify-v2.test.mjs} +1315 -1315
- package/{formal-verify-v3.test.mjs → tests/formal-verify-v3.test.mjs} +1257 -1257
- package/{formal-verify-v4.test.mjs → tests/formal-verify-v4.test.mjs} +1082 -1082
- package/{formal-verify-v5.test.mjs → tests/formal-verify-v5.test.mjs} +708 -708
- package/{prompt-v5-integrity.test.mjs → tests/prompt-v5-integrity.test.mjs} +3 -3
- package/{run-tests.mjs → tests/run-tests.mjs} +121 -118
- package/{selfdrive-v5.mjs → tests/selfdrive-v5.mjs} +470 -470
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +1 -1
- /package/{RELEASE-NOTES-2.0.22.md → docs/release-notes/RELEASE-NOTES-2.0.22.md} +0 -0
|
@@ -1,470 +1,470 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Vibe-Math-V5 SELF-DRIVING TEST — drives the REAL vibe-math-v5 plugin with a mock
|
|
3
|
-
// host, including a faithful mock of the HOST-ONLY session projection unit it relies
|
|
4
|
-
// on (register / eager fold on append / stateOf), so the projection storage path is
|
|
5
|
-
// actually exercised rather than stubbed out.
|
|
6
|
-
//
|
|
7
|
-
// Run: node selfdrive-v5.mjs (temp workspace; assertions; non-zero exit on failure)
|
|
8
|
-
// ============================================================
|
|
9
|
-
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
10
|
-
import { tmpdir } from 'node:os'
|
|
11
|
-
import { join, dirname, isAbsolute } from 'node:path'
|
|
12
|
-
|
|
13
|
-
// V5_PLUGIN lets a sensitivity probe point this suite at a deliberately broken copy.
|
|
14
|
-
const PLUGIN = process.env.V5_PLUGIN
|
|
15
|
-
? new URL('file:///' + String(process.env.V5_PLUGIN).replace(/\\/g, '/'))
|
|
16
|
-
: new URL('
|
|
17
|
-
const WS = mkdtempSync(join(tmpdir(), 'vibe-v5-'))
|
|
18
|
-
let passed = 0, failed = 0
|
|
19
|
-
const failures = []
|
|
20
|
-
const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
|
|
21
|
-
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
22
|
-
|
|
23
|
-
// ---------- mock host: projection registry ----------
|
|
24
|
-
// Mirrors the contract the plugin depends on: register returns a disposer, every
|
|
25
|
-
// committed event is folded EAGERLY into every unit's cell, stateOf materialises a
|
|
26
|
-
// cell lazily with init().
|
|
27
|
-
function makeProjectionRegistry() {
|
|
28
|
-
const units = new Map()
|
|
29
|
-
const cells = new Map()
|
|
30
|
-
function cellMap(sess) {
|
|
31
|
-
const id = String(sess.id)
|
|
32
|
-
let m = cells.get(id)
|
|
33
|
-
if (!m) { m = new Map(); cells.set(id, m) }
|
|
34
|
-
return m
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
register(def) {
|
|
38
|
-
if (!def || typeof def.key !== 'string') throw new Error('projection: key required')
|
|
39
|
-
units.set(def.key, def)
|
|
40
|
-
return () => { units.delete(def.key) }
|
|
41
|
-
},
|
|
42
|
-
stateOf(session, key) {
|
|
43
|
-
const def = units.get(key)
|
|
44
|
-
if (!def) return undefined
|
|
45
|
-
const m = cellMap(session)
|
|
46
|
-
if (!m.has(key)) m.set(key, def.init(session.header, session.inheritedEventCount || 0))
|
|
47
|
-
return m.get(key)
|
|
48
|
-
},
|
|
49
|
-
checkpoint(session) {
|
|
50
|
-
const out = {}
|
|
51
|
-
const m = cellMap(session)
|
|
52
|
-
for (const [k, def] of units) out[k] = { ver: def.stateVersion, seq: session.seq, val: m.get(k) }
|
|
53
|
-
return out
|
|
54
|
-
},
|
|
55
|
-
_drive(session, event) {
|
|
56
|
-
const m = cellMap(session)
|
|
57
|
-
for (const [k, def] of units) {
|
|
58
|
-
const cur = m.has(k) ? m.get(k) : def.init(session.header, session.inheritedEventCount || 0)
|
|
59
|
-
let next
|
|
60
|
-
try { next = def.apply(cur, event) } catch (e) { next = cur }
|
|
61
|
-
m.set(k, next)
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
_units: units,
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const projections = makeProjectionRegistry()
|
|
69
|
-
const listeners = {}
|
|
70
|
-
const toolRegs = []
|
|
71
|
-
const commandRegs = []
|
|
72
|
-
const spawns = []
|
|
73
|
-
const wakes = []
|
|
74
|
-
const interrupts = []
|
|
75
|
-
const drains = []
|
|
76
|
-
const liveAgents = new Map()
|
|
77
|
-
|
|
78
|
-
function makeMockSession(id, parentSession) {
|
|
79
|
-
const events = []
|
|
80
|
-
const s = {
|
|
81
|
-
id,
|
|
82
|
-
header: { version: 1, id, createdAt: Date.now(), cwd: WS, parentSession, isSeeded: false },
|
|
83
|
-
inheritedEventCount: 0,
|
|
84
|
-
get seq() { return events.length },
|
|
85
|
-
append(type, data) {
|
|
86
|
-
const ev = { type, data, seq: events.length, time: Date.now() }
|
|
87
|
-
events.push(ev)
|
|
88
|
-
projections._drive(s, ev)
|
|
89
|
-
return ev
|
|
90
|
-
},
|
|
91
|
-
deriveMessages() { return [] }, // non-surface events must never become messages
|
|
92
|
-
snapshotEvents(from) { return events.slice(from || 0) },
|
|
93
|
-
ownEvents() { return events.slice() },
|
|
94
|
-
_events: events,
|
|
95
|
-
}
|
|
96
|
-
return s
|
|
97
|
-
}
|
|
98
|
-
const ROOT_SESSION = makeMockSession('sess-A', undefined)
|
|
99
|
-
const ROOT = { id: 'sess-A', options: { provider: 'mock', model: 'm' }, session: ROOT_SESSION, ctx: undefined }
|
|
100
|
-
|
|
101
|
-
const ctx = {
|
|
102
|
-
get(name) {
|
|
103
|
-
if (name === 'sessionProjections') return projections
|
|
104
|
-
if (name === 'sandboxPolicy') return undefined
|
|
105
|
-
if (name === 'compaction') return undefined
|
|
106
|
-
if (name === 'subprocess') {
|
|
107
|
-
return {
|
|
108
|
-
async spawn({ argv }) {
|
|
109
|
-
const script = argv[argv.length - 1] || ''
|
|
110
|
-
if (/New-Item/.test(script)) {
|
|
111
|
-
const paths = []
|
|
112
|
-
const re = /'((?:[^']|'')*)'/g
|
|
113
|
-
let m
|
|
114
|
-
while ((m = re.exec(script)) !== null) paths.push(m[1].replace(/''/g, "'"))
|
|
115
|
-
for (const p of paths) if (p && !/^-/.test(p)) mkdirSync(p, { recursive: true })
|
|
116
|
-
}
|
|
117
|
-
return { done: Promise.resolve({ exitCode: 0 }) }
|
|
118
|
-
},
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return undefined
|
|
122
|
-
},
|
|
123
|
-
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
124
|
-
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
125
|
-
logger: { info() {}, warn() {}, error() {} },
|
|
126
|
-
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
127
|
-
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
128
|
-
commands: { register(spec) { commandRegs.push(spec); return () => {} } },
|
|
129
|
-
sessions: { async flush() { return true } },
|
|
130
|
-
subagents: {
|
|
131
|
-
list() { return ['spawn'] },
|
|
132
|
-
async startContinuable({ label, request }) {
|
|
133
|
-
const id = 'c' + (spawns.length + 1)
|
|
134
|
-
const childSession = makeMockSession(id, 'sess-A')
|
|
135
|
-
const agent = { id, session: childSession, options: request && request.agentOptions }
|
|
136
|
-
liveAgents.set(id, agent)
|
|
137
|
-
spawns.push({ label, request, childId: id, persona: request && request.persona, toolFilter: request && request.toolFilter, ended: false })
|
|
138
|
-
return { childId: id, messageId: 'm' + spawns.length }
|
|
139
|
-
},
|
|
140
|
-
async sendMessage(parent, childId, blocks, opts) {
|
|
141
|
-
wakes.push({ childId, blocks })
|
|
142
|
-
return 'w' + wakes.length
|
|
143
|
-
},
|
|
144
|
-
interrupt(childId) { interrupts.push(childId) },
|
|
145
|
-
async drainContinuableChildren(parent, ids) { drains.push(...ids); for (const i of ids) liveAgents.delete(i) },
|
|
146
|
-
},
|
|
147
|
-
agents: {
|
|
148
|
-
roots() { return [ROOT] },
|
|
149
|
-
get(id) { return id === 'sess-A' ? ROOT : liveAgents.get(id) },
|
|
150
|
-
list() { return [ROOT, ...liveAgents.values()] },
|
|
151
|
-
},
|
|
152
|
-
fs: {
|
|
153
|
-
async resolve(rel, opts) {
|
|
154
|
-
const b = (opts && opts.cwd) || WS
|
|
155
|
-
const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/'))
|
|
156
|
-
return { targetKey: p, displayPath: p }
|
|
157
|
-
},
|
|
158
|
-
async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
|
|
159
|
-
async readText(t) { return readFileSync(t.targetKey, 'utf8') },
|
|
160
|
-
async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
|
|
161
|
-
async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
162
|
-
},
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const mod = await import(PLUGIN.href + '?t=' + Date.now())
|
|
166
|
-
const plugin = mod.default || mod
|
|
167
|
-
plugin.apply(ctx)
|
|
168
|
-
|
|
169
|
-
async function callTool(name, args, agent) {
|
|
170
|
-
const spec = toolRegs.find(x => x.name === name)
|
|
171
|
-
if (!spec) throw new Error('no tool ' + name)
|
|
172
|
-
return JSON.parse(await spec.execute(args || {}, { agent: agent || ROOT }))
|
|
173
|
-
}
|
|
174
|
-
const childAgent = (childId) => liveAgents.get(childId) || { id: childId, session: { header: { parentSession: 'sess-A' } } }
|
|
175
|
-
function fireEnd(childId, reply, stopReason) {
|
|
176
|
-
const blocks = reply === undefined ? [] : [{ type: 'text', text: '```json\n' + JSON.stringify(reply) + '\n```' }]
|
|
177
|
-
for (const h of (listeners['subagent/end'] || [])) {
|
|
178
|
-
h({ id: childId, runId: 'r', provider: 'spawn', local: true, stopReason: stopReason || 'completed', lastAssistantMessage: blocks })
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const settleAll = async () => { await sleep(30) }
|
|
182
|
-
const spawnOf = (memberId) => spawns.find(s => s.label.indexOf('vibe5 ' + memberId + ' ') !== -1)
|
|
183
|
-
const childOf = (memberId) => { const s = spawnOf(memberId); return s ? s.childId : memberId }
|
|
184
|
-
const wakeKindOf = (prompt) => {
|
|
185
|
-
if (/【入职首轮/.test(prompt)) return 'initial'
|
|
186
|
-
if (/【研究所会议/.test(prompt)) return 'meeting'
|
|
187
|
-
if (/【求真表决/.test(prompt)) return 'verify'
|
|
188
|
-
if (/【心跳检查/.test(prompt)) return 'checkpoint'
|
|
189
|
-
return 'normal'
|
|
190
|
-
}
|
|
191
|
-
const memberLabelOf = (childId) => { const s = spawns.find(x => x.childId === childId); const m = s ? /vibe5 (\S+) /.exec(s.label) : null; return m ? m[1] : '' }
|
|
192
|
-
|
|
193
|
-
// A planned-vote table lets the driver answer verification prompts realistically.
|
|
194
|
-
let plannedVotes = null
|
|
195
|
-
let verifyProposals = []
|
|
196
|
-
async function settleSpawn(sp) {
|
|
197
|
-
if (sp.ended) return
|
|
198
|
-
sp.ended = true
|
|
199
|
-
const who = memberLabelOf(sp.childId)
|
|
200
|
-
if (who && !/^t-/.test(who)) {
|
|
201
|
-
await callTool('vibe_v5_record_proposition', {
|
|
202
|
-
id: 'p-' + who, title: '引理 ' + who, statement: '设 n>2 且存在整数解 x^n+y^n=z^n,则可由最小反例归约得矛盾。',
|
|
203
|
-
value: 0.6, motive: '用于反证原命题', p: 0.8,
|
|
204
|
-
}, childAgent(sp.childId))
|
|
205
|
-
}
|
|
206
|
-
fireEnd(sp.childId, { progress: who + ':初始见解——先做最小反例归约,再处理边界情形。', solved: false, contextPct: 10 })
|
|
207
|
-
await settleAll()
|
|
208
|
-
}
|
|
209
|
-
async function settleAllSpawns() { for (const sp of spawns.slice()) await settleSpawn(sp) }
|
|
210
|
-
|
|
211
|
-
// Answer ONE queued wake, according to the prompt's actual kind.
|
|
212
|
-
async function answerWake(w) {
|
|
213
|
-
const prompt = (w.blocks && w.blocks[0] && w.blocks[0].text) || ''
|
|
214
|
-
const kind = wakeKindOf(prompt)
|
|
215
|
-
const who = memberLabelOf(w.childId)
|
|
216
|
-
let reply
|
|
217
|
-
if (kind === 'verify') {
|
|
218
|
-
// Read the target from the JSON spec line the prompt ends with — parsing the
|
|
219
|
-
// Chinese prose line would swallow the full-width colon into the id.
|
|
220
|
-
const tm = /"target"\s*:\s*"([^"]+)"/.exec(prompt)
|
|
221
|
-
const target = tm ? tm[1] : (verifyProposals[verifyProposals.length - 1] || 'p-r-1')
|
|
222
|
-
const v = plannedVotes && plannedVotes.has(who) ? plannedVotes.get(who) : 0.5
|
|
223
|
-
reply = { verdict: { target, verdict: v, reason: who + ' 的判断:' + (v === 1 ? '成立' : v === 0 ? '不成立' : '不确定') }, contextPct: 20 }
|
|
224
|
-
} else if (kind === 'meeting') {
|
|
225
|
-
reply = { input: who + ':我的意见已写在 Progress/ 里。', vote_solved: solvePlan === true, solved: solvePlan === true, contextPct: 20 }
|
|
226
|
-
} else {
|
|
227
|
-
reply = { progress: who + ':本轮继续推进最小反例路线。', solved: false, contextPct: 20 }
|
|
228
|
-
}
|
|
229
|
-
fireEnd(w.childId, reply)
|
|
230
|
-
await settleAll()
|
|
231
|
-
return kind
|
|
232
|
-
}
|
|
233
|
-
// Drive every queued wake: answer by the prompt's actual kind.
|
|
234
|
-
async function drainWakes(budget = 40) {
|
|
235
|
-
let n = 0
|
|
236
|
-
while (wakes.length && n < budget) { await answerWake(wakes.shift()); n++ }
|
|
237
|
-
return n
|
|
238
|
-
}
|
|
239
|
-
let solvePlan = null
|
|
240
|
-
|
|
241
|
-
// Answer exactly ONE voting round: keep pulling wakes (heartbeats and work prompts also
|
|
242
|
-
// sit in the queue) until `n` VERIFY prompts have been answered, then stop.
|
|
243
|
-
//
|
|
244
|
-
// A fixed `drainWakes(n)` budget is not enough — it can stop before every voter has
|
|
245
|
-
// answered, and an assertion like "this must not be verified yet" then passes vacuously
|
|
246
|
-
// however badly the quorum rule is broken. (Proven: weakening the m floor left this suite
|
|
247
|
-
// GREEN.) Equally, a helper that loops until the verification CLOSES would silently burn
|
|
248
|
-
// through every debate round, so this stops the moment the last voter of one round has
|
|
249
|
-
// answered.
|
|
250
|
-
async function drainVerifyRound(n) {
|
|
251
|
-
let answered = 0
|
|
252
|
-
for (let guard = 0; guard < n * 6 && answered < n; guard++) {
|
|
253
|
-
const idx = wakes.findIndex(w => wakeKindOf((w.blocks && w.blocks[0] && w.blocks[0].text) || '') === 'verify')
|
|
254
|
-
if (idx === -1) {
|
|
255
|
-
if (!wakes.length) break
|
|
256
|
-
await answerWake(wakes.shift())
|
|
257
|
-
continue
|
|
258
|
-
}
|
|
259
|
-
const w = wakes.splice(idx, 1)[0]
|
|
260
|
-
await answerWake(w)
|
|
261
|
-
answered++
|
|
262
|
-
}
|
|
263
|
-
return answered
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ============================================================
|
|
267
|
-
console.log('-- V5 self-drive --')
|
|
268
|
-
|
|
269
|
-
// ---------- founding ----------
|
|
270
|
-
const started = await callTool('vibe_v5_start', { problem: '证明:不存在整数解 x^n + y^n = z^n(n>2)的初等情形', researcherCount: 3 })
|
|
271
|
-
assert(started.ok === true, 'vibe_v5_start ok (' + JSON.stringify(started).slice(0, 150) + ')')
|
|
272
|
-
assert(spawns.length === 4, 'founded 1 academician + 3 researchers (got ' + spawns.length + ')')
|
|
273
|
-
assert(!!spawnOf('acad'), 'academician acad exists')
|
|
274
|
-
assert(['r-1', 'r-2', 'r-3'].every(r => !!spawnOf(r)), '3 permanent researchers r-1..r-3')
|
|
275
|
-
assert(spawns.every(s => typeof s.persona === 'string' && s.persona.length > 2000), 'every member got the full charter as its persona')
|
|
276
|
-
assert(spawns[0].persona.indexOf('progress.md') !== -1 && spawns[0].persona.indexOf('它的用途') !== -1,
|
|
277
|
-
'charter carries the progress definition AND its purpose explanation')
|
|
278
|
-
assert(spawns[0].persona.indexOf('分派') !== -1 && spawns[0].persona.indexOf('边界') !== -1,
|
|
279
|
-
'charter describes the academician as the organizational centre (duties + four boundaries)')
|
|
280
|
-
assert(spawns[0].persona.indexOf('m = 3') !== -1 || /至少有 m = \d+ 名有表决权者/.test(spawns[0].persona), 'charter states the m-vote rule with the live m')
|
|
281
|
-
|
|
282
|
-
const st0 = await callTool('vibe_v5_status', {})
|
|
283
|
-
assert(st0.backend === 'projection', 'durable state uses the session-projections backend (got ' + st0.backend + ')')
|
|
284
|
-
assert(st0.members.length === 4 && st0.members.filter(m => m.kind === 'researcher').length === 3, 'status roster is 1 academician + 3 researchers')
|
|
285
|
-
assert(ROOT_SESSION._events.some(e => String(e.type).indexOf('vibe5/') === 0), 'institute events are appended to the session log')
|
|
286
|
-
assert(ROOT_SESSION.deriveMessages().length === 0, 'institute events never enter the model history (zero context cost)')
|
|
287
|
-
assert(st0.quorum.m === 3 && st0.quorum.voterCount === 4, 'm = min(quorumCap=3, P=4) = 3 (got ' + st0.quorum.m + ')')
|
|
288
|
-
assert(st0.members.every(m => m.busy === true), 'every member is marked in-flight while its founding turn runs')
|
|
289
|
-
|
|
290
|
-
// founding turns complete
|
|
291
|
-
await settleAllSpawns()
|
|
292
|
-
let st1 = await callTool('vibe_v5_status', {})
|
|
293
|
-
assert(st1.members.every(m => m.busy === false), 'founding turns were processed (no member left marked busy)')
|
|
294
|
-
assert(st1.members.every(m => m.rounds >= 1), 'each member completed its founding round')
|
|
295
|
-
const propWritten = existsSync(join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Members', 'r-1', 'Propos', 'p-r-1.md'))
|
|
296
|
-
assert(propWritten, 'per-member proposition library file was written (Members/r-1/Propos/p-r-1.md)')
|
|
297
|
-
const progWritten = existsSync(join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Members', 'r-1', 'Progress', 'progress.md'))
|
|
298
|
-
assert(progWritten, 'per-member progress.md was written')
|
|
299
|
-
|
|
300
|
-
// missing-mandatory-fields refusal (the charter's three hard requirements)
|
|
301
|
-
const badRec = await callTool('vibe_v5_record_proposition', { statement: 'x', value: 0.5 }, childAgent(childOf('r-2')))
|
|
302
|
-
assert(badRec.ok === false && badRec.code === 'V5_INVALID_ARGUMENT', 'recording without motive/p is refused (three hard requirements enforced)')
|
|
303
|
-
|
|
304
|
-
// ---------- group chat fan-out / DM ----------
|
|
305
|
-
const sayR1 = await callTool('vibe_v5_say', { text: '各位,我建议先做最小反例归约。' }, childAgent(childOf('r-1')))
|
|
306
|
-
assert(sayR1.ok === true && sayR1.delivered === 3, 'group chat fans out to every OTHER active member (4 active - sender = 3; got ' + sayR1.delivered + ')')
|
|
307
|
-
const sayDM = await callTool('vibe_v5_say', { to: 'r-2', text: '私下问你一下。' }, childAgent(childOf('r-1')))
|
|
308
|
-
assert(sayDM.ok === true && sayDM.delivered === 1, 'a private message reaches exactly one member')
|
|
309
|
-
const selfMsg = await callTool('vibe_v5_say', { to: 'r-1', text: 'x' }, childAgent(childOf('r-1')))
|
|
310
|
-
assert(selfMsg.ok === false && selfMsg.code === 'V5_SELF_MESSAGE', 'messaging yourself is refused')
|
|
311
|
-
|
|
312
|
-
// ---------- temp workers ----------
|
|
313
|
-
const hired = await callTool('vibe_v5_hire', { purpose: '核对文献中的引理', initial_task: '核对第 3 节引理是否成立' }, childAgent(childOf('r-1')))
|
|
314
|
-
assert(hired.ok === true && /^t-\d+$/.test(hired.id || ''), 'a permanent researcher CAN hire a temp worker (got ' + JSON.stringify(hired).slice(0, 80) + ')')
|
|
315
|
-
await settleAllSpawns()
|
|
316
|
-
const tempHire = await callTool('vibe_v5_hire', { purpose: 'x', initial_task: 'y' }, childAgent(childOf(hired.id)))
|
|
317
|
-
assert(tempHire.ok === false && tempHire.code === 'V5_NOT_VOTER', 'a temp worker CANNOT hire (V5_NOT_VOTER)')
|
|
318
|
-
const tempVote = await callTool('vibe_v5_verdict', { target: 'p-r-2', verdict: 1, reason: '我觉得对' }, childAgent(childOf(hired.id)))
|
|
319
|
-
assert(tempVote.ok === false && tempVote.code === 'V5_NOT_VOTER', 'a temp worker CANNOT vote (V5_NOT_VOTER)')
|
|
320
|
-
const stT = await callTool('vibe_v5_status', {})
|
|
321
|
-
assert(stT.quorum.voterCount === 4 && stT.quorum.m === 3, 'the temp worker did NOT change the voter count or m')
|
|
322
|
-
|
|
323
|
-
// ---------- verification: the m-quorum boolean rule ----------
|
|
324
|
-
const target = 'p-r-1'
|
|
325
|
-
verifyProposals.push(target)
|
|
326
|
-
const prop = await callTool('vibe_v5_propose_verify', { target, kind: 'proposition', reason: '我已在库里给出完整证明' }, childAgent(childOf('r-1')))
|
|
327
|
-
assert(prop.ok === true, 'proposing verification is accepted')
|
|
328
|
-
// The proposal itself must kick the scheduler, so the object starts verifying without
|
|
329
|
-
// needing any unrelated event to drive a pass.
|
|
330
|
-
let sv = await callTool('vibe_v5_status', {})
|
|
331
|
-
assert(!!sv.verify, 'verification of ' + target + ' is in flight right after proposing (nobody has voted yet) — got ' +
|
|
332
|
-
JSON.stringify({ verify: sv.verify, queue: sv.verifyQueue, undecided: sv.undecided, running: sv.running, autoDone: sv.autoDone, phase: sv.phase, meeting: sv.meeting, parked: sv.parkedMeeting, tasks: sv.tasks.length }))
|
|
333
|
-
if (sv.verify) {
|
|
334
|
-
assert(sv.verify.m === 3 && sv.verify.P === 4, 'verification reports m=3 over P=4 voters')
|
|
335
|
-
|
|
336
|
-
// Round: only ONE boolean vote (< m) must NOT verify.
|
|
337
|
-
plannedVotes = new Map([['acad', 0.9], ['r-1', 1], ['r-2', 0.5], ['r-3', 0.7]])
|
|
338
|
-
assert(await drainVerifyRound(4) === 4, 'all four voters answered round 1')
|
|
339
|
-
await settleAll()
|
|
340
|
-
let sA = await callTool('vibe_v5_status', {})
|
|
341
|
-
assert(sA.verified.indexOf(target) === -1, 'a single boolean vote (m-1=2 short) does NOT verify')
|
|
342
|
-
assert(!!sA.verify, 'the verification is still open (abstentions do not settle it)')
|
|
343
|
-
assert(sA.verify && sA.verify.stage === 'debate', 'the round really completed and moved to the DEBATE stage (got ' + (sA.verify && sA.verify.stage) + ')')
|
|
344
|
-
|
|
345
|
-
// Debate round: 1 vs 0 conflict must NOT verify.
|
|
346
|
-
plannedVotes = new Map([['acad', 0], ['r-1', 1], ['r-2', 1], ['r-3', 1]])
|
|
347
|
-
assert(await drainVerifyRound(4) === 4, 'all four voters answered the debate round')
|
|
348
|
-
await settleAll()
|
|
349
|
-
let sB = await callTool('vibe_v5_status', {})
|
|
350
|
-
assert(sB.verified.indexOf(target) === -1, 'a conflicting 1/0 assertion BLOCKS the verdict (no minority override)')
|
|
351
|
-
assert(!!sB.verify, 'the verification survived the conflicting round (not silently forced)')
|
|
352
|
-
|
|
353
|
-
// Final: three unanimous TRUE votes (m=3) DO verify.
|
|
354
|
-
plannedVotes = new Map([['acad', 1], ['r-1', 1], ['r-2', 1], ['r-3', 1]])
|
|
355
|
-
await drainVerifyRound(4)
|
|
356
|
-
await settleAll()
|
|
357
|
-
let sC = await callTool('vibe_v5_status', {})
|
|
358
|
-
if (!sC.verified.includes(target)) { await drainWakes(8); sC = await callTool('vibe_v5_status', {}) }
|
|
359
|
-
assert(sC.verified.indexOf(target) !== -1, 'THREE unanimous TRUE votes (m=3) DID verify ' + target)
|
|
360
|
-
const verifiedCard = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', '命题', target + '.md')
|
|
361
|
-
assert(existsSync(verifiedCard), 'Verified/命题/' + target + '.md was written')
|
|
362
|
-
if (existsSync(verifiedCard)) {
|
|
363
|
-
const card = readFileSync(verifiedCard, 'utf8')
|
|
364
|
-
assert(/- 结论: 真/.test(card), 'the Verified card records the conclusion 真')
|
|
365
|
-
assert(/名有表决权者一致判真/.test(card) && /m=3/.test(card), 'the Verified card records the m-vote basis (not "unanimous of all")')
|
|
366
|
-
}
|
|
367
|
-
const srcCard = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Members', 'r-1', 'Propos', 'p-r-1.md')
|
|
368
|
-
assert(existsSync(srcCard) && /- 状态: 已验证·真/.test(readFileSync(srcCard, 'utf8')), 'the source card was rewritten to 已验证·真')
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// ---------- the academician's organization powers ----------
|
|
372
|
-
const asg = await callTool('vibe_v5_assign', { subject: '核验 n=3 的情形', to: 'r-2', why: '你在同余方向最强', acceptance: '给出完整的模 9 分析' }, childAgent(childOf('acad')))
|
|
373
|
-
assert(asg.ok === true && asg.task && asg.task.ownerId === 'r-2', 'the academician CAN assign a task to a member')
|
|
374
|
-
assert(!!asg.task.why && !!asg.task.acceptance, 'the assignment records WHY and the acceptance criteria')
|
|
375
|
-
const asgNoWhy = await callTool('vibe_v5_assign', { to: 'r-3', why: '', acceptance: 'a' }, childAgent(childOf('acad')))
|
|
376
|
-
assert(asgNoWhy.ok === false, 'an assignment without a reason is refused')
|
|
377
|
-
const asgByR1 = await callTool('vibe_v5_assign', { subject: 'x', to: 'r-3', why: 'w', acceptance: 'a' }, childAgent(childOf('r-1')))
|
|
378
|
-
assert(asgByR1.ok === false && asgByR1.code === 'V5_NOT_ACADEMICIAN', 'a researcher CANNOT assign (V5_NOT_ACADEMICIAN)')
|
|
379
|
-
const nudgeBad = await callTool('vibe_v5_nudge', { to: 'r-3', why: 'w' }, childAgent(childOf('r-1')))
|
|
380
|
-
assert(nudgeBad.ok === false && nudgeBad.code === 'V5_NOT_ACADEMICIAN', 'a researcher CANNOT nudge (V5_NOT_ACADEMICIAN)')
|
|
381
|
-
const prioBad = await callTool('vibe_v5_prioritize', { order: [{ task_id: asg.task.id, priority: 5 }], why: 'x' }, childAgent(childOf('r-1')))
|
|
382
|
-
assert(prioBad.ok === false && prioBad.code === 'V5_NOT_ACADEMICIAN', 'a researcher CANNOT set priorities')
|
|
383
|
-
const prioOk = await callTool('vibe_v5_prioritize', { order: [{ task_id: asg.task.id, priority: 5 }], why: '先做这个' }, childAgent(childOf('acad')))
|
|
384
|
-
assert(prioOk.ok === true, 'the academician CAN set priorities')
|
|
385
|
-
const ov = await callTool('vibe_v5_overview', {}, childAgent(childOf('acad')))
|
|
386
|
-
assert(ov.ok === true && /编制/.test(ov.overview) && /任务板/.test(ov.overview), 'the academician CAN get the institute-wide overview')
|
|
387
|
-
|
|
388
|
-
// an assignment objection is broadcast (not silently swallowed)
|
|
389
|
-
fireEnd(childOf('r-2'), { reject_assign: { task_id: asg.task.id, why: '我手上有更紧急的方向' }, contextPct: 20 })
|
|
390
|
-
await settleAll()
|
|
391
|
-
const objectionDelivered = wakes.some(w => JSON.stringify(w.blocks).includes('反对分派'))
|
|
392
|
-
assert(objectionDelivered, 'an assignment objection is broadcast into the institute mail (not dropped)')
|
|
393
|
-
|
|
394
|
-
// ---------- task board CAS ----------
|
|
395
|
-
const t1 = await callTool('vibe_v5_task_create', { subject: '整理已知特例', description: 'n=3,4,5 的已知结论', write_scopes: ['Members/r-3/Propos'] }, childAgent(childOf('r-3')))
|
|
396
|
-
assert(t1.ok === true && t1.task.revision === 1, 'a member can open a task (revision 1)')
|
|
397
|
-
const stale = await callTool('vibe_v5_task_update', { task_id: t1.task.id, expected_revision: 99, action: 'claim' }, childAgent(childOf('r-3')))
|
|
398
|
-
assert(stale.ok === false && stale.code === 'V5_TASK_STALE_REVISION', 'a stale revision is refused (compare-and-set)')
|
|
399
|
-
const claimed = await callTool('vibe_v5_task_update', { task_id: t1.task.id, expected_revision: 1, action: 'claim' }, childAgent(childOf('r-3')))
|
|
400
|
-
assert(claimed.ok === true && claimed.task.ownerId === 'r-3', 'claiming sets the owner')
|
|
401
|
-
const blocked = await callTool('vibe_v5_task_create', { subject: '依赖前一个', description: 'd', blocked_by: [t1.task.id] }, childAgent(childOf('r-3')))
|
|
402
|
-
assert(blocked.ok === true && blocked.task.ready === false, 'a task with an incomplete blocker is not ready')
|
|
403
|
-
const claimBlocked = await callTool('vibe_v5_task_update', { task_id: blocked.task.id, expected_revision: 1, action: 'claim' }, childAgent(childOf('r-1')))
|
|
404
|
-
assert(claimBlocked.ok === false && claimBlocked.code === 'V5_TASK_BLOCKED', 'a blocked task cannot be claimed')
|
|
405
|
-
const cyc = await callTool('vibe_v5_task_update', { task_id: t1.task.id, expected_revision: 2, action: 'set_dependencies', blocked_by: [blocked.task.id] }, childAgent(childOf('r-3')))
|
|
406
|
-
assert(cyc.ok === false && cyc.code === 'V5_TASK_DEPENDENCY_CYCLE', 'a dependency cycle is refused')
|
|
407
|
-
|
|
408
|
-
// ---------- firing a temp worker is real ----------
|
|
409
|
-
const beforeFire = (await callTool('vibe_v5_status', {})).members.map(m => m.id)
|
|
410
|
-
const fireRes = await callTool('vibe_v5_fire', { id: hired.id, reason: '任务已完成' }, childAgent(childOf('r-1')))
|
|
411
|
-
assert(fireRes.ok === true && fireRes.dismissed === hired.id, 'the hirer CAN fire its own temp worker')
|
|
412
|
-
assert(interrupts.indexOf(childOf(hired.id)) !== -1, 'firing INTERRUPTS the temp worker\'s current turn')
|
|
413
|
-
assert(drains.indexOf(childOf(hired.id)) !== -1, 'firing RELEASES its resident continuable child (real release, not just a flag)')
|
|
414
|
-
const afterFire = (await callTool('vibe_v5_status', {})).members.find(m => m.id === hired.id)
|
|
415
|
-
assert(afterFire && afterFire.phase === 'dismissed', 'the dismissed member is marked dismissed')
|
|
416
|
-
const reHire = await callTool('vibe_v5_hire', { purpose: '新的核对任务', initial_task: '核对第 4 节' }, childAgent(childOf('r-1')))
|
|
417
|
-
assert(reHire.ok === true && reHire.id !== hired.id, 'a re-hire gets a NEW id (ids are never reused): ' + hired.id + ' -> ' + reHire.id)
|
|
418
|
-
await settleAllSpawns() // the new temp's founding turn must complete, or it blocks a meeting as a busy ghost
|
|
419
|
-
const fireForeign = await callTool('vibe_v5_fire', { id: reHire.id, reason: 'x' }, childAgent(childOf('r-3')))
|
|
420
|
-
assert(fireForeign.ok === false, 'a researcher CANNOT fire someone else\'s temp worker')
|
|
421
|
-
|
|
422
|
-
// ---------- human-readable mirrors are write-only snapshots ----------
|
|
423
|
-
const rosterMirror = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Institutes.md')
|
|
424
|
-
assert(existsSync(rosterMirror), 'the human-readable roster mirror (Institutes.md) was written')
|
|
425
|
-
if (existsSync(rosterMirror)) {
|
|
426
|
-
const rosterText = readFileSync(rosterMirror, 'utf8')
|
|
427
|
-
assert(rosterText.includes(hired.id) && /已除名/.test(rosterText), 'the roster mirror lists the dismissed member under 已除名 (ids are never reused)')
|
|
428
|
-
assert(new RegExp('m = ' + stT.quorum.m).test(rosterText), 'the roster mirror states the live quorum m')
|
|
429
|
-
}
|
|
430
|
-
const boardMirror = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Shared', 'TaskBoard.md')
|
|
431
|
-
assert(existsSync(boardMirror), 'the human-readable task board mirror (Shared/TaskBoard.md) was written')
|
|
432
|
-
assert(existsSync(join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'State', 'README.md')),
|
|
433
|
-
'State/ carries a README stating that the authoritative state is the session-log projection')
|
|
434
|
-
|
|
435
|
-
// ---------- the institute stops only on a unanimous solve vote ----------
|
|
436
|
-
solvePlan = false
|
|
437
|
-
const mtg = await callTool('vibe_v5_meeting', { agenda: '是否已解决原问题?', kind: 'solve-vote' }, childAgent(childOf('acad')))
|
|
438
|
-
assert(mtg.ok === true, 'the academician can convene a meeting')
|
|
439
|
-
await drainWakes()
|
|
440
|
-
let sNo = await callTool('vibe_v5_status', {})
|
|
441
|
-
assert(sNo.autoDone === false && sNo.running === true, 'a non-unanimous solve vote does NOT stop the institute')
|
|
442
|
-
|
|
443
|
-
solvePlan = true
|
|
444
|
-
const m2 = await callTool('vibe_v5_meeting', { agenda: '再次表决是否已解决', kind: 'solve-vote' }, childAgent(childOf('acad')))
|
|
445
|
-
assert(m2.ok === true, 'the second solve-vote meeting is requested (' + JSON.stringify(m2).slice(0, 100) + ')')
|
|
446
|
-
for (let i = 0; i < 60; i++) {
|
|
447
|
-
const s = await callTool('vibe_v5_status', {})
|
|
448
|
-
if (s.autoDone) break
|
|
449
|
-
await drainWakes(6)
|
|
450
|
-
await sleep(15)
|
|
451
|
-
}
|
|
452
|
-
const sDone = await callTool('vibe_v5_status', {})
|
|
453
|
-
assert(sDone.autoDone === true, 'a unanimous solve vote from every voting member DOES stop the institute — ' +
|
|
454
|
-
JSON.stringify({ autoDone: sDone.autoDone, running: sDone.running, meeting: sDone.meeting, parked: sDone.parkedMeeting, solveVotes: sDone.solveVotes }))
|
|
455
|
-
assert(sDone.running === false, 'scheduling halted after the unanimous solve vote')
|
|
456
|
-
const concl = existsSync(join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Problems', 'conclusion.md'))
|
|
457
|
-
assert(concl, 'a conclusion record was written on completion')
|
|
458
|
-
|
|
459
|
-
// ---------- resume must not resurrect a concluded institute ----------
|
|
460
|
-
const res = await callTool('vibe_v5_resume', {})
|
|
461
|
-
assert(res.ok === false, 'resume refuses to resurrect a concluded institute')
|
|
462
|
-
|
|
463
|
-
// ---------- /v5 command is registered ----------
|
|
464
|
-
assert(commandRegs.some(c => c.name === 'v5'), 'the /v5 slash command is registered')
|
|
465
|
-
|
|
466
|
-
console.log('')
|
|
467
|
-
console.log('passed=' + passed + ' failed=' + failed)
|
|
468
|
-
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
469
|
-
console.log('ALL GREEN')
|
|
470
|
-
process.exit(0)
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Vibe-Math-V5 SELF-DRIVING TEST — drives the REAL vibe-math-v5 plugin with a mock
|
|
3
|
+
// host, including a faithful mock of the HOST-ONLY session projection unit it relies
|
|
4
|
+
// on (register / eager fold on append / stateOf), so the projection storage path is
|
|
5
|
+
// actually exercised rather than stubbed out.
|
|
6
|
+
//
|
|
7
|
+
// Run: node tests/selfdrive-v5.mjs (temp workspace; assertions; non-zero exit on failure)
|
|
8
|
+
// ============================================================
|
|
9
|
+
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
11
|
+
import { join, dirname, isAbsolute } from 'node:path'
|
|
12
|
+
|
|
13
|
+
// V5_PLUGIN lets a sensitivity probe point this suite at a deliberately broken copy.
|
|
14
|
+
const PLUGIN = process.env.V5_PLUGIN
|
|
15
|
+
? new URL('file:///' + String(process.env.V5_PLUGIN).replace(/\\/g, '/'))
|
|
16
|
+
: new URL('../vibe-math-v5/vibe-math-v5.js', import.meta.url)
|
|
17
|
+
const WS = mkdtempSync(join(tmpdir(), 'vibe-v5-'))
|
|
18
|
+
let passed = 0, failed = 0
|
|
19
|
+
const failures = []
|
|
20
|
+
const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
|
|
21
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
22
|
+
|
|
23
|
+
// ---------- mock host: projection registry ----------
|
|
24
|
+
// Mirrors the contract the plugin depends on: register returns a disposer, every
|
|
25
|
+
// committed event is folded EAGERLY into every unit's cell, stateOf materialises a
|
|
26
|
+
// cell lazily with init().
|
|
27
|
+
function makeProjectionRegistry() {
|
|
28
|
+
const units = new Map()
|
|
29
|
+
const cells = new Map()
|
|
30
|
+
function cellMap(sess) {
|
|
31
|
+
const id = String(sess.id)
|
|
32
|
+
let m = cells.get(id)
|
|
33
|
+
if (!m) { m = new Map(); cells.set(id, m) }
|
|
34
|
+
return m
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
register(def) {
|
|
38
|
+
if (!def || typeof def.key !== 'string') throw new Error('projection: key required')
|
|
39
|
+
units.set(def.key, def)
|
|
40
|
+
return () => { units.delete(def.key) }
|
|
41
|
+
},
|
|
42
|
+
stateOf(session, key) {
|
|
43
|
+
const def = units.get(key)
|
|
44
|
+
if (!def) return undefined
|
|
45
|
+
const m = cellMap(session)
|
|
46
|
+
if (!m.has(key)) m.set(key, def.init(session.header, session.inheritedEventCount || 0))
|
|
47
|
+
return m.get(key)
|
|
48
|
+
},
|
|
49
|
+
checkpoint(session) {
|
|
50
|
+
const out = {}
|
|
51
|
+
const m = cellMap(session)
|
|
52
|
+
for (const [k, def] of units) out[k] = { ver: def.stateVersion, seq: session.seq, val: m.get(k) }
|
|
53
|
+
return out
|
|
54
|
+
},
|
|
55
|
+
_drive(session, event) {
|
|
56
|
+
const m = cellMap(session)
|
|
57
|
+
for (const [k, def] of units) {
|
|
58
|
+
const cur = m.has(k) ? m.get(k) : def.init(session.header, session.inheritedEventCount || 0)
|
|
59
|
+
let next
|
|
60
|
+
try { next = def.apply(cur, event) } catch (e) { next = cur }
|
|
61
|
+
m.set(k, next)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
_units: units,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const projections = makeProjectionRegistry()
|
|
69
|
+
const listeners = {}
|
|
70
|
+
const toolRegs = []
|
|
71
|
+
const commandRegs = []
|
|
72
|
+
const spawns = []
|
|
73
|
+
const wakes = []
|
|
74
|
+
const interrupts = []
|
|
75
|
+
const drains = []
|
|
76
|
+
const liveAgents = new Map()
|
|
77
|
+
|
|
78
|
+
function makeMockSession(id, parentSession) {
|
|
79
|
+
const events = []
|
|
80
|
+
const s = {
|
|
81
|
+
id,
|
|
82
|
+
header: { version: 1, id, createdAt: Date.now(), cwd: WS, parentSession, isSeeded: false },
|
|
83
|
+
inheritedEventCount: 0,
|
|
84
|
+
get seq() { return events.length },
|
|
85
|
+
append(type, data) {
|
|
86
|
+
const ev = { type, data, seq: events.length, time: Date.now() }
|
|
87
|
+
events.push(ev)
|
|
88
|
+
projections._drive(s, ev)
|
|
89
|
+
return ev
|
|
90
|
+
},
|
|
91
|
+
deriveMessages() { return [] }, // non-surface events must never become messages
|
|
92
|
+
snapshotEvents(from) { return events.slice(from || 0) },
|
|
93
|
+
ownEvents() { return events.slice() },
|
|
94
|
+
_events: events,
|
|
95
|
+
}
|
|
96
|
+
return s
|
|
97
|
+
}
|
|
98
|
+
const ROOT_SESSION = makeMockSession('sess-A', undefined)
|
|
99
|
+
const ROOT = { id: 'sess-A', options: { provider: 'mock', model: 'm' }, session: ROOT_SESSION, ctx: undefined }
|
|
100
|
+
|
|
101
|
+
const ctx = {
|
|
102
|
+
get(name) {
|
|
103
|
+
if (name === 'sessionProjections') return projections
|
|
104
|
+
if (name === 'sandboxPolicy') return undefined
|
|
105
|
+
if (name === 'compaction') return undefined
|
|
106
|
+
if (name === 'subprocess') {
|
|
107
|
+
return {
|
|
108
|
+
async spawn({ argv }) {
|
|
109
|
+
const script = argv[argv.length - 1] || ''
|
|
110
|
+
if (/New-Item/.test(script)) {
|
|
111
|
+
const paths = []
|
|
112
|
+
const re = /'((?:[^']|'')*)'/g
|
|
113
|
+
let m
|
|
114
|
+
while ((m = re.exec(script)) !== null) paths.push(m[1].replace(/''/g, "'"))
|
|
115
|
+
for (const p of paths) if (p && !/^-/.test(p)) mkdirSync(p, { recursive: true })
|
|
116
|
+
}
|
|
117
|
+
return { done: Promise.resolve({ exitCode: 0 }) }
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return undefined
|
|
122
|
+
},
|
|
123
|
+
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
124
|
+
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
125
|
+
logger: { info() {}, warn() {}, error() {} },
|
|
126
|
+
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
127
|
+
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
128
|
+
commands: { register(spec) { commandRegs.push(spec); return () => {} } },
|
|
129
|
+
sessions: { async flush() { return true } },
|
|
130
|
+
subagents: {
|
|
131
|
+
list() { return ['spawn'] },
|
|
132
|
+
async startContinuable({ label, request }) {
|
|
133
|
+
const id = 'c' + (spawns.length + 1)
|
|
134
|
+
const childSession = makeMockSession(id, 'sess-A')
|
|
135
|
+
const agent = { id, session: childSession, options: request && request.agentOptions }
|
|
136
|
+
liveAgents.set(id, agent)
|
|
137
|
+
spawns.push({ label, request, childId: id, persona: request && request.persona, toolFilter: request && request.toolFilter, ended: false })
|
|
138
|
+
return { childId: id, messageId: 'm' + spawns.length }
|
|
139
|
+
},
|
|
140
|
+
async sendMessage(parent, childId, blocks, opts) {
|
|
141
|
+
wakes.push({ childId, blocks })
|
|
142
|
+
return 'w' + wakes.length
|
|
143
|
+
},
|
|
144
|
+
interrupt(childId) { interrupts.push(childId) },
|
|
145
|
+
async drainContinuableChildren(parent, ids) { drains.push(...ids); for (const i of ids) liveAgents.delete(i) },
|
|
146
|
+
},
|
|
147
|
+
agents: {
|
|
148
|
+
roots() { return [ROOT] },
|
|
149
|
+
get(id) { return id === 'sess-A' ? ROOT : liveAgents.get(id) },
|
|
150
|
+
list() { return [ROOT, ...liveAgents.values()] },
|
|
151
|
+
},
|
|
152
|
+
fs: {
|
|
153
|
+
async resolve(rel, opts) {
|
|
154
|
+
const b = (opts && opts.cwd) || WS
|
|
155
|
+
const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/'))
|
|
156
|
+
return { targetKey: p, displayPath: p }
|
|
157
|
+
},
|
|
158
|
+
async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
|
|
159
|
+
async readText(t) { return readFileSync(t.targetKey, 'utf8') },
|
|
160
|
+
async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
|
|
161
|
+
async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const mod = await import(PLUGIN.href + '?t=' + Date.now())
|
|
166
|
+
const plugin = mod.default || mod
|
|
167
|
+
plugin.apply(ctx)
|
|
168
|
+
|
|
169
|
+
async function callTool(name, args, agent) {
|
|
170
|
+
const spec = toolRegs.find(x => x.name === name)
|
|
171
|
+
if (!spec) throw new Error('no tool ' + name)
|
|
172
|
+
return JSON.parse(await spec.execute(args || {}, { agent: agent || ROOT }))
|
|
173
|
+
}
|
|
174
|
+
const childAgent = (childId) => liveAgents.get(childId) || { id: childId, session: { header: { parentSession: 'sess-A' } } }
|
|
175
|
+
function fireEnd(childId, reply, stopReason) {
|
|
176
|
+
const blocks = reply === undefined ? [] : [{ type: 'text', text: '```json\n' + JSON.stringify(reply) + '\n```' }]
|
|
177
|
+
for (const h of (listeners['subagent/end'] || [])) {
|
|
178
|
+
h({ id: childId, runId: 'r', provider: 'spawn', local: true, stopReason: stopReason || 'completed', lastAssistantMessage: blocks })
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const settleAll = async () => { await sleep(30) }
|
|
182
|
+
const spawnOf = (memberId) => spawns.find(s => s.label.indexOf('vibe5 ' + memberId + ' ') !== -1)
|
|
183
|
+
const childOf = (memberId) => { const s = spawnOf(memberId); return s ? s.childId : memberId }
|
|
184
|
+
const wakeKindOf = (prompt) => {
|
|
185
|
+
if (/【入职首轮/.test(prompt)) return 'initial'
|
|
186
|
+
if (/【研究所会议/.test(prompt)) return 'meeting'
|
|
187
|
+
if (/【求真表决/.test(prompt)) return 'verify'
|
|
188
|
+
if (/【心跳检查/.test(prompt)) return 'checkpoint'
|
|
189
|
+
return 'normal'
|
|
190
|
+
}
|
|
191
|
+
const memberLabelOf = (childId) => { const s = spawns.find(x => x.childId === childId); const m = s ? /vibe5 (\S+) /.exec(s.label) : null; return m ? m[1] : '' }
|
|
192
|
+
|
|
193
|
+
// A planned-vote table lets the driver answer verification prompts realistically.
|
|
194
|
+
let plannedVotes = null
|
|
195
|
+
let verifyProposals = []
|
|
196
|
+
async function settleSpawn(sp) {
|
|
197
|
+
if (sp.ended) return
|
|
198
|
+
sp.ended = true
|
|
199
|
+
const who = memberLabelOf(sp.childId)
|
|
200
|
+
if (who && !/^t-/.test(who)) {
|
|
201
|
+
await callTool('vibe_v5_record_proposition', {
|
|
202
|
+
id: 'p-' + who, title: '引理 ' + who, statement: '设 n>2 且存在整数解 x^n+y^n=z^n,则可由最小反例归约得矛盾。',
|
|
203
|
+
value: 0.6, motive: '用于反证原命题', p: 0.8,
|
|
204
|
+
}, childAgent(sp.childId))
|
|
205
|
+
}
|
|
206
|
+
fireEnd(sp.childId, { progress: who + ':初始见解——先做最小反例归约,再处理边界情形。', solved: false, contextPct: 10 })
|
|
207
|
+
await settleAll()
|
|
208
|
+
}
|
|
209
|
+
async function settleAllSpawns() { for (const sp of spawns.slice()) await settleSpawn(sp) }
|
|
210
|
+
|
|
211
|
+
// Answer ONE queued wake, according to the prompt's actual kind.
|
|
212
|
+
async function answerWake(w) {
|
|
213
|
+
const prompt = (w.blocks && w.blocks[0] && w.blocks[0].text) || ''
|
|
214
|
+
const kind = wakeKindOf(prompt)
|
|
215
|
+
const who = memberLabelOf(w.childId)
|
|
216
|
+
let reply
|
|
217
|
+
if (kind === 'verify') {
|
|
218
|
+
// Read the target from the JSON spec line the prompt ends with — parsing the
|
|
219
|
+
// Chinese prose line would swallow the full-width colon into the id.
|
|
220
|
+
const tm = /"target"\s*:\s*"([^"]+)"/.exec(prompt)
|
|
221
|
+
const target = tm ? tm[1] : (verifyProposals[verifyProposals.length - 1] || 'p-r-1')
|
|
222
|
+
const v = plannedVotes && plannedVotes.has(who) ? plannedVotes.get(who) : 0.5
|
|
223
|
+
reply = { verdict: { target, verdict: v, reason: who + ' 的判断:' + (v === 1 ? '成立' : v === 0 ? '不成立' : '不确定') }, contextPct: 20 }
|
|
224
|
+
} else if (kind === 'meeting') {
|
|
225
|
+
reply = { input: who + ':我的意见已写在 Progress/ 里。', vote_solved: solvePlan === true, solved: solvePlan === true, contextPct: 20 }
|
|
226
|
+
} else {
|
|
227
|
+
reply = { progress: who + ':本轮继续推进最小反例路线。', solved: false, contextPct: 20 }
|
|
228
|
+
}
|
|
229
|
+
fireEnd(w.childId, reply)
|
|
230
|
+
await settleAll()
|
|
231
|
+
return kind
|
|
232
|
+
}
|
|
233
|
+
// Drive every queued wake: answer by the prompt's actual kind.
|
|
234
|
+
async function drainWakes(budget = 40) {
|
|
235
|
+
let n = 0
|
|
236
|
+
while (wakes.length && n < budget) { await answerWake(wakes.shift()); n++ }
|
|
237
|
+
return n
|
|
238
|
+
}
|
|
239
|
+
let solvePlan = null
|
|
240
|
+
|
|
241
|
+
// Answer exactly ONE voting round: keep pulling wakes (heartbeats and work prompts also
|
|
242
|
+
// sit in the queue) until `n` VERIFY prompts have been answered, then stop.
|
|
243
|
+
//
|
|
244
|
+
// A fixed `drainWakes(n)` budget is not enough — it can stop before every voter has
|
|
245
|
+
// answered, and an assertion like "this must not be verified yet" then passes vacuously
|
|
246
|
+
// however badly the quorum rule is broken. (Proven: weakening the m floor left this suite
|
|
247
|
+
// GREEN.) Equally, a helper that loops until the verification CLOSES would silently burn
|
|
248
|
+
// through every debate round, so this stops the moment the last voter of one round has
|
|
249
|
+
// answered.
|
|
250
|
+
async function drainVerifyRound(n) {
|
|
251
|
+
let answered = 0
|
|
252
|
+
for (let guard = 0; guard < n * 6 && answered < n; guard++) {
|
|
253
|
+
const idx = wakes.findIndex(w => wakeKindOf((w.blocks && w.blocks[0] && w.blocks[0].text) || '') === 'verify')
|
|
254
|
+
if (idx === -1) {
|
|
255
|
+
if (!wakes.length) break
|
|
256
|
+
await answerWake(wakes.shift())
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
const w = wakes.splice(idx, 1)[0]
|
|
260
|
+
await answerWake(w)
|
|
261
|
+
answered++
|
|
262
|
+
}
|
|
263
|
+
return answered
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================================
|
|
267
|
+
console.log('-- V5 self-drive --')
|
|
268
|
+
|
|
269
|
+
// ---------- founding ----------
|
|
270
|
+
const started = await callTool('vibe_v5_start', { problem: '证明:不存在整数解 x^n + y^n = z^n(n>2)的初等情形', researcherCount: 3 })
|
|
271
|
+
assert(started.ok === true, 'vibe_v5_start ok (' + JSON.stringify(started).slice(0, 150) + ')')
|
|
272
|
+
assert(spawns.length === 4, 'founded 1 academician + 3 researchers (got ' + spawns.length + ')')
|
|
273
|
+
assert(!!spawnOf('acad'), 'academician acad exists')
|
|
274
|
+
assert(['r-1', 'r-2', 'r-3'].every(r => !!spawnOf(r)), '3 permanent researchers r-1..r-3')
|
|
275
|
+
assert(spawns.every(s => typeof s.persona === 'string' && s.persona.length > 2000), 'every member got the full charter as its persona')
|
|
276
|
+
assert(spawns[0].persona.indexOf('progress.md') !== -1 && spawns[0].persona.indexOf('它的用途') !== -1,
|
|
277
|
+
'charter carries the progress definition AND its purpose explanation')
|
|
278
|
+
assert(spawns[0].persona.indexOf('分派') !== -1 && spawns[0].persona.indexOf('边界') !== -1,
|
|
279
|
+
'charter describes the academician as the organizational centre (duties + four boundaries)')
|
|
280
|
+
assert(spawns[0].persona.indexOf('m = 3') !== -1 || /至少有 m = \d+ 名有表决权者/.test(spawns[0].persona), 'charter states the m-vote rule with the live m')
|
|
281
|
+
|
|
282
|
+
const st0 = await callTool('vibe_v5_status', {})
|
|
283
|
+
assert(st0.backend === 'projection', 'durable state uses the session-projections backend (got ' + st0.backend + ')')
|
|
284
|
+
assert(st0.members.length === 4 && st0.members.filter(m => m.kind === 'researcher').length === 3, 'status roster is 1 academician + 3 researchers')
|
|
285
|
+
assert(ROOT_SESSION._events.some(e => String(e.type).indexOf('vibe5/') === 0), 'institute events are appended to the session log')
|
|
286
|
+
assert(ROOT_SESSION.deriveMessages().length === 0, 'institute events never enter the model history (zero context cost)')
|
|
287
|
+
assert(st0.quorum.m === 3 && st0.quorum.voterCount === 4, 'm = min(quorumCap=3, P=4) = 3 (got ' + st0.quorum.m + ')')
|
|
288
|
+
assert(st0.members.every(m => m.busy === true), 'every member is marked in-flight while its founding turn runs')
|
|
289
|
+
|
|
290
|
+
// founding turns complete
|
|
291
|
+
await settleAllSpawns()
|
|
292
|
+
let st1 = await callTool('vibe_v5_status', {})
|
|
293
|
+
assert(st1.members.every(m => m.busy === false), 'founding turns were processed (no member left marked busy)')
|
|
294
|
+
assert(st1.members.every(m => m.rounds >= 1), 'each member completed its founding round')
|
|
295
|
+
const propWritten = existsSync(join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Members', 'r-1', 'Propos', 'p-r-1.md'))
|
|
296
|
+
assert(propWritten, 'per-member proposition library file was written (Members/r-1/Propos/p-r-1.md)')
|
|
297
|
+
const progWritten = existsSync(join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Members', 'r-1', 'Progress', 'progress.md'))
|
|
298
|
+
assert(progWritten, 'per-member progress.md was written')
|
|
299
|
+
|
|
300
|
+
// missing-mandatory-fields refusal (the charter's three hard requirements)
|
|
301
|
+
const badRec = await callTool('vibe_v5_record_proposition', { statement: 'x', value: 0.5 }, childAgent(childOf('r-2')))
|
|
302
|
+
assert(badRec.ok === false && badRec.code === 'V5_INVALID_ARGUMENT', 'recording without motive/p is refused (three hard requirements enforced)')
|
|
303
|
+
|
|
304
|
+
// ---------- group chat fan-out / DM ----------
|
|
305
|
+
const sayR1 = await callTool('vibe_v5_say', { text: '各位,我建议先做最小反例归约。' }, childAgent(childOf('r-1')))
|
|
306
|
+
assert(sayR1.ok === true && sayR1.delivered === 3, 'group chat fans out to every OTHER active member (4 active - sender = 3; got ' + sayR1.delivered + ')')
|
|
307
|
+
const sayDM = await callTool('vibe_v5_say', { to: 'r-2', text: '私下问你一下。' }, childAgent(childOf('r-1')))
|
|
308
|
+
assert(sayDM.ok === true && sayDM.delivered === 1, 'a private message reaches exactly one member')
|
|
309
|
+
const selfMsg = await callTool('vibe_v5_say', { to: 'r-1', text: 'x' }, childAgent(childOf('r-1')))
|
|
310
|
+
assert(selfMsg.ok === false && selfMsg.code === 'V5_SELF_MESSAGE', 'messaging yourself is refused')
|
|
311
|
+
|
|
312
|
+
// ---------- temp workers ----------
|
|
313
|
+
const hired = await callTool('vibe_v5_hire', { purpose: '核对文献中的引理', initial_task: '核对第 3 节引理是否成立' }, childAgent(childOf('r-1')))
|
|
314
|
+
assert(hired.ok === true && /^t-\d+$/.test(hired.id || ''), 'a permanent researcher CAN hire a temp worker (got ' + JSON.stringify(hired).slice(0, 80) + ')')
|
|
315
|
+
await settleAllSpawns()
|
|
316
|
+
const tempHire = await callTool('vibe_v5_hire', { purpose: 'x', initial_task: 'y' }, childAgent(childOf(hired.id)))
|
|
317
|
+
assert(tempHire.ok === false && tempHire.code === 'V5_NOT_VOTER', 'a temp worker CANNOT hire (V5_NOT_VOTER)')
|
|
318
|
+
const tempVote = await callTool('vibe_v5_verdict', { target: 'p-r-2', verdict: 1, reason: '我觉得对' }, childAgent(childOf(hired.id)))
|
|
319
|
+
assert(tempVote.ok === false && tempVote.code === 'V5_NOT_VOTER', 'a temp worker CANNOT vote (V5_NOT_VOTER)')
|
|
320
|
+
const stT = await callTool('vibe_v5_status', {})
|
|
321
|
+
assert(stT.quorum.voterCount === 4 && stT.quorum.m === 3, 'the temp worker did NOT change the voter count or m')
|
|
322
|
+
|
|
323
|
+
// ---------- verification: the m-quorum boolean rule ----------
|
|
324
|
+
const target = 'p-r-1'
|
|
325
|
+
verifyProposals.push(target)
|
|
326
|
+
const prop = await callTool('vibe_v5_propose_verify', { target, kind: 'proposition', reason: '我已在库里给出完整证明' }, childAgent(childOf('r-1')))
|
|
327
|
+
assert(prop.ok === true, 'proposing verification is accepted')
|
|
328
|
+
// The proposal itself must kick the scheduler, so the object starts verifying without
|
|
329
|
+
// needing any unrelated event to drive a pass.
|
|
330
|
+
let sv = await callTool('vibe_v5_status', {})
|
|
331
|
+
assert(!!sv.verify, 'verification of ' + target + ' is in flight right after proposing (nobody has voted yet) — got ' +
|
|
332
|
+
JSON.stringify({ verify: sv.verify, queue: sv.verifyQueue, undecided: sv.undecided, running: sv.running, autoDone: sv.autoDone, phase: sv.phase, meeting: sv.meeting, parked: sv.parkedMeeting, tasks: sv.tasks.length }))
|
|
333
|
+
if (sv.verify) {
|
|
334
|
+
assert(sv.verify.m === 3 && sv.verify.P === 4, 'verification reports m=3 over P=4 voters')
|
|
335
|
+
|
|
336
|
+
// Round: only ONE boolean vote (< m) must NOT verify.
|
|
337
|
+
plannedVotes = new Map([['acad', 0.9], ['r-1', 1], ['r-2', 0.5], ['r-3', 0.7]])
|
|
338
|
+
assert(await drainVerifyRound(4) === 4, 'all four voters answered round 1')
|
|
339
|
+
await settleAll()
|
|
340
|
+
let sA = await callTool('vibe_v5_status', {})
|
|
341
|
+
assert(sA.verified.indexOf(target) === -1, 'a single boolean vote (m-1=2 short) does NOT verify')
|
|
342
|
+
assert(!!sA.verify, 'the verification is still open (abstentions do not settle it)')
|
|
343
|
+
assert(sA.verify && sA.verify.stage === 'debate', 'the round really completed and moved to the DEBATE stage (got ' + (sA.verify && sA.verify.stage) + ')')
|
|
344
|
+
|
|
345
|
+
// Debate round: 1 vs 0 conflict must NOT verify.
|
|
346
|
+
plannedVotes = new Map([['acad', 0], ['r-1', 1], ['r-2', 1], ['r-3', 1]])
|
|
347
|
+
assert(await drainVerifyRound(4) === 4, 'all four voters answered the debate round')
|
|
348
|
+
await settleAll()
|
|
349
|
+
let sB = await callTool('vibe_v5_status', {})
|
|
350
|
+
assert(sB.verified.indexOf(target) === -1, 'a conflicting 1/0 assertion BLOCKS the verdict (no minority override)')
|
|
351
|
+
assert(!!sB.verify, 'the verification survived the conflicting round (not silently forced)')
|
|
352
|
+
|
|
353
|
+
// Final: three unanimous TRUE votes (m=3) DO verify.
|
|
354
|
+
plannedVotes = new Map([['acad', 1], ['r-1', 1], ['r-2', 1], ['r-3', 1]])
|
|
355
|
+
await drainVerifyRound(4)
|
|
356
|
+
await settleAll()
|
|
357
|
+
let sC = await callTool('vibe_v5_status', {})
|
|
358
|
+
if (!sC.verified.includes(target)) { await drainWakes(8); sC = await callTool('vibe_v5_status', {}) }
|
|
359
|
+
assert(sC.verified.indexOf(target) !== -1, 'THREE unanimous TRUE votes (m=3) DID verify ' + target)
|
|
360
|
+
const verifiedCard = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', '命题', target + '.md')
|
|
361
|
+
assert(existsSync(verifiedCard), 'Verified/命题/' + target + '.md was written')
|
|
362
|
+
if (existsSync(verifiedCard)) {
|
|
363
|
+
const card = readFileSync(verifiedCard, 'utf8')
|
|
364
|
+
assert(/- 结论: 真/.test(card), 'the Verified card records the conclusion 真')
|
|
365
|
+
assert(/名有表决权者一致判真/.test(card) && /m=3/.test(card), 'the Verified card records the m-vote basis (not "unanimous of all")')
|
|
366
|
+
}
|
|
367
|
+
const srcCard = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Members', 'r-1', 'Propos', 'p-r-1.md')
|
|
368
|
+
assert(existsSync(srcCard) && /- 状态: 已验证·真/.test(readFileSync(srcCard, 'utf8')), 'the source card was rewritten to 已验证·真')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ---------- the academician's organization powers ----------
|
|
372
|
+
const asg = await callTool('vibe_v5_assign', { subject: '核验 n=3 的情形', to: 'r-2', why: '你在同余方向最强', acceptance: '给出完整的模 9 分析' }, childAgent(childOf('acad')))
|
|
373
|
+
assert(asg.ok === true && asg.task && asg.task.ownerId === 'r-2', 'the academician CAN assign a task to a member')
|
|
374
|
+
assert(!!asg.task.why && !!asg.task.acceptance, 'the assignment records WHY and the acceptance criteria')
|
|
375
|
+
const asgNoWhy = await callTool('vibe_v5_assign', { to: 'r-3', why: '', acceptance: 'a' }, childAgent(childOf('acad')))
|
|
376
|
+
assert(asgNoWhy.ok === false, 'an assignment without a reason is refused')
|
|
377
|
+
const asgByR1 = await callTool('vibe_v5_assign', { subject: 'x', to: 'r-3', why: 'w', acceptance: 'a' }, childAgent(childOf('r-1')))
|
|
378
|
+
assert(asgByR1.ok === false && asgByR1.code === 'V5_NOT_ACADEMICIAN', 'a researcher CANNOT assign (V5_NOT_ACADEMICIAN)')
|
|
379
|
+
const nudgeBad = await callTool('vibe_v5_nudge', { to: 'r-3', why: 'w' }, childAgent(childOf('r-1')))
|
|
380
|
+
assert(nudgeBad.ok === false && nudgeBad.code === 'V5_NOT_ACADEMICIAN', 'a researcher CANNOT nudge (V5_NOT_ACADEMICIAN)')
|
|
381
|
+
const prioBad = await callTool('vibe_v5_prioritize', { order: [{ task_id: asg.task.id, priority: 5 }], why: 'x' }, childAgent(childOf('r-1')))
|
|
382
|
+
assert(prioBad.ok === false && prioBad.code === 'V5_NOT_ACADEMICIAN', 'a researcher CANNOT set priorities')
|
|
383
|
+
const prioOk = await callTool('vibe_v5_prioritize', { order: [{ task_id: asg.task.id, priority: 5 }], why: '先做这个' }, childAgent(childOf('acad')))
|
|
384
|
+
assert(prioOk.ok === true, 'the academician CAN set priorities')
|
|
385
|
+
const ov = await callTool('vibe_v5_overview', {}, childAgent(childOf('acad')))
|
|
386
|
+
assert(ov.ok === true && /编制/.test(ov.overview) && /任务板/.test(ov.overview), 'the academician CAN get the institute-wide overview')
|
|
387
|
+
|
|
388
|
+
// an assignment objection is broadcast (not silently swallowed)
|
|
389
|
+
fireEnd(childOf('r-2'), { reject_assign: { task_id: asg.task.id, why: '我手上有更紧急的方向' }, contextPct: 20 })
|
|
390
|
+
await settleAll()
|
|
391
|
+
const objectionDelivered = wakes.some(w => JSON.stringify(w.blocks).includes('反对分派'))
|
|
392
|
+
assert(objectionDelivered, 'an assignment objection is broadcast into the institute mail (not dropped)')
|
|
393
|
+
|
|
394
|
+
// ---------- task board CAS ----------
|
|
395
|
+
const t1 = await callTool('vibe_v5_task_create', { subject: '整理已知特例', description: 'n=3,4,5 的已知结论', write_scopes: ['Members/r-3/Propos'] }, childAgent(childOf('r-3')))
|
|
396
|
+
assert(t1.ok === true && t1.task.revision === 1, 'a member can open a task (revision 1)')
|
|
397
|
+
const stale = await callTool('vibe_v5_task_update', { task_id: t1.task.id, expected_revision: 99, action: 'claim' }, childAgent(childOf('r-3')))
|
|
398
|
+
assert(stale.ok === false && stale.code === 'V5_TASK_STALE_REVISION', 'a stale revision is refused (compare-and-set)')
|
|
399
|
+
const claimed = await callTool('vibe_v5_task_update', { task_id: t1.task.id, expected_revision: 1, action: 'claim' }, childAgent(childOf('r-3')))
|
|
400
|
+
assert(claimed.ok === true && claimed.task.ownerId === 'r-3', 'claiming sets the owner')
|
|
401
|
+
const blocked = await callTool('vibe_v5_task_create', { subject: '依赖前一个', description: 'd', blocked_by: [t1.task.id] }, childAgent(childOf('r-3')))
|
|
402
|
+
assert(blocked.ok === true && blocked.task.ready === false, 'a task with an incomplete blocker is not ready')
|
|
403
|
+
const claimBlocked = await callTool('vibe_v5_task_update', { task_id: blocked.task.id, expected_revision: 1, action: 'claim' }, childAgent(childOf('r-1')))
|
|
404
|
+
assert(claimBlocked.ok === false && claimBlocked.code === 'V5_TASK_BLOCKED', 'a blocked task cannot be claimed')
|
|
405
|
+
const cyc = await callTool('vibe_v5_task_update', { task_id: t1.task.id, expected_revision: 2, action: 'set_dependencies', blocked_by: [blocked.task.id] }, childAgent(childOf('r-3')))
|
|
406
|
+
assert(cyc.ok === false && cyc.code === 'V5_TASK_DEPENDENCY_CYCLE', 'a dependency cycle is refused')
|
|
407
|
+
|
|
408
|
+
// ---------- firing a temp worker is real ----------
|
|
409
|
+
const beforeFire = (await callTool('vibe_v5_status', {})).members.map(m => m.id)
|
|
410
|
+
const fireRes = await callTool('vibe_v5_fire', { id: hired.id, reason: '任务已完成' }, childAgent(childOf('r-1')))
|
|
411
|
+
assert(fireRes.ok === true && fireRes.dismissed === hired.id, 'the hirer CAN fire its own temp worker')
|
|
412
|
+
assert(interrupts.indexOf(childOf(hired.id)) !== -1, 'firing INTERRUPTS the temp worker\'s current turn')
|
|
413
|
+
assert(drains.indexOf(childOf(hired.id)) !== -1, 'firing RELEASES its resident continuable child (real release, not just a flag)')
|
|
414
|
+
const afterFire = (await callTool('vibe_v5_status', {})).members.find(m => m.id === hired.id)
|
|
415
|
+
assert(afterFire && afterFire.phase === 'dismissed', 'the dismissed member is marked dismissed')
|
|
416
|
+
const reHire = await callTool('vibe_v5_hire', { purpose: '新的核对任务', initial_task: '核对第 4 节' }, childAgent(childOf('r-1')))
|
|
417
|
+
assert(reHire.ok === true && reHire.id !== hired.id, 'a re-hire gets a NEW id (ids are never reused): ' + hired.id + ' -> ' + reHire.id)
|
|
418
|
+
await settleAllSpawns() // the new temp's founding turn must complete, or it blocks a meeting as a busy ghost
|
|
419
|
+
const fireForeign = await callTool('vibe_v5_fire', { id: reHire.id, reason: 'x' }, childAgent(childOf('r-3')))
|
|
420
|
+
assert(fireForeign.ok === false, 'a researcher CANNOT fire someone else\'s temp worker')
|
|
421
|
+
|
|
422
|
+
// ---------- human-readable mirrors are write-only snapshots ----------
|
|
423
|
+
const rosterMirror = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Institutes.md')
|
|
424
|
+
assert(existsSync(rosterMirror), 'the human-readable roster mirror (Institutes.md) was written')
|
|
425
|
+
if (existsSync(rosterMirror)) {
|
|
426
|
+
const rosterText = readFileSync(rosterMirror, 'utf8')
|
|
427
|
+
assert(rosterText.includes(hired.id) && /已除名/.test(rosterText), 'the roster mirror lists the dismissed member under 已除名 (ids are never reused)')
|
|
428
|
+
assert(new RegExp('m = ' + stT.quorum.m).test(rosterText), 'the roster mirror states the live quorum m')
|
|
429
|
+
}
|
|
430
|
+
const boardMirror = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Shared', 'TaskBoard.md')
|
|
431
|
+
assert(existsSync(boardMirror), 'the human-readable task board mirror (Shared/TaskBoard.md) was written')
|
|
432
|
+
assert(existsSync(join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'State', 'README.md')),
|
|
433
|
+
'State/ carries a README stating that the authoritative state is the session-log projection')
|
|
434
|
+
|
|
435
|
+
// ---------- the institute stops only on a unanimous solve vote ----------
|
|
436
|
+
solvePlan = false
|
|
437
|
+
const mtg = await callTool('vibe_v5_meeting', { agenda: '是否已解决原问题?', kind: 'solve-vote' }, childAgent(childOf('acad')))
|
|
438
|
+
assert(mtg.ok === true, 'the academician can convene a meeting')
|
|
439
|
+
await drainWakes()
|
|
440
|
+
let sNo = await callTool('vibe_v5_status', {})
|
|
441
|
+
assert(sNo.autoDone === false && sNo.running === true, 'a non-unanimous solve vote does NOT stop the institute')
|
|
442
|
+
|
|
443
|
+
solvePlan = true
|
|
444
|
+
const m2 = await callTool('vibe_v5_meeting', { agenda: '再次表决是否已解决', kind: 'solve-vote' }, childAgent(childOf('acad')))
|
|
445
|
+
assert(m2.ok === true, 'the second solve-vote meeting is requested (' + JSON.stringify(m2).slice(0, 100) + ')')
|
|
446
|
+
for (let i = 0; i < 60; i++) {
|
|
447
|
+
const s = await callTool('vibe_v5_status', {})
|
|
448
|
+
if (s.autoDone) break
|
|
449
|
+
await drainWakes(6)
|
|
450
|
+
await sleep(15)
|
|
451
|
+
}
|
|
452
|
+
const sDone = await callTool('vibe_v5_status', {})
|
|
453
|
+
assert(sDone.autoDone === true, 'a unanimous solve vote from every voting member DOES stop the institute — ' +
|
|
454
|
+
JSON.stringify({ autoDone: sDone.autoDone, running: sDone.running, meeting: sDone.meeting, parked: sDone.parkedMeeting, solveVotes: sDone.solveVotes }))
|
|
455
|
+
assert(sDone.running === false, 'scheduling halted after the unanimous solve vote')
|
|
456
|
+
const concl = existsSync(join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Problems', 'conclusion.md'))
|
|
457
|
+
assert(concl, 'a conclusion record was written on completion')
|
|
458
|
+
|
|
459
|
+
// ---------- resume must not resurrect a concluded institute ----------
|
|
460
|
+
const res = await callTool('vibe_v5_resume', {})
|
|
461
|
+
assert(res.ok === false, 'resume refuses to resurrect a concluded institute')
|
|
462
|
+
|
|
463
|
+
// ---------- /v5 command is registered ----------
|
|
464
|
+
assert(commandRegs.some(c => c.name === 'v5'), 'the /v5 slash command is registered')
|
|
465
|
+
|
|
466
|
+
console.log('')
|
|
467
|
+
console.log('passed=' + passed + ' failed=' + failed)
|
|
468
|
+
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
469
|
+
console.log('ALL GREEN')
|
|
470
|
+
process.exit(0)
|