Files
atomic-design-poc/docs/backlog/WP-03-contracts-purity.md
Edwin van den Houdt f9b76e7f6a feat(boundaries): WP-03 — contracts purity + ApiClient confinement
Lint-enforce two architecture rules that were only documented (ADR-0001),
landing the rules with the fixes so the build stays green:

- contracts/ imports nothing: dashboard-view.dto.ts is now pure wire shapes
  (inline string-union enums, no domain imports). The DashboardView FE-view
  type moves to the adapter, which maps wire → domain (compiler-enforced seam).
- ApiClient lives only in infrastructure: change-request-form (UI) no longer
  injects ApiClient — a new ChangeRequestAdapter owns the client and the submit
  becomes a createSubmitChangeRequest() command factory (createDraftSync shape).
  draft-sync's wire-DTO import becomes type-only (allowed via allowTypeImports).
- Role type moves to shared/domain/role.ts; the ?role= reader stays in
  shared/infrastructure/role.ts.
- eslint: contracts import-ban + @typescript-eslint/no-restricted-imports on
  api-client (value-only; type imports permitted; infra + shared/upload exempt).

Also fixes a PRE-EXISTING bug found while verifying the flow: change-request-form
never imported FormsModule, so (ngSubmit) didn't bind and the submit button did a
native form submit (page reload) instead of submitting. Verified end-to-end in the
running app: submit → command → adapter → backend → reference, success alert shown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:19:58 +02:00

3.8 KiB

WP-03 — Boundaries I: contracts purity + ApiClient confinement

Status: done (pending commit) Phase: 0 — enforcement & gates

Why

contracts/ is the FE⇄BE wire seam and must import nothing (CLAUDE.md §1, ADR-0001) — but registratie/contracts/dashboard-view.dto.ts imports domain types, and change-request-form.component.ts injects ApiClient straight into UI. Neither rule is lint-enforced. Rule + fixes land together so this WP ends green.

Read first

  • CLAUDE.md §1 + §4 (layers, BFF-lite)
  • eslint.config.mjs (existing zone style to extend)
  • src/app/registratie/contracts/dashboard-view.dto.ts
  • src/app/registratie/infrastructure/dashboard-view.adapter.ts
  • src/app/registratie/ui/change-request-form/change-request-form.component.ts (~line 53)
  • src/app/registratie/application/submit-change-request.ts
  • src/app/shared/infrastructure/role.ts and src/app/brief/ui/letter-composer/letter-composer.component.ts (line 2)

Decisions (pre-made, don't relitigate)

  • Wire DTOs express enums as plain string-literal unions inlined in the DTO file — they describe the wire, not the domain. The adapter's parse* maps them to domain types.
  • The Role type moves out of infrastructure (to shared/domain or shared/application — pick where its current consumers point most naturally); the role-reading mechanism (role.interceptor.ts etc.) stays infrastructure.

Files

  • eslint.config.mjs — two new restrictions
  • src/app/registratie/contracts/dashboard-view.dto.ts — remove domain imports
  • src/app/registratie/infrastructure/dashboard-view.adapter.ts — absorb the mapping
  • src/app/registratie/ui/change-request-form/change-request-form.component.ts — stop injecting ApiClient; the component calls the application command, which owns the client dependency (move the inject into submit-change-request.ts or a thin registratie adapter, matching how other commands get the client)
  • src/app/shared/infrastructure/role.ts + new home for the Role type + the letter-composer.component.ts / brief.store.ts import sites

Steps

  1. Rewrite dashboard-view.dto.ts as pure wire shapes (no imports at all); update the adapter's parse*/toDomain to map wire → domain; run the adapter spec.
  2. Move the Role type; update import sites (mechanical; lint will find them).
  3. Refactor change-request-form: UI keeps dispatching to the machine; the submit command receives/owns the ApiClient (pattern: other submit-*.ts commands).
  4. Add eslint zones:
    • src/app/**/contracts/** may import nothing from @angular/*, any @* context alias, or relative app code.
    • @shared/infrastructure/api-client importable only from **/infrastructure/** (+ the app config/provider and the generated file itself).
  5. Lint the repo; there must be zero new disables.

Acceptance criteria

  • dashboard-view.dto.ts has no import statements.
  • No value import of @shared/infrastructure/api-client outside infrastructure/ (the only remaining non-infra reference is draft-sync's type-only DTO import, allowed by allowTypeImports). ApiClient confinement is lint-enforced.
  • Both eslint rules active; planted violations fail lint (contracts→domain, UI→ApiClient value); type-only DTO import still passes.
  • Change-request flow works — verified end-to-end in the running app (submit → command → adapter → backend → reference BIG-2026-…, success alert); specs green.

Verification

GREEN + npm run test-storybook:ci. Manual smoke: npm start → Gegevens wijzigen → submit a change request.

Out of scope

The ui ↛ infrastructure rule for adapter injections in wizards — that's WP-04.

Risks

Role-type move ripples through brief imports — mechanical; run tests before/after.