dsh-vibe-math 2.2.2 → 2.3.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/AUDIT-CHECKLIST.md +61 -3
- package/README.md +119 -1
- package/RELEASE-NOTES-2.3.0.md +207 -0
- package/RELEASE-NOTES-2.3.1.md +134 -0
- package/audit-formal-sensitivity.mjs +333 -0
- package/audit-persona-sensitivity.mjs +249 -0
- package/audit-persona-surface.test.mjs +349 -0
- package/audit-v5-integrity.mjs +43 -2
- package/audit-v5-sensitivity.mjs +77 -6
- package/docs/formal-verification.md +401 -0
- package/docs/generate_framework_diagram_v5.mjs +22 -16
- package/docs/test-timing.md +79 -0
- package/formal-verify-v2.test.mjs +951 -0
- package/formal-verify-v3.test.mjs +1031 -0
- package/formal-verify-v4.test.mjs +882 -0
- package/formal-verify-v5.test.mjs +598 -0
- package/package.json +22 -2
- package/prompt-corpus-persona/persona-corpus.json +32 -0
- package/prompt-corpus-persona/persona-corpus.md +674 -0
- package/prompt-corpus-v2/formal-verify-v2.json +394 -0
- package/prompt-corpus-v2/formal-verify-v2.md +4250 -0
- package/prompt-corpus-v3/formal-verify-v3.json +382 -0
- package/prompt-corpus-v3/formal-verify-v3.md +3843 -0
- package/prompt-corpus-v4/formal-verify-v4.json +84 -0
- package/prompt-corpus-v4/formal-verify-v4.md +255 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +109 -5
- package/prompt-corpus-v5/prompt-corpus-v5.md +653 -109
- package/prompt-v5-integrity.test.mjs +1158 -984
- package/run-tests.mjs +99 -0
- package/vibe-math-v2/agent.cordis.yml +40 -2
- package/vibe-math-v2/vibe-math-v2.js +811 -21
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +218 -1
- package/vibe-math-v3/agent.cordis.yml +46 -2
- package/vibe-math-v3/vibe-math-v3.js +810 -21
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +104 -2
- package/vibe-math-v4/agent.cordis.yml +46 -4
- package/vibe-math-v4/vibe-math-v4.js +744 -15
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +255 -0
- package/vibe-math-v5/agent.cordis.yml +41 -5
- package/vibe-math-v5/vibe-math-v5.js +621 -9
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +131 -4
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +57 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +51 -46
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// V2 LEAN FORMAL VERIFICATION SUITE (docs/formal-verification.md)
|
|
3
|
+
//
|
|
4
|
+
// Asserts the whole contract of the v2 `formalVerify` knob:
|
|
5
|
+
// · 'off' is a TRUE no-op: no Lean text in ANY prompt it builds, no formal record,
|
|
6
|
+
// no gate — while the three tools stay registered (registration is static)
|
|
7
|
+
// · 'encourage' injects the Lean section into BOTH the review and the debate prompt and the
|
|
8
|
+
// "顺手形式化" line into the solver/explorer prompts, and — the actual point of
|
|
9
|
+
// the feature — turns the review into a FIDELITY check once a Lean run passed
|
|
10
|
+
// · 'require' withholds a true/false verdict (未定论 + Formal/TODO.md + announcement) until
|
|
11
|
+
// the object is Lean-passed or carries an explicit, reasoned blocker; then the
|
|
12
|
+
// same verdict DOES write the card, and the card records the formal status
|
|
13
|
+
// · the three tools (run / archive / lib) write the right things to the right paths
|
|
14
|
+
// · the REPLY CHANNEL is real, not dead code (contract §4 / §6.3 / §10.8): a verifier's
|
|
15
|
+
// `formal:{decision:'blocked'|'defect', note}` reply is actually absorbed into the durable
|
|
16
|
+
// record for BOTH id spaces, a missing note is refused, and `defect` (a fidelity defect,
|
|
17
|
+
// i.e. the Lean code does not say what the proposition says) downgrades the proof, deletes
|
|
18
|
+
// the archived file, writes the TODO and defers the verdict — it is NEVER recorded as
|
|
19
|
+
// "the proposition is false" (contract §4.1)
|
|
20
|
+
//
|
|
21
|
+
// The Lean toolchain is mocked through the subprocess SERVICE (a fake Lean: exit 0 unless the
|
|
22
|
+
// file still contains `sorry` or the marker `-- FAIL`), so these tests exercise the REAL code
|
|
23
|
+
// path (resolveExecutable → spawn → collected stdout → exit code) without Lean installed.
|
|
24
|
+
//
|
|
25
|
+
// The plugin path honours V2_PLUGIN so a sensitivity probe can point this suite at a mutated
|
|
26
|
+
// copy of the plugin (a suite that ignored the override would make every probe vacuous —
|
|
27
|
+
// AUDIT-CHECKLIST §2.5).
|
|
28
|
+
//
|
|
29
|
+
// Run: node formal-verify-v2.test.mjs
|
|
30
|
+
// ============================================================
|
|
31
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, writeFileSync, statSync, mkdirSync } from 'node:fs'
|
|
32
|
+
import { tmpdir } from 'node:os'
|
|
33
|
+
import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
|
|
34
|
+
import { fileURLToPath } from 'node:url'
|
|
35
|
+
|
|
36
|
+
const PLUGIN = process.env.V2_PLUGIN
|
|
37
|
+
? new URL('file:///' + String(process.env.V2_PLUGIN).replace(/\\/g, '/'))
|
|
38
|
+
: new URL('./vibe-math-v2/vibe-math-v2.js', import.meta.url)
|
|
39
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
40
|
+
// Human-reviewable corpus (contract §10.10). A sensitivity probe runs THIS suite against a
|
|
41
|
+
// MUTATED plugin copy: writing the repository corpus from such a run would replace the
|
|
42
|
+
// reviewed text with mutated text, so a mutated run goes to a scratch directory instead
|
|
43
|
+
// (and V2_CORPUS_DIR overrides both, exactly like the v3 suite's V3_CORPUS_DIR).
|
|
44
|
+
const CORPUS_DIR = process.env.V2_CORPUS_DIR
|
|
45
|
+
? pathResolve(process.env.V2_CORPUS_DIR)
|
|
46
|
+
: (process.env.V2_PLUGIN ? join(tmpdir(), 'vibe-v2-prompt-corpus') : join(HERE, 'prompt-corpus-v2'))
|
|
47
|
+
|
|
48
|
+
let passed = 0, failed = 0
|
|
49
|
+
const failures = []
|
|
50
|
+
const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
|
|
51
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
52
|
+
const section = (t) => console.log('\n[' + t + ']')
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------
|
|
55
|
+
// the fake Lean toolchain (mocked at the SERVICE boundary)
|
|
56
|
+
// ---------------------------------------------------------------
|
|
57
|
+
let toolchainAvailable = true
|
|
58
|
+
const leanRuns = []
|
|
59
|
+
const subprocess = {
|
|
60
|
+
async resolveExecutable(cmd) {
|
|
61
|
+
if (!toolchainAvailable) throw new Error('spawn lean ENOENT')
|
|
62
|
+
if (String(cmd) !== 'lean') throw new Error('unknown executable ' + cmd)
|
|
63
|
+
return 'lean'
|
|
64
|
+
},
|
|
65
|
+
spawn(spec) {
|
|
66
|
+
const last = spec.argv[spec.argv.length - 1]
|
|
67
|
+
const script = String(last || '')
|
|
68
|
+
// v2 creates its directory tree through this SAME subprocess service
|
|
69
|
+
// (`powershell … New-Item -Force -ItemType Directory -Path 'a','b'`). Without honouring
|
|
70
|
+
// it, the Formal/ and Verified/Lean/ assertions would be vacuous: they would fail for the
|
|
71
|
+
// wrong reason (a mocked no-op) instead of a real defect.
|
|
72
|
+
if (/New-Item/.test(script)) {
|
|
73
|
+
const m = /-Path\s+(.+?)\s*(\||$)/.exec(script)
|
|
74
|
+
if (m) m[1].split(',').forEach((p) => { const q = p.trim().replace(/^'|'$/g, '').replace(/''/g, "'"); if (q) mkdirSync(q, { recursive: true }) })
|
|
75
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
76
|
+
}
|
|
77
|
+
if (/mkdir -p /.test(script)) {
|
|
78
|
+
const m = /mkdir -p (.+)$/.exec(script)
|
|
79
|
+
if (m) m[1].split(/\s+/).forEach((p) => { const q = p.trim().replace(/^'|'$/g, ''); if (q) mkdirSync(q, { recursive: true }) })
|
|
80
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
81
|
+
}
|
|
82
|
+
// v2 ALSO deletes files through this same service (`powershell … Remove-Item -LiteralPath 'x'`,
|
|
83
|
+
// POSIX `rm -f 'x'`). Without honouring it, the `defect` assertion "the archived proof is
|
|
84
|
+
// gone" would pass vacuously (nothing was ever deleted) instead of testing the real code path.
|
|
85
|
+
if (/Remove-Item/.test(script)) {
|
|
86
|
+
const m = /-LiteralPath\s+'((?:[^']|'')*)'/.exec(script)
|
|
87
|
+
if (m) { try { rmSync(m[1].replace(/''/g, "'"), { force: true }) } catch (e) { /* best effort */ } }
|
|
88
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
89
|
+
}
|
|
90
|
+
if (/^\s*rm -f /.test(script)) {
|
|
91
|
+
const re = /'((?:[^']|'\\'')*)'/g
|
|
92
|
+
let m
|
|
93
|
+
while ((m = re.exec(script)) !== null) { try { rmSync(m[1].replace(/'\\''/g, "'"), { force: true }) } catch (e) { /* best effort */ } }
|
|
94
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
95
|
+
}
|
|
96
|
+
const text = existsSync(last) ? readFileSync(last, 'utf8') : ''
|
|
97
|
+
const bad = /sorry|-- FAIL/.test(text)
|
|
98
|
+
leanRuns.push({ argv: spec.argv.slice(0, -1), file: last, cwd: spec.cwd, graceMs: spec.graceMs, stdio: spec.stdio })
|
|
99
|
+
const stdout = bad ? '' : 'ok\n'
|
|
100
|
+
const stderr = bad ? 'error: declaration uses sorry\n' : ''
|
|
101
|
+
return {
|
|
102
|
+
done: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null }),
|
|
103
|
+
collected: {
|
|
104
|
+
stdout: { readFrom: () => ({ text: stdout, nextOffset: stdout.length, lossy: false }) },
|
|
105
|
+
stderr: { readFrom: () => ({ text: stderr, nextOffset: stderr.length, lossy: false }) },
|
|
106
|
+
},
|
|
107
|
+
terminate() {},
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------
|
|
113
|
+
// mock host (v2 standing-mount shape: one plugin instance, many sessions)
|
|
114
|
+
// ---------------------------------------------------------------
|
|
115
|
+
function makeHost(WS, opts = {}) {
|
|
116
|
+
const listeners = {}
|
|
117
|
+
const toolRegs = []
|
|
118
|
+
const cmdRegs = []
|
|
119
|
+
const spawns = []
|
|
120
|
+
const followups = []
|
|
121
|
+
const interrupts = []
|
|
122
|
+
const ctx = {
|
|
123
|
+
get(name) {
|
|
124
|
+
if (name === 'subprocess') return opts.noSubprocess ? undefined : subprocess
|
|
125
|
+
if (name === 'sandboxPolicy') return { workspaceRoot: WS, resolve: () => ({ workspaceRoot: WS }) }
|
|
126
|
+
return undefined
|
|
127
|
+
},
|
|
128
|
+
on(event, fn) { (listeners[event] = listeners[event] || []).push(fn) },
|
|
129
|
+
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
130
|
+
logger: { info() {}, warn() {}, error() {} },
|
|
131
|
+
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
132
|
+
commands: { register(spec) { cmdRegs.push(spec); return () => {} } },
|
|
133
|
+
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
134
|
+
subagents: {
|
|
135
|
+
list() { return ['spawn'] },
|
|
136
|
+
async startContinuable({ label, request }) {
|
|
137
|
+
const childId = 'child-' + (spawns.length + 1) + '-' + Math.random().toString(36).slice(2, 6)
|
|
138
|
+
spawns.push({ label, childId, rootId: (request && request.parent && request.parent.id) || 'root', prompt: request && request.prompt && request.prompt[0] && request.prompt[0].text })
|
|
139
|
+
return { childId }
|
|
140
|
+
},
|
|
141
|
+
async followup(parent, childId, blocks) { followups.push({ childId, prompt: (blocks && blocks[0] && blocks[0].text) || '' }) },
|
|
142
|
+
async sendMessage(parent, childId, blocks) { followups.push({ childId, prompt: (blocks && blocks[0] && blocks[0].text) || '' }) },
|
|
143
|
+
interrupt(childId) { interrupts.push(childId) },
|
|
144
|
+
},
|
|
145
|
+
agents: { roots() { return [] }, get() { return undefined } },
|
|
146
|
+
fs: {
|
|
147
|
+
async resolve(rel, o) {
|
|
148
|
+
const raw = String(rel)
|
|
149
|
+
const p = isAbsolute(raw) ? raw : join((o && o.cwd) || WS, ...raw.split('/'))
|
|
150
|
+
return p.replace(/\\/g, '/')
|
|
151
|
+
},
|
|
152
|
+
async stat(t) { if (!existsSync(t)) return undefined; return { type: statSync(t).isDirectory() ? 'directory' : 'file' } },
|
|
153
|
+
async readText(t) { return readFileSync(t, 'utf8') },
|
|
154
|
+
async writeText(t, c) { mkdirSync(dirname(t), { recursive: true }); writeFileSync(t, c, 'utf8') },
|
|
155
|
+
async listDir(t) { if (!existsSync(t)) return []; return readdirSync(t, { withFileTypes: true }).map((e) => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
const fireEnd = (info) => { for (const h of (listeners['subagent/end'] || [])) h(info) }
|
|
159
|
+
return { ctx, toolRegs, cmdRegs, spawns, followups, interrupts, fireEnd, WS }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function makeRoot(id, WS) {
|
|
163
|
+
return { id, options: { provider: 'mock', model: 'mock' }, session: { id, header: { cwd: WS, parentSession: undefined } }, followup() {} }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const hosts = []
|
|
167
|
+
/**
|
|
168
|
+
* Build an INDEPENDENT plugin instance + workspace for one test case.
|
|
169
|
+
* AUDIT-CHECKLIST §2.3: cases must not share a session root/workspace, or a leftover
|
|
170
|
+
* heartbeat/verification from one case pollutes the next.
|
|
171
|
+
*/
|
|
172
|
+
async function makeCase(label, opts = {}) {
|
|
173
|
+
const WS = mkdtempSync(join(tmpdir(), 'vibe-v2-lean-' + label + '-'))
|
|
174
|
+
const h = makeHost(WS, opts)
|
|
175
|
+
const mod = await import(PLUGIN.href + '?t=' + Date.now() + '-' + Math.random().toString(36).slice(2, 8))
|
|
176
|
+
;(mod.default || mod).apply(h.ctx)
|
|
177
|
+
const root = makeRoot('sess-' + label, WS)
|
|
178
|
+
const call = async (name, args, agent) => {
|
|
179
|
+
const spec = h.toolRegs.find((s) => s.name === name)
|
|
180
|
+
if (!spec) throw new Error('no tool ' + name)
|
|
181
|
+
return JSON.parse(await spec.execute(args || {}, { agent: agent || root }))
|
|
182
|
+
}
|
|
183
|
+
await call('vibe_math_new_project', { name: 'proj' })
|
|
184
|
+
// 200ms is the sanitizer floor; verifierCount=2 (the sanitizer floor too) keeps each
|
|
185
|
+
// verification exactly two children, so a round settles predictably.
|
|
186
|
+
await call('vibe_math_set_params', { tickIntervalMs: 200, verifierCount: 2 })
|
|
187
|
+
const c = Object.assign(h, { WS, root, call, label })
|
|
188
|
+
hosts.push(c)
|
|
189
|
+
return c
|
|
190
|
+
}
|
|
191
|
+
// The scheduler only picks objects up while it is RUNNING (scheduleTick early-returns).
|
|
192
|
+
async function startScheduler(h) { await h.call('vibe_math_start', {}) }
|
|
193
|
+
|
|
194
|
+
// ── test speed: fast-forward the SCHEDULER POLL (test-only, no production impact) ─────────
|
|
195
|
+
// The plugin registers its scheduler poll as `setInterval(..., 1000)` at apply() time, so every
|
|
196
|
+
// suite-side `tick()` had to wait a full wall-clock second. With ~9 verification rounds × ~17
|
|
197
|
+
// ticks that alone accounted for ~99% of this suite's 186 s (its v3/v4/v5 siblings take seconds).
|
|
198
|
+
// Patching ONLY setInterval (the suite's own `sleep` uses setTimeout) makes a poll cost ~25 ms,
|
|
199
|
+
// while the plugin's due-ness logic still uses the real 200 ms `tickIntervalMs` floor — no
|
|
200
|
+
// scheduler behaviour depends on the poll period, and `tickTheScheduler` below stays above it.
|
|
201
|
+
const REAL_SET_INTERVAL = globalThis.setInterval
|
|
202
|
+
globalThis.setInterval = function (fn, ms, ...rest) {
|
|
203
|
+
return REAL_SET_INTERVAL(fn, Math.min(Number(ms) || 0, 25), ...rest)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const projRoot = (h) => join(h.WS, 'VibeMath', 'Projects', 'proj')
|
|
207
|
+
const vibeRoot = (h) => join(h.WS, 'VibeMath')
|
|
208
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
209
|
+
const formalStateOf = (h) => JSON.parse(readIf(join(projRoot(h), 'VibeMath_State', 'formal.json')) || '{}')
|
|
210
|
+
// One scheduler pass. With the poll fast-forwarded above, the poll fires every ~25 ms and a tick
|
|
211
|
+
// runs whenever the plugin's own 200 ms `tickIntervalMs` floor has elapsed, so 260 ms is one pass.
|
|
212
|
+
const tick = (ms = 260) => sleep(ms)
|
|
213
|
+
async function waitFor(pred, tries = 80, ms = 60) {
|
|
214
|
+
for (let i = 0; i < tries; i++) { const v = pred(); if (v) return v; await sleep(ms) }
|
|
215
|
+
return undefined
|
|
216
|
+
}
|
|
217
|
+
const verifiersOf = (h, rKind, exclude) => h.spawns.filter((s) => s.label.startsWith('verifier:' + rKind) && !(exclude || []).some((o) => o.childId === s.childId))
|
|
218
|
+
/** A reply exactly as an agent would emit it (a fenced JSON block) — the framework's real input. */
|
|
219
|
+
const fence = (obj) => '```json\n' + JSON.stringify(obj) + '\n```'
|
|
220
|
+
/** Feed ONE agent reply (a fresh turn's end) to the framework through the real dispatch path. */
|
|
221
|
+
const replyFrom = (h, childId, obj) => h.fireEnd({
|
|
222
|
+
id: childId, runId: 'r-' + childId, provider: 'spawn', local: true, stopReason: 'completed',
|
|
223
|
+
lastAssistantMessage: [{ type: 'text', text: fence(obj) }],
|
|
224
|
+
})
|
|
225
|
+
const fireVerdicts = (h, kids, vale) => {
|
|
226
|
+
for (let i = 0; i < kids.length; i++) {
|
|
227
|
+
h.fireEnd({ id: kids[i].childId, runId: 'v' + i, provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: '```json\n' + JSON.stringify({ Result: vale, Reason: 'review ' + i }) + '\n```' }] })
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Verify one object and DRIVE IT TO A SETTLEMENT.
|
|
232
|
+
*
|
|
233
|
+
* Important: v2's `processVerify` starts at most ONE verification and skips every candidate
|
|
234
|
+
* while a task is live, so leaving an object mid-debate would silently block the NEXT case's
|
|
235
|
+
* verification. The first `firstRounds` rounds are answered with `vale` (a non-unanimous value
|
|
236
|
+
* forces a real debate round, which is exactly what the debate-prompt assertions need), and any
|
|
237
|
+
* later round is answered with `settle` so the object actually closes.
|
|
238
|
+
*/
|
|
239
|
+
async function verifyWithDebate(h, rKind, vale, firstRounds = 1, settle = 1) {
|
|
240
|
+
// v2 re-wakes the SAME verifier children for every debate round (it does not spawn new
|
|
241
|
+
// ones), so a new round is detected by counting the followups written to those children.
|
|
242
|
+
const kids = new Set()
|
|
243
|
+
// Early exit: a settled object stops producing followups. Without this the loop always spent
|
|
244
|
+
// its FULL 16 passes (~21 s at the old 1.3 s/pass), which is what made this suite take 3
|
|
245
|
+
// minutes: the debate is over, but the helper kept ticking at nothing. Three consecutive
|
|
246
|
+
// passes with no new followup = the framework has nothing left to ask.
|
|
247
|
+
let lastN = -1
|
|
248
|
+
let quiet = 0
|
|
249
|
+
for (let i = 0; i < 16; i++) {
|
|
250
|
+
// The scheduler may need a whole pass before it notices the object and another before it
|
|
251
|
+
// fills the verifier quota, so each wait must span several timer ticks.
|
|
252
|
+
const cand = await waitFor(() => {
|
|
253
|
+
const x = verifiersOf(h, rKind)
|
|
254
|
+
return x.length >= 2 ? x : undefined
|
|
255
|
+
}, 40, 250)
|
|
256
|
+
if (!cand) break
|
|
257
|
+
cand.forEach((s) => kids.add(s.childId))
|
|
258
|
+
const rounds = 1 + h.followups.filter((f) => kids.has(f.childId)).length
|
|
259
|
+
// `fireEnd` is a NO-OP for a child with no in-flight turn, so answering the whole current
|
|
260
|
+
// verifier set each pass is safe; the extra answers land before the next round's wakes.
|
|
261
|
+
fireVerdicts(h, cand, rounds <= firstRounds ? vale : settle)
|
|
262
|
+
await sleep(200)
|
|
263
|
+
await tick()
|
|
264
|
+
if (h.followups.length === lastN) { if (++quiet >= 3) break } else { quiet = 0; lastN = h.followups.length }
|
|
265
|
+
}
|
|
266
|
+
const rounds = 1 + h.followups.filter((f) => kids.has(f.childId)).length
|
|
267
|
+
return { first: h.spawns.filter((s) => s.label.startsWith('verifier:' + rKind)), rounds: kids.size ? rounds : 0 }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ===============================================================
|
|
271
|
+
console.log('-- V2 Lean formal verification --')
|
|
272
|
+
|
|
273
|
+
// ---------- 1. 'off' is a true no-op ----------
|
|
274
|
+
section("1 'off' (default) is a true no-op")
|
|
275
|
+
{
|
|
276
|
+
const h = await makeCase('off')
|
|
277
|
+
const st = await h.call('vibe_math_status', {})
|
|
278
|
+
assert(st.params.formalVerify === 'off', "the default mode is 'off' (got " + st.params.formalVerify + ')')
|
|
279
|
+
assert(st.formal.mode === 'off' && st.formal.required === false, 'status exposes the formal mode and gate flag')
|
|
280
|
+
assert(st.params.leanCommand === 'lean' && Array.isArray(st.params.leanArgs), 'the Lean knobs are readable in status.params')
|
|
281
|
+
assert(st.params.leanTimeoutMs === 120000, 'leanTimeoutMs defaults to 120000 (got ' + st.params.leanTimeoutMs + ')')
|
|
282
|
+
assert(!!h.toolRegs.find((t) => t.name === 'vibe_math_lean_run') && !!h.toolRegs.find((t) => t.name === 'vibe_math_lean_archive') && !!h.toolRegs.find((t) => t.name === 'vibe_math_lean_lib'),
|
|
283
|
+
'the three Lean tools are registered in every mode (registration is static)')
|
|
284
|
+
assert(existsSync(join(vibeRoot(h), 'Formal', 'Lib')) && existsSync(join(vibeRoot(h), 'Formal', 'Proved')), 'the GLOBAL Formal/Lib + Formal/Proved dirs are created outside the project')
|
|
285
|
+
assert(existsSync(join(projRoot(h), 'Formal')) && existsSync(join(projRoot(h), 'Verified', 'Lean')), 'the project Formal/ and Verified/Lean/ dirs are created')
|
|
286
|
+
await h.call('vibe_math_add_problem', { id: 'q1', description: 'off 模式无操作测试' })
|
|
287
|
+
await startScheduler(h)
|
|
288
|
+
await tick(2000)
|
|
289
|
+
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:q1')), 60, 200)
|
|
290
|
+
assert(!!ex, 'the explorer was spawned (the scheduler really is running)')
|
|
291
|
+
assert(!!ex && !/Lean|形式化/.test(ex.prompt || ''), 'the explorer prompt contains no Lean/形式化 text in off mode')
|
|
292
|
+
if (ex) h.fireEnd({ id: ex.childId, runId: 'r1', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: '```json\n{"directions":[{"id":"d1","title":"D","method":"m","core_assumption":"c","feasibility":0.6}]}\n```' }] })
|
|
293
|
+
await sleep(300)
|
|
294
|
+
const so = await waitFor(() => h.spawns.find((s) => s.label.startsWith('solver:q1')), 40, 200)
|
|
295
|
+
assert(!!so, 'the solver was spawned after the explorer returned directions')
|
|
296
|
+
assert(!!so && !/Lean|形式化/.test(so.prompt || ''), 'the solver prompt contains no Lean/形式化 text in off mode')
|
|
297
|
+
if (so) h.fireEnd({ id: so.childId, runId: 'r2', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: '```json\n{"status":"success","solution":"complete solution","solution_probability":0.8,"lemmas":[],"routes":[],"lessons":[],"survival_probability":0.9,"dead_end_reason":null,"sub_questions":[]}\n```' }] })
|
|
298
|
+
const vs = await verifyWithDebate(h, 'r-q1-s0', 1)
|
|
299
|
+
assert(!!vs.first && vs.first.length >= 2, 'verifiers were spawned for the solution (' + (vs.first ? vs.first.length : 0) + ')')
|
|
300
|
+
const vp = vs.first ? (h.spawns.find((s) => s.label === 'verifier:r-q1-s0:0').prompt || '') : ''
|
|
301
|
+
assert(!/Lean|形式化/.test(vp), 'the review prompt contains no Lean/形式化 text in off mode')
|
|
302
|
+
const debateText = h.followups.map((f) => f.prompt || '').join('\n')
|
|
303
|
+
assert(!/Lean|形式化/.test(debateText), 'no debate/followup prompt mentions Lean in off mode')
|
|
304
|
+
const qs = JSON.parse(readIf(join(projRoot(h), 'qs', 'qs.json')) || '[]')
|
|
305
|
+
const q = qs.find((x) => x.id === 'q1')
|
|
306
|
+
assert(!!q && q.已解决 === true, "'off' still finalizes: the problem is marked solved with NO Lean artifact")
|
|
307
|
+
assert(!!q && q.解法列表 && q.解法列表[0].正确概率 === 1, 'the solution probability is 1 in off mode')
|
|
308
|
+
assert(existsSync(join(projRoot(h), 'Verified', '问题_Verified.json')), 'the Verified card was written in off mode')
|
|
309
|
+
assert(!/形式化/.test(readIf(join(projRoot(h), 'Verified', '问题_Verified.json'))), 'the card carries no formal line in off mode')
|
|
310
|
+
assert(!existsSync(join(projRoot(h), 'Formal', 'TODO.md')), 'no Formal/TODO.md is produced in off mode')
|
|
311
|
+
const fs0 = formalStateOf(h)
|
|
312
|
+
assert(!fs0.records || Object.keys(fs0.records).length === 0, 'off mode records no formal object state')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------- 2. parameter validation + runtime switching ----------
|
|
316
|
+
section('2 parameter validation and runtime switching')
|
|
317
|
+
{
|
|
318
|
+
const h = await makeCase('params')
|
|
319
|
+
const bad = await h.call('vibe_math_set_params', { formalVerify: 'banana' })
|
|
320
|
+
assert(bad.params.formalVerify === 'off', "an unknown mode degrades to 'off', never to a stronger mode (got " + bad.params.formalVerify + ')')
|
|
321
|
+
const enc = await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
322
|
+
assert(enc.params.formalVerify === 'encourage', "'encourage' is accepted")
|
|
323
|
+
const req = await h.call('vibe_math_set_params', { formalVerify: 'require', leanTimeoutMs: -5, leanCommand: ' ' })
|
|
324
|
+
assert(req.params.formalVerify === 'require', "'require' is accepted")
|
|
325
|
+
assert(req.params.leanTimeoutMs === 120000, 'a non-positive leanTimeoutMs falls back to the default (' + req.params.leanTimeoutMs + ')')
|
|
326
|
+
assert(req.params.leanCommand === 'lean', 'a blank leanCommand falls back to "lean"')
|
|
327
|
+
const lake = await h.call('vibe_math_set_params', { leanCommand: 'lake', leanArgs: ['env', 'lean'] })
|
|
328
|
+
assert(lake.params.leanCommand === 'lake' && lake.params.leanArgs.join(' ') === 'env lean', 'leanCommand/leanArgs are settable (lake env lean)')
|
|
329
|
+
const st = await h.call('vibe_math_status', {})
|
|
330
|
+
assert(st.formal.leanCommand === 'lake' && st.formal.leanArgs.join(' ') === 'env lean', 'the Lean knobs round-trip through status')
|
|
331
|
+
const setup = await h.call('vibe_math_setup', {})
|
|
332
|
+
const names = setup.parameters.map((p) => p.name)
|
|
333
|
+
assert(['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs'].every((n) => names.indexOf(n) !== -1), 'vibe_math_setup schema lists all four Lean knobs')
|
|
334
|
+
const fv = setup.parameters.find((p) => p.name === 'formalVerify')
|
|
335
|
+
assert(!!fv && fv.current === 'require' && fv.default === 'off', 'the schema reports the current and default formalVerify')
|
|
336
|
+
const tpl = await h.call('vibe_math_template', { where: 'global' })
|
|
337
|
+
const tplText = readIf(tpl.path)
|
|
338
|
+
assert(/formalVerify/.test(tplText) && /leanCommand/.test(tplText) && /leanTimeoutMs/.test(tplText), 'the settings template documents the Lean knobs')
|
|
339
|
+
assert(/leanArgs/.test(tplText), 'the settings template also documents leanArgs')
|
|
340
|
+
assert(!/:\s*undefined/.test(tplText), 'the template has no bare undefined token (settings round-trip safety)')
|
|
341
|
+
const saved = await h.call('vibe_math_save_settings', {})
|
|
342
|
+
assert(saved.ok === true, 'save_settings still round-trips with the new knobs')
|
|
343
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
344
|
+
await h.call('vibe_math_add_problem', { id: 'q2', description: '模式切换测试' })
|
|
345
|
+
await startScheduler(h)
|
|
346
|
+
await tick(2000)
|
|
347
|
+
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:q2')), 60, 200)
|
|
348
|
+
assert(!!ex && /【顺手形式化(鼓励)】/.test(ex.prompt || ''), '★ switching to encourage changes the NEXT prompt immediately')
|
|
349
|
+
if (ex) h.fireEnd({ id: ex.childId, runId: 'r', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: '```json\n{"directions":[{"id":"d1","title":"D","method":"m","core_assumption":"c","feasibility":0.5}]}\n```' }] })
|
|
350
|
+
await h.call('vibe_math_set_params', { formalVerify: 'off' })
|
|
351
|
+
await sleep(400)
|
|
352
|
+
const so = await waitFor(() => h.spawns.find((s) => s.label.startsWith('solver:q2')), 40, 200)
|
|
353
|
+
assert(!!so, 'the solver was spawned after the mode was switched back to off')
|
|
354
|
+
assert(!!so && !/形式化|Lean/.test(so.prompt || ''), '★ switching back to off removes the Lean text from the next prompt')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------- 3. 'encourage' injection ----------
|
|
358
|
+
section("3 'encourage' injects the Lean section into review AND debate prompts")
|
|
359
|
+
{
|
|
360
|
+
const h = await makeCase('enc')
|
|
361
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage', maxParallelThreshold: 8 })
|
|
362
|
+
// Keep the scheduler alive: v2 STOPS itself when there is no unsolved problem, no agent and
|
|
363
|
+
// no task, and a stopped scheduler never starts a verification (nor spawns an explorer).
|
|
364
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
365
|
+
await startScheduler(h)
|
|
366
|
+
await h.call('vibe_math_add_proposition', { id: 'pEnc', 概述: '鼓励模式下的忠实性审查', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
367
|
+
const vs = await verifyWithDebate(h, 'r-pEnc', 0.5, 1) // 0.5 -> no consensus -> a REAL debate round
|
|
368
|
+
assert(!!vs.first, 'verifiers spawned for the bare proposition')
|
|
369
|
+
assert(vs.rounds >= 2, 'the object really went through a debate round (' + vs.rounds + ' rounds)')
|
|
370
|
+
const review = h.spawns.find((s) => s.label === 'verifier:r-pEnc:0')
|
|
371
|
+
assert(!!review, 'the first review prompt was captured')
|
|
372
|
+
const reviewText = review ? review.prompt : ''
|
|
373
|
+
assert(/【Lean 形式化验证(鼓励模式)】/.test(reviewText), '★ the REVIEW prompt carries the Lean section')
|
|
374
|
+
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(reviewText), 'the review prompt states that a passing Lean run shrinks the question to fidelity')
|
|
375
|
+
assert(/实现难度/.test(reviewText), 'the review prompt asks for the implementation-difficulty judgement')
|
|
376
|
+
// NOTE: this assertion is only the WORDING half. Which is exactly the trap the first version
|
|
377
|
+
// of this feature fell into (a green suite guarding a dead channel). The behaviour — the reply
|
|
378
|
+
// really landing in the durable record — is asserted in section 11.
|
|
379
|
+
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(reviewText), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
380
|
+
assert(/"decision":"used\|blocked\|defect"/.test(reviewText), '★ the review contract lists the real decision enum (incl. defect)')
|
|
381
|
+
assert(/vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库)/.test(reviewText), 'the review prompt names the three v2 tools')
|
|
382
|
+
assert(/归档可复用定义\/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run)/.test(reviewText), '★ the review prompt says a reusable artifact must run green BEFORE it is archived')
|
|
383
|
+
assert(/LEAN_NOT_FOUND/.test(reviewText) && /宿主无 Lean 工具链/.test(reviewText), '★ the review prompt writes out the missing-toolchain escape hatch (a host without Lean must not deadlock the agent)')
|
|
384
|
+
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
385
|
+
assert(/DEBATE/.test(debate), 'the debate round actually happened (a followup with the debate prompt was issued)')
|
|
386
|
+
assert(/【Lean 形式化验证(鼓励模式)】/.test(debate), '★ the DEBATE prompt carries the Lean section too')
|
|
387
|
+
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(debate), 'the debate prompt states the fidelity shift')
|
|
388
|
+
await h.call('vibe_math_add_problem', { id: 'qW', description: '顺手形式化测试' })
|
|
389
|
+
await tick(2000)
|
|
390
|
+
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:qW')), 60, 200)
|
|
391
|
+
assert(!!ex && /【顺手形式化(鼓励)】/.test(ex.prompt || ''), '★ the explorer work prompt carries the 顺手形式化 line')
|
|
392
|
+
assert(!!ex && /vibe_math_lean_archive kind='def'/.test(ex.prompt || ''), 'the work line points at the archive tool for reusable definitions')
|
|
393
|
+
assert(!!ex && /vibe_math_lean_lib 查重/.test(ex.prompt || ''), 'the work line tells members to check the reuse library first')
|
|
394
|
+
assert(!!ex && /归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。/.test(ex.prompt || ''), '★ the work line forbids archiving a definition that has not run green')
|
|
395
|
+
assert(!!ex && /"formal":\{"target":"<对象id>","decision":"used\|blocked\|defect"/.test(ex.prompt || ''), '★ the WORK-round contract advertises the formal reply field too (otherwise a working agent has nowhere to write its judgement)')
|
|
396
|
+
// Every agent-facing string must name the REGISTERED tools: an abbreviated `lean_archive`
|
|
397
|
+
// is not a tool that exists, and agents copy these literals verbatim (contract §6 hard req. 1).
|
|
398
|
+
const workText = (ex.prompt || '') + '\n' + reviewText + '\n' + debate
|
|
399
|
+
assert(!/(^|[^a-z_])lean_(run|archive|lib)/.test(workText), '★ no injected prompt names an abbreviated tool (every occurrence is prefixed)')
|
|
400
|
+
const offHost = await makeCase('enc-off')
|
|
401
|
+
await offHost.call('vibe_math_add_problem', { id: 'qN', description: 'x' })
|
|
402
|
+
await startScheduler(offHost)
|
|
403
|
+
await tick(2000)
|
|
404
|
+
const exN = await waitFor(() => offHost.spawns.find((s) => s.label.startsWith('explorer:qN')), 60, 200)
|
|
405
|
+
assert(!!exN && !/形式化|Lean/.test(exN.prompt || ''), 'the same prompt in off mode is free of Lean text (injection is mode-computed, not frozen)')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------- 4. the run tool ----------
|
|
409
|
+
section('4 lean_run executes through the subprocess service and reports honestly')
|
|
410
|
+
{
|
|
411
|
+
const h = await makeCase('run')
|
|
412
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
413
|
+
const proj = projRoot(h)
|
|
414
|
+
mkdirSync(join(proj, 'Formal'), { recursive: true })
|
|
415
|
+
writeFileSync(join(proj, 'Formal', 'good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
416
|
+
writeFileSync(join(proj, 'Formal', 'bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
|
|
417
|
+
const before = leanRuns.length
|
|
418
|
+
const runGood = await h.call('vibe_math_lean_run', { file: 'Formal/good.lean', target: 'pRun' })
|
|
419
|
+
assert(runGood.ok === true && runGood.exitCode === 0, 'a file with no sorry runs green (' + JSON.stringify({ ok: runGood.ok, exitCode: runGood.exitCode }) + ')')
|
|
420
|
+
assert(leanRuns.length > before, 'the run really went through the mocked subprocess service')
|
|
421
|
+
const spec = leanRuns[leanRuns.length - 1]
|
|
422
|
+
assert(spec.cwd.replace(/\\/g, '/') === proj.replace(/\\/g, '/'), 'the toolchain runs with the PROJECT root as cwd (got ' + spec.cwd + ')')
|
|
423
|
+
assert(!!spec.stdio && spec.stdio.stdin === 'ignore' && spec.stdio.stdout.maxBytes === 64 * 1024 && spec.stdio.stderr.maxBytes === 64 * 1024, 'stdio is stdin=ignore with a 64KB output cap')
|
|
424
|
+
assert(typeof spec.graceMs === 'number' && spec.graceMs >= 1000, 'the configured timeout is passed as graceMs (' + spec.graceMs + ')')
|
|
425
|
+
assert(typeof runGood.command === 'string' && runGood.command.indexOf('good.lean') !== -1, 'the result reports the exact command line')
|
|
426
|
+
const runBad = await h.call('vibe_math_lean_run', { file: 'Formal/bad.lean' })
|
|
427
|
+
assert(runBad.ok === false && runBad.exitCode === 1, 'a file that still uses sorry reports a red run')
|
|
428
|
+
assert(/sorry/.test(runBad.stderr), 'the compiler output is returned verbatim (' + JSON.stringify(runBad.stderr).slice(0, 60) + ')')
|
|
429
|
+
assert(/修复后重跑/.test(runBad.hint || ''), 'a red run tells the member to fix and rerun')
|
|
430
|
+
const runMissing = await h.call('vibe_math_lean_run', { file: 'Formal/nope.lean' })
|
|
431
|
+
assert(runMissing.ok === false && runMissing.code === 'V2_NOT_FOUND', 'a missing file is refused with a typed code (' + runMissing.code + ')')
|
|
432
|
+
const runNoFile = await h.call('vibe_math_lean_run', {})
|
|
433
|
+
assert(runNoFile.ok === false && runNoFile.code === 'V2_INVALID_ARGUMENT', 'a run without `file` is refused')
|
|
434
|
+
// The guard's boundary is the VibeMath ROOT, not the project: the global reuse library
|
|
435
|
+
// deliberately lives at <VibeMath>/Formal/{Lib,Proved}, so climbing out of the project but
|
|
436
|
+
// staying inside VibeMath is legal (it just fails as a missing file).
|
|
437
|
+
const runOutOfProject = await h.call('vibe_math_lean_run', { file: '../../Formal/Lib/x.lean' })
|
|
438
|
+
assert(runOutOfProject.ok === false && runOutOfProject.code === 'V2_NOT_FOUND',
|
|
439
|
+
'climbing out of the project but staying inside the VibeMath root is ALLOWED (got ' + runOutOfProject.code + ')')
|
|
440
|
+
const runEscape = await h.call('vibe_math_lean_run', { file: '../../../../etc/evil.lean' })
|
|
441
|
+
assert(runEscape.ok === false && runEscape.code === 'V2_INVALID_ARGUMENT', '★ a traversal that climbs ABOVE the VibeMath root is refused')
|
|
442
|
+
const runEscape2 = await h.call('vibe_math_lean_run', { file: 'Formal/../../../../../evil.lean' })
|
|
443
|
+
assert(runEscape2.ok === false && runEscape2.code === 'V2_INVALID_ARGUMENT', 'a deeper traversal is refused too')
|
|
444
|
+
const runEscape3 = await h.call('vibe_math_lean_run', { file: '/etc/evil.lean' })
|
|
445
|
+
assert(runEscape3.ok === false && runEscape3.code === 'V2_INVALID_ARGUMENT', 'an unrelated absolute path is refused')
|
|
446
|
+
const runEscape4 = await h.call('vibe_math_lean_run', { file: 'C:/Windows/evil.lean' })
|
|
447
|
+
assert(runEscape4.ok === false && runEscape4.code === 'V2_INVALID_ARGUMENT', 'an unrelated Windows absolute path is refused')
|
|
448
|
+
const runNotLean = await h.call('vibe_math_lean_run', { file: 'Formal/good.txt' })
|
|
449
|
+
assert(runNotLean.ok === false && runNotLean.code === 'V2_INVALID_ARGUMENT', 'only .lean files can be executed')
|
|
450
|
+
assert(/VibeMath/.test(runEscape.message || ''), 'the refusal names the boundary it enforced')
|
|
451
|
+
mkdirSync(join(vibeRoot(h), 'Formal', 'Lib'), { recursive: true })
|
|
452
|
+
writeFileSync(join(vibeRoot(h), 'Formal', 'Lib', 'abs.lean'), 'def absTest := 1\n', 'utf8')
|
|
453
|
+
const runAbs = await h.call('vibe_math_lean_run', { file: join(vibeRoot(h), 'Formal', 'Lib', 'abs.lean').replace(/\\/g, '/') })
|
|
454
|
+
assert(runAbs.ok === true, 'an absolute path inside the VibeMath root is accepted')
|
|
455
|
+
const rec = formalStateOf(h)
|
|
456
|
+
assert(!!rec.records && !!rec.records.pRun && rec.records.pRun.status === 'attempted', 'a green lean_run records the object as attempted (stored, not promoted)')
|
|
457
|
+
assert(!!rec.records.pRun.run && rec.records.pRun.run.ok === true && rec.records.pRun.run.exitCode === 0, 'the run record keeps ok/exitCode')
|
|
458
|
+
assert(rec.records.pRun.run.stdoutTail !== undefined && rec.records.pRun.run.stderrTail !== undefined, 'the run record keeps truncated output tails')
|
|
459
|
+
toolchainAvailable = false
|
|
460
|
+
const runNoTc = await h.call('vibe_math_lean_run', { file: 'Formal/good.lean' })
|
|
461
|
+
assert(runNoTc.ok === false && runNoTc.code === 'LEAN_NOT_FOUND', 'a missing toolchain returns LEAN_NOT_FOUND instead of crashing')
|
|
462
|
+
assert(/仍可把形式化代码写下来归档/.test(runNoTc.message), 'the failure explains the graceful degradation')
|
|
463
|
+
toolchainAvailable = true
|
|
464
|
+
const noSub = await makeCase('nosub', { noSubprocess: true })
|
|
465
|
+
mkdirSync(join(projRoot(noSub), 'Formal'), { recursive: true })
|
|
466
|
+
writeFileSync(join(projRoot(noSub), 'Formal', 'x.lean'), 'theorem x : 1 = 1 := rfl\n', 'utf8')
|
|
467
|
+
const noSubRun = await noSub.call('vibe_math_lean_run', { file: 'Formal/x.lean' })
|
|
468
|
+
assert(noSubRun.ok === false && noSubRun.code === 'NO_SUBPROCESS', 'a host without the subprocess service returns NO_SUBPROCESS and does not crash (got ' + noSubRun.code + ')')
|
|
469
|
+
const noSubStatus = await noSub.call('vibe_math_status', {})
|
|
470
|
+
assert(noSubStatus.ok === true, 'the scheduler still answers status after that (nothing was thrown into the loop)')
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ---------- 5. archive: def / lemma / proof / blocked ----------
|
|
474
|
+
section('5 lean_archive writes the contract paths and indexes')
|
|
475
|
+
{
|
|
476
|
+
const h = await makeCase('arc')
|
|
477
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
478
|
+
const proj = projRoot(h)
|
|
479
|
+
const libPath = join(vibeRoot(h), 'Formal', 'Lib')
|
|
480
|
+
const provedPath = join(vibeRoot(h), 'Formal', 'Proved')
|
|
481
|
+
const defRes = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'ZMod5', content: 'def ZMod5 := Fin 5\n' })
|
|
482
|
+
assert(defRes.ok === true && defRes.file === 'Formal/Lib/ZMod5.lean', 'a reusable definition is archived to the global lib (' + defRes.file + ')')
|
|
483
|
+
assert(existsSync(join(libPath, 'ZMod5.lean')), '★ the definition exists under <VibeMath>/Formal/Lib/ (cross-project, NOT inside the project)')
|
|
484
|
+
assert(!existsSync(join(proj, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the project tree')
|
|
485
|
+
assert(!!defRes.run && defRes.run.ok === true, 'the archived definition was executed (kind=def runs by default)')
|
|
486
|
+
const lemmaRes = await h.call('vibe_math_lean_archive', { kind: 'lemma', name: 'sq_odd', content: 'theorem sq_odd (n : Nat) : Odd (n*n) → Odd n := by omega\n' })
|
|
487
|
+
assert(lemmaRes.ok === true && lemmaRes.file === 'Formal/Proved/sq_odd.lean', 'a lemma is archived to the global Proved/ (' + lemmaRes.file + ')')
|
|
488
|
+
assert(existsSync(join(provedPath, 'sq_odd.lean')), 'the lemma exists under <VibeMath>/Formal/Proved/')
|
|
489
|
+
const defNoRun = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'quiet', content: 'def quiet := 2\n', run: false })
|
|
490
|
+
assert(defNoRun.ok === true && defNoRun.run === undefined, 'run:false archives without executing (still indexed)')
|
|
491
|
+
assert(/ZMod5/.test(readIf(join(libPath, 'Index.md'))), 'Lib/Index.md lists the new definition')
|
|
492
|
+
assert(/quiet/.test(readIf(join(libPath, 'Index.md'))), 'Lib/Index.md lists the run:false definition too')
|
|
493
|
+
assert(/sq_odd/.test(readIf(join(provedPath, 'Index.md'))), 'Proved/Index.md lists the new lemma')
|
|
494
|
+
writeFileSync(join(proj, 'Formal', 'src.lean'), 'def copied := 3\n', 'utf8')
|
|
495
|
+
const defFrom = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'copied', from: 'Formal/src.lean' })
|
|
496
|
+
assert(defFrom.ok === true && existsSync(join(libPath, 'copied.lean')), 'kind=def can archive from an existing .lean file')
|
|
497
|
+
const fromOutside = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'escape', from: '../../../../etc/passwd' })
|
|
498
|
+
assert(fromOutside.ok === false && fromOutside.code === 'V2_INVALID_ARGUMENT', 'from=<path outside the VibeMath root> is refused')
|
|
499
|
+
const noName = await h.call('vibe_math_lean_archive', { kind: 'def', content: 'def x := 1\n' })
|
|
500
|
+
assert(noName.ok === false && noName.code === 'V2_INVALID_ARGUMENT', 'archiving a definition without a name is refused')
|
|
501
|
+
const noBody = await h.call('vibe_math_lean_archive', { kind: 'lemma', name: 'xb' })
|
|
502
|
+
assert(noBody.ok === false && noBody.code === 'V2_INVALID_ARGUMENT', 'archiving a lemma with neither content nor from is refused')
|
|
503
|
+
const badKind = await h.call('vibe_math_lean_archive', { kind: 'nonsense' })
|
|
504
|
+
assert(badKind.ok === false && badKind.code === 'V2_INVALID_ARGUMENT', 'an unknown archive kind is refused')
|
|
505
|
+
const noTarget = await h.call('vibe_math_lean_archive', { kind: 'proof', content: 'theorem x : 1 = 1 := rfl\n' })
|
|
506
|
+
assert(noTarget.ok === false && noTarget.code === 'V2_INVALID_ARGUMENT', 'kind=proof without a target is refused')
|
|
507
|
+
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pProof', content: 'theorem p_proof : 3 * 1 ^ 2 - 2 = (1:Nat) ^ 2 := by decide\n' })
|
|
508
|
+
assert(proof.ok === true && proof.passed === true, 'a passing proof is archived (' + JSON.stringify({ ok: proof.ok, passed: proof.passed }) + ')')
|
|
509
|
+
assert(proof.file === 'Formal/pProof.lean', 'the working file is Formal/<target>.lean')
|
|
510
|
+
assert(proof.proof === 'Verified/Lean/pProof.lean', 'the archived proof path is Verified/Lean/<target>.lean')
|
|
511
|
+
assert(existsSync(join(proj, 'Formal', 'pProof.lean')), 'the working file exists on disk')
|
|
512
|
+
assert(existsSync(join(proj, 'Verified', 'Lean', 'pProof.lean')), '★ the proof is archived under Verified/Lean/')
|
|
513
|
+
assert(proof.status === 'passed', 'the object status becomes passed')
|
|
514
|
+
const proofBad = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pRed', content: 'theorem p_red : 1 = 2 := by sorry\n' })
|
|
515
|
+
assert(proofBad.ok === true && proofBad.passed === false && proofBad.status === 'attempted', 'a red proof is recorded as attempted (not passed)')
|
|
516
|
+
assert(!existsSync(join(proj, 'Verified', 'Lean', 'pRed.lean')), 'a red proof is NOT archived under Verified/Lean/')
|
|
517
|
+
assert(/sorry/.test((proofBad.run && proofBad.run.stderr) || ''), 'the red run keeps the compiler output for the member to fix')
|
|
518
|
+
const st = await h.call('vibe_math_status', {})
|
|
519
|
+
assert(st.formal.objects.some((o) => o.target === 'pProof' && o.status === 'passed'), 'status lists the passed object')
|
|
520
|
+
const idx = readIf(join(proj, 'Formal', 'Index.md'))
|
|
521
|
+
assert(/pProof/.test(idx) && /passed/.test(idx) && /Verified\/Lean\/pProof\.lean/.test(idx), 'Formal/Index.md indexes the object, its status and its archived proof')
|
|
522
|
+
assert(/pRed/.test(idx) && /attempted/.test(idx), 'Formal/Index.md also records the failed attempt')
|
|
523
|
+
const blkNoNote = await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'pBlk' })
|
|
524
|
+
assert(blkNoNote.ok === false && blkNoNote.code === 'V2_INVALID_ARGUMENT', 'blocked without a note is refused')
|
|
525
|
+
const blk = await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'pBlk', note: '需要外层解析数论框架,本轮工作量不可接受' })
|
|
526
|
+
assert(blk.ok === true && blk.status === 'blocked', 'a reasoned blocker is recorded')
|
|
527
|
+
assert(/需要外层解析数论框架/.test(readIf(join(proj, 'Formal', 'Index.md'))), 'the blocker reason reaches the index')
|
|
528
|
+
const stBlk = await h.call('vibe_math_status', {})
|
|
529
|
+
assert(stBlk.formal.objects.some((o) => o.target === 'pBlk' && o.status === 'blocked'), 'status lists the blocked object')
|
|
530
|
+
const lib = await h.call('vibe_math_lean_lib', {})
|
|
531
|
+
assert(lib.ok === true && lib.counts.lib >= 3 && lib.counts.proved >= 1, 'lean_lib reports the reuse library sizes (' + JSON.stringify(lib.counts) + ')')
|
|
532
|
+
assert(lib.objects.some((o) => o.target === 'pProof' && o.status === 'passed'), 'lean_lib lists the per-object formal status')
|
|
533
|
+
assert(/复用优先/.test(lib.hint || ''), 'lean_lib tells agents to reuse before redefining')
|
|
534
|
+
assert(lib.mode === 'encourage', 'lean_lib reports the active mode')
|
|
535
|
+
const noRefresh = await h.call('vibe_math_lean_lib', { refresh: false })
|
|
536
|
+
assert(noRefresh.rebuilt === false && noRefresh.counts.lib === null, 'refresh:false lists without rebuilding')
|
|
537
|
+
assert(existsSync(join(proj, 'Formal', 'Index.md')), 'the project Formal/Index.md exists')
|
|
538
|
+
assert(existsSync(join(libPath, 'Index.md')) && existsSync(join(provedPath, 'Index.md')), 'the two GLOBAL indexes exist')
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ---------- 6. a passing proof flips the review to FIDELITY ----------
|
|
542
|
+
section('6 a passing proof flips the review subject to fidelity')
|
|
543
|
+
{
|
|
544
|
+
const h = await makeCase('fid')
|
|
545
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage', maxParallelThreshold: 8 })
|
|
546
|
+
// v2's scheduler STOPS itself when there is no unsolved problem, no agent and no task; a
|
|
547
|
+
// case that only wants to verify a proposition therefore needs one live problem to keep
|
|
548
|
+
// the tick loop alive (otherwise no verification would ever be started).
|
|
549
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
550
|
+
await startScheduler(h)
|
|
551
|
+
await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'r-pFid', content: 'theorem p_fid : 2 + 2 = 4 := by decide\n' })
|
|
552
|
+
await h.call('vibe_math_add_proposition', { id: 'pFid', 概述: '2+2=4(已有 Lean 证明)', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
553
|
+
// 0.5 in round 1 forces a debate round whose prompt must ALSO carry the fidelity wording
|
|
554
|
+
const vs = await verifyWithDebate(h, 'r-pFid', 0.5, 1)
|
|
555
|
+
assert(!!vs.first, 'a verifier was spawned for the Lean-passed object')
|
|
556
|
+
const vp = h.spawns.find((s) => s.label === 'verifier:r-pFid:0')
|
|
557
|
+
const vpText = vp ? vp.prompt : ''
|
|
558
|
+
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(vpText), 'the review prompt announces the passing proof')
|
|
559
|
+
assert(/你不需要重新检查推导/.test(vpText), '★ it tells reviewers NOT to re-derive')
|
|
560
|
+
assert(/忠实性审查/.test(vpText), '★ it tells reviewers the review subject is now fidelity')
|
|
561
|
+
assert(/定义 \/ 对象 \/ 条件 \/ 假设 \/ 结论是否与命题原文\*\*完全一致\*\*/.test(vpText), 'it enumerates exactly what fidelity means')
|
|
562
|
+
assert(/Verified\/Lean\/r-pFid\.lean/.test(vpText), 'it points at the archived proof')
|
|
563
|
+
assert(/一致 → Result = 1/.test(vpText), '★ the fidelity guidance names the REAL field (v2\'s contract field is Result)')
|
|
564
|
+
assert(!/verdict/.test(vpText), '★ the fidelity guidance never names a `verdict` field (that vote would be silently dropped)')
|
|
565
|
+
assert(/发现任何偏差,不要投 0/.test(vpText), '★ a fidelity defect is explicitly NOT to be voted as 0 (it is not a refutation)')
|
|
566
|
+
assert(/formal:\{decision:'defect'/.test(vpText), '★ the reviewers are given the defect reply channel that withdraws the proof')
|
|
567
|
+
assert(!/偏离 → 0/.test(vpText), '★ the "any deviation → 0" instruction is gone (it would fabricate a false conclusion)')
|
|
568
|
+
assert(!/请先判断该对象的\*\*实现难度\*\*/.test(vpText), 'the "judge the difficulty first" wording is gone when a proof already exists')
|
|
569
|
+
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
570
|
+
assert(/你不需要重新检查推导/.test(debate), '★ the debate prompt for a Lean-passed object also asks for fidelity, not re-derivation')
|
|
571
|
+
assert(/发现任何偏差,不要投 0/.test(debate), '★ and it carries the same no-zero rule in the debate round')
|
|
572
|
+
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'r-pBlk2', note: '涉及未形式化的分析学前置' })
|
|
573
|
+
await h.call('vibe_math_add_proposition', { id: 'pBlk2', 概述: '已记录阻塞的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
574
|
+
const vs2 = await verifyWithDebate(h, 'r-pBlk2', 0.5, 1)
|
|
575
|
+
assert(!!vs2.first, 'verifiers spawned for the blocked object')
|
|
576
|
+
const vp2 = h.spawns.find((s) => s.label === 'verifier:r-pBlk2:0')
|
|
577
|
+
const vp2Text = vp2 ? vp2.prompt : ''
|
|
578
|
+
assert(/该对象已被记录为\*\*形式化阻塞\*\*/.test(vp2Text), 'a blocked object is announced as such')
|
|
579
|
+
assert(/涉及未形式化的分析学前置/.test(vp2Text), 'the blocker reason is shown to the reviewers')
|
|
580
|
+
assert(/这个阻塞判断是否成立/.test(vp2Text), 'the verdict guidance asks whether the blocker is justified')
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ---------- 7. the 'require' gate (proposition) ----------
|
|
584
|
+
section("7 'require' withholds a verdict until the formal record exists")
|
|
585
|
+
{
|
|
586
|
+
const h = await makeCase('gate')
|
|
587
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
588
|
+
// a live problem keeps the scheduler from stopping itself (see the comment in case 6)
|
|
589
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
590
|
+
await startScheduler(h)
|
|
591
|
+
const proj = projRoot(h)
|
|
592
|
+
await h.call('vibe_math_add_proposition', { id: 'pGate', 概述: '必须形式化的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
593
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pGate'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
594
|
+
assert(!!vs, 'verifiers were spawned under require mode')
|
|
595
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pGate:0') || {}).prompt || ''
|
|
596
|
+
assert(/【Lean 形式化验证(强制模式)】/.test(vp), 'the review prompt says 强制模式')
|
|
597
|
+
assert(/必须产出 Lean 形式化/.test(vp), "'require' states the formalization is mandatory")
|
|
598
|
+
assert(/本次裁定不会生效/.test(vp), 'the prompt warns that the verdict will not take effect without it')
|
|
599
|
+
assert(/formal-required/.test(vp), 'the prompt names the machine-readable reason')
|
|
600
|
+
assert(/vibe_math_lean_archive kind='blocked' note=… 或回执 formal\.note/.test(vp), '★ the require wording names BOTH blocking routes with the FULL tool name')
|
|
601
|
+
assert(/"formal":\{"target":"r-pGate","decision":"used\|blocked\|defect"/.test(vp), '★ the require review contract carries the formal reply field keyed by the verification id')
|
|
602
|
+
assert(!/(^|[^a-z_])lean_(run|archive|lib)/.test(vp), '★ the require prompt contains no abbreviated tool name either')
|
|
603
|
+
fireVerdicts(h, vs, 1)
|
|
604
|
+
// The deferral writes Formal/TODO.md from INSIDE the settle path; waiting for that file is
|
|
605
|
+
// the observable proof that the round settled (the task is dropped right after).
|
|
606
|
+
const todoSettled = await waitFor(() => /pGate/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 40, 150)
|
|
607
|
+
assert(!!todoSettled, 'the deferred round settled (Formal/TODO.md recorded the object)')
|
|
608
|
+
await tick(600)
|
|
609
|
+
const propFile = join(proj, 'Propos', '数论_Propos.json')
|
|
610
|
+
const props = JSON.parse(readIf(propFile) || '[]')
|
|
611
|
+
const pGate = props.find((x) => x.id === 'pGate') || {}
|
|
612
|
+
assert(pGate.布尔估计 === 0.5, "★ the verdict did NOT take effect: the proposition's 布尔估计 is unchanged (got " + pGate.布尔估计 + ')')
|
|
613
|
+
assert(!(pGate.证明列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 proof entry was recorded')
|
|
614
|
+
assert(pGate.优先级 !== 'never', 'the priority was not pinned to never')
|
|
615
|
+
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), '★ no Verified card was written')
|
|
616
|
+
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
617
|
+
assert(existsSync(join(proj, 'Formal', 'TODO.md')), 'Formal/TODO.md was created')
|
|
618
|
+
assert(/pGate/.test(todo) && /formal-required/.test(todo), '★ the object is on the formalization TODO with the machine-readable reason')
|
|
619
|
+
const recAfterGate = formalStateOf(h)
|
|
620
|
+
// record key = the verification object id (rId), which is what lean_archive target= also uses
|
|
621
|
+
assert(!!recAfterGate.records && !!recAfterGate.records['r-pGate'] && !!recAfterGate.records['r-pGate'].deferredAt, 'the deferral is persisted in the durable formal state (survives resume)')
|
|
622
|
+
assert(!!recAfterGate.records['r-pGate'] && recAfterGate.records['r-pGate'].status === 'none', 'the object stays at status none (nothing was formalized)')
|
|
623
|
+
const vlogs = existsSync(join(proj, 'Verification_logs')) ? readdirSync(join(proj, 'Verification_logs')) : []
|
|
624
|
+
assert(vlogs.some((f) => f.startsWith('r-pGate')), '★ the verification round DID complete (its debate log exists) — the gate withheld the conclusion, it did not stall the round')
|
|
625
|
+
const st1 = await h.call('vibe_math_status', {})
|
|
626
|
+
assert(/require 模式搁置/.test(JSON.stringify(st1.recentActivity)), 'the withholding is announced on v2-readable channel (activity log; v2 has no Shared/Chat/)')
|
|
627
|
+
assert(st1.formal.todo.some((t) => t.id === 'r-pGate'), 'status exposes the formalization TODO')
|
|
628
|
+
const rep = await h.call('vibe_math_report', {})
|
|
629
|
+
assert(!!rep.formal && rep.formal.required === true && rep.formal.mode === 'require', 'the report exposes the formal mode and gate flag')
|
|
630
|
+
assert(rep.recentActivity.some((a) => /formal-gate/.test(a.event)), 'the deferral also lands in the report log')
|
|
631
|
+
// The deferral leaves this project with no agent, task or solved problem, and v2's strict
|
|
632
|
+
// termination stops the scheduler there — so a case that wants to RE-verify the object after
|
|
633
|
+
// formalizing it must (re)start the scheduler, exactly as a human would.
|
|
634
|
+
await startScheduler(h)
|
|
635
|
+
const proofNow = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pGate', content: 'theorem p_gate : 2 + 2 = 4 := by decide\n' })
|
|
636
|
+
assert(proofNow.ok === true && proofNow.passed === true, 'the object is now Lean-passed')
|
|
637
|
+
const spawned2 = await verifyWithDebate(h, 'r-pGate', 1, 0)
|
|
638
|
+
assert(!!spawned2.first, 'the object was re-verified after the proof was archived')
|
|
639
|
+
const props2 = JSON.parse(readIf(propFile) || '[]')
|
|
640
|
+
const pGateNow = props2.find((x) => x.id === 'pGate') || {}
|
|
641
|
+
assert(pGateNow.布尔估计 === 1, '★ with a passing Lean artifact the same unanimous verdict DOES take effect (布尔估计=1)')
|
|
642
|
+
const cardFile = join(proj, 'Verified', '数论_Verified.json')
|
|
643
|
+
const cards = JSON.parse(readIf(cardFile) || '[]')
|
|
644
|
+
const card = cards.find((c) => c.id === 'pGate')
|
|
645
|
+
assert(!!card, '★ the Verified card was written after the proof passed')
|
|
646
|
+
assert(!!card && /Lean 通过/.test(card['形式化'] || ''), '★ the card records how strong the result is (Lean 通过)')
|
|
647
|
+
assert(!!card && /Verified\/Lean\/pGate\.lean/.test(card['形式化'] || ''), 'the card points at the archived proof')
|
|
648
|
+
await h.call('vibe_math_add_proposition', { id: 'pBlkOk', 概述: '记录阻塞后可定论', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
649
|
+
const sp3 = await waitFor(() => { const x = verifiersOf(h, 'r-pBlkOk'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
650
|
+
assert(!!sp3, 'verifiers spawned for the blocker-escape case')
|
|
651
|
+
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'r-pBlkOk', note: '命题涉及未形式化的分析学,本轮不做' })
|
|
652
|
+
fireVerdicts(h, sp3 || [], 1)
|
|
653
|
+
await waitFor(() => { const c = JSON.parse(readIf(cardFile) || '[]').find((x) => x.id === 'pBlkOk'); return c ? c : undefined }, 40, 150)
|
|
654
|
+
await tick(400)
|
|
655
|
+
const cards3 = JSON.parse(readIf(cardFile) || '[]')
|
|
656
|
+
const card3 = cards3.find((c) => c.id === 'pBlkOk')
|
|
657
|
+
assert(!!card3, '★ an explicit reasoned blocker also lets the verdict through (decide by difficulty, but decide out loud)')
|
|
658
|
+
assert(!!card3 && /阻塞(/.test(card3['形式化'] || ''), 'the card records the blocker')
|
|
659
|
+
assert(!!card3 && /未形式化的分析学/.test(card3['形式化'] || ''), 'the card records the blocker REASON, not just the fact')
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ---------- 8. the gate covers the qs (problem) choke point ----------
|
|
663
|
+
section('8 the gate also covers a problem-solution verdict')
|
|
664
|
+
{
|
|
665
|
+
const h = await makeCase('gq')
|
|
666
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
667
|
+
// a live problem keeps the scheduler from stopping itself (see the comment in case 3)
|
|
668
|
+
await h.call('vibe_math_add_problem', { id: 'qStay', description: '保持调度器运行的占位问题', priority: 9 })
|
|
669
|
+
await startScheduler(h)
|
|
670
|
+
const proj = projRoot(h)
|
|
671
|
+
await h.call('vibe_math_add_problem', { id: 'qG', description: '带解法的 require 门禁问题', priority: 1 })
|
|
672
|
+
const qsFile = join(proj, 'qs', 'qs.json')
|
|
673
|
+
const qs0 = JSON.parse(readIf(qsFile) || '[]')
|
|
674
|
+
qs0.find((q) => q.id === 'qG').解法列表 = [{ 完整解法: 'closing argument', 正确概率: 0.8, 已验: false }]
|
|
675
|
+
writeFileSync(qsFile, JSON.stringify(qs0, null, 2), 'utf8')
|
|
676
|
+
await tick(2000)
|
|
677
|
+
const sp = await waitFor(() => { const x = verifiersOf(h, 'r-qG-s0'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
678
|
+
assert(!!sp, 'verifiers spawned for the problem solution')
|
|
679
|
+
fireVerdicts(h, sp || [], 1)
|
|
680
|
+
const todoG = await waitFor(() => /qG/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 40, 150)
|
|
681
|
+
assert(!!todoG, 'the deferred problem round settled (Formal/TODO.md recorded it)')
|
|
682
|
+
await tick(600)
|
|
683
|
+
const qs1 = JSON.parse(readIf(qsFile) || '[]')
|
|
684
|
+
const qG = qs1.find((q) => q.id === 'qG')
|
|
685
|
+
assert(!!qG && qG.已解决 !== true, '★ the problem was NOT marked solved (the require gate blocked the promotion)')
|
|
686
|
+
assert(!!qG && qG.解法列表[0].正确概率 !== 1, "the solution's probability was left as it was")
|
|
687
|
+
assert(!existsSync(join(proj, 'Verified', '问题_Verified.json')), 'no problem Verified card was written')
|
|
688
|
+
assert(/qG/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 'the problem is on the formalization TODO')
|
|
689
|
+
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'qG', note: '问题的形式化超出本轮工作量' })
|
|
690
|
+
await startScheduler(h) // see case 7: the deferral may have stopped the scheduler
|
|
691
|
+
const sp2 = await verifyWithDebate(h, 'r-qG-s0', 1, 0)
|
|
692
|
+
assert(!!sp2.first, 'the problem solution was re-verified after the blocker was recorded')
|
|
693
|
+
const qs2 = JSON.parse(readIf(qsFile) || '[]')
|
|
694
|
+
const qG2 = qs2.find((q) => q.id === 'qG')
|
|
695
|
+
assert(!!qG2 && qG2.已解决 === true, 'with a reasoned blocker the problem DOES close')
|
|
696
|
+
assert(!!qG2 && qG2.解法列表[0].正确概率 === 1, 'the solution probability becomes 1 after the gate opened')
|
|
697
|
+
const cards = JSON.parse(readIf(join(proj, 'Verified', '问题_Verified.json')) || '[]')
|
|
698
|
+
const card = cards.find((c) => c.id === 'qG')
|
|
699
|
+
assert(!!card && /阻塞(/.test(card['形式化'] || ''), 'the problem card records the formal status')
|
|
700
|
+
assert(!existsSync(join(vibeRoot(h), 'Formal', 'Lib', 'qG.lean')), 'a blocked record does not fabricate a library file')
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ---------- 9. persistence across resume ----------
|
|
704
|
+
section('9 the formal state survives resume (v2 has no session projection)')
|
|
705
|
+
{
|
|
706
|
+
const h = await makeCase('persist')
|
|
707
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require' })
|
|
708
|
+
await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pPersist', content: 'theorem p_persist : 1 + 1 = 2 := by decide\n' })
|
|
709
|
+
const before = await h.call('vibe_math_status', {})
|
|
710
|
+
assert(before.formal.objects.some((o) => o.target === 'pPersist' && o.status === 'passed'), 'the object is passed before the restart')
|
|
711
|
+
const proj = projRoot(h)
|
|
712
|
+
writeFileSync(join(proj, 'VibeMath_State', 'process_epoch.json'), JSON.stringify('OLD-PROCESS-EPOCH'), 'utf8')
|
|
713
|
+
const res = await h.call('vibe_math_resume', {})
|
|
714
|
+
assert(res.ok === true, 'resume succeeded')
|
|
715
|
+
const after = await h.call('vibe_math_status', {})
|
|
716
|
+
assert(after.formal.objects.some((o) => o.target === 'pPersist' && o.status === 'passed'),
|
|
717
|
+
'★ the Lean-passed object is still passed after resume (the gate does not forget)')
|
|
718
|
+
assert(/pPersist/.test(readIf(join(proj, 'Formal', 'Index.md'))), 'Formal/Index.md still indexes it (the files are the readable mirror)')
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ---------- 10. the gate is a no-op unless require ----------
|
|
722
|
+
section("10 'encourage' never gates (a verdict still lands with no Lean artifact)")
|
|
723
|
+
{
|
|
724
|
+
const h = await makeCase('nogate')
|
|
725
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage', maxParallelThreshold: 8 })
|
|
726
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
727
|
+
await startScheduler(h)
|
|
728
|
+
const proj = projRoot(h)
|
|
729
|
+
await h.call('vibe_math_add_proposition', { id: 'pFree', 概述: '鼓励模式不设门禁', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
730
|
+
const vs = await verifyWithDebate(h, 'r-pFree', 1, 0)
|
|
731
|
+
assert(!!vs.first, 'verifiers spawned under encourage mode')
|
|
732
|
+
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
733
|
+
const p = props.find((x) => x.id === 'pFree') || {}
|
|
734
|
+
assert(p.布尔估计 === 1, "'encourage' finalizes normally with NO Lean artifact (布尔估计=1)")
|
|
735
|
+
const cards = JSON.parse(readIf(join(proj, 'Verified', '数论_Verified.json')) || '[]')
|
|
736
|
+
assert(!!cards.find((c) => c.id === 'pFree'), "'encourage' writes the Verified card")
|
|
737
|
+
assert(!existsSync(join(proj, 'Formal', 'TODO.md')) || !/pFree/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 'no formalization TODO is created in encourage mode')
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ---------- 11. the reply channel is REAL (contract §4 / §6.3 / §10.8) ----------
|
|
741
|
+
// The first version of this feature only WROTE "请在回执的 formal 字段写明难度判断" into the prompt
|
|
742
|
+
// and never parsed it: 177 green assertions guarded a dead channel (AUDIT-CHECKLIST §2.2). These
|
|
743
|
+
// cases feed a genuine agent reply through the framework's own dispatch path (subagent/end →
|
|
744
|
+
// handleVerifier → absorb) and assert the DURABLE record, not the wording.
|
|
745
|
+
section('11 the reply channel really lands in the record (blocked / note validation)')
|
|
746
|
+
{
|
|
747
|
+
const h = await makeCase('reply')
|
|
748
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
749
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
750
|
+
await startScheduler(h)
|
|
751
|
+
await h.call('vibe_math_add_proposition', { id: 'pReply', 概述: '用回执记录阻塞', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
752
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pReply'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
753
|
+
assert(!!vs, 'verifiers were spawned for the reply-channel proposition')
|
|
754
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pReply:0') || {}).prompt || ''
|
|
755
|
+
assert(/"formal":\{"target":"r-pReply","decision":"used\|blocked\|defect"/.test(vp), '★ the review contract itself advertises the formal field, keyed by the verification id')
|
|
756
|
+
if (vs) {
|
|
757
|
+
// ① `blocked` WITH a note, named by the VERIFICATION id → the OBJECT record must be synced too.
|
|
758
|
+
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '我判断形式化不划算', formal: { target: 'r-pReply', decision: 'blocked', note: '需要大量未形式化的实分析前置知识' } })
|
|
759
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['r-pReply'] && r['r-pReply'].status === 'blocked') ? r : undefined }, 40, 150)
|
|
760
|
+
assert(!!rec, '★ a `formal.decision=blocked` reply is really absorbed — the channel is not dead code')
|
|
761
|
+
assert(!!rec && rec['r-pReply'].decision === 'blocked' && /实分析前置知识/.test(rec['r-pReply'].note || ''), 'the record keeps the decision AND the reason')
|
|
762
|
+
assert(!!rec && !!rec['pReply'] && rec['pReply'].status === 'blocked' && /实分析前置知识/.test(rec['pReply'].note || ''), '★ the OBJECT-id record is synced too (the two id spaces must not drift)')
|
|
763
|
+
assert(/实分析前置知识/.test(readIf(join(projRoot(h), 'Formal', 'Index.md'))), 'the reply-recorded blocker reaches Formal/Index.md')
|
|
764
|
+
// ② `blocked` WITHOUT a note (a different target) → refused with V2_INVALID_ARGUMENT, no record.
|
|
765
|
+
replyFrom(h, vs[1].childId, { Result: 0.5, Reason: '不想做', formal: { target: 'r-pNoNote', decision: 'blocked' } })
|
|
766
|
+
let acts = ''
|
|
767
|
+
for (let i = 0; i < 25; i++) {
|
|
768
|
+
const st = await h.call('vibe_math_status', {})
|
|
769
|
+
acts = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
770
|
+
if (/V2_INVALID_ARGUMENT/.test(acts)) break
|
|
771
|
+
await sleep(120)
|
|
772
|
+
}
|
|
773
|
+
assert(/V2_INVALID_ARGUMENT/.test(acts), '★ a blocked/defect judgement without a note is REJECTED with V2_INVALID_ARGUMENT')
|
|
774
|
+
assert(/没有写明 note/.test(acts), 'the refusal says why (an explicit decision is required, never a silent skip)')
|
|
775
|
+
const recN = formalStateOf(h)
|
|
776
|
+
assert(!(recN.records || {})['r-pNoNote'] && !(recN.records || {})['pNoNote'], 'no record is invented for the refused judgement')
|
|
777
|
+
assert(!/pNoNote/.test(readIf(join(projRoot(h), 'Formal', 'Index.md'))), 'the refused judgement does not reach the index either')
|
|
778
|
+
// ③ a reply with NO target must not invent an object (safeId('') would fall back to 'anon').
|
|
779
|
+
// The 0.5/0.5 round has no consensus, so the debate round re-woke both children — that is the
|
|
780
|
+
// observable proof that these children are registered and can answer again.
|
|
781
|
+
const woke = await waitFor(() => (h.followups.filter((f) => /DEBATE/.test(f.prompt || '')).length >= 2 ? true : undefined), 40, 150)
|
|
782
|
+
assert(!!woke, 'the debate round re-woke the verifiers (round-2 replies reach the framework again)')
|
|
783
|
+
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '漏写 target', formal: { decision: 'blocked', note: '没有写 target' } })
|
|
784
|
+
let acts2 = ''
|
|
785
|
+
for (let i = 0; i < 25; i++) {
|
|
786
|
+
const st = await h.call('vibe_math_status', {})
|
|
787
|
+
acts2 = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
788
|
+
if (/没有 target/.test(acts2)) break
|
|
789
|
+
await sleep(120)
|
|
790
|
+
}
|
|
791
|
+
assert(/没有 target/.test(acts2), '★ a `formal` reply without a target is refused, not guessed')
|
|
792
|
+
const recT = formalStateOf(h)
|
|
793
|
+
assert(!(recT.records || {}).anon, '★ a `formal` reply without a target cannot invent a record (no "anon" object)')
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ---------- 12. `defect`: a fidelity defect is NOT "the proposition is false" (§4.1) ----------
|
|
798
|
+
section('12 a defect reply withdraws the proof, writes the TODO and defers the verdict')
|
|
799
|
+
{
|
|
800
|
+
const h = await makeCase('defect')
|
|
801
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
802
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
803
|
+
await startScheduler(h)
|
|
804
|
+
const proj = projRoot(h)
|
|
805
|
+
// A PASSING Lean proof exists for the object — archived under the OBJECT id, the realistic path.
|
|
806
|
+
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pDefect', content: 'theorem p_defect : 2 + 2 = 4 := by decide\n' })
|
|
807
|
+
assert(proof.ok === true && proof.passed === true, 'precondition: the object starts Lean-passed')
|
|
808
|
+
const proofFile = join(proj, 'Verified', 'Lean', 'pDefect.lean')
|
|
809
|
+
assert(existsSync(proofFile), 'the archived proof exists before the defect is reported')
|
|
810
|
+
await h.call('vibe_math_add_proposition', { id: 'pDefect', 概述: '形式化写窄了的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
811
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pDefect'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
812
|
+
assert(!!vs, 'verifiers were spawned for the Lean-passed object')
|
|
813
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pDefect:0') || {}).prompt || ''
|
|
814
|
+
assert(/忠实性审查/.test(vp) && /不要投 0/.test(vp), 'the reviewers were told to audit fidelity and NOT to vote 0 on a defect')
|
|
815
|
+
const DEFECT = 'Lean 只证了 n>0 的情形,命题原文是 n≥0'
|
|
816
|
+
if (vs) {
|
|
817
|
+
// A fidelity defect: the voter ABSTAINS (0.3) and records it through the reply channel.
|
|
818
|
+
replyFrom(h, vs[0].childId, { Result: 0.3, Reason: '形式化写窄了(弃权)', formal: { target: 'pDefect', decision: 'defect', note: DEFECT } })
|
|
819
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pDefect'] && r['pDefect'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
820
|
+
assert(!!rec, '★ the defect reply is absorbed')
|
|
821
|
+
assert(!!rec && rec['pDefect'].status === 'attempted', '★ the object is DOWNGRADED to attempted (the "passed" status is withdrawn)')
|
|
822
|
+
assert(!!rec && rec['pDefect'].proof === '', '★ the proof pointer is cleared')
|
|
823
|
+
assert(!!rec && rec['pDefect'].note === DEFECT, 'the concrete deviation is recorded on the object record')
|
|
824
|
+
assert(!!rec && !!rec['r-pDefect'] && rec['r-pDefect'].status === 'attempted' && rec['r-pDefect'].decision === 'defect', '★ the verification-id record is downgraded too (both id spaces)')
|
|
825
|
+
assert(!existsSync(proofFile), '★ the archived proof Verified/Lean/pDefect.lean is DELETED')
|
|
826
|
+
assert(existsSync(join(proj, 'Formal', 'pDefect.lean')), 'the working file Formal/pDefect.lean is kept (the code is not lost)')
|
|
827
|
+
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
828
|
+
assert(/pDefect/.test(todo) && /defect/.test(todo), '★ the object is listed in Formal/TODO.md with the defect reason')
|
|
829
|
+
assert(/只证了 n>0 的情形/.test(todo), 'the TODO carries the concrete deviation, not just a flag')
|
|
830
|
+
let acts = ''
|
|
831
|
+
for (let i = 0; i < 25; i++) {
|
|
832
|
+
const st = await h.call('vibe_math_status', {})
|
|
833
|
+
acts = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
834
|
+
if (/忠实性缺陷/.test(acts)) break
|
|
835
|
+
await sleep(120)
|
|
836
|
+
}
|
|
837
|
+
assert(/忠实性缺陷/.test(acts), 'the downgrade is announced on the v2-readable channel (activity log)')
|
|
838
|
+
assert(/撤回「已通过」状态/.test(acts), 'the announcement says the passed status was withdrawn')
|
|
839
|
+
assert(/本次裁定不定论/.test(acts), 'and that the verdict is undecided — a defect withdraws a proof, it does not refute the proposition')
|
|
840
|
+
// Now drive the same round to a UNANIMOUS "true": without the defect it would conclude.
|
|
841
|
+
const settled = await verifyWithDebate(h, 'r-pDefect', 1, 0)
|
|
842
|
+
assert(!!settled.first, 'the round was driven to a verdict after the defect')
|
|
843
|
+
const todo2 = await waitFor(() => { const t = readIf(join(proj, 'Formal', 'TODO.md')); return /formal-required/.test(t) ? t : undefined }, 40, 150)
|
|
844
|
+
assert(!!todo2 && /r-pDefect/.test(todo2), '★ require mode DEFERS after a defect (the object enters the formalization TODO as formal-required)')
|
|
845
|
+
await tick(400)
|
|
846
|
+
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
847
|
+
const p = props.find((x) => x.id === 'pDefect') || {}
|
|
848
|
+
assert(p.布尔估计 === 0.5, '★★ the proposition is NOT recorded as false — 布尔估计 unchanged (got ' + p.布尔估计 + ')')
|
|
849
|
+
assert(!(p.证明列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 proof entry was written')
|
|
850
|
+
assert(p.优先级 !== 'never', 'the priority was not pinned to never')
|
|
851
|
+
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), '★ no Verified card: the verdict is UNDECIDED, not "false"')
|
|
852
|
+
}
|
|
853
|
+
// The reverse direction: a defect named by the VERIFICATION id must still downgrade the OBJECT
|
|
854
|
+
// record that actually holds the proof (this is the pair that silently drifts when only one side
|
|
855
|
+
// is written — formalGateRecord would read `passed` from the other side).
|
|
856
|
+
const h2 = await makeCase('defect-rid')
|
|
857
|
+
await h2.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
858
|
+
await h2.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
859
|
+
await startScheduler(h2)
|
|
860
|
+
const proj2 = projRoot(h2)
|
|
861
|
+
await h2.call('vibe_math_lean_archive', { kind: 'proof', target: 'pDefect2', content: 'theorem p_defect2 : 2 + 2 = 4 := by decide\n' })
|
|
862
|
+
await h2.call('vibe_math_add_proposition', { id: 'pDefect2', 概述: '回执用验证 id 命名的缺陷', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
863
|
+
const vs2 = await waitFor(() => { const x = verifiersOf(h2, 'r-pDefect2'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
864
|
+
assert(!!vs2, 'verifiers were spawned for the reverse-direction case')
|
|
865
|
+
if (vs2) {
|
|
866
|
+
replyFrom(h2, vs2[0].childId, { Result: 0.3, Reason: '写宽了', formal: { target: 'r-pDefect2', decision: 'defect', note: 'Lean 版本没有假设 n≥1,比原文更宽' } })
|
|
867
|
+
const rec2 = await waitFor(() => { const r = (formalStateOf(h2).records || {}); return (r['r-pDefect2'] && r['r-pDefect2'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
868
|
+
assert(!!rec2, '★ a defect named by the verification id is absorbed')
|
|
869
|
+
assert(!!rec2 && !!rec2['pDefect2'] && rec2['pDefect2'].status === 'attempted', '★ and the OBJECT record that held `passed` is downgraded as well')
|
|
870
|
+
assert(!!rec2 && rec2['pDefect2'].proof === '', 'its proof pointer is cleared too')
|
|
871
|
+
assert(!existsSync(join(proj2, 'Verified', 'Lean', 'pDefect2.lean')), '★ the archived proof is deleted even though the reply named the other id')
|
|
872
|
+
assert(/pDefect2/.test(readIf(join(proj2, 'Formal', 'TODO.md'))), 'the object is on the formalization TODO')
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ---------- 13. the prompt corpus (contract §10.10) ----------
|
|
877
|
+
// A HUMAN must be able to re-read every prompt the framework emitted, not just the assertions
|
|
878
|
+
// about them. Paths are normalised so the dump is deterministic, diffable and machine-free.
|
|
879
|
+
section('13 the captured prompt corpus is written for human review')
|
|
880
|
+
{
|
|
881
|
+
// Freeze the scheduler in every case FIRST: a still-running tick loop could emit one more
|
|
882
|
+
// prompt between two runs and make the corpus non-deterministic.
|
|
883
|
+
for (const h of hosts) { try { await h.call('vibe_math_pause', {}) } catch (e) { /* ignore */ } }
|
|
884
|
+
const scrub = (h, s) => {
|
|
885
|
+
const ws = String(h.WS)
|
|
886
|
+
const slash = ws.replace(/\\/g, '/')
|
|
887
|
+
return String(s == null ? '' : s)
|
|
888
|
+
.split(slash + '/VibeMath').join('<VIBEMATH>')
|
|
889
|
+
.split(ws + '\\VibeMath').join('<VIBEMATH>')
|
|
890
|
+
.split(slash).join('<WS>')
|
|
891
|
+
.split(ws).join('<WS>')
|
|
892
|
+
}
|
|
893
|
+
const entries = []
|
|
894
|
+
for (const h of hosts) {
|
|
895
|
+
for (const s of h.spawns) entries.push({ kind: 'spawn', case: h.label, label: s.label, prompt: scrub(h, s.prompt) })
|
|
896
|
+
for (const f of h.followups) {
|
|
897
|
+
const owner = h.spawns.find((s) => s.childId === f.childId)
|
|
898
|
+
entries.push({ kind: 'wake', case: h.label, label: owner ? owner.label : f.childId, prompt: scrub(h, f.prompt) })
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
mkdirSync(CORPUS_DIR, { recursive: true })
|
|
902
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.json'), JSON.stringify({ entries: entries }, null, 2), 'utf8')
|
|
903
|
+
const md = ['# V2 形式化验证交互语料(prompt corpus)', '',
|
|
904
|
+
'> 由 `formal-verify-v2.test.mjs` 落盘:框架**真正发出**的每一条提示词原文。',
|
|
905
|
+
'> 工作区路径归一化为 `<WS>`、VibeMath 根归一化为 `<VIBEMATH>`,因此可 diff、不泄露本机路径。',
|
|
906
|
+
'> 覆盖:off 档(无任何 Lean 文字)、encourage 与 require 的表决初评/辩论、passed 后的忠实性分支、',
|
|
907
|
+
'> 以及平时工作轮的「顺手形式化」段落与 formal 回执契约。', '']
|
|
908
|
+
for (let i = 0; i < entries.length; i++) {
|
|
909
|
+
const e = entries[i]
|
|
910
|
+
md.push('## [' + i + '] ' + e.kind + ' · ' + e.label + ' · case=' + e.case)
|
|
911
|
+
md.push('')
|
|
912
|
+
md.push('```text')
|
|
913
|
+
md.push(e.prompt)
|
|
914
|
+
md.push('```')
|
|
915
|
+
md.push('')
|
|
916
|
+
}
|
|
917
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.md'), md.join('\n'), 'utf8')
|
|
918
|
+
assert(existsSync(join(CORPUS_DIR, 'formal-verify-v2.json')) && existsSync(join(CORPUS_DIR, 'formal-verify-v2.md')), 'the prompt corpus was written (JSON + Markdown)')
|
|
919
|
+
assert(entries.length >= 15, 'the corpus covers the whole run (' + entries.length + ' prompts)')
|
|
920
|
+
assert(entries.some((e) => /【Lean 形式化验证(鼓励模式)】/.test(e.prompt)), 'the corpus contains the encourage verify prompt')
|
|
921
|
+
assert(entries.some((e) => /【Lean 形式化验证(强制模式)】/.test(e.prompt)), '★ the corpus contains the REQUIRE verify prompt')
|
|
922
|
+
assert(entries.some((e) => /你不需要重新检查推导/.test(e.prompt) && /一致 → Result = 1/.test(e.prompt)), 'the corpus contains the passed/fidelity prompt')
|
|
923
|
+
assert(entries.some((e) => /【顺手形式化(鼓励)】/.test(e.prompt)), 'the corpus contains the work-round 顺手形式化 prompt')
|
|
924
|
+
assert(entries.some((e) => /【顺手形式化/.test(e.prompt) && /"formal":\{"target":"<对象id>","decision":"used\|blocked\|defect"/.test(e.prompt)), 'the corpus contains the formal reply contract line')
|
|
925
|
+
assert(entries.some((e) => !/Lean|形式化/.test(e.prompt)), 'the corpus contains off-mode prompts with no Lean text at all')
|
|
926
|
+
assert(entries.some((e) => e.kind === 'wake'), 'the corpus also keeps the continuation prompts (debate rounds)')
|
|
927
|
+
// Generic sweeps over EVERY captured prompt, not spot checks (AUDIT §2.1).
|
|
928
|
+
const joined = entries.map((e) => e.prompt).join('\n')
|
|
929
|
+
const bare = entries.filter((e) => /(^|[^a-z_])lean_(run|archive|lib)/.test(e.prompt))
|
|
930
|
+
assert(bare.length === 0, '★ no captured prompt names an abbreviated tool (' + bare.map((b) => b.label).join(',') + ')')
|
|
931
|
+
const zero = entries.filter((e) => /偏离\s*→\s*0/.test(e.prompt))
|
|
932
|
+
assert(zero.length === 0, '★ no captured prompt turns a fidelity defect into a 0 vote (' + zero.map((z) => z.label).join(',') + ')')
|
|
933
|
+
const wrongField = entries.filter((e) => /忠实性/.test(e.prompt) && /verdict/.test(e.prompt))
|
|
934
|
+
assert(wrongField.length === 0, '★ no fidelity prompt names a `verdict` field (' + wrongField.map((w) => w.label).join(',') + ')')
|
|
935
|
+
const dirty = entries.filter((e) => /\[object Object\]|\bNaN\b|:\s*undefined|["']undefined["']|undefined\s*[,}\]]/.test(e.prompt))
|
|
936
|
+
assert(dirty.length === 0, 'no captured prompt contains placeholder garbage (' + dirty.map((d) => d.label).join(',') + ')')
|
|
937
|
+
assert(joined.indexOf('<VIBEMATH>') !== -1, 'the VibeMath root is normalised to <VIBEMATH>')
|
|
938
|
+
for (const h of hosts) {
|
|
939
|
+
const ws = String(h.WS)
|
|
940
|
+
assert(joined.indexOf(ws) === -1 && joined.indexOf(ws.replace(/\\/g, '/')) === -1, 'no captured prompt leaks a machine path (' + h.label + ')')
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// cleanup
|
|
945
|
+
for (const h of hosts) { try { rmSync(h.WS, { recursive: true, force: true }) } catch (e) { /* ignore */ } }
|
|
946
|
+
|
|
947
|
+
console.log('')
|
|
948
|
+
console.log('passed=' + passed + ' failed=' + failed)
|
|
949
|
+
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
950
|
+
console.log('ALL GREEN')
|
|
951
|
+
process.exit(0)
|