/** * Recommendation detail page with validation context. * * Shows original confidence alongside calibrated confidence (historical win rate), * evidence quality indicators, source reliability, and live eligibility status. * * Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7 */ import { useParams, Link } from '@tanstack/react-router'; import { AlertTriangle, ShieldCheck, ShieldX, Info } from 'lucide-react'; import { useRecommendation, useValidationCalibration, useValidationGateStatus, useValidationAttributionSources, } from '../api/hooks'; import { StatusBadge, ConfidenceBar, LoadingSpinner, Card } from '../components/ui'; export function RecommendationDetailPage() { const { id } = useParams({ from: '/recommendations/$id' }); const { data: rec, isLoading } = useRecommendation(id); const { data: calibration } = useValidationCalibration(); const { data: gateData } = useValidationGateStatus(); const { data: sourcesData } = useValidationAttributionSources(); if (isLoading || !rec) return ; // --- Calibration: find the bucket matching this recommendation's confidence --- const matchingBucket = calibration?.buckets?.find( (b) => rec.confidence >= b.bucket_low && rec.confidence < b.bucket_high, ); // Handle edge case: confidence of exactly 1.0 falls in the last bucket [0.90, 1.00] const calibratedBucket = matchingBucket ?? (rec.confidence >= 1.0 ? calibration?.buckets?.find((b) => b.bucket_high >= 1.0) : undefined); const historicalWinRate = calibratedBucket?.observed_win_rate; // --- Evidence counts --- const totalEvidenceCount = rec.evidence.length; // Compute duplicate evidence: group by normalized title, count extras const titleCounts = new Map(); for (const ev of rec.evidence) { const key = (ev.title ?? '').toLowerCase().trim(); titleCounts.set(key, (titleCounts.get(key) ?? 0) + 1); } let duplicateEvidenceCount = 0; for (const count of titleCounts.values()) { if (count > 1) duplicateEvidenceCount += count - 1; } const uniqueEvidenceCount = totalEvidenceCount - duplicateEvidenceCount; const duplicateRatio = totalEvidenceCount > 0 ? duplicateEvidenceCount / totalEvidenceCount : 0; const hasDuplicateWarning = duplicateRatio > 0.2; // --- Source reliability: find primary contributing sources --- const evidenceSources = new Map(); for (const ev of rec.evidence) { const src = ev.source_type ?? ev.publisher ?? 'unknown'; evidenceSources.set(src, (evidenceSources.get(src) ?? 0) + ev.weight); } // Sort by total weight descending to find primary source const sortedSources = [...evidenceSources.entries()].sort((a, b) => b[1] - a[1]); const primarySourceType = sortedSources[0]?.[0]; // Look up source reliability from attribution data const primarySourceAttribution = sourcesData?.sources?.find( (s) => s.source_type === primarySourceType || s.source === primarySourceType, ); // Source reliability is approximated from win_rate via Bayesian shrinkage // The attribution data has win_rate which is the observed metric const primarySourceWinRate = primarySourceAttribution?.win_rate; // Bayesian shrinkage: reliability = 0.5 + (n/(n+30)) * (win_rate - 0.5) const primarySourceCount = primarySourceAttribution?.prediction_count ?? 0; const primarySourceReliability = primarySourceWinRate != null ? 0.5 + (primarySourceCount / (primarySourceCount + 30)) * (primarySourceWinRate - 0.5) : undefined; const hasLowReliabilityWarning = primarySourceReliability != null && primarySourceReliability < 0.4; // --- Gate status --- const gateStatus = gateData?.gate_status as { passed?: boolean; reason?: string; threshold_results?: Array<{ name: string; threshold: number; actual: number; passed: boolean }>; } | null; return ( {rec.ticker} Confidence Horizon{rec.time_horizon} Risk Generated{new Date(rec.generated_at).toLocaleString()} Portfolio %{rec.portfolio_pct != null ? `${(rec.portfolio_pct * 100).toFixed(1)}%` : '—'} Max Loss %{rec.max_loss_pct != null ? `${(rec.max_loss_pct * 100).toFixed(2)}%` : '—'} Model{rec.model_version ?? '—'} {/* Validation Context Card — Requirements 13.1–13.7 */} Validation Context {/* 13.1: Original confidence alongside calibrated confidence */} Original Confidence {(rec.confidence * 100).toFixed(1)}% Calibrated Confidence {historicalWinRate != null ? `${(historicalWinRate * 100).toFixed(1)}%` : 'N/A'} {/* 13.2: Historical win rate for similar confidence levels */} Historical Win Rate {historicalWinRate != null ? ( {(historicalWinRate * 100).toFixed(1)}% {calibratedBucket && ( ({calibratedBucket.prediction_count} predictions) )} ) : ( 'N/A' )} {/* 13.3: Evidence count, unique evidence count, duplicate evidence count */} Evidence Count {totalEvidenceCount} Unique Evidence {uniqueEvidenceCount} Duplicate Evidence {/* 13.6: Warning badge when duplicate evidence count > 20% of total */} {hasDuplicateWarning && ( >20% )} {duplicateEvidenceCount} {totalEvidenceCount > 0 && ( ({(duplicateRatio * 100).toFixed(0)}%) )} {/* 13.4: Source reliability indicator */} Primary Source Reliability {/* 13.7: Warning badge when primary source reliability < 0.4 */} {hasLowReliabilityWarning && ( Low )} {primarySourceReliability != null ? ( {primarySourceReliability.toFixed(3)} {primarySourceType && ( ({primarySourceType}) )} ) : ( 'N/A' )} {/* 13.5: Live eligibility status with reason */} Live Eligibility {gateStatus != null ? ( {gateStatus.passed ? ( Gate Passed ) : ( Gate Failed )} {gateStatus.reason && ( {gateStatus.reason} )} ) : ( N/A — no gate evaluation available )} {rec.thesis && ( Thesis {rec.thesis} )} {rec.invalidation_conditions && rec.invalidation_conditions.length > 0 && ( Invalidation Conditions {rec.invalidation_conditions.map((c, i) => {c})} )} {/* Risk Evaluation */} {rec.risk_evaluation && ( Risk Evaluation Allowed mode: {rec.risk_evaluation.allowed_mode} {rec.risk_evaluation.rejection_reasons && rec.risk_evaluation.rejection_reasons.length > 0 && ( {rec.risk_evaluation.rejection_reasons.map((r, i) => {r})} )} )} {/* Evidence */} Evidence ({rec.evidence.length}) {rec.evidence.length === 0 ? ( No evidence linked ) : ( {rec.evidence.map((ev) => { const isMacro = ev.document_type === 'macro_event' || ev.evidence_type === 'macro_event'; return ( {isMacro && ( e.stopPropagation()} > MACRO ↗ )} {ev.title ?? 'Untitled'} weight: {ev.weight.toFixed(3)} {ev.document_type} {ev.source_type} {ev.publisher && {ev.publisher}} {ev.published_at && {new Date(ev.published_at).toLocaleDateString()}} ); })} )} ); }
{rec.thesis}
No evidence linked