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:
@@ -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',
|
||||
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.
|
||||
@@ -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-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-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-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 |
|
||||
|
||||
Reference in New Issue
Block a user