dsh-vibe-math 2.5.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 +175 -369
- package/README.md +175 -370
- package/cordis.patch.yml +78 -22
- package/docs/AUDIT-CHECKLIST.md +3 -3
- package/docs/final-paper.md +172 -0
- package/docs/formal-verification.md +184 -29
- package/docs/math-computation.md +121 -0
- package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -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 +25 -9
- package/installer.js +7 -4
- package/package.json +21 -3
- package/prompt-corpus-persona/persona-corpus.json +16 -16
- package/prompt-corpus-persona/persona-corpus.md +164 -52
- package/prompt-corpus-v2/formal-verify-v2.json +128 -92
- package/prompt-corpus-v2/formal-verify-v2.md +1574 -108
- package/prompt-corpus-v3/formal-verify-v3.json +137 -53
- package/prompt-corpus-v3/formal-verify-v3.md +1406 -100
- 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 +64 -11
- package/tests/audit-persona-surface.test.mjs +53 -16
- package/tests/audit-v5-integrity.mjs +14 -0
- package/tests/e2e-v5-round2.test.mjs +923 -3
- package/tests/formal-verify-v2.test.mjs +295 -6
- package/tests/formal-verify-v3.test.mjs +275 -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 +14 -8
- package/tests/selfdrive-v5.mjs +55 -4
- package/vibe-math-v2/agent.cordis.yml +36 -14
- 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 +1653 -48
- 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 +30 -12
- 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 +1623 -53
- 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 +44 -8
- 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 +1592 -37
- 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 +46 -10
- 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 +2233 -42
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +153 -4
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
|
@@ -36,6 +36,8 @@ import { fileURLToPath } from 'node:url'
|
|
|
36
36
|
const PLUGIN = process.env.V2_PLUGIN
|
|
37
37
|
? new URL('file:///' + String(process.env.V2_PLUGIN).replace(/\\/g, '/'))
|
|
38
38
|
: new URL('../vibe-math-v2/vibe-math-v2.js', import.meta.url)
|
|
39
|
+
// 纯函数测试面(__testHelpers):参数归一化/搜索路径计划/哈希/指纹都从这里守卫。
|
|
40
|
+
const MOD = await import(PLUGIN.href)
|
|
39
41
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
40
42
|
// Human-reviewable corpus (contract §10.10). A sensitivity probe runs THIS suite against a
|
|
41
43
|
// MUTATED plugin copy: writing the repository corpus from such a run would replace the
|
|
@@ -143,6 +145,7 @@ function makeHost(WS, opts = {}) {
|
|
|
143
145
|
const spawns = []
|
|
144
146
|
const followups = []
|
|
145
147
|
const interrupts = []
|
|
148
|
+
const disposers = []
|
|
146
149
|
const ctx = {
|
|
147
150
|
get(name) {
|
|
148
151
|
if (name === 'subprocess') return opts.noSubprocess ? undefined : subprocess
|
|
@@ -150,7 +153,7 @@ function makeHost(WS, opts = {}) {
|
|
|
150
153
|
return undefined
|
|
151
154
|
},
|
|
152
155
|
on(event, fn) { (listeners[event] = listeners[event] || []).push(fn) },
|
|
153
|
-
effect(fn) { const d = fn();
|
|
156
|
+
effect(fn) { const d = fn(); const off = () => { if (typeof d === 'function') d() }; disposers.push(off); return off },
|
|
154
157
|
logger: { info() {}, warn() {}, error() {} },
|
|
155
158
|
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
156
159
|
commands: { register(spec) { cmdRegs.push(spec); return () => {} } },
|
|
@@ -180,7 +183,7 @@ function makeHost(WS, opts = {}) {
|
|
|
180
183
|
},
|
|
181
184
|
}
|
|
182
185
|
const fireEnd = (info) => { for (const h of (listeners['subagent/end'] || [])) h(info) }
|
|
183
|
-
return { ctx, toolRegs, cmdRegs, spawns, followups, interrupts, fireEnd, WS }
|
|
186
|
+
return { ctx, toolRegs, cmdRegs, spawns, followups, interrupts, disposers, unmount: () => { for (const d of disposers) { try { d() } catch (e) { /* ignore */ } } }, fireEnd, WS }
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
function makeRoot(id, WS) {
|
|
@@ -212,6 +215,10 @@ async function makeCase(label, opts = {}) {
|
|
|
212
215
|
// 200ms is the sanitizer floor; verifierCount=2 (the sanitizer floor too) keeps each
|
|
213
216
|
// verification exactly two children, so a round settles predictably.
|
|
214
217
|
await call('vibe_math_set_params', { tickIntervalMs: 200, verifierCount: 2 })
|
|
218
|
+
// leanAsync defaults to TRUE (background queue). This suite's Lean sections guard the
|
|
219
|
+
// **synchronous** path verbatim, so every case pins leanAsync=false here; the async state
|
|
220
|
+
// machine (default档) is guarded by its own section, which flips it back on explicitly.
|
|
221
|
+
await call('vibe_math_set_params', { leanAsync: false })
|
|
215
222
|
const c = Object.assign(h, { WS, root, call, label })
|
|
216
223
|
hosts.push(c)
|
|
217
224
|
return c
|
|
@@ -407,7 +414,7 @@ section('2 parameter validation and runtime switching')
|
|
|
407
414
|
await startScheduler(h)
|
|
408
415
|
await tick(2000)
|
|
409
416
|
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:q2')), 60, 200)
|
|
410
|
-
assert(!!ex &&
|
|
417
|
+
assert(!!ex && /【顺手形式化(鼓励[^)]*)】/.test(ex.prompt || ''), '★ switching to encourage changes the NEXT prompt immediately')
|
|
411
418
|
if (ex) h.fireEnd({ id: ex.childId, runId: 'r', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: '```json\n{"directions":[{"id":"d1","title":"D","method":"m","core_assumption":"c","feasibility":0.5}]}\n```' }] })
|
|
412
419
|
await h.call('vibe_math_set_params', { formalVerify: 'off' })
|
|
413
420
|
await sleep(400)
|
|
@@ -440,7 +447,7 @@ section("3 'encourage' injects the Lean section into review AND debate prompts")
|
|
|
440
447
|
// really landing in the durable record — is asserted in section 11.
|
|
441
448
|
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(reviewText), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
442
449
|
assert(/"decision":"used\|blocked\|defect"/.test(reviewText), '★ the review contract lists the real decision enum (incl. defect)')
|
|
443
|
-
assert(/vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib
|
|
450
|
+
assert(/vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库\/jobs)· vibe_math_lean_read(取回归档原文)/.test(reviewText), 'the review prompt names all four v2 tools (incl. the new read-only one)')
|
|
444
451
|
assert(/归档可复用定义\/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run)/.test(reviewText), '★ the review prompt says a reusable artifact must run green BEFORE it is archived')
|
|
445
452
|
assert(/LEAN_NOT_FOUND/.test(reviewText) && /宿主无 Lean 工具链/.test(reviewText), '★ the review prompt writes out the missing-toolchain escape hatch (a host without Lean must not deadlock the agent)')
|
|
446
453
|
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
@@ -450,7 +457,7 @@ section("3 'encourage' injects the Lean section into review AND debate prompts")
|
|
|
450
457
|
await h.call('vibe_math_add_problem', { id: 'qW', description: '顺手形式化测试' })
|
|
451
458
|
await tick(2000)
|
|
452
459
|
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:qW')), 60, 200)
|
|
453
|
-
assert(!!ex &&
|
|
460
|
+
assert(!!ex && /【顺手形式化(鼓励[^)]*)】/.test(ex.prompt || ''), '★ the explorer work prompt carries the 顺手形式化 line')
|
|
454
461
|
assert(!!ex && /vibe_math_lean_archive kind='def'/.test(ex.prompt || ''), 'the work line points at the archive tool for reusable definitions')
|
|
455
462
|
assert(!!ex && /vibe_math_lean_lib 查重/.test(ex.prompt || ''), 'the work line tells members to check the reuse library first')
|
|
456
463
|
assert(!!ex && /归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。/.test(ex.prompt || ''), '★ the work line forbids archiving a definition that has not run green')
|
|
@@ -472,6 +479,10 @@ section('4 lean_run executes through the subprocess service and reports honestly
|
|
|
472
479
|
{
|
|
473
480
|
const h = await makeCase('run')
|
|
474
481
|
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
482
|
+
// 这一节把 leanAsync 固定为 false:它逐字守卫**同步路径**(旧的 await 语义)。
|
|
483
|
+
// 异步(默认档)的接口与状态机由后面的「Lean async」小节守卫——两条路径都要有绿灯。
|
|
484
|
+
await h.call('vibe_math_set_params', { leanAsync: false })
|
|
485
|
+
assert((await h.call('vibe_math_status', {})).params.leanAsync === false, 'the sync section pins leanAsync=false explicitly')
|
|
475
486
|
const proj = projRoot(h)
|
|
476
487
|
mkdirSync(join(proj, 'Formal'), { recursive: true })
|
|
477
488
|
writeFileSync(join(proj, 'Formal', 'good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
@@ -1282,6 +1293,281 @@ section('15c the framework push report and the gate notice are captured')
|
|
|
1282
1293
|
await g.call('vibe_math_pause', {})
|
|
1283
1294
|
}
|
|
1284
1295
|
|
|
1296
|
+
section('15d Lean 增量 + 异步(spec §1–§4 + 修订 §1–§5):参数、argv、队列、去重、只读面、提示词')
|
|
1297
|
+
{
|
|
1298
|
+
const H = MOD.__testHelpers
|
|
1299
|
+
const slash = (s) => String(s == null ? '' : s).replace(/\\/g, '/') // 插件内部用 '/',测试用 path.join ⇒ 比较前统一
|
|
1300
|
+
const SAME = (a, b) => slash(a) === slash(b)
|
|
1301
|
+
// ── 参数:默认值 + 归一化(字符串 'false' 绝不能滑过去) ────────────────────────────
|
|
1302
|
+
const a = await makeCase('lean-params')
|
|
1303
|
+
const st0 = await a.call('vibe_math_status', {})
|
|
1304
|
+
assert(st0.params.leanAsync === false, 'the suite pins leanAsync=false by default (sync-path guard)')
|
|
1305
|
+
await a.call('vibe_math_set_params', { leanAsync: true })
|
|
1306
|
+
assert((await a.call('vibe_math_status', {})).params.leanAsync === true, 'leanAsync=true is accepted (the async档)')
|
|
1307
|
+
const bad = await a.call('vibe_math_set_params', { leanAsync: 'false' })
|
|
1308
|
+
assert(bad.ok === true, 'set_params accepts a (badly typed) leanAsync')
|
|
1309
|
+
assert((await a.call('vibe_math_status', {})).params.leanAsync === true, '★★★ leanAsync="false" (a STRING) does not slip through the `else out[k]=v` tail: it falls back to the boolean default (true) — a truthy string must never be read as a boolean')
|
|
1310
|
+
const defs = (await a.call('vibe_math_status', {})).params
|
|
1311
|
+
assert(defs.leanJobsMaxParallel === 1 && defs.leanInitiative === 'normal' && Array.isArray(defs.leanSearchPaths) && defs.leanSearchPaths.length === 0, 'the amendment defaults are leanJobsMaxParallel=1 / leanInitiative=normal / leanSearchPaths=[]')
|
|
1312
|
+
await a.call('vibe_math_set_params', { leanInitiative: 'banana', leanJobsMaxParallel: 0, leanSearchPaths: 'not-an-array' })
|
|
1313
|
+
const p1 = (await a.call('vibe_math_status', {})).params
|
|
1314
|
+
assert(p1.leanInitiative === 'normal', 'leanInitiative rejects an unknown mode back to normal')
|
|
1315
|
+
assert(p1.leanJobsMaxParallel === 1, 'leanJobsMaxParallel is floored at 1 (never 0/unbounded)')
|
|
1316
|
+
assert(Array.isArray(p1.leanSearchPaths) && p1.leanSearchPaths.length === 0, 'leanSearchPaths rejects a non-array back to []')
|
|
1317
|
+
await a.call('vibe_math_set_params', { leanSearchPaths: [' /opt/lean-lib ', '/opt/lean-lib', ''] })
|
|
1318
|
+
assert(JSON.stringify((await a.call('vibe_math_status', {})).params.leanSearchPaths) === JSON.stringify(['/opt/lean-lib']), 'leanSearchPaths trims, drops empties and de-duplicates')
|
|
1319
|
+
// 纯函数:搜索路径计划(修订 §2)
|
|
1320
|
+
const planDefault = H.leanSearchPathPlan([], [], '/root/vm')
|
|
1321
|
+
assert(JSON.stringify(planDefault.paths) === JSON.stringify(['/root/vm']) && planDefault.inject.length === 2, '★ default plan injects exactly the automatic VibeMath root')
|
|
1322
|
+
const planExtra = H.leanSearchPathPlan(['env', 'lean'], ['/a', '/b'], '/root/vm')
|
|
1323
|
+
assert(JSON.stringify(planExtra.paths) === JSON.stringify(['/a', '/b', '/root/vm']), '★★ user leanSearchPaths come FIRST, then the automatic root')
|
|
1324
|
+
assert(JSON.stringify(planExtra.inject) === JSON.stringify(['--search-path', '/a', '--search-path', '/b', '--search-path', '/root/vm']), 'and each path is injected with its own --search-path flag')
|
|
1325
|
+
assert(H.leanSearchPathPlan(['--search-path', '/x'], ['/a'], '/root/vm').inject.length === 0, '★ an explicit --search-path in leanArgs suppresses all injection (user wins)')
|
|
1326
|
+
assert(H.leanSearchPathPlan(['-R', '/x'], ['/a'], '/root/vm').inject.length === 0 && H.leanSearchPathPlan(['--root', '/x'], ['/a'], '/root/vm').inject.length === 0, '★ -R / --root are honoured too')
|
|
1327
|
+
|
|
1328
|
+
// ── argv:注入位置在用户参数之后、文件名之前 ──────────────────────────────────────
|
|
1329
|
+
const b = await makeCase('lean-argv')
|
|
1330
|
+
await b.call('vibe_math_set_params', { leanAsync: false, leanArgs: ['env', 'lean'], leanCommand: 'lean' })
|
|
1331
|
+
const projB = projRoot(b)
|
|
1332
|
+
mkdirSync(join(projB, 'Formal'), { recursive: true })
|
|
1333
|
+
writeFileSync(join(projB, 'Formal', 'srch.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
1334
|
+
const runsBefore = leanRuns.length
|
|
1335
|
+
await b.call('vibe_math_lean_run', { file: 'Formal/srch.lean' })
|
|
1336
|
+
const argv1 = leanRuns[leanRuns.length - 1].argv
|
|
1337
|
+
assert(leanRuns.length === runsBefore + 1, 'the sync run really reached the compiler')
|
|
1338
|
+
assert(JSON.stringify(argv1.slice(0, 2)) === JSON.stringify(['lean', 'env']), 'the user leanArgs are preserved in order')
|
|
1339
|
+
assert(argv1[argv1.length - 2] === '--search-path' && SAME(argv1[argv1.length - 1], vibeRoot(b)), '★★ --search-path + the VibeMath root are injected AFTER the user args and immediately BEFORE the file (argv=' + JSON.stringify(argv1) + ')')
|
|
1340
|
+
await b.call('vibe_math_set_params', { leanArgs: ['env', 'lean', '--search-path', '/explicit'] })
|
|
1341
|
+
await b.call('vibe_math_lean_run', { file: 'Formal/srch.lean' })
|
|
1342
|
+
const argv2 = leanRuns[leanRuns.length - 1].argv
|
|
1343
|
+
assert(argv2.indexOf('--search-path') === 3 && argv2[4] === '/explicit' && argv2.filter((x) => x === '--search-path').length === 1, '★ an explicit --search-path is respected verbatim (no second injection) — got ' + JSON.stringify(argv2))
|
|
1344
|
+
await b.call('vibe_math_set_params', { leanArgs: ['env', 'lean'], leanSearchPaths: ['/extra1', '/extra2'] })
|
|
1345
|
+
await b.call('vibe_math_lean_run', { file: 'Formal/srch.lean' })
|
|
1346
|
+
const argv3 = leanRuns[leanRuns.length - 1].argv
|
|
1347
|
+
assert(JSON.stringify(argv3.slice(-6)) === JSON.stringify(['--search-path', '/extra1', '--search-path', '/extra2', '--search-path', slash(vibeRoot(b))]), '★★ leanSearchPaths are injected first, then the automatic root (got ' + JSON.stringify(argv3.slice(-6)) + ')')
|
|
1348
|
+
|
|
1349
|
+
// ── 异步状态机:入队立即返回 → 心跳排空 → 只有 settled(ok) 才是通过 ────────────────
|
|
1350
|
+
const c = await makeCase('lean-async')
|
|
1351
|
+
await c.call('vibe_math_set_params', { leanAsync: true, tickIntervalMs: 200 })
|
|
1352
|
+
const projC = projRoot(c)
|
|
1353
|
+
mkdirSync(join(projC, 'Formal'), { recursive: true })
|
|
1354
|
+
writeFileSync(join(projC, 'Formal', 'async-good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
1355
|
+
writeFileSync(join(projC, 'Formal', 'async-bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
|
|
1356
|
+
const nBefore = leanRuns.length
|
|
1357
|
+
const q1 = await c.call('vibe_math_lean_run', { file: 'Formal/async-good.lean', target: 'pAsync' })
|
|
1358
|
+
assert(q1.ok === true && !!q1.async && q1.async.state === 'queued' && typeof q1.async.jobId === 'string' && q1.async.jobId.length > 3, '★★ leanAsync=true returns IMMEDIATELY with {async:{jobId,state:queued}} (got ' + JSON.stringify(q1.async) + ')')
|
|
1359
|
+
assert(leanRuns.length === nBefore, '★★ the tool call itself did NOT block on the compiler (it is enqueued, not run)')
|
|
1360
|
+
assert(q1.async.jobId.indexOf('pAsync-') === 0, 'the jobId is <target>-<fingerprint[0:12]> (got ' + q1.async.jobId + ')')
|
|
1361
|
+
const jobFile = join(projC, 'Formal', 'Jobs', q1.async.jobId + '.json')
|
|
1362
|
+
assert(existsSync(jobFile), 'the job record is mirrored to <project>/Formal/Jobs/<jobId>.json')
|
|
1363
|
+
const rec0 = JSON.parse(readIf(jobFile))
|
|
1364
|
+
assert(rec0.state === 'queued' && typeof rec0.sha256 === 'string' && typeof rec0.buildSha256 === 'string' && rec0.buildContext.length > 0, '★ the job record carries state + content hash + build-context hash')
|
|
1365
|
+
assert((formalStateOf(c).records || {}).pAsync === undefined || formalStateOf(c).records.pAsync.status !== 'passed', '★★ a queued job never marks the object passed')
|
|
1366
|
+
const done1 = await waitFor(() => { const r = JSON.parse(readIf(jobFile) || '{}'); return r.state === 'settled' ? r : undefined }, 80, 60)
|
|
1367
|
+
assert(!!done1, 'the apply-level heartbeat drained the queue and settled the job (got ' + JSON.parse(readIf(jobFile) || '{}').state + ')')
|
|
1368
|
+
assert(done1 && done1.exitCode === 0, 'the settled record keeps exitCode 0')
|
|
1369
|
+
const recAfter = formalStateOf(c).records.pAsync
|
|
1370
|
+
assert(!!recAfter && recAfter.status === 'attempted' && recAfter.run && recAfter.run.ok === true, 'a green RUN record lands as attempted (stored, not promoted) with run.ok=true')
|
|
1371
|
+
assert(!!recAfter.async && recAfter.async.state === 'settled' && recAfter.async.jobId === q1.async.jobId, 'the object record carries the async job pointer/state')
|
|
1372
|
+
// 红色作业:attempted + 编译器输出入库,绝不 passed
|
|
1373
|
+
const q2 = await c.call('vibe_math_lean_run', { file: 'Formal/async-bad.lean', target: 'pAsyncBad' })
|
|
1374
|
+
const done2 = await waitFor(() => { const r = JSON.parse(readIf(join(projC, 'Formal', 'Jobs', q2.async.jobId + '.json')) || '{}'); return r.state === 'failed' ? r : undefined }, 80, 60)
|
|
1375
|
+
assert(!!done2 && done2.exitCode === 1, '★ a red async run settles as failed with exitCode 1')
|
|
1376
|
+
const recBad = formalStateOf(c).records.pAsyncBad
|
|
1377
|
+
assert(!!recBad && recBad.status === 'attempted' && recBad.run.ok === false, '★ a red async run is recorded as attempted (never passed)')
|
|
1378
|
+
assert(/sorry/.test(String(recBad.run.stderrTail || '')), 'and the compiler output is kept in the record for the member to fix')
|
|
1379
|
+
// ── 归档证明(异步):落地前不是 passed;落地 ok 才写 Verified/Lean/ ────────────────
|
|
1380
|
+
await c.call('vibe_math_add_proposition', { id: 'pAsymProof', 概述: '异步归档证明', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1381
|
+
mkdirSync(join(projC, 'Formal'), { recursive: true })
|
|
1382
|
+
const q3 = await c.call('vibe_math_lean_archive', { kind: 'proof', target: 'pAsymProof', content: 'theorem pAsymProof : 1 = 1 := rfl\n' })
|
|
1383
|
+
assert(q3.ok === true && q3.async && q3.async.state === 'queued' && q3.status === 'attempted', '★★ kind=proof in async档 returns queued + attempted (NOT passed)')
|
|
1384
|
+
assert(!existsSync(join(projC, 'Verified', 'Lean', 'pAsymProof.lean')), '★★ the archived proof file does NOT exist while the job is still queued')
|
|
1385
|
+
assert((formalStateOf(c).records.pAsymProof || {}).status !== 'passed', 'and the record is not passed yet')
|
|
1386
|
+
const done3 = await waitFor(() => { const r = JSON.parse(readIf(join(projC, 'Formal', 'Jobs', q3.async.jobId + '.json')) || '{}'); return r.state === 'settled' ? r : undefined }, 80, 60)
|
|
1387
|
+
assert(!!done3, 'the proof job settled green')
|
|
1388
|
+
assert(existsSync(join(projC, 'Verified', 'Lean', 'pAsymProof.lean')), '★★ only after settled(ok) is Verified/Lean/<id>.lean written')
|
|
1389
|
+
assert((formalStateOf(c).records.pAsymProof || {}).status === 'passed', '★★ and only then does the object become passed')
|
|
1390
|
+
assert(/pAsymProof/.test(readIf(join(projC, 'Formal', 'Index.md'))), 'the index reflects the passed object')
|
|
1391
|
+
// ── lean_job(修订 §3):清单 / 单查 / waitMs ─────────────────────────────────────
|
|
1392
|
+
const listJobs = await c.call('vibe_math_lean_job', {})
|
|
1393
|
+
assert(listJobs.ok === true && Array.isArray(listJobs.jobs) && listJobs.jobs.length >= 3, '★ lean_job without jobId returns the session job list')
|
|
1394
|
+
assert(listJobs.jobs.every((j) => !!j.paths && typeof j.paths.receipt === 'string'), 'every listed job carries its receipt path')
|
|
1395
|
+
const one = await c.call('vibe_math_lean_job', { jobId: q3.async.jobId })
|
|
1396
|
+
assert(one.ok === true && one.state === 'settled' && one.passed === true && one.paths.archive === 'Verified/Lean/pAsymProof.lean', '★ lean_job {jobId} returns state/exitCode/paths and the passed verdict')
|
|
1397
|
+
assert((await c.call('vibe_math_lean_job', { jobId: 'nope-000000000000' })).code === 'V2_NOT_FOUND', 'an unknown jobId is refused with a typed code')
|
|
1398
|
+
const q4 = await c.call('vibe_math_lean_run', { file: 'Formal/async-bad.lean', target: 'pAsyncBad' })
|
|
1399
|
+
const waited = await c.call('vibe_math_lean_job', { jobId: q4.async.jobId, waitMs: 4000 })
|
|
1400
|
+
assert(waited.ok === true && waited.state !== 'queued' && waited.stillRunning === false && waited.waitedMs > 0, '★★ lean_job waitMs>0 waits for the job to leave queued/running (got ' + waited.state + ')')
|
|
1401
|
+
// ── 超时:leanTimeoutMs 是每个作业的预算,主动 terminate,绝不 passed ─────────────
|
|
1402
|
+
const d = await makeCase('lean-timeout')
|
|
1403
|
+
await d.call('vibe_math_set_params', { leanAsync: true, leanTimeoutMs: 1000 })
|
|
1404
|
+
const projD = projRoot(d)
|
|
1405
|
+
mkdirSync(join(projD, 'Formal'), { recursive: true })
|
|
1406
|
+
writeFileSync(join(projD, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1 := rfl\n', 'utf8')
|
|
1407
|
+
const termBefore = terminations.length
|
|
1408
|
+
const q5 = await d.call('vibe_math_lean_run', { file: 'Formal/hang.lean', target: 'pHang' })
|
|
1409
|
+
const done5 = await waitFor(() => { const r = JSON.parse(readIf(join(projD, 'Formal', 'Jobs', q5.async.jobId + '.json')) || '{}'); return r.state === 'timeout' ? r : undefined }, 120, 60)
|
|
1410
|
+
assert(!!done5 && done5.timedOut === true, '★★ a hanging job times out (state=timeout, timedOut=true) with leanTimeoutMs as its budget')
|
|
1411
|
+
assert(terminations.length > termBefore, '★★ the timeout ACTIVELY called handle.terminate()')
|
|
1412
|
+
assert((formalStateOf(d).records.pHang || {}).status !== 'passed', '★ a timed-out job never sets passed')
|
|
1413
|
+
// ── 并发参数(修订 §5):leanJobsMaxParallel>1 时两个作业同时进入 running ──────────
|
|
1414
|
+
const e = await makeCase('lean-parallel')
|
|
1415
|
+
await e.call('vibe_math_set_params', { leanAsync: true, leanJobsMaxParallel: 2 })
|
|
1416
|
+
const projE = projRoot(e)
|
|
1417
|
+
mkdirSync(join(projE, 'Formal'), { recursive: true })
|
|
1418
|
+
writeFileSync(join(projE, 'Formal', 'h1.lean'), '-- HANG\ntheorem h1 : 1 = 1 := rfl\n', 'utf8')
|
|
1419
|
+
writeFileSync(join(projE, 'Formal', 'h2.lean'), '-- HANG\ntheorem h2 : 1 = 1 := rfl\n', 'utf8')
|
|
1420
|
+
await e.call('vibe_math_set_params', { leanTimeoutMs: 3000 })
|
|
1421
|
+
const j1 = await e.call('vibe_math_lean_run', { file: 'Formal/h1.lean', target: 'pH1' })
|
|
1422
|
+
const j2 = await e.call('vibe_math_lean_run', { file: 'Formal/h2.lean', target: 'pH2' })
|
|
1423
|
+
// Both are HANG jobs: with concurrency 2 they must be in `running` AT THE SAME TIME. A serial
|
|
1424
|
+
// queue can never reach that state, so the probe discriminates the `leanJobsMaxParallel` parameter.
|
|
1425
|
+
const both = await waitFor(() => {
|
|
1426
|
+
const a = JSON.parse(readIf(join(projE, 'Formal', 'Jobs', j1.async.jobId + '.json')) || '{}')
|
|
1427
|
+
const b = JSON.parse(readIf(join(projE, 'Formal', 'Jobs', j2.async.jobId + '.json')) || '{}')
|
|
1428
|
+
return (a.state === 'running' && b.state === 'running') ? { a: a, b: b } : undefined
|
|
1429
|
+
}, 120, 50)
|
|
1430
|
+
assert(!!both, '★★ with leanJobsMaxParallel=2 BOTH queued jobs are in running at the same time (concurrency is a parameter, not a constant)')
|
|
1431
|
+
for (const k of [j1, j2]) { try { await e.call('vibe_math_lean_job', { jobId: k.async.jobId, waitMs: 3500 }) } catch (err) { /* ignore */ } }
|
|
1432
|
+
// ── 卸载(spec §2.5 / §7-6):terminate + interrupted,绝不 passed、绝不留孤儿 ────────
|
|
1433
|
+
const dz = await makeCase('lean-dispose')
|
|
1434
|
+
await dz.call('vibe_math_set_params', { leanAsync: true, leanTimeoutMs: 60000 })
|
|
1435
|
+
const projZ = projRoot(dz)
|
|
1436
|
+
mkdirSync(join(projZ, 'Formal'), { recursive: true })
|
|
1437
|
+
writeFileSync(join(projZ, 'Formal', 'orphan.lean'), '-- HANG\ntheorem orphan : 1 = 1 := rfl\n', 'utf8')
|
|
1438
|
+
const termZ = terminations.length
|
|
1439
|
+
const qz = await dz.call('vibe_math_lean_run', { file: 'Formal/orphan.lean', target: 'pOrphan' })
|
|
1440
|
+
const startedZ = await waitFor(() => { const r = JSON.parse(readIf(join(projZ, 'Formal', 'Jobs', qz.async.jobId + '.json')) || '{}'); return r.state === 'running' ? r : undefined }, 80, 60)
|
|
1441
|
+
assert(!!startedZ, 'the dispose case got a job into running')
|
|
1442
|
+
dz.unmount() // ctx.effect disposer ⇒ disposeLeanJobs()
|
|
1443
|
+
assert(terminations.length > termZ, '★★ unmounting the plugin terminated the in-flight compile (no orphan process)')
|
|
1444
|
+
assert((formalStateOf(dz).records.pOrphan || {}).status !== 'passed', '★ dispose never marks the object passed')
|
|
1445
|
+
const afterZ = JSON.parse(readIf(join(projZ, 'Formal', 'Jobs', qz.async.jobId + '.json')) || '{}')
|
|
1446
|
+
assert(afterZ.interrupted === true || afterZ.state === 'interrupted' || afterZ.state === 'running', '★ the disposed job is recorded as interrupted (or left running for the next recovery to mark)')
|
|
1447
|
+
// ── 去重(§4.3):同内容 + 同构建上下文 ⇒ 第二次不再编译 ──────────────────────────
|
|
1448
|
+
const f = await makeCase('lean-dedupe')
|
|
1449
|
+
await f.call('vibe_math_set_params', { leanAsync: true })
|
|
1450
|
+
const defBody = 'def reusableOne : Nat := 1\n'
|
|
1451
|
+
const r1 = await f.call('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: defBody })
|
|
1452
|
+
assert(r1.ok === true && r1.async && r1.deduped === false, 'the first archive of a definition is enqueued (not deduped)')
|
|
1453
|
+
await waitFor(() => { const r = JSON.parse(readIf(join(projRoot(f), 'Formal', 'Jobs', r1.async.jobId + '.json')) || '{}'); return r.state === 'settled' ? r : undefined }, 80, 60)
|
|
1454
|
+
const runsAfter1 = leanRuns.length
|
|
1455
|
+
const r2 = await f.call('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: defBody })
|
|
1456
|
+
assert(r2.ok === true && r2.deduped === true && r2.async === undefined, '★★★ re-archiving identical content is DEDUPED (no job, no recompile) — got ' + JSON.stringify({ deduped: r2.deduped, async: r2.async }))
|
|
1457
|
+
assert(leanRuns.length === runsAfter1, '★★ the deduped archive did NOT recompile')
|
|
1458
|
+
// 构建上下文进指纹(修订 §4):同内容、不同 leanArgs ⇒ 不同 jobId / 不命中"已验证"
|
|
1459
|
+
await f.call('vibe_math_set_params', { leanArgs: ['--threads=2'] })
|
|
1460
|
+
const r5 = await f.call('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: defBody })
|
|
1461
|
+
assert(r5.ok === true && r5.deduped === false && !!r5.async && !!r1.async && r5.async.jobId !== r1.async.jobId, '★★★ the SAME content under a DIFFERENT build context (leanArgs) gets a different jobId — it is not treated as already-verified (got ' + JSON.stringify({ deduped: r5.deduped, async: r5.async }) + ')')
|
|
1462
|
+
if (r5.async) await waitFor(() => { const r = JSON.parse(readIf(join(projRoot(f), 'Formal', 'Jobs', r5.async.jobId + '.json')) || '{}'); return r.state === 'settled' ? r : undefined }, 80, 60)
|
|
1463
|
+
await f.call('vibe_math_set_params', { leanArgs: [] })
|
|
1464
|
+
const changed = defBody + '\n-- 内容变了\n'
|
|
1465
|
+
const r3 = await f.call('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: changed })
|
|
1466
|
+
assert(r3.ok === true && r3.deduped === false && !!r3.async, 'a changed body is NOT deduped (content hash differs)')
|
|
1467
|
+
if (r3.async) await waitFor(() => { const r = JSON.parse(readIf(join(projRoot(f), 'Formal', 'Jobs', r3.async.jobId + '.json')) || '{}'); return r.state === 'settled' ? r : undefined }, 80, 60)
|
|
1468
|
+
const r6 = await f.call('vibe_math_lean_archive', { kind: 'def', name: 'reusableOne', content: changed })
|
|
1469
|
+
assert(r6.ok === true && r6.deduped === true, 'and the changed body dedupes on its own (content-based, not "first wins")')
|
|
1470
|
+
// ── 崩溃恢复(§2.6 + 修订 §4):绝不置 passed ─────────────────────────────────────
|
|
1471
|
+
const g = await makeCase('lean-recover')
|
|
1472
|
+
await g.call('vibe_math_set_params', { leanAsync: true })
|
|
1473
|
+
const projG = projRoot(g)
|
|
1474
|
+
mkdirSync(join(projG, 'Formal', 'Jobs'), { recursive: true })
|
|
1475
|
+
const goodBody = 'theorem recGood : 1 = 1 := rfl\n'
|
|
1476
|
+
writeFileSync(join(projG, 'Formal', 'recGood.lean'), goodBody, 'utf8')
|
|
1477
|
+
const ctxG = H.leanBuildContext('lean', [], [slash(vibeRoot(g))])
|
|
1478
|
+
const fpG = H.leanJobFingerprint(goodBody, ctxG)
|
|
1479
|
+
const jobIdG = H.leanJobId('pRecGood', fpG)
|
|
1480
|
+
writeFileSync(join(projG, 'Formal', 'Jobs', jobIdG + '.json'), JSON.stringify({
|
|
1481
|
+
jobId: jobIdG, kind: 'archive', state: 'running', attempts: 1, target: 'pRecGood', name: 'pRecGood',
|
|
1482
|
+
rel: 'Formal/recGood.lean', scope: 'project', sha256: H.leanContentSha(goodBody), buildSha256: fpG, buildContext: ctxG,
|
|
1483
|
+
engine: 'lean', argv: [], searchPaths: [slash(vibeRoot(g))], archiveKind: 'proof', enqueuedAt: 1, startedAt: 2, settledAt: 0, exitCode: null,
|
|
1484
|
+
}), 'utf8')
|
|
1485
|
+
// 哈希不匹配的遗留作业:只标记,绝不重驱、绝不 passed
|
|
1486
|
+
const otherJob = H.leanJobId('pRecOther', H.leanJobFingerprint('theorem other : 1 = 1 := rfl\n', ctxG))
|
|
1487
|
+
writeFileSync(join(projG, 'Formal', 'Jobs', otherJob + '.json'), JSON.stringify({
|
|
1488
|
+
jobId: otherJob, kind: 'archive', state: 'running', attempts: 3, target: 'pRecOther', name: 'pRecOther',
|
|
1489
|
+
rel: 'Formal/recMissing.lean', scope: 'project', sha256: H.leanContentSha('theorem other : 1 = 1 := rfl\n'), buildSha256: 'ffffffffffff', buildContext: ctxG,
|
|
1490
|
+
engine: 'lean', argv: [], searchPaths: [slash(vibeRoot(g))], archiveKind: 'proof', enqueuedAt: 1, startedAt: 2, settledAt: 0, exitCode: null,
|
|
1491
|
+
}), 'utf8')
|
|
1492
|
+
await g.call('vibe_math_new_project', { name: 'proj' }) // 触发 setProject ⇒ recoverLeanJobs()
|
|
1493
|
+
const recG = formalStateOf(g).records.pRecGood || {}
|
|
1494
|
+
assert(recG.status === 'attempted', '★★ recovery marks a crashed running job as attempted (never passed)')
|
|
1495
|
+
const recOther = formalStateOf(g).records.pRecOther || {}
|
|
1496
|
+
assert(recOther.status === 'attempted', 'recovery marks the hash-mismatched job object as attempted too')
|
|
1497
|
+
assert(!existsSync(join(projG, 'Verified', 'Lean', 'pRecGood.lean')), '★ recovery never writes an archived proof for a crashed job')
|
|
1498
|
+
const recJobG = JSON.parse(readIf(join(projG, 'Formal', 'Jobs', jobIdG + '.json')) || '{}')
|
|
1499
|
+
assert(Number(recJobG.attempts) === 2 && ['queued', 'running', 'settled'].indexOf(recJobG.state) !== -1, '★★ the matching-hash job was re-enqueued with attempts+1 (got ' + JSON.stringify({ state: recJobG.state, attempts: recJobG.attempts }) + ')')
|
|
1500
|
+
const otherAfter = JSON.parse(readIf(join(projG, 'Formal', 'Jobs', otherJob + '.json')) || '{}')
|
|
1501
|
+
assert(otherAfter.state === 'interrupted' && Number(otherAfter.attempts) === 3, '★★ the mismatched job is only marked interrupted (NOT re-driven, attempts unchanged)')
|
|
1502
|
+
const recDone = await waitFor(() => { const r = JSON.parse(readIf(join(projG, 'Formal', 'Jobs', jobIdG + '.json')) || '{}'); return r.state === 'settled' ? r : undefined }, 80, 60)
|
|
1503
|
+
assert(!!recDone, 'the re-enqueued job then settles normally on the heartbeat (the queue really resumed)')
|
|
1504
|
+
// ── lean_read(§1.2):只读、路径守卫、64KB 截断 ─────────────────────────────────
|
|
1505
|
+
mkdirSync(join(vibeRoot(g), 'Formal', 'Lib'), { recursive: true })
|
|
1506
|
+
writeFileSync(join(vibeRoot(g), 'Formal', 'Lib', 'recGood.lean'), goodBody, 'utf8')
|
|
1507
|
+
const rd = await g.call('vibe_math_lean_read', { name: 'recGood' })
|
|
1508
|
+
assert(rd.ok === true && rd.kind === 'lib' && rd.sha256 === H.leanContentSha(goodBody) && rd.truncated === false && rd.text === goodBody, '★ lean_read returns the archived text + sha256 + bytes for a Lib file (got ' + JSON.stringify(rd).slice(0, 200) + ')')
|
|
1509
|
+
assert(typeof rd.bytes === 'number' && rd.bytes === Buffer.byteLength(goodBody, 'utf8'), 'lean_read reports the byte length')
|
|
1510
|
+
assert((await g.call('vibe_math_lean_read', { name: 'recGood', kind: 'proved' })).code === 'V2_NOT_FOUND', 'kind=proved looks only in Formal/Proved')
|
|
1511
|
+
for (const badName of ['../etc/passwd', 'a/b', 'C:\\x\\y', '..']) {
|
|
1512
|
+
const rbad = await g.call('vibe_math_lean_read', { name: badName })
|
|
1513
|
+
assert(rbad.ok === false && rbad.code === 'V2_INVALID_ARGUMENT', '★ lean_read rejects the path-escape attempt ' + JSON.stringify(badName))
|
|
1514
|
+
}
|
|
1515
|
+
const bigName = 'bigOne'
|
|
1516
|
+
mkdirSync(join(vibeRoot(g), 'Formal', 'Lib'), { recursive: true })
|
|
1517
|
+
writeFileSync(join(vibeRoot(g), 'Formal', 'Lib', bigName + '.lean'), '-- ' + 'x'.repeat(70 * 1024) + '\n', 'utf8')
|
|
1518
|
+
const rbig = await g.call('vibe_math_lean_read', { name: bigName })
|
|
1519
|
+
assert(rbig.ok === true && rbig.truncated === true && Buffer.byteLength(rbig.text, 'utf8') <= 64 * 1024, '★ lean_read truncates at 64KB and says so (truncated=true)')
|
|
1520
|
+
// ── 索引依赖列(§4.4)与 lean_lib 的 jobs/searchPath ─────────────────────────────
|
|
1521
|
+
writeFileSync(join(vibeRoot(g), 'Formal', 'Lib', 'depUser.lean'), 'import Formal.Lib.bigOne\n\ndef depUser := 1\n', 'utf8')
|
|
1522
|
+
const lib = await g.call('vibe_math_lean_lib', {})
|
|
1523
|
+
assert(!!lib.paths && SAME(lib.paths.searchPath, vibeRoot(g)), '★ lean_lib exposes paths.searchPath = the VibeMath root (got ' + JSON.stringify(lib.paths) + ')')
|
|
1524
|
+
assert(Array.isArray(lib.jobs) && lib.jobs.length >= 1, '★ lean_lib exposes the job list')
|
|
1525
|
+
assert(lib.async === true, 'lean_lib reports the current async mode')
|
|
1526
|
+
const libIdx = readIf(join(vibeRoot(g), 'Formal', 'Lib', 'Index.md'))
|
|
1527
|
+
assert(/依赖(import)/.test(libIdx) && /Formal\.Lib\.bigOne/.test(libIdx), '★★ the Lib index carries the dependency column scanned from `import` lines')
|
|
1528
|
+
// ── 主动性档位(修订 §1):提示词读 leanInitiative,不是 formalVerify ──────────────
|
|
1529
|
+
const wNormal = await makeCase('lean-init-normal')
|
|
1530
|
+
await wNormal.call('vibe_math_set_params', { formalVerify: 'encourage', leanInitiative: 'normal' })
|
|
1531
|
+
await wNormal.call('vibe_math_add_problem', { id: 'qInit', description: '主动性提示词 normal' })
|
|
1532
|
+
await startScheduler(wNormal)
|
|
1533
|
+
const exN = await waitFor(() => wNormal.spawns.find((s) => s.label.indexOf('explorer') === 0), 60, 60)
|
|
1534
|
+
const promptNormal = (exN && exN.prompt) || ''
|
|
1535
|
+
assert(/主动性 normal/.test(promptNormal) && /三条筛选判据/.test(promptNormal), '★ the work prompt names the initiative档位 and the three selection criteria')
|
|
1536
|
+
await wNormal.call('vibe_math_pause', {})
|
|
1537
|
+
|
|
1538
|
+
const wEager = await makeCase('lean-init-eager')
|
|
1539
|
+
await wEager.call('vibe_math_set_params', { formalVerify: 'encourage', leanInitiative: 'eager' })
|
|
1540
|
+
await wEager.call('vibe_math_add_problem', { id: 'qInitE', description: '主动性提示词 eager' })
|
|
1541
|
+
await startScheduler(wEager)
|
|
1542
|
+
const exE = await waitFor(() => wEager.spawns.find((s) => s.label.indexOf('explorer') === 0), 60, 60)
|
|
1543
|
+
assert(!!exE && /主动档(leanInitiative=eager)/.test(exE.prompt || ''), '★★ leanInitiative=eager adds the eager line (the prompt reads the initiative parameter)')
|
|
1544
|
+
await wEager.call('vibe_math_pause', {})
|
|
1545
|
+
|
|
1546
|
+
// v5 统一口径:主动性(leanInitiative)与验证要求强度(formalVerify)是两根独立的轴。
|
|
1547
|
+
// (a)leanInitiative:'off' ⇒ 连日线都不注入(真无操作)。
|
|
1548
|
+
const wOff = await makeCase('lean-init-off')
|
|
1549
|
+
await wOff.call('vibe_math_set_params', { formalVerify: 'encourage', leanInitiative: 'off' })
|
|
1550
|
+
await wOff.call('vibe_math_add_problem', { id: 'qInitO', description: '主动性提示词 off' })
|
|
1551
|
+
await startScheduler(wOff)
|
|
1552
|
+
const exO = await waitFor(() => wOff.spawns.find((s) => s.label.indexOf('explorer') === 0), 60, 60)
|
|
1553
|
+
assert(!!exO && !/顺手形式化/.test(exO.prompt || ''), '★★ leanInitiative=off drops the daily line entirely (initiative is a separate axis from formalVerify, which is still encourage here)')
|
|
1554
|
+
await wOff.call('vibe_math_pause', {})
|
|
1555
|
+
|
|
1556
|
+
// (b)formalVerify:'off' + leanInitiative:'eager' ⇒ 日线仍必须注入(eager 是独立轴,不许被 off 静默吞掉),
|
|
1557
|
+
// 而**验证阶段**的 Lean 文本仍然关闭。
|
|
1558
|
+
const wOffEager = await makeCase('lean-init-off-eager')
|
|
1559
|
+
await wOffEager.call('vibe_math_set_params', { formalVerify: 'off', leanInitiative: 'eager' })
|
|
1560
|
+
await wOffEager.call('vibe_math_add_problem', { id: 'qInitOE', description: 'off+eager 主动性' })
|
|
1561
|
+
await startScheduler(wOffEager)
|
|
1562
|
+
const exOE = await waitFor(() => wOffEager.spawns.find((s) => s.label.indexOf('explorer') === 0), 60, 60)
|
|
1563
|
+
const pOE = (exOE && exOE.prompt) || ''
|
|
1564
|
+
assert(/主动档(leanInitiative=eager)/.test(pOE) && /三条筛选判据/.test(pOE), '★★★ formalVerify:off + leanInitiative:eager STILL injects the full daily line (v5 rule: the initiative axis is independent)')
|
|
1565
|
+
assert(!/【Lean 形式化验证(/.test(pOE) && !wOffEager.spawns.some((s) => /【Lean 形式化验证(/.test(s.prompt || '')), '★★★ …while the verification-phase Lean block stays closed (formalVerify:off) in every captured prompt of that root')
|
|
1566
|
+
const stOE = await wOffEager.call('vibe_math_status', {})
|
|
1567
|
+
assert(stOE.params.formalVerify === 'off' && stOE.params.leanInitiative === 'eager', 'the off+eager case really ran with formalVerify:off + leanInitiative:eager')
|
|
1568
|
+
await wOffEager.call('vibe_math_pause', {})
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1285
1571
|
section('16 the captured prompt corpus is written for human review')
|
|
1286
1572
|
{
|
|
1287
1573
|
// Freeze the scheduler in every case FIRST: a still-running tick loop could emit one more
|
|
@@ -1295,6 +1581,9 @@ section('16 the captured prompt corpus is written for human review')
|
|
|
1295
1581
|
.split(ws + '\\VibeMath').join('<VIBEMATH>')
|
|
1296
1582
|
.split(slash).join('<WS>')
|
|
1297
1583
|
.split(ws).join('<WS>')
|
|
1584
|
+
// Epoch-ms in a verification-log FILE NAME is run metadata: the final-paper material embeds the
|
|
1585
|
+
// evidence index verbatim, so it must be normalised or the shipped corpus changes every run.
|
|
1586
|
+
.replace(/_[0-9]{10,16}\.json/g, '_<TIME>.json')
|
|
1298
1587
|
}
|
|
1299
1588
|
const entries = []
|
|
1300
1589
|
for (const h of hosts) {
|
|
@@ -1334,7 +1623,7 @@ section('16 the captured prompt corpus is written for human review')
|
|
|
1334
1623
|
assert(entries.some((e) => /【Lean 形式化验证(鼓励模式)】/.test(e.prompt)), 'the corpus contains the encourage verify prompt')
|
|
1335
1624
|
assert(entries.some((e) => /【Lean 形式化验证(强制模式)】/.test(e.prompt)), '★ the corpus contains the REQUIRE verify prompt')
|
|
1336
1625
|
assert(entries.some((e) => /你不需要重新检查推导/.test(e.prompt) && /一致 → Result = 1/.test(e.prompt)), 'the corpus contains the passed/fidelity prompt')
|
|
1337
|
-
assert(entries.some((e) =>
|
|
1626
|
+
assert(entries.some((e) => /【顺手形式化(鼓励[^)]*)】/.test(e.prompt)), 'the corpus contains the work-round 顺手形式化 prompt')
|
|
1338
1627
|
assert(entries.some((e) => /【顺手形式化/.test(e.prompt) && /"formal":\{"target":"<对象id>","decision":"used\|blocked\|defect"/.test(e.prompt)), 'the corpus contains the formal reply contract line')
|
|
1339
1628
|
assert(entries.some((e) => !/Lean|形式化/.test(e.prompt)), 'the corpus contains off-mode prompts with no Lean text at all')
|
|
1340
1629
|
assert(entries.some((e) => e.kind === 'wake'), 'the corpus also keeps the continuation prompts (debate rounds)')
|