dsh-vibe-math 2.3.12 → 2.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -13
- package/cordis.patch.yml +1 -1
- package/{AUDIT-CHECKLIST.md → docs/AUDIT-CHECKLIST.md} +323 -304
- package/docs/COMPAT-AUDIT-ROUND2.md +325 -0
- package/docs/generate_framework_diagram_v2.py +114 -0
- package/docs/generate_framework_diagram_v3.py +127 -0
- package/{RELEASE-NOTES-2.1.0.md → docs/release-notes/RELEASE-NOTES-2.1.0.md} +143 -143
- package/{RELEASE-NOTES-2.2.0.md → docs/release-notes/RELEASE-NOTES-2.2.0.md} +266 -266
- package/{RELEASE-NOTES-2.2.1.md → docs/release-notes/RELEASE-NOTES-2.2.1.md} +43 -43
- package/{RELEASE-NOTES-2.2.2.md → docs/release-notes/RELEASE-NOTES-2.2.2.md} +88 -88
- package/{RELEASE-NOTES-2.3.0.md → docs/release-notes/RELEASE-NOTES-2.3.0.md} +207 -207
- package/{RELEASE-NOTES-2.3.1.md → docs/release-notes/RELEASE-NOTES-2.3.1.md} +134 -134
- package/{RELEASE-NOTES-2.3.10.md → docs/release-notes/RELEASE-NOTES-2.3.10.md} +105 -105
- package/{RELEASE-NOTES-2.3.11.md → docs/release-notes/RELEASE-NOTES-2.3.11.md} +57 -57
- package/{RELEASE-NOTES-2.3.12.md → docs/release-notes/RELEASE-NOTES-2.3.12.md} +80 -80
- package/docs/release-notes/RELEASE-NOTES-2.3.13.md +137 -0
- package/docs/release-notes/RELEASE-NOTES-2.3.14.md +83 -0
- package/{RELEASE-NOTES-2.3.2.md → docs/release-notes/RELEASE-NOTES-2.3.2.md} +145 -145
- package/{RELEASE-NOTES-2.3.3.md → docs/release-notes/RELEASE-NOTES-2.3.3.md} +115 -115
- package/{RELEASE-NOTES-2.3.4.md → docs/release-notes/RELEASE-NOTES-2.3.4.md} +69 -69
- package/{RELEASE-NOTES-2.3.5.md → docs/release-notes/RELEASE-NOTES-2.3.5.md} +63 -63
- package/{RELEASE-NOTES-2.3.6.md → docs/release-notes/RELEASE-NOTES-2.3.6.md} +66 -66
- package/{RELEASE-NOTES-2.3.7.md → docs/release-notes/RELEASE-NOTES-2.3.7.md} +59 -59
- package/{RELEASE-NOTES-2.3.8.md → docs/release-notes/RELEASE-NOTES-2.3.8.md} +45 -45
- package/{RELEASE-NOTES-2.3.9.md → docs/release-notes/RELEASE-NOTES-2.3.9.md} +70 -70
- package/docs/test-timing.md +19 -18
- package/installer.js +115 -41
- package/package.json +43 -37
- package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
- package/{audit-installer-compat.test.mjs → tests/audit-installer-compat.test.mjs} +136 -136
- package/tests/audit-installer-policy.test.mjs +261 -0
- package/{audit-persona-sensitivity.mjs → tests/audit-persona-sensitivity.mjs} +249 -249
- package/{audit-persona-surface.test.mjs → tests/audit-persona-surface.test.mjs} +349 -349
- package/{audit-prompt-invariants.mjs → tests/audit-prompt-invariants.mjs} +508 -508
- package/{audit-spec-traceability.mjs → tests/audit-spec-traceability.mjs} +193 -193
- package/{audit-v5-integrity.mjs → tests/audit-v5-integrity.mjs} +448 -448
- package/{audit-v5-sensitivity.mjs → tests/audit-v5-sensitivity.mjs} +384 -384
- package/{e2e-v5-round2.test.mjs → tests/e2e-v5-round2.test.mjs} +521 -521
- package/{formal-verify-v2.test.mjs → tests/formal-verify-v2.test.mjs} +1315 -1315
- package/{formal-verify-v3.test.mjs → tests/formal-verify-v3.test.mjs} +1257 -1257
- package/{formal-verify-v4.test.mjs → tests/formal-verify-v4.test.mjs} +1082 -1082
- package/{formal-verify-v5.test.mjs → tests/formal-verify-v5.test.mjs} +708 -708
- package/{prompt-v5-integrity.test.mjs → tests/prompt-v5-integrity.test.mjs} +3 -3
- package/{run-tests.mjs → tests/run-tests.mjs} +121 -118
- package/{selfdrive-v5.mjs → tests/selfdrive-v5.mjs} +470 -470
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +1 -1
- /package/{RELEASE-NOTES-2.0.22.md → docs/release-notes/RELEASE-NOTES-2.0.22.md} +0 -0
|
@@ -1,1315 +1,1315 @@
|
|
|
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('
|
|
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 terminations = []
|
|
60
|
-
const subprocess = {
|
|
61
|
-
async resolveExecutable(cmd) {
|
|
62
|
-
if (!toolchainAvailable) throw new Error('spawn lean ENOENT')
|
|
63
|
-
if (String(cmd) !== 'lean') throw new Error('unknown executable ' + cmd)
|
|
64
|
-
return 'lean'
|
|
65
|
-
},
|
|
66
|
-
spawn(spec) {
|
|
67
|
-
const last = spec.argv[spec.argv.length - 1]
|
|
68
|
-
const script = String(last || '')
|
|
69
|
-
// v2 creates its directory tree through this SAME subprocess service
|
|
70
|
-
// (`powershell … New-Item -Force -ItemType Directory -Path 'a','b'`). Without honouring
|
|
71
|
-
// it, the Formal/ and Verified/Lean/ assertions would be vacuous: they would fail for the
|
|
72
|
-
// wrong reason (a mocked no-op) instead of a real defect.
|
|
73
|
-
if (/New-Item/.test(script)) {
|
|
74
|
-
const m = /-Path\s+(.+?)\s*(\||$)/.exec(script)
|
|
75
|
-
if (m) m[1].split(',').forEach((p) => { const q = p.trim().replace(/^'|'$/g, '').replace(/''/g, "'"); if (q) mkdirSync(q, { recursive: true }) })
|
|
76
|
-
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
77
|
-
}
|
|
78
|
-
if (/mkdir -p /.test(script)) {
|
|
79
|
-
const m = /mkdir -p (.+)$/.exec(script)
|
|
80
|
-
if (m) m[1].split(/\s+/).forEach((p) => { const q = p.trim().replace(/^'|'$/g, ''); if (q) mkdirSync(q, { recursive: true }) })
|
|
81
|
-
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
82
|
-
}
|
|
83
|
-
// v2 ALSO deletes files through this same service (`powershell … Remove-Item -LiteralPath 'x'`,
|
|
84
|
-
// POSIX `rm -f 'x'`). Without honouring it, the `defect` assertion "the archived proof is
|
|
85
|
-
// gone" would pass vacuously (nothing was ever deleted) instead of testing the real code path.
|
|
86
|
-
if (/Remove-Item/.test(script)) {
|
|
87
|
-
const m = /-LiteralPath\s+'((?:[^']|'')*)'/.exec(script)
|
|
88
|
-
if (m) { try { rmSync(m[1].replace(/''/g, "'"), { force: true }) } catch (e) { /* best effort */ } }
|
|
89
|
-
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
90
|
-
}
|
|
91
|
-
if (/^\s*rm -f /.test(script)) {
|
|
92
|
-
const re = /'((?:[^']|'\\'')*)'/g
|
|
93
|
-
let m
|
|
94
|
-
while ((m = re.exec(script)) !== null) { try { rmSync(m[1].replace(/'\\''/g, "'"), { force: true }) } catch (e) { /* best effort */ } }
|
|
95
|
-
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
96
|
-
}
|
|
97
|
-
const text = existsSync(last) ? readFileSync(last, 'utf8') : ''
|
|
98
|
-
const bad = /sorry|-- FAIL/.test(text)
|
|
99
|
-
leanRuns.push({ argv: spec.argv.slice(0, -1), file: last, cwd: spec.cwd, graceMs: spec.graceMs, stdio: spec.stdio })
|
|
100
|
-
const stdout = bad ? '' : 'ok\n'
|
|
101
|
-
const stderr = bad ? 'error: declaration uses sorry\n' : ''
|
|
102
|
-
if (/-- HANG/.test(text)) {
|
|
103
|
-
// A run that never finishes by itself. It settles ONLY when the plugin actively terminates it
|
|
104
|
-
// (contract §7: "对超时调用 handle.terminate()"), with a bounded 2.5 s fallback so a plugin
|
|
105
|
-
// that FORGETS to terminate FAILS the terminate/speed assertions instead of hanging this suite.
|
|
106
|
-
let settled = false
|
|
107
|
-
let finish
|
|
108
|
-
const done = new Promise((resolve) => { finish = resolve })
|
|
109
|
-
const fallback = REAL_SET_TIMEOUT(() => { if (!settled) { settled = true; finish({ exitCode: null, signal: 'SIGKILL' }) } }, 2500)
|
|
110
|
-
return {
|
|
111
|
-
done,
|
|
112
|
-
collected: {
|
|
113
|
-
stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) },
|
|
114
|
-
stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) },
|
|
115
|
-
},
|
|
116
|
-
terminate() {
|
|
117
|
-
if (settled) return
|
|
118
|
-
settled = true
|
|
119
|
-
REAL_CLEAR_TIMEOUT(fallback)
|
|
120
|
-
terminations.push({ file: last })
|
|
121
|
-
finish({ exitCode: null, signal: 'SIGTERM' })
|
|
122
|
-
},
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
done: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null }),
|
|
127
|
-
collected: {
|
|
128
|
-
stdout: { readFrom: () => ({ text: stdout, nextOffset: stdout.length, lossy: false }) },
|
|
129
|
-
stderr: { readFrom: () => ({ text: stderr, nextOffset: stderr.length, lossy: false }) },
|
|
130
|
-
},
|
|
131
|
-
terminate() {},
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---------------------------------------------------------------
|
|
137
|
-
// mock host (v2 standing-mount shape: one plugin instance, many sessions)
|
|
138
|
-
// ---------------------------------------------------------------
|
|
139
|
-
function makeHost(WS, opts = {}) {
|
|
140
|
-
const listeners = {}
|
|
141
|
-
const toolRegs = []
|
|
142
|
-
const cmdRegs = []
|
|
143
|
-
const spawns = []
|
|
144
|
-
const followups = []
|
|
145
|
-
const interrupts = []
|
|
146
|
-
const ctx = {
|
|
147
|
-
get(name) {
|
|
148
|
-
if (name === 'subprocess') return opts.noSubprocess ? undefined : subprocess
|
|
149
|
-
if (name === 'sandboxPolicy') return { workspaceRoot: WS, resolve: () => ({ workspaceRoot: WS }) }
|
|
150
|
-
return undefined
|
|
151
|
-
},
|
|
152
|
-
on(event, fn) { (listeners[event] = listeners[event] || []).push(fn) },
|
|
153
|
-
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
154
|
-
logger: { info() {}, warn() {}, error() {} },
|
|
155
|
-
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
156
|
-
commands: { register(spec) { cmdRegs.push(spec); return () => {} } },
|
|
157
|
-
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
158
|
-
subagents: {
|
|
159
|
-
list() { return ['spawn'] },
|
|
160
|
-
async startContinuable({ label, request }) {
|
|
161
|
-
const childId = 'child-' + (spawns.length + 1) + '-' + Math.random().toString(36).slice(2, 6)
|
|
162
|
-
spawns.push({ label, childId, rootId: (request && request.parent && request.parent.id) || 'root', prompt: request && request.prompt && request.prompt[0] && request.prompt[0].text })
|
|
163
|
-
return { childId }
|
|
164
|
-
},
|
|
165
|
-
async followup(parent, childId, blocks) { followups.push({ childId, prompt: (blocks && blocks[0] && blocks[0].text) || '' }) },
|
|
166
|
-
async sendMessage(parent, childId, blocks) { followups.push({ childId, prompt: (blocks && blocks[0] && blocks[0].text) || '' }) },
|
|
167
|
-
interrupt(childId) { interrupts.push(childId) },
|
|
168
|
-
},
|
|
169
|
-
agents: { roots() { return [] }, get() { return undefined } },
|
|
170
|
-
fs: {
|
|
171
|
-
async resolve(rel, o) {
|
|
172
|
-
const raw = String(rel)
|
|
173
|
-
const p = isAbsolute(raw) ? raw : join((o && o.cwd) || WS, ...raw.split('/'))
|
|
174
|
-
return p.replace(/\\/g, '/')
|
|
175
|
-
},
|
|
176
|
-
async stat(t) { if (!existsSync(t)) return undefined; return { type: statSync(t).isDirectory() ? 'directory' : 'file' } },
|
|
177
|
-
async readText(t) { return readFileSync(t, 'utf8') },
|
|
178
|
-
async writeText(t, c) { mkdirSync(dirname(t), { recursive: true }); writeFileSync(t, c, 'utf8') },
|
|
179
|
-
async listDir(t) { if (!existsSync(t)) return []; return readdirSync(t, { withFileTypes: true }).map((e) => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
180
|
-
},
|
|
181
|
-
}
|
|
182
|
-
const fireEnd = (info) => { for (const h of (listeners['subagent/end'] || [])) h(info) }
|
|
183
|
-
return { ctx, toolRegs, cmdRegs, spawns, followups, interrupts, fireEnd, WS }
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function makeRoot(id, WS) {
|
|
187
|
-
return { id, options: { provider: 'mock', model: 'mock' }, session: { id, header: { cwd: WS, parentSession: undefined } }, followup() {} }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const hosts = []
|
|
191
|
-
/**
|
|
192
|
-
* Build an INDEPENDENT plugin instance + workspace for one test case.
|
|
193
|
-
* AUDIT-CHECKLIST §2.3: cases must not share a session root/workspace, or a leftover
|
|
194
|
-
* heartbeat/verification from one case pollutes the next.
|
|
195
|
-
*/
|
|
196
|
-
async function makeCase(label, opts = {}) {
|
|
197
|
-
const WS = mkdtempSync(join(tmpdir(), 'vibe-v2-lean-' + label + '-'))
|
|
198
|
-
const h = makeHost(WS, opts)
|
|
199
|
-
const mod = await import(PLUGIN.href + '?t=' + Date.now() + '-' + Math.random().toString(36).slice(2, 8))
|
|
200
|
-
;(mod.default || mod).apply(h.ctx)
|
|
201
|
-
const root = makeRoot('sess-' + label, WS)
|
|
202
|
-
const call = async (name, args, agent) => {
|
|
203
|
-
const spec = h.toolRegs.find((s) => s.name === name)
|
|
204
|
-
if (!spec) throw new Error('no tool ' + name)
|
|
205
|
-
return JSON.parse(await spec.execute(args || {}, { agent: agent || root }))
|
|
206
|
-
}
|
|
207
|
-
await call('vibe_math_new_project', { name: 'proj' })
|
|
208
|
-
// 200ms is the sanitizer floor; verifierCount=2 (the sanitizer floor too) keeps each
|
|
209
|
-
// verification exactly two children, so a round settles predictably.
|
|
210
|
-
await call('vibe_math_set_params', { tickIntervalMs: 200, verifierCount: 2 })
|
|
211
|
-
const c = Object.assign(h, { WS, root, call, label })
|
|
212
|
-
hosts.push(c)
|
|
213
|
-
return c
|
|
214
|
-
}
|
|
215
|
-
// The scheduler only picks objects up while it is RUNNING (scheduleTick early-returns).
|
|
216
|
-
async function startScheduler(h) { await h.call('vibe_math_start', {}) }
|
|
217
|
-
|
|
218
|
-
// ── test speed: fast-forward the SCHEDULER POLL (test-only, no production impact) ─────────
|
|
219
|
-
// The plugin registers its scheduler poll as `setInterval(..., 1000)` at apply() time, so every
|
|
220
|
-
// suite-side `tick()` had to wait a full wall-clock second. With ~9 verification rounds × ~17
|
|
221
|
-
// ticks that alone accounted for ~99% of this suite's 186 s (its v3/v4/v5 siblings take seconds).
|
|
222
|
-
// Patching ONLY setInterval (the suite's own `sleep` uses setTimeout) makes a poll cost ~25 ms,
|
|
223
|
-
// while the plugin's due-ness logic still uses the real 200 ms `tickIntervalMs` floor — no
|
|
224
|
-
// scheduler behaviour depends on the poll period, and `tickTheScheduler` below stays above it.
|
|
225
|
-
const REAL_SET_INTERVAL = globalThis.setInterval
|
|
226
|
-
globalThis.setInterval = function (fn, ms, ...rest) {
|
|
227
|
-
return REAL_SET_INTERVAL(fn, Math.min(Number(ms) || 0, 25), ...rest)
|
|
228
|
-
}
|
|
229
|
-
// ── test speed / leak check: track OUTSTANDING setTimeout handles (test-only) ──────────────
|
|
230
|
-
// `leanRunFile` races `handle.done` against its own `cap`-ms timer; forgetting to clear that timer on
|
|
231
|
-
// the normal path leaves one pending multi-minute timer PER RUN — a leak no output assertion can see.
|
|
232
|
-
// The plugin resolves the global at call time, so wrapping it here covers it. The mock's own bounded
|
|
233
|
-
// fallback uses the REAL functions, so it never pollutes the count.
|
|
234
|
-
const REAL_SET_TIMEOUT = globalThis.setTimeout
|
|
235
|
-
const REAL_CLEAR_TIMEOUT = globalThis.clearTimeout
|
|
236
|
-
const liveTimers = new Set()
|
|
237
|
-
globalThis.setTimeout = function (fn, ms, ...rest) {
|
|
238
|
-
let h
|
|
239
|
-
h = REAL_SET_TIMEOUT(function (...a) { liveTimers.delete(h); return fn.apply(this, a) }, ms, ...rest)
|
|
240
|
-
liveTimers.add(h)
|
|
241
|
-
return h
|
|
242
|
-
}
|
|
243
|
-
globalThis.clearTimeout = function (h) { liveTimers.delete(h); return REAL_CLEAR_TIMEOUT(h) }
|
|
244
|
-
|
|
245
|
-
const projRoot = (h) => join(h.WS, 'VibeMath', 'Projects', 'proj')
|
|
246
|
-
const vibeRoot = (h) => join(h.WS, 'VibeMath')
|
|
247
|
-
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
248
|
-
const formalStateOf = (h) => JSON.parse(readIf(join(projRoot(h), 'VibeMath_State', 'formal.json')) || '{}')
|
|
249
|
-
// One scheduler pass. With the poll fast-forwarded above, the poll fires every ~25 ms and a tick
|
|
250
|
-
// runs whenever the plugin's own 200 ms `tickIntervalMs` floor has elapsed, so 260 ms is one pass.
|
|
251
|
-
const tick = (ms = 260) => sleep(ms)
|
|
252
|
-
async function waitFor(pred, tries = 80, ms = 60) {
|
|
253
|
-
for (let i = 0; i < tries; i++) { const v = pred(); if (v) return v; await sleep(ms) }
|
|
254
|
-
return undefined
|
|
255
|
-
}
|
|
256
|
-
const verifiersOf = (h, rKind, exclude) => h.spawns.filter((s) => s.label.startsWith('verifier:' + rKind) && !(exclude || []).some((o) => o.childId === s.childId))
|
|
257
|
-
/** A reply exactly as an agent would emit it (a fenced JSON block) — the framework's real input. */
|
|
258
|
-
const fence = (obj) => '```json\n' + JSON.stringify(obj) + '\n```'
|
|
259
|
-
/** Feed ONE agent reply (a fresh turn's end) to the framework through the real dispatch path. */
|
|
260
|
-
const replyFrom = (h, childId, obj) => h.fireEnd({
|
|
261
|
-
id: childId, runId: 'r-' + childId, provider: 'spawn', local: true, stopReason: 'completed',
|
|
262
|
-
lastAssistantMessage: [{ type: 'text', text: fence(obj) }],
|
|
263
|
-
})
|
|
264
|
-
const fireVerdicts = (h, kids, vale) => {
|
|
265
|
-
for (let i = 0; i < kids.length; i++) {
|
|
266
|
-
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```' }] })
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Verify one object and DRIVE IT TO A SETTLEMENT.
|
|
271
|
-
*
|
|
272
|
-
* Important: v2's `processVerify` starts at most ONE verification and skips every candidate
|
|
273
|
-
* while a task is live, so leaving an object mid-debate would silently block the NEXT case's
|
|
274
|
-
* verification. The first `firstRounds` rounds are answered with `vale` (a non-unanimous value
|
|
275
|
-
* forces a real debate round, which is exactly what the debate-prompt assertions need), and any
|
|
276
|
-
* later round is answered with `settle` so the object actually closes.
|
|
277
|
-
*/
|
|
278
|
-
async function verifyWithDebate(h, rKind, vale, firstRounds = 1, settle = 1) {
|
|
279
|
-
// v2 re-wakes the SAME verifier children for every debate round (it does not spawn new
|
|
280
|
-
// ones), so a new round is detected by counting the followups written to those children.
|
|
281
|
-
const kids = new Set()
|
|
282
|
-
// Early exit: a settled object stops producing followups. Without this the loop always spent
|
|
283
|
-
// its FULL 16 passes (~21 s at the old 1.3 s/pass), which is what made this suite take 3
|
|
284
|
-
// minutes: the debate is over, but the helper kept ticking at nothing. Three consecutive
|
|
285
|
-
// passes with no new followup = the framework has nothing left to ask.
|
|
286
|
-
let lastN = -1
|
|
287
|
-
let quiet = 0
|
|
288
|
-
for (let i = 0; i < 16; i++) {
|
|
289
|
-
// The scheduler may need a whole pass before it notices the object and another before it
|
|
290
|
-
// fills the verifier quota, so each wait must span several timer ticks.
|
|
291
|
-
const cand = await waitFor(() => {
|
|
292
|
-
const x = verifiersOf(h, rKind)
|
|
293
|
-
return x.length >= 2 ? x : undefined
|
|
294
|
-
}, 40, 250)
|
|
295
|
-
if (!cand) break
|
|
296
|
-
cand.forEach((s) => kids.add(s.childId))
|
|
297
|
-
const rounds = 1 + h.followups.filter((f) => kids.has(f.childId)).length
|
|
298
|
-
// `fireEnd` is a NO-OP for a child with no in-flight turn, so answering the whole current
|
|
299
|
-
// verifier set each pass is safe; the extra answers land before the next round's wakes.
|
|
300
|
-
fireVerdicts(h, cand, rounds <= firstRounds ? vale : settle)
|
|
301
|
-
await sleep(200)
|
|
302
|
-
await tick()
|
|
303
|
-
if (h.followups.length === lastN) { if (++quiet >= 3) break } else { quiet = 0; lastN = h.followups.length }
|
|
304
|
-
}
|
|
305
|
-
const rounds = 1 + h.followups.filter((f) => kids.has(f.childId)).length
|
|
306
|
-
return { first: h.spawns.filter((s) => s.label.startsWith('verifier:' + rKind)), rounds: kids.size ? rounds : 0 }
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// ===============================================================
|
|
310
|
-
console.log('-- V2 Lean formal verification --')
|
|
311
|
-
|
|
312
|
-
// ---------- 1. 'off' is a true no-op ----------
|
|
313
|
-
section("1 'off' (default) is a true no-op")
|
|
314
|
-
{
|
|
315
|
-
const h = await makeCase('off')
|
|
316
|
-
const st = await h.call('vibe_math_status', {})
|
|
317
|
-
assert(st.params.formalVerify === 'off', "the default mode is 'off' (got " + st.params.formalVerify + ')')
|
|
318
|
-
assert(st.formal.mode === 'off' && st.formal.required === false, 'status exposes the formal mode and gate flag')
|
|
319
|
-
assert(st.params.leanCommand === 'lean' && Array.isArray(st.params.leanArgs), 'the Lean knobs are readable in status.params')
|
|
320
|
-
assert(st.params.leanTimeoutMs === 120000, 'leanTimeoutMs defaults to 120000 (got ' + st.params.leanTimeoutMs + ')')
|
|
321
|
-
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'),
|
|
322
|
-
'the three Lean tools are registered in every mode (registration is static)')
|
|
323
|
-
// ★ The mode switch must be REACHABLE THROUGH THE TOOL SCHEMA (2.3.2 defect D1) ──────────────
|
|
324
|
-
// Every tool schema here is closed (`additionalProperties:false`), so a key the schema does not
|
|
325
|
-
// advertise is REJECTED by any schema-validating provider. v3 shipped 2.3.0/2.3.1 with all four Lean
|
|
326
|
-
// parameters missing from the set-params schema while every assertion in this file stayed green —
|
|
327
|
-
// because the suite calls the handler DIRECTLY and never inspects the registered schema. The feature
|
|
328
|
-
// could not be switched on at all through the tool interface.
|
|
329
|
-
{
|
|
330
|
-
const setSpec = h.toolRegs.find((t) => t.name === 'vibe_math_set_params')
|
|
331
|
-
assert(!!setSpec, "vibe_math_set_params is registered")
|
|
332
|
-
assert(setSpec.parameters && setSpec.parameters.type === 'object' && setSpec.parameters.additionalProperties === false,
|
|
333
|
-
'★ vibe_math_set_params publishes a CLOSED object schema (an unlisted key is rejected, so the schema IS the contract)')
|
|
334
|
-
for (const k of ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']) {
|
|
335
|
-
assert(Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k),
|
|
336
|
-
'★ the registered schema advertises ' + k + ' (every other surface documents it; a schema that omits it makes the switch unreachable)')
|
|
337
|
-
}
|
|
338
|
-
assert(JSON.stringify(setSpec.parameters.properties.formalVerify.enum) === JSON.stringify(['off', 'encourage', 'require']),
|
|
339
|
-
'the schema narrows formalVerify to the three real modes (a typo must not be a fourth)')
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
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')
|
|
343
|
-
assert(existsSync(join(projRoot(h), 'Formal')) && existsSync(join(projRoot(h), 'Verified', 'Lean')), 'the project Formal/ and Verified/Lean/ dirs are created')
|
|
344
|
-
await h.call('vibe_math_add_problem', { id: 'q1', description: 'off 模式无操作测试' })
|
|
345
|
-
await startScheduler(h)
|
|
346
|
-
await tick(2000)
|
|
347
|
-
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:q1')), 60, 200)
|
|
348
|
-
assert(!!ex, 'the explorer was spawned (the scheduler really is running)')
|
|
349
|
-
assert(!!ex && !/Lean|形式化/.test(ex.prompt || ''), 'the explorer prompt contains no Lean/形式化 text in off mode')
|
|
350
|
-
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```' }] })
|
|
351
|
-
await sleep(300)
|
|
352
|
-
const so = await waitFor(() => h.spawns.find((s) => s.label.startsWith('solver:q1')), 40, 200)
|
|
353
|
-
assert(!!so, 'the solver was spawned after the explorer returned directions')
|
|
354
|
-
assert(!!so && !/Lean|形式化/.test(so.prompt || ''), 'the solver prompt contains no Lean/形式化 text in off mode')
|
|
355
|
-
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```' }] })
|
|
356
|
-
const vs = await verifyWithDebate(h, 'r-q1-s0', 1)
|
|
357
|
-
assert(!!vs.first && vs.first.length >= 2, 'verifiers were spawned for the solution (' + (vs.first ? vs.first.length : 0) + ')')
|
|
358
|
-
const vp = vs.first ? (h.spawns.find((s) => s.label === 'verifier:r-q1-s0:0').prompt || '') : ''
|
|
359
|
-
assert(!/Lean|形式化/.test(vp), 'the review prompt contains no Lean/形式化 text in off mode')
|
|
360
|
-
const debateText = h.followups.map((f) => f.prompt || '').join('\n')
|
|
361
|
-
assert(!/Lean|形式化/.test(debateText), 'no debate/followup prompt mentions Lean in off mode')
|
|
362
|
-
const qs = JSON.parse(readIf(join(projRoot(h), 'qs', 'qs.json')) || '[]')
|
|
363
|
-
const q = qs.find((x) => x.id === 'q1')
|
|
364
|
-
assert(!!q && q.已解决 === true, "'off' still finalizes: the problem is marked solved with NO Lean artifact")
|
|
365
|
-
assert(!!q && q.解法列表 && q.解法列表[0].正确概率 === 1, 'the solution probability is 1 in off mode')
|
|
366
|
-
assert(existsSync(join(projRoot(h), 'Verified', '问题_Verified.json')), 'the Verified card was written in off mode')
|
|
367
|
-
assert(!/形式化/.test(readIf(join(projRoot(h), 'Verified', '问题_Verified.json'))), 'the card carries no formal line in off mode')
|
|
368
|
-
assert(!existsSync(join(projRoot(h), 'Formal', 'TODO.md')), 'no Formal/TODO.md is produced in off mode')
|
|
369
|
-
const fs0 = formalStateOf(h)
|
|
370
|
-
assert(!fs0.records || Object.keys(fs0.records).length === 0, 'off mode records no formal object state')
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ---------- 2. parameter validation + runtime switching ----------
|
|
374
|
-
section('2 parameter validation and runtime switching')
|
|
375
|
-
{
|
|
376
|
-
const h = await makeCase('params')
|
|
377
|
-
const bad = await h.call('vibe_math_set_params', { formalVerify: 'banana' })
|
|
378
|
-
assert(bad.params.formalVerify === 'off', "an unknown mode degrades to 'off', never to a stronger mode (got " + bad.params.formalVerify + ')')
|
|
379
|
-
const enc = await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
380
|
-
assert(enc.params.formalVerify === 'encourage', "'encourage' is accepted")
|
|
381
|
-
const req = await h.call('vibe_math_set_params', { formalVerify: 'require', leanTimeoutMs: -5, leanCommand: ' ' })
|
|
382
|
-
assert(req.params.formalVerify === 'require', "'require' is accepted")
|
|
383
|
-
assert(req.params.leanTimeoutMs === 120000, 'a non-positive leanTimeoutMs falls back to the default (' + req.params.leanTimeoutMs + ')')
|
|
384
|
-
assert(req.params.leanCommand === 'lean', 'a blank leanCommand falls back to "lean"')
|
|
385
|
-
const lake = await h.call('vibe_math_set_params', { leanCommand: 'lake', leanArgs: ['env', 'lean'] })
|
|
386
|
-
assert(lake.params.leanCommand === 'lake' && lake.params.leanArgs.join(' ') === 'env lean', 'leanCommand/leanArgs are settable (lake env lean)')
|
|
387
|
-
const st = await h.call('vibe_math_status', {})
|
|
388
|
-
assert(st.formal.leanCommand === 'lake' && st.formal.leanArgs.join(' ') === 'env lean', 'the Lean knobs round-trip through status')
|
|
389
|
-
const setup = await h.call('vibe_math_setup', {})
|
|
390
|
-
const names = setup.parameters.map((p) => p.name)
|
|
391
|
-
assert(['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs'].every((n) => names.indexOf(n) !== -1), 'vibe_math_setup schema lists all four Lean knobs')
|
|
392
|
-
const fv = setup.parameters.find((p) => p.name === 'formalVerify')
|
|
393
|
-
assert(!!fv && fv.current === 'require' && fv.default === 'off', 'the schema reports the current and default formalVerify')
|
|
394
|
-
const tpl = await h.call('vibe_math_template', { where: 'global' })
|
|
395
|
-
const tplText = readIf(tpl.path)
|
|
396
|
-
assert(/formalVerify/.test(tplText) && /leanCommand/.test(tplText) && /leanTimeoutMs/.test(tplText), 'the settings template documents the Lean knobs')
|
|
397
|
-
assert(/leanArgs/.test(tplText), 'the settings template also documents leanArgs')
|
|
398
|
-
assert(!/:\s*undefined/.test(tplText), 'the template has no bare undefined token (settings round-trip safety)')
|
|
399
|
-
const saved = await h.call('vibe_math_save_settings', {})
|
|
400
|
-
assert(saved.ok === true, 'save_settings still round-trips with the new knobs')
|
|
401
|
-
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
402
|
-
await h.call('vibe_math_add_problem', { id: 'q2', description: '模式切换测试' })
|
|
403
|
-
await startScheduler(h)
|
|
404
|
-
await tick(2000)
|
|
405
|
-
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:q2')), 60, 200)
|
|
406
|
-
assert(!!ex && /【顺手形式化(鼓励)】/.test(ex.prompt || ''), '★ switching to encourage changes the NEXT prompt immediately')
|
|
407
|
-
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```' }] })
|
|
408
|
-
await h.call('vibe_math_set_params', { formalVerify: 'off' })
|
|
409
|
-
await sleep(400)
|
|
410
|
-
const so = await waitFor(() => h.spawns.find((s) => s.label.startsWith('solver:q2')), 40, 200)
|
|
411
|
-
assert(!!so, 'the solver was spawned after the mode was switched back to off')
|
|
412
|
-
assert(!!so && !/形式化|Lean/.test(so.prompt || ''), '★ switching back to off removes the Lean text from the next prompt')
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ---------- 3. 'encourage' injection ----------
|
|
416
|
-
section("3 'encourage' injects the Lean section into review AND debate prompts")
|
|
417
|
-
{
|
|
418
|
-
const h = await makeCase('enc')
|
|
419
|
-
await h.call('vibe_math_set_params', { formalVerify: 'encourage', maxParallelThreshold: 8 })
|
|
420
|
-
// Keep the scheduler alive: v2 STOPS itself when there is no unsolved problem, no agent and
|
|
421
|
-
// no task, and a stopped scheduler never starts a verification (nor spawns an explorer).
|
|
422
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
423
|
-
await startScheduler(h)
|
|
424
|
-
await h.call('vibe_math_add_proposition', { id: 'pEnc', 概述: '鼓励模式下的忠实性审查', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
425
|
-
const vs = await verifyWithDebate(h, 'r-pEnc', 0.5, 1) // 0.5 -> no consensus -> a REAL debate round
|
|
426
|
-
assert(!!vs.first, 'verifiers spawned for the bare proposition')
|
|
427
|
-
assert(vs.rounds >= 2, 'the object really went through a debate round (' + vs.rounds + ' rounds)')
|
|
428
|
-
const review = h.spawns.find((s) => s.label === 'verifier:r-pEnc:0')
|
|
429
|
-
assert(!!review, 'the first review prompt was captured')
|
|
430
|
-
const reviewText = review ? review.prompt : ''
|
|
431
|
-
assert(/【Lean 形式化验证(鼓励模式)】/.test(reviewText), '★ the REVIEW prompt carries the Lean section')
|
|
432
|
-
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(reviewText), 'the review prompt states that a passing Lean run shrinks the question to fidelity')
|
|
433
|
-
assert(/实现难度/.test(reviewText), 'the review prompt asks for the implementation-difficulty judgement')
|
|
434
|
-
// NOTE: this assertion is only the WORDING half. Which is exactly the trap the first version
|
|
435
|
-
// of this feature fell into (a green suite guarding a dead channel). The behaviour — the reply
|
|
436
|
-
// really landing in the durable record — is asserted in section 11.
|
|
437
|
-
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(reviewText), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
438
|
-
assert(/"decision":"used\|blocked\|defect"/.test(reviewText), '★ the review contract lists the real decision enum (incl. defect)')
|
|
439
|
-
assert(/vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库)/.test(reviewText), 'the review prompt names the three v2 tools')
|
|
440
|
-
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')
|
|
441
|
-
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)')
|
|
442
|
-
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
443
|
-
assert(/DEBATE/.test(debate), 'the debate round actually happened (a followup with the debate prompt was issued)')
|
|
444
|
-
assert(/【Lean 形式化验证(鼓励模式)】/.test(debate), '★ the DEBATE prompt carries the Lean section too')
|
|
445
|
-
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(debate), 'the debate prompt states the fidelity shift')
|
|
446
|
-
await h.call('vibe_math_add_problem', { id: 'qW', description: '顺手形式化测试' })
|
|
447
|
-
await tick(2000)
|
|
448
|
-
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:qW')), 60, 200)
|
|
449
|
-
assert(!!ex && /【顺手形式化(鼓励)】/.test(ex.prompt || ''), '★ the explorer work prompt carries the 顺手形式化 line')
|
|
450
|
-
assert(!!ex && /vibe_math_lean_archive kind='def'/.test(ex.prompt || ''), 'the work line points at the archive tool for reusable definitions')
|
|
451
|
-
assert(!!ex && /vibe_math_lean_lib 查重/.test(ex.prompt || ''), 'the work line tells members to check the reuse library first')
|
|
452
|
-
assert(!!ex && /归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。/.test(ex.prompt || ''), '★ the work line forbids archiving a definition that has not run green')
|
|
453
|
-
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)')
|
|
454
|
-
// Every agent-facing string must name the REGISTERED tools: an abbreviated `lean_archive`
|
|
455
|
-
// is not a tool that exists, and agents copy these literals verbatim (contract §6 hard req. 1).
|
|
456
|
-
const workText = (ex.prompt || '') + '\n' + reviewText + '\n' + debate
|
|
457
|
-
assert(!/(^|[^a-z_])lean_(run|archive|lib)/.test(workText), '★ no injected prompt names an abbreviated tool (every occurrence is prefixed)')
|
|
458
|
-
const offHost = await makeCase('enc-off')
|
|
459
|
-
await offHost.call('vibe_math_add_problem', { id: 'qN', description: 'x' })
|
|
460
|
-
await startScheduler(offHost)
|
|
461
|
-
await tick(2000)
|
|
462
|
-
const exN = await waitFor(() => offHost.spawns.find((s) => s.label.startsWith('explorer:qN')), 60, 200)
|
|
463
|
-
assert(!!exN && !/形式化|Lean/.test(exN.prompt || ''), 'the same prompt in off mode is free of Lean text (injection is mode-computed, not frozen)')
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// ---------- 4. the run tool ----------
|
|
467
|
-
section('4 lean_run executes through the subprocess service and reports honestly')
|
|
468
|
-
{
|
|
469
|
-
const h = await makeCase('run')
|
|
470
|
-
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
471
|
-
const proj = projRoot(h)
|
|
472
|
-
mkdirSync(join(proj, 'Formal'), { recursive: true })
|
|
473
|
-
writeFileSync(join(proj, 'Formal', 'good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
474
|
-
writeFileSync(join(proj, 'Formal', 'bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
|
|
475
|
-
const before = leanRuns.length
|
|
476
|
-
const runGood = await h.call('vibe_math_lean_run', { file: 'Formal/good.lean', target: 'pRun' })
|
|
477
|
-
assert(runGood.ok === true && runGood.exitCode === 0, 'a file with no sorry runs green (' + JSON.stringify({ ok: runGood.ok, exitCode: runGood.exitCode }) + ')')
|
|
478
|
-
assert(leanRuns.length > before, 'the run really went through the mocked subprocess service')
|
|
479
|
-
const spec = leanRuns[leanRuns.length - 1]
|
|
480
|
-
assert(spec.cwd.replace(/\\/g, '/') === proj.replace(/\\/g, '/'), 'the toolchain runs with the PROJECT root as cwd (got ' + spec.cwd + ')')
|
|
481
|
-
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')
|
|
482
|
-
assert(typeof spec.graceMs === 'number' && spec.graceMs >= 1000, 'the configured timeout is passed as graceMs (' + spec.graceMs + ')')
|
|
483
|
-
assert(typeof runGood.command === 'string' && runGood.command.indexOf('good.lean') !== -1, 'the result reports the exact command line')
|
|
484
|
-
const runBad = await h.call('vibe_math_lean_run', { file: 'Formal/bad.lean' })
|
|
485
|
-
assert(runBad.ok === false && runBad.exitCode === 1, 'a file that still uses sorry reports a red run')
|
|
486
|
-
assert(/sorry/.test(runBad.stderr), 'the compiler output is returned verbatim (' + JSON.stringify(runBad.stderr).slice(0, 60) + ')')
|
|
487
|
-
assert(/修复后重跑/.test(runBad.hint || ''), 'a red run tells the member to fix and rerun')
|
|
488
|
-
const runMissing = await h.call('vibe_math_lean_run', { file: 'Formal/nope.lean' })
|
|
489
|
-
assert(runMissing.ok === false && runMissing.code === 'V2_NOT_FOUND', 'a missing file is refused with a typed code (' + runMissing.code + ')')
|
|
490
|
-
const runNoFile = await h.call('vibe_math_lean_run', {})
|
|
491
|
-
assert(runNoFile.ok === false && runNoFile.code === 'V2_INVALID_ARGUMENT', 'a run without `file` is refused')
|
|
492
|
-
// The guard's boundary is the VibeMath ROOT, not the project: the global reuse library
|
|
493
|
-
// deliberately lives at <VibeMath>/Formal/{Lib,Proved}, so climbing out of the project but
|
|
494
|
-
// staying inside VibeMath is legal (it just fails as a missing file).
|
|
495
|
-
const runOutOfProject = await h.call('vibe_math_lean_run', { file: '../../Formal/Lib/x.lean' })
|
|
496
|
-
assert(runOutOfProject.ok === false && runOutOfProject.code === 'V2_NOT_FOUND',
|
|
497
|
-
'climbing out of the project but staying inside the VibeMath root is ALLOWED (got ' + runOutOfProject.code + ')')
|
|
498
|
-
const runEscape = await h.call('vibe_math_lean_run', { file: '../../../../etc/evil.lean' })
|
|
499
|
-
assert(runEscape.ok === false && runEscape.code === 'V2_INVALID_ARGUMENT', '★ a traversal that climbs ABOVE the VibeMath root is refused')
|
|
500
|
-
const runEscape2 = await h.call('vibe_math_lean_run', { file: 'Formal/../../../../../evil.lean' })
|
|
501
|
-
assert(runEscape2.ok === false && runEscape2.code === 'V2_INVALID_ARGUMENT', 'a deeper traversal is refused too')
|
|
502
|
-
const runEscape3 = await h.call('vibe_math_lean_run', { file: '/etc/evil.lean' })
|
|
503
|
-
assert(runEscape3.ok === false && runEscape3.code === 'V2_INVALID_ARGUMENT', 'an unrelated absolute path is refused')
|
|
504
|
-
const runEscape4 = await h.call('vibe_math_lean_run', { file: 'C:/Windows/evil.lean' })
|
|
505
|
-
assert(runEscape4.ok === false && runEscape4.code === 'V2_INVALID_ARGUMENT', 'an unrelated Windows absolute path is refused')
|
|
506
|
-
const runNotLean = await h.call('vibe_math_lean_run', { file: 'Formal/good.txt' })
|
|
507
|
-
assert(runNotLean.ok === false && runNotLean.code === 'V2_INVALID_ARGUMENT', 'only .lean files can be executed')
|
|
508
|
-
assert(/VibeMath/.test(runEscape.message || ''), 'the refusal names the boundary it enforced')
|
|
509
|
-
mkdirSync(join(vibeRoot(h), 'Formal', 'Lib'), { recursive: true })
|
|
510
|
-
writeFileSync(join(vibeRoot(h), 'Formal', 'Lib', 'abs.lean'), 'def absTest := 1\n', 'utf8')
|
|
511
|
-
const runAbs = await h.call('vibe_math_lean_run', { file: join(vibeRoot(h), 'Formal', 'Lib', 'abs.lean').replace(/\\/g, '/') })
|
|
512
|
-
assert(runAbs.ok === true, 'an absolute path inside the VibeMath root is accepted')
|
|
513
|
-
const rec = formalStateOf(h)
|
|
514
|
-
assert(!!rec.records && !!rec.records.pRun && rec.records.pRun.status === 'attempted', 'a green lean_run records the object as attempted (stored, not promoted)')
|
|
515
|
-
assert(!!rec.records.pRun.run && rec.records.pRun.run.ok === true && rec.records.pRun.run.exitCode === 0, 'the run record keeps ok/exitCode')
|
|
516
|
-
assert(rec.records.pRun.run.stdoutTail !== undefined && rec.records.pRun.run.stderrTail !== undefined, 'the run record keeps truncated output tails')
|
|
517
|
-
toolchainAvailable = false
|
|
518
|
-
const runNoTc = await h.call('vibe_math_lean_run', { file: 'Formal/good.lean' })
|
|
519
|
-
assert(runNoTc.ok === false && runNoTc.code === 'LEAN_NOT_FOUND', 'a missing toolchain returns LEAN_NOT_FOUND instead of crashing')
|
|
520
|
-
assert(/仍可把形式化代码写下来归档/.test(runNoTc.message), 'the failure explains the graceful degradation')
|
|
521
|
-
toolchainAvailable = true
|
|
522
|
-
const noSub = await makeCase('nosub', { noSubprocess: true })
|
|
523
|
-
mkdirSync(join(projRoot(noSub), 'Formal'), { recursive: true })
|
|
524
|
-
writeFileSync(join(projRoot(noSub), 'Formal', 'x.lean'), 'theorem x : 1 = 1 := rfl\n', 'utf8')
|
|
525
|
-
const noSubRun = await noSub.call('vibe_math_lean_run', { file: 'Formal/x.lean' })
|
|
526
|
-
assert(noSubRun.ok === false && noSubRun.code === 'NO_SUBPROCESS', 'a host without the subprocess service returns NO_SUBPROCESS and does not crash (got ' + noSubRun.code + ')')
|
|
527
|
-
const noSubStatus = await noSub.call('vibe_math_status', {})
|
|
528
|
-
assert(noSubStatus.ok === true, 'the scheduler still answers status after that (nothing was thrown into the loop)')
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// ---------- 4b. the timeout must STOP the process, not just report it ----------
|
|
532
|
-
// Contract §7: "必须给 cwd…,并对超时调用 handle.terminate()". Reporting LEAN_TIMEOUT while the Lean
|
|
533
|
-
// process keeps running is a silent resource leak, and the framework's own contract says otherwise.
|
|
534
|
-
section('4b lean_run ACTIVELY terminates on timeout (contract §7)')
|
|
535
|
-
{
|
|
536
|
-
const h = await makeCase('timeout')
|
|
537
|
-
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
538
|
-
const proj = projRoot(h)
|
|
539
|
-
mkdirSync(join(proj, 'Formal'), { recursive: true })
|
|
540
|
-
writeFileSync(join(proj, 'Formal', 'hang.lean'), 'theorem t : 1 = 1 := rfl -- HANG\n', 'utf8')
|
|
541
|
-
writeFileSync(join(proj, 'Formal', 'fast.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
542
|
-
const before = terminations.length
|
|
543
|
-
const t0 = Date.now()
|
|
544
|
-
const run = await h.call('vibe_math_lean_run', { file: 'Formal/hang.lean', timeout_ms: 1000 })
|
|
545
|
-
const elapsed = Date.now() - t0
|
|
546
|
-
assert(run.ok === false && run.code === 'LEAN_TIMEOUT', '★ a run that outlives its timeout is reported as LEAN_TIMEOUT (got ' + run.code + ')')
|
|
547
|
-
assert(run.timedOut === true, 'the result carries timedOut=true')
|
|
548
|
-
assert(terminations.length === before + 1 && /hang\.lean/.test(terminations[terminations.length - 1].file),
|
|
549
|
-
'★★ the timeout ACTIVELY called handle.terminate() (graceMs alone does not stop a lingering Lean process)')
|
|
550
|
-
assert(elapsed < 2400, 'the call returned AT its timeout instead of waiting the process out (took ' + elapsed + 'ms)')
|
|
551
|
-
const fast = await h.call('vibe_math_lean_run', { file: 'Formal/fast.lean' })
|
|
552
|
-
assert(fast.ok === true && fast.timedOut === false, 'a normal run still reports success')
|
|
553
|
-
assert(terminations.length === before + 1, 'a normal run terminates nothing')
|
|
554
|
-
await sleep(60)
|
|
555
|
-
assert(liveTimers.size === 0, '★ no timeout timer is left pending after either run (it is cleared as soon as `done` wins)')
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// ---------- 5. archive: def / lemma / proof / blocked ----------
|
|
559
|
-
section('5 lean_archive writes the contract paths and indexes')
|
|
560
|
-
{
|
|
561
|
-
const h = await makeCase('arc')
|
|
562
|
-
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
563
|
-
const proj = projRoot(h)
|
|
564
|
-
const libPath = join(vibeRoot(h), 'Formal', 'Lib')
|
|
565
|
-
const provedPath = join(vibeRoot(h), 'Formal', 'Proved')
|
|
566
|
-
const defRes = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'ZMod5', content: 'def ZMod5 := Fin 5\n' })
|
|
567
|
-
assert(defRes.ok === true && defRes.file === 'Formal/Lib/ZMod5.lean', 'a reusable definition is archived to the global lib (' + defRes.file + ')')
|
|
568
|
-
assert(existsSync(join(libPath, 'ZMod5.lean')), '★ the definition exists under <VibeMath>/Formal/Lib/ (cross-project, NOT inside the project)')
|
|
569
|
-
assert(!existsSync(join(proj, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the project tree')
|
|
570
|
-
assert(!!defRes.run && defRes.run.ok === true, 'the archived definition was executed (kind=def runs by default)')
|
|
571
|
-
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' })
|
|
572
|
-
assert(lemmaRes.ok === true && lemmaRes.file === 'Formal/Proved/sq_odd.lean', 'a lemma is archived to the global Proved/ (' + lemmaRes.file + ')')
|
|
573
|
-
assert(existsSync(join(provedPath, 'sq_odd.lean')), 'the lemma exists under <VibeMath>/Formal/Proved/')
|
|
574
|
-
const defNoRun = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'quiet', content: 'def quiet := 2\n', run: false })
|
|
575
|
-
assert(defNoRun.ok === true && defNoRun.run === undefined, 'run:false archives without executing (still indexed)')
|
|
576
|
-
assert(/ZMod5/.test(readIf(join(libPath, 'Index.md'))), 'Lib/Index.md lists the new definition')
|
|
577
|
-
assert(/quiet/.test(readIf(join(libPath, 'Index.md'))), 'Lib/Index.md lists the run:false definition too')
|
|
578
|
-
assert(/sq_odd/.test(readIf(join(provedPath, 'Index.md'))), 'Proved/Index.md lists the new lemma')
|
|
579
|
-
writeFileSync(join(proj, 'Formal', 'src.lean'), 'def copied := 3\n', 'utf8')
|
|
580
|
-
const defFrom = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'copied', from: 'Formal/src.lean' })
|
|
581
|
-
assert(defFrom.ok === true && existsSync(join(libPath, 'copied.lean')), 'kind=def can archive from an existing .lean file')
|
|
582
|
-
// The tool RESULT is agent-facing text too: a definition whose run just failed must not be advertised
|
|
583
|
-
// as "directly importable" (contract §6 hard req. 3 / 实现方案 §9.4 — a red file must not be presented
|
|
584
|
-
// as usable, whatever the framework decides to do with the file itself).
|
|
585
|
-
const defRed = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'polluted', content: 'def polluted := 1 -- FAIL\n' })
|
|
586
|
-
assert(defRed.ok === true && !!defRed.run && defRed.run.ok === false, 'a definition that does not compile is reported as a red run')
|
|
587
|
-
assert(!/可直接 import 复用/.test(defRed.note || ''), '★ a red definition must NOT be advertised as directly reusable')
|
|
588
|
-
assert(/运行未通过/.test(defRed.note || ''), '…and the note says what to do instead')
|
|
589
|
-
const fromOutside = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'escape', from: '../../../../etc/passwd' })
|
|
590
|
-
assert(fromOutside.ok === false && fromOutside.code === 'V2_INVALID_ARGUMENT', 'from=<path outside the VibeMath root> is refused')
|
|
591
|
-
const noName = await h.call('vibe_math_lean_archive', { kind: 'def', content: 'def x := 1\n' })
|
|
592
|
-
assert(noName.ok === false && noName.code === 'V2_INVALID_ARGUMENT', 'archiving a definition without a name is refused')
|
|
593
|
-
const noBody = await h.call('vibe_math_lean_archive', { kind: 'lemma', name: 'xb' })
|
|
594
|
-
assert(noBody.ok === false && noBody.code === 'V2_INVALID_ARGUMENT', 'archiving a lemma with neither content nor from is refused')
|
|
595
|
-
const badKind = await h.call('vibe_math_lean_archive', { kind: 'nonsense' })
|
|
596
|
-
assert(badKind.ok === false && badKind.code === 'V2_INVALID_ARGUMENT', 'an unknown archive kind is refused')
|
|
597
|
-
const noTarget = await h.call('vibe_math_lean_archive', { kind: 'proof', content: 'theorem x : 1 = 1 := rfl\n' })
|
|
598
|
-
assert(noTarget.ok === false && noTarget.code === 'V2_INVALID_ARGUMENT', 'kind=proof without a target is refused')
|
|
599
|
-
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' })
|
|
600
|
-
assert(proof.ok === true && proof.passed === true, 'a passing proof is archived (' + JSON.stringify({ ok: proof.ok, passed: proof.passed }) + ')')
|
|
601
|
-
assert(proof.file === 'Formal/pProof.lean', 'the working file is Formal/<target>.lean')
|
|
602
|
-
assert(proof.proof === 'Verified/Lean/pProof.lean', 'the archived proof path is Verified/Lean/<target>.lean')
|
|
603
|
-
assert(existsSync(join(proj, 'Formal', 'pProof.lean')), 'the working file exists on disk')
|
|
604
|
-
assert(existsSync(join(proj, 'Verified', 'Lean', 'pProof.lean')), '★ the proof is archived under Verified/Lean/')
|
|
605
|
-
assert(proof.status === 'passed', 'the object status becomes passed')
|
|
606
|
-
const proofBad = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pRed', content: 'theorem p_red : 1 = 2 := by sorry\n' })
|
|
607
|
-
assert(proofBad.ok === true && proofBad.passed === false && proofBad.status === 'attempted', 'a red proof is recorded as attempted (not passed)')
|
|
608
|
-
assert(!existsSync(join(proj, 'Verified', 'Lean', 'pRed.lean')), 'a red proof is NOT archived under Verified/Lean/')
|
|
609
|
-
assert(/sorry/.test((proofBad.run && proofBad.run.stderr) || ''), 'the red run keeps the compiler output for the member to fix')
|
|
610
|
-
const st = await h.call('vibe_math_status', {})
|
|
611
|
-
assert(st.formal.objects.some((o) => o.target === 'pProof' && o.status === 'passed'), 'status lists the passed object')
|
|
612
|
-
const idx = readIf(join(proj, 'Formal', 'Index.md'))
|
|
613
|
-
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')
|
|
614
|
-
assert(/pRed/.test(idx) && /attempted/.test(idx), 'Formal/Index.md also records the failed attempt')
|
|
615
|
-
const blkNoNote = await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'pBlk' })
|
|
616
|
-
assert(blkNoNote.ok === false && blkNoNote.code === 'V2_INVALID_ARGUMENT', 'blocked without a note is refused')
|
|
617
|
-
const blk = await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'pBlk', note: '需要外层解析数论框架,本轮工作量不可接受' })
|
|
618
|
-
assert(blk.ok === true && blk.status === 'blocked', 'a reasoned blocker is recorded')
|
|
619
|
-
assert(/需要外层解析数论框架/.test(readIf(join(proj, 'Formal', 'Index.md'))), 'the blocker reason reaches the index')
|
|
620
|
-
const stBlk = await h.call('vibe_math_status', {})
|
|
621
|
-
assert(stBlk.formal.objects.some((o) => o.target === 'pBlk' && o.status === 'blocked'), 'status lists the blocked object')
|
|
622
|
-
const lib = await h.call('vibe_math_lean_lib', {})
|
|
623
|
-
assert(lib.ok === true && lib.counts.lib >= 3 && lib.counts.proved >= 1, 'lean_lib reports the reuse library sizes (' + JSON.stringify(lib.counts) + ')')
|
|
624
|
-
assert(lib.objects.some((o) => o.target === 'pProof' && o.status === 'passed'), 'lean_lib lists the per-object formal status')
|
|
625
|
-
assert(/复用优先/.test(lib.hint || ''), 'lean_lib tells agents to reuse before redefining')
|
|
626
|
-
assert(lib.mode === 'encourage', 'lean_lib reports the active mode')
|
|
627
|
-
const noRefresh = await h.call('vibe_math_lean_lib', { refresh: false })
|
|
628
|
-
assert(noRefresh.rebuilt === false && noRefresh.counts.lib === null, 'refresh:false lists without rebuilding')
|
|
629
|
-
assert(existsSync(join(proj, 'Formal', 'Index.md')), 'the project Formal/Index.md exists')
|
|
630
|
-
assert(existsSync(join(libPath, 'Index.md')) && existsSync(join(provedPath, 'Index.md')), 'the two GLOBAL indexes exist')
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// ---------- 6. a passing proof flips the review to FIDELITY ----------
|
|
634
|
-
section('6 a passing proof flips the review subject to fidelity')
|
|
635
|
-
{
|
|
636
|
-
const h = await makeCase('fid')
|
|
637
|
-
await h.call('vibe_math_set_params', { formalVerify: 'encourage', maxParallelThreshold: 8 })
|
|
638
|
-
// v2's scheduler STOPS itself when there is no unsolved problem, no agent and no task; a
|
|
639
|
-
// case that only wants to verify a proposition therefore needs one live problem to keep
|
|
640
|
-
// the tick loop alive (otherwise no verification would ever be started).
|
|
641
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
642
|
-
await startScheduler(h)
|
|
643
|
-
await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'r-pFid', content: 'theorem p_fid : 2 + 2 = 4 := by decide\n' })
|
|
644
|
-
await h.call('vibe_math_add_proposition', { id: 'pFid', 概述: '2+2=4(已有 Lean 证明)', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
645
|
-
// 0.5 in round 1 forces a debate round whose prompt must ALSO carry the fidelity wording
|
|
646
|
-
const vs = await verifyWithDebate(h, 'r-pFid', 0.5, 1)
|
|
647
|
-
assert(!!vs.first, 'a verifier was spawned for the Lean-passed object')
|
|
648
|
-
const vp = h.spawns.find((s) => s.label === 'verifier:r-pFid:0')
|
|
649
|
-
const vpText = vp ? vp.prompt : ''
|
|
650
|
-
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(vpText), 'the review prompt announces the passing proof')
|
|
651
|
-
assert(/你不需要重新检查推导/.test(vpText), '★ it tells reviewers NOT to re-derive')
|
|
652
|
-
assert(/忠实性审查/.test(vpText), '★ it tells reviewers the review subject is now fidelity')
|
|
653
|
-
assert(/定义 \/ 对象 \/ 条件 \/ 假设 \/ 结论是否与命题原文\*\*完全一致\*\*/.test(vpText), 'it enumerates exactly what fidelity means')
|
|
654
|
-
assert(/Verified\/Lean\/r-pFid\.lean/.test(vpText), 'it points at the archived proof')
|
|
655
|
-
assert(/一致 → Result = 1/.test(vpText), '★ the fidelity guidance names the REAL field (v2\'s contract field is Result)')
|
|
656
|
-
assert(!/verdict/.test(vpText), '★ the fidelity guidance never names a `verdict` field (that vote would be silently dropped)')
|
|
657
|
-
assert(/发现任何偏差,不要投 0/.test(vpText), '★ a fidelity defect is explicitly NOT to be voted as 0 (it is not a refutation)')
|
|
658
|
-
assert(/formal:\{decision:'defect'/.test(vpText), '★ the reviewers are given the defect reply channel that withdraws the proof')
|
|
659
|
-
assert(!/偏离 → 0/.test(vpText), '★ the "any deviation → 0" instruction is gone (it would fabricate a false conclusion)')
|
|
660
|
-
assert(!/请先判断该对象的\*\*实现难度\*\*/.test(vpText), 'the "judge the difficulty first" wording is gone when a proof already exists')
|
|
661
|
-
// The withdrawal sentence must match the MODE's real strength (contract §4.1 pt.3 / §6.1; this case is
|
|
662
|
-
// 'encourage'): encouraging mode has NO gate, so the framework cannot hold the verdict — promising a
|
|
663
|
-
// hold there is a lie the voter would rely on (AUDIT-CHECKLIST §1.7, "提示词承诺的强度档位").
|
|
664
|
-
assert(/本档没有门禁/.test(vpText), '★ encourage fidelity text says THIS MODE HAS NO GATE (the framework cannot hold the verdict)')
|
|
665
|
-
assert(!/本次裁定\*\*不定论\*\*/.test(vpText), '★ encourage must NOT promise a hold the framework cannot enforce')
|
|
666
|
-
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
667
|
-
assert(/你不需要重新检查推导/.test(debate), '★ the debate prompt for a Lean-passed object also asks for fidelity, not re-derivation')
|
|
668
|
-
assert(/发现任何偏差,不要投 0/.test(debate), '★ and it carries the same no-zero rule in the debate round')
|
|
669
|
-
assert(/本档没有门禁/.test(debate), '★ the debate round carries the same mode-qualified withdrawal wording')
|
|
670
|
-
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'r-pBlk2', note: '涉及未形式化的分析学前置' })
|
|
671
|
-
await h.call('vibe_math_add_proposition', { id: 'pBlk2', 概述: '已记录阻塞的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
672
|
-
const vs2 = await verifyWithDebate(h, 'r-pBlk2', 0.5, 1)
|
|
673
|
-
assert(!!vs2.first, 'verifiers spawned for the blocked object')
|
|
674
|
-
const vp2 = h.spawns.find((s) => s.label === 'verifier:r-pBlk2:0')
|
|
675
|
-
const vp2Text = vp2 ? vp2.prompt : ''
|
|
676
|
-
assert(/该对象已被记录为\*\*形式化阻塞\*\*/.test(vp2Text), 'a blocked object is announced as such')
|
|
677
|
-
assert(/涉及未形式化的分析学前置/.test(vp2Text), 'the blocker reason is shown to the reviewers')
|
|
678
|
-
assert(/这个阻塞判断是否成立/.test(vp2Text), 'the verdict guidance asks whether the blocker is justified')
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// ---------- 7. the 'require' gate (proposition) ----------
|
|
682
|
-
section("7 'require' withholds a verdict until the formal record exists")
|
|
683
|
-
{
|
|
684
|
-
const h = await makeCase('gate')
|
|
685
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
686
|
-
// a live problem keeps the scheduler from stopping itself (see the comment in case 6)
|
|
687
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
688
|
-
await startScheduler(h)
|
|
689
|
-
const proj = projRoot(h)
|
|
690
|
-
await h.call('vibe_math_add_proposition', { id: 'pGate', 概述: '必须形式化的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
691
|
-
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pGate'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
692
|
-
assert(!!vs, 'verifiers were spawned under require mode')
|
|
693
|
-
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pGate:0') || {}).prompt || ''
|
|
694
|
-
assert(/【Lean 形式化验证(强制模式)】/.test(vp), 'the review prompt says 强制模式')
|
|
695
|
-
assert(/必须产出 Lean 形式化/.test(vp), "'require' states the formalization is mandatory")
|
|
696
|
-
assert(/本次裁定不会生效/.test(vp), 'the prompt warns that the verdict will not take effect without it')
|
|
697
|
-
assert(/formal-required/.test(vp), 'the prompt names the machine-readable reason')
|
|
698
|
-
assert(/vibe_math_lean_archive kind='blocked' note=… 或回执 formal\.note/.test(vp), '★ the require wording names BOTH blocking routes with the FULL tool name')
|
|
699
|
-
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')
|
|
700
|
-
assert(!/(^|[^a-z_])lean_(run|archive|lib)/.test(vp), '★ the require prompt contains no abbreviated tool name either')
|
|
701
|
-
fireVerdicts(h, vs, 1)
|
|
702
|
-
// The deferral writes Formal/TODO.md from INSIDE the settle path; waiting for that file is
|
|
703
|
-
// the observable proof that the round settled (the task is dropped right after).
|
|
704
|
-
const todoSettled = await waitFor(() => /pGate/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 40, 150)
|
|
705
|
-
assert(!!todoSettled, 'the deferred round settled (Formal/TODO.md recorded the object)')
|
|
706
|
-
await tick(600)
|
|
707
|
-
const propFile = join(proj, 'Propos', '数论_Propos.json')
|
|
708
|
-
const props = JSON.parse(readIf(propFile) || '[]')
|
|
709
|
-
const pGate = props.find((x) => x.id === 'pGate') || {}
|
|
710
|
-
assert(pGate.布尔估计 === 0.5, "★ the verdict did NOT take effect: the proposition's 布尔估计 is unchanged (got " + pGate.布尔估计 + ')')
|
|
711
|
-
assert(!(pGate.证明列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 proof entry was recorded')
|
|
712
|
-
assert(pGate.优先级 !== 'never', 'the priority was not pinned to never')
|
|
713
|
-
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), '★ no Verified card was written')
|
|
714
|
-
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
715
|
-
assert(existsSync(join(proj, 'Formal', 'TODO.md')), 'Formal/TODO.md was created')
|
|
716
|
-
assert(/pGate/.test(todo) && /formal-required/.test(todo), '★ the object is on the formalization TODO with the machine-readable reason')
|
|
717
|
-
const recAfterGate = formalStateOf(h)
|
|
718
|
-
// record key = the verification object id (rId), which is what lean_archive target= also uses
|
|
719
|
-
assert(!!recAfterGate.records && !!recAfterGate.records['r-pGate'] && !!recAfterGate.records['r-pGate'].deferredAt, 'the deferral is persisted in the durable formal state (survives resume)')
|
|
720
|
-
assert(!!recAfterGate.records['r-pGate'] && recAfterGate.records['r-pGate'].status === 'none', 'the object stays at status none (nothing was formalized)')
|
|
721
|
-
const vlogs = existsSync(join(proj, 'Verification_logs')) ? readdirSync(join(proj, 'Verification_logs')) : []
|
|
722
|
-
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')
|
|
723
|
-
const st1 = await h.call('vibe_math_status', {})
|
|
724
|
-
assert(/require 模式搁置/.test(JSON.stringify(st1.recentActivity)), 'the withholding is announced on v2-readable channel (activity log; v2 has no Shared/Chat/)')
|
|
725
|
-
assert(st1.formal.todo.some((t) => t.id === 'r-pGate'), 'status exposes the formalization TODO')
|
|
726
|
-
const rep = await h.call('vibe_math_report', {})
|
|
727
|
-
assert(!!rep.formal && rep.formal.required === true && rep.formal.mode === 'require', 'the report exposes the formal mode and gate flag')
|
|
728
|
-
assert(rep.recentActivity.some((a) => /formal-gate/.test(a.event)), 'the deferral also lands in the report log')
|
|
729
|
-
// The deferral leaves this project with no agent, task or solved problem, and v2's strict
|
|
730
|
-
// termination stops the scheduler there — so a case that wants to RE-verify the object after
|
|
731
|
-
// formalizing it must (re)start the scheduler, exactly as a human would.
|
|
732
|
-
await startScheduler(h)
|
|
733
|
-
const proofNow = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pGate', content: 'theorem p_gate : 2 + 2 = 4 := by decide\n' })
|
|
734
|
-
assert(proofNow.ok === true && proofNow.passed === true, 'the object is now Lean-passed')
|
|
735
|
-
const spawned2 = await verifyWithDebate(h, 'r-pGate', 1, 0)
|
|
736
|
-
assert(!!spawned2.first, 'the object was re-verified after the proof was archived')
|
|
737
|
-
const props2 = JSON.parse(readIf(propFile) || '[]')
|
|
738
|
-
const pGateNow = props2.find((x) => x.id === 'pGate') || {}
|
|
739
|
-
assert(pGateNow.布尔估计 === 1, '★ with a passing Lean artifact the same unanimous verdict DOES take effect (布尔估计=1)')
|
|
740
|
-
const cardFile = join(proj, 'Verified', '数论_Verified.json')
|
|
741
|
-
const cards = JSON.parse(readIf(cardFile) || '[]')
|
|
742
|
-
const card = cards.find((c) => c.id === 'pGate')
|
|
743
|
-
assert(!!card, '★ the Verified card was written after the proof passed')
|
|
744
|
-
assert(!!card && /Lean 通过/.test(card['形式化'] || ''), '★ the card records how strong the result is (Lean 通过)')
|
|
745
|
-
assert(!!card && /Verified\/Lean\/pGate\.lean/.test(card['形式化'] || ''), 'the card points at the archived proof')
|
|
746
|
-
await h.call('vibe_math_add_proposition', { id: 'pBlkOk', 概述: '记录阻塞后可定论', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
747
|
-
const sp3 = await waitFor(() => { const x = verifiersOf(h, 'r-pBlkOk'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
748
|
-
assert(!!sp3, 'verifiers spawned for the blocker-escape case')
|
|
749
|
-
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'r-pBlkOk', note: '命题涉及未形式化的分析学,本轮不做' })
|
|
750
|
-
fireVerdicts(h, sp3 || [], 1)
|
|
751
|
-
await waitFor(() => { const c = JSON.parse(readIf(cardFile) || '[]').find((x) => x.id === 'pBlkOk'); return c ? c : undefined }, 40, 150)
|
|
752
|
-
await tick(400)
|
|
753
|
-
const cards3 = JSON.parse(readIf(cardFile) || '[]')
|
|
754
|
-
const card3 = cards3.find((c) => c.id === 'pBlkOk')
|
|
755
|
-
assert(!!card3, '★ an explicit reasoned blocker also lets the verdict through (decide by difficulty, but decide out loud)')
|
|
756
|
-
assert(!!card3 && /阻塞(/.test(card3['形式化'] || ''), 'the card records the blocker')
|
|
757
|
-
assert(!!card3 && /未形式化的分析学/.test(card3['形式化'] || ''), 'the card records the blocker REASON, not just the fact')
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// ---------- 8. the gate covers the qs (problem) choke point ----------
|
|
761
|
-
section('8 the gate also covers a problem-solution verdict')
|
|
762
|
-
{
|
|
763
|
-
const h = await makeCase('gq')
|
|
764
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
765
|
-
// a live problem keeps the scheduler from stopping itself (see the comment in case 3)
|
|
766
|
-
await h.call('vibe_math_add_problem', { id: 'qStay', description: '保持调度器运行的占位问题', priority: 9 })
|
|
767
|
-
await startScheduler(h)
|
|
768
|
-
const proj = projRoot(h)
|
|
769
|
-
await h.call('vibe_math_add_problem', { id: 'qG', description: '带解法的 require 门禁问题', priority: 1 })
|
|
770
|
-
const qsFile = join(proj, 'qs', 'qs.json')
|
|
771
|
-
const qs0 = JSON.parse(readIf(qsFile) || '[]')
|
|
772
|
-
qs0.find((q) => q.id === 'qG').解法列表 = [{ 完整解法: 'closing argument', 正确概率: 0.8, 已验: false }]
|
|
773
|
-
writeFileSync(qsFile, JSON.stringify(qs0, null, 2), 'utf8')
|
|
774
|
-
await tick(2000)
|
|
775
|
-
const sp = await waitFor(() => { const x = verifiersOf(h, 'r-qG-s0'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
776
|
-
assert(!!sp, 'verifiers spawned for the problem solution')
|
|
777
|
-
fireVerdicts(h, sp || [], 1)
|
|
778
|
-
const todoG = await waitFor(() => /qG/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 40, 150)
|
|
779
|
-
assert(!!todoG, 'the deferred problem round settled (Formal/TODO.md recorded it)')
|
|
780
|
-
await tick(600)
|
|
781
|
-
const qs1 = JSON.parse(readIf(qsFile) || '[]')
|
|
782
|
-
const qG = qs1.find((q) => q.id === 'qG')
|
|
783
|
-
assert(!!qG && qG.已解决 !== true, '★ the problem was NOT marked solved (the require gate blocked the promotion)')
|
|
784
|
-
assert(!!qG && qG.解法列表[0].正确概率 !== 1, "the solution's probability was left as it was")
|
|
785
|
-
assert(!existsSync(join(proj, 'Verified', '问题_Verified.json')), 'no problem Verified card was written')
|
|
786
|
-
assert(/qG/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 'the problem is on the formalization TODO')
|
|
787
|
-
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'qG', note: '问题的形式化超出本轮工作量' })
|
|
788
|
-
await startScheduler(h) // see case 7: the deferral may have stopped the scheduler
|
|
789
|
-
const sp2 = await verifyWithDebate(h, 'r-qG-s0', 1, 0)
|
|
790
|
-
assert(!!sp2.first, 'the problem solution was re-verified after the blocker was recorded')
|
|
791
|
-
const qs2 = JSON.parse(readIf(qsFile) || '[]')
|
|
792
|
-
const qG2 = qs2.find((q) => q.id === 'qG')
|
|
793
|
-
assert(!!qG2 && qG2.已解决 === true, 'with a reasoned blocker the problem DOES close')
|
|
794
|
-
assert(!!qG2 && qG2.解法列表[0].正确概率 === 1, 'the solution probability becomes 1 after the gate opened')
|
|
795
|
-
const cards = JSON.parse(readIf(join(proj, 'Verified', '问题_Verified.json')) || '[]')
|
|
796
|
-
const card = cards.find((c) => c.id === 'qG')
|
|
797
|
-
assert(!!card && /阻塞(/.test(card['形式化'] || ''), 'the problem card records the formal status')
|
|
798
|
-
assert(!existsSync(join(vibeRoot(h), 'Formal', 'Lib', 'qG.lean')), 'a blocked record does not fabricate a library file')
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// ---------- 9. persistence across resume ----------
|
|
802
|
-
section('9 the formal state survives resume (v2 has no session projection)')
|
|
803
|
-
{
|
|
804
|
-
const h = await makeCase('persist')
|
|
805
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require' })
|
|
806
|
-
await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pPersist', content: 'theorem p_persist : 1 + 1 = 2 := by decide\n' })
|
|
807
|
-
const before = await h.call('vibe_math_status', {})
|
|
808
|
-
assert(before.formal.objects.some((o) => o.target === 'pPersist' && o.status === 'passed'), 'the object is passed before the restart')
|
|
809
|
-
const proj = projRoot(h)
|
|
810
|
-
writeFileSync(join(proj, 'VibeMath_State', 'process_epoch.json'), JSON.stringify('OLD-PROCESS-EPOCH'), 'utf8')
|
|
811
|
-
const res = await h.call('vibe_math_resume', {})
|
|
812
|
-
assert(res.ok === true, 'resume succeeded')
|
|
813
|
-
const after = await h.call('vibe_math_status', {})
|
|
814
|
-
assert(after.formal.objects.some((o) => o.target === 'pPersist' && o.status === 'passed'),
|
|
815
|
-
'★ the Lean-passed object is still passed after resume (the gate does not forget)')
|
|
816
|
-
assert(/pPersist/.test(readIf(join(proj, 'Formal', 'Index.md'))), 'Formal/Index.md still indexes it (the files are the readable mirror)')
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// ---------- 10. the gate is a no-op unless require ----------
|
|
820
|
-
section("10 'encourage' never gates (a verdict still lands with no Lean artifact)")
|
|
821
|
-
{
|
|
822
|
-
const h = await makeCase('nogate')
|
|
823
|
-
await h.call('vibe_math_set_params', { formalVerify: 'encourage', maxParallelThreshold: 8 })
|
|
824
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
825
|
-
await startScheduler(h)
|
|
826
|
-
const proj = projRoot(h)
|
|
827
|
-
await h.call('vibe_math_add_proposition', { id: 'pFree', 概述: '鼓励模式不设门禁', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
828
|
-
const vs = await verifyWithDebate(h, 'r-pFree', 1, 0)
|
|
829
|
-
assert(!!vs.first, 'verifiers spawned under encourage mode')
|
|
830
|
-
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
831
|
-
const p = props.find((x) => x.id === 'pFree') || {}
|
|
832
|
-
assert(p.布尔估计 === 1, "'encourage' finalizes normally with NO Lean artifact (布尔估计=1)")
|
|
833
|
-
const cards = JSON.parse(readIf(join(proj, 'Verified', '数论_Verified.json')) || '[]')
|
|
834
|
-
assert(!!cards.find((c) => c.id === 'pFree'), "'encourage' writes the Verified card")
|
|
835
|
-
assert(!existsSync(join(proj, 'Formal', 'TODO.md')) || !/pFree/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 'no formalization TODO is created in encourage mode')
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// ---------- 11. the reply channel is REAL (contract §4 / §6.3 / §10.8) ----------
|
|
839
|
-
// The first version of this feature only WROTE "请在回执的 formal 字段写明难度判断" into the prompt
|
|
840
|
-
// and never parsed it: 177 green assertions guarded a dead channel (AUDIT-CHECKLIST §2.2). These
|
|
841
|
-
// cases feed a genuine agent reply through the framework's own dispatch path (subagent/end →
|
|
842
|
-
// handleVerifier → absorb) and assert the DURABLE record, not the wording.
|
|
843
|
-
section('11 the reply channel really lands in the record (blocked / note validation)')
|
|
844
|
-
{
|
|
845
|
-
const h = await makeCase('reply')
|
|
846
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
847
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
848
|
-
await startScheduler(h)
|
|
849
|
-
await h.call('vibe_math_add_proposition', { id: 'pReply', 概述: '用回执记录阻塞', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
850
|
-
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pReply'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
851
|
-
assert(!!vs, 'verifiers were spawned for the reply-channel proposition')
|
|
852
|
-
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pReply:0') || {}).prompt || ''
|
|
853
|
-
assert(/"formal":\{"target":"r-pReply","decision":"used\|blocked\|defect"/.test(vp), '★ the review contract itself advertises the formal field, keyed by the verification id')
|
|
854
|
-
if (vs) {
|
|
855
|
-
// ① `blocked` WITH a note, named by the VERIFICATION id → the OBJECT record must be synced too.
|
|
856
|
-
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '我判断形式化不划算', formal: { target: 'r-pReply', decision: 'blocked', note: '需要大量未形式化的实分析前置知识' } })
|
|
857
|
-
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['r-pReply'] && r['r-pReply'].status === 'blocked') ? r : undefined }, 40, 150)
|
|
858
|
-
assert(!!rec, '★ a `formal.decision=blocked` reply is really absorbed — the channel is not dead code')
|
|
859
|
-
assert(!!rec && rec['r-pReply'].decision === 'blocked' && /实分析前置知识/.test(rec['r-pReply'].note || ''), 'the record keeps the decision AND the reason')
|
|
860
|
-
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)')
|
|
861
|
-
assert(/实分析前置知识/.test(readIf(join(projRoot(h), 'Formal', 'Index.md'))), 'the reply-recorded blocker reaches Formal/Index.md')
|
|
862
|
-
// ② `blocked` WITHOUT a note (a different target) → refused with V2_INVALID_ARGUMENT, no record.
|
|
863
|
-
replyFrom(h, vs[1].childId, { Result: 0.5, Reason: '不想做', formal: { target: 'r-pNoNote', decision: 'blocked' } })
|
|
864
|
-
let acts = ''
|
|
865
|
-
for (let i = 0; i < 25; i++) {
|
|
866
|
-
const st = await h.call('vibe_math_status', {})
|
|
867
|
-
acts = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
868
|
-
if (/V2_INVALID_ARGUMENT/.test(acts)) break
|
|
869
|
-
await sleep(120)
|
|
870
|
-
}
|
|
871
|
-
assert(/V2_INVALID_ARGUMENT/.test(acts), '★ a blocked/defect judgement without a note is REJECTED with V2_INVALID_ARGUMENT')
|
|
872
|
-
assert(/没有写明 note/.test(acts), 'the refusal says why (an explicit decision is required, never a silent skip)')
|
|
873
|
-
const recN = formalStateOf(h)
|
|
874
|
-
assert(!(recN.records || {})['r-pNoNote'] && !(recN.records || {})['pNoNote'], 'no record is invented for the refused judgement')
|
|
875
|
-
assert(!/pNoNote/.test(readIf(join(projRoot(h), 'Formal', 'Index.md'))), 'the refused judgement does not reach the index either')
|
|
876
|
-
// ③ a reply with NO target must not invent an object (safeId('') would fall back to 'anon').
|
|
877
|
-
// The 0.5/0.5 round has no consensus, so the debate round re-woke both children — that is the
|
|
878
|
-
// observable proof that these children are registered and can answer again.
|
|
879
|
-
const woke = await waitFor(() => (h.followups.filter((f) => /DEBATE/.test(f.prompt || '')).length >= 2 ? true : undefined), 40, 150)
|
|
880
|
-
assert(!!woke, 'the debate round re-woke the verifiers (round-2 replies reach the framework again)')
|
|
881
|
-
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '漏写 target', formal: { decision: 'blocked', note: '没有写 target' } })
|
|
882
|
-
let acts2 = ''
|
|
883
|
-
for (let i = 0; i < 25; i++) {
|
|
884
|
-
const st = await h.call('vibe_math_status', {})
|
|
885
|
-
acts2 = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
886
|
-
if (/没有 target/.test(acts2)) break
|
|
887
|
-
await sleep(120)
|
|
888
|
-
}
|
|
889
|
-
assert(/没有 target/.test(acts2), '★ a `formal` reply without a target is refused, not guessed')
|
|
890
|
-
const recT = formalStateOf(h)
|
|
891
|
-
assert(!(recT.records || {}).anon, '★ a `formal` reply without a target cannot invent a record (no "anon" object)')
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// ---------- 12. `defect`: a fidelity defect is NOT "the proposition is false" (§4.1) ----------
|
|
896
|
-
section('12 a defect reply withdraws the proof, writes the TODO and defers the verdict')
|
|
897
|
-
{
|
|
898
|
-
const h = await makeCase('defect')
|
|
899
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
900
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
901
|
-
await startScheduler(h)
|
|
902
|
-
const proj = projRoot(h)
|
|
903
|
-
// A PASSING Lean proof exists for the object — archived under the OBJECT id, the realistic path.
|
|
904
|
-
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pDefect', content: 'theorem p_defect : 2 + 2 = 4 := by decide\n' })
|
|
905
|
-
assert(proof.ok === true && proof.passed === true, 'precondition: the object starts Lean-passed')
|
|
906
|
-
const proofFile = join(proj, 'Verified', 'Lean', 'pDefect.lean')
|
|
907
|
-
assert(existsSync(proofFile), 'the archived proof exists before the defect is reported')
|
|
908
|
-
await h.call('vibe_math_add_proposition', { id: 'pDefect', 概述: '形式化写窄了的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
909
|
-
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pDefect'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
910
|
-
assert(!!vs, 'verifiers were spawned for the Lean-passed object')
|
|
911
|
-
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pDefect:0') || {}).prompt || ''
|
|
912
|
-
assert(/忠实性审查/.test(vp) && /不要投 0/.test(vp), 'the reviewers were told to audit fidelity and NOT to vote 0 on a defect')
|
|
913
|
-
// In REQUIRE mode the framework really does hold the verdict, so THAT promise is the correct one here.
|
|
914
|
-
assert(/本次裁定\*\*不定论\*\*/.test(vp), '★ require fidelity text does promise the hold (the gate really enforces it)')
|
|
915
|
-
assert(!/本档没有门禁/.test(vp), 'require must not claim it has no gate')
|
|
916
|
-
const DEFECT = 'Lean 只证了 n>0 的情形,命题原文是 n≥0'
|
|
917
|
-
if (vs) {
|
|
918
|
-
// A fidelity defect: the voter ABSTAINS (0.3) and records it through the reply channel.
|
|
919
|
-
replyFrom(h, vs[0].childId, { Result: 0.3, Reason: '形式化写窄了(弃权)', formal: { target: 'pDefect', decision: 'defect', note: DEFECT } })
|
|
920
|
-
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pDefect'] && r['pDefect'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
921
|
-
assert(!!rec, '★ the defect reply is absorbed')
|
|
922
|
-
assert(!!rec && rec['pDefect'].status === 'attempted', '★ the object is DOWNGRADED to attempted (the "passed" status is withdrawn)')
|
|
923
|
-
assert(!!rec && rec['pDefect'].proof === '', '★ the proof pointer is cleared')
|
|
924
|
-
assert(!!rec && rec['pDefect'].note === DEFECT, 'the concrete deviation is recorded on the object record')
|
|
925
|
-
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)')
|
|
926
|
-
assert(!existsSync(proofFile), '★ the archived proof Verified/Lean/pDefect.lean is DELETED')
|
|
927
|
-
assert(existsSync(join(proj, 'Formal', 'pDefect.lean')), 'the working file Formal/pDefect.lean is kept (the code is not lost)')
|
|
928
|
-
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
929
|
-
assert(/pDefect/.test(todo) && /defect/.test(todo), '★ the object is listed in Formal/TODO.md with the defect reason')
|
|
930
|
-
assert(/只证了 n>0 的情形/.test(todo), 'the TODO carries the concrete deviation, not just a flag')
|
|
931
|
-
let acts = ''
|
|
932
|
-
for (let i = 0; i < 25; i++) {
|
|
933
|
-
const st = await h.call('vibe_math_status', {})
|
|
934
|
-
acts = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
935
|
-
if (/忠实性缺陷/.test(acts)) break
|
|
936
|
-
await sleep(120)
|
|
937
|
-
}
|
|
938
|
-
assert(/忠实性缺陷/.test(acts), 'the downgrade is announced on the v2-readable channel (activity log)')
|
|
939
|
-
assert(/撤回「已通过」状态/.test(acts), 'the announcement says the passed status was withdrawn')
|
|
940
|
-
assert(/本次裁定不定论/.test(acts), 'and that the verdict is undecided — a defect withdraws a proof, it does not refute the proposition')
|
|
941
|
-
// Now drive the same round to a UNANIMOUS "true": without the defect it would conclude.
|
|
942
|
-
const settled = await verifyWithDebate(h, 'r-pDefect', 1, 0)
|
|
943
|
-
assert(!!settled.first, 'the round was driven to a verdict after the defect')
|
|
944
|
-
const todo2 = await waitFor(() => { const t = readIf(join(proj, 'Formal', 'TODO.md')); return /formal-required/.test(t) ? t : undefined }, 40, 150)
|
|
945
|
-
assert(!!todo2 && /r-pDefect/.test(todo2), '★ require mode DEFERS after a defect (the object enters the formalization TODO as formal-required)')
|
|
946
|
-
await tick(400)
|
|
947
|
-
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
948
|
-
const p = props.find((x) => x.id === 'pDefect') || {}
|
|
949
|
-
assert(p.布尔估计 === 0.5, '★★ the proposition is NOT recorded as false — 布尔估计 unchanged (got ' + p.布尔估计 + ')')
|
|
950
|
-
assert(!(p.证明列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 proof entry was written')
|
|
951
|
-
assert(p.优先级 !== 'never', 'the priority was not pinned to never')
|
|
952
|
-
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), '★ no Verified card: the verdict is UNDECIDED, not "false"')
|
|
953
|
-
}
|
|
954
|
-
// The reverse direction: a defect named by the VERIFICATION id must still downgrade the OBJECT
|
|
955
|
-
// record that actually holds the proof (this is the pair that silently drifts when only one side
|
|
956
|
-
// is written — formalGateRecord would read `passed` from the other side).
|
|
957
|
-
const h2 = await makeCase('defect-rid')
|
|
958
|
-
await h2.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
959
|
-
await h2.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
960
|
-
await startScheduler(h2)
|
|
961
|
-
const proj2 = projRoot(h2)
|
|
962
|
-
await h2.call('vibe_math_lean_archive', { kind: 'proof', target: 'pDefect2', content: 'theorem p_defect2 : 2 + 2 = 4 := by decide\n' })
|
|
963
|
-
await h2.call('vibe_math_add_proposition', { id: 'pDefect2', 概述: '回执用验证 id 命名的缺陷', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
964
|
-
const vs2 = await waitFor(() => { const x = verifiersOf(h2, 'r-pDefect2'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
965
|
-
assert(!!vs2, 'verifiers were spawned for the reverse-direction case')
|
|
966
|
-
if (vs2) {
|
|
967
|
-
replyFrom(h2, vs2[0].childId, { Result: 0.3, Reason: '写宽了', formal: { target: 'r-pDefect2', decision: 'defect', note: 'Lean 版本没有假设 n≥1,比原文更宽' } })
|
|
968
|
-
const rec2 = await waitFor(() => { const r = (formalStateOf(h2).records || {}); return (r['r-pDefect2'] && r['r-pDefect2'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
969
|
-
assert(!!rec2, '★ a defect named by the verification id is absorbed')
|
|
970
|
-
assert(!!rec2 && !!rec2['pDefect2'] && rec2['pDefect2'].status === 'attempted', '★ and the OBJECT record that held `passed` is downgraded as well')
|
|
971
|
-
assert(!!rec2 && rec2['pDefect2'].proof === '', 'its proof pointer is cleared too')
|
|
972
|
-
assert(!existsSync(join(proj2, 'Verified', 'Lean', 'pDefect2.lean')), '★ the archived proof is deleted even though the reply named the other id')
|
|
973
|
-
assert(/pDefect2/.test(readIf(join(proj2, 'Formal', 'TODO.md'))), 'the object is on the formalization TODO')
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// ---------- 13. the gate must read BOTH id spaces ----------
|
|
978
|
-
// v2 has two id spaces that name the same object: the verification id (rId, `r-pX`, `r-pX-s0`) that the
|
|
979
|
-
// verification prompts and the gate use, and the OBJECT id (`pX`) that the agent reads in the target
|
|
980
|
-
// block and normally archives under. `lean_archive` can only ADD a record for the id it was given, and
|
|
981
|
-
// `syncVerificationTarget` merely UPDATES aliases that already exist — so "archived under the object id,
|
|
982
|
-
// rId record does not exist yet" is a real first-time state. A gate that reads only its own side calls
|
|
983
|
-
// that object un-formalized (a false negative), and because the deferral itself materialises the rId
|
|
984
|
-
// record as `status:'none'`, the object can then NEVER conclude: every round re-defers and re-debates
|
|
985
|
-
// (AUDIT-CHECKLIST §3: "成对关系只做一半").
|
|
986
|
-
section('13 the require gate reads BOTH id spaces (archiving under the OBJECT id must not wedge)')
|
|
987
|
-
{
|
|
988
|
-
const h = await makeCase('gate-objid')
|
|
989
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
990
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
991
|
-
await startScheduler(h)
|
|
992
|
-
const proj = projRoot(h)
|
|
993
|
-
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pObjId', content: 'theorem p_objid : 2 + 2 = 4 := by decide\n' })
|
|
994
|
-
assert(proof.ok === true && proof.passed === true, 'precondition: the object is Lean-passed under its OBJECT id')
|
|
995
|
-
const rec0 = formalStateOf(h)
|
|
996
|
-
assert(!!rec0.records && !!rec0.records.pObjId && rec0.records.pObjId.status === 'passed', 'the passed record lives under the object id')
|
|
997
|
-
assert(!(rec0.records || {})['r-pObjId'], 'precondition: no rId record exists yet (this is the state that used to fool the gate)')
|
|
998
|
-
await h.call('vibe_math_add_proposition', { id: 'pObjId', 概述: '用对象 id 归档后必须能定论', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
999
|
-
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pObjId'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1000
|
-
assert(!!vs, 'verifiers were spawned for the object-id case')
|
|
1001
|
-
fireVerdicts(h, vs || [], 1)
|
|
1002
|
-
await tick(600)
|
|
1003
|
-
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
1004
|
-
const p = props.find((x) => x.id === 'pObjId') || {}
|
|
1005
|
-
assert(p.布尔估计 === 1, "★ a Lean-passed object DOES conclude when the proof was archived under the object id (got " + p.布尔估计 + ')')
|
|
1006
|
-
const cards = JSON.parse(readIf(join(proj, 'Verified', '数论_Verified.json')) || '[]')
|
|
1007
|
-
assert(!!cards.find((c) => c.id === 'pObjId'), 'the Verified card was written')
|
|
1008
|
-
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
1009
|
-
assert(!/pObjId/.test(todo), '★ the already-formalized object is NOT put on the formalization TODO')
|
|
1010
|
-
const todo2 = await h.call('vibe_math_status', {})
|
|
1011
|
-
assert(!(todo2.formal.todo || []).some((t) => String(t.id).indexOf('pObjId') !== -1), 'and the status TODO is empty for it too')
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// ---------- 14. withdrawing a proof must reach EVERY archived copy ----------
|
|
1015
|
-
// Contract §4.1 requires the withdrawal, not a best-effort delete: after a `defect` the archived proof
|
|
1016
|
-
// must be gone from `Verified/Lean/` — the one place everyone looks for "the proof of this object".
|
|
1017
|
-
// Two independent ways that fails silently:
|
|
1018
|
-
// (1) the proof was archived under a DIFFERENT id than the one the reply names (`Verified/Lean/<rId>.lean`
|
|
1019
|
-
// while the reply names the object id) — scanning only the two named ids misses it, and the record's
|
|
1020
|
-
// `proof` pointer is cleared anyway, so nothing ever points at the orphan again;
|
|
1021
|
-
// (2) the host cannot delete at all (no subprocess service) — the delete fails and the stale proof stays.
|
|
1022
|
-
section('14 defect withdrawal covers every id alias AND a host that cannot delete')
|
|
1023
|
-
{
|
|
1024
|
-
// (a) archived under the VERIFICATION id, defect named by the OBJECT id.
|
|
1025
|
-
const h = await makeCase('defect-alias')
|
|
1026
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1027
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1028
|
-
await startScheduler(h)
|
|
1029
|
-
const proj = projRoot(h)
|
|
1030
|
-
const pr = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'r-pAlias', content: 'theorem p_alias : 2 + 2 = 4 := by decide\n' })
|
|
1031
|
-
assert(pr.ok === true && pr.passed === true, 'precondition: the proof is archived under the VERIFICATION id')
|
|
1032
|
-
const aliasProof = join(proj, 'Verified', 'Lean', 'r-pAlias.lean')
|
|
1033
|
-
assert(existsSync(aliasProof), 'the archived proof sits at Verified/Lean/<rId>.lean')
|
|
1034
|
-
await h.call('vibe_math_add_proposition', { id: 'pAlias', 概述: '归档写在验证 id 上', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1035
|
-
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pAlias'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1036
|
-
assert(!!vs, 'verifiers were spawned for the alias case')
|
|
1037
|
-
if (vs) {
|
|
1038
|
-
replyFrom(h, vs[0].childId, { Result: 0.3, Reason: '写宽了(弃权)', formal: { target: 'pAlias', decision: 'defect', note: '原文还有 n≥1 的假设' } })
|
|
1039
|
-
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pAlias'] && r['pAlias'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
1040
|
-
assert(!!rec, '★ the defect named by the object id is absorbed')
|
|
1041
|
-
assert(!existsSync(aliasProof), '★★ the proof archived under the VERIFICATION id is withdrawn too (every id alias, not just the two named ones)')
|
|
1042
|
-
assert(!!rec && (!rec['r-pAlias'] || rec['r-pAlias'].proof === ''), 'the rId record no longer points at a proof')
|
|
1043
|
-
assert(!rec['r-pAlias'] || !rec['r-pAlias'].objectId || rec['r-pAlias'].objectId === 'pAlias', '★★ the rId record owner stamp is the OBJECT id (pAlias), never an r-shaped poisoned anchor (got ' + JSON.stringify(rec['r-pAlias'] && rec['r-pAlias'].objectId) + ')')
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
// (b) a host whose deletion cannot work: the archived path must not keep reading as the proof.
|
|
1047
|
-
const h2 = await makeCase('defect-nosub', { noSubprocess: true })
|
|
1048
|
-
await h2.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1049
|
-
const proj2 = projRoot(h2)
|
|
1050
|
-
mkdirSync(join(proj2, 'Verified', 'Lean'), { recursive: true })
|
|
1051
|
-
mkdirSync(join(proj2, 'Formal'), { recursive: true })
|
|
1052
|
-
const WORK = 'theorem p_stale : 2 + 2 = 4 := by decide\n'
|
|
1053
|
-
writeFileSync(join(proj2, 'Formal', 'pStale.lean'), WORK, 'utf8')
|
|
1054
|
-
writeFileSync(join(proj2, 'Verified', 'Lean', 'pStale.lean'), WORK, 'utf8')
|
|
1055
|
-
// The record is written directly (this host has no subprocess, so the LEAN tools cannot run at all):
|
|
1056
|
-
// what is under test is the WITHDRAWAL path, not the archiving path.
|
|
1057
|
-
writeFileSync(join(proj2, 'VibeMath_State', 'formal.json'), JSON.stringify({ records: { pStale: { status: 'passed', file: 'Formal/pStale.lean', proof: 'Verified/Lean/pStale.lean', decision: 'used', updatedAt: 1 } }, todo: [] }), 'utf8')
|
|
1058
|
-
await h2.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1059
|
-
await h2.call('vibe_math_add_proposition', { id: 'pStale', 概述: '宿主删不掉归档证明时的撤回', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1060
|
-
await startScheduler(h2)
|
|
1061
|
-
const st0 = await h2.call('vibe_math_status', {})
|
|
1062
|
-
assert(st0.formal.objects.some((o) => o.target === 'pStale' && o.status === 'passed'), 'precondition: the passed record with an archived proof survives resume on the no-subprocess host')
|
|
1063
|
-
const vs2 = await waitFor(() => { const x = verifiersOf(h2, 'r-pStale'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1064
|
-
assert(!!vs2, 'verifiers were spawned on the no-subprocess host')
|
|
1065
|
-
if (vs2) {
|
|
1066
|
-
replyFrom(h2, vs2[0].childId, { Result: 0.3, Reason: '不忠实(弃权)', formal: { target: 'pStale', decision: 'defect', note: 'Lean 少了 n≥1' } })
|
|
1067
|
-
const rec2 = await waitFor(() => { const r = (formalStateOf(h2).records || {}); return (r['pStale'] && r['pStale'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
1068
|
-
assert(!!rec2, '★ the defect is absorbed even though this host cannot run a delete')
|
|
1069
|
-
assert(!!rec2 && rec2['pStale'].status === 'attempted' && rec2['pStale'].proof === '', 'the record is downgraded and its proof pointer cleared')
|
|
1070
|
-
const stalePath = join(proj2, 'Verified', 'Lean', 'pStale.lean')
|
|
1071
|
-
assert(existsSync(stalePath), 'the archived file could NOT be deleted here (no subprocess) — so the fallback has to handle it')
|
|
1072
|
-
const body = readIf(stalePath)
|
|
1073
|
-
assert(!/theorem p_stale/.test(body), '★★ the original proof text is GONE from the archived path (it can no longer be read as the proof)')
|
|
1074
|
-
assert(/已撤回/.test(body), '★★ …and that path carries an explicit withdrawal notice instead')
|
|
1075
|
-
assert(/Formal\/pStale\.lean/.test(body), 'the notice points at the working file that is kept')
|
|
1076
|
-
const acts = (await h2.call('vibe_math_status', {})).recentActivity.map((a) => a.detail).join('\n')
|
|
1077
|
-
assert(/覆写/.test(acts), '★ the announcement says WHAT actually happened (overwritten, not deleted)')
|
|
1078
|
-
assert(/撤回「已通过」状态/.test(acts), 'the announcement still says the passed status was withdrawn')
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// ---------- 15. the「判断命题」transfer is a verdict on the SOURCE proposition ----------
|
|
1083
|
-
// When a solver's solution for a "判断下述命题是否成立:X" problem is verified, the result is TRANSFERRED
|
|
1084
|
-
// onto the source proposition: `布尔估计 = v`, `已验证 = true`, a probability-1 proof/refutation entry,
|
|
1085
|
-
// and `优先级 = 'never'` for a boolean v. That IS a boolean verdict on X, so require mode must gate it —
|
|
1086
|
-
// otherwise a proposition nobody formalized is silently concluded (and permanently de-scheduled).
|
|
1087
|
-
// (c) an OBJECT ID that itself ends in `-sN` / `-pfN` / `-rfN` — exactly the shapes the rId suffix
|
|
1088
|
-
// parser strips. `formalObjectIdOf` used to strip such a suffix unconditionally, so the verification id
|
|
1089
|
-
// `r-pAmb-s1` resolved to object `pAmb`: a DIFFERENT object. A defect about the former would then
|
|
1090
|
-
// downgrade the latter and even retract ITS archived proof, while the real owner stayed `passed`.
|
|
1091
|
-
// The verification task knows its owner (r.pId / r.qid), so that is the authoritative source.
|
|
1092
|
-
{
|
|
1093
|
-
const h = await makeCase('defect-ambig')
|
|
1094
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1095
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1096
|
-
await startScheduler(h)
|
|
1097
|
-
const proj = projRoot(h)
|
|
1098
|
-
const innocent = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pAmb', content: 'theorem p_amb : 2 + 2 = 4 := by decide\n' })
|
|
1099
|
-
assert(innocent.ok === true && innocent.passed === true, 'ambiguity: the neighbour pAmb has its own passing proof')
|
|
1100
|
-
const innocentProof = join(proj, 'Verified', 'Lean', 'pAmb.lean')
|
|
1101
|
-
const prone = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pAmb-s1', content: 'theorem p_amb_s1 : 3 + 3 = 6 := by decide\n' })
|
|
1102
|
-
assert(prone.ok === true && prone.passed === true, 'ambiguity: pAmb-s1 (id ending in -s1) has its own passing proof')
|
|
1103
|
-
const proneProof = join(proj, 'Verified', 'Lean', 'pAmb-s1.lean')
|
|
1104
|
-
assert(existsSync(innocentProof) && existsSync(proneProof), 'ambiguity: both archived proofs exist')
|
|
1105
|
-
await h.call('vibe_math_add_proposition', { id: 'pAmb', 概述: '同前缀的邻居对象', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1106
|
-
await h.call('vibe_math_add_proposition', { id: 'pAmb-s1', 概述: '对象 id 本身以 -s1 结尾', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1107
|
-
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pAmb-s1'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1108
|
-
assert(!!vs, 'ambiguity: verifiers were spawned for r-pAmb-s1')
|
|
1109
|
-
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pAmb-s1:0') || {}).prompt || ''
|
|
1110
|
-
assert(/忠实性审查/.test(vp), 'ambiguity: the review subject is the Lean-passed object')
|
|
1111
|
-
assert(/Verified\/Lean\/pAmb-s1\.lean/.test(vp), '★★ and the fidelity prompt points at pAmb-s1 OWN proof, not at a same-prefix neighbour (got ' + (vp.match(/Verified\/Lean\/[^\s))]+/) || ['none'])[0] + ')')
|
|
1112
|
-
if (vs) {
|
|
1113
|
-
replyFrom(h, vs[0].childId, { Result: 0.3, Reason: '结论写窄了(弃权)', formal: { target: 'r-pAmb-s1', decision: 'defect', note: 'Lean 只证了 n>0 的情形' } })
|
|
1114
|
-
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pAmb-s1'] && r['pAmb-s1'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
1115
|
-
assert(!!rec, '★★ a defect named by r-pAmb-s1 downgrades its REAL owner (pAmb-s1)')
|
|
1116
|
-
assert(!existsSync(proneProof) || /已撤回/.test(readIf(proneProof)), '★★ and retracts pAmb-s1 own archived proof')
|
|
1117
|
-
const recs = formalStateOf(h).records || {}
|
|
1118
|
-
assert(!!recs['pAmb'] && recs['pAmb'].status === 'passed', '★★ the same-prefix NEIGHBOUR pAmb keeps its passed status (got ' + (recs['pAmb'] ? recs['pAmb'].status : 'no record') + ')')
|
|
1119
|
-
assert(existsSync(innocentProof) && !/已撤回/.test(readIf(innocentProof)), '★★ and its archived proof file is untouched — a defect must never retract another object\'s proof')
|
|
1120
|
-
assert(/pAmb-s1/.test(readIf(join(proj, 'Formal', 'TODO.md')) || ''), 'the TODO names the real owner')
|
|
1121
|
-
// The record carries the authoritative owner, so the mapping survives a resume even when the
|
|
1122
|
-
// in-memory verification task is gone (the fallback string parse would pick the neighbour again).
|
|
1123
|
-
assert(!!recs['r-pAmb-s1'] && recs['r-pAmb-s1'].objectId === 'pAmb-s1',
|
|
1124
|
-
'★★ the verification record remembers its object id (authoritative, not re-parsed from the name) — got ' + JSON.stringify(recs['r-pAmb-s1'] && recs['r-pAmb-s1'].objectId))
|
|
1125
|
-
assert(!!recs['pAmb'] && !recs['pAmb'].objectId, 'the neighbour record carries no bogus owner stamp')
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// (d) THE RECORD-OWNER FALLBACK must not be hijacked by a hand-edited state file. The state file is
|
|
1130
|
-
// user-editable, and a record whose `objectId` is itself r-shaped is not a usable owner: it would make
|
|
1131
|
-
// the verification id map to ITSELF, so a defect named by that rId would never reach the object side.
|
|
1132
|
-
// This case uses a WORK-ROUND (explorer) reply, which also carries `formal` and runs with NO verification
|
|
1133
|
-
// task in memory — i.e. it is the path that actually reads the owner from the record (a verifier reply
|
|
1134
|
-
// would take the task-registry path instead, which is authoritative and already covered above).
|
|
1135
|
-
{
|
|
1136
|
-
const h = await makeCase('anchor-poison', { noSubprocess: true })
|
|
1137
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1138
|
-
const proj = projRoot(h)
|
|
1139
|
-
mkdirSync(join(proj, 'Verified', 'Lean'), { recursive: true })
|
|
1140
|
-
mkdirSync(join(proj, 'Formal'), { recursive: true })
|
|
1141
|
-
const WORK = 'theorem p_x : 2 + 2 = 4 := by decide\n'
|
|
1142
|
-
writeFileSync(join(proj, 'Formal', 'pX.lean'), WORK, 'utf8')
|
|
1143
|
-
writeFileSync(join(proj, 'Verified', 'Lean', 'pX.lean'), WORK, 'utf8')
|
|
1144
|
-
const base = { status: 'passed', file: 'Formal/pX.lean', proof: 'Verified/Lean/pX.lean', decision: 'used', updatedAt: 1 }
|
|
1145
|
-
writeFileSync(join(proj, 'VibeMath_State', 'formal.json'), JSON.stringify({ records: {
|
|
1146
|
-
pX: Object.assign({}, base),
|
|
1147
|
-
'r-pX': Object.assign({}, base, { objectId: 'r-pX' }), // ← the r-shaped anchor a hand edit could leave
|
|
1148
|
-
}, todo: [] }), 'utf8')
|
|
1149
|
-
await h.call('vibe_math_add_problem', { id: 'qA', description: '锚点污染用例', priority: 1 })
|
|
1150
|
-
await startScheduler(h)
|
|
1151
|
-
await tick(2000)
|
|
1152
|
-
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:qA')), 60, 200)
|
|
1153
|
-
assert(!!ex, 'anchor-poison: the explorer was spawned (the scheduler is running)')
|
|
1154
|
-
if (ex) {
|
|
1155
|
-
const reply = { directions: [{ id: 'd1', title: 'D', method: 'm', core_assumption: 'c', feasibility: 0.6 }], formal: { target: 'r-pX', decision: 'defect', note: 'Lean 少了 n≥1' } }
|
|
1156
|
-
h.fireEnd({ id: ex.childId, runId: 'rp', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: '```json\n' + JSON.stringify(reply) + '\n```' }] })
|
|
1157
|
-
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pX'] && r['pX'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
1158
|
-
assert(!!rec, '★★ a work-round defect named by an rId still reaches the OBJECT side when the record carries an r-shaped anchor (the anchor is rejected, not trusted)')
|
|
1159
|
-
const all = formalStateOf(h).records || {}
|
|
1160
|
-
assert(!!all['r-pX'] && all['r-pX'].status === 'attempted', '★★ and the rId record is downgraded as well (both id spaces stay in step)')
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
section('15 the require gate also covers the judge-problem transfer')
|
|
1165
|
-
{
|
|
1166
|
-
const h = await makeCase('judge-gate')
|
|
1167
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1168
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1169
|
-
const proj = projRoot(h)
|
|
1170
|
-
await h.call('vibe_math_add_proposition', { id: 'pJudgeSrc', 概述: '被判断的源命题(未形式化)', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1171
|
-
await h.call('vibe_math_add_problem', { id: 'qJudge', description: '判断下述命题是否成立:pJudgeSrc', priority: 1 })
|
|
1172
|
-
const qsFile = join(proj, 'qs', 'qs.json')
|
|
1173
|
-
const qs0 = JSON.parse(readIf(qsFile) || '[]')
|
|
1174
|
-
const qj = qs0.find((q) => q.id === 'qJudge')
|
|
1175
|
-
qj.判断命题 = 'pJudgeSrc'
|
|
1176
|
-
// A solver-produced solution carries no 来源列表 — that is exactly the branch that transfers to the source.
|
|
1177
|
-
qj.解法列表 = [{ 完整解法: '该命题不成立的论证', 正确概率: 0.8, 已验: false }]
|
|
1178
|
-
writeFileSync(qsFile, JSON.stringify(qs0, null, 2), 'utf8')
|
|
1179
|
-
await startScheduler(h)
|
|
1180
|
-
const sp = await waitFor(() => { const x = verifiersOf(h, 'r-qJudge-s0'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1181
|
-
assert(!!sp, 'verifiers were spawned for the judge problem solution')
|
|
1182
|
-
fireVerdicts(h, sp || [], 0)
|
|
1183
|
-
await tick(600)
|
|
1184
|
-
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
1185
|
-
const ap = props.find((x) => x.id === 'pJudgeSrc') || {}
|
|
1186
|
-
assert(ap.布尔估计 === 0.5, "★★ the source proposition's 布尔估计 is UNCHANGED (require mode must not conclude 假 through the transfer; got " + ap.布尔估计 + ')')
|
|
1187
|
-
assert(!(ap.证伪列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 refutation was written onto the source proposition')
|
|
1188
|
-
assert(ap.优先级 !== 'never', 'the source proposition was not pinned to never')
|
|
1189
|
-
assert(ap.已验证 !== true, '★ it stays re-verifiable (the ungated path also marked it 已验证, removing it from every future candidate set)')
|
|
1190
|
-
assert(/pJudgeSrc/.test(readIf(join(proj, 'Formal', 'TODO.md'))), '★ the deferred source proposition is on the formalization TODO')
|
|
1191
|
-
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), 'no Verified card for the source proposition')
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// ---------- 16. the prompt corpus (contract §10.10) ----------
|
|
1195
|
-
// A HUMAN must be able to re-read every prompt the framework emitted, not just the assertions
|
|
1196
|
-
// about them. Paths are normalised so the dump is deterministic, diffable and machine-free.
|
|
1197
|
-
section("15b a \`used\` reply must NOT downgrade an already-passed object")
|
|
1198
|
-
{
|
|
1199
|
-
// Contract §4: a fidelity defect WITHDRAWS a proof; a plain `used` judgement merely says "this round
|
|
1200
|
-
// touched the formalization". Letting `used` downgrade an established proof would silently remove the
|
|
1201
|
-
// fidelity branch from every later review prompt and re-close the require gate — the exact class that
|
|
1202
|
-
// was already fixed once for v4's formalSetRun. v3/v4/v5 preserve passed/blocked here; v2 did not.
|
|
1203
|
-
const h = await makeCase('used-keep')
|
|
1204
|
-
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1205
|
-
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1206
|
-
await startScheduler(h)
|
|
1207
|
-
const proj = projRoot(h)
|
|
1208
|
-
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pUsedKeep', content: 'theorem p_used_keep : 2 + 2 = 4 := by decide\n' })
|
|
1209
|
-
assert(proof.ok === true && proof.passed === true, 'used-keep: the object starts Lean-passed')
|
|
1210
|
-
const proofFile = join(proj, 'Verified', 'Lean', 'pUsedKeep.lean')
|
|
1211
|
-
assert(existsSync(proofFile), 'used-keep: the archived proof exists before the used reply')
|
|
1212
|
-
await h.call('vibe_math_add_proposition', { id: 'pUsedKeep', 概述: '已有通过证明后再写一次 used 回执', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1213
|
-
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pUsedKeep'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1214
|
-
assert(!!vs, 'used-keep: verifiers were spawned for the Lean-passed object')
|
|
1215
|
-
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pUsedKeep:0') || {}).prompt || ''
|
|
1216
|
-
assert(/忠实性审查/.test(vp), 'used-keep: the reviewers were told to audit FIDELITY (the Lean code is the subject)')
|
|
1217
|
-
if (vs) {
|
|
1218
|
-
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '这一轮只是又写了一遍草稿', formal: { target: 'pUsedKeep', decision: 'used', file: 'Formal/pUsedKeep.lean' } })
|
|
1219
|
-
// The reply is what materialises the canonical verification alias r-<objectId>, so waiting for
|
|
1220
|
-
// THAT is a signal only the reply can produce (the archive already wrote decision:'used').
|
|
1221
|
-
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['r-pUsedKeep'] && r['pUsedKeep']) ? r : undefined }, 40, 150)
|
|
1222
|
-
assert(!!rec, 'used-keep: the `used` reply is absorbed (the canonical verification alias was materialised)')
|
|
1223
|
-
assert(!!rec && rec['pUsedKeep'].status === 'passed', '★★ a `used` reply does NOT downgrade an already-passed object (got ' + (rec && rec['pUsedKeep'] ? rec['pUsedKeep'].status : 'no record') + ')')
|
|
1224
|
-
assert(!!rec && rec['pUsedKeep'].proof === 'Verified/Lean/pUsedKeep.lean', '★★ and its proof pointer survives the reply')
|
|
1225
|
-
assert(!!rec && !!rec['r-pUsedKeep'] && rec['r-pUsedKeep'].status === 'passed', '★★ the verification-id alias keeps passed too (both id spaces)')
|
|
1226
|
-
assert(existsSync(proofFile), '★★ the archived proof is still on disk')
|
|
1227
|
-
const st = await h.call('vibe_math_status', {})
|
|
1228
|
-
const obj = (st.formal.objects || []).find((o) => o.target === 'pUsedKeep')
|
|
1229
|
-
assert(!!obj && obj.status === 'passed', '★★ status still reports the object as Lean-passed (v2 exposes status.formal.objects, not a "passed" array)')
|
|
1230
|
-
// The consequence that actually matters: with 'passed' preserved the require gate stays OPEN, so
|
|
1231
|
-
// driving the round to a unanimous "true" still concludes (a wrongly downgraded object is deferred).
|
|
1232
|
-
const settled = await verifyWithDebate(h, 'r-pUsedKeep', 1, 0)
|
|
1233
|
-
assert(!!settled.first, 'used-keep: the round can still be driven to a verdict')
|
|
1234
|
-
assert(!/pUsedKeep/.test(readIf(join(proj, 'Formal', 'TODO.md')) || ''), '★★ no formal-required TODO was created: the surviving passed status still opens the require gate')
|
|
1235
|
-
// The same rule covers the RUN path (docs §4): a run attributed to the object may not downgrade it.
|
|
1236
|
-
const rerun = await h.call('vibe_math_lean_run', { file: 'Formal/pUsedKeep.lean', target: 'pUsedKeep' })
|
|
1237
|
-
assert(rerun.ok === true, 'used-keep: the object work file runs green (the record assertion below is the point)')
|
|
1238
|
-
const obj2 = ((await h.call('vibe_math_status', {})).formal.objects || []).find((o) => o.target === 'pUsedKeep')
|
|
1239
|
-
assert(!!obj2 && obj2.status === 'passed', '★★ and the record is still passed (a scratch run may not withdraw a proof)')
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
section('16 the captured prompt corpus is written for human review')
|
|
1244
|
-
{
|
|
1245
|
-
// Freeze the scheduler in every case FIRST: a still-running tick loop could emit one more
|
|
1246
|
-
// prompt between two runs and make the corpus non-deterministic.
|
|
1247
|
-
for (const h of hosts) { try { await h.call('vibe_math_pause', {}) } catch (e) { /* ignore */ } }
|
|
1248
|
-
const scrub = (h, s) => {
|
|
1249
|
-
const ws = String(h.WS)
|
|
1250
|
-
const slash = ws.replace(/\\/g, '/')
|
|
1251
|
-
return String(s == null ? '' : s)
|
|
1252
|
-
.split(slash + '/VibeMath').join('<VIBEMATH>')
|
|
1253
|
-
.split(ws + '\\VibeMath').join('<VIBEMATH>')
|
|
1254
|
-
.split(slash).join('<WS>')
|
|
1255
|
-
.split(ws).join('<WS>')
|
|
1256
|
-
}
|
|
1257
|
-
const entries = []
|
|
1258
|
-
for (const h of hosts) {
|
|
1259
|
-
for (const s of h.spawns) entries.push({ kind: 'spawn', case: h.label, label: s.label, prompt: scrub(h, s.prompt) })
|
|
1260
|
-
for (const f of h.followups) {
|
|
1261
|
-
const owner = h.spawns.find((s) => s.childId === f.childId)
|
|
1262
|
-
entries.push({ kind: 'wake', case: h.label, label: owner ? owner.label : f.childId, prompt: scrub(h, f.prompt) })
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
mkdirSync(CORPUS_DIR, { recursive: true })
|
|
1266
|
-
writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.json'), JSON.stringify({ entries: entries }, null, 2), 'utf8')
|
|
1267
|
-
const md = ['# V2 形式化验证交互语料(prompt corpus)', '',
|
|
1268
|
-
'> 由 `formal-verify-v2.test.mjs` 落盘:框架**真正发出**的每一条提示词原文。',
|
|
1269
|
-
'> 工作区路径归一化为 `<WS>`、VibeMath 根归一化为 `<VIBEMATH>`,因此可 diff、不泄露本机路径。',
|
|
1270
|
-
'> 覆盖:off 档(无任何 Lean 文字)、encourage 与 require 的表决初评/辩论、passed 后的忠实性分支、',
|
|
1271
|
-
'> 以及平时工作轮的「顺手形式化」段落与 formal 回执契约。', '']
|
|
1272
|
-
for (let i = 0; i < entries.length; i++) {
|
|
1273
|
-
const e = entries[i]
|
|
1274
|
-
md.push('## [' + i + '] ' + e.kind + ' · ' + e.label + ' · case=' + e.case)
|
|
1275
|
-
md.push('')
|
|
1276
|
-
md.push('```text')
|
|
1277
|
-
md.push(e.prompt)
|
|
1278
|
-
md.push('```')
|
|
1279
|
-
md.push('')
|
|
1280
|
-
}
|
|
1281
|
-
writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.md'), md.join('\n'), 'utf8')
|
|
1282
|
-
assert(existsSync(join(CORPUS_DIR, 'formal-verify-v2.json')) && existsSync(join(CORPUS_DIR, 'formal-verify-v2.md')), 'the prompt corpus was written (JSON + Markdown)')
|
|
1283
|
-
assert(entries.length >= 15, 'the corpus covers the whole run (' + entries.length + ' prompts)')
|
|
1284
|
-
assert(entries.some((e) => /【Lean 形式化验证(鼓励模式)】/.test(e.prompt)), 'the corpus contains the encourage verify prompt')
|
|
1285
|
-
assert(entries.some((e) => /【Lean 形式化验证(强制模式)】/.test(e.prompt)), '★ the corpus contains the REQUIRE verify prompt')
|
|
1286
|
-
assert(entries.some((e) => /你不需要重新检查推导/.test(e.prompt) && /一致 → Result = 1/.test(e.prompt)), 'the corpus contains the passed/fidelity prompt')
|
|
1287
|
-
assert(entries.some((e) => /【顺手形式化(鼓励)】/.test(e.prompt)), 'the corpus contains the work-round 顺手形式化 prompt')
|
|
1288
|
-
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')
|
|
1289
|
-
assert(entries.some((e) => !/Lean|形式化/.test(e.prompt)), 'the corpus contains off-mode prompts with no Lean text at all')
|
|
1290
|
-
assert(entries.some((e) => e.kind === 'wake'), 'the corpus also keeps the continuation prompts (debate rounds)')
|
|
1291
|
-
// Generic sweeps over EVERY captured prompt, not spot checks (AUDIT §2.1).
|
|
1292
|
-
const joined = entries.map((e) => e.prompt).join('\n')
|
|
1293
|
-
const bare = entries.filter((e) => /(^|[^a-z_])lean_(run|archive|lib)/.test(e.prompt))
|
|
1294
|
-
assert(bare.length === 0, '★ no captured prompt names an abbreviated tool (' + bare.map((b) => b.label).join(',') + ')')
|
|
1295
|
-
const zero = entries.filter((e) => /偏离\s*→\s*0/.test(e.prompt))
|
|
1296
|
-
assert(zero.length === 0, '★ no captured prompt turns a fidelity defect into a 0 vote (' + zero.map((z) => z.label).join(',') + ')')
|
|
1297
|
-
const wrongField = entries.filter((e) => /忠实性/.test(e.prompt) && /verdict/.test(e.prompt))
|
|
1298
|
-
assert(wrongField.length === 0, '★ no fidelity prompt names a `verdict` field (' + wrongField.map((w) => w.label).join(',') + ')')
|
|
1299
|
-
const dirty = entries.filter((e) => /\[object Object\]|\bNaN\b|:\s*undefined|["']undefined["']|undefined\s*[,}\]]/.test(e.prompt))
|
|
1300
|
-
assert(dirty.length === 0, 'no captured prompt contains placeholder garbage (' + dirty.map((d) => d.label).join(',') + ')')
|
|
1301
|
-
assert(joined.indexOf('<VIBEMATH>') !== -1, 'the VibeMath root is normalised to <VIBEMATH>')
|
|
1302
|
-
for (const h of hosts) {
|
|
1303
|
-
const ws = String(h.WS)
|
|
1304
|
-
assert(joined.indexOf(ws) === -1 && joined.indexOf(ws.replace(/\\/g, '/')) === -1, 'no captured prompt leaks a machine path (' + h.label + ')')
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
// cleanup
|
|
1309
|
-
for (const h of hosts) { try { rmSync(h.WS, { recursive: true, force: true }) } catch (e) { /* ignore */ } }
|
|
1310
|
-
|
|
1311
|
-
console.log('')
|
|
1312
|
-
console.log('passed=' + passed + ' failed=' + failed)
|
|
1313
|
-
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
1314
|
-
console.log('ALL GREEN')
|
|
1315
|
-
process.exit(0)
|
|
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 tests/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 terminations = []
|
|
60
|
+
const subprocess = {
|
|
61
|
+
async resolveExecutable(cmd) {
|
|
62
|
+
if (!toolchainAvailable) throw new Error('spawn lean ENOENT')
|
|
63
|
+
if (String(cmd) !== 'lean') throw new Error('unknown executable ' + cmd)
|
|
64
|
+
return 'lean'
|
|
65
|
+
},
|
|
66
|
+
spawn(spec) {
|
|
67
|
+
const last = spec.argv[spec.argv.length - 1]
|
|
68
|
+
const script = String(last || '')
|
|
69
|
+
// v2 creates its directory tree through this SAME subprocess service
|
|
70
|
+
// (`powershell … New-Item -Force -ItemType Directory -Path 'a','b'`). Without honouring
|
|
71
|
+
// it, the Formal/ and Verified/Lean/ assertions would be vacuous: they would fail for the
|
|
72
|
+
// wrong reason (a mocked no-op) instead of a real defect.
|
|
73
|
+
if (/New-Item/.test(script)) {
|
|
74
|
+
const m = /-Path\s+(.+?)\s*(\||$)/.exec(script)
|
|
75
|
+
if (m) m[1].split(',').forEach((p) => { const q = p.trim().replace(/^'|'$/g, '').replace(/''/g, "'"); if (q) mkdirSync(q, { recursive: true }) })
|
|
76
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
77
|
+
}
|
|
78
|
+
if (/mkdir -p /.test(script)) {
|
|
79
|
+
const m = /mkdir -p (.+)$/.exec(script)
|
|
80
|
+
if (m) m[1].split(/\s+/).forEach((p) => { const q = p.trim().replace(/^'|'$/g, ''); if (q) mkdirSync(q, { recursive: true }) })
|
|
81
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
82
|
+
}
|
|
83
|
+
// v2 ALSO deletes files through this same service (`powershell … Remove-Item -LiteralPath 'x'`,
|
|
84
|
+
// POSIX `rm -f 'x'`). Without honouring it, the `defect` assertion "the archived proof is
|
|
85
|
+
// gone" would pass vacuously (nothing was ever deleted) instead of testing the real code path.
|
|
86
|
+
if (/Remove-Item/.test(script)) {
|
|
87
|
+
const m = /-LiteralPath\s+'((?:[^']|'')*)'/.exec(script)
|
|
88
|
+
if (m) { try { rmSync(m[1].replace(/''/g, "'"), { force: true }) } catch (e) { /* best effort */ } }
|
|
89
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
90
|
+
}
|
|
91
|
+
if (/^\s*rm -f /.test(script)) {
|
|
92
|
+
const re = /'((?:[^']|'\\'')*)'/g
|
|
93
|
+
let m
|
|
94
|
+
while ((m = re.exec(script)) !== null) { try { rmSync(m[1].replace(/'\\''/g, "'"), { force: true }) } catch (e) { /* best effort */ } }
|
|
95
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
96
|
+
}
|
|
97
|
+
const text = existsSync(last) ? readFileSync(last, 'utf8') : ''
|
|
98
|
+
const bad = /sorry|-- FAIL/.test(text)
|
|
99
|
+
leanRuns.push({ argv: spec.argv.slice(0, -1), file: last, cwd: spec.cwd, graceMs: spec.graceMs, stdio: spec.stdio })
|
|
100
|
+
const stdout = bad ? '' : 'ok\n'
|
|
101
|
+
const stderr = bad ? 'error: declaration uses sorry\n' : ''
|
|
102
|
+
if (/-- HANG/.test(text)) {
|
|
103
|
+
// A run that never finishes by itself. It settles ONLY when the plugin actively terminates it
|
|
104
|
+
// (contract §7: "对超时调用 handle.terminate()"), with a bounded 2.5 s fallback so a plugin
|
|
105
|
+
// that FORGETS to terminate FAILS the terminate/speed assertions instead of hanging this suite.
|
|
106
|
+
let settled = false
|
|
107
|
+
let finish
|
|
108
|
+
const done = new Promise((resolve) => { finish = resolve })
|
|
109
|
+
const fallback = REAL_SET_TIMEOUT(() => { if (!settled) { settled = true; finish({ exitCode: null, signal: 'SIGKILL' }) } }, 2500)
|
|
110
|
+
return {
|
|
111
|
+
done,
|
|
112
|
+
collected: {
|
|
113
|
+
stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) },
|
|
114
|
+
stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) },
|
|
115
|
+
},
|
|
116
|
+
terminate() {
|
|
117
|
+
if (settled) return
|
|
118
|
+
settled = true
|
|
119
|
+
REAL_CLEAR_TIMEOUT(fallback)
|
|
120
|
+
terminations.push({ file: last })
|
|
121
|
+
finish({ exitCode: null, signal: 'SIGTERM' })
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
done: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null }),
|
|
127
|
+
collected: {
|
|
128
|
+
stdout: { readFrom: () => ({ text: stdout, nextOffset: stdout.length, lossy: false }) },
|
|
129
|
+
stderr: { readFrom: () => ({ text: stderr, nextOffset: stderr.length, lossy: false }) },
|
|
130
|
+
},
|
|
131
|
+
terminate() {},
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------
|
|
137
|
+
// mock host (v2 standing-mount shape: one plugin instance, many sessions)
|
|
138
|
+
// ---------------------------------------------------------------
|
|
139
|
+
function makeHost(WS, opts = {}) {
|
|
140
|
+
const listeners = {}
|
|
141
|
+
const toolRegs = []
|
|
142
|
+
const cmdRegs = []
|
|
143
|
+
const spawns = []
|
|
144
|
+
const followups = []
|
|
145
|
+
const interrupts = []
|
|
146
|
+
const ctx = {
|
|
147
|
+
get(name) {
|
|
148
|
+
if (name === 'subprocess') return opts.noSubprocess ? undefined : subprocess
|
|
149
|
+
if (name === 'sandboxPolicy') return { workspaceRoot: WS, resolve: () => ({ workspaceRoot: WS }) }
|
|
150
|
+
return undefined
|
|
151
|
+
},
|
|
152
|
+
on(event, fn) { (listeners[event] = listeners[event] || []).push(fn) },
|
|
153
|
+
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
154
|
+
logger: { info() {}, warn() {}, error() {} },
|
|
155
|
+
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
156
|
+
commands: { register(spec) { cmdRegs.push(spec); return () => {} } },
|
|
157
|
+
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
158
|
+
subagents: {
|
|
159
|
+
list() { return ['spawn'] },
|
|
160
|
+
async startContinuable({ label, request }) {
|
|
161
|
+
const childId = 'child-' + (spawns.length + 1) + '-' + Math.random().toString(36).slice(2, 6)
|
|
162
|
+
spawns.push({ label, childId, rootId: (request && request.parent && request.parent.id) || 'root', prompt: request && request.prompt && request.prompt[0] && request.prompt[0].text })
|
|
163
|
+
return { childId }
|
|
164
|
+
},
|
|
165
|
+
async followup(parent, childId, blocks) { followups.push({ childId, prompt: (blocks && blocks[0] && blocks[0].text) || '' }) },
|
|
166
|
+
async sendMessage(parent, childId, blocks) { followups.push({ childId, prompt: (blocks && blocks[0] && blocks[0].text) || '' }) },
|
|
167
|
+
interrupt(childId) { interrupts.push(childId) },
|
|
168
|
+
},
|
|
169
|
+
agents: { roots() { return [] }, get() { return undefined } },
|
|
170
|
+
fs: {
|
|
171
|
+
async resolve(rel, o) {
|
|
172
|
+
const raw = String(rel)
|
|
173
|
+
const p = isAbsolute(raw) ? raw : join((o && o.cwd) || WS, ...raw.split('/'))
|
|
174
|
+
return p.replace(/\\/g, '/')
|
|
175
|
+
},
|
|
176
|
+
async stat(t) { if (!existsSync(t)) return undefined; return { type: statSync(t).isDirectory() ? 'directory' : 'file' } },
|
|
177
|
+
async readText(t) { return readFileSync(t, 'utf8') },
|
|
178
|
+
async writeText(t, c) { mkdirSync(dirname(t), { recursive: true }); writeFileSync(t, c, 'utf8') },
|
|
179
|
+
async listDir(t) { if (!existsSync(t)) return []; return readdirSync(t, { withFileTypes: true }).map((e) => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
const fireEnd = (info) => { for (const h of (listeners['subagent/end'] || [])) h(info) }
|
|
183
|
+
return { ctx, toolRegs, cmdRegs, spawns, followups, interrupts, fireEnd, WS }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function makeRoot(id, WS) {
|
|
187
|
+
return { id, options: { provider: 'mock', model: 'mock' }, session: { id, header: { cwd: WS, parentSession: undefined } }, followup() {} }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const hosts = []
|
|
191
|
+
/**
|
|
192
|
+
* Build an INDEPENDENT plugin instance + workspace for one test case.
|
|
193
|
+
* AUDIT-CHECKLIST §2.3: cases must not share a session root/workspace, or a leftover
|
|
194
|
+
* heartbeat/verification from one case pollutes the next.
|
|
195
|
+
*/
|
|
196
|
+
async function makeCase(label, opts = {}) {
|
|
197
|
+
const WS = mkdtempSync(join(tmpdir(), 'vibe-v2-lean-' + label + '-'))
|
|
198
|
+
const h = makeHost(WS, opts)
|
|
199
|
+
const mod = await import(PLUGIN.href + '?t=' + Date.now() + '-' + Math.random().toString(36).slice(2, 8))
|
|
200
|
+
;(mod.default || mod).apply(h.ctx)
|
|
201
|
+
const root = makeRoot('sess-' + label, WS)
|
|
202
|
+
const call = async (name, args, agent) => {
|
|
203
|
+
const spec = h.toolRegs.find((s) => s.name === name)
|
|
204
|
+
if (!spec) throw new Error('no tool ' + name)
|
|
205
|
+
return JSON.parse(await spec.execute(args || {}, { agent: agent || root }))
|
|
206
|
+
}
|
|
207
|
+
await call('vibe_math_new_project', { name: 'proj' })
|
|
208
|
+
// 200ms is the sanitizer floor; verifierCount=2 (the sanitizer floor too) keeps each
|
|
209
|
+
// verification exactly two children, so a round settles predictably.
|
|
210
|
+
await call('vibe_math_set_params', { tickIntervalMs: 200, verifierCount: 2 })
|
|
211
|
+
const c = Object.assign(h, { WS, root, call, label })
|
|
212
|
+
hosts.push(c)
|
|
213
|
+
return c
|
|
214
|
+
}
|
|
215
|
+
// The scheduler only picks objects up while it is RUNNING (scheduleTick early-returns).
|
|
216
|
+
async function startScheduler(h) { await h.call('vibe_math_start', {}) }
|
|
217
|
+
|
|
218
|
+
// ── test speed: fast-forward the SCHEDULER POLL (test-only, no production impact) ─────────
|
|
219
|
+
// The plugin registers its scheduler poll as `setInterval(..., 1000)` at apply() time, so every
|
|
220
|
+
// suite-side `tick()` had to wait a full wall-clock second. With ~9 verification rounds × ~17
|
|
221
|
+
// ticks that alone accounted for ~99% of this suite's 186 s (its v3/v4/v5 siblings take seconds).
|
|
222
|
+
// Patching ONLY setInterval (the suite's own `sleep` uses setTimeout) makes a poll cost ~25 ms,
|
|
223
|
+
// while the plugin's due-ness logic still uses the real 200 ms `tickIntervalMs` floor — no
|
|
224
|
+
// scheduler behaviour depends on the poll period, and `tickTheScheduler` below stays above it.
|
|
225
|
+
const REAL_SET_INTERVAL = globalThis.setInterval
|
|
226
|
+
globalThis.setInterval = function (fn, ms, ...rest) {
|
|
227
|
+
return REAL_SET_INTERVAL(fn, Math.min(Number(ms) || 0, 25), ...rest)
|
|
228
|
+
}
|
|
229
|
+
// ── test speed / leak check: track OUTSTANDING setTimeout handles (test-only) ──────────────
|
|
230
|
+
// `leanRunFile` races `handle.done` against its own `cap`-ms timer; forgetting to clear that timer on
|
|
231
|
+
// the normal path leaves one pending multi-minute timer PER RUN — a leak no output assertion can see.
|
|
232
|
+
// The plugin resolves the global at call time, so wrapping it here covers it. The mock's own bounded
|
|
233
|
+
// fallback uses the REAL functions, so it never pollutes the count.
|
|
234
|
+
const REAL_SET_TIMEOUT = globalThis.setTimeout
|
|
235
|
+
const REAL_CLEAR_TIMEOUT = globalThis.clearTimeout
|
|
236
|
+
const liveTimers = new Set()
|
|
237
|
+
globalThis.setTimeout = function (fn, ms, ...rest) {
|
|
238
|
+
let h
|
|
239
|
+
h = REAL_SET_TIMEOUT(function (...a) { liveTimers.delete(h); return fn.apply(this, a) }, ms, ...rest)
|
|
240
|
+
liveTimers.add(h)
|
|
241
|
+
return h
|
|
242
|
+
}
|
|
243
|
+
globalThis.clearTimeout = function (h) { liveTimers.delete(h); return REAL_CLEAR_TIMEOUT(h) }
|
|
244
|
+
|
|
245
|
+
const projRoot = (h) => join(h.WS, 'VibeMath', 'Projects', 'proj')
|
|
246
|
+
const vibeRoot = (h) => join(h.WS, 'VibeMath')
|
|
247
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
248
|
+
const formalStateOf = (h) => JSON.parse(readIf(join(projRoot(h), 'VibeMath_State', 'formal.json')) || '{}')
|
|
249
|
+
// One scheduler pass. With the poll fast-forwarded above, the poll fires every ~25 ms and a tick
|
|
250
|
+
// runs whenever the plugin's own 200 ms `tickIntervalMs` floor has elapsed, so 260 ms is one pass.
|
|
251
|
+
const tick = (ms = 260) => sleep(ms)
|
|
252
|
+
async function waitFor(pred, tries = 80, ms = 60) {
|
|
253
|
+
for (let i = 0; i < tries; i++) { const v = pred(); if (v) return v; await sleep(ms) }
|
|
254
|
+
return undefined
|
|
255
|
+
}
|
|
256
|
+
const verifiersOf = (h, rKind, exclude) => h.spawns.filter((s) => s.label.startsWith('verifier:' + rKind) && !(exclude || []).some((o) => o.childId === s.childId))
|
|
257
|
+
/** A reply exactly as an agent would emit it (a fenced JSON block) — the framework's real input. */
|
|
258
|
+
const fence = (obj) => '```json\n' + JSON.stringify(obj) + '\n```'
|
|
259
|
+
/** Feed ONE agent reply (a fresh turn's end) to the framework through the real dispatch path. */
|
|
260
|
+
const replyFrom = (h, childId, obj) => h.fireEnd({
|
|
261
|
+
id: childId, runId: 'r-' + childId, provider: 'spawn', local: true, stopReason: 'completed',
|
|
262
|
+
lastAssistantMessage: [{ type: 'text', text: fence(obj) }],
|
|
263
|
+
})
|
|
264
|
+
const fireVerdicts = (h, kids, vale) => {
|
|
265
|
+
for (let i = 0; i < kids.length; i++) {
|
|
266
|
+
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```' }] })
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Verify one object and DRIVE IT TO A SETTLEMENT.
|
|
271
|
+
*
|
|
272
|
+
* Important: v2's `processVerify` starts at most ONE verification and skips every candidate
|
|
273
|
+
* while a task is live, so leaving an object mid-debate would silently block the NEXT case's
|
|
274
|
+
* verification. The first `firstRounds` rounds are answered with `vale` (a non-unanimous value
|
|
275
|
+
* forces a real debate round, which is exactly what the debate-prompt assertions need), and any
|
|
276
|
+
* later round is answered with `settle` so the object actually closes.
|
|
277
|
+
*/
|
|
278
|
+
async function verifyWithDebate(h, rKind, vale, firstRounds = 1, settle = 1) {
|
|
279
|
+
// v2 re-wakes the SAME verifier children for every debate round (it does not spawn new
|
|
280
|
+
// ones), so a new round is detected by counting the followups written to those children.
|
|
281
|
+
const kids = new Set()
|
|
282
|
+
// Early exit: a settled object stops producing followups. Without this the loop always spent
|
|
283
|
+
// its FULL 16 passes (~21 s at the old 1.3 s/pass), which is what made this suite take 3
|
|
284
|
+
// minutes: the debate is over, but the helper kept ticking at nothing. Three consecutive
|
|
285
|
+
// passes with no new followup = the framework has nothing left to ask.
|
|
286
|
+
let lastN = -1
|
|
287
|
+
let quiet = 0
|
|
288
|
+
for (let i = 0; i < 16; i++) {
|
|
289
|
+
// The scheduler may need a whole pass before it notices the object and another before it
|
|
290
|
+
// fills the verifier quota, so each wait must span several timer ticks.
|
|
291
|
+
const cand = await waitFor(() => {
|
|
292
|
+
const x = verifiersOf(h, rKind)
|
|
293
|
+
return x.length >= 2 ? x : undefined
|
|
294
|
+
}, 40, 250)
|
|
295
|
+
if (!cand) break
|
|
296
|
+
cand.forEach((s) => kids.add(s.childId))
|
|
297
|
+
const rounds = 1 + h.followups.filter((f) => kids.has(f.childId)).length
|
|
298
|
+
// `fireEnd` is a NO-OP for a child with no in-flight turn, so answering the whole current
|
|
299
|
+
// verifier set each pass is safe; the extra answers land before the next round's wakes.
|
|
300
|
+
fireVerdicts(h, cand, rounds <= firstRounds ? vale : settle)
|
|
301
|
+
await sleep(200)
|
|
302
|
+
await tick()
|
|
303
|
+
if (h.followups.length === lastN) { if (++quiet >= 3) break } else { quiet = 0; lastN = h.followups.length }
|
|
304
|
+
}
|
|
305
|
+
const rounds = 1 + h.followups.filter((f) => kids.has(f.childId)).length
|
|
306
|
+
return { first: h.spawns.filter((s) => s.label.startsWith('verifier:' + rKind)), rounds: kids.size ? rounds : 0 }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ===============================================================
|
|
310
|
+
console.log('-- V2 Lean formal verification --')
|
|
311
|
+
|
|
312
|
+
// ---------- 1. 'off' is a true no-op ----------
|
|
313
|
+
section("1 'off' (default) is a true no-op")
|
|
314
|
+
{
|
|
315
|
+
const h = await makeCase('off')
|
|
316
|
+
const st = await h.call('vibe_math_status', {})
|
|
317
|
+
assert(st.params.formalVerify === 'off', "the default mode is 'off' (got " + st.params.formalVerify + ')')
|
|
318
|
+
assert(st.formal.mode === 'off' && st.formal.required === false, 'status exposes the formal mode and gate flag')
|
|
319
|
+
assert(st.params.leanCommand === 'lean' && Array.isArray(st.params.leanArgs), 'the Lean knobs are readable in status.params')
|
|
320
|
+
assert(st.params.leanTimeoutMs === 120000, 'leanTimeoutMs defaults to 120000 (got ' + st.params.leanTimeoutMs + ')')
|
|
321
|
+
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'),
|
|
322
|
+
'the three Lean tools are registered in every mode (registration is static)')
|
|
323
|
+
// ★ The mode switch must be REACHABLE THROUGH THE TOOL SCHEMA (2.3.2 defect D1) ──────────────
|
|
324
|
+
// Every tool schema here is closed (`additionalProperties:false`), so a key the schema does not
|
|
325
|
+
// advertise is REJECTED by any schema-validating provider. v3 shipped 2.3.0/2.3.1 with all four Lean
|
|
326
|
+
// parameters missing from the set-params schema while every assertion in this file stayed green —
|
|
327
|
+
// because the suite calls the handler DIRECTLY and never inspects the registered schema. The feature
|
|
328
|
+
// could not be switched on at all through the tool interface.
|
|
329
|
+
{
|
|
330
|
+
const setSpec = h.toolRegs.find((t) => t.name === 'vibe_math_set_params')
|
|
331
|
+
assert(!!setSpec, "vibe_math_set_params is registered")
|
|
332
|
+
assert(setSpec.parameters && setSpec.parameters.type === 'object' && setSpec.parameters.additionalProperties === false,
|
|
333
|
+
'★ vibe_math_set_params publishes a CLOSED object schema (an unlisted key is rejected, so the schema IS the contract)')
|
|
334
|
+
for (const k of ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']) {
|
|
335
|
+
assert(Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k),
|
|
336
|
+
'★ the registered schema advertises ' + k + ' (every other surface documents it; a schema that omits it makes the switch unreachable)')
|
|
337
|
+
}
|
|
338
|
+
assert(JSON.stringify(setSpec.parameters.properties.formalVerify.enum) === JSON.stringify(['off', 'encourage', 'require']),
|
|
339
|
+
'the schema narrows formalVerify to the three real modes (a typo must not be a fourth)')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
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')
|
|
343
|
+
assert(existsSync(join(projRoot(h), 'Formal')) && existsSync(join(projRoot(h), 'Verified', 'Lean')), 'the project Formal/ and Verified/Lean/ dirs are created')
|
|
344
|
+
await h.call('vibe_math_add_problem', { id: 'q1', description: 'off 模式无操作测试' })
|
|
345
|
+
await startScheduler(h)
|
|
346
|
+
await tick(2000)
|
|
347
|
+
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:q1')), 60, 200)
|
|
348
|
+
assert(!!ex, 'the explorer was spawned (the scheduler really is running)')
|
|
349
|
+
assert(!!ex && !/Lean|形式化/.test(ex.prompt || ''), 'the explorer prompt contains no Lean/形式化 text in off mode')
|
|
350
|
+
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```' }] })
|
|
351
|
+
await sleep(300)
|
|
352
|
+
const so = await waitFor(() => h.spawns.find((s) => s.label.startsWith('solver:q1')), 40, 200)
|
|
353
|
+
assert(!!so, 'the solver was spawned after the explorer returned directions')
|
|
354
|
+
assert(!!so && !/Lean|形式化/.test(so.prompt || ''), 'the solver prompt contains no Lean/形式化 text in off mode')
|
|
355
|
+
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```' }] })
|
|
356
|
+
const vs = await verifyWithDebate(h, 'r-q1-s0', 1)
|
|
357
|
+
assert(!!vs.first && vs.first.length >= 2, 'verifiers were spawned for the solution (' + (vs.first ? vs.first.length : 0) + ')')
|
|
358
|
+
const vp = vs.first ? (h.spawns.find((s) => s.label === 'verifier:r-q1-s0:0').prompt || '') : ''
|
|
359
|
+
assert(!/Lean|形式化/.test(vp), 'the review prompt contains no Lean/形式化 text in off mode')
|
|
360
|
+
const debateText = h.followups.map((f) => f.prompt || '').join('\n')
|
|
361
|
+
assert(!/Lean|形式化/.test(debateText), 'no debate/followup prompt mentions Lean in off mode')
|
|
362
|
+
const qs = JSON.parse(readIf(join(projRoot(h), 'qs', 'qs.json')) || '[]')
|
|
363
|
+
const q = qs.find((x) => x.id === 'q1')
|
|
364
|
+
assert(!!q && q.已解决 === true, "'off' still finalizes: the problem is marked solved with NO Lean artifact")
|
|
365
|
+
assert(!!q && q.解法列表 && q.解法列表[0].正确概率 === 1, 'the solution probability is 1 in off mode')
|
|
366
|
+
assert(existsSync(join(projRoot(h), 'Verified', '问题_Verified.json')), 'the Verified card was written in off mode')
|
|
367
|
+
assert(!/形式化/.test(readIf(join(projRoot(h), 'Verified', '问题_Verified.json'))), 'the card carries no formal line in off mode')
|
|
368
|
+
assert(!existsSync(join(projRoot(h), 'Formal', 'TODO.md')), 'no Formal/TODO.md is produced in off mode')
|
|
369
|
+
const fs0 = formalStateOf(h)
|
|
370
|
+
assert(!fs0.records || Object.keys(fs0.records).length === 0, 'off mode records no formal object state')
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---------- 2. parameter validation + runtime switching ----------
|
|
374
|
+
section('2 parameter validation and runtime switching')
|
|
375
|
+
{
|
|
376
|
+
const h = await makeCase('params')
|
|
377
|
+
const bad = await h.call('vibe_math_set_params', { formalVerify: 'banana' })
|
|
378
|
+
assert(bad.params.formalVerify === 'off', "an unknown mode degrades to 'off', never to a stronger mode (got " + bad.params.formalVerify + ')')
|
|
379
|
+
const enc = await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
380
|
+
assert(enc.params.formalVerify === 'encourage', "'encourage' is accepted")
|
|
381
|
+
const req = await h.call('vibe_math_set_params', { formalVerify: 'require', leanTimeoutMs: -5, leanCommand: ' ' })
|
|
382
|
+
assert(req.params.formalVerify === 'require', "'require' is accepted")
|
|
383
|
+
assert(req.params.leanTimeoutMs === 120000, 'a non-positive leanTimeoutMs falls back to the default (' + req.params.leanTimeoutMs + ')')
|
|
384
|
+
assert(req.params.leanCommand === 'lean', 'a blank leanCommand falls back to "lean"')
|
|
385
|
+
const lake = await h.call('vibe_math_set_params', { leanCommand: 'lake', leanArgs: ['env', 'lean'] })
|
|
386
|
+
assert(lake.params.leanCommand === 'lake' && lake.params.leanArgs.join(' ') === 'env lean', 'leanCommand/leanArgs are settable (lake env lean)')
|
|
387
|
+
const st = await h.call('vibe_math_status', {})
|
|
388
|
+
assert(st.formal.leanCommand === 'lake' && st.formal.leanArgs.join(' ') === 'env lean', 'the Lean knobs round-trip through status')
|
|
389
|
+
const setup = await h.call('vibe_math_setup', {})
|
|
390
|
+
const names = setup.parameters.map((p) => p.name)
|
|
391
|
+
assert(['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs'].every((n) => names.indexOf(n) !== -1), 'vibe_math_setup schema lists all four Lean knobs')
|
|
392
|
+
const fv = setup.parameters.find((p) => p.name === 'formalVerify')
|
|
393
|
+
assert(!!fv && fv.current === 'require' && fv.default === 'off', 'the schema reports the current and default formalVerify')
|
|
394
|
+
const tpl = await h.call('vibe_math_template', { where: 'global' })
|
|
395
|
+
const tplText = readIf(tpl.path)
|
|
396
|
+
assert(/formalVerify/.test(tplText) && /leanCommand/.test(tplText) && /leanTimeoutMs/.test(tplText), 'the settings template documents the Lean knobs')
|
|
397
|
+
assert(/leanArgs/.test(tplText), 'the settings template also documents leanArgs')
|
|
398
|
+
assert(!/:\s*undefined/.test(tplText), 'the template has no bare undefined token (settings round-trip safety)')
|
|
399
|
+
const saved = await h.call('vibe_math_save_settings', {})
|
|
400
|
+
assert(saved.ok === true, 'save_settings still round-trips with the new knobs')
|
|
401
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
402
|
+
await h.call('vibe_math_add_problem', { id: 'q2', description: '模式切换测试' })
|
|
403
|
+
await startScheduler(h)
|
|
404
|
+
await tick(2000)
|
|
405
|
+
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:q2')), 60, 200)
|
|
406
|
+
assert(!!ex && /【顺手形式化(鼓励)】/.test(ex.prompt || ''), '★ switching to encourage changes the NEXT prompt immediately')
|
|
407
|
+
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```' }] })
|
|
408
|
+
await h.call('vibe_math_set_params', { formalVerify: 'off' })
|
|
409
|
+
await sleep(400)
|
|
410
|
+
const so = await waitFor(() => h.spawns.find((s) => s.label.startsWith('solver:q2')), 40, 200)
|
|
411
|
+
assert(!!so, 'the solver was spawned after the mode was switched back to off')
|
|
412
|
+
assert(!!so && !/形式化|Lean/.test(so.prompt || ''), '★ switching back to off removes the Lean text from the next prompt')
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ---------- 3. 'encourage' injection ----------
|
|
416
|
+
section("3 'encourage' injects the Lean section into review AND debate prompts")
|
|
417
|
+
{
|
|
418
|
+
const h = await makeCase('enc')
|
|
419
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage', maxParallelThreshold: 8 })
|
|
420
|
+
// Keep the scheduler alive: v2 STOPS itself when there is no unsolved problem, no agent and
|
|
421
|
+
// no task, and a stopped scheduler never starts a verification (nor spawns an explorer).
|
|
422
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
423
|
+
await startScheduler(h)
|
|
424
|
+
await h.call('vibe_math_add_proposition', { id: 'pEnc', 概述: '鼓励模式下的忠实性审查', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
425
|
+
const vs = await verifyWithDebate(h, 'r-pEnc', 0.5, 1) // 0.5 -> no consensus -> a REAL debate round
|
|
426
|
+
assert(!!vs.first, 'verifiers spawned for the bare proposition')
|
|
427
|
+
assert(vs.rounds >= 2, 'the object really went through a debate round (' + vs.rounds + ' rounds)')
|
|
428
|
+
const review = h.spawns.find((s) => s.label === 'verifier:r-pEnc:0')
|
|
429
|
+
assert(!!review, 'the first review prompt was captured')
|
|
430
|
+
const reviewText = review ? review.prompt : ''
|
|
431
|
+
assert(/【Lean 形式化验证(鼓励模式)】/.test(reviewText), '★ the REVIEW prompt carries the Lean section')
|
|
432
|
+
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(reviewText), 'the review prompt states that a passing Lean run shrinks the question to fidelity')
|
|
433
|
+
assert(/实现难度/.test(reviewText), 'the review prompt asks for the implementation-difficulty judgement')
|
|
434
|
+
// NOTE: this assertion is only the WORDING half. Which is exactly the trap the first version
|
|
435
|
+
// of this feature fell into (a green suite guarding a dead channel). The behaviour — the reply
|
|
436
|
+
// really landing in the durable record — is asserted in section 11.
|
|
437
|
+
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(reviewText), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
438
|
+
assert(/"decision":"used\|blocked\|defect"/.test(reviewText), '★ the review contract lists the real decision enum (incl. defect)')
|
|
439
|
+
assert(/vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库)/.test(reviewText), 'the review prompt names the three v2 tools')
|
|
440
|
+
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')
|
|
441
|
+
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)')
|
|
442
|
+
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
443
|
+
assert(/DEBATE/.test(debate), 'the debate round actually happened (a followup with the debate prompt was issued)')
|
|
444
|
+
assert(/【Lean 形式化验证(鼓励模式)】/.test(debate), '★ the DEBATE prompt carries the Lean section too')
|
|
445
|
+
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(debate), 'the debate prompt states the fidelity shift')
|
|
446
|
+
await h.call('vibe_math_add_problem', { id: 'qW', description: '顺手形式化测试' })
|
|
447
|
+
await tick(2000)
|
|
448
|
+
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:qW')), 60, 200)
|
|
449
|
+
assert(!!ex && /【顺手形式化(鼓励)】/.test(ex.prompt || ''), '★ the explorer work prompt carries the 顺手形式化 line')
|
|
450
|
+
assert(!!ex && /vibe_math_lean_archive kind='def'/.test(ex.prompt || ''), 'the work line points at the archive tool for reusable definitions')
|
|
451
|
+
assert(!!ex && /vibe_math_lean_lib 查重/.test(ex.prompt || ''), 'the work line tells members to check the reuse library first')
|
|
452
|
+
assert(!!ex && /归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。/.test(ex.prompt || ''), '★ the work line forbids archiving a definition that has not run green')
|
|
453
|
+
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)')
|
|
454
|
+
// Every agent-facing string must name the REGISTERED tools: an abbreviated `lean_archive`
|
|
455
|
+
// is not a tool that exists, and agents copy these literals verbatim (contract §6 hard req. 1).
|
|
456
|
+
const workText = (ex.prompt || '') + '\n' + reviewText + '\n' + debate
|
|
457
|
+
assert(!/(^|[^a-z_])lean_(run|archive|lib)/.test(workText), '★ no injected prompt names an abbreviated tool (every occurrence is prefixed)')
|
|
458
|
+
const offHost = await makeCase('enc-off')
|
|
459
|
+
await offHost.call('vibe_math_add_problem', { id: 'qN', description: 'x' })
|
|
460
|
+
await startScheduler(offHost)
|
|
461
|
+
await tick(2000)
|
|
462
|
+
const exN = await waitFor(() => offHost.spawns.find((s) => s.label.startsWith('explorer:qN')), 60, 200)
|
|
463
|
+
assert(!!exN && !/形式化|Lean/.test(exN.prompt || ''), 'the same prompt in off mode is free of Lean text (injection is mode-computed, not frozen)')
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ---------- 4. the run tool ----------
|
|
467
|
+
section('4 lean_run executes through the subprocess service and reports honestly')
|
|
468
|
+
{
|
|
469
|
+
const h = await makeCase('run')
|
|
470
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
471
|
+
const proj = projRoot(h)
|
|
472
|
+
mkdirSync(join(proj, 'Formal'), { recursive: true })
|
|
473
|
+
writeFileSync(join(proj, 'Formal', 'good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
474
|
+
writeFileSync(join(proj, 'Formal', 'bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
|
|
475
|
+
const before = leanRuns.length
|
|
476
|
+
const runGood = await h.call('vibe_math_lean_run', { file: 'Formal/good.lean', target: 'pRun' })
|
|
477
|
+
assert(runGood.ok === true && runGood.exitCode === 0, 'a file with no sorry runs green (' + JSON.stringify({ ok: runGood.ok, exitCode: runGood.exitCode }) + ')')
|
|
478
|
+
assert(leanRuns.length > before, 'the run really went through the mocked subprocess service')
|
|
479
|
+
const spec = leanRuns[leanRuns.length - 1]
|
|
480
|
+
assert(spec.cwd.replace(/\\/g, '/') === proj.replace(/\\/g, '/'), 'the toolchain runs with the PROJECT root as cwd (got ' + spec.cwd + ')')
|
|
481
|
+
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')
|
|
482
|
+
assert(typeof spec.graceMs === 'number' && spec.graceMs >= 1000, 'the configured timeout is passed as graceMs (' + spec.graceMs + ')')
|
|
483
|
+
assert(typeof runGood.command === 'string' && runGood.command.indexOf('good.lean') !== -1, 'the result reports the exact command line')
|
|
484
|
+
const runBad = await h.call('vibe_math_lean_run', { file: 'Formal/bad.lean' })
|
|
485
|
+
assert(runBad.ok === false && runBad.exitCode === 1, 'a file that still uses sorry reports a red run')
|
|
486
|
+
assert(/sorry/.test(runBad.stderr), 'the compiler output is returned verbatim (' + JSON.stringify(runBad.stderr).slice(0, 60) + ')')
|
|
487
|
+
assert(/修复后重跑/.test(runBad.hint || ''), 'a red run tells the member to fix and rerun')
|
|
488
|
+
const runMissing = await h.call('vibe_math_lean_run', { file: 'Formal/nope.lean' })
|
|
489
|
+
assert(runMissing.ok === false && runMissing.code === 'V2_NOT_FOUND', 'a missing file is refused with a typed code (' + runMissing.code + ')')
|
|
490
|
+
const runNoFile = await h.call('vibe_math_lean_run', {})
|
|
491
|
+
assert(runNoFile.ok === false && runNoFile.code === 'V2_INVALID_ARGUMENT', 'a run without `file` is refused')
|
|
492
|
+
// The guard's boundary is the VibeMath ROOT, not the project: the global reuse library
|
|
493
|
+
// deliberately lives at <VibeMath>/Formal/{Lib,Proved}, so climbing out of the project but
|
|
494
|
+
// staying inside VibeMath is legal (it just fails as a missing file).
|
|
495
|
+
const runOutOfProject = await h.call('vibe_math_lean_run', { file: '../../Formal/Lib/x.lean' })
|
|
496
|
+
assert(runOutOfProject.ok === false && runOutOfProject.code === 'V2_NOT_FOUND',
|
|
497
|
+
'climbing out of the project but staying inside the VibeMath root is ALLOWED (got ' + runOutOfProject.code + ')')
|
|
498
|
+
const runEscape = await h.call('vibe_math_lean_run', { file: '../../../../etc/evil.lean' })
|
|
499
|
+
assert(runEscape.ok === false && runEscape.code === 'V2_INVALID_ARGUMENT', '★ a traversal that climbs ABOVE the VibeMath root is refused')
|
|
500
|
+
const runEscape2 = await h.call('vibe_math_lean_run', { file: 'Formal/../../../../../evil.lean' })
|
|
501
|
+
assert(runEscape2.ok === false && runEscape2.code === 'V2_INVALID_ARGUMENT', 'a deeper traversal is refused too')
|
|
502
|
+
const runEscape3 = await h.call('vibe_math_lean_run', { file: '/etc/evil.lean' })
|
|
503
|
+
assert(runEscape3.ok === false && runEscape3.code === 'V2_INVALID_ARGUMENT', 'an unrelated absolute path is refused')
|
|
504
|
+
const runEscape4 = await h.call('vibe_math_lean_run', { file: 'C:/Windows/evil.lean' })
|
|
505
|
+
assert(runEscape4.ok === false && runEscape4.code === 'V2_INVALID_ARGUMENT', 'an unrelated Windows absolute path is refused')
|
|
506
|
+
const runNotLean = await h.call('vibe_math_lean_run', { file: 'Formal/good.txt' })
|
|
507
|
+
assert(runNotLean.ok === false && runNotLean.code === 'V2_INVALID_ARGUMENT', 'only .lean files can be executed')
|
|
508
|
+
assert(/VibeMath/.test(runEscape.message || ''), 'the refusal names the boundary it enforced')
|
|
509
|
+
mkdirSync(join(vibeRoot(h), 'Formal', 'Lib'), { recursive: true })
|
|
510
|
+
writeFileSync(join(vibeRoot(h), 'Formal', 'Lib', 'abs.lean'), 'def absTest := 1\n', 'utf8')
|
|
511
|
+
const runAbs = await h.call('vibe_math_lean_run', { file: join(vibeRoot(h), 'Formal', 'Lib', 'abs.lean').replace(/\\/g, '/') })
|
|
512
|
+
assert(runAbs.ok === true, 'an absolute path inside the VibeMath root is accepted')
|
|
513
|
+
const rec = formalStateOf(h)
|
|
514
|
+
assert(!!rec.records && !!rec.records.pRun && rec.records.pRun.status === 'attempted', 'a green lean_run records the object as attempted (stored, not promoted)')
|
|
515
|
+
assert(!!rec.records.pRun.run && rec.records.pRun.run.ok === true && rec.records.pRun.run.exitCode === 0, 'the run record keeps ok/exitCode')
|
|
516
|
+
assert(rec.records.pRun.run.stdoutTail !== undefined && rec.records.pRun.run.stderrTail !== undefined, 'the run record keeps truncated output tails')
|
|
517
|
+
toolchainAvailable = false
|
|
518
|
+
const runNoTc = await h.call('vibe_math_lean_run', { file: 'Formal/good.lean' })
|
|
519
|
+
assert(runNoTc.ok === false && runNoTc.code === 'LEAN_NOT_FOUND', 'a missing toolchain returns LEAN_NOT_FOUND instead of crashing')
|
|
520
|
+
assert(/仍可把形式化代码写下来归档/.test(runNoTc.message), 'the failure explains the graceful degradation')
|
|
521
|
+
toolchainAvailable = true
|
|
522
|
+
const noSub = await makeCase('nosub', { noSubprocess: true })
|
|
523
|
+
mkdirSync(join(projRoot(noSub), 'Formal'), { recursive: true })
|
|
524
|
+
writeFileSync(join(projRoot(noSub), 'Formal', 'x.lean'), 'theorem x : 1 = 1 := rfl\n', 'utf8')
|
|
525
|
+
const noSubRun = await noSub.call('vibe_math_lean_run', { file: 'Formal/x.lean' })
|
|
526
|
+
assert(noSubRun.ok === false && noSubRun.code === 'NO_SUBPROCESS', 'a host without the subprocess service returns NO_SUBPROCESS and does not crash (got ' + noSubRun.code + ')')
|
|
527
|
+
const noSubStatus = await noSub.call('vibe_math_status', {})
|
|
528
|
+
assert(noSubStatus.ok === true, 'the scheduler still answers status after that (nothing was thrown into the loop)')
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ---------- 4b. the timeout must STOP the process, not just report it ----------
|
|
532
|
+
// Contract §7: "必须给 cwd…,并对超时调用 handle.terminate()". Reporting LEAN_TIMEOUT while the Lean
|
|
533
|
+
// process keeps running is a silent resource leak, and the framework's own contract says otherwise.
|
|
534
|
+
section('4b lean_run ACTIVELY terminates on timeout (contract §7)')
|
|
535
|
+
{
|
|
536
|
+
const h = await makeCase('timeout')
|
|
537
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
538
|
+
const proj = projRoot(h)
|
|
539
|
+
mkdirSync(join(proj, 'Formal'), { recursive: true })
|
|
540
|
+
writeFileSync(join(proj, 'Formal', 'hang.lean'), 'theorem t : 1 = 1 := rfl -- HANG\n', 'utf8')
|
|
541
|
+
writeFileSync(join(proj, 'Formal', 'fast.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
542
|
+
const before = terminations.length
|
|
543
|
+
const t0 = Date.now()
|
|
544
|
+
const run = await h.call('vibe_math_lean_run', { file: 'Formal/hang.lean', timeout_ms: 1000 })
|
|
545
|
+
const elapsed = Date.now() - t0
|
|
546
|
+
assert(run.ok === false && run.code === 'LEAN_TIMEOUT', '★ a run that outlives its timeout is reported as LEAN_TIMEOUT (got ' + run.code + ')')
|
|
547
|
+
assert(run.timedOut === true, 'the result carries timedOut=true')
|
|
548
|
+
assert(terminations.length === before + 1 && /hang\.lean/.test(terminations[terminations.length - 1].file),
|
|
549
|
+
'★★ the timeout ACTIVELY called handle.terminate() (graceMs alone does not stop a lingering Lean process)')
|
|
550
|
+
assert(elapsed < 2400, 'the call returned AT its timeout instead of waiting the process out (took ' + elapsed + 'ms)')
|
|
551
|
+
const fast = await h.call('vibe_math_lean_run', { file: 'Formal/fast.lean' })
|
|
552
|
+
assert(fast.ok === true && fast.timedOut === false, 'a normal run still reports success')
|
|
553
|
+
assert(terminations.length === before + 1, 'a normal run terminates nothing')
|
|
554
|
+
await sleep(60)
|
|
555
|
+
assert(liveTimers.size === 0, '★ no timeout timer is left pending after either run (it is cleared as soon as `done` wins)')
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ---------- 5. archive: def / lemma / proof / blocked ----------
|
|
559
|
+
section('5 lean_archive writes the contract paths and indexes')
|
|
560
|
+
{
|
|
561
|
+
const h = await makeCase('arc')
|
|
562
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage' })
|
|
563
|
+
const proj = projRoot(h)
|
|
564
|
+
const libPath = join(vibeRoot(h), 'Formal', 'Lib')
|
|
565
|
+
const provedPath = join(vibeRoot(h), 'Formal', 'Proved')
|
|
566
|
+
const defRes = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'ZMod5', content: 'def ZMod5 := Fin 5\n' })
|
|
567
|
+
assert(defRes.ok === true && defRes.file === 'Formal/Lib/ZMod5.lean', 'a reusable definition is archived to the global lib (' + defRes.file + ')')
|
|
568
|
+
assert(existsSync(join(libPath, 'ZMod5.lean')), '★ the definition exists under <VibeMath>/Formal/Lib/ (cross-project, NOT inside the project)')
|
|
569
|
+
assert(!existsSync(join(proj, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the project tree')
|
|
570
|
+
assert(!!defRes.run && defRes.run.ok === true, 'the archived definition was executed (kind=def runs by default)')
|
|
571
|
+
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' })
|
|
572
|
+
assert(lemmaRes.ok === true && lemmaRes.file === 'Formal/Proved/sq_odd.lean', 'a lemma is archived to the global Proved/ (' + lemmaRes.file + ')')
|
|
573
|
+
assert(existsSync(join(provedPath, 'sq_odd.lean')), 'the lemma exists under <VibeMath>/Formal/Proved/')
|
|
574
|
+
const defNoRun = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'quiet', content: 'def quiet := 2\n', run: false })
|
|
575
|
+
assert(defNoRun.ok === true && defNoRun.run === undefined, 'run:false archives without executing (still indexed)')
|
|
576
|
+
assert(/ZMod5/.test(readIf(join(libPath, 'Index.md'))), 'Lib/Index.md lists the new definition')
|
|
577
|
+
assert(/quiet/.test(readIf(join(libPath, 'Index.md'))), 'Lib/Index.md lists the run:false definition too')
|
|
578
|
+
assert(/sq_odd/.test(readIf(join(provedPath, 'Index.md'))), 'Proved/Index.md lists the new lemma')
|
|
579
|
+
writeFileSync(join(proj, 'Formal', 'src.lean'), 'def copied := 3\n', 'utf8')
|
|
580
|
+
const defFrom = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'copied', from: 'Formal/src.lean' })
|
|
581
|
+
assert(defFrom.ok === true && existsSync(join(libPath, 'copied.lean')), 'kind=def can archive from an existing .lean file')
|
|
582
|
+
// The tool RESULT is agent-facing text too: a definition whose run just failed must not be advertised
|
|
583
|
+
// as "directly importable" (contract §6 hard req. 3 / 实现方案 §9.4 — a red file must not be presented
|
|
584
|
+
// as usable, whatever the framework decides to do with the file itself).
|
|
585
|
+
const defRed = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'polluted', content: 'def polluted := 1 -- FAIL\n' })
|
|
586
|
+
assert(defRed.ok === true && !!defRed.run && defRed.run.ok === false, 'a definition that does not compile is reported as a red run')
|
|
587
|
+
assert(!/可直接 import 复用/.test(defRed.note || ''), '★ a red definition must NOT be advertised as directly reusable')
|
|
588
|
+
assert(/运行未通过/.test(defRed.note || ''), '…and the note says what to do instead')
|
|
589
|
+
const fromOutside = await h.call('vibe_math_lean_archive', { kind: 'def', name: 'escape', from: '../../../../etc/passwd' })
|
|
590
|
+
assert(fromOutside.ok === false && fromOutside.code === 'V2_INVALID_ARGUMENT', 'from=<path outside the VibeMath root> is refused')
|
|
591
|
+
const noName = await h.call('vibe_math_lean_archive', { kind: 'def', content: 'def x := 1\n' })
|
|
592
|
+
assert(noName.ok === false && noName.code === 'V2_INVALID_ARGUMENT', 'archiving a definition without a name is refused')
|
|
593
|
+
const noBody = await h.call('vibe_math_lean_archive', { kind: 'lemma', name: 'xb' })
|
|
594
|
+
assert(noBody.ok === false && noBody.code === 'V2_INVALID_ARGUMENT', 'archiving a lemma with neither content nor from is refused')
|
|
595
|
+
const badKind = await h.call('vibe_math_lean_archive', { kind: 'nonsense' })
|
|
596
|
+
assert(badKind.ok === false && badKind.code === 'V2_INVALID_ARGUMENT', 'an unknown archive kind is refused')
|
|
597
|
+
const noTarget = await h.call('vibe_math_lean_archive', { kind: 'proof', content: 'theorem x : 1 = 1 := rfl\n' })
|
|
598
|
+
assert(noTarget.ok === false && noTarget.code === 'V2_INVALID_ARGUMENT', 'kind=proof without a target is refused')
|
|
599
|
+
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' })
|
|
600
|
+
assert(proof.ok === true && proof.passed === true, 'a passing proof is archived (' + JSON.stringify({ ok: proof.ok, passed: proof.passed }) + ')')
|
|
601
|
+
assert(proof.file === 'Formal/pProof.lean', 'the working file is Formal/<target>.lean')
|
|
602
|
+
assert(proof.proof === 'Verified/Lean/pProof.lean', 'the archived proof path is Verified/Lean/<target>.lean')
|
|
603
|
+
assert(existsSync(join(proj, 'Formal', 'pProof.lean')), 'the working file exists on disk')
|
|
604
|
+
assert(existsSync(join(proj, 'Verified', 'Lean', 'pProof.lean')), '★ the proof is archived under Verified/Lean/')
|
|
605
|
+
assert(proof.status === 'passed', 'the object status becomes passed')
|
|
606
|
+
const proofBad = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pRed', content: 'theorem p_red : 1 = 2 := by sorry\n' })
|
|
607
|
+
assert(proofBad.ok === true && proofBad.passed === false && proofBad.status === 'attempted', 'a red proof is recorded as attempted (not passed)')
|
|
608
|
+
assert(!existsSync(join(proj, 'Verified', 'Lean', 'pRed.lean')), 'a red proof is NOT archived under Verified/Lean/')
|
|
609
|
+
assert(/sorry/.test((proofBad.run && proofBad.run.stderr) || ''), 'the red run keeps the compiler output for the member to fix')
|
|
610
|
+
const st = await h.call('vibe_math_status', {})
|
|
611
|
+
assert(st.formal.objects.some((o) => o.target === 'pProof' && o.status === 'passed'), 'status lists the passed object')
|
|
612
|
+
const idx = readIf(join(proj, 'Formal', 'Index.md'))
|
|
613
|
+
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')
|
|
614
|
+
assert(/pRed/.test(idx) && /attempted/.test(idx), 'Formal/Index.md also records the failed attempt')
|
|
615
|
+
const blkNoNote = await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'pBlk' })
|
|
616
|
+
assert(blkNoNote.ok === false && blkNoNote.code === 'V2_INVALID_ARGUMENT', 'blocked without a note is refused')
|
|
617
|
+
const blk = await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'pBlk', note: '需要外层解析数论框架,本轮工作量不可接受' })
|
|
618
|
+
assert(blk.ok === true && blk.status === 'blocked', 'a reasoned blocker is recorded')
|
|
619
|
+
assert(/需要外层解析数论框架/.test(readIf(join(proj, 'Formal', 'Index.md'))), 'the blocker reason reaches the index')
|
|
620
|
+
const stBlk = await h.call('vibe_math_status', {})
|
|
621
|
+
assert(stBlk.formal.objects.some((o) => o.target === 'pBlk' && o.status === 'blocked'), 'status lists the blocked object')
|
|
622
|
+
const lib = await h.call('vibe_math_lean_lib', {})
|
|
623
|
+
assert(lib.ok === true && lib.counts.lib >= 3 && lib.counts.proved >= 1, 'lean_lib reports the reuse library sizes (' + JSON.stringify(lib.counts) + ')')
|
|
624
|
+
assert(lib.objects.some((o) => o.target === 'pProof' && o.status === 'passed'), 'lean_lib lists the per-object formal status')
|
|
625
|
+
assert(/复用优先/.test(lib.hint || ''), 'lean_lib tells agents to reuse before redefining')
|
|
626
|
+
assert(lib.mode === 'encourage', 'lean_lib reports the active mode')
|
|
627
|
+
const noRefresh = await h.call('vibe_math_lean_lib', { refresh: false })
|
|
628
|
+
assert(noRefresh.rebuilt === false && noRefresh.counts.lib === null, 'refresh:false lists without rebuilding')
|
|
629
|
+
assert(existsSync(join(proj, 'Formal', 'Index.md')), 'the project Formal/Index.md exists')
|
|
630
|
+
assert(existsSync(join(libPath, 'Index.md')) && existsSync(join(provedPath, 'Index.md')), 'the two GLOBAL indexes exist')
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ---------- 6. a passing proof flips the review to FIDELITY ----------
|
|
634
|
+
section('6 a passing proof flips the review subject to fidelity')
|
|
635
|
+
{
|
|
636
|
+
const h = await makeCase('fid')
|
|
637
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage', maxParallelThreshold: 8 })
|
|
638
|
+
// v2's scheduler STOPS itself when there is no unsolved problem, no agent and no task; a
|
|
639
|
+
// case that only wants to verify a proposition therefore needs one live problem to keep
|
|
640
|
+
// the tick loop alive (otherwise no verification would ever be started).
|
|
641
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
642
|
+
await startScheduler(h)
|
|
643
|
+
await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'r-pFid', content: 'theorem p_fid : 2 + 2 = 4 := by decide\n' })
|
|
644
|
+
await h.call('vibe_math_add_proposition', { id: 'pFid', 概述: '2+2=4(已有 Lean 证明)', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
645
|
+
// 0.5 in round 1 forces a debate round whose prompt must ALSO carry the fidelity wording
|
|
646
|
+
const vs = await verifyWithDebate(h, 'r-pFid', 0.5, 1)
|
|
647
|
+
assert(!!vs.first, 'a verifier was spawned for the Lean-passed object')
|
|
648
|
+
const vp = h.spawns.find((s) => s.label === 'verifier:r-pFid:0')
|
|
649
|
+
const vpText = vp ? vp.prompt : ''
|
|
650
|
+
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(vpText), 'the review prompt announces the passing proof')
|
|
651
|
+
assert(/你不需要重新检查推导/.test(vpText), '★ it tells reviewers NOT to re-derive')
|
|
652
|
+
assert(/忠实性审查/.test(vpText), '★ it tells reviewers the review subject is now fidelity')
|
|
653
|
+
assert(/定义 \/ 对象 \/ 条件 \/ 假设 \/ 结论是否与命题原文\*\*完全一致\*\*/.test(vpText), 'it enumerates exactly what fidelity means')
|
|
654
|
+
assert(/Verified\/Lean\/r-pFid\.lean/.test(vpText), 'it points at the archived proof')
|
|
655
|
+
assert(/一致 → Result = 1/.test(vpText), '★ the fidelity guidance names the REAL field (v2\'s contract field is Result)')
|
|
656
|
+
assert(!/verdict/.test(vpText), '★ the fidelity guidance never names a `verdict` field (that vote would be silently dropped)')
|
|
657
|
+
assert(/发现任何偏差,不要投 0/.test(vpText), '★ a fidelity defect is explicitly NOT to be voted as 0 (it is not a refutation)')
|
|
658
|
+
assert(/formal:\{decision:'defect'/.test(vpText), '★ the reviewers are given the defect reply channel that withdraws the proof')
|
|
659
|
+
assert(!/偏离 → 0/.test(vpText), '★ the "any deviation → 0" instruction is gone (it would fabricate a false conclusion)')
|
|
660
|
+
assert(!/请先判断该对象的\*\*实现难度\*\*/.test(vpText), 'the "judge the difficulty first" wording is gone when a proof already exists')
|
|
661
|
+
// The withdrawal sentence must match the MODE's real strength (contract §4.1 pt.3 / §6.1; this case is
|
|
662
|
+
// 'encourage'): encouraging mode has NO gate, so the framework cannot hold the verdict — promising a
|
|
663
|
+
// hold there is a lie the voter would rely on (AUDIT-CHECKLIST §1.7, "提示词承诺的强度档位").
|
|
664
|
+
assert(/本档没有门禁/.test(vpText), '★ encourage fidelity text says THIS MODE HAS NO GATE (the framework cannot hold the verdict)')
|
|
665
|
+
assert(!/本次裁定\*\*不定论\*\*/.test(vpText), '★ encourage must NOT promise a hold the framework cannot enforce')
|
|
666
|
+
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
667
|
+
assert(/你不需要重新检查推导/.test(debate), '★ the debate prompt for a Lean-passed object also asks for fidelity, not re-derivation')
|
|
668
|
+
assert(/发现任何偏差,不要投 0/.test(debate), '★ and it carries the same no-zero rule in the debate round')
|
|
669
|
+
assert(/本档没有门禁/.test(debate), '★ the debate round carries the same mode-qualified withdrawal wording')
|
|
670
|
+
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'r-pBlk2', note: '涉及未形式化的分析学前置' })
|
|
671
|
+
await h.call('vibe_math_add_proposition', { id: 'pBlk2', 概述: '已记录阻塞的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
672
|
+
const vs2 = await verifyWithDebate(h, 'r-pBlk2', 0.5, 1)
|
|
673
|
+
assert(!!vs2.first, 'verifiers spawned for the blocked object')
|
|
674
|
+
const vp2 = h.spawns.find((s) => s.label === 'verifier:r-pBlk2:0')
|
|
675
|
+
const vp2Text = vp2 ? vp2.prompt : ''
|
|
676
|
+
assert(/该对象已被记录为\*\*形式化阻塞\*\*/.test(vp2Text), 'a blocked object is announced as such')
|
|
677
|
+
assert(/涉及未形式化的分析学前置/.test(vp2Text), 'the blocker reason is shown to the reviewers')
|
|
678
|
+
assert(/这个阻塞判断是否成立/.test(vp2Text), 'the verdict guidance asks whether the blocker is justified')
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ---------- 7. the 'require' gate (proposition) ----------
|
|
682
|
+
section("7 'require' withholds a verdict until the formal record exists")
|
|
683
|
+
{
|
|
684
|
+
const h = await makeCase('gate')
|
|
685
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
686
|
+
// a live problem keeps the scheduler from stopping itself (see the comment in case 6)
|
|
687
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
688
|
+
await startScheduler(h)
|
|
689
|
+
const proj = projRoot(h)
|
|
690
|
+
await h.call('vibe_math_add_proposition', { id: 'pGate', 概述: '必须形式化的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
691
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pGate'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
692
|
+
assert(!!vs, 'verifiers were spawned under require mode')
|
|
693
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pGate:0') || {}).prompt || ''
|
|
694
|
+
assert(/【Lean 形式化验证(强制模式)】/.test(vp), 'the review prompt says 强制模式')
|
|
695
|
+
assert(/必须产出 Lean 形式化/.test(vp), "'require' states the formalization is mandatory")
|
|
696
|
+
assert(/本次裁定不会生效/.test(vp), 'the prompt warns that the verdict will not take effect without it')
|
|
697
|
+
assert(/formal-required/.test(vp), 'the prompt names the machine-readable reason')
|
|
698
|
+
assert(/vibe_math_lean_archive kind='blocked' note=… 或回执 formal\.note/.test(vp), '★ the require wording names BOTH blocking routes with the FULL tool name')
|
|
699
|
+
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')
|
|
700
|
+
assert(!/(^|[^a-z_])lean_(run|archive|lib)/.test(vp), '★ the require prompt contains no abbreviated tool name either')
|
|
701
|
+
fireVerdicts(h, vs, 1)
|
|
702
|
+
// The deferral writes Formal/TODO.md from INSIDE the settle path; waiting for that file is
|
|
703
|
+
// the observable proof that the round settled (the task is dropped right after).
|
|
704
|
+
const todoSettled = await waitFor(() => /pGate/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 40, 150)
|
|
705
|
+
assert(!!todoSettled, 'the deferred round settled (Formal/TODO.md recorded the object)')
|
|
706
|
+
await tick(600)
|
|
707
|
+
const propFile = join(proj, 'Propos', '数论_Propos.json')
|
|
708
|
+
const props = JSON.parse(readIf(propFile) || '[]')
|
|
709
|
+
const pGate = props.find((x) => x.id === 'pGate') || {}
|
|
710
|
+
assert(pGate.布尔估计 === 0.5, "★ the verdict did NOT take effect: the proposition's 布尔估计 is unchanged (got " + pGate.布尔估计 + ')')
|
|
711
|
+
assert(!(pGate.证明列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 proof entry was recorded')
|
|
712
|
+
assert(pGate.优先级 !== 'never', 'the priority was not pinned to never')
|
|
713
|
+
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), '★ no Verified card was written')
|
|
714
|
+
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
715
|
+
assert(existsSync(join(proj, 'Formal', 'TODO.md')), 'Formal/TODO.md was created')
|
|
716
|
+
assert(/pGate/.test(todo) && /formal-required/.test(todo), '★ the object is on the formalization TODO with the machine-readable reason')
|
|
717
|
+
const recAfterGate = formalStateOf(h)
|
|
718
|
+
// record key = the verification object id (rId), which is what lean_archive target= also uses
|
|
719
|
+
assert(!!recAfterGate.records && !!recAfterGate.records['r-pGate'] && !!recAfterGate.records['r-pGate'].deferredAt, 'the deferral is persisted in the durable formal state (survives resume)')
|
|
720
|
+
assert(!!recAfterGate.records['r-pGate'] && recAfterGate.records['r-pGate'].status === 'none', 'the object stays at status none (nothing was formalized)')
|
|
721
|
+
const vlogs = existsSync(join(proj, 'Verification_logs')) ? readdirSync(join(proj, 'Verification_logs')) : []
|
|
722
|
+
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')
|
|
723
|
+
const st1 = await h.call('vibe_math_status', {})
|
|
724
|
+
assert(/require 模式搁置/.test(JSON.stringify(st1.recentActivity)), 'the withholding is announced on v2-readable channel (activity log; v2 has no Shared/Chat/)')
|
|
725
|
+
assert(st1.formal.todo.some((t) => t.id === 'r-pGate'), 'status exposes the formalization TODO')
|
|
726
|
+
const rep = await h.call('vibe_math_report', {})
|
|
727
|
+
assert(!!rep.formal && rep.formal.required === true && rep.formal.mode === 'require', 'the report exposes the formal mode and gate flag')
|
|
728
|
+
assert(rep.recentActivity.some((a) => /formal-gate/.test(a.event)), 'the deferral also lands in the report log')
|
|
729
|
+
// The deferral leaves this project with no agent, task or solved problem, and v2's strict
|
|
730
|
+
// termination stops the scheduler there — so a case that wants to RE-verify the object after
|
|
731
|
+
// formalizing it must (re)start the scheduler, exactly as a human would.
|
|
732
|
+
await startScheduler(h)
|
|
733
|
+
const proofNow = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pGate', content: 'theorem p_gate : 2 + 2 = 4 := by decide\n' })
|
|
734
|
+
assert(proofNow.ok === true && proofNow.passed === true, 'the object is now Lean-passed')
|
|
735
|
+
const spawned2 = await verifyWithDebate(h, 'r-pGate', 1, 0)
|
|
736
|
+
assert(!!spawned2.first, 'the object was re-verified after the proof was archived')
|
|
737
|
+
const props2 = JSON.parse(readIf(propFile) || '[]')
|
|
738
|
+
const pGateNow = props2.find((x) => x.id === 'pGate') || {}
|
|
739
|
+
assert(pGateNow.布尔估计 === 1, '★ with a passing Lean artifact the same unanimous verdict DOES take effect (布尔估计=1)')
|
|
740
|
+
const cardFile = join(proj, 'Verified', '数论_Verified.json')
|
|
741
|
+
const cards = JSON.parse(readIf(cardFile) || '[]')
|
|
742
|
+
const card = cards.find((c) => c.id === 'pGate')
|
|
743
|
+
assert(!!card, '★ the Verified card was written after the proof passed')
|
|
744
|
+
assert(!!card && /Lean 通过/.test(card['形式化'] || ''), '★ the card records how strong the result is (Lean 通过)')
|
|
745
|
+
assert(!!card && /Verified\/Lean\/pGate\.lean/.test(card['形式化'] || ''), 'the card points at the archived proof')
|
|
746
|
+
await h.call('vibe_math_add_proposition', { id: 'pBlkOk', 概述: '记录阻塞后可定论', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
747
|
+
const sp3 = await waitFor(() => { const x = verifiersOf(h, 'r-pBlkOk'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
748
|
+
assert(!!sp3, 'verifiers spawned for the blocker-escape case')
|
|
749
|
+
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'r-pBlkOk', note: '命题涉及未形式化的分析学,本轮不做' })
|
|
750
|
+
fireVerdicts(h, sp3 || [], 1)
|
|
751
|
+
await waitFor(() => { const c = JSON.parse(readIf(cardFile) || '[]').find((x) => x.id === 'pBlkOk'); return c ? c : undefined }, 40, 150)
|
|
752
|
+
await tick(400)
|
|
753
|
+
const cards3 = JSON.parse(readIf(cardFile) || '[]')
|
|
754
|
+
const card3 = cards3.find((c) => c.id === 'pBlkOk')
|
|
755
|
+
assert(!!card3, '★ an explicit reasoned blocker also lets the verdict through (decide by difficulty, but decide out loud)')
|
|
756
|
+
assert(!!card3 && /阻塞(/.test(card3['形式化'] || ''), 'the card records the blocker')
|
|
757
|
+
assert(!!card3 && /未形式化的分析学/.test(card3['形式化'] || ''), 'the card records the blocker REASON, not just the fact')
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ---------- 8. the gate covers the qs (problem) choke point ----------
|
|
761
|
+
section('8 the gate also covers a problem-solution verdict')
|
|
762
|
+
{
|
|
763
|
+
const h = await makeCase('gq')
|
|
764
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
765
|
+
// a live problem keeps the scheduler from stopping itself (see the comment in case 3)
|
|
766
|
+
await h.call('vibe_math_add_problem', { id: 'qStay', description: '保持调度器运行的占位问题', priority: 9 })
|
|
767
|
+
await startScheduler(h)
|
|
768
|
+
const proj = projRoot(h)
|
|
769
|
+
await h.call('vibe_math_add_problem', { id: 'qG', description: '带解法的 require 门禁问题', priority: 1 })
|
|
770
|
+
const qsFile = join(proj, 'qs', 'qs.json')
|
|
771
|
+
const qs0 = JSON.parse(readIf(qsFile) || '[]')
|
|
772
|
+
qs0.find((q) => q.id === 'qG').解法列表 = [{ 完整解法: 'closing argument', 正确概率: 0.8, 已验: false }]
|
|
773
|
+
writeFileSync(qsFile, JSON.stringify(qs0, null, 2), 'utf8')
|
|
774
|
+
await tick(2000)
|
|
775
|
+
const sp = await waitFor(() => { const x = verifiersOf(h, 'r-qG-s0'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
776
|
+
assert(!!sp, 'verifiers spawned for the problem solution')
|
|
777
|
+
fireVerdicts(h, sp || [], 1)
|
|
778
|
+
const todoG = await waitFor(() => /qG/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 40, 150)
|
|
779
|
+
assert(!!todoG, 'the deferred problem round settled (Formal/TODO.md recorded it)')
|
|
780
|
+
await tick(600)
|
|
781
|
+
const qs1 = JSON.parse(readIf(qsFile) || '[]')
|
|
782
|
+
const qG = qs1.find((q) => q.id === 'qG')
|
|
783
|
+
assert(!!qG && qG.已解决 !== true, '★ the problem was NOT marked solved (the require gate blocked the promotion)')
|
|
784
|
+
assert(!!qG && qG.解法列表[0].正确概率 !== 1, "the solution's probability was left as it was")
|
|
785
|
+
assert(!existsSync(join(proj, 'Verified', '问题_Verified.json')), 'no problem Verified card was written')
|
|
786
|
+
assert(/qG/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 'the problem is on the formalization TODO')
|
|
787
|
+
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'qG', note: '问题的形式化超出本轮工作量' })
|
|
788
|
+
await startScheduler(h) // see case 7: the deferral may have stopped the scheduler
|
|
789
|
+
const sp2 = await verifyWithDebate(h, 'r-qG-s0', 1, 0)
|
|
790
|
+
assert(!!sp2.first, 'the problem solution was re-verified after the blocker was recorded')
|
|
791
|
+
const qs2 = JSON.parse(readIf(qsFile) || '[]')
|
|
792
|
+
const qG2 = qs2.find((q) => q.id === 'qG')
|
|
793
|
+
assert(!!qG2 && qG2.已解决 === true, 'with a reasoned blocker the problem DOES close')
|
|
794
|
+
assert(!!qG2 && qG2.解法列表[0].正确概率 === 1, 'the solution probability becomes 1 after the gate opened')
|
|
795
|
+
const cards = JSON.parse(readIf(join(proj, 'Verified', '问题_Verified.json')) || '[]')
|
|
796
|
+
const card = cards.find((c) => c.id === 'qG')
|
|
797
|
+
assert(!!card && /阻塞(/.test(card['形式化'] || ''), 'the problem card records the formal status')
|
|
798
|
+
assert(!existsSync(join(vibeRoot(h), 'Formal', 'Lib', 'qG.lean')), 'a blocked record does not fabricate a library file')
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ---------- 9. persistence across resume ----------
|
|
802
|
+
section('9 the formal state survives resume (v2 has no session projection)')
|
|
803
|
+
{
|
|
804
|
+
const h = await makeCase('persist')
|
|
805
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require' })
|
|
806
|
+
await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pPersist', content: 'theorem p_persist : 1 + 1 = 2 := by decide\n' })
|
|
807
|
+
const before = await h.call('vibe_math_status', {})
|
|
808
|
+
assert(before.formal.objects.some((o) => o.target === 'pPersist' && o.status === 'passed'), 'the object is passed before the restart')
|
|
809
|
+
const proj = projRoot(h)
|
|
810
|
+
writeFileSync(join(proj, 'VibeMath_State', 'process_epoch.json'), JSON.stringify('OLD-PROCESS-EPOCH'), 'utf8')
|
|
811
|
+
const res = await h.call('vibe_math_resume', {})
|
|
812
|
+
assert(res.ok === true, 'resume succeeded')
|
|
813
|
+
const after = await h.call('vibe_math_status', {})
|
|
814
|
+
assert(after.formal.objects.some((o) => o.target === 'pPersist' && o.status === 'passed'),
|
|
815
|
+
'★ the Lean-passed object is still passed after resume (the gate does not forget)')
|
|
816
|
+
assert(/pPersist/.test(readIf(join(proj, 'Formal', 'Index.md'))), 'Formal/Index.md still indexes it (the files are the readable mirror)')
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ---------- 10. the gate is a no-op unless require ----------
|
|
820
|
+
section("10 'encourage' never gates (a verdict still lands with no Lean artifact)")
|
|
821
|
+
{
|
|
822
|
+
const h = await makeCase('nogate')
|
|
823
|
+
await h.call('vibe_math_set_params', { formalVerify: 'encourage', maxParallelThreshold: 8 })
|
|
824
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
825
|
+
await startScheduler(h)
|
|
826
|
+
const proj = projRoot(h)
|
|
827
|
+
await h.call('vibe_math_add_proposition', { id: 'pFree', 概述: '鼓励模式不设门禁', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
828
|
+
const vs = await verifyWithDebate(h, 'r-pFree', 1, 0)
|
|
829
|
+
assert(!!vs.first, 'verifiers spawned under encourage mode')
|
|
830
|
+
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
831
|
+
const p = props.find((x) => x.id === 'pFree') || {}
|
|
832
|
+
assert(p.布尔估计 === 1, "'encourage' finalizes normally with NO Lean artifact (布尔估计=1)")
|
|
833
|
+
const cards = JSON.parse(readIf(join(proj, 'Verified', '数论_Verified.json')) || '[]')
|
|
834
|
+
assert(!!cards.find((c) => c.id === 'pFree'), "'encourage' writes the Verified card")
|
|
835
|
+
assert(!existsSync(join(proj, 'Formal', 'TODO.md')) || !/pFree/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 'no formalization TODO is created in encourage mode')
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ---------- 11. the reply channel is REAL (contract §4 / §6.3 / §10.8) ----------
|
|
839
|
+
// The first version of this feature only WROTE "请在回执的 formal 字段写明难度判断" into the prompt
|
|
840
|
+
// and never parsed it: 177 green assertions guarded a dead channel (AUDIT-CHECKLIST §2.2). These
|
|
841
|
+
// cases feed a genuine agent reply through the framework's own dispatch path (subagent/end →
|
|
842
|
+
// handleVerifier → absorb) and assert the DURABLE record, not the wording.
|
|
843
|
+
section('11 the reply channel really lands in the record (blocked / note validation)')
|
|
844
|
+
{
|
|
845
|
+
const h = await makeCase('reply')
|
|
846
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
847
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
848
|
+
await startScheduler(h)
|
|
849
|
+
await h.call('vibe_math_add_proposition', { id: 'pReply', 概述: '用回执记录阻塞', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
850
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pReply'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
851
|
+
assert(!!vs, 'verifiers were spawned for the reply-channel proposition')
|
|
852
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pReply:0') || {}).prompt || ''
|
|
853
|
+
assert(/"formal":\{"target":"r-pReply","decision":"used\|blocked\|defect"/.test(vp), '★ the review contract itself advertises the formal field, keyed by the verification id')
|
|
854
|
+
if (vs) {
|
|
855
|
+
// ① `blocked` WITH a note, named by the VERIFICATION id → the OBJECT record must be synced too.
|
|
856
|
+
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '我判断形式化不划算', formal: { target: 'r-pReply', decision: 'blocked', note: '需要大量未形式化的实分析前置知识' } })
|
|
857
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['r-pReply'] && r['r-pReply'].status === 'blocked') ? r : undefined }, 40, 150)
|
|
858
|
+
assert(!!rec, '★ a `formal.decision=blocked` reply is really absorbed — the channel is not dead code')
|
|
859
|
+
assert(!!rec && rec['r-pReply'].decision === 'blocked' && /实分析前置知识/.test(rec['r-pReply'].note || ''), 'the record keeps the decision AND the reason')
|
|
860
|
+
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)')
|
|
861
|
+
assert(/实分析前置知识/.test(readIf(join(projRoot(h), 'Formal', 'Index.md'))), 'the reply-recorded blocker reaches Formal/Index.md')
|
|
862
|
+
// ② `blocked` WITHOUT a note (a different target) → refused with V2_INVALID_ARGUMENT, no record.
|
|
863
|
+
replyFrom(h, vs[1].childId, { Result: 0.5, Reason: '不想做', formal: { target: 'r-pNoNote', decision: 'blocked' } })
|
|
864
|
+
let acts = ''
|
|
865
|
+
for (let i = 0; i < 25; i++) {
|
|
866
|
+
const st = await h.call('vibe_math_status', {})
|
|
867
|
+
acts = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
868
|
+
if (/V2_INVALID_ARGUMENT/.test(acts)) break
|
|
869
|
+
await sleep(120)
|
|
870
|
+
}
|
|
871
|
+
assert(/V2_INVALID_ARGUMENT/.test(acts), '★ a blocked/defect judgement without a note is REJECTED with V2_INVALID_ARGUMENT')
|
|
872
|
+
assert(/没有写明 note/.test(acts), 'the refusal says why (an explicit decision is required, never a silent skip)')
|
|
873
|
+
const recN = formalStateOf(h)
|
|
874
|
+
assert(!(recN.records || {})['r-pNoNote'] && !(recN.records || {})['pNoNote'], 'no record is invented for the refused judgement')
|
|
875
|
+
assert(!/pNoNote/.test(readIf(join(projRoot(h), 'Formal', 'Index.md'))), 'the refused judgement does not reach the index either')
|
|
876
|
+
// ③ a reply with NO target must not invent an object (safeId('') would fall back to 'anon').
|
|
877
|
+
// The 0.5/0.5 round has no consensus, so the debate round re-woke both children — that is the
|
|
878
|
+
// observable proof that these children are registered and can answer again.
|
|
879
|
+
const woke = await waitFor(() => (h.followups.filter((f) => /DEBATE/.test(f.prompt || '')).length >= 2 ? true : undefined), 40, 150)
|
|
880
|
+
assert(!!woke, 'the debate round re-woke the verifiers (round-2 replies reach the framework again)')
|
|
881
|
+
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '漏写 target', formal: { decision: 'blocked', note: '没有写 target' } })
|
|
882
|
+
let acts2 = ''
|
|
883
|
+
for (let i = 0; i < 25; i++) {
|
|
884
|
+
const st = await h.call('vibe_math_status', {})
|
|
885
|
+
acts2 = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
886
|
+
if (/没有 target/.test(acts2)) break
|
|
887
|
+
await sleep(120)
|
|
888
|
+
}
|
|
889
|
+
assert(/没有 target/.test(acts2), '★ a `formal` reply without a target is refused, not guessed')
|
|
890
|
+
const recT = formalStateOf(h)
|
|
891
|
+
assert(!(recT.records || {}).anon, '★ a `formal` reply without a target cannot invent a record (no "anon" object)')
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ---------- 12. `defect`: a fidelity defect is NOT "the proposition is false" (§4.1) ----------
|
|
896
|
+
section('12 a defect reply withdraws the proof, writes the TODO and defers the verdict')
|
|
897
|
+
{
|
|
898
|
+
const h = await makeCase('defect')
|
|
899
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
900
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
901
|
+
await startScheduler(h)
|
|
902
|
+
const proj = projRoot(h)
|
|
903
|
+
// A PASSING Lean proof exists for the object — archived under the OBJECT id, the realistic path.
|
|
904
|
+
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pDefect', content: 'theorem p_defect : 2 + 2 = 4 := by decide\n' })
|
|
905
|
+
assert(proof.ok === true && proof.passed === true, 'precondition: the object starts Lean-passed')
|
|
906
|
+
const proofFile = join(proj, 'Verified', 'Lean', 'pDefect.lean')
|
|
907
|
+
assert(existsSync(proofFile), 'the archived proof exists before the defect is reported')
|
|
908
|
+
await h.call('vibe_math_add_proposition', { id: 'pDefect', 概述: '形式化写窄了的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
909
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pDefect'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
910
|
+
assert(!!vs, 'verifiers were spawned for the Lean-passed object')
|
|
911
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pDefect:0') || {}).prompt || ''
|
|
912
|
+
assert(/忠实性审查/.test(vp) && /不要投 0/.test(vp), 'the reviewers were told to audit fidelity and NOT to vote 0 on a defect')
|
|
913
|
+
// In REQUIRE mode the framework really does hold the verdict, so THAT promise is the correct one here.
|
|
914
|
+
assert(/本次裁定\*\*不定论\*\*/.test(vp), '★ require fidelity text does promise the hold (the gate really enforces it)')
|
|
915
|
+
assert(!/本档没有门禁/.test(vp), 'require must not claim it has no gate')
|
|
916
|
+
const DEFECT = 'Lean 只证了 n>0 的情形,命题原文是 n≥0'
|
|
917
|
+
if (vs) {
|
|
918
|
+
// A fidelity defect: the voter ABSTAINS (0.3) and records it through the reply channel.
|
|
919
|
+
replyFrom(h, vs[0].childId, { Result: 0.3, Reason: '形式化写窄了(弃权)', formal: { target: 'pDefect', decision: 'defect', note: DEFECT } })
|
|
920
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pDefect'] && r['pDefect'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
921
|
+
assert(!!rec, '★ the defect reply is absorbed')
|
|
922
|
+
assert(!!rec && rec['pDefect'].status === 'attempted', '★ the object is DOWNGRADED to attempted (the "passed" status is withdrawn)')
|
|
923
|
+
assert(!!rec && rec['pDefect'].proof === '', '★ the proof pointer is cleared')
|
|
924
|
+
assert(!!rec && rec['pDefect'].note === DEFECT, 'the concrete deviation is recorded on the object record')
|
|
925
|
+
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)')
|
|
926
|
+
assert(!existsSync(proofFile), '★ the archived proof Verified/Lean/pDefect.lean is DELETED')
|
|
927
|
+
assert(existsSync(join(proj, 'Formal', 'pDefect.lean')), 'the working file Formal/pDefect.lean is kept (the code is not lost)')
|
|
928
|
+
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
929
|
+
assert(/pDefect/.test(todo) && /defect/.test(todo), '★ the object is listed in Formal/TODO.md with the defect reason')
|
|
930
|
+
assert(/只证了 n>0 的情形/.test(todo), 'the TODO carries the concrete deviation, not just a flag')
|
|
931
|
+
let acts = ''
|
|
932
|
+
for (let i = 0; i < 25; i++) {
|
|
933
|
+
const st = await h.call('vibe_math_status', {})
|
|
934
|
+
acts = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
935
|
+
if (/忠实性缺陷/.test(acts)) break
|
|
936
|
+
await sleep(120)
|
|
937
|
+
}
|
|
938
|
+
assert(/忠实性缺陷/.test(acts), 'the downgrade is announced on the v2-readable channel (activity log)')
|
|
939
|
+
assert(/撤回「已通过」状态/.test(acts), 'the announcement says the passed status was withdrawn')
|
|
940
|
+
assert(/本次裁定不定论/.test(acts), 'and that the verdict is undecided — a defect withdraws a proof, it does not refute the proposition')
|
|
941
|
+
// Now drive the same round to a UNANIMOUS "true": without the defect it would conclude.
|
|
942
|
+
const settled = await verifyWithDebate(h, 'r-pDefect', 1, 0)
|
|
943
|
+
assert(!!settled.first, 'the round was driven to a verdict after the defect')
|
|
944
|
+
const todo2 = await waitFor(() => { const t = readIf(join(proj, 'Formal', 'TODO.md')); return /formal-required/.test(t) ? t : undefined }, 40, 150)
|
|
945
|
+
assert(!!todo2 && /r-pDefect/.test(todo2), '★ require mode DEFERS after a defect (the object enters the formalization TODO as formal-required)')
|
|
946
|
+
await tick(400)
|
|
947
|
+
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
948
|
+
const p = props.find((x) => x.id === 'pDefect') || {}
|
|
949
|
+
assert(p.布尔估计 === 0.5, '★★ the proposition is NOT recorded as false — 布尔估计 unchanged (got ' + p.布尔估计 + ')')
|
|
950
|
+
assert(!(p.证明列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 proof entry was written')
|
|
951
|
+
assert(p.优先级 !== 'never', 'the priority was not pinned to never')
|
|
952
|
+
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), '★ no Verified card: the verdict is UNDECIDED, not "false"')
|
|
953
|
+
}
|
|
954
|
+
// The reverse direction: a defect named by the VERIFICATION id must still downgrade the OBJECT
|
|
955
|
+
// record that actually holds the proof (this is the pair that silently drifts when only one side
|
|
956
|
+
// is written — formalGateRecord would read `passed` from the other side).
|
|
957
|
+
const h2 = await makeCase('defect-rid')
|
|
958
|
+
await h2.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
959
|
+
await h2.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
960
|
+
await startScheduler(h2)
|
|
961
|
+
const proj2 = projRoot(h2)
|
|
962
|
+
await h2.call('vibe_math_lean_archive', { kind: 'proof', target: 'pDefect2', content: 'theorem p_defect2 : 2 + 2 = 4 := by decide\n' })
|
|
963
|
+
await h2.call('vibe_math_add_proposition', { id: 'pDefect2', 概述: '回执用验证 id 命名的缺陷', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
964
|
+
const vs2 = await waitFor(() => { const x = verifiersOf(h2, 'r-pDefect2'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
965
|
+
assert(!!vs2, 'verifiers were spawned for the reverse-direction case')
|
|
966
|
+
if (vs2) {
|
|
967
|
+
replyFrom(h2, vs2[0].childId, { Result: 0.3, Reason: '写宽了', formal: { target: 'r-pDefect2', decision: 'defect', note: 'Lean 版本没有假设 n≥1,比原文更宽' } })
|
|
968
|
+
const rec2 = await waitFor(() => { const r = (formalStateOf(h2).records || {}); return (r['r-pDefect2'] && r['r-pDefect2'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
969
|
+
assert(!!rec2, '★ a defect named by the verification id is absorbed')
|
|
970
|
+
assert(!!rec2 && !!rec2['pDefect2'] && rec2['pDefect2'].status === 'attempted', '★ and the OBJECT record that held `passed` is downgraded as well')
|
|
971
|
+
assert(!!rec2 && rec2['pDefect2'].proof === '', 'its proof pointer is cleared too')
|
|
972
|
+
assert(!existsSync(join(proj2, 'Verified', 'Lean', 'pDefect2.lean')), '★ the archived proof is deleted even though the reply named the other id')
|
|
973
|
+
assert(/pDefect2/.test(readIf(join(proj2, 'Formal', 'TODO.md'))), 'the object is on the formalization TODO')
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// ---------- 13. the gate must read BOTH id spaces ----------
|
|
978
|
+
// v2 has two id spaces that name the same object: the verification id (rId, `r-pX`, `r-pX-s0`) that the
|
|
979
|
+
// verification prompts and the gate use, and the OBJECT id (`pX`) that the agent reads in the target
|
|
980
|
+
// block and normally archives under. `lean_archive` can only ADD a record for the id it was given, and
|
|
981
|
+
// `syncVerificationTarget` merely UPDATES aliases that already exist — so "archived under the object id,
|
|
982
|
+
// rId record does not exist yet" is a real first-time state. A gate that reads only its own side calls
|
|
983
|
+
// that object un-formalized (a false negative), and because the deferral itself materialises the rId
|
|
984
|
+
// record as `status:'none'`, the object can then NEVER conclude: every round re-defers and re-debates
|
|
985
|
+
// (AUDIT-CHECKLIST §3: "成对关系只做一半").
|
|
986
|
+
section('13 the require gate reads BOTH id spaces (archiving under the OBJECT id must not wedge)')
|
|
987
|
+
{
|
|
988
|
+
const h = await makeCase('gate-objid')
|
|
989
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
990
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
991
|
+
await startScheduler(h)
|
|
992
|
+
const proj = projRoot(h)
|
|
993
|
+
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pObjId', content: 'theorem p_objid : 2 + 2 = 4 := by decide\n' })
|
|
994
|
+
assert(proof.ok === true && proof.passed === true, 'precondition: the object is Lean-passed under its OBJECT id')
|
|
995
|
+
const rec0 = formalStateOf(h)
|
|
996
|
+
assert(!!rec0.records && !!rec0.records.pObjId && rec0.records.pObjId.status === 'passed', 'the passed record lives under the object id')
|
|
997
|
+
assert(!(rec0.records || {})['r-pObjId'], 'precondition: no rId record exists yet (this is the state that used to fool the gate)')
|
|
998
|
+
await h.call('vibe_math_add_proposition', { id: 'pObjId', 概述: '用对象 id 归档后必须能定论', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
999
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pObjId'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1000
|
+
assert(!!vs, 'verifiers were spawned for the object-id case')
|
|
1001
|
+
fireVerdicts(h, vs || [], 1)
|
|
1002
|
+
await tick(600)
|
|
1003
|
+
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
1004
|
+
const p = props.find((x) => x.id === 'pObjId') || {}
|
|
1005
|
+
assert(p.布尔估计 === 1, "★ a Lean-passed object DOES conclude when the proof was archived under the object id (got " + p.布尔估计 + ')')
|
|
1006
|
+
const cards = JSON.parse(readIf(join(proj, 'Verified', '数论_Verified.json')) || '[]')
|
|
1007
|
+
assert(!!cards.find((c) => c.id === 'pObjId'), 'the Verified card was written')
|
|
1008
|
+
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
1009
|
+
assert(!/pObjId/.test(todo), '★ the already-formalized object is NOT put on the formalization TODO')
|
|
1010
|
+
const todo2 = await h.call('vibe_math_status', {})
|
|
1011
|
+
assert(!(todo2.formal.todo || []).some((t) => String(t.id).indexOf('pObjId') !== -1), 'and the status TODO is empty for it too')
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// ---------- 14. withdrawing a proof must reach EVERY archived copy ----------
|
|
1015
|
+
// Contract §4.1 requires the withdrawal, not a best-effort delete: after a `defect` the archived proof
|
|
1016
|
+
// must be gone from `Verified/Lean/` — the one place everyone looks for "the proof of this object".
|
|
1017
|
+
// Two independent ways that fails silently:
|
|
1018
|
+
// (1) the proof was archived under a DIFFERENT id than the one the reply names (`Verified/Lean/<rId>.lean`
|
|
1019
|
+
// while the reply names the object id) — scanning only the two named ids misses it, and the record's
|
|
1020
|
+
// `proof` pointer is cleared anyway, so nothing ever points at the orphan again;
|
|
1021
|
+
// (2) the host cannot delete at all (no subprocess service) — the delete fails and the stale proof stays.
|
|
1022
|
+
section('14 defect withdrawal covers every id alias AND a host that cannot delete')
|
|
1023
|
+
{
|
|
1024
|
+
// (a) archived under the VERIFICATION id, defect named by the OBJECT id.
|
|
1025
|
+
const h = await makeCase('defect-alias')
|
|
1026
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1027
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1028
|
+
await startScheduler(h)
|
|
1029
|
+
const proj = projRoot(h)
|
|
1030
|
+
const pr = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'r-pAlias', content: 'theorem p_alias : 2 + 2 = 4 := by decide\n' })
|
|
1031
|
+
assert(pr.ok === true && pr.passed === true, 'precondition: the proof is archived under the VERIFICATION id')
|
|
1032
|
+
const aliasProof = join(proj, 'Verified', 'Lean', 'r-pAlias.lean')
|
|
1033
|
+
assert(existsSync(aliasProof), 'the archived proof sits at Verified/Lean/<rId>.lean')
|
|
1034
|
+
await h.call('vibe_math_add_proposition', { id: 'pAlias', 概述: '归档写在验证 id 上', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1035
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pAlias'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1036
|
+
assert(!!vs, 'verifiers were spawned for the alias case')
|
|
1037
|
+
if (vs) {
|
|
1038
|
+
replyFrom(h, vs[0].childId, { Result: 0.3, Reason: '写宽了(弃权)', formal: { target: 'pAlias', decision: 'defect', note: '原文还有 n≥1 的假设' } })
|
|
1039
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pAlias'] && r['pAlias'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
1040
|
+
assert(!!rec, '★ the defect named by the object id is absorbed')
|
|
1041
|
+
assert(!existsSync(aliasProof), '★★ the proof archived under the VERIFICATION id is withdrawn too (every id alias, not just the two named ones)')
|
|
1042
|
+
assert(!!rec && (!rec['r-pAlias'] || rec['r-pAlias'].proof === ''), 'the rId record no longer points at a proof')
|
|
1043
|
+
assert(!rec['r-pAlias'] || !rec['r-pAlias'].objectId || rec['r-pAlias'].objectId === 'pAlias', '★★ the rId record owner stamp is the OBJECT id (pAlias), never an r-shaped poisoned anchor (got ' + JSON.stringify(rec['r-pAlias'] && rec['r-pAlias'].objectId) + ')')
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// (b) a host whose deletion cannot work: the archived path must not keep reading as the proof.
|
|
1047
|
+
const h2 = await makeCase('defect-nosub', { noSubprocess: true })
|
|
1048
|
+
await h2.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1049
|
+
const proj2 = projRoot(h2)
|
|
1050
|
+
mkdirSync(join(proj2, 'Verified', 'Lean'), { recursive: true })
|
|
1051
|
+
mkdirSync(join(proj2, 'Formal'), { recursive: true })
|
|
1052
|
+
const WORK = 'theorem p_stale : 2 + 2 = 4 := by decide\n'
|
|
1053
|
+
writeFileSync(join(proj2, 'Formal', 'pStale.lean'), WORK, 'utf8')
|
|
1054
|
+
writeFileSync(join(proj2, 'Verified', 'Lean', 'pStale.lean'), WORK, 'utf8')
|
|
1055
|
+
// The record is written directly (this host has no subprocess, so the LEAN tools cannot run at all):
|
|
1056
|
+
// what is under test is the WITHDRAWAL path, not the archiving path.
|
|
1057
|
+
writeFileSync(join(proj2, 'VibeMath_State', 'formal.json'), JSON.stringify({ records: { pStale: { status: 'passed', file: 'Formal/pStale.lean', proof: 'Verified/Lean/pStale.lean', decision: 'used', updatedAt: 1 } }, todo: [] }), 'utf8')
|
|
1058
|
+
await h2.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1059
|
+
await h2.call('vibe_math_add_proposition', { id: 'pStale', 概述: '宿主删不掉归档证明时的撤回', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1060
|
+
await startScheduler(h2)
|
|
1061
|
+
const st0 = await h2.call('vibe_math_status', {})
|
|
1062
|
+
assert(st0.formal.objects.some((o) => o.target === 'pStale' && o.status === 'passed'), 'precondition: the passed record with an archived proof survives resume on the no-subprocess host')
|
|
1063
|
+
const vs2 = await waitFor(() => { const x = verifiersOf(h2, 'r-pStale'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1064
|
+
assert(!!vs2, 'verifiers were spawned on the no-subprocess host')
|
|
1065
|
+
if (vs2) {
|
|
1066
|
+
replyFrom(h2, vs2[0].childId, { Result: 0.3, Reason: '不忠实(弃权)', formal: { target: 'pStale', decision: 'defect', note: 'Lean 少了 n≥1' } })
|
|
1067
|
+
const rec2 = await waitFor(() => { const r = (formalStateOf(h2).records || {}); return (r['pStale'] && r['pStale'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
1068
|
+
assert(!!rec2, '★ the defect is absorbed even though this host cannot run a delete')
|
|
1069
|
+
assert(!!rec2 && rec2['pStale'].status === 'attempted' && rec2['pStale'].proof === '', 'the record is downgraded and its proof pointer cleared')
|
|
1070
|
+
const stalePath = join(proj2, 'Verified', 'Lean', 'pStale.lean')
|
|
1071
|
+
assert(existsSync(stalePath), 'the archived file could NOT be deleted here (no subprocess) — so the fallback has to handle it')
|
|
1072
|
+
const body = readIf(stalePath)
|
|
1073
|
+
assert(!/theorem p_stale/.test(body), '★★ the original proof text is GONE from the archived path (it can no longer be read as the proof)')
|
|
1074
|
+
assert(/已撤回/.test(body), '★★ …and that path carries an explicit withdrawal notice instead')
|
|
1075
|
+
assert(/Formal\/pStale\.lean/.test(body), 'the notice points at the working file that is kept')
|
|
1076
|
+
const acts = (await h2.call('vibe_math_status', {})).recentActivity.map((a) => a.detail).join('\n')
|
|
1077
|
+
assert(/覆写/.test(acts), '★ the announcement says WHAT actually happened (overwritten, not deleted)')
|
|
1078
|
+
assert(/撤回「已通过」状态/.test(acts), 'the announcement still says the passed status was withdrawn')
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ---------- 15. the「判断命题」transfer is a verdict on the SOURCE proposition ----------
|
|
1083
|
+
// When a solver's solution for a "判断下述命题是否成立:X" problem is verified, the result is TRANSFERRED
|
|
1084
|
+
// onto the source proposition: `布尔估计 = v`, `已验证 = true`, a probability-1 proof/refutation entry,
|
|
1085
|
+
// and `优先级 = 'never'` for a boolean v. That IS a boolean verdict on X, so require mode must gate it —
|
|
1086
|
+
// otherwise a proposition nobody formalized is silently concluded (and permanently de-scheduled).
|
|
1087
|
+
// (c) an OBJECT ID that itself ends in `-sN` / `-pfN` / `-rfN` — exactly the shapes the rId suffix
|
|
1088
|
+
// parser strips. `formalObjectIdOf` used to strip such a suffix unconditionally, so the verification id
|
|
1089
|
+
// `r-pAmb-s1` resolved to object `pAmb`: a DIFFERENT object. A defect about the former would then
|
|
1090
|
+
// downgrade the latter and even retract ITS archived proof, while the real owner stayed `passed`.
|
|
1091
|
+
// The verification task knows its owner (r.pId / r.qid), so that is the authoritative source.
|
|
1092
|
+
{
|
|
1093
|
+
const h = await makeCase('defect-ambig')
|
|
1094
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1095
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1096
|
+
await startScheduler(h)
|
|
1097
|
+
const proj = projRoot(h)
|
|
1098
|
+
const innocent = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pAmb', content: 'theorem p_amb : 2 + 2 = 4 := by decide\n' })
|
|
1099
|
+
assert(innocent.ok === true && innocent.passed === true, 'ambiguity: the neighbour pAmb has its own passing proof')
|
|
1100
|
+
const innocentProof = join(proj, 'Verified', 'Lean', 'pAmb.lean')
|
|
1101
|
+
const prone = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pAmb-s1', content: 'theorem p_amb_s1 : 3 + 3 = 6 := by decide\n' })
|
|
1102
|
+
assert(prone.ok === true && prone.passed === true, 'ambiguity: pAmb-s1 (id ending in -s1) has its own passing proof')
|
|
1103
|
+
const proneProof = join(proj, 'Verified', 'Lean', 'pAmb-s1.lean')
|
|
1104
|
+
assert(existsSync(innocentProof) && existsSync(proneProof), 'ambiguity: both archived proofs exist')
|
|
1105
|
+
await h.call('vibe_math_add_proposition', { id: 'pAmb', 概述: '同前缀的邻居对象', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1106
|
+
await h.call('vibe_math_add_proposition', { id: 'pAmb-s1', 概述: '对象 id 本身以 -s1 结尾', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1107
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pAmb-s1'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1108
|
+
assert(!!vs, 'ambiguity: verifiers were spawned for r-pAmb-s1')
|
|
1109
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pAmb-s1:0') || {}).prompt || ''
|
|
1110
|
+
assert(/忠实性审查/.test(vp), 'ambiguity: the review subject is the Lean-passed object')
|
|
1111
|
+
assert(/Verified\/Lean\/pAmb-s1\.lean/.test(vp), '★★ and the fidelity prompt points at pAmb-s1 OWN proof, not at a same-prefix neighbour (got ' + (vp.match(/Verified\/Lean\/[^\s))]+/) || ['none'])[0] + ')')
|
|
1112
|
+
if (vs) {
|
|
1113
|
+
replyFrom(h, vs[0].childId, { Result: 0.3, Reason: '结论写窄了(弃权)', formal: { target: 'r-pAmb-s1', decision: 'defect', note: 'Lean 只证了 n>0 的情形' } })
|
|
1114
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pAmb-s1'] && r['pAmb-s1'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
1115
|
+
assert(!!rec, '★★ a defect named by r-pAmb-s1 downgrades its REAL owner (pAmb-s1)')
|
|
1116
|
+
assert(!existsSync(proneProof) || /已撤回/.test(readIf(proneProof)), '★★ and retracts pAmb-s1 own archived proof')
|
|
1117
|
+
const recs = formalStateOf(h).records || {}
|
|
1118
|
+
assert(!!recs['pAmb'] && recs['pAmb'].status === 'passed', '★★ the same-prefix NEIGHBOUR pAmb keeps its passed status (got ' + (recs['pAmb'] ? recs['pAmb'].status : 'no record') + ')')
|
|
1119
|
+
assert(existsSync(innocentProof) && !/已撤回/.test(readIf(innocentProof)), '★★ and its archived proof file is untouched — a defect must never retract another object\'s proof')
|
|
1120
|
+
assert(/pAmb-s1/.test(readIf(join(proj, 'Formal', 'TODO.md')) || ''), 'the TODO names the real owner')
|
|
1121
|
+
// The record carries the authoritative owner, so the mapping survives a resume even when the
|
|
1122
|
+
// in-memory verification task is gone (the fallback string parse would pick the neighbour again).
|
|
1123
|
+
assert(!!recs['r-pAmb-s1'] && recs['r-pAmb-s1'].objectId === 'pAmb-s1',
|
|
1124
|
+
'★★ the verification record remembers its object id (authoritative, not re-parsed from the name) — got ' + JSON.stringify(recs['r-pAmb-s1'] && recs['r-pAmb-s1'].objectId))
|
|
1125
|
+
assert(!!recs['pAmb'] && !recs['pAmb'].objectId, 'the neighbour record carries no bogus owner stamp')
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// (d) THE RECORD-OWNER FALLBACK must not be hijacked by a hand-edited state file. The state file is
|
|
1130
|
+
// user-editable, and a record whose `objectId` is itself r-shaped is not a usable owner: it would make
|
|
1131
|
+
// the verification id map to ITSELF, so a defect named by that rId would never reach the object side.
|
|
1132
|
+
// This case uses a WORK-ROUND (explorer) reply, which also carries `formal` and runs with NO verification
|
|
1133
|
+
// task in memory — i.e. it is the path that actually reads the owner from the record (a verifier reply
|
|
1134
|
+
// would take the task-registry path instead, which is authoritative and already covered above).
|
|
1135
|
+
{
|
|
1136
|
+
const h = await makeCase('anchor-poison', { noSubprocess: true })
|
|
1137
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1138
|
+
const proj = projRoot(h)
|
|
1139
|
+
mkdirSync(join(proj, 'Verified', 'Lean'), { recursive: true })
|
|
1140
|
+
mkdirSync(join(proj, 'Formal'), { recursive: true })
|
|
1141
|
+
const WORK = 'theorem p_x : 2 + 2 = 4 := by decide\n'
|
|
1142
|
+
writeFileSync(join(proj, 'Formal', 'pX.lean'), WORK, 'utf8')
|
|
1143
|
+
writeFileSync(join(proj, 'Verified', 'Lean', 'pX.lean'), WORK, 'utf8')
|
|
1144
|
+
const base = { status: 'passed', file: 'Formal/pX.lean', proof: 'Verified/Lean/pX.lean', decision: 'used', updatedAt: 1 }
|
|
1145
|
+
writeFileSync(join(proj, 'VibeMath_State', 'formal.json'), JSON.stringify({ records: {
|
|
1146
|
+
pX: Object.assign({}, base),
|
|
1147
|
+
'r-pX': Object.assign({}, base, { objectId: 'r-pX' }), // ← the r-shaped anchor a hand edit could leave
|
|
1148
|
+
}, todo: [] }), 'utf8')
|
|
1149
|
+
await h.call('vibe_math_add_problem', { id: 'qA', description: '锚点污染用例', priority: 1 })
|
|
1150
|
+
await startScheduler(h)
|
|
1151
|
+
await tick(2000)
|
|
1152
|
+
const ex = await waitFor(() => h.spawns.find((s) => s.label.startsWith('explorer:qA')), 60, 200)
|
|
1153
|
+
assert(!!ex, 'anchor-poison: the explorer was spawned (the scheduler is running)')
|
|
1154
|
+
if (ex) {
|
|
1155
|
+
const reply = { directions: [{ id: 'd1', title: 'D', method: 'm', core_assumption: 'c', feasibility: 0.6 }], formal: { target: 'r-pX', decision: 'defect', note: 'Lean 少了 n≥1' } }
|
|
1156
|
+
h.fireEnd({ id: ex.childId, runId: 'rp', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: '```json\n' + JSON.stringify(reply) + '\n```' }] })
|
|
1157
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pX'] && r['pX'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
1158
|
+
assert(!!rec, '★★ a work-round defect named by an rId still reaches the OBJECT side when the record carries an r-shaped anchor (the anchor is rejected, not trusted)')
|
|
1159
|
+
const all = formalStateOf(h).records || {}
|
|
1160
|
+
assert(!!all['r-pX'] && all['r-pX'].status === 'attempted', '★★ and the rId record is downgraded as well (both id spaces stay in step)')
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
section('15 the require gate also covers the judge-problem transfer')
|
|
1165
|
+
{
|
|
1166
|
+
const h = await makeCase('judge-gate')
|
|
1167
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1168
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1169
|
+
const proj = projRoot(h)
|
|
1170
|
+
await h.call('vibe_math_add_proposition', { id: 'pJudgeSrc', 概述: '被判断的源命题(未形式化)', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1171
|
+
await h.call('vibe_math_add_problem', { id: 'qJudge', description: '判断下述命题是否成立:pJudgeSrc', priority: 1 })
|
|
1172
|
+
const qsFile = join(proj, 'qs', 'qs.json')
|
|
1173
|
+
const qs0 = JSON.parse(readIf(qsFile) || '[]')
|
|
1174
|
+
const qj = qs0.find((q) => q.id === 'qJudge')
|
|
1175
|
+
qj.判断命题 = 'pJudgeSrc'
|
|
1176
|
+
// A solver-produced solution carries no 来源列表 — that is exactly the branch that transfers to the source.
|
|
1177
|
+
qj.解法列表 = [{ 完整解法: '该命题不成立的论证', 正确概率: 0.8, 已验: false }]
|
|
1178
|
+
writeFileSync(qsFile, JSON.stringify(qs0, null, 2), 'utf8')
|
|
1179
|
+
await startScheduler(h)
|
|
1180
|
+
const sp = await waitFor(() => { const x = verifiersOf(h, 'r-qJudge-s0'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1181
|
+
assert(!!sp, 'verifiers were spawned for the judge problem solution')
|
|
1182
|
+
fireVerdicts(h, sp || [], 0)
|
|
1183
|
+
await tick(600)
|
|
1184
|
+
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
1185
|
+
const ap = props.find((x) => x.id === 'pJudgeSrc') || {}
|
|
1186
|
+
assert(ap.布尔估计 === 0.5, "★★ the source proposition's 布尔估计 is UNCHANGED (require mode must not conclude 假 through the transfer; got " + ap.布尔估计 + ')')
|
|
1187
|
+
assert(!(ap.证伪列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 refutation was written onto the source proposition')
|
|
1188
|
+
assert(ap.优先级 !== 'never', 'the source proposition was not pinned to never')
|
|
1189
|
+
assert(ap.已验证 !== true, '★ it stays re-verifiable (the ungated path also marked it 已验证, removing it from every future candidate set)')
|
|
1190
|
+
assert(/pJudgeSrc/.test(readIf(join(proj, 'Formal', 'TODO.md'))), '★ the deferred source proposition is on the formalization TODO')
|
|
1191
|
+
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), 'no Verified card for the source proposition')
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// ---------- 16. the prompt corpus (contract §10.10) ----------
|
|
1195
|
+
// A HUMAN must be able to re-read every prompt the framework emitted, not just the assertions
|
|
1196
|
+
// about them. Paths are normalised so the dump is deterministic, diffable and machine-free.
|
|
1197
|
+
section("15b a \`used\` reply must NOT downgrade an already-passed object")
|
|
1198
|
+
{
|
|
1199
|
+
// Contract §4: a fidelity defect WITHDRAWS a proof; a plain `used` judgement merely says "this round
|
|
1200
|
+
// touched the formalization". Letting `used` downgrade an established proof would silently remove the
|
|
1201
|
+
// fidelity branch from every later review prompt and re-close the require gate — the exact class that
|
|
1202
|
+
// was already fixed once for v4's formalSetRun. v3/v4/v5 preserve passed/blocked here; v2 did not.
|
|
1203
|
+
const h = await makeCase('used-keep')
|
|
1204
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
1205
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
1206
|
+
await startScheduler(h)
|
|
1207
|
+
const proj = projRoot(h)
|
|
1208
|
+
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pUsedKeep', content: 'theorem p_used_keep : 2 + 2 = 4 := by decide\n' })
|
|
1209
|
+
assert(proof.ok === true && proof.passed === true, 'used-keep: the object starts Lean-passed')
|
|
1210
|
+
const proofFile = join(proj, 'Verified', 'Lean', 'pUsedKeep.lean')
|
|
1211
|
+
assert(existsSync(proofFile), 'used-keep: the archived proof exists before the used reply')
|
|
1212
|
+
await h.call('vibe_math_add_proposition', { id: 'pUsedKeep', 概述: '已有通过证明后再写一次 used 回执', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
1213
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pUsedKeep'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
1214
|
+
assert(!!vs, 'used-keep: verifiers were spawned for the Lean-passed object')
|
|
1215
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pUsedKeep:0') || {}).prompt || ''
|
|
1216
|
+
assert(/忠实性审查/.test(vp), 'used-keep: the reviewers were told to audit FIDELITY (the Lean code is the subject)')
|
|
1217
|
+
if (vs) {
|
|
1218
|
+
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '这一轮只是又写了一遍草稿', formal: { target: 'pUsedKeep', decision: 'used', file: 'Formal/pUsedKeep.lean' } })
|
|
1219
|
+
// The reply is what materialises the canonical verification alias r-<objectId>, so waiting for
|
|
1220
|
+
// THAT is a signal only the reply can produce (the archive already wrote decision:'used').
|
|
1221
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['r-pUsedKeep'] && r['pUsedKeep']) ? r : undefined }, 40, 150)
|
|
1222
|
+
assert(!!rec, 'used-keep: the `used` reply is absorbed (the canonical verification alias was materialised)')
|
|
1223
|
+
assert(!!rec && rec['pUsedKeep'].status === 'passed', '★★ a `used` reply does NOT downgrade an already-passed object (got ' + (rec && rec['pUsedKeep'] ? rec['pUsedKeep'].status : 'no record') + ')')
|
|
1224
|
+
assert(!!rec && rec['pUsedKeep'].proof === 'Verified/Lean/pUsedKeep.lean', '★★ and its proof pointer survives the reply')
|
|
1225
|
+
assert(!!rec && !!rec['r-pUsedKeep'] && rec['r-pUsedKeep'].status === 'passed', '★★ the verification-id alias keeps passed too (both id spaces)')
|
|
1226
|
+
assert(existsSync(proofFile), '★★ the archived proof is still on disk')
|
|
1227
|
+
const st = await h.call('vibe_math_status', {})
|
|
1228
|
+
const obj = (st.formal.objects || []).find((o) => o.target === 'pUsedKeep')
|
|
1229
|
+
assert(!!obj && obj.status === 'passed', '★★ status still reports the object as Lean-passed (v2 exposes status.formal.objects, not a "passed" array)')
|
|
1230
|
+
// The consequence that actually matters: with 'passed' preserved the require gate stays OPEN, so
|
|
1231
|
+
// driving the round to a unanimous "true" still concludes (a wrongly downgraded object is deferred).
|
|
1232
|
+
const settled = await verifyWithDebate(h, 'r-pUsedKeep', 1, 0)
|
|
1233
|
+
assert(!!settled.first, 'used-keep: the round can still be driven to a verdict')
|
|
1234
|
+
assert(!/pUsedKeep/.test(readIf(join(proj, 'Formal', 'TODO.md')) || ''), '★★ no formal-required TODO was created: the surviving passed status still opens the require gate')
|
|
1235
|
+
// The same rule covers the RUN path (docs §4): a run attributed to the object may not downgrade it.
|
|
1236
|
+
const rerun = await h.call('vibe_math_lean_run', { file: 'Formal/pUsedKeep.lean', target: 'pUsedKeep' })
|
|
1237
|
+
assert(rerun.ok === true, 'used-keep: the object work file runs green (the record assertion below is the point)')
|
|
1238
|
+
const obj2 = ((await h.call('vibe_math_status', {})).formal.objects || []).find((o) => o.target === 'pUsedKeep')
|
|
1239
|
+
assert(!!obj2 && obj2.status === 'passed', '★★ and the record is still passed (a scratch run may not withdraw a proof)')
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
section('16 the captured prompt corpus is written for human review')
|
|
1244
|
+
{
|
|
1245
|
+
// Freeze the scheduler in every case FIRST: a still-running tick loop could emit one more
|
|
1246
|
+
// prompt between two runs and make the corpus non-deterministic.
|
|
1247
|
+
for (const h of hosts) { try { await h.call('vibe_math_pause', {}) } catch (e) { /* ignore */ } }
|
|
1248
|
+
const scrub = (h, s) => {
|
|
1249
|
+
const ws = String(h.WS)
|
|
1250
|
+
const slash = ws.replace(/\\/g, '/')
|
|
1251
|
+
return String(s == null ? '' : s)
|
|
1252
|
+
.split(slash + '/VibeMath').join('<VIBEMATH>')
|
|
1253
|
+
.split(ws + '\\VibeMath').join('<VIBEMATH>')
|
|
1254
|
+
.split(slash).join('<WS>')
|
|
1255
|
+
.split(ws).join('<WS>')
|
|
1256
|
+
}
|
|
1257
|
+
const entries = []
|
|
1258
|
+
for (const h of hosts) {
|
|
1259
|
+
for (const s of h.spawns) entries.push({ kind: 'spawn', case: h.label, label: s.label, prompt: scrub(h, s.prompt) })
|
|
1260
|
+
for (const f of h.followups) {
|
|
1261
|
+
const owner = h.spawns.find((s) => s.childId === f.childId)
|
|
1262
|
+
entries.push({ kind: 'wake', case: h.label, label: owner ? owner.label : f.childId, prompt: scrub(h, f.prompt) })
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
mkdirSync(CORPUS_DIR, { recursive: true })
|
|
1266
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.json'), JSON.stringify({ entries: entries }, null, 2), 'utf8')
|
|
1267
|
+
const md = ['# V2 形式化验证交互语料(prompt corpus)', '',
|
|
1268
|
+
'> 由 `formal-verify-v2.test.mjs` 落盘:框架**真正发出**的每一条提示词原文。',
|
|
1269
|
+
'> 工作区路径归一化为 `<WS>`、VibeMath 根归一化为 `<VIBEMATH>`,因此可 diff、不泄露本机路径。',
|
|
1270
|
+
'> 覆盖:off 档(无任何 Lean 文字)、encourage 与 require 的表决初评/辩论、passed 后的忠实性分支、',
|
|
1271
|
+
'> 以及平时工作轮的「顺手形式化」段落与 formal 回执契约。', '']
|
|
1272
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1273
|
+
const e = entries[i]
|
|
1274
|
+
md.push('## [' + i + '] ' + e.kind + ' · ' + e.label + ' · case=' + e.case)
|
|
1275
|
+
md.push('')
|
|
1276
|
+
md.push('```text')
|
|
1277
|
+
md.push(e.prompt)
|
|
1278
|
+
md.push('```')
|
|
1279
|
+
md.push('')
|
|
1280
|
+
}
|
|
1281
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.md'), md.join('\n'), 'utf8')
|
|
1282
|
+
assert(existsSync(join(CORPUS_DIR, 'formal-verify-v2.json')) && existsSync(join(CORPUS_DIR, 'formal-verify-v2.md')), 'the prompt corpus was written (JSON + Markdown)')
|
|
1283
|
+
assert(entries.length >= 15, 'the corpus covers the whole run (' + entries.length + ' prompts)')
|
|
1284
|
+
assert(entries.some((e) => /【Lean 形式化验证(鼓励模式)】/.test(e.prompt)), 'the corpus contains the encourage verify prompt')
|
|
1285
|
+
assert(entries.some((e) => /【Lean 形式化验证(强制模式)】/.test(e.prompt)), '★ the corpus contains the REQUIRE verify prompt')
|
|
1286
|
+
assert(entries.some((e) => /你不需要重新检查推导/.test(e.prompt) && /一致 → Result = 1/.test(e.prompt)), 'the corpus contains the passed/fidelity prompt')
|
|
1287
|
+
assert(entries.some((e) => /【顺手形式化(鼓励)】/.test(e.prompt)), 'the corpus contains the work-round 顺手形式化 prompt')
|
|
1288
|
+
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')
|
|
1289
|
+
assert(entries.some((e) => !/Lean|形式化/.test(e.prompt)), 'the corpus contains off-mode prompts with no Lean text at all')
|
|
1290
|
+
assert(entries.some((e) => e.kind === 'wake'), 'the corpus also keeps the continuation prompts (debate rounds)')
|
|
1291
|
+
// Generic sweeps over EVERY captured prompt, not spot checks (AUDIT §2.1).
|
|
1292
|
+
const joined = entries.map((e) => e.prompt).join('\n')
|
|
1293
|
+
const bare = entries.filter((e) => /(^|[^a-z_])lean_(run|archive|lib)/.test(e.prompt))
|
|
1294
|
+
assert(bare.length === 0, '★ no captured prompt names an abbreviated tool (' + bare.map((b) => b.label).join(',') + ')')
|
|
1295
|
+
const zero = entries.filter((e) => /偏离\s*→\s*0/.test(e.prompt))
|
|
1296
|
+
assert(zero.length === 0, '★ no captured prompt turns a fidelity defect into a 0 vote (' + zero.map((z) => z.label).join(',') + ')')
|
|
1297
|
+
const wrongField = entries.filter((e) => /忠实性/.test(e.prompt) && /verdict/.test(e.prompt))
|
|
1298
|
+
assert(wrongField.length === 0, '★ no fidelity prompt names a `verdict` field (' + wrongField.map((w) => w.label).join(',') + ')')
|
|
1299
|
+
const dirty = entries.filter((e) => /\[object Object\]|\bNaN\b|:\s*undefined|["']undefined["']|undefined\s*[,}\]]/.test(e.prompt))
|
|
1300
|
+
assert(dirty.length === 0, 'no captured prompt contains placeholder garbage (' + dirty.map((d) => d.label).join(',') + ')')
|
|
1301
|
+
assert(joined.indexOf('<VIBEMATH>') !== -1, 'the VibeMath root is normalised to <VIBEMATH>')
|
|
1302
|
+
for (const h of hosts) {
|
|
1303
|
+
const ws = String(h.WS)
|
|
1304
|
+
assert(joined.indexOf(ws) === -1 && joined.indexOf(ws.replace(/\\/g, '/')) === -1, 'no captured prompt leaks a machine path (' + h.label + ')')
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// cleanup
|
|
1309
|
+
for (const h of hosts) { try { rmSync(h.WS, { recursive: true, force: true }) } catch (e) { /* ignore */ } }
|
|
1310
|
+
|
|
1311
|
+
console.log('')
|
|
1312
|
+
console.log('passed=' + passed + ' failed=' + failed)
|
|
1313
|
+
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
1314
|
+
console.log('ALL GREEN')
|
|
1315
|
+
process.exit(0)
|