dsh-vibe-math 2.7.3 → 2.8.1

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 (106) 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 +758 -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 +2 -0
  14. package/docs/release-notes/README.md +2 -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-notes/RELEASE-NOTES-2.8.1.en.md +49 -0
  36. package/docs/release-notes/RELEASE-NOTES-2.8.1.md +49 -0
  37. package/docs/release-playbook.md +111 -0
  38. package/docs/slv-playbook.md +117 -0
  39. package/docs/test-timing.md +158 -157
  40. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  41. package/installer.js +792 -792
  42. package/package.json +18 -3
  43. package/prompt-corpus-v2/formal-verify-v2.json +2 -2
  44. package/prompt-corpus-v2/formal-verify-v2.md +4 -0
  45. package/prompt-corpus-v3/formal-verify-v3.json +7 -7
  46. package/prompt-corpus-v3/formal-verify-v3.md +14 -0
  47. package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
  48. package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
  49. package/scripts/clean-temp.mjs +234 -0
  50. package/scripts/release-check.mjs +181 -0
  51. package/tests/audit-engine-faces.mjs +298 -0
  52. package/tests/audit-formal-sensitivity.mjs +345 -342
  53. package/tests/audit-installer-compat.test.mjs +256 -256
  54. package/tests/audit-math-computation-sensitivity.mjs +1 -1
  55. package/tests/audit-persona-sensitivity.mjs +323 -323
  56. package/tests/audit-persona-surface.test.mjs +431 -431
  57. package/tests/audit-prompt-duplication.mutants.mjs +3 -2
  58. package/tests/audit-prompt-invariants.mjs +589 -546
  59. package/tests/audit-registration.mjs +78 -78
  60. package/tests/audit-spec-traceability.mjs +193 -193
  61. package/tests/audit-status-report-fields.mjs +172 -172
  62. package/tests/audit-v5-integrity.mjs +795 -795
  63. package/tests/audit-v5-sensitivity.mjs +433 -432
  64. package/tests/e2e-v5-round2.test.mjs +2453 -2426
  65. package/tests/formal-verify-v2.test.mjs +1806 -1800
  66. package/tests/formal-verify-v3.test.mjs +1757 -1750
  67. package/tests/formal-verify-v4.test.mjs +2114 -2106
  68. package/tests/formal-verify-v5.test.mjs +774 -767
  69. package/tests/math-computation-discovery.mutants.mjs +3 -3
  70. package/tests/math-computation-shared.test.mjs +122 -5
  71. package/tests/math-computation-v2.test.mjs +14 -2
  72. package/tests/math-computation-v3.test.mjs +14 -2
  73. package/tests/math-computation-v4.test.mjs +739 -739
  74. package/tests/prompt-v5-integrity.test.mjs +1291 -1291
  75. package/tests/release-check.mutants.mjs +78 -0
  76. package/tests/run-tests.mjs +393 -332
  77. package/tests/selfdrive-v5.mjs +560 -560
  78. package/tests/temp-hygiene.mutants.mjs +80 -0
  79. package/tests/v2-fix-probes.mutants.mjs +4 -0
  80. package/tests/v2-fix-probes.test.mjs +1034 -1033
  81. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  82. package/tests/v3-fix-probes.mutants.mjs +99 -95
  83. package/tests/v3-fix-probes.test.mjs +891 -890
  84. package/tests/v4-final-paper.mutants.mjs +116 -0
  85. package/tests/v4-final-paper.test.mjs +628 -0
  86. package/tests/v5-institute-fixes.mutants.mjs +218 -190
  87. package/vibe-math-v2/agent.cordis.yml +376 -376
  88. package/vibe-math-v2/math-computation.js +17 -15
  89. package/vibe-math-v2/preset.yml +2 -2
  90. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  91. package/vibe-math-v2/vibe-math-v2.js +5078 -5011
  92. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
  93. package/vibe-math-v3/agent.cordis.yml +420 -420
  94. package/vibe-math-v3/math-computation.js +17 -15
  95. package/vibe-math-v3/preset.yml +2 -2
  96. package/vibe-math-v3/vibe-math-v3.js +6215 -6149
  97. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  98. package/vibe-math-v4/agent.cordis.yml +438 -438
  99. package/vibe-math-v4/math-computation.js +17 -15
  100. package/vibe-math-v4/preset.yml +2 -2
  101. package/vibe-math-v4/vibe-math-v4.js +4520 -4422
  102. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
  103. package/vibe-math-v5/math-computation.js +17 -15
  104. package/vibe-math-v5/vibe-math-v5.js +93 -11
  105. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
  106. 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)