Files
atomic-design-poc/docs/project/archive/refactor-backlog-setup/refactor-backlog/implementation/rb-10.md
T
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
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>
2026-09-08 23:00:38 +02:00

6.9 KiB
Raw Blame History

RB-10 — extract parseStoredSession (both apps) and spec redactProfile

Status: implemented · 2026-08-27 · Source findings: 02-testability.md TE-001 (ssp/auth and bhp/auth) · 07-bio2-compliance.md BIO-017 · 99-backlog.md RB-10

What was wrong

SessionStore.restore() — identical in apps/ssp/src/app/auth/application/session.store.ts and apps/behandelportal/src/app/auth/application/session.store.ts — called localStorage.getItem(STORAGE_KEY) itself and did the parse + shape validation in the same module-private function. It was invoked from a field initializer (private _session = signal<Session | null>(restore())), so the storage read happened the instant the singleton was constructed; a spec could not feed it a raw string without stubbing the localStorage global before the injector built the store.

The logic behind that guard is a trust boundary, not incidental validation — the comment above it names two guarantees: G1 (never persist the BSN) and G2 (validate the shape before trusting it). CLAUDE.md §5 mandates a spec for boundary parse* adapters, and none existed. Baseline evidence: 02-testability.md §3a cites ssp/auth and bhp/auth at 42.9% line / 46.2% branch — jointly the worst line coverage in the frontend table — with this file's own lcov at LH 2/LF 20 (10.0% line), BRH 3/BRF 13 (23.1% branch).

BIO-017 read the same code and confirmed G1 holds on every path by inspection (restore() returns { bsn: '', naam }, the persistence effect() writes only naam, login()/ logout() never touch storage with a BSN) — but "correct, unverified by a test" is exactly the gap TE-001 already targeted, so BIO-017 folds into it and adds one required assertion: a stored {"bsn":"…","naam":"…"} must yield a session whose bsn is ''.

Separately, apps/ssp/src/app/shell/debug-state/mask.ts — confirmed at the path the finding cites — has redactProfile, a pure, exported, directly callable PII-redaction function with no spec. It redacts name, birthdate and address and masks the BIG-nummer; BIO-017 verified it correct by reading, same "no regression net" gap.

What changed

File Change
apps/ssp/src/app/auth/domain/session.ts added export function parseStoredSession(raw: string | null): Session | null — the exact parse+validate body restore() used to hold
apps/ssp/src/app/auth/application/session.store.ts restore() collapses to parseStoredSession(localStorage.getItem(STORAGE_KEY))
apps/ssp/src/app/auth/domain/session.spec.ts 4 new cases: absent, non-JSON, wrong shape, and the G1 assertion
apps/behandelportal/src/app/auth/domain/session.ts identical extraction, second app
apps/behandelportal/src/app/auth/application/session.store.ts identical collapse, second app
apps/behandelportal/src/app/auth/domain/session.spec.ts identical 4 cases, second app
apps/ssp/src/app/shell/debug-state/mask.spec.ts new file — spec for redactProfile: masks the BIG-nummer, redacts name/geboortedatum/adres on both registration and person, leaves beroep/registratiedatum/status untouched

The extracted function's body is a byte-for-byte move — same try/catch, same JSON.parse cast, same typeof parsed?.naam === 'string' guard, same { bsn: '', naam } construction. Only its location and the doc comment (rewritten to explain the why of G1/G2 for a function now read on its own, rather than inline next to the effect() it used to sit beside) changed.

The seam lands twice, on purpose

auth is deliberately unshared per ADR-0002 / CLAUDE.md §1: Zorgverlener and Medewerker are different Principal variants with different login flows, and the two session.ts files are expected to diverge. TE-001 says this outright, and BL-002 flags any extract-to-libs/shared here as contradicting an accepted ADR. parseStoredSession was therefore written twice, once per app's own domain/session.ts — not factored into a shared helper, and not resisted only in this note; the two functions are word-for-word identical today and that is expected to change the moment RB-13 (Session → Principal) lands.

Judgement calls

  • redactProfile's spec asserts on the concrete shape, not just "not equal to the input" — it pins bigNummer to '********901', checks REDACTED on each PII field individually, and separately asserts the non-PII fields (beroep, registratiedatum, status) survive unchanged. A looser "no PII substring appears" assertion would have been weaker at catching the regression this ticket exists to prevent (e.g. a future field added to redactProfile's output that is left unmasked by accident).
  • The G1 spec case uses toEqual, not toBe, since the parser constructs a new object; this matches the existing isAuthenticated spec's style in the same file.
  • No production code beyond the restore() one-liner in each session.store.ts changed — login(), logout(), and the persistence effect() were already correct and are unaffected.

Verification

Confirmed both new specs are red without the fix:

  • Temporarily changed parseStoredSession to keep a stored bsn (bsn: parsed.bsn ?? '' instead of bsn: '') — the new G1 test failed with expected { bsn: '19012345601', naam: 'Test' } to deeply equal { bsn: '', naam: 'Test' }, all 243 other tests stayed green. Reverted; git diff on the file is empty afterward.
  • Temporarily changed redactProfile to pass naam through unmasked — the new "redacts the name" test failed with expected 'J. Jansen' to be '‹redacted›'. Reverted; git diff on the file is empty afterward.

npm run ci: green (see PR/commit for the run this doc ships with).