/** * 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()}}
); })}
)}
); }