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_report1.1.0; envelope v1, signing domaindeid_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.
kidis 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.intrudergains 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) andconfidence, 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 (aucfor a character n-gram classifier trained in-process on the run's own pairs and scored on held-out folds,listAucfor a lookup against the published stand-in name lists,npairs,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 overdeid_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.