@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,1143 @@
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
+
9
+ import { EventEmitter } from 'events';
10
+
11
+ // ═══════════════════════════════════════════════════════════════════════════════
12
+ // CLINICAL TRIAL TYPES
13
+ // ═══════════════════════════════════════════════════════════════════════════════
14
+
15
+ export interface ClinicalTrial {
16
+ nctId: string;
17
+ title: string;
18
+ briefTitle?: string;
19
+ officialTitle?: string;
20
+ status: TrialStatus;
21
+ phase: TrialPhase;
22
+ studyType: 'interventional' | 'observational' | 'expanded-access';
23
+ conditions: string[];
24
+ interventions: TrialIntervention[];
25
+ eligibility: TrialEligibility;
26
+ locations: TrialLocation[];
27
+ sponsors: TrialSponsor[];
28
+ contacts: TrialContact[];
29
+ dates: {
30
+ startDate?: Date;
31
+ primaryCompletionDate?: Date;
32
+ completionDate?: Date;
33
+ firstPostedDate?: Date;
34
+ lastUpdatePostedDate?: Date;
35
+ };
36
+ enrollment?: {
37
+ count: number;
38
+ type: 'actual' | 'anticipated';
39
+ };
40
+ arms?: TrialArm[];
41
+ outcomes?: TrialOutcome[];
42
+ biomarkerRequirements?: BiomarkerRequirement[];
43
+ references?: string[];
44
+ url: string;
45
+ }
46
+
47
+ export type TrialStatus =
48
+ | 'not-yet-recruiting'
49
+ | 'recruiting'
50
+ | 'enrolling-by-invitation'
51
+ | 'active-not-recruiting'
52
+ | 'suspended'
53
+ | 'terminated'
54
+ | 'completed'
55
+ | 'withdrawn'
56
+ | 'unknown';
57
+
58
+ export type TrialPhase =
59
+ | 'early-phase-1'
60
+ | 'phase-1'
61
+ | 'phase-1-2'
62
+ | 'phase-2'
63
+ | 'phase-2-3'
64
+ | 'phase-3'
65
+ | 'phase-4'
66
+ | 'not-applicable';
67
+
68
+ export interface TrialIntervention {
69
+ type: 'drug' | 'biological' | 'device' | 'procedure' | 'radiation' | 'behavioral' | 'genetic' | 'dietary' | 'combination' | 'other';
70
+ name: string;
71
+ description?: string;
72
+ armGroupLabels?: string[];
73
+ otherNames?: string[];
74
+ }
75
+
76
+ export interface TrialEligibility {
77
+ criteria: string;
78
+ gender: 'all' | 'female' | 'male';
79
+ minimumAge?: string;
80
+ maximumAge?: string;
81
+ healthyVolunteers: boolean;
82
+ inclusionCriteria?: string[];
83
+ exclusionCriteria?: string[];
84
+ }
85
+
86
+ export interface TrialLocation {
87
+ facility: string;
88
+ city: string;
89
+ state?: string;
90
+ country: string;
91
+ zip?: string;
92
+ status?: 'recruiting' | 'not-recruiting' | 'withdrawn' | 'active' | 'completed';
93
+ contact?: {
94
+ name?: string;
95
+ phone?: string;
96
+ email?: string;
97
+ };
98
+ coordinates?: {
99
+ latitude: number;
100
+ longitude: number;
101
+ };
102
+ distance?: number; // Distance from patient in miles
103
+ }
104
+
105
+ export interface TrialSponsor {
106
+ name: string;
107
+ type: 'principal-investigator' | 'sponsor' | 'sponsor-investigator';
108
+ leadOrCollaborator: 'lead' | 'collaborator';
109
+ }
110
+
111
+ export interface TrialContact {
112
+ name?: string;
113
+ phone?: string;
114
+ email?: string;
115
+ role?: string;
116
+ }
117
+
118
+ export interface TrialArm {
119
+ label: string;
120
+ type: 'experimental' | 'active-comparator' | 'placebo-comparator' | 'sham-comparator' | 'no-intervention' | 'other';
121
+ description?: string;
122
+ interventions?: string[];
123
+ }
124
+
125
+ export interface TrialOutcome {
126
+ type: 'primary' | 'secondary' | 'other';
127
+ measure: string;
128
+ description?: string;
129
+ timeFrame?: string;
130
+ }
131
+
132
+ export interface BiomarkerRequirement {
133
+ biomarker: string;
134
+ requirement: 'required' | 'excluded' | 'preferred';
135
+ value?: string;
136
+ operator?: 'equals' | 'greater-than' | 'less-than' | 'between';
137
+ }
138
+
139
+ // ═══════════════════════════════════════════════════════════════════════════════
140
+ // SEARCH PARAMETERS
141
+ // ═══════════════════════════════════════════════════════════════════════════════
142
+
143
+ export interface TrialSearchParams {
144
+ // Disease/condition
145
+ condition?: string;
146
+ conditionTerms?: string[];
147
+
148
+ // Intervention
149
+ intervention?: string;
150
+ interventionType?: TrialIntervention['type'];
151
+ drugName?: string;
152
+
153
+ // Biomarkers
154
+ biomarkers?: string[];
155
+ genomicAlterations?: string[];
156
+
157
+ // Status
158
+ status?: TrialStatus[];
159
+ phase?: TrialPhase[];
160
+
161
+ // Location
162
+ country?: string;
163
+ state?: string;
164
+ city?: string;
165
+ zipCode?: string;
166
+ distance?: number; // miles from zipCode
167
+ coordinates?: { lat: number; lon: number };
168
+
169
+ // Demographics
170
+ age?: number;
171
+ gender?: 'male' | 'female';
172
+
173
+ // Other
174
+ sponsorType?: 'industry' | 'academic' | 'government' | 'other';
175
+ funderType?: 'nih' | 'industry' | 'other';
176
+ studyType?: 'interventional' | 'observational' | 'expanded-access';
177
+ hasResults?: boolean;
178
+
179
+ // Pagination
180
+ pageSize?: number;
181
+ pageToken?: string;
182
+ }
183
+
184
+ export interface TrialSearchResult {
185
+ trials: ClinicalTrial[];
186
+ totalCount: number;
187
+ nextPageToken?: string;
188
+ }
189
+
190
+ // ═══════════════════════════════════════════════════════════════════════════════
191
+ // PATIENT MATCHING
192
+ // ═══════════════════════════════════════════════════════════════════════════════
193
+
194
+ export interface PatientProfile {
195
+ cancerType: string;
196
+ stage?: string;
197
+ age?: number;
198
+ gender?: 'male' | 'female';
199
+ ecogStatus?: number;
200
+ biomarkers?: {
201
+ name: string;
202
+ value: string | number;
203
+ status?: 'positive' | 'negative';
204
+ }[];
205
+ genomicAlterations?: {
206
+ gene: string;
207
+ alteration: string;
208
+ type: 'mutation' | 'fusion' | 'amplification' | 'deletion';
209
+ }[];
210
+ msiStatus?: 'MSI-H' | 'MSI-L' | 'MSS';
211
+ tmbLevel?: 'high' | 'low';
212
+ pdl1Score?: number;
213
+ hrdStatus?: 'positive' | 'negative';
214
+ priorTherapies?: string[];
215
+ comorbidities?: string[];
216
+ location?: {
217
+ zipCode?: string;
218
+ city?: string;
219
+ state?: string;
220
+ country?: string;
221
+ coordinates?: { lat: number; lon: number };
222
+ };
223
+ maxTravelDistance?: number; // miles
224
+ }
225
+
226
+ export interface TrialMatch {
227
+ trial: ClinicalTrial;
228
+ matchScore: number;
229
+ matchReasons: string[];
230
+ eligibilityAssessment: {
231
+ status: 'likely-eligible' | 'possibly-eligible' | 'likely-ineligible' | 'unknown';
232
+ matchingCriteria: string[];
233
+ potentialExclusions: string[];
234
+ missingInformation: string[];
235
+ };
236
+ nearestLocation?: TrialLocation;
237
+ biomarkerMatches?: {
238
+ biomarker: string;
239
+ trialRequirement: string;
240
+ patientValue: string;
241
+ match: boolean;
242
+ }[];
243
+ }
244
+
245
+ // ═══════════════════════════════════════════════════════════════════════════════
246
+ // CLINICAL TRIALS API CLIENT
247
+ // ═══════════════════════════════════════════════════════════════════════════════
248
+
249
+ export class ClinicalTrialsGovClient extends EventEmitter {
250
+ private baseUrl = 'https://clinicaltrials.gov/api/v2';
251
+ private timeout: number;
252
+ private cache: Map<string, { data: any; expiry: Date }> = new Map();
253
+ private cacheDuration: number; // minutes
254
+
255
+ constructor(options?: { timeout?: number; cacheDuration?: number }) {
256
+ super();
257
+ this.timeout = options?.timeout || 30000;
258
+ this.cacheDuration = options?.cacheDuration || 60; // 1 hour default
259
+ }
260
+
261
+ /**
262
+ * Search for clinical trials
263
+ */
264
+ async searchTrials(params: TrialSearchParams): Promise<TrialSearchResult> {
265
+ const cacheKey = JSON.stringify(params);
266
+ const cached = this.getFromCache(cacheKey);
267
+ if (cached) return cached;
268
+
269
+ const query = this.buildSearchQuery(params);
270
+ const url = `${this.baseUrl}/studies?${query}`;
271
+
272
+ const response = await this.httpRequest(url);
273
+ const data = JSON.parse(response);
274
+
275
+ const result: TrialSearchResult = {
276
+ trials: (data.studies || []).map((s: any) => this.mapStudyToTrial(s)),
277
+ totalCount: data.totalCount || 0,
278
+ nextPageToken: data.nextPageToken
279
+ };
280
+
281
+ this.setCache(cacheKey, result);
282
+ return result;
283
+ }
284
+
285
+ /**
286
+ * Get a specific trial by NCT ID
287
+ */
288
+ async getTrial(nctId: string): Promise<ClinicalTrial> {
289
+ const cacheKey = `trial:${nctId}`;
290
+ const cached = this.getFromCache(cacheKey);
291
+ if (cached) return cached;
292
+
293
+ const url = `${this.baseUrl}/studies/${nctId}`;
294
+ const response = await this.httpRequest(url);
295
+ const data = JSON.parse(response);
296
+
297
+ const trial = this.mapStudyToTrial(data);
298
+ this.setCache(cacheKey, trial);
299
+ return trial;
300
+ }
301
+
302
+ /**
303
+ * Get multiple trials by NCT IDs
304
+ */
305
+ async getTrials(nctIds: string[]): Promise<ClinicalTrial[]> {
306
+ return Promise.all(nctIds.map(id => this.getTrial(id)));
307
+ }
308
+
309
+ /**
310
+ * Search for trials matching a patient profile
311
+ */
312
+ async findMatchingTrials(patient: PatientProfile): Promise<TrialMatch[]> {
313
+ // Build search query based on patient profile
314
+ const searchParams: TrialSearchParams = {
315
+ condition: patient.cancerType,
316
+ status: ['recruiting', 'enrolling-by-invitation', 'not-yet-recruiting'],
317
+ studyType: 'interventional',
318
+ age: patient.age,
319
+ gender: patient.gender,
320
+ pageSize: 100
321
+ };
322
+
323
+ // Add location-based filtering
324
+ if (patient.location?.zipCode) {
325
+ searchParams.zipCode = patient.location.zipCode;
326
+ searchParams.distance = patient.maxTravelDistance || 100;
327
+ } else if (patient.location?.country) {
328
+ searchParams.country = patient.location.country;
329
+ if (patient.location.state) {
330
+ searchParams.state = patient.location.state;
331
+ }
332
+ }
333
+
334
+ // Add biomarker-specific searches
335
+ if (patient.genomicAlterations && patient.genomicAlterations.length > 0) {
336
+ searchParams.genomicAlterations = patient.genomicAlterations.map(g => `${g.gene} ${g.alteration}`);
337
+ }
338
+
339
+ // Execute search
340
+ const searchResult = await this.searchTrials(searchParams);
341
+
342
+ // Score and filter trials
343
+ const matches: TrialMatch[] = [];
344
+
345
+ for (const trial of searchResult.trials) {
346
+ const match = this.assessTrialMatch(trial, patient);
347
+ if (match.matchScore > 0) {
348
+ matches.push(match);
349
+ }
350
+ }
351
+
352
+ // Sort by match score
353
+ matches.sort((a, b) => b.matchScore - a.matchScore);
354
+
355
+ return matches;
356
+ }
357
+
358
+ /**
359
+ * Search for trials by biomarker
360
+ */
361
+ async searchByBiomarker(biomarker: string, options?: {
362
+ cancerType?: string;
363
+ status?: TrialStatus[];
364
+ phase?: TrialPhase[];
365
+ }): Promise<ClinicalTrial[]> {
366
+ const params: TrialSearchParams = {
367
+ biomarkers: [biomarker],
368
+ condition: options?.cancerType,
369
+ status: options?.status || ['recruiting', 'not-yet-recruiting'],
370
+ phase: options?.phase,
371
+ studyType: 'interventional',
372
+ pageSize: 50
373
+ };
374
+
375
+ const result = await this.searchTrials(params);
376
+ return result.trials;
377
+ }
378
+
379
+ /**
380
+ * Search for trials by drug/intervention
381
+ */
382
+ async searchByDrug(drugName: string, options?: {
383
+ cancerType?: string;
384
+ status?: TrialStatus[];
385
+ phase?: TrialPhase[];
386
+ }): Promise<ClinicalTrial[]> {
387
+ const params: TrialSearchParams = {
388
+ drugName,
389
+ condition: options?.cancerType,
390
+ status: options?.status || ['recruiting', 'not-yet-recruiting'],
391
+ phase: options?.phase,
392
+ studyType: 'interventional',
393
+ pageSize: 50
394
+ };
395
+
396
+ const result = await this.searchTrials(params);
397
+ return result.trials;
398
+ }
399
+
400
+ /**
401
+ * Get trials for a specific cancer type
402
+ */
403
+ async getTrialsForCancerType(cancerType: string, options?: {
404
+ phase?: TrialPhase[];
405
+ location?: { country?: string; state?: string };
406
+ biomarkers?: string[];
407
+ }): Promise<ClinicalTrial[]> {
408
+ const params: TrialSearchParams = {
409
+ condition: cancerType,
410
+ status: ['recruiting', 'not-yet-recruiting', 'enrolling-by-invitation'],
411
+ studyType: 'interventional',
412
+ phase: options?.phase,
413
+ country: options?.location?.country,
414
+ state: options?.location?.state,
415
+ biomarkers: options?.biomarkers,
416
+ pageSize: 100
417
+ };
418
+
419
+ const result = await this.searchTrials(params);
420
+ return result.trials;
421
+ }
422
+
423
+ // ═══════════════════════════════════════════════════════════════════════════════
424
+ // HELPER METHODS
425
+ // ═══════════════════════════════════════════════════════════════════════════════
426
+
427
+ private buildSearchQuery(params: TrialSearchParams): string {
428
+ const queryParts: string[] = [];
429
+
430
+ // Condition/disease
431
+ if (params.condition) {
432
+ queryParts.push(`query.cond=${encodeURIComponent(params.condition)}`);
433
+ }
434
+ if (params.conditionTerms && params.conditionTerms.length > 0) {
435
+ queryParts.push(`query.term=${encodeURIComponent(params.conditionTerms.join(' OR '))}`);
436
+ }
437
+
438
+ // Intervention/drug
439
+ if (params.intervention) {
440
+ queryParts.push(`query.intr=${encodeURIComponent(params.intervention)}`);
441
+ }
442
+ if (params.drugName) {
443
+ queryParts.push(`query.intr=${encodeURIComponent(params.drugName)}`);
444
+ }
445
+
446
+ // Biomarkers/genomic alterations (search in full text)
447
+ if (params.biomarkers && params.biomarkers.length > 0) {
448
+ const biomarkerQuery = params.biomarkers.join(' OR ');
449
+ queryParts.push(`query.term=${encodeURIComponent(biomarkerQuery)}`);
450
+ }
451
+ if (params.genomicAlterations && params.genomicAlterations.length > 0) {
452
+ const genomicQuery = params.genomicAlterations.join(' OR ');
453
+ queryParts.push(`query.term=${encodeURIComponent(genomicQuery)}`);
454
+ }
455
+
456
+ // Status filter
457
+ if (params.status && params.status.length > 0) {
458
+ const statusMap: Record<TrialStatus, string> = {
459
+ 'not-yet-recruiting': 'NOT_YET_RECRUITING',
460
+ 'recruiting': 'RECRUITING',
461
+ 'enrolling-by-invitation': 'ENROLLING_BY_INVITATION',
462
+ 'active-not-recruiting': 'ACTIVE_NOT_RECRUITING',
463
+ 'suspended': 'SUSPENDED',
464
+ 'terminated': 'TERMINATED',
465
+ 'completed': 'COMPLETED',
466
+ 'withdrawn': 'WITHDRAWN',
467
+ 'unknown': 'UNKNOWN'
468
+ };
469
+ const statuses = params.status.map(s => statusMap[s]).join(',');
470
+ queryParts.push(`filter.overallStatus=${statuses}`);
471
+ }
472
+
473
+ // Phase filter
474
+ if (params.phase && params.phase.length > 0) {
475
+ const phaseMap: Record<TrialPhase, string> = {
476
+ 'early-phase-1': 'EARLY_PHASE1',
477
+ 'phase-1': 'PHASE1',
478
+ 'phase-1-2': 'PHASE1_PHASE2',
479
+ 'phase-2': 'PHASE2',
480
+ 'phase-2-3': 'PHASE2_PHASE3',
481
+ 'phase-3': 'PHASE3',
482
+ 'phase-4': 'PHASE4',
483
+ 'not-applicable': 'NA'
484
+ };
485
+ const phases = params.phase.map(p => phaseMap[p]).join(',');
486
+ queryParts.push(`filter.phase=${phases}`);
487
+ }
488
+
489
+ // Study type
490
+ if (params.studyType) {
491
+ const studyTypeMap: Record<string, string> = {
492
+ 'interventional': 'INTERVENTIONAL',
493
+ 'observational': 'OBSERVATIONAL',
494
+ 'expanded-access': 'EXPANDED_ACCESS'
495
+ };
496
+ queryParts.push(`filter.studyType=${studyTypeMap[params.studyType]}`);
497
+ }
498
+
499
+ // Location filters
500
+ if (params.country) {
501
+ queryParts.push(`query.locn=${encodeURIComponent(params.country)}`);
502
+ }
503
+ if (params.state) {
504
+ queryParts.push(`query.locn=${encodeURIComponent(params.state)}`);
505
+ }
506
+ if (params.city) {
507
+ queryParts.push(`query.locn=${encodeURIComponent(params.city)}`);
508
+ }
509
+
510
+ // Geographic search (if zip code and distance provided)
511
+ if (params.zipCode && params.distance) {
512
+ queryParts.push(`postFilter.geo=distance(${params.zipCode},${params.distance}mi)`);
513
+ }
514
+
515
+ // Age filter
516
+ if (params.age) {
517
+ queryParts.push(`aggFilters=ages:adult`); // Simplified - would need more logic
518
+ }
519
+
520
+ // Gender filter
521
+ if (params.gender) {
522
+ queryParts.push(`filter.sex=${params.gender.toUpperCase()}`);
523
+ }
524
+
525
+ // Results filter
526
+ if (params.hasResults !== undefined) {
527
+ queryParts.push(`filter.results=${params.hasResults}`);
528
+ }
529
+
530
+ // Pagination
531
+ queryParts.push(`pageSize=${params.pageSize || 20}`);
532
+ if (params.pageToken) {
533
+ queryParts.push(`pageToken=${params.pageToken}`);
534
+ }
535
+
536
+ // Request all needed fields
537
+ queryParts.push('fields=NCTId,BriefTitle,OfficialTitle,OverallStatus,Phase,StudyType,Condition,' +
538
+ 'InterventionName,InterventionType,InterventionDescription,EligibilityCriteria,' +
539
+ 'MinimumAge,MaximumAge,Gender,LocationFacility,LocationCity,LocationState,LocationCountry,' +
540
+ 'LeadSponsorName,StartDate,PrimaryCompletionDate,EnrollmentCount,ArmGroupLabel,' +
541
+ 'ArmGroupType,PrimaryOutcomeMeasure,SecondaryOutcomeMeasure,CentralContactName,' +
542
+ 'CentralContactPhone,CentralContactEMail,BriefSummary,DetailedDescription');
543
+
544
+ return queryParts.join('&');
545
+ }
546
+
547
+ private mapStudyToTrial(study: any): ClinicalTrial {
548
+ const protocol = study.protocolSection || study;
549
+ const identification = protocol.identificationModule || {};
550
+ const status = protocol.statusModule || {};
551
+ const description = protocol.descriptionModule || {};
552
+ const conditions = protocol.conditionsModule || {};
553
+ const design = protocol.designModule || {};
554
+ const arms = protocol.armsInterventionsModule || {};
555
+ const eligibility = protocol.eligibilityModule || {};
556
+ const contacts = protocol.contactsLocationsModule || {};
557
+ const sponsor = protocol.sponsorCollaboratorsModule || {};
558
+ const outcomes = protocol.outcomesModule || {};
559
+
560
+ // Map interventions
561
+ const interventions: TrialIntervention[] = (arms.interventions || []).map((i: any) => ({
562
+ type: this.mapInterventionType(i.type),
563
+ name: i.name,
564
+ description: i.description,
565
+ armGroupLabels: i.armGroupLabels,
566
+ otherNames: i.otherNames
567
+ }));
568
+
569
+ // Map locations
570
+ const locations: TrialLocation[] = (contacts.locations || []).map((loc: any) => ({
571
+ facility: loc.facility,
572
+ city: loc.city,
573
+ state: loc.state,
574
+ country: loc.country,
575
+ zip: loc.zip,
576
+ status: this.mapLocationStatus(loc.status),
577
+ contact: loc.contacts?.[0] ? {
578
+ name: loc.contacts[0].name,
579
+ phone: loc.contacts[0].phone,
580
+ email: loc.contacts[0].email
581
+ } : undefined,
582
+ coordinates: loc.geoPoint ? {
583
+ latitude: loc.geoPoint.lat,
584
+ longitude: loc.geoPoint.lon
585
+ } : undefined
586
+ }));
587
+
588
+ // Map sponsors
589
+ const sponsors: TrialSponsor[] = [];
590
+ if (sponsor.leadSponsor) {
591
+ sponsors.push({
592
+ name: sponsor.leadSponsor.name,
593
+ type: 'sponsor',
594
+ leadOrCollaborator: 'lead'
595
+ });
596
+ }
597
+ for (const collab of sponsor.collaborators || []) {
598
+ sponsors.push({
599
+ name: collab.name,
600
+ type: 'sponsor',
601
+ leadOrCollaborator: 'collaborator'
602
+ });
603
+ }
604
+
605
+ // Map contacts
606
+ const trialContacts: TrialContact[] = (contacts.centralContacts || []).map((c: any) => ({
607
+ name: c.name,
608
+ phone: c.phone,
609
+ email: c.email,
610
+ role: c.role
611
+ }));
612
+
613
+ // Map arms
614
+ const trialArms: TrialArm[] = (arms.armGroups || []).map((arm: any) => ({
615
+ label: arm.label,
616
+ type: this.mapArmType(arm.type),
617
+ description: arm.description,
618
+ interventions: arm.interventionNames
619
+ }));
620
+
621
+ // Map outcomes
622
+ const trialOutcomes: TrialOutcome[] = [
623
+ ...(outcomes.primaryOutcomes || []).map((o: any) => ({
624
+ type: 'primary' as const,
625
+ measure: o.measure,
626
+ description: o.description,
627
+ timeFrame: o.timeFrame
628
+ })),
629
+ ...(outcomes.secondaryOutcomes || []).map((o: any) => ({
630
+ type: 'secondary' as const,
631
+ measure: o.measure,
632
+ description: o.description,
633
+ timeFrame: o.timeFrame
634
+ }))
635
+ ];
636
+
637
+ // Parse eligibility criteria into structured format
638
+ const criteriaText = eligibility.eligibilityCriteria || '';
639
+ const { inclusion, exclusion } = this.parseEligibilityCriteria(criteriaText);
640
+
641
+ // Extract biomarker requirements from criteria
642
+ const biomarkerRequirements = this.extractBiomarkerRequirements(criteriaText, interventions);
643
+
644
+ return {
645
+ nctId: identification.nctId,
646
+ title: identification.briefTitle || identification.officialTitle || '',
647
+ briefTitle: identification.briefTitle,
648
+ officialTitle: identification.officialTitle,
649
+ status: this.mapTrialStatus(status.overallStatus),
650
+ phase: this.mapTrialPhase(design.phases?.[0]),
651
+ studyType: this.mapStudyType(design.studyType),
652
+ conditions: conditions.conditions || [],
653
+ interventions,
654
+ eligibility: {
655
+ criteria: criteriaText,
656
+ gender: this.mapGender(eligibility.sex),
657
+ minimumAge: eligibility.minimumAge,
658
+ maximumAge: eligibility.maximumAge,
659
+ healthyVolunteers: eligibility.healthyVolunteers === 'Yes',
660
+ inclusionCriteria: inclusion,
661
+ exclusionCriteria: exclusion
662
+ },
663
+ locations,
664
+ sponsors,
665
+ contacts: trialContacts,
666
+ dates: {
667
+ startDate: status.startDateStruct ? new Date(status.startDateStruct.date) : undefined,
668
+ primaryCompletionDate: status.primaryCompletionDateStruct ? new Date(status.primaryCompletionDateStruct.date) : undefined,
669
+ completionDate: status.completionDateStruct ? new Date(status.completionDateStruct.date) : undefined,
670
+ firstPostedDate: status.studyFirstPostDateStruct ? new Date(status.studyFirstPostDateStruct.date) : undefined,
671
+ lastUpdatePostedDate: status.lastUpdatePostDateStruct ? new Date(status.lastUpdatePostDateStruct.date) : undefined
672
+ },
673
+ enrollment: design.enrollmentInfo ? {
674
+ count: design.enrollmentInfo.count,
675
+ type: design.enrollmentInfo.type?.toLowerCase() as 'actual' | 'anticipated'
676
+ } : undefined,
677
+ arms: trialArms.length > 0 ? trialArms : undefined,
678
+ outcomes: trialOutcomes.length > 0 ? trialOutcomes : undefined,
679
+ biomarkerRequirements: biomarkerRequirements.length > 0 ? biomarkerRequirements : undefined,
680
+ url: `https://clinicaltrials.gov/study/${identification.nctId}`
681
+ };
682
+ }
683
+
684
+ private mapTrialStatus(status: string): TrialStatus {
685
+ const statusMap: Record<string, TrialStatus> = {
686
+ 'NOT_YET_RECRUITING': 'not-yet-recruiting',
687
+ 'RECRUITING': 'recruiting',
688
+ 'ENROLLING_BY_INVITATION': 'enrolling-by-invitation',
689
+ 'ACTIVE_NOT_RECRUITING': 'active-not-recruiting',
690
+ 'SUSPENDED': 'suspended',
691
+ 'TERMINATED': 'terminated',
692
+ 'COMPLETED': 'completed',
693
+ 'WITHDRAWN': 'withdrawn'
694
+ };
695
+ return statusMap[status] || 'unknown';
696
+ }
697
+
698
+ private mapTrialPhase(phase: string): TrialPhase {
699
+ const phaseMap: Record<string, TrialPhase> = {
700
+ 'EARLY_PHASE1': 'early-phase-1',
701
+ 'PHASE1': 'phase-1',
702
+ 'PHASE1_PHASE2': 'phase-1-2',
703
+ 'PHASE2': 'phase-2',
704
+ 'PHASE2_PHASE3': 'phase-2-3',
705
+ 'PHASE3': 'phase-3',
706
+ 'PHASE4': 'phase-4',
707
+ 'NA': 'not-applicable'
708
+ };
709
+ return phaseMap[phase] || 'not-applicable';
710
+ }
711
+
712
+ private mapStudyType(type: string): 'interventional' | 'observational' | 'expanded-access' {
713
+ const typeMap: Record<string, 'interventional' | 'observational' | 'expanded-access'> = {
714
+ 'INTERVENTIONAL': 'interventional',
715
+ 'OBSERVATIONAL': 'observational',
716
+ 'EXPANDED_ACCESS': 'expanded-access'
717
+ };
718
+ return typeMap[type] || 'interventional';
719
+ }
720
+
721
+ private mapInterventionType(type: string): TrialIntervention['type'] {
722
+ const typeMap: Record<string, TrialIntervention['type']> = {
723
+ 'DRUG': 'drug',
724
+ 'BIOLOGICAL': 'biological',
725
+ 'DEVICE': 'device',
726
+ 'PROCEDURE': 'procedure',
727
+ 'RADIATION': 'radiation',
728
+ 'BEHAVIORAL': 'behavioral',
729
+ 'GENETIC': 'genetic',
730
+ 'DIETARY_SUPPLEMENT': 'dietary',
731
+ 'COMBINATION_PRODUCT': 'combination',
732
+ 'OTHER': 'other'
733
+ };
734
+ return typeMap[type] || 'other';
735
+ }
736
+
737
+ private mapLocationStatus(status: string): TrialLocation['status'] {
738
+ const statusMap: Record<string, TrialLocation['status']> = {
739
+ 'RECRUITING': 'recruiting',
740
+ 'NOT_YET_RECRUITING': 'not-recruiting',
741
+ 'ACTIVE_NOT_RECRUITING': 'active',
742
+ 'COMPLETED': 'completed',
743
+ 'WITHDRAWN': 'withdrawn'
744
+ };
745
+ return statusMap[status] || 'active';
746
+ }
747
+
748
+ private mapArmType(type: string): TrialArm['type'] {
749
+ const typeMap: Record<string, TrialArm['type']> = {
750
+ 'EXPERIMENTAL': 'experimental',
751
+ 'ACTIVE_COMPARATOR': 'active-comparator',
752
+ 'PLACEBO_COMPARATOR': 'placebo-comparator',
753
+ 'SHAM_COMPARATOR': 'sham-comparator',
754
+ 'NO_INTERVENTION': 'no-intervention',
755
+ 'OTHER': 'other'
756
+ };
757
+ return typeMap[type] || 'other';
758
+ }
759
+
760
+ private mapGender(sex: string): 'all' | 'female' | 'male' {
761
+ if (sex === 'FEMALE') return 'female';
762
+ if (sex === 'MALE') return 'male';
763
+ return 'all';
764
+ }
765
+
766
+ private parseEligibilityCriteria(criteria: string): { inclusion: string[]; exclusion: string[] } {
767
+ const inclusion: string[] = [];
768
+ const exclusion: string[] = [];
769
+
770
+ if (!criteria) return { inclusion, exclusion };
771
+
772
+ // Try to split by Inclusion/Exclusion headers
773
+ const sections = criteria.split(/(?:Inclusion|Exclusion)\s*Criteria:?/i);
774
+
775
+ // Simple parsing - look for patterns
776
+ const lines = criteria.split(/\n|•|·|-\s+|\*\s+|\d+\.\s+/);
777
+
778
+ let inExclusion = false;
779
+ for (const line of lines) {
780
+ const trimmed = line.trim();
781
+ if (!trimmed) continue;
782
+
783
+ // Detect section changes
784
+ if (trimmed.toLowerCase().includes('exclusion')) {
785
+ inExclusion = true;
786
+ continue;
787
+ }
788
+ if (trimmed.toLowerCase().includes('inclusion')) {
789
+ inExclusion = false;
790
+ continue;
791
+ }
792
+
793
+ // Add to appropriate list
794
+ if (trimmed.length > 10) { // Filter out very short fragments
795
+ if (inExclusion) {
796
+ exclusion.push(trimmed);
797
+ } else {
798
+ inclusion.push(trimmed);
799
+ }
800
+ }
801
+ }
802
+
803
+ return { inclusion, exclusion };
804
+ }
805
+
806
+ private extractBiomarkerRequirements(criteria: string, interventions: TrialIntervention[]): BiomarkerRequirement[] {
807
+ const requirements: BiomarkerRequirement[] = [];
808
+ const criteriaLower = criteria.toLowerCase();
809
+
810
+ // Common biomarker patterns
811
+ const biomarkerPatterns: { pattern: RegExp; biomarker: string }[] = [
812
+ { pattern: /egfr\s*(mutation|mutant|positive|\+)/i, biomarker: 'EGFR mutation' },
813
+ { pattern: /egfr\s*(wild[- ]?type|negative|wt)/i, biomarker: 'EGFR wild-type' },
814
+ { pattern: /alk\s*(rearrangement|fusion|positive|\+)/i, biomarker: 'ALK fusion' },
815
+ { pattern: /ros1\s*(rearrangement|fusion|positive|\+)/i, biomarker: 'ROS1 fusion' },
816
+ { pattern: /braf\s*v600[ek]?/i, biomarker: 'BRAF V600' },
817
+ { pattern: /kras\s*g12c/i, biomarker: 'KRAS G12C' },
818
+ { pattern: /kras\s*(mutation|mutant|positive)/i, biomarker: 'KRAS mutation' },
819
+ { pattern: /her2\s*(positive|overexpression|amplification|\+|3\+)/i, biomarker: 'HER2 positive' },
820
+ { pattern: /her2\s*(negative|\-|0|1\+)/i, biomarker: 'HER2 negative' },
821
+ { pattern: /brca[12]?\s*(mutation|mutant|positive|pathogenic)/i, biomarker: 'BRCA mutation' },
822
+ { pattern: /msi[- ]?h(igh)?|microsatellite\s*instability[- ]?high/i, biomarker: 'MSI-H' },
823
+ { pattern: /mss|microsatellite\s*stable/i, biomarker: 'MSS' },
824
+ { pattern: /pd[- ]?l1\s*(positive|expression|tps|cps)/i, biomarker: 'PD-L1 positive' },
825
+ { pattern: /pd[- ]?l1\s*[\u2265>=]\s*(\d+)/i, biomarker: 'PD-L1' },
826
+ { pattern: /tmb[- ]?h(igh)?|tumor\s*mutational\s*burden[- ]?high/i, biomarker: 'TMB-H' },
827
+ { pattern: /hrd\s*(positive|deficient)/i, biomarker: 'HRD positive' },
828
+ { pattern: /ntrk\s*(fusion|rearrangement)/i, biomarker: 'NTRK fusion' },
829
+ { pattern: /ret\s*(fusion|rearrangement|mutation)/i, biomarker: 'RET alteration' },
830
+ { pattern: /met\s*(exon\s*14|amplification)/i, biomarker: 'MET alteration' },
831
+ { pattern: /fgfr[1234]?\s*(alteration|fusion|mutation|amplification)/i, biomarker: 'FGFR alteration' },
832
+ { pattern: /pik3ca\s*(mutation|mutant)/i, biomarker: 'PIK3CA mutation' },
833
+ { pattern: /idh[12]\s*(mutation|mutant)/i, biomarker: 'IDH mutation' }
834
+ ];
835
+
836
+ for (const { pattern, biomarker } of biomarkerPatterns) {
837
+ if (pattern.test(criteria)) {
838
+ // Determine if it's required or excluded based on context
839
+ const match = criteria.match(new RegExp(`.{0,50}${pattern.source}.{0,50}`, 'i'));
840
+ if (match) {
841
+ const context = match[0].toLowerCase();
842
+ let requirement: BiomarkerRequirement['requirement'] = 'required';
843
+
844
+ if (context.includes('exclud') || context.includes('must not') ||
845
+ context.includes('no ') || context.includes('without') ||
846
+ context.includes('ineligible')) {
847
+ requirement = 'excluded';
848
+ }
849
+
850
+ requirements.push({
851
+ biomarker,
852
+ requirement
853
+ });
854
+ }
855
+ }
856
+ }
857
+
858
+ return requirements;
859
+ }
860
+
861
+ private assessTrialMatch(trial: ClinicalTrial, patient: PatientProfile): TrialMatch {
862
+ let score = 0;
863
+ const matchReasons: string[] = [];
864
+ const matchingCriteria: string[] = [];
865
+ const potentialExclusions: string[] = [];
866
+ const missingInformation: string[] = [];
867
+ const biomarkerMatches: TrialMatch['biomarkerMatches'] = [];
868
+
869
+ // Check cancer type match
870
+ const cancerMatch = trial.conditions.some(c =>
871
+ c.toLowerCase().includes(patient.cancerType.toLowerCase()) ||
872
+ patient.cancerType.toLowerCase().includes(c.toLowerCase())
873
+ );
874
+ if (cancerMatch) {
875
+ score += 30;
876
+ matchReasons.push(`Cancer type matches: ${patient.cancerType}`);
877
+ matchingCriteria.push('Cancer type');
878
+ }
879
+
880
+ // Check biomarker requirements
881
+ if (trial.biomarkerRequirements && patient.genomicAlterations) {
882
+ for (const req of trial.biomarkerRequirements) {
883
+ const patientHasBiomarker = patient.genomicAlterations.some(g =>
884
+ req.biomarker.toLowerCase().includes(g.gene.toLowerCase()) ||
885
+ req.biomarker.toLowerCase().includes(g.alteration.toLowerCase())
886
+ );
887
+
888
+ if (req.requirement === 'required') {
889
+ if (patientHasBiomarker) {
890
+ score += 25;
891
+ matchReasons.push(`Has required biomarker: ${req.biomarker}`);
892
+ matchingCriteria.push(req.biomarker);
893
+ biomarkerMatches.push({
894
+ biomarker: req.biomarker,
895
+ trialRequirement: 'Required',
896
+ patientValue: 'Present',
897
+ match: true
898
+ });
899
+ } else {
900
+ score -= 10;
901
+ potentialExclusions.push(`Missing required biomarker: ${req.biomarker}`);
902
+ biomarkerMatches.push({
903
+ biomarker: req.biomarker,
904
+ trialRequirement: 'Required',
905
+ patientValue: 'Not detected',
906
+ match: false
907
+ });
908
+ }
909
+ } else if (req.requirement === 'excluded') {
910
+ if (patientHasBiomarker) {
911
+ score -= 50;
912
+ potentialExclusions.push(`Has excluded biomarker: ${req.biomarker}`);
913
+ biomarkerMatches.push({
914
+ biomarker: req.biomarker,
915
+ trialRequirement: 'Excluded',
916
+ patientValue: 'Present',
917
+ match: false
918
+ });
919
+ }
920
+ }
921
+ }
922
+ }
923
+
924
+ // Check MSI status
925
+ if (patient.msiStatus) {
926
+ const trialMentionsMSI = trial.eligibility.criteria.toLowerCase().includes('msi');
927
+ if (trialMentionsMSI) {
928
+ if (patient.msiStatus === 'MSI-H' && trial.eligibility.criteria.toLowerCase().includes('msi-h')) {
929
+ score += 20;
930
+ matchReasons.push('MSI-H status matches trial requirement');
931
+ matchingCriteria.push('MSI-H');
932
+ }
933
+ }
934
+ }
935
+
936
+ // Check TMB status
937
+ if (patient.tmbLevel === 'high') {
938
+ const trialMentionsTMB = trial.eligibility.criteria.toLowerCase().includes('tmb');
939
+ if (trialMentionsTMB) {
940
+ score += 15;
941
+ matchReasons.push('TMB-High may enhance eligibility');
942
+ matchingCriteria.push('TMB-H');
943
+ }
944
+ }
945
+
946
+ // Check age eligibility
947
+ if (patient.age) {
948
+ let ageEligible = true;
949
+ if (trial.eligibility.minimumAge) {
950
+ const minAge = parseInt(trial.eligibility.minimumAge);
951
+ if (!isNaN(minAge) && patient.age < minAge) {
952
+ ageEligible = false;
953
+ potentialExclusions.push(`Below minimum age (${trial.eligibility.minimumAge})`);
954
+ }
955
+ }
956
+ if (trial.eligibility.maximumAge && trial.eligibility.maximumAge !== 'N/A') {
957
+ const maxAge = parseInt(trial.eligibility.maximumAge);
958
+ if (!isNaN(maxAge) && patient.age > maxAge) {
959
+ ageEligible = false;
960
+ potentialExclusions.push(`Above maximum age (${trial.eligibility.maximumAge})`);
961
+ }
962
+ }
963
+ if (ageEligible) {
964
+ score += 5;
965
+ matchingCriteria.push('Age');
966
+ } else {
967
+ score -= 30;
968
+ }
969
+ } else {
970
+ missingInformation.push('Patient age');
971
+ }
972
+
973
+ // Check gender eligibility
974
+ if (patient.gender) {
975
+ if (trial.eligibility.gender === 'all' || trial.eligibility.gender === patient.gender) {
976
+ matchingCriteria.push('Gender');
977
+ } else {
978
+ score -= 50;
979
+ potentialExclusions.push(`Trial only accepts ${trial.eligibility.gender} patients`);
980
+ }
981
+ }
982
+
983
+ // Check prior therapy exclusions
984
+ if (patient.priorTherapies && patient.priorTherapies.length > 0) {
985
+ const criteriaLower = trial.eligibility.criteria.toLowerCase();
986
+ for (const therapy of patient.priorTherapies) {
987
+ if (criteriaLower.includes(`no prior ${therapy.toLowerCase()}`) ||
988
+ criteriaLower.includes(`not received ${therapy.toLowerCase()}`)) {
989
+ potentialExclusions.push(`Prior ${therapy} may exclude patient`);
990
+ score -= 10;
991
+ }
992
+ }
993
+ }
994
+
995
+ // Check ECOG status
996
+ if (patient.ecogStatus !== undefined) {
997
+ const ecogMatch = trial.eligibility.criteria.match(/ecog\s*(?:performance\s*status)?\s*(?:of\s*)?(\d)(?:\s*(?:or|to|-)\s*(\d))?/i);
998
+ if (ecogMatch) {
999
+ const maxEcog = parseInt(ecogMatch[2] || ecogMatch[1]);
1000
+ if (patient.ecogStatus <= maxEcog) {
1001
+ matchingCriteria.push(`ECOG ${patient.ecogStatus}`);
1002
+ } else {
1003
+ potentialExclusions.push(`ECOG ${patient.ecogStatus} may be too high (trial requires ≤${maxEcog})`);
1004
+ score -= 20;
1005
+ }
1006
+ }
1007
+ } else {
1008
+ missingInformation.push('ECOG performance status');
1009
+ }
1010
+
1011
+ // Boost score for phase based on patient preference
1012
+ if (trial.phase === 'phase-3') {
1013
+ score += 10;
1014
+ matchReasons.push('Phase 3 trial (more established efficacy data)');
1015
+ } else if (trial.phase === 'phase-2') {
1016
+ score += 5;
1017
+ }
1018
+
1019
+ // Find nearest location
1020
+ let nearestLocation: TrialLocation | undefined;
1021
+ if (patient.location) {
1022
+ const recruitingLocations = trial.locations.filter(l => l.status === 'recruiting');
1023
+
1024
+ if (patient.location.coordinates && recruitingLocations.some(l => l.coordinates)) {
1025
+ // Calculate distances
1026
+ for (const loc of recruitingLocations) {
1027
+ if (loc.coordinates) {
1028
+ loc.distance = this.calculateDistance(
1029
+ patient.location.coordinates.lat,
1030
+ patient.location.coordinates.lon,
1031
+ loc.coordinates.latitude,
1032
+ loc.coordinates.longitude
1033
+ );
1034
+ }
1035
+ }
1036
+ recruitingLocations.sort((a, b) => (a.distance || 999999) - (b.distance || 999999));
1037
+ nearestLocation = recruitingLocations[0];
1038
+
1039
+ if (nearestLocation?.distance && patient.maxTravelDistance) {
1040
+ if (nearestLocation.distance <= patient.maxTravelDistance) {
1041
+ score += 10;
1042
+ matchReasons.push(`Trial site within ${Math.round(nearestLocation.distance)} miles`);
1043
+ } else {
1044
+ score -= 5;
1045
+ matchReasons.push(`Nearest site is ${Math.round(nearestLocation.distance)} miles away`);
1046
+ }
1047
+ }
1048
+ } else if (patient.location.state) {
1049
+ nearestLocation = recruitingLocations.find(l =>
1050
+ l.state?.toLowerCase() === patient.location?.state?.toLowerCase()
1051
+ );
1052
+ if (nearestLocation) {
1053
+ score += 5;
1054
+ matchReasons.push(`Trial site in ${patient.location.state}`);
1055
+ }
1056
+ }
1057
+ }
1058
+
1059
+ // Determine eligibility status
1060
+ let eligibilityStatus: TrialMatch['eligibilityAssessment']['status'];
1061
+ if (score >= 50 && potentialExclusions.length === 0) {
1062
+ eligibilityStatus = 'likely-eligible';
1063
+ } else if (score >= 30 && potentialExclusions.length <= 1) {
1064
+ eligibilityStatus = 'possibly-eligible';
1065
+ } else if (score < 0 || potentialExclusions.length >= 3) {
1066
+ eligibilityStatus = 'likely-ineligible';
1067
+ } else {
1068
+ eligibilityStatus = 'unknown';
1069
+ }
1070
+
1071
+ // Ensure minimum score of 0
1072
+ score = Math.max(0, score);
1073
+
1074
+ return {
1075
+ trial,
1076
+ matchScore: score,
1077
+ matchReasons,
1078
+ eligibilityAssessment: {
1079
+ status: eligibilityStatus,
1080
+ matchingCriteria,
1081
+ potentialExclusions,
1082
+ missingInformation
1083
+ },
1084
+ nearestLocation,
1085
+ biomarkerMatches: biomarkerMatches.length > 0 ? biomarkerMatches : undefined
1086
+ };
1087
+ }
1088
+
1089
+ private calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
1090
+ // Haversine formula to calculate distance between two points
1091
+ const R = 3959; // Earth's radius in miles
1092
+ const dLat = this.toRadians(lat2 - lat1);
1093
+ const dLon = this.toRadians(lon2 - lon1);
1094
+ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
1095
+ Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
1096
+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
1097
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1098
+ return R * c;
1099
+ }
1100
+
1101
+ private toRadians(degrees: number): number {
1102
+ return degrees * (Math.PI / 180);
1103
+ }
1104
+
1105
+ private async httpRequest(url: string): Promise<string> {
1106
+ const response = await fetch(url, {
1107
+ method: 'GET',
1108
+ headers: {
1109
+ 'Accept': 'application/json'
1110
+ },
1111
+ signal: AbortSignal.timeout(this.timeout)
1112
+ });
1113
+
1114
+ if (!response.ok) {
1115
+ throw new Error(`ClinicalTrials.gov API error: ${response.status} ${response.statusText}`);
1116
+ }
1117
+
1118
+ return await response.text();
1119
+ }
1120
+
1121
+ private getFromCache(key: string): any | null {
1122
+ const cached = this.cache.get(key);
1123
+ if (cached && cached.expiry > new Date()) {
1124
+ return cached.data;
1125
+ }
1126
+ this.cache.delete(key);
1127
+ return null;
1128
+ }
1129
+
1130
+ private setCache(key: string, data: any): void {
1131
+ const expiry = new Date(Date.now() + this.cacheDuration * 60 * 1000);
1132
+ this.cache.set(key, { data, expiry });
1133
+ }
1134
+
1135
+ /**
1136
+ * Clear the cache
1137
+ */
1138
+ clearCache(): void {
1139
+ this.cache.clear();
1140
+ }
1141
+ }
1142
+
1143
+ export default ClinicalTrialsGovClient;