refactor: enforce the atomic ladder in dependency-cruiser (RD-29)

Add three dependency-cruiser rules that forbid upward edges inside
libs/shared/src/ui: atoms-compose-nothing-above, molecules-below-organisms,
and design-system-not-layout. RD-27 made the atomic ladder expressible by
moving components into atoms/molecules/organisms folders; these rules make
it enforced, so an upward import now fails dep:check instead of shipping.

No pathNot exemption for specs and stories: measured, zero upward edges
exist anywhere in libs/shared/src/ui today, in production code, specs, and
stories alike, so the exemption PLAN proposed has nothing to protect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-09-05 08:32:52 +02:00
co-authored by Claude Sonnet 5
parent d4c5a9450a
commit b6bd7eea5a
3 changed files with 167 additions and 1 deletions
+23
View File
@@ -109,6 +109,29 @@ module.exports = function buildConfig(contextAllowed, appName, tsConfigFileName)
}, },
}, },
// --- Atomic ladder within libs/shared/src/ui (folder = layer, CLAUDE.md decision 2) ---
{
name: 'atoms-compose-nothing-above',
comment: 'An atom composes nothing above it — no molecule or organism. See CLAUDE.md §2.',
severity: 'error',
from: { path: '^libs/shared/src/ui/atoms/' },
to: { path: '^libs/shared/src/ui/(molecules|organisms)/' },
},
{
name: 'molecules-below-organisms',
comment: 'A molecule composes nothing above it — no organism. See CLAUDE.md §2.',
severity: 'error',
from: { path: '^libs/shared/src/ui/molecules/' },
to: { path: '^libs/shared/src/ui/organisms/' },
},
{
name: 'design-system-not-layout',
comment: 'The design system (ui/) does not depend on layout/ templates. See CLAUDE.md §2.',
severity: 'error',
from: { path: '^libs/shared/src/ui/' },
to: { path: '^libs/shared/src/layout/' },
},
{ {
name: 'no-testing-in-production', name: 'no-testing-in-production',
comment: comment:
@@ -0,0 +1,143 @@
# 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.
+1 -1
View File
@@ -123,7 +123,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
| RD-26 | `letter-canvas`: inline the labels + `letter-line`; keep one disable | 02 | yes | done | | RD-26 | `letter-canvas`: inline the labels + `letter-line`; keep one disable | 02 | yes | done |
| RD-27 | **The layer move:** 33 `git mv` + 28 specifiers + 8 MDX imports | 21 | yes | done | | RD-27 | **The layer move:** 33 `git mv` + 28 specifiers + 8 MDX imports | 21 | yes | done |
| RD-28 | Layer-tag fixes + the `libs/beheer` title rule | 27 | yes | done | | RD-28 | Layer-tag fixes + the `libs/beheer` title rule | 27 | yes | done |
| RD-29 | The 3 atomic-ladder rules in dependency-cruiser | 27 | | todo | | RD-29 | The 3 atomic-ladder rules in dependency-cruiser | 27 | | done |
| RD-30 | Archive the finished backlogs (16,300 lines) + an archive README | 01 | | todo | | RD-30 | Archive the finished backlogs (16,300 lines) + an archive README | 01 | | todo |
| RD-31 | `ARCHITECTURE.md` section 6a: symbols not lines, 2 dead paths, new names | 03, 08, 16 | | todo | | RD-31 | `ARCHITECTURE.md` section 6a: symbols not lines, 2 dead paths, new names | 03, 08, 16 | | todo |
| RD-32 | `fp-tea-atomic-design.md`: 11 broken paths + the broken anchor | 27 | | todo | | RD-32 | `fp-tea-atomic-design.md`: 11 broken paths + the broken anchor | 27 | | todo |