@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,1480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genomic Testing Platform Integrations
|
|
3
|
+
*
|
|
4
|
+
* Connects to major genomic testing providers:
|
|
5
|
+
* - Foundation Medicine (FoundationOne CDx, FoundationOne Liquid CDx)
|
|
6
|
+
* - Guardant Health (Guardant360, GuardantOMNI)
|
|
7
|
+
* - Tempus (xT, xF, xR panels)
|
|
8
|
+
* - Caris Life Sciences (Caris Molecular Intelligence)
|
|
9
|
+
* - NeoGenomics
|
|
10
|
+
*
|
|
11
|
+
* All data handling complies with HIPAA and CAP/CLIA requirements.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
17
|
+
// COMMON GENOMIC DATA TYPES
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
export interface GenomicTestOrder {
|
|
21
|
+
orderId: string;
|
|
22
|
+
patientId: string;
|
|
23
|
+
testType: string;
|
|
24
|
+
panelName: string;
|
|
25
|
+
specimenType: 'tissue' | 'blood' | 'bone-marrow' | 'other';
|
|
26
|
+
specimenId?: string;
|
|
27
|
+
orderDate: Date;
|
|
28
|
+
clinicalIndication: string;
|
|
29
|
+
icdCodes: string[];
|
|
30
|
+
orderingPhysician: {
|
|
31
|
+
name: string;
|
|
32
|
+
npi: string;
|
|
33
|
+
facility: string;
|
|
34
|
+
};
|
|
35
|
+
status: 'ordered' | 'specimen-received' | 'in-process' | 'completed' | 'failed' | 'cancelled';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GenomicTestResult {
|
|
39
|
+
reportId: string;
|
|
40
|
+
orderId: string;
|
|
41
|
+
patientId: string;
|
|
42
|
+
testType: string;
|
|
43
|
+
panelName: string;
|
|
44
|
+
specimenInfo: {
|
|
45
|
+
type: 'tissue' | 'blood' | 'bone-marrow' | 'other';
|
|
46
|
+
site?: string;
|
|
47
|
+
collectionDate: Date;
|
|
48
|
+
tumorPurity?: number;
|
|
49
|
+
cellularity?: number;
|
|
50
|
+
};
|
|
51
|
+
reportDate: Date;
|
|
52
|
+
variants: GenomicVariant[];
|
|
53
|
+
copyNumberAlterations: CopyNumberAlteration[];
|
|
54
|
+
fusions: GeneFusion[];
|
|
55
|
+
biomarkers: GenomicBiomarker[];
|
|
56
|
+
therapyMatches: TherapyMatch[];
|
|
57
|
+
clinicalTrialMatches: ClinicalTrialMatch[];
|
|
58
|
+
signatures?: {
|
|
59
|
+
microsatelliteInstability?: MSIResult;
|
|
60
|
+
tumorMutationalBurden?: TMBResult;
|
|
61
|
+
homologousRecombinationDeficiency?: HRDResult;
|
|
62
|
+
lossOfHeterozygosity?: LOHResult;
|
|
63
|
+
};
|
|
64
|
+
qualityMetrics: {
|
|
65
|
+
meanCoverage?: number;
|
|
66
|
+
percentBases100x?: number;
|
|
67
|
+
tumorFraction?: number;
|
|
68
|
+
contamination?: number;
|
|
69
|
+
mappingRate?: number;
|
|
70
|
+
};
|
|
71
|
+
reportPdf?: string; // Base64 encoded PDF
|
|
72
|
+
rawData?: {
|
|
73
|
+
vcfUrl?: string;
|
|
74
|
+
bamUrl?: string;
|
|
75
|
+
fastqUrl?: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface GenomicVariant {
|
|
80
|
+
gene: string;
|
|
81
|
+
hgvsC?: string; // cDNA change
|
|
82
|
+
hgvsP?: string; // Protein change
|
|
83
|
+
transcript?: string;
|
|
84
|
+
chromosome?: string;
|
|
85
|
+
position?: number;
|
|
86
|
+
refAllele?: string;
|
|
87
|
+
altAllele?: string;
|
|
88
|
+
variantType: 'SNV' | 'insertion' | 'deletion' | 'indel' | 'MNV' | 'complex';
|
|
89
|
+
variantAlleleFrequency: number;
|
|
90
|
+
coverage?: number;
|
|
91
|
+
zygosity?: 'heterozygous' | 'homozygous' | 'hemizygous';
|
|
92
|
+
clinicalSignificance: 'pathogenic' | 'likely-pathogenic' | 'vus' | 'likely-benign' | 'benign';
|
|
93
|
+
tier?: 'I' | 'II' | 'III' | 'IV'; // AMP/ASCO/CAP tiering
|
|
94
|
+
oncogenicity?: 'oncogenic' | 'likely-oncogenic' | 'vus' | 'likely-neutral' | 'neutral';
|
|
95
|
+
functionalEffect?: 'loss-of-function' | 'gain-of-function' | 'switch-of-function' | 'unknown';
|
|
96
|
+
somaticStatus?: 'somatic' | 'germline' | 'unknown';
|
|
97
|
+
actionability?: {
|
|
98
|
+
level: 'FDA-approved' | 'clinical-guideline' | 'clinical-evidence' | 'preclinical';
|
|
99
|
+
therapies: string[];
|
|
100
|
+
evidence: string[];
|
|
101
|
+
};
|
|
102
|
+
annotations?: {
|
|
103
|
+
cosmic?: string;
|
|
104
|
+
dbSNP?: string;
|
|
105
|
+
clinVar?: string;
|
|
106
|
+
gnomAD?: { frequency: number; popMax?: number };
|
|
107
|
+
oncokb?: { level: string; description: string };
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface CopyNumberAlteration {
|
|
112
|
+
gene: string;
|
|
113
|
+
chromosome?: string;
|
|
114
|
+
startPosition?: number;
|
|
115
|
+
endPosition?: number;
|
|
116
|
+
type: 'amplification' | 'gain' | 'loss' | 'deep-deletion';
|
|
117
|
+
copyNumber?: number;
|
|
118
|
+
logRatio?: number;
|
|
119
|
+
clinicalSignificance: 'pathogenic' | 'likely-pathogenic' | 'vus' | 'likely-benign' | 'benign';
|
|
120
|
+
actionability?: {
|
|
121
|
+
level: 'FDA-approved' | 'clinical-guideline' | 'clinical-evidence' | 'preclinical';
|
|
122
|
+
therapies: string[];
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface GeneFusion {
|
|
127
|
+
gene5Prime: string;
|
|
128
|
+
gene3Prime: string;
|
|
129
|
+
fusionName: string;
|
|
130
|
+
breakpoint5Prime?: string;
|
|
131
|
+
breakpoint3Prime?: string;
|
|
132
|
+
readsSupporting?: number;
|
|
133
|
+
inFrame?: boolean;
|
|
134
|
+
clinicalSignificance: 'pathogenic' | 'likely-pathogenic' | 'vus';
|
|
135
|
+
actionability?: {
|
|
136
|
+
level: 'FDA-approved' | 'clinical-guideline' | 'clinical-evidence' | 'preclinical';
|
|
137
|
+
therapies: string[];
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface GenomicBiomarker {
|
|
142
|
+
name: string;
|
|
143
|
+
value: number | string;
|
|
144
|
+
unit?: string;
|
|
145
|
+
status: 'positive' | 'negative' | 'equivocal' | 'indeterminate';
|
|
146
|
+
threshold?: number;
|
|
147
|
+
method?: string;
|
|
148
|
+
clinicalImplication?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface MSIResult {
|
|
152
|
+
status: 'MSI-H' | 'MSI-L' | 'MSS' | 'indeterminate';
|
|
153
|
+
score?: number;
|
|
154
|
+
markersAnalyzed?: number;
|
|
155
|
+
markersUnstable?: number;
|
|
156
|
+
method: 'NGS' | 'PCR' | 'IHC';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface TMBResult {
|
|
160
|
+
value: number;
|
|
161
|
+
unit: 'mutations/Mb';
|
|
162
|
+
status: 'high' | 'intermediate' | 'low';
|
|
163
|
+
threshold: number;
|
|
164
|
+
percentile?: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface HRDResult {
|
|
168
|
+
status: 'positive' | 'negative' | 'indeterminate';
|
|
169
|
+
score?: number;
|
|
170
|
+
components?: {
|
|
171
|
+
loh?: number;
|
|
172
|
+
tai?: number;
|
|
173
|
+
lst?: number;
|
|
174
|
+
};
|
|
175
|
+
brcaStatus?: 'BRCA1-mut' | 'BRCA2-mut' | 'BRCA-wt';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface LOHResult {
|
|
179
|
+
percentage: number;
|
|
180
|
+
status: 'high' | 'intermediate' | 'low';
|
|
181
|
+
genomeFraction?: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface TherapyMatch {
|
|
185
|
+
therapy: string;
|
|
186
|
+
drugs: string[];
|
|
187
|
+
biomarkers: string[];
|
|
188
|
+
evidenceLevel: 'FDA-approved' | 'NCCN-guideline' | 'clinical-evidence' | 'case-report' | 'preclinical';
|
|
189
|
+
cancerType: string;
|
|
190
|
+
approvalStatus?: string;
|
|
191
|
+
clinicalTrials?: string[];
|
|
192
|
+
responseRate?: number;
|
|
193
|
+
references: string[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface ClinicalTrialMatch {
|
|
197
|
+
trialId: string; // NCT number
|
|
198
|
+
title: string;
|
|
199
|
+
phase: 'I' | 'I/II' | 'II' | 'II/III' | 'III' | 'IV';
|
|
200
|
+
matchingBiomarkers: string[];
|
|
201
|
+
status: 'recruiting' | 'active-not-recruiting' | 'enrolling-by-invitation';
|
|
202
|
+
locations?: { name: string; city: string; state: string; country: string }[];
|
|
203
|
+
sponsor?: string;
|
|
204
|
+
drugs?: string[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
208
|
+
// PLATFORM CONFIGURATION
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
210
|
+
|
|
211
|
+
export interface GenomicPlatformConfig {
|
|
212
|
+
platform: 'foundation-medicine' | 'guardant' | 'tempus' | 'caris' | 'neogenomics' | 'generic';
|
|
213
|
+
apiBaseUrl: string;
|
|
214
|
+
apiKey?: string;
|
|
215
|
+
clientId?: string;
|
|
216
|
+
clientSecret?: string;
|
|
217
|
+
organizationId?: string;
|
|
218
|
+
webhookUrl?: string;
|
|
219
|
+
timeout?: number;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
223
|
+
// ABSTRACT GENOMIC PLATFORM CLIENT
|
|
224
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
225
|
+
|
|
226
|
+
export abstract class GenomicPlatformClient extends EventEmitter {
|
|
227
|
+
protected config: GenomicPlatformConfig;
|
|
228
|
+
protected accessToken?: string;
|
|
229
|
+
protected tokenExpiry?: Date;
|
|
230
|
+
|
|
231
|
+
constructor(config: GenomicPlatformConfig) {
|
|
232
|
+
super();
|
|
233
|
+
this.config = {
|
|
234
|
+
timeout: 60000,
|
|
235
|
+
...config
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
abstract authenticate(): Promise<void>;
|
|
240
|
+
abstract submitOrder(order: GenomicTestOrder): Promise<{ orderId: string; status: string }>;
|
|
241
|
+
abstract getOrderStatus(orderId: string): Promise<GenomicTestOrder>;
|
|
242
|
+
abstract getResults(orderId: string): Promise<GenomicTestResult>;
|
|
243
|
+
abstract listPatientResults(patientId: string): Promise<GenomicTestResult[]>;
|
|
244
|
+
|
|
245
|
+
protected async httpRequest(url: string, options: {
|
|
246
|
+
method: string;
|
|
247
|
+
headers?: Record<string, string>;
|
|
248
|
+
body?: string;
|
|
249
|
+
}): Promise<string> {
|
|
250
|
+
const response = await fetch(url, {
|
|
251
|
+
method: options.method,
|
|
252
|
+
headers: {
|
|
253
|
+
'Content-Type': 'application/json',
|
|
254
|
+
...options.headers
|
|
255
|
+
},
|
|
256
|
+
body: options.body,
|
|
257
|
+
signal: AbortSignal.timeout(this.config.timeout || 60000)
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
const errorBody = await response.text();
|
|
262
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return await response.text();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
protected async getAuthHeaders(): Promise<Record<string, string>> {
|
|
269
|
+
if (this.tokenExpiry && new Date() >= this.tokenExpiry) {
|
|
270
|
+
await this.authenticate();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (this.accessToken) {
|
|
274
|
+
return { 'Authorization': `Bearer ${this.accessToken}` };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (this.config.apiKey) {
|
|
278
|
+
return { 'X-API-Key': this.config.apiKey };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
286
|
+
// FOUNDATION MEDICINE CLIENT
|
|
287
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
288
|
+
|
|
289
|
+
export class FoundationMedicineClient extends GenomicPlatformClient {
|
|
290
|
+
constructor(config: Omit<GenomicPlatformConfig, 'platform'>) {
|
|
291
|
+
super({ ...config, platform: 'foundation-medicine' });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async authenticate(): Promise<void> {
|
|
295
|
+
const tokenUrl = `${this.config.apiBaseUrl}/oauth/token`;
|
|
296
|
+
|
|
297
|
+
const response = await this.httpRequest(tokenUrl, {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: {
|
|
300
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
301
|
+
},
|
|
302
|
+
body: new URLSearchParams({
|
|
303
|
+
grant_type: 'client_credentials',
|
|
304
|
+
client_id: this.config.clientId || '',
|
|
305
|
+
client_secret: this.config.clientSecret || ''
|
|
306
|
+
}).toString()
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const data = JSON.parse(response);
|
|
310
|
+
this.accessToken = data.access_token;
|
|
311
|
+
this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async submitOrder(order: GenomicTestOrder): Promise<{ orderId: string; status: string }> {
|
|
315
|
+
const url = `${this.config.apiBaseUrl}/v1/orders`;
|
|
316
|
+
|
|
317
|
+
const fmiOrder = this.mapToFMIOrder(order);
|
|
318
|
+
|
|
319
|
+
const response = await this.httpRequest(url, {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: await this.getAuthHeaders(),
|
|
322
|
+
body: JSON.stringify(fmiOrder)
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const result = JSON.parse(response);
|
|
326
|
+
return {
|
|
327
|
+
orderId: result.orderId || result.id,
|
|
328
|
+
status: result.status || 'submitted'
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async getOrderStatus(orderId: string): Promise<GenomicTestOrder> {
|
|
333
|
+
const url = `${this.config.apiBaseUrl}/v1/orders/${orderId}`;
|
|
334
|
+
|
|
335
|
+
const response = await this.httpRequest(url, {
|
|
336
|
+
method: 'GET',
|
|
337
|
+
headers: await this.getAuthHeaders()
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const fmiOrder = JSON.parse(response);
|
|
341
|
+
return this.mapFromFMIOrder(fmiOrder);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async getResults(orderId: string): Promise<GenomicTestResult> {
|
|
345
|
+
const url = `${this.config.apiBaseUrl}/v1/orders/${orderId}/report`;
|
|
346
|
+
|
|
347
|
+
const response = await this.httpRequest(url, {
|
|
348
|
+
method: 'GET',
|
|
349
|
+
headers: await this.getAuthHeaders()
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const fmiReport = JSON.parse(response);
|
|
353
|
+
return this.mapFromFMIReport(fmiReport, orderId);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async listPatientResults(patientId: string): Promise<GenomicTestResult[]> {
|
|
357
|
+
const url = `${this.config.apiBaseUrl}/v1/patients/${patientId}/reports`;
|
|
358
|
+
|
|
359
|
+
const response = await this.httpRequest(url, {
|
|
360
|
+
method: 'GET',
|
|
361
|
+
headers: await this.getAuthHeaders()
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const reports = JSON.parse(response);
|
|
365
|
+
return reports.map((r: any) => this.mapFromFMIReport(r, r.orderId));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get FoundationOne CDx specific data
|
|
370
|
+
*/
|
|
371
|
+
async getFoundationOneCDxReport(orderId: string): Promise<{
|
|
372
|
+
variants: GenomicVariant[];
|
|
373
|
+
cnvs: CopyNumberAlteration[];
|
|
374
|
+
fusions: GeneFusion[];
|
|
375
|
+
msi: MSIResult;
|
|
376
|
+
tmb: TMBResult;
|
|
377
|
+
loh: LOHResult;
|
|
378
|
+
therapies: TherapyMatch[];
|
|
379
|
+
}> {
|
|
380
|
+
const result = await this.getResults(orderId);
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
variants: result.variants,
|
|
384
|
+
cnvs: result.copyNumberAlterations,
|
|
385
|
+
fusions: result.fusions,
|
|
386
|
+
msi: result.signatures?.microsatelliteInstability || { status: 'MSS', method: 'NGS' },
|
|
387
|
+
tmb: result.signatures?.tumorMutationalBurden || { value: 0, unit: 'mutations/Mb', status: 'low', threshold: 10 },
|
|
388
|
+
loh: result.signatures?.lossOfHeterozygosity || { percentage: 0, status: 'low' },
|
|
389
|
+
therapies: result.therapyMatches
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private mapToFMIOrder(order: GenomicTestOrder): Record<string, any> {
|
|
394
|
+
return {
|
|
395
|
+
externalOrderId: order.orderId,
|
|
396
|
+
patient: {
|
|
397
|
+
externalId: order.patientId
|
|
398
|
+
},
|
|
399
|
+
specimen: {
|
|
400
|
+
type: order.specimenType,
|
|
401
|
+
externalId: order.specimenId
|
|
402
|
+
},
|
|
403
|
+
test: {
|
|
404
|
+
code: this.mapPanelToFMICode(order.panelName)
|
|
405
|
+
},
|
|
406
|
+
diagnosis: {
|
|
407
|
+
icdCodes: order.icdCodes,
|
|
408
|
+
clinicalHistory: order.clinicalIndication
|
|
409
|
+
},
|
|
410
|
+
orderingPhysician: {
|
|
411
|
+
name: order.orderingPhysician.name,
|
|
412
|
+
npi: order.orderingPhysician.npi,
|
|
413
|
+
facility: order.orderingPhysician.facility
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private mapPanelToFMICode(panelName: string): string {
|
|
419
|
+
const mapping: Record<string, string> = {
|
|
420
|
+
'FoundationOne CDx': 'F1CDX',
|
|
421
|
+
'FoundationOne Liquid CDx': 'F1LCDX',
|
|
422
|
+
'FoundationOne Heme': 'F1HEME',
|
|
423
|
+
'FoundationACT': 'FACT'
|
|
424
|
+
};
|
|
425
|
+
return mapping[panelName] || 'F1CDX';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private mapFromFMIOrder(fmiOrder: any): GenomicTestOrder {
|
|
429
|
+
return {
|
|
430
|
+
orderId: fmiOrder.externalOrderId || fmiOrder.id,
|
|
431
|
+
patientId: fmiOrder.patient?.externalId,
|
|
432
|
+
testType: 'comprehensive-genomic-profiling',
|
|
433
|
+
panelName: this.mapFMICodeToPanel(fmiOrder.test?.code),
|
|
434
|
+
specimenType: fmiOrder.specimen?.type || 'tissue',
|
|
435
|
+
specimenId: fmiOrder.specimen?.externalId,
|
|
436
|
+
orderDate: new Date(fmiOrder.createdAt),
|
|
437
|
+
clinicalIndication: fmiOrder.diagnosis?.clinicalHistory || '',
|
|
438
|
+
icdCodes: fmiOrder.diagnosis?.icdCodes || [],
|
|
439
|
+
orderingPhysician: {
|
|
440
|
+
name: fmiOrder.orderingPhysician?.name || '',
|
|
441
|
+
npi: fmiOrder.orderingPhysician?.npi || '',
|
|
442
|
+
facility: fmiOrder.orderingPhysician?.facility || ''
|
|
443
|
+
},
|
|
444
|
+
status: this.mapFMIStatus(fmiOrder.status)
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private mapFMICodeToPanel(code: string): string {
|
|
449
|
+
const mapping: Record<string, string> = {
|
|
450
|
+
'F1CDX': 'FoundationOne CDx',
|
|
451
|
+
'F1LCDX': 'FoundationOne Liquid CDx',
|
|
452
|
+
'F1HEME': 'FoundationOne Heme',
|
|
453
|
+
'FACT': 'FoundationACT'
|
|
454
|
+
};
|
|
455
|
+
return mapping[code] || 'FoundationOne CDx';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private mapFMIStatus(status: string): GenomicTestOrder['status'] {
|
|
459
|
+
const mapping: Record<string, GenomicTestOrder['status']> = {
|
|
460
|
+
'ORDERED': 'ordered',
|
|
461
|
+
'SPECIMEN_RECEIVED': 'specimen-received',
|
|
462
|
+
'IN_PROCESS': 'in-process',
|
|
463
|
+
'COMPLETE': 'completed',
|
|
464
|
+
'FAILED': 'failed',
|
|
465
|
+
'CANCELLED': 'cancelled'
|
|
466
|
+
};
|
|
467
|
+
return mapping[status] || 'ordered';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private mapFromFMIReport(fmiReport: any, orderId: string): GenomicTestResult {
|
|
471
|
+
const variants: GenomicVariant[] = (fmiReport.shortVariants || []).map((v: any) => ({
|
|
472
|
+
gene: v.gene,
|
|
473
|
+
hgvsC: v.cdsEffect,
|
|
474
|
+
hgvsP: v.proteinEffect,
|
|
475
|
+
transcript: v.transcript,
|
|
476
|
+
variantType: this.mapVariantType(v.variantType),
|
|
477
|
+
variantAlleleFrequency: v.alleleFrequency || 0,
|
|
478
|
+
coverage: v.depth,
|
|
479
|
+
clinicalSignificance: this.mapPathogenicity(v.pathogenicity),
|
|
480
|
+
tier: this.mapTier(v.tier),
|
|
481
|
+
actionability: v.therapies ? {
|
|
482
|
+
level: v.fdaApproved ? 'FDA-approved' : 'clinical-evidence',
|
|
483
|
+
therapies: v.therapies,
|
|
484
|
+
evidence: v.evidenceSummary ? [v.evidenceSummary] : []
|
|
485
|
+
} : undefined
|
|
486
|
+
}));
|
|
487
|
+
|
|
488
|
+
const cnvs: CopyNumberAlteration[] = (fmiReport.copyNumberAlterations || []).map((c: any) => ({
|
|
489
|
+
gene: c.gene,
|
|
490
|
+
type: c.type?.toLowerCase() === 'amplification' ? 'amplification' : 'deep-deletion',
|
|
491
|
+
copyNumber: c.copyNumber,
|
|
492
|
+
clinicalSignificance: this.mapPathogenicity(c.pathogenicity),
|
|
493
|
+
actionability: c.therapies ? {
|
|
494
|
+
level: c.fdaApproved ? 'FDA-approved' : 'clinical-evidence',
|
|
495
|
+
therapies: c.therapies
|
|
496
|
+
} : undefined
|
|
497
|
+
}));
|
|
498
|
+
|
|
499
|
+
const fusions: GeneFusion[] = (fmiReport.rearrangements || []).map((r: any) => ({
|
|
500
|
+
gene5Prime: r.gene1,
|
|
501
|
+
gene3Prime: r.gene2,
|
|
502
|
+
fusionName: `${r.gene1}-${r.gene2}`,
|
|
503
|
+
inFrame: r.inFrame,
|
|
504
|
+
clinicalSignificance: this.mapPathogenicity(r.pathogenicity),
|
|
505
|
+
actionability: r.therapies ? {
|
|
506
|
+
level: r.fdaApproved ? 'FDA-approved' : 'clinical-evidence',
|
|
507
|
+
therapies: r.therapies
|
|
508
|
+
} : undefined
|
|
509
|
+
}));
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
reportId: fmiReport.reportId || fmiReport.id,
|
|
513
|
+
orderId,
|
|
514
|
+
patientId: fmiReport.patient?.externalId,
|
|
515
|
+
testType: 'comprehensive-genomic-profiling',
|
|
516
|
+
panelName: this.mapFMICodeToPanel(fmiReport.testCode),
|
|
517
|
+
specimenInfo: {
|
|
518
|
+
type: fmiReport.specimen?.type || 'tissue',
|
|
519
|
+
site: fmiReport.specimen?.site,
|
|
520
|
+
collectionDate: new Date(fmiReport.specimen?.collectionDate || Date.now()),
|
|
521
|
+
tumorPurity: fmiReport.specimen?.tumorPurity,
|
|
522
|
+
cellularity: fmiReport.specimen?.cellularity
|
|
523
|
+
},
|
|
524
|
+
reportDate: new Date(fmiReport.reportDate || Date.now()),
|
|
525
|
+
variants,
|
|
526
|
+
copyNumberAlterations: cnvs,
|
|
527
|
+
fusions,
|
|
528
|
+
biomarkers: [],
|
|
529
|
+
therapyMatches: (fmiReport.therapyMatches || []).map((t: any) => ({
|
|
530
|
+
therapy: t.therapy,
|
|
531
|
+
drugs: t.drugs || [t.therapy],
|
|
532
|
+
biomarkers: t.biomarkers || [],
|
|
533
|
+
evidenceLevel: t.fdaApproved ? 'FDA-approved' : 'clinical-evidence',
|
|
534
|
+
cancerType: t.indication || '',
|
|
535
|
+
references: t.references || []
|
|
536
|
+
})),
|
|
537
|
+
clinicalTrialMatches: (fmiReport.clinicalTrials || []).map((ct: any) => ({
|
|
538
|
+
trialId: ct.nctId,
|
|
539
|
+
title: ct.title,
|
|
540
|
+
phase: ct.phase,
|
|
541
|
+
matchingBiomarkers: ct.matchingBiomarkers || [],
|
|
542
|
+
status: ct.status || 'recruiting'
|
|
543
|
+
})),
|
|
544
|
+
signatures: {
|
|
545
|
+
microsatelliteInstability: fmiReport.msi ? {
|
|
546
|
+
status: fmiReport.msi.status,
|
|
547
|
+
score: fmiReport.msi.score,
|
|
548
|
+
method: 'NGS'
|
|
549
|
+
} : undefined,
|
|
550
|
+
tumorMutationalBurden: fmiReport.tmb ? {
|
|
551
|
+
value: fmiReport.tmb.score,
|
|
552
|
+
unit: 'mutations/Mb',
|
|
553
|
+
status: fmiReport.tmb.score >= 10 ? 'high' : fmiReport.tmb.score >= 6 ? 'intermediate' : 'low',
|
|
554
|
+
threshold: 10,
|
|
555
|
+
percentile: fmiReport.tmb.percentile
|
|
556
|
+
} : undefined,
|
|
557
|
+
lossOfHeterozygosity: fmiReport.loh ? {
|
|
558
|
+
percentage: fmiReport.loh.percentage,
|
|
559
|
+
status: fmiReport.loh.percentage >= 16 ? 'high' : 'low',
|
|
560
|
+
genomeFraction: fmiReport.loh.genomeFraction
|
|
561
|
+
} : undefined
|
|
562
|
+
},
|
|
563
|
+
qualityMetrics: {
|
|
564
|
+
meanCoverage: fmiReport.qc?.meanCoverage,
|
|
565
|
+
percentBases100x: fmiReport.qc?.percentBases100x,
|
|
566
|
+
tumorFraction: fmiReport.qc?.tumorFraction
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private mapVariantType(type: string): GenomicVariant['variantType'] {
|
|
572
|
+
const mapping: Record<string, GenomicVariant['variantType']> = {
|
|
573
|
+
'SNV': 'SNV',
|
|
574
|
+
'INSERTION': 'insertion',
|
|
575
|
+
'DELETION': 'deletion',
|
|
576
|
+
'INDEL': 'indel',
|
|
577
|
+
'MNV': 'MNV'
|
|
578
|
+
};
|
|
579
|
+
return mapping[type?.toUpperCase()] || 'SNV';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private mapPathogenicity(path: string): GenomicVariant['clinicalSignificance'] {
|
|
583
|
+
const mapping: Record<string, GenomicVariant['clinicalSignificance']> = {
|
|
584
|
+
'PATHOGENIC': 'pathogenic',
|
|
585
|
+
'LIKELY_PATHOGENIC': 'likely-pathogenic',
|
|
586
|
+
'VUS': 'vus',
|
|
587
|
+
'LIKELY_BENIGN': 'likely-benign',
|
|
588
|
+
'BENIGN': 'benign'
|
|
589
|
+
};
|
|
590
|
+
return mapping[path?.toUpperCase()] || 'vus';
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private mapTier(tier: string): GenomicVariant['tier'] {
|
|
594
|
+
if (tier === '1' || tier === 'I') return 'I';
|
|
595
|
+
if (tier === '2' || tier === 'II') return 'II';
|
|
596
|
+
if (tier === '3' || tier === 'III') return 'III';
|
|
597
|
+
return 'IV';
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
602
|
+
// GUARDANT HEALTH CLIENT
|
|
603
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
604
|
+
|
|
605
|
+
export class GuardantHealthClient extends GenomicPlatformClient {
|
|
606
|
+
constructor(config: Omit<GenomicPlatformConfig, 'platform'>) {
|
|
607
|
+
super({ ...config, platform: 'guardant' });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async authenticate(): Promise<void> {
|
|
611
|
+
// Guardant uses API key authentication
|
|
612
|
+
if (!this.config.apiKey) {
|
|
613
|
+
throw new Error('Guardant Health API requires an API key');
|
|
614
|
+
}
|
|
615
|
+
// No token fetch needed for API key auth
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async submitOrder(order: GenomicTestOrder): Promise<{ orderId: string; status: string }> {
|
|
619
|
+
const url = `${this.config.apiBaseUrl}/v2/orders`;
|
|
620
|
+
|
|
621
|
+
const guardantOrder = {
|
|
622
|
+
externalOrderId: order.orderId,
|
|
623
|
+
patient: {
|
|
624
|
+
externalId: order.patientId
|
|
625
|
+
},
|
|
626
|
+
test: this.mapPanelToGuardantTest(order.panelName),
|
|
627
|
+
specimen: {
|
|
628
|
+
type: 'blood', // Guardant is primarily liquid biopsy
|
|
629
|
+
collectionDate: new Date().toISOString()
|
|
630
|
+
},
|
|
631
|
+
diagnosis: {
|
|
632
|
+
icdCodes: order.icdCodes,
|
|
633
|
+
cancerType: order.clinicalIndication
|
|
634
|
+
},
|
|
635
|
+
orderingProvider: {
|
|
636
|
+
name: order.orderingPhysician.name,
|
|
637
|
+
npi: order.orderingPhysician.npi,
|
|
638
|
+
organization: order.orderingPhysician.facility
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const response = await this.httpRequest(url, {
|
|
643
|
+
method: 'POST',
|
|
644
|
+
headers: {
|
|
645
|
+
...await this.getAuthHeaders()
|
|
646
|
+
},
|
|
647
|
+
body: JSON.stringify(guardantOrder)
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const result = JSON.parse(response);
|
|
651
|
+
return {
|
|
652
|
+
orderId: result.orderId,
|
|
653
|
+
status: 'ordered'
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async getOrderStatus(orderId: string): Promise<GenomicTestOrder> {
|
|
658
|
+
const url = `${this.config.apiBaseUrl}/v2/orders/${orderId}`;
|
|
659
|
+
|
|
660
|
+
const response = await this.httpRequest(url, {
|
|
661
|
+
method: 'GET',
|
|
662
|
+
headers: await this.getAuthHeaders()
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const data = JSON.parse(response);
|
|
666
|
+
return this.mapFromGuardantOrder(data);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async getResults(orderId: string): Promise<GenomicTestResult> {
|
|
670
|
+
const url = `${this.config.apiBaseUrl}/v2/orders/${orderId}/results`;
|
|
671
|
+
|
|
672
|
+
const response = await this.httpRequest(url, {
|
|
673
|
+
method: 'GET',
|
|
674
|
+
headers: await this.getAuthHeaders()
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const data = JSON.parse(response);
|
|
678
|
+
return this.mapFromGuardantReport(data, orderId);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async listPatientResults(patientId: string): Promise<GenomicTestResult[]> {
|
|
682
|
+
const url = `${this.config.apiBaseUrl}/v2/patients/${patientId}/results`;
|
|
683
|
+
|
|
684
|
+
const response = await this.httpRequest(url, {
|
|
685
|
+
method: 'GET',
|
|
686
|
+
headers: await this.getAuthHeaders()
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const reports = JSON.parse(response);
|
|
690
|
+
return reports.map((r: any) => this.mapFromGuardantReport(r, r.orderId));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Get Guardant360 specific metrics including ctDNA fraction
|
|
695
|
+
*/
|
|
696
|
+
async getGuardant360Metrics(orderId: string): Promise<{
|
|
697
|
+
ctDNAFraction: number;
|
|
698
|
+
maxMAF: number;
|
|
699
|
+
somatic: GenomicVariant[];
|
|
700
|
+
clonalHematopoiesis: GenomicVariant[];
|
|
701
|
+
msi: MSIResult;
|
|
702
|
+
}> {
|
|
703
|
+
const result = await this.getResults(orderId);
|
|
704
|
+
|
|
705
|
+
// Filter for clonal hematopoiesis variants (common in blood-based testing)
|
|
706
|
+
const chVariants = result.variants.filter(v =>
|
|
707
|
+
['DNMT3A', 'TET2', 'ASXL1', 'PPM1D', 'TP53', 'SF3B1', 'SRSF2'].includes(v.gene) &&
|
|
708
|
+
v.variantAlleleFrequency < 0.1
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
const somaticVariants = result.variants.filter(v =>
|
|
712
|
+
!chVariants.includes(v)
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
return {
|
|
716
|
+
ctDNAFraction: result.qualityMetrics.tumorFraction || 0,
|
|
717
|
+
maxMAF: Math.max(...result.variants.map(v => v.variantAlleleFrequency), 0),
|
|
718
|
+
somatic: somaticVariants,
|
|
719
|
+
clonalHematopoiesis: chVariants,
|
|
720
|
+
msi: result.signatures?.microsatelliteInstability || { status: 'MSS', method: 'NGS' }
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private mapPanelToGuardantTest(panelName: string): string {
|
|
725
|
+
const mapping: Record<string, string> = {
|
|
726
|
+
'Guardant360': 'G360',
|
|
727
|
+
'Guardant360 CDx': 'G360CDX',
|
|
728
|
+
'GuardantOMNI': 'GOMNI',
|
|
729
|
+
'Guardant360 TissueNext': 'G360TN',
|
|
730
|
+
'GuardantReveal': 'GREVEAL'
|
|
731
|
+
};
|
|
732
|
+
return mapping[panelName] || 'G360';
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private mapFromGuardantOrder(data: any): GenomicTestOrder {
|
|
736
|
+
return {
|
|
737
|
+
orderId: data.externalOrderId || data.orderId,
|
|
738
|
+
patientId: data.patient?.externalId,
|
|
739
|
+
testType: 'liquid-biopsy',
|
|
740
|
+
panelName: 'Guardant360',
|
|
741
|
+
specimenType: 'blood',
|
|
742
|
+
specimenId: data.specimen?.id,
|
|
743
|
+
orderDate: new Date(data.orderDate),
|
|
744
|
+
clinicalIndication: data.diagnosis?.cancerType || '',
|
|
745
|
+
icdCodes: data.diagnosis?.icdCodes || [],
|
|
746
|
+
orderingPhysician: {
|
|
747
|
+
name: data.orderingProvider?.name || '',
|
|
748
|
+
npi: data.orderingProvider?.npi || '',
|
|
749
|
+
facility: data.orderingProvider?.organization || ''
|
|
750
|
+
},
|
|
751
|
+
status: this.mapGuardantStatus(data.status)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private mapGuardantStatus(status: string): GenomicTestOrder['status'] {
|
|
756
|
+
const mapping: Record<string, GenomicTestOrder['status']> = {
|
|
757
|
+
'ORDERED': 'ordered',
|
|
758
|
+
'RECEIVED': 'specimen-received',
|
|
759
|
+
'PROCESSING': 'in-process',
|
|
760
|
+
'COMPLETE': 'completed',
|
|
761
|
+
'FAILED': 'failed',
|
|
762
|
+
'CANCELLED': 'cancelled'
|
|
763
|
+
};
|
|
764
|
+
return mapping[status?.toUpperCase()] || 'ordered';
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private mapFromGuardantReport(data: any, orderId: string): GenomicTestResult {
|
|
768
|
+
const variants: GenomicVariant[] = (data.alterations || [])
|
|
769
|
+
.filter((a: any) => a.type === 'SNV' || a.type === 'INDEL')
|
|
770
|
+
.map((v: any) => ({
|
|
771
|
+
gene: v.gene,
|
|
772
|
+
hgvsP: v.proteinChange,
|
|
773
|
+
hgvsC: v.cdsChange,
|
|
774
|
+
variantType: v.type === 'INDEL' ? 'indel' : 'SNV',
|
|
775
|
+
variantAlleleFrequency: v.plasmaAF || v.af || 0,
|
|
776
|
+
clinicalSignificance: 'likely-pathogenic',
|
|
777
|
+
actionability: v.therapies ? {
|
|
778
|
+
level: v.fdaApproved ? 'FDA-approved' : 'clinical-evidence',
|
|
779
|
+
therapies: v.therapies,
|
|
780
|
+
evidence: []
|
|
781
|
+
} : undefined
|
|
782
|
+
}));
|
|
783
|
+
|
|
784
|
+
const cnvs: CopyNumberAlteration[] = (data.alterations || [])
|
|
785
|
+
.filter((a: any) => a.type === 'AMPLIFICATION' || a.type === 'LOSS')
|
|
786
|
+
.map((c: any) => ({
|
|
787
|
+
gene: c.gene,
|
|
788
|
+
type: c.type === 'AMPLIFICATION' ? 'amplification' : 'deep-deletion',
|
|
789
|
+
copyNumber: c.copyNumber,
|
|
790
|
+
clinicalSignificance: 'likely-pathogenic'
|
|
791
|
+
}));
|
|
792
|
+
|
|
793
|
+
const fusions: GeneFusion[] = (data.alterations || [])
|
|
794
|
+
.filter((a: any) => a.type === 'FUSION')
|
|
795
|
+
.map((f: any) => ({
|
|
796
|
+
gene5Prime: f.gene.split('-')[0] || f.gene,
|
|
797
|
+
gene3Prime: f.gene.split('-')[1] || f.partner,
|
|
798
|
+
fusionName: f.gene,
|
|
799
|
+
clinicalSignificance: 'pathogenic'
|
|
800
|
+
}));
|
|
801
|
+
|
|
802
|
+
return {
|
|
803
|
+
reportId: data.reportId,
|
|
804
|
+
orderId,
|
|
805
|
+
patientId: data.patient?.externalId,
|
|
806
|
+
testType: 'liquid-biopsy',
|
|
807
|
+
panelName: 'Guardant360',
|
|
808
|
+
specimenInfo: {
|
|
809
|
+
type: 'blood',
|
|
810
|
+
collectionDate: new Date(data.collectionDate || Date.now()),
|
|
811
|
+
tumorPurity: data.ctDNAFraction
|
|
812
|
+
},
|
|
813
|
+
reportDate: new Date(data.reportDate || Date.now()),
|
|
814
|
+
variants,
|
|
815
|
+
copyNumberAlterations: cnvs,
|
|
816
|
+
fusions,
|
|
817
|
+
biomarkers: [],
|
|
818
|
+
therapyMatches: (data.therapyAssociations || []).map((t: any) => ({
|
|
819
|
+
therapy: t.therapy,
|
|
820
|
+
drugs: t.drugs || [t.therapy],
|
|
821
|
+
biomarkers: [t.biomarker],
|
|
822
|
+
evidenceLevel: t.level || 'clinical-evidence',
|
|
823
|
+
cancerType: t.indication || '',
|
|
824
|
+
references: t.references || []
|
|
825
|
+
})),
|
|
826
|
+
clinicalTrialMatches: [],
|
|
827
|
+
signatures: {
|
|
828
|
+
microsatelliteInstability: data.msi ? {
|
|
829
|
+
status: data.msi.status,
|
|
830
|
+
method: 'NGS'
|
|
831
|
+
} : undefined,
|
|
832
|
+
tumorMutationalBurden: data.bTMB ? {
|
|
833
|
+
value: data.bTMB.score,
|
|
834
|
+
unit: 'mutations/Mb',
|
|
835
|
+
status: data.bTMB.score >= 16 ? 'high' : 'low',
|
|
836
|
+
threshold: 16
|
|
837
|
+
} : undefined
|
|
838
|
+
},
|
|
839
|
+
qualityMetrics: {
|
|
840
|
+
tumorFraction: data.ctDNAFraction,
|
|
841
|
+
meanCoverage: data.qc?.meanCoverage
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
848
|
+
// TEMPUS CLIENT
|
|
849
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
850
|
+
|
|
851
|
+
export class TempusClient extends GenomicPlatformClient {
|
|
852
|
+
constructor(config: Omit<GenomicPlatformConfig, 'platform'>) {
|
|
853
|
+
super({ ...config, platform: 'tempus' });
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async authenticate(): Promise<void> {
|
|
857
|
+
const tokenUrl = `${this.config.apiBaseUrl}/auth/token`;
|
|
858
|
+
|
|
859
|
+
const response = await this.httpRequest(tokenUrl, {
|
|
860
|
+
method: 'POST',
|
|
861
|
+
headers: {
|
|
862
|
+
'Content-Type': 'application/json'
|
|
863
|
+
},
|
|
864
|
+
body: JSON.stringify({
|
|
865
|
+
client_id: this.config.clientId,
|
|
866
|
+
client_secret: this.config.clientSecret,
|
|
867
|
+
grant_type: 'client_credentials'
|
|
868
|
+
})
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
const data = JSON.parse(response);
|
|
872
|
+
this.accessToken = data.access_token;
|
|
873
|
+
this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000));
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async submitOrder(order: GenomicTestOrder): Promise<{ orderId: string; status: string }> {
|
|
877
|
+
const url = `${this.config.apiBaseUrl}/v1/orders`;
|
|
878
|
+
|
|
879
|
+
const tempusOrder = {
|
|
880
|
+
externalId: order.orderId,
|
|
881
|
+
patient: {
|
|
882
|
+
externalId: order.patientId
|
|
883
|
+
},
|
|
884
|
+
testCode: this.mapPanelToTempusCode(order.panelName),
|
|
885
|
+
specimen: {
|
|
886
|
+
type: order.specimenType,
|
|
887
|
+
externalId: order.specimenId
|
|
888
|
+
},
|
|
889
|
+
indication: {
|
|
890
|
+
codes: order.icdCodes,
|
|
891
|
+
description: order.clinicalIndication
|
|
892
|
+
},
|
|
893
|
+
provider: {
|
|
894
|
+
name: order.orderingPhysician.name,
|
|
895
|
+
npi: order.orderingPhysician.npi,
|
|
896
|
+
facility: order.orderingPhysician.facility
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const response = await this.httpRequest(url, {
|
|
901
|
+
method: 'POST',
|
|
902
|
+
headers: await this.getAuthHeaders(),
|
|
903
|
+
body: JSON.stringify(tempusOrder)
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
const result = JSON.parse(response);
|
|
907
|
+
return {
|
|
908
|
+
orderId: result.id,
|
|
909
|
+
status: 'ordered'
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async getOrderStatus(orderId: string): Promise<GenomicTestOrder> {
|
|
914
|
+
const url = `${this.config.apiBaseUrl}/v1/orders/${orderId}`;
|
|
915
|
+
|
|
916
|
+
const response = await this.httpRequest(url, {
|
|
917
|
+
method: 'GET',
|
|
918
|
+
headers: await this.getAuthHeaders()
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
const data = JSON.parse(response);
|
|
922
|
+
return this.mapFromTempusOrder(data);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async getResults(orderId: string): Promise<GenomicTestResult> {
|
|
926
|
+
const url = `${this.config.apiBaseUrl}/v1/orders/${orderId}/report`;
|
|
927
|
+
|
|
928
|
+
const response = await this.httpRequest(url, {
|
|
929
|
+
method: 'GET',
|
|
930
|
+
headers: await this.getAuthHeaders()
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
const data = JSON.parse(response);
|
|
934
|
+
return this.mapFromTempusReport(data, orderId);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async listPatientResults(patientId: string): Promise<GenomicTestResult[]> {
|
|
938
|
+
const url = `${this.config.apiBaseUrl}/v1/patients/${patientId}/reports`;
|
|
939
|
+
|
|
940
|
+
const response = await this.httpRequest(url, {
|
|
941
|
+
method: 'GET',
|
|
942
|
+
headers: await this.getAuthHeaders()
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
const reports = JSON.parse(response);
|
|
946
|
+
return reports.map((r: any) => this.mapFromTempusReport(r, r.orderId));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Get Tempus xT/xF specific analysis including RNA expression
|
|
951
|
+
*/
|
|
952
|
+
async getTempusAnalysis(orderId: string): Promise<{
|
|
953
|
+
dnaFindings: GenomicVariant[];
|
|
954
|
+
rnaExpression?: { gene: string; zscore: number; percentile: number }[];
|
|
955
|
+
immuneProfile?: {
|
|
956
|
+
pdl1: { score: number; method: string };
|
|
957
|
+
tils: number;
|
|
958
|
+
immuneScore: number;
|
|
959
|
+
};
|
|
960
|
+
hrd: HRDResult;
|
|
961
|
+
}> {
|
|
962
|
+
const result = await this.getResults(orderId);
|
|
963
|
+
|
|
964
|
+
// Tempus provides RNA expression data for xT panel
|
|
965
|
+
let rnaExpression;
|
|
966
|
+
if (result.biomarkers.some(b => b.name.toLowerCase().includes('expression'))) {
|
|
967
|
+
rnaExpression = result.biomarkers
|
|
968
|
+
.filter(b => b.name.toLowerCase().includes('expression'))
|
|
969
|
+
.map(b => ({
|
|
970
|
+
gene: b.name.replace(' expression', ''),
|
|
971
|
+
zscore: typeof b.value === 'number' ? b.value : 0,
|
|
972
|
+
percentile: 50 // Would come from actual data
|
|
973
|
+
}));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return {
|
|
977
|
+
dnaFindings: result.variants,
|
|
978
|
+
rnaExpression,
|
|
979
|
+
immuneProfile: result.biomarkers.some(b => b.name === 'PD-L1') ? {
|
|
980
|
+
pdl1: {
|
|
981
|
+
score: Number(result.biomarkers.find(b => b.name === 'PD-L1')?.value || 0),
|
|
982
|
+
method: 'IHC'
|
|
983
|
+
},
|
|
984
|
+
tils: Number(result.biomarkers.find(b => b.name === 'TILs')?.value || 0),
|
|
985
|
+
immuneScore: 0
|
|
986
|
+
} : undefined,
|
|
987
|
+
hrd: result.signatures?.homologousRecombinationDeficiency || {
|
|
988
|
+
status: 'indeterminate'
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
private mapPanelToTempusCode(panelName: string): string {
|
|
994
|
+
const mapping: Record<string, string> = {
|
|
995
|
+
'Tempus xT': 'XT',
|
|
996
|
+
'Tempus xF': 'XF',
|
|
997
|
+
'Tempus xR': 'XR',
|
|
998
|
+
'Tempus xG': 'XG',
|
|
999
|
+
'Tempus xE': 'XE'
|
|
1000
|
+
};
|
|
1001
|
+
return mapping[panelName] || 'XT';
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private mapFromTempusOrder(data: any): GenomicTestOrder {
|
|
1005
|
+
return {
|
|
1006
|
+
orderId: data.externalId || data.id,
|
|
1007
|
+
patientId: data.patient?.externalId,
|
|
1008
|
+
testType: 'comprehensive-genomic-profiling',
|
|
1009
|
+
panelName: this.mapTempusCodeToPanel(data.testCode),
|
|
1010
|
+
specimenType: data.specimen?.type || 'tissue',
|
|
1011
|
+
specimenId: data.specimen?.externalId,
|
|
1012
|
+
orderDate: new Date(data.createdAt),
|
|
1013
|
+
clinicalIndication: data.indication?.description || '',
|
|
1014
|
+
icdCodes: data.indication?.codes || [],
|
|
1015
|
+
orderingPhysician: {
|
|
1016
|
+
name: data.provider?.name || '',
|
|
1017
|
+
npi: data.provider?.npi || '',
|
|
1018
|
+
facility: data.provider?.facility || ''
|
|
1019
|
+
},
|
|
1020
|
+
status: this.mapTempusStatus(data.status)
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private mapTempusCodeToPanel(code: string): string {
|
|
1025
|
+
const mapping: Record<string, string> = {
|
|
1026
|
+
'XT': 'Tempus xT',
|
|
1027
|
+
'XF': 'Tempus xF',
|
|
1028
|
+
'XR': 'Tempus xR',
|
|
1029
|
+
'XG': 'Tempus xG',
|
|
1030
|
+
'XE': 'Tempus xE'
|
|
1031
|
+
};
|
|
1032
|
+
return mapping[code] || 'Tempus xT';
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private mapTempusStatus(status: string): GenomicTestOrder['status'] {
|
|
1036
|
+
const mapping: Record<string, GenomicTestOrder['status']> = {
|
|
1037
|
+
'ordered': 'ordered',
|
|
1038
|
+
'received': 'specimen-received',
|
|
1039
|
+
'processing': 'in-process',
|
|
1040
|
+
'completed': 'completed',
|
|
1041
|
+
'failed': 'failed',
|
|
1042
|
+
'cancelled': 'cancelled'
|
|
1043
|
+
};
|
|
1044
|
+
return mapping[status?.toLowerCase()] || 'ordered';
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
private mapFromTempusReport(data: any, orderId: string): GenomicTestResult {
|
|
1048
|
+
// Map Tempus report format to our common format
|
|
1049
|
+
const variants: GenomicVariant[] = (data.somaticVariants || []).map((v: any) => ({
|
|
1050
|
+
gene: v.gene,
|
|
1051
|
+
hgvsP: v.proteinChange,
|
|
1052
|
+
hgvsC: v.codingChange,
|
|
1053
|
+
transcript: v.transcript,
|
|
1054
|
+
variantType: v.type || 'SNV',
|
|
1055
|
+
variantAlleleFrequency: v.vaf || 0,
|
|
1056
|
+
coverage: v.depth,
|
|
1057
|
+
clinicalSignificance: v.pathogenicity || 'vus',
|
|
1058
|
+
tier: v.tier,
|
|
1059
|
+
oncogenicity: v.oncogenicity,
|
|
1060
|
+
functionalEffect: v.functionalEffect,
|
|
1061
|
+
somaticStatus: 'somatic'
|
|
1062
|
+
}));
|
|
1063
|
+
|
|
1064
|
+
return {
|
|
1065
|
+
reportId: data.reportId || data.id,
|
|
1066
|
+
orderId,
|
|
1067
|
+
patientId: data.patient?.externalId,
|
|
1068
|
+
testType: 'comprehensive-genomic-profiling',
|
|
1069
|
+
panelName: this.mapTempusCodeToPanel(data.testCode),
|
|
1070
|
+
specimenInfo: {
|
|
1071
|
+
type: data.specimen?.type || 'tissue',
|
|
1072
|
+
site: data.specimen?.site,
|
|
1073
|
+
collectionDate: new Date(data.specimen?.collectionDate || Date.now()),
|
|
1074
|
+
tumorPurity: data.specimen?.tumorContent
|
|
1075
|
+
},
|
|
1076
|
+
reportDate: new Date(data.reportDate || Date.now()),
|
|
1077
|
+
variants,
|
|
1078
|
+
copyNumberAlterations: (data.copyNumberVariants || []).map((c: any) => ({
|
|
1079
|
+
gene: c.gene,
|
|
1080
|
+
type: c.type,
|
|
1081
|
+
copyNumber: c.copyNumber,
|
|
1082
|
+
clinicalSignificance: c.pathogenicity || 'vus'
|
|
1083
|
+
})),
|
|
1084
|
+
fusions: (data.fusions || []).map((f: any) => ({
|
|
1085
|
+
gene5Prime: f.gene1,
|
|
1086
|
+
gene3Prime: f.gene2,
|
|
1087
|
+
fusionName: `${f.gene1}-${f.gene2}`,
|
|
1088
|
+
inFrame: f.inFrame,
|
|
1089
|
+
clinicalSignificance: f.pathogenicity || 'pathogenic'
|
|
1090
|
+
})),
|
|
1091
|
+
biomarkers: (data.biomarkers || []).map((b: any) => ({
|
|
1092
|
+
name: b.name,
|
|
1093
|
+
value: b.value,
|
|
1094
|
+
unit: b.unit,
|
|
1095
|
+
status: b.status,
|
|
1096
|
+
method: b.method
|
|
1097
|
+
})),
|
|
1098
|
+
therapyMatches: (data.therapyOptions || []).map((t: any) => ({
|
|
1099
|
+
therapy: t.name,
|
|
1100
|
+
drugs: t.drugs || [t.name],
|
|
1101
|
+
biomarkers: t.biomarkers || [],
|
|
1102
|
+
evidenceLevel: t.evidenceLevel || 'clinical-evidence',
|
|
1103
|
+
cancerType: t.indication || '',
|
|
1104
|
+
references: t.references || []
|
|
1105
|
+
})),
|
|
1106
|
+
clinicalTrialMatches: (data.clinicalTrials || []).map((ct: any) => ({
|
|
1107
|
+
trialId: ct.nctNumber,
|
|
1108
|
+
title: ct.title,
|
|
1109
|
+
phase: ct.phase,
|
|
1110
|
+
matchingBiomarkers: ct.matchingBiomarkers || [],
|
|
1111
|
+
status: ct.status || 'recruiting',
|
|
1112
|
+
locations: ct.sites
|
|
1113
|
+
})),
|
|
1114
|
+
signatures: {
|
|
1115
|
+
microsatelliteInstability: data.msi,
|
|
1116
|
+
tumorMutationalBurden: data.tmb ? {
|
|
1117
|
+
value: data.tmb.score,
|
|
1118
|
+
unit: 'mutations/Mb',
|
|
1119
|
+
status: data.tmb.score >= 10 ? 'high' : 'low',
|
|
1120
|
+
threshold: 10
|
|
1121
|
+
} : undefined,
|
|
1122
|
+
homologousRecombinationDeficiency: data.hrd
|
|
1123
|
+
},
|
|
1124
|
+
qualityMetrics: data.qcMetrics
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1130
|
+
// UNIFIED GENOMICS SERVICE
|
|
1131
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1132
|
+
|
|
1133
|
+
export class UnifiedGenomicsService {
|
|
1134
|
+
private clients: Map<string, GenomicPlatformClient> = new Map();
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Register a genomic platform client
|
|
1138
|
+
*/
|
|
1139
|
+
registerClient(name: string, client: GenomicPlatformClient): void {
|
|
1140
|
+
this.clients.set(name, client);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Get results from all platforms for a patient
|
|
1145
|
+
*/
|
|
1146
|
+
async getAllPatientResults(patientId: string): Promise<{
|
|
1147
|
+
platform: string;
|
|
1148
|
+
results: GenomicTestResult[];
|
|
1149
|
+
}[]> {
|
|
1150
|
+
const allResults: { platform: string; results: GenomicTestResult[] }[] = [];
|
|
1151
|
+
|
|
1152
|
+
for (const [platform, client] of this.clients) {
|
|
1153
|
+
try {
|
|
1154
|
+
const results = await client.listPatientResults(patientId);
|
|
1155
|
+
allResults.push({ platform, results });
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
console.error(`Failed to get results from ${platform}:`, error);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
return allResults;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Aggregate and deduplicate variants across all platforms
|
|
1166
|
+
*/
|
|
1167
|
+
async getAggregatedVariants(patientId: string): Promise<{
|
|
1168
|
+
variants: GenomicVariant[];
|
|
1169
|
+
cnvs: CopyNumberAlteration[];
|
|
1170
|
+
fusions: GeneFusion[];
|
|
1171
|
+
biomarkers: {
|
|
1172
|
+
msi?: MSIResult;
|
|
1173
|
+
tmb?: TMBResult;
|
|
1174
|
+
hrd?: HRDResult;
|
|
1175
|
+
pdl1?: { score: number; scoreType: string };
|
|
1176
|
+
};
|
|
1177
|
+
}> {
|
|
1178
|
+
const allResults = await this.getAllPatientResults(patientId);
|
|
1179
|
+
|
|
1180
|
+
const variantMap = new Map<string, GenomicVariant>();
|
|
1181
|
+
const cnvMap = new Map<string, CopyNumberAlteration>();
|
|
1182
|
+
const fusionMap = new Map<string, GeneFusion>();
|
|
1183
|
+
let latestMsi: MSIResult | undefined;
|
|
1184
|
+
let latestTmb: TMBResult | undefined;
|
|
1185
|
+
let latestHrd: HRDResult | undefined;
|
|
1186
|
+
let latestReportDate = new Date(0);
|
|
1187
|
+
|
|
1188
|
+
for (const { results } of allResults) {
|
|
1189
|
+
for (const result of results) {
|
|
1190
|
+
// Track latest report for biomarkers
|
|
1191
|
+
if (result.reportDate > latestReportDate) {
|
|
1192
|
+
latestReportDate = result.reportDate;
|
|
1193
|
+
latestMsi = result.signatures?.microsatelliteInstability;
|
|
1194
|
+
latestTmb = result.signatures?.tumorMutationalBurden;
|
|
1195
|
+
latestHrd = result.signatures?.homologousRecombinationDeficiency;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Deduplicate variants by gene + protein change
|
|
1199
|
+
for (const variant of result.variants) {
|
|
1200
|
+
const key = `${variant.gene}:${variant.hgvsP || variant.hgvsC}`;
|
|
1201
|
+
const existing = variantMap.get(key);
|
|
1202
|
+
|
|
1203
|
+
// Keep the variant with higher VAF or better evidence
|
|
1204
|
+
if (!existing || variant.variantAlleleFrequency > existing.variantAlleleFrequency) {
|
|
1205
|
+
variantMap.set(key, variant);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Deduplicate CNVs
|
|
1210
|
+
for (const cnv of result.copyNumberAlterations) {
|
|
1211
|
+
const key = `${cnv.gene}:${cnv.type}`;
|
|
1212
|
+
if (!cnvMap.has(key)) {
|
|
1213
|
+
cnvMap.set(key, cnv);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Deduplicate fusions
|
|
1218
|
+
for (const fusion of result.fusions) {
|
|
1219
|
+
const key = fusion.fusionName;
|
|
1220
|
+
if (!fusionMap.has(key)) {
|
|
1221
|
+
fusionMap.set(key, fusion);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
return {
|
|
1228
|
+
variants: Array.from(variantMap.values()),
|
|
1229
|
+
cnvs: Array.from(cnvMap.values()),
|
|
1230
|
+
fusions: Array.from(fusionMap.values()),
|
|
1231
|
+
biomarkers: {
|
|
1232
|
+
msi: latestMsi,
|
|
1233
|
+
tmb: latestTmb,
|
|
1234
|
+
hrd: latestHrd
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Match patient genomics to actionable therapies
|
|
1241
|
+
*/
|
|
1242
|
+
async matchToTherapies(patientId: string, cancerType: string): Promise<TherapyMatch[]> {
|
|
1243
|
+
const genomics = await this.getAggregatedVariants(patientId);
|
|
1244
|
+
const therapyMatches: TherapyMatch[] = [];
|
|
1245
|
+
|
|
1246
|
+
// FDA-approved biomarker-drug matches
|
|
1247
|
+
const fdaMatches = this.getFDAApprovedMatches(genomics, cancerType);
|
|
1248
|
+
therapyMatches.push(...fdaMatches);
|
|
1249
|
+
|
|
1250
|
+
// NCCN guideline matches
|
|
1251
|
+
const nccnMatches = this.getNCCNMatches(genomics, cancerType);
|
|
1252
|
+
therapyMatches.push(...nccnMatches);
|
|
1253
|
+
|
|
1254
|
+
// Deduplicate and sort by evidence level
|
|
1255
|
+
const evidenceOrder = {
|
|
1256
|
+
'FDA-approved': 0,
|
|
1257
|
+
'NCCN-guideline': 1,
|
|
1258
|
+
'clinical-evidence': 2,
|
|
1259
|
+
'case-report': 3,
|
|
1260
|
+
'preclinical': 4
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
const uniqueMatches = new Map<string, TherapyMatch>();
|
|
1264
|
+
for (const match of therapyMatches) {
|
|
1265
|
+
const key = `${match.therapy}:${match.biomarkers.join(',')}`;
|
|
1266
|
+
const existing = uniqueMatches.get(key);
|
|
1267
|
+
if (!existing || evidenceOrder[match.evidenceLevel] < evidenceOrder[existing.evidenceLevel]) {
|
|
1268
|
+
uniqueMatches.set(key, match);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
return Array.from(uniqueMatches.values())
|
|
1273
|
+
.sort((a, b) => evidenceOrder[a.evidenceLevel] - evidenceOrder[b.evidenceLevel]);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private getFDAApprovedMatches(genomics: {
|
|
1277
|
+
variants: GenomicVariant[];
|
|
1278
|
+
cnvs: CopyNumberAlteration[];
|
|
1279
|
+
fusions: GeneFusion[];
|
|
1280
|
+
biomarkers: { msi?: MSIResult; tmb?: TMBResult; hrd?: HRDResult };
|
|
1281
|
+
}, cancerType: string): TherapyMatch[] {
|
|
1282
|
+
const matches: TherapyMatch[] = [];
|
|
1283
|
+
|
|
1284
|
+
// Check MSI-H for pembrolizumab (tumor-agnostic)
|
|
1285
|
+
if (genomics.biomarkers.msi?.status === 'MSI-H') {
|
|
1286
|
+
matches.push({
|
|
1287
|
+
therapy: 'Pembrolizumab',
|
|
1288
|
+
drugs: ['Pembrolizumab'],
|
|
1289
|
+
biomarkers: ['MSI-H'],
|
|
1290
|
+
evidenceLevel: 'FDA-approved',
|
|
1291
|
+
cancerType: 'Tumor-agnostic',
|
|
1292
|
+
approvalStatus: 'FDA-approved for MSI-H/dMMR solid tumors',
|
|
1293
|
+
references: ['KEYNOTE-158', 'KEYNOTE-177']
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Check TMB-H for pembrolizumab
|
|
1298
|
+
if (genomics.biomarkers.tmb?.status === 'high' && genomics.biomarkers.tmb.value >= 10) {
|
|
1299
|
+
matches.push({
|
|
1300
|
+
therapy: 'Pembrolizumab',
|
|
1301
|
+
drugs: ['Pembrolizumab'],
|
|
1302
|
+
biomarkers: ['TMB-H (' + genomics.biomarkers.tmb.value + ' mut/Mb)'],
|
|
1303
|
+
evidenceLevel: 'FDA-approved',
|
|
1304
|
+
cancerType: 'Tumor-agnostic',
|
|
1305
|
+
approvalStatus: 'FDA-approved for TMB-H solid tumors',
|
|
1306
|
+
references: ['KEYNOTE-158']
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Check NTRK fusions
|
|
1311
|
+
const ntrkFusion = genomics.fusions.find(f =>
|
|
1312
|
+
f.gene5Prime.includes('NTRK') || f.gene3Prime.includes('NTRK')
|
|
1313
|
+
);
|
|
1314
|
+
if (ntrkFusion) {
|
|
1315
|
+
matches.push({
|
|
1316
|
+
therapy: 'Larotrectinib or Entrectinib',
|
|
1317
|
+
drugs: ['Larotrectinib', 'Entrectinib'],
|
|
1318
|
+
biomarkers: [ntrkFusion.fusionName],
|
|
1319
|
+
evidenceLevel: 'FDA-approved',
|
|
1320
|
+
cancerType: 'Tumor-agnostic',
|
|
1321
|
+
approvalStatus: 'FDA-approved for NTRK fusion-positive solid tumors',
|
|
1322
|
+
references: ['NAVIGATE', 'STARTRK-2']
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Check BRAF V600E
|
|
1327
|
+
const brafV600 = genomics.variants.find(v =>
|
|
1328
|
+
v.gene === 'BRAF' && v.hgvsP?.includes('V600')
|
|
1329
|
+
);
|
|
1330
|
+
if (brafV600) {
|
|
1331
|
+
if (cancerType.toLowerCase().includes('melanoma')) {
|
|
1332
|
+
matches.push({
|
|
1333
|
+
therapy: 'Dabrafenib + Trametinib',
|
|
1334
|
+
drugs: ['Dabrafenib', 'Trametinib'],
|
|
1335
|
+
biomarkers: ['BRAF V600E/K'],
|
|
1336
|
+
evidenceLevel: 'FDA-approved',
|
|
1337
|
+
cancerType: 'Melanoma',
|
|
1338
|
+
references: ['COMBI-d', 'COMBI-v']
|
|
1339
|
+
});
|
|
1340
|
+
} else if (cancerType.toLowerCase().includes('nsclc') || cancerType.toLowerCase().includes('lung')) {
|
|
1341
|
+
matches.push({
|
|
1342
|
+
therapy: 'Dabrafenib + Trametinib',
|
|
1343
|
+
drugs: ['Dabrafenib', 'Trametinib'],
|
|
1344
|
+
biomarkers: ['BRAF V600E'],
|
|
1345
|
+
evidenceLevel: 'FDA-approved',
|
|
1346
|
+
cancerType: 'NSCLC',
|
|
1347
|
+
references: ['BRF113928']
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Check EGFR mutations
|
|
1353
|
+
const egfrMut = genomics.variants.find(v => v.gene === 'EGFR');
|
|
1354
|
+
if (egfrMut && (cancerType.toLowerCase().includes('nsclc') || cancerType.toLowerCase().includes('lung'))) {
|
|
1355
|
+
matches.push({
|
|
1356
|
+
therapy: 'Osimertinib',
|
|
1357
|
+
drugs: ['Osimertinib'],
|
|
1358
|
+
biomarkers: [`EGFR ${egfrMut.hgvsP || 'mutation'}`],
|
|
1359
|
+
evidenceLevel: 'FDA-approved',
|
|
1360
|
+
cancerType: 'NSCLC',
|
|
1361
|
+
references: ['FLAURA', 'ADAURA']
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Check ALK fusions
|
|
1366
|
+
const alkFusion = genomics.fusions.find(f =>
|
|
1367
|
+
f.gene5Prime === 'ALK' || f.gene3Prime === 'ALK'
|
|
1368
|
+
);
|
|
1369
|
+
if (alkFusion && (cancerType.toLowerCase().includes('nsclc') || cancerType.toLowerCase().includes('lung'))) {
|
|
1370
|
+
matches.push({
|
|
1371
|
+
therapy: 'Alectinib',
|
|
1372
|
+
drugs: ['Alectinib'],
|
|
1373
|
+
biomarkers: [alkFusion.fusionName],
|
|
1374
|
+
evidenceLevel: 'FDA-approved',
|
|
1375
|
+
cancerType: 'NSCLC',
|
|
1376
|
+
references: ['ALEX', 'J-ALEX']
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// Check HER2 amplification
|
|
1381
|
+
const her2Amp = genomics.cnvs.find(c => c.gene === 'HER2' || c.gene === 'ERBB2');
|
|
1382
|
+
if (her2Amp && cancerType.toLowerCase().includes('breast')) {
|
|
1383
|
+
matches.push({
|
|
1384
|
+
therapy: 'Trastuzumab + Pertuzumab',
|
|
1385
|
+
drugs: ['Trastuzumab', 'Pertuzumab'],
|
|
1386
|
+
biomarkers: ['HER2 amplification'],
|
|
1387
|
+
evidenceLevel: 'FDA-approved',
|
|
1388
|
+
cancerType: 'Breast',
|
|
1389
|
+
references: ['CLEOPATRA', 'APHINITY']
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Check BRCA1/2 mutations
|
|
1394
|
+
const brcaMut = genomics.variants.find(v =>
|
|
1395
|
+
(v.gene === 'BRCA1' || v.gene === 'BRCA2') &&
|
|
1396
|
+
(v.clinicalSignificance === 'pathogenic' || v.clinicalSignificance === 'likely-pathogenic')
|
|
1397
|
+
);
|
|
1398
|
+
if (brcaMut || genomics.biomarkers.hrd?.status === 'positive') {
|
|
1399
|
+
const biomarker = brcaMut ? `${brcaMut.gene} ${brcaMut.hgvsP || 'mutation'}` : 'HRD-positive';
|
|
1400
|
+
|
|
1401
|
+
if (cancerType.toLowerCase().includes('ovarian')) {
|
|
1402
|
+
matches.push({
|
|
1403
|
+
therapy: 'Olaparib',
|
|
1404
|
+
drugs: ['Olaparib'],
|
|
1405
|
+
biomarkers: [biomarker],
|
|
1406
|
+
evidenceLevel: 'FDA-approved',
|
|
1407
|
+
cancerType: 'Ovarian',
|
|
1408
|
+
references: ['SOLO-1', 'PAOLA-1']
|
|
1409
|
+
});
|
|
1410
|
+
} else if (cancerType.toLowerCase().includes('breast')) {
|
|
1411
|
+
matches.push({
|
|
1412
|
+
therapy: 'Olaparib or Talazoparib',
|
|
1413
|
+
drugs: ['Olaparib', 'Talazoparib'],
|
|
1414
|
+
biomarkers: [biomarker],
|
|
1415
|
+
evidenceLevel: 'FDA-approved',
|
|
1416
|
+
cancerType: 'Breast',
|
|
1417
|
+
references: ['OlympiAD', 'EMBRACA']
|
|
1418
|
+
});
|
|
1419
|
+
} else if (cancerType.toLowerCase().includes('prostate')) {
|
|
1420
|
+
matches.push({
|
|
1421
|
+
therapy: 'Olaparib or Rucaparib',
|
|
1422
|
+
drugs: ['Olaparib', 'Rucaparib'],
|
|
1423
|
+
biomarkers: [biomarker],
|
|
1424
|
+
evidenceLevel: 'FDA-approved',
|
|
1425
|
+
cancerType: 'Prostate',
|
|
1426
|
+
references: ['PROfound', 'TRITON2']
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Check KRAS G12C
|
|
1432
|
+
const krasG12C = genomics.variants.find(v =>
|
|
1433
|
+
v.gene === 'KRAS' && v.hgvsP?.includes('G12C')
|
|
1434
|
+
);
|
|
1435
|
+
if (krasG12C) {
|
|
1436
|
+
if (cancerType.toLowerCase().includes('nsclc') || cancerType.toLowerCase().includes('lung')) {
|
|
1437
|
+
matches.push({
|
|
1438
|
+
therapy: 'Sotorasib or Adagrasib',
|
|
1439
|
+
drugs: ['Sotorasib', 'Adagrasib'],
|
|
1440
|
+
biomarkers: ['KRAS G12C'],
|
|
1441
|
+
evidenceLevel: 'FDA-approved',
|
|
1442
|
+
cancerType: 'NSCLC',
|
|
1443
|
+
references: ['CodeBreaK 100', 'KRYSTAL-1']
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
return matches;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
private getNCCNMatches(genomics: {
|
|
1452
|
+
variants: GenomicVariant[];
|
|
1453
|
+
cnvs: CopyNumberAlteration[];
|
|
1454
|
+
fusions: GeneFusion[];
|
|
1455
|
+
biomarkers: { msi?: MSIResult; tmb?: TMBResult; hrd?: HRDResult };
|
|
1456
|
+
}, cancerType: string): TherapyMatch[] {
|
|
1457
|
+
// Additional NCCN guideline-based matches would go here
|
|
1458
|
+
// This is a simplified version
|
|
1459
|
+
return [];
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1464
|
+
// FACTORY FUNCTION
|
|
1465
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1466
|
+
|
|
1467
|
+
export function createGenomicClient(config: GenomicPlatformConfig): GenomicPlatformClient {
|
|
1468
|
+
switch (config.platform) {
|
|
1469
|
+
case 'foundation-medicine':
|
|
1470
|
+
return new FoundationMedicineClient(config);
|
|
1471
|
+
case 'guardant':
|
|
1472
|
+
return new GuardantHealthClient(config);
|
|
1473
|
+
case 'tempus':
|
|
1474
|
+
return new TempusClient(config);
|
|
1475
|
+
default:
|
|
1476
|
+
throw new Error(`Unsupported genomic platform: ${config.platform}`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
export default UnifiedGenomicsService;
|