feat(showcase): PII section — mask + elfproef parse (WP-42, mask/parse half)
Add a "PII — maskeren & parsen" section to /concepts demonstrating the WP-40 pieces with FP + atomic design, framed for AVG art. 9 / data-minimisation: a live <app-masked-value> atom (masked BSN that reveals on click; real reveal is step-up + audited in behandel-scherm) and a live parseBsn elfproef parse. Both show the real linked source via the WP-39 snippet mechanism (new showcase regions in bsn.ts + pii.ts, registered in gen-snippets.mjs). Delivers WP-42's showcase demo; the persisted-audit half stays pending WP-41. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,54 +43,54 @@ WP-19's own file), so it's a separate manual/CI step, not chained into the other
|
||||
Gates land before the work they cover; each lint rule lands in the same WP as the fixes
|
||||
for its existing violations, so every WP ends green.
|
||||
|
||||
| WP | Title | Phase | Status |
|
||||
| ---------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------- | ------ |
|
||||
| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done |
|
||||
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
|
||||
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
|
||||
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
|
||||
| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done |
|
||||
| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done |
|
||||
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done |
|
||||
| [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | done |
|
||||
| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | done |
|
||||
| [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | done |
|
||||
| [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | done |
|
||||
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
|
||||
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
|
||||
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
|
||||
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
|
||||
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done |
|
||||
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done |
|
||||
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
||||
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done |
|
||||
| [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | done |
|
||||
| [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | done |
|
||||
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | done |
|
||||
| [WP-23](WP-23-org-template-backend.md) | Org-template backend + admin role | 6 · Brief v2 | done |
|
||||
| [WP-24](WP-24-letter-canvas.md) | Letter canvas (edit on the letter) | 6 · Brief v2 | done |
|
||||
| [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | done |
|
||||
| [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done |
|
||||
| [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | done |
|
||||
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | todo |
|
||||
| [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done |
|
||||
| [WP-30](WP-30-ci-perf-followups.md) | CI performance follow-ups (node_modules cache, runner image, path filters) | follow-on · CI/infra | todo |
|
||||
| [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done |
|
||||
| [WP-32](WP-32-stamdata-undo.md) | Undo/redo in the stamdata editor | 7 · refinements | done |
|
||||
| [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done |
|
||||
| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done |
|
||||
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done |
|
||||
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | done |
|
||||
| [WP-37](WP-37-dev-switcher-reset.md) | Dev-switcher reset fix (scenario/role URL param) | 8 · platform/DX/showcase | done |
|
||||
| [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | done |
|
||||
| [WP-39](WP-39-showcase-snippets-animations.md) | Showcase: linked code snippets + teaching animations | 8 · platform/DX/showcase | done |
|
||||
| [WP-40](WP-40-pii-kernel.md) | PII kernel: branded `Bsn` VO (elfproef) + masked-value atom | 8 · platform/DX/showcase | done |
|
||||
| [WP-41](WP-41-persisted-authz-audit.md) | Persisted, queryable authz/PII-reveal audit (no PII) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-42](WP-42-privacy-security-showcase.md) | Privacy & security showcase page (mask + no-PII log) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-43](WP-43-scaffold-generators.md) | Runnable generators: value-object / form-machine / bff-endpoint / ui-component | 8 · platform/DX/showcase | todo |
|
||||
| [WP-44](WP-44-context-generator.md) | Runnable generator: `gen:context` | 8 · platform/DX/showcase | todo |
|
||||
| [WP-45](WP-45-create-ssp-generator.md) | `create-ssp` bootstrap generator (mechanise new-ssp) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-46](WP-46-vitest-coverage.md) | Vitest coverage (report + report-only thresholds) | 8 · platform/DX/showcase | done |
|
||||
| WP | Title | Phase | Status |
|
||||
| ---------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------- | ------- |
|
||||
| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done |
|
||||
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
|
||||
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
|
||||
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
|
||||
| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done |
|
||||
| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done |
|
||||
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done |
|
||||
| [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | done |
|
||||
| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | done |
|
||||
| [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | done |
|
||||
| [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | done |
|
||||
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
|
||||
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
|
||||
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
|
||||
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
|
||||
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done |
|
||||
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done |
|
||||
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
||||
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done |
|
||||
| [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | done |
|
||||
| [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | done |
|
||||
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | done |
|
||||
| [WP-23](WP-23-org-template-backend.md) | Org-template backend + admin role | 6 · Brief v2 | done |
|
||||
| [WP-24](WP-24-letter-canvas.md) | Letter canvas (edit on the letter) | 6 · Brief v2 | done |
|
||||
| [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | done |
|
||||
| [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done |
|
||||
| [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | done |
|
||||
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | todo |
|
||||
| [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done |
|
||||
| [WP-30](WP-30-ci-perf-followups.md) | CI performance follow-ups (node_modules cache, runner image, path filters) | follow-on · CI/infra | todo |
|
||||
| [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done |
|
||||
| [WP-32](WP-32-stamdata-undo.md) | Undo/redo in the stamdata editor | 7 · refinements | done |
|
||||
| [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done |
|
||||
| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done |
|
||||
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done |
|
||||
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | done |
|
||||
| [WP-37](WP-37-dev-switcher-reset.md) | Dev-switcher reset fix (scenario/role URL param) | 8 · platform/DX/showcase | done |
|
||||
| [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | done |
|
||||
| [WP-39](WP-39-showcase-snippets-animations.md) | Showcase: linked code snippets + teaching animations | 8 · platform/DX/showcase | done |
|
||||
| [WP-40](WP-40-pii-kernel.md) | PII kernel: branded `Bsn` VO (elfproef) + masked-value atom | 8 · platform/DX/showcase | done |
|
||||
| [WP-41](WP-41-persisted-authz-audit.md) | Persisted, queryable authz/PII-reveal audit (no PII) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-42](WP-42-privacy-security-showcase.md) | Privacy & security showcase page (mask + no-PII log) | 8 · platform/DX/showcase | partial |
|
||||
| [WP-43](WP-43-scaffold-generators.md) | Runnable generators: value-object / form-machine / bff-endpoint / ui-component | 8 · platform/DX/showcase | todo |
|
||||
| [WP-44](WP-44-context-generator.md) | Runnable generator: `gen:context` | 8 · platform/DX/showcase | todo |
|
||||
| [WP-45](WP-45-create-ssp-generator.md) | `create-ssp` bootstrap generator (mechanise new-ssp) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-46](WP-46-vitest-coverage.md) | Vitest coverage (report + report-only thresholds) | 8 · platform/DX/showcase | done |
|
||||
|
||||
Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn);
|
||||
03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
# WP-42 — Privacy & security showcase page
|
||||
|
||||
Status: todo
|
||||
Status: partial — mask/parse showcase done; audit half pending WP-41
|
||||
Phase: 8 — platform/DX/showcase
|
||||
Priority: P2
|
||||
Depends on: WP-40, WP-41
|
||||
|
||||
## Outcome (mask/parse half — done, on user request ahead of WP-41)
|
||||
|
||||
Added a "6 · PII — maskeren & parsen" section to `/concepts` demonstrating the WP-40 pieces with
|
||||
FP + atomic design, framed for AVG art. 9 / data-minimisation: a live `<app-masked-value>` atom
|
||||
(masked-by-default BSN that reveals on click; note points to the real step-up + audited reveal in
|
||||
behandel-scherm) and a live `parseBsn` elfproef parse mirroring the postcode demo. Both show the
|
||||
real linked source via the WP-39 snippet mechanism (`// #region showcase:parseBsn` in bsn.ts,
|
||||
`showcase:mask` in pii.ts, registered in `gen-snippets.mjs` → `snippets.generated.ts`, drift-gated).
|
||||
No i18n (showcase is Dutch-only teaching text). No behaviour change outside the showcase.
|
||||
|
||||
**Still pending (needs WP-41):** the "log PII / no-PII audit trail" half — visualizing the
|
||||
persisted authz/reveal audit — plus an optional Foundations MDX writeup.
|
||||
|
||||
## Why
|
||||
|
||||
Once the reusable privacy pieces exist (WP-40 masked-value atom + pure maskers, WP-41 persisted
|
||||
|
||||
@@ -14,6 +14,8 @@ const SOURCES = [
|
||||
'src/app/registratie/domain/value-objects/postcode.ts',
|
||||
'src/app/registratie/domain/change-request.machine.ts',
|
||||
'src/app/herregistratie/domain/intake.machine.ts',
|
||||
'src/app/shared/kernel/bsn.ts',
|
||||
'src/app/shared/kernel/pii.ts',
|
||||
];
|
||||
|
||||
const START = /\/\/\s*#region showcase:([\w-]+)/;
|
||||
|
||||
@@ -11,6 +11,7 @@ export type Bsn = Brand<string, 'Bsn'>;
|
||||
// Positional weights for the elfproef: 9·d1 + 8·d2 + … + 2·d8 − 1·d9 ≡ 0 (mod 11).
|
||||
const WEIGHTS = [9, 8, 7, 6, 5, 4, 3, 2, -1];
|
||||
|
||||
// #region showcase:parseBsn
|
||||
export function parseBsn(raw: string): Result<string, Bsn> {
|
||||
const t = raw.trim();
|
||||
if (!/^\d{9}$/.test(t)) {
|
||||
@@ -22,5 +23,6 @@ export function parseBsn(raw: string): Result<string, Bsn> {
|
||||
$localize`:@@validation.bsnElfproef:Dit is geen geldig BSN (klopt niet met de elfproef).`,
|
||||
);
|
||||
}
|
||||
return ok(t as Bsn);
|
||||
return ok(t as Bsn); // holding a Bsn is proof it passed the elfproef
|
||||
}
|
||||
// #endregion showcase:parseBsn
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
export const REDACTED = '‹redacted›';
|
||||
|
||||
// #region showcase:mask
|
||||
/** Keep the last `keep` characters, mask the rest with `*`. */
|
||||
export function maskTail(value: string, keep: number): string {
|
||||
if (value.length <= keep) return '*'.repeat(value.length);
|
||||
@@ -15,3 +16,4 @@ export function maskTail(value: string, keep: number): string {
|
||||
export function maskBsn(value: string): string {
|
||||
return maskTail(value, 3);
|
||||
}
|
||||
// #endregion showcase:mask
|
||||
|
||||
@@ -11,6 +11,9 @@ import { HerregistratieWizardComponent } from '@herregistratie/ui/herregistratie
|
||||
import { IntakeWizardComponent } from '@herregistratie/ui/intake-wizard/intake-wizard.component';
|
||||
import { Registration } from '@registratie/domain/registration';
|
||||
import { parsePostcode } from '@registratie/domain/value-objects/postcode';
|
||||
import { parseBsn } from '@shared/kernel/bsn';
|
||||
import { maskBsn } from '@shared/kernel/pii';
|
||||
import { MaskedValueComponent } from '@shared/ui/masked-value/masked-value.component';
|
||||
import { SNIPPETS } from './snippets.generated';
|
||||
import { highlightTs } from './highlight-ts';
|
||||
|
||||
@@ -39,6 +42,7 @@ function fakeResource<T>(status: string, value?: T, error?: Error): Resource<T>
|
||||
RegistrationSummaryComponent,
|
||||
HerregistratieWizardComponent,
|
||||
IntakeWizardComponent,
|
||||
MaskedValueComponent,
|
||||
],
|
||||
styles: [
|
||||
`
|
||||
@@ -371,6 +375,65 @@ function fakeResource<T>(status: string, value?: T, error?: Error): Resource<T>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. PII: mask + parse -->
|
||||
<section class="section">
|
||||
<app-heading [level]="2">6 · PII — maskeren & parsen</app-heading>
|
||||
<p class="lead">
|
||||
Een BSN is bijzondere persoonsgegevens (AVG art. 9). Dataminimalisatie: standaard
|
||||
gemaskeerd tonen, alleen tonen na een vastgelegde handeling; en "parse, don't validate" op
|
||||
het gevoeligste veld — een pure functie die de <em>elfproef</em> afdwingt.
|
||||
</p>
|
||||
<div class="cols">
|
||||
<div class="card card--good">
|
||||
<p class="tag good">Maskeren — atom</p>
|
||||
<p>
|
||||
BSN:
|
||||
<app-masked-value
|
||||
[value]="bsnShown()"
|
||||
[canReveal]="true"
|
||||
revealLabel="Toon BSN"
|
||||
(reveal)="bsnRevealed.set(true)"
|
||||
/>
|
||||
</p>
|
||||
<figure class="linked">
|
||||
<pre [innerHTML]="code['mask']"></pre>
|
||||
<figcaption class="src">↳ {{ src['mask'] }}</figcaption>
|
||||
</figure>
|
||||
<p class="note">
|
||||
Standaard gemaskeerd; het echte tonen is step-up-geverifieerd én vastgelegd (zie het
|
||||
behandelscherm). De atom bevat de maskeer-detectie — geen los <code>*</code>-gesnuffel
|
||||
bij elke gebruiker.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<p class="tag good">Parse (elfproef) → Result</p>
|
||||
<app-text-input
|
||||
inputId="bsn"
|
||||
[ngModel]="bsnRaw()"
|
||||
(ngModelChange)="bsnRaw.set($event)"
|
||||
name="bsn"
|
||||
placeholder="Typ een BSN, bijv. 123456782"
|
||||
/>
|
||||
@let b = bsnParsed();
|
||||
@if (bsnRaw()) {
|
||||
<div animate.enter="app-item-enter">
|
||||
@if (b.ok) {
|
||||
<p class="tag good">ok</p>
|
||||
<pre>Bsn ="{{ b.value }}"</pre>
|
||||
} @else {
|
||||
<p class="tag bad">err</p>
|
||||
<pre>{{ b.error }}</pre>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<figure class="linked">
|
||||
<pre [innerHTML]="code['parseBsn']"></pre>
|
||||
<figcaption class="src">↳ {{ src['parseBsn'] }}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
@@ -394,6 +457,14 @@ export class ConceptsPage {
|
||||
raw = signal('');
|
||||
parsed = computed(() => parsePostcode(this.raw()));
|
||||
|
||||
// 6 · PII demo. Masked-by-default value that reveals locally (the real reveal is
|
||||
// step-up-gated + audited elsewhere); plus a live elfproef parse mirroring the postcode demo.
|
||||
demoBsn = '123456782';
|
||||
bsnRevealed = signal(false);
|
||||
bsnShown = computed(() => (this.bsnRevealed() ? this.demoBsn : maskBsn(this.demoBsn)));
|
||||
bsnRaw = signal('');
|
||||
bsnParsed = computed(() => parseBsn(this.bsnRaw()));
|
||||
|
||||
// Deliberately-wrong illustrations (no real source to link — they show the anti-pattern).
|
||||
private readonly illustrations: Record<string, string> = {
|
||||
unionBad: `interface Registration {
|
||||
@@ -419,5 +490,7 @@ errors = signal<...>({});
|
||||
parse: 'registratie/domain/value-objects/postcode.ts',
|
||||
machine: 'registratie/domain/change-request.machine.ts',
|
||||
steps: 'herregistratie/domain/intake.machine.ts',
|
||||
parseBsn: 'shared/kernel/bsn.ts',
|
||||
mask: 'shared/kernel/pii.ts',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
export const SNIPPETS: Record<string, string> = {
|
||||
"fold": "/** Exhaustive fold: you must handle every case, checked at compile time. */\nexport function foldRemote<E, T, R>(\n rd: RemoteData<E, T>,\n h: { loading: () => R; empty: () => R; failure: (e: E) => R; success: (v: T) => R },\n): R {\n switch (rd.tag) {\n case 'Loading':\n return h.loading();\n case 'Empty':\n return h.empty();\n case 'Failure':\n return h.failure(rd.error);\n case 'Success':\n return h.success(rd.value);\n default:\n return assertNever(rd); // add a variant → compile error until handled\n }\n}",
|
||||
"machine": "export type ChangeRequestState =\n | { tag: 'Editing'; draft: Draft; errors: Errors } // draft/errors exist ONLY while editing\n | { tag: 'Submitting'; data: Valid } // carries the parsed value, no errors\n | { tag: 'Submitted'; data: Valid; referentie: string }\n | { tag: 'Failed'; data: Valid; error: string };",
|
||||
"mask": "/** Keep the last `keep` characters, mask the rest with `*`. */\nexport function maskTail(value: string, keep: number): string {\n if (value.length <= keep) return '*'.repeat(value.length);\n return '*'.repeat(value.length - keep) + value.slice(-keep);\n}\n\n/** Mask a BSN / BIG-nummer for display: keep the last 3 digits, mask the rest. */\nexport function maskBsn(value: string): string {\n return maskTail(value, 3);\n}",
|
||||
"parse": "export function parsePostcode(raw: string): Result<string, Postcode> {\n const t = raw.trim().toUpperCase();\n if (!/^[1-9]\\d{3}\\s?[A-Z]{2}$/.test(t)) {\n return err($localize`:@@validation.postcode:Voer een geldige postcode in, bijv. 1234 AB.`);\n }\n // Normalise to \"1234 AB\" — the parser also cleans up.\n return ok(t.replace(/^(\\d{4})\\s?([A-Z]{2})$/, '$1 $2') as Postcode);\n}",
|
||||
"parseBsn": "export function parseBsn(raw: string): Result<string, Bsn> {\n const t = raw.trim();\n if (!/^\\d{9}$/.test(t)) {\n return err($localize`:@@validation.bsn:Voer een geldig BSN van 9 cijfers in.`);\n }\n const sum = [...t].reduce((acc, ch, i) => acc + Number(ch) * WEIGHTS[i], 0);\n if (t === '000000000' || sum % 11 !== 0) {\n return err(\n $localize`:@@validation.bsnElfproef:Dit is geen geldig BSN (klopt niet met de elfproef).`,\n );\n }\n return ok(t as Bsn); // holding a Bsn is proof it passed the elfproef\n}",
|
||||
"steps": "/** The fixed step list. Number of steps never changes; questions reveal inline. */\nexport const STEPS: StepId[] = ['buitenland', 'werk', 'review'];",
|
||||
"union": "export type RegistrationStatus =\n | { tag: 'Geregistreerd'; herregistratieDatum: string } // only this variant carries the date\n | { tag: 'Geschorst'; geschorstTot: string; reden: string }\n | { tag: 'Doorgehaald'; doorgehaaldOp: string; reden: string };",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user