@cosyte/synth 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +414 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/astm/index.cjs +847 -0
- package/dist/astm/index.cjs.map +1 -0
- package/dist/astm/index.d.cts +418 -0
- package/dist/astm/index.d.ts +418 -0
- package/dist/astm/index.mjs +828 -0
- package/dist/astm/index.mjs.map +1 -0
- package/dist/ccda/index.cjs +1103 -0
- package/dist/ccda/index.cjs.map +1 -0
- package/dist/ccda/index.d.cts +380 -0
- package/dist/ccda/index.d.ts +380 -0
- package/dist/ccda/index.mjs +1077 -0
- package/dist/ccda/index.mjs.map +1 -0
- package/dist/deid/index.cjs +2809 -0
- package/dist/deid/index.cjs.map +1 -0
- package/dist/deid/index.d.cts +464 -0
- package/dist/deid/index.d.ts +464 -0
- package/dist/deid/index.mjs +2793 -0
- package/dist/deid/index.mjs.map +1 -0
- package/dist/example-codes-DeXcnCSK.d.cts +105 -0
- package/dist/example-codes-DeXcnCSK.d.ts +105 -0
- package/dist/fhir/index.cjs +1429 -0
- package/dist/fhir/index.cjs.map +1 -0
- package/dist/fhir/index.d.cts +772 -0
- package/dist/fhir/index.d.ts +772 -0
- package/dist/fhir/index.mjs +1384 -0
- package/dist/fhir/index.mjs.map +1 -0
- package/dist/hl7/index.cjs +1012 -0
- package/dist/hl7/index.cjs.map +1 -0
- package/dist/hl7/index.d.cts +548 -0
- package/dist/hl7/index.d.ts +548 -0
- package/dist/hl7/index.mjs +990 -0
- package/dist/hl7/index.mjs.map +1 -0
- package/dist/index.cjs +535 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +407 -0
- package/dist/index.d.ts +407 -0
- package/dist/index.mjs +488 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ncpdp/index.cjs +714 -0
- package/dist/ncpdp/index.cjs.map +1 -0
- package/dist/ncpdp/index.d.cts +432 -0
- package/dist/ncpdp/index.d.ts +432 -0
- package/dist/ncpdp/index.mjs +695 -0
- package/dist/ncpdp/index.mjs.map +1 -0
- package/dist/providers-OLz3zAc-.d.cts +343 -0
- package/dist/providers-OLz3zAc-.d.ts +343 -0
- package/dist/quirk-DmkgoZdh.d.cts +239 -0
- package/dist/quirk-JLyO1Ncj.d.ts +239 -0
- package/dist/x12/index.cjs +920 -0
- package/dist/x12/index.cjs.map +1 -0
- package/dist/x12/index.d.cts +484 -0
- package/dist/x12/index.d.ts +484 -0
- package/dist/x12/index.mjs +892 -0
- package/dist/x12/index.mjs.map +1 -0
- package/package.json +210 -0
package/README.md
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# @cosyte/synth
|
|
2
|
+
|
|
3
|
+
> Deterministic, seedable **synthetic healthcare-fixture generator** for Node.js and TypeScript —
|
|
4
|
+
> spec-clean by construction, and **never real PHI**.
|
|
5
|
+
|
|
6
|
+
`@cosyte/synth` generates reproducible synthetic test corpora across the six cosyte formats (HL7 v2,
|
|
7
|
+
FHIR R4 / US Core, C-CDA, X12, NCPDP, and ASTM). It is a **consumer** of the
|
|
8
|
+
cosyte parsers, not a parser: it builds each artifact **through the parser's own builder/serializer**
|
|
9
|
+
(so the output is spec-clean by the same mechanism the parser proves) and draws every identifier, name,
|
|
10
|
+
date, phone, and address from a **guaranteed-non-colliding synthetic source**. It is a
|
|
11
|
+
**format/conformance generator, not a clinical simulator** — it does not model disease progression
|
|
12
|
+
(that is Synthea).
|
|
13
|
+
|
|
14
|
+
> **Status:** pre-alpha (`0.0.x`), not yet published to npm. The generator is **feature-complete**: the
|
|
15
|
+
> seeded-PRNG core, the synthetic-safety providers, and the round-trip harness; **spec-clean generation
|
|
16
|
+
> across all six formats** — HL7 v2 (`ADT`/`ORU`/`ORM`/`SIU`/`VXU`), FHIR R4 / US Core (the full clinical
|
|
17
|
+
> set + `collection`/`transaction`/`document` Bundles), C-CDA R2.1 (CCD + Referral Note), X12 005010
|
|
18
|
+
> (837P/I/D, 835, 271), NCPDP (SCRIPT NewRx / RxRenewal / RxChange + Telecom B1/B2/B3), and ASTM (E1394
|
|
19
|
+
> record reports + E1381 framing), each built **through its parser's own builder/serializer**; **vendor-quirk
|
|
20
|
+
> mode** — profile-driven off-spec fixtures for the three richest profile systems (**HL7 v2, C-CDA, ASTM**),
|
|
21
|
+
> each round-tripping to exactly the intended parser warning (the intended-warning contract); and the
|
|
22
|
+
> **`@cosyte/deid` pairing loop** — a closed-loop co-validation harness that de-identifies generated output
|
|
23
|
+
> and proves every planted synthetic PHI sentinel is removed.
|
|
24
|
+
>
|
|
25
|
+
> **Deferred (honestly out of scope for now):** quirk recipes for **FHIR / X12 / NCPDP** (and any quirk
|
|
26
|
+
> needing a private vendor corpus); the X12 **270** eligibility _request_ and NCPDP SCRIPT lifecycle
|
|
27
|
+
> **responses** (both track the parsers' builder surfaces); **NCPDP SCRIPT / ASTM / DICOM** de-identify
|
|
28
|
+
> pairing (no `@cosyte/deid` adapter, or not generated); and optional **Synthea** clinical-content ingestion.
|
|
29
|
+
> The two remaining founder gates are the actual `npm publish` and the repo public-flip.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @cosyte/synth @cosyte/hl7 @cosyte/fhir @cosyte/ccda @cosyte/x12 @cosyte/ncpdp @cosyte/astm
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`@cosyte/hl7`, `@cosyte/fhir`, `@cosyte/ccda`, `@cosyte/x12`, `@cosyte/ncpdp`, `@cosyte/astm`, and
|
|
38
|
+
`@cosyte/deid` are **optional peer dependencies**, each needed only for its subpath (`@cosyte/synth/hl7`,
|
|
39
|
+
`@cosyte/synth/fhir`, `@cosyte/synth/ccda`, `@cosyte/synth/x12`, `@cosyte/synth/ncpdp`,
|
|
40
|
+
`@cosyte/synth/astm`, and `@cosyte/synth/deid` — the last needs `@cosyte/deid` **plus** the parsers for
|
|
41
|
+
the formats it pairs) — install only the packages whose fixtures you generate. The package core has
|
|
42
|
+
**zero third-party runtime dependencies**.
|
|
43
|
+
|
|
44
|
+
## Generate a spec-clean HL7 v2 message
|
|
45
|
+
|
|
46
|
+
The HL7 v2 set covers `ADT` (`A01`/`A04`/`A08`), `ORU^R01`, `ORM^O01`, `SIU^S12`, and `VXU^V04` —
|
|
47
|
+
each built through `@cosyte/hl7`'s `buildMessage`, so it is spec-clean by construction.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { generateAdt, generateOru, generateHl7, hl7Corpus, roundTrip } from "@cosyte/synth/hl7";
|
|
51
|
+
|
|
52
|
+
// Same seed → byte-identical message, on any machine, any run.
|
|
53
|
+
const adt = generateAdt({ seed: 12345, trigger: "A01" });
|
|
54
|
+
const oru = generateOru({ seed: 12345 });
|
|
55
|
+
|
|
56
|
+
// Spec-clean by construction: it round-trips through @cosyte/hl7 with zero warnings.
|
|
57
|
+
roundTrip(adt).specClean; // true
|
|
58
|
+
roundTrip(oru).specClean; // true
|
|
59
|
+
|
|
60
|
+
// Or generate a reproducible mixed corpus across every family:
|
|
61
|
+
const corpus = hl7Corpus({ seed: 42, count: 7 }); // one of each family, cycled
|
|
62
|
+
corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
|
|
63
|
+
|
|
64
|
+
// Dispatch by kind when the message type is data:
|
|
65
|
+
generateHl7("VXU^V04", 12345);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Generate a spec-clean FHIR R4 / US Core resource
|
|
69
|
+
|
|
70
|
+
The `@cosyte/synth/fhir` subpath builds resources **through `@cosyte/fhir`'s model constructors**, so
|
|
71
|
+
they are spec-clean by construction — validating under `validateResource` and, against the **real US
|
|
72
|
+
Core 6.1.0 profiles** (bring your own `StructureDefinition`s — none is bundled), conformant to US Core.
|
|
73
|
+
The clinical set covers `Patient` (base + US Core), `Condition`, `Observation` (US Core Laboratory
|
|
74
|
+
Result + Vital Signs), `MedicationRequest`, `Encounter`, `DiagnosticReport`, `Immunization`,
|
|
75
|
+
`AllergyIntolerance`, and `Procedure`, assembled into a `collection`, `transaction`, or `document`
|
|
76
|
+
`Bundle`.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { generatePatient, generateBundle, fhirCorpus, roundTrip } from "@cosyte/synth/fhir";
|
|
80
|
+
|
|
81
|
+
// A US Core Patient — same seed → byte-identical resource, anywhere.
|
|
82
|
+
const patient = generatePatient({ seed: 12345, profile: "us-core" });
|
|
83
|
+
|
|
84
|
+
// Spec-clean by construction: it round-trips through @cosyte/fhir with zero errors, byte-stable.
|
|
85
|
+
roundTrip(patient).specClean; // true
|
|
86
|
+
|
|
87
|
+
// A self-contained Bundle assembling a patient + its clinical spine, wired by urn:uuid references:
|
|
88
|
+
const bundle = generateBundle({ seed: 42, type: "transaction" });
|
|
89
|
+
|
|
90
|
+
// Or a reproducible mixed corpus across the whole spine:
|
|
91
|
+
const corpus = fhirCorpus({ seed: 2026, count: 6 });
|
|
92
|
+
corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`@cosyte/fhir` is an **optional peer dependency**, needed only for the `@cosyte/synth/fhir` subpath.
|
|
96
|
+
|
|
97
|
+
## Generate a spec-clean C-CDA document
|
|
98
|
+
|
|
99
|
+
The `@cosyte/synth/ccda` subpath builds Consolidated CDA R2.1 documents **through `@cosyte/ccda`'s
|
|
100
|
+
`buildCcda`**, so template IDs, LOINC section codes, and structured/narrative agreement are the
|
|
101
|
+
builder's own — the document round-trips through `parseCcda` with **zero warnings**. It emits a
|
|
102
|
+
**CCD** (`generateCcd`) or a **Referral Note** (`generateReferralNote`), each with the CCD sections
|
|
103
|
+
(Problems, Allergies, Medications, Results, Vital Signs, Immunizations, Procedures, Social History)
|
|
104
|
+
populated from the reused, license-clean example-code pools.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { serializeCcda } from "@cosyte/ccda";
|
|
108
|
+
import { generateCcd, generateReferralNote, ccdaCorpus, roundTrip } from "@cosyte/synth/ccda";
|
|
109
|
+
|
|
110
|
+
// Same seed → byte-identical document. The patient name is from the shipped fake-name pool, the MRN
|
|
111
|
+
// lives under a synthetic assigning-authority OID, and every date comes from the seeded generator.
|
|
112
|
+
const ccd = generateCcd({ seed: 12345 });
|
|
113
|
+
serializeCcda(ccd); // spec-clean C-CDA R2.1 XML
|
|
114
|
+
|
|
115
|
+
// Spec-clean by construction: it round-trips through @cosyte/ccda with zero warnings.
|
|
116
|
+
roundTrip(ccd).specClean; // true
|
|
117
|
+
roundTrip(generateReferralNote({ seed: 12345 })).specClean; // true
|
|
118
|
+
|
|
119
|
+
// Or a reproducible mixed corpus (CCD + Referral Note, cycled):
|
|
120
|
+
const corpus = ccdaCorpus({ seed: 42, count: 4 });
|
|
121
|
+
corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`@cosyte/ccda` is an **optional peer dependency**, needed only for the `@cosyte/synth/ccda` subpath.
|
|
125
|
+
|
|
126
|
+
## Generate a spec-clean X12 transaction
|
|
127
|
+
|
|
128
|
+
The `@cosyte/synth/x12` subpath builds HIPAA **005010** transactions **through `@cosyte/x12`'s domain
|
|
129
|
+
builders** (`build837P/I/D`, `build835`, `build271`), so the ISA/GS/ST…SE/GE/IEA envelope, the
|
|
130
|
+
computed HL spine, the control numbers, and every segment are the builder's own — each transaction
|
|
131
|
+
round-trips through `@cosyte/x12` with **zero warnings**. It emits **837** professional / institutional
|
|
132
|
+
/ dental claims, the **835** remittance (balance-checked by construction), and the **271** eligibility
|
|
133
|
+
response.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { generate837P, generate835, generate271, x12Corpus, roundTrip } from "@cosyte/synth/x12";
|
|
137
|
+
|
|
138
|
+
// Same seed → byte-identical EDI. Every subscriber/patient/provider identifier is
|
|
139
|
+
// synthetic-by-construction: the provider NPI has a deliberately-INVALID Luhn check digit (so it can
|
|
140
|
+
// never be a NPPES-issued NPI), the provider tax id is an SSA never-issued 900-range SSN, member ids
|
|
141
|
+
// live under a synthetic assigning authority, and names come from the shipped fake-name pool.
|
|
142
|
+
const claim = generate837P({ seed: 12345 });
|
|
143
|
+
roundTrip(claim).specClean; // true — re-parses through @cosyte/x12 with zero warnings, byte-stable
|
|
144
|
+
roundTrip(generate835({ seed: 7 })).specClean; // true
|
|
145
|
+
roundTrip(generate271({ seed: 3 })).specClean; // true
|
|
146
|
+
|
|
147
|
+
// Or a reproducible mixed corpus (837P/I/D + 835 + 271):
|
|
148
|
+
const corpus = x12Corpus({ seed: 42 });
|
|
149
|
+
corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`@cosyte/x12` is an **optional peer dependency**, needed only for the `@cosyte/synth/x12` subpath.
|
|
153
|
+
|
|
154
|
+
**Deferred:** the **270** eligibility _request_ (`@cosyte/x12` ships a `build271` but no `build270`, and
|
|
155
|
+
`synth` never hand-writes bytes around a missing builder) and **vendor-quirk mode**.
|
|
156
|
+
|
|
157
|
+
## Generate a spec-clean NCPDP message
|
|
158
|
+
|
|
159
|
+
The `@cosyte/synth/ncpdp` subpath builds both NCPDP standards **through `@cosyte/ncpdp`'s own emit
|
|
160
|
+
surface**, so each message round-trips through the parser with **zero warnings**. It emits **SCRIPT**
|
|
161
|
+
ePrescribing (`generateNewRx` via `buildNewRx`; `generateRxRenewalRequest` / `generateRxChangeRequest`
|
|
162
|
+
via the parser's typed `ScriptMessage` model + `serializeScript`) and **Telecom** pharmacy claims
|
|
163
|
+
(`generateB1` billing / `generateB2` reversal / `generateB3` rebill via `buildTelecomRequest`).
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import {
|
|
167
|
+
generateNewRx,
|
|
168
|
+
generateB1,
|
|
169
|
+
ncpdpCorpus,
|
|
170
|
+
scriptRoundTrip,
|
|
171
|
+
telecomRoundTrip,
|
|
172
|
+
} from "@cosyte/synth/ncpdp";
|
|
173
|
+
|
|
174
|
+
// Same seed → byte-identical output. NCPDP carries patient AND prescriber identity: the prescriber NPI
|
|
175
|
+
// has a deliberately-INVALID Luhn check digit and the prescriber DEA a deliberately-INVALID checksum
|
|
176
|
+
// (so neither can denote a real provider); patient/cardholder ids live under a synthetic assigning
|
|
177
|
+
// authority, phones are reserved 555-01xx, and names come from the shipped fake-name pool.
|
|
178
|
+
scriptRoundTrip(generateNewRx({ seed: 12345 })).specClean; // true — zero warnings, byte-stable
|
|
179
|
+
telecomRoundTrip(generateB1({ seed: 777 })).specClean; // true
|
|
180
|
+
|
|
181
|
+
// Or a reproducible mixed corpus (NewRx + RxRenewal + RxChange + B1 + B2 + B3):
|
|
182
|
+
const corpus = ncpdpCorpus({ seed: 42 });
|
|
183
|
+
corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`@cosyte/ncpdp` is an **optional peer dependency**, needed only for the `@cosyte/synth/ncpdp` subpath.
|
|
187
|
+
|
|
188
|
+
**Deferred:** vendor-quirk mode. SCRIPT coverage tracks the parser's builder surface: the
|
|
189
|
+
renewal/change _responses_ land as `@cosyte/ncpdp` grows builders.
|
|
190
|
+
|
|
191
|
+
## Generate a spec-clean ASTM message
|
|
192
|
+
|
|
193
|
+
The `@cosyte/synth/astm` subpath builds ASTM laboratory messages **through `@cosyte/astm`'s own emit
|
|
194
|
+
surface** — `buildAstmMessage` for the E1394 record layer, `composeAstmFrames` for the E1381 frame
|
|
195
|
+
layer — so each message round-trips through the parser with **zero warnings**. It emits the
|
|
196
|
+
`H`/`P`/`O`/`R`…/`C`/`L` **result report** (`generateAstmResult`), the `H`/`P`/`O`/`L` **order**
|
|
197
|
+
(`generateAstmOrder`), and the **framed** twin (`generateAstmResultFramed`).
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import {
|
|
201
|
+
generateAstmResult,
|
|
202
|
+
generateAstmResultFramed,
|
|
203
|
+
astmRoundTrip,
|
|
204
|
+
astmFramedRoundTrip,
|
|
205
|
+
astmCorpus,
|
|
206
|
+
} from "@cosyte/synth/astm";
|
|
207
|
+
|
|
208
|
+
// Same seed → byte-identical output. The P (patient) record carries the name, birthdate, and the
|
|
209
|
+
// practice- and laboratory-assigned patient ids — all synthetic-by-construction: names from the shipped
|
|
210
|
+
// fake-name pool, DOB seeded, and the two ids minted independently under a synthetic assigning authority
|
|
211
|
+
// (so they stay DISTINCT, exactly as @cosyte/astm keeps them on parse).
|
|
212
|
+
astmRoundTrip(generateAstmResult({ seed: 12345 })).specClean; // true — zero warnings, byte-stable
|
|
213
|
+
|
|
214
|
+
// The E1381-framed twin: the modulo-256 checksum and 0–7 frame numbers are computed by @cosyte/astm.
|
|
215
|
+
astmFramedRoundTrip(generateAstmResultFramed({ seed: 12345 })).specClean; // true
|
|
216
|
+
|
|
217
|
+
// Or a reproducible mixed corpus (a result report + an order):
|
|
218
|
+
const corpus = astmCorpus({ seed: 42 });
|
|
219
|
+
corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`@cosyte/astm` is an **optional peer dependency**, needed only for the `@cosyte/synth/astm` subpath.
|
|
223
|
+
|
|
224
|
+
## Generate a vendor-quirk fixture
|
|
225
|
+
|
|
226
|
+
Spec-clean fixtures test that a parser reads a _correct_ message; **quirk mode** tests that it tolerates
|
|
227
|
+
the realistic vendor deviations real traffic carries — and surfaces exactly the right diagnostic. The
|
|
228
|
+
quirk vocabulary **is the parsers' own profile systems**: a quirk deviates the message _structure_ so it
|
|
229
|
+
round-trips to **exactly one intended, stable warning code** (the **intended-warning contract**), and
|
|
230
|
+
where a built-in **public** parser profile claims the deviation, it round-trips cleanly under it
|
|
231
|
+
(suppressed, or re-badged to `PROFILE_QUIRK_APPLIED`). A quirk never introduces a real-looking value —
|
|
232
|
+
it changes shape, never provenance, so the synthetic-safety gate still passes.
|
|
233
|
+
|
|
234
|
+
Quirks ship for the three richest profile systems — **HL7 v2, C-CDA, and ASTM**:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { generateHl7Quirk, hl7QuirkRoundTrip } from "@cosyte/synth/hl7";
|
|
238
|
+
import { generateCcdaQuirk, ccdaQuirkRoundTrip } from "@cosyte/synth/ccda";
|
|
239
|
+
import { generateAstmQuirk, astmQuirkRoundTrip } from "@cosyte/synth/astm";
|
|
240
|
+
|
|
241
|
+
// A site-defined HL7 v2 Z-segment → exactly UNKNOWN_SEGMENT; the `visage` profile suppresses it.
|
|
242
|
+
hl7QuirkRoundTrip(generateHl7Quirk({ seed: 1, quirk: "unknown-zsegment" })).warnings; // ["UNKNOWN_SEGMENT"]
|
|
243
|
+
|
|
244
|
+
// A deprecated C-CDA LOINC → exactly DEPRECATED_LOINC; `smartScorecard` re-badges it.
|
|
245
|
+
ccdaQuirkRoundTrip(generateCcdaQuirk({ seed: 1, quirk: "deprecated-loinc" })).withProfile
|
|
246
|
+
?.tolerated; // true
|
|
247
|
+
|
|
248
|
+
// A non-standard ASTM &Z& escape → exactly ASTM_UNKNOWN_ESCAPE_SEQUENCE; `referenceCorpus` re-badges it.
|
|
249
|
+
astmQuirkRoundTrip(generateAstmQuirk({ seed: 1, quirk: "unknown-escape" })).intendedWarningHeld; // true
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
A quirk a format's profile system does not support fails closed with a stable `SYNTH_UNSUPPORTED_QUIRK`
|
|
253
|
+
diagnostic — never a silently-wrong fixture. Every quirk is grounded in a **publicly-documented**
|
|
254
|
+
deviation or a parser's **public** profile, never a private vendor corpus.
|
|
255
|
+
|
|
256
|
+
**Deferred:** quirk recipes for **FHIR, X12, and NCPDP**, and any quirk that would need a **private,
|
|
257
|
+
vendor-attributed corpus** to ground.
|
|
258
|
+
|
|
259
|
+
## Co-validate with `@cosyte/deid` (the pairing loop)
|
|
260
|
+
|
|
261
|
+
The `@cosyte/synth/deid` subpath is a deterministic **closed-loop co-validation harness** for the
|
|
262
|
+
`synth` ⇄ `deid` pair: it **generates** a spec-clean artifact, **plants** distinctive synthetic PHI
|
|
263
|
+
sentinels at the patient loci, **de-identifies** it through `@cosyte/deid`, and **verifies** every
|
|
264
|
+
sentinel is gone from the output (a surviving sentinel is a hard failure) while the clinical payload
|
|
265
|
+
survives (the over-scrub guard).
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import {
|
|
269
|
+
hl7DeidLoop,
|
|
270
|
+
fhirDeidLoop,
|
|
271
|
+
x12DeidLoop,
|
|
272
|
+
ncpdpTelecomDeidLoop,
|
|
273
|
+
ccdaDeidLoop,
|
|
274
|
+
summarizeDeidCoverage,
|
|
275
|
+
} from "@cosyte/synth/deid";
|
|
276
|
+
|
|
277
|
+
const r = hl7DeidLoop({ seed: 42, kind: "ORU^R01" });
|
|
278
|
+
r.pass; // true
|
|
279
|
+
r.survivors; // [] — every planted synthetic PHI sentinel was removed
|
|
280
|
+
r.clinicalScrubbed; // [] — no clinical value was over-scrubbed
|
|
281
|
+
|
|
282
|
+
const summary = summarizeDeidCoverage([
|
|
283
|
+
hl7DeidLoop({ seed: 1 }),
|
|
284
|
+
fhirDeidLoop({ seed: 1 }),
|
|
285
|
+
x12DeidLoop({ seed: 1, variant: "837P" }),
|
|
286
|
+
ncpdpTelecomDeidLoop({ seed: 1, transaction: "B1" }),
|
|
287
|
+
ccdaDeidLoop({ seed: 1 }),
|
|
288
|
+
]);
|
|
289
|
+
summary.allPass; // true — zero survivors, zero over-scrub, per format
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
This is a **co-validation harness, not an independent audit** of `@cosyte/deid` against real-world
|
|
293
|
+
data — it proves the pair works on `synth`'s own output. The removal check is **locus-scoped**: it
|
|
294
|
+
sweeps only the de-identified values remaining at the former PHI loci, so provider/organization
|
|
295
|
+
identity a de-identifier legitimately retains never reads as a false survivor. `@cosyte/deid` is an
|
|
296
|
+
**optional peer dependency**, needed only for this subpath. **Skipped and named** (`DEID_LOOP_SKIPPED`):
|
|
297
|
+
NCPDP **SCRIPT** and **ASTM** (no `@cosyte/deid` adapter) and **DICOM** (not generated by `synth`).
|
|
298
|
+
|
|
299
|
+
## Draw a synthetic value
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { createRng, safe, isSyntheticSsn, isSyntheticNpi } from "@cosyte/synth";
|
|
303
|
+
|
|
304
|
+
const rng = createRng(42);
|
|
305
|
+
isSyntheticSsn(safe.ssn(rng)); // true — always an SSA never-issued SSN
|
|
306
|
+
isSyntheticNpi(safe.npi(rng)); // true — always a deliberately-invalid-Luhn NPI (never a real NPI)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## What makes it trustworthy
|
|
310
|
+
|
|
311
|
+
- **Synthetic-by-construction** — no code path emits a value not drawn from a reserved range or the
|
|
312
|
+
shipped fake-name pool (SSA never-issued SSNs, NANP `555-01xx` phones, RFC 2606/6761 `example.*`
|
|
313
|
+
domains, RFC 5737/3849 TEST-NET IPs, a synthetic assigning authority for MRNs). A CI gate proves it.
|
|
314
|
+
**No generated value can be real or plausibly-real PHI.**
|
|
315
|
+
- **Spec-clean by the parser's own judgment** — built through the parser's conservative serializer, and
|
|
316
|
+
checked by feeding the artifact straight back in: a spec-clean artifact re-parses with zero warnings.
|
|
317
|
+
- **Deterministic** — a hand-rolled seeded PRNG (`sfc32`/`splitmix32`); `Math.random` is lint-banned.
|
|
318
|
+
A seed, and only the seed, determines the output — byte-for-byte, anywhere.
|
|
319
|
+
- **Immutable** — generated artifacts and the `Corpus` result are deep-frozen.
|
|
320
|
+
- **Zero third-party runtime dependencies** — the parser peers are first-party cosyte packages,
|
|
321
|
+
vendored for dev/test; dual ESM + CJS, validated with `attw`.
|
|
322
|
+
|
|
323
|
+
## License
|
|
324
|
+
|
|
325
|
+
MIT © Cosyte
|