Two backlog trees are complete: `docs/project/backlog/` (75 files, every WP done) and `docs/project/refactor-backlog-setup/` (the arc before it). Move both under `docs/project/archive/` with `git mv`, so history stays intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them, because it points at the now-archived backlog README. Add `docs/project/archive/README.md`. It states that these trees are historical and names the two directories that are still live. Repoint every inbound reference named in RD-30's Files table: CLAUDE.md, the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the `document-feature` and `new-ssp` skills, and the readable-codebase PLAN, README, and RD-19 ticket. Fix two upward-relative links inside the moved WP files (WP-68, WP-69) that gained a directory level and would otherwise break. Repoint `.prettierignore`'s two agent-prompt exclusions to their new path, so prettier keeps leaving those files' exact wording alone. Mark RD-30 done and check off its acceptance criteria; flip its README row to done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.4 KiB
WP-75 — Close the remaining FE/BE seams
Status: done (6fa27d1)
Phase: 12 — DDD hardening
Why
WP-71 added scripts/check-seam.sh guarding one literal pair (the scholing threshold) and
documented three further FE/BE duplications that nothing tested across the seam. This closes
them — two by deletion, one by a guard, one by an actual fix.
Decisions (pre-made)
- Dead reference impls get deleted, and
CLAUDE.mdis amended. This overturns the documented policy that server-owned rules "stay indomain/*.policy.tsas reference impl + unit test". That policy is precisely what kept dead code alive. Blast radius is small:registration.policy.tsis the only*.policy.tsin the repo. - Guard the
Besluittag list by extendingcheck-seam.sh, not adding a second script. - The phone seam gets a contract test, not a grep check — see below.
Acceptance criteria
isHerregistratieEligibledeleted (uncalled; dead by its own doc-comment) along withisStatusConsistent(also uncalled — WP-71 had added a spec for it the session before). The three live exports (statusLabel,statusColor,herregistratieDeadline) stay, andherregistratieDeadlinegained direct coverage it previously only had transitively.CLAUDE.mdamended: server-owned rules live only on the server; the FE may mirror a server-supplied value (a threshold, a bound) for instant feedback, but never reimplements the algorithm. ADR-0001's matching claim aligned.check-seam.shguards theBesluittag list, proven to fail when a fourth member is added to the C# enum only, naming both files and both lists. Anchored on the full declaration so it avoids the "greps all matches" trap WP-69 documented.- Phone contract test added and green; backend stripping fixed.
The phone divergence was real, not latent
WP-71 recorded this as latent because the Angular app normalises before sending — true of that
path. The contract test proved the two sides genuinely disagreed: the backend returned 422
for +31612345678 and (06) 12345678, both of which the FE's own parseTelefoonnummer
accepts. Any non-Angular client, crafted POST, or future FE change would have hit it.
SubmissionRules.RejectPhoneChange now strips exactly what the FE strips ([\s\-()], then a
leading +31 → 0) before applying the shared ^0\d{9}$. The FE value object was not touched —
it is the more permissive and correct side.
Why a contract test rather than a grep check: both sides carry the identical ^0\d{9}$
literal, so a drift check would have compared them, found them equal, and reported all clear.
The divergence was in the normalisation before the regex — invisible to text comparison. Worth
remembering when choosing between the two guard styles: grep checks catch drifting constants,
contract tests catch drifting behaviour.
Verification
npm run check:seam # both checks OK
npm run ci
cd backend && dotnet test BigRegister.slnx --filter "Category!=Integration"
Follow-ups
- Making
Besluitflow through the generated client as an enum rather than astringwould remove that seam entirely rather than guarding it — a wire change, so not done here. - The herregistratie-eligibility seam is closed by deletion; if a FE mirror is ever reintroduced, the disjoint-fixture problem returns and would need a contract test, not a grep check.