Core Concepts
@cosyte/synth borrows the cosyte parser archetype's disciplines (immutability, stable typed codes,
the profile system) but its central reflex is neither the parser's liberal parse nor a fail-closed
de-identifier - it is synthetic-by-construction.
Synthetic-by-construction
There is no code path that emits a value not drawn from a guaranteed-non-colliding synthetic
source. This inverts a parser's liberality: a parser is liberal on input; synth is closed-world on
its data sources. The hazard is asymmetric and specific - a generated value that looks real enough to
be mistaken for, or collide with, a real person's PHI. So every identifier comes from a reserved range
(SSA never-issued SSNs, NANP 555-01xx phones, example.* domains, RFC 5737 TEST-NET IPs) or a shipped
clearly-fake pool, and a CI gate proves it:
import { createRng, safe, isSyntheticSsn, isSyntheticPhone } from "@cosyte/synth";
const rng = createRng(1);
[isSyntheticSsn(safe.ssn(rng)), isSyntheticPhone(safe.phone(rng))]; // => [true, true]
Spec-clean, by the parser's own judgment
synth never hand-writes bytes. It builds through the parser's own conservative serializer, so an
artifact is spec-clean by the exact mechanism the parser already proves - and its correctness is
checkable by feeding it straight back into that parser. A spec-clean artifact re-parses with zero
warnings and re-serializes byte-identically:
import { generateAdt, roundTrip } from "@cosyte/synth/hl7";
roundTrip(generateAdt({ seed: 3 })).specClean; // => true
Determinism: a seed → the same bytes
Reproducibility is a first-class guarantee, not a nicety. The PRNG is a hand-rolled, seeded, zero-dep
generator (sfc32 seeded by splitmix32); Math.random is lint-banned in source because it is not
seedable. The seeded state is threaded explicitly, never global - so the same seed yields byte-identical
output on any machine, any run.
Immutability
Generated artifacts and the Corpus result are deep-frozen - a fixture is safe to share across a
pipeline without defensive copying.
Stable fatal codes
A generator has no input to tolerate, so its diagnostics are fatal: a request it cannot honor
spec-clean (an unsupported format or quirk) throws a typed SynthError carrying a stable
SYNTH_FATAL_CODES value - never a silent fabrication. Codes are key === value, so the full set
survives an Object.values(...) snapshot into a stability tripwire.
SynthError takes a code and nothing else. Its message is looked up in the frozen
SYNTH_FATAL_MESSAGES table, so a caller-supplied value has no parameter to enter through and cannot
reach message, stack, or any field on the thrown object. The trade is deliberate: a fatal names the
rule that refused rather than the value that tripped it, and the caller reads the value back off the
arguments it passed.