dsh-vibe-math 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT-CHECKLIST.md +48 -2
- package/README.md +368 -81
- package/RELEASE-NOTES-2.2.2.md +88 -0
- package/RELEASE-NOTES-2.3.0.md +207 -0
- package/audit-formal-sensitivity.mjs +247 -0
- package/audit-persona-sensitivity.mjs +249 -0
- package/audit-persona-surface.test.mjs +349 -0
- package/audit-v5-integrity.mjs +40 -1
- package/audit-v5-sensitivity.mjs +84 -6
- package/docs/formal-verification.md +321 -0
- package/docs/generate_framework_diagram_v5.mjs +290 -0
- package/docs//346/236/266/346/236/204/345/233/276.md +75 -0
- package/formal-verify-v2.test.mjs +672 -0
- package/formal-verify-v3.test.mjs +824 -0
- package/formal-verify-v4.test.mjs +603 -0
- package/formal-verify-v5.test.mjs +526 -0
- package/package.json +33 -15
- package/prompt-corpus-persona/persona-corpus.json +32 -0
- package/prompt-corpus-persona/persona-corpus.md +674 -0
- package/prompt-corpus-v3/formal-verify-v3.json +280 -0
- package/prompt-corpus-v3/formal-verify-v3.md +2826 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +131 -713
- package/prompt-corpus-v5/prompt-corpus-v5.md +1828 -5225
- package/prompt-v5-integrity.test.mjs +154 -11
- package/vibe-math-v2/agent.cordis.yml +40 -2
- package/vibe-math-v2/vibe-math-v2.js +627 -19
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +145 -1
- package/vibe-math-v3/agent.cordis.yml +46 -2
- package/vibe-math-v3/vibe-math-v3.js +749 -21
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +87 -2
- package/vibe-math-v4/agent.cordis.yml +46 -4
- package/vibe-math-v4/vibe-math-v4.js +652 -15
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +226 -0
- package/vibe-math-v5/agent.cordis.yml +41 -5
- package/vibe-math-v5/vibe-math-v5.js +572 -9
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +122 -4
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +426 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +173 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// V3 LEAN FORMAL VERIFICATION SUITE (docs/formal-verification.md)
|
|
3
|
+
//
|
|
4
|
+
// Asserts the whole contract of the `formalVerify` knob on the v3 preset:
|
|
5
|
+
// · 'off' is a TRUE no-op (no Lean text in ANY prompt, no Formal record, no gate)
|
|
6
|
+
// · 'encourage' injects the Lean section into the work prompts AND into both verification
|
|
7
|
+
// prompts, and — the actual point of the feature — turns the voting prompt into
|
|
8
|
+
// a FIDELITY review once a Lean run has passed
|
|
9
|
+
// · 'require' withholds a true/false verdict as 未定论 until the object is Lean-passed or
|
|
10
|
+
// carries an explicit, reasoned blocker record; then allows it, and the Verified
|
|
11
|
+
// card records how strong the result really is
|
|
12
|
+
// · the three tools (run / archive / lib) write the right things to the right paths
|
|
13
|
+
//
|
|
14
|
+
// The Lean toolchain is mocked through the subprocess SERVICE, so the tests exercise the real
|
|
15
|
+
// code path (resolveExecutable → spawn → collected stdout → exit code) without requiring Lean.
|
|
16
|
+
//
|
|
17
|
+
// V3_PLUGIN overrides the plugin under test: a sensitivity probe MUST point this suite at a
|
|
18
|
+
// mutated copy, otherwise every probe would exercise the unmutated plugin and stay green.
|
|
19
|
+
//
|
|
20
|
+
// Run: node formal-verify-v3.test.mjs
|
|
21
|
+
// ============================================================
|
|
22
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
23
|
+
import { tmpdir } from 'node:os'
|
|
24
|
+
import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
|
|
27
|
+
const PLUGIN = process.env.V3_PLUGIN
|
|
28
|
+
? new URL('file:///' + String(process.env.V3_PLUGIN).replace(/\\/g, '/'))
|
|
29
|
+
: new URL('./vibe-math-v3/vibe-math-v3.js', import.meta.url)
|
|
30
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
31
|
+
// AUDIT-CHECKLIST §2.4: the suite must also keep the INTERACTION TEXT it drove, so a human can
|
|
32
|
+
// re-read the prompts the framework really emitted. V3_CORPUS_DIR overrides the destination.
|
|
33
|
+
const CORPUS_DIR = process.env.V3_CORPUS_DIR ? pathResolve(process.env.V3_CORPUS_DIR) : join(HERE, 'prompt-corpus-v3')
|
|
34
|
+
const WS = mkdtempSync(join(tmpdir(), 'vibe-v3-lean-'))
|
|
35
|
+
const VIBE = join(WS, 'VibeMath')
|
|
36
|
+
const projRoot = (slug) => join(VIBE, 'Projects', slug)
|
|
37
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
38
|
+
const reEsc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
39
|
+
const verifyRe = (target) => new RegExp('^verifier:r-' + reEsc(target) + '(?:-\\w+)?:\\d+$')
|
|
40
|
+
|
|
41
|
+
let passed = 0, failed = 0
|
|
42
|
+
const failures = []
|
|
43
|
+
const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
|
|
44
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
45
|
+
const section = (t) => console.log('\n[' + t + ']')
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------
|
|
48
|
+
// mock host
|
|
49
|
+
// ---------------------------------------------------------------
|
|
50
|
+
const listeners = {}
|
|
51
|
+
const toolRegs = []
|
|
52
|
+
const cmdRegs = []
|
|
53
|
+
const spawns = [] // { label, childId, rootId, prompt }
|
|
54
|
+
const wakes = [] // { childId, rootId, prompt }
|
|
55
|
+
const interrupts = []
|
|
56
|
+
const leanRuns = [] // { argv, file, cwd, graceMs }
|
|
57
|
+
const terminated = []
|
|
58
|
+
const roots = []
|
|
59
|
+
let rootSeq = 0
|
|
60
|
+
let subprocessAvailable = true
|
|
61
|
+
let toolchainAvailable = true
|
|
62
|
+
let spawnThrows = false
|
|
63
|
+
// Interaction corpus (AUDIT-CHECKLIST §2.4): every prompt the framework actually sent, with the
|
|
64
|
+
// workspace path normalised so the dump is deterministic and diffable.
|
|
65
|
+
const corpus = []
|
|
66
|
+
// Normalise BOTH slash forms: prompts built through vibeRoot() carry forward slashes while other
|
|
67
|
+
// framework hints carry native backslashes, and a half-normalised corpus is not diffable.
|
|
68
|
+
const scrub = (s) => String(s == null ? '' : s).split(WS).join('<WS>').split(WS.replace(/\\/g, '/')).join('<WS>')
|
|
69
|
+
|
|
70
|
+
// A fake Lean: a file PASSES unless it still contains `sorry` or the marker `-- FAIL`.
|
|
71
|
+
// `-- HANG` simulates a toolchain that never returns (the timeout path).
|
|
72
|
+
const subprocess = {
|
|
73
|
+
async resolveExecutable(cmd) {
|
|
74
|
+
if (!toolchainAvailable) throw new Error('spawn ' + cmd + ' ENOENT')
|
|
75
|
+
if (String(cmd) !== 'lean' && String(cmd) !== 'lake') throw new Error('unknown executable ' + cmd)
|
|
76
|
+
return String(cmd)
|
|
77
|
+
},
|
|
78
|
+
spawn(spec) {
|
|
79
|
+
const argv = spec.argv || []
|
|
80
|
+
const last = String(argv[argv.length - 1] || '')
|
|
81
|
+
const isShell = /powershell|cmd\.exe|\/bin\/sh|(^|\/)sh$/i.test(String(argv[0] || '')) || /New-Item|Remove-Item|Move-Item|^mkdir -p/.test(last)
|
|
82
|
+
if (isShell) {
|
|
83
|
+
// directory shim (v3's runShell: powershell New-Item / POSIX mkdir -p)
|
|
84
|
+
if (/New-Item/.test(last)) {
|
|
85
|
+
const m = last.match(/-Path\s+(?:'((?:[^']|'')*)'|"((?:[^"]|"")*)")/)
|
|
86
|
+
const raw = (m && (m[1] || m[2])) || ''
|
|
87
|
+
for (const p of raw.split(',').map((x) => x.replace(/''/g, "'"))) if (p) mkdirSync(p, { recursive: true })
|
|
88
|
+
}
|
|
89
|
+
if (/^mkdir -p/.test(last)) {
|
|
90
|
+
const re = /'((?:[^']|'\\'')*)'/g
|
|
91
|
+
let m
|
|
92
|
+
while ((m = re.exec(last)) !== null) mkdirSync(m[1].replace(/'\\''/g, "'"), { recursive: true })
|
|
93
|
+
}
|
|
94
|
+
if (/Remove-Item/.test(last)) {
|
|
95
|
+
const m = last.match(/-LiteralPath\s+'((?:[^']|'')*)'/)
|
|
96
|
+
if (m) rmSync(m[1].replace(/''/g, "'"), { force: true, recursive: true })
|
|
97
|
+
}
|
|
98
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {} }
|
|
99
|
+
}
|
|
100
|
+
const file = last
|
|
101
|
+
const text = existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
102
|
+
leanRuns.push({ argv: argv.slice(0, -1), file, cwd: spec.cwd, graceMs: spec.graceMs })
|
|
103
|
+
if (spawnThrows) throw new Error('spawn ' + String(argv[0]) + ' EPERM')
|
|
104
|
+
if (/-- REJECT/.test(text)) {
|
|
105
|
+
return {
|
|
106
|
+
done: Promise.reject(new Error('child process died before reporting')),
|
|
107
|
+
collected: { stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) }, stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) } },
|
|
108
|
+
terminate() {},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (/-- HANG/.test(text)) {
|
|
112
|
+
return {
|
|
113
|
+
done: new Promise(() => {}),
|
|
114
|
+
collected: { stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) }, stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) } },
|
|
115
|
+
terminate() { terminated.push(file) },
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const bad = /sorry|-- FAIL/.test(text)
|
|
119
|
+
const stdout = bad ? '' : 'ok\n'
|
|
120
|
+
const stderr = bad ? 'error: declaration uses sorry\n' : ''
|
|
121
|
+
return {
|
|
122
|
+
done: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null }),
|
|
123
|
+
collected: {
|
|
124
|
+
stdout: { readFrom: () => ({ text: stdout, nextOffset: stdout.length, lossy: false }) },
|
|
125
|
+
stderr: { readFrom: () => ({ text: stderr, nextOffset: stderr.length, lossy: false }) },
|
|
126
|
+
},
|
|
127
|
+
terminate() {},
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const ctx = {
|
|
133
|
+
get(name) {
|
|
134
|
+
if (name === 'subprocess') return subprocessAvailable ? subprocess : undefined
|
|
135
|
+
if (name === 'sandboxPolicy') return { resolve() { return { workspaceRoot: WS } } }
|
|
136
|
+
return undefined
|
|
137
|
+
},
|
|
138
|
+
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
139
|
+
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
140
|
+
logger: { info() {}, warn() {}, error() {} },
|
|
141
|
+
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
142
|
+
commands: { register(spec) { cmdRegs.push(spec); return () => {} } },
|
|
143
|
+
subagents: {
|
|
144
|
+
list() { return ['spawn'] },
|
|
145
|
+
async startContinuable({ label, request }) {
|
|
146
|
+
const childId = 'c' + (spawns.length + 1)
|
|
147
|
+
const rootId = (request && request.parent && request.parent.id) || ''
|
|
148
|
+
const prompt = (request && request.prompt && request.prompt[0] && request.prompt[0].text) || ''
|
|
149
|
+
spawns.push({ label, childId, rootId, prompt })
|
|
150
|
+
corpus.push({ kind: 'spawn', label, root: rootId, prompt: scrub(prompt) })
|
|
151
|
+
return { childId, messageId: 'm' + spawns.length }
|
|
152
|
+
},
|
|
153
|
+
async sendMessage(parent, childId, blocks) {
|
|
154
|
+
const rootId = (parent && parent.id) || ''
|
|
155
|
+
const prompt = (blocks && blocks[0] && blocks[0].text) || ''
|
|
156
|
+
wakes.push({ childId, rootId, prompt })
|
|
157
|
+
const sp = spawns.find((s) => s.childId === childId)
|
|
158
|
+
corpus.push({ kind: 'wake', label: sp ? sp.label : childId, root: rootId, prompt: scrub(prompt) })
|
|
159
|
+
return 'w' + wakes.length
|
|
160
|
+
},
|
|
161
|
+
async followup(parent, childId, blocks) { return await this.sendMessage(parent, childId, blocks) },
|
|
162
|
+
interrupt(childId) { interrupts.push(childId) },
|
|
163
|
+
},
|
|
164
|
+
agents: { roots() { return roots.slice() }, get() { return undefined } },
|
|
165
|
+
// DSH 0.1.1 fs API: resolve → { targetKey, displayPath }
|
|
166
|
+
fs: {
|
|
167
|
+
async resolve(rel, opts) {
|
|
168
|
+
const base = (opts && opts.cwd) || WS
|
|
169
|
+
const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(base, ...String(rel).split('/'))
|
|
170
|
+
return { targetKey: p, displayPath: p }
|
|
171
|
+
},
|
|
172
|
+
async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
|
|
173
|
+
async readText(t) { return readFileSync(t.targetKey, 'utf8') },
|
|
174
|
+
async writeText(t, content) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, content, 'utf8') },
|
|
175
|
+
async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map((e) => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const mod = await import(PLUGIN.href + '?t=' + Date.now())
|
|
180
|
+
;(mod.default || mod).apply(ctx)
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------
|
|
183
|
+
// driving helpers
|
|
184
|
+
// ---------------------------------------------------------------
|
|
185
|
+
function makeRoot() {
|
|
186
|
+
const id = 'sess-' + String.fromCharCode(65 + rootSeq++)
|
|
187
|
+
const root = { id, options: { provider: 'mock', model: 'mock-model' }, session: { id, header: { cwd: WS, parentSession: undefined } }, followup() {}, ctx: undefined }
|
|
188
|
+
roots.push(root)
|
|
189
|
+
return root
|
|
190
|
+
}
|
|
191
|
+
async function callTool(name, args, agent) {
|
|
192
|
+
const spec = toolRegs.find((s) => s.name === name)
|
|
193
|
+
if (!spec) throw new Error('no tool ' + name)
|
|
194
|
+
return JSON.parse(await spec.execute(args || {}, { agent }))
|
|
195
|
+
}
|
|
196
|
+
function fireEnd(childId, reply) {
|
|
197
|
+
const blocks = reply === undefined ? [] : [{ type: 'text', text: '```json\n' + JSON.stringify(reply) + '\n```' }]
|
|
198
|
+
for (const h of (listeners['subagent/end'] || [])) h({ id: childId, runId: 'r', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: blocks })
|
|
199
|
+
}
|
|
200
|
+
const firedChildren = new Set()
|
|
201
|
+
// Spawns created before a restart belong to a task the restart discarded; they are never a valid
|
|
202
|
+
// target for a later round (their onChildEnd has no agentRegistry entry any more). The floor keeps
|
|
203
|
+
// `unfiredVerifiers` from selecting them.
|
|
204
|
+
const spawnFloor = {}
|
|
205
|
+
const markFloor = (root) => { spawnFloor[root.id] = spawns.length }
|
|
206
|
+
const spawnOf = (root, prefix) => spawns.filter((s) => s.rootId === root.id && s.label.startsWith(prefix))
|
|
207
|
+
const lastSpawn = (root, prefix) => spawnOf(root, prefix).slice(-1)[0]
|
|
208
|
+
const unfiredVerifiers = (root, re) => spawns.filter((s, i) => s.rootId === root.id && (spawnFloor[root.id] || 0) <= i && re.test(s.label) && !firedChildren.has(s.childId))
|
|
209
|
+
const promptText = (root) => spawns.filter((s) => s.rootId === root.id).map((s) => s.prompt).join('\n') + '\n' + wakes.filter((w) => w.rootId === root.id).map((w) => w.prompt).join('\n')
|
|
210
|
+
|
|
211
|
+
/** Parameters every verification scenario uses. A high concurrency cap keeps the fallback
|
|
212
|
+
* scheduler from starving a later object behind a pile of unfired mock verifiers. */
|
|
213
|
+
const VPARAMS = { plannerEnabled: false, verifierCount: 2, debateMaxRounds: 1, maxParallelThreshold: 64, tickIntervalMs: 200 }
|
|
214
|
+
|
|
215
|
+
/** Fire the one in-flight forced planner with an empty plan (an empty plan schedules a tick). */
|
|
216
|
+
async function pump(root) {
|
|
217
|
+
const pending = spawns.filter((s) => s.rootId === root.id && s.label.startsWith('planner:') && !firedChildren.has(s.childId)).slice(-1)[0]
|
|
218
|
+
if (pending) { firedChildren.add(pending.childId); fireEnd(pending.childId, { summary: 'noop', plan: [] }); return }
|
|
219
|
+
try { await callTool('vibe_math_plan', { force: true }, root) } catch (e) { /* not running / gated */ }
|
|
220
|
+
}
|
|
221
|
+
/** Poll `pred`, nudging the scheduler along with an empty forced plan (the 1s tick timer is the fallback). */
|
|
222
|
+
async function drive(root, pred, label, timeoutMs) {
|
|
223
|
+
const deadline = Date.now() + (timeoutMs || 15000)
|
|
224
|
+
while (Date.now() < deadline) {
|
|
225
|
+
if (pred()) return true
|
|
226
|
+
await pump(root)
|
|
227
|
+
await sleep(80)
|
|
228
|
+
}
|
|
229
|
+
if (pred()) return true
|
|
230
|
+
console.error(' .. drive timed out: ' + label)
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
/** Fire the next `results.length` verifiers of one object (any of its candidate rIds). */
|
|
234
|
+
async function runVerifyRound(root, target, results, timeoutMs) {
|
|
235
|
+
const re = verifyRe(target)
|
|
236
|
+
const ok = await drive(root, () => unfiredVerifiers(root, re).length >= results.length, 'verifiers for r-' + target, timeoutMs)
|
|
237
|
+
if (!ok) return null
|
|
238
|
+
const un = unfiredVerifiers(root, re).slice(0, results.length)
|
|
239
|
+
for (let i = 0; i < un.length; i++) { firedChildren.add(un[i].childId); fireEnd(un[i].childId, { Result: results[i], Reason: 'mock 裁决 ' + results[i] }) }
|
|
240
|
+
await sleep(220)
|
|
241
|
+
return un
|
|
242
|
+
}
|
|
243
|
+
/** Abort + restart: gives the next verification a FRESH task, so a prompt is guaranteed to be
|
|
244
|
+
* constructed after whatever changed in between (mode switch, archived proof, …). */
|
|
245
|
+
async function restart(root) {
|
|
246
|
+
await callTool('vibe_math_abort', {}, root)
|
|
247
|
+
await callTool('vibe_math_start', {}, root)
|
|
248
|
+
markFloor(root)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log('-- V3 Lean formal verification --')
|
|
252
|
+
console.log('plugin under test: ' + PLUGIN.href)
|
|
253
|
+
|
|
254
|
+
// ===============================================================
|
|
255
|
+
// 1. 'off' is a true no-op
|
|
256
|
+
// ===============================================================
|
|
257
|
+
section("1 'off' (default) is a true no-op")
|
|
258
|
+
const RA = makeRoot()
|
|
259
|
+
await callTool('vibe_math_new_project', { name: 'lean-off' }, RA)
|
|
260
|
+
await callTool('vibe_math_set_params', Object.assign({}, VPARAMS), RA)
|
|
261
|
+
await callTool('vibe_math_add_proposition', { id: 'p-off', 概述: '关模式下的普通命题', 概率: 0.6, 分类: '数论' }, RA)
|
|
262
|
+
await callTool('vibe_math_start', {}, RA)
|
|
263
|
+
const offRound = await runVerifyRound(RA, 'p-off', [1, 1])
|
|
264
|
+
assert(offRound !== null && offRound.length === 2, 'off: two independent verifiers were asked (投票流程照常)')
|
|
265
|
+
const offProj = projRoot('lean-off')
|
|
266
|
+
assert(await drive(RA, () => existsSync(join(offProj, 'Verified', '命题', 'p-off.md')), 'off Verified card'), "'off' still finalizes on a unanimous boolean vote with NO Lean artifact")
|
|
267
|
+
{
|
|
268
|
+
const st = await callTool('vibe_math_status', {}, RA)
|
|
269
|
+
assert(st.params.formalVerify === 'off', "the default is 'off' (got " + st.params.formalVerify + ')')
|
|
270
|
+
const all = promptText(RA)
|
|
271
|
+
assert(!/Lean/.test(all), 'no prompt mentions Lean in off mode')
|
|
272
|
+
assert(!/形式化/.test(all), 'no prompt mentions 形式化 in off mode')
|
|
273
|
+
assert(!/\[形式化\]/.test(all), 'the [形式化] announcement line is absent in off mode')
|
|
274
|
+
}
|
|
275
|
+
assert(!/形式化/.test(readIf(join(offProj, 'Verified', '命题', 'p-off.md'))), 'the Verified card carries no formal line in off mode')
|
|
276
|
+
assert(!existsSync(join(offProj, 'Formal', 'Index.md')), 'no Formal/Index.md is produced in off mode (no formal record)')
|
|
277
|
+
assert(!existsSync(join(offProj, 'State', 'formal.json')), 'no State/formal.json is produced in off mode')
|
|
278
|
+
{
|
|
279
|
+
const st = await callTool('vibe_math_status', {}, RA)
|
|
280
|
+
assert(st.formal.mode === 'off' && st.formal.objects.length === 0, 'status reports mode=off with zero formal objects')
|
|
281
|
+
}
|
|
282
|
+
// the tools still EXIST in off mode (static registration), they are just never advertised
|
|
283
|
+
assert(!!toolRegs.find((t) => t.name === 'vibe_math_lean_run') && !!toolRegs.find((t) => t.name === 'vibe_math_lean_archive') && !!toolRegs.find((t) => t.name === 'vibe_math_lean_lib'),
|
|
284
|
+
'the three Lean tools are registered in every mode (registration is static)')
|
|
285
|
+
|
|
286
|
+
// ===============================================================
|
|
287
|
+
// 2. parameter validation + runtime switching
|
|
288
|
+
// ===============================================================
|
|
289
|
+
section('2 parameter validation and runtime switching')
|
|
290
|
+
const RB = makeRoot()
|
|
291
|
+
await callTool('vibe_math_new_project', { name: 'lean-params' }, RB)
|
|
292
|
+
const bad = await callTool('vibe_math_set_params', { formalVerify: 'banana' }, RB)
|
|
293
|
+
assert(bad.params.formalVerify === 'off', "an unknown mode degrades to 'off', never to a stronger mode (got " + bad.params.formalVerify + ')')
|
|
294
|
+
const bad2 = await callTool('vibe_math_set_params', { formalVerify: 'REQUIRE' }, RB)
|
|
295
|
+
assert(bad2.params.formalVerify === 'off', 'the mode is matched exactly (a case typo cannot force formalization)')
|
|
296
|
+
const enc = await callTool('vibe_math_set_params', { formalVerify: 'encourage' }, RB)
|
|
297
|
+
assert(enc.params.formalVerify === 'encourage', "'encourage' is accepted")
|
|
298
|
+
const req = await callTool('vibe_math_set_params', { formalVerify: 'require', leanTimeoutMs: -5, leanCommand: ' ' }, RB)
|
|
299
|
+
assert(req.params.formalVerify === 'require', "'require' is accepted")
|
|
300
|
+
assert(req.params.leanTimeoutMs === 120000, 'a non-positive leanTimeoutMs falls back to the default (' + req.params.leanTimeoutMs + ')')
|
|
301
|
+
assert((await callTool('vibe_math_set_params', { leanTimeoutMs: 0 }, RB)).params.leanTimeoutMs === 120000, 'leanTimeoutMs=0 also falls back to the default')
|
|
302
|
+
assert(req.params.leanCommand === 'lean', 'a blank leanCommand falls back to "lean"')
|
|
303
|
+
const t5 = await callTool('vibe_math_set_params', { leanTimeoutMs: 5000 }, RB)
|
|
304
|
+
assert(t5.params.leanTimeoutMs === 5000, 'a positive leanTimeoutMs is accepted (got ' + t5.params.leanTimeoutMs + ')')
|
|
305
|
+
const arrBad = await callTool('vibe_math_set_params', { leanArgs: 'not-an-array' }, RB)
|
|
306
|
+
assert(Array.isArray(arrBad.params.leanArgs) && arrBad.params.leanArgs.length === 0, 'a non-array leanArgs falls back to []')
|
|
307
|
+
await callTool('vibe_math_set_params', { leanCommand: 'lake', leanArgs: ['env', 'lean'] }, RB)
|
|
308
|
+
const stL = await callTool('vibe_math_status', {}, RB)
|
|
309
|
+
assert(stL.params.leanCommand === 'lake' && stL.params.leanArgs.join(' ') === 'env lean', 'leanCommand/leanArgs are settable (lake env lean)')
|
|
310
|
+
{
|
|
311
|
+
const setup = await callTool('vibe_math_setup', {}, RB)
|
|
312
|
+
const names = setup.parameters.map((p) => p.name)
|
|
313
|
+
assert(names.indexOf('formalVerify') !== -1 && names.indexOf('leanCommand') !== -1 && names.indexOf('leanArgs') !== -1 && names.indexOf('leanTimeoutMs') !== -1,
|
|
314
|
+
'the settings schema surfaces all four Lean parameters')
|
|
315
|
+
const f = setup.parameters.find((p) => p.name === 'formalVerify')
|
|
316
|
+
assert(f.default === 'off' && f.current === 'require', 'the schema carries the default (off) and the current value (require)')
|
|
317
|
+
const tmpl = await callTool('vibe_math_template', { where: 'project' }, RB)
|
|
318
|
+
const tj = readIf(join(projRoot('lean-params'), 'vibe_math_setting.json'))
|
|
319
|
+
assert(tmpl.ok === true && /"formalVerify": "off"/.test(tj) && /"leanArgs": \[\]/.test(tj), 'the generated settings template documents the Lean parameters with their defaults')
|
|
320
|
+
assert(/formalVerify — /.test(tj), 'the template carries the human-readable parameter description as a comment')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ===============================================================
|
|
324
|
+
// 3. 'encourage' injection into the WORK prompts
|
|
325
|
+
// ===============================================================
|
|
326
|
+
section("3 'encourage' injects 顺手形式化 into solver / explorer / method-keeper prompts")
|
|
327
|
+
const RC = makeRoot()
|
|
328
|
+
await callTool('vibe_math_new_project', { name: 'lean-work' }, RC)
|
|
329
|
+
await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'encourage', methodKeepEvery: 1 }), RC)
|
|
330
|
+
await callTool('vibe_math_add_problem', { id: 'qE', description: '证明 log 2 是无理数', priority: 0 }, RC)
|
|
331
|
+
await callTool('vibe_math_start', {}, RC)
|
|
332
|
+
assert(await drive(RC, () => !!lastSpawn(RC, 'explorer:qE'), 'explorer:qE'), 'fallback scheduler spawned explorer:qE')
|
|
333
|
+
{
|
|
334
|
+
const p = lastSpawn(RC, 'explorer:qE').prompt
|
|
335
|
+
assert(/【顺手形式化(鼓励)】/.test(p), 'the explorer work prompt carries the 顺手形式化 line')
|
|
336
|
+
assert(/vibe_math_lean_archive kind='def'/.test(p), 'the work prompt points at the archive tool for reusable definitions')
|
|
337
|
+
assert(/vibe_math_lean_lib 查重/.test(p), 'the work prompt tells agents to check the reuse library first')
|
|
338
|
+
assert(/形式化回执/.test(p) && /"decision":"used\|blocked"/.test(p), '★ the work-round reply contract also advertises the formal field (契约 §6.3)')
|
|
339
|
+
}
|
|
340
|
+
fireEnd(lastSpawn(RC, 'explorer:qE').childId, { meta: { kind: 'directions', qid: 'qE', formal: { target: 'qE', decision: 'blocked', note: '需要先形式化连分数收敛定理' }, directions: [{ id: 'd1', title: '连分数法', method: 'e 的连分数', core_assumption: '', feasibility: 0.7 }] } })
|
|
341
|
+
// Answer EVERY explorer the fallback scheduler dispatches (it re-derives when a direction looks
|
|
342
|
+
// unattended) until the solver for d1 appears — otherwise the retry cap can stall the project.
|
|
343
|
+
for (let guard = 0; guard < 40 && !lastSpawn(RC, 'solver:qE:d1'); guard++) {
|
|
344
|
+
const ex = spawns.filter((s) => s.rootId === RC.id && s.label.startsWith('explorer:qE') && !firedChildren.has(s.childId)).slice(-1)[0]
|
|
345
|
+
if (ex) {
|
|
346
|
+
firedChildren.add(ex.childId)
|
|
347
|
+
fireEnd(ex.childId, { meta: { kind: 'directions', qid: 'qE', directions: [{ id: 'd1', title: '连分数法', method: 'e 的连分数', core_assumption: '', feasibility: 0.7 }] } })
|
|
348
|
+
}
|
|
349
|
+
await pump(RC)
|
|
350
|
+
await sleep(80)
|
|
351
|
+
}
|
|
352
|
+
assert(!!lastSpawn(RC, 'solver:qE:d1'), 'solver spawned for direction d1')
|
|
353
|
+
assert(/【顺手形式化(鼓励)】/.test(lastSpawn(RC, 'solver:qE:d1').prompt), 'the solver work prompt carries the 顺手形式化 line')
|
|
354
|
+
{
|
|
355
|
+
// the explorer's `meta.formal` judgement is recorded even though it never called a Lean tool
|
|
356
|
+
const st = await callTool('vibe_math_status', {}, RC)
|
|
357
|
+
assert(st.formal.blocked.indexOf('qE') !== -1, '★ an explorer `meta.formal` judgement is recorded (no Lean tool call needed)')
|
|
358
|
+
assert(/- 形式化: 阻塞(/.test(readIf(join(projRoot('lean-work'), 'Problems', 'qE.md'))), 'and it lands on the problem card anchor')
|
|
359
|
+
}
|
|
360
|
+
fireEnd(lastSpawn(RC, 'solver:qE:d1').childId, { status: 'continue', survival_probability: 0.6, formal: { target: 'd1-lemma', decision: 'used', file: 'Formal/d1-lemma.lean' }, new_inventions: [{ 类型: '工具', 标题: '连分数估值工具', 内容描述: '控制收敛速度', 是否已入库: false }] })
|
|
361
|
+
await sleep(250)
|
|
362
|
+
{
|
|
363
|
+
const st = await callTool('vibe_math_status', {}, RC)
|
|
364
|
+
assert(st.formal.objects.some((o) => o.target === 'd1-lemma' && o.status === 'attempted'), 'a solver top-level `formal.decision=used` reply records the object as attempted')
|
|
365
|
+
}
|
|
366
|
+
assert(await drive(RC, () => !!lastSpawn(RC, 'method-keeper'), 'method-keeper'), 'method keeper spawned for the pending invention')
|
|
367
|
+
{
|
|
368
|
+
const p = lastSpawn(RC, 'method-keeper').prompt
|
|
369
|
+
assert(/【顺手形式化(鼓励)】/.test(p), 'the method-keeper prompt carries the 顺手形式化 line')
|
|
370
|
+
assert(/【方法沉淀 × Lean 形式化】/.test(p), '★ the method keeper is told to ALSO sediment reusable Lean definitions/lemmas')
|
|
371
|
+
assert(/kind='lemma'/.test(p) && /Proved/.test(p), 'and where the proved lemmas go')
|
|
372
|
+
}
|
|
373
|
+
await callTool('vibe_math_abort', {}, RC)
|
|
374
|
+
assert((await callTool('vibe_math_status', {}, RC)).running === false, 'work-prompt scenario aborted (keeps later scenarios deterministic)')
|
|
375
|
+
|
|
376
|
+
// ===============================================================
|
|
377
|
+
// 4. 'encourage' injection into BOTH verification prompts + the fidelity switch
|
|
378
|
+
// ===============================================================
|
|
379
|
+
section("4 'encourage' reaches the review prompt and the debate prompt")
|
|
380
|
+
const RD = makeRoot()
|
|
381
|
+
await callTool('vibe_math_new_project', { name: 'lean-verify' }, RD)
|
|
382
|
+
await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'encourage', debateMaxRounds: 2 }), RD)
|
|
383
|
+
await callTool('vibe_math_add_proposition', { id: 'p-enc', 概述: '鼓励模式下的忠实性审查', 概率: 0.6, 分类: '数论' }, RD)
|
|
384
|
+
await callTool('vibe_math_start', {}, RD)
|
|
385
|
+
const encBatch = await runVerifyRound(RD, 'p-enc', [0.9, 0.95])
|
|
386
|
+
assert(encBatch !== null, 'encourage: the first review round was asked')
|
|
387
|
+
if (encBatch) {
|
|
388
|
+
const vp = encBatch.map((s) => s.prompt).join('\n')
|
|
389
|
+
assert(/【Lean 形式化验证(鼓励模式)】/.test(vp), 'the review prompt explains the Lean mode')
|
|
390
|
+
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(vp), 'the review prompt states that a passing Lean run shrinks the question to fidelity')
|
|
391
|
+
assert(/实现难度/.test(vp), 'the review prompt asks for the implementation-difficulty judgement')
|
|
392
|
+
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(vp), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
393
|
+
assert(/"formal":/.test(vp) && /"decision":"used\|blocked"/.test(vp), 'the reply contract documents the formal field')
|
|
394
|
+
assert(/vibe_math_lean_run/.test(vp) && /vibe_math_lean_archive/.test(vp) && /vibe_math_lean_lib/.test(vp), 'the prompt names the three v3 Lean tools')
|
|
395
|
+
assert(/Formal\/(相对项目根)/.test(vp) && /VibeMath\/Formal\/Lib/.test(vp), 'the prompt states the path layout')
|
|
396
|
+
}
|
|
397
|
+
// a non-consensus round moves to the public debate, whose prompt must carry the same injection
|
|
398
|
+
assert(await drive(RD, () => wakes.some((w) => w.rootId === RD.id && /交流群/.test(w.prompt)), 'debate prompt'), 'a non-consensus round moved to the debate')
|
|
399
|
+
{
|
|
400
|
+
const dp = wakes.filter((w) => w.rootId === RD.id).map((w) => w.prompt).join('\n')
|
|
401
|
+
assert(/【Lean 形式化验证(鼓励模式)】/.test(dp), 'the debate prompt names the mode')
|
|
402
|
+
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性|你不需要重新检查推导/.test(dp), 'the debate prompt carries the Lean block too')
|
|
403
|
+
assert(/"formal":/.test(dp), 'the debate reply contract carries the formal field')
|
|
404
|
+
}
|
|
405
|
+
// finish the debate: a unanimous round concludes
|
|
406
|
+
if (encBatch) for (const b of encBatch) fireEnd(b.childId, { Result: 1, Reason: 'mock 第二轮一致' })
|
|
407
|
+
assert(await drive(RD, () => existsSync(join(projRoot('lean-verify'), 'Verified', '命题', 'p-enc.md')), 'p-enc verified'), 'the debate finished and the object was verified')
|
|
408
|
+
|
|
409
|
+
// ===============================================================
|
|
410
|
+
// 5. lean_run executes through the subprocess service and reports honestly
|
|
411
|
+
// ===============================================================
|
|
412
|
+
section('5 lean_run executes through the subprocess service and reports honestly')
|
|
413
|
+
const RE = makeRoot()
|
|
414
|
+
await callTool('vibe_math_new_project', { name: 'lean-tools' }, RE)
|
|
415
|
+
await callTool('vibe_math_set_params', { formalVerify: 'encourage' }, RE)
|
|
416
|
+
await callTool('vibe_math_add_proposition', { id: 'p-tool', 概述: '工具实验命题', 概率: 0.6, 分类: '数论' }, RE)
|
|
417
|
+
const toolProj = projRoot('lean-tools')
|
|
418
|
+
mkdirSync(join(toolProj, 'Formal'), { recursive: true })
|
|
419
|
+
writeFileSync(join(toolProj, 'Formal', 'good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
420
|
+
writeFileSync(join(toolProj, 'Formal', 'bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
|
|
421
|
+
writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1 := rfl\n', 'utf8')
|
|
422
|
+
{
|
|
423
|
+
const runGood = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean', target: 'p-tool' }, RE)
|
|
424
|
+
assert(runGood.ok === true && runGood.exitCode === 0, 'a file with no sorry runs green (' + JSON.stringify({ ok: runGood.ok, exitCode: runGood.exitCode }) + ')')
|
|
425
|
+
assert(/ok/.test(runGood.stdout), 'the compiler stdout is returned')
|
|
426
|
+
const last = leanRuns[leanRuns.length - 1]
|
|
427
|
+
assert(last.cwd.replace(/\\/g, '/') === toolProj.replace(/\\/g, '/'), 'the toolchain runs with the PROJECT root as cwd')
|
|
428
|
+
assert(last.argv.join(' ') === 'lean', 'by default the argv is just the executable (got ' + last.argv.join(' ') + ')')
|
|
429
|
+
assert(last.graceMs === 120000, 'the default leanTimeoutMs is passed down as graceMs (' + last.graceMs + ')')
|
|
430
|
+
}
|
|
431
|
+
{
|
|
432
|
+
const st = await callTool('vibe_math_status', {}, RE)
|
|
433
|
+
const o = st.formal.objects.find((x) => x.target === 'p-tool')
|
|
434
|
+
assert(!!o && o.status === 'attempted', 'lean_run with target records the object as attempted (not passed)')
|
|
435
|
+
assert(!!o.run && o.run.ok === true, 'the run result is recorded on the object')
|
|
436
|
+
const card = readIf(join(toolProj, 'Propos', '数论', 'p-tool.md'))
|
|
437
|
+
assert(/- 形式化: 已尝试未通过/.test(card), '★ the object card anchor block gains `- 形式化: <状态>` alongside 状态/概率')
|
|
438
|
+
assert(/\n- ID: p-tool\n/.test(card) && /\n- 状态: 未定论\n/.test(card) && /\n- 概率: 0.6\n/.test(card), 'the pre-existing anchors are untouched (the scheduler parser keeps working)')
|
|
439
|
+
}
|
|
440
|
+
{
|
|
441
|
+
const runBad = await callTool('vibe_math_lean_run', { file: 'Formal/bad.lean' }, RE)
|
|
442
|
+
assert(runBad.ok === false && runBad.exitCode === 1, 'a file that still uses sorry reports a red run')
|
|
443
|
+
assert(/sorry/.test(runBad.stderr), 'the compiler output is returned verbatim (' + JSON.stringify(runBad.stderr).slice(0, 60) + ')')
|
|
444
|
+
}
|
|
445
|
+
{
|
|
446
|
+
const runMissing = await callTool('vibe_math_lean_run', { file: 'Formal/nope.lean' }, RE)
|
|
447
|
+
assert(runMissing.ok === false && runMissing.code === 'V3_NOT_FOUND', 'a missing file is refused with a typed code')
|
|
448
|
+
}
|
|
449
|
+
{
|
|
450
|
+
// The guard's boundary is the VibeMath ROOT, not the project: the global reuse library
|
|
451
|
+
// deliberately lives at <VibeMath>/Formal/{Lib,Proved}, so climbing out of the project but
|
|
452
|
+
// staying inside VibeMath must remain legal (it just fails as a missing file).
|
|
453
|
+
const inside = await callTool('vibe_math_lean_run', { file: '../../Formal/Lib/x.lean' }, RE)
|
|
454
|
+
assert(inside.code === 'V3_NOT_FOUND', 'climbing out of the project but staying inside VibeMath is allowed (the global library lives there)')
|
|
455
|
+
const esc1 = await callTool('vibe_math_lean_run', { file: '../../../evil.lean' }, RE)
|
|
456
|
+
assert(esc1.ok === false && esc1.code === 'V3_INVALID_ARGUMENT', '★ a traversal that climbs ABOVE the VibeMath root is refused')
|
|
457
|
+
const esc2 = await callTool('vibe_math_lean_run', { file: 'Formal/../../../../../../evil.lean' }, RE)
|
|
458
|
+
assert(esc2.ok === false && esc2.code === 'V3_INVALID_ARGUMENT', 'a deeper traversal is refused too')
|
|
459
|
+
const esc3 = await callTool('vibe_math_lean_run', { file: '/etc/evil.lean' }, RE)
|
|
460
|
+
assert(esc3.ok === false && esc3.code === 'V3_INVALID_ARGUMENT', 'an unrelated absolute path is refused')
|
|
461
|
+
const notLean = await callTool('vibe_math_lean_run', { file: 'Formal/good.txt' }, RE)
|
|
462
|
+
assert(notLean.ok === false && notLean.code === 'V3_INVALID_ARGUMENT', 'only .lean files can be executed')
|
|
463
|
+
const noFile = await callTool('vibe_math_lean_run', {}, RE)
|
|
464
|
+
assert(noFile.ok === false && noFile.code === 'V3_INVALID_ARGUMENT', 'a missing file argument is refused')
|
|
465
|
+
}
|
|
466
|
+
{
|
|
467
|
+
toolchainAvailable = false
|
|
468
|
+
const runNoTc = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
|
|
469
|
+
assert(runNoTc.ok === false && runNoTc.code === 'LEAN_NOT_FOUND', 'a missing toolchain returns LEAN_NOT_FOUND instead of crashing')
|
|
470
|
+
assert(/仍可把形式化代码写下来归档/.test(runNoTc.message), 'the failure explains the graceful degradation')
|
|
471
|
+
toolchainAvailable = true
|
|
472
|
+
}
|
|
473
|
+
{
|
|
474
|
+
subprocessAvailable = false
|
|
475
|
+
const runNoSub = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
|
|
476
|
+
assert(runNoSub.ok === false && runNoSub.code === 'NO_SUBPROCESS', 'a host without the subprocess service returns NO_SUBPROCESS')
|
|
477
|
+
subprocessAvailable = true
|
|
478
|
+
}
|
|
479
|
+
{
|
|
480
|
+
const runHang = await callTool('vibe_math_lean_run', { file: 'Formal/hang.lean', timeout_ms: 1000 }, RE)
|
|
481
|
+
assert(runHang.ok === false && runHang.code === 'LEAN_TIMEOUT' && runHang.timedOut === true, '★ a hanging toolchain returns LEAN_TIMEOUT instead of wedging the scheduler')
|
|
482
|
+
assert(terminated.some((f) => /hang\.lean$/.test(f)), '★ the timeout path calls handle.terminate()')
|
|
483
|
+
}
|
|
484
|
+
{
|
|
485
|
+
spawnThrows = true
|
|
486
|
+
const runSpawn = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
|
|
487
|
+
assert(runSpawn.ok === false && runSpawn.code === 'LEAN_SPAWN_FAILED', 'a spawn failure is reported as LEAN_SPAWN_FAILED instead of throwing')
|
|
488
|
+
spawnThrows = false
|
|
489
|
+
writeFileSync(join(toolProj, 'Formal', 'reject.lean'), '-- REJECT\ntheorem t : 1 = 1 := rfl\n', 'utf8')
|
|
490
|
+
const runReject = await callTool('vibe_math_lean_run', { file: 'Formal/reject.lean' }, RE)
|
|
491
|
+
assert(runReject.ok === false && runReject.code === 'LEAN_RUN_FAILED', 'a rejecting handle.done is reported as LEAN_RUN_FAILED instead of throwing into the loop')
|
|
492
|
+
}
|
|
493
|
+
{
|
|
494
|
+
await callTool('vibe_math_set_params', { leanCommand: 'lake', leanArgs: ['env', 'lean'] }, RE)
|
|
495
|
+
const runLake = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
|
|
496
|
+
assert(runLake.ok === true, 'lake env lean works through leanCommand/leanArgs')
|
|
497
|
+
assert(leanRuns[leanRuns.length - 1].argv.join(' ') === 'lake env lean', 'the argv is [exe, ...leanArgs, file] (got ' + leanRuns[leanRuns.length - 1].argv.join(' ') + ')')
|
|
498
|
+
await callTool('vibe_math_set_params', { leanCommand: 'lean', leanArgs: [] }, RE)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ===============================================================
|
|
502
|
+
// 6. lean_archive: def / lemma / proof / blocked → the right paths + indexes
|
|
503
|
+
// ===============================================================
|
|
504
|
+
section('6 lean_archive writes the contract paths and rebuilds the indexes')
|
|
505
|
+
const libPath = join(VIBE, 'Formal', 'Lib')
|
|
506
|
+
const provedPath = join(VIBE, 'Formal', 'Proved')
|
|
507
|
+
{
|
|
508
|
+
const defRes = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'ZMod5', content: 'def ZMod5 := Fin 5\n' }, RE)
|
|
509
|
+
assert(defRes.ok === true && defRes.file === 'Formal/Lib/ZMod5.lean', 'a reusable definition is archived to the global lib (' + defRes.file + ')')
|
|
510
|
+
assert(existsSync(join(libPath, 'ZMod5.lean')), '★ the definition exists under VibeMath/Formal/Lib/ (cross-project, NOT inside the project)')
|
|
511
|
+
assert(!existsSync(join(toolProj, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the project tree')
|
|
512
|
+
assert(defRes.run && defRes.run.ok === true, 'the reusable definition is executed on archive (run defaults to true)')
|
|
513
|
+
const libIdx = readIf(join(libPath, 'Index.md'))
|
|
514
|
+
assert(/\| 名称 \| 文件 \| 类别 \| 摘要 \| 最近运行 \|/.test(libIdx), 'Lib/Index.md uses the contract columns')
|
|
515
|
+
assert(/ZMod5/.test(libIdx) && /Lib\/ZMod5\.lean/.test(libIdx) && /\| ok \|/.test(libIdx), 'Lib/Index.md lists the new definition with its run result')
|
|
516
|
+
assert(!/ZMod5/.test(readIf(join(toolProj, 'Formal', 'Index.md'))), 'the global definition does not pollute the project formal index')
|
|
517
|
+
}
|
|
518
|
+
{
|
|
519
|
+
const lemRes = await callTool('vibe_math_lean_archive', { kind: 'lemma', name: 'sq_odd', content: 'import Mathlib\n\ntheorem sq_odd (n : Nat) : Odd (n*n) → Odd n := by omega\n' }, RE)
|
|
520
|
+
assert(lemRes.ok === true && lemRes.file === 'Formal/Proved/sq_odd.lean', 'a lemma is archived to Proved/')
|
|
521
|
+
assert(existsSync(join(provedPath, 'sq_odd.lean')), 'the lemma exists under VibeMath/Formal/Proved/')
|
|
522
|
+
const provedIdx = readIf(join(provedPath, 'Index.md'))
|
|
523
|
+
assert(/\| 名称 \| 文件 \| 陈述 \| 依赖 \| 最近运行 \|/.test(provedIdx), 'Proved/Index.md uses the contract columns')
|
|
524
|
+
assert(/sq_odd/.test(provedIdx) && /Mathlib/.test(provedIdx), 'Proved/Index.md lists the lemma and its imports as 依赖')
|
|
525
|
+
}
|
|
526
|
+
{
|
|
527
|
+
const noRun = await callTool('vibe_math_lean_archive', { kind: 'def', name: 'NoRun', content: 'def NoRun := 1\n', run: false }, RE)
|
|
528
|
+
assert(noRun.ok === true && noRun.run === undefined, 'run=false archives WITHOUT executing (no toolchain needed)')
|
|
529
|
+
}
|
|
530
|
+
{
|
|
531
|
+
const noName = await callTool('vibe_math_lean_archive', { kind: 'def', content: 'def x := 1\n' }, RE)
|
|
532
|
+
assert(noName.ok === false && noName.code === 'V3_INVALID_ARGUMENT', 'archiving a definition without a name is refused')
|
|
533
|
+
const badKind = await callTool('vibe_math_lean_archive', { kind: 'nonsense' }, RE)
|
|
534
|
+
assert(badKind.ok === false && badKind.code === 'V3_INVALID_ARGUMENT', 'an unknown archive kind is refused')
|
|
535
|
+
const noTarget = await callTool('vibe_math_lean_archive', { kind: 'proof', content: 'theorem x : 1 = 1 := rfl\n' }, RE)
|
|
536
|
+
assert(noTarget.ok === false && noTarget.code === 'V3_INVALID_ARGUMENT', 'kind=proof without a target is refused')
|
|
537
|
+
const noBody = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-x' }, RE)
|
|
538
|
+
assert(noBody.ok === false && noBody.code === 'V3_INVALID_ARGUMENT', 'kind=proof without content/from is refused')
|
|
539
|
+
const badFrom = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-x', from: '../../../evil.lean' }, RE)
|
|
540
|
+
assert(badFrom.ok === false && badFrom.code === 'V3_INVALID_ARGUMENT', 'kind=proof with an out-of-tree from is refused')
|
|
541
|
+
}
|
|
542
|
+
{
|
|
543
|
+
// a green proof → passed + Verified/Lean (the archived proof lives next to the conclusion)
|
|
544
|
+
const arc = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-proof', content: 'theorem p_proof : 3 * 1 ^ 2 - 2 = (1:Nat) ^ 2 := by decide\n' }, RE)
|
|
545
|
+
assert(arc.ok === true && arc.passed === true, 'the proof is archived and passes (' + JSON.stringify({ ok: arc.ok, passed: arc.passed }) + ')')
|
|
546
|
+
assert(arc.file === 'Formal/p-proof.lean', 'the working file is Formal/<target>.lean')
|
|
547
|
+
assert(arc.proof === 'Verified/Lean/p-proof.lean', 'the archived proof path is Verified/Lean/<target>.lean')
|
|
548
|
+
assert(existsSync(join(toolProj, 'Formal', 'p-proof.lean')), 'the working file exists on disk')
|
|
549
|
+
assert(existsSync(join(toolProj, 'Verified', 'Lean', 'p-proof.lean')), '★ the proof is archived under Verified/Lean/ as the proof of that object')
|
|
550
|
+
const st = await callTool('vibe_math_status', {}, RE)
|
|
551
|
+
assert(st.formal.passed.indexOf('p-proof') !== -1, 'status reports the object as Lean-passed')
|
|
552
|
+
const idx = readIf(join(toolProj, 'Formal', 'Index.md'))
|
|
553
|
+
assert(/# Lean 形式化索引/.test(idx) && /p-proof/.test(idx) && /passed/.test(idx) && /Verified\/Lean\/p-proof\.lean/.test(idx), 'Formal/Index.md indexes the object, its status and its archived proof')
|
|
554
|
+
assert(/ok(exit 0,/.test(idx), 'the index shows the run result in the contract format')
|
|
555
|
+
// a RED proof must NOT become passed
|
|
556
|
+
const red = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-red', content: 'theorem p_red : 1 = 2 := by sorry\n' }, RE)
|
|
557
|
+
assert(red.ok === true && red.passed === false && red.status === 'attempted', 'a red proof is archived as attempted, not passed')
|
|
558
|
+
assert(!existsSync(join(toolProj, 'Verified', 'Lean', 'p-red.lean')), '★ no archived proof is written for a red run')
|
|
559
|
+
}
|
|
560
|
+
{
|
|
561
|
+
const blkNoNote = await callTool('vibe_math_lean_archive', { kind: 'blocked', target: 'p-blk' }, RE)
|
|
562
|
+
assert(blkNoNote.ok === false && blkNoNote.code === 'V3_INVALID_ARGUMENT', 'blocked without a note is refused')
|
|
563
|
+
const blk = await callTool('vibe_math_lean_archive', { kind: 'blocked', target: 'p-blk', note: '需要外层解析数论框架,本轮工作量不可接受' }, RE)
|
|
564
|
+
assert(blk.ok === true && blk.status === 'blocked', 'a reasoned blocker is recorded')
|
|
565
|
+
const stBlk = await callTool('vibe_math_status', {}, RE)
|
|
566
|
+
assert(stBlk.formal.blocked.indexOf('p-blk') !== -1, 'status lists the blocked object')
|
|
567
|
+
assert(/需要外层解析数论框架/.test(readIf(join(toolProj, 'Formal', 'Index.md'))), 'the blocker reason is written into the index')
|
|
568
|
+
}
|
|
569
|
+
{
|
|
570
|
+
const libList = await callTool('vibe_math_lean_lib', {}, RE)
|
|
571
|
+
assert(libList.ok === true && libList.counts.lib >= 2 && libList.counts.proved >= 1, 'lean_lib reports the reuse library sizes (' + JSON.stringify(libList.counts) + ')')
|
|
572
|
+
assert(libList.objects.some((o) => o.target === 'p-proof' && o.status === 'passed'), 'lean_lib lists per-object formal status')
|
|
573
|
+
assert(/复用优先/.test(libList.hint || ''), 'lean_lib tells agents to reuse before redefining')
|
|
574
|
+
assert(libList.paths && /VibeMath\/Formal\/Lib/.test(libList.paths.lib), 'lean_lib reports the cross-project path layout')
|
|
575
|
+
const ro = await callTool('vibe_math_lean_lib', { refresh: false }, RE)
|
|
576
|
+
assert(ro.rebuilt === false && ro.counts.lib === null, 'refresh:false only reads (does not rebuild the indexes)')
|
|
577
|
+
}
|
|
578
|
+
{
|
|
579
|
+
const ann = readIf(join(toolProj, 'Logs', '形式化.md'))
|
|
580
|
+
assert(/# 形式化公告/.test(ann) && /ZMod5/.test(ann) && /p-proof/.test(ann), 'the announcement log records what was formalized')
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ===============================================================
|
|
584
|
+
// 7. the 'require' gate
|
|
585
|
+
// ===============================================================
|
|
586
|
+
section("7 'require' withholds a verdict until the formal record exists")
|
|
587
|
+
const RF = makeRoot()
|
|
588
|
+
await callTool('vibe_math_new_project', { name: 'lean-gate' }, RF)
|
|
589
|
+
await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'require' }), RF)
|
|
590
|
+
await callTool('vibe_math_add_proposition', { id: 'p-gate', 概述: '必须形式化的命题', 概率: 0.6, 分类: '数论' }, RF)
|
|
591
|
+
const gateProj = projRoot('lean-gate')
|
|
592
|
+
await callTool('vibe_math_start', {}, RF)
|
|
593
|
+
{
|
|
594
|
+
const batch = await runVerifyRound(RF, 'p-gate', [1, 1])
|
|
595
|
+
assert(batch !== null, 'require: the review round was asked')
|
|
596
|
+
if (batch) {
|
|
597
|
+
const vp = batch.map((s) => s.prompt).join('\n')
|
|
598
|
+
assert(/【Lean 形式化验证(强制模式)】/.test(vp), 'the review prompt says 强制模式')
|
|
599
|
+
assert(/必须产出 Lean 形式化/.test(vp), "'require' states the formalization is mandatory")
|
|
600
|
+
assert(/本次裁定不会生效/.test(vp), 'the prompt warns that the verdict will not take effect without it')
|
|
601
|
+
assert(/formal-required/.test(vp), 'the prompt names the machine-readable reason')
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
assert(await drive(RF, () => /p-gate/.test(readIf(join(gateProj, 'Formal', 'TODO.md'))), 'p-gate in Formal/TODO.md'), '★ a unanimous TRUE verdict was withheld: the object is on the formalization TODO')
|
|
605
|
+
assert(!existsSync(join(gateProj, 'Verified', '命题', 'p-gate.md')), 'no Verified card was written')
|
|
606
|
+
{
|
|
607
|
+
const card = readIf(join(gateProj, 'Propos', '数论', 'p-gate.md'))
|
|
608
|
+
assert(/- 概率: 0.6/.test(card), '★ the object keeps its existing probability (no silent promotion to 1)')
|
|
609
|
+
assert(/- 状态: 未定论/.test(card), 'and its 状态 stays 未定论')
|
|
610
|
+
const todo = readIf(join(gateProj, 'Formal', 'TODO.md'))
|
|
611
|
+
assert(/# 形式化待办/.test(todo) && /p-gate/.test(todo) && /formal-required/.test(todo), '★ the TODO records the object with the machine-readable reason')
|
|
612
|
+
const idx = readIf(join(gateProj, 'Formal', 'Index.md'))
|
|
613
|
+
assert(/## 形式化待办/.test(idx) && /p-gate/.test(idx), 'Formal/Index.md mirrors the formalization TODO')
|
|
614
|
+
const ann = readIf(join(gateProj, 'Logs', '形式化.md'))
|
|
615
|
+
assert(/require 模式/.test(ann) && /不定论/.test(ann), '★ the withholding is announced')
|
|
616
|
+
const st = await callTool('vibe_math_status', {}, RF)
|
|
617
|
+
assert(st.formal.todo.some((t) => t.id === 'p-gate'), 'status lists the deferred object')
|
|
618
|
+
assert(/formal-required/.test(st.recentActivity.map((e) => e.detail).join('\n')), 'the activity feed shows the formal-required deferral')
|
|
619
|
+
const voteLog = readdirSync(join(gateProj, 'Logs', 'Verification')).filter((f) => /^r-p-gate_/.test(f))
|
|
620
|
+
assert(voteLog.length >= 1, 'the votes themselves are still recorded in Logs/Verification (nothing is lost)')
|
|
621
|
+
}
|
|
622
|
+
// runtime switch: the mode is read when the prompt is CONSTRUCTED, so a fresh round reflects it
|
|
623
|
+
await callTool('vibe_math_add_proposition', { id: 'p-mode', 概述: '模式切换观察对象', 概率: 0.6, 分类: '数论' }, RF)
|
|
624
|
+
await restart(RF)
|
|
625
|
+
assert(await drive(RF, () => unfiredVerifiers(RF, verifyRe('p-mode')).length >= 2, 'fresh verifiers in require mode'), 'a fresh object is still verified in require mode')
|
|
626
|
+
{
|
|
627
|
+
const fresh = unfiredVerifiers(RF, verifyRe('p-mode'))[0]
|
|
628
|
+
assert(!!fresh && /【Lean 形式化验证(强制模式)】/.test(fresh.prompt) && /本次裁定不会生效/.test(fresh.prompt), 'a fresh round reflects the current mode (强制模式)')
|
|
629
|
+
}
|
|
630
|
+
// ★ anti-idle: a deferred object must NOT be re-voted while it sits on the TODO (each round would
|
|
631
|
+
// just be withheld again, burning verifiers on the same rId every tick). The assertion is only
|
|
632
|
+
// meaningful because the previous block proved the scheduler IS allocating verify tasks here.
|
|
633
|
+
assert(unfiredVerifiers(RF, verifyRe('p-gate')).length === 0, '★ a deferred object is NOT re-voted while it sits on the formalization TODO (no verification livelock)')
|
|
634
|
+
await callTool('vibe_math_set_params', { formalVerify: 'encourage' }, RF)
|
|
635
|
+
await restart(RF)
|
|
636
|
+
assert(await drive(RF, () => unfiredVerifiers(RF, verifyRe('p-mode')).length >= 2, 'fresh verifiers after the mode switch'), 'the unresolved object is verified again after the mode switch')
|
|
637
|
+
{
|
|
638
|
+
const fresh = unfiredVerifiers(RF, verifyRe('p-mode'))[0]
|
|
639
|
+
assert(!!fresh && /【Lean 形式化验证(鼓励模式)】/.test(fresh.prompt), '★ switching the mode at runtime immediately changes the prompt (now 鼓励模式)')
|
|
640
|
+
assert(!!fresh && !/本次裁定不会生效/.test(fresh.prompt), 'and the mandatory wording is gone in encourage mode')
|
|
641
|
+
}
|
|
642
|
+
await callTool('vibe_math_set_params', { formalVerify: 'require' }, RF)
|
|
643
|
+
// formalize it, restart (so the next prompt is built AFTER the proof exists), then re-verify
|
|
644
|
+
{
|
|
645
|
+
const proofNow = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-gate', content: 'theorem p_gate : 2 + 2 = 4 := by decide\n' }, RF)
|
|
646
|
+
assert(proofNow.ok === true && proofNow.passed === true, 'the object is now Lean-passed')
|
|
647
|
+
const st = await callTool('vibe_math_status', {}, RF)
|
|
648
|
+
assert(st.formal.passed.indexOf('p-gate') !== -1, 'status reports p-gate as passed')
|
|
649
|
+
assert(!st.formal.todo.some((t) => t.id === 'p-gate'), '★ satisfying the gate removes the object from the formalization TODO (no stale TODO)')
|
|
650
|
+
assert(!/p-gate/.test(readIf(join(gateProj, 'Formal', 'TODO.md'))), 'Formal/TODO.md no longer lists it')
|
|
651
|
+
assert(/- 形式化: Lean 通过/.test(readIf(join(gateProj, 'Propos', '数论', 'p-gate.md'))), 'the object card anchor now records Lean 通过')
|
|
652
|
+
}
|
|
653
|
+
await restart(RF)
|
|
654
|
+
assert(await drive(RF, () => unfiredVerifiers(RF, verifyRe('p-gate')).length >= 2, 'p-gate re-eligible after passing'), '★ once the formal record satisfies the gate, the object becomes eligible for verification again')
|
|
655
|
+
{
|
|
656
|
+
const batch = await runVerifyRound(RF, 'p-gate', [1, 1])
|
|
657
|
+
assert(batch !== null, 'the re-verification round was asked')
|
|
658
|
+
if (batch) {
|
|
659
|
+
const vp = batch.map((s) => s.prompt).join('\n')
|
|
660
|
+
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(vp), 'the voting prompt announces the passing proof')
|
|
661
|
+
assert(/你不需要重新检查推导/.test(vp), '★ it tells voters NOT to re-derive')
|
|
662
|
+
assert(/忠实性审查/.test(vp), '★ it tells voters the review subject is now fidelity')
|
|
663
|
+
assert(/定义 \/ 对象 \/ 条件 \/ 假设 \/ 结论是否与命题原文\*\*完全一致\*\*/.test(vp), 'it enumerates exactly what fidelity means')
|
|
664
|
+
assert(!/必须产出 Lean 形式化/.test(vp), 'and the "must formalize" wording is replaced (the object already has one)')
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
assert(await drive(RF, () => existsSync(join(gateProj, 'Verified', '命题', 'p-gate.md')), 'p-gate Verified card'), '★ with a passing Lean artifact the same vote DOES promote it')
|
|
668
|
+
{
|
|
669
|
+
const card = readIf(join(gateProj, 'Verified', '命题', 'p-gate.md'))
|
|
670
|
+
assert(/- 形式化: Lean 通过/.test(card), '★ the Verified card records how strong the result is (Lean 通过)')
|
|
671
|
+
assert(/Verified\/Lean\/p-gate\.lean/.test(card), 'the card points at the archived proof')
|
|
672
|
+
assert(/- 结论: 真/.test(card), 'the card states the conclusion')
|
|
673
|
+
}
|
|
674
|
+
// the blocker escape hatch must also open the gate
|
|
675
|
+
await callTool('vibe_math_add_proposition', { id: 'p-blocked-ok', 概述: '记录阻塞后可定论', 概率: 0.6, 分类: '数论' }, RF)
|
|
676
|
+
await callTool('vibe_math_lean_archive', { kind: 'blocked', target: 'p-blocked-ok', note: '命题涉及未形式化的分析学,本轮不做' }, RF)
|
|
677
|
+
await restart(RF)
|
|
678
|
+
assert(await runVerifyRound(RF, 'p-blocked-ok', [1, 1]) !== null, 'the blocked object is put to a vote')
|
|
679
|
+
assert(await drive(RF, () => existsSync(join(gateProj, 'Verified', '命题', 'p-blocked-ok.md')), 'p-blocked-ok Verified card'), '★ an explicit reasoned blocker also lets the verdict through (decide by difficulty, but decide out loud)')
|
|
680
|
+
{
|
|
681
|
+
const card = readIf(join(gateProj, 'Verified', '命题', 'p-blocked-ok.md'))
|
|
682
|
+
assert(/- 形式化: 阻塞(/.test(card), 'the Verified card records the blocker')
|
|
683
|
+
assert(/未形式化的分析学/.test(card), 'and quotes the reason')
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ===============================================================
|
|
687
|
+
// 8. the reply-channel judgement (formal field in the verifier reply)
|
|
688
|
+
// ===============================================================
|
|
689
|
+
section('8 the per-round `formal` reply channel records the difficulty judgement')
|
|
690
|
+
const RG = makeRoot()
|
|
691
|
+
await callTool('vibe_math_new_project', { name: 'lean-reply' }, RG)
|
|
692
|
+
await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'require' }), RG)
|
|
693
|
+
const replyProj = projRoot('lean-reply')
|
|
694
|
+
|
|
695
|
+
// (a) blocked WITH a note → recorded, reason reaches the index + the card anchor
|
|
696
|
+
await callTool('vibe_math_add_proposition', { id: 'p-reply', 概述: '用回执记录阻塞', 概率: 0.6, 分类: '数论' }, RG)
|
|
697
|
+
await callTool('vibe_math_start', {}, RG)
|
|
698
|
+
{
|
|
699
|
+
const re = verifyRe('p-reply')
|
|
700
|
+
assert(await drive(RG, () => unfiredVerifiers(RG, re).length >= 2, 'verifiers for r-p-reply'), 'p-reply was put to a vote')
|
|
701
|
+
const vs = unfiredVerifiers(RG, re).slice(0, 2)
|
|
702
|
+
if (vs.length === 2) {
|
|
703
|
+
for (const v of vs) firedChildren.add(v.childId)
|
|
704
|
+
// A verifier that never calls a Lean tool still has to state its difficulty judgement: the
|
|
705
|
+
// JSON reply channel is the path that actually fires in practice.
|
|
706
|
+
fireEnd(vs[0].childId, { Result: 0.5, Reason: '我判断形式化不划算', formal: { target: 'p-reply', decision: 'blocked', note: '需要大量未形式化的实分析前置知识' } })
|
|
707
|
+
await sleep(150)
|
|
708
|
+
const st1 = await callTool('vibe_math_status', {}, RG)
|
|
709
|
+
assert(st1.formal.blocked.indexOf('p-reply') !== -1, 'a `formal.decision=blocked` reply is recorded as a blocker')
|
|
710
|
+
assert(/实分析前置知识/.test(readIf(join(replyProj, 'Formal', 'Index.md'))), 'and its reason reaches the index')
|
|
711
|
+
assert(/- 形式化: 阻塞(/.test(readIf(join(replyProj, 'Propos', '数论', 'p-reply.md'))), 'the reply-recorded blocker also lands on the object card anchor')
|
|
712
|
+
fireEnd(vs[1].childId, { Result: 0.5, Reason: '同上' })
|
|
713
|
+
await sleep(200)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// (b) `used` records attempted — which does NOT open the require gate
|
|
717
|
+
await restart(RG)
|
|
718
|
+
await callTool('vibe_math_add_proposition', { id: 'p-used', 概述: '写了草稿但没跑通', 概率: 0.6, 分类: '数论' }, RG)
|
|
719
|
+
{
|
|
720
|
+
const re = verifyRe('p-used')
|
|
721
|
+
assert(await drive(RG, () => unfiredVerifiers(RG, re).length >= 2, 'verifiers for r-p-used'), 'p-used was put to a vote')
|
|
722
|
+
const vs = unfiredVerifiers(RG, re).slice(0, 2)
|
|
723
|
+
if (vs.length === 2) {
|
|
724
|
+
for (const v of vs) firedChildren.add(v.childId)
|
|
725
|
+
fireEnd(vs[0].childId, { Result: 1, Reason: '看起来对', formal: { target: 'p-used', decision: 'used', file: 'Formal/p-used.lean' } })
|
|
726
|
+
await sleep(150)
|
|
727
|
+
const st = await callTool('vibe_math_status', {}, RG)
|
|
728
|
+
const o = st.formal.objects.find((x) => x.target === 'p-used')
|
|
729
|
+
assert(!!o && o.status === 'attempted' && o.file === 'Formal/p-used.lean', 'a `formal.decision=used` reply records the object as attempted with its file')
|
|
730
|
+
fireEnd(vs[1].childId, { Result: 1, Reason: '同样看起来对' })
|
|
731
|
+
await sleep(250)
|
|
732
|
+
assert(!existsSync(join(replyProj, 'Verified', '命题', 'p-used.md')), '★ attempted is NOT passed: the require gate still withholds the verdict')
|
|
733
|
+
assert(/p-used/.test(readIf(join(replyProj, 'Formal', 'TODO.md'))), 'and the object is on the formalization TODO')
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// (c) a blocker with no note is refused (an explicit decision is required, never a silent skip)
|
|
737
|
+
await restart(RG)
|
|
738
|
+
await callTool('vibe_math_add_proposition', { id: 'p-nonote', 概述: '没有理由的阻塞', 概率: 0.6, 分类: '数论' }, RG)
|
|
739
|
+
{
|
|
740
|
+
const re = verifyRe('p-nonote')
|
|
741
|
+
assert(await drive(RG, () => unfiredVerifiers(RG, re).length >= 2, 'verifiers for r-p-nonote'), 'p-nonote was put to a vote')
|
|
742
|
+
const vs = unfiredVerifiers(RG, re).slice(0, 2)
|
|
743
|
+
if (vs.length === 2) {
|
|
744
|
+
for (const v of vs) firedChildren.add(v.childId)
|
|
745
|
+
fireEnd(vs[0].childId, { Result: 0.5, Reason: '不想做', formal: { target: 'p-nonote', decision: 'blocked' } })
|
|
746
|
+
await sleep(180)
|
|
747
|
+
assert(/未写明 note/.test(readIf(join(replyProj, 'Logs', '形式化.md'))), 'a blocked judgement without a note is refused with an explicit announcement')
|
|
748
|
+
const st = await callTool('vibe_math_status', {}, RG)
|
|
749
|
+
assert(st.formal.objects.every((o) => o.target !== 'p-nonote'), 'and no blocker record is created for the refused judgement')
|
|
750
|
+
// a formal reply without a target must not invent an object (idSafe('') would fall back to 'id')
|
|
751
|
+
fireEnd(vs[1].childId, { Result: 0.5, Reason: '同上', formal: { decision: 'blocked', note: '没有写 target' } })
|
|
752
|
+
await sleep(180)
|
|
753
|
+
const st2 = await callTool('vibe_math_status', {}, RG)
|
|
754
|
+
assert(st2.formal.objects.every((o) => o.target !== 'id' && o.target !== ''), 'a `formal` reply with no target cannot invent an object record')
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
await callTool('vibe_math_abort', {}, RG)
|
|
758
|
+
|
|
759
|
+
// ===============================================================
|
|
760
|
+
// 9. reporting + persistence
|
|
761
|
+
// ===============================================================
|
|
762
|
+
section('9 the office can audit formal strength')
|
|
763
|
+
{
|
|
764
|
+
const rep = await callTool('vibe_math_report', {}, RF)
|
|
765
|
+
assert(rep.formal && rep.formal.mode === 'require', 'the JSON report carries the formal mode')
|
|
766
|
+
assert(rep.formal.passed.indexOf('p-gate') !== -1 && rep.formal.blocked.indexOf('p-blocked-ok') !== -1, 'the JSON report lists both passed and blocked objects')
|
|
767
|
+
assert(rep.params.formalVerify === 'require' && rep.params.leanCommand === 'lean', 'the readable parameter table carries the Lean parameters')
|
|
768
|
+
const md = readIf(join(gateProj, 'Logs', '报告.md'))
|
|
769
|
+
assert(/## Lean 形式化/.test(md), 'the human-readable report has a Lean formal-verification section')
|
|
770
|
+
assert(/已通过:.*p-gate/.test(md), 'it lists Lean-passed objects')
|
|
771
|
+
assert(/已记录阻塞:.*p-blocked-ok/.test(md), 'it lists blocked objects')
|
|
772
|
+
assert(/可复用库:VibeMath\/Formal\/\{Lib,Proved\}\//.test(md), 'it documents the path layout')
|
|
773
|
+
await callTool('vibe_math_report', {}, RA)
|
|
774
|
+
assert(/未启用(`formalVerify` = off/.test(readIf(join(offProj, 'Logs', '报告.md'))), 'in off mode the report says the feature is not enabled')
|
|
775
|
+
const offRep = await callTool('vibe_math_report', {}, RA)
|
|
776
|
+
assert(offRep.formal.mode === 'off' && /off/.test(offRep.formal.note || ''), 'and the JSON report says the same')
|
|
777
|
+
}
|
|
778
|
+
{
|
|
779
|
+
const before = JSON.parse(readIf(join(gateProj, 'State', 'formal.json')))
|
|
780
|
+
assert(!!before && !!before.records && before.records['p-gate'] && before.records['p-gate'].status === 'passed', 'the formal record is persisted in State/formal.json, keyed by object id')
|
|
781
|
+
assert(Array.isArray(before.todo), 'the formalization TODO is persisted alongside it')
|
|
782
|
+
assert(before.records['p-blocked-ok'].status === 'blocked' && /分析学/.test(before.records['p-blocked-ok'].note), 'a blocker record keeps its reason across sessions')
|
|
783
|
+
assert(existsSync(join(VIBE, 'Formal', 'Proved', 'Index.md')) && existsSync(join(libPath, 'Index.md')), 'both global indexes are framework-maintained')
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ===============================================================
|
|
787
|
+
// 10. interaction corpus (AUDIT-CHECKLIST §2.4) — a HUMAN must be able to re-read
|
|
788
|
+
// every prompt the framework emitted, not just the assertions about them.
|
|
789
|
+
// ===============================================================
|
|
790
|
+
section('10 the captured prompt corpus is written for human review')
|
|
791
|
+
{
|
|
792
|
+
mkdirSync(CORPUS_DIR, { recursive: true })
|
|
793
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v3.json'), JSON.stringify({ entries: corpus }, null, 2), 'utf8')
|
|
794
|
+
const md = ['# V3 形式化验证交互语料(prompt corpus)', '',
|
|
795
|
+
'> 由 `formal-verify-v3.test.mjs` 落盘:框架**真正发出**的每一条提示词原文(工作区路径归一化为 `<WS>`,可 diff)。',
|
|
796
|
+
'> 覆盖:explorer / solver / method-keeper 的日常工作提示词、三种模式下的表决初评与辩论提示词、规划提示词。', '']
|
|
797
|
+
for (let i = 0; i < corpus.length; i++) {
|
|
798
|
+
const c = corpus[i]
|
|
799
|
+
md.push('## [' + i + '] ' + c.kind + ' · ' + c.label)
|
|
800
|
+
md.push('')
|
|
801
|
+
md.push('```text')
|
|
802
|
+
md.push(c.prompt)
|
|
803
|
+
md.push('```')
|
|
804
|
+
md.push('')
|
|
805
|
+
}
|
|
806
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v3.md'), md.join('\n'), 'utf8')
|
|
807
|
+
assert(existsSync(join(CORPUS_DIR, 'formal-verify-v3.json')) && existsSync(join(CORPUS_DIR, 'formal-verify-v3.md')), 'the prompt corpus was written (JSON + Markdown)')
|
|
808
|
+
assert(corpus.length >= 25, 'the corpus covers the whole run (' + corpus.length + ' prompts)')
|
|
809
|
+
assert(corpus.some((c) => c.label.startsWith('explorer:')) && corpus.some((c) => c.label.startsWith('solver:')) && corpus.some((c) => c.label.startsWith('method-keeper')) && corpus.some((c) => c.label.startsWith('verifier:')), 'the corpus covers every interaction type this suite drives')
|
|
810
|
+
assert(corpus.some((c) => c.kind === 'wake'), 'the corpus also keeps the continuation prompts (debate rounds)')
|
|
811
|
+
// generic sweep over EVERY captured prompt, not spot checks (AUDIT §2.1)
|
|
812
|
+
const dirty = corpus.filter((c) => /\[object Object\]|\bNaN\b|\bundefined\b/.test(c.prompt))
|
|
813
|
+
assert(dirty.length === 0, 'no captured prompt contains placeholder garbage (' + dirty.map((d) => d.label).join(',') + ')')
|
|
814
|
+
const joined = corpus.map((c) => c.prompt).join('\n')
|
|
815
|
+
assert(joined.indexOf(WS) === -1 && joined.indexOf(WS.replace(/\\/g, '/')) === -1, 'every captured prompt normalises the workspace path to <WS> (the corpus stays diffable)')
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
console.log('')
|
|
819
|
+
console.log('passed=' + passed + ' failed=' + failed)
|
|
820
|
+
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f) }
|
|
821
|
+
rmSync(WS, { recursive: true, force: true })
|
|
822
|
+
if (failed) process.exit(1)
|
|
823
|
+
console.log('ALL GREEN')
|
|
824
|
+
process.exit(0)
|