@cosyte/deid
Public entry point for @cosyte/deid: a healthcare de-identification engine.
@cosyte/deid is not a parser. It is a consumer-tier library: it applies a HIPAA-grounded
de-identification policy (Safe Harbor by default) to a structurally-located model of a healthcare
document and returns a transformed model plus a value-free manifest of what it acted on. It
borrows the parser archetype's disciplines (typed diagnostics, immutable output, a policy/profile
system) but inverts the parser's reflex: it fails closed, an unrecognized structure or an
un-locatable identifier is blocked, never passed through as safe.
Honesty line (governs the whole library). Results are "Safe-Harbor-transformed per the configured policy", never "de-identified" and never "HIPAA-compliant". Safe Harbor is implemented mechanically; the §164.514(b)(2)(ii) actual-knowledge condition is the consumer's; Expert Determination (§164.514(b)(1)) is supported, never rendered or certified here.
The root entry ships the format-agnostic core: the policy engine, the five transforms, the 18-category Safe Harbor model, the fail-closed rule, and the value-free manifest: tested against a generic locus model. The per-format locus maps (HL7 v2, C-CDA, FHIR, X12, NCPDP, DICOM) live behind the matching subpath exports.
Classes
DeidContext
An opaque handle to the consumer's key material. Construct it with createDeidContext; it exposes no secret field and redacts itself through every stringify channel.
Example
import { createDeidContext } from "@cosyte/deid";
const ctx = createDeidContext({ key: "secret" });
JSON.stringify({ ctx }); // => '{"ctx":"[DeidContext:redacted]"}'
Constructors
Constructor
new DeidContext():
DeidContext
Internal
Use createDeidContext.
Returns
Methods
forPatient()
forPatient(
patientId):DeidContext
Derive a sibling context scoped to a different patient, reusing the same key and seed. Used to de-identify a multi-patient corpus with one key while keeping each patient's date-shift offset distinct.
Parameters
patientId
string
The patient scope for the derived context.
Returns
A new DeidContext sharing this one's key/seed, scoped to patientId.
Example
import { createDeidContext } from "@cosyte/deid";
const base = createDeidContext({ key: "secret" });
const forPatient = base.forPatient("patient-2");
toJSON()
toJSON():
string
Redacts through JSON.stringify.
Returns
string
toString()
toString():
string
Redacts through String(...) and template interpolation.
Returns
string
DeidError
The error thrown on a FATAL_CODES condition. Carries a stable code; its message is
safe to log: it never contains PHI (no value, no key, no offset).
Example
import { deidentify, DeidError, FATAL_CODES } from "@cosyte/deid";
try {
deidentify(null as never, {});
} catch (err) {
if (err instanceof DeidError && err.code === FATAL_CODES.EMPTY_INPUT) {
// handle empty input
}
}
Extends
Error
Constructors
Constructor
new DeidError(
code,message):DeidError
Parameters
code
The FatalCode classifying this fatal.
message
string
A PHI-free explanation safe to log.
Returns
Overrides
Error.constructor
Properties
code
readonlycode:FatalCode
The stable fatal code.
DeidRegistry
The corpus-level consistency handle. Construct it with createDeidRegistry. It holds the consumer's key in a module-private registry: the handle exposes no secret field and redacts itself through every stringify channel.
Example
import { createDeidRegistry } from "@cosyte/deid";
const registry = createDeidRegistry({ key: "secret" });
// Same patient across documents shifts by the same offset:
const a = registry.forPatient("patient-1");
const b = registry.forPatient("patient-1");
a === b; // => true (memoized, same handle, same offset)
Constructors
Constructor
new DeidRegistry():
DeidRegistry
Internal
Use createDeidRegistry.
Returns
Methods
forPatient()
forPatient(
patientKey):DeidContext
Return the DeidContext scoped to patientKey, minting and memoizing it on first use. The
same key always yields the same context, hence the same deterministic date-shift offset, so a
patient's dates shift consistently across every document in the corpus. Pass this context as
DeidOptions.context to deidentify (or a per-format adapter) for that patient's documents.
Parameters
patientKey
string
A stable per-patient key (an MRN, an enterprise patient id, the consumer's choice, provided it is the same across that patient's documents).
Returns
The patient-scoped, self-redacting context.
Example
import { createDeidRegistry } from "@cosyte/deid";
const registry = createDeidRegistry({ key: "secret" });
const ctx = registry.forPatient("patient-1");
pseudonym()
pseudonym(
id):string
Map an identifier to its corpus-wide consistent pseudonym: a keyed-HMAC surrogate that is the same for the same input everywhere and not reversible without the key. Patient-independent: the same MRN maps to the same token regardless of which patient scope processes it, so records link.
Parameters
id
string
The identifier to pseudonymize (MRN, beneficiary number, account number).
Returns
string
The consistent lowercase-hex surrogate.
Example
import { createDeidRegistry } from "@cosyte/deid";
const registry = createDeidRegistry({ key: "secret" });
registry.pseudonym("MRN-1") === registry.pseudonym("MRN-1"); // => true (consistent)
remapUid()
remapUid(
uid):string
Map an opaque unique identifier (a DICOM study/series/instance UID, a GUID) to its corpus-wide
consistent surrogate, so cross-document UID linkage survives de-identification. Domain-separated
from pseudonym, so a UID and an identifier with the same text never share a surrogate. This
is the format-agnostic linkage primitive; a format that owns UID validity (DICOM UIDs must be a
valid 0.… OID) handles that in its own adapter.
Parameters
uid
string
The unique identifier to remap.
Returns
string
The consistent lowercase-hex surrogate.
Example
import { createDeidRegistry } from "@cosyte/deid";
const registry = createDeidRegistry({ key: "secret" });
registry.remapUid("1.2.840.113619.2.55") === registry.remapUid("1.2.840.113619.2.55"); // => true
toJSON()
toJSON():
string
Redacts through JSON.stringify.
Returns
string
toString()
toString():
string
Redacts through String(...) and template interpolation.
Returns
string
Interfaces
CategoryCoverage
Per-category coverage, for one of the 18 Safe Harbor categories (45 CFR §164.514(b)(2)(i)(A)–(R)), whether the pass acted on it and how. Present for all 18 categories in the report (in regulatory order A→R), so a reader sees the categories not acted on as plainly as those that were.
Example
import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const report = buildExpertDeterminationSupportReport([
{ category: SAFE_HARBOR_CATEGORIES.SSN, transform: "redact", locus: "PID-19", count: 1,
disposition: "removed", code: "DEID_CATEGORY_REMOVED", reidentificationCode: false },
]);
const ssn = report.categoryCoverage.find((c) => c.category === SAFE_HARBOR_CATEGORIES.SSN);
ssn?.actedOn; // => true
Properties
actedOn
readonlyactedOn:boolean
true when at least one manifest entry acted on this category.
category
readonlycategory:SafeHarborCategory
The Safe Harbor category.
codes
readonlycodes: readonlyDeidDispositionCode[]
The distinct disposition codes recorded for this category, sorted.
dispositions
readonlydispositions:Readonly<Record<ReportDisposition,number>>
Count of acted-on values by disposition.
letter
readonlyletter:string
The §164.514(b)(2)(i) sub-paragraph letter (A–R).
number
readonlynumber:number
The category ordinal (1–18).
residualRetained
readonlyresidualRetained:boolean
true when a coarse identifying residual was retained for this category.
title
readonlytitle:string
A short human title (no PHI).
totalCount
readonlytotalCount:number
Total count of values acted on across every locus of this category.
transforms
readonlytransforms: readonlyTransformName[]
The distinct transforms applied to this category, sorted.
DeidContextSpec
Specification for createDeidContext.
Example
import { createDeidContext } from "@cosyte/deid";
const ctx = createDeidContext({ key: "consumer-held-secret", patientId: "patient-1" });
Properties
dateShiftSeed?
readonlyoptionaldateShiftSeed?:string|Uint8Array<ArrayBufferLike>
A separate seed for deriving per-patient date-shift offsets. Defaults to the key when omitted,
so date-shifting works out of the box; supply a distinct seed to decouple the two secrets.
key
readonlykey:string|Uint8Array<ArrayBufferLike>
The HMAC key for keyed transforms. Consumer-held; never emitted. Must be non-empty.
maxShiftDays?
readonlyoptionalmaxShiftDays?:number
Absolute bound (days) on the per-patient date-shift offset. Defaults to 365.
patientId?
readonlyoptionalpatientId?:string
The patient scope this context de-identifies. Required for the deterministic date-shift offset.
DeidDocument
The transformed document: the same loci with de-identified values. Format-specific documents
replace this unknown-shaped placeholder; the core returns this generic shape.
Example
import { type DeidDocument } from "@cosyte/deid";
const doc: DeidDocument = { loci: [] };
Properties
loci
readonlyloci: readonlyTransformedLocus[]
The transformed loci.
DeidManifestEntry
A single value-free manifest entry.
Example
import { type DeidManifestEntry, SAFE_HARBOR_CATEGORIES, DEID_DISPOSITION_CODES } from "@cosyte/deid";
const entry: DeidManifestEntry = {
category: SAFE_HARBOR_CATEGORIES.SSN,
transform: "redact",
locus: "PID-19",
count: 1,
disposition: "removed",
code: DEID_DISPOSITION_CODES.DEID_CATEGORY_REMOVED,
reidentificationCode: false,
};
Properties
category
readonlycategory:SafeHarborCategory
The Safe Harbor category acted on.
code
readonlycode:DeidDispositionCode
The stable disposition code.
count
readonlycount:number
How many values at this locus/category/disposition were acted on.
disposition
readonlydisposition:"transformed"|"removed"|"blocked"|"retained"
The disposition. retained is the honest fourth outcome, and it covers two different kept things,
told apart by the entry's code:
- an identifying locus the configured profile's retention set deliberately kept unchanged,
paired with
DEID_RESIDUAL_RETAINED, so a kept identifier reaches the determiner's residual inventory rather than being invisible; - a party whose role places it outside §164.514(b)(2)(i)'s scope clause, paired with
DEID_PARTY_ROLE_RETAINEDand the partyRole code that placed it there. That is not a residual of the individual's identity and does not join the residual inventory.
(Clinical values kept by the over-scrub guard are not identifiers and are not manifest entries at all.)
locus
readonlylocus:string
The format-neutral locus (segment/field index · path · tag), never a value.
partyRole?
readonlyoptionalpartyRole?:string
The role code that placed a party outside §164.514(b)(2)(i)'s scope clause, present only on
a DEID_PARTY_ROLE_RETAINED entry. It answers, at the party's own locus, the question a silent
retention could not: on what did the pass decide this party is not the individual, a relative, an
employer or a household member?
Value-free and additive, like reidentificationCode: it is a code from a role table this library ships (an X12 element 98 entity-identifier code; the role an HL7 v2.5.1 field definition types at an organisation-typed position), never a name, an identifier, an address or any other value read off the document, and no other field of the entry changes on account of it.
reidentificationCode
readonlyreidentificationCode:boolean
true exactly at a locus where the pass emitted a keyed surrogate: the output of
pseudonymize, hash or date-shift, which is computed from the value it replaced and is
therefore a re-identification code anyone holding the key can reverse the linkage of. false
at every other locus.
It is value-free (a boolean; never a value, a key or a shift offset) and additive: no other
field of this entry changes on account of it, so a consumer branching on transform,
disposition or code reads exactly what it read before. It is what the Expert-Determination
support report's keyed-surrogate residual inventory is built from, so an expert can reason
about the residual a surrogate leaves rather than discover it from the transform name.
transform
readonlytransform:TransformName
The transform applied.
DeidOptions
Options for deidentify.
Example
import { type DeidOptions, createDeidContext } from "@cosyte/deid";
const opts: DeidOptions = { policy: "safe-harbor", context: createDeidContext({ key: "secret" }) };
Properties
context?
readonlyoptionalcontext?:DeidContext
The context carrying the consumer's key, required only when the policy uses a keyed transform.
policy?
readonlyoptionalpolicy?:DeidPolicy|"safe-harbor"
The policy to apply. Defaults to the built-in Safe Harbor policy.
redactor?
readonlyoptionalredactor?:FreeTextRedactor
A consumer-supplied free-text redactor. When present, the engine invokes it
at each free-text locus instead of blocking, and records its output as consumer-asserted
(DEID_FREETEXT_CONSUMER_REDACTED). The library bundles no redactor. Fail-closed contract:
when this is omitted, or when the redactor throws or returns nothing, the free-text locus is
blocked (the safe default), never emitted un-redacted. A returned redaction is trusted as the
consumer's; the engine does not re-verify it and does not touch the structural PHI the adapters
remove. See FreeTextRedactor.
retainedLoci?
readonlyoptionalretainedLoci?: readonlyRetainedLocusClass[]
The retention classes the configured profile permits: the named groups of identifying loci a format adapter may pass through unchanged (each one still recorded as a residual). Build this with profileOptions rather than by hand; the widen-never-narrow contract on a derived profile is what keeps a site preset from adding one.
Absent or empty retains nothing, so a bare options bag gets the strict treatment.
DeidPolicy
A de-identification policy: a name plus a per-category transform assignment covering all 18 categories.
Example
import { SAFE_HARBOR_POLICY } from "@cosyte/deid";
SAFE_HARBOR_POLICY.name; // => "safe-harbor"
Properties
name
readonlyname:string
The policy name: surfaced in output labelling ("Safe-Harbor-transformed per the configured policy").
transforms
readonlytransforms:Readonly<Record<SafeHarborCategory,TransformName>>
The transform applied to each Safe Harbor category.
DeidPolicySpec
The spec accepted by defineDeidPolicy: a name and a partial transform map that overrides the Safe Harbor defaults for the categories it names.
Example
import { type DeidPolicySpec, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const spec: DeidPolicySpec = {
name: "research",
transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" },
};
Properties
name
readonlyname:string
The policy name.
transforms?
readonlyoptionaltransforms?:Partial<Readonly<Record<SafeHarborCategory,TransformName>>>
Per-category transform overrides; unlisted categories keep their Safe Harbor default.
DeidProfile
A reusable, named de-identification preset: a policy plus its honest usage posture and an optional default free-text redactor.
Example
import { SAFE_HARBOR_PROFILE } from "@cosyte/deid";
SAFE_HARBOR_PROFILE.name; // => "safe-harbor"
Properties
description
readonlydescription:string
A one-line honest description of what the profile does and does not guarantee: surfaced in docs and tooling so a preset is never adopted without its caveats.
name
readonlyname:string
The profile name (also the policy name).
policy
readonlypolicy:DeidPolicy
The concrete policy the engine applies.
redactor?
readonlyoptionalredactor?:FreeTextRedactor
An optional default free-text redactor the profile carries into profileOptions.
requiresContext
readonlyrequiresContext:boolean
Whether the profile requires a keyed per-patient DeidContext to run at all (true when
any category uses a keyed transform such as date-shift on a category that is always present).
retainedLoci
readonlyretainedLoci: readonlyRetainedLocusClass[]
The retention classes this profile permits: named groups of identifying loci a format
adapter passes through unchanged, each one still recorded as a DEID_RESIDUAL_RETAINED residual.
Empty on SAFE_HARBOR_PROFILE; LIMITED_DATA_SET_PROFILE carries the three classes
§164.514(e)(2) permits a limited data set to keep. A profile that DECLARES the
limited-data-set standard is checked against that regulation's own list, so a class beyond it
is a fatal FATAL_CODES.DEID_PROFILE_INVALID rather than an unsupported claim.
standard
readonlystandard:DeidStandard
The standard this profile targets: governs how its output may honestly be described.
DeidProfileSpec
The spec accepted by defineDeidProfile: a name, an optional base profile (default SAFE_HARBOR_PROFILE), per-category transform overrides, and an optional default redactor.
Example
import { type DeidProfileSpec, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const spec: DeidProfileSpec = {
name: "site-a",
transforms: { [SAFE_HARBOR_CATEGORIES.GEOGRAPHIC]: "redact" }, // tighten: generalize -> redact
};
Properties
base?
readonlyoptionalbase?:DeidProfile
The base profile to derive from. Defaults to SAFE_HARBOR_PROFILE.
description?
readonlyoptionaldescription?:string
An optional human description; a default is synthesized from the base when omitted.
name
readonlyname:string
The profile name. Must not be a reserved standard label unless it genuinely matches it.
redactor?
readonlyoptionalredactor?:FreeTextRedactor
An optional default free-text redactor the derived profile carries.
retainedLoci?
readonlyoptionalretainedLoci?: readonlyRetainedLocusClass[]
The retention classes the derived profile permits. Must be a subset of the base's: dropping a class removes more (a widening, allowed), adding one keeps more (a narrowing, rejected). Omit to inherit the base's set unchanged.
transforms?
readonlyoptionaltransforms?:Partial<Readonly<Record<SafeHarborCategory,TransformName>>>
Per-category transform overrides. Each may only move a category to an equal-or-stronger transform than the base (widen-never-narrow); a weakening override is rejected.
DeidRegistrySpec
Specification for createDeidRegistry. Mirrors DeidContextSpec minus the per-patient scope: the registry mints patient scopes itself via DeidRegistry.forPatient.
Example
import { createDeidRegistry } from "@cosyte/deid";
const registry = createDeidRegistry({ key: process.env.DEID_KEY! });
Properties
dateShiftSeed?
readonlyoptionaldateShiftSeed?:string|Uint8Array<ArrayBufferLike>
A separate seed for deriving per-patient date-shift offsets. Defaults to the key when omitted;
supply a distinct seed to decouple pseudonymization from date-shifting.
key
readonlykey:string|Uint8Array<ArrayBufferLike>
The HMAC key for keyed transforms. Consumer-held; never emitted. Must be non-empty.
maxShiftDays?
readonlyoptionalmaxShiftDays?:number
Absolute bound (days) on each derived per-patient date-shift offset. Defaults to 365.
DeidResult
The immutable result of a de-id pass: the transformed document plus the value-free manifest.
Example
import { deidentify } from "@cosyte/deid";
const result = deidentify({ loci: [] }, {});
result.manifest; // => [] (nothing acted on)
Properties
document
readonlydocument:DeidDocument
The transformed document (the generic locus document from the core).
manifest
readonlymanifest: readonlyDeidManifestEntry[]
The value-free audit of every action, in locus order.
DispositionSummary
A roll-up of how many values landed in each disposition across the whole report: the one-glance posture of the pass. Every field is a count; none is a value.
Example
import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const report = buildExpertDeterminationSupportReport([
{ category: SAFE_HARBOR_CATEGORIES.SSN, transform: "redact", locus: "PID-19", count: 2,
disposition: "removed", code: "DEID_CATEGORY_REMOVED", reidentificationCode: false },
]);
report.dispositionSummary.removed; // => 2
Properties
blocked
readonlyblocked:number
Loci failed closed (blocked; value withheld).
freeTextBlocked
readonlyfreeTextBlocked:number
Free-text loci blocked by default (DEID_FREETEXT_BLOCKED).
freeTextConsumerRedacted
readonlyfreeTextConsumerRedacted:number
Free-text loci redacted by a consumer-supplied BYO redactor (DEID_FREETEXT_CONSUMER_REDACTED).
removed
readonlyremoved:number
Values removed outright.
residualRetained
readonlyresidualRetained:number
Of the transformed values, how many retained a coarse residual (DEID_RESIDUAL_RETAINED).
retained
readonlyretained:number
Loci the pass kept unchanged and recorded: identifying loci the profile's retention set kept (each one a residual below), plus parties whose role placed them outside §164.514(b)(2)(i)'s scope clause (each one naming the role code it was classified on, and none of them a residual).
transformed
readonlytransformed:number
Values replaced with a surrogate / generalized / shifted / hashed / BYO-redacted.
unexaminedResidualPositions
readonlyunexaminedResidualPositions:number|null
Value-bearing positions the pass handed through that no locus rule named
(DEID_POSITION_UNEXAMINED), or null when the pass reported no measurement at all.
The null is the point: a bare 0 in a roll-up is read as "nothing was handed through
unexamined", which is exactly the claim an unmeasured pass cannot make. A number here is a
measurement; null says there is none to report.
These positions are not counted in transformed, removed, blocked or retained: no value was acted on, blocked or kept by a decision, so folding them into any of those four would overstate what the pass did.
ExpertDeterminationReportOptions
Options for buildExpertDeterminationSupportReport.
Properties
policy?
readonlyoptionalpolicy?:string|DeidPolicy
The policy applied (or its name): surfaced as the report's policy label.
quasiIdentifiers?
readonlyoptionalquasiIdentifiers?:QuasiIdentifierClassInput
Consumer-supplied quasi-identifier equivalence-class sizes for the descriptive k-indicator.
unexaminedResiduals?
readonlyoptionalunexaminedResiduals?: readonlyUnexaminedResidual[] | readonly readonlyUnexaminedResidual[][]
The unexamined residual positions a pass measured, as returned alongside its manifest by every
format adapter. A single pass's list, or an array of them for a corpus, matching the shape of the
manifests argument.
Supplying it, even empty, is what makes the inventory measured. Omitting it leaves
ExpertDeterminationSupportReport.unexaminedResidualsMeasured false and the roll-up count
null, so the report says plainly that nothing was counted rather than printing a zero a reader
would take for a clearance. An empty array is the opposite claim, and a real one: the pass
enumerated the positions it handed through and none went unexamined.
ExpertDeterminationSupportReport
The structured, value-free Expert-Determination support report. Machine-readable (this object) and
human-readable (via formatExpertDeterminationSupportReport). It supports a determination
and is never one: determination is always null and disclaimer leads.
Example
import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const report = buildExpertDeterminationSupportReport([
{ category: SAFE_HARBOR_CATEGORIES.NAMES, transform: "redact", locus: "PID-5", count: 1,
disposition: "removed", code: "DEID_CATEGORY_REMOVED", reidentificationCode: false },
]);
report.determination; // => null (the library never renders one)
report.totals.categoriesActedOn; // => 1
Properties
categoryCoverage
readonlycategoryCoverage: readonlyCategoryCoverage[]
Coverage for all 18 Safe Harbor categories, in regulatory order (A→R).
determination
readonlydetermination:null
Always null: the library renders no determination.
disclaimer
readonlydisclaimer:string
The prominent non-certification statement (EXPERT_DETERMINATION_DISCLAIMER).
dispositionSummary
readonlydispositionSummary:DispositionSummary
The disposition roll-up.
documentCount
readonlydocumentCount:number
How many documents' manifests this report summarizes (1 for a single manifest).
keyedSurrogateResiduals
readonlykeyedSurrogateResiduals: readonlyKeyedSurrogateResidual[]
The keyed-surrogate residual inventory: every locus whose manifest entry carries
reidentificationCode, so a determiner can reason about the linkage a keyed surrogate preserves.
A sibling of retainedQuasiIdentifiers, never folded into it.
kind
readonlykind:"expert-determination-support"
A stable discriminant for the report shape.
outputLabel
readonlyoutputLabel:string
The output label the pass applied: "Safe-Harbor-transformed per the configured policy".
perLocus
readonlyperLocus: readonlyDeidManifestEntry[]
Every acted-on locus, aggregated and in a deterministic order: the value-free manifest, structured.
policy
readonlypolicy:string|null
The policy name applied, or null if not supplied.
quasiIdentifierStatistics
readonlyquasiIdentifierStatistics:QuasiIdentifierStatistics|null
Descriptive quasi-identifier statistics, only when the consumer supplied class sizes; else null.
retainedQuasiIdentifiers
readonlyretainedQuasiIdentifiers: readonlyRetainedQuasiIdentifier[]
The retained-quasi-identifier residual inventory (coarse residuals the pass recorded as retained).
totals
readonlytotals:object
Headline totals.
categoriesActedOn
readonlycategoriesActedOn:number
How many of the 18 Safe Harbor categories were acted on.
loci
readonlyloci:number
Count of distinct acted-on loci (distinct locus paths across the whole report).
rows
readonlyrows:number
Count of aggregated perLocus rows (distinct category·transform·locus·disposition·code tuples).
unexaminedResiduals
readonlyunexaminedResiduals: readonlyUnexaminedResidual[]
The unexamined-residual inventory: every value-bearing position the pass handed through that no locus rule named, with its structural locus and a count. A sibling of retainedQuasiIdentifiers, never folded into it: that inventory holds residuals of values the pass examined, and admitting an unexamined position would make a kept year and a position nothing looked at indistinguishable.
Empty when the pass measured none and when nothing was measured at all: read unexaminedResidualsMeasured to tell those apart.
unexaminedResidualsMeasured
readonlyunexaminedResidualsMeasured:boolean
true when the pass reported a measurement of its unexamined residual positions, whatever the
count. false says no measurement was supplied, so an empty unexaminedResiduals is silence
rather than a measured zero, and the determiner is told which it is instead of inferring it.
FreeTextRedactionRequest
The value-bearing request the engine hands a FreeTextRedactor at each free-text locus. This is the one place a value crosses into consumer code by design: it never reaches the manifest.
Example
import { type FreeTextRedactionRequest } from "@cosyte/deid";
const request: FreeTextRedactionRequest = { text: "patient prose…", locus: "OBX-5" };
Properties
category?
readonlyoptionalcategory?:SafeHarborCategory
The Safe Harbor category associated with the locus, when the adapter could classify it.
locus
readonlylocus:string
The format-neutral locus path (e.g. "OBX-5", "section/text"): value-free, for routing/logging.
text
readonlytext:string
The free-text prose at the locus, for the consumer's redactor to de-identify.
FreeTextRedactionResult
The successful result of a FreeTextRedactor: the redacted prose to write back in place. To
decline a locus (so the engine fails closed and blocks it), a redactor returns null /
undefined or throws: it never returns un-redacted text expecting the engine to catch a miss.
Example
import { type FreeTextRedactionResult } from "@cosyte/deid";
const result: FreeTextRedactionResult = { text: "patient [REDACTED]…" };
Properties
text
readonlytext:string
The redacted prose. May be an empty string (all prose removed); that is a valid redaction.
GeneralizeOutcome
The outcome of a generalization: the reduced value plus whether it retains a coarse residual
(a kept year, a retained safe 3-digit ZIP prefix) that the manifest must surface for the
actual-knowledge test, versus a fully non-identifying result (000, 90+).
Properties
residual
readonlyresidual:boolean
true when a coarse identifying residual was retained; false when fully suppressed.
value
readonlyvalue:string
The generalized value.
GenericLocus
A single structurally-located candidate value. category is omitted when the caller cannot classify
the locus: an unclassified PHI-bearing locus is treated as catch-all (R) and fails closed.
Example
import { type GenericLocus, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const locus: GenericLocus = {
path: "PID-5",
kind: "identifier",
category: SAFE_HARBOR_CATEGORIES.NAMES,
value: "SENTINEL_NAME_01",
};
Properties
category?
readonlyoptionalcategory?:SafeHarborCategory
The Safe Harbor category, when known. Omit to force fail-closed handling as category (R).
kind
readonlykind:LocusKind
The kind of value at this locus.
partyRole?
readonlyoptionalpartyRole?:string
The role code an adapter classified a party on, present only at a locus that records a
party whose role places it outside §164.514(b)(2)(i)'s scope clause and whose name and
identifiers the pass therefore left in place. The engine records the code in the manifest at this
locus (DEID_PARTY_ROLE_RETAINED) and writes nothing back, so a retention that used to be silent
can be audited.
It is a code, never a value: an adapter sets it from its own committed role table (see
classifyPartyRole), and a locus carrying it must carry an empty value: a party-role
record has no value to hand anywhere. A non-empty one fails closed (blocked) rather than being
retained, so this marker can never become a route for passing an identifier through.
path
readonlypath:string
The format-neutral path to the value. Recorded in the manifest; never a value.
retainedPart?
readonlyoptionalretainedPart?:RetainedLocusPart
The named part of an otherwise-excluded category this locus carries, present only where the regulation's exclusion is partial. §164.514(e)(2)(ii) is the one such clause: it removes postal address information "other than town or city, State, and zip code", so a locus addressing one of those three parts is marked here and the rest of the address is not marked at all.
Like retention it is a proposal: the engine keeps the value only if the options list
the class and isRetainablePart accepts the class, the resolved category and this
part together. It is the only route past the never-retainable category guard, and it does
not widen that guard: GEOGRAPHIC remains a category no profile may retain whole.
An adapter that sets this must address ONE part, at its own structural locus (a component, not the whole address), because the engine keeps exactly what the locus carries. Marking a whole address with a part name would keep the whole address, which is the failure this field exists to make impossible to reach by accident rather than one it can detect.
retention?
readonlyoptionalretention?:RetainedLocusClass
The retention class this locus belongs to, when an adapter can name one. It is a proposal, never a decision: the engine keeps the value only if the configured options also list this class, and the resolved category is one a limited data set may carry at all. Both keys are required, so an adapter cannot retain anything on its own and a stale marker cannot leak a value.
Absent (the default) the locus takes its normal policy transform. This is not the over-scrub
guard: a clinical locus is not an identifier and is retained without a manifest row, whereas a
locus marked here is identifying and is always recorded when it is kept.
value
readonlyvalue:string
The value at the locus. Consumed by the engine, never copied into the manifest.
KeyedSurrogateResidual
One entry in the keyed-surrogate residual inventory: a locus where the pass replaced the value
with a surrogate derived from that value under the consumer's key (pseudonymize, hash or
date-shift), which the manifest marks with reidentificationCode. Anyone holding the key can
re-link those records, so §164.514(c)(1) does not permit such a code under Safe Harbor and the
residual is an Expert-Determination consideration.
This inventory is a sibling of RetainedQuasiIdentifier, never a member of it: a retained
quasi-identifier is a piece of the original value that survived (a year, a safe 3-digit ZIP prefix,
a whole kept date), while this is a computed replacement that carries no plaintext but preserves
linkage. Carrying transform on every row is what keeps the two distinguishable at a glance.
Value-free, like everything else here: the locus, the category, the count and the transform, never a value, a key or a shift offset.
Example
import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const report = buildExpertDeterminationSupportReport([
{ category: SAFE_HARBOR_CATEGORIES.MRN, transform: "pseudonymize", locus: "PID-3", count: 1,
disposition: "transformed", code: "DEID_CATEGORY_PSEUDONYMIZED", reidentificationCode: true },
]);
report.keyedSurrogateResiduals[0]?.transform; // => "pseudonymize"
Properties
category
readonlycategory:SafeHarborCategory
The Safe Harbor category the surrogate stands in for.
count
readonlycount:number
How many values at this locus were replaced by a keyed surrogate.
locus
readonlylocus:string
The format-neutral locus (segment/field index · path · tag), never a value.
transform
readonlytransform:TransformName
Which keyed transform produced it: pseudonymize, hash or date-shift.
LocusModel
The format-agnostic input model: a flat list of located candidate values. A per-format adapter produces this from a parsed HL7 / C-CDA / FHIR / X12 / NCPDP / DICOM model.
Example
import { type LocusModel } from "@cosyte/deid";
const model: LocusModel = { loci: [{ path: "PID-3", kind: "identifier", value: "X" }] };
Properties
loci
readonlyloci: readonlyGenericLocus[]
The located candidate values to de-identify.
PartyRoleTable
One format's two role-code lists. Both are the format's own committed vocabulary (X12 element 98 entity-identifier codes; the role an HL7 v2.5.1 field definition types at a party), never a value read off a document.
Example
import { type PartyRoleTable } from "@cosyte/deid";
const table: PartyRoleTable = { subject: new Set(["36"]), outside: new Set(["85"]) };
Properties
outside
readonlyoutside:ReadonlySet<string>
Role codes that place the party outside the clause: provider / facility / payer / payee / submitter.
subject
readonlysubject:ReadonlySet<string>
Role codes the scope clause reaches: the individual, a relative, an employer, a household member.
QuasiIdentifierClassInput
Consumer-supplied quasi-identifier equivalence-class data. The library never derives this: it has no view of the quasi-identifier values. The consumer, who holds the values, groups their records by the chosen quasi-identifier set (e.g. 3-digit ZIP × birth year × sex) and supplies the size of each distinct group; the report echoes descriptive counts over those sizes (see QuasiIdentifierStatistics).
Example
import { type QuasiIdentifierClassInput } from "@cosyte/deid";
// Consumer grouped 100 records into classes of these sizes over their chosen quasi-identifier set:
const qi: QuasiIdentifierClassInput = {
quasiIdentifierSet: "3-digit ZIP × birth year × sex",
equivalenceClassSizes: [40, 33, 20, 5, 1, 1],
};
Properties
equivalenceClassSizes
readonlyequivalenceClassSizes: readonlynumber[]
One size per distinct quasi-identifier combination: the record count in that equivalence class.
quasiIdentifierSet?
readonlyoptionalquasiIdentifierSet?:string
A human label for the quasi-identifier set the sizes were computed over. No PHI.
QuasiIdentifierStatistics
Descriptive statistics over consumer-supplied equivalence-class sizes, including the smallest class size, the widely-used k-anonymity indicator.
This is not a risk score and not a determination. It is arithmetic over sizes the consumer
supplied: the library counts what it is given, applies no threshold, draws no k ≥ n ⇒ safe
conclusion, and emits no verdict. A statistician documents an indicator like this as one input to a
§164.514(b)(1) determination: the determination remains theirs. See note.
Example
import { buildExpertDeterminationSupportReport } from "@cosyte/deid";
const report = buildExpertDeterminationSupportReport([], {
quasiIdentifiers: { equivalenceClassSizes: [40, 20, 5, 1, 1] },
});
report.quasiIdentifierStatistics?.minimumEquivalenceClassSize; // => 1
report.quasiIdentifierStatistics?.uniqueRecords; // => 2
Properties
distinctCombinations
readonlydistinctCombinations:number
Number of distinct quasi-identifier combinations = the number of equivalence classes supplied.
minimumEquivalenceClassSize
readonlyminimumEquivalenceClassSize:number
The smallest equivalence-class size: the k-anonymity indicator. Descriptive only.
note
readonlynote:string
The honesty note: this is a descriptive input, never a risk score or determination.
quasiIdentifierSet
readonlyquasiIdentifierSet:string|null
The label the consumer gave the quasi-identifier set, or null if unlabelled.
totalRecords
readonlytotalRecords:number
Total records across all classes = the sum of the supplied sizes.
uniqueRecords
readonlyuniqueRecords:number
How many records fall in a class of size 1 (sample-uniques on the chosen set). Descriptive only.
RetainedQuasiIdentifier
One entry in the retained-quasi-identifier residual inventory: an identifying element the pass
deliberately kept for analytic utility and recorded as DEID_RESIDUAL_RETAINED. Two things
land here:
- a coarse residual left by a generalization: a year-only date, a retained safe 3-digit ZIP prefix, an exact age ≤ 89;
- a whole value kept by the profile's retention set: under a limited-data-set preset, an admission / discharge / service date or an encounter or order number, which a Safe-Harbor-labelled policy removes instead.
These are exactly the residuals an expert reasons about under the §164.514(b)(2)(ii) actual-knowledge test, and the second kind is the stronger one: it is a full, unreduced value.
A keyed surrogate is NOT one of these and never joins this list. It is a replacement computed from the value rather than a piece of the value that survived, so it is inventoried separately as a KeyedSurrogateResidual: the two kinds are different residuals and a determiner reasons about them differently.
Example
import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const report = buildExpertDeterminationSupportReport([
{ category: SAFE_HARBOR_CATEGORIES.DATES, transform: "generalize", locus: "PID-7", count: 1,
disposition: "transformed", code: "DEID_RESIDUAL_RETAINED", reidentificationCode: false },
]);
report.retainedQuasiIdentifiers[0]?.locus; // => "PID-7"
Properties
category
readonlycategory:SafeHarborCategory
The Safe Harbor category of the retained residual.
count
readonlycount:number
How many values at this locus retained a residual.
locus
readonlylocus:string
The format-neutral locus (segment/field index · path · tag), never a value.
transform
readonlytransform:TransformName
How the residual arose, so the two kinds are not indistinguishable in the inventory: generalize
left a coarse residual (a year, a safe 3-digit ZIP prefix, an age ≤ 89), while retain kept the
whole unreduced value. A determiner reasons about those very differently.
TransformedLocus
The transformed value of a locus after a de-id pass: the reduced/surrogate value, or null when
the value was removed or blocked (fail-closed). Carries no secret and no original value.
Example
import { type TransformedLocus } from "@cosyte/deid";
const t: TransformedLocus = { path: "PID-3", kind: "identifier", value: null, disposition: "removed" };
Properties
disposition
readonlydisposition:"transformed"|"removed"|"blocked"|"retained"
What happened to the value.
kind
readonlykind:LocusKind
The kind of value at this locus (unchanged from input).
path
readonlypath:string
The format-neutral path (unchanged from input).
value
readonlyvalue:string|null
The transformed value, or null when removed / blocked.
UnexaminedResidual
One value-bearing position a pass handed through that no locus rule named, counted and located in the value-free record.
Example
import { type UnexaminedResidual } from "@cosyte/deid";
const residual: UnexaminedResidual = {
locus: "PV1-7",
count: 1,
examined: false,
locusWithheld: false,
code: "DEID_POSITION_UNEXAMINED",
};
Properties
code
readonlycode:"DEID_POSITION_UNEXAMINED"
The stable disposition code: always DEID_POSITION_UNEXAMINED.
count
readonlycount:number
How many value-bearing positions at this locus were handed through unexamined.
examined
readonlyexamined:false
Always false: the fact this record exists to state. No locus rule reached the position, so the
pass neither acted on it, nor blocked it, nor decided to keep it. A literal rather than a
convention, so a consumer merging inventories cannot mistake one of these for an acted-on entry.
locus
readonlylocus:string
The format-neutral structural locus of the position (segment/field index · path · tag), or WITHHELD_LOCUS_TOKEN when it could not be expressed. Never a value, a key or an offset.
locusWithheld
readonlylocusWithheld:boolean
true when the position's structural locus could not be expressed and locus is
therefore WITHHELD_LOCUS_TOKEN. The position is still counted: an inexpressible "where" is
never a reason to drop a "how many". Value-free (a boolean).
Type Aliases
DeidDispositionCode
DeidDispositionCode = typeof
DEID_DISPOSITION_CODES[keyof typeofDEID_DISPOSITION_CODES]
A value from DEID_DISPOSITION_CODES: the code every manifest entry carries.
Example
import { DEID_DISPOSITION_CODES, type DeidDispositionCode } from "@cosyte/deid";
const code: DeidDispositionCode = DEID_DISPOSITION_CODES.DEID_CATEGORY_REMOVED;
DeidStandard
DeidStandard =
"safe-harbor"|"limited-data-set"|"custom"
The named standard a profile targets, surfaced so output labelling can never overclaim.
FatalCode
FatalCode = typeof
FATAL_CODES[keyof typeofFATAL_CODES]
A value from FATAL_CODES: the code carried by a thrown DeidError.
Example
import { FATAL_CODES, type FatalCode } from "@cosyte/deid";
const code: FatalCode = FATAL_CODES.EMPTY_INPUT;
FreeTextRedactor
FreeTextRedactor = (
request) =>FreeTextRedactionResult|null|undefined
A consumer-supplied free-text redactor. The engine invokes it at each free-text locus and treats a
returned FreeTextRedactionResult as consumer-asserted (recorded as
DEID_FREETEXT_CONSUMER_REDACTED). Returning null / undefined, or throwing, makes the engine
fail closed and block the locus. The library never inspects the returned text for residual PHI;
completeness is the consumer's responsibility.
Parameters
request
The free-text value, its value-free locus path, and its category when known.
Returns
FreeTextRedactionResult | null | undefined
The redacted prose as { text }, or null / undefined to decline (engine blocks).
Example
import { deidentify, type FreeTextRedactor } from "@cosyte/deid";
// A consumer plugs in their own detector; the library bundles none.
const redactor: FreeTextRedactor = ({ text }) => ({ text: myNerModel.scrub(text) });
const result = deidentify(model, { redactor });
LocusKind
LocusKind =
"identifier"|"date"|"age"|"zip"|"freetext"|"clinical"|"unknown"
The kind of value at a locus: drives which generalization applies and whether the engine must
fail closed. clinical is the over-scrub guard: a clinical value (a lab result, a dose, a code, a
status) is not an identifier and must survive untouched.
Example
import { type LocusKind } from "@cosyte/deid";
const kind: LocusKind = "identifier";
PartyRoleClassification
PartyRoleClassification = {
roleCode:string;scope:"safe-harbor-subject"|"outside-scope"; } | {roleCode?:undefined;scope:"unknown"; }
The outcome of classifyPartyRole. roleCode is present only when the role was recognized,
and is then the matching member of the caller's own table: it carries no name, no identifier and no
other value, so it is safe to record in the value-free manifest.
Union Members
Type Literal
{ roleCode: string; scope: "safe-harbor-subject" | "outside-scope"; }
roleCode
readonlyroleCode:string
The matching member of the caller's table: a code, never a value.
scope
readonlyscope:"safe-harbor-subject"|"outside-scope"
The role was recognized on one of the two lists.
Type Literal
{ roleCode?: undefined; scope: "unknown"; }
roleCode?
readonlyoptionalroleCode?:undefined
Never carried for an unrecognized role: an unknown code is not this library's to echo.
scope
readonlyscope:"unknown"
The role is absent, empty, or on neither list: fail closed.
Example
import { classifyPartyRole, type PartyRoleClassification } from "@cosyte/deid";
const c: PartyRoleClassification = classifyPartyRole("85", {
subject: new Set(["36"]),
outside: new Set(["85"]),
});
c.scope; // => "outside-scope"
PartyRoleScope
PartyRoleScope =
"safe-harbor-subject"|"outside-scope"|"unknown"
Where a party's role places it relative to §164.514(b)(2)(i):
safe-harbor-subject: the individual, a relative, an employer or a household member. Name and identifiers are transformed under the Safe Harbor categories.outside-scope: a party the clause does not reach (provider / facility / payer / payee / submitter / receiver / clearinghouse). Name and identifiers are left in place.unknown: the role could not be established. Fails closed, handled exactly like a subject.
Example
import { type PartyRoleScope } from "@cosyte/deid";
const scope: PartyRoleScope = "outside-scope";
ReportDisposition
ReportDisposition =
"transformed"|"removed"|"blocked"|"retained"
A manifest disposition: the four outcomes an acted-on locus can have. retained covers two kept
things, told apart by the entry's code: an identifying locus the configured profile's retention
set deliberately kept unchanged (DEID_RESIDUAL_RETAINED, which also appears in the residual
inventory below), and a party whose role places it outside §164.514(b)(2)(i)'s scope clause
(DEID_PARTY_ROLE_RETAINED, which does not: it is not a residual of the individual's identity).
RetainedLocusClass
RetainedLocusClass = typeof
RETAINED_LOCUS_CLASSES[keyof typeofRETAINED_LOCUS_CLASSES]
A value from RETAINED_LOCUS_CLASSES: the class a profile lists to keep, and an adapter checks before it passes a locus through.
Example
import { RETAINED_LOCUS_CLASSES, type RetainedLocusClass } from "@cosyte/deid";
const cls: RetainedLocusClass = RETAINED_LOCUS_CLASSES.ENCOUNTER_IDENTIFIERS;
RetainedLocusPart
RetainedLocusPart = typeof
LIMITED_DATA_SET_ADDRESS_PARTS[keyof typeofLIMITED_DATA_SET_ADDRESS_PARTS]
A value from LIMITED_DATA_SET_ADDRESS_PARTS: the named part of an otherwise-excluded category that an adapter proposes keeping, and that isRetainablePart checks.
Example
import { LIMITED_DATA_SET_ADDRESS_PARTS, type RetainedLocusPart } from "@cosyte/deid";
const part: RetainedLocusPart = LIMITED_DATA_SET_ADDRESS_PARTS.STATE;
SafeHarborCategory
SafeHarborCategory = typeof
SAFE_HARBOR_CATEGORIES[keyof typeofSAFE_HARBOR_CATEGORIES]
A value from SAFE_HARBOR_CATEGORIES: the type a policy and a manifest entry carry.
Example
import { SAFE_HARBOR_CATEGORIES, type SafeHarborCategory } from "@cosyte/deid";
const c: SafeHarborCategory = SAFE_HARBOR_CATEGORIES.SSN;
TransformName
TransformName =
"redact"|"generalize"|"date-shift"|"pseudonymize"|"hash"|"block"|"byo-redact"|"retain"
The name of a transform a policy can assign to a category. block is the fail-closed action
(withhold the value); generalize selects the correct generalization from the locus kind (date /
ZIP / age).
byo-redact is not a policy-assignable Safe Harbor transform and not something the library
performs: it is the manifest marker the engine records when a consumer-supplied free-text
redactor redacts a free-text locus. The library bundles no redactor; the
consumer brings the detector. Assigning byo-redact to a category in a policy has no effect beyond
the fail-closed default (the engine blocks it), because free-text redaction is driven by the
redactor option, not by the per-category policy map.
retain is likewise not policy-assignable: it is the manifest marker for a locus the profile's
retention set deliberately kept unchanged, which is driven by that set and not by the
per-category map. Assigning it to a category fails closed to a block, exactly like byo-redact.
Example
import { type TransformName } from "@cosyte/deid";
const t: TransformName = "pseudonymize";
Variables
DEID_DISPOSITION_CODES
constDEID_DISPOSITION_CODES:object
Disposition codes: the value-free record of what the engine did at a locus. Every manifest entry carries exactly one. They describe the action and its residual, never the value acted on.
Type Declaration
DEID_CATEGORY_DATE_SHIFTED
readonlyDEID_CATEGORY_DATE_SHIFTED:"DEID_CATEGORY_DATE_SHIFTED"="DEID_CATEGORY_DATE_SHIFTED"
A date was shifted by a deterministic per-patient offset (interval-preserving).
DEID_CATEGORY_GENERALIZED
readonlyDEID_CATEGORY_GENERALIZED:"DEID_CATEGORY_GENERALIZED"="DEID_CATEGORY_GENERALIZED"
A category was generalized to a fully non-identifying form (ZIP → 000, age → 90+).
DEID_CATEGORY_HASHED
readonlyDEID_CATEGORY_HASHED:"DEID_CATEGORY_HASHED"="DEID_CATEGORY_HASHED"
A value was replaced by a keyed one-way digest (keyed hash).
DEID_CATEGORY_PSEUDONYMIZED
readonlyDEID_CATEGORY_PSEUDONYMIZED:"DEID_CATEGORY_PSEUDONYMIZED"="DEID_CATEGORY_PSEUDONYMIZED"
A category was replaced by a consistent keyed-HMAC surrogate (pseudonymization).
DEID_CATEGORY_REMOVED
readonlyDEID_CATEGORY_REMOVED:"DEID_CATEGORY_REMOVED"="DEID_CATEGORY_REMOVED"
A category was removed outright (redaction).
DEID_FREETEXT_BLOCKED
readonlyDEID_FREETEXT_BLOCKED:"DEID_FREETEXT_BLOCKED"="DEID_FREETEXT_BLOCKED"
Fail-closed: a free-text locus was blocked by default (no naive regex scrub).
DEID_FREETEXT_CONSUMER_REDACTED
readonlyDEID_FREETEXT_CONSUMER_REDACTED:"DEID_FREETEXT_CONSUMER_REDACTED"="DEID_FREETEXT_CONSUMER_REDACTED"
A free-text locus was redacted by a consumer-supplied BYO redactor, not by the library. The library ships no NLP/PHI-detection engine; it orchestrates the consumer's redactor at free-text loci and records the outcome here. This code is consumer-asserted, never a library guarantee: "no findings" from a BYO redactor is not an attestation, and a redactor's completeness is the consumer's responsibility (Expert-Determination territory). The structural PHI removal the format adapters perform is unaffected: this covers only the free prose.
DEID_LOCUS_BLOCKED
readonlyDEID_LOCUS_BLOCKED:"DEID_LOCUS_BLOCKED"="DEID_LOCUS_BLOCKED"
Fail-closed: an unrecognized / un-locatable / uncertain locus was blocked (value withheld).
DEID_PARTY_ROLE_RETAINED
readonlyDEID_PARTY_ROLE_RETAINED:"DEID_PARTY_ROLE_RETAINED"="DEID_PARTY_ROLE_RETAINED"
A party was left in place because the role its format types at that party puts it outside
§164.514(b)(2)(i)'s scope clause (a treating clinician, a facility, a payer, a payee, a submitter,
a receiver, a clearinghouse: not the individual, a relative, an employer or a household member).
The entry names the party's structural locus and, in partyRole, the role code the pass
classified on, so a retention decision that used to be silent can be audited. It carries no name,
no identifier and no other value.
It is deliberately not DEID_RESIDUAL_RETAINED: that code is a residual of the individual's
own identity (a kept year, a safe ZIP prefix, a whole value a limited-data-set retention set kept)
and feeds the determiner's retained-quasi-identifier inventory. A party outside the scope clause is
a different fact and stays out of that inventory. The disposition-code set is additions-only.
DEID_POSITION_UNEXAMINED
readonlyDEID_POSITION_UNEXAMINED:"DEID_POSITION_UNEXAMINED"="DEID_POSITION_UNEXAMINED"
A value-bearing position inside a structure the pass handed through that no locus rule names. The pass reached no decision there: it neither acted on the position, nor blocked it, nor decided to keep it. The record exists so that such a position is counted and located rather than passing through in silence.
It is deliberately not DEID_RESIDUAL_RETAINED, and the difference is the whole point of the
code: that one is a residual of a value the pass examined (a kept year, a safe 3-digit ZIP
prefix, a whole value a retention class kept) and it feeds the determiner's retained-quasi-identifier
inventory. This one is the opposite fact, an unexamined position, and it has its own inventory
so a kept year and a position nothing looked at can never be read as the same thing.
It is a measurement, not an allegation. An unexamined position is not thereby an identifier: a clinical code, a unit and a status all sit at positions no locus rule names. Nothing is scrubbed, removed or generalized on account of this record, and the position has no established Safe Harbor category, because no rule established one. The record carries the structural locus, a count and the fact of being unexamined: never a value, never a key, never an offset. The disposition-code set is additions-only.
DEID_RESIDUAL_RETAINED
readonlyDEID_RESIDUAL_RETAINED:"DEID_RESIDUAL_RETAINED"="DEID_RESIDUAL_RETAINED"
A generalization retained a coarse residual (a kept year, a retained safe 3-digit ZIP prefix). Surfaced so a human can apply the §164.514(b)(2)(ii) actual-knowledge test with the facts present.
Example
import { DEID_DISPOSITION_CODES } from "@cosyte/deid";
DEID_DISPOSITION_CODES.DEID_LOCUS_BLOCKED; // => "DEID_LOCUS_BLOCKED"
EXPERT_DETERMINATION_DISCLAIMER
constEXPERT_DETERMINATION_DISCLAIMER:string
The prominent non-certification statement. It is the first field a reader sees and is repeated at the head of the human-readable rendering. Its job is to make over-claiming impossible to do by accident: the report supports a determination, it is never the determination.
Example
import { EXPERT_DETERMINATION_DISCLAIMER } from "@cosyte/deid";
EXPERT_DETERMINATION_DISCLAIMER.includes("NOT a determination"); // => true
FATAL_CODES
constFATAL_CODES:object
Fatal codes: conditions that abort a de-identification pass by throwing a DeidError. The engine fails closed: it never silently degrades a fatal into a pass-through of PHI.
Type Declaration
DEID_CONTEXT_INVALID
readonlyDEID_CONTEXT_INVALID:"DEID_CONTEXT_INVALID"="DEID_CONTEXT_INVALID"
A DeidContext was configured with an invalid parameter that would silently weaken
de-identification: most importantly a maxShiftDays that floors to 0, which pins every
per-patient date-shift offset to zero, so a date-shift policy would emit the original real
dates under a research label. A no-op shift is a leak, so the engine rejects the degenerate
configuration at construction rather than silently shipping unshifted dates. The fatal set is
additions-only.
DEID_DECLARATION_UNNAMEABLE
readonlyDEID_DECLARATION_UNNAMEABLE:"DEID_DECLARATION_UNNAMEABLE"="DEID_DECLARATION_UNNAMEABLE"
A pass would apply a profile or option that its published coding vocabulary cannot name, so the machine-readable declaration it is required to write could only be an approximation.
Fail closed on the declaration itself. A coded term is read by a downstream system as a property of the document and is acted on without a human, so an approximate code is worse than no output at all: it is a claim about what was removed that nobody re-checks, and a document released on a false coded claim cannot be un-released. A pass that cannot name what it did therefore returns nothing rather than a document stamped with a declaration that is not true of it.
The message names the profile or option (a bounded structural token from the pass's own closed option set, never a value read from the document) and carries no value, no key and no offset. The fatal set is additions-only.
DEID_FORMAT_UNSUPPORTED
readonlyDEID_FORMAT_UNSUPPORTED:"DEID_FORMAT_UNSUPPORTED"="DEID_FORMAT_UNSUPPORTED"
A caller handed an adapter a document in a format that adapter refuses outright, rather than the format its entry point de-identifies.
Refusal, not best effort. An adapter reaches a document only through the parser surface its peer package publishes, and where that surface cannot express a faithful structural pass, a partial pass is a false-safety hazard: it would return a document a consumer reads as Safe-Harbor-transformed while positions the surface never modelled rode straight through it. Refusing is the fail-closed answer and it is the one a test can pin; a documented non-goal in prose is not, because prose is not a behaviour.
The message names the format and the parser-surface reason, both fixed text this library owns. It carries no value read from the document, no key and no offset, and the pass returns no transformed document, no manifest and no partial output of any kind. The fatal set is additions-only.
DEID_NO_KEY
readonlyDEID_NO_KEY:"DEID_NO_KEY"="DEID_NO_KEY"
A keyed transform (pseudonymize / keyed-hash / date-shift) was required for a category present in the model, but no key (or, for date-shift, no per-patient scope) was supplied. The engine never falls back to an unkeyed transform: an unkeyed hash of an identifier is re-identifiable.
DEID_OUTPUT_INVALID
readonlyDEID_OUTPUT_INVALID:"DEID_OUTPUT_INVALID"="DEID_OUTPUT_INVALID"
The transformed document could not be re-serialized, or no longer round-trips through its own parser: what came back out of the writer is not what the reader reads. A de-identification pass that cannot vouch for the shape of its own output cannot vouch for what a downstream reader will make of the values inside it, so the pass fails rather than hand back a partially transformed document. The fatal set is additions-only.
DEID_POLICY_INVALID
readonlyDEID_POLICY_INVALID:"DEID_POLICY_INVALID"="DEID_POLICY_INVALID"
A policy violates the key/label contract: whatever claims the reserved safe-harbor label,
either as a policy name or as a profile's declared standard, assigns a category a
transform whose output for that category is derived from that category's own value. A
shifted-but-real date is still "an element of a date" under §164.514(b)(2)(i)(C), and a keyed
surrogate of a medical record, beneficiary or account number is a code "derived from ...
information about the individual", which §164.514(c)(1) does not permit and the (R) exception
therefore does not reach. Both are Expert-Determination techniques, not Safe Harbor ones, and
labelling either safe-harbor would misrepresent the residual risk.
The refusal names the offending category and transform, carries no value / key / offset, and fires both at mint time and at point of use, so a hand-built policy object cannot slip past. It also covers an assignment that is not a published transform name at all: a pair whose derivation cannot be established is refused, never permitted. A policy that does not claim the label is entitled to its keyed surrogate and is applied unchanged. The fatal set is additions-only.
DEID_POSITIONS_UNENUMERABLE
readonlyDEID_POSITIONS_UNENUMERABLE:"DEID_POSITIONS_UNENUMERABLE"="DEID_POSITIONS_UNENUMERABLE"
The value-bearing positions of a structure the pass would hand through could not be enumerated, so the pass cannot say how much of that structure it never examined.
Fail closed on the measurement itself. The alternative outcomes are both worse than a failure: a count of zero would tell a determiner the structure held nothing unexamined, and a partial count would understate it, and either is read as a measurement rather than as a gap. A pass that cannot enumerate a structure therefore returns nothing at all rather than a number nobody can qualify.
The message names the structure (a bounded structural token: a segment identifier, an element name, a tag) and carries no value, no key and no offset. The fatal set is additions-only.
DEID_PROFILE_INVALID
readonlyDEID_PROFILE_INVALID:"DEID_PROFILE_INVALID"="DEID_PROFILE_INVALID"
A DeidProfile spec violates the widen-never-narrow contract: a per-site profile derived from a base profile may only move a category to an equal-or-stronger transform (more removal, never less), and may never re-weaken a category the base scrubs. A profile that would reduce the de-identification strength of any category is rejected, so a site preset can only ever tighten, not quietly loosen, the base standard's protection. The fatal set is additions-only.
EMPTY_INPUT
readonlyEMPTY_INPUT:"EMPTY_INPUT"="EMPTY_INPUT"
The input model was null/undefined or carried no locus list, nothing to de-identify.
Example
import { FATAL_CODES } from "@cosyte/deid";
FATAL_CODES.DEID_NO_KEY; // => "DEID_NO_KEY"
KEYED_TRANSFORMS
constKEYED_TRANSFORMS:ReadonlySet<TransformName>
The transforms that require the consumer's key (and, for date-shift, a per-patient scope).
LIMITED_DATA_SET_ADDRESS_PARTS
constLIMITED_DATA_SET_ADDRESS_PARTS:object
The three parts of a postal address §164.514(e)(2)(ii) names as surviving a limited data set, in the regulation's own words: "other than town or city, State, and zip code". Nothing else in an address is named, so nothing else survives: not the street, not a second address line, not the county or parish, not the census tract, not the country, not a birth place.
Type Declaration
STATE
readonlySTATE:"state"="state"
"State": the state or province the address sits in.
TOWN_OR_CITY
readonlyTOWN_OR_CITY:"town-or-city"="town-or-city"
"town or city": the populated place, and nothing finer.
ZIP_CODE
readonlyZIP_CODE:"zip-code"="zip-code"
"zip code": the WHOLE zip code. The three-digit rule is Safe Harbor's, not (e)(2)'s.
Example
import { LIMITED_DATA_SET_ADDRESS_PARTS } from "@cosyte/deid";
LIMITED_DATA_SET_ADDRESS_PARTS.ZIP_CODE; // => "zip-code"
LIMITED_DATA_SET_DIRECT_IDENTIFIERS
constLIMITED_DATA_SET_DIRECT_IDENTIFIERS:ReadonlySet<SafeHarborCategory>
The sixteen direct identifiers §164.514(e)(2) excludes from a limited data set, mapped onto this library's category model: (i) names, (ii) postal address other than town/city/State/ZIP, (iii) telephone, (iv) fax, (v) email, (vi) social security, (vii) medical record, (viii) health plan beneficiary, (ix) account, (x) certificate/licence, (xi) vehicle, (xii) device, (xiii) URL, (xiv) IP, (xv) biometric, (xvi) full-face image.
This is the guard that makes the retention citation true rather than merely asserted. The argument for keeping an encounter or order number is that this list has no catch-all; the argument for keeping a service date is that it has no date. Neither argument survives if the value at the locus turns out to be one of the sixteen -- and a visit number field routinely carries a medical record or account number, typed as such by the standard's own identifier-type code. So retention is refused whenever the resolved category is on this list, whatever an adapter asked for, in both the adapter and the engine.
Exactly two of the eighteen Safe Harbor categories are absent from it: DATES and the (R) catch-all
OTHER_UNIQUE_ID. Those two, and only those two, are retainable.
Example
import { LIMITED_DATA_SET_DIRECT_IDENTIFIERS, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
LIMITED_DATA_SET_DIRECT_IDENTIFIERS.has(SAFE_HARBOR_CATEGORIES.MRN); // => true (never retainable)
LIMITED_DATA_SET_DIRECT_IDENTIFIERS.has(SAFE_HARBOR_CATEGORIES.DATES); // => false (retainable)
LIMITED_DATA_SET_PROFILE
constLIMITED_DATA_SET_PROFILE:DeidProfile
The Limited Data Set / longitudinal research profile. Identical to Safe Harbor except that
dates are date-shifted (a single consistent per-patient offset, intervals preserved) rather than
generalized to year, and that the medical record, health plan beneficiary and account numbers are
replaced by a consistent keyed surrogate rather than removed, so cross-document linkage
survives. Both are keyed transforms, and both are why this preset does not, and may not, claim Safe
Harbor: a surrogate derived from the individual's own value is a re-identification code
§164.514(c)(1) does not permit. Every such locus is flagged reidentificationCode in the manifest
and listed in the support report's keyed-surrogate residual inventory.
It also keeps, unchanged, the three classes of identifying locus §164.514(e)(2) permits a limited
data set to carry and Safe Harbor does not: encounter dates (admission / discharge / service /
diagnosis), encounter and order identifiers (visit number, placer and filler order numbers), and
the postal address parts (e)(2)(ii) names: town or city, State and the whole zip code, with
the street address and every other geographic component removed. That list of sixteen direct
identifiers names no date and has no catch-all, which is precisely why the first two survive
here and are removed under Safe Harbor; (e)(2)(ii) is its only partial exclusion, which is why
the third does. The three-digit / 000 ZIP rule is §164.514(b)(2)(i)(B), Safe Harbor's, and has
no (e)(2) counterpart, so a restricted-prefix ZIP is kept in full here too. Each kept locus is still
recorded as a DEID_RESIDUAL_RETAINED residual, located to its own component, so it reaches the
determiner's residual inventory.
The geographic allowance is honoured by the HL7 v2 pass alone. The C-CDA, FHIR, X12, NCPDP and DICOM adapters do not read retention classes, so under those five an address is reduced exactly as Safe Harbor reduces it. The direction is the safe one and it is stated rather than left to be discovered.
On dates the preset is deliberately STRICTER than §164.514(e)(2). The list of sixteen names no date, so a limited data set may carry dates at full precision; this preset date-shifts them anyway, by choice. Removing more than the regulation requires is always lawful, and the alternative would hand every existing consumer real patient dates on an upgrade, which is the direction no re-run undoes.
This is deliberately less protective than Safe Harbor and is NOT Safe Harbor. A shifted-but-real date is still "an element of a date" (§164.514(b)(2)(i)(C)), so this profile:
- is not labelled
safe-harbor(the reserved-label guard would reject it); - requires a keyed per-patient DeidContext (an absent key is a fatal
DEID_NO_KEY); - produces an Expert-Determination-supporting dataset, not a certified de-identification, and not, on its own, a HIPAA §164.514(e) Limited Data Set: disclosing an actual Limited Data Set additionally requires a Data Use Agreement, which is the consumer's responsibility.
Example
import { LIMITED_DATA_SET_PROFILE } from "@cosyte/deid";
LIMITED_DATA_SET_PROFILE.requiresContext; // => true (date-shift needs a per-patient key)
LIMITED_DATA_SET_RETENTION_CLASSES
constLIMITED_DATA_SET_RETENTION_CLASSES:ReadonlySet<RetainedLocusClass>
The retention classes a profile declaring the limited-data-set standard may carry: exactly
those §164.514(e)(2) leaves out of its sixteen direct identifiers, plus the parts (e)(2)(ii) names.
The list has no date and no catch-all, so encounter dates and encounter / order identifiers survive it; (e)(2)(ii) is a partial exclusion, so the three named address parts survive it. A class outside this set is a claim the regulation does not support, and assertLimitedDataSetRetention refuses it rather than letting the profile wear the name.
Example
import { LIMITED_DATA_SET_RETENTION_CLASSES, RETAINED_LOCUS_CLASSES } from "@cosyte/deid";
LIMITED_DATA_SET_RETENTION_CLASSES.has(RETAINED_LOCUS_CLASSES.ENCOUNTER_DATES); // => true
NO_RETAINED_LOCI
constNO_RETAINED_LOCI: readonlyRetainedLocusClass[]
The empty retention set: the fail-closed default every entry point uses.
OUTPUT_LABEL
constOUTPUT_LABEL:"Safe-Harbor-transformed per the configured policy"="Safe-Harbor-transformed per the configured policy"
The label the library applies to its output. Deliberately not "de-identified" / "HIPAA-compliant": the certification is always the consumer's.
Example
import { OUTPUT_LABEL } from "@cosyte/deid";
OUTPUT_LABEL; // => "Safe-Harbor-transformed per the configured policy"
RESTRICTED_ZIP3
constRESTRICTED_ZIP3:ReadonlySet<string>
The 17 restricted three-digit ZIP prefixes (population ≤ 20,000 per 2000 Census). A ZIP whose first
three digits are in this set is generalized to 000; any other prefix retains its three digits.
Example
import { RESTRICTED_ZIP3 } from "@cosyte/deid";
RESTRICTED_ZIP3.has("036"); // => true
RESTRICTED_ZIP3.has("902"); // => false
RESTRICTED_ZIP3_SOURCE
constRESTRICTED_ZIP3_SOURCE:Readonly<{census:"2000";citation:"HHS OCR, Guidance Regarding Methods for De-identification of PHI (2012); 45 CFR §164.514(b)(2)(i)(B)"; }>
The Census vintage this list is grounded in, surfaced so a consumer knows exactly which published
artifact the 000 rule is applied from.
Example
import { RESTRICTED_ZIP3_SOURCE } from "@cosyte/deid";
RESTRICTED_ZIP3_SOURCE.census; // => "2000"
RETAINED_LOCUS_CLASSES
constRETAINED_LOCUS_CLASSES:object
The stable registry of retention classes. key === value so the full set survives an
Object.values(...) snapshot into a stability tripwire. These are part of the public contract:
renaming or removing one is a breaking change; new classes may be added in a later release.
Type Declaration
ENCOUNTER_DATES
readonlyENCOUNTER_DATES:"encounter-dates"="encounter-dates"
Patient-related dates carried by retained clinical / visit structures: admission, discharge, observation / service, and diagnosis dates. These are elements of dates directly related to the individual, so Safe Harbor removes them (only the year may remain); a limited data set may keep them, because §164.514(e)(2)'s direct-identifier list names no date.
ENCOUNTER_IDENTIFIERS
readonlyENCOUNTER_IDENTIFIERS:"encounter-identifiers"="encounter-identifiers"
Encounter and order identifiers: the visit / encounter number, and the placer and filler order numbers. These are not one of the seventeen concrete Safe Harbor identifier types, so Safe Harbor reaches them through the (R) catch-all and they are blocked; §164.514(e)(2) has no catch-all, so a limited data set may keep them.
LIMITED_DATA_SET_GEOGRAPHY
readonlyLIMITED_DATA_SET_GEOGRAPHY:"limited-data-set-geography"="limited-data-set-geography"
The named parts of a postal address §164.514(e)(2)(ii) permits a limited data set to carry. The clause excludes "Postal address information, other than town or city, State, and zip code", which makes it the only PARTIAL exclusion in the list of sixteen: three named parts survive and everything else in the address does not.
What survives: town or city, State, and the whole zip code. The digit limit and the
000 substitution are §164.514(b)(2)(i)(B), which is Safe Harbor's rule; (e)(2) states no
digit limit and no population condition, so the ZIP is kept in full here.
What does not: the street address, any second address line, the county or parish, the census tract or other geographic designation, the country, and a birth place. None of those is named by the clause, so none of them is widened by this class.
It permits PARTS, never the category. GEOGRAPHIC stays on
LIMITED_DATA_SET_DIRECT_IDENTIFIERS and isRetainableCategory still returns
false for it, because a partial exclusion is not a whole-category one. The only route past
that guard is isRetainablePart, which is keyed on this class, that category and one of
the three names above.
Scope, stated rather than implied: only the HL7 v2 adapter reads this class today, like every other. Under the C-CDA, FHIR, X12, NCPDP and DICOM adapters an address is reduced exactly as it is without it (the Safe Harbor generalization), which is the stricter direction.
Example
import { RETAINED_LOCUS_CLASSES } from "@cosyte/deid";
RETAINED_LOCUS_CLASSES.ENCOUNTER_DATES; // => "encounter-dates"
SAFE_HARBOR_CATEGORIES
constSAFE_HARBOR_CATEGORIES:object
The stable registry of the 18 Safe Harbor identifier categories. key === value so the set
survives an Object.values(...) snapshot into a stability tripwire. Renaming a category is a
breaking change: consumers branch on these in policies and manifests.
Type Declaration
ACCOUNT
readonlyACCOUNT:"ACCOUNT"="ACCOUNT"
(J) Account numbers.
BIOMETRIC
readonlyBIOMETRIC:"BIOMETRIC"="BIOMETRIC"
(P) Biometric identifiers, including finger and voice prints.
CERTIFICATE_LICENSE
readonlyCERTIFICATE_LICENSE:"CERTIFICATE_LICENSE"="CERTIFICATE_LICENSE"
(K) Certificate / license numbers.
DATES
readonlyDATES:"DATES"="DATES"
(C) All elements of dates (except year) directly related to the individual, and all ages > 89.
DEVICE
readonlyDEVICE:"DEVICE"="DEVICE"
(M) Device identifiers and serial numbers.
EMAIL
readonlyEMAIL:"EMAIL"="EMAIL"
(F) Email addresses.
FAX
readonlyFAX:"FAX"="FAX"
(E) Fax numbers.
FULL_FACE_PHOTO
readonlyFULL_FACE_PHOTO:"FULL_FACE_PHOTO"="FULL_FACE_PHOTO"
(Q) Full-face photographs and any comparable images.
GEOGRAPHIC
readonlyGEOGRAPHIC:"GEOGRAPHIC"="GEOGRAPHIC"
(B) All geographic subdivisions smaller than a state: street, city, county, precinct, ZIP, geocodes.
HEALTH_PLAN_BENEFICIARY
readonlyHEALTH_PLAN_BENEFICIARY:"HEALTH_PLAN_BENEFICIARY"="HEALTH_PLAN_BENEFICIARY"
(I) Health plan beneficiary numbers.
IP_ADDRESS
readonlyIP_ADDRESS:"IP_ADDRESS"="IP_ADDRESS"
(O) IP addresses.
MRN
readonlyMRN:"MRN"="MRN"
(H) Medical record numbers.
NAMES
readonlyNAMES:"NAMES"="NAMES"
(A) Names: patient, relatives, employers, household members.
OTHER_UNIQUE_ID
readonlyOTHER_UNIQUE_ID:"OTHER_UNIQUE_ID"="OTHER_UNIQUE_ID"
(R) Any other unique identifying number, characteristic, or code (the open-ended catch-all).
PHONE
readonlyPHONE:"PHONE"="PHONE"
(D) Telephone numbers.
SSN
readonlySSN:"SSN"="SSN"
(G) Social Security numbers.
URL
readonlyURL:"URL"="URL"
(N) Web URLs.
VEHICLE
readonlyVEHICLE:"VEHICLE"="VEHICLE"
(L) Vehicle identifiers and serial numbers, including license plates.
Example
import { SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
SAFE_HARBOR_CATEGORIES.MRN; // => "MRN"
SAFE_HARBOR_CATEGORY_META
constSAFE_HARBOR_CATEGORY_META:Readonly<Record<SafeHarborCategory, {letter:string;number:number;title:string; }>>
Per-category regulatory metadata: the §164.514(b)(2)(i) sub-paragraph letter (A–R), the ordinal number (1–18), and a short human title. Grounded firsthand in the regulation text; contains no PHI.
Example
import { SAFE_HARBOR_CATEGORY_META, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
SAFE_HARBOR_CATEGORY_META[SAFE_HARBOR_CATEGORIES.GEOGRAPHIC].letter; // => "B"
SAFE_HARBOR_POLICY
constSAFE_HARBOR_POLICY:DeidPolicy
The built-in Safe Harbor policy. Direct identifiers with no analytic value are redacted, and
that includes the medical record, health plan beneficiary and account numbers: their value is
removed, never replaced by a keyed surrogate. Geography and dates are generalized; the
open-ended catch-all (R) is blocked (fail-closed). Dates generalize to year, and a keyed
surrogate of an identifier is derived from information about the individual, so both date-shift and
pseudonymization are Expert-Determination techniques rather than Safe Harbor ones. A consumer who
needs a consistent keyed surrogate for those three categories uses
LIMITED_DATA_SET_PROFILE, which does not claim Safe Harbor.
Example
import { SAFE_HARBOR_POLICY, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
SAFE_HARBOR_POLICY.transforms[SAFE_HARBOR_CATEGORIES.MRN]; // => "redact"
SAFE_HARBOR_PROFILE
constSAFE_HARBOR_PROFILE:DeidProfile
The Safe Harbor profile: the fail-closed default. Wraps SAFE_HARBOR_POLICY: direct identifiers removed, medical record / beneficiary / account numbers removed rather than replaced by a keyed surrogate, geography and dates generalized, the catch-all (R) blocked. It therefore needs no key at all. Output is "Safe-Harbor-transformed per the configured policy", never "de-identified".
Example
import { SAFE_HARBOR_PROFILE } from "@cosyte/deid";
SAFE_HARBOR_PROFILE.standard; // => "safe-harbor"
VERSION
constVERSION:string="0.1.0"
Library version string, synced with package.json#version at release time.
Example
import { VERSION } from "@cosyte/deid";
typeof VERSION; // => "string"
WITHHELD_LOCUS_TOKEN
constWITHHELD_LOCUS_TOKEN:"<withheld>"="<withheld>"
What a manifest locus prints in place of an identifier it may not echo, i.e. an identifier the adapter read out of the document that does not match the shape its position promises.
Deliberately carries no length and no prefix of the refused token: the length of a refused identifier is itself derived from the content that was refused.
Example
import { WITHHELD_LOCUS_TOKEN } from "@cosyte/deid";
// A manifest entry whose position could not be trusted reads, e.g., "<withheld>-1".
WITHHELD_LOCUS_TOKEN; // => "<withheld>"
Functions
assertLimitedDataSetRetention()
assertLimitedDataSetRetention(
subject,retainedLoci):void
Enforce §164.514(e)(2) on a profile that declares the limited-data-set standard: every class
it retains must be one LIMITED_DATA_SET_RETENTION_CLASSES permits. A profile carrying the
regulation's name while keeping something the regulation excludes is refused, naming the class,
before any locus is transformed.
Parameters
subject
string
What is being checked, for the diagnostic (a profile, a derivation base).
retainedLoci
readonly RetainedLocusClass[] | undefined
The retention classes the profile carries, if any.
Returns
void
Throws
DeidError DEID_PROFILE_INVALID naming every class outside the permitted set.
Example
import { assertLimitedDataSetRetention } from "@cosyte/deid";
assertLimitedDataSetRetention("a profile", ["encounter-dates"]); // ok
assertRetentionContract()
assertRetentionContract(
policyName,retainedLoci):void
Enforce the label contract on retention, failing closed: a policy carrying the reserved
safe-harbor label may not run with a non-empty retention set. Retaining an admission date or an
encounter number is strictly weaker than the transform the label promises, so allowing it would let
an options bag emit a Safe-Harbor-labelled result that is not Safe Harbor. This is the retention
analogue of the guard that stops a date-shifting policy wearing the same label, and it closes the
hand-built-options route that no profile check can see.
Parameters
policyName
string
The name of the resolved policy.
retainedLoci
readonly RetainedLocusClass[] | undefined
The retention classes the options bag carries, if any.
Returns
void
Throws
DeidError DEID_POLICY_INVALID when a safe-harbor-labelled policy is asked to retain.
Example
import { assertRetentionContract } from "@cosyte/deid";
assertRetentionContract("limited-data-set", ["encounter-dates"]); // ok
assertRetentionContract("safe-harbor", []); // ok (retains nothing)
buildExpertDeterminationSupportReport()
buildExpertDeterminationSupportReport(
manifests,options?):ExpertDeterminationSupportReport
Build the ExpertDeterminationSupportReport from the value-free manifest(s) of one or more de-identification passes. Deterministic; the input is never mutated; the result is deeply frozen.
Accepts either a single manifest (readonly DeidManifestEntry[]) or a corpus (an array of
manifests). Counts for identical loci are summed across the corpus. The report is value-free: it
carries categories, dispositions, loci, and counts, never a PHI value, and it renders no
determination: determination is null and the disclaimer leads.
Parameters
manifests
readonly DeidManifestEntry[] | readonly readonly DeidManifestEntry[][]
A single manifest, or an array of manifests (a corpus).
options?
ExpertDeterminationReportOptions = {}
The policy label and optional consumer-supplied quasi-identifier class sizes.
Returns
ExpertDeterminationSupportReport
The frozen, value-free support report.
Example
import { deidentify, buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const { manifest } = deidentify(
{ loci: [{ path: "PID-5", kind: "name", category: SAFE_HARBOR_CATEGORIES.NAMES, value: "X" }] },
{},
);
const report = buildExpertDeterminationSupportReport(manifest, { policy: "safe-harbor" });
report.determination; // => null
report.policy; // => "safe-harbor"
classifyPartyRole()
classifyPartyRole(
roleCode,table):PartyRoleClassification
Classify a party from the role code its format types at the party, against that format's own
PartyRoleTable. Comparison is trimmed and upper-cased, so a lower-case wire code resolves to
the same committed member. An absent, empty or unlisted code is unknown and fails closed.
Parameters
roleCode
string
The role code the format types at the party (NM1-01 / N1-01; the role an HL7
v2.5.1 field definition names at an organisation-typed position).
table
The format's committed subject / outside-scope role lists.
Returns
The classification, carrying the table's own code when the role was recognized.
Example
import { classifyPartyRole } from "@cosyte/deid";
const table = { subject: new Set(["36"]), outside: new Set(["85"]) };
classifyPartyRole("36", table); // => { scope: "safe-harbor-subject", roleCode: "36" }
classifyPartyRole("zq", table); // => { scope: "unknown" } (fails closed)
createDeidContext()
createDeidContext(
spec):DeidContext
Create a DeidContext from consumer-held key material. The key is coerced to bytes and stored only in the module-private registry, never on the returned handle.
Parameters
spec
The key, optional date-shift seed, optional patient scope, and offset bound.
Returns
An opaque, self-redacting context.
Throws
DeidError with code DEID_NO_KEY if the key (or an explicit seed) is empty.
Example
import { createDeidContext } from "@cosyte/deid";
const ctx = createDeidContext({ key: "consumer-secret", patientId: "patient-1" });
createDeidRegistry()
createDeidRegistry(
spec):DeidRegistry
Create a DeidRegistry from consumer-held key material. Fails closed on an absent/empty
key: there is no default or weak key (the underlying createDeidContext throws DEID_NO_KEY).
The key is stored only in the module-private state, never on the returned handle.
Parameters
spec
The key, optional date-shift seed, and optional offset bound.
Returns
An opaque, self-redacting corpus consistency handle.
Throws
DeidError with code DEID_NO_KEY if the key (or an explicit seed) is empty.
Example
import { createDeidRegistry } from "@cosyte/deid";
const registry = createDeidRegistry({ key: "consumer-secret" });
dateShift()
dateShift(
value,ctx):string|null
Shift a date by the context's deterministic per-patient offset, preserving intervals. Supports ISO
YYYY-MM-DD, HL7 YYYYMMDD, and an ISO datetime (YYYY-MM-DDThh:mm…). Fails closed (null) on an
unparseable or invalid date.
Timezone-independent. Only the calendar-date portion is shifted (via UTC calendar math); a
datetime's time-of-day and zone designator are preserved verbatim, so the same input yields the
same output on every host regardless of the machine's TZ. Because the offset is a whole number of
days and the clock/zone are untouched, intervals are preserved exactly.
The offset is not returned: only the shifted value is. Two dates for the same patient move by the same amount, so the number of days between them is unchanged.
Parameters
value
string
The date value to shift.
ctx
The de-identification context (must carry a patientId scope for the offset).
Returns
string | null
The shifted date at the original precision, or null if it could not be parsed.
Throws
DeidError with code DEID_NO_KEY if the context has no per-patient scope.
Example
import { dateShift, createDeidContext } from "@cosyte/deid";
const ctx = createDeidContext({ key: "secret", patientId: "patient-1" });
const a = dateShift("2020-01-01", ctx);
const b = dateShift("2020-01-11", ctx);
// a and b are shifted, but remain exactly 10 days apart.
defineDeidPolicy()
defineDeidPolicy(
spec):DeidPolicy
Derive a custom policy from the Safe Harbor defaults. Unlisted categories keep the safe default, so a custom policy can only ever be built by deviating from Safe Harbor deliberately, never by forgetting a category. The result is frozen.
Parameters
spec
The policy name and per-category overrides.
Returns
A frozen DeidPolicy covering all 18 categories.
Example
import { defineDeidPolicy, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const research = defineDeidPolicy({
name: "research",
transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" },
});
research.transforms[SAFE_HARBOR_CATEGORIES.NAMES]; // => "redact" (kept from Safe Harbor)
defineDeidProfile()
defineDeidProfile(
spec):DeidProfile
Derive a per-site DeidProfile from a base profile (Safe Harbor by default), enforcing the widen-never-narrow contract: every override must move its category to an equal-or-stronger transform than the base's. A weakening override, or reclaiming a reserved standard label, is rejected (FATAL_CODES.DEID_PROFILE_INVALID), so a site preset can only tighten, never loosen, the base standard's protection.
Parameters
spec
The profile name, base, per-category overrides, and optional redactor.
Returns
A frozen DeidProfile.
Throws
DeidError DEID_PROFILE_INVALID if an override weakens a category, if
retainedLoci adds a retention class the base does not retain, or if the name reclaims a reserved
standard label; DEID_POLICY_INVALID if the base declares the safe-harbor standard while
carrying a derived-output pair, or if the derived policy violates the key/label contract (e.g. a
safe-harbor-labelled policy that date-shifts). A base refused at derive time is refused with
DEID_PROFILE_INVALID first: a profile that is refused never mints a policy to label.
Example
import { defineDeidProfile, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const strict = defineDeidProfile({
name: "site-strict",
transforms: { [SAFE_HARBOR_CATEGORIES.GEOGRAPHIC]: "redact" }, // generalize -> redact: OK
});
strict.policy.transforms[SAFE_HARBOR_CATEGORIES.GEOGRAPHIC]; // => "redact"
deidentify()
deidentify(
model,options):DeidResult
De-identify a format-agnostic LocusModel under a policy. Returns the transformed document and a value-free manifest. The input model is never mutated; the result is deeply frozen.
The output is "Safe-Harbor-transformed per the configured policy": it is not certified de-identified, and Expert Determination is not rendered.
Parameters
model
The located candidate values to de-identify.
loci?
readonly GenericLocus[]
options
The policy and (for keyed transforms) the key context.
Returns
The frozen DeidResult: transformed document + value-free manifest.
Throws
DeidError EMPTY_INPUT if the model is null or carries no locus list; DEID_NO_KEY
if a keyed transform is required but no key context was supplied; DEID_POLICY_INVALID if a
safe-harbor-labelled policy is asked to retain an identifying locus.
Example
import { deidentify, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
const result = deidentify(
{ loci: [{ path: "PID-19", kind: "identifier", category: SAFE_HARBOR_CATEGORIES.SSN, value: "SENTINEL" }] },
{},
);
result.document.loci[0]?.value; // => null (removed)
result.manifest[0]?.disposition; // => "removed"
formatExpertDeterminationSupportReport()
formatExpertDeterminationSupportReport(
report):string
Render an ExpertDeterminationSupportReport as a human-readable Markdown document: the same value-free facts as the structured object, led by the non-certification disclaimer. Suitable to hand to a statistician alongside the machine-readable report.
Parameters
report
ExpertDeterminationSupportReport
A report from buildExpertDeterminationSupportReport.
Returns
string
A Markdown string (value-free: categories, dispositions, loci, counts, never a value).
Example
import { buildExpertDeterminationSupportReport, formatExpertDeterminationSupportReport } from "@cosyte/deid";
const md = formatExpertDeterminationSupportReport(buildExpertDeterminationSupportReport([]));
md.startsWith("# Expert-Determination support report"); // => true
generalizeAge()
generalizeAge(
age):GeneralizeOutcome|null
Generalize an age (§164.514(b)(2)(i)(C)): any age over 89 aggregates to "90+"; ages 0–89 are
retained (a residual). Fails closed (null) for a non-finite or negative age.
Parameters
age
number
The age in years.
Returns
GeneralizeOutcome | null
The { value, residual } outcome (residual: false for "90+", true for a kept age),
or null if the age is not a finite non-negative number.
Example
import { generalizeAge } from "@cosyte/deid";
generalizeAge(92)?.value; // => "90+"
generalizeAge(89)?.value; // => "89"
generalizeDate()
generalizeDate(
value):GeneralizeOutcome|null
Generalize a date to its year (§164.514(b)(2)(i)(C)). Accepts common encodings: ISO
YYYY-MM-DD / YYYY-MM-DDThh:mm:ss, HL7 YYYYMMDD, anything beginning with a plausible
four-digit year. Fails closed (null) when no year can be extracted; the retained year is a
residual.
Parameters
value
string
The date value to generalize.
Returns
GeneralizeOutcome | null
The { value: year, residual: true } outcome, or null if no year is present.
Example
import { generalizeDate } from "@cosyte/deid";
generalizeDate("2019-03-14")?.value; // => "2019"
generalizeDate("not-a-date"); // => null
generalizeZip()
generalizeZip(
zip):GeneralizeOutcome|null
Generalize a ZIP code to its safe form (§164.514(b)(2)(i)(B)): the initial three digits, or 000
when those three digits name an area with ≤ 20,000 people (the cited restricted list). Fails closed
(null) when three leading digits cannot be read.
Parameters
zip
string
The ZIP code (5-digit, ZIP+4, or any form beginning with digits).
Returns
GeneralizeOutcome | null
The { value, residual } outcome (residual: false for 000, true for a kept prefix),
or null if fewer than three leading digits are present.
Example
import { generalizeZip } from "@cosyte/deid";
generalizeZip("90210")?.value; // => "902"
generalizeZip("03601")?.value; // => "000" (036 is a restricted prefix)
isRetainableCategory()
isRetainableCategory(
category):boolean
Whether a locus of this category may be retained at all. false for every one of the sixteen direct
identifiers LIMITED_DATA_SET_DIRECT_IDENTIFIERS names, whatever retention class an adapter
attached to it.
Parameters
category
The resolved Safe Harbor category of the locus.
Returns
boolean
true only for DATES and the (R) catch-all.
Example
import { isRetainableCategory, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";
isRetainableCategory(SAFE_HARBOR_CATEGORIES.MRN); // => false
isRetainableCategory(SAFE_HARBOR_CATEGORIES.DATES); // => true
isRetainablePart()
isRetainablePart(
cls,category,part):boolean
Whether a named part of a locus whose category is otherwise excluded may be retained. This is the only route past isRetainableCategory, and it is narrow by construction: all three of the class, the resolved category and the part name must line up, and each is an allow-list membership rather than a negation.
It exists because §164.514(e)(2)(ii) is the list's only partial exclusion. Making GEOGRAPHIC
retainable as a category would keep a county code, a birth place and a street address along with
the three parts the clause names, so the category stays excluded and the parts are named here.
Parameters
cls
The retention class the adapter proposed for the locus.
category
The resolved Safe Harbor category of the locus.
part
The named part the adapter proposed keeping.
Returns
boolean
true only for the geographic class, the GEOGRAPHIC category, and a named address part.
Example
import {
isRetainablePart,
LIMITED_DATA_SET_ADDRESS_PARTS,
RETAINED_LOCUS_CLASSES,
SAFE_HARBOR_CATEGORIES,
} from "@cosyte/deid";
const geo = RETAINED_LOCUS_CLASSES.LIMITED_DATA_SET_GEOGRAPHY;
isRetainablePart(geo, SAFE_HARBOR_CATEGORIES.GEOGRAPHIC, LIMITED_DATA_SET_ADDRESS_PARTS.STATE); // => true
isRetainablePart(geo, SAFE_HARBOR_CATEGORIES.NAMES, LIMITED_DATA_SET_ADDRESS_PARTS.STATE); // => false
isRetainableZipCode()
isRetainableZipCode(
zip):boolean
Whether a value is a whole zip code this library will carry through unreduced under RETAINED_LOCUS_CLASSES.LIMITED_DATA_SET_GEOGRAPHY: five digits, optionally followed by the ZIP+4 add-on with or without its hyphen. Nothing else, and no surrounding whitespace.
It is deliberately stricter than the generalization's input rule, and the asymmetry is the point: generalization reduces whatever it is given until it is safe, so reading three leading digits off a malformed value is safe. Retention emits the value unreduced, so a value whose shape this library cannot vouch for is not retained at all: the locus falls back to the Safe Harbor generalization, which fails closed to dropping the whole address.
Parameters
zip
string
The candidate zip code, exactly as it sits at the locus.
Returns
boolean
true only for 12345, 12345-6789 or 123456789.
Example
import { isRetainableZipCode } from "@cosyte/deid";
isRetainableZipCode("62704"); // => true
isRetainableZipCode("627"); // => false (a partial ZIP is not a zip code)
keyedHash()
keyedHash(
value,ctx):string
Replace a value with a keyed one-way digest (HMAC-SHA-256). Like pseudonymize it is consistent and non-reversible without the key, but domain-separated so it is a distinct surrogate space from pseudonyms.
Parameters
value
string
The value to hash.
ctx
The de-identification context holding the consumer's key.
Returns
string
A lowercase-hex keyed digest, consistent per (key, value).
Example
import { keyedHash, createDeidContext } from "@cosyte/deid";
const ctx = createDeidContext({ key: "secret" });
typeof keyedHash("value", ctx); // => "string"
profileOptions()
profileOptions(
profile,context?,overrides?):DeidOptions
Build the DeidOptions to pass to any adapter (deidentifyHl7, deidentifyFhir, …) from a
profile: its policy, its retention set, the supplied key context, and the profile's default redactor
(unless overridden). Going through here is what carries the retention set: an options bag built
by hand from profile.policy alone retains nothing, which is the fail-closed direction.
It is also where the label contract is enforced on a profile that DECLARES the safe-harbor
standard: a profile claiming that standard while assigning a category a transform whose output is
derived from that category's own value is refused here, before any locus is transformed, because
the engine downstream sees only the policy and not the standard the profile claimed.
Parameters
profile
The profile to apply.
context?
The keyed per-patient context (required by profiles whose requiresContext is true).
overrides?
Optional context/redactor overrides merged over the profile's defaults.
context?
redactor?
Returns
The DeidOptions for an adapter call.
Throws
DeidError DEID_POLICY_INVALID when the profile claims the safe-harbor label, on
either its policy name or its declared standard, while carrying a derived-output pair.
Example
import { SAFE_HARBOR_PROFILE, profileOptions, createDeidContext } from "@cosyte/deid";
const opts = profileOptions(SAFE_HARBOR_PROFILE, createDeidContext({ key: "k" }));
opts.policy === SAFE_HARBOR_PROFILE.policy; // => true
pseudonymize()
pseudonymize(
id,ctx):string
Replace an identifier with a consistent keyed-HMAC surrogate. The same input under the same key always yields the same surrogate (linkage preserved); the surrogate is not reversible without the key (collision-resistant, key-dependent). Domain-separated from keyedHash.
Parameters
id
string
The identifier to pseudonymize (MRN, beneficiary number, account number).
ctx
The de-identification context holding the consumer's key.
Returns
string
A lowercase-hex surrogate, consistent per (key, id).
Example
import { pseudonymize, createDeidContext } from "@cosyte/deid";
const ctx = createDeidContext({ key: "secret" });
pseudonymize("MRN-123", ctx) === pseudonymize("MRN-123", ctx); // => true (consistent)
redact()
redact():
null
Redact a value. Returns null: the removed-value sentinel used throughout the transformed
document. Pure and keyless.
Returns
null
null, signalling the value has been removed.
Example
import { redact } from "@cosyte/deid";
redact(); // => null
resolvePolicy()
resolvePolicy(
policy):DeidPolicy
Resolve the policy argument accepted by the engine: the string "safe-harbor" (or undefined)
yields the built-in policy; a DeidPolicy object is returned as-is.
Parameters
policy
DeidPolicy | "safe-harbor" | undefined
"safe-harbor", a DeidPolicy, or undefined.
Returns
The concrete policy to apply.
Example
import { resolvePolicy, SAFE_HARBOR_POLICY } from "@cosyte/deid";
resolvePolicy("safe-harbor") === SAFE_HARBOR_POLICY; // => true
retains()
retains(
classes,cls):boolean
Test whether a retention class is enabled by a (possibly absent) retention set. An absent or empty set retains nothing, so a caller that never mentions retention gets the strict treatment.
Parameters
classes
readonly RetainedLocusClass[] | undefined
The retention classes a profile or options bag enabled, if any.
cls
The class to test.
Returns
boolean
true only when classes is present and lists cls.
Example
import { retains, RETAINED_LOCUS_CLASSES } from "@cosyte/deid";
retains(undefined, RETAINED_LOCUS_CLASSES.ENCOUNTER_DATES); // => false (fail closed)
retains(["encounter-dates"], RETAINED_LOCUS_CLASSES.ENCOUNTER_DATES); // => true
unkeyedHash()
unkeyedHash(
value):string
A plain, unsalted SHA-256 digest: NON-CONFORMING to §164.514(c) and never used by the engine. It is re-identifiable for a small, enumerable identifier space (an attacker hashes every candidate and matches). Exported solely so the test suite can prove the reversibility hazard the keyed path avoids. Do not use this to de-identify anything.
Parameters
value
string
The value to hash.
Returns
string
The lowercase-hex SHA-256 of value: reversible for a small input space.
Example
import { unkeyedHash } from "@cosyte/deid";
// Deterministic and unsalted: this is the footgun, shown so tests can assert against it.
unkeyedHash("a") === unkeyedHash("a"); // => true