feat: add Primary message to the 3 wizard machines (RD-07)
Each wizard component re-derives the step-boundary decision the reducer already owns: advance on a middle step, submit on the last step. This ticket moves that decision into the machine, so RD-08 can replace the component's guard with one dispatch. Add a `Primary` message to each Msg union, and export a `primary(s)` function next to the existing `next`/`submit` pair. `primary` is a three-line branch that delegates to `next`/`submit` and writes no new validation. Each machine tests "last step" in its own vocabulary, per the ticket's Decisions block: `herregistratie` checks `step === 3`, `intake` checks `currentStep(s) === 'review'`, `registratie` checks `currentStep(s) === 'controle'`. `Next` and `Submit` stay in every union and every reducer — `Primary` is purely additive. Add 3 spec cases per machine (9 total): Primary advances from a non-final step, Primary submits from the final step, and Primary is a no-op outside the editing state. Each case also asserts the equivalence the ticket requires for RD-08's migration: `reduce(s, Primary)` equals `reduce(s, Next)` at a non-final step, and equals `reduce(s, Submit)` at the final step. Regenerate `behaviour-spec.mdx` for the 9 new `it()` titles. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
# RD-07 — Move the step-boundary decision into the 3 wizard machines
|
||||
|
||||
Status: done
|
||||
Source: PLAN.md 1a
|
||||
|
||||
## Why
|
||||
|
||||
Each wizard component decides in the UI whether the primary button means "next step" or
|
||||
"submit", duplicating a rule the reducer already owns:
|
||||
|
||||
| Component | Line | Body |
|
||||
| ------------------------------------ | ------- | -------------------------------------------------------------------------------- |
|
||||
| `herregistratie-wizard.component.ts` | 256-259 | `if (s.tag !== 'Editing') return;` then `s.step < 3 ? Next : Submit` |
|
||||
| `intake-wizard.component.ts` | 368-371 | `if (s.tag !== 'Answering') return;` then `step() === 'review' ? Submit : Next` |
|
||||
| `registratie-wizard.component.ts` | 613-616 | `if (s.tag !== 'Invullen') return;` then `step() === 'controle' ? Submit : Next` |
|
||||
|
||||
Each already-exported `next`/`submit` pair holds the real transition, so the component is
|
||||
re-deriving a decision the machine can make. After this ticket, `onPrimary()` in RD-08 becomes
|
||||
a single `dispatch({ tag: 'Primary' })`, and the wizard shell's `primary`/`back`/`retry`
|
||||
outputs map 1:1 onto messages — which is what `.claude/skills/form-machine/SKILL.md:74-78`
|
||||
already claims they do.
|
||||
|
||||
This ticket is **domain-only**: machines and their specs. No component changes.
|
||||
|
||||
## Read first
|
||||
|
||||
- `apps/ssp/src/app/herregistratie/domain/herregistratie.machine.ts` — `next` at 87,
|
||||
`submit` at 118, `reduce`'s `Next`/`Submit` cases at 165/171
|
||||
- `apps/ssp/src/app/herregistratie/domain/intake.machine.ts` — `currentStep` at 86, `next` at
|
||||
173, `submit` at 197, cases at 226/232
|
||||
- `apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts` — `currentStep` at 96,
|
||||
`next` at 265, `submit` at 284, cases at 337/343
|
||||
- The three matching `*.machine.spec.ts` files
|
||||
- CLAUDE.md decision 3 (state, and the naming rule for machines)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
1. **Add `{ tag: 'Primary' }` to each of the three `Msg` unions**, plus an exported
|
||||
`primary(s)` function, plus a `case 'Primary'` in each `reduce` that delegates to it. Same
|
||||
shape as the existing `Next`/`Submit` cases.
|
||||
|
||||
2. **Express "last step" in each machine's own vocabulary. Do NOT invent a shared
|
||||
`isLastStep` helper.** The three state shapes genuinely differ:
|
||||
- `herregistratie`: `Editing` carries `step: 1 | 2 | 3` and there is **no `STEPS` array** →
|
||||
the test is `s.step === 3`.
|
||||
- `intake`: `Answering` carries `cursor: number` against
|
||||
`STEPS = ['buitenland','werk','review']` → the test is `currentStep(s) === 'review'`.
|
||||
- `registratie`: `Invullen` carries `cursor: number` against
|
||||
`STEPS = ['adres','beroep','controle']` → the test is `currentStep(s) === 'controle'`.
|
||||
|
||||
Two of the three could share a `cursor === STEPS.length - 1` form, but the third cannot.
|
||||
A helper covering two of three, plus a special case, is more to read than three plain
|
||||
expressions.
|
||||
|
||||
3. **`primary` delegates to the existing exported `next` and `submit`.** Write no new
|
||||
validation and duplicate no transition logic. The whole function is a branch:
|
||||
|
||||
```ts
|
||||
export function primary(s: WizardState): WizardState {
|
||||
if (s.tag !== 'Editing') return s;
|
||||
return s.step === 3 ? submit(s) : next(s);
|
||||
}
|
||||
```
|
||||
|
||||
(…and the equivalent, in its own vocabulary, for the other two.)
|
||||
|
||||
4. **The guard moves into the machine.** `primary` returns `s` unchanged when the state is not
|
||||
the editing state, so RD-08 can delete the component preamble. Note the editing tag differs
|
||||
per machine: `Editing`, `Answering`, and `Invullen`. **`Invullen`/`Indienen`/`Ingediend`/
|
||||
`Mislukt` are correct Dutch domain tags per CLAUDE.md — do not "fix" them to English.**
|
||||
|
||||
5. **KEEP `Next` and `Submit` in all three `Msg` unions.** Verified: they are dispatched
|
||||
across **9 spec files**, including `intake.acceptance.spec.ts`, which uses them as a
|
||||
readable behaviour narrative, and the three `*-has-progress.spec.ts` files. Removing them
|
||||
would rewrite dozens of spec lines for no gain. `Primary` is purely **additive**.
|
||||
|
||||
6. **Do not touch any component.** RD-08 migrates the three wizards. If you edit a
|
||||
`*.component.ts` in this ticket, it is out of scope.
|
||||
|
||||
7. **Keep `SCHOLING_THRESHOLD_DEFAULT` greppable.** `npm run check:seam` greps for it as a
|
||||
top-level `export const` in `intake.machine.ts:43`. Do not move or inline it.
|
||||
|
||||
## Files
|
||||
|
||||
- `apps/ssp/src/app/herregistratie/domain/herregistratie.machine.ts` + `.spec.ts`
|
||||
- `apps/ssp/src/app/herregistratie/domain/intake.machine.ts` + `.spec.ts`
|
||||
- `apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts` + `.spec.ts`
|
||||
|
||||
## Steps
|
||||
|
||||
1. In each machine: add `{ tag: 'Primary' }` to the `Msg` union, export `primary(s)` next to
|
||||
`next`/`submit`, and add the `case 'Primary'` to `reduce`.
|
||||
2. In each machine spec: add the three cases from Acceptance below.
|
||||
3. Run `npm run gen:behaviour-spec` — new `it()` titles otherwise fail the drift check.
|
||||
4. Update this ticket's `Status:` to `done` and the README's RD-07 row to `done`.
|
||||
5. Commit all of it together.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
Three cases per machine spec (9 total), pure `reduce` calls, no TestBed:
|
||||
|
||||
```
|
||||
- Primary advances to the next step from a non-final step
|
||||
- Primary submits from the final step
|
||||
- Primary is a no-op from a non-editing state
|
||||
```
|
||||
|
||||
The third case is what lets RD-08 delete the component guard, so do not skip it.
|
||||
|
||||
```bash
|
||||
npm test # exits 0
|
||||
npm run ci # exits 0
|
||||
```
|
||||
|
||||
Prove `Primary` produces exactly what the components produce today, so the migration in RD-08
|
||||
is behaviour-preserving. For each machine, these must be equal:
|
||||
|
||||
```
|
||||
reduce(s, { tag: 'Primary' }) === reduce(s, { tag: 'Next' }) // at a non-final step
|
||||
reduce(s, { tag: 'Primary' }) === reduce(s, { tag: 'Submit' }) // at the final step
|
||||
```
|
||||
|
||||
Prove nothing was removed:
|
||||
|
||||
```bash
|
||||
grep -c "tag: 'Next'\|tag: 'Submit'" apps/ssp/src/app/herregistratie/domain/intake.machine.ts
|
||||
# Next and Submit must still be in the union and still handled in reduce
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
`npm run ci`. This ticket touches no story, no `.mdx` and no component, so `--full` is not
|
||||
required.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The three components. RD-08.
|
||||
- `besluit.machine.ts` and `change-request.machine.ts` — single-step forms with no step
|
||||
boundary, so `Primary` would mean nothing there. RD-06 handles those two.
|
||||
- Removing `Next`/`Submit` (decision 5).
|
||||
- Renaming the Dutch tags in `registratie-wizard.machine.ts` (decision 4).
|
||||
|
||||
## Risks
|
||||
|
||||
- **`behaviour-spec.mdx` drift.** 9 new `it()` titles across 3 spec files.
|
||||
`scripts/ci-local.sh` regenerates `libs/shared/docs/behaviour-spec.mdx` from a path-sorted
|
||||
walk of spec titles and fails on any drift. Run `npm run gen:behaviour-spec` in the same
|
||||
commit.
|
||||
- **`check:seam`** greps `SCHOLING_THRESHOLD_DEFAULT` (decision 7) and
|
||||
`besluit.machine.ts`'s `BESLUIT_TAGS`. Neither should move, but if `check:seam` fails, that
|
||||
is why.
|
||||
- **`snippets.generated.ts` drift.** `intake.machine.ts:59` carries a
|
||||
`// #region showcase:steps` marker around `STEPS`. If your edit moves or splits that region,
|
||||
run `npm run gen:snippets` in the same commit.
|
||||
- **Do not make `primary` clever.** It is a three-line branch delegating to two existing
|
||||
functions. If it grows validation, error mapping, or a cursor calculation, the transition
|
||||
logic has been duplicated instead of reused.
|
||||
@@ -101,7 +101,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
|
||||
| RD-04 | Story titles to `Domein/<Context>/<Name>`; add the missing stories | 03 | yes | todo |
|
||||
| RD-05 | `createStore` gains the effect map + specs | 02 | | done |
|
||||
| RD-06 | **Bug fix:** 2 single-step forms to the effect map + retry affordance | 05 | yes | done |
|
||||
| RD-07 | Add `Primary` to the 3 wizard machines + specs | 05 | | todo |
|
||||
| RD-07 | Add `Primary` to the 3 wizard machines + specs | 05 | | done |
|
||||
| RD-08 | Migrate the 3 wizards to the effect map + `Primary` | 07 | yes | todo |
|
||||
| RD-09 | **Docs + generator:** `form-machine.hbs`, ARCHITECTURE, fp-tea, skill | 08 | | todo |
|
||||
| RD-10 | `WizardStatus` to a payload-carrying `WizardPhase` | 08 | yes | todo |
|
||||
|
||||
Reference in New Issue
Block a user