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>
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 objectcreateUploadControllerreturns gained one more method,previewUrlFor(documentId), built on the existing pureuploadContentUrl. Both wizard components already hold acreateUploadControllerinstance (uploadCtl) for their other upload effects; theirpreviewUrlForfield now delegates touploadCtl.previewUrlForinstead of injectingUploadAdapteritself.apps/ssp/src/app/brief/application/org-template.store.ts(already injectsUploadAdapterlegitimately — it's application layer) gained one more computed-style field,previewUrlFor, on the same pureuploadContentUrl.org-template.page.tsnow readsthis.store.previewUrlForinstead of injectingUploadAdapter.
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.tspasses 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 intodomain/together, so its./upload.machinerelative 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.cssand/letter.cssnot 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:coveragenarrowed toshared): the folder now includesupload.machine.tsat 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/uploadorlibs/shared/src/uploadin 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.mdxfile referenceslibs/shared/src/uploador@shared/upload.atomic-design.mdxandmachines.mdxmentionupload.machine.tsandshared/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.tsannotations — auto-generated by Angular's$localizeextractor, informational only (they tell a translator where a string originated; they are not read by the build or byi18nMissingTranslation). Left as-is: regenerating them isnpm run extract-i18n's job for the source-locale file and does not touch the hand-maintainedmessages.en.xlftranslations at all, and this ticket's scope is the move plus import updates, not a translation-tooling refresh. They will self-correct the next timeextract-i18nruns 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, except99-backlog.md's RB-24 status cell (see below).
What RB-25/26/27 now find where
- RB-25 (
UPLOAD_TRANSPORTinjection token, replacinginject(KeepaliveTransport)):KeepaliveTransportandUploadShellServiceare both now inlibs/shared/src/application/upload-shell.service.ts(unchanged content, new path). The token belongs inapplication/alongside them — nothing about the token's shape or location changes because of this move. - RB-26 (
planFileSelectioninupload.machine.ts): the machine is nowlibs/shared/src/domain/upload.machine.ts.createUploadController'sonFileSelectedcallback — the accept/reject decision RB-26 targets — is inlibs/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 extendupload.machine.tsin 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 inlibs/shared/src/infrastructure/upload.adapter.ts'sxhrUploadmethod — same file, same method, new path only.load/error/aborthandlers,parseError, andgenericErrorare all still exactly where they were, just underinfrastructure/.
npm run ci
Result and step count reported in the final answer.