# 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) 1. **Dead reference impls get deleted, and `CLAUDE.md` is amended.** This overturns the documented policy that server-owned rules "stay in `domain/*.policy.ts` as reference impl + unit test". That policy is precisely what kept dead code alive. Blast radius is small: `registration.policy.ts` is the only `*.policy.ts` in the repo. 2. Guard the `Besluit` tag list by **extending** `check-seam.sh`, not adding a second script. 3. The phone seam gets a **contract test**, not a grep check — see below. ## Acceptance criteria - [x] `isHerregistratieEligible` deleted (uncalled; dead by its own doc-comment) along with `isStatusConsistent` (also uncalled — WP-71 had added a spec for it the session before). The three live exports (`statusLabel`, `statusColor`, `herregistratieDeadline`) stay, and `herregistratieDeadline` gained direct coverage it previously only had transitively. - [x] `CLAUDE.md` amended: 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. - [x] `check-seam.sh` guards the `Besluit` tag 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. - [x] 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 ```bash npm run check:seam # both checks OK npm run ci cd backend && dotnet test BigRegister.slnx --filter "Category!=Integration" ``` ## Follow-ups - Making `Besluit` flow through the generated client as an enum rather than a `string` would 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.