dsh-vibe-math 2.7.1 → 2.7.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 (114) hide show
  1. package/README.en.md +682 -690
  2. package/README.md +19 -27
  3. package/cordis.patch.yml +112 -16
  4. package/docs/AUDIT-CHECKLIST.md +744 -518
  5. package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
  6. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  7. package/docs/final-paper.md +172 -172
  8. package/docs/formal-verification.md +588 -588
  9. package/docs/math-computation.md +194 -163
  10. package/docs/parameter-schema.md +37 -0
  11. package/docs/release-notes/README.en.md +49 -0
  12. package/docs/release-notes/README.md +48 -0
  13. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
  14. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
  15. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  16. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  17. package/docs/release-notes/TEMPLATE.md +126 -0
  18. package/docs/status-report-fields.md +167 -0
  19. package/docs/test-timing.md +157 -154
  20. package/installer.js +27 -17
  21. package/package.json +55 -3
  22. package/prompt-corpus-persona/persona-corpus.json +9 -9
  23. package/prompt-corpus-persona/persona-corpus.md +225 -33
  24. package/prompt-corpus-v2/formal-verify-v2.json +305 -113
  25. package/prompt-corpus-v2/formal-verify-v2.md +2399 -279
  26. package/prompt-corpus-v3/formal-verify-v3.json +134 -110
  27. package/prompt-corpus-v3/formal-verify-v3.md +409 -141
  28. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  29. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  30. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  31. package/prompt-corpus-v5/prompt-corpus-v5.md +513 -259
  32. package/scripts/update-doc-counts.mjs +71 -0
  33. package/tests/audit-artifact-docs.mjs +72 -0
  34. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  35. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  36. package/tests/audit-installer-compat.test.mjs +52 -4
  37. package/tests/audit-installer-policy.test.mjs +41 -8
  38. package/tests/audit-math-computation-contract.mjs +107 -9
  39. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  40. package/tests/audit-math-computation-parity.mjs +164 -3
  41. package/tests/audit-math-computation-sensitivity.mjs +6 -4
  42. package/tests/audit-package-membership.mjs +57 -0
  43. package/tests/audit-package-membership.mutants.mjs +28 -0
  44. package/tests/audit-participant-set-parity.mjs +160 -0
  45. package/tests/audit-path-discipline.mjs +83 -0
  46. package/tests/audit-path-discipline.mutants.mjs +51 -0
  47. package/tests/audit-persona-sensitivity.mjs +14 -0
  48. package/tests/audit-persona-surface.mutants.mjs +69 -0
  49. package/tests/audit-persona-surface.test.mjs +63 -22
  50. package/tests/audit-prompt-duplication.mjs +143 -0
  51. package/tests/audit-prompt-duplication.mutants.mjs +50 -0
  52. package/tests/audit-prompt-invariants.mjs +43 -5
  53. package/tests/audit-readme-bilingual.test.mjs +152 -0
  54. package/tests/audit-readme-counts.mjs +95 -0
  55. package/tests/audit-readme-counts.mutants.mjs +52 -0
  56. package/tests/audit-registration.mjs +78 -0
  57. package/tests/audit-status-report-fields.mjs +172 -0
  58. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  59. package/tests/audit-v3-registration-parity.mjs +156 -0
  60. package/tests/audit-v5-integrity.mjs +300 -13
  61. package/tests/audit-v5-lean-abstention.mjs +392 -0
  62. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  63. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  64. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  65. package/tests/audit-v5-sensitivity.mjs +15 -8
  66. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  67. package/tests/e2e-v5-round2.test.mjs +566 -12
  68. package/tests/formal-verify-v2.test.mjs +91 -5
  69. package/tests/formal-verify-v3.mutants.mjs +117 -0
  70. package/tests/formal-verify-v3.test.mjs +85 -6
  71. package/tests/formal-verify-v4.mutants.mjs +152 -0
  72. package/tests/formal-verify-v4.test.mjs +628 -9
  73. package/tests/formal-verify-v5.test.mjs +68 -9
  74. package/tests/helpers/doc-counts-format.mjs +41 -0
  75. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  76. package/tests/math-computation-a1.mutants.mjs +72 -0
  77. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  78. package/tests/math-computation-discovery.mutants.mjs +76 -0
  79. package/tests/math-computation-shared.test.mjs +396 -2
  80. package/tests/math-computation-v2.test.mjs +767 -0
  81. package/tests/math-computation-v3.test.mjs +759 -0
  82. package/tests/math-computation-v4.mutants.mjs +133 -0
  83. package/tests/math-computation-v4.test.mjs +739 -0
  84. package/tests/prompt-v5-integrity.test.mjs +13 -2
  85. package/tests/run-tests.mjs +139 -9
  86. package/tests/run-tests.mutants.mjs +70 -0
  87. package/tests/selfdrive-v5.mjs +53 -1
  88. package/tests/selfdrive-v5.mutants.mjs +132 -0
  89. package/tests/v2-fix-probes.mutants.mjs +100 -0
  90. package/tests/v2-fix-probes.test.mjs +1039 -0
  91. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  92. package/tests/v3-fix-probes.mutants.mjs +95 -0
  93. package/tests/v3-fix-probes.test.mjs +891 -0
  94. package/tests/v5-institute-fixes.mutants.mjs +190 -0
  95. package/vibe-math-v2/agent.cordis.yml +62 -6
  96. package/vibe-math-v2/math-computation.js +229 -29
  97. package/vibe-math-v2/math-engines.js +12 -12
  98. package/vibe-math-v2/vibe-math-v2.js +458 -119
  99. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
  100. package/vibe-math-v3/agent.cordis.yml +72 -6
  101. package/vibe-math-v3/math-computation.js +229 -29
  102. package/vibe-math-v3/math-engines.js +12 -12
  103. package/vibe-math-v3/vibe-math-v3.js +482 -139
  104. package/vibe-math-v4/agent.cordis.yml +44 -8
  105. package/vibe-math-v4/math-computation.js +229 -29
  106. package/vibe-math-v4/math-engines.js +12 -12
  107. package/vibe-math-v4/vibe-math-v4.js +245 -52
  108. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
  109. package/vibe-math-v5/agent.cordis.yml +46 -12
  110. package/vibe-math-v5/math-computation.js +229 -29
  111. package/vibe-math-v5/math-engines.js +12 -12
  112. package/vibe-math-v5/vibe-math-v5.js +1307 -243
  113. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1655
  114. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -28,11 +28,20 @@
28
28
  //
29
29
  // Run: node tests/formal-verify-v2.test.mjs
30
30
  // ============================================================
31
- import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, writeFileSync, statSync, mkdirSync } from 'node:fs'
31
+ import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, writeFileSync, statSync, mkdirSync, renameSync } from 'node:fs'
32
32
  import { tmpdir } from 'node:os'
33
33
  import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
34
34
  import { fileURLToPath, pathToFileURL } from 'node:url'
35
35
 
36
+ // Concurrency isolation: the shipped corpus is read by other probes (audit-prompt-invariants) while
37
+ // this suite runs under the gate, so the write must be atomic — a reader must never see a truncated file.
38
+ function writeFileAtomic(target, text) {
39
+ const tmp = target + '.tmp-' + process.pid + '-' + Math.random().toString(36).slice(2, 8)
40
+ writeFileSync(tmp, text, 'utf8')
41
+ renameSync(tmp, target)
42
+ }
43
+
44
+
36
45
  // V2_PLUGIN may be a REPO-RELATIVE path or an ABSOLUTE one (a mutant copy outside the repo, e.g. a
37
46
  // temp dir). The previous `new URL('file:///' + path)` form only worked for drive-qualified Windows
38
47
  // paths and made out-of-repo variants impossible; this is the same isAbsolute-aware resolution the
@@ -1210,6 +1219,26 @@ section('15 the require gate also covers the judge-problem transfer')
1210
1219
  assert(!existsSync(join(proj, 'Verified', '数论_Verified.json')), 'no Verified card for the source proposition')
1211
1220
  }
1212
1221
 
1222
+ // ---------- 15g. P2 #7: the accuracy-sample caps are documented AND announced ----------
1223
+ // The caps (8 samples per object, 32 objects) are deliberate bounded retention, but they used to
1224
+ // evict SILENTLY — the H4 accuracy statistic then drifted with the object count and nothing said so.
1225
+ // This case drives a long debate (2 verifiers × several rounds ⇒ > 8 samples for one object) and
1226
+ // requires the eviction to show up in the activity log.
1227
+ section('15g P2 #7: accuracy-sample cap eviction is announced, not silent')
1228
+ {
1229
+ const h = await makeCase('score-cap-warn')
1230
+ // 一轮就凑够样本:10 个验证器 ⇒ 同一对象一次记 10 条(> 每对象上限 8),无需多轮辩论。
1231
+ await h.call('vibe_math_set_params', { verifierCount: 10, maxParallelThreshold: 20, debateMaxRounds: 1, verdictMode: 'flat' })
1232
+ await h.call('vibe_math_add_proposition', { id: 'p-cap', 概述: '计分上限告警', 概率: 0.6, 分类: '数论' })
1233
+ await startScheduler(h)
1234
+ const cand = await waitFor(() => { const x = verifiersOf(h, 'r-p-cap'); return x.length >= 10 ? x : undefined }, 80, 250)
1235
+ assert(!!cand, '对照:10 个验证器都被派出(实测 verifier spawns=' + h.spawns.filter((s) => s.label.indexOf('verifier') === 0).length + ')')
1236
+ if (cand) { fireVerdicts(h, cand, 0.5); await sleep(400); await tick() }
1237
+ const acts = (await h.call('vibe_math_status', {})).recentActivity.map((a) => a.detail).join('\n')
1238
+ assert(/评审计分样本达上限/.test(acts), '★★ [P2#7] 超过每对象 8 条待计分样本时**写告警**,不再静默丢弃(verifier spawns=' + h.spawns.filter((s) => s.label.indexOf('verifier') === 0).length + ' verifiers=' + verifiersOf(h, 'r-p-cap').length + ' followups=' + h.followups.length + ' acts=' + JSON.stringify(acts.slice(-260)) + ')')
1239
+ await h.call('vibe_math_abort', {})
1240
+ }
1241
+
1213
1242
  // ---------- 16. the prompt corpus (contract §10.10) ----------
1214
1243
  // A HUMAN must be able to re-read every prompt the framework emitted, not just the assertions
1215
1244
  // about them. Paths are normalised so the dump is deterministic, diffable and machine-free.
@@ -1292,7 +1321,9 @@ section('15c the framework push report and the gate notice are captured')
1292
1321
  else await sleep(150)
1293
1322
  }
1294
1323
  assert(!!gline && /require 模式搁置/.test(gline), '★★ the require-gate withholding reaches the human-readable channel (captured verbatim)')
1295
- assert(!!gline && /Formal\/TODO\.md/.test(gline), 'and the notice says where the withholding was recorded')
1324
+ assert(!!gline && /Formal\/TODO\.md/.test(gline), 'and the notice says where the withholding was recorded')
1325
+ assert(/下一步:/.test(String(gline)) && /代码 formal-required/.test(String(gline)) && !/(当前状态 none)/.test(String(gline)),
1326
+ '★★ [A5] require-gate 反馈行给出**下一步**且状态已本地化(代码 token 保留),实测 ' + JSON.stringify(gline))
1296
1327
  g.feedbackLine = gline || ''
1297
1328
  await g.call('vibe_math_pause', {})
1298
1329
  }
@@ -1636,6 +1667,49 @@ async function driveSites(h, opts, qid) {
1636
1667
  await hv.call('vibe_math_abort', {})
1637
1668
  }
1638
1669
 
1670
+ // ---------- 15f. P2-v2 D4: ONE participant set / ONE count across the two views ----------
1671
+ // Pinned definition (must stay in sync with the plugin's voteCount() doc comment):
1672
+ // * participant set of a verify task = `t.children` of the CURRENT round (their `childResults`
1673
+ // entries are that round's votes);
1674
+ // * "how many votes are required" = the task's **creation-time snapshot** `t.expectedCount`
1675
+ // (written when processVerify creates the task) — NEVER live `params.verifierCount` /
1676
+ // `params.maxParallelThreshold`.
1677
+ // Before the convergence the answer lived in three places (`consensus()` over reported votes with
1678
+ // the MIN_REVIEWERS constant, `finalizeVerification()` counting again, and two inline `allReported`
1679
+ // checks over `t.children`), so a mid-round hire/fire could make the debate view and the verdict
1680
+ // view disagree about whether the round is complete.
1681
+ section('15f P2-v2 D4: both views read ONE participant set (snapshot + round)')
1682
+ {
1683
+ const h = await makeCase('d4-v2-midvote')
1684
+ await h.call('vibe_math_set_params', { verifierCount: 2, debateMaxRounds: 2, verdictMode: 'flat' })
1685
+ await h.call('vibe_math_add_proposition', { id: 'p-d4v2', 概述: 'D4 v2 参与集', 概率: 0.6, 分类: '数论' })
1686
+ await startScheduler(h)
1687
+ const staffed = await waitFor(() => { const x = h.spawns.filter((s) => s.label.startsWith('verifier:r-p-d4v2')); return x.length >= 2 ? x : undefined }, 60, 250)
1688
+ assert(!!staffed, 'two reviewers were staffed for the round (the creation-time snapshot)')
1689
+ const pick = (o) => ((o && o.verifyTasks) || []).filter((t) => String(t.rId).indexOf('p-d4v2') !== -1)[0]
1690
+ const st1 = pick(await h.call('vibe_math_status', {}))
1691
+ const rp1 = pick(await h.call('vibe_math_report', {}))
1692
+ assert(!!st1 && !!rp1 && st1.expected === 2 && st1.dispatched === 2 && st1.round === 1,
1693
+ '★★ [D4-v2] the projection reports the round snapshot: expected=2 dispatched=2 round=1(实测 ' + JSON.stringify({ status: st1, report: rp1 }) + ')')
1694
+ assert(JSON.stringify(st1) === JSON.stringify(rp1),
1695
+ '★★★ [D4-v2] status 与 report 两个视图对同一任务的参与集/票数**逐字段相同**(一个来源;实测 ' + JSON.stringify({ status: st1, report: rp1 }) + ')')
1696
+ // mid-vote "hire more": the office raises verifierCount. The in-flight round keeps its snapshot —
1697
+ // a view that re-read the live roster/params would now claim 4.
1698
+ await h.call('vibe_math_set_params', { verifierCount: 4 })
1699
+ const after = pick(await h.call('vibe_math_status', {}))
1700
+ const afterR = pick(await h.call('vibe_math_report', {}))
1701
+ assert(!!after && after.expected === 2 && after.dispatched === 2,
1702
+ '★★★ [D4-v2] 中途调高 verifierCount **不改变**在飞轮次的参与集/需求票数(快照仍是 2;实测 ' + JSON.stringify(after) + ')')
1703
+ assert(JSON.stringify(after) === JSON.stringify(afterR),
1704
+ '★★ [D4-v2] 调参后两个视图仍然逐字段相同(实测 ' + JSON.stringify({ status: after, report: afterR }) + ')')
1705
+ // 6.5:两条路径(buildReport / getStatus)都必须走同一个 verifyTasksView(),整数组逐字段相同。
1706
+ const stAll = (await h.call('vibe_math_status', {})).verifyTasks || []
1707
+ const rpAll = (await h.call('vibe_math_report', {})).verifyTasks || []
1708
+ assert(stAll.length >= 1 && JSON.stringify(stAll) === JSON.stringify(rpAll),
1709
+ '★★★★ [6.5] status/report 的 verifyTasks 由同一个 verifyTasksView() 产出、逐字段相同(实测 ' + JSON.stringify({ status: stAll, report: rpAll }) + ')')
1710
+ await h.call('vibe_math_abort', {})
1711
+ }
1712
+
1639
1713
  section('16 the captured prompt corpus is written for human review')
1640
1714
  {
1641
1715
  // Freeze the scheduler in every case FIRST: a still-running tick loop could emit one more
@@ -1651,7 +1725,11 @@ section('16 the captured prompt corpus is written for human review')
1651
1725
  .split(ws).join('<WS>')
1652
1726
  // Epoch-ms in a verification-log FILE NAME is run metadata: the final-paper material embeds the
1653
1727
  // evidence index verbatim, so it must be normalised or the shipped corpus changes every run.
1654
- .replace(/_[0-9]{10,16}\.json/g, '_<TIME>.json')
1728
+ // 为什么必须保留可选短 id 组(不要"简化"掉):P7 把裁决日志名改成了
1729
+ // `<rId>_<epochMs>_<shortId>.json`,旧的 `_[0-9]{10,16}\.json` 因此不再匹配,
1730
+ // 于是每次跑动都把自己那次的 时间戳+shortId 写进 shipped 语料(语料非确定性)。
1731
+ // 去掉 `(_[0-9a-f]{8})?` 这个组会静默地重新引入"每次跑都不同"的语料。
1732
+ .replace(/_[0-9]{10,16}(_[0-9a-f]{8})?\.json/g, '_<TIME>.json')
1655
1733
  }
1656
1734
  const entries = []
1657
1735
  for (const h of hosts) {
@@ -1668,8 +1746,16 @@ section('16 the captured prompt corpus is written for human review')
1668
1746
  // gate's withholding notice. It is captured once, in case 15c, for determinism.
1669
1747
  if (typeof h.feedbackLine === 'string' && h.feedbackLine) entries.push({ kind: 'feedback', case: h.label, label: 'activity-log', prompt: scrub(h, h.feedbackLine) })
1670
1748
  }
1749
+ // F7:v2 各回执的对象类型不同(验证用 Result/Reason 大写;计划/求解/方法卡用小写键),历史上只在示例里
1750
+ // 体现大小写。现在每一条**含 json 契约**的提示词都必须显式写明「键名大小写精确匹配、不接受别名」。
1751
+ const jsonFenced = entries.filter((e) => e.prompt.indexOf('```json') !== -1 || /single JSON object/.test(e.prompt))
1752
+ const missingCaseRule = jsonFenced.filter((e) => e.prompt.indexOf('KEY CASE IS PART OF THE REPLY CONTRACT') === -1)
1753
+ assert(jsonFenced.length >= 4, 'F7: prompts carrying a json contract were captured (' + jsonFenced.length + ')')
1754
+ assert(missingCaseRule.length === 0, '★★ [F7] every json-contract prompt states the canonical key-case rule (offenders=' + JSON.stringify(missingCaseRule.map((e) => e.label)) + ')')
1755
+ const capitalised = entries.filter((e) => /"Result":/.test(e.prompt))
1756
+ assert(capitalised.every((e) => e.prompt.indexOf('KEY CASE IS PART OF THE REPLY CONTRACT') !== -1), '★★ [F7] …including the capitalised Result/Reason contracts (' + capitalised.length + ')')
1671
1757
  mkdirSync(CORPUS_DIR, { recursive: true })
1672
- writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.json'), JSON.stringify({ entries: entries }, null, 2), 'utf8')
1758
+ writeFileAtomic(join(CORPUS_DIR, 'formal-verify-v2.json'), JSON.stringify({ entries: entries }, null, 2))
1673
1759
  const md = ['# V2 形式化验证交互语料(prompt corpus)', '',
1674
1760
  '> 由 `formal-verify-v2.test.mjs` 落盘:框架**真正发出**的每一条提示词原文。',
1675
1761
  '> 工作区路径归一化为 `<WS>`、VibeMath 根归一化为 `<VIBEMATH>`,因此可 diff、不泄露本机路径。',
@@ -1685,7 +1771,7 @@ section('16 the captured prompt corpus is written for human review')
1685
1771
  md.push('```')
1686
1772
  md.push('')
1687
1773
  }
1688
- writeFileSync(join(CORPUS_DIR, 'formal-verify-v2.md'), md.join('\n'), 'utf8')
1774
+ writeFileAtomic(join(CORPUS_DIR, 'formal-verify-v2.md'), md.join('\n'))
1689
1775
  assert(existsSync(join(CORPUS_DIR, 'formal-verify-v2.json')) && existsSync(join(CORPUS_DIR, 'formal-verify-v2.md')), 'the prompt corpus was written (JSON + Markdown)')
1690
1776
  assert(entries.length >= 15, 'the corpus covers the whole run (' + entries.length + ' prompts)')
1691
1777
  assert(entries.some((e) => /【Lean 形式化验证(鼓励模式)】/.test(e.prompt)), 'the corpus contains the encourage verify prompt')
@@ -0,0 +1,117 @@
1
+ // Registered mutant harness for the v3 formal-verify suite — the suite carries its own proof.
2
+ // Mirrors tests/formal-verify-v4.mutants.mjs and tests/v5-institute-fixes.mutants.mjs: each family is a
3
+ // SINGLE content-anchored mutation of the v3 preset (no line numbers), the suite runs through the
4
+ // V3_PLUGIN seam, child runs are bounded (a timeout is a HANG, never a pass), and skipped families are
5
+ // printed and counted. Exit criterion: every family reddens BY NAME; ALL MUTANTS RED AS REQUIRED.
6
+ // Run: node tests/formal-verify-v3.mutants.mjs
7
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
8
+ import { execFileSync } from 'node:child_process'
9
+ import { dirname, join } from 'node:path'
10
+ import { tmpdir } from 'node:os'
11
+ import { fileURLToPath } from 'node:url'
12
+ const HERE = dirname(fileURLToPath(import.meta.url))
13
+ const REPO = join(HERE, '..')
14
+ const PRESET = 'vibe-math-v3'
15
+ const MAIN = PRESET + '.js'
16
+ const SUITE = 'tests/formal-verify-v3.test.mjs'
17
+ const ENV = 'V3_PLUGIN'
18
+ // Measured: the v3 formal-verify suite is ~40 s per child run; MUTANT_CHILD_TIMEOUT_MS overrides it.
19
+ const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 300000)
20
+ const TIMES = []
21
+ const hangs = []
22
+ const skipped = []
23
+ function copyGraph(file, dest) {
24
+ const src = join(REPO, PRESET, file)
25
+ copyFileSync(src, join(dest, file))
26
+ for (const m of readFileSync(src, 'utf8').matchAll(/from\s+'(\.\/[A-Za-z0-9_.-]+\.js)'/g)) {
27
+ const dep = m[1].slice(2)
28
+ if (!readdirSync(dest).includes(dep)) copyGraph(dep, dest)
29
+ }
30
+ }
31
+ function runFamily(f) {
32
+ const dest = join(tmpdir(), 'v3fv-mut-' + Math.random().toString(36).slice(2, 10))
33
+ mkdirSync(dest, { recursive: true })
34
+ copyGraph(MAIN, dest)
35
+ const target = join(dest, f.editFile || MAIN)
36
+ const before = readFileSync(target, 'utf8')
37
+ const n = before.split(f.from).length - 1
38
+ const okCount = f.lastOnly || f.all ? n >= 1 : n === 1
39
+ if (!okCount) {
40
+ skipped.push(f.name + ' (anchor x' + n + ')')
41
+ console.error(' SKIP - ' + f.name + ' (anchor x' + n + ')')
42
+ rmSync(dest, { recursive: true, force: true })
43
+ return false
44
+ }
45
+ const mutated = before.split(f.from).join(f.to)
46
+ writeFileSync(target, mutated, 'utf8')
47
+ try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
48
+ catch (e) {
49
+ writeFileSync(target, before, 'utf8')
50
+ skipped.push(f.name + ' (mutation did not compile)')
51
+ console.error(' SKIP - ' + f.name + ' (mutation did not compile; restored)')
52
+ rmSync(dest, { recursive: true, force: true })
53
+ return false
54
+ }
55
+ const t0 = Date.now()
56
+ let out = ''
57
+ let code = 0
58
+ let hang = false
59
+ try {
60
+ out = execFileSync(process.execPath, [SUITE], {
61
+ cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL',
62
+ env: Object.assign({}, process.env, { [ENV]: join(dest, MAIN) }),
63
+ })
64
+ } catch (e) {
65
+ if (e && (e.killed || e.signal === 'SIGKILL')) hang = true
66
+ code = (e && e.status) || 1
67
+ out = String((e && e.stdout) || '') + String((e && e.stderr) || '')
68
+ }
69
+ const ms = Date.now() - t0
70
+ TIMES.push([f.name, ms])
71
+ if (hang) hangs.push(f.name + '(' + Math.round(ms / 1000) + 's)')
72
+ const named = out.split(/\r?\n/).filter((l) => /^\s*(FAIL - | {2}- )/.test(l) && f.expect.test(l)).map((l) => l.trim())
73
+ const ok = !hang && code !== 0 && named.length > 0
74
+ console.log((hang ? ' HANG - ' : (ok ? ' ok - ' : ' FAIL - ')) + f.name + ' [' + ms + 'ms]' + (named.length ? ' :: ' + named[0].slice(0, 150) : ' :: no named red (exit=' + code + ')'))
75
+ rmSync(dest, { recursive: true, force: true })
76
+ return ok
77
+ }
78
+ // MEASURED BOUNDARY (V3-G2): at this revision NO strictly count-preserving mutant exists for the
79
+ // re-eligible-SLOT-set assertion. Reason (measured in-product): a re-verification creates a FRESH
80
+ // task with `children: []` and `round: 1` (vibe-math-v3.js:2164) and the slot index comes from
81
+ // `const index = t.children.length` (:2172), so any fixed index offset shifts round one too and the
82
+ // two slot sets stay equal. The only mutant that reddens it therefore also changes the COUNT - it is
83
+ // labelled NOT COUNT-PRESERVING below, and it is landed because the assertion itself is the semantic
84
+ // unit (a re-verification that re-armed different slots satisfies the old count).
85
+ //
86
+ // TWO CANDIDATE MUTANTS WERE THEN MEASURED - BOTH REJECTED (so no V3-G2 family is shipped):
87
+ // (i) the ruled shape (children: [] inherits the previous children) is INERT: the concluded verify
88
+ // task is already deleted when the new one is built, so `tasks['verify:'+rId]` is undefined and
89
+ // the expression degrades to `[]` => the suite ran to exit 0 with NO named red (a mutant that
90
+ // changes nothing proves nothing);
91
+ // (ii) the broad form (verifier backfill disabled entirely) made the suite exceed the 300 s child
92
+ // cap => classified as a HANG, which is not a valid red either.
93
+ // => RECORDED BOUNDARY: at this revision the re-eligible-SLOT-set assertion ships WITHOUT a
94
+ // discriminating mutant (measured, not assumed). The assertion itself is landed and green.
95
+
96
+ const FAMILIES = [
97
+ {
98
+ // V3-G1: every verifier spawn for one object is stamped with the SAME slot index, so two "independent
99
+ // reviews" are really two copies of one slot. The fired-verifier COUNT stays 2 (the old assertion
100
+ // passes); the distinct-slot assertion is what catches it.
101
+ name: 'V3-G1: every review is stamped with the same slot index (count still 2)',
102
+ from: "'verifier:' + t.rId + ':' + index",
103
+ to: "'verifier:' + t.rId + ':0'",
104
+ expect: /V3-G1 .*DISTINCT verifier slots/,
105
+ },
106
+ ]
107
+ let red = 0
108
+ for (const f of FAMILIES) { const ok = runFamily(f); if (ok) red++ }
109
+ const totalMs = TIMES.reduce((a, t) => a + t[1], 0)
110
+ console.log('')
111
+ console.log('mutant families reddening the v3 formal-verify suite by name: ' + red + '/' + FAMILIES.length)
112
+ console.log('timings (per family): ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
113
+ console.log('TOTAL WALL TIME (all families + setup): ' + totalMs + 'ms (' + Math.round(totalMs / 1000) + 's)')
114
+ console.log('hangs=[' + hangs.join(' | ') + ']')
115
+ console.log('skipped=[' + skipped.join(' | ') + ']')
116
+ if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
117
+ console.log('ALL MUTANTS RED AS REQUIRED')
@@ -19,11 +19,20 @@
19
19
  //
20
20
  // Run: node tests/formal-verify-v3.test.mjs
21
21
  // ============================================================
22
- import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs'
22
+ import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, renameSync } from 'node:fs'
23
23
  import { tmpdir } from 'node:os'
24
24
  import { join, dirname, isAbsolute, resolve as pathResolve } from 'node:path'
25
25
  import { fileURLToPath, pathToFileURL } from 'node:url'
26
26
 
27
+ // Concurrency isolation: the shipped corpus is read by other probes (audit-prompt-invariants) while
28
+ // this suite runs under the gate, so the write must be atomic — a reader must never see a truncated file.
29
+ function writeFileAtomic(target, text) {
30
+ const tmp = target + '.tmp-' + process.pid + '-' + Math.random().toString(36).slice(2, 8)
31
+ writeFileSync(tmp, text, 'utf8')
32
+ renameSync(tmp, target)
33
+ }
34
+
35
+
27
36
  // V3_PLUGIN may be a REPO-RELATIVE path or an ABSOLUTE one (a mutant copy outside the repo, e.g. a
28
37
  // temp dir). The previous `new URL('file:///' + path)` form only worked for drive-qualified Windows
29
38
  // paths and made out-of-repo variants impossible; this is the same isAbsolute-aware resolution the
@@ -34,11 +43,16 @@ const PLUGIN = process.env.V3_PLUGIN
34
43
  const HERE = dirname(fileURLToPath(import.meta.url))
35
44
  // AUDIT-CHECKLIST §2.4: the suite must also keep the INTERACTION TEXT it drove, so a human can
36
45
  // re-read the prompts the framework really emitted. V3_CORPUS_DIR overrides the destination.
37
- const CORPUS_DIR = process.env.V3_CORPUS_DIR ? pathResolve(process.env.V3_CORPUS_DIR) : join(HERE, '..', 'prompt-corpus-v3')
46
+ const CORPUS_DIR = process.env.V3_CORPUS_DIR
47
+ ? pathResolve(process.env.V3_CORPUS_DIR)
48
+ // A MUTATED run (V3_PLUGIN set) must never rewrite the shipped corpus — same rule v2 and v4 already follow.
49
+ : (process.env.V3_PLUGIN ? join(tmpdir(), 'vibe-v3-prompt-corpus') : join(HERE, '..', 'prompt-corpus-v3'))
38
50
  const WS = mkdtempSync(join(tmpdir(), 'vibe-v3-lean-'))
39
51
  const VIBE = join(WS, 'VibeMath')
40
52
  const projRoot = (slug) => join(VIBE, 'Projects', slug)
41
53
  const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '')
54
+ /** A missing directory lists as [] (a mutant cascade must surface as a NAMED red, not an ENOENT crash). */
55
+ const listDirIf = (p) => (existsSync(p) ? readdirSync(p) : [])
42
56
  const reEsc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
43
57
  const verifyRe = (target) => new RegExp('^verifier:r-' + reEsc(target) + '(?:-\\w+)?:\\d+$')
44
58
 
@@ -121,7 +135,11 @@ const scrub = (s) => canonBrief(String(s == null ? '' : s)
121
135
  // Epoch-ms in a verification-log FILE NAME (`Logs/Verification/<rId>_<Date.now()>.json`) is run
122
136
  // metadata too: the final-paper material embeds the evidence index verbatim, so without this the
123
137
  // shipped corpus changed on every run (contract §10 item 10 demands byte determinism).
124
- .replace(/_[0-9]{10,16}\.json/g, '_<TIME>.json')
138
+ // 为什么必须保留可选短 id 组(不要"简化"掉):P7 把裁决日志名改成了
139
+ // `<rId>_<epochMs>_<shortId>.json`,旧的 `_[0-9]{10,16}\.json` 因此不再匹配,
140
+ // 于是每次跑动都把自己那次的 时间戳+shortId 写进 shipped 语料(语料非确定性)。
141
+ // 去掉 `(_[0-9a-f]{8})?` 这个组会静默地重新引入"每次跑都不同"的语料。
142
+ .replace(/_[0-9]{10,16}(_[0-9a-f]{8})?\.json/g, '_<TIME>.json')
125
143
 
126
144
  // A fake Lean: a file PASSES unless it still contains `sorry` or the marker `-- FAIL`.
127
145
  // `-- HANG` simulates a toolchain that never returns (the timeout path).
@@ -302,6 +320,21 @@ async function runVerifyRound(root, target, results, timeoutMs) {
302
320
  await sleep(220)
303
321
  return un
304
322
  }
323
+ /** V3-G1: `unfiredVerifiers(...).length` is a COUNT - the semantic unit is that the fired reviews are
324
+ * DISTINCT slots (the `:<index>` suffix), i.e. two genuinely independent reviews of the same object.
325
+ * A founding that spawns the same slot twice keeps the count at 2 and used to pass. */
326
+ /** The slot indices (`:<index>`) of a fired/eligible verifier round. */
327
+ function slotsOf(round) { return (round || []).map((x) => String(x.label).split(':').pop()) }
328
+ let gateSlots1 = null // V3-G2: round-one slots (module scope: the require round lives in a block)
329
+
330
+ function assertDistinctSlots(round, tag) {
331
+ assert(Array.isArray(round) && round.length > 0, tag + ': verifiers were fired')
332
+ const idx = round.map((x) => String(x.label).split(':').pop())
333
+ const rIds = round.map((x) => String(x.label).split(':')[1])
334
+ assert(new Set(idx).size === idx.length, '* V3-G1 ' + tag + ': the reviews are DISTINCT verifier slots (indices=' + JSON.stringify(idx) + ')')
335
+ assert(new Set(rIds).size === 1, '* V3-G1 ' + tag + ': every review targets the SAME object (targets=' + JSON.stringify(rIds) + ')')
336
+ }
337
+
305
338
  /** Abort + restart: gives the next verification a FRESH task, so a prompt is guaranteed to be
306
339
  * constructed after whatever changed in between (mode switch, archived proof, …). */
307
340
  async function restart(root) {
@@ -324,6 +357,7 @@ await callTool('vibe_math_add_proposition', { id: 'p-off', 概述: '关模式下
324
357
  await callTool('vibe_math_start', {}, RA)
325
358
  const offRound = await runVerifyRound(RA, 'p-off', [1, 1])
326
359
  assert(offRound !== null && offRound.length === 2, 'off: two independent verifiers were asked (投票流程照常)')
360
+ if (offRound) assertDistinctSlots(offRound, 'off round')
327
361
  const offProj = projRoot('lean-off')
328
362
  assert(await drive(RA, () => existsSync(join(offProj, 'Verified', '命题', 'p-off.md')), 'off Verified card'), "'off' still finalizes on a unanimous boolean vote with NO Lean artifact")
329
363
  {
@@ -488,6 +522,7 @@ await callTool('vibe_math_add_proposition', { id: 'p-enc', 概述: '鼓励模式
488
522
  await callTool('vibe_math_start', {}, RD)
489
523
  const encBatch = await runVerifyRound(RD, 'p-enc', [0.9, 0.95])
490
524
  assert(encBatch !== null, 'encourage: the first review round was asked')
525
+ if (encBatch) assertDistinctSlots(encBatch, 'encourage round')
491
526
  if (encBatch) {
492
527
  const vp = encBatch.map((s) => s.prompt).join('\n')
493
528
  assert(/【Lean 形式化验证(鼓励模式)】/.test(vp), 'the review prompt explains the Lean mode')
@@ -701,6 +736,7 @@ const gateProj = projRoot('lean-gate')
701
736
  await callTool('vibe_math_start', {}, RF)
702
737
  {
703
738
  const batch = await runVerifyRound(RF, 'p-gate', [1, 1])
739
+ gateSlots1 = slotsOf(batch) // V3-G2: measured ["0","1"]
704
740
  assert(batch !== null, 'require: the review round was asked')
705
741
  if (batch) {
706
742
  const vp = batch.map((s) => s.prompt).join('\n')
@@ -731,7 +767,7 @@ assert(!existsSync(join(gateProj, 'Verified', '命题', 'p-gate.md')), 'no Verif
731
767
  const st = await callTool('vibe_math_status', {}, RF)
732
768
  assert(st.formal.todo.some((t) => t.id === 'p-gate'), 'status lists the deferred object')
733
769
  assert(/formal-required/.test(st.recentActivity.map((e) => e.detail).join('\n')), 'the activity feed shows the formal-required deferral')
734
- const voteLog = readdirSync(join(gateProj, 'Logs', 'Verification')).filter((f) => /^r-p-gate_/.test(f))
770
+ const voteLog = listDirIf(join(gateProj, 'Logs', 'Verification')).filter((f) => /^r-p-gate_/.test(f))
735
771
  assert(voteLog.length >= 1, 'the votes themselves are still recorded in Logs/Verification (nothing is lost)')
736
772
  }
737
773
  // runtime switch: the mode is read when the prompt is CONSTRUCTED, so a fresh round reflects it
@@ -767,6 +803,15 @@ await callTool('vibe_math_set_params', { formalVerify: 'require' }, RF)
767
803
  }
768
804
  await restart(RF)
769
805
  assert(await drive(RF, () => unfiredVerifiers(RF, verifyRe('p-gate')).length >= 2, 'p-gate re-eligible after passing'), '★ once the formal record satisfies the gate, the object becomes eligible for verification again')
806
+ {
807
+ // V3-G2: `.length >= 2` is a COUNT - the semantic unit is that the RE-ELIGIBLE reviews are the
808
+ // SAME slots that fired in round one (a re-verification that re-armed different slots would
809
+ // still satisfy the count). Measured round-one slots: ["0","1"].
810
+ const un = unfiredVerifiers(RF, verifyRe('p-gate'))
811
+ const slots = slotsOf(un)
812
+ assert(slots.slice().sort().join(',') === gateSlots1.slice().sort().join(','), '* V3-G2 the re-eligible reviews are the SAME slots that fired in round one (before=' + JSON.stringify(gateSlots1) + ' after=' + JSON.stringify(slots) + ')')
813
+ assert(new Set(slots).size === slots.length, '* V3-G2 and they are still DISTINCT slots (the V3-G1 clause restated at this site)')
814
+ }
770
815
  {
771
816
  const batch = await runVerifyRound(RF, 'p-gate', [1, 1])
772
817
  assert(batch !== null, 'the re-verification round was asked')
@@ -1136,6 +1181,7 @@ await callTool('vibe_math_lean_archive', { kind: 'proof', target: 'p-fid', conte
1136
1181
  await callTool('vibe_math_start', {}, RK)
1137
1182
  const fidBatch = await runVerifyRound(RK, 'p-fid', [0.9, 0.95])
1138
1183
  assert(fidBatch !== null, 'fidelity: the review round was asked')
1184
+ if (fidBatch) assertDistinctSlots(fidBatch, 'fidelity round')
1139
1185
  if (fidBatch) {
1140
1186
  const vp = fidBatch.map((s) => s.prompt).join('\n')
1141
1187
  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)")
@@ -1577,10 +1623,43 @@ async function driveSiteV3(rw, opts, qid) {
1577
1623
  await callTool('vibe_math_abort', {}, rvv)
1578
1624
  }
1579
1625
 
1626
+ // ===============================================================
1627
+ // 5c. round-2 D4: ONE vote-count source — a mid-verification verifierCount change must not re-derive
1628
+ // the in-flight task's quorum from live params (the audit's "same count, different code" bug).
1629
+ // ===============================================================
1630
+ section('5c D4 vote count: quorum comes from the task snapshot, not from live params')
1631
+ {
1632
+ const RD4 = makeRoot()
1633
+ await callTool('vibe_math_new_project', { name: 'd4-votecount' }, RD4)
1634
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { verifierCount: 2 }), RD4)
1635
+ await callTool('vibe_math_add_proposition', { id: 'p-d4', 概述: 'D4 票数单一来源', 概率: 0.6, 分类: '数论' }, RD4)
1636
+ await callTool('vibe_math_start', {}, RD4)
1637
+ const reD4 = verifyRe('p-d4')
1638
+ assert(await drive(RD4, () => unfiredVerifiers(RD4, reD4).length >= 2, 'two verifiers for p-d4'), 'two verifiers were dispatched for p-d4 (the creation-time snapshot)')
1639
+ // Mid-verification: the office asks for MORE verifiers. Intent-wise that is a NEW requirement for
1640
+ // FUTURE objects; this task's participant set was frozen when it was created.
1641
+ await callTool('vibe_math_set_params', Object.assign({}, VPARAMS, { verifierCount: 4 }), RD4)
1642
+ await sleep(300)
1643
+ // Count PER EXACT rId: the proposition may legitimately have several candidate rIds
1644
+ // (r-p-d4, r-p-d4-pf0, …); only the in-flight task for one rId must stay pinned.
1645
+ const labels = spawnOf(RD4, 'verifier:').map((x) => x.label)
1646
+ const exact = {}
1647
+ for (const l of labels) { const m = /^verifier:(r-p-d4):(\d+)$/.exec(l); if (m) exact[m[1]] = (exact[m[1]] || 0) + 1 }
1648
+ const inFlight = exact['r-p-d4'] || 0
1649
+ assert(inFlight === 2, '★ [D4] raising verifierCount mid-verification does NOT re-staff the in-flight task (participant set = the snapshot;实测 ' + JSON.stringify({ inFlight: inFlight, labels: labels }) + ')')
1650
+ // Both dispatched votes satisfy the quorum ⇒ the object must conclude. A view that re-derived its
1651
+ // quorum from the live params would demand 4 votes and stall here forever.
1652
+ await runVerifyRound(RD4, 'p-d4', [1, 1])
1653
+ const cardOf = () => readIf(join(projRoot('d4-votecount'), 'Propos', '数论', 'p-d4.md'))
1654
+ const concluded = await drive(RD4, () => { const c = cardOf(); return /-\s*概率:\s*1(\D|$)/.test(c) || /状态:\s*已验证/.test(c) }, 'p-d4 concludes with the two votes it has', 15000)
1655
+ assert(concluded, '★★★ [D4] the two dispatched votes still satisfy the quorum and the object concludes (no view may re-derive its quorum from live params;card=' + JSON.stringify(cardOf().slice(0, 220)) + ')')
1656
+ await callTool('vibe_math_abort', {}, RD4)
1657
+ }
1658
+
1580
1659
  section('10 the captured prompt corpus is written for human review')
1581
1660
  {
1582
1661
  mkdirSync(CORPUS_DIR, { recursive: true })
1583
- writeFileSync(join(CORPUS_DIR, 'formal-verify-v3.json'), JSON.stringify({ entries: corpus }, null, 2), 'utf8')
1662
+ writeFileAtomic(join(CORPUS_DIR, 'formal-verify-v3.json'), JSON.stringify({ entries: corpus }, null, 2))
1584
1663
  const md = ['# V3 形式化验证交互语料(prompt corpus)', '',
1585
1664
  '> 由 `formal-verify-v3.test.mjs` 落盘:框架**真正发出**的每一条提示词原文。路径归一化:工作区 → `<WS>`,',
1586
1665
  '> VibeMath 根 → `<VIBEMATH>`(两者都按正/反斜杠两种写法替换,因此语料是确定性的、可 diff 的、不泄露本机路径)。',
@@ -1596,7 +1675,7 @@ section('10 the captured prompt corpus is written for human review')
1596
1675
  md.push('```')
1597
1676
  md.push('')
1598
1677
  }
1599
- writeFileSync(join(CORPUS_DIR, 'formal-verify-v3.md'), md.join('\n'), 'utf8')
1678
+ writeFileAtomic(join(CORPUS_DIR, 'formal-verify-v3.md'), md.join('\n'))
1600
1679
  assert(existsSync(join(CORPUS_DIR, 'formal-verify-v3.json')) && existsSync(join(CORPUS_DIR, 'formal-verify-v3.md')), 'the prompt corpus was written (JSON + Markdown)')
1601
1680
  assert(corpus.length >= 25, 'the corpus covers the whole run (' + corpus.length + ' prompts)')
1602
1681
  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')
@@ -0,0 +1,152 @@
1
+ // Registered mutant harness for the v4 formal-verify suite — the suite carries its own proof.
2
+ // Mirrors tests/v5-institute-fixes.mutants.mjs: each family is a SINGLE content-anchored mutation of the
3
+ // v4 preset (no line numbers), the suite runs through the V4_PLUGIN seam, child runs are bounded (a
4
+ // timeout is a HANG, never a pass), and skipped families are printed and counted.
5
+ // Exit criterion: every family reddens BY NAME; ALL MUTANTS RED AS REQUIRED + exit 0 only then.
6
+ // Run: node tests/formal-verify-v4.mutants.mjs
7
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
8
+ import { execFileSync } from 'node:child_process'
9
+ import { dirname, join } from 'node:path'
10
+ import { tmpdir } from 'node:os'
11
+ import { fileURLToPath } from 'node:url'
12
+ const HERE = dirname(fileURLToPath(import.meta.url))
13
+ const REPO = join(HERE, '..')
14
+ const PRESET = 'vibe-math-v4'
15
+ const MAIN = PRESET + '.js'
16
+ const SUITE = 'tests/formal-verify-v4.test.mjs'
17
+ const ENV = 'V4_PLUGIN'
18
+ // The formal-verify suite is long, so the default child budget is generous; MUTANT_CHILD_TIMEOUT_MS
19
+ // overrides it.
20
+ const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 300000)
21
+ const TIMES = []
22
+ const hangs = []
23
+ const skipped = []
24
+ function copyGraph(file, dest) {
25
+ const src = join(REPO, PRESET, file)
26
+ copyFileSync(src, join(dest, file))
27
+ for (const m of readFileSync(src, 'utf8').matchAll(/from\s+'(\.\/[A-Za-z0-9_.-]+\.js)'/g)) {
28
+ const dep = m[1].slice(2)
29
+ if (!readdirSync(dest).includes(dep)) copyGraph(dep, dest)
30
+ }
31
+ }
32
+ function runFamily(f) {
33
+ const dest = join(tmpdir(), 'v4fv-mut-' + Math.random().toString(36).slice(2, 10))
34
+ mkdirSync(dest, { recursive: true })
35
+ copyGraph(MAIN, dest)
36
+ const target = join(dest, f.editFile || MAIN)
37
+ const before = readFileSync(target, 'utf8')
38
+ const n = before.split(f.from).length - 1
39
+ const okCount = f.lastOnly || f.all ? n >= 1 : n === 1
40
+ if (!okCount) {
41
+ skipped.push(f.name + ' (anchor x' + n + ')')
42
+ console.error(' SKIP - ' + f.name + ' (anchor x' + n + ')')
43
+ rmSync(dest, { recursive: true, force: true })
44
+ return false
45
+ }
46
+ const mutated = f.lastOnly
47
+ ? (() => { const i = before.lastIndexOf(f.from); return before.slice(0, i) + f.to + before.slice(i + f.from.length) })()
48
+ : before.split(f.from).join(f.to)
49
+ writeFileSync(target, mutated, 'utf8')
50
+ try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
51
+ catch (e) {
52
+ writeFileSync(target, before, 'utf8')
53
+ skipped.push(f.name + ' (mutation did not compile)')
54
+ console.error(' SKIP - ' + f.name + ' (mutation did not compile; restored)')
55
+ rmSync(dest, { recursive: true, force: true })
56
+ return false
57
+ }
58
+ const t0 = Date.now()
59
+ let out = ''
60
+ let code = 0
61
+ let hang = false
62
+ try {
63
+ out = execFileSync(process.execPath, [SUITE], {
64
+ cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL',
65
+ env: Object.assign({}, process.env, { [ENV]: join(dest, MAIN) }),
66
+ })
67
+ } catch (e) {
68
+ if (e && (e.killed || e.signal === 'SIGKILL')) hang = true
69
+ code = (e && e.status) || 1
70
+ out = String((e && e.stdout) || '') + String((e && e.stderr) || '')
71
+ }
72
+ const ms = Date.now() - t0
73
+ TIMES.push([f.name, ms])
74
+ if (hang) hangs.push(f.name + '(' + Math.round(ms / 1000) + 's)')
75
+ const named = out.split(/\r?\n/).filter((l) => /^\s*(FAIL - | {2}- )/.test(l) && f.expect.test(l)).map((l) => l.trim())
76
+ const ok = !hang && code !== 0 && named.length > 0
77
+ console.log((hang ? ' HANG - ' : (ok ? ' ok - ' : ' FAIL - ')) + f.name + ' [' + ms + 'ms]' + (named.length ? ' :: ' + named[0].slice(0, 150) : ' :: no named red (exit=' + code + ')'))
78
+ rmSync(dest, { recursive: true, force: true })
79
+ return ok
80
+ }
81
+ const FAMILIES = [
82
+ {
83
+ // R10-F1: reading `allow` (a permission LIST) as a fence root — this is the branch the no-op path
84
+ // must keep dead, and the red proves case 1b actually reaches the helper with that shape.
85
+ name: 'F-5/1b: treat `allow` as a fence root',
86
+ from: ' else if(p.config && p.config.workspaceRoot !== undefined) pick = p.config.workspaceRoot',
87
+ to: ' else if(p.config && p.config.workspaceRoot !== undefined) pick = p.config.workspaceRoot\n else if(p.allow !== undefined) pick = p.allow',
88
+ expect: /F-5\/1b .*produces NO warning/,
89
+ },
90
+ {
91
+ // the whole comparison removed: a real drift goes unnamed
92
+ name: 'F-5/ii: the drift comparison is never called',
93
+ from: 'fenceRootDriftNote(p);',
94
+ to: 'void 0;',
95
+ expect: /F-5\/ii the drift is NAMED exactly once/,
96
+ },
97
+ {
98
+ // containment disabled: agreeing/ancestor roots would warn (the per-session false alarm)
99
+ name: 'F-5/iii+iv: the containment predicate is disabled (always warn)',
100
+ from: ' if(contained) return',
101
+ to: ' if(false) return',
102
+ expect: /F-5\/iv an AGREEING resolved root produces NO warning/,
103
+ },
104
+ {
105
+ // one-shot dropped: the warning repeats on every policy resolution
106
+ name: 'F-5/ii: the one-shot is dropped',
107
+ from: ' if(!warnedFenceDrift){',
108
+ to: ' if(true){',
109
+ expect: /F-5\/ii the drift is NAMED exactly once .*matched=2/,
110
+ },
111
+ {
112
+ // F-6 site 4: the swallowed compaction failure becomes silent again
113
+ name: 'N15/4: the compaction-failure warning is silenced',
114
+ from: '} catch(e){ if(!warnedCompaction){ warnedCompaction=true;',
115
+ to: '} catch(e){ if(false){ warnedCompaction=true;',
116
+ expect: /N15\/4 realCompact: a failed compaction is NAMED exactly once/,
117
+ },
118
+ {
119
+ // F-5/1c: the `.` clause of the guard is dropped, so an UNKNOWN session cwd (workspaceRoot() ===
120
+ // ".") is reported as drift even though it is constructively unknown, not drifted.
121
+ name: 'F-5/1c: an unknown session cwd is warned about',
122
+ from: " if(!session || session === '.') return",
123
+ to: ' if(!session) return',
124
+ expect: /F-5\/1c an UNKNOWN session cwd stays SILENT/,
125
+ },
126
+ {
127
+ // F6-v4: the `.current` marker write fails silently again (its guard is removed).
128
+ name: 'N18: a failed current-project marker write is silent again',
129
+ from: 'if(ok===false && !warnedCurrentProject){',
130
+ to: 'if(false){',
131
+ expect: /N18 the failed .*write is NAMED exactly once/,
132
+ },
133
+ {
134
+ // ③: the in-function naming is removed again, so a failed index write is silent for all six
135
+ // callers (the measured pre-fix behaviour).
136
+ name: 'N20-3b: a failed index write is silent again',
137
+ from: 'if((_idxLibOk === false || _idxProvedOk === false) && !warnedIndexRebuild){',
138
+ to: 'if(false){',
139
+ expect: /N20-3b a failed index write is NAMED exactly once/
140
+ },
141
+ ]
142
+ let red = 0
143
+ for (const f of FAMILIES) { const ok = runFamily(f); if (ok) red++ }
144
+ console.log('')
145
+ console.log('mutant families reddening the v4 formal-verify suite by name: ' + red + '/' + FAMILIES.length)
146
+ console.log('timings: ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
147
+ const totalMs = TIMES.reduce((a, t) => a + t[1], 0)
148
+ console.log('TOTAL WALL TIME (all families + setup): ' + totalMs + 'ms (' + Math.round(totalMs / 1000) + 's)')
149
+ console.log('hangs=[' + hangs.join(' | ') + ']')
150
+ console.log('skipped=[' + skipped.join(' | ') + ']')
151
+ if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
152
+ console.log('ALL MUTANTS RED AS REQUIRED')