dsh-vibe-math 2.3.11 → 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.
- package/README.md +17 -40
- package/{AUDIT-CHECKLIST.md → docs/AUDIT-CHECKLIST.md} +323 -304
- package/docs/COMPAT-AUDIT-ROUND2.md +325 -0
- package/docs/generate_framework_diagram_v2.py +114 -0
- package/docs/generate_framework_diagram_v3.py +127 -0
- package/{RELEASE-NOTES-2.1.0.md → docs/release-notes/RELEASE-NOTES-2.1.0.md} +143 -143
- package/{RELEASE-NOTES-2.2.0.md → docs/release-notes/RELEASE-NOTES-2.2.0.md} +266 -266
- package/{RELEASE-NOTES-2.2.1.md → docs/release-notes/RELEASE-NOTES-2.2.1.md} +43 -43
- package/{RELEASE-NOTES-2.2.2.md → docs/release-notes/RELEASE-NOTES-2.2.2.md} +88 -88
- package/{RELEASE-NOTES-2.3.0.md → docs/release-notes/RELEASE-NOTES-2.3.0.md} +207 -207
- package/{RELEASE-NOTES-2.3.1.md → docs/release-notes/RELEASE-NOTES-2.3.1.md} +134 -134
- package/{RELEASE-NOTES-2.3.10.md → docs/release-notes/RELEASE-NOTES-2.3.10.md} +105 -105
- package/{RELEASE-NOTES-2.3.11.md → docs/release-notes/RELEASE-NOTES-2.3.11.md} +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -0
- package/docs/release-notes/RELEASE-NOTES-2.3.13.md +137 -0
- package/{RELEASE-NOTES-2.3.2.md → docs/release-notes/RELEASE-NOTES-2.3.2.md} +145 -145
- package/{RELEASE-NOTES-2.3.3.md → docs/release-notes/RELEASE-NOTES-2.3.3.md} +115 -115
- package/{RELEASE-NOTES-2.3.4.md → docs/release-notes/RELEASE-NOTES-2.3.4.md} +69 -69
- package/{RELEASE-NOTES-2.3.5.md → docs/release-notes/RELEASE-NOTES-2.3.5.md} +63 -63
- package/{RELEASE-NOTES-2.3.6.md → docs/release-notes/RELEASE-NOTES-2.3.6.md} +66 -66
- package/{RELEASE-NOTES-2.3.7.md → docs/release-notes/RELEASE-NOTES-2.3.7.md} +59 -59
- package/{RELEASE-NOTES-2.3.8.md → docs/release-notes/RELEASE-NOTES-2.3.8.md} +45 -45
- package/{RELEASE-NOTES-2.3.9.md → docs/release-notes/RELEASE-NOTES-2.3.9.md} +70 -70
- package/docs/test-timing.md +18 -17
- package/installer.js +258 -53
- package/package.json +50 -40
- package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
- package/tests/audit-installer-compat.test.mjs +136 -0
- package/tests/audit-installer-policy.test.mjs +245 -0
- package/{audit-persona-sensitivity.mjs → tests/audit-persona-sensitivity.mjs} +249 -249
- package/{audit-persona-surface.test.mjs → tests/audit-persona-surface.test.mjs} +349 -349
- package/{audit-prompt-invariants.mjs → tests/audit-prompt-invariants.mjs} +508 -508
- package/{audit-spec-traceability.mjs → tests/audit-spec-traceability.mjs} +193 -193
- package/{audit-v5-integrity.mjs → tests/audit-v5-integrity.mjs} +448 -448
- package/{audit-v5-sensitivity.mjs → tests/audit-v5-sensitivity.mjs} +384 -384
- package/{e2e-v5-round2.test.mjs → tests/e2e-v5-round2.test.mjs} +521 -521
- package/{formal-verify-v2.test.mjs → tests/formal-verify-v2.test.mjs} +1315 -1315
- package/{formal-verify-v3.test.mjs → tests/formal-verify-v3.test.mjs} +1257 -1257
- package/{formal-verify-v4.test.mjs → tests/formal-verify-v4.test.mjs} +1082 -1082
- package/{formal-verify-v5.test.mjs → tests/formal-verify-v5.test.mjs} +708 -708
- package/{prompt-v5-integrity.test.mjs → tests/prompt-v5-integrity.test.mjs} +3 -3
- package/{run-tests.mjs → tests/run-tests.mjs} +121 -118
- package/{selfdrive-v5.mjs → tests/selfdrive-v5.mjs} +470 -470
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +1 -1
- package//347/244/272/344/276/213/345/233/276//345/256/236/351/231/205/344/275/277/347/224/250/347/244/272/344/276/213-/351/225/277/346/210/252/345/233/276.png +0 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v2.png +0 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v3.png +0 -0
- /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('
|
|
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)
|