Compare commits

...
2 Commits
Author SHA1 Message Date
ehoandClaude Sonnet 5 b6bd7eea5a 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>
2026-09-05 08:32:52 +02:00
ehoandClaude Sonnet 5 d4c5a9450a fix: layer-tag mismatches + libs/beheer title rule (RD-28)
Two components disagreed with their own story title. async.component.ts
had no layer tag; add `Molecule:` to its class header, not to the
Convenience: comment on the ASYNC array (that comment describes an
export, not a layer, and stays as it is). breadcrumb.component.ts had
`Chrome:`; rename it to `Molecule:` to match its story title.

CLAUDE.md and layers.mdx both claimed `libs/beheer/ui` is Design System.
The code disagrees: its story title is `Domein/Beheer/...`, because
libs/beheer is a bounded context that lives under libs/ only because two
apps share it. Fix the two doc lines to match the code; the story title
does not change.

The ticket's own tag-count check asserted 68 after the edits, but adding
a new tag to async.component.ts (which had none) is a net +1 over the
Chrome-to-Molecule rename (a wash) — the true post-edit count is 69.
Corrected the number in the ticket text rather than deleting the
mandated tag to force the wrong count. Also fixed pre-existing invalid
nested-backtick markdown in the ticket's decision 4 bullets: prettier
mis-rewrapped it and dropped spaces, so the phrasing was rewritten in
valid CommonMark with no change in meaning.

npm run ci --full passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 08:26:47 +02:00
8 changed files with 330 additions and 10 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',
comment:
+4 -4
View File
@@ -210,10 +210,10 @@ each app has its **own Storybook instance** (`.storybook-ssp/`, `.storybook-beha
WP-67 — a single merged tsconfig can't resolve both apps' `@auth/*` at once), each globbing
its own app's stories plus both shared libraries'. **Story titles mirror the sidebar's
Design System/Domein split** (see `libs/shared/docs/layers.mdx`): a `libs/shared/ui|layout`
or `libs/beheer/ui` component is titled `Design System/<Atoms|Molecules|Organisms|Templates|Devtools>/<Name>`;
a component in an app context's `ui/` is titled `Domein/<Context>/<Name>` — full stop,
regardless of which atomic layer it is (a context organism doesn't get its own
`Organisms/` bucket).
component is titled `Design System/<Atoms|Molecules|Organisms|Templates|Devtools>/<Name>`;
a component in an app context's `ui/`, or in `libs/beheer/ui`, is titled
`Domein/<Context>/<Name>` — full stop, regardless of which atomic layer it is (a context
organism doesn't get its own `Organisms/` bucket).
## Conventions
@@ -0,0 +1,154 @@
# RD-28 — Two layer-tag fixes, and the `libs/beheer` title rule
Status: done
Source: PLAN.md 4a (the two mislabels) and 4b (keep the tags)
## Why
RD-27 made the folder equal the layer. Three statements about a component's layer now exist —
the folder, the header comment's tag, and the story title — and a three-way agreement that has
never once disagreed is cheap documentation.
It disagrees in exactly two places. And one repository-wide rule in CLAUDE.md is simply wrong
about `libs/beheer`.
## Read first
- `libs/shared/src/ui/molecules/async/async.component.ts:45` — the class header, with no layer
tag; and line 154, the `Convenience:` comment on the `ASYNC` array, which is **correct and
stays**.
- `libs/shared/src/layout/breadcrumb/breadcrumb.component.ts:9``/** Chrome: …`, against a
story titled `Design System/Molecules/Breadcrumb`.
- `CLAUDE.md:213` and `libs/shared/docs/layers.mdx:13` — the two lines that claim
`libs/beheer/ui` is Design System.
- PLAN.md 4a's third paragraph and 4b.
## Decisions (pre-made, don't relitigate)
1. **Exactly two components are mislabelled. Verified by scanning all 36**, comparing each
component's first layer tag against its own story title:
| File | Story title | Tag today | Becomes |
| ------------------------------------------- | ----------- | --------- | ----------- |
| `ui/molecules/async/async.component.ts` | Molecules | _(none)_ | `Molecule:` |
| `layout/breadcrumb/breadcrumb.component.ts` | Molecules | `Chrome:` | `Molecule:` |
Every other component already agrees. Do not "tidy" any other tag.
2. **`async.component.ts` gets the tag added to its class header at line 45, not to line 154.**
The `Convenience:` comment at 154 describes the `ASYNC` array export — "import this array to
get the wrapper + all slot directives" — which is an accurate description of a convenience
export, not a layer claim. Leave it exactly as it is.
3. **Keep all 68 layer-tag comments. Delete none.** PLAN 4b reversed an earlier proposal to
strip them: the tag prefixes a real one-line description, so removing the word leaves the
sentence and buys nothing. Only the tags in `libs/shared/src/ui/` are made redundant by the
new folders; the ones in `apps/**/ui/` are the sole carrier of the layer, because a context
component's title deliberately omits it.
4. **`libs/beheer` is Domein, not Design System. The docs are wrong; the code is right.**
`stamdata-table-editor.stories.ts` is titled `Domein/Beheer/Stamdata Table Editor`, and that
is correct: `libs/beheer` **is** a bounded context, which lives in `libs/` only because two
apps share it. It is not part of the design system, and it correctly has no layer folders.
Amend the two doc lines that say otherwise:
- `CLAUDE.md:213` — drop the words "or `libs/beheer/ui`" from the Design System title rule,
and say that a `libs/beheer/ui` component is titled `Domein/Beheer/<Name>`.
- `libs/shared/docs/layers.mdx:13` — drop the parenthetical "(or `libs/beheer/ui`)", and put
`libs/beheer` on the Domein side of the same sentence.
**Do not change the story title.** This ticket fixes the documentation to match the code.
5. **No code behaviour changes anywhere.** Two comment words and two doc sentences.
## Files
- `libs/shared/src/ui/molecules/async/async.component.ts`
- `libs/shared/src/layout/breadcrumb/breadcrumb.component.ts`
- `CLAUDE.md`
- `libs/shared/docs/layers.mdx`
## Steps
1. Add `Molecule: ` to `async.component.ts`'s class header (decision 2).
2. Change `Chrome:` to `Molecule:` in `breadcrumb.component.ts`.
3. Amend `CLAUDE.md:213` and `layers.mdx:13` per decision 4.
4. `git add -A`, then run the acceptance commands.
5. Update this ticket's `Status:` to `done`, the README's RD-28 row to `done`, and that row's
`--full`? column to `yes` (see Verification).
6. Commit all of it together.
## Acceptance criteria
Measured against the tree before handover.
**The real check is the scan itself.** Run it; it must print nothing but `--- done`:
```bash
for f in $(git ls-files 'libs/shared/src/ui/**/*.component.ts' 'libs/shared/src/layout/**/*.component.ts'); do
d=$(dirname $f)
title=$(grep -ho "title: 'Design System/[A-Za-z]*" $d/*.stories.ts 2>/dev/null | head -1 | sed "s/.*Design System\///")
tag=$(grep -oE "\b(Atom|Molecule|Organism|Template|Chrome|Convenience|Devtool)s?:" $f | head -1 | tr -d ':')
exp=$(echo "$title" | sed 's/s$//')
[ "$tag" = "$exp" ] || echo "MISMATCH $f | title=${title:-NONE} | tag=${tag:-NONE}"
done
echo "--- done"
```
The two edits, and the comment that must survive (decisions 1 and 2):
```bash
git grep -c "Chrome:" -- libs/shared/src/layout/breadcrumb/breadcrumb.component.ts # is 1 -> MUST be 0
git grep -c "Molecule:" -- libs/shared/src/ui/molecules/async/async.component.ts # is 0 -> MUST be 1
git grep -c "Convenience:" -- libs/shared/src/ui/molecules/async/async.component.ts # is 1 -> MUST still be 1
```
Nothing was stripped (decision 3):
```bash
git grep -ohE "\b(Atom|Molecule|Organism|Template|Chrome|Convenience|Devtool)s?:" -- libs apps | wc -l # is 68 before the edits -> MUST be 69 after
```
Note: the pre-edit tree measures 68. Decision 2 adds a brand-new `Molecule:` tag to
`async.component.ts` (which had no tag at all), a net +1; the `Chrome:``Molecule:`
rename in `breadcrumb.component.ts` is a wash. 68 unchanged would mean decision 2 was
not applied. This is the same class of miss the README's ticket-writing rules log for
RD-11/RD-14/RD-20/RD-27: add up every decision that touches the counted thing before
writing the number.
The docs changed and the code did not (decision 4):
```bash
grep -c 'or `libs/beheer/ui` component is titled' CLAUDE.md # is 1 -> MUST be 0
grep -c '(or `libs/beheer/ui`)' libs/shared/docs/layers.mdx # is 1 -> MUST be 0
git grep -c "Domein/Beheer/Stamdata Table Editor" -- libs/beheer # is 1 -> MUST still be 1
```
```bash
npm run ci --full # exits 0
```
## Verification
**`--full` is required, although the README's Order table leaves that column blank.** This
ticket edits `layers.mdx`, and the README's own rule says an `.mdx` edit needs `--full`. Fix the
column to `yes` in the same commit — the same correction RD-18 needed.
The tag count of 68 is the guard against a well-meant tidy-up. Decision 3 says keep every one.
## Out of scope
- Every other layer tag (decision 1). The scan found no other disagreement.
- The `libs/beheer` story title (decision 4). The code is right.
- Layer folders for `libs/beheer`. It is a context; it does not want them.
- The dependency-cruiser ladder rules — RD-29, which depends on this ticket only by ordering.
## Risks
- **`async.component.ts` has two comments that look like tags.** Line 154's `Convenience:` is
about the `ASYNC` array export and is accurate. Edit line 45's class header instead, and leave
154 alone — the acceptance commands check both.
- **Do not "fix" the beheer story title.** The instinct is to make the code match CLAUDE.md; here
CLAUDE.md is the thing that is wrong.
- **Do not delete a tag** (decision 3). The count must hold at 68.
@@ -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.
+2 -2
View File
@@ -122,8 +122,8 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
| RD-25 | `org-template-editor` to `sample-letter.ts` + labels + 2 children | 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-28 | Layer-tag fixes + the `libs/beheer` title rule | 27 | | todo |
| RD-29 | The 3 atomic-ladder rules in dependency-cruiser | 27 | | todo |
| RD-28 | Layer-tag fixes + the `libs/beheer` title rule | 27 | yes | done |
| 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 |
+2 -2
View File
@@ -9,8 +9,8 @@ This project is **domain-driven**: the code is organised first by **bounded cont
with dependencies pointing inward. The Storybook sidebar is laid out to **be** that
architecture, not just document it: **Foundations** (this curriculum) → **Design System**
(reusable, domain-free) → **Domein** (the app-local DDD contexts). If a component lives
under a context's `ui/`, it's in Domein; everything else in `libs/shared/ui`/`layout`
(or `libs/beheer/ui`) is Design System. See [Atomic design](?path=/docs/foundations-atomic-design--docs)
under a context's `ui/`, or under `libs/beheer/ui`, it's in Domein; everything else in
`libs/shared/ui`/`layout` is Design System. See [Atomic design](?path=/docs/foundations-atomic-design--docs)
for the Atoms → Molecules → Organisms → Templates ladder inside Design System.
## Two apps, two shared libraries
@@ -6,7 +6,7 @@ export interface BreadcrumbItem {
link?: string; // omit on the current (last) page
}
/** Chrome: breadcrumb navigation, styled for the CIBG titlebar (`.titlebar .title`) —
/** Molecule: breadcrumb navigation, styled for the CIBG titlebar (`.titlebar .title`) —
plain links with a chevron `::after` from the CIBG Icons font, current page as an
unlinked, bold span. Domain-free — the caller supplies the trail. */
@Component({
@@ -43,7 +43,7 @@ export class AsyncErrorDirective {
}
/**
* Renders exactly ONE of loading / empty / error / loaded for a signal-based
* Molecule: renders exactly ONE of loading / empty / error / loaded for a signal-based
* resource (e.g. httpResource). Built on a RemoteData tagged union (see
* core/remote-data.ts), so the states are mutually exclusive by construction —
* the UI can never show two at once ("impossible states"). Unprovided slots