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.0 KiB
3.0 KiB
WP-40 — PII kernel: branded Bsn VO + masked-value atom
Status: done Phase: 8 — platform/DX/showcase Priority: P2
Outcome
shared/kernel/bsn.ts—Bsnbranded VO +parseBsnwith the elfproef (11-test) checksum (+spec). Wired into the DigiD login boundary (digid.adapter): login now does real BSN validation instead of "any 9 digits" — login hint + e2e BSNs updated to a valid one (123456782).shared/kernel/pii.ts— puremaskBsn/maskTail/REDACTED(+spec), the functional core. Moved out ofdebug-state/mask.ts, which keeps onlyredactProfile(it depends on the registratieBigProfile, so it can't live inshared/kernel— boundary; debug-state is the sanctioned cross-context devtool).mask.specported topii.spec.<app-masked-value>atom (shared/ui/masked-value) + story — masked-by-default value with an optional reveal affordance; centralises the.includes('*')masked-detection thatbehandel-schermused to sniff inline (now refactored to the atom). The atom only emitsreveal; the caller keeps the step-up confirm + audited fetch.- Deviation (flagged):
Session.bsnstays typedstring— the brand guards the login ENTRY, but Session crosses a persistence boundary where the restore path deliberately drops the bsn (privacy) and JSON can't carry a brand, so re-typing it would add a''/nullable ripple for no real gain. BackendMaskTailtwin unchanged (still in sync).
Why
Masking is real but ad-hoc: pure helpers live in debug-state/mask.ts, the BSN is a bare string
in session.ts (no branded VO / checksum, unlike BigNummer/Postcode), and masked state is
detected by .includes('*') sniffing — no reusable atomic-design component. For a register this is
AVG art. 9 (BSN = special category) territory; the pieces should be first-class and reusable.
Decisions
Bsnbranded value object +parseBsnwith the elfproef (9 digits, weighted mod-11) checksum — parity with the other value objects. Replace the barestringinsession.ts.- Consolidate the pure maskers into
shared/kernel/pii.ts(functional core):maskBsn,maskTail,redactProfile, … Keep the backendMaskTailtwin in sync. <app-masked-value>atom (shared/ui) — masked by default, optional reveal affordance + a11y; replaces.includes('*')sniffing and composes into the behandel-scherm reveal.
Files
- New
src/app/shared/kernel/bsn.ts(+spec, elfproef cases) andshared/kernel/pii.ts(+spec). src/app/auth/domain/session.ts—bsn: Bsn.- New
src/app/shared/ui/masked-value/atom (+ story). src/app/shared/ui/debug-state/mask.ts— re-export/move to the kernel module.
Acceptance criteria
parseBsnaccepts valid elfproef numbers, rejects bad checksum/length/all-zeros (spec).<app-masked-value>renders masked by default with an optional reveal; behandel-scherm uses it (no more inline sniff).- Pure maskers have specs (no TestBed); backend
MaskTailparity noted;npm run cigreen.