dsh-vibe-math 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT-CHECKLIST.md +33 -3
- package/README.md +21 -0
- package/RELEASE-NOTES-2.3.1.md +134 -0
- package/audit-formal-sensitivity.mjs +125 -39
- package/audit-v5-integrity.mjs +5 -3
- package/docs/formal-verification.md +92 -12
- package/docs/test-timing.md +79 -0
- package/formal-verify-v2.test.mjs +286 -7
- package/formal-verify-v3.test.mjs +215 -8
- package/formal-verify-v4.test.mjs +282 -3
- package/formal-verify-v5.test.mjs +72 -0
- package/package.json +9 -2
- package/prompt-corpus-v2/formal-verify-v2.json +394 -0
- package/prompt-corpus-v2/formal-verify-v2.md +4250 -0
- package/prompt-corpus-v3/formal-verify-v3.json +159 -57
- package/prompt-corpus-v3/formal-verify-v3.md +1302 -285
- package/prompt-corpus-v4/formal-verify-v4.json +84 -0
- package/prompt-corpus-v4/formal-verify-v4.md +255 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +54 -16
- package/prompt-corpus-v5/prompt-corpus-v5.md +378 -153
- package/prompt-v5-integrity.test.mjs +1158 -1085
- package/run-tests.mjs +99 -0
- package/vibe-math-v2/vibe-math-v2.js +204 -22
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +77 -4
- package/vibe-math-v3/vibe-math-v3.js +82 -21
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +17 -0
- package/vibe-math-v4/vibe-math-v4.js +114 -22
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +29 -0
- package/vibe-math-v5/vibe-math-v5.js +81 -22
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +27 -4
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
// the object is Lean-passed or carries an explicit, reasoned blocker; then the
|
|
12
12
|
// same verdict DOES write the card, and the card records the formal status
|
|
13
13
|
// · the three tools (run / archive / lib) write the right things to the right paths
|
|
14
|
+
// · the REPLY CHANNEL is real, not dead code (contract §4 / §6.3 / §10.8): a verifier's
|
|
15
|
+
// `formal:{decision:'blocked'|'defect', note}` reply is actually absorbed into the durable
|
|
16
|
+
// record for BOTH id spaces, a missing note is refused, and `defect` (a fidelity defect,
|
|
17
|
+
// i.e. the Lean code does not say what the proposition says) downgrades the proof, deletes
|
|
18
|
+
// the archived file, writes the TODO and defers the verdict — it is NEVER recorded as
|
|
19
|
+
// "the proposition is false" (contract §4.1)
|
|
14
20
|
//
|
|
15
21
|
// The Lean toolchain is mocked through the subprocess SERVICE (a fake Lean: exit 0 unless the
|
|
16
22
|
// file still contains `sorry` or the marker `-- FAIL`), so these tests exercise the REAL code
|
|
@@ -24,11 +30,20 @@
|
|
|
24
30
|
// ============================================================
|
|
25
31
|
import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, writeFileSync, statSync, mkdirSync } from 'node:fs'
|
|
26
32
|
import { tmpdir } from 'node:os'
|
|
27
|
-
import { join, dirname, isAbsolute } from 'node:path'
|
|
33
|
+
import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
|
|
34
|
+
import { fileURLToPath } from 'node:url'
|
|
28
35
|
|
|
29
36
|
const PLUGIN = process.env.V2_PLUGIN
|
|
30
37
|
? new URL('file:///' + String(process.env.V2_PLUGIN).replace(/\\/g, '/'))
|
|
31
38
|
: new URL('./vibe-math-v2/vibe-math-v2.js', import.meta.url)
|
|
39
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
40
|
+
// Human-reviewable corpus (contract §10.10). A sensitivity probe runs THIS suite against a
|
|
41
|
+
// MUTATED plugin copy: writing the repository corpus from such a run would replace the
|
|
42
|
+
// reviewed text with mutated text, so a mutated run goes to a scratch directory instead
|
|
43
|
+
// (and V2_CORPUS_DIR overrides both, exactly like the v3 suite's V3_CORPUS_DIR).
|
|
44
|
+
const CORPUS_DIR = process.env.V2_CORPUS_DIR
|
|
45
|
+
? pathResolve(process.env.V2_CORPUS_DIR)
|
|
46
|
+
: (process.env.V2_PLUGIN ? join(tmpdir(), 'vibe-v2-prompt-corpus') : join(HERE, 'prompt-corpus-v2'))
|
|
32
47
|
|
|
33
48
|
let passed = 0, failed = 0
|
|
34
49
|
const failures = []
|
|
@@ -64,6 +79,20 @@ const subprocess = {
|
|
|
64
79
|
if (m) m[1].split(/\s+/).forEach((p) => { const q = p.trim().replace(/^'|'$/g, ''); if (q) mkdirSync(q, { recursive: true }) })
|
|
65
80
|
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
66
81
|
}
|
|
82
|
+
// v2 ALSO deletes files through this same service (`powershell … Remove-Item -LiteralPath 'x'`,
|
|
83
|
+
// POSIX `rm -f 'x'`). Without honouring it, the `defect` assertion "the archived proof is
|
|
84
|
+
// gone" would pass vacuously (nothing was ever deleted) instead of testing the real code path.
|
|
85
|
+
if (/Remove-Item/.test(script)) {
|
|
86
|
+
const m = /-LiteralPath\s+'((?:[^']|'')*)'/.exec(script)
|
|
87
|
+
if (m) { try { rmSync(m[1].replace(/''/g, "'"), { force: true }) } catch (e) { /* best effort */ } }
|
|
88
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
89
|
+
}
|
|
90
|
+
if (/^\s*rm -f /.test(script)) {
|
|
91
|
+
const re = /'((?:[^']|'\\'')*)'/g
|
|
92
|
+
let m
|
|
93
|
+
while ((m = re.exec(script)) !== null) { try { rmSync(m[1].replace(/'\\''/g, "'"), { force: true }) } catch (e) { /* best effort */ } }
|
|
94
|
+
return { done: Promise.resolve({ exitCode: 0 }), collected: {}, terminate() {} }
|
|
95
|
+
}
|
|
67
96
|
const text = existsSync(last) ? readFileSync(last, 'utf8') : ''
|
|
68
97
|
const bad = /sorry|-- FAIL/.test(text)
|
|
69
98
|
leanRuns.push({ argv: spec.argv.slice(0, -1), file: last, cwd: spec.cwd, graceMs: spec.graceMs, stdio: spec.stdio })
|
|
@@ -162,18 +191,37 @@ async function makeCase(label, opts = {}) {
|
|
|
162
191
|
// The scheduler only picks objects up while it is RUNNING (scheduleTick early-returns).
|
|
163
192
|
async function startScheduler(h) { await h.call('vibe_math_start', {}) }
|
|
164
193
|
|
|
194
|
+
// ── test speed: fast-forward the SCHEDULER POLL (test-only, no production impact) ─────────
|
|
195
|
+
// The plugin registers its scheduler poll as `setInterval(..., 1000)` at apply() time, so every
|
|
196
|
+
// suite-side `tick()` had to wait a full wall-clock second. With ~9 verification rounds × ~17
|
|
197
|
+
// ticks that alone accounted for ~99% of this suite's 186 s (its v3/v4/v5 siblings take seconds).
|
|
198
|
+
// Patching ONLY setInterval (the suite's own `sleep` uses setTimeout) makes a poll cost ~25 ms,
|
|
199
|
+
// while the plugin's due-ness logic still uses the real 200 ms `tickIntervalMs` floor — no
|
|
200
|
+
// scheduler behaviour depends on the poll period, and `tickTheScheduler` below stays above it.
|
|
201
|
+
const REAL_SET_INTERVAL = globalThis.setInterval
|
|
202
|
+
globalThis.setInterval = function (fn, ms, ...rest) {
|
|
203
|
+
return REAL_SET_INTERVAL(fn, Math.min(Number(ms) || 0, 25), ...rest)
|
|
204
|
+
}
|
|
205
|
+
|
|
165
206
|
const projRoot = (h) => join(h.WS, 'VibeMath', 'Projects', 'proj')
|
|
166
207
|
const vibeRoot = (h) => join(h.WS, 'VibeMath')
|
|
167
208
|
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
168
209
|
const formalStateOf = (h) => JSON.parse(readIf(join(projRoot(h), 'VibeMath_State', 'formal.json')) || '{}')
|
|
169
|
-
// One scheduler pass.
|
|
170
|
-
//
|
|
171
|
-
const tick = (ms =
|
|
172
|
-
async function waitFor(pred, tries =
|
|
210
|
+
// One scheduler pass. With the poll fast-forwarded above, the poll fires every ~25 ms and a tick
|
|
211
|
+
// runs whenever the plugin's own 200 ms `tickIntervalMs` floor has elapsed, so 260 ms is one pass.
|
|
212
|
+
const tick = (ms = 260) => sleep(ms)
|
|
213
|
+
async function waitFor(pred, tries = 80, ms = 60) {
|
|
173
214
|
for (let i = 0; i < tries; i++) { const v = pred(); if (v) return v; await sleep(ms) }
|
|
174
215
|
return undefined
|
|
175
216
|
}
|
|
176
217
|
const verifiersOf = (h, rKind, exclude) => h.spawns.filter((s) => s.label.startsWith('verifier:' + rKind) && !(exclude || []).some((o) => o.childId === s.childId))
|
|
218
|
+
/** A reply exactly as an agent would emit it (a fenced JSON block) — the framework's real input. */
|
|
219
|
+
const fence = (obj) => '```json\n' + JSON.stringify(obj) + '\n```'
|
|
220
|
+
/** Feed ONE agent reply (a fresh turn's end) to the framework through the real dispatch path. */
|
|
221
|
+
const replyFrom = (h, childId, obj) => h.fireEnd({
|
|
222
|
+
id: childId, runId: 'r-' + childId, provider: 'spawn', local: true, stopReason: 'completed',
|
|
223
|
+
lastAssistantMessage: [{ type: 'text', text: fence(obj) }],
|
|
224
|
+
})
|
|
177
225
|
const fireVerdicts = (h, kids, vale) => {
|
|
178
226
|
for (let i = 0; i < kids.length; i++) {
|
|
179
227
|
h.fireEnd({ id: kids[i].childId, runId: 'v' + i, provider: 'spawn', local: true, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: '```json\n' + JSON.stringify({ Result: vale, Reason: 'review ' + i }) + '\n```' }] })
|
|
@@ -192,6 +240,12 @@ async function verifyWithDebate(h, rKind, vale, firstRounds = 1, settle = 1) {
|
|
|
192
240
|
// v2 re-wakes the SAME verifier children for every debate round (it does not spawn new
|
|
193
241
|
// ones), so a new round is detected by counting the followups written to those children.
|
|
194
242
|
const kids = new Set()
|
|
243
|
+
// Early exit: a settled object stops producing followups. Without this the loop always spent
|
|
244
|
+
// its FULL 16 passes (~21 s at the old 1.3 s/pass), which is what made this suite take 3
|
|
245
|
+
// minutes: the debate is over, but the helper kept ticking at nothing. Three consecutive
|
|
246
|
+
// passes with no new followup = the framework has nothing left to ask.
|
|
247
|
+
let lastN = -1
|
|
248
|
+
let quiet = 0
|
|
195
249
|
for (let i = 0; i < 16; i++) {
|
|
196
250
|
// The scheduler may need a whole pass before it notices the object and another before it
|
|
197
251
|
// fills the verifier quota, so each wait must span several timer ticks.
|
|
@@ -206,7 +260,8 @@ async function verifyWithDebate(h, rKind, vale, firstRounds = 1, settle = 1) {
|
|
|
206
260
|
// verifier set each pass is safe; the extra answers land before the next round's wakes.
|
|
207
261
|
fireVerdicts(h, cand, rounds <= firstRounds ? vale : settle)
|
|
208
262
|
await sleep(200)
|
|
209
|
-
await tick(
|
|
263
|
+
await tick()
|
|
264
|
+
if (h.followups.length === lastN) { if (++quiet >= 3) break } else { quiet = 0; lastN = h.followups.length }
|
|
210
265
|
}
|
|
211
266
|
const rounds = 1 + h.followups.filter((f) => kids.has(f.childId)).length
|
|
212
267
|
return { first: h.spawns.filter((s) => s.label.startsWith('verifier:' + rKind)), rounds: kids.size ? rounds : 0 }
|
|
@@ -318,8 +373,14 @@ section("3 'encourage' injects the Lean section into review AND debate prompts")
|
|
|
318
373
|
assert(/【Lean 形式化验证(鼓励模式)】/.test(reviewText), '★ the REVIEW prompt carries the Lean section')
|
|
319
374
|
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(reviewText), 'the review prompt states that a passing Lean run shrinks the question to fidelity')
|
|
320
375
|
assert(/实现难度/.test(reviewText), 'the review prompt asks for the implementation-difficulty judgement')
|
|
376
|
+
// NOTE: this assertion is only the WORDING half. Which is exactly the trap the first version
|
|
377
|
+
// of this feature fell into (a green suite guarding a dead channel). The behaviour — the reply
|
|
378
|
+
// really landing in the durable record — is asserted in section 11.
|
|
321
379
|
assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(reviewText), "'encourage' explicitly allows skipping (with a recorded judgement)")
|
|
380
|
+
assert(/"decision":"used\|blocked\|defect"/.test(reviewText), '★ the review contract lists the real decision enum (incl. defect)')
|
|
322
381
|
assert(/vibe_math_lean_run(执行)· vibe_math_lean_archive(归档)· vibe_math_lean_lib(查已有可复用库)/.test(reviewText), 'the review prompt names the three v2 tools')
|
|
382
|
+
assert(/归档可复用定义\/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run)/.test(reviewText), '★ the review prompt says a reusable artifact must run green BEFORE it is archived')
|
|
383
|
+
assert(/LEAN_NOT_FOUND/.test(reviewText) && /宿主无 Lean 工具链/.test(reviewText), '★ the review prompt writes out the missing-toolchain escape hatch (a host without Lean must not deadlock the agent)')
|
|
323
384
|
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
324
385
|
assert(/DEBATE/.test(debate), 'the debate round actually happened (a followup with the debate prompt was issued)')
|
|
325
386
|
assert(/【Lean 形式化验证(鼓励模式)】/.test(debate), '★ the DEBATE prompt carries the Lean section too')
|
|
@@ -330,6 +391,12 @@ section("3 'encourage' injects the Lean section into review AND debate prompts")
|
|
|
330
391
|
assert(!!ex && /【顺手形式化(鼓励)】/.test(ex.prompt || ''), '★ the explorer work prompt carries the 顺手形式化 line')
|
|
331
392
|
assert(!!ex && /vibe_math_lean_archive kind='def'/.test(ex.prompt || ''), 'the work line points at the archive tool for reusable definitions')
|
|
332
393
|
assert(!!ex && /vibe_math_lean_lib 查重/.test(ex.prompt || ''), 'the work line tells members to check the reuse library first')
|
|
394
|
+
assert(!!ex && /归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。/.test(ex.prompt || ''), '★ the work line forbids archiving a definition that has not run green')
|
|
395
|
+
assert(!!ex && /"formal":\{"target":"<对象id>","decision":"used\|blocked\|defect"/.test(ex.prompt || ''), '★ the WORK-round contract advertises the formal reply field too (otherwise a working agent has nowhere to write its judgement)')
|
|
396
|
+
// Every agent-facing string must name the REGISTERED tools: an abbreviated `lean_archive`
|
|
397
|
+
// is not a tool that exists, and agents copy these literals verbatim (contract §6 hard req. 1).
|
|
398
|
+
const workText = (ex.prompt || '') + '\n' + reviewText + '\n' + debate
|
|
399
|
+
assert(!/(^|[^a-z_])lean_(run|archive|lib)/.test(workText), '★ no injected prompt names an abbreviated tool (every occurrence is prefixed)')
|
|
333
400
|
const offHost = await makeCase('enc-off')
|
|
334
401
|
await offHost.call('vibe_math_add_problem', { id: 'qN', description: 'x' })
|
|
335
402
|
await startScheduler(offHost)
|
|
@@ -493,10 +560,15 @@ section('6 a passing proof flips the review subject to fidelity')
|
|
|
493
560
|
assert(/忠实性审查/.test(vpText), '★ it tells reviewers the review subject is now fidelity')
|
|
494
561
|
assert(/定义 \/ 对象 \/ 条件 \/ 假设 \/ 结论是否与命题原文\*\*完全一致\*\*/.test(vpText), 'it enumerates exactly what fidelity means')
|
|
495
562
|
assert(/Verified\/Lean\/r-pFid\.lean/.test(vpText), 'it points at the archived proof')
|
|
496
|
-
assert(
|
|
563
|
+
assert(/一致 → Result = 1/.test(vpText), '★ the fidelity guidance names the REAL field (v2\'s contract field is Result)')
|
|
564
|
+
assert(!/verdict/.test(vpText), '★ the fidelity guidance never names a `verdict` field (that vote would be silently dropped)')
|
|
565
|
+
assert(/发现任何偏差,不要投 0/.test(vpText), '★ a fidelity defect is explicitly NOT to be voted as 0 (it is not a refutation)')
|
|
566
|
+
assert(/formal:\{decision:'defect'/.test(vpText), '★ the reviewers are given the defect reply channel that withdraws the proof')
|
|
567
|
+
assert(!/偏离 → 0/.test(vpText), '★ the "any deviation → 0" instruction is gone (it would fabricate a false conclusion)')
|
|
497
568
|
assert(!/请先判断该对象的\*\*实现难度\*\*/.test(vpText), 'the "judge the difficulty first" wording is gone when a proof already exists')
|
|
498
569
|
const debate = h.followups.map((f) => f.prompt || '').filter((p) => /DEBATE/.test(p)).join('\n')
|
|
499
570
|
assert(/你不需要重新检查推导/.test(debate), '★ the debate prompt for a Lean-passed object also asks for fidelity, not re-derivation')
|
|
571
|
+
assert(/发现任何偏差,不要投 0/.test(debate), '★ and it carries the same no-zero rule in the debate round')
|
|
500
572
|
await h.call('vibe_math_lean_archive', { kind: 'blocked', target: 'r-pBlk2', note: '涉及未形式化的分析学前置' })
|
|
501
573
|
await h.call('vibe_math_add_proposition', { id: 'pBlk2', 概述: '已记录阻塞的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
502
574
|
const vs2 = await verifyWithDebate(h, 'r-pBlk2', 0.5, 1)
|
|
@@ -525,6 +597,9 @@ section("7 'require' withholds a verdict until the formal record exists")
|
|
|
525
597
|
assert(/必须产出 Lean 形式化/.test(vp), "'require' states the formalization is mandatory")
|
|
526
598
|
assert(/本次裁定不会生效/.test(vp), 'the prompt warns that the verdict will not take effect without it')
|
|
527
599
|
assert(/formal-required/.test(vp), 'the prompt names the machine-readable reason')
|
|
600
|
+
assert(/vibe_math_lean_archive kind='blocked' note=… 或回执 formal\.note/.test(vp), '★ the require wording names BOTH blocking routes with the FULL tool name')
|
|
601
|
+
assert(/"formal":\{"target":"r-pGate","decision":"used\|blocked\|defect"/.test(vp), '★ the require review contract carries the formal reply field keyed by the verification id')
|
|
602
|
+
assert(!/(^|[^a-z_])lean_(run|archive|lib)/.test(vp), '★ the require prompt contains no abbreviated tool name either')
|
|
528
603
|
fireVerdicts(h, vs, 1)
|
|
529
604
|
// The deferral writes Formal/TODO.md from INSIDE the settle path; waiting for that file is
|
|
530
605
|
// the observable proof that the round settled (the task is dropped right after).
|
|
@@ -662,6 +737,210 @@ section("10 'encourage' never gates (a verdict still lands with no Lean artifact
|
|
|
662
737
|
assert(!existsSync(join(proj, 'Formal', 'TODO.md')) || !/pFree/.test(readIf(join(proj, 'Formal', 'TODO.md'))), 'no formalization TODO is created in encourage mode')
|
|
663
738
|
}
|
|
664
739
|
|
|
740
|
+
// ---------- 11. the reply channel is REAL (contract §4 / §6.3 / §10.8) ----------
|
|
741
|
+
// The first version of this feature only WROTE "请在回执的 formal 字段写明难度判断" into the prompt
|
|
742
|
+
// and never parsed it: 177 green assertions guarded a dead channel (AUDIT-CHECKLIST §2.2). These
|
|
743
|
+
// cases feed a genuine agent reply through the framework's own dispatch path (subagent/end →
|
|
744
|
+
// handleVerifier → absorb) and assert the DURABLE record, not the wording.
|
|
745
|
+
section('11 the reply channel really lands in the record (blocked / note validation)')
|
|
746
|
+
{
|
|
747
|
+
const h = await makeCase('reply')
|
|
748
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
749
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
750
|
+
await startScheduler(h)
|
|
751
|
+
await h.call('vibe_math_add_proposition', { id: 'pReply', 概述: '用回执记录阻塞', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
752
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pReply'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
753
|
+
assert(!!vs, 'verifiers were spawned for the reply-channel proposition')
|
|
754
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pReply:0') || {}).prompt || ''
|
|
755
|
+
assert(/"formal":\{"target":"r-pReply","decision":"used\|blocked\|defect"/.test(vp), '★ the review contract itself advertises the formal field, keyed by the verification id')
|
|
756
|
+
if (vs) {
|
|
757
|
+
// ① `blocked` WITH a note, named by the VERIFICATION id → the OBJECT record must be synced too.
|
|
758
|
+
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '我判断形式化不划算', formal: { target: 'r-pReply', decision: 'blocked', note: '需要大量未形式化的实分析前置知识' } })
|
|
759
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['r-pReply'] && r['r-pReply'].status === 'blocked') ? r : undefined }, 40, 150)
|
|
760
|
+
assert(!!rec, '★ a `formal.decision=blocked` reply is really absorbed — the channel is not dead code')
|
|
761
|
+
assert(!!rec && rec['r-pReply'].decision === 'blocked' && /实分析前置知识/.test(rec['r-pReply'].note || ''), 'the record keeps the decision AND the reason')
|
|
762
|
+
assert(!!rec && !!rec['pReply'] && rec['pReply'].status === 'blocked' && /实分析前置知识/.test(rec['pReply'].note || ''), '★ the OBJECT-id record is synced too (the two id spaces must not drift)')
|
|
763
|
+
assert(/实分析前置知识/.test(readIf(join(projRoot(h), 'Formal', 'Index.md'))), 'the reply-recorded blocker reaches Formal/Index.md')
|
|
764
|
+
// ② `blocked` WITHOUT a note (a different target) → refused with V2_INVALID_ARGUMENT, no record.
|
|
765
|
+
replyFrom(h, vs[1].childId, { Result: 0.5, Reason: '不想做', formal: { target: 'r-pNoNote', decision: 'blocked' } })
|
|
766
|
+
let acts = ''
|
|
767
|
+
for (let i = 0; i < 25; i++) {
|
|
768
|
+
const st = await h.call('vibe_math_status', {})
|
|
769
|
+
acts = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
770
|
+
if (/V2_INVALID_ARGUMENT/.test(acts)) break
|
|
771
|
+
await sleep(120)
|
|
772
|
+
}
|
|
773
|
+
assert(/V2_INVALID_ARGUMENT/.test(acts), '★ a blocked/defect judgement without a note is REJECTED with V2_INVALID_ARGUMENT')
|
|
774
|
+
assert(/没有写明 note/.test(acts), 'the refusal says why (an explicit decision is required, never a silent skip)')
|
|
775
|
+
const recN = formalStateOf(h)
|
|
776
|
+
assert(!(recN.records || {})['r-pNoNote'] && !(recN.records || {})['pNoNote'], 'no record is invented for the refused judgement')
|
|
777
|
+
assert(!/pNoNote/.test(readIf(join(projRoot(h), 'Formal', 'Index.md'))), 'the refused judgement does not reach the index either')
|
|
778
|
+
// ③ a reply with NO target must not invent an object (safeId('') would fall back to 'anon').
|
|
779
|
+
// The 0.5/0.5 round has no consensus, so the debate round re-woke both children — that is the
|
|
780
|
+
// observable proof that these children are registered and can answer again.
|
|
781
|
+
const woke = await waitFor(() => (h.followups.filter((f) => /DEBATE/.test(f.prompt || '')).length >= 2 ? true : undefined), 40, 150)
|
|
782
|
+
assert(!!woke, 'the debate round re-woke the verifiers (round-2 replies reach the framework again)')
|
|
783
|
+
replyFrom(h, vs[0].childId, { Result: 0.5, Reason: '漏写 target', formal: { decision: 'blocked', note: '没有写 target' } })
|
|
784
|
+
let acts2 = ''
|
|
785
|
+
for (let i = 0; i < 25; i++) {
|
|
786
|
+
const st = await h.call('vibe_math_status', {})
|
|
787
|
+
acts2 = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
788
|
+
if (/没有 target/.test(acts2)) break
|
|
789
|
+
await sleep(120)
|
|
790
|
+
}
|
|
791
|
+
assert(/没有 target/.test(acts2), '★ a `formal` reply without a target is refused, not guessed')
|
|
792
|
+
const recT = formalStateOf(h)
|
|
793
|
+
assert(!(recT.records || {}).anon, '★ a `formal` reply without a target cannot invent a record (no "anon" object)')
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ---------- 12. `defect`: a fidelity defect is NOT "the proposition is false" (§4.1) ----------
|
|
798
|
+
section('12 a defect reply withdraws the proof, writes the TODO and defers the verdict')
|
|
799
|
+
{
|
|
800
|
+
const h = await makeCase('defect')
|
|
801
|
+
await h.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
802
|
+
await h.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
803
|
+
await startScheduler(h)
|
|
804
|
+
const proj = projRoot(h)
|
|
805
|
+
// A PASSING Lean proof exists for the object — archived under the OBJECT id, the realistic path.
|
|
806
|
+
const proof = await h.call('vibe_math_lean_archive', { kind: 'proof', target: 'pDefect', content: 'theorem p_defect : 2 + 2 = 4 := by decide\n' })
|
|
807
|
+
assert(proof.ok === true && proof.passed === true, 'precondition: the object starts Lean-passed')
|
|
808
|
+
const proofFile = join(proj, 'Verified', 'Lean', 'pDefect.lean')
|
|
809
|
+
assert(existsSync(proofFile), 'the archived proof exists before the defect is reported')
|
|
810
|
+
await h.call('vibe_math_add_proposition', { id: 'pDefect', 概述: '形式化写窄了的命题', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
811
|
+
const vs = await waitFor(() => { const x = verifiersOf(h, 'r-pDefect'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
812
|
+
assert(!!vs, 'verifiers were spawned for the Lean-passed object')
|
|
813
|
+
const vp = (h.spawns.find((s) => s.label === 'verifier:r-pDefect:0') || {}).prompt || ''
|
|
814
|
+
assert(/忠实性审查/.test(vp) && /不要投 0/.test(vp), 'the reviewers were told to audit fidelity and NOT to vote 0 on a defect')
|
|
815
|
+
const DEFECT = 'Lean 只证了 n>0 的情形,命题原文是 n≥0'
|
|
816
|
+
if (vs) {
|
|
817
|
+
// A fidelity defect: the voter ABSTAINS (0.3) and records it through the reply channel.
|
|
818
|
+
replyFrom(h, vs[0].childId, { Result: 0.3, Reason: '形式化写窄了(弃权)', formal: { target: 'pDefect', decision: 'defect', note: DEFECT } })
|
|
819
|
+
const rec = await waitFor(() => { const r = (formalStateOf(h).records || {}); return (r['pDefect'] && r['pDefect'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
820
|
+
assert(!!rec, '★ the defect reply is absorbed')
|
|
821
|
+
assert(!!rec && rec['pDefect'].status === 'attempted', '★ the object is DOWNGRADED to attempted (the "passed" status is withdrawn)')
|
|
822
|
+
assert(!!rec && rec['pDefect'].proof === '', '★ the proof pointer is cleared')
|
|
823
|
+
assert(!!rec && rec['pDefect'].note === DEFECT, 'the concrete deviation is recorded on the object record')
|
|
824
|
+
assert(!!rec && !!rec['r-pDefect'] && rec['r-pDefect'].status === 'attempted' && rec['r-pDefect'].decision === 'defect', '★ the verification-id record is downgraded too (both id spaces)')
|
|
825
|
+
assert(!existsSync(proofFile), '★ the archived proof Verified/Lean/pDefect.lean is DELETED')
|
|
826
|
+
assert(existsSync(join(proj, 'Formal', 'pDefect.lean')), 'the working file Formal/pDefect.lean is kept (the code is not lost)')
|
|
827
|
+
const todo = readIf(join(proj, 'Formal', 'TODO.md'))
|
|
828
|
+
assert(/pDefect/.test(todo) && /defect/.test(todo), '★ the object is listed in Formal/TODO.md with the defect reason')
|
|
829
|
+
assert(/只证了 n>0 的情形/.test(todo), 'the TODO carries the concrete deviation, not just a flag')
|
|
830
|
+
let acts = ''
|
|
831
|
+
for (let i = 0; i < 25; i++) {
|
|
832
|
+
const st = await h.call('vibe_math_status', {})
|
|
833
|
+
acts = (st.recentActivity || []).map((a) => a.detail).join('\n')
|
|
834
|
+
if (/忠实性缺陷/.test(acts)) break
|
|
835
|
+
await sleep(120)
|
|
836
|
+
}
|
|
837
|
+
assert(/忠实性缺陷/.test(acts), 'the downgrade is announced on the v2-readable channel (activity log)')
|
|
838
|
+
assert(/撤回「已通过」状态/.test(acts), 'the announcement says the passed status was withdrawn')
|
|
839
|
+
assert(/本次裁定不定论/.test(acts), 'and that the verdict is undecided — a defect withdraws a proof, it does not refute the proposition')
|
|
840
|
+
// Now drive the same round to a UNANIMOUS "true": without the defect it would conclude.
|
|
841
|
+
const settled = await verifyWithDebate(h, 'r-pDefect', 1, 0)
|
|
842
|
+
assert(!!settled.first, 'the round was driven to a verdict after the defect')
|
|
843
|
+
const todo2 = await waitFor(() => { const t = readIf(join(proj, 'Formal', 'TODO.md')); return /formal-required/.test(t) ? t : undefined }, 40, 150)
|
|
844
|
+
assert(!!todo2 && /r-pDefect/.test(todo2), '★ require mode DEFERS after a defect (the object enters the formalization TODO as formal-required)')
|
|
845
|
+
await tick(400)
|
|
846
|
+
const props = JSON.parse(readIf(join(proj, 'Propos', '数论_Propos.json')) || '[]')
|
|
847
|
+
const p = props.find((x) => x.id === 'pDefect') || {}
|
|
848
|
+
assert(p.布尔估计 === 0.5, '★★ the proposition is NOT recorded as false — 布尔估计 unchanged (got ' + p.布尔估计 + ')')
|
|
849
|
+
assert(!(p.证明列表 || []).some((x) => x.正确概率 === 1), 'no probability-1 proof entry was written')
|
|
850
|
+
assert(p.优先级 !== 'never', 'the priority was not pinned to never')
|
|
851
|
+
assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), '★ no Verified card: the verdict is UNDECIDED, not "false"')
|
|
852
|
+
}
|
|
853
|
+
// The reverse direction: a defect named by the VERIFICATION id must still downgrade the OBJECT
|
|
854
|
+
// record that actually holds the proof (this is the pair that silently drifts when only one side
|
|
855
|
+
// is written — formalGateRecord would read `passed` from the other side).
|
|
856
|
+
const h2 = await makeCase('defect-rid')
|
|
857
|
+
await h2.call('vibe_math_set_params', { formalVerify: 'require', maxParallelThreshold: 8 })
|
|
858
|
+
await h2.call('vibe_math_add_problem', { id: 'qKeep', description: '保持调度器运行的占位问题', priority: 9 })
|
|
859
|
+
await startScheduler(h2)
|
|
860
|
+
const proj2 = projRoot(h2)
|
|
861
|
+
await h2.call('vibe_math_lean_archive', { kind: 'proof', target: 'pDefect2', content: 'theorem p_defect2 : 2 + 2 = 4 := by decide\n' })
|
|
862
|
+
await h2.call('vibe_math_add_proposition', { id: 'pDefect2', 概述: '回执用验证 id 命名的缺陷', 布尔估计: 0.5, 优先级: 1, '价值/关键性': 0.5, 细类型: { 数论: {} } })
|
|
863
|
+
const vs2 = await waitFor(() => { const x = verifiersOf(h2, 'r-pDefect2'); return x.length >= 2 ? x : undefined }, 60, 250)
|
|
864
|
+
assert(!!vs2, 'verifiers were spawned for the reverse-direction case')
|
|
865
|
+
if (vs2) {
|
|
866
|
+
replyFrom(h2, vs2[0].childId, { Result: 0.3, Reason: '写宽了', formal: { target: 'r-pDefect2', decision: 'defect', note: 'Lean 版本没有假设 n≥1,比原文更宽' } })
|
|
867
|
+
const rec2 = await waitFor(() => { const r = (formalStateOf(h2).records || {}); return (r['r-pDefect2'] && r['r-pDefect2'].decision === 'defect') ? r : undefined }, 40, 150)
|
|
868
|
+
assert(!!rec2, '★ a defect named by the verification id is absorbed')
|
|
869
|
+
assert(!!rec2 && !!rec2['pDefect2'] && rec2['pDefect2'].status === 'attempted', '★ and the OBJECT record that held `passed` is downgraded as well')
|
|
870
|
+
assert(!!rec2 && rec2['pDefect2'].proof === '', 'its proof pointer is cleared too')
|
|
871
|
+
assert(!existsSync(join(proj2, 'Verified', 'Lean', 'pDefect2.lean')), '★ the archived proof is deleted even though the reply named the other id')
|
|
872
|
+
assert(/pDefect2/.test(readIf(join(proj2, 'Formal', 'TODO.md'))), 'the object is on the formalization TODO')
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ---------- 13. the prompt corpus (contract §10.10) ----------
|
|
877
|
+
// A HUMAN must be able to re-read every prompt the framework emitted, not just the assertions
|
|
878
|
+
// about them. Paths are normalised so the dump is deterministic, diffable and machine-free.
|
|
879
|
+
section('13 the captured prompt corpus is written for human review')
|
|
880
|
+
{
|
|
881
|
+
// Freeze the scheduler in every case FIRST: a still-running tick loop could emit one more
|
|
882
|
+
// prompt between two runs and make the corpus non-deterministic.
|
|
883
|
+
for (const h of hosts) { try { await h.call('vibe_math_pause', {}) } catch (e) { /* ignore */ } }
|
|
884
|
+
const scrub = (h, s) => {
|
|
885
|
+
const ws = String(h.WS)
|
|
886
|
+
const slash = ws.replace(/\\/g, '/')
|
|
887
|
+
return String(s == null ? '' : s)
|
|
888
|
+
.split(slash + '/VibeMath').join('<VIBEMATH>')
|
|
889
|
+
.split(ws + '\\VibeMath').join('<VIBEMATH>')
|
|
890
|
+
.split(slash).join('<WS>')
|
|
891
|
+
.split(ws).join('<WS>')
|
|
892
|
+
}
|
|
893
|
+
const entries = []
|
|
894
|
+
for (const h of hosts) {
|
|
895
|
+
for (const s of h.spawns) entries.push({ kind: 'spawn', case: h.label, label: s.label, prompt: scrub(h, s.prompt) })
|
|
896
|
+
for (const f of h.followups) {
|
|
897
|
+
const owner = h.spawns.find((s) => s.childId === f.childId)
|
|
898
|
+
entries.push({ kind: 'wake', case: h.label, label: owner ? owner.label : f.childId, prompt: scrub(h, f.prompt) })
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
mkdirSync(CORPUS_DIR, { recursive: true })
|
|
902
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.json'), JSON.stringify({ entries: entries }, null, 2), 'utf8')
|
|
903
|
+
const md = ['# V2 形式化验证交互语料(prompt corpus)', '',
|
|
904
|
+
'> 由 `formal-verify-v2.test.mjs` 落盘:框架**真正发出**的每一条提示词原文。',
|
|
905
|
+
'> 工作区路径归一化为 `<WS>`、VibeMath 根归一化为 `<VIBEMATH>`,因此可 diff、不泄露本机路径。',
|
|
906
|
+
'> 覆盖:off 档(无任何 Lean 文字)、encourage 与 require 的表决初评/辩论、passed 后的忠实性分支、',
|
|
907
|
+
'> 以及平时工作轮的「顺手形式化」段落与 formal 回执契约。', '']
|
|
908
|
+
for (let i = 0; i < entries.length; i++) {
|
|
909
|
+
const e = entries[i]
|
|
910
|
+
md.push('## [' + i + '] ' + e.kind + ' · ' + e.label + ' · case=' + e.case)
|
|
911
|
+
md.push('')
|
|
912
|
+
md.push('```text')
|
|
913
|
+
md.push(e.prompt)
|
|
914
|
+
md.push('```')
|
|
915
|
+
md.push('')
|
|
916
|
+
}
|
|
917
|
+
writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.md'), md.join('\n'), 'utf8')
|
|
918
|
+
assert(existsSync(join(CORPUS_DIR, 'formal-verify-v2.json')) && existsSync(join(CORPUS_DIR, 'formal-verify-v2.md')), 'the prompt corpus was written (JSON + Markdown)')
|
|
919
|
+
assert(entries.length >= 15, 'the corpus covers the whole run (' + entries.length + ' prompts)')
|
|
920
|
+
assert(entries.some((e) => /【Lean 形式化验证(鼓励模式)】/.test(e.prompt)), 'the corpus contains the encourage verify prompt')
|
|
921
|
+
assert(entries.some((e) => /【Lean 形式化验证(强制模式)】/.test(e.prompt)), '★ the corpus contains the REQUIRE verify prompt')
|
|
922
|
+
assert(entries.some((e) => /你不需要重新检查推导/.test(e.prompt) && /一致 → Result = 1/.test(e.prompt)), 'the corpus contains the passed/fidelity prompt')
|
|
923
|
+
assert(entries.some((e) => /【顺手形式化(鼓励)】/.test(e.prompt)), 'the corpus contains the work-round 顺手形式化 prompt')
|
|
924
|
+
assert(entries.some((e) => /【顺手形式化/.test(e.prompt) && /"formal":\{"target":"<对象id>","decision":"used\|blocked\|defect"/.test(e.prompt)), 'the corpus contains the formal reply contract line')
|
|
925
|
+
assert(entries.some((e) => !/Lean|形式化/.test(e.prompt)), 'the corpus contains off-mode prompts with no Lean text at all')
|
|
926
|
+
assert(entries.some((e) => e.kind === 'wake'), 'the corpus also keeps the continuation prompts (debate rounds)')
|
|
927
|
+
// Generic sweeps over EVERY captured prompt, not spot checks (AUDIT §2.1).
|
|
928
|
+
const joined = entries.map((e) => e.prompt).join('\n')
|
|
929
|
+
const bare = entries.filter((e) => /(^|[^a-z_])lean_(run|archive|lib)/.test(e.prompt))
|
|
930
|
+
assert(bare.length === 0, '★ no captured prompt names an abbreviated tool (' + bare.map((b) => b.label).join(',') + ')')
|
|
931
|
+
const zero = entries.filter((e) => /偏离\s*→\s*0/.test(e.prompt))
|
|
932
|
+
assert(zero.length === 0, '★ no captured prompt turns a fidelity defect into a 0 vote (' + zero.map((z) => z.label).join(',') + ')')
|
|
933
|
+
const wrongField = entries.filter((e) => /忠实性/.test(e.prompt) && /verdict/.test(e.prompt))
|
|
934
|
+
assert(wrongField.length === 0, '★ no fidelity prompt names a `verdict` field (' + wrongField.map((w) => w.label).join(',') + ')')
|
|
935
|
+
const dirty = entries.filter((e) => /\[object Object\]|\bNaN\b|:\s*undefined|["']undefined["']|undefined\s*[,}\]]/.test(e.prompt))
|
|
936
|
+
assert(dirty.length === 0, 'no captured prompt contains placeholder garbage (' + dirty.map((d) => d.label).join(',') + ')')
|
|
937
|
+
assert(joined.indexOf('<VIBEMATH>') !== -1, 'the VibeMath root is normalised to <VIBEMATH>')
|
|
938
|
+
for (const h of hosts) {
|
|
939
|
+
const ws = String(h.WS)
|
|
940
|
+
assert(joined.indexOf(ws) === -1 && joined.indexOf(ws.replace(/\\/g, '/')) === -1, 'no captured prompt leaks a machine path (' + h.label + ')')
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
665
944
|
// cleanup
|
|
666
945
|
for (const h of hosts) { try { rmSync(h.WS, { recursive: true, force: true }) } catch (e) { /* ignore */ } }
|
|
667
946
|
|