@erosolaraijs/cure 1.0.1 → 1.0.2

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 (68) hide show
  1. package/dist/bin/cure.d.ts +2 -5
  2. package/dist/bin/cure.d.ts.map +1 -1
  3. package/dist/bin/cure.js +285 -124
  4. package/dist/bin/cure.js.map +1 -1
  5. package/dist/clinician/decisionSupport.d.ts +325 -0
  6. package/dist/clinician/decisionSupport.d.ts.map +1 -0
  7. package/dist/clinician/decisionSupport.js +604 -0
  8. package/dist/clinician/decisionSupport.js.map +1 -0
  9. package/dist/clinician/index.d.ts +5 -0
  10. package/dist/clinician/index.d.ts.map +1 -0
  11. package/dist/clinician/index.js +5 -0
  12. package/dist/clinician/index.js.map +1 -0
  13. package/dist/compliance/index.d.ts +5 -0
  14. package/dist/compliance/index.d.ts.map +1 -0
  15. package/dist/compliance/index.js +5 -0
  16. package/dist/compliance/index.js.map +1 -0
  17. package/dist/integrations/clinicalTrials/index.d.ts +5 -0
  18. package/dist/integrations/clinicalTrials/index.d.ts.map +1 -0
  19. package/dist/integrations/clinicalTrials/index.js +5 -0
  20. package/dist/integrations/clinicalTrials/index.js.map +1 -0
  21. package/dist/integrations/ehr/index.d.ts +5 -0
  22. package/dist/integrations/ehr/index.d.ts.map +1 -0
  23. package/dist/integrations/ehr/index.js +5 -0
  24. package/dist/integrations/ehr/index.js.map +1 -0
  25. package/dist/integrations/genomics/index.d.ts +5 -0
  26. package/dist/integrations/genomics/index.d.ts.map +1 -0
  27. package/dist/integrations/genomics/index.js +5 -0
  28. package/dist/integrations/genomics/index.js.map +1 -0
  29. package/dist/integrations/index.d.ts +7 -0
  30. package/dist/integrations/index.d.ts.map +1 -0
  31. package/dist/integrations/index.js +10 -0
  32. package/dist/integrations/index.js.map +1 -0
  33. package/dist/ml/index.d.ts +5 -0
  34. package/dist/ml/index.d.ts.map +1 -0
  35. package/dist/ml/index.js +5 -0
  36. package/dist/ml/index.js.map +1 -0
  37. package/dist/ml/outcomePredictor.d.ts +297 -0
  38. package/dist/ml/outcomePredictor.d.ts.map +1 -0
  39. package/dist/ml/outcomePredictor.js +823 -0
  40. package/dist/ml/outcomePredictor.js.map +1 -0
  41. package/dist/patient/index.d.ts +5 -0
  42. package/dist/patient/index.d.ts.map +1 -0
  43. package/dist/patient/index.js +5 -0
  44. package/dist/patient/index.js.map +1 -0
  45. package/dist/patient/patientPortal.d.ts +337 -0
  46. package/dist/patient/patientPortal.d.ts.map +1 -0
  47. package/dist/patient/patientPortal.js +667 -0
  48. package/dist/patient/patientPortal.js.map +1 -0
  49. package/dist/safety/drugInteractions.d.ts +230 -0
  50. package/dist/safety/drugInteractions.d.ts.map +1 -0
  51. package/dist/safety/drugInteractions.js +697 -0
  52. package/dist/safety/drugInteractions.js.map +1 -0
  53. package/dist/safety/index.d.ts +5 -0
  54. package/dist/safety/index.d.ts.map +1 -0
  55. package/dist/safety/index.js +5 -0
  56. package/dist/safety/index.js.map +1 -0
  57. package/dist/validation/index.d.ts +5 -0
  58. package/dist/validation/index.d.ts.map +1 -0
  59. package/dist/validation/index.js +5 -0
  60. package/dist/validation/index.js.map +1 -0
  61. package/dist/validation/retrospectiveValidator.d.ts +246 -0
  62. package/dist/validation/retrospectiveValidator.d.ts.map +1 -0
  63. package/dist/validation/retrospectiveValidator.js +602 -0
  64. package/dist/validation/retrospectiveValidator.js.map +1 -0
  65. package/package.json +1 -1
  66. package/src/bin/cure.ts +331 -140
  67. package/src/clinician/decisionSupport.ts +949 -0
  68. package/src/patient/patientPortal.ts +1039 -0
@@ -0,0 +1,949 @@
1
+ /**
2
+ * Clinician Decision Support System
3
+ *
4
+ * Provides clinical decision support features:
5
+ * - Treatment recommendations with explanations
6
+ * - Override management and documentation
7
+ * - Tumor board integration
8
+ * - Evidence summaries
9
+ * - Risk-benefit visualization
10
+ * - Alert acknowledgment workflows
11
+ *
12
+ * Designed to augment (not replace) clinical judgment.
13
+ */
14
+
15
+ import { EventEmitter } from 'events';
16
+
17
+ // ═══════════════════════════════════════════════════════════════════════════════
18
+ // DECISION SUPPORT TYPES
19
+ // ═══════════════════════════════════════════════════════════════════════════════
20
+
21
+ export interface ClinicalRecommendation {
22
+ id: string;
23
+ patientId: string;
24
+ generatedAt: Date;
25
+ validUntil: Date;
26
+
27
+ primaryRecommendation: {
28
+ regimen: string;
29
+ drugs: { name: string; dose: string; schedule: string; route: string }[];
30
+ setting: 'neoadjuvant' | 'adjuvant' | 'first-line' | 'second-line' | 'third-line-plus' | 'maintenance';
31
+ confidenceScore: number;
32
+ };
33
+
34
+ alternativeOptions: {
35
+ regimen: string;
36
+ drugs: string[];
37
+ confidenceScore: number;
38
+ comparisonToPrimary: string;
39
+ }[];
40
+
41
+ supportingEvidence: {
42
+ type: 'clinical-trial' | 'guideline' | 'real-world' | 'expert-consensus';
43
+ source: string;
44
+ description: string;
45
+ strength: 'strong' | 'moderate' | 'limited';
46
+ url?: string;
47
+ }[];
48
+
49
+ biomarkerRationale: {
50
+ biomarker: string;
51
+ status: string;
52
+ impact: string;
53
+ evidenceLevel: string;
54
+ }[];
55
+
56
+ predictions: {
57
+ responseRate: { value: number; ci95: [number, number] };
58
+ medianPFS: { value: number; ci95: [number, number]; unit: 'months' };
59
+ medianOS: { value: number; ci95: [number, number]; unit: 'months' };
60
+ grade3PlusToxicity: { value: number; topRisks: string[] };
61
+ qualityOfLife: { score: number; considerations: string[] };
62
+ };
63
+
64
+ clinicalConsiderations: {
65
+ category: 'efficacy' | 'safety' | 'practical' | 'cost';
66
+ consideration: string;
67
+ importance: 'critical' | 'important' | 'notable';
68
+ }[];
69
+
70
+ monitoringPlan: {
71
+ item: string;
72
+ frequency: string;
73
+ rationale: string;
74
+ }[];
75
+
76
+ clinicalTrialOptions?: {
77
+ nctId: string;
78
+ title: string;
79
+ phase: string;
80
+ matchingBiomarkers: string[];
81
+ distance?: number;
82
+ }[];
83
+
84
+ status: 'active' | 'accepted' | 'modified' | 'rejected' | 'expired';
85
+ reviewedBy?: string;
86
+ reviewedAt?: Date;
87
+ }
88
+
89
+ export interface ClinicianOverride {
90
+ id: string;
91
+ recommendationId: string;
92
+ patientId: string;
93
+ clinicianId: string;
94
+ clinicianName: string;
95
+ clinicianRole: string;
96
+
97
+ timestamp: Date;
98
+ overrideType: 'full' | 'partial' | 'modification';
99
+
100
+ originalRecommendation: {
101
+ regimen: string;
102
+ drugs: string[];
103
+ };
104
+
105
+ selectedTreatment: {
106
+ regimen: string;
107
+ drugs: string[];
108
+ doses?: string[];
109
+ };
110
+
111
+ reason: {
112
+ category: OverrideCategory;
113
+ freeText: string;
114
+ clinicalJustification: string;
115
+ };
116
+
117
+ riskAcknowledgment?: {
118
+ acknowledgedRisks: string[];
119
+ mitigationPlan?: string;
120
+ signature: string;
121
+ timestamp: Date;
122
+ };
123
+
124
+ supervisorApproval?: {
125
+ required: boolean;
126
+ supervisorId?: string;
127
+ supervisorName?: string;
128
+ approved?: boolean;
129
+ approvalTimestamp?: Date;
130
+ comments?: string;
131
+ };
132
+
133
+ auditStatus: 'pending-review' | 'reviewed' | 'flagged';
134
+ }
135
+
136
+ export type OverrideCategory =
137
+ | 'patient-preference'
138
+ | 'comorbidity'
139
+ | 'prior-treatment'
140
+ | 'access-availability'
141
+ | 'clinical-trial'
142
+ | 'contraindication'
143
+ | 'organ-function'
144
+ | 'performance-status'
145
+ | 'cost-coverage'
146
+ | 'clinical-judgment'
147
+ | 'other';
148
+
149
+ export interface TumorBoardCase {
150
+ id: string;
151
+ patientId: string;
152
+ scheduledDate: Date;
153
+ status: 'scheduled' | 'presented' | 'completed' | 'deferred';
154
+
155
+ presenter: {
156
+ id: string;
157
+ name: string;
158
+ role: string;
159
+ department: string;
160
+ };
161
+
162
+ attendees: {
163
+ id: string;
164
+ name: string;
165
+ specialty: string;
166
+ present: boolean;
167
+ }[];
168
+
169
+ caseSummary: {
170
+ diagnosis: string;
171
+ stage: string;
172
+ relevantHistory: string;
173
+ currentStatus: string;
174
+ questionForBoard: string;
175
+ };
176
+
177
+ systemRecommendation?: ClinicalRecommendation;
178
+
179
+ boardDecision?: {
180
+ recommendation: string;
181
+ rationale: string;
182
+ votingResult?: { agree: number; disagree: number; abstain: number };
183
+ dissenting_opinions?: string[];
184
+ followUpPlan: string;
185
+ recordedBy: string;
186
+ timestamp: Date;
187
+ };
188
+
189
+ actions?: {
190
+ action: string;
191
+ assignedTo: string;
192
+ dueDate?: Date;
193
+ completed: boolean;
194
+ }[];
195
+ }
196
+
197
+ export interface AlertAcknowledgment {
198
+ alertId: string;
199
+ alertType: string;
200
+ severity: 'critical' | 'high' | 'moderate' | 'low';
201
+ acknowledgedBy: string;
202
+ acknowledgedAt: Date;
203
+ action: 'proceed' | 'modify' | 'cancel' | 'defer';
204
+ reason?: string;
205
+ signature: string;
206
+ }
207
+
208
+ // ═══════════════════════════════════════════════════════════════════════════════
209
+ // DECISION SUPPORT SERVICE
210
+ // ═══════════════════════════════════════════════════════════════════════════════
211
+
212
+ export class ClinicalDecisionSupportService extends EventEmitter {
213
+ private recommendations: Map<string, ClinicalRecommendation> = new Map();
214
+ private overrides: Map<string, ClinicianOverride[]> = new Map();
215
+ private tumorBoardCases: Map<string, TumorBoardCase> = new Map();
216
+ private alertAcknowledgments: AlertAcknowledgment[] = [];
217
+
218
+ constructor() {
219
+ super();
220
+ }
221
+
222
+ /**
223
+ * Generate a clinical recommendation
224
+ */
225
+ async generateRecommendation(params: {
226
+ patientId: string;
227
+ cancerType: string;
228
+ stage: string;
229
+ biomarkers: { name: string; value: string | number; status?: string }[];
230
+ genomicAlterations: { gene: string; alteration: string }[];
231
+ priorTherapies?: string[];
232
+ performanceStatus?: number;
233
+ comorbidities?: string[];
234
+ patientPreferences?: string[];
235
+ }): Promise<ClinicalRecommendation> {
236
+ const id = `REC-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
237
+ const now = new Date();
238
+
239
+ // Generate recommendation based on inputs
240
+ // In production, this would call the ML prediction service and knowledge base
241
+ const recommendation: ClinicalRecommendation = {
242
+ id,
243
+ patientId: params.patientId,
244
+ generatedAt: now,
245
+ validUntil: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000), // 7 days
246
+
247
+ primaryRecommendation: this.determinePrimaryRecommendation(params),
248
+ alternativeOptions: this.determineAlternatives(params),
249
+ supportingEvidence: this.gatherEvidence(params),
250
+ biomarkerRationale: this.generateBiomarkerRationale(params.biomarkers, params.genomicAlterations),
251
+ predictions: this.generatePredictions(params),
252
+ clinicalConsiderations: this.identifyConsiderations(params),
253
+ monitoringPlan: this.generateMonitoringPlan(params),
254
+ clinicalTrialOptions: [],
255
+ status: 'active'
256
+ };
257
+
258
+ this.recommendations.set(id, recommendation);
259
+ this.emit('recommendation-generated', recommendation);
260
+
261
+ return recommendation;
262
+ }
263
+
264
+ /**
265
+ * Get recommendation by ID
266
+ */
267
+ getRecommendation(recommendationId: string): ClinicalRecommendation | undefined {
268
+ return this.recommendations.get(recommendationId);
269
+ }
270
+
271
+ /**
272
+ * Get recommendations for a patient
273
+ */
274
+ getPatientRecommendations(patientId: string): ClinicalRecommendation[] {
275
+ return Array.from(this.recommendations.values())
276
+ .filter(r => r.patientId === patientId)
277
+ .sort((a, b) => b.generatedAt.getTime() - a.generatedAt.getTime());
278
+ }
279
+
280
+ /**
281
+ * Accept a recommendation
282
+ */
283
+ async acceptRecommendation(
284
+ recommendationId: string,
285
+ clinicianId: string,
286
+ clinicianName: string
287
+ ): Promise<void> {
288
+ const rec = this.recommendations.get(recommendationId);
289
+ if (!rec) throw new Error('Recommendation not found');
290
+
291
+ rec.status = 'accepted';
292
+ rec.reviewedBy = clinicianId;
293
+ rec.reviewedAt = new Date();
294
+
295
+ this.emit('recommendation-accepted', {
296
+ recommendationId,
297
+ clinicianId,
298
+ clinicianName,
299
+ recommendation: rec.primaryRecommendation.regimen
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Record an override
305
+ */
306
+ async recordOverride(override: Omit<ClinicianOverride, 'id' | 'timestamp' | 'auditStatus'>): Promise<string> {
307
+ const id = `OVR-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
308
+
309
+ const fullOverride: ClinicianOverride = {
310
+ ...override,
311
+ id,
312
+ timestamp: new Date(),
313
+ auditStatus: this.requiresSupervisorApproval(override) ? 'pending-review' : 'reviewed'
314
+ };
315
+
316
+ // Check if supervisor approval is required
317
+ if (this.requiresSupervisorApproval(override)) {
318
+ fullOverride.supervisorApproval = {
319
+ required: true
320
+ };
321
+ }
322
+
323
+ // Update recommendation status
324
+ const rec = this.recommendations.get(override.recommendationId);
325
+ if (rec) {
326
+ rec.status = override.overrideType === 'full' ? 'rejected' : 'modified';
327
+ rec.reviewedBy = override.clinicianId;
328
+ rec.reviewedAt = new Date();
329
+ }
330
+
331
+ // Store override
332
+ const patientOverrides = this.overrides.get(override.patientId) || [];
333
+ patientOverrides.push(fullOverride);
334
+ this.overrides.set(override.patientId, patientOverrides);
335
+
336
+ this.emit('override-recorded', fullOverride);
337
+
338
+ return id;
339
+ }
340
+
341
+ /**
342
+ * Check if supervisor approval is required
343
+ */
344
+ private requiresSupervisorApproval(override: Omit<ClinicianOverride, 'id' | 'timestamp' | 'auditStatus'>): boolean {
345
+ // Require approval for certain categories
346
+ const requiresApproval = [
347
+ 'clinical-judgment',
348
+ 'other'
349
+ ];
350
+
351
+ if (requiresApproval.includes(override.reason.category)) return true;
352
+
353
+ // Require approval if clinician is a trainee
354
+ if (['resident', 'fellow', 'np-trainee', 'pa-trainee'].some(r =>
355
+ override.clinicianRole.toLowerCase().includes(r)
356
+ )) {
357
+ return true;
358
+ }
359
+
360
+ return false;
361
+ }
362
+
363
+ /**
364
+ * Approve an override
365
+ */
366
+ async approveOverride(
367
+ overrideId: string,
368
+ supervisorId: string,
369
+ supervisorName: string,
370
+ approved: boolean,
371
+ comments?: string
372
+ ): Promise<void> {
373
+ for (const [patientId, overrides] of this.overrides) {
374
+ const override = overrides.find(o => o.id === overrideId);
375
+ if (override && override.supervisorApproval) {
376
+ override.supervisorApproval.supervisorId = supervisorId;
377
+ override.supervisorApproval.supervisorName = supervisorName;
378
+ override.supervisorApproval.approved = approved;
379
+ override.supervisorApproval.approvalTimestamp = new Date();
380
+ override.supervisorApproval.comments = comments;
381
+ override.auditStatus = approved ? 'reviewed' : 'flagged';
382
+
383
+ this.emit('override-reviewed', { overrideId, approved, supervisorName });
384
+ return;
385
+ }
386
+ }
387
+
388
+ throw new Error('Override not found');
389
+ }
390
+
391
+ /**
392
+ * Get overrides for a patient
393
+ */
394
+ getPatientOverrides(patientId: string): ClinicianOverride[] {
395
+ return this.overrides.get(patientId) || [];
396
+ }
397
+
398
+ /**
399
+ * Get pending approvals for a supervisor
400
+ */
401
+ getPendingApprovals(): ClinicianOverride[] {
402
+ const pending: ClinicianOverride[] = [];
403
+
404
+ for (const overrides of this.overrides.values()) {
405
+ for (const override of overrides) {
406
+ if (override.supervisorApproval?.required && !override.supervisorApproval.approved) {
407
+ pending.push(override);
408
+ }
409
+ }
410
+ }
411
+
412
+ return pending.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
413
+ }
414
+
415
+ /**
416
+ * Create a tumor board case
417
+ */
418
+ async createTumorBoardCase(caseData: Omit<TumorBoardCase, 'id' | 'status' | 'boardDecision' | 'actions'>): Promise<string> {
419
+ const id = `TB-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
420
+
421
+ const tumorBoardCase: TumorBoardCase = {
422
+ ...caseData,
423
+ id,
424
+ status: 'scheduled',
425
+ actions: []
426
+ };
427
+
428
+ // Attach active recommendation if available
429
+ const recommendations = this.getPatientRecommendations(caseData.patientId);
430
+ if (recommendations.length > 0 && recommendations[0].status === 'active') {
431
+ tumorBoardCase.systemRecommendation = recommendations[0];
432
+ }
433
+
434
+ this.tumorBoardCases.set(id, tumorBoardCase);
435
+ this.emit('tumor-board-case-created', { caseId: id, patientId: caseData.patientId });
436
+
437
+ return id;
438
+ }
439
+
440
+ /**
441
+ * Record tumor board decision
442
+ */
443
+ async recordTumorBoardDecision(
444
+ caseId: string,
445
+ decision: TumorBoardCase['boardDecision']
446
+ ): Promise<void> {
447
+ const tbCase = this.tumorBoardCases.get(caseId);
448
+ if (!tbCase) throw new Error('Tumor board case not found');
449
+
450
+ tbCase.boardDecision = decision;
451
+ tbCase.status = 'completed';
452
+
453
+ this.emit('tumor-board-decision-recorded', { caseId, decision });
454
+ }
455
+
456
+ /**
457
+ * Acknowledge an alert
458
+ */
459
+ async acknowledgeAlert(acknowledgment: Omit<AlertAcknowledgment, 'acknowledgedAt'>): Promise<void> {
460
+ const fullAcknowledgment: AlertAcknowledgment = {
461
+ ...acknowledgment,
462
+ acknowledgedAt: new Date()
463
+ };
464
+
465
+ this.alertAcknowledgments.push(fullAcknowledgment);
466
+ this.emit('alert-acknowledged', fullAcknowledgment);
467
+ }
468
+
469
+ /**
470
+ * Generate explanation for a recommendation
471
+ */
472
+ generateExplanation(recommendation: ClinicalRecommendation): {
473
+ summary: string;
474
+ detailedRationale: string[];
475
+ keyFactors: { factor: string; impact: 'positive' | 'negative' | 'neutral'; weight: number }[];
476
+ uncertainties: string[];
477
+ alternativeConsiderations: string[];
478
+ } {
479
+ const summary = `Based on the patient's ${recommendation.biomarkerRationale.map(b => b.biomarker).join(', ')} status, ` +
480
+ `${recommendation.primaryRecommendation.regimen} is recommended with ` +
481
+ `${(recommendation.predictions.responseRate.value * 100).toFixed(0)}% expected response rate.`;
482
+
483
+ const detailedRationale: string[] = [];
484
+
485
+ // Add biomarker rationale
486
+ for (const br of recommendation.biomarkerRationale) {
487
+ detailedRationale.push(`${br.biomarker} ${br.status}: ${br.impact}`);
488
+ }
489
+
490
+ // Add evidence rationale
491
+ for (const ev of recommendation.supportingEvidence.filter(e => e.strength === 'strong')) {
492
+ detailedRationale.push(`${ev.source}: ${ev.description}`);
493
+ }
494
+
495
+ // Key factors
496
+ const keyFactors = recommendation.biomarkerRationale.map(br => ({
497
+ factor: `${br.biomarker} ${br.status}`,
498
+ impact: this.determineImpact(br.impact),
499
+ weight: this.determineWeight(br.evidenceLevel)
500
+ }));
501
+
502
+ // Uncertainties
503
+ const uncertainties: string[] = [];
504
+ if (recommendation.predictions.responseRate.ci95[1] - recommendation.predictions.responseRate.ci95[0] > 0.3) {
505
+ uncertainties.push('Response rate prediction has wide confidence interval');
506
+ }
507
+ if (recommendation.supportingEvidence.every(e => e.strength !== 'strong')) {
508
+ uncertainties.push('Limited strong evidence available for this combination');
509
+ }
510
+
511
+ // Alternative considerations
512
+ const alternativeConsiderations = recommendation.alternativeOptions.map(alt =>
513
+ `${alt.regimen}: ${alt.comparisonToPrimary}`
514
+ );
515
+
516
+ return {
517
+ summary,
518
+ detailedRationale,
519
+ keyFactors,
520
+ uncertainties,
521
+ alternativeConsiderations
522
+ };
523
+ }
524
+
525
+ /**
526
+ * Generate risk-benefit analysis
527
+ */
528
+ generateRiskBenefitAnalysis(recommendation: ClinicalRecommendation): {
529
+ benefits: { benefit: string; magnitude: 'high' | 'moderate' | 'low'; confidence: number }[];
530
+ risks: { risk: string; probability: number; severity: 'severe' | 'moderate' | 'mild'; manageable: boolean }[];
531
+ overallAssessment: string;
532
+ numberNeededToTreat?: number;
533
+ numberNeededToHarm?: number;
534
+ } {
535
+ const benefits = [
536
+ {
537
+ benefit: `${(recommendation.predictions.responseRate.value * 100).toFixed(0)}% objective response rate`,
538
+ magnitude: recommendation.predictions.responseRate.value > 0.5 ? 'high' as const :
539
+ recommendation.predictions.responseRate.value > 0.3 ? 'moderate' as const : 'low' as const,
540
+ confidence: recommendation.primaryRecommendation.confidenceScore
541
+ },
542
+ {
543
+ benefit: `Median PFS ${recommendation.predictions.medianPFS.value.toFixed(1)} months`,
544
+ magnitude: recommendation.predictions.medianPFS.value > 12 ? 'high' as const :
545
+ recommendation.predictions.medianPFS.value > 6 ? 'moderate' as const : 'low' as const,
546
+ confidence: recommendation.primaryRecommendation.confidenceScore
547
+ }
548
+ ];
549
+
550
+ const risks = recommendation.predictions.grade3PlusToxicity.topRisks.map((risk, i) => ({
551
+ risk,
552
+ probability: recommendation.predictions.grade3PlusToxicity.value * (1 - i * 0.1),
553
+ severity: 'moderate' as const,
554
+ manageable: true
555
+ }));
556
+
557
+ // Calculate NNT (simplified)
558
+ const baselineResponse = 0.2; // Assumed baseline
559
+ const treatmentResponse = recommendation.predictions.responseRate.value;
560
+ const absoluteBenefit = treatmentResponse - baselineResponse;
561
+ const numberNeededToTreat = absoluteBenefit > 0 ? Math.ceil(1 / absoluteBenefit) : undefined;
562
+
563
+ // Calculate NNH (simplified)
564
+ const toxicityRate = recommendation.predictions.grade3PlusToxicity.value;
565
+ const baselineToxicity = 0.1;
566
+ const absoluteHarm = toxicityRate - baselineToxicity;
567
+ const numberNeededToHarm = absoluteHarm > 0 ? Math.ceil(1 / absoluteHarm) : undefined;
568
+
569
+ const overallAssessment = this.generateOverallAssessment(benefits, risks, numberNeededToTreat, numberNeededToHarm);
570
+
571
+ return {
572
+ benefits,
573
+ risks,
574
+ overallAssessment,
575
+ numberNeededToTreat,
576
+ numberNeededToHarm
577
+ };
578
+ }
579
+
580
+ // ═══════════════════════════════════════════════════════════════════════════════
581
+ // HELPER METHODS
582
+ // ═══════════════════════════════════════════════════════════════════════════════
583
+
584
+ private determinePrimaryRecommendation(params: any): ClinicalRecommendation['primaryRecommendation'] {
585
+ // Simplified logic - would use ML service in production
586
+ let regimen = 'Standard therapy';
587
+ let drugs: ClinicalRecommendation['primaryRecommendation']['drugs'] = [];
588
+ let confidenceScore = 0.75;
589
+
590
+ // Check for biomarker-matched therapy
591
+ const alterations = params.genomicAlterations || [];
592
+
593
+ // EGFR mutation
594
+ if (alterations.some((a: any) => a.gene === 'EGFR')) {
595
+ regimen = 'Osimertinib';
596
+ drugs = [{
597
+ name: 'Osimertinib',
598
+ dose: '80mg',
599
+ schedule: 'Daily',
600
+ route: 'Oral'
601
+ }];
602
+ confidenceScore = 0.92;
603
+ }
604
+ // ALK fusion
605
+ else if (alterations.some((a: any) => a.gene === 'ALK')) {
606
+ regimen = 'Alectinib';
607
+ drugs = [{
608
+ name: 'Alectinib',
609
+ dose: '600mg',
610
+ schedule: 'BID',
611
+ route: 'Oral'
612
+ }];
613
+ confidenceScore = 0.90;
614
+ }
615
+ // PD-L1 high
616
+ else if (params.biomarkers.some((b: any) =>
617
+ b.name.toLowerCase().includes('pd-l1') && parseFloat(b.value) >= 50
618
+ )) {
619
+ regimen = 'Pembrolizumab';
620
+ drugs = [{
621
+ name: 'Pembrolizumab',
622
+ dose: '200mg',
623
+ schedule: 'Q3W',
624
+ route: 'IV'
625
+ }];
626
+ confidenceScore = 0.85;
627
+ }
628
+ // Default chemotherapy
629
+ else {
630
+ regimen = 'Carboplatin + Pemetrexed + Pembrolizumab';
631
+ drugs = [
632
+ { name: 'Carboplatin', dose: 'AUC 5', schedule: 'Q3W', route: 'IV' },
633
+ { name: 'Pemetrexed', dose: '500mg/m²', schedule: 'Q3W', route: 'IV' },
634
+ { name: 'Pembrolizumab', dose: '200mg', schedule: 'Q3W', route: 'IV' }
635
+ ];
636
+ confidenceScore = 0.78;
637
+ }
638
+
639
+ return {
640
+ regimen,
641
+ drugs,
642
+ setting: params.priorTherapies?.length > 0 ? 'second-line' : 'first-line',
643
+ confidenceScore
644
+ };
645
+ }
646
+
647
+ private determineAlternatives(params: any): ClinicalRecommendation['alternativeOptions'] {
648
+ // Generate 2-3 alternative options
649
+ return [
650
+ {
651
+ regimen: 'Clinical trial',
652
+ drugs: ['Investigational agent'],
653
+ confidenceScore: 0.7,
654
+ comparisonToPrimary: 'Access to novel therapies; additional monitoring required'
655
+ },
656
+ {
657
+ regimen: 'Alternative standard therapy',
658
+ drugs: ['Docetaxel'],
659
+ confidenceScore: 0.65,
660
+ comparisonToPrimary: 'Lower efficacy but familiar toxicity profile'
661
+ }
662
+ ];
663
+ }
664
+
665
+ private gatherEvidence(params: any): ClinicalRecommendation['supportingEvidence'] {
666
+ const evidence: ClinicalRecommendation['supportingEvidence'] = [];
667
+
668
+ // Add guideline evidence
669
+ evidence.push({
670
+ type: 'guideline',
671
+ source: 'NCCN Guidelines',
672
+ description: `Recommended for ${params.cancerType} stage ${params.stage}`,
673
+ strength: 'strong',
674
+ url: 'https://www.nccn.org/guidelines'
675
+ });
676
+
677
+ // Add clinical trial evidence based on biomarkers
678
+ if (params.genomicAlterations?.some((a: any) => a.gene === 'EGFR')) {
679
+ evidence.push({
680
+ type: 'clinical-trial',
681
+ source: 'FLAURA trial',
682
+ description: 'Osimertinib improved median PFS vs 1st-gen TKI (18.9 vs 10.2 months)',
683
+ strength: 'strong'
684
+ });
685
+ }
686
+
687
+ if (params.biomarkers?.some((b: any) =>
688
+ b.name.toLowerCase().includes('pd-l1') && parseFloat(b.value) >= 50
689
+ )) {
690
+ evidence.push({
691
+ type: 'clinical-trial',
692
+ source: 'KEYNOTE-024',
693
+ description: 'Pembrolizumab improved OS vs chemotherapy in PD-L1 ≥50%',
694
+ strength: 'strong'
695
+ });
696
+ }
697
+
698
+ return evidence;
699
+ }
700
+
701
+ private generateBiomarkerRationale(
702
+ biomarkers: { name: string; value: string | number; status?: string }[],
703
+ alterations: { gene: string; alteration: string }[]
704
+ ): ClinicalRecommendation['biomarkerRationale'] {
705
+ const rationale: ClinicalRecommendation['biomarkerRationale'] = [];
706
+
707
+ for (const alt of alterations) {
708
+ rationale.push({
709
+ biomarker: alt.gene,
710
+ status: alt.alteration,
711
+ impact: this.getBiomarkerImpact(alt.gene, alt.alteration),
712
+ evidenceLevel: this.getBiomarkerEvidenceLevel(alt.gene)
713
+ });
714
+ }
715
+
716
+ for (const bm of biomarkers) {
717
+ if (!rationale.some(r => r.biomarker === bm.name)) {
718
+ rationale.push({
719
+ biomarker: bm.name,
720
+ status: String(bm.value),
721
+ impact: this.getBiomarkerImpact(bm.name, String(bm.value)),
722
+ evidenceLevel: this.getBiomarkerEvidenceLevel(bm.name)
723
+ });
724
+ }
725
+ }
726
+
727
+ return rationale;
728
+ }
729
+
730
+ private getBiomarkerImpact(biomarker: string, value: string): string {
731
+ const impacts: Record<string, string> = {
732
+ 'EGFR': 'Sensitive to EGFR TKI therapy',
733
+ 'ALK': 'Sensitive to ALK TKI therapy',
734
+ 'BRAF': 'Sensitive to BRAF/MEK inhibitor combination',
735
+ 'KRAS G12C': 'Sensitive to KRAS G12C inhibitors',
736
+ 'PD-L1': parseFloat(value) >= 50 ? 'High likelihood of response to immunotherapy' :
737
+ parseFloat(value) >= 1 ? 'May benefit from immunotherapy' :
738
+ 'Consider immunotherapy combination',
739
+ 'MSI-H': 'High likelihood of response to immunotherapy',
740
+ 'TMB': 'May predict immunotherapy response'
741
+ };
742
+
743
+ return impacts[biomarker] || 'Prognostic significance';
744
+ }
745
+
746
+ private getBiomarkerEvidenceLevel(biomarker: string): string {
747
+ const fdaApproved = ['EGFR', 'ALK', 'ROS1', 'BRAF', 'NTRK', 'RET', 'MET', 'KRAS G12C', 'HER2'];
748
+ if (fdaApproved.includes(biomarker)) return 'FDA-approved';
749
+
750
+ const nccnRecommended = ['PD-L1', 'MSI-H', 'TMB'];
751
+ if (nccnRecommended.includes(biomarker)) return 'NCCN-recommended';
752
+
753
+ return 'Clinical evidence';
754
+ }
755
+
756
+ private generatePredictions(params: any): ClinicalRecommendation['predictions'] {
757
+ // Simplified predictions - would use ML service in production
758
+ let responseRate = 0.4;
759
+ let pfs = 8;
760
+ let os = 18;
761
+ let toxicityRisk = 0.25;
762
+
763
+ // Adjust based on biomarkers
764
+ if (params.genomicAlterations?.some((a: any) => ['EGFR', 'ALK', 'ROS1'].includes(a.gene))) {
765
+ responseRate = 0.75;
766
+ pfs = 15;
767
+ os = 30;
768
+ toxicityRisk = 0.15;
769
+ }
770
+
771
+ // Adjust based on PD-L1
772
+ const pdl1 = params.biomarkers?.find((b: any) => b.name.toLowerCase().includes('pd-l1'));
773
+ if (pdl1 && parseFloat(pdl1.value) >= 50) {
774
+ responseRate = Math.max(responseRate, 0.45);
775
+ }
776
+
777
+ // Adjust based on performance status
778
+ if (params.performanceStatus && params.performanceStatus >= 2) {
779
+ responseRate *= 0.8;
780
+ pfs *= 0.7;
781
+ os *= 0.7;
782
+ toxicityRisk *= 1.3;
783
+ }
784
+
785
+ return {
786
+ responseRate: {
787
+ value: responseRate,
788
+ ci95: [responseRate * 0.8, Math.min(responseRate * 1.2, 0.95)]
789
+ },
790
+ medianPFS: {
791
+ value: pfs,
792
+ ci95: [pfs * 0.75, pfs * 1.3],
793
+ unit: 'months'
794
+ },
795
+ medianOS: {
796
+ value: os,
797
+ ci95: [os * 0.7, os * 1.4],
798
+ unit: 'months'
799
+ },
800
+ grade3PlusToxicity: {
801
+ value: toxicityRisk,
802
+ topRisks: ['Neutropenia', 'Fatigue', 'Nausea']
803
+ },
804
+ qualityOfLife: {
805
+ score: 0.75,
806
+ considerations: ['Oral therapy allows home administration', 'Weekly monitoring during first cycle']
807
+ }
808
+ };
809
+ }
810
+
811
+ private identifyConsiderations(params: any): ClinicalRecommendation['clinicalConsiderations'] {
812
+ const considerations: ClinicalRecommendation['clinicalConsiderations'] = [];
813
+
814
+ // Performance status
815
+ if (params.performanceStatus && params.performanceStatus >= 2) {
816
+ considerations.push({
817
+ category: 'safety',
818
+ consideration: 'Consider dose reduction due to performance status',
819
+ importance: 'critical'
820
+ });
821
+ }
822
+
823
+ // Prior therapy
824
+ if (params.priorTherapies && params.priorTherapies.length > 2) {
825
+ considerations.push({
826
+ category: 'efficacy',
827
+ consideration: 'Heavily pretreated patient - consider clinical trial',
828
+ importance: 'important'
829
+ });
830
+ }
831
+
832
+ // Comorbidities
833
+ if (params.comorbidities?.some((c: string) => c.toLowerCase().includes('renal'))) {
834
+ considerations.push({
835
+ category: 'safety',
836
+ consideration: 'Dose adjustment may be needed for renal impairment',
837
+ importance: 'critical'
838
+ });
839
+ }
840
+
841
+ return considerations;
842
+ }
843
+
844
+ private generateMonitoringPlan(params: any): ClinicalRecommendation['monitoringPlan'] {
845
+ return [
846
+ { item: 'CBC with differential', frequency: 'Before each cycle', rationale: 'Monitor for myelosuppression' },
847
+ { item: 'Comprehensive metabolic panel', frequency: 'Before each cycle', rationale: 'Monitor liver/kidney function' },
848
+ { item: 'Imaging (CT chest/abdomen)', frequency: 'Every 8-12 weeks', rationale: 'Response assessment (RECIST)' },
849
+ { item: 'Clinical assessment', frequency: 'Each visit', rationale: 'Monitor for adverse events' }
850
+ ];
851
+ }
852
+
853
+ private determineImpact(impactText: string): 'positive' | 'negative' | 'neutral' {
854
+ if (impactText.toLowerCase().includes('sensitive') ||
855
+ impactText.toLowerCase().includes('response') ||
856
+ impactText.toLowerCase().includes('benefit')) {
857
+ return 'positive';
858
+ }
859
+ if (impactText.toLowerCase().includes('resistant') ||
860
+ impactText.toLowerCase().includes('poor')) {
861
+ return 'negative';
862
+ }
863
+ return 'neutral';
864
+ }
865
+
866
+ private determineWeight(evidenceLevel: string): number {
867
+ const weights: Record<string, number> = {
868
+ 'FDA-approved': 1.0,
869
+ 'NCCN-recommended': 0.9,
870
+ 'Clinical evidence': 0.7,
871
+ 'Preclinical': 0.3
872
+ };
873
+ return weights[evidenceLevel] || 0.5;
874
+ }
875
+
876
+ private generateOverallAssessment(
877
+ benefits: { magnitude: string }[],
878
+ risks: { severity: string; probability: number }[],
879
+ nnt?: number,
880
+ nnh?: number
881
+ ): string {
882
+ const highBenefits = benefits.filter(b => b.magnitude === 'high').length;
883
+ const severeRisks = risks.filter(r => r.severity === 'severe' && r.probability > 0.1).length;
884
+
885
+ if (highBenefits >= 2 && severeRisks === 0) {
886
+ return 'Favorable risk-benefit profile. Treatment strongly recommended.';
887
+ }
888
+ if (highBenefits >= 1 && severeRisks <= 1) {
889
+ return 'Acceptable risk-benefit profile. Treatment recommended with appropriate monitoring.';
890
+ }
891
+ if (severeRisks >= 2) {
892
+ return 'Elevated risk profile. Consider alternatives or enhanced monitoring if proceeding.';
893
+ }
894
+ return 'Moderate risk-benefit profile. Individual patient factors should guide decision.';
895
+ }
896
+
897
+ /**
898
+ * Export decision data for quality review
899
+ */
900
+ exportDecisionData(startDate: Date, endDate: Date): {
901
+ recommendations: ClinicalRecommendation[];
902
+ overrides: ClinicianOverride[];
903
+ tumorBoardCases: TumorBoardCase[];
904
+ summary: {
905
+ totalRecommendations: number;
906
+ acceptanceRate: number;
907
+ overrideRate: number;
908
+ commonOverrideReasons: { reason: OverrideCategory; count: number }[];
909
+ };
910
+ } {
911
+ const recommendations = Array.from(this.recommendations.values())
912
+ .filter(r => r.generatedAt >= startDate && r.generatedAt <= endDate);
913
+
914
+ const allOverrides = Array.from(this.overrides.values()).flat()
915
+ .filter(o => o.timestamp >= startDate && o.timestamp <= endDate);
916
+
917
+ const cases = Array.from(this.tumorBoardCases.values())
918
+ .filter(c => c.scheduledDate >= startDate && c.scheduledDate <= endDate);
919
+
920
+ // Calculate summary statistics
921
+ const accepted = recommendations.filter(r => r.status === 'accepted').length;
922
+ const overridden = recommendations.filter(r => r.status === 'modified' || r.status === 'rejected').length;
923
+
924
+ // Count override reasons
925
+ const reasonCounts = new Map<OverrideCategory, number>();
926
+ for (const override of allOverrides) {
927
+ const count = reasonCounts.get(override.reason.category) || 0;
928
+ reasonCounts.set(override.reason.category, count + 1);
929
+ }
930
+
931
+ const commonOverrideReasons = Array.from(reasonCounts.entries())
932
+ .map(([reason, count]) => ({ reason, count }))
933
+ .sort((a, b) => b.count - a.count);
934
+
935
+ return {
936
+ recommendations,
937
+ overrides: allOverrides,
938
+ tumorBoardCases: cases,
939
+ summary: {
940
+ totalRecommendations: recommendations.length,
941
+ acceptanceRate: recommendations.length > 0 ? accepted / recommendations.length : 0,
942
+ overrideRate: recommendations.length > 0 ? overridden / recommendations.length : 0,
943
+ commonOverrideReasons
944
+ }
945
+ };
946
+ }
947
+ }
948
+
949
+ export default ClinicalDecisionSupportService;