From b9d572cfdc65cce41af3466449f9f3f431df7711 Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Fri, 4 Sep 2026 16:17:07 +0200 Subject: [PATCH] feat: add max-lines guard with self-cleaning exemptions (RD-02) The dashboard refactor proved a page can be 42 lines. This rule holds the rest of the app to that budget, before the split work in RD-20 through RD-26 begins. Add max-lines at 250 (skipBlankLines, skipComments), scoped to {apps,libs}/**/*.{page,component,section,step}.ts. The glob includes section and step, the file kinds the dashboard refactor invented, so new files from this arc do not escape the guard. Add linterOptions.reportUnusedDisableDirectives: 'error' repo-wide. ESLint 9 only warns on an unused disable by default, so a stale exemption would not fail the build. At 'error', every later file split must delete its own exemption or the build goes red. Add a dated /* eslint-disable max-lines */ header to each of the seven files that exceed the budget today, each with a reason and the ticket that removes it. letter-canvas keeps its disable after RD-26, because most of its lines are CSS and the rest is one letter. Co-Authored-By: Claude Sonnet 5 --- .../letter-canvas/letter-canvas.component.ts | 1 + .../org-template-editor.component.ts | 1 + .../herregistratie-wizard.component.ts | 1 + .../intake-wizard/intake-wizard.component.ts | 1 + .../registratie-wizard.component.ts | 1 + apps/ssp/src/app/showcase/concepts.page.ts | 1 + .../RD-02-max-lines-guard.md | 118 ++++++++++++++++++ docs/project/readable-codebase/README.md | 2 +- eslint.config.mjs | 19 +++ .../rich-text-editor.component.ts | 1 + 10 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 docs/project/readable-codebase/RD-02-max-lines-guard.md diff --git a/apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts b/apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts index 07ac186..b5aca9a 100644 --- a/apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts +++ b/apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ // 77 lines are CSS, the rest is one letter — RD-26 rewrites this reason, keeps the disable import { Component, DestroyRef, diff --git a/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts b/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts index 2690e0f..7cd9b69 100644 --- a/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts +++ b/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ // sample letter + labels + editor in one file — removed by RD-25 import { Component, computed, input, output } from '@angular/core'; import { DatePipe } from '@angular/common'; import { HeadingComponent } from '@shared/ui/heading/heading.component'; diff --git a/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts b/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts index 986dae0..4aaf099 100644 --- a/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts +++ b/apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ // single-step wizard shell — removed by RD-20 import { Component, computed, inject, input } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { FormFieldComponent } from '@shared/ui/form-field/form-field.component'; diff --git a/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts b/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts index 1bdb683..81f8414 100644 --- a/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts +++ b/apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ // one wizard shell for the intake steps — removed by RD-22 import { Component, computed, effect, inject, input, untracked } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { FormFieldComponent } from '@shared/ui/form-field/form-field.component'; diff --git a/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts b/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts index 25e3b19..26cc230 100644 --- a/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts +++ b/apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ // one wizard shell for 3 steps + upload — removed by RD-23 import { Component, computed, effect, inject, input, untracked } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { FormFieldComponent } from '@shared/ui/form-field/form-field.component'; diff --git a/apps/ssp/src/app/showcase/concepts.page.ts b/apps/ssp/src/app/showcase/concepts.page.ts index cb4eeee..829a216 100644 --- a/apps/ssp/src/app/showcase/concepts.page.ts +++ b/apps/ssp/src/app/showcase/concepts.page.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ // teaching page covering every concept — removed by RD-24 import { Component, computed, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import type { Resource } from '@angular/core'; diff --git a/docs/project/readable-codebase/RD-02-max-lines-guard.md b/docs/project/readable-codebase/RD-02-max-lines-guard.md new file mode 100644 index 0000000..f57c767 --- /dev/null +++ b/docs/project/readable-codebase/RD-02-max-lines-guard.md @@ -0,0 +1,118 @@ +# RD-02 — `max-lines` guard with a self-cleaning exemption list + +Status: done +Source: PLAN.md section 3a + +## Why + +The dashboard refactor proved a page can be 42 lines. Nothing stops the next one being 340 +again, and nothing stops this arc itself adding a new oversized file. The guard therefore +lands **before** the work it protects, not after. + +Seven files exceed the budget today. Each becomes a visible, dated to-do rather than a +silent one. + +## Read first + +- `eslint.config.mjs` — a flat array of 4 config objects. You add a 5th. +- `docs/project/readable-codebase/README.md` — the session protocol and GREEN. +- CLAUDE.md, "Enforced, not just hoped-for". + +## Decisions (pre-made, don't relitigate) + +1. **The rule.** `max-lines` at 250, `skipBlankLines: true`, `skipComments: true`, scoped to + `['{apps,libs}/**/*.{page,component,section,step}.ts']`. + +2. **The glob must include `section` and `step`.** `*.{page,component}.ts` does **not** match + `*.section.ts` — the file kind the dashboard refactor invented, and the kind RD-22 through + RD-26 create most of. Omitting them lets every new file this arc produces escape the guard. + This is the single most important line of the ticket. + +3. **Add `linterOptions: { reportUnusedDisableDirectives: 'error' }`, in this same commit.** + ESLint 9 only _warns_ by default, and `npm run lint` does not fail on warnings. At `error`, + a disable that is no longer needed becomes a lint failure — so **every later split commit + is forced to delete its own exemption or go red.** The exemption list cannot rot into + permanent debt. Verified clean on the current tree, so it lands green. + +4. **250 is chosen because the dashboard proves it is reachable**, not from a style guide. + +5. **Seven files, not eight or nine.** The original plan counted with `wc -l`; this rule + counts without blank lines or comments. `behandel-scherm.component.ts` (232) and + `stamdata-table-editor.component.ts` (236) were on the old list and **already pass** — + leave them alone. + +6. **Each disable names its reason and the ticket that removes it**, per CLAUDE.md's standing + rule. Use the exact mapping in Files below. Note `letter-canvas` is the one file whose + disable is **expected to stay** — RD-26 rewrites its reason rather than deleting it, + because 77 of its lines are CSS and the rest is one letter (PLAN.md 3d). + +## Files + +Add the config block to `eslint.config.mjs`, then a `/* eslint-disable max-lines */` header +with a one-line reason to each of these seven. Verified counts under the rule: + +| Rule lines | File | Removed by | +| ---------- | --------------------------------------------------------------------------------------------- | ----------------------- | +| 574 | `apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts` | RD-23 | +| 472 | `apps/ssp/src/app/showcase/concepts.page.ts` | RD-24 | +| 414 | `apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts` | RD-26 (rewrites, keeps) | +| 368 | `apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts` | RD-22 | +| 329 | `apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts` | RD-25 | +| 253 | `libs/shared/src/ui/rich-text-editor/rich-text-editor.component.ts` | RD-21 | +| 252 | `apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts` | RD-20 | + +## Steps + +1. Add the 5th config object to `eslint.config.mjs` with the rule from decision 1, and a + short comment saying why 250 and that the exemptions are the to-do list. +2. Add `linterOptions: { reportUnusedDisableDirectives: 'error' }` so it applies repo-wide. +3. Add the seven disable headers, each of the form + `/* eslint-disable max-lines */ // — removed by RD-NN`. +4. Update this ticket's `Status:` to `done` and the README's RD-02 row to `done`. +5. Commit all of it together. + +## Acceptance criteria + +Commands, not judgements: + +```bash +npm run lint # exits 0 +npm run ci # exits 0 +``` + +Then prove the guard actually bites, and that the glob covers section files: + +```bash +# 1. A new oversized section file must FAIL. Expect a max-lines error, then delete the file. +printf '/* x */\nexport class X {\n%s\n}\n' "$(for i in $(seq 260); do echo " p$i = $i;"; done)" \ + > apps/ssp/src/app/registratie/ui/dashboard/tmp-probe.section.ts +npx eslint apps/ssp/src/app/registratie/ui/dashboard/tmp-probe.section.ts # must report max-lines +rm apps/ssp/src/app/registratie/ui/dashboard/tmp-probe.section.ts + +# 2. Removing any one disable must FAIL (proves all 7 are load-bearing, none is decoration). +``` + +## Verification + +`npm run ci`. This ticket touches no story, no `.mdx` and no `libs/shared/src/ui/**` +_content_, so `--full` is not required — but `rich-text-editor.component.ts` does live under +`libs/shared/src/ui/`, and adding a comment line to it is harmless. Skip `--full`. + +## Out of scope + +- Splitting any of the seven files. That is RD-20 through RD-26. +- `behandel-scherm.component.ts` and `stamdata-table-editor.component.ts` — already compliant. +- The 7 non-component files over 250 lines (`brief.adapter.ts` 408, `upload.machine.spec.ts` + 364, `brief.store.spec.ts` 357, `registratie-wizard.machine.spec.ts` 287, + `registratie-wizard.machine.ts` 285, `upload.machine.ts` 256). The glob deliberately does + not reach specs, adapters or machines — this budget is about components. + +## Risks + +- **The inline-template processor can make `max-lines` fire twice per file** (once for the + `.ts`, once for the extracted virtual `.html`). Measured: it does **not** with this glob, + because the virtual path ends in `.html` and the glob ends in `.ts`. If a doubled report + appears, the glob is wrong — do not "fix" it by raising the limit. +- **`reportUnusedDisableDirectives` is repo-wide**, so it also polices the generated + `api-client.ts` header. That file is already in `ignores`, so it is out of reach. Confirmed + clean before this change. diff --git a/docs/project/readable-codebase/README.md b/docs/project/readable-codebase/README.md index 1cee96b..ac91dc6 100644 --- a/docs/project/readable-codebase/README.md +++ b/docs/project/readable-codebase/README.md @@ -96,7 +96,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di | ID | Ticket | Deps | `--full`? | Status | | ----- | ---------------------------------------------------------------------------- | ---------- | --------- | ------ | | RD-01 | Scaffold this backlog: README, PLAN, ticket template | — | | done | -| RD-02 | `max-lines` rule + `reportUnusedDisableDirectives` + 7 disables | 01 | | todo | +| RD-02 | `max-lines` rule + `reportUnusedDisableDirectives` + 7 disables | 01 | | done | | RD-03 | `overzicht` context: page + 2 nav sections, boundary edge, admin-links token | 02 | yes | todo | | RD-04 | Story titles to `Domein//`; add the missing stories | 03 | yes | todo | | RD-05 | `createStore` gains the effect map + specs | 02 | | todo | diff --git a/eslint.config.mjs b/eslint.config.mjs index 80ffb5c..392e245 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,13 @@ import angular from 'angular-eslint'; * `herregistratie` uncovered). */ export default [ + // A disable directive that no rule needs any more is a lint failure, repo-wide (no + // `files` key, so this applies everywhere — ESLint 9 flat config rule). This forces + // every later file split in the readable-codebase arc (RD-02) to delete its own + // `max-lines` exemption instead of leaving it as permanent debt. + { + linterOptions: { reportUnusedDisableDirectives: 'error' }, + }, { ignores: [ 'dist/**', @@ -48,4 +55,16 @@ export default [ ...c, files: ['{apps,libs}/**/*.html'], })), + + // The dashboard refactor proved a page can be 42 lines. This rule holds the rest of + // the app to that budget: 250 is reachable, not a style-guide default. The glob must + // include `section` and `step` — the dashboard refactor's own file kinds — or every + // file this arc creates escapes the guard. Each exemption below names its reason and + // the ticket that removes it, so the list is a dated to-do, not silent debt. + { + files: ['{apps,libs}/**/*.{page,component,section,step}.ts'], + rules: { + 'max-lines': ['error', { max: 250, skipBlankLines: true, skipComments: true }], + }, + }, ]; diff --git a/libs/shared/src/ui/rich-text-editor/rich-text-editor.component.ts b/libs/shared/src/ui/rich-text-editor/rich-text-editor.component.ts index 587c0c4..0cd6983 100644 --- a/libs/shared/src/ui/rich-text-editor/rich-text-editor.component.ts +++ b/libs/shared/src/ui/rich-text-editor/rich-text-editor.component.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ // toolbar + contenteditable logic in one component — removed by RD-21 import { Component, ElementRef, computed, effect, input, output, viewChild } from '@angular/core'; import { RichTextBlock, emptyBlock } from '@shared/kernel/rich-text'; import { adjacentChip, createChip, readBlock, renderInto } from './rich-text-dom';