# RD-29 — Enforce the atomic ladder in dependency-cruiser Status: done Source: PLAN.md 4c ## Why RD-27 made the ladder **expressible**: `libs/shared/src/ui/` is now `atoms/`, `molecules/`, `organisms/`. Nothing yet makes it **enforced** — an atom importing an organism compiles, lints, tests and ships. This ticket adds the three rules. It is the payoff for the move, and PLAN calls it "the real prize". ## Read first - `.dependency-cruiser.base.js:38` — `anyRoot`, and the atomic-layer rules that follow it. The new rules go beside them. - `.dependency-cruiser.base.js:93-99` — `ui-not-infrastructure`, the closest existing rule in shape. - PLAN.md 4c. ## Decisions (pre-made, don't relitigate) 1. **Three rules, in `.dependency-cruiser.base.js`, forbidding upward edges only:** | Rule name | from | to | | ----------------------------- | -------------------------------- | --------------------------------------------- | | `atoms-compose-nothing-above` | `^libs/shared/src/ui/atoms/` | `^libs/shared/src/ui/(molecules\|organisms)/` | | `molecules-below-organisms` | `^libs/shared/src/ui/molecules/` | `^libs/shared/src/ui/organisms/` | | `design-system-not-layout` | `^libs/shared/src/ui/` | `^libs/shared/src/layout/` | `severity: 'error'`, each with a `comment` naming CLAUDE.md decision 2, matching the house style of every other rule in that file. Literal `libs/shared/src/...` paths, not `anyRoot`: only `libs/shared` has layer folders. The rules are evaluated once per app cruise, which is harmless — the same tree, the same answer. 2. **Never "atoms are leaves". Same-layer edges are legitimate and four exist today:** ``` atoms/masked-value -> atoms/button molecules/review-section -> molecules/data-block molecules/task-list -> molecules/choice-list molecules/task-list -> molecules/choice-link ``` A rule forbidding an atom from importing any atom would fail on the first of these. Forbid the layers **above**, nothing else. 3. **No `pathNot` exemption for specs and stories. This corrects PLAN.** PLAN says the exemption is needed "because `async.stories.ts` composes `skeleton`". Measured: `async` is a molecule and `skeleton` is an atom, so that edge points **downward** and is legal under decision 1. The example does not justify an exemption. Measured further: **zero upward edges exist anywhere in `libs/shared/src/ui/`, in production code, specs and stories alike.** Nothing needs the exemption today. So leave it out. An exemption that nothing needs is dead flexibility, and it silently widens the rule the moment someone does write an upward import in a story. If a story ever earns one — an atom's story demonstrating it inside a molecule is the plausible case — add the exemption then, with that real example in the comment. 4. **All three land green immediately.** This ticket adds enforcement and changes no application code. If `dep:check` fails after adding them, the rule is written wrongly — do not "fix" the application to satisfy it without saying so. 5. **Dependency-cruiser, not ESLint.** It is where every other boundary rule in this repository lives, and it emits the architecture graph. Do not add an ESLint variant. ## Files - `.dependency-cruiser.base.js` Nothing else. No application code changes. ## Steps 1. Add the three rules per decision 1, beside the existing atomic-layer rules. 2. Run `npm run dep:check` — it must pass. 3. **Prove each rule bites** (see Verification). This is the point of the ticket. 4. `git add -A`, then run the acceptance commands. 5. Update this ticket's `Status:` to `done` and the README's RD-29 row to `done`. 6. Commit all of it together. ## Acceptance criteria Measured against the tree before handover. ```bash git grep -c "atoms-compose-nothing-above" -- .dependency-cruiser.base.js # is 0 -> MUST be 1 git grep -c "molecules-below-organisms" -- .dependency-cruiser.base.js # is 0 -> MUST be 1 git grep -c "design-system-not-layout" -- .dependency-cruiser.base.js # is 0 -> MUST be 1 ``` The rules pass on the current tree, and no application file changed (decisions 4 and 5): ```bash npm run dep:check # exits 0 git diff --cached --name-only | grep -v '^docs/' # MUST list only .dependency-cruiser.base.js ``` ```bash npm run ci # exits 0 ``` `--full` is not required: no story, no `.mdx`, and nothing under `libs/shared/src/ui/**` is edited. The Order table's blank column is correct here. ## Verification **A rule that matches nothing is worse than no rule, because it reads as protection.** Prove each of the three actually fires, one at a time: 1. Add a temporary import that violates it — for example, in `libs/shared/src/ui/atoms/button/button.component.ts`, import `@shared/ui/molecules/data-row/data-row.component`. 2. Run `npm run dep:check` and confirm it fails, naming that rule. 3. Revert the temporary import. Do this for all three. Report which rule name each violation produced. **Do not commit any temporary import** — `git status` must be clean of them before you commit, and the acceptance command above checks that only the config file changed. ## Out of scope - Layer folders or ladder rules for `libs/beheer`. It is a bounded context, not a design system (RD-28 settled this). - `layout/`'s internal structure. It is sanctioned to hold several layers. - Any rule about app contexts' own `ui/` folders. They have no layer folders by design — a context organism does not get its own bucket. - Changing an existing dependency-cruiser rule. ## Risks - **Do not add the spec/stories exemption out of habit** (decision 3). Two other rules in the file have one; these three do not need it, and the ticket explains why. - **Forbid upward, not sideways** (decision 2). Four same-layer edges exist and are correct. - **`from` must not match the layer it forbids.** `design-system-not-layout` starts at `^libs/shared/src/ui/`, which covers all three layer folders; that is intended. - **If `dep:check` goes red, suspect the rule, not the code** (decision 4). Nothing in the tree violates the ladder today.