# ADR-C-003 — state that the generated client is the wire contract Status: **implemented** · 2026-08-27 · Source finding: `06-adr-conformance.md` §ADR-C-003 ## What was wrong ADR-0001 set "one source of truth that generates types for both sides" as the target state. The code reached it. CLAUDE.md §4 still stated the pre-codegen rule — _"DTO lives in `contracts/`"_ — as standing law, so §4 could be cited to justify both deleting the four survivors and adding new hand-written DTOs for already-generated endpoints. ## What changed | File | Change | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `docs/reference/architecture/0001-...md` | **new** §"Where the contract lives, after codegen" | | `CLAUDE.md` §4 | the flat "DTO lives in `contracts/`" rule replaced with the generated-client rule + the two exceptions | No code changed. Per CLAUDE.md's own precedence rule, the ADR was amended first and CLAUDE.md corrected to match, in one diff. ## The decision the finding asked for: the four survivors stay ADR-C-003 required an explicit, recorded decision on the four remaining hand-written DTOs. **They stay**, all four under exception case 2 ("the generator types the shape too loosely"). This is not a preference — adopting the generated shapes would violate CLAUDE.md §3. Evidence. NSwag emits every property as optional, and flattens a discriminated union into a bag of optional fields: | | generated (`api-client.ts`) | hand-written (`dashboard-view.dto.ts`) | | ----------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `DashboardViewDto` | `registration?`, `person?`, `decisions?` — all optional (`:2017`) | all three required | | `RegistrationDto` | six optional fields (`:2202`) | six required fields | | `RegistrationStatusDto` | **one flat record of five optional strings**, `tag?: string` (`:2211`) | a real union of three variants, `tag: 'Geregistreerd' \| 'Geschorst' \| 'Doorgehaald'`, per-variant fields required | The generated `RegistrationStatusDto` makes `{ tag: 'Geregistreerd', doorgehaaldOp: '…' }` representable. That is precisely the illegal state CLAUDE.md §3 exists to forbid, and the `parse*` boundary would have to reconstruct the union by hand anyway. The ADR therefore records that retiring these four is **not** a cleanup to schedule. It becomes correct only if the backend annotates its DTOs so the generator emits required properties and real unions — which names the actual prerequisite instead of leaving the question open. ## Verified counts, not carried over from the finding - Hand-written `contracts/*.dto.ts`: **4** — `apps/ssp/src/app/registratie/contracts/{brp-address,dashboard-view,duo-diplomas}.dto.ts` and `libs/beheer/src/contracts/stamdata.dto.ts`. - All four duplicate generated types **by the same names**: `BrpAddressDto` (`:1997`), `DashboardViewDto` (`:2017`), `DuoLookupDto` (`:2056`), `DuoDiplomaDto` (`:2047`), `PolicyQuestionDto` (`:2168`), `ManualDiplomaPolicyDto` (`:2106`), `StamdataColumnDto` (`:2246`), `StamdataTableDto` (`:2253`), `StamdataTableSummaryDto` (`:2261`). None is a codegen gap — the finding's "case 1" has no occupant today, which is worth knowing. - The `parse*` boundary is restated as mandatory regardless of type provenance. The amendment says why in one line: a generated type is a compile-time claim about the wire, not a runtime guarantee. ## Gate released ADR-C-003 blocked any ticket that would delete the four `contracts/*.dto.ts` files or add a hand-written DTO for a generated endpoint. No open ticket needed it. The rule is now written down, so a future one can be judged against it rather than against a stale §4.