dsh-vibe-math 2.3.1 → 2.3.2
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 +45 -0
- package/README.md +16 -6
- package/RELEASE-NOTES-2.3.2.md +145 -0
- package/audit-formal-sensitivity.mjs +11 -2
- package/audit-prompt-invariants.mjs +414 -0
- package/audit-spec-traceability.mjs +173 -0
- package/docs/formal-verification.md +33 -10
- package/docs/generate_framework_diagram_v5.mjs +2 -1
- package/docs/test-timing.md +32 -10
- package/formal-verify-v2.test.mjs +242 -2
- package/formal-verify-v3.test.mjs +176 -4
- package/formal-verify-v4.test.mjs +184 -5
- package/formal-verify-v5.test.mjs +91 -4
- package/installer.js +3 -1
- package/package.json +5 -2
- package/prompt-corpus-persona/persona-corpus.json +2 -2
- package/prompt-corpus-persona/persona-corpus.md +6 -2
- package/prompt-corpus-v2/formal-verify-v2.json +134 -44
- package/prompt-corpus-v2/formal-verify-v2.md +1033 -44
- package/prompt-corpus-v3/formal-verify-v3.json +200 -128
- package/prompt-corpus-v3/formal-verify-v3.md +948 -243
- package/prompt-corpus-v4/formal-verify-v4.json +8 -3
- package/prompt-corpus-v4/formal-verify-v4.md +38 -10
- package/prompt-corpus-v5/prompt-corpus-v5.json +175 -246
- package/prompt-corpus-v5/prompt-corpus-v5.md +341 -781
- package/prompt-v5-integrity.test.mjs +136 -22
- package/run-tests.mjs +30 -11
- package/vibe-math-v2/vibe-math-v2.js +149 -35
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +53 -5
- package/vibe-math-v3/vibe-math-v3.js +88 -23
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +7 -6
- package/vibe-math-v4/vibe-math-v4.js +103 -24
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +34 -11
- package/vibe-math-v5/agent.cordis.yml +6 -2
- package/vibe-math-v5/vibe-math-v5.js +56 -10
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +40 -13
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +16 -2
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +6 -5
|
@@ -66,11 +66,21 @@ const corpus = []
|
|
|
66
66
|
// Normalise BOTH slash forms. The VibeMath ROOT must be replaced BEFORE the workspace root,
|
|
67
67
|
// otherwise `<WS>/VibeMath` would survive as a half-substituted path: the corpus would still leak
|
|
68
68
|
// the machine layout and would not be diffable against another checkout.
|
|
69
|
+
//
|
|
70
|
+
// The PLANNER prompt embeds the raw state brief, whose volatile RUN METADATA would otherwise change
|
|
71
|
+
// on every run and make the corpus undiffable (contract §10 item 10 demands byte determinism; the
|
|
72
|
+
// same class of bug is recorded in AUDIT-CHECKLIST §2.4 as a real accident): the random plan id in
|
|
73
|
+
// the label, epoch-millisecond `at` timestamps, child ids, and the free-slot count. They are run
|
|
74
|
+
// metadata, not the text under review, so they are normalised to placeholders.
|
|
69
75
|
const scrub = (s) => String(s == null ? '' : s)
|
|
70
76
|
.split(VIBE).join('<VIBEMATH>')
|
|
71
77
|
.split(VIBE.replace(/\\/g, '/')).join('<VIBEMATH>')
|
|
72
78
|
.split(WS).join('<WS>')
|
|
73
79
|
.split(WS.replace(/\\/g, '/')).join('<WS>')
|
|
80
|
+
.replace(/plan-[0-9a-f]{8}/g, 'plan-<ID>')
|
|
81
|
+
.replace(/("at"\s*:\s*)\d{10,16}/g, '$1"<TIME>"')
|
|
82
|
+
.replace(/("childId"\s*:\s*")c\d+(")/g, '$1<CHILD>$2')
|
|
83
|
+
.replace(/("free_slots"\s*:\s*)\d+/g, '$1<SLOTS>')
|
|
74
84
|
|
|
75
85
|
// A fake Lean: a file PASSES unless it still contains `sorry` or the marker `-- FAIL`.
|
|
76
86
|
// `-- HANG` simulates a toolchain that never returns (the timeout path).
|
|
@@ -152,7 +162,7 @@ const ctx = {
|
|
|
152
162
|
const rootId = (request && request.parent && request.parent.id) || ''
|
|
153
163
|
const prompt = (request && request.prompt && request.prompt[0] && request.prompt[0].text) || ''
|
|
154
164
|
spawns.push({ label, childId, rootId, prompt })
|
|
155
|
-
corpus.push({ kind: 'spawn', label, root: rootId, prompt: scrub(prompt) })
|
|
165
|
+
corpus.push({ kind: 'spawn', label: scrub(label), root: rootId, prompt: scrub(prompt) })
|
|
156
166
|
return { childId, messageId: 'm' + spawns.length }
|
|
157
167
|
},
|
|
158
168
|
async sendMessage(parent, childId, blocks) {
|
|
@@ -160,7 +170,7 @@ const ctx = {
|
|
|
160
170
|
const prompt = (blocks && blocks[0] && blocks[0].text) || ''
|
|
161
171
|
wakes.push({ childId, rootId, prompt })
|
|
162
172
|
const sp = spawns.find((s) => s.childId === childId)
|
|
163
|
-
corpus.push({ kind: 'wake', label: sp ? sp.label : childId, root: rootId, prompt: scrub(prompt) })
|
|
173
|
+
corpus.push({ kind: 'wake', label: scrub(sp ? sp.label : childId), root: rootId, prompt: scrub(prompt) })
|
|
164
174
|
return 'w' + wakes.length
|
|
165
175
|
},
|
|
166
176
|
async followup(parent, childId, blocks) { return await this.sendMessage(parent, childId, blocks) },
|
|
@@ -288,6 +298,48 @@ assert(!existsSync(join(offProj, 'State', 'formal.json')), 'no State/formal.json
|
|
|
288
298
|
assert(!!toolRegs.find((t) => t.name === 'vibe_math_lean_run') && !!toolRegs.find((t) => t.name === 'vibe_math_lean_archive') && !!toolRegs.find((t) => t.name === 'vibe_math_lean_lib'),
|
|
289
299
|
'the three Lean tools are registered in every mode (registration is static)')
|
|
290
300
|
|
|
301
|
+
// ★ The mode switch must be REACHABLE THROUGH THE TOOL SCHEMA (2.3.2 defect D1) ──────────────
|
|
302
|
+
// Every tool schema here is closed (`additionalProperties:false`), so a key the schema does not
|
|
303
|
+
// advertise is REJECTED by any schema-validating provider. v3 shipped 2.3.0/2.3.1 with all four Lean
|
|
304
|
+
// parameters missing from the set-params schema while every assertion in this file stayed green —
|
|
305
|
+
// because the suite calls the handler DIRECTLY and never inspects the registered schema. The feature
|
|
306
|
+
// could not be switched on at all through the tool interface.
|
|
307
|
+
{
|
|
308
|
+
const setSpec = toolRegs.find((t) => t.name === 'vibe_math_set_params')
|
|
309
|
+
assert(!!setSpec, "vibe_math_set_params is registered")
|
|
310
|
+
assert(setSpec.parameters && setSpec.parameters.type === 'object' && setSpec.parameters.additionalProperties === false,
|
|
311
|
+
'★ vibe_math_set_params publishes a CLOSED object schema (an unlisted key is rejected, so the schema IS the contract)')
|
|
312
|
+
for (const k of ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']) {
|
|
313
|
+
assert(Object.prototype.hasOwnProperty.call(setSpec.parameters.properties, k),
|
|
314
|
+
'★ the registered schema advertises ' + k + ' (every other surface documents it; a schema that omits it makes the switch unreachable)')
|
|
315
|
+
}
|
|
316
|
+
assert(JSON.stringify(setSpec.parameters.properties.formalVerify.enum) === JSON.stringify(['off', 'encourage', 'require']),
|
|
317
|
+
'the schema narrows formalVerify to the three real modes (a typo must not be a fourth)')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// A stray `formal` reply in OFF mode must be INERT (finding #1): the absorber is mode-gated, and the
|
|
321
|
+
// reply contract does not offer the field in off mode. The TOOLS stay usable on purpose.
|
|
322
|
+
{
|
|
323
|
+
const RB0 = makeRoot()
|
|
324
|
+
await callTool('vibe_math_new_project', { name: 'lean-off-reply' }, RB0)
|
|
325
|
+
await callTool('vibe_math_set_params', Object.assign({}, VPARAMS), RB0)
|
|
326
|
+
await callTool('vibe_math_add_proposition', { id: 'p-offr', 概述: '关模式下的回执注入测试', 概率: 0.6, 分类: '数论' }, RB0)
|
|
327
|
+
await callTool('vibe_math_start', {}, RB0)
|
|
328
|
+
const re0 = verifyRe('p-offr')
|
|
329
|
+
const ok0 = await drive(RB0, () => unfiredVerifiers(RB0, re0).length >= 1, 'off-reply verifier')
|
|
330
|
+
assert(ok0, 'off mode: a verifier wake exists to carry the stray formal reply')
|
|
331
|
+
const un0 = unfiredVerifiers(RB0, re0).slice(0, 1)
|
|
332
|
+
if (un0.length) {
|
|
333
|
+
firedChildren.add(un0[0].childId)
|
|
334
|
+
fireEnd(un0[0].childId, { Result: 0.5, Reason: '普通评审', formal: { target: 'p-offr', decision: 'defect', note: '不应被记录' } })
|
|
335
|
+
}
|
|
336
|
+
await sleep(220)
|
|
337
|
+
const pj0 = projRoot('lean-off-reply')
|
|
338
|
+
assert(!existsSync(join(pj0, 'State', 'formal.json')), '★ a stray `formal` reply in off mode writes NO State/formal.json')
|
|
339
|
+
const st0 = await callTool('vibe_math_status', {}, RB0)
|
|
340
|
+
assert(st0.formal.objects.length === 0, '★ and records no formal object')
|
|
341
|
+
}
|
|
342
|
+
|
|
291
343
|
// ===============================================================
|
|
292
344
|
// 2. parameter validation + runtime switching
|
|
293
345
|
// ===============================================================
|
|
@@ -446,6 +498,7 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
|
|
|
446
498
|
const runBad = await callTool('vibe_math_lean_run', { file: 'Formal/bad.lean' }, RE)
|
|
447
499
|
assert(runBad.ok === false && runBad.exitCode === 1, 'a file that still uses sorry reports a red run')
|
|
448
500
|
assert(/sorry/.test(runBad.stderr), 'the compiler output is returned verbatim (' + JSON.stringify(runBad.stderr).slice(0, 60) + ')')
|
|
501
|
+
assert(/修复后重跑/.test(runBad.hint || ''), 'a genuine compile failure still points at the compiler output (the hint is failure-code aware, not blanket)')
|
|
449
502
|
}
|
|
450
503
|
{
|
|
451
504
|
const runMissing = await callTool('vibe_math_lean_run', { file: 'Formal/nope.lean' }, RE)
|
|
@@ -473,12 +526,16 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
|
|
|
473
526
|
const runNoTc = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
|
|
474
527
|
assert(runNoTc.ok === false && runNoTc.code === 'LEAN_NOT_FOUND', 'a missing toolchain returns LEAN_NOT_FOUND instead of crashing')
|
|
475
528
|
assert(/仍可把形式化代码写下来归档/.test(runNoTc.message), 'the failure explains the graceful degradation')
|
|
529
|
+
assert(/本宿主无法执行 Lean(LEAN_NOT_FOUND)/.test(runNoTc.hint || '') && !/编译器输出修复后重跑/.test(runNoTc.hint || ''),
|
|
530
|
+
'★ the failure hint never tells the agent to fix compiler output that does not exist — it points at the archive + explicit-blocker way out (§6 hard requirement 4)')
|
|
476
531
|
toolchainAvailable = true
|
|
477
532
|
}
|
|
478
533
|
{
|
|
479
534
|
subprocessAvailable = false
|
|
480
535
|
const runNoSub = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
|
|
481
536
|
assert(runNoSub.ok === false && runNoSub.code === 'NO_SUBPROCESS', 'a host without the subprocess service returns NO_SUBPROCESS')
|
|
537
|
+
assert(/本宿主无法执行 Lean(NO_SUBPROCESS)/.test(runNoSub.hint || '') && /vibe_math_lean_archive/.test(runNoSub.hint || ''),
|
|
538
|
+
'★ the NO_SUBPROCESS hint names the way out with the FULL tool name (no retry loop on an impossible run)')
|
|
482
539
|
subprocessAvailable = true
|
|
483
540
|
}
|
|
484
541
|
{
|
|
@@ -606,8 +663,8 @@ await callTool('vibe_math_start', {}, RF)
|
|
|
606
663
|
assert(/formal-required/.test(vp), 'the prompt names the machine-readable reason')
|
|
607
664
|
assert(/归档可复用定义\/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run);跑不通不要入库。/.test(vp),
|
|
608
665
|
'★ the verification prompt requires a GREEN run before archiving into the reuse library (§6 hard requirement 3)')
|
|
609
|
-
assert(/宿主没有 Lean 工具链(LEAN_NOT_FOUND)时:把代码写下来归档,并在回执的 note 里写明"宿主无 Lean 工具链"/.test(vp),
|
|
610
|
-
'★ and spells out the way out when the host has no Lean toolchain (§6 hard requirement 4)')
|
|
666
|
+
assert(/宿主没有 Lean 工具链(LEAN_NOT_FOUND)或宿主不提供 subprocess 服务(NO_SUBPROCESS)时:把代码写下来归档,并在回执的 note 里写明"宿主无 Lean 工具链"/.test(vp),
|
|
667
|
+
'★ and spells out the way out when the host has no Lean toolchain, naming BOTH failure codes (LEAN_NOT_FOUND / NO_SUBPROCESS, §6 hard requirement 4)')
|
|
611
668
|
assert(/Result/.test(vp) && !/verdict/.test(vp) && !/(^|[^a-z_])lean_(run|archive|lib)/.test(vp),
|
|
612
669
|
'★ the voting prompt uses FULL tool names and names Result, never verdict (§6 hard requirements 1-2)')
|
|
613
670
|
}
|
|
@@ -795,6 +852,8 @@ await callTool('vibe_math_start', {}, RH)
|
|
|
795
852
|
for (const v of vs) firedChildren.add(v.childId)
|
|
796
853
|
assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(vs[0].prompt) && /不要投 0/.test(vs[0].prompt) && /formal:\{decision:'defect'/.test(vs[0].prompt),
|
|
797
854
|
'defect: the fidelity prompt asks for the defect reply and forbids recording the deviation as 0')
|
|
855
|
+
assert(/本次裁定\*\*不定论\*\*/.test(vs[0].prompt),
|
|
856
|
+
"★ `require` DOES keep the hold clause (the clause is mode-dependent, not deleted)")
|
|
798
857
|
// The defect reply carries an EXTREME Result on purpose: the gate (not the vote value) has to
|
|
799
858
|
// be what withholds the verdict — a defect must never be harvested as "the proposition is false".
|
|
800
859
|
fireEnd(vs[0].childId, { Result: 1, Reason: '逐条核对后认定形式化不忠实', formal: { target: 'p-defect', decision: 'defect', note: 'Lean 代码多加了 h>0 假设,命题原文未要求' } })
|
|
@@ -883,6 +942,111 @@ await restart(RI)
|
|
|
883
942
|
assert(!existsSync(join(nonoteProj, 'Verified', '命题', 'p-blocked-defect.md')), 'require after a blocked→defect downgrade: still no Verified card (undecided, not "false")')
|
|
884
943
|
await callTool('vibe_math_abort', {}, RI)
|
|
885
944
|
|
|
945
|
+
// ===============================================================
|
|
946
|
+
// 8b-3. the withdrawal is not a best-effort delete (contract §4.1)
|
|
947
|
+
//
|
|
948
|
+
// `Verified/Lean/<id>.lean` is exactly where everyone looks for "the proof of this object", so a
|
|
949
|
+
// downgraded record with the old code still sitting there is worse than no record at all. The fs
|
|
950
|
+
// service exposes no unlink and `subprocess` is optional, so on a host that cannot delete, the
|
|
951
|
+
// withdrawal must fall back to overwriting the file with an explicit notice. This runs in
|
|
952
|
+
// `encourage` mode on purpose: the same scenario also proves the injected text and the
|
|
953
|
+
// framework's own announcement do NOT promise a hold that only `require` can enforce (§4.1 item 3).
|
|
954
|
+
// ===============================================================
|
|
955
|
+
section('8b-3 a defect withdraws the archived proof even on a host that cannot delete files')
|
|
956
|
+
const RL = makeRoot()
|
|
957
|
+
await callTool('vibe_math_new_project', { name: 'lean-defect-nodelete' }, RL)
|
|
958
|
+
await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'encourage' }), RL)
|
|
959
|
+
const nodeleteProj = projRoot('lean-defect-nodelete')
|
|
960
|
+
await callTool('vibe_math_add_proposition', { id: 'p-nodelete', 概述: '宿主无法删除文件时的撤回', 概率: 0.6, 分类: '数论' }, RL)
|
|
961
|
+
const proofFile = join(nodeleteProj, 'Verified', 'Lean', 'p-nodelete.lean')
|
|
962
|
+
{
|
|
963
|
+
const pass = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-nodelete', content: 'theorem p_nodelete : 2 + 2 = 4 := by decide\n' }, RL)
|
|
964
|
+
assert(pass.ok === true && pass.passed === true && /theorem p_nodelete/.test(readIf(proofFile)),
|
|
965
|
+
'no-delete: the object starts out Lean-passed with its archived proof on disk')
|
|
966
|
+
}
|
|
967
|
+
await callTool('vibe_math_start', {}, RL)
|
|
968
|
+
{
|
|
969
|
+
const re = verifyRe('p-nodelete')
|
|
970
|
+
assert(await drive(RL, () => unfiredVerifiers(RL, re).length >= 2, 'verifiers for r-p-nodelete'), 'no-delete: the Lean-passed object is put to a fidelity review')
|
|
971
|
+
const vs = unfiredVerifiers(RL, re).slice(0, 2)
|
|
972
|
+
if (vs.length === 2) {
|
|
973
|
+
for (const v of vs) firedChildren.add(v.childId)
|
|
974
|
+
assert(/发现任何偏差,不要投 0/.test(vs[0].prompt) && /本档没有门禁/.test(vs[0].prompt) && !/不定论/.test(vs[0].prompt),
|
|
975
|
+
'★ encourage: the fidelity text does NOT claim the framework will withhold the verdict (only require gates; §4.1 item 3)')
|
|
976
|
+
// A host whose shell cannot delete anything: no subprocess service at all. The withdrawal must
|
|
977
|
+
// therefore be observable as an OVERWRITE, not as a missing file.
|
|
978
|
+
subprocessAvailable = false
|
|
979
|
+
fireEnd(vs[0].childId, { Result: 0.5, Reason: '逐条核对后认定形式化不忠实', formal: { target: 'p-nodelete', decision: 'defect', note: 'Lean 里把自然数写成了整数' } })
|
|
980
|
+
await sleep(320)
|
|
981
|
+
subprocessAvailable = true
|
|
982
|
+
const after = readIf(proofFile)
|
|
983
|
+
assert(existsSync(proofFile) && !/theorem p_nodelete/.test(after),
|
|
984
|
+
'★ without a working delete the archived proof text is gone (overwritten, not left readable as a proof)')
|
|
985
|
+
assert(/已撤回(/.test(after) && /Formal\/p-nodelete\.lean/.test(after),
|
|
986
|
+
'★ and it carries the withdrawal notice pointing at the kept working file Formal/p-nodelete.lean')
|
|
987
|
+
const st = await callTool('vibe_math_status', {}, RL)
|
|
988
|
+
const rec = st.formal.objects.find((o) => o.target === 'p-nodelete')
|
|
989
|
+
assert(!!rec && rec.status === 'attempted' && rec.proof === '' && /自然数写成了整数/.test(rec.note || ''),
|
|
990
|
+
'★ the record is still downgraded to attempted with an emptied proof pointer')
|
|
991
|
+
const ann = readIf(join(nodeleteProj, 'Logs', '形式化.md'))
|
|
992
|
+
assert(/忠实性缺陷/.test(ann) && /就地覆盖/.test(ann),
|
|
993
|
+
'★ the announcement says the proof was OVERWRITTEN (not deleted), so the reader knows which withdrawal happened')
|
|
994
|
+
assert(ann.indexOf('不定论') === -1, '★ and `encourage` never claims the framework withheld the verdict')
|
|
995
|
+
const todo = readIf(join(nodeleteProj, 'Formal', 'TODO.md'))
|
|
996
|
+
assert(/# 形式化待办/.test(todo) && /p-nodelete/.test(todo) && !/定论被搁置/.test(todo) && /没有定论门禁/.test(todo),
|
|
997
|
+
'★ Formal/TODO.md states what this mode really does (it does not claim 定论被搁置)')
|
|
998
|
+
const idx = readIf(join(nodeleteProj, 'Formal', 'Index.md'))
|
|
999
|
+
assert(/## 形式化待办/.test(idx) && !/定论被搁置/.test(idx), '★ and Formal/Index.md mirrors that framing')
|
|
1000
|
+
fireEnd(vs[1].childId, { Result: 0.5, Reason: '同意,形式化不忠实' })
|
|
1001
|
+
await sleep(280)
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
await callTool('vibe_math_abort', {}, RL)
|
|
1005
|
+
|
|
1006
|
+
// ===============================================================
|
|
1007
|
+
// 8b-4. a defect on an ALREADY-verified object must not leave a stale card anchor
|
|
1008
|
+
//
|
|
1009
|
+
// `defect` withdraws the formalization (record → attempted, proof pointer cleared, archived file
|
|
1010
|
+
// deleted/overwritten). A Verified card written earlier still says 「形式化: Lean 通过(Verified/
|
|
1011
|
+
// Lean/<id>.lean)」 — a pointer to a proof that no longer exists. The gate must not prevent that
|
|
1012
|
+
// refresh: the gate decides whether a NEW conclusion may be declared, not whether the framework may
|
|
1013
|
+
// tell the truth about one it already declared.
|
|
1014
|
+
// ===============================================================
|
|
1015
|
+
section('8b-4 a later defect refreshes an existing Verified card instead of leaving a stale anchor')
|
|
1016
|
+
const RM = makeRoot()
|
|
1017
|
+
await callTool('vibe_math_new_project', { name: 'lean-stale-card' }, RM)
|
|
1018
|
+
await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'require' }), RM)
|
|
1019
|
+
const staleProj = projRoot('lean-stale-card')
|
|
1020
|
+
await callTool('vibe_math_add_proposition', { id: 'p-stale', 概述: '定论后才被认定形式化不忠实', 概率: 0.6, 分类: '数论' }, RM)
|
|
1021
|
+
await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-stale', content: 'theorem p_stale : 2 + 2 = 4 := by decide\n' }, RM)
|
|
1022
|
+
await callTool('vibe_math_start', {}, RM)
|
|
1023
|
+
assert(await runVerifyRound(RM, 'p-stale', [1, 1]) !== null, 'stale-card: the Lean-passed proposition was put to a vote')
|
|
1024
|
+
assert(await drive(RM, () => existsSync(join(staleProj, 'Verified', '命题', 'p-stale.md')), 'p-stale Verified card'), 'stale-card: the gate was satisfied, so it reaches a Verified card')
|
|
1025
|
+
{
|
|
1026
|
+
const card = readIf(join(staleProj, 'Verified', '命题', 'p-stale.md'))
|
|
1027
|
+
assert(/- 形式化: Lean 通过/.test(card) && /Verified\/Lean\/p-stale\.lean/.test(card), 'stale-card: the card records the machine-checked strength and its proof path')
|
|
1028
|
+
}
|
|
1029
|
+
// A LATER work-round reply (e.g. a solver that reuses the object and finds the Lean statement too
|
|
1030
|
+
// wide) reports the fidelity defect — long after the card was written.
|
|
1031
|
+
await callTool('vibe_math_add_problem', { id: 'q-w', description: '让 explorer 起来以便回执一条 defect', priority: 0 }, RM)
|
|
1032
|
+
await restart(RM)
|
|
1033
|
+
assert(await drive(RM, () => !!lastSpawn(RM, 'explorer:q-w'), 'explorer:q-w'), 'stale-card: an explorer exists to carry the work-round reply')
|
|
1034
|
+
fireEnd(lastSpawn(RM, 'explorer:q-w').childId, {
|
|
1035
|
+
meta: { kind: 'directions', qid: 'q-w', formal: { target: 'p-stale', decision: 'defect', note: '这份形式化把结论写宽了' },
|
|
1036
|
+
directions: [{ id: 'd1', title: '直接法', method: '', core_assumption: '', feasibility: 0.6 }] },
|
|
1037
|
+
})
|
|
1038
|
+
assert(await drive(RM, () => !/Lean 通过/.test(readIf(join(staleProj, 'Verified', '命题', 'p-stale.md'))), 'p-stale card refreshed'),
|
|
1039
|
+
'★ a defect on an already-verified object refreshes the existing Verified card (no stale 「Lean 通过」 anchor pointing at a withdrawn proof)')
|
|
1040
|
+
{
|
|
1041
|
+
const card = readIf(join(staleProj, 'Verified', '命题', 'p-stale.md'))
|
|
1042
|
+
assert(/- 形式化: 已尝试未通过/.test(card), '★ the refreshed card states the honest formal status (已尝试未通过)')
|
|
1043
|
+
assert(!/Verified\/Lean\/p-stale\.lean/.test(card), '★ and no longer points at the withdrawn proof')
|
|
1044
|
+
assert(/- 结论: 真/.test(card), 'the conclusion itself is untouched — a fidelity defect is not a refutation')
|
|
1045
|
+
assert(!existsSync(join(staleProj, 'Verified', 'Lean', 'p-stale.lean')), 'and the archived proof really is gone')
|
|
1046
|
+
assert(await drive(RM, () => /p-stale/.test(readIf(join(staleProj, 'Formal', 'TODO.md'))), 'p-stale formalization TODO'), 'the object is on the formalization TODO for redoing')
|
|
1047
|
+
}
|
|
1048
|
+
await callTool('vibe_math_abort', {}, RM)
|
|
1049
|
+
|
|
886
1050
|
// ===============================================================
|
|
887
1051
|
// 8c. the injected text obeys the five hard requirements of contract §6
|
|
888
1052
|
// ===============================================================
|
|
@@ -1020,6 +1184,14 @@ section('10 the captured prompt corpus is written for human review')
|
|
|
1020
1184
|
assert(bareTools.length === 0, '★ no captured prompt abbreviates a Lean tool name (§6 hard requirement 1): ' + bareTools.map((b) => b.label).join(','))
|
|
1021
1185
|
const zeroDeviation = corpus.filter((c) => c.prompt.indexOf('偏离 → 0') !== -1)
|
|
1022
1186
|
assert(zeroDeviation.length === 0, '★ no captured prompt turns a fidelity defect into a 0 vote (§6 hard requirement 5 / §10 item 9): ' + zeroDeviation.map((b) => b.label).join(','))
|
|
1187
|
+
// Contract §10 item 10 + AUDIT-CHECKLIST §2.4: the shipped corpus must be BYTE-deterministic, so
|
|
1188
|
+
// the volatile run metadata the planner brief carries (random plan id, epoch timestamps, child
|
|
1189
|
+
// ids, free-slot count) must be normalised out — otherwise every run diffs and the corpus loses
|
|
1190
|
+
// its only purpose (human review of what agents actually read).
|
|
1191
|
+
const volatile = corpus.filter((c) => /plan-[0-9a-f]{8}/.test(c.label) || /plan-[0-9a-f]{8}|"at": \d{10,}|"childId": "c\d+"|"free_slots": \d+/.test(c.prompt))
|
|
1192
|
+
assert(volatile.length === 0, '★ no captured prompt/label keeps volatile run metadata (random plan id / epoch timestamps / child ids / slot count) — the corpus is byte-deterministic: ' + volatile.map((b) => b.label).join(','))
|
|
1193
|
+
assert(corpus.some((c) => /"free_slots": <SLOTS>/.test(c.prompt)) && corpus.some((c) => /plan-<ID>/.test(c.label)),
|
|
1194
|
+
'★ and the normalisation actually fired (a planner brief and its plan id were captured)')
|
|
1023
1195
|
}
|
|
1024
1196
|
|
|
1025
1197
|
console.log('')
|
|
@@ -60,7 +60,16 @@ const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
|
|
|
60
60
|
// MOCK HOST — the shape selfdrive-v4 / e2e-v4-fixes use, plus a fake Lean
|
|
61
61
|
// ===============================================================
|
|
62
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
|
|
63
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)
|
|
64
73
|
const shellCalls = [] // every platform-shell script (mkdir at mount, Remove-Item on defect)
|
|
65
74
|
|
|
66
75
|
function makeSubprocess() {
|
|
@@ -81,8 +90,8 @@ function makeSubprocess() {
|
|
|
81
90
|
const script = String(spec.argv[spec.argv.length - 1] || '')
|
|
82
91
|
shellCalls.push(script)
|
|
83
92
|
const m = script.match(/-LiteralPath\s+'((?:[^']|'')*)'/)
|
|
84
|
-
if (/Remove-Item/.test(script) && m) rmSync(m[1].replace(/''/g, "'"), { force: true })
|
|
85
|
-
if (/^rm -f /.test(script)) for (const q of script.slice(6).match(/'[^']*'/g) || []) rmSync(q.slice(1, -1), { force: true })
|
|
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 })
|
|
86
95
|
return {
|
|
87
96
|
done: Promise.resolve({ exitCode: 0, signal: null }),
|
|
88
97
|
collected: { stdout: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) }, stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) } },
|
|
@@ -96,6 +105,16 @@ function makeSubprocess() {
|
|
|
96
105
|
const text = existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
97
106
|
const bad = /sorry|-- FAIL/.test(text)
|
|
98
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
|
+
}
|
|
99
118
|
const stdout = bad ? '' : 'ok\n'
|
|
100
119
|
const stderr = bad ? 'error: declaration uses sorry\n' : ''
|
|
101
120
|
return {
|
|
@@ -116,7 +135,7 @@ function makeHost() {
|
|
|
116
135
|
const subprocess = makeSubprocess()
|
|
117
136
|
let ROOT
|
|
118
137
|
const ctx = {
|
|
119
|
-
get(name) { return name === 'subprocess' ? subprocess : undefined },
|
|
138
|
+
get(name) { return name === 'subprocess' && subprocessAvailable ? subprocess : undefined },
|
|
120
139
|
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
121
140
|
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
122
141
|
logger: { info() {}, warn() {}, error() {} },
|
|
@@ -273,6 +292,9 @@ const A = await establish()
|
|
|
273
292
|
assert(!/Lean/.test(all), 'no founding/round prompt mentions Lean in off mode')
|
|
274
293
|
assert(!/顺手形式化|Lean 通过|忠实性审查|vibe_v4_lean/.test(all), 'no founding/round prompt carries any Lean-formalization text in off mode')
|
|
275
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)')
|
|
276
298
|
}
|
|
277
299
|
// an off-mode verification must behave exactly as before: unanimous 真 → Verified/, no formal record
|
|
278
300
|
const offSettled = await proposeAndVote(A, 'p-off', {
|
|
@@ -289,6 +311,39 @@ assert(!/顺手形式化/.test(await A.prompts('normal', 'r-1')), 'the work prom
|
|
|
289
311
|
assert(['vibe_v4_lean_run', 'vibe_v4_lean_archive', 'vibe_v4_lean_lib'].every(n => !!A.toolRegs.find(t => t.name === n)),
|
|
290
312
|
'the three Lean tools are registered in every mode (registration is static)')
|
|
291
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
|
+
|
|
292
347
|
// ---------- 2. parameter validation + runtime switching ----------
|
|
293
348
|
section('2 parameter validation and runtime switching')
|
|
294
349
|
const B = await establish()
|
|
@@ -401,6 +456,31 @@ assert(noTc.ok === false && noTc.code === 'LEAN_NOT_FOUND', '★ a missing toolc
|
|
|
401
456
|
assert(/仍可把形式化代码写下来归档/.test(noTc.message || ''), 'the failure explains the graceful degradation')
|
|
402
457
|
assert((await D.callTool('vibe_v4_status', {})).ok === true, 'the group did NOT crash on the missing toolchain (status still answers)')
|
|
403
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
|
+
}
|
|
404
484
|
|
|
405
485
|
// ---------- 5. a passing proof flips the review subject to fidelity ----------
|
|
406
486
|
section('5 a passing proof flips the review subject to fidelity')
|
|
@@ -414,6 +494,18 @@ const stD = await D.callTool('vibe_v4_status', {})
|
|
|
414
494
|
assert(stD.formal.passed.indexOf('p-proof') !== -1, 'status reports the object as Lean-passed')
|
|
415
495
|
const idxD = readIf(join(D.projectRoot, 'Formal', 'Index.md'))
|
|
416
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
|
+
}
|
|
417
509
|
// a RED proof must NOT mint a Verified/Lean/ copy or mark the object passed
|
|
418
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)
|
|
419
511
|
assert(arcRed.ok === true && arcRed.passed === false && arcRed.status === 'attempted', 'a red proof is archived as attempted, not passed')
|
|
@@ -639,7 +731,7 @@ await G.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
|
639
731
|
assert(/实现难度/.test(enc), 'it still asks for the implementation-difficulty judgement')
|
|
640
732
|
assert(/工具:vibe_v4_lean_run(执行)· vibe_v4_lean_archive(归档)· vibe_v4_lean_lib(查已有可复用库)/.test(enc), 'it names all three tools in FULL')
|
|
641
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')
|
|
642
|
-
assert(/宿主没有 Lean 工具链(LEAN_NOT_FOUND)时:把代码写下来归档,并在回执的 note 里写明"宿主无 Lean 工具链"/.test(enc), '★
|
|
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')
|
|
643
735
|
assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(enc), 'a green Lean run still shrinks the open question to fidelity')
|
|
644
736
|
assert(/decision='blocked' 时必须写明 note/.test(enc), 'the encourage opt-out documents the mandatory note')
|
|
645
737
|
assert(!/偏离 → 0/.test(enc) && !/发现任何偏离/.test(enc), '★ no "deviation ⇒ 0" instruction anywhere in the encourage block')
|
|
@@ -650,6 +742,15 @@ await G.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
|
650
742
|
assert(/本次裁定不会生效/.test(req) && /进入「形式化待办」/.test(req), 'it warns the verdict is withheld as 未定论 (formal-required)')
|
|
651
743
|
assert(!/可以不做/.test(req), "'require' does NOT offer the encourage-mode opt-out")
|
|
652
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
|
+
}
|
|
653
754
|
assert(/\*\*本模式要求\*\*/.test(req), 'the require bullet replaces the encourage opt-out in place')
|
|
654
755
|
// normal / heartbeat / post-compact recap all carry the standing work line; the two that ARE a
|
|
655
756
|
// reply contract also document the `defect` decision (coreRules is a recap prefix, not a contract)
|
|
@@ -680,12 +781,36 @@ await G.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
|
680
781
|
addText('LEAN_NOT_FOUND message', (await G.callTool('vibe_v4_lean_run', { file: 'Formal/p-hint.lean' }, r1G)).message)
|
|
681
782
|
toolchainAvailable = true
|
|
682
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 */ }
|
|
683
791
|
const bare = [/(^|[^a-z_])lean_run/, /(^|[^a-z_])lean_archive/, /(^|[^a-z_])lean_lib/]
|
|
684
792
|
const offenders = []
|
|
685
793
|
for (const s of scanned) for (const re of bare) if (re.test(s.text)) offenders.push(s.label + ' :: ' + re.source)
|
|
686
794
|
assert(offenders.length === 0, '★ no injected text (prompt, tool hint or tool description) uses a bare tool abbreviation (' + offenders.slice(0, 3).join(' | ') + ')')
|
|
687
795
|
assert(scanned.length >= 15, 'the sweep really covered the injected-text surface (' + scanned.length + ' texts)')
|
|
688
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
|
+
}
|
|
689
814
|
|
|
690
815
|
// ===============================================================
|
|
691
816
|
// 13. the `defect` reply channel (docs §4.1 / §10 item 8) — BEHAVIOURAL, not wording
|
|
@@ -716,6 +841,31 @@ const g1 = G.resAgent(G.childOf('r-1'))
|
|
|
716
841
|
assert(/\| p-defect \| attempted \|/.test(idx), 'Formal/Index.md downgrades the object to attempted')
|
|
717
842
|
assert(/只证了 n ≥ 1 的情形/.test(idx), 'the deviation is the record note in the index')
|
|
718
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)')
|
|
719
869
|
}
|
|
720
870
|
{
|
|
721
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)
|
|
@@ -816,6 +966,12 @@ const K = await establish()
|
|
|
816
966
|
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)
|
|
817
967
|
const fid = await K.prompts('verify', 'r-1', { target: 'p-corpus-passed', stage: 'independent' })
|
|
818
968
|
add('verify', 'passed/fidelity', fid)
|
|
969
|
+
// (d2) the SAME passed object in `encourage`: the promise must shrink to what that mode enforces
|
|
970
|
+
// (docs §4.1-3: no gate there, so no claim that the framework withholds the verdict).
|
|
971
|
+
await K.callTool('vibe_v4_set', { formalVerify: 'encourage' })
|
|
972
|
+
const fidEnc = await K.prompts('verify', 'r-1', { target: 'p-corpus-passed', stage: 'independent' })
|
|
973
|
+
add('verify', 'passed/fidelity (encourage)', fidEnc)
|
|
974
|
+
await K.callTool('vibe_v4_set', { formalVerify: 'require' })
|
|
819
975
|
// (e) a BLOCKED object
|
|
820
976
|
await K.callTool('vibe_v4_lean_archive', { kind: 'blocked', target: 'p-corpus-blocked', note: '需要未形式化的解析数论框架' }, k1)
|
|
821
977
|
add('verify', 'blocked/verify', await K.prompts('verify', 'r-1', { target: 'p-corpus-blocked', stage: 'independent' }))
|
|
@@ -845,7 +1001,8 @@ const K = await establish()
|
|
|
845
1001
|
'> 由 `formal-verify-v4.test.mjs` 落盘:非 `off` 模式下常驻**真正会读到**的 Lean 提示词原文',
|
|
846
1002
|
'> (`vibe_v4_prompts` 的只读回显 + 一条真实投递的工作轮 + 工具 `hint`)。',
|
|
847
1003
|
'> 工作区路径归一化为 `<WS>`,VibeMath 根归一化为 `<VIBEMATH>`:确定、可 diff、不含任何本机路径。', '',
|
|
848
|
-
'> 覆盖:`off`(无 Lean 文本)、`encourage`、**`require`**、对象 `passed`
|
|
1004
|
+
'> 覆盖:`off`(无 Lean 文本)、`encourage`、**`require`**、对象 `passed` 后的**忠实性分支**',
|
|
1005
|
+
'> (`encourage` / `require` 两种措辞各一份:只有 `require` 会声称"不定论")、',
|
|
849
1006
|
'> `blocked` 分支、平时工作轮的「顺手形式化」,以及回执契约里的 `formal` 字段。', '']
|
|
850
1007
|
for (let i = 0; i < corpus.length; i++) {
|
|
851
1008
|
const c = corpus[i]
|
|
@@ -865,6 +1022,9 @@ const K = await establish()
|
|
|
865
1022
|
assert(/【Lean 形式化验证(鼓励模式)】/.test(byLabel('encourage/verify').prompt), 'the corpus carries the encourage voting prompt')
|
|
866
1023
|
assert(/【Lean 形式化验证(强制模式)】/.test(byLabel('require/verify').prompt), '★ the corpus carries the REQUIRE voting prompt')
|
|
867
1024
|
assert(/不要投 0/.test(byLabel('passed/fidelity').prompt), '★ the corpus carries the passed/fidelity branch')
|
|
1025
|
+
assert(/本次裁定\*\*不定论\*\*/.test(byLabel('passed/fidelity').prompt), 'the require fidelity entry keeps the hold it really enforces')
|
|
1026
|
+
assert(!!byLabel('passed/fidelity (encourage)') && !/本次裁定\*\*不定论\*\*/.test(byLabel('passed/fidelity (encourage)').prompt)
|
|
1027
|
+
&& /本档没有门禁/.test(byLabel('passed/fidelity (encourage)').prompt), '★ the encourage fidelity entry does NOT promise the hold only require has (docs §4.1-3)')
|
|
868
1028
|
assert(/【顺手形式化(强制)】/.test(byLabel('require/normal').prompt), 'the corpus carries the ordinary work-round line')
|
|
869
1029
|
assert(/"decision":"used\|blocked\|defect"/.test(byLabel('formal reply contract (voting prompt)').prompt), '★ the corpus carries the `formal` reply contract line with decision=defect')
|
|
870
1030
|
assert(/【顺手形式化/.test(byLabel('require/real work wake').prompt), 'the corpus also keeps a prompt the framework REALLY delivered')
|
|
@@ -874,6 +1034,25 @@ const K = await establish()
|
|
|
874
1034
|
assert(corpus.every((c) => c.prompt && c.prompt.length > 20), 'every corpus entry carries real prompt text')
|
|
875
1035
|
}
|
|
876
1036
|
|
|
1037
|
+
// ===============================================================
|
|
1038
|
+
// 16. a FRESH run must not inherit the previous run's formal records (docs §31.6): object ids are
|
|
1039
|
+
// reused (p-*, r-1..), so the in-memory reset in `start()` has to REACH DISK even when the mode
|
|
1040
|
+
// has been switched back to `off` — otherwise a later `resume` restores a stale `passed` and
|
|
1041
|
+
// re-opens the require gate for an object of the new run. This is the exact hazard that makes
|
|
1042
|
+
// the `off`-mode persistence guard ("do not create State/formal.json" ) non-trivial.
|
|
1043
|
+
// ===============================================================
|
|
1044
|
+
section('16 a fresh run clears the persisted formal records (even in off mode)')
|
|
1045
|
+
{
|
|
1046
|
+
const stale = readIf(join(D.projectRoot, 'State', 'formal.json'))
|
|
1047
|
+
assert(/"p-proof"/.test(stale), 'precondition: the previous run left formal records on disk')
|
|
1048
|
+
await D.callTool('vibe_v4_set', { formalVerify: 'off' })
|
|
1049
|
+
const beforeSpawns = D.spawns.length
|
|
1050
|
+
const st = await D.callTool('vibe_v4_start', { problem: '新一轮:形式化清白起点', residentCount: 1 })
|
|
1051
|
+
assert(st.ok === true && D.spawns.length > beforeSpawns, 'the fresh run actually started (so the clean-slate assertion is falsifiable)')
|
|
1052
|
+
const now = readIf(join(D.projectRoot, 'State', 'formal.json'))
|
|
1053
|
+
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)')
|
|
1054
|
+
}
|
|
1055
|
+
|
|
877
1056
|
// ===============================================================
|
|
878
1057
|
console.log('')
|
|
879
1058
|
console.log('passed=' + passed + ' failed=' + failed)
|