diff --git a/.dependency-cruiser.base.js b/.dependency-cruiser.base.js index ff945cd..b6f1e0b 100644 --- a/.dependency-cruiser.base.js +++ b/.dependency-cruiser.base.js @@ -100,9 +100,9 @@ module.exports = function buildConfig(contextAllowed, appName, tsConfigFileName) { name: 'apiclient-infrastructure-only', comment: - 'The generated ApiClient is a value only inside infrastructure/ (+ shared/upload); elsewhere type-only.', + 'The generated ApiClient is a value only inside infrastructure/; elsewhere type-only.', severity: 'error', - from: { pathNot: '/infrastructure/|^libs/shared/src/upload/' }, + from: { pathNot: '/infrastructure/' }, to: { path: '^libs/shared/src/infrastructure/api-client\\.ts$', dependencyTypesNot: ['type-only'], diff --git a/apps/ssp/src/app/brief/application/brief.store.ts b/apps/ssp/src/app/brief/application/brief.store.ts index 6bb8073..623535a 100644 --- a/apps/ssp/src/app/brief/application/brief.store.ts +++ b/apps/ssp/src/app/brief/application/brief.store.ts @@ -19,7 +19,7 @@ import { OrgTemplate } from '@brief/domain/org-template'; import { BRIEF_LOAD_FAILED, BriefAdapter, BriefView } from '@brief/infrastructure/brief.adapter'; import { LetterPreviewAdapter } from '@brief/infrastructure/letter-preview.adapter'; import { RevealBigNummerAdapter } from '@brief/infrastructure/reveal-bignummer.adapter'; -import { uploadContentUrl } from '@shared/upload/upload.adapter'; +import { uploadContentUrl } from '@shared/infrastructure/upload.adapter'; import { PendingSave, registerPendingSave } from '@shared/application/pending-saves'; /** diff --git a/apps/ssp/src/app/brief/application/org-template.store.ts b/apps/ssp/src/app/brief/application/org-template.store.ts index 6135944..c78213f 100644 --- a/apps/ssp/src/app/brief/application/org-template.store.ts +++ b/apps/ssp/src/app/brief/application/org-template.store.ts @@ -3,9 +3,9 @@ import { createStore } from '@shared/application/store'; import { ActionState, SaveState } from '@shared/application/action-state'; import { createDebouncedSave } from '@shared/application/debounced-save'; import { machineRemoteData } from '@shared/application/machine-remote-data'; -import { UploadAdapter } from '@shared/upload/upload.adapter'; -import { UploadShellService } from '@shared/upload/upload-shell.service'; -import { UploadMsg, initialUpload, rejectReason } from '@shared/upload/upload.machine'; +import { UploadAdapter, uploadContentUrl } from '@shared/infrastructure/upload.adapter'; +import { UploadShellService } from '@shared/application/upload-shell.service'; +import { UploadMsg, initialUpload, rejectReason } from '@shared/domain/upload.machine'; import { MARGIN_MAX_MM, MARGIN_MIN_MM, @@ -72,6 +72,9 @@ export class OrgTemplateStore implements PendingSave { return id ? this.uploadAdapter.contentUrl(id) : null; }); + /** Preview/download link for any completed upload in the editor's document list. */ + readonly previewUrlFor = (documentId: string): string | undefined => uploadContentUrl(documentId); + /** Client-side mirror of the server rules (`OrgTemplateRules`) for instant feedback; the server re-validates and stays the authority — publish is gated on this. */ readonly draftValid = computed(() => { diff --git a/apps/ssp/src/app/brief/domain/org-template.machine.spec.ts b/apps/ssp/src/app/brief/domain/org-template.machine.spec.ts index fb223fa..1b8f24b 100644 --- a/apps/ssp/src/app/brief/domain/org-template.machine.spec.ts +++ b/apps/ssp/src/app/brief/domain/org-template.machine.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { expectTag } from '@shared/testing/expect-tag'; import { OrgTemplate, OrgTemplateAdminView } from './org-template'; import { OrgTemplateState, reduce } from './org-template.machine'; -import { DocumentCategory } from '@shared/upload/upload.machine'; +import { DocumentCategory } from '@shared/domain/upload.machine'; const template: OrgTemplate = { subOrgId: 'cibg-registers', diff --git a/apps/ssp/src/app/brief/domain/org-template.machine.ts b/apps/ssp/src/app/brief/domain/org-template.machine.ts index de90a36..7932941 100644 --- a/apps/ssp/src/app/brief/domain/org-template.machine.ts +++ b/apps/ssp/src/app/brief/domain/org-template.machine.ts @@ -1,6 +1,6 @@ import { assertNever } from '@shared/kernel/fp'; import { Margins, OrgTemplate, OrgTemplateAdminView, OrgTemplateVersion } from './org-template'; -import { UploadMsg, UploadState, initialUpload, reduceUpload } from '@shared/upload/upload.machine'; +import { UploadMsg, UploadState, initialUpload, reduceUpload } from '@shared/domain/upload.machine'; /** * The admin org-template editor as one Elm-style machine (WP-26, PRD Brief v2 §5) — diff --git a/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts b/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts index b9b8c98..2690e0f 100644 --- a/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts +++ b/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts @@ -5,7 +5,7 @@ import { ButtonComponent } from '@shared/ui/button/button.component'; import { AlertComponent } from '@shared/ui/alert/alert.component'; import { FileInputComponent } from '@shared/ui/upload/file-input/file-input.component'; import { SingleUploadComponent } from '@shared/ui/upload/single-upload/single-upload.component'; -import { UploadState } from '@shared/upload/upload.machine'; +import { UploadState } from '@shared/domain/upload.machine'; import { Brief } from '@brief/domain/brief'; import { MARGIN_MAX_MM, diff --git a/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.stories.ts b/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.stories.ts index 2b4d987..11fca36 100644 --- a/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.stories.ts +++ b/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.stories.ts @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/angular'; import { OrgTemplateEditorComponent } from './org-template-editor.component'; import { OrgTemplate, OrgTemplateVersion, SubOrgSummary } from '@brief/domain/org-template'; -import { UploadState, initialUpload } from '@shared/upload/upload.machine'; +import { UploadState, initialUpload } from '@shared/domain/upload.machine'; const draft: OrgTemplate = { subOrgId: 'cibg-registers', diff --git a/apps/ssp/src/app/brief/ui/org-template.page.ts b/apps/ssp/src/app/brief/ui/org-template.page.ts index f28b7b4..697f19d 100644 --- a/apps/ssp/src/app/brief/ui/org-template.page.ts +++ b/apps/ssp/src/app/brief/ui/org-template.page.ts @@ -4,7 +4,6 @@ import { AlertComponent } from '@shared/ui/alert/alert.component'; import { ButtonComponent } from '@shared/ui/button/button.component'; import { ASYNC } from '@shared/ui/async/async.component'; import { AccessStore } from '@shared/application/access.store'; -import { UploadAdapter } from '@shared/upload/upload.adapter'; import { OrgTemplateStore } from '@brief/application/org-template.store'; import { OrgTemplateEditorComponent } from '@brief/ui/org-template-editor/org-template-editor.component'; @@ -86,10 +85,9 @@ import { OrgTemplateEditorComponent } from '@brief/ui/org-template-editor/org-te export class OrgTemplatePage { protected store = inject(OrgTemplateStore); protected access = inject(AccessStore); - private uploadAdapter = inject(UploadAdapter); protected canEdit = computed(() => this.access.can('orgtemplate:edit')); - protected previewUrlFor = (documentId: string) => this.uploadAdapter.contentUrl(documentId); + protected previewUrlFor = this.store.previewUrlFor; protected heading = $localize`:@@orgTemplate.page.heading:Huisstijl beheren`; protected intro = $localize`:@@orgTemplate.page.intro:Beheer per organisatieonderdeel het uiterlijk van de brief: logo, afzender, ondertekening, voettekst en marges.`; diff --git a/apps/ssp/src/app/herregistratie/domain/herregistratie.machine.ts b/apps/ssp/src/app/herregistratie/domain/herregistratie.machine.ts index 2c457a7..ffed921 100644 --- a/apps/ssp/src/app/herregistratie/domain/herregistratie.machine.ts +++ b/apps/ssp/src/app/herregistratie/domain/herregistratie.machine.ts @@ -7,7 +7,7 @@ import { reduceUpload, requiredCategoriesSatisfied, deliveryRefs, -} from '@shared/upload/upload.machine'; +} from '@shared/domain/upload.machine'; /** What the user is typing (raw, possibly invalid). */ export interface Draft { diff --git a/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts b/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts index f2cc4ad..986dae0 100644 --- a/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts +++ b/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts @@ -23,9 +23,8 @@ import { } from '@herregistratie/domain/herregistratie.machine'; import { createDraftSync } from '@registratie/application/draft-sync'; import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component'; -import { createUploadController } from '@shared/upload/upload-controller'; -import { UploadAdapter } from '@shared/upload/upload.adapter'; -import { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload.machine'; +import { createUploadController } from '@shared/application/upload-controller'; +import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.machine'; /** Organism: multi-step herregistratie wizard. ALL state lives in one signal driven by the pure `reduce` function (see herregistratie.machine.ts) via an @@ -149,13 +148,13 @@ import { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload. }) export class HerregistratieWizardComponent { private profile = inject(BigProfileStore); - private uploadAdapter = inject(UploadAdapter); private store = createStore(initial, reduce); - /** Preview/download link for a completed upload; dev-simulation `demo-*` ids have - no stored bytes, so they get no link. */ + /** Preview/download link for a completed upload; delegates to the upload + controller (application layer), which knows the dev-simulation `demo-*` ids + have no stored bytes and returns no link for them. */ protected previewUrlFor = (documentId: string): string | undefined => - documentId.startsWith('demo-') ? undefined : this.uploadAdapter.contentUrl(documentId); + this.uploadCtl.previewUrlFor(documentId); /** Optional seed so Storybook / the showcase can mount any state directly. */ seed = input(initial); diff --git a/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.stories.ts b/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.stories.ts index f876c5e..4a632a3 100644 --- a/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.stories.ts +++ b/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.stories.ts @@ -4,7 +4,7 @@ import { provideHttpClient } from '@angular/common/http'; import { provideApiClient } from '@shared/infrastructure/api-client.provider'; import { HerregistratieWizardComponent } from './herregistratie-wizard.component'; import { WizardState } from '@herregistratie/domain/herregistratie.machine'; -import { initialUpload } from '@shared/upload/upload.machine'; +import { initialUpload } from '@shared/domain/upload.machine'; import { Uren } from '@registratie/domain/value-objects/uren'; const validData = { uren: 4160 as Uren, jaren: 5, punten: 200, documents: [] }; diff --git a/apps/ssp/src/app/registratie/domain/registratie-wizard.machine.spec.ts b/apps/ssp/src/app/registratie/domain/registratie-wizard.machine.spec.ts index 695b314..30aac1a 100644 --- a/apps/ssp/src/app/registratie/domain/registratie-wizard.machine.spec.ts +++ b/apps/ssp/src/app/registratie/domain/registratie-wizard.machine.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { ok, err } from '@shared/kernel/fp'; -import { initialUpload } from '@shared/upload/upload.machine'; +import { initialUpload } from '@shared/domain/upload.machine'; import { expectTag } from '@shared/testing/expect-tag'; import { Draft, diff --git a/apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts b/apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts index 73b96d4..069178a 100644 --- a/apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts +++ b/apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts @@ -9,7 +9,7 @@ import { reduceUpload, requiredCategoriesSatisfied, deliveryRefs, -} from '@shared/upload/upload.machine'; +} from '@shared/domain/upload.machine'; /** * A FIXED 3-step registration wizard. The steps never change in number (always diff --git a/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts b/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts index aa2b60a..25e3b19 100644 --- a/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts +++ b/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts @@ -37,9 +37,8 @@ import { } from '@registratie/domain/registratie-wizard.machine'; import { createDraftSync } from '@registratie/application/draft-sync'; import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component'; -import { createUploadController } from '@shared/upload/upload-controller'; -import { UploadAdapter } from '@shared/upload/upload.adapter'; -import { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload.machine'; +import { createUploadController } from '@shared/application/upload-controller'; +import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.machine'; const KANALEN = [ { value: 'email', label: $localize`:@@registratie.kanaalEmail:E-mail` }, @@ -368,13 +367,13 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid'; }) export class RegistratieWizardComponent { private lookup = inject(RegistratieLookupStore); - private uploadAdapter = inject(UploadAdapter); private store = createStore(initial, reduce); - /** Preview/download link for a completed upload; the dev-simulation `demo-*` ids - have no stored bytes, so they get no link. */ + /** Preview/download link for a completed upload; delegates to the upload + controller (application layer), which knows the dev-simulation `demo-*` ids + have no stored bytes and returns no link for them. */ protected previewUrlFor = (documentId: string): string | undefined => - documentId.startsWith('demo-') ? undefined : this.uploadAdapter.contentUrl(documentId); + this.uploadCtl.previewUrlFor(documentId); /** Optional seed so Storybook / tests can mount any state directly. */ seed = input(initial); diff --git a/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.stories.ts b/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.stories.ts index 5fd9ef4..29f31db 100644 --- a/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.stories.ts +++ b/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.stories.ts @@ -8,7 +8,7 @@ import { RegistratieState, ValidRegistratie, } from '@registratie/domain/registratie-wizard.machine'; -import { initialUpload } from '@shared/upload/upload.machine'; +import { initialUpload } from '@shared/domain/upload.machine'; import { Postcode } from '@registratie/domain/value-objects/postcode'; const adres: Partial = { diff --git a/docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md b/docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md index fe54364..d55b046 100644 --- a/docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md +++ b/docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md @@ -125,7 +125,7 @@ Every ticket tracing to a `BIO-` finding, plus every row on agent 07's authorita | **RB-21** | ssp/registratie | CQRS-light | Extract the read half of `createDraftSync` into `application/find-concept.ts` | §4a `createDraftSync` 143 lines — longest fn in the repo; §9 (>40) | M | Med | P2 | 4 | — | — | **done** | | **RB-22** | ssp/brief | CQRS-light | _(expand)_ `BriefStore.load()` tolerates a 404 by calling the existing `reset()` once | BL-003; §7 Backend CQRS-light row | S | Low | P2 | 4 | — | **SIGN-OFF** | **done** | | **RB-23** | backend/Program.cs + Data | CQRS-light | _(contract)_ `GET /brief` 404s when absent; `GetOrCreate` → `Get` | BL-003; §7 Backend CQRS-light row | S | Med | P2 | 4 | RB-22 | **SIGN-OFF** | **done** | -| **RB-24** | libs/shared/upload | ADR conform. | Move `upload/` into `infrastructure`/`domain`/`application`; **delete** the depcruise carve-out | BL-010; §7 "+1 adapter outside `infrastructure/`", "8 of 9 machines in `domain/`"; §3b shared/domain 0% reach | M | Med | P2 | 5 | — | **SIGN-OFF** | open | +| **RB-24** | libs/shared/upload | ADR conform. | Move `upload/` into `infrastructure`/`domain`/`application`; **delete** the depcruise carve-out | BL-010; §7 "+1 adapter outside `infrastructure/`", "8 of 9 machines in `domain/`"; §3b shared/domain 0% reach | M | Med | P2 | 5 | — | **SIGN-OFF** | **done** | | **RB-25** | libs/shared/upload | testability | `UPLOAD_TRANSPORT` injection token (the `SESSION_PORT` shape) instead of `inject(KeepaliveTransport)` | §3a upload 52.0%/50.0%; §3b file unreached, non-`ui/` | S | Low | P2 | 5 | RB-24 | **SIGN-OFF** | open | | **RB-26** | libs/shared/upload | testability | Move the accept/reject decision to `planFileSelection` in `upload.machine.ts` | §3a upload 52.0%/50.0%; §4a module max CC 27 | S | Low | P2 | 5 | RB-24 | **SIGN-OFF** | open | | **RB-27** | libs/shared/upload | testability | Extract `uploadOutcome(status, responseText)` out of the XHR closure | file LH 5/64 (**7.8% line**), BRH 3/57 (**5.3% branch**) | S–M | Low | P2 | 5 | RB-25 | **SIGN-OFF** | open | diff --git a/docs/project/refactor-backlog-setup/refactor-backlog/implementation/rb-24.md b/docs/project/refactor-backlog-setup/refactor-backlog/implementation/rb-24.md new file mode 100644 index 0000000..599034b --- /dev/null +++ b/docs/project/refactor-backlog-setup/refactor-backlog/implementation/rb-24.md @@ -0,0 +1,178 @@ +# 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 `src/app/shared/upload/upload.machine.ts` + /`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. diff --git a/docs/reference/architecture/dependencies.md b/docs/reference/architecture/dependencies.md index 8b97476..cb878e2 100644 --- a/docs/reference/architecture/dependencies.md +++ b/docs/reference/architecture/dependencies.md @@ -35,7 +35,7 @@ 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/` (+ `libs/shared/src/upload`). Plus **no circular** +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 diff --git a/libs/shared/src/upload/upload-controller.ts b/libs/shared/src/application/upload-controller.ts similarity index 88% rename from libs/shared/src/upload/upload-controller.ts rename to libs/shared/src/application/upload-controller.ts index cf6cb56..7fa1522 100644 --- a/libs/shared/src/upload/upload-controller.ts +++ b/libs/shared/src/application/upload-controller.ts @@ -1,9 +1,19 @@ import { DestroyRef, effect, inject } from '@angular/core'; -import { CategoryParams, UploadAdapter } from './upload.adapter'; +import { + CategoryParams, + UploadAdapter, + uploadContentUrl, +} from '@shared/infrastructure/upload.adapter'; import { UploadShellService } from './upload-shell.service'; import { problemDetail } from '@shared/infrastructure/api-error'; import { SUBMIT_FAILED } from '@shared/application/submit'; -import { DeliveryChannel, UploadMsg, UploadState, inFlight, rejectReason } from './upload.machine'; +import { + DeliveryChannel, + UploadMsg, + UploadState, + inFlight, + rejectReason, +} from '@shared/domain/upload.machine'; export interface UploadControllerDeps { wizardId: string; @@ -59,6 +69,11 @@ export function createUploadController(deps: UploadControllerDeps) { } return { + /** Preview/download link for a completed upload; the dev-simulation `demo-*` ids + have no stored bytes, so they get no link. */ + previewUrlFor(documentId: string): string | undefined { + return documentId.startsWith('demo-') ? undefined : uploadContentUrl(documentId); + }, onFileSelected(categoryId: string, selected: File[]) { const cat = deps.getUpload().categories.find((c) => c.categoryId === categoryId); if (!cat) return; diff --git a/libs/shared/src/upload/upload-shell.service.ts b/libs/shared/src/application/upload-shell.service.ts similarity index 94% rename from libs/shared/src/upload/upload-shell.service.ts rename to libs/shared/src/application/upload-shell.service.ts index 4b88329..bab1e67 100644 --- a/libs/shared/src/upload/upload-shell.service.ts +++ b/libs/shared/src/application/upload-shell.service.ts @@ -1,7 +1,12 @@ import { Injectable, inject } from '@angular/core'; -import { UploadAdapter, XhrUploadRequest, XhrUploadHandle, UPLOAD_ABORTED } from './upload.adapter'; +import { + UploadAdapter, + XhrUploadRequest, + XhrUploadHandle, + UPLOAD_ABORTED, +} from '@shared/infrastructure/upload.adapter'; import { problemDetail } from '@shared/infrastructure/api-error'; -import { UploadMsg, Upload } from './upload.machine'; +import { UploadMsg, Upload } from '@shared/domain/upload.machine'; /** * Transport seam (PRD §6): how upload bytes leave the browser. The shipped impl is diff --git a/libs/shared/src/upload/upload.machine.spec.ts b/libs/shared/src/domain/upload.machine.spec.ts similarity index 100% rename from libs/shared/src/upload/upload.machine.spec.ts rename to libs/shared/src/domain/upload.machine.spec.ts diff --git a/libs/shared/src/upload/upload.machine.ts b/libs/shared/src/domain/upload.machine.ts similarity index 100% rename from libs/shared/src/upload/upload.machine.ts rename to libs/shared/src/domain/upload.machine.ts diff --git a/libs/shared/src/upload/upload.adapter.ts b/libs/shared/src/infrastructure/upload.adapter.ts similarity index 99% rename from libs/shared/src/upload/upload.adapter.ts rename to libs/shared/src/infrastructure/upload.adapter.ts index a0d1620..0cb0e7e 100644 --- a/libs/shared/src/upload/upload.adapter.ts +++ b/libs/shared/src/infrastructure/upload.adapter.ts @@ -8,7 +8,7 @@ import { problemDetail } from '@shared/infrastructure/api-error'; import { currentScenario } from '@shared/infrastructure/scenario'; import { currentSubject } from '@shared/infrastructure/subject'; import { environment } from '@shared/environments/environment'; -import { DocumentCategory } from './upload.machine'; +import { DocumentCategory } from '@shared/domain/upload.machine'; /** Answer-derived query params that affect which categories the server presents. */ export interface CategoryParams { diff --git a/libs/shared/src/ui/upload/delivery-channel-toggle/delivery-channel-toggle.component.ts b/libs/shared/src/ui/upload/delivery-channel-toggle/delivery-channel-toggle.component.ts index 41fab35..59d2bc4 100644 --- a/libs/shared/src/ui/upload/delivery-channel-toggle/delivery-channel-toggle.component.ts +++ b/libs/shared/src/ui/upload/delivery-channel-toggle/delivery-channel-toggle.component.ts @@ -1,5 +1,5 @@ import { Component, input, output } from '@angular/core'; -import type { DeliveryChannel } from '@shared/upload/upload.machine'; +import type { DeliveryChannel } from '@shared/domain/upload.machine'; /** Atom: choose how a document is delivered — uploaded digitally or sent by post. Thin wrapper over the Utrecht/RHC radio CSS. Pure UI: emits the chosen channel. */ diff --git a/libs/shared/src/ui/upload/document-category/document-category.component.ts b/libs/shared/src/ui/upload/document-category/document-category.component.ts index 33193bb..6540530 100644 --- a/libs/shared/src/ui/upload/document-category/document-category.component.ts +++ b/libs/shared/src/ui/upload/document-category/document-category.component.ts @@ -1,5 +1,5 @@ import { Component, computed, input, output } from '@angular/core'; -import type { DeliveryChannel, DocumentCategory, Upload } from '@shared/upload/upload.machine'; +import type { DeliveryChannel, DocumentCategory, Upload } from '@shared/domain/upload.machine'; import { DeliveryChannelToggleComponent } from '../delivery-channel-toggle/delivery-channel-toggle.component'; import { FileInputComponent } from '../file-input/file-input.component'; import { SingleUploadComponent } from '../single-upload/single-upload.component'; diff --git a/libs/shared/src/ui/upload/document-category/document-category.stories.ts b/libs/shared/src/ui/upload/document-category/document-category.stories.ts index a82275f..1bc2aed 100644 --- a/libs/shared/src/ui/upload/document-category/document-category.stories.ts +++ b/libs/shared/src/ui/upload/document-category/document-category.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/angular'; -import type { DocumentCategory, Upload } from '@shared/upload/upload.machine'; +import type { DocumentCategory, Upload } from '@shared/domain/upload.machine'; import { DocumentCategoryComponent } from './document-category.component'; const meta: Meta = { diff --git a/libs/shared/src/ui/upload/document-chip/document-chip.component.ts b/libs/shared/src/ui/upload/document-chip/document-chip.component.ts index e3a10b8..b5053e3 100644 --- a/libs/shared/src/ui/upload/document-chip/document-chip.component.ts +++ b/libs/shared/src/ui/upload/document-chip/document-chip.component.ts @@ -1,5 +1,5 @@ import { Component, computed, input } from '@angular/core'; -import type { UploadStatus } from '@shared/upload/upload.machine'; +import type { UploadStatus } from '@shared/domain/upload.machine'; import { UploadStatusIconComponent } from '../upload-status-icon/upload-status-icon.component'; const STATUS_LABELS: Record = { diff --git a/libs/shared/src/ui/upload/document-chip/document-chip.stories.ts b/libs/shared/src/ui/upload/document-chip/document-chip.stories.ts index ac1ff05..4747651 100644 --- a/libs/shared/src/ui/upload/document-chip/document-chip.stories.ts +++ b/libs/shared/src/ui/upload/document-chip/document-chip.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/angular'; -import type { UploadStatus } from '@shared/upload/upload.machine'; +import type { UploadStatus } from '@shared/domain/upload.machine'; import { DocumentChipComponent } from './document-chip.component'; const meta: Meta = { diff --git a/libs/shared/src/ui/upload/document-upload/document-upload.component.ts b/libs/shared/src/ui/upload/document-upload/document-upload.component.ts index 9aee010..eb024f1 100644 --- a/libs/shared/src/ui/upload/document-upload/document-upload.component.ts +++ b/libs/shared/src/ui/upload/document-upload/document-upload.component.ts @@ -1,5 +1,5 @@ import { Component, input, output } from '@angular/core'; -import type { DeliveryChannel, UploadState } from '@shared/upload/upload.machine'; +import type { DeliveryChannel, UploadState } from '@shared/domain/upload.machine'; import { AlertComponent } from '@shared/ui/alert/alert.component'; import { DocumentCategoryComponent } from '../document-category/document-category.component'; diff --git a/libs/shared/src/ui/upload/document-upload/document-upload.stories.ts b/libs/shared/src/ui/upload/document-upload/document-upload.stories.ts index 578ca75..af6fff8 100644 --- a/libs/shared/src/ui/upload/document-upload/document-upload.stories.ts +++ b/libs/shared/src/ui/upload/document-upload/document-upload.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/angular'; -import type { UploadState } from '@shared/upload/upload.machine'; +import type { UploadState } from '@shared/domain/upload.machine'; import { DocumentUploadComponent } from './document-upload.component'; const meta: Meta = { diff --git a/libs/shared/src/ui/upload/single-upload/single-upload.component.ts b/libs/shared/src/ui/upload/single-upload/single-upload.component.ts index 25454bd..6fdda98 100644 --- a/libs/shared/src/ui/upload/single-upload/single-upload.component.ts +++ b/libs/shared/src/ui/upload/single-upload/single-upload.component.ts @@ -1,5 +1,5 @@ import { Component, computed, input, output } from '@angular/core'; -import type { Upload } from '@shared/upload/upload.machine'; +import type { Upload } from '@shared/domain/upload.machine'; import { DocumentChipComponent } from '../document-chip/document-chip.component'; import { UploadProgressBarComponent } from '../upload-progress-bar/upload-progress-bar.component'; diff --git a/libs/shared/src/ui/upload/single-upload/single-upload.stories.ts b/libs/shared/src/ui/upload/single-upload/single-upload.stories.ts index 5d4ee64..f612abe 100644 --- a/libs/shared/src/ui/upload/single-upload/single-upload.stories.ts +++ b/libs/shared/src/ui/upload/single-upload/single-upload.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/angular'; -import type { Upload } from '@shared/upload/upload.machine'; +import type { Upload } from '@shared/domain/upload.machine'; import { SingleUploadComponent } from './single-upload.component'; const meta: Meta = { diff --git a/libs/shared/src/ui/upload/upload-status-icon/upload-status-icon.component.ts b/libs/shared/src/ui/upload/upload-status-icon/upload-status-icon.component.ts index b5ac079..a2cdf4a 100644 --- a/libs/shared/src/ui/upload/upload-status-icon/upload-status-icon.component.ts +++ b/libs/shared/src/ui/upload/upload-status-icon/upload-status-icon.component.ts @@ -1,5 +1,5 @@ import { Component, computed, input } from '@angular/core'; -import type { UploadStatus } from '@shared/upload/upload.machine'; +import type { UploadStatus } from '@shared/domain/upload.machine'; interface Glyph { char: string;