Files
atomic-design-poc/docs/project/refactor-backlog-setup/refactor-backlog/implementation/adr-c-003.md
T
ehoandClaude Opus 5 25a5d415a5 docs(adr): land ADR-C-001, ADR-C-003, ADR-C-007 and ADR-C-009
The architect approved the four ADR-fix tickets. All four change what the
architecture documents claim. No code changes.

ADR-0001, ADR-C-001: the worked example claimed the POC has no real backend.
It rewrites against `backend/src/BigRegister.Api`. Every path it named is
repointed. The out-of-scope list drops two discharged bullets: 33 `parse*`
boundaries exist, and `npm run gen:api` is real.

ADR-0001, ADR-C-003: a new section states that the generated client is the wire
contract. A hand-written `contracts/*.dto.ts` is the exception for two cases
only. The four survivors stay, because NSwag emits every property as optional
and flattens `RegistrationStatusDto` into five optional strings. The `parse*`
trust boundary stays mandatory, because a generated type is a compile-time
claim about the wire and not a runtime guarantee.

ADR-0003, ADR-C-007: four paths moved in WP-67 and are repointed. Point 4 kept
the principle and changed its example to `skeleton` and `spinner`. Two of its
claims were false and the amendment says so: `app-alert` wraps the vendored
`.feedback` classes, and `site-header` composes the vendored `.titlebar`.

ADR-0004, ADR-C-009: the exception section states a four-part test instead of
one named exception. `OrgTemplateStore` and `FeatureFlagStore` both pass it. RB-07
gated this ticket, because clause 4 needs an audited allow path. RB-07 landed
that, so the ADR does not ratify a control that the code lacks.

Three tickets need a matching CLAUDE.md correction in the same diff. CLAUDE.md
section 2 loses the false `alert` example. Section 4 gets the generated-client
rule and the four-part test.

Two findings were wrong. ADR-C-001 asked to keep an out-of-scope bullet that
reads "SessionStore is in-memory". The session persists to `localStorage` now,
so the bullet covers multi-tab sync only. ADR-C-007 flagged one half of point 4
and missed that the other half is equally false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 18:29:05 +02:00

4.4 KiB

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.