Files
atomic-design-poc/docs/project/readable-codebase/RD-20-wizard-errors.md
T
ehoandClaude Opus 5 dff5f96bb3 docs: correct RD-20's file count, and record the ninth miss
RD-20 asserted `git grep -l "toWizardErrors"` would find 5 files, but its own
Steps list regenerates `behaviour-spec.mdx`, and the generator publishes every
`describe` title. Naming a spec after the function it tests puts the name in
the generated document too, so the honest count is 6.

The agent refused to rename the describe block to satisfy the number, which is
the correct response and matches the precedent from RD-14.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:01:01 +02:00

8.6 KiB

RD-20 — wizard-errors.ts + spec, adopted by all three wizards

Status: done Source: PLAN.md 3c

Why

Each of the three wizards flattens its per-field error record into the shell's WizardError[] summary, and each writes the flattening itself. Two of the three are the same six lines with a different key type. The third does the same job plus a nested group.

The loop is pure, it has no spec, and it lives in a component — so the one part of the wizard that decides what the user sees in the error summary is the one part no test covers.

Extract one pure helper beside naarStapLabel, which lives in that folder for exactly this reason.

Read first

  • libs/shared/src/layout/wizard-shell/wizard-shell.component.ts:19-27naarStapLabel and the WizardError interface. The new file sits beside this one.
  • The three call sites, in the order they get easier:
    • herregistratie-wizard.component.ts:253-259
    • intake-wizard.component.ts:361-368
    • registratie-wizard.component.ts:468-479 — the one with the nested group.
  • apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts:69Errors, declared as an interface. Decision 3 is about this line.

Decisions (pre-made, don't relitigate)

  1. One helper, two parameters, both used today:

    /** Flatten a machine's per-field error record into the shell's summary list.
        Values that are not a non-empty string are skipped, so a nested group
        (the registratie wizard's `antwoorden`) is appended by the caller with its
        own `idPrefix` rather than special-cased here. */
    export function toWizardErrors(errors: Record<string, unknown>, idPrefix = ''): WizardError[] {
      const out: WizardError[] = [];
      for (const [k, v] of Object.entries(errors)) {
        if (typeof v === 'string' && v) out.push({ id: idPrefix + k, message: v });
      }
      return out;
    }
    

    Record<string, unknown> plus the typeof guard narrows v to string with no cast, which keeps the file inside the repo's any-free rule. Do not reach for a generic key type; the three machines key their errors differently and the helper does not care.

  2. New file libs/shared/src/layout/wizard-shell/wizard-errors.ts, plus its spec. It takes the WizardError type from the component file with a type-only import:

    import type { WizardError } from './wizard-shell.component';
    

    import type is erased at compile time, so the helper stays pure and its spec needs no TestBed and pulls in no Angular at run time. Moving the WizardError interface into the new file would be tidier on paper and would touch four more files for no behaviour; not worth it.

  3. registratie-wizard.machine.ts:69 changes from interface Errors to type Errors. This is required, not cosmetic. TypeScript gives an implicit index signature to a type alias but not to an interface, so Errors as an interface is not assignable to Record<string, unknown> and the call site will not compile. The other two machines already declare their error maps as type aliases (Partial<Record<…, string>>), so this also makes the three consistent. Nothing extends or implements Errors — verified.

  4. The three call sites become:

    Wizard Body
    herregistratie toWizardErrors(this.editing()?.errors ?? {})
    intake toWizardErrors(this.answering()?.errors ?? {})
    registratie [...toWizardErrors(e), ...toWizardErrors(e.antwoorden ?? {}, 'vraag-')]

    The registratie site keeps its const e = this.invullen()?.errors ?? {}; line. The vraag- prefix and the skipping of non-string values together replace its if (k !== 'antwoorden' && typeof v === 'string' && v) filter.

  5. Do not touch the three shellStatus switches. The tags genuinely differ per machine and an exhaustive switch is the house style. This ticket is about the error list only.

  6. No story. The helper is a pure function. Its spec is the test; wizard-shell.stories.ts already renders the error summary.

Files

  • libs/shared/src/layout/wizard-shell/wizard-errors.ts (new)
  • libs/shared/src/layout/wizard-shell/wizard-errors.spec.ts (new)
  • apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts
  • apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts
  • apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts
  • apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts (decision 3, one line)
  • libs/shared/docs/behaviour-spec.mdx (regenerated, never hand-edited)

Steps

  1. Write wizard-errors.ts per decisions 1 and 2.
  2. Write wizard-errors.spec.ts. Cover: a flat record; an empty record; a record whose values are undefined or '' (both skipped); the idPrefix; and a record holding a nested object value (skipped, which is what lets decision 4's registratie case work).
  3. Apply decision 3 — one word, interfacetype, and the { stays.
  4. Convert the three call sites per decision 4.
  5. Run npm run gen:behaviour-spec — the new spec titles otherwise fail the drift check.
  6. git add -A, then run the acceptance commands.
  7. Update this ticket's Status: to done and the README's RD-20 row to done.
  8. Commit all of it together.

Acceptance criteria

Measured against the tree before handover.

git grep -c "export function toWizardErrors" -- libs/shared/src/layout/wizard-shell/wizard-errors.ts   # MUST be 1
git grep -l "toWizardErrors" -- apps libs | wc -l    # is 0 -> MUST be 6

Corrected after the ticket ran: 6, not the 5 first written. The sixth file is libs/shared/docs/behaviour-spec.mdx. Step 5 regenerates it, and the generator publishes every describe title — so describe('toWizardErrors', …) puts the name in the generated document. The helper, its spec, the three wizards, and the generated page.

The hand-rolled flattening is gone from all three:

git grep -c "filter((k) => e\[k\])" -- apps | awk -F: '{s+=$NF} END {print s+0}'   # is 2 -> MUST be 0
git grep -c "k !== 'antwoorden'" -- apps | awk -F: '{s+=$NF} END {print s+0}'       # is 1 -> MUST be 0

Decision 3 landed:

git grep -c "export interface Errors" -- apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts   # is 1 -> MUST be 0
git grep -c "export type Errors" -- apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts        # MUST be 1
npm run ci   # exits 0

Verification

npm run ci is enough. --full is not required: no story changes, and the new file is under libs/shared/src/layout/, not libs/shared/src/ui/. Regenerating behaviour-spec.mdx does not trigger --full on its own — RD-17 set that precedent.

npm run lint inside the gate is what proves the three /* eslint-disable max-lines */ directives are still needed. reportUnusedDisableDirectives is error, so a directive that stops being necessary fails the build.

Out of scope

  • Splitting any wizard into step components. RD-22 (intake) and RD-23 (registratie) own that, and this ticket makes both smaller first.
  • The shellStatus switches (decision 5).
  • change-request-form and besluit-form. Neither builds a WizardError[]; they render field errors directly. There is nothing to share.

Risks

  • The interfacetype change is load-bearing (decision 3). Skipping it produces "Index signature for type 'string' is missing in type 'Errors'" at the registratie call site, and the tempting wrong fix is to widen the helper's parameter to object, which forces an any and fails lint.
  • PLAN says this ticket deletes an eslint-disable max-lines from herregistratie-wizard.component.ts. That is stale — there is no such directive. Earlier tickets already brought the file under the budget: it measures ~248 effective lines against a limit of 250. The two disables that do exist (intake, registratie) stay; RD-22 and RD-23 remove them. Both files are far above 250 (~362 and ~574 effective), so this ticket's saving cannot make either directive unused.
  • behaviour-spec.mdx drift from the new spec titles. Run gen:behaviour-spec in the same commit, and never edit that file by hand.
  • Keep import type, not a value import (decision 2). A value import of the component into the helper drags Angular into a pure module and its spec.