fhir-test-data 0.1.0

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.
@@ -0,0 +1,850 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ COMMON_ALLERGY_CODES,
4
+ COMMON_LOINC_CODES,
5
+ COMMON_MEDICATION_CODES,
6
+ COMMON_SNOMED_CONDITIONS,
7
+ SUPPORTED_FHIR_VERSIONS,
8
+ SUPPORTED_LOCALES,
9
+ US_RXNORM_MEDICATION_CODES,
10
+ createAllergyIntoleranceBuilder,
11
+ createBundleBuilder,
12
+ createConditionBuilder,
13
+ createMedicationStatementBuilder,
14
+ createObservationBuilder,
15
+ createOrganizationBuilder,
16
+ createPatientBuilder,
17
+ createPractitionerBuilder,
18
+ createPractitionerRoleBuilder,
19
+ deepMerge,
20
+ getAllLocales,
21
+ getLocale
22
+ } from "../chunk-T46LJ67Q.js";
23
+ import {
24
+ FAULT_TYPES,
25
+ injectFaults
26
+ } from "../chunk-CBIPVWLL.js";
27
+ import {
28
+ createRng
29
+ } from "../chunk-U2QJNKBG.js";
30
+
31
+ // src/cli/index.ts
32
+ import { Command } from "commander";
33
+
34
+ // src/cli/commands/generate.ts
35
+ import { writeFileSync, mkdirSync } from "fs";
36
+ import { join } from "path";
37
+
38
+ // src/core/annotations/index.ts
39
+ var SHARED_NOTES = [
40
+ {
41
+ path: "id",
42
+ note: "Unique resource identifier (UUID v4)"
43
+ }
44
+ ];
45
+ function identifierNotes(identifiers, defs) {
46
+ if (!identifiers) return [];
47
+ const notes = [];
48
+ identifiers.forEach((id, i) => {
49
+ const def = defs.find((d) => d.system === id.system);
50
+ if (!def) return;
51
+ const algorithmClause = def.algorithm ? `, validated with ${def.algorithm}` : "";
52
+ notes.push({
53
+ path: `identifier[${i}].value`,
54
+ note: `${def.name}${algorithmClause}`
55
+ });
56
+ notes.push({
57
+ path: `identifier[${i}].system`,
58
+ note: `FHIR NamingSystem URI for ${def.name} (${def.system})`
59
+ });
60
+ });
61
+ return notes;
62
+ }
63
+ function nameNotes(names) {
64
+ if (!names) return [];
65
+ return [
66
+ {
67
+ path: "name[0].use",
68
+ note: "FHIR name use \u2014 'official' is the primary legal name used in clinical records"
69
+ },
70
+ {
71
+ path: "name[0].prefix",
72
+ note: "Cultural or professional title prefix (e.g., Mr, Dr, Prof)"
73
+ }
74
+ ];
75
+ }
76
+ function addressNotes(locale) {
77
+ return [
78
+ {
79
+ path: "address[0].country",
80
+ note: `ISO 3166-1 alpha-2 country code \u2014 ${locale.address.country} = ${locale.name}`
81
+ },
82
+ {
83
+ path: "address[0].postalCode",
84
+ note: `Locale-appropriate postal code format for ${locale.name}`
85
+ }
86
+ ];
87
+ }
88
+ function annotatePatient(resource, locale) {
89
+ const identifiers = resource["identifier"];
90
+ const communication = resource["communication"];
91
+ const langCode = communication?.[0]?.language?.coding?.[0]?.code;
92
+ return [
93
+ ...SHARED_NOTES,
94
+ ...identifierNotes(identifiers, locale.patientIdentifiers),
95
+ ...nameNotes(resource["name"]),
96
+ {
97
+ path: "telecom",
98
+ note: "Home phone number and email address in FHIR ContactPoint format"
99
+ },
100
+ {
101
+ path: "gender",
102
+ note: "FHIR administrative gender code (male | female | other | unknown)"
103
+ },
104
+ {
105
+ path: "birthDate",
106
+ note: "Patient date of birth \u2014 ISO 8601 format (YYYY-MM-DD)"
107
+ },
108
+ ...addressNotes(locale),
109
+ ...langCode ? [
110
+ {
111
+ path: "communication[0].language.coding[0].code",
112
+ note: `BCP 47 language tag \u2014 ${langCode} identifies the primary language for this locale`
113
+ },
114
+ {
115
+ path: "communication[0].language.coding[0].system",
116
+ note: "BCP 47 \u2014 IETF language tag standard (urn:ietf:bcp:47)"
117
+ }
118
+ ] : []
119
+ ];
120
+ }
121
+ function annotatePractitioner(resource, locale) {
122
+ const identifiers = resource["identifier"];
123
+ return [
124
+ ...SHARED_NOTES,
125
+ ...identifierNotes(identifiers, locale.practitionerIdentifiers),
126
+ ...nameNotes(resource["name"]),
127
+ {
128
+ path: "name[0].prefix",
129
+ note: "Professional title prefix for practitioners (e.g., Dr, Prof)"
130
+ },
131
+ {
132
+ path: "telecom",
133
+ note: "Work email address in FHIR ContactPoint format"
134
+ },
135
+ {
136
+ path: "gender",
137
+ note: "FHIR administrative gender code (male | female | other | unknown)"
138
+ },
139
+ {
140
+ path: "qualification[0].code.coding[0].code",
141
+ note: "MD \u2014 Doctor of Medicine credential code"
142
+ },
143
+ {
144
+ path: "qualification[0].code.coding[0].system",
145
+ note: "http://terminology.hl7.org/CodeSystem/v2-0360 \u2014 HL7 degree/license/certificate code system"
146
+ }
147
+ ];
148
+ }
149
+ function annotatePractitionerRole(_resource) {
150
+ return [
151
+ ...SHARED_NOTES,
152
+ {
153
+ path: "active",
154
+ note: "Whether this role relationship is currently active"
155
+ },
156
+ {
157
+ path: "practitioner",
158
+ note: "Reference to the Practitioner resource this role belongs to (urn:uuid: format)"
159
+ },
160
+ {
161
+ path: "organization",
162
+ note: "Reference to the Organization where this role is performed (urn:uuid: format)"
163
+ },
164
+ {
165
+ path: "code[0].coding[0].code",
166
+ note: "doctor \u2014 SNOMED CT code for general practitioner role"
167
+ },
168
+ {
169
+ path: "code[0].coding[0].system",
170
+ note: "SNOMED CT code system (http://snomed.info/sct)"
171
+ }
172
+ ];
173
+ }
174
+ function annotateOrganization(resource, locale) {
175
+ const identifiers = resource["identifier"];
176
+ return [
177
+ ...SHARED_NOTES,
178
+ ...identifierNotes(identifiers, locale.organizationIdentifiers),
179
+ {
180
+ path: "active",
181
+ note: "Whether this organization is currently active \u2014 always true for generated resources"
182
+ },
183
+ {
184
+ path: "type[0].coding[0].code",
185
+ note: "prov \u2014 FHIR organization type code for healthcare provider"
186
+ },
187
+ {
188
+ path: "type[0].coding[0].system",
189
+ note: "http://terminology.hl7.org/CodeSystem/organization-type \u2014 HL7 organization type code system"
190
+ },
191
+ {
192
+ path: "name",
193
+ note: "Locale-appropriate hospital or organization name"
194
+ },
195
+ ...addressNotes(locale)
196
+ ];
197
+ }
198
+ function annotateObservation(resource) {
199
+ const coding = resource["code"]?.coding;
200
+ const loincCode = coding?.[0]?.code;
201
+ const loinc = loincCode ? COMMON_LOINC_CODES.find((c) => c.code === loincCode) : void 0;
202
+ const vq = resource["valueQuantity"];
203
+ const category = resource["category"]?.[0]?.coding?.[0]?.code;
204
+ const notes = [...SHARED_NOTES];
205
+ if (loinc) {
206
+ notes.push({
207
+ path: "code.coding[0].code",
208
+ note: `LOINC ${loinc.code} \u2014 ${loinc.display}`
209
+ });
210
+ notes.push({
211
+ path: "code.coding[0].system",
212
+ note: "LOINC \u2014 Logical Observation Identifiers Names and Codes (https://loinc.org)"
213
+ });
214
+ notes.push({
215
+ path: "code.coding[0].display",
216
+ note: `Clinical display name for LOINC ${loinc.code}`
217
+ });
218
+ }
219
+ if (category) {
220
+ notes.push({
221
+ path: "category[0].coding[0].code",
222
+ note: `${category} \u2014 FHIR observation category; groups this observation for clinical workflows`
223
+ });
224
+ }
225
+ notes.push({
226
+ path: "status",
227
+ note: "FHIR observation status \u2014 'final' indicates a completed, unmodified observation"
228
+ });
229
+ notes.push({
230
+ path: "effectiveDateTime",
231
+ note: "Date and time the observation was clinically relevant \u2014 ISO 8601 format"
232
+ });
233
+ if (vq) {
234
+ if (loinc) {
235
+ notes.push({
236
+ path: "valueQuantity.value",
237
+ note: `${loinc.display} measurement \u2014 clinically plausible range: ${loinc.valueRange.min}\u2013${loinc.valueRange.max}`
238
+ });
239
+ notes.push({
240
+ path: "valueQuantity.unit",
241
+ note: `${loinc.unit} \u2014 UCUM code: ${loinc.unitCode}, per HL7 FHIR guidelines for units of measure`
242
+ });
243
+ } else {
244
+ notes.push({
245
+ path: "valueQuantity.value",
246
+ note: "Observation measurement value"
247
+ });
248
+ }
249
+ notes.push({
250
+ path: "valueQuantity.system",
251
+ note: "UCUM \u2014 Unified Code for Units of Measure (https://ucum.org), required by HL7 FHIR guidelines"
252
+ });
253
+ }
254
+ notes.push({
255
+ path: "subject",
256
+ note: "Reference to the Patient this observation belongs to (urn:uuid: format)"
257
+ });
258
+ return notes;
259
+ }
260
+ function annotateCondition(resource) {
261
+ const coding = resource["code"]?.coding;
262
+ const snomedCode = coding?.[0]?.code;
263
+ const snomed = snomedCode ? COMMON_SNOMED_CONDITIONS.find((c) => c.code === snomedCode) : void 0;
264
+ const notes = [...SHARED_NOTES];
265
+ if (snomed) {
266
+ notes.push({
267
+ path: "code.coding[0].code",
268
+ note: `SNOMED CT ${snomed.code} \u2014 ${snomed.display}`
269
+ });
270
+ notes.push({
271
+ path: "code.coding[0].system",
272
+ note: "SNOMED CT \u2014 Systematized Nomenclature of Medicine Clinical Terms (http://snomed.info/sct)"
273
+ });
274
+ }
275
+ notes.push({
276
+ path: "clinicalStatus.coding[0].code",
277
+ note: "FHIR condition clinical status \u2014 'active' or 'remission'; indicates current state of the condition"
278
+ });
279
+ notes.push({
280
+ path: "verificationStatus.coding[0].code",
281
+ note: "FHIR verification status \u2014 'confirmed' indicates the condition has been clinically verified"
282
+ });
283
+ notes.push({
284
+ path: "onsetDateTime",
285
+ note: "Date when the condition was first clinically noted \u2014 ISO 8601 format"
286
+ });
287
+ notes.push({
288
+ path: "subject",
289
+ note: "Reference to the Patient this condition belongs to (urn:uuid: format)"
290
+ });
291
+ return notes;
292
+ }
293
+ function annotateAllergyIntolerance(resource) {
294
+ const coding = resource["code"]?.coding;
295
+ const code = coding?.[0]?.code;
296
+ const allergy = code ? COMMON_ALLERGY_CODES.find((c) => c.code === code) : void 0;
297
+ const notes = [...SHARED_NOTES];
298
+ if (allergy) {
299
+ notes.push({
300
+ path: "code.coding[0].code",
301
+ note: `SNOMED CT ${allergy.code} \u2014 ${allergy.display}`
302
+ });
303
+ notes.push({
304
+ path: "code.coding[0].system",
305
+ note: "SNOMED CT \u2014 Systematized Nomenclature of Medicine Clinical Terms (http://snomed.info/sct)"
306
+ });
307
+ notes.push({
308
+ path: "category",
309
+ note: `FHIR allergy category \u2014 ${allergy.category}: type of substance causing the reaction`
310
+ });
311
+ }
312
+ notes.push({
313
+ path: "type",
314
+ note: "FHIR allergy type \u2014 'allergy' (immune-mediated) or 'intolerance' (non-immune mechanism)"
315
+ });
316
+ notes.push({
317
+ path: "criticality",
318
+ note: "FHIR allergy criticality \u2014 potential severity: low | high | unable-to-assess"
319
+ });
320
+ notes.push({
321
+ path: "patient",
322
+ note: "Reference to the Patient this allergy record belongs to (urn:uuid: format)"
323
+ });
324
+ notes.push({
325
+ path: "recordedDate",
326
+ note: "Date when this allergy was first recorded \u2014 ISO 8601 format (YYYY-MM-DD)"
327
+ });
328
+ return notes;
329
+ }
330
+ function annotateMedicationStatement(resource) {
331
+ const r4Coding = resource["medicationCodeableConcept"]?.coding;
332
+ const r5Coding = resource["medication"]?.concept?.coding;
333
+ const coding = r4Coding ?? r5Coding;
334
+ const code = coding?.[0]?.code;
335
+ const allMeds = [...COMMON_MEDICATION_CODES, ...US_RXNORM_MEDICATION_CODES];
336
+ const med = code ? allMeds.find((m) => m.code === code) : void 0;
337
+ const resourceType = resource["resourceType"];
338
+ const isR5 = resourceType === "MedicationUsage";
339
+ const notes = [...SHARED_NOTES];
340
+ if (med) {
341
+ const codePath = isR5 ? "medication.concept.coding[0].code" : "medicationCodeableConcept.coding[0].code";
342
+ const systemPath = isR5 ? "medication.concept.coding[0].system" : "medicationCodeableConcept.coding[0].system";
343
+ notes.push({
344
+ path: codePath,
345
+ note: `${med.system.includes("rxnorm") ? "RxNorm" : "SNOMED CT"} ${med.code} \u2014 ${med.display}; typical dose: ${med.typicalDoseMg}mg ${med.frequency}`
346
+ });
347
+ notes.push({
348
+ path: systemPath,
349
+ note: med.system.includes("rxnorm") ? "RxNorm \u2014 US National Library of Medicine drug terminology (http://www.nlm.nih.gov/research/umls/rxnorm)" : "SNOMED CT \u2014 Systematized Nomenclature of Medicine Clinical Terms (http://snomed.info/sct)"
350
+ });
351
+ }
352
+ notes.push({
353
+ path: "status",
354
+ note: `FHIR ${isR5 ? "MedicationUsage" : "MedicationStatement"} status \u2014 'active' or 'stopped'`
355
+ });
356
+ notes.push({
357
+ path: "subject",
358
+ note: "Reference to the Patient this medication record belongs to (urn:uuid: format)"
359
+ });
360
+ return notes;
361
+ }
362
+ function annotateBundle(resource) {
363
+ const entries = resource["entry"];
364
+ const resourceTypes = entries ? [...new Set(entries.map((e) => e.resource?.resourceType).filter(Boolean))] : [];
365
+ return [
366
+ ...SHARED_NOTES,
367
+ {
368
+ path: "type",
369
+ note: "FHIR Bundle type \u2014 'transaction' includes request entries; 'collection' is a plain grouping"
370
+ },
371
+ {
372
+ path: "entry",
373
+ note: `Bundle entries \u2014 ${entries?.length ?? 0} resources: ${resourceTypes.join(", ")}`
374
+ },
375
+ {
376
+ path: "entry[*].fullUrl",
377
+ note: "urn:uuid: format \u2014 temporary URIs used for cross-reference within the bundle"
378
+ },
379
+ {
380
+ path: "entry[*].request",
381
+ note: "FHIR transaction request metadata \u2014 method (PUT/POST) and URL for server processing"
382
+ }
383
+ ];
384
+ }
385
+ function annotateResource(resource, locale) {
386
+ switch (resource.resourceType) {
387
+ case "Patient":
388
+ return annotatePatient(resource, locale);
389
+ case "Practitioner":
390
+ return annotatePractitioner(resource, locale);
391
+ case "PractitionerRole":
392
+ return annotatePractitionerRole(resource);
393
+ case "Organization":
394
+ return annotateOrganization(resource, locale);
395
+ case "Observation":
396
+ return annotateObservation(resource);
397
+ case "Condition":
398
+ return annotateCondition(resource);
399
+ case "AllergyIntolerance":
400
+ return annotateAllergyIntolerance(resource);
401
+ case "MedicationStatement":
402
+ case "MedicationUsage":
403
+ return annotateMedicationStatement(resource);
404
+ case "Bundle":
405
+ return annotateBundle(resource);
406
+ default:
407
+ return [...SHARED_NOTES];
408
+ }
409
+ }
410
+
411
+ // src/cli/commands/generate.ts
412
+ var BUILDER_FACTORIES = {
413
+ patient: (l, c, s, v) => createPatientBuilder().locale(l).count(c).seed(s).fhirVersion(v).build(),
414
+ practitioner: (l, c, s, v) => createPractitionerBuilder().locale(l).count(c).seed(s).fhirVersion(v).build(),
415
+ "practitioner-role": (l, c, s, v) => createPractitionerRoleBuilder().locale(l).count(c).seed(s).fhirVersion(v).build(),
416
+ organization: (l, c, s, v) => createOrganizationBuilder().locale(l).count(c).seed(s).fhirVersion(v).build(),
417
+ observation: (l, c, s, v) => createObservationBuilder().locale(l).count(c).seed(s).fhirVersion(v).build(),
418
+ condition: (l, c, s, v) => createConditionBuilder().locale(l).count(c).seed(s).fhirVersion(v).build(),
419
+ "allergy-intolerance": (l, c, s, v) => createAllergyIntoleranceBuilder().locale(l).count(c).seed(s).fhirVersion(v).build(),
420
+ "medication-statement": (l, c, s, v) => createMedicationStatementBuilder().locale(l).count(c).seed(s).fhirVersion(v).build(),
421
+ bundle: (l, c, s, v) => createBundleBuilder().locale(l).count(c).seed(s).fhirVersion(v).build()
422
+ };
423
+ var CONCRETE_RESOURCE_TYPES = Object.keys(BUILDER_FACTORIES);
424
+ var ALL_RESOURCE_TYPES = [...CONCRETE_RESOURCE_TYPES, "all"];
425
+ function parseFaults(raw) {
426
+ const types = raw.split(",").map((s) => s.trim()).filter(Boolean);
427
+ const invalid = types.filter((t) => !FAULT_TYPES.includes(t));
428
+ if (invalid.length > 0) {
429
+ return {
430
+ error: `Unknown fault type(s): ${invalid.join(", ")}. Valid types: ${FAULT_TYPES.join(", ")}`
431
+ };
432
+ }
433
+ return types;
434
+ }
435
+ async function readStdinOverrides() {
436
+ if (process.stdin.isTTY) return null;
437
+ return new Promise((resolve, reject) => {
438
+ let data = "";
439
+ process.stdin.setEncoding("utf8");
440
+ process.stdin.on("data", (chunk) => {
441
+ data += chunk;
442
+ });
443
+ process.stdin.on("end", () => {
444
+ if (!data.trim()) {
445
+ resolve(null);
446
+ return;
447
+ }
448
+ try {
449
+ const parsed = JSON.parse(data);
450
+ resolve(parsed);
451
+ } catch {
452
+ reject(
453
+ new Error(
454
+ `stdin is not valid JSON. Received: ${data.slice(0, 120)}${data.length > 120 ? "\u2026" : ""}`
455
+ )
456
+ );
457
+ }
458
+ });
459
+ process.stdin.on("error", reject);
460
+ });
461
+ }
462
+ function formatIndex(i, total) {
463
+ const width = Math.max(3, String(total).length);
464
+ return String(i + 1).padStart(width, "0");
465
+ }
466
+ function writeToOutput(units, resourceType, outputDir, format, annotate) {
467
+ mkdirSync(outputDir, { recursive: true });
468
+ const firstResource = annotate ? units[0]?.resource : units[0];
469
+ const fhirType = firstResource?.["resourceType"] ?? resourceType;
470
+ if (format === "ndjson") {
471
+ const content = units.map((u) => JSON.stringify(u)).join("\n") + "\n";
472
+ const filePath = join(outputDir, `${fhirType}.ndjson`);
473
+ writeFileSync(filePath, content, "utf8");
474
+ } else {
475
+ for (let i = 0; i < units.length; i++) {
476
+ const idx = formatIndex(i, units.length);
477
+ const filePath = join(outputDir, `${fhirType}-${idx}.json`);
478
+ writeFileSync(filePath, JSON.stringify(units[i], null, 2) + "\n", "utf8");
479
+ }
480
+ }
481
+ process.stderr.write(
482
+ `Generated ${units.length} ${fhirType} resource${units.length === 1 ? "" : "s"} in ${outputDir}
483
+ `
484
+ );
485
+ }
486
+ function writeToStdout(units, format, pretty, forceCompact = false) {
487
+ if (format === "ndjson" || forceCompact) {
488
+ for (const u of units) {
489
+ process.stdout.write(JSON.stringify(u) + "\n");
490
+ }
491
+ } else if (units.length === 1) {
492
+ const indent = pretty ? 2 : void 0;
493
+ process.stdout.write(JSON.stringify(units[0], null, indent) + "\n");
494
+ } else {
495
+ const indent = pretty ? 2 : void 0;
496
+ process.stdout.write(JSON.stringify(units, null, indent) + "\n");
497
+ }
498
+ }
499
+ async function runGenerate(resourceType, opts) {
500
+ if (!ALL_RESOURCE_TYPES.includes(resourceType)) {
501
+ process.stderr.write(
502
+ `Error: unknown resource type "${resourceType}". Valid types: ${ALL_RESOURCE_TYPES.join(", ")}
503
+ `
504
+ );
505
+ process.exit(1);
506
+ }
507
+ if (!SUPPORTED_LOCALES.includes(opts.locale)) {
508
+ process.stderr.write(
509
+ `Error: unknown locale "${opts.locale}". Supported locales: ${SUPPORTED_LOCALES.join(", ")}
510
+ `
511
+ );
512
+ process.exit(1);
513
+ }
514
+ if (!SUPPORTED_FHIR_VERSIONS.includes(opts.fhirVersion)) {
515
+ process.stderr.write(
516
+ `Error: unknown FHIR version "${opts.fhirVersion}". Supported versions: ${SUPPORTED_FHIR_VERSIONS.join(", ")}
517
+ `
518
+ );
519
+ process.exit(1);
520
+ }
521
+ let faults = [];
522
+ if (opts.faults !== void 0) {
523
+ const parsed = parseFaults(opts.faults);
524
+ if ("error" in parsed) {
525
+ process.stderr.write(`Error: ${parsed.error}
526
+ `);
527
+ process.exit(1);
528
+ }
529
+ faults = parsed;
530
+ }
531
+ let overridesObj = {};
532
+ try {
533
+ const stdinOverrides = await readStdinOverrides();
534
+ if (stdinOverrides !== null) {
535
+ overridesObj = stdinOverrides;
536
+ }
537
+ } catch (err) {
538
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
539
+ `);
540
+ process.exit(1);
541
+ }
542
+ if (opts.overrides !== void 0) {
543
+ try {
544
+ const cliOverrides = JSON.parse(opts.overrides);
545
+ overridesObj = deepMerge(overridesObj, cliOverrides);
546
+ } catch {
547
+ process.stderr.write(
548
+ `Error: --overrides value is not valid JSON. Received: ${opts.overrides.slice(0, 120)}
549
+ `
550
+ );
551
+ process.exit(1);
552
+ }
553
+ }
554
+ const hasOverrides = Object.keys(overridesObj).length > 0;
555
+ const locale = opts.locale;
556
+ const fhirVersion = opts.fhirVersion;
557
+ const count = Number.parseInt(opts.count, 10);
558
+ const seed = opts.seed !== void 0 ? Number.parseInt(opts.seed, 10) : Math.floor(Math.random() * 2147483647);
559
+ const format = opts.format === "ndjson" ? "ndjson" : "json";
560
+ const localeDefinition = getLocale(locale);
561
+ const typesToGenerate = resourceType === "all" ? CONCRETE_RESOURCE_TYPES : [resourceType];
562
+ let coordinatedPractId;
563
+ let coordinatedOrgId;
564
+ if (resourceType === "all") {
565
+ const [firstPract] = BUILDER_FACTORIES["practitioner"](locale, count, seed, fhirVersion);
566
+ const [firstOrg] = BUILDER_FACTORIES["organization"](locale, count, seed, fhirVersion);
567
+ coordinatedPractId = firstPract?.["id"];
568
+ coordinatedOrgId = firstOrg?.["id"];
569
+ }
570
+ for (const type of typesToGenerate) {
571
+ let resources;
572
+ if (type === "practitioner-role" && coordinatedPractId !== void 0 && coordinatedOrgId !== void 0) {
573
+ resources = createPractitionerRoleBuilder().locale(locale).count(count).seed(seed).fhirVersion(fhirVersion).practitionerId(coordinatedPractId).organizationId(coordinatedOrgId).build();
574
+ } else {
575
+ resources = BUILDER_FACTORIES[type](locale, count, seed, fhirVersion);
576
+ }
577
+ if (faults.length > 0) {
578
+ const faultRng = createRng(seed + 1);
579
+ resources = resources.map((r) => injectFaults(r, faults, faultRng));
580
+ }
581
+ if (hasOverrides) {
582
+ resources = resources.map(
583
+ (r) => deepMerge(r, overridesObj)
584
+ );
585
+ }
586
+ const units = opts.annotate ? resources.map((r) => ({
587
+ resource: r,
588
+ notes: annotateResource(r, localeDefinition)
589
+ })) : resources;
590
+ if (opts.output !== void 0) {
591
+ try {
592
+ writeToOutput(units, type, opts.output, format, opts.annotate);
593
+ } catch (err) {
594
+ const message = err instanceof Error ? err.message : String(err);
595
+ process.stderr.write(`Error writing output: ${message}
596
+ `);
597
+ process.exit(2);
598
+ }
599
+ } else {
600
+ writeToStdout(units, format, opts.pretty, typesToGenerate.length > 1);
601
+ }
602
+ }
603
+ }
604
+ function registerGenerateCommand(program2) {
605
+ program2.command("generate <resource-type>").description(`Generate FHIR resources. Resource types: ${ALL_RESOURCE_TYPES.join(", ")}`).option("--locale <code>", "locale for identifiers and addresses", "us").option("--count <n>", "number of resources to generate", "1").option("--seed <n>", "seed for deterministic output").option(
606
+ "--fhir-version <version>",
607
+ `FHIR version to target: ${SUPPORTED_FHIR_VERSIONS.join(" | ")}`,
608
+ "R4"
609
+ ).option("--output <dir>", "output directory (one file per resource)").option("--format <fmt>", "output format: json | ndjson", "json").option("--pretty", "pretty-print JSON (default for stdout)", true).option("--no-pretty", "compact JSON output").option(
610
+ "--faults <types>",
611
+ `comma-separated fault types to inject. Valid: ${FAULT_TYPES.join(", ")}`
612
+ ).option(
613
+ "--overrides <json>",
614
+ "JSON object to deep-merge into every generated resource (also readable from stdin)"
615
+ ).option(
616
+ "--annotate",
617
+ "wrap each resource with a notes array explaining its fields in plain language",
618
+ false
619
+ ).action(runGenerate);
620
+ }
621
+
622
+ // src/cli/commands/locales.ts
623
+ function runLocales(opts) {
624
+ const locales = getAllLocales().map((locale) => ({
625
+ code: locale.code,
626
+ name: locale.name,
627
+ patientIdentifiers: locale.patientIdentifiers.map((id) => ({
628
+ name: id.name,
629
+ system: id.system,
630
+ ...id.algorithm !== void 0 ? { algorithm: id.algorithm } : {}
631
+ })),
632
+ practitionerIdentifiers: locale.practitionerIdentifiers.map((id) => ({
633
+ name: id.name,
634
+ system: id.system,
635
+ ...id.algorithm !== void 0 ? { algorithm: id.algorithm } : {}
636
+ })),
637
+ organizationIdentifiers: locale.organizationIdentifiers.map((id) => ({
638
+ name: id.name,
639
+ system: id.system,
640
+ ...id.algorithm !== void 0 ? { algorithm: id.algorithm } : {}
641
+ }))
642
+ }));
643
+ const indent = opts.pretty ? 2 : void 0;
644
+ process.stdout.write(JSON.stringify(locales, null, indent) + "\n");
645
+ }
646
+ function registerLocalesCommand(program2) {
647
+ program2.command("locales").description(
648
+ "List all supported locales with their identifier systems and check-digit algorithms"
649
+ ).option("--pretty", "pretty-print JSON (default for stdout)", true).option("--no-pretty", "compact JSON output").action(runLocales);
650
+ }
651
+
652
+ // src/cli/commands/describe.ts
653
+ var RESOURCE_DESCRIPTIONS = {
654
+ patient: {
655
+ resourceType: "Patient",
656
+ description: "FHIR Patient with locale-appropriate identifiers, names, address, and communication language",
657
+ fields: {
658
+ id: "UUID v4 \u2014 unique resource identifier",
659
+ identifier: "Locale-specific patient identifiers (check-digit validated where applicable)",
660
+ "name[0].use": "FHIR name use \u2014 'official' is the primary legal name for clinical records",
661
+ "name[0].family": "Family (last) name drawn from locale name pool",
662
+ "name[0].given": "Given (first) name drawn from locale name pool",
663
+ "name[0].prefix": "Cultural or professional title prefix (Mr, Mrs, Dr, etc.)",
664
+ "telecom[0]": "Home phone number in locale-appropriate format (FHIR ContactPoint)",
665
+ "telecom[1]": "Email address (FHIR ContactPoint)",
666
+ gender: "FHIR administrative gender: male | female",
667
+ birthDate: "Random date in adult age range \u2014 ISO 8601 format (YYYY-MM-DD)",
668
+ "address[0]": "Locale-appropriate street address with city and postal code",
669
+ "communication[0].language.coding[0].code": "BCP 47 language tag \u2014 primary language for the locale"
670
+ }
671
+ },
672
+ practitioner: {
673
+ resourceType: "Practitioner",
674
+ description: "FHIR Practitioner with locale-appropriate identifiers, professional name, and MD qualification",
675
+ fields: {
676
+ id: "UUID v4 \u2014 unique resource identifier",
677
+ identifier: "Locale-specific practitioner identifiers (check-digit validated where applicable)",
678
+ "name[0].prefix": "Professional title prefix (Dr, Prof)",
679
+ "name[0].family": "Family name drawn from locale name pool",
680
+ "name[0].given": "Given name drawn from locale name pool",
681
+ "telecom[0]": "Work email address (FHIR ContactPoint)",
682
+ gender: "FHIR administrative gender: male | female",
683
+ "qualification[0].code.coding[0].code": "MD \u2014 Doctor of Medicine credential code",
684
+ "qualification[0].code.coding[0].system": "http://terminology.hl7.org/CodeSystem/v2-0360 \u2014 HL7 degree/license/certificate code system"
685
+ }
686
+ },
687
+ "practitioner-role": {
688
+ resourceType: "PractitionerRole",
689
+ description: "FHIR PractitionerRole linking a Practitioner to an Organization with a coded role",
690
+ fields: {
691
+ id: "UUID v4 \u2014 unique resource identifier",
692
+ active: "Whether this role relationship is currently active",
693
+ practitioner: "Reference to the Practitioner (urn:uuid: format)",
694
+ organization: "Reference to the Organization where this role is performed (urn:uuid: format)",
695
+ "code[0].coding[0].code": "doctor \u2014 SNOMED CT code for general practitioner role",
696
+ "code[0].coding[0].system": "SNOMED CT code system (http://snomed.info/sct)"
697
+ }
698
+ },
699
+ organization: {
700
+ resourceType: "Organization",
701
+ description: "FHIR Organization (healthcare provider) with locale-appropriate identifiers and address",
702
+ fields: {
703
+ id: "UUID v4 \u2014 unique resource identifier",
704
+ identifier: "Locale-specific organization identifiers",
705
+ active: "Whether this organization is currently active \u2014 always true for generated resources",
706
+ "type[0].coding[0].code": "prov \u2014 FHIR organization type code for healthcare provider",
707
+ "type[0].coding[0].system": "http://terminology.hl7.org/CodeSystem/organization-type \u2014 HL7 organization type code system",
708
+ name: "Locale-appropriate hospital or clinic name",
709
+ "telecom[0]": "Main phone number (FHIR ContactPoint)",
710
+ "address[0]": "Locale-appropriate street address with city and postal code"
711
+ }
712
+ },
713
+ observation: {
714
+ resourceType: "Observation",
715
+ description: "FHIR Observation with a real LOINC code, value in a clinically plausible range, and HL7-consistent UCUM units",
716
+ fields: {
717
+ id: "UUID v4 \u2014 unique resource identifier",
718
+ status: "FHIR observation status \u2014 'final' indicates a completed, unmodified observation",
719
+ "category[0].coding[0].code": "FHIR observation category \u2014 vital-signs or laboratory",
720
+ "code.coding[0].code": "LOINC code identifying the observation type (https://loinc.org)",
721
+ "code.coding[0].system": "LOINC \u2014 Logical Observation Identifiers Names and Codes (https://loinc.org)",
722
+ "code.coding[0].display": "Human-readable LOINC display name",
723
+ subject: "Reference to the Patient this observation belongs to (urn:uuid: format)",
724
+ effectiveDateTime: "Date and time the observation was clinically relevant \u2014 ISO 8601 format",
725
+ "valueQuantity.value": "Numeric measurement in a clinically plausible range for the LOINC code",
726
+ "valueQuantity.unit": "Human-readable unit name (e.g., 'mmHg' for blood pressure)",
727
+ "valueQuantity.system": "UCUM \u2014 Unified Code for Units of Measure (https://ucum.org), required by HL7 FHIR",
728
+ "valueQuantity.code": "UCUM code for the unit (e.g., 'mm[Hg]'), machine-readable unit identifier"
729
+ }
730
+ },
731
+ condition: {
732
+ resourceType: "Condition",
733
+ description: "FHIR Condition with a real SNOMED CT code, clinical status, and verification status",
734
+ fields: {
735
+ id: "UUID v4 \u2014 unique resource identifier",
736
+ "clinicalStatus.coding[0].code": "FHIR condition clinical status \u2014 'active' or 'remission'",
737
+ "clinicalStatus.coding[0].system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
738
+ "verificationStatus.coding[0].code": "FHIR verification status \u2014 'confirmed' indicates clinically verified",
739
+ "code.coding[0].code": "SNOMED CT code identifying the clinical condition (https://snomed.info/sct)",
740
+ "code.coding[0].system": "SNOMED CT \u2014 Systematized Nomenclature of Medicine Clinical Terms",
741
+ "code.coding[0].display": "Human-readable SNOMED CT display name",
742
+ subject: "Reference to the Patient this condition belongs to (urn:uuid: format)",
743
+ onsetDateTime: "Date when the condition was first noted \u2014 ISO 8601 format"
744
+ }
745
+ },
746
+ "allergy-intolerance": {
747
+ resourceType: "AllergyIntolerance",
748
+ description: "FHIR AllergyIntolerance with SNOMED CT coded substance, type, category, and criticality",
749
+ fields: {
750
+ id: "UUID v4 \u2014 unique resource identifier",
751
+ type: "FHIR allergy type \u2014 'allergy' (immune-mediated) or 'intolerance' (non-immune)",
752
+ category: "FHIR allergy category \u2014 food | medication | environment | biologic",
753
+ criticality: "Potential severity \u2014 low | high | unable-to-assess",
754
+ "code.coding[0].code": "SNOMED CT code identifying the substance or reaction",
755
+ "code.coding[0].system": "SNOMED CT \u2014 Systematized Nomenclature of Medicine Clinical Terms",
756
+ patient: "Reference to the Patient this allergy record belongs to (urn:uuid: format)",
757
+ recordedDate: "Date when this allergy was first recorded \u2014 ISO 8601 (YYYY-MM-DD)"
758
+ }
759
+ },
760
+ "medication-statement": {
761
+ resourceType: "MedicationStatement (R4/R4B) | MedicationUsage (R5)",
762
+ description: "FHIR MedicationStatement (R4/R4B) or MedicationUsage (R5) with a SNOMED CT or RxNorm coded medication",
763
+ fields: {
764
+ id: "UUID v4 \u2014 unique resource identifier",
765
+ status: "FHIR medication status \u2014 'active' or 'stopped'",
766
+ "medicationCodeableConcept.coding[0].code": "SNOMED CT or RxNorm code identifying the medication (R4/R4B only)",
767
+ "medication.concept.coding[0].code": "SNOMED CT or RxNorm code identifying the medication (R5 MedicationUsage only)",
768
+ subject: "Reference to the Patient this medication record belongs to (urn:uuid: format)",
769
+ "effectivePeriod.start": "Start date of medication use \u2014 ISO 8601 format",
770
+ "effectivePeriod.end": "End date of medication use \u2014 ISO 8601 format (may be absent for active)"
771
+ }
772
+ },
773
+ bundle: {
774
+ resourceType: "Bundle",
775
+ description: "FHIR Bundle composing Patient, Practitioner, PractitionerRole, Organization, and clinical resources with automatic urn:uuid: reference wiring",
776
+ fields: {
777
+ id: "UUID v4 \u2014 unique bundle identifier",
778
+ type: "FHIR Bundle type \u2014 transaction | collection | searchset | document",
779
+ "entry[*].fullUrl": "urn:uuid: URIs used for cross-referencing resources within the bundle",
780
+ "entry[*].resource": "Embedded FHIR resource \u2014 Patient, Practitioner, Organization, Observation, etc.",
781
+ "entry[*].request": "Transaction request metadata \u2014 method and URL (transaction bundles only)",
782
+ "entry[*].search": "Search mode metadata \u2014 match or include (searchset bundles only)"
783
+ }
784
+ }
785
+ };
786
+ var DESCRIBABLE_RESOURCE_TYPES = Object.keys(
787
+ RESOURCE_DESCRIPTIONS
788
+ );
789
+ function runDescribe(resourceType, opts) {
790
+ if (!DESCRIBABLE_RESOURCE_TYPES.includes(resourceType)) {
791
+ process.stderr.write(
792
+ `Error: unknown resource type "${resourceType}". Valid types: ${DESCRIBABLE_RESOURCE_TYPES.join(", ")}
793
+ `
794
+ );
795
+ process.exit(1);
796
+ }
797
+ if (opts.locale !== void 0 && !SUPPORTED_LOCALES.includes(opts.locale)) {
798
+ process.stderr.write(
799
+ `Error: unknown locale "${opts.locale}". Supported locales: ${SUPPORTED_LOCALES.join(", ")}
800
+ `
801
+ );
802
+ process.exit(1);
803
+ }
804
+ const description = RESOURCE_DESCRIPTIONS[resourceType];
805
+ const output = {
806
+ resourceType: description.resourceType,
807
+ description: description.description,
808
+ fields: description.fields,
809
+ supportedLocales: [...SUPPORTED_LOCALES]
810
+ };
811
+ if (opts.locale !== void 0) {
812
+ const locale = getLocale(opts.locale);
813
+ output["localeDetail"] = {
814
+ code: locale.code,
815
+ name: locale.name,
816
+ patientIdentifiers: locale.patientIdentifiers.map((id) => ({
817
+ name: id.name,
818
+ system: id.system,
819
+ ...id.algorithm !== void 0 ? { algorithm: id.algorithm } : {}
820
+ })),
821
+ practitionerIdentifiers: locale.practitionerIdentifiers.map((id) => ({
822
+ name: id.name,
823
+ system: id.system,
824
+ ...id.algorithm !== void 0 ? { algorithm: id.algorithm } : {}
825
+ })),
826
+ organizationIdentifiers: locale.organizationIdentifiers.map((id) => ({
827
+ name: id.name,
828
+ system: id.system,
829
+ ...id.algorithm !== void 0 ? { algorithm: id.algorithm } : {}
830
+ }))
831
+ };
832
+ }
833
+ const indent = opts.pretty ? 2 : void 0;
834
+ process.stdout.write(JSON.stringify(output, null, indent) + "\n");
835
+ }
836
+ function registerDescribeCommand(program2) {
837
+ program2.command("describe <resource-type>").description(
838
+ `Describe what a resource type generates. Types: ${DESCRIBABLE_RESOURCE_TYPES.join(", ")}`
839
+ ).option("--locale <code>", "include locale-specific identifier details for this locale").option("--pretty", "pretty-print JSON (default for stdout)", true).option("--no-pretty", "compact JSON output").action(runDescribe);
840
+ }
841
+
842
+ // src/cli/index.ts
843
+ var program = new Command();
844
+ program.name("fhir-test-data").description("Generate valid FHIR R4 test resources with country-aware identifiers").version("0.1.0");
845
+ registerGenerateCommand(program);
846
+ registerLocalesCommand(program);
847
+ registerDescribeCommand(program);
848
+ var argv = process.argv[2] === "--" ? [...process.argv.slice(0, 2), ...process.argv.slice(3)] : process.argv;
849
+ await program.parseAsync(argv);
850
+ //# sourceMappingURL=index.js.map