Files
ehoandClaude Opus 5 9520d6c24e refactor(shared): move upload/ into infrastructure/domain/application (RB-24)
libs/shared/src/upload/ held a network adapter, an Elm machine, and two
application-layer coordinators outside the folder-per-layer convention every
other context follows. The dependency-cruiser rule carved an exception around
the misplaced adapter instead of the violation being fixed.

Move all five files to the layer each belongs to (git mv), update every
import across 24 consumer files, then delete the carve-out clause from
.dependency-cruiser.base.js. No export renamed, no file split, no spec
content changed.

Deleting the carve-out exposed a second, pre-existing rule violation:
ui-not-infrastructure had never fired against upload.adapter.ts because its
old path did not match /infrastructure/. Three UI components injected
UploadAdapter directly for its one-line contentUrl() wrapper. Route each
through the existing pure uploadContentUrl() function via the application
layer (upload-controller's new previewUrlFor, OrgTemplateStore's new
previewUrlFor) instead — the same idiom brief.store.ts already used.

npm run ci passes; dep:check is clean for both apps with the carve-out gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 20:40:41 +02:00

4.1 KiB
Raw Permalink Blame History

Dependencies & boundaries

How the app's bounded-context and atomic-layer boundaries are declared, enforced, and visualized (WP-38, generalized for the two-app monorepo by WP-67). One declarative source — .dependency-cruiser.base.js (a rule factory) plus one thin .dependency-cruiser.<app>.js per app — both guards the edges and draws the graph, replacing the per-context no-restricted-imports blocks that previously had to be hand-copied (and that had left herregistratie uncovered). Each app is cruised separately, against its own tsconfig.json: a single merged tsconfig can't resolve both apps' @auth/* alias at once (each points at a different physical directory), so there is no single global config file.

The rules (single source: .dependency-cruiser.base.js)

Bounded-context direction — dependencies point inward; every app context may use libs/shared and libs/beheer, nothing imports showcase:

Context (apps/ssp) May import
auth libs/shared, libs/beheer
registratie libs/shared, libs/beheer
herregistratie registratie, libs/shared, libs/beheer
brief libs/shared, libs/beheer
showcase everything in apps/ssp (sanctioned teaching page)
Context (apps/behandelportal) May import
auth libs/shared, libs/beheer
behandeling libs/shared, libs/beheer

libs/shared is the base (no feature context, no libs/beheer — that direction is forbidden too, so shared never grows a dependency on a peer library). libs/beheer is a real bounded context (admin/stamdata), used identically by both apps — it may depend on libs/shared, never the reverse. An app may not import the other app's source directly.

Atomic-layer rules: domain/ is framework-free (no Angular); contracts/ import nothing (pure wire DTOs, ADR-0001); ui/ + layout/ never import infrastructure/ directly (reach data through an application store/command — type-only DTO imports are fine); the generated ApiClient is a value only inside infrastructure/. Plus no circular dependencies. These apply uniformly across an app's tree and both libraries — no debug-state exception anymore (WP-67 moved the dev panel component out of libs/shared into apps/ssp since it's genuinely SSP-specific, coupled to BigProfileStore; the shared ShellComponent hosts whichever app-provided component the DEBUG_PANEL injection token supplies, or none).

See the graph

npm run dep:graph      # regenerates docs/reference/architecture/dependency-graph.md (mermaid) — one diagram per app

dependency-graph.md is the generated, committed view — contexts × atomic layers, edges are real imports. It renders on the git host; regenerate + commit after a structural change.

Enforce

npm run dep:check      # runs both apps' configs; fails on any forbidden edge; part of `npm run ci` and CI

A violation prints the offending from → to and the rule name. dep:check runs in the local gate (scripts/ci-local.sh) and the frontend CI job.

What still lives in ESLint

Only the non-dependency rules: @typescript-eslint/no-explicit-any and the angular-eslint template accessibility bundle (see eslint.config.mjs, scoped to {apps,libs}/**). Everything about who may import whom is in dependency-cruiser.

Adding a context

Add one context entry to the relevant app's object literal in .dependency-cruiser.ssp.js (or .dependency-cruiser.behandelportal.js) — passed straight into the shared buildConfig factory in .dependency-cruiser.base.js (and the tsconfig path alias + lazy route) — no more hand-copying ESLint blocks. The new-context skill / gen:context (plopfile.mjs, ssp-only today) covers the full checklist.