export interface SignalQualityInput { sourceCount: number; isAlert: boolean; sourceTier?: number; threatLevel?: string; velocity?: { sourcesPerHour: number; level: string; trend?: string }; countryCode?: string | null; } export interface SignalQuality { confidence: number; intensity: number; expectationGap: number; timeliness: number; composite: number; tier: 'strong' | 'notable' | 'weak' | 'noise'; } export type WeightProfile = 'default' | 'risk' | 'macro' | 'shortTerm'; const WEIGHTS: Record = { default: [0.35, 0.30, 0.20, 0.15], risk: [0.45, 0.25, 0.20, 0.10], macro: [0.25, 0.40, 0.20, 0.15], shortTerm: [0.30, 0.25, 0.20, 0.25], }; export function normalizeThreatLevel(level: string | undefined): number { switch (level?.toLowerCase()) { case 'critical': return 1.0; case 'high': return 0.75; case 'elevated': return 0.55; case 'moderate': case 'medium': return 0.4; case 'low': return 0.2; case 'info': return 0.1; default: return 0.3; } } export function computeISQ( input: SignalQualityInput, focalPointFn: (code: string) => { focalScore: number; urgency: string } | null, ciiScoreFn: (code: string) => number | null, isFocalDataAvailableFn: () => boolean, profile: WeightProfile = 'default', ): SignalQuality { const [wConf, wIntensity, wGap, wTime] = WEIGHTS[profile]; const confidence = Math.min(1, ( (input.sourceCount >= 3 ? 1.0 : input.sourceCount === 2 ? 0.7 : 0.4) + (input.isAlert ? 0.2 : 0) + ((input.sourceTier !== undefined && input.sourceTier <= 2) ? 0.1 : 0) )); const threatNorm = normalizeThreatLevel(input.threatLevel); let focalScore = 0; let ciiScore = 0; const fp = input.countryCode ? focalPointFn(input.countryCode) : null; if (fp) focalScore = fp.focalScore / 100; if (input.countryCode) { const cii = ciiScoreFn(input.countryCode); if (cii !== null) ciiScore = cii / 100; } const intensity = Math.max(threatNorm, focalScore, ciiScore); let expectationGap: number; if (!input.countryCode) { expectationGap = 0.5; } else if (fp !== null) { expectationGap = 0.4; } else if (isFocalDataAvailableFn()) { expectationGap = 0.8; } else { expectationGap = 0.5; } const velLevel = input.velocity?.level ?? 'normal'; let timeliness = velLevel === 'spike' ? 1.0 : velLevel === 'elevated' ? 0.6 : 0.2; if (input.velocity?.trend === 'rising') { timeliness = Math.min(1.0, timeliness + 0.2); } const composite = wConf * confidence + wIntensity * intensity + wGap * expectationGap + wTime * timeliness; const tier: SignalQuality['tier'] = composite >= 0.75 ? 'strong' : composite >= 0.50 ? 'notable' : composite >= 0.25 ? 'weak' : 'noise'; return { confidence, intensity, expectationGap, timeliness, composite, tier }; }