dsh-vibe-math 2.3.12 → 2.3.13

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 (47) hide show
  1. package/README.md +17 -13
  2. package/{AUDIT-CHECKLIST.md → docs/AUDIT-CHECKLIST.md} +323 -304
  3. package/docs/COMPAT-AUDIT-ROUND2.md +325 -0
  4. package/docs/generate_framework_diagram_v2.py +114 -0
  5. package/docs/generate_framework_diagram_v3.py +127 -0
  6. package/{RELEASE-NOTES-2.1.0.md → docs/release-notes/RELEASE-NOTES-2.1.0.md} +143 -143
  7. package/{RELEASE-NOTES-2.2.0.md → docs/release-notes/RELEASE-NOTES-2.2.0.md} +266 -266
  8. package/{RELEASE-NOTES-2.2.1.md → docs/release-notes/RELEASE-NOTES-2.2.1.md} +43 -43
  9. package/{RELEASE-NOTES-2.2.2.md → docs/release-notes/RELEASE-NOTES-2.2.2.md} +88 -88
  10. package/{RELEASE-NOTES-2.3.0.md → docs/release-notes/RELEASE-NOTES-2.3.0.md} +207 -207
  11. package/{RELEASE-NOTES-2.3.1.md → docs/release-notes/RELEASE-NOTES-2.3.1.md} +134 -134
  12. package/{RELEASE-NOTES-2.3.10.md → docs/release-notes/RELEASE-NOTES-2.3.10.md} +105 -105
  13. package/{RELEASE-NOTES-2.3.11.md → docs/release-notes/RELEASE-NOTES-2.3.11.md} +57 -57
  14. package/{RELEASE-NOTES-2.3.12.md → docs/release-notes/RELEASE-NOTES-2.3.12.md} +80 -80
  15. package/docs/release-notes/RELEASE-NOTES-2.3.13.md +137 -0
  16. package/{RELEASE-NOTES-2.3.2.md → docs/release-notes/RELEASE-NOTES-2.3.2.md} +145 -145
  17. package/{RELEASE-NOTES-2.3.3.md → docs/release-notes/RELEASE-NOTES-2.3.3.md} +115 -115
  18. package/{RELEASE-NOTES-2.3.4.md → docs/release-notes/RELEASE-NOTES-2.3.4.md} +69 -69
  19. package/{RELEASE-NOTES-2.3.5.md → docs/release-notes/RELEASE-NOTES-2.3.5.md} +63 -63
  20. package/{RELEASE-NOTES-2.3.6.md → docs/release-notes/RELEASE-NOTES-2.3.6.md} +66 -66
  21. package/{RELEASE-NOTES-2.3.7.md → docs/release-notes/RELEASE-NOTES-2.3.7.md} +59 -59
  22. package/{RELEASE-NOTES-2.3.8.md → docs/release-notes/RELEASE-NOTES-2.3.8.md} +45 -45
  23. package/{RELEASE-NOTES-2.3.9.md → docs/release-notes/RELEASE-NOTES-2.3.9.md} +70 -70
  24. package/docs/test-timing.md +18 -17
  25. package/installer.js +105 -41
  26. package/package.json +42 -37
  27. package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
  28. package/{audit-installer-compat.test.mjs → tests/audit-installer-compat.test.mjs} +136 -136
  29. package/tests/audit-installer-policy.test.mjs +245 -0
  30. package/{audit-persona-sensitivity.mjs → tests/audit-persona-sensitivity.mjs} +249 -249
  31. package/{audit-persona-surface.test.mjs → tests/audit-persona-surface.test.mjs} +349 -349
  32. package/{audit-prompt-invariants.mjs → tests/audit-prompt-invariants.mjs} +508 -508
  33. package/{audit-spec-traceability.mjs → tests/audit-spec-traceability.mjs} +193 -193
  34. package/{audit-v5-integrity.mjs → tests/audit-v5-integrity.mjs} +448 -448
  35. package/{audit-v5-sensitivity.mjs → tests/audit-v5-sensitivity.mjs} +384 -384
  36. package/{e2e-v5-round2.test.mjs → tests/e2e-v5-round2.test.mjs} +521 -521
  37. package/{formal-verify-v2.test.mjs → tests/formal-verify-v2.test.mjs} +1315 -1315
  38. package/{formal-verify-v3.test.mjs → tests/formal-verify-v3.test.mjs} +1257 -1257
  39. package/{formal-verify-v4.test.mjs → tests/formal-verify-v4.test.mjs} +1082 -1082
  40. package/{formal-verify-v5.test.mjs → tests/formal-verify-v5.test.mjs} +708 -708
  41. package/{prompt-v5-integrity.test.mjs → tests/prompt-v5-integrity.test.mjs} +3 -3
  42. package/{run-tests.mjs → tests/run-tests.mjs} +121 -118
  43. package/{selfdrive-v5.mjs → tests/selfdrive-v5.mjs} +470 -470
  44. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
  45. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
  46. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +1 -1
  47. /package/{RELEASE-NOTES-2.0.22.md → docs/release-notes/RELEASE-NOTES-2.0.22.md} +0 -0
@@ -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 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)