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>
4.1 KiB
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.