@cosyte/synth 0.0.1
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.
- package/CHANGELOG.md +414 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/astm/index.cjs +847 -0
- package/dist/astm/index.cjs.map +1 -0
- package/dist/astm/index.d.cts +418 -0
- package/dist/astm/index.d.ts +418 -0
- package/dist/astm/index.mjs +828 -0
- package/dist/astm/index.mjs.map +1 -0
- package/dist/ccda/index.cjs +1103 -0
- package/dist/ccda/index.cjs.map +1 -0
- package/dist/ccda/index.d.cts +380 -0
- package/dist/ccda/index.d.ts +380 -0
- package/dist/ccda/index.mjs +1077 -0
- package/dist/ccda/index.mjs.map +1 -0
- package/dist/deid/index.cjs +2809 -0
- package/dist/deid/index.cjs.map +1 -0
- package/dist/deid/index.d.cts +464 -0
- package/dist/deid/index.d.ts +464 -0
- package/dist/deid/index.mjs +2793 -0
- package/dist/deid/index.mjs.map +1 -0
- package/dist/example-codes-DeXcnCSK.d.cts +105 -0
- package/dist/example-codes-DeXcnCSK.d.ts +105 -0
- package/dist/fhir/index.cjs +1429 -0
- package/dist/fhir/index.cjs.map +1 -0
- package/dist/fhir/index.d.cts +772 -0
- package/dist/fhir/index.d.ts +772 -0
- package/dist/fhir/index.mjs +1384 -0
- package/dist/fhir/index.mjs.map +1 -0
- package/dist/hl7/index.cjs +1012 -0
- package/dist/hl7/index.cjs.map +1 -0
- package/dist/hl7/index.d.cts +548 -0
- package/dist/hl7/index.d.ts +548 -0
- package/dist/hl7/index.mjs +990 -0
- package/dist/hl7/index.mjs.map +1 -0
- package/dist/index.cjs +535 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +407 -0
- package/dist/index.d.ts +407 -0
- package/dist/index.mjs +488 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ncpdp/index.cjs +714 -0
- package/dist/ncpdp/index.cjs.map +1 -0
- package/dist/ncpdp/index.d.cts +432 -0
- package/dist/ncpdp/index.d.ts +432 -0
- package/dist/ncpdp/index.mjs +695 -0
- package/dist/ncpdp/index.mjs.map +1 -0
- package/dist/providers-OLz3zAc-.d.cts +343 -0
- package/dist/providers-OLz3zAc-.d.ts +343 -0
- package/dist/quirk-DmkgoZdh.d.cts +239 -0
- package/dist/quirk-JLyO1Ncj.d.ts +239 -0
- package/dist/x12/index.cjs +920 -0
- package/dist/x12/index.cjs.map +1 -0
- package/dist/x12/index.d.cts +484 -0
- package/dist/x12/index.d.ts +484 -0
- package/dist/x12/index.mjs +892 -0
- package/dist/x12/index.mjs.map +1 -0
- package/package.json +210 -0
|
@@ -0,0 +1,2793 @@
|
|
|
1
|
+
import { extractHl7Loci, deidentifyHl7 } from '@cosyte/deid/hl7';
|
|
2
|
+
import { extractFhirLoci, deidentifyFhir } from '@cosyte/deid/fhir';
|
|
3
|
+
import { extractX12Loci, deidentifyX12 } from '@cosyte/deid/x12';
|
|
4
|
+
import { extractTelecomLoci, deidentifyTelecom } from '@cosyte/deid/ncpdp';
|
|
5
|
+
import { deidentifyCcda } from '@cosyte/deid/ccda';
|
|
6
|
+
import { serializeResource, complex, list, primitive, decimal } from '@cosyte/fhir';
|
|
7
|
+
import { serializeX12, parseX12, build835, build271, X12Decimal, build837P, build837I, build837D } from '@cosyte/x12';
|
|
8
|
+
import { serializeCcda, buildCcda, NCI_ROUTE, SNOMED_CT, CVX, RXNORM, LOINC } from '@cosyte/ccda';
|
|
9
|
+
import { parseTelecom, serializeTelecom, buildTelecomRequest } from '@cosyte/ncpdp/telecom';
|
|
10
|
+
import { buildMessage } from '@cosyte/hl7';
|
|
11
|
+
import { defineDeidPolicy, SAFE_HARBOR_CATEGORIES } from '@cosyte/deid';
|
|
12
|
+
|
|
13
|
+
// src/deid/index.ts
|
|
14
|
+
|
|
15
|
+
// src/rng/splitmix32.ts
|
|
16
|
+
function splitmix32(seed) {
|
|
17
|
+
let a = seed | 0;
|
|
18
|
+
return function next() {
|
|
19
|
+
a = a + 2654435769 | 0;
|
|
20
|
+
let t = a ^ a >>> 16;
|
|
21
|
+
t = Math.imul(t, 569420461);
|
|
22
|
+
t = t ^ t >>> 15;
|
|
23
|
+
t = Math.imul(t, 1935289751);
|
|
24
|
+
t = t ^ t >>> 15;
|
|
25
|
+
return t >>> 0;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/rng/sfc32.ts
|
|
30
|
+
function sfc32Next(s) {
|
|
31
|
+
s.a |= 0;
|
|
32
|
+
s.b |= 0;
|
|
33
|
+
s.c |= 0;
|
|
34
|
+
s.d |= 0;
|
|
35
|
+
const t = (s.a + s.b | 0) + s.d | 0;
|
|
36
|
+
s.d = s.d + 1 | 0;
|
|
37
|
+
s.a = s.b ^ s.b >>> 9;
|
|
38
|
+
s.b = s.c + (s.c << 3) | 0;
|
|
39
|
+
s.c = s.c << 21 | s.c >>> 11;
|
|
40
|
+
s.c = s.c + t | 0;
|
|
41
|
+
return t >>> 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/rng/rng.ts
|
|
45
|
+
var Sfc32Rng = class {
|
|
46
|
+
seed;
|
|
47
|
+
#state;
|
|
48
|
+
constructor(seed) {
|
|
49
|
+
this.seed = seed | 0;
|
|
50
|
+
const mix = splitmix32(this.seed);
|
|
51
|
+
this.#state = { a: mix(), b: mix(), c: mix(), d: mix() };
|
|
52
|
+
for (let i = 0; i < 8; i += 1) sfc32Next(this.#state);
|
|
53
|
+
}
|
|
54
|
+
nextUint32() {
|
|
55
|
+
return sfc32Next(this.#state);
|
|
56
|
+
}
|
|
57
|
+
float() {
|
|
58
|
+
return this.nextUint32() / 4294967296;
|
|
59
|
+
}
|
|
60
|
+
int(min, max) {
|
|
61
|
+
if (max < min) throw new RangeError(`Rng.int: max (${String(max)}) < min (${String(min)})`);
|
|
62
|
+
const span = max - min + 1;
|
|
63
|
+
return min + Math.floor(this.float() * span);
|
|
64
|
+
}
|
|
65
|
+
bool(p = 0.5) {
|
|
66
|
+
return this.float() < p;
|
|
67
|
+
}
|
|
68
|
+
pick(items) {
|
|
69
|
+
if (items.length === 0) throw new RangeError("Rng.pick: empty array");
|
|
70
|
+
return items[this.int(0, items.length - 1)];
|
|
71
|
+
}
|
|
72
|
+
digits(n) {
|
|
73
|
+
let out = "";
|
|
74
|
+
for (let i = 0; i < n; i += 1) out += String(this.int(0, 9));
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
function createRng(seed) {
|
|
79
|
+
return new Sfc32Rng(seed);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/hl7/field.ts
|
|
83
|
+
function componentsField(components) {
|
|
84
|
+
return {
|
|
85
|
+
repetitions: [{ components: components.map((c) => ({ subcomponents: [c] })) }],
|
|
86
|
+
isNull: false
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/safe/reserved.ts
|
|
91
|
+
var SYNTHETIC_ASSIGNING_AUTHORITY = Object.freeze({
|
|
92
|
+
/** The human-readable assigning-authority namespace id (HL7 HD.1). */
|
|
93
|
+
namespaceId: "COSYTE-SYNTH",
|
|
94
|
+
/** The universal id — an OID under HL7's example arc `2.16.840.1.113883.19` (HD.2). */
|
|
95
|
+
universalId: "2.16.840.1.113883.19.999",
|
|
96
|
+
/** The universal id type (HD.3). */
|
|
97
|
+
universalIdType: "ISO"
|
|
98
|
+
});
|
|
99
|
+
var RESERVED_EMAIL_DOMAINS = Object.freeze([
|
|
100
|
+
"example.com",
|
|
101
|
+
"example.org",
|
|
102
|
+
"example.net"
|
|
103
|
+
]);
|
|
104
|
+
var TEST_NET_V4_PREFIXES = Object.freeze([
|
|
105
|
+
"192.0.2",
|
|
106
|
+
// TEST-NET-1
|
|
107
|
+
"198.51.100",
|
|
108
|
+
// TEST-NET-2
|
|
109
|
+
"203.0.113"
|
|
110
|
+
// TEST-NET-3
|
|
111
|
+
]);
|
|
112
|
+
var DOC_V6_PREFIX = "2001:db8";
|
|
113
|
+
var NPI_LUHN_PREFIX = "80840";
|
|
114
|
+
function luhnMod10(digits) {
|
|
115
|
+
let sum = 0;
|
|
116
|
+
let double = false;
|
|
117
|
+
for (let i = digits.length - 1; i >= 0; i -= 1) {
|
|
118
|
+
let d = digits.charCodeAt(i) - 48;
|
|
119
|
+
if (d < 0 || d > 9) continue;
|
|
120
|
+
if (double) {
|
|
121
|
+
d *= 2;
|
|
122
|
+
if (d > 9) d -= 9;
|
|
123
|
+
}
|
|
124
|
+
sum += d;
|
|
125
|
+
double = !double;
|
|
126
|
+
}
|
|
127
|
+
return sum % 10;
|
|
128
|
+
}
|
|
129
|
+
function npiCheckDigit(base9) {
|
|
130
|
+
const partial = luhnMod10(`${NPI_LUHN_PREFIX}${base9}0`);
|
|
131
|
+
return (10 - partial) % 10;
|
|
132
|
+
}
|
|
133
|
+
var DEA_REGISTRANT_TYPES = Object.freeze([
|
|
134
|
+
"A",
|
|
135
|
+
"B",
|
|
136
|
+
"F",
|
|
137
|
+
"G",
|
|
138
|
+
"M",
|
|
139
|
+
"P",
|
|
140
|
+
"R",
|
|
141
|
+
"X"
|
|
142
|
+
]);
|
|
143
|
+
function deaCheckDigit(base6) {
|
|
144
|
+
let odd = 0;
|
|
145
|
+
let even = 0;
|
|
146
|
+
for (let i = 0; i < 6; i += 1) {
|
|
147
|
+
const digit = base6.charCodeAt(i) - 48;
|
|
148
|
+
if (i % 2 === 0) odd += digit;
|
|
149
|
+
else even += digit;
|
|
150
|
+
}
|
|
151
|
+
return (odd + 2 * even) % 10;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/safe/names-pool.ts
|
|
155
|
+
var SYNTHETIC_GIVEN_NAMES = Object.freeze([
|
|
156
|
+
"Testina",
|
|
157
|
+
"Fixtura",
|
|
158
|
+
"Synthos",
|
|
159
|
+
"Placeholda",
|
|
160
|
+
"Sampleton",
|
|
161
|
+
"Prototius",
|
|
162
|
+
"Stubbina",
|
|
163
|
+
"Exampla",
|
|
164
|
+
"Quilliam",
|
|
165
|
+
"Fabrica",
|
|
166
|
+
"Simula",
|
|
167
|
+
"Testry",
|
|
168
|
+
"Seedwin",
|
|
169
|
+
"Corpora",
|
|
170
|
+
"Reprodo",
|
|
171
|
+
"Mocktavia",
|
|
172
|
+
"Dummett",
|
|
173
|
+
"Voidwin",
|
|
174
|
+
"Deteria",
|
|
175
|
+
"Randomir"
|
|
176
|
+
]);
|
|
177
|
+
var SYNTHETIC_FAMILY_NAMES = Object.freeze([
|
|
178
|
+
"Testerson",
|
|
179
|
+
"Fauxman",
|
|
180
|
+
"Placeholt",
|
|
181
|
+
"Mockridge",
|
|
182
|
+
"Fixtingham",
|
|
183
|
+
"Synthwell",
|
|
184
|
+
"Dummerton",
|
|
185
|
+
"Examplewood",
|
|
186
|
+
"Fabricant",
|
|
187
|
+
"Simulacre",
|
|
188
|
+
"Nonesuch",
|
|
189
|
+
"Seedman",
|
|
190
|
+
"Corpusworth",
|
|
191
|
+
"Reprodus",
|
|
192
|
+
"Voidmark",
|
|
193
|
+
"Deterwood",
|
|
194
|
+
"Randomson",
|
|
195
|
+
"Quillfeather",
|
|
196
|
+
"Notreal",
|
|
197
|
+
"Genfield"
|
|
198
|
+
]);
|
|
199
|
+
var SYNTHETIC_STREET_NAMES = Object.freeze([
|
|
200
|
+
"Fixture Lane",
|
|
201
|
+
"Sample Street",
|
|
202
|
+
"Placeholder Avenue",
|
|
203
|
+
"Synthetic Way",
|
|
204
|
+
"Example Boulevard",
|
|
205
|
+
"Testing Terrace",
|
|
206
|
+
"Mock Road",
|
|
207
|
+
"Prototype Court"
|
|
208
|
+
]);
|
|
209
|
+
var SYNTHETIC_CITY_NAMES = Object.freeze([
|
|
210
|
+
"Faketon",
|
|
211
|
+
"Synthville",
|
|
212
|
+
"Exampleburg",
|
|
213
|
+
"Testford",
|
|
214
|
+
"Mockhaven",
|
|
215
|
+
"Fixtureton"
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
// src/safe/providers.ts
|
|
219
|
+
function ssn(rng, block = "never-issued") {
|
|
220
|
+
if (block === "advertising") {
|
|
221
|
+
return `987-65-432${String(rng.int(0, 9))}`;
|
|
222
|
+
}
|
|
223
|
+
const area = rng.int(900, 999);
|
|
224
|
+
const group = rng.digits(2);
|
|
225
|
+
const serial = rng.digits(4);
|
|
226
|
+
return `${String(area)}-${group}-${serial}`;
|
|
227
|
+
}
|
|
228
|
+
function phone(rng) {
|
|
229
|
+
const area = `${String(rng.int(2, 9))}${rng.digits(2)}`;
|
|
230
|
+
const line = `01${rng.digits(2)}`;
|
|
231
|
+
return `(${area}) 555-${line}`;
|
|
232
|
+
}
|
|
233
|
+
function name(rng) {
|
|
234
|
+
return { given: rng.pick(SYNTHETIC_GIVEN_NAMES), family: rng.pick(SYNTHETIC_FAMILY_NAMES) };
|
|
235
|
+
}
|
|
236
|
+
function email(rng, person) {
|
|
237
|
+
const domain = rng.pick(RESERVED_EMAIL_DOMAINS);
|
|
238
|
+
const slug = person ? `${person.given}.${person.family}`.toLowerCase() : `synth${rng.digits(6)}`;
|
|
239
|
+
return `${slug}@${domain}`;
|
|
240
|
+
}
|
|
241
|
+
function ipv4(rng) {
|
|
242
|
+
return `${rng.pick(TEST_NET_V4_PREFIXES)}.${String(rng.int(1, 254))}`;
|
|
243
|
+
}
|
|
244
|
+
function ipv6(rng) {
|
|
245
|
+
const tail = rng.nextUint32().toString(16).padStart(4, "0").slice(-4);
|
|
246
|
+
return `${DOC_V6_PREFIX}::${tail}`;
|
|
247
|
+
}
|
|
248
|
+
function uuid(rng) {
|
|
249
|
+
const bytes = new Uint8Array(16);
|
|
250
|
+
for (let i = 0; i < 16; i += 1) bytes[i] = rng.int(0, 255);
|
|
251
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
252
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
253
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
|
|
254
|
+
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
|
255
|
+
}
|
|
256
|
+
function npi(rng) {
|
|
257
|
+
const base9 = rng.digits(9);
|
|
258
|
+
const wrongCheck = (npiCheckDigit(base9) + 1) % 10;
|
|
259
|
+
return `${base9}${String(wrongCheck)}`;
|
|
260
|
+
}
|
|
261
|
+
function dea(rng, person) {
|
|
262
|
+
const type = rng.pick(DEA_REGISTRANT_TYPES);
|
|
263
|
+
const initialSource = person?.family ?? rng.pick(SYNTHETIC_FAMILY_NAMES);
|
|
264
|
+
const initial = initialSource.slice(0, 1).toUpperCase();
|
|
265
|
+
const base6 = rng.digits(6);
|
|
266
|
+
const wrongCheck = (deaCheckDigit(base6) + 1) % 10;
|
|
267
|
+
return `${type}${initial}${base6}${String(wrongCheck)}`;
|
|
268
|
+
}
|
|
269
|
+
function identifier(rng, typeCode = "MR") {
|
|
270
|
+
return {
|
|
271
|
+
value: rng.digits(8),
|
|
272
|
+
typeCode,
|
|
273
|
+
assigningAuthority: SYNTHETIC_ASSIGNING_AUTHORITY.namespaceId,
|
|
274
|
+
assigningAuthorityOid: SYNTHETIC_ASSIGNING_AUTHORITY.universalId
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function address(rng) {
|
|
278
|
+
const number = rng.int(1, 9999);
|
|
279
|
+
return {
|
|
280
|
+
street: `${String(number)} ${rng.pick(SYNTHETIC_STREET_NAMES)}`,
|
|
281
|
+
city: rng.pick(SYNTHETIC_CITY_NAMES),
|
|
282
|
+
state: rng.pick(US_STATES),
|
|
283
|
+
zip: "00000"
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function dateYmd(rng, minYear = 1930, maxYear = 2010) {
|
|
287
|
+
const year = rng.int(minYear, maxYear);
|
|
288
|
+
const month = rng.int(1, 12);
|
|
289
|
+
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
290
|
+
const day = rng.int(1, daysInMonth);
|
|
291
|
+
return `${String(year).padStart(4, "0")}${String(month).padStart(2, "0")}${String(day).padStart(2, "0")}`;
|
|
292
|
+
}
|
|
293
|
+
var US_STATES = Object.freeze([
|
|
294
|
+
"AL",
|
|
295
|
+
"AK",
|
|
296
|
+
"AZ",
|
|
297
|
+
"AR",
|
|
298
|
+
"CA",
|
|
299
|
+
"CO",
|
|
300
|
+
"CT",
|
|
301
|
+
"DE",
|
|
302
|
+
"FL",
|
|
303
|
+
"GA",
|
|
304
|
+
"HI",
|
|
305
|
+
"ID",
|
|
306
|
+
"IL",
|
|
307
|
+
"IN",
|
|
308
|
+
"IA",
|
|
309
|
+
"KS",
|
|
310
|
+
"KY",
|
|
311
|
+
"LA",
|
|
312
|
+
"ME",
|
|
313
|
+
"MD",
|
|
314
|
+
"MA",
|
|
315
|
+
"MI",
|
|
316
|
+
"MN",
|
|
317
|
+
"MS",
|
|
318
|
+
"MO",
|
|
319
|
+
"MT",
|
|
320
|
+
"NE",
|
|
321
|
+
"NV",
|
|
322
|
+
"NH",
|
|
323
|
+
"NJ",
|
|
324
|
+
"NM",
|
|
325
|
+
"NY",
|
|
326
|
+
"NC",
|
|
327
|
+
"ND",
|
|
328
|
+
"OH",
|
|
329
|
+
"OK",
|
|
330
|
+
"OR",
|
|
331
|
+
"PA",
|
|
332
|
+
"RI",
|
|
333
|
+
"SC",
|
|
334
|
+
"SD",
|
|
335
|
+
"TN",
|
|
336
|
+
"TX",
|
|
337
|
+
"UT",
|
|
338
|
+
"VT",
|
|
339
|
+
"VA",
|
|
340
|
+
"WA",
|
|
341
|
+
"WV",
|
|
342
|
+
"WI",
|
|
343
|
+
"WY"
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
// src/safe/index.ts
|
|
347
|
+
var safe = Object.freeze({
|
|
348
|
+
ssn,
|
|
349
|
+
phone,
|
|
350
|
+
name,
|
|
351
|
+
email,
|
|
352
|
+
ipv4,
|
|
353
|
+
ipv6,
|
|
354
|
+
uuid,
|
|
355
|
+
identifier,
|
|
356
|
+
address,
|
|
357
|
+
dateYmd,
|
|
358
|
+
npi,
|
|
359
|
+
dea
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// src/hl7/common.ts
|
|
363
|
+
function seededTimestamp(rng) {
|
|
364
|
+
const date = safe.dateYmd(rng, 2020, 2025);
|
|
365
|
+
const hh = String(rng.int(0, 23)).padStart(2, "0");
|
|
366
|
+
const mm = String(rng.int(0, 59)).padStart(2, "0");
|
|
367
|
+
const ss = String(rng.int(0, 59)).padStart(2, "0");
|
|
368
|
+
return `${date}${hh}${mm}${ss}`;
|
|
369
|
+
}
|
|
370
|
+
function mshScaffold(rng, type) {
|
|
371
|
+
const timestamp = seededTimestamp(rng);
|
|
372
|
+
const controlId = `SYNTH${rng.digits(10)}`;
|
|
373
|
+
const message = buildMessage({
|
|
374
|
+
type,
|
|
375
|
+
sendingApp: "COSYTE-SYNTH",
|
|
376
|
+
sendingFacility: "SYNTH-FAC",
|
|
377
|
+
receivingApp: "RECEIVER",
|
|
378
|
+
receivingFacility: "RECV-FAC",
|
|
379
|
+
controlId,
|
|
380
|
+
timestamp,
|
|
381
|
+
version: "2.5",
|
|
382
|
+
processingId: "P"
|
|
383
|
+
});
|
|
384
|
+
return { message, timestamp, controlId };
|
|
385
|
+
}
|
|
386
|
+
function patientIdentity(rng) {
|
|
387
|
+
const person = safe.name(rng);
|
|
388
|
+
const mrn = safe.identifier(rng, "MR");
|
|
389
|
+
const dob = safe.dateYmd(rng, 1930, 2010);
|
|
390
|
+
const sex = rng.pick(["M", "F"]);
|
|
391
|
+
const address2 = safe.address(rng);
|
|
392
|
+
const phone2 = safe.phone(rng);
|
|
393
|
+
const ssnDigits = safe.ssn(rng).replace(/-/g, "");
|
|
394
|
+
return { person, mrn, dob, sex, address: address2, phone: phone2, ssnDigits };
|
|
395
|
+
}
|
|
396
|
+
function pidSegment(id) {
|
|
397
|
+
return [
|
|
398
|
+
"1",
|
|
399
|
+
// PID-1 set id
|
|
400
|
+
"",
|
|
401
|
+
// PID-2 (legacy, absent)
|
|
402
|
+
componentsField([id.mrn.value, "", "", id.mrn.assigningAuthority, id.mrn.typeCode]),
|
|
403
|
+
// PID-3 CX
|
|
404
|
+
"",
|
|
405
|
+
// PID-4
|
|
406
|
+
componentsField([id.person.family, id.person.given]),
|
|
407
|
+
// PID-5 XPN
|
|
408
|
+
"",
|
|
409
|
+
// PID-6
|
|
410
|
+
id.dob,
|
|
411
|
+
// PID-7 DOB
|
|
412
|
+
id.sex,
|
|
413
|
+
// PID-8 sex
|
|
414
|
+
"",
|
|
415
|
+
// PID-9
|
|
416
|
+
"",
|
|
417
|
+
// PID-10
|
|
418
|
+
componentsField([id.address.street, "", id.address.city, id.address.state, id.address.zip]),
|
|
419
|
+
// PID-11 XAD
|
|
420
|
+
"",
|
|
421
|
+
// PID-12
|
|
422
|
+
id.phone,
|
|
423
|
+
// PID-13 phone
|
|
424
|
+
"",
|
|
425
|
+
// PID-14
|
|
426
|
+
"",
|
|
427
|
+
// PID-15
|
|
428
|
+
"",
|
|
429
|
+
// PID-16
|
|
430
|
+
"",
|
|
431
|
+
// PID-17
|
|
432
|
+
"",
|
|
433
|
+
// PID-18
|
|
434
|
+
id.ssnDigits
|
|
435
|
+
// PID-19 SSN (digits)
|
|
436
|
+
];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/hl7/adt.ts
|
|
440
|
+
function generateAdt(options = {}) {
|
|
441
|
+
const seed = options.seed ?? 0;
|
|
442
|
+
const trigger = options.trigger ?? "A01";
|
|
443
|
+
const rng = createRng(seed);
|
|
444
|
+
const { message, timestamp } = mshScaffold(rng, `ADT^${trigger}`);
|
|
445
|
+
message.addSegment("EVN", [trigger, timestamp]);
|
|
446
|
+
message.addSegment("PID", pidSegment(patientIdentity(rng)));
|
|
447
|
+
const patientClass = rng.pick(["I", "O", "E"]);
|
|
448
|
+
message.addSegment("PV1", [
|
|
449
|
+
"1",
|
|
450
|
+
// PV1-1 set id
|
|
451
|
+
patientClass,
|
|
452
|
+
// PV1-2 patient class
|
|
453
|
+
componentsField(["SYNTHWARD", String(rng.int(1, 999)).padStart(3, "0"), "01"])
|
|
454
|
+
// PV1-3 PL
|
|
455
|
+
]);
|
|
456
|
+
return message;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/hl7/example-codes.ts
|
|
460
|
+
var EXAMPLE_LAB_OBSERVATIONS = Object.freeze([
|
|
461
|
+
Object.freeze({ code: "4548-4", text: "Hemoglobin A1c", system: "LN", units: "%" }),
|
|
462
|
+
Object.freeze({ code: "718-7", text: "Hemoglobin", system: "LN", units: "g/dL" }),
|
|
463
|
+
Object.freeze({ code: "2345-7", text: "Glucose", system: "LN", units: "mg/dL" }),
|
|
464
|
+
Object.freeze({ code: "2951-2", text: "Sodium", system: "LN", units: "mmol/L" }),
|
|
465
|
+
Object.freeze({ code: "2823-3", text: "Potassium", system: "LN", units: "mmol/L" }),
|
|
466
|
+
Object.freeze({ code: "789-8", text: "Erythrocytes", system: "LN", units: "10*6/uL" })
|
|
467
|
+
]);
|
|
468
|
+
var EXAMPLE_ORDER_SERVICES = Object.freeze([
|
|
469
|
+
Object.freeze({ code: "24323-8", text: "Comprehensive metabolic panel", system: "LN" }),
|
|
470
|
+
Object.freeze({ code: "58410-2", text: "CBC panel", system: "LN" }),
|
|
471
|
+
Object.freeze({ code: "24356-8", text: "Urinalysis panel", system: "LN" }),
|
|
472
|
+
Object.freeze({ code: "24331-1", text: "Lipid panel", system: "LN" })
|
|
473
|
+
]);
|
|
474
|
+
var EXAMPLE_VACCINES = Object.freeze([
|
|
475
|
+
Object.freeze({ code: "08", text: "Hep B, adolescent or pediatric", system: "CVX" }),
|
|
476
|
+
Object.freeze({ code: "20", text: "DTaP", system: "CVX" }),
|
|
477
|
+
Object.freeze({ code: "03", text: "MMR", system: "CVX" }),
|
|
478
|
+
Object.freeze({ code: "10", text: "IPV", system: "CVX" }),
|
|
479
|
+
Object.freeze({ code: "141", text: "Influenza, seasonal, injectable", system: "CVX" }),
|
|
480
|
+
Object.freeze({ code: "21", text: "Varicella", system: "CVX" })
|
|
481
|
+
]);
|
|
482
|
+
|
|
483
|
+
// src/hl7/oru.ts
|
|
484
|
+
function obxSegment(rng, setId) {
|
|
485
|
+
const obs = rng.pick(EXAMPLE_LAB_OBSERVATIONS);
|
|
486
|
+
const value = `${String(rng.int(1, 300))}.${String(rng.int(0, 9))}`;
|
|
487
|
+
const observedAt = seededTimestamp(rng);
|
|
488
|
+
return [
|
|
489
|
+
String(setId),
|
|
490
|
+
// OBX-1 set id
|
|
491
|
+
"NM",
|
|
492
|
+
// OBX-2 value type (numeric)
|
|
493
|
+
componentsField([obs.code, obs.text, obs.system]),
|
|
494
|
+
// OBX-3 observation identifier (CE)
|
|
495
|
+
"",
|
|
496
|
+
// OBX-4 observation sub-id
|
|
497
|
+
value,
|
|
498
|
+
// OBX-5 observation value
|
|
499
|
+
obs.units ?? "",
|
|
500
|
+
// OBX-6 units
|
|
501
|
+
"",
|
|
502
|
+
// OBX-7 reference range
|
|
503
|
+
"N",
|
|
504
|
+
// OBX-8 abnormal flags (normal)
|
|
505
|
+
"",
|
|
506
|
+
// OBX-9
|
|
507
|
+
"",
|
|
508
|
+
// OBX-10
|
|
509
|
+
"F",
|
|
510
|
+
// OBX-11 observation result status (final)
|
|
511
|
+
"",
|
|
512
|
+
// OBX-12
|
|
513
|
+
"",
|
|
514
|
+
// OBX-13
|
|
515
|
+
observedAt
|
|
516
|
+
// OBX-14 date/time of the observation
|
|
517
|
+
];
|
|
518
|
+
}
|
|
519
|
+
function generateOru(options = {}) {
|
|
520
|
+
const seed = options.seed ?? 0;
|
|
521
|
+
const rng = createRng(seed);
|
|
522
|
+
const { message } = mshScaffold(rng, "ORU^R01");
|
|
523
|
+
message.addSegment("PID", pidSegment(patientIdentity(rng)));
|
|
524
|
+
const service = rng.pick(EXAMPLE_ORDER_SERVICES);
|
|
525
|
+
const placer = rng.digits(8);
|
|
526
|
+
const filler = rng.digits(8);
|
|
527
|
+
message.addSegment("OBR", [
|
|
528
|
+
"1",
|
|
529
|
+
// OBR-1 set id
|
|
530
|
+
placer,
|
|
531
|
+
// OBR-2 placer order number
|
|
532
|
+
filler,
|
|
533
|
+
// OBR-3 filler order number
|
|
534
|
+
componentsField([service.code, service.text, service.system]),
|
|
535
|
+
// OBR-4 universal service id (CE)
|
|
536
|
+
"",
|
|
537
|
+
// OBR-5
|
|
538
|
+
"",
|
|
539
|
+
// OBR-6
|
|
540
|
+
seededTimestamp(rng)
|
|
541
|
+
// OBR-7 observation date/time
|
|
542
|
+
]);
|
|
543
|
+
const obxCount = rng.int(1, 3);
|
|
544
|
+
for (let i = 1; i <= obxCount; i += 1) {
|
|
545
|
+
message.addSegment("OBX", obxSegment(rng, i));
|
|
546
|
+
}
|
|
547
|
+
return message;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/hl7/orm.ts
|
|
551
|
+
function generateOrm(options = {}) {
|
|
552
|
+
const seed = options.seed ?? 0;
|
|
553
|
+
const rng = createRng(seed);
|
|
554
|
+
const { message } = mshScaffold(rng, "ORM^O01");
|
|
555
|
+
message.addSegment("PID", pidSegment(patientIdentity(rng)));
|
|
556
|
+
const service = rng.pick(EXAMPLE_ORDER_SERVICES);
|
|
557
|
+
const placer = rng.digits(8);
|
|
558
|
+
const filler = rng.digits(8);
|
|
559
|
+
const orderedAt = seededTimestamp(rng);
|
|
560
|
+
message.addSegment("ORC", [
|
|
561
|
+
"NW",
|
|
562
|
+
// ORC-1 order control (new order)
|
|
563
|
+
placer,
|
|
564
|
+
// ORC-2 placer order number
|
|
565
|
+
filler,
|
|
566
|
+
// ORC-3 filler order number
|
|
567
|
+
"",
|
|
568
|
+
// ORC-4 placer group number
|
|
569
|
+
"",
|
|
570
|
+
// ORC-5 order status
|
|
571
|
+
"",
|
|
572
|
+
// ORC-6
|
|
573
|
+
"",
|
|
574
|
+
// ORC-7 quantity/timing
|
|
575
|
+
"",
|
|
576
|
+
// ORC-8
|
|
577
|
+
orderedAt
|
|
578
|
+
// ORC-9 date/time of transaction
|
|
579
|
+
]);
|
|
580
|
+
message.addSegment("OBR", [
|
|
581
|
+
"1",
|
|
582
|
+
// OBR-1 set id
|
|
583
|
+
placer,
|
|
584
|
+
// OBR-2 placer order number (matches ORC)
|
|
585
|
+
filler,
|
|
586
|
+
// OBR-3 filler order number (matches ORC)
|
|
587
|
+
componentsField([service.code, service.text, service.system])
|
|
588
|
+
// OBR-4 universal service id (CE)
|
|
589
|
+
]);
|
|
590
|
+
return message;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/hl7/siu.ts
|
|
594
|
+
function generateSiu(options = {}) {
|
|
595
|
+
const seed = options.seed ?? 0;
|
|
596
|
+
const rng = createRng(seed);
|
|
597
|
+
const { message } = mshScaffold(rng, "SIU^S12");
|
|
598
|
+
const placerAppt = rng.digits(8);
|
|
599
|
+
const fillerAppt = rng.digits(8);
|
|
600
|
+
const startAt = seededTimestamp(rng);
|
|
601
|
+
const durationMinutes = String(rng.int(15, 90));
|
|
602
|
+
message.addSegment("SCH", [
|
|
603
|
+
placerAppt,
|
|
604
|
+
// SCH-1 placer appointment id
|
|
605
|
+
fillerAppt,
|
|
606
|
+
// SCH-2 filler appointment id
|
|
607
|
+
"",
|
|
608
|
+
// SCH-3 occurrence number
|
|
609
|
+
"",
|
|
610
|
+
// SCH-4 placer group number
|
|
611
|
+
"",
|
|
612
|
+
// SCH-5 schedule id
|
|
613
|
+
"",
|
|
614
|
+
// SCH-6 event reason
|
|
615
|
+
componentsField(["ROUTINE", "Routine appointment", "L"]),
|
|
616
|
+
// SCH-7 appointment reason (CE)
|
|
617
|
+
"",
|
|
618
|
+
// SCH-8 appointment type
|
|
619
|
+
durationMinutes,
|
|
620
|
+
// SCH-9 appointment duration
|
|
621
|
+
componentsField(["min", "minutes", "UCUM"]),
|
|
622
|
+
// SCH-10 appointment duration units (CE)
|
|
623
|
+
componentsField(["1", "", startAt, "", "", "", durationMinutes])
|
|
624
|
+
// SCH-11 appointment timing quantity (TQ)
|
|
625
|
+
]);
|
|
626
|
+
message.addSegment("PID", pidSegment(patientIdentity(rng)));
|
|
627
|
+
message.addSegment("RGS", [
|
|
628
|
+
"1",
|
|
629
|
+
// RGS-1 set id
|
|
630
|
+
"A"
|
|
631
|
+
// RGS-2 segment action code (add)
|
|
632
|
+
]);
|
|
633
|
+
message.addSegment("AIL", [
|
|
634
|
+
"1",
|
|
635
|
+
// AIL-1 set id
|
|
636
|
+
"A",
|
|
637
|
+
// AIL-2 segment action code
|
|
638
|
+
componentsField(["SYNTHCLINIC", "Synthetic Clinic", "COSYTE-SYNTH"])
|
|
639
|
+
// AIL-3 location resource id
|
|
640
|
+
]);
|
|
641
|
+
return message;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/hl7/vxu.ts
|
|
645
|
+
function generateVxu(options = {}) {
|
|
646
|
+
const seed = options.seed ?? 0;
|
|
647
|
+
const rng = createRng(seed);
|
|
648
|
+
const { message } = mshScaffold(rng, "VXU^V04");
|
|
649
|
+
message.addSegment("PID", pidSegment(patientIdentity(rng)));
|
|
650
|
+
const placer = rng.digits(8);
|
|
651
|
+
const filler = rng.digits(8);
|
|
652
|
+
message.addSegment("ORC", [
|
|
653
|
+
"RE",
|
|
654
|
+
// ORC-1 order control (observation to follow / record)
|
|
655
|
+
placer,
|
|
656
|
+
// ORC-2 placer order number
|
|
657
|
+
filler
|
|
658
|
+
// ORC-3 filler order number
|
|
659
|
+
]);
|
|
660
|
+
const vaccine = rng.pick(EXAMPLE_VACCINES);
|
|
661
|
+
const administeredAt = seededTimestamp(rng);
|
|
662
|
+
const doseAmount = String(rng.int(1, 5) / 10);
|
|
663
|
+
message.addSegment("RXA", [
|
|
664
|
+
"0",
|
|
665
|
+
// RXA-1 give sub-id counter
|
|
666
|
+
"1",
|
|
667
|
+
// RXA-2 administration sub-id counter
|
|
668
|
+
administeredAt,
|
|
669
|
+
// RXA-3 date/time start of administration
|
|
670
|
+
administeredAt,
|
|
671
|
+
// RXA-4 date/time end of administration
|
|
672
|
+
componentsField([vaccine.code, vaccine.text, vaccine.system]),
|
|
673
|
+
// RXA-5 administered code (CE)
|
|
674
|
+
doseAmount,
|
|
675
|
+
// RXA-6 administered amount
|
|
676
|
+
componentsField(["mL", "milliliters", "UCUM"]),
|
|
677
|
+
// RXA-7 administered units (CE)
|
|
678
|
+
"",
|
|
679
|
+
// RXA-8
|
|
680
|
+
"",
|
|
681
|
+
// RXA-9 administration notes
|
|
682
|
+
"",
|
|
683
|
+
// RXA-10
|
|
684
|
+
"",
|
|
685
|
+
// RXA-11
|
|
686
|
+
"",
|
|
687
|
+
// RXA-12
|
|
688
|
+
"",
|
|
689
|
+
// RXA-13
|
|
690
|
+
"",
|
|
691
|
+
// RXA-14
|
|
692
|
+
"",
|
|
693
|
+
// RXA-15 substance lot number
|
|
694
|
+
"",
|
|
695
|
+
// RXA-16
|
|
696
|
+
"",
|
|
697
|
+
// RXA-17
|
|
698
|
+
"",
|
|
699
|
+
// RXA-18
|
|
700
|
+
"",
|
|
701
|
+
// RXA-19
|
|
702
|
+
"CP"
|
|
703
|
+
// RXA-20 completion status (complete)
|
|
704
|
+
]);
|
|
705
|
+
message.addSegment("RXR", [
|
|
706
|
+
componentsField(["IM", "Intramuscular", "HL70162"]),
|
|
707
|
+
// RXR-1 route (CE)
|
|
708
|
+
componentsField(["LD", "Left Deltoid", "HL70163"])
|
|
709
|
+
// RXR-2 administration site (CE)
|
|
710
|
+
]);
|
|
711
|
+
return message;
|
|
712
|
+
}
|
|
713
|
+
function generateHl7(kind, seed) {
|
|
714
|
+
switch (kind) {
|
|
715
|
+
case "ADT^A01":
|
|
716
|
+
return generateAdt({ seed, trigger: "A01" });
|
|
717
|
+
case "ADT^A04":
|
|
718
|
+
return generateAdt({ seed, trigger: "A04" });
|
|
719
|
+
case "ADT^A08":
|
|
720
|
+
return generateAdt({ seed, trigger: "A08" });
|
|
721
|
+
case "ORU^R01":
|
|
722
|
+
return generateOru({ seed });
|
|
723
|
+
case "ORM^O01":
|
|
724
|
+
return generateOrm({ seed });
|
|
725
|
+
case "SIU^S12":
|
|
726
|
+
return generateSiu({ seed });
|
|
727
|
+
case "VXU^V04":
|
|
728
|
+
return generateVxu({ seed });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/fhir/us-core.ts
|
|
733
|
+
var US_CORE_PROFILE = Object.freeze({
|
|
734
|
+
/** US Core Patient. */
|
|
735
|
+
PATIENT: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient",
|
|
736
|
+
/** US Core Condition (Problems and Health Concerns). */
|
|
737
|
+
CONDITION: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition-problems-health-concerns",
|
|
738
|
+
/** US Core Laboratory Result Observation. */
|
|
739
|
+
OBSERVATION_LAB: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab",
|
|
740
|
+
/** US Core Vital Signs (derived from the base FHIR vital-signs profile). */
|
|
741
|
+
VITAL_SIGNS: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs",
|
|
742
|
+
/** US Core MedicationRequest. */
|
|
743
|
+
MEDICATION_REQUEST: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-medicationrequest",
|
|
744
|
+
/** US Core Encounter. */
|
|
745
|
+
ENCOUNTER: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter",
|
|
746
|
+
/** US Core DiagnosticReport Profile for Laboratory Results Reporting. */
|
|
747
|
+
DIAGNOSTIC_REPORT_LAB: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-diagnosticreport-lab",
|
|
748
|
+
/** US Core Immunization. */
|
|
749
|
+
IMMUNIZATION: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-immunization",
|
|
750
|
+
/** US Core AllergyIntolerance. */
|
|
751
|
+
ALLERGY_INTOLERANCE: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-allergyintolerance",
|
|
752
|
+
/** US Core Procedure. */
|
|
753
|
+
PROCEDURE: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-procedure"
|
|
754
|
+
});
|
|
755
|
+
var US_CORE_RACE_EXTENSION = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race";
|
|
756
|
+
var US_CORE_ETHNICITY_EXTENSION = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity";
|
|
757
|
+
var US_CORE_BIRTHSEX_EXTENSION = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex";
|
|
758
|
+
var SYSTEM = Object.freeze({
|
|
759
|
+
/** FHIR `administrative-gender` (`Patient.gender`). */
|
|
760
|
+
ADMINISTRATIVE_GENDER: "http://hl7.org/fhir/administrative-gender",
|
|
761
|
+
/** HL7 Terminology `observation-category`. */
|
|
762
|
+
OBSERVATION_CATEGORY: "http://terminology.hl7.org/CodeSystem/observation-category",
|
|
763
|
+
/** HL7 Terminology `condition-category`. */
|
|
764
|
+
CONDITION_CATEGORY: "http://terminology.hl7.org/CodeSystem/condition-category",
|
|
765
|
+
/** HL7 Terminology `condition-clinical`. */
|
|
766
|
+
CONDITION_CLINICAL: "http://terminology.hl7.org/CodeSystem/condition-clinical",
|
|
767
|
+
/** HL7 Terminology `condition-ver-status`. */
|
|
768
|
+
CONDITION_VER_STATUS: "http://terminology.hl7.org/CodeSystem/condition-ver-status",
|
|
769
|
+
/** HL7 v2 `0203` identifier-type (`Identifier.type.coding.code` = `MR`). */
|
|
770
|
+
IDENTIFIER_TYPE: "http://terminology.hl7.org/CodeSystem/v2-0203",
|
|
771
|
+
/** OMB race & ethnicity category system (US Core race/ethnicity `ombCategory`). */
|
|
772
|
+
OMB_RACE_ETHNICITY: "urn:oid:2.16.840.1.113883.6.238",
|
|
773
|
+
/** LOINC — `Observation.code` (lab + vital-signs). */
|
|
774
|
+
LOINC: "http://loinc.org",
|
|
775
|
+
/** SNOMED CT — `Condition.code`. */
|
|
776
|
+
SNOMED: "http://snomed.info/sct",
|
|
777
|
+
/** RxNorm — `MedicationRequest.medicationCodeableConcept` + an allergen substance. */
|
|
778
|
+
RXNORM: "http://www.nlm.nih.gov/research/umls/rxnorm",
|
|
779
|
+
/** UCUM — `Quantity.system` for units of measure. */
|
|
780
|
+
UCUM: "http://unitsofmeasure.org",
|
|
781
|
+
/** CVX (CDC vaccine administered) — `Immunization.vaccineCode`. */
|
|
782
|
+
CVX: "http://hl7.org/fhir/sid/cvx",
|
|
783
|
+
/** HL7 v3 `ActCode` — `Encounter.class`. */
|
|
784
|
+
V3_ACT_CODE: "http://terminology.hl7.org/CodeSystem/v3-ActCode",
|
|
785
|
+
/** HL7 v2 `0074` diagnostic-service-section — `DiagnosticReport.category` (`LAB`). */
|
|
786
|
+
DIAGNOSTIC_SERVICE_SECTION: "http://terminology.hl7.org/CodeSystem/v2-0074",
|
|
787
|
+
/** HL7 Terminology `allergyintolerance-clinical` — `AllergyIntolerance.clinicalStatus`. */
|
|
788
|
+
ALLERGY_CLINICAL: "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical",
|
|
789
|
+
/** HL7 Terminology `allergyintolerance-verification` — `AllergyIntolerance.verificationStatus`. */
|
|
790
|
+
ALLERGY_VERIFICATION: "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification"
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// src/fhir/builder.ts
|
|
794
|
+
function prop(name2, value) {
|
|
795
|
+
return { name: name2, value };
|
|
796
|
+
}
|
|
797
|
+
function str(value) {
|
|
798
|
+
return primitive(value);
|
|
799
|
+
}
|
|
800
|
+
function dec(raw) {
|
|
801
|
+
return primitive(decimal(raw));
|
|
802
|
+
}
|
|
803
|
+
function bool(value) {
|
|
804
|
+
return primitive(value);
|
|
805
|
+
}
|
|
806
|
+
function coding(concept) {
|
|
807
|
+
return complex([
|
|
808
|
+
prop("system", str(concept.system)),
|
|
809
|
+
prop("code", str(concept.code)),
|
|
810
|
+
prop("display", str(concept.display))
|
|
811
|
+
]);
|
|
812
|
+
}
|
|
813
|
+
function codeableConcept(concept) {
|
|
814
|
+
return complex([prop("coding", list([coding(concept)]))]);
|
|
815
|
+
}
|
|
816
|
+
function reference(ref) {
|
|
817
|
+
return complex([prop("reference", str(ref))]);
|
|
818
|
+
}
|
|
819
|
+
function narrative(text) {
|
|
820
|
+
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
821
|
+
return complex([
|
|
822
|
+
prop("status", str("generated")),
|
|
823
|
+
prop("div", str(`<div xmlns="http://www.w3.org/1999/xhtml"><p>${escaped}</p></div>`))
|
|
824
|
+
]);
|
|
825
|
+
}
|
|
826
|
+
function meta(profiles) {
|
|
827
|
+
return complex([prop("profile", list(profiles.map((p) => str(p))))]);
|
|
828
|
+
}
|
|
829
|
+
function toFhirDate(ymd) {
|
|
830
|
+
return `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}`;
|
|
831
|
+
}
|
|
832
|
+
function fhirPatientIdentity(rng) {
|
|
833
|
+
const id = safe.uuid(rng);
|
|
834
|
+
const person = safe.name(rng);
|
|
835
|
+
const mrn = safe.identifier(rng, "MR");
|
|
836
|
+
const birthDate = toFhirDate(safe.dateYmd(rng, 1930, 2010));
|
|
837
|
+
const gender = rng.pick(["male", "female"]);
|
|
838
|
+
const address2 = safe.address(rng);
|
|
839
|
+
const phone2 = safe.phone(rng);
|
|
840
|
+
const email2 = safe.email(rng, person);
|
|
841
|
+
return { id, person, mrn, birthDate, gender, address: address2, phone: phone2, email: email2 };
|
|
842
|
+
}
|
|
843
|
+
function mrnIdentifier(mrn) {
|
|
844
|
+
return complex([
|
|
845
|
+
prop(
|
|
846
|
+
"type",
|
|
847
|
+
complex([
|
|
848
|
+
prop(
|
|
849
|
+
"coding",
|
|
850
|
+
list([
|
|
851
|
+
coding({
|
|
852
|
+
system: SYSTEM.IDENTIFIER_TYPE,
|
|
853
|
+
code: "MR",
|
|
854
|
+
display: "Medical Record Number"
|
|
855
|
+
})
|
|
856
|
+
])
|
|
857
|
+
)
|
|
858
|
+
])
|
|
859
|
+
),
|
|
860
|
+
prop("system", str(`urn:oid:${mrn.assigningAuthorityOid}`)),
|
|
861
|
+
prop("value", str(mrn.value))
|
|
862
|
+
]);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/fhir/example-codes.ts
|
|
866
|
+
var EXAMPLE_LAB_OBSERVATIONS2 = Object.freeze([
|
|
867
|
+
Object.freeze({
|
|
868
|
+
system: SYSTEM.LOINC,
|
|
869
|
+
code: "2345-7",
|
|
870
|
+
display: "Glucose [Mass/volume] in Serum or Plasma",
|
|
871
|
+
unit: "mg/dL",
|
|
872
|
+
low: 70,
|
|
873
|
+
high: 140,
|
|
874
|
+
decimals: 0
|
|
875
|
+
}),
|
|
876
|
+
Object.freeze({
|
|
877
|
+
system: SYSTEM.LOINC,
|
|
878
|
+
code: "718-7",
|
|
879
|
+
display: "Hemoglobin [Mass/volume] in Blood",
|
|
880
|
+
unit: "g/dL",
|
|
881
|
+
low: 12,
|
|
882
|
+
high: 17,
|
|
883
|
+
decimals: 1
|
|
884
|
+
}),
|
|
885
|
+
Object.freeze({
|
|
886
|
+
system: SYSTEM.LOINC,
|
|
887
|
+
code: "2951-2",
|
|
888
|
+
display: "Sodium [Moles/volume] in Serum or Plasma",
|
|
889
|
+
unit: "mmol/L",
|
|
890
|
+
low: 135,
|
|
891
|
+
high: 145,
|
|
892
|
+
decimals: 0
|
|
893
|
+
}),
|
|
894
|
+
Object.freeze({
|
|
895
|
+
system: SYSTEM.LOINC,
|
|
896
|
+
code: "2823-3",
|
|
897
|
+
display: "Potassium [Moles/volume] in Serum or Plasma",
|
|
898
|
+
unit: "mmol/L",
|
|
899
|
+
low: 4,
|
|
900
|
+
high: 5,
|
|
901
|
+
decimals: 1
|
|
902
|
+
}),
|
|
903
|
+
Object.freeze({
|
|
904
|
+
system: SYSTEM.LOINC,
|
|
905
|
+
code: "4548-4",
|
|
906
|
+
display: "Hemoglobin A1c/Hemoglobin.total in Blood",
|
|
907
|
+
unit: "%",
|
|
908
|
+
low: 4,
|
|
909
|
+
high: 9,
|
|
910
|
+
decimals: 1
|
|
911
|
+
})
|
|
912
|
+
]);
|
|
913
|
+
var EXAMPLE_VITAL_SIGNS = Object.freeze([
|
|
914
|
+
Object.freeze({
|
|
915
|
+
system: SYSTEM.LOINC,
|
|
916
|
+
code: "8867-4",
|
|
917
|
+
display: "Heart rate",
|
|
918
|
+
unit: "/min",
|
|
919
|
+
low: 55,
|
|
920
|
+
high: 100,
|
|
921
|
+
decimals: 0
|
|
922
|
+
}),
|
|
923
|
+
Object.freeze({
|
|
924
|
+
system: SYSTEM.LOINC,
|
|
925
|
+
code: "9279-1",
|
|
926
|
+
display: "Respiratory rate",
|
|
927
|
+
unit: "/min",
|
|
928
|
+
low: 12,
|
|
929
|
+
high: 20,
|
|
930
|
+
decimals: 0
|
|
931
|
+
}),
|
|
932
|
+
Object.freeze({
|
|
933
|
+
system: SYSTEM.LOINC,
|
|
934
|
+
code: "8310-5",
|
|
935
|
+
display: "Body temperature",
|
|
936
|
+
unit: "Cel",
|
|
937
|
+
low: 36,
|
|
938
|
+
high: 38,
|
|
939
|
+
decimals: 1
|
|
940
|
+
}),
|
|
941
|
+
Object.freeze({
|
|
942
|
+
system: SYSTEM.LOINC,
|
|
943
|
+
code: "29463-7",
|
|
944
|
+
display: "Body weight",
|
|
945
|
+
unit: "kg",
|
|
946
|
+
low: 50,
|
|
947
|
+
high: 100,
|
|
948
|
+
decimals: 1
|
|
949
|
+
}),
|
|
950
|
+
Object.freeze({
|
|
951
|
+
system: SYSTEM.LOINC,
|
|
952
|
+
code: "8302-2",
|
|
953
|
+
display: "Body height",
|
|
954
|
+
unit: "cm",
|
|
955
|
+
low: 150,
|
|
956
|
+
high: 190,
|
|
957
|
+
decimals: 0
|
|
958
|
+
})
|
|
959
|
+
]);
|
|
960
|
+
var EXAMPLE_CONDITIONS = Object.freeze([
|
|
961
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "59621000", display: "Essential hypertension" }),
|
|
962
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "44054006", display: "Type 2 diabetes mellitus" }),
|
|
963
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "195967001", display: "Asthma" }),
|
|
964
|
+
Object.freeze({
|
|
965
|
+
system: SYSTEM.SNOMED,
|
|
966
|
+
code: "13645005",
|
|
967
|
+
display: "Chronic obstructive lung disease"
|
|
968
|
+
}),
|
|
969
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "38341003", display: "Hypertensive disorder" })
|
|
970
|
+
]);
|
|
971
|
+
var EXAMPLE_MEDICATIONS = Object.freeze([
|
|
972
|
+
Object.freeze({
|
|
973
|
+
system: SYSTEM.RXNORM,
|
|
974
|
+
code: "1049221",
|
|
975
|
+
display: "Acetaminophen 325 MG Oral Tablet"
|
|
976
|
+
}),
|
|
977
|
+
Object.freeze({ system: SYSTEM.RXNORM, code: "197361", display: "Amlodipine 5 MG Oral Tablet" }),
|
|
978
|
+
Object.freeze({
|
|
979
|
+
system: SYSTEM.RXNORM,
|
|
980
|
+
code: "860975",
|
|
981
|
+
display: "24 HR Metformin hydrochloride 500 MG Extended Release Oral Tablet"
|
|
982
|
+
}),
|
|
983
|
+
Object.freeze({
|
|
984
|
+
system: SYSTEM.RXNORM,
|
|
985
|
+
code: "308136",
|
|
986
|
+
display: "Amoxicillin 250 MG Oral Capsule"
|
|
987
|
+
}),
|
|
988
|
+
Object.freeze({
|
|
989
|
+
system: SYSTEM.RXNORM,
|
|
990
|
+
code: "617314",
|
|
991
|
+
display: "Atorvastatin 40 MG Oral Tablet"
|
|
992
|
+
})
|
|
993
|
+
]);
|
|
994
|
+
var EXAMPLE_RACE_CATEGORIES = Object.freeze([
|
|
995
|
+
Object.freeze({ system: SYSTEM.OMB_RACE_ETHNICITY, code: "2106-3", display: "White" }),
|
|
996
|
+
Object.freeze({
|
|
997
|
+
system: SYSTEM.OMB_RACE_ETHNICITY,
|
|
998
|
+
code: "2054-5",
|
|
999
|
+
display: "Black or African American"
|
|
1000
|
+
}),
|
|
1001
|
+
Object.freeze({ system: SYSTEM.OMB_RACE_ETHNICITY, code: "2028-9", display: "Asian" }),
|
|
1002
|
+
Object.freeze({
|
|
1003
|
+
system: SYSTEM.OMB_RACE_ETHNICITY,
|
|
1004
|
+
code: "1002-5",
|
|
1005
|
+
display: "American Indian or Alaska Native"
|
|
1006
|
+
}),
|
|
1007
|
+
Object.freeze({
|
|
1008
|
+
system: SYSTEM.OMB_RACE_ETHNICITY,
|
|
1009
|
+
code: "2076-8",
|
|
1010
|
+
display: "Native Hawaiian or Other Pacific Islander"
|
|
1011
|
+
})
|
|
1012
|
+
]);
|
|
1013
|
+
var EXAMPLE_ETHNICITY_CATEGORIES = Object.freeze([
|
|
1014
|
+
Object.freeze({
|
|
1015
|
+
system: SYSTEM.OMB_RACE_ETHNICITY,
|
|
1016
|
+
code: "2135-2",
|
|
1017
|
+
display: "Hispanic or Latino"
|
|
1018
|
+
}),
|
|
1019
|
+
Object.freeze({
|
|
1020
|
+
system: SYSTEM.OMB_RACE_ETHNICITY,
|
|
1021
|
+
code: "2186-5",
|
|
1022
|
+
display: "Not Hispanic or Latino"
|
|
1023
|
+
})
|
|
1024
|
+
]);
|
|
1025
|
+
var EXAMPLE_VACCINES2 = Object.freeze([
|
|
1026
|
+
Object.freeze({
|
|
1027
|
+
system: SYSTEM.CVX,
|
|
1028
|
+
code: "140",
|
|
1029
|
+
display: "Influenza, seasonal, injectable, preservative free"
|
|
1030
|
+
}),
|
|
1031
|
+
Object.freeze({ system: SYSTEM.CVX, code: "03", display: "MMR" }),
|
|
1032
|
+
Object.freeze({ system: SYSTEM.CVX, code: "20", display: "DTaP" }),
|
|
1033
|
+
Object.freeze({ system: SYSTEM.CVX, code: "133", display: "Pneumococcal conjugate PCV 13" }),
|
|
1034
|
+
Object.freeze({
|
|
1035
|
+
system: SYSTEM.CVX,
|
|
1036
|
+
code: "208",
|
|
1037
|
+
display: "COVID-19, mRNA, LNP-S, PF, 30 mcg/0.3 mL dose"
|
|
1038
|
+
})
|
|
1039
|
+
]);
|
|
1040
|
+
var EXAMPLE_ALLERGENS = Object.freeze([
|
|
1041
|
+
Object.freeze({ system: SYSTEM.RXNORM, code: "7980", display: "Penicillin G" }),
|
|
1042
|
+
Object.freeze({ system: SYSTEM.RXNORM, code: "2670", display: "Codeine" }),
|
|
1043
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "762952008", display: "Peanut (substance)" }),
|
|
1044
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "227493005", display: "Cashew nuts (substance)" }),
|
|
1045
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "3718001", display: "Cow's milk (substance)" })
|
|
1046
|
+
]);
|
|
1047
|
+
var EXAMPLE_ALLERGY_MANIFESTATIONS = Object.freeze([
|
|
1048
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "247472004", display: "Wheal (finding)" }),
|
|
1049
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "126485001", display: "Urticaria (disorder)" }),
|
|
1050
|
+
Object.freeze({
|
|
1051
|
+
system: SYSTEM.SNOMED,
|
|
1052
|
+
code: "271807003",
|
|
1053
|
+
display: "Eruption of skin (disorder)"
|
|
1054
|
+
}),
|
|
1055
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "267036007", display: "Dyspnea (finding)" }),
|
|
1056
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "422587007", display: "Nausea (finding)" })
|
|
1057
|
+
]);
|
|
1058
|
+
var EXAMPLE_PROCEDURES = Object.freeze([
|
|
1059
|
+
Object.freeze({
|
|
1060
|
+
system: SYSTEM.SNOMED,
|
|
1061
|
+
code: "80146002",
|
|
1062
|
+
display: "Excision of appendix (procedure)"
|
|
1063
|
+
}),
|
|
1064
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "73761001", display: "Colonoscopy (procedure)" }),
|
|
1065
|
+
Object.freeze({
|
|
1066
|
+
system: SYSTEM.SNOMED,
|
|
1067
|
+
code: "5880005",
|
|
1068
|
+
display: "Physical examination procedure (procedure)"
|
|
1069
|
+
}),
|
|
1070
|
+
Object.freeze({
|
|
1071
|
+
system: SYSTEM.SNOMED,
|
|
1072
|
+
code: "108252007",
|
|
1073
|
+
display: "Laboratory procedure (procedure)"
|
|
1074
|
+
}),
|
|
1075
|
+
Object.freeze({ system: SYSTEM.SNOMED, code: "71651007", display: "Mammography (procedure)" })
|
|
1076
|
+
]);
|
|
1077
|
+
var EXAMPLE_DIAGNOSTIC_REPORTS = Object.freeze([
|
|
1078
|
+
Object.freeze({
|
|
1079
|
+
system: SYSTEM.LOINC,
|
|
1080
|
+
code: "24323-8",
|
|
1081
|
+
display: "Comprehensive metabolic 2000 panel - Serum or Plasma"
|
|
1082
|
+
}),
|
|
1083
|
+
Object.freeze({
|
|
1084
|
+
system: SYSTEM.LOINC,
|
|
1085
|
+
code: "58410-2",
|
|
1086
|
+
display: "CBC panel - Blood by Automated count"
|
|
1087
|
+
}),
|
|
1088
|
+
Object.freeze({
|
|
1089
|
+
system: SYSTEM.LOINC,
|
|
1090
|
+
code: "24357-6",
|
|
1091
|
+
display: "Urinalysis complete panel - Urine"
|
|
1092
|
+
}),
|
|
1093
|
+
Object.freeze({
|
|
1094
|
+
system: SYSTEM.LOINC,
|
|
1095
|
+
code: "24331-1",
|
|
1096
|
+
display: "Lipid 1996 panel - Serum or Plasma"
|
|
1097
|
+
}),
|
|
1098
|
+
Object.freeze({
|
|
1099
|
+
system: SYSTEM.LOINC,
|
|
1100
|
+
code: "24321-2",
|
|
1101
|
+
display: "Basic metabolic 1998 panel - Serum or Plasma"
|
|
1102
|
+
})
|
|
1103
|
+
]);
|
|
1104
|
+
var EXAMPLE_ENCOUNTER_TYPES = Object.freeze([
|
|
1105
|
+
Object.freeze({
|
|
1106
|
+
system: SYSTEM.SNOMED,
|
|
1107
|
+
code: "308335008",
|
|
1108
|
+
display: "Patient encounter procedure (procedure)"
|
|
1109
|
+
}),
|
|
1110
|
+
Object.freeze({
|
|
1111
|
+
system: SYSTEM.SNOMED,
|
|
1112
|
+
code: "185349003",
|
|
1113
|
+
display: "Encounter for check up (procedure)"
|
|
1114
|
+
}),
|
|
1115
|
+
Object.freeze({
|
|
1116
|
+
system: SYSTEM.SNOMED,
|
|
1117
|
+
code: "185347001",
|
|
1118
|
+
display: "Encounter for problem (procedure)"
|
|
1119
|
+
}),
|
|
1120
|
+
Object.freeze({
|
|
1121
|
+
system: SYSTEM.SNOMED,
|
|
1122
|
+
code: "390906007",
|
|
1123
|
+
display: "Follow-up encounter (procedure)"
|
|
1124
|
+
})
|
|
1125
|
+
]);
|
|
1126
|
+
var EXAMPLE_ENCOUNTER_CLASSES = Object.freeze([
|
|
1127
|
+
Object.freeze({ system: SYSTEM.V3_ACT_CODE, code: "AMB", display: "ambulatory" }),
|
|
1128
|
+
Object.freeze({ system: SYSTEM.V3_ACT_CODE, code: "EMER", display: "emergency" }),
|
|
1129
|
+
Object.freeze({ system: SYSTEM.V3_ACT_CODE, code: "IMP", display: "inpatient encounter" })
|
|
1130
|
+
]);
|
|
1131
|
+
|
|
1132
|
+
// src/fhir/patient.ts
|
|
1133
|
+
function ombExtension(url, category) {
|
|
1134
|
+
return complex([
|
|
1135
|
+
prop("url", str(url)),
|
|
1136
|
+
prop(
|
|
1137
|
+
"extension",
|
|
1138
|
+
list([
|
|
1139
|
+
complex([prop("url", str("ombCategory")), prop("valueCoding", coding(category))]),
|
|
1140
|
+
complex([prop("url", str("text")), prop("valueString", str(category.display))])
|
|
1141
|
+
])
|
|
1142
|
+
)
|
|
1143
|
+
]);
|
|
1144
|
+
}
|
|
1145
|
+
function generatePatient(options = {}) {
|
|
1146
|
+
const { seed = 0, profile = "base" } = options;
|
|
1147
|
+
const rng = createRng(seed);
|
|
1148
|
+
const id = fhirPatientIdentity(rng);
|
|
1149
|
+
const usCore = profile === "us-core";
|
|
1150
|
+
const props = [
|
|
1151
|
+
prop("resourceType", str("Patient")),
|
|
1152
|
+
prop("id", str(`syn-patient-${id.id}`))
|
|
1153
|
+
];
|
|
1154
|
+
if (usCore) props.push(prop("meta", meta([US_CORE_PROFILE.PATIENT])));
|
|
1155
|
+
props.push(
|
|
1156
|
+
prop(
|
|
1157
|
+
"text",
|
|
1158
|
+
narrative(
|
|
1159
|
+
`Synthetic patient ${id.person.given} ${id.person.family} (${id.gender}, born ${id.birthDate}).`
|
|
1160
|
+
)
|
|
1161
|
+
)
|
|
1162
|
+
);
|
|
1163
|
+
if (usCore) {
|
|
1164
|
+
const race = rng.pick(EXAMPLE_RACE_CATEGORIES);
|
|
1165
|
+
const ethnicity = rng.pick(EXAMPLE_ETHNICITY_CATEGORIES);
|
|
1166
|
+
const birthsex = id.gender === "male" ? "M" : "F";
|
|
1167
|
+
props.push(
|
|
1168
|
+
prop(
|
|
1169
|
+
"extension",
|
|
1170
|
+
list([
|
|
1171
|
+
ombExtension(US_CORE_RACE_EXTENSION, race),
|
|
1172
|
+
ombExtension(US_CORE_ETHNICITY_EXTENSION, ethnicity),
|
|
1173
|
+
complex([prop("url", str(US_CORE_BIRTHSEX_EXTENSION)), prop("valueCode", str(birthsex))])
|
|
1174
|
+
])
|
|
1175
|
+
)
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
props.push(prop("identifier", list([mrnIdentifier(id.mrn)])));
|
|
1179
|
+
props.push(prop("active", bool(true)));
|
|
1180
|
+
props.push(
|
|
1181
|
+
prop(
|
|
1182
|
+
"name",
|
|
1183
|
+
list([
|
|
1184
|
+
complex([
|
|
1185
|
+
prop("use", str("official")),
|
|
1186
|
+
prop("family", str(id.person.family)),
|
|
1187
|
+
prop("given", list([str(id.person.given)]))
|
|
1188
|
+
])
|
|
1189
|
+
])
|
|
1190
|
+
)
|
|
1191
|
+
);
|
|
1192
|
+
props.push(
|
|
1193
|
+
prop(
|
|
1194
|
+
"telecom",
|
|
1195
|
+
list([
|
|
1196
|
+
complex([
|
|
1197
|
+
prop("system", str("phone")),
|
|
1198
|
+
prop("value", str(id.phone)),
|
|
1199
|
+
prop("use", str("home"))
|
|
1200
|
+
]),
|
|
1201
|
+
complex([prop("system", str("email")), prop("value", str(id.email))])
|
|
1202
|
+
])
|
|
1203
|
+
)
|
|
1204
|
+
);
|
|
1205
|
+
props.push(prop("gender", str(id.gender)));
|
|
1206
|
+
props.push(prop("birthDate", str(id.birthDate)));
|
|
1207
|
+
props.push(
|
|
1208
|
+
prop(
|
|
1209
|
+
"address",
|
|
1210
|
+
list([
|
|
1211
|
+
complex([
|
|
1212
|
+
prop("use", str("home")),
|
|
1213
|
+
prop("line", list([str(id.address.street)])),
|
|
1214
|
+
prop("city", str(id.address.city)),
|
|
1215
|
+
prop("state", str(id.address.state)),
|
|
1216
|
+
prop("postalCode", str(id.address.zip))
|
|
1217
|
+
])
|
|
1218
|
+
])
|
|
1219
|
+
)
|
|
1220
|
+
);
|
|
1221
|
+
return complex(props);
|
|
1222
|
+
}
|
|
1223
|
+
function generateCondition(options = {}) {
|
|
1224
|
+
const { seed = 0, subject = "Patient/syn-patient-1", usCore = true } = options;
|
|
1225
|
+
const rng = createRng(seed);
|
|
1226
|
+
const code = rng.pick(EXAMPLE_CONDITIONS);
|
|
1227
|
+
const props = [
|
|
1228
|
+
prop("resourceType", str("Condition")),
|
|
1229
|
+
prop("id", str(`syn-cond-${rng.digits(8)}`))
|
|
1230
|
+
];
|
|
1231
|
+
if (usCore) props.push(prop("meta", meta([US_CORE_PROFILE.CONDITION])));
|
|
1232
|
+
props.push(prop("text", narrative(`Synthetic condition: ${code.display}.`)));
|
|
1233
|
+
props.push(
|
|
1234
|
+
prop(
|
|
1235
|
+
"clinicalStatus",
|
|
1236
|
+
codeableConcept({ system: SYSTEM.CONDITION_CLINICAL, code: "active", display: "Active" })
|
|
1237
|
+
)
|
|
1238
|
+
);
|
|
1239
|
+
props.push(
|
|
1240
|
+
prop(
|
|
1241
|
+
"verificationStatus",
|
|
1242
|
+
codeableConcept({
|
|
1243
|
+
system: SYSTEM.CONDITION_VER_STATUS,
|
|
1244
|
+
code: "confirmed",
|
|
1245
|
+
display: "Confirmed"
|
|
1246
|
+
})
|
|
1247
|
+
)
|
|
1248
|
+
);
|
|
1249
|
+
props.push(
|
|
1250
|
+
prop(
|
|
1251
|
+
"category",
|
|
1252
|
+
list([
|
|
1253
|
+
codeableConcept({
|
|
1254
|
+
system: SYSTEM.CONDITION_CATEGORY,
|
|
1255
|
+
code: "problem-list-item",
|
|
1256
|
+
display: "Problem List Item"
|
|
1257
|
+
})
|
|
1258
|
+
])
|
|
1259
|
+
)
|
|
1260
|
+
);
|
|
1261
|
+
props.push(prop("code", codeableConcept(code)));
|
|
1262
|
+
props.push(prop("subject", reference(subject)));
|
|
1263
|
+
return complex(props);
|
|
1264
|
+
}
|
|
1265
|
+
function drawValue(rng, concept) {
|
|
1266
|
+
const span = concept.high - concept.low;
|
|
1267
|
+
const raw = concept.low + rng.float() * span;
|
|
1268
|
+
return raw.toFixed(concept.decimals);
|
|
1269
|
+
}
|
|
1270
|
+
function valueQuantity(rng, concept) {
|
|
1271
|
+
return complex([
|
|
1272
|
+
prop("value", dec(drawValue(rng, concept))),
|
|
1273
|
+
prop("unit", str(concept.unit)),
|
|
1274
|
+
prop("system", str(SYSTEM.UCUM)),
|
|
1275
|
+
prop("code", str(concept.unit))
|
|
1276
|
+
]);
|
|
1277
|
+
}
|
|
1278
|
+
function buildObservation(seed, subject, usCore, profileUrl, category, pool, effective) {
|
|
1279
|
+
const rng = createRng(seed);
|
|
1280
|
+
const concept = rng.pick(pool);
|
|
1281
|
+
const props = [
|
|
1282
|
+
prop("resourceType", str("Observation")),
|
|
1283
|
+
prop("id", str(`syn-obs-${rng.digits(8)}`))
|
|
1284
|
+
];
|
|
1285
|
+
if (usCore) props.push(prop("meta", meta([profileUrl])));
|
|
1286
|
+
props.push(
|
|
1287
|
+
prop("text", narrative(`Synthetic ${category.display.toLowerCase()}: ${concept.display}.`))
|
|
1288
|
+
);
|
|
1289
|
+
props.push(prop("status", str("final")));
|
|
1290
|
+
props.push(
|
|
1291
|
+
prop(
|
|
1292
|
+
"category",
|
|
1293
|
+
list([
|
|
1294
|
+
codeableConcept({
|
|
1295
|
+
system: SYSTEM.OBSERVATION_CATEGORY,
|
|
1296
|
+
code: category.code,
|
|
1297
|
+
display: category.display
|
|
1298
|
+
})
|
|
1299
|
+
])
|
|
1300
|
+
)
|
|
1301
|
+
);
|
|
1302
|
+
props.push(prop("code", codeableConcept(concept)));
|
|
1303
|
+
props.push(prop("subject", reference(subject)));
|
|
1304
|
+
const y = rng.int(2020, 2025);
|
|
1305
|
+
const m = String(rng.int(1, 12)).padStart(2, "0");
|
|
1306
|
+
const d = String(rng.int(1, 28)).padStart(2, "0");
|
|
1307
|
+
if (effective) props.push(prop("effectiveDateTime", str(`${String(y)}-${m}-${d}`)));
|
|
1308
|
+
props.push(prop("valueQuantity", valueQuantity(rng, concept)));
|
|
1309
|
+
return complex(props);
|
|
1310
|
+
}
|
|
1311
|
+
function generateObservationLab(options = {}) {
|
|
1312
|
+
const { seed = 0, subject = "Patient/syn-patient-1", usCore = true } = options;
|
|
1313
|
+
return buildObservation(
|
|
1314
|
+
seed,
|
|
1315
|
+
subject,
|
|
1316
|
+
usCore,
|
|
1317
|
+
US_CORE_PROFILE.OBSERVATION_LAB,
|
|
1318
|
+
{ code: "laboratory", display: "Laboratory" },
|
|
1319
|
+
EXAMPLE_LAB_OBSERVATIONS2,
|
|
1320
|
+
false
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
function generateVitalSign(options = {}) {
|
|
1324
|
+
const { seed = 0, subject = "Patient/syn-patient-1", usCore = true } = options;
|
|
1325
|
+
return buildObservation(
|
|
1326
|
+
seed,
|
|
1327
|
+
subject,
|
|
1328
|
+
usCore,
|
|
1329
|
+
US_CORE_PROFILE.VITAL_SIGNS,
|
|
1330
|
+
{ code: "vital-signs", display: "Vital Signs" },
|
|
1331
|
+
EXAMPLE_VITAL_SIGNS,
|
|
1332
|
+
true
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
function generateMedicationRequest(options = {}) {
|
|
1336
|
+
const {
|
|
1337
|
+
seed = 0,
|
|
1338
|
+
subject = "Patient/syn-patient-1",
|
|
1339
|
+
requester = "Practitioner/syn-practitioner-1",
|
|
1340
|
+
usCore = true
|
|
1341
|
+
} = options;
|
|
1342
|
+
const rng = createRng(seed);
|
|
1343
|
+
const med = rng.pick(EXAMPLE_MEDICATIONS);
|
|
1344
|
+
const y = rng.int(2020, 2025);
|
|
1345
|
+
const m = String(rng.int(1, 12)).padStart(2, "0");
|
|
1346
|
+
const d = String(rng.int(1, 28)).padStart(2, "0");
|
|
1347
|
+
const props = [
|
|
1348
|
+
prop("resourceType", str("MedicationRequest")),
|
|
1349
|
+
prop("id", str(`syn-medreq-${rng.digits(8)}`))
|
|
1350
|
+
];
|
|
1351
|
+
if (usCore) props.push(prop("meta", meta([US_CORE_PROFILE.MEDICATION_REQUEST])));
|
|
1352
|
+
props.push(prop("text", narrative(`Synthetic medication order: ${med.display}.`)));
|
|
1353
|
+
props.push(prop("status", str("active")));
|
|
1354
|
+
props.push(prop("intent", str("order")));
|
|
1355
|
+
props.push(prop("medicationCodeableConcept", codeableConcept(med)));
|
|
1356
|
+
props.push(prop("subject", reference(subject)));
|
|
1357
|
+
props.push(prop("authoredOn", str(`${String(y)}-${m}-${d}`)));
|
|
1358
|
+
props.push(prop("requester", reference(requester)));
|
|
1359
|
+
return complex(props);
|
|
1360
|
+
}
|
|
1361
|
+
function generateEncounter(options = {}) {
|
|
1362
|
+
const { seed = 0, subject = "Patient/syn-patient-1", usCore = true } = options;
|
|
1363
|
+
const rng = createRng(seed);
|
|
1364
|
+
const cls = rng.pick(EXAMPLE_ENCOUNTER_CLASSES);
|
|
1365
|
+
const type = rng.pick(EXAMPLE_ENCOUNTER_TYPES);
|
|
1366
|
+
const id = safe.identifier(rng, "AN");
|
|
1367
|
+
const start = toFhirDate(safe.dateYmd(rng, 2018, 2024));
|
|
1368
|
+
const props = [
|
|
1369
|
+
prop("resourceType", str("Encounter")),
|
|
1370
|
+
prop("id", str(`syn-enc-${rng.digits(8)}`))
|
|
1371
|
+
];
|
|
1372
|
+
if (usCore) props.push(prop("meta", meta([US_CORE_PROFILE.ENCOUNTER])));
|
|
1373
|
+
props.push(prop("text", narrative(`Synthetic encounter: ${type.display}.`)));
|
|
1374
|
+
props.push(
|
|
1375
|
+
prop(
|
|
1376
|
+
"identifier",
|
|
1377
|
+
list([
|
|
1378
|
+
complex([
|
|
1379
|
+
prop("system", str(`urn:oid:${id.assigningAuthorityOid}`)),
|
|
1380
|
+
prop("value", str(id.value))
|
|
1381
|
+
])
|
|
1382
|
+
])
|
|
1383
|
+
)
|
|
1384
|
+
);
|
|
1385
|
+
props.push(prop("status", str("finished")));
|
|
1386
|
+
props.push(prop("class", coding(cls)));
|
|
1387
|
+
props.push(prop("type", list([codeableConcept(type)])));
|
|
1388
|
+
props.push(prop("subject", reference(subject)));
|
|
1389
|
+
props.push(prop("period", complex([prop("start", str(`${start}T09:00:00Z`))])));
|
|
1390
|
+
return complex(props);
|
|
1391
|
+
}
|
|
1392
|
+
function generateImmunization(options = {}) {
|
|
1393
|
+
const { seed = 0, patient = "Patient/syn-patient-1", usCore = true } = options;
|
|
1394
|
+
const rng = createRng(seed);
|
|
1395
|
+
const vaccine = rng.pick(EXAMPLE_VACCINES2);
|
|
1396
|
+
const occurrence = toFhirDate(safe.dateYmd(rng, 2015, 2024));
|
|
1397
|
+
const props = [
|
|
1398
|
+
prop("resourceType", str("Immunization")),
|
|
1399
|
+
prop("id", str(`syn-imm-${rng.digits(8)}`))
|
|
1400
|
+
];
|
|
1401
|
+
if (usCore) props.push(prop("meta", meta([US_CORE_PROFILE.IMMUNIZATION])));
|
|
1402
|
+
props.push(prop("text", narrative(`Synthetic immunization: ${vaccine.display}.`)));
|
|
1403
|
+
props.push(prop("status", str("completed")));
|
|
1404
|
+
props.push(prop("vaccineCode", codeableConcept(vaccine)));
|
|
1405
|
+
props.push(prop("patient", reference(patient)));
|
|
1406
|
+
props.push(prop("occurrenceDateTime", str(occurrence)));
|
|
1407
|
+
props.push(prop("primarySource", bool(true)));
|
|
1408
|
+
return complex(props);
|
|
1409
|
+
}
|
|
1410
|
+
function generateAllergyIntolerance(options = {}) {
|
|
1411
|
+
const { seed = 0, patient = "Patient/syn-patient-1", usCore = true } = options;
|
|
1412
|
+
const rng = createRng(seed);
|
|
1413
|
+
const allergen = rng.pick(EXAMPLE_ALLERGENS);
|
|
1414
|
+
const manifestation = rng.pick(EXAMPLE_ALLERGY_MANIFESTATIONS);
|
|
1415
|
+
const props = [
|
|
1416
|
+
prop("resourceType", str("AllergyIntolerance")),
|
|
1417
|
+
prop("id", str(`syn-allergy-${rng.digits(8)}`))
|
|
1418
|
+
];
|
|
1419
|
+
if (usCore) props.push(prop("meta", meta([US_CORE_PROFILE.ALLERGY_INTOLERANCE])));
|
|
1420
|
+
props.push(prop("text", narrative(`Synthetic allergy: ${allergen.display}.`)));
|
|
1421
|
+
props.push(
|
|
1422
|
+
prop(
|
|
1423
|
+
"clinicalStatus",
|
|
1424
|
+
codeableConcept({ system: SYSTEM.ALLERGY_CLINICAL, code: "active", display: "Active" })
|
|
1425
|
+
)
|
|
1426
|
+
);
|
|
1427
|
+
props.push(
|
|
1428
|
+
prop(
|
|
1429
|
+
"verificationStatus",
|
|
1430
|
+
codeableConcept({
|
|
1431
|
+
system: SYSTEM.ALLERGY_VERIFICATION,
|
|
1432
|
+
code: "confirmed",
|
|
1433
|
+
display: "Confirmed"
|
|
1434
|
+
})
|
|
1435
|
+
)
|
|
1436
|
+
);
|
|
1437
|
+
props.push(prop("code", codeableConcept(allergen)));
|
|
1438
|
+
props.push(prop("patient", reference(patient)));
|
|
1439
|
+
props.push(
|
|
1440
|
+
prop(
|
|
1441
|
+
"reaction",
|
|
1442
|
+
list([complex([prop("manifestation", list([codeableConcept(manifestation)]))])])
|
|
1443
|
+
)
|
|
1444
|
+
);
|
|
1445
|
+
return complex(props);
|
|
1446
|
+
}
|
|
1447
|
+
function generateProcedure(options = {}) {
|
|
1448
|
+
const { seed = 0, subject = "Patient/syn-patient-1", usCore = true } = options;
|
|
1449
|
+
const rng = createRng(seed);
|
|
1450
|
+
const code = rng.pick(EXAMPLE_PROCEDURES);
|
|
1451
|
+
const performed = toFhirDate(safe.dateYmd(rng, 2018, 2024));
|
|
1452
|
+
const props = [
|
|
1453
|
+
prop("resourceType", str("Procedure")),
|
|
1454
|
+
prop("id", str(`syn-proc-${rng.digits(8)}`))
|
|
1455
|
+
];
|
|
1456
|
+
if (usCore) props.push(prop("meta", meta([US_CORE_PROFILE.PROCEDURE])));
|
|
1457
|
+
props.push(prop("text", narrative(`Synthetic procedure: ${code.display}.`)));
|
|
1458
|
+
props.push(prop("status", str("completed")));
|
|
1459
|
+
props.push(prop("code", codeableConcept(code)));
|
|
1460
|
+
props.push(prop("subject", reference(subject)));
|
|
1461
|
+
props.push(prop("performedDateTime", str(`${performed}T10:00:00Z`)));
|
|
1462
|
+
return complex(props);
|
|
1463
|
+
}
|
|
1464
|
+
function generateDiagnosticReport(options = {}) {
|
|
1465
|
+
const { seed = 0, subject = "Patient/syn-patient-1", results = [], usCore = true } = options;
|
|
1466
|
+
const rng = createRng(seed);
|
|
1467
|
+
const code = rng.pick(EXAMPLE_DIAGNOSTIC_REPORTS);
|
|
1468
|
+
const day = toFhirDate(safe.dateYmd(rng, 2018, 2024));
|
|
1469
|
+
const props = [
|
|
1470
|
+
prop("resourceType", str("DiagnosticReport")),
|
|
1471
|
+
prop("id", str(`syn-dr-${rng.digits(8)}`))
|
|
1472
|
+
];
|
|
1473
|
+
if (usCore) props.push(prop("meta", meta([US_CORE_PROFILE.DIAGNOSTIC_REPORT_LAB])));
|
|
1474
|
+
props.push(prop("text", narrative(`Synthetic laboratory report: ${code.display}.`)));
|
|
1475
|
+
props.push(prop("status", str("final")));
|
|
1476
|
+
props.push(
|
|
1477
|
+
prop(
|
|
1478
|
+
"category",
|
|
1479
|
+
list([
|
|
1480
|
+
codeableConcept({
|
|
1481
|
+
system: SYSTEM.DIAGNOSTIC_SERVICE_SECTION,
|
|
1482
|
+
code: "LAB",
|
|
1483
|
+
display: "Laboratory"
|
|
1484
|
+
})
|
|
1485
|
+
])
|
|
1486
|
+
)
|
|
1487
|
+
);
|
|
1488
|
+
props.push(prop("code", codeableConcept(code)));
|
|
1489
|
+
props.push(prop("subject", reference(subject)));
|
|
1490
|
+
props.push(prop("effectiveDateTime", str(`${day}T08:30:00Z`)));
|
|
1491
|
+
props.push(prop("issued", str(`${day}T09:00:00.000Z`)));
|
|
1492
|
+
if (results.length > 0) {
|
|
1493
|
+
props.push(prop("result", list(results.map((r) => reference(r)))));
|
|
1494
|
+
}
|
|
1495
|
+
return complex(props);
|
|
1496
|
+
}
|
|
1497
|
+
function section(sec) {
|
|
1498
|
+
return complex([
|
|
1499
|
+
prop("title", str(sec.title)),
|
|
1500
|
+
prop("code", codeableConcept({ system: SYSTEM.LOINC, code: sec.code, display: sec.title })),
|
|
1501
|
+
prop("entry", list(sec.entries.map((e) => reference(e))))
|
|
1502
|
+
]);
|
|
1503
|
+
}
|
|
1504
|
+
function buildComposition(rng, input) {
|
|
1505
|
+
const props = [
|
|
1506
|
+
prop("resourceType", str("Composition")),
|
|
1507
|
+
prop("id", str(`syn-comp-${rng.digits(8)}`)),
|
|
1508
|
+
prop("text", narrative("Synthetic continuity-of-care document summary.")),
|
|
1509
|
+
prop("status", str("final")),
|
|
1510
|
+
prop(
|
|
1511
|
+
"type",
|
|
1512
|
+
codeableConcept({
|
|
1513
|
+
system: SYSTEM.LOINC,
|
|
1514
|
+
code: "34133-9",
|
|
1515
|
+
display: "Summarization of Episode Note"
|
|
1516
|
+
})
|
|
1517
|
+
),
|
|
1518
|
+
prop("subject", reference(input.subject)),
|
|
1519
|
+
prop("date", str(`${input.date}T09:00:00Z`)),
|
|
1520
|
+
prop("author", list([reference(input.organization)])),
|
|
1521
|
+
prop("title", str("Synthetic Continuity of Care Document")),
|
|
1522
|
+
prop("custodian", reference(input.organization)),
|
|
1523
|
+
prop("section", list(input.sections.map((s) => section(s))))
|
|
1524
|
+
];
|
|
1525
|
+
return complex(props);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// src/fhir/bundle.ts
|
|
1529
|
+
function resourceTypeOf(resource) {
|
|
1530
|
+
const rt = resource.properties.find((p) => p.name === "resourceType");
|
|
1531
|
+
return rt !== void 0 && rt.value.kind === "primitive" ? String(rt.value.value) : "Resource";
|
|
1532
|
+
}
|
|
1533
|
+
function buildOrganization(rng) {
|
|
1534
|
+
const id = safe.identifier(rng, "AN");
|
|
1535
|
+
return complex([
|
|
1536
|
+
prop("resourceType", str("Organization")),
|
|
1537
|
+
prop("id", str(`syn-org-${rng.digits(8)}`)),
|
|
1538
|
+
prop("text", narrative("Synthetic health organization.")),
|
|
1539
|
+
prop(
|
|
1540
|
+
"identifier",
|
|
1541
|
+
list([
|
|
1542
|
+
complex([
|
|
1543
|
+
prop("system", str(`urn:oid:${id.assigningAuthorityOid}`)),
|
|
1544
|
+
prop("value", str(id.value))
|
|
1545
|
+
])
|
|
1546
|
+
])
|
|
1547
|
+
),
|
|
1548
|
+
prop("active", bool(true)),
|
|
1549
|
+
prop("name", str("Synthetic Health Organization"))
|
|
1550
|
+
]);
|
|
1551
|
+
}
|
|
1552
|
+
function buildSpine(rng) {
|
|
1553
|
+
const patientUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1554
|
+
const orgUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1555
|
+
const date = toFhirDate(safe.dateYmd(rng, 2018, 2024));
|
|
1556
|
+
const patient = {
|
|
1557
|
+
fullUrl: patientUrl,
|
|
1558
|
+
resource: generatePatient({ seed: rng.nextUint32(), profile: "us-core" })
|
|
1559
|
+
};
|
|
1560
|
+
const org = { fullUrl: orgUrl, resource: buildOrganization(rng) };
|
|
1561
|
+
const condUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1562
|
+
const cond = generateCondition({ seed: rng.nextUint32(), subject: patientUrl });
|
|
1563
|
+
const labUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1564
|
+
const lab = generateObservationLab({ seed: rng.nextUint32(), subject: patientUrl });
|
|
1565
|
+
const vitalUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1566
|
+
const vital = generateVitalSign({ seed: rng.nextUint32(), subject: patientUrl });
|
|
1567
|
+
const medUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1568
|
+
const med = generateMedicationRequest({
|
|
1569
|
+
seed: rng.nextUint32(),
|
|
1570
|
+
subject: patientUrl,
|
|
1571
|
+
requester: orgUrl
|
|
1572
|
+
});
|
|
1573
|
+
const encUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1574
|
+
const enc = generateEncounter({ seed: rng.nextUint32(), subject: patientUrl });
|
|
1575
|
+
const immUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1576
|
+
const imm = generateImmunization({ seed: rng.nextUint32(), patient: patientUrl });
|
|
1577
|
+
const allergyUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1578
|
+
const allergy = generateAllergyIntolerance({ seed: rng.nextUint32(), patient: patientUrl });
|
|
1579
|
+
const procUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1580
|
+
const proc = generateProcedure({ seed: rng.nextUint32(), subject: patientUrl });
|
|
1581
|
+
const drUrl = `urn:uuid:${safe.uuid(rng)}`;
|
|
1582
|
+
const dr = generateDiagnosticReport({
|
|
1583
|
+
seed: rng.nextUint32(),
|
|
1584
|
+
subject: patientUrl,
|
|
1585
|
+
results: [labUrl]
|
|
1586
|
+
});
|
|
1587
|
+
const entries = [
|
|
1588
|
+
patient,
|
|
1589
|
+
org,
|
|
1590
|
+
{ fullUrl: condUrl, resource: cond },
|
|
1591
|
+
{ fullUrl: labUrl, resource: lab },
|
|
1592
|
+
{ fullUrl: vitalUrl, resource: vital },
|
|
1593
|
+
{ fullUrl: medUrl, resource: med },
|
|
1594
|
+
{ fullUrl: encUrl, resource: enc },
|
|
1595
|
+
{ fullUrl: immUrl, resource: imm },
|
|
1596
|
+
{ fullUrl: allergyUrl, resource: allergy },
|
|
1597
|
+
{ fullUrl: procUrl, resource: proc },
|
|
1598
|
+
{ fullUrl: drUrl, resource: dr }
|
|
1599
|
+
];
|
|
1600
|
+
const urls = {
|
|
1601
|
+
condition: condUrl,
|
|
1602
|
+
lab: labUrl,
|
|
1603
|
+
vital: vitalUrl,
|
|
1604
|
+
medication: medUrl,
|
|
1605
|
+
encounter: encUrl,
|
|
1606
|
+
immunization: immUrl,
|
|
1607
|
+
allergy: allergyUrl,
|
|
1608
|
+
procedure: procUrl,
|
|
1609
|
+
diagnosticReport: drUrl
|
|
1610
|
+
};
|
|
1611
|
+
return { patientUrl, orgUrl, date, urls, entries };
|
|
1612
|
+
}
|
|
1613
|
+
function documentSections(urls) {
|
|
1614
|
+
const at = (k) => urls[k] !== void 0 ? [urls[k]] : [];
|
|
1615
|
+
return [
|
|
1616
|
+
{ code: "11450-4", title: "Problem List", entries: at("condition") },
|
|
1617
|
+
{
|
|
1618
|
+
code: "30954-2",
|
|
1619
|
+
title: "Relevant Diagnostic Tests/Laboratory Data",
|
|
1620
|
+
entries: [...at("diagnosticReport"), ...at("lab"), ...at("vital")]
|
|
1621
|
+
},
|
|
1622
|
+
{ code: "10160-0", title: "History of Medication Use", entries: at("medication") },
|
|
1623
|
+
{ code: "48765-2", title: "Allergies and Adverse Reactions", entries: at("allergy") },
|
|
1624
|
+
{ code: "11369-6", title: "History of Immunizations", entries: at("immunization") },
|
|
1625
|
+
{ code: "47519-4", title: "History of Procedures", entries: at("procedure") },
|
|
1626
|
+
{ code: "46240-8", title: "History of Encounters", entries: at("encounter") }
|
|
1627
|
+
];
|
|
1628
|
+
}
|
|
1629
|
+
function generateBundle(options = {}) {
|
|
1630
|
+
const { seed = 0, type = "collection" } = options;
|
|
1631
|
+
const rng = createRng(seed);
|
|
1632
|
+
const spine = buildSpine(rng);
|
|
1633
|
+
const members = type === "document" ? [
|
|
1634
|
+
{
|
|
1635
|
+
fullUrl: `urn:uuid:${safe.uuid(rng)}`,
|
|
1636
|
+
resource: buildComposition(rng, {
|
|
1637
|
+
subject: spine.patientUrl,
|
|
1638
|
+
organization: spine.orgUrl,
|
|
1639
|
+
date: spine.date,
|
|
1640
|
+
sections: documentSections(spine.urls)
|
|
1641
|
+
})
|
|
1642
|
+
},
|
|
1643
|
+
...spine.entries
|
|
1644
|
+
] : [...spine.entries];
|
|
1645
|
+
const entries = members.map((member) => {
|
|
1646
|
+
const entryProps = [
|
|
1647
|
+
prop("fullUrl", str(member.fullUrl)),
|
|
1648
|
+
prop("resource", member.resource)
|
|
1649
|
+
];
|
|
1650
|
+
if (type === "transaction") {
|
|
1651
|
+
entryProps.push(
|
|
1652
|
+
prop(
|
|
1653
|
+
"request",
|
|
1654
|
+
complex([prop("method", str("POST")), prop("url", str(resourceTypeOf(member.resource)))])
|
|
1655
|
+
)
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
return complex(entryProps);
|
|
1659
|
+
});
|
|
1660
|
+
const props = [
|
|
1661
|
+
prop("resourceType", str("Bundle")),
|
|
1662
|
+
prop("id", str(`syn-bundle-${rng.digits(8)}`))
|
|
1663
|
+
];
|
|
1664
|
+
if (type === "document") {
|
|
1665
|
+
const id = safe.identifier(rng, "AN");
|
|
1666
|
+
props.push(
|
|
1667
|
+
prop(
|
|
1668
|
+
"identifier",
|
|
1669
|
+
complex([
|
|
1670
|
+
prop("system", str(`urn:oid:${id.assigningAuthorityOid}`)),
|
|
1671
|
+
prop("value", str(`urn:uuid:${safe.uuid(rng)}`))
|
|
1672
|
+
])
|
|
1673
|
+
)
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
props.push(prop("type", str(type)));
|
|
1677
|
+
if (type === "document") props.push(prop("timestamp", str(`${spine.date}T09:00:00.000Z`)));
|
|
1678
|
+
props.push(prop("entry", list(entries)));
|
|
1679
|
+
return complex(props);
|
|
1680
|
+
}
|
|
1681
|
+
function dec2(value) {
|
|
1682
|
+
const d = X12Decimal.fromString(value);
|
|
1683
|
+
if (d === void 0) throw new Error(`invalid synthetic decimal: ${value}`);
|
|
1684
|
+
return d;
|
|
1685
|
+
}
|
|
1686
|
+
function money(n) {
|
|
1687
|
+
return `${String(n)}.00`;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// src/x12/identity.ts
|
|
1691
|
+
var SYNTHETIC_ORG_NAMES = Object.freeze([
|
|
1692
|
+
"SYNTH BILLING GROUP",
|
|
1693
|
+
"FIXTURE CLINIC INC",
|
|
1694
|
+
"PLACEHOLDER MEDICAL CTR",
|
|
1695
|
+
"SAMPLE HEALTH PARTNERS",
|
|
1696
|
+
"MOCK CARE ASSOCIATES",
|
|
1697
|
+
"PROTOTYPE PHYSICIANS LLC"
|
|
1698
|
+
]);
|
|
1699
|
+
var SYNTHETIC_PAYER_NAMES = Object.freeze([
|
|
1700
|
+
"SYNTHCARE HEALTH PLAN",
|
|
1701
|
+
"FIXTURE MUTUAL INSURANCE",
|
|
1702
|
+
"PLACEHOLDER BENEFIT ADMIN",
|
|
1703
|
+
"SAMPLE STATE MEDICAID",
|
|
1704
|
+
"MOCK NATIONAL INSURER"
|
|
1705
|
+
]);
|
|
1706
|
+
function x12Person(rng) {
|
|
1707
|
+
const person = safe.name(rng);
|
|
1708
|
+
const memberId = `MBR${safe.identifier(rng, "MB").value}`;
|
|
1709
|
+
const dob = safe.dateYmd(rng, 1935, 2010);
|
|
1710
|
+
const sex = rng.pick(["M", "F"]);
|
|
1711
|
+
const address2 = safe.address(rng);
|
|
1712
|
+
return { person, memberId, dob, sex, address: address2 };
|
|
1713
|
+
}
|
|
1714
|
+
function x12Organization(rng) {
|
|
1715
|
+
const name2 = rng.pick(SYNTHETIC_ORG_NAMES);
|
|
1716
|
+
const npiValue = safe.npi(rng);
|
|
1717
|
+
const taxIdSsn = safe.ssn(rng).replace(/\D/g, "");
|
|
1718
|
+
const address2 = safe.address(rng);
|
|
1719
|
+
return { name: name2, npi: npiValue, taxIdSsn, address: address2 };
|
|
1720
|
+
}
|
|
1721
|
+
function x12ProviderPerson(rng) {
|
|
1722
|
+
const person = safe.name(rng);
|
|
1723
|
+
const npiValue = safe.npi(rng);
|
|
1724
|
+
return { person, npi: npiValue };
|
|
1725
|
+
}
|
|
1726
|
+
function x12Payer(rng) {
|
|
1727
|
+
const name2 = rng.pick(SYNTHETIC_PAYER_NAMES);
|
|
1728
|
+
const payerId = `SYN${rng.digits(5)}`;
|
|
1729
|
+
return { name: name2, payerId };
|
|
1730
|
+
}
|
|
1731
|
+
function x12TradingPartners(rng) {
|
|
1732
|
+
return { senderId: `SYNSUB${rng.digits(3)}`, receiverId: `SYNRCV${rng.digits(3)}` };
|
|
1733
|
+
}
|
|
1734
|
+
function x12EnvelopeTiming(rng) {
|
|
1735
|
+
const serviceDate2 = safe.dateYmd(rng, 2024, 2026);
|
|
1736
|
+
const yy = serviceDate2.slice(2, 4);
|
|
1737
|
+
const mmdd = serviceDate2.slice(4, 8);
|
|
1738
|
+
const hh = String(rng.int(0, 23)).padStart(2, "0");
|
|
1739
|
+
const mi = String(rng.int(0, 59)).padStart(2, "0");
|
|
1740
|
+
return {
|
|
1741
|
+
interchangeDate: `${yy}${mmdd}`,
|
|
1742
|
+
interchangeTime: `${hh}${mi}`,
|
|
1743
|
+
interchangeControlNumber: rng.digits(9),
|
|
1744
|
+
groupControlNumber: String(rng.int(1, 99999)),
|
|
1745
|
+
transactionSetControlNumber: rng.digits(4),
|
|
1746
|
+
serviceDate: serviceDate2
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/x12/example-codes.ts
|
|
1751
|
+
var PROFESSIONAL_PROCEDURES = Object.freeze([
|
|
1752
|
+
{ code: "99213", label: "office/outpatient visit, established patient" },
|
|
1753
|
+
{ code: "99214", label: "office/outpatient visit, established patient, moderate" },
|
|
1754
|
+
{ code: "99203", label: "office/outpatient visit, new patient" },
|
|
1755
|
+
{ code: "85025", label: "complete blood count with differential" },
|
|
1756
|
+
{ code: "80053", label: "comprehensive metabolic panel" },
|
|
1757
|
+
{ code: "93000", label: "electrocardiogram, routine" }
|
|
1758
|
+
]);
|
|
1759
|
+
var PROCEDURE_MODIFIERS = Object.freeze(["25", "59", "76", "GT"]);
|
|
1760
|
+
var PLACES_OF_SERVICE = Object.freeze(["11", "22", "21", "12", "49"]);
|
|
1761
|
+
var DIAGNOSES = Object.freeze([
|
|
1762
|
+
{ code: "J20.9", label: "acute bronchitis, unspecified" },
|
|
1763
|
+
{ code: "E11.9", label: "type 2 diabetes mellitus without complications" },
|
|
1764
|
+
{ code: "I10", label: "essential (primary) hypertension" },
|
|
1765
|
+
{ code: "M54.5", label: "low back pain" },
|
|
1766
|
+
{ code: "R07.9", label: "chest pain, unspecified" },
|
|
1767
|
+
{ code: "Z00.00", label: "general adult medical exam without abnormal findings" }
|
|
1768
|
+
]);
|
|
1769
|
+
var REVENUE_CODES = Object.freeze([
|
|
1770
|
+
{ code: "0120", label: "room & board, semi-private" },
|
|
1771
|
+
{ code: "0300", label: "laboratory, general" },
|
|
1772
|
+
{ code: "0450", label: "emergency room, general" },
|
|
1773
|
+
{ code: "0636", label: "drugs requiring detailed coding" }
|
|
1774
|
+
]);
|
|
1775
|
+
var INSTITUTIONAL_PROCEDURES = Object.freeze([
|
|
1776
|
+
{ code: "99221", label: "initial hospital care, per day" },
|
|
1777
|
+
{ code: "99231", label: "subsequent hospital care, per day" }
|
|
1778
|
+
]);
|
|
1779
|
+
var DENTAL_PROCEDURES = Object.freeze([
|
|
1780
|
+
{ code: "D2391", label: "resin-based composite, one surface, posterior" },
|
|
1781
|
+
{ code: "D1110", label: "prophylaxis, adult" },
|
|
1782
|
+
{ code: "D0120", label: "periodic oral evaluation" }
|
|
1783
|
+
]);
|
|
1784
|
+
var TOOTH_CODES = Object.freeze(["14", "3", "19", "30"]);
|
|
1785
|
+
var TOOTH_SURFACES = Object.freeze(["M", "O", "D", "B", "L"]);
|
|
1786
|
+
var SERVICE_TYPE_CODES = Object.freeze(["30", "1", "35", "88", "98"]);
|
|
1787
|
+
|
|
1788
|
+
// src/x12/claim-837.ts
|
|
1789
|
+
function serviceDtp(serviceDate2) {
|
|
1790
|
+
return { qualifier: "472", formatQualifier: "D8", value: serviceDate2 };
|
|
1791
|
+
}
|
|
1792
|
+
function envelopeOf(partners, timing) {
|
|
1793
|
+
return {
|
|
1794
|
+
senderId: partners.senderId,
|
|
1795
|
+
receiverId: partners.receiverId,
|
|
1796
|
+
interchangeDate: timing.interchangeDate,
|
|
1797
|
+
interchangeTime: timing.interchangeTime,
|
|
1798
|
+
interchangeControlNumber: timing.interchangeControlNumber,
|
|
1799
|
+
groupControlNumber: timing.groupControlNumber,
|
|
1800
|
+
transactionSetControlNumber: timing.transactionSetControlNumber,
|
|
1801
|
+
usageIndicator: "T"
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function professionalLine(rng, serviceDate2) {
|
|
1805
|
+
const charge = dec2(`${String(rng.int(75, 400))}.00`);
|
|
1806
|
+
const line = {
|
|
1807
|
+
variant: "P",
|
|
1808
|
+
procedureQualifier: "HC",
|
|
1809
|
+
procedureCode: rng.pick(PROFESSIONAL_PROCEDURES).code,
|
|
1810
|
+
...rng.bool(0.4) ? { modifiers: [rng.pick(PROCEDURE_MODIFIERS)] } : {},
|
|
1811
|
+
charge,
|
|
1812
|
+
unitOfMeasure: "UN",
|
|
1813
|
+
units: dec2("1"),
|
|
1814
|
+
placeOfServiceCode: rng.pick(PLACES_OF_SERVICE),
|
|
1815
|
+
diagnosisPointers: ["1"],
|
|
1816
|
+
dates: [serviceDtp(serviceDate2)]
|
|
1817
|
+
};
|
|
1818
|
+
return { line, charge };
|
|
1819
|
+
}
|
|
1820
|
+
function institutionalLine(rng, serviceDate2) {
|
|
1821
|
+
const charge = dec2(`${String(rng.int(500, 5e3))}.00`);
|
|
1822
|
+
const line = {
|
|
1823
|
+
variant: "I",
|
|
1824
|
+
revenueCode: rng.pick(REVENUE_CODES).code,
|
|
1825
|
+
procedureQualifier: "HC",
|
|
1826
|
+
procedureCode: rng.pick(INSTITUTIONAL_PROCEDURES).code,
|
|
1827
|
+
charge,
|
|
1828
|
+
unitOfMeasure: "UN",
|
|
1829
|
+
units: dec2("1"),
|
|
1830
|
+
dates: [serviceDtp(serviceDate2)]
|
|
1831
|
+
};
|
|
1832
|
+
return { line, charge };
|
|
1833
|
+
}
|
|
1834
|
+
function dentalLine(rng, serviceDate2) {
|
|
1835
|
+
const charge = dec2(`${String(rng.int(80, 900))}.00`);
|
|
1836
|
+
const line = {
|
|
1837
|
+
variant: "D",
|
|
1838
|
+
procedureQualifier: "AD",
|
|
1839
|
+
procedureCode: rng.pick(DENTAL_PROCEDURES).code,
|
|
1840
|
+
charge,
|
|
1841
|
+
units: dec2("1"),
|
|
1842
|
+
placeOfServiceCode: "11",
|
|
1843
|
+
toothInformation: [
|
|
1844
|
+
{ qualifier: "JP", toothCode: rng.pick(TOOTH_CODES), surfaces: [rng.pick(TOOTH_SURFACES)] }
|
|
1845
|
+
],
|
|
1846
|
+
dates: [serviceDtp(serviceDate2)]
|
|
1847
|
+
};
|
|
1848
|
+
return { line, charge };
|
|
1849
|
+
}
|
|
1850
|
+
function claimOf(rng, variant, serviceDate2) {
|
|
1851
|
+
const lineCount = rng.int(1, 2);
|
|
1852
|
+
const built = Array.from({ length: lineCount }, () => {
|
|
1853
|
+
if (variant === "P") return professionalLine(rng, serviceDate2);
|
|
1854
|
+
if (variant === "I") return institutionalLine(rng, serviceDate2);
|
|
1855
|
+
return dentalLine(rng, serviceDate2);
|
|
1856
|
+
});
|
|
1857
|
+
const total = built.reduce((acc, b) => acc.add(b.charge), dec2("0"));
|
|
1858
|
+
const diagnosis = rng.pick(DIAGNOSES).code;
|
|
1859
|
+
const secondary = rng.pick(DIAGNOSES).code;
|
|
1860
|
+
return {
|
|
1861
|
+
claimId: `PTACCT${rng.digits(6)}`,
|
|
1862
|
+
totalCharge: total,
|
|
1863
|
+
placeOfServiceCode: variant === "P" ? rng.pick(PLACES_OF_SERVICE) : "11",
|
|
1864
|
+
facilityCodeQualifier: "B",
|
|
1865
|
+
claimFrequencyCode: "1",
|
|
1866
|
+
providerSignatureOnFile: "Y",
|
|
1867
|
+
providerAcceptAssignment: "A",
|
|
1868
|
+
benefitsAssignment: "Y",
|
|
1869
|
+
releaseOfInformationCode: "Y",
|
|
1870
|
+
diagnoses: [
|
|
1871
|
+
{ qualifier: "ABK", code: diagnosis },
|
|
1872
|
+
{ qualifier: "ABF", code: secondary }
|
|
1873
|
+
],
|
|
1874
|
+
dates: [{ qualifier: "431", formatQualifier: "D8", value: serviceDate2 }],
|
|
1875
|
+
serviceLines: built.map((b) => b.line)
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
function specOf(rng, variant) {
|
|
1879
|
+
const partners = x12TradingPartners(rng);
|
|
1880
|
+
const timing = x12EnvelopeTiming(rng);
|
|
1881
|
+
const envelope = envelopeOf(partners, timing);
|
|
1882
|
+
const org = x12Organization(rng);
|
|
1883
|
+
const subscriberPerson = x12Person(rng);
|
|
1884
|
+
const rendering = x12ProviderPerson(rng);
|
|
1885
|
+
const payer = x12Payer(rng);
|
|
1886
|
+
const claim = claimOf(rng, variant, timing.serviceDate);
|
|
1887
|
+
const subscriber = {
|
|
1888
|
+
info: {
|
|
1889
|
+
payerResponsibilityCode: "P",
|
|
1890
|
+
individualRelationshipCode: "18",
|
|
1891
|
+
// self
|
|
1892
|
+
groupNumber: `GRP${rng.digits(5)}`,
|
|
1893
|
+
claimFilingIndicator: "MB"
|
|
1894
|
+
},
|
|
1895
|
+
subscriber: {
|
|
1896
|
+
entityIdentifierCode: "IL",
|
|
1897
|
+
entityTypeQualifier: "1",
|
|
1898
|
+
name: subscriberPerson.person.family,
|
|
1899
|
+
firstName: subscriberPerson.person.given,
|
|
1900
|
+
idQualifier: "MI",
|
|
1901
|
+
idCode: subscriberPerson.memberId,
|
|
1902
|
+
address: {
|
|
1903
|
+
lines: [subscriberPerson.address.street],
|
|
1904
|
+
city: subscriberPerson.address.city,
|
|
1905
|
+
state: subscriberPerson.address.state,
|
|
1906
|
+
postalCode: subscriberPerson.address.zip
|
|
1907
|
+
}
|
|
1908
|
+
},
|
|
1909
|
+
payer: {
|
|
1910
|
+
entityIdentifierCode: "PR",
|
|
1911
|
+
entityTypeQualifier: "2",
|
|
1912
|
+
name: payer.name,
|
|
1913
|
+
idQualifier: "PI",
|
|
1914
|
+
idCode: payer.payerId
|
|
1915
|
+
},
|
|
1916
|
+
claims: [
|
|
1917
|
+
{
|
|
1918
|
+
...claim,
|
|
1919
|
+
// Rendering provider (Loop 2310B, NM1 fields round-trip) — a person with an invalid-Luhn NPI.
|
|
1920
|
+
providers: [
|
|
1921
|
+
{
|
|
1922
|
+
entityIdentifierCode: "82",
|
|
1923
|
+
entityTypeQualifier: "1",
|
|
1924
|
+
name: rendering.person.family,
|
|
1925
|
+
firstName: rendering.person.given,
|
|
1926
|
+
idQualifier: "XX",
|
|
1927
|
+
idCode: rendering.npi
|
|
1928
|
+
}
|
|
1929
|
+
]
|
|
1930
|
+
}
|
|
1931
|
+
]
|
|
1932
|
+
};
|
|
1933
|
+
const billingProvider = {
|
|
1934
|
+
provider: {
|
|
1935
|
+
entityIdentifierCode: "85",
|
|
1936
|
+
entityTypeQualifier: "2",
|
|
1937
|
+
name: org.name,
|
|
1938
|
+
idQualifier: "XX",
|
|
1939
|
+
idCode: org.npi,
|
|
1940
|
+
address: {
|
|
1941
|
+
lines: [org.address.street],
|
|
1942
|
+
city: org.address.city,
|
|
1943
|
+
state: org.address.state,
|
|
1944
|
+
postalCode: org.address.zip
|
|
1945
|
+
},
|
|
1946
|
+
// Provider tax id as a never-issued (900-range) SSN at REF*SY — provably synthetic (roadmap §4.1).
|
|
1947
|
+
references: [{ qualifier: "SY", value: org.taxIdSsn }]
|
|
1948
|
+
},
|
|
1949
|
+
subscribers: [subscriber]
|
|
1950
|
+
};
|
|
1951
|
+
return {
|
|
1952
|
+
envelope,
|
|
1953
|
+
submitter: {
|
|
1954
|
+
entityIdentifierCode: "41",
|
|
1955
|
+
entityTypeQualifier: "2",
|
|
1956
|
+
name: org.name,
|
|
1957
|
+
idQualifier: "46",
|
|
1958
|
+
idCode: envelope.senderId
|
|
1959
|
+
},
|
|
1960
|
+
receiver: {
|
|
1961
|
+
entityIdentifierCode: "40",
|
|
1962
|
+
entityTypeQualifier: "2",
|
|
1963
|
+
name: payer.name,
|
|
1964
|
+
idQualifier: "46",
|
|
1965
|
+
idCode: envelope.receiverId
|
|
1966
|
+
},
|
|
1967
|
+
billingProviders: [billingProvider]
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
function generate837(variant, options) {
|
|
1971
|
+
const rng = createRng(options.seed);
|
|
1972
|
+
const spec = specOf(rng, variant);
|
|
1973
|
+
if (variant === "P") return build837P(spec);
|
|
1974
|
+
if (variant === "I") return build837I(spec);
|
|
1975
|
+
return build837D(spec);
|
|
1976
|
+
}
|
|
1977
|
+
function generate837P(options) {
|
|
1978
|
+
return generate837("P", options);
|
|
1979
|
+
}
|
|
1980
|
+
function generate837I(options) {
|
|
1981
|
+
return generate837("I", options);
|
|
1982
|
+
}
|
|
1983
|
+
function generate837D(options) {
|
|
1984
|
+
return generate837("D", options);
|
|
1985
|
+
}
|
|
1986
|
+
function generate835(options) {
|
|
1987
|
+
const rng = createRng(options.seed);
|
|
1988
|
+
const partners = x12TradingPartners(rng);
|
|
1989
|
+
const timing = x12EnvelopeTiming(rng);
|
|
1990
|
+
const patient = x12Person(rng);
|
|
1991
|
+
const org = x12Organization(rng);
|
|
1992
|
+
const payer = x12Payer(rng);
|
|
1993
|
+
const chargeN = rng.int(200, 2e3);
|
|
1994
|
+
const patientRespN = rng.int(10, Math.min(chargeN - 1, 150));
|
|
1995
|
+
const paymentN = chargeN - patientRespN;
|
|
1996
|
+
const casReason = rng.pick(["1", "2", "3"]);
|
|
1997
|
+
const line = {
|
|
1998
|
+
productServiceIdQualifier: "HC",
|
|
1999
|
+
productServiceId: rng.pick(PROFESSIONAL_PROCEDURES).code,
|
|
2000
|
+
...rng.bool(0.4) ? { modifiers: [rng.pick(PROCEDURE_MODIFIERS)] } : {},
|
|
2001
|
+
chargeAmount: dec2(money(chargeN)),
|
|
2002
|
+
paymentAmount: dec2(money(paymentN)),
|
|
2003
|
+
serviceDateStart: timing.serviceDate,
|
|
2004
|
+
serviceDateEnd: timing.serviceDate,
|
|
2005
|
+
adjustments: [{ groupCode: "PR", reasonCode: casReason, amount: dec2(money(patientRespN)) }],
|
|
2006
|
+
amounts: [{ qualifier: "B6", amount: dec2(money(paymentN)) }]
|
|
2007
|
+
};
|
|
2008
|
+
const claim = {
|
|
2009
|
+
patientControlNumber: `PTACCT${rng.digits(6)}`,
|
|
2010
|
+
claimStatusCode: "1",
|
|
2011
|
+
// processed as primary
|
|
2012
|
+
totalChargeAmount: dec2(money(chargeN)),
|
|
2013
|
+
totalPaymentAmount: dec2(money(paymentN)),
|
|
2014
|
+
patientResponsibilityAmount: dec2(money(patientRespN)),
|
|
2015
|
+
claimFilingIndicatorCode: "MB",
|
|
2016
|
+
payerClaimControlNumber: `ICN${rng.digits(9)}`,
|
|
2017
|
+
facilityTypeCode: "11",
|
|
2018
|
+
claimFrequencyCode: "1",
|
|
2019
|
+
patient: {
|
|
2020
|
+
entityIdentifierCode: "QC",
|
|
2021
|
+
lastName: patient.person.family,
|
|
2022
|
+
firstName: patient.person.given,
|
|
2023
|
+
idQualifier: "MI",
|
|
2024
|
+
idCode: patient.memberId
|
|
2025
|
+
},
|
|
2026
|
+
serviceProvider: {
|
|
2027
|
+
entityIdentifierCode: "82",
|
|
2028
|
+
name: org.name,
|
|
2029
|
+
idQualifier: "XX",
|
|
2030
|
+
idCode: org.npi
|
|
2031
|
+
},
|
|
2032
|
+
servicePeriodStart: timing.serviceDate,
|
|
2033
|
+
servicePeriodEnd: timing.serviceDate,
|
|
2034
|
+
serviceLines: [line]
|
|
2035
|
+
};
|
|
2036
|
+
const spec = {
|
|
2037
|
+
envelope: {
|
|
2038
|
+
senderId: partners.senderId,
|
|
2039
|
+
receiverId: partners.receiverId,
|
|
2040
|
+
interchangeDate: timing.interchangeDate,
|
|
2041
|
+
interchangeTime: timing.interchangeTime,
|
|
2042
|
+
interchangeControlNumber: timing.interchangeControlNumber,
|
|
2043
|
+
groupControlNumber: timing.groupControlNumber,
|
|
2044
|
+
transactionSetControlNumber: timing.transactionSetControlNumber,
|
|
2045
|
+
usageIndicator: "T"
|
|
2046
|
+
},
|
|
2047
|
+
payment: {
|
|
2048
|
+
transactionHandlingCode: "I",
|
|
2049
|
+
totalActualPayment: dec2(money(paymentN)),
|
|
2050
|
+
creditDebitFlag: "C",
|
|
2051
|
+
method: "ACH",
|
|
2052
|
+
paymentDate: timing.serviceDate
|
|
2053
|
+
},
|
|
2054
|
+
traces: [
|
|
2055
|
+
{
|
|
2056
|
+
traceTypeCode: "1",
|
|
2057
|
+
referenceId: rng.digits(7),
|
|
2058
|
+
originatingCompanyId: `1${rng.digits(9)}`
|
|
2059
|
+
}
|
|
2060
|
+
],
|
|
2061
|
+
payer: {
|
|
2062
|
+
entityIdentifierCode: "PR",
|
|
2063
|
+
name: payer.name,
|
|
2064
|
+
address: {
|
|
2065
|
+
lines: [org.address.street],
|
|
2066
|
+
city: org.address.city,
|
|
2067
|
+
state: org.address.state,
|
|
2068
|
+
postalCode: org.address.zip
|
|
2069
|
+
},
|
|
2070
|
+
additionalIdentifiers: [{ qualifier: "2U", value: payer.payerId }]
|
|
2071
|
+
},
|
|
2072
|
+
payee: {
|
|
2073
|
+
entityIdentifierCode: "PE",
|
|
2074
|
+
name: org.name,
|
|
2075
|
+
idQualifier: "XX",
|
|
2076
|
+
idCode: org.npi,
|
|
2077
|
+
address: {
|
|
2078
|
+
lines: [org.address.street],
|
|
2079
|
+
city: org.address.city,
|
|
2080
|
+
state: org.address.state,
|
|
2081
|
+
postalCode: org.address.zip
|
|
2082
|
+
}
|
|
2083
|
+
},
|
|
2084
|
+
claims: [claim]
|
|
2085
|
+
};
|
|
2086
|
+
return build835(spec);
|
|
2087
|
+
}
|
|
2088
|
+
function generate271(options) {
|
|
2089
|
+
const rng = createRng(options.seed);
|
|
2090
|
+
const partners = x12TradingPartners(rng);
|
|
2091
|
+
const timing = x12EnvelopeTiming(rng);
|
|
2092
|
+
const subscriber = x12Person(rng);
|
|
2093
|
+
const providerOrg = x12Organization(rng);
|
|
2094
|
+
const payer = x12Payer(rng);
|
|
2095
|
+
const serviceTypes = [
|
|
2096
|
+
{ code: rng.pick(SERVICE_TYPE_CODES) },
|
|
2097
|
+
{ code: rng.pick(SERVICE_TYPE_CODES) }
|
|
2098
|
+
];
|
|
2099
|
+
const spec = {
|
|
2100
|
+
envelope: {
|
|
2101
|
+
senderId: partners.senderId,
|
|
2102
|
+
receiverId: partners.receiverId,
|
|
2103
|
+
interchangeDate: timing.interchangeDate,
|
|
2104
|
+
interchangeTime: timing.interchangeTime,
|
|
2105
|
+
interchangeControlNumber: timing.interchangeControlNumber,
|
|
2106
|
+
groupControlNumber: timing.groupControlNumber,
|
|
2107
|
+
transactionSetControlNumber: timing.transactionSetControlNumber,
|
|
2108
|
+
usageIndicator: "T"
|
|
2109
|
+
},
|
|
2110
|
+
informationSources: [
|
|
2111
|
+
{
|
|
2112
|
+
entity: {
|
|
2113
|
+
entityIdentifierCode: "PR",
|
|
2114
|
+
entityTypeQualifier: "2",
|
|
2115
|
+
name: payer.name,
|
|
2116
|
+
idQualifier: "PI",
|
|
2117
|
+
idCode: payer.payerId
|
|
2118
|
+
},
|
|
2119
|
+
receivers: [
|
|
2120
|
+
{
|
|
2121
|
+
entity: {
|
|
2122
|
+
entityIdentifierCode: "1P",
|
|
2123
|
+
entityTypeQualifier: "2",
|
|
2124
|
+
name: providerOrg.name,
|
|
2125
|
+
idQualifier: "XX",
|
|
2126
|
+
idCode: providerOrg.npi
|
|
2127
|
+
},
|
|
2128
|
+
subscribers: [
|
|
2129
|
+
{
|
|
2130
|
+
traces: [{ traceTypeCode: "2", referenceId: `ELIG${rng.digits(10)}` }],
|
|
2131
|
+
name: {
|
|
2132
|
+
entityIdentifierCode: "IL",
|
|
2133
|
+
entityTypeQualifier: "1",
|
|
2134
|
+
lastName: subscriber.person.family,
|
|
2135
|
+
firstName: subscriber.person.given,
|
|
2136
|
+
idQualifier: "MI",
|
|
2137
|
+
idCode: subscriber.memberId,
|
|
2138
|
+
address: {
|
|
2139
|
+
lines: [subscriber.address.street],
|
|
2140
|
+
city: subscriber.address.city,
|
|
2141
|
+
state: subscriber.address.state,
|
|
2142
|
+
postalCode: subscriber.address.zip
|
|
2143
|
+
},
|
|
2144
|
+
dateOfBirth: subscriber.dob,
|
|
2145
|
+
genderCode: subscriber.sex
|
|
2146
|
+
},
|
|
2147
|
+
references: [{ qualifier: "6P", value: `GRP${rng.digits(5)}` }],
|
|
2148
|
+
dates: [{ qualifier: "307", formatQualifier: "D8", value: timing.serviceDate }],
|
|
2149
|
+
benefits: [
|
|
2150
|
+
{
|
|
2151
|
+
eligibilityCode: "1",
|
|
2152
|
+
// active coverage
|
|
2153
|
+
coverageLevelCode: "IND",
|
|
2154
|
+
serviceTypeCodes: serviceTypes,
|
|
2155
|
+
inPlanNetwork: "Y",
|
|
2156
|
+
monetaryAmount: dec2(money(rng.int(500, 5e3))),
|
|
2157
|
+
percent: dec2(String(rng.pick([80, 90, 100])))
|
|
2158
|
+
}
|
|
2159
|
+
]
|
|
2160
|
+
}
|
|
2161
|
+
]
|
|
2162
|
+
}
|
|
2163
|
+
]
|
|
2164
|
+
}
|
|
2165
|
+
]
|
|
2166
|
+
};
|
|
2167
|
+
return build271(spec);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// src/ncpdp/identity.ts
|
|
2171
|
+
var SYNTHETIC_PHARMACY_NAMES = Object.freeze([
|
|
2172
|
+
"SYNTH COMMUNITY PHARMACY",
|
|
2173
|
+
"FIXTURE DRUG MART",
|
|
2174
|
+
"PLACEHOLDER RX SERVICES",
|
|
2175
|
+
"SAMPLE APOTHECARY LLC",
|
|
2176
|
+
"MOCK PHARMACY GROUP"
|
|
2177
|
+
]);
|
|
2178
|
+
function ncpdpPatient(rng) {
|
|
2179
|
+
const person = safe.name(rng);
|
|
2180
|
+
const dob = safe.dateYmd(rng, 1935, 2010);
|
|
2181
|
+
const gender = rng.pick(["1", "2"]);
|
|
2182
|
+
const patientId = `MBR${safe.identifier(rng, "MR").value}`;
|
|
2183
|
+
const phone2 = safe.phone(rng);
|
|
2184
|
+
const address2 = safe.address(rng);
|
|
2185
|
+
return { person, dob, gender, patientId, phone: phone2, address: address2 };
|
|
2186
|
+
}
|
|
2187
|
+
function ncpdpPrescriber(rng) {
|
|
2188
|
+
const person = safe.name(rng);
|
|
2189
|
+
const npi2 = safe.npi(rng);
|
|
2190
|
+
const dea2 = safe.dea(rng, person);
|
|
2191
|
+
return { person, npi: npi2, dea: dea2 };
|
|
2192
|
+
}
|
|
2193
|
+
function ncpdpPharmacy(rng) {
|
|
2194
|
+
const businessName = rng.pick(SYNTHETIC_PHARMACY_NAMES);
|
|
2195
|
+
const npi2 = safe.npi(rng);
|
|
2196
|
+
const ncpdpId = rng.digits(7);
|
|
2197
|
+
return { businessName, npi: npi2, ncpdpId };
|
|
2198
|
+
}
|
|
2199
|
+
function ncpdpCardholder(rng) {
|
|
2200
|
+
const person = safe.name(rng);
|
|
2201
|
+
const cardholderId = `MBR${safe.identifier(rng, "MB").value}`;
|
|
2202
|
+
const groupId = `GRP${rng.digits(5)}`;
|
|
2203
|
+
const personCode = rng.pick(["01", "02", "03"]);
|
|
2204
|
+
return { person, cardholderId, groupId, personCode };
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/ncpdp/example-codes.ts
|
|
2208
|
+
var EXAMPLE_DRUGS = Object.freeze([
|
|
2209
|
+
{ description: "Amoxicillin 500 MG Oral Capsule", ndc: "00000010101", form: "EA" },
|
|
2210
|
+
{ description: "Lisinopril 10 MG Oral Tablet", ndc: "00000020202", form: "EA" },
|
|
2211
|
+
{ description: "Atorvastatin 20 MG Oral Tablet", ndc: "00000030303", form: "EA" },
|
|
2212
|
+
{ description: "Metformin 500 MG Oral Tablet", ndc: "00000040404", form: "EA" },
|
|
2213
|
+
{ description: "Amlodipine 5 MG Oral Tablet", ndc: "00000050505", form: "EA" },
|
|
2214
|
+
{ description: "Omeprazole 20 MG Delayed Release Capsule", ndc: "00000060606", form: "EA" },
|
|
2215
|
+
{ description: "Levothyroxine 50 MCG Oral Tablet", ndc: "00000070707", form: "EA" },
|
|
2216
|
+
{ description: "Azithromycin 250 MG Oral Tablet", ndc: "00000080808", form: "EA" },
|
|
2217
|
+
{ description: "Sertraline 50 MG Oral Tablet", ndc: "00000090909", form: "EA" },
|
|
2218
|
+
{ description: "Albuterol 90 MCG Metered Dose Inhaler", ndc: "00000101010", form: "EA" }
|
|
2219
|
+
]);
|
|
2220
|
+
var DAW_CODES = Object.freeze(["0", "1", "2"]);
|
|
2221
|
+
var SYNTHETIC_BIN = "999999";
|
|
2222
|
+
function digitsOnly(value) {
|
|
2223
|
+
return value.replace(/\D/g, "");
|
|
2224
|
+
}
|
|
2225
|
+
function header(rng, transactionCode, pharmacyNpi) {
|
|
2226
|
+
const date = serviceDate(rng);
|
|
2227
|
+
return {
|
|
2228
|
+
binNumber: SYNTHETIC_BIN,
|
|
2229
|
+
versionRelease: "D0",
|
|
2230
|
+
transactionCode,
|
|
2231
|
+
processorControlNumber: `SYN${rng.digits(4)}`,
|
|
2232
|
+
transactionCount: "1",
|
|
2233
|
+
serviceProviderIdQualifier: "01",
|
|
2234
|
+
// 01 = NPI
|
|
2235
|
+
serviceProviderId: pharmacyNpi,
|
|
2236
|
+
dateOfService: date,
|
|
2237
|
+
softwareCertificationId: `SW${rng.digits(6)}`
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
function serviceDate(rng) {
|
|
2241
|
+
const year = rng.int(2024, 2026);
|
|
2242
|
+
const month = rng.int(1, 12);
|
|
2243
|
+
const day = rng.int(1, 28);
|
|
2244
|
+
return `${String(year)}${String(month).padStart(2, "0")}${String(day).padStart(2, "0")}`;
|
|
2245
|
+
}
|
|
2246
|
+
function field(id, value) {
|
|
2247
|
+
return { id, value };
|
|
2248
|
+
}
|
|
2249
|
+
function patientSegment(rng) {
|
|
2250
|
+
const p = ncpdpPatient(rng);
|
|
2251
|
+
return {
|
|
2252
|
+
segmentId: "01",
|
|
2253
|
+
fields: [
|
|
2254
|
+
field("C4", p.dob),
|
|
2255
|
+
field("C5", p.gender),
|
|
2256
|
+
field("CA", p.person.given),
|
|
2257
|
+
field("CB", p.person.family),
|
|
2258
|
+
field("CM", p.address.street),
|
|
2259
|
+
field("CQ", digitsOnly(p.phone)),
|
|
2260
|
+
field("CY", p.patientId)
|
|
2261
|
+
]
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
function insuranceSegment(rng) {
|
|
2265
|
+
const c = ncpdpCardholder(rng);
|
|
2266
|
+
return {
|
|
2267
|
+
segmentId: "04",
|
|
2268
|
+
fields: [
|
|
2269
|
+
field("C2", c.cardholderId),
|
|
2270
|
+
field("C1", c.groupId),
|
|
2271
|
+
field("C3", c.personCode),
|
|
2272
|
+
field("CC", c.person.given),
|
|
2273
|
+
field("CD", c.person.family)
|
|
2274
|
+
]
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
function prescriberSegment(rng) {
|
|
2278
|
+
const dr = ncpdpPrescriber(rng);
|
|
2279
|
+
return {
|
|
2280
|
+
segmentId: "03",
|
|
2281
|
+
fields: [field("EZ", "01"), field("DB", dr.npi)]
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
function claimSegment(rng, minimal) {
|
|
2285
|
+
const drug = rng.pick(EXAMPLE_DRUGS);
|
|
2286
|
+
const rxRef = `RX${rng.digits(7)}`;
|
|
2287
|
+
const fill = String(rng.int(0, 5));
|
|
2288
|
+
if (minimal) {
|
|
2289
|
+
return {
|
|
2290
|
+
segmentId: "07",
|
|
2291
|
+
fields: [field("EM", "1"), field("D2", rxRef), field("D3", fill)]
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
return {
|
|
2295
|
+
segmentId: "07",
|
|
2296
|
+
fields: [
|
|
2297
|
+
field("EM", "1"),
|
|
2298
|
+
// 1 = Rx Billing
|
|
2299
|
+
field("D2", rxRef),
|
|
2300
|
+
field("D3", fill),
|
|
2301
|
+
field("E1", "03"),
|
|
2302
|
+
// 03 = NDC
|
|
2303
|
+
field("D7", drug.ndc),
|
|
2304
|
+
field("E7", String(rng.int(30, 90))),
|
|
2305
|
+
field("D5", String(rng.pick([30, 60, 90]))),
|
|
2306
|
+
field("D8", rng.pick(DAW_CODES))
|
|
2307
|
+
]
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
function generateTelecom(code, options) {
|
|
2311
|
+
const rng = createRng(options.seed);
|
|
2312
|
+
const pharmacy = ncpdpPharmacy(rng);
|
|
2313
|
+
const head = header(rng, code, pharmacy.npi);
|
|
2314
|
+
const segments = code === "B2" ? [insuranceSegment(rng), claimSegment(rng, true)] : [
|
|
2315
|
+
patientSegment(rng),
|
|
2316
|
+
insuranceSegment(rng),
|
|
2317
|
+
prescriberSegment(rng),
|
|
2318
|
+
claimSegment(rng, false)
|
|
2319
|
+
];
|
|
2320
|
+
return serializeTelecom(buildTelecomRequest({ header: head, segments }));
|
|
2321
|
+
}
|
|
2322
|
+
function generateB1(options) {
|
|
2323
|
+
return generateTelecom("B1", options);
|
|
2324
|
+
}
|
|
2325
|
+
function generateB2(options) {
|
|
2326
|
+
return generateTelecom("B2", options);
|
|
2327
|
+
}
|
|
2328
|
+
function generateB3(options) {
|
|
2329
|
+
return generateTelecom("B3", options);
|
|
2330
|
+
}
|
|
2331
|
+
var URI_TO_OID = Object.freeze({
|
|
2332
|
+
"http://loinc.org": LOINC,
|
|
2333
|
+
"http://snomed.info/sct": SNOMED_CT,
|
|
2334
|
+
"http://www.nlm.nih.gov/research/umls/rxnorm": RXNORM,
|
|
2335
|
+
"http://hl7.org/fhir/sid/cvx": CVX
|
|
2336
|
+
});
|
|
2337
|
+
function toBuildCode(concept) {
|
|
2338
|
+
const codeSystem = URI_TO_OID[concept.system];
|
|
2339
|
+
if (codeSystem === void 0) {
|
|
2340
|
+
throw new Error(`ccda example-codes: no OID mapping for code system "${concept.system}"`);
|
|
2341
|
+
}
|
|
2342
|
+
return { code: concept.code, codeSystem, displayName: concept.display };
|
|
2343
|
+
}
|
|
2344
|
+
function quantityFor(rng, concept) {
|
|
2345
|
+
const scale = 10 ** concept.decimals;
|
|
2346
|
+
const value = rng.int(concept.low * scale, concept.high * scale) / scale;
|
|
2347
|
+
return { value, unit: concept.unit };
|
|
2348
|
+
}
|
|
2349
|
+
var PROBLEMS = EXAMPLE_CONDITIONS;
|
|
2350
|
+
var ALLERGENS = EXAMPLE_ALLERGENS;
|
|
2351
|
+
var ALLERGY_REACTIONS = EXAMPLE_ALLERGY_MANIFESTATIONS;
|
|
2352
|
+
var MEDICATIONS = EXAMPLE_MEDICATIONS;
|
|
2353
|
+
var LAB_RESULTS = EXAMPLE_LAB_OBSERVATIONS2;
|
|
2354
|
+
var RESULT_PANELS = EXAMPLE_DIAGNOSTIC_REPORTS;
|
|
2355
|
+
var VITAL_SIGNS = EXAMPLE_VITAL_SIGNS;
|
|
2356
|
+
var VACCINES = EXAMPLE_VACCINES2;
|
|
2357
|
+
var PROCEDURES = EXAMPLE_PROCEDURES;
|
|
2358
|
+
var SMOKING_STATUSES = Object.freeze([
|
|
2359
|
+
Object.freeze({ code: "266919005", codeSystem: SNOMED_CT, displayName: "Never smoker" }),
|
|
2360
|
+
Object.freeze({ code: "8517006", codeSystem: SNOMED_CT, displayName: "Former smoker" }),
|
|
2361
|
+
Object.freeze({
|
|
2362
|
+
code: "449868002",
|
|
2363
|
+
codeSystem: SNOMED_CT,
|
|
2364
|
+
displayName: "Current every day smoker"
|
|
2365
|
+
}),
|
|
2366
|
+
Object.freeze({
|
|
2367
|
+
code: "428041000124106",
|
|
2368
|
+
codeSystem: SNOMED_CT,
|
|
2369
|
+
displayName: "Current some day smoker"
|
|
2370
|
+
})
|
|
2371
|
+
]);
|
|
2372
|
+
var ROUTES = Object.freeze([
|
|
2373
|
+
Object.freeze({ code: "C38288", codeSystem: NCI_ROUTE, displayName: "Oral" }),
|
|
2374
|
+
Object.freeze({ code: "C28161", codeSystem: NCI_ROUTE, displayName: "Intramuscular" }),
|
|
2375
|
+
Object.freeze({ code: "C38276", codeSystem: NCI_ROUTE, displayName: "Intravenous" }),
|
|
2376
|
+
Object.freeze({ code: "C38299", codeSystem: NCI_ROUTE, displayName: "Subcutaneous" })
|
|
2377
|
+
]);
|
|
2378
|
+
|
|
2379
|
+
// src/ccda/identity.ts
|
|
2380
|
+
function ccdaPatientIdentity(rng) {
|
|
2381
|
+
const person = safe.name(rng);
|
|
2382
|
+
const mrn = safe.identifier(rng, "MR");
|
|
2383
|
+
const birthTime = safe.dateYmd(rng, 1930, 2010);
|
|
2384
|
+
const gender = rng.pick(["M", "F"]);
|
|
2385
|
+
const patient = {
|
|
2386
|
+
mrn: mrn.value,
|
|
2387
|
+
mrnRoot: mrn.assigningAuthorityOid,
|
|
2388
|
+
mrnAssigningAuthority: mrn.assigningAuthority,
|
|
2389
|
+
given: [person.given],
|
|
2390
|
+
family: person.family,
|
|
2391
|
+
gender,
|
|
2392
|
+
birthTime
|
|
2393
|
+
};
|
|
2394
|
+
return { patient, person, mrn };
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// src/ccda/ccd.ts
|
|
2398
|
+
function pickN(rng, pool, n) {
|
|
2399
|
+
const take = Math.min(n, pool.length);
|
|
2400
|
+
const indices = pool.map((_v, i) => i);
|
|
2401
|
+
const out = [];
|
|
2402
|
+
for (let i = 0; i < take; i += 1) {
|
|
2403
|
+
const j = rng.int(0, indices.length - 1);
|
|
2404
|
+
const idx = indices[j];
|
|
2405
|
+
indices.splice(j, 1);
|
|
2406
|
+
out.push(pool[idx]);
|
|
2407
|
+
}
|
|
2408
|
+
return out;
|
|
2409
|
+
}
|
|
2410
|
+
function buildInit(rng, documentType) {
|
|
2411
|
+
const effectiveTime = safe.dateYmd(rng, 2020, 2025);
|
|
2412
|
+
const { patient, person } = ccdaPatientIdentity(rng);
|
|
2413
|
+
const problems = pickN(rng, PROBLEMS, rng.int(1, 3)).map((c) => ({
|
|
2414
|
+
problem: toBuildCode(c),
|
|
2415
|
+
status: "active",
|
|
2416
|
+
onset: safe.dateYmd(rng, 2010, 2019)
|
|
2417
|
+
}));
|
|
2418
|
+
const allergen = rng.pick(ALLERGENS);
|
|
2419
|
+
const reaction = rng.pick(ALLERGY_REACTIONS);
|
|
2420
|
+
const allergies = [{ allergen: toBuildCode(allergen), reaction: toBuildCode(reaction) }];
|
|
2421
|
+
const medications = pickN(rng, MEDICATIONS, rng.int(1, 2)).map((c) => ({
|
|
2422
|
+
drug: toBuildCode(c),
|
|
2423
|
+
dose: { value: 1, unit: "{tablet}" },
|
|
2424
|
+
route: rng.pick(ROUTES),
|
|
2425
|
+
frequency: { value: rng.pick([8, 12, 24]), unit: "h" }
|
|
2426
|
+
}));
|
|
2427
|
+
const panel = rng.pick(RESULT_PANELS);
|
|
2428
|
+
const resultMembers = pickN(rng, LAB_RESULTS, 2).map((q) => ({
|
|
2429
|
+
test: toBuildCode(q),
|
|
2430
|
+
quantity: quantityFor(rng, q),
|
|
2431
|
+
effectiveTime
|
|
2432
|
+
}));
|
|
2433
|
+
const results = [{ code: toBuildCode(panel), effectiveTime, results: resultMembers }];
|
|
2434
|
+
const vitalMembers = pickN(rng, VITAL_SIGNS, 2).map((q) => ({
|
|
2435
|
+
code: toBuildCode(q),
|
|
2436
|
+
quantity: quantityFor(rng, q)
|
|
2437
|
+
}));
|
|
2438
|
+
const vitalSigns = [{ effectiveTime, vitals: vitalMembers }];
|
|
2439
|
+
const vaccine = rng.pick(VACCINES);
|
|
2440
|
+
const immunizations = [
|
|
2441
|
+
{
|
|
2442
|
+
vaccine: toBuildCode(vaccine),
|
|
2443
|
+
dose: { value: 0.5, unit: "mL" },
|
|
2444
|
+
route: rng.pick(ROUTES),
|
|
2445
|
+
effectiveTime: safe.dateYmd(rng, 2018, 2024)
|
|
2446
|
+
}
|
|
2447
|
+
];
|
|
2448
|
+
const procedure = rng.pick(PROCEDURES);
|
|
2449
|
+
const procedures = [
|
|
2450
|
+
{
|
|
2451
|
+
code: toBuildCode(procedure),
|
|
2452
|
+
disposition: "performed",
|
|
2453
|
+
effectiveTime: safe.dateYmd(rng, 2015, 2023)
|
|
2454
|
+
}
|
|
2455
|
+
];
|
|
2456
|
+
const smokingStatus = [{ value: rng.pick(SMOKING_STATUSES), effectiveTime }];
|
|
2457
|
+
const base = {
|
|
2458
|
+
documentType,
|
|
2459
|
+
effectiveTime,
|
|
2460
|
+
patient,
|
|
2461
|
+
problems,
|
|
2462
|
+
allergies,
|
|
2463
|
+
medications,
|
|
2464
|
+
results,
|
|
2465
|
+
vitalSigns,
|
|
2466
|
+
immunizations,
|
|
2467
|
+
procedures,
|
|
2468
|
+
smokingStatus
|
|
2469
|
+
};
|
|
2470
|
+
if (documentType === "referralNote") {
|
|
2471
|
+
return {
|
|
2472
|
+
...base,
|
|
2473
|
+
reasonForReferral: `Synthetic referral for evaluation of ${person.given} ${person.family}.`,
|
|
2474
|
+
assessment: "Synthetic assessment narrative. Not a real clinical assessment."
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
return base;
|
|
2478
|
+
}
|
|
2479
|
+
function generateCcda(options = {}) {
|
|
2480
|
+
const { seed = 0, documentType = "ccd" } = options;
|
|
2481
|
+
const rng = createRng(seed);
|
|
2482
|
+
return buildCcda(buildInit(rng, documentType));
|
|
2483
|
+
}
|
|
2484
|
+
var DEID_LOOP_POLICY_NAME = "synth-deid-loop-removal";
|
|
2485
|
+
function deidLoopPolicy() {
|
|
2486
|
+
return defineDeidPolicy({
|
|
2487
|
+
name: DEID_LOOP_POLICY_NAME,
|
|
2488
|
+
transforms: {
|
|
2489
|
+
[SAFE_HARBOR_CATEGORIES.MRN]: "redact",
|
|
2490
|
+
[SAFE_HARBOR_CATEGORIES.HEALTH_PLAN_BENEFICIARY]: "redact",
|
|
2491
|
+
[SAFE_HARBOR_CATEGORIES.ACCOUNT]: "redact"
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
2496
|
+
"official",
|
|
2497
|
+
"usual",
|
|
2498
|
+
"temp",
|
|
2499
|
+
"old",
|
|
2500
|
+
"anonymous",
|
|
2501
|
+
"maiden",
|
|
2502
|
+
"nickname",
|
|
2503
|
+
"home",
|
|
2504
|
+
"work",
|
|
2505
|
+
"mobile",
|
|
2506
|
+
"phone",
|
|
2507
|
+
"email",
|
|
2508
|
+
"fax",
|
|
2509
|
+
"sms",
|
|
2510
|
+
"pager",
|
|
2511
|
+
"other",
|
|
2512
|
+
"use",
|
|
2513
|
+
"street",
|
|
2514
|
+
"road",
|
|
2515
|
+
"avenue",
|
|
2516
|
+
"court",
|
|
2517
|
+
"lane",
|
|
2518
|
+
"drive",
|
|
2519
|
+
"boulevard",
|
|
2520
|
+
"place",
|
|
2521
|
+
"way",
|
|
2522
|
+
"suite",
|
|
2523
|
+
"apt",
|
|
2524
|
+
"unit",
|
|
2525
|
+
"null",
|
|
2526
|
+
"undefined"
|
|
2527
|
+
]);
|
|
2528
|
+
function tokenize(value) {
|
|
2529
|
+
return value.split(/[\s^~&|,()/\\]+/).map((t) => t.trim()).filter((t) => t.length > 0);
|
|
2530
|
+
}
|
|
2531
|
+
function isDistinctive(token) {
|
|
2532
|
+
if (token.length < 4) return false;
|
|
2533
|
+
if (STOPWORDS.has(token.toLowerCase())) return false;
|
|
2534
|
+
if (/^\d+$/.test(token) && token.length < 5) return false;
|
|
2535
|
+
return true;
|
|
2536
|
+
}
|
|
2537
|
+
function identifierSentinels(loci, original) {
|
|
2538
|
+
const out = [];
|
|
2539
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2540
|
+
for (const locus of loci) {
|
|
2541
|
+
if (locus.kind !== "identifier" || locus.value.length === 0) continue;
|
|
2542
|
+
for (const token of tokenize(locus.value)) {
|
|
2543
|
+
if (!isDistinctive(token)) continue;
|
|
2544
|
+
if (!original.includes(token)) continue;
|
|
2545
|
+
if (seen.has(token)) continue;
|
|
2546
|
+
seen.add(token);
|
|
2547
|
+
out.push({
|
|
2548
|
+
token,
|
|
2549
|
+
locus: locus.path,
|
|
2550
|
+
...locus.category !== void 0 ? { category: locus.category } : {}
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
return out;
|
|
2555
|
+
}
|
|
2556
|
+
function recordTargetSentinels(xml) {
|
|
2557
|
+
const block = /<recordTarget[\s\S]*?<\/recordTarget>/i.exec(xml);
|
|
2558
|
+
if (block === null) return [];
|
|
2559
|
+
const scope = block[0];
|
|
2560
|
+
const out = [];
|
|
2561
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2562
|
+
const add = (token, locus) => {
|
|
2563
|
+
const trimmed = token.trim();
|
|
2564
|
+
if (!isDistinctive(trimmed) || seen.has(trimmed)) return;
|
|
2565
|
+
seen.add(trimmed);
|
|
2566
|
+
out.push({ token: trimmed, locus, category: "names" });
|
|
2567
|
+
};
|
|
2568
|
+
for (const m of scope.matchAll(/<given>([^<]+)<\/given>/g)) add(m[1] ?? "", "recordTarget/given");
|
|
2569
|
+
for (const m of scope.matchAll(/<family>([^<]+)<\/family>/g))
|
|
2570
|
+
add(m[1] ?? "", "recordTarget/family");
|
|
2571
|
+
for (const m of scope.matchAll(/<id\b[^>]*\bextension="([^"]+)"/g))
|
|
2572
|
+
add(m[1] ?? "", "recordTarget/id");
|
|
2573
|
+
return out;
|
|
2574
|
+
}
|
|
2575
|
+
function sweepSurvivors(phiResidue, sentinels) {
|
|
2576
|
+
return sentinels.filter((s) => phiResidue.includes(s.token));
|
|
2577
|
+
}
|
|
2578
|
+
function clinicalRetention(original, deidentified, clinicalCodes) {
|
|
2579
|
+
const probed = [...new Set(clinicalCodes.filter((c) => c.length >= 4 && original.includes(c)))];
|
|
2580
|
+
const scrubbed = probed.filter((c) => !deidentified.includes(c));
|
|
2581
|
+
return { probed, scrubbed };
|
|
2582
|
+
}
|
|
2583
|
+
function assembleVerdict(parts) {
|
|
2584
|
+
const survivors = sweepSurvivors(parts.phiResidue ?? parts.deidentified, parts.planted);
|
|
2585
|
+
const { probed, scrubbed } = clinicalRetention(
|
|
2586
|
+
parts.original,
|
|
2587
|
+
parts.deidentified,
|
|
2588
|
+
parts.clinicalCodes
|
|
2589
|
+
);
|
|
2590
|
+
const pass = parts.planted.length > 0 && survivors.length === 0 && scrubbed.length === 0;
|
|
2591
|
+
return Object.freeze({
|
|
2592
|
+
format: parts.format,
|
|
2593
|
+
artifact: parts.artifact,
|
|
2594
|
+
seed: parts.seed,
|
|
2595
|
+
planted: Object.freeze([...parts.planted]),
|
|
2596
|
+
survivors: Object.freeze(survivors),
|
|
2597
|
+
clinicalProbed: Object.freeze(probed),
|
|
2598
|
+
clinicalScrubbed: Object.freeze(scrubbed),
|
|
2599
|
+
original: parts.original,
|
|
2600
|
+
deidentified: parts.deidentified,
|
|
2601
|
+
pass
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// src/deid/index.ts
|
|
2606
|
+
var DEID_LOOP_COVERED_FORMATS = Object.freeze([
|
|
2607
|
+
"hl7",
|
|
2608
|
+
"fhir",
|
|
2609
|
+
"ccda",
|
|
2610
|
+
"x12",
|
|
2611
|
+
"ncpdp-telecom"
|
|
2612
|
+
]);
|
|
2613
|
+
var DEID_LOOP_SKIPPED = Object.freeze([
|
|
2614
|
+
Object.freeze({
|
|
2615
|
+
format: "ncpdp-script",
|
|
2616
|
+
reason: "@cosyte/deid ships no NCPDP SCRIPT locus map; synth generates SCRIPT but the loop cannot pair it."
|
|
2617
|
+
}),
|
|
2618
|
+
Object.freeze({
|
|
2619
|
+
format: "astm",
|
|
2620
|
+
reason: "@cosyte/deid ships no ASTM adapter; synth generates ASTM but the loop cannot pair it."
|
|
2621
|
+
}),
|
|
2622
|
+
Object.freeze({
|
|
2623
|
+
format: "dicom",
|
|
2624
|
+
reason: "@cosyte/synth does not generate DICOM, so there is nothing to pair."
|
|
2625
|
+
})
|
|
2626
|
+
]);
|
|
2627
|
+
var POLICY = deidLoopPolicy();
|
|
2628
|
+
function residue(loci) {
|
|
2629
|
+
return loci.map((l) => l.value).join("\n");
|
|
2630
|
+
}
|
|
2631
|
+
function recordTargetResidue(xml) {
|
|
2632
|
+
return /<recordTarget[\s\S]*?<\/recordTarget>/i.exec(xml)?.[0] ?? "";
|
|
2633
|
+
}
|
|
2634
|
+
var HL7_CLINICAL = [...EXAMPLE_LAB_OBSERVATIONS, ...EXAMPLE_ORDER_SERVICES, ...EXAMPLE_VACCINES].map(
|
|
2635
|
+
(c) => c.code
|
|
2636
|
+
);
|
|
2637
|
+
var FHIR_CLINICAL = [
|
|
2638
|
+
...EXAMPLE_LAB_OBSERVATIONS2,
|
|
2639
|
+
...EXAMPLE_VITAL_SIGNS,
|
|
2640
|
+
...EXAMPLE_CONDITIONS,
|
|
2641
|
+
...EXAMPLE_MEDICATIONS
|
|
2642
|
+
].map((c) => c.code);
|
|
2643
|
+
var X12_CLINICAL = [...PROFESSIONAL_PROCEDURES, ...INSTITUTIONAL_PROCEDURES, ...DIAGNOSES].map(
|
|
2644
|
+
(c) => c.code
|
|
2645
|
+
);
|
|
2646
|
+
var NCPDP_CLINICAL = EXAMPLE_DRUGS.map((d) => d.ndc);
|
|
2647
|
+
function hl7DeidLoop(options = {}) {
|
|
2648
|
+
const seed = options.seed ?? 0;
|
|
2649
|
+
const kind = options.kind ?? "ORU^R01";
|
|
2650
|
+
const message = generateHl7(kind, seed);
|
|
2651
|
+
const original = message.toString();
|
|
2652
|
+
const planted = identifierSentinels(extractHl7Loci(message).loci, original);
|
|
2653
|
+
const deidDoc = deidentifyHl7(message, { policy: POLICY }).document;
|
|
2654
|
+
const deidentified = deidDoc.toString();
|
|
2655
|
+
return assembleVerdict({
|
|
2656
|
+
format: "hl7",
|
|
2657
|
+
artifact: kind,
|
|
2658
|
+
seed,
|
|
2659
|
+
planted,
|
|
2660
|
+
original,
|
|
2661
|
+
deidentified,
|
|
2662
|
+
phiResidue: residue(extractHl7Loci(deidDoc).loci),
|
|
2663
|
+
clinicalCodes: HL7_CLINICAL
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
function fhirDeidLoop(options = {}) {
|
|
2667
|
+
const seed = options.seed ?? 0;
|
|
2668
|
+
const bundle = generateBundle({ seed });
|
|
2669
|
+
const original = serializeResource(bundle);
|
|
2670
|
+
const planted = identifierSentinels(extractFhirLoci(bundle).loci, original);
|
|
2671
|
+
const deidDoc = deidentifyFhir(bundle, { policy: POLICY }).document;
|
|
2672
|
+
const deidentified = serializeResource(deidDoc);
|
|
2673
|
+
return assembleVerdict({
|
|
2674
|
+
format: "fhir",
|
|
2675
|
+
artifact: "Bundle",
|
|
2676
|
+
seed,
|
|
2677
|
+
planted,
|
|
2678
|
+
original,
|
|
2679
|
+
deidentified,
|
|
2680
|
+
phiResidue: residue(extractFhirLoci(deidDoc).loci),
|
|
2681
|
+
clinicalCodes: FHIR_CLINICAL
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
function generateX12(variant, seed) {
|
|
2685
|
+
switch (variant) {
|
|
2686
|
+
case "837P":
|
|
2687
|
+
return generate837P({ seed });
|
|
2688
|
+
case "837I":
|
|
2689
|
+
return generate837I({ seed });
|
|
2690
|
+
case "837D":
|
|
2691
|
+
return generate837D({ seed });
|
|
2692
|
+
case "271":
|
|
2693
|
+
return generate271({ seed });
|
|
2694
|
+
case "835":
|
|
2695
|
+
return generate835({ seed });
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
function x12DeidLoop(options = {}) {
|
|
2699
|
+
const seed = options.seed ?? 0;
|
|
2700
|
+
const variant = options.variant ?? "837P";
|
|
2701
|
+
const interchange = generateX12(variant, seed);
|
|
2702
|
+
const original = serializeX12(interchange);
|
|
2703
|
+
const planted = identifierSentinels(extractX12Loci(interchange).loci, original);
|
|
2704
|
+
const deidentified = deidentifyX12(interchange, { policy: POLICY }).x12;
|
|
2705
|
+
return assembleVerdict({
|
|
2706
|
+
format: "x12",
|
|
2707
|
+
artifact: variant,
|
|
2708
|
+
seed,
|
|
2709
|
+
planted,
|
|
2710
|
+
original,
|
|
2711
|
+
deidentified,
|
|
2712
|
+
phiResidue: residue(extractX12Loci(parseX12(deidentified)).loci),
|
|
2713
|
+
clinicalCodes: X12_CLINICAL
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
function generateTelecomWire(transaction, seed) {
|
|
2717
|
+
switch (transaction) {
|
|
2718
|
+
case "B1":
|
|
2719
|
+
return generateB1({ seed });
|
|
2720
|
+
case "B2":
|
|
2721
|
+
return generateB2({ seed });
|
|
2722
|
+
case "B3":
|
|
2723
|
+
return generateB3({ seed });
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
function ncpdpTelecomDeidLoop(options = {}) {
|
|
2727
|
+
const seed = options.seed ?? 0;
|
|
2728
|
+
const transaction = options.transaction ?? "B1";
|
|
2729
|
+
const original = generateTelecomWire(transaction, seed);
|
|
2730
|
+
const tx = parseTelecom(original);
|
|
2731
|
+
const planted = identifierSentinels(extractTelecomLoci(tx).loci, original);
|
|
2732
|
+
const deidentified = deidentifyTelecom(tx, { policy: POLICY }).telecom;
|
|
2733
|
+
return assembleVerdict({
|
|
2734
|
+
format: "ncpdp-telecom",
|
|
2735
|
+
artifact: transaction,
|
|
2736
|
+
seed,
|
|
2737
|
+
planted,
|
|
2738
|
+
original,
|
|
2739
|
+
deidentified,
|
|
2740
|
+
phiResidue: residue(extractTelecomLoci(parseTelecom(deidentified)).loci),
|
|
2741
|
+
clinicalCodes: NCPDP_CLINICAL
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
function ccdaDeidLoop(options = {}) {
|
|
2745
|
+
const seed = options.seed ?? 0;
|
|
2746
|
+
const documentType = options.documentType ?? "ccd";
|
|
2747
|
+
const doc = generateCcda({ seed, documentType: documentType === "ccd" ? "ccd" : "referralNote" });
|
|
2748
|
+
const original = serializeCcda(doc);
|
|
2749
|
+
const planted = recordTargetSentinels(original);
|
|
2750
|
+
const deidentified = deidentifyCcda(doc, { policy: POLICY }).document.toString();
|
|
2751
|
+
return assembleVerdict({
|
|
2752
|
+
format: "ccda",
|
|
2753
|
+
artifact: documentType === "ccd" ? "CCD" : "ReferralNote",
|
|
2754
|
+
seed,
|
|
2755
|
+
planted,
|
|
2756
|
+
original,
|
|
2757
|
+
deidentified,
|
|
2758
|
+
phiResidue: recordTargetResidue(deidentified),
|
|
2759
|
+
clinicalCodes: [...EXAMPLE_CONDITIONS, ...EXAMPLE_MEDICATIONS].map((c) => c.code)
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
function summarizeDeidCoverage(results) {
|
|
2763
|
+
const byFormatMap = /* @__PURE__ */ new Map();
|
|
2764
|
+
for (const r of results) {
|
|
2765
|
+
const list10 = byFormatMap.get(r.format) ?? [];
|
|
2766
|
+
list10.push(r);
|
|
2767
|
+
byFormatMap.set(r.format, list10);
|
|
2768
|
+
}
|
|
2769
|
+
const byFormat = [];
|
|
2770
|
+
for (const [format, runs] of byFormatMap) {
|
|
2771
|
+
byFormat.push({
|
|
2772
|
+
format,
|
|
2773
|
+
artifacts: runs.map((r) => r.artifact),
|
|
2774
|
+
planted: runs.reduce((n, r) => n + r.planted.length, 0),
|
|
2775
|
+
survivors: runs.reduce((n, r) => n + r.survivors.length, 0),
|
|
2776
|
+
clinicalProbed: runs.reduce((n, r) => n + r.clinicalProbed.length, 0),
|
|
2777
|
+
clinicalScrubbed: runs.reduce((n, r) => n + r.clinicalScrubbed.length, 0),
|
|
2778
|
+
pass: runs.every((r) => r.pass)
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
return {
|
|
2782
|
+
byFormat,
|
|
2783
|
+
totalPlanted: results.reduce((n, r) => n + r.planted.length, 0),
|
|
2784
|
+
totalSurvivors: results.reduce((n, r) => n + r.survivors.length, 0),
|
|
2785
|
+
totalClinicalScrubbed: results.reduce((n, r) => n + r.clinicalScrubbed.length, 0),
|
|
2786
|
+
allPass: results.every((r) => r.pass),
|
|
2787
|
+
skipped: DEID_LOOP_SKIPPED
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
export { DEID_LOOP_COVERED_FORMATS, DEID_LOOP_POLICY_NAME, DEID_LOOP_SKIPPED, assembleVerdict, ccdaDeidLoop, clinicalRetention, deidLoopPolicy, fhirDeidLoop, hl7DeidLoop, identifierSentinels, ncpdpTelecomDeidLoop, recordTargetSentinels, summarizeDeidCoverage, sweepSurvivors, x12DeidLoop };
|
|
2792
|
+
//# sourceMappingURL=index.mjs.map
|
|
2793
|
+
//# sourceMappingURL=index.mjs.map
|