Skip to main content
Version: v0.0.6

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's buildMessage, @cosyte/fhir's model + serializer, @cosyte/ccda's buildCcda, 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. synth never 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: deid proves real PHI is gone; synth proves 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.random is lint-banned in src/.

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. synth is the opposite kind of tool: its randomness is structural (field shapes, delimiters, quirks, edge cases), not epidemiological. A synth-generated ORU may 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. synth does 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. synth optimizes 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 synth ships 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. synth never bundles SNOMED, CPT, or a full LOINC table.
  • No real-data ingestion as a value source. synth never 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 synth does not support is a typed SYNTH_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:

LocusSourceWhy it cannot be real
SSNarea 900–999 (never issued) + the 987-65-432x advertising blockSSA never issues these areas
NPI10 digits with a deliberately-invalid Luhn check digita real NPI must pass Luhn - isSyntheticNpi proves the failure
DEAregistrant letter + 7 digits with a deliberately-invalid check digita real DEA must pass its checksum - isSyntheticDea proves the failure
PhoneNANP 555-0100 … 555-0199 onlythe reserved fictional block (not "any 555 number")
Email / domainexample.com/.net/.org, .test/.example/.invalid/.localhostRFC 2606 / RFC 6761 special-use
IP192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32RFC 5737 / RFC 3849 documentation ranges
MRN / member / accounta clearly-synthetic assigning-authority namespace (COSYTE-SYNTH)there is no reserved MRN range - so the namespace is synthetic, not the digits
ZIP00000not an assignable ZIP
Names / streets / citiesa shipped, curated clearly-fake poolnot 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. SynthError takes a code and no value parameter, so there is no position through which a value could reach message, 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 (or SYNTH_UNSUPPORTED_QUIRK), so it does not reach a peer builder that might quote it back and does not become an artifact kind or 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 deid pairing 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_SKIPPED names each).
  • Format-specific gaps flagged, never faked: the X12 270 request (no build270 upstream) and NCPDP SCRIPT lifecycle responses are not generated - a gap is surfaced, never hand-written.
  • Built-in synth profiles. 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 deid peers 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 a phi-scan gate 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.