# 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.