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
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
// · the host session log is NEVER written, and the state reloads through the JSON file
|
|
17
17
|
// Run: node tests/e2e-v5-round2.test.mjs
|
|
18
18
|
// ============================================================
|
|
19
|
-
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
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.
|
|
@@ -70,6 +74,9 @@ function makeHost(opts) {
|
|
|
70
74
|
if (name === 'sandboxPolicy') return undefined
|
|
71
75
|
if (name === 'compaction') return o.compaction
|
|
72
76
|
if (name === 'subprocess') {
|
|
77
|
+
// o.subprocess injects a FAKE LaTeX toolchain for the final-paper tests (the plugin
|
|
78
|
+
// detects/compiles through this exact service, the same seam the Lean tests use).
|
|
79
|
+
if (o.subprocess) return o.subprocess
|
|
73
80
|
return {
|
|
74
81
|
async spawn({ argv }) {
|
|
75
82
|
const script = argv[argv.length - 1] || ''
|
|
@@ -84,7 +91,7 @@ function makeHost(opts) {
|
|
|
84
91
|
return undefined
|
|
85
92
|
},
|
|
86
93
|
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
87
|
-
effect(fn) { const d = fn();
|
|
94
|
+
effect(fn) { const d = fn(); const disp = () => { if (typeof d === 'function') d() }; effectDisposers.push(disp); return disp },
|
|
88
95
|
logger: { info() {}, warn() {}, error() {} },
|
|
89
96
|
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
90
97
|
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
@@ -191,12 +198,165 @@ function makeHost(opts) {
|
|
|
191
198
|
}
|
|
192
199
|
return null
|
|
193
200
|
}
|
|
194
|
-
|
|
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 } }
|
|
195
220
|
}
|
|
196
221
|
|
|
197
222
|
const pluginModule = await import(PLUGIN.href + '?t=' + Date.now())
|
|
198
223
|
const PROBLEM = '证明素数有无穷多个'
|
|
199
224
|
|
|
225
|
+
// ── final-paper test helpers ────────────────────────────────────────────────
|
|
226
|
+
// A FAKE LaTeX toolchain (docs/final-paper.md §10: "临时目录里放一个假的 xelatex/latexmk 脚本,或注入 runner").
|
|
227
|
+
// It is injected through the SAME `subprocess` service the plugin detects and compiles with, so
|
|
228
|
+
// the plugin's real detection/compile/repair code runs unchanged; only the toolchain is fake.
|
|
229
|
+
// `installed` — which engine names resolve (others throw, like a missing binary)
|
|
230
|
+
// `failEngines` — engines whose process exits 1
|
|
231
|
+
// `failOn` — a RegExp: if the tex on disk matches, the run exits 1 (drives the repair)
|
|
232
|
+
// `alwaysFail` — every run exits 1 (drives the degrade path)
|
|
233
|
+
function fakeLatex(opts) {
|
|
234
|
+
const o = opts || {}
|
|
235
|
+
const calls = []
|
|
236
|
+
const installed = o.installed || ['xelatex', 'pdflatex']
|
|
237
|
+
return {
|
|
238
|
+
calls,
|
|
239
|
+
async resolveExecutable(name) {
|
|
240
|
+
if (installed.indexOf(name) === -1) throw new Error('ENOENT: ' + name)
|
|
241
|
+
return 'C:/fake/' + name
|
|
242
|
+
},
|
|
243
|
+
spawn(spec) {
|
|
244
|
+
const argv = spec.argv || []
|
|
245
|
+
const name = String(argv[0] || '').split(/[\\/]/).pop()
|
|
246
|
+
const dir = String(spec.cwd || '')
|
|
247
|
+
let tex = ''
|
|
248
|
+
try { tex = readFileSync(join(dir, 'paper.tex'), 'utf8') } catch (e) { /* first attempt may race */ }
|
|
249
|
+
calls.push({ name, args: argv.slice(1), dir, tex })
|
|
250
|
+
let ok = true
|
|
251
|
+
if (o.failEngines && o.failEngines.indexOf(name) !== -1) ok = false
|
|
252
|
+
if (o.failOn && o.failOn.test(tex)) ok = false
|
|
253
|
+
if (o.alwaysFail) ok = false
|
|
254
|
+
if (ok) {
|
|
255
|
+
try { mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, 'paper.pdf'), '%PDF-1.4 fake\n', 'utf8') } catch (e) { ok = false }
|
|
256
|
+
}
|
|
257
|
+
return { done: Promise.resolve({ exitCode: ok ? 0 : 1 }), terminate() {} }
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const paperKindOf = (p) => /【最终论文·撰写/.test(p) ? 'write'
|
|
262
|
+
: /【最终论文·互审/.test(p) ? 'review'
|
|
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
|
+
}
|
|
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
|
+
}
|
|
317
|
+
function listFilesUnder(root, rel) {
|
|
318
|
+
const out = []
|
|
319
|
+
const base = rel === undefined ? '' : rel
|
|
320
|
+
for (const e of readdirSync(root, { withFileTypes: true })) {
|
|
321
|
+
const r = base ? base + '/' + e.name : e.name
|
|
322
|
+
if (e.isDirectory()) out.push(...listFilesUnder(join(root, e.name), r))
|
|
323
|
+
else out.push(r)
|
|
324
|
+
}
|
|
325
|
+
return out
|
|
326
|
+
}
|
|
327
|
+
// Answer the paper phase's own prompts until the flow stops asking: each member writes its
|
|
328
|
+
// part, cross-reviews another member's part, and the editor finalises. `opts.write/review/final`
|
|
329
|
+
// override the default (deliverable) replies, which is how the objection/cap paths are driven.
|
|
330
|
+
async function drivePaper(h, opts) {
|
|
331
|
+
const o = opts || {}
|
|
332
|
+
let steps = 0, idle = 0
|
|
333
|
+
while (steps < (o.max || 40) && idle < 8) {
|
|
334
|
+
const i = h.wakes.findIndex(w => paperKindOf((w.blocks && w.blocks[0] && w.blocks[0].text) || ''))
|
|
335
|
+
if (i === -1) { idle++; await sleep(25); continue }
|
|
336
|
+
idle = 0
|
|
337
|
+
const w = h.wakes.splice(i, 1)[0]
|
|
338
|
+
const prompt = (w.blocks && w.blocks[0] && w.blocks[0].text) || ''
|
|
339
|
+
const kind = paperKindOf(prompt)
|
|
340
|
+
const who = h.labelOf(w.childId)
|
|
341
|
+
let reply
|
|
342
|
+
if (kind === 'write') {
|
|
343
|
+
reply = o.write ? o.write(who, prompt) : ({ paper_part: { title: who + ' 的贡献', solution: who + ':原问题的完整解法(只写有证据的部分)。', methods: who + ' 的方法与经验。', rules: who + ' 归纳的规律。', limits: who + ' 的局限(未定论项已标注)。', evidence: ['Members/' + who + '/Propos/p-' + who + '.md'] } })
|
|
344
|
+
} else if (kind === 'review') {
|
|
345
|
+
const om = /待审部分(([^)]*))/.exec(prompt)
|
|
346
|
+
reply = o.review ? o.review(who, om ? om[1] : '', prompt) : ({ paper_review: { of: om ? om[1] : '', deliverable: true, comments: who + ':证据与表决记录一致,可交付。' } })
|
|
347
|
+
} else {
|
|
348
|
+
reply = o.final ? o.final(who, prompt) : ({ paper_final: { decision: 'deliverable', note: '已核对合并稿与互审意见,统一术语与符号。', conclusion: '题设范围内结论成立。' } })
|
|
349
|
+
}
|
|
350
|
+
// `capture` lets a case assert on the TEXT the member really received (the evidence-only
|
|
351
|
+
// clause lives in the prompt, not only in the plugin source).
|
|
352
|
+
if (o.capture) o.capture.push({ kind, who, prompt })
|
|
353
|
+
h.fireEnd(w.childId, reply)
|
|
354
|
+
steps++
|
|
355
|
+
await sleep(20)
|
|
356
|
+
}
|
|
357
|
+
return steps
|
|
358
|
+
}
|
|
359
|
+
|
|
200
360
|
// The durable authority is the hardened JSON file. A commit is applied to the in-memory
|
|
201
361
|
// snapshot immediately but written through a deferred per-file chain, so a test that wants to
|
|
202
362
|
// observe "this was committed" must poll the FILE — reading the session log is no longer possible
|
|
@@ -938,6 +1098,766 @@ console.log('\n[22] configure/start/resume/pause/stop/set belong to the PROVABLE
|
|
|
938
1098
|
assert(finalStop.ok === true, 'the office can still stop it (' + JSON.stringify(finalStop).slice(0, 90) + ')')
|
|
939
1099
|
}
|
|
940
1100
|
|
|
1101
|
+
// ---------- 23. final-paper params: closed schema, defaults, explicit coercion ----------
|
|
1102
|
+
console.log('\n[23] final-paper params: closed schema, defaults and explicit coercion')
|
|
1103
|
+
{
|
|
1104
|
+
const h = makeHost({ pluginModule })
|
|
1105
|
+
const setSpec = h.toolRegs.find(t => t.name === 'vibe_v5_set')
|
|
1106
|
+
const keys = ['finalPaper', 'paperFormat', 'paperLanguage', 'paperCompilePdf', 'paperEditor', 'paperLatexCommand']
|
|
1107
|
+
assert(!!setSpec && keys.every(k => Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k)),
|
|
1108
|
+
'★ vibe_v5_set advertises all six final-paper keys (the schema is closed, so an unlisted key is unreachable)')
|
|
1109
|
+
const props = setSpec.parameters.properties
|
|
1110
|
+
assert(JSON.stringify(props.paperFormat.enum) === JSON.stringify(['both', 'md', 'tex']) &&
|
|
1111
|
+
JSON.stringify(props.paperLanguage.enum) === JSON.stringify(['zh', 'en']) &&
|
|
1112
|
+
JSON.stringify(props.paperEditor.enum) === JSON.stringify(['office', 'academician']),
|
|
1113
|
+
'the three enums are narrowed in the schema (a typo must not become a fourth mode)')
|
|
1114
|
+
const s0 = (await h.callTool('vibe_v5_status', {})).params
|
|
1115
|
+
assert(s0.finalPaper === true && s0.paperFormat === 'both' && s0.paperLanguage === 'zh' && s0.paperCompilePdf === true &&
|
|
1116
|
+
s0.paperEditor === 'academician' && s0.paperLatexCommand === '',
|
|
1117
|
+
'the documented defaults are live (' + JSON.stringify({ f: s0.finalPaper, fmt: s0.paperFormat, lang: s0.paperLanguage, pdf: s0.paperCompilePdf, ed: s0.paperEditor }) + ')')
|
|
1118
|
+
const r = await h.callTool('vibe_v5_set', { finalPaper: 'false', paperFormat: 'bogus', paperLanguage: 'EN', paperEditor: 'root', paperCompilePdf: 1, paperLatexCommand: 'lualatex' })
|
|
1119
|
+
assert(r.ok === true && r.params.finalPaper === false, "★ finalPaper:'false' is coerced to false (v2 §B: an unknown spelling must not stay truthy)")
|
|
1120
|
+
assert(r.params.paperFormat === 'both', 'an unknown paperFormat falls back to both')
|
|
1121
|
+
assert(r.params.paperLanguage === 'en' && r.params.paperEditor === 'office', 'enums are case-insensitive and legacy aliases resolve (EN → en, root → office)')
|
|
1122
|
+
assert(r.params.paperCompilePdf === true && r.params.paperLatexCommand === 'lualatex', 'paperCompilePdf accepts 1 as true; paperLatexCommand is stored')
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// ---------- 24. the team flow with the OFFICE as editor (+ the consultation gate) ----------
|
|
1126
|
+
console.log('\n[24] manual paper, office editor: parts → cross-review → consultation → finalise')
|
|
1127
|
+
{
|
|
1128
|
+
const h = makeHost({ pluginModule })
|
|
1129
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1130
|
+
await h.settleSpawns()
|
|
1131
|
+
// ONE verified object, so section 4 has real evidence to index.
|
|
1132
|
+
await h.callTool('vibe_v5_record_proposition', { id: 'p-paper', statement: '最终论文只整理已有证据,不得编造', value: 0.9, motive: '论文证据', p: 1 }, h.childAgent(h.childOf('r-1')))
|
|
1133
|
+
await h.callTool('vibe_v5_propose_verify', { target: 'p-paper', kind: 'proposition', reason: '论文证据' }, h.childAgent(h.childOf('r-1')))
|
|
1134
|
+
h.plannedVotes = new Map([['acad', 1], ['r-1', 1]])
|
|
1135
|
+
await h.drain(10)
|
|
1136
|
+
const sv = await h.callTool('vibe_v5_status', {})
|
|
1137
|
+
assert(sv.verified.indexOf('p-paper') !== -1, 'precondition: one object is verified (the paper needs evidence)')
|
|
1138
|
+
assert(sv.paper === null, 'NEGATIVE: a run that has NOT concluded does not auto-start a paper')
|
|
1139
|
+
// A second object is left UNDECIDED (both voters abstain), so section 8 has an unresolved item
|
|
1140
|
+
// that MUST be marked as such.
|
|
1141
|
+
await h.callTool('vibe_v5_set', { verdictMaxRounds: 1 })
|
|
1142
|
+
await h.callTool('vibe_v5_record_proposition', { id: 'p-open', statement: '仍未定论的对象必须显式标注', value: 0.5, motive: '未决项', p: 0.5 }, h.childAgent(h.childOf('r-1')))
|
|
1143
|
+
await h.callTool('vibe_v5_propose_verify', { target: 'p-open', kind: 'proposition', reason: '未决项' }, h.childAgent(h.childOf('r-1')))
|
|
1144
|
+
h.plannedVotes = new Map([['acad', 0.5], ['r-1', 0.5]])
|
|
1145
|
+
await h.drain(10)
|
|
1146
|
+
const su = await h.callTool('vibe_v5_status', {})
|
|
1147
|
+
assert(su.undecided.indexOf('p-open') !== -1, 'precondition: one object is recorded as 未定论 (got ' + JSON.stringify(su.undecided) + ')')
|
|
1148
|
+
h.plannedVotes = new Map()
|
|
1149
|
+
const started = await h.callTool('vibe_v5_paper', { lang: 'en', format: 'both', editor: 'office' })
|
|
1150
|
+
assert(started.ok === true && started.started === true && started.editor === 'office',
|
|
1151
|
+
'the manual paper starts with the office as its editor (' + JSON.stringify(started).slice(0, 130) + ')')
|
|
1152
|
+
await drivePaper(h)
|
|
1153
|
+
let st = await h.callTool('vibe_v5_status', {})
|
|
1154
|
+
assert(st.paper && st.paper.status === 'awaiting-editor' && st.paper.parts.length === 2 && st.paper.reviews.length === 2,
|
|
1155
|
+
'★ both permanent members wrote a part and cross-reviewed another part (' + JSON.stringify({ s: st.paper && st.paper.status, parts: st.paper && st.paper.parts.length, reviews: st.paper && st.paper.reviews.length }) + ')')
|
|
1156
|
+
const refused = await h.callTool('vibe_v5_finalize_paper', { decision: 'deliverable', note: 'x' })
|
|
1157
|
+
assert(refused.ok === false && refused.code === 'V5_PAPER_CONSULT_REQUIRED',
|
|
1158
|
+
'★ NEGATIVE: the office cannot finalise before consulting the institute (' + JSON.stringify(refused).slice(0, 140) + ')')
|
|
1159
|
+
const msg = await h.callTool('vibe_v5_message', { to: 'all', content: '请各自核对证据与结论,我们随后定稿。' })
|
|
1160
|
+
assert(msg.ok === true, 'the office message is delivered')
|
|
1161
|
+
await h.callTool('vibe_v5_meeting', { agenda: '定稿前审查:证据与结论是否一致', kind: 'sync' })
|
|
1162
|
+
await h.drain(12)
|
|
1163
|
+
st = await h.callTool('vibe_v5_status', {})
|
|
1164
|
+
assert(st.paper.consult.messages >= 1 && st.paper.consult.meetings >= 1,
|
|
1165
|
+
'★ the office consultation is RECORDED (messages=' + st.paper.consult.messages + ', meetings=' + st.paper.consult.meetings + ')')
|
|
1166
|
+
const fin = await h.callTool('vibe_v5_finalize_paper', { decision: 'deliverable', note: '已与全所逐条核对证据,术语与符号统一,结论与表决记录一致。', conclusion: '题设范围内结论成立。' })
|
|
1167
|
+
assert(fin.ok === true && fin.finalized === true, 'the office finalises after consulting (' + JSON.stringify(fin).slice(0, 150) + ')')
|
|
1168
|
+
assert(fin.compile === 'not-detected', '★ this machine has no LaTeX: the compile degrades to not-detected instead of failing (' + fin.compile + ')')
|
|
1169
|
+
const dir = paperDirOf(h, 'institute')
|
|
1170
|
+
const md = existsSync(join(dir, 'paper.md')) ? readFileSync(join(dir, 'paper.md'), 'utf8') : ''
|
|
1171
|
+
const tex = existsSync(join(dir, 'paper.tex')) ? readFileSync(join(dir, 'paper.tex'), 'utf8') : ''
|
|
1172
|
+
const meta = existsSync(join(dir, 'paper.meta.json')) ? JSON.parse(readFileSync(join(dir, 'paper.meta.json'), 'utf8')) : null
|
|
1173
|
+
const heads = (md.match(/^## \d+\. /gm) || []).length
|
|
1174
|
+
assert(heads === 9, '★ the md carries exactly the 9-section skeleton (got ' + heads + ')')
|
|
1175
|
+
assert(md.indexOf('p-paper') !== -1, '★ the verified proposition is listed with its evidence path')
|
|
1176
|
+
assert(md.indexOf('未定论:p-open') !== -1, '★ an undecided object is EXPLICITLY marked in the paper (never silently dropped or promoted)')
|
|
1177
|
+
assert(tex.indexOf('\\documentclass') !== -1 && /\\section\{1\. /.test(tex), '★ the tex version is generated from the same sections')
|
|
1178
|
+
assert(md.indexOf('已与全所逐条核对证据') !== -1, 'the office finalisation note is written into the paper')
|
|
1179
|
+
assert(!!meta && meta.editor === 'office' && meta.consultation.messages >= 1 && meta.consultation.meetings >= 1 && meta.compile.status === 'not-detected',
|
|
1180
|
+
'the meta records the editor, the consultation evidence and the compile result')
|
|
1181
|
+
assert(existsSync(join(dir, 'paper.log.md')), 'the paper log records who wrote/reviewed what')
|
|
1182
|
+
const instRoot = join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute')
|
|
1183
|
+
const stray = listFilesUnder(instRoot).filter(p => /(^|\/)paper\.(md|tex|pdf|meta\.json|log\.md)$/i.test(p) && p.indexOf('Paper/institute/') !== 0)
|
|
1184
|
+
assert(stray.length === 0, '★ every paper artifact lives under Paper/<id>/ only (no Verified//State/ pollution): ' + JSON.stringify(stray))
|
|
1185
|
+
assert((await h.callTool('vibe_v5_status', {})).autoDone === false, 'a MANUAL paper does not conclude the run')
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// ---------- 25. idempotency: a repeated trigger only fills missing artifacts ----------
|
|
1189
|
+
console.log('\n[25] paper idempotency: repeated triggers only fill missing artifacts')
|
|
1190
|
+
{
|
|
1191
|
+
const h = makeHost({ pluginModule })
|
|
1192
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1193
|
+
await h.settleSpawns()
|
|
1194
|
+
const first = await h.callTool('vibe_v5_paper', {})
|
|
1195
|
+
assert(first.ok === true && first.started === true, 'the single-member institute starts its paper')
|
|
1196
|
+
await drivePaper(h)
|
|
1197
|
+
const dir = paperDirOf(h, 'institute')
|
|
1198
|
+
const metaPath = join(dir, 'paper.meta.json')
|
|
1199
|
+
assert(existsSync(join(dir, 'paper.md')) && existsSync(metaPath), 'the first run delivered md + meta')
|
|
1200
|
+
const meta1 = readFileSync(metaPath, 'utf8')
|
|
1201
|
+
const again = await h.callTool('vibe_v5_paper', {})
|
|
1202
|
+
assert(again.ok === true && again.alreadyFinalized === true, 'a repeated trigger reports alreadyFinalized instead of re-running the team (' + JSON.stringify(again).slice(0, 120) + ')')
|
|
1203
|
+
assert(readFileSync(metaPath, 'utf8') === meta1, '★ paper.meta.json is NOT rewritten (finalizedAt/inputs untouched)')
|
|
1204
|
+
unlinkSync(join(dir, 'paper.md'))
|
|
1205
|
+
const refill = await h.callTool('vibe_v5_paper', {})
|
|
1206
|
+
assert(refill.ok === true && existsSync(join(dir, 'paper.md')), '★ the missing artifact is refilled (' + JSON.stringify(refill.refill) + ')')
|
|
1207
|
+
assert(readFileSync(metaPath, 'utf8') === meta1, 'the refill did not touch the settled meta either')
|
|
1208
|
+
const forced = await h.callTool('vibe_v5_paper', { force: true, reason: 'manual force' })
|
|
1209
|
+
assert(forced.ok === true && forced.started === true && !forced.alreadyFinalized,
|
|
1210
|
+
'★ force re-runs the whole team flow instead of refilling (' + JSON.stringify(forced).slice(0, 120) + ')')
|
|
1211
|
+
const stF = await h.callTool('vibe_v5_status', {})
|
|
1212
|
+
assert(stF.paper.status === 'writing' && stF.paper.round === 1, 'the forced run restarted at round 1')
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// ---------- 26. fake LaTeX: the success path produces paper.pdf ----------
|
|
1216
|
+
console.log('\n[26] fake LaTeX compiler: success path produces paper.pdf')
|
|
1217
|
+
{
|
|
1218
|
+
const fake = fakeLatex({ installed: ['xelatex', 'pdflatex'] })
|
|
1219
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1220
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1221
|
+
await h.settleSpawns()
|
|
1222
|
+
await h.callTool('vibe_v5_paper', { format: 'both' })
|
|
1223
|
+
await drivePaper(h)
|
|
1224
|
+
const st = await h.callTool('vibe_v5_status', {})
|
|
1225
|
+
assert(st.paper && st.paper.status === 'finalized' && st.paper.compile === 'compiled', 'the paper finalised with the fake compiler (' + JSON.stringify(st.paper && { s: st.paper.status, c: st.paper.compile }) + ')')
|
|
1226
|
+
const dir = paperDirOf(h, 'institute')
|
|
1227
|
+
assert(existsSync(join(dir, 'paper.pdf')), '★ paper.pdf was produced by the compiler subprocess inside Paper/<id>/')
|
|
1228
|
+
const meta = JSON.parse(readFileSync(join(dir, 'paper.meta.json'), 'utf8'))
|
|
1229
|
+
assert(meta.compile.status === 'compiled' && meta.compile.engine === 'xelatex' && meta.compile.attempts.length === 1,
|
|
1230
|
+
'the meta records compiled/xelatex on the FIRST attempt (' + JSON.stringify(meta.compile) + ')')
|
|
1231
|
+
assert(fake.calls.length >= 2 && fake.calls.every(c => c.args.indexOf('-interaction=nonstopmode') !== -1),
|
|
1232
|
+
'★ the engine ran twice with -interaction=nonstopmode (docs/final-paper.md §8)')
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// ---------- 27. fake LaTeX: a failing package is repaired, then it degrades ----------
|
|
1236
|
+
console.log('\n[27] fake LaTeX compiler: repair path and persistent-failure degradation')
|
|
1237
|
+
{
|
|
1238
|
+
// The fake refuses `\\usepackage[hidelinks]{hyperref}`; the repair pass strips optional
|
|
1239
|
+
// packages, so attempt 3 succeeds (attempts 1-2 fail on the full tex).
|
|
1240
|
+
const fake = fakeLatex({ installed: ['xelatex'], failOn: /\\usepackage\[hidelinks\]\{hyperref\}/ })
|
|
1241
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1242
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1243
|
+
await h.settleSpawns()
|
|
1244
|
+
await h.callTool('vibe_v5_paper', { format: 'both' })
|
|
1245
|
+
await drivePaper(h)
|
|
1246
|
+
const dir = paperDirOf(h, 'institute')
|
|
1247
|
+
const meta = JSON.parse(readFileSync(join(dir, 'paper.meta.json'), 'utf8'))
|
|
1248
|
+
const at = meta.compile.attempts || []
|
|
1249
|
+
assert(meta.compile.status === 'compiled' && existsSync(join(dir, 'paper.pdf')), '★ the repair retry turned a failing compile into a PDF')
|
|
1250
|
+
assert(at.length === 3 && at[0].ok === false && at[1].ok === false && at[2].ok === true && at[2].label.indexOf('stripped') !== -1,
|
|
1251
|
+
'★ attempts: full FAIL → nonstopmode rerun FAIL → stripped-package SUCCESS (' + JSON.stringify(at.map(a => a.label + ':' + a.ok)) + ')')
|
|
1252
|
+
// A compiler that always fails must degrade, not throw, and must not block finalisation.
|
|
1253
|
+
const fake2 = fakeLatex({ installed: ['xelatex'], alwaysFail: true })
|
|
1254
|
+
const h2 = makeHost({ pluginModule, subprocess: fake2 })
|
|
1255
|
+
await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1256
|
+
await h2.settleSpawns()
|
|
1257
|
+
const fin = await h2.callTool('vibe_v5_paper', { format: 'both' })
|
|
1258
|
+
await drivePaper(h2)
|
|
1259
|
+
const st2 = await h2.callTool('vibe_v5_status', {})
|
|
1260
|
+
const dir2 = paperDirOf(h2, 'institute')
|
|
1261
|
+
const meta2 = JSON.parse(readFileSync(join(dir2, 'paper.meta.json'), 'utf8'))
|
|
1262
|
+
assert(fin.ok === true && st2.paper.status === 'finalized' && st2.paper.compile === 'failed',
|
|
1263
|
+
'★ a persistent compile failure DEGRADES (finalisation still succeeds, compile=failed) (' + st2.paper.compile + ')')
|
|
1264
|
+
assert(existsSync(join(dir2, 'paper.tex')) && existsSync(join(dir2, 'paper.md')) && !existsSync(join(dir2, 'paper.pdf')),
|
|
1265
|
+
'tex+md are kept and no bogus pdf is written')
|
|
1266
|
+
assert(/\\title\{/.test(readFileSync(join(dir2, 'paper.tex'), 'utf8')),
|
|
1267
|
+
'★ the DELIVERED tex is the canonical generated one, not the minimal repair variant used by the last attempt')
|
|
1268
|
+
assert((meta2.compile.attempts || []).length === 4, '★ the retry plan is CAPPED at 4 attempts (full → nonstopmode → stripped/engine-swap → minimal template)')
|
|
1269
|
+
assert((meta2.warnings || []).join(' ').indexOf('编译失败') !== -1, 'the failure is reported as a warning in the meta')
|
|
1270
|
+
// Engine swap: the first installed engine fails, so attempt 3 (the NEXT engine) succeeds.
|
|
1271
|
+
const fake3 = fakeLatex({ installed: ['xelatex', 'pdflatex'], failEngines: ['xelatex'] })
|
|
1272
|
+
const h3 = makeHost({ pluginModule, subprocess: fake3 })
|
|
1273
|
+
await h3.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1274
|
+
await h3.settleSpawns()
|
|
1275
|
+
await h3.callTool('vibe_v5_paper', { format: 'both' })
|
|
1276
|
+
await drivePaper(h3)
|
|
1277
|
+
const dir3 = paperDirOf(h3, 'institute')
|
|
1278
|
+
const meta3 = JSON.parse(readFileSync(join(dir3, 'paper.meta.json'), 'utf8'))
|
|
1279
|
+
assert(meta3.compile.status === 'compiled' && meta3.compile.engine === 'pdflatex' && (meta3.compile.attempts || []).length === 3,
|
|
1280
|
+
'★ engine swap: xelatex fails twice, pdflatex succeeds on the third attempt (' + JSON.stringify({ e: meta3.compile.engine, n: (meta3.compile.attempts || []).length }) + ')')
|
|
1281
|
+
// paperFormat=md with paperCompilePdf=true must skip compilation SILENTLY (no "missing tex"
|
|
1282
|
+
// warning) — docs/final-paper.md §8.
|
|
1283
|
+
const hm = makeHost({ pluginModule, subprocess: fakeLatex({ installed: ['xelatex'] }) })
|
|
1284
|
+
await hm.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1285
|
+
await hm.settleSpawns()
|
|
1286
|
+
await hm.callTool('vibe_v5_paper', { format: 'md' })
|
|
1287
|
+
await drivePaper(hm)
|
|
1288
|
+
const dm = paperDirOf(hm, 'institute')
|
|
1289
|
+
const metam = JSON.parse(readFileSync(join(dm, 'paper.meta.json'), 'utf8'))
|
|
1290
|
+
assert(existsSync(join(dm, 'paper.md')) && !existsSync(join(dm, 'paper.tex')) && !existsSync(join(dm, 'paper.pdf')),
|
|
1291
|
+
'paperFormat=md delivers md only (no tex, no pdf)')
|
|
1292
|
+
assert(metam.compile.status === 'skipped' && !/编译|tex/.test((metam.warnings || []).join(' ')),
|
|
1293
|
+
'★ paperFormat=md skips compilation silently (no missing-tex warning): ' + JSON.stringify(metam.warnings))
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// ---------- 28. office-only surface + id normalisation ----------
|
|
1297
|
+
console.log('\n[28] the final-paper surface is office-only and its directory id cannot escape Paper/')
|
|
1298
|
+
{
|
|
1299
|
+
const h = makeHost({ pluginModule })
|
|
1300
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1301
|
+
await h.settleSpawns()
|
|
1302
|
+
const member = h.childAgent(h.childOf('r-1'))
|
|
1303
|
+
const asMember = await h.callTool('vibe_v5_paper', {}, member)
|
|
1304
|
+
assert(asMember.ok === false && asMember.code === 'V5_NOT_OFFICE', 'a member cannot start the paper (' + JSON.stringify(asMember).slice(0, 100) + ')')
|
|
1305
|
+
const finAsMember = await h.callTool('vibe_v5_finalize_paper', { decision: 'deliverable', note: 'x' }, member)
|
|
1306
|
+
assert(finAsMember.ok === false && finAsMember.code === 'V5_NOT_OFFICE', 'a member cannot finalise the paper')
|
|
1307
|
+
// A traversal-shaped institute name must never become a path in the paper id.
|
|
1308
|
+
const h2 = makeHost({ pluginModule })
|
|
1309
|
+
await h2.callTool('vibe_v5_configure', { institute: 'evil/../x' })
|
|
1310
|
+
await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1311
|
+
await h2.settleSpawns()
|
|
1312
|
+
await h2.callTool('vibe_v5_paper', {})
|
|
1313
|
+
await drivePaper(h2)
|
|
1314
|
+
const p2 = (await h2.callTool('vibe_v5_status', {})).paper
|
|
1315
|
+
const id = String(p2.dir || '').replace(/^Paper\//, '').replace(/\/$/, '')
|
|
1316
|
+
assert(id.length > 0 && id.indexOf('/') === -1 && id.indexOf('\\') === -1 && id !== '.' && id !== '..',
|
|
1317
|
+
'★ the paper directory id is ONE normalised name (never a path): ' + JSON.stringify(id))
|
|
1318
|
+
assert(existsSync(join(h2.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'x', 'Paper', id, 'paper.log.md')),
|
|
1319
|
+
'the paper landed under Paper/<id>/ inside the resolved institute tree')
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// ---------- 29. /v5 paper: overrides, force, kind:error; finalPaper=false gates AUTO ----------
|
|
1323
|
+
console.log('\n[29] /v5 paper: one-shot overrides, kind:error, and finalPaper=false')
|
|
1324
|
+
{
|
|
1325
|
+
const h = makeHost({ pluginModule })
|
|
1326
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1327
|
+
await h.settleSpawns()
|
|
1328
|
+
const cmd = h.commandRegs.find(c => c.name === 'v5')
|
|
1329
|
+
assert(!!cmd, 'the /v5 command is registered')
|
|
1330
|
+
const bad = await cmd.handler({ agent: h.ROOT, rawInput: 'paper bogus=1' })
|
|
1331
|
+
assert(bad.kind === 'error', '★ an unknown paper option is a FAILED command (kind:error): ' + String(bad.text).slice(0, 100))
|
|
1332
|
+
const memberCall = await cmd.handler({ agent: h.childAgent(h.childOf('acad')), rawInput: 'paper' })
|
|
1333
|
+
assert(memberCall.kind === 'error', 'a member child cannot drive the /v5 control line')
|
|
1334
|
+
const okCmd = await cmd.handler({ agent: h.ROOT, rawInput: 'paper lang=en format=tex editor=office' })
|
|
1335
|
+
assert(okCmd.kind === 'success', 'the paper subcommand succeeds: ' + String(okCmd.text).slice(0, 120))
|
|
1336
|
+
const st = await h.callTool('vibe_v5_status', {})
|
|
1337
|
+
assert(st.paper && st.paper.lang === 'en' && st.paper.format === 'tex' && st.paper.editor === 'office',
|
|
1338
|
+
'★ the command overrides language/format/editor FOR THIS PAPER (' + JSON.stringify({ l: st.paper.lang, f: st.paper.format, e: st.paper.editor }) + ')')
|
|
1339
|
+
assert(st.params.paperLanguage === 'zh' && st.params.paperEditor === 'academician',
|
|
1340
|
+
'the one-shot overrides do NOT rewrite the persisted params')
|
|
1341
|
+
// finalPaper=false: the AUTO trigger is off, the run concludes immediately, the manual path says so.
|
|
1342
|
+
const h2 = makeHost({ pluginModule })
|
|
1343
|
+
await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1344
|
+
await h2.settleSpawns()
|
|
1345
|
+
await h2.callTool('vibe_v5_set', { finalPaper: false })
|
|
1346
|
+
h2.solvePlan = true
|
|
1347
|
+
await h2.callTool('vibe_v5_meeting', { agenda: '是否已解决原问题?', kind: 'solve-vote' })
|
|
1348
|
+
await h2.drain(24)
|
|
1349
|
+
const s2 = await h2.callTool('vibe_v5_status', {})
|
|
1350
|
+
assert(s2.autoDone === true && !s2.paper, '★ finalPaper=false: the run concludes WITHOUT starting a paper (' + JSON.stringify({ autoDone: s2.autoDone, paper: s2.paper }) + ')')
|
|
1351
|
+
const manual = await h2.callTool('vibe_v5_paper', {})
|
|
1352
|
+
assert(manual.ok === true && manual.autoDisabled === true && /自动已关闭/.test(String(manual.note)),
|
|
1353
|
+
'the manual command still works when finalPaper=false and says the automatic path is off (' + JSON.stringify(manual).slice(0, 220) + ')')
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// ---------- 30. unanimity is required; the round cap records the disagreement ----------
|
|
1357
|
+
console.log('\n[30] unanimity: an objection does NOT finalise, the cap records the disagreement in the appendix')
|
|
1358
|
+
{
|
|
1359
|
+
const h = makeHost({ pluginModule })
|
|
1360
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1361
|
+
await h.settleSpawns()
|
|
1362
|
+
const objecting = () => ({ paper_review: { deliverable: false, comments: '证据不足,需补充' } })
|
|
1363
|
+
await h.callTool('vibe_v5_paper', {})
|
|
1364
|
+
// Round 1 stopped after its write+review: the objection must NOT finalise anything.
|
|
1365
|
+
await drivePaper(h, { max: 2, review: () => objecting() })
|
|
1366
|
+
let st = await h.callTool('vibe_v5_status', {})
|
|
1367
|
+
assert(st.paper && st.paper.status === 'writing' && st.paper.round === 2,
|
|
1368
|
+
'★ an objection does NOT finalise: the flow iterates to the next round (status=' + st.paper.status + ', round=' + st.paper.round + ')')
|
|
1369
|
+
assert(st.paper.disagreement.length >= 1, 'the objection is recorded as a disagreement')
|
|
1370
|
+
// Keep objecting; at the cap the editor (academician) is asked and asks to revise again — the
|
|
1371
|
+
// spec says a cap is reached, a warning is recorded and the disagreement goes into the appendix.
|
|
1372
|
+
await drivePaper(h, {
|
|
1373
|
+
review: () => objecting(),
|
|
1374
|
+
final: () => ({ paper_final: { decision: 'revise', note: '仍有未达成一致的意见' } }),
|
|
1375
|
+
})
|
|
1376
|
+
const st2 = await h.callTool('vibe_v5_status', {})
|
|
1377
|
+
assert(st2.paper && st2.paper.status === 'finalized' && st2.paper.forcedAfterCap === true,
|
|
1378
|
+
'★ the round cap was enforced, the disagreement recorded, and the editor finalised (' + JSON.stringify({ s: st2.paper && st2.paper.status, cap: st2.paper && st2.paper.forcedAfterCap }) + ')')
|
|
1379
|
+
assert((st2.paper.warnings || []).join(' ').indexOf('上限') !== -1, 'the cap produced a WARNING (' + JSON.stringify(st2.paper.warnings) + ')')
|
|
1380
|
+
const md = readFileSync(join(paperDirOf(h, 'institute'), 'paper.md'), 'utf8')
|
|
1381
|
+
assert(md.indexOf('分歧记录') !== -1 && md.indexOf('证据不足,需补充') !== -1,
|
|
1382
|
+
'★ the unresolved disagreement is written into the APPENDIX of the paper')
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// ---------- 31. the DEFAULT auto trigger: the paper phase runs BEFORE completion ----------
|
|
1386
|
+
console.log('\n[31] a unanimous solve vote with finalPaper=true enters the paper phase BEFORE completion')
|
|
1387
|
+
{
|
|
1388
|
+
// The mutant guard: deleting the auto-trigger branch in `checkSolved`
|
|
1389
|
+
// (`if (params.finalPaper !== false)` -> `if (false)`) used to leave this whole suite green,
|
|
1390
|
+
// because every other paper case drives the flow through the MANUAL vibe_v5_paper tool and
|
|
1391
|
+
// the only auto assertion was "finalPaper=false => no paper" (which the mutant satisfies).
|
|
1392
|
+
// This case pins the real contract: the default path must NOT complete the run until the
|
|
1393
|
+
// paper is finalised, and the run must be observably alive with a paper in progress.
|
|
1394
|
+
const h = makeHost({ pluginModule })
|
|
1395
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1396
|
+
await h.settleSpawns()
|
|
1397
|
+
await h.callTool('vibe_v5_set', { activityTimeoutMs: 50 })
|
|
1398
|
+
h.solvePlan = true
|
|
1399
|
+
await h.callTool('vibe_v5_meeting', { agenda: '是否已解决原问题?', kind: 'solve-vote' })
|
|
1400
|
+
await h.drain(24)
|
|
1401
|
+
const mid = await h.callTool('vibe_v5_status', {})
|
|
1402
|
+
assert(mid.autoDone === false,
|
|
1403
|
+
'★ a unanimous solve vote with finalPaper=true does NOT complete the run immediately (got autoDone=' + mid.autoDone + ')')
|
|
1404
|
+
const p0 = mid.paper || {}
|
|
1405
|
+
assert(!!mid.paper && p0.status !== 'finalized',
|
|
1406
|
+
'★ ... it ENTERS the paper phase instead (' + JSON.stringify({ s: p0.status, e: p0.editor }) + ')')
|
|
1407
|
+
assert(p0.editor === 'academician' && p0.completesRun === true,
|
|
1408
|
+
'the automatic run is edited by the wakeable academician and is marked as completing the run (' + JSON.stringify({ e: p0.editor, c: p0.completesRun }) + ')')
|
|
1409
|
+
assert(mid.running === true, 'the run stays ALIVE while the paper phase runs (the machinery would refuse a concluded institute)')
|
|
1410
|
+
const capture = []
|
|
1411
|
+
await drivePaper(h, { capture })
|
|
1412
|
+
let done = null
|
|
1413
|
+
for (let i = 0; i < 40; i++) {
|
|
1414
|
+
done = await h.callTool('vibe_v5_status', {})
|
|
1415
|
+
if (done.autoDone) break
|
|
1416
|
+
await drivePaper(h, { max: 4, capture })
|
|
1417
|
+
await sleep(20)
|
|
1418
|
+
}
|
|
1419
|
+
assert(done.autoDone === true && done.running === false,
|
|
1420
|
+
'★ the run is marked complete only AFTER the paper finalised (' + JSON.stringify({ a: done.autoDone, r: done.running }) + ')')
|
|
1421
|
+
assert(done.paper && done.paper.status === 'finalized', 'the paper reached the finalized stage (' + JSON.stringify(done.paper && done.paper.status) + ')')
|
|
1422
|
+
assert(existsSync(join(paperDirOf(h, 'institute'), 'paper.md')), 'the automatic path delivered Paper/<id>/paper.md')
|
|
1423
|
+
// The members' own prompts carry the evidence-only clause (never invent content).
|
|
1424
|
+
const write = capture.find(c => c.kind === 'write')
|
|
1425
|
+
assert(!!write && /不得编造/.test(write.prompt) && /只写你自己库里已有证据支撑/.test(write.prompt),
|
|
1426
|
+
'★ the paper write prompt says only-evidence/no-fabrication: ' + JSON.stringify(write && write.prompt.slice(0, 120)))
|
|
1427
|
+
assert(!!write && /未决 \/ 被否证的条目必须显式标注/.test(write.prompt), 'the prompt requires undecided/refuted items to be marked explicitly')
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// ---------- 32. a run that has NOT closed writes no Paper/ directory ----------
|
|
1431
|
+
console.log('\n[32] a run that has NOT closed writes NO Paper/ directory')
|
|
1432
|
+
{
|
|
1433
|
+
const h = makeHost({ pluginModule })
|
|
1434
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1435
|
+
await h.settleSpawns()
|
|
1436
|
+
// Real, non-closing work: one verification concludes, and the solve vote is NOT unanimous.
|
|
1437
|
+
await h.callTool('vibe_v5_record_proposition', { id: 'p-not-closed', statement: '未收口就不得产出论文目录', value: 0.8, motive: '负用例', p: 1 }, h.childAgent(h.childOf('r-1')))
|
|
1438
|
+
await h.callTool('vibe_v5_propose_verify', { target: 'p-not-closed', kind: 'proposition', reason: '负用例' }, h.childAgent(h.childOf('r-1')))
|
|
1439
|
+
h.plannedVotes = new Map([['acad', 1], ['r-1', 1]])
|
|
1440
|
+
await h.drain(12)
|
|
1441
|
+
h.solvePlan = false
|
|
1442
|
+
await h.callTool('vibe_v5_meeting', { agenda: '是否已解决?(未达成一致)', kind: 'solve-vote' })
|
|
1443
|
+
await h.drain(16)
|
|
1444
|
+
const st = await h.callTool('vibe_v5_status', {})
|
|
1445
|
+
assert(st.autoDone === false && !st.paper, 'the run is still open and no paper phase started (' + JSON.stringify({ a: st.autoDone, p: st.paper }) + ')')
|
|
1446
|
+
const paperRoot = join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Paper')
|
|
1447
|
+
assert(!existsSync(paperRoot), '★ a run that has NOT closed writes no Paper/ directory at all')
|
|
1448
|
+
const rel = await h.callTool('vibe_v5_report', {})
|
|
1449
|
+
assert(String(rel.report || '').indexOf('最终论文') !== -1 && String(rel.report || '').indexOf('尚未开始') !== -1,
|
|
1450
|
+
'the report tells the operator the paper has not started yet')
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// ---------- 33. an existing paper.pdf is never clobbered ----------
|
|
1454
|
+
console.log('\n[33] an existing Paper/<id>/paper.pdf is never overwritten or deleted')
|
|
1455
|
+
{
|
|
1456
|
+
const PRE = '%PDF-1.4 PRE-EXISTING DELIVERY\n'
|
|
1457
|
+
// (a) a compiler that WOULD succeed: the existing pdf must make the flow skip compilation.
|
|
1458
|
+
const fake = fakeLatex({ installed: ['xelatex'] })
|
|
1459
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1460
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1461
|
+
await h.settleSpawns()
|
|
1462
|
+
const dir = paperDirOf(h, 'institute')
|
|
1463
|
+
mkdirSync(dir, { recursive: true })
|
|
1464
|
+
writeFileSync(join(dir, 'paper.pdf'), PRE, 'utf8')
|
|
1465
|
+
await h.callTool('vibe_v5_paper', { format: 'both' })
|
|
1466
|
+
await drivePaper(h)
|
|
1467
|
+
assert(readFileSync(join(dir, 'paper.pdf'), 'utf8') === PRE, '★ the pre-existing pdf is byte-identical after the run (never overwritten)')
|
|
1468
|
+
assert(fake.calls.length === 0, '★ compilation was SKIPPED entirely because a pdf already existed (never clobber)')
|
|
1469
|
+
const meta = JSON.parse(readFileSync(join(dir, 'paper.meta.json'), 'utf8'))
|
|
1470
|
+
assert(meta.compile.status === 'kept-existing', 'the meta records compile=kept-existing (' + meta.compile.status + ')')
|
|
1471
|
+
// (b) a compiler that ALWAYS FAILS: still skipped, so a failing toolchain cannot destroy it.
|
|
1472
|
+
const fake2 = fakeLatex({ installed: ['xelatex'], alwaysFail: true })
|
|
1473
|
+
const h2 = makeHost({ pluginModule, subprocess: fake2 })
|
|
1474
|
+
await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1475
|
+
await h2.settleSpawns()
|
|
1476
|
+
const dir2 = paperDirOf(h2, 'institute')
|
|
1477
|
+
mkdirSync(dir2, { recursive: true })
|
|
1478
|
+
writeFileSync(join(dir2, 'paper.pdf'), PRE, 'utf8')
|
|
1479
|
+
await h2.callTool('vibe_v5_paper', { format: 'both' })
|
|
1480
|
+
await drivePaper(h2)
|
|
1481
|
+
assert(readFileSync(join(dir2, 'paper.pdf'), 'utf8') === PRE, '★ even an always-failing toolchain leaves the existing pdf untouched')
|
|
1482
|
+
assert(fake2.calls.length === 0, 'no compiler process was launched for the already-delivered pdf')
|
|
1483
|
+
}
|
|
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
|
+
|
|
941
1861
|
console.log('')
|
|
942
1862
|
console.log('passed=' + passed + ' failed=' + failed)
|
|
943
1863
|
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|