/** Root Cause Lab — a guided, skeptical workbench for "offline says +5%, * shadow says −2%": 13 enforced stages from comparison validity through * root-cause scoring to recommended experiments. */ import { type ComponentType, useCallback, useEffect, useMemo, useState } from 'react'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import CircularProgress from '@mui/material/CircularProgress'; import MenuItem from '@mui/material/MenuItem'; import Paper from '@mui/material/Paper'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { ArrowLeft, CheckCheck, FlaskConical, Plus, StickyNote } from 'lucide-react'; import * as api from '../../services/rca'; import type { Confidence, DecisionTree, DiagnosticsResponse, Experiments, Investigation, InvestigationSummary, RcaReport, RevealResponse, RootCause, Scoreboard, } from '../../types/rca'; import { BG_PANEL, BORDER, Explainer, FindingChips, SectionCard, tableSx, } from '../../components/rca/common'; import StageRail, { firstIncompleteIndex } from '../../components/rca/StageRail'; import { ComparisonValidityView, LabelIntegrityView, OfflineAuditView, PopulationView, ServingParityView, TrafficAuditView, } from '../../components/rca/stageViews'; import { ConditionalHeatmapView, FeatureParityView, PairedTransitionView, ShiftView, SignificanceView, } from '../../components/rca/chartViews'; import { DecisionTreeView, ScoreBoardView } from '../../components/rca/boardViews'; import { ExperimentsView, ReportView } from '../../components/rca/ReportView'; import { AcknowledgeUnknownsDialog, RecordFindingDialog } from '../../components/rca/dialogs'; const STAGE_VIEWS: Record> = { comparison_validity: ComparisonValidityView, offline_audit: OfflineAuditView, population_validation: PopulationView, distribution_shift: ShiftView, conditional_performance: ConditionalHeatmapView, paired_comparison: PairedTransitionView, statistical_significance: SignificanceView, feature_parity: FeatureParityView, serving_parity: ServingParityView, shadow_traffic: TrafficAuditView, label_integrity: LabelIntegrityView, }; // ----------------------------------------------------------- creation panel function CreatePanel({ onCreated }: { onCreated: (inv: Investigation) => void }) { const [causes, setCauses] = useState<{ id: RootCause; label: string }[]>([]); const [baseline, setBaseline] = useState('detr3d-a-v41'); const [candidate, setCandidate] = useState('detr3d-b-v42'); const [cause, setCause] = useState('DEMO'); const [seed, setSeed] = useState(''); const [busy, setBusy] = useState(false); const [err, setErr] = useState(''); useEffect(() => { api.getCauses().then((r) => setCauses(r.causes)).catch(() => undefined); }, []); const create = async () => { setBusy(true); setErr(''); try { const inv = await api.createInvestigation({ baseline_model: baseline, candidate_model: candidate, cause: cause === 'DEMO' ? null : (cause as RootCause), seed: seed ? Number(seed) : null, training_mode: cause === 'DEMO', }); onCreated(inv); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } }; return ( setBaseline(e.target.value)} sx={{ width: 180 }} /> setCandidate(e.target.value)} sx={{ width: 180 }} /> setCause(e.target.value)} sx={{ width: 320 }}> Demo investigation — hidden random root cause (training mode) {causes.map((c) => ( {c.id} — {c.label} ))} setSeed(e.target.value.replace(/\D/g, ''))} sx={{ width: 130 }} /> {err ? {err} : null} ); } // -------------------------------------------------------- investigation list function InvestigationList({ onOpen }: { onOpen: (id: string) => void }) { const [rows, setRows] = useState([]); useEffect(() => { api.listInvestigations().then((r) => setRows(r.investigations)).catch(() => undefined); }, []); return ( {rows.length === 0 ? ( None yet — create one above. ) : ( NameModelsClaimsProgressMode {rows.map((r) => ( {r.name} {r.candidate_model} vs {r.baseline_model} +{r.claims.offline_delta_pp}pp off{' / '} {r.claims.shadow_delta_pp}pp sh {r.stages_complete}/{r.stages_total} stages {r.training_mode ? ( ) : ( )} ))} )} ); } // ----------------------------------------------------------- hypothesis banner function HypothesisBanner({ inv, workingSet }: { inv: Investigation; workingSet: RootCause[] }) { return ( CLAIMS WORKING HYPOTHESES ({workingSet.length}) {workingSet.map((h) => ( ))} narrowed by recorded evidence only — never a premature single conclusion ); } // -------------------------------------------------------- investigation view function InvestigationWorkspace({ invId, onBack }: { invId: string; onBack: () => void }) { const [inv, setInv] = useState(null); const [stageIdx, setStageIdx] = useState(0); const [diag, setDiag] = useState>({}); const [workingSet, setWorkingSet] = useState([]); const [board, setBoard] = useState(null); const [tree, setTree] = useState(null); const [experiments, setExperiments] = useState(null); const [report, setReport] = useState(null); const [reveal, setReveal] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [findingOpen, setFindingOpen] = useState(false); const [ackOpen, setAckOpen] = useState(false); const stage = inv?.stages[stageIdx]; const stageKey = stage?.key ?? ''; const refreshWorkingSet = useCallback((id: string) => { api.getScoreboard(id, true) .then((b) => setWorkingSet(b.working_hypothesis_set)) .catch(() => undefined); }, []); const load = useCallback(async () => { const got = await api.getInvestigation(invId); setInv(got); setStageIdx((cur) => Math.min(cur, firstIncompleteIndex(got.stages))); refreshWorkingSet(invId); return got; }, [invId, refreshWorkingSet]); useEffect(() => { load().catch((e) => setError(String(e))); }, [load]); // Load whatever the selected stage needs. useEffect(() => { if (!inv || !stage) return; setError(''); if (STAGE_VIEWS[stage.key] && !diag[stage.key]) { setLoading(true); api.getDiagnostics(inv.id, stage.key) .then((d) => { setDiag((m) => ({ ...m, [stage.key]: d })); refreshWorkingSet(inv.id); }) .catch((e) => setError(e instanceof Error ? e.message : String(e))) .finally(() => setLoading(false)); } if (stage.key === 'root_cause_scoring' && (!board || !tree)) { setLoading(true); Promise.all([api.getScoreboard(inv.id), api.getDecisionTree(inv.id)]) .then(([b, t]) => { setBoard(b); setTree(t); }) .catch((e) => setError(e instanceof Error ? e.message : String(e))) .finally(() => setLoading(false)); } if (stage.key === 'recommendations_report' && (!experiments || !report)) { setLoading(true); Promise.all([api.getExperiments(inv.id), api.getReport(inv.id)]) .then(([x, r]) => { setExperiments(x); setReport(r); }) .catch((e) => setError(e instanceof Error ? e.message : String(e))) .finally(() => setLoading(false)); } }, [inv, stage, diag, board, tree, experiments, report, refreshWorkingSet]); const complete = async (ack = false, note = '') => { if (!inv || !stage) return; try { const res = await api.completeStage(inv.id, stage.index, { acknowledge_unknowns: ack, note, }); setInv(res.investigation); setAckOpen(false); if (stage.index + 1 < res.investigation.stages.length) setStageIdx(stage.index + 1); refreshWorkingSet(inv.id); } catch (e) { const err = e as Error & { status?: number }; if (err.status === 409 && /UNKNOWN/i.test(err.message)) { setAckOpen(true); } else { setError(err.message); } } }; const submitFinding = async (f: { title: string; status: any; severity: any; detail: string }) => { if (!inv || !stage) return; await api.recordFinding(inv.id, { stage: stage.key, ...f }); setFindingOpen(false); await load(); // Board scores include human findings — invalidate cached views. setBoard(null); setReport(null); }; const assess = async (hypothesis: RootCause, confidence: Confidence, note: string) => { if (!inv) return; const b = await api.assessHypothesis(inv.id, { hypothesis, confidence, note }); setBoard(b); setReport(null); }; const doReveal = async () => { if (!inv) return; const r = await api.revealCause(inv.id); setReveal(r); await load(); }; const jumpToStage = useCallback((sk: string) => { if (!inv) return; const target = inv.stages.find((s) => s.key === sk); if (target && target.index <= firstIncompleteIndex(inv.stages)) setStageIdx(target.index); }, [inv]); const stageFindings = useMemo( () => (inv && stage ? inv.findings.filter((f) => f.stage === stage.key) : []), [inv, stage] ); const unknownTitles = stageFindings .filter((f) => f.status === 'UNKNOWN' && f.severity === 'CRITICAL') .map((f) => f.title); if (!inv || !stage) { return error ? {error} : ; } const isDone = stage.status === 'complete' || stage.status === 'complete_with_unknowns'; const View = STAGE_VIEWS[stage.key]; return ( {inv.name} {inv.candidate_model} vs {inv.baseline_model} · seed {inv.seed} {inv.training_mode ? ( } label="training mode" sx={{ height: 20, fontSize: 10, bgcolor: '#232a31' }} /> ) : null} Stage {stage.index}: {stage.title} {STAGE_VIEWS[stage.key] || stage.key === 'root_cause_scoring' ? ( ) : null} {!isDone ? ( ) : ( )} {stageFindings.length ? ( ) : null} {error ? {error} : null} {loading ? : null} {View && diag[stage.key] ? : null} {stage.key === 'root_cause_scoring' && board && tree ? ( <> ) : null} {stage.key === 'recommendations_report' && experiments && report ? ( <> ) : null} {stage.key === 'root_cause_scoring' && !board && !loading ? ( ) : null} setFindingOpen(false)} onSubmit={submitFinding} /> setAckOpen(false)} onAcknowledge={(note) => complete(true, note)} /> ); } // -------------------------------------------------------------------- page export default function RootCauseLabPage() { const [activeId, setActiveId] = useState(null); const [refreshKey, setRefreshKey] = useState(0); if (activeId) { return ( { setActiveId(null); setRefreshKey((k) => k + 1); }} /> ); } return ( setActiveId(inv.id)} /> ); }