dsh-vibe-math 2.3.11 → 2.3.13
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 -40
- 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/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -0
- package/docs/release-notes/RELEASE-NOTES-2.3.13.md +137 -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 +18 -17
- package/installer.js +258 -53
- package/package.json +50 -40
- package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
- package/tests/audit-installer-compat.test.mjs +136 -0
- package/tests/audit-installer-policy.test.mjs +245 -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//347/244/272/344/276/213/345/233/276//345/256/236/351/231/205/344/275/277/347/224/250/347/244/272/344/276/213-/351/225/277/346/210/252/345/233/276.png +0 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v2.png +0 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v3.png +0 -0
- /package/{RELEASE-NOTES-2.0.22.md → docs/release-notes/RELEASE-NOTES-2.0.22.md} +0 -0
|
@@ -1,708 +1,708 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// V5 LEAN FORMAL VERIFICATION SUITE (docs/formal-verification.md)
|
|
3
|
-
//
|
|
4
|
-
// Asserts the whole contract of the `formalVerify` knob:
|
|
5
|
-
// · 'off' is a TRUE no-op (no Lean text anywhere, no gate)
|
|
6
|
-
// · 'encourage' injects the Lean section AND — the actual point of the feature — turns the
|
|
7
|
-
// voting prompt into a FIDELITY review once a Lean run has passed
|
|
8
|
-
// · 'require' withholds a true/false verdict as 未定论 until the object is Lean-passed
|
|
9
|
-
// or carries an explicit, reasoned blocker record; then allows it, and the
|
|
10
|
-
// Verified card records how strong the result really is
|
|
11
|
-
// · the three tools (run / archive / lib) write the right things to the right paths
|
|
12
|
-
//
|
|
13
|
-
// The Lean toolchain is mocked through the subprocess SERVICE, so the tests exercise the
|
|
14
|
-
// real code path (resolveExecutable → spawn → collected stdout → exit code) without
|
|
15
|
-
// requiring Lean to be installed.
|
|
16
|
-
//
|
|
17
|
-
// Run: node formal-verify-v5.test.mjs
|
|
18
|
-
// ============================================================
|
|
19
|
-
import { mkdtempSync, existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs'
|
|
20
|
-
import { tmpdir } from 'node:os'
|
|
21
|
-
import { join, dirname, isAbsolute } from 'node:path'
|
|
22
|
-
import { fileURLToPath } from 'node:url'
|
|
23
|
-
|
|
24
|
-
const PLUGIN = process.env.V5_PLUGIN
|
|
25
|
-
? new URL('file:///' + String(process.env.V5_PLUGIN).replace(/\\/g, '/'))
|
|
26
|
-
: new URL('
|
|
27
|
-
const WS = mkdtempSync(join(tmpdir(), 'vibe-v5-lean-'))
|
|
28
|
-
|
|
29
|
-
let passed = 0, failed = 0
|
|
30
|
-
const failures = []
|
|
31
|
-
const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
|
|
32
|
-
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
33
|
-
const section = (t) => console.log('\n[' + t + ']')
|
|
34
|
-
|
|
35
|
-
// ---------------------------------------------------------------
|
|
36
|
-
// mock host
|
|
37
|
-
// ---------------------------------------------------------------
|
|
38
|
-
function makeProjectionRegistry() {
|
|
39
|
-
const units = new Map(); const cells = new Map()
|
|
40
|
-
const cellMap = (s) => { const id = String(s.id); let m = cells.get(id); if (!m) { m = new Map(); cells.set(id, m) } return m }
|
|
41
|
-
return {
|
|
42
|
-
register(def) { units.set(def.key, def); return () => { units.delete(def.key) } },
|
|
43
|
-
stateOf(session, key) {
|
|
44
|
-
const def = units.get(key); if (!def) return undefined
|
|
45
|
-
const m = cellMap(session)
|
|
46
|
-
if (!m.has(key)) m.set(key, def.init(session.header, session.inheritedEventCount || 0))
|
|
47
|
-
return m.get(key)
|
|
48
|
-
},
|
|
49
|
-
_drive(session, event) {
|
|
50
|
-
const m = cellMap(session)
|
|
51
|
-
for (const [k, def] of units) {
|
|
52
|
-
const cur = m.has(k) ? m.get(k) : def.init(session.header, session.inheritedEventCount || 0)
|
|
53
|
-
let next; try { next = def.apply(cur, event) } catch (e) { next = cur }
|
|
54
|
-
m.set(k, next)
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const projections = makeProjectionRegistry()
|
|
61
|
-
const listeners = {}
|
|
62
|
-
const toolRegs = []
|
|
63
|
-
const liveAgents = new Map()
|
|
64
|
-
const spawns = []
|
|
65
|
-
const wakes = []
|
|
66
|
-
const delivered = []
|
|
67
|
-
const leanRuns = []
|
|
68
|
-
|
|
69
|
-
// A fake Lean: a file PASSES unless it still contains `sorry` or the marker `-- FAIL`.
|
|
70
|
-
// This mirrors the one property that matters for the feature — an exit code that says
|
|
71
|
-
// "the kernel accepted this".
|
|
72
|
-
let toolchainAvailable = true
|
|
73
|
-
// A host with NO `subprocess` service at all: `ctx.get('subprocess')` returns undefined, which
|
|
74
|
-
// must become a readable NO_SUBPROCESS result (and must NOT stop the code from being archived).
|
|
75
|
-
let noSubprocess = false
|
|
76
|
-
// A run that HANGS: `hangLean` makes the fake toolchain return a `done` that only settles
|
|
77
|
-
// after `hangMs` (far beyond any cap the tests use), so the run can only be reported as a
|
|
78
|
-
// timeout by actually racing `done` against a timer. `terminations` records every
|
|
79
|
-
// `handle.terminate()` call, which is the observable proof that the guard fired.
|
|
80
|
-
let hangLean = false
|
|
81
|
-
let hangMs = 60000
|
|
82
|
-
const terminations = []
|
|
83
|
-
let subprocess = {
|
|
84
|
-
async resolveExecutable(cmd) {
|
|
85
|
-
if (!toolchainAvailable) throw new Error('spawn lean ENOENT')
|
|
86
|
-
if (String(cmd) !== 'lean') throw new Error('unknown executable ' + cmd)
|
|
87
|
-
return 'lean'
|
|
88
|
-
},
|
|
89
|
-
spawn(spec) {
|
|
90
|
-
const file = spec.argv[spec.argv.length - 1]
|
|
91
|
-
const text = existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
92
|
-
const bad = /sorry|-- FAIL/.test(text)
|
|
93
|
-
leanRuns.push({ argv: spec.argv.slice(0, -1), file, cwd: spec.cwd })
|
|
94
|
-
const stdout = bad ? '' : 'ok\n'
|
|
95
|
-
const stderr = bad ? 'error: declaration uses sorry\n' : ''
|
|
96
|
-
const done = hangLean
|
|
97
|
-
? new Promise(r => setTimeout(() => r({ exitCode: 0, signal: null }), hangMs))
|
|
98
|
-
: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null })
|
|
99
|
-
return {
|
|
100
|
-
done,
|
|
101
|
-
collected: {
|
|
102
|
-
stdout: { readFrom: () => ({ text: stdout, nextOffset: stdout.length, lossy: false }) },
|
|
103
|
-
stderr: { readFrom: () => ({ text: stderr, nextOffset: stderr.length, lossy: false }) },
|
|
104
|
-
},
|
|
105
|
-
terminate() { terminations.push(file) },
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function makeMockSession(id, parentSession) {
|
|
111
|
-
const events = []
|
|
112
|
-
const s = {
|
|
113
|
-
id,
|
|
114
|
-
header: { version: 1, id, createdAt: Date.now(), cwd: WS, parentSession, isSeeded: false },
|
|
115
|
-
inheritedEventCount: 0,
|
|
116
|
-
get seq() { return events.length },
|
|
117
|
-
append(type, data) { const ev = { type, data, seq: events.length, time: Date.now() }; events.push(ev); projections._drive(s, ev); return ev },
|
|
118
|
-
deriveMessages() { return [] },
|
|
119
|
-
snapshotEvents(from) { return events.slice(from || 0) },
|
|
120
|
-
ownEvents() { return events.slice() },
|
|
121
|
-
_events: events,
|
|
122
|
-
}
|
|
123
|
-
return s
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const roots = new Map()
|
|
127
|
-
let rootSeq = 0
|
|
128
|
-
function makeRoot() {
|
|
129
|
-
const id = 'sess-' + String.fromCharCode(65 + rootSeq++)
|
|
130
|
-
const session = makeMockSession(id, undefined)
|
|
131
|
-
const root = { id, options: { provider: 'mock', model: 'm' }, session, ctx: undefined }
|
|
132
|
-
roots.set(id, root)
|
|
133
|
-
return root
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const ctx = {
|
|
137
|
-
get(name) {
|
|
138
|
-
if (name === 'sessionProjections') return projections
|
|
139
|
-
if (name === 'sandboxPolicy') return undefined
|
|
140
|
-
if (name === 'compaction') return undefined
|
|
141
|
-
if (name === 'subprocess') return noSubprocess ? undefined : subprocess
|
|
142
|
-
return undefined
|
|
143
|
-
},
|
|
144
|
-
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
145
|
-
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
146
|
-
logger: { info() {}, warn() {}, error() {} },
|
|
147
|
-
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
148
|
-
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
149
|
-
commands: { register() { return () => {} } },
|
|
150
|
-
sessions: { async flush() { return true } },
|
|
151
|
-
subagents: {
|
|
152
|
-
list() { return ['spawn'] },
|
|
153
|
-
async startContinuable({ label, request }) {
|
|
154
|
-
const rootId = (request && request.parent && request.parent.id) || 'sess-A'
|
|
155
|
-
const id = 'c' + (spawns.length + 1)
|
|
156
|
-
liveAgents.set(id, { id, session: makeMockSession(id, rootId), options: request && request.agentOptions })
|
|
157
|
-
spawns.push({ label, childId: id, rootId, persona: request && request.persona, prompt: request && request.prompt && request.prompt[0] && request.prompt[0].text })
|
|
158
|
-
return { childId: id, messageId: 'm' + spawns.length }
|
|
159
|
-
},
|
|
160
|
-
async sendMessage(parent, childId, blocks) {
|
|
161
|
-
wakes.push({ childId, rootId: (parent && parent.id) || 'sess-A', prompt: (blocks && blocks[0] && blocks[0].text) || '' })
|
|
162
|
-
return 'w' + (wakes.length + delivered.length)
|
|
163
|
-
},
|
|
164
|
-
interrupt() {},
|
|
165
|
-
async drainContinuableChildren(parent, ids) { for (const i of ids) liveAgents.delete(i) },
|
|
166
|
-
},
|
|
167
|
-
agents: {
|
|
168
|
-
roots() { return [...roots.values()] },
|
|
169
|
-
get(id) { return roots.get(id) || liveAgents.get(id) },
|
|
170
|
-
list() { return [...roots.values(), ...liveAgents.values()] },
|
|
171
|
-
},
|
|
172
|
-
fs: {
|
|
173
|
-
async resolve(rel, opts) {
|
|
174
|
-
const b = (opts && opts.cwd) || WS
|
|
175
|
-
const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/'))
|
|
176
|
-
return { targetKey: p, displayPath: p }
|
|
177
|
-
},
|
|
178
|
-
async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
|
|
179
|
-
async readText(t) { return readFileSync(t.targetKey, 'utf8') },
|
|
180
|
-
async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
|
|
181
|
-
async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
182
|
-
},
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const mod = await import(PLUGIN.href + '?t=' + Date.now())
|
|
186
|
-
;(mod.default || mod).apply(ctx)
|
|
187
|
-
|
|
188
|
-
// ---------------------------------------------------------------
|
|
189
|
-
// driving helpers
|
|
190
|
-
// ---------------------------------------------------------------
|
|
191
|
-
async function callTool(name, args, agent) {
|
|
192
|
-
const spec = toolRegs.find(x => x.name === name)
|
|
193
|
-
if (!spec) throw new Error('no tool ' + name)
|
|
194
|
-
return JSON.parse(await spec.execute(args || {}, { agent }))
|
|
195
|
-
}
|
|
196
|
-
const childAgent = (childId) => liveAgents.get(childId)
|
|
197
|
-
function fireEnd(childId, reply) {
|
|
198
|
-
const blocks = reply === undefined ? [] : [{ type: 'text', text: '```json\n' + JSON.stringify(reply) + '\n```' }]
|
|
199
|
-
for (const h of (listeners['subagent/end'] || [])) {
|
|
200
|
-
h({ id: childId, runId: 'r', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: blocks })
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
const settle = async () => { await sleep(30) }
|
|
204
|
-
const memberOfChild = (childId) => { const s = spawns.find(x => x.childId === childId); const m = s ? /vibe5 (\S+) /.exec(s.label) : null; return m ? m[1] : '' }
|
|
205
|
-
const spawnOf = (root, id) => spawns.find(s => s.rootId === root.id && s.label.indexOf('vibe5 ' + id + ' ') !== -1)
|
|
206
|
-
const childOf = (root, id) => { const s = spawnOf(root, id); return s ? s.childId : '' }
|
|
207
|
-
const spawnsFor = (root) => spawns.filter(s => s.rootId === root.id)
|
|
208
|
-
|
|
209
|
-
let votePlan = new Map()
|
|
210
|
-
async function drainWakes(budget, root) {
|
|
211
|
-
let n = 0
|
|
212
|
-
while (n < budget) {
|
|
213
|
-
const idx = wakes.findIndex(w => !root || w.rootId === root.id)
|
|
214
|
-
if (idx === -1) break
|
|
215
|
-
const w = wakes.splice(idx, 1)[0]
|
|
216
|
-
const owner = memberOfChild(w.childId)
|
|
217
|
-
delivered.push({ prompt: w.prompt, owner, rootId: w.rootId })
|
|
218
|
-
let reply
|
|
219
|
-
if (/【求真表决/.test(w.prompt)) {
|
|
220
|
-
const target = (/"target"\s*:\s*"([^"]+)"/.exec(w.prompt) || [])[1] || ''
|
|
221
|
-
const v = votePlan.has(owner) ? votePlan.get(owner) : 0.5
|
|
222
|
-
reply = { verdict: { target, verdict: v, reason: owner + ' 的判断' }, contextPct: 20 }
|
|
223
|
-
} else if (/【研究所会议/.test(w.prompt)) reply = { input: owner + ':意见。', solved: false, contextPct: 20 }
|
|
224
|
-
else reply = { progress: owner + ':继续推进。', solved: false, contextPct: 20 }
|
|
225
|
-
fireEnd(w.childId, reply)
|
|
226
|
-
n++
|
|
227
|
-
await settle()
|
|
228
|
-
}
|
|
229
|
-
return n
|
|
230
|
-
}
|
|
231
|
-
async function settleInstitute(root, rounds = 12) {
|
|
232
|
-
for (let i = 0; i < rounds; i++) {
|
|
233
|
-
await drainWakes(40, root)
|
|
234
|
-
await sleep(20)
|
|
235
|
-
const st = await callTool('vibe_v5_status', {}, root)
|
|
236
|
-
if (!st.members.some(m => m.busy) && !st.meeting && !st.verify && wakes.filter(w => w.rootId === root.id).length === 0) return st
|
|
237
|
-
}
|
|
238
|
-
return await callTool('vibe_v5_status', {}, root)
|
|
239
|
-
}
|
|
240
|
-
async function foundInstitute(root, problem, params) {
|
|
241
|
-
await callTool('vibe_v5_start', Object.assign({ problem, researcherCount: 2 }, params || {}), root)
|
|
242
|
-
for (const sp of spawnsFor(root)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初见解。', solved: false, contextPct: 10 }); await settle() }
|
|
243
|
-
await callTool('vibe_v5_set', { maxParallel: 8 }, root)
|
|
244
|
-
return await settleInstitute(root)
|
|
245
|
-
}
|
|
246
|
-
// Vote a verdict through to completion. `waitRounds` bounds the debate loop.
|
|
247
|
-
async function voteToConclusion(root, target, plan, rounds = 5) {
|
|
248
|
-
votePlan = plan
|
|
249
|
-
for (let i = 0; i < rounds; i++) {
|
|
250
|
-
delivered.length = 0
|
|
251
|
-
await settle(); await drainWakes(10, root)
|
|
252
|
-
const st = await callTool('vibe_v5_status', {}, root)
|
|
253
|
-
if (!st.verify || st.verify.target !== target) return st
|
|
254
|
-
if (st.undecided.indexOf(target) !== -1 || st.verified.indexOf(target) !== -1) return st
|
|
255
|
-
}
|
|
256
|
-
return await callTool('vibe_v5_status', {}, root)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Deterministically wake ONE member and answer with a chosen reply.
|
|
260
|
-
// Firing an end for a member that has no in-flight turn is a no-op (the in-flight token is
|
|
261
|
-
// what makes onMemberEnd honour the event), so the wake must be created first.
|
|
262
|
-
async function wakeAndReply(root, memberId, reply, fromMember) {
|
|
263
|
-
delivered.length = 0
|
|
264
|
-
await callTool('vibe_v5_say', { to: memberId, text: '请处理这件事。' }, childAgent(childOf(root, fromMember || 'acad')))
|
|
265
|
-
await settle()
|
|
266
|
-
const idx = wakes.findIndex(w => w.rootId === root.id && memberOfChild(w.childId) === memberId)
|
|
267
|
-
if (idx === -1) return null
|
|
268
|
-
const w = wakes.splice(idx, 1)[0]
|
|
269
|
-
fireEnd(w.childId, reply)
|
|
270
|
-
await settle()
|
|
271
|
-
await drainWakes(8, root)
|
|
272
|
-
return w
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const instRootOf = (root) => join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute')
|
|
276
|
-
const vibeRoot = join(WS, 'VibeMath')
|
|
277
|
-
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
278
|
-
// Every Lean tool mention in AGENT-FACING text must be the registered name (vibe_v5_lean_*).
|
|
279
|
-
// An abbreviated `lean_archive` is not a tool: an agent that copies it calls nothing.
|
|
280
|
-
const noBareLeanTool = (t) => !/(^|[^a-z_])lean_(run|archive|lib)/.test(String(t || ''))
|
|
281
|
-
|
|
282
|
-
// ===============================================================
|
|
283
|
-
console.log('-- V5 Lean formal verification --')
|
|
284
|
-
|
|
285
|
-
// ---------- 1. 'off' is a true no-op ----------
|
|
286
|
-
section("1 'off' (default) is a true no-op")
|
|
287
|
-
const RA = makeRoot()
|
|
288
|
-
const st0 = await foundInstitute(RA, '形式化开关默认关闭测试')
|
|
289
|
-
assert(st0.params.formalVerify === 'off', "the default is 'off' (got " + st0.params.formalVerify + ')')
|
|
290
|
-
{
|
|
291
|
-
const allPrompts = spawnsFor(RA).map(s => s.prompt || '').join('\n') + '\n' + delivered.filter(d => d.rootId === RA.id).map(d => d.prompt).join('\n')
|
|
292
|
-
assert(!/Lean/.test(allPrompts), 'no founding/round prompt mentions Lean in off mode')
|
|
293
|
-
assert(!/形式化/.test(allPrompts), 'no prompt mentions 形式化 in off mode')
|
|
294
|
-
assert(!/\[形式化\]/.test(allPrompts), 'the [形式化] state line is absent in off mode')
|
|
295
|
-
}
|
|
296
|
-
// an off-mode verification must behave exactly as before (no gate at all)
|
|
297
|
-
await callTool('vibe_v5_record_proposition', { id: 'p-off', statement: '关模式下的普通命题', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RA, 'r-1')))
|
|
298
|
-
await callTool('vibe_v5_propose_verify', { target: 'p-off', kind: 'proposition', reason: '直接表决' }, childAgent(childOf(RA, 'r-1')))
|
|
299
|
-
const offDone = await voteToConclusion(RA, 'p-off', new Map([['acad', 1], ['r-1', 1], ['r-2', 1]]))
|
|
300
|
-
assert(offDone.verified.indexOf('p-off') !== -1, "'off' mode still finalizes on a unanimous boolean vote with NO Lean artifact")
|
|
301
|
-
assert(existsSync(join(instRootOf(RA), 'Verified', '命题', 'p-off.md')), "the Verified card was written in 'off' mode")
|
|
302
|
-
assert(!/形式化/.test(readIf(join(instRootOf(RA), 'Verified', '命题', 'p-off.md'))), 'the card carries no formal line in off mode')
|
|
303
|
-
// the tools still EXIST in off mode (static registration), they are just never advertised
|
|
304
|
-
assert(!!toolRegs.find(t => t.name === 'vibe_v5_lean_run') && !!toolRegs.find(t => t.name === 'vibe_v5_lean_archive') && !!toolRegs.find(t => t.name === 'vibe_v5_lean_lib'),
|
|
305
|
-
'the three Lean tools are registered in every mode (registration is static)')
|
|
306
|
-
|
|
307
|
-
// ★ The mode switch must be REACHABLE THROUGH THE TOOL SCHEMA (2.3.2 defect D1) ──────────────
|
|
308
|
-
// Every tool schema here is closed (`additionalProperties:false`), so a key the schema does not
|
|
309
|
-
// advertise is REJECTED by any schema-validating provider. v3 shipped 2.3.0/2.3.1 with all four Lean
|
|
310
|
-
// parameters missing from the set-params schema while every assertion in this file stayed green —
|
|
311
|
-
// because the suite calls the handler DIRECTLY and never inspects the registered schema. The feature
|
|
312
|
-
// could not be switched on at all through the tool interface.
|
|
313
|
-
{
|
|
314
|
-
const setSpec = toolRegs.find((t) => t.name === 'vibe_v5_set')
|
|
315
|
-
assert(!!setSpec, "vibe_v5_set is registered")
|
|
316
|
-
assert(setSpec.parameters && setSpec.parameters.type === 'object' && setSpec.parameters.additionalProperties === false,
|
|
317
|
-
'★ vibe_v5_set publishes a CLOSED object schema (an unlisted key is rejected, so the schema IS the contract)')
|
|
318
|
-
for (const k of ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']) {
|
|
319
|
-
assert(Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k),
|
|
320
|
-
'★ the registered schema advertises ' + k + ' (every other surface documents it; a schema that omits it makes the switch unreachable)')
|
|
321
|
-
}
|
|
322
|
-
assert(JSON.stringify(setSpec.parameters.properties.formalVerify.enum) === JSON.stringify(['off', 'encourage', 'require']),
|
|
323
|
-
'the schema narrows formalVerify to the three real modes (a typo must not be a fourth)')
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// A stray `formal` reply in OFF mode must be INERT: the field is not offered in the reply contract
|
|
327
|
-
// there, and honouring it would create Formal/ state in a mode documented as a "TRUE no-op" (the
|
|
328
|
-
// TOOLS stay usable on purpose — a tool call is deliberate, a stray reply field is not).
|
|
329
|
-
{
|
|
330
|
-
const wOff = await wakeAndReply(RA, 'r-1', {
|
|
331
|
-
progress: '关模式下的普通回轮。',
|
|
332
|
-
formal: { target: 'p-off-stray', decision: 'blocked', note: '不应被记录' },
|
|
333
|
-
contextPct: 20,
|
|
334
|
-
})
|
|
335
|
-
assert(!!wOff, 'off mode: a wake carrying a stray formal reply was fed to the framework')
|
|
336
|
-
const stOff2 = await callTool('vibe_v5_status', {}, RA)
|
|
337
|
-
assert((stOff2.formal.objects || []).length === 0, '★ a stray `formal` reply in off mode records NO formal object')
|
|
338
|
-
assert((stOff2.formal.todo || []).length === 0, '★ and adds nothing to the formalization TODO')
|
|
339
|
-
assert(!/p-off-stray/.test(readIf(join(instRootOf(RA), 'Formal', 'TODO.md'))), '★ and writes no TODO entry for it')
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// ---------- 2. parameter validation ----------
|
|
343
|
-
section('2 parameter validation and runtime switching')
|
|
344
|
-
const RB = makeRoot()
|
|
345
|
-
await foundInstitute(RB, '形式化参数校验测试')
|
|
346
|
-
const bad = await callTool('vibe_v5_set', { formalVerify: 'banana' }, RB)
|
|
347
|
-
assert(bad.params.formalVerify === 'off', "an unknown mode degrades to 'off', never to a stronger mode (got " + bad.params.formalVerify + ')')
|
|
348
|
-
const enc = await callTool('vibe_v5_set', { formalVerify: 'encourage' }, RB)
|
|
349
|
-
assert(enc.params.formalVerify === 'encourage', "'encourage' is accepted")
|
|
350
|
-
const req = await callTool('vibe_v5_set', { formalVerify: 'require', leanTimeoutMs: -5, leanCommand: ' ' }, RB)
|
|
351
|
-
assert(req.params.formalVerify === 'require', "'require' is accepted")
|
|
352
|
-
assert(req.params.leanTimeoutMs === 120000, 'a non-positive leanTimeoutMs falls back to the default (' + req.params.leanTimeoutMs + ')')
|
|
353
|
-
assert(req.params.leanCommand === 'lean', 'a blank leanCommand falls back to "lean"')
|
|
354
|
-
await callTool('vibe_v5_set', { leanCommand: 'lake', leanArgs: ['env', 'lean'] }, RB)
|
|
355
|
-
const stL = await callTool('vibe_v5_status', {}, RB)
|
|
356
|
-
assert(stL.params.leanCommand === 'lake' && stL.params.leanArgs.join(' ') === 'env lean', 'leanCommand/leanArgs are settable (lake env lean)')
|
|
357
|
-
|
|
358
|
-
// ---------- 3. 'encourage' injection ----------
|
|
359
|
-
section("3 'encourage' injects the Lean section into the right prompts")
|
|
360
|
-
const RC = makeRoot()
|
|
361
|
-
await foundInstitute(RC, '鼓励模式注入测试')
|
|
362
|
-
await callTool('vibe_v5_set', { formalVerify: 'encourage' }, RC)
|
|
363
|
-
{
|
|
364
|
-
// a plain work round must carry the "formalize reusable things as you go" line
|
|
365
|
-
delivered.length = 0
|
|
366
|
-
await callTool('vibe_v5_say', { to: 'r-1', text: '请继续。' }, childAgent(childOf(RC, 'acad')))
|
|
367
|
-
await settle(); await drainWakes(6, RC)
|
|
368
|
-
const work = delivered.filter(d => d.rootId === RC.id).map(d => d.prompt).join('\n')
|
|
369
|
-
assert(/\[形式化\] 鼓励 Lean/.test(work), "the state block gains a [形式化] 鼓励 Lean line")
|
|
370
|
-
assert(/【顺手形式化(鼓励)】/.test(work), 'the work round tells members to formalize reusable objects as they go')
|
|
371
|
-
assert(/vibe_v5_lean_archive kind='def'/.test(work), 'the work round points at the archive tool for reusable definitions')
|
|
372
|
-
assert(noBareLeanTool(work), 'no abbreviated tool name appears in the injected work-round prompt')
|
|
373
|
-
assert(/归档前先跑通/.test(work), 'the work-round prompt requires a green run before archiving into the reuse library')
|
|
374
|
-
}
|
|
375
|
-
await callTool('vibe_v5_record_proposition', { id: 'p-enc', statement: '鼓励模式下的忠实性审查', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RC, 'r-1')))
|
|
376
|
-
await callTool('vibe_v5_propose_verify', { target: 'p-enc', kind: 'proposition', reason: '先看看提示词' }, childAgent(childOf(RC, 'r-1')))
|
|
377
|
-
await settle(); delivered.length = 0; await drainWakes(3, RC)
|
|
378
|
-
{
|
|
379
|
-
const vp = delivered.filter(d => d.rootId === RC.id).map(d => d.prompt).join('\n')
|
|
380
|
-
assert(/【Lean 形式化验证(鼓励模式)】/.test(vp), 'the voting prompt explains the Lean mode')
|
|
381
|
-
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(vp), 'the voting prompt states that a passing Lean run shrinks the question to fidelity')
|
|
382
|
-
assert(/归档可复用定义\/引理前先跑通/.test(vp), 'the voting prompt requires a GREEN RUN before archiving a reusable definition')
|
|
383
|
-
assert(/LEAN_NOT_FOUND/.test(vp) && /宿主无 Lean 工具链/.test(vp), 'the voting prompt says what to do when the host has no Lean toolchain')
|
|
384
|
-
assert(/NO_SUBPROCESS/.test(vp), '§6 rule 4: the no-toolchain route names NO_SUBPROCESS too (an agent that only knows LEAN_NOT_FOUND treats a service-less host as an unknown failure and retries)')
|
|
385
|
-
assert(noBareLeanTool(vp), 'no abbreviated tool name appears in the injected voting prompt')
|
|
386
|
-
assert(/实现难度/.test(vp), 'the voting prompt asks for the implementation-difficulty judgement')
|
|
387
|
-
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(vp), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
388
|
-
assert(/"formal":/.test(vp), 'the reply contract documents the formal field')
|
|
389
|
-
assert(/"decision":"used\|blocked\|defect"/.test(vp), '★ the reply contract offers the defect decision (a faithfulness defect is recordable)')
|
|
390
|
-
}
|
|
391
|
-
await drainWakes(10, RC)
|
|
392
|
-
|
|
393
|
-
// ---------- 4. the run tool ----------
|
|
394
|
-
section('4 lean_run executes through the subprocess service and reports honestly')
|
|
395
|
-
const RD = makeRoot()
|
|
396
|
-
await foundInstitute(RD, 'Lean 运行测试')
|
|
397
|
-
await callTool('vibe_v5_set', { formalVerify: 'encourage' }, RD)
|
|
398
|
-
const instD = instRootOf(RD)
|
|
399
|
-
mkdirSync(join(instD, 'Formal'), { recursive: true })
|
|
400
|
-
writeFileSync(join(instD, 'Formal', 'good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
401
|
-
writeFileSync(join(instD, 'Formal', 'bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
|
|
402
|
-
const runGood = await callTool('vibe_v5_lean_run', { file: 'Formal/good.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
403
|
-
assert(runGood.ok === true && runGood.exitCode === 0, 'a file with no sorry runs green (' + JSON.stringify({ ok: runGood.ok, exitCode: runGood.exitCode }) + ')')
|
|
404
|
-
assert(leanRuns.length > 0 && leanRuns[leanRuns.length - 1].cwd.replace(/\\/g, '/') === instD.replace(/\\/g, '/'), 'the toolchain runs with the institute root as cwd')
|
|
405
|
-
const runBad = await callTool('vibe_v5_lean_run', { file: 'Formal/bad.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
406
|
-
assert(runBad.ok === false && runBad.exitCode === 1, 'a file that still uses sorry reports a red run')
|
|
407
|
-
assert(/sorry/.test(runBad.stderr), 'the compiler output is returned verbatim (' + JSON.stringify(runBad.stderr).slice(0, 60) + ')')
|
|
408
|
-
const runMissing = await callTool('vibe_v5_lean_run', { file: 'Formal/nope.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
409
|
-
assert(runMissing.ok === false && runMissing.code === 'V5_NOT_FOUND', 'a missing file is refused with a typed code')
|
|
410
|
-
// The guard's boundary is the VibeMath ROOT, not the institute: the global reuse library
|
|
411
|
-
// deliberately lives at <VibeMath>/Formal/{Lib,Proved}, so climbing out of the institute
|
|
412
|
-
// but staying inside VibeMath must remain legal (it just fails as a missing file).
|
|
413
|
-
const runOutsideInst = await callTool('vibe_v5_lean_run', { file: '../../../Formal/Lib/x.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
414
|
-
assert(runOutsideInst.code === 'V5_NOT_FOUND', 'climbing out of the institute but staying inside VibeMath is allowed (the global library lives there)')
|
|
415
|
-
const runEscape = await callTool('vibe_v5_lean_run', { file: '../../../../../etc/evil.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
416
|
-
assert(runEscape.ok === false && runEscape.code === 'V5_INVALID_ARGUMENT', '★ a traversal that climbs ABOVE the VibeMath root is refused')
|
|
417
|
-
const runEscape2 = await callTool('vibe_v5_lean_run', { file: 'Formal/../../../../../../evil.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
418
|
-
assert(runEscape2.ok === false && runEscape2.code === 'V5_INVALID_ARGUMENT', 'a deeper traversal is refused too')
|
|
419
|
-
const runEscape3 = await callTool('vibe_v5_lean_run', { file: '/etc/evil.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
420
|
-
assert(runEscape3.ok === false && runEscape3.code === 'V5_INVALID_ARGUMENT', 'an unrelated absolute path is refused')
|
|
421
|
-
const runNotLean = await callTool('vibe_v5_lean_run', { file: 'Formal/good.txt' }, childAgent(childOf(RD, 'r-1')))
|
|
422
|
-
assert(runNotLean.ok === false && runNotLean.code === 'V5_INVALID_ARGUMENT', 'only .lean files can be executed')
|
|
423
|
-
toolchainAvailable = false
|
|
424
|
-
const runNoTc = await callTool('vibe_v5_lean_run', { file: 'Formal/good.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
425
|
-
assert(runNoTc.ok === false && runNoTc.code === 'LEAN_NOT_FOUND', 'a missing toolchain returns LEAN_NOT_FOUND instead of crashing')
|
|
426
|
-
assert(/仍可把形式化代码写下来归档/.test(runNoTc.message), 'the failure explains the graceful degradation')
|
|
427
|
-
toolchainAvailable = true
|
|
428
|
-
// docs/formal-verification.md §7: a TIMEOUT must TERMINATE the process. `graceMs` is only a
|
|
429
|
-
// request to the host, so a run whose `done` never settles within the cap must be ended by
|
|
430
|
-
// `handle.terminate()` — otherwise a runaway toolchain lingers while we report LEAN_TIMEOUT.
|
|
431
|
-
{
|
|
432
|
-
const before = terminations.length
|
|
433
|
-
hangLean = true
|
|
434
|
-
hangMs = 60000
|
|
435
|
-
const runHang = await callTool('vibe_v5_lean_run', { file: 'Formal/good.lean', timeout_ms: 1000 }, childAgent(childOf(RD, 'r-1')))
|
|
436
|
-
hangLean = false
|
|
437
|
-
assert(runHang.ok === false && runHang.code === 'LEAN_TIMEOUT', '★ a run that outlives the cap is reported as LEAN_TIMEOUT (got ' + runHang.code + ')')
|
|
438
|
-
assert(runHang.timedOut === true, 'the result is flagged timedOut')
|
|
439
|
-
assert(terminations.slice(before).some(f => /good\.lean$/.test(String(f))), '★ the timeout path really called handle.terminate() (the toolchain is not left running)')
|
|
440
|
-
assert(runHang.signal === 'SIGTERM', 'the synthetic outcome names the signal that ended it')
|
|
441
|
-
}
|
|
442
|
-
// A host that exposes NO `subprocess` service must produce the readable NO_SUBPROCESS result
|
|
443
|
-
// (never a throw into the scheduler), must leave the object record honest (`attempted`, and NOT
|
|
444
|
-
// passed), and must still let the code be written down through the archive route.
|
|
445
|
-
{
|
|
446
|
-
noSubprocess = true
|
|
447
|
-
const runNoSvc = await callTool('vibe_v5_lean_run', { file: 'Formal/good.lean', target: 'p-nosub' }, childAgent(childOf(RD, 'r-1')))
|
|
448
|
-
assert(runNoSvc.ok === false && runNoSvc.code === 'NO_SUBPROCESS', '★ a host with no subprocess service returns NO_SUBPROCESS instead of throwing (got ' + runNoSvc.code + ')')
|
|
449
|
-
assert(typeof runNoSvc.message === 'string' && /no subprocess service/.test(runNoSvc.message), 'the result explains why Lean cannot run here')
|
|
450
|
-
const stNoSvc = await callTool('vibe_v5_status', {}, RD)
|
|
451
|
-
assert(stNoSvc.ok === true, 'the institute still answers status after that (nothing was thrown into the scheduling loop)')
|
|
452
|
-
const recNoSvc = (stNoSvc.formal.objects || []).find(o => o.target === 'p-nosub') || {}
|
|
453
|
-
assert(recNoSvc.status === 'attempted', 'the run is recorded as attempted (no toolchain = no proof), got ' + recNoSvc.status)
|
|
454
|
-
assert(!recNoSvc.proof, 'and it is NOT recorded as passed')
|
|
455
|
-
// The way out (contract §6 rule 4): the code can still be written down. The archive route
|
|
456
|
-
// itself needs no toolchain at all.
|
|
457
|
-
const arcNoSvc = await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-nosub', content: 'theorem p_nosub : 1 + 1 = 2 := by decide\n' }, childAgent(childOf(RD, 'r-1')))
|
|
458
|
-
assert(arcNoSvc.ok === true && arcNoSvc.passed === false && arcNoSvc.run && arcNoSvc.run.code === 'NO_SUBPROCESS',
|
|
459
|
-
'the code is still written down and archived with no toolchain, and the record stays honest (' + JSON.stringify({ ok: arcNoSvc.ok, status: arcNoSvc.status, code: arcNoSvc.run && arcNoSvc.run.code }) + ')')
|
|
460
|
-
assert(existsSync(join(instD, 'Formal', 'p-nosub.lean')), 'the working file exists on disk even though nothing could execute it')
|
|
461
|
-
assert(!existsSync(join(instD, 'Verified', 'Lean', 'p-nosub.lean')), '★ nothing is promoted to Verified/Lean/ without a green run')
|
|
462
|
-
noSubprocess = false
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// ---------- 5. archive a proof → the vote becomes a FIDELITY review ----------
|
|
466
|
-
section('5 a passing proof flips the review subject to fidelity')
|
|
467
|
-
const arc = await callTool('vibe_v5_lean_archive', {
|
|
468
|
-
kind: 'proof', target: 'p-proof', content: 'theorem p_proof : 3 * 1 ^ 2 - 2 = (1:Nat) ^ 2 := by decide\n', note: '',
|
|
469
|
-
}, childAgent(childOf(RD, 'r-1')))
|
|
470
|
-
assert(arc.ok === true && arc.passed === true, 'the proof is archived and passes (' + JSON.stringify({ ok: arc.ok, passed: arc.passed }) + ')')
|
|
471
|
-
assert(arc.file === 'Formal/p-proof.lean', 'the working file is Formal/<target>.lean')
|
|
472
|
-
assert(arc.proof === 'Verified/Lean/p-proof.lean', 'the archived proof path is Verified/Lean/<target>.lean')
|
|
473
|
-
assert(existsSync(join(instD, 'Formal', 'p-proof.lean')), 'the working file exists on disk')
|
|
474
|
-
assert(existsSync(join(instD, 'Verified', 'Lean', 'p-proof.lean')), '★ the proof is archived under Verified/Lean/ as the proof of that object')
|
|
475
|
-
const stD = await callTool('vibe_v5_status', {}, RD)
|
|
476
|
-
assert(stD.formal.passed.indexOf('p-proof') !== -1, 'status reports the object as Lean-passed')
|
|
477
|
-
const idxD = readIf(join(instD, 'Formal', 'Index.md'))
|
|
478
|
-
assert(/p-proof/.test(idxD) && /passed/.test(idxD) && /Verified\/Lean\/p-proof\.lean/.test(idxD), 'Formal/Index.md indexes the object, its status and its archived proof')
|
|
479
|
-
// now the voting prompt must ASK FOR FIDELITY, not for a re-derivation
|
|
480
|
-
await callTool('vibe_v5_record_proposition', { id: 'p-proof', statement: '3N²−2=b² 在 N=1 时成立', value: 0.6, motive: 'm', p: 0.9 }, childAgent(childOf(RD, 'r-1')))
|
|
481
|
-
await callTool('vibe_v5_propose_verify', { target: 'p-proof', kind: 'proposition', reason: '已有 Lean 证明' }, childAgent(childOf(RD, 'r-1')))
|
|
482
|
-
await settle(); delivered.length = 0; await drainWakes(3, RD)
|
|
483
|
-
{
|
|
484
|
-
const vp = delivered.filter(d => d.rootId === RD.id).map(d => d.prompt).join('\n')
|
|
485
|
-
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(vp), 'the voting prompt announces the passing proof')
|
|
486
|
-
assert(/你不需要重新检查推导/.test(vp), '★ it tells voters NOT to re-derive')
|
|
487
|
-
assert(/忠实性审查/.test(vp), '★ it tells voters the review subject is now fidelity')
|
|
488
|
-
assert(/定义 \/ 对象 \/ 条件 \/ 假设 \/ 结论是否与命题原文\*\*完全一致\*\*/.test(vp), 'it enumerates exactly what fidelity means')
|
|
489
|
-
assert(/不要投 0/.test(vp), '★ a faithfulness defect must NOT be expressed as 0 (that would record 命题为假)')
|
|
490
|
-
assert(/形式化不合格/.test(vp), 'it names the failure a formalisation defect, not a refutation')
|
|
491
|
-
assert(/decision:'defect'/.test(vp), 'it names the defect reply channel')
|
|
492
|
-
assert(!/偏离 → 0/.test(vp), '★ the old "any deviation → 0" instruction is GONE')
|
|
493
|
-
assert(/本档没有门禁:请务必给一个严格介于 0 与 1 之间的弃权值,以保证本轮无法得出一致结论/.test(vp),
|
|
494
|
-
'★ encourage mode is explicit that there is NO gate and the voter\'s abstention is what prevents a conclusion (wording shared with v2/v3/v4)')
|
|
495
|
-
assert(noBareLeanTool(vp), 'no abbreviated tool name appears in the fidelity prompt')
|
|
496
|
-
}
|
|
497
|
-
await drainWakes(10, RD)
|
|
498
|
-
|
|
499
|
-
// ---------- 6. the reusable cross-project library ----------
|
|
500
|
-
section('6 reusable definitions and lemmas go to the GLOBAL library')
|
|
501
|
-
const libPath = join(vibeRoot, 'Formal', 'Lib')
|
|
502
|
-
const provedPath = join(vibeRoot, 'Formal', 'Proved')
|
|
503
|
-
const defRes = await callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZMod5', content: 'def ZMod5 := Fin 5\n' }, childAgent(childOf(RD, 'r-1')))
|
|
504
|
-
assert(defRes.ok === true && defRes.file === 'Formal/Lib/ZMod5.lean', 'a reusable definition is archived to the global lib (' + defRes.file + ')')
|
|
505
|
-
assert(existsSync(join(libPath, 'ZMod5.lean')), '★ the definition exists under VibeMath/Formal/Lib/ (cross-project, NOT inside the institute)')
|
|
506
|
-
assert(!existsSync(join(instD, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the institute tree')
|
|
507
|
-
const lemRes = await callTool('vibe_v5_lean_archive', { kind: 'lemma', name: 'sq_odd', content: 'theorem sq_odd (n : Nat) : Odd (n*n) → Odd n := by omega\n' }, childAgent(childOf(RD, 'r-1')))
|
|
508
|
-
assert(lemRes.ok === true && lemRes.file === 'Formal/Proved/sq_odd.lean', 'a lemma is archived to Proved/')
|
|
509
|
-
assert(existsSync(join(provedPath, 'sq_odd.lean')), 'the lemma exists under VibeMath/Formal/Proved/')
|
|
510
|
-
const libIdx = readIf(join(libPath, 'Index.md'))
|
|
511
|
-
assert(/ZMod5/.test(libIdx), 'Lib/Index.md lists the new definition')
|
|
512
|
-
const provedIdx = readIf(join(provedPath, 'Index.md'))
|
|
513
|
-
assert(/sq_odd/.test(provedIdx), 'Proved/Index.md lists the new lemma')
|
|
514
|
-
const libList = await callTool('vibe_v5_lean_lib', {}, childAgent(childOf(RD, 'r-1')))
|
|
515
|
-
assert(libList.ok === true && libList.counts.lib >= 1 && libList.counts.proved >= 1, 'lean_lib reports the reuse library sizes (' + JSON.stringify(libList.counts) + ')')
|
|
516
|
-
assert(libList.objects.some(o => o.target === 'p-proof' && o.status === 'passed'), 'lean_lib lists per-object formal status')
|
|
517
|
-
assert(/复用优先/.test(libList.hint || ''), 'lean_lib tells agents to reuse before redefining')
|
|
518
|
-
const noName = await callTool('vibe_v5_lean_archive', { kind: 'def', content: 'def x := 1\n' }, childAgent(childOf(RD, 'r-1')))
|
|
519
|
-
assert(noName.ok === false && noName.code === 'V5_INVALID_ARGUMENT', 'archiving a definition without a name is refused')
|
|
520
|
-
const badKind = await callTool('vibe_v5_lean_archive', { kind: 'nonsense' }, childAgent(childOf(RD, 'r-1')))
|
|
521
|
-
assert(badKind.ok === false && badKind.code === 'V5_INVALID_ARGUMENT', 'an unknown archive kind is refused')
|
|
522
|
-
|
|
523
|
-
// ---------- 7. blocked needs a reason ----------
|
|
524
|
-
section('7 a "blocker" record must be explicit and reasoned')
|
|
525
|
-
const blkNoNote = await callTool('vibe_v5_lean_archive', { kind: 'blocked', target: 'p-blk' }, childAgent(childOf(RD, 'r-1')))
|
|
526
|
-
assert(blkNoNote.ok === false && blkNoNote.code === 'V5_INVALID_ARGUMENT', 'blocked without a note is refused')
|
|
527
|
-
const blk = await callTool('vibe_v5_lean_archive', { kind: 'blocked', target: 'p-blk', note: '需要外层解析数论框架,本轮工作量不可接受' }, childAgent(childOf(RD, 'r-1')))
|
|
528
|
-
assert(blk.ok === true && blk.status === 'blocked', 'a reasoned blocker is recorded')
|
|
529
|
-
const stBlk = await callTool('vibe_v5_status', {}, RD)
|
|
530
|
-
assert(stBlk.formal.blocked.indexOf('p-blk') !== -1, 'status lists the blocked object')
|
|
531
|
-
assert(/需要外层解析数论框架/.test(readIf(join(instD, 'Formal', 'Index.md'))), 'the blocker reason is written into the index')
|
|
532
|
-
|
|
533
|
-
// ---------- 8. the 'require' gate ----------
|
|
534
|
-
section("8 'require' withholds a verdict until the formal record exists")
|
|
535
|
-
const RE = makeRoot()
|
|
536
|
-
await foundInstitute(RE, 'require 门禁测试')
|
|
537
|
-
await callTool('vibe_v5_set', { formalVerify: 'require' }, RE)
|
|
538
|
-
const instE = instRootOf(RE)
|
|
539
|
-
await callTool('vibe_v5_record_proposition', { id: 'p-gate', statement: '必须形式化的命题', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RE, 'r-1')))
|
|
540
|
-
await callTool('vibe_v5_propose_verify', { target: 'p-gate', kind: 'proposition', reason: '直接表决试试' }, childAgent(childOf(RE, 'r-1')))
|
|
541
|
-
await settle(); delivered.length = 0; await drainWakes(3, RE)
|
|
542
|
-
{
|
|
543
|
-
const vp = delivered.filter(d => d.rootId === RE.id).map(d => d.prompt).join('\n')
|
|
544
|
-
assert(/【Lean 形式化验证(强制模式)】/.test(vp), 'the voting prompt says 强制模式')
|
|
545
|
-
assert(/必须产出 Lean 形式化/.test(vp), "'require' states the formalization is mandatory")
|
|
546
|
-
assert(/本次裁定不会生效/.test(vp), 'the prompt warns that the verdict will not take effect without it')
|
|
547
|
-
assert(/formal-required/.test(vp), 'it names the machine-readable reason code in the prompt itself')
|
|
548
|
-
assert(/vibe_v5_lean_archive/.test(vp) && /kind='blocked'/.test(vp), 'it gives the full tool name for the blocker route')
|
|
549
|
-
}
|
|
550
|
-
const gated = await voteToConclusion(RE, 'p-gate', new Map([['acad', 1], ['r-1', 1], ['r-2', 1]]))
|
|
551
|
-
assert(gated.verified.indexOf('p-gate') === -1, '★ a unanimous TRUE verdict did NOT promote the object to Verified/')
|
|
552
|
-
assert(gated.undecided.indexOf('p-gate') !== -1, '★ it is recorded as 未定论 instead')
|
|
553
|
-
assert(!existsSync(join(instE, 'Verified', '命题', 'p-gate.md')), 'no Verified card was written')
|
|
554
|
-
const todoE = readIf(join(instE, 'Formal', 'TODO.md'))
|
|
555
|
-
assert(existsSync(join(instE, 'Formal', 'TODO.md')), 'Formal/TODO.md was created')
|
|
556
|
-
assert(/p-gate/.test(todoE) && /formal-required/.test(todoE), '★ the object is on the formalization TODO with the machine-readable reason')
|
|
557
|
-
{
|
|
558
|
-
const chat = readdirSync(join(instE, 'Shared', 'Chat')).map(f => readIf(join(instE, 'Shared', 'Chat', f))).join('\n')
|
|
559
|
-
assert(/require 模式.*先有 Lean 通过或显式阻塞记录/.test(chat), 'the withholding is announced in the group chat')
|
|
560
|
-
}
|
|
561
|
-
// now formalize it and re-verify: the gate must open
|
|
562
|
-
const proofNow = await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-gate', content: 'theorem p_gate : 2 + 2 = 4 := by decide\n' }, childAgent(childOf(RE, 'r-1')))
|
|
563
|
-
assert(proofNow.ok === true && proofNow.passed === true, 'the object is now Lean-passed')
|
|
564
|
-
await callTool('vibe_v5_propose_verify', { target: 'p-gate', kind: 'proposition', reason: '已形式化,重新表决' }, childAgent(childOf(RE, 'r-1')))
|
|
565
|
-
const gated2 = await voteToConclusion(RE, 'p-gate', new Map([['acad', 1], ['r-1', 1], ['r-2', 1]]))
|
|
566
|
-
assert(gated2.verified.indexOf('p-gate') !== -1, '★ with a passing Lean artifact the same vote DOES promote it')
|
|
567
|
-
const card = readIf(join(instE, 'Verified', '命题', 'p-gate.md'))
|
|
568
|
-
assert(/- 形式化: Lean 通过/.test(card), '★ the Verified card records how strong the result is (Lean 通过)')
|
|
569
|
-
assert(/Verified\/Lean\/p-gate\.lean/.test(card), 'the card points at the archived proof')
|
|
570
|
-
// the blocker escape hatch must also open the gate
|
|
571
|
-
await callTool('vibe_v5_record_proposition', { id: 'p-blocked-ok', statement: '记录阻塞后可定论', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RE, 'r-1')))
|
|
572
|
-
await callTool('vibe_v5_lean_archive', { kind: 'blocked', target: 'p-blocked-ok', note: '命题涉及未形式化的分析学,本轮不做' }, childAgent(childOf(RE, 'r-1')))
|
|
573
|
-
await callTool('vibe_v5_propose_verify', { target: 'p-blocked-ok', kind: 'proposition', reason: '已记录阻塞' }, childAgent(childOf(RE, 'r-1')))
|
|
574
|
-
const gated3 = await voteToConclusion(RE, 'p-blocked-ok', new Map([['acad', 1], ['r-1', 1], ['r-2', 1]]))
|
|
575
|
-
assert(gated3.verified.indexOf('p-blocked-ok') !== -1, '★ an explicit reasoned blocker also lets the verdict through (decide by difficulty, but decide out loud)')
|
|
576
|
-
assert(/- 形式化: 阻塞(/.test(readIf(join(instE, 'Verified', '命题', 'p-blocked-ok.md'))), 'the card records the blocker')
|
|
577
|
-
|
|
578
|
-
// ---------- 9. the reply-channel judgement ----------
|
|
579
|
-
section('9 the per-round `formal` reply channel records the difficulty judgement')
|
|
580
|
-
const RF = makeRoot()
|
|
581
|
-
await foundInstitute(RF, '回执 formal 通道测试')
|
|
582
|
-
await callTool('vibe_v5_set', { formalVerify: 'require' }, RF)
|
|
583
|
-
const instF = instRootOf(RF)
|
|
584
|
-
await callTool('vibe_v5_record_proposition', { id: 'p-reply', statement: '用回执记录阻塞', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RF, 'r-1')))
|
|
585
|
-
// A member that never calls a Lean tool still has to state its difficulty judgement, so the
|
|
586
|
-
// JSON reply channel matters: it is the path that actually fires in practice.
|
|
587
|
-
const w1 = await wakeAndReply(RF, 'r-1', {
|
|
588
|
-
progress: '难度太高,本轮不做形式化。',
|
|
589
|
-
formal: { target: 'p-reply', decision: 'blocked', note: '需要大量未形式化的实分析前置知识' },
|
|
590
|
-
contextPct: 20,
|
|
591
|
-
})
|
|
592
|
-
assert(!!w1, 'r-1 was woken and answered')
|
|
593
|
-
const stF = await callTool('vibe_v5_status', {}, RF)
|
|
594
|
-
assert(stF.formal.blocked.indexOf('p-reply') !== -1, 'a `formal.decision=blocked` reply is recorded as a blocker')
|
|
595
|
-
assert(/实分析前置知识/.test(readIf(join(instF, 'Formal', 'Index.md'))), 'and its reason reaches the index')
|
|
596
|
-
// a `used` judgement records the file as attempted (not yet passed — nothing ran green)
|
|
597
|
-
const w2 = await wakeAndReply(RF, 'r-2', {
|
|
598
|
-
progress: '我写了形式化草稿。',
|
|
599
|
-
formal: { target: 'p-reply2', decision: 'used', file: 'Formal/p-reply2.lean' },
|
|
600
|
-
contextPct: 20,
|
|
601
|
-
})
|
|
602
|
-
assert(!!w2, 'r-2 was woken and answered')
|
|
603
|
-
const stF2 = await callTool('vibe_v5_status', {}, RF)
|
|
604
|
-
assert(stF2.formal.objects.some(o => o.target === 'p-reply2' && o.status === 'attempted'),
|
|
605
|
-
'a `formal.decision=used` reply records the object as attempted with its file')
|
|
606
|
-
// a plain `used` judgement must NOT withdraw an ESTABLISHED proof (contract §4): only a fidelity
|
|
607
|
-
// defect retracts one. v2 shipped the unconditional downgrade here; all four are asserted behaviourally.
|
|
608
|
-
await callTool('vibe_v5_record_proposition', { id: 'p-usedkeep', statement: '已有通过证明后再写一次 used 回执', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RF, 'r-1')))
|
|
609
|
-
{
|
|
610
|
-
const arcU = await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-usedkeep', content: 'theorem p_usedkeep : 2 + 2 = 4 := by decide\n' }, RF)
|
|
611
|
-
assert(arcU.ok === true && arcU.passed === true, 'used-keep: the object starts Lean-passed')
|
|
612
|
-
const wU = await wakeAndReply(RF, 'r-2', {
|
|
613
|
-
progress: '这一轮只是又写了一遍草稿。',
|
|
614
|
-
formal: { target: 'p-usedkeep', decision: 'used', file: 'Formal/p-usedkeep.lean' },
|
|
615
|
-
contextPct: 20,
|
|
616
|
-
})
|
|
617
|
-
assert(!!wU, 'r-2 was woken for the used judgement')
|
|
618
|
-
const stU = await callTool('vibe_v5_status', {}, RF)
|
|
619
|
-
const recU = stU.formal.objects.find((o) => o.target === 'p-usedkeep')
|
|
620
|
-
assert(!!recU && recU.status === 'passed', '★ a `used` reply does NOT downgrade an already-passed object (got ' + JSON.stringify(recU) + ')')
|
|
621
|
-
assert(!!recU && recU.proof === 'Verified/Lean/p-usedkeep.lean', '★ and the proof pointer survives the reply')
|
|
622
|
-
assert(existsSync(join(instF, 'Verified', 'Lean', 'p-usedkeep.lean')), '★ and the archived proof is still on disk')
|
|
623
|
-
assert(stU.formal.passed.indexOf('p-usedkeep') !== -1, '★ status still reports it as Lean-passed')
|
|
624
|
-
const rerunU = await callTool('vibe_v5_lean_run', { file: 'Formal/p-usedkeep.lean', target: 'p-usedkeep' }, RF)
|
|
625
|
-
assert(rerunU.ok === true, 'used-keep: the work file runs green')
|
|
626
|
-
const recU2 = (await callTool('vibe_v5_status', {}, RF)).formal.objects.find((o) => o.target === 'p-usedkeep')
|
|
627
|
-
assert(!!recU2 && recU2.status === 'passed', '★ a plain re-run does not downgrade a passed record (contract §4)')
|
|
628
|
-
}
|
|
629
|
-
// a blocker with no note must be refused AND reported back to the member
|
|
630
|
-
delivered.length = 0
|
|
631
|
-
const w3 = await wakeAndReply(RF, 'r-1', { formal: { target: 'p-reply3', decision: 'blocked' }, contextPct: 20 })
|
|
632
|
-
assert(!!w3, 'r-1 was woken for the third judgement')
|
|
633
|
-
const said = delivered.map(d => d.prompt).join('\n')
|
|
634
|
-
assert(/必须写明 note/.test(said), 'a blocked judgement without a note is refused with an explicit notice to the member')
|
|
635
|
-
assert((await callTool('vibe_v5_status', {}, RF)).formal.objects.every(o => o.target !== 'p-reply3'),
|
|
636
|
-
'and no blocker record is created for the refused judgement')
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
// ---------- 10. reporting ----------
|
|
640
|
-
section('10 the office can audit formal strength')
|
|
641
|
-
const rep = await callTool('vibe_v5_report', {}, RE)
|
|
642
|
-
assert(/## Lean 形式化/.test(rep.report), 'the report has a Lean formal-verification section')
|
|
643
|
-
assert(/已通过:.*p-gate/.test(rep.report), 'the report lists Lean-passed objects')
|
|
644
|
-
assert(/已记录阻塞:.*p-blocked-ok/.test(rep.report), 'the report lists blocked objects')
|
|
645
|
-
const stOff = await callTool('vibe_v5_report', {}, RA)
|
|
646
|
-
assert(/未启用(`formalVerify` = off/.test(stOff.report), 'in off mode the report says the feature is not enabled')
|
|
647
|
-
|
|
648
|
-
// ---------- 11. a faithfulness defect withdraws the proof, it is NOT a refutation ----------
|
|
649
|
-
section('11 ★ a faithfulness defect withdraws the proof instead of recording 命题为假 (contract §4.1)')
|
|
650
|
-
const RG = makeRoot()
|
|
651
|
-
await foundInstitute(RG, '忠实性缺陷语义测试')
|
|
652
|
-
await callTool('vibe_v5_set', { formalVerify: 'encourage' }, RG)
|
|
653
|
-
const instG = instRootOf(RG)
|
|
654
|
-
// make p-def Lean-PASSED first: the archived proof is what a defect must withdraw
|
|
655
|
-
const arcG = await callTool('vibe_v5_lean_archive', {
|
|
656
|
-
kind: 'proof', target: 'p-def', content: 'theorem p_def : (1:Nat) + 1 = 2 := by decide\n',
|
|
657
|
-
}, childAgent(childOf(RG, 'r-1')))
|
|
658
|
-
assert(arcG.ok === true && arcG.passed === true, 'p-def is Lean-passed before the review')
|
|
659
|
-
assert(existsSync(join(instG, 'Verified', 'Lean', 'p-def.lean')), 'its archived proof exists on disk')
|
|
660
|
-
// a voter reports a FIDELITY DEFECT through the reply channel
|
|
661
|
-
const wDef = await wakeAndReply(RG, 'r-1', {
|
|
662
|
-
progress: '逐条核对后发现 Lean 陈述与命题不一致。',
|
|
663
|
-
formal: { target: 'p-def', decision: 'defect', note: 'Lean 里把"连续"写成了逐点连续,条件被加强了' },
|
|
664
|
-
contextPct: 20,
|
|
665
|
-
})
|
|
666
|
-
assert(!!wDef, 'the reviewer was woken and answered with a defect report')
|
|
667
|
-
const stG = await callTool('vibe_v5_status', {}, RG)
|
|
668
|
-
const recG = (stG.formal.objects || []).find(o => o.target === 'p-def') || {}
|
|
669
|
-
assert(recG.status === 'attempted', '★ the object is demoted to attempted (a defect is not a proof any more), got ' + recG.status)
|
|
670
|
-
assert(!recG.proof, '★ the archived proof is cleared from the record')
|
|
671
|
-
assert(/加强了/.test(String(recG.note || '')), 'the concrete deviation is recorded on the object')
|
|
672
|
-
const proofPathG = join(instG, 'Verified', 'Lean', 'p-def.lean')
|
|
673
|
-
const proofNowG = existsSync(proofPathG) ? readFileSync(proofPathG, 'utf8') : ''
|
|
674
|
-
assert(!existsSync(proofPathG) || /已撤回/.test(proofNowG),
|
|
675
|
-
'★ the archived proof is withdrawn from Verified/Lean/ (deleted, or replaced by a withdrawal notice when the host cannot delete)')
|
|
676
|
-
assert(!/theorem p_def/.test(proofNowG), '★ the original proof text is no longer readable as the object\'s proof')
|
|
677
|
-
assert(existsSync(join(instG, 'Formal', 'p-def.lean')), 'the working file is kept (the code is not lost)')
|
|
678
|
-
const todoG = readIf(join(instG, 'Formal', 'TODO.md'))
|
|
679
|
-
assert(/p-def/.test(todoG) && /忠实性缺陷/.test(todoG), 'the object enters Formal/TODO.md as a formalisation defect')
|
|
680
|
-
const idxG = readIf(join(instG, 'Formal', 'Index.md'))
|
|
681
|
-
assert(/加强了/.test(idxG), 'the human-readable index carries the concrete deviation')
|
|
682
|
-
assert(/attempted/.test(idxG) && !/Verified\/Lean\/p-def\.lean/.test(idxG.split('p-def')[1] || ''),
|
|
683
|
-
'the index shows attempted and no longer points at a proof')
|
|
684
|
-
// a defect without a note is refused
|
|
685
|
-
delivered.length = 0
|
|
686
|
-
const wDef2 = await wakeAndReply(RG, 'r-1', { formal: { target: 'p-def2', decision: 'defect' }, contextPct: 20 })
|
|
687
|
-
assert(!!wDef2, 'the reviewer was woken for the note-less defect')
|
|
688
|
-
assert(/必须写明 note/.test(delivered.map(d => d.prompt).join('\n')), 'a defect without a note is refused with an explicit notice')
|
|
689
|
-
assert((await callTool('vibe_v5_status', {}, RG)).formal.objects.every(o => o.target !== 'p-def2'),
|
|
690
|
-
'and no record is created for the refused defect')
|
|
691
|
-
|
|
692
|
-
// ---------- 12. after a defect, require mode refuses to conclude -------------
|
|
693
|
-
section('12 ★ a defect makes the require gate block the conclusion (re-formalise, do not conclude 假)')
|
|
694
|
-
await callTool('vibe_v5_set', { formalVerify: 'require' }, RG)
|
|
695
|
-
await callTool('vibe_v5_record_proposition', { id: 'p-def', statement: '连续函数在闭区间上一致连续(被写窄的形式化)', value: 0.6, motive: 'm', p: 0.9 }, childAgent(childOf(RG, 'r-1')))
|
|
696
|
-
await callTool('vibe_v5_propose_verify', { target: 'p-def', kind: 'proposition', reason: '缺陷后重验' }, childAgent(childOf(RG, 'r-1')))
|
|
697
|
-
// every voter says TRUE — without the fix this would be recorded as a concluded object
|
|
698
|
-
const stDef = await voteToConclusion(RG, 'p-def', new Map([['acad', 1], ['r-1', 1], ['r-2', 1]]))
|
|
699
|
-
assert(stDef.verified.indexOf('p-def') === -1, '★ the object is NOT verified: a withdrawn proof cannot support a conclusion')
|
|
700
|
-
assert(stDef.undecided.indexOf('p-def') !== -1, '★ it is recorded as 未定论 and stays in the library')
|
|
701
|
-
assert(!existsSync(join(instG, 'Verified', '命题', 'p-def.md')), 'no Verified card is written for it')
|
|
702
|
-
assert(/p-def/.test(readIf(join(instG, 'Formal', 'TODO.md'))), 'it is on the formalisation TODO list')
|
|
703
|
-
|
|
704
|
-
console.log('')
|
|
705
|
-
console.log('passed=' + passed + ' failed=' + failed)
|
|
706
|
-
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
707
|
-
console.log('ALL GREEN')
|
|
708
|
-
process.exit(0)
|
|
1
|
+
// ============================================================
|
|
2
|
+
// V5 LEAN FORMAL VERIFICATION SUITE (docs/formal-verification.md)
|
|
3
|
+
//
|
|
4
|
+
// Asserts the whole contract of the `formalVerify` knob:
|
|
5
|
+
// · 'off' is a TRUE no-op (no Lean text anywhere, no gate)
|
|
6
|
+
// · 'encourage' injects the Lean section AND — the actual point of the feature — turns the
|
|
7
|
+
// voting prompt into a FIDELITY review once a Lean run has passed
|
|
8
|
+
// · 'require' withholds a true/false verdict as 未定论 until the object is Lean-passed
|
|
9
|
+
// or carries an explicit, reasoned blocker record; then allows it, and the
|
|
10
|
+
// Verified card records how strong the result really is
|
|
11
|
+
// · the three tools (run / archive / lib) write the right things to the right paths
|
|
12
|
+
//
|
|
13
|
+
// The Lean toolchain is mocked through the subprocess SERVICE, so the tests exercise the
|
|
14
|
+
// real code path (resolveExecutable → spawn → collected stdout → exit code) without
|
|
15
|
+
// requiring Lean to be installed.
|
|
16
|
+
//
|
|
17
|
+
// Run: node tests/formal-verify-v5.test.mjs
|
|
18
|
+
// ============================================================
|
|
19
|
+
import { mkdtempSync, existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs'
|
|
20
|
+
import { tmpdir } from 'node:os'
|
|
21
|
+
import { join, dirname, isAbsolute } from 'node:path'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
|
|
24
|
+
const PLUGIN = process.env.V5_PLUGIN
|
|
25
|
+
? new URL('file:///' + String(process.env.V5_PLUGIN).replace(/\\/g, '/'))
|
|
26
|
+
: new URL('../vibe-math-v5/vibe-math-v5.js', import.meta.url)
|
|
27
|
+
const WS = mkdtempSync(join(tmpdir(), 'vibe-v5-lean-'))
|
|
28
|
+
|
|
29
|
+
let passed = 0, failed = 0
|
|
30
|
+
const failures = []
|
|
31
|
+
const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
|
|
32
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
33
|
+
const section = (t) => console.log('\n[' + t + ']')
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------
|
|
36
|
+
// mock host
|
|
37
|
+
// ---------------------------------------------------------------
|
|
38
|
+
function makeProjectionRegistry() {
|
|
39
|
+
const units = new Map(); const cells = new Map()
|
|
40
|
+
const cellMap = (s) => { const id = String(s.id); let m = cells.get(id); if (!m) { m = new Map(); cells.set(id, m) } return m }
|
|
41
|
+
return {
|
|
42
|
+
register(def) { units.set(def.key, def); return () => { units.delete(def.key) } },
|
|
43
|
+
stateOf(session, key) {
|
|
44
|
+
const def = units.get(key); if (!def) return undefined
|
|
45
|
+
const m = cellMap(session)
|
|
46
|
+
if (!m.has(key)) m.set(key, def.init(session.header, session.inheritedEventCount || 0))
|
|
47
|
+
return m.get(key)
|
|
48
|
+
},
|
|
49
|
+
_drive(session, event) {
|
|
50
|
+
const m = cellMap(session)
|
|
51
|
+
for (const [k, def] of units) {
|
|
52
|
+
const cur = m.has(k) ? m.get(k) : def.init(session.header, session.inheritedEventCount || 0)
|
|
53
|
+
let next; try { next = def.apply(cur, event) } catch (e) { next = cur }
|
|
54
|
+
m.set(k, next)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const projections = makeProjectionRegistry()
|
|
61
|
+
const listeners = {}
|
|
62
|
+
const toolRegs = []
|
|
63
|
+
const liveAgents = new Map()
|
|
64
|
+
const spawns = []
|
|
65
|
+
const wakes = []
|
|
66
|
+
const delivered = []
|
|
67
|
+
const leanRuns = []
|
|
68
|
+
|
|
69
|
+
// A fake Lean: a file PASSES unless it still contains `sorry` or the marker `-- FAIL`.
|
|
70
|
+
// This mirrors the one property that matters for the feature — an exit code that says
|
|
71
|
+
// "the kernel accepted this".
|
|
72
|
+
let toolchainAvailable = true
|
|
73
|
+
// A host with NO `subprocess` service at all: `ctx.get('subprocess')` returns undefined, which
|
|
74
|
+
// must become a readable NO_SUBPROCESS result (and must NOT stop the code from being archived).
|
|
75
|
+
let noSubprocess = false
|
|
76
|
+
// A run that HANGS: `hangLean` makes the fake toolchain return a `done` that only settles
|
|
77
|
+
// after `hangMs` (far beyond any cap the tests use), so the run can only be reported as a
|
|
78
|
+
// timeout by actually racing `done` against a timer. `terminations` records every
|
|
79
|
+
// `handle.terminate()` call, which is the observable proof that the guard fired.
|
|
80
|
+
let hangLean = false
|
|
81
|
+
let hangMs = 60000
|
|
82
|
+
const terminations = []
|
|
83
|
+
let subprocess = {
|
|
84
|
+
async resolveExecutable(cmd) {
|
|
85
|
+
if (!toolchainAvailable) throw new Error('spawn lean ENOENT')
|
|
86
|
+
if (String(cmd) !== 'lean') throw new Error('unknown executable ' + cmd)
|
|
87
|
+
return 'lean'
|
|
88
|
+
},
|
|
89
|
+
spawn(spec) {
|
|
90
|
+
const file = spec.argv[spec.argv.length - 1]
|
|
91
|
+
const text = existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
92
|
+
const bad = /sorry|-- FAIL/.test(text)
|
|
93
|
+
leanRuns.push({ argv: spec.argv.slice(0, -1), file, cwd: spec.cwd })
|
|
94
|
+
const stdout = bad ? '' : 'ok\n'
|
|
95
|
+
const stderr = bad ? 'error: declaration uses sorry\n' : ''
|
|
96
|
+
const done = hangLean
|
|
97
|
+
? new Promise(r => setTimeout(() => r({ exitCode: 0, signal: null }), hangMs))
|
|
98
|
+
: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null })
|
|
99
|
+
return {
|
|
100
|
+
done,
|
|
101
|
+
collected: {
|
|
102
|
+
stdout: { readFrom: () => ({ text: stdout, nextOffset: stdout.length, lossy: false }) },
|
|
103
|
+
stderr: { readFrom: () => ({ text: stderr, nextOffset: stderr.length, lossy: false }) },
|
|
104
|
+
},
|
|
105
|
+
terminate() { terminations.push(file) },
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function makeMockSession(id, parentSession) {
|
|
111
|
+
const events = []
|
|
112
|
+
const s = {
|
|
113
|
+
id,
|
|
114
|
+
header: { version: 1, id, createdAt: Date.now(), cwd: WS, parentSession, isSeeded: false },
|
|
115
|
+
inheritedEventCount: 0,
|
|
116
|
+
get seq() { return events.length },
|
|
117
|
+
append(type, data) { const ev = { type, data, seq: events.length, time: Date.now() }; events.push(ev); projections._drive(s, ev); return ev },
|
|
118
|
+
deriveMessages() { return [] },
|
|
119
|
+
snapshotEvents(from) { return events.slice(from || 0) },
|
|
120
|
+
ownEvents() { return events.slice() },
|
|
121
|
+
_events: events,
|
|
122
|
+
}
|
|
123
|
+
return s
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const roots = new Map()
|
|
127
|
+
let rootSeq = 0
|
|
128
|
+
function makeRoot() {
|
|
129
|
+
const id = 'sess-' + String.fromCharCode(65 + rootSeq++)
|
|
130
|
+
const session = makeMockSession(id, undefined)
|
|
131
|
+
const root = { id, options: { provider: 'mock', model: 'm' }, session, ctx: undefined }
|
|
132
|
+
roots.set(id, root)
|
|
133
|
+
return root
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ctx = {
|
|
137
|
+
get(name) {
|
|
138
|
+
if (name === 'sessionProjections') return projections
|
|
139
|
+
if (name === 'sandboxPolicy') return undefined
|
|
140
|
+
if (name === 'compaction') return undefined
|
|
141
|
+
if (name === 'subprocess') return noSubprocess ? undefined : subprocess
|
|
142
|
+
return undefined
|
|
143
|
+
},
|
|
144
|
+
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
145
|
+
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
146
|
+
logger: { info() {}, warn() {}, error() {} },
|
|
147
|
+
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
148
|
+
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
149
|
+
commands: { register() { return () => {} } },
|
|
150
|
+
sessions: { async flush() { return true } },
|
|
151
|
+
subagents: {
|
|
152
|
+
list() { return ['spawn'] },
|
|
153
|
+
async startContinuable({ label, request }) {
|
|
154
|
+
const rootId = (request && request.parent && request.parent.id) || 'sess-A'
|
|
155
|
+
const id = 'c' + (spawns.length + 1)
|
|
156
|
+
liveAgents.set(id, { id, session: makeMockSession(id, rootId), options: request && request.agentOptions })
|
|
157
|
+
spawns.push({ label, childId: id, rootId, persona: request && request.persona, prompt: request && request.prompt && request.prompt[0] && request.prompt[0].text })
|
|
158
|
+
return { childId: id, messageId: 'm' + spawns.length }
|
|
159
|
+
},
|
|
160
|
+
async sendMessage(parent, childId, blocks) {
|
|
161
|
+
wakes.push({ childId, rootId: (parent && parent.id) || 'sess-A', prompt: (blocks && blocks[0] && blocks[0].text) || '' })
|
|
162
|
+
return 'w' + (wakes.length + delivered.length)
|
|
163
|
+
},
|
|
164
|
+
interrupt() {},
|
|
165
|
+
async drainContinuableChildren(parent, ids) { for (const i of ids) liveAgents.delete(i) },
|
|
166
|
+
},
|
|
167
|
+
agents: {
|
|
168
|
+
roots() { return [...roots.values()] },
|
|
169
|
+
get(id) { return roots.get(id) || liveAgents.get(id) },
|
|
170
|
+
list() { return [...roots.values(), ...liveAgents.values()] },
|
|
171
|
+
},
|
|
172
|
+
fs: {
|
|
173
|
+
async resolve(rel, opts) {
|
|
174
|
+
const b = (opts && opts.cwd) || WS
|
|
175
|
+
const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/'))
|
|
176
|
+
return { targetKey: p, displayPath: p }
|
|
177
|
+
},
|
|
178
|
+
async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
|
|
179
|
+
async readText(t) { return readFileSync(t.targetKey, 'utf8') },
|
|
180
|
+
async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
|
|
181
|
+
async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const mod = await import(PLUGIN.href + '?t=' + Date.now())
|
|
186
|
+
;(mod.default || mod).apply(ctx)
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------
|
|
189
|
+
// driving helpers
|
|
190
|
+
// ---------------------------------------------------------------
|
|
191
|
+
async function callTool(name, args, agent) {
|
|
192
|
+
const spec = toolRegs.find(x => x.name === name)
|
|
193
|
+
if (!spec) throw new Error('no tool ' + name)
|
|
194
|
+
return JSON.parse(await spec.execute(args || {}, { agent }))
|
|
195
|
+
}
|
|
196
|
+
const childAgent = (childId) => liveAgents.get(childId)
|
|
197
|
+
function fireEnd(childId, reply) {
|
|
198
|
+
const blocks = reply === undefined ? [] : [{ type: 'text', text: '```json\n' + JSON.stringify(reply) + '\n```' }]
|
|
199
|
+
for (const h of (listeners['subagent/end'] || [])) {
|
|
200
|
+
h({ id: childId, runId: 'r', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: blocks })
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const settle = async () => { await sleep(30) }
|
|
204
|
+
const memberOfChild = (childId) => { const s = spawns.find(x => x.childId === childId); const m = s ? /vibe5 (\S+) /.exec(s.label) : null; return m ? m[1] : '' }
|
|
205
|
+
const spawnOf = (root, id) => spawns.find(s => s.rootId === root.id && s.label.indexOf('vibe5 ' + id + ' ') !== -1)
|
|
206
|
+
const childOf = (root, id) => { const s = spawnOf(root, id); return s ? s.childId : '' }
|
|
207
|
+
const spawnsFor = (root) => spawns.filter(s => s.rootId === root.id)
|
|
208
|
+
|
|
209
|
+
let votePlan = new Map()
|
|
210
|
+
async function drainWakes(budget, root) {
|
|
211
|
+
let n = 0
|
|
212
|
+
while (n < budget) {
|
|
213
|
+
const idx = wakes.findIndex(w => !root || w.rootId === root.id)
|
|
214
|
+
if (idx === -1) break
|
|
215
|
+
const w = wakes.splice(idx, 1)[0]
|
|
216
|
+
const owner = memberOfChild(w.childId)
|
|
217
|
+
delivered.push({ prompt: w.prompt, owner, rootId: w.rootId })
|
|
218
|
+
let reply
|
|
219
|
+
if (/【求真表决/.test(w.prompt)) {
|
|
220
|
+
const target = (/"target"\s*:\s*"([^"]+)"/.exec(w.prompt) || [])[1] || ''
|
|
221
|
+
const v = votePlan.has(owner) ? votePlan.get(owner) : 0.5
|
|
222
|
+
reply = { verdict: { target, verdict: v, reason: owner + ' 的判断' }, contextPct: 20 }
|
|
223
|
+
} else if (/【研究所会议/.test(w.prompt)) reply = { input: owner + ':意见。', solved: false, contextPct: 20 }
|
|
224
|
+
else reply = { progress: owner + ':继续推进。', solved: false, contextPct: 20 }
|
|
225
|
+
fireEnd(w.childId, reply)
|
|
226
|
+
n++
|
|
227
|
+
await settle()
|
|
228
|
+
}
|
|
229
|
+
return n
|
|
230
|
+
}
|
|
231
|
+
async function settleInstitute(root, rounds = 12) {
|
|
232
|
+
for (let i = 0; i < rounds; i++) {
|
|
233
|
+
await drainWakes(40, root)
|
|
234
|
+
await sleep(20)
|
|
235
|
+
const st = await callTool('vibe_v5_status', {}, root)
|
|
236
|
+
if (!st.members.some(m => m.busy) && !st.meeting && !st.verify && wakes.filter(w => w.rootId === root.id).length === 0) return st
|
|
237
|
+
}
|
|
238
|
+
return await callTool('vibe_v5_status', {}, root)
|
|
239
|
+
}
|
|
240
|
+
async function foundInstitute(root, problem, params) {
|
|
241
|
+
await callTool('vibe_v5_start', Object.assign({ problem, researcherCount: 2 }, params || {}), root)
|
|
242
|
+
for (const sp of spawnsFor(root)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初见解。', solved: false, contextPct: 10 }); await settle() }
|
|
243
|
+
await callTool('vibe_v5_set', { maxParallel: 8 }, root)
|
|
244
|
+
return await settleInstitute(root)
|
|
245
|
+
}
|
|
246
|
+
// Vote a verdict through to completion. `waitRounds` bounds the debate loop.
|
|
247
|
+
async function voteToConclusion(root, target, plan, rounds = 5) {
|
|
248
|
+
votePlan = plan
|
|
249
|
+
for (let i = 0; i < rounds; i++) {
|
|
250
|
+
delivered.length = 0
|
|
251
|
+
await settle(); await drainWakes(10, root)
|
|
252
|
+
const st = await callTool('vibe_v5_status', {}, root)
|
|
253
|
+
if (!st.verify || st.verify.target !== target) return st
|
|
254
|
+
if (st.undecided.indexOf(target) !== -1 || st.verified.indexOf(target) !== -1) return st
|
|
255
|
+
}
|
|
256
|
+
return await callTool('vibe_v5_status', {}, root)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Deterministically wake ONE member and answer with a chosen reply.
|
|
260
|
+
// Firing an end for a member that has no in-flight turn is a no-op (the in-flight token is
|
|
261
|
+
// what makes onMemberEnd honour the event), so the wake must be created first.
|
|
262
|
+
async function wakeAndReply(root, memberId, reply, fromMember) {
|
|
263
|
+
delivered.length = 0
|
|
264
|
+
await callTool('vibe_v5_say', { to: memberId, text: '请处理这件事。' }, childAgent(childOf(root, fromMember || 'acad')))
|
|
265
|
+
await settle()
|
|
266
|
+
const idx = wakes.findIndex(w => w.rootId === root.id && memberOfChild(w.childId) === memberId)
|
|
267
|
+
if (idx === -1) return null
|
|
268
|
+
const w = wakes.splice(idx, 1)[0]
|
|
269
|
+
fireEnd(w.childId, reply)
|
|
270
|
+
await settle()
|
|
271
|
+
await drainWakes(8, root)
|
|
272
|
+
return w
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const instRootOf = (root) => join(WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute')
|
|
276
|
+
const vibeRoot = join(WS, 'VibeMath')
|
|
277
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
278
|
+
// Every Lean tool mention in AGENT-FACING text must be the registered name (vibe_v5_lean_*).
|
|
279
|
+
// An abbreviated `lean_archive` is not a tool: an agent that copies it calls nothing.
|
|
280
|
+
const noBareLeanTool = (t) => !/(^|[^a-z_])lean_(run|archive|lib)/.test(String(t || ''))
|
|
281
|
+
|
|
282
|
+
// ===============================================================
|
|
283
|
+
console.log('-- V5 Lean formal verification --')
|
|
284
|
+
|
|
285
|
+
// ---------- 1. 'off' is a true no-op ----------
|
|
286
|
+
section("1 'off' (default) is a true no-op")
|
|
287
|
+
const RA = makeRoot()
|
|
288
|
+
const st0 = await foundInstitute(RA, '形式化开关默认关闭测试')
|
|
289
|
+
assert(st0.params.formalVerify === 'off', "the default is 'off' (got " + st0.params.formalVerify + ')')
|
|
290
|
+
{
|
|
291
|
+
const allPrompts = spawnsFor(RA).map(s => s.prompt || '').join('\n') + '\n' + delivered.filter(d => d.rootId === RA.id).map(d => d.prompt).join('\n')
|
|
292
|
+
assert(!/Lean/.test(allPrompts), 'no founding/round prompt mentions Lean in off mode')
|
|
293
|
+
assert(!/形式化/.test(allPrompts), 'no prompt mentions 形式化 in off mode')
|
|
294
|
+
assert(!/\[形式化\]/.test(allPrompts), 'the [形式化] state line is absent in off mode')
|
|
295
|
+
}
|
|
296
|
+
// an off-mode verification must behave exactly as before (no gate at all)
|
|
297
|
+
await callTool('vibe_v5_record_proposition', { id: 'p-off', statement: '关模式下的普通命题', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RA, 'r-1')))
|
|
298
|
+
await callTool('vibe_v5_propose_verify', { target: 'p-off', kind: 'proposition', reason: '直接表决' }, childAgent(childOf(RA, 'r-1')))
|
|
299
|
+
const offDone = await voteToConclusion(RA, 'p-off', new Map([['acad', 1], ['r-1', 1], ['r-2', 1]]))
|
|
300
|
+
assert(offDone.verified.indexOf('p-off') !== -1, "'off' mode still finalizes on a unanimous boolean vote with NO Lean artifact")
|
|
301
|
+
assert(existsSync(join(instRootOf(RA), 'Verified', '命题', 'p-off.md')), "the Verified card was written in 'off' mode")
|
|
302
|
+
assert(!/形式化/.test(readIf(join(instRootOf(RA), 'Verified', '命题', 'p-off.md'))), 'the card carries no formal line in off mode')
|
|
303
|
+
// the tools still EXIST in off mode (static registration), they are just never advertised
|
|
304
|
+
assert(!!toolRegs.find(t => t.name === 'vibe_v5_lean_run') && !!toolRegs.find(t => t.name === 'vibe_v5_lean_archive') && !!toolRegs.find(t => t.name === 'vibe_v5_lean_lib'),
|
|
305
|
+
'the three Lean tools are registered in every mode (registration is static)')
|
|
306
|
+
|
|
307
|
+
// ★ The mode switch must be REACHABLE THROUGH THE TOOL SCHEMA (2.3.2 defect D1) ──────────────
|
|
308
|
+
// Every tool schema here is closed (`additionalProperties:false`), so a key the schema does not
|
|
309
|
+
// advertise is REJECTED by any schema-validating provider. v3 shipped 2.3.0/2.3.1 with all four Lean
|
|
310
|
+
// parameters missing from the set-params schema while every assertion in this file stayed green —
|
|
311
|
+
// because the suite calls the handler DIRECTLY and never inspects the registered schema. The feature
|
|
312
|
+
// could not be switched on at all through the tool interface.
|
|
313
|
+
{
|
|
314
|
+
const setSpec = toolRegs.find((t) => t.name === 'vibe_v5_set')
|
|
315
|
+
assert(!!setSpec, "vibe_v5_set is registered")
|
|
316
|
+
assert(setSpec.parameters && setSpec.parameters.type === 'object' && setSpec.parameters.additionalProperties === false,
|
|
317
|
+
'★ vibe_v5_set publishes a CLOSED object schema (an unlisted key is rejected, so the schema IS the contract)')
|
|
318
|
+
for (const k of ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']) {
|
|
319
|
+
assert(Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k),
|
|
320
|
+
'★ the registered schema advertises ' + k + ' (every other surface documents it; a schema that omits it makes the switch unreachable)')
|
|
321
|
+
}
|
|
322
|
+
assert(JSON.stringify(setSpec.parameters.properties.formalVerify.enum) === JSON.stringify(['off', 'encourage', 'require']),
|
|
323
|
+
'the schema narrows formalVerify to the three real modes (a typo must not be a fourth)')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// A stray `formal` reply in OFF mode must be INERT: the field is not offered in the reply contract
|
|
327
|
+
// there, and honouring it would create Formal/ state in a mode documented as a "TRUE no-op" (the
|
|
328
|
+
// TOOLS stay usable on purpose — a tool call is deliberate, a stray reply field is not).
|
|
329
|
+
{
|
|
330
|
+
const wOff = await wakeAndReply(RA, 'r-1', {
|
|
331
|
+
progress: '关模式下的普通回轮。',
|
|
332
|
+
formal: { target: 'p-off-stray', decision: 'blocked', note: '不应被记录' },
|
|
333
|
+
contextPct: 20,
|
|
334
|
+
})
|
|
335
|
+
assert(!!wOff, 'off mode: a wake carrying a stray formal reply was fed to the framework')
|
|
336
|
+
const stOff2 = await callTool('vibe_v5_status', {}, RA)
|
|
337
|
+
assert((stOff2.formal.objects || []).length === 0, '★ a stray `formal` reply in off mode records NO formal object')
|
|
338
|
+
assert((stOff2.formal.todo || []).length === 0, '★ and adds nothing to the formalization TODO')
|
|
339
|
+
assert(!/p-off-stray/.test(readIf(join(instRootOf(RA), 'Formal', 'TODO.md'))), '★ and writes no TODO entry for it')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ---------- 2. parameter validation ----------
|
|
343
|
+
section('2 parameter validation and runtime switching')
|
|
344
|
+
const RB = makeRoot()
|
|
345
|
+
await foundInstitute(RB, '形式化参数校验测试')
|
|
346
|
+
const bad = await callTool('vibe_v5_set', { formalVerify: 'banana' }, RB)
|
|
347
|
+
assert(bad.params.formalVerify === 'off', "an unknown mode degrades to 'off', never to a stronger mode (got " + bad.params.formalVerify + ')')
|
|
348
|
+
const enc = await callTool('vibe_v5_set', { formalVerify: 'encourage' }, RB)
|
|
349
|
+
assert(enc.params.formalVerify === 'encourage', "'encourage' is accepted")
|
|
350
|
+
const req = await callTool('vibe_v5_set', { formalVerify: 'require', leanTimeoutMs: -5, leanCommand: ' ' }, RB)
|
|
351
|
+
assert(req.params.formalVerify === 'require', "'require' is accepted")
|
|
352
|
+
assert(req.params.leanTimeoutMs === 120000, 'a non-positive leanTimeoutMs falls back to the default (' + req.params.leanTimeoutMs + ')')
|
|
353
|
+
assert(req.params.leanCommand === 'lean', 'a blank leanCommand falls back to "lean"')
|
|
354
|
+
await callTool('vibe_v5_set', { leanCommand: 'lake', leanArgs: ['env', 'lean'] }, RB)
|
|
355
|
+
const stL = await callTool('vibe_v5_status', {}, RB)
|
|
356
|
+
assert(stL.params.leanCommand === 'lake' && stL.params.leanArgs.join(' ') === 'env lean', 'leanCommand/leanArgs are settable (lake env lean)')
|
|
357
|
+
|
|
358
|
+
// ---------- 3. 'encourage' injection ----------
|
|
359
|
+
section("3 'encourage' injects the Lean section into the right prompts")
|
|
360
|
+
const RC = makeRoot()
|
|
361
|
+
await foundInstitute(RC, '鼓励模式注入测试')
|
|
362
|
+
await callTool('vibe_v5_set', { formalVerify: 'encourage' }, RC)
|
|
363
|
+
{
|
|
364
|
+
// a plain work round must carry the "formalize reusable things as you go" line
|
|
365
|
+
delivered.length = 0
|
|
366
|
+
await callTool('vibe_v5_say', { to: 'r-1', text: '请继续。' }, childAgent(childOf(RC, 'acad')))
|
|
367
|
+
await settle(); await drainWakes(6, RC)
|
|
368
|
+
const work = delivered.filter(d => d.rootId === RC.id).map(d => d.prompt).join('\n')
|
|
369
|
+
assert(/\[形式化\] 鼓励 Lean/.test(work), "the state block gains a [形式化] 鼓励 Lean line")
|
|
370
|
+
assert(/【顺手形式化(鼓励)】/.test(work), 'the work round tells members to formalize reusable objects as they go')
|
|
371
|
+
assert(/vibe_v5_lean_archive kind='def'/.test(work), 'the work round points at the archive tool for reusable definitions')
|
|
372
|
+
assert(noBareLeanTool(work), 'no abbreviated tool name appears in the injected work-round prompt')
|
|
373
|
+
assert(/归档前先跑通/.test(work), 'the work-round prompt requires a green run before archiving into the reuse library')
|
|
374
|
+
}
|
|
375
|
+
await callTool('vibe_v5_record_proposition', { id: 'p-enc', statement: '鼓励模式下的忠实性审查', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RC, 'r-1')))
|
|
376
|
+
await callTool('vibe_v5_propose_verify', { target: 'p-enc', kind: 'proposition', reason: '先看看提示词' }, childAgent(childOf(RC, 'r-1')))
|
|
377
|
+
await settle(); delivered.length = 0; await drainWakes(3, RC)
|
|
378
|
+
{
|
|
379
|
+
const vp = delivered.filter(d => d.rootId === RC.id).map(d => d.prompt).join('\n')
|
|
380
|
+
assert(/【Lean 形式化验证(鼓励模式)】/.test(vp), 'the voting prompt explains the Lean mode')
|
|
381
|
+
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(vp), 'the voting prompt states that a passing Lean run shrinks the question to fidelity')
|
|
382
|
+
assert(/归档可复用定义\/引理前先跑通/.test(vp), 'the voting prompt requires a GREEN RUN before archiving a reusable definition')
|
|
383
|
+
assert(/LEAN_NOT_FOUND/.test(vp) && /宿主无 Lean 工具链/.test(vp), 'the voting prompt says what to do when the host has no Lean toolchain')
|
|
384
|
+
assert(/NO_SUBPROCESS/.test(vp), '§6 rule 4: the no-toolchain route names NO_SUBPROCESS too (an agent that only knows LEAN_NOT_FOUND treats a service-less host as an unknown failure and retries)')
|
|
385
|
+
assert(noBareLeanTool(vp), 'no abbreviated tool name appears in the injected voting prompt')
|
|
386
|
+
assert(/实现难度/.test(vp), 'the voting prompt asks for the implementation-difficulty judgement')
|
|
387
|
+
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(vp), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
388
|
+
assert(/"formal":/.test(vp), 'the reply contract documents the formal field')
|
|
389
|
+
assert(/"decision":"used\|blocked\|defect"/.test(vp), '★ the reply contract offers the defect decision (a faithfulness defect is recordable)')
|
|
390
|
+
}
|
|
391
|
+
await drainWakes(10, RC)
|
|
392
|
+
|
|
393
|
+
// ---------- 4. the run tool ----------
|
|
394
|
+
section('4 lean_run executes through the subprocess service and reports honestly')
|
|
395
|
+
const RD = makeRoot()
|
|
396
|
+
await foundInstitute(RD, 'Lean 运行测试')
|
|
397
|
+
await callTool('vibe_v5_set', { formalVerify: 'encourage' }, RD)
|
|
398
|
+
const instD = instRootOf(RD)
|
|
399
|
+
mkdirSync(join(instD, 'Formal'), { recursive: true })
|
|
400
|
+
writeFileSync(join(instD, 'Formal', 'good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
401
|
+
writeFileSync(join(instD, 'Formal', 'bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
|
|
402
|
+
const runGood = await callTool('vibe_v5_lean_run', { file: 'Formal/good.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
403
|
+
assert(runGood.ok === true && runGood.exitCode === 0, 'a file with no sorry runs green (' + JSON.stringify({ ok: runGood.ok, exitCode: runGood.exitCode }) + ')')
|
|
404
|
+
assert(leanRuns.length > 0 && leanRuns[leanRuns.length - 1].cwd.replace(/\\/g, '/') === instD.replace(/\\/g, '/'), 'the toolchain runs with the institute root as cwd')
|
|
405
|
+
const runBad = await callTool('vibe_v5_lean_run', { file: 'Formal/bad.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
406
|
+
assert(runBad.ok === false && runBad.exitCode === 1, 'a file that still uses sorry reports a red run')
|
|
407
|
+
assert(/sorry/.test(runBad.stderr), 'the compiler output is returned verbatim (' + JSON.stringify(runBad.stderr).slice(0, 60) + ')')
|
|
408
|
+
const runMissing = await callTool('vibe_v5_lean_run', { file: 'Formal/nope.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
409
|
+
assert(runMissing.ok === false && runMissing.code === 'V5_NOT_FOUND', 'a missing file is refused with a typed code')
|
|
410
|
+
// The guard's boundary is the VibeMath ROOT, not the institute: the global reuse library
|
|
411
|
+
// deliberately lives at <VibeMath>/Formal/{Lib,Proved}, so climbing out of the institute
|
|
412
|
+
// but staying inside VibeMath must remain legal (it just fails as a missing file).
|
|
413
|
+
const runOutsideInst = await callTool('vibe_v5_lean_run', { file: '../../../Formal/Lib/x.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
414
|
+
assert(runOutsideInst.code === 'V5_NOT_FOUND', 'climbing out of the institute but staying inside VibeMath is allowed (the global library lives there)')
|
|
415
|
+
const runEscape = await callTool('vibe_v5_lean_run', { file: '../../../../../etc/evil.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
416
|
+
assert(runEscape.ok === false && runEscape.code === 'V5_INVALID_ARGUMENT', '★ a traversal that climbs ABOVE the VibeMath root is refused')
|
|
417
|
+
const runEscape2 = await callTool('vibe_v5_lean_run', { file: 'Formal/../../../../../../evil.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
418
|
+
assert(runEscape2.ok === false && runEscape2.code === 'V5_INVALID_ARGUMENT', 'a deeper traversal is refused too')
|
|
419
|
+
const runEscape3 = await callTool('vibe_v5_lean_run', { file: '/etc/evil.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
420
|
+
assert(runEscape3.ok === false && runEscape3.code === 'V5_INVALID_ARGUMENT', 'an unrelated absolute path is refused')
|
|
421
|
+
const runNotLean = await callTool('vibe_v5_lean_run', { file: 'Formal/good.txt' }, childAgent(childOf(RD, 'r-1')))
|
|
422
|
+
assert(runNotLean.ok === false && runNotLean.code === 'V5_INVALID_ARGUMENT', 'only .lean files can be executed')
|
|
423
|
+
toolchainAvailable = false
|
|
424
|
+
const runNoTc = await callTool('vibe_v5_lean_run', { file: 'Formal/good.lean' }, childAgent(childOf(RD, 'r-1')))
|
|
425
|
+
assert(runNoTc.ok === false && runNoTc.code === 'LEAN_NOT_FOUND', 'a missing toolchain returns LEAN_NOT_FOUND instead of crashing')
|
|
426
|
+
assert(/仍可把形式化代码写下来归档/.test(runNoTc.message), 'the failure explains the graceful degradation')
|
|
427
|
+
toolchainAvailable = true
|
|
428
|
+
// docs/formal-verification.md §7: a TIMEOUT must TERMINATE the process. `graceMs` is only a
|
|
429
|
+
// request to the host, so a run whose `done` never settles within the cap must be ended by
|
|
430
|
+
// `handle.terminate()` — otherwise a runaway toolchain lingers while we report LEAN_TIMEOUT.
|
|
431
|
+
{
|
|
432
|
+
const before = terminations.length
|
|
433
|
+
hangLean = true
|
|
434
|
+
hangMs = 60000
|
|
435
|
+
const runHang = await callTool('vibe_v5_lean_run', { file: 'Formal/good.lean', timeout_ms: 1000 }, childAgent(childOf(RD, 'r-1')))
|
|
436
|
+
hangLean = false
|
|
437
|
+
assert(runHang.ok === false && runHang.code === 'LEAN_TIMEOUT', '★ a run that outlives the cap is reported as LEAN_TIMEOUT (got ' + runHang.code + ')')
|
|
438
|
+
assert(runHang.timedOut === true, 'the result is flagged timedOut')
|
|
439
|
+
assert(terminations.slice(before).some(f => /good\.lean$/.test(String(f))), '★ the timeout path really called handle.terminate() (the toolchain is not left running)')
|
|
440
|
+
assert(runHang.signal === 'SIGTERM', 'the synthetic outcome names the signal that ended it')
|
|
441
|
+
}
|
|
442
|
+
// A host that exposes NO `subprocess` service must produce the readable NO_SUBPROCESS result
|
|
443
|
+
// (never a throw into the scheduler), must leave the object record honest (`attempted`, and NOT
|
|
444
|
+
// passed), and must still let the code be written down through the archive route.
|
|
445
|
+
{
|
|
446
|
+
noSubprocess = true
|
|
447
|
+
const runNoSvc = await callTool('vibe_v5_lean_run', { file: 'Formal/good.lean', target: 'p-nosub' }, childAgent(childOf(RD, 'r-1')))
|
|
448
|
+
assert(runNoSvc.ok === false && runNoSvc.code === 'NO_SUBPROCESS', '★ a host with no subprocess service returns NO_SUBPROCESS instead of throwing (got ' + runNoSvc.code + ')')
|
|
449
|
+
assert(typeof runNoSvc.message === 'string' && /no subprocess service/.test(runNoSvc.message), 'the result explains why Lean cannot run here')
|
|
450
|
+
const stNoSvc = await callTool('vibe_v5_status', {}, RD)
|
|
451
|
+
assert(stNoSvc.ok === true, 'the institute still answers status after that (nothing was thrown into the scheduling loop)')
|
|
452
|
+
const recNoSvc = (stNoSvc.formal.objects || []).find(o => o.target === 'p-nosub') || {}
|
|
453
|
+
assert(recNoSvc.status === 'attempted', 'the run is recorded as attempted (no toolchain = no proof), got ' + recNoSvc.status)
|
|
454
|
+
assert(!recNoSvc.proof, 'and it is NOT recorded as passed')
|
|
455
|
+
// The way out (contract §6 rule 4): the code can still be written down. The archive route
|
|
456
|
+
// itself needs no toolchain at all.
|
|
457
|
+
const arcNoSvc = await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-nosub', content: 'theorem p_nosub : 1 + 1 = 2 := by decide\n' }, childAgent(childOf(RD, 'r-1')))
|
|
458
|
+
assert(arcNoSvc.ok === true && arcNoSvc.passed === false && arcNoSvc.run && arcNoSvc.run.code === 'NO_SUBPROCESS',
|
|
459
|
+
'the code is still written down and archived with no toolchain, and the record stays honest (' + JSON.stringify({ ok: arcNoSvc.ok, status: arcNoSvc.status, code: arcNoSvc.run && arcNoSvc.run.code }) + ')')
|
|
460
|
+
assert(existsSync(join(instD, 'Formal', 'p-nosub.lean')), 'the working file exists on disk even though nothing could execute it')
|
|
461
|
+
assert(!existsSync(join(instD, 'Verified', 'Lean', 'p-nosub.lean')), '★ nothing is promoted to Verified/Lean/ without a green run')
|
|
462
|
+
noSubprocess = false
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ---------- 5. archive a proof → the vote becomes a FIDELITY review ----------
|
|
466
|
+
section('5 a passing proof flips the review subject to fidelity')
|
|
467
|
+
const arc = await callTool('vibe_v5_lean_archive', {
|
|
468
|
+
kind: 'proof', target: 'p-proof', content: 'theorem p_proof : 3 * 1 ^ 2 - 2 = (1:Nat) ^ 2 := by decide\n', note: '',
|
|
469
|
+
}, childAgent(childOf(RD, 'r-1')))
|
|
470
|
+
assert(arc.ok === true && arc.passed === true, 'the proof is archived and passes (' + JSON.stringify({ ok: arc.ok, passed: arc.passed }) + ')')
|
|
471
|
+
assert(arc.file === 'Formal/p-proof.lean', 'the working file is Formal/<target>.lean')
|
|
472
|
+
assert(arc.proof === 'Verified/Lean/p-proof.lean', 'the archived proof path is Verified/Lean/<target>.lean')
|
|
473
|
+
assert(existsSync(join(instD, 'Formal', 'p-proof.lean')), 'the working file exists on disk')
|
|
474
|
+
assert(existsSync(join(instD, 'Verified', 'Lean', 'p-proof.lean')), '★ the proof is archived under Verified/Lean/ as the proof of that object')
|
|
475
|
+
const stD = await callTool('vibe_v5_status', {}, RD)
|
|
476
|
+
assert(stD.formal.passed.indexOf('p-proof') !== -1, 'status reports the object as Lean-passed')
|
|
477
|
+
const idxD = readIf(join(instD, 'Formal', 'Index.md'))
|
|
478
|
+
assert(/p-proof/.test(idxD) && /passed/.test(idxD) && /Verified\/Lean\/p-proof\.lean/.test(idxD), 'Formal/Index.md indexes the object, its status and its archived proof')
|
|
479
|
+
// now the voting prompt must ASK FOR FIDELITY, not for a re-derivation
|
|
480
|
+
await callTool('vibe_v5_record_proposition', { id: 'p-proof', statement: '3N²−2=b² 在 N=1 时成立', value: 0.6, motive: 'm', p: 0.9 }, childAgent(childOf(RD, 'r-1')))
|
|
481
|
+
await callTool('vibe_v5_propose_verify', { target: 'p-proof', kind: 'proposition', reason: '已有 Lean 证明' }, childAgent(childOf(RD, 'r-1')))
|
|
482
|
+
await settle(); delivered.length = 0; await drainWakes(3, RD)
|
|
483
|
+
{
|
|
484
|
+
const vp = delivered.filter(d => d.rootId === RD.id).map(d => d.prompt).join('\n')
|
|
485
|
+
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(vp), 'the voting prompt announces the passing proof')
|
|
486
|
+
assert(/你不需要重新检查推导/.test(vp), '★ it tells voters NOT to re-derive')
|
|
487
|
+
assert(/忠实性审查/.test(vp), '★ it tells voters the review subject is now fidelity')
|
|
488
|
+
assert(/定义 \/ 对象 \/ 条件 \/ 假设 \/ 结论是否与命题原文\*\*完全一致\*\*/.test(vp), 'it enumerates exactly what fidelity means')
|
|
489
|
+
assert(/不要投 0/.test(vp), '★ a faithfulness defect must NOT be expressed as 0 (that would record 命题为假)')
|
|
490
|
+
assert(/形式化不合格/.test(vp), 'it names the failure a formalisation defect, not a refutation')
|
|
491
|
+
assert(/decision:'defect'/.test(vp), 'it names the defect reply channel')
|
|
492
|
+
assert(!/偏离 → 0/.test(vp), '★ the old "any deviation → 0" instruction is GONE')
|
|
493
|
+
assert(/本档没有门禁:请务必给一个严格介于 0 与 1 之间的弃权值,以保证本轮无法得出一致结论/.test(vp),
|
|
494
|
+
'★ encourage mode is explicit that there is NO gate and the voter\'s abstention is what prevents a conclusion (wording shared with v2/v3/v4)')
|
|
495
|
+
assert(noBareLeanTool(vp), 'no abbreviated tool name appears in the fidelity prompt')
|
|
496
|
+
}
|
|
497
|
+
await drainWakes(10, RD)
|
|
498
|
+
|
|
499
|
+
// ---------- 6. the reusable cross-project library ----------
|
|
500
|
+
section('6 reusable definitions and lemmas go to the GLOBAL library')
|
|
501
|
+
const libPath = join(vibeRoot, 'Formal', 'Lib')
|
|
502
|
+
const provedPath = join(vibeRoot, 'Formal', 'Proved')
|
|
503
|
+
const defRes = await callTool('vibe_v5_lean_archive', { kind: 'def', name: 'ZMod5', content: 'def ZMod5 := Fin 5\n' }, childAgent(childOf(RD, 'r-1')))
|
|
504
|
+
assert(defRes.ok === true && defRes.file === 'Formal/Lib/ZMod5.lean', 'a reusable definition is archived to the global lib (' + defRes.file + ')')
|
|
505
|
+
assert(existsSync(join(libPath, 'ZMod5.lean')), '★ the definition exists under VibeMath/Formal/Lib/ (cross-project, NOT inside the institute)')
|
|
506
|
+
assert(!existsSync(join(instD, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the institute tree')
|
|
507
|
+
const lemRes = await callTool('vibe_v5_lean_archive', { kind: 'lemma', name: 'sq_odd', content: 'theorem sq_odd (n : Nat) : Odd (n*n) → Odd n := by omega\n' }, childAgent(childOf(RD, 'r-1')))
|
|
508
|
+
assert(lemRes.ok === true && lemRes.file === 'Formal/Proved/sq_odd.lean', 'a lemma is archived to Proved/')
|
|
509
|
+
assert(existsSync(join(provedPath, 'sq_odd.lean')), 'the lemma exists under VibeMath/Formal/Proved/')
|
|
510
|
+
const libIdx = readIf(join(libPath, 'Index.md'))
|
|
511
|
+
assert(/ZMod5/.test(libIdx), 'Lib/Index.md lists the new definition')
|
|
512
|
+
const provedIdx = readIf(join(provedPath, 'Index.md'))
|
|
513
|
+
assert(/sq_odd/.test(provedIdx), 'Proved/Index.md lists the new lemma')
|
|
514
|
+
const libList = await callTool('vibe_v5_lean_lib', {}, childAgent(childOf(RD, 'r-1')))
|
|
515
|
+
assert(libList.ok === true && libList.counts.lib >= 1 && libList.counts.proved >= 1, 'lean_lib reports the reuse library sizes (' + JSON.stringify(libList.counts) + ')')
|
|
516
|
+
assert(libList.objects.some(o => o.target === 'p-proof' && o.status === 'passed'), 'lean_lib lists per-object formal status')
|
|
517
|
+
assert(/复用优先/.test(libList.hint || ''), 'lean_lib tells agents to reuse before redefining')
|
|
518
|
+
const noName = await callTool('vibe_v5_lean_archive', { kind: 'def', content: 'def x := 1\n' }, childAgent(childOf(RD, 'r-1')))
|
|
519
|
+
assert(noName.ok === false && noName.code === 'V5_INVALID_ARGUMENT', 'archiving a definition without a name is refused')
|
|
520
|
+
const badKind = await callTool('vibe_v5_lean_archive', { kind: 'nonsense' }, childAgent(childOf(RD, 'r-1')))
|
|
521
|
+
assert(badKind.ok === false && badKind.code === 'V5_INVALID_ARGUMENT', 'an unknown archive kind is refused')
|
|
522
|
+
|
|
523
|
+
// ---------- 7. blocked needs a reason ----------
|
|
524
|
+
section('7 a "blocker" record must be explicit and reasoned')
|
|
525
|
+
const blkNoNote = await callTool('vibe_v5_lean_archive', { kind: 'blocked', target: 'p-blk' }, childAgent(childOf(RD, 'r-1')))
|
|
526
|
+
assert(blkNoNote.ok === false && blkNoNote.code === 'V5_INVALID_ARGUMENT', 'blocked without a note is refused')
|
|
527
|
+
const blk = await callTool('vibe_v5_lean_archive', { kind: 'blocked', target: 'p-blk', note: '需要外层解析数论框架,本轮工作量不可接受' }, childAgent(childOf(RD, 'r-1')))
|
|
528
|
+
assert(blk.ok === true && blk.status === 'blocked', 'a reasoned blocker is recorded')
|
|
529
|
+
const stBlk = await callTool('vibe_v5_status', {}, RD)
|
|
530
|
+
assert(stBlk.formal.blocked.indexOf('p-blk') !== -1, 'status lists the blocked object')
|
|
531
|
+
assert(/需要外层解析数论框架/.test(readIf(join(instD, 'Formal', 'Index.md'))), 'the blocker reason is written into the index')
|
|
532
|
+
|
|
533
|
+
// ---------- 8. the 'require' gate ----------
|
|
534
|
+
section("8 'require' withholds a verdict until the formal record exists")
|
|
535
|
+
const RE = makeRoot()
|
|
536
|
+
await foundInstitute(RE, 'require 门禁测试')
|
|
537
|
+
await callTool('vibe_v5_set', { formalVerify: 'require' }, RE)
|
|
538
|
+
const instE = instRootOf(RE)
|
|
539
|
+
await callTool('vibe_v5_record_proposition', { id: 'p-gate', statement: '必须形式化的命题', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RE, 'r-1')))
|
|
540
|
+
await callTool('vibe_v5_propose_verify', { target: 'p-gate', kind: 'proposition', reason: '直接表决试试' }, childAgent(childOf(RE, 'r-1')))
|
|
541
|
+
await settle(); delivered.length = 0; await drainWakes(3, RE)
|
|
542
|
+
{
|
|
543
|
+
const vp = delivered.filter(d => d.rootId === RE.id).map(d => d.prompt).join('\n')
|
|
544
|
+
assert(/【Lean 形式化验证(强制模式)】/.test(vp), 'the voting prompt says 强制模式')
|
|
545
|
+
assert(/必须产出 Lean 形式化/.test(vp), "'require' states the formalization is mandatory")
|
|
546
|
+
assert(/本次裁定不会生效/.test(vp), 'the prompt warns that the verdict will not take effect without it')
|
|
547
|
+
assert(/formal-required/.test(vp), 'it names the machine-readable reason code in the prompt itself')
|
|
548
|
+
assert(/vibe_v5_lean_archive/.test(vp) && /kind='blocked'/.test(vp), 'it gives the full tool name for the blocker route')
|
|
549
|
+
}
|
|
550
|
+
const gated = await voteToConclusion(RE, 'p-gate', new Map([['acad', 1], ['r-1', 1], ['r-2', 1]]))
|
|
551
|
+
assert(gated.verified.indexOf('p-gate') === -1, '★ a unanimous TRUE verdict did NOT promote the object to Verified/')
|
|
552
|
+
assert(gated.undecided.indexOf('p-gate') !== -1, '★ it is recorded as 未定论 instead')
|
|
553
|
+
assert(!existsSync(join(instE, 'Verified', '命题', 'p-gate.md')), 'no Verified card was written')
|
|
554
|
+
const todoE = readIf(join(instE, 'Formal', 'TODO.md'))
|
|
555
|
+
assert(existsSync(join(instE, 'Formal', 'TODO.md')), 'Formal/TODO.md was created')
|
|
556
|
+
assert(/p-gate/.test(todoE) && /formal-required/.test(todoE), '★ the object is on the formalization TODO with the machine-readable reason')
|
|
557
|
+
{
|
|
558
|
+
const chat = readdirSync(join(instE, 'Shared', 'Chat')).map(f => readIf(join(instE, 'Shared', 'Chat', f))).join('\n')
|
|
559
|
+
assert(/require 模式.*先有 Lean 通过或显式阻塞记录/.test(chat), 'the withholding is announced in the group chat')
|
|
560
|
+
}
|
|
561
|
+
// now formalize it and re-verify: the gate must open
|
|
562
|
+
const proofNow = await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-gate', content: 'theorem p_gate : 2 + 2 = 4 := by decide\n' }, childAgent(childOf(RE, 'r-1')))
|
|
563
|
+
assert(proofNow.ok === true && proofNow.passed === true, 'the object is now Lean-passed')
|
|
564
|
+
await callTool('vibe_v5_propose_verify', { target: 'p-gate', kind: 'proposition', reason: '已形式化,重新表决' }, childAgent(childOf(RE, 'r-1')))
|
|
565
|
+
const gated2 = await voteToConclusion(RE, 'p-gate', new Map([['acad', 1], ['r-1', 1], ['r-2', 1]]))
|
|
566
|
+
assert(gated2.verified.indexOf('p-gate') !== -1, '★ with a passing Lean artifact the same vote DOES promote it')
|
|
567
|
+
const card = readIf(join(instE, 'Verified', '命题', 'p-gate.md'))
|
|
568
|
+
assert(/- 形式化: Lean 通过/.test(card), '★ the Verified card records how strong the result is (Lean 通过)')
|
|
569
|
+
assert(/Verified\/Lean\/p-gate\.lean/.test(card), 'the card points at the archived proof')
|
|
570
|
+
// the blocker escape hatch must also open the gate
|
|
571
|
+
await callTool('vibe_v5_record_proposition', { id: 'p-blocked-ok', statement: '记录阻塞后可定论', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RE, 'r-1')))
|
|
572
|
+
await callTool('vibe_v5_lean_archive', { kind: 'blocked', target: 'p-blocked-ok', note: '命题涉及未形式化的分析学,本轮不做' }, childAgent(childOf(RE, 'r-1')))
|
|
573
|
+
await callTool('vibe_v5_propose_verify', { target: 'p-blocked-ok', kind: 'proposition', reason: '已记录阻塞' }, childAgent(childOf(RE, 'r-1')))
|
|
574
|
+
const gated3 = await voteToConclusion(RE, 'p-blocked-ok', new Map([['acad', 1], ['r-1', 1], ['r-2', 1]]))
|
|
575
|
+
assert(gated3.verified.indexOf('p-blocked-ok') !== -1, '★ an explicit reasoned blocker also lets the verdict through (decide by difficulty, but decide out loud)')
|
|
576
|
+
assert(/- 形式化: 阻塞(/.test(readIf(join(instE, 'Verified', '命题', 'p-blocked-ok.md'))), 'the card records the blocker')
|
|
577
|
+
|
|
578
|
+
// ---------- 9. the reply-channel judgement ----------
|
|
579
|
+
section('9 the per-round `formal` reply channel records the difficulty judgement')
|
|
580
|
+
const RF = makeRoot()
|
|
581
|
+
await foundInstitute(RF, '回执 formal 通道测试')
|
|
582
|
+
await callTool('vibe_v5_set', { formalVerify: 'require' }, RF)
|
|
583
|
+
const instF = instRootOf(RF)
|
|
584
|
+
await callTool('vibe_v5_record_proposition', { id: 'p-reply', statement: '用回执记录阻塞', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RF, 'r-1')))
|
|
585
|
+
// A member that never calls a Lean tool still has to state its difficulty judgement, so the
|
|
586
|
+
// JSON reply channel matters: it is the path that actually fires in practice.
|
|
587
|
+
const w1 = await wakeAndReply(RF, 'r-1', {
|
|
588
|
+
progress: '难度太高,本轮不做形式化。',
|
|
589
|
+
formal: { target: 'p-reply', decision: 'blocked', note: '需要大量未形式化的实分析前置知识' },
|
|
590
|
+
contextPct: 20,
|
|
591
|
+
})
|
|
592
|
+
assert(!!w1, 'r-1 was woken and answered')
|
|
593
|
+
const stF = await callTool('vibe_v5_status', {}, RF)
|
|
594
|
+
assert(stF.formal.blocked.indexOf('p-reply') !== -1, 'a `formal.decision=blocked` reply is recorded as a blocker')
|
|
595
|
+
assert(/实分析前置知识/.test(readIf(join(instF, 'Formal', 'Index.md'))), 'and its reason reaches the index')
|
|
596
|
+
// a `used` judgement records the file as attempted (not yet passed — nothing ran green)
|
|
597
|
+
const w2 = await wakeAndReply(RF, 'r-2', {
|
|
598
|
+
progress: '我写了形式化草稿。',
|
|
599
|
+
formal: { target: 'p-reply2', decision: 'used', file: 'Formal/p-reply2.lean' },
|
|
600
|
+
contextPct: 20,
|
|
601
|
+
})
|
|
602
|
+
assert(!!w2, 'r-2 was woken and answered')
|
|
603
|
+
const stF2 = await callTool('vibe_v5_status', {}, RF)
|
|
604
|
+
assert(stF2.formal.objects.some(o => o.target === 'p-reply2' && o.status === 'attempted'),
|
|
605
|
+
'a `formal.decision=used` reply records the object as attempted with its file')
|
|
606
|
+
// a plain `used` judgement must NOT withdraw an ESTABLISHED proof (contract §4): only a fidelity
|
|
607
|
+
// defect retracts one. v2 shipped the unconditional downgrade here; all four are asserted behaviourally.
|
|
608
|
+
await callTool('vibe_v5_record_proposition', { id: 'p-usedkeep', statement: '已有通过证明后再写一次 used 回执', value: 0.6, motive: 'm', p: 0.8 }, childAgent(childOf(RF, 'r-1')))
|
|
609
|
+
{
|
|
610
|
+
const arcU = await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-usedkeep', content: 'theorem p_usedkeep : 2 + 2 = 4 := by decide\n' }, RF)
|
|
611
|
+
assert(arcU.ok === true && arcU.passed === true, 'used-keep: the object starts Lean-passed')
|
|
612
|
+
const wU = await wakeAndReply(RF, 'r-2', {
|
|
613
|
+
progress: '这一轮只是又写了一遍草稿。',
|
|
614
|
+
formal: { target: 'p-usedkeep', decision: 'used', file: 'Formal/p-usedkeep.lean' },
|
|
615
|
+
contextPct: 20,
|
|
616
|
+
})
|
|
617
|
+
assert(!!wU, 'r-2 was woken for the used judgement')
|
|
618
|
+
const stU = await callTool('vibe_v5_status', {}, RF)
|
|
619
|
+
const recU = stU.formal.objects.find((o) => o.target === 'p-usedkeep')
|
|
620
|
+
assert(!!recU && recU.status === 'passed', '★ a `used` reply does NOT downgrade an already-passed object (got ' + JSON.stringify(recU) + ')')
|
|
621
|
+
assert(!!recU && recU.proof === 'Verified/Lean/p-usedkeep.lean', '★ and the proof pointer survives the reply')
|
|
622
|
+
assert(existsSync(join(instF, 'Verified', 'Lean', 'p-usedkeep.lean')), '★ and the archived proof is still on disk')
|
|
623
|
+
assert(stU.formal.passed.indexOf('p-usedkeep') !== -1, '★ status still reports it as Lean-passed')
|
|
624
|
+
const rerunU = await callTool('vibe_v5_lean_run', { file: 'Formal/p-usedkeep.lean', target: 'p-usedkeep' }, RF)
|
|
625
|
+
assert(rerunU.ok === true, 'used-keep: the work file runs green')
|
|
626
|
+
const recU2 = (await callTool('vibe_v5_status', {}, RF)).formal.objects.find((o) => o.target === 'p-usedkeep')
|
|
627
|
+
assert(!!recU2 && recU2.status === 'passed', '★ a plain re-run does not downgrade a passed record (contract §4)')
|
|
628
|
+
}
|
|
629
|
+
// a blocker with no note must be refused AND reported back to the member
|
|
630
|
+
delivered.length = 0
|
|
631
|
+
const w3 = await wakeAndReply(RF, 'r-1', { formal: { target: 'p-reply3', decision: 'blocked' }, contextPct: 20 })
|
|
632
|
+
assert(!!w3, 'r-1 was woken for the third judgement')
|
|
633
|
+
const said = delivered.map(d => d.prompt).join('\n')
|
|
634
|
+
assert(/必须写明 note/.test(said), 'a blocked judgement without a note is refused with an explicit notice to the member')
|
|
635
|
+
assert((await callTool('vibe_v5_status', {}, RF)).formal.objects.every(o => o.target !== 'p-reply3'),
|
|
636
|
+
'and no blocker record is created for the refused judgement')
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
// ---------- 10. reporting ----------
|
|
640
|
+
section('10 the office can audit formal strength')
|
|
641
|
+
const rep = await callTool('vibe_v5_report', {}, RE)
|
|
642
|
+
assert(/## Lean 形式化/.test(rep.report), 'the report has a Lean formal-verification section')
|
|
643
|
+
assert(/已通过:.*p-gate/.test(rep.report), 'the report lists Lean-passed objects')
|
|
644
|
+
assert(/已记录阻塞:.*p-blocked-ok/.test(rep.report), 'the report lists blocked objects')
|
|
645
|
+
const stOff = await callTool('vibe_v5_report', {}, RA)
|
|
646
|
+
assert(/未启用(`formalVerify` = off/.test(stOff.report), 'in off mode the report says the feature is not enabled')
|
|
647
|
+
|
|
648
|
+
// ---------- 11. a faithfulness defect withdraws the proof, it is NOT a refutation ----------
|
|
649
|
+
section('11 ★ a faithfulness defect withdraws the proof instead of recording 命题为假 (contract §4.1)')
|
|
650
|
+
const RG = makeRoot()
|
|
651
|
+
await foundInstitute(RG, '忠实性缺陷语义测试')
|
|
652
|
+
await callTool('vibe_v5_set', { formalVerify: 'encourage' }, RG)
|
|
653
|
+
const instG = instRootOf(RG)
|
|
654
|
+
// make p-def Lean-PASSED first: the archived proof is what a defect must withdraw
|
|
655
|
+
const arcG = await callTool('vibe_v5_lean_archive', {
|
|
656
|
+
kind: 'proof', target: 'p-def', content: 'theorem p_def : (1:Nat) + 1 = 2 := by decide\n',
|
|
657
|
+
}, childAgent(childOf(RG, 'r-1')))
|
|
658
|
+
assert(arcG.ok === true && arcG.passed === true, 'p-def is Lean-passed before the review')
|
|
659
|
+
assert(existsSync(join(instG, 'Verified', 'Lean', 'p-def.lean')), 'its archived proof exists on disk')
|
|
660
|
+
// a voter reports a FIDELITY DEFECT through the reply channel
|
|
661
|
+
const wDef = await wakeAndReply(RG, 'r-1', {
|
|
662
|
+
progress: '逐条核对后发现 Lean 陈述与命题不一致。',
|
|
663
|
+
formal: { target: 'p-def', decision: 'defect', note: 'Lean 里把"连续"写成了逐点连续,条件被加强了' },
|
|
664
|
+
contextPct: 20,
|
|
665
|
+
})
|
|
666
|
+
assert(!!wDef, 'the reviewer was woken and answered with a defect report')
|
|
667
|
+
const stG = await callTool('vibe_v5_status', {}, RG)
|
|
668
|
+
const recG = (stG.formal.objects || []).find(o => o.target === 'p-def') || {}
|
|
669
|
+
assert(recG.status === 'attempted', '★ the object is demoted to attempted (a defect is not a proof any more), got ' + recG.status)
|
|
670
|
+
assert(!recG.proof, '★ the archived proof is cleared from the record')
|
|
671
|
+
assert(/加强了/.test(String(recG.note || '')), 'the concrete deviation is recorded on the object')
|
|
672
|
+
const proofPathG = join(instG, 'Verified', 'Lean', 'p-def.lean')
|
|
673
|
+
const proofNowG = existsSync(proofPathG) ? readFileSync(proofPathG, 'utf8') : ''
|
|
674
|
+
assert(!existsSync(proofPathG) || /已撤回/.test(proofNowG),
|
|
675
|
+
'★ the archived proof is withdrawn from Verified/Lean/ (deleted, or replaced by a withdrawal notice when the host cannot delete)')
|
|
676
|
+
assert(!/theorem p_def/.test(proofNowG), '★ the original proof text is no longer readable as the object\'s proof')
|
|
677
|
+
assert(existsSync(join(instG, 'Formal', 'p-def.lean')), 'the working file is kept (the code is not lost)')
|
|
678
|
+
const todoG = readIf(join(instG, 'Formal', 'TODO.md'))
|
|
679
|
+
assert(/p-def/.test(todoG) && /忠实性缺陷/.test(todoG), 'the object enters Formal/TODO.md as a formalisation defect')
|
|
680
|
+
const idxG = readIf(join(instG, 'Formal', 'Index.md'))
|
|
681
|
+
assert(/加强了/.test(idxG), 'the human-readable index carries the concrete deviation')
|
|
682
|
+
assert(/attempted/.test(idxG) && !/Verified\/Lean\/p-def\.lean/.test(idxG.split('p-def')[1] || ''),
|
|
683
|
+
'the index shows attempted and no longer points at a proof')
|
|
684
|
+
// a defect without a note is refused
|
|
685
|
+
delivered.length = 0
|
|
686
|
+
const wDef2 = await wakeAndReply(RG, 'r-1', { formal: { target: 'p-def2', decision: 'defect' }, contextPct: 20 })
|
|
687
|
+
assert(!!wDef2, 'the reviewer was woken for the note-less defect')
|
|
688
|
+
assert(/必须写明 note/.test(delivered.map(d => d.prompt).join('\n')), 'a defect without a note is refused with an explicit notice')
|
|
689
|
+
assert((await callTool('vibe_v5_status', {}, RG)).formal.objects.every(o => o.target !== 'p-def2'),
|
|
690
|
+
'and no record is created for the refused defect')
|
|
691
|
+
|
|
692
|
+
// ---------- 12. after a defect, require mode refuses to conclude -------------
|
|
693
|
+
section('12 ★ a defect makes the require gate block the conclusion (re-formalise, do not conclude 假)')
|
|
694
|
+
await callTool('vibe_v5_set', { formalVerify: 'require' }, RG)
|
|
695
|
+
await callTool('vibe_v5_record_proposition', { id: 'p-def', statement: '连续函数在闭区间上一致连续(被写窄的形式化)', value: 0.6, motive: 'm', p: 0.9 }, childAgent(childOf(RG, 'r-1')))
|
|
696
|
+
await callTool('vibe_v5_propose_verify', { target: 'p-def', kind: 'proposition', reason: '缺陷后重验' }, childAgent(childOf(RG, 'r-1')))
|
|
697
|
+
// every voter says TRUE — without the fix this would be recorded as a concluded object
|
|
698
|
+
const stDef = await voteToConclusion(RG, 'p-def', new Map([['acad', 1], ['r-1', 1], ['r-2', 1]]))
|
|
699
|
+
assert(stDef.verified.indexOf('p-def') === -1, '★ the object is NOT verified: a withdrawn proof cannot support a conclusion')
|
|
700
|
+
assert(stDef.undecided.indexOf('p-def') !== -1, '★ it is recorded as 未定论 and stays in the library')
|
|
701
|
+
assert(!existsSync(join(instG, 'Verified', '命题', 'p-def.md')), 'no Verified card is written for it')
|
|
702
|
+
assert(/p-def/.test(readIf(join(instG, 'Formal', 'TODO.md'))), 'it is on the formalisation TODO list')
|
|
703
|
+
|
|
704
|
+
console.log('')
|
|
705
|
+
console.log('passed=' + passed + ' failed=' + failed)
|
|
706
|
+
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
707
|
+
console.log('ALL GREEN')
|
|
708
|
+
process.exit(0)
|