Files
atomic-design-poc/docs/project/refactor-backlog-setup/refactor-backlog/implementation/rb-24.md
T
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

12 KiB

RB-24 — libs/shared/upload moves into infrastructure//domain//application/; the depcruise carve-out is deleted

Status: implemented · 2026-08-27 · Source finding: 06-adr-conformance.md ADR-C-002 · 99-backlog.md RB-24, "Merges" table row for RB-25/26/27

What was wrong

libs/shared/src/upload/ held five files outside the folder-per-layer convention every other context follows. upload.adapter.ts injects ApiClient and opens a raw XMLHttpRequest — a genuine network adapter — yet sat outside infrastructure/. upload.machine.ts was the only Elm-style machine (of 9 in the repo) outside a domain/ folder. The exception was hard-coded into the enforcement itself: .dependency-cruiser.base.js's apiclient-infrastructure-only rule read from: { pathNot: '/infrastructure/|^libs/shared/src/upload/' } — carved around the violation instead of the violation being fixed, which is why the baseline scan reported 0 violations despite this.

What changed

From libs/shared/src/upload/ To
upload.adapter.ts libs/shared/src/infrastructure/upload.adapter.ts
upload.machine.ts + .spec.ts libs/shared/src/domain/upload.machine.ts (+ spec)
upload-controller.ts libs/shared/src/application/upload-controller.ts
upload-shell.service.ts libs/shared/src/application/upload-shell.service.ts

All five moves used git mv. libs/shared/src/upload/ no longer exists.

Import updates. 24 consumer files import from @shared/upload/* (found with grep -rln "shared/upload" apps libs --include=*.ts, filtered to exclude the unrelated @shared/ui/upload/* component folder, which was not touched). All 24 files' import paths were rewritten to the new locations (30 import statements total, some files import more than one symbol). No export was renamed, no file was split, no logic changed in any of these 24 files beyond the import path string.

Within the five moved files, three had relative imports (./upload.adapter, ./upload.machine) that now crossed layers and were rewritten to @shared/* aliases: upload.adapter.ts's import of DocumentCategory from ./upload.machine@shared/domain/upload.machine; upload-controller.ts's imports of UploadAdapter and upload.machine symbols → @shared/infrastructure/... / @shared/domain/...; upload-shell.service.ts likewise. upload.machine.spec.ts needed no import change — it and upload.machine.ts moved into the same domain/ folder together, so its ./upload.machine import stayed correct; git diff --find-renames confirms this file as a 0-line-changed pure rename.

The carve-out. .dependency-cruiser.base.js's apiclient-infrastructure-only rule: from: { pathNot: '/infrastructure/|^libs/shared/src/upload/' }from: { pathNot: '/infrastructure/' }, comment updated to drop the now-false "(+ shared/upload)" parenthetical. One further consequence: docs/reference/architecture/dependencies.md's "Atomic-layer rules" paragraph stated the same carve-out in prose ("the generated ApiClient is a value only inside infrastructure/ (+ libs/shared/src/upload)") — corrected in the same diff, since leaving it would document a rule that no longer exists.

A second, real violation the move exposed — fixed, not just reported

Deleting the carve-out did not by itself make dep:check pass. A separate, pre-existing rule — ui-not-infrastructure (ui/+layout/ may not import infrastructure/ as a value) — had never fired against upload.adapter.ts, because before this move the file's path did not contain /infrastructure/ at all. Three UI components were injecting UploadAdapter directly: apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts, apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts, and apps/ssp/src/app/brief/ui/org-template.page.ts. Once upload.adapter.ts physically moved into infrastructure/, dep:check correctly flagged all three:

error ui-not-infrastructure: .../registratie-wizard.component.ts → libs/shared/src/infrastructure/upload.adapter.ts
error ui-not-infrastructure: .../herregistratie-wizard.component.ts → libs/shared/src/infrastructure/upload.adapter.ts
error ui-not-infrastructure: .../org-template.page.ts → libs/shared/src/infrastructure/upload.adapter.ts

This is judged in-scope to fix, not a second unrelated finding to merely report, for three reasons. First, the ticket's own DoD is explicit: "if dep:check fails after the deletion, the move is incomplete, so fix the move rather than restoring the clause." Second, all three call sites used UploadAdapter for exactly one thing — .contentUrl(documentId), a thin wrapper around the adapter's own already-exported, injection-free pure function uploadContentUrl(documentId) (its doc comment: "Pure (no injection) so a store can build a letterhead-logo src without pulling ApiClient into its dependency graph" — written for precisely this case). apps/ssp/src/app/brief/application/brief.store.ts already used that pure function directly; the three UI files had independently reinvented inject(UploadAdapter) + .contentUrl() instead. Third, the fix is mechanical and stays inside the ADR's own established idiom — no new architecture, no touch to any RB-25/26/27 target:

  • libs/shared/src/application/upload-controller.ts — the object createUploadController returns gained one more method, previewUrlFor(documentId), built on the existing pure uploadContentUrl. Both wizard components already hold a createUploadController instance (uploadCtl) for their other upload effects; their previewUrlFor field now delegates to uploadCtl.previewUrlFor instead of injecting UploadAdapter itself.
  • apps/ssp/src/app/brief/application/org-template.store.ts (already injects UploadAdapter legitimately — it's application layer) gained one more computed-style field, previewUrlFor, on the same pure uploadContentUrl. org-template.page.ts now reads this.store.previewUrlFor instead of injecting UploadAdapter.

No behaviour changed: uploadContentUrl(id) and uploadAdapter.contentUrl(id) return the identical string (the method is a one-line pass-through to the function), and the demo-* short-circuit in the two wizards moved into upload-controller.ts's new method verbatim.

Verification

  • upload.machine.spec.ts passes unchanged. git diff --find-renames=30% shows it as a 0-insertion/0-deletion pure rename — no content changed, including its own imports (both files moved into domain/ together, so its ./upload.machine relative import needed no edit). No spec content changed anywhere in this ticket.
  • npm run dep:check: passes for both apps with the carve-out clause removed — ✔ no dependency violations found (344 modules, 1200 dependencies cruised) (ssp), ✔ no dependency violations found (226 modules, 588 dependencies cruised) (behandelportal).
  • npm run lint: clean.
  • npm test: 43+6+24+4 = 77 test files, 274+37+138+23 = 472 tests, all passing (ssp / behandelportal / shared / beheer).
  • npm run build: both apps build (pre-existing, unrelated warnings about /cibg-huisstijl/css/huisstijl.min.css and /letter.css not being found at build time — present before this ticket, vendored assets resolved at serve/deploy time, not a regression from this move).
  • Coverage, libs/shared/src/domain/ (npm run test:coverage narrowed to shared): the folder now includes upload.machine.ts at 98.82% statements / 91.8% branches / 100% functions / 98.36% lines (84/85, 56/61, 28/28, 60/61) — the "well-specced machine" ADR-C-002 predicted landing in a folder the baseline reported at "0% spec reach across 3 files" (capability.ts, feature-flag.ts, role.ts, which this ticket does not touch and which remain unspecced — that gap is pre-existing and out of this ticket's scope).

Non-TypeScript references to the old path — findings

Checked .storybook-ssp/, .storybook-behandelportal/, angular.json, no vitest config file exists separately (Angular's builder owns test config), both .dependency-cruiser.*.js files, and libs/shared/docs/*.mdx.

  • Storybook config, angular.json, dependency-cruiser app configs: no reference to shared/upload or libs/shared/src/upload in any of these. Nothing to change.
  • .dependency-cruiser.base.js: the one real reference — the carve-out clause itself, deleted (see above).
  • docs/reference/architecture/dependencies.md: one prose reference to the same carve-out, corrected in this diff (see above) since it directly describes the rule this ticket edits.
  • libs/shared/docs/*.mdx: no .mdx file references libs/shared/src/upload or @shared/upload. atomic-design.mdx and machines.mdx mention upload.machine.ts and shared/ui/upload/... by filename/short-path only, never the full old directory path — both remain accurate (the filename didn't change; ui/upload/ is the untouched sibling folder).
  • apps/ssp/src/locale/messages.xlf, messages.en.xlf, apps/behandelportal/src/locale/messages.en.xlf: each carries a handful of <context context-type="sourcefile">src/app/shared/upload/upload.machine.ts</context> /upload.adapter.ts annotations — auto-generated by Angular's $localize extractor, informational only (they tell a translator where a string originated; they are not read by the build or by i18nMissingTranslation). Left as-is: regenerating them is npm run extract-i18n's job for the source-locale file and does not touch the hand-maintained messages.en.xlf translations at all, and this ticket's scope is the move plus import updates, not a translation-tooling refresh. They will self-correct the next time extract-i18n runs for an unrelated reason.
  • docs/project/backlog/*.md, docs/project/refactor-backlog-setup/refactor-backlog/*.md: several planning/history documents (WP-25, WP-74, the baseline scan, 02-testability.md, 06-adr-conformance.md, 07-bio2-compliance.md, 99-backlog.md, rb-01.md, rb-09.md) reference the old path — expected, since most of them describe or cite the violation this ticket resolves, as history. Not edited, except 99-backlog.md's RB-24 status cell (see below).

What RB-25/26/27 now find where

  • RB-25 (UPLOAD_TRANSPORT injection token, replacing inject(KeepaliveTransport)): KeepaliveTransport and UploadShellService are both now in libs/shared/src/application/upload-shell.service.ts (unchanged content, new path). The token belongs in application/ alongside them — nothing about the token's shape or location changes because of this move.
  • RB-26 (planFileSelection in upload.machine.ts): the machine is now libs/shared/src/domain/upload.machine.ts. createUploadController's onFileSelected callback — the accept/reject decision RB-26 targets — is in libs/shared/src/application/upload-controller.ts (also renumbered, otherwise unchanged; it also now exports one more method, previewUrlFor, added by this ticket — see above). RB-26 should extend upload.machine.ts in its new location; no import path in that file needs touching beyond what this ticket already did.
  • RB-27 (uploadOutcome(status, responseText) out of the XHR closure): the XHR closure is in libs/shared/src/infrastructure/upload.adapter.ts's xhrUpload method — same file, same method, new path only. load/error/abort handlers, parseError, and genericError are all still exactly where they were, just under infrastructure/.

npm run ci

Result and step count reported in the final answer.