dsh-vibe-math 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +156 -389
- package/README.md +156 -390
- package/cordis.patch.yml +56 -14
- package/docs/AUDIT-CHECKLIST.md +2 -2
- package/docs/final-paper.md +1 -0
- package/docs/formal-verification.md +184 -29
- package/docs/math-computation.md +121 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.0.en.md +151 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.0.md +115 -0
- package/docs/test-timing.md +24 -12
- package/installer.js +7 -4
- package/package.json +18 -2
- package/prompt-corpus-persona/persona-corpus.json +12 -12
- package/prompt-corpus-persona/persona-corpus.md +116 -32
- package/prompt-corpus-v2/formal-verify-v2.json +116 -92
- package/prompt-corpus-v2/formal-verify-v2.md +1430 -55
- package/prompt-corpus-v3/formal-verify-v3.json +101 -53
- package/prompt-corpus-v3/formal-verify-v3.md +1001 -22
- package/prompt-corpus-v4/formal-verify-v4.json +14 -14
- package/prompt-corpus-v4/formal-verify-v4.md +247 -14
- package/prompt-corpus-v5/prompt-corpus-v5.json +62 -62
- package/prompt-corpus-v5/prompt-corpus-v5.md +570 -3
- package/tests/audit-math-computation-contract.mjs +191 -0
- package/tests/audit-math-computation-parity.mjs +169 -0
- package/tests/audit-math-computation-sensitivity.mjs +231 -0
- package/tests/audit-persona-sensitivity.mjs +4 -4
- package/tests/audit-persona-surface.test.mjs +38 -10
- package/tests/audit-v5-integrity.mjs +14 -0
- package/tests/e2e-v5-round2.test.mjs +452 -2
- package/tests/formal-verify-v2.test.mjs +292 -6
- package/tests/formal-verify-v3.test.mjs +271 -4
- package/tests/formal-verify-v4.test.mjs +295 -6
- package/tests/formal-verify-v5.test.mjs +9 -1
- package/tests/helpers/math-computation-fake-seam.mjs +135 -0
- package/tests/math-computation-shared.test.mjs +504 -0
- package/tests/prompt-v5-integrity.test.mjs +5 -5
- package/vibe-math-v2/agent.cordis.yml +20 -8
- package/vibe-math-v2/math-computation.js +936 -0
- package/vibe-math-v2/math-engines.js +156 -0
- package/vibe-math-v2/vibe-math-v2.js +837 -40
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v3/agent.cordis.yml +20 -8
- package/vibe-math-v3/math-computation.js +936 -0
- package/vibe-math-v3/math-engines.js +156 -0
- package/vibe-math-v3/vibe-math-v3.js +828 -47
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v4/agent.cordis.yml +34 -6
- package/vibe-math-v4/math-computation.js +936 -0
- package/vibe-math-v4/math-engines.js +156 -0
- package/vibe-math-v4/vibe-math-v4.js +801 -31
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v5/agent.cordis.yml +38 -6
- package/vibe-math-v5/math-computation.js +936 -0
- package/vibe-math-v5/math-engines.js +156 -0
- package/vibe-math-v5/vibe-math-v5.js +1120 -37
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +83 -3
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
|
@@ -132,29 +132,35 @@ const PRESETS = [
|
|
|
132
132
|
dir: 'vibe-math-v2',
|
|
133
133
|
js: 'vibe-math-v2.js',
|
|
134
134
|
prefix: 'vibe_math_',
|
|
135
|
-
tools:
|
|
135
|
+
tools: 28,
|
|
136
136
|
// member-facing write-lock / scheduler-metadata tools; the v2 coordinator never
|
|
137
137
|
// writes Markdown itself, so they stay out of its persona.
|
|
138
138
|
undocumented: [],
|
|
139
|
-
lean: { tools: ['vibe_math_lean_run', 'vibe_math_lean_archive', 'vibe_math_lean_lib'], extra: [] },
|
|
139
|
+
lean: { tools: ['vibe_math_lean_run', 'vibe_math_lean_archive', 'vibe_math_lean_lib', 'vibe_math_lean_read', 'vibe_math_lean_job'], extra: [] },
|
|
140
|
+
// the shared math-computation tool (docs/math-computation.md) is prefix-less by
|
|
141
|
+
// design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
|
|
142
|
+
math: { tools: ['math_computation'] },
|
|
140
143
|
},
|
|
141
144
|
{
|
|
142
145
|
dir: 'vibe-math-v3',
|
|
143
146
|
js: 'vibe-math-v3.js',
|
|
144
147
|
prefix: 'vibe_math_',
|
|
145
|
-
tools:
|
|
148
|
+
tools: 36,
|
|
146
149
|
// `vibe_math_sync_meta` is no longer undocumented: the promotion-contract line of the
|
|
147
150
|
// persona now names it as the place to report `lemmas[].价值/关键性` (v3 audit M1 —
|
|
148
151
|
// without a prompt-side source for that field the promotion main line is unreachable).
|
|
149
152
|
// The write-lock pair stays out: solver/method-keeper prompts carry it via kcWriteRules().
|
|
150
153
|
undocumented: ['vibe_math_claim_write', 'vibe_math_release_write'],
|
|
151
|
-
lean: { tools: ['vibe_math_lean_run', 'vibe_math_lean_archive', 'vibe_math_lean_lib'], extra: [] },
|
|
154
|
+
lean: { tools: ['vibe_math_lean_run', 'vibe_math_lean_archive', 'vibe_math_lean_lib', 'vibe_math_lean_read', 'vibe_math_lean_job'], extra: [] },
|
|
155
|
+
// the shared math-computation tool (docs/math-computation.md) is prefix-less by
|
|
156
|
+
// design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
|
|
157
|
+
math: { tools: ['math_computation'] },
|
|
152
158
|
},
|
|
153
159
|
{
|
|
154
160
|
dir: 'vibe-math-v4',
|
|
155
161
|
js: 'vibe-math-v4.js',
|
|
156
162
|
prefix: 'vibe_v4_',
|
|
157
|
-
tools:
|
|
163
|
+
tools: 35,
|
|
158
164
|
// resident-facing tools (mail, library cards, task board, write lock). The
|
|
159
165
|
// coordinator drives residents through vibe_v4_message / _meeting / _add_member.
|
|
160
166
|
undocumented: [
|
|
@@ -164,13 +170,16 @@ const PRESETS = [
|
|
|
164
170
|
'vibe_v4_task_done', 'vibe_v4_list_tasks', 'vibe_v4_report_context',
|
|
165
171
|
'vibe_v4_claim_write', 'vibe_v4_release_write',
|
|
166
172
|
],
|
|
167
|
-
lean: { tools: ['vibe_v4_lean_run', 'vibe_v4_lean_archive', 'vibe_v4_lean_lib'], extra: ['vibe_v4_formal_report'] },
|
|
173
|
+
lean: { tools: ['vibe_v4_lean_run', 'vibe_v4_lean_archive', 'vibe_v4_lean_lib', 'vibe_v4_lean_read', 'vibe_v4_lean_job'], extra: ['vibe_v4_formal_report'] },
|
|
174
|
+
// the shared math-computation tool (docs/math-computation.md) is prefix-less by
|
|
175
|
+
// design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
|
|
176
|
+
math: { tools: ['math_computation'] },
|
|
168
177
|
},
|
|
169
178
|
{
|
|
170
179
|
dir: 'vibe-math-v5',
|
|
171
180
|
js: 'vibe-math-v5.js',
|
|
172
181
|
prefix: 'vibe_v5_',
|
|
173
|
-
tools:
|
|
182
|
+
tools: 40,
|
|
174
183
|
// member- and academician-facing tools; the office (main agent) holds only the
|
|
175
184
|
// institute-level controls plus the hiring authority. The final-paper pair is an
|
|
176
185
|
// OFFICE control and IS named in the persona (vibe_v5_paper / vibe_v5_finalize_paper),
|
|
@@ -182,11 +191,14 @@ const PRESETS = [
|
|
|
182
191
|
'vibe_v5_task_list', 'vibe_v5_task_get', 'vibe_v5_task_update',
|
|
183
192
|
'vibe_v5_overview', 'vibe_v5_assign', 'vibe_v5_prioritize', 'vibe_v5_nudge',
|
|
184
193
|
],
|
|
185
|
-
lean: { tools: ['vibe_v5_lean_run', 'vibe_v5_lean_archive', 'vibe_v5_lean_lib'], extra: [] },
|
|
194
|
+
lean: { tools: ['vibe_v5_lean_run', 'vibe_v5_lean_archive', 'vibe_v5_lean_lib', 'vibe_v5_lean_read', 'vibe_v5_lean_job'], extra: [] },
|
|
195
|
+
// the shared math-computation tool (docs/math-computation.md) is prefix-less by
|
|
196
|
+
// design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
|
|
197
|
+
math: { tools: ['math_computation'] },
|
|
186
198
|
},
|
|
187
199
|
]
|
|
188
200
|
|
|
189
|
-
const LEAN_PARAMS = ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']
|
|
201
|
+
const LEAN_PARAMS = ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs', 'leanAsync', 'leanJobsMaxParallel', 'leanInitiative', 'leanSearchPaths']
|
|
190
202
|
|
|
191
203
|
for (const P of PRESETS) {
|
|
192
204
|
const ymlPath = join(BASE, P.dir, 'agent.cordis.yml')
|
|
@@ -195,9 +207,19 @@ for (const P of PRESETS) {
|
|
|
195
207
|
const src = readFileSync(jsPath, 'utf8')
|
|
196
208
|
|
|
197
209
|
// ---- registered tools -------------------------------------------------
|
|
210
|
+
// Two registration shapes exist now:
|
|
211
|
+
// 1. a literal `registerTool('<name>', …)` (most tools, including the shared math tool in
|
|
212
|
+
// v2/v3, which register it by its frozen prefix-less name);
|
|
213
|
+
// 2. the shared math tool in v4/v5, where the plugin only wires a host callback and the module
|
|
214
|
+
// calls `host.register(MATH_TOOL_NAME, …)`. The module's name is FROZEN
|
|
215
|
+
// (MATH_TOOL_NAME = 'math_computation', INTERFACE-FREEZE.md §3), so a source that wires
|
|
216
|
+
// `registerMathComputation(` contributes exactly that one tool. Without this the shared tool
|
|
217
|
+
// would be invisible to this guard on v4/v5 - a silent coverage hole.
|
|
198
218
|
const registered = new Set()
|
|
199
219
|
for (const m of src.matchAll(/registerTool\(\s*'([A-Za-z0-9_]+)'/g)) registered.add(m[1])
|
|
220
|
+
if (/registerMathComputation\s*\(/.test(src)) registered.add('math_computation')
|
|
200
221
|
eq(registered.size, P.tools, `${P.dir}: registered tool count changed (update this snapshot deliberately)`)
|
|
222
|
+
for (const tool of (P.math ? P.math.tools : [])) ok(registered.has(tool), `${P.dir}: the shared math tool ${tool} is not registered`)
|
|
201
223
|
|
|
202
224
|
// ---- A. both literal blocks exist and are non-empty -------------------
|
|
203
225
|
const blocks = literalBlocks(yml)
|
|
@@ -226,7 +248,13 @@ for (const P of PRESETS) {
|
|
|
226
248
|
// Uppercase is part of the token class on purpose: a drifted name such as
|
|
227
249
|
// `vibe_v4_formal_reportX` must be captured whole and judged unregistered, instead of
|
|
228
250
|
// being read as a mention of the registered `vibe_v4_formal_report`.
|
|
229
|
-
|
|
251
|
+
// Names that do NOT carry this preset's prefix (the shared `math_computation`) are added to the
|
|
252
|
+
// alternation from the registry, so "every registered tool must be mentioned" stays true for
|
|
253
|
+
// them too instead of forcing them into the `undocumented` snapshot.
|
|
254
|
+
const prefixRoot = P.prefix.replace(/_$/, '')
|
|
255
|
+
const prefixless = [...registered].filter((n) => !new RegExp('^' + prefixRoot + '_').test(n))
|
|
256
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
257
|
+
const re = new RegExp('\\b' + prefixRoot + '_[A-Za-z0-9_]+' + (prefixless.length ? '|\\b' + prefixless.map(escapeRe).join('\\b|\\b') + '\\b' : ''), 'g')
|
|
230
258
|
for (const m of body.matchAll(re)) {
|
|
231
259
|
if (body[m.index + m[0].length] === '*') { wildcards.add(m[0]); continue }
|
|
232
260
|
mentioned.add(m[0])
|
|
@@ -90,6 +90,20 @@ for (const m of src.matchAll(/(?:const|let|var)\s+\{([^}]+)\}\s*=/g)) {
|
|
|
90
90
|
for (const part of m[1].split(',')) { const n = part.split(':').pop().trim(); if (n) defined.add(n) }
|
|
91
91
|
}
|
|
92
92
|
for (const m of src.matchAll(/catch\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g)) defined.add(m[1])
|
|
93
|
+
// ESM imports are DEFINITIONS too: the preset imports the shared `math_computation` module
|
|
94
|
+
// (INTERFACE-FREEZE §5.1), so calling an imported function is not a ReferenceError. Without
|
|
95
|
+
// this every imported symbol was reported as "called but never defined"; a call to a name that
|
|
96
|
+
// is neither declared nor imported is still flagged.
|
|
97
|
+
for (const m of src.matchAll(/import\s*(?:\*\s*as\s+([A-Za-z_$][\w$]*)|([A-Za-z_$][\w$]*)\s*,?\s*)?(?:\{([^}]*)\}\s*)?from\s*['"][^'"]*['"]/g)) {
|
|
98
|
+
if (m[1]) defined.add(m[1])
|
|
99
|
+
if (m[2]) defined.add(m[2])
|
|
100
|
+
if (m[3]) {
|
|
101
|
+
for (const part of m[3].split(',')) {
|
|
102
|
+
const name = part.split(/\s+as\s+/).pop().trim()
|
|
103
|
+
if (name) defined.add(name)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
93
107
|
// Function/method PARAMETERS are locally bound identifiers too, not free calls.
|
|
94
108
|
for (const m of src.matchAll(/function\s*[A-Za-z_$]*\s*\(([^)]*)\)/g)) {
|
|
95
109
|
for (const raw of m[1].split(',')) {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// Run: node tests/e2e-v5-round2.test.mjs
|
|
18
18
|
// ============================================================
|
|
19
19
|
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
20
|
+
import { createHash } from 'node:crypto'
|
|
20
21
|
import { tmpdir } from 'node:os'
|
|
21
22
|
import { join, dirname, isAbsolute } from 'node:path'
|
|
22
23
|
|
|
@@ -59,6 +60,9 @@ function makeHost(opts) {
|
|
|
59
60
|
const o = opts || {}
|
|
60
61
|
const WS = o.ws || mkdtempSync(join(tmpdir(), 'vibe-v5r2-'))
|
|
61
62
|
const listeners = {}, toolRegs = [], commandRegs = [], spawns = [], wakes = [], interrupts = [], drains = []
|
|
63
|
+
// Every effect disposer, so a test can simulate a plugin UNLOAD (the Lean queue's disposer is
|
|
64
|
+
// registered first: it terminates in-flight compiles and marks them interrupted).
|
|
65
|
+
const effectDisposers = []
|
|
62
66
|
const liveAgents = new Map()
|
|
63
67
|
// The two services the fix removed. This host does not provide them, and records every
|
|
64
68
|
// request so the suite can assert the plugin never even LOOKS for them any more.
|
|
@@ -87,7 +91,7 @@ function makeHost(opts) {
|
|
|
87
91
|
return undefined
|
|
88
92
|
},
|
|
89
93
|
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
90
|
-
effect(fn) { const d = fn();
|
|
94
|
+
effect(fn) { const d = fn(); const disp = () => { if (typeof d === 'function') d() }; effectDisposers.push(disp); return disp },
|
|
91
95
|
logger: { info() {}, warn() {}, error() {} },
|
|
92
96
|
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
93
97
|
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
@@ -194,7 +198,25 @@ function makeHost(opts) {
|
|
|
194
198
|
}
|
|
195
199
|
return null
|
|
196
200
|
}
|
|
197
|
-
|
|
201
|
+
// Any member's next prompt matching a predicate — used to capture the CHECKPOINT heartbeat,
|
|
202
|
+
// whose recipient is whichever member the scheduler picked.
|
|
203
|
+
async function peekWakeWhere(pred, maxWaitMs) {
|
|
204
|
+
const t0 = Date.now()
|
|
205
|
+
while (Date.now() - t0 < (maxWaitMs || 3000)) {
|
|
206
|
+
const i = wakes.findIndex(w => pred((w.blocks && w.blocks[0] && w.blocks[0].text) || ''))
|
|
207
|
+
if (i !== -1) {
|
|
208
|
+
const w = wakes.splice(i, 1)[0]
|
|
209
|
+
return { childId: w.childId, text: (w.blocks && w.blocks[0] && w.blocks[0].text) || '' }
|
|
210
|
+
}
|
|
211
|
+
if (wakes.length) {
|
|
212
|
+
const w = wakes.shift()
|
|
213
|
+
fireEnd(w.childId, { progress: '其他成员推进中', solved: false, contextPct: 10 })
|
|
214
|
+
}
|
|
215
|
+
await sleep(20)
|
|
216
|
+
}
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
return { WS, ctx, ROOT, ROOT_SESSION, removedServiceQueries, spawns, wakes, interrupts, drains, toolRegs, commandRegs, listeners, effectDisposers, callTool, childAgent, fireEnd, spawnOf, childOf, labelOf, kindOf, settleSpawns, drain, peekWakeOf, peekWakeWhere, set plannedVotes(v) { plannedVotes = v }, get plannedVotes() { return plannedVotes }, set solvePlan(v) { solvePlan = v } }
|
|
198
220
|
}
|
|
199
221
|
|
|
200
222
|
const pluginModule = await import(PLUGIN.href + '?t=' + Date.now())
|
|
@@ -239,7 +261,59 @@ function fakeLatex(opts) {
|
|
|
239
261
|
const paperKindOf = (p) => /【最终论文·撰写/.test(p) ? 'write'
|
|
240
262
|
: /【最终论文·互审/.test(p) ? 'review'
|
|
241
263
|
: /【最终论文·定稿/.test(p) ? 'final' : ''
|
|
264
|
+
|
|
265
|
+
// ── fake Lean toolchain (docs/formal-verification.md §7) ────────────────────
|
|
266
|
+
// Injected through the SAME `subprocess` service the plugin compiles with, so detection,
|
|
267
|
+
// argv assembly, the queue and the settle logic all run unchanged; only the toolchain is fake.
|
|
268
|
+
// exitFor(argv, file) -> exitCode | undefined terminated -> count of terminate() calls
|
|
269
|
+
// defer: true every spawn stays alive until releaseAll() (for timeout/dispose/build-context)
|
|
270
|
+
function fakeLean(opts) {
|
|
271
|
+
const o = opts || {}
|
|
272
|
+
const calls = []
|
|
273
|
+
return {
|
|
274
|
+
calls,
|
|
275
|
+
terminated: 0,
|
|
276
|
+
async resolveExecutable(cmd) {
|
|
277
|
+
const name = String(cmd || 'lean')
|
|
278
|
+
if (o.missing) throw new Error('ENOENT: ' + name)
|
|
279
|
+
return 'C:/fake/' + name
|
|
280
|
+
},
|
|
281
|
+
spawn(spec) {
|
|
282
|
+
const argv = (spec.argv || []).map(String)
|
|
283
|
+
// Only the LEAN engine is a compile: the plugin also uses the subprocess service for
|
|
284
|
+
// platform shell helpers (directory setup) AND for the math_computation engine probe
|
|
285
|
+
// (which resolves its own candidates through the same seam) — neither may pollute the
|
|
286
|
+
// Lean call log, nor be deferred by the gate.
|
|
287
|
+
const head = String(argv[0] || '')
|
|
288
|
+
if (!/(^|[\\/])lean$/i.test(head)) {
|
|
289
|
+
return { done: Promise.resolve({ exitCode: 0, stdout: '', stderr: '' }), terminate() { /* not a Lean compile */ } }
|
|
290
|
+
}
|
|
291
|
+
const file = argv[argv.length - 1]
|
|
292
|
+
calls.push({ argv, file, cwd: spec.cwd })
|
|
293
|
+
let exitCode = 0
|
|
294
|
+
if (o.alwaysFail) exitCode = 1
|
|
295
|
+
else if (typeof o.exitFor === 'function') { const r = o.exitFor(argv, file); if (r !== undefined && r !== null) exitCode = r }
|
|
296
|
+
const self = this
|
|
297
|
+
if (!o.defer) return { done: Promise.resolve({ exitCode }), terminate() { self.terminated += 1 } }
|
|
298
|
+
let settled = false
|
|
299
|
+
let resolveDone = null
|
|
300
|
+
const done = new Promise((resolve) => { resolveDone = resolve })
|
|
301
|
+
const finish = (v) => { if (!settled) { settled = true; resolveDone(v) } }
|
|
302
|
+
o._release = o._release || []
|
|
303
|
+
o._release.push(() => finish({ exitCode }))
|
|
304
|
+
return { done, terminate() { self.terminated += 1; finish({ exitCode: null, signal: 'SIGTERM' }) } }
|
|
305
|
+
},
|
|
306
|
+
releaseAll() { for (const f of (o._release || []).splice(0)) { try { f() } catch (e) { /* ignore */ } } },
|
|
307
|
+
}
|
|
308
|
+
}
|
|
242
309
|
const paperDirOf = (h, id) => join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Paper', id || 'institute')
|
|
310
|
+
// Seed a .lean work file inside the institute tree (a run job needs an existing file).
|
|
311
|
+
function seedLeanFile(h, rel, text) {
|
|
312
|
+
const abs = join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', ...String(rel).split('/'))
|
|
313
|
+
mkdirSync(dirname(abs), { recursive: true })
|
|
314
|
+
writeFileSync(abs, text, 'utf8')
|
|
315
|
+
return abs
|
|
316
|
+
}
|
|
243
317
|
function listFilesUnder(root, rel) {
|
|
244
318
|
const out = []
|
|
245
319
|
const base = rel === undefined ? '' : rel
|
|
@@ -1408,6 +1482,382 @@ console.log('\n[33] an existing Paper/<id>/paper.pdf is never overwritten or del
|
|
|
1408
1482
|
assert(fake2.calls.length === 0, 'no compiler process was launched for the already-delivered pdf')
|
|
1409
1483
|
}
|
|
1410
1484
|
|
|
1485
|
+
// ---------- 34. Lean async/initiative/search params: closed schema + coercion ----------
|
|
1486
|
+
console.log('\n[34] Lean async params: closed schema, coercion, visibleParams, embedded sha256')
|
|
1487
|
+
{
|
|
1488
|
+
const h = makeHost({ pluginModule })
|
|
1489
|
+
const setSpec = h.toolRegs.find(t => t.name === 'vibe_v5_set')
|
|
1490
|
+
const keys = ['leanAsync', 'leanInitiative', 'leanSearchPaths', 'leanJobsMaxParallel']
|
|
1491
|
+
assert(keys.every(k => Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k)),
|
|
1492
|
+
'★ vibe_v5_set advertises all four new Lean keys (the schema is closed, so an unlisted key is unreachable)')
|
|
1493
|
+
assert(JSON.stringify(setSpec.parameters.properties.leanInitiative.enum) === JSON.stringify(['off', 'normal', 'eager']),
|
|
1494
|
+
'leanInitiative is a closed enum in the schema')
|
|
1495
|
+
const p0 = (await h.callTool('vibe_v5_status', {})).params
|
|
1496
|
+
assert(p0.leanAsync === true && p0.leanInitiative === 'normal' && Array.isArray(p0.leanSearchPaths) && p0.leanSearchPaths.length === 0 && p0.leanJobsMaxParallel === 1,
|
|
1497
|
+
'★ the documented defaults are live and visibleParams exposes all four (' + JSON.stringify({ a: p0.leanAsync, i: p0.leanInitiative, sp: p0.leanSearchPaths, mp: p0.leanJobsMaxParallel }) + ')')
|
|
1498
|
+
const r = await h.callTool('vibe_v5_set', { leanAsync: 'false', leanInitiative: 'bogus', leanJobsMaxParallel: 0, leanSearchPaths: '/libA, /libB' })
|
|
1499
|
+
assert(r.params.leanAsync === false, "★ leanAsync:'false' normalises to FALSE (a string must never stay truthy)")
|
|
1500
|
+
assert(r.params.leanInitiative === 'normal', 'a bogus leanInitiative falls back to the documented default')
|
|
1501
|
+
assert(r.params.leanJobsMaxParallel === 1, 'leanJobsMaxParallel:0 floors at 1')
|
|
1502
|
+
assert(JSON.stringify(r.params.leanSearchPaths) === JSON.stringify(['/libA', '/libB']),
|
|
1503
|
+
'leanSearchPaths accepts a comma string and keeps the given order (' + JSON.stringify(r.params.leanSearchPaths) + ')')
|
|
1504
|
+
const help = String(setSpec.description || '')
|
|
1505
|
+
assert(/leanAsync/.test(help) && /leanInitiative/.test(help) && /leanSearchPaths/.test(help) && /leanJobsMaxParallel/.test(help),
|
|
1506
|
+
'the set-tool help text names all four (the switch is discoverable)')
|
|
1507
|
+
const H = pluginModule.__testHelpers
|
|
1508
|
+
assert(H.sha256Hex('abc') === 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' &&
|
|
1509
|
+
H.sha256Hex('') === 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
1510
|
+
'★ the embedded sha256 matches the published vectors (job ids/dedupe rest on it)')
|
|
1511
|
+
assert(H.sha256Hex('中文abc') === createHash('sha256').update('中文abc', 'utf8').digest('hex'), 'it also matches node:crypto on non-ASCII input')
|
|
1512
|
+
assert(H.leanHasSearchFlag(['-R', 'x']) && H.leanHasSearchFlag(['--search-path=o']) && H.leanHasSearchFlag(['--root', 'r']) && !H.leanHasSearchFlag(['-j4']),
|
|
1513
|
+
'the search-flag guard recognises -R / --search-path(=) / --root and nothing else')
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// ---------- 35. async Lean: enqueue → settle(ok) → passed ----------
|
|
1517
|
+
console.log('\n[35] async Lean: enqueue returns at once, settle(ok) is the only path to passed')
|
|
1518
|
+
{
|
|
1519
|
+
const fake = fakeLean({})
|
|
1520
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1521
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1522
|
+
await h.settleSpawns()
|
|
1523
|
+
await h.callTool('vibe_v5_set', { formalVerify: 'encourage' })
|
|
1524
|
+
const arch = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-async', content: 'theorem p_async : 1 + 1 = 2 := by decide\n' })
|
|
1525
|
+
assert(arch.ok === true && arch.async && arch.async.state === 'queued' && arch.status === 'attempted',
|
|
1526
|
+
'★ the archive ENQUEUES and returns async:{jobId,state} with the object at attempted (' + JSON.stringify(arch).slice(0, 150) + ')')
|
|
1527
|
+
assert(fake.calls.length === 0, '★ the compiler has NOT been started when the tool returns')
|
|
1528
|
+
const st0 = await h.callTool('vibe_v5_status', {})
|
|
1529
|
+
assert(st0.formal.passed.indexOf('p-async') === -1, 'the queued object is not passed')
|
|
1530
|
+
const listed = await h.callTool('vibe_v5_lean_job', {})
|
|
1531
|
+
assert(listed.ok === true && listed.jobs.some(j => j.jobId === arch.jobId), 'lean_job lists the job (' + JSON.stringify(listed.jobs).slice(0, 140) + ')')
|
|
1532
|
+
const waited = await h.callTool('vibe_v5_lean_job', { jobId: arch.jobId, waitMs: 3000 })
|
|
1533
|
+
assert(waited.ok === true && waited.job.state === 'settled' && waited.job.exitCode === 0 && waited.job.proof === 'Verified/Lean/p-async.lean',
|
|
1534
|
+
'★ lean_job {jobId,waitMs} waits for the settle and reports the archived proof (' + JSON.stringify(waited.job).slice(0, 170) + ')')
|
|
1535
|
+
const st1 = await h.callTool('vibe_v5_status', {})
|
|
1536
|
+
assert(st1.formal.passed.indexOf('p-async') !== -1 && st1.formal.objects.find(o => o.target === 'p-async').async.jobId === arch.jobId,
|
|
1537
|
+
'★ ONLY the settled(ok) job set the object passed, and its receipt is on the record')
|
|
1538
|
+
assert(existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-async.lean')),
|
|
1539
|
+
'the proof is archived under Verified/Lean/<id>.lean')
|
|
1540
|
+
const argv = fake.calls[0].argv
|
|
1541
|
+
const root = join(h.WS, 'VibeMath').replace(/\\/g, '/')
|
|
1542
|
+
const si = argv.indexOf('--search-path')
|
|
1543
|
+
assert(si > 0 && argv[si + 1] === root && si + 2 === argv.length - 1 && /\.lean$/.test(argv[argv.length - 1]),
|
|
1544
|
+
'★ argv carries `--search-path <ABSOLUTE VibeMath root>` right before the file (' + JSON.stringify(argv) + ')')
|
|
1545
|
+
assert(fake.calls.length === 1, 'exactly one compile ran for the settle (got ' + fake.calls.length + ')')
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// ---------- 36. failure / timeout / dispose never verify ----------
|
|
1549
|
+
console.log('\n[36] async Lean: failure, timeout and session dispose never verify')
|
|
1550
|
+
{
|
|
1551
|
+
const fakeFail = fakeLean({ alwaysFail: true })
|
|
1552
|
+
const h = makeHost({ pluginModule, subprocess: fakeFail })
|
|
1553
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1554
|
+
await h.settleSpawns()
|
|
1555
|
+
const a = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-fail', content: 'theorem p_fail : 1 = 2 := by decide\n' })
|
|
1556
|
+
const w = await h.callTool('vibe_v5_lean_job', { jobId: a.jobId, waitMs: 3000 })
|
|
1557
|
+
assert(w.job.state === 'failed', '★ a failed compile settles as failed (' + w.job.state + ')')
|
|
1558
|
+
assert(!existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-fail.lean')),
|
|
1559
|
+
'★ no Verified/Lean file is written for a failure')
|
|
1560
|
+
assert((await h.callTool('vibe_v5_status', {})).formal.passed.indexOf('p-fail') === -1, 'the failed object stays attempted')
|
|
1561
|
+
// (b) the budget terminates the process
|
|
1562
|
+
const fakeHang = fakeLean({ defer: true })
|
|
1563
|
+
const h2 = makeHost({ pluginModule, subprocess: fakeHang })
|
|
1564
|
+
await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1565
|
+
await h2.settleSpawns()
|
|
1566
|
+
seedLeanFile(h2, 'Formal/good.lean', 'theorem good : 1 + 1 = 2 := by decide\n')
|
|
1567
|
+
const r = await h2.callTool('vibe_v5_lean_run', { file: 'Formal/good.lean', timeout_ms: 1000 })
|
|
1568
|
+
assert(r.ok === true && r.async.state === 'queued', 'the run is queued')
|
|
1569
|
+
const w2 = await h2.callTool('vibe_v5_lean_job', { jobId: r.jobId, waitMs: 5000 })
|
|
1570
|
+
assert(w2.job.state === 'timeout' && fakeHang.terminated >= 1,
|
|
1571
|
+
'★ the per-job budget TERMINATED the process and the job is timeout (' + JSON.stringify({ s: w2.job.state, t: fakeHang.terminated }) + ')')
|
|
1572
|
+
// (c) session dispose (the plugin-unload disposer)
|
|
1573
|
+
const fakeGate = fakeLean({ defer: true })
|
|
1574
|
+
const h3 = makeHost({ pluginModule, subprocess: fakeGate })
|
|
1575
|
+
await h3.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1576
|
+
await h3.settleSpawns()
|
|
1577
|
+
const a3 = await h3.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dispose', content: 'theorem p_dispose : 1 + 1 = 2 := by decide\n' })
|
|
1578
|
+
await sleep(40)
|
|
1579
|
+
assert(fakeGate.calls.length >= 1, 'precondition: the compile is in flight')
|
|
1580
|
+
h3.effectDisposers[0]()
|
|
1581
|
+
assert(fakeGate.terminated >= 1, '★ dispose TERMINATED the in-flight compiler (no orphan process)')
|
|
1582
|
+
const w3 = await h3.callTool('vibe_v5_lean_job', { jobId: a3.jobId })
|
|
1583
|
+
assert(w3.job.state === 'interrupted', '★ the job is marked interrupted, not settled (' + w3.job.state + ')')
|
|
1584
|
+
assert((await h3.callTool('vibe_v5_status', {})).formal.passed.indexOf('p-dispose') === -1, '★ dispose NEVER verifies an object')
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// ---------- 37. content-hash dedupe ----------
|
|
1588
|
+
console.log('\n[37] identical re-archive is de-duplicated (no rewrite, no recompile)')
|
|
1589
|
+
{
|
|
1590
|
+
const fake = fakeLean({})
|
|
1591
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1592
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1593
|
+
await h.settleSpawns()
|
|
1594
|
+
const PROOF = 'theorem p_dup : 2 + 2 = 4 := by decide\n'
|
|
1595
|
+
const one = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dup', content: PROOF })
|
|
1596
|
+
await h.callTool('vibe_v5_lean_job', { jobId: one.jobId, waitMs: 3000 })
|
|
1597
|
+
const before = fake.calls.length
|
|
1598
|
+
const again = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dup', content: PROOF })
|
|
1599
|
+
assert(again.deduped === true && fake.calls.length === before,
|
|
1600
|
+
'★ identical content is deduped: no new compile (' + JSON.stringify({ d: again.deduped, before, after: fake.calls.length }) + ')')
|
|
1601
|
+
const diff = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-dup', content: PROOF + '-- changed\n' })
|
|
1602
|
+
assert(diff.deduped !== true && diff.async && diff.async.state === 'queued', 'different content is NOT deduped (a new job is queued)')
|
|
1603
|
+
const DEF = 'def ZDup := Fin 2\n'
|
|
1604
|
+
const d1 = await h.callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZDup', content: DEF })
|
|
1605
|
+
await h.callTool('vibe_v5_lean_job', { jobId: d1.jobId, waitMs: 3000 })
|
|
1606
|
+
const callsAfter = fake.calls.length
|
|
1607
|
+
const d2 = await h.callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZDup', content: DEF })
|
|
1608
|
+
assert(d2.deduped === true && fake.calls.length === callsAfter, '★ the global library dedupes on the same content hash too')
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// ---------- 38. lean_read: verbatim text + path guard ----------
|
|
1612
|
+
console.log('\n[38] lean_read returns archived text verbatim and refuses a path-shaped name')
|
|
1613
|
+
{
|
|
1614
|
+
const fake = fakeLean({})
|
|
1615
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1616
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1617
|
+
await h.settleSpawns()
|
|
1618
|
+
const body = 'def ZRead : Nat := 7\n'
|
|
1619
|
+
const a = await h.callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZRead', content: body })
|
|
1620
|
+
await h.callTool('vibe_v5_lean_job', { jobId: a.jobId, waitMs: 3000 })
|
|
1621
|
+
const rd = await h.callTool('vibe_v5_lean_read', { name: 'ZRead' })
|
|
1622
|
+
assert(rd.ok === true && rd.kind === 'lib' && rd.text === body && rd.bytes === body.length && /^[0-9a-f]{64}$/.test(rd.sha256) && rd.truncated === false,
|
|
1623
|
+
'★ lean_read returns the archived text verbatim with sha/bytes (' + JSON.stringify({ k: rd.kind, b: rd.bytes }) + ')')
|
|
1624
|
+
const esc = await h.callTool('vibe_v5_lean_read', { name: '../ZRead' })
|
|
1625
|
+
assert(esc.ok === false && esc.code === 'V5_INVALID_ARGUMENT', 'a path-shaped name is REFUSED, never sanitised into another file (' + JSON.stringify(esc).slice(0, 90) + ')')
|
|
1626
|
+
assert((await h.callTool('vibe_v5_lean_read', { name: 'C:/x/ZRead' })).ok === false, 'an absolute name is refused')
|
|
1627
|
+
const miss = await h.callTool('vibe_v5_lean_read', { name: 'NoSuchThing' })
|
|
1628
|
+
assert(miss.ok === false && miss.code === 'V5_NOT_FOUND', 'an unknown name is V5_NOT_FOUND')
|
|
1629
|
+
assert((await h.callTool('vibe_v5_lean_read', { name: 'ZRead', kind: 'bogus' })).ok === false, 'a bogus kind is refused')
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// ---------- 39. a job only settles the BUILD CONTEXT it was queued under ----------
|
|
1633
|
+
console.log('\n[39] a job only settles the build context it was queued under')
|
|
1634
|
+
{
|
|
1635
|
+
const fake = fakeLean({ defer: true })
|
|
1636
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1637
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1638
|
+
await h.settleSpawns()
|
|
1639
|
+
seedLeanFile(h, 'Formal/good.lean', 'theorem good : 1 + 1 = 2 := by decide\n')
|
|
1640
|
+
await h.callTool('vibe_v5_lean_run', { file: 'Formal/good.lean' })
|
|
1641
|
+
await sleep(40)
|
|
1642
|
+
const proof = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-ctx', content: 'theorem p_ctx : 3 + 3 = 6 := by decide\n' })
|
|
1643
|
+
await h.callTool('vibe_v5_set', { leanArgs: ['-Dctx=1'] }) // retune AFTER queueing
|
|
1644
|
+
for (let i = 0; i < 10; i++) { fake.releaseAll(); await sleep(25) }
|
|
1645
|
+
const w = await h.callTool('vibe_v5_lean_job', { jobId: proof.jobId, waitMs: 1000 })
|
|
1646
|
+
assert(w.job.state === 'failed' && w.job.buildMatched === false,
|
|
1647
|
+
'★ a job compiled under retuned args does NOT settle (' + JSON.stringify({ s: w.job.state, b: w.job.buildMatched }) + ')')
|
|
1648
|
+
const st = await h.callTool('vibe_v5_status', {})
|
|
1649
|
+
assert(st.formal.passed.indexOf('p-ctx') === -1, '★ the object stays attempted on a build-context mismatch')
|
|
1650
|
+
assert(!existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-ctx.lean')),
|
|
1651
|
+
'no Verified/Lean file for a mismatched build')
|
|
1652
|
+
// (b) the CONTENT changed after the job was queued: the compile succeeds, but it compiled
|
|
1653
|
+
// something else — the object must stay attempted.
|
|
1654
|
+
const blocker2 = await h.callTool('vibe_v5_lean_run', { file: 'Formal/good.lean' })
|
|
1655
|
+
await sleep(40)
|
|
1656
|
+
const p2 = await h.callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-hash', content: 'theorem p_hash : 4 + 4 = 8 := by decide\n' })
|
|
1657
|
+
seedLeanFile(h, 'Formal/p-hash.lean', 'theorem p_hash : 4 + 4 = 8 := by decide\n-- edited while queued\n')
|
|
1658
|
+
for (let i = 0; i < 10; i++) { fake.releaseAll(); await sleep(25) }
|
|
1659
|
+
const w2 = await h.callTool('vibe_v5_lean_job', { jobId: p2.jobId, waitMs: 1000 })
|
|
1660
|
+
assert(w2.job.exitCode === 0 && w2.job.state === 'failed' && w2.job.buildMatched === true,
|
|
1661
|
+
'★ exit 0 with CHANGED content does not settle either (' + JSON.stringify({ s: w2.job.state, e: w2.job.exitCode, b: w2.job.buildMatched, blocker: blocker2.jobId }) + ')')
|
|
1662
|
+
assert(!existsSync(join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Verified', 'Lean', 'p-hash.lean')),
|
|
1663
|
+
'a changed-content compile writes no proof either')
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// ---------- 40. leanJobsMaxParallel (default 1 = serial) ----------
|
|
1667
|
+
console.log('\n[40] leanJobsMaxParallel caps simultaneous compiles')
|
|
1668
|
+
{
|
|
1669
|
+
const fake = fakeLean({ defer: true })
|
|
1670
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1671
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1672
|
+
await h.settleSpawns()
|
|
1673
|
+
seedLeanFile(h, 'Formal/a.lean', 'theorem a : 1 = 1 := rfl\n')
|
|
1674
|
+
seedLeanFile(h, 'Formal/b.lean', 'theorem b : 2 = 2 := rfl\n')
|
|
1675
|
+
await h.callTool('vibe_v5_lean_run', { file: 'Formal/a.lean' })
|
|
1676
|
+
await h.callTool('vibe_v5_lean_run', { file: 'Formal/b.lean' })
|
|
1677
|
+
await sleep(50)
|
|
1678
|
+
assert(fake.calls.length === 1, '★ with the default maxParallel=1 only ONE compile is in flight (got ' + fake.calls.length + ')')
|
|
1679
|
+
const listed = await h.callTool('vibe_v5_lean_job', {})
|
|
1680
|
+
assert(listed.maxParallel === 1 && listed.jobs.filter(j => j.state === 'queued').length === 1,
|
|
1681
|
+
'lean_job reports maxParallel=1 with the second job queued (' + JSON.stringify(listed).slice(0, 150) + ')')
|
|
1682
|
+
const q = listed.jobs.find(j => j.state === 'queued')
|
|
1683
|
+
await h.callTool('vibe_v5_set', { leanJobsMaxParallel: 2 })
|
|
1684
|
+
await h.callTool('vibe_v5_lean_job', { jobId: q.jobId, waitMs: 150 })
|
|
1685
|
+
await sleep(30)
|
|
1686
|
+
assert(fake.calls.length === 2, '★ raising maxParallel to 2 starts the queued job as well (got ' + fake.calls.length + ')')
|
|
1687
|
+
for (let i = 0; i < 8; i++) { fake.releaseAll(); await sleep(20) }
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// ---------- 41. the daily/verify Lean text is gated by leanInitiative ----------
|
|
1691
|
+
console.log('\n[41] leanInitiative gates the daily Lean reminders; the async rule is in both blocks')
|
|
1692
|
+
{
|
|
1693
|
+
const h = makeHost({ pluginModule })
|
|
1694
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1695
|
+
await h.settleSpawns()
|
|
1696
|
+
const promptFor = async (member, plan) => {
|
|
1697
|
+
if (plan) await h.callTool('vibe_v5_set', plan)
|
|
1698
|
+
await h.callTool('vibe_v5_say', { to: member, text: '请继续推进。' }, h.childAgent(h.childOf('acad')))
|
|
1699
|
+
const w = await h.peekWakeOf(member, 3000)
|
|
1700
|
+
if (!w) return ''
|
|
1701
|
+
h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
|
|
1702
|
+
await sleep(25)
|
|
1703
|
+
return w.text
|
|
1704
|
+
}
|
|
1705
|
+
const normal = await promptFor('r-1', { formalVerify: 'off', leanInitiative: 'normal' })
|
|
1706
|
+
assert(normal.indexOf('【顺手形式化') === -1, "leanInitiative=normal + formalVerify=off keeps today's behaviour (no daily Lean line)")
|
|
1707
|
+
const eager = await promptFor('r-1', { leanInitiative: 'eager' })
|
|
1708
|
+
assert(eager.indexOf('【顺手形式化') !== -1 && eager.indexOf('**主动**') !== -1,
|
|
1709
|
+
'★ leanInitiative=eager injects the daily line even when formalVerify is off')
|
|
1710
|
+
assert(eager.indexOf('判断标准:①') !== -1 && eager.indexOf('先 vibe_v5_lean_lib 查已有库') !== -1 && eager.indexOf('**查不到再新写**') !== -1,
|
|
1711
|
+
'★ the three criteria + reuse-first ride in the daily line')
|
|
1712
|
+
assert(eager.indexOf('import Formal.Lib.<name>') !== -1 && eager.indexOf('vibe_v5_lean_read') !== -1, 'the import/read reuse routes are named')
|
|
1713
|
+
assert(eager.indexOf('没把握就记 blocked') !== -1, 'the no-confidence→blocked rule rides in the daily line')
|
|
1714
|
+
assert(eager.indexOf('leanAsync=true') !== -1 && eager.indexOf('不得把该对象当成已通过') !== -1, '★ the async honesty rule rides in the daily line')
|
|
1715
|
+
const offInit = await promptFor('r-1', { formalVerify: 'encourage', leanInitiative: 'off' })
|
|
1716
|
+
assert(offInit.indexOf('【顺手形式化') === -1, '★ leanInitiative=off suppresses the daily line even when formalVerify is on')
|
|
1717
|
+
// The VERIFY block carries the async rule too.
|
|
1718
|
+
await h.callTool('vibe_v5_set', { formalVerify: 'encourage', leanInitiative: 'normal' })
|
|
1719
|
+
await h.callTool('vibe_v5_record_proposition', { id: 'p-leanv', statement: '异步落地前不得当已通过', value: 0.6, motive: 'm', p: 0.9 }, h.childAgent(h.childOf('r-1')))
|
|
1720
|
+
await h.callTool('vibe_v5_propose_verify', { target: 'p-leanv', kind: 'proposition', reason: '测试' }, h.childAgent(h.childOf('r-1')))
|
|
1721
|
+
const vw = await h.peekWakeOf('r-1', 3000)
|
|
1722
|
+
const vtxt = vw ? vw.text : ''
|
|
1723
|
+
assert(/形式化只写你有把握的版本/.test(vtxt) && /不得\*\*在它落地前声称已通过/.test(vtxt),
|
|
1724
|
+
'★ the verify block carries the confidence + async-honesty lines (' + JSON.stringify(vtxt.slice(-260)) + ')')
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// ---------- 42. per-injection-site gate: every Lean prompt site, both directions ----------
|
|
1728
|
+
// Lens-1 (HIGH coverage gap): the gate `leanInitiative !== 'off' && (formalOn() || eager)` is
|
|
1729
|
+
// correct, but a static scan found ZERO assertions naming `eager` per SITE. v5 has exactly two
|
|
1730
|
+
// DAILY-line sites (normalPrompt, checkpointPrompt) plus two mode-gated Lean sites (briefBlock's
|
|
1731
|
+
// `[形式化]` state line and verifyPrompt's formalPromptBlock). Each site below is asserted in BOTH
|
|
1732
|
+
// directions: (1) formalVerify=off + leanInitiative=eager ⇒ that site HAS the daily line (with the
|
|
1733
|
+
// `**主动**` mark) while the VERIFY block stays absent; (2) leanInitiative=off ⇒ that site has NO
|
|
1734
|
+
// daily line while the mode/verify text still follows formalVerify.
|
|
1735
|
+
console.log('\n[42] every Lean prompt site obeys the leanInitiative gate in both directions')
|
|
1736
|
+
{
|
|
1737
|
+
const DAILY = '【顺手形式化'
|
|
1738
|
+
const EAGER = '**主动**'
|
|
1739
|
+
// The verify-phase BLOCK header (its mode suffix distinguishes it from the normal prompt's
|
|
1740
|
+
// reply contract, which also *mentions* 【Lean 形式化验证】).
|
|
1741
|
+
const VERIFY_BLOCK = '【Lean 形式化验证('
|
|
1742
|
+
const STATE = '[形式化]'
|
|
1743
|
+
const h = makeHost({ pluginModule })
|
|
1744
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1745
|
+
await h.settleSpawns()
|
|
1746
|
+
const setAndAssert = async (plan) => {
|
|
1747
|
+
const r = await h.callTool('vibe_v5_set', plan)
|
|
1748
|
+
assert(r.ok === true, 'precondition: vibe_v5_set applied ' + JSON.stringify(plan) + ' (' + JSON.stringify(r).slice(0, 120) + ')')
|
|
1749
|
+
return r.params
|
|
1750
|
+
}
|
|
1751
|
+
// Pending turns must be ANSWERED, never dropped: a discarded wake leaves its member busy
|
|
1752
|
+
// forever, so the scheduler would have no idle member left to send a heartbeat to.
|
|
1753
|
+
const flushWakes = async (rounds) => {
|
|
1754
|
+
for (let i = 0; i < (rounds || 8); i++) {
|
|
1755
|
+
const w = h.wakes.shift()
|
|
1756
|
+
if (!w) return
|
|
1757
|
+
h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
|
|
1758
|
+
await sleep(10)
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
const normalFor = async (member) => {
|
|
1762
|
+
await h.callTool('vibe_v5_say', { to: member, text: '请继续推进。' }, h.childAgent(h.childOf('acad')))
|
|
1763
|
+
const w = await h.peekWakeOf(member, 3000)
|
|
1764
|
+
if (!w) return ''
|
|
1765
|
+
h.fireEnd(w.childId, { progress: '收到。', solved: false, contextPct: 10 })
|
|
1766
|
+
await sleep(25)
|
|
1767
|
+
return w.text
|
|
1768
|
+
}
|
|
1769
|
+
// ── SITE 1: normalPrompt ────────────────────────────────────────────────────
|
|
1770
|
+
const pA = await setAndAssert({ formalVerify: 'off', leanInitiative: 'eager' })
|
|
1771
|
+
assert(pA.formalVerify === 'off' && pA.leanInitiative === 'eager', 'precondition: formalVerify=off + leanInitiative=eager is live')
|
|
1772
|
+
await flushWakes()
|
|
1773
|
+
const s1eager = await normalFor('r-1')
|
|
1774
|
+
assert(s1eager.indexOf(DAILY) !== -1 && s1eager.indexOf(EAGER) !== -1,
|
|
1775
|
+
'S1 normalPrompt: formalVerify=off + leanInitiative=eager ⇒ the daily line IS injected (with **主动**)')
|
|
1776
|
+
assert(s1eager.indexOf(VERIFY_BLOCK) === -1 && s1eager.indexOf(STATE) === -1,
|
|
1777
|
+
'S1 normalPrompt: …while the VERIFY block and the [形式化] state line stay ABSENT (formalVerify=off)')
|
|
1778
|
+
const pB = await setAndAssert({ formalVerify: 'encourage', leanInitiative: 'off' })
|
|
1779
|
+
assert(pB.formalVerify === 'encourage' && pB.leanInitiative === 'off', 'precondition: formalVerify=encourage + leanInitiative=off is live')
|
|
1780
|
+
await sleep(30) // let prompts built with the previous params drain
|
|
1781
|
+
await flushWakes()
|
|
1782
|
+
const s1off = await normalFor('r-1')
|
|
1783
|
+
assert(s1off.indexOf(DAILY) === -1, 'S1 normalPrompt: leanInitiative=off ⇒ NO daily line')
|
|
1784
|
+
assert(s1off.indexOf(VERIFY_BLOCK) === -1 && s1off.indexOf(STATE) !== -1,
|
|
1785
|
+
'S1 normalPrompt: …but the mode-gated [形式化] state line still follows formalVerify=encourage (stateIdx=' + s1off.indexOf(STATE) + ')')
|
|
1786
|
+
// ── SITE 2: checkpointPrompt (the heartbeat) ────────────────────────────────
|
|
1787
|
+
// Delivered to the longest-idle member that owns no in-progress task, so the queue is answered
|
|
1788
|
+
// FIFO (a live institute keeps working) until a 【心跳检查】 turn shows up.
|
|
1789
|
+
const checkpointFor = async (plan) => {
|
|
1790
|
+
await setAndAssert(plan)
|
|
1791
|
+
await flushWakes()
|
|
1792
|
+
const seen = []
|
|
1793
|
+
for (let i = 0; i < 30; i++) {
|
|
1794
|
+
// A tuning call drives one scheduling pass, exactly like a real institute where events keep
|
|
1795
|
+
// arriving; the heartbeat branch then picks the longest-idle member.
|
|
1796
|
+
await h.callTool('vibe_v5_set', { maxParallel: i % 2 === 0 ? 3 : 4 })
|
|
1797
|
+
const w = await h.peekWakeWhere(() => true, 300)
|
|
1798
|
+
if (!w) continue
|
|
1799
|
+
seen.push(w.text.slice(0, 20))
|
|
1800
|
+
const isCheckpoint = w.text.indexOf('【心跳检查') !== -1
|
|
1801
|
+
h.fireEnd(w.childId, { progress: '继续推进。', solved: false, contextPct: 10 })
|
|
1802
|
+
await sleep(15)
|
|
1803
|
+
if (isCheckpoint) return { text: w.text, seen }
|
|
1804
|
+
}
|
|
1805
|
+
return { text: '', seen }
|
|
1806
|
+
}
|
|
1807
|
+
await h.callTool('vibe_v5_set', { activityTimeoutMs: 40, chatDigestMs: 600000, stallAutoMeetingMs: 600000 })
|
|
1808
|
+
const s2a = await checkpointFor({ formalVerify: 'off', leanInitiative: 'eager' })
|
|
1809
|
+
const s2st = await h.callTool('vibe_v5_status', {})
|
|
1810
|
+
assert(s2a.text !== '' && s2a.text.indexOf(DAILY) !== -1 && s2a.text.indexOf(EAGER) !== -1,
|
|
1811
|
+
'S2 checkpointPrompt (heartbeat): formalVerify=off + leanInitiative=eager ⇒ the daily line IS injected (seen=' + JSON.stringify(s2a.seen) + ' status=' + JSON.stringify({ verify: s2st.verify && s2st.verify.target, meeting: s2st.meeting, paper: s2st.paper && s2st.paper.stage, busy: (s2st.members || []).filter((m) => m.busy).map((m) => m.id), undecided: s2st.undecided }) + ')')
|
|
1812
|
+
assert(s2a.text.indexOf(VERIFY_BLOCK) === -1, 'S2 checkpointPrompt: the VERIFY block stays absent with formalVerify=off')
|
|
1813
|
+
const s2b = await checkpointFor({ formalVerify: 'encourage', leanInitiative: 'off' })
|
|
1814
|
+
assert(s2b.text !== '' && s2b.text.indexOf(DAILY) === -1,
|
|
1815
|
+
'S2 checkpointPrompt: leanInitiative=off ⇒ NO daily line (seen=' + JSON.stringify(s2b.seen) + ')')
|
|
1816
|
+
assert(s2b.text.indexOf(STATE) !== -1, 'S2 checkpointPrompt: …while the [形式化] state line still follows formalVerify=encourage')
|
|
1817
|
+
// ── SITE 3: verifyPrompt's formalPromptBlock ────────────────────────────────
|
|
1818
|
+
// Finish any verification that is already in flight (answering every voter turn) so that the
|
|
1819
|
+
// NEXT verify wake is necessarily built with the params we set afterwards.
|
|
1820
|
+
const drainVerify = async () => {
|
|
1821
|
+
for (let i = 0; i < 25; i++) {
|
|
1822
|
+
const st = await h.callTool('vibe_v5_status', {})
|
|
1823
|
+
if (!st.verify) return true
|
|
1824
|
+
await h.callTool('vibe_v5_set', { maxParallel: i % 2 === 0 ? 3 : 4 })
|
|
1825
|
+
const w = await h.peekWakeWhere((p) => p.indexOf('【求真表决') !== -1, 250)
|
|
1826
|
+
if (w) {
|
|
1827
|
+
const tm = /"target"\s*:\s*"([^"]+)"/.exec(w.text)
|
|
1828
|
+
h.fireEnd(w.childId, { verdict: { target: tm ? tm[1] : st.verify.target, verdict: 0.5, reason: '存疑' }, contextPct: 20 })
|
|
1829
|
+
}
|
|
1830
|
+
await sleep(20)
|
|
1831
|
+
}
|
|
1832
|
+
return false
|
|
1833
|
+
}
|
|
1834
|
+
const verifyFor = async (plan) => {
|
|
1835
|
+
await drainVerify()
|
|
1836
|
+
await setAndAssert(plan)
|
|
1837
|
+
const id = 'p-site-' + Math.random().toString(36).slice(2, 8)
|
|
1838
|
+
await h.callTool('vibe_v5_record_proposition', { id, statement: '站点门控', value: 0.6, motive: 'm', p: 0.9 }, h.childAgent(h.childOf('r-1')))
|
|
1839
|
+
await h.callTool('vibe_v5_propose_verify', { target: id, kind: 'proposition', reason: 'x' }, h.childAgent(h.childOf('r-1')))
|
|
1840
|
+
for (let i = 0; i < 25; i++) {
|
|
1841
|
+
await h.callTool('vibe_v5_set', { maxParallel: i % 2 === 0 ? 3 : 4 })
|
|
1842
|
+
const w = await h.peekWakeWhere((p) => p.indexOf('【求真表决') !== -1, 300)
|
|
1843
|
+
if (!w) continue
|
|
1844
|
+
const tm = /"target"\s*:\s*"([^"]+)"/.exec(w.text)
|
|
1845
|
+
h.fireEnd(w.childId, { verdict: { target: tm ? tm[1] : id, verdict: 0.5, reason: '存疑' }, contextPct: 20 })
|
|
1846
|
+
await sleep(25)
|
|
1847
|
+
return w.text
|
|
1848
|
+
}
|
|
1849
|
+
return ''
|
|
1850
|
+
}
|
|
1851
|
+
const s3off = await verifyFor({ formalVerify: 'off', leanInitiative: 'eager' })
|
|
1852
|
+
assert(s3off.indexOf('【求真表决') !== -1 && s3off.indexOf(VERIFY_BLOCK) === -1,
|
|
1853
|
+
'S3 verifyPrompt: formalVerify=off ⇒ NO verify-phase Lean block even when leanInitiative=eager')
|
|
1854
|
+
assert(s3off.indexOf(DAILY) === -1, 'S3 verifyPrompt: the daily line is never injected into the verify prompt')
|
|
1855
|
+
const s3on = await verifyFor({ formalVerify: 'encourage', leanInitiative: 'off' })
|
|
1856
|
+
assert(s3on.indexOf('【求真表决') !== -1 && s3on.indexOf(VERIFY_BLOCK) !== -1,
|
|
1857
|
+
'S3 verifyPrompt: formalVerify=encourage ⇒ the verify-phase block appears even with leanInitiative=off')
|
|
1858
|
+
assert(s3on.indexOf(DAILY) === -1, 'S3 verifyPrompt: …and the daily line is still absent there')
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1411
1861
|
console.log('')
|
|
1412
1862
|
console.log('passed=' + passed + ' failed=' + failed)
|
|
1413
1863
|
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|