contract-driven-delivery 2.1.3 → 2.2.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.
- package/CHANGELOG.md +219 -0
- package/README.md +124 -1
- package/assets/CLAUDE.template.md +13 -0
- package/assets/agents/backend-engineer.md +3 -1
- package/assets/agents/frontend-engineer.md +3 -2
- package/assets/cdd/conformance.json +16 -0
- package/assets/cdd/tier-policy.json +35 -0
- package/assets/contracts/api/api-contract.md +26 -0
- package/assets/hooks/pre-tool-use-graph-first.sh +65 -0
- package/assets/skills/contract-driven-delivery/scripts/validate_api_conformance.py +671 -0
- package/assets/skills/contract-driven-delivery/scripts/validate_api_semantic.py +8 -1
- package/assets/skills/contract-driven-delivery/scripts/validate_contract_versions.py +4 -0
- package/dist/cli/index.js +2118 -491
- package/docs/adr/0001-contract-to-openapi-export.md +142 -0
- package/docs/adr/0002-schema-carrying-contract-format.md +277 -0
- package/docs/adr/0003-code-intelligence-indexing-strategy.md +110 -0
- package/docs/api-conformance.md +145 -0
- package/docs/openapi-export.md +157 -0
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# API Conformance: catching frontend/backend drift mechanically
|
|
2
|
+
|
|
3
|
+
## Why this exists
|
|
4
|
+
|
|
5
|
+
cdd-kit calls `contracts/api/api-contract.md` the single source of truth for the
|
|
6
|
+
API. But the other validators only check that the contract *document* is well
|
|
7
|
+
formed (`validate_api_semantic.py` validates the endpoint table's columns). They
|
|
8
|
+
never look at code. So the frontend and backend can both drift away from the
|
|
9
|
+
contract — the frontend can call `/api/v2/orders` while the backend serves
|
|
10
|
+
`/api/orders` and the contract documents neither — and every gate stays green.
|
|
11
|
+
|
|
12
|
+
In a workflow where **no human reviews the contract by hand**, prose review is
|
|
13
|
+
worthless and the markdown is only worth what a machine can enforce against real
|
|
14
|
+
code. `validate_api_conformance.py` is that machine check: it parses the actual
|
|
15
|
+
backend routes and frontend call sites and diffs them against the contract.
|
|
16
|
+
|
|
17
|
+
## What it checks
|
|
18
|
+
|
|
19
|
+
| Check | Meaning | Default severity |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `backendRouteNotInContract` | a route is declared in backend code but not in the contract | warning |
|
|
22
|
+
| `contractEndpointNotImplemented` | a contract endpoint has no backend route in scanned source | warning |
|
|
23
|
+
| `frontendCallNotInContract` | the frontend calls a path/method that is not in the contract | error |
|
|
24
|
+
|
|
25
|
+
Paths are normalized so `/users/:id`, `/users/{id}`, and `/users/${id}` all
|
|
26
|
+
compare equal. Methods are compared too; `fetch()` and Django/Spring-style
|
|
27
|
+
declarations that don't expose a verb to the regex are treated as method-agnostic.
|
|
28
|
+
|
|
29
|
+
It is **heuristic** (regex, stack-agnostic) by design — a generic kit cannot ship
|
|
30
|
+
a parser for every framework. It recognizes:
|
|
31
|
+
|
|
32
|
+
- **Backend**: Express/Koa/Fastify (`app/router.get('/x')`), NestJS
|
|
33
|
+
(`@Controller` prefix + `@Get(':id')`), Flask/FastAPI/Django, Spring
|
|
34
|
+
(`@GetMapping`, and `@RequestMapping(..., method=...)` with the method parsed),
|
|
35
|
+
Go (chi/gin/echo/mux + `HandleFunc`), and Laravel (`Route::get` and
|
|
36
|
+
`Route::match([...])`).
|
|
37
|
+
- **Flask Blueprint / FastAPI APIRouter prefixes** are resolved across files: a
|
|
38
|
+
pre-pass maps each router variable to its prefix — from the constructor kwarg
|
|
39
|
+
(`Blueprint(..., url_prefix="/admin")`, `APIRouter(prefix="/admin")`) and/or the
|
|
40
|
+
registration call (`register_blueprint(bp, url_prefix=...)`,
|
|
41
|
+
`include_router(router, prefix=...)`) — and folds it into every route on that
|
|
42
|
+
router. The registration-site prefix wins over the constructor's.
|
|
43
|
+
- **Frontend**: `fetch` (method read from the options object; defaults to GET),
|
|
44
|
+
`axios`/`ky`/`$http`/`client`/`http`/`api.*` verb calls, the
|
|
45
|
+
`axios({ url, method })` config-object form, and `useFetch`/`useSWR`/`useQuery`.
|
|
46
|
+
|
|
47
|
+
Backend patterns are **gated by file extension** so a Python/Go/PHP pattern can
|
|
48
|
+
never match a JS/TS file (and vice versa). Treat it as a high-signal net, not a
|
|
49
|
+
proof.
|
|
50
|
+
|
|
51
|
+
### Known heuristic limits
|
|
52
|
+
|
|
53
|
+
- **Ruby/Rails is not supported.** Rails routing is a stateful `routes.rb draw`
|
|
54
|
+
DSL that a regex cannot parse honestly, so `.rb` is not in the default
|
|
55
|
+
`backendGlobsExt` and Rails routes are not claimed.
|
|
56
|
+
- **Mounted Express routers** (`app.use('/api', router)` + `router.get('/users')`)
|
|
57
|
+
record only `/users`; the validator does not resolve the mount prefix across
|
|
58
|
+
files. (Flask Blueprint and FastAPI APIRouter prefixes *are* resolved — see
|
|
59
|
+
above — but the Express `app.use` mount form is not.) If you use mounted
|
|
60
|
+
routers, either declare the unprefixed paths in the contract, add the mount
|
|
61
|
+
prefix in the route literal, or set `contractEndpointNotImplemented` to `off`.
|
|
62
|
+
- **Prefix resolution keys on the local variable name.** Constructor prefixes
|
|
63
|
+
(`Blueprint(url_prefix=...)`, `APIRouter(prefix=...)`) are scoped per file, so a
|
|
64
|
+
`router` reused across modules does not collide; registration prefixes
|
|
65
|
+
(`register_blueprint`/`include_router`) are matched across files, with Flask's
|
|
66
|
+
override and FastAPI's additive (`include_router` prefix + `APIRouter` prefix)
|
|
67
|
+
semantics each respected. What is **not** resolved: a router imported under an
|
|
68
|
+
alias (`from x import router as r`), or a name registered under conflicting
|
|
69
|
+
prefixes across files — the latter is detected and dropped (so the per-file
|
|
70
|
+
constructor prefix decides) rather than guessed.
|
|
71
|
+
- **Registrations resolved by a shared bare receiver name can cross modules.**
|
|
72
|
+
Because a registration is keyed by the variable name (`router`, `bp`), not the
|
|
73
|
+
module it was imported from, the following are not resolved — they all need
|
|
74
|
+
import tracking the regex heuristic deliberately does not attempt:
|
|
75
|
+
- **Module-qualified registration**: `include_router(users.router,
|
|
76
|
+
prefix="/api")` (router referenced through an imported module) is not matched.
|
|
77
|
+
- **Same name, conflicting prefixes**: two modules each `include_router(router,
|
|
78
|
+
prefix=...)` under different prefixes — the ambiguous registration is dropped,
|
|
79
|
+
so those routes are left unresolved (a warning) rather than mis-attributed.
|
|
80
|
+
- **One registration leaking onto a same-named local router**: if `users.py`
|
|
81
|
+
exports `router` mounted under `/api`, while `admin.py` has its own file-local
|
|
82
|
+
`router = APIRouter(prefix="/admin")` mounted without a prefix, the `/api`
|
|
83
|
+
registration is applied to `admin.py`'s routes too (scanned as `/api/admin/…`
|
|
84
|
+
though FastAPI serves `/admin/…`). To avoid this, give routers distinct names
|
|
85
|
+
or set `backendRouteNotInContract`/`contractEndpointNotImplemented` to
|
|
86
|
+
`warning` (the default) so it does not fail CI.
|
|
87
|
+
- **Dynamic routes** built from variables or registered via framework modules
|
|
88
|
+
(NestJS `RouterModule`, dynamic prefixes) are not detected.
|
|
89
|
+
|
|
90
|
+
Because of these residual blind spots, `backendRouteNotInContract` **defaults to
|
|
91
|
+
`warning`**: a route the scanner mislocates must not break CI on a contract that
|
|
92
|
+
is actually correct. Raise it to `error` (or set `"strict": true`) once your
|
|
93
|
+
project's routing shape is known to resolve cleanly.
|
|
94
|
+
|
|
95
|
+
## Enabling it
|
|
96
|
+
|
|
97
|
+
It is **off unless `.cdd/conformance.json` exists with `"enabled": true`**, so it
|
|
98
|
+
never breaks repos that ship the kit. `cdd-kit init` scaffolds a disabled config;
|
|
99
|
+
flip it on:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"enabled": true,
|
|
104
|
+
"apiPrefixes": ["/api"],
|
|
105
|
+
"sourceRoots": ["src", "app"],
|
|
106
|
+
"ignorePaths": ["/health", "/metrics"],
|
|
107
|
+
"checks": {
|
|
108
|
+
"backendRouteNotInContract": "warning",
|
|
109
|
+
"contractEndpointNotImplemented": "warning",
|
|
110
|
+
"frontendCallNotInContract": "error"
|
|
111
|
+
},
|
|
112
|
+
"strict": false
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- `apiPrefixes` — only frontend calls under these prefixes are checked, so
|
|
117
|
+
static-asset and third-party URLs don't produce noise. Leave empty to check all.
|
|
118
|
+
- `sourceRoots` — directories to scan. Empty means auto-detect common roots
|
|
119
|
+
(`src`, `app`, `server`, `frontend`, …) that exist.
|
|
120
|
+
- `ignorePaths` — contract/code paths to skip; a trailing `*` matches a prefix.
|
|
121
|
+
- `strict` — escalate every warning to an error.
|
|
122
|
+
- Per-check severity can be set to `"error"`, `"warning"`, or `"off"`.
|
|
123
|
+
|
|
124
|
+
## How it runs
|
|
125
|
+
|
|
126
|
+
It is chained under contract validation, so both of these pick it up:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
cdd-kit validate --contracts # runs it alongside the markdown validators
|
|
130
|
+
cdd-kit gate <change-id> # gate runs the contract validators, so drift blocks the gate
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`cdd-kit doctor` reports whether the net is armed (`enabled` / `present but
|
|
134
|
+
disabled` / `not configured`) without failing — turning it on is a project policy
|
|
135
|
+
decision, not a doctor error.
|
|
136
|
+
|
|
137
|
+
## Tuning false positives
|
|
138
|
+
|
|
139
|
+
Because it is regex-based, an internal helper that isn't really an HTTP route can
|
|
140
|
+
occasionally match. Options, in order of preference:
|
|
141
|
+
|
|
142
|
+
1. Add the genuine endpoint to the contract (usually the right fix).
|
|
143
|
+
2. Add the path to `ignorePaths`.
|
|
144
|
+
3. Narrow `sourceRoots` to where real routes/calls live.
|
|
145
|
+
4. Lower a specific check to `"warning"` or `"off"`.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# OpenAPI export
|
|
2
|
+
|
|
3
|
+
`cdd-kit openapi export` projects `contracts/api/api-contract.md` (the source of
|
|
4
|
+
truth) into a minimal **OpenAPI 3.1** skeleton for tooling. The markdown contract
|
|
5
|
+
stays authoritative; the OpenAPI document is a one-way, regenerable projection.
|
|
6
|
+
|
|
7
|
+
See `docs/adr/0001-contract-to-openapi-export.md` and
|
|
8
|
+
`docs/adr/0002-schema-carrying-contract-format.md` for the design rationale.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cdd-kit openapi export # JSON to stdout
|
|
14
|
+
cdd-kit openapi export --yaml # YAML to stdout
|
|
15
|
+
cdd-kit openapi export --out build/openapi.json # write to a file
|
|
16
|
+
cdd-kit openapi export --yaml --out openapi.yaml
|
|
17
|
+
cdd-kit openapi export --contract path/to/api-contract.md
|
|
18
|
+
cdd-kit openapi export --check --out build/openapi.json # sync gate
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## The sync gate: `--check`
|
|
22
|
+
|
|
23
|
+
A regenerable artifact is only safe if it is actually regenerated. `--check`
|
|
24
|
+
makes that mechanical instead of a habit:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cdd-kit openapi export --check --out build/openapi.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
It does **not** write. It compares the committed artifact at `--out` against what
|
|
31
|
+
the contract produces right now and exits:
|
|
32
|
+
|
|
33
|
+
- `0` - in sync.
|
|
34
|
+
- `1` - the artifact is missing, or the contract changed but the export was not
|
|
35
|
+
regenerated. The command prints the exact `openapi export --out ...` command
|
|
36
|
+
to fix it.
|
|
37
|
+
|
|
38
|
+
Wire it into CI or a pre-commit hook so a contract edit that forgets to
|
|
39
|
+
regenerate the export, and therefore the typed client downstream, fails the
|
|
40
|
+
build. `--check` honors `--yaml`, so check the same format you committed.
|
|
41
|
+
|
|
42
|
+
## What it derives
|
|
43
|
+
|
|
44
|
+
From the endpoint table (`| method | path | auth | request schema | response schema | errors | tests |`):
|
|
45
|
+
|
|
46
|
+
| Contract column | OpenAPI output |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `method` + `path` | operation under `paths`, with `:id`/`{id}` normalized to `{id}` |
|
|
49
|
+
| path templates | `parameters` (`in: path`, `required: true`, `type: string`) |
|
|
50
|
+
| `auth` | `security` (`bearerAuth`) for `required`/`admin`, optional+anonymous for `optional`, none for `none`/`public` |
|
|
51
|
+
| `method` | success status: `201` for `POST`, else `200` |
|
|
52
|
+
| `errors` | extra response entries for any explicit `4xx`/`5xx` codes listed |
|
|
53
|
+
| `response schema` | if it names a schema in `## Schemas`, emitted as response JSON Schema; otherwise recorded as `x-cdd-response-contract` prose |
|
|
54
|
+
| `request schema` | if it names a schema in `## Schemas`, emitted as request JSON Schema; otherwise `requestBody` is marked `x-cdd-unresolved: true` |
|
|
55
|
+
|
|
56
|
+
The exporter does not fabricate field-level schemas. Request/response cells that
|
|
57
|
+
do not resolve to a named schema remain prose. The unresolved markers are
|
|
58
|
+
deliberate: emitting a fake schema would be a new drift source. Add a
|
|
59
|
+
`## Schemas` section when a body shape should become machine-typed.
|
|
60
|
+
|
|
61
|
+
## Schema-carrying contracts
|
|
62
|
+
|
|
63
|
+
Add optional `### Name` subsections under `## Schemas`. Existing endpoint table
|
|
64
|
+
cells like `CreateUser`, `User`, or `User[]` become references when a matching
|
|
65
|
+
schema exists.
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
## Endpoint Requirements
|
|
69
|
+
| method | path | auth | request schema | response schema | errors | tests |
|
|
70
|
+
|---|---|---|---|---|---|---|
|
|
71
|
+
| POST | /api/users | admin | CreateUser | User | 400 | yes |
|
|
72
|
+
|
|
73
|
+
## Schemas
|
|
74
|
+
|
|
75
|
+
### CreateUser
|
|
76
|
+
| field | type | required | format | notes |
|
|
77
|
+
|---|---|---|---|---|
|
|
78
|
+
| email | string | yes | email | login identity |
|
|
79
|
+
| name | string | yes | | display name |
|
|
80
|
+
| role | enum(admin, member) | no | | |
|
|
81
|
+
|
|
82
|
+
### User
|
|
83
|
+
| field | type | required | notes |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| id | string | yes | |
|
|
86
|
+
| email | string | yes | |
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Field-table types are intentionally small and closed:
|
|
90
|
+
|
|
91
|
+
| Type cell | Output |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `string`, `integer`, `number`, `boolean` | primitive JSON Schema |
|
|
94
|
+
| `OtherSchema` | `$ref` to another named schema |
|
|
95
|
+
| `OtherSchema[]` or `string[]` | array wrapper |
|
|
96
|
+
| `enum(active, disabled)` | string enum |
|
|
97
|
+
|
|
98
|
+
`required: yes` adds the field to JSON Schema `required`. `notes` becomes
|
|
99
|
+
`description`. An optional `format` column is emitted as JSON Schema `format`
|
|
100
|
+
and may be enforced by downstream tooling.
|
|
101
|
+
|
|
102
|
+
For complex bodies, use a raw Tier B escape hatch:
|
|
103
|
+
|
|
104
|
+
````markdown
|
|
105
|
+
### Event
|
|
106
|
+
```json-schema
|
|
107
|
+
{
|
|
108
|
+
"type": "object",
|
|
109
|
+
"oneOf": [
|
|
110
|
+
{ "required": ["createdAt"] },
|
|
111
|
+
{ "required": ["deletedAt"] }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
````
|
|
116
|
+
|
|
117
|
+
The exporter fails instead of weakening types when a schema is ambiguous:
|
|
118
|
+
duplicate schema names, a section that mixes a field table and `json-schema`
|
|
119
|
+
block, invalid JSON, or an unknown field type all exit non-zero.
|
|
120
|
+
|
|
121
|
+
## Wiring a typed client in a consumer repo
|
|
122
|
+
|
|
123
|
+
The kit produces the OpenAPI seam; you generate the client with an existing,
|
|
124
|
+
well-maintained generator in your own CI. When a `package.json` is present,
|
|
125
|
+
`cdd-kit init` scaffolds this for you as two editable npm scripts:
|
|
126
|
+
|
|
127
|
+
```jsonc
|
|
128
|
+
"scripts": {
|
|
129
|
+
// regenerate the OpenAPI artifact + the typed client
|
|
130
|
+
"contract:client": "cdd-kit openapi export --out contracts/api/openapi.json && npx --yes openapi-typescript contracts/api/openapi.json -o src/api/types.ts",
|
|
131
|
+
// the sync gate - fails if the artifact drifted from the contract
|
|
132
|
+
"contract:client:check": "cdd-kit openapi export --check --out contracts/api/openapi.json"
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
These are a starting point, not a hard dependency: the generator
|
|
137
|
+
(`openapi-typescript`) and the output path are yours to change. The kit owns the
|
|
138
|
+
generic contract-to-OpenAPI half (`openapi export` / `--check`); the
|
|
139
|
+
stack-specific codegen stays in your repo, which is why init writes an editable
|
|
140
|
+
script rather than hard-coding a tool. Run `npm run contract:client:check` in CI
|
|
141
|
+
as the gate.
|
|
142
|
+
|
|
143
|
+
Doing it by hand instead, for a TypeScript frontend:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# 1. Export the contract to OpenAPI (committed or generated in CI)
|
|
147
|
+
cdd-kit openapi export --yaml --out openapi.yaml
|
|
148
|
+
|
|
149
|
+
# 2. Generate types with openapi-typescript (or orval / openapi-generator)
|
|
150
|
+
npx openapi-typescript openapi.yaml -o src/api/schema.d.ts
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Now frontend calls typed against `schema.d.ts` make a divergent path, method, or
|
|
154
|
+
schema-resolved body shape a compile error. Run both generated clients and
|
|
155
|
+
`validate_api_conformance.py`: conformance stays the universal floor for code
|
|
156
|
+
that cannot be regenerated, generated types are the stronger path where the
|
|
157
|
+
stack allows it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contract-driven-delivery",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Contract-driven delivery kit for AI coding agents with deterministic context indexes, manifest-backed read-scope governance, and orchestrated contracts-first delivery.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contract-driven",
|