dsh-vibe-math 2.3.1 → 2.3.3

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.
Files changed (39) hide show
  1. package/AUDIT-CHECKLIST.md +59 -0
  2. package/README.md +16 -6
  3. package/RELEASE-NOTES-2.3.2.md +145 -0
  4. package/RELEASE-NOTES-2.3.3.md +115 -0
  5. package/audit-formal-sensitivity.mjs +11 -2
  6. package/audit-prompt-invariants.mjs +459 -0
  7. package/audit-spec-traceability.mjs +193 -0
  8. package/docs/formal-verification.md +37 -13
  9. package/docs/generate_framework_diagram_v5.mjs +2 -1
  10. package/docs/test-timing.md +32 -10
  11. package/formal-verify-v2.test.mjs +288 -2
  12. package/formal-verify-v3.test.mjs +230 -4
  13. package/formal-verify-v4.test.mjs +205 -5
  14. package/formal-verify-v5.test.mjs +114 -4
  15. package/installer.js +3 -1
  16. package/package.json +6 -2
  17. package/prompt-corpus-persona/persona-corpus.json +2 -2
  18. package/prompt-corpus-persona/persona-corpus.md +6 -2
  19. package/prompt-corpus-v2/formal-verify-v2.json +164 -44
  20. package/prompt-corpus-v2/formal-verify-v2.md +1374 -44
  21. package/prompt-corpus-v3/formal-verify-v3.json +236 -128
  22. package/prompt-corpus-v3/formal-verify-v3.md +1509 -400
  23. package/prompt-corpus-v4/formal-verify-v4.json +8 -3
  24. package/prompt-corpus-v4/formal-verify-v4.md +38 -10
  25. package/prompt-corpus-v5/prompt-corpus-v5.json +175 -246
  26. package/prompt-corpus-v5/prompt-corpus-v5.md +341 -781
  27. package/prompt-v5-integrity.test.mjs +136 -22
  28. package/run-tests.mjs +30 -11
  29. package/vibe-math-v2/vibe-math-v2.js +163 -38
  30. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +58 -6
  31. package/vibe-math-v3/vibe-math-v3.js +95 -25
  32. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +8 -7
  33. package/vibe-math-v4/vibe-math-v4.js +103 -24
  34. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +37 -14
  35. package/vibe-math-v5/agent.cordis.yml +6 -2
  36. package/vibe-math-v5/vibe-math-v5.js +56 -10
  37. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +45 -14
  38. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +16 -2
  39. 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
  // ===============================================================
@@ -946,6 +1110,60 @@ await callTool('vibe_math_abort', {}, RK)
946
1110
  // ===============================================================
947
1111
  // 9. reporting + persistence
948
1112
  // ===============================================================
1113
+ // (d) a plain `used` judgement must NOT withdraw an ESTABLISHED proof (contract §4). Only a
1114
+ // fidelity defect retracts a proof; `used` just reports "this round touched the formalization".
1115
+ // v2 shipped the unconditional downgrade here and silently re-closed the require gate.
1116
+ section('8d a \`used\` reply must NOT downgrade an already-passed object')
1117
+ await restart(RG)
1118
+ await callTool('vibe_math_add_proposition', { id: 'p-usedkeep', 概述: '已有通过证明后再写一次 used 回执', 概率: 0.6, 分类: '数论' }, RG)
1119
+ {
1120
+ const pass = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-usedkeep', content: 'theorem p_usedkeep : 2 + 2 = 4 := by decide\n' }, RG)
1121
+ assert(pass.ok === true && pass.passed === true, 'used-keep: the object starts out Lean-passed')
1122
+ const re = verifyRe('p-usedkeep')
1123
+ assert(await drive(RG, () => unfiredVerifiers(RG, re).length >= 2, 'verifiers for r-p-usedkeep'), 'used-keep: the Lean-passed object is put to a fidelity review')
1124
+ const vs = unfiredVerifiers(RG, re).slice(0, 2)
1125
+ if (vs.length === 2) {
1126
+ for (const v of vs) firedChildren.add(v.childId)
1127
+ fireEnd(vs[0].childId, { Result: 0.5, Reason: '这一轮只是又写了一遍草稿', formal: { target: 'p-usedkeep', decision: 'used', file: 'Formal/p-usedkeep.lean' } })
1128
+ await sleep(150)
1129
+ const st = await callTool('vibe_math_status', {}, RG)
1130
+ const o = st.formal.objects.find((x) => x.target === 'p-usedkeep')
1131
+ assert(!!o && o.status === 'passed', '★ a `used` reply does NOT downgrade an already-passed object (got ' + JSON.stringify(o) + ')')
1132
+ assert(st.formal.passed.indexOf('p-usedkeep') !== -1, '★ and status still reports it as Lean-passed (the fidelity branch stays the reviewers\' subject)')
1133
+ assert(existsSync(join(replyProj, 'Verified', 'Lean', 'p-usedkeep.lean')), '★ and the archived proof is still on disk by that name')
1134
+ fireEnd(vs[1].childId, { Result: 0.5, Reason: '同样只是又写了一遍草稿' })
1135
+ await sleep(250)
1136
+ assert(!/p-usedkeep/.test(readIf(join(replyProj, 'Formal', 'TODO.md')) || ''), '★ no formal-required TODO was created: the surviving passed still opens the require gate')
1137
+ // The same rule covers the RUN path (contract §4): a run attributed to the object may not downgrade it.
1138
+ const rerun = await callTool('vibe_math_lean_run', { file: 'Formal/p-usedkeep.lean', target: 'p-usedkeep' }, RG)
1139
+ assert(rerun.ok === true, 'used-keep: the object work file runs green (the record assertion below is the point)')
1140
+ const o2 = (await callTool('vibe_math_status', {}, RG)).formal.objects.find((x) => x.target === 'p-usedkeep')
1141
+ assert(!!o2 && o2.status === 'passed', '★ and the record is still passed')
1142
+ }
1143
+ }
1144
+
1145
+ // (e) the same rule for `blocked`: a blocker is a gate-opening record, so a later `used` reply
1146
+ // must not push the object back to `attempted` and silently re-close the gate (v3 used to preserve
1147
+ // only `passed` here — v2/v4/v5 preserve both).
1148
+ await restart(RG)
1149
+ await callTool('vibe_math_add_proposition', { id: 'p-usedblocked', 概述: '已记录阻塞后再写一次 used 回执', 概率: 0.6, 分类: '数论' }, RG)
1150
+ {
1151
+ const blk = await callTool('vibe_math_lean_archive', { kind: 'blocked', target: 'p-usedblocked', note: '需要大量未形式化的实分析前置知识' }, RG)
1152
+ assert(blk.ok === true && blk.status === 'blocked', 'used-blocked: the object starts with an explicit blocker (the gate is open)')
1153
+ const re = verifyRe('p-usedblocked')
1154
+ assert(await drive(RG, () => unfiredVerifiers(RG, re).length >= 2, 'verifiers for r-p-usedblocked'), 'used-blocked: the object is put to a vote')
1155
+ const vs = unfiredVerifiers(RG, re).slice(0, 2)
1156
+ if (vs.length === 2) {
1157
+ for (const v of vs) firedChildren.add(v.childId)
1158
+ fireEnd(vs[0].childId, { Result: 0.5, Reason: '这一轮只是又写了一遍草稿', formal: { target: 'p-usedblocked', decision: 'used', file: 'Formal/p-usedblocked.lean' } })
1159
+ await sleep(150)
1160
+ const o = (await callTool('vibe_math_status', {}, RG)).formal.objects.find((x) => x.target === 'p-usedblocked')
1161
+ assert(!!o && o.status === 'blocked', '★ a `used` reply does not re-close a gate an explicit blocker opened (got ' + JSON.stringify(o) + ')')
1162
+ fireEnd(vs[1].childId, { Result: 0.5, Reason: '同上' })
1163
+ await sleep(200)
1164
+ }
1165
+ }
1166
+
949
1167
  section('9 the office can audit formal strength')
950
1168
  {
951
1169
  const rep = await callTool('vibe_math_report', {}, RF)
@@ -1020,6 +1238,14 @@ section('10 the captured prompt corpus is written for human review')
1020
1238
  assert(bareTools.length === 0, '★ no captured prompt abbreviates a Lean tool name (§6 hard requirement 1): ' + bareTools.map((b) => b.label).join(','))
1021
1239
  const zeroDeviation = corpus.filter((c) => c.prompt.indexOf('偏离 → 0') !== -1)
1022
1240
  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(','))
1241
+ // Contract §10 item 10 + AUDIT-CHECKLIST §2.4: the shipped corpus must be BYTE-deterministic, so
1242
+ // the volatile run metadata the planner brief carries (random plan id, epoch timestamps, child
1243
+ // ids, free-slot count) must be normalised out — otherwise every run diffs and the corpus loses
1244
+ // its only purpose (human review of what agents actually read).
1245
+ 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))
1246
+ 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(','))
1247
+ assert(corpus.some((c) => /"free_slots": <SLOTS>/.test(c.prompt)) && corpus.some((c) => /plan-<ID>/.test(c.label)),
1248
+ '★ and the normalisation actually fired (a planner brief and its plan id were captured)')
1023
1249
  }
1024
1250
 
1025
1251
  console.log('')