ADR 0006 — Per-effect / per-layout LOC budgets¶
Status: Accepted, 2026-05-17. Supersedes: none. Related: process architecture §2 — guardrails obey their own rules; CLAUDE.md Rule #1; check_loc.py.
Context¶
src/modules/lights/ had a single aggregate LOC budget ("src/modules/lights": 600).
The lights domain is the one surface designed to grow indefinitely: every new
visual effect and every new panel layout is a new file there. A single
aggregate ceiling has two failure modes against Rule #1:
- It hides per-effect bloat. A 400-line effect and a 40-line effect are indistinguishable under one total; the ceiling can be satisfied while an individual effect is doing far too much.
- It becomes a ratchet. As effects accumulate the number is bumped 600 → 1400 → 2200 → … . Each bump is "true" yet the bumps carry no per-effect deliberation — exactly the unbounded-expansion pattern the minimalism rule exists to prevent. Projected to 10 000+ LOC the aggregate number means nothing.
The codebase already solved this exact problem for src/pal/: one budget
per file, and a gate (check_pal_files_have_budgets) that fails if any pal
file lacks an entry — "adding a new pal concern is a moment of deliberation,
not a free expansion." Effects and layouts are the same shape of growth.
This change also lands alongside the v1 effect/layout port (5 effects + 2
layouts) and a PixelEffectBase / LayoutModule extraction that removed the
~6× duplicated resolve/allocate/teardown boilerplate the ports first carried.
Alternatives considered:
- Keep one aggregate, bump 600 → 1400. Rejected: defers the ratchet one step; the number stops being a discipline signal as the surface grows.
- Drop the lights budget entirely. Rejected: removes the only mechanical brake on the surface most prone to drift — the opposite of Rule #1.
- Per-directory sub-budgets (
effects/,layouts/). Rejected: same hiding problem one level down; a bloated single effect is still masked by lean siblings in the same directory.
Decision¶
Effects and layouts are per-file budgeted, mirroring src/pal/:
scripts/checks/check_loc.pyBUDGETSgains one entry per file undersrc/modules/lights/effects/andsrc/modules/lights/layouts/, each with a one-line comment naming the effect/layout.- A new gate
check_lights_files_have_budgets()fails the check if any.h/.cppunder those two directories has noBUDGETSentry. Adding an effect or layout therefore requires adding its budget line — the deliberation moment. "src/modules/lights"stays as a (smaller) aggregate for the shared non-effect/non-layout files only (RGB.h,Pixelable.h,PreviewModule.h,ArtnetOutModule.h,GridLayoutModule.h).check_loc.pyis already nested-aware: per-file entries are excluded from the directory total, so there is no double-counting.RipplesEffect.his onPixelEffectBaselike every other effect and so is per-file budgeted by name, even though it sits inlights/root rather thaneffects/(historical placement; moving the file is a cosmetic change deliberately not bundled here). "Every effect is per-file budgeted" is the invariant; directory is incidental. The gate (below) enforces it mechanically for theeffects/+layouts/dirs; root-dir effects are enumerated explicitly.
check_loc.py's own script budget is bumped (170 → 210) with a justification
comment for the new gate + entries — the guardrail obeying its own rule.
Consequences¶
Cost.
- BUDGETS grows by one entry per effect/layout (≈10 lines now; +1 line per
future effect). This is intentional: the line is the deliberation record.
- A new effect PR must touch check_loc.py (add its budget). Same friction
src/pal/ already imposes; accepted there, accepted here.
- check_loc.py grew ~20 LOC (the gate + entries); budget bumped with reason.
Benefit.
- A single bloated effect is now visible (its own line goes OVER) instead of
hidden under an aggregate.
- No more aggregate ratchet: the lights surface can grow to many effects
without the budget number degrading into noise.
- Consistent with the established, accepted src/pal/ discipline — one
pattern for "surfaces that grow one concern at a time", not two.
What is not changed.
- The minimalism rule itself; this strengthens its mechanical enforcement.
- Other aggregate budgets (src/core, src/modules/network,
src/modules/system) — those are bounded surfaces, not per-concern growth,
and stay aggregate.
- Adding a new kind of lights sub-surface (e.g. drivers/, modifiers/)
silently: extend check_lights_files_have_budgets()'s directory list and
note it here or in a follow-up ADR — same bar as a new pal concern.