/** * ClinicalTrials.gov Integration * * Provides real-time access to clinical trial data from ClinicalTrials.gov * using the official API v2. Enables matching patients to eligible trials * based on their cancer type, biomarkers, and treatment history. */ import { EventEmitter } from 'events'; // ═══════════════════════════════════════════════════════════════════════════════ // CLINICAL TRIAL TYPES // ═══════════════════════════════════════════════════════════════════════════════ export interface ClinicalTrial { nctId: string; title: string; briefTitle?: string; officialTitle?: string; status: TrialStatus; phase: TrialPhase; studyType: 'interventional' | 'observational' | 'expanded-access'; conditions: string[]; interventions: TrialIntervention[]; eligibility: TrialEligibility; locations: TrialLocation[]; sponsors: TrialSponsor[]; contacts: TrialContact[]; dates: { startDate?: Date; primaryCompletionDate?: Date; completionDate?: Date; firstPostedDate?: Date; lastUpdatePostedDate?: Date; }; enrollment?: { count: number; type: 'actual' | 'anticipated'; }; arms?: TrialArm[]; outcomes?: TrialOutcome[]; biomarkerRequirements?: BiomarkerRequirement[]; references?: string[]; url: string; } export type TrialStatus = | 'not-yet-recruiting' | 'recruiting' | 'enrolling-by-invitation' | 'active-not-recruiting' | 'suspended' | 'terminated' | 'completed' | 'withdrawn' | 'unknown'; export type TrialPhase = | 'early-phase-1' | 'phase-1' | 'phase-1-2' | 'phase-2' | 'phase-2-3' | 'phase-3' | 'phase-4' | 'not-applicable'; export interface TrialIntervention { type: 'drug' | 'biological' | 'device' | 'procedure' | 'radiation' | 'behavioral' | 'genetic' | 'dietary' | 'combination' | 'other'; name: string; description?: string; armGroupLabels?: string[]; otherNames?: string[]; } export interface TrialEligibility { criteria: string; gender: 'all' | 'female' | 'male'; minimumAge?: string; maximumAge?: string; healthyVolunteers: boolean; inclusionCriteria?: string[]; exclusionCriteria?: string[]; } export interface TrialLocation { facility: string; city: string; state?: string; country: string; zip?: string; status?: 'recruiting' | 'not-recruiting' | 'withdrawn' | 'active' | 'completed'; contact?: { name?: string; phone?: string; email?: string; }; coordinates?: { latitude: number; longitude: number; }; distance?: number; // Distance from patient in miles } export interface TrialSponsor { name: string; type: 'principal-investigator' | 'sponsor' | 'sponsor-investigator'; leadOrCollaborator: 'lead' | 'collaborator'; } export interface TrialContact { name?: string; phone?: string; email?: string; role?: string; } export interface TrialArm { label: string; type: 'experimental' | 'active-comparator' | 'placebo-comparator' | 'sham-comparator' | 'no-intervention' | 'other'; description?: string; interventions?: string[]; } export interface TrialOutcome { type: 'primary' | 'secondary' | 'other'; measure: string; description?: string; timeFrame?: string; } export interface BiomarkerRequirement { biomarker: string; requirement: 'required' | 'excluded' | 'preferred'; value?: string; operator?: 'equals' | 'greater-than' | 'less-than' | 'between'; } // ═══════════════════════════════════════════════════════════════════════════════ // SEARCH PARAMETERS // ═══════════════════════════════════════════════════════════════════════════════ export interface TrialSearchParams { // Disease/condition condition?: string; conditionTerms?: string[]; // Intervention intervention?: string; interventionType?: TrialIntervention['type']; drugName?: string; // Biomarkers biomarkers?: string[]; genomicAlterations?: string[]; // Status status?: TrialStatus[]; phase?: TrialPhase[]; // Location country?: string; state?: string; city?: string; zipCode?: string; distance?: number; // miles from zipCode coordinates?: { lat: number; lon: number }; // Demographics age?: number; gender?: 'male' | 'female'; // Other sponsorType?: 'industry' | 'academic' | 'government' | 'other'; funderType?: 'nih' | 'industry' | 'other'; studyType?: 'interventional' | 'observational' | 'expanded-access'; hasResults?: boolean; // Pagination pageSize?: number; pageToken?: string; } export interface TrialSearchResult { trials: ClinicalTrial[]; totalCount: number; nextPageToken?: string; } // ═══════════════════════════════════════════════════════════════════════════════ // PATIENT MATCHING // ═══════════════════════════════════════════════════════════════════════════════ export interface PatientProfile { cancerType: string; stage?: string; age?: number; gender?: 'male' | 'female'; ecogStatus?: number; biomarkers?: { name: string; value: string | number; status?: 'positive' | 'negative'; }[]; genomicAlterations?: { gene: string; alteration: string; type: 'mutation' | 'fusion' | 'amplification' | 'deletion'; }[]; msiStatus?: 'MSI-H' | 'MSI-L' | 'MSS'; tmbLevel?: 'high' | 'low'; pdl1Score?: number; hrdStatus?: 'positive' | 'negative'; priorTherapies?: string[]; comorbidities?: string[]; location?: { zipCode?: string; city?: string; state?: string; country?: string; coordinates?: { lat: number; lon: number }; }; maxTravelDistance?: number; // miles } export interface TrialMatch { trial: ClinicalTrial; matchScore: number; matchReasons: string[]; eligibilityAssessment: { status: 'likely-eligible' | 'possibly-eligible' | 'likely-ineligible' | 'unknown'; matchingCriteria: string[]; potentialExclusions: string[]; missingInformation: string[]; }; nearestLocation?: TrialLocation; biomarkerMatches?: { biomarker: string; trialRequirement: string; patientValue: string; match: boolean; }[]; } // ═══════════════════════════════════════════════════════════════════════════════ // CLINICAL TRIALS API CLIENT // ═══════════════════════════════════════════════════════════════════════════════ export class ClinicalTrialsGovClient extends EventEmitter { private baseUrl = 'https://clinicaltrials.gov/api/v2'; private timeout: number; private cache: Map = new Map(); private cacheDuration: number; // minutes constructor(options?: { timeout?: number; cacheDuration?: number }) { super(); this.timeout = options?.timeout || 30000; this.cacheDuration = options?.cacheDuration || 60; // 1 hour default } /** * Search for clinical trials */ async searchTrials(params: TrialSearchParams): Promise { const cacheKey = JSON.stringify(params); const cached = this.getFromCache(cacheKey); if (cached) return cached; const query = this.buildSearchQuery(params); const url = `${this.baseUrl}/studies?${query}`; const response = await this.httpRequest(url); const data = JSON.parse(response); const result: TrialSearchResult = { trials: (data.studies || []).map((s: any) => this.mapStudyToTrial(s)), totalCount: data.totalCount || 0, nextPageToken: data.nextPageToken }; this.setCache(cacheKey, result); return result; } /** * Get a specific trial by NCT ID */ async getTrial(nctId: string): Promise { const cacheKey = `trial:${nctId}`; const cached = this.getFromCache(cacheKey); if (cached) return cached; const url = `${this.baseUrl}/studies/${nctId}`; const response = await this.httpRequest(url); const data = JSON.parse(response); const trial = this.mapStudyToTrial(data); this.setCache(cacheKey, trial); return trial; } /** * Get multiple trials by NCT IDs */ async getTrials(nctIds: string[]): Promise { return Promise.all(nctIds.map(id => this.getTrial(id))); } /** * Search for trials matching a patient profile */ async findMatchingTrials(patient: PatientProfile): Promise { // Build search query based on patient profile const searchParams: TrialSearchParams = { condition: patient.cancerType, status: ['recruiting', 'enrolling-by-invitation', 'not-yet-recruiting'], studyType: 'interventional', age: patient.age, gender: patient.gender, pageSize: 100 }; // Add location-based filtering if (patient.location?.zipCode) { searchParams.zipCode = patient.location.zipCode; searchParams.distance = patient.maxTravelDistance || 100; } else if (patient.location?.country) { searchParams.country = patient.location.country; if (patient.location.state) { searchParams.state = patient.location.state; } } // Add biomarker-specific searches if (patient.genomicAlterations && patient.genomicAlterations.length > 0) { searchParams.genomicAlterations = patient.genomicAlterations.map(g => `${g.gene} ${g.alteration}`); } // Execute search const searchResult = await this.searchTrials(searchParams); // Score and filter trials const matches: TrialMatch[] = []; for (const trial of searchResult.trials) { const match = this.assessTrialMatch(trial, patient); if (match.matchScore > 0) { matches.push(match); } } // Sort by match score matches.sort((a, b) => b.matchScore - a.matchScore); return matches; } /** * Search for trials by biomarker */ async searchByBiomarker(biomarker: string, options?: { cancerType?: string; status?: TrialStatus[]; phase?: TrialPhase[]; }): Promise { const params: TrialSearchParams = { biomarkers: [biomarker], condition: options?.cancerType, status: options?.status || ['recruiting', 'not-yet-recruiting'], phase: options?.phase, studyType: 'interventional', pageSize: 50 }; const result = await this.searchTrials(params); return result.trials; } /** * Search for trials by drug/intervention */ async searchByDrug(drugName: string, options?: { cancerType?: string; status?: TrialStatus[]; phase?: TrialPhase[]; }): Promise { const params: TrialSearchParams = { drugName, condition: options?.cancerType, status: options?.status || ['recruiting', 'not-yet-recruiting'], phase: options?.phase, studyType: 'interventional', pageSize: 50 }; const result = await this.searchTrials(params); return result.trials; } /** * Get trials for a specific cancer type */ async getTrialsForCancerType(cancerType: string, options?: { phase?: TrialPhase[]; location?: { country?: string; state?: string }; biomarkers?: string[]; }): Promise { const params: TrialSearchParams = { condition: cancerType, status: ['recruiting', 'not-yet-recruiting', 'enrolling-by-invitation'], studyType: 'interventional', phase: options?.phase, country: options?.location?.country, state: options?.location?.state, biomarkers: options?.biomarkers, pageSize: 100 }; const result = await this.searchTrials(params); return result.trials; } // ═══════════════════════════════════════════════════════════════════════════════ // HELPER METHODS // ═══════════════════════════════════════════════════════════════════════════════ private buildSearchQuery(params: TrialSearchParams): string { const queryParts: string[] = []; // Condition/disease if (params.condition) { queryParts.push(`query.cond=${encodeURIComponent(params.condition)}`); } if (params.conditionTerms && params.conditionTerms.length > 0) { queryParts.push(`query.term=${encodeURIComponent(params.conditionTerms.join(' OR '))}`); } // Intervention/drug if (params.intervention) { queryParts.push(`query.intr=${encodeURIComponent(params.intervention)}`); } if (params.drugName) { queryParts.push(`query.intr=${encodeURIComponent(params.drugName)}`); } // Biomarkers/genomic alterations (search in full text) if (params.biomarkers && params.biomarkers.length > 0) { const biomarkerQuery = params.biomarkers.join(' OR '); queryParts.push(`query.term=${encodeURIComponent(biomarkerQuery)}`); } if (params.genomicAlterations && params.genomicAlterations.length > 0) { const genomicQuery = params.genomicAlterations.join(' OR '); queryParts.push(`query.term=${encodeURIComponent(genomicQuery)}`); } // Status filter if (params.status && params.status.length > 0) { const statusMap: Record = { 'not-yet-recruiting': 'NOT_YET_RECRUITING', 'recruiting': 'RECRUITING', 'enrolling-by-invitation': 'ENROLLING_BY_INVITATION', 'active-not-recruiting': 'ACTIVE_NOT_RECRUITING', 'suspended': 'SUSPENDED', 'terminated': 'TERMINATED', 'completed': 'COMPLETED', 'withdrawn': 'WITHDRAWN', 'unknown': 'UNKNOWN' }; const statuses = params.status.map(s => statusMap[s]).join(','); queryParts.push(`filter.overallStatus=${statuses}`); } // Phase filter if (params.phase && params.phase.length > 0) { const phaseMap: Record = { 'early-phase-1': 'EARLY_PHASE1', 'phase-1': 'PHASE1', 'phase-1-2': 'PHASE1_PHASE2', 'phase-2': 'PHASE2', 'phase-2-3': 'PHASE2_PHASE3', 'phase-3': 'PHASE3', 'phase-4': 'PHASE4', 'not-applicable': 'NA' }; const phases = params.phase.map(p => phaseMap[p]).join(','); queryParts.push(`filter.phase=${phases}`); } // Study type if (params.studyType) { const studyTypeMap: Record = { 'interventional': 'INTERVENTIONAL', 'observational': 'OBSERVATIONAL', 'expanded-access': 'EXPANDED_ACCESS' }; queryParts.push(`filter.studyType=${studyTypeMap[params.studyType]}`); } // Location filters if (params.country) { queryParts.push(`query.locn=${encodeURIComponent(params.country)}`); } if (params.state) { queryParts.push(`query.locn=${encodeURIComponent(params.state)}`); } if (params.city) { queryParts.push(`query.locn=${encodeURIComponent(params.city)}`); } // Geographic search (if zip code and distance provided) if (params.zipCode && params.distance) { queryParts.push(`postFilter.geo=distance(${params.zipCode},${params.distance}mi)`); } // Age filter if (params.age) { queryParts.push(`aggFilters=ages:adult`); // Simplified - would need more logic } // Gender filter if (params.gender) { queryParts.push(`filter.sex=${params.gender.toUpperCase()}`); } // Results filter if (params.hasResults !== undefined) { queryParts.push(`filter.results=${params.hasResults}`); } // Pagination queryParts.push(`pageSize=${params.pageSize || 20}`); if (params.pageToken) { queryParts.push(`pageToken=${params.pageToken}`); } // Request all needed fields queryParts.push('fields=NCTId,BriefTitle,OfficialTitle,OverallStatus,Phase,StudyType,Condition,' + 'InterventionName,InterventionType,InterventionDescription,EligibilityCriteria,' + 'MinimumAge,MaximumAge,Gender,LocationFacility,LocationCity,LocationState,LocationCountry,' + 'LeadSponsorName,StartDate,PrimaryCompletionDate,EnrollmentCount,ArmGroupLabel,' + 'ArmGroupType,PrimaryOutcomeMeasure,SecondaryOutcomeMeasure,CentralContactName,' + 'CentralContactPhone,CentralContactEMail,BriefSummary,DetailedDescription'); return queryParts.join('&'); } private mapStudyToTrial(study: any): ClinicalTrial { const protocol = study.protocolSection || study; const identification = protocol.identificationModule || {}; const status = protocol.statusModule || {}; const description = protocol.descriptionModule || {}; const conditions = protocol.conditionsModule || {}; const design = protocol.designModule || {}; const arms = protocol.armsInterventionsModule || {}; const eligibility = protocol.eligibilityModule || {}; const contacts = protocol.contactsLocationsModule || {}; const sponsor = protocol.sponsorCollaboratorsModule || {}; const outcomes = protocol.outcomesModule || {}; // Map interventions const interventions: TrialIntervention[] = (arms.interventions || []).map((i: any) => ({ type: this.mapInterventionType(i.type), name: i.name, description: i.description, armGroupLabels: i.armGroupLabels, otherNames: i.otherNames })); // Map locations const locations: TrialLocation[] = (contacts.locations || []).map((loc: any) => ({ facility: loc.facility, city: loc.city, state: loc.state, country: loc.country, zip: loc.zip, status: this.mapLocationStatus(loc.status), contact: loc.contacts?.[0] ? { name: loc.contacts[0].name, phone: loc.contacts[0].phone, email: loc.contacts[0].email } : undefined, coordinates: loc.geoPoint ? { latitude: loc.geoPoint.lat, longitude: loc.geoPoint.lon } : undefined })); // Map sponsors const sponsors: TrialSponsor[] = []; if (sponsor.leadSponsor) { sponsors.push({ name: sponsor.leadSponsor.name, type: 'sponsor', leadOrCollaborator: 'lead' }); } for (const collab of sponsor.collaborators || []) { sponsors.push({ name: collab.name, type: 'sponsor', leadOrCollaborator: 'collaborator' }); } // Map contacts const trialContacts: TrialContact[] = (contacts.centralContacts || []).map((c: any) => ({ name: c.name, phone: c.phone, email: c.email, role: c.role })); // Map arms const trialArms: TrialArm[] = (arms.armGroups || []).map((arm: any) => ({ label: arm.label, type: this.mapArmType(arm.type), description: arm.description, interventions: arm.interventionNames })); // Map outcomes const trialOutcomes: TrialOutcome[] = [ ...(outcomes.primaryOutcomes || []).map((o: any) => ({ type: 'primary' as const, measure: o.measure, description: o.description, timeFrame: o.timeFrame })), ...(outcomes.secondaryOutcomes || []).map((o: any) => ({ type: 'secondary' as const, measure: o.measure, description: o.description, timeFrame: o.timeFrame })) ]; // Parse eligibility criteria into structured format const criteriaText = eligibility.eligibilityCriteria || ''; const { inclusion, exclusion } = this.parseEligibilityCriteria(criteriaText); // Extract biomarker requirements from criteria const biomarkerRequirements = this.extractBiomarkerRequirements(criteriaText, interventions); return { nctId: identification.nctId, title: identification.briefTitle || identification.officialTitle || '', briefTitle: identification.briefTitle, officialTitle: identification.officialTitle, status: this.mapTrialStatus(status.overallStatus), phase: this.mapTrialPhase(design.phases?.[0]), studyType: this.mapStudyType(design.studyType), conditions: conditions.conditions || [], interventions, eligibility: { criteria: criteriaText, gender: this.mapGender(eligibility.sex), minimumAge: eligibility.minimumAge, maximumAge: eligibility.maximumAge, healthyVolunteers: eligibility.healthyVolunteers === 'Yes', inclusionCriteria: inclusion, exclusionCriteria: exclusion }, locations, sponsors, contacts: trialContacts, dates: { startDate: status.startDateStruct ? new Date(status.startDateStruct.date) : undefined, primaryCompletionDate: status.primaryCompletionDateStruct ? new Date(status.primaryCompletionDateStruct.date) : undefined, completionDate: status.completionDateStruct ? new Date(status.completionDateStruct.date) : undefined, firstPostedDate: status.studyFirstPostDateStruct ? new Date(status.studyFirstPostDateStruct.date) : undefined, lastUpdatePostedDate: status.lastUpdatePostDateStruct ? new Date(status.lastUpdatePostDateStruct.date) : undefined }, enrollment: design.enrollmentInfo ? { count: design.enrollmentInfo.count, type: design.enrollmentInfo.type?.toLowerCase() as 'actual' | 'anticipated' } : undefined, arms: trialArms.length > 0 ? trialArms : undefined, outcomes: trialOutcomes.length > 0 ? trialOutcomes : undefined, biomarkerRequirements: biomarkerRequirements.length > 0 ? biomarkerRequirements : undefined, url: `https://clinicaltrials.gov/study/${identification.nctId}` }; } private mapTrialStatus(status: string): TrialStatus { const statusMap: Record = { 'NOT_YET_RECRUITING': 'not-yet-recruiting', 'RECRUITING': 'recruiting', 'ENROLLING_BY_INVITATION': 'enrolling-by-invitation', 'ACTIVE_NOT_RECRUITING': 'active-not-recruiting', 'SUSPENDED': 'suspended', 'TERMINATED': 'terminated', 'COMPLETED': 'completed', 'WITHDRAWN': 'withdrawn' }; return statusMap[status] || 'unknown'; } private mapTrialPhase(phase: string): TrialPhase { const phaseMap: Record = { 'EARLY_PHASE1': 'early-phase-1', 'PHASE1': 'phase-1', 'PHASE1_PHASE2': 'phase-1-2', 'PHASE2': 'phase-2', 'PHASE2_PHASE3': 'phase-2-3', 'PHASE3': 'phase-3', 'PHASE4': 'phase-4', 'NA': 'not-applicable' }; return phaseMap[phase] || 'not-applicable'; } private mapStudyType(type: string): 'interventional' | 'observational' | 'expanded-access' { const typeMap: Record = { 'INTERVENTIONAL': 'interventional', 'OBSERVATIONAL': 'observational', 'EXPANDED_ACCESS': 'expanded-access' }; return typeMap[type] || 'interventional'; } private mapInterventionType(type: string): TrialIntervention['type'] { const typeMap: Record = { 'DRUG': 'drug', 'BIOLOGICAL': 'biological', 'DEVICE': 'device', 'PROCEDURE': 'procedure', 'RADIATION': 'radiation', 'BEHAVIORAL': 'behavioral', 'GENETIC': 'genetic', 'DIETARY_SUPPLEMENT': 'dietary', 'COMBINATION_PRODUCT': 'combination', 'OTHER': 'other' }; return typeMap[type] || 'other'; } private mapLocationStatus(status: string): TrialLocation['status'] { const statusMap: Record = { 'RECRUITING': 'recruiting', 'NOT_YET_RECRUITING': 'not-recruiting', 'ACTIVE_NOT_RECRUITING': 'active', 'COMPLETED': 'completed', 'WITHDRAWN': 'withdrawn' }; return statusMap[status] || 'active'; } private mapArmType(type: string): TrialArm['type'] { const typeMap: Record = { 'EXPERIMENTAL': 'experimental', 'ACTIVE_COMPARATOR': 'active-comparator', 'PLACEBO_COMPARATOR': 'placebo-comparator', 'SHAM_COMPARATOR': 'sham-comparator', 'NO_INTERVENTION': 'no-intervention', 'OTHER': 'other' }; return typeMap[type] || 'other'; } private mapGender(sex: string): 'all' | 'female' | 'male' { if (sex === 'FEMALE') return 'female'; if (sex === 'MALE') return 'male'; return 'all'; } private parseEligibilityCriteria(criteria: string): { inclusion: string[]; exclusion: string[] } { const inclusion: string[] = []; const exclusion: string[] = []; if (!criteria) return { inclusion, exclusion }; // Try to split by Inclusion/Exclusion headers const sections = criteria.split(/(?:Inclusion|Exclusion)\s*Criteria:?/i); // Simple parsing - look for patterns const lines = criteria.split(/\n|•|·|-\s+|\*\s+|\d+\.\s+/); let inExclusion = false; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; // Detect section changes if (trimmed.toLowerCase().includes('exclusion')) { inExclusion = true; continue; } if (trimmed.toLowerCase().includes('inclusion')) { inExclusion = false; continue; } // Add to appropriate list if (trimmed.length > 10) { // Filter out very short fragments if (inExclusion) { exclusion.push(trimmed); } else { inclusion.push(trimmed); } } } return { inclusion, exclusion }; } private extractBiomarkerRequirements(criteria: string, interventions: TrialIntervention[]): BiomarkerRequirement[] { const requirements: BiomarkerRequirement[] = []; const criteriaLower = criteria.toLowerCase(); // Common biomarker patterns const biomarkerPatterns: { pattern: RegExp; biomarker: string }[] = [ { pattern: /egfr\s*(mutation|mutant|positive|\+)/i, biomarker: 'EGFR mutation' }, { pattern: /egfr\s*(wild[- ]?type|negative|wt)/i, biomarker: 'EGFR wild-type' }, { pattern: /alk\s*(rearrangement|fusion|positive|\+)/i, biomarker: 'ALK fusion' }, { pattern: /ros1\s*(rearrangement|fusion|positive|\+)/i, biomarker: 'ROS1 fusion' }, { pattern: /braf\s*v600[ek]?/i, biomarker: 'BRAF V600' }, { pattern: /kras\s*g12c/i, biomarker: 'KRAS G12C' }, { pattern: /kras\s*(mutation|mutant|positive)/i, biomarker: 'KRAS mutation' }, { pattern: /her2\s*(positive|overexpression|amplification|\+|3\+)/i, biomarker: 'HER2 positive' }, { pattern: /her2\s*(negative|\-|0|1\+)/i, biomarker: 'HER2 negative' }, { pattern: /brca[12]?\s*(mutation|mutant|positive|pathogenic)/i, biomarker: 'BRCA mutation' }, { pattern: /msi[- ]?h(igh)?|microsatellite\s*instability[- ]?high/i, biomarker: 'MSI-H' }, { pattern: /mss|microsatellite\s*stable/i, biomarker: 'MSS' }, { pattern: /pd[- ]?l1\s*(positive|expression|tps|cps)/i, biomarker: 'PD-L1 positive' }, { pattern: /pd[- ]?l1\s*[\u2265>=]\s*(\d+)/i, biomarker: 'PD-L1' }, { pattern: /tmb[- ]?h(igh)?|tumor\s*mutational\s*burden[- ]?high/i, biomarker: 'TMB-H' }, { pattern: /hrd\s*(positive|deficient)/i, biomarker: 'HRD positive' }, { pattern: /ntrk\s*(fusion|rearrangement)/i, biomarker: 'NTRK fusion' }, { pattern: /ret\s*(fusion|rearrangement|mutation)/i, biomarker: 'RET alteration' }, { pattern: /met\s*(exon\s*14|amplification)/i, biomarker: 'MET alteration' }, { pattern: /fgfr[1234]?\s*(alteration|fusion|mutation|amplification)/i, biomarker: 'FGFR alteration' }, { pattern: /pik3ca\s*(mutation|mutant)/i, biomarker: 'PIK3CA mutation' }, { pattern: /idh[12]\s*(mutation|mutant)/i, biomarker: 'IDH mutation' } ]; for (const { pattern, biomarker } of biomarkerPatterns) { if (pattern.test(criteria)) { // Determine if it's required or excluded based on context const match = criteria.match(new RegExp(`.{0,50}${pattern.source}.{0,50}`, 'i')); if (match) { const context = match[0].toLowerCase(); let requirement: BiomarkerRequirement['requirement'] = 'required'; if (context.includes('exclud') || context.includes('must not') || context.includes('no ') || context.includes('without') || context.includes('ineligible')) { requirement = 'excluded'; } requirements.push({ biomarker, requirement }); } } } return requirements; } private assessTrialMatch(trial: ClinicalTrial, patient: PatientProfile): TrialMatch { let score = 0; const matchReasons: string[] = []; const matchingCriteria: string[] = []; const potentialExclusions: string[] = []; const missingInformation: string[] = []; const biomarkerMatches: TrialMatch['biomarkerMatches'] = []; // Check cancer type match const cancerMatch = trial.conditions.some(c => c.toLowerCase().includes(patient.cancerType.toLowerCase()) || patient.cancerType.toLowerCase().includes(c.toLowerCase()) ); if (cancerMatch) { score += 30; matchReasons.push(`Cancer type matches: ${patient.cancerType}`); matchingCriteria.push('Cancer type'); } // Check biomarker requirements if (trial.biomarkerRequirements && patient.genomicAlterations) { for (const req of trial.biomarkerRequirements) { const patientHasBiomarker = patient.genomicAlterations.some(g => req.biomarker.toLowerCase().includes(g.gene.toLowerCase()) || req.biomarker.toLowerCase().includes(g.alteration.toLowerCase()) ); if (req.requirement === 'required') { if (patientHasBiomarker) { score += 25; matchReasons.push(`Has required biomarker: ${req.biomarker}`); matchingCriteria.push(req.biomarker); biomarkerMatches.push({ biomarker: req.biomarker, trialRequirement: 'Required', patientValue: 'Present', match: true }); } else { score -= 10; potentialExclusions.push(`Missing required biomarker: ${req.biomarker}`); biomarkerMatches.push({ biomarker: req.biomarker, trialRequirement: 'Required', patientValue: 'Not detected', match: false }); } } else if (req.requirement === 'excluded') { if (patientHasBiomarker) { score -= 50; potentialExclusions.push(`Has excluded biomarker: ${req.biomarker}`); biomarkerMatches.push({ biomarker: req.biomarker, trialRequirement: 'Excluded', patientValue: 'Present', match: false }); } } } } // Check MSI status if (patient.msiStatus) { const trialMentionsMSI = trial.eligibility.criteria.toLowerCase().includes('msi'); if (trialMentionsMSI) { if (patient.msiStatus === 'MSI-H' && trial.eligibility.criteria.toLowerCase().includes('msi-h')) { score += 20; matchReasons.push('MSI-H status matches trial requirement'); matchingCriteria.push('MSI-H'); } } } // Check TMB status if (patient.tmbLevel === 'high') { const trialMentionsTMB = trial.eligibility.criteria.toLowerCase().includes('tmb'); if (trialMentionsTMB) { score += 15; matchReasons.push('TMB-High may enhance eligibility'); matchingCriteria.push('TMB-H'); } } // Check age eligibility if (patient.age) { let ageEligible = true; if (trial.eligibility.minimumAge) { const minAge = parseInt(trial.eligibility.minimumAge); if (!isNaN(minAge) && patient.age < minAge) { ageEligible = false; potentialExclusions.push(`Below minimum age (${trial.eligibility.minimumAge})`); } } if (trial.eligibility.maximumAge && trial.eligibility.maximumAge !== 'N/A') { const maxAge = parseInt(trial.eligibility.maximumAge); if (!isNaN(maxAge) && patient.age > maxAge) { ageEligible = false; potentialExclusions.push(`Above maximum age (${trial.eligibility.maximumAge})`); } } if (ageEligible) { score += 5; matchingCriteria.push('Age'); } else { score -= 30; } } else { missingInformation.push('Patient age'); } // Check gender eligibility if (patient.gender) { if (trial.eligibility.gender === 'all' || trial.eligibility.gender === patient.gender) { matchingCriteria.push('Gender'); } else { score -= 50; potentialExclusions.push(`Trial only accepts ${trial.eligibility.gender} patients`); } } // Check prior therapy exclusions if (patient.priorTherapies && patient.priorTherapies.length > 0) { const criteriaLower = trial.eligibility.criteria.toLowerCase(); for (const therapy of patient.priorTherapies) { if (criteriaLower.includes(`no prior ${therapy.toLowerCase()}`) || criteriaLower.includes(`not received ${therapy.toLowerCase()}`)) { potentialExclusions.push(`Prior ${therapy} may exclude patient`); score -= 10; } } } // Check ECOG status if (patient.ecogStatus !== undefined) { const ecogMatch = trial.eligibility.criteria.match(/ecog\s*(?:performance\s*status)?\s*(?:of\s*)?(\d)(?:\s*(?:or|to|-)\s*(\d))?/i); if (ecogMatch) { const maxEcog = parseInt(ecogMatch[2] || ecogMatch[1]); if (patient.ecogStatus <= maxEcog) { matchingCriteria.push(`ECOG ${patient.ecogStatus}`); } else { potentialExclusions.push(`ECOG ${patient.ecogStatus} may be too high (trial requires ≤${maxEcog})`); score -= 20; } } } else { missingInformation.push('ECOG performance status'); } // Boost score for phase based on patient preference if (trial.phase === 'phase-3') { score += 10; matchReasons.push('Phase 3 trial (more established efficacy data)'); } else if (trial.phase === 'phase-2') { score += 5; } // Find nearest location let nearestLocation: TrialLocation | undefined; if (patient.location) { const recruitingLocations = trial.locations.filter(l => l.status === 'recruiting'); if (patient.location.coordinates && recruitingLocations.some(l => l.coordinates)) { // Calculate distances for (const loc of recruitingLocations) { if (loc.coordinates) { loc.distance = this.calculateDistance( patient.location.coordinates.lat, patient.location.coordinates.lon, loc.coordinates.latitude, loc.coordinates.longitude ); } } recruitingLocations.sort((a, b) => (a.distance || 999999) - (b.distance || 999999)); nearestLocation = recruitingLocations[0]; if (nearestLocation?.distance && patient.maxTravelDistance) { if (nearestLocation.distance <= patient.maxTravelDistance) { score += 10; matchReasons.push(`Trial site within ${Math.round(nearestLocation.distance)} miles`); } else { score -= 5; matchReasons.push(`Nearest site is ${Math.round(nearestLocation.distance)} miles away`); } } } else if (patient.location.state) { nearestLocation = recruitingLocations.find(l => l.state?.toLowerCase() === patient.location?.state?.toLowerCase() ); if (nearestLocation) { score += 5; matchReasons.push(`Trial site in ${patient.location.state}`); } } } // Determine eligibility status let eligibilityStatus: TrialMatch['eligibilityAssessment']['status']; if (score >= 50 && potentialExclusions.length === 0) { eligibilityStatus = 'likely-eligible'; } else if (score >= 30 && potentialExclusions.length <= 1) { eligibilityStatus = 'possibly-eligible'; } else if (score < 0 || potentialExclusions.length >= 3) { eligibilityStatus = 'likely-ineligible'; } else { eligibilityStatus = 'unknown'; } // Ensure minimum score of 0 score = Math.max(0, score); return { trial, matchScore: score, matchReasons, eligibilityAssessment: { status: eligibilityStatus, matchingCriteria, potentialExclusions, missingInformation }, nearestLocation, biomarkerMatches: biomarkerMatches.length > 0 ? biomarkerMatches : undefined }; } private calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { // Haversine formula to calculate distance between two points const R = 3959; // Earth's radius in miles const dLat = this.toRadians(lat2 - lat1); const dLon = this.toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } private toRadians(degrees: number): number { return degrees * (Math.PI / 180); } private async httpRequest(url: string): Promise { const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json' }, signal: AbortSignal.timeout(this.timeout) }); if (!response.ok) { throw new Error(`ClinicalTrials.gov API error: ${response.status} ${response.statusText}`); } return await response.text(); } private getFromCache(key: string): any | null { const cached = this.cache.get(key); if (cached && cached.expiry > new Date()) { return cached.data; } this.cache.delete(key); return null; } private setCache(key: string, data: any): void { const expiry = new Date(Date.now() + this.cacheDuration * 60 * 1000); this.cache.set(key, { data, expiry }); } /** * Clear the cache */ clearCache(): void { this.cache.clear(); } } export default ClinicalTrialsGovClient;