Files
atomic-design-poc/docs/project/readable-codebase/RD-27-layer-move.md
T
ehoandClaude Opus 5 57fc72f254 docs: correct RD-27's occurrence count, the fourth time in one pattern
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>
2026-09-05 08:15:20 +02:00

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 plus upload/ with 8 of its own.
  • libs/shared/docs/atomic-design.mdx:2-5 — three of the seven .mdx story 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)

  1. 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-input
    

    libs/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-list
    

    upload/ 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-icon
    ui/molecules/upload/ single-upload
    ui/organisms/upload/ document-category, document-upload

    ui/organisms/ holds nothing but upload/. 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>/….

  2. Use git mv per directory, so the diff reads as renames. git diff --stat -M must show renames plus one-line import edits, nothing else.

  3. 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.

  4. 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-toggle
    organisms/upload/document-category ../file-input @shared/ui/atoms/upload/file-input
    organisms/upload/document-category ../single-upload @shared/ui/molecules/upload/single-upload
    molecules/upload/single-upload ../document-chip @shared/ui/atoms/upload/document-chip
    molecules/upload/single-upload ../upload-progress-bar @shared/ui/atoms/upload/upload-progress-bar

    Unchanged, because both ends stay in the same layer: atoms/upload/document-chip → ../upload-status-icon, and organisms/upload/document-upload → ../document-category.

    Use the alias, not ../../../. Cross-directory imports inside libs/shared already use @shared/… (see wizard-shell.component.ts), and a three-level relative path is exactly the thing a later move breaks silently.

  5. Seven .mdx story 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:4 imports ../src/layout/page-shell/…. Leave it alone — layout/ does not move.

  6. layout/ does not move, and neither does libs/beheer/src/ui/. CLAUDE.md §5 explicitly sanctions libs/shared/layout holding several layers, and libs/beheer is a bounded context that lives in libs/ only because two apps share it. Both are settled; do not revisit them.

  7. 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 (async has /** Convenience: */, breadcrumb says /** Chrome: */) and the two missing ones (masked-value, rich-text-editor) are RD-28's job, not this ticket's.

  8. 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 .mdx files (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

  1. mkdir the three layer folders, then git mv all 33 directories per decision 1.
  2. Rewrite the 28 specifiers across apps/ and libs/ (decision 3).
  3. Fix the five relative imports inside upload/ (decision 4).
  4. Fix the seven .mdx imports (decision 5).
  5. npm run typecheck — four tsconfigs, and the fastest way to catch a missed specifier.
  6. git add -A, then run the acceptance commands.
  7. Update this ticket's Status: to done and the README's RD-27 row to done.
  8. 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/beheer title rule — RD-28.
  • layout/ (decision 6).
  • Any component's code, template, styles or story title.

Risks

  • A broken .mdx import 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, not mv + 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.mdx has four story imports and only three of them move. The fourth is page-shell, in layout/.