How it works / Technical docs

The deid_report

The deid_report

One report per scrub of one source, built by report.build from the finished scrub's counts, signed by the runner, verified and sealed by the control plane, which adds what only it measures (the desk's output sample, the Merkle root once the delivery exists) and re-runs the bands. Counts by plain category, never values; no vendor or library name appears in a report.

  • Schema deid_report 1.1.0; envelope v1, signing domain deid_report.v1\n.
  • Small-cell rule: counts under 11 read <11; k for k-anonymity: 5.
  • Rule-file hashes in engine.rules: patterns (patterns.json), profiles (profiles.json), coverage (coverage.json), thresholds (thresholds.json), sensitive (sensitive.json), bizids (bizids.json), workflowBands (workflow/bands.json), reportBands (report/bands.json)
  • Model aliases as reports show them: detector_a → detector A, detector_b → detector B, detector_c → detector C, ner-a → name model A, langid-a → language model A

The runner writes the signed envelope and the buyer's HTML report and data card (report.render) to __report__/deid_report.json and .html, through the same gate as every delivered file, and, when the claim carries them, the product's signed-off plain-language one-pagers next to them as markdown and a one-page PDF (jobs/onepager.py lays the text out and adds no words). With no signed-off text, none are written. The delivery manifest and Merkle root cover them.

Envelope

{"v": 1, "alg": "Ed25519", "kid": "<16 hex>", "payload": "<the report as compact JSON text>", "sig": "<base64>"}
  • The payload is the report serialized once (sorted keys, compact separators, UTF-8). The signature covers the signing domain followed by the payload text's bytes, so a verifier checks the exact text and never re-serializes; there is no canonical-JSON drift between implementations.
  • kid is the first 16 hex of SHA-256 of the raw 32-byte public key.
  • The runner holds the 32-byte seed; the package never reads it from anywhere. Without a seed the envelope says alg: "none" with no signature, and the control planes show it as unsigned, never as signed to a buyer.
  • report.verify(envelope, public_keys) returns the payload only when the version, algorithm, kid and signature check against one of the given keys. Vectors for other implementations: tests/vectors/deid_report.json.

Versioning

The schema, the band thresholds and the envelope each carry their own version. A minor schema version may add a field; a field's type or meaning only changes with a major version. Band thresholds change only with a new bands.json version, and every report names the version it was judged by. The engine version, the rule-file hashes and the model hashes in engine pin exactly what ran.

Schema

deid_report/1.1.0 (2020-12 draft), title deid_report.

One de-identification report per scrub of one source, built by datacool_scrub.report and signed by the runner (see envelope). Counts drawn from the data read "<11" under 11. No paths (path hashes only), no names, no values, no vendor names: models and detectors by alias. Versioning: a field may be added in a minor version; a field's type or meaning never changes without a major version (CHANGELOG.md).

Field Type Required Description
schema const "deid_report" yes
version string yes
createdAt string yes
subject object yes Binds the report to one job: the control plane accepts it only from that job's finish call and checks the job id. No deal or listing id, since the report ships to buyers.
profile object yes
engine object yes
models array yes
privacy object yes
quality object yes
dataCard object yes
workflow object or null yes The workflow score's privacy-filtered copy (workflow/score.py private()), or null when it didn't run.
provenance object yes
bands object yes

subject

Field Type Required Description
subject.source string or null yes
subject.kind string or null yes
subject.job string or null yes

profile

Field Type Required Description
profile.id string yes
profile.label string yes
profile.version string or null yes
profile.hipaa enum "safe_harbor", "expert", null yes
profile.dates string or null yes
profile.standIns enum "keyed", "random" yes
profile.pending array yes

engine

Field Type Required Description
engine.package string yes
engine.packageSha256 $defs/hex yes
engine.patterns string yes
engine.rules object yes
engine.detection enum "ensemble", "ner", "regex" yes
engine.detectors array yes
engine.bands string yes

privacy

Field Type Required Description
privacy.removed object yes
privacy.coverage object yes
privacy.canary object yes
privacy.missBound object yes Exact one-sided upper bound on the miss rate from the desk's random output sample (rule of three when no misses). Not yet measured when the scrub runs before the sample is reviewed; the control plane adds it.
privacy.bench null or object yes The engine's benchmark on the synthetic corpus (hard cases), not this delivery.
privacy.kAnon object yes
privacy.linkage object yes
privacy.intruder object yes Motivated-intruder test. Desk part (basis "review sample"): on up to 50 snippets per source drawn from the desk's output sample, a reviewer who hasn't seen the original values tries to identify a person or the business; upper is the exact one-sided upper bound on the share where they could, counting unsure as identified. Not yet measured until the desk has run it; the control plane adds it. Since 1.1.0.
privacy.recheck object yes
privacy.sensitive object yes
privacy.phiOver boolean yes
privacy.held object yes
privacy.rescanClean boolean yes
privacy.confidential boolean yes

quality

Field Type Required Description
quality.files $defs/cell yes
quality.chars integer yes
quality.tokensEst integer yes
quality.duplicates object yes
quality.aiShare null or object yes
quality.padding object yes
quality.languages object yes
quality.redactionDensity number or null yes
quality.precision $defs/share yes
quality.forensics null or object yes

dataCard

Field Type Required Description
dataCard.sources array yes
dataCard.span object yes
dataCard.tables array yes
dataCard.normalized null or object yes
dataCard.limitations array yes

provenance

Field Type Required Description
provenance.merkleRoot string or null yes
provenance.manifestSha256 string or null yes
provenance.image string or null yes
provenance.signed boolean yes
provenance.modelsOk boolean or null yes

bands

Field Type Required Description
bands.version string yes
bands.privacy $defs/band yes
bands.quality $defs/band yes
bands.provenance $defs/band yes
bands.metrics object yes
bands.notApplicable array yes
bands.workflow object or null yes

Shared definitions:

$defs Shape
cell {"description": "A count; under 11 it reads "<11".", "oneOf": [{"type": "integer", "minimum": 11}, {"const": "<11"}]}
share {"oneOf": [{"type": "number", "minimum": 0, "maximum": 1}, {"type": "null"}]}
hex {"type": "string", "pattern": "^[0-9a-f]{16,64}$"}
basis {"enum": ["measured", "review sample", "not yet measured"]}
band {"type": "object", "required": ["id", "label", "provisional", "ceiling", "unmeasured", "failed", "basis"], "properties": {"id": {"enum": ["strong", "good", "needs_work"]}, "label": {"type": "string"}, "provisional": {"type": "boolean"}, "ce

Changelog

The schema (schema.json), the band thresholds (bands.json) and the envelope each carry their own version. A minor version may add a field; a field's type or meaning only changes with a major version. Band thresholds change only with a new bands.json version, and every report names the version it was judged by.

Schema 1.1.0, bands 1.0.0, envelope v1 (package 0.6.0)

  • privacy.intruder gains the motivated-intruder test. The desk part (basis "review sample", added by the control plane like the miss bound): n, identified, unsure, upper (exact one-sided bound counting unsure as identified) and confidence, from up to 50 snippets per source drawn from the desk's output sample. Until the desk runs it, basis stays "not yet measured" as in 1.0.0.
  • privacy.intruder.surrogates, when the deal uses realistic stand-ins: how well real names can be told from their stand-ins (auc for a character n-gram classifier trained in-process on the run's own pairs and scored on held-out folds, listAuc for a lookup against the published stand-in name lists, n pairs, folds); "not applicable" with tag stand-ins, "not yet measured" under 30 pairs. Scalars only.
  • Known limitations name the missing intruder test, and that realistic stand-in emails and phone numbers are recognisable as stand-ins by design. The page adds a residual-risk sentence naming the controls the results rest on. Bands are unchanged: the intruder results are reported, not banded.

Schema 1.0.0, bands 1.0.0, envelope v1 (package 0.6.0)

  • First version: one report per scrub of one source, built by report.build, signed by the runner (Ed25519 over deid_report.v1\n + the payload text; kid = first 16 hex of sha256 of the raw public key).
  • Privacy: removals by plain category, the coverage matrix (coverage.json 1.0.0) with removals per row, the canary self-test, the miss bound (from the desk's output sample, added by the control plane), the engine benchmark, k-anonymity on the linked view (per table when linkage is off), linkage counts, sensitive categories, holds by reason (file-intrinsic reasons only).
  • Not yet measured, reported as such: the motivated-intruder test and redaction precision from the desk sample. The independent re-check reads "measured" with its counts when the runner ran one, else "not yet measured".
  • Quality and data card: near- and exact-duplicate rates, machine-generated share, padding, languages, estimated tokens, redaction density, span, tables by path hash with their quasi-identifiers, known limitations.
  • Provenance: engine version and hash, rule-file hashes, model aliases with their pinned hashes, runner image, Merkle root and manifest hash once the delivery has them.
  • Bands: Privacy, Quality, Provenance (bands.json 1.0.0) plus the workflow band when the workflow score ran.

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