What @cosyte/synth does and does not do
@cosyte/synth is a format/conformance generator, not a clinical simulator (not Synthea); every
value is synthetic-by-construction; output is deterministic per seed within a version window; no
terminology is bundled; and there is no DICOM in v1.
That one sentence governs the whole library. This page is the honest, blunt shape of the promise and
its edges, read it before you rely on synth. The API Reference is always the exact truth of what
a given release ships; this page is the shape of the whole.
The promise (narrow, on purpose)
@cosyte/synth emits deterministic, seedable, spec-clean (and, in quirk mode, deliberately
off-spec) synthetic fixtures across the six cosyte formats, and every value it emits is drawn from
a guaranteed-non-colliding synthetic source.
- Spec-clean by construction. Each artifact is built through the parser's own
builder/serializer (
@cosyte/hl7'sbuildMessage,@cosyte/fhir's model + serializer,@cosyte/ccda'sbuildCcda, the X12/NCPDP/ASTM domain builders), so it is spec-clean by the same mechanism that makes the parser's emit side spec-clean, and it is proven by feeding the output straight back into that parser and asserting zero warnings.synthnever hand-writes wire bytes around a builder. - Synthetic-by-construction. There is no code path that can emit a name, identifier, date, phone,
email, address, or IP not sourced from a reserved range or a shipped clearly-fake pool (see the
posture below). This is the inverse of a de-identifier:
deidproves real PHI is gone;synthproves plausibly-real PHI was never generated. - Deterministic. The same seed yields byte-identical output on any machine, any run, the
property every downstream regression and golden-file suite depends on. Determinism is threaded
through a hand-rolled seeded PRNG (
splitmix32/sfc32);Math.randomis lint-banned insrc/.
What it does not do
These are non-goals, not missing features: named so nothing over-trusts the generator.
- Not a clinical-simulation engine: this is the load-bearing boundary vs Synthea. Synthea
(MITRE) models each patient's disease progression and lifetime medical history and emits
clinically-coherent records.
synthis the opposite kind of tool: its randomness is structural (field shapes, delimiters, quirks, edge cases), not epidemiological. Asynth-generatedORUmay pair a diagnosis code and a result value that make no clinical sense, and that is correct, because its job is to exercise the parser, not to be a plausible patient.synthdoes not reimplement Synthea (optional Synthea-content ingestion is a documented future concern, below). - Not statistically-representative populations. No claim is made that a generated cohort matches any
real-world distribution of age, sex, condition prevalence, or geography.
synthoptimizes for structural coverage (every message type, every quirk, every edge case), not demographic fidelity. - Not a load / performance-test harness. A seeded corpus is a fine throughput input, but
synthships no benchmark runner. - No bundled terminology. Codes (LOINC/SNOMED/ICD/RxNorm/CVX/NDC) come from a small curated,
license-clean example pool (spec-example codes, invented
00000-labeler NDCs,X-prefixed local codes) or a consumer-supplied pool.synthnever bundles SNOMED, CPT, or a full LOINC table. - No real-data ingestion as a value source.
synthnever reads a real patient record to "learn" values. The only external content it may consume (later, optionally) is Synthea output, itself synthetic and PHI-free by construction. - No DICOM (or any format outside the six). DICOM Part 10 is binary with a distinct pixel-data
hazard surface; v1 scopes the six text/EDI formats (HL7 v2, FHIR, C-CDA, X12, NCPDP, ASTM). A format
synthdoes not support is a typedSYNTH_UNSUPPORTED_FORMAT, never a hand-written byte fallback.
The synthetic-safety posture (the floors)
Every PHI-bearing locus is filled from a source provably incapable of denoting a real person or resource. The floors, each an authoritative never-collide range or a deliberately-invalid check value:
| Locus | Source | Why it cannot be real |
|---|---|---|
| SSN | area 900–999 (never issued) + the 987-65-432x advertising block | SSA never issues these areas |
| NPI | 10 digits with a deliberately-invalid Luhn check digit | a real NPI must pass Luhn: isSyntheticNpi proves the failure |
| DEA | registrant letter + 7 digits with a deliberately-invalid check digit | a real DEA must pass its checksum: isSyntheticDea proves the failure |
| Phone | NANP 555-0100 … 555-0199 only | the reserved fictional block (not "any 555 number") |
| Email / domain | example.com/.net/.org, .test/.example/.invalid/.localhost | RFC 2606 / RFC 6761 special-use |
| IP | 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32 | RFC 5737 / RFC 3849 documentation ranges |
| MRN / member / account | a clearly-synthetic assigning-authority namespace (COSYTE-SYNTH) | there is no reserved MRN range, so the namespace is synthetic, not the digits |
| ZIP | 00000 | not an assignable ZIP |
| Names / streets / cities | a shipped, curated clearly-fake pool | not a realistic-name corpus that could match a real person at a real address |
The floors are enforced two ways: a union phi-scan (the parsers' own PHI scanners) sweeps a
representative generated corpus in CI and must report zero real-data hits, and a property suite
asserts, for arbitrary seeds and every format, that no emitted value escapes these sources. A quirk
fixture deviates structure, never provenance: the safety floor holds over quirk output too.
The diagnostics have their own floor, and it is a different one. The table above is about the
values synth emits. A separate guarantee covers what synth says about your call:
- A fatal message is a fixed string from a frozen registry.
SynthErrortakes a code and no value parameter, so there is no position through which a value could reachmessage,stack, or a field on the thrown object. - Caller-supplied selectors (a message kind, a document type, a corpus mix entry, a claim
variant, a Bundle type, a resource profile, a quirk name) are resolved against their own closed set
before anything is generated. An unrecognised one is a fatal
SYNTH_UNSUPPORTED_KIND(orSYNTH_UNSUPPORTED_QUIRK), so it does not reach a peer builder that might quote it back and does not become an artifactkindor a manifest key. - A round-trip result keeps only the sibling parser's warning codes, never the parser's message and never a snippet of the document.
Each of those is asserted per position, against a marker planted in each one, rather than argued from
the fact that a fixture generator's values are synthetic anyway. The list of positions is an
enumeration, not a proof of exhaustiveness. What holds generally is the mechanism: the error type
has no value parameter, and a selector is resolved against its closed set in one place, everywhere
except the @cosyte/synth/deid pairing loops. Those are the stated exception: x12DeidLoop({ variant })
and ncpdpTelecomDeidLoop({ transaction }) still surface an uncoded TypeError for an unrecognised
value, and ccdaDeidLoop({ documentType }) still generates a Referral Note for anything but "ccd".
None echoes your value into a message, and all three are tracked separately.
Three things it does not cover, stated rather than implied. The artifact is not a diagnostic:
content is what you asked to be built and carries what you asked for, and the same goes for a name
pool or a profile name you supply and get handed back. A document or model you pass to a round-trip
harness is parsed by the sibling parser, and a fatal it raises on input it could not read is that
parser's diagnostic, not this one's. And a caller-authored label (a SynthProfile name, an
Artifact you construct yourself and hand to makeCorpus) is stored and returned verbatim, because
that is what you asked for; no synth code path derives anything from it.
import { createRng, safe, isSyntheticSsn, isSyntheticNpi, isSyntheticDea } from "@cosyte/synth";
const rng = createRng(1);
// Every provider draws from a never-collide source: the checks below can never be false.
isSyntheticSsn(safe.ssn(rng)) && isSyntheticNpi(safe.npi(rng)) && isSyntheticDea(safe.dea(rng)); // => true
Determinism holds within a version window
A seed maps to the same bytes within a documented compatibility window, not across major
synth versions. A version bump may change a value list or the algorithm and thus the seed→bytes
mapping; that is a documented breaking change. For a long-lived golden fixture, pin the synth
version alongside the seed. Cross-engine determinism assumes the pinned toolchain (Node ≥22, ES2019+
stable sort and spec key order); it is not promised on arbitrary old engines.
Coverage, and what is deferred
The spec-clean generation core is feature-complete across all six formats. Quirk mode and the
deid pairing loop ship for a subset; the honest gaps:
- Vendor-quirk mode ships for the three richest profile systems: HL7 v2, C-CDA, ASTM. Quirk recipes for FHIR / X12 / NCPDP are deferred, as is any quirk needing a private vendor corpus (built-in quirks are grounded only on public vendor profiles).
- The
deidpairing loop ships for HL7 v2, FHIR, C-CDA, X12, and NCPDP Telecom. NCPDP SCRIPT, ASTM, and DICOM pairing are deferred (no adapter, or not generated:DEID_LOOP_SKIPPEDnames each). - Format-specific gaps flagged, never faked: the X12 270 request (no
build270upstream) and NCPDP SCRIPT lifecycle responses are not generated, a gap is surfaced, never hand-written. - Built-in
synthprofiles.defineSynthProfile()is the public growth-loop hook, and ready-made quirk profiles ship for the three quirk formats; broader named site/vendor recipes stay consumer-authored until a public spec grounds a built-in one (the same public-only discipline the parsers hold). - Optional Synthea clinical-content ingestion (re-serialize Synthea's coherent records through the cosyte parsers) is a documented future concern, not a v1 promise.
Licensing & PHI posture
- The library is MIT. Third-party runtime dependencies are zero; the parser and
deidpeers are first-party, optional, and lazily loaded per format. - HIPAA-capable, not HIPAA-compliant, and here that framing is nearly vacuous, because there is no
real PHI:
synth's entire output looks like PHI and contains none, by construction. Every value it emits is drawn from the reserved sources above, and aphi-scangate sweeps this project's own sources, fixtures and tooling on every change. You can commit and log a generated corpus without a PHI review of its contents: that is the whole point.