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,1082 +1,1082 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// V4 LEAN FORMAL VERIFICATION SUITE (docs/formal-verification.md)
|
|
3
|
-
//
|
|
4
|
-
// Asserts the whole contract of the `formalVerify` knob for the v4 preset:
|
|
5
|
-
// · 'off' is a TRUE no-op (no Lean text anywhere, no gate, verification unchanged)
|
|
6
|
-
// · 'encourage' injects the Lean section into the VERIFICATION prompt AND the ordinary work
|
|
7
|
-
// round, and — the actual point of the feature — turns the voting prompt into a
|
|
8
|
-
// FIDELITY review once a Lean run has passed
|
|
9
|
-
// · 'require' withholds a unanimous 真/假 verdict as 未定论 until the object is Lean-passed
|
|
10
|
-
// or carries an explicit, reasoned blocker record; then it allows it, and the
|
|
11
|
-
// Verified card records how strong the result really is
|
|
12
|
-
// · the three tools (run / archive / lib) write the right things to the right paths
|
|
13
|
-
//
|
|
14
|
-
// The Lean toolchain is mocked through the `subprocess` SERVICE (a fake Lean: a file passes unless
|
|
15
|
-
// it still contains `sorry` or the marker `-- FAIL`), so the tests exercise the REAL code path
|
|
16
|
-
// (resolveExecutable → spawn → collected stdout → exit code) without requiring Lean to be installed.
|
|
17
|
-
//
|
|
18
|
-
// V4_PLUGIN lets a sensitivity probe point this suite at a MUTATED copy of the plugin; without that
|
|
19
|
-
// override every probe would run the unmutated file and pass vacuously (AUDIT-CHECKLIST §2.5).
|
|
20
|
-
//
|
|
21
|
-
// Prompt text is asserted through the plugin's own `vibe_v4_prompts` host tool — the exact builder
|
|
22
|
-
// the framework uses — rather than by guessing which queued wake carries which prompt. That keeps
|
|
23
|
-
// the assertions precise even when the scheduler queues several rounds in one pass, and the suite
|
|
24
|
-
// still drives the real consensus path for every STATE transition (AUDIT-CHECKLIST §0.1/§2.2).
|
|
25
|
-
//
|
|
26
|
-
// Run: node formal-verify-v4.test.mjs
|
|
27
|
-
// ============================================================
|
|
28
|
-
import { mkdtempSync, existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, rmSync } from 'node:fs'
|
|
29
|
-
import { tmpdir } from 'node:os'
|
|
30
|
-
import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
|
|
31
|
-
import { pathToFileURL, fileURLToPath } from 'node:url'
|
|
32
|
-
|
|
33
|
-
// V4_PLUGIN (same convention the v5 suite uses for V5_PLUGIN): point the suite at a MUTATED copy of
|
|
34
|
-
// the plugin so a sensitivity probe can prove each invariant actually turns this suite red. Accept a
|
|
35
|
-
// plain filesystem path or a file:// URL, and never touch the value otherwise.
|
|
36
|
-
function pluginUrl() {
|
|
37
|
-
const raw = process.env.V4_PLUGIN
|
|
38
|
-
if (!raw || !String(raw).trim()) return new URL('
|
|
39
|
-
const s = String(raw).trim()
|
|
40
|
-
if (/^file:/i.test(s)) return new URL(s)
|
|
41
|
-
return pathToFileURL(fileURLToPath(new URL('file:///' + s.replace(/\\/g, '/'))))
|
|
42
|
-
}
|
|
43
|
-
const PLUGIN = pluginUrl()
|
|
44
|
-
// docs §10 item 10: the Lean prompt corpus is shipped under prompt-corpus-v4/ so a HUMAN can read
|
|
45
|
-
// the exact text the framework sends. V4_CORPUS_DIR overrides the destination (same convention v3
|
|
46
|
-
// and v5 use for V3_CORPUS_DIR / V5_CORPUS_DIR).
|
|
47
|
-
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
48
|
-
const CORPUS_DIR = process.env.V4_CORPUS_DIR ? pathResolve(process.env.V4_CORPUS_DIR) : join(HERE, 'prompt-corpus-v4')
|
|
49
|
-
|
|
50
|
-
let passed = 0, failed = 0
|
|
51
|
-
const failures = []
|
|
52
|
-
const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
|
|
53
|
-
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
54
|
-
const section = (t) => console.log('\n[' + t + ']')
|
|
55
|
-
const JSONX = o => '```json\n' + JSON.stringify(o) + '\n```'
|
|
56
|
-
const promptOf = (entry) => (entry && entry.blocks && entry.blocks[0] && entry.blocks[0].text) || ''
|
|
57
|
-
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
58
|
-
|
|
59
|
-
// ===============================================================
|
|
60
|
-
// MOCK HOST — the shape selfdrive-v4 / e2e-v4-fixes use, plus a fake Lean
|
|
61
|
-
// ===============================================================
|
|
62
|
-
let toolchainAvailable = true
|
|
63
|
-
// A host that exposes NO `subprocess` service at all (docs §7 → NO_SUBPROCESS, and §6 hard rule 4
|
|
64
|
-
// requires the injected guidance to name that code so the resident records a blocker instead of
|
|
65
|
-
// retrying forever).
|
|
66
|
-
let subprocessAvailable = true
|
|
67
|
-
// A stub host whose shell EXITS 0 WITHOUT deleting anything (a permissions quirk / a stub host).
|
|
68
|
-
// Used by §13 to prove the withdrawal is not a best-effort delete: the framework must confirm the
|
|
69
|
-
// file is gone through the fs service and fall back to overwriting it with a withdrawal notice.
|
|
70
|
-
let shellDeletesFiles = true
|
|
71
|
-
const leanRuns = [] // every spawn the framework made, for cwd/argv assertions
|
|
72
|
-
const terminated = [] // files whose handle the framework actively terminate()d (docs §7)
|
|
73
|
-
const shellCalls = [] // every platform-shell script (mkdir at mount, Remove-Item on defect)
|
|
74
|
-
|
|
75
|
-
function makeSubprocess() {
|
|
76
|
-
return {
|
|
77
|
-
async resolveExecutable(cmd) {
|
|
78
|
-
if (!toolchainAvailable) throw new Error('spawn lean ENOENT')
|
|
79
|
-
if (String(cmd) !== 'lean' && String(cmd) !== 'lake') throw new Error('unknown executable ' + cmd)
|
|
80
|
-
return String(cmd)
|
|
81
|
-
},
|
|
82
|
-
spawn(spec) {
|
|
83
|
-
// The preset drives the platform shell through this SAME `subprocess` service (runShell:
|
|
84
|
-
// powershell / /bin/sh) for mkdir at mount, and — since docs §4.1 — for Remove-Item when a
|
|
85
|
-
// `defect` retracts an archived proof. Handle it here so that retraction is a REAL filesystem
|
|
86
|
-
// deletion, exactly as the real host performs it; otherwise "the proof file is gone" could
|
|
87
|
-
// only be asserted against a mock's bookkeeping.
|
|
88
|
-
const argv0 = String((spec.argv && spec.argv[0]) || '')
|
|
89
|
-
if (/powershell|cmd\.exe|\/bin\/sh|(^|\/)sh$/i.test(argv0)) {
|
|
90
|
-
const script = String(spec.argv[spec.argv.length - 1] || '')
|
|
91
|
-
shellCalls.push(script)
|
|
92
|
-
const m = script.match(/-LiteralPath\s+'((?:[^']|'')*)'/)
|
|
93
|
-
if (shellDeletesFiles && /Remove-Item/.test(script) && m) rmSync(m[1].replace(/''/g, "'"), { force: true })
|
|
94
|
-
if (shellDeletesFiles && /^rm -f /.test(script)) for (const q of script.slice(6).match(/'[^']*'/g) || []) rmSync(q.slice(1, -1), { force: true })
|
|
95
|
-
return {
|
|
96
|
-
done: Promise.resolve({ exitCode: 0, signal: null }),
|
|
97
|
-
collected: { stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) }, stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) } },
|
|
98
|
-
terminate() {},
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// The plugin passes an ABSOLUTE path as the last argv element, so resolve it directly; fall
|
|
102
|
-
// back to scanning the argv for an existing .lean file (mirrors leanAbsPath).
|
|
103
|
-
const last = spec.argv[spec.argv.length - 1]
|
|
104
|
-
const file = existsSync(last) ? last : (spec.argv.find(a => /\.lean$/.test(String(a)) && existsSync(a)) || last)
|
|
105
|
-
const text = existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
106
|
-
const bad = /sorry|-- FAIL/.test(text)
|
|
107
|
-
leanRuns.push({ argv: spec.argv.slice(), file, cwd: spec.cwd, graceMs: spec.graceMs, stdio: spec.stdio })
|
|
108
|
-
// A HANG file never settles and never exits: the ONLY thing that can end it is the framework's
|
|
109
|
-
// own timeout calling handle.terminate() (docs §7). Records each terminate so the suite can
|
|
110
|
-
// assert the timeout path is ACTIVE, not merely reported.
|
|
111
|
-
if (/-- HANG/.test(text)) {
|
|
112
|
-
return {
|
|
113
|
-
done: new Promise(() => {}),
|
|
114
|
-
collected: { stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) }, stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) } },
|
|
115
|
-
terminate() { terminated.push(file) },
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const stdout = bad ? '' : 'ok\n'
|
|
119
|
-
const stderr = bad ? 'error: declaration uses sorry\n' : ''
|
|
120
|
-
return {
|
|
121
|
-
done: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null }),
|
|
122
|
-
collected: {
|
|
123
|
-
stdout: { readFrom: () => ({ text: stdout, nextOffset: stdout.length, lossy: false }) },
|
|
124
|
-
stderr: { readFrom: () => ({ text: stderr, nextOffset: stderr.length, lossy: false }) },
|
|
125
|
-
},
|
|
126
|
-
terminate() {},
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function makeHost() {
|
|
133
|
-
const WS = mkdtempSync(join(tmpdir(), 'vibe-v4-lean-'))
|
|
134
|
-
const listeners = {}, toolRegs = [], spawns = [], followups = []
|
|
135
|
-
const subprocess = makeSubprocess()
|
|
136
|
-
let ROOT
|
|
137
|
-
const ctx = {
|
|
138
|
-
get(name) { return name === 'subprocess' && subprocessAvailable ? subprocess : undefined },
|
|
139
|
-
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
140
|
-
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
141
|
-
logger: { info() {}, warn() {}, error() {} },
|
|
142
|
-
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
143
|
-
tools: { register(s) { toolRegs.push(s) } }, commands: { register() {} },
|
|
144
|
-
// Real DSH exposes ONLY subagents.sendMessage for continuable wakes (followup is an Agent
|
|
145
|
-
// method, not a subagents service method) — deliberately no followup, so a regression fails.
|
|
146
|
-
subagents: {
|
|
147
|
-
list() { return ['spawn'] },
|
|
148
|
-
async startContinuable({ label, request }) { const id = 'c' + (spawns.length + 1); spawns.push({ label, request, childId: id }); return { childId: id } },
|
|
149
|
-
async sendMessage(parent, childId, blocks) { followups.push({ childId, blocks }) },
|
|
150
|
-
interrupt() {},
|
|
151
|
-
},
|
|
152
|
-
agents: { roots() { return [] }, get(id) { return id === 'sess-A' ? ROOT : undefined } },
|
|
153
|
-
fs: {
|
|
154
|
-
async resolve(rel, opts) { const b = (opts && opts.cwd) || WS; const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/')); return { targetKey: p, displayPath: p } },
|
|
155
|
-
async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
|
|
156
|
-
async readText(t) { return readFileSync(t.targetKey, 'utf8') },
|
|
157
|
-
async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
|
|
158
|
-
async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
159
|
-
},
|
|
160
|
-
}
|
|
161
|
-
ROOT = { id: 'sess-A', options: { provider: 'mock', model: 'm' }, session: { id: 'sess-A', header: { cwd: WS, parentSession: undefined } }, followup() {}, ctx: undefined }
|
|
162
|
-
const projectRoot = join(WS, 'VibeMath', 'Projects', 'default')
|
|
163
|
-
const vibeRoot = join(WS, 'VibeMath')
|
|
164
|
-
let consumed = 0
|
|
165
|
-
const h = {
|
|
166
|
-
WS, ctx, toolRegs, spawns, followups, ROOT, projectRoot, vibeRoot,
|
|
167
|
-
async callTool(n, a, agent) {
|
|
168
|
-
const s = toolRegs.find(x => x.name === n); if (!s) throw new Error('no tool ' + n)
|
|
169
|
-
return JSON.parse(await s.execute(a || {}, { agent: agent || ROOT }))
|
|
170
|
-
},
|
|
171
|
-
/** The exact prompt text a resident would receive, read through the plugin's own host tool. */
|
|
172
|
-
async prompts(which, rId, arg) {
|
|
173
|
-
const out = await h.callTool('vibe_v4_prompts', Object.assign({ which, member: rId }, arg || {}), ROOT)
|
|
174
|
-
return out.text
|
|
175
|
-
},
|
|
176
|
-
resAgent: (cid) => ({ id: cid, session: { id: cid, header: { cwd: WS, parentSession: 'sess-A' } } }),
|
|
177
|
-
ridOfChild(cid) { for (const sp of spawns) if (sp.childId === cid) return sp.label; return '' },
|
|
178
|
-
childOf(rId) { for (const sp of spawns) if (sp.label === rId) return sp.childId; return '' },
|
|
179
|
-
fireEnd(cid, reply) {
|
|
180
|
-
const blocks = reply === undefined ? [] : [{ type: 'text', text: JSONX(reply) }]
|
|
181
|
-
for (const fn of (listeners['subagent/end'] || [])) fn({ id: cid, runId: 'r', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: blocks })
|
|
182
|
-
},
|
|
183
|
-
/** Every prompt this project's residents have received (founding + wakes). */
|
|
184
|
-
allPrompts() {
|
|
185
|
-
return spawns.map(s => (s.request && s.request.prompt && s.request.prompt[0] && s.request.prompt[0].text) || '').join('\n')
|
|
186
|
-
+ '\n' + followups.map(promptOf).join('\n')
|
|
187
|
-
},
|
|
188
|
-
nextWake() { while (consumed < followups.length) return followups[consumed++]; return null },
|
|
189
|
-
resetCursor() { consumed = followups.length },
|
|
190
|
-
}
|
|
191
|
-
return h
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function mount() {
|
|
195
|
-
const h = makeHost()
|
|
196
|
-
const mod = await import(PLUGIN.href + '?t=' + Date.now() + Math.random())
|
|
197
|
-
;(mod.default || mod).apply(h.ctx)
|
|
198
|
-
return h
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ===============================================================
|
|
202
|
-
// driving helpers
|
|
203
|
-
// ===============================================================
|
|
204
|
-
/** Complete every brainstorm round so the run becomes phase=active, then park the heartbeat. */
|
|
205
|
-
async function establish() {
|
|
206
|
-
const h = await mount()
|
|
207
|
-
const before = h.spawns.length
|
|
208
|
-
await h.callTool('vibe_v4_start', { problem: '形式化契约验证测试', residentCount: 2 })
|
|
209
|
-
for (let i = 0; i < 60 && h.spawns.length < before + 2; i++) await sleep(10)
|
|
210
|
-
for (const sp of h.spawns.slice(before)) { h.fireEnd(sp.childId, { summary: '初始见解。', solved: false, contextPct: 10 }); await sleep(30) }
|
|
211
|
-
// Park the heartbeat far away: these tests drive wakes explicitly, so a short heartbeat would
|
|
212
|
-
// only add unrelated normal rounds and make the followup stream harder to reason about.
|
|
213
|
-
await h.callTool('vibe_v4_set', { activityTimeoutMs: 600000 })
|
|
214
|
-
for (let i = 0; i < 40; i++) { const s = await h.callTool('vibe_v4_status', {}); if (s.phase === 'active') break; await sleep(20) }
|
|
215
|
-
await sleep(30)
|
|
216
|
-
h.resetCursor()
|
|
217
|
-
return h
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Answer the next wake with `replyFor(promptText, rId)`; returns how many wakes were answered.
|
|
221
|
-
* `stop` is evaluated AFTER processing each wake, never before the first one: a run that is simply
|
|
222
|
-
* idle between rounds would otherwise satisfy "no verification in flight" at entry and the loop
|
|
223
|
-
* would exit without answering anything.
|
|
224
|
-
*/
|
|
225
|
-
async function drive(h, replyFor, stop, budget = 40) {
|
|
226
|
-
let n = 0
|
|
227
|
-
for (; n < budget; n++) {
|
|
228
|
-
const w = h.nextWake()
|
|
229
|
-
if (!w) break
|
|
230
|
-
const pt = promptOf(w)
|
|
231
|
-
h.fireEnd(w.childId, replyFor(pt, h.ridOfChild(w.childId)))
|
|
232
|
-
await sleep(12)
|
|
233
|
-
if (stop && await stop()) { n++; break }
|
|
234
|
-
}
|
|
235
|
-
return n
|
|
236
|
-
}
|
|
237
|
-
const verifySettled = (h) => async () => { const s = await h.callTool('vibe_v4_status', {}); return s.verifyInProgress === false && s.pendingVerify === null }
|
|
238
|
-
const voteReply = (plan) => (pt, rId) => (/团队验证/.test(pt)
|
|
239
|
-
? { vote: { verdict: plan(rId), reason: rId + ' 的判断' } }
|
|
240
|
-
: { summary: '继续推进。', solved: false, contextPct: 20 })
|
|
241
|
-
/** Drive the pending verification to a settled state and return the resulting status. */
|
|
242
|
-
async function settleVerify(h, plan = () => 1, budget = 40) {
|
|
243
|
-
await drive(h, voteReply(plan), verifySettled(h), budget)
|
|
244
|
-
return await h.callTool('vibe_v4_status', {})
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Record a card for `target`, have r-1 propose verifying it, vote it wherever it lands, and return
|
|
248
|
-
* the settled status. Prompt TEXT is asserted separately (see `h.prompts`) — this helper exercises
|
|
249
|
-
* the real consensus path for the STATE transitions.
|
|
250
|
-
*/
|
|
251
|
-
async function proposeAndVote(h, target, opts = {}) {
|
|
252
|
-
const card = opts.card || { id: target, title: target, statement: '待验证对象 ' + target, prob: 0.8, value: 0.6, motivation: 'm' }
|
|
253
|
-
await h.callTool('vibe_v4_record_proposition', card, h.resAgent(h.childOf('r-1')))
|
|
254
|
-
// The heartbeat is parked far away, so drive the FIRST round explicitly: an idle resident is
|
|
255
|
-
// woken immediately by an incoming message (postMessage → wakeResident).
|
|
256
|
-
const before = h.followups.length
|
|
257
|
-
await h.callTool('vibe_v4_message', { to: 'r-1', content: '请处理本轮工作。' })
|
|
258
|
-
for (let i = 0; i < 300 && h.followups.length === before; i++) await sleep(10)
|
|
259
|
-
const proposal = opts.proposal || { summary: '提议验证 ' + target + '。', solved: false, propose_verify: target, contextPct: 20 }
|
|
260
|
-
await drive(h, (pt) => (/团队验证/.test(pt)
|
|
261
|
-
? { vote: { verdict: (opts.verdict === undefined ? 1 : opts.verdict), reason: '预看提示词' } }
|
|
262
|
-
: proposal), verifySettled(h))
|
|
263
|
-
// The verification may only have been QUEUED by the proposal round; finish it.
|
|
264
|
-
return await settleVerify(h, opts.plan || (() => (opts.verdict === undefined ? 1 : opts.verdict)))
|
|
265
|
-
}
|
|
266
|
-
/** Wake exactly the given resident with an ordinary work round and return that wake. */
|
|
267
|
-
async function workWake(h, rId) {
|
|
268
|
-
const before = h.followups.length
|
|
269
|
-
await h.callTool('vibe_v4_message', { to: rId, content: '请继续推进。' })
|
|
270
|
-
for (let i = 0; i < 300 && h.followups.length === before; i++) await sleep(10)
|
|
271
|
-
const w = h.nextWake()
|
|
272
|
-
assert(w !== null, 'a work wake was delivered to ' + rId)
|
|
273
|
-
return w
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ===============================================================
|
|
277
|
-
console.log('-- V4 Lean formal verification --')
|
|
278
|
-
|
|
279
|
-
// ---------- 1. 'off' is a true no-op ----------
|
|
280
|
-
section("1 'off' (default) is a true no-op")
|
|
281
|
-
const A = await establish()
|
|
282
|
-
{
|
|
283
|
-
const st = await A.callTool('vibe_v4_status', {})
|
|
284
|
-
assert(st.formal.mode === 'off' && st.formal.on === false, "the default mode is 'off' (got " + st.formal.mode + ')')
|
|
285
|
-
assert(/formalVerify=off/.test(st.params), "status exposes formalVerify=off in its readable params (got " + st.params + ')')
|
|
286
|
-
// Scan BEFORE any hand-written test message is injected: this test's own text would otherwise
|
|
287
|
-
// taint the scan. The scan is on the feature's own TOKENS rather than the bare word 形式化: the
|
|
288
|
-
// pre-existing brainstorm brief already contains 形式化 in the unrelated sentence about being
|
|
289
|
-
// free to invent theories, so asserting the bare word would fail for a reason that has nothing
|
|
290
|
-
// to do with this contract (AUDIT-CHECKLIST §2.2).
|
|
291
|
-
const all = A.allPrompts()
|
|
292
|
-
assert(!/Lean/.test(all), 'no founding/round prompt mentions Lean in off mode')
|
|
293
|
-
assert(!/顺手形式化|Lean 通过|忠实性审查|vibe_v4_lean/.test(all), 'no founding/round prompt carries any Lean-formalization text in off mode')
|
|
294
|
-
assert(all.length > 0, 'sanity: the scan actually saw the founding prompts')
|
|
295
|
-
// `off` must not CREATE Lean state at all: the feature's durable record file must not appear
|
|
296
|
-
// for a session that never used the feature (v3's suite pins the same invariant).
|
|
297
|
-
assert(!existsSync(join(A.projectRoot, 'State', 'formal.json')), '★ off mode creates NO State/formal.json (a TRUE no-op, not just an empty record store)')
|
|
298
|
-
}
|
|
299
|
-
// an off-mode verification must behave exactly as before: unanimous 真 → Verified/, no formal record
|
|
300
|
-
const offSettled = await proposeAndVote(A, 'p-off', {
|
|
301
|
-
card: { id: 'p-off', title: '关模式', statement: '关模式下的普通命题', prob: 0.8, value: 0.6, motivation: 'm' },
|
|
302
|
-
proposal: { summary: '请提议验证 p-off。', solved: false, propose_verify: 'p-off', contextPct: 20 },
|
|
303
|
-
})
|
|
304
|
-
assert(existsSync(join(A.projectRoot, 'Verified', '命题', 'p-off.md')), "★ 'off' still closes a unanimous TRUE verdict with NO Lean artifact")
|
|
305
|
-
assert(offSettled.formal.passed.length === 0 && offSettled.formal.objects.length === 0, "'off' records no formal object at all")
|
|
306
|
-
assert(!/形式化/.test(readIf(join(A.projectRoot, 'Verified', '命题', 'p-off.md'))), 'the Verified card carries no formal line in off mode')
|
|
307
|
-
assert(!existsSync(join(A.projectRoot, 'Formal', 'TODO.md')) || !/p-off/.test(readIf(join(A.projectRoot, 'Formal', 'TODO.md'))), 'no formalization TODO was created for p-off')
|
|
308
|
-
assert(!/顺手形式化|Lean/.test(await A.prompts('verify', 'r-1', { target: 'p-off' })), 'the voting prompt carries no Lean text in off mode')
|
|
309
|
-
assert(!/顺手形式化/.test(await A.prompts('normal', 'r-1')), 'the work prompt carries no formalization line in off mode')
|
|
310
|
-
// the tools still EXIST in off mode (static registration), they are just never advertised
|
|
311
|
-
assert(['vibe_v4_lean_run', 'vibe_v4_lean_archive', 'vibe_v4_lean_lib'].every(n => !!A.toolRegs.find(t => t.name === n)),
|
|
312
|
-
'the three Lean tools are registered in every mode (registration is static)')
|
|
313
|
-
|
|
314
|
-
// ★ The mode switch must be REACHABLE THROUGH THE TOOL SCHEMA (2.3.2 defect D1) ──────────────
|
|
315
|
-
// Every tool schema here is closed (`additionalProperties:false`), so a key the schema does not
|
|
316
|
-
// advertise is REJECTED by any schema-validating provider. v3 shipped 2.3.0/2.3.1 with all four Lean
|
|
317
|
-
// parameters missing from the set-params schema while every assertion in this file stayed green —
|
|
318
|
-
// because the suite calls the handler DIRECTLY and never inspects the registered schema. The feature
|
|
319
|
-
// could not be switched on at all through the tool interface.
|
|
320
|
-
{
|
|
321
|
-
const setSpec = A.toolRegs.find((t) => t.name === 'vibe_v4_set')
|
|
322
|
-
assert(!!setSpec, "vibe_v4_set is registered")
|
|
323
|
-
assert(setSpec.parameters && setSpec.parameters.type === 'object' && setSpec.parameters.additionalProperties === false,
|
|
324
|
-
'★ vibe_v4_set publishes a CLOSED object schema (an unlisted key is rejected, so the schema IS the contract)')
|
|
325
|
-
for (const k of ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']) {
|
|
326
|
-
assert(Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k),
|
|
327
|
-
'★ the registered schema advertises ' + k + ' (every other surface documents it; a schema that omits it makes the switch unreachable)')
|
|
328
|
-
}
|
|
329
|
-
assert(JSON.stringify(setSpec.parameters.properties.formalVerify.enum) === JSON.stringify(['off', 'encourage', 'require']),
|
|
330
|
-
'the schema narrows formalVerify to the three real modes (a typo must not be a fourth)')
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// A stray `formal` reply in OFF mode must be INERT (finding #1): the reply contract does not offer
|
|
334
|
-
// the field there, so honouring it would contradict "off is a TRUE no-op". The TOOLS stay usable.
|
|
335
|
-
{
|
|
336
|
-
await A.callTool('vibe_v4_record_proposition', { id: 'p-offreply', title: 'off', statement: '关模式下注入回执', prob: 0.8, value: 0.6, motivation: 'm' }, A.resAgent(A.childOf('r-1')))
|
|
337
|
-
const wOffR = await workWake(A, 'r-1')
|
|
338
|
-
A.fireEnd(wOffR.childId, { summary: '继续。', solved: false, contextPct: 20, formal: { target: 'p-offreply', decision: 'defect', note: '不应被记录' } })
|
|
339
|
-
await sleep(80)
|
|
340
|
-
const stOffReply = await A.callTool('vibe_v4_status', {})
|
|
341
|
-
assert(stOffReply.formal.objects.length === 0, '★ a stray `formal` reply in off mode records NO formal object')
|
|
342
|
-
assert(!existsSync(join(A.projectRoot, 'Formal', 'p-offreply.lean')), '★ and writes no formal working file')
|
|
343
|
-
assert(!existsSync(join(A.projectRoot, 'Formal', 'TODO.md')) || !/p-offreply/.test(readIf(join(A.projectRoot, 'Formal', 'TODO.md'))), '★ and creates no TODO entry')
|
|
344
|
-
assert(!existsSync(join(A.projectRoot, 'State', 'formal.json')), '★ and still no State/formal.json after the stray reply (the off-mode guard is a true no-op)')
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ---------- 2. parameter validation + runtime switching ----------
|
|
348
|
-
section('2 parameter validation and runtime switching')
|
|
349
|
-
const B = await establish()
|
|
350
|
-
{
|
|
351
|
-
const bad = await B.callTool('vibe_v4_set', { formalVerify: 'banana' })
|
|
352
|
-
assert(bad.formal.mode === 'off' && /formalVerify=off/.test(bad.params), "an unknown mode degrades to 'off', never to a stronger mode (got " + bad.formal.mode + ')')
|
|
353
|
-
const enc = await B.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
354
|
-
assert(enc.formal.mode === 'encourage', "'encourage' is accepted")
|
|
355
|
-
const req = await B.callTool('vibe_v4_set', { formalVerify: 'require', leanTimeoutMs: -5, leanCommand: ' ' })
|
|
356
|
-
assert(req.formal.mode === 'require', "'require' is accepted")
|
|
357
|
-
assert(/leanTimeoutMs=120000/.test(req.params), 'a non-positive leanTimeoutMs falls back to the default (' + (/leanTimeoutMs=\S+/.exec(req.params) || [])[0] + ')')
|
|
358
|
-
assert(/leanCommand=lean/.test(req.params), 'a blank leanCommand falls back to "lean"')
|
|
359
|
-
const lk = await B.callTool('vibe_v4_set', { leanCommand: 'lake', leanArgs: ['env', 'lean'] })
|
|
360
|
-
assert(/leanCommand=lake/.test(lk.params) && /leanArgs=env,lean/.test(lk.params), 'leanCommand/leanArgs are settable (lake env lean)')
|
|
361
|
-
const la = await B.callTool('vibe_v4_set', { leanArgs: 'a, b ,,c' })
|
|
362
|
-
assert(/leanArgs=a,b,c/.test(la.params), 'leanArgs accepts a comma-separated string and drops blanks (got ' + (/leanArgs=[^,]*,[^,]*,[^,]*/.exec(la.params) || [])[0] + ')')
|
|
363
|
-
await B.callTool('vibe_v4_set', { leanTimeoutMs: 5 })
|
|
364
|
-
assert(/leanTimeoutMs=5/.test((await B.callTool('vibe_v4_status', {})).params), 'a legitimate leanTimeoutMs is preserved')
|
|
365
|
-
await B.callTool('vibe_v4_set', { leanTimeoutMs: 0 })
|
|
366
|
-
assert(/leanTimeoutMs=120000/.test((await B.callTool('vibe_v4_status', {})).params), 'leanTimeoutMs:0 also falls back to the default')
|
|
367
|
-
// the switch must be visible to the very NEXT prompt, not to some frozen brief
|
|
368
|
-
await B.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
369
|
-
assert(/顺手形式化/.test(await B.prompts('normal', 'r-1')), '★ switching the mode at runtime changes the NEXT prompt immediately')
|
|
370
|
-
await B.callTool('vibe_v4_set', { formalVerify: 'off' })
|
|
371
|
-
assert(!/顺手形式化/.test(await B.prompts('normal', 'r-1')), 'switching back to off removes it again')
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ---------- 3. 'encourage' injection into BOTH prompts ----------
|
|
375
|
-
section("3 'encourage' injects the Lean section into the verification AND the work prompt")
|
|
376
|
-
const C = await establish()
|
|
377
|
-
await C.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
378
|
-
{
|
|
379
|
-
// The ordinary work round. Read the builder AND drive a real wake, so the assertion cannot pass
|
|
380
|
-
// on a builder that is never actually used to address a resident.
|
|
381
|
-
const built = await C.prompts('normal', 'r-2')
|
|
382
|
-
assert(/【顺手形式化(鼓励)】/.test(built), '★ the ordinary work round tells residents to formalize reusable objects as they go')
|
|
383
|
-
assert(/vibe_v4_lean_archive kind='def'/.test(built), 'the work round points at the archive tool for reusable definitions')
|
|
384
|
-
assert(/vibe_v4_lean_lib 查重/.test(built), 'the work round tells them to de-duplicate against the reuse library first')
|
|
385
|
-
assert(/"formal":/.test(built), 'the work reply contract documents the formal field')
|
|
386
|
-
assert(/Resident researcher r-2/.test(built), 'the work prompt is addressed to the resident it is built for')
|
|
387
|
-
const hb = await C.prompts('heartbeat', 'r-2')
|
|
388
|
-
assert(/【顺手形式化(鼓励)】/.test(hb), 'the CHECKPOINT/heartbeat round carries it too')
|
|
389
|
-
const w = await workWake(C, 'r-2')
|
|
390
|
-
assert(/【顺手形式化(鼓励)】/.test(promptOf(w)), '★ a REAL delivered work wake contains the line (the builder is the one in use)')
|
|
391
|
-
C.fireEnd(w.childId, { summary: '继续推进。', solved: false, contextPct: 20 })
|
|
392
|
-
await sleep(30)
|
|
393
|
-
const core = await C.prompts('coreRules')
|
|
394
|
-
assert(/\[核心规则重申\]/.test(core) && /【顺手形式化(鼓励)】/.test(core), 'the post-compact core-rules recap also carries the formalization line')
|
|
395
|
-
}
|
|
396
|
-
{
|
|
397
|
-
await proposeAndVote(C, 'p-enc', {
|
|
398
|
-
card: { id: 'p-enc', title: '鼓励注入', statement: '鼓励模式下的忠实性审查', prob: 0.5, value: 0.6, motivation: 'm' },
|
|
399
|
-
proposal: { summary: '提议验证 p-enc。', solved: false, propose_verify: 'p-enc', contextPct: 20 },
|
|
400
|
-
verdict: 0.5, plan: () => 0.5,
|
|
401
|
-
})
|
|
402
|
-
const prompt = await C.prompts('verify', 'r-1', { target: 'p-enc', stage: 'independent' })
|
|
403
|
-
assert(/【Lean 形式化验证(鼓励模式)】/.test(prompt), 'the voting prompt explains the Lean mode')
|
|
404
|
-
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(prompt), 'the voting prompt states that a passing Lean run shrinks the question to fidelity')
|
|
405
|
-
assert(/实现难度/.test(prompt), 'the voting prompt asks for the implementation-difficulty judgement')
|
|
406
|
-
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(prompt), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
407
|
-
assert(/vibe_v4_lean_run/.test(prompt) && /vibe_v4_lean_archive/.test(prompt) && /vibe_v4_lean_lib/.test(prompt), 'the voting prompt names all three tools')
|
|
408
|
-
assert(/"formal":/.test(prompt), '★ the voting reply contract documents the formal field')
|
|
409
|
-
assert(/Resident r-1/.test(prompt), 'the voting prompt is addressed to the voter it is built for')
|
|
410
|
-
assert(!/强制模式/.test(prompt), "'encourage' does not use the mandatory wording")
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// ---------- 4. the run tool ----------
|
|
414
|
-
section('4 lean_run executes through the subprocess service and reports honestly')
|
|
415
|
-
const D = await establish()
|
|
416
|
-
await D.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
417
|
-
mkdirSync(join(D.projectRoot, 'Formal'), { recursive: true })
|
|
418
|
-
writeFileSync(join(D.projectRoot, 'Formal', 'good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
419
|
-
writeFileSync(join(D.projectRoot, 'Formal', 'bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
|
|
420
|
-
const r1 = D.resAgent(D.childOf('r-1'))
|
|
421
|
-
const runGood = await D.callTool('vibe_v4_lean_run', { file: 'Formal/good.lean' }, r1)
|
|
422
|
-
assert(runGood.ok === true && runGood.exitCode === 0, 'a file with no sorry runs green (' + JSON.stringify({ ok: runGood.ok, exitCode: runGood.exitCode }) + ')')
|
|
423
|
-
assert(typeof runGood.ms === 'number' && typeof runGood.command === 'string' && runGood.file === 'Formal/good.lean', 'the result carries {ms, command, file} as the spec requires')
|
|
424
|
-
assert(leanRuns.length > 0 && String(leanRuns[leanRuns.length - 1].cwd).replace(/\\/g, '/') === D.projectRoot.replace(/\\/g, '/'), 'the toolchain runs with the PROJECT root as cwd')
|
|
425
|
-
{
|
|
426
|
-
const last = leanRuns[leanRuns.length - 1]
|
|
427
|
-
assert(String(last.argv[0]).indexOf('lean') !== -1 && /\.lean$/.test(String(last.argv[last.argv.length - 1])), 'argv is [exe, ...leanArgs, absolute file]')
|
|
428
|
-
assert(last.stdio && last.stdio.stdin === 'ignore', "stdin is ignored (the spec's stdio shape)")
|
|
429
|
-
}
|
|
430
|
-
const runBad = await D.callTool('vibe_v4_lean_run', { file: 'Formal/bad.lean' }, r1)
|
|
431
|
-
assert(runBad.ok === false && runBad.exitCode === 1, 'a file that still uses sorry reports a red run')
|
|
432
|
-
assert(/sorry/.test(runBad.stderr), 'the compiler output is returned verbatim (' + JSON.stringify(runBad.stderr).slice(0, 60) + ')')
|
|
433
|
-
assert(runBad.code === 'LEAN_FAILED', 'a red run is typed LEAN_FAILED')
|
|
434
|
-
const runMissing = await D.callTool('vibe_v4_lean_run', { file: 'Formal/nope.lean' }, r1)
|
|
435
|
-
assert(runMissing.ok === false && runMissing.code === 'V4_NOT_FOUND', 'a missing file is refused with a typed code')
|
|
436
|
-
const runNoFile = await D.callTool('vibe_v4_lean_run', {}, r1)
|
|
437
|
-
assert(runNoFile.ok === false && runNoFile.code === 'V4_INVALID_ARGUMENT', 'a run without a file is refused')
|
|
438
|
-
// The guard's boundary is the VibeMath ROOT, not the project: the global reuse library
|
|
439
|
-
// deliberately lives at <VibeMath>/Formal/{Lib,Proved}, so climbing out of the project but staying
|
|
440
|
-
// inside VibeMath must remain legal (it just fails as a missing file).
|
|
441
|
-
const runOutside = await D.callTool('vibe_v4_lean_run', { file: '../../Formal/Lib/x.lean' }, r1)
|
|
442
|
-
assert(runOutside.code === 'V4_NOT_FOUND', 'climbing out of the project but staying inside VibeMath is allowed (the global library lives there)')
|
|
443
|
-
const esc1 = await D.callTool('vibe_v4_lean_run', { file: '../../../../../etc/evil.lean' }, r1)
|
|
444
|
-
assert(esc1.ok === false && esc1.code === 'V4_INVALID_ARGUMENT', '★ a traversal that climbs ABOVE the VibeMath root is refused')
|
|
445
|
-
const esc2 = await D.callTool('vibe_v4_lean_run', { file: 'Formal/../../../../../../evil.lean' }, r1)
|
|
446
|
-
assert(esc2.ok === false && esc2.code === 'V4_INVALID_ARGUMENT', 'a deeper traversal is refused too')
|
|
447
|
-
const esc3 = await D.callTool('vibe_v4_lean_run', { file: '/etc/evil.lean' }, r1)
|
|
448
|
-
assert(esc3.ok === false && esc3.code === 'V4_INVALID_ARGUMENT', 'an unrelated absolute path is refused')
|
|
449
|
-
const esc4 = await D.callTool('vibe_v4_lean_run', { file: 'C:\\Windows\\evil.lean' }, r1)
|
|
450
|
-
assert(esc4.ok === false && esc4.code === 'V4_INVALID_ARGUMENT', 'an unrelated absolute path with a drive letter is refused')
|
|
451
|
-
const notLean = await D.callTool('vibe_v4_lean_run', { file: 'Formal/good.txt' }, r1)
|
|
452
|
-
assert(notLean.ok === false && notLean.code === 'V4_INVALID_ARGUMENT', 'only .lean files can be executed')
|
|
453
|
-
toolchainAvailable = false
|
|
454
|
-
const noTc = await D.callTool('vibe_v4_lean_run', { file: 'Formal/good.lean' }, r1)
|
|
455
|
-
assert(noTc.ok === false && noTc.code === 'LEAN_NOT_FOUND', '★ a missing toolchain returns LEAN_NOT_FOUND instead of crashing')
|
|
456
|
-
assert(/仍可把形式化代码写下来归档/.test(noTc.message || ''), 'the failure explains the graceful degradation')
|
|
457
|
-
assert((await D.callTool('vibe_v4_status', {})).ok === true, 'the group did NOT crash on the missing toolchain (status still answers)')
|
|
458
|
-
toolchainAvailable = true
|
|
459
|
-
// No `subprocess` service at all (docs §7): the run must degrade to a typed NO_SUBPROCESS result —
|
|
460
|
-
// never a thrown error into the scheduler — and an object it was asked to record must stay
|
|
461
|
-
// `attempted` (an un-runnable host must not mint a `passed` record).
|
|
462
|
-
{
|
|
463
|
-
subprocessAvailable = false
|
|
464
|
-
const noSub = await D.callTool('vibe_v4_lean_run', { file: 'Formal/good.lean', target: 'p-nosub' }, r1)
|
|
465
|
-
assert(noSub.ok === false && noSub.code === 'NO_SUBPROCESS', '★ a host with no subprocess service returns NO_SUBPROCESS instead of crashing (got ' + noSub.code + ')')
|
|
466
|
-
assert(/no subprocess service/.test(String(noSub.message || '')), 'the failure is readable (it explains that Lean cannot be executed here)')
|
|
467
|
-
const stNoSub = await D.callTool('vibe_v4_status', {})
|
|
468
|
-
assert(stNoSub.ok === true, 'the group did NOT crash on the missing service (status still answers)')
|
|
469
|
-
const recNoSub = stNoSub.formal.objects.find((o) => o.target === 'p-nosub')
|
|
470
|
-
assert(recNoSub && recNoSub.status === 'attempted' && !recNoSub.proof, '★ the object record stays honest: attempted (never passed) when nothing could be executed')
|
|
471
|
-
subprocessAvailable = true
|
|
472
|
-
}
|
|
473
|
-
// A run that never finishes: the framework must ACTIVELY terminate it (docs §7) and still report a
|
|
474
|
-
// readable LEAN_TIMEOUT — relying on the host's own graceMs alone leaves the Lean process running.
|
|
475
|
-
{
|
|
476
|
-
writeFileSync(join(D.projectRoot, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1 := rfl\n', 'utf8')
|
|
477
|
-
const t0 = Date.now()
|
|
478
|
-
const runHang = await D.callTool('vibe_v4_lean_run', { file: 'Formal/hang.lean', timeout_ms: 1000 }, r1)
|
|
479
|
-
const waited = Date.now() - t0
|
|
480
|
-
assert(runHang.ok === false && runHang.code === 'LEAN_TIMEOUT' && runHang.timedOut === true, '★ a run that never finishes is reported as LEAN_TIMEOUT (got ' + runHang.code + ')')
|
|
481
|
-
assert(terminated.some(f => /hang\.lean$/.test(String(f))), '★ the timeout path really calls handle.terminate() (docs §7) instead of leaving the Lean process running')
|
|
482
|
-
assert(waited >= 900 && waited < 15000, 'the timeout waited for the cap before terminating (' + waited + 'ms)')
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// ---------- 5. a passing proof flips the review subject to fidelity ----------
|
|
486
|
-
section('5 a passing proof flips the review subject to fidelity')
|
|
487
|
-
const arc = await D.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-proof', content: 'theorem p_proof : 3 * 1 ^ 2 - 2 = (1:Nat) ^ 2 := by decide\n' }, r1)
|
|
488
|
-
assert(arc.ok === true && arc.passed === true, 'the proof is archived and passes (' + JSON.stringify({ ok: arc.ok, passed: arc.passed }) + ')')
|
|
489
|
-
assert(arc.file === 'Formal/p-proof.lean', 'the working file is Formal/<target>.lean')
|
|
490
|
-
assert(arc.proof === 'Verified/Lean/p-proof.lean', 'the archived proof path is Verified/Lean/<target>.lean')
|
|
491
|
-
assert(existsSync(join(D.projectRoot, 'Formal', 'p-proof.lean')), 'the working file exists on disk')
|
|
492
|
-
assert(existsSync(join(D.projectRoot, 'Verified', 'Lean', 'p-proof.lean')), '★ the proof is archived under Verified/Lean/ as the proof of that object')
|
|
493
|
-
const stD = await D.callTool('vibe_v4_status', {})
|
|
494
|
-
assert(stD.formal.passed.indexOf('p-proof') !== -1, 'status reports the object as Lean-passed')
|
|
495
|
-
const idxD = readIf(join(D.projectRoot, 'Formal', 'Index.md'))
|
|
496
|
-
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')
|
|
497
|
-
// A later run recorded against an ALREADY-passed object records the run but must never strip
|
|
498
|
-
// `passed`/`proof`: doing so would silently remove the fidelity branch from the next voting prompt
|
|
499
|
-
// AND (in `require`) re-close the gate on an object that already has a green archived proof.
|
|
500
|
-
{
|
|
501
|
-
writeFileSync(join(D.projectRoot, 'Formal', 'p-proof.lean'), 'theorem p_proof : 1 = 2 := by sorry\n', 'utf8')
|
|
502
|
-
const rerun = await D.callTool('vibe_v4_lean_run', { file: 'Formal/p-proof.lean', target: 'p-proof' }, r1)
|
|
503
|
-
assert(rerun.ok === false, 'precondition: the recorded re-run is red')
|
|
504
|
-
const rec = (await D.callTool('vibe_v4_status', {})).formal.objects.find(o => o.target === 'p-proof')
|
|
505
|
-
assert(rec && rec.status === 'passed' && rec.proof === 'Verified/Lean/p-proof.lean',
|
|
506
|
-
'★ a RED re-run never downgrades a `passed` record (docs §31.6: only an explicit re-archive decides) — the object keeps its fidelity prompt and its require-mode gate')
|
|
507
|
-
writeFileSync(join(D.projectRoot, 'Formal', 'p-proof.lean'), 'theorem p_proof : 3 * 1 ^ 2 - 2 = (1:Nat) ^ 2 := by decide\n', 'utf8')
|
|
508
|
-
}
|
|
509
|
-
// a RED proof must NOT mint a Verified/Lean/ copy or mark the object passed
|
|
510
|
-
const arcRed = await D.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-red', content: 'theorem p_red : 1 = 2 := by sorry\n' }, r1)
|
|
511
|
-
assert(arcRed.ok === true && arcRed.passed === false && arcRed.status === 'attempted', 'a red proof is archived as attempted, not passed')
|
|
512
|
-
assert(!existsSync(join(D.projectRoot, 'Verified', 'Lean', 'p-red.lean')), '★ a red run does NOT mint a Verified/Lean proof copy')
|
|
513
|
-
// durable state: the record survives a resume (v4 has no session projection)
|
|
514
|
-
{
|
|
515
|
-
const raw = readIf(join(D.projectRoot, 'State', 'formal.json'))
|
|
516
|
-
assert(/"p-proof"/.test(raw) && /"passed"/.test(raw), "★ the formal record is persisted in v4's own durable State/formal.json")
|
|
517
|
-
const before = D.spawns.length
|
|
518
|
-
await D.callTool('vibe_v4_abort', {})
|
|
519
|
-
const res = await D.callTool('vibe_v4_resume', {})
|
|
520
|
-
assert(res.ok === true && D.spawns.length > before, 'abort → resume re-spawns residents')
|
|
521
|
-
const stR = await D.callTool('vibe_v4_status', {})
|
|
522
|
-
assert(stR.formal.passed.indexOf('p-proof') !== -1, '★ the Lean-passed record SURVIVES resume (durable state, not an in-memory projection)')
|
|
523
|
-
for (const sp of D.spawns.slice(before)) { D.fireEnd(sp.childId, { summary: '恢复见解。', solved: false, contextPct: 10 }); await sleep(25) }
|
|
524
|
-
await D.callTool('vibe_v4_set', { activityTimeoutMs: 600000 })
|
|
525
|
-
D.resetCursor()
|
|
526
|
-
}
|
|
527
|
-
// now the voting prompt must ASK FOR FIDELITY, not for a re-derivation
|
|
528
|
-
{
|
|
529
|
-
await proposeAndVote(D, 'p-proof', {
|
|
530
|
-
card: { id: 'p-proof', title: '忠实性', statement: '3N²−2=b² 在 N=1 时成立', prob: 0.9, value: 0.6, motivation: 'm' },
|
|
531
|
-
proposal: { summary: '提议验证 p-proof。', solved: false, propose_verify: 'p-proof', contextPct: 20 },
|
|
532
|
-
verdict: 0.5, plan: () => 0.5,
|
|
533
|
-
})
|
|
534
|
-
const prompt = await D.prompts('verify', 'r-1', { target: 'p-proof', stage: 'independent' })
|
|
535
|
-
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(prompt), 'the voting prompt announces the passing proof')
|
|
536
|
-
assert(/你不需要重新检查推导/.test(prompt), '★ it tells voters NOT to re-derive')
|
|
537
|
-
assert(/忠实性审查/.test(prompt), '★ it tells voters the review subject is now fidelity')
|
|
538
|
-
assert(/定义 \/ 对象 \/ 条件 \/ 假设 \/ 结论是否与命题原文\*\*完全一致\*\*/.test(prompt), 'it enumerates exactly what fidelity means')
|
|
539
|
-
assert(/一致 → verdict = 1/.test(prompt), "the fidelity instruction uses v4's REAL reply field name (`verdict`)")
|
|
540
|
-
assert(/发现任何偏差,不要投 0/.test(prompt), '★ it forbids expressing a fidelity deviation as "0 / false"')
|
|
541
|
-
assert(/偏差只说明\*\*形式化不合格\*\*,不代表命题为假/.test(prompt), '★ it states WHY: a defect is a failed formalization, not a refutation')
|
|
542
|
-
assert(/formal:\{decision:'defect', note:'<具体偏差>'\}/.test(prompt), '★ it hands the voter the exact `defect` reply contract')
|
|
543
|
-
assert(/降级为 attempted、删除归档证明、写入形式化待办/.test(prompt), 'it says what the framework WILL do with a defect (downgrade + delete + TODO)')
|
|
544
|
-
assert(/只有当你\*\*独立于这份 Lean 代码\*\*也能确定命题为假时,才投 0/.test(prompt), 'only an INDEPENDENT refutation may be expressed as 0')
|
|
545
|
-
assert(!/偏离 → 0/.test(prompt), '★ the old "a deviation ⇒ 0" wording is gone (it would fabricate a negative conclusion)')
|
|
546
|
-
assert(/Verified\/Lean\/p-proof\.lean/.test(prompt), 'it points at the archived proof')
|
|
547
|
-
// the SAME object's work round must not inherit the fidelity framing (that is a voting
|
|
548
|
-
// instruction), but it does carry the standing formalization line.
|
|
549
|
-
const work = await D.prompts('normal', 'r-1')
|
|
550
|
-
assert(!/忠实性审查/.test(work), 'the fidelity instruction is confined to the voting prompt')
|
|
551
|
-
assert(/【顺手形式化(鼓励)】/.test(work), 'the work round carries the standing formalization line instead')
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// ---------- 6. the reusable cross-project library ----------
|
|
555
|
-
section('6 reusable definitions and lemmas go to the GLOBAL library')
|
|
556
|
-
const libPath = join(D.vibeRoot, 'Formal', 'Lib')
|
|
557
|
-
const provedPath = join(D.vibeRoot, 'Formal', 'Proved')
|
|
558
|
-
const defRes = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'ZMod5', content: 'def ZMod5 := Fin 5\n' }, r1)
|
|
559
|
-
assert(defRes.ok === true && defRes.file === 'Formal/Lib/ZMod5.lean', 'a reusable definition is archived to the global lib (' + defRes.file + ')')
|
|
560
|
-
assert(existsSync(join(libPath, 'ZMod5.lean')), '★ the definition exists under VibeMath/Formal/Lib/ (cross-project, NOT inside the project)')
|
|
561
|
-
assert(!existsSync(join(D.projectRoot, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the project tree')
|
|
562
|
-
const lemRes = await D.callTool('vibe_v4_lean_archive', { kind: 'lemma', name: 'sq_odd', content: 'theorem sq_odd (n : Nat) : Odd (n*n) → Odd n := by omega\n' }, r1)
|
|
563
|
-
assert(lemRes.ok === true && lemRes.file === 'Formal/Proved/sq_odd.lean', 'a lemma is archived to Proved/')
|
|
564
|
-
assert(existsSync(join(provedPath, 'sq_odd.lean')), 'the lemma exists under VibeMath/Formal/Proved/')
|
|
565
|
-
assert(/ZMod5/.test(readIf(join(libPath, 'Index.md'))), 'Lib/Index.md lists the new definition')
|
|
566
|
-
assert(/sq_odd/.test(readIf(join(provedPath, 'Index.md'))), 'Proved/Index.md lists the new lemma')
|
|
567
|
-
// `from` copies an existing workspace .lean file instead of inline content
|
|
568
|
-
writeFileSync(join(D.projectRoot, 'Formal', 'src.lean'), 'def copied := 7\n', 'utf8')
|
|
569
|
-
const fromRes = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'Copied', from: 'Formal/src.lean' }, r1)
|
|
570
|
-
assert(fromRes.ok === true && readIf(join(libPath, 'Copied.lean')) === 'def copied := 7\n', "kind='def' accepts from=<existing .lean file>")
|
|
571
|
-
const fromEsc = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'Evil', from: '../../../../etc/passwd' }, r1)
|
|
572
|
-
assert(fromEsc.ok === false && fromEsc.code === 'V4_INVALID_ARGUMENT', "★ kind='def' refuses a `from` outside the VibeMath root")
|
|
573
|
-
const libList = await D.callTool('vibe_v4_lean_lib', {}, r1)
|
|
574
|
-
assert(libList.ok === true && libList.counts.lib >= 2 && libList.counts.proved >= 1, 'lean_lib reports the reuse library sizes (' + JSON.stringify(libList.counts) + ')')
|
|
575
|
-
assert(libList.objects.some(o => o.target === 'p-proof' && o.status === 'passed'), 'lean_lib lists per-object formal status')
|
|
576
|
-
assert(/复用优先/.test(libList.hint || ''), 'lean_lib tells agents to reuse before redefining')
|
|
577
|
-
assert(libList.paths && /Formal\/Lib/.test(libList.paths.lib), 'lean_lib reports the global library paths')
|
|
578
|
-
const noName = await D.callTool('vibe_v4_lean_archive', { kind: 'def', content: 'def x := 1\n' }, r1)
|
|
579
|
-
assert(noName.ok === false && noName.code === 'V4_INVALID_ARGUMENT', 'archiving a definition without a name is refused')
|
|
580
|
-
const noBody = await D.callTool('vibe_v4_lean_archive', { kind: 'lemma', name: 'empty' }, r1)
|
|
581
|
-
assert(noBody.ok === false && noBody.code === 'V4_INVALID_ARGUMENT', 'archiving without content or from is refused')
|
|
582
|
-
const badKind = await D.callTool('vibe_v4_lean_archive', { kind: 'nonsense' }, r1)
|
|
583
|
-
assert(badKind.ok === false && badKind.code === 'V4_INVALID_ARGUMENT', 'an unknown archive kind is refused')
|
|
584
|
-
const libNoRefresh = await D.callTool('vibe_v4_lean_lib', { refresh: false }, r1)
|
|
585
|
-
assert(libNoRefresh.ok === true && libNoRefresh.rebuilt === false && libNoRefresh.counts.lib === null, 'refresh:false reads without rebuilding')
|
|
586
|
-
|
|
587
|
-
// ---------- 7. blocked needs a reason ----------
|
|
588
|
-
section('7 a "blocker" record must be explicit and reasoned')
|
|
589
|
-
const blkNoNote = await D.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-blk' }, r1)
|
|
590
|
-
assert(blkNoNote.ok === false && blkNoNote.code === 'V4_INVALID_ARGUMENT', '★ blocked without a note is refused')
|
|
591
|
-
const blkBlank = await D.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-blk', note: ' ' }, r1)
|
|
592
|
-
assert(blkBlank.ok === false && blkBlank.code === 'V4_INVALID_ARGUMENT', 'a whitespace-only note is refused too')
|
|
593
|
-
const blk = await D.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-blk', note: '需要外层解析数论框架,本轮工作量不可接受' }, r1)
|
|
594
|
-
assert(blk.ok === true && blk.status === 'blocked', 'a reasoned blocker is recorded')
|
|
595
|
-
const stBlk = await D.callTool('vibe_v4_status', {})
|
|
596
|
-
assert(stBlk.formal.blocked.indexOf('p-blk') !== -1, 'status lists the blocked object')
|
|
597
|
-
assert(/需要外层解析数论框架/.test(readIf(join(D.projectRoot, 'Formal', 'Index.md'))), 'the blocker reason is written into the index')
|
|
598
|
-
|
|
599
|
-
// ---------- 8. the per-round `formal` reply channel ----------
|
|
600
|
-
section('8 the per-round `formal` reply judgement is recorded')
|
|
601
|
-
{
|
|
602
|
-
const w1 = await workWake(D, 'r-1')
|
|
603
|
-
D.fireEnd(w1.childId, { summary: '难度太高,本轮不做形式化。', formal: { target: 'p-reply', decision: 'blocked', note: '需要大量未形式化的实分析前置知识' }, contextPct: 20 })
|
|
604
|
-
await sleep(60)
|
|
605
|
-
const st = await D.callTool('vibe_v4_status', {})
|
|
606
|
-
assert(st.formal.blocked.indexOf('p-reply') !== -1, '★ a `formal.decision=blocked` reply is recorded as a blocker')
|
|
607
|
-
assert(/实分析前置知识/.test(readIf(join(D.projectRoot, 'Formal', 'Index.md'))), 'and its reason reaches the index')
|
|
608
|
-
}
|
|
609
|
-
{
|
|
610
|
-
const w2 = await workWake(D, 'r-2')
|
|
611
|
-
D.fireEnd(w2.childId, { summary: '我写了形式化草稿。', formal: { target: 'p-reply2', decision: 'used', file: 'Formal/p-reply2.lean' }, contextPct: 20 })
|
|
612
|
-
await sleep(60)
|
|
613
|
-
const st = await D.callTool('vibe_v4_status', {})
|
|
614
|
-
assert(st.formal.objects.some(o => o.target === 'p-reply2' && o.status === 'attempted'),
|
|
615
|
-
'a `formal.decision=used` reply records the object as ATTEMPTED with its file (no green run yet)')
|
|
616
|
-
}
|
|
617
|
-
{
|
|
618
|
-
const w3 = await workWake(D, 'r-1')
|
|
619
|
-
D.fireEnd(w3.childId, { summary: '不做。', formal: { target: 'p-reply3', decision: 'blocked' }, contextPct: 20 })
|
|
620
|
-
await sleep(60)
|
|
621
|
-
const st = await D.callTool('vibe_v4_status', {})
|
|
622
|
-
assert(!st.formal.objects.some(o => o.target === 'p-reply3'), 'a blocker with no note creates NO record')
|
|
623
|
-
assert(st.ok === true, 'the refusal did not crash the run')
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// ---------- 9. the 'require' gate ----------
|
|
627
|
-
section("9 'require' withholds a verdict until the formal record exists")
|
|
628
|
-
const E = await establish()
|
|
629
|
-
await E.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
630
|
-
{
|
|
631
|
-
await proposeAndVote(E, 'p-gate', {
|
|
632
|
-
card: { id: 'p-gate', title: '门禁', statement: '必须形式化的命题', prob: 0.8, value: 0.6, motivation: 'm' },
|
|
633
|
-
proposal: { summary: '提议验证 p-gate。', solved: false, propose_verify: 'p-gate', contextPct: 20 },
|
|
634
|
-
verdict: 1,
|
|
635
|
-
})
|
|
636
|
-
const prompt = await E.prompts('verify', 'r-1', { target: 'p-gate', stage: 'independent' })
|
|
637
|
-
assert(/【Lean 形式化验证(强制模式)】/.test(prompt), 'the voting prompt says 强制模式')
|
|
638
|
-
assert(/必须产出 Lean 形式化/.test(prompt), "'require' states the formalization is mandatory")
|
|
639
|
-
assert(/本次裁定不会生效/.test(prompt), 'the prompt warns that the verdict will not take effect without it')
|
|
640
|
-
assert(!/可以不做/.test(prompt), "'require' does NOT offer the encourage-mode opt-out")
|
|
641
|
-
}
|
|
642
|
-
const gated = await E.callTool('vibe_v4_status', {})
|
|
643
|
-
assert(gated.verifyInProgress === false && gated.pendingVerify === null, 'the verification actually RAN to a settled state (so the next assertion is falsifiable)')
|
|
644
|
-
assert(!existsSync(join(E.projectRoot, 'Verified', '命题', 'p-gate.md')), '★ a unanimous TRUE verdict did NOT promote the object to Verified/')
|
|
645
|
-
assert(gated.formal.todo.indexOf('p-gate') !== -1, '★ it is recorded as 未定论 (a formalization TODO entry)')
|
|
646
|
-
assert(gated.formal.objects.some(o => o.target === 'p-gate' && o.status === 'none'), 'the object keeps a `none` formal record (never silently marked passed)')
|
|
647
|
-
assert(!/已验证·真/.test(readIf(join(E.projectRoot, 'Propos', 'r-1', 'p-gate.md'))), 'the source card was NOT rewritten to 已验证·真 (the conclusion was withheld, not taken)')
|
|
648
|
-
const todoE = readIf(join(E.projectRoot, 'Formal', 'TODO.md'))
|
|
649
|
-
assert(existsSync(join(E.projectRoot, 'Formal', 'TODO.md')), 'Formal/TODO.md was created')
|
|
650
|
-
assert(/p-gate/.test(todoE) && /formal-required/.test(todoE), '★ the object is on the formalization TODO with the machine-readable reason')
|
|
651
|
-
assert(!/已验证·真/.test(readIf(join(E.projectRoot, 'Shared', 'debates', 'p-gate.md'))), 'the debate record does NOT claim a 真 conclusion')
|
|
652
|
-
assert(/formal-required/.test(readIf(join(E.projectRoot, 'Formal', 'Index.md'))), 'the index carries the withheld verdict too')
|
|
653
|
-
// now formalize it and re-verify: the gate must open
|
|
654
|
-
const proofNow = await E.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-gate', content: 'theorem p_gate : 2 + 2 = 4 := by decide\n' }, E.resAgent(E.childOf('r-1')))
|
|
655
|
-
assert(proofNow.ok === true && proofNow.passed === true, 'the object is now Lean-passed')
|
|
656
|
-
{
|
|
657
|
-
await proposeAndVote(E, 'p-gate', {
|
|
658
|
-
card: { id: 'p-gate', title: '门禁', statement: '必须形式化的命题', prob: 0.8, value: 0.6, motivation: 'm' },
|
|
659
|
-
proposal: { summary: '已形式化,重新提议验证 p-gate。', solved: false, propose_verify: 'p-gate', contextPct: 20 },
|
|
660
|
-
verdict: 1,
|
|
661
|
-
})
|
|
662
|
-
const prompt = await E.prompts('verify', 'r-1', { target: 'p-gate', stage: 'independent' })
|
|
663
|
-
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(prompt), 'the re-verify prompt now points at the passing proof')
|
|
664
|
-
}
|
|
665
|
-
const gated2 = await E.callTool('vibe_v4_status', {})
|
|
666
|
-
assert(existsSync(join(E.projectRoot, 'Verified', '命题', 'p-gate.md')), '★ with a passing Lean artifact the same verdict DOES promote it')
|
|
667
|
-
const card = readIf(join(E.projectRoot, 'Verified', '命题', 'p-gate.md'))
|
|
668
|
-
assert(/- 形式化: Lean 通过/.test(card), '★ the Verified card records how strong the result is (Lean 通过)')
|
|
669
|
-
assert(/Verified\/Lean\/p-gate\.lean/.test(card), 'the card points at the archived proof')
|
|
670
|
-
assert(gated2.formal.todo.indexOf('p-gate') === -1, 'the satisfied TODO entry was cleared')
|
|
671
|
-
// the blocker escape hatch must also open the gate
|
|
672
|
-
await E.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-blocked-ok', note: '命题涉及未形式化的分析学,本轮不做' }, E.resAgent(E.childOf('r-1')))
|
|
673
|
-
await proposeAndVote(E, 'p-blocked-ok', {
|
|
674
|
-
card: { id: 'p-blocked-ok', title: '阻塞放行', statement: '记录阻塞后可定论', prob: 0.8, value: 0.6, motivation: 'm' },
|
|
675
|
-
proposal: { summary: '提议验证 p-blocked-ok。', solved: false, propose_verify: 'p-blocked-ok', contextPct: 20 },
|
|
676
|
-
verdict: 1,
|
|
677
|
-
})
|
|
678
|
-
assert(existsSync(join(E.projectRoot, 'Verified', '命题', 'p-blocked-ok.md')), '★ an explicit reasoned blocker also lets the verdict through (decide by difficulty, but decide out loud)')
|
|
679
|
-
assert(/- 形式化: 阻塞(/.test(readIf(join(E.projectRoot, 'Verified', '命题', 'p-blocked-ok.md'))), 'the card records the blocker')
|
|
680
|
-
assert((await E.callTool('vibe_v4_status', {})).formal.blocked.indexOf('p-blocked-ok') !== -1, 'status lists it as blocked')
|
|
681
|
-
// a `require`-mode FALSE verdict must be gated exactly the same way (the gate sits on the choke
|
|
682
|
-
// point, not on the 真 branch): assert the tally was recorded BEFORE asserting it was withheld.
|
|
683
|
-
await proposeAndVote(E, 'p-gate-false', {
|
|
684
|
-
card: { id: 'p-gate-false', title: '假也要门禁', statement: '一个会被一致判假的命题', prob: 0.1, value: 0.6, motivation: 'm' },
|
|
685
|
-
proposal: { summary: '提议验证 p-gate-false。', solved: false, propose_verify: 'p-gate-false', contextPct: 20 },
|
|
686
|
-
verdict: 0,
|
|
687
|
-
})
|
|
688
|
-
const gatedF = await E.callTool('vibe_v4_status', {})
|
|
689
|
-
assert(!existsSync(join(E.projectRoot, 'Verified', '命题', 'p-gate-false.md')), '★ a unanimous FALSE verdict is gated too')
|
|
690
|
-
assert(gatedF.formal.todo.indexOf('p-gate-false') !== -1, 'the false verdict is recorded as 未定论 as well')
|
|
691
|
-
assert(!/已验证·假/.test(readIf(join(E.projectRoot, 'Propos', 'r-1', 'p-gate-false.md'))), 'the source card was not rewritten to 已验证·假')
|
|
692
|
-
|
|
693
|
-
// ---------- 10. 'encourage' does NOT gate ----------
|
|
694
|
-
section("10 'encourage' has no gate (the verdict still closes)")
|
|
695
|
-
const F = await establish()
|
|
696
|
-
await F.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
697
|
-
await proposeAndVote(F, 'p-enc-ok', {
|
|
698
|
-
card: { id: 'p-enc-ok', title: '鼓励不门禁', statement: '鼓励模式下无形式化产物也能定论', prob: 0.8, value: 0.6, motivation: 'm' },
|
|
699
|
-
proposal: { summary: '提议验证 p-enc-ok。', solved: false, propose_verify: 'p-enc-ok', contextPct: 20 },
|
|
700
|
-
verdict: 1,
|
|
701
|
-
})
|
|
702
|
-
assert(existsSync(join(F.projectRoot, 'Verified', '命题', 'p-enc-ok.md')), "★ 'encourage' imposes NO gate: the verdict closes with no Lean artifact")
|
|
703
|
-
assert(/- 形式化: 未尝试/.test(readIf(join(F.projectRoot, 'Verified', '命题', 'p-enc-ok.md'))), 'the card still states the object was never formalized')
|
|
704
|
-
|
|
705
|
-
// ---------- 11. reporting ----------
|
|
706
|
-
section('11 the host can audit formal strength')
|
|
707
|
-
const repF = await F.callTool('vibe_v4_formal_report', {}, F.ROOT)
|
|
708
|
-
assert(/## Lean 形式化/.test(repF.formalReport), 'the formal report has a Lean section')
|
|
709
|
-
assert(/模式:encourage/.test(repF.formalReport), 'it states the mode')
|
|
710
|
-
const repOff = await A.callTool('vibe_v4_formal_report', {}, A.ROOT)
|
|
711
|
-
assert(/未启用/.test(repOff.formalReport), 'in off mode the report says the feature is not enabled')
|
|
712
|
-
const repE = await E.callTool('vibe_v4_report', {}, E.ROOT)
|
|
713
|
-
assert(repE.formal && repE.formal.passed.indexOf('p-gate') !== -1, 'the structured report exposes the Lean-passed objects')
|
|
714
|
-
assert(repE.formal.blocked.indexOf('p-blocked-ok') !== -1, 'and the blocked objects')
|
|
715
|
-
const reportE = await E.callTool('vibe_v4_formal_report', {}, E.ROOT)
|
|
716
|
-
assert(/已通过:.*p-gate/.test(reportE.formalReport), 'the human report lists Lean-passed objects')
|
|
717
|
-
assert(/已记录阻塞:.*p-blocked-ok/.test(reportE.formalReport), 'the human report lists blocked objects')
|
|
718
|
-
assert(/形式化待办:.*p-gate-false/.test(reportE.formalReport), 'the human report lists the formalization TODO')
|
|
719
|
-
|
|
720
|
-
// ===============================================================
|
|
721
|
-
// 12. the fidelity rule + the prompt hard requirements (docs §6, §10 items 9/11)
|
|
722
|
-
// The v2 lesson (docs §10 item 8) is why every claim here is paired with a BEHAVIOURAL
|
|
723
|
-
// assertion in §13/§14: wording alone guards nothing.
|
|
724
|
-
// ===============================================================
|
|
725
|
-
section('12 the injected text states the fidelity rule and never abbreviates a tool name')
|
|
726
|
-
const G = await establish()
|
|
727
|
-
await G.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
728
|
-
{
|
|
729
|
-
const enc = await G.prompts('verify', 'r-1', { target: 'p-text', stage: 'independent' })
|
|
730
|
-
assert(/【Lean 形式化验证(鼓励模式)】/.test(enc), 'the encourage voting prompt keeps its header')
|
|
731
|
-
assert(/实现难度/.test(enc), 'it still asks for the implementation-difficulty judgement')
|
|
732
|
-
assert(/工具:vibe_v4_lean_run(执行)· vibe_v4_lean_archive(归档)· vibe_v4_lean_lib(查已有可复用库)/.test(enc), 'it names all three tools in FULL')
|
|
733
|
-
assert(/归档可复用定义\/引理前先跑通(vibe_v4_lean_archive run=true 或先 vibe_v4_lean_run);跑不通不要入库。/.test(enc), '★ a reusable definition/lemma must be RUN GREEN before it is archived')
|
|
734
|
-
assert(/宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并在回执的 note 里写明"宿主无 Lean 工具链"/.test(enc), '★ both unavailable-toolchain paths are written out (missing Lean OR no subprocess service) as explicit blocker reasons')
|
|
735
|
-
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(enc), 'a green Lean run still shrinks the open question to fidelity')
|
|
736
|
-
assert(/decision='blocked' 时必须写明 note/.test(enc), 'the encourage opt-out documents the mandatory note')
|
|
737
|
-
assert(!/偏离 → 0/.test(enc) && !/发现任何偏离/.test(enc), '★ no "deviation ⇒ 0" instruction anywhere in the encourage block')
|
|
738
|
-
await G.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
739
|
-
const req = await G.prompts('verify', 'r-1', { target: 'p-text', stage: 'independent' })
|
|
740
|
-
assert(/【Lean 形式化验证(强制模式)】/.test(req), 'the require voting prompt says 强制模式')
|
|
741
|
-
assert(/必须产出 Lean 形式化/.test(req) && /必须\*\*给出显式的阻塞原因/.test(req), "'require' states the formalization OR an explicit blocker is mandatory")
|
|
742
|
-
assert(/本次裁定不会生效/.test(req) && /进入「形式化待办」/.test(req), 'it warns the verdict is withheld as 未定论 (formal-required)')
|
|
743
|
-
assert(!/可以不做/.test(req), "'require' does NOT offer the encourage-mode opt-out")
|
|
744
|
-
assert(/归档可复用定义\/引理前先跑通/.test(req) && /宿主没有 Lean 工具链/.test(req), 'the run-before-archive and toolchain rules are in the require text as well')
|
|
745
|
-
assert(/LEAN_NOT_FOUND/.test(req) && /NO_SUBPROCESS/.test(req), '★ the require text names BOTH unavailable-toolchain codes (the escape route must be documented in every mode)')
|
|
746
|
-
// The sentence is written ONCE and reused by both modes: a second copy would drift (one branch
|
|
747
|
-
// kept up to date, the other stale). The anchor `(LEAN_NOT_FOUND)` therefore occurs exactly once
|
|
748
|
-
// in the plugin, which is also what the sensitivity probe table assumes.
|
|
749
|
-
{
|
|
750
|
-
const src = readFileSync(fileURLToPath(PLUGIN), 'utf8')
|
|
751
|
-
assert(src.split('(LEAN_NOT_FOUND)').length - 1 === 1, '★ the unavailable-toolchain guidance is written once (not duplicated per mode/branch)')
|
|
752
|
-
assert(src.split('(NO_SUBPROCESS)').length - 1 === 1, 'the same single sentence names NO_SUBPROCESS (no per-branch copy)')
|
|
753
|
-
}
|
|
754
|
-
assert(/\*\*本模式要求\*\*/.test(req), 'the require bullet replaces the encourage opt-out in place')
|
|
755
|
-
// normal / heartbeat / post-compact recap all carry the standing work line; the two that ARE a
|
|
756
|
-
// reply contract also document the `defect` decision (coreRules is a recap prefix, not a contract)
|
|
757
|
-
for (const [label, text] of [['normal', await G.prompts('normal', 'r-1')], ['heartbeat', await G.prompts('heartbeat', 'r-1')], ['coreRules', await G.prompts('coreRules', 'r-1')]]) {
|
|
758
|
-
assert(/【顺手形式化(强制)】/.test(text), label + ': the standing formalization line uses the require wording')
|
|
759
|
-
assert(/归档前先跑通(vibe_v4_lean_run 或 run=true);跑不通的定义不要进可复用库。/.test(text), label + ': ★ it requires a green run before archiving a reusable definition')
|
|
760
|
-
if (label !== 'coreRules') assert(/"decision":"used\|blocked\|defect"/.test(text), label + ': the reply contract documents the `defect` decision')
|
|
761
|
-
}
|
|
762
|
-
assert(/"decision":"used\|blocked\|defect"/.test(req) && /具体偏差/.test(req), '★ the voting reply contract documents decision=defect and its note')
|
|
763
|
-
}
|
|
764
|
-
{
|
|
765
|
-
// ---- the sweep: EVERY agent-facing string must spell the three tools in full (docs §6-1) ----
|
|
766
|
-
const scanned = []
|
|
767
|
-
const addText = (label, t) => { if (typeof t === 'string' && t) scanned.push({ label, text: t }) }
|
|
768
|
-
addText('captured run prompts', G.allPrompts())
|
|
769
|
-
for (const which of ['brainstorm', 'normal', 'heartbeat', 'coreRules']) addText(which, await G.prompts(which, 'r-1'))
|
|
770
|
-
for (const t of ['p-text', 'p-corpus']) for (const st of ['independent', 'debate']) addText('verify:' + t + ':' + st, await G.prompts('verify', 'r-1', { target: t, stage: st }))
|
|
771
|
-
// tool hints are injected text too (docs §6 hard requirement 1 names them explicitly)
|
|
772
|
-
const r1G = G.resAgent(G.childOf('r-1'))
|
|
773
|
-
const hintGreen = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-hint', content: 'theorem p_hint : 1 = 1 := rfl\n' }, r1G)
|
|
774
|
-
assert(hintGreen.ok === true && hintGreen.passed === true, 'precondition: a green file exists for the hint sweep')
|
|
775
|
-
const hintRed = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-hint-red', content: 'theorem p_hint_red : 1 = 2 := by sorry\n' }, r1G)
|
|
776
|
-
assert(hintRed.ok === true && hintRed.passed === false, 'precondition: a red file exists for the hint sweep')
|
|
777
|
-
addText('lean_run hint (green)', (await G.callTool('vibe_v4_lean_run', { file: 'Formal/p-hint.lean' }, r1G)).hint)
|
|
778
|
-
addText('lean_run hint (red)', (await G.callTool('vibe_v4_lean_run', { file: 'Formal/p-hint-red.lean' }, r1G)).hint)
|
|
779
|
-
addText('lean_lib hint', (await G.callTool('vibe_v4_lean_lib', {}, r1G)).hint)
|
|
780
|
-
toolchainAvailable = false
|
|
781
|
-
addText('LEAN_NOT_FOUND message', (await G.callTool('vibe_v4_lean_run', { file: 'Formal/p-hint.lean' }, r1G)).message)
|
|
782
|
-
toolchainAvailable = true
|
|
783
|
-
for (const t of G.toolRegs.filter((x) => /lean/.test(x.name))) addText('tool description ' + t.name, t.description)
|
|
784
|
-
// The activity log is agent/host-readable text too (vibe_v4_report exposes `recentActivity`, and
|
|
785
|
-
// a host reads it out to the group): a bare abbreviation there names a tool that does not exist,
|
|
786
|
-
// for exactly the same reason as in a prompt.
|
|
787
|
-
try {
|
|
788
|
-
const log = JSON.parse(readIf(join(G.projectRoot, 'State', 'session.json')) || '{}').activityLog || []
|
|
789
|
-
for (const e of log) addText('activity log: ' + e.event, String((e && e.detail) || ''))
|
|
790
|
-
} catch (e) { /* the log is best-effort */ }
|
|
791
|
-
const bare = [/(^|[^a-z_])lean_run/, /(^|[^a-z_])lean_archive/, /(^|[^a-z_])lean_lib/]
|
|
792
|
-
const offenders = []
|
|
793
|
-
for (const s of scanned) for (const re of bare) if (re.test(s.text)) offenders.push(s.label + ' :: ' + re.source)
|
|
794
|
-
assert(offenders.length === 0, '★ no injected text (prompt, tool hint or tool description) uses a bare tool abbreviation (' + offenders.slice(0, 3).join(' | ') + ')')
|
|
795
|
-
assert(scanned.length >= 15, 'the sweep really covered the injected-text surface (' + scanned.length + ' texts)')
|
|
796
|
-
}
|
|
797
|
-
{
|
|
798
|
-
// docs §4.1-3 / §6.1: the fidelity branch's "the framework withholds the verdict" clause may only
|
|
799
|
-
// appear in `require` (the only mode with a gate). In `encourage` the framework still withdraws
|
|
800
|
-
// the proof and records the TODO, but it CANNOT hold the ballot — so the text must not promise
|
|
801
|
-
// that, and must instead point at the voter's own abstention.
|
|
802
|
-
await G.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
803
|
-
const reqFid = await G.prompts('verify', 'r-1', { target: 'p-hint', stage: 'independent' })
|
|
804
|
-
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(reqFid), 'precondition: p-hint is the passed/fidelity case in this prompt')
|
|
805
|
-
assert(/本次裁定\*\*不定论\*\*/.test(reqFid), "'require' fidelity text states the verdict will be withheld (it has a gate)")
|
|
806
|
-
await G.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
807
|
-
const encFid = await G.prompts('verify', 'r-1', { target: 'p-hint', stage: 'independent' })
|
|
808
|
-
assert(!/本次裁定\*\*不定论\*\*/.test(encFid), "★ 'encourage' does NOT promise a hold it cannot enforce (docs §4.1-3)")
|
|
809
|
-
assert(/本档没有门禁/.test(encFid) && /弃权值/.test(encFid), "★ instead it says the voter's own abstention is what keeps the ballot from concluding")
|
|
810
|
-
assert(/降级为 attempted、删除归档证明、写入形式化待办/.test(encFid), 'the withdrawal the framework CAN enforce is still stated in both modes')
|
|
811
|
-
assert(/发现任何偏差,不要投 0/.test(encFid), 'the fidelity rule itself is mode-independent')
|
|
812
|
-
await G.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// ===============================================================
|
|
816
|
-
// 13. the `defect` reply channel (docs §4.1 / §10 item 8) — BEHAVIOURAL, not wording
|
|
817
|
-
// ===============================================================
|
|
818
|
-
section('13 the `defect` reply withdraws a passing proof (spec §4.1)')
|
|
819
|
-
await G.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
820
|
-
const activityOf = (h) => { try { return JSON.parse(readIf(join(h.projectRoot, 'State', 'session.json')) || '{}').activityLog || [] } catch (e) { return [] } }
|
|
821
|
-
const g1 = G.resAgent(G.childOf('r-1'))
|
|
822
|
-
{
|
|
823
|
-
const arc = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-defect', content: 'theorem p_defect : 2 + 2 = 4 := by decide\n' }, g1)
|
|
824
|
-
assert(arc.passed === true && existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-defect.lean')), 'precondition: p-defect has a green archived proof')
|
|
825
|
-
const w = await workWake(G, 'r-1')
|
|
826
|
-
G.fireEnd(w.childId, { summary: '我逐条核对了 Lean 代码,发现偏差。', formal: { target: 'p-defect', decision: 'defect', note: 'Lean 里的条件比命题弱:只证了 n ≥ 1 的情形' }, contextPct: 20 })
|
|
827
|
-
await sleep(90)
|
|
828
|
-
const st = await G.callTool('vibe_v4_status', {})
|
|
829
|
-
const rec = st.formal.objects.find((o) => o.target === 'p-defect')
|
|
830
|
-
assert(rec && rec.status === 'attempted', '★ a `formal.decision=defect` reply downgrades the object to attempted (never a refutation)')
|
|
831
|
-
assert(rec && rec.proof === '', '★ the archived proof is cleared from the record')
|
|
832
|
-
assert(rec && rec.note === 'Lean 里的条件比命题弱:只证了 n ≥ 1 的情形', 'the concrete deviation is stored as the record note')
|
|
833
|
-
assert(!existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-defect.lean')), '★ the archived proof file is DELETED (the formalization is 不合格 — the proposition is NOT false)')
|
|
834
|
-
assert(existsSync(join(G.projectRoot, 'Formal', 'p-defect.lean')), 'the WORKING file is kept — the code is not lost, only its "passed" claim')
|
|
835
|
-
assert(st.formal.passed.indexOf('p-defect') === -1, 'the object is no longer reported as Lean-passed')
|
|
836
|
-
assert(shellCalls.some((c) => /Remove-Item|^rm -f/.test(c) && /p-defect\.lean/.test(c)), 'the withdrawal really went through the platform shell (the fs service has no delete)')
|
|
837
|
-
const todo = readIf(join(G.projectRoot, 'Formal', 'TODO.md'))
|
|
838
|
-
assert(/p-defect/.test(todo) && /formal-defect/.test(todo), '★ Formal/TODO.md lists the object with the DEFECT reason')
|
|
839
|
-
assert(/只证了 n ≥ 1 的情形/.test(todo), '★ the concrete deviation reaches the human-readable TODO')
|
|
840
|
-
const idx = readIf(join(G.projectRoot, 'Formal', 'Index.md'))
|
|
841
|
-
assert(/\| p-defect \| attempted \|/.test(idx), 'Formal/Index.md downgrades the object to attempted')
|
|
842
|
-
assert(/只证了 n ≥ 1 的情形/.test(idx), 'the deviation is the record note in the index')
|
|
843
|
-
assert(activityOf(G).some((e) => /忠实性缺陷/.test(e.detail) && /p-defect/.test(e.detail)), '★ the retraction is announced in the activity log')
|
|
844
|
-
// the log line is agent/host-readable text, so it is bound by the same §4.1-3 rule as the prompt:
|
|
845
|
-
// in `encourage` it may not claim a hold the mode does not have.
|
|
846
|
-
{
|
|
847
|
-
const line = activityOf(G).filter((e) => /忠实性缺陷/.test(e.detail) && /p-defect[^-]/.test(e.detail)).pop() || { detail: '' }
|
|
848
|
-
assert(/本档没有门禁/.test(line.detail) && !/本次裁定\*\*不定论\*\*/.test(line.detail), "★ the encourage-mode activity line does not promise the 不定论 hold that only `require` enforces")
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
{
|
|
852
|
-
// §4.1 degraded withdrawal: the record must never be the ONLY thing withdrawn. A host whose shell
|
|
853
|
-
// "succeeds" without deleting (a stub host / a permissions quirk) must be caught by re-reading the
|
|
854
|
-
// file through the fs service, and the archive must then be OVERWRITTEN with a withdrawal notice —
|
|
855
|
-
// otherwise the retracted proof keeps sitting at the exact path everyone looks for the proof.
|
|
856
|
-
const arcD = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-defect-degraded', content: 'theorem p_defect_degraded : 5 = 5 := rfl\n' }, g1)
|
|
857
|
-
assert(arcD.passed === true && existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-defect-degraded.lean')), 'precondition: the degraded-path object has a green archived proof')
|
|
858
|
-
shellDeletesFiles = false
|
|
859
|
-
const wD = await workWake(G, 'r-1')
|
|
860
|
-
G.fireEnd(wD.childId, { summary: '核对后发现偏差。', formal: { target: 'p-defect-degraded', decision: 'defect', note: '结论方向相反' }, contextPct: 20 })
|
|
861
|
-
await sleep(90)
|
|
862
|
-
shellDeletesFiles = true
|
|
863
|
-
const body = readIf(join(G.projectRoot, 'Verified', 'Lean', 'p-defect-degraded.lean'))
|
|
864
|
-
assert(!/p_defect_degraded/.test(body), '★ a shell that exits 0 without deleting does NOT leave the retracted proof readable — its original text is gone')
|
|
865
|
-
assert(/已撤回/.test(body) && /原代码保留在工作文件/.test(body), '★ the archived file was OVERWRITTEN with an explicit withdrawal notice instead (delete → confirm → overwrite)')
|
|
866
|
-
const recD = (await G.callTool('vibe_v4_status', {})).formal.objects.find((o) => o.target === 'p-defect-degraded')
|
|
867
|
-
assert(recD && recD.status === 'attempted' && recD.proof === '', 'the record is downgraded on the degraded path too')
|
|
868
|
-
assert(activityOf(G).some((e) => /覆盖归档证明/.test(e.detail) && /p-defect-degraded/.test(e.detail)), '★ the activity log says WHICH withdrawal path was taken (overwritten, not silently reported as deleted)')
|
|
869
|
-
}
|
|
870
|
-
{
|
|
871
|
-
const arc2 = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-defect2', content: 'theorem p_defect2 : 3 + 3 = 6 := by decide\n' }, g1)
|
|
872
|
-
assert(arc2.passed === true, 'precondition: p-defect2 is Lean-passed')
|
|
873
|
-
const w2 = await workWake(G, 'r-2')
|
|
874
|
-
G.fireEnd(w2.childId, { summary: '有偏差,但没写清是什么。', formal: { target: 'p-defect2', decision: 'defect' }, contextPct: 20 })
|
|
875
|
-
await sleep(90)
|
|
876
|
-
const st2 = await G.callTool('vibe_v4_status', {})
|
|
877
|
-
const rec2 = st2.formal.objects.find((o) => o.target === 'p-defect2')
|
|
878
|
-
assert(rec2 && rec2.status === 'passed' && rec2.proof === 'Verified/Lean/p-defect2.lean', '★ a `defect` reply WITHOUT a note is REJECTED — the record is untouched')
|
|
879
|
-
assert(existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-defect2.lean')), '★ and the archived proof is NOT withdrawn')
|
|
880
|
-
assert(activityOf(G).some((e) => /defect/.test(e.detail) && /V4_INVALID_ARGUMENT/.test(e.detail)), '★ the rejection carries the preset error code (V4_INVALID_ARGUMENT), not a silent drop')
|
|
881
|
-
assert((await G.callTool('vibe_v4_status', {})).ok === true, 'the refusal did not crash the run')
|
|
882
|
-
}
|
|
883
|
-
{
|
|
884
|
-
const blk = await G.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-defect3', note: '前置知识未形式化,本轮不做' }, g1)
|
|
885
|
-
assert(blk.ok === true && blk.status === 'blocked', 'precondition: p-defect3 carries a reasoned blocker record')
|
|
886
|
-
const w3 = await workWake(G, 'r-1')
|
|
887
|
-
G.fireEnd(w3.childId, { summary: '复核后发现归档的形式化换了对象。', formal: { target: 'p-defect3', decision: 'defect', note: '归档的形式化证的是特例,换了对象' }, contextPct: 20 })
|
|
888
|
-
await sleep(90)
|
|
889
|
-
const st3 = await G.callTool('vibe_v4_status', {})
|
|
890
|
-
const rec3 = st3.formal.objects.find((o) => o.target === 'p-defect3')
|
|
891
|
-
assert(rec3 && rec3.status === 'attempted', '★ `defect` downgrades even a `blocked` record (spec §4.1: ALWAYS downgrade)')
|
|
892
|
-
assert(st3.formal.blocked.indexOf('p-defect3') === -1, '★ a bad formalization may not stay in the gate-passing `blocked` state')
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// ===============================================================
|
|
896
|
-
// 14. `require` after a defect: the retraction closes the gate (docs §4.1-3 / §10 item 9)
|
|
897
|
-
// ===============================================================
|
|
898
|
-
// 13b. a plain `used` judgement must NOT withdraw an ESTABLISHED proof (contract §4): only a
|
|
899
|
-
// fidelity defect retracts one. v2 shipped the unconditional downgrade and silently re-closed the gate.
|
|
900
|
-
section('13b a \`used\` reply must NOT downgrade an already-passed object')
|
|
901
|
-
{
|
|
902
|
-
const arcU = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-usedkeep', content: 'theorem p_usedkeep : 2 + 2 = 4 := by decide\n' }, g1)
|
|
903
|
-
assert(arcU.passed === true && existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-usedkeep.lean')), 'used-keep: the object starts with a green archived proof')
|
|
904
|
-
const wU = await workWake(G, 'r-1')
|
|
905
|
-
G.fireEnd(wU.childId, { summary: '这一轮只是又写了一遍草稿。', formal: { target: 'p-usedkeep', decision: 'used', file: 'Formal/p-usedkeep.lean' }, contextPct: 20 })
|
|
906
|
-
await sleep(90)
|
|
907
|
-
const stU = await G.callTool('vibe_v4_status', {})
|
|
908
|
-
const recU = stU.formal.objects.find((o) => o.target === 'p-usedkeep')
|
|
909
|
-
assert(!!recU && recU.status === 'passed', '★ a `used` reply does NOT downgrade an already-passed object (got ' + JSON.stringify(recU) + ')')
|
|
910
|
-
assert(!!recU && recU.proof === 'Verified/Lean/p-usedkeep.lean', '★ and the proof pointer survives the reply')
|
|
911
|
-
assert(existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-usedkeep.lean')), '★ and the archived proof is still on disk')
|
|
912
|
-
assert(stU.formal.passed.indexOf('p-usedkeep') !== -1, '★ status still reports it as Lean-passed')
|
|
913
|
-
const rerunU = await G.callTool('vibe_v4_lean_run', { file: 'Formal/p-usedkeep.lean', target: 'p-usedkeep' }, g1)
|
|
914
|
-
assert(rerunU.ok === true, 'used-keep: the work file runs green')
|
|
915
|
-
const recU2 = (await G.callTool('vibe_v4_status', {})).formal.objects.find((o) => o.target === 'p-usedkeep')
|
|
916
|
-
assert(!!recU2 && recU2.status === 'passed', '★ a plain re-run does not downgrade a passed record (contract §4)')
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
section("14 'require' withholds the verdict after a defect, even on a unanimous 1")
|
|
920
|
-
const H = await establish()
|
|
921
|
-
await H.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
922
|
-
{
|
|
923
|
-
const h1 = H.resAgent(H.childOf('r-1'))
|
|
924
|
-
const arc = await H.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-defect-req', content: 'theorem p_defect_req : 4 * 1 ^ 2 - 2 = (1:Nat) ^ 2 := by decide\n' }, h1)
|
|
925
|
-
assert(arc.passed === true, 'precondition: p-defect-req is Lean-passed')
|
|
926
|
-
const before = await H.prompts('verify', 'r-1', { target: 'p-defect-req', stage: 'independent' })
|
|
927
|
-
assert(/不要投 0/.test(before) && /formal:\{decision:'defect'/.test(before), 'the voter is told (before voting) how to report a fidelity defect')
|
|
928
|
-
await H.callTool('vibe_v4_record_proposition', { id: 'p-defect-req', title: '缺陷不是证伪', statement: '形式化写窄了不代表命题为假', prob: 0.9, value: 0.6, motivation: 'm' }, h1)
|
|
929
|
-
const n0 = H.followups.length
|
|
930
|
-
await H.callTool('vibe_v4_message', { to: 'r-1', content: '请处理本轮工作。' })
|
|
931
|
-
for (let i = 0; i < 300 && H.followups.length === n0; i++) await sleep(10)
|
|
932
|
-
// r-1 reports the defect AND votes 1 in the SAME reply; r-2 votes 1. The framework must NOT take
|
|
933
|
-
// the unanimous 1: the formalization was just retracted, so the verdict has to be deferred.
|
|
934
|
-
const replyFor = (pt, rId) => (/团队验证/.test(pt)
|
|
935
|
-
? (rId === 'r-1'
|
|
936
|
-
? { vote: { verdict: 1, reason: '我发现形式化写窄了,但独立看命题仍为真' }, formal: { target: 'p-defect-req', decision: 'defect', note: 'Lean 只证了 x=1 的特例,命题要求所有整数 x' } }
|
|
937
|
-
: { vote: { verdict: 1, reason: '独立复核为真' } })
|
|
938
|
-
: { summary: '提议验证 p-defect-req。', solved: false, propose_verify: 'p-defect-req', contextPct: 20 })
|
|
939
|
-
for (let i = 0; i < 4; i++) { await drive(H, replyFor, verifySettled(H)); if (await verifySettled(H)()) break }
|
|
940
|
-
const st = await H.callTool('vibe_v4_status', {})
|
|
941
|
-
assert(st.verifyInProgress === false && st.pendingVerify === null, 'the verification actually settled (so the next assertion is falsifiable)')
|
|
942
|
-
assert(!existsSync(join(H.projectRoot, 'Verified', '命题', 'p-defect-req.md')), '★ a defect in `require` mode writes NO Verified card, even with a unanimous 1')
|
|
943
|
-
assert(st.formal.todo.indexOf('p-defect-req') !== -1, '★ the object stays 未定论 on the formalization TODO')
|
|
944
|
-
const rec = st.formal.objects.find((o) => o.target === 'p-defect-req')
|
|
945
|
-
assert(rec && rec.status === 'attempted' && rec.proof === '', 'the formal record is the retracted one (attempted, no proof)')
|
|
946
|
-
assert(rec && /只证了 x=1 的特例/.test(rec.note || ''), "the record's note is the reported deviation")
|
|
947
|
-
assert(!existsSync(join(H.projectRoot, 'Verified', 'Lean', 'p-defect-req.lean')), '★ the archived proof was withdrawn as part of the retraction')
|
|
948
|
-
assert(!/已验证·真/.test(readIf(join(H.projectRoot, 'Propos', 'r-1', 'p-defect-req.md'))), 'the source card was NOT rewritten to 已验证·真 (the conclusion was withheld, not taken)')
|
|
949
|
-
assert(!/已验证·真/.test(readIf(join(H.projectRoot, 'Shared', 'debates', 'p-defect-req.md'))), 'the debate record does not claim a 真 conclusion')
|
|
950
|
-
const todo = readIf(join(H.projectRoot, 'Formal', 'TODO.md'))
|
|
951
|
-
assert(/p-defect-req/.test(todo) && /formal-defect/.test(todo), '★ the TODO keeps the DEFECT reason, not merely formal-required')
|
|
952
|
-
assert(/只证了 x=1 的特例/.test(todo), 'the concrete deviation is what a human reads in the TODO')
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// ===============================================================
|
|
956
|
-
// 15. the shipped Lean prompt corpus (docs §10 item 10) — a HUMAN must be able to re-read the
|
|
957
|
-
// exact text, not just the assertions about it.
|
|
958
|
-
// ===============================================================
|
|
959
|
-
section('15 the captured Lean prompt corpus is written for human review')
|
|
960
|
-
const K = await establish()
|
|
961
|
-
{
|
|
962
|
-
const vibe = K.vibeRoot
|
|
963
|
-
// Normalise BOTH slash forms, and the VibeMath root FIRST: it sits INSIDE the workspace, so
|
|
964
|
-
// replacing the workspace first would leave `<WS>/VibeMath` instead of `<VIBEMATH>`.
|
|
965
|
-
const scrub = (s) => String(s == null ? '' : s)
|
|
966
|
-
.split(vibe).join('<VIBEMATH>').split(vibe.replace(/\\/g, '/')).join('<VIBEMATH>')
|
|
967
|
-
.split(K.WS).join('<WS>').split(K.WS.replace(/\\/g, '/')).join('<WS>')
|
|
968
|
-
const corpus = []
|
|
969
|
-
const add = (kind, label, prompt) => corpus.push({ kind, label, prompt: scrub(prompt) })
|
|
970
|
-
// (a) off: a TRUE no-op must be visible in the corpus, not merely asserted
|
|
971
|
-
add('verify', 'off/verify', await K.prompts('verify', 'r-1', { target: 'p-corpus', stage: 'independent' }))
|
|
972
|
-
add('work', 'off/normal', await K.prompts('normal', 'r-1'))
|
|
973
|
-
// (b) encourage
|
|
974
|
-
await K.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
975
|
-
add('verify', 'encourage/verify', await K.prompts('verify', 'r-1', { target: 'p-corpus', stage: 'independent' }))
|
|
976
|
-
const encNormal = await K.prompts('normal', 'r-1')
|
|
977
|
-
add('work', 'encourage/normal', encNormal)
|
|
978
|
-
add('work', 'encourage/heartbeat', await K.prompts('heartbeat', 'r-1'))
|
|
979
|
-
add('work', 'encourage/coreRules', await K.prompts('coreRules', 'r-1'))
|
|
980
|
-
// (c) require
|
|
981
|
-
await K.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
982
|
-
add('verify', 'require/verify', await K.prompts('verify', 'r-1', { target: 'p-corpus', stage: 'independent' }))
|
|
983
|
-
add('verify', 'require/verify/debate', await K.prompts('verify', 'r-1', { target: 'p-corpus', stage: 'debate' }))
|
|
984
|
-
add('work', 'require/normal', await K.prompts('normal', 'r-1'))
|
|
985
|
-
// (d) a PASSED object: the review subject has changed to fidelity
|
|
986
|
-
const k1 = K.resAgent(K.childOf('r-1'))
|
|
987
|
-
await K.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-corpus-passed', content: 'theorem p_corpus_passed : 1 + 1 = 2 := by decide\n' }, k1)
|
|
988
|
-
const fid = await K.prompts('verify', 'r-1', { target: 'p-corpus-passed', stage: 'independent' })
|
|
989
|
-
add('verify', 'passed/fidelity', fid)
|
|
990
|
-
// (d2) the SAME passed object in `encourage`: the promise must shrink to what that mode enforces
|
|
991
|
-
// (docs §4.1-3: no gate there, so no claim that the framework withholds the verdict).
|
|
992
|
-
await K.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
993
|
-
const fidEnc = await K.prompts('verify', 'r-1', { target: 'p-corpus-passed', stage: 'independent' })
|
|
994
|
-
add('verify', 'passed/fidelity (encourage)', fidEnc)
|
|
995
|
-
await K.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
996
|
-
// (e) a BLOCKED object
|
|
997
|
-
await K.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-corpus-blocked', note: '需要未形式化的解析数论框架' }, k1)
|
|
998
|
-
add('verify', 'blocked/verify', await K.prompts('verify', 'r-1', { target: 'p-corpus-blocked', stage: 'independent' }))
|
|
999
|
-
// (f) the `formal` reply contract line itself (the field whose parsing §13/§14 prove)
|
|
1000
|
-
const contractLine = (t) => { const m = String(t).match(/"formal":\{[^\n]*\}\}/); return m ? m[0] : '' }
|
|
1001
|
-
add('contract', 'formal reply contract (voting prompt)', contractLine(fid))
|
|
1002
|
-
add('contract', 'formal reply contract (work prompt)', contractLine(encNormal))
|
|
1003
|
-
// (g) a REAL delivered work wake (proves the builder is the one actually used to address a resident)
|
|
1004
|
-
const w = await workWake(K, 'r-2')
|
|
1005
|
-
add('work', 'require/real work wake', promptOf(w))
|
|
1006
|
-
K.fireEnd(w.childId, { summary: '继续推进。', solved: false, contextPct: 20 })
|
|
1007
|
-
await sleep(40)
|
|
1008
|
-
// (h) the tool hints are injected text too (docs §6 hard requirement 1)
|
|
1009
|
-
add('hint', 'lean_run hint (green)', (await K.callTool('vibe_v4_lean_run', { file: 'Formal/p-corpus-passed.lean' }, k1)).hint)
|
|
1010
|
-
add('hint', 'lean_lib hint', (await K.callTool('vibe_v4_lean_lib', {}, k1)).hint)
|
|
1011
|
-
|
|
1012
|
-
// The corpus is REGENERATED on every run. Dumping it while V4_PLUGIN points at a MUTATED copy
|
|
1013
|
-
// would let a sensitivity probe overwrite the SHIPPED corpus with mutated text (the probe's job
|
|
1014
|
-
// is to run this suite against a broken plugin), so the dump is skipped then — unless the probe
|
|
1015
|
-
// explicitly redirects it with V4_CORPUS_DIR. The IN-MEMORY corpus assertions below still run, so
|
|
1016
|
-
// nothing is weakened.
|
|
1017
|
-
const writeCorpus = !process.env.V4_PLUGIN || !!process.env.V4_CORPUS_DIR
|
|
1018
|
-
if (writeCorpus) {
|
|
1019
|
-
mkdirSync(CORPUS_DIR, { recursive: true })
|
|
1020
|
-
writeFileSync(join(CORPUS_DIR, 'formal-verify-v4.json'), JSON.stringify({ entries: corpus }, null, 2), 'utf8')
|
|
1021
|
-
const md = ['# V4 形式化验证交互语料(prompt corpus)', '',
|
|
1022
|
-
'> 由 `formal-verify-v4.test.mjs` 落盘:非 `off` 模式下常驻**真正会读到**的 Lean 提示词原文',
|
|
1023
|
-
'> (`vibe_v4_prompts` 的只读回显 + 一条真实投递的工作轮 + 工具 `hint`)。',
|
|
1024
|
-
'> 工作区路径归一化为 `<WS>`,VibeMath 根归一化为 `<VIBEMATH>`:确定、可 diff、不含任何本机路径。', '',
|
|
1025
|
-
'> 覆盖:`off`(无 Lean 文本)、`encourage`、**`require`**、对象 `passed` 后的**忠实性分支**',
|
|
1026
|
-
'> (`encourage` / `require` 两种措辞各一份:只有 `require` 会声称"不定论")、',
|
|
1027
|
-
'> `blocked` 分支、平时工作轮的「顺手形式化」,以及回执契约里的 `formal` 字段。', '']
|
|
1028
|
-
for (let i = 0; i < corpus.length; i++) {
|
|
1029
|
-
const c = corpus[i]
|
|
1030
|
-
md.push('## [' + i + '] ' + c.kind + ' · ' + c.label)
|
|
1031
|
-
md.push('')
|
|
1032
|
-
md.push('```text')
|
|
1033
|
-
md.push(c.prompt)
|
|
1034
|
-
md.push('```')
|
|
1035
|
-
md.push('')
|
|
1036
|
-
}
|
|
1037
|
-
writeFileSync(join(CORPUS_DIR, 'formal-verify-v4.md'), md.join('\n'), 'utf8')
|
|
1038
|
-
assert(existsSync(join(CORPUS_DIR, 'formal-verify-v4.json')) && existsSync(join(CORPUS_DIR, 'formal-verify-v4.md')), 'the prompt corpus was written (JSON + Markdown)')
|
|
1039
|
-
}
|
|
1040
|
-
assert(corpus.length >= 14, 'the corpus covers the whole Lean prompt surface (' + corpus.length + ' prompts)')
|
|
1041
|
-
const byLabel = (l) => corpus.find((c) => c.label === l)
|
|
1042
|
-
assert(!!byLabel('off/verify') && !/Lean/.test(byLabel('off/verify').prompt) && !/Lean/.test(byLabel('off/normal').prompt), '★ the corpus keeps the off-mode entries and they contain NO Lean text')
|
|
1043
|
-
assert(/【Lean 形式化验证(鼓励模式)】/.test(byLabel('encourage/verify').prompt), 'the corpus carries the encourage voting prompt')
|
|
1044
|
-
assert(/【Lean 形式化验证(强制模式)】/.test(byLabel('require/verify').prompt), '★ the corpus carries the REQUIRE voting prompt')
|
|
1045
|
-
assert(/不要投 0/.test(byLabel('passed/fidelity').prompt), '★ the corpus carries the passed/fidelity branch')
|
|
1046
|
-
assert(/本次裁定\*\*不定论\*\*/.test(byLabel('passed/fidelity').prompt), 'the require fidelity entry keeps the hold it really enforces')
|
|
1047
|
-
assert(!!byLabel('passed/fidelity (encourage)') && !/本次裁定\*\*不定论\*\*/.test(byLabel('passed/fidelity (encourage)').prompt)
|
|
1048
|
-
&& /本档没有门禁/.test(byLabel('passed/fidelity (encourage)').prompt), '★ the encourage fidelity entry does NOT promise the hold only require has (docs §4.1-3)')
|
|
1049
|
-
assert(/【顺手形式化(强制)】/.test(byLabel('require/normal').prompt), 'the corpus carries the ordinary work-round line')
|
|
1050
|
-
assert(/"decision":"used\|blocked\|defect"/.test(byLabel('formal reply contract (voting prompt)').prompt), '★ the corpus carries the `formal` reply contract line with decision=defect')
|
|
1051
|
-
assert(/【顺手形式化/.test(byLabel('require/real work wake').prompt), 'the corpus also keeps a prompt the framework REALLY delivered')
|
|
1052
|
-
const joined = corpus.map((c) => c.prompt).join('\n')
|
|
1053
|
-
assert(joined.indexOf(K.WS) === -1 && joined.indexOf(K.WS.replace(/\\/g, '/')) === -1 && joined.indexOf(vibe) === -1 && joined.indexOf(vibe.replace(/\\/g, '/')) === -1, '★ every captured prompt normalises <WS> and <VIBEMATH> (diffable, no machine paths)')
|
|
1054
|
-
assert(!/\[object Object\]|\bNaN\b|:\s*undefined|["']undefined["']|undefined\s*[,}\]]/.test(joined), 'no captured prompt contains placeholder garbage')
|
|
1055
|
-
assert(corpus.every((c) => c.prompt && c.prompt.length > 20), 'every corpus entry carries real prompt text')
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// ===============================================================
|
|
1059
|
-
// 16. a FRESH run must not inherit the previous run's formal records (docs §31.6): object ids are
|
|
1060
|
-
// reused (p-*, r-1..), so the in-memory reset in `start()` has to REACH DISK even when the mode
|
|
1061
|
-
// has been switched back to `off` — otherwise a later `resume` restores a stale `passed` and
|
|
1062
|
-
// re-opens the require gate for an object of the new run. This is the exact hazard that makes
|
|
1063
|
-
// the `off`-mode persistence guard ("do not create State/formal.json" ) non-trivial.
|
|
1064
|
-
// ===============================================================
|
|
1065
|
-
section('16 a fresh run clears the persisted formal records (even in off mode)')
|
|
1066
|
-
{
|
|
1067
|
-
const stale = readIf(join(D.projectRoot, 'State', 'formal.json'))
|
|
1068
|
-
assert(/"p-proof"/.test(stale), 'precondition: the previous run left formal records on disk')
|
|
1069
|
-
await D.callTool('vibe_v4_set', { formalVerify: 'off' })
|
|
1070
|
-
const beforeSpawns = D.spawns.length
|
|
1071
|
-
const st = await D.callTool('vibe_v4_start', { problem: '新一轮:形式化清白起点', residentCount: 1 })
|
|
1072
|
-
assert(st.ok === true && D.spawns.length > beforeSpawns, 'the fresh run actually started (so the clean-slate assertion is falsifiable)')
|
|
1073
|
-
const now = readIf(join(D.projectRoot, 'State', 'formal.json'))
|
|
1074
|
-
assert(!/"p-proof"/.test(now) && !/"passed"/.test(now), '★ a fresh run clears the persisted formal records on disk, even in off mode (no stale `passed` can open the new run’s gate)')
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// ===============================================================
|
|
1078
|
-
console.log('')
|
|
1079
|
-
console.log('passed=' + passed + ' failed=' + failed)
|
|
1080
|
-
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
1081
|
-
console.log('ALL GREEN')
|
|
1082
|
-
process.exit(0)
|
|
1
|
+
// ============================================================
|
|
2
|
+
// V4 LEAN FORMAL VERIFICATION SUITE (docs/formal-verification.md)
|
|
3
|
+
//
|
|
4
|
+
// Asserts the whole contract of the `formalVerify` knob for the v4 preset:
|
|
5
|
+
// · 'off' is a TRUE no-op (no Lean text anywhere, no gate, verification unchanged)
|
|
6
|
+
// · 'encourage' injects the Lean section into the VERIFICATION prompt AND the ordinary work
|
|
7
|
+
// round, and — the actual point of the feature — turns the voting prompt into a
|
|
8
|
+
// FIDELITY review once a Lean run has passed
|
|
9
|
+
// · 'require' withholds a unanimous 真/假 verdict as 未定论 until the object is Lean-passed
|
|
10
|
+
// or carries an explicit, reasoned blocker record; then it allows it, and the
|
|
11
|
+
// Verified card records how strong the result really is
|
|
12
|
+
// · the three tools (run / archive / lib) write the right things to the right paths
|
|
13
|
+
//
|
|
14
|
+
// The Lean toolchain is mocked through the `subprocess` SERVICE (a fake Lean: a file passes unless
|
|
15
|
+
// it still contains `sorry` or the marker `-- FAIL`), so the tests exercise the REAL code path
|
|
16
|
+
// (resolveExecutable → spawn → collected stdout → exit code) without requiring Lean to be installed.
|
|
17
|
+
//
|
|
18
|
+
// V4_PLUGIN lets a sensitivity probe point this suite at a MUTATED copy of the plugin; without that
|
|
19
|
+
// override every probe would run the unmutated file and pass vacuously (AUDIT-CHECKLIST §2.5).
|
|
20
|
+
//
|
|
21
|
+
// Prompt text is asserted through the plugin's own `vibe_v4_prompts` host tool — the exact builder
|
|
22
|
+
// the framework uses — rather than by guessing which queued wake carries which prompt. That keeps
|
|
23
|
+
// the assertions precise even when the scheduler queues several rounds in one pass, and the suite
|
|
24
|
+
// still drives the real consensus path for every STATE transition (AUDIT-CHECKLIST §0.1/§2.2).
|
|
25
|
+
//
|
|
26
|
+
// Run: node tests/formal-verify-v4.test.mjs
|
|
27
|
+
// ============================================================
|
|
28
|
+
import { mkdtempSync, existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, rmSync } from 'node:fs'
|
|
29
|
+
import { tmpdir } from 'node:os'
|
|
30
|
+
import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
|
|
31
|
+
import { pathToFileURL, fileURLToPath } from 'node:url'
|
|
32
|
+
|
|
33
|
+
// V4_PLUGIN (same convention the v5 suite uses for V5_PLUGIN): point the suite at a MUTATED copy of
|
|
34
|
+
// the plugin so a sensitivity probe can prove each invariant actually turns this suite red. Accept a
|
|
35
|
+
// plain filesystem path or a file:// URL, and never touch the value otherwise.
|
|
36
|
+
function pluginUrl() {
|
|
37
|
+
const raw = process.env.V4_PLUGIN
|
|
38
|
+
if (!raw || !String(raw).trim()) return new URL('../vibe-math-v4/vibe-math-v4.js', import.meta.url)
|
|
39
|
+
const s = String(raw).trim()
|
|
40
|
+
if (/^file:/i.test(s)) return new URL(s)
|
|
41
|
+
return pathToFileURL(fileURLToPath(new URL('file:///' + s.replace(/\\/g, '/'))))
|
|
42
|
+
}
|
|
43
|
+
const PLUGIN = pluginUrl()
|
|
44
|
+
// docs §10 item 10: the Lean prompt corpus is shipped under prompt-corpus-v4/ so a HUMAN can read
|
|
45
|
+
// the exact text the framework sends. V4_CORPUS_DIR overrides the destination (same convention v3
|
|
46
|
+
// and v5 use for V3_CORPUS_DIR / V5_CORPUS_DIR).
|
|
47
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
48
|
+
const CORPUS_DIR = process.env.V4_CORPUS_DIR ? pathResolve(process.env.V4_CORPUS_DIR) : join(HERE, '..', 'prompt-corpus-v4')
|
|
49
|
+
|
|
50
|
+
let passed = 0, failed = 0
|
|
51
|
+
const failures = []
|
|
52
|
+
const assert = (c, m) => { if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) } }
|
|
53
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
54
|
+
const section = (t) => console.log('\n[' + t + ']')
|
|
55
|
+
const JSONX = o => '```json\n' + JSON.stringify(o) + '\n```'
|
|
56
|
+
const promptOf = (entry) => (entry && entry.blocks && entry.blocks[0] && entry.blocks[0].text) || ''
|
|
57
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
58
|
+
|
|
59
|
+
// ===============================================================
|
|
60
|
+
// MOCK HOST — the shape selfdrive-v4 / e2e-v4-fixes use, plus a fake Lean
|
|
61
|
+
// ===============================================================
|
|
62
|
+
let toolchainAvailable = true
|
|
63
|
+
// A host that exposes NO `subprocess` service at all (docs §7 → NO_SUBPROCESS, and §6 hard rule 4
|
|
64
|
+
// requires the injected guidance to name that code so the resident records a blocker instead of
|
|
65
|
+
// retrying forever).
|
|
66
|
+
let subprocessAvailable = true
|
|
67
|
+
// A stub host whose shell EXITS 0 WITHOUT deleting anything (a permissions quirk / a stub host).
|
|
68
|
+
// Used by §13 to prove the withdrawal is not a best-effort delete: the framework must confirm the
|
|
69
|
+
// file is gone through the fs service and fall back to overwriting it with a withdrawal notice.
|
|
70
|
+
let shellDeletesFiles = true
|
|
71
|
+
const leanRuns = [] // every spawn the framework made, for cwd/argv assertions
|
|
72
|
+
const terminated = [] // files whose handle the framework actively terminate()d (docs §7)
|
|
73
|
+
const shellCalls = [] // every platform-shell script (mkdir at mount, Remove-Item on defect)
|
|
74
|
+
|
|
75
|
+
function makeSubprocess() {
|
|
76
|
+
return {
|
|
77
|
+
async resolveExecutable(cmd) {
|
|
78
|
+
if (!toolchainAvailable) throw new Error('spawn lean ENOENT')
|
|
79
|
+
if (String(cmd) !== 'lean' && String(cmd) !== 'lake') throw new Error('unknown executable ' + cmd)
|
|
80
|
+
return String(cmd)
|
|
81
|
+
},
|
|
82
|
+
spawn(spec) {
|
|
83
|
+
// The preset drives the platform shell through this SAME `subprocess` service (runShell:
|
|
84
|
+
// powershell / /bin/sh) for mkdir at mount, and — since docs §4.1 — for Remove-Item when a
|
|
85
|
+
// `defect` retracts an archived proof. Handle it here so that retraction is a REAL filesystem
|
|
86
|
+
// deletion, exactly as the real host performs it; otherwise "the proof file is gone" could
|
|
87
|
+
// only be asserted against a mock's bookkeeping.
|
|
88
|
+
const argv0 = String((spec.argv && spec.argv[0]) || '')
|
|
89
|
+
if (/powershell|cmd\.exe|\/bin\/sh|(^|\/)sh$/i.test(argv0)) {
|
|
90
|
+
const script = String(spec.argv[spec.argv.length - 1] || '')
|
|
91
|
+
shellCalls.push(script)
|
|
92
|
+
const m = script.match(/-LiteralPath\s+'((?:[^']|'')*)'/)
|
|
93
|
+
if (shellDeletesFiles && /Remove-Item/.test(script) && m) rmSync(m[1].replace(/''/g, "'"), { force: true })
|
|
94
|
+
if (shellDeletesFiles && /^rm -f /.test(script)) for (const q of script.slice(6).match(/'[^']*'/g) || []) rmSync(q.slice(1, -1), { force: true })
|
|
95
|
+
return {
|
|
96
|
+
done: Promise.resolve({ exitCode: 0, signal: null }),
|
|
97
|
+
collected: { stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) }, stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) } },
|
|
98
|
+
terminate() {},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// The plugin passes an ABSOLUTE path as the last argv element, so resolve it directly; fall
|
|
102
|
+
// back to scanning the argv for an existing .lean file (mirrors leanAbsPath).
|
|
103
|
+
const last = spec.argv[spec.argv.length - 1]
|
|
104
|
+
const file = existsSync(last) ? last : (spec.argv.find(a => /\.lean$/.test(String(a)) && existsSync(a)) || last)
|
|
105
|
+
const text = existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
106
|
+
const bad = /sorry|-- FAIL/.test(text)
|
|
107
|
+
leanRuns.push({ argv: spec.argv.slice(), file, cwd: spec.cwd, graceMs: spec.graceMs, stdio: spec.stdio })
|
|
108
|
+
// A HANG file never settles and never exits: the ONLY thing that can end it is the framework's
|
|
109
|
+
// own timeout calling handle.terminate() (docs §7). Records each terminate so the suite can
|
|
110
|
+
// assert the timeout path is ACTIVE, not merely reported.
|
|
111
|
+
if (/-- HANG/.test(text)) {
|
|
112
|
+
return {
|
|
113
|
+
done: new Promise(() => {}),
|
|
114
|
+
collected: { stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) }, stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) } },
|
|
115
|
+
terminate() { terminated.push(file) },
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const stdout = bad ? '' : 'ok\n'
|
|
119
|
+
const stderr = bad ? 'error: declaration uses sorry\n' : ''
|
|
120
|
+
return {
|
|
121
|
+
done: Promise.resolve({ exitCode: bad ? 1 : 0, signal: null }),
|
|
122
|
+
collected: {
|
|
123
|
+
stdout: { readFrom: () => ({ text: stdout, nextOffset: stdout.length, lossy: false }) },
|
|
124
|
+
stderr: { readFrom: () => ({ text: stderr, nextOffset: stderr.length, lossy: false }) },
|
|
125
|
+
},
|
|
126
|
+
terminate() {},
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function makeHost() {
|
|
133
|
+
const WS = mkdtempSync(join(tmpdir(), 'vibe-v4-lean-'))
|
|
134
|
+
const listeners = {}, toolRegs = [], spawns = [], followups = []
|
|
135
|
+
const subprocess = makeSubprocess()
|
|
136
|
+
let ROOT
|
|
137
|
+
const ctx = {
|
|
138
|
+
get(name) { return name === 'subprocess' && subprocessAvailable ? subprocess : undefined },
|
|
139
|
+
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
140
|
+
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
141
|
+
logger: { info() {}, warn() {}, error() {} },
|
|
142
|
+
timeout(cb, ms) { const h = setTimeout(cb, ms); return () => clearTimeout(h) },
|
|
143
|
+
tools: { register(s) { toolRegs.push(s) } }, commands: { register() {} },
|
|
144
|
+
// Real DSH exposes ONLY subagents.sendMessage for continuable wakes (followup is an Agent
|
|
145
|
+
// method, not a subagents service method) — deliberately no followup, so a regression fails.
|
|
146
|
+
subagents: {
|
|
147
|
+
list() { return ['spawn'] },
|
|
148
|
+
async startContinuable({ label, request }) { const id = 'c' + (spawns.length + 1); spawns.push({ label, request, childId: id }); return { childId: id } },
|
|
149
|
+
async sendMessage(parent, childId, blocks) { followups.push({ childId, blocks }) },
|
|
150
|
+
interrupt() {},
|
|
151
|
+
},
|
|
152
|
+
agents: { roots() { return [] }, get(id) { return id === 'sess-A' ? ROOT : undefined } },
|
|
153
|
+
fs: {
|
|
154
|
+
async resolve(rel, opts) { const b = (opts && opts.cwd) || WS; const p = (typeof rel === 'string' && isAbsolute(rel)) ? rel.replace(/\//g, '\\') : join(b, ...String(rel).split('/')); return { targetKey: p, displayPath: p } },
|
|
155
|
+
async stat(t) { return existsSync(t.targetKey) ? { version: 'v1', type: 'file', size: 1 } : undefined },
|
|
156
|
+
async readText(t) { return readFileSync(t.targetKey, 'utf8') },
|
|
157
|
+
async writeText(t, c) { mkdirSync(dirname(t.targetKey), { recursive: true }); writeFileSync(t.targetKey, c, 'utf8') },
|
|
158
|
+
async listDir(t) { if (!existsSync(t.targetKey)) return []; return readdirSync(t.targetKey, { withFileTypes: true }).map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })) },
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
ROOT = { id: 'sess-A', options: { provider: 'mock', model: 'm' }, session: { id: 'sess-A', header: { cwd: WS, parentSession: undefined } }, followup() {}, ctx: undefined }
|
|
162
|
+
const projectRoot = join(WS, 'VibeMath', 'Projects', 'default')
|
|
163
|
+
const vibeRoot = join(WS, 'VibeMath')
|
|
164
|
+
let consumed = 0
|
|
165
|
+
const h = {
|
|
166
|
+
WS, ctx, toolRegs, spawns, followups, ROOT, projectRoot, vibeRoot,
|
|
167
|
+
async callTool(n, a, agent) {
|
|
168
|
+
const s = toolRegs.find(x => x.name === n); if (!s) throw new Error('no tool ' + n)
|
|
169
|
+
return JSON.parse(await s.execute(a || {}, { agent: agent || ROOT }))
|
|
170
|
+
},
|
|
171
|
+
/** The exact prompt text a resident would receive, read through the plugin's own host tool. */
|
|
172
|
+
async prompts(which, rId, arg) {
|
|
173
|
+
const out = await h.callTool('vibe_v4_prompts', Object.assign({ which, member: rId }, arg || {}), ROOT)
|
|
174
|
+
return out.text
|
|
175
|
+
},
|
|
176
|
+
resAgent: (cid) => ({ id: cid, session: { id: cid, header: { cwd: WS, parentSession: 'sess-A' } } }),
|
|
177
|
+
ridOfChild(cid) { for (const sp of spawns) if (sp.childId === cid) return sp.label; return '' },
|
|
178
|
+
childOf(rId) { for (const sp of spawns) if (sp.label === rId) return sp.childId; return '' },
|
|
179
|
+
fireEnd(cid, reply) {
|
|
180
|
+
const blocks = reply === undefined ? [] : [{ type: 'text', text: JSONX(reply) }]
|
|
181
|
+
for (const fn of (listeners['subagent/end'] || [])) fn({ id: cid, runId: 'r', provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: blocks })
|
|
182
|
+
},
|
|
183
|
+
/** Every prompt this project's residents have received (founding + wakes). */
|
|
184
|
+
allPrompts() {
|
|
185
|
+
return spawns.map(s => (s.request && s.request.prompt && s.request.prompt[0] && s.request.prompt[0].text) || '').join('\n')
|
|
186
|
+
+ '\n' + followups.map(promptOf).join('\n')
|
|
187
|
+
},
|
|
188
|
+
nextWake() { while (consumed < followups.length) return followups[consumed++]; return null },
|
|
189
|
+
resetCursor() { consumed = followups.length },
|
|
190
|
+
}
|
|
191
|
+
return h
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function mount() {
|
|
195
|
+
const h = makeHost()
|
|
196
|
+
const mod = await import(PLUGIN.href + '?t=' + Date.now() + Math.random())
|
|
197
|
+
;(mod.default || mod).apply(h.ctx)
|
|
198
|
+
return h
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ===============================================================
|
|
202
|
+
// driving helpers
|
|
203
|
+
// ===============================================================
|
|
204
|
+
/** Complete every brainstorm round so the run becomes phase=active, then park the heartbeat. */
|
|
205
|
+
async function establish() {
|
|
206
|
+
const h = await mount()
|
|
207
|
+
const before = h.spawns.length
|
|
208
|
+
await h.callTool('vibe_v4_start', { problem: '形式化契约验证测试', residentCount: 2 })
|
|
209
|
+
for (let i = 0; i < 60 && h.spawns.length < before + 2; i++) await sleep(10)
|
|
210
|
+
for (const sp of h.spawns.slice(before)) { h.fireEnd(sp.childId, { summary: '初始见解。', solved: false, contextPct: 10 }); await sleep(30) }
|
|
211
|
+
// Park the heartbeat far away: these tests drive wakes explicitly, so a short heartbeat would
|
|
212
|
+
// only add unrelated normal rounds and make the followup stream harder to reason about.
|
|
213
|
+
await h.callTool('vibe_v4_set', { activityTimeoutMs: 600000 })
|
|
214
|
+
for (let i = 0; i < 40; i++) { const s = await h.callTool('vibe_v4_status', {}); if (s.phase === 'active') break; await sleep(20) }
|
|
215
|
+
await sleep(30)
|
|
216
|
+
h.resetCursor()
|
|
217
|
+
return h
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Answer the next wake with `replyFor(promptText, rId)`; returns how many wakes were answered.
|
|
221
|
+
* `stop` is evaluated AFTER processing each wake, never before the first one: a run that is simply
|
|
222
|
+
* idle between rounds would otherwise satisfy "no verification in flight" at entry and the loop
|
|
223
|
+
* would exit without answering anything.
|
|
224
|
+
*/
|
|
225
|
+
async function drive(h, replyFor, stop, budget = 40) {
|
|
226
|
+
let n = 0
|
|
227
|
+
for (; n < budget; n++) {
|
|
228
|
+
const w = h.nextWake()
|
|
229
|
+
if (!w) break
|
|
230
|
+
const pt = promptOf(w)
|
|
231
|
+
h.fireEnd(w.childId, replyFor(pt, h.ridOfChild(w.childId)))
|
|
232
|
+
await sleep(12)
|
|
233
|
+
if (stop && await stop()) { n++; break }
|
|
234
|
+
}
|
|
235
|
+
return n
|
|
236
|
+
}
|
|
237
|
+
const verifySettled = (h) => async () => { const s = await h.callTool('vibe_v4_status', {}); return s.verifyInProgress === false && s.pendingVerify === null }
|
|
238
|
+
const voteReply = (plan) => (pt, rId) => (/团队验证/.test(pt)
|
|
239
|
+
? { vote: { verdict: plan(rId), reason: rId + ' 的判断' } }
|
|
240
|
+
: { summary: '继续推进。', solved: false, contextPct: 20 })
|
|
241
|
+
/** Drive the pending verification to a settled state and return the resulting status. */
|
|
242
|
+
async function settleVerify(h, plan = () => 1, budget = 40) {
|
|
243
|
+
await drive(h, voteReply(plan), verifySettled(h), budget)
|
|
244
|
+
return await h.callTool('vibe_v4_status', {})
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Record a card for `target`, have r-1 propose verifying it, vote it wherever it lands, and return
|
|
248
|
+
* the settled status. Prompt TEXT is asserted separately (see `h.prompts`) — this helper exercises
|
|
249
|
+
* the real consensus path for the STATE transitions.
|
|
250
|
+
*/
|
|
251
|
+
async function proposeAndVote(h, target, opts = {}) {
|
|
252
|
+
const card = opts.card || { id: target, title: target, statement: '待验证对象 ' + target, prob: 0.8, value: 0.6, motivation: 'm' }
|
|
253
|
+
await h.callTool('vibe_v4_record_proposition', card, h.resAgent(h.childOf('r-1')))
|
|
254
|
+
// The heartbeat is parked far away, so drive the FIRST round explicitly: an idle resident is
|
|
255
|
+
// woken immediately by an incoming message (postMessage → wakeResident).
|
|
256
|
+
const before = h.followups.length
|
|
257
|
+
await h.callTool('vibe_v4_message', { to: 'r-1', content: '请处理本轮工作。' })
|
|
258
|
+
for (let i = 0; i < 300 && h.followups.length === before; i++) await sleep(10)
|
|
259
|
+
const proposal = opts.proposal || { summary: '提议验证 ' + target + '。', solved: false, propose_verify: target, contextPct: 20 }
|
|
260
|
+
await drive(h, (pt) => (/团队验证/.test(pt)
|
|
261
|
+
? { vote: { verdict: (opts.verdict === undefined ? 1 : opts.verdict), reason: '预看提示词' } }
|
|
262
|
+
: proposal), verifySettled(h))
|
|
263
|
+
// The verification may only have been QUEUED by the proposal round; finish it.
|
|
264
|
+
return await settleVerify(h, opts.plan || (() => (opts.verdict === undefined ? 1 : opts.verdict)))
|
|
265
|
+
}
|
|
266
|
+
/** Wake exactly the given resident with an ordinary work round and return that wake. */
|
|
267
|
+
async function workWake(h, rId) {
|
|
268
|
+
const before = h.followups.length
|
|
269
|
+
await h.callTool('vibe_v4_message', { to: rId, content: '请继续推进。' })
|
|
270
|
+
for (let i = 0; i < 300 && h.followups.length === before; i++) await sleep(10)
|
|
271
|
+
const w = h.nextWake()
|
|
272
|
+
assert(w !== null, 'a work wake was delivered to ' + rId)
|
|
273
|
+
return w
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ===============================================================
|
|
277
|
+
console.log('-- V4 Lean formal verification --')
|
|
278
|
+
|
|
279
|
+
// ---------- 1. 'off' is a true no-op ----------
|
|
280
|
+
section("1 'off' (default) is a true no-op")
|
|
281
|
+
const A = await establish()
|
|
282
|
+
{
|
|
283
|
+
const st = await A.callTool('vibe_v4_status', {})
|
|
284
|
+
assert(st.formal.mode === 'off' && st.formal.on === false, "the default mode is 'off' (got " + st.formal.mode + ')')
|
|
285
|
+
assert(/formalVerify=off/.test(st.params), "status exposes formalVerify=off in its readable params (got " + st.params + ')')
|
|
286
|
+
// Scan BEFORE any hand-written test message is injected: this test's own text would otherwise
|
|
287
|
+
// taint the scan. The scan is on the feature's own TOKENS rather than the bare word 形式化: the
|
|
288
|
+
// pre-existing brainstorm brief already contains 形式化 in the unrelated sentence about being
|
|
289
|
+
// free to invent theories, so asserting the bare word would fail for a reason that has nothing
|
|
290
|
+
// to do with this contract (AUDIT-CHECKLIST §2.2).
|
|
291
|
+
const all = A.allPrompts()
|
|
292
|
+
assert(!/Lean/.test(all), 'no founding/round prompt mentions Lean in off mode')
|
|
293
|
+
assert(!/顺手形式化|Lean 通过|忠实性审查|vibe_v4_lean/.test(all), 'no founding/round prompt carries any Lean-formalization text in off mode')
|
|
294
|
+
assert(all.length > 0, 'sanity: the scan actually saw the founding prompts')
|
|
295
|
+
// `off` must not CREATE Lean state at all: the feature's durable record file must not appear
|
|
296
|
+
// for a session that never used the feature (v3's suite pins the same invariant).
|
|
297
|
+
assert(!existsSync(join(A.projectRoot, 'State', 'formal.json')), '★ off mode creates NO State/formal.json (a TRUE no-op, not just an empty record store)')
|
|
298
|
+
}
|
|
299
|
+
// an off-mode verification must behave exactly as before: unanimous 真 → Verified/, no formal record
|
|
300
|
+
const offSettled = await proposeAndVote(A, 'p-off', {
|
|
301
|
+
card: { id: 'p-off', title: '关模式', statement: '关模式下的普通命题', prob: 0.8, value: 0.6, motivation: 'm' },
|
|
302
|
+
proposal: { summary: '请提议验证 p-off。', solved: false, propose_verify: 'p-off', contextPct: 20 },
|
|
303
|
+
})
|
|
304
|
+
assert(existsSync(join(A.projectRoot, 'Verified', '命题', 'p-off.md')), "★ 'off' still closes a unanimous TRUE verdict with NO Lean artifact")
|
|
305
|
+
assert(offSettled.formal.passed.length === 0 && offSettled.formal.objects.length === 0, "'off' records no formal object at all")
|
|
306
|
+
assert(!/形式化/.test(readIf(join(A.projectRoot, 'Verified', '命题', 'p-off.md'))), 'the Verified card carries no formal line in off mode')
|
|
307
|
+
assert(!existsSync(join(A.projectRoot, 'Formal', 'TODO.md')) || !/p-off/.test(readIf(join(A.projectRoot, 'Formal', 'TODO.md'))), 'no formalization TODO was created for p-off')
|
|
308
|
+
assert(!/顺手形式化|Lean/.test(await A.prompts('verify', 'r-1', { target: 'p-off' })), 'the voting prompt carries no Lean text in off mode')
|
|
309
|
+
assert(!/顺手形式化/.test(await A.prompts('normal', 'r-1')), 'the work prompt carries no formalization line in off mode')
|
|
310
|
+
// the tools still EXIST in off mode (static registration), they are just never advertised
|
|
311
|
+
assert(['vibe_v4_lean_run', 'vibe_v4_lean_archive', 'vibe_v4_lean_lib'].every(n => !!A.toolRegs.find(t => t.name === n)),
|
|
312
|
+
'the three Lean tools are registered in every mode (registration is static)')
|
|
313
|
+
|
|
314
|
+
// ★ The mode switch must be REACHABLE THROUGH THE TOOL SCHEMA (2.3.2 defect D1) ──────────────
|
|
315
|
+
// Every tool schema here is closed (`additionalProperties:false`), so a key the schema does not
|
|
316
|
+
// advertise is REJECTED by any schema-validating provider. v3 shipped 2.3.0/2.3.1 with all four Lean
|
|
317
|
+
// parameters missing from the set-params schema while every assertion in this file stayed green —
|
|
318
|
+
// because the suite calls the handler DIRECTLY and never inspects the registered schema. The feature
|
|
319
|
+
// could not be switched on at all through the tool interface.
|
|
320
|
+
{
|
|
321
|
+
const setSpec = A.toolRegs.find((t) => t.name === 'vibe_v4_set')
|
|
322
|
+
assert(!!setSpec, "vibe_v4_set is registered")
|
|
323
|
+
assert(setSpec.parameters && setSpec.parameters.type === 'object' && setSpec.parameters.additionalProperties === false,
|
|
324
|
+
'★ vibe_v4_set publishes a CLOSED object schema (an unlisted key is rejected, so the schema IS the contract)')
|
|
325
|
+
for (const k of ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']) {
|
|
326
|
+
assert(Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k),
|
|
327
|
+
'★ the registered schema advertises ' + k + ' (every other surface documents it; a schema that omits it makes the switch unreachable)')
|
|
328
|
+
}
|
|
329
|
+
assert(JSON.stringify(setSpec.parameters.properties.formalVerify.enum) === JSON.stringify(['off', 'encourage', 'require']),
|
|
330
|
+
'the schema narrows formalVerify to the three real modes (a typo must not be a fourth)')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// A stray `formal` reply in OFF mode must be INERT (finding #1): the reply contract does not offer
|
|
334
|
+
// the field there, so honouring it would contradict "off is a TRUE no-op". The TOOLS stay usable.
|
|
335
|
+
{
|
|
336
|
+
await A.callTool('vibe_v4_record_proposition', { id: 'p-offreply', title: 'off', statement: '关模式下注入回执', prob: 0.8, value: 0.6, motivation: 'm' }, A.resAgent(A.childOf('r-1')))
|
|
337
|
+
const wOffR = await workWake(A, 'r-1')
|
|
338
|
+
A.fireEnd(wOffR.childId, { summary: '继续。', solved: false, contextPct: 20, formal: { target: 'p-offreply', decision: 'defect', note: '不应被记录' } })
|
|
339
|
+
await sleep(80)
|
|
340
|
+
const stOffReply = await A.callTool('vibe_v4_status', {})
|
|
341
|
+
assert(stOffReply.formal.objects.length === 0, '★ a stray `formal` reply in off mode records NO formal object')
|
|
342
|
+
assert(!existsSync(join(A.projectRoot, 'Formal', 'p-offreply.lean')), '★ and writes no formal working file')
|
|
343
|
+
assert(!existsSync(join(A.projectRoot, 'Formal', 'TODO.md')) || !/p-offreply/.test(readIf(join(A.projectRoot, 'Formal', 'TODO.md'))), '★ and creates no TODO entry')
|
|
344
|
+
assert(!existsSync(join(A.projectRoot, 'State', 'formal.json')), '★ and still no State/formal.json after the stray reply (the off-mode guard is a true no-op)')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---------- 2. parameter validation + runtime switching ----------
|
|
348
|
+
section('2 parameter validation and runtime switching')
|
|
349
|
+
const B = await establish()
|
|
350
|
+
{
|
|
351
|
+
const bad = await B.callTool('vibe_v4_set', { formalVerify: 'banana' })
|
|
352
|
+
assert(bad.formal.mode === 'off' && /formalVerify=off/.test(bad.params), "an unknown mode degrades to 'off', never to a stronger mode (got " + bad.formal.mode + ')')
|
|
353
|
+
const enc = await B.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
354
|
+
assert(enc.formal.mode === 'encourage', "'encourage' is accepted")
|
|
355
|
+
const req = await B.callTool('vibe_v4_set', { formalVerify: 'require', leanTimeoutMs: -5, leanCommand: ' ' })
|
|
356
|
+
assert(req.formal.mode === 'require', "'require' is accepted")
|
|
357
|
+
assert(/leanTimeoutMs=120000/.test(req.params), 'a non-positive leanTimeoutMs falls back to the default (' + (/leanTimeoutMs=\S+/.exec(req.params) || [])[0] + ')')
|
|
358
|
+
assert(/leanCommand=lean/.test(req.params), 'a blank leanCommand falls back to "lean"')
|
|
359
|
+
const lk = await B.callTool('vibe_v4_set', { leanCommand: 'lake', leanArgs: ['env', 'lean'] })
|
|
360
|
+
assert(/leanCommand=lake/.test(lk.params) && /leanArgs=env,lean/.test(lk.params), 'leanCommand/leanArgs are settable (lake env lean)')
|
|
361
|
+
const la = await B.callTool('vibe_v4_set', { leanArgs: 'a, b ,,c' })
|
|
362
|
+
assert(/leanArgs=a,b,c/.test(la.params), 'leanArgs accepts a comma-separated string and drops blanks (got ' + (/leanArgs=[^,]*,[^,]*,[^,]*/.exec(la.params) || [])[0] + ')')
|
|
363
|
+
await B.callTool('vibe_v4_set', { leanTimeoutMs: 5 })
|
|
364
|
+
assert(/leanTimeoutMs=5/.test((await B.callTool('vibe_v4_status', {})).params), 'a legitimate leanTimeoutMs is preserved')
|
|
365
|
+
await B.callTool('vibe_v4_set', { leanTimeoutMs: 0 })
|
|
366
|
+
assert(/leanTimeoutMs=120000/.test((await B.callTool('vibe_v4_status', {})).params), 'leanTimeoutMs:0 also falls back to the default')
|
|
367
|
+
// the switch must be visible to the very NEXT prompt, not to some frozen brief
|
|
368
|
+
await B.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
369
|
+
assert(/顺手形式化/.test(await B.prompts('normal', 'r-1')), '★ switching the mode at runtime changes the NEXT prompt immediately')
|
|
370
|
+
await B.callTool('vibe_v4_set', { formalVerify: 'off' })
|
|
371
|
+
assert(!/顺手形式化/.test(await B.prompts('normal', 'r-1')), 'switching back to off removes it again')
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---------- 3. 'encourage' injection into BOTH prompts ----------
|
|
375
|
+
section("3 'encourage' injects the Lean section into the verification AND the work prompt")
|
|
376
|
+
const C = await establish()
|
|
377
|
+
await C.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
378
|
+
{
|
|
379
|
+
// The ordinary work round. Read the builder AND drive a real wake, so the assertion cannot pass
|
|
380
|
+
// on a builder that is never actually used to address a resident.
|
|
381
|
+
const built = await C.prompts('normal', 'r-2')
|
|
382
|
+
assert(/【顺手形式化(鼓励)】/.test(built), '★ the ordinary work round tells residents to formalize reusable objects as they go')
|
|
383
|
+
assert(/vibe_v4_lean_archive kind='def'/.test(built), 'the work round points at the archive tool for reusable definitions')
|
|
384
|
+
assert(/vibe_v4_lean_lib 查重/.test(built), 'the work round tells them to de-duplicate against the reuse library first')
|
|
385
|
+
assert(/"formal":/.test(built), 'the work reply contract documents the formal field')
|
|
386
|
+
assert(/Resident researcher r-2/.test(built), 'the work prompt is addressed to the resident it is built for')
|
|
387
|
+
const hb = await C.prompts('heartbeat', 'r-2')
|
|
388
|
+
assert(/【顺手形式化(鼓励)】/.test(hb), 'the CHECKPOINT/heartbeat round carries it too')
|
|
389
|
+
const w = await workWake(C, 'r-2')
|
|
390
|
+
assert(/【顺手形式化(鼓励)】/.test(promptOf(w)), '★ a REAL delivered work wake contains the line (the builder is the one in use)')
|
|
391
|
+
C.fireEnd(w.childId, { summary: '继续推进。', solved: false, contextPct: 20 })
|
|
392
|
+
await sleep(30)
|
|
393
|
+
const core = await C.prompts('coreRules')
|
|
394
|
+
assert(/\[核心规则重申\]/.test(core) && /【顺手形式化(鼓励)】/.test(core), 'the post-compact core-rules recap also carries the formalization line')
|
|
395
|
+
}
|
|
396
|
+
{
|
|
397
|
+
await proposeAndVote(C, 'p-enc', {
|
|
398
|
+
card: { id: 'p-enc', title: '鼓励注入', statement: '鼓励模式下的忠实性审查', prob: 0.5, value: 0.6, motivation: 'm' },
|
|
399
|
+
proposal: { summary: '提议验证 p-enc。', solved: false, propose_verify: 'p-enc', contextPct: 20 },
|
|
400
|
+
verdict: 0.5, plan: () => 0.5,
|
|
401
|
+
})
|
|
402
|
+
const prompt = await C.prompts('verify', 'r-1', { target: 'p-enc', stage: 'independent' })
|
|
403
|
+
assert(/【Lean 形式化验证(鼓励模式)】/.test(prompt), 'the voting prompt explains the Lean mode')
|
|
404
|
+
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(prompt), 'the voting prompt states that a passing Lean run shrinks the question to fidelity')
|
|
405
|
+
assert(/实现难度/.test(prompt), 'the voting prompt asks for the implementation-difficulty judgement')
|
|
406
|
+
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(prompt), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
407
|
+
assert(/vibe_v4_lean_run/.test(prompt) && /vibe_v4_lean_archive/.test(prompt) && /vibe_v4_lean_lib/.test(prompt), 'the voting prompt names all three tools')
|
|
408
|
+
assert(/"formal":/.test(prompt), '★ the voting reply contract documents the formal field')
|
|
409
|
+
assert(/Resident r-1/.test(prompt), 'the voting prompt is addressed to the voter it is built for')
|
|
410
|
+
assert(!/强制模式/.test(prompt), "'encourage' does not use the mandatory wording")
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ---------- 4. the run tool ----------
|
|
414
|
+
section('4 lean_run executes through the subprocess service and reports honestly')
|
|
415
|
+
const D = await establish()
|
|
416
|
+
await D.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
417
|
+
mkdirSync(join(D.projectRoot, 'Formal'), { recursive: true })
|
|
418
|
+
writeFileSync(join(D.projectRoot, 'Formal', 'good.lean'), 'theorem t : 1 = 1 := rfl\n', 'utf8')
|
|
419
|
+
writeFileSync(join(D.projectRoot, 'Formal', 'bad.lean'), 'theorem t : 1 = 2 := by sorry\n', 'utf8')
|
|
420
|
+
const r1 = D.resAgent(D.childOf('r-1'))
|
|
421
|
+
const runGood = await D.callTool('vibe_v4_lean_run', { file: 'Formal/good.lean' }, r1)
|
|
422
|
+
assert(runGood.ok === true && runGood.exitCode === 0, 'a file with no sorry runs green (' + JSON.stringify({ ok: runGood.ok, exitCode: runGood.exitCode }) + ')')
|
|
423
|
+
assert(typeof runGood.ms === 'number' && typeof runGood.command === 'string' && runGood.file === 'Formal/good.lean', 'the result carries {ms, command, file} as the spec requires')
|
|
424
|
+
assert(leanRuns.length > 0 && String(leanRuns[leanRuns.length - 1].cwd).replace(/\\/g, '/') === D.projectRoot.replace(/\\/g, '/'), 'the toolchain runs with the PROJECT root as cwd')
|
|
425
|
+
{
|
|
426
|
+
const last = leanRuns[leanRuns.length - 1]
|
|
427
|
+
assert(String(last.argv[0]).indexOf('lean') !== -1 && /\.lean$/.test(String(last.argv[last.argv.length - 1])), 'argv is [exe, ...leanArgs, absolute file]')
|
|
428
|
+
assert(last.stdio && last.stdio.stdin === 'ignore', "stdin is ignored (the spec's stdio shape)")
|
|
429
|
+
}
|
|
430
|
+
const runBad = await D.callTool('vibe_v4_lean_run', { file: 'Formal/bad.lean' }, r1)
|
|
431
|
+
assert(runBad.ok === false && runBad.exitCode === 1, 'a file that still uses sorry reports a red run')
|
|
432
|
+
assert(/sorry/.test(runBad.stderr), 'the compiler output is returned verbatim (' + JSON.stringify(runBad.stderr).slice(0, 60) + ')')
|
|
433
|
+
assert(runBad.code === 'LEAN_FAILED', 'a red run is typed LEAN_FAILED')
|
|
434
|
+
const runMissing = await D.callTool('vibe_v4_lean_run', { file: 'Formal/nope.lean' }, r1)
|
|
435
|
+
assert(runMissing.ok === false && runMissing.code === 'V4_NOT_FOUND', 'a missing file is refused with a typed code')
|
|
436
|
+
const runNoFile = await D.callTool('vibe_v4_lean_run', {}, r1)
|
|
437
|
+
assert(runNoFile.ok === false && runNoFile.code === 'V4_INVALID_ARGUMENT', 'a run without a file is refused')
|
|
438
|
+
// The guard's boundary is the VibeMath ROOT, not the project: the global reuse library
|
|
439
|
+
// deliberately lives at <VibeMath>/Formal/{Lib,Proved}, so climbing out of the project but staying
|
|
440
|
+
// inside VibeMath must remain legal (it just fails as a missing file).
|
|
441
|
+
const runOutside = await D.callTool('vibe_v4_lean_run', { file: '../../Formal/Lib/x.lean' }, r1)
|
|
442
|
+
assert(runOutside.code === 'V4_NOT_FOUND', 'climbing out of the project but staying inside VibeMath is allowed (the global library lives there)')
|
|
443
|
+
const esc1 = await D.callTool('vibe_v4_lean_run', { file: '../../../../../etc/evil.lean' }, r1)
|
|
444
|
+
assert(esc1.ok === false && esc1.code === 'V4_INVALID_ARGUMENT', '★ a traversal that climbs ABOVE the VibeMath root is refused')
|
|
445
|
+
const esc2 = await D.callTool('vibe_v4_lean_run', { file: 'Formal/../../../../../../evil.lean' }, r1)
|
|
446
|
+
assert(esc2.ok === false && esc2.code === 'V4_INVALID_ARGUMENT', 'a deeper traversal is refused too')
|
|
447
|
+
const esc3 = await D.callTool('vibe_v4_lean_run', { file: '/etc/evil.lean' }, r1)
|
|
448
|
+
assert(esc3.ok === false && esc3.code === 'V4_INVALID_ARGUMENT', 'an unrelated absolute path is refused')
|
|
449
|
+
const esc4 = await D.callTool('vibe_v4_lean_run', { file: 'C:\\Windows\\evil.lean' }, r1)
|
|
450
|
+
assert(esc4.ok === false && esc4.code === 'V4_INVALID_ARGUMENT', 'an unrelated absolute path with a drive letter is refused')
|
|
451
|
+
const notLean = await D.callTool('vibe_v4_lean_run', { file: 'Formal/good.txt' }, r1)
|
|
452
|
+
assert(notLean.ok === false && notLean.code === 'V4_INVALID_ARGUMENT', 'only .lean files can be executed')
|
|
453
|
+
toolchainAvailable = false
|
|
454
|
+
const noTc = await D.callTool('vibe_v4_lean_run', { file: 'Formal/good.lean' }, r1)
|
|
455
|
+
assert(noTc.ok === false && noTc.code === 'LEAN_NOT_FOUND', '★ a missing toolchain returns LEAN_NOT_FOUND instead of crashing')
|
|
456
|
+
assert(/仍可把形式化代码写下来归档/.test(noTc.message || ''), 'the failure explains the graceful degradation')
|
|
457
|
+
assert((await D.callTool('vibe_v4_status', {})).ok === true, 'the group did NOT crash on the missing toolchain (status still answers)')
|
|
458
|
+
toolchainAvailable = true
|
|
459
|
+
// No `subprocess` service at all (docs §7): the run must degrade to a typed NO_SUBPROCESS result —
|
|
460
|
+
// never a thrown error into the scheduler — and an object it was asked to record must stay
|
|
461
|
+
// `attempted` (an un-runnable host must not mint a `passed` record).
|
|
462
|
+
{
|
|
463
|
+
subprocessAvailable = false
|
|
464
|
+
const noSub = await D.callTool('vibe_v4_lean_run', { file: 'Formal/good.lean', target: 'p-nosub' }, r1)
|
|
465
|
+
assert(noSub.ok === false && noSub.code === 'NO_SUBPROCESS', '★ a host with no subprocess service returns NO_SUBPROCESS instead of crashing (got ' + noSub.code + ')')
|
|
466
|
+
assert(/no subprocess service/.test(String(noSub.message || '')), 'the failure is readable (it explains that Lean cannot be executed here)')
|
|
467
|
+
const stNoSub = await D.callTool('vibe_v4_status', {})
|
|
468
|
+
assert(stNoSub.ok === true, 'the group did NOT crash on the missing service (status still answers)')
|
|
469
|
+
const recNoSub = stNoSub.formal.objects.find((o) => o.target === 'p-nosub')
|
|
470
|
+
assert(recNoSub && recNoSub.status === 'attempted' && !recNoSub.proof, '★ the object record stays honest: attempted (never passed) when nothing could be executed')
|
|
471
|
+
subprocessAvailable = true
|
|
472
|
+
}
|
|
473
|
+
// A run that never finishes: the framework must ACTIVELY terminate it (docs §7) and still report a
|
|
474
|
+
// readable LEAN_TIMEOUT — relying on the host's own graceMs alone leaves the Lean process running.
|
|
475
|
+
{
|
|
476
|
+
writeFileSync(join(D.projectRoot, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1 := rfl\n', 'utf8')
|
|
477
|
+
const t0 = Date.now()
|
|
478
|
+
const runHang = await D.callTool('vibe_v4_lean_run', { file: 'Formal/hang.lean', timeout_ms: 1000 }, r1)
|
|
479
|
+
const waited = Date.now() - t0
|
|
480
|
+
assert(runHang.ok === false && runHang.code === 'LEAN_TIMEOUT' && runHang.timedOut === true, '★ a run that never finishes is reported as LEAN_TIMEOUT (got ' + runHang.code + ')')
|
|
481
|
+
assert(terminated.some(f => /hang\.lean$/.test(String(f))), '★ the timeout path really calls handle.terminate() (docs §7) instead of leaving the Lean process running')
|
|
482
|
+
assert(waited >= 900 && waited < 15000, 'the timeout waited for the cap before terminating (' + waited + 'ms)')
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ---------- 5. a passing proof flips the review subject to fidelity ----------
|
|
486
|
+
section('5 a passing proof flips the review subject to fidelity')
|
|
487
|
+
const arc = await D.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-proof', content: 'theorem p_proof : 3 * 1 ^ 2 - 2 = (1:Nat) ^ 2 := by decide\n' }, r1)
|
|
488
|
+
assert(arc.ok === true && arc.passed === true, 'the proof is archived and passes (' + JSON.stringify({ ok: arc.ok, passed: arc.passed }) + ')')
|
|
489
|
+
assert(arc.file === 'Formal/p-proof.lean', 'the working file is Formal/<target>.lean')
|
|
490
|
+
assert(arc.proof === 'Verified/Lean/p-proof.lean', 'the archived proof path is Verified/Lean/<target>.lean')
|
|
491
|
+
assert(existsSync(join(D.projectRoot, 'Formal', 'p-proof.lean')), 'the working file exists on disk')
|
|
492
|
+
assert(existsSync(join(D.projectRoot, 'Verified', 'Lean', 'p-proof.lean')), '★ the proof is archived under Verified/Lean/ as the proof of that object')
|
|
493
|
+
const stD = await D.callTool('vibe_v4_status', {})
|
|
494
|
+
assert(stD.formal.passed.indexOf('p-proof') !== -1, 'status reports the object as Lean-passed')
|
|
495
|
+
const idxD = readIf(join(D.projectRoot, 'Formal', 'Index.md'))
|
|
496
|
+
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')
|
|
497
|
+
// A later run recorded against an ALREADY-passed object records the run but must never strip
|
|
498
|
+
// `passed`/`proof`: doing so would silently remove the fidelity branch from the next voting prompt
|
|
499
|
+
// AND (in `require`) re-close the gate on an object that already has a green archived proof.
|
|
500
|
+
{
|
|
501
|
+
writeFileSync(join(D.projectRoot, 'Formal', 'p-proof.lean'), 'theorem p_proof : 1 = 2 := by sorry\n', 'utf8')
|
|
502
|
+
const rerun = await D.callTool('vibe_v4_lean_run', { file: 'Formal/p-proof.lean', target: 'p-proof' }, r1)
|
|
503
|
+
assert(rerun.ok === false, 'precondition: the recorded re-run is red')
|
|
504
|
+
const rec = (await D.callTool('vibe_v4_status', {})).formal.objects.find(o => o.target === 'p-proof')
|
|
505
|
+
assert(rec && rec.status === 'passed' && rec.proof === 'Verified/Lean/p-proof.lean',
|
|
506
|
+
'★ a RED re-run never downgrades a `passed` record (docs §31.6: only an explicit re-archive decides) — the object keeps its fidelity prompt and its require-mode gate')
|
|
507
|
+
writeFileSync(join(D.projectRoot, 'Formal', 'p-proof.lean'), 'theorem p_proof : 3 * 1 ^ 2 - 2 = (1:Nat) ^ 2 := by decide\n', 'utf8')
|
|
508
|
+
}
|
|
509
|
+
// a RED proof must NOT mint a Verified/Lean/ copy or mark the object passed
|
|
510
|
+
const arcRed = await D.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-red', content: 'theorem p_red : 1 = 2 := by sorry\n' }, r1)
|
|
511
|
+
assert(arcRed.ok === true && arcRed.passed === false && arcRed.status === 'attempted', 'a red proof is archived as attempted, not passed')
|
|
512
|
+
assert(!existsSync(join(D.projectRoot, 'Verified', 'Lean', 'p-red.lean')), '★ a red run does NOT mint a Verified/Lean proof copy')
|
|
513
|
+
// durable state: the record survives a resume (v4 has no session projection)
|
|
514
|
+
{
|
|
515
|
+
const raw = readIf(join(D.projectRoot, 'State', 'formal.json'))
|
|
516
|
+
assert(/"p-proof"/.test(raw) && /"passed"/.test(raw), "★ the formal record is persisted in v4's own durable State/formal.json")
|
|
517
|
+
const before = D.spawns.length
|
|
518
|
+
await D.callTool('vibe_v4_abort', {})
|
|
519
|
+
const res = await D.callTool('vibe_v4_resume', {})
|
|
520
|
+
assert(res.ok === true && D.spawns.length > before, 'abort → resume re-spawns residents')
|
|
521
|
+
const stR = await D.callTool('vibe_v4_status', {})
|
|
522
|
+
assert(stR.formal.passed.indexOf('p-proof') !== -1, '★ the Lean-passed record SURVIVES resume (durable state, not an in-memory projection)')
|
|
523
|
+
for (const sp of D.spawns.slice(before)) { D.fireEnd(sp.childId, { summary: '恢复见解。', solved: false, contextPct: 10 }); await sleep(25) }
|
|
524
|
+
await D.callTool('vibe_v4_set', { activityTimeoutMs: 600000 })
|
|
525
|
+
D.resetCursor()
|
|
526
|
+
}
|
|
527
|
+
// now the voting prompt must ASK FOR FIDELITY, not for a re-derivation
|
|
528
|
+
{
|
|
529
|
+
await proposeAndVote(D, 'p-proof', {
|
|
530
|
+
card: { id: 'p-proof', title: '忠实性', statement: '3N²−2=b² 在 N=1 时成立', prob: 0.9, value: 0.6, motivation: 'm' },
|
|
531
|
+
proposal: { summary: '提议验证 p-proof。', solved: false, propose_verify: 'p-proof', contextPct: 20 },
|
|
532
|
+
verdict: 0.5, plan: () => 0.5,
|
|
533
|
+
})
|
|
534
|
+
const prompt = await D.prompts('verify', 'r-1', { target: 'p-proof', stage: 'independent' })
|
|
535
|
+
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(prompt), 'the voting prompt announces the passing proof')
|
|
536
|
+
assert(/你不需要重新检查推导/.test(prompt), '★ it tells voters NOT to re-derive')
|
|
537
|
+
assert(/忠实性审查/.test(prompt), '★ it tells voters the review subject is now fidelity')
|
|
538
|
+
assert(/定义 \/ 对象 \/ 条件 \/ 假设 \/ 结论是否与命题原文\*\*完全一致\*\*/.test(prompt), 'it enumerates exactly what fidelity means')
|
|
539
|
+
assert(/一致 → verdict = 1/.test(prompt), "the fidelity instruction uses v4's REAL reply field name (`verdict`)")
|
|
540
|
+
assert(/发现任何偏差,不要投 0/.test(prompt), '★ it forbids expressing a fidelity deviation as "0 / false"')
|
|
541
|
+
assert(/偏差只说明\*\*形式化不合格\*\*,不代表命题为假/.test(prompt), '★ it states WHY: a defect is a failed formalization, not a refutation')
|
|
542
|
+
assert(/formal:\{decision:'defect', note:'<具体偏差>'\}/.test(prompt), '★ it hands the voter the exact `defect` reply contract')
|
|
543
|
+
assert(/降级为 attempted、删除归档证明、写入形式化待办/.test(prompt), 'it says what the framework WILL do with a defect (downgrade + delete + TODO)')
|
|
544
|
+
assert(/只有当你\*\*独立于这份 Lean 代码\*\*也能确定命题为假时,才投 0/.test(prompt), 'only an INDEPENDENT refutation may be expressed as 0')
|
|
545
|
+
assert(!/偏离 → 0/.test(prompt), '★ the old "a deviation ⇒ 0" wording is gone (it would fabricate a negative conclusion)')
|
|
546
|
+
assert(/Verified\/Lean\/p-proof\.lean/.test(prompt), 'it points at the archived proof')
|
|
547
|
+
// the SAME object's work round must not inherit the fidelity framing (that is a voting
|
|
548
|
+
// instruction), but it does carry the standing formalization line.
|
|
549
|
+
const work = await D.prompts('normal', 'r-1')
|
|
550
|
+
assert(!/忠实性审查/.test(work), 'the fidelity instruction is confined to the voting prompt')
|
|
551
|
+
assert(/【顺手形式化(鼓励)】/.test(work), 'the work round carries the standing formalization line instead')
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ---------- 6. the reusable cross-project library ----------
|
|
555
|
+
section('6 reusable definitions and lemmas go to the GLOBAL library')
|
|
556
|
+
const libPath = join(D.vibeRoot, 'Formal', 'Lib')
|
|
557
|
+
const provedPath = join(D.vibeRoot, 'Formal', 'Proved')
|
|
558
|
+
const defRes = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'ZMod5', content: 'def ZMod5 := Fin 5\n' }, r1)
|
|
559
|
+
assert(defRes.ok === true && defRes.file === 'Formal/Lib/ZMod5.lean', 'a reusable definition is archived to the global lib (' + defRes.file + ')')
|
|
560
|
+
assert(existsSync(join(libPath, 'ZMod5.lean')), '★ the definition exists under VibeMath/Formal/Lib/ (cross-project, NOT inside the project)')
|
|
561
|
+
assert(!existsSync(join(D.projectRoot, 'Formal', 'Lib', 'ZMod5.lean')), 'it is NOT duplicated inside the project tree')
|
|
562
|
+
const lemRes = await D.callTool('vibe_v4_lean_archive', { kind: 'lemma', name: 'sq_odd', content: 'theorem sq_odd (n : Nat) : Odd (n*n) → Odd n := by omega\n' }, r1)
|
|
563
|
+
assert(lemRes.ok === true && lemRes.file === 'Formal/Proved/sq_odd.lean', 'a lemma is archived to Proved/')
|
|
564
|
+
assert(existsSync(join(provedPath, 'sq_odd.lean')), 'the lemma exists under VibeMath/Formal/Proved/')
|
|
565
|
+
assert(/ZMod5/.test(readIf(join(libPath, 'Index.md'))), 'Lib/Index.md lists the new definition')
|
|
566
|
+
assert(/sq_odd/.test(readIf(join(provedPath, 'Index.md'))), 'Proved/Index.md lists the new lemma')
|
|
567
|
+
// `from` copies an existing workspace .lean file instead of inline content
|
|
568
|
+
writeFileSync(join(D.projectRoot, 'Formal', 'src.lean'), 'def copied := 7\n', 'utf8')
|
|
569
|
+
const fromRes = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'Copied', from: 'Formal/src.lean' }, r1)
|
|
570
|
+
assert(fromRes.ok === true && readIf(join(libPath, 'Copied.lean')) === 'def copied := 7\n', "kind='def' accepts from=<existing .lean file>")
|
|
571
|
+
const fromEsc = await D.callTool('vibe_v4_lean_archive', { kind: 'def', name: 'Evil', from: '../../../../etc/passwd' }, r1)
|
|
572
|
+
assert(fromEsc.ok === false && fromEsc.code === 'V4_INVALID_ARGUMENT', "★ kind='def' refuses a `from` outside the VibeMath root")
|
|
573
|
+
const libList = await D.callTool('vibe_v4_lean_lib', {}, r1)
|
|
574
|
+
assert(libList.ok === true && libList.counts.lib >= 2 && libList.counts.proved >= 1, 'lean_lib reports the reuse library sizes (' + JSON.stringify(libList.counts) + ')')
|
|
575
|
+
assert(libList.objects.some(o => o.target === 'p-proof' && o.status === 'passed'), 'lean_lib lists per-object formal status')
|
|
576
|
+
assert(/复用优先/.test(libList.hint || ''), 'lean_lib tells agents to reuse before redefining')
|
|
577
|
+
assert(libList.paths && /Formal\/Lib/.test(libList.paths.lib), 'lean_lib reports the global library paths')
|
|
578
|
+
const noName = await D.callTool('vibe_v4_lean_archive', { kind: 'def', content: 'def x := 1\n' }, r1)
|
|
579
|
+
assert(noName.ok === false && noName.code === 'V4_INVALID_ARGUMENT', 'archiving a definition without a name is refused')
|
|
580
|
+
const noBody = await D.callTool('vibe_v4_lean_archive', { kind: 'lemma', name: 'empty' }, r1)
|
|
581
|
+
assert(noBody.ok === false && noBody.code === 'V4_INVALID_ARGUMENT', 'archiving without content or from is refused')
|
|
582
|
+
const badKind = await D.callTool('vibe_v4_lean_archive', { kind: 'nonsense' }, r1)
|
|
583
|
+
assert(badKind.ok === false && badKind.code === 'V4_INVALID_ARGUMENT', 'an unknown archive kind is refused')
|
|
584
|
+
const libNoRefresh = await D.callTool('vibe_v4_lean_lib', { refresh: false }, r1)
|
|
585
|
+
assert(libNoRefresh.ok === true && libNoRefresh.rebuilt === false && libNoRefresh.counts.lib === null, 'refresh:false reads without rebuilding')
|
|
586
|
+
|
|
587
|
+
// ---------- 7. blocked needs a reason ----------
|
|
588
|
+
section('7 a "blocker" record must be explicit and reasoned')
|
|
589
|
+
const blkNoNote = await D.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-blk' }, r1)
|
|
590
|
+
assert(blkNoNote.ok === false && blkNoNote.code === 'V4_INVALID_ARGUMENT', '★ blocked without a note is refused')
|
|
591
|
+
const blkBlank = await D.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-blk', note: ' ' }, r1)
|
|
592
|
+
assert(blkBlank.ok === false && blkBlank.code === 'V4_INVALID_ARGUMENT', 'a whitespace-only note is refused too')
|
|
593
|
+
const blk = await D.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-blk', note: '需要外层解析数论框架,本轮工作量不可接受' }, r1)
|
|
594
|
+
assert(blk.ok === true && blk.status === 'blocked', 'a reasoned blocker is recorded')
|
|
595
|
+
const stBlk = await D.callTool('vibe_v4_status', {})
|
|
596
|
+
assert(stBlk.formal.blocked.indexOf('p-blk') !== -1, 'status lists the blocked object')
|
|
597
|
+
assert(/需要外层解析数论框架/.test(readIf(join(D.projectRoot, 'Formal', 'Index.md'))), 'the blocker reason is written into the index')
|
|
598
|
+
|
|
599
|
+
// ---------- 8. the per-round `formal` reply channel ----------
|
|
600
|
+
section('8 the per-round `formal` reply judgement is recorded')
|
|
601
|
+
{
|
|
602
|
+
const w1 = await workWake(D, 'r-1')
|
|
603
|
+
D.fireEnd(w1.childId, { summary: '难度太高,本轮不做形式化。', formal: { target: 'p-reply', decision: 'blocked', note: '需要大量未形式化的实分析前置知识' }, contextPct: 20 })
|
|
604
|
+
await sleep(60)
|
|
605
|
+
const st = await D.callTool('vibe_v4_status', {})
|
|
606
|
+
assert(st.formal.blocked.indexOf('p-reply') !== -1, '★ a `formal.decision=blocked` reply is recorded as a blocker')
|
|
607
|
+
assert(/实分析前置知识/.test(readIf(join(D.projectRoot, 'Formal', 'Index.md'))), 'and its reason reaches the index')
|
|
608
|
+
}
|
|
609
|
+
{
|
|
610
|
+
const w2 = await workWake(D, 'r-2')
|
|
611
|
+
D.fireEnd(w2.childId, { summary: '我写了形式化草稿。', formal: { target: 'p-reply2', decision: 'used', file: 'Formal/p-reply2.lean' }, contextPct: 20 })
|
|
612
|
+
await sleep(60)
|
|
613
|
+
const st = await D.callTool('vibe_v4_status', {})
|
|
614
|
+
assert(st.formal.objects.some(o => o.target === 'p-reply2' && o.status === 'attempted'),
|
|
615
|
+
'a `formal.decision=used` reply records the object as ATTEMPTED with its file (no green run yet)')
|
|
616
|
+
}
|
|
617
|
+
{
|
|
618
|
+
const w3 = await workWake(D, 'r-1')
|
|
619
|
+
D.fireEnd(w3.childId, { summary: '不做。', formal: { target: 'p-reply3', decision: 'blocked' }, contextPct: 20 })
|
|
620
|
+
await sleep(60)
|
|
621
|
+
const st = await D.callTool('vibe_v4_status', {})
|
|
622
|
+
assert(!st.formal.objects.some(o => o.target === 'p-reply3'), 'a blocker with no note creates NO record')
|
|
623
|
+
assert(st.ok === true, 'the refusal did not crash the run')
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ---------- 9. the 'require' gate ----------
|
|
627
|
+
section("9 'require' withholds a verdict until the formal record exists")
|
|
628
|
+
const E = await establish()
|
|
629
|
+
await E.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
630
|
+
{
|
|
631
|
+
await proposeAndVote(E, 'p-gate', {
|
|
632
|
+
card: { id: 'p-gate', title: '门禁', statement: '必须形式化的命题', prob: 0.8, value: 0.6, motivation: 'm' },
|
|
633
|
+
proposal: { summary: '提议验证 p-gate。', solved: false, propose_verify: 'p-gate', contextPct: 20 },
|
|
634
|
+
verdict: 1,
|
|
635
|
+
})
|
|
636
|
+
const prompt = await E.prompts('verify', 'r-1', { target: 'p-gate', stage: 'independent' })
|
|
637
|
+
assert(/【Lean 形式化验证(强制模式)】/.test(prompt), 'the voting prompt says 强制模式')
|
|
638
|
+
assert(/必须产出 Lean 形式化/.test(prompt), "'require' states the formalization is mandatory")
|
|
639
|
+
assert(/本次裁定不会生效/.test(prompt), 'the prompt warns that the verdict will not take effect without it')
|
|
640
|
+
assert(!/可以不做/.test(prompt), "'require' does NOT offer the encourage-mode opt-out")
|
|
641
|
+
}
|
|
642
|
+
const gated = await E.callTool('vibe_v4_status', {})
|
|
643
|
+
assert(gated.verifyInProgress === false && gated.pendingVerify === null, 'the verification actually RAN to a settled state (so the next assertion is falsifiable)')
|
|
644
|
+
assert(!existsSync(join(E.projectRoot, 'Verified', '命题', 'p-gate.md')), '★ a unanimous TRUE verdict did NOT promote the object to Verified/')
|
|
645
|
+
assert(gated.formal.todo.indexOf('p-gate') !== -1, '★ it is recorded as 未定论 (a formalization TODO entry)')
|
|
646
|
+
assert(gated.formal.objects.some(o => o.target === 'p-gate' && o.status === 'none'), 'the object keeps a `none` formal record (never silently marked passed)')
|
|
647
|
+
assert(!/已验证·真/.test(readIf(join(E.projectRoot, 'Propos', 'r-1', 'p-gate.md'))), 'the source card was NOT rewritten to 已验证·真 (the conclusion was withheld, not taken)')
|
|
648
|
+
const todoE = readIf(join(E.projectRoot, 'Formal', 'TODO.md'))
|
|
649
|
+
assert(existsSync(join(E.projectRoot, 'Formal', 'TODO.md')), 'Formal/TODO.md was created')
|
|
650
|
+
assert(/p-gate/.test(todoE) && /formal-required/.test(todoE), '★ the object is on the formalization TODO with the machine-readable reason')
|
|
651
|
+
assert(!/已验证·真/.test(readIf(join(E.projectRoot, 'Shared', 'debates', 'p-gate.md'))), 'the debate record does NOT claim a 真 conclusion')
|
|
652
|
+
assert(/formal-required/.test(readIf(join(E.projectRoot, 'Formal', 'Index.md'))), 'the index carries the withheld verdict too')
|
|
653
|
+
// now formalize it and re-verify: the gate must open
|
|
654
|
+
const proofNow = await E.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-gate', content: 'theorem p_gate : 2 + 2 = 4 := by decide\n' }, E.resAgent(E.childOf('r-1')))
|
|
655
|
+
assert(proofNow.ok === true && proofNow.passed === true, 'the object is now Lean-passed')
|
|
656
|
+
{
|
|
657
|
+
await proposeAndVote(E, 'p-gate', {
|
|
658
|
+
card: { id: 'p-gate', title: '门禁', statement: '必须形式化的命题', prob: 0.8, value: 0.6, motivation: 'm' },
|
|
659
|
+
proposal: { summary: '已形式化,重新提议验证 p-gate。', solved: false, propose_verify: 'p-gate', contextPct: 20 },
|
|
660
|
+
verdict: 1,
|
|
661
|
+
})
|
|
662
|
+
const prompt = await E.prompts('verify', 'r-1', { target: 'p-gate', stage: 'independent' })
|
|
663
|
+
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(prompt), 'the re-verify prompt now points at the passing proof')
|
|
664
|
+
}
|
|
665
|
+
const gated2 = await E.callTool('vibe_v4_status', {})
|
|
666
|
+
assert(existsSync(join(E.projectRoot, 'Verified', '命题', 'p-gate.md')), '★ with a passing Lean artifact the same verdict DOES promote it')
|
|
667
|
+
const card = readIf(join(E.projectRoot, 'Verified', '命题', 'p-gate.md'))
|
|
668
|
+
assert(/- 形式化: Lean 通过/.test(card), '★ the Verified card records how strong the result is (Lean 通过)')
|
|
669
|
+
assert(/Verified\/Lean\/p-gate\.lean/.test(card), 'the card points at the archived proof')
|
|
670
|
+
assert(gated2.formal.todo.indexOf('p-gate') === -1, 'the satisfied TODO entry was cleared')
|
|
671
|
+
// the blocker escape hatch must also open the gate
|
|
672
|
+
await E.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-blocked-ok', note: '命题涉及未形式化的分析学,本轮不做' }, E.resAgent(E.childOf('r-1')))
|
|
673
|
+
await proposeAndVote(E, 'p-blocked-ok', {
|
|
674
|
+
card: { id: 'p-blocked-ok', title: '阻塞放行', statement: '记录阻塞后可定论', prob: 0.8, value: 0.6, motivation: 'm' },
|
|
675
|
+
proposal: { summary: '提议验证 p-blocked-ok。', solved: false, propose_verify: 'p-blocked-ok', contextPct: 20 },
|
|
676
|
+
verdict: 1,
|
|
677
|
+
})
|
|
678
|
+
assert(existsSync(join(E.projectRoot, 'Verified', '命题', 'p-blocked-ok.md')), '★ an explicit reasoned blocker also lets the verdict through (decide by difficulty, but decide out loud)')
|
|
679
|
+
assert(/- 形式化: 阻塞(/.test(readIf(join(E.projectRoot, 'Verified', '命题', 'p-blocked-ok.md'))), 'the card records the blocker')
|
|
680
|
+
assert((await E.callTool('vibe_v4_status', {})).formal.blocked.indexOf('p-blocked-ok') !== -1, 'status lists it as blocked')
|
|
681
|
+
// a `require`-mode FALSE verdict must be gated exactly the same way (the gate sits on the choke
|
|
682
|
+
// point, not on the 真 branch): assert the tally was recorded BEFORE asserting it was withheld.
|
|
683
|
+
await proposeAndVote(E, 'p-gate-false', {
|
|
684
|
+
card: { id: 'p-gate-false', title: '假也要门禁', statement: '一个会被一致判假的命题', prob: 0.1, value: 0.6, motivation: 'm' },
|
|
685
|
+
proposal: { summary: '提议验证 p-gate-false。', solved: false, propose_verify: 'p-gate-false', contextPct: 20 },
|
|
686
|
+
verdict: 0,
|
|
687
|
+
})
|
|
688
|
+
const gatedF = await E.callTool('vibe_v4_status', {})
|
|
689
|
+
assert(!existsSync(join(E.projectRoot, 'Verified', '命题', 'p-gate-false.md')), '★ a unanimous FALSE verdict is gated too')
|
|
690
|
+
assert(gatedF.formal.todo.indexOf('p-gate-false') !== -1, 'the false verdict is recorded as 未定论 as well')
|
|
691
|
+
assert(!/已验证·假/.test(readIf(join(E.projectRoot, 'Propos', 'r-1', 'p-gate-false.md'))), 'the source card was not rewritten to 已验证·假')
|
|
692
|
+
|
|
693
|
+
// ---------- 10. 'encourage' does NOT gate ----------
|
|
694
|
+
section("10 'encourage' has no gate (the verdict still closes)")
|
|
695
|
+
const F = await establish()
|
|
696
|
+
await F.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
697
|
+
await proposeAndVote(F, 'p-enc-ok', {
|
|
698
|
+
card: { id: 'p-enc-ok', title: '鼓励不门禁', statement: '鼓励模式下无形式化产物也能定论', prob: 0.8, value: 0.6, motivation: 'm' },
|
|
699
|
+
proposal: { summary: '提议验证 p-enc-ok。', solved: false, propose_verify: 'p-enc-ok', contextPct: 20 },
|
|
700
|
+
verdict: 1,
|
|
701
|
+
})
|
|
702
|
+
assert(existsSync(join(F.projectRoot, 'Verified', '命题', 'p-enc-ok.md')), "★ 'encourage' imposes NO gate: the verdict closes with no Lean artifact")
|
|
703
|
+
assert(/- 形式化: 未尝试/.test(readIf(join(F.projectRoot, 'Verified', '命题', 'p-enc-ok.md'))), 'the card still states the object was never formalized')
|
|
704
|
+
|
|
705
|
+
// ---------- 11. reporting ----------
|
|
706
|
+
section('11 the host can audit formal strength')
|
|
707
|
+
const repF = await F.callTool('vibe_v4_formal_report', {}, F.ROOT)
|
|
708
|
+
assert(/## Lean 形式化/.test(repF.formalReport), 'the formal report has a Lean section')
|
|
709
|
+
assert(/模式:encourage/.test(repF.formalReport), 'it states the mode')
|
|
710
|
+
const repOff = await A.callTool('vibe_v4_formal_report', {}, A.ROOT)
|
|
711
|
+
assert(/未启用/.test(repOff.formalReport), 'in off mode the report says the feature is not enabled')
|
|
712
|
+
const repE = await E.callTool('vibe_v4_report', {}, E.ROOT)
|
|
713
|
+
assert(repE.formal && repE.formal.passed.indexOf('p-gate') !== -1, 'the structured report exposes the Lean-passed objects')
|
|
714
|
+
assert(repE.formal.blocked.indexOf('p-blocked-ok') !== -1, 'and the blocked objects')
|
|
715
|
+
const reportE = await E.callTool('vibe_v4_formal_report', {}, E.ROOT)
|
|
716
|
+
assert(/已通过:.*p-gate/.test(reportE.formalReport), 'the human report lists Lean-passed objects')
|
|
717
|
+
assert(/已记录阻塞:.*p-blocked-ok/.test(reportE.formalReport), 'the human report lists blocked objects')
|
|
718
|
+
assert(/形式化待办:.*p-gate-false/.test(reportE.formalReport), 'the human report lists the formalization TODO')
|
|
719
|
+
|
|
720
|
+
// ===============================================================
|
|
721
|
+
// 12. the fidelity rule + the prompt hard requirements (docs §6, §10 items 9/11)
|
|
722
|
+
// The v2 lesson (docs §10 item 8) is why every claim here is paired with a BEHAVIOURAL
|
|
723
|
+
// assertion in §13/§14: wording alone guards nothing.
|
|
724
|
+
// ===============================================================
|
|
725
|
+
section('12 the injected text states the fidelity rule and never abbreviates a tool name')
|
|
726
|
+
const G = await establish()
|
|
727
|
+
await G.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
728
|
+
{
|
|
729
|
+
const enc = await G.prompts('verify', 'r-1', { target: 'p-text', stage: 'independent' })
|
|
730
|
+
assert(/【Lean 形式化验证(鼓励模式)】/.test(enc), 'the encourage voting prompt keeps its header')
|
|
731
|
+
assert(/实现难度/.test(enc), 'it still asks for the implementation-difficulty judgement')
|
|
732
|
+
assert(/工具:vibe_v4_lean_run(执行)· vibe_v4_lean_archive(归档)· vibe_v4_lean_lib(查已有可复用库)/.test(enc), 'it names all three tools in FULL')
|
|
733
|
+
assert(/归档可复用定义\/引理前先跑通(vibe_v4_lean_archive run=true 或先 vibe_v4_lean_run);跑不通不要入库。/.test(enc), '★ a reusable definition/lemma must be RUN GREEN before it is archived')
|
|
734
|
+
assert(/宿主没有 Lean 工具链(LEAN_NOT_FOUND)或根本没有 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并在回执的 note 里写明"宿主无 Lean 工具链"/.test(enc), '★ both unavailable-toolchain paths are written out (missing Lean OR no subprocess service) as explicit blocker reasons')
|
|
735
|
+
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(enc), 'a green Lean run still shrinks the open question to fidelity')
|
|
736
|
+
assert(/decision='blocked' 时必须写明 note/.test(enc), 'the encourage opt-out documents the mandatory note')
|
|
737
|
+
assert(!/偏离 → 0/.test(enc) && !/发现任何偏离/.test(enc), '★ no "deviation ⇒ 0" instruction anywhere in the encourage block')
|
|
738
|
+
await G.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
739
|
+
const req = await G.prompts('verify', 'r-1', { target: 'p-text', stage: 'independent' })
|
|
740
|
+
assert(/【Lean 形式化验证(强制模式)】/.test(req), 'the require voting prompt says 强制模式')
|
|
741
|
+
assert(/必须产出 Lean 形式化/.test(req) && /必须\*\*给出显式的阻塞原因/.test(req), "'require' states the formalization OR an explicit blocker is mandatory")
|
|
742
|
+
assert(/本次裁定不会生效/.test(req) && /进入「形式化待办」/.test(req), 'it warns the verdict is withheld as 未定论 (formal-required)')
|
|
743
|
+
assert(!/可以不做/.test(req), "'require' does NOT offer the encourage-mode opt-out")
|
|
744
|
+
assert(/归档可复用定义\/引理前先跑通/.test(req) && /宿主没有 Lean 工具链/.test(req), 'the run-before-archive and toolchain rules are in the require text as well')
|
|
745
|
+
assert(/LEAN_NOT_FOUND/.test(req) && /NO_SUBPROCESS/.test(req), '★ the require text names BOTH unavailable-toolchain codes (the escape route must be documented in every mode)')
|
|
746
|
+
// The sentence is written ONCE and reused by both modes: a second copy would drift (one branch
|
|
747
|
+
// kept up to date, the other stale). The anchor `(LEAN_NOT_FOUND)` therefore occurs exactly once
|
|
748
|
+
// in the plugin, which is also what the sensitivity probe table assumes.
|
|
749
|
+
{
|
|
750
|
+
const src = readFileSync(fileURLToPath(PLUGIN), 'utf8')
|
|
751
|
+
assert(src.split('(LEAN_NOT_FOUND)').length - 1 === 1, '★ the unavailable-toolchain guidance is written once (not duplicated per mode/branch)')
|
|
752
|
+
assert(src.split('(NO_SUBPROCESS)').length - 1 === 1, 'the same single sentence names NO_SUBPROCESS (no per-branch copy)')
|
|
753
|
+
}
|
|
754
|
+
assert(/\*\*本模式要求\*\*/.test(req), 'the require bullet replaces the encourage opt-out in place')
|
|
755
|
+
// normal / heartbeat / post-compact recap all carry the standing work line; the two that ARE a
|
|
756
|
+
// reply contract also document the `defect` decision (coreRules is a recap prefix, not a contract)
|
|
757
|
+
for (const [label, text] of [['normal', await G.prompts('normal', 'r-1')], ['heartbeat', await G.prompts('heartbeat', 'r-1')], ['coreRules', await G.prompts('coreRules', 'r-1')]]) {
|
|
758
|
+
assert(/【顺手形式化(强制)】/.test(text), label + ': the standing formalization line uses the require wording')
|
|
759
|
+
assert(/归档前先跑通(vibe_v4_lean_run 或 run=true);跑不通的定义不要进可复用库。/.test(text), label + ': ★ it requires a green run before archiving a reusable definition')
|
|
760
|
+
if (label !== 'coreRules') assert(/"decision":"used\|blocked\|defect"/.test(text), label + ': the reply contract documents the `defect` decision')
|
|
761
|
+
}
|
|
762
|
+
assert(/"decision":"used\|blocked\|defect"/.test(req) && /具体偏差/.test(req), '★ the voting reply contract documents decision=defect and its note')
|
|
763
|
+
}
|
|
764
|
+
{
|
|
765
|
+
// ---- the sweep: EVERY agent-facing string must spell the three tools in full (docs §6-1) ----
|
|
766
|
+
const scanned = []
|
|
767
|
+
const addText = (label, t) => { if (typeof t === 'string' && t) scanned.push({ label, text: t }) }
|
|
768
|
+
addText('captured run prompts', G.allPrompts())
|
|
769
|
+
for (const which of ['brainstorm', 'normal', 'heartbeat', 'coreRules']) addText(which, await G.prompts(which, 'r-1'))
|
|
770
|
+
for (const t of ['p-text', 'p-corpus']) for (const st of ['independent', 'debate']) addText('verify:' + t + ':' + st, await G.prompts('verify', 'r-1', { target: t, stage: st }))
|
|
771
|
+
// tool hints are injected text too (docs §6 hard requirement 1 names them explicitly)
|
|
772
|
+
const r1G = G.resAgent(G.childOf('r-1'))
|
|
773
|
+
const hintGreen = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-hint', content: 'theorem p_hint : 1 = 1 := rfl\n' }, r1G)
|
|
774
|
+
assert(hintGreen.ok === true && hintGreen.passed === true, 'precondition: a green file exists for the hint sweep')
|
|
775
|
+
const hintRed = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-hint-red', content: 'theorem p_hint_red : 1 = 2 := by sorry\n' }, r1G)
|
|
776
|
+
assert(hintRed.ok === true && hintRed.passed === false, 'precondition: a red file exists for the hint sweep')
|
|
777
|
+
addText('lean_run hint (green)', (await G.callTool('vibe_v4_lean_run', { file: 'Formal/p-hint.lean' }, r1G)).hint)
|
|
778
|
+
addText('lean_run hint (red)', (await G.callTool('vibe_v4_lean_run', { file: 'Formal/p-hint-red.lean' }, r1G)).hint)
|
|
779
|
+
addText('lean_lib hint', (await G.callTool('vibe_v4_lean_lib', {}, r1G)).hint)
|
|
780
|
+
toolchainAvailable = false
|
|
781
|
+
addText('LEAN_NOT_FOUND message', (await G.callTool('vibe_v4_lean_run', { file: 'Formal/p-hint.lean' }, r1G)).message)
|
|
782
|
+
toolchainAvailable = true
|
|
783
|
+
for (const t of G.toolRegs.filter((x) => /lean/.test(x.name))) addText('tool description ' + t.name, t.description)
|
|
784
|
+
// The activity log is agent/host-readable text too (vibe_v4_report exposes `recentActivity`, and
|
|
785
|
+
// a host reads it out to the group): a bare abbreviation there names a tool that does not exist,
|
|
786
|
+
// for exactly the same reason as in a prompt.
|
|
787
|
+
try {
|
|
788
|
+
const log = JSON.parse(readIf(join(G.projectRoot, 'State', 'session.json')) || '{}').activityLog || []
|
|
789
|
+
for (const e of log) addText('activity log: ' + e.event, String((e && e.detail) || ''))
|
|
790
|
+
} catch (e) { /* the log is best-effort */ }
|
|
791
|
+
const bare = [/(^|[^a-z_])lean_run/, /(^|[^a-z_])lean_archive/, /(^|[^a-z_])lean_lib/]
|
|
792
|
+
const offenders = []
|
|
793
|
+
for (const s of scanned) for (const re of bare) if (re.test(s.text)) offenders.push(s.label + ' :: ' + re.source)
|
|
794
|
+
assert(offenders.length === 0, '★ no injected text (prompt, tool hint or tool description) uses a bare tool abbreviation (' + offenders.slice(0, 3).join(' | ') + ')')
|
|
795
|
+
assert(scanned.length >= 15, 'the sweep really covered the injected-text surface (' + scanned.length + ' texts)')
|
|
796
|
+
}
|
|
797
|
+
{
|
|
798
|
+
// docs §4.1-3 / §6.1: the fidelity branch's "the framework withholds the verdict" clause may only
|
|
799
|
+
// appear in `require` (the only mode with a gate). In `encourage` the framework still withdraws
|
|
800
|
+
// the proof and records the TODO, but it CANNOT hold the ballot — so the text must not promise
|
|
801
|
+
// that, and must instead point at the voter's own abstention.
|
|
802
|
+
await G.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
803
|
+
const reqFid = await G.prompts('verify', 'r-1', { target: 'p-hint', stage: 'independent' })
|
|
804
|
+
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(reqFid), 'precondition: p-hint is the passed/fidelity case in this prompt')
|
|
805
|
+
assert(/本次裁定\*\*不定论\*\*/.test(reqFid), "'require' fidelity text states the verdict will be withheld (it has a gate)")
|
|
806
|
+
await G.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
807
|
+
const encFid = await G.prompts('verify', 'r-1', { target: 'p-hint', stage: 'independent' })
|
|
808
|
+
assert(!/本次裁定\*\*不定论\*\*/.test(encFid), "★ 'encourage' does NOT promise a hold it cannot enforce (docs §4.1-3)")
|
|
809
|
+
assert(/本档没有门禁/.test(encFid) && /弃权值/.test(encFid), "★ instead it says the voter's own abstention is what keeps the ballot from concluding")
|
|
810
|
+
assert(/降级为 attempted、删除归档证明、写入形式化待办/.test(encFid), 'the withdrawal the framework CAN enforce is still stated in both modes')
|
|
811
|
+
assert(/发现任何偏差,不要投 0/.test(encFid), 'the fidelity rule itself is mode-independent')
|
|
812
|
+
await G.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ===============================================================
|
|
816
|
+
// 13. the `defect` reply channel (docs §4.1 / §10 item 8) — BEHAVIOURAL, not wording
|
|
817
|
+
// ===============================================================
|
|
818
|
+
section('13 the `defect` reply withdraws a passing proof (spec §4.1)')
|
|
819
|
+
await G.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
820
|
+
const activityOf = (h) => { try { return JSON.parse(readIf(join(h.projectRoot, 'State', 'session.json')) || '{}').activityLog || [] } catch (e) { return [] } }
|
|
821
|
+
const g1 = G.resAgent(G.childOf('r-1'))
|
|
822
|
+
{
|
|
823
|
+
const arc = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-defect', content: 'theorem p_defect : 2 + 2 = 4 := by decide\n' }, g1)
|
|
824
|
+
assert(arc.passed === true && existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-defect.lean')), 'precondition: p-defect has a green archived proof')
|
|
825
|
+
const w = await workWake(G, 'r-1')
|
|
826
|
+
G.fireEnd(w.childId, { summary: '我逐条核对了 Lean 代码,发现偏差。', formal: { target: 'p-defect', decision: 'defect', note: 'Lean 里的条件比命题弱:只证了 n ≥ 1 的情形' }, contextPct: 20 })
|
|
827
|
+
await sleep(90)
|
|
828
|
+
const st = await G.callTool('vibe_v4_status', {})
|
|
829
|
+
const rec = st.formal.objects.find((o) => o.target === 'p-defect')
|
|
830
|
+
assert(rec && rec.status === 'attempted', '★ a `formal.decision=defect` reply downgrades the object to attempted (never a refutation)')
|
|
831
|
+
assert(rec && rec.proof === '', '★ the archived proof is cleared from the record')
|
|
832
|
+
assert(rec && rec.note === 'Lean 里的条件比命题弱:只证了 n ≥ 1 的情形', 'the concrete deviation is stored as the record note')
|
|
833
|
+
assert(!existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-defect.lean')), '★ the archived proof file is DELETED (the formalization is 不合格 — the proposition is NOT false)')
|
|
834
|
+
assert(existsSync(join(G.projectRoot, 'Formal', 'p-defect.lean')), 'the WORKING file is kept — the code is not lost, only its "passed" claim')
|
|
835
|
+
assert(st.formal.passed.indexOf('p-defect') === -1, 'the object is no longer reported as Lean-passed')
|
|
836
|
+
assert(shellCalls.some((c) => /Remove-Item|^rm -f/.test(c) && /p-defect\.lean/.test(c)), 'the withdrawal really went through the platform shell (the fs service has no delete)')
|
|
837
|
+
const todo = readIf(join(G.projectRoot, 'Formal', 'TODO.md'))
|
|
838
|
+
assert(/p-defect/.test(todo) && /formal-defect/.test(todo), '★ Formal/TODO.md lists the object with the DEFECT reason')
|
|
839
|
+
assert(/只证了 n ≥ 1 的情形/.test(todo), '★ the concrete deviation reaches the human-readable TODO')
|
|
840
|
+
const idx = readIf(join(G.projectRoot, 'Formal', 'Index.md'))
|
|
841
|
+
assert(/\| p-defect \| attempted \|/.test(idx), 'Formal/Index.md downgrades the object to attempted')
|
|
842
|
+
assert(/只证了 n ≥ 1 的情形/.test(idx), 'the deviation is the record note in the index')
|
|
843
|
+
assert(activityOf(G).some((e) => /忠实性缺陷/.test(e.detail) && /p-defect/.test(e.detail)), '★ the retraction is announced in the activity log')
|
|
844
|
+
// the log line is agent/host-readable text, so it is bound by the same §4.1-3 rule as the prompt:
|
|
845
|
+
// in `encourage` it may not claim a hold the mode does not have.
|
|
846
|
+
{
|
|
847
|
+
const line = activityOf(G).filter((e) => /忠实性缺陷/.test(e.detail) && /p-defect[^-]/.test(e.detail)).pop() || { detail: '' }
|
|
848
|
+
assert(/本档没有门禁/.test(line.detail) && !/本次裁定\*\*不定论\*\*/.test(line.detail), "★ the encourage-mode activity line does not promise the 不定论 hold that only `require` enforces")
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
{
|
|
852
|
+
// §4.1 degraded withdrawal: the record must never be the ONLY thing withdrawn. A host whose shell
|
|
853
|
+
// "succeeds" without deleting (a stub host / a permissions quirk) must be caught by re-reading the
|
|
854
|
+
// file through the fs service, and the archive must then be OVERWRITTEN with a withdrawal notice —
|
|
855
|
+
// otherwise the retracted proof keeps sitting at the exact path everyone looks for the proof.
|
|
856
|
+
const arcD = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-defect-degraded', content: 'theorem p_defect_degraded : 5 = 5 := rfl\n' }, g1)
|
|
857
|
+
assert(arcD.passed === true && existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-defect-degraded.lean')), 'precondition: the degraded-path object has a green archived proof')
|
|
858
|
+
shellDeletesFiles = false
|
|
859
|
+
const wD = await workWake(G, 'r-1')
|
|
860
|
+
G.fireEnd(wD.childId, { summary: '核对后发现偏差。', formal: { target: 'p-defect-degraded', decision: 'defect', note: '结论方向相反' }, contextPct: 20 })
|
|
861
|
+
await sleep(90)
|
|
862
|
+
shellDeletesFiles = true
|
|
863
|
+
const body = readIf(join(G.projectRoot, 'Verified', 'Lean', 'p-defect-degraded.lean'))
|
|
864
|
+
assert(!/p_defect_degraded/.test(body), '★ a shell that exits 0 without deleting does NOT leave the retracted proof readable — its original text is gone')
|
|
865
|
+
assert(/已撤回/.test(body) && /原代码保留在工作文件/.test(body), '★ the archived file was OVERWRITTEN with an explicit withdrawal notice instead (delete → confirm → overwrite)')
|
|
866
|
+
const recD = (await G.callTool('vibe_v4_status', {})).formal.objects.find((o) => o.target === 'p-defect-degraded')
|
|
867
|
+
assert(recD && recD.status === 'attempted' && recD.proof === '', 'the record is downgraded on the degraded path too')
|
|
868
|
+
assert(activityOf(G).some((e) => /覆盖归档证明/.test(e.detail) && /p-defect-degraded/.test(e.detail)), '★ the activity log says WHICH withdrawal path was taken (overwritten, not silently reported as deleted)')
|
|
869
|
+
}
|
|
870
|
+
{
|
|
871
|
+
const arc2 = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-defect2', content: 'theorem p_defect2 : 3 + 3 = 6 := by decide\n' }, g1)
|
|
872
|
+
assert(arc2.passed === true, 'precondition: p-defect2 is Lean-passed')
|
|
873
|
+
const w2 = await workWake(G, 'r-2')
|
|
874
|
+
G.fireEnd(w2.childId, { summary: '有偏差,但没写清是什么。', formal: { target: 'p-defect2', decision: 'defect' }, contextPct: 20 })
|
|
875
|
+
await sleep(90)
|
|
876
|
+
const st2 = await G.callTool('vibe_v4_status', {})
|
|
877
|
+
const rec2 = st2.formal.objects.find((o) => o.target === 'p-defect2')
|
|
878
|
+
assert(rec2 && rec2.status === 'passed' && rec2.proof === 'Verified/Lean/p-defect2.lean', '★ a `defect` reply WITHOUT a note is REJECTED — the record is untouched')
|
|
879
|
+
assert(existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-defect2.lean')), '★ and the archived proof is NOT withdrawn')
|
|
880
|
+
assert(activityOf(G).some((e) => /defect/.test(e.detail) && /V4_INVALID_ARGUMENT/.test(e.detail)), '★ the rejection carries the preset error code (V4_INVALID_ARGUMENT), not a silent drop')
|
|
881
|
+
assert((await G.callTool('vibe_v4_status', {})).ok === true, 'the refusal did not crash the run')
|
|
882
|
+
}
|
|
883
|
+
{
|
|
884
|
+
const blk = await G.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-defect3', note: '前置知识未形式化,本轮不做' }, g1)
|
|
885
|
+
assert(blk.ok === true && blk.status === 'blocked', 'precondition: p-defect3 carries a reasoned blocker record')
|
|
886
|
+
const w3 = await workWake(G, 'r-1')
|
|
887
|
+
G.fireEnd(w3.childId, { summary: '复核后发现归档的形式化换了对象。', formal: { target: 'p-defect3', decision: 'defect', note: '归档的形式化证的是特例,换了对象' }, contextPct: 20 })
|
|
888
|
+
await sleep(90)
|
|
889
|
+
const st3 = await G.callTool('vibe_v4_status', {})
|
|
890
|
+
const rec3 = st3.formal.objects.find((o) => o.target === 'p-defect3')
|
|
891
|
+
assert(rec3 && rec3.status === 'attempted', '★ `defect` downgrades even a `blocked` record (spec §4.1: ALWAYS downgrade)')
|
|
892
|
+
assert(st3.formal.blocked.indexOf('p-defect3') === -1, '★ a bad formalization may not stay in the gate-passing `blocked` state')
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ===============================================================
|
|
896
|
+
// 14. `require` after a defect: the retraction closes the gate (docs §4.1-3 / §10 item 9)
|
|
897
|
+
// ===============================================================
|
|
898
|
+
// 13b. a plain `used` judgement must NOT withdraw an ESTABLISHED proof (contract §4): only a
|
|
899
|
+
// fidelity defect retracts one. v2 shipped the unconditional downgrade and silently re-closed the gate.
|
|
900
|
+
section('13b a \`used\` reply must NOT downgrade an already-passed object')
|
|
901
|
+
{
|
|
902
|
+
const arcU = await G.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-usedkeep', content: 'theorem p_usedkeep : 2 + 2 = 4 := by decide\n' }, g1)
|
|
903
|
+
assert(arcU.passed === true && existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-usedkeep.lean')), 'used-keep: the object starts with a green archived proof')
|
|
904
|
+
const wU = await workWake(G, 'r-1')
|
|
905
|
+
G.fireEnd(wU.childId, { summary: '这一轮只是又写了一遍草稿。', formal: { target: 'p-usedkeep', decision: 'used', file: 'Formal/p-usedkeep.lean' }, contextPct: 20 })
|
|
906
|
+
await sleep(90)
|
|
907
|
+
const stU = await G.callTool('vibe_v4_status', {})
|
|
908
|
+
const recU = stU.formal.objects.find((o) => o.target === 'p-usedkeep')
|
|
909
|
+
assert(!!recU && recU.status === 'passed', '★ a `used` reply does NOT downgrade an already-passed object (got ' + JSON.stringify(recU) + ')')
|
|
910
|
+
assert(!!recU && recU.proof === 'Verified/Lean/p-usedkeep.lean', '★ and the proof pointer survives the reply')
|
|
911
|
+
assert(existsSync(join(G.projectRoot, 'Verified', 'Lean', 'p-usedkeep.lean')), '★ and the archived proof is still on disk')
|
|
912
|
+
assert(stU.formal.passed.indexOf('p-usedkeep') !== -1, '★ status still reports it as Lean-passed')
|
|
913
|
+
const rerunU = await G.callTool('vibe_v4_lean_run', { file: 'Formal/p-usedkeep.lean', target: 'p-usedkeep' }, g1)
|
|
914
|
+
assert(rerunU.ok === true, 'used-keep: the work file runs green')
|
|
915
|
+
const recU2 = (await G.callTool('vibe_v4_status', {})).formal.objects.find((o) => o.target === 'p-usedkeep')
|
|
916
|
+
assert(!!recU2 && recU2.status === 'passed', '★ a plain re-run does not downgrade a passed record (contract §4)')
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
section("14 'require' withholds the verdict after a defect, even on a unanimous 1")
|
|
920
|
+
const H = await establish()
|
|
921
|
+
await H.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
922
|
+
{
|
|
923
|
+
const h1 = H.resAgent(H.childOf('r-1'))
|
|
924
|
+
const arc = await H.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-defect-req', content: 'theorem p_defect_req : 4 * 1 ^ 2 - 2 = (1:Nat) ^ 2 := by decide\n' }, h1)
|
|
925
|
+
assert(arc.passed === true, 'precondition: p-defect-req is Lean-passed')
|
|
926
|
+
const before = await H.prompts('verify', 'r-1', { target: 'p-defect-req', stage: 'independent' })
|
|
927
|
+
assert(/不要投 0/.test(before) && /formal:\{decision:'defect'/.test(before), 'the voter is told (before voting) how to report a fidelity defect')
|
|
928
|
+
await H.callTool('vibe_v4_record_proposition', { id: 'p-defect-req', title: '缺陷不是证伪', statement: '形式化写窄了不代表命题为假', prob: 0.9, value: 0.6, motivation: 'm' }, h1)
|
|
929
|
+
const n0 = H.followups.length
|
|
930
|
+
await H.callTool('vibe_v4_message', { to: 'r-1', content: '请处理本轮工作。' })
|
|
931
|
+
for (let i = 0; i < 300 && H.followups.length === n0; i++) await sleep(10)
|
|
932
|
+
// r-1 reports the defect AND votes 1 in the SAME reply; r-2 votes 1. The framework must NOT take
|
|
933
|
+
// the unanimous 1: the formalization was just retracted, so the verdict has to be deferred.
|
|
934
|
+
const replyFor = (pt, rId) => (/团队验证/.test(pt)
|
|
935
|
+
? (rId === 'r-1'
|
|
936
|
+
? { vote: { verdict: 1, reason: '我发现形式化写窄了,但独立看命题仍为真' }, formal: { target: 'p-defect-req', decision: 'defect', note: 'Lean 只证了 x=1 的特例,命题要求所有整数 x' } }
|
|
937
|
+
: { vote: { verdict: 1, reason: '独立复核为真' } })
|
|
938
|
+
: { summary: '提议验证 p-defect-req。', solved: false, propose_verify: 'p-defect-req', contextPct: 20 })
|
|
939
|
+
for (let i = 0; i < 4; i++) { await drive(H, replyFor, verifySettled(H)); if (await verifySettled(H)()) break }
|
|
940
|
+
const st = await H.callTool('vibe_v4_status', {})
|
|
941
|
+
assert(st.verifyInProgress === false && st.pendingVerify === null, 'the verification actually settled (so the next assertion is falsifiable)')
|
|
942
|
+
assert(!existsSync(join(H.projectRoot, 'Verified', '命题', 'p-defect-req.md')), '★ a defect in `require` mode writes NO Verified card, even with a unanimous 1')
|
|
943
|
+
assert(st.formal.todo.indexOf('p-defect-req') !== -1, '★ the object stays 未定论 on the formalization TODO')
|
|
944
|
+
const rec = st.formal.objects.find((o) => o.target === 'p-defect-req')
|
|
945
|
+
assert(rec && rec.status === 'attempted' && rec.proof === '', 'the formal record is the retracted one (attempted, no proof)')
|
|
946
|
+
assert(rec && /只证了 x=1 的特例/.test(rec.note || ''), "the record's note is the reported deviation")
|
|
947
|
+
assert(!existsSync(join(H.projectRoot, 'Verified', 'Lean', 'p-defect-req.lean')), '★ the archived proof was withdrawn as part of the retraction')
|
|
948
|
+
assert(!/已验证·真/.test(readIf(join(H.projectRoot, 'Propos', 'r-1', 'p-defect-req.md'))), 'the source card was NOT rewritten to 已验证·真 (the conclusion was withheld, not taken)')
|
|
949
|
+
assert(!/已验证·真/.test(readIf(join(H.projectRoot, 'Shared', 'debates', 'p-defect-req.md'))), 'the debate record does not claim a 真 conclusion')
|
|
950
|
+
const todo = readIf(join(H.projectRoot, 'Formal', 'TODO.md'))
|
|
951
|
+
assert(/p-defect-req/.test(todo) && /formal-defect/.test(todo), '★ the TODO keeps the DEFECT reason, not merely formal-required')
|
|
952
|
+
assert(/只证了 x=1 的特例/.test(todo), 'the concrete deviation is what a human reads in the TODO')
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ===============================================================
|
|
956
|
+
// 15. the shipped Lean prompt corpus (docs §10 item 10) — a HUMAN must be able to re-read the
|
|
957
|
+
// exact text, not just the assertions about it.
|
|
958
|
+
// ===============================================================
|
|
959
|
+
section('15 the captured Lean prompt corpus is written for human review')
|
|
960
|
+
const K = await establish()
|
|
961
|
+
{
|
|
962
|
+
const vibe = K.vibeRoot
|
|
963
|
+
// Normalise BOTH slash forms, and the VibeMath root FIRST: it sits INSIDE the workspace, so
|
|
964
|
+
// replacing the workspace first would leave `<WS>/VibeMath` instead of `<VIBEMATH>`.
|
|
965
|
+
const scrub = (s) => String(s == null ? '' : s)
|
|
966
|
+
.split(vibe).join('<VIBEMATH>').split(vibe.replace(/\\/g, '/')).join('<VIBEMATH>')
|
|
967
|
+
.split(K.WS).join('<WS>').split(K.WS.replace(/\\/g, '/')).join('<WS>')
|
|
968
|
+
const corpus = []
|
|
969
|
+
const add = (kind, label, prompt) => corpus.push({ kind, label, prompt: scrub(prompt) })
|
|
970
|
+
// (a) off: a TRUE no-op must be visible in the corpus, not merely asserted
|
|
971
|
+
add('verify', 'off/verify', await K.prompts('verify', 'r-1', { target: 'p-corpus', stage: 'independent' }))
|
|
972
|
+
add('work', 'off/normal', await K.prompts('normal', 'r-1'))
|
|
973
|
+
// (b) encourage
|
|
974
|
+
await K.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
975
|
+
add('verify', 'encourage/verify', await K.prompts('verify', 'r-1', { target: 'p-corpus', stage: 'independent' }))
|
|
976
|
+
const encNormal = await K.prompts('normal', 'r-1')
|
|
977
|
+
add('work', 'encourage/normal', encNormal)
|
|
978
|
+
add('work', 'encourage/heartbeat', await K.prompts('heartbeat', 'r-1'))
|
|
979
|
+
add('work', 'encourage/coreRules', await K.prompts('coreRules', 'r-1'))
|
|
980
|
+
// (c) require
|
|
981
|
+
await K.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
982
|
+
add('verify', 'require/verify', await K.prompts('verify', 'r-1', { target: 'p-corpus', stage: 'independent' }))
|
|
983
|
+
add('verify', 'require/verify/debate', await K.prompts('verify', 'r-1', { target: 'p-corpus', stage: 'debate' }))
|
|
984
|
+
add('work', 'require/normal', await K.prompts('normal', 'r-1'))
|
|
985
|
+
// (d) a PASSED object: the review subject has changed to fidelity
|
|
986
|
+
const k1 = K.resAgent(K.childOf('r-1'))
|
|
987
|
+
await K.callTool('vibe_v4_lean_archive', { kind: 'proof', target: 'p-corpus-passed', content: 'theorem p_corpus_passed : 1 + 1 = 2 := by decide\n' }, k1)
|
|
988
|
+
const fid = await K.prompts('verify', 'r-1', { target: 'p-corpus-passed', stage: 'independent' })
|
|
989
|
+
add('verify', 'passed/fidelity', fid)
|
|
990
|
+
// (d2) the SAME passed object in `encourage`: the promise must shrink to what that mode enforces
|
|
991
|
+
// (docs §4.1-3: no gate there, so no claim that the framework withholds the verdict).
|
|
992
|
+
await K.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
993
|
+
const fidEnc = await K.prompts('verify', 'r-1', { target: 'p-corpus-passed', stage: 'independent' })
|
|
994
|
+
add('verify', 'passed/fidelity (encourage)', fidEnc)
|
|
995
|
+
await K.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
996
|
+
// (e) a BLOCKED object
|
|
997
|
+
await K.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-corpus-blocked', note: '需要未形式化的解析数论框架' }, k1)
|
|
998
|
+
add('verify', 'blocked/verify', await K.prompts('verify', 'r-1', { target: 'p-corpus-blocked', stage: 'independent' }))
|
|
999
|
+
// (f) the `formal` reply contract line itself (the field whose parsing §13/§14 prove)
|
|
1000
|
+
const contractLine = (t) => { const m = String(t).match(/"formal":\{[^\n]*\}\}/); return m ? m[0] : '' }
|
|
1001
|
+
add('contract', 'formal reply contract (voting prompt)', contractLine(fid))
|
|
1002
|
+
add('contract', 'formal reply contract (work prompt)', contractLine(encNormal))
|
|
1003
|
+
// (g) a REAL delivered work wake (proves the builder is the one actually used to address a resident)
|
|
1004
|
+
const w = await workWake(K, 'r-2')
|
|
1005
|
+
add('work', 'require/real work wake', promptOf(w))
|
|
1006
|
+
K.fireEnd(w.childId, { summary: '继续推进。', solved: false, contextPct: 20 })
|
|
1007
|
+
await sleep(40)
|
|
1008
|
+
// (h) the tool hints are injected text too (docs §6 hard requirement 1)
|
|
1009
|
+
add('hint', 'lean_run hint (green)', (await K.callTool('vibe_v4_lean_run', { file: 'Formal/p-corpus-passed.lean' }, k1)).hint)
|
|
1010
|
+
add('hint', 'lean_lib hint', (await K.callTool('vibe_v4_lean_lib', {}, k1)).hint)
|
|
1011
|
+
|
|
1012
|
+
// The corpus is REGENERATED on every run. Dumping it while V4_PLUGIN points at a MUTATED copy
|
|
1013
|
+
// would let a sensitivity probe overwrite the SHIPPED corpus with mutated text (the probe's job
|
|
1014
|
+
// is to run this suite against a broken plugin), so the dump is skipped then — unless the probe
|
|
1015
|
+
// explicitly redirects it with V4_CORPUS_DIR. The IN-MEMORY corpus assertions below still run, so
|
|
1016
|
+
// nothing is weakened.
|
|
1017
|
+
const writeCorpus = !process.env.V4_PLUGIN || !!process.env.V4_CORPUS_DIR
|
|
1018
|
+
if (writeCorpus) {
|
|
1019
|
+
mkdirSync(CORPUS_DIR, { recursive: true })
|
|
1020
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v4.json'), JSON.stringify({ entries: corpus }, null, 2), 'utf8')
|
|
1021
|
+
const md = ['# V4 形式化验证交互语料(prompt corpus)', '',
|
|
1022
|
+
'> 由 `formal-verify-v4.test.mjs` 落盘:非 `off` 模式下常驻**真正会读到**的 Lean 提示词原文',
|
|
1023
|
+
'> (`vibe_v4_prompts` 的只读回显 + 一条真实投递的工作轮 + 工具 `hint`)。',
|
|
1024
|
+
'> 工作区路径归一化为 `<WS>`,VibeMath 根归一化为 `<VIBEMATH>`:确定、可 diff、不含任何本机路径。', '',
|
|
1025
|
+
'> 覆盖:`off`(无 Lean 文本)、`encourage`、**`require`**、对象 `passed` 后的**忠实性分支**',
|
|
1026
|
+
'> (`encourage` / `require` 两种措辞各一份:只有 `require` 会声称"不定论")、',
|
|
1027
|
+
'> `blocked` 分支、平时工作轮的「顺手形式化」,以及回执契约里的 `formal` 字段。', '']
|
|
1028
|
+
for (let i = 0; i < corpus.length; i++) {
|
|
1029
|
+
const c = corpus[i]
|
|
1030
|
+
md.push('## [' + i + '] ' + c.kind + ' · ' + c.label)
|
|
1031
|
+
md.push('')
|
|
1032
|
+
md.push('```text')
|
|
1033
|
+
md.push(c.prompt)
|
|
1034
|
+
md.push('```')
|
|
1035
|
+
md.push('')
|
|
1036
|
+
}
|
|
1037
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v4.md'), md.join('\n'), 'utf8')
|
|
1038
|
+
assert(existsSync(join(CORPUS_DIR, 'formal-verify-v4.json')) && existsSync(join(CORPUS_DIR, 'formal-verify-v4.md')), 'the prompt corpus was written (JSON + Markdown)')
|
|
1039
|
+
}
|
|
1040
|
+
assert(corpus.length >= 14, 'the corpus covers the whole Lean prompt surface (' + corpus.length + ' prompts)')
|
|
1041
|
+
const byLabel = (l) => corpus.find((c) => c.label === l)
|
|
1042
|
+
assert(!!byLabel('off/verify') && !/Lean/.test(byLabel('off/verify').prompt) && !/Lean/.test(byLabel('off/normal').prompt), '★ the corpus keeps the off-mode entries and they contain NO Lean text')
|
|
1043
|
+
assert(/【Lean 形式化验证(鼓励模式)】/.test(byLabel('encourage/verify').prompt), 'the corpus carries the encourage voting prompt')
|
|
1044
|
+
assert(/【Lean 形式化验证(强制模式)】/.test(byLabel('require/verify').prompt), '★ the corpus carries the REQUIRE voting prompt')
|
|
1045
|
+
assert(/不要投 0/.test(byLabel('passed/fidelity').prompt), '★ the corpus carries the passed/fidelity branch')
|
|
1046
|
+
assert(/本次裁定\*\*不定论\*\*/.test(byLabel('passed/fidelity').prompt), 'the require fidelity entry keeps the hold it really enforces')
|
|
1047
|
+
assert(!!byLabel('passed/fidelity (encourage)') && !/本次裁定\*\*不定论\*\*/.test(byLabel('passed/fidelity (encourage)').prompt)
|
|
1048
|
+
&& /本档没有门禁/.test(byLabel('passed/fidelity (encourage)').prompt), '★ the encourage fidelity entry does NOT promise the hold only require has (docs §4.1-3)')
|
|
1049
|
+
assert(/【顺手形式化(强制)】/.test(byLabel('require/normal').prompt), 'the corpus carries the ordinary work-round line')
|
|
1050
|
+
assert(/"decision":"used\|blocked\|defect"/.test(byLabel('formal reply contract (voting prompt)').prompt), '★ the corpus carries the `formal` reply contract line with decision=defect')
|
|
1051
|
+
assert(/【顺手形式化/.test(byLabel('require/real work wake').prompt), 'the corpus also keeps a prompt the framework REALLY delivered')
|
|
1052
|
+
const joined = corpus.map((c) => c.prompt).join('\n')
|
|
1053
|
+
assert(joined.indexOf(K.WS) === -1 && joined.indexOf(K.WS.replace(/\\/g, '/')) === -1 && joined.indexOf(vibe) === -1 && joined.indexOf(vibe.replace(/\\/g, '/')) === -1, '★ every captured prompt normalises <WS> and <VIBEMATH> (diffable, no machine paths)')
|
|
1054
|
+
assert(!/\[object Object\]|\bNaN\b|:\s*undefined|["']undefined["']|undefined\s*[,}\]]/.test(joined), 'no captured prompt contains placeholder garbage')
|
|
1055
|
+
assert(corpus.every((c) => c.prompt && c.prompt.length > 20), 'every corpus entry carries real prompt text')
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ===============================================================
|
|
1059
|
+
// 16. a FRESH run must not inherit the previous run's formal records (docs §31.6): object ids are
|
|
1060
|
+
// reused (p-*, r-1..), so the in-memory reset in `start()` has to REACH DISK even when the mode
|
|
1061
|
+
// has been switched back to `off` — otherwise a later `resume` restores a stale `passed` and
|
|
1062
|
+
// re-opens the require gate for an object of the new run. This is the exact hazard that makes
|
|
1063
|
+
// the `off`-mode persistence guard ("do not create State/formal.json" ) non-trivial.
|
|
1064
|
+
// ===============================================================
|
|
1065
|
+
section('16 a fresh run clears the persisted formal records (even in off mode)')
|
|
1066
|
+
{
|
|
1067
|
+
const stale = readIf(join(D.projectRoot, 'State', 'formal.json'))
|
|
1068
|
+
assert(/"p-proof"/.test(stale), 'precondition: the previous run left formal records on disk')
|
|
1069
|
+
await D.callTool('vibe_v4_set', { formalVerify: 'off' })
|
|
1070
|
+
const beforeSpawns = D.spawns.length
|
|
1071
|
+
const st = await D.callTool('vibe_v4_start', { problem: '新一轮:形式化清白起点', residentCount: 1 })
|
|
1072
|
+
assert(st.ok === true && D.spawns.length > beforeSpawns, 'the fresh run actually started (so the clean-slate assertion is falsifiable)')
|
|
1073
|
+
const now = readIf(join(D.projectRoot, 'State', 'formal.json'))
|
|
1074
|
+
assert(!/"p-proof"/.test(now) && !/"passed"/.test(now), '★ a fresh run clears the persisted formal records on disk, even in off mode (no stale `passed` can open the new run’s gate)')
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// ===============================================================
|
|
1078
|
+
console.log('')
|
|
1079
|
+
console.log('passed=' + passed + ' failed=' + failed)
|
|
1080
|
+
if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
1081
|
+
console.log('ALL GREEN')
|
|
1082
|
+
process.exit(0)
|