dsh-vibe-math 2.7.3 → 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 (101) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -682
  3. package/README.md +683 -683
  4. package/docs/AUDIT-CHECKLIST.md +755 -744
  5. package/docs/COMPAT-AUDIT-ROUND2.md +351 -351
  6. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  7. package/docs/audit-methodology.md +93 -0
  8. package/docs/final-paper.md +172 -172
  9. package/docs/formal-verification.md +613 -588
  10. package/docs/generate_framework_diagram_v2.py +114 -114
  11. package/docs/generate_framework_diagram_v3.py +127 -127
  12. package/docs/math-computation.md +195 -194
  13. package/docs/release-notes/README.en.md +1 -0
  14. package/docs/release-notes/README.md +1 -0
  15. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  16. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  17. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  18. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  19. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  20. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  21. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  22. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  23. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  24. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  25. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  26. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  27. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  28. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  29. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  30. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  31. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  32. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  33. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  34. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  35. package/docs/release-playbook.md +103 -0
  36. package/docs/slv-playbook.md +117 -0
  37. package/docs/test-timing.md +158 -157
  38. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  39. package/installer.js +792 -792
  40. package/package.json +13 -3
  41. package/prompt-corpus-v2/formal-verify-v2.json +2 -2
  42. package/prompt-corpus-v2/formal-verify-v2.md +4 -0
  43. package/prompt-corpus-v3/formal-verify-v3.json +7 -7
  44. package/prompt-corpus-v3/formal-verify-v3.md +14 -0
  45. package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
  46. package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
  47. package/scripts/clean-temp.mjs +234 -0
  48. package/tests/audit-engine-faces.mjs +298 -0
  49. package/tests/audit-formal-sensitivity.mjs +345 -342
  50. package/tests/audit-installer-compat.test.mjs +256 -256
  51. package/tests/audit-math-computation-sensitivity.mjs +1 -1
  52. package/tests/audit-persona-sensitivity.mjs +323 -323
  53. package/tests/audit-persona-surface.test.mjs +431 -431
  54. package/tests/audit-prompt-duplication.mutants.mjs +3 -2
  55. package/tests/audit-prompt-invariants.mjs +589 -546
  56. package/tests/audit-registration.mjs +78 -78
  57. package/tests/audit-spec-traceability.mjs +193 -193
  58. package/tests/audit-status-report-fields.mjs +172 -172
  59. package/tests/audit-v5-integrity.mjs +795 -795
  60. package/tests/audit-v5-sensitivity.mjs +433 -432
  61. package/tests/e2e-v5-round2.test.mjs +2453 -2426
  62. package/tests/formal-verify-v2.test.mjs +1806 -1800
  63. package/tests/formal-verify-v3.test.mjs +1757 -1750
  64. package/tests/formal-verify-v4.test.mjs +2114 -2106
  65. package/tests/formal-verify-v5.test.mjs +774 -767
  66. package/tests/math-computation-discovery.mutants.mjs +3 -3
  67. package/tests/math-computation-shared.test.mjs +122 -5
  68. package/tests/math-computation-v2.test.mjs +14 -2
  69. package/tests/math-computation-v3.test.mjs +14 -2
  70. package/tests/math-computation-v4.test.mjs +739 -739
  71. package/tests/prompt-v5-integrity.test.mjs +1291 -1291
  72. package/tests/run-tests.mjs +393 -332
  73. package/tests/selfdrive-v5.mjs +560 -560
  74. package/tests/temp-hygiene.mutants.mjs +80 -0
  75. package/tests/v2-fix-probes.mutants.mjs +4 -0
  76. package/tests/v2-fix-probes.test.mjs +1034 -1033
  77. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  78. package/tests/v3-fix-probes.mutants.mjs +99 -95
  79. package/tests/v3-fix-probes.test.mjs +891 -890
  80. package/tests/v4-final-paper.mutants.mjs +108 -0
  81. package/tests/v5-institute-fixes.mutants.mjs +218 -190
  82. package/vibe-math-v2/agent.cordis.yml +376 -376
  83. package/vibe-math-v2/math-computation.js +17 -15
  84. package/vibe-math-v2/preset.yml +2 -2
  85. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  86. package/vibe-math-v2/vibe-math-v2.js +5078 -5011
  87. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
  88. package/vibe-math-v3/agent.cordis.yml +420 -420
  89. package/vibe-math-v3/math-computation.js +17 -15
  90. package/vibe-math-v3/preset.yml +2 -2
  91. package/vibe-math-v3/vibe-math-v3.js +6215 -6149
  92. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  93. package/vibe-math-v4/agent.cordis.yml +438 -438
  94. package/vibe-math-v4/math-computation.js +17 -15
  95. package/vibe-math-v4/preset.yml +2 -2
  96. package/vibe-math-v4/vibe-math-v4.js +4490 -4422
  97. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
  98. package/vibe-math-v5/math-computation.js +17 -15
  99. package/vibe-math-v5/vibe-math-v5.js +93 -11
  100. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
  101. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -1,78 +1,78 @@
1
- /**
2
- * ROUND 2 — verify the tool-registration surface against the REAL host contract.
3
- *
4
- * `dsh-tools` register(): "duplicates within one layer ... fail". So if a preset
5
- * registers the same tool name twice in the same scope, the preset cannot mount.
6
- * The e2e mocks only push to an array, so they cannot see this. This counts what the
7
- * plugin actually registers and reports duplicates.
8
- */
9
- import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
10
- import { tmpdir } from 'node:os';
11
- import { join, dirname, resolve, isAbsolute } from 'node:path';
12
- import { fileURLToPath, pathToFileURL } from 'node:url';
13
-
14
- const WS = mkdtempSync(join(tmpdir(), 'reg-'));
15
- const toolRegs = [];
16
- const cmdRegs = [];
17
- const ctx = {
18
- get(n) {
19
- if (n === 'subprocess') return { async spawn({ argv }) {
20
- const s = argv[argv.length - 1] || '';
21
- if (/New-Item/.test(s)) { const m = s.match(/-Path\s+'((?:[^']|'')*)'/); if (m) m[1].split(',').forEach((p) => { if (p) mkdirSync(p.replace(/''/g, "'"), { recursive: true }); }); }
22
- return { done: Promise.resolve({ exitCode: 0 }) };
23
- } };
24
- return undefined;
25
- },
26
- on() {}, effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d(); } },
27
- logger: { info() {}, warn() {}, error() {} },
28
- tools: { register(spec) { toolRegs.push(spec); } },
29
- commands: { register(spec) { cmdRegs.push(spec); } },
30
- subagents: { list() { return ['spawn']; }, async startContinuable() { return { childId: 'c1' }; }, async sendMessage() {}, async followup() {}, interrupt() {} },
31
- agents: { roots() { return []; }, get() { return undefined; } },
32
- fs: {
33
- async resolve(rel, o) { return join((o && o.cwd) || WS, ...String(rel).split('/')); },
34
- async stat(t) { return existsSync(t) ? { type: 'file' } : undefined; },
35
- async readText(t) { return readFileSync(t, 'utf8'); },
36
- async writeText(t, c) { mkdirSync(dirname(t), { recursive: true }); writeFileSync(t, c, 'utf8'); },
37
- async listDir(t) { if (!existsSync(t)) return []; return readdirSync(t, { withFileTypes: true }).map((e) => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })); },
38
- },
39
- };
40
-
41
- const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..');
42
- const FILE = process.argv[2] || 'vibe-math-v3/vibe-math-v3.js';
43
- const TARGET = isAbsolute(FILE) ? FILE : resolve(REPO, FILE);
44
- const mod = await import(pathToFileURL(TARGET).href + '?t=' + Date.now());
45
- const plugin = mod.default || mod;
46
-
47
- console.log(`=== ${FILE} ===`);
48
- console.log(`exported name : ${plugin.name}`);
49
- console.log(`exported inject : ${JSON.stringify(plugin.inject)}`);
50
-
51
- let applyError = null;
52
- try { plugin.apply(ctx); } catch (e) { applyError = e; }
53
- if (applyError) { console.log(`apply() threw : ${applyError.message}`); }
54
-
55
- console.log(`tools.register calls : ${toolRegs.length}`);
56
- console.log(`commands.register calls: ${cmdRegs.length}`);
57
-
58
- const dup = (specs) => {
59
- const seen = new Map();
60
- for (const s of specs) seen.set(s.name, (seen.get(s.name) || 0) + 1);
61
- return [...seen.entries()].filter(([, c]) => c > 1);
62
- };
63
- const toolDup = dup(toolRegs);
64
- const cmdDup = dup(cmdRegs);
65
-
66
- console.log(`DUPLICATE tool names : ${toolDup.length === 0 ? 'none' : JSON.stringify(toolDup)}`);
67
- console.log(`DUPLICATE command names: ${cmdDup.length === 0 ? 'none' : JSON.stringify(cmdDup)}`);
68
-
69
- // Every registered tool must satisfy the host's mandatory output contract.
70
- const missingOutput = toolRegs.filter((s) => !s.output || typeof s.output.render !== 'function' || !s.output.schema);
71
- console.log(`tools missing output{}/render: ${missingOutput.length === 0 ? 'none' : missingOutput.map((s) => s.name).join(', ')}`);
72
- const badName = toolRegs.filter((s) => typeof s.name !== 'string' || s.name === '');
73
- console.log(`tools with a bad name : ${badName.length === 0 ? 'none' : badName.length}`);
74
-
75
- console.log(`\nfirst 8 tool names: ${toolRegs.slice(0, 8).map((s) => s.name).join(', ')}`);
76
-
77
- rmSync(WS, { recursive: true, force: true });
78
- process.exit(toolDup.length > 0 || cmdDup.length > 0 || missingOutput.length > 0 ? 1 : 0);
1
+ /**
2
+ * ROUND 2 — verify the tool-registration surface against the REAL host contract.
3
+ *
4
+ * `dsh-tools` register(): "duplicates within one layer ... fail". So if a preset
5
+ * registers the same tool name twice in the same scope, the preset cannot mount.
6
+ * The e2e mocks only push to an array, so they cannot see this. This counts what the
7
+ * plugin actually registers and reports duplicates.
8
+ */
9
+ import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join, dirname, resolve, isAbsolute } from 'node:path';
12
+ import { fileURLToPath, pathToFileURL } from 'node:url';
13
+
14
+ const WS = mkdtempSync(join(tmpdir(), 'reg-'));
15
+ const toolRegs = [];
16
+ const cmdRegs = [];
17
+ const ctx = {
18
+ get(n) {
19
+ if (n === 'subprocess') return { async spawn({ argv }) {
20
+ const s = argv[argv.length - 1] || '';
21
+ if (/New-Item/.test(s)) { const m = s.match(/-Path\s+'((?:[^']|'')*)'/); if (m) m[1].split(',').forEach((p) => { if (p) mkdirSync(p.replace(/''/g, "'"), { recursive: true }); }); }
22
+ return { done: Promise.resolve({ exitCode: 0 }) };
23
+ } };
24
+ return undefined;
25
+ },
26
+ on() {}, effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d(); } },
27
+ logger: { info() {}, warn() {}, error() {} },
28
+ tools: { register(spec) { toolRegs.push(spec); } },
29
+ commands: { register(spec) { cmdRegs.push(spec); } },
30
+ subagents: { list() { return ['spawn']; }, async startContinuable() { return { childId: 'c1' }; }, async sendMessage() {}, async followup() {}, interrupt() {} },
31
+ agents: { roots() { return []; }, get() { return undefined; } },
32
+ fs: {
33
+ async resolve(rel, o) { return join((o && o.cwd) || WS, ...String(rel).split('/')); },
34
+ async stat(t) { return existsSync(t) ? { type: 'file' } : undefined; },
35
+ async readText(t) { return readFileSync(t, 'utf8'); },
36
+ async writeText(t, c) { mkdirSync(dirname(t), { recursive: true }); writeFileSync(t, c, 'utf8'); },
37
+ async listDir(t) { if (!existsSync(t)) return []; return readdirSync(t, { withFileTypes: true }).map((e) => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })); },
38
+ },
39
+ };
40
+
41
+ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..');
42
+ const FILE = process.argv[2] || 'vibe-math-v3/vibe-math-v3.js';
43
+ const TARGET = isAbsolute(FILE) ? FILE : resolve(REPO, FILE);
44
+ const mod = await import(pathToFileURL(TARGET).href + '?t=' + Date.now());
45
+ const plugin = mod.default || mod;
46
+
47
+ console.log(`=== ${FILE} ===`);
48
+ console.log(`exported name : ${plugin.name}`);
49
+ console.log(`exported inject : ${JSON.stringify(plugin.inject)}`);
50
+
51
+ let applyError = null;
52
+ try { plugin.apply(ctx); } catch (e) { applyError = e; }
53
+ if (applyError) { console.log(`apply() threw : ${applyError.message}`); }
54
+
55
+ console.log(`tools.register calls : ${toolRegs.length}`);
56
+ console.log(`commands.register calls: ${cmdRegs.length}`);
57
+
58
+ const dup = (specs) => {
59
+ const seen = new Map();
60
+ for (const s of specs) seen.set(s.name, (seen.get(s.name) || 0) + 1);
61
+ return [...seen.entries()].filter(([, c]) => c > 1);
62
+ };
63
+ const toolDup = dup(toolRegs);
64
+ const cmdDup = dup(cmdRegs);
65
+
66
+ console.log(`DUPLICATE tool names : ${toolDup.length === 0 ? 'none' : JSON.stringify(toolDup)}`);
67
+ console.log(`DUPLICATE command names: ${cmdDup.length === 0 ? 'none' : JSON.stringify(cmdDup)}`);
68
+
69
+ // Every registered tool must satisfy the host's mandatory output contract.
70
+ const missingOutput = toolRegs.filter((s) => !s.output || typeof s.output.render !== 'function' || !s.output.schema);
71
+ console.log(`tools missing output{}/render: ${missingOutput.length === 0 ? 'none' : missingOutput.map((s) => s.name).join(', ')}`);
72
+ const badName = toolRegs.filter((s) => typeof s.name !== 'string' || s.name === '');
73
+ console.log(`tools with a bad name : ${badName.length === 0 ? 'none' : badName.length}`);
74
+
75
+ console.log(`\nfirst 8 tool names: ${toolRegs.slice(0, 8).map((s) => s.name).join(', ')}`);
76
+
77
+ rmSync(WS, { recursive: true, force: true });
78
+ process.exit(toolDup.length > 0 || cmdDup.length > 0 || missingOutput.length > 0 ? 1 : 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 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)