How it works / Technical docs

Detectors

Detectors

Detection has four layers, applied to every string as written and to its decoded view:

  1. 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.
  2. Ensemble (detectors B and C, language model A). Pinned models the reference runner loads and hands to ensemble.py as plain callables. Off when no runner loads them; then name model A is the name detector.
  3. Owner identity (blind.py): the deal's own terms (company, brands, domains, people) in every variant, when the control plane sends them.
  4. 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 email personal_email – 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.json are 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, by the 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 faces is blur.

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.

Engine docs at commit 1c5af8f39ebb. Plain-language versions: What happens to your data, the workflow score.