Skip to main content
Version: v0.0.7

Core Concepts

@cosyte/deid is built from five pieces: the policy engine, the five transforms, the 18-category Safe Harbor model, the fail-closed rule, and the value-free manifest.

Fail closed (the inverted reflex)

A parser is liberal in what it accepts. A de-identifier is the opposite: on any ambiguity - an unrecognized structure, an un-locatable identifier, a field it cannot classify, a free-text blob, a date/ZIP it cannot generalize - it blocks the value (withholds it), and records the decision. It never passes a value through as "probably safe". Liberality here leaks patients.

The mirror guard is over-scrub: a locus marked clinical (a lab value, a dose, a code, a status) is retained untouched. The engine never degenerates into a blanket-blanking "safe but useless" scrubber.

The five transforms

  • redact - remove the value (the fail-safe floor; the default for SSN, phone, email, URL, IP, …).
  • generalize - reduce precision: date → year, ZIP → initial 3 digits or 000 (the cited ≤20,000-population rule), age → 90+ for ages over 89.
  • date-shift - shift every date for a patient by a single deterministic per-patient offset, preserving intervals. An Expert-Determination technique (a shifted real date is still a date), so it is not used under the Safe Harbor policy - the offset never leaks.
  • pseudonymize - replace an identifier (MRN, beneficiary, account) with a consistent keyed HMAC-SHA-256 surrogate so records still link, without being reversible without the key.
  • hash - a keyed one-way digest. An unsalted hash of an identifier is re-identifiable, so the library keys it; the unkeyed path is off by default and documented non-conforming to §164.514(c).

The 18 Safe Harbor categories

The SAFE_HARBOR_CATEGORIES registry models §164.514(b)(2)(i)(A)–(R), including the open-ended catch-all (R) - "any other unique identifying number, characteristic, or code" - which is exactly why fail-closed is not optional: a closed allow-list of 17 concrete types can never satisfy (R).

The value-free manifest

Every DeidManifestEntry records the category acted on, the transform applied, the locus (a path - segment/field index, XPath, FHIRPath, DICOM tag), a count, a disposition, and a stable code. It never records the value that was removed, generalized, or pseudonymized - a manifest that logged a value would be a PHI leak in the audit trail. The date-shift offset and the HMAC key never appear anywhere.

The locus is the one field a per-format adapter builds out of the document, by naming the position with the identifier that sits there (a segment id, an element name, a tag). Each of those is checked against the shape its position promises before it is used, because a malformed document can put something at that position that is not an identifier; one that does not match is refused and the locus reads WITHHELD_LOCUS_TOKEN (<withheld>) plus a structural index. Content that happens to match the shape is still echoed, so this is a bound rather than an impossibility claim.

Immutability

The input model is never mutated; deidentify returns a deeply frozen DeidResult.