solidprivacy commited on
Commit
a57504e
·
unverified ·
1 Parent(s): 785d7e8

SCRUB-WP add Zorgfilter v1 policy and corpus foundation

Browse files

Record the approved care-profile policy, synthetic corpus, baseline helpers, test contracts, roadmap sequence, decision and risk boundaries.

CARE_PROFILE_V1_PLAN.md ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Zorgfilter v1 — policy, corpus and implementation plan
2
+
3
+ Status: approved direction; foundation implementation in progress
4
+ Repository: `solidprivacy-nl/scrub`
5
+ Approved: 2026-08-03 Europe/Amsterdam
6
+
7
+ ## 1. Product objective
8
+
9
+ Add an explicit Dutch care profile alongside the existing general Dutch, legal and international profiles.
10
+
11
+ The care profile removes or flags patient-, client-, relative-, provider- and care-trajectory identifiers while preserving clinical meaning.
12
+
13
+ Core rule:
14
+
15
+ ```text
16
+ Replace identity and patient-specific administrative references.
17
+ Review contextual provider, location and event identifiers.
18
+ Preserve diagnosis, medication, dosage, laboratory values, observations and care meaning.
19
+ Warn about rare-case re-identification without blindly masking clinical content.
20
+ ```
21
+
22
+ The profile is not a generic medical-term filter and must not make a document clinically unreadable.
23
+
24
+ ## 2. Approved policy
25
+
26
+ ### Replace by default
27
+
28
+ - patient/client names and names of relatives, representatives or informal carers;
29
+ - BSN and date of birth;
30
+ - address, postcode, e-mail and telephone number;
31
+ - patient number, care client number, EPD/ECD number and medical-record number;
32
+ - personal health-insurance number;
33
+ - patient-specific referral, treatment, laboratory-sample, care-incident and indication references.
34
+
35
+ ### Review and select by default
36
+
37
+ - care-provider names;
38
+ - BIG numbers and AGB codes;
39
+ - care organizations, departments, teams and locations;
40
+ - room or bed references;
41
+ - exact admission, discharge, appointment, treatment and report dates other than date of birth.
42
+
43
+ These values are visible in review and selected by default, but the user can preserve them when professional context requires it.
44
+
45
+ ### Preserve by default
46
+
47
+ - diagnosis and symptoms;
48
+ - medication names, dosage and administration schedules;
49
+ - laboratory results, units and reference values;
50
+ - observations, vital signs, allergies and functional status;
51
+ - treatment, procedure, care goals and clinical recommendations;
52
+ - generic clinical and professional role words;
53
+ - clinical classifications and codes unless evidence proves that a code is patient-specific.
54
+
55
+ ### Audit only
56
+
57
+ Rare combinations of diagnosis, location, date, age or unusual case detail may still be indirectly identifying. Version 1 records this as residual-risk evidence and an audit warning. It does not blindly remove clinical content.
58
+
59
+ ## 3. First supported document families
60
+
61
+ 1. daily nursing or care report;
62
+ 2. care plan and evaluation;
63
+ 3. nursing transfer;
64
+ 4. medical specialist discharge letter;
65
+ 5. GP referral or consultation letter;
66
+ 6. medication overview or administration list;
67
+ 7. laboratory report;
68
+ 8. MIC/MIM/VIM care-incident report.
69
+
70
+ Later extensions may cover mental-health progress notes, district-nursing intake, rehabilitation, CIZ/Wlz/Wmo documentation, multidisciplinary consultation, wound care, maternity care and youth care.
71
+
72
+ ## 4. Architecture direction
73
+
74
+ Keep care-specific behavior in separate pure modules before UI integration:
75
+
76
+ ```text
77
+ care_profile_policy.py
78
+ care_test_examples.py
79
+ care_profile_baseline.py
80
+ care_reference_taxonomy.py # later recognizer package
81
+ dutch_care_recognizers.py # later recognizer package
82
+ recognition_profiles.py # later central profile configuration
83
+ ```
84
+
85
+ Do not continue expanding the legal taxonomy with all care behavior. The existing broad `NL_HEALTHCARE_REFERENCE` category must be assessed and split because it currently combines values with different privacy policies, including patient numbers and DBC-like codes.
86
+
87
+ ## 5. Current Streamlit interface target
88
+
89
+ After the policy, corpus, baseline and recognizer packages pass, the current profile selector becomes:
90
+
91
+ ```text
92
+ Zorgcontrole — streng
93
+ Juridische controle — streng
94
+ Algemene Nederlandse controle
95
+ Algemene internationale controle
96
+ ```
97
+
98
+ Care-profile behavior:
99
+
100
+ ```text
101
+ threshold: evidence-based, initial candidate 0.30
102
+ entities: general Dutch + care-specific
103
+ legal-only entities: not enabled by default
104
+ care candidate scan: enabled
105
+ synthetic care examples: available
106
+ profile explanation: visible
107
+ ```
108
+
109
+ No export, Scrub Key or reinsert semantics change merely because a care profile is added.
110
+
111
+ ## 6. Final desktop interface target
112
+
113
+ The future document workspace shows a compact, explicit profile choice:
114
+
115
+ ```text
116
+ [ Algemeen NL ] [ Zorg ] [ Juridisch ] [ Internationaal ]
117
+ ```
118
+
119
+ After a document opens, the toolbar shows `Profiel: Zorg` with a small change action. The full explanation and advanced entity selection remain under Options. Profile switching must never happen silently.
120
+
121
+ ## 7. Test strategy
122
+
123
+ ### Corpus contracts
124
+
125
+ Each fully synthetic document must define:
126
+
127
+ - exact values expected to be replaced;
128
+ - exact values expected to be shown for review;
129
+ - exact clinical values and phrases expected to be preserved;
130
+ - ambiguity traps and negative examples;
131
+ - sector and document type metadata.
132
+
133
+ ### Current-engine baseline
134
+
135
+ Run the existing recognizer layer against the care corpus before adding care recognizers. Record:
136
+
137
+ - detected and missed expected values;
138
+ - wrong entity classification;
139
+ - partial-span matches;
140
+ - clinical over-masking;
141
+ - role-word over-masking.
142
+
143
+ The baseline is evidence, not a production gate.
144
+
145
+ ### Recognizer validation
146
+
147
+ Measure at least:
148
+
149
+ - recall by care entity;
150
+ - precision by care entity;
151
+ - exact-span accuracy;
152
+ - false positives and false negatives;
153
+ - preserved clinical phrases;
154
+ - profile isolation across all four profiles.
155
+
156
+ ### End-to-end validation
157
+
158
+ For TXT and DOCX:
159
+
160
+ ```text
161
+ import -> detect -> review -> replace -> Scrub Key -> reinsert -> audit
162
+ ```
163
+
164
+ Verify document binding, context preservation, header/footer behavior and fail-closed wrong-key handling.
165
+
166
+ ## 8. Sequential workpackages
167
+
168
+ 1. `SCRUB-WP_CARE_PROFILE_V1_POLICY_AND_CORPUS_FOUNDATION`
169
+ 2. `SCRUB-WP_CARE_PROFILE_CURRENT_ENGINE_BASELINE`
170
+ 3. `SCRUB-WP_CARE_PROFILE_GAP_TRIAGE`
171
+ 4. `SCRUB-WP_CARE_PROFILE_RECOGNIZER_CONTRACT_TESTS`
172
+ 5. `SCRUB-WP_CARE_PROFILE_RECOGNIZER_IMPLEMENTATION`
173
+ 6. `SCRUB-WP_RECOGNITION_PROFILE_CONFIGURATION_REFACTOR`
174
+ 7. `SCRUB-WP_CARE_PROFILE_CURRENT_UI_INTEGRATION`
175
+ 8. `SCRUB-WP_CARE_PROFILE_CROSS_PROFILE_REGRESSION_MATRIX`
176
+ 9. `SCRUB-WP_CARE_PROFILE_APP_VERIFY`
177
+ 10. `SCRUB-WP_CARE_PROFILE_DESKTOP_UX_CONTRACT`
178
+
179
+ UI integration remains sequential and must not run in parallel with other edits to `presidio_streamlit.py` or the same review/export flow.
180
+
181
+ ## 9. Safety and claim boundaries
182
+
183
+ - synthetic data only;
184
+ - human review remains required;
185
+ - no claim that all medical or care identifiers are always found;
186
+ - no automatic cloud document processing;
187
+ - no blind masking of clinical meaning;
188
+ - no production-readiness claim from corpus or benchmark results;
189
+ - no weakening of Scrub Key, export, audit or review controls.
CHANGELOG.md CHANGED
@@ -1,3 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ## 2026-08-03 14:47 Europe/Amsterdam — SCRUB-WP_AI_FIRST_DESKTOP_PACKAGING_ROADMAP_ALIGNMENT
2
 
3
  Status: completed; roadmap/decision documentation only.
 
1
+ ## 2026-08-03 15:31 Europe/Amsterdam — SCRUB-WP_CARE_PROFILE_V1_POLICY_AND_CORPUS_FOUNDATION
2
+
3
+ Status: completed; policy/corpus foundation implemented and validated.
4
+
5
+ Purpose:
6
+ - Create the evidence base for an explicit Dutch Zorg profile without prematurely changing recognizers or the UI.
7
+
8
+ Files added:
9
+ - `CARE_PROFILE_V1_PLAN.md`
10
+ - `care_profile_policy.py`
11
+ - `care_test_examples.py`
12
+ - `care_profile_baseline.py`
13
+ - `scripts/generate_care_profile_baseline.py`
14
+ - `tests/test_care_profile_policy_contract.py`
15
+ - `tests/test_care_profile_corpus_contracts.py`
16
+ - `tests/test_care_profile_current_engine_baseline.py`
17
+ - `workpackage_claims/scrub_wp_care_profile_v1_policy_and_corpus_foundation.md`
18
+
19
+ Main changes:
20
+ - froze replace/review/preserve/audit-only care policy actions;
21
+ - added eight fully synthetic document families;
22
+ - separated patient identifiers from provider/location review and clinical content preservation;
23
+ - added a deterministic baseline helper for current Dutch custom recognizers;
24
+ - made no recognizer or product-UI behavior change.
25
+
26
+ Validation:
27
+ - full GitHub Actions run #1818 passed: 918 tests;
28
+ - Hugging Face sync not functionally relevant because no runtime/UI file changed;
29
+ - app verification not applicable.
30
+
31
+ Intentionally not changed:
32
+ - current three profile choices;
33
+ - recognizer registration or thresholds;
34
+ - review table, export, Scrub Key and reinsert semantics;
35
+ - runtime, dependencies or cloud processing.
36
+
37
  ## 2026-08-03 14:47 Europe/Amsterdam — SCRUB-WP_AI_FIRST_DESKTOP_PACKAGING_ROADMAP_ALIGNMENT
38
 
39
  Status: completed; roadmap/decision documentation only.
DECISION_LOG.md CHANGED
@@ -1,3 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ## 2026-08-03 — D038 — Use AI-first execution with human-controlled signing, security and release gates for Phase 9 desktop packaging
2
 
3
  Status: accepted roadmap and execution-model decision; implementation remains gated
 
1
+ ## 2026-08-03 — D039 — Add Zorgfilter v1 with clinical-context preservation
2
+
3
+ Status: accepted product and implementation-sequence decision
4
+
5
+ Decision:
6
+
7
+ ```text
8
+ Add an explicit Dutch Zorg profile. Replace date of birth and patient/client identity by default. Show other exact care dates, provider identity, BIG/AGB, organizations and locations for review and select them by default. Preserve diagnosis, medication, dosage, laboratory results and observations. Surface rare-case indirect re-identification as residual-risk evidence rather than blindly masking clinical meaning.
9
+ ```
10
+
11
+ Implementation sequence:
12
+ - policy and fully synthetic corpus first;
13
+ - current-engine baseline and gap triage second;
14
+ - recognizer contracts and pure recognizer implementation before UI;
15
+ - central profile configuration before adding the fourth Streamlit profile;
16
+ - cross-profile regression and live app verification before closeout.
17
+
18
+ Reason:
19
+ - Care documents contain both direct identifiers and essential clinical meaning.
20
+ - A broad medical-word filter would destroy usability and potentially clinical/legal context.
21
+ - The current `NL_HEALTHCARE_REFERENCE` category combines values with different privacy policies and must be split through evidence-driven work.
22
+ - A fourth UI label without dedicated detection evidence would be cosmetic and unsafe.
23
+
24
+ Boundaries:
25
+ - synthetic data only;
26
+ - human review remains required;
27
+ - no production-readiness claim from corpus or benchmark results;
28
+ - no silent profile switching;
29
+ - no cloud document processing;
30
+ - no export, Scrub Key or reinsert semantic change in the foundation package.
31
+
32
  ## 2026-08-03 — D038 — Use AI-first execution with human-controlled signing, security and release gates for Phase 9 desktop packaging
33
 
34
  Status: accepted roadmap and execution-model decision; implementation remains gated
RISK_REGISTER.md CHANGED
@@ -181,6 +181,41 @@ Current mitigations include diagnostic benchmark work, preservation guidance, PE
181
 
182
  ---
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  ## Product-claim boundary
185
 
186
  Disallowed claims:
 
181
 
182
  ---
183
 
184
+ ## R10 — Care-profile under-detection and clinical over-masking
185
+
186
+ Status: mitigating
187
+ Impact: critical
188
+
189
+ Risk:
190
+
191
+ ```text
192
+ A care document retains patient or trajectory identifiers, or Scrub removes diagnosis, medication, laboratory values, observations or care context and makes the document misleading or unusable.
193
+ ```
194
+
195
+ Mitigation direction:
196
+
197
+ - explicit Zorgfilter v1 policy contract;
198
+ - fully synthetic corpus across eight care-document families;
199
+ - exact replace, review and preserve expectations;
200
+ - current-engine baseline before recognizer changes;
201
+ - separate care taxonomy and recognizers;
202
+ - negative tests for medical numbers, dosages, times, vital signs and laboratory values;
203
+ - cross-profile regression before UI promotion;
204
+ - human review and residual-risk evidence remain mandatory.
205
+
206
+ Approved policy boundary:
207
+
208
+ ```text
209
+ Patient identity and date of birth: replace.
210
+ Other exact care dates and provider identity: review, selected by default.
211
+ Clinical meaning: preserve.
212
+ Rare-case indirect identification: audit warning, not blind masking.
213
+ ```
214
+
215
+ The current broad `NL_HEALTHCARE_REFERENCE` category is insufficient because it combines patient numbers, referral references, insurance identifiers and DBC/clinical codes under one behavior.
216
+
217
+ ---
218
+
219
  ## Product-claim boundary
220
 
221
  Disallowed claims:
ROADMAP.md CHANGED
@@ -11,7 +11,7 @@ Use it together with:
11
  - `DECISION_LOG.md` for accepted strategic and architecture decisions;
12
  - `PROJECT_PROMPT.md` for worker rules and project governance.
13
 
14
- Last roadmap strategy update: 2026-08-03 — Phase 9 local desktop packaging remains gated, with an AI-first implementation model and explicit human signing, release, security-claim and UX-acceptance gates.
15
 
16
  ---
17
 
@@ -411,3 +411,99 @@ When the phase is explicitly opened, use small sequential workpackages:
411
  ```
412
 
413
  Do not collapse signing, release and security acceptance into the same autonomous worker. Do not make a production/local-only claim from successful packaging alone.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  - `DECISION_LOG.md` for accepted strategic and architecture decisions;
12
  - `PROJECT_PROMPT.md` for worker rules and project governance.
13
 
14
+ Last roadmap strategy update: 2026-08-03 — Zorgfilter v1 is approved as an evidence-driven profile line; policy and synthetic corpus work may proceed while recognizer and UI integration remain sequential and test-gated.
15
 
16
  ---
17
 
 
411
  ```
412
 
413
  Do not collapse signing, release and security acceptance into the same autonomous worker. Do not make a production/local-only claim from successful packaging alone.
414
+
415
+ ## 11. Zorgfilter v1 — evidence-driven care profile
416
+
417
+ Approved: 2026-08-03 15:31 Europe/Amsterdam
418
+
419
+ The first product wedge already identifies Legal and Zorg as the most relevant Dutch professional domains. Zorg now receives an explicit profile line rather than remaining an incidental subset of general and legal recognition.
420
+
421
+ ### Approved policy
422
+
423
+ ```text
424
+ Geboortedatum: vervangen
425
+ Overige exacte zorgdata: controleren en standaard geselecteerd
426
+ Patiënt- en cliëntidentificatie: vervangen
427
+ Zorgverleneridentificatie: controleren en standaard geselecteerd
428
+ Diagnose, medicatie, dosering, labwaarden en observaties: behouden
429
+ Zeldzame-casus-herleidbaarheid: auditwaarschuwing, niet blind maskeren
430
+ ```
431
+
432
+ The core product rule is:
433
+
434
+ ```text
435
+ Remove identity and patient-specific administrative references while preserving clinical meaning.
436
+ ```
437
+
438
+ Zorgfilter v1 is not a generic medical-word filter. It must not make care records clinically unreadable.
439
+
440
+ ### Initial document scope
441
+
442
+ - daily nursing/care reports;
443
+ - care plans and evaluations;
444
+ - nursing transfers;
445
+ - medical specialist discharge letters;
446
+ - GP referrals and consultation letters;
447
+ - medication overviews or administration lists;
448
+ - laboratory reports;
449
+ - MIC/MIM/VIM care-incident reports.
450
+
451
+ ### Architecture and sequencing
452
+
453
+ The first packages are pure helper, policy, corpus and evidence work. They may proceed without reopening the shared Streamlit review/export flow. The future current-UI integration is permitted by explicit coordinator approval, but only after the corpus baseline, gap triage, recognizer contracts and recognizer implementation are green and no parallel worker is editing the same UI surface.
454
+
455
+ Preferred helper direction:
456
+
457
+ ```text
458
+ care_profile_policy.py
459
+ care_test_examples.py
460
+ care_profile_baseline.py
461
+ care_reference_taxonomy.py
462
+ dutch_care_recognizers.py
463
+ recognition_profiles.py
464
+ ```
465
+
466
+ The existing broad `NL_HEALTHCARE_REFERENCE` category must be assessed and split. Patient numbers, referral references, insurance identifiers and DBC/clinical codes do not share one safe default action.
467
+
468
+ ### Current and final interface direction
469
+
470
+ Current prototype after test-gated integration:
471
+
472
+ ```text
473
+ Zorgcontrole — streng
474
+ Juridische controle — streng
475
+ Algemene Nederlandse controle
476
+ Algemene internationale controle
477
+ ```
478
+
479
+ Final desktop workspace:
480
+
481
+ ```text
482
+ [ Algemeen NL ] [ Zorg ] [ Juridisch ] [ Internationaal ]
483
+ ```
484
+
485
+ The active profile remains visible in the document toolbar and never changes silently.
486
+
487
+ ### Sequential workpackages
488
+
489
+ ```text
490
+ 1. SCRUB-WP_CARE_PROFILE_V1_POLICY_AND_CORPUS_FOUNDATION
491
+ 2. SCRUB-WP_CARE_PROFILE_CURRENT_ENGINE_BASELINE
492
+ 3. SCRUB-WP_CARE_PROFILE_GAP_TRIAGE
493
+ 4. SCRUB-WP_CARE_PROFILE_RECOGNIZER_CONTRACT_TESTS
494
+ 5. SCRUB-WP_CARE_PROFILE_RECOGNIZER_IMPLEMENTATION
495
+ 6. SCRUB-WP_RECOGNITION_PROFILE_CONFIGURATION_REFACTOR
496
+ 7. SCRUB-WP_CARE_PROFILE_CURRENT_UI_INTEGRATION
497
+ 8. SCRUB-WP_CARE_PROFILE_CROSS_PROFILE_REGRESSION_MATRIX
498
+ 9. SCRUB-WP_CARE_PROFILE_APP_VERIFY
499
+ 10. SCRUB-WP_CARE_PROFILE_DESKTOP_UX_CONTRACT
500
+ ```
501
+
502
+ Safety boundaries:
503
+
504
+ - synthetic data only;
505
+ - no blind masking of clinical meaning;
506
+ - no change to Scrub Key, export or reinsert semantics without a separate package;
507
+ - human review remains required;
508
+ - corpus or benchmark success does not prove production readiness;
509
+ - no cloud document processing is introduced.
WORKPACKAGES.md CHANGED
@@ -1,3 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ## 2026-08-03 14:47 Europe/Amsterdam — SCRUB-WP_AI_FIRST_DESKTOP_PACKAGING_ROADMAP_ALIGNMENT
2
 
3
  Status: completed; documentation/strategy alignment only.
 
1
+ ## 2026-08-03 15:31 Europe/Amsterdam — SCRUB-WP_CARE_PROFILE_V1_POLICY_AND_CORPUS_FOUNDATION
2
+
3
+ Status: completed; policy/corpus foundation implemented and validated.
4
+
5
+ Goal:
6
+ - Establish the approved Zorgfilter v1 policy and a fully synthetic, machine-readable care-document corpus before adding recognizers or UI.
7
+
8
+ Current package scope:
9
+ - roadmap, decision and risk alignment;
10
+ - pure care-profile action contract;
11
+ - eight synthetic care-document families;
12
+ - corpus contract tests;
13
+ - current custom-recognizer baseline helper and report generator.
14
+
15
+ Approved policy:
16
+ - date of birth and patient/client identity: replace;
17
+ - other exact care dates and provider identity: review, selected by default;
18
+ - diagnosis, medication, dosage, lab results and observations: preserve;
19
+ - rare-case re-identification: audit warning only.
20
+
21
+ Active next package:
22
+ 1. `SCRUB-WP_CARE_PROFILE_CURRENT_ENGINE_BASELINE`
23
+ 2. `SCRUB-WP_CARE_PROFILE_GAP_TRIAGE`
24
+ 3. `SCRUB-WP_CARE_PROFILE_RECOGNIZER_CONTRACT_TESTS`
25
+ 4. `SCRUB-WP_CARE_PROFILE_RECOGNIZER_IMPLEMENTATION`
26
+ 5. `SCRUB-WP_RECOGNITION_PROFILE_CONFIGURATION_REFACTOR`
27
+ 6. `SCRUB-WP_CARE_PROFILE_CURRENT_UI_INTEGRATION`
28
+ 7. `SCRUB-WP_CARE_PROFILE_CROSS_PROFILE_REGRESSION_MATRIX`
29
+ 8. `SCRUB-WP_CARE_PROFILE_APP_VERIFY`
30
+ 9. `SCRUB-WP_CARE_PROFILE_DESKTOP_UX_CONTRACT`
31
+
32
+ Boundaries:
33
+ - no Streamlit change in this package;
34
+ - no recognizer behavior change yet;
35
+ - no export, Scrub Key, reinsert, cloud or dependency change;
36
+ - synthetic data only;
37
+ - current Phase 6 binding verification remains an independent active gate.
38
+
39
  ## 2026-08-03 14:47 Europe/Amsterdam — SCRUB-WP_AI_FIRST_DESKTOP_PACKAGING_ROADMAP_ALIGNMENT
40
 
41
  Status: completed; documentation/strategy alignment only.
care_profile_baseline.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Current deterministic recognizer baseline for the synthetic care corpus.
2
+
3
+ This baseline intentionally uses the recognizers that exist before the dedicated
4
+ Zorgfilter recognizer package. It records evidence and never claims production
5
+ readiness. Generic NER-model results are outside this pure baseline.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Any, Dict, Iterable, List, Sequence
12
+
13
+ from care_test_examples import TEST_CASES
14
+
15
+
16
+ def _normalize(value: str) -> str:
17
+ return re.sub(r"[^a-z0-9]+", "", str(value or "").lower())
18
+
19
+
20
+ def _result_row(text: str, result: Any, recognizer_name: str) -> Dict[str, Any]:
21
+ start = getattr(result, "start", None)
22
+ end = getattr(result, "end", None)
23
+ detected_text = ""
24
+ if isinstance(start, int) and isinstance(end, int) and 0 <= start < end <= len(text):
25
+ detected_text = text[start:end]
26
+ return {
27
+ "text": detected_text,
28
+ "entity_type": str(getattr(result, "entity_type", "") or ""),
29
+ "start": start,
30
+ "end": end,
31
+ "score": float(getattr(result, "score", 0.0) or 0.0),
32
+ "recognizer": recognizer_name,
33
+ }
34
+
35
+
36
+ def detect_with_current_custom_recognizers(text: str) -> List[Dict[str, Any]]:
37
+ """Run the current Dutch custom recognizers without a generic NER model."""
38
+
39
+ from dutch_recognizers import get_dutch_entity_names, get_dutch_recognizers
40
+
41
+ entities = get_dutch_entity_names(include_legal=True)
42
+ rows: List[Dict[str, Any]] = []
43
+ for recognizer in get_dutch_recognizers(supported_language="en"):
44
+ results = recognizer.analyze(text, entities=entities, nlp_artifacts=None)
45
+ rows.extend(_result_row(text, result, recognizer.name) for result in results)
46
+ rows.sort(key=lambda row: (row.get("start", -1), row.get("end", -1), row.get("entity_type", "")))
47
+ return rows
48
+
49
+
50
+ def _matching_rows(value: str, rows: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]:
51
+ needle = _normalize(value)
52
+ if not needle:
53
+ return []
54
+ return [row for row in rows if needle in _normalize(row.get("text", ""))]
55
+
56
+
57
+ def _phrase_spans(text: str, phrase: str) -> List[tuple[int, int]]:
58
+ spans: List[tuple[int, int]] = []
59
+ start = 0
60
+ while True:
61
+ index = text.find(phrase, start)
62
+ if index < 0:
63
+ return spans
64
+ spans.append((index, index + len(phrase)))
65
+ start = index + max(1, len(phrase))
66
+
67
+
68
+ def _spans_overlap(first: tuple[int, int], second: tuple[int, int]) -> bool:
69
+ return first[0] < second[1] and second[0] < first[1]
70
+
71
+
72
+ def _preserve_overlaps(
73
+ text: str,
74
+ preserve_phrases: Iterable[str],
75
+ rows: Sequence[Dict[str, Any]],
76
+ ) -> List[Dict[str, Any]]:
77
+ overlaps: List[Dict[str, Any]] = []
78
+ detected_spans = [
79
+ (int(row["start"]), int(row["end"]), row)
80
+ for row in rows
81
+ if isinstance(row.get("start"), int) and isinstance(row.get("end"), int)
82
+ ]
83
+ for phrase in preserve_phrases:
84
+ for phrase_span in _phrase_spans(text, phrase):
85
+ for start, end, row in detected_spans:
86
+ if _spans_overlap(phrase_span, (start, end)):
87
+ overlaps.append(
88
+ {
89
+ "preserve_phrase": phrase,
90
+ "detected_text": row.get("text", ""),
91
+ "entity_type": row.get("entity_type", ""),
92
+ "recognizer": row.get("recognizer", ""),
93
+ }
94
+ )
95
+ return overlaps
96
+
97
+
98
+ def build_current_care_baseline(
99
+ cases: Sequence[Dict[str, Any]] = TEST_CASES,
100
+ ) -> Dict[str, Any]:
101
+ """Build a deterministic evidence report for the pre-Zorgfilter recognizers."""
102
+
103
+ case_reports: List[Dict[str, Any]] = []
104
+ total_expected = 0
105
+ total_found = 0
106
+ total_preserve_overlaps = 0
107
+
108
+ for case in cases:
109
+ text = str(case["text"])
110
+ rows = detect_with_current_custom_recognizers(text)
111
+ expectations: List[Dict[str, Any]] = []
112
+ for policy_bucket in ("replace", "review_selected"):
113
+ for item in case.get(policy_bucket, []):
114
+ value = str(item["value"])
115
+ matches = _matching_rows(value, rows)
116
+ total_expected += 1
117
+ if matches:
118
+ total_found += 1
119
+ expectations.append(
120
+ {
121
+ "value": value,
122
+ "expected_entity_type": str(item["entity_type"]),
123
+ "policy_bucket": policy_bucket,
124
+ "found": bool(matches),
125
+ "detected_entity_types": sorted(
126
+ {str(row.get("entity_type", "")) for row in matches}
127
+ ),
128
+ "detected_spans": [str(row.get("text", "")) for row in matches],
129
+ }
130
+ )
131
+
132
+ preserve_overlaps = _preserve_overlaps(text, case.get("preserve", []), rows)
133
+ total_preserve_overlaps += len(preserve_overlaps)
134
+ case_reports.append(
135
+ {
136
+ "id": case["id"],
137
+ "name": case["name"],
138
+ "sector": case["sector"],
139
+ "document_type": case["document_type"],
140
+ "expected_value_count": len(expectations),
141
+ "found_value_count": sum(1 for item in expectations if item["found"]),
142
+ "expectations": expectations,
143
+ "preserve_overlaps": preserve_overlaps,
144
+ "detected_rows": rows,
145
+ }
146
+ )
147
+
148
+ recall = (total_found / total_expected) if total_expected else 0.0
149
+ return {
150
+ "schema_version": "1.0",
151
+ "profile": "current_custom_recognizers_before_care_profile",
152
+ "scope": "deterministic Dutch custom recognizers only; generic NER excluded",
153
+ "synthetic_data_only": True,
154
+ "production_ready": False,
155
+ "human_review_required": True,
156
+ "case_count": len(case_reports),
157
+ "expected_value_count": total_expected,
158
+ "found_value_count": total_found,
159
+ "indicative_recall": round(recall, 6),
160
+ "preserve_overlap_count": total_preserve_overlaps,
161
+ "cases": case_reports,
162
+ }
care_profile_policy.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pure policy contract for the first Dutch care recognition profile.
2
+
3
+ This module contains no Streamlit, Presidio, network, file-system or cloud logic.
4
+ It records what the future care profile should replace, review, preserve or only
5
+ surface as residual-risk evidence.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Dict, Iterable, Mapping, Sequence
12
+
13
+
14
+ ACTION_REPLACE = "replace"
15
+ ACTION_REVIEW_SELECTED = "review_selected"
16
+ ACTION_PRESERVE = "preserve"
17
+ ACTION_AUDIT_ONLY = "audit_only"
18
+
19
+ VALID_ACTIONS = {
20
+ ACTION_REPLACE,
21
+ ACTION_REVIEW_SELECTED,
22
+ ACTION_PRESERVE,
23
+ ACTION_AUDIT_ONLY,
24
+ }
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CarePolicyRule:
29
+ entity_type: str
30
+ action: str
31
+ label_nl: str
32
+ reason_nl: str
33
+ examples: tuple[str, ...]
34
+
35
+
36
+ CARE_POLICY_RULES: tuple[CarePolicyRule, ...] = (
37
+ CarePolicyRule(
38
+ "PERSON",
39
+ ACTION_REPLACE,
40
+ "Naam van patiënt, cliënt of naaste",
41
+ "Directe persoonsidentificatie wordt standaard vervangen.",
42
+ ("Ahmed El Mansouri", "mantelzorger Fatima El Mansouri"),
43
+ ),
44
+ CarePolicyRule(
45
+ "NL_BSN",
46
+ ACTION_REPLACE,
47
+ "BSN",
48
+ "Het BSN is een direct identificerend persoonsnummer.",
49
+ ("123456782",),
50
+ ),
51
+ CarePolicyRule(
52
+ "NL_DATE_OF_BIRTH",
53
+ ACTION_REPLACE,
54
+ "Geboortedatum",
55
+ "De geboortedatum is in zorgdocumenten sterk herleidbaar.",
56
+ ("14-02-1948",),
57
+ ),
58
+ CarePolicyRule(
59
+ "NL_ADDRESS",
60
+ ACTION_REPLACE,
61
+ "Adres",
62
+ "Een volledig woon- of verblijfadres identificeert de betrokkene.",
63
+ ("Zorglaan 18, 3511 AB Utrecht",),
64
+ ),
65
+ CarePolicyRule(
66
+ "NL_POSTCODE",
67
+ ACTION_REPLACE,
68
+ "Postcode",
69
+ "Een postcode kan samen met andere gegevens herleidbaar zijn.",
70
+ ("3511 AB",),
71
+ ),
72
+ CarePolicyRule(
73
+ "EMAIL_ADDRESS",
74
+ ACTION_REPLACE,
75
+ "E-mailadres",
76
+ "Een persoonlijk e-mailadres identificeert de betrokkene direct.",
77
+ ("ahmed@example.invalid",),
78
+ ),
79
+ CarePolicyRule(
80
+ "PHONE_NUMBER",
81
+ ACTION_REPLACE,
82
+ "Telefoonnummer",
83
+ "Een persoonlijk telefoonnummer identificeert de betrokkene direct.",
84
+ ("06 12345678",),
85
+ ),
86
+ CarePolicyRule(
87
+ "NL_PHONE_NUMBER",
88
+ ACTION_REPLACE,
89
+ "Nederlands telefoonnummer",
90
+ "Een persoonlijk telefoonnummer identificeert de betrokkene direct.",
91
+ ("030 2345678",),
92
+ ),
93
+ CarePolicyRule(
94
+ "NL_PATIENT_NUMBER",
95
+ ACTION_REPLACE,
96
+ "Patiëntnummer",
97
+ "Een patiëntnummer koppelt tekst aan een specifieke patiëntregistratie.",
98
+ ("PAT-2026-1148",),
99
+ ),
100
+ CarePolicyRule(
101
+ "NL_CARE_CLIENT_NUMBER",
102
+ ACTION_REPLACE,
103
+ "Zorgcliëntnummer",
104
+ "Een cliëntnummer koppelt tekst aan een specifieke zorgcliënt.",
105
+ ("CL-ZORG-7712",),
106
+ ),
107
+ CarePolicyRule(
108
+ "NL_MEDICAL_RECORD_NUMBER",
109
+ ACTION_REPLACE,
110
+ "Medisch dossiernummer",
111
+ "Een medisch dossiernummer verwijst naar een individueel dossier.",
112
+ ("MD-2026-4412",),
113
+ ),
114
+ CarePolicyRule(
115
+ "NL_EPD_ECD_NUMBER",
116
+ ACTION_REPLACE,
117
+ "EPD- of ECD-nummer",
118
+ "Een EPD/ECD-nummer koppelt het document aan een individuele registratie.",
119
+ ("ECD-889144", "EPD-2026-5501"),
120
+ ),
121
+ CarePolicyRule(
122
+ "NL_HEALTH_INSURANCE_NUMBER",
123
+ ACTION_REPLACE,
124
+ "Verzekerdennummer",
125
+ "Een persoonsgebonden verzekerdennummer is administratieve identificatie.",
126
+ ("VERZ-88441122",),
127
+ ),
128
+ CarePolicyRule(
129
+ "NL_REFERRAL_NUMBER",
130
+ ACTION_REPLACE,
131
+ "Verwijsnummer",
132
+ "Een patiëntspecifieke verwijzing kan het zorgtraject identificeren.",
133
+ ("VERW-2026-7711",),
134
+ ),
135
+ CarePolicyRule(
136
+ "NL_TREATMENT_REFERENCE",
137
+ ACTION_REPLACE,
138
+ "Behandel- of zorgtrajectnummer",
139
+ "Een patiëntspecifieke behandelreferentie koppelt tekst aan een traject.",
140
+ ("BEH-2026-1188",),
141
+ ),
142
+ CarePolicyRule(
143
+ "NL_LAB_SAMPLE_NUMBER",
144
+ ACTION_REPLACE,
145
+ "Laboratorium- of monsternummer",
146
+ "Een laboratoriumreferentie kan rechtstreeks naar de patiëntorder leiden.",
147
+ ("LAB-2026-4412",),
148
+ ),
149
+ CarePolicyRule(
150
+ "NL_CARE_INCIDENT_NUMBER",
151
+ ACTION_REPLACE,
152
+ "Zorgincidentnummer",
153
+ "Een MIC/MIM/VIM- of incidentnummer koppelt tekst aan een specifieke melding.",
154
+ ("MIC-2026-0912",),
155
+ ),
156
+ CarePolicyRule(
157
+ "NL_CARE_INDICATION_REFERENCE",
158
+ ACTION_REPLACE,
159
+ "Indicatie- of beschikkingreferentie",
160
+ "Een CIZ, Wlz, Wmo of zorgtoewijzingsreferentie kan de cliënt identificeren.",
161
+ ("CIZ-2026-5512",),
162
+ ),
163
+ CarePolicyRule(
164
+ "NL_CARE_PROVIDER_NAME",
165
+ ACTION_REVIEW_SELECTED,
166
+ "Naam zorgverlener",
167
+ "De naam is herleidbaar, maar kan voor professionele context nodig zijn.",
168
+ ("dr. Karin de Groot", "verpleegkundige Omar El Idrissi"),
169
+ ),
170
+ CarePolicyRule(
171
+ "NL_BIG_NUMBER",
172
+ ACTION_REVIEW_SELECTED,
173
+ "BIG-nummer",
174
+ "Het nummer identificeert een zorgverlener en wordt daarom gecontroleerd.",
175
+ ("12345678901",),
176
+ ),
177
+ CarePolicyRule(
178
+ "NL_AGB_CODE",
179
+ ACTION_REVIEW_SELECTED,
180
+ "AGB-code",
181
+ "De code identificeert een zorgverlener, vestiging of onderneming.",
182
+ ("01020304",),
183
+ ),
184
+ CarePolicyRule(
185
+ "NL_CARE_ORGANIZATION",
186
+ ACTION_REVIEW_SELECTED,
187
+ "Zorgorganisatie",
188
+ "Een instelling kan in combinatie met andere details indirect herleidbaar zijn.",
189
+ ("Stichting Morgenlicht",),
190
+ ),
191
+ CarePolicyRule(
192
+ "NL_CARE_LOCATION_REFERENCE",
193
+ ACTION_REVIEW_SELECTED,
194
+ "Zorglocatie, afdeling of team",
195
+ "Een specifieke locatie kan een kleine patiëntgroep of casus identificeren.",
196
+ ("locatie Parkzicht", "afdeling Neurologie B"),
197
+ ),
198
+ CarePolicyRule(
199
+ "NL_ROOM_OR_BED_REFERENCE",
200
+ ACTION_REVIEW_SELECTED,
201
+ "Kamer- of bednummer",
202
+ "Een kamer of bed kan samen met datum en locatie indirect identificeren.",
203
+ ("kamer 214", "bed B-12"),
204
+ ),
205
+ CarePolicyRule(
206
+ "NL_CARE_EVENT_DATE",
207
+ ACTION_REVIEW_SELECTED,
208
+ "Exacte zorgdatum",
209
+ "Exacte opname-, ontslag-, afspraak- of behandeldatums worden gecontroleerd.",
210
+ ("opgenomen op 12-06-2026",),
211
+ ),
212
+ CarePolicyRule(
213
+ "CARE_DIAGNOSIS",
214
+ ACTION_PRESERVE,
215
+ "Diagnose",
216
+ "Diagnose is klinische betekenis en wordt niet blind gemaskeerd.",
217
+ ("diabetes mellitus type 2",),
218
+ ),
219
+ CarePolicyRule(
220
+ "CARE_MEDICATION",
221
+ ACTION_PRESERVE,
222
+ "Medicatie",
223
+ "Geneesmiddelnaam is noodzakelijke klinische inhoud.",
224
+ ("metformine", "paracetamol"),
225
+ ),
226
+ CarePolicyRule(
227
+ "CARE_DOSAGE",
228
+ ACTION_PRESERVE,
229
+ "Dosering en toedieningsschema",
230
+ "Dosering en schema zijn noodzakelijke klinische inhoud.",
231
+ ("500 mg tweemaal daags",),
232
+ ),
233
+ CarePolicyRule(
234
+ "CARE_LAB_RESULT",
235
+ ACTION_PRESERVE,
236
+ "Laboratoriumuitslag",
237
+ "Uitslag, eenheid en referentiewaarde moeten inhoudelijk intact blijven.",
238
+ ("Hb 7,8 mmol/L",),
239
+ ),
240
+ CarePolicyRule(
241
+ "CARE_OBSERVATION",
242
+ ACTION_PRESERVE,
243
+ "Observatie of voortgang",
244
+ "Verpleegkundige en medische observaties dragen de inhoud van het document.",
245
+ ("mobiliseert met rollator",),
246
+ ),
247
+ CarePolicyRule(
248
+ "CARE_VITAL_SIGN",
249
+ ACTION_PRESERVE,
250
+ "Vitale parameter",
251
+ "Bloeddruk, pols, temperatuur en saturatie zijn klinische inhoud.",
252
+ ("bloeddruk 123/78 mmHg",),
253
+ ),
254
+ CarePolicyRule(
255
+ "CARE_ALLERGY",
256
+ ACTION_PRESERVE,
257
+ "Allergie",
258
+ "Een allergie is essentiële klinische informatie.",
259
+ ("allergisch voor penicilline",),
260
+ ),
261
+ CarePolicyRule(
262
+ "CARE_TREATMENT",
263
+ ACTION_PRESERVE,
264
+ "Behandeling of verrichting",
265
+ "Behandelinhoud moet leesbaar blijven.",
266
+ ("wondzorg volgens protocol",),
267
+ ),
268
+ CarePolicyRule(
269
+ "CARE_GOAL",
270
+ ACTION_PRESERVE,
271
+ "Zorgdoel",
272
+ "Zorgdoelen en evaluatiecriteria zijn noodzakelijke professionele context.",
273
+ ("zelfstandig transfereren binnen zes weken",),
274
+ ),
275
+ CarePolicyRule(
276
+ "CARE_ROLE",
277
+ ACTION_PRESERVE,
278
+ "Zorgrol",
279
+ "Woorden zoals patiënt, arts en verpleegkundige blijven als rol leesbaar.",
280
+ ("patiënt", "arts", "verpleegkundige"),
281
+ ),
282
+ CarePolicyRule(
283
+ "CARE_CLINICAL_CODE",
284
+ ACTION_PRESERVE,
285
+ "Klinische classificatiecode",
286
+ "Klinische codes worden behouden tenzij bewijs toont dat ze patiëntspecifiek zijn.",
287
+ ("ICD-10 E11.9",),
288
+ ),
289
+ CarePolicyRule(
290
+ "CARE_RARE_CASE_COMBINATION",
291
+ ACTION_AUDIT_ONLY,
292
+ "Zeldzame combinatie van casusdetails",
293
+ "Indirecte herleidbaarheid wordt als restrisico gemeld zonder klinische inhoud blind te verwijderen.",
294
+ ("zeldzame diagnose plus kleine locatie en exacte datum",),
295
+ ),
296
+ )
297
+
298
+
299
+ CARE_POLICY_BY_ENTITY: Dict[str, CarePolicyRule] = {
300
+ rule.entity_type: rule for rule in CARE_POLICY_RULES
301
+ }
302
+
303
+
304
+ def policy_for_entity(entity_type: str) -> CarePolicyRule | None:
305
+ """Return the frozen care-profile rule for an entity type, if defined."""
306
+
307
+ return CARE_POLICY_BY_ENTITY.get(str(entity_type or "").strip())
308
+
309
+
310
+ def entities_for_action(action: str) -> tuple[str, ...]:
311
+ """Return entity types assigned to one policy action."""
312
+
313
+ if action not in VALID_ACTIONS:
314
+ raise ValueError(f"Unknown care-profile action: {action}")
315
+ return tuple(rule.entity_type for rule in CARE_POLICY_RULES if rule.action == action)
316
+
317
+
318
+ def validate_care_policy_rules(
319
+ rules: Sequence[CarePolicyRule] = CARE_POLICY_RULES,
320
+ ) -> tuple[str, ...]:
321
+ """Return deterministic contract errors; an empty tuple means valid."""
322
+
323
+ errors: list[str] = []
324
+ seen: set[str] = set()
325
+ for index, rule in enumerate(rules):
326
+ prefix = f"rule[{index}]"
327
+ if not rule.entity_type.strip():
328
+ errors.append(f"{prefix}: missing entity_type")
329
+ elif rule.entity_type in seen:
330
+ errors.append(f"{prefix}: duplicate entity_type {rule.entity_type}")
331
+ seen.add(rule.entity_type)
332
+ if rule.action not in VALID_ACTIONS:
333
+ errors.append(f"{prefix}: invalid action {rule.action}")
334
+ if not rule.label_nl.strip():
335
+ errors.append(f"{prefix}: missing Dutch label")
336
+ if not rule.reason_nl.strip():
337
+ errors.append(f"{prefix}: missing reason")
338
+ if not rule.examples:
339
+ errors.append(f"{prefix}: missing examples")
340
+ elif any(not str(value).strip() for value in rule.examples):
341
+ errors.append(f"{prefix}: contains empty example")
342
+ return tuple(errors)
343
+
344
+
345
+ def policy_snapshot() -> Mapping[str, str]:
346
+ """Return a stable entity-to-action mapping for reports and tests."""
347
+
348
+ return {rule.entity_type: rule.action for rule in CARE_POLICY_RULES}
care_test_examples.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fully synthetic Dutch care-document corpus for Zorgfilter v1.
2
+
3
+ The corpus contains no real people, organizations, identifiers or care records.
4
+ Every case separates values that should be replaced, values that should be
5
+ reviewed and selected by default, and clinical phrases that must be preserved.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, List
11
+
12
+
13
+ TEST_CASES: List[Dict[str, Any]] = [
14
+ {
15
+ "id": "care_daily_nursing_report_v1",
16
+ "name": "VVT - dagelijkse verpleegkundige rapportage",
17
+ "sector": "verpleging_verzorging_thuiszorg",
18
+ "document_type": "dagrapportage",
19
+ "text": """DAGELIJKSE RAPPORTAGE\n
20
+ Cliënt: Ahmed El Mansouri\n
21
+ Cliëntnummer: CL-ZORG-7712\n
22
+ ECD-nummer: ECD-889144\n
23
+ Geboortedatum: 14-02-1948\n
24
+ Adres: Zorglaan 18, 3511 AB Utrecht\n
25
+ Telefoon contactpersoon: 06 12345678\n
26
+ Contactpersoon: Fatima El Mansouri\n
27
+ Locatie: Stichting Morgenlicht, locatie Parkzicht, kamer 214\n
28
+ Rapportagedatum: 12-06-2026\n
29
+ Rapporteur: verpleegkundige Omar El Idrissi, BIG-nummer 12345678901\n
30
+
31
+ Cliënt was vanochtend helder en goed aanspreekbaar. Bloeddruk 123/78 mmHg, pols 72 per minuut en temperatuur 36,8 °C. Cliënt mobiliseert met rollator en had pijnscore 6 bij het opstaan. Na rust daalde de pijnscore naar 3. Metformine 500 mg werd volgens schema toegediend. Geen misselijkheid gemeld. Zorgdoel blijft: zelfstandig transfereren binnen zes weken.\n""",
32
+ "replace": [
33
+ {"value": "Ahmed El Mansouri", "entity_type": "PERSON"},
34
+ {"value": "CL-ZORG-7712", "entity_type": "NL_CARE_CLIENT_NUMBER"},
35
+ {"value": "ECD-889144", "entity_type": "NL_EPD_ECD_NUMBER"},
36
+ {"value": "14-02-1948", "entity_type": "NL_DATE_OF_BIRTH"},
37
+ {"value": "Zorglaan 18, 3511 AB Utrecht", "entity_type": "NL_ADDRESS"},
38
+ {"value": "06 12345678", "entity_type": "NL_PHONE_NUMBER"},
39
+ {"value": "Fatima El Mansouri", "entity_type": "PERSON"},
40
+ ],
41
+ "review_selected": [
42
+ {"value": "Stichting Morgenlicht", "entity_type": "NL_CARE_ORGANIZATION"},
43
+ {"value": "locatie Parkzicht", "entity_type": "NL_CARE_LOCATION_REFERENCE"},
44
+ {"value": "kamer 214", "entity_type": "NL_ROOM_OR_BED_REFERENCE"},
45
+ {"value": "12-06-2026", "entity_type": "NL_CARE_EVENT_DATE"},
46
+ {"value": "Omar El Idrissi", "entity_type": "NL_CARE_PROVIDER_NAME"},
47
+ {"value": "12345678901", "entity_type": "NL_BIG_NUMBER"},
48
+ ],
49
+ "preserve": [
50
+ "verpleegkundige",
51
+ "Bloeddruk 123/78 mmHg",
52
+ "pols 72 per minuut",
53
+ "temperatuur 36,8 °C",
54
+ "mobiliseert met rollator",
55
+ "pijnscore 6",
56
+ "Metformine 500 mg",
57
+ "zelfstandig transfereren binnen zes weken",
58
+ ],
59
+ "audit_only": [],
60
+ "ambiguity_traps": ["123/78", "72", "36,8", "500 mg", "pijnscore 6"],
61
+ },
62
+ {
63
+ "id": "care_plan_evaluation_v1",
64
+ "name": "Gehandicaptenzorg - zorgplan en evaluatie",
65
+ "sector": "gehandicaptenzorg",
66
+ "document_type": "zorgplan_evaluatie",
67
+ "text": """ZORGPLAN EN EVALUATIE\n
68
+ Cliënt: Noor van den Berg\n
69
+ Patiëntnummer: PAT-2026-1148\n
70
+ Medisch dossiernummer: MD-2026-4412\n
71
+ Vertegenwoordiger: mevrouw Elise van den Berg\n
72
+ E-mailadres vertegenwoordiger: elise.vandenberg@example.invalid\n
73
+ Zorgorganisatie: Zorggroep Kompas, woonlocatie De Linde, appartement 3B\n
74
+ Evaluatiedatum: 01-07-2026\n
75
+ Persoonlijk begeleider: Sanne de Wit\n
76
+
77
+ Probleem: cliënt raakt overprikkeld bij onverwachte wijzigingen in het dagprogramma. Diagnose autismespectrumstoornis blijft relevante klinische context. Zorgdoel: cliënt geeft met een pictogram aan wanneer rust nodig is. Actie: begeleider kondigt wijzigingen minimaal vijftien minuten vooraf aan. Evaluatie: het aantal escalaties daalde van vier naar één per week. Allergie: geen bekende medicatieallergieën.\n""",
78
+ "replace": [
79
+ {"value": "Noor van den Berg", "entity_type": "PERSON"},
80
+ {"value": "PAT-2026-1148", "entity_type": "NL_PATIENT_NUMBER"},
81
+ {"value": "MD-2026-4412", "entity_type": "NL_MEDICAL_RECORD_NUMBER"},
82
+ {"value": "Elise van den Berg", "entity_type": "PERSON"},
83
+ {"value": "elise.vandenberg@example.invalid", "entity_type": "EMAIL_ADDRESS"},
84
+ ],
85
+ "review_selected": [
86
+ {"value": "Zorggroep Kompas", "entity_type": "NL_CARE_ORGANIZATION"},
87
+ {"value": "woonlocatie De Linde", "entity_type": "NL_CARE_LOCATION_REFERENCE"},
88
+ {"value": "appartement 3B", "entity_type": "NL_ROOM_OR_BED_REFERENCE"},
89
+ {"value": "01-07-2026", "entity_type": "NL_CARE_EVENT_DATE"},
90
+ {"value": "Sanne de Wit", "entity_type": "NL_CARE_PROVIDER_NAME"},
91
+ ],
92
+ "preserve": [
93
+ "autismespectrumstoornis",
94
+ "raakt overprikkeld",
95
+ "Zorgdoel",
96
+ "pictogram",
97
+ "vijftien minuten vooraf",
98
+ "vier naar één per week",
99
+ "geen bekende medicatieallergieën",
100
+ ],
101
+ "audit_only": ["diagnose plus kleine woonlocatie en exact evaluatiemoment"],
102
+ "ambiguity_traps": ["vier", "één per week", "3B"],
103
+ },
104
+ {
105
+ "id": "care_nursing_transfer_v1",
106
+ "name": "Ziekenhuis naar wijkverpleging - verpleegkundige overdracht",
107
+ "sector": "ziekenhuis_en_wijkverpleging",
108
+ "document_type": "verpleegkundige_overdracht",
109
+ "text": """VERPLEEGKUNDIGE OVERDRACHT\n
110
+ Patiënt: mevrouw Laila Ait Haddou\n
111
+ EPD-nummer: EPD-2026-5501\n
112
+ BSN: 123456782\n
113
+ Geboortedatum: 03-09-1957\n
114
+ Woonadres: Herstelstraat 42, 3062 KL Rotterdam\n
115
+ Telefoon: 010 2345678\n
116
+ Verzendende organisatie: Stedelijk Ziekenhuis Noord, afdeling Neurologie B, bed B-12\n
117
+ Ontvangende organisatie: Wijkzorg Rijnmond, team Kralingen\n
118
+ Ontslagdatum: 18-06-2026\n
119
+ Overdragend verpleegkundige: Karin de Groot\n
120
+
121
+ Medische context: ischemisch CVA links. Patiënt is alert, heeft lichte rechtszijdige krachtsvermindering en spreekt korte zinnen. Mobiliteit: transfer met één persoon en rollator. Voeding: normaal dieet, dunne vloeistoffen toegestaan. Medicatie: clopidogrel 75 mg eenmaal daags en atorvastatine 40 mg eenmaal daags. Allergisch voor penicilline. Wondzorg is niet van toepassing. Vervolg: fysiotherapie en controle bij neuroloog over zes weken.\n""",
122
+ "replace": [
123
+ {"value": "Laila Ait Haddou", "entity_type": "PERSON"},
124
+ {"value": "EPD-2026-5501", "entity_type": "NL_EPD_ECD_NUMBER"},
125
+ {"value": "123456782", "entity_type": "NL_BSN"},
126
+ {"value": "03-09-1957", "entity_type": "NL_DATE_OF_BIRTH"},
127
+ {"value": "Herstelstraat 42, 3062 KL Rotterdam", "entity_type": "NL_ADDRESS"},
128
+ {"value": "010 2345678", "entity_type": "NL_PHONE_NUMBER"},
129
+ ],
130
+ "review_selected": [
131
+ {"value": "Stedelijk Ziekenhuis Noord", "entity_type": "NL_CARE_ORGANIZATION"},
132
+ {"value": "afdeling Neurologie B", "entity_type": "NL_CARE_LOCATION_REFERENCE"},
133
+ {"value": "bed B-12", "entity_type": "NL_ROOM_OR_BED_REFERENCE"},
134
+ {"value": "Wijkzorg Rijnmond", "entity_type": "NL_CARE_ORGANIZATION"},
135
+ {"value": "team Kralingen", "entity_type": "NL_CARE_LOCATION_REFERENCE"},
136
+ {"value": "18-06-2026", "entity_type": "NL_CARE_EVENT_DATE"},
137
+ {"value": "Karin de Groot", "entity_type": "NL_CARE_PROVIDER_NAME"},
138
+ ],
139
+ "preserve": [
140
+ "ischemisch CVA links",
141
+ "lichte rechtszijdige krachtsvermindering",
142
+ "transfer met één persoon en rollator",
143
+ "clopidogrel 75 mg eenmaal daags",
144
+ "atorvastatine 40 mg eenmaal daags",
145
+ "Allergisch voor penicilline",
146
+ "fysiotherapie",
147
+ ],
148
+ "audit_only": [],
149
+ "ambiguity_traps": ["75 mg", "40 mg", "zes weken", "B-12"],
150
+ },
151
+ {
152
+ "id": "care_discharge_letter_v1",
153
+ "name": "Medisch-specialistische ontslagbrief",
154
+ "sector": "medisch_specialistische_zorg",
155
+ "document_type": "ontslagbrief",
156
+ "text": """ONTSLAGBRIEF INTERNE GENEESKUNDE\n
157
+ Patiënt: Peter van Leeuwen\n
158
+ Patiëntnummer: PAT-2026-7781\n
159
+ Verzekerdennummer: VERZ-88441122\n
160
+ Opnamedatum: 10-06-2026\n
161
+ Ontslagdatum: 14-06-2026\n
162
+ Huisarts: dr. Miriam Vos, AGB-code 01020304\n
163
+ Behandelend internist: dr. Jeroen de Bruin, BIG-nummer 10987654321\n
164
+ Ziekenhuis: Medisch Centrum Rivierenland, afdeling Interne Geneeskunde\n
165
+
166
+ Reden opname: dehydratie bij gastro-enteritis. Hoofddiagnose: acute nierinsufficiëntie, volledig hersteld na intraveneuze vochttoediening. Bij ontslag creatinine 88 µmol/L en natrium 139 mmol/L. Medicatie bij ontslag: amlodipine 5 mg eenmaal daags. Advies: voldoende drinken en nierfunctiecontrole bij de huisarts binnen één week.\n""",
167
+ "replace": [
168
+ {"value": "Peter van Leeuwen", "entity_type": "PERSON"},
169
+ {"value": "PAT-2026-7781", "entity_type": "NL_PATIENT_NUMBER"},
170
+ {"value": "VERZ-88441122", "entity_type": "NL_HEALTH_INSURANCE_NUMBER"},
171
+ ],
172
+ "review_selected": [
173
+ {"value": "10-06-2026", "entity_type": "NL_CARE_EVENT_DATE"},
174
+ {"value": "14-06-2026", "entity_type": "NL_CARE_EVENT_DATE"},
175
+ {"value": "Miriam Vos", "entity_type": "NL_CARE_PROVIDER_NAME"},
176
+ {"value": "01020304", "entity_type": "NL_AGB_CODE"},
177
+ {"value": "Jeroen de Bruin", "entity_type": "NL_CARE_PROVIDER_NAME"},
178
+ {"value": "10987654321", "entity_type": "NL_BIG_NUMBER"},
179
+ {"value": "Medisch Centrum Rivierenland", "entity_type": "NL_CARE_ORGANIZATION"},
180
+ {"value": "afdeling Interne Geneeskunde", "entity_type": "NL_CARE_LOCATION_REFERENCE"},
181
+ ],
182
+ "preserve": [
183
+ "dehydratie bij gastro-enteritis",
184
+ "Hoofddiagnose: acute nierinsufficiëntie",
185
+ "intraveneuze vochttoediening",
186
+ "creatinine 88 µmol/L",
187
+ "natrium 139 mmol/L",
188
+ "amlodipine 5 mg eenmaal daags",
189
+ "nierfunctiecontrole",
190
+ ],
191
+ "audit_only": ["zeldzame diagnose in combinatie met kleine afdeling en exacte opnameperiode"],
192
+ "ambiguity_traps": ["88 µmol/L", "139 mmol/L", "5 mg", "één week"],
193
+ },
194
+ {
195
+ "id": "care_gp_referral_v1",
196
+ "name": "Huisartsverwijzing cardiologie",
197
+ "sector": "huisartsenzorg",
198
+ "document_type": "verwijsbrief",
199
+ "text": """VERWIJSBRIEF CARDIOLOGIE\n
200
+ Patiënt: Samira Benali\n
201
+ Geboortedatum: 22-11-1966\n
202
+ BSN: 123456782\n
203
+ Verwijsnummer: VERW-2026-7711\n
204
+ Behandelnummer: BEH-2026-1188\n
205
+ Huisarts: dr. Thomas Mulder, praktijk De Stadspoort, AGB-code 87654321\n
206
+ E-mail patiënt: samira.benali@example.invalid\n
207
+
208
+ Vraagstelling: beoordeling van inspanningsgebonden thoracale druk. Voorgeschiedenis: hypertensie en diabetes mellitus type 2. Medicatie: metformine 500 mg tweemaal daags en lisinopril 10 mg eenmaal daags. Bloeddruk op het spreekuur 148/86 mmHg. Geen bekende geneesmiddelenallergieën. Graag beoordeling en behandeladvies.\n""",
209
+ "replace": [
210
+ {"value": "Samira Benali", "entity_type": "PERSON"},
211
+ {"value": "22-11-1966", "entity_type": "NL_DATE_OF_BIRTH"},
212
+ {"value": "123456782", "entity_type": "NL_BSN"},
213
+ {"value": "VERW-2026-7711", "entity_type": "NL_REFERRAL_NUMBER"},
214
+ {"value": "BEH-2026-1188", "entity_type": "NL_TREATMENT_REFERENCE"},
215
+ {"value": "samira.benali@example.invalid", "entity_type": "EMAIL_ADDRESS"},
216
+ ],
217
+ "review_selected": [
218
+ {"value": "Thomas Mulder", "entity_type": "NL_CARE_PROVIDER_NAME"},
219
+ {"value": "praktijk De Stadspoort", "entity_type": "NL_CARE_ORGANIZATION"},
220
+ {"value": "87654321", "entity_type": "NL_AGB_CODE"},
221
+ ],
222
+ "preserve": [
223
+ "inspanningsgebonden thoracale druk",
224
+ "hypertensie",
225
+ "diabetes mellitus type 2",
226
+ "metformine 500 mg tweemaal daags",
227
+ "lisinopril 10 mg eenmaal daags",
228
+ "148/86 mmHg",
229
+ "Geen bekende geneesmiddelenallergieën",
230
+ ],
231
+ "audit_only": [],
232
+ "ambiguity_traps": ["148/86", "500 mg", "10 mg", "type 2"],
233
+ },
234
+ {
235
+ "id": "care_medication_overview_v1",
236
+ "name": "Apotheek - medicatieoverzicht",
237
+ "sector": "farmaceutische_zorg",
238
+ "document_type": "medicatieoverzicht",
239
+ "text": """ACTUEEL MEDICATIEOVERZICHT\n
240
+ Patiënt: Willem de Jong\n
241
+ Patiëntnummer: PAT-2026-3321\n
242
+ Geboortedatum: 09-04-1952\n
243
+ Apotheek: Apotheek Waterlinie, AGB-code 11223344\n
244
+ Voorschrijver: huisarts dr. Eva Smit, BIG-nummer 11223344556\n
245
+ Receptnummer: BEH-2026-2244\n
246
+
247
+ 1. Metoprolol 50 mg, eenmaal daags om 08:00 uur.\n
248
+ 2. Apixaban 5 mg, tweemaal daags om 08:00 en 20:00 uur.\n
249
+ 3. Omeprazol 20 mg, eenmaal daags voor het ontbijt.\n
250
+ Allergie: naproxen veroorzaakt urticaria.\n""",
251
+ "replace": [
252
+ {"value": "Willem de Jong", "entity_type": "PERSON"},
253
+ {"value": "PAT-2026-3321", "entity_type": "NL_PATIENT_NUMBER"},
254
+ {"value": "09-04-1952", "entity_type": "NL_DATE_OF_BIRTH"},
255
+ {"value": "BEH-2026-2244", "entity_type": "NL_TREATMENT_REFERENCE"},
256
+ ],
257
+ "review_selected": [
258
+ {"value": "Apotheek Waterlinie", "entity_type": "NL_CARE_ORGANIZATION"},
259
+ {"value": "11223344", "entity_type": "NL_AGB_CODE"},
260
+ {"value": "Eva Smit", "entity_type": "NL_CARE_PROVIDER_NAME"},
261
+ {"value": "11223344556", "entity_type": "NL_BIG_NUMBER"},
262
+ ],
263
+ "preserve": [
264
+ "Metoprolol 50 mg",
265
+ "eenmaal daags om 08:00 uur",
266
+ "Apixaban 5 mg",
267
+ "tweemaal daags om 08:00 en 20:00 uur",
268
+ "Omeprazol 20 mg",
269
+ "naproxen veroorzaakt urticaria",
270
+ ],
271
+ "audit_only": [],
272
+ "ambiguity_traps": ["50 mg", "5 mg", "20 mg", "08:00", "20:00"],
273
+ },
274
+ {
275
+ "id": "care_laboratory_report_v1",
276
+ "name": "Laboratorium - klinisch chemisch rapport",
277
+ "sector": "laboratoriumzorg",
278
+ "document_type": "laboratoriumrapport",
279
+ "text": """KLINISCH CHEMISCH RAPPORT\n
280
+ Patiënt: Daan Vermeer\n
281
+ Medisch dossiernummer: MD-2026-7744\n
282
+ Monsternummer: LAB-2026-4412\n
283
+ Afnamedatum: 20-06-2026\n
284
+ Aanvrager: dr. Nadia Bos\n
285
+ Laboratorium: Diagnostiek Centrum Delta\n
286
+
287
+ Hemoglobine: 7,8 mmol/L (referentie 8,5-11,0).\n
288
+ Leukocyten: 6,2 x10^9/L (referentie 4,0-10,0).\n
289
+ CRP: 4 mg/L (referentie kleiner dan 5).\n
290
+ Glucose: 4,4 mmol/L.\n
291
+ Conclusie: licht verlaagd hemoglobine, overige waarden zonder bijzonderheden.\n""",
292
+ "replace": [
293
+ {"value": "Daan Vermeer", "entity_type": "PERSON"},
294
+ {"value": "MD-2026-7744", "entity_type": "NL_MEDICAL_RECORD_NUMBER"},
295
+ {"value": "LAB-2026-4412", "entity_type": "NL_LAB_SAMPLE_NUMBER"},
296
+ ],
297
+ "review_selected": [
298
+ {"value": "20-06-2026", "entity_type": "NL_CARE_EVENT_DATE"},
299
+ {"value": "Nadia Bos", "entity_type": "NL_CARE_PROVIDER_NAME"},
300
+ {"value": "Diagnostiek Centrum Delta", "entity_type": "NL_CARE_ORGANIZATION"},
301
+ ],
302
+ "preserve": [
303
+ "Hemoglobine: 7,8 mmol/L",
304
+ "referentie 8,5-11,0",
305
+ "Leukocyten: 6,2 x10^9/L",
306
+ "CRP: 4 mg/L",
307
+ "Glucose: 4,4 mmol/L",
308
+ "licht verlaagd hemoglobine",
309
+ ],
310
+ "audit_only": [],
311
+ "ambiguity_traps": ["7,8", "8,5-11,0", "6,2", "4 mg/L", "4,4"],
312
+ },
313
+ {
314
+ "id": "care_incident_report_v1",
315
+ "name": "VVT - MIC medicatie-incident",
316
+ "sector": "verpleging_verzorging_thuiszorg",
317
+ "document_type": "mic_mim_vim_rapport",
318
+ "text": """MIC-MELDING MEDICATIE-INCIDENT\n
319
+ Cliënt: mevrouw Johanna Peters\n
320
+ Cliëntnummer: CL-ZORG-9901\n
321
+ Incidentnummer: MIC-2026-0912\n
322
+ Locatie: woonzorgcentrum De Brug, afdeling Waterkant, kamer 18\n
323
+ Incidentdatum: 21-06-2026 om 20:15 uur\n
324
+ Melder: verzorgende IG Youssef Amrani\n
325
+ Betrokken arts: dr. Lotte Kramer\n
326
+ Contactpersoon familie: Mark Peters, telefoon 06 87654321\n
327
+
328
+ Omschrijving: de avonddosering apixaban 5 mg is om 20:00 uur niet toegediend. De omissie werd om 20:15 uur ontdekt. De arts beoordeelde dat geen inhaaldosis nodig was. Cliënt had geen klachten en de vitale parameters waren stabiel. Maatregel: dubbele controle van de medicatielijst bij de avonddienst.\n""",
329
+ "replace": [
330
+ {"value": "Johanna Peters", "entity_type": "PERSON"},
331
+ {"value": "CL-ZORG-9901", "entity_type": "NL_CARE_CLIENT_NUMBER"},
332
+ {"value": "MIC-2026-0912", "entity_type": "NL_CARE_INCIDENT_NUMBER"},
333
+ {"value": "Mark Peters", "entity_type": "PERSON"},
334
+ {"value": "06 87654321", "entity_type": "NL_PHONE_NUMBER"},
335
+ ],
336
+ "review_selected": [
337
+ {"value": "woonzorgcentrum De Brug", "entity_type": "NL_CARE_ORGANIZATION"},
338
+ {"value": "afdeling Waterkant", "entity_type": "NL_CARE_LOCATION_REFERENCE"},
339
+ {"value": "kamer 18", "entity_type": "NL_ROOM_OR_BED_REFERENCE"},
340
+ {"value": "21-06-2026", "entity_type": "NL_CARE_EVENT_DATE"},
341
+ {"value": "Youssef Amrani", "entity_type": "NL_CARE_PROVIDER_NAME"},
342
+ {"value": "Lotte Kramer", "entity_type": "NL_CARE_PROVIDER_NAME"},
343
+ ],
344
+ "preserve": [
345
+ "verzorgende IG",
346
+ "apixaban 5 mg",
347
+ "20:00 uur niet toegediend",
348
+ "geen inhaaldosis nodig",
349
+ "geen klachten",
350
+ "vitale parameters waren stabiel",
351
+ "dubbele controle van de medicatielijst",
352
+ ],
353
+ "audit_only": ["kleine woonlocatie plus exact incidentmoment en zeldzame gebeurtenis"],
354
+ "ambiguity_traps": ["5 mg", "20:00", "20:15", "kamer 18"],
355
+ },
356
+ ]
357
+
358
+
359
+ def get_case(case_id: str) -> Dict[str, Any]:
360
+ """Return one synthetic case by stable ID."""
361
+
362
+ for case in TEST_CASES:
363
+ if case["id"] == case_id:
364
+ return case
365
+ raise KeyError(case_id)
366
+
367
+
368
+ def all_expected_values(case: Dict[str, Any]) -> List[str]:
369
+ """Return all exact replace/review values from one case."""
370
+
371
+ values: List[str] = []
372
+ for bucket in ("replace", "review_selected"):
373
+ values.extend(str(item["value"]) for item in case.get(bucket, []))
374
+ return values
handover/workpackages/20260803_1531_care_profile_v1_policy_and_corpus_foundation.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Handover — SCRUB-WP_CARE_PROFILE_V1_POLICY_AND_CORPUS_FOUNDATION
2
+
3
+ Repository: solidprivacy-nl/scrub
4
+ Workpackage title: Zorgfilter v1 policy and corpus foundation
5
+ Status: completed; final clean PR validation initiated
6
+
7
+ ## Summary
8
+
9
+ Recorded the approved Zorgfilter v1 policy and workpackage sequence. Added a pure policy contract, eight fully synthetic care-document families, corpus contract tests and a deterministic baseline helper for the current Dutch custom recognizers. No recognizer or UI behavior has changed.
10
+
11
+ ## Files added
12
+
13
+ - `CARE_PROFILE_V1_PLAN.md`
14
+ - `care_profile_policy.py`
15
+ - `care_test_examples.py`
16
+ - `care_profile_baseline.py`
17
+ - `scripts/generate_care_profile_baseline.py`
18
+ - `tests/test_care_profile_policy_contract.py`
19
+ - `tests/test_care_profile_corpus_contracts.py`
20
+ - `tests/test_care_profile_current_engine_baseline.py`
21
+ - `workpackage_claims/scrub_wp_care_profile_v1_policy_and_corpus_foundation.md`
22
+ - `handover/workpackages/20260803_1531_care_profile_v1_policy_and_corpus_foundation.md`
23
+
24
+ ## Files changed
25
+
26
+ - `ROADMAP.md`
27
+ - `WORKPACKAGES.md`
28
+ - `CHANGELOG.md`
29
+ - `DECISION_LOG.md`
30
+ - `RISK_REGISTER.md`
31
+
32
+ ## Tests
33
+
34
+ - Pure policy-contract tests.
35
+ - Synthetic corpus structural and policy-alignment tests.
36
+ - Current deterministic recognizer baseline-schema tests.
37
+
38
+ ## Validation
39
+
40
+ - Initial GitHub Actions run: 917 passed, 1 new corpus-contract test failed because a valid discharge-letter preserve phrase lacked one of the test's clinical marker words.
41
+ - Corrective action: replaced the brittle marker-word assertion with a contract for multiple protected clinical passages that remain disjoint from replace/review values.
42
+ - GitHub Actions run #1818: 918 tests passed on the corrected implementation.
43
+ - Final clean PR validation: initiated after all temporary workflow/operator files were removed.
44
+ - Hugging Face sync status: not functionally relevant; no runtime/UI change.
45
+ - App verification status: not applicable.
46
+
47
+ ## Remaining risks
48
+
49
+ - The current broad `NL_HEALTHCARE_REFERENCE` behavior remains unchanged and must be split through later evidence-driven recognizer packages.
50
+ - The baseline excludes generic NER-model results and is not a production benchmark.
51
+ - Exact care-date, provider, organization and location policy requires later recognizer precision testing.
52
+ - Human review remains mandatory.
53
+
54
+ ## Next recommended step
55
+
56
+ Generate and inspect the current-engine baseline report, perform gap triage, then freeze care-recognizer contracts before implementing care-specific recognizers.
scripts/generate_care_profile_baseline.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Generate a machine-readable pre-Zorgfilter recognizer baseline."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+
10
+ from care_profile_baseline import build_current_care_baseline
11
+
12
+
13
+ def main() -> int:
14
+ parser = argparse.ArgumentParser()
15
+ parser.add_argument(
16
+ "--output",
17
+ default="output/validation/care_profile_v1_current_engine_baseline.json",
18
+ )
19
+ args = parser.parse_args()
20
+
21
+ output_path = Path(args.output)
22
+ output_path.parent.mkdir(parents=True, exist_ok=True)
23
+ output_path.write_text(
24
+ json.dumps(build_current_care_baseline(), ensure_ascii=False, indent=2) + "\n",
25
+ encoding="utf-8",
26
+ )
27
+ print(output_path)
28
+ return 0
29
+
30
+
31
+ if __name__ == "__main__":
32
+ raise SystemExit(main())
tests/test_care_profile_corpus_contracts.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from care_profile_policy import ACTION_REPLACE, ACTION_REVIEW_SELECTED, policy_for_entity
2
+ from care_test_examples import TEST_CASES, all_expected_values, get_case
3
+
4
+
5
+ REQUIRED_DOCUMENT_TYPES = {
6
+ "dagrapportage",
7
+ "zorgplan_evaluatie",
8
+ "verpleegkundige_overdracht",
9
+ "ontslagbrief",
10
+ "verwijsbrief",
11
+ "medicatieoverzicht",
12
+ "laboratoriumrapport",
13
+ "mic_mim_vim_rapport",
14
+ }
15
+
16
+
17
+ def test_care_corpus_contains_eight_stable_document_families():
18
+ assert len(TEST_CASES) == 8
19
+ assert {case["document_type"] for case in TEST_CASES} == REQUIRED_DOCUMENT_TYPES
20
+ assert len({case["id"] for case in TEST_CASES}) == len(TEST_CASES)
21
+
22
+
23
+ def test_every_expected_identifier_occurs_exactly_in_its_synthetic_text():
24
+ for case in TEST_CASES:
25
+ text = case["text"]
26
+ for value in all_expected_values(case):
27
+ assert value in text, f"{case['id']}: expected value not present: {value}"
28
+
29
+
30
+ def test_every_preserve_phrase_and_ambiguity_trap_occurs_in_text():
31
+ for case in TEST_CASES:
32
+ text = case["text"]
33
+ assert case["preserve"], f"{case['id']}: preserve list must not be empty"
34
+ for phrase in case["preserve"]:
35
+ assert phrase in text, f"{case['id']}: preserve phrase not present: {phrase}"
36
+ for trap in case["ambiguity_traps"]:
37
+ assert trap in text, f"{case['id']}: ambiguity trap not present: {trap}"
38
+
39
+
40
+ def test_corpus_buckets_match_the_frozen_policy():
41
+ for case in TEST_CASES:
42
+ for item in case["replace"]:
43
+ rule = policy_for_entity(item["entity_type"])
44
+ assert rule is not None, f"{case['id']}: missing policy for {item['entity_type']}"
45
+ assert rule.action == ACTION_REPLACE
46
+ for item in case["review_selected"]:
47
+ rule = policy_for_entity(item["entity_type"])
48
+ assert rule is not None, f"{case['id']}: missing policy for {item['entity_type']}"
49
+ assert rule.action == ACTION_REVIEW_SELECTED
50
+
51
+
52
+ def test_corpus_uses_only_synthetic_email_domains():
53
+ for case in TEST_CASES:
54
+ for item in case["replace"]:
55
+ if item["entity_type"] == "EMAIL_ADDRESS":
56
+ assert item["value"].endswith(".invalid")
57
+
58
+
59
+ def test_clinical_meaning_is_represented_in_every_case():
60
+ for case in TEST_CASES:
61
+ protected_values = set(all_expected_values(case))
62
+ preserve_phrases = list(case["preserve"])
63
+
64
+ assert len(preserve_phrases) >= 3, case["id"]
65
+ assert sum(len(phrase.strip()) for phrase in preserve_phrases) >= 40, case["id"]
66
+ assert protected_values.isdisjoint(preserve_phrases), case["id"]
67
+
68
+
69
+ def test_get_case_uses_stable_ids():
70
+ case = get_case("care_laboratory_report_v1")
71
+ assert case["document_type"] == "laboratoriumrapport"
tests/test_care_profile_current_engine_baseline.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from care_profile_baseline import build_current_care_baseline
2
+
3
+
4
+ def test_current_care_baseline_is_complete_evidence_not_a_readiness_claim():
5
+ report = build_current_care_baseline()
6
+
7
+ assert report["schema_version"] == "1.0"
8
+ assert report["synthetic_data_only"] is True
9
+ assert report["production_ready"] is False
10
+ assert report["human_review_required"] is True
11
+ assert report["case_count"] == 8
12
+ assert len(report["cases"]) == 8
13
+ assert report["expected_value_count"] >= report["found_value_count"] >= 0
14
+ assert 0.0 <= report["indicative_recall"] <= 1.0
15
+ assert report["preserve_overlap_count"] >= 0
16
+
17
+
18
+ def test_each_baseline_case_keeps_exact_expectation_evidence():
19
+ report = build_current_care_baseline()
20
+
21
+ for case in report["cases"]:
22
+ assert case["expected_value_count"] == len(case["expectations"])
23
+ assert case["found_value_count"] == sum(
24
+ 1 for expectation in case["expectations"] if expectation["found"]
25
+ )
26
+ for expectation in case["expectations"]:
27
+ assert expectation["policy_bucket"] in {"replace", "review_selected"}
28
+ assert expectation["value"]
29
+ assert expectation["expected_entity_type"]
30
+ assert isinstance(expectation["detected_entity_types"], list)
31
+ assert isinstance(expectation["detected_spans"], list)
32
+
33
+
34
+ def test_baseline_scope_excludes_generic_ner_and_cloud_processing():
35
+ report = build_current_care_baseline()
36
+
37
+ assert "generic NER excluded" in report["scope"]
38
+ assert report["profile"] == "current_custom_recognizers_before_care_profile"
tests/test_care_profile_policy_contract.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from care_profile_policy import (
2
+ ACTION_AUDIT_ONLY,
3
+ ACTION_PRESERVE,
4
+ ACTION_REPLACE,
5
+ ACTION_REVIEW_SELECTED,
6
+ entities_for_action,
7
+ policy_for_entity,
8
+ policy_snapshot,
9
+ validate_care_policy_rules,
10
+ )
11
+
12
+
13
+ def test_care_policy_contract_is_structurally_valid():
14
+ assert validate_care_policy_rules() == ()
15
+
16
+
17
+ def test_all_four_policy_actions_are_used():
18
+ assert entities_for_action(ACTION_REPLACE)
19
+ assert entities_for_action(ACTION_REVIEW_SELECTED)
20
+ assert entities_for_action(ACTION_PRESERVE)
21
+ assert entities_for_action(ACTION_AUDIT_ONLY)
22
+
23
+
24
+ def test_approved_care_policy_preferences_are_frozen():
25
+ assert policy_for_entity("NL_DATE_OF_BIRTH").action == ACTION_REPLACE
26
+ assert policy_for_entity("NL_PATIENT_NUMBER").action == ACTION_REPLACE
27
+ assert policy_for_entity("NL_CARE_PROVIDER_NAME").action == ACTION_REVIEW_SELECTED
28
+ assert policy_for_entity("NL_BIG_NUMBER").action == ACTION_REVIEW_SELECTED
29
+ assert policy_for_entity("NL_AGB_CODE").action == ACTION_REVIEW_SELECTED
30
+ assert policy_for_entity("CARE_DIAGNOSIS").action == ACTION_PRESERVE
31
+ assert policy_for_entity("CARE_MEDICATION").action == ACTION_PRESERVE
32
+ assert policy_for_entity("CARE_LAB_RESULT").action == ACTION_PRESERVE
33
+ assert policy_for_entity("CARE_OBSERVATION").action == ACTION_PRESERVE
34
+ assert policy_for_entity("CARE_RARE_CASE_COMBINATION").action == ACTION_AUDIT_ONLY
35
+
36
+
37
+ def test_clinical_codes_are_not_blindly_replaced():
38
+ assert policy_for_entity("CARE_CLINICAL_CODE").action == ACTION_PRESERVE
39
+ assert "CARE_CLINICAL_CODE" not in entities_for_action(ACTION_REPLACE)
40
+
41
+
42
+ def test_policy_snapshot_is_stable_and_complete():
43
+ snapshot = policy_snapshot()
44
+ assert snapshot["NL_EPD_ECD_NUMBER"] == ACTION_REPLACE
45
+ assert snapshot["NL_CARE_EVENT_DATE"] == ACTION_REVIEW_SELECTED
46
+ assert snapshot["CARE_DOSAGE"] == ACTION_PRESERVE
47
+ assert len(snapshot) == len(set(snapshot))
workpackage_claims/scrub_wp_care_profile_v1_policy_and_corpus_foundation.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Workpackage claim — SCRUB-WP_CARE_PROFILE_V1_POLICY_AND_CORPUS_FOUNDATION
2
+
3
+ Repository: `solidprivacy-nl/scrub`
4
+ Claimed at: 2026-08-03 15:31 Europe/Amsterdam
5
+ Status: completed
6
+
7
+ ## Scope
8
+
9
+ - Record the approved Zorgfilter v1 policy in roadmap, decision, risk and workpackage documentation.
10
+ - Define the sequential Zorgfilter workpackage line.
11
+ - Add a pure, machine-readable care-profile policy contract.
12
+ - Add a first fully synthetic care-document corpus with explicit detect, review and preserve expectations.
13
+ - Add contract tests and a current-engine baseline artifact without changing recognizer, UI, export, Scrub Key or reinsert behavior.
14
+
15
+ ## Boundaries
16
+
17
+ - Synthetic data only.
18
+ - No real care records or personal data.
19
+ - No UI integration in this first package.
20
+ - No automatic masking of diagnosis, medication, lab results or observations.
21
+ - No change to current legal/general/international profile behavior.
22
+ - No cloud document processing.