@amitgaikwad37/api-contract-drift-detector 0.1.0-beta.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/docs/implementation-plan.md +44 -0
- package/docs/requirements.md +60 -0
- package/docs/v0.1-engineering-backlog.md +118 -0
- package/eslint.config.mjs +18 -0
- package/examples/README.md +21 -0
- package/package.json +30 -0
- package/src/adapters/backend-parser.ts +18 -0
- package/src/adapters/openapi-parser.ts +39 -0
- package/src/cli/index.ts +82 -0
- package/src/core/drift-engine.ts +45 -0
- package/src/core/run-core.ts +78 -0
- package/src/index.ts +5 -0
- package/src/types.ts +87 -0
- package/tests/fixtures/README.md +3 -0
- package/tests/fixtures/backend.yaml +10 -0
- package/tests/fixtures/openapi.yaml +46 -0
- package/tests/integration/README.md +3 -0
- package/tests/unit/scaffold.test.ts +58 -0
- package/tsconfig.json +6 -0
- package/vitest.config.ts +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# API Contract Drift Detector
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Detect drift between OpenAPI specs and backend implementations before it reaches production.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
~~~bash
|
|
10
|
+
npm install @public-sdk/api-contract-drift-detector
|
|
11
|
+
~~~
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
~~~bash
|
|
16
|
+
npx drift-check --help
|
|
17
|
+
~~~
|
|
18
|
+
|
|
19
|
+
## Integration Example
|
|
20
|
+
|
|
21
|
+
1. Add this SDK to your CI workflow or local tooling script.
|
|
22
|
+
2. Run the command against your project inputs.
|
|
23
|
+
3. Fail the pipeline on non-zero exit code to enforce quality gates.
|
|
24
|
+
|
|
25
|
+
~~~bash
|
|
26
|
+
npx drift-check --openapi ./examples/openapi.yaml --backend ./examples/backend.yaml --json
|
|
27
|
+
~~~
|
|
28
|
+
|
|
29
|
+
## Typical Output
|
|
30
|
+
|
|
31
|
+
~~~json
|
|
32
|
+
{
|
|
33
|
+
"command": "drift-check",
|
|
34
|
+
"summary": "2 warnings detected",
|
|
35
|
+
"stats": {
|
|
36
|
+
"totalEndpoints": 18,
|
|
37
|
+
"errors": 0,
|
|
38
|
+
"warnings": 2
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
~~~
|
|
42
|
+
|
|
43
|
+
## Local Development
|
|
44
|
+
|
|
45
|
+
~~~bash
|
|
46
|
+
npm ci
|
|
47
|
+
npm run build
|
|
48
|
+
npm test
|
|
49
|
+
~~~
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Implementation Plan - API Contract Drift Detector
|
|
2
|
+
|
|
3
|
+
## Phase 0: Discovery and Scope (Week 1)
|
|
4
|
+
|
|
5
|
+
- Validate user problem with 5-10 real-world examples.
|
|
6
|
+
- Define MVP boundary and out-of-scope items.
|
|
7
|
+
- Produce architecture sketch and data flow.
|
|
8
|
+
|
|
9
|
+
## Phase 1: Foundation (Week 1-2)
|
|
10
|
+
|
|
11
|
+
- Initialize project structure and coding standards.
|
|
12
|
+
- Implement configuration loader and CLI parsing.
|
|
13
|
+
- Add logging, error handling, and output formatter.
|
|
14
|
+
|
|
15
|
+
## Phase 2: Core Engine (Week 2-4)
|
|
16
|
+
|
|
17
|
+
- Implement the primary analysis/generation capability.
|
|
18
|
+
- Add rule engine or model orchestration layer.
|
|
19
|
+
- Build deterministic fixtures for regression safety.
|
|
20
|
+
|
|
21
|
+
## Phase 3: Integrations (Week 4-5)
|
|
22
|
+
|
|
23
|
+
- Add GitHub Action workflow support.
|
|
24
|
+
- Add JSON/SARIF output when relevant.
|
|
25
|
+
- Add optional webhook or notification adapters.
|
|
26
|
+
|
|
27
|
+
## Phase 4: Quality and Hardening (Week 5-6)
|
|
28
|
+
|
|
29
|
+
- Add unit, integration, and snapshot tests.
|
|
30
|
+
- Benchmark performance on representative repositories.
|
|
31
|
+
- Improve diagnostics and false-positive handling.
|
|
32
|
+
|
|
33
|
+
## Phase 5: Documentation and Release (Week 6)
|
|
34
|
+
|
|
35
|
+
- Publish getting-started guide and examples.
|
|
36
|
+
- Add migration/upgrade notes for future versions.
|
|
37
|
+
- Release v0.1.0 and collect feedback.
|
|
38
|
+
|
|
39
|
+
## Deliverables
|
|
40
|
+
|
|
41
|
+
- Runnable CLI/SDK/Action artifact.
|
|
42
|
+
- CI-ready examples and sample configs.
|
|
43
|
+
- Stable contract for outputs and exit codes.
|
|
44
|
+
- Public roadmap for v0.2+ features.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Requirements - Api Contract Drift Detector
|
|
2
|
+
|
|
3
|
+
## 1. Product Requirements
|
|
4
|
+
|
|
5
|
+
### 1.1 Problem Statement
|
|
6
|
+
|
|
7
|
+
Define the core pain point this project solves and how teams currently suffer due to manual, error-prone, or fragmented workflows.
|
|
8
|
+
|
|
9
|
+
### 1.2 Objectives
|
|
10
|
+
|
|
11
|
+
- Reduce engineering effort for the target workflow.
|
|
12
|
+
- Improve reliability and consistency of delivery.
|
|
13
|
+
- Provide CI-friendly and developer-friendly outputs.
|
|
14
|
+
|
|
15
|
+
### 1.3 Success Metrics
|
|
16
|
+
|
|
17
|
+
- Adoption: stars, forks, downloads, active users.
|
|
18
|
+
- Efficiency: measurable time saved for key workflows.
|
|
19
|
+
- Quality: reduction in defects or drift incidents.
|
|
20
|
+
|
|
21
|
+
### 1.4 Target Audience
|
|
22
|
+
|
|
23
|
+
- Small to medium engineering teams
|
|
24
|
+
- API-first companies
|
|
25
|
+
- Enterprise engineering organizations
|
|
26
|
+
|
|
27
|
+
## 2. Functional Requirements
|
|
28
|
+
|
|
29
|
+
- Provide a primary command/API for the core workflow.
|
|
30
|
+
- Support configuration via file and CLI flags.
|
|
31
|
+
- Emit human-readable and machine-readable results (e.g., JSON).
|
|
32
|
+
- Return deterministic exit codes for CI integration.
|
|
33
|
+
- Provide a dry-run mode where applicable.
|
|
34
|
+
|
|
35
|
+
## 3. Non-Functional Requirements
|
|
36
|
+
|
|
37
|
+
- Performance: complete typical runs within acceptable CI time budgets.
|
|
38
|
+
- Reliability: deterministic behavior for the same inputs.
|
|
39
|
+
- Security: avoid leaking sensitive data in logs and outputs.
|
|
40
|
+
- Extensibility: plugin/hooks design for custom rules or providers.
|
|
41
|
+
- Usability: concise, actionable messaging and docs.
|
|
42
|
+
|
|
43
|
+
## 4. Integrations
|
|
44
|
+
|
|
45
|
+
- GitHub Actions support.
|
|
46
|
+
- Optional integration with issue trackers and chat notifications.
|
|
47
|
+
- Optional telemetry (opt-in) for usage insights.
|
|
48
|
+
|
|
49
|
+
## 5. Constraints and Risks
|
|
50
|
+
|
|
51
|
+
- Fast-changing frameworks and ecosystem compatibility.
|
|
52
|
+
- Risk of false positives in AI-assisted analysis.
|
|
53
|
+
- Need for clear explainability in automated decisions.
|
|
54
|
+
|
|
55
|
+
## 6. Acceptance Criteria
|
|
56
|
+
|
|
57
|
+
- Core workflow works end-to-end with sample fixtures.
|
|
58
|
+
- CI integration example passes in a reference repository.
|
|
59
|
+
- Documentation includes quickstart, config, and troubleshooting.
|
|
60
|
+
- Baseline tests cover critical paths and edge cases.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# v0.1 Engineering Backlog - API Contract Drift Detector
|
|
2
|
+
|
|
3
|
+
## 1. Release Objective
|
|
4
|
+
|
|
5
|
+
- Product Type: CLI
|
|
6
|
+
- Primary Command/API: drift-check
|
|
7
|
+
- v0.1 Goal: Detect drift between OpenAPI, backend routes, generated SDKs, and docs examples.
|
|
8
|
+
|
|
9
|
+
## 2. v0.1 Scope
|
|
10
|
+
|
|
11
|
+
### In Scope
|
|
12
|
+
|
|
13
|
+
- Implement one reliable end-to-end workflow for the primary command/API.
|
|
14
|
+
- Support local CLI usage and CI-ready machine-readable output.
|
|
15
|
+
- Provide deterministic fixtures and baseline test coverage.
|
|
16
|
+
|
|
17
|
+
### Out of Scope
|
|
18
|
+
|
|
19
|
+
- Multi-tenant cloud control planes and hosted dashboards.
|
|
20
|
+
- Broad ecosystem plugin marketplace.
|
|
21
|
+
- Advanced enterprise SSO/billing lifecycle automation.
|
|
22
|
+
|
|
23
|
+
## 3. CLI/API Contract
|
|
24
|
+
|
|
25
|
+
### Inputs
|
|
26
|
+
|
|
27
|
+
openapi.yaml, backend route metadata, sdk surface metadata, markdown docs
|
|
28
|
+
|
|
29
|
+
### Outputs
|
|
30
|
+
|
|
31
|
+
terminal report, json report, CI exit code
|
|
32
|
+
|
|
33
|
+
### Exit Code Policy
|
|
34
|
+
|
|
35
|
+
- 0: successful run with no blocking findings.
|
|
36
|
+
- 1: blocking findings, failed policies, or generation validation errors.
|
|
37
|
+
- 2: invalid arguments/configuration.
|
|
38
|
+
- 3: unexpected internal runtime error.
|
|
39
|
+
|
|
40
|
+
## 4. Proposed Folder Structure
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
project/
|
|
44
|
+
src/
|
|
45
|
+
cli/
|
|
46
|
+
core/
|
|
47
|
+
adapters/
|
|
48
|
+
formatters/
|
|
49
|
+
tests/
|
|
50
|
+
fixtures/
|
|
51
|
+
integration/
|
|
52
|
+
unit/
|
|
53
|
+
examples/
|
|
54
|
+
docs/
|
|
55
|
+
requirements.md
|
|
56
|
+
implementation-plan.md
|
|
57
|
+
v0.1-engineering-backlog.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 5. Epics and Tasks
|
|
61
|
+
|
|
62
|
+
### Epic 1 - Foundations
|
|
63
|
+
|
|
64
|
+
- Setup language/runtime, linting, formatting, and test harness.
|
|
65
|
+
- Implement config loader and schema validation.
|
|
66
|
+
- Implement logging and output formatter (human + JSON).
|
|
67
|
+
- Define typed domain models for inputs and findings.
|
|
68
|
+
|
|
69
|
+
### Epic 2 - Core Engine
|
|
70
|
+
|
|
71
|
+
- Implement parser/collector pipeline for source inputs.
|
|
72
|
+
- Implement rule/evaluation engine with deterministic ordering.
|
|
73
|
+
- Implement result normalization and deduplication.
|
|
74
|
+
- Add clear diagnostic messages with file/entity pointers.
|
|
75
|
+
|
|
76
|
+
### Epic 3 - Integrations
|
|
77
|
+
|
|
78
|
+
- Add CI usage examples and default command presets.
|
|
79
|
+
- Add GitHub workflow/action integration sample.
|
|
80
|
+
- Add JSON artifact emission for downstream automation.
|
|
81
|
+
|
|
82
|
+
### Epic 4 - Quality and Release
|
|
83
|
+
|
|
84
|
+
- Build fixture-based integration tests for happy/edge/error paths.
|
|
85
|
+
- Add performance baseline test for representative input size.
|
|
86
|
+
- Add release notes template and semantic version tagging process.
|
|
87
|
+
|
|
88
|
+
## 6. Acceptance Tests
|
|
89
|
+
|
|
90
|
+
- Core Acceptance Test: Given mismatched OpenAPI and SDK fixtures, command reports missing endpoints and schema mismatches with non-zero exit.
|
|
91
|
+
- Invalid Config Test: malformed config returns exit code 2 and actionable error.
|
|
92
|
+
- Output Contract Test: json output schema remains stable across fixture runs.
|
|
93
|
+
- Regression Test: known fixture continues to produce expected findings/generation.
|
|
94
|
+
|
|
95
|
+
## 7. Two-Week Execution Plan
|
|
96
|
+
|
|
97
|
+
### Week 1
|
|
98
|
+
|
|
99
|
+
- Day 1: Finalize v0.1 interface and fixture list.
|
|
100
|
+
- Day 2-3: Implement foundations and parser/collector layer.
|
|
101
|
+
- Day 4-5: Implement first end-to-end core workflow.
|
|
102
|
+
|
|
103
|
+
### Week 2
|
|
104
|
+
|
|
105
|
+
- Day 6-7: Add remaining critical rules/features for MVP.
|
|
106
|
+
- Day 8: Add integration examples and CI contract checks.
|
|
107
|
+
- Day 9: Harden tests, snapshots, and error handling.
|
|
108
|
+
- Day 10: Ship v0.1.0 release candidate with docs.
|
|
109
|
+
|
|
110
|
+
## 8. Definition of Done for v0.1
|
|
111
|
+
|
|
112
|
+
- Command/API is usable in local and CI contexts.
|
|
113
|
+
- Output contract is documented and tested.
|
|
114
|
+
- At least 1 reference example is included and reproducible.
|
|
115
|
+
- Known high-priority edge cases are covered by tests.
|
|
116
|
+
- Release notes include limitations and v0.2 roadmap themes.
|
|
117
|
+
|
|
118
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import tseslint from "typescript-eslint";
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
js.configs.recommended,
|
|
6
|
+
...tseslint.configs.recommended,
|
|
7
|
+
{
|
|
8
|
+
files: ["**/*.ts"],
|
|
9
|
+
languageOptions: {
|
|
10
|
+
parserOptions: {
|
|
11
|
+
project: false
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
rules: {
|
|
15
|
+
"no-console": "off"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# API Contract Drift Detector Examples
|
|
2
|
+
|
|
3
|
+
## CLI Example
|
|
4
|
+
|
|
5
|
+
Run this command from your project root:
|
|
6
|
+
|
|
7
|
+
~~~bash
|
|
8
|
+
npx drift-check --openapi ./examples/openapi.yaml --backend ./examples/backend.yaml --json
|
|
9
|
+
~~~
|
|
10
|
+
|
|
11
|
+
## CI Example (GitHub Actions)
|
|
12
|
+
|
|
13
|
+
~~~yaml
|
|
14
|
+
- name: Run API Contract Drift Detector
|
|
15
|
+
run: npx drift-check --openapi ./examples/openapi.yaml --backend ./examples/backend.yaml --json
|
|
16
|
+
~~~
|
|
17
|
+
|
|
18
|
+
## Notes
|
|
19
|
+
|
|
20
|
+
- Keep example inputs small and deterministic.
|
|
21
|
+
- Commit expected outputs when you want regression visibility in pull requests.
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@amitgaikwad37/api-contract-drift-detector",
|
|
3
|
+
"version": "0.1.0-beta.18",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Api Contract Drift Detector starter implementation scaffold",
|
|
6
|
+
"bin": {
|
|
7
|
+
"drift-check": "dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p tsconfig.json",
|
|
11
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
12
|
+
"start": "node dist/cli/index.js",
|
|
13
|
+
"dev": "tsx src/cli/index.ts",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"lint": "eslint ."
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@eslint/js": "^9.16.0",
|
|
20
|
+
"@types/node": "^22.10.1",
|
|
21
|
+
"eslint": "^9.16.0",
|
|
22
|
+
"tsx": "^4.19.2",
|
|
23
|
+
"typescript": "^5.7.2",
|
|
24
|
+
"typescript-eslint": "^8.18.0",
|
|
25
|
+
"vitest": "^2.1.8"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"yaml": "^2.4.1"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BackendRoute } from "../types.js";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
|
|
5
|
+
export function parseBackendRoutes(filePath: string): BackendRoute[] {
|
|
6
|
+
const content = readFileSync(filePath, "utf-8");
|
|
7
|
+
const spec = parse(content) as any;
|
|
8
|
+
|
|
9
|
+
if (!Array.isArray(spec.routes)) {
|
|
10
|
+
throw new Error("Invalid backend metadata: routes must be an array");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return spec.routes.map((route: any) => ({
|
|
14
|
+
path: route.path,
|
|
15
|
+
method: (route.method || "GET").toUpperCase(),
|
|
16
|
+
handler: route.handler || "unknown"
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { OpenAPISpec, Endpoint } from "../types.js";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
|
|
5
|
+
export function parseOpenAPI(filePath: string): OpenAPISpec {
|
|
6
|
+
const content = readFileSync(filePath, "utf-8");
|
|
7
|
+
const spec = parse(content) as any;
|
|
8
|
+
|
|
9
|
+
if (!spec.openapi && !spec.swagger) {
|
|
10
|
+
throw new Error("Invalid OpenAPI spec: missing openapi or swagger field");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const endpoints: Endpoint[] = [];
|
|
14
|
+
const paths = spec.paths || {};
|
|
15
|
+
|
|
16
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
17
|
+
const item = pathItem as any;
|
|
18
|
+
for (const method of ["get", "post", "put", "delete", "patch", "head"]) {
|
|
19
|
+
if (method in item) {
|
|
20
|
+
const operation = item[method];
|
|
21
|
+
endpoints.push({
|
|
22
|
+
path,
|
|
23
|
+
method: method.toUpperCase(),
|
|
24
|
+
summary: operation.summary,
|
|
25
|
+
parameters: operation.parameters || [],
|
|
26
|
+
requestBody: operation.requestBody,
|
|
27
|
+
responses: operation.responses || {}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
version: spec.openapi || spec.swagger,
|
|
35
|
+
title: spec.info?.title || "Unknown",
|
|
36
|
+
endpoints,
|
|
37
|
+
schemas: spec.components?.schemas || spec.definitions || {}
|
|
38
|
+
};
|
|
39
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { runCore } from "../core/run-core.js";
|
|
2
|
+
import type { RunOptions } from "../types.js";
|
|
3
|
+
|
|
4
|
+
function printHelp(): void {
|
|
5
|
+
console.log("drift-check - Detect drift between OpenAPI, backend, SDK, and documentation");
|
|
6
|
+
console.log("");
|
|
7
|
+
console.log("Usage:");
|
|
8
|
+
console.log(" drift-check --openapi <path> --backend <path> [--json] [--help]");
|
|
9
|
+
console.log("");
|
|
10
|
+
console.log("Options:");
|
|
11
|
+
console.log(" --openapi <path> Path to OpenAPI YAML file (required)");
|
|
12
|
+
console.log(" --backend <path> Path to backend metadata YAML file (required)");
|
|
13
|
+
console.log(" --json Print JSON output");
|
|
14
|
+
console.log(" --help Show this help message");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseArgs(args: string[]): RunOptions | null {
|
|
18
|
+
const opts: RunOptions = { json: false };
|
|
19
|
+
let i = 0;
|
|
20
|
+
|
|
21
|
+
while (i < args.length) {
|
|
22
|
+
const arg = args[i];
|
|
23
|
+
if (arg === "--json") {
|
|
24
|
+
opts.json = true;
|
|
25
|
+
i++;
|
|
26
|
+
} else if (arg === "--openapi" && i + 1 < args.length) {
|
|
27
|
+
opts.openapi = args[i + 1];
|
|
28
|
+
i += 2;
|
|
29
|
+
} else if (arg === "--backend" && i + 1 < args.length) {
|
|
30
|
+
opts.backend = args[i + 1];
|
|
31
|
+
i += 2;
|
|
32
|
+
} else {
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!opts.openapi || !opts.backend) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return opts;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function main(): void {
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
|
|
47
|
+
if (args.length === 0 || args.includes("--help")) {
|
|
48
|
+
printHelp();
|
|
49
|
+
process.exit(args.length === 0 ? 2 : 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const opts = parseArgs(args);
|
|
53
|
+
if (!opts) {
|
|
54
|
+
console.error("Error: --openapi and --backend are required");
|
|
55
|
+
printHelp();
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result = runCore(opts);
|
|
60
|
+
|
|
61
|
+
if (opts.json) {
|
|
62
|
+
console.log(JSON.stringify(result, null, 2));
|
|
63
|
+
} else {
|
|
64
|
+
console.log(`[${result.command}] ${result.summary}`);
|
|
65
|
+
console.log(`\nStats: ${result.stats.totalEndpoints} endpoints, ${result.stats.errors} errors, ${result.stats.warnings} warnings\n`);
|
|
66
|
+
|
|
67
|
+
if (result.findings.length > 0) {
|
|
68
|
+
for (const finding of result.findings) {
|
|
69
|
+
const icon = finding.severity === "error" ? "❌" : "⚠️";
|
|
70
|
+
console.log(`${icon} [${finding.type}] ${finding.message}`);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
console.log("✅ No drift detected!");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const exitCode = result.stats.errors > 0 ? 1 : 0;
|
|
78
|
+
process.exit(exitCode);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main();
|
|
82
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Endpoint, BackendRoute, DriftFinding } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export function compareEndpoints(openapi: Endpoint[], backend: BackendRoute[]): DriftFinding[] {
|
|
4
|
+
const findings: DriftFinding[] = [];
|
|
5
|
+
const backendMap = new Map<string, BackendRoute>();
|
|
6
|
+
|
|
7
|
+
// Build backend route map for fast lookup
|
|
8
|
+
for (const route of backend) {
|
|
9
|
+
backendMap.set(`${route.method}:${route.path}`, route);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Check for missing or mismatched endpoints in backend
|
|
13
|
+
for (const endpoint of openapi) {
|
|
14
|
+
const key = `${endpoint.method}:${endpoint.path}`;
|
|
15
|
+
const backendRoute = backendMap.get(key);
|
|
16
|
+
|
|
17
|
+
if (!backendRoute) {
|
|
18
|
+
findings.push({
|
|
19
|
+
type: "missing_in_backend",
|
|
20
|
+
severity: "error",
|
|
21
|
+
endpoint: endpoint.path,
|
|
22
|
+
method: endpoint.method,
|
|
23
|
+
message: `OpenAPI endpoint not found in backend: ${endpoint.method} ${endpoint.path}`,
|
|
24
|
+
source: "backend"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check for extra endpoints in backend (not in OpenAPI)
|
|
30
|
+
for (const [key, route] of backendMap.entries()) {
|
|
31
|
+
const found = openapi.some(e => `${e.method}:${e.path}` === key);
|
|
32
|
+
if (!found) {
|
|
33
|
+
findings.push({
|
|
34
|
+
type: "extra_in_sdk",
|
|
35
|
+
severity: "warning",
|
|
36
|
+
endpoint: route.path,
|
|
37
|
+
method: route.method,
|
|
38
|
+
message: `Backend route not documented in OpenAPI: ${route.method} ${route.path}`,
|
|
39
|
+
source: "backend"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return findings;
|
|
45
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { RunOptions, RunResult, DriftFinding, OpenAPISpec, BackendRoute } from "../types.js";
|
|
2
|
+
import { parseOpenAPI } from "../adapters/openapi-parser.js";
|
|
3
|
+
import { parseBackendRoutes } from "../adapters/backend-parser.js";
|
|
4
|
+
import { compareEndpoints } from "../core/drift-engine.js";
|
|
5
|
+
|
|
6
|
+
export function runCore(options: RunOptions): RunResult {
|
|
7
|
+
try {
|
|
8
|
+
let findings: DriftFinding[] = [];
|
|
9
|
+
let totalEndpoints = 0;
|
|
10
|
+
|
|
11
|
+
// Parse inputs
|
|
12
|
+
let openapi: OpenAPISpec | null = null;
|
|
13
|
+
let backend: BackendRoute[] = [];
|
|
14
|
+
|
|
15
|
+
if (options.openapi) {
|
|
16
|
+
try {
|
|
17
|
+
openapi = parseOpenAPI(options.openapi);
|
|
18
|
+
totalEndpoints = openapi.endpoints.length;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
findings.push({
|
|
21
|
+
type: "missing_in_backend",
|
|
22
|
+
severity: "error",
|
|
23
|
+
endpoint: "(global)",
|
|
24
|
+
message: `Failed to parse OpenAPI: ${e instanceof Error ? e.message : String(e)}`,
|
|
25
|
+
source: "openapi"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (options.backend) {
|
|
31
|
+
try {
|
|
32
|
+
backend = parseBackendRoutes(options.backend);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
findings.push({
|
|
35
|
+
type: "missing_in_backend",
|
|
36
|
+
severity: "error",
|
|
37
|
+
endpoint: "(global)",
|
|
38
|
+
message: `Failed to parse backend metadata: ${e instanceof Error ? e.message : String(e)}`,
|
|
39
|
+
source: "backend"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Run drift detection
|
|
45
|
+
if (openapi && backend.length > 0) {
|
|
46
|
+
const driftResults = compareEndpoints(openapi.endpoints, backend);
|
|
47
|
+
findings.push(...driftResults);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const stats = {
|
|
51
|
+
totalEndpoints,
|
|
52
|
+
drifted: findings.filter(f => f.severity === "error").length,
|
|
53
|
+
errors: findings.filter(f => f.severity === "error").length,
|
|
54
|
+
warnings: findings.filter(f => f.severity === "warning").length
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const hasErrors = stats.errors > 0;
|
|
58
|
+
const summary = hasErrors
|
|
59
|
+
? `Found ${stats.drifted} drift issues across ${totalEndpoints} endpoints.`
|
|
60
|
+
: `All ${totalEndpoints} endpoints match specifications.`;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
project: "api-contract-drift-detector",
|
|
64
|
+
command: "drift-check",
|
|
65
|
+
summary,
|
|
66
|
+
findings,
|
|
67
|
+
stats
|
|
68
|
+
};
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return {
|
|
71
|
+
project: "api-contract-drift-detector",
|
|
72
|
+
command: "drift-check",
|
|
73
|
+
summary: `Internal error: ${e instanceof Error ? e.message : String(e)}`,
|
|
74
|
+
findings: [],
|
|
75
|
+
stats: { totalEndpoints: 0, drifted: 0, errors: 1, warnings: 0 }
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { runCore } from "./core/run-core.js";
|
|
2
|
+
export type { RunOptions, RunResult, DriftFinding, Endpoint } from "./types.js";
|
|
3
|
+
export { parseOpenAPI } from "./adapters/openapi-parser.js";
|
|
4
|
+
export { parseBackendRoutes } from "./adapters/backend-parser.js";
|
|
5
|
+
export { compareEndpoints } from "./core/drift-engine.js";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// ============ INPUT MODELS ============
|
|
2
|
+
|
|
3
|
+
export type Endpoint = {
|
|
4
|
+
path: string;
|
|
5
|
+
method: string;
|
|
6
|
+
summary?: string;
|
|
7
|
+
parameters?: Parameter[];
|
|
8
|
+
requestBody?: RequestBody;
|
|
9
|
+
responses: Record<string, Response>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type Parameter = {
|
|
13
|
+
name: string;
|
|
14
|
+
in: "query" | "path" | "header" | "cookie";
|
|
15
|
+
required: boolean;
|
|
16
|
+
schema: any;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type RequestBody = {
|
|
20
|
+
required: boolean;
|
|
21
|
+
content: Record<string, { schema: any }>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type Response = {
|
|
25
|
+
description: string;
|
|
26
|
+
content?: Record<string, { schema: any }>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type OpenAPISpec = {
|
|
30
|
+
version: string;
|
|
31
|
+
title: string;
|
|
32
|
+
endpoints: Endpoint[];
|
|
33
|
+
schemas: Record<string, any>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type BackendRoute = {
|
|
37
|
+
path: string;
|
|
38
|
+
method: string;
|
|
39
|
+
handler: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type SDKMethod = {
|
|
43
|
+
name: string;
|
|
44
|
+
endpoint: string;
|
|
45
|
+
signature: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type DocExample = {
|
|
49
|
+
endpoint: string;
|
|
50
|
+
method: string;
|
|
51
|
+
example: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ============ FINDINGS ============
|
|
55
|
+
|
|
56
|
+
export type DriftFinding = {
|
|
57
|
+
type: "missing_in_sdk" | "missing_in_backend" | "schema_mismatch" | "doc_stale" | "extra_in_sdk";
|
|
58
|
+
severity: "error" | "warning";
|
|
59
|
+
endpoint: string;
|
|
60
|
+
method?: string;
|
|
61
|
+
message: string;
|
|
62
|
+
details?: string;
|
|
63
|
+
source: "openapi" | "backend" | "sdk" | "docs";
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ============ OUTPUT MODELS ============
|
|
67
|
+
|
|
68
|
+
export type RunResult = {
|
|
69
|
+
project: string;
|
|
70
|
+
command: string;
|
|
71
|
+
summary: string;
|
|
72
|
+
findings: DriftFinding[];
|
|
73
|
+
stats: {
|
|
74
|
+
totalEndpoints: number;
|
|
75
|
+
drifted: number;
|
|
76
|
+
errors: number;
|
|
77
|
+
warnings: number;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type RunOptions = {
|
|
82
|
+
json: boolean;
|
|
83
|
+
openapi?: string;
|
|
84
|
+
backend?: string;
|
|
85
|
+
sdk?: string;
|
|
86
|
+
docs?: string;
|
|
87
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
openapi: 3.0.0
|
|
2
|
+
info:
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
title: Pet Store API
|
|
5
|
+
paths:
|
|
6
|
+
/pets:
|
|
7
|
+
get:
|
|
8
|
+
summary: List all pets
|
|
9
|
+
responses:
|
|
10
|
+
'200':
|
|
11
|
+
description: A list of pets
|
|
12
|
+
post:
|
|
13
|
+
summary: Create a pet
|
|
14
|
+
requestBody:
|
|
15
|
+
required: true
|
|
16
|
+
content:
|
|
17
|
+
application/json:
|
|
18
|
+
schema:
|
|
19
|
+
type: object
|
|
20
|
+
properties:
|
|
21
|
+
name:
|
|
22
|
+
type: string
|
|
23
|
+
responses:
|
|
24
|
+
'201':
|
|
25
|
+
description: Pet created
|
|
26
|
+
/pets/{id}:
|
|
27
|
+
get:
|
|
28
|
+
summary: Get a pet by ID
|
|
29
|
+
parameters:
|
|
30
|
+
- name: id
|
|
31
|
+
in: path
|
|
32
|
+
required: true
|
|
33
|
+
schema:
|
|
34
|
+
type: string
|
|
35
|
+
responses:
|
|
36
|
+
'200':
|
|
37
|
+
description: A pet
|
|
38
|
+
components:
|
|
39
|
+
schemas:
|
|
40
|
+
Pet:
|
|
41
|
+
type: object
|
|
42
|
+
properties:
|
|
43
|
+
id:
|
|
44
|
+
type: string
|
|
45
|
+
name:
|
|
46
|
+
type: string
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { runCore } from "../../src/core/run-core.js";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
7
|
+
const fixturesDir = resolve(__dirname, "../fixtures");
|
|
8
|
+
|
|
9
|
+
describe("drift-check core", () => {
|
|
10
|
+
it("returns a basic scaffold result", () => {
|
|
11
|
+
// Verify SDK returns correct structure with command, project name, and stats
|
|
12
|
+
const result = runCore({ json: false });
|
|
13
|
+
expect(result.command).toBe("drift-check");
|
|
14
|
+
expect(result.stats).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("detects drift between OpenAPI and backend", () => {
|
|
18
|
+
// Verify SDK identifies discrepancies between API spec and actual backend implementation
|
|
19
|
+
// Fixture has 3 defined endpoints; should detect missing/extra implementations
|
|
20
|
+
const result = runCore({
|
|
21
|
+
json: false,
|
|
22
|
+
openapi: `${fixturesDir}/openapi.yaml`,
|
|
23
|
+
backend: `${fixturesDir}/backend.yaml`
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result.stats.totalEndpoints).toBe(3); // GET /pets, POST /pets, GET /pets/{id}
|
|
27
|
+
expect(result.findings.length).toBeGreaterThan(0);
|
|
28
|
+
expect(result.stats.errors).toBeGreaterThan(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("includes missing endpoint in findings", () => {
|
|
32
|
+
// Verify SDK detects POST /pets endpoint in spec but missing in backend implementation
|
|
33
|
+
const result = runCore({
|
|
34
|
+
json: false,
|
|
35
|
+
openapi: `${fixturesDir}/openapi.yaml`,
|
|
36
|
+
backend: `${fixturesDir}/backend.yaml`
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const missingPost = result.findings.some(
|
|
40
|
+
f => f.type === "missing_in_backend" && f.method === "POST"
|
|
41
|
+
);
|
|
42
|
+
expect(missingPost).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("includes extra endpoint in findings", () => {
|
|
46
|
+
// Verify SDK detects /users endpoint in backend that's not in OpenAPI spec (potential security risk)
|
|
47
|
+
const result = runCore({
|
|
48
|
+
json: false,
|
|
49
|
+
openapi: `${fixturesDir}/openapi.yaml`,
|
|
50
|
+
backend: `${fixturesDir}/backend.yaml`
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const extraUsers = result.findings.some(
|
|
54
|
+
f => f.endpoint === "/users" && f.severity === "warning"
|
|
55
|
+
);
|
|
56
|
+
expect(extraUsers).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/tsconfig.json
ADDED