Move the adres, beroep and controle cases out of registratie-wizard.component.ts
into adres.step.ts, beroep.step.ts and controle.step.ts, matching RD-22's
*.step.ts convention. The parent drops from ~568 to 274 lines and loses its
`eslint-disable max-lines`.
The upload controller moves into beroep.step.ts and emits `uploadMsg` instead of
dispatching directly; the parent maps that back onto the machine's `Upload`
message. `onDiplomaKeuze` stays in the parent (message construction from the DUO
payload belongs in the container) and now takes only the chosen id, reading its
own `duoData` computed instead of receiving the DUO payload as an argument. Each
step injects `RegistratieLookupStore` directly for its own async presentation
(adresStatus, the DUO lookup, samenvattingVragen) — the sanctioned exception,
since it is a root singleton. Markup moved verbatim; the `@@` id count across
the directory stays 43.
Two of the ticket's acceptance numbers do not hold against correct code and are
corrected in the ticket file: `createUploadController` is 2 lines (import +
call), not 1 — `git grep -c` counts lines, and the same shape gives 2 for
`createStore` and 3 for `createDraftSync` elsewhere in this codebase. `dispatch`
is 1, not 0 — decision 4's mandated `UploadControllerDeps.dispatch` property
name is that string even though it is not the machine's dispatch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each wizard flattened its per-field error record into the shell's
WizardError[] summary with its own copy of the same loop. Extract one
pure helper, wizard-errors.ts, next to naarStapLabel. Add a spec that
covers a flat record, an empty record, skipped undefined/empty-string
values, the idPrefix, and a skipped nested object.
registratie-wizard.machine.ts changes Errors from an interface to a
type alias, because only a type alias gets an implicit index
signature and is assignable to the helper's Record<string, unknown>
parameter. The other two machines already declare their error maps as
type aliases, so this also makes the three consistent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
204 WP-NN/RB-NN comments named a closed ticket instead of the code they
sit next to. git blame already records history and stays correct when
code moves; the comment does not. This sweep removes the reference and
keeps the sentence, across 95 files in apps/ and libs/ plus the
behaviour-spec generator's header text.
Eleven references stay: five story files justify an a11y disable per
the README's rule, and one line in a11y.mdx documents that convention.
Two sentences needed a rewrite, not a deletion, so the reference's
meaning survives its removal. behaviour-spec.mdx is regenerated, not
hand-edited.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three dashboard-section stories used a fourth title segment
(`Domein/Registratie/Dashboard/<Name>`) that the sidebar rule does not have.
Drop the `Dashboard/` segment so all story titles follow the one rule from
CLAUDE.md decision 5.
Add the two missing stories for sections that have more than one visual
state: `wat-moet-ik-regelen` (4 states) and `wat-wilt-u-doen` (2 states, the
first story in the new `Domein/Overzicht/` bucket). `beheer-links` gets no
story — it has one visual state and its other branch renders nothing.
The `MetTaken` story disables the a11y addon with the same reason and WP-11
reference already used on `task-list.stories.ts` and `choice-list.stories.ts`:
`app-choice-link`'s host sits between the keuzelijst `<ul>` and its `<li>`,
a pre-existing structural gap this ticket does not fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Eight sites hand-rolled `rd.tag === 'Success' ? rd.value : fallback`. Six
take the new `successOr(rd, fallback)`, one takes the existing `successOf`,
and one (`big-profile.store.ts`) uses the existing `map`, since it returns a
RemoteData rather than an unwrapped value.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The wizard shell took two inputs to say one thing: a flat WizardStatus
string and a separate errorMessage input. Each wizard needed three
computeds (failedError, errorMessage, shellStatus) to take the state
apart and put it back together for the shell.
WizardPhase replaces both inputs with one discriminated union. Its
Failed variant carries the message directly, so no data travels through
a second channel. Each wizard now maps its own tags onto WizardPhase in
one computed, composing the localized failure prefix at the same spot
errorMessage did before. The three machines and their own vocabulary
(Editing/Answering/Invullen, Indienen/Ingediend/Mislukt) are unchanged;
only the shell's input contract changes.
The shell reads the Failed message via the existing whenTag helper,
because @switch cannot narrow a union in an Angular template.
Both $localize ids (wizard.indienenMislukt, regWizard.indienenMislukt)
keep byte-identical source text, so no locale file changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
Before this fix, a failed submit on the besluit-form or the
change-request-form left the user stuck. Both templates rendered `Failed`
through the same `@else` branch as the editable form. In besluit-form the
fields read from `Editing` only, so they went blank. In change-request-form
the fields still showed the sent value, but `SetField` only applies to
`Editing`, so typing did nothing. In both forms the submit button stayed
enabled, but `Submit` is a no-op outside `Editing`. The only escape was a
page reload.
After this fix, `Failed` gets its own template branch: an error message, a
read-only summary of what was sent (an `app-data-block`, reused from the
existing BRP-address pattern), and a "Opnieuw proberen" button that
dispatches `Retry`. Both machines already handle `Retry` (`Failed ->
Submitting` with the preserved data), so no machine change was needed.
Both components also move to `createStore`'s effect map (RD-05): the
`Submitting` handler replaces the hand-called `runIfSubmitting`, so
`onSubmit` is now a single `dispatch`. `runIfSubmitting`/`runIfIndienen`
now remain only in the three wizards, migrated later by RD-08.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The dashboard page lived inside registratie, a context with no permitted
outbound edges. Two of its sections do cross-context navigation: action
links to herregistratie/intake/brief/concepts, and an admin links section
that imported ADMIN_LINKS directly from the app shell.
Move the page and these two navigation sections into a new overzicht
context. Declare overzicht: ['registratie'] as the one sanctioned edge, so
the four data sections that stay in registratie/ui/dashboard/ remain
reachable. The route stays at /dashboard: it is a user-visible URL and four
e2e specs assert it, so only the import path and class name change.
beheer-links.section.ts now injects HEADER_ADMIN_LINKS, the same token the
site header uses, instead of importing the app shell's nav config directly.
Used npm run gen:context and corrected its three known-wrong outputs: the
overzicht: [] edge, the scaffolded overzicht.page.ts (the real page is the
moved dashboard.page.ts), and the scaffolded path: 'overzicht' route.
Verified the boundary is enforced, not decorative: a temporary import of a
herregistratie file into overzicht.page.ts makes dep:check fail, as
expected, then reverted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
BrpAddressDto's fields are generated as optional, so returning it
directly from parseBrpAddress lost the narrowing the runtime check
already did. This broke the build once registratie-lookup.store.ts
assigned the parsed address into a stricter local type. Map to a
proper BrpAddress domain shape at the trust boundary instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each dashboard section (Mijn aanvragen, Wat moet ik regelen, Mijn
registratie, Specialismen, Wat wilt u doen, Beheer) now owns its own
store access, async state, and template. DashboardPage becomes pure
composition.
Extract the repeated RemoteData Success-narrowing pattern into
successOf() and the dashboard sort/split logic into
sortForDashboard/concepten/ingediend, both with tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dashboard-view.dto.ts and brp-address.dto.ts each shadowed a generated
type: DashboardViewDto was declared twice (hand-written with required
fields, generated with everything optional), reconciled only by
structural typing. Both are gone.
dashboard-view.adapter.ts now imports the generated DashboardViewDto/
RegistrationDto/PersonDto/RegistrationStatusDto directly. Its parse
does real work now instead of an identity copy: parseRegistrationStatus
validates each status variant's required fields per-tag (the generated
type flattens the union, so a Geregistreerd row missing
herregistratieDatum previously passed the boundary unnoticed — it no
longer does). HerregistratieDecisions moves from contracts/ to
domain/registration.ts, so no contracts-typed value reaches a page.
brp.adapter.ts drops its own BrpAddressDto shadow the same way.
Hand-written contracts/*.dto.ts count: 4 -> 2 (duo-diplomas.dto.ts and
stamdata.dto.ts remain — both parse fields codegen can't type at all).
Part of the dashboard-readability refactor (see the approved plan).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wire said Application, the domain said Aanvraag — one aggregate with
two names at every hop. Rename the backend DTOs and the /applications
route to /aanvragen, regenerate the typed client, and rename the frontend
adapter/store to match.
Renamed: ApplicationSummaryDto/DetailDto, CreateApplicationRequest,
SubmitApplicationRequest/Response → Aanvraag* equivalents;
ApplicationsAdapter/Store → AanvragenAdapter/Store;
applications.adapter.ts/applications.store.ts → aanvragen.*.
Left untouched: the admin Case/Zaak vocabulary (/admin/cases,
AdminCasesStore) — a separate read model, not part of this rename; the
internal BigRegister.Domain.Applications namespace and the Applications
EF table (renaming those needs a new EF migration, out of scope here).
Part of the dashboard-readability refactor (see the approved plan).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four machine specs built their starting state with a hand-rolled object
literal instead of replaying real Msgs through the real reduce, the
exact anti-pattern ADR-0006 section 2 forbids. Three of the four also
hardcoded errors: {}, a shape the reducer might never actually produce.
intake.machine.spec.ts now imports the existing givenIntake from
intake.testing.ts (previously used only by intake.acceptance.spec.ts).
Three new one-line *.testing.ts files export the same given(reduce,
initial) wrapper for registratie-wizard, besluit, and brief. Every old
literal helper (answering, invullen, editingWith, loaded) is replaced
by a message replay that reaches the same state.
Two tests in registratie-wizard.machine.spec.ts asserted a cursor value
the real reducer cannot reach (cursor 2 with no diploma chosen yet,
which requires a diploma to already be set). Both are re-pointed at the
reachable cursor-1 equivalent; submit() validates the whole draft
regardless of cursor, so no assertion changed. Recorded in
implementation/rb-31.md, not worked around.
No *.machine.ts reducer was touched. All four specs pass; npm run ci
is green (lint, typecheck, dep:check, format, tokens, seam, all four
test suites, both app builds, backend 293/293, api-client drift).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
libs/shared/src/upload/ held a network adapter, an Elm machine, and two
application-layer coordinators outside the folder-per-layer convention every
other context follows. The dependency-cruiser rule carved an exception around
the misplaced adapter instead of the violation being fixed.
Move all five files to the layer each belongs to (git mv), update every
import across 24 consumer files, then delete the carve-out clause from
.dependency-cruiser.base.js. No export renamed, no file split, no spec
content changed.
Deleting the carve-out exposed a second, pre-existing rule violation:
ui-not-infrastructure had never fired against upload.adapter.ts because its
old path did not match /infrastructure/. Three UI components injected
UploadAdapter directly for its one-line contentUrl() wrapper. Route each
through the existing pure uploadContentUrl() function via the application
layer (upload-controller's new previewUrlFor, OrgTemplateStore's new
previewUrlFor) instead — the same idiom brief.store.ts already used.
npm run ci passes; dep:check is clean for both apps with the carve-out gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CQ-002: ApplicationsStore.cancel and AdminCasesStore.delete reached the raw
ApiClient and swallowed the failure in a bare catch, so a failed cancel made the
row reappear with no message. Both now fold through runSubmit and expose
lastError, which the two pages render.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
# libs/shared/docs/behaviour-spec.mdx
ApplicationsStore.cancel and AdminCasesStore.delete rolled an optimistic
write back on failure but showed no message — a bare catch with no
Result and no error channel (CQ-002). Both now call runSubmit and set a
lastError signal on failure, mirroring createSubmitChangeRequest in the
same folder. Each page renders the error with the existing app-alert
atom, the same pattern brief.page.ts already uses for lastError.
Added a spec file for ApplicationsStore (none existed) and extended
AdminCasesStore's spec, each asserting the rollback AND the surfaced
error. Verified both new assertions fail without the fix (an Edit
undo/redo of the store method, not git checkout).
Regenerated libs/shared/docs/behaviour-spec.mdx (gen:behaviour-spec) to
pick up the new/renamed test names. Marked RB-20 done in 99-backlog.md
and recorded the change in implementation/rb-20.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
createDraftSync mixed a read path (findConcept, load, the read half of
resume) with its write path (ensureId, flush, submit, reset) in one
187-line function -- CQ-001's finding. Move findConcept and loadConcept
into a new application/find-concept.ts as free functions that take the
adapter, so they get a direct spec with no Angular TestBed.
createDraftSync keeps the closure state (id, ensuring, resumeGate) and
the whole write path unchanged -- this is a move, not a redesign. The
resumeGate coupling that lets the write path wait for the read path
stays exactly where it was.
createDraftSync shrinks from 187 to 169 lines. draft-sync.spec.ts is
unchanged -- it never called resume()/load() directly, and its 409
recovery test for submit() still exercises the extracted findConcept
through ensureId's catch branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three seams WP-71 documented but left unguarded.
Deletes the FE's isHerregistratieEligible and isStatusConsistent — both
uncalled, the first dead by its own doc-comment. Their tests used fixtures
completely disjoint from the backend's (the backend even had an exact-window
boundary case the FE lacked), so the two sides could diverge indefinitely
without failing anything. CLAUDE.md's policy of keeping server-owned rules
as FE "reference impls" is what kept them alive, so it is amended: the FE may
mirror a server-supplied value for instant feedback, never reimplement the
algorithm. registration.policy.ts keeps its three live exports.
check-seam.sh now also guards the Besluit tag list — the C# enum and the TS
BESLUIT_TAGS array are identical ordered name lists with nothing linking
them, and Enum.TryParse fails at request time rather than build time. Anchored
on the full declaration so it avoids the "greps all matches" trap WP-69 hit.
The phone-format divergence turned out to be real, not latent as recorded:
the backend returned 422 for +31612345678 and (06) 12345678, both of which
the FE's own parseTelefoonnummer accepts. A grep check would have compared
the shared ^0\d{9}$ regex and reported all clear — the difference was in
stripping. RejectPhoneChange now strips what the FE strips, pinned by a
contract test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bdd.mdx previously banned "Given/When/Then ceremony" outright, which
directly contradicted WP-70's own acceptance tests (Acceptance/
BesluitLifecycleTests.cs already used // Given/When/Then comments) and
the backend's organically-evolved PascalCase_snake_sentence convention,
which the doc gave zero guidance for. Reverses that rule: every test is
now structured Given -> When -> Then, with a genuinely empty phase
omitted rather than faked; present-tense declarative naming and the
one-behaviour-per-test rule are unchanged. ADR-0006 gets a cross-reference
so both documents agree everywhere, not just in acceptance tests.
Also closes out the doc's other named-but-unenforced rules found by the
audit: fixes the 5 files asserting rendered $localize copy instead of
the underlying tag/message-id (the compliant pattern already existed in
werkvoorraad-item-view.spec.ts), splits the multi-behaviour titles the
doc itself calls a smell (";", "and", "/"), and fixes bdd.mdx's own false
citation of registratie-wizard.machine.spec.ts as "one transition per
test" by actually splitting that test into one-transition-per-test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RuleTests.cs held five aggregates' rules as nested classes in one file,
misaligned with Domain/<Aggregate>/ and with the Acceptance/Builders/
folder convention WP-70 started. Split into Domain/<Aggregate>RuleTests.cs
(pure move — same names, same bodies, same count) plus a new
ApplicationRuleTests.cs (the enum invariant moved out of the
WebApplicationFactory-booting ApplicationTests.cs, since it's a pure
Enum.GetNames check with no business needing a web host) and
OrgTemplateRuleTests.cs (RejectDraft had no direct unit test before,
only endpoint coverage).
libs/shared/docs/layers.mdx still taught the pre-WP-67 shape (six
contexts, no apps/libs split, enforcement via ESLint) — updated to the
real monorepo structure and to dependency-cruiser as the actual
enforcement mechanism. Adds specs for registration.policy.ts's
isStatusConsistent (untested; its backend mirror is) and both apps'
auth/domain/session.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ESLint blanket-exempted every *.spec.ts from the any ban, and no gate
type-checked spec files at all (ng test is transpile-only), so a wrong
cast in a test could never fail the build. 76 `as any` + 12 `as
Extract<>` state-narrowing casts in the three biggest wizard specs read
one variant's fields off a whole-union value: if the reducer returned
the wrong variant, the assertion silently read undefined instead of
failing.
expectTag(state, tag) (libs/shared/src/testing/expect-tag.ts) asserts
and narrows in one call, replacing every one of those casts. Removes
the spec-file any exemption, adds `npm run typecheck` (tsc --noEmit
over each project's tsconfig.spec.json) to CI, and forbids production
code from importing libs/shared/src/testing via dependency-cruiser.
Backend: AanvraagBuilder now models ZaakUrl (closing the last
post-Build() mutation) and guards AtStep; null-forgiving `!` on
endpoint assertions replaced with Assert.NotNull so a null DTO fails by
name, not NullReferenceException.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every machine spec redefined its own throwaway fixture helper (editing1/2/3,
editingWith), hardcoding fields like errors: {} that assert against shapes
the reducer may never actually produce. given(reduce, initial)(...msgs)
(libs/shared/src/testing/machine.ts) replaces them by replaying real Msgs
through the real reduce, so a fixture is provably reachable. Adds the same
idiom for value objects (unwrapOk) and RemoteData (loading/success/failure),
plus intake.acceptance.spec.ts as a worked full-journey example.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Restructures into apps/ssp + apps/behandelportal (two Angular projects)
plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's
separate sibling repo. That split had already produced real drift: a
hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree
forked and silently diverging (7 files), and beheer + the styles.scss
token bridge duplicated byte-for-byte across both repos.
- git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/,
environments/, the Storybook docs/*.mdx, and styles.scss into
libs/shared + libs/beheer (all confirmed identical between the two
repos before merging). auth stays deliberately duplicated per
ADR-0002 (actor-specific, expected to diverge) - amended there.
- One generated API client (libs/shared), no more vendored swagger.json.
- .dependency-cruiser split into a base factory + one config per app,
and Storybook into .storybook-ssp/.storybook-behandelportal - both
forced by the @auth/* alias resolving to different directories per app.
- SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/
HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies
its own nav/admin-links/dev-panel instead of one being hardcoded.
- CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated;
WP-67 backlog entry documents the full decision trail.
npm run ci green (lint, dep:check x2, 360 tests across ssp/
behandelportal/shared/beheer, both localized builds, backend tests,
snippet + api-client drift); both dev servers, both Storybook
instances, and docker compose verified working.
The old sibling repo (/home/eho/repos/behandelportal) is left
untouched, not deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>