@erosolaraijs/cure 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/dist/bin/cure.d.ts +10 -0
  4. package/dist/bin/cure.d.ts.map +1 -0
  5. package/dist/bin/cure.js +169 -0
  6. package/dist/bin/cure.js.map +1 -0
  7. package/dist/capabilities/cancerTreatmentCapability.d.ts +167 -0
  8. package/dist/capabilities/cancerTreatmentCapability.d.ts.map +1 -0
  9. package/dist/capabilities/cancerTreatmentCapability.js +912 -0
  10. package/dist/capabilities/cancerTreatmentCapability.js.map +1 -0
  11. package/dist/capabilities/index.d.ts +2 -0
  12. package/dist/capabilities/index.d.ts.map +1 -0
  13. package/dist/capabilities/index.js +3 -0
  14. package/dist/capabilities/index.js.map +1 -0
  15. package/dist/compliance/hipaa.d.ts +337 -0
  16. package/dist/compliance/hipaa.d.ts.map +1 -0
  17. package/dist/compliance/hipaa.js +929 -0
  18. package/dist/compliance/hipaa.js.map +1 -0
  19. package/dist/examples/cancerTreatmentDemo.d.ts +21 -0
  20. package/dist/examples/cancerTreatmentDemo.d.ts.map +1 -0
  21. package/dist/examples/cancerTreatmentDemo.js +216 -0
  22. package/dist/examples/cancerTreatmentDemo.js.map +1 -0
  23. package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts +265 -0
  24. package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts.map +1 -0
  25. package/dist/integrations/clinicalTrials/clinicalTrialsGov.js +808 -0
  26. package/dist/integrations/clinicalTrials/clinicalTrialsGov.js.map +1 -0
  27. package/dist/integrations/ehr/fhir.d.ts +455 -0
  28. package/dist/integrations/ehr/fhir.d.ts.map +1 -0
  29. package/dist/integrations/ehr/fhir.js +859 -0
  30. package/dist/integrations/ehr/fhir.js.map +1 -0
  31. package/dist/integrations/genomics/genomicPlatforms.d.ts +362 -0
  32. package/dist/integrations/genomics/genomicPlatforms.d.ts.map +1 -0
  33. package/dist/integrations/genomics/genomicPlatforms.js +1079 -0
  34. package/dist/integrations/genomics/genomicPlatforms.js.map +1 -0
  35. package/package.json +52 -0
  36. package/src/bin/cure.ts +182 -0
  37. package/src/capabilities/cancerTreatmentCapability.ts +1161 -0
  38. package/src/capabilities/index.ts +2 -0
  39. package/src/compliance/hipaa.ts +1365 -0
  40. package/src/examples/cancerTreatmentDemo.ts +241 -0
  41. package/src/integrations/clinicalTrials/clinicalTrialsGov.ts +1143 -0
  42. package/src/integrations/ehr/fhir.ts +1304 -0
  43. package/src/integrations/genomics/genomicPlatforms.ts +1480 -0
  44. package/src/ml/outcomePredictor.ts +1301 -0
  45. package/src/safety/drugInteractions.ts +942 -0
  46. package/src/validation/retrospectiveValidator.ts +887 -0
@@ -0,0 +1,859 @@
1
+ /**
2
+ * HL7 FHIR Integration Layer
3
+ *
4
+ * Connects to Electronic Health Record systems using the FHIR R4 standard.
5
+ * Supports Epic, Cerner, AllScripts, and other FHIR-compliant EHR systems.
6
+ *
7
+ * IMPORTANT: This module requires proper HIPAA compliance setup before use.
8
+ * All PHI access must be logged and patient consent must be verified.
9
+ */
10
+ import { EventEmitter } from 'events';
11
+ // ═══════════════════════════════════════════════════════════════════════════════
12
+ // FHIR CLIENT IMPLEMENTATION
13
+ // ═══════════════════════════════════════════════════════════════════════════════
14
+ export class FHIRClient extends EventEmitter {
15
+ config;
16
+ accessToken;
17
+ tokenExpiry;
18
+ auditLogger;
19
+ constructor(config) {
20
+ super();
21
+ this.config = {
22
+ timeout: 30000,
23
+ retryAttempts: 3,
24
+ ...config
25
+ };
26
+ }
27
+ /**
28
+ * Set audit logger for HIPAA compliance
29
+ */
30
+ setAuditLogger(logger) {
31
+ this.auditLogger = logger;
32
+ }
33
+ /**
34
+ * Authenticate with the FHIR server
35
+ */
36
+ async authenticate() {
37
+ const startTime = Date.now();
38
+ try {
39
+ switch (this.config.authType) {
40
+ case 'smart-on-fhir':
41
+ await this.smartOnFhirAuth();
42
+ break;
43
+ case 'client-credentials':
44
+ await this.clientCredentialsAuth();
45
+ break;
46
+ case 'oauth2':
47
+ await this.oauth2Auth();
48
+ break;
49
+ case 'basic':
50
+ // Basic auth doesn't require pre-authentication
51
+ break;
52
+ default:
53
+ throw new Error(`Unsupported auth type: ${this.config.authType}`);
54
+ }
55
+ await this.logAudit({
56
+ action: 'authenticate',
57
+ resourceType: 'System',
58
+ outcome: 'success',
59
+ duration: Date.now() - startTime
60
+ });
61
+ }
62
+ catch (error) {
63
+ await this.logAudit({
64
+ action: 'authenticate',
65
+ resourceType: 'System',
66
+ outcome: 'failure',
67
+ duration: Date.now() - startTime,
68
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
69
+ });
70
+ throw error;
71
+ }
72
+ }
73
+ async smartOnFhirAuth() {
74
+ // SMART on FHIR authentication flow
75
+ // This is a simplified version - production would need full OAuth2 flow
76
+ const tokenEndpoint = await this.discoverTokenEndpoint();
77
+ const response = await this.httpRequest(tokenEndpoint, {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/x-www-form-urlencoded'
81
+ },
82
+ body: new URLSearchParams({
83
+ grant_type: 'client_credentials',
84
+ client_id: this.config.clientId,
85
+ client_secret: this.config.clientSecret || '',
86
+ scope: this.config.scopes.join(' ')
87
+ }).toString()
88
+ });
89
+ const data = JSON.parse(response);
90
+ this.accessToken = data.access_token;
91
+ this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000));
92
+ }
93
+ async clientCredentialsAuth() {
94
+ const tokenEndpoint = `${this.config.baseUrl}/oauth2/token`;
95
+ const response = await this.httpRequest(tokenEndpoint, {
96
+ method: 'POST',
97
+ headers: {
98
+ 'Content-Type': 'application/x-www-form-urlencoded',
99
+ 'Authorization': `Basic ${Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64')}`
100
+ },
101
+ body: new URLSearchParams({
102
+ grant_type: 'client_credentials',
103
+ scope: this.config.scopes.join(' ')
104
+ }).toString()
105
+ });
106
+ const data = JSON.parse(response);
107
+ this.accessToken = data.access_token;
108
+ this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000));
109
+ }
110
+ async oauth2Auth() {
111
+ // OAuth2 flow - similar to client credentials for server-to-server
112
+ await this.clientCredentialsAuth();
113
+ }
114
+ async discoverTokenEndpoint() {
115
+ const wellKnown = `${this.config.baseUrl}/.well-known/smart-configuration`;
116
+ try {
117
+ const response = await this.httpRequest(wellKnown, { method: 'GET' });
118
+ const config = JSON.parse(response);
119
+ return config.token_endpoint;
120
+ }
121
+ catch {
122
+ // Fall back to standard OAuth2 endpoint
123
+ return `${this.config.baseUrl}/oauth2/token`;
124
+ }
125
+ }
126
+ /**
127
+ * Get a patient by ID
128
+ */
129
+ async getPatient(patientId) {
130
+ return await this.read('Patient', patientId);
131
+ }
132
+ /**
133
+ * Search for patients
134
+ */
135
+ async searchPatients(params) {
136
+ return await this.search('Patient', params);
137
+ }
138
+ /**
139
+ * Get cancer diagnosis for a patient
140
+ */
141
+ async getCancerDiagnosis(patientId) {
142
+ const bundle = await this.search('Condition', {
143
+ patient: patientId,
144
+ category: 'encounter-diagnosis',
145
+ 'code:below': '363346000' // SNOMED CT code for malignant neoplasm
146
+ });
147
+ const diagnoses = [];
148
+ for (const entry of bundle.entry || []) {
149
+ const condition = entry.resource;
150
+ if (condition.resourceType === 'Condition') {
151
+ diagnoses.push(this.mapConditionToCancerDiagnosis(condition, patientId));
152
+ }
153
+ }
154
+ return diagnoses;
155
+ }
156
+ /**
157
+ * Get biomarkers and genomic data for a patient
158
+ */
159
+ async getBiomarkers(patientId) {
160
+ // Get lab observations
161
+ const labBundle = await this.search('Observation', {
162
+ patient: patientId,
163
+ category: 'laboratory',
164
+ _sort: '-date',
165
+ _count: '100'
166
+ });
167
+ // Get genomic observations
168
+ const genomicBundle = await this.search('Observation', {
169
+ patient: patientId,
170
+ category: 'genomic-variant',
171
+ _sort: '-date',
172
+ _count: '100'
173
+ });
174
+ // Get diagnostic reports (for NGS results)
175
+ const reportBundle = await this.search('DiagnosticReport', {
176
+ patient: patientId,
177
+ category: 'GE', // Genetics
178
+ _sort: '-date',
179
+ _count: '50'
180
+ });
181
+ return this.aggregateBiomarkerData(patientId, labBundle, genomicBundle, reportBundle);
182
+ }
183
+ /**
184
+ * Get treatment history for a patient
185
+ */
186
+ async getTreatmentHistory(patientId) {
187
+ const [medications, procedures] = await Promise.all([
188
+ this.search('MedicationRequest', {
189
+ patient: patientId,
190
+ _sort: '-authoredon',
191
+ _count: '200'
192
+ }),
193
+ this.search('Procedure', {
194
+ patient: patientId,
195
+ _sort: '-date',
196
+ _count: '200'
197
+ })
198
+ ]);
199
+ return this.aggregateTreatmentHistory(patientId, medications, procedures);
200
+ }
201
+ /**
202
+ * Get all observations for a patient
203
+ */
204
+ async getObservations(patientId, category) {
205
+ const params = {
206
+ patient: patientId,
207
+ _sort: '-date',
208
+ _count: '100'
209
+ };
210
+ if (category) {
211
+ params.category = category;
212
+ }
213
+ const bundle = await this.search('Observation', params);
214
+ return (bundle.entry || [])
215
+ .map(e => e.resource)
216
+ .filter((r) => r?.resourceType === 'Observation');
217
+ }
218
+ /**
219
+ * Get all medications for a patient
220
+ */
221
+ async getMedications(patientId) {
222
+ const bundle = await this.search('MedicationRequest', {
223
+ patient: patientId,
224
+ _sort: '-authoredon'
225
+ });
226
+ return (bundle.entry || [])
227
+ .map(e => e.resource)
228
+ .filter((r) => r?.resourceType === 'MedicationRequest');
229
+ }
230
+ /**
231
+ * Get diagnostic reports for a patient
232
+ */
233
+ async getDiagnosticReports(patientId) {
234
+ const bundle = await this.search('DiagnosticReport', {
235
+ patient: patientId,
236
+ _sort: '-date'
237
+ });
238
+ return (bundle.entry || [])
239
+ .map(e => e.resource)
240
+ .filter((r) => r?.resourceType === 'DiagnosticReport');
241
+ }
242
+ /**
243
+ * Create a comprehensive cancer patient summary
244
+ */
245
+ async getComprehensiveCancerSummary(patientId) {
246
+ const [patient, diagnoses, biomarkers, treatmentHistory, recentLabs] = await Promise.all([
247
+ this.getPatient(patientId),
248
+ this.getCancerDiagnosis(patientId),
249
+ this.getBiomarkers(patientId),
250
+ this.getTreatmentHistory(patientId),
251
+ this.getObservations(patientId, 'laboratory')
252
+ ]);
253
+ // Get performance status (ECOG/KPS)
254
+ const performanceObs = await this.search('Observation', {
255
+ patient: patientId,
256
+ code: '89247-1', // LOINC for ECOG performance status
257
+ _sort: '-date',
258
+ _count: '1'
259
+ });
260
+ let performanceStatus;
261
+ const perfEntry = performanceObs.entry?.[0]?.resource;
262
+ if (perfEntry?.valueQuantity?.value !== undefined) {
263
+ performanceStatus = {
264
+ score: perfEntry.valueQuantity.value,
265
+ scale: 'ECOG',
266
+ date: new Date(perfEntry.effectiveDateTime || Date.now())
267
+ };
268
+ }
269
+ return {
270
+ patient,
271
+ diagnoses,
272
+ biomarkers,
273
+ treatmentHistory,
274
+ recentLabs: recentLabs.slice(0, 20),
275
+ performanceStatus
276
+ };
277
+ }
278
+ // ═══════════════════════════════════════════════════════════════════════════════
279
+ // CORE FHIR OPERATIONS
280
+ // ═══════════════════════════════════════════════════════════════════════════════
281
+ /**
282
+ * Read a resource by ID
283
+ */
284
+ async read(resourceType, id) {
285
+ const url = `${this.config.baseUrl}/${resourceType}/${id}`;
286
+ const startTime = Date.now();
287
+ try {
288
+ const response = await this.httpRequest(url, {
289
+ method: 'GET',
290
+ headers: await this.getAuthHeaders()
291
+ });
292
+ await this.logAudit({
293
+ action: 'read',
294
+ resourceType,
295
+ resourceId: id,
296
+ outcome: 'success',
297
+ duration: Date.now() - startTime
298
+ });
299
+ return JSON.parse(response);
300
+ }
301
+ catch (error) {
302
+ await this.logAudit({
303
+ action: 'read',
304
+ resourceType,
305
+ resourceId: id,
306
+ outcome: 'failure',
307
+ duration: Date.now() - startTime,
308
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
309
+ });
310
+ throw error;
311
+ }
312
+ }
313
+ /**
314
+ * Search for resources
315
+ */
316
+ async search(resourceType, params) {
317
+ const searchParams = new URLSearchParams(params);
318
+ const url = `${this.config.baseUrl}/${resourceType}?${searchParams.toString()}`;
319
+ const startTime = Date.now();
320
+ try {
321
+ const response = await this.httpRequest(url, {
322
+ method: 'GET',
323
+ headers: await this.getAuthHeaders()
324
+ });
325
+ await this.logAudit({
326
+ action: 'search',
327
+ resourceType,
328
+ outcome: 'success',
329
+ duration: Date.now() - startTime,
330
+ details: { searchParams: params }
331
+ });
332
+ return JSON.parse(response);
333
+ }
334
+ catch (error) {
335
+ await this.logAudit({
336
+ action: 'search',
337
+ resourceType,
338
+ outcome: 'failure',
339
+ duration: Date.now() - startTime,
340
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
341
+ });
342
+ throw error;
343
+ }
344
+ }
345
+ /**
346
+ * Create a resource
347
+ */
348
+ async create(resource) {
349
+ const url = `${this.config.baseUrl}/${resource.resourceType}`;
350
+ const startTime = Date.now();
351
+ try {
352
+ const response = await this.httpRequest(url, {
353
+ method: 'POST',
354
+ headers: {
355
+ ...await this.getAuthHeaders(),
356
+ 'Content-Type': 'application/fhir+json'
357
+ },
358
+ body: JSON.stringify(resource)
359
+ });
360
+ await this.logAudit({
361
+ action: 'create',
362
+ resourceType: resource.resourceType,
363
+ outcome: 'success',
364
+ duration: Date.now() - startTime
365
+ });
366
+ return JSON.parse(response);
367
+ }
368
+ catch (error) {
369
+ await this.logAudit({
370
+ action: 'create',
371
+ resourceType: resource.resourceType,
372
+ outcome: 'failure',
373
+ duration: Date.now() - startTime,
374
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
375
+ });
376
+ throw error;
377
+ }
378
+ }
379
+ /**
380
+ * Update a resource
381
+ */
382
+ async update(resource) {
383
+ const url = `${this.config.baseUrl}/${resource.resourceType}/${resource.id}`;
384
+ const startTime = Date.now();
385
+ try {
386
+ const response = await this.httpRequest(url, {
387
+ method: 'PUT',
388
+ headers: {
389
+ ...await this.getAuthHeaders(),
390
+ 'Content-Type': 'application/fhir+json'
391
+ },
392
+ body: JSON.stringify(resource)
393
+ });
394
+ await this.logAudit({
395
+ action: 'update',
396
+ resourceType: resource.resourceType,
397
+ resourceId: resource.id,
398
+ outcome: 'success',
399
+ duration: Date.now() - startTime
400
+ });
401
+ return JSON.parse(response);
402
+ }
403
+ catch (error) {
404
+ await this.logAudit({
405
+ action: 'update',
406
+ resourceType: resource.resourceType,
407
+ resourceId: resource.id,
408
+ outcome: 'failure',
409
+ duration: Date.now() - startTime,
410
+ errorMessage: error instanceof Error ? error.message : 'Unknown error'
411
+ });
412
+ throw error;
413
+ }
414
+ }
415
+ // ═══════════════════════════════════════════════════════════════════════════════
416
+ // HELPER METHODS
417
+ // ═══════════════════════════════════════════════════════════════════════════════
418
+ async getAuthHeaders() {
419
+ // Check if token needs refresh
420
+ if (this.tokenExpiry && new Date() >= this.tokenExpiry) {
421
+ await this.authenticate();
422
+ }
423
+ const headers = {
424
+ 'Accept': 'application/fhir+json'
425
+ };
426
+ if (this.accessToken) {
427
+ headers['Authorization'] = `Bearer ${this.accessToken}`;
428
+ }
429
+ else if (this.config.authType === 'basic') {
430
+ headers['Authorization'] = `Basic ${Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64')}`;
431
+ }
432
+ return headers;
433
+ }
434
+ async httpRequest(url, options) {
435
+ // Use native fetch in Node.js 18+
436
+ const response = await fetch(url, {
437
+ method: options.method,
438
+ headers: options.headers,
439
+ body: options.body,
440
+ signal: AbortSignal.timeout(this.config.timeout || 30000)
441
+ });
442
+ if (!response.ok) {
443
+ const errorBody = await response.text();
444
+ throw new Error(`FHIR request failed: ${response.status} ${response.statusText} - ${errorBody}`);
445
+ }
446
+ return await response.text();
447
+ }
448
+ mapConditionToCancerDiagnosis(condition, patientId) {
449
+ const icdCoding = condition.code.coding?.find(c => c.system?.includes('icd-10') || c.system?.includes('icd-9'));
450
+ const snomedCoding = condition.code.coding?.find(c => c.system?.includes('snomed'));
451
+ const stageInfo = condition.stage?.[0];
452
+ let stage;
453
+ if (stageInfo?.summary) {
454
+ const stageCode = stageInfo.summary.coding?.[0]?.code || stageInfo.summary.text || '';
455
+ stage = {
456
+ system: this.determineStageSystem(stageCode),
457
+ stage: stageCode
458
+ };
459
+ }
460
+ return {
461
+ patientId,
462
+ conditionId: condition.id,
463
+ cancerType: condition.code.text || condition.code.coding?.[0]?.display || 'Unknown',
464
+ icdCode: icdCoding?.code || '',
465
+ snomedCode: snomedCoding?.code,
466
+ primarySite: condition.bodySite?.[0]?.text || condition.bodySite?.[0]?.coding?.[0]?.display,
467
+ stage,
468
+ diagnosisDate: new Date(condition.onsetDateTime || condition.recordedDate || Date.now()),
469
+ verificationStatus: this.mapVerificationStatus(condition.verificationStatus)
470
+ };
471
+ }
472
+ determineStageSystem(stageCode) {
473
+ const code = stageCode.toUpperCase();
474
+ if (code.includes('AJCC') || /^[IV]+[ABC]?$/.test(code) || code.includes('TNM')) {
475
+ return 'ajcc';
476
+ }
477
+ if (code.includes('FIGO'))
478
+ return 'figo';
479
+ if (code.includes('RAI'))
480
+ return 'rai';
481
+ if (code.includes('BINET'))
482
+ return 'binet';
483
+ if (code.includes('ISS'))
484
+ return 'iss';
485
+ return 'other';
486
+ }
487
+ mapVerificationStatus(status) {
488
+ const code = status.coding?.[0]?.code?.toLowerCase() || '';
489
+ if (code.includes('confirmed'))
490
+ return 'confirmed';
491
+ if (code.includes('provisional'))
492
+ return 'provisional';
493
+ if (code.includes('differential'))
494
+ return 'differential';
495
+ if (code.includes('refuted'))
496
+ return 'refuted';
497
+ return 'confirmed';
498
+ }
499
+ aggregateBiomarkerData(patientId, labBundle, genomicBundle, reportBundle) {
500
+ const biomarkers = [];
501
+ const genomicAlterations = [];
502
+ let latestDate = new Date(0);
503
+ // Process lab observations
504
+ for (const entry of labBundle.entry || []) {
505
+ const obs = entry.resource;
506
+ if (obs.resourceType !== 'Observation')
507
+ continue;
508
+ const date = new Date(obs.effectiveDateTime || Date.now());
509
+ if (date > latestDate)
510
+ latestDate = date;
511
+ // Map common cancer biomarkers
512
+ const biomarker = this.mapObservationToBiomarker(obs);
513
+ if (biomarker)
514
+ biomarkers.push(biomarker);
515
+ }
516
+ // Process genomic observations
517
+ for (const entry of genomicBundle.entry || []) {
518
+ const obs = entry.resource;
519
+ if (obs.resourceType !== 'Observation')
520
+ continue;
521
+ const date = new Date(obs.effectiveDateTime || Date.now());
522
+ if (date > latestDate)
523
+ latestDate = date;
524
+ const alteration = this.mapObservationToGenomicAlteration(obs);
525
+ if (alteration)
526
+ genomicAlterations.push(alteration);
527
+ }
528
+ // Process diagnostic reports for additional genomic data
529
+ for (const entry of reportBundle.entry || []) {
530
+ const report = entry.resource;
531
+ if (report.resourceType !== 'DiagnosticReport')
532
+ continue;
533
+ const date = new Date(report.effectiveDateTime || Date.now());
534
+ if (date > latestDate)
535
+ latestDate = date;
536
+ }
537
+ if (biomarkers.length === 0 && genomicAlterations.length === 0) {
538
+ return null;
539
+ }
540
+ // Extract special biomarkers
541
+ const tmbObs = this.findBiomarker(biomarkers, ['TMB', 'tumor mutational burden']);
542
+ const msiObs = this.findBiomarker(biomarkers, ['MSI', 'microsatellite']);
543
+ const pdl1Obs = this.findBiomarker(biomarkers, ['PD-L1', 'PDL1']);
544
+ const hrdObs = this.findBiomarker(biomarkers, ['HRD', 'homologous recombination']);
545
+ return {
546
+ patientId,
547
+ collectionDate: latestDate,
548
+ biomarkers,
549
+ genomicAlterations: genomicAlterations.length > 0 ? genomicAlterations : undefined,
550
+ tumorMutationalBurden: tmbObs ? {
551
+ value: typeof tmbObs.value === 'number' ? tmbObs.value : parseFloat(String(tmbObs.value)) || 0,
552
+ unit: 'mutations/Mb',
553
+ status: this.categorizeTMB(tmbObs.value)
554
+ } : undefined,
555
+ microsatelliteInstability: msiObs ? {
556
+ status: this.categorizeMSI(msiObs.value),
557
+ method: 'NGS'
558
+ } : undefined,
559
+ pdl1Expression: pdl1Obs ? {
560
+ score: typeof pdl1Obs.value === 'number' ? pdl1Obs.value : parseFloat(String(pdl1Obs.value)) || 0,
561
+ scoreType: 'TPS'
562
+ } : undefined,
563
+ hrdStatus: hrdObs ? {
564
+ status: hrdObs.status === 'positive' ? 'positive' : 'negative',
565
+ score: typeof hrdObs.value === 'number' ? hrdObs.value : undefined
566
+ } : undefined
567
+ };
568
+ }
569
+ mapObservationToBiomarker(obs) {
570
+ const name = obs.code.text || obs.code.coding?.[0]?.display;
571
+ if (!name)
572
+ return null;
573
+ let value;
574
+ let status;
575
+ if (obs.valueQuantity?.value !== undefined) {
576
+ value = obs.valueQuantity.value;
577
+ status = 'positive'; // Will be refined based on interpretation
578
+ }
579
+ else if (obs.valueCodeableConcept) {
580
+ value = obs.valueCodeableConcept.text || obs.valueCodeableConcept.coding?.[0]?.display || '';
581
+ status = this.interpretBiomarkerStatus(value);
582
+ }
583
+ else if (obs.valueString) {
584
+ value = obs.valueString;
585
+ status = this.interpretBiomarkerStatus(value);
586
+ }
587
+ else {
588
+ return null;
589
+ }
590
+ // Check interpretation if available
591
+ if (obs.interpretation?.[0]?.coding?.[0]?.code) {
592
+ const interpCode = obs.interpretation[0].coding[0].code;
593
+ if (interpCode === 'POS' || interpCode === 'H')
594
+ status = 'positive';
595
+ else if (interpCode === 'NEG' || interpCode === 'N')
596
+ status = 'negative';
597
+ else if (interpCode === 'IND')
598
+ status = 'equivocal';
599
+ }
600
+ return {
601
+ name,
602
+ value,
603
+ unit: obs.valueQuantity?.unit,
604
+ status,
605
+ loincCode: obs.code.coding?.find(c => c.system?.includes('loinc'))?.code
606
+ };
607
+ }
608
+ mapObservationToGenomicAlteration(obs) {
609
+ // This would need to be expanded to handle the full mCode/genomics-reporting IG
610
+ const geneComponent = obs.component?.find(c => c.code.coding?.some(coding => coding.code === '48018-6') // Gene studied
611
+ );
612
+ const variantComponent = obs.component?.find(c => c.code.coding?.some(coding => coding.code === '81252-9') // DNA change
613
+ );
614
+ if (!geneComponent || !variantComponent)
615
+ return null;
616
+ const gene = geneComponent.valueCodeableConcept?.text ||
617
+ geneComponent.valueCodeableConcept?.coding?.[0]?.display || '';
618
+ const alteration = variantComponent.valueString ||
619
+ variantComponent.valueCodeableConcept?.text || '';
620
+ if (!gene || !alteration)
621
+ return null;
622
+ return {
623
+ gene,
624
+ alteration,
625
+ type: this.determineAlterationType(alteration)
626
+ };
627
+ }
628
+ determineAlterationType(alteration) {
629
+ const lower = alteration.toLowerCase();
630
+ if (lower.includes('amp') || lower.includes('gain'))
631
+ return 'amplification';
632
+ if (lower.includes('del') || lower.includes('loss'))
633
+ return 'deletion';
634
+ if (lower.includes('fusion') || lower.includes('::'))
635
+ return 'fusion';
636
+ if (lower.includes('rearr') || lower.includes('transloc'))
637
+ return 'rearrangement';
638
+ return 'mutation';
639
+ }
640
+ interpretBiomarkerStatus(value) {
641
+ const lower = String(value).toLowerCase();
642
+ if (lower.includes('positive') || lower.includes('detected') || lower === 'yes')
643
+ return 'positive';
644
+ if (lower.includes('negative') || lower.includes('not detected') || lower === 'no')
645
+ return 'negative';
646
+ if (lower.includes('equivocal') || lower.includes('indeterminate') || lower.includes('borderline'))
647
+ return 'equivocal';
648
+ return 'positive'; // Default to positive if we have a value
649
+ }
650
+ findBiomarker(biomarkers, keywords) {
651
+ return biomarkers.find(b => keywords.some(k => b.name.toLowerCase().includes(k.toLowerCase())));
652
+ }
653
+ categorizeTMB(value) {
654
+ const numValue = typeof value === 'number' ? value : parseFloat(String(value));
655
+ if (isNaN(numValue))
656
+ return 'low';
657
+ if (numValue >= 10)
658
+ return 'high';
659
+ if (numValue >= 6)
660
+ return 'intermediate';
661
+ return 'low';
662
+ }
663
+ categorizeMSI(value) {
664
+ const strValue = String(value).toUpperCase();
665
+ if (strValue.includes('MSI-H') || strValue.includes('HIGH') || strValue.includes('UNSTABLE'))
666
+ return 'MSI-H';
667
+ if (strValue.includes('MSI-L') || strValue.includes('LOW'))
668
+ return 'MSI-L';
669
+ return 'MSS';
670
+ }
671
+ aggregateTreatmentHistory(patientId, medications, procedures) {
672
+ const treatments = [];
673
+ // Process medications
674
+ for (const entry of medications.entry || []) {
675
+ const med = entry.resource;
676
+ if (med.resourceType !== 'MedicationRequest')
677
+ continue;
678
+ const drugName = med.medicationCodeableConcept?.text ||
679
+ med.medicationCodeableConcept?.coding?.[0]?.display ||
680
+ 'Unknown medication';
681
+ const treatment = this.categorizeTreatment(drugName);
682
+ treatments.push({
683
+ id: med.id,
684
+ type: treatment.type,
685
+ regimen: treatment.regimen,
686
+ drugs: [{
687
+ name: drugName,
688
+ dose: med.dosageInstruction?.[0]?.text,
689
+ route: med.dosageInstruction?.[0]?.route?.text,
690
+ rxNormCode: med.medicationCodeableConcept?.coding?.find(c => c.system?.includes('rxnorm'))?.code
691
+ }],
692
+ startDate: new Date(med.authoredOn || Date.now()),
693
+ status: this.mapMedicationStatus(med.status)
694
+ });
695
+ }
696
+ // Process procedures (surgery, radiation)
697
+ for (const entry of procedures.entry || []) {
698
+ const proc = entry.resource;
699
+ if (proc.resourceType !== 'Procedure')
700
+ continue;
701
+ const procName = proc.code.text || proc.code.coding?.[0]?.display || 'Unknown procedure';
702
+ const isSurgery = this.isSurgicalProcedure(procName);
703
+ const isRadiation = this.isRadiationProcedure(procName);
704
+ if (isSurgery || isRadiation) {
705
+ treatments.push({
706
+ id: proc.id,
707
+ type: isRadiation ? 'radiation' : 'surgery',
708
+ drugs: [],
709
+ startDate: new Date(proc.performedDateTime || proc.performedPeriod?.start || Date.now()),
710
+ endDate: proc.performedPeriod?.end ? new Date(proc.performedPeriod.end) : undefined,
711
+ status: this.mapProcedureStatus(proc.status)
712
+ });
713
+ }
714
+ }
715
+ // Sort by date
716
+ treatments.sort((a, b) => b.startDate.getTime() - a.startDate.getTime());
717
+ return { patientId, treatments };
718
+ }
719
+ categorizeTreatment(drugName) {
720
+ const lower = drugName.toLowerCase();
721
+ // Immunotherapy
722
+ const immunotherapyDrugs = ['pembrolizumab', 'nivolumab', 'ipilimumab', 'atezolizumab', 'durvalumab',
723
+ 'avelumab', 'cemiplimab', 'dostarlimab', 'relatlimab', 'tremelimumab'];
724
+ if (immunotherapyDrugs.some(d => lower.includes(d))) {
725
+ return { type: 'immunotherapy' };
726
+ }
727
+ // Targeted therapy
728
+ const targetedDrugs = ['imatinib', 'erlotinib', 'gefitinib', 'osimertinib', 'crizotinib', 'alectinib',
729
+ 'palbociclib', 'ribociclib', 'abemaciclib', 'olaparib', 'rucaparib', 'niraparib', 'trastuzumab',
730
+ 'pertuzumab', 'lapatinib', 'vemurafenib', 'dabrafenib', 'trametinib', 'sotorasib', 'adagrasib',
731
+ 'venetoclax', 'ibrutinib', 'acalabrutinib', 'lenvatinib', 'sorafenib', 'regorafenib', 'cabozantinib',
732
+ 'bevacizumab', 'cetuximab', 'panitumumab', 'rituximab'];
733
+ if (targetedDrugs.some(d => lower.includes(d))) {
734
+ return { type: 'targeted-therapy' };
735
+ }
736
+ // Hormone therapy
737
+ const hormoneDrugs = ['tamoxifen', 'letrozole', 'anastrozole', 'exemestane', 'fulvestrant',
738
+ 'enzalutamide', 'abiraterone', 'apalutamide', 'darolutamide', 'lupron', 'leuprolide'];
739
+ if (hormoneDrugs.some(d => lower.includes(d))) {
740
+ return { type: 'hormone-therapy' };
741
+ }
742
+ // CAR-T
743
+ const carTDrugs = ['tisagenlecleucel', 'axicabtagene', 'brexucabtagene', 'lisocabtagene',
744
+ 'idecabtagene', 'ciltacabtagene', 'kymriah', 'yescarta', 'tecartus', 'breyanzi', 'abecma', 'carvykti'];
745
+ if (carTDrugs.some(d => lower.includes(d))) {
746
+ return { type: 'car-t' };
747
+ }
748
+ // Chemotherapy (catch-all for cytotoxic agents)
749
+ const chemoDrugs = ['carboplatin', 'cisplatin', 'oxaliplatin', 'paclitaxel', 'docetaxel',
750
+ 'doxorubicin', 'epirubicin', 'cyclophosphamide', 'fluorouracil', '5-fu', 'capecitabine',
751
+ 'gemcitabine', 'pemetrexed', 'etoposide', 'irinotecan', 'vincristine', 'vinblastine',
752
+ 'methotrexate', 'cytarabine', 'azacitidine', 'decitabine', 'temozolomide'];
753
+ if (chemoDrugs.some(d => lower.includes(d))) {
754
+ return { type: 'chemotherapy' };
755
+ }
756
+ return { type: 'other' };
757
+ }
758
+ mapMedicationStatus(status) {
759
+ switch (status) {
760
+ case 'active': return 'active';
761
+ case 'completed': return 'completed';
762
+ case 'stopped': return 'stopped';
763
+ case 'on-hold': return 'on-hold';
764
+ case 'draft': return 'planned';
765
+ default: return 'active';
766
+ }
767
+ }
768
+ mapProcedureStatus(status) {
769
+ switch (status) {
770
+ case 'completed': return 'completed';
771
+ case 'in-progress': return 'active';
772
+ case 'preparation': return 'planned';
773
+ case 'on-hold': return 'on-hold';
774
+ case 'stopped': return 'stopped';
775
+ default: return 'completed';
776
+ }
777
+ }
778
+ isSurgicalProcedure(name) {
779
+ const surgeryKeywords = ['surgery', 'resection', 'excision', 'mastectomy', 'lobectomy',
780
+ 'colectomy', 'gastrectomy', 'prostatectomy', 'nephrectomy', 'hysterectomy',
781
+ 'lymphadenectomy', 'biopsy', 'debulking', 'whipple', 'hepatectomy'];
782
+ return surgeryKeywords.some(k => name.toLowerCase().includes(k));
783
+ }
784
+ isRadiationProcedure(name) {
785
+ const radiationKeywords = ['radiation', 'radiotherapy', 'sbrt', 'imrt', 'proton',
786
+ 'brachytherapy', 'cyberknife', 'gamma knife', 'external beam'];
787
+ return radiationKeywords.some(k => name.toLowerCase().includes(k));
788
+ }
789
+ async logAudit(event) {
790
+ if (!this.auditLogger)
791
+ return;
792
+ const fullEvent = {
793
+ timestamp: new Date(),
794
+ userId: 'system',
795
+ ipAddress: '0.0.0.0',
796
+ action: event.action || 'unknown',
797
+ resourceType: event.resourceType || 'unknown',
798
+ outcome: event.outcome || 'unknown',
799
+ ...event
800
+ };
801
+ try {
802
+ await this.auditLogger(fullEvent);
803
+ }
804
+ catch (error) {
805
+ console.error('Failed to log audit event:', error);
806
+ }
807
+ }
808
+ }
809
+ // ═══════════════════════════════════════════════════════════════════════════════
810
+ // EHR VENDOR-SPECIFIC ADAPTERS
811
+ // ═══════════════════════════════════════════════════════════════════════════════
812
+ export class EpicFHIRClient extends FHIRClient {
813
+ constructor(config) {
814
+ super({ ...config, ehrVendor: 'epic' });
815
+ }
816
+ /**
817
+ * Epic-specific: Get MyChart patient context
818
+ */
819
+ async getMyChartContext(launchToken) {
820
+ // Epic SMART launch context parsing
821
+ const decoded = Buffer.from(launchToken, 'base64').toString();
822
+ return JSON.parse(decoded);
823
+ }
824
+ }
825
+ export class CernerFHIRClient extends FHIRClient {
826
+ constructor(config) {
827
+ super({ ...config, ehrVendor: 'cerner' });
828
+ }
829
+ /**
830
+ * Cerner-specific: Handle Millennium-specific extensions
831
+ */
832
+ parseMillenniumExtensions(resource) {
833
+ const extensions = {};
834
+ if ('extension' in resource && resource.extension) {
835
+ for (const ext of resource.extension) {
836
+ if (ext.url.includes('cerner.com')) {
837
+ const key = ext.url.split('/').pop() || ext.url;
838
+ extensions[key] = ext.valueString || ext.valueCode || ext.valueBoolean || ext.valueCoding;
839
+ }
840
+ }
841
+ }
842
+ return extensions;
843
+ }
844
+ }
845
+ // ═══════════════════════════════════════════════════════════════════════════════
846
+ // FACTORY FUNCTION
847
+ // ═══════════════════════════════════════════════════════════════════════════════
848
+ export function createFHIRClient(config) {
849
+ switch (config.ehrVendor) {
850
+ case 'epic':
851
+ return new EpicFHIRClient(config);
852
+ case 'cerner':
853
+ return new CernerFHIRClient(config);
854
+ default:
855
+ return new FHIRClient(config);
856
+ }
857
+ }
858
+ export default FHIRClient;
859
+ //# sourceMappingURL=fhir.js.map