dsh-vibe-math 2.3.16 → 2.4.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.
- package/README.en.md +64 -61
- package/README.md +64 -61
- package/SECURITY.md +56 -0
- package/cordis.patch.yml +1123 -10
- package/docs/AUDIT-CHECKLIST.md +499 -384
- package/docs/COMPAT-AUDIT-ROUND2.md +3 -0
- package/docs/COMPAT-AUDIT-ROUND3.md +139 -0
- package/docs/generate_framework_diagram_v5.mjs +27 -27
- package/docs/release-notes/RELEASE-NOTES-2.4.0.md +172 -0
- package/docs/release-notes/RELEASE-NOTES-2.4.1.md +129 -0
- package/docs/test-timing.md +2 -1
- package/installer.js +89 -21
- package/package.json +36 -10
- package/preset-declaration.js +62 -0
- package/prompt-corpus-persona/persona-corpus.json +2 -2
- package/prompt-corpus-persona/persona-corpus.md +2 -2
- package/prompt-corpus-v5/prompt-corpus-v5.json +7 -7
- package/prompt-corpus-v5/prompt-corpus-v5.md +8 -129
- package/scripts/build-preset-rows.mjs +146 -0
- package/tests/audit-installer-compat.test.mjs +9 -3
- package/tests/audit-preset-rows.test.mjs +185 -0
- package/tests/audit-v5-integrity.mjs +49 -3
- package/tests/e2e-v5-round2.test.mjs +137 -67
- package/tests/formal-verify-v5.test.mjs +43 -35
- package/tests/prompt-v5-integrity.test.mjs +49 -47
- package/tests/selfdrive-v5.mjs +42 -55
- package/vibe-math-v2/agent.cordis.yml +5 -5
- package/vibe-math-v2/preset.yml +1 -1
- package/vibe-math-v2/vibe-math-v2.js +111 -64
- package/vibe-math-v3/vibe-math-v3.js +213 -150
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -2
- package/vibe-math-v4/vibe-math-v4.js +257 -34
- package/vibe-math-v5/agent.cordis.yml +8 -8
- package/vibe-math-v5/preset.yml +1 -1
- package/vibe-math-v5/vibe-math-v5.js +292 -129
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +56 -44
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +24 -22
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5-en.svg +14 -14
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +14 -14
|
@@ -35,29 +35,12 @@ const section = (t) => console.log('\n[' + t + ']')
|
|
|
35
35
|
// ---------------------------------------------------------------
|
|
36
36
|
// mock host
|
|
37
37
|
// ---------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const def = units.get(key); 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
|
-
_drive(session, event) {
|
|
50
|
-
const m = cellMap(session)
|
|
51
|
-
for (const [k, def] of units) {
|
|
52
|
-
const cur = m.has(k) ? m.get(k) : def.init(session.header, session.inheritedEventCount || 0)
|
|
53
|
-
let next; try { next = def.apply(cur, event) } catch (e) { next = cur }
|
|
54
|
-
m.set(k, next)
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const projections = makeProjectionRegistry()
|
|
38
|
+
// The host offers NO session services: v5 injects only
|
|
39
|
+
// ['subagents','agents','fs','tools','commands','timer'] and keeps its durable state in the
|
|
40
|
+
// hardened JSON file State/<institute>.v5state.json. Sessions still RECORD anything appended to
|
|
41
|
+
// them, so the suite can assert that the plugin appended nothing (an institute event in the log
|
|
42
|
+
// would make the user session unresumable — DSH refuses to load an unknown event type unless it
|
|
43
|
+
// is marked `ignorable`, which `Session.append` cannot set).
|
|
61
44
|
const listeners = {}
|
|
62
45
|
const toolRegs = []
|
|
63
46
|
const liveAgents = new Map()
|
|
@@ -107,14 +90,14 @@ let subprocess = {
|
|
|
107
90
|
},
|
|
108
91
|
}
|
|
109
92
|
|
|
110
|
-
function makeMockSession(id, parentSession) {
|
|
93
|
+
function makeMockSession(id, parentSession, cwd) {
|
|
111
94
|
const events = []
|
|
112
95
|
const s = {
|
|
113
96
|
id,
|
|
114
|
-
header: { version: 1, id, createdAt: Date.now(), cwd: WS, parentSession, isSeeded: false },
|
|
97
|
+
header: { version: 1, id, createdAt: Date.now(), cwd: cwd || WS, parentSession, isSeeded: false },
|
|
115
98
|
inheritedEventCount: 0,
|
|
116
99
|
get seq() { return events.length },
|
|
117
|
-
append(type, data) { const ev = { type, data, seq: events.length, time: Date.now() }; events.push(ev);
|
|
100
|
+
append(type, data) { const ev = { type, data, seq: events.length, time: Date.now() }; events.push(ev); return ev },
|
|
118
101
|
deriveMessages() { return [] },
|
|
119
102
|
snapshotEvents(from) { return events.slice(from || 0) },
|
|
120
103
|
ownEvents() { return events.slice() },
|
|
@@ -125,9 +108,14 @@ function makeMockSession(id, parentSession) {
|
|
|
125
108
|
|
|
126
109
|
const roots = new Map()
|
|
127
110
|
let rootSeq = 0
|
|
128
|
-
|
|
111
|
+
// Every case gets its OWN workspace. The institute's durable state is a file under the
|
|
112
|
+
// workspace (State/<institute>.v5state.json), so roots sharing one workspace would share one
|
|
113
|
+
// institute and let one case's state leak into the next. `ws` lets a case open a SECOND root
|
|
114
|
+
// over the same workspace, which is exactly what a reload looks like.
|
|
115
|
+
function makeRoot(ws) {
|
|
129
116
|
const id = 'sess-' + String.fromCharCode(65 + rootSeq++)
|
|
130
|
-
const
|
|
117
|
+
const cwd = ws || mkdtempSync(join(tmpdir(), 'vibe-v5-lean-'))
|
|
118
|
+
const session = makeMockSession(id, undefined, cwd)
|
|
131
119
|
const root = { id, options: { provider: 'mock', model: 'm' }, session, ctx: undefined }
|
|
132
120
|
roots.set(id, root)
|
|
133
121
|
return root
|
|
@@ -135,7 +123,6 @@ function makeRoot() {
|
|
|
135
123
|
|
|
136
124
|
const ctx = {
|
|
137
125
|
get(name) {
|
|
138
|
-
if (name === 'sessionProjections') return projections
|
|
139
126
|
if (name === 'sandboxPolicy') return undefined
|
|
140
127
|
if (name === 'compaction') return undefined
|
|
141
128
|
if (name === 'subprocess') return noSubprocess ? undefined : subprocess
|
|
@@ -147,13 +134,12 @@ const ctx = {
|
|
|
147
134
|
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
148
135
|
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
149
136
|
commands: { register() { return () => {} } },
|
|
150
|
-
sessions: { async flush() { return true } },
|
|
151
137
|
subagents: {
|
|
152
138
|
list() { return ['spawn'] },
|
|
153
139
|
async startContinuable({ label, request }) {
|
|
154
140
|
const rootId = (request && request.parent && request.parent.id) || 'sess-A'
|
|
155
141
|
const id = 'c' + (spawns.length + 1)
|
|
156
|
-
liveAgents.set(id, { id, session: makeMockSession(id, rootId), options: request && request.agentOptions })
|
|
142
|
+
liveAgents.set(id, { id, session: makeMockSession(id, rootId, (request && request.parent && request.parent.session && request.parent.session.header.cwd) || WS), options: request && request.agentOptions })
|
|
157
143
|
spawns.push({ label, childId: id, rootId, persona: request && request.persona, prompt: request && request.prompt && request.prompt[0] && request.prompt[0].text })
|
|
158
144
|
return { childId: id, messageId: 'm' + spawns.length }
|
|
159
145
|
},
|
|
@@ -272,8 +258,10 @@ async function wakeAndReply(root, memberId, reply, fromMember) {
|
|
|
272
258
|
return w
|
|
273
259
|
}
|
|
274
260
|
|
|
275
|
-
|
|
276
|
-
const
|
|
261
|
+
// Paths are per ROOT: each case's workspace holds its own institute.
|
|
262
|
+
const wsOf = (root) => root.session.header.cwd
|
|
263
|
+
const instRootOf = (root) => join(wsOf(root), 'VibeMath', 'Projects', 'default', 'Institutes', 'institute')
|
|
264
|
+
const vibeRootOf = (root) => join(wsOf(root), 'VibeMath')
|
|
277
265
|
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
278
266
|
// Every Lean tool mention in AGENT-FACING text must be the registered name (vibe_v5_lean_*).
|
|
279
267
|
// An abbreviated `lean_archive` is not a tool: an agent that copies it calls nothing.
|
|
@@ -287,6 +275,26 @@ section("1 'off' (default) is a true no-op")
|
|
|
287
275
|
const RA = makeRoot()
|
|
288
276
|
const st0 = await foundInstitute(RA, '形式化开关默认关闭测试')
|
|
289
277
|
assert(st0.params.formalVerify === 'off', "the default is 'off' (got " + st0.params.formalVerify + ')')
|
|
278
|
+
|
|
279
|
+
// ★ The persistence contract (the DSH 0.2.0 audit fix). v5 used to keep the institute in a
|
|
280
|
+
// host-only session projection: it appended `vibe5/*` events to the user's OWN session log, and
|
|
281
|
+
// DSH's session persistence then REFUSED to load that session (unknown event type without
|
|
282
|
+
// `ignorable: true`, which `Session.append` cannot set) — the user's session became unresumable.
|
|
283
|
+
// The hardened JSON file is now the only authority, so the log must stay empty AND the state must
|
|
284
|
+
// come back from the file in a brand-new root.
|
|
285
|
+
{
|
|
286
|
+
assert(RA.session._events.length === 0,
|
|
287
|
+
'★ the plugin appends NOTHING to the host session log (' + RA.session._events.length + ' events after founding): an institute event would make the session unresumable')
|
|
288
|
+
const stateFile = join(instRootOf(RA), 'State', 'institute.v5state.json')
|
|
289
|
+
for (let i = 0; i < 40 && !existsSync(stateFile); i++) await settle()
|
|
290
|
+
assert(existsSync(stateFile), 'the institute state is on disk in State/institute.v5state.json')
|
|
291
|
+
const before = await callTool('vibe_v5_status', {}, RA)
|
|
292
|
+
const RA2 = makeRoot(wsOf(RA)) // fresh root, fresh backend, same workspace = a reload
|
|
293
|
+
const after = await callTool('vibe_v5_status', {}, RA2)
|
|
294
|
+
assert(after.members.length === before.members.length && after.params.formalVerify === before.params.formalVerify,
|
|
295
|
+
'★ a reload reads the roster and the parameters back out of the JSON file (' + after.members.length + ' members, formalVerify=' + after.params.formalVerify + ')')
|
|
296
|
+
assert(RA2.session._events.length === 0, '★ the reloading root appends nothing to its session log either')
|
|
297
|
+
}
|
|
290
298
|
{
|
|
291
299
|
const allPrompts = spawnsFor(RA).map(s => s.prompt || '').join('\n') + '\n' + delivered.filter(d => d.rootId === RA.id).map(d => d.prompt).join('\n')
|
|
292
300
|
assert(!/Lean/.test(allPrompts), 'no founding/round prompt mentions Lean in off mode')
|
|
@@ -498,8 +506,8 @@ await drainWakes(10, RD)
|
|
|
498
506
|
|
|
499
507
|
// ---------- 6. the reusable cross-project library ----------
|
|
500
508
|
section('6 reusable definitions and lemmas go to the GLOBAL library')
|
|
501
|
-
const libPath = join(
|
|
502
|
-
const provedPath = join(
|
|
509
|
+
const libPath = join(vibeRootOf(RD), 'Formal', 'Lib')
|
|
510
|
+
const provedPath = join(vibeRootOf(RD), 'Formal', 'Proved')
|
|
503
511
|
const defRes = await callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZMod5', content: 'def ZMod5 := Fin 5\n' }, childAgent(childOf(RD, 'r-1')))
|
|
504
512
|
assert(defRes.ok === true && defRes.file === 'Formal/Lib/ZMod5.lean', 'a reusable definition is archived to the global lib (' + defRes.file + ')')
|
|
505
513
|
assert(existsSync(join(libPath, 'ZMod5.lean')), '★ the definition exists under VibeMath/Formal/Lib/ (cross-project, NOT inside the institute)')
|
|
@@ -83,37 +83,11 @@ const sleep = async (ms) => { advanceClock(ms); for (let i = 0; i < 3; i++) awai
|
|
|
83
83
|
// ---------------------------------------------------------------
|
|
84
84
|
// mock host
|
|
85
85
|
// ---------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let m = cells.get(id)
|
|
92
|
-
if (!m) { m = new Map(); cells.set(id, m) }
|
|
93
|
-
return m
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
register(def) { units.set(def.key, def); return () => { units.delete(def.key) } },
|
|
97
|
-
stateOf(session, key) {
|
|
98
|
-
const def = units.get(key)
|
|
99
|
-
if (!def) return undefined
|
|
100
|
-
const m = cellMap(session)
|
|
101
|
-
if (!m.has(key)) m.set(key, def.init(session.header, session.inheritedEventCount || 0))
|
|
102
|
-
return m.get(key)
|
|
103
|
-
},
|
|
104
|
-
_drive(session, event) {
|
|
105
|
-
const m = cellMap(session)
|
|
106
|
-
for (const [k, def] of units) {
|
|
107
|
-
const cur = m.has(k) ? m.get(k) : def.init(session.header, session.inheritedEventCount || 0)
|
|
108
|
-
let next
|
|
109
|
-
try { next = def.apply(cur, event) } catch (e) { next = cur }
|
|
110
|
-
m.set(k, next)
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const projections = makeProjectionRegistry()
|
|
86
|
+
// NO session services: v5 injects only ['subagents','agents','fs','tools','commands','timer'] and
|
|
87
|
+
// keeps its durable state in the hardened JSON file State/<institute>.v5state.json. Sessions still
|
|
88
|
+
// record anything appended to them so the suite can assert that the plugin appended NOTHING — an
|
|
89
|
+
// institute event in the user's session log makes that session unresumable (DSH refuses to load an
|
|
90
|
+
// unknown event type unless it is marked `ignorable`, which `Session.append` cannot set).
|
|
117
91
|
const listeners = {}
|
|
118
92
|
const toolRegs = []
|
|
119
93
|
const liveAgents = new Map()
|
|
@@ -123,17 +97,25 @@ const wakes = [] // queued sends not yet handled
|
|
|
123
97
|
const delivered = [] // sends that drainWakes actually handled
|
|
124
98
|
let failNextStarts = 0
|
|
125
99
|
|
|
126
|
-
|
|
100
|
+
// One workspace PER CASE. The institute's durable state is a file under the workspace, so a shared
|
|
101
|
+
// workspace would let case 1's institute (and its running state) leak into every later case.
|
|
102
|
+
const WORKSPACES = [WS]
|
|
103
|
+
function newWorkspace() {
|
|
104
|
+
const ws = mkdtempSync(join(tmpdir(), 'vibe-v5-prompt-'))
|
|
105
|
+
WORKSPACES.push(ws)
|
|
106
|
+
return ws
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function makeMockSession(id, parentSession, cwd) {
|
|
127
110
|
const events = []
|
|
128
111
|
const s = {
|
|
129
112
|
id,
|
|
130
|
-
header: { version: 1, id, createdAt: Date.now(), cwd: WS, parentSession, isSeeded: false },
|
|
113
|
+
header: { version: 1, id, createdAt: Date.now(), cwd: cwd || WS, parentSession, isSeeded: false },
|
|
131
114
|
inheritedEventCount: 0,
|
|
132
115
|
get seq() { return events.length },
|
|
133
116
|
append(type, data) {
|
|
134
117
|
const ev = { type, data, seq: events.length, time: Date.now() }
|
|
135
118
|
events.push(ev)
|
|
136
|
-
projections._drive(s, ev)
|
|
137
119
|
return ev
|
|
138
120
|
},
|
|
139
121
|
deriveMessages() { return [] },
|
|
@@ -146,17 +128,19 @@ function makeMockSession(id, parentSession) {
|
|
|
146
128
|
|
|
147
129
|
const roots = new Map()
|
|
148
130
|
let rootSeq = 0
|
|
149
|
-
|
|
131
|
+
// `ws` opens a SECOND root over an existing workspace, which is what a reload looks like.
|
|
132
|
+
function makeRoot(ws) {
|
|
150
133
|
const id = 'sess-' + String.fromCharCode(65 + rootSeq++)
|
|
151
|
-
const
|
|
134
|
+
const cwd = ws || newWorkspace()
|
|
135
|
+
const session = makeMockSession(id, undefined, cwd)
|
|
152
136
|
const root = { id, options: { provider: 'mock', model: 'm' }, session, ctx: undefined }
|
|
153
137
|
roots.set(id, root)
|
|
154
138
|
return root
|
|
155
139
|
}
|
|
140
|
+
const wsOf = (root) => root.session.header.cwd
|
|
156
141
|
|
|
157
142
|
const ctx = {
|
|
158
143
|
get(name) {
|
|
159
|
-
if (name === 'sessionProjections') return projections
|
|
160
144
|
if (name === 'sandboxPolicy') return undefined
|
|
161
145
|
if (name === 'compaction') return undefined
|
|
162
146
|
if (name === 'subprocess') {
|
|
@@ -201,14 +185,14 @@ const ctx = {
|
|
|
201
185
|
},
|
|
202
186
|
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
203
187
|
commands: { register() { return () => {} } },
|
|
204
|
-
sessions: { async flush() { return true } },
|
|
205
188
|
subagents: {
|
|
206
189
|
list() { return ['spawn'] },
|
|
207
190
|
async startContinuable({ label, request }) {
|
|
208
191
|
if (failNextStarts > 0) { failNextStarts -= 1; throw new Error('mock provisioning failure') }
|
|
209
192
|
const rootId = (request && request.parent && request.parent.id) || 'sess-A'
|
|
210
193
|
const id = 'c' + (spawns.length + 1)
|
|
211
|
-
|
|
194
|
+
const cwd = (request && request.parent && request.parent.session && request.parent.session.header.cwd) || WS
|
|
195
|
+
liveAgents.set(id, { id, session: makeMockSession(id, rootId, cwd), options: request && request.agentOptions })
|
|
212
196
|
spawns.push({
|
|
213
197
|
label, childId: id, rootId,
|
|
214
198
|
persona: request && request.persona,
|
|
@@ -428,24 +412,26 @@ function checkPromptSweep(prompt, owner, where) {
|
|
|
428
412
|
// corpus recorder
|
|
429
413
|
// ---------------------------------------------------------------
|
|
430
414
|
const corpus = []
|
|
431
|
-
// Normalise
|
|
415
|
+
// Normalise every workspace OUT of the corpus. A plain `split(WS)` is not enough on Windows:
|
|
432
416
|
// the plugin renders paths with forward slashes while os.tmpdir() may hand back a different
|
|
433
417
|
// CASE ("...\ADMIN\..." vs ".../admin/..."), so the absolute workspace path used to survive
|
|
434
418
|
// into the SHIPPED corpus — non-deterministic (the temp dir changes every run) and a machine
|
|
435
|
-
// path leak. Match case-insensitively, on either separator.
|
|
419
|
+
// path leak. Match case-insensitively, on either separator, for EVERY per-case workspace.
|
|
436
420
|
const scrub = (s) => {
|
|
437
|
-
|
|
438
|
-
const ws = WS.replace(/\\/g, '/')
|
|
439
|
-
const re = new RegExp(ws.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
|
|
421
|
+
let t = String(s == null ? '' : s).replace(/\\/g, '/')
|
|
440
422
|
// Timestamps are part of the prompt a member reads, but not part of what a reviewer needs:
|
|
441
423
|
// normalise them too. Otherwise the shipped corpus changes on EVERY run — its headings carry
|
|
442
424
|
// `### YYYY-MM-DD hh:mm:ss|<member>` — and its diffs stop being meaningful.
|
|
443
425
|
// The VibeMath ROOT gets its own token (not `<WS>/VibeMath`), so v5's corpus can be diffed
|
|
444
426
|
// side by side with v2/v3/v4's, which render the same root as `<VIBEMATH>`.
|
|
445
|
-
|
|
427
|
+
t = t.replace(/\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2})?/g, '<TIME>')
|
|
446
428
|
.replace(/[A-Za-z]:\/[^\s"'`)),。;:]*?[\\/]VibeMath/g, '<VIBEMATH>')
|
|
447
429
|
.replace(/\/VibeMath/g, '<VIBEMATH>')
|
|
448
|
-
|
|
430
|
+
for (const ws of WORKSPACES) {
|
|
431
|
+
const re = new RegExp(ws.replace(/\\/g, '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
|
|
432
|
+
t = t.replace(re, '<WS>')
|
|
433
|
+
}
|
|
434
|
+
return t
|
|
449
435
|
}
|
|
450
436
|
// Every Lean tool mention in agent-facing text must be the REGISTERED name.
|
|
451
437
|
// `Verified/Lean/` is a contract PATH, not a tool name, and on a case-insensitive reading it
|
|
@@ -521,6 +507,22 @@ for (const sp of founding) {
|
|
|
521
507
|
}
|
|
522
508
|
for (const sp of founding) { sp._ended = true; fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
|
|
523
509
|
await settleInstitute(RA)
|
|
510
|
+
// ★ The persistence contract after the DSH 0.2.0 audit: institute state lives in the hardened
|
|
511
|
+
// JSON file State/<institute>.v5state.json and NOTHING is appended to the host session log. The
|
|
512
|
+
// old projection backend wrote `vibe5/*` events into the user's own session, and DSH's session
|
|
513
|
+
// persistence refuses to load a log with an unknown event type unless it is marked
|
|
514
|
+
// `ignorable: true` — which `Session.append` cannot set — so that session became unresumable.
|
|
515
|
+
{
|
|
516
|
+
assert(RA.session._events.length === 0,
|
|
517
|
+
'★ the plugin appends NOTHING to the host session log (' + RA.session._events.length + ' events after a full founding)')
|
|
518
|
+
const stateFile = join(wsOf(RA), 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'State', 'institute.v5state.json')
|
|
519
|
+
for (let i = 0; i < 40 && !existsSync(stateFile); i++) await settle()
|
|
520
|
+
assert(existsSync(stateFile), 'the institute state is on disk in State/institute.v5state.json')
|
|
521
|
+
const RB0 = makeRoot(wsOf(RA)) // a second root over the same workspace = a reload
|
|
522
|
+
const stReload = await callTool('vibe_v5_status', {}, RB0)
|
|
523
|
+
assert(stReload.members.length === 4, '★ a reload reads the 4-member roster back out of the JSON file (got ' + stReload.members.length + ')')
|
|
524
|
+
assert(RB0.session._events.length === 0, '★ the reloading root appends nothing to its session log either')
|
|
525
|
+
}
|
|
524
526
|
await endCase(RA)
|
|
525
527
|
|
|
526
528
|
// =============== CASE 2: round prompts (normal + checkpoint) =====================
|
|
@@ -901,7 +903,7 @@ for (const w of meetingOne) {
|
|
|
901
903
|
await settleInstitute(RI)
|
|
902
904
|
const stMtg = await callTool('vibe_v5_status', {}, RI)
|
|
903
905
|
assert(stMtg.meeting === null, 'the meeting finished instead of deadlocking')
|
|
904
|
-
const minutes = join(
|
|
906
|
+
const minutes = join(wsOf(RI), 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Shared', 'Meetings', 'mt-1.md')
|
|
905
907
|
assert(existsSync(minutes), 'the meeting minutes were written')
|
|
906
908
|
if (existsSync(minutes)) {
|
|
907
909
|
const t = readFileSync(minutes, 'utf8')
|
package/tests/selfdrive-v5.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// ============================================================
|
|
2
2
|
// Vibe-Math-V5 SELF-DRIVING TEST — drives the REAL vibe-math-v5 plugin with a mock
|
|
3
|
-
// host
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// host. The host deliberately offers NO session services: v5 keeps its durable state in
|
|
4
|
+
// the hardened JSON file State/<institute>.v5state.json and appends NOTHING to the host
|
|
5
|
+
// session log (an unknown event type in the log makes the session unresumable, because
|
|
6
|
+
// DSH's session persistence refuses to load it unless the writer marked it `ignorable`
|
|
7
|
+
// — and `Session.append` cannot set that field).
|
|
6
8
|
//
|
|
7
9
|
// Run: node tests/selfdrive-v5.mjs (temp workspace; assertions; non-zero exit on failure)
|
|
8
10
|
// ============================================================
|
|
@@ -20,52 +22,22 @@ const failures = []
|
|
|
20
22
|
const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
|
|
21
23
|
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
22
24
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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,
|
|
25
|
+
// The durable authority. Reading it back is how this suite observes "committed": the
|
|
26
|
+
// plugin no longer writes anything a test could read out of the session log.
|
|
27
|
+
const V5STATE = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'State', 'institute.v5state.json')
|
|
28
|
+
const readV5State = () => { try { return JSON.parse(readFileSync(V5STATE, 'utf8')) } catch (e) { return null } }
|
|
29
|
+
// Commits are applied to the in-memory snapshot at once but WRITTEN through a deferred per-file
|
|
30
|
+
// chain, so a test that wants to observe a commit must wait for it to land on disk.
|
|
31
|
+
const waitV5State = async (pred, tries = 80) => {
|
|
32
|
+
let s = readV5State()
|
|
33
|
+
for (let i = 0; i < tries; i++) {
|
|
34
|
+
if (s && (!pred || pred(s))) return s
|
|
35
|
+
await sleep(10)
|
|
36
|
+
s = readV5State()
|
|
65
37
|
}
|
|
38
|
+
return s
|
|
66
39
|
}
|
|
67
40
|
|
|
68
|
-
const projections = makeProjectionRegistry()
|
|
69
41
|
const listeners = {}
|
|
70
42
|
const toolRegs = []
|
|
71
43
|
const commandRegs = []
|
|
@@ -75,6 +47,7 @@ const interrupts = []
|
|
|
75
47
|
const drains = []
|
|
76
48
|
const liveAgents = new Map()
|
|
77
49
|
|
|
50
|
+
// The session records whatever it is handed, so the suite can assert that it was handed NOTHING.
|
|
78
51
|
function makeMockSession(id, parentSession) {
|
|
79
52
|
const events = []
|
|
80
53
|
const s = {
|
|
@@ -85,10 +58,9 @@ function makeMockSession(id, parentSession) {
|
|
|
85
58
|
append(type, data) {
|
|
86
59
|
const ev = { type, data, seq: events.length, time: Date.now() }
|
|
87
60
|
events.push(ev)
|
|
88
|
-
projections._drive(s, ev)
|
|
89
61
|
return ev
|
|
90
62
|
},
|
|
91
|
-
deriveMessages() { return [] }, //
|
|
63
|
+
deriveMessages() { return [] }, // nothing may enter the model history
|
|
92
64
|
snapshotEvents(from) { return events.slice(from || 0) },
|
|
93
65
|
ownEvents() { return events.slice() },
|
|
94
66
|
_events: events,
|
|
@@ -100,7 +72,6 @@ const ROOT = { id: 'sess-A', options: { provider: 'mock', model: 'm' }, session:
|
|
|
100
72
|
|
|
101
73
|
const ctx = {
|
|
102
74
|
get(name) {
|
|
103
|
-
if (name === 'sessionProjections') return projections
|
|
104
75
|
if (name === 'sandboxPolicy') return undefined
|
|
105
76
|
if (name === 'compaction') return undefined
|
|
106
77
|
if (name === 'subprocess') {
|
|
@@ -126,7 +97,6 @@ const ctx = {
|
|
|
126
97
|
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
127
98
|
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
128
99
|
commands: { register(spec) { commandRegs.push(spec); return () => {} } },
|
|
129
|
-
sessions: { async flush() { return true } },
|
|
130
100
|
subagents: {
|
|
131
101
|
list() { return ['spawn'] },
|
|
132
102
|
async startContinuable({ label, request }) {
|
|
@@ -280,10 +250,20 @@ assert(spawns[0].persona.indexOf('分派') !== -1 && spawns[0].persona.indexOf('
|
|
|
280
250
|
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
251
|
|
|
282
252
|
const st0 = await callTool('vibe_v5_status', {})
|
|
283
|
-
assert(st0.backend === '
|
|
253
|
+
assert(st0.backend === 'file', 'durable state uses the hardened JSON backend (got ' + st0.backend + ')')
|
|
284
254
|
assert(st0.members.length === 4 && st0.members.filter(m => m.kind === 'researcher').length === 3, 'status roster is 1 academician + 3 researchers')
|
|
285
|
-
|
|
286
|
-
|
|
255
|
+
// ★ The whole point of the persistence fix: the plugin used to append `vibe5/*` events to the
|
|
256
|
+
// user's own session log, which made that session UNRESUMABLE (DSH refuses to load a log with an
|
|
257
|
+
// unknown event type unless it is marked `ignorable`, and `Session.append` cannot set that field).
|
|
258
|
+
// The state now lives in the JSON file, so the log must stay COMPLETELY empty.
|
|
259
|
+
assert(ROOT_SESSION._events.filter(e => String(e.type).indexOf('vibe5/') === 0).length === 0,
|
|
260
|
+
'★ the plugin appends NO institute events to the host session log')
|
|
261
|
+
assert(ROOT_SESSION._events.length === 0,
|
|
262
|
+
'★ the host session log is untouched by the whole founding (the user session stays resumable)')
|
|
263
|
+
assert(ROOT_SESSION.deriveMessages().length === 0, 'nothing enters the model history (zero context cost)')
|
|
264
|
+
const st0File = await waitV5State(s => s.institutes['default::institute'].members.length === 4)
|
|
265
|
+
assert(!!st0File && !!st0File.institutes['default::institute'], 'the institute state is on disk in State/institute.v5state.json')
|
|
266
|
+
assert(!!st0File && st0File.institutes['default::institute'].members.length === 4, 'the JSON state holds the same 4 members (got ' + (st0File ? st0File.institutes['default::institute'].members.length : 'none') + ')')
|
|
287
267
|
assert(st0.quorum.m === 3 && st0.quorum.voterCount === 4, 'm = min(quorumCap=3, P=4) = 3 (got ' + st0.quorum.m + ')')
|
|
288
268
|
assert(st0.members.every(m => m.busy === true), 'every member is marked in-flight while its founding turn runs')
|
|
289
269
|
|
|
@@ -429,8 +409,15 @@ if (existsSync(rosterMirror)) {
|
|
|
429
409
|
}
|
|
430
410
|
const boardMirror = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Shared', 'TaskBoard.md')
|
|
431
411
|
assert(existsSync(boardMirror), 'the human-readable task board mirror (Shared/TaskBoard.md) was written')
|
|
432
|
-
|
|
433
|
-
|
|
412
|
+
const stateReadme = join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'State', 'README.md')
|
|
413
|
+
assert(existsSync(stateReadme), 'State/ carries a README')
|
|
414
|
+
if (existsSync(stateReadme)) {
|
|
415
|
+
const readmeText = readFileSync(stateReadme, 'utf8')
|
|
416
|
+
assert(/权威状态/.test(readmeText) && /v5state\.json/.test(readmeText),
|
|
417
|
+
'the State/ README names State/<研究所>.v5state.json as the authoritative state')
|
|
418
|
+
assert(/不在任何宿主会话日志里/.test(readmeText),
|
|
419
|
+
'the State/ README states that the authority is NOT in any host session log')
|
|
420
|
+
}
|
|
434
421
|
|
|
435
422
|
// ---------- the institute stops only on a unanimous solve vote ----------
|
|
436
423
|
solvePlan = false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The `vibe-math-v2` agent preset: the full coding agent (`standard`), plus the
|
|
2
|
-
#
|
|
3
|
-
# framework (see
|
|
2
|
+
# classic-architecture multi-agent mathematical problem-solving & verification
|
|
3
|
+
# framework (see vibe-math-v2/实现方案.md).
|
|
4
4
|
#
|
|
5
5
|
# The vibe-math-v2 plugin row consumes the HOST subagents/agents/fs/tools/commands
|
|
6
6
|
# services and publishes nothing, so it needs no isolate realm.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
## Vibe Math V2 toolkit
|
|
17
17
|
|
|
18
18
|
This session includes the "Vibe Math V2" multi-agent mathematical problem-solving and
|
|
19
|
-
verification framework (
|
|
19
|
+
verification framework (classic architecture). It is driven by a background scheduler (code),
|
|
20
20
|
NOT by the model: you only issue the control tools below and read status; the scheduler
|
|
21
21
|
programmatically runs explorer (direction setting) → per-direction solvers (agent_self_iteration)
|
|
22
22
|
→ multi-reviewer independent review → debate → verdict, and promotes/updates data itself.
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
## Vibe Math V2 toolkit
|
|
80
80
|
|
|
81
81
|
This session includes the "Vibe Math V2" multi-agent mathematical problem-solving and
|
|
82
|
-
verification framework (
|
|
82
|
+
verification framework (classic architecture). It is driven by a background scheduler (code),
|
|
83
83
|
NOT by the model: you only issue the control tools below and read status; the scheduler
|
|
84
84
|
programmatically runs explorer (direction setting) → per-direction solvers (agent_self_iteration)
|
|
85
85
|
→ multi-reviewer independent review → debate → verdict, and promotes/updates data itself.
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
|
|
286
286
|
# ── Vibe Math V2 ────────────────────────────────────────────────────────────
|
|
287
287
|
|
|
288
|
-
# The
|
|
288
|
+
# The classic-architecture framework: a preset-local plugin (./vibe-math-v2.js)
|
|
289
289
|
# that registers the vibe_math_* tools, a /vibe slash command, and a background
|
|
290
290
|
# scheduler (explorer → per-direction solvers → multi-reviewer verification).
|
|
291
291
|
# It consumes the HOST subagents/agents/fs/tools/commands services and provides
|
package/vibe-math-v2/preset.yml
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
name: Vibe Math V2
|
|
2
|
-
description:
|
|
2
|
+
description: 多代理数学问题求解与验证框架(经典):概率驱动调度 —— qs.json 问题清单 + Propos/ 命题库 + Reliable/ 参考文献,explorer 拆方向 → 逐方向 Solver 迭代 → 多验证器独立审查→辩论→裁决;断点续跑、人工/自动干预、file/push 双模式进度汇报。
|