dsh-vibe-math 2.7.3 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.en.md +682 -682
- package/README.md +683 -683
- package/docs/AUDIT-CHECKLIST.md +758 -744
- package/docs/COMPAT-AUDIT-ROUND2.md +351 -351
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/audit-methodology.md +93 -0
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +613 -588
- package/docs/generate_framework_diagram_v2.py +114 -114
- package/docs/generate_framework_diagram_v3.py +127 -127
- package/docs/math-computation.md +195 -194
- package/docs/release-notes/README.en.md +2 -0
- package/docs/release-notes/README.md +2 -0
- package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
- package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
- package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
- package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
- package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
- package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
- package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
- package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
- package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
- package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
- package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
- package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
- package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
- package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
- package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
- package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
- package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
- package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.1.en.md +49 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.1.md +49 -0
- package/docs/release-playbook.md +111 -0
- package/docs/slv-playbook.md +117 -0
- package/docs/test-timing.md +158 -157
- package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
- package/installer.js +792 -792
- package/package.json +18 -3
- package/prompt-corpus-v2/formal-verify-v2.json +2 -2
- package/prompt-corpus-v2/formal-verify-v2.md +4 -0
- package/prompt-corpus-v3/formal-verify-v3.json +7 -7
- package/prompt-corpus-v3/formal-verify-v3.md +14 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
- package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
- package/scripts/clean-temp.mjs +234 -0
- package/scripts/release-check.mjs +181 -0
- package/tests/audit-engine-faces.mjs +298 -0
- package/tests/audit-formal-sensitivity.mjs +345 -342
- package/tests/audit-installer-compat.test.mjs +256 -256
- package/tests/audit-math-computation-sensitivity.mjs +1 -1
- package/tests/audit-persona-sensitivity.mjs +323 -323
- package/tests/audit-persona-surface.test.mjs +431 -431
- package/tests/audit-prompt-duplication.mutants.mjs +3 -2
- package/tests/audit-prompt-invariants.mjs +589 -546
- package/tests/audit-registration.mjs +78 -78
- package/tests/audit-spec-traceability.mjs +193 -193
- package/tests/audit-status-report-fields.mjs +172 -172
- package/tests/audit-v5-integrity.mjs +795 -795
- package/tests/audit-v5-sensitivity.mjs +433 -432
- package/tests/e2e-v5-round2.test.mjs +2453 -2426
- package/tests/formal-verify-v2.test.mjs +1806 -1800
- package/tests/formal-verify-v3.test.mjs +1757 -1750
- package/tests/formal-verify-v4.test.mjs +2114 -2106
- package/tests/formal-verify-v5.test.mjs +774 -767
- package/tests/math-computation-discovery.mutants.mjs +3 -3
- package/tests/math-computation-shared.test.mjs +122 -5
- package/tests/math-computation-v2.test.mjs +14 -2
- package/tests/math-computation-v3.test.mjs +14 -2
- package/tests/math-computation-v4.test.mjs +739 -739
- package/tests/prompt-v5-integrity.test.mjs +1291 -1291
- package/tests/release-check.mutants.mjs +78 -0
- package/tests/run-tests.mjs +393 -332
- package/tests/selfdrive-v5.mjs +560 -560
- package/tests/temp-hygiene.mutants.mjs +80 -0
- package/tests/v2-fix-probes.mutants.mjs +4 -0
- package/tests/v2-fix-probes.test.mjs +1034 -1033
- package/tests/v2v3-interrupt.mutants.mjs +111 -0
- package/tests/v3-fix-probes.mutants.mjs +99 -95
- package/tests/v3-fix-probes.test.mjs +891 -890
- package/tests/v4-final-paper.mutants.mjs +116 -0
- package/tests/v4-final-paper.test.mjs +628 -0
- package/tests/v5-institute-fixes.mutants.mjs +218 -190
- package/vibe-math-v2/agent.cordis.yml +376 -376
- package/vibe-math-v2/math-computation.js +17 -15
- package/vibe-math-v2/preset.yml +2 -2
- package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
- package/vibe-math-v2/vibe-math-v2.js +5078 -5011
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
- package/vibe-math-v3/agent.cordis.yml +420 -420
- package/vibe-math-v3/math-computation.js +17 -15
- package/vibe-math-v3/preset.yml +2 -2
- package/vibe-math-v3/vibe-math-v3.js +6215 -6149
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
- package/vibe-math-v4/agent.cordis.yml +438 -438
- package/vibe-math-v4/math-computation.js +17 -15
- package/vibe-math-v4/preset.yml +2 -2
- package/vibe-math-v4/vibe-math-v4.js +4520 -4422
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
- package/vibe-math-v5/math-computation.js +17 -15
- package/vibe-math-v5/vibe-math-v5.js +93 -11
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// V4 FINAL PAPER SUITE (docs/final-paper.md, v1 + v2 amendments)
|
|
3
|
+
//
|
|
4
|
+
// Covers the v4 side of docs/final-paper.md §6:
|
|
5
|
+
// · the five/six new parameters: defaults, schema presence, boolean/enum coercion of RAW STRINGS
|
|
6
|
+
// (the `/v4 set` path hands the parameter layer strings) and rejection/fallback of bad values;
|
|
7
|
+
// · the trigger: NOT fired on a run that has not closed, fired on the closing meeting's unanimous
|
|
8
|
+
// stop vote, disabled by `finalPaper=false`, and idempotent when re-triggered;
|
|
9
|
+
// · the team flow: parts -> framework merge -> cross-review (each participant reviews ANOTHER
|
|
10
|
+
// part) -> editor finalisation -> deliverability vote, for `paperEditor=office` and
|
|
11
|
+
// `resident:<id>` (including the dismissed-editor downgrade);
|
|
12
|
+
// · NEGATIVE cases: a review that names nobody is refused and cannot finalise; dissent never
|
|
13
|
+
// finalises silently (bounded iteration, then the appendix + a warning);
|
|
14
|
+
// · the md+tex nine-section skeleton, the evidence index, and explicit 未决 flags;
|
|
15
|
+
// · the compile branches via an INJECTED FAKE COMPILER (success / repaired via the minimal
|
|
16
|
+
// template / persistent failure -> degrade) plus the no-LaTeX branch and `paperCompilePdf=false`;
|
|
17
|
+
// · `Paper/<id>/` containment for a hostile problem id, and no pollution of Verified/State;
|
|
18
|
+
// · `/v4 paper` with parameter overrides, `force`, and `kind:'error'` on a bad option.
|
|
19
|
+
//
|
|
20
|
+
// V4_PLUGIN overrides the plugin under test: a sensitivity probe MUST point this suite at a mutated
|
|
21
|
+
// copy, otherwise the probe would exercise the unmutated plugin and stay green
|
|
22
|
+
// (AUDIT-CHECKLIST §2.5).
|
|
23
|
+
//
|
|
24
|
+
// Run: node tests/v4-final-paper.test.mjs
|
|
25
|
+
// ============================================================
|
|
26
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
27
|
+
import { tmpdir } from 'node:os'
|
|
28
|
+
import { join, dirname } from 'node:path'
|
|
29
|
+
let g3WriteFail = null // G3 test-side: RegExp matched against the fs targetKey; null = unchanged
|
|
30
|
+
const PLUGIN = process.env.V4_PLUGIN
|
|
31
|
+
? new URL('file:///' + String(process.env.V4_PLUGIN).replace(/\\/g, '/'))
|
|
32
|
+
: new URL('../vibe-math-v4/vibe-math-v4.js', import.meta.url)
|
|
33
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
34
|
+
let passed = 0, failed = 0
|
|
35
|
+
const failures = []
|
|
36
|
+
const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
|
|
37
|
+
const section = (t) => console.log('\n[' + t + ']')
|
|
38
|
+
const JSONX = o => '```json\n' + JSON.stringify(o) + '\n```'
|
|
39
|
+
const readIf = (p) => { try { return readFileSync(p, 'utf8') } catch (e) { return '' } }
|
|
40
|
+
/** Parse a JSON artifact, or `null` when it is missing/unparseable — a missing artifact must be ONE
|
|
41
|
+
* clean assertion failure, never an uncaught SyntaxError that swallows the rest of the file. */
|
|
42
|
+
const readJsonIf = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')) } catch (e) { return null } }
|
|
43
|
+
|
|
44
|
+
// ---- injectable fake LaTeX compiler ------------------------------------------------------------
|
|
45
|
+
// The host fs is text-only, so paper.pdf can ONLY come from the compiler subprocess. This mock is
|
|
46
|
+
// that subprocess: `mode` decides how the fake engine behaves.
|
|
47
|
+
const COMPILER = { mode: 'none', engines: [], ws: '' }
|
|
48
|
+
function makeSubprocess() {
|
|
49
|
+
return {
|
|
50
|
+
async resolveExecutable(cmd) {
|
|
51
|
+
if (COMPILER.mode === 'none' || COMPILER.engines.indexOf(String(cmd)) === -1) throw new Error('spawn ' + cmd + ' ENOENT')
|
|
52
|
+
return String(cmd)
|
|
53
|
+
},
|
|
54
|
+
spawn(spec) {
|
|
55
|
+
// The plugin passes the paper directory RELATIVE to the project root (a real host resolves it
|
|
56
|
+
// against the session workspace); the mock must do the same.
|
|
57
|
+
const raw = String(spec.cwd || '')
|
|
58
|
+
const cwd = raw.indexOf(':') !== -1 || raw[0] === '/' ? raw : join(COMPILER.ws, 'VibeMath', 'Projects', 'default', raw)
|
|
59
|
+
const argv = (spec.argv || []).map(String)
|
|
60
|
+
const exe = argv[0] || ''
|
|
61
|
+
const texRel = argv[argv.length - 1] || ''
|
|
62
|
+
const tex = existsSync(join(cwd, texRel)) ? readFileSync(join(cwd, texRel), 'utf8') : ''
|
|
63
|
+
const usesPackages = /\\usepackage/.test(tex)
|
|
64
|
+
let exitCode = 0
|
|
65
|
+
if (COMPILER.mode === 'ok') exitCode = 0
|
|
66
|
+
else if (COMPILER.mode === 'repair') exitCode = usesPackages ? 1 : 0 // only the minimal template compiles
|
|
67
|
+
else if (COMPILER.mode === 'fail') exitCode = 1
|
|
68
|
+
if (exitCode === 0) { try { writeFileSync(join(cwd, 'paper.pdf'), '%PDF-1.4 fake compiler output for ' + exe) } catch (e) {} }
|
|
69
|
+
const text = exitCode === 0 ? 'fake engine ok\n' : '! Fake LaTeX error: unsupported package\n'
|
|
70
|
+
return {
|
|
71
|
+
done: Promise.resolve({ exitCode, signal: null }),
|
|
72
|
+
collected: {
|
|
73
|
+
stdout: { readFrom: () => ({ text: exitCode === 0 ? text : '', nextOffset: 0, lossy: false }) },
|
|
74
|
+
stderr: { readFrom: () => ({ text: exitCode === 0 ? '' : text, nextOffset: 0, lossy: false }) },
|
|
75
|
+
},
|
|
76
|
+
terminate() {},
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeCtx() {
|
|
83
|
+
const WS = mkdtempSync(join(tmpdir(), 'vibe-v4-paper-'))
|
|
84
|
+
COMPILER.ws = WS
|
|
85
|
+
const listeners = {}, toolRegs = [], cmdRegs = [], spawns = [], followups = []
|
|
86
|
+
let ROOT
|
|
87
|
+
const ctx = {
|
|
88
|
+
get(name) { return name === 'subprocess' ? makeSubprocess() : undefined },
|
|
89
|
+
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
90
|
+
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
91
|
+
logger: { info() {}, warn() {}, error() {} },
|
|
92
|
+
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
93
|
+
tools: { register(s) { toolRegs.push(s) } },
|
|
94
|
+
commands: { register(s) { cmdRegs.push(s) } },
|
|
95
|
+
subagents: {
|
|
96
|
+
list() { return ['spawn'] },
|
|
97
|
+
async startContinuable({ label, request }) { const id = 'c' + (spawns.length + 1); spawns.push({ label, childId: id, request }); return { childId: id } },
|
|
98
|
+
async sendMessage(parent, childId, blocks) { followups.push({ childId, blocks }); return 'w' + followups.length },
|
|
99
|
+
interrupt() {},
|
|
100
|
+
},
|
|
101
|
+
agents: { roots() { return [] }, get(id) { return id === 'sess-A' ? ROOT : undefined } },
|
|
102
|
+
fs: {
|
|
103
|
+
async resolve(rel, opts) { const b = (opts && opts.cwd) || WS; return { targetKey: join(b, ...String(rel).split('/')), displayPath: 'x' } },
|
|
104
|
+
async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
|
|
105
|
+
async readText(t) { return readFileSync(t.targetKey, 'utf8') },
|
|
106
|
+
async writeText(t, c) { if (g3WriteFail && g3WriteFail.test(String(t.targetKey))) throw new Error('G3_WRITE_FAIL (injected): ' + t.targetKey); mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
|
|
107
|
+
async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
ROOT = { id: 'sess-A', options: { provider: 'mock', model: 'm' }, session: { id: 'sess-A', header: { cwd: WS, parentSession: undefined } }, followup() {}, ctx: undefined }
|
|
111
|
+
return {
|
|
112
|
+
WS, ctx, toolRegs, cmdRegs, spawns, followups,
|
|
113
|
+
callTool: async (n, a) => { const s = toolRegs.find(x => x.name === n); if (!s) throw new Error('no tool ' + n); return JSON.parse(await s.execute(a || {}, { agent: ROOT })) },
|
|
114
|
+
callToolAs: async (n, a, cid) => { const s = toolRegs.find(x => x.name === n); if (!s) throw new Error('no tool ' + n); const agent = { id: cid, session: { id: cid, header: { cwd: WS, parentSession: 'sess-A' } } }; return JSON.parse(await s.execute(a, { agent })) },
|
|
115
|
+
fireEnd: (info) => { for (const h of (listeners['subagent/end'] || [])) h(info) },
|
|
116
|
+
cmd: async (rawInput) => { const c = cmdRegs.find(x => x.name === 'v4'); if (!c) throw new Error('no /v4 command'); return await c.handler({ agent: ROOT, rawInput }) },
|
|
117
|
+
ridOf: (cid) => { const s = spawns.find(x => x.childId === cid); return s ? s.label : '' },
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function newPlugin(m) {
|
|
121
|
+
const q = Date.now() + Math.random()
|
|
122
|
+
const mod = await import(PLUGIN.href + '?t=' + q)
|
|
123
|
+
;(mod.default || mod).apply(m.ctx)
|
|
124
|
+
}
|
|
125
|
+
async function waitFor(pred, ms = 3000) { const t0 = Date.now(); while (Date.now() - t0 < ms) { if (pred()) return true; await sleep(5) } return !!pred() }
|
|
126
|
+
/** Poll `status()` until `pred` holds (or the wall-clock budget expires) and return the last status.
|
|
127
|
+
* Every timing-sensitive precondition is WAITED for, then asserted — a fixed iteration budget is not
|
|
128
|
+
* a clock, and under a 4-way parallel gate (measured with 4 CPU hogs + 4 copies) a 60-iteration
|
|
129
|
+
* budget expired before the plugin had even reached the step under test. */
|
|
130
|
+
async function waitStatus(m, pred, ms = 30000) {
|
|
131
|
+
const t0 = Date.now()
|
|
132
|
+
let st = await m.callTool('vibe_v4_status', {})
|
|
133
|
+
while (!pred(st) && (Date.now() - t0) < ms) { await sleep(10); st = await m.callTool('vibe_v4_status', {}) }
|
|
134
|
+
return st
|
|
135
|
+
}
|
|
136
|
+
/** Drive one resident turn at a time; `reply(text, rid, fu)` returns the JSON object to answer with.
|
|
137
|
+
* Bounded by WALL-CLOCK (`maxMs`) as well as a generous iteration cap, so CPU contention can only
|
|
138
|
+
* make the drive take longer, never make it give up early on an outer assertion. */
|
|
139
|
+
async function drive(m, reply, stop, opts) {
|
|
140
|
+
const o = opts || {}
|
|
141
|
+
const budget = o.budget || 20000
|
|
142
|
+
const maxMs = o.maxMs || 60000
|
|
143
|
+
const t0 = Date.now()
|
|
144
|
+
let fi = 0
|
|
145
|
+
for (let i = 0; i < budget && (Date.now() - t0) < maxMs; i++) {
|
|
146
|
+
if (fi < m.followups.length) {
|
|
147
|
+
const fu = m.followups[fi++]
|
|
148
|
+
const rid = m.ridOf(fu.childId)
|
|
149
|
+
const pt = (fu.blocks && fu.blocks[0] && fu.blocks[0].text) || ''
|
|
150
|
+
const r = reply(pt, rid, fu)
|
|
151
|
+
const obj = (r && typeof r === 'object' && r.__raw) ? r.__raw : r
|
|
152
|
+
m.fireEnd({ id: fu.childId, runId: 'p-' + i, provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: JSONX(obj === undefined ? {} : obj) }] })
|
|
153
|
+
await sleep(2)
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
const st = await m.callTool('vibe_v4_status', {})
|
|
157
|
+
if (stop && stop(st)) return st
|
|
158
|
+
await sleep(5)
|
|
159
|
+
}
|
|
160
|
+
return await m.callTool('vibe_v4_status', {})
|
|
161
|
+
}
|
|
162
|
+
/** Per-case teardown: stop this case's scheduler/heartbeat (a still-armed heartbeat from a finished
|
|
163
|
+
* case keeps burning the event loop and makes the NEXT case slower) and delete its scratch tree. */
|
|
164
|
+
async function finish(m) {
|
|
165
|
+
try { await m.callTool('vibe_v4_abort', {}) } catch (e) { /* best effort */ }
|
|
166
|
+
rmSync(m.WS, { recursive: true, force: true })
|
|
167
|
+
}
|
|
168
|
+
/** Start a 2-resident run with one recorded proposition + one method (evidence for §4/§6). */
|
|
169
|
+
async function startRun(m, problem, extra) {
|
|
170
|
+
await m.callTool('vibe_v4_start', { problem: problem || 'final-paper-test', residentCount: 2 })
|
|
171
|
+
await waitFor(() => m.spawns.length >= 2)
|
|
172
|
+
// `activityTimeoutMs` doubles as the consensus-watchdog period (`recoverStallMs() = 2×`) and as the
|
|
173
|
+
// paper step deadline (`max(2000, 4×recoverStallMs)`). 60 ms → a 240 ms paper deadline, which a
|
|
174
|
+
// loaded host can blow past while merely answering the fixture's own wakes (a successful
|
|
175
|
+
// contribution would be recorded as "missing"). 1000 ms ⇒ an 8 s step budget: still a fast run,
|
|
176
|
+
// but no longer a bet on the scheduler.
|
|
177
|
+
await m.callTool('vibe_v4_set', { activityTimeoutMs: 1000, verdictMaxRounds: 1, ...(extra || {}) })
|
|
178
|
+
for (const sp of m.spawns.slice()) {
|
|
179
|
+
m.fireEnd({ id: sp.childId, runId: 'br-' + sp.label, provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: JSONX({ summary: 'insight ' + sp.label, solved: false }) }] })
|
|
180
|
+
await sleep(30)
|
|
181
|
+
}
|
|
182
|
+
await m.callToolAs('vibe_v4_record_proposition', { id: 'p-ok', title: '已证命题', statement: 's', prob: 0.8, value: 0.7, motivation: 'm' }, m.spawns[0].childId)
|
|
183
|
+
await m.callToolAs('vibe_v4_record_method', { id: 'm-tool', title: '工具法', type: '工具', content: 'c', value: 0.6, motivation: 'm' }, m.spawns[1].childId)
|
|
184
|
+
// `subagent/end` is delivered to ASYNC listeners: WAIT for the phase transition (the old code
|
|
185
|
+
// asserted it 30 ms after the last end, which is a clock-dependent precondition).
|
|
186
|
+
await waitStatus(m, (s) => s.phase === 'active', 30000)
|
|
187
|
+
return m
|
|
188
|
+
}
|
|
189
|
+
/** Drive the closing meeting to a UNANIMOUS stop vote → the paper trigger (docs/final-paper.md §A4).
|
|
190
|
+
* The same driver then answers the paper's own wakes, so the auto path is exercised end to end.
|
|
191
|
+
* `expectPaper=false` is for the `finalPaper=false` case, where the run concludes immediately and
|
|
192
|
+
* no paper state is ever entered — waiting for `paper.status==='done'` there burns the whole wall
|
|
193
|
+
* budget for nothing. */
|
|
194
|
+
async function closeRun(m, paperOpts, expectPaper = true) {
|
|
195
|
+
const st0 = await waitStatus(m, (s) => s.phase === 'active', 30000)
|
|
196
|
+
assert(st0.phase === 'active', 'run reached the active phase (phase=' + st0.phase + ')')
|
|
197
|
+
await m.callTool('vibe_v4_meeting', { agenda: '收口会议' })
|
|
198
|
+
const answerPaper = paperReply(paperOpts || {})
|
|
199
|
+
return await drive(m, (pt, rid, fu) => {
|
|
200
|
+
if (/meeting is in progress/i.test(pt)) return { input: '同意收口 ' + rid, voteSolved: true, propose_verify: null, propose_task: null, claim_task: null }
|
|
201
|
+
if (/verifying object/i.test(pt)) return { vote: { verdict: 1, reason: 'ok' } }
|
|
202
|
+
if (/\[PAPER /.test(pt)) return answerPaper(pt, rid, fu)
|
|
203
|
+
return { summary: '继续', solved: false }
|
|
204
|
+
}, (st) => st.autoDone === true && (!expectPaper || (st.paper && st.paper.status === 'done')), { maxMs: 90000 })
|
|
205
|
+
}
|
|
206
|
+
const paperReply = (opts) => (pt, rid, fu) => {
|
|
207
|
+
const o = opts || {}
|
|
208
|
+
if (/\[PAPER PART\]/.test(pt)) {
|
|
209
|
+
if (o.partMissing && o.partMissing.indexOf(rid) !== -1) return { summary: '(不发部分)' }
|
|
210
|
+
return { paperPart: { title: '部分 ' + rid, markdown: (o.partText || defaultPart)(rid) } }
|
|
211
|
+
}
|
|
212
|
+
if (/\[PAPER REVIEW\]/.test(pt)) {
|
|
213
|
+
if (o.badReview && o.badReview.indexOf(rid) !== -1) return { paperReview: { reviewed: rid, points: ['自审'], improvements: 'x' } }
|
|
214
|
+
const others = fu && fu.childId ? null : null
|
|
215
|
+
const other = (o.otherOf && o.otherOf[rid]) || (rid === 'r-1' ? 'r-2' : 'r-1')
|
|
216
|
+
return { paperReview: { reviewed: other, points: ['记号不统一:请统一 ' + rid], improvements: '已统一记号与术语' } }
|
|
217
|
+
}
|
|
218
|
+
if (/\[PAPER FINAL\]/.test(pt)) return { paperFinal: { markdown: '# 最终稿(resident 定稿)\n\n## 原问题\n\n由 ' + rid + ' 定稿。\n\n## 规律\n\n- 复用法则一\n' } }
|
|
219
|
+
if (/\[PAPER DELIVERABLE\]/.test(pt) || /\[PAPER REVISE\]/.test(pt)) {
|
|
220
|
+
if (o.dissent && o.dissent.indexOf(rid) !== -1) return { paperDeliverable: false, reason: '第 4 节证据不足' }
|
|
221
|
+
return { paperDeliverable: true, reason: '可交付' }
|
|
222
|
+
}
|
|
223
|
+
return { summary: '继续', solved: false }
|
|
224
|
+
}
|
|
225
|
+
const defaultPart = (rid) => ['## ' + rid + ' 的部分', '', '我们证明了结论 ' + rid + '。', '', '- 记号:统一用 $n$ 表示自然数', '',
|
|
226
|
+
'- 证据:`Verified/命题/p-ok.md`'].join('\n')
|
|
227
|
+
|
|
228
|
+
// =====================================================================================
|
|
229
|
+
section('1 parameters: defaults, schema, raw-string coercion, rejection')
|
|
230
|
+
{
|
|
231
|
+
const m = makeCtx(); await newPlugin(m)
|
|
232
|
+
const st = await m.callTool('vibe_v4_status', {})
|
|
233
|
+
assert(st.paper.params.finalPaper === true && st.paper.params.paperFormat === 'both' && st.paper.params.paperLanguage === 'zh'
|
|
234
|
+
&& st.paper.params.paperCompilePdf === true && st.paper.params.paperEditor === 'office' && st.paper.params.paperLatexCommand === '',
|
|
235
|
+
'★ defaults: finalPaper=true, paperFormat=both, paperLanguage=zh, paperCompilePdf=true, paperEditor=office, paperLatexCommand="" (got ' + JSON.stringify(st.paper.params) + ')')
|
|
236
|
+
const setSpec = m.toolRegs.find(t => t.name === 'vibe_v4_set')
|
|
237
|
+
for (const k of ['finalPaper', 'paperFormat', 'paperLanguage', 'paperCompilePdf', 'paperEditor', 'paperLatexCommand']) {
|
|
238
|
+
assert(!!(setSpec.parameters.properties && setSpec.parameters.properties[k]), '★ vibe_v4_set schema advertises ' + k)
|
|
239
|
+
}
|
|
240
|
+
assert(JSON.stringify(setSpec.parameters.properties.paperFormat.enum) === JSON.stringify(['both', 'md', 'tex'])
|
|
241
|
+
&& JSON.stringify(setSpec.parameters.properties.paperLanguage.enum) === JSON.stringify(['zh', 'en']),
|
|
242
|
+
'the schema narrows paperFormat/paperLanguage to their real enums')
|
|
243
|
+
assert(/finalPaper/.test(setSpec.description) && /paperEditor/.test(setSpec.description) && /paperLatexCommand/.test(setSpec.description),
|
|
244
|
+
'★ the tool HELP names the paper parameters (a schema key the help never mentions is not discoverable)')
|
|
245
|
+
// RAW STRINGS are what `/v4 set` hands the parameter layer (docs/final-paper.md §B): 'false' must not stay truthy.
|
|
246
|
+
const r1 = await m.callTool('vibe_v4_set', { finalPaper: 'false', paperFormat: 'tex', paperLanguage: 'en', paperCompilePdf: '0', paperEditor: 'resident:r-2' })
|
|
247
|
+
assert(r1.paper.params.finalPaper === false && r1.paper.params.paperFormat === 'tex' && r1.paper.params.paperLanguage === 'en' && r1.paper.params.paperCompilePdf === false && r1.paper.params.paperEditor === 'resident:r-2',
|
|
248
|
+
'★ raw strings are coerced: finalPaper="false"→false, paperCompilePdf="0"→false, enums accepted (got ' + JSON.stringify(r1.paper.params) + ')')
|
|
249
|
+
const r2 = await m.callTool('vibe_v4_set', { finalPaper: 'banana', paperFormat: 'docx', paperLanguage: 'fr', paperEditor: 'nobody' })
|
|
250
|
+
assert(r2.paper.params.finalPaper === true && r2.paper.params.paperFormat === 'both' && r2.paper.params.paperLanguage === 'zh' && r2.paper.params.paperEditor === 'office',
|
|
251
|
+
'★ bad values fall back to the DEFAULTS (never to a stronger/different behaviour): ' + JSON.stringify(r2.paper.params))
|
|
252
|
+
const r3 = await m.callTool('vibe_v4_set', { paperNonsense: 1 })
|
|
253
|
+
assert(r3.ok === false && Array.isArray(r3.ignored) && r3.ignored.indexOf('paperNonsense') !== -1, 'an unknown key is refused with ok:false + ignored[] (never silent)')
|
|
254
|
+
assert(st.paper.closureSignal.indexOf('finalizeMeeting/allSolved') === 0, '★ status cites the REAL closure branch (v2 §A4): ' + st.paper.closureSignal)
|
|
255
|
+
await finish(m)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// =====================================================================================
|
|
259
|
+
section('2 negative triggers: not closed, and finalPaper=false')
|
|
260
|
+
{
|
|
261
|
+
const m = makeCtx(); await newPlugin(m)
|
|
262
|
+
await startRun(m)
|
|
263
|
+
const st = await m.callTool('vibe_v4_status', {})
|
|
264
|
+
assert(!existsSync(join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper')), '★ a run that has NOT closed writes no Paper/ directory (negative trigger)')
|
|
265
|
+
assert(st.paper.status === 'idle', 'and status reports idle (got ' + st.paper.status + ')')
|
|
266
|
+
await finish(m)
|
|
267
|
+
}
|
|
268
|
+
{
|
|
269
|
+
const m = makeCtx(); await newPlugin(m)
|
|
270
|
+
await startRun(m, 'final-paper-off', { finalPaper: false })
|
|
271
|
+
const st = await closeRun(m, {}, false) // finalPaper=false: the run concludes with NO paper phase
|
|
272
|
+
assert(st.autoDone === true, 'finalPaper=false: the run still closes normally')
|
|
273
|
+
assert(!existsSync(join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper')), '★ finalPaper=false suppresses the automatic paper entirely')
|
|
274
|
+
const cmd = await m.cmd('paper')
|
|
275
|
+
const parsed = JSON.parse(cmd.text)
|
|
276
|
+
assert(cmd.kind === 'success' && parsed.ok === true && /finalPaper=false/.test(JSON.stringify(parsed)),
|
|
277
|
+
'★ ...but the MANUAL /v4 paper command still works and says the automatic trigger is off')
|
|
278
|
+
await finish(m)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// =====================================================================================
|
|
282
|
+
section('3 automatic trigger on the unanimous stop vote + full team flow + 9-section md/tex')
|
|
283
|
+
{
|
|
284
|
+
const m = makeCtx(); await newPlugin(m)
|
|
285
|
+
await startRun(m, '团队合写测试')
|
|
286
|
+
await m.callToolAs('vibe_v4_record_proposition', { id: 'p-unres', title: '未决命题', statement: 's', prob: 0.4, value: 0.3, motivation: 'm' }, m.spawns[0].childId)
|
|
287
|
+
COMPILER.mode = 'ok'; COMPILER.engines = ['xelatex', 'latexmk']
|
|
288
|
+
const st = await closeRun(m)
|
|
289
|
+
// The PAPER must actually have run: without `st.paper.status==='done' && finalizedAt>0` this
|
|
290
|
+
// assertion also holds when the paper phase is disabled (docs/final-paper.md §A1 mutant), and the case would
|
|
291
|
+
// only die later on a missing artifact. It is the paper phase's own success that must be asserted.
|
|
292
|
+
assert(st.autoDone === true && st.running === false && st.paper.status === 'done' && st.paper.finalizedAt > 0 && st.paper.trigger === 'auto',
|
|
293
|
+
'★ the paper ran to completion (status=' + st.paper.status + ', finalizedAt=' + st.paper.finalizedAt + ', trigger=' + st.paper.trigger + ') and only THEN the run was marked done (docs/final-paper.md §A1)')
|
|
294
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
295
|
+
const meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
296
|
+
const md = readIf(join(dir, 'paper.md')), tex = readIf(join(dir, 'paper.tex')), log = readIf(join(dir, 'paper.log.md'))
|
|
297
|
+
assert(existsSync(dir) && !!md && !!tex && !!log && meta.dir === 'Paper/' + meta.id, '★ Paper/<run id>/{paper.md,paper.tex,paper.meta.json,paper.log.md} all exist (meta.dir=' + meta.dir + ')')
|
|
298
|
+
const heads = ['标题、作者、日期与摘要', '引言与问题背景', '原问题的完整解法', '已检验通过的命题', '已解决的子问题与中间成果', '创造或发现的有价值之物', '规律总结', '讨论、局限与展望', '附录:证据与文件索引']
|
|
299
|
+
assert(heads.every((h, i) => md.indexOf('## ' + (i + 1) + '. ' + h) !== -1), '★ the md carries the exact 9-section skeleton (docs/final-paper.md §3)')
|
|
300
|
+
assert(heads.every((h, i) => tex.indexOf('\\section{' + h + '}') !== -1), '★ the tex carries the same 9 sections')
|
|
301
|
+
assert(/\\documentclass\[11pt\]\{ctexart\}/.test(tex) && /\\usepackage\{amsmath\}/.test(tex) && /\\usepackage\{hyperref\}/.test(tex),
|
|
302
|
+
'the zh template is ctexart with the documented package set (docs/final-paper.md §5)')
|
|
303
|
+
assert(/Verified\/命题\/p-ok\.md|Propos\//.test(md) && /p-ok/.test(md), '★ the evidence index names real card paths')
|
|
304
|
+
assert(/\[未决\]/.test(md) && /p-unres/.test(md), '★ an unverified object is explicitly flagged [未决] (docs/final-paper.md §3 writing rule)')
|
|
305
|
+
assert(/Vibe-Mathematics v4/.test(md) && /团队合写测试/.test(md), 'the title/abstract carry the preset name and the original problem')
|
|
306
|
+
assert(meta.finalizedAt > 0 && meta.trigger === 'auto' && meta.params && meta.params.paperFormat === 'both' && meta.params.paperLanguage === 'zh',
|
|
307
|
+
'★ paper.meta.json records the trigger, the params, the language and finalizedAt')
|
|
308
|
+
assert((meta.parts || []).length === 2 && (meta.reviews || []).length === 2 && (meta.deliverable || []).length === 2,
|
|
309
|
+
'★ meta records who wrote a part, who reviewed (and whom), and every deliverability statement')
|
|
310
|
+
assert((meta.reviews || []).every(r => r.reviewed && r.reviewed !== r.id), '★ every review names ANOTHER participant (docs/final-paper.md §4.3)')
|
|
311
|
+
// The no-invention contract must reach EVERY paper wake (v2/v3 already assert it; v4 shipped the
|
|
312
|
+
// clause in the prompt but nothing guarded it — a prompt-level regression would have been silent).
|
|
313
|
+
const paperPrompts = m.followups.map(f => (f.blocks && f.blocks[0] && f.blocks[0].text) || '').filter(t => /\[PAPER /.test(t))
|
|
314
|
+
assert(paperPrompts.length >= 6 && paperPrompts.every(t => /只整理\*\*已有证据\*\*/.test(t) && /不得编造/.test(t) && /未决\/被否证的条目必须显式标注/.test(t)),
|
|
315
|
+
'★ every one of the ' + paperPrompts.length + ' paper wakes carries the no-invention contract: only reorganise EXISTING evidence, never invent, and flag unresolved/refuted items explicitly')
|
|
316
|
+
assert(/互审/.test(log) && /已采纳/.test(log) && /交付表态/.test(log) && /定稿/.test(log),
|
|
317
|
+
'★ paper.log.md records who wrote what, who raised which review point and how it was handled')
|
|
318
|
+
assert(/facilitator 合并|合并完成/.test(log) && meta.mergeNotes && typeof meta.mergeNotes.dups === 'number',
|
|
319
|
+
'★ the framework merge step ran and recorded its dedup/term-unification bookkeeping')
|
|
320
|
+
assert(m.spawns.length === 2, 'the team was reused (no extra residents were spawned for the paper)')
|
|
321
|
+
await finish(m)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// =====================================================================================
|
|
325
|
+
section('4 idempotency: re-triggering the same run only fills missing artifacts')
|
|
326
|
+
{
|
|
327
|
+
const m = makeCtx(); await newPlugin(m)
|
|
328
|
+
await startRun(m, '幂等测试')
|
|
329
|
+
COMPILER.mode = 'ok'; COMPILER.engines = ['xelatex']
|
|
330
|
+
const st = await closeRun(m)
|
|
331
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
332
|
+
const meta1 = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
333
|
+
const md1 = readIf(join(dir, 'paper.md'))
|
|
334
|
+
await sleep(20)
|
|
335
|
+
const again = await m.cmd('paper')
|
|
336
|
+
const parsed = JSON.parse(again.text)
|
|
337
|
+
assert(again.kind === 'success' && parsed.ok === true && parsed.idempotent === true, '★ a second trigger is recognised as already finalized (idempotent=true)')
|
|
338
|
+
const meta2 = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
339
|
+
assert(meta2.finalizedAt === meta1.finalizedAt && meta2.filled === undefined && readIf(join(dir, 'paper.md')) === md1,
|
|
340
|
+
'★ nothing was rewritten: finalizedAt unchanged, paper.md byte-identical')
|
|
341
|
+
rmSync(join(dir, 'paper.pdf'), { force: true })
|
|
342
|
+
const refill = await m.cmd('paper')
|
|
343
|
+
const p3 = JSON.parse(refill.text)
|
|
344
|
+
assert(p3.idempotent === true && existsSync(join(dir, 'paper.pdf')), '★ a missing artifact (deleted pdf) is the ONLY thing a re-trigger writes (filled=' + JSON.stringify(p3.filled) + ')')
|
|
345
|
+
const forced = await m.cmd('paper force')
|
|
346
|
+
const p4 = JSON.parse(forced.text)
|
|
347
|
+
assert(forced.kind === 'success' && p4.ok === true && !p4.idempotent, '★ /v4 paper force rewrites instead of short-circuiting')
|
|
348
|
+
await finish(m)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// =====================================================================================
|
|
352
|
+
section('5 /v4 paper overrides: lang=en, format=tex, editor, and error kind')
|
|
353
|
+
{
|
|
354
|
+
const m = makeCtx(); await newPlugin(m)
|
|
355
|
+
await startRun(m, 'override-test')
|
|
356
|
+
COMPILER.mode = 'none'
|
|
357
|
+
const r = await m.cmd('paper lang=en format=tex')
|
|
358
|
+
const parsed = JSON.parse(r.text)
|
|
359
|
+
assert(r.kind === 'success' && parsed.ok === true, '/v4 paper lang=en format=tex was accepted')
|
|
360
|
+
const st = await drive(m, paperReply({}), (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
361
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
362
|
+
const tex = readIf(join(dir, 'paper.tex'))
|
|
363
|
+
assert(existsSync(join(dir, 'paper.tex')) && !existsSync(join(dir, 'paper.md')), '★ format=tex produces paper.tex and NO paper.md')
|
|
364
|
+
assert(/\\documentclass\[11pt\]\{article\}/.test(tex), '★ lang=en switches the template to article (no ctexart)')
|
|
365
|
+
// The English variant must carry the same no-invention contract as the zh one.
|
|
366
|
+
const enPaperPrompts = m.followups.map(f => (f.blocks && f.blocks[0] && f.blocks[0].text) || '').filter(t => /\[PAPER /.test(t))
|
|
367
|
+
assert(enPaperPrompts.length > 0 && enPaperPrompts.every(t => /Only organise EXISTING evidence/.test(t) && /never invent/.test(t) && /flag unresolved\/refuted items explicitly/.test(t)),
|
|
368
|
+
'★ the lang=en paper wakes carry the English no-invention contract too (' + enPaperPrompts.length + ' wakes)')
|
|
369
|
+
assert(/\[PAPER DELIVERABLE\]/.test(m.followups.map(f => (f.blocks && f.blocks[0] && f.blocks[0].text) || '').join('\n')), 'the deliverability vote was held')
|
|
370
|
+
const bad = await m.cmd('paper nonsense=1')
|
|
371
|
+
assert(bad.kind === 'error' && JSON.parse(bad.text).ok === false, '★ a bad /v4 paper option returns kind:\'error\' (never a silent success)')
|
|
372
|
+
const hint = m.cmdRegs.find(c => c.name === 'v4').input.hint
|
|
373
|
+
assert(/paper/.test(hint), 'the /v4 slash hint advertises the paper subcommand')
|
|
374
|
+
await finish(m)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// =====================================================================================
|
|
378
|
+
section('6 paperEditor=resident:<id> and the dismissed-editor downgrade (v2 §A5)')
|
|
379
|
+
{
|
|
380
|
+
const m = makeCtx(); await newPlugin(m)
|
|
381
|
+
await startRun(m, 'resident-editor')
|
|
382
|
+
COMPILER.mode = 'ok'; COMPILER.engines = ['xelatex']
|
|
383
|
+
const start = await m.cmd('paper editor=resident:r-2')
|
|
384
|
+
assert(JSON.parse(start.text).ok === true, 'paperEditor=resident:r-2 accepted by the command')
|
|
385
|
+
let sawFinal = false, finalRid = ''
|
|
386
|
+
const st = await drive(m, (pt, rid, fu) => {
|
|
387
|
+
if (/\[PAPER FINAL\]/.test(pt)) { sawFinal = true; finalRid = rid }
|
|
388
|
+
return paperReply({})(pt, rid, fu)
|
|
389
|
+
}, (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
390
|
+
assert(sawFinal && finalRid === 'r-2', '★ the NAMED resident (r-2) received the [PAPER FINAL] turn (got ' + finalRid + ')')
|
|
391
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
392
|
+
assert(/resident 定稿/.test(readIf(join(dir, 'paper.md'))), '★ the resident\'s own final text reached paper.md')
|
|
393
|
+
const meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
394
|
+
assert(meta.editor === 'resident:r-2' && meta.editorId === 'r-2' && meta.editorDowngraded === false, 'meta records the named editor')
|
|
395
|
+
await finish(m)
|
|
396
|
+
}
|
|
397
|
+
{
|
|
398
|
+
const m = makeCtx(); await newPlugin(m)
|
|
399
|
+
await startRun(m, 'dismissed-editor')
|
|
400
|
+
await m.callTool('vibe_v4_remove_member', { id: 'r-2' })
|
|
401
|
+
COMPILER.mode = 'ok'; COMPILER.engines = ['xelatex']
|
|
402
|
+
const start = await m.cmd('paper editor=resident:r-2')
|
|
403
|
+
assert(JSON.parse(start.text).ok === true, 'paper started with an editor who has just been dismissed')
|
|
404
|
+
const st = await drive(m, paperReply({}), (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
405
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
406
|
+
const meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
407
|
+
assert(meta.editorDowngraded === true && meta.editorId === 'office' && /降级/.test(readIf(join(dir, 'paper.log.md'))),
|
|
408
|
+
'★ a dismissed editor is DOWNGRADED to office and the downgrade is recorded in meta + log (never silently substituted)')
|
|
409
|
+
await finish(m)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// =====================================================================================
|
|
413
|
+
section('7 negative team flow: an invalid review cannot finalise (and is warned if it never lands)')
|
|
414
|
+
{
|
|
415
|
+
const m = makeCtx(); await newPlugin(m)
|
|
416
|
+
await startRun(m, 'bad-review')
|
|
417
|
+
const start = await m.cmd('paper lang=en format=md')
|
|
418
|
+
assert(JSON.parse(start.text).ok === true, 'paper started to observe the review guard')
|
|
419
|
+
let badAnswers = 0
|
|
420
|
+
// r-1 answers its review by naming ITSELF → must be refused; r-2 answers properly.
|
|
421
|
+
const st1 = await drive(m, (pt, rid, fu) => {
|
|
422
|
+
if (/\[PAPER REVIEW\]/.test(pt) && rid === 'r-1') { badAnswers++; return { paperReview: { reviewed: 'r-1', points: ['自审'] } } }
|
|
423
|
+
return paperReply({})(pt, rid, fu)
|
|
424
|
+
}, (s) => badAnswers >= 1 && s.paper && s.paper.status === 'review', { maxMs: 30000 })
|
|
425
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st1.paper.id)
|
|
426
|
+
assert(badAnswers >= 1, 'the invalid review was actually submitted at least once')
|
|
427
|
+
assert(st1.paper.status === 'review' && !existsSync(join(dir, 'paper.md')),
|
|
428
|
+
'★ an invalid review is REFUSED and the paper does NOT finalise (status=' + st1.paper.status + ', bad review still outstanding)')
|
|
429
|
+
// Let the step deadline expire: the flow must escape with an explicit warning + an appendix note.
|
|
430
|
+
const st2 = await drive(m, (pt, rid, fu) => {
|
|
431
|
+
if (/\[PAPER REVIEW\]/.test(pt) && rid === 'r-1') return { paperReview: { reviewed: 'r-1', points: ['仍然自审'] } }
|
|
432
|
+
return paperReply({})(pt, rid, fu)
|
|
433
|
+
}, (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
434
|
+
const meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
435
|
+
assert(st2.paper.status === 'done' && /超时未回应|互审/.test(meta.warning || ''), '★ after the deadline the incomplete review is recorded as a warning, never silently dropped')
|
|
436
|
+
assert(/\[未决\]|互审超时/.test(readIf(join(dir, 'paper.md')) + readIf(join(dir, 'paper.log.md'))), '★ the incomplete review is visible in the artifacts')
|
|
437
|
+
await finish(m)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// =====================================================================================
|
|
441
|
+
section('8 dissent: bounded iteration, then the appendix records it and a warning is raised')
|
|
442
|
+
{
|
|
443
|
+
const m = makeCtx(); await newPlugin(m)
|
|
444
|
+
await startRun(m, 'dissent-test')
|
|
445
|
+
COMPILER.mode = 'none'
|
|
446
|
+
const start = await m.cmd('paper lang=en format=md')
|
|
447
|
+
assert(JSON.parse(start.text).ok === true, 'paper started for the dissent case')
|
|
448
|
+
const st = await drive(m, paperReply({ dissent: ['r-1'] }), (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
449
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
450
|
+
const md = readIf(join(dir, 'paper.md')), meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
451
|
+
assert(st.paper.status === 'done' && (meta.dissent || []).indexOf('r-1') !== -1, '★ dissent did not block finalisation forever: the run iterated and then recorded the dissent (rounds=' + meta.rounds + '/' + meta.maxRounds + ')')
|
|
452
|
+
assert(/附录:交付分歧|Appendix: deliverability dissent/.test(md), '★ the disagreement is written into the appendix (docs/final-paper.md §4.5)')
|
|
453
|
+
assert(/迭代达到上限/.test(meta.warning || ''), '★ and the cap is warned about in meta.log')
|
|
454
|
+
await finish(m)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// =====================================================================================
|
|
458
|
+
section('9 compile branches: fake compiler success / repaired / persistent failure / none')
|
|
459
|
+
{
|
|
460
|
+
const m = makeCtx(); await newPlugin(m)
|
|
461
|
+
await startRun(m, 'compile-ok')
|
|
462
|
+
COMPILER.mode = 'ok'; COMPILER.engines = ['xelatex']
|
|
463
|
+
const st = await closeRun(m)
|
|
464
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
465
|
+
const meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
466
|
+
assert(existsSync(join(dir, 'paper.pdf')) && (meta.compile || {}).result === 'ok' && (meta.compile || {}).engine === 'xelatex',
|
|
467
|
+
'★ fake compiler success: paper.pdf exists, meta.compile={result:ok, engine:xelatex}')
|
|
468
|
+
const c0 = (meta.compile || {}).attempts || []
|
|
469
|
+
assert(!!c0[0] && c0[0].stage === 'initial' && c0[0].engine === 'xelatex', 'the initial attempt is recorded')
|
|
470
|
+
await finish(m)
|
|
471
|
+
}
|
|
472
|
+
{
|
|
473
|
+
const m = makeCtx(); await newPlugin(m)
|
|
474
|
+
await startRun(m, 'repair-path')
|
|
475
|
+
COMPILER.mode = 'repair'; COMPILER.engines = ['xelatex']
|
|
476
|
+
const start = await m.cmd('paper lang=en format=both')
|
|
477
|
+
assert(JSON.parse(start.text).ok === true, 'paper started for the repair branch')
|
|
478
|
+
const st = await drive(m, paperReply({}), (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
479
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
480
|
+
const meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
481
|
+
assert(existsSync(join(dir, 'paper.pdf')) && (meta.compile || {}).result === 'ok' && (meta.compile || {}).repaired === 'minimal-template',
|
|
482
|
+
'★ a failing tex is REPAIRED by the one minimal-template retry, then compiles (repaired=' + ((meta.compile || {}).repaired || '') + ')')
|
|
483
|
+
assert(((meta.compile || {}).attempts || []).some(a => a.stage === 'minimal-template' && a.ok), 'the repair attempt is recorded in meta')
|
|
484
|
+
assert(/\\documentclass/.test(readIf(join(dir, 'paper.tex'))) && !/\\usepackage/.test(readIf(join(dir, 'paper.tex'))),
|
|
485
|
+
'the on-disk tex is the minimal template that actually compiled')
|
|
486
|
+
await finish(m)
|
|
487
|
+
}
|
|
488
|
+
{
|
|
489
|
+
const m = makeCtx(); await newPlugin(m)
|
|
490
|
+
await startRun(m, 'compile-degrade')
|
|
491
|
+
COMPILER.mode = 'fail'; COMPILER.engines = ['xelatex', 'latexmk']
|
|
492
|
+
const start = await m.cmd('paper lang=en format=both')
|
|
493
|
+
const st = await drive(m, paperReply({}), (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
494
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
495
|
+
const meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
496
|
+
assert(st.paper.status === 'done' && !existsSync(join(dir, 'paper.pdf')) && existsSync(join(dir, 'paper.tex')) && existsSync(join(dir, 'paper.md')),
|
|
497
|
+
'★ persistent compiler failure DEGRADES: tex+md kept, no pdf, finalisation NOT blocked')
|
|
498
|
+
assert(meta.compile && meta.compile.result === 'failed' && /编译失败/.test(meta.compile.error || '') && /编译失败/.test(meta.warning || readIf(join(dir, 'paper.log.md'))),
|
|
499
|
+
'★ meta records compile:failed and the failure is warned + reported')
|
|
500
|
+
assert(((meta.compile || {}).attempts || []).length >= 2 && meta.compile.attempts.every(a => a.ok === false), 'every capped repair attempt is recorded')
|
|
501
|
+
await finish(m)
|
|
502
|
+
}
|
|
503
|
+
{
|
|
504
|
+
const m = makeCtx(); await newPlugin(m)
|
|
505
|
+
await startRun(m, 'no-latex')
|
|
506
|
+
COMPILER.mode = 'none'; COMPILER.engines = []
|
|
507
|
+
const start = await m.cmd('paper lang=zh format=both')
|
|
508
|
+
const st = await drive(m, paperReply({}), (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
509
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
510
|
+
const meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
511
|
+
assert(st.paper.status === 'done' && (meta.compile || {}).result === 'not-detected' && /未检测到 LaTeX/.test((meta.compile || {}).reason || ''),
|
|
512
|
+
'★ no LaTeX engine: compile=not-detected, tex+md kept, NO error thrown')
|
|
513
|
+
assert(existsSync(join(dir, 'paper.tex')) && existsSync(join(dir, 'paper.md')), 'tex+md are still produced on the no-LaTeX branch')
|
|
514
|
+
// The paper body is composed BEFORE compilation, so the outcome cannot live in the spec: the delivered
|
|
515
|
+
// artifacts themselves must state it, exactly once (finalize can be re-entered -> idempotent).
|
|
516
|
+
const mdDelivered = readIf(join(dir, 'paper.md'))
|
|
517
|
+
const texDelivered = readIf(join(dir, 'paper.tex'))
|
|
518
|
+
const mdHits = mdDelivered.split('只产出 tex+md').length - 1
|
|
519
|
+
const texHits = texDelivered.split('只产出 tex+md').length - 1
|
|
520
|
+
assert(mdHits === 1 && texHits === 1,
|
|
521
|
+
'★ [deliverable/v4] the DELIVERED paper.md and paper.tex state that no engine was detected (once each; md=' + mdHits + ' tex=' + texHits + ')')
|
|
522
|
+
const stNoTexJson = JSON.stringify(await m.callTool('vibe_v4_status', {}))
|
|
523
|
+
assert(stNoTexJson.indexOf('只产出 tex+md') !== -1 && stNoTexJson.indexOf('已探测 PATH 与文档化的常见 TeX 根;可用 paperLatexCommand 指定绝对路径') !== -1,
|
|
524
|
+
'★ [task-9/v4] the not-detected state WARNS with the old tail + where it looked + how to pin an engine')
|
|
525
|
+
await finish(m)
|
|
526
|
+
}
|
|
527
|
+
{
|
|
528
|
+
// task-10: an explicit paperLatexCommand is honoured LITERALLY — if it cannot be resolved the run
|
|
529
|
+
// degrades as "not detected"; it must NEVER silently fall through to a different engine.
|
|
530
|
+
const m = makeCtx(); await newPlugin(m)
|
|
531
|
+
await startRun(m, 'explicit-unresolvable', { paperLatexCommand: 'Z:/definitely/not/here/xelatex.exe' })
|
|
532
|
+
COMPILER.mode = 'ok'; COMPILER.engines = ['xelatex', 'pdflatex'] // engines ARE available: the explicit choice still wins
|
|
533
|
+
await m.cmd('paper lang=zh format=both')
|
|
534
|
+
const stExplicit = await drive(m, paperReply({}), (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
535
|
+
const metaExplicit = readJsonIf(join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', stExplicit.paper.id, 'paper.meta.json')) || {}
|
|
536
|
+
assert((metaExplicit.compile || {}).result === 'not-detected',
|
|
537
|
+
'★ [task-10/v4] an unresolvable explicit paperLatexCommand degrades (never falls through to another engine) — got ' + JSON.stringify((metaExplicit.compile || {}).result))
|
|
538
|
+
await finish(m)
|
|
539
|
+
}
|
|
540
|
+
{
|
|
541
|
+
const m = makeCtx(); await newPlugin(m)
|
|
542
|
+
await startRun(m, 'no-compile', { paperCompilePdf: false })
|
|
543
|
+
COMPILER.mode = 'ok'; COMPILER.engines = ['xelatex'] // an engine IS available — the knob must still win
|
|
544
|
+
const start = await m.cmd('paper lang=en format=both')
|
|
545
|
+
const st = await drive(m, paperReply({}), (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
546
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
547
|
+
const meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
548
|
+
assert((meta.compile || {}).result === 'skipped' && /paperCompilePdf=false/.test((meta.compile || {}).reason || '') && !existsSync(join(dir, 'paper.pdf')),
|
|
549
|
+
'★ paperCompilePdf=false skips compilation even when a LaTeX engine is available')
|
|
550
|
+
await finish(m)
|
|
551
|
+
}
|
|
552
|
+
{
|
|
553
|
+
const m = makeCtx(); await newPlugin(m)
|
|
554
|
+
await startRun(m, 'no-compile')
|
|
555
|
+
COMPILER.mode = 'none'
|
|
556
|
+
const start = await m.cmd('paper lang=en format=md')
|
|
557
|
+
const st = await drive(m, paperReply({}), (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
558
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
559
|
+
const meta = readJsonIf(join(dir, 'paper.meta.json')) || {}
|
|
560
|
+
assert((meta.compile || {}).result === 'skipped' && /paperFormat=md/.test((meta.compile || {}).reason || ''),
|
|
561
|
+
'★ format=md skips compilation without a "cannot compile without tex" warning (v2 §E)')
|
|
562
|
+
await finish(m)
|
|
563
|
+
}
|
|
564
|
+
{
|
|
565
|
+
const m = makeCtx(); await newPlugin(m)
|
|
566
|
+
await startRun(m, 'existing-pdf')
|
|
567
|
+
COMPILER.mode = 'ok'; COMPILER.engines = ['xelatex']
|
|
568
|
+
const st = await closeRun(m)
|
|
569
|
+
const dir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', st.paper.id)
|
|
570
|
+
// The directory may legitimately not exist (a missing/failed paper phase): create it so the probe's
|
|
571
|
+
// own write cannot abort the suite with an uncaught ENOENT — the assertion below must be the
|
|
572
|
+
// failure, not a crash (AUDIT-CHECKLIST §4: a missing artifact is one clean assertion failure).
|
|
573
|
+
mkdirSync(dir, { recursive: true })
|
|
574
|
+
writeFileSync(join(dir, 'paper.pdf'), 'PRE-EXISTING')
|
|
575
|
+
const again = await m.cmd('paper')
|
|
576
|
+
assert(JSON.parse(again.text).idempotent === true && readIf(join(dir, 'paper.pdf')) === 'PRE-EXISTING',
|
|
577
|
+
'★ an existing paper.pdf is never clobbered (docs/final-paper.md §D)')
|
|
578
|
+
await finish(m)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// =====================================================================================
|
|
582
|
+
section('10 containment: a hostile problem id stays inside Paper/<id>/')
|
|
583
|
+
{
|
|
584
|
+
const m = makeCtx(); await newPlugin(m)
|
|
585
|
+
await startRun(m, '../../etc/evil:name')
|
|
586
|
+
COMPILER.mode = 'none'
|
|
587
|
+
const start = await m.cmd('paper')
|
|
588
|
+
const st = await drive(m, paperReply({}), (s) => s.paper && s.paper.status === 'done', { maxMs: 90000 })
|
|
589
|
+
const id = st.paper.id
|
|
590
|
+
assert(id.indexOf('..') === -1 && id.indexOf('/') === -1 && id.indexOf('\\') === -1 && id.indexOf(':') === -1,
|
|
591
|
+
'★ the paper id is normalised (no separators / no traversal): ' + id)
|
|
592
|
+
const paperDir = join(m.WS, 'VibeMath', 'Projects', 'default', 'Paper', id)
|
|
593
|
+
assert(existsSync(join(paperDir, 'paper.meta.json')), 'everything landed under Paper/<id>/')
|
|
594
|
+
const stray = join(m.WS, 'VibeMath', 'etc')
|
|
595
|
+
assert(!existsSync(stray), '★ no file escaped the Paper/ directory')
|
|
596
|
+
const stateDir = join(m.WS, 'VibeMath', 'Projects', 'default', 'State')
|
|
597
|
+
const before = (existsSync(stateDir) ? readdirSync(stateDir) : []).sort().join(',')
|
|
598
|
+
assert(before.indexOf('paper') === -1, '★ State/ is not polluted by paper artifacts')
|
|
599
|
+
await finish(m)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
console.log('')
|
|
603
|
+
section('G3 a paper whose REQUIRED artifact was not written is FAILED, never done')
|
|
604
|
+
{
|
|
605
|
+
const m = makeCtx(); await newPlugin(m)
|
|
606
|
+
await startRun(m, 'G3 缺失产物测试')
|
|
607
|
+
const seen = []
|
|
608
|
+
const realErr = console.error
|
|
609
|
+
console.error = (...a) => { seen.push(a.map(String).join(' ')); realErr(...a) }
|
|
610
|
+
try {
|
|
611
|
+
// only the two artifacts whose failure used to be invisible are made to fail
|
|
612
|
+
g3WriteFail = /paper\.(meta\.json|log\.md)$/
|
|
613
|
+
// expectPaper=false: the paper will NOT reach `done`, so closing must not wait for it
|
|
614
|
+
const st = await closeRun(m, {}, false)
|
|
615
|
+
const warned = seen.filter(l => /paperFinalize: required paper artifact\(s\) MISSING after finalize/.test(l))
|
|
616
|
+
assert(warned.length === 1, '* G3: the missing required artifact is NAMED exactly once (matched=' + warned.length + ' of ' + seen.length + ' stderr lines)')
|
|
617
|
+
assert(/paper\.meta\.json|paper\.log\.md/.test(warned[0] || ''), '* G3: the warning NAMES the missing artifact(s) (' + String(warned[0] || '').slice(0, 120) + ')')
|
|
618
|
+
assert(st.paper.status === 'failed', '* G3: the paper status is FAILED, never done (status=' + st.paper.status + ')')
|
|
619
|
+
assert((st.paper.files || []).indexOf('paper.meta.json') === -1 && (st.paper.files || []).indexOf('paper.log.md') === -1, '* G3: `files` never claims the artifacts that were not written (' + JSON.stringify(st.paper.files || []) + ')')
|
|
620
|
+
} finally { console.error = realErr; g3WriteFail = null }
|
|
621
|
+
await finish(m)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
console.log('passed=' + passed + ' failed=' + failed)
|
|
625
|
+
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f) }
|
|
626
|
+
if (failed) process.exit(1)
|
|
627
|
+
console.log('ALL GREEN')
|
|
628
|
+
process.exit(0)
|