Muthukumarank commited on
Commit
e5baec5
Β·
verified Β·
1 Parent(s): 97d7252

Add lib/advanced-engines.ts

Browse files
Files changed (1) hide show
  1. lib/advanced-engines.ts +362 -0
lib/advanced-engines.ts ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════
3
+ * Cross-Case Intelligence & Advanced TOD Engine
4
+ * ═══════════════════════════════════════════════════════════════
5
+ */
6
+
7
+ // ═══ CROSS-CASE INTELLIGENCE MATCHING ═══
8
+
9
+ export interface CaseSignature {
10
+ id: string;
11
+ caseNumber: string;
12
+ features: {
13
+ mannerOfDeath: string;
14
+ weaponType: string[];
15
+ injuryPattern: string[];
16
+ toxicology: string[];
17
+ location: string;
18
+ timeOfDay: string;
19
+ victimProfile: string;
20
+ moPattern: string[];
21
+ };
22
+ }
23
+
24
+ export interface CaseMatch {
25
+ caseId: string;
26
+ caseNumber: string;
27
+ similarity: number;
28
+ matchingFeatures: string[];
29
+ potentialLink: 'serial' | 'related' | 'similar_mo' | 'coincidental';
30
+ explanation: string;
31
+ }
32
+
33
+ export class CrossCaseEngine {
34
+ // Simulated historical case database
35
+ private historicalCases: CaseSignature[] = [
36
+ {
37
+ id: 'hist-1', caseNumber: 'CASE-2023-0512',
38
+ features: { mannerOfDeath: 'homicide', weaponType: ['ligature', 'blunt'], injuryPattern: ['strangulation', 'head_trauma'], toxicology: ['benzodiazepine'], location: 'industrial', timeOfDay: 'night', victimProfile: 'adult_male', moPattern: ['sedation_then_violence', 'single_attacker'] }
39
+ },
40
+ {
41
+ id: 'hist-2', caseNumber: 'CASE-2023-0298',
42
+ features: { mannerOfDeath: 'homicide', weaponType: ['ligature'], injuryPattern: ['strangulation'], toxicology: ['rohypnol'], location: 'residential', timeOfDay: 'night', victimProfile: 'adult_female', moPattern: ['sedation_then_violence', 'single_attacker'] }
43
+ },
44
+ {
45
+ id: 'hist-3', caseNumber: 'CASE-2022-0891',
46
+ features: { mannerOfDeath: 'homicide', weaponType: ['sharp'], injuryPattern: ['stab_wounds'], toxicology: [], location: 'outdoor', timeOfDay: 'evening', victimProfile: 'adult_male', moPattern: ['confrontation', 'multiple_attackers'] }
47
+ },
48
+ {
49
+ id: 'hist-4', caseNumber: 'CASE-2024-0103',
50
+ features: { mannerOfDeath: 'homicide', weaponType: ['blunt', 'ligature'], injuryPattern: ['head_trauma', 'strangulation', 'defensive_wounds'], toxicology: ['diazepam'], location: 'commercial', timeOfDay: 'night', victimProfile: 'adult_male', moPattern: ['sedation_then_violence', 'abandoned_location', 'single_attacker'] }
51
+ },
52
+ ];
53
+
54
+ matchCase(currentCase: CaseSignature): CaseMatch[] {
55
+ const matches: CaseMatch[] = [];
56
+
57
+ for (const historic of this.historicalCases) {
58
+ const matchingFeatures: string[] = [];
59
+ let score = 0;
60
+ let maxScore = 0;
61
+
62
+ // Manner of death (weight: 2)
63
+ maxScore += 2;
64
+ if (historic.features.mannerOfDeath === currentCase.features.mannerOfDeath) {
65
+ score += 2;
66
+ matchingFeatures.push('manner_of_death');
67
+ }
68
+
69
+ // Weapon overlap (weight: 3)
70
+ maxScore += 3;
71
+ const weaponOverlap = currentCase.features.weaponType.filter(w => historic.features.weaponType.includes(w));
72
+ if (weaponOverlap.length > 0) {
73
+ score += Math.min(3, weaponOverlap.length * 1.5);
74
+ matchingFeatures.push(`weapons: ${weaponOverlap.join(', ')}`);
75
+ }
76
+
77
+ // Injury pattern (weight: 3)
78
+ maxScore += 3;
79
+ const injuryOverlap = currentCase.features.injuryPattern.filter(i => historic.features.injuryPattern.includes(i));
80
+ if (injuryOverlap.length > 0) {
81
+ score += Math.min(3, injuryOverlap.length * 1.5);
82
+ matchingFeatures.push(`injuries: ${injuryOverlap.join(', ')}`);
83
+ }
84
+
85
+ // Toxicology (weight: 2)
86
+ maxScore += 2;
87
+ const toxOverlap = currentCase.features.toxicology.filter(t => historic.features.toxicology.includes(t));
88
+ if (toxOverlap.length > 0) {
89
+ score += 2;
90
+ matchingFeatures.push(`toxicology: ${toxOverlap.join(', ')}`);
91
+ }
92
+
93
+ // MO pattern (weight: 4 β€” most important)
94
+ maxScore += 4;
95
+ const moOverlap = currentCase.features.moPattern.filter(m => historic.features.moPattern.includes(m));
96
+ if (moOverlap.length > 0) {
97
+ score += Math.min(4, moOverlap.length * 2);
98
+ matchingFeatures.push(`MO: ${moOverlap.join(', ')}`);
99
+ }
100
+
101
+ // Time of day (weight: 1)
102
+ maxScore += 1;
103
+ if (historic.features.timeOfDay === currentCase.features.timeOfDay) {
104
+ score += 1;
105
+ matchingFeatures.push('time_of_day');
106
+ }
107
+
108
+ // Victim profile (weight: 1)
109
+ maxScore += 1;
110
+ if (historic.features.victimProfile === currentCase.features.victimProfile) {
111
+ score += 1;
112
+ matchingFeatures.push('victim_profile');
113
+ }
114
+
115
+ const similarity = score / maxScore;
116
+
117
+ if (similarity > 0.4) {
118
+ let potentialLink: CaseMatch['potentialLink'] = 'coincidental';
119
+ if (similarity > 0.8) potentialLink = 'serial';
120
+ else if (similarity > 0.65) potentialLink = 'related';
121
+ else if (similarity > 0.5) potentialLink = 'similar_mo';
122
+
123
+ matches.push({
124
+ caseId: historic.id,
125
+ caseNumber: historic.caseNumber,
126
+ similarity: Math.round(similarity * 100) / 100,
127
+ matchingFeatures,
128
+ potentialLink,
129
+ explanation: this.generateMatchExplanation(similarity, matchingFeatures, potentialLink)
130
+ });
131
+ }
132
+ }
133
+
134
+ return matches.sort((a, b) => b.similarity - a.similarity);
135
+ }
136
+
137
+ private generateMatchExplanation(similarity: number, features: string[], link: string): string {
138
+ if (link === 'serial') {
139
+ return `⚠️ SERIAL PATTERN DETECTED (${(similarity * 100).toFixed(0)}% match). Matching features: ${features.join('; ')}. Recommend immediate cross-referencing with cold case unit.`;
140
+ }
141
+ if (link === 'related') {
142
+ return `Strong similarity (${(similarity * 100).toFixed(0)}%). Cases share: ${features.join('; ')}. May indicate same perpetrator or organized pattern.`;
143
+ }
144
+ return `Moderate similarity (${(similarity * 100).toFixed(0)}%). Shared characteristics: ${features.join('; ')}.`;
145
+ }
146
+ }
147
+
148
+ // ═══ DUAL-MODE TIME-OF-DEATH ENGINE ═══
149
+
150
+ export interface DualTodResult {
151
+ earlyPhase: {
152
+ method: string;
153
+ pmiHours: number;
154
+ lowerBound: number;
155
+ upperBound: number;
156
+ confidence: string;
157
+ indicators: { name: string; value: string; contribution: string }[];
158
+ };
159
+ latePhase: {
160
+ method: string;
161
+ pmiHours: number | null;
162
+ indicators: { name: string; value: string; contribution: string }[];
163
+ applicable: boolean;
164
+ };
165
+ combined: {
166
+ bestEstimate: number;
167
+ range: string;
168
+ confidence: string;
169
+ methodology: string;
170
+ };
171
+ coolingCurve: { time: number; temp: number }[];
172
+ }
173
+
174
+ export class DualModeTodEngine {
175
+ private T_INITIAL = 37.2;
176
+
177
+ estimate(params: {
178
+ rectalTemp: number;
179
+ ambientTemp: number;
180
+ bodyWeight: number;
181
+ correctiveFactor: number;
182
+ rigorMortis: string;
183
+ lividity: string;
184
+ decomposition: string;
185
+ vitreousPotassium?: number; // mEq/L
186
+ bodyCondition?: string;
187
+ }): DualTodResult {
188
+
189
+ // ═══ EARLY PHASE (0-72h): Henssge + Physical Signs ═══
190
+ const effectiveWeight = params.correctiveFactor * params.bodyWeight;
191
+ const B = 1.2815 * Math.pow(effectiveWeight, -0.625) + 0.0284;
192
+ const Q = (params.rectalTemp - params.ambientTemp) / (this.T_INITIAL - params.ambientTemp);
193
+
194
+ let henssePMI = 0;
195
+ if (Q > 0 && Q < 1) {
196
+ let t = 10;
197
+ for (let i = 0; i < 100; i++) {
198
+ const f = 1.25 * Math.exp(-B * t) - 0.25 * Math.exp(-5 * B * t) - Q;
199
+ const fp = -1.25 * B * Math.exp(-B * t) + 1.25 * B * Math.exp(-5 * B * t);
200
+ if (Math.abs(fp) < 1e-12) break;
201
+ t = t - f / fp;
202
+ if (Math.abs(f) < 0.0001) break;
203
+ }
204
+ henssePMI = Math.max(0, t);
205
+ }
206
+
207
+ // Vitreous potassium (if available)
208
+ let potassiumPMI: number | null = null;
209
+ if (params.vitreousPotassium) {
210
+ // Sturner formula: PMI (hours) = 7.14 * [K+] - 39.1
211
+ potassiumPMI = 7.14 * params.vitreousPotassium - 39.1;
212
+ }
213
+
214
+ // Rigor/lividity ranges
215
+ const rigorRanges: Record<string, [number, number]> = {
216
+ absent: [0, 3], developing: [2, 8], full: [8, 24], resolving: [24, 72]
217
+ };
218
+ const lividityRanges: Record<string, [number, number]> = {
219
+ absent: [0, 1], developing: [0.5, 4], present_movable: [2, 12], fixed: [8, 200]
220
+ };
221
+
222
+ const rigorRange = rigorRanges[params.rigorMortis] || [0, 72];
223
+ const lividRange = lividityRanges[params.lividity] || [0, 200];
224
+
225
+ const stdError = params.bodyWeight >= 50 && params.bodyWeight <= 100 ? 2.8 : 3.2;
226
+
227
+ const earlyPhase = {
228
+ method: 'Henssge Nomogram (1988) + Physical Indicators',
229
+ pmiHours: Math.round(henssePMI * 10) / 10,
230
+ lowerBound: Math.round(Math.max(0, henssePMI - stdError) * 10) / 10,
231
+ upperBound: Math.round((henssePMI + stdError) * 10) / 10,
232
+ confidence: Q > 0.2 && Q < 0.8 ? 'HIGH' : 'MODERATE',
233
+ indicators: [
234
+ { name: 'Body Temperature', value: `${params.rectalTemp}Β°C`, contribution: `Primary (Q=${Q.toFixed(3)})` },
235
+ { name: 'Rigor Mortis', value: params.rigorMortis, contribution: `${rigorRange[0]}-${rigorRange[1]}h range` },
236
+ { name: 'Lividity', value: params.lividity, contribution: `${lividRange[0]}-${Math.min(lividRange[1], 72)}h range` },
237
+ ...(potassiumPMI ? [{ name: 'Vitreous K+', value: `${params.vitreousPotassium} mEq/L`, contribution: `~${potassiumPMI.toFixed(1)}h (Sturner)` }] : []),
238
+ ]
239
+ };
240
+
241
+ // ═══ LATE PHASE (>72h): Metabolomic/Decomposition ML ═══
242
+ const decompScores: Record<string, number> = {
243
+ absent: 0, early: 48, bloating: 96, advanced: 200
244
+ };
245
+ const decompPMI = decompScores[params.decomposition] || 0;
246
+
247
+ const latePhase = {
248
+ method: 'Metabolomic Regression + Decomposition Staging',
249
+ pmiHours: decompPMI > 0 ? decompPMI : null,
250
+ applicable: params.decomposition !== 'absent',
251
+ indicators: [
252
+ { name: 'Decomposition Stage', value: params.decomposition, contribution: decompPMI > 0 ? `~${decompPMI}h estimate` : 'Not applicable' },
253
+ { name: 'Biochemical Markers', value: 'Simulated', contribution: 'ML regression model' },
254
+ ]
255
+ };
256
+
257
+ // ═══ COMBINED ESTIMATE ═══
258
+ let bestEstimate = henssePMI;
259
+ let methodology = 'Henssge primary';
260
+
261
+ if (potassiumPMI && Math.abs(potassiumPMI - henssePMI) < 6) {
262
+ bestEstimate = (henssePMI * 0.7 + potassiumPMI * 0.3);
263
+ methodology = 'Henssge (70%) + Vitreous K+ (30%)';
264
+ }
265
+ if (latePhase.applicable && decompPMI > 72) {
266
+ bestEstimate = decompPMI;
267
+ methodology = 'Decomposition staging (late-phase dominant)';
268
+ }
269
+
270
+ bestEstimate = Math.round(bestEstimate * 10) / 10;
271
+
272
+ // Cooling curve
273
+ const coolingCurve = Array.from({ length: 48 }, (_, i) => ({
274
+ time: i,
275
+ temp: params.ambientTemp + (this.T_INITIAL - params.ambientTemp) * (1.25 * Math.exp(-B * i) - 0.25 * Math.exp(-5 * B * i))
276
+ }));
277
+
278
+ return {
279
+ earlyPhase,
280
+ latePhase,
281
+ combined: {
282
+ bestEstimate,
283
+ range: `${Math.max(0, bestEstimate - stdError).toFixed(1)} β€” ${(bestEstimate + stdError).toFixed(1)} hours`,
284
+ confidence: earlyPhase.confidence,
285
+ methodology
286
+ },
287
+ coolingCurve
288
+ };
289
+ }
290
+ }
291
+
292
+ // ═══ SMART EVIDENCE PRIORITIZATION ═══
293
+
294
+ export interface PrioritizedEvidence {
295
+ id: string;
296
+ content: string;
297
+ type: string;
298
+ priority: number; // 1-10
299
+ reasoning: string;
300
+ actionRequired: string;
301
+ timeUrgency: 'immediate' | 'high' | 'medium' | 'low';
302
+ }
303
+
304
+ export function prioritizeEvidence(findings: any[]): PrioritizedEvidence[] {
305
+ const prioritized: PrioritizedEvidence[] = [];
306
+
307
+ findings.forEach((f, i) => {
308
+ let priority = 5;
309
+ let reasoning = '';
310
+ let action = '';
311
+ let urgency: PrioritizedEvidence['timeUrgency'] = 'medium';
312
+
313
+ if (f.content?.toLowerCase().includes('dna') || f.content?.toLowerCase().includes('fingernail')) {
314
+ priority = 10;
315
+ reasoning = 'DNA evidence directly identifies suspect';
316
+ action = 'Submit for immediate DNA analysis and CODIS search';
317
+ urgency = 'immediate';
318
+ } else if (f.type === 'PERSON_DISCREPANCY' || f.content?.toLowerCase().includes('defensive')) {
319
+ priority = 9;
320
+ reasoning = 'Directly indicates interpersonal violence and suspect presence';
321
+ action = 'Cross-reference CCTV for facial identification';
322
+ urgency = 'immediate';
323
+ } else if (f.content?.toLowerCase().includes('vehicle') || f.content?.toLowerCase().includes('sedan')) {
324
+ priority = 8;
325
+ reasoning = 'Vehicle identification can lead directly to suspect';
326
+ action = 'Run plate recognition on CCTV footage';
327
+ urgency = 'high';
328
+ } else if (f.content?.toLowerCase().includes('fiber') || f.content?.toLowerCase().includes('foreign')) {
329
+ priority = 7;
330
+ reasoning = 'Trace evidence links suspect to specific items';
331
+ action = 'Lab analysis for material identification';
332
+ urgency = 'high';
333
+ } else if (f.type === 'TOXICOLOGY') {
334
+ priority = 6;
335
+ reasoning = 'Toxicology may indicate premeditation (sedation)';
336
+ action = 'Check victim prescription records and purchase history';
337
+ urgency = 'medium';
338
+ } else if (f.type === 'TIMELINE_GAP') {
339
+ priority = 5;
340
+ reasoning = 'Gaps may contain unrecovered evidence';
341
+ action = 'Search for additional CCTV coverage in gap periods';
342
+ urgency = 'medium';
343
+ } else {
344
+ priority = 3;
345
+ reasoning = 'Supporting evidence for case building';
346
+ action = 'Document and preserve';
347
+ urgency = 'low';
348
+ }
349
+
350
+ prioritized.push({
351
+ id: `pri-${i}`,
352
+ content: f.content?.slice(0, 100) || f.type || 'Unknown',
353
+ type: f.type || 'GENERAL',
354
+ priority,
355
+ reasoning,
356
+ actionRequired: action,
357
+ timeUrgency: urgency
358
+ });
359
+ });
360
+
361
+ return prioritized.sort((a, b) => b.priority - a.priority);
362
+ }