pii-detector-ca 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pii_detector_ca-0.1.0/.gitignore +33 -0
- pii_detector_ca-0.1.0/CHANGELOG.md +35 -0
- pii_detector_ca-0.1.0/CITATION.cff +28 -0
- pii_detector_ca-0.1.0/CONTRIBUTING.md +53 -0
- pii_detector_ca-0.1.0/LICENSE +21 -0
- pii_detector_ca-0.1.0/PKG-INFO +172 -0
- pii_detector_ca-0.1.0/README.md +115 -0
- pii_detector_ca-0.1.0/SECURITY.md +29 -0
- pii_detector_ca-0.1.0/benchmark/check_determinism.py +91 -0
- pii_detector_ca-0.1.0/benchmark/curate_oda.py +154 -0
- pii_detector_ca-0.1.0/benchmark/curate_oda_eval.py +91 -0
- pii_detector_ca-0.1.0/benchmark/curate_statcan_names.py +100 -0
- pii_detector_ca-0.1.0/benchmark/generate_address_eval.py +80 -0
- pii_detector_ca-0.1.0/benchmark/generate_corpus.py +158 -0
- pii_detector_ca-0.1.0/benchmark/generate_hard.py +248 -0
- pii_detector_ca-0.1.0/benchmark/generate_negatives.py +185 -0
- pii_detector_ca-0.1.0/benchmark/pii_generators.py +260 -0
- pii_detector_ca-0.1.0/benchmark/templates.py +138 -0
- pii_detector_ca-0.1.0/docs/architecture/core_contract.md +64 -0
- pii_detector_ca-0.1.0/docs/identifiers/README.md +24 -0
- pii_detector_ca-0.1.0/docs/identifiers/ca_ab_uli.md +44 -0
- pii_detector_ca-0.1.0/docs/identifiers/ca_bc_phn.md +34 -0
- pii_detector_ca-0.1.0/docs/identifiers/ca_health_card_generic.md +41 -0
- pii_detector_ca-0.1.0/docs/identifiers/ca_ohip.md +38 -0
- pii_detector_ca-0.1.0/docs/identifiers/ca_postal_code.md +36 -0
- pii_detector_ca-0.1.0/docs/identifiers/ca_ramq.md +50 -0
- pii_detector_ca-0.1.0/docs/identifiers/ca_sin.md +45 -0
- pii_detector_ca-0.1.0/docs/identifiers/ca_street_address.md +37 -0
- pii_detector_ca-0.1.0/docs/user_manual.md +309 -0
- pii_detector_ca-0.1.0/evaluation/check_thresholds.py +164 -0
- pii_detector_ca-0.1.0/evaluation/metrics.py +190 -0
- pii_detector_ca-0.1.0/evaluation/requirements-eval.txt +12 -0
- pii_detector_ca-0.1.0/evaluation/run_address_eval.py +93 -0
- pii_detector_ca-0.1.0/evaluation/run_core_eval.py +163 -0
- pii_detector_ca-0.1.0/evaluation/run_eval.py +243 -0
- pii_detector_ca-0.1.0/evaluation/run_hard.py +161 -0
- pii_detector_ca-0.1.0/evaluation/run_negatives.py +116 -0
- pii_detector_ca-0.1.0/evaluation/seed_variance.py +89 -0
- pii_detector_ca-0.1.0/evaluation/thresholds.json +41 -0
- pii_detector_ca-0.1.0/pyproject.toml +86 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/__init__.py +59 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/adapters/__init__.py +5 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/adapters/presidio.py +47 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/base.py +63 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/canada/__init__.py +34 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/canada/health_cards.py +23 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/canada/postal_code.py +7 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/canada/sin.py +7 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/canada/street_address.py +7 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/core/__init__.py +21 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/core/scanner.py +85 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/core/specs.py +212 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/core/types.py +38 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/core/validators.py +158 -0
- pii_detector_ca-0.1.0/src/pii_detector_ca/validators.py +5 -0
- pii_detector_ca-0.1.0/tests/test_core_scanner.py +105 -0
- pii_detector_ca-0.1.0/tests/test_negatives.py +104 -0
- pii_detector_ca-0.1.0/tests/test_properties.py +108 -0
- pii_detector_ca-0.1.0/tests/test_recognizers.py +204 -0
- pii_detector_ca-0.1.0/tests/test_validators.py +89 -0
- pii_detector_ca-0.1.0/uv.lock +2173 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
|
|
11
|
+
# Benchmark artifacts (generated; gold split is kept in-repo)
|
|
12
|
+
benchmark/data/generated/
|
|
13
|
+
|
|
14
|
+
# Real clinical text must never be committed (licensing/PHI)
|
|
15
|
+
benchmark/data/real/
|
|
16
|
+
|
|
17
|
+
# Raw external datasets (e.g. StatCan ODA CSVs): too large to commit; a small
|
|
18
|
+
# curated sample is committed instead (see benchmark/curate_oda.py).
|
|
19
|
+
benchmark/data/external/
|
|
20
|
+
|
|
21
|
+
# OS / editors
|
|
22
|
+
.DS_Store
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
|
|
26
|
+
# Secrets
|
|
27
|
+
.env
|
|
28
|
+
*.key
|
|
29
|
+
*credentials*.json
|
|
30
|
+
|
|
31
|
+
# Tool caches
|
|
32
|
+
.hypothesis/
|
|
33
|
+
.mypy_cache/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - unreleased
|
|
8
|
+
|
|
9
|
+
First public release, Canada only. (Pre-release history: the package was developed as `presidio-recognizers-ca-in` with experimental Indian recognizers; those were removed before 0.1.0 and will return as a separate `pii-detector-in` package once they have their own benchmark.)
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Canadian recognizers: `CA_SIN` (Luhn), `CA_OHIP` (Luhn + version code), `CA_RAMQ` (structural), `CA_BC_PHN` (mod-11), `CA_AB_ULI` (context gated), `CA_HEALTH_CARD` (generic, context gated), `CA_POSTAL_CODE`, `CA_STREET_ADDRESS`.
|
|
14
|
+
- `presidio-analyzer` is an optional extra (`pip install pii-detector-ca[presidio]`); the base install has no dependencies.
|
|
15
|
+
- Framework-neutral core (`pii_detector_ca.core`): stdlib-only validators, entity specs, and a `scan()` API that runs without Presidio or spaCy. Presidio recognizers are thin adapters over the same specs.
|
|
16
|
+
- `add_recognizers(registry, exclude=[...])` / `get_recognizers(exclude=[...])` to skip entities that Presidio's own (default-disabled) `CA_SIN` / `CA_POSTAL_CODE` recognizers already cover.
|
|
17
|
+
- Synthetic clinical-note benchmark: a seeded, deterministic generator with a determinism manifest, a false-positive negatives corpus, an adversarial hard split, and a street-address holdout derived from the StatCan Open Database of Addresses.
|
|
18
|
+
- Evaluation harness: leak-oriented and strict recall, bootstrap 95% CIs, three baselines (stock Presidio, Presidio with its CA built-ins enabled, Presidio + pack), and a CI threshold gate in a locked environment.
|
|
19
|
+
- Provenance docs for every identifier (`docs/identifiers/`), a core contract doc, and a non-technical user manual.
|
|
20
|
+
|
|
21
|
+
### Changed (pre-release)
|
|
22
|
+
|
|
23
|
+
- `CA_AB_ULI` now requires Alberta-specific context ("alberta", "uli", "ahcip", "netcare"). A 9-digit number under generic "health card" wording is reported as `CA_HEALTH_CARD` only; the generic recognizer gained separated-form patterns (5-4, 3-3-3, 4-3-3) so those renderings are still covered. ULI typed-exact recall on the benchmark is 0 by design (values are labelled under generic headers); leak recall stays 1.0.
|
|
24
|
+
- "card" is not a context token: it boosted credit-card chunks ("payment card on file") by +200 FPs/300 notes on the negatives corpus.
|
|
25
|
+
- `evaluation/run_eval.py` reports a third system, "Presidio + CA builtins", and `results_en_core_web_lg.*` are regenerated for the Canada-only corpus.
|
|
26
|
+
|
|
27
|
+
### Fixed (pre-release)
|
|
28
|
+
|
|
29
|
+
- `CA_OHIP`: the separated pattern was compiled case-insensitively, so a lowercase English word following the number ("… 897 on file") was absorbed into the span as a bogus version code at score 1.0. Version codes are now matched case-sensitively, and the validator enforces the MOH HCV schema's non-zero first digit.
|
|
30
|
+
- `CA_HEALTH_CARD` and the French context terms never fired through Presidio: its lemma context enhancer matches single tokens, and the context lists were multi-word phrases. Single-token context words ("health", "card", "maladie", "msp", "hsn", …) added.
|
|
31
|
+
- Core-scanner evaluation counted correct `CA_STREET_ADDRESS` / `CA_POSTAL_CODE` predictions as false positives because `ADDRESS` gold was filtered out; predictions are now type-mapped and `ADDRESS` kept.
|
|
32
|
+
- Benchmark realism: dates of birth were drawn from the encounter-date range (including future dates); RAMQ letter blocks did not match the patient named in the note; RAMQ birth years stopped at 1999.
|
|
33
|
+
|
|
34
|
+
[Unreleased]: https://github.com/thisiskarthikeyan/pii-detector-ca/compare/v0.1.0...HEAD
|
|
35
|
+
[0.1.0]: https://github.com/thisiskarthikeyan/pii-detector-ca/releases/tag/v0.1.0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it using the metadata below."
|
|
3
|
+
title: "pii-detector-ca: Checksum-validated recognizers and a synthetic benchmark for Canadian clinical de-identification"
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Murugaiah
|
|
7
|
+
given-names: Karthikeyan
|
|
8
|
+
# TODO: orcid before minting the Zenodo DOI.
|
|
9
|
+
email: blue1agave@gmail.com
|
|
10
|
+
alias: thisiskarthikeyan
|
|
11
|
+
version: 0.1.0
|
|
12
|
+
date-released: 2026-10-27
|
|
13
|
+
license: MIT
|
|
14
|
+
repository-code: "https://github.com/thisiskarthikeyan/pii-detector-ca"
|
|
15
|
+
url: "https://github.com/thisiskarthikeyan/pii-detector-ca"
|
|
16
|
+
abstract: >-
|
|
17
|
+
Checksum-validated detectors for Canadian health and government identifiers
|
|
18
|
+
(SIN, Ontario OHIP, Quebec RAMQ, BC PHN, Alberta ULI, postal codes, street
|
|
19
|
+
addresses), usable standalone or as Microsoft Presidio recognizers, with a
|
|
20
|
+
reproducible synthetic clinical-text benchmark, adversarial and
|
|
21
|
+
false-positive splits, and an evaluation harness.
|
|
22
|
+
keywords:
|
|
23
|
+
- de-identification
|
|
24
|
+
- PII
|
|
25
|
+
- PHI
|
|
26
|
+
- clinical NLP
|
|
27
|
+
- Presidio
|
|
28
|
+
- Canada
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping close the locale gap in clinical de-identification. This project's value is that every detection decision is **auditable and reproducible**, so contributions are held to that bar: sourced rules, deterministic tests, and no regressions on the evaluation gate.
|
|
4
|
+
|
|
5
|
+
## Ground rules
|
|
6
|
+
|
|
7
|
+
- **Never commit real patient, customer, or personal data.** That includes in tests, issues, PRs, and screenshots. Use synthetic values that pass the relevant checksum (the generators in `benchmark/pii_generators.py` produce them). `benchmark/data/real/` is gitignored for this reason.
|
|
8
|
+
- **Canada only.** Indian identifiers are planned as a separate package (`pii-detector-in`); PRs adding other jurisdictions to this package will be redirected.
|
|
9
|
+
- Missed-identifier (leak) findings with real-world impact go through [SECURITY.md](SECURITY.md), not public issues.
|
|
10
|
+
|
|
11
|
+
## Development setup
|
|
12
|
+
|
|
13
|
+
Requires Python ≥ 3.10 and [uv](https://docs.astral.sh/uv/).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv sync --extra dev # includes the presidio extra
|
|
17
|
+
uv pip install -r evaluation/requirements-eval.txt # pinned spaCy model, for evaluation
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Fast checks (run all three before opening a PR; CI runs them on 3.10 and 3.12):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv run ruff check .
|
|
24
|
+
uv run mypy
|
|
25
|
+
uv run pytest -q
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The full evaluation gate (CI's `eval` job) is the command sequence in the README's *Benchmark* section, ending with `python evaluation/check_thresholds.py`.
|
|
29
|
+
|
|
30
|
+
## Adding or changing an identifier
|
|
31
|
+
|
|
32
|
+
Work through these in order. A PR that skips a step will be asked to add it.
|
|
33
|
+
|
|
34
|
+
1. **Source the rule.** Find the authoritative format and checksum definition (government or issuing-authority publication). If no official source exists, say so explicitly.
|
|
35
|
+
2. **Validator.** Add a stdlib-only function to `src/pii_detector_ca/core/validators.py`. Checksumless identifiers must never *boost* a score on plausibility alone: return `False` to reject or `None` to defer to context, never `True` (see `tests/test_recognizers.py` for the regression tests that enforce this).
|
|
36
|
+
3. **Entity spec.** Add an `EntitySpec` in `core/specs.py` (patterns, base scores, context words, validator binding, provenance doc path). The core `scan()` API picks it up automatically. Context lists must include **single tokens**: Presidio's lemma context enhancer never matches a multi-word phrase.
|
|
37
|
+
4. **Presidio adapter.** Add a thin `SpecRecognizer` subclass in `canada/` and register it in `get_canada_recognizers()`.
|
|
38
|
+
5. **Tests.** Positive cases for every surface variant (compact / spaced / dashed), negatives for near-misses and checksum failures, and a property-based test where a checksum exists (`tests/test_properties.py`).
|
|
39
|
+
6. **Generator + template slot.** Add a `gen_*` function in `benchmark/pii_generators.py`, wire it into the slot filler in `benchmark/generate_corpus.py`, and place a `("SLOT", "<ENTITY>")` segment in the relevant templates in `benchmark/templates.py`. Generators must take the passed `random.Random` instance, never global `random`, or determinism breaks.
|
|
40
|
+
7. **Regenerate and re-pin.** Regenerate the corpora, then run `python benchmark/check_determinism.py --update` and commit the updated `benchmark/data/checksums.json` together with the generator change.
|
|
41
|
+
8. **Threshold.** Add a floor for the new entity in `evaluation/thresholds.json`. Checksum-backed identifiers are expected to hold 1.0 recall.
|
|
42
|
+
9. **Provenance doc.** Add `docs/identifiers/<entity>.md` (source, implemented rule, known gaps) and a row in `docs/identifiers/README.md`.
|
|
43
|
+
10. **Docs.** Update the entity table in `README.md` and `docs/user_manual.md`, and add a `CHANGELOG.md` entry.
|
|
44
|
+
|
|
45
|
+
## Changing evaluation results
|
|
46
|
+
|
|
47
|
+
Committed files under `evaluation/results/` back the paper's numbers (manuscript on the `paper` branch). If your change moves them, regenerate them in the locked environment (`uv sync --locked`), commit them in the same PR, and explain the delta in the PR description. Lowering a threshold needs a stated reason. "It failed" is not one.
|
|
48
|
+
|
|
49
|
+
## Pull requests
|
|
50
|
+
|
|
51
|
+
- Keep PRs focused; one identifier or one fix per PR.
|
|
52
|
+
- Describe *why*, and include before/after numbers when detection behavior changes.
|
|
53
|
+
- By contributing you agree your work is released under the project's MIT license.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Karthik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pii-detector-ca
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Checksum-validated detectors for Canadian health and government identifiers (SIN, OHIP, RAMQ, BC PHN, Alberta ULI, postal codes, street addresses): standalone or as Microsoft Presidio recognizers, with a synthetic clinical-text benchmark
|
|
5
|
+
Project-URL: Homepage, https://github.com/thisiskarthikeyan/pii-detector-ca
|
|
6
|
+
Project-URL: Repository, https://github.com/thisiskarthikeyan/pii-detector-ca
|
|
7
|
+
Project-URL: Issues, https://github.com/thisiskarthikeyan/pii-detector-ca/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/thisiskarthikeyan/pii-detector-ca/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Karthikeyan Murugaiah <blue1agave@gmail.com>
|
|
10
|
+
Maintainer-email: Karthikeyan Murugaiah <blue1agave@gmail.com>
|
|
11
|
+
License: MIT License
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2026 Karthik
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
|
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
|
23
|
+
copies or substantial portions of the Software.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Keywords: canada,clinical-nlp,de-identification,health-card,phi,pii,presidio,sin
|
|
34
|
+
Classifier: Development Status :: 3 - Alpha
|
|
35
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
36
|
+
Classifier: Intended Audience :: Science/Research
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Operating System :: OS Independent
|
|
39
|
+
Classifier: Programming Language :: Python :: 3
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
44
|
+
Requires-Python: >=3.10
|
|
45
|
+
Provides-Extra: benchmark
|
|
46
|
+
Requires-Dist: pandas>=2.0; extra == 'benchmark'
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
49
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
50
|
+
Requires-Dist: pandas>=2.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: presidio-analyzer>=2.2; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
53
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
54
|
+
Provides-Extra: presidio
|
|
55
|
+
Requires-Dist: presidio-analyzer>=2.2; extra == 'presidio'
|
|
56
|
+
Description-Content-Type: text/markdown
|
|
57
|
+
|
|
58
|
+
# pii-detector-ca
|
|
59
|
+
|
|
60
|
+
Checksum-validated detectors for **Canadian** health and government identifiers, usable standalone or as [Microsoft Presidio](https://microsoft.github.io/presidio/) recognizers, plus a synthetic clinical-text benchmark and evaluation harness.
|
|
61
|
+
|
|
62
|
+
Most clinical de-identification tooling is built and tested on US identifier formats. Presidio's default registry has no provincial health-card recognizers: an Ontario health number, a Quebec RAMQ number or a BC PHN passes through a stock de-identification pipeline silently. This package closes that gap with deterministic, checksum-backed recognizers that plug into any Presidio `AnalyzerEngine`, or run standalone with no Presidio/spaCy dependency at runtime.
|
|
63
|
+
|
|
64
|
+
> **Scope.** Canada only. An Indian identifier pack is planned as a separate package (`pii-detector-in`); it is not part of this one.
|
|
65
|
+
|
|
66
|
+
## Supported entities
|
|
67
|
+
|
|
68
|
+
| Entity | Identifier | Validation | Spec |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| `CA_SIN` | Social Insurance Number | Luhn checksum; leading digit not 0/8 | [doc](docs/identifiers/ca_sin.md) |
|
|
71
|
+
| `CA_OHIP` | Ontario health number (10 digits + version code) | Luhn (mod-10); leading digit 1–9 | [doc](docs/identifiers/ca_ohip.md) |
|
|
72
|
+
| `CA_RAMQ` | Quebec health insurance number (NAM) | Structural (name/DOB encoding) | [doc](docs/identifiers/ca_ramq.md) |
|
|
73
|
+
| `CA_BC_PHN` | British Columbia Personal Health Number | Mod-11 weighted checksum | [doc](docs/identifiers/ca_bc_phn.md) |
|
|
74
|
+
| `CA_AB_ULI` | Alberta Unique Lifetime Identifier | Pattern + context gated | [doc](docs/identifiers/ca_ab_uli.md) |
|
|
75
|
+
| `CA_HEALTH_CARD` | Other provinces/territories (generic) | Context gated only | [doc](docs/identifiers/ca_health_card_generic.md) |
|
|
76
|
+
| `CA_POSTAL_CODE` | Canadian postal code | Structural (letter-class rules) | [doc](docs/identifiers/ca_postal_code.md) |
|
|
77
|
+
| `CA_STREET_ADDRESS` | Full civic/mailing address | Structural (postal + province anchor) | [doc](docs/identifiers/ca_street_address.md) |
|
|
78
|
+
|
|
79
|
+
**Relationship to Presidio's own Canadian recognizers.** Presidio ≥ 2.2.363 ships a `CA_SIN` recognizer and ≥ 2.2.365 a `CA_POSTAL_CODE` recognizer, both **disabled by default** in `default_recognizers.yaml`. The six health-card and address entities above have no upstream equivalent. If you enable Presidio's built-ins, skip ours to avoid duplicate detections: `add_recognizers(registry, exclude=["CA_SIN", "CA_POSTAL_CODE"])`. The evaluation reports a "Presidio + CA builtins" baseline so the health-card contribution is measured against the strongest no-pack configuration.
|
|
80
|
+
|
|
81
|
+
## Install
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install pii-detector-ca # standalone scan(): no dependencies
|
|
85
|
+
pip install "pii-detector-ca[presidio]" # + Presidio recognizers
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The Presidio integration also needs a spaCy model: `python -m spacy download en_core_web_sm` (see the snippet below for why `sm` is configured explicitly). The standalone `scan()` API needs neither.
|
|
89
|
+
|
|
90
|
+
From source: `git clone https://github.com/thisiskarthikeyan/pii-detector-ca.git && cd pii-detector-ca && pip install -e ".[presidio]"`.
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
### Standalone detection (no Presidio required at runtime)
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from pii_detector_ca import scan
|
|
98
|
+
|
|
99
|
+
matches = scan("Patient health card 7577 752 616 AB, SIN 123-456-782.", threshold=0.35)
|
|
100
|
+
for hit in matches:
|
|
101
|
+
print(hit.entity, hit.start, hit.end, hit.text, hit.score)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`scan()` is pattern + validator only — it applies **no context-word boosting**. Checksum/structure-backed entities (`CA_SIN`, `CA_OHIP`, `CA_RAMQ`, `CA_BC_PHN`, `CA_POSTAL_CODE`, `CA_STREET_ADDRESS`) work fully; the context-gated `CA_AB_ULI` and `CA_HEALTH_CARD` never reach the default threshold standalone and need the Presidio integration. Entity rules live in `pii_detector_ca.core` (validators, specs, scanner); the Presidio recognizers are thin adapters over the same specs. See [docs/architecture/core_contract.md](docs/architecture/core_contract.md).
|
|
105
|
+
|
|
106
|
+
### Presidio integration
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from presidio_analyzer import AnalyzerEngine
|
|
110
|
+
from presidio_analyzer.nlp_engine import NlpEngineProvider
|
|
111
|
+
from pii_detector_ca import add_recognizers
|
|
112
|
+
|
|
113
|
+
# Same NLP engine as the published evaluation. A bare AnalyzerEngine()
|
|
114
|
+
# defaults to en_core_web_lg (~400 MB) and downloads it on first use.
|
|
115
|
+
nlp_engine = NlpEngineProvider(nlp_configuration={
|
|
116
|
+
"nlp_engine_name": "spacy",
|
|
117
|
+
"models": [{"lang_code": "en", "model_name": "en_core_web_sm"}],
|
|
118
|
+
}).create_engine()
|
|
119
|
+
|
|
120
|
+
analyzer = AnalyzerEngine(nlp_engine=nlp_engine, supported_languages=["en"])
|
|
121
|
+
add_recognizers(analyzer.registry)
|
|
122
|
+
|
|
123
|
+
results = analyzer.analyze(
|
|
124
|
+
text="Patient health card 7577 752 616 AB, SIN 123-456-782.",
|
|
125
|
+
language="en",
|
|
126
|
+
)
|
|
127
|
+
for r in results:
|
|
128
|
+
print(r.entity_type, r.start, r.end, round(r.score, 2))
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A non-technical walkthrough is in [docs/user_manual.md](docs/user_manual.md).
|
|
132
|
+
|
|
133
|
+
## What this is not
|
|
134
|
+
|
|
135
|
+
- **Not a compliance guarantee.** Using this package does not by itself satisfy PIPEDA, PHIPA, provincial health-information acts, or any other regulation. It is one detection layer; keep human review and your organization's privacy process in the loop.
|
|
136
|
+
- **Not a full de-identifier.** It detects identifiers. Person names, dates, and free-text clues come from Presidio's underlying NLP engine (spaCy), which this package does not improve. Most remaining misses on the benchmark are non-Anglophone person names.
|
|
137
|
+
- **Not validated on real clinical text.** All reported numbers come from a synthetic benchmark (below). Real notes carry OCR noise, dictation artifacts, and formats a template generator cannot anticipate.
|
|
138
|
+
- **Checksum-valid ≠ real, and the collision floor is not small.** About 8% of random 9-digit strings pass the SIN check and about 10% of random 10-digit strings pass the OHIP check, with no context required. Compact phone numbers, MRNs and accession numbers will be flagged at that rate. Checksumless types (Alberta ULI, generic health cards) rely on nearby context words instead.
|
|
139
|
+
- **French prose is out of scope.** French context words are implemented (and exercised in tests) but the benchmark is English-language clinical prose only.
|
|
140
|
+
|
|
141
|
+
## Benchmark
|
|
142
|
+
|
|
143
|
+
`benchmark/` contains a seeded, deterministic generator for synthetic clinical notes (six note types, from radiology reports to ED triage records) with span-labeled PII injection. It also has a false-positive negatives corpus, an adversarial hard split, and a street-address holdout built from the StatCan Open Database of Addresses. `evaluation/` compares stock Presidio, Presidio with its own Canadian built-ins enabled, and Presidio + this pack; results are committed under `evaluation/results/`.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
pip install -e ".[dev]" && pip install -r evaluation/requirements-eval.txt
|
|
147
|
+
python benchmark/generate_corpus.py --n 1000
|
|
148
|
+
python benchmark/generate_negatives.py --n 300
|
|
149
|
+
python benchmark/generate_hard.py --per-cell 10
|
|
150
|
+
python benchmark/generate_address_eval.py --n 300
|
|
151
|
+
python benchmark/check_determinism.py
|
|
152
|
+
python evaluation/run_eval.py --all
|
|
153
|
+
python evaluation/run_negatives.py
|
|
154
|
+
python evaluation/run_hard.py
|
|
155
|
+
python evaluation/run_address_eval.py
|
|
156
|
+
python evaluation/check_thresholds.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
CI runs this exact sequence in a locked environment (`uv.lock` + pinned spaCy model) and fails on any regression below `evaluation/thresholds.json`.
|
|
160
|
+
|
|
161
|
+
**Benchmark caveat:** the corpus is generated by the same project that wrote the recognizers, so in-distribution recall is partly circular. The robust findings are the stock-Presidio gap and the results on the negatives, hard, and address-holdout splits. The "gold" split is the first 100 notes of the same generator stream, not an independently annotated set. Synthetic data only; no real patient data is used or included.
|
|
162
|
+
|
|
163
|
+
## Contributing, security, citation
|
|
164
|
+
|
|
165
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md): dev setup and the recipe for adding an identifier.
|
|
166
|
+
- [SECURITY.md](SECURITY.md): report missed identifiers (leaks) privately, never with real data.
|
|
167
|
+
- [CITATION.cff](CITATION.cff): how to cite this software.
|
|
168
|
+
- [CHANGELOG.md](CHANGELOG.md).
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT. Benchmark data derived from Statistics Canada sources is used under the Open Government Licence – Canada; see the READMEs in `benchmark/data/oda/` and `benchmark/data/statcan_names/`.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# pii-detector-ca
|
|
2
|
+
|
|
3
|
+
Checksum-validated detectors for **Canadian** health and government identifiers, usable standalone or as [Microsoft Presidio](https://microsoft.github.io/presidio/) recognizers, plus a synthetic clinical-text benchmark and evaluation harness.
|
|
4
|
+
|
|
5
|
+
Most clinical de-identification tooling is built and tested on US identifier formats. Presidio's default registry has no provincial health-card recognizers: an Ontario health number, a Quebec RAMQ number or a BC PHN passes through a stock de-identification pipeline silently. This package closes that gap with deterministic, checksum-backed recognizers that plug into any Presidio `AnalyzerEngine`, or run standalone with no Presidio/spaCy dependency at runtime.
|
|
6
|
+
|
|
7
|
+
> **Scope.** Canada only. An Indian identifier pack is planned as a separate package (`pii-detector-in`); it is not part of this one.
|
|
8
|
+
|
|
9
|
+
## Supported entities
|
|
10
|
+
|
|
11
|
+
| Entity | Identifier | Validation | Spec |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| `CA_SIN` | Social Insurance Number | Luhn checksum; leading digit not 0/8 | [doc](docs/identifiers/ca_sin.md) |
|
|
14
|
+
| `CA_OHIP` | Ontario health number (10 digits + version code) | Luhn (mod-10); leading digit 1–9 | [doc](docs/identifiers/ca_ohip.md) |
|
|
15
|
+
| `CA_RAMQ` | Quebec health insurance number (NAM) | Structural (name/DOB encoding) | [doc](docs/identifiers/ca_ramq.md) |
|
|
16
|
+
| `CA_BC_PHN` | British Columbia Personal Health Number | Mod-11 weighted checksum | [doc](docs/identifiers/ca_bc_phn.md) |
|
|
17
|
+
| `CA_AB_ULI` | Alberta Unique Lifetime Identifier | Pattern + context gated | [doc](docs/identifiers/ca_ab_uli.md) |
|
|
18
|
+
| `CA_HEALTH_CARD` | Other provinces/territories (generic) | Context gated only | [doc](docs/identifiers/ca_health_card_generic.md) |
|
|
19
|
+
| `CA_POSTAL_CODE` | Canadian postal code | Structural (letter-class rules) | [doc](docs/identifiers/ca_postal_code.md) |
|
|
20
|
+
| `CA_STREET_ADDRESS` | Full civic/mailing address | Structural (postal + province anchor) | [doc](docs/identifiers/ca_street_address.md) |
|
|
21
|
+
|
|
22
|
+
**Relationship to Presidio's own Canadian recognizers.** Presidio ≥ 2.2.363 ships a `CA_SIN` recognizer and ≥ 2.2.365 a `CA_POSTAL_CODE` recognizer, both **disabled by default** in `default_recognizers.yaml`. The six health-card and address entities above have no upstream equivalent. If you enable Presidio's built-ins, skip ours to avoid duplicate detections: `add_recognizers(registry, exclude=["CA_SIN", "CA_POSTAL_CODE"])`. The evaluation reports a "Presidio + CA builtins" baseline so the health-card contribution is measured against the strongest no-pack configuration.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install pii-detector-ca # standalone scan(): no dependencies
|
|
28
|
+
pip install "pii-detector-ca[presidio]" # + Presidio recognizers
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The Presidio integration also needs a spaCy model: `python -m spacy download en_core_web_sm` (see the snippet below for why `sm` is configured explicitly). The standalone `scan()` API needs neither.
|
|
32
|
+
|
|
33
|
+
From source: `git clone https://github.com/thisiskarthikeyan/pii-detector-ca.git && cd pii-detector-ca && pip install -e ".[presidio]"`.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Standalone detection (no Presidio required at runtime)
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pii_detector_ca import scan
|
|
41
|
+
|
|
42
|
+
matches = scan("Patient health card 7577 752 616 AB, SIN 123-456-782.", threshold=0.35)
|
|
43
|
+
for hit in matches:
|
|
44
|
+
print(hit.entity, hit.start, hit.end, hit.text, hit.score)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`scan()` is pattern + validator only — it applies **no context-word boosting**. Checksum/structure-backed entities (`CA_SIN`, `CA_OHIP`, `CA_RAMQ`, `CA_BC_PHN`, `CA_POSTAL_CODE`, `CA_STREET_ADDRESS`) work fully; the context-gated `CA_AB_ULI` and `CA_HEALTH_CARD` never reach the default threshold standalone and need the Presidio integration. Entity rules live in `pii_detector_ca.core` (validators, specs, scanner); the Presidio recognizers are thin adapters over the same specs. See [docs/architecture/core_contract.md](docs/architecture/core_contract.md).
|
|
48
|
+
|
|
49
|
+
### Presidio integration
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from presidio_analyzer import AnalyzerEngine
|
|
53
|
+
from presidio_analyzer.nlp_engine import NlpEngineProvider
|
|
54
|
+
from pii_detector_ca import add_recognizers
|
|
55
|
+
|
|
56
|
+
# Same NLP engine as the published evaluation. A bare AnalyzerEngine()
|
|
57
|
+
# defaults to en_core_web_lg (~400 MB) and downloads it on first use.
|
|
58
|
+
nlp_engine = NlpEngineProvider(nlp_configuration={
|
|
59
|
+
"nlp_engine_name": "spacy",
|
|
60
|
+
"models": [{"lang_code": "en", "model_name": "en_core_web_sm"}],
|
|
61
|
+
}).create_engine()
|
|
62
|
+
|
|
63
|
+
analyzer = AnalyzerEngine(nlp_engine=nlp_engine, supported_languages=["en"])
|
|
64
|
+
add_recognizers(analyzer.registry)
|
|
65
|
+
|
|
66
|
+
results = analyzer.analyze(
|
|
67
|
+
text="Patient health card 7577 752 616 AB, SIN 123-456-782.",
|
|
68
|
+
language="en",
|
|
69
|
+
)
|
|
70
|
+
for r in results:
|
|
71
|
+
print(r.entity_type, r.start, r.end, round(r.score, 2))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A non-technical walkthrough is in [docs/user_manual.md](docs/user_manual.md).
|
|
75
|
+
|
|
76
|
+
## What this is not
|
|
77
|
+
|
|
78
|
+
- **Not a compliance guarantee.** Using this package does not by itself satisfy PIPEDA, PHIPA, provincial health-information acts, or any other regulation. It is one detection layer; keep human review and your organization's privacy process in the loop.
|
|
79
|
+
- **Not a full de-identifier.** It detects identifiers. Person names, dates, and free-text clues come from Presidio's underlying NLP engine (spaCy), which this package does not improve. Most remaining misses on the benchmark are non-Anglophone person names.
|
|
80
|
+
- **Not validated on real clinical text.** All reported numbers come from a synthetic benchmark (below). Real notes carry OCR noise, dictation artifacts, and formats a template generator cannot anticipate.
|
|
81
|
+
- **Checksum-valid ≠ real, and the collision floor is not small.** About 8% of random 9-digit strings pass the SIN check and about 10% of random 10-digit strings pass the OHIP check, with no context required. Compact phone numbers, MRNs and accession numbers will be flagged at that rate. Checksumless types (Alberta ULI, generic health cards) rely on nearby context words instead.
|
|
82
|
+
- **French prose is out of scope.** French context words are implemented (and exercised in tests) but the benchmark is English-language clinical prose only.
|
|
83
|
+
|
|
84
|
+
## Benchmark
|
|
85
|
+
|
|
86
|
+
`benchmark/` contains a seeded, deterministic generator for synthetic clinical notes (six note types, from radiology reports to ED triage records) with span-labeled PII injection. It also has a false-positive negatives corpus, an adversarial hard split, and a street-address holdout built from the StatCan Open Database of Addresses. `evaluation/` compares stock Presidio, Presidio with its own Canadian built-ins enabled, and Presidio + this pack; results are committed under `evaluation/results/`.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install -e ".[dev]" && pip install -r evaluation/requirements-eval.txt
|
|
90
|
+
python benchmark/generate_corpus.py --n 1000
|
|
91
|
+
python benchmark/generate_negatives.py --n 300
|
|
92
|
+
python benchmark/generate_hard.py --per-cell 10
|
|
93
|
+
python benchmark/generate_address_eval.py --n 300
|
|
94
|
+
python benchmark/check_determinism.py
|
|
95
|
+
python evaluation/run_eval.py --all
|
|
96
|
+
python evaluation/run_negatives.py
|
|
97
|
+
python evaluation/run_hard.py
|
|
98
|
+
python evaluation/run_address_eval.py
|
|
99
|
+
python evaluation/check_thresholds.py
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
CI runs this exact sequence in a locked environment (`uv.lock` + pinned spaCy model) and fails on any regression below `evaluation/thresholds.json`.
|
|
103
|
+
|
|
104
|
+
**Benchmark caveat:** the corpus is generated by the same project that wrote the recognizers, so in-distribution recall is partly circular. The robust findings are the stock-Presidio gap and the results on the negatives, hard, and address-holdout splits. The "gold" split is the first 100 notes of the same generator stream, not an independently annotated set. Synthetic data only; no real patient data is used or included.
|
|
105
|
+
|
|
106
|
+
## Contributing, security, citation
|
|
107
|
+
|
|
108
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md): dev setup and the recipe for adding an identifier.
|
|
109
|
+
- [SECURITY.md](SECURITY.md): report missed identifiers (leaks) privately, never with real data.
|
|
110
|
+
- [CITATION.cff](CITATION.cff): how to cite this software.
|
|
111
|
+
- [CHANGELOG.md](CHANGELOG.md).
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT. Benchmark data derived from Statistics Canada sources is used under the Open Government Licence – Canada; see the READMEs in `benchmark/data/oda/` and `benchmark/data/statcan_names/`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
This package sits on a privacy boundary: a missed identifier is a data leak. Please report such findings privately so they can be fixed before they are public.
|
|
4
|
+
|
|
5
|
+
## What to report privately
|
|
6
|
+
|
|
7
|
+
- **Leaks (false negatives):** a real-world identifier format that the recognizers fail to detect, especially a *class* of misses (a formatting variant, a province's card layout, a separator style).
|
|
8
|
+
- **Validation bypasses:** inputs that defeat a checksum or context gate in a way that hides identifiers.
|
|
9
|
+
- **Unsafe handling:** any code path that logs, persists, or transmits input text.
|
|
10
|
+
|
|
11
|
+
Ordinary false positives, documentation errors, and feature requests are fine as public GitHub issues.
|
|
12
|
+
|
|
13
|
+
## How to report
|
|
14
|
+
|
|
15
|
+
Use GitHub's private vulnerability reporting: open the repository's **Security** tab, then click **Report a vulnerability**.
|
|
16
|
+
|
|
17
|
+
**Never include real personal or patient data in a report.** Reproduce the issue with a synthetic value of the same shape (`benchmark/pii_generators.py` generates checksum-valid ones), or describe the format with digits replaced by `X`.
|
|
18
|
+
|
|
19
|
+
You can expect an acknowledgement within 7 days. Confirmed leaks get a fix, a regression test, and a CHANGELOG entry crediting you (unless you prefer otherwise).
|
|
20
|
+
|
|
21
|
+
## Supported versions
|
|
22
|
+
|
|
23
|
+
| Version | Supported |
|
|
24
|
+
|---|---|
|
|
25
|
+
| 0.1.x | Yes |
|
|
26
|
+
|
|
27
|
+
## Scope reminder
|
|
28
|
+
|
|
29
|
+
This project is a detection aid, not a compliance guarantee. See *What this is not* in the [README](README.md).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Verify the benchmark corpus regenerates bit-identically from the fixed seed.
|
|
2
|
+
|
|
3
|
+
The reproducibility claim of this project depends on the generator being
|
|
4
|
+
deterministic across environments. This script hashes the corpus JSONL files
|
|
5
|
+
and compares them against the committed manifest, so any drift (stdlib RNG
|
|
6
|
+
behavior change, accidental generator edit without regeneration, template
|
|
7
|
+
change) fails CI immediately.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python benchmark/check_determinism.py # verify against manifest
|
|
11
|
+
python benchmark/check_determinism.py --update # rewrite the manifest
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
DATA_DIR = Path(__file__).parent / "data"
|
|
23
|
+
MANIFEST = DATA_DIR / "checksums.json"
|
|
24
|
+
|
|
25
|
+
CORPUS_FILES = [
|
|
26
|
+
"gold/ca_gold.jsonl",
|
|
27
|
+
"generated/ca.jsonl",
|
|
28
|
+
"negatives/ca_negatives.jsonl",
|
|
29
|
+
"hard/ca_hard.jsonl",
|
|
30
|
+
"addresses/ca_address_eval.jsonl",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def sha256_of(path: Path) -> str:
|
|
35
|
+
h = hashlib.sha256()
|
|
36
|
+
with open(path, "rb") as f:
|
|
37
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
38
|
+
h.update(chunk)
|
|
39
|
+
return h.hexdigest()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def compute_checksums() -> dict[str, str]:
|
|
43
|
+
checksums = {}
|
|
44
|
+
for rel in CORPUS_FILES:
|
|
45
|
+
path = DATA_DIR / rel
|
|
46
|
+
if not path.exists():
|
|
47
|
+
sys.exit(f"ERROR: {path} missing — run the benchmark generators first.")
|
|
48
|
+
checksums[rel] = sha256_of(path)
|
|
49
|
+
return checksums
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main() -> None:
|
|
53
|
+
parser = argparse.ArgumentParser()
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--update", action="store_true", help="rewrite the manifest from current files"
|
|
56
|
+
)
|
|
57
|
+
args = parser.parse_args()
|
|
58
|
+
|
|
59
|
+
current = compute_checksums()
|
|
60
|
+
|
|
61
|
+
if args.update:
|
|
62
|
+
MANIFEST.write_text(json.dumps(current, indent=2) + "\n", encoding="utf-8")
|
|
63
|
+
print(f"Wrote {MANIFEST}")
|
|
64
|
+
for rel, digest in current.items():
|
|
65
|
+
print(f" {digest} {rel}")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if not MANIFEST.exists():
|
|
69
|
+
sys.exit(f"ERROR: manifest {MANIFEST} missing — run with --update to create it.")
|
|
70
|
+
|
|
71
|
+
expected = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
72
|
+
failures = []
|
|
73
|
+
for rel in CORPUS_FILES:
|
|
74
|
+
exp = expected.get(rel)
|
|
75
|
+
got = current[rel]
|
|
76
|
+
status = "OK" if exp == got else "MISMATCH"
|
|
77
|
+
if exp != got:
|
|
78
|
+
failures.append(rel)
|
|
79
|
+
print(f"{status:9s} {rel}\n expected {exp}\n got {got}")
|
|
80
|
+
|
|
81
|
+
if failures:
|
|
82
|
+
sys.exit(
|
|
83
|
+
f"\nDeterminism check FAILED for: {', '.join(failures)}.\n"
|
|
84
|
+
"If the generator changed intentionally, regenerate the corpus, re-run the "
|
|
85
|
+
"evaluation, and update the manifest with --update."
|
|
86
|
+
)
|
|
87
|
+
print("\nDeterminism check passed: corpus regenerates bit-identically.")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
main()
|