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,142 @@
|
|
|
1
|
+
# ADR 0001: Contract → OpenAPI export, and the path to generated clients
|
|
2
|
+
|
|
3
|
+
- Status: Accepted (export PoC); Proposed (client generation)
|
|
4
|
+
- Date: 2026-06-01
|
|
5
|
+
- Deciders: maintainer + AI delivery agent
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The kit's recurring failure mode is **prose governance**: a rule that only
|
|
10
|
+
works if a human or agent chooses to follow it. PRs #6–#8 converted three such
|
|
11
|
+
rules into mechanical chokepoints (conformance validator, `--with-source`,
|
|
12
|
+
installed graph-first hook). One gap remains, and it is the strongest one.
|
|
13
|
+
|
|
14
|
+
`contracts/api/api-contract.md` is called the single source of truth for the
|
|
15
|
+
API, but **nothing generates code from it**. The conformance validator added in
|
|
16
|
+
#6 is *detective*: it parses real backend routes and frontend calls and diffs
|
|
17
|
+
them against the contract table *after* the code is written. That is valuable,
|
|
18
|
+
but it has two structural limits:
|
|
19
|
+
|
|
20
|
+
1. **It is heuristic.** As the Codex review rounds on #6 showed, regex route
|
|
21
|
+
detection has an endless tail of framework-shape edge cases (NestJS
|
|
22
|
+
`RouterModule`, mounted Express prefixes, Rails' stateful DSL). Each is a
|
|
23
|
+
patch; none is a proof.
|
|
24
|
+
2. **It catches drift; it cannot prevent it.** A divergent call is written,
|
|
25
|
+
committed, and only then flagged.
|
|
26
|
+
|
|
27
|
+
The category that *prevents* drift is **generation**: if the frontend client (or
|
|
28
|
+
its types) is generated from the contract, a divergent call is unrepresentable —
|
|
29
|
+
it fails to typecheck. No regex, no edge cases. This ADR decides how the kit
|
|
30
|
+
moves toward that without betraying its "generic, ships to any repo" constraint.
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
Split the work along the **generic / stack-specific** seam, and only build the
|
|
35
|
+
generic half in the kit core.
|
|
36
|
+
|
|
37
|
+
### 1. The kit owns contract → OpenAPI extraction (generic, build now)
|
|
38
|
+
|
|
39
|
+
Add `cdd-kit openapi export`, which reads `contracts/api/api-contract.md` and
|
|
40
|
+
emits a **minimal OpenAPI 3.1 skeleton** (`paths`, `operations`, `parameters`
|
|
41
|
+
inferred from path templates, response status placeholders, and the API-style
|
|
42
|
+
metadata as `info`/`description`). This is:
|
|
43
|
+
|
|
44
|
+
- **stack-agnostic** — it reads the same markdown table the conformance
|
|
45
|
+
validator already parses; it produces a standard artifact, not code;
|
|
46
|
+
- **safe** — it never writes into source trees, only emits an OpenAPI document
|
|
47
|
+
to stdout or a chosen path;
|
|
48
|
+
- **non-authoritative by direction** — the markdown contract remains the SoT
|
|
49
|
+
that humans/agents edit; OpenAPI is a *projection* of it for tooling. (We do
|
|
50
|
+
**not** invert this to "OpenAPI is the SoT" — that would require every contract
|
|
51
|
+
author to write OpenAPI, which contradicts the kit's "non-engineers author
|
|
52
|
+
contracts" audience.)
|
|
53
|
+
|
|
54
|
+
The skeleton is intentionally partial: request/response *schemas* in the
|
|
55
|
+
contract table are free-form prose today (e.g. "User", "User[]"), not JSON
|
|
56
|
+
Schema, so the export cannot fabricate field-level schemas it does not have. It
|
|
57
|
+
emits what is mechanically derivable (path, method, params, status codes,
|
|
58
|
+
auth → security scheme hints) and marks the rest as `TODO`/`x-cdd-unresolved`.
|
|
59
|
+
This honesty is the point — same principle as removing `.rb` from the
|
|
60
|
+
conformance defaults rather than shipping a fake Rails parser.
|
|
61
|
+
|
|
62
|
+
### 2. Per-stack client generation stays an opt-in adapter (do NOT build in core)
|
|
63
|
+
|
|
64
|
+
Generating a typed FE client from the OpenAPI document is **stack-specific** and
|
|
65
|
+
belongs in the consumer repo, wired by the user with an existing, well-maintained
|
|
66
|
+
generator (`openapi-typescript`, `orval`, `openapi-generator`, …). The kit's
|
|
67
|
+
role is to **produce the seam** (the OpenAPI doc) and **document the wiring**, not
|
|
68
|
+
to ship a universal generator. Shipping one would:
|
|
69
|
+
|
|
70
|
+
- recreate the maintenance tail we just escaped, now for every target language;
|
|
71
|
+
- risk generating subtly wrong clients across arbitrary stacks — itself a new
|
|
72
|
+
drift source, which is antithetical to the kit.
|
|
73
|
+
|
|
74
|
+
So: kit exports OpenAPI; user runs their generator of choice in their own CI;
|
|
75
|
+
the generated client makes divergence a compile error. The kit provides a
|
|
76
|
+
documented recipe per common stack, not code.
|
|
77
|
+
|
|
78
|
+
### 3. Relationship to the conformance validator
|
|
79
|
+
|
|
80
|
+
The OpenAPI export does **not** replace `validate_api_conformance.py`; they are
|
|
81
|
+
complementary along the brownfield axis:
|
|
82
|
+
|
|
83
|
+
| | Generated client (preventive) | Conformance validator (detective) |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| Applicable when | repo owns both sides in a typed stack and can regenerate | brownfield/polyglot, cannot regenerate the client |
|
|
86
|
+
| Strength | divergence is unrepresentable (compile error) | divergence is flagged post-hoc, heuristically |
|
|
87
|
+
| Cost | requires generator wiring per stack | zero per-stack wiring |
|
|
88
|
+
|
|
89
|
+
The kit keeps the validator as the universal floor and offers OpenAPI export as
|
|
90
|
+
the on-ramp to the stronger, opt-in preventive path. A repo can also feed the
|
|
91
|
+
exported OpenAPI **back into** conformance checking later (validate real routes
|
|
92
|
+
against the OpenAPI paths instead of the markdown table) — a future unification,
|
|
93
|
+
explicitly out of scope here.
|
|
94
|
+
|
|
95
|
+
## Consequences
|
|
96
|
+
|
|
97
|
+
**Positive**
|
|
98
|
+
- A standard, tool-consumable artifact is derivable from the contract with zero
|
|
99
|
+
per-stack assumptions.
|
|
100
|
+
- The kit stays generic; stack-specific risk stays in the consumer repo where it
|
|
101
|
+
belongs.
|
|
102
|
+
- Clear, honest boundary: the export emits only what the markdown actually
|
|
103
|
+
determines and flags the rest.
|
|
104
|
+
|
|
105
|
+
**Negative / limits**
|
|
106
|
+
- The skeleton is partial until contracts carry field-level schemas. This ADR
|
|
107
|
+
does not mandate that migration; it leaves request/response bodies as `TODO`.
|
|
108
|
+
- Two artifacts now describe the API (markdown SoT + derived OpenAPI). We accept
|
|
109
|
+
this because the direction is fixed (markdown → OpenAPI, never reverse-edited),
|
|
110
|
+
so they cannot become competing sources of truth. `cdd-kit openapi export
|
|
111
|
+
--check` asserts the committed OpenAPI is in sync, the same way `code-map
|
|
112
|
+
--check` does (shipped — see the follow-up note below).
|
|
113
|
+
|
|
114
|
+
## Scope of the accompanying PoC
|
|
115
|
+
|
|
116
|
+
This PR ships **only** the generic export half:
|
|
117
|
+
|
|
118
|
+
- `cdd-kit openapi export [--out <path>] [--json|--yaml]` reading
|
|
119
|
+
`contracts/api/api-contract.md`;
|
|
120
|
+
- path-template → OpenAPI `parameters` inference (`/users/:id`, `/users/{id}`);
|
|
121
|
+
- method/auth/status extraction; API-style metadata into `info`;
|
|
122
|
+
- unresolved request/response schemas emitted as clearly-marked placeholders;
|
|
123
|
+
- tests; docs recipe for wiring `openapi-typescript` in a consumer repo.
|
|
124
|
+
|
|
125
|
+
It does **not** ship any client generation or schema authoring format. Those
|
|
126
|
+
remain follow-ups, each its own decision.
|
|
127
|
+
|
|
128
|
+
## Follow-up shipped since the PoC
|
|
129
|
+
|
|
130
|
+
- **`--check` sync gate** (`cdd-kit openapi export --check --out <path>`):
|
|
131
|
+
verifies the committed artifact still matches the contract, exiting non-zero on
|
|
132
|
+
drift. This closes the "two artifacts" risk noted above — the derived OpenAPI
|
|
133
|
+
can no longer silently fall out of step with the markdown source of truth.
|
|
134
|
+
- **Consumer codegen wiring**: `cdd-kit init` now scaffolds editable
|
|
135
|
+
`contract:client` / `contract:client:check` npm scripts when a `package.json`
|
|
136
|
+
is present, materializing the consumer half of the seam as a chokepoint rather
|
|
137
|
+
than a doc — while keeping the actual generator choice in the consumer repo, as
|
|
138
|
+
this ADR decided.
|
|
139
|
+
|
|
140
|
+
Still deferred: a schema-carrying contract format (so request/response **bodies**
|
|
141
|
+
become preventive too, not just paths/methods), and feeding the exported OpenAPI
|
|
142
|
+
back into `validate_api_conformance.py`.
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# ADR 0002: Schema-carrying contract format (preventive request/response bodies)
|
|
2
|
+
|
|
3
|
+
- Status: Accepted (design); implementation to follow in a separate PR
|
|
4
|
+
- Date: 2026-06-01
|
|
5
|
+
- Deciders: maintainer + AI delivery agent
|
|
6
|
+
- Supersedes: nothing; extends ADR 0001 (Contract → OpenAPI export)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
ADR 0001 made **paths and methods** preventive: `cdd-kit openapi export`
|
|
11
|
+
projects the markdown contract into OpenAPI, a consumer generates a typed
|
|
12
|
+
client, and a wrong *URL or verb* becomes a compile error. It deliberately
|
|
13
|
+
stopped there. The contract's `request schema` / `response schema` columns are
|
|
14
|
+
free-form prose today — cells say `User`, `User[]`, `CreateUser` — so the
|
|
15
|
+
exporter cannot fabricate field-level schemas and marks every request body
|
|
16
|
+
`x-cdd-unresolved` (see `src/commands/openapi-export.ts`).
|
|
17
|
+
|
|
18
|
+
The consequence is a **half-preventive client**. The generated client knows
|
|
19
|
+
`POST /api/users` exists and returns *something*, but the request body is typed
|
|
20
|
+
`unknown` / `any`. The exact class of bug the kit exists to kill — "the URL is
|
|
21
|
+
right but the payload is wrong" (missing required field, wrong field name,
|
|
22
|
+
wrong type) — is still only catchable *detectively*, if at all. The conformance
|
|
23
|
+
validator (ADR 0001 §3) diffs *routes*, not *body shapes*, so it does not cover
|
|
24
|
+
this either.
|
|
25
|
+
|
|
26
|
+
This ADR decides **how a contract can carry field-level schemas** so that
|
|
27
|
+
request/response bodies become preventive too, **without** betraying the two
|
|
28
|
+
constraints ADR 0001 was careful about:
|
|
29
|
+
|
|
30
|
+
1. **Non-engineers author contracts.** We cannot require every author to write
|
|
31
|
+
raw OpenAPI / JSON Schema.
|
|
32
|
+
2. **The markdown contract stays the single source of truth**, projected
|
|
33
|
+
one-way into OpenAPI; we never reverse-edit the generated artifact.
|
|
34
|
+
|
|
35
|
+
## Decision
|
|
36
|
+
|
|
37
|
+
### 1. Schemas live in the same contract file, referenced by name
|
|
38
|
+
|
|
39
|
+
Add an optional `## Schemas` section to `contracts/api/api-contract.md`. Each
|
|
40
|
+
named schema is a `### <Name>` subsection. The existing endpoint table is
|
|
41
|
+
**unchanged**: a `request schema` / `response schema` cell that already says
|
|
42
|
+
`CreateUser` simply *becomes a reference* to `### CreateUser` when that section
|
|
43
|
+
exists. No new column, no migration of existing rows.
|
|
44
|
+
|
|
45
|
+
```markdown
|
|
46
|
+
## Endpoint Requirements
|
|
47
|
+
| method | path | auth | request schema | response schema | errors | tests |
|
|
48
|
+
|---|---|---|---|---|---|---|
|
|
49
|
+
| POST | /api/users | admin | CreateUser | User | 400 | yes |
|
|
50
|
+
|
|
51
|
+
## Schemas
|
|
52
|
+
|
|
53
|
+
### CreateUser
|
|
54
|
+
| field | type | required | notes |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| email | string | yes | login identity |
|
|
57
|
+
| name | string | yes | |
|
|
58
|
+
| age | integer | no | |
|
|
59
|
+
|
|
60
|
+
### User
|
|
61
|
+
| field | type | required | notes |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| id | string | yes | |
|
|
64
|
+
| email | string | yes | |
|
|
65
|
+
| name | string | yes | |
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This reuses the **exact authoring idiom the audience already uses** for
|
|
69
|
+
endpoints (a markdown table), so the non-engineer constraint holds. One file
|
|
70
|
+
stays the source of truth.
|
|
71
|
+
|
|
72
|
+
**Naming rules** (so cell→schema resolution is unambiguous):
|
|
73
|
+
|
|
74
|
+
- A schema name must match `^[A-Za-z][A-Za-z0-9_]*$` (the OpenAPI
|
|
75
|
+
`components.schemas` key charset). A `### Name` heading that does not match is
|
|
76
|
+
**not** treated as a schema (it is ordinary prose), so unrelated `###`
|
|
77
|
+
headings in the file are never misread as types.
|
|
78
|
+
- A cell value is **first stripped of a single trailing `[]`** (the existing
|
|
79
|
+
list shorthand: `User[]`), then the inner name is matched. `User[]` resolves to
|
|
80
|
+
`### User` and emits an array wrapper (`{ type: array, items: { $ref } }`), per
|
|
81
|
+
§4. The `[]` lives only in the *cell* grammar; a `### User[]` heading is invalid
|
|
82
|
+
under the name grammar above and is never a schema. (Only one level of `[]` is
|
|
83
|
+
recognized; `User[][]` is over the ceiling → Tier B.)
|
|
84
|
+
- After `[]`-stripping, resolution is **exact and case-sensitive**: the inner
|
|
85
|
+
name resolves only to a `### Name` whose text matches byte-for-byte after
|
|
86
|
+
trimming. `user` does not resolve to `### User`. This avoids a class of silent
|
|
87
|
+
mis-binding.
|
|
88
|
+
- **Duplicate `### Name` sections are a hard error** (the export fails, it does
|
|
89
|
+
not pick one), because a duplicate is an ambiguous source of truth — the same
|
|
90
|
+
stance the kit takes elsewhere.
|
|
91
|
+
- A rename is just an edit: rename the `### Name` *and* the cells that reference
|
|
92
|
+
it. **A cell that names a non-existent schema is, by construction,
|
|
93
|
+
indistinguishable from ordinary prose** — every existing contract's cells are
|
|
94
|
+
exactly that (prose with no `## Schemas` section) — so it **stays Tier C and is
|
|
95
|
+
not an error**. `openapi export --check` will **not** flag it: `--check` is
|
|
96
|
+
byte-equality of the committed artifact against a fresh export, so once the
|
|
97
|
+
Tier C artifact is committed it stays in sync. There is deliberately **no
|
|
98
|
+
automatic "did-you-mean" net** for a dangling ref, because adding one would
|
|
99
|
+
either break the no-migration guarantee (it would fail on every legitimately
|
|
100
|
+
prose cell) or require guessing intent. Surfacing *suspected* typo-refs is a
|
|
101
|
+
possible **opt-in strict mode**, called out as a follow-up below — not a
|
|
102
|
+
promise made by `--check`.
|
|
103
|
+
|
|
104
|
+
### 2. Three fidelity tiers, with graceful degradation
|
|
105
|
+
|
|
106
|
+
For each `request schema` / `response schema` cell value, the exporter resolves
|
|
107
|
+
in this order:
|
|
108
|
+
|
|
109
|
+
| Tier | Author writes | Export emits | Body is |
|
|
110
|
+
|---|---|---|---|
|
|
111
|
+
| **A. Field table** | a `### Name` field sub-table | `components.schemas.Name` (JSON Schema) + `$ref` | **preventive** |
|
|
112
|
+
| **B. Raw escape hatch** | a fenced ` ```json-schema ` block under `### Name` | that JSON Schema verbatim + `$ref` | **preventive** |
|
|
113
|
+
| **C. Unresolved** | nothing (cell is prose, no `### Name`) | **today's markers, unchanged** (see below) | best-effort |
|
|
114
|
+
|
|
115
|
+
Tier C is the current behavior, so **every existing contract keeps exporting
|
|
116
|
+
exactly as it does now**. Crucially, "today's behavior" is **not uniform across
|
|
117
|
+
request and response**, and Tier C must preserve each side exactly:
|
|
118
|
+
|
|
119
|
+
- an unresolved **request** cell keeps emitting the `requestBody` with
|
|
120
|
+
`x-cdd-unresolved` (as `buildDoc` does today);
|
|
121
|
+
- an unresolved **response** cell keeps emitting the prose annotation
|
|
122
|
+
`x-cdd-response-contract` (as `buildDoc` does today) — it does **not** gain an
|
|
123
|
+
`x-cdd-unresolved` marker.
|
|
124
|
+
|
|
125
|
+
Flattening both onto a single `x-cdd-unresolved` marker would rewrite every
|
|
126
|
+
existing unresolved-response artifact and break the byte-for-byte/no-migration
|
|
127
|
+
guarantee. So Tier C is defined as "emit precisely what the current exporter
|
|
128
|
+
emits for this cell"; only resolution to a real `### Name` (Tier A/B) changes
|
|
129
|
+
the output. Adoption is incremental and per-schema: define `### User` and only
|
|
130
|
+
`User` becomes preventive; everything else degrades untouched. This mirrors
|
|
131
|
+
ADR 0001's "emit what is mechanically derivable, mark the rest" honesty — and
|
|
132
|
+
mirrors how client codegen itself is opt-in.
|
|
133
|
+
|
|
134
|
+
Tier B exists because the field-table notation has a deliberate ceiling (below):
|
|
135
|
+
power users get a verbatim escape hatch instead of being blocked.
|
|
136
|
+
|
|
137
|
+
**A and B are mutually exclusive within one `### Name`.** A section is exactly
|
|
138
|
+
one of: a field table (Tier A), a single fenced ` ```json-schema ` block
|
|
139
|
+
(Tier B), or neither (Tier C). If a section contains **both** a field table and
|
|
140
|
+
a `json-schema` block, the export **fails** rather than guessing precedence —
|
|
141
|
+
the same no-silent-pick rule as duplicate names. This keeps every schema's
|
|
142
|
+
source unambiguous; a power user who needs both a readable table *and* a
|
|
143
|
+
constraint the grammar can't express writes the whole schema in Tier B.
|
|
144
|
+
|
|
145
|
+
### 3. The field-table type grammar (Tier A)
|
|
146
|
+
|
|
147
|
+
Minimal, closed, table-friendly. The `type` cell accepts:
|
|
148
|
+
|
|
149
|
+
| `type` cell | JSON Schema |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `string`, `integer`, `number`, `boolean` | `{ "type": ... }` |
|
|
152
|
+
| `<Name>` (matches another `### Name`) | `{ "$ref": "#/components/schemas/Name" }` |
|
|
153
|
+
| `<T>[]` (T = any of the above) | `{ "type": "array", "items": <T> }` |
|
|
154
|
+
| `enum(a, b, c)` | `{ "type": "string", "enum": [...] }` |
|
|
155
|
+
|
|
156
|
+
- `required: yes` adds the field to the schema's `required` array.
|
|
157
|
+
- `notes` becomes `description` (always non-binding prose). An optional `format`
|
|
158
|
+
column becomes JSON Schema `format` — which is an **optional downstream
|
|
159
|
+
constraint, not guaranteed-inert metadata**: format-aware tooling (ajv with
|
|
160
|
+
formats, `openapi-generator`) *does* enforce values like `email`, `uuid`,
|
|
161
|
+
`date-time` and may narrow generated types. The ADR does not promise `format`
|
|
162
|
+
is never validated; an author writing `format: email` is choosing a constraint
|
|
163
|
+
their generator may apply, and should mean it.
|
|
164
|
+
- An unknown `type` value (e.g. `strng`) in a schema that is otherwise Tier A
|
|
165
|
+
**fails the export** (non-zero exit, naming the schema, field, and bad type).
|
|
166
|
+
It must **not** degrade just that one field to an `x-cdd-unresolved` marker:
|
|
167
|
+
extension keywords are ignored by validators and code generators, so a
|
|
168
|
+
per-field marker leaves the property unconstrained while the operation is still
|
|
169
|
+
advertised as Tier A/preventive — the client would accept any value for it.
|
|
170
|
+
Worse, byte-equality `--check` would not catch this, so CI would stay green on
|
|
171
|
+
a silently weakened client. Failing the whole export is the only outcome
|
|
172
|
+
consistent with "Tier A means preventive": a schema is either fully resolvable
|
|
173
|
+
or it is not authored yet (leave the cell prose → Tier C). There is no
|
|
174
|
+
half-preventive Tier A schema.
|
|
175
|
+
|
|
176
|
+
**Common edge cases, decided explicitly** (so authors know the Tier A/Tier B
|
|
177
|
+
line without guessing):
|
|
178
|
+
|
|
179
|
+
| Need | Tier A answer |
|
|
180
|
+
|---|---|
|
|
181
|
+
| **Optional vs nullable** | `required: no` means the field *may be absent*. It does **not** make the value `null`. A field that must be present but may hold `null` is a `oneOf`-shaped constraint → **drop to Tier B**. We do not overload the `required` column with two meanings. |
|
|
182
|
+
| **Numeric / non-string enums** | `enum(...)` is **string-only** (the 90% case: status strings). A numeric or mixed-type enum → **Tier B**. The grammar stays closed rather than inventing a per-member type syntax in a table cell. |
|
|
183
|
+
| **Date / time** | `string` with a `format` note (e.g. `date-time`, `date`) — emitted as JSON Schema `format`. Whether it is *enforced* depends on the consumer's generator (see the `format` note above): format-aware tooling will validate/narrow, format-blind tooling treats it as a hint. Author it only when you mean that constraint; if you need shape beyond a single `format`, → **Tier B**. |
|
|
184
|
+
|
|
185
|
+
The rule behind all three: Tier A covers objects of scalars, refs, arrays,
|
|
186
|
+
string enums, and a **single `format` per field** (the optional `format`
|
|
187
|
+
column). The moment a field needs a `null`-union, a non-string enum, or a shape
|
|
188
|
+
**beyond a single `format`** (e.g. `format` *plus* a union, or multiple
|
|
189
|
+
constraints the grammar has no column for), that one schema moves to Tier B. A
|
|
190
|
+
lone `date-time`/`email`/`uuid` `format` stays in Tier A — it is exactly what the
|
|
191
|
+
`format` column is for. The field table never grows new columns past this — that
|
|
192
|
+
is the ceiling, by design.
|
|
193
|
+
|
|
194
|
+
Anything past this grammar (`oneOf`, discriminated unions, deep nesting beyond
|
|
195
|
+
named refs, tuple types) is **out of scope for Tier A by design** — that is what
|
|
196
|
+
Tier B's raw JSON Schema block is for. We are not rebuilding JSON Schema in
|
|
197
|
+
markdown tables; we are covering the 90% object-of-scalars-and-refs case in the
|
|
198
|
+
audience's own idiom and providing a clean hatch for the rest.
|
|
199
|
+
|
|
200
|
+
### 4. What the export changes
|
|
201
|
+
|
|
202
|
+
- A `components.schemas` map is populated from resolved `### Name` sections.
|
|
203
|
+
- A resolvable `request schema` cell emits a real
|
|
204
|
+
`requestBody.content['application/json'].schema = { $ref }` **instead of** the
|
|
205
|
+
`x-cdd-unresolved` placeholder.
|
|
206
|
+
- A resolvable `response schema` cell emits
|
|
207
|
+
`responses[code].content['application/json'].schema = { $ref }` (and `User[]`
|
|
208
|
+
→ an array wrapper), upgrading today's `x-cdd-response-contract` annotation
|
|
209
|
+
from prose to a typed shape.
|
|
210
|
+
- `--check` is unaffected in mechanism: it stays byte-equality of the committed
|
|
211
|
+
artifact against the freshly-generated projection. More of the document is now
|
|
212
|
+
determined by the contract, so the sync gate simply covers more.
|
|
213
|
+
|
|
214
|
+
Unresolved cells keep emitting exactly the markers they do today, so the
|
|
215
|
+
artifact's shape is a **superset** — no existing field is removed or renamed.
|
|
216
|
+
|
|
217
|
+
## Consequences
|
|
218
|
+
|
|
219
|
+
**Positive**
|
|
220
|
+
- The payload-shape bug class (`wrong/missing field`) becomes a **compile
|
|
221
|
+
error** in the generated client — the strongest, non-heuristic guarantee,
|
|
222
|
+
extended from URLs to bodies.
|
|
223
|
+
- Zero forced migration: contracts without a `## Schemas` section are byte-for-
|
|
224
|
+
byte unaffected; the `## Schemas` block is purely additive.
|
|
225
|
+
- Authors stay in markdown tables; no one is required to learn OpenAPI.
|
|
226
|
+
- The escape hatch keeps complex APIs unblocked without polluting the common
|
|
227
|
+
notation.
|
|
228
|
+
|
|
229
|
+
**Negative / limits**
|
|
230
|
+
- Field-level precision **is** engineering work. This tier targets the engineer
|
|
231
|
+
(or the agent acting on their behalf), not the non-engineer contract author —
|
|
232
|
+
honestly an opt-in enrichment layer, the same status client codegen has. We
|
|
233
|
+
accept this rather than pretend field types can be authored without rigor.
|
|
234
|
+
- Two notations now describe a body in some repos (Tier A table for simple,
|
|
235
|
+
Tier B raw for complex). We bound this by making Tier B an explicit fenced
|
|
236
|
+
block, never an inline cell, so the table never becomes a JSON Schema dumping
|
|
237
|
+
ground.
|
|
238
|
+
- **Response-body conformance against real code stays out of scope.** Once
|
|
239
|
+
schemas are real, one *could* check that backend response types match the
|
|
240
|
+
contract schema — but that requires per-language type extraction, the exact
|
|
241
|
+
heuristic tail ADR 0001 refused. Generation (preventive) remains the path;
|
|
242
|
+
code-vs-schema conformance is a separate future decision, not this one.
|
|
243
|
+
- **Schema-level breaking-change diffing** (adding a required request field is
|
|
244
|
+
breaking) is enabled by this format but **not** built here; it is a follow-up
|
|
245
|
+
that would consume two exported artifacts and apply the contract's existing
|
|
246
|
+
`breaking-change-policy`.
|
|
247
|
+
- **Suspected-dangling-ref strict mode** is a possible follow-up, not part of
|
|
248
|
+
this ADR: an opt-in check that flags cells which *look* like an intended schema
|
|
249
|
+
ref (e.g. capitalized single-token, near-miss of an existing `### Name`) but
|
|
250
|
+
resolve to no section. It must stay opt-in precisely because, in the general
|
|
251
|
+
case, such a cell is indistinguishable from legitimate prose; making it a
|
|
252
|
+
default would break the no-migration guarantee.
|
|
253
|
+
|
|
254
|
+
## Scope of the proposed implementation
|
|
255
|
+
|
|
256
|
+
If accepted, the implementing PR ships:
|
|
257
|
+
|
|
258
|
+
1. A `## Schemas` parser (`### Name` field sub-tables + Tier B fenced blocks)
|
|
259
|
+
in the export path, behind the existing markdown reader.
|
|
260
|
+
2. The Tier A type grammar compiler and `components.schemas` emission.
|
|
261
|
+
3. `request schema` / `response schema` cells resolving to `$ref` when defined,
|
|
262
|
+
degrading to each side's existing marker when not (`x-cdd-unresolved` for
|
|
263
|
+
requests, `x-cdd-response-contract` for responses); a duplicate `### Name`,
|
|
264
|
+
a section mixing Tier A + Tier B, or an unknown field type **fails the
|
|
265
|
+
export** (non-zero, no partial artifact).
|
|
266
|
+
4. Template + docs: a `## Schemas` stub in the **source** template
|
|
267
|
+
`contracts/api/api-contract.md` (NOT `assets/contracts/...`, which `build.js`
|
|
268
|
+
generates from `contracts/` via `copy('contracts', 'assets/contracts')`) and a
|
|
269
|
+
worked example in `docs/openapi-export.md`.
|
|
270
|
+
5. Tests: resolved object, `[]` array, `enum`, nested `$ref`, Tier B passthrough;
|
|
271
|
+
the **no-migration** cases (undefined name leaves request vs response markers
|
|
272
|
+
exactly as today) proving existing contracts export unchanged; and the
|
|
273
|
+
**fail-fast** cases (unknown type, duplicate name, mixed A+B) proving no
|
|
274
|
+
silently-weakened artifact is ever produced.
|
|
275
|
+
|
|
276
|
+
It does **not** ship code-vs-schema conformance or schema breaking-change
|
|
277
|
+
diffing. Those remain follow-ups, each its own decision.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# ADR 0003: Code-intelligence indexing strategy (trigger vs background, AST vs LSP)
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-06-02
|
|
5
|
+
- Deciders: maintainer + AI delivery agent
|
|
6
|
+
- Relates to: `cdd-kit code-map`, `cdd-kit graph`, `cdd-kit index`, `cdd-kit mcp`
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
The kit's token-efficiency moat is its deterministic, low-token code
|
|
11
|
+
intelligence: `cdd-kit code-map` parses source into a structural index, the
|
|
12
|
+
native code-graph adds files/symbols/imports/calls, and agents query symbols and
|
|
13
|
+
line ranges (`--with-source`) instead of `Read`-ing whole files. Two design
|
|
14
|
+
questions were never written down and are now load-bearing as the kit positions
|
|
15
|
+
itself for a fully automated, no-human-reviewer workflow:
|
|
16
|
+
|
|
17
|
+
1. **What parser tier?** Today the kit uses its own AST scanners (Babel for
|
|
18
|
+
JS/TS/Vue, a Python subprocess) producing a YAML map + JSON sidecar + native
|
|
19
|
+
graph. Should it adopt a **Language Server Protocol (LSP)** backend like
|
|
20
|
+
[Serena](https://github.com/oraios/serena), which gives IDE-grade
|
|
21
|
+
"go-to-definition" precision across 20+ languages?
|
|
22
|
+
|
|
23
|
+
2. **What refresh model?** Today indexing is **trigger-based**: the map is
|
|
24
|
+
regenerated when a command needs it (`gate`, `index query --refresh`,
|
|
25
|
+
`doctor --fix`, the pre-commit code-map hook). Should it instead run as a
|
|
26
|
+
**background daemon** that watches the filesystem and keeps the index live,
|
|
27
|
+
as Serena, [CocoIndex](https://cocoindex.io/cocoindex-code/), and most
|
|
28
|
+
tree-sitter-based indexers do?
|
|
29
|
+
|
|
30
|
+
### What the field does (2026)
|
|
31
|
+
|
|
32
|
+
- **Serena** pairs LLMs with LSP for deterministic symbol resolution. It relies
|
|
33
|
+
on lazy per-language server startup, **incremental indexing** (only modified
|
|
34
|
+
files re-index), symbol-table caching, and a serialized background task queue.
|
|
35
|
+
- **CocoIndex / tree-sitter indexers** use a **background file watcher** with
|
|
36
|
+
debounced (~500 ms) incremental re-parse; only changed AST nodes are
|
|
37
|
+
re-processed, and unchanged chunks reuse cached work. Content-hash (XXH3)
|
|
38
|
+
comparison gives ~4× speedup over full re-index, and full-repo rebuilds are
|
|
39
|
+
explicitly avoided because on large repos they "cost real money and take real
|
|
40
|
+
hours, and the context is stale on arrival."
|
|
41
|
+
- A recurring independent finding: **LSP, built for interactive human IDE
|
|
42
|
+
sessions, does not translate cleanly to autonomous agents** — symbol-resolution
|
|
43
|
+
failures, empty reference searches, and coordinate-precision requirements bite
|
|
44
|
+
in headless runs. Several agent indexers therefore use *LSP-inspired*
|
|
45
|
+
tree-sitter + graph (e.g. PageRank over the dependency graph) and report
|
|
46
|
+
symbol-level awareness at only 8.5–13k tokens, without a live LSP.
|
|
47
|
+
|
|
48
|
+
## Decision
|
|
49
|
+
|
|
50
|
+
### 1. Stay on native AST scanners; do not adopt an LSP daemon
|
|
51
|
+
|
|
52
|
+
The kit keeps its own AST/graph scanners as the default engine. Rationale:
|
|
53
|
+
|
|
54
|
+
- **Determinism over precision-at-any-cost.** The kit's value is a *stable,
|
|
55
|
+
diffable, byte-identical* index that the gate and `--with-source` can rely on.
|
|
56
|
+
An LSP server's answers vary with workspace state, plugin versions, and
|
|
57
|
+
warm-up; that is the wrong trade for a mechanical chokepoint.
|
|
58
|
+
- **Autonomous-agent fit.** The published failure modes of LSP-in-agents are
|
|
59
|
+
exactly our usage pattern (headless, ephemeral containers, no editor).
|
|
60
|
+
- **Zero heavy runtime.** No per-language server processes to install, warm, or
|
|
61
|
+
babysit inside short-lived CI/agent containers.
|
|
62
|
+
- **External LSP/CodeGraph stays opt-in.** `--engine codegraph` already exists
|
|
63
|
+
for users who want a heavier external graph; LSP can join later as another
|
|
64
|
+
opt-in adapter, never the default.
|
|
65
|
+
|
|
66
|
+
### 2. Keep trigger-based refresh as the default; add opt-in background watch
|
|
67
|
+
|
|
68
|
+
Trigger-based stays the default because it is correct for the dominant
|
|
69
|
+
execution context — **ephemeral containers and one-shot agent runs**, where a
|
|
70
|
+
daemon would build an index that is discarded minutes later. But the trigger
|
|
71
|
+
model has a real gap for **long-lived co-editing sessions** (a human and an agent
|
|
72
|
+
editing the same repo over hours): between triggers the map is stale, and
|
|
73
|
+
re-deriving the whole map on every query is wasteful.
|
|
74
|
+
|
|
75
|
+
We close that gap with an **opt-in background mode**, `cdd-kit code-map --watch`:
|
|
76
|
+
|
|
77
|
+
- A debounced (default 500 ms, matching field practice) recursive `fs.watch`
|
|
78
|
+
rebuilds the map after change bursts settle.
|
|
79
|
+
- Self-triggering is avoided by ignoring writes under `.cdd/`.
|
|
80
|
+
- Where recursive `fs.watch` is unavailable (older Linux Node), it falls back to
|
|
81
|
+
freshness polling using the existing `checkCodeMapFreshness` digest check.
|
|
82
|
+
- It is **never armed automatically** — daemons are a poor default for CI.
|
|
83
|
+
|
|
84
|
+
### 3. Make detection cheaper and more honest now; make rebuild incremental next
|
|
85
|
+
|
|
86
|
+
Two follow-ups, sequenced:
|
|
87
|
+
|
|
88
|
+
- **(shipped here)** Background watch + the existing content-hash freshness check
|
|
89
|
+
(`# sources-digest` header, verified against `computeSourcesDigest`) already
|
|
90
|
+
prevent false "stale" verdicts after `git clone` and let watch/poll skip
|
|
91
|
+
no-op rebuilds.
|
|
92
|
+
- **(next PR)** **Incremental rebuild.** Today `--watch` still rebuilds the whole
|
|
93
|
+
map per debounce window because the scanners are whole-repo. The high-value
|
|
94
|
+
follow-up is per-file incremental: keep prior map entries for files whose
|
|
95
|
+
content hash is unchanged, re-scan only the changed set, and merge. This is
|
|
96
|
+
the ~4× win the field reports and is the prerequisite for watch to be cheap on
|
|
97
|
+
large repos. The content-hash sidecar already gives us the per-file digests to
|
|
98
|
+
build on.
|
|
99
|
+
|
|
100
|
+
## Consequences
|
|
101
|
+
|
|
102
|
+
- **Positive:** default behaviour unchanged for CI/agents; a real option for
|
|
103
|
+
live sessions; explicit rationale for *not* chasing LSP; a clear, scoped
|
|
104
|
+
incremental-rebuild roadmap.
|
|
105
|
+
- **Negative / accepted:** `--watch` on a large repo is currently O(full scan)
|
|
106
|
+
per change burst until incremental lands — documented, and gated behind an
|
|
107
|
+
explicit flag so no one pays for it unawares.
|
|
108
|
+
- **Revisit when:** an LSP/tree-sitter adapter shows a *deterministic*,
|
|
109
|
+
container-friendly mode, or incremental rebuild lands and changes the
|
|
110
|
+
watch cost profile.
|