@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,808 @@
1
+ /**
2
+ * ClinicalTrials.gov Integration
3
+ *
4
+ * Provides real-time access to clinical trial data from ClinicalTrials.gov
5
+ * using the official API v2. Enables matching patients to eligible trials
6
+ * based on their cancer type, biomarkers, and treatment history.
7
+ */
8
+ import { EventEmitter } from 'events';
9
+ // ═══════════════════════════════════════════════════════════════════════════════
10
+ // CLINICAL TRIALS API CLIENT
11
+ // ═══════════════════════════════════════════════════════════════════════════════
12
+ export class ClinicalTrialsGovClient extends EventEmitter {
13
+ baseUrl = 'https://clinicaltrials.gov/api/v2';
14
+ timeout;
15
+ cache = new Map();
16
+ cacheDuration; // minutes
17
+ constructor(options) {
18
+ super();
19
+ this.timeout = options?.timeout || 30000;
20
+ this.cacheDuration = options?.cacheDuration || 60; // 1 hour default
21
+ }
22
+ /**
23
+ * Search for clinical trials
24
+ */
25
+ async searchTrials(params) {
26
+ const cacheKey = JSON.stringify(params);
27
+ const cached = this.getFromCache(cacheKey);
28
+ if (cached)
29
+ return cached;
30
+ const query = this.buildSearchQuery(params);
31
+ const url = `${this.baseUrl}/studies?${query}`;
32
+ const response = await this.httpRequest(url);
33
+ const data = JSON.parse(response);
34
+ const result = {
35
+ trials: (data.studies || []).map((s) => this.mapStudyToTrial(s)),
36
+ totalCount: data.totalCount || 0,
37
+ nextPageToken: data.nextPageToken
38
+ };
39
+ this.setCache(cacheKey, result);
40
+ return result;
41
+ }
42
+ /**
43
+ * Get a specific trial by NCT ID
44
+ */
45
+ async getTrial(nctId) {
46
+ const cacheKey = `trial:${nctId}`;
47
+ const cached = this.getFromCache(cacheKey);
48
+ if (cached)
49
+ return cached;
50
+ const url = `${this.baseUrl}/studies/${nctId}`;
51
+ const response = await this.httpRequest(url);
52
+ const data = JSON.parse(response);
53
+ const trial = this.mapStudyToTrial(data);
54
+ this.setCache(cacheKey, trial);
55
+ return trial;
56
+ }
57
+ /**
58
+ * Get multiple trials by NCT IDs
59
+ */
60
+ async getTrials(nctIds) {
61
+ return Promise.all(nctIds.map(id => this.getTrial(id)));
62
+ }
63
+ /**
64
+ * Search for trials matching a patient profile
65
+ */
66
+ async findMatchingTrials(patient) {
67
+ // Build search query based on patient profile
68
+ const searchParams = {
69
+ condition: patient.cancerType,
70
+ status: ['recruiting', 'enrolling-by-invitation', 'not-yet-recruiting'],
71
+ studyType: 'interventional',
72
+ age: patient.age,
73
+ gender: patient.gender,
74
+ pageSize: 100
75
+ };
76
+ // Add location-based filtering
77
+ if (patient.location?.zipCode) {
78
+ searchParams.zipCode = patient.location.zipCode;
79
+ searchParams.distance = patient.maxTravelDistance || 100;
80
+ }
81
+ else if (patient.location?.country) {
82
+ searchParams.country = patient.location.country;
83
+ if (patient.location.state) {
84
+ searchParams.state = patient.location.state;
85
+ }
86
+ }
87
+ // Add biomarker-specific searches
88
+ if (patient.genomicAlterations && patient.genomicAlterations.length > 0) {
89
+ searchParams.genomicAlterations = patient.genomicAlterations.map(g => `${g.gene} ${g.alteration}`);
90
+ }
91
+ // Execute search
92
+ const searchResult = await this.searchTrials(searchParams);
93
+ // Score and filter trials
94
+ const matches = [];
95
+ for (const trial of searchResult.trials) {
96
+ const match = this.assessTrialMatch(trial, patient);
97
+ if (match.matchScore > 0) {
98
+ matches.push(match);
99
+ }
100
+ }
101
+ // Sort by match score
102
+ matches.sort((a, b) => b.matchScore - a.matchScore);
103
+ return matches;
104
+ }
105
+ /**
106
+ * Search for trials by biomarker
107
+ */
108
+ async searchByBiomarker(biomarker, options) {
109
+ const params = {
110
+ biomarkers: [biomarker],
111
+ condition: options?.cancerType,
112
+ status: options?.status || ['recruiting', 'not-yet-recruiting'],
113
+ phase: options?.phase,
114
+ studyType: 'interventional',
115
+ pageSize: 50
116
+ };
117
+ const result = await this.searchTrials(params);
118
+ return result.trials;
119
+ }
120
+ /**
121
+ * Search for trials by drug/intervention
122
+ */
123
+ async searchByDrug(drugName, options) {
124
+ const params = {
125
+ drugName,
126
+ condition: options?.cancerType,
127
+ status: options?.status || ['recruiting', 'not-yet-recruiting'],
128
+ phase: options?.phase,
129
+ studyType: 'interventional',
130
+ pageSize: 50
131
+ };
132
+ const result = await this.searchTrials(params);
133
+ return result.trials;
134
+ }
135
+ /**
136
+ * Get trials for a specific cancer type
137
+ */
138
+ async getTrialsForCancerType(cancerType, options) {
139
+ const params = {
140
+ condition: cancerType,
141
+ status: ['recruiting', 'not-yet-recruiting', 'enrolling-by-invitation'],
142
+ studyType: 'interventional',
143
+ phase: options?.phase,
144
+ country: options?.location?.country,
145
+ state: options?.location?.state,
146
+ biomarkers: options?.biomarkers,
147
+ pageSize: 100
148
+ };
149
+ const result = await this.searchTrials(params);
150
+ return result.trials;
151
+ }
152
+ // ═══════════════════════════════════════════════════════════════════════════════
153
+ // HELPER METHODS
154
+ // ═══════════════════════════════════════════════════════════════════════════════
155
+ buildSearchQuery(params) {
156
+ const queryParts = [];
157
+ // Condition/disease
158
+ if (params.condition) {
159
+ queryParts.push(`query.cond=${encodeURIComponent(params.condition)}`);
160
+ }
161
+ if (params.conditionTerms && params.conditionTerms.length > 0) {
162
+ queryParts.push(`query.term=${encodeURIComponent(params.conditionTerms.join(' OR '))}`);
163
+ }
164
+ // Intervention/drug
165
+ if (params.intervention) {
166
+ queryParts.push(`query.intr=${encodeURIComponent(params.intervention)}`);
167
+ }
168
+ if (params.drugName) {
169
+ queryParts.push(`query.intr=${encodeURIComponent(params.drugName)}`);
170
+ }
171
+ // Biomarkers/genomic alterations (search in full text)
172
+ if (params.biomarkers && params.biomarkers.length > 0) {
173
+ const biomarkerQuery = params.biomarkers.join(' OR ');
174
+ queryParts.push(`query.term=${encodeURIComponent(biomarkerQuery)}`);
175
+ }
176
+ if (params.genomicAlterations && params.genomicAlterations.length > 0) {
177
+ const genomicQuery = params.genomicAlterations.join(' OR ');
178
+ queryParts.push(`query.term=${encodeURIComponent(genomicQuery)}`);
179
+ }
180
+ // Status filter
181
+ if (params.status && params.status.length > 0) {
182
+ const statusMap = {
183
+ 'not-yet-recruiting': 'NOT_YET_RECRUITING',
184
+ 'recruiting': 'RECRUITING',
185
+ 'enrolling-by-invitation': 'ENROLLING_BY_INVITATION',
186
+ 'active-not-recruiting': 'ACTIVE_NOT_RECRUITING',
187
+ 'suspended': 'SUSPENDED',
188
+ 'terminated': 'TERMINATED',
189
+ 'completed': 'COMPLETED',
190
+ 'withdrawn': 'WITHDRAWN',
191
+ 'unknown': 'UNKNOWN'
192
+ };
193
+ const statuses = params.status.map(s => statusMap[s]).join(',');
194
+ queryParts.push(`filter.overallStatus=${statuses}`);
195
+ }
196
+ // Phase filter
197
+ if (params.phase && params.phase.length > 0) {
198
+ const phaseMap = {
199
+ 'early-phase-1': 'EARLY_PHASE1',
200
+ 'phase-1': 'PHASE1',
201
+ 'phase-1-2': 'PHASE1_PHASE2',
202
+ 'phase-2': 'PHASE2',
203
+ 'phase-2-3': 'PHASE2_PHASE3',
204
+ 'phase-3': 'PHASE3',
205
+ 'phase-4': 'PHASE4',
206
+ 'not-applicable': 'NA'
207
+ };
208
+ const phases = params.phase.map(p => phaseMap[p]).join(',');
209
+ queryParts.push(`filter.phase=${phases}`);
210
+ }
211
+ // Study type
212
+ if (params.studyType) {
213
+ const studyTypeMap = {
214
+ 'interventional': 'INTERVENTIONAL',
215
+ 'observational': 'OBSERVATIONAL',
216
+ 'expanded-access': 'EXPANDED_ACCESS'
217
+ };
218
+ queryParts.push(`filter.studyType=${studyTypeMap[params.studyType]}`);
219
+ }
220
+ // Location filters
221
+ if (params.country) {
222
+ queryParts.push(`query.locn=${encodeURIComponent(params.country)}`);
223
+ }
224
+ if (params.state) {
225
+ queryParts.push(`query.locn=${encodeURIComponent(params.state)}`);
226
+ }
227
+ if (params.city) {
228
+ queryParts.push(`query.locn=${encodeURIComponent(params.city)}`);
229
+ }
230
+ // Geographic search (if zip code and distance provided)
231
+ if (params.zipCode && params.distance) {
232
+ queryParts.push(`postFilter.geo=distance(${params.zipCode},${params.distance}mi)`);
233
+ }
234
+ // Age filter
235
+ if (params.age) {
236
+ queryParts.push(`aggFilters=ages:adult`); // Simplified - would need more logic
237
+ }
238
+ // Gender filter
239
+ if (params.gender) {
240
+ queryParts.push(`filter.sex=${params.gender.toUpperCase()}`);
241
+ }
242
+ // Results filter
243
+ if (params.hasResults !== undefined) {
244
+ queryParts.push(`filter.results=${params.hasResults}`);
245
+ }
246
+ // Pagination
247
+ queryParts.push(`pageSize=${params.pageSize || 20}`);
248
+ if (params.pageToken) {
249
+ queryParts.push(`pageToken=${params.pageToken}`);
250
+ }
251
+ // Request all needed fields
252
+ queryParts.push('fields=NCTId,BriefTitle,OfficialTitle,OverallStatus,Phase,StudyType,Condition,' +
253
+ 'InterventionName,InterventionType,InterventionDescription,EligibilityCriteria,' +
254
+ 'MinimumAge,MaximumAge,Gender,LocationFacility,LocationCity,LocationState,LocationCountry,' +
255
+ 'LeadSponsorName,StartDate,PrimaryCompletionDate,EnrollmentCount,ArmGroupLabel,' +
256
+ 'ArmGroupType,PrimaryOutcomeMeasure,SecondaryOutcomeMeasure,CentralContactName,' +
257
+ 'CentralContactPhone,CentralContactEMail,BriefSummary,DetailedDescription');
258
+ return queryParts.join('&');
259
+ }
260
+ mapStudyToTrial(study) {
261
+ const protocol = study.protocolSection || study;
262
+ const identification = protocol.identificationModule || {};
263
+ const status = protocol.statusModule || {};
264
+ const description = protocol.descriptionModule || {};
265
+ const conditions = protocol.conditionsModule || {};
266
+ const design = protocol.designModule || {};
267
+ const arms = protocol.armsInterventionsModule || {};
268
+ const eligibility = protocol.eligibilityModule || {};
269
+ const contacts = protocol.contactsLocationsModule || {};
270
+ const sponsor = protocol.sponsorCollaboratorsModule || {};
271
+ const outcomes = protocol.outcomesModule || {};
272
+ // Map interventions
273
+ const interventions = (arms.interventions || []).map((i) => ({
274
+ type: this.mapInterventionType(i.type),
275
+ name: i.name,
276
+ description: i.description,
277
+ armGroupLabels: i.armGroupLabels,
278
+ otherNames: i.otherNames
279
+ }));
280
+ // Map locations
281
+ const locations = (contacts.locations || []).map((loc) => ({
282
+ facility: loc.facility,
283
+ city: loc.city,
284
+ state: loc.state,
285
+ country: loc.country,
286
+ zip: loc.zip,
287
+ status: this.mapLocationStatus(loc.status),
288
+ contact: loc.contacts?.[0] ? {
289
+ name: loc.contacts[0].name,
290
+ phone: loc.contacts[0].phone,
291
+ email: loc.contacts[0].email
292
+ } : undefined,
293
+ coordinates: loc.geoPoint ? {
294
+ latitude: loc.geoPoint.lat,
295
+ longitude: loc.geoPoint.lon
296
+ } : undefined
297
+ }));
298
+ // Map sponsors
299
+ const sponsors = [];
300
+ if (sponsor.leadSponsor) {
301
+ sponsors.push({
302
+ name: sponsor.leadSponsor.name,
303
+ type: 'sponsor',
304
+ leadOrCollaborator: 'lead'
305
+ });
306
+ }
307
+ for (const collab of sponsor.collaborators || []) {
308
+ sponsors.push({
309
+ name: collab.name,
310
+ type: 'sponsor',
311
+ leadOrCollaborator: 'collaborator'
312
+ });
313
+ }
314
+ // Map contacts
315
+ const trialContacts = (contacts.centralContacts || []).map((c) => ({
316
+ name: c.name,
317
+ phone: c.phone,
318
+ email: c.email,
319
+ role: c.role
320
+ }));
321
+ // Map arms
322
+ const trialArms = (arms.armGroups || []).map((arm) => ({
323
+ label: arm.label,
324
+ type: this.mapArmType(arm.type),
325
+ description: arm.description,
326
+ interventions: arm.interventionNames
327
+ }));
328
+ // Map outcomes
329
+ const trialOutcomes = [
330
+ ...(outcomes.primaryOutcomes || []).map((o) => ({
331
+ type: 'primary',
332
+ measure: o.measure,
333
+ description: o.description,
334
+ timeFrame: o.timeFrame
335
+ })),
336
+ ...(outcomes.secondaryOutcomes || []).map((o) => ({
337
+ type: 'secondary',
338
+ measure: o.measure,
339
+ description: o.description,
340
+ timeFrame: o.timeFrame
341
+ }))
342
+ ];
343
+ // Parse eligibility criteria into structured format
344
+ const criteriaText = eligibility.eligibilityCriteria || '';
345
+ const { inclusion, exclusion } = this.parseEligibilityCriteria(criteriaText);
346
+ // Extract biomarker requirements from criteria
347
+ const biomarkerRequirements = this.extractBiomarkerRequirements(criteriaText, interventions);
348
+ return {
349
+ nctId: identification.nctId,
350
+ title: identification.briefTitle || identification.officialTitle || '',
351
+ briefTitle: identification.briefTitle,
352
+ officialTitle: identification.officialTitle,
353
+ status: this.mapTrialStatus(status.overallStatus),
354
+ phase: this.mapTrialPhase(design.phases?.[0]),
355
+ studyType: this.mapStudyType(design.studyType),
356
+ conditions: conditions.conditions || [],
357
+ interventions,
358
+ eligibility: {
359
+ criteria: criteriaText,
360
+ gender: this.mapGender(eligibility.sex),
361
+ minimumAge: eligibility.minimumAge,
362
+ maximumAge: eligibility.maximumAge,
363
+ healthyVolunteers: eligibility.healthyVolunteers === 'Yes',
364
+ inclusionCriteria: inclusion,
365
+ exclusionCriteria: exclusion
366
+ },
367
+ locations,
368
+ sponsors,
369
+ contacts: trialContacts,
370
+ dates: {
371
+ startDate: status.startDateStruct ? new Date(status.startDateStruct.date) : undefined,
372
+ primaryCompletionDate: status.primaryCompletionDateStruct ? new Date(status.primaryCompletionDateStruct.date) : undefined,
373
+ completionDate: status.completionDateStruct ? new Date(status.completionDateStruct.date) : undefined,
374
+ firstPostedDate: status.studyFirstPostDateStruct ? new Date(status.studyFirstPostDateStruct.date) : undefined,
375
+ lastUpdatePostedDate: status.lastUpdatePostDateStruct ? new Date(status.lastUpdatePostDateStruct.date) : undefined
376
+ },
377
+ enrollment: design.enrollmentInfo ? {
378
+ count: design.enrollmentInfo.count,
379
+ type: design.enrollmentInfo.type?.toLowerCase()
380
+ } : undefined,
381
+ arms: trialArms.length > 0 ? trialArms : undefined,
382
+ outcomes: trialOutcomes.length > 0 ? trialOutcomes : undefined,
383
+ biomarkerRequirements: biomarkerRequirements.length > 0 ? biomarkerRequirements : undefined,
384
+ url: `https://clinicaltrials.gov/study/${identification.nctId}`
385
+ };
386
+ }
387
+ mapTrialStatus(status) {
388
+ const statusMap = {
389
+ 'NOT_YET_RECRUITING': 'not-yet-recruiting',
390
+ 'RECRUITING': 'recruiting',
391
+ 'ENROLLING_BY_INVITATION': 'enrolling-by-invitation',
392
+ 'ACTIVE_NOT_RECRUITING': 'active-not-recruiting',
393
+ 'SUSPENDED': 'suspended',
394
+ 'TERMINATED': 'terminated',
395
+ 'COMPLETED': 'completed',
396
+ 'WITHDRAWN': 'withdrawn'
397
+ };
398
+ return statusMap[status] || 'unknown';
399
+ }
400
+ mapTrialPhase(phase) {
401
+ const phaseMap = {
402
+ 'EARLY_PHASE1': 'early-phase-1',
403
+ 'PHASE1': 'phase-1',
404
+ 'PHASE1_PHASE2': 'phase-1-2',
405
+ 'PHASE2': 'phase-2',
406
+ 'PHASE2_PHASE3': 'phase-2-3',
407
+ 'PHASE3': 'phase-3',
408
+ 'PHASE4': 'phase-4',
409
+ 'NA': 'not-applicable'
410
+ };
411
+ return phaseMap[phase] || 'not-applicable';
412
+ }
413
+ mapStudyType(type) {
414
+ const typeMap = {
415
+ 'INTERVENTIONAL': 'interventional',
416
+ 'OBSERVATIONAL': 'observational',
417
+ 'EXPANDED_ACCESS': 'expanded-access'
418
+ };
419
+ return typeMap[type] || 'interventional';
420
+ }
421
+ mapInterventionType(type) {
422
+ const typeMap = {
423
+ 'DRUG': 'drug',
424
+ 'BIOLOGICAL': 'biological',
425
+ 'DEVICE': 'device',
426
+ 'PROCEDURE': 'procedure',
427
+ 'RADIATION': 'radiation',
428
+ 'BEHAVIORAL': 'behavioral',
429
+ 'GENETIC': 'genetic',
430
+ 'DIETARY_SUPPLEMENT': 'dietary',
431
+ 'COMBINATION_PRODUCT': 'combination',
432
+ 'OTHER': 'other'
433
+ };
434
+ return typeMap[type] || 'other';
435
+ }
436
+ mapLocationStatus(status) {
437
+ const statusMap = {
438
+ 'RECRUITING': 'recruiting',
439
+ 'NOT_YET_RECRUITING': 'not-recruiting',
440
+ 'ACTIVE_NOT_RECRUITING': 'active',
441
+ 'COMPLETED': 'completed',
442
+ 'WITHDRAWN': 'withdrawn'
443
+ };
444
+ return statusMap[status] || 'active';
445
+ }
446
+ mapArmType(type) {
447
+ const typeMap = {
448
+ 'EXPERIMENTAL': 'experimental',
449
+ 'ACTIVE_COMPARATOR': 'active-comparator',
450
+ 'PLACEBO_COMPARATOR': 'placebo-comparator',
451
+ 'SHAM_COMPARATOR': 'sham-comparator',
452
+ 'NO_INTERVENTION': 'no-intervention',
453
+ 'OTHER': 'other'
454
+ };
455
+ return typeMap[type] || 'other';
456
+ }
457
+ mapGender(sex) {
458
+ if (sex === 'FEMALE')
459
+ return 'female';
460
+ if (sex === 'MALE')
461
+ return 'male';
462
+ return 'all';
463
+ }
464
+ parseEligibilityCriteria(criteria) {
465
+ const inclusion = [];
466
+ const exclusion = [];
467
+ if (!criteria)
468
+ return { inclusion, exclusion };
469
+ // Try to split by Inclusion/Exclusion headers
470
+ const sections = criteria.split(/(?:Inclusion|Exclusion)\s*Criteria:?/i);
471
+ // Simple parsing - look for patterns
472
+ const lines = criteria.split(/\n|•|·|-\s+|\*\s+|\d+\.\s+/);
473
+ let inExclusion = false;
474
+ for (const line of lines) {
475
+ const trimmed = line.trim();
476
+ if (!trimmed)
477
+ continue;
478
+ // Detect section changes
479
+ if (trimmed.toLowerCase().includes('exclusion')) {
480
+ inExclusion = true;
481
+ continue;
482
+ }
483
+ if (trimmed.toLowerCase().includes('inclusion')) {
484
+ inExclusion = false;
485
+ continue;
486
+ }
487
+ // Add to appropriate list
488
+ if (trimmed.length > 10) { // Filter out very short fragments
489
+ if (inExclusion) {
490
+ exclusion.push(trimmed);
491
+ }
492
+ else {
493
+ inclusion.push(trimmed);
494
+ }
495
+ }
496
+ }
497
+ return { inclusion, exclusion };
498
+ }
499
+ extractBiomarkerRequirements(criteria, interventions) {
500
+ const requirements = [];
501
+ const criteriaLower = criteria.toLowerCase();
502
+ // Common biomarker patterns
503
+ const biomarkerPatterns = [
504
+ { pattern: /egfr\s*(mutation|mutant|positive|\+)/i, biomarker: 'EGFR mutation' },
505
+ { pattern: /egfr\s*(wild[- ]?type|negative|wt)/i, biomarker: 'EGFR wild-type' },
506
+ { pattern: /alk\s*(rearrangement|fusion|positive|\+)/i, biomarker: 'ALK fusion' },
507
+ { pattern: /ros1\s*(rearrangement|fusion|positive|\+)/i, biomarker: 'ROS1 fusion' },
508
+ { pattern: /braf\s*v600[ek]?/i, biomarker: 'BRAF V600' },
509
+ { pattern: /kras\s*g12c/i, biomarker: 'KRAS G12C' },
510
+ { pattern: /kras\s*(mutation|mutant|positive)/i, biomarker: 'KRAS mutation' },
511
+ { pattern: /her2\s*(positive|overexpression|amplification|\+|3\+)/i, biomarker: 'HER2 positive' },
512
+ { pattern: /her2\s*(negative|\-|0|1\+)/i, biomarker: 'HER2 negative' },
513
+ { pattern: /brca[12]?\s*(mutation|mutant|positive|pathogenic)/i, biomarker: 'BRCA mutation' },
514
+ { pattern: /msi[- ]?h(igh)?|microsatellite\s*instability[- ]?high/i, biomarker: 'MSI-H' },
515
+ { pattern: /mss|microsatellite\s*stable/i, biomarker: 'MSS' },
516
+ { pattern: /pd[- ]?l1\s*(positive|expression|tps|cps)/i, biomarker: 'PD-L1 positive' },
517
+ { pattern: /pd[- ]?l1\s*[\u2265>=]\s*(\d+)/i, biomarker: 'PD-L1' },
518
+ { pattern: /tmb[- ]?h(igh)?|tumor\s*mutational\s*burden[- ]?high/i, biomarker: 'TMB-H' },
519
+ { pattern: /hrd\s*(positive|deficient)/i, biomarker: 'HRD positive' },
520
+ { pattern: /ntrk\s*(fusion|rearrangement)/i, biomarker: 'NTRK fusion' },
521
+ { pattern: /ret\s*(fusion|rearrangement|mutation)/i, biomarker: 'RET alteration' },
522
+ { pattern: /met\s*(exon\s*14|amplification)/i, biomarker: 'MET alteration' },
523
+ { pattern: /fgfr[1234]?\s*(alteration|fusion|mutation|amplification)/i, biomarker: 'FGFR alteration' },
524
+ { pattern: /pik3ca\s*(mutation|mutant)/i, biomarker: 'PIK3CA mutation' },
525
+ { pattern: /idh[12]\s*(mutation|mutant)/i, biomarker: 'IDH mutation' }
526
+ ];
527
+ for (const { pattern, biomarker } of biomarkerPatterns) {
528
+ if (pattern.test(criteria)) {
529
+ // Determine if it's required or excluded based on context
530
+ const match = criteria.match(new RegExp(`.{0,50}${pattern.source}.{0,50}`, 'i'));
531
+ if (match) {
532
+ const context = match[0].toLowerCase();
533
+ let requirement = 'required';
534
+ if (context.includes('exclud') || context.includes('must not') ||
535
+ context.includes('no ') || context.includes('without') ||
536
+ context.includes('ineligible')) {
537
+ requirement = 'excluded';
538
+ }
539
+ requirements.push({
540
+ biomarker,
541
+ requirement
542
+ });
543
+ }
544
+ }
545
+ }
546
+ return requirements;
547
+ }
548
+ assessTrialMatch(trial, patient) {
549
+ let score = 0;
550
+ const matchReasons = [];
551
+ const matchingCriteria = [];
552
+ const potentialExclusions = [];
553
+ const missingInformation = [];
554
+ const biomarkerMatches = [];
555
+ // Check cancer type match
556
+ const cancerMatch = trial.conditions.some(c => c.toLowerCase().includes(patient.cancerType.toLowerCase()) ||
557
+ patient.cancerType.toLowerCase().includes(c.toLowerCase()));
558
+ if (cancerMatch) {
559
+ score += 30;
560
+ matchReasons.push(`Cancer type matches: ${patient.cancerType}`);
561
+ matchingCriteria.push('Cancer type');
562
+ }
563
+ // Check biomarker requirements
564
+ if (trial.biomarkerRequirements && patient.genomicAlterations) {
565
+ for (const req of trial.biomarkerRequirements) {
566
+ const patientHasBiomarker = patient.genomicAlterations.some(g => req.biomarker.toLowerCase().includes(g.gene.toLowerCase()) ||
567
+ req.biomarker.toLowerCase().includes(g.alteration.toLowerCase()));
568
+ if (req.requirement === 'required') {
569
+ if (patientHasBiomarker) {
570
+ score += 25;
571
+ matchReasons.push(`Has required biomarker: ${req.biomarker}`);
572
+ matchingCriteria.push(req.biomarker);
573
+ biomarkerMatches.push({
574
+ biomarker: req.biomarker,
575
+ trialRequirement: 'Required',
576
+ patientValue: 'Present',
577
+ match: true
578
+ });
579
+ }
580
+ else {
581
+ score -= 10;
582
+ potentialExclusions.push(`Missing required biomarker: ${req.biomarker}`);
583
+ biomarkerMatches.push({
584
+ biomarker: req.biomarker,
585
+ trialRequirement: 'Required',
586
+ patientValue: 'Not detected',
587
+ match: false
588
+ });
589
+ }
590
+ }
591
+ else if (req.requirement === 'excluded') {
592
+ if (patientHasBiomarker) {
593
+ score -= 50;
594
+ potentialExclusions.push(`Has excluded biomarker: ${req.biomarker}`);
595
+ biomarkerMatches.push({
596
+ biomarker: req.biomarker,
597
+ trialRequirement: 'Excluded',
598
+ patientValue: 'Present',
599
+ match: false
600
+ });
601
+ }
602
+ }
603
+ }
604
+ }
605
+ // Check MSI status
606
+ if (patient.msiStatus) {
607
+ const trialMentionsMSI = trial.eligibility.criteria.toLowerCase().includes('msi');
608
+ if (trialMentionsMSI) {
609
+ if (patient.msiStatus === 'MSI-H' && trial.eligibility.criteria.toLowerCase().includes('msi-h')) {
610
+ score += 20;
611
+ matchReasons.push('MSI-H status matches trial requirement');
612
+ matchingCriteria.push('MSI-H');
613
+ }
614
+ }
615
+ }
616
+ // Check TMB status
617
+ if (patient.tmbLevel === 'high') {
618
+ const trialMentionsTMB = trial.eligibility.criteria.toLowerCase().includes('tmb');
619
+ if (trialMentionsTMB) {
620
+ score += 15;
621
+ matchReasons.push('TMB-High may enhance eligibility');
622
+ matchingCriteria.push('TMB-H');
623
+ }
624
+ }
625
+ // Check age eligibility
626
+ if (patient.age) {
627
+ let ageEligible = true;
628
+ if (trial.eligibility.minimumAge) {
629
+ const minAge = parseInt(trial.eligibility.minimumAge);
630
+ if (!isNaN(minAge) && patient.age < minAge) {
631
+ ageEligible = false;
632
+ potentialExclusions.push(`Below minimum age (${trial.eligibility.minimumAge})`);
633
+ }
634
+ }
635
+ if (trial.eligibility.maximumAge && trial.eligibility.maximumAge !== 'N/A') {
636
+ const maxAge = parseInt(trial.eligibility.maximumAge);
637
+ if (!isNaN(maxAge) && patient.age > maxAge) {
638
+ ageEligible = false;
639
+ potentialExclusions.push(`Above maximum age (${trial.eligibility.maximumAge})`);
640
+ }
641
+ }
642
+ if (ageEligible) {
643
+ score += 5;
644
+ matchingCriteria.push('Age');
645
+ }
646
+ else {
647
+ score -= 30;
648
+ }
649
+ }
650
+ else {
651
+ missingInformation.push('Patient age');
652
+ }
653
+ // Check gender eligibility
654
+ if (patient.gender) {
655
+ if (trial.eligibility.gender === 'all' || trial.eligibility.gender === patient.gender) {
656
+ matchingCriteria.push('Gender');
657
+ }
658
+ else {
659
+ score -= 50;
660
+ potentialExclusions.push(`Trial only accepts ${trial.eligibility.gender} patients`);
661
+ }
662
+ }
663
+ // Check prior therapy exclusions
664
+ if (patient.priorTherapies && patient.priorTherapies.length > 0) {
665
+ const criteriaLower = trial.eligibility.criteria.toLowerCase();
666
+ for (const therapy of patient.priorTherapies) {
667
+ if (criteriaLower.includes(`no prior ${therapy.toLowerCase()}`) ||
668
+ criteriaLower.includes(`not received ${therapy.toLowerCase()}`)) {
669
+ potentialExclusions.push(`Prior ${therapy} may exclude patient`);
670
+ score -= 10;
671
+ }
672
+ }
673
+ }
674
+ // Check ECOG status
675
+ if (patient.ecogStatus !== undefined) {
676
+ const ecogMatch = trial.eligibility.criteria.match(/ecog\s*(?:performance\s*status)?\s*(?:of\s*)?(\d)(?:\s*(?:or|to|-)\s*(\d))?/i);
677
+ if (ecogMatch) {
678
+ const maxEcog = parseInt(ecogMatch[2] || ecogMatch[1]);
679
+ if (patient.ecogStatus <= maxEcog) {
680
+ matchingCriteria.push(`ECOG ${patient.ecogStatus}`);
681
+ }
682
+ else {
683
+ potentialExclusions.push(`ECOG ${patient.ecogStatus} may be too high (trial requires ≤${maxEcog})`);
684
+ score -= 20;
685
+ }
686
+ }
687
+ }
688
+ else {
689
+ missingInformation.push('ECOG performance status');
690
+ }
691
+ // Boost score for phase based on patient preference
692
+ if (trial.phase === 'phase-3') {
693
+ score += 10;
694
+ matchReasons.push('Phase 3 trial (more established efficacy data)');
695
+ }
696
+ else if (trial.phase === 'phase-2') {
697
+ score += 5;
698
+ }
699
+ // Find nearest location
700
+ let nearestLocation;
701
+ if (patient.location) {
702
+ const recruitingLocations = trial.locations.filter(l => l.status === 'recruiting');
703
+ if (patient.location.coordinates && recruitingLocations.some(l => l.coordinates)) {
704
+ // Calculate distances
705
+ for (const loc of recruitingLocations) {
706
+ if (loc.coordinates) {
707
+ loc.distance = this.calculateDistance(patient.location.coordinates.lat, patient.location.coordinates.lon, loc.coordinates.latitude, loc.coordinates.longitude);
708
+ }
709
+ }
710
+ recruitingLocations.sort((a, b) => (a.distance || 999999) - (b.distance || 999999));
711
+ nearestLocation = recruitingLocations[0];
712
+ if (nearestLocation?.distance && patient.maxTravelDistance) {
713
+ if (nearestLocation.distance <= patient.maxTravelDistance) {
714
+ score += 10;
715
+ matchReasons.push(`Trial site within ${Math.round(nearestLocation.distance)} miles`);
716
+ }
717
+ else {
718
+ score -= 5;
719
+ matchReasons.push(`Nearest site is ${Math.round(nearestLocation.distance)} miles away`);
720
+ }
721
+ }
722
+ }
723
+ else if (patient.location.state) {
724
+ nearestLocation = recruitingLocations.find(l => l.state?.toLowerCase() === patient.location?.state?.toLowerCase());
725
+ if (nearestLocation) {
726
+ score += 5;
727
+ matchReasons.push(`Trial site in ${patient.location.state}`);
728
+ }
729
+ }
730
+ }
731
+ // Determine eligibility status
732
+ let eligibilityStatus;
733
+ if (score >= 50 && potentialExclusions.length === 0) {
734
+ eligibilityStatus = 'likely-eligible';
735
+ }
736
+ else if (score >= 30 && potentialExclusions.length <= 1) {
737
+ eligibilityStatus = 'possibly-eligible';
738
+ }
739
+ else if (score < 0 || potentialExclusions.length >= 3) {
740
+ eligibilityStatus = 'likely-ineligible';
741
+ }
742
+ else {
743
+ eligibilityStatus = 'unknown';
744
+ }
745
+ // Ensure minimum score of 0
746
+ score = Math.max(0, score);
747
+ return {
748
+ trial,
749
+ matchScore: score,
750
+ matchReasons,
751
+ eligibilityAssessment: {
752
+ status: eligibilityStatus,
753
+ matchingCriteria,
754
+ potentialExclusions,
755
+ missingInformation
756
+ },
757
+ nearestLocation,
758
+ biomarkerMatches: biomarkerMatches.length > 0 ? biomarkerMatches : undefined
759
+ };
760
+ }
761
+ calculateDistance(lat1, lon1, lat2, lon2) {
762
+ // Haversine formula to calculate distance between two points
763
+ const R = 3959; // Earth's radius in miles
764
+ const dLat = this.toRadians(lat2 - lat1);
765
+ const dLon = this.toRadians(lon2 - lon1);
766
+ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
767
+ Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
768
+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
769
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
770
+ return R * c;
771
+ }
772
+ toRadians(degrees) {
773
+ return degrees * (Math.PI / 180);
774
+ }
775
+ async httpRequest(url) {
776
+ const response = await fetch(url, {
777
+ method: 'GET',
778
+ headers: {
779
+ 'Accept': 'application/json'
780
+ },
781
+ signal: AbortSignal.timeout(this.timeout)
782
+ });
783
+ if (!response.ok) {
784
+ throw new Error(`ClinicalTrials.gov API error: ${response.status} ${response.statusText}`);
785
+ }
786
+ return await response.text();
787
+ }
788
+ getFromCache(key) {
789
+ const cached = this.cache.get(key);
790
+ if (cached && cached.expiry > new Date()) {
791
+ return cached.data;
792
+ }
793
+ this.cache.delete(key);
794
+ return null;
795
+ }
796
+ setCache(key, data) {
797
+ const expiry = new Date(Date.now() + this.cacheDuration * 60 * 1000);
798
+ this.cache.set(key, { data, expiry });
799
+ }
800
+ /**
801
+ * Clear the cache
802
+ */
803
+ clearCache() {
804
+ this.cache.clear();
805
+ }
806
+ }
807
+ export default ClinicalTrialsGovClient;
808
+ //# sourceMappingURL=clinicalTrialsGov.js.map