Files
atomic-design-poc/docs/project/readable-codebase/RD-08-wizards-to-effect-map.md
T
ehoandClaude Sonnet 5 b8aced75e3 refactor: migrate the 3 wizards to the effect map and Primary (RD-08)
The three wizards paired a dispatch with a hand-written effect call
(onPrimary/onRetry + runIfSubmitting/runIfIndienen). A missed call failed
silently. RD-05 added the effect map and RD-07 added the Primary message;
this ticket moves each wizard onto both.

Each wizard now registers its submit effect on createStore, keyed by its
own submitting tag (Submitting for herregistratie and intake, Indienen for
registratie — the type catches a wrong key at compile time). The optimistic
begin/confirm/rollback calls stay inside the effect body, unchanged. The
template dispatches Primary and Retry directly, matching how Back already
worked. onPrimary, onRetry, and runIfSubmitting/runIfIndienen are deleted
from all three components.

herregistratie-wizard drops under the 250-rule-line budget, so its
eslint-disable max-lines header is removed in this same commit (RD-02's
self-cleaning mechanism). intake-wizard and registratie-wizard stay over
budget and keep theirs, both already flagged for RD-22/RD-23.

Three doc comments (in the three machine files, plus one in store.ts) named
the deleted onPrimary()/runIfSubmitting() identifiers in prose. Reworded
them so the "idiom is gone from the repo" grep check is not defeated by its
own explanatory comments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 17:20:52 +02:00

166 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RD-08 — Migrate the 3 wizards to the effect map and `Primary`
Status: done
Source: PLAN.md 1a
## Why
This is the ticket that removes the silent-failure idiom from the last three call sites. Each
wizard still pairs a `dispatch` with a hand-written effect call, and forgetting the second
line fails silently:
```ts
onPrimary() { … this.dispatch(…); } // decides Next vs Submit in the UI
onRetry() { this.dispatch({ tag: 'Retry' }); this.runIfSubmitting(); }
```
RD-05 added the effect map; RD-07 added `Primary`. After this ticket the wizard shell's five
outputs all map 1:1 onto messages in the template, and each component loses three methods.
## Read first
- `libs/shared/src/application/store.ts` — the effect map, its trigger rule, and the `Seed`
exemption
- The three machines' new `primary` exports (RD-07)
- `apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts`
— `onPrimary` 256, `onRetry` 263, `runIfSubmitting` 276-288
- `apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts` — `onPrimary`
368, `onRetry` 375, `runIfSubmitting` 387-406
- `apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts` —
`onPrimary` 613, `onRetry` 620, `runIfIndienen` 635-644
- `docs/project/readable-codebase/RD-06-…md` — the same migration, already done for the two
single-step forms. Copy its shape.
## Decisions (pre-made, don't relitigate)
1. **The effect-map key is the machine's own submitting tag, and it is not the same for all
three:**
| Wizard | Effect-map key |
| ----------------------- | -------------- |
| `herregistratie-wizard` | `Submitting` |
| `intake-wizard` | `Submitting` |
| `registratie-wizard` | **`Indienen`** |
`StoreEffects` keys are typed as `Model['tag']`, so writing `Submitting` for the registratie
wizard is a **compile error**, not a silent no-op. That is the type doing its job — do not
work around it by widening the type.
2. **Move each effect body verbatim into the map.** The narrowed state arrives as argument one,
so delete the `const s = this.state(); if (s.tag !== '…') return;` preamble and use the
parameter. Keep everything else identical, including the optimistic store calls.
3. **The optimistic `begin`/`confirm`/`rollback` calls stay inside the effect body**, exactly
where they are today (`herregistratie-wizard:279,283,286`; `intake-wizard:390,401,404`).
The effect slot is the sanctioned place for side effects, so `reduce` stays pure. The
registratie wizard has **no** optimistic calls — do not add any.
4. **Delete `onPrimary` and `onRetry` entirely** and dispatch from the template, matching how
`(back)` already does it:
```text
(primary)="dispatch({ tag: 'Primary' })"
(retry)="dispatch({ tag: 'Retry' })"
(back)="dispatch({ tag: 'Back' })" <-- already this shape today
```
`Retry` needs no effect call because `Failed → Submitting`/`Indienen` is a tag transition,
so the map fires it. This is the 1:1 output-to-message mapping
`.claude/skills/form-machine/SKILL.md:74-78` already claims.
5. **Leave the two `dispatch` calls that sit inside Angular `effect()`s alone** —
`intake-wizard`'s `SetPolicy` and `registratie-wizard`'s `PrefillAdres`. Both land on an
unchanged tag, so no effect fires, and both are already `untracked`. Do not key an effect on
an editing tag (`Editing`/`Answering`/`Invullen`) — that is the livelock
`store.spec.ts:18-31` guards against.
6. **Preserve the WP-69 comment in `intake-wizard`'s effect body verbatim**, including its
ticket reference. It explains why the scholing answer rides along for server-side
re-validation. RD-19 strips ticket prefixes across the repo later; do not do it early and do
not drop the explanation.
7. **If a wizard drops below 250 rule-lines, delete its `eslint-disable max-lines` header in
this same commit.** See Risks — this is expected for `herregistratie-wizard` and is
RD-02's mechanism working, not a problem to route around.
## Files
- `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`
No machine changes. No story changes expected. No xlf changes.
## Steps
1. For each wizard: register the effect body on `createStore` under the key from decision 1,
dropping the guard preamble.
2. Delete `runIfSubmitting` / `runIfIndienen`.
3. Delete `onPrimary` and `onRetry`; dispatch `Primary` and `Retry` from the template
(decision 4).
4. Run `npm run lint`. If a file is now under budget, delete its `eslint-disable max-lines`
header (decision 7).
5. Update this ticket's `Status:` to `done` and the README's RD-08 row to `done`.
6. Commit all of it together.
## Acceptance criteria
The idiom is gone from the whole repo. Anchor on the **declaration**, not on any occurrence
of the name:
```bash
grep -rn "runIfSubmitting\|runIfIndienen" apps libs # MUST return nothing
grep -rnE "^ (onPrimary|onRetry)\(\)" apps libs # MUST return nothing
```
The second pattern is anchored deliberately. A bare `grep -rn "onPrimary\|onRetry"` **cannot
pass**: `libs/shared/src/application/upload-controller.ts:94` has an unrelated
`onRetry(localId)` method, called from two wizard templates as
`uploadCtl.onRetry($event)`. Those three matches are correct code and must stay.
Behaviour is unchanged. Walk each wizard end to end with `npm start`:
1. The primary button advances through every step and submits on the last one.
2. A failed submit shows the error, and Retry re-submits (it must leave `Failed`).
3. Clicking the primary button twice quickly submits **once** — the trigger rule's
tag-transition condition gives this for free.
4. For the two herregistratie wizards, the dashboard's "herregistratie in behandeling" notice
still appears after submit and disappears after a rollback.
```bash
npm run ci # exits 0
npm run ci --full # exits 0 — the wizards all have stories
```
## Verification
`npm run ci --full`. `--full` is required: all three wizards have stories, and
`registratie-wizard.stories.ts:88`, `intake-wizard.stories.ts:38` and
`herregistratie-wizard.stories.ts:70` each seed a submitting state.
## Out of scope
- Splitting any wizard into step components. RD-22 and RD-23, after RD-20.
- `WizardStatus` → `WizardPhase`. RD-10.
- Stripping the WP-69 ticket reference (decision 6). RD-19.
- Touching `shellStatus`, `errorList`, or any other computed. Only the three methods and the
template bindings change here.
## Risks
- **`herregistratie-wizard` is 252 rule-lines and will likely drop below 250.** Deleting three
methods (~21 lines) and adding a map registration (~12) nets roughly −9. Its
`eslint-disable max-lines` then becomes unnecessary and
`reportUnusedDisableDirectives: 'error'` **fails the build**. That is RD-02's self-cleaning
mechanism doing its job: delete the header (decision 7). RD-20 also expects to remove it —
whichever ticket gets there first removes it, and the other finds nothing to do.
- **The Storybook trap.** All three wizards seed a submitting state in their stories with a
real `provideHttpClient()` and no request mocking. RD-05's `Seed` exemption is what stops
them firing real network calls. If a story flips to a failed state on load, or
`storybook-a11y` goes red, the exemption is not working — fix that, do not delete the story.
- **Double-submit protection is now structural, not incidental.** The old code could submit
twice if `dispatch` and the effect call were paired twice. The tag-transition rule prevents
it. Do not add a `busy` guard on top; that would be a second mechanism for one rule.
- **`behaviour-spec.mdx` drift** if you add or rename a spec. Run `npm run gen:behaviour-spec`
in the same commit if so.