dsh-vibe-math 2.5.0 → 2.6.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 +41 -2
- package/README.md +41 -2
- package/cordis.patch.yml +22 -8
- package/docs/AUDIT-CHECKLIST.md +2 -2
- package/docs/final-paper.md +171 -0
- package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -0
- package/docs/test-timing.md +8 -4
- package/package.json +5 -3
- package/prompt-corpus-persona/persona-corpus.json +13 -13
- package/prompt-corpus-persona/persona-corpus.md +49 -21
- package/prompt-corpus-v2/formal-verify-v2.json +12 -0
- package/prompt-corpus-v2/formal-verify-v2.md +144 -53
- package/prompt-corpus-v3/formal-verify-v3.json +36 -0
- package/prompt-corpus-v3/formal-verify-v3.md +405 -78
- package/tests/audit-persona-sensitivity.mjs +60 -7
- package/tests/audit-persona-surface.test.mjs +16 -7
- package/tests/e2e-v5-round2.test.mjs +471 -1
- package/tests/formal-verify-v2.test.mjs +3 -0
- package/tests/formal-verify-v3.test.mjs +4 -0
- package/tests/prompt-v5-integrity.test.mjs +9 -3
- package/tests/selfdrive-v5.mjs +55 -4
- package/vibe-math-v2/agent.cordis.yml +14 -4
- package/vibe-math-v2/vibe-math-v2.js +819 -11
- package/vibe-math-v3/agent.cordis.yml +10 -4
- package/vibe-math-v3/vibe-math-v3.js +800 -11
- package/vibe-math-v4/agent.cordis.yml +12 -4
- package/vibe-math-v4/vibe-math-v4.js +793 -8
- package/vibe-math-v5/agent.cordis.yml +8 -4
- package/vibe-math-v5/vibe-math-v5.js +1116 -8
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +70 -1
|
@@ -16,7 +16,7 @@
|
|
|
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
20
|
import { tmpdir } from 'node:os'
|
|
21
21
|
import { join, dirname, isAbsolute } from 'node:path'
|
|
22
22
|
|
|
@@ -70,6 +70,9 @@ function makeHost(opts) {
|
|
|
70
70
|
if (name === 'sandboxPolicy') return undefined
|
|
71
71
|
if (name === 'compaction') return o.compaction
|
|
72
72
|
if (name === 'subprocess') {
|
|
73
|
+
// o.subprocess injects a FAKE LaTeX toolchain for the final-paper tests (the plugin
|
|
74
|
+
// detects/compiles through this exact service, the same seam the Lean tests use).
|
|
75
|
+
if (o.subprocess) return o.subprocess
|
|
73
76
|
return {
|
|
74
77
|
async spawn({ argv }) {
|
|
75
78
|
const script = argv[argv.length - 1] || ''
|
|
@@ -197,6 +200,89 @@ function makeHost(opts) {
|
|
|
197
200
|
const pluginModule = await import(PLUGIN.href + '?t=' + Date.now())
|
|
198
201
|
const PROBLEM = '证明素数有无穷多个'
|
|
199
202
|
|
|
203
|
+
// ── final-paper test helpers ────────────────────────────────────────────────
|
|
204
|
+
// A FAKE LaTeX toolchain (docs/final-paper.md §10: "临时目录里放一个假的 xelatex/latexmk 脚本,或注入 runner").
|
|
205
|
+
// It is injected through the SAME `subprocess` service the plugin detects and compiles with, so
|
|
206
|
+
// the plugin's real detection/compile/repair code runs unchanged; only the toolchain is fake.
|
|
207
|
+
// `installed` — which engine names resolve (others throw, like a missing binary)
|
|
208
|
+
// `failEngines` — engines whose process exits 1
|
|
209
|
+
// `failOn` — a RegExp: if the tex on disk matches, the run exits 1 (drives the repair)
|
|
210
|
+
// `alwaysFail` — every run exits 1 (drives the degrade path)
|
|
211
|
+
function fakeLatex(opts) {
|
|
212
|
+
const o = opts || {}
|
|
213
|
+
const calls = []
|
|
214
|
+
const installed = o.installed || ['xelatex', 'pdflatex']
|
|
215
|
+
return {
|
|
216
|
+
calls,
|
|
217
|
+
async resolveExecutable(name) {
|
|
218
|
+
if (installed.indexOf(name) === -1) throw new Error('ENOENT: ' + name)
|
|
219
|
+
return 'C:/fake/' + name
|
|
220
|
+
},
|
|
221
|
+
spawn(spec) {
|
|
222
|
+
const argv = spec.argv || []
|
|
223
|
+
const name = String(argv[0] || '').split(/[\\/]/).pop()
|
|
224
|
+
const dir = String(spec.cwd || '')
|
|
225
|
+
let tex = ''
|
|
226
|
+
try { tex = readFileSync(join(dir, 'paper.tex'), 'utf8') } catch (e) { /* first attempt may race */ }
|
|
227
|
+
calls.push({ name, args: argv.slice(1), dir, tex })
|
|
228
|
+
let ok = true
|
|
229
|
+
if (o.failEngines && o.failEngines.indexOf(name) !== -1) ok = false
|
|
230
|
+
if (o.failOn && o.failOn.test(tex)) ok = false
|
|
231
|
+
if (o.alwaysFail) ok = false
|
|
232
|
+
if (ok) {
|
|
233
|
+
try { mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, 'paper.pdf'), '%PDF-1.4 fake\n', 'utf8') } catch (e) { ok = false }
|
|
234
|
+
}
|
|
235
|
+
return { done: Promise.resolve({ exitCode: ok ? 0 : 1 }), terminate() {} }
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const paperKindOf = (p) => /【最终论文·撰写/.test(p) ? 'write'
|
|
240
|
+
: /【最终论文·互审/.test(p) ? 'review'
|
|
241
|
+
: /【最终论文·定稿/.test(p) ? 'final' : ''
|
|
242
|
+
const paperDirOf = (h, id) => join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Paper', id || 'institute')
|
|
243
|
+
function listFilesUnder(root, rel) {
|
|
244
|
+
const out = []
|
|
245
|
+
const base = rel === undefined ? '' : rel
|
|
246
|
+
for (const e of readdirSync(root, { withFileTypes: true })) {
|
|
247
|
+
const r = base ? base + '/' + e.name : e.name
|
|
248
|
+
if (e.isDirectory()) out.push(...listFilesUnder(join(root, e.name), r))
|
|
249
|
+
else out.push(r)
|
|
250
|
+
}
|
|
251
|
+
return out
|
|
252
|
+
}
|
|
253
|
+
// Answer the paper phase's own prompts until the flow stops asking: each member writes its
|
|
254
|
+
// part, cross-reviews another member's part, and the editor finalises. `opts.write/review/final`
|
|
255
|
+
// override the default (deliverable) replies, which is how the objection/cap paths are driven.
|
|
256
|
+
async function drivePaper(h, opts) {
|
|
257
|
+
const o = opts || {}
|
|
258
|
+
let steps = 0, idle = 0
|
|
259
|
+
while (steps < (o.max || 40) && idle < 8) {
|
|
260
|
+
const i = h.wakes.findIndex(w => paperKindOf((w.blocks && w.blocks[0] && w.blocks[0].text) || ''))
|
|
261
|
+
if (i === -1) { idle++; await sleep(25); continue }
|
|
262
|
+
idle = 0
|
|
263
|
+
const w = h.wakes.splice(i, 1)[0]
|
|
264
|
+
const prompt = (w.blocks && w.blocks[0] && w.blocks[0].text) || ''
|
|
265
|
+
const kind = paperKindOf(prompt)
|
|
266
|
+
const who = h.labelOf(w.childId)
|
|
267
|
+
let reply
|
|
268
|
+
if (kind === 'write') {
|
|
269
|
+
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'] } })
|
|
270
|
+
} else if (kind === 'review') {
|
|
271
|
+
const om = /待审部分(([^)]*))/.exec(prompt)
|
|
272
|
+
reply = o.review ? o.review(who, om ? om[1] : '', prompt) : ({ paper_review: { of: om ? om[1] : '', deliverable: true, comments: who + ':证据与表决记录一致,可交付。' } })
|
|
273
|
+
} else {
|
|
274
|
+
reply = o.final ? o.final(who, prompt) : ({ paper_final: { decision: 'deliverable', note: '已核对合并稿与互审意见,统一术语与符号。', conclusion: '题设范围内结论成立。' } })
|
|
275
|
+
}
|
|
276
|
+
// `capture` lets a case assert on the TEXT the member really received (the evidence-only
|
|
277
|
+
// clause lives in the prompt, not only in the plugin source).
|
|
278
|
+
if (o.capture) o.capture.push({ kind, who, prompt })
|
|
279
|
+
h.fireEnd(w.childId, reply)
|
|
280
|
+
steps++
|
|
281
|
+
await sleep(20)
|
|
282
|
+
}
|
|
283
|
+
return steps
|
|
284
|
+
}
|
|
285
|
+
|
|
200
286
|
// The durable authority is the hardened JSON file. A commit is applied to the in-memory
|
|
201
287
|
// snapshot immediately but written through a deferred per-file chain, so a test that wants to
|
|
202
288
|
// observe "this was committed" must poll the FILE — reading the session log is no longer possible
|
|
@@ -938,6 +1024,390 @@ console.log('\n[22] configure/start/resume/pause/stop/set belong to the PROVABLE
|
|
|
938
1024
|
assert(finalStop.ok === true, 'the office can still stop it (' + JSON.stringify(finalStop).slice(0, 90) + ')')
|
|
939
1025
|
}
|
|
940
1026
|
|
|
1027
|
+
// ---------- 23. final-paper params: closed schema, defaults, explicit coercion ----------
|
|
1028
|
+
console.log('\n[23] final-paper params: closed schema, defaults and explicit coercion')
|
|
1029
|
+
{
|
|
1030
|
+
const h = makeHost({ pluginModule })
|
|
1031
|
+
const setSpec = h.toolRegs.find(t => t.name === 'vibe_v5_set')
|
|
1032
|
+
const keys = ['finalPaper', 'paperFormat', 'paperLanguage', 'paperCompilePdf', 'paperEditor', 'paperLatexCommand']
|
|
1033
|
+
assert(!!setSpec && keys.every(k => Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k)),
|
|
1034
|
+
'★ vibe_v5_set advertises all six final-paper keys (the schema is closed, so an unlisted key is unreachable)')
|
|
1035
|
+
const props = setSpec.parameters.properties
|
|
1036
|
+
assert(JSON.stringify(props.paperFormat.enum) === JSON.stringify(['both', 'md', 'tex']) &&
|
|
1037
|
+
JSON.stringify(props.paperLanguage.enum) === JSON.stringify(['zh', 'en']) &&
|
|
1038
|
+
JSON.stringify(props.paperEditor.enum) === JSON.stringify(['office', 'academician']),
|
|
1039
|
+
'the three enums are narrowed in the schema (a typo must not become a fourth mode)')
|
|
1040
|
+
const s0 = (await h.callTool('vibe_v5_status', {})).params
|
|
1041
|
+
assert(s0.finalPaper === true && s0.paperFormat === 'both' && s0.paperLanguage === 'zh' && s0.paperCompilePdf === true &&
|
|
1042
|
+
s0.paperEditor === 'academician' && s0.paperLatexCommand === '',
|
|
1043
|
+
'the documented defaults are live (' + JSON.stringify({ f: s0.finalPaper, fmt: s0.paperFormat, lang: s0.paperLanguage, pdf: s0.paperCompilePdf, ed: s0.paperEditor }) + ')')
|
|
1044
|
+
const r = await h.callTool('vibe_v5_set', { finalPaper: 'false', paperFormat: 'bogus', paperLanguage: 'EN', paperEditor: 'root', paperCompilePdf: 1, paperLatexCommand: 'lualatex' })
|
|
1045
|
+
assert(r.ok === true && r.params.finalPaper === false, "★ finalPaper:'false' is coerced to false (v2 §B: an unknown spelling must not stay truthy)")
|
|
1046
|
+
assert(r.params.paperFormat === 'both', 'an unknown paperFormat falls back to both')
|
|
1047
|
+
assert(r.params.paperLanguage === 'en' && r.params.paperEditor === 'office', 'enums are case-insensitive and legacy aliases resolve (EN → en, root → office)')
|
|
1048
|
+
assert(r.params.paperCompilePdf === true && r.params.paperLatexCommand === 'lualatex', 'paperCompilePdf accepts 1 as true; paperLatexCommand is stored')
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// ---------- 24. the team flow with the OFFICE as editor (+ the consultation gate) ----------
|
|
1052
|
+
console.log('\n[24] manual paper, office editor: parts → cross-review → consultation → finalise')
|
|
1053
|
+
{
|
|
1054
|
+
const h = makeHost({ pluginModule })
|
|
1055
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1056
|
+
await h.settleSpawns()
|
|
1057
|
+
// ONE verified object, so section 4 has real evidence to index.
|
|
1058
|
+
await h.callTool('vibe_v5_record_proposition', { id: 'p-paper', statement: '最终论文只整理已有证据,不得编造', value: 0.9, motive: '论文证据', p: 1 }, h.childAgent(h.childOf('r-1')))
|
|
1059
|
+
await h.callTool('vibe_v5_propose_verify', { target: 'p-paper', kind: 'proposition', reason: '论文证据' }, h.childAgent(h.childOf('r-1')))
|
|
1060
|
+
h.plannedVotes = new Map([['acad', 1], ['r-1', 1]])
|
|
1061
|
+
await h.drain(10)
|
|
1062
|
+
const sv = await h.callTool('vibe_v5_status', {})
|
|
1063
|
+
assert(sv.verified.indexOf('p-paper') !== -1, 'precondition: one object is verified (the paper needs evidence)')
|
|
1064
|
+
assert(sv.paper === null, 'NEGATIVE: a run that has NOT concluded does not auto-start a paper')
|
|
1065
|
+
// A second object is left UNDECIDED (both voters abstain), so section 8 has an unresolved item
|
|
1066
|
+
// that MUST be marked as such.
|
|
1067
|
+
await h.callTool('vibe_v5_set', { verdictMaxRounds: 1 })
|
|
1068
|
+
await h.callTool('vibe_v5_record_proposition', { id: 'p-open', statement: '仍未定论的对象必须显式标注', value: 0.5, motive: '未决项', p: 0.5 }, h.childAgent(h.childOf('r-1')))
|
|
1069
|
+
await h.callTool('vibe_v5_propose_verify', { target: 'p-open', kind: 'proposition', reason: '未决项' }, h.childAgent(h.childOf('r-1')))
|
|
1070
|
+
h.plannedVotes = new Map([['acad', 0.5], ['r-1', 0.5]])
|
|
1071
|
+
await h.drain(10)
|
|
1072
|
+
const su = await h.callTool('vibe_v5_status', {})
|
|
1073
|
+
assert(su.undecided.indexOf('p-open') !== -1, 'precondition: one object is recorded as 未定论 (got ' + JSON.stringify(su.undecided) + ')')
|
|
1074
|
+
h.plannedVotes = new Map()
|
|
1075
|
+
const started = await h.callTool('vibe_v5_paper', { lang: 'en', format: 'both', editor: 'office' })
|
|
1076
|
+
assert(started.ok === true && started.started === true && started.editor === 'office',
|
|
1077
|
+
'the manual paper starts with the office as its editor (' + JSON.stringify(started).slice(0, 130) + ')')
|
|
1078
|
+
await drivePaper(h)
|
|
1079
|
+
let st = await h.callTool('vibe_v5_status', {})
|
|
1080
|
+
assert(st.paper && st.paper.status === 'awaiting-editor' && st.paper.parts.length === 2 && st.paper.reviews.length === 2,
|
|
1081
|
+
'★ 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 }) + ')')
|
|
1082
|
+
const refused = await h.callTool('vibe_v5_finalize_paper', { decision: 'deliverable', note: 'x' })
|
|
1083
|
+
assert(refused.ok === false && refused.code === 'V5_PAPER_CONSULT_REQUIRED',
|
|
1084
|
+
'★ NEGATIVE: the office cannot finalise before consulting the institute (' + JSON.stringify(refused).slice(0, 140) + ')')
|
|
1085
|
+
const msg = await h.callTool('vibe_v5_message', { to: 'all', content: '请各自核对证据与结论,我们随后定稿。' })
|
|
1086
|
+
assert(msg.ok === true, 'the office message is delivered')
|
|
1087
|
+
await h.callTool('vibe_v5_meeting', { agenda: '定稿前审查:证据与结论是否一致', kind: 'sync' })
|
|
1088
|
+
await h.drain(12)
|
|
1089
|
+
st = await h.callTool('vibe_v5_status', {})
|
|
1090
|
+
assert(st.paper.consult.messages >= 1 && st.paper.consult.meetings >= 1,
|
|
1091
|
+
'★ the office consultation is RECORDED (messages=' + st.paper.consult.messages + ', meetings=' + st.paper.consult.meetings + ')')
|
|
1092
|
+
const fin = await h.callTool('vibe_v5_finalize_paper', { decision: 'deliverable', note: '已与全所逐条核对证据,术语与符号统一,结论与表决记录一致。', conclusion: '题设范围内结论成立。' })
|
|
1093
|
+
assert(fin.ok === true && fin.finalized === true, 'the office finalises after consulting (' + JSON.stringify(fin).slice(0, 150) + ')')
|
|
1094
|
+
assert(fin.compile === 'not-detected', '★ this machine has no LaTeX: the compile degrades to not-detected instead of failing (' + fin.compile + ')')
|
|
1095
|
+
const dir = paperDirOf(h, 'institute')
|
|
1096
|
+
const md = existsSync(join(dir, 'paper.md')) ? readFileSync(join(dir, 'paper.md'), 'utf8') : ''
|
|
1097
|
+
const tex = existsSync(join(dir, 'paper.tex')) ? readFileSync(join(dir, 'paper.tex'), 'utf8') : ''
|
|
1098
|
+
const meta = existsSync(join(dir, 'paper.meta.json')) ? JSON.parse(readFileSync(join(dir, 'paper.meta.json'), 'utf8')) : null
|
|
1099
|
+
const heads = (md.match(/^## \d+\. /gm) || []).length
|
|
1100
|
+
assert(heads === 9, '★ the md carries exactly the 9-section skeleton (got ' + heads + ')')
|
|
1101
|
+
assert(md.indexOf('p-paper') !== -1, '★ the verified proposition is listed with its evidence path')
|
|
1102
|
+
assert(md.indexOf('未定论:p-open') !== -1, '★ an undecided object is EXPLICITLY marked in the paper (never silently dropped or promoted)')
|
|
1103
|
+
assert(tex.indexOf('\\documentclass') !== -1 && /\\section\{1\. /.test(tex), '★ the tex version is generated from the same sections')
|
|
1104
|
+
assert(md.indexOf('已与全所逐条核对证据') !== -1, 'the office finalisation note is written into the paper')
|
|
1105
|
+
assert(!!meta && meta.editor === 'office' && meta.consultation.messages >= 1 && meta.consultation.meetings >= 1 && meta.compile.status === 'not-detected',
|
|
1106
|
+
'the meta records the editor, the consultation evidence and the compile result')
|
|
1107
|
+
assert(existsSync(join(dir, 'paper.log.md')), 'the paper log records who wrote/reviewed what')
|
|
1108
|
+
const instRoot = join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute')
|
|
1109
|
+
const stray = listFilesUnder(instRoot).filter(p => /(^|\/)paper\.(md|tex|pdf|meta\.json|log\.md)$/i.test(p) && p.indexOf('Paper/institute/') !== 0)
|
|
1110
|
+
assert(stray.length === 0, '★ every paper artifact lives under Paper/<id>/ only (no Verified//State/ pollution): ' + JSON.stringify(stray))
|
|
1111
|
+
assert((await h.callTool('vibe_v5_status', {})).autoDone === false, 'a MANUAL paper does not conclude the run')
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// ---------- 25. idempotency: a repeated trigger only fills missing artifacts ----------
|
|
1115
|
+
console.log('\n[25] paper idempotency: repeated triggers only fill missing artifacts')
|
|
1116
|
+
{
|
|
1117
|
+
const h = makeHost({ pluginModule })
|
|
1118
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1119
|
+
await h.settleSpawns()
|
|
1120
|
+
const first = await h.callTool('vibe_v5_paper', {})
|
|
1121
|
+
assert(first.ok === true && first.started === true, 'the single-member institute starts its paper')
|
|
1122
|
+
await drivePaper(h)
|
|
1123
|
+
const dir = paperDirOf(h, 'institute')
|
|
1124
|
+
const metaPath = join(dir, 'paper.meta.json')
|
|
1125
|
+
assert(existsSync(join(dir, 'paper.md')) && existsSync(metaPath), 'the first run delivered md + meta')
|
|
1126
|
+
const meta1 = readFileSync(metaPath, 'utf8')
|
|
1127
|
+
const again = await h.callTool('vibe_v5_paper', {})
|
|
1128
|
+
assert(again.ok === true && again.alreadyFinalized === true, 'a repeated trigger reports alreadyFinalized instead of re-running the team (' + JSON.stringify(again).slice(0, 120) + ')')
|
|
1129
|
+
assert(readFileSync(metaPath, 'utf8') === meta1, '★ paper.meta.json is NOT rewritten (finalizedAt/inputs untouched)')
|
|
1130
|
+
unlinkSync(join(dir, 'paper.md'))
|
|
1131
|
+
const refill = await h.callTool('vibe_v5_paper', {})
|
|
1132
|
+
assert(refill.ok === true && existsSync(join(dir, 'paper.md')), '★ the missing artifact is refilled (' + JSON.stringify(refill.refill) + ')')
|
|
1133
|
+
assert(readFileSync(metaPath, 'utf8') === meta1, 'the refill did not touch the settled meta either')
|
|
1134
|
+
const forced = await h.callTool('vibe_v5_paper', { force: true, reason: 'manual force' })
|
|
1135
|
+
assert(forced.ok === true && forced.started === true && !forced.alreadyFinalized,
|
|
1136
|
+
'★ force re-runs the whole team flow instead of refilling (' + JSON.stringify(forced).slice(0, 120) + ')')
|
|
1137
|
+
const stF = await h.callTool('vibe_v5_status', {})
|
|
1138
|
+
assert(stF.paper.status === 'writing' && stF.paper.round === 1, 'the forced run restarted at round 1')
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// ---------- 26. fake LaTeX: the success path produces paper.pdf ----------
|
|
1142
|
+
console.log('\n[26] fake LaTeX compiler: success path produces paper.pdf')
|
|
1143
|
+
{
|
|
1144
|
+
const fake = fakeLatex({ installed: ['xelatex', 'pdflatex'] })
|
|
1145
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1146
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1147
|
+
await h.settleSpawns()
|
|
1148
|
+
await h.callTool('vibe_v5_paper', { format: 'both' })
|
|
1149
|
+
await drivePaper(h)
|
|
1150
|
+
const st = await h.callTool('vibe_v5_status', {})
|
|
1151
|
+
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 }) + ')')
|
|
1152
|
+
const dir = paperDirOf(h, 'institute')
|
|
1153
|
+
assert(existsSync(join(dir, 'paper.pdf')), '★ paper.pdf was produced by the compiler subprocess inside Paper/<id>/')
|
|
1154
|
+
const meta = JSON.parse(readFileSync(join(dir, 'paper.meta.json'), 'utf8'))
|
|
1155
|
+
assert(meta.compile.status === 'compiled' && meta.compile.engine === 'xelatex' && meta.compile.attempts.length === 1,
|
|
1156
|
+
'the meta records compiled/xelatex on the FIRST attempt (' + JSON.stringify(meta.compile) + ')')
|
|
1157
|
+
assert(fake.calls.length >= 2 && fake.calls.every(c => c.args.indexOf('-interaction=nonstopmode') !== -1),
|
|
1158
|
+
'★ the engine ran twice with -interaction=nonstopmode (docs/final-paper.md §8)')
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// ---------- 27. fake LaTeX: a failing package is repaired, then it degrades ----------
|
|
1162
|
+
console.log('\n[27] fake LaTeX compiler: repair path and persistent-failure degradation')
|
|
1163
|
+
{
|
|
1164
|
+
// The fake refuses `\\usepackage[hidelinks]{hyperref}`; the repair pass strips optional
|
|
1165
|
+
// packages, so attempt 3 succeeds (attempts 1-2 fail on the full tex).
|
|
1166
|
+
const fake = fakeLatex({ installed: ['xelatex'], failOn: /\\usepackage\[hidelinks\]\{hyperref\}/ })
|
|
1167
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1168
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1169
|
+
await h.settleSpawns()
|
|
1170
|
+
await h.callTool('vibe_v5_paper', { format: 'both' })
|
|
1171
|
+
await drivePaper(h)
|
|
1172
|
+
const dir = paperDirOf(h, 'institute')
|
|
1173
|
+
const meta = JSON.parse(readFileSync(join(dir, 'paper.meta.json'), 'utf8'))
|
|
1174
|
+
const at = meta.compile.attempts || []
|
|
1175
|
+
assert(meta.compile.status === 'compiled' && existsSync(join(dir, 'paper.pdf')), '★ the repair retry turned a failing compile into a PDF')
|
|
1176
|
+
assert(at.length === 3 && at[0].ok === false && at[1].ok === false && at[2].ok === true && at[2].label.indexOf('stripped') !== -1,
|
|
1177
|
+
'★ attempts: full FAIL → nonstopmode rerun FAIL → stripped-package SUCCESS (' + JSON.stringify(at.map(a => a.label + ':' + a.ok)) + ')')
|
|
1178
|
+
// A compiler that always fails must degrade, not throw, and must not block finalisation.
|
|
1179
|
+
const fake2 = fakeLatex({ installed: ['xelatex'], alwaysFail: true })
|
|
1180
|
+
const h2 = makeHost({ pluginModule, subprocess: fake2 })
|
|
1181
|
+
await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1182
|
+
await h2.settleSpawns()
|
|
1183
|
+
const fin = await h2.callTool('vibe_v5_paper', { format: 'both' })
|
|
1184
|
+
await drivePaper(h2)
|
|
1185
|
+
const st2 = await h2.callTool('vibe_v5_status', {})
|
|
1186
|
+
const dir2 = paperDirOf(h2, 'institute')
|
|
1187
|
+
const meta2 = JSON.parse(readFileSync(join(dir2, 'paper.meta.json'), 'utf8'))
|
|
1188
|
+
assert(fin.ok === true && st2.paper.status === 'finalized' && st2.paper.compile === 'failed',
|
|
1189
|
+
'★ a persistent compile failure DEGRADES (finalisation still succeeds, compile=failed) (' + st2.paper.compile + ')')
|
|
1190
|
+
assert(existsSync(join(dir2, 'paper.tex')) && existsSync(join(dir2, 'paper.md')) && !existsSync(join(dir2, 'paper.pdf')),
|
|
1191
|
+
'tex+md are kept and no bogus pdf is written')
|
|
1192
|
+
assert(/\\title\{/.test(readFileSync(join(dir2, 'paper.tex'), 'utf8')),
|
|
1193
|
+
'★ the DELIVERED tex is the canonical generated one, not the minimal repair variant used by the last attempt')
|
|
1194
|
+
assert((meta2.compile.attempts || []).length === 4, '★ the retry plan is CAPPED at 4 attempts (full → nonstopmode → stripped/engine-swap → minimal template)')
|
|
1195
|
+
assert((meta2.warnings || []).join(' ').indexOf('编译失败') !== -1, 'the failure is reported as a warning in the meta')
|
|
1196
|
+
// Engine swap: the first installed engine fails, so attempt 3 (the NEXT engine) succeeds.
|
|
1197
|
+
const fake3 = fakeLatex({ installed: ['xelatex', 'pdflatex'], failEngines: ['xelatex'] })
|
|
1198
|
+
const h3 = makeHost({ pluginModule, subprocess: fake3 })
|
|
1199
|
+
await h3.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1200
|
+
await h3.settleSpawns()
|
|
1201
|
+
await h3.callTool('vibe_v5_paper', { format: 'both' })
|
|
1202
|
+
await drivePaper(h3)
|
|
1203
|
+
const dir3 = paperDirOf(h3, 'institute')
|
|
1204
|
+
const meta3 = JSON.parse(readFileSync(join(dir3, 'paper.meta.json'), 'utf8'))
|
|
1205
|
+
assert(meta3.compile.status === 'compiled' && meta3.compile.engine === 'pdflatex' && (meta3.compile.attempts || []).length === 3,
|
|
1206
|
+
'★ engine swap: xelatex fails twice, pdflatex succeeds on the third attempt (' + JSON.stringify({ e: meta3.compile.engine, n: (meta3.compile.attempts || []).length }) + ')')
|
|
1207
|
+
// paperFormat=md with paperCompilePdf=true must skip compilation SILENTLY (no "missing tex"
|
|
1208
|
+
// warning) — docs/final-paper.md §8.
|
|
1209
|
+
const hm = makeHost({ pluginModule, subprocess: fakeLatex({ installed: ['xelatex'] }) })
|
|
1210
|
+
await hm.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1211
|
+
await hm.settleSpawns()
|
|
1212
|
+
await hm.callTool('vibe_v5_paper', { format: 'md' })
|
|
1213
|
+
await drivePaper(hm)
|
|
1214
|
+
const dm = paperDirOf(hm, 'institute')
|
|
1215
|
+
const metam = JSON.parse(readFileSync(join(dm, 'paper.meta.json'), 'utf8'))
|
|
1216
|
+
assert(existsSync(join(dm, 'paper.md')) && !existsSync(join(dm, 'paper.tex')) && !existsSync(join(dm, 'paper.pdf')),
|
|
1217
|
+
'paperFormat=md delivers md only (no tex, no pdf)')
|
|
1218
|
+
assert(metam.compile.status === 'skipped' && !/编译|tex/.test((metam.warnings || []).join(' ')),
|
|
1219
|
+
'★ paperFormat=md skips compilation silently (no missing-tex warning): ' + JSON.stringify(metam.warnings))
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// ---------- 28. office-only surface + id normalisation ----------
|
|
1223
|
+
console.log('\n[28] the final-paper surface is office-only and its directory id cannot escape Paper/')
|
|
1224
|
+
{
|
|
1225
|
+
const h = makeHost({ pluginModule })
|
|
1226
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1227
|
+
await h.settleSpawns()
|
|
1228
|
+
const member = h.childAgent(h.childOf('r-1'))
|
|
1229
|
+
const asMember = await h.callTool('vibe_v5_paper', {}, member)
|
|
1230
|
+
assert(asMember.ok === false && asMember.code === 'V5_NOT_OFFICE', 'a member cannot start the paper (' + JSON.stringify(asMember).slice(0, 100) + ')')
|
|
1231
|
+
const finAsMember = await h.callTool('vibe_v5_finalize_paper', { decision: 'deliverable', note: 'x' }, member)
|
|
1232
|
+
assert(finAsMember.ok === false && finAsMember.code === 'V5_NOT_OFFICE', 'a member cannot finalise the paper')
|
|
1233
|
+
// A traversal-shaped institute name must never become a path in the paper id.
|
|
1234
|
+
const h2 = makeHost({ pluginModule })
|
|
1235
|
+
await h2.callTool('vibe_v5_configure', { institute: 'evil/../x' })
|
|
1236
|
+
await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1237
|
+
await h2.settleSpawns()
|
|
1238
|
+
await h2.callTool('vibe_v5_paper', {})
|
|
1239
|
+
await drivePaper(h2)
|
|
1240
|
+
const p2 = (await h2.callTool('vibe_v5_status', {})).paper
|
|
1241
|
+
const id = String(p2.dir || '').replace(/^Paper\//, '').replace(/\/$/, '')
|
|
1242
|
+
assert(id.length > 0 && id.indexOf('/') === -1 && id.indexOf('\\') === -1 && id !== '.' && id !== '..',
|
|
1243
|
+
'★ the paper directory id is ONE normalised name (never a path): ' + JSON.stringify(id))
|
|
1244
|
+
assert(existsSync(join(h2.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'x', 'Paper', id, 'paper.log.md')),
|
|
1245
|
+
'the paper landed under Paper/<id>/ inside the resolved institute tree')
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// ---------- 29. /v5 paper: overrides, force, kind:error; finalPaper=false gates AUTO ----------
|
|
1249
|
+
console.log('\n[29] /v5 paper: one-shot overrides, kind:error, and finalPaper=false')
|
|
1250
|
+
{
|
|
1251
|
+
const h = makeHost({ pluginModule })
|
|
1252
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1253
|
+
await h.settleSpawns()
|
|
1254
|
+
const cmd = h.commandRegs.find(c => c.name === 'v5')
|
|
1255
|
+
assert(!!cmd, 'the /v5 command is registered')
|
|
1256
|
+
const bad = await cmd.handler({ agent: h.ROOT, rawInput: 'paper bogus=1' })
|
|
1257
|
+
assert(bad.kind === 'error', '★ an unknown paper option is a FAILED command (kind:error): ' + String(bad.text).slice(0, 100))
|
|
1258
|
+
const memberCall = await cmd.handler({ agent: h.childAgent(h.childOf('acad')), rawInput: 'paper' })
|
|
1259
|
+
assert(memberCall.kind === 'error', 'a member child cannot drive the /v5 control line')
|
|
1260
|
+
const okCmd = await cmd.handler({ agent: h.ROOT, rawInput: 'paper lang=en format=tex editor=office' })
|
|
1261
|
+
assert(okCmd.kind === 'success', 'the paper subcommand succeeds: ' + String(okCmd.text).slice(0, 120))
|
|
1262
|
+
const st = await h.callTool('vibe_v5_status', {})
|
|
1263
|
+
assert(st.paper && st.paper.lang === 'en' && st.paper.format === 'tex' && st.paper.editor === 'office',
|
|
1264
|
+
'★ the command overrides language/format/editor FOR THIS PAPER (' + JSON.stringify({ l: st.paper.lang, f: st.paper.format, e: st.paper.editor }) + ')')
|
|
1265
|
+
assert(st.params.paperLanguage === 'zh' && st.params.paperEditor === 'academician',
|
|
1266
|
+
'the one-shot overrides do NOT rewrite the persisted params')
|
|
1267
|
+
// finalPaper=false: the AUTO trigger is off, the run concludes immediately, the manual path says so.
|
|
1268
|
+
const h2 = makeHost({ pluginModule })
|
|
1269
|
+
await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1270
|
+
await h2.settleSpawns()
|
|
1271
|
+
await h2.callTool('vibe_v5_set', { finalPaper: false })
|
|
1272
|
+
h2.solvePlan = true
|
|
1273
|
+
await h2.callTool('vibe_v5_meeting', { agenda: '是否已解决原问题?', kind: 'solve-vote' })
|
|
1274
|
+
await h2.drain(24)
|
|
1275
|
+
const s2 = await h2.callTool('vibe_v5_status', {})
|
|
1276
|
+
assert(s2.autoDone === true && !s2.paper, '★ finalPaper=false: the run concludes WITHOUT starting a paper (' + JSON.stringify({ autoDone: s2.autoDone, paper: s2.paper }) + ')')
|
|
1277
|
+
const manual = await h2.callTool('vibe_v5_paper', {})
|
|
1278
|
+
assert(manual.ok === true && manual.autoDisabled === true && /自动已关闭/.test(String(manual.note)),
|
|
1279
|
+
'the manual command still works when finalPaper=false and says the automatic path is off (' + JSON.stringify(manual).slice(0, 220) + ')')
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// ---------- 30. unanimity is required; the round cap records the disagreement ----------
|
|
1283
|
+
console.log('\n[30] unanimity: an objection does NOT finalise, the cap records the disagreement in the appendix')
|
|
1284
|
+
{
|
|
1285
|
+
const h = makeHost({ pluginModule })
|
|
1286
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1287
|
+
await h.settleSpawns()
|
|
1288
|
+
const objecting = () => ({ paper_review: { deliverable: false, comments: '证据不足,需补充' } })
|
|
1289
|
+
await h.callTool('vibe_v5_paper', {})
|
|
1290
|
+
// Round 1 stopped after its write+review: the objection must NOT finalise anything.
|
|
1291
|
+
await drivePaper(h, { max: 2, review: () => objecting() })
|
|
1292
|
+
let st = await h.callTool('vibe_v5_status', {})
|
|
1293
|
+
assert(st.paper && st.paper.status === 'writing' && st.paper.round === 2,
|
|
1294
|
+
'★ an objection does NOT finalise: the flow iterates to the next round (status=' + st.paper.status + ', round=' + st.paper.round + ')')
|
|
1295
|
+
assert(st.paper.disagreement.length >= 1, 'the objection is recorded as a disagreement')
|
|
1296
|
+
// Keep objecting; at the cap the editor (academician) is asked and asks to revise again — the
|
|
1297
|
+
// spec says a cap is reached, a warning is recorded and the disagreement goes into the appendix.
|
|
1298
|
+
await drivePaper(h, {
|
|
1299
|
+
review: () => objecting(),
|
|
1300
|
+
final: () => ({ paper_final: { decision: 'revise', note: '仍有未达成一致的意见' } }),
|
|
1301
|
+
})
|
|
1302
|
+
const st2 = await h.callTool('vibe_v5_status', {})
|
|
1303
|
+
assert(st2.paper && st2.paper.status === 'finalized' && st2.paper.forcedAfterCap === true,
|
|
1304
|
+
'★ 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 }) + ')')
|
|
1305
|
+
assert((st2.paper.warnings || []).join(' ').indexOf('上限') !== -1, 'the cap produced a WARNING (' + JSON.stringify(st2.paper.warnings) + ')')
|
|
1306
|
+
const md = readFileSync(join(paperDirOf(h, 'institute'), 'paper.md'), 'utf8')
|
|
1307
|
+
assert(md.indexOf('分歧记录') !== -1 && md.indexOf('证据不足,需补充') !== -1,
|
|
1308
|
+
'★ the unresolved disagreement is written into the APPENDIX of the paper')
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// ---------- 31. the DEFAULT auto trigger: the paper phase runs BEFORE completion ----------
|
|
1312
|
+
console.log('\n[31] a unanimous solve vote with finalPaper=true enters the paper phase BEFORE completion')
|
|
1313
|
+
{
|
|
1314
|
+
// The mutant guard: deleting the auto-trigger branch in `checkSolved`
|
|
1315
|
+
// (`if (params.finalPaper !== false)` -> `if (false)`) used to leave this whole suite green,
|
|
1316
|
+
// because every other paper case drives the flow through the MANUAL vibe_v5_paper tool and
|
|
1317
|
+
// the only auto assertion was "finalPaper=false => no paper" (which the mutant satisfies).
|
|
1318
|
+
// This case pins the real contract: the default path must NOT complete the run until the
|
|
1319
|
+
// paper is finalised, and the run must be observably alive with a paper in progress.
|
|
1320
|
+
const h = makeHost({ pluginModule })
|
|
1321
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1322
|
+
await h.settleSpawns()
|
|
1323
|
+
await h.callTool('vibe_v5_set', { activityTimeoutMs: 50 })
|
|
1324
|
+
h.solvePlan = true
|
|
1325
|
+
await h.callTool('vibe_v5_meeting', { agenda: '是否已解决原问题?', kind: 'solve-vote' })
|
|
1326
|
+
await h.drain(24)
|
|
1327
|
+
const mid = await h.callTool('vibe_v5_status', {})
|
|
1328
|
+
assert(mid.autoDone === false,
|
|
1329
|
+
'★ a unanimous solve vote with finalPaper=true does NOT complete the run immediately (got autoDone=' + mid.autoDone + ')')
|
|
1330
|
+
const p0 = mid.paper || {}
|
|
1331
|
+
assert(!!mid.paper && p0.status !== 'finalized',
|
|
1332
|
+
'★ ... it ENTERS the paper phase instead (' + JSON.stringify({ s: p0.status, e: p0.editor }) + ')')
|
|
1333
|
+
assert(p0.editor === 'academician' && p0.completesRun === true,
|
|
1334
|
+
'the automatic run is edited by the wakeable academician and is marked as completing the run (' + JSON.stringify({ e: p0.editor, c: p0.completesRun }) + ')')
|
|
1335
|
+
assert(mid.running === true, 'the run stays ALIVE while the paper phase runs (the machinery would refuse a concluded institute)')
|
|
1336
|
+
const capture = []
|
|
1337
|
+
await drivePaper(h, { capture })
|
|
1338
|
+
let done = null
|
|
1339
|
+
for (let i = 0; i < 40; i++) {
|
|
1340
|
+
done = await h.callTool('vibe_v5_status', {})
|
|
1341
|
+
if (done.autoDone) break
|
|
1342
|
+
await drivePaper(h, { max: 4, capture })
|
|
1343
|
+
await sleep(20)
|
|
1344
|
+
}
|
|
1345
|
+
assert(done.autoDone === true && done.running === false,
|
|
1346
|
+
'★ the run is marked complete only AFTER the paper finalised (' + JSON.stringify({ a: done.autoDone, r: done.running }) + ')')
|
|
1347
|
+
assert(done.paper && done.paper.status === 'finalized', 'the paper reached the finalized stage (' + JSON.stringify(done.paper && done.paper.status) + ')')
|
|
1348
|
+
assert(existsSync(join(paperDirOf(h, 'institute'), 'paper.md')), 'the automatic path delivered Paper/<id>/paper.md')
|
|
1349
|
+
// The members' own prompts carry the evidence-only clause (never invent content).
|
|
1350
|
+
const write = capture.find(c => c.kind === 'write')
|
|
1351
|
+
assert(!!write && /不得编造/.test(write.prompt) && /只写你自己库里已有证据支撑/.test(write.prompt),
|
|
1352
|
+
'★ the paper write prompt says only-evidence/no-fabrication: ' + JSON.stringify(write && write.prompt.slice(0, 120)))
|
|
1353
|
+
assert(!!write && /未决 \/ 被否证的条目必须显式标注/.test(write.prompt), 'the prompt requires undecided/refuted items to be marked explicitly')
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// ---------- 32. a run that has NOT closed writes no Paper/ directory ----------
|
|
1357
|
+
console.log('\n[32] a run that has NOT closed writes NO Paper/ directory')
|
|
1358
|
+
{
|
|
1359
|
+
const h = makeHost({ pluginModule })
|
|
1360
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
|
|
1361
|
+
await h.settleSpawns()
|
|
1362
|
+
// Real, non-closing work: one verification concludes, and the solve vote is NOT unanimous.
|
|
1363
|
+
await h.callTool('vibe_v5_record_proposition', { id: 'p-not-closed', statement: '未收口就不得产出论文目录', value: 0.8, motive: '负用例', p: 1 }, h.childAgent(h.childOf('r-1')))
|
|
1364
|
+
await h.callTool('vibe_v5_propose_verify', { target: 'p-not-closed', kind: 'proposition', reason: '负用例' }, h.childAgent(h.childOf('r-1')))
|
|
1365
|
+
h.plannedVotes = new Map([['acad', 1], ['r-1', 1]])
|
|
1366
|
+
await h.drain(12)
|
|
1367
|
+
h.solvePlan = false
|
|
1368
|
+
await h.callTool('vibe_v5_meeting', { agenda: '是否已解决?(未达成一致)', kind: 'solve-vote' })
|
|
1369
|
+
await h.drain(16)
|
|
1370
|
+
const st = await h.callTool('vibe_v5_status', {})
|
|
1371
|
+
assert(st.autoDone === false && !st.paper, 'the run is still open and no paper phase started (' + JSON.stringify({ a: st.autoDone, p: st.paper }) + ')')
|
|
1372
|
+
const paperRoot = join(h.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Paper')
|
|
1373
|
+
assert(!existsSync(paperRoot), '★ a run that has NOT closed writes no Paper/ directory at all')
|
|
1374
|
+
const rel = await h.callTool('vibe_v5_report', {})
|
|
1375
|
+
assert(String(rel.report || '').indexOf('最终论文') !== -1 && String(rel.report || '').indexOf('尚未开始') !== -1,
|
|
1376
|
+
'the report tells the operator the paper has not started yet')
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// ---------- 33. an existing paper.pdf is never clobbered ----------
|
|
1380
|
+
console.log('\n[33] an existing Paper/<id>/paper.pdf is never overwritten or deleted')
|
|
1381
|
+
{
|
|
1382
|
+
const PRE = '%PDF-1.4 PRE-EXISTING DELIVERY\n'
|
|
1383
|
+
// (a) a compiler that WOULD succeed: the existing pdf must make the flow skip compilation.
|
|
1384
|
+
const fake = fakeLatex({ installed: ['xelatex'] })
|
|
1385
|
+
const h = makeHost({ pluginModule, subprocess: fake })
|
|
1386
|
+
await h.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1387
|
+
await h.settleSpawns()
|
|
1388
|
+
const dir = paperDirOf(h, 'institute')
|
|
1389
|
+
mkdirSync(dir, { recursive: true })
|
|
1390
|
+
writeFileSync(join(dir, 'paper.pdf'), PRE, 'utf8')
|
|
1391
|
+
await h.callTool('vibe_v5_paper', { format: 'both' })
|
|
1392
|
+
await drivePaper(h)
|
|
1393
|
+
assert(readFileSync(join(dir, 'paper.pdf'), 'utf8') === PRE, '★ the pre-existing pdf is byte-identical after the run (never overwritten)')
|
|
1394
|
+
assert(fake.calls.length === 0, '★ compilation was SKIPPED entirely because a pdf already existed (never clobber)')
|
|
1395
|
+
const meta = JSON.parse(readFileSync(join(dir, 'paper.meta.json'), 'utf8'))
|
|
1396
|
+
assert(meta.compile.status === 'kept-existing', 'the meta records compile=kept-existing (' + meta.compile.status + ')')
|
|
1397
|
+
// (b) a compiler that ALWAYS FAILS: still skipped, so a failing toolchain cannot destroy it.
|
|
1398
|
+
const fake2 = fakeLatex({ installed: ['xelatex'], alwaysFail: true })
|
|
1399
|
+
const h2 = makeHost({ pluginModule, subprocess: fake2 })
|
|
1400
|
+
await h2.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 0 })
|
|
1401
|
+
await h2.settleSpawns()
|
|
1402
|
+
const dir2 = paperDirOf(h2, 'institute')
|
|
1403
|
+
mkdirSync(dir2, { recursive: true })
|
|
1404
|
+
writeFileSync(join(dir2, 'paper.pdf'), PRE, 'utf8')
|
|
1405
|
+
await h2.callTool('vibe_v5_paper', { format: 'both' })
|
|
1406
|
+
await drivePaper(h2)
|
|
1407
|
+
assert(readFileSync(join(dir2, 'paper.pdf'), 'utf8') === PRE, '★ even an always-failing toolchain leaves the existing pdf untouched')
|
|
1408
|
+
assert(fake2.calls.length === 0, 'no compiler process was launched for the already-delivered pdf')
|
|
1409
|
+
}
|
|
1410
|
+
|
|
941
1411
|
console.log('')
|
|
942
1412
|
console.log('passed=' + passed + ' failed=' + failed)
|
|
943
1413
|
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
@@ -1295,6 +1295,9 @@ section('16 the captured prompt corpus is written for human review')
|
|
|
1295
1295
|
.split(ws + '\\VibeMath').join('<VIBEMATH>')
|
|
1296
1296
|
.split(slash).join('<WS>')
|
|
1297
1297
|
.split(ws).join('<WS>')
|
|
1298
|
+
// Epoch-ms in a verification-log FILE NAME is run metadata: the final-paper material embeds the
|
|
1299
|
+
// evidence index verbatim, so it must be normalised or the shipped corpus changes every run.
|
|
1300
|
+
.replace(/_[0-9]{10,16}\.json/g, '_<TIME>.json')
|
|
1298
1301
|
}
|
|
1299
1302
|
const entries = []
|
|
1300
1303
|
for (const h of hosts) {
|
|
@@ -113,6 +113,10 @@ const scrub = (s) => canonBrief(String(s == null ? '' : s)
|
|
|
113
113
|
.replace(/("at"\s*:\s*)\d{10,16}/g, '$1"<TIME>"')
|
|
114
114
|
.replace(/("childId"\s*:\s*")c\d+(")/g, '$1<CHILD>$2')
|
|
115
115
|
.replace(/("free_slots"\s*:\s*)\d+/g, '$1<SLOTS>')
|
|
116
|
+
// Epoch-ms in a verification-log FILE NAME (`Logs/Verification/<rId>_<Date.now()>.json`) is run
|
|
117
|
+
// metadata too: the final-paper material embeds the evidence index verbatim, so without this the
|
|
118
|
+
// shipped corpus changed on every run (contract §10 item 10 demands byte determinism).
|
|
119
|
+
.replace(/_[0-9]{10,16}\.json/g, '_<TIME>.json')
|
|
116
120
|
|
|
117
121
|
// A fake Lean: a file PASSES unless it still contains `sorry` or the marker `-- FAIL`.
|
|
118
122
|
// `-- HANG` simulates a toolchain that never returns (the timeout path).
|
|
@@ -862,7 +862,7 @@ assert(stq2.verify === null || stq2.verify.target === 'p-second',
|
|
|
862
862
|
await endCase(RL)
|
|
863
863
|
|
|
864
864
|
// =============== CASE 9c: a solve vote OUTSIDE a meeting ==========================
|
|
865
|
-
section('9c a unanimous solve vote landing outside a meeting still
|
|
865
|
+
section('9c a unanimous solve vote landing outside a meeting still triggers the stop evaluation')
|
|
866
866
|
const RM = makeRoot()
|
|
867
867
|
await callTool('vibe_v5_start', { problem: '会外表决停工测试', researcherCount: 1 }, RM)
|
|
868
868
|
for (const sp of spawnsFor(RM)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
|
|
@@ -877,8 +877,14 @@ await callTool('vibe_v5_say', { to: 'r-1', text: '请你就"是否已解决"表
|
|
|
877
877
|
await settle(); await drainWakes(4, RM)
|
|
878
878
|
const stSolved = await callTool('vibe_v5_status', {}, RM)
|
|
879
879
|
assert(stSolved.solveVotes.length >= 2, 'both voters recorded a solve vote outside any meeting (' + JSON.stringify(stSolved.solveVotes) + ')')
|
|
880
|
-
|
|
881
|
-
|
|
880
|
+
// docs/final-paper.md §3: the unanimous solve vote no longer flips the completion flags immediately — the
|
|
881
|
+
// FINAL PAPER phase runs first (after phase='solved' the machinery refuses to wake members,
|
|
882
|
+
// convene meetings or dispatch an end). What this case exists for is that `checkSolved` is
|
|
883
|
+
// re-evaluated on a vote that arrived OUTSIDE a meeting, and the paper phase starting is that
|
|
884
|
+
// evaluation firing; `selfdrive-v5`/`e2e-v5-round2` carry the run all the way to autoDone.
|
|
885
|
+
assert(stSolved.autoDone === false && !!stSolved.paper && stSolved.paper.status !== 'finalized',
|
|
886
|
+
'the unanimous solve vote outside a meeting reached the stop EVALUATION and entered the final-paper phase ' +
|
|
887
|
+
JSON.stringify({ autoDone: stSolved.autoDone, solveVotes: stSolved.solveVotes, paper: stSolved.paper && stSolved.paper.status }))
|
|
882
888
|
await endCase(RM)
|
|
883
889
|
|
|
884
890
|
// =============== CASE 10: meeting prompts =======================================
|