Decision 3 said the `@shared/ui/` occurrence count must not change at 200, while decision 4, nine lines below, mandated converting five relative imports into `@shared/ui/` aliases — five new occurrences by construction. The answer is 205, and the agent verified by hand that the 21 occurrences now inside `libs/shared/src/ui/` are the 16 from before plus exactly those five. This is the fourth instance of the pattern named in the previous docs commit, and the first ticket written after naming it. Recorded as such: when two decisions both touch the thing you are counting, add them up first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 KiB
RD-27 — Make the folder equal the layer in libs/shared/src/ui/
Status: done Source: PLAN.md 4a, Phase 4
Why
CLAUDE.md decision 2 says "folder = layer": libs/shared/ui atoms → molecules → organisms.
Today libs/shared/src/ui/ is 26 flat directories, and the only record of a component's layer
is its story title and a header comment. Nothing stops an atom importing an organism.
This ticket makes the structure say what the rule says. It is a pure move: no component changes, no story title changes, no behaviour. RD-29 then adds the dependency-cruiser rules that the folders make expressible.
This is the highest-risk ticket in the arc, because a broken .mdx story import compiles
fine and only fails when Storybook builds. The README says it must not be pushed without
npm run ci --full.
Read first
libs/shared/src/ui/— 25 flat component directories plusupload/with 8 of its own.libs/shared/docs/atomic-design.mdx:2-5— three of the seven.mdxstory imports that break.- PLAN.md 4a — the design record, including why
layout/does not move. .storybook-ssp/main.ts:11-14— the globs, which are recursive and need no edit.
Decisions (pre-made, don't relitigate)
-
Three layer folders, and the full 33-directory mapping. This table is the ticket.
libs/shared/src/ui/atoms/— 12 flat:alert button checkbox heading link masked-value placeholder-chip radio-group skeleton spinner status-badge text-inputlibs/shared/src/ui/molecules/— 13 flat:application-link application-list async choice-link choice-list confirmation data-block data-row form-field review-section rich-text-editor stepper task-listupload/keeps its feature subfolder inside each layer — 8 directories:New location Directories ui/atoms/upload/delivery-channel-toggle,document-chip,file-input,upload-progress-bar,upload-status-iconui/molecules/upload/single-uploadui/organisms/upload/document-category,document-uploadui/organisms/holds nothing butupload/. That is correct and worth seeing.The mapping is derived from each component's own story title, not invented — every one of the 33 already declares its layer as
Design System/<Layer>/…. -
Use
git mvper directory, so the diff reads as renames.git diff --stat -Mmust show renames plus one-line import edits, nothing else. -
Rewrite the 28 distinct specifiers, longest key first. All 28 are listed by the mapping in decision 1; 25 are flat and 3 are the upload components imported from outside (
upload/document-upload→organisms/upload/document-upload,upload/file-input→atoms/upload/file-input,upload/single-upload→molecules/upload/single-upload).Verified: no specifier is a prefix of another, so ordering cannot corrupt a rewrite here. Do it longest-first anyway — it costs nothing and the property is not guaranteed to hold if this is ever repeated.
There are 200 occurrences across 73 files, and this rewrite alone changes none of them.
Corrected after the ticket ran: the total lands at 205, not 200. Decision 4 converts five relative imports into
@shared/ui/…aliases, and each of those is itself a new occurrence. This decision's "must not change" governs this rewrite; decision 4 adds five on top. The two decisions were written as if they counted different things, and they do not.The honest check is per-source: 200 from the specifier rewrite, plus exactly the 5 that decision 4 names.
-
Five of the seven relative imports inside
upload/become aliases; two stay relative. A../sibling/import only breaks when the sibling lands in a different layer:File (new location) Import Becomes organisms/upload/document-category../delivery-channel-toggle@shared/ui/atoms/upload/delivery-channel-toggleorganisms/upload/document-category../file-input@shared/ui/atoms/upload/file-inputorganisms/upload/document-category../single-upload@shared/ui/molecules/upload/single-uploadmolecules/upload/single-upload../document-chip@shared/ui/atoms/upload/document-chipmolecules/upload/single-upload../upload-progress-bar@shared/ui/atoms/upload/upload-progress-barUnchanged, because both ends stay in the same layer:
atoms/upload/document-chip→../upload-status-icon, andorganisms/upload/document-upload→../document-category.Use the alias, not
../../../. Cross-directory imports insidelibs/sharedalready use@shared/…(seewizard-shell.component.ts), and a three-level relative path is exactly the thing a later move breaks silently. -
Seven
.mdxstory imports break. All seven are real imports, not prose. The Order table says eight; measured, it is seven:a11y.mdx:2 ../src/ui/alert/alert.stories -> ui/atoms/alert/… a11y.mdx:3 ../src/ui/form-field/form-field.stories -> ui/molecules/form-field/… atomic-design.mdx:2 ../src/ui/button/button.stories -> ui/atoms/button/… atomic-design.mdx:3 ../src/ui/form-field/form-field.stories -> ui/molecules/form-field/… atomic-design.mdx:5 ../src/ui/upload/document-upload/… -> ui/organisms/upload/document-upload/… fp-in-ui.mdx:2 ../src/ui/async/async.stories -> ui/molecules/async/… remote-data.mdx:2 ../src/ui/async/async.stories -> ui/molecules/async/…atomic-design.mdx:4imports../src/layout/page-shell/…. Leave it alone —layout/does not move. -
layout/does not move, and neither doeslibs/beheer/src/ui/. CLAUDE.md §5 explicitly sanctionslibs/shared/layoutholding several layers, andlibs/beheeris a bounded context that lives inlibs/only because two apps share it. Both are settled; do not revisit them. -
Change no story title, no layer-tag comment, and no component code. The titles already say the right thing, which is what made decision 1's mapping derivable. The two mislabelled tags (
asynchas/** Convenience: */,breadcrumbsays/** Chrome: */) and the two missing ones (masked-value,rich-text-editor) are RD-28's job, not this ticket's. -
No barrel file. The repository has none and does not need one. A barrel would also hide exactly the layer boundary this ticket exists to expose.
Files
- 33 directories moved under
libs/shared/src/ui/ - ~73 files with a one-line specifier edit
- 4
.mdxfiles (7 import lines)
No changes to angular.json, eslint.config.mjs, plopfile.mjs, any tsconfig.json,
.dependency-cruiser.*, scripts/check-tokens.sh, or e2e/. Verified: @shared/* maps to
libs/shared/src/*, so a deeper path resolves unchanged; both Storybook globs are recursive;
dependency-cruiser's ui-not-infrastructure pattern is (/ui/|/layout/), which still matches a
nested path; and check-tokens.sh's CIBG-GAP check keys on the directory basename, which a
parent-folder move preserves.
Steps
mkdirthe three layer folders, thengit mvall 33 directories per decision 1.- Rewrite the 28 specifiers across
apps/andlibs/(decision 3). - Fix the five relative imports inside
upload/(decision 4). - Fix the seven
.mdximports (decision 5). npm run typecheck— four tsconfigs, and the fastest way to catch a missed specifier.git add -A, then run the acceptance commands.- Update this ticket's
Status:todoneand the README's RD-27 row todone. - Commit all of it together.
Acceptance criteria
Measured against the tree before handover. Run after git add -A.
The structure is three folders, and every directory landed:
ls -d libs/shared/src/ui/*/ | wc -l # is 26 -> MUST be 3
ls -d libs/shared/src/ui/atoms/*/ | wc -l # MUST be 13 (12 + upload)
ls -d libs/shared/src/ui/atoms/upload/*/ | wc -l # MUST be 5
ls -d libs/shared/src/ui/molecules/*/ | wc -l # MUST be 14 (13 + upload)
ls -d libs/shared/src/ui/molecules/upload/*/ | wc -l # MUST be 1
ls -d libs/shared/src/ui/organisms/*/ | wc -l # MUST be 1 (upload)
ls -d libs/shared/src/ui/organisms/upload/*/ | wc -l # MUST be 2
The single strongest check in this ticket — every specifier now names a layer, so only three distinct values may remain:
git grep -ho "@shared/ui/[a-z0-9-]*" -- apps libs | sort -u # is 26 values -> MUST be exactly these 3:
# @shared/ui/atoms
# @shared/ui/molecules
# @shared/ui/organisms
Nothing was lost or duplicated in the rewrite:
git grep -ho "@shared/ui/" -- apps libs | wc -l # is 200 -> MUST be 205 (200 + decision 4's 5)
git grep -c "src/ui/" -- '*.mdx' | awk -F: '{s+=$NF} END {print s+0}' # is 7 -> MUST still be 7
git grep -c "from '\.\./" -- libs/shared/src/ui/ | awk -F: '{s+=$NF} END {print s+0}' # is 7 -> MUST be 2
The move reads as a move (decision 2):
git diff --cached --stat -M | tail -1 # inspect: renames + one-line edits, no rewritten files
npm run ci --full # exits 0 — REQUIRED, see Verification
Verification
npm run ci --full is mandatory and non-negotiable for this ticket. Plain npm run ci does
not build Storybook, and a .mdx importing a moved story path is invisible to the type-checker,
the linter and every unit test. It fails only when build-storybook runs. The README calls out
RD-27 by name for this reason.
Run npm run typecheck first anyway (step 5): it covers four tsconfigs and catches a missed
@shared/ui/* specifier in seconds rather than at the end of a full gate.
libs/shared/docs/layers.mdx deep-links two story ids
(design-system-molecules-application-link--navigatie,
domein-registratie-aanvraag-block--concept). Story ids derive from titles, and decision 7
changes no title, so both links survive. Do not "fix" them.
Out of scope
- The dependency-cruiser ladder rules. RD-29 adds them, and it depends on this ticket.
- Layer-tag comments and the
libs/beheertitle rule — RD-28. layout/(decision 6).- Any component's code, template, styles or story title.
Risks
- A broken
.mdximport is invisible until Storybook builds. This is the whole reason the ticket carries--full. Decision 5 lists all seven; check each one after the move. git mv, notmv+git add. Both produce the same tree, but only the first keeps the diff readable as renames — and this diff is 33 directories wide.- Do not flatten
upload/(decision 1). Its subfolder survives inside each layer. - Do not reach for
../../../(decision 4). Use the alias. - The occurrence count is the tripwire for a bad
sed. 200 before, 200 after. A rewrite that accidentally matches twice, or drops a line, moves that number. atomic-design.mdxhas four story imports and only three of them move. The fourth ispage-shell, inlayout/.