delimit-cli 2.4.0 → 3.0.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 (112) hide show
  1. package/.dockerignore +7 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/CODE_OF_CONDUCT.md +48 -0
  4. package/CONTRIBUTING.md +67 -0
  5. package/Dockerfile +9 -0
  6. package/LICENSE +21 -0
  7. package/README.md +18 -69
  8. package/SECURITY.md +42 -0
  9. package/adapters/gemini-forge.js +11 -0
  10. package/adapters/gemini-jamsons.js +152 -0
  11. package/bin/delimit-cli.js +8 -0
  12. package/bin/delimit-setup.js +258 -0
  13. package/gateway/ai/backends/__init__.py +0 -0
  14. package/gateway/ai/backends/async_utils.py +21 -0
  15. package/gateway/ai/backends/deploy_bridge.py +150 -0
  16. package/gateway/ai/backends/gateway_core.py +261 -0
  17. package/gateway/ai/backends/generate_bridge.py +38 -0
  18. package/gateway/ai/backends/governance_bridge.py +196 -0
  19. package/gateway/ai/backends/intel_bridge.py +59 -0
  20. package/gateway/ai/backends/memory_bridge.py +93 -0
  21. package/gateway/ai/backends/ops_bridge.py +137 -0
  22. package/gateway/ai/backends/os_bridge.py +82 -0
  23. package/gateway/ai/backends/repo_bridge.py +117 -0
  24. package/gateway/ai/backends/ui_bridge.py +118 -0
  25. package/gateway/ai/backends/vault_bridge.py +129 -0
  26. package/gateway/ai/server.py +1182 -0
  27. package/gateway/core/__init__.py +3 -0
  28. package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
  29. package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
  30. package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
  31. package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
  32. package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
  33. package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
  34. package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
  35. package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
  36. package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
  37. package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
  38. package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
  39. package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
  40. package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
  41. package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
  42. package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
  43. package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
  44. package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
  45. package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
  46. package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
  47. package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
  48. package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
  49. package/gateway/core/auto_baseline.py +304 -0
  50. package/gateway/core/ci_formatter.py +283 -0
  51. package/gateway/core/complexity_analyzer.py +386 -0
  52. package/gateway/core/contract_ledger.py +345 -0
  53. package/gateway/core/dependency_graph.py +218 -0
  54. package/gateway/core/dependency_manifest.py +223 -0
  55. package/gateway/core/diff_engine_v2.py +477 -0
  56. package/gateway/core/diff_engine_v2.py.bak +426 -0
  57. package/gateway/core/event_backbone.py +268 -0
  58. package/gateway/core/event_schema.py +258 -0
  59. package/gateway/core/explainer.py +438 -0
  60. package/gateway/core/gateway.py +128 -0
  61. package/gateway/core/gateway_v2.py +154 -0
  62. package/gateway/core/gateway_v3.py +224 -0
  63. package/gateway/core/impact_analyzer.py +163 -0
  64. package/gateway/core/policies/default.yml +13 -0
  65. package/gateway/core/policies/relaxed.yml +48 -0
  66. package/gateway/core/policies/strict.yml +55 -0
  67. package/gateway/core/policy_engine.py +464 -0
  68. package/gateway/core/registry.py +52 -0
  69. package/gateway/core/registry_v2.py +132 -0
  70. package/gateway/core/registry_v3.py +134 -0
  71. package/gateway/core/semver_classifier.py +152 -0
  72. package/gateway/core/spec_detector.py +130 -0
  73. package/gateway/core/surface_bridge.py +307 -0
  74. package/gateway/core/zero_spec/__init__.py +4 -0
  75. package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
  76. package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
  77. package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
  78. package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
  79. package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
  80. package/gateway/core/zero_spec/detector.py +353 -0
  81. package/gateway/core/zero_spec/express_extractor.py +483 -0
  82. package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
  83. package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
  84. package/gateway/tasks/__init__.py +1 -0
  85. package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
  86. package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
  87. package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
  88. package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
  89. package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
  90. package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
  91. package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
  92. package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
  93. package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
  94. package/gateway/tasks/check_policy.py +177 -0
  95. package/gateway/tasks/check_policy_v2.py +255 -0
  96. package/gateway/tasks/check_policy_v3.py +255 -0
  97. package/gateway/tasks/explain_diff.py +305 -0
  98. package/gateway/tasks/explain_diff_v2.py +267 -0
  99. package/gateway/tasks/validate_api.py +131 -0
  100. package/gateway/tasks/validate_api_v2.py +208 -0
  101. package/gateway/tasks/validate_api_v3.py +163 -0
  102. package/package.json +4 -3
  103. package/adapters/codex-skill.js +0 -87
  104. package/adapters/cursor-extension.js +0 -190
  105. package/adapters/gemini-action.js +0 -93
  106. package/adapters/openai-function.js +0 -112
  107. package/adapters/xai-plugin.js +0 -151
  108. package/test-decision-engine.js +0 -181
  109. package/test-hook.js +0 -27
  110. package/tests/cli.test.js +0 -359
  111. package/tests/fixtures/openapi-changed.yaml +0 -56
  112. package/tests/fixtures/openapi.yaml +0 -87
@@ -1,181 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const DecisionEngine = require('./lib/decision-engine');
4
- const axios = require('axios');
5
-
6
- async function runTests() {
7
- console.log('=== DECISION ENGINE HOSTILE VERIFICATION ===\n');
8
-
9
- // First restore good policy and restart agent
10
- const fs = require('fs');
11
- const goodPolicy = `defaultMode: advisory
12
-
13
- rules:
14
- - name: "Production Protection"
15
- mode: enforce
16
- triggers:
17
- - gitBranch: [main, master, production]
18
-
19
- - name: "Payment Code Security"
20
- mode: enforce
21
- triggers:
22
- - path: "**/payment/**"
23
- - content: ["stripe", "payment", "billing"]
24
-
25
- - name: "Documentation Freedom"
26
- mode: advisory
27
- triggers:
28
- - path: "**/*.md"
29
- final: true`;
30
-
31
- fs.writeFileSync('delimit.yml', goodPolicy);
32
-
33
- // Kill existing agent
34
- try {
35
- require('child_process').execSync('pkill -f "node lib/agent.js"');
36
- } catch(e) {}
37
-
38
- // Start fresh agent
39
- const agent = require('child_process').spawn('node', ['lib/agent.js'], {
40
- detached: true,
41
- stdio: 'ignore'
42
- });
43
- agent.unref();
44
-
45
- // Wait for agent
46
- await new Promise(r => setTimeout(r, 2000));
47
-
48
- const tests = [
49
- {
50
- name: 'TEST 1: Documentation file -> Advisory',
51
- context: {
52
- command: 'pre-commit',
53
- pwd: '/test',
54
- gitBranch: 'feature',
55
- files: ['README.md', 'docs/api.md'],
56
- diff: 'documentation changes'
57
- },
58
- expected: 'advisory'
59
- },
60
- {
61
- name: 'TEST 2: Payment path -> Enforce',
62
- context: {
63
- command: 'pre-commit',
64
- pwd: '/test',
65
- gitBranch: 'feature',
66
- files: ['lib/payment/stripe.js'],
67
- diff: 'payment code changes'
68
- },
69
- expected: 'enforce'
70
- },
71
- {
72
- name: 'TEST 3: Main branch -> Enforce',
73
- context: {
74
- command: 'pre-commit',
75
- pwd: '/test',
76
- gitBranch: 'main',
77
- files: ['lib/utils.js'],
78
- diff: 'utility changes'
79
- },
80
- expected: 'enforce'
81
- },
82
- {
83
- name: 'TEST 4: No match -> Default advisory',
84
- context: {
85
- command: 'pre-commit',
86
- pwd: '/test',
87
- gitBranch: 'feature',
88
- files: ['lib/utils.js'],
89
- diff: 'regular code'
90
- },
91
- expected: 'advisory'
92
- },
93
- {
94
- name: 'TEST 5: Conflicting rules -> Stronger wins',
95
- context: {
96
- command: 'pre-commit',
97
- pwd: '/test',
98
- gitBranch: 'main',
99
- files: ['README.md'],
100
- diff: 'readme on main branch'
101
- },
102
- expected: 'enforce' // Production Protection should win over Documentation Freedom
103
- },
104
- {
105
- name: 'TEST 6: Determinism check (repeat test 2)',
106
- context: {
107
- command: 'pre-commit',
108
- pwd: '/test',
109
- gitBranch: 'feature',
110
- files: ['lib/payment/stripe.js'],
111
- diff: 'payment code changes'
112
- },
113
- expected: 'enforce'
114
- }
115
- ];
116
-
117
- const results = [];
118
- for (const test of tests) {
119
- try {
120
- const response = await axios.post('http://127.0.0.1:7823/evaluate', test.context);
121
- const decision = response.data;
122
-
123
- const result = {
124
- test: test.name,
125
- expected: test.expected,
126
- actual: decision.mode,
127
- action: decision.action,
128
- rule: decision.rule,
129
- pass: decision.mode === test.expected
130
- };
131
-
132
- results.push(result);
133
- console.log(`${test.name}`);
134
- console.log(` Expected: ${test.expected}, Actual: ${decision.mode}`);
135
- console.log(` Rule: ${decision.rule || 'none'}`);
136
- console.log(` Status: ${result.pass ? '✅ PASS' : '❌ FAIL'}\n`);
137
-
138
- // Get explanation for this decision
139
- const explainResponse = await axios.get('http://127.0.0.1:7823/explain/last');
140
- if (explainResponse.data.explanation) {
141
- console.log(' Explanation quality check:');
142
- const exp = explainResponse.data.explanation;
143
- console.log(` - Has decision ID: ${exp.includes('Decision ID:') ? '✓' : '✗'}`);
144
- console.log(` - Has effective mode: ${exp.includes('Effective:') ? '✓' : '✗'}`);
145
- console.log(` - Has matched rules: ${exp.includes('MATCHED RULES') || exp.includes('No matching rules') ? '✓' : '✗'}`);
146
- console.log(` - Has context: ${exp.includes('CONTEXT') ? '✓' : '✗'}\n`);
147
- }
148
-
149
- } catch (e) {
150
- results.push({
151
- test: test.name,
152
- expected: test.expected,
153
- actual: 'ERROR',
154
- error: e.message,
155
- pass: false
156
- });
157
- console.log(`${test.name}: ❌ ERROR - ${e.message}\n`);
158
- }
159
- }
160
-
161
- // Summary
162
- console.log('=== SUMMARY ===');
163
- const passed = results.filter(r => r.pass).length;
164
- console.log(`Passed: ${passed}/${results.length}`);
165
-
166
- // Check determinism
167
- if (results[1].actual === results[5].actual && results[1].rule === results[5].rule) {
168
- console.log('✅ DETERMINISM CHECK: Same input produced same output');
169
- } else {
170
- console.log('❌ DETERMINISM CHECK: Same input produced different outputs!');
171
- }
172
-
173
- // Kill agent
174
- try {
175
- require('child_process').execSync('pkill -f "node lib/agent.js"');
176
- } catch(e) {}
177
-
178
- process.exit(passed === results.length ? 0 : 1);
179
- }
180
-
181
- runTests().catch(console.error);
package/test-hook.js DELETED
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // Test script to simulate a Git pre-commit hook
4
- const axios = require('axios');
5
-
6
- async function testHook() {
7
- const context = {
8
- command: 'pre-commit',
9
- pwd: '/home/delimit/npm-delimit',
10
- gitBranch: 'main',
11
- files: ['lib/payment/stripe.js', 'README.md'],
12
- diff: 'diff --git a/lib/payment/stripe.js\n+const stripe = require("stripe");'
13
- };
14
-
15
- try {
16
- const response = await axios.post('http://127.0.0.1:7823/evaluate', context);
17
- console.log('Decision:', response.data);
18
-
19
- // Now test the explain endpoint
20
- const explainResponse = await axios.get('http://127.0.0.1:7823/explain/last');
21
- console.log('\n' + explainResponse.data.explanation);
22
- } catch (e) {
23
- console.error('Error:', e.message);
24
- }
25
- }
26
-
27
- testHook();
package/tests/cli.test.js DELETED
@@ -1,359 +0,0 @@
1
- /**
2
- * Delimit CLI Tests
3
- *
4
- * Uses Node.js built-in test runner (node:test).
5
- * Run with: node --test tests/cli.test.js
6
- */
7
-
8
- const { describe, it, before, after } = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const { execSync } = require('node:child_process');
11
- const path = require('node:path');
12
- const fs = require('node:fs');
13
- const os = require('node:os');
14
-
15
- const CLI = path.join(__dirname, '..', 'bin', 'delimit-cli.js');
16
- const FIXTURES = path.join(__dirname, 'fixtures');
17
- const SPEC_CLEAN = path.join(FIXTURES, 'openapi.yaml');
18
- const SPEC_BREAKING = path.join(FIXTURES, 'openapi-changed.yaml');
19
-
20
- /**
21
- * Run the CLI and return { stdout, stderr, exitCode }.
22
- * Does not throw on non-zero exit.
23
- */
24
- function run(args, opts = {}) {
25
- const cwd = opts.cwd || os.tmpdir();
26
- try {
27
- const stdout = execSync(`node "${CLI}" ${args}`, {
28
- cwd,
29
- encoding: 'utf-8',
30
- timeout: 30000,
31
- env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
32
- });
33
- return { stdout, stderr: '', exitCode: 0 };
34
- } catch (err) {
35
- return {
36
- stdout: err.stdout || '',
37
- stderr: err.stderr || '',
38
- exitCode: err.status ?? 1,
39
- };
40
- }
41
- }
42
-
43
- // ---------------------------------------------------------------------------
44
- // 1. CLI entry point loads without error
45
- // ---------------------------------------------------------------------------
46
- describe('CLI entry point', () => {
47
- it('loads the module without throwing', () => {
48
- assert.doesNotThrow(() => {
49
- execSync(`node --check "${CLI}"`, { encoding: 'utf-8' });
50
- });
51
- });
52
- });
53
-
54
- // ---------------------------------------------------------------------------
55
- // 2. --version returns the version from package.json
56
- // ---------------------------------------------------------------------------
57
- describe('--version', () => {
58
- it('prints the version from package.json', () => {
59
- const pkg = require(path.join(__dirname, '..', 'package.json'));
60
- const { stdout } = run('--version');
61
- assert.equal(stdout.trim(), pkg.version);
62
- });
63
- });
64
-
65
- // ---------------------------------------------------------------------------
66
- // 3. --help works at top level
67
- // ---------------------------------------------------------------------------
68
- describe('--help (top level)', () => {
69
- it('shows usage and lists commands', () => {
70
- const { stdout } = run('--help');
71
- assert.match(stdout, /Usage:/);
72
- assert.match(stdout, /init/);
73
- assert.match(stdout, /lint/);
74
- assert.match(stdout, /diff/);
75
- assert.match(stdout, /explain/);
76
- assert.match(stdout, /doctor/);
77
- });
78
- });
79
-
80
- // ---------------------------------------------------------------------------
81
- // 4. Each command has help text
82
- // ---------------------------------------------------------------------------
83
- describe('subcommand --help', () => {
84
- for (const cmd of ['init', 'lint', 'diff', 'explain', 'doctor']) {
85
- it(`"help ${cmd}" shows description`, () => {
86
- const { stdout } = run(`help ${cmd}`);
87
- assert.match(stdout, /Usage:/);
88
- assert.match(stdout, new RegExp(cmd));
89
- });
90
- }
91
- });
92
-
93
- // ---------------------------------------------------------------------------
94
- // 5. init creates .delimit/policies.yml with default preset
95
- // ---------------------------------------------------------------------------
96
- describe('init command', () => {
97
- let tmpDir;
98
-
99
- before(() => {
100
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'delimit-test-init-'));
101
- });
102
-
103
- after(() => {
104
- fs.rmSync(tmpDir, { recursive: true, force: true });
105
- });
106
-
107
- it('creates .delimit/policies.yml with default preset', () => {
108
- const { stdout, exitCode } = run('init', { cwd: tmpDir });
109
- assert.equal(exitCode, 0);
110
- const policyPath = path.join(tmpDir, '.delimit', 'policies.yml');
111
- assert.ok(fs.existsSync(policyPath), 'policies.yml should exist');
112
- const content = fs.readFileSync(policyPath, 'utf-8');
113
- assert.match(content, /Delimit Policy Preset: default/);
114
- assert.match(content, /override_defaults: false/);
115
- });
116
-
117
- it('reports already initialized on second run', () => {
118
- const { stdout, exitCode } = run('init', { cwd: tmpDir });
119
- assert.equal(exitCode, 0);
120
- assert.match(stdout, /Already initialized/);
121
- });
122
- });
123
-
124
- describe('init --preset strict', () => {
125
- let tmpDir;
126
-
127
- before(() => {
128
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'delimit-test-init-strict-'));
129
- });
130
-
131
- after(() => {
132
- fs.rmSync(tmpDir, { recursive: true, force: true });
133
- });
134
-
135
- it('creates policies.yml with strict preset', () => {
136
- const { stdout, exitCode } = run('init --preset strict', { cwd: tmpDir });
137
- assert.equal(exitCode, 0);
138
- const content = fs.readFileSync(
139
- path.join(tmpDir, '.delimit', 'policies.yml'),
140
- 'utf-8'
141
- );
142
- assert.match(content, /Delimit Policy Preset: strict/);
143
- assert.match(content, /no_endpoint_removal/);
144
- });
145
- });
146
-
147
- describe('init --preset relaxed', () => {
148
- let tmpDir;
149
-
150
- before(() => {
151
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'delimit-test-init-relaxed-'));
152
- });
153
-
154
- after(() => {
155
- fs.rmSync(tmpDir, { recursive: true, force: true });
156
- });
157
-
158
- it('creates policies.yml with relaxed preset', () => {
159
- const { stdout, exitCode } = run('init --preset relaxed', { cwd: tmpDir });
160
- assert.equal(exitCode, 0);
161
- const content = fs.readFileSync(
162
- path.join(tmpDir, '.delimit', 'policies.yml'),
163
- 'utf-8'
164
- );
165
- assert.match(content, /Delimit Policy Preset: relaxed/);
166
- assert.match(content, /warn_endpoint_removal/);
167
- });
168
- });
169
-
170
- // ---------------------------------------------------------------------------
171
- // 6. lint with identical specs returns clean (exit 0)
172
- // ---------------------------------------------------------------------------
173
- describe('lint (clean)', () => {
174
- it('returns exit 0 with no violations for identical specs', () => {
175
- const { stdout, exitCode } = run(
176
- `lint "${SPEC_CLEAN}" "${SPEC_CLEAN}" --json`
177
- );
178
- assert.equal(exitCode, 0);
179
- const result = JSON.parse(stdout);
180
- assert.equal(result.decision, 'pass');
181
- assert.equal(result.exit_code, 0);
182
- assert.equal(result.summary.breaking_changes, 0);
183
- assert.equal(result.violations.length, 0);
184
- });
185
- });
186
-
187
- // ---------------------------------------------------------------------------
188
- // 7. lint with breaking change returns exit 1
189
- // ---------------------------------------------------------------------------
190
- describe('lint (breaking)', () => {
191
- it('returns exit 1 when breaking changes are detected', () => {
192
- const { stdout, exitCode } = run(
193
- `lint "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
194
- );
195
- assert.equal(exitCode, 1);
196
- const result = JSON.parse(stdout);
197
- assert.equal(result.decision, 'fail');
198
- assert.equal(result.exit_code, 1);
199
- assert.ok(result.summary.breaking_changes > 0, 'should have breaking changes');
200
- assert.ok(result.violations.length > 0, 'should have violations');
201
- });
202
-
203
- it('includes endpoint_removed in changes', () => {
204
- const { stdout } = run(
205
- `lint "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
206
- );
207
- const result = JSON.parse(stdout);
208
- const types = result.all_changes.map(c => c.type);
209
- assert.ok(types.includes('endpoint_removed'), 'should detect endpoint removal');
210
- });
211
-
212
- it('includes semver bump classification', () => {
213
- const { stdout } = run(
214
- `lint "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
215
- );
216
- const result = JSON.parse(stdout);
217
- assert.ok(result.semver, 'should have semver field');
218
- assert.equal(result.semver.bump, 'major');
219
- });
220
- });
221
-
222
- // ---------------------------------------------------------------------------
223
- // 8. diff outputs change types
224
- // ---------------------------------------------------------------------------
225
- describe('diff command', () => {
226
- it('outputs changes between two specs', () => {
227
- const { stdout, exitCode } = run(
228
- `diff "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
229
- );
230
- assert.equal(exitCode, 0);
231
- const result = JSON.parse(stdout);
232
- assert.ok(result.total_changes > 0, 'should have changes');
233
- assert.ok(result.breaking_changes > 0, 'should have breaking changes');
234
- assert.ok(Array.isArray(result.changes), 'changes should be an array');
235
- });
236
-
237
- it('reports change types correctly', () => {
238
- const { stdout } = run(
239
- `diff "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
240
- );
241
- const result = JSON.parse(stdout);
242
- const types = result.changes.map(c => c.type);
243
- assert.ok(types.includes('endpoint_removed'));
244
- assert.ok(types.includes('type_changed'));
245
- assert.ok(types.includes('enum_value_removed'));
246
- });
247
-
248
- it('marks breaking changes with is_breaking flag', () => {
249
- const { stdout } = run(
250
- `diff "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
251
- );
252
- const result = JSON.parse(stdout);
253
- const breakingChanges = result.changes.filter(c => c.is_breaking);
254
- assert.equal(breakingChanges.length, result.breaking_changes);
255
- });
256
-
257
- it('returns no changes for identical specs', () => {
258
- const { stdout, exitCode } = run(
259
- `diff "${SPEC_CLEAN}" "${SPEC_CLEAN}" --json`
260
- );
261
- assert.equal(exitCode, 0);
262
- const result = JSON.parse(stdout);
263
- assert.equal(result.total_changes, 0);
264
- assert.equal(result.breaking_changes, 0);
265
- });
266
- });
267
-
268
- // ---------------------------------------------------------------------------
269
- // 9. explain command
270
- // ---------------------------------------------------------------------------
271
- describe('explain command', () => {
272
- it('generates human-readable explanation', () => {
273
- const { stdout, exitCode } = run(
274
- `explain "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
275
- );
276
- assert.equal(exitCode, 0);
277
- const result = JSON.parse(stdout);
278
- assert.ok(result.output, 'should have output text');
279
- assert.ok(result.output.length > 0, 'output should not be empty');
280
- assert.ok(result.template, 'should report template used');
281
- });
282
-
283
- it('supports --template flag', () => {
284
- const { stdout, exitCode } = run(
285
- `explain "${SPEC_CLEAN}" "${SPEC_BREAKING}" --template migration --json`
286
- );
287
- assert.equal(exitCode, 0);
288
- const result = JSON.parse(stdout);
289
- assert.equal(result.template, 'migration');
290
- });
291
- });
292
-
293
- // ---------------------------------------------------------------------------
294
- // 10. lint with --policy preset
295
- // ---------------------------------------------------------------------------
296
- describe('lint --policy', () => {
297
- it('accepts relaxed preset and does not fail on breaking changes', () => {
298
- // relaxed preset uses action:warn, so decision is "warn" not "fail"
299
- const { stdout, exitCode } = run(
300
- `lint "${SPEC_CLEAN}" "${SPEC_BREAKING}" --policy relaxed --json`
301
- );
302
- assert.equal(exitCode, 0, 'relaxed preset should exit 0');
303
- const result = JSON.parse(stdout);
304
- assert.notEqual(result.decision, 'fail', 'relaxed should not produce fail decision');
305
- });
306
-
307
- it('accepts strict preset and fails on breaking changes', () => {
308
- const { stdout, exitCode } = run(
309
- `lint "${SPEC_CLEAN}" "${SPEC_BREAKING}" --policy strict --json`
310
- );
311
- assert.equal(exitCode, 1);
312
- const result = JSON.parse(stdout);
313
- assert.equal(result.decision, 'fail');
314
- assert.ok(result.violations.length > 0);
315
- });
316
- });
317
-
318
- // ---------------------------------------------------------------------------
319
- // 11. Error handling -- missing files
320
- // ---------------------------------------------------------------------------
321
- describe('error handling', () => {
322
- it('lint with nonexistent spec file reports error', () => {
323
- const { exitCode } = run('lint /nonexistent/old.yaml /nonexistent/new.yaml --json');
324
- assert.notEqual(exitCode, 0);
325
- });
326
-
327
- it('diff with nonexistent spec file reports error', () => {
328
- const { exitCode } = run('diff /nonexistent/old.yaml /nonexistent/new.yaml --json');
329
- assert.notEqual(exitCode, 0);
330
- });
331
- });
332
-
333
- // ---------------------------------------------------------------------------
334
- // 12. api-engine module exports
335
- // ---------------------------------------------------------------------------
336
- describe('api-engine module', () => {
337
- it('exports lint, diff, explain, semver, zeroSpec functions', () => {
338
- const engine = require(path.join(__dirname, '..', 'lib', 'api-engine.js'));
339
- assert.equal(typeof engine.lint, 'function');
340
- assert.equal(typeof engine.diff, 'function');
341
- assert.equal(typeof engine.explain, 'function');
342
- assert.equal(typeof engine.semver, 'function');
343
- assert.equal(typeof engine.zeroSpec, 'function');
344
- });
345
-
346
- it('lint returns parsed JSON with decision field', () => {
347
- const engine = require(path.join(__dirname, '..', 'lib', 'api-engine.js'));
348
- const result = engine.lint(SPEC_CLEAN, SPEC_CLEAN);
349
- assert.ok(result.decision, 'should have decision field');
350
- assert.equal(result.decision, 'pass');
351
- });
352
-
353
- it('diff returns parsed JSON with changes array', () => {
354
- const engine = require(path.join(__dirname, '..', 'lib', 'api-engine.js'));
355
- const result = engine.diff(SPEC_CLEAN, SPEC_BREAKING);
356
- assert.ok(Array.isArray(result.changes), 'should have changes array');
357
- assert.ok(result.total_changes > 0);
358
- });
359
- });
@@ -1,56 +0,0 @@
1
- openapi: "3.0.3"
2
- info:
3
- title: Pet Store API
4
- version: "2.0.0"
5
- description: Sample API for Delimit quickstart
6
- paths:
7
- /pets:
8
- get:
9
- summary: List all pets
10
- operationId: listPets
11
- parameters:
12
- - name: limit
13
- in: query
14
- required: false
15
- schema:
16
- type: integer
17
- format: int32
18
- responses:
19
- "200":
20
- description: A list of pets
21
- content:
22
- application/json:
23
- schema:
24
- type: array
25
- items:
26
- $ref: "#/components/schemas/Pet"
27
- post:
28
- summary: Create a pet
29
- operationId: createPet
30
- requestBody:
31
- required: true
32
- content:
33
- application/json:
34
- schema:
35
- $ref: "#/components/schemas/Pet"
36
- responses:
37
- "201":
38
- description: Pet created
39
- # /pets/{petId} removed -- this is the breaking change
40
- components:
41
- schemas:
42
- Pet:
43
- type: object
44
- required:
45
- - id
46
- - name
47
- properties:
48
- id:
49
- type: integer
50
- name:
51
- type: string
52
- status:
53
- type: string
54
- enum:
55
- - available
56
- - adopted