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.
@@ -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.