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.