dsh-vibe-math 2.7.2 → 2.8.0

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 (153) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -681
  3. package/README.md +683 -682
  4. package/cordis.patch.yml +112 -16
  5. package/docs/AUDIT-CHECKLIST.md +755 -518
  6. package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
  7. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  8. package/docs/audit-methodology.md +93 -0
  9. package/docs/final-paper.md +172 -172
  10. package/docs/formal-verification.md +613 -588
  11. package/docs/generate_framework_diagram_v2.py +114 -114
  12. package/docs/generate_framework_diagram_v3.py +127 -127
  13. package/docs/math-computation.md +195 -187
  14. package/docs/parameter-schema.md +37 -0
  15. package/docs/release-notes/README.en.md +2 -0
  16. package/docs/release-notes/README.md +2 -0
  17. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  18. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  19. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  20. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  21. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  22. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  23. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  24. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  25. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  26. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  27. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  28. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  29. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  30. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  31. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  32. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  33. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  34. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  35. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  36. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  37. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  38. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  39. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  40. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  41. package/docs/release-notes/TEMPLATE.md +6 -0
  42. package/docs/release-playbook.md +103 -0
  43. package/docs/slv-playbook.md +117 -0
  44. package/docs/status-report-fields.md +167 -0
  45. package/docs/test-timing.md +158 -154
  46. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  47. package/installer.js +793 -783
  48. package/package.json +260 -203
  49. package/prompt-corpus-persona/persona-corpus.json +9 -9
  50. package/prompt-corpus-persona/persona-corpus.md +225 -33
  51. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  52. package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
  53. package/prompt-corpus-v3/formal-verify-v3.json +112 -112
  54. package/prompt-corpus-v3/formal-verify-v3.md +227 -143
  55. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  56. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  57. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  58. package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
  59. package/scripts/clean-temp.mjs +234 -0
  60. package/scripts/update-doc-counts.mjs +71 -0
  61. package/tests/audit-artifact-docs.mjs +72 -0
  62. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  63. package/tests/audit-engine-faces.mjs +298 -0
  64. package/tests/audit-formal-sensitivity.mjs +345 -342
  65. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  66. package/tests/audit-installer-compat.test.mjs +256 -208
  67. package/tests/audit-installer-policy.test.mjs +41 -8
  68. package/tests/audit-math-computation-contract.mjs +64 -8
  69. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  70. package/tests/audit-math-computation-parity.mjs +164 -3
  71. package/tests/audit-math-computation-sensitivity.mjs +5 -3
  72. package/tests/audit-package-membership.mjs +57 -0
  73. package/tests/audit-package-membership.mutants.mjs +28 -0
  74. package/tests/audit-participant-set-parity.mjs +160 -0
  75. package/tests/audit-path-discipline.mjs +83 -0
  76. package/tests/audit-path-discipline.mutants.mjs +51 -0
  77. package/tests/audit-persona-sensitivity.mjs +323 -309
  78. package/tests/audit-persona-surface.mutants.mjs +69 -0
  79. package/tests/audit-persona-surface.test.mjs +431 -390
  80. package/tests/audit-prompt-duplication.mjs +143 -0
  81. package/tests/audit-prompt-duplication.mutants.mjs +51 -0
  82. package/tests/audit-prompt-invariants.mjs +589 -508
  83. package/tests/audit-readme-bilingual.test.mjs +152 -0
  84. package/tests/audit-readme-counts.mjs +95 -0
  85. package/tests/audit-readme-counts.mutants.mjs +52 -0
  86. package/tests/audit-registration.mjs +78 -0
  87. package/tests/audit-spec-traceability.mjs +193 -193
  88. package/tests/audit-status-report-fields.mjs +172 -0
  89. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  90. package/tests/audit-v3-registration-parity.mjs +156 -0
  91. package/tests/audit-v5-integrity.mjs +795 -508
  92. package/tests/audit-v5-lean-abstention.mjs +392 -0
  93. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  94. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  95. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  96. package/tests/audit-v5-sensitivity.mjs +433 -427
  97. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  98. package/tests/e2e-v5-round2.test.mjs +2453 -1981
  99. package/tests/formal-verify-v2.test.mjs +1778 -1724
  100. package/tests/formal-verify-v3.mutants.mjs +117 -0
  101. package/tests/formal-verify-v3.test.mjs +1724 -1671
  102. package/tests/formal-verify-v4.mutants.mjs +152 -0
  103. package/tests/formal-verify-v4.test.mjs +2114 -1574
  104. package/tests/formal-verify-v5.test.mjs +790 -724
  105. package/tests/helpers/doc-counts-format.mjs +41 -0
  106. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  107. package/tests/math-computation-a1.mutants.mjs +72 -0
  108. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  109. package/tests/math-computation-discovery.mutants.mjs +76 -0
  110. package/tests/math-computation-shared.test.mjs +392 -6
  111. package/tests/math-computation-v2.test.mjs +779 -0
  112. package/tests/math-computation-v3.test.mjs +771 -0
  113. package/tests/math-computation-v4.mutants.mjs +133 -0
  114. package/tests/math-computation-v4.test.mjs +739 -0
  115. package/tests/prompt-v5-integrity.test.mjs +1291 -1280
  116. package/tests/run-tests.mjs +393 -202
  117. package/tests/run-tests.mutants.mjs +70 -0
  118. package/tests/selfdrive-v5.mjs +560 -508
  119. package/tests/selfdrive-v5.mutants.mjs +132 -0
  120. package/tests/temp-hygiene.mutants.mjs +80 -0
  121. package/tests/v2-fix-probes.mutants.mjs +104 -0
  122. package/tests/v2-fix-probes.test.mjs +1040 -0
  123. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  124. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  125. package/tests/v3-fix-probes.mutants.mjs +99 -0
  126. package/tests/v3-fix-probes.test.mjs +892 -0
  127. package/tests/v4-final-paper.mutants.mjs +108 -0
  128. package/tests/v5-institute-fixes.mutants.mjs +218 -0
  129. package/vibe-math-v2/agent.cordis.yml +378 -322
  130. package/vibe-math-v2/math-computation.js +144 -53
  131. package/vibe-math-v2/math-engines.js +12 -12
  132. package/vibe-math-v2/preset.yml +2 -2
  133. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  134. package/vibe-math-v2/vibe-math-v2.js +5097 -4755
  135. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
  136. package/vibe-math-v3/agent.cordis.yml +422 -356
  137. package/vibe-math-v3/math-computation.js +144 -53
  138. package/vibe-math-v3/math-engines.js +12 -12
  139. package/vibe-math-v3/preset.yml +2 -2
  140. package/vibe-math-v3/vibe-math-v3.js +6235 -5873
  141. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  142. package/vibe-math-v4/agent.cordis.yml +438 -402
  143. package/vibe-math-v4/math-computation.js +144 -53
  144. package/vibe-math-v4/math-engines.js +12 -12
  145. package/vibe-math-v4/preset.yml +2 -2
  146. package/vibe-math-v4/vibe-math-v4.js +4490 -4269
  147. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
  148. package/vibe-math-v5/agent.cordis.yml +46 -12
  149. package/vibe-math-v5/math-computation.js +144 -53
  150. package/vibe-math-v5/math-engines.js +12 -12
  151. package/vibe-math-v5/vibe-math-v5.js +1266 -229
  152. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
  153. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -1,193 +1,193 @@
1
- #!/usr/bin/env node
2
- /**
3
- * SPEC/README ↔ CODE TRACEABILITY (all four presets).
4
- *
5
- * This class of drift has produced real bugs here twice: a `/v4` usage string that advertised a
6
- * `message` subcommand no branch implemented, and personas that never named registered tools. The
7
- * rule is asymmetric on purpose:
8
- *
9
- * · a tool the SPEC/README advertises that the code does NOT register → FAIL (a documented
10
- * capability that does not exist is a lie the agent will act on);
11
- * · a tool the code registers that the spec never names → NOTE (documenting
12
- * every member-only tool in the spec is not always desirable, but the gap must be visible);
13
- * · the four Lean parameters must be documented in the spec AND the README AND accepted by the
14
- * code — a parameter nobody documents cannot be discovered, and one nobody accepts cannot be set.
15
- *
16
- * Run: node tests/audit-spec-traceability.mjs [--json]
17
- */
18
- import { readFileSync, existsSync } from 'node:fs'
19
- import { fileURLToPath } from 'node:url'
20
- import { join } from 'node:path'
21
-
22
- const HERE = fileURLToPath(new URL('../', import.meta.url))
23
- const read = (rel) => (existsSync(join(HERE, rel)) ? readFileSync(join(HERE, rel), 'utf8') : null)
24
-
25
- const PRESETS = [
26
- { tag: 'v2', js: 'vibe-math-v2/vibe-math-v2.js', spec: 'vibe-math-v2/实现方案.md', prefix: 'vibe_math_' },
27
- { tag: 'v3', js: 'vibe-math-v3/vibe-math-v3.js', spec: 'vibe-math-v3/实现方案.md', prefix: 'vibe_math_' },
28
- { tag: 'v4', js: 'vibe-math-v4/vibe-math-v4.js', spec: 'vibe-math-v4/实现方案.md', prefix: 'vibe_v4_' },
29
- { tag: 'v5', js: 'vibe-math-v5/vibe-math-v5.js', spec: 'vibe-math-v5/实现方案.md', prefix: 'vibe_v5_' },
30
- ]
31
- // Tokens that look like tool names but are FILE names / namespace prose, not tools.
32
- const NOT_A_TOOL = new Set([
33
- 'vibe_math_setting.json', 'vibe_math_installed.json', 'vibe_math_lean', 'vibe_math_lean_',
34
- 'vibe_v4_setting.json', 'vibe_v5_state', 'vibe_v5_state.json', 'vibe_v5_lean', 'vibe_v5_lean_',
35
- 'vibe_v4_lean', 'vibe_v4_lean_', 'vibe_math_state', 'vibe_math_state.json',
36
- ])
37
- const LEAN_PARAMS = ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']
38
-
39
- const README = read('README.md') || ''
40
- const findings = []
41
- const notes = []
42
- let passed = 0
43
- const ok = (cond, label, detail) => {
44
- if (cond) { passed++; return true }
45
- findings.push(label + (detail ? ' — ' + detail : ''))
46
- return false
47
- }
48
- // The README documents ALL FOUR presets at once, and v2/v3 share the `vibe_math_` prefix, so a
49
- // repo-wide token is legitimate if ANY preset registers it. The per-preset SPEC is checked strictly
50
- // against that preset's own registry.
51
- const ALL_TOOLS = new Set()
52
- for (const P of PRESETS) {
53
- const src = read(P.js)
54
- if (!src) continue
55
- for (const m of src.matchAll(/registerTool\(\s*'([A-Za-z0-9_]+)'/g)) ALL_TOOLS.add(m[1])
56
- }
57
- /** Collect `vibe_*` tokens that are used as TOOL names (not file names such as vibe_math_setting.json). */
58
- function toolTokens(text) {
59
- const out = new Set()
60
- for (const m of text.matchAll(/\b(vibe_(?:math|v4|v5)_[A-Za-z0-9_]+)/g)) {
61
- const after = text[m.index + m[0].length] || ''
62
- if (after === '.') continue // a file name, e.g. vibe_math_setting.json
63
- // A doc may name a tool precisely to say it does NOT exist ("v4 没有 vibe_v4_propose_verify 这个工具",
64
- // "写了不存在的工具 …"). That is documentation of the fix, not a phantom capability — skip it, or
65
- // the guard would forbid the very sentence that records the correction.
66
- const lineStart = text.lastIndexOf('\n', m.index) + 1
67
- const lineEnd = text.indexOf('\n', m.index)
68
- const line = text.slice(lineStart, lineEnd === -1 ? text.length : lineEnd)
69
- if (/没有|不存在|并非|不是工具|未注册|从未注册|无此|does not exist|no such|never registered|not a tool/i.test(line)) continue
70
- out.add(m[1])
71
- }
72
- return out
73
- }
74
-
75
- for (const P of PRESETS) {
76
- const js = read(P.js)
77
- const spec = read(P.spec)
78
- if (!js || !spec) { findings.push(P.tag + ': missing plugin or spec file'); continue }
79
-
80
- const codeTools = new Set([...js.matchAll(/registerTool\(\s*'([A-Za-z0-9_]+)'/g)].map((m) => m[1]))
81
- ok(codeTools.size > 0, P.tag + ': the plugin registers tools at all')
82
-
83
- // direction 1 (FAIL): anything the spec names as a tool must exist in THIS preset; anything the
84
- // README names must exist in at least one preset (the README covers all four).
85
- for (const [label, text, own] of [['实现方案', spec, true], ['README', README, false]]) {
86
- for (const t of toolTokens(text)) {
87
- if (NOT_A_TOOL.has(t)) continue
88
- if (t.endsWith('_')) continue // a `vibe_x_*` wildcard placeholder
89
- if (!t.startsWith(P.prefix)) continue
90
- const known = own ? codeTools.has(t) : ALL_TOOLS.has(t)
91
- if (!known) {
92
- findings.push(P.tag + ' [' + label + ']: documents tool ' + t
93
- + (own ? ' but the plugin never registers it' : ' but NO preset registers it'))
94
- }
95
- }
96
- }
97
-
98
- // direction 2 (NOTE): registered but not documented anywhere
99
- const md = spec + '\n' + README
100
- const undocumented = [...codeTools].filter((t) => !md.includes(t)).sort()
101
- if (undocumented.length) notes.push(P.tag + ': registered but not named in spec/README: ' + undocumented.join(', '))
102
-
103
- // the four Lean parameters: documented in spec + README, and accepted by the code
104
- for (const prm of LEAN_PARAMS) {
105
- ok(spec.includes(prm), P.tag + ': 实现方案 documents ' + prm)
106
- ok(README.includes(prm), P.tag + ': README documents ' + prm)
107
- ok(js.includes(prm), P.tag + ': the plugin accepts ' + prm)
108
- }
109
-
110
- // the Lean tools must appear in the spec's tool table AND in the code
111
- for (const t of [P.prefix + 'lean_run', P.prefix + 'lean_archive', P.prefix + 'lean_lib']) {
112
- ok(codeTools.has(t), P.tag + ': registers ' + t)
113
- ok(spec.includes(t), P.tag + ': 实现方案 names ' + t)
114
- }
115
-
116
- // the contract §7 requires an ACTIVE stop on timeout: `handle.terminate()`. Relying on the
117
- // host's `graceMs` alone lets a runaway Lean process linger while the framework reports
118
- // LEAN_TIMEOUT — three of four presets shipped that way, so this is now guarded statically.
119
- ok(/terminate\s*\(/.test(js), P.tag + ': the Lean run path actively terminates on timeout (contract §7)')
120
-
121
- // the contract §8 puts the `require` gate at the ONE choke point that writes a Verified card.
122
- // A card writer reachable WITHOUT passing a gated function would be a gate bypass, i.e. the
123
- // framework could conclude a verdict that `require` is supposed to withhold.
124
- {
125
- const cfg = {
126
- v2: { writers: ['writeVerifiedCardIfNeeded', 'writeVerifiedProblemCardIfNeeded'], gate: /formalRequired\s*\(|formalGateOk\s*\(|formalVerdictDeferred\s*\(/ },
127
- v3: { writers: ['writeVerifiedCardIfChanged', 'writeVerifiedPropositionCardIfNeeded', 'writeVerifiedProblemCardIfNeeded'], gate: /formalBlocksConclusion\s*\(/ },
128
- v4: { writers: ['writeVerifiedCard'], gate: /formalGateOk\s*\(/ },
129
- v5: { writers: ['writeVerifiedCard'], gate: /formalGateOk\s*\(/ },
130
- }[P.tag]
131
- const lines = js.split(/\r?\n/)
132
- const fns = []
133
- for (let i = 0; i < lines.length; i++) {
134
- const m = /^(\s*)(?:async\s+)?function\s+([A-Za-z0-9_$]+)\s*\(/.exec(lines[i])
135
- if (m) fns.push({ name: m[2], indent: m[1].length, start: i })
136
- }
137
- for (let k = 0; k < fns.length; k++) {
138
- const f = fns[k]
139
- let end = lines.length
140
- for (const g of fns) if (g.start > f.start && g.indent <= f.indent) { end = g.start; break }
141
- f.body = lines.slice(f.start, end).join('\n')
142
- }
143
- const byName = (n) => fns.find((f) => f.name === n)
144
- for (const w of cfg.writers) {
145
- const f = byName(w)
146
- if (!f) { ok(false, P.tag + ' §8: the documented card writer ' + w + ' does not exist'); continue }
147
- const selfGated = cfg.gate.test(f.body)
148
- const callers = fns.filter((g) => g !== f && new RegExp('(^|[^A-Za-z0-9_$])' + w + '\\s*\\(').test(g.body))
149
- const allCallersGatedOrRecursive = callers.length > 0 && callers.every((c) => cfg.gate.test(c.body) || fns.some((d) => cfg.gate.test(d.body) && new RegExp('(^|[^A-Za-z0-9_$])' + c.name + '\\s*\\(').test(d.body)))
150
- ok(selfGated || allCallersGatedOrRecursive,
151
- P.tag + ' §8: ' + w + ' is reachable only through a gated function (no gate bypass)',
152
- 'callers: ' + callers.map((c) => c.name).join(', '))
153
- }
154
- }
155
-
156
- notes.push(P.tag + ': ' + codeTools.size + ' tools registered; spec ' + spec.length + 'B')
157
- }
158
-
159
- // cross-preset: the shared contract must name the four parameters too
160
- const contract = read('docs/formal-verification.md') || ''
161
- for (const prm of LEAN_PARAMS) ok(contract.includes(prm), 'contract documents ' + prm)
162
-
163
- // The §4 transition table is the normative statement every preset implements. Two rows were WRONG
164
- // here (the code was right, the contract was not), and a wrong contract is how the next edit gets
165
- // written:
166
- // · `used` must NOT withdraw an established proof (only a fidelity `defect` does). v2 actually
167
- // shipped the downgrade, so the contract literal and reality disagreed in opposite directions.
168
- // · a RED re-archive of kind='proof' overwrites the work file, so the old archived proof no longer
169
- // corresponds to any code — it must be retracted, not left at the "everyone looks here" path.
170
- {
171
- const usedRow = (contract.split(/\r?\n/).find((l) => /decision:'used'/.test(l)) || '')
172
- ok(/不得/.test(usedRow) && /passed/.test(usedRow) && /blocked/.test(usedRow),
173
- "contract §4: the `used` row says an existing passed/blocked is PRESERVED (only `defect` retracts)",
174
- 'row: ' + usedRow.slice(0, 120))
175
- const runRows = contract.split(/\r?\n/).filter((l) => /^\|\s*`lean_run`/.test(l))
176
- ok(runRows.length >= 2 && runRows.every((l) => /不降级|保持原状/.test(l)),
177
- 'contract §4: both `lean_run` rows say they do not downgrade an existing passed/blocked',
178
- 'rows: ' + runRows.map((l) => l.slice(0, 60)).join(' || '))
179
- ok(/该文件最近一次运行\*\*失败\*\*/.test(contract) && /撤回/.test(contract),
180
- 'contract §4: a FAILED proof re-archive is documented as retracting the previous archived proof')
181
- }
182
-
183
- const out = { passed, failed: findings.length, findings, notes }
184
- if (process.argv.includes('--json')) console.log(JSON.stringify(out, null, 2))
185
- else {
186
- console.log('-- spec/README ↔ code traceability --')
187
- for (const n of notes) console.log(' note ' + n)
188
- console.log('')
189
- for (const f of findings) console.error(' FAIL ' + f)
190
- console.log('')
191
- console.log('TRACEABILITY: ' + passed + ' passed, ' + findings.length + ' failed')
192
- }
193
- process.exit(findings.length === 0 ? 0 : 1)
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SPEC/README ↔ CODE TRACEABILITY (all four presets).
4
+ *
5
+ * This class of drift has produced real bugs here twice: a `/v4` usage string that advertised a
6
+ * `message` subcommand no branch implemented, and personas that never named registered tools. The
7
+ * rule is asymmetric on purpose:
8
+ *
9
+ * · a tool the SPEC/README advertises that the code does NOT register → FAIL (a documented
10
+ * capability that does not exist is a lie the agent will act on);
11
+ * · a tool the code registers that the spec never names → NOTE (documenting
12
+ * every member-only tool in the spec is not always desirable, but the gap must be visible);
13
+ * · the four Lean parameters must be documented in the spec AND the README AND accepted by the
14
+ * code — a parameter nobody documents cannot be discovered, and one nobody accepts cannot be set.
15
+ *
16
+ * Run: node tests/audit-spec-traceability.mjs [--json]
17
+ */
18
+ import { readFileSync, existsSync } from 'node:fs'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { join } from 'node:path'
21
+
22
+ const HERE = fileURLToPath(new URL('../', import.meta.url))
23
+ const read = (rel) => (existsSync(join(HERE, rel)) ? readFileSync(join(HERE, rel), 'utf8') : null)
24
+
25
+ const PRESETS = [
26
+ { tag: 'v2', js: 'vibe-math-v2/vibe-math-v2.js', spec: 'vibe-math-v2/实现方案.md', prefix: 'vibe_math_' },
27
+ { tag: 'v3', js: 'vibe-math-v3/vibe-math-v3.js', spec: 'vibe-math-v3/实现方案.md', prefix: 'vibe_math_' },
28
+ { tag: 'v4', js: 'vibe-math-v4/vibe-math-v4.js', spec: 'vibe-math-v4/实现方案.md', prefix: 'vibe_v4_' },
29
+ { tag: 'v5', js: 'vibe-math-v5/vibe-math-v5.js', spec: 'vibe-math-v5/实现方案.md', prefix: 'vibe_v5_' },
30
+ ]
31
+ // Tokens that look like tool names but are FILE names / namespace prose, not tools.
32
+ const NOT_A_TOOL = new Set([
33
+ 'vibe_math_setting.json', 'vibe_math_installed.json', 'vibe_math_lean', 'vibe_math_lean_',
34
+ 'vibe_v4_setting.json', 'vibe_v5_state', 'vibe_v5_state.json', 'vibe_v5_lean', 'vibe_v5_lean_',
35
+ 'vibe_v4_lean', 'vibe_v4_lean_', 'vibe_math_state', 'vibe_math_state.json',
36
+ ])
37
+ const LEAN_PARAMS = ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs']
38
+
39
+ const README = read('README.md') || ''
40
+ const findings = []
41
+ const notes = []
42
+ let passed = 0
43
+ const ok = (cond, label, detail) => {
44
+ if (cond) { passed++; return true }
45
+ findings.push(label + (detail ? ' — ' + detail : ''))
46
+ return false
47
+ }
48
+ // The README documents ALL FOUR presets at once, and v2/v3 share the `vibe_math_` prefix, so a
49
+ // repo-wide token is legitimate if ANY preset registers it. The per-preset SPEC is checked strictly
50
+ // against that preset's own registry.
51
+ const ALL_TOOLS = new Set()
52
+ for (const P of PRESETS) {
53
+ const src = read(P.js)
54
+ if (!src) continue
55
+ for (const m of src.matchAll(/registerTool\(\s*'([A-Za-z0-9_]+)'/g)) ALL_TOOLS.add(m[1])
56
+ }
57
+ /** Collect `vibe_*` tokens that are used as TOOL names (not file names such as vibe_math_setting.json). */
58
+ function toolTokens(text) {
59
+ const out = new Set()
60
+ for (const m of text.matchAll(/\b(vibe_(?:math|v4|v5)_[A-Za-z0-9_]+)/g)) {
61
+ const after = text[m.index + m[0].length] || ''
62
+ if (after === '.') continue // a file name, e.g. vibe_math_setting.json
63
+ // A doc may name a tool precisely to say it does NOT exist ("v4 没有 vibe_v4_propose_verify 这个工具",
64
+ // "写了不存在的工具 …"). That is documentation of the fix, not a phantom capability — skip it, or
65
+ // the guard would forbid the very sentence that records the correction.
66
+ const lineStart = text.lastIndexOf('\n', m.index) + 1
67
+ const lineEnd = text.indexOf('\n', m.index)
68
+ const line = text.slice(lineStart, lineEnd === -1 ? text.length : lineEnd)
69
+ if (/没有|不存在|并非|不是工具|未注册|从未注册|无此|does not exist|no such|never registered|not a tool/i.test(line)) continue
70
+ out.add(m[1])
71
+ }
72
+ return out
73
+ }
74
+
75
+ for (const P of PRESETS) {
76
+ const js = read(P.js)
77
+ const spec = read(P.spec)
78
+ if (!js || !spec) { findings.push(P.tag + ': missing plugin or spec file'); continue }
79
+
80
+ const codeTools = new Set([...js.matchAll(/registerTool\(\s*'([A-Za-z0-9_]+)'/g)].map((m) => m[1]))
81
+ ok(codeTools.size > 0, P.tag + ': the plugin registers tools at all')
82
+
83
+ // direction 1 (FAIL): anything the spec names as a tool must exist in THIS preset; anything the
84
+ // README names must exist in at least one preset (the README covers all four).
85
+ for (const [label, text, own] of [['实现方案', spec, true], ['README', README, false]]) {
86
+ for (const t of toolTokens(text)) {
87
+ if (NOT_A_TOOL.has(t)) continue
88
+ if (t.endsWith('_')) continue // a `vibe_x_*` wildcard placeholder
89
+ if (!t.startsWith(P.prefix)) continue
90
+ const known = own ? codeTools.has(t) : ALL_TOOLS.has(t)
91
+ if (!known) {
92
+ findings.push(P.tag + ' [' + label + ']: documents tool ' + t
93
+ + (own ? ' but the plugin never registers it' : ' but NO preset registers it'))
94
+ }
95
+ }
96
+ }
97
+
98
+ // direction 2 (NOTE): registered but not documented anywhere
99
+ const md = spec + '\n' + README
100
+ const undocumented = [...codeTools].filter((t) => !md.includes(t)).sort()
101
+ if (undocumented.length) notes.push(P.tag + ': registered but not named in spec/README: ' + undocumented.join(', '))
102
+
103
+ // the four Lean parameters: documented in spec + README, and accepted by the code
104
+ for (const prm of LEAN_PARAMS) {
105
+ ok(spec.includes(prm), P.tag + ': 实现方案 documents ' + prm)
106
+ ok(README.includes(prm), P.tag + ': README documents ' + prm)
107
+ ok(js.includes(prm), P.tag + ': the plugin accepts ' + prm)
108
+ }
109
+
110
+ // the Lean tools must appear in the spec's tool table AND in the code
111
+ for (const t of [P.prefix + 'lean_run', P.prefix + 'lean_archive', P.prefix + 'lean_lib']) {
112
+ ok(codeTools.has(t), P.tag + ': registers ' + t)
113
+ ok(spec.includes(t), P.tag + ': 实现方案 names ' + t)
114
+ }
115
+
116
+ // the contract §7 requires an ACTIVE stop on timeout: `handle.terminate()`. Relying on the
117
+ // host's `graceMs` alone lets a runaway Lean process linger while the framework reports
118
+ // LEAN_TIMEOUT — three of four presets shipped that way, so this is now guarded statically.
119
+ ok(/terminate\s*\(/.test(js), P.tag + ': the Lean run path actively terminates on timeout (contract §7)')
120
+
121
+ // the contract §8 puts the `require` gate at the ONE choke point that writes a Verified card.
122
+ // A card writer reachable WITHOUT passing a gated function would be a gate bypass, i.e. the
123
+ // framework could conclude a verdict that `require` is supposed to withhold.
124
+ {
125
+ const cfg = {
126
+ v2: { writers: ['writeVerifiedCardIfNeeded', 'writeVerifiedProblemCardIfNeeded'], gate: /formalRequired\s*\(|formalGateOk\s*\(|formalVerdictDeferred\s*\(/ },
127
+ v3: { writers: ['writeVerifiedCardIfChanged', 'writeVerifiedPropositionCardIfNeeded', 'writeVerifiedProblemCardIfNeeded'], gate: /formalBlocksConclusion\s*\(/ },
128
+ v4: { writers: ['writeVerifiedCard'], gate: /formalGateOk\s*\(/ },
129
+ v5: { writers: ['writeVerifiedCard'], gate: /formalGateOk\s*\(/ },
130
+ }[P.tag]
131
+ const lines = js.split(/\r?\n/)
132
+ const fns = []
133
+ for (let i = 0; i < lines.length; i++) {
134
+ const m = /^(\s*)(?:async\s+)?function\s+([A-Za-z0-9_$]+)\s*\(/.exec(lines[i])
135
+ if (m) fns.push({ name: m[2], indent: m[1].length, start: i })
136
+ }
137
+ for (let k = 0; k < fns.length; k++) {
138
+ const f = fns[k]
139
+ let end = lines.length
140
+ for (const g of fns) if (g.start > f.start && g.indent <= f.indent) { end = g.start; break }
141
+ f.body = lines.slice(f.start, end).join('\n')
142
+ }
143
+ const byName = (n) => fns.find((f) => f.name === n)
144
+ for (const w of cfg.writers) {
145
+ const f = byName(w)
146
+ if (!f) { ok(false, P.tag + ' §8: the documented card writer ' + w + ' does not exist'); continue }
147
+ const selfGated = cfg.gate.test(f.body)
148
+ const callers = fns.filter((g) => g !== f && new RegExp('(^|[^A-Za-z0-9_$])' + w + '\\s*\\(').test(g.body))
149
+ const allCallersGatedOrRecursive = callers.length > 0 && callers.every((c) => cfg.gate.test(c.body) || fns.some((d) => cfg.gate.test(d.body) && new RegExp('(^|[^A-Za-z0-9_$])' + c.name + '\\s*\\(').test(d.body)))
150
+ ok(selfGated || allCallersGatedOrRecursive,
151
+ P.tag + ' §8: ' + w + ' is reachable only through a gated function (no gate bypass)',
152
+ 'callers: ' + callers.map((c) => c.name).join(', '))
153
+ }
154
+ }
155
+
156
+ notes.push(P.tag + ': ' + codeTools.size + ' tools registered; spec ' + spec.length + 'B')
157
+ }
158
+
159
+ // cross-preset: the shared contract must name the four parameters too
160
+ const contract = read('docs/formal-verification.md') || ''
161
+ for (const prm of LEAN_PARAMS) ok(contract.includes(prm), 'contract documents ' + prm)
162
+
163
+ // The §4 transition table is the normative statement every preset implements. Two rows were WRONG
164
+ // here (the code was right, the contract was not), and a wrong contract is how the next edit gets
165
+ // written:
166
+ // · `used` must NOT withdraw an established proof (only a fidelity `defect` does). v2 actually
167
+ // shipped the downgrade, so the contract literal and reality disagreed in opposite directions.
168
+ // · a RED re-archive of kind='proof' overwrites the work file, so the old archived proof no longer
169
+ // corresponds to any code — it must be retracted, not left at the "everyone looks here" path.
170
+ {
171
+ const usedRow = (contract.split(/\r?\n/).find((l) => /decision:'used'/.test(l)) || '')
172
+ ok(/不得/.test(usedRow) && /passed/.test(usedRow) && /blocked/.test(usedRow),
173
+ "contract §4: the `used` row says an existing passed/blocked is PRESERVED (only `defect` retracts)",
174
+ 'row: ' + usedRow.slice(0, 120))
175
+ const runRows = contract.split(/\r?\n/).filter((l) => /^\|\s*`lean_run`/.test(l))
176
+ ok(runRows.length >= 2 && runRows.every((l) => /不降级|保持原状/.test(l)),
177
+ 'contract §4: both `lean_run` rows say they do not downgrade an existing passed/blocked',
178
+ 'rows: ' + runRows.map((l) => l.slice(0, 60)).join(' || '))
179
+ ok(/该文件最近一次运行\*\*失败\*\*/.test(contract) && /撤回/.test(contract),
180
+ 'contract §4: a FAILED proof re-archive is documented as retracting the previous archived proof')
181
+ }
182
+
183
+ const out = { passed, failed: findings.length, findings, notes }
184
+ if (process.argv.includes('--json')) console.log(JSON.stringify(out, null, 2))
185
+ else {
186
+ console.log('-- spec/README ↔ code traceability --')
187
+ for (const n of notes) console.log(' note ' + n)
188
+ console.log('')
189
+ for (const f of findings) console.error(' FAIL ' + f)
190
+ console.log('')
191
+ console.log('TRACEABILITY: ' + passed + ' passed, ' + findings.length + ' failed')
192
+ }
193
+ process.exit(findings.length === 0 ? 0 : 1)
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * STATUS/REPORT FIELD TABLE vs SOURCE (G-6) — the doc's per-preset field/scope rows must be exactly the
4
+ * fields the code declares, in BOTH directions, and each row's scope must match the source classification.
5
+ *
6
+ * DEFECT CLASS: "machine-face field documented nowhere / documented field that does not exist" (F12/F3):
7
+ * `实现方案.md §14.9` claimed every `status()` field had a row while >=13 top-level keys had none; and a
8
+ * table written by hand drifts the moment a field is added. Sources are derived BY MARKER (line numbers
9
+ * are claims, not anchors):
10
+ * v2/v3 -> `fieldScopes: { session: [...], durable: [...] }` (20 / 25 entries)
11
+ * v5 -> the top-level keys of the object literal `status()` returns (its `fieldScopes` is coarse)
12
+ * v4 -> has NO `fieldScopes`; the table must say so rather than invent rows
13
+ *
14
+ * Run: node tests/audit-status-report-fields.mjs
15
+ * Env (mutant harness): FIELD_DOC, FIELD_V2_JS, FIELD_V3_JS, FIELD_V5_JS, FIELD_V4_JS (absolute or relative).
16
+ */
17
+ import { readFileSync } from 'node:fs'
18
+ import { dirname, isAbsolute, join } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ const HERE = dirname(fileURLToPath(import.meta.url))
22
+ const REPO = join(HERE, '..')
23
+ let passed = 0, failed = 0
24
+ const failures = []
25
+ const ok = (cond, label, detail) => { if (cond) passed++; else { failed++; failures.push(label + (detail ? ' — ' + detail : '')) } }
26
+ const read = (envName, rel) => {
27
+ const v = process.env[envName]
28
+ return readFileSync(v ? (isAbsolute(v) ? v : join(REPO, v)) : join(REPO, rel), 'utf8')
29
+ }
30
+ function fieldScopesOf(text) {
31
+ const lines = text.split(/\r?\n/)
32
+ const start = lines.findIndex((l) => /^\s*fieldScopes:\s*\{/.test(l))
33
+ if (start === -1) return null
34
+ let depth = 0
35
+ const buf = []
36
+ for (let i = start; i < lines.length; i++) {
37
+ buf.push(lines[i]); depth += (lines[i].match(/\{/g) || []).length - (lines[i].match(/\}/g) || []).length
38
+ if (i > start && depth <= 0) break
39
+ }
40
+ const out = {}
41
+ for (const m of buf.join('\n').matchAll(/(\w+):\s*\[([\s\S]*?)\]/g)) {
42
+ out[m[1]] = m[2].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
43
+ }
44
+ return out
45
+ }
46
+ function statusKeysOf(text) {
47
+ // Robust: brace/comment/string aware, and it reads SHORTHAND properties too —
48
+ // `institute: instituteName, project, key, phase,` is FOUR keys, not one. The earlier line-anchored
49
+ // version missed 8 real keys (R19 F-A: project/key/phase/running/autoDone/runId/lastProgressAt/params).
50
+ const fn = text.search(/function\s+status\s*\(/)
51
+ if (fn === -1) return null
52
+ const ret = text.indexOf('return {', fn)
53
+ if (ret === -1) return null
54
+ const start = text.indexOf('{', ret)
55
+ let d0 = 0, stop = start, k = start
56
+ while (k < text.length) {
57
+ const c = text[k]
58
+ if (c === '/' && text[k + 1] === '/') { while (k < text.length && text[k] !== '\n') k++; continue }
59
+ if (c === '/' && text[k + 1] === '*') { k += 2; while (k < text.length && !(text[k] === '*' && text[k + 1] === '/')) k++; k += 2; continue }
60
+ if (c === '"' || c === "'" || c === '`') { const q = c; k++; while (k < text.length) { if (text[k] === '\\') k += 2; else if (text[k] === q) break; else k++ } k++; continue }
61
+ if (c === '{' || c === '[' || c === '(') d0++
62
+ else if (c === '}' || c === ']' || c === ')') { d0--; if (d0 === 0) { stop = k; break } }
63
+ k++
64
+ }
65
+ if (d0 !== 0) return null
66
+ const body = text.slice(start + 1, stop)
67
+ const segs = []
68
+ let buf = '', d = 0, j = 0
69
+ while (j < body.length) {
70
+ const c = body[j]
71
+ if (c === '/' && body[j + 1] === '/') { while (j < body.length && body[j] !== '\n') j++; continue }
72
+ if (c === '/' && body[j + 1] === '*') { j += 2; while (j < body.length && !(body[j] === '*' && body[j + 1] === '/')) j++; j += 2; continue }
73
+ if (c === '"' || c === "'" || c === '`') { const q = c; buf += c; j++; while (j < body.length) { buf += body[j]; if (body[j] === '\\') { j++; buf += body[j] } else if (body[j] === q) break; j++ } j++; continue }
74
+ if (c === '{' || c === '[' || c === '(') d++
75
+ if (c === '}' || c === ']' || c === ')') d--
76
+ if (c === ',' && d === 0) { segs.push(buf); buf = ''; j++; continue }
77
+ buf += c; j++
78
+ }
79
+ if (buf.trim()) segs.push(buf)
80
+ const keys = []
81
+ for (const seg of segs) {
82
+ const t = seg.trim()
83
+ if (!t) continue
84
+ const m = /^([A-Za-z_$][\w$]*)\s*(?::|$|,)/.exec(t)
85
+ if (m) keys.push(m[1])
86
+ }
87
+ return [...new Set(keys)]
88
+ }
89
+ /** Rows of the doc section that starts with `headingPrefix`, up to the next `### `. */
90
+ function docRows(doc, headingPrefix) {
91
+ const lines = doc.split(/\r?\n/)
92
+ const start = lines.findIndex((l) => l.startsWith(headingPrefix))
93
+ if (start === -1) return null
94
+ let end = lines.findIndex((l, i) => i > start && /^### /.test(l))
95
+ if (end === -1) end = lines.length
96
+ const rows = []
97
+ for (let i = start; i < end; i++) {
98
+ const m = /^\|\s*`([^`]+)`\s*\|\s*([^|]+?)\s*\|/.exec(lines[i])
99
+ if (m) rows.push({ name: m[1], scope: m[2] })
100
+ }
101
+ return rows
102
+ }
103
+ const diff = (a, b) => [...a].filter((x) => !b.has(x))
104
+
105
+ const doc = read('FIELD_DOC', 'docs/status-report-fields.md')
106
+ ok(doc.indexOf('G-6-FIELD-TABLE-START') !== -1 && doc.indexOf('G-6-FIELD-TABLE-END') !== -1,
107
+ 'the doc carries the machine-generated field table between its markers (so the guard knows what to read)')
108
+
109
+ const f2 = fieldScopesOf(read('FIELD_V2_JS', 'vibe-math-v2/vibe-math-v2.js'))
110
+ const f3 = fieldScopesOf(read('FIELD_V3_JS', 'vibe-math-v3/vibe-math-v3.js'))
111
+ const s5 = statusKeysOf(read('FIELD_V5_JS', 'vibe-math-v5/vibe-math-v5.js'))
112
+ const v4 = read('FIELD_V4_JS', 'vibe-math-v4/vibe-math-v4.js')
113
+ ok(!!f2 && !!f3 && !!s5, 'all three sources are parseable by marker (v2/v3 fieldScopes, v5 status())')
114
+
115
+ if (f2 && f3 && s5) {
116
+ const cases = [
117
+ { preset: 'v2', src: [...(f2.session || []), ...(f2.durable || [])], head: '### v2 的字段与作用域', scopeMap: { session: '会话', durable: '耐久' }, fs: f2 },
118
+ { preset: 'v3', src: [...(f3.session || []), ...(f3.durable || [])], head: '### v3 的字段与作用域', scopeMap: { session: '会话', durable: '耐久' }, fs: f3 },
119
+ { preset: 'v5', src: s5, head: '### v5 的字段与作用域', scopeMap: null, fs: null },
120
+ ]
121
+ for (const c of cases) {
122
+ const rows = docRows(doc, c.head)
123
+ ok(!!rows && rows.length > 0, c.preset + ': the doc section exists and has rows', String(rows && rows.length))
124
+ if (!rows) continue
125
+ const docSet = new Set(rows.map((r) => r.name))
126
+ const srcSet = new Set(c.src)
127
+ const missingInDoc = diff(srcSet, docSet)
128
+ const extraInDoc = diff(docSet, srcSet)
129
+ ok(missingInDoc.length === 0, '★ ' + c.preset + ': every source field has a doc row (doc↔source, source→doc)', JSON.stringify(missingInDoc))
130
+ ok(extraInDoc.length === 0, '★ ' + c.preset + ': every doc row names a real source field (doc↔source, doc→source)', JSON.stringify(extraInDoc))
131
+ if (c.scopeMap && c.fs) {
132
+ const wrong = []
133
+ for (const scope of ['session', 'durable']) {
134
+ for (const name of c.fs[scope] || []) {
135
+ const r = rows.find((x) => x.name === name)
136
+ if (r && r.scope !== c.scopeMap[scope]) wrong.push(name + '=' + r.scope)
137
+ }
138
+ }
139
+ ok(wrong.length === 0, '★ ' + c.preset + ': every row scope matches the source classification', JSON.stringify(wrong))
140
+ }
141
+ }
142
+ // ---- code -> doc (R19 F-A): EVERY status() top-level key lands in the v5 table OR is an explicit
143
+ // exception. The exceptions list is empty today; adding one requires a reason HERE, in this file.
144
+ const V5_EXCEPTIONS = []
145
+ {
146
+ const v5DocRows = docRows(doc, '### v5 的字段与作用域') || []
147
+ const docSet5 = new Set(v5DocRows.map((r) => r.name))
148
+ const frozen = s5
149
+ const exceptions = V5_EXCEPTIONS
150
+ console.log(' counts: frozen=' + frozen.length + ' doc-rows=' + docSet5.size + ' exceptions=' + exceptions.length)
151
+ ok(frozen.length > 0 && docSet5.size > 0, '★ the three counts are non-zero (the comparison cannot pass vacuously)',
152
+ 'frozen=' + frozen.length + ' doc-rows=' + docSet5.size + ' exceptions=' + exceptions.length)
153
+ const covered = new Set([...docSet5, ...exceptions])
154
+ const uncovered = frozen.filter((k) => !covered.has(k))
155
+ ok(uncovered.length === 0, '★ every status() top-level key is documented or an explicit exception (code->doc)',
156
+ JSON.stringify(uncovered))
157
+ const phantom = [...docSet5].filter((k) => frozen.indexOf(k) === -1 && exceptions.indexOf(k) === -1)
158
+ ok(phantom.length === 0, '★ no v5 doc row names a key status() does not return (doc->code)',
159
+ JSON.stringify(phantom))
160
+ }
161
+ // v4 must be stated as having none, and the source must agree
162
+ const v4Rows = docRows(doc, '### v4')
163
+ ok(!v4Rows || v4Rows.length === 0, '★ v4: no rows are invented (it has no fieldScopes to derive from)', String(v4Rows && v4Rows.length))
164
+ ok(doc.indexOf('**没有** `fieldScopes`') !== -1 || doc.indexOf('不存在** `fieldScopes`') !== -1,
165
+ '★ v4: the doc says explicitly that v4 has no fieldScopes')
166
+ ok(!/fieldScopes/.test(v4), '★ v4: the source confirms it (zero occurrences of fieldScopes)')
167
+ }
168
+
169
+ console.log('')
170
+ console.log('=== STATUS FIELD TABLE: ' + passed + ' passed, ' + failed + ' failed ===')
171
+ for (const f of failures) console.error(' - ' + f)
172
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mutant harness for tests/audit-status-report-fields.mjs (ships: `tests/*.mutants.mjs`).
4
+ *
5
+ * ONE MUTANT PER DIRECTION (the doc↔source comparison must bite both ways):
6
+ * A) doc→source: delete one v2 row from a COPY of docs/status-report-fields.md -> the "every source
7
+ * field has a doc row" assertion reddens by name
8
+ * B) source→doc: add a field to a COPY of vibe-math-v2.js's `fieldScopes.session` -> the "every doc row
9
+ * names a real source field" / missing-row assertion reddens by name
10
+ *
11
+ * Run: node tests/audit-status-report-fields.mutants.mjs
12
+ */
13
+ import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
14
+ import { tmpdir } from 'node:os'
15
+ import { join } from 'node:path'
16
+ import { spawnSync } from 'node:child_process'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
20
+ const REPO = join(HERE, '..')
21
+ const GUARD = join(HERE, 'audit-status-report-fields.mjs')
22
+ let passed = 0, failed = 0
23
+ const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
24
+ const run = (env) => {
25
+ const r = spawnSync(process.execPath, [GUARD], { cwd: REPO, encoding: 'utf8', env: Object.assign({}, process.env, env || {}) })
26
+ const out = String(r.stdout || '') + String(r.stderr || '')
27
+ return { status: r.status, failLines: out.split('\n').filter((l) => /^ - /.test(l)), tail: out.trim().split('\n').filter(Boolean).slice(-1)[0] || '' }
28
+ }
29
+ const baseline = run(null)
30
+ ok(baseline.status === 0, 'baseline: the field-table guard is green', baseline.tail.slice(0, 80))
31
+
32
+ const dir = mkdtempSync(join(tmpdir(), 'fieldtable-mut-'))
33
+ // A) doc -> source
34
+ const docPath = join(REPO, 'docs/status-report-fields.md')
35
+ const docLines = readFileSync(docPath, 'utf8').split(/\r?\n/)
36
+ const v2Head = docLines.findIndex((l) => l.startsWith('### v2 的字段与作用域'))
37
+ const firstRow = docLines.findIndex((l, i) => i > v2Head && /^\|\s*`[^`]+`\s*\|/.test(l))
38
+ if (v2Head === -1 || firstRow === -1) ok(false, 'A: the v2 section and its first row exist (harness and guard agree on the shape)', 'ANCHOR MISS')
39
+ else {
40
+ const removed = docLines[firstRow]
41
+ const tampered = docLines.slice(0, firstRow).concat(docLines.slice(firstRow + 1)).join('\n')
42
+ writeFileSync(join(dir, 'status-report-fields.md'), tampered)
43
+ const a = run({ FIELD_DOC: join(dir, 'status-report-fields.md') })
44
+ const named = a.failLines.some((l) => l.indexOf('v2: every source field has a doc row') !== -1)
45
+ ok(a.status !== 0 && named, '★ A (doc→source): deleting a v2 row reddens the guard by name', 'removed=' + removed.trim().slice(0, 60) + ' exit=' + a.status)
46
+ if (!named) for (const l of a.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
47
+ }
48
+ // B) source -> doc
49
+ const v2Path = join(REPO, 'vibe-math-v2/vibe-math-v2.js')
50
+ const v2src = readFileSync(v2Path, 'utf8')
51
+ const m = /(\n\s*session:\s*\[)([\s\S]*?)(\])/.exec(v2src)
52
+ if (!m) ok(false, 'B: the v2 fieldScopes session array exists (anchor check)', 'ANCHOR MISS')
53
+ else {
54
+ const injected = m[1] + m[2].replace(/\s*$/, '') + ", 'ghostFieldFromMutant'" + m[3]
55
+ writeFileSync(join(dir, 'vibe-math-v2.js'), v2src.replace(m[0], injected))
56
+ const b = run({ FIELD_V2_JS: join(dir, 'vibe-math-v2.js') })
57
+ const named = b.failLines.some((l) => l.indexOf('v2: every source field has a doc row') !== -1 || l.indexOf('v2: every doc row names a real source field') !== -1)
58
+ ok(b.status !== 0 && named, '★ B (source→doc): adding a field to the source reddens the guard by name', 'exit=' + b.status)
59
+ if (!named) for (const l of b.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
60
+ }
61
+ rmSync(dir, { recursive: true, force: true })
62
+ console.log('')
63
+ console.log('=== STATUS FIELD TABLE MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
64
+ process.exit(failed === 0 ? 0 : 1)