Merge RB-24 — move upload/ into its proper layers, delete the carve-out
ADR-C-002: libs/shared/src/upload/ held a network adapter outside infrastructure/ and the only Elm machine outside a domain/ folder, and the dependency-cruiser rule was written around the violation rather than the violation being fixed. The five files move to infrastructure/, domain/ and application/, and the ^libs/shared/src/upload/ carve-out is gone. Deleting the carve-out exposed a second, real violation that the old path had hidden from the ui-not-infrastructure rule: three UI components injected UploadAdapter for nothing but a one-line wrapper over its own exported pure uploadContentUrl. They now read previewUrlFor from their application-layer collaborator. dep:check passes for both apps with the clause removed, which is the ticket's acceptance criterion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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'],
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) —
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.`;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+6
-7
@@ -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<WizardState, WizardMsg>(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<WizardState>(initial);
|
||||
|
||||
+1
-1
@@ -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: [] };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<RegistratieState, RegistratieMsg>(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<RegistratieState>(initial);
|
||||
|
||||
@@ -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<Draft> = {
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 `<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.
|
||||
@@ -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
|
||||
|
||||
+17
-2
@@ -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;
|
||||
+7
-2
@@ -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
|
||||
+1
-1
@@ -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 {
|
||||
+1
-1
@@ -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. */
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<DocumentCategoryComponent> = {
|
||||
|
||||
@@ -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<UploadStatus['type'], string> = {
|
||||
|
||||
@@ -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<DocumentChipComponent> = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<DocumentUploadComponent> = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<SingleUploadComponent> = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user