PIIcorpus is a detector-neutral Python tool for generating, auditing, and scoring against deterministic synthetic contextual-PII corpora without using real personal data.
Its focus is structural quality, not volume:
PIIcorpus does not merely generate synthetic examples. It checks whether its generated splits are structurally independent, exposes common synthetic-corpus failure modes — contamination, template memorization, morphology-to-label and shape-to-entity shortcuts, cue shortcuts, within-split redundancy, insufficient diversity, and generator fingerprints — and tests for learnable surface signal with a class-balanced trivial-model probe. The same audit runs on external NER datasets, and a scoring harness turns detector predictions into mechanism diagnostics.
The project generates, audits, and scores against corpora. It does not train, evaluate, select, approve, or package machine-learning models.
flowchart LR
A["TOML configuration"] --> B["piicorpus generate"]
B --> C["Corpus files + manifest"]
C --> D{"piicorpus validate"}
D -->|"valid"| E["piicorpus audit [--probe]"]
D -->|"invalid"| X["Fail with findings"]
E --> F["piicorpus export"]
F --> G["JSONL · BIO · Hugging Face · spaCy · Presidio"]
G --> H["your detector"]
H --> I["piicorpus score"]
C -.-> R["piicorpus reproduce"]
J["any NER dataset"] --> K["piicorpus audit-external"]
Generation is deterministic for a fixed package version, normalized configuration, and seed —
piicorpus reproduce verifies it by regenerating from the corpus's own configuration snapshot and
byte-comparing. Validation recalculates corpus invariants before audit, export, or scoring
consumes the files.
Python 3.11 or newer is required.
python -m venv .venv
python -m pip install -e .
piicorpus generate --config configs/demo.toml --out demo-output
piicorpus validate demo-output
piicorpus audit demo-output --probe
piicorpus export demo-output --format huggingface
piicorpus reproduce demo-outputTwo configurations ship with the project:
configs/demo.toml— the fictional SYN- demo. LabelsPATIENT_RECORD_ID,TRAVEL_DOCUMENT_ID,DRIVER_CREDENTIAL_ID, andBIRTH_DATEuse intentionally fictional identifier shapes with a synthetic prefix. The audit deliberately reports the constant prefix as aWARN(value_shared_affix) — safe values, visible trade-off.configs/realistic-safe.toml— realistic-but-reserved surfaces: RFC 2606 emails, 555-01XX phone numbers, Luhn-invalid card shapes, RFC 5737 documentation IPs, and never-issued 9XX-XX-XXXX national-id shapes. Safety runs in verifier mode (each plugin proves its values are reserved), so detector regexes get realistic shapes with no synthetic prefix and no affix warning. Seedocs/DATA_SAFETY.mdfor the reservation rationale.
The manifest records the seed, generator version, normalized configuration digest, counts, file hashes, determinism metadata, and the CC0-1.0 generated-data license.
demo-output/
corpus-config.json
manifest.json
splits/
train.jsonl
eval.jsonl
holdout.jsonl
Running the same package version with the same configuration and seed produces byte-identical files. A different seed changes generated records while keeping configured sizes, ratios, safety rules, and diversity requirements intact. Splits draw personas, organizations, letters, and years from shared pools partitioned by interleaving, so they stay disjoint without the obvious order-driven shift a contiguous alphabetical or chronological split would create. Arbitrary user-supplied pools are not claimed to be statistically identical across splits.
The repository forces LF line endings for JSON and JSONL files through .gitattributes, including
on Git for Windows checkouts, so checkout conversion does not invalidate corpus byte hashes.
The validator recalculates invariants from emitted files. It does not accept manifest counts as proof. If a split is changed after generation, validation exits with code 1:
FAIL: 2 validation finding(s)
- file_hash: SHA-256 mismatch for splits/eval.jsonl
- manifest_counts: manifest counts for eval do not match emitted records
Operational errors, such as a missing file or invalid TOML, exit with code 2 and are never presented
as a clean corpus verdict (--traceback re-raises them for debugging).
Audit, export, and score all run strict validation before consuming a corpus. Tampered records,
stale content-derived IDs, or inconsistent manifests exit with findings and produce no output by
default. --forensic-allow-invalid is available for investigation, but it is deliberately
failure-preserving and cannot produce a clean audit.
PASS shape_entity_shortcut count=0 measured=0.8161 threshold=0.9
PASS cue_label_shortcuts count=59 measured=0.3151 threshold=0.45
PASS intra_split_redundancy count=0 measured=0.0 threshold=0.05
WARN value_shared_affix count=4 measured=11 threshold=6
PASS probe_kind_separability count=2 measured=0.8554 threshold=0.9
PASS threshold_strictness count=0
UNMEASURED same_generator_holdout_dependence
Every risk is reported as PASS, FAIL, WARN, or UNMEASURED with a count, the measured
value, the threshold it was judged against, and a reason. The threshold_strictness finding warns
whenever the corpus configuration is laxer than the recommended reference profile, and
--profile reference runs the audit with the reference thresholds directly. JSON and Markdown
output are available for automation and review. The full risk catalog is in
docs/FAILURE_MODEL.md.
--probe additionally trains a deterministic stdlib trivial model (hashed character n-grams and
logistic regression) and reports balanced accuracy, macro-F1, raw accuracy, and split-specific
baselines for kind, value-to-label, and context-to-label prediction. A failure requires balanced
accuracy above both the configured ceiling and the majority-predictor baseline margin.
A holdout produced by the same generator is useful for regression testing but is not an independent generalization test.
The structural checks that need no generator metadata — contamination, duplicates, redundancy, shape and marker shortcuts, value diversity, span integrity, and the probe — run on any NER dataset:
piicorpus audit-external --format hf --split train=train.jsonl --split test=test.jsonl
piicorpus audit-external data.conll --format conll
piicorpus audit-external records.jsonl --format jsonl # import output is directly consumableChecks that require the generating configuration report UNMEASURED, and a sensitive-content
scan over the text is reported as a warning (--fail-on-safety promotes it).
piicorpus score consumes span predictions from any detector (see
docs/FORMAT.md for the one-line-per-record format) and reports
precision/recall/F1 per label, family, kind, and split — plus mechanism diagnostics built from
the engineered families:
diagnostics:
cue_dependence 0.04 (cued recall minus cue-free recall)
conflict_gold_recall 1.0
shape_hint_substitution_rate 0.0
other_error_rate 0.0
abstention_rate 0.0
spoken_recall 0.0
over_trigger_per_hard_negative_family {'hard_negative_near_misses': 1.0, ...}
A shape-only regex detector will over-trigger on near-miss hard negatives — which now carry
identifier-shaped values precisely so that this failure is measurable. Scores on synthetic data
demonstrate mechanism failures, never real-world adequacy; see
docs/CLAIM_BOUNDARIES.md.
The demo covers narrative prose, structural records, OCR-like noise, spoken values, mixed-entity documents, cue-free positives, cue-versus-shape conflicts, near misses, placeholders, negation, documentation references, unrelated identifier-shaped values, and adjacent non-sensitive values. Near-miss and adjacent negatives generate their values through the configured label plugins, so hard negatives mirror the positive value distribution for any label set.
Configuration controls labels, cue surfaces, value plugins, family plugins, persona and
organization pools ([surfaces]), split sizes, class balance, diversity floors, audit thresholds,
probe ceilings, and safety rules. Applications register extensions with register_value_plugin,
register_family, register_shape, and register_value_verifier; no built-in label set is
required by the engine. The CLI loads registration modules with --plugins mymodule or
automatically through the piicorpus.plugins entry-point group:
[project.entry-points."piicorpus.plugins"]
acme = "acme_pii:register"Human-readable markup uses [[ENTITY_TYPE:value]]:
The record identifier is [[PATIENT_RECORD_ID:SYN-ID-A10427]].
The parser emits clean text, Unicode code-point offsets, and UTF-8 byte offsets. Nested, unclosed,
malformed, or overlapping annotations fail loudly. Exporters are provided for generic JSONL, BIO,
Hugging Face-compatible per-split JSONL, a spaCy-convertible JSONL form, and Presidio fixtures;
every export includes a labels.json tag map. Details are in docs/FORMAT.md.
Imported marked text remains human_supplied and unassigned; unannotated lines are typed
unannotated, not hard negatives. Import scans text for sensitive-content patterns and reports
findings in its manifest, but import does not establish consent, privacy, provenance, licensing,
safety, or release suitability, and it never mixes records into generated splits without a
separate explicit process.
config.pyparses and normalizes TOML, including surfaces, probe, and safety modes.generator.py,skeletons.py, andmorphology.pyimplement deterministic plugin registries (values, families, shapes) and thirty-template-per-family banks.plugins_realistic.pyprovides reserved-surface value plugins with reservedness verifiers.annotation.pyowns marked-text parsing and span round trips.validators/derives structural, diversity, hash, and safety verdicts from output files.failure_model.pyruns the registered audit checks;similarity.pyprovides MinHash/LSH near-duplicate detection;probe.pyis the trivial-model learnability probe;profiles.pyholds the reference threshold profile.scoring.pycompares detector predictions against a corpus.importers/(annotated and external) andexporters/keep provenance and spans explicit.
The base installation has no third-party runtime dependency.
- Synthetic data does not prove real-world accuracy.
- A same-generator holdout is not independent.
- Diversity counts do not prove semantic diversity.
- Template variation can still leave a generator fingerprint.
- PIIcorpus does not guarantee that generated identifiers are realistic; the realistic-safe plugins guarantee only that realistic shapes are reserved, fictional, or invalid by design.
- PIIcorpus does not guarantee regulatory compliance.
- PIIcorpus does not de-identify real data.
- PIIcorpus does not make a model ready for deployment.
- Human-authored or externally sourced evaluation remains necessary.
- The project generates, audits, and scores against corpora; it does not train or approve models.
See docs/CLAIM_BOUNDARIES.md for the full boundary.
Install the reviewed development lock and the editable package, then run:
python -m pip install --requirement requirements-dev.lock
python -m pip install --no-deps --no-build-isolation -e .
ruff check .
mypy src
pytest
python -m buildContribution guidance is in CONTRIBUTING.md. Report vulnerabilities through
GitHub private vulnerability reporting,
not a public issue.
Source code is Apache-2.0. Generated demo data is CC0-1.0; see DATA_LICENSE. User-supplied and
external data retains its own terms.