@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.
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/bin/cure.d.ts +10 -0
- package/dist/bin/cure.d.ts.map +1 -0
- package/dist/bin/cure.js +169 -0
- package/dist/bin/cure.js.map +1 -0
- package/dist/capabilities/cancerTreatmentCapability.d.ts +167 -0
- package/dist/capabilities/cancerTreatmentCapability.d.ts.map +1 -0
- package/dist/capabilities/cancerTreatmentCapability.js +912 -0
- package/dist/capabilities/cancerTreatmentCapability.js.map +1 -0
- package/dist/capabilities/index.d.ts +2 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +3 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/compliance/hipaa.d.ts +337 -0
- package/dist/compliance/hipaa.d.ts.map +1 -0
- package/dist/compliance/hipaa.js +929 -0
- package/dist/compliance/hipaa.js.map +1 -0
- package/dist/examples/cancerTreatmentDemo.d.ts +21 -0
- package/dist/examples/cancerTreatmentDemo.d.ts.map +1 -0
- package/dist/examples/cancerTreatmentDemo.js +216 -0
- package/dist/examples/cancerTreatmentDemo.js.map +1 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts +265 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts.map +1 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.js +808 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.js.map +1 -0
- package/dist/integrations/ehr/fhir.d.ts +455 -0
- package/dist/integrations/ehr/fhir.d.ts.map +1 -0
- package/dist/integrations/ehr/fhir.js +859 -0
- package/dist/integrations/ehr/fhir.js.map +1 -0
- package/dist/integrations/genomics/genomicPlatforms.d.ts +362 -0
- package/dist/integrations/genomics/genomicPlatforms.d.ts.map +1 -0
- package/dist/integrations/genomics/genomicPlatforms.js +1079 -0
- package/dist/integrations/genomics/genomicPlatforms.js.map +1 -0
- package/package.json +52 -0
- package/src/bin/cure.ts +182 -0
- package/src/capabilities/cancerTreatmentCapability.ts +1161 -0
- package/src/capabilities/index.ts +2 -0
- package/src/compliance/hipaa.ts +1365 -0
- package/src/examples/cancerTreatmentDemo.ts +241 -0
- package/src/integrations/clinicalTrials/clinicalTrialsGov.ts +1143 -0
- package/src/integrations/ehr/fhir.ts +1304 -0
- package/src/integrations/genomics/genomicPlatforms.ts +1480 -0
- package/src/ml/outcomePredictor.ts +1301 -0
- package/src/safety/drugInteractions.ts +942 -0
- 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;
|