@erosolaraijs/cure 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/dist/bin/cure.d.ts +10 -0
  4. package/dist/bin/cure.d.ts.map +1 -0
  5. package/dist/bin/cure.js +169 -0
  6. package/dist/bin/cure.js.map +1 -0
  7. package/dist/capabilities/cancerTreatmentCapability.d.ts +167 -0
  8. package/dist/capabilities/cancerTreatmentCapability.d.ts.map +1 -0
  9. package/dist/capabilities/cancerTreatmentCapability.js +912 -0
  10. package/dist/capabilities/cancerTreatmentCapability.js.map +1 -0
  11. package/dist/capabilities/index.d.ts +2 -0
  12. package/dist/capabilities/index.d.ts.map +1 -0
  13. package/dist/capabilities/index.js +3 -0
  14. package/dist/capabilities/index.js.map +1 -0
  15. package/dist/compliance/hipaa.d.ts +337 -0
  16. package/dist/compliance/hipaa.d.ts.map +1 -0
  17. package/dist/compliance/hipaa.js +929 -0
  18. package/dist/compliance/hipaa.js.map +1 -0
  19. package/dist/examples/cancerTreatmentDemo.d.ts +21 -0
  20. package/dist/examples/cancerTreatmentDemo.d.ts.map +1 -0
  21. package/dist/examples/cancerTreatmentDemo.js +216 -0
  22. package/dist/examples/cancerTreatmentDemo.js.map +1 -0
  23. package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts +265 -0
  24. package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts.map +1 -0
  25. package/dist/integrations/clinicalTrials/clinicalTrialsGov.js +808 -0
  26. package/dist/integrations/clinicalTrials/clinicalTrialsGov.js.map +1 -0
  27. package/dist/integrations/ehr/fhir.d.ts +455 -0
  28. package/dist/integrations/ehr/fhir.d.ts.map +1 -0
  29. package/dist/integrations/ehr/fhir.js +859 -0
  30. package/dist/integrations/ehr/fhir.js.map +1 -0
  31. package/dist/integrations/genomics/genomicPlatforms.d.ts +362 -0
  32. package/dist/integrations/genomics/genomicPlatforms.d.ts.map +1 -0
  33. package/dist/integrations/genomics/genomicPlatforms.js +1079 -0
  34. package/dist/integrations/genomics/genomicPlatforms.js.map +1 -0
  35. package/package.json +52 -0
  36. package/src/bin/cure.ts +182 -0
  37. package/src/capabilities/cancerTreatmentCapability.ts +1161 -0
  38. package/src/capabilities/index.ts +2 -0
  39. package/src/compliance/hipaa.ts +1365 -0
  40. package/src/examples/cancerTreatmentDemo.ts +241 -0
  41. package/src/integrations/clinicalTrials/clinicalTrialsGov.ts +1143 -0
  42. package/src/integrations/ehr/fhir.ts +1304 -0
  43. package/src/integrations/genomics/genomicPlatforms.ts +1480 -0
  44. package/src/ml/outcomePredictor.ts +1301 -0
  45. package/src/safety/drugInteractions.ts +942 -0
  46. package/src/validation/retrospectiveValidator.ts +887 -0
@@ -0,0 +1,887 @@
1
+ /**
2
+ * Retrospective Validation Framework
3
+ *
4
+ * Provides tools for validating the treatment recommendation system against
5
+ * real patient outcomes. Essential for:
6
+ * - Model performance assessment
7
+ * - Calibration checking
8
+ * - Concordance analysis with actual treatments
9
+ * - Outcome correlation studies
10
+ * - Continuous quality improvement
11
+ *
12
+ * IMPORTANT: All patient data used for validation must be properly de-identified
13
+ * or used with appropriate IRB approval and patient consent.
14
+ */
15
+
16
+ import { EventEmitter } from 'events';
17
+ import { createHash } from 'crypto';
18
+
19
+ // ═══════════════════════════════════════════════════════════════════════════════
20
+ // VALIDATION DATA TYPES
21
+ // ═══════════════════════════════════════════════════════════════════════════════
22
+
23
+ export interface ValidationPatient {
24
+ id: string; // De-identified ID
25
+ demographics: {
26
+ ageAtDiagnosis: number;
27
+ gender: 'male' | 'female';
28
+ ethnicity?: string;
29
+ };
30
+ diagnosis: {
31
+ cancerType: string;
32
+ histology?: string;
33
+ stage: string;
34
+ diagnosisDate: Date;
35
+ biomarkers?: { name: string; value: string | number; status?: string }[];
36
+ genomicAlterations?: { gene: string; alteration: string }[];
37
+ msiStatus?: 'MSI-H' | 'MSI-L' | 'MSS';
38
+ tmbValue?: number;
39
+ pdl1Score?: number;
40
+ hrdStatus?: boolean;
41
+ };
42
+ treatment: {
43
+ regimen: string;
44
+ drugs: string[];
45
+ setting: string;
46
+ startDate: Date;
47
+ endDate?: Date;
48
+ cycles?: number;
49
+ doseModifications?: boolean;
50
+ };
51
+ outcomes: {
52
+ bestResponse?: 'CR' | 'PR' | 'SD' | 'PD' | 'NE';
53
+ responseDate?: Date;
54
+ progressionDate?: Date;
55
+ deathDate?: Date;
56
+ lastFollowUpDate: Date;
57
+ causeOfDeath?: 'disease' | 'treatment' | 'other' | 'unknown';
58
+ toxicities?: { name: string; grade: number; date?: Date }[];
59
+ };
60
+ ecogAtBaseline?: number;
61
+ priorLines?: number;
62
+ }
63
+
64
+ export interface SystemRecommendation {
65
+ patientId: string;
66
+ recommendedRegimen: string;
67
+ recommendedDrugs: string[];
68
+ predictions: {
69
+ responseRate: number;
70
+ pfsMonths: number;
71
+ osMonths: number;
72
+ toxicityRisk: number;
73
+ };
74
+ matchingBiomarkers: string[];
75
+ confidenceScore: number;
76
+ timestamp: Date;
77
+ }
78
+
79
+ export interface ValidationResult {
80
+ patientId: string;
81
+ concordance: {
82
+ regimenMatch: boolean;
83
+ partialMatch: boolean; // At least one drug matches
84
+ matchedDrugs: string[];
85
+ };
86
+ outcomeComparison: {
87
+ predictedResponse: number;
88
+ actualResponse?: 'CR' | 'PR' | 'SD' | 'PD' | 'NE';
89
+ responseCorrect?: boolean;
90
+
91
+ predictedPFS: number;
92
+ actualPFS?: number; // months
93
+ pfsDifference?: number;
94
+
95
+ predictedOS: number;
96
+ actualOS?: number; // months
97
+ osDifference?: number;
98
+
99
+ predictedToxicityRisk: number;
100
+ actualGrade3PlusToxicity: boolean;
101
+ };
102
+ clinicalBenefit: {
103
+ objectiveResponse: boolean;
104
+ diseaseControl: boolean;
105
+ durableBenefit: boolean; // PFS > 6 months
106
+ };
107
+ }
108
+
109
+ export interface CohortAnalysis {
110
+ cohortId: string;
111
+ description: string;
112
+ patientCount: number;
113
+ dateRange: { start: Date; end: Date };
114
+
115
+ // Demographics
116
+ demographics: {
117
+ medianAge: number;
118
+ ageRange: [number, number];
119
+ genderDistribution: { male: number; female: number };
120
+ stageDistribution: Record<string, number>;
121
+ };
122
+
123
+ // Concordance metrics
124
+ concordance: {
125
+ fullConcordance: number; // Percentage
126
+ partialConcordance: number;
127
+ noConcordance: number;
128
+ concordanceByBiomarker: Record<string, number>;
129
+ };
130
+
131
+ // Prediction accuracy
132
+ predictionAccuracy: {
133
+ responseAccuracy: number;
134
+ responseSensitivity: number; // Ability to predict responders
135
+ responseSpecificity: number; // Ability to predict non-responders
136
+ responseAUC?: number;
137
+
138
+ pfsCIndex: number; // Concordance index for PFS
139
+ pfsCalibration: number; // How well predicted PFS matches actual
140
+
141
+ osCIndex: number;
142
+ osCalibration: number;
143
+
144
+ toxicityAccuracy: number;
145
+ toxicityAUC?: number;
146
+ };
147
+
148
+ // Clinical impact
149
+ clinicalImpact: {
150
+ objectiveResponseRate: number;
151
+ diseaseControlRate: number;
152
+ medianPFS: number;
153
+ medianOS: number;
154
+
155
+ // Comparison metrics
156
+ concordantVsDiscordant: {
157
+ concordantORR: number;
158
+ discordantORR: number;
159
+ concordantMedianPFS: number;
160
+ discordantMedianPFS: number;
161
+ pValue?: number;
162
+ };
163
+ };
164
+
165
+ // Subgroup analyses
166
+ subgroupAnalyses: {
167
+ subgroup: string;
168
+ patientCount: number;
169
+ concordance: number;
170
+ responseAccuracy: number;
171
+ medianPFS: number;
172
+ }[];
173
+ }
174
+
175
+ // ═══════════════════════════════════════════════════════════════════════════════
176
+ // VALIDATION SERVICE
177
+ // ═══════════════════════════════════════════════════════════════════════════════
178
+
179
+ export class RetrospectiveValidationService extends EventEmitter {
180
+ private validationCohorts: Map<string, ValidationPatient[]> = new Map();
181
+ private systemRecommendations: Map<string, SystemRecommendation> = new Map();
182
+ private validationResults: Map<string, ValidationResult> = new Map();
183
+
184
+ constructor() {
185
+ super();
186
+ }
187
+
188
+ /**
189
+ * Load a validation cohort
190
+ */
191
+ loadCohort(cohortId: string, patients: ValidationPatient[]): void {
192
+ // De-identify and validate data
193
+ const validatedPatients = patients.map(p => this.validateAndDeidentify(p));
194
+ this.validationCohorts.set(cohortId, validatedPatients);
195
+ this.emit('cohort-loaded', { cohortId, patientCount: validatedPatients.length });
196
+ }
197
+
198
+ /**
199
+ * Load system recommendations for comparison
200
+ */
201
+ loadRecommendations(recommendations: SystemRecommendation[]): void {
202
+ for (const rec of recommendations) {
203
+ this.systemRecommendations.set(rec.patientId, rec);
204
+ }
205
+ this.emit('recommendations-loaded', { count: recommendations.length });
206
+ }
207
+
208
+ /**
209
+ * Run validation for a cohort
210
+ */
211
+ async runValidation(cohortId: string): Promise<CohortAnalysis> {
212
+ const patients = this.validationCohorts.get(cohortId);
213
+ if (!patients) {
214
+ throw new Error(`Cohort ${cohortId} not found`);
215
+ }
216
+
217
+ const results: ValidationResult[] = [];
218
+
219
+ // Validate each patient
220
+ for (const patient of patients) {
221
+ const recommendation = this.systemRecommendations.get(patient.id);
222
+ const result = this.validatePatient(patient, recommendation);
223
+ results.push(result);
224
+ this.validationResults.set(patient.id, result);
225
+ }
226
+
227
+ // Compute cohort-level metrics
228
+ const analysis = this.computeCohortAnalysis(cohortId, patients, results);
229
+
230
+ this.emit('validation-complete', { cohortId, analysis });
231
+
232
+ return analysis;
233
+ }
234
+
235
+ /**
236
+ * Validate a single patient
237
+ */
238
+ private validatePatient(
239
+ patient: ValidationPatient,
240
+ recommendation?: SystemRecommendation
241
+ ): ValidationResult {
242
+ // Concordance analysis
243
+ const concordance = this.assessConcordance(patient, recommendation);
244
+
245
+ // Calculate actual outcomes
246
+ const actualPFS = this.calculatePFS(patient);
247
+ const actualOS = this.calculateOS(patient);
248
+ const actualGrade3Plus = (patient.outcomes.toxicities || []).some(t => t.grade >= 3);
249
+
250
+ // Compare predictions to outcomes
251
+ const outcomeComparison = {
252
+ predictedResponse: recommendation?.predictions.responseRate || 0,
253
+ actualResponse: patient.outcomes.bestResponse,
254
+ responseCorrect: this.assessResponseAccuracy(
255
+ recommendation?.predictions.responseRate || 0,
256
+ patient.outcomes.bestResponse
257
+ ),
258
+ predictedPFS: recommendation?.predictions.pfsMonths || 0,
259
+ actualPFS,
260
+ pfsDifference: actualPFS !== undefined ? actualPFS - (recommendation?.predictions.pfsMonths || 0) : undefined,
261
+ predictedOS: recommendation?.predictions.osMonths || 0,
262
+ actualOS,
263
+ osDifference: actualOS !== undefined ? actualOS - (recommendation?.predictions.osMonths || 0) : undefined,
264
+ predictedToxicityRisk: recommendation?.predictions.toxicityRisk || 0,
265
+ actualGrade3PlusToxicity: actualGrade3Plus
266
+ };
267
+
268
+ // Clinical benefit assessment
269
+ const clinicalBenefit = {
270
+ objectiveResponse: ['CR', 'PR'].includes(patient.outcomes.bestResponse || ''),
271
+ diseaseControl: ['CR', 'PR', 'SD'].includes(patient.outcomes.bestResponse || ''),
272
+ durableBenefit: actualPFS !== undefined && actualPFS >= 6
273
+ };
274
+
275
+ return {
276
+ patientId: patient.id,
277
+ concordance,
278
+ outcomeComparison,
279
+ clinicalBenefit
280
+ };
281
+ }
282
+
283
+ /**
284
+ * Assess concordance between recommendation and actual treatment
285
+ */
286
+ private assessConcordance(
287
+ patient: ValidationPatient,
288
+ recommendation?: SystemRecommendation
289
+ ): ValidationResult['concordance'] {
290
+ if (!recommendation) {
291
+ return {
292
+ regimenMatch: false,
293
+ partialMatch: false,
294
+ matchedDrugs: []
295
+ };
296
+ }
297
+
298
+ const actualDrugs = patient.treatment.drugs.map(d => d.toLowerCase());
299
+ const recommendedDrugs = recommendation.recommendedDrugs.map(d => d.toLowerCase());
300
+
301
+ const matchedDrugs = recommendedDrugs.filter(rd =>
302
+ actualDrugs.some(ad => this.drugsMatch(ad, rd))
303
+ );
304
+
305
+ const regimenMatch = matchedDrugs.length === recommendedDrugs.length &&
306
+ matchedDrugs.length === actualDrugs.length;
307
+ const partialMatch = matchedDrugs.length > 0;
308
+
309
+ return {
310
+ regimenMatch,
311
+ partialMatch,
312
+ matchedDrugs
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Check if two drug names refer to the same drug
318
+ */
319
+ private drugsMatch(drug1: string, drug2: string): boolean {
320
+ // Normalize names
321
+ const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
322
+ if (normalize(drug1) === normalize(drug2)) return true;
323
+
324
+ // Check brand/generic equivalents
325
+ const equivalents: Record<string, string[]> = {
326
+ 'pembrolizumab': ['keytruda'],
327
+ 'nivolumab': ['opdivo'],
328
+ 'osimertinib': ['tagrisso'],
329
+ 'alectinib': ['alecensa'],
330
+ 'trastuzumab': ['herceptin'],
331
+ 'pertuzumab': ['perjeta'],
332
+ 'olaparib': ['lynparza'],
333
+ 'palbociclib': ['ibrance'],
334
+ 'carboplatin': ['paraplatin'],
335
+ 'paclitaxel': ['taxol'],
336
+ 'docetaxel': ['taxotere']
337
+ };
338
+
339
+ for (const [generic, brands] of Object.entries(equivalents)) {
340
+ const allNames = [generic, ...brands].map(normalize);
341
+ if (allNames.includes(normalize(drug1)) && allNames.includes(normalize(drug2))) {
342
+ return true;
343
+ }
344
+ }
345
+
346
+ return false;
347
+ }
348
+
349
+ /**
350
+ * Calculate PFS in months
351
+ */
352
+ private calculatePFS(patient: ValidationPatient): number | undefined {
353
+ const startDate = patient.treatment.startDate;
354
+ let endDate: Date;
355
+
356
+ if (patient.outcomes.progressionDate) {
357
+ endDate = patient.outcomes.progressionDate;
358
+ } else if (patient.outcomes.deathDate) {
359
+ endDate = patient.outcomes.deathDate;
360
+ } else {
361
+ // Censored at last follow-up
362
+ endDate = patient.outcomes.lastFollowUpDate;
363
+ }
364
+
365
+ const months = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44);
366
+ return Math.round(months * 10) / 10;
367
+ }
368
+
369
+ /**
370
+ * Calculate OS in months
371
+ */
372
+ private calculateOS(patient: ValidationPatient): number | undefined {
373
+ const startDate = patient.diagnosis.diagnosisDate;
374
+ let endDate: Date;
375
+
376
+ if (patient.outcomes.deathDate) {
377
+ endDate = patient.outcomes.deathDate;
378
+ } else {
379
+ // Censored at last follow-up
380
+ endDate = patient.outcomes.lastFollowUpDate;
381
+ }
382
+
383
+ const months = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44);
384
+ return Math.round(months * 10) / 10;
385
+ }
386
+
387
+ /**
388
+ * Assess if response prediction was accurate
389
+ */
390
+ private assessResponseAccuracy(predictedRate: number, actualResponse?: string): boolean | undefined {
391
+ if (!actualResponse || actualResponse === 'NE') return undefined;
392
+
393
+ const predicted = predictedRate >= 0.5 ? 'responder' : 'non-responder';
394
+ const actual = ['CR', 'PR'].includes(actualResponse) ? 'responder' : 'non-responder';
395
+
396
+ return predicted === actual;
397
+ }
398
+
399
+ /**
400
+ * Compute cohort-level analysis
401
+ */
402
+ private computeCohortAnalysis(
403
+ cohortId: string,
404
+ patients: ValidationPatient[],
405
+ results: ValidationResult[]
406
+ ): CohortAnalysis {
407
+ // Demographics
408
+ const ages = patients.map(p => p.demographics.ageAtDiagnosis);
409
+ const maleCount = patients.filter(p => p.demographics.gender === 'male').length;
410
+
411
+ const stageDistribution: Record<string, number> = {};
412
+ for (const p of patients) {
413
+ stageDistribution[p.diagnosis.stage] = (stageDistribution[p.diagnosis.stage] || 0) + 1;
414
+ }
415
+
416
+ // Concordance metrics
417
+ const fullConcordance = results.filter(r => r.concordance.regimenMatch).length / results.length;
418
+ const partialConcordance = results.filter(r => r.concordance.partialMatch && !r.concordance.regimenMatch).length / results.length;
419
+
420
+ // Response accuracy
421
+ const responseResults = results.filter(r => r.outcomeComparison.responseCorrect !== undefined);
422
+ const responseAccuracy = responseResults.length > 0
423
+ ? responseResults.filter(r => r.outcomeComparison.responseCorrect).length / responseResults.length
424
+ : 0;
425
+
426
+ // Sensitivity (correctly predicted responders)
427
+ const actualResponders = results.filter(r =>
428
+ ['CR', 'PR'].includes(r.outcomeComparison.actualResponse || '')
429
+ );
430
+ const responseSensitivity = actualResponders.length > 0
431
+ ? actualResponders.filter(r => r.outcomeComparison.predictedResponse >= 0.5).length / actualResponders.length
432
+ : 0;
433
+
434
+ // Specificity (correctly predicted non-responders)
435
+ const actualNonResponders = results.filter(r =>
436
+ ['SD', 'PD'].includes(r.outcomeComparison.actualResponse || '')
437
+ );
438
+ const responseSpecificity = actualNonResponders.length > 0
439
+ ? actualNonResponders.filter(r => r.outcomeComparison.predictedResponse < 0.5).length / actualNonResponders.length
440
+ : 0;
441
+
442
+ // PFS concordance index (simplified)
443
+ const pfsCIndex = this.calculateCIndex(
444
+ results.map(r => r.outcomeComparison.predictedPFS),
445
+ results.map(r => r.outcomeComparison.actualPFS || 0)
446
+ );
447
+
448
+ // PFS calibration
449
+ const pfsCalibration = this.calculateCalibration(
450
+ results.map(r => r.outcomeComparison.predictedPFS),
451
+ results.map(r => r.outcomeComparison.actualPFS || 0)
452
+ );
453
+
454
+ // OS metrics (similar calculation)
455
+ const osCIndex = this.calculateCIndex(
456
+ results.map(r => r.outcomeComparison.predictedOS),
457
+ results.map(r => r.outcomeComparison.actualOS || 0)
458
+ );
459
+ const osCalibration = this.calculateCalibration(
460
+ results.map(r => r.outcomeComparison.predictedOS),
461
+ results.map(r => r.outcomeComparison.actualOS || 0)
462
+ );
463
+
464
+ // Toxicity accuracy
465
+ const toxicityResults = results.filter(r => r.outcomeComparison.predictedToxicityRisk > 0);
466
+ const toxicityAccuracy = toxicityResults.length > 0
467
+ ? toxicityResults.filter(r => {
468
+ const predicted = r.outcomeComparison.predictedToxicityRisk >= 0.3;
469
+ return predicted === r.outcomeComparison.actualGrade3PlusToxicity;
470
+ }).length / toxicityResults.length
471
+ : 0;
472
+
473
+ // Clinical outcomes
474
+ const objectiveResponseRate = results.filter(r => r.clinicalBenefit.objectiveResponse).length / results.length;
475
+ const diseaseControlRate = results.filter(r => r.clinicalBenefit.diseaseControl).length / results.length;
476
+
477
+ const pfsValues = results.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[];
478
+ const medianPFS = this.calculateMedian(pfsValues);
479
+
480
+ const osValues = results.map(r => r.outcomeComparison.actualOS).filter(v => v !== undefined) as number[];
481
+ const medianOS = this.calculateMedian(osValues);
482
+
483
+ // Concordant vs discordant outcomes
484
+ const concordantResults = results.filter(r => r.concordance.regimenMatch || r.concordance.partialMatch);
485
+ const discordantResults = results.filter(r => !r.concordance.regimenMatch && !r.concordance.partialMatch);
486
+
487
+ const concordantORR = concordantResults.length > 0
488
+ ? concordantResults.filter(r => r.clinicalBenefit.objectiveResponse).length / concordantResults.length
489
+ : 0;
490
+ const discordantORR = discordantResults.length > 0
491
+ ? discordantResults.filter(r => r.clinicalBenefit.objectiveResponse).length / discordantResults.length
492
+ : 0;
493
+
494
+ const concordantPFS = this.calculateMedian(
495
+ concordantResults.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[]
496
+ );
497
+ const discordantPFS = this.calculateMedian(
498
+ discordantResults.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[]
499
+ );
500
+
501
+ // Subgroup analyses
502
+ const subgroupAnalyses = this.performSubgroupAnalyses(patients, results);
503
+
504
+ return {
505
+ cohortId,
506
+ description: `Validation cohort with ${patients.length} patients`,
507
+ patientCount: patients.length,
508
+ dateRange: {
509
+ start: new Date(Math.min(...patients.map(p => p.diagnosis.diagnosisDate.getTime()))),
510
+ end: new Date(Math.max(...patients.map(p => p.outcomes.lastFollowUpDate.getTime())))
511
+ },
512
+ demographics: {
513
+ medianAge: this.calculateMedian(ages),
514
+ ageRange: [Math.min(...ages), Math.max(...ages)],
515
+ genderDistribution: {
516
+ male: maleCount / patients.length,
517
+ female: (patients.length - maleCount) / patients.length
518
+ },
519
+ stageDistribution
520
+ },
521
+ concordance: {
522
+ fullConcordance,
523
+ partialConcordance,
524
+ noConcordance: 1 - fullConcordance - partialConcordance,
525
+ concordanceByBiomarker: this.calculateConcordanceByBiomarker(patients, results)
526
+ },
527
+ predictionAccuracy: {
528
+ responseAccuracy,
529
+ responseSensitivity,
530
+ responseSpecificity,
531
+ pfsCIndex,
532
+ pfsCalibration,
533
+ osCIndex,
534
+ osCalibration,
535
+ toxicityAccuracy
536
+ },
537
+ clinicalImpact: {
538
+ objectiveResponseRate,
539
+ diseaseControlRate,
540
+ medianPFS,
541
+ medianOS,
542
+ concordantVsDiscordant: {
543
+ concordantORR,
544
+ discordantORR,
545
+ concordantMedianPFS: concordantPFS,
546
+ discordantMedianPFS: discordantPFS
547
+ }
548
+ },
549
+ subgroupAnalyses
550
+ };
551
+ }
552
+
553
+ /**
554
+ * Calculate concordance index (C-index)
555
+ */
556
+ private calculateCIndex(predicted: number[], actual: number[]): number {
557
+ let concordant = 0;
558
+ let discordant = 0;
559
+ let tied = 0;
560
+
561
+ for (let i = 0; i < predicted.length; i++) {
562
+ for (let j = i + 1; j < predicted.length; j++) {
563
+ if (actual[i] !== actual[j]) {
564
+ if ((predicted[i] > predicted[j] && actual[i] > actual[j]) ||
565
+ (predicted[i] < predicted[j] && actual[i] < actual[j])) {
566
+ concordant++;
567
+ } else if (predicted[i] !== predicted[j]) {
568
+ discordant++;
569
+ } else {
570
+ tied++;
571
+ }
572
+ }
573
+ }
574
+ }
575
+
576
+ const total = concordant + discordant + tied;
577
+ return total > 0 ? (concordant + 0.5 * tied) / total : 0.5;
578
+ }
579
+
580
+ /**
581
+ * Calculate calibration score (0-1, higher is better)
582
+ */
583
+ private calculateCalibration(predicted: number[], actual: number[]): number {
584
+ if (predicted.length === 0) return 0;
585
+
586
+ // Group predictions into deciles and compare to actual
587
+ const combined = predicted.map((p, i) => ({ predicted: p, actual: actual[i] }))
588
+ .filter(c => c.actual > 0)
589
+ .sort((a, b) => a.predicted - b.predicted);
590
+
591
+ if (combined.length < 10) {
592
+ // Simple correlation for small samples
593
+ const meanPred = combined.reduce((s, c) => s + c.predicted, 0) / combined.length;
594
+ const meanActual = combined.reduce((s, c) => s + c.actual, 0) / combined.length;
595
+
596
+ const errors = combined.map(c => Math.abs(c.predicted - c.actual) / Math.max(c.actual, 1));
597
+ const meanError = errors.reduce((s, e) => s + e, 0) / errors.length;
598
+
599
+ return Math.max(0, 1 - meanError);
600
+ }
601
+
602
+ // For larger samples, use decile calibration
603
+ const decileSize = Math.ceil(combined.length / 10);
604
+ let totalError = 0;
605
+
606
+ for (let i = 0; i < 10; i++) {
607
+ const start = i * decileSize;
608
+ const end = Math.min((i + 1) * decileSize, combined.length);
609
+ const decile = combined.slice(start, end);
610
+
611
+ const avgPredicted = decile.reduce((s, c) => s + c.predicted, 0) / decile.length;
612
+ const avgActual = decile.reduce((s, c) => s + c.actual, 0) / decile.length;
613
+
614
+ const relativeError = avgActual > 0 ? Math.abs(avgPredicted - avgActual) / avgActual : 0;
615
+ totalError += relativeError;
616
+ }
617
+
618
+ return Math.max(0, 1 - totalError / 10);
619
+ }
620
+
621
+ /**
622
+ * Calculate median of an array
623
+ */
624
+ private calculateMedian(values: number[]): number {
625
+ if (values.length === 0) return 0;
626
+ const sorted = [...values].sort((a, b) => a - b);
627
+ const mid = Math.floor(sorted.length / 2);
628
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
629
+ }
630
+
631
+ /**
632
+ * Calculate concordance by biomarker subgroup
633
+ */
634
+ private calculateConcordanceByBiomarker(
635
+ patients: ValidationPatient[],
636
+ results: ValidationResult[]
637
+ ): Record<string, number> {
638
+ const biomarkerGroups: Record<string, { concordant: number; total: number }> = {};
639
+
640
+ for (let i = 0; i < patients.length; i++) {
641
+ const patient = patients[i];
642
+ const result = results[i];
643
+
644
+ // Check various biomarker categories
645
+ const biomarkers: string[] = [];
646
+
647
+ if (patient.diagnosis.msiStatus === 'MSI-H') biomarkers.push('MSI-H');
648
+ if (patient.diagnosis.tmbValue && patient.diagnosis.tmbValue >= 10) biomarkers.push('TMB-H');
649
+ if (patient.diagnosis.pdl1Score && patient.diagnosis.pdl1Score >= 50) biomarkers.push('PD-L1≥50%');
650
+ if (patient.diagnosis.hrdStatus) biomarkers.push('HRD+');
651
+
652
+ // Check genomic alterations
653
+ for (const alt of patient.diagnosis.genomicAlterations || []) {
654
+ biomarkers.push(`${alt.gene} ${alt.alteration}`);
655
+ }
656
+
657
+ // Record concordance for each biomarker
658
+ for (const biomarker of biomarkers) {
659
+ if (!biomarkerGroups[biomarker]) {
660
+ biomarkerGroups[biomarker] = { concordant: 0, total: 0 };
661
+ }
662
+ biomarkerGroups[biomarker].total++;
663
+ if (result.concordance.regimenMatch || result.concordance.partialMatch) {
664
+ biomarkerGroups[biomarker].concordant++;
665
+ }
666
+ }
667
+ }
668
+
669
+ // Convert to concordance rates
670
+ const concordanceByBiomarker: Record<string, number> = {};
671
+ for (const [biomarker, data] of Object.entries(biomarkerGroups)) {
672
+ if (data.total >= 5) { // Only include groups with meaningful sample size
673
+ concordanceByBiomarker[biomarker] = data.concordant / data.total;
674
+ }
675
+ }
676
+
677
+ return concordanceByBiomarker;
678
+ }
679
+
680
+ /**
681
+ * Perform subgroup analyses
682
+ */
683
+ private performSubgroupAnalyses(
684
+ patients: ValidationPatient[],
685
+ results: ValidationResult[]
686
+ ): CohortAnalysis['subgroupAnalyses'] {
687
+ const subgroups: CohortAnalysis['subgroupAnalyses'] = [];
688
+
689
+ // By cancer type
690
+ const cancerTypes = [...new Set(patients.map(p => p.diagnosis.cancerType))];
691
+ for (const cancerType of cancerTypes) {
692
+ const indices = patients.map((p, i) => p.diagnosis.cancerType === cancerType ? i : -1).filter(i => i >= 0);
693
+ if (indices.length >= 10) {
694
+ const subgroupResults = indices.map(i => results[i]);
695
+ const subgroupPatients = indices.map(i => patients[i]);
696
+
697
+ subgroups.push({
698
+ subgroup: cancerType,
699
+ patientCount: indices.length,
700
+ concordance: subgroupResults.filter(r => r.concordance.regimenMatch || r.concordance.partialMatch).length / indices.length,
701
+ responseAccuracy: this.calculateSubgroupResponseAccuracy(subgroupResults),
702
+ medianPFS: this.calculateMedian(
703
+ subgroupResults.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[]
704
+ )
705
+ });
706
+ }
707
+ }
708
+
709
+ // By stage
710
+ const stages = ['I', 'II', 'III', 'IV'];
711
+ for (const stageGroup of stages) {
712
+ const indices = patients.map((p, i) => p.diagnosis.stage.startsWith(stageGroup) ? i : -1).filter(i => i >= 0);
713
+ if (indices.length >= 10) {
714
+ const subgroupResults = indices.map(i => results[i]);
715
+
716
+ subgroups.push({
717
+ subgroup: `Stage ${stageGroup}`,
718
+ patientCount: indices.length,
719
+ concordance: subgroupResults.filter(r => r.concordance.regimenMatch || r.concordance.partialMatch).length / indices.length,
720
+ responseAccuracy: this.calculateSubgroupResponseAccuracy(subgroupResults),
721
+ medianPFS: this.calculateMedian(
722
+ subgroupResults.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[]
723
+ )
724
+ });
725
+ }
726
+ }
727
+
728
+ // By age group
729
+ const ageGroups = [
730
+ { name: 'Age <50', filter: (p: ValidationPatient) => p.demographics.ageAtDiagnosis < 50 },
731
+ { name: 'Age 50-65', filter: (p: ValidationPatient) => p.demographics.ageAtDiagnosis >= 50 && p.demographics.ageAtDiagnosis < 65 },
732
+ { name: 'Age 65-75', filter: (p: ValidationPatient) => p.demographics.ageAtDiagnosis >= 65 && p.demographics.ageAtDiagnosis < 75 },
733
+ { name: 'Age ≥75', filter: (p: ValidationPatient) => p.demographics.ageAtDiagnosis >= 75 }
734
+ ];
735
+
736
+ for (const ageGroup of ageGroups) {
737
+ const indices = patients.map((p, i) => ageGroup.filter(p) ? i : -1).filter(i => i >= 0);
738
+ if (indices.length >= 10) {
739
+ const subgroupResults = indices.map(i => results[i]);
740
+
741
+ subgroups.push({
742
+ subgroup: ageGroup.name,
743
+ patientCount: indices.length,
744
+ concordance: subgroupResults.filter(r => r.concordance.regimenMatch || r.concordance.partialMatch).length / indices.length,
745
+ responseAccuracy: this.calculateSubgroupResponseAccuracy(subgroupResults),
746
+ medianPFS: this.calculateMedian(
747
+ subgroupResults.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[]
748
+ )
749
+ });
750
+ }
751
+ }
752
+
753
+ return subgroups;
754
+ }
755
+
756
+ /**
757
+ * Calculate response accuracy for a subgroup
758
+ */
759
+ private calculateSubgroupResponseAccuracy(results: ValidationResult[]): number {
760
+ const withResponse = results.filter(r => r.outcomeComparison.responseCorrect !== undefined);
761
+ return withResponse.length > 0
762
+ ? withResponse.filter(r => r.outcomeComparison.responseCorrect).length / withResponse.length
763
+ : 0;
764
+ }
765
+
766
+ /**
767
+ * Validate and de-identify patient data
768
+ */
769
+ private validateAndDeidentify(patient: ValidationPatient): ValidationPatient {
770
+ // Generate hashed de-identified ID
771
+ const deidentifiedId = createHash('sha256')
772
+ .update(patient.id + 'SALT_CHANGE_IN_PRODUCTION')
773
+ .digest('hex')
774
+ .substring(0, 12);
775
+
776
+ return {
777
+ ...patient,
778
+ id: deidentifiedId
779
+ };
780
+ }
781
+
782
+ /**
783
+ * Generate validation report
784
+ */
785
+ generateReport(analysis: CohortAnalysis): string {
786
+ const lines: string[] = [
787
+ '═'.repeat(70),
788
+ ' RETROSPECTIVE VALIDATION REPORT',
789
+ '═'.repeat(70),
790
+ '',
791
+ `Cohort: ${analysis.cohortId}`,
792
+ `Patients: ${analysis.patientCount}`,
793
+ `Date Range: ${analysis.dateRange.start.toISOString().split('T')[0]} to ${analysis.dateRange.end.toISOString().split('T')[0]}`,
794
+ '',
795
+ '─'.repeat(70),
796
+ ' CONCORDANCE METRICS',
797
+ '─'.repeat(70),
798
+ `Full Concordance: ${(analysis.concordance.fullConcordance * 100).toFixed(1)}%`,
799
+ `Partial Concordance: ${(analysis.concordance.partialConcordance * 100).toFixed(1)}%`,
800
+ `No Concordance: ${(analysis.concordance.noConcordance * 100).toFixed(1)}%`,
801
+ '',
802
+ '─'.repeat(70),
803
+ ' PREDICTION ACCURACY',
804
+ '─'.repeat(70),
805
+ `Response Accuracy: ${(analysis.predictionAccuracy.responseAccuracy * 100).toFixed(1)}%`,
806
+ `Response Sensitivity: ${(analysis.predictionAccuracy.responseSensitivity * 100).toFixed(1)}%`,
807
+ `Response Specificity: ${(analysis.predictionAccuracy.responseSpecificity * 100).toFixed(1)}%`,
808
+ `PFS C-Index: ${analysis.predictionAccuracy.pfsCIndex.toFixed(3)}`,
809
+ `PFS Calibration: ${(analysis.predictionAccuracy.pfsCalibration * 100).toFixed(1)}%`,
810
+ `OS C-Index: ${analysis.predictionAccuracy.osCIndex.toFixed(3)}`,
811
+ `Toxicity Accuracy: ${(analysis.predictionAccuracy.toxicityAccuracy * 100).toFixed(1)}%`,
812
+ '',
813
+ '─'.repeat(70),
814
+ ' CLINICAL OUTCOMES',
815
+ '─'.repeat(70),
816
+ `Objective Response Rate: ${(analysis.clinicalImpact.objectiveResponseRate * 100).toFixed(1)}%`,
817
+ `Disease Control Rate: ${(analysis.clinicalImpact.diseaseControlRate * 100).toFixed(1)}%`,
818
+ `Median PFS: ${analysis.clinicalImpact.medianPFS.toFixed(1)} months`,
819
+ `Median OS: ${analysis.clinicalImpact.medianOS.toFixed(1)} months`,
820
+ '',
821
+ ' Concordant vs Discordant Treatment:',
822
+ ` Concordant ORR: ${(analysis.clinicalImpact.concordantVsDiscordant.concordantORR * 100).toFixed(1)}%`,
823
+ ` Discordant ORR: ${(analysis.clinicalImpact.concordantVsDiscordant.discordantORR * 100).toFixed(1)}%`,
824
+ ` Concordant Median PFS: ${analysis.clinicalImpact.concordantVsDiscordant.concordantMedianPFS.toFixed(1)} months`,
825
+ ` Discordant Median PFS: ${analysis.clinicalImpact.concordantVsDiscordant.discordantMedianPFS.toFixed(1)} months`,
826
+ '',
827
+ '─'.repeat(70),
828
+ ' SUBGROUP ANALYSES',
829
+ '─'.repeat(70),
830
+ ...analysis.subgroupAnalyses.map(sg =>
831
+ `${sg.subgroup}: n=${sg.patientCount}, Concordance=${(sg.concordance * 100).toFixed(0)}%, ` +
832
+ `Accuracy=${(sg.responseAccuracy * 100).toFixed(0)}%, mPFS=${sg.medianPFS.toFixed(1)}mo`
833
+ ),
834
+ '',
835
+ '═'.repeat(70)
836
+ ];
837
+
838
+ return lines.join('\n');
839
+ }
840
+
841
+ /**
842
+ * Export validation results
843
+ */
844
+ exportResults(cohortId: string, format: 'json' | 'csv'): string {
845
+ const patients = this.validationCohorts.get(cohortId);
846
+ if (!patients) return '';
847
+
848
+ const results: any[] = [];
849
+ for (const patient of patients) {
850
+ const result = this.validationResults.get(patient.id);
851
+ if (result) {
852
+ results.push({
853
+ patientId: patient.id,
854
+ cancerType: patient.diagnosis.cancerType,
855
+ stage: patient.diagnosis.stage,
856
+ actualRegimen: patient.treatment.regimen,
857
+ regimenMatch: result.concordance.regimenMatch,
858
+ partialMatch: result.concordance.partialMatch,
859
+ predictedResponse: result.outcomeComparison.predictedResponse,
860
+ actualResponse: result.outcomeComparison.actualResponse,
861
+ responseCorrect: result.outcomeComparison.responseCorrect,
862
+ predictedPFS: result.outcomeComparison.predictedPFS,
863
+ actualPFS: result.outcomeComparison.actualPFS,
864
+ predictedOS: result.outcomeComparison.predictedOS,
865
+ actualOS: result.outcomeComparison.actualOS,
866
+ objectiveResponse: result.clinicalBenefit.objectiveResponse,
867
+ diseaseControl: result.clinicalBenefit.diseaseControl
868
+ });
869
+ }
870
+ }
871
+
872
+ if (format === 'json') {
873
+ return JSON.stringify(results, null, 2);
874
+ }
875
+
876
+ // CSV format
877
+ const headers = Object.keys(results[0] || {});
878
+ const rows = [
879
+ headers.join(','),
880
+ ...results.map(r => headers.map(h => JSON.stringify(r[h] ?? '')).join(','))
881
+ ];
882
+
883
+ return rows.join('\n');
884
+ }
885
+ }
886
+
887
+ export default RetrospectiveValidationService;