dsh-vibe-math 2.3.0 → 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.
Files changed (40) hide show
  1. package/AUDIT-CHECKLIST.md +78 -3
  2. package/README.md +33 -2
  3. package/RELEASE-NOTES-2.3.1.md +134 -0
  4. package/RELEASE-NOTES-2.3.2.md +145 -0
  5. package/audit-formal-sensitivity.mjs +134 -39
  6. package/audit-prompt-invariants.mjs +414 -0
  7. package/audit-spec-traceability.mjs +173 -0
  8. package/audit-v5-integrity.mjs +5 -3
  9. package/docs/formal-verification.md +122 -19
  10. package/docs/generate_framework_diagram_v5.mjs +2 -1
  11. package/docs/test-timing.md +101 -0
  12. package/formal-verify-v2.test.mjs +526 -7
  13. package/formal-verify-v3.test.mjs +389 -10
  14. package/formal-verify-v4.test.mjs +462 -4
  15. package/formal-verify-v5.test.mjs +163 -4
  16. package/installer.js +3 -1
  17. package/package.json +12 -2
  18. package/prompt-corpus-persona/persona-corpus.json +2 -2
  19. package/prompt-corpus-persona/persona-corpus.md +6 -2
  20. package/prompt-corpus-v2/formal-verify-v2.json +484 -0
  21. package/prompt-corpus-v2/formal-verify-v2.md +5239 -0
  22. package/prompt-corpus-v3/formal-verify-v3.json +274 -100
  23. package/prompt-corpus-v3/formal-verify-v3.md +2057 -335
  24. package/prompt-corpus-v4/formal-verify-v4.json +89 -0
  25. package/prompt-corpus-v4/formal-verify-v4.md +283 -0
  26. package/prompt-corpus-v5/prompt-corpus-v5.json +186 -219
  27. package/prompt-corpus-v5/prompt-corpus-v5.md +485 -700
  28. package/prompt-v5-integrity.test.mjs +1272 -1085
  29. package/run-tests.mjs +118 -0
  30. package/vibe-math-v2/vibe-math-v2.js +341 -45
  31. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +129 -8
  32. package/vibe-math-v3/vibe-math-v3.js +162 -36
  33. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +21 -3
  34. package/vibe-math-v4/vibe-math-v4.js +201 -30
  35. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +54 -2
  36. package/vibe-math-v5/agent.cordis.yml +6 -2
  37. package/vibe-math-v5/vibe-math-v5.js +133 -28
  38. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +55 -5
  39. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +16 -2
  40. package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +6 -5
@@ -63,9 +63,24 @@ let spawnThrows = false
63
63
  // Interaction corpus (AUDIT-CHECKLIST §2.4): every prompt the framework actually sent, with the
64
64
  // workspace path normalised so the dump is deterministic and diffable.
65
65
  const corpus = []
66
- // Normalise BOTH slash forms: prompts built through vibeRoot() carry forward slashes while other
67
- // framework hints carry native backslashes, and a half-normalised corpus is not diffable.
68
- const scrub = (s) => String(s == null ? '' : s).split(WS).join('<WS>').split(WS.replace(/\\/g, '/')).join('<WS>')
66
+ // Normalise BOTH slash forms. The VibeMath ROOT must be replaced BEFORE the workspace root,
67
+ // otherwise `<WS>/VibeMath` would survive as a half-substituted path: the corpus would still leak
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.
75
+ const scrub = (s) => String(s == null ? '' : s)
76
+ .split(VIBE).join('<VIBEMATH>')
77
+ .split(VIBE.replace(/\\/g, '/')).join('<VIBEMATH>')
78
+ .split(WS).join('<WS>')
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>')
69
84
 
70
85
  // A fake Lean: a file PASSES unless it still contains `sorry` or the marker `-- FAIL`.
71
86
  // `-- HANG` simulates a toolchain that never returns (the timeout path).
@@ -147,7 +162,7 @@ const ctx = {
147
162
  const rootId = (request && request.parent && request.parent.id) || ''
148
163
  const prompt = (request && request.prompt && request.prompt[0] && request.prompt[0].text) || ''
149
164
  spawns.push({ label, childId, rootId, prompt })
150
- corpus.push({ kind: 'spawn', label, root: rootId, prompt: scrub(prompt) })
165
+ corpus.push({ kind: 'spawn', label: scrub(label), root: rootId, prompt: scrub(prompt) })
151
166
  return { childId, messageId: 'm' + spawns.length }
152
167
  },
153
168
  async sendMessage(parent, childId, blocks) {
@@ -155,7 +170,7 @@ const ctx = {
155
170
  const prompt = (blocks && blocks[0] && blocks[0].text) || ''
156
171
  wakes.push({ childId, rootId, prompt })
157
172
  const sp = spawns.find((s) => s.childId === childId)
158
- 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) })
159
174
  return 'w' + wakes.length
160
175
  },
161
176
  async followup(parent, childId, blocks) { return await this.sendMessage(parent, childId, blocks) },
@@ -283,6 +298,48 @@ assert(!existsSync(join(offProj, 'State', 'formal.json')), 'no State/formal.json
283
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'),
284
299
  'the three Lean tools are registered in every mode (registration is static)')
285
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
+
286
343
  // ===============================================================
287
344
  // 2. parameter validation + runtime switching
288
345
  // ===============================================================
@@ -335,7 +392,7 @@ assert(await drive(RC, () => !!lastSpawn(RC, 'explorer:qE'), 'explorer:qE'), 'fa
335
392
  assert(/【顺手形式化(鼓励)】/.test(p), 'the explorer work prompt carries the 顺手形式化 line')
336
393
  assert(/vibe_math_lean_archive kind='def'/.test(p), 'the work prompt points at the archive tool for reusable definitions')
337
394
  assert(/vibe_math_lean_lib 查重/.test(p), 'the work prompt tells agents to check the reuse library first')
338
- assert(/形式化回执/.test(p) && /"decision":"used\|blocked"/.test(p), '★ the work-round reply contract also advertises the formal field (契约 §6.3)')
395
+ assert(/形式化回执/.test(p) && /"decision":"used\|blocked\|defect"/.test(p), '★ the work-round reply contract also advertises the formal field, defect included (契约 §6.3)')
339
396
  }
340
397
  fireEnd(lastSpawn(RC, 'explorer:qE').childId, { meta: { kind: 'directions', qid: 'qE', formal: { target: 'qE', decision: 'blocked', note: '需要先形式化连分数收敛定理' }, directions: [{ id: 'd1', title: '连分数法', method: 'e 的连分数', core_assumption: '', feasibility: 0.7 }] } })
341
398
  // Answer EVERY explorer the fallback scheduler dispatches (it re-derives when a direction looks
@@ -390,7 +447,7 @@ if (encBatch) {
390
447
  assert(/一旦 Lean 通过,你唯一需要确认的就是忠实性/.test(vp), 'the review prompt states that a passing Lean run shrinks the question to fidelity')
391
448
  assert(/实现难度/.test(vp), 'the review prompt asks for the implementation-difficulty judgement')
392
449
  assert(/可以不做,但请在回执的 formal 字段写明难度判断/.test(vp), "'encourage' explicitly allows skipping (with a recorded judgement)")
393
- assert(/"formal":/.test(vp) && /"decision":"used\|blocked"/.test(vp), 'the reply contract documents the formal field')
450
+ assert(/"formal":/.test(vp) && /"decision":"used\|blocked\|defect"/.test(vp), 'the reply contract documents the formal field (defect included)')
394
451
  assert(/vibe_math_lean_run/.test(vp) && /vibe_math_lean_archive/.test(vp) && /vibe_math_lean_lib/.test(vp), 'the prompt names the three v3 Lean tools')
395
452
  assert(/Formal\/(相对项目根)/.test(vp) && /VibeMath\/Formal\/Lib/.test(vp), 'the prompt states the path layout')
396
453
  }
@@ -441,6 +498,7 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
441
498
  const runBad = await callTool('vibe_math_lean_run', { file: 'Formal/bad.lean' }, RE)
442
499
  assert(runBad.ok === false && runBad.exitCode === 1, 'a file that still uses sorry reports a red run')
443
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)')
444
502
  }
445
503
  {
446
504
  const runMissing = await callTool('vibe_math_lean_run', { file: 'Formal/nope.lean' }, RE)
@@ -468,12 +526,16 @@ writeFileSync(join(toolProj, 'Formal', 'hang.lean'), '-- HANG\ntheorem t : 1 = 1
468
526
  const runNoTc = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
469
527
  assert(runNoTc.ok === false && runNoTc.code === 'LEAN_NOT_FOUND', 'a missing toolchain returns LEAN_NOT_FOUND instead of crashing')
470
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)')
471
531
  toolchainAvailable = true
472
532
  }
473
533
  {
474
534
  subprocessAvailable = false
475
535
  const runNoSub = await callTool('vibe_math_lean_run', { file: 'Formal/good.lean' }, RE)
476
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)')
477
539
  subprocessAvailable = true
478
540
  }
479
541
  {
@@ -599,6 +661,12 @@ await callTool('vibe_math_start', {}, RF)
599
661
  assert(/必须产出 Lean 形式化/.test(vp), "'require' states the formalization is mandatory")
600
662
  assert(/本次裁定不会生效/.test(vp), 'the prompt warns that the verdict will not take effect without it')
601
663
  assert(/formal-required/.test(vp), 'the prompt names the machine-readable reason')
664
+ assert(/归档可复用定义\/引理前先跑通(vibe_math_lean_archive run=true 或先 vibe_math_lean_run);跑不通不要入库。/.test(vp),
665
+ '★ the verification prompt requires a GREEN run before archiving into the reuse library (§6 hard requirement 3)')
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)')
668
+ assert(/Result/.test(vp) && !/verdict/.test(vp) && !/(^|[^a-z_])lean_(run|archive|lib)/.test(vp),
669
+ '★ the voting prompt uses FULL tool names and names Result, never verdict (§6 hard requirements 1-2)')
602
670
  }
603
671
  }
604
672
  assert(await drive(RF, () => /p-gate/.test(readIf(join(gateProj, 'Formal', 'TODO.md'))), 'p-gate in Formal/TODO.md'), '★ a unanimous TRUE verdict was withheld: the object is on the formalization TODO')
@@ -756,6 +824,289 @@ await callTool('vibe_math_add_proposition', { id: 'p-nonote', 概述: '没有理
756
824
  }
757
825
  await callTool('vibe_math_abort', {}, RG)
758
826
 
827
+ // ===============================================================
828
+ // 8b. the §4.1 `defect` channel (contract §4.1 / §6 / §10 items 8-9)
829
+ //
830
+ // A fidelity defect is NOT "the proposition is false". These are BEHAVIOURAL assertions: a real
831
+ // agent reply carrying `formal:{decision:'defect', note}` is fed through the real reply path
832
+ // (subagent/end → handleVerifier → absorbFormalReply) and the record, the archived file and the
833
+ // formalization TODO are inspected on disk.
834
+ // ===============================================================
835
+ section('8b a formal.decision=defect reply withdraws the passed proof and withholds the verdict')
836
+ const RH = makeRoot()
837
+ await callTool('vibe_math_new_project', { name: 'lean-defect' }, RH)
838
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'require' }), RH)
839
+ const defectProj = projRoot('lean-defect')
840
+ await callTool('vibe_math_add_proposition', { id: 'p-defect', 概述: '形式化写窄了的命题', 概率: 0.6, 分类: '数论' }, RH)
841
+ {
842
+ const pass = await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-defect', content: 'theorem p_defect : 2 + 2 = 4 := by decide\n' }, RH)
843
+ assert(pass.ok === true && pass.passed === true, 'defect: the object starts out Lean-passed')
844
+ assert(existsSync(join(defectProj, 'Verified', 'Lean', 'p-defect.lean')), 'defect: the archived proof is on disk before the fidelity review')
845
+ }
846
+ await callTool('vibe_math_start', {}, RH)
847
+ {
848
+ const re = verifyRe('p-defect')
849
+ assert(await drive(RH, () => unfiredVerifiers(RH, re).length >= 2, 'verifiers for r-p-defect'), 'defect: the Lean-passed object is put to a fidelity review')
850
+ const vs = unfiredVerifiers(RH, re).slice(0, 2)
851
+ if (vs.length === 2) {
852
+ for (const v of vs) firedChildren.add(v.childId)
853
+ assert(/该对象已有\*\*通过的 Lean 形式化证明\*\*/.test(vs[0].prompt) && /不要投 0/.test(vs[0].prompt) && /formal:\{decision:'defect'/.test(vs[0].prompt),
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)")
857
+ // The defect reply carries an EXTREME Result on purpose: the gate (not the vote value) has to
858
+ // be what withholds the verdict — a defect must never be harvested as "the proposition is false".
859
+ fireEnd(vs[0].childId, { Result: 1, Reason: '逐条核对后认定形式化不忠实', formal: { target: 'p-defect', decision: 'defect', note: 'Lean 代码多加了 h>0 假设,命题原文未要求' } })
860
+ await sleep(240)
861
+ const st = await callTool('vibe_math_status', {}, RH)
862
+ const rec = st.formal.objects.find((o) => o.target === 'p-defect')
863
+ assert(!!rec && rec.status === 'attempted', '★ defect downgrades the formal record to attempted (observed ' + (rec && rec.status) + ')')
864
+ assert(st.formal.passed.indexOf('p-defect') === -1, '★ and the object is no longer reported as Lean-passed')
865
+ assert(!!rec && rec.proof === '', '★ defect clears the `proof` field')
866
+ assert(!existsSync(join(defectProj, 'Verified', 'Lean', 'p-defect.lean')), '★ defect deletes the archived proof Verified/Lean/p-defect.lean')
867
+ assert(existsSync(join(defectProj, 'Formal', 'p-defect.lean')), 'the WORK file Formal/p-defect.lean survives (the code itself is not lost)')
868
+ const persisted = JSON.parse(readIf(join(defectProj, 'State', 'formal.json')))
869
+ assert(persisted.records['p-defect'].decision === 'defect' && /多加了 h>0 假设/.test(persisted.records['p-defect'].note),
870
+ '★ the note (the concrete deviation) is recorded in the persisted formal record')
871
+ assert(/- 形式化: 已尝试未通过/.test(readIf(join(defectProj, 'Propos', '数论', 'p-defect.md'))), 'and the object card anchor is refreshed')
872
+ fireEnd(vs[1].childId, { Result: 1, Reason: '同意:形式化不忠实' })
873
+ await sleep(300)
874
+ }
875
+ }
876
+ {
877
+ const todo = readIf(join(defectProj, 'Formal', 'TODO.md'))
878
+ assert(/p-defect/.test(todo) && /多加了 h>0 假设/.test(todo), '★ the deviation is written into Formal/TODO.md')
879
+ const idx = readIf(join(defectProj, 'Formal', 'Index.md'))
880
+ assert(/p-defect/.test(idx) && /attempted/.test(idx) && !/Verified\/Lean\/p-defect\.lean/.test(idx), 'Formal/Index.md now shows attempted with no archived proof')
881
+ assert(!existsSync(join(defectProj, 'Verified', '命题', 'p-defect.md')), '★ require after a defect: NO Verified card is written (the verdict is withheld, not turned into "false")')
882
+ const card = readIf(join(defectProj, 'Propos', '数论', 'p-defect.md'))
883
+ assert(/- 状态: 未定论/.test(card), '★ and the object stays 未定论')
884
+ assert(/- 概率: 0.6/.test(card), 'the object keeps its existing probability: a defect must NOT be harvested as a refutation')
885
+ const ann = readIf(join(defectProj, 'Logs', '形式化.md'))
886
+ assert(/忠实性缺陷/.test(ann) && /多加了 h>0 假设/.test(ann), '★ the defect is announced with its concrete deviation')
887
+ assert(ann.indexOf('不是"命题为假"') !== -1, 'the announcement spells out that a fidelity defect is NOT "the proposition is false"')
888
+ const st = await callTool('vibe_math_status', {}, RH)
889
+ assert(st.formal.todo.some((t) => t.id === 'p-defect'), '★ the object is on the formalization TODO (undecided until the formalization is fixed and re-run)')
890
+ }
891
+ await callTool('vibe_math_abort', {}, RH)
892
+
893
+ section('8b-2 a defect without a note is refused (the deviation must be auditable)')
894
+ const RI = makeRoot()
895
+ await callTool('vibe_math_new_project', { name: 'lean-defect-nonote' }, RI)
896
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'require' }), RI)
897
+ const nonoteProj = projRoot('lean-defect-nonote')
898
+ await callTool('vibe_math_add_proposition', { id: 'p-nonote-defect', 概述: '没有偏差说明的缺陷回执', 概率: 0.6, 分类: '数论' }, RI)
899
+ await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-nonote-defect', content: 'theorem p_nn_defect : 2 + 2 = 4 := by decide\n' }, RI)
900
+ await callTool('vibe_math_start', {}, RI)
901
+ {
902
+ const re = verifyRe('p-nonote-defect')
903
+ assert(await drive(RI, () => unfiredVerifiers(RI, re).length >= 2, 'verifiers for r-p-nonote-defect'), 'defect: the passed object is put to a fidelity review')
904
+ const vs = unfiredVerifiers(RI, re).slice(0, 2)
905
+ if (vs.length === 2) {
906
+ for (const v of vs) firedChildren.add(v.childId)
907
+ fireEnd(vs[0].childId, { Result: 1, Reason: '觉得不忠实但没写清楚', formal: { target: 'p-nonote-defect', decision: 'defect' } })
908
+ await sleep(240)
909
+ const st = await callTool('vibe_math_status', {}, RI)
910
+ const rec = st.formal.objects.find((o) => o.target === 'p-nonote-defect')
911
+ assert(!!rec && rec.status === 'passed', '★ a defect WITHOUT a note is refused: the object stays Lean-passed (no silent downgrade)')
912
+ assert(existsSync(join(nonoteProj, 'Verified', 'Lean', 'p-nonote-defect.lean')), '★ and the archived proof is NOT deleted')
913
+ assert(st.formal.todo.every((t) => t.id !== 'p-nonote-defect') && !/p-nonote-defect/.test(readIf(join(nonoteProj, 'Formal', 'TODO.md'))),
914
+ 'and no bogus formalization-TODO entry is created')
915
+ assert(/未写明 note/.test(readIf(join(nonoteProj, 'Logs', '形式化.md'))), '★ the refusal is announced explicitly')
916
+ fireEnd(vs[1].childId, { Result: 1, Reason: '核对后认为一致' })
917
+ await sleep(300)
918
+ }
919
+ }
920
+ assert(await drive(RI, () => existsSync(join(nonoteProj, 'Verified', '命题', 'p-nonote-defect.md')), 'Verified card'), 'a refused defect leaves the gate open: the same vote still promotes the object')
921
+ // §4.1: the downgrade is unconditional — a `blocked` record loses to a defect too (it needs REDOING,
922
+ // not a free pass through the gate, which `blocked` would otherwise grant).
923
+ await callTool('vibe_math_add_proposition', { id: 'p-blocked-defect', 概述: '阻塞后仍被认定不忠实', 概率: 0.6, 分类: '数论' }, RI)
924
+ await callTool('vibe_math_lean_archive', { kind: 'blocked', target: 'p-blocked-defect', note: '先按难度记为阻塞' }, RI)
925
+ await restart(RI)
926
+ {
927
+ const re = verifyRe('p-blocked-defect')
928
+ assert(await drive(RI, () => unfiredVerifiers(RI, re).length >= 2, 'verifiers for r-p-blocked-defect'), 'a blocked object is put to a vote (the gate is open for blocked)')
929
+ const vs = unfiredVerifiers(RI, re).slice(0, 2)
930
+ if (vs.length === 2) {
931
+ for (const v of vs) firedChildren.add(v.childId)
932
+ fireEnd(vs[0].childId, { Result: 1, Reason: '形式化与命题不对应', formal: { target: 'p-blocked-defect', decision: 'defect', note: '阻塞所依据的形式化本身写错了对象' } })
933
+ await sleep(240)
934
+ const st = await callTool('vibe_math_status', {}, RI)
935
+ const rec = st.formal.objects.find((o) => o.target === 'p-blocked-defect')
936
+ assert(!!rec && rec.status === 'attempted', '★★ a defect ALWAYS downgrades, even from `blocked` (observed ' + (rec && rec.status) + ')')
937
+ assert(st.formal.blocked.indexOf('p-blocked-defect') === -1 && /阻塞所依据的形式化本身写错了对象/.test(rec.note || ''), 'and the blocked record is replaced by the concrete deviation')
938
+ fireEnd(vs[1].childId, { Result: 1, Reason: '同意,形式化写错了对象' })
939
+ await sleep(280)
940
+ }
941
+ }
942
+ assert(!existsSync(join(nonoteProj, 'Verified', '命题', 'p-blocked-defect.md')), 'require after a blocked→defect downgrade: still no Verified card (undecided, not "false")')
943
+ await callTool('vibe_math_abort', {}, RI)
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
+
1050
+ // ===============================================================
1051
+ // 8c. the injected text obeys the five hard requirements of contract §6
1052
+ // ===============================================================
1053
+ section('8c the injected text uses full tool names, Result (not verdict) and the run-before-archive rule')
1054
+ const RJ = makeRoot()
1055
+ await callTool('vibe_math_new_project', { name: 'lean-workline' }, RJ)
1056
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'require' }), RJ)
1057
+ await callTool('vibe_math_add_problem', { id: 'q-defect', description: '顺手形式化的对象', priority: 0 }, RJ)
1058
+ const workProj = projRoot('lean-workline')
1059
+ // a `meta.formal` defect on the WORK-round path (absorbFormalFromReply) must downgrade too
1060
+ await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'q-defect', content: 'theorem q_defect : 2 + 2 = 4 := by decide\n' }, RJ)
1061
+ await callTool('vibe_math_start', {}, RJ)
1062
+ assert(await drive(RJ, () => !!lastSpawn(RJ, 'explorer:q-defect'), 'explorer:q-defect'), 'the explorer for q-defect was spawned')
1063
+ {
1064
+ const p = lastSpawn(RJ, 'explorer:q-defect').prompt
1065
+ assert(/归档前先跑通(vibe_math_lean_run 或 run=true);跑不通的定义不要进可复用库。/.test(p),
1066
+ '★ the work-round prompt requires a GREEN run before archiving a reusable definition (§6 hard requirement 3)')
1067
+ assert(/"decision":"used\|blocked\|defect"/.test(p), 'the work-round reply contract advertises the defect decision too')
1068
+ assert(!/verdict/.test(p) && !/(^|[^a-z_])lean_(run|archive|lib)/.test(p),
1069
+ '★ the work-round prompt uses FULL tool names only and never the v4/v5 field name `verdict`')
1070
+ fireEnd(lastSpawn(RJ, 'explorer:q-defect').childId, {
1071
+ meta: { kind: 'directions', qid: 'q-defect', formal: { target: 'q-defect', decision: 'defect', note: '陈述里的自然数范围被写成了整数' },
1072
+ directions: [{ id: 'd1', title: '直接形式化', method: 'Lean', core_assumption: '', feasibility: 0.6 }] },
1073
+ })
1074
+ await sleep(260)
1075
+ const st = await callTool('vibe_math_status', {}, RJ)
1076
+ const rec = st.formal.objects.find((o) => o.target === 'q-defect')
1077
+ assert(!!rec && rec.status === 'attempted' && /自然数范围被写成了整数/.test(rec.note || ''),
1078
+ '★ a `meta.formal` defect from a WORK reply downgrades the record too (absorbFormalFromReply, not just the verifier path)')
1079
+ assert(!existsSync(join(workProj, 'Verified', 'Lean', 'q-defect.lean')), '★ and its archived proof is deleted')
1080
+ }
1081
+ await callTool('vibe_math_abort', {}, RJ)
1082
+
1083
+ section('8c-2 the fidelity branch reaches BOTH the review and the debate prompt, and names Result')
1084
+ const RK = makeRoot()
1085
+ await callTool('vibe_math_new_project', { name: 'lean-fidelity' }, RK)
1086
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { formalVerify: 'encourage', debateMaxRounds: 2 }), RK)
1087
+ await callTool('vibe_math_add_proposition', { id: 'p-fid', 概述: '忠实性审查措辞观察对象', 概率: 0.6, 分类: '数论' }, RK)
1088
+ await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-fid', content: 'theorem p_fid : 2 + 2 = 4 := by decide\n' }, RK)
1089
+ await callTool('vibe_math_start', {}, RK)
1090
+ const fidBatch = await runVerifyRound(RK, 'p-fid', [0.9, 0.95])
1091
+ assert(fidBatch !== null, 'fidelity: the review round was asked')
1092
+ if (fidBatch) {
1093
+ const vp = fidBatch.map((s) => s.prompt).join('\n')
1094
+ assert(/一致 → Result = 1/.test(vp), "★ the review prompt states the faithful case as `Result = 1` (v3's REAL reply field, not v4/v5's verdict)")
1095
+ assert(/发现任何偏差,不要投 0/.test(vp) && /形式化不合格/.test(vp), '★ and forbids expressing a fidelity defect as 0')
1096
+ assert(/formal:\{decision:'defect', note:'<具体偏差>'\}/.test(vp), 'and points at the defect reply field to record it')
1097
+ assert(/独立于这份 Lean 代码/.test(vp), 'only an INDEPENDENT refutation may be voted 0')
1098
+ assert(/Result/.test(vp) && !/verdict/.test(vp), '★ the voting prompt names Result, never verdict (§6 hard requirement 2)')
1099
+ assert(vp.indexOf('偏离 → 0') === -1, '★ no "偏离 → 0" instruction anywhere in the fidelity branch (contract §10 item 9)')
1100
+ }
1101
+ assert(await drive(RK, () => wakes.some((w) => w.rootId === RK.id && /交流群/.test(w.prompt)), 'debate prompt'), 'a non-consensus fidelity round moved to the debate')
1102
+ {
1103
+ const dp = wakes.filter((w) => w.rootId === RK.id).map((w) => w.prompt).join('\n')
1104
+ assert(/一致 → Result = 1/.test(dp) && /不要投 0/.test(dp), '★ the DEBATE prompt carries the same fidelity wording')
1105
+ assert(/Result/.test(dp) && !/verdict/.test(dp), 'the debate prompt names Result, never verdict')
1106
+ assert(dp.indexOf('偏离 → 0') === -1, 'the debate prompt also refuses "a deviation is a 0"')
1107
+ }
1108
+ await callTool('vibe_math_abort', {}, RK)
1109
+
759
1110
  // ===============================================================
760
1111
  // 9. reporting + persistence
761
1112
  // ===============================================================
@@ -792,8 +1143,11 @@ section('10 the captured prompt corpus is written for human review')
792
1143
  mkdirSync(CORPUS_DIR, { recursive: true })
793
1144
  writeFileSync(join(CORPUS_DIR, 'formal-verify-v3.json'), JSON.stringify({ entries: corpus }, null, 2), 'utf8')
794
1145
  const md = ['# V3 形式化验证交互语料(prompt corpus)', '',
795
- '> 由 `formal-verify-v3.test.mjs` 落盘:框架**真正发出**的每一条提示词原文(工作区路径归一化为 `<WS>`,可 diff)。',
796
- '> 覆盖:explorer / solver / method-keeper 的日常工作提示词、三种模式下的表决初评与辩论提示词、规划提示词。', '']
1146
+ '> 由 `formal-verify-v3.test.mjs` 落盘:框架**真正发出**的每一条提示词原文。路径归一化:工作区 → `<WS>`,',
1147
+ '> VibeMath 根 → `<VIBEMATH>`(两者都按正/反斜杠两种写法替换,因此语料是确定性的、可 diff 的、不泄露本机路径)。',
1148
+ '> 覆盖:explorer / solver / method-keeper 的日常工作提示词(含「顺手形式化」与"归档前先跑通"),',
1149
+ '> `off`(零 Lean 文本)、`encourage`、**`require`** 三档下的表决初评与辩论提示词,`passed` 之后的忠实性审查分支',
1150
+ '> (含 `defect` 出口),以及规划提示词。', '']
797
1151
  for (let i = 0; i < corpus.length; i++) {
798
1152
  const c = corpus[i]
799
1153
  md.push('## [' + i + '] ' + c.kind + ' · ' + c.label)
@@ -809,10 +1163,35 @@ section('10 the captured prompt corpus is written for human review')
809
1163
  assert(corpus.some((c) => c.label.startsWith('explorer:')) && corpus.some((c) => c.label.startsWith('solver:')) && corpus.some((c) => c.label.startsWith('method-keeper')) && corpus.some((c) => c.label.startsWith('verifier:')), 'the corpus covers every interaction type this suite drives')
810
1164
  assert(corpus.some((c) => c.kind === 'wake'), 'the corpus also keeps the continuation prompts (debate rounds)')
811
1165
  // generic sweep over EVERY captured prompt, not spot checks (AUDIT §2.1)
812
- const dirty = corpus.filter((c) => /\[object Object\]|\bNaN\b|\bundefined\b/.test(c.prompt))
1166
+ const dirty = corpus.filter((c) => /\[object Object\]|\bNaN\b|:\s*undefined|["']undefined["']|undefined\s*[,}\]]/.test(c.prompt))
813
1167
  assert(dirty.length === 0, 'no captured prompt contains placeholder garbage (' + dirty.map((d) => d.label).join(',') + ')')
814
1168
  const joined = corpus.map((c) => c.prompt).join('\n')
815
1169
  assert(joined.indexOf(WS) === -1 && joined.indexOf(WS.replace(/\\/g, '/')) === -1, 'every captured prompt normalises the workspace path to <WS> (the corpus stays diffable)')
1170
+ assert(joined.indexOf(VIBE) === -1 && joined.indexOf(VIBE.replace(/\\/g, '/')) === -1 && joined.indexOf('<VIBEMATH>') !== -1,
1171
+ '★ every captured prompt normalises the VibeMath root to <VIBEMATH> (no machine path leaks into the shipped corpus, contract §10 item 10)')
1172
+ assert(!corpus.some((c) => c.root === RA.id && /Lean|形式化/.test(c.prompt)),
1173
+ '★ the off-mode prompts captured in the corpus contain ZERO Lean text (off stays a true no-op)')
1174
+ // contract §10 item 10: the corpus must cover require AND the work round (not just encourage + fidelity)
1175
+ assert(corpus.some((c) => /【Lean 形式化验证(鼓励模式)】/.test(c.prompt)), '★ the corpus covers the encourage-mode verification prompt')
1176
+ assert(corpus.some((c) => /【Lean 形式化验证(强制模式)】/.test(c.prompt)), '★ the corpus covers the REQUIRE-mode verification prompt')
1177
+ assert(corpus.some((c) => /【顺手形式化(鼓励)】/.test(c.prompt)) && corpus.some((c) => /【顺手形式化(强制)】/.test(c.prompt)), '★ the corpus covers the work-round 顺手形式化 prompt in both modes')
1178
+ assert(corpus.some((c) => /一致 → Result = 1/.test(c.prompt)), '★ the corpus keeps the passed/fidelity branch verbatim for human review')
1179
+ // contract §6 hard requirements 1-2 + §10 item 9, swept over EVERY captured prompt
1180
+ const verifier = corpus.filter((c) => c.label.startsWith('verifier:'))
1181
+ assert(verifier.length >= 5 && verifier.every((c) => /Result/.test(c.prompt) && !/verdict/.test(c.prompt)),
1182
+ '★ every captured voting prompt names Result and never verdict (§6 hard requirement 2)')
1183
+ const bareTools = corpus.filter((c) => /(^|[^a-z_])lean_(run|archive|lib)/.test(c.prompt))
1184
+ assert(bareTools.length === 0, '★ no captured prompt abbreviates a Lean tool name (§6 hard requirement 1): ' + bareTools.map((b) => b.label).join(','))
1185
+ const zeroDeviation = corpus.filter((c) => c.prompt.indexOf('偏离 → 0') !== -1)
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)')
816
1195
  }
817
1196
 
818
1197
  console.log('')