Detectors
Detection has four layers, applied to every string as written and to its decoded view:
- Rules (detector A).
patterns.json: credential formats, labeled secrets, personal-data patterns with validators, header-driven column rules, name rules, plus the company rule (capitalized words before an unambiguous legal suffix). Always on; no model. - Ensemble (detectors B and C, language model A). Pinned models the reference runner loads and hands to
ensemble.pyas plain callables. Off when no runner loads them; then name model A is the name detector. - Owner identity (
blind.py): the deal's own terms (company, brands, domains, people) in every variant, when the control plane sends them. - After the scrub: the inline residual gate (
patterns.residual, the same rules over the bytes about to ship), the independent re-check (separate code, below), and the release gate's human review (measurement.md).
Rules
patterns.json 1.4.0.
| Tier | Rules | Ids |
|---|---|---|
| Credentials, provider formats | 28 | auditor edition |
| Credentials, generic | 5 | private_key, jwt, db_url, bearer, env_secret |
| Labeled secrets | 1 | labeled |
| Labeled personal data (needs a label word) | 13 | dob, card_cvv, card_expiry, person_id, vin_labeled, plate, routing, account, mrn, npi, gov_id, ein, vat |
| Personal data | 25 | email, uuid_id, iban, uk_nino, in_pan, vin, ssn, au_tfn, ca_sin, in_aadhaar, imei, card_track, card, phone, phone_intl, phone_fr, phone_uk, phone_au, phone_in, mac, ip6, ip4, address, geo, url_pii |
| Column rules (by header) | 21 | account, address, age, device_id, dob, email, geo, id, imei, ip, mac, mrn, name, npi, org, phone, plate, ssn, username, vin, zip |
| Name rules | 3 | greeting, greet_keep, hint |
Free-mail domains (kept as freemail) |
18 | – |
| Reserved TLDs (never personal) | 4 | example, invalid, localhost, test |
Restricted ZIP3 (become 000) |
17 | 036, 059, 063, 102, 203, 556, 692, 790, 821, 823, 830, 831, 878, 879, 884, 890, 893 |
| Lexicon (flagged, not redacted) | 4 | slur, harassment, threat, self_harm |
| Rule | Kind | Validator (checks.py) | Needs a nearby label | Stand-in class |
|---|---|---|---|---|
dob |
dob | – | – | dob |
card_cvv |
card_cvv | – | – | secret |
card_expiry |
card_expiry | – | – | stand_in |
person_id |
id | has_digit |
– | stand_in |
vin_labeled |
vin | has_digit |
– | stand_in |
plate |
plate | plate |
– | stand_in |
routing |
routing | aba |
– | stand_in |
account |
account | – | – | stand_in |
mrn |
mrn | has_digit |
– | stand_in |
npi |
npi | npi |
– | stand_in |
gov_id |
gov_id | has_digit |
– | stand_in |
ein |
ein | – | – | stand_in |
vat |
vat | vat |
– | stand_in |
email |
personal_email |
– | ||
uuid_id |
id | uuid |
yes | stand_in |
iban |
iban | iban |
– | stand_in |
uk_nino |
uk_nino | – | – | stand_in |
in_pan |
in_pan | – | – | stand_in |
vin |
vin | vin |
– | stand_in |
ssn |
ssn | – | – | stand_in |
au_tfn |
au_tfn | au_tfn |
yes | stand_in |
ca_sin |
ca_sin | ca_sin |
yes | stand_in |
in_aadhaar |
in_aadhaar | in_aadhaar |
yes | stand_in |
imei |
imei | imei |
yes | stand_in |
card_track |
card_track | – | – | secret |
card |
card | card |
– | stand_in |
phone |
phone | – | – | stand_in |
phone_intl |
phone | phone_intl |
– | stand_in |
phone_fr |
phone | – | – | stand_in |
phone_uk |
phone | – | – | stand_in |
phone_au |
phone | – | – | stand_in |
phone_in |
phone | – | – | stand_in |
mac |
mac | mac |
– | stand_in |
ip6 |
ip | ip6 |
– | stand_in |
ip4 |
ip | – | – | stand_in |
address |
address | – | – | stand_in |
geo |
geo | geo |
– | stand_in |
url_pii |
url_pii | url_param |
– | url_param |
Validators are checksums and structural tests (checks.py: Luhn for cards, IMEI and Canadian SIN; ISO 7064
mod 97 for IBANs; ABA routing; Verhoeff for Aadhaar; TFN weights; German and Dutch VAT; NPI and VIN check digits;
IPv6, MAC, UUID, coordinate and plate shape) plus a few in patterns.py (personal email, URL parameters that name
a person, international phone, has-a-digit for labeled ids). A pattern hit whose validator fails isn't redacted.
Ensemble logic
ensemble.merge, for every text a detector covers (the language model routes each block; a detector with a
language list runs only on those languages; name model A reads English only):
- Union. A hit at or over its kind's threshold for that detector is redacted, whichever detector found it.
Kinds a detector reports without a threshold in
thresholds.jsonare ignored. - Merge. Overlapping kept hits become one span covering all of them. Its kind is the one first in
precedence(a checksummed card beats a name), its score the best score for that kind,bythe detectors in it. - Disputed. A kept span that another detector which ran and covers that kind didn't find. Its stand-in goes into the watch set, and the output sampler puts windows showing it first in the desk's review queue.
- Near. A hit at or over its detector's review floor but under its threshold, overlapping no kept span: not redacted, sent to desk review with its context.
- Only adds. Rule hits are redacted on their own path; the ensemble never removes one.
- Blocks. Short texts are joined into blocks and long ones cut into overlapping windows, so one model call covers many lines. Results are cached per string.
- Columns. For a column whose header says nothing, a sample of its values goes through the detectors; a kind that wins enough of the sample claims the column.
- Failure. A detector exception is counted under its alias (never with its message). Under a profile with no
fallback (strict) it raises, and the runner holds the file (
detector_failed). Otherwise the other detectors' results stand and the report says which detector failed. - Third-party companies. Kind
org. The profile decides: tokenize (strict, standard) or keep (b2b). The seller's own names are never third-party; on owner-identity deals they are the owner pass's job.
thresholds.json 1.0.0.
- Precedence when overlapping hits merge (first wins the kind):
secret,card,iban,ssn,uk_nino,ca_sin,au_tfn,in_aadhaar,in_pan,gov_id,mrn,routing,account,email,phone,ip,dob,address,username,name,org - Kinds the package's own rules (detector A) report:
secret,card,iban,ssn,uk_nino,ca_sin,au_tfn,in_aadhaar,in_pan,gov_id,mrn,routing,account,email,phone,ip,dob,address,username - Default countries for country-filtered recognizers:
us,uk,ca,au,in - Languages routed to the multilingual detectors:
en,de,fr,es,it,pt,nl - Block size: 1500 characters per model call, 120 characters of overlap
- Column inference: 12 sampled values per column, at least 3, a kind wins the column at a 0.6 share; kinds
name,account,org,address,phone,email,gov_id,iban,card - Watch set cap 20,000 stand-ins; near-miss review items kept per run: 40; context shown: 60 characters
| Alias | Name in reports | Kind | Threshold | Review floor |
|---|---|---|---|---|
detector_b |
detector B | phone | 0.6 | 0.3 |
detector_b |
detector B | card | 0.9 | 0.3 |
detector_b |
detector B | iban | 0.9 | 0.3 |
detector_b |
detector B | ip | 0.9 | 0.3 |
detector_b |
detector B | ssn | 0.6 | 0.3 |
detector_b |
detector B | uk_nino | 0.6 | 0.3 |
detector_b |
detector B | gov_id | 0.75 | 0.3 |
detector_b |
detector B | account | 0.6 | 0.3 |
detector_b |
detector B | routing | 0.6 | 0.3 |
detector_b |
detector B | mrn | 0.6 | 0.3 |
detector_b |
detector B | ca_sin | 0.6 | 0.3 |
detector_b |
detector B | au_tfn | 0.6 | 0.3 |
detector_b |
detector B | in_aadhaar | 0.6 | 0.3 |
detector_b |
detector B | in_pan | 0.6 | 0.3 |
detector_c |
detector C | name | 0.2 | 0.15 |
detector_c |
detector C | org | 0.8 | 0.15 |
detector_c |
detector C | address | 0.6 | 0.15 |
detector_c |
detector C | account | 0.8 | 0.15 |
detector_c |
detector C | gov_id | 0.7 | 0.15 |
ner-a |
name model A | name | 0 | – |
detector_a |
detector A | org | 0 | – |
| Alias | What it is (thresholds.json) |
|---|---|
detector_b |
recognizer engine: pattern and checksum recognizers filtered by the deal's countries, phone numbers, context words |
detector_c |
multilingual span model: names, organizations, addresses, account and government ids |
ner-a |
English named-entity model (the fallback name detector), English text only |
detector_a |
the package's own company rule: capitalized words before an unambiguous legal suffix |
Models
Reports and these pages name models by alias. The tree hash is the one every report carries per alias in
engine.models and models[]: the sha256 the reference runner computes over the installed files (for packages)
or the weights directory (for weights), checked again before any data moves.
| Alias | Name in reports | Status | Kinds (thresholds.json) | License | Tree hash (sha256) |
|---|---|---|---|---|---|
ner-a |
name model A | baked | name |
MIT | 8e12e3a14b354aa52c1d047adb1a96277484c2a66282dc225e46e8015d5439ba |
detector_b |
detector B | baked, ensemble member | account, au_tfn, ca_sin, card, gov_id, iban, in_aadhaar, in_pan, ip, mrn, phone, routing, ssn, uk_nino |
MIT | 742c544d91acbd3627fbad52e094d1e4b295ca2a4c68de28bd33406b7a5fd447 |
detector_c |
detector C | baked, ensemble member | account, address, gov_id, name, org |
Apache-2.0 (plus a component license: auditor edition) | 8ba9ef935b7656def6f25512d23cb9f4b5948cb3db28085f0c24ce453499b991 |
langid-a |
language model A | baked, ensemble member | – | Apache-2.0 | ccd46ef066d17939eeef73fa0429bc29bc1d31b1a2f0d06f131e00a948d9f1d5 |
verifier-a |
verifier A | optional (never baked, never a hold) | – | Apache-2.0 | – |
verifier-b |
verifier B (reserved) | reserved (not used) | – | other (plus a component license: auditor edition) | – |
asr |
speech model | baked (audio and video) | speech | MIT | – |
face_detector |
face detector | baked (images and video) | face | MIT | – |
Lock-file commitments (sha256 of the file bytes at this version; the auditor edition prints the files):
| Lock file | sha256 |
|---|---|
models.lock.json |
a9dc648c5904099d9ab9e40aa06af4f2b610864a76e0d1eb4492667532f751d1 |
models/av.lock.json |
fd1d3d2cba92daf4bb843188356bd65bf37611511b7aaed5a50bb25f454a236c |
models/media.lock.json |
0cebeabf76d249fa5272cde0e72e5033497677e559b18aff6dc6eaec35031da5 |
What each one is, in plain terms:
- Detector A: the package's own rules and company rule. No model.
- Detector B: a recognizer engine of pattern and checksum recognizers with context words, filtered by the deal's countries, plus phone-number parsing. It runs on a slim tokenizer pipeline with its own entity recognition off: no remote, cloud or large-language-model recognizers.
- Detector C: a multilingual span model (about 205M parameters, CPU, full precision; an int8 quantization was measured to change 46% of outputs and rejected). Names, organizations, addresses, account and government ids.
- Name model A: a small English named-entity model, the name detector when no ensemble runs.
- Language model A: language identification in a low-accuracy trigram mode, to route blocks to detectors.
- Verifier A (optional): a token classifier from a different model family than detector C, used only by the re-check for review counts; never baked into the image, never a hold, loaded only when every pinned file matches its hash.
- Verifier B (reserved): a candidate backup verifier from the same family as detector C (so weaker as an independent check), under a custom license that needs review before adoption. Not used.
- Speech model (audio and video): multilingual speech recognition, int8, CPU. Its transcript feeds the same detectors; detected spans are toned out of the audio.
- Face detector (images and video): a small face detection model, CPU. Faces are blurred under every profile
whose
facesisblur.
A baked model that doesn't match its hash fails the job for name model A; for an ensemble member it fails a strict job and drops the member otherwise (reported). A missing model fails a strict job; otherwise the fallback path runs and the report says so.
Full manifest
The full manifest (model names, versions, sources, per-file hashes, runtime packages) is in the auditor edition only.
Benchmarks
python -m jobs.bench_adapter --out DIR --check (CI's bench job, with the pinned weights fetched) runs the
synthetic benchmark corpus through the reference runner's real scrub path in every modality and compares it with
bench/baseline.json. A gated metric below the baseline minus the tolerance, or a file leak rate above it plus
the tolerance, fails the job. Text, tables and JSON are always gated; mail, office files, born-digital PDFs and
healthcare files are gated when their tools are present; image and scan numbers depend on the OCR build, so they
are reported and not gated.
Metrics, per entity kind:
- Recall (overlap): share of planted spans (text, tables, JSON) that any detection touches. Recall (exact): that a detection matches at both boundaries. Keyed by the planted kind.
- Precision (overlap): share of detections that touch a planted span, keyed by the detector's kind. Over-redaction lowers it.
- Removed: share of planted values that don't survive in anything a buyer can pull out of the output (OOXML parts and attribute values, PDF strings, decoded bytes, text the runner reads out, and for text drawn in an image, whether its box was blacked out). A held file leaks nothing.
- Leaked / Held: values that survived, and values in files that were held.
bench/baseline.json v1: corpus ca9a55297689575d, seed 1, regression tolerance 0.02. Recorded at package 0.6.0, patterns 1.3.0.
The baseline predates the current rules (patterns 1.4.0, package 0.6.0). CI's bench job fails on any gated metric below these numbers minus the tolerance, so they are floors, not this version's measurements.
| Mode | Kinds | Values | Recall (overlap) | Recall (exact) | Removed |
|---|---|---|---|---|---|
| Ensemble (detectors A, B, C, language model A) | 28 | 5419 | 0.974 | 0.962 | 0.862 |
| Name model A plus rules (no ensemble) | 29 | 5617 | 0.869 | 0.835 | 0.875 |
| Rules only (no model) | 25 | 4158 | 0.763 | 0.753 | 0.746 |
Weighted by planted values across kinds. Per-kind results are in the auditor edition.
Per modality and file type (a file leaks when any planted value survives in it):
Modalities in each mode's baseline:
- ensemble: audio, doc, face, image, json, mail, pdf, qr, scan, table, text
- ner: audio, doc, face, health, image, json, mail, pdf, qr, scan, table, text
- regex: json, table, text
Per-modality and per-file-type results are in the auditor edition.
Known caveats: the corpus is synthetic and generated by the package (bench/gen.py); real data is messier. Notes on
individual baseline rows are in the auditor edition.
Sensitive attributes, per category, on labelled cases plus a held-out set written after the last tuning pass and never tuned on (read the held-out recall as the expected rate on new phrasing):
ner (cases 00d9dbd1af29a75b)
| Category | Recall | Precision | Held-out recall | Held-out precision |
|---|---|---|---|---|
| overall | 0.989 | 1.000 | 0.513 | 1.000 |
regex (cases 00d9dbd1af29a75b)
| Category | Recall | Precision | Held-out recall | Held-out precision |
|---|---|---|---|---|
| overall | 0.989 | 1.000 | 0.513 | 1.000 |
Per-category results are in the auditor edition.
Independent re-check
recheck/ is a second detection pass over every byte about to ship, written apart from patterns.py so a bug
in the scrub's path isn't repeated: its own formats, its own checksums (not checks.py), its own decoder (not
textnorm.py) and its own stand-in recognizer, which blanks what the scrub left before any rule runs. It
rewrites nothing. A hit holds the file (recheck_hit); an unreadable shipped file is held
(recheck_unreadable). With verifier A loaded, the re-check also hands it a bounded slice of text it already
passed; verifier hits at or over the floor count as review items, never holds.
Re-check version 1; kinds it reports: secret, email, phone, ssn, health_id, card, iban, ip, dob, date, url_param.