diff --git a/apps/desktop/src/components/unpack-workspace/HistoryGraphSlider.tsx b/apps/desktop/src/components/unpack-workspace/HistoryGraphSlider.tsx index b498898b..1f33d576 100644 --- a/apps/desktop/src/components/unpack-workspace/HistoryGraphSlider.tsx +++ b/apps/desktop/src/components/unpack-workspace/HistoryGraphSlider.tsx @@ -24,6 +24,7 @@ import { HistoryContributorPanel } from '@/components/unpack-workspace/HistoryCo import { HistoryReleaseNavigator } from '@/components/unpack-workspace/HistoryReleaseNavigator'; import { useHistoryReleaseNavigation } from '@/components/unpack-workspace/useHistoryReleaseNavigation'; import { + type HistoryRevisionMatch, deriveHistoryGraphTransition, filterHistoryRevisions, historyInspectionAriaLabel, @@ -47,7 +48,10 @@ import { onHistoryBackfillProgress, openInApp, type HistoryBackfillProgress, + type HistoryCausalEvent, + type HistoryCausalLink, type HistoryCausalTrace, + type HistoryChangeEpisode, type HistoryContributorSummary, type HistoryGraphStatus, type HistoryFacetPacket, @@ -55,6 +59,8 @@ import { type HistoryEvidenceAdapterDescriptor, type HistoryAnnotation, type HistoryAnnotationDecision, + type HistoryReleaseCatalogEntry, + type HistoryRevision, type HistoryTimeline, type HistoryStructuralState, type HistoryStructuralDelta, @@ -86,6 +92,1023 @@ function viewerGraph(state: HistoryStructuralState | null): UnpackRepoGraph { }; } +// ─── Extracted sub-components ─────────────────────────────────────────────── + +function CausalEventCard({ + event, + openEvidenceSource, +}: { + event: HistoryCausalEvent; + openEvidenceSource: (path: string) => void; +}) { + return ( +
+
+ + {event.stage.replace('_', ' ')} + + {event.event_kind.replaceAll('_', ' ')} + · + {new Date(event.effective_at ?? event.recorded_at).toLocaleString()} +
+

{event.summary}

+
+ {event.trust} + · {event.source_id} + {event.revision_sha ? · {event.revision_sha.slice(0, 8)} : null} + {event.sources.length > 0 && !event.source_available ? ( + + source rotated or missing + + ) : null} +
+ {event.sources.length > 0 ? ( +
+ {event.sources.slice(0, 2).map((source) => ( + + ))} +
+ ) : null} +
+ ); +} + +function CausalEpisodeCard({ + episode, + episodeIndex, + openEvidenceSource, +}: { + episode: HistoryChangeEpisode; + episodeIndex: number; + openEvidenceSource: (path: string) => void; +}) { + return ( +
+
+
+ Episode {episodeIndex + 1} · {episode.events.length} evidenced events +
+
+ {episode.stages_present.map((stage) => ( + + {stage.replace('_', ' ')} + + ))} +
+
+
+ {episode.events.slice(0, 24).map((event) => ( + + ))} +
+ {episode.contradictions.length > 0 ? ( +
+ + {episode.contradictions.join(' · ')} +
+ ) : null} + {episode.gaps.length > 0 ? ( +
+ Missing: {episode.gaps.join(' · ')} +
+ ) : null} + {episode.qualified_leads.length > 0 ? ( +
+
Qualified leads · not merged
+
+ {episode.qualified_leads.slice(0, 8).map((lead) => { + const candidate = episode.qualified_lead_events.find( + (event) => event.id === lead.to_event_id + ); + return ( +
+ {candidate?.summary ?? lead.to_event_id} · {lead.evidence} +
+ ); + })} +
+
+ ) : null} +
+ ); +} + +function CausalTraceSection({ + causalTrace, + openEvidenceSource, +}: { + causalTrace: HistoryCausalTrace; + openEvidenceSource: (path: string) => void; +}) { + return ( +
+
+
+
+ Causal history +
+

+ Explicit IDs, revisions, entities, and episode keys form threads. Time or path proximity + stays a qualified lead until stronger evidence links it. +

+
+
+ + {causalTrace.stale ? 'ledger stale' : 'ledger current'} + + + {causalTrace.scanned_events.toLocaleString()} events scanned + +
+
+ {causalTrace.gaps.length > 0 ? ( +
+ {causalTrace.gaps.join(' · ')} +
+ ) : null} + {causalTrace.episodes.length === 0 ? ( +
+ No explicit causal episode currently links to this entity. The absence is preserved + instead of guessing from nearby activity. +
+ ) : ( +
+ {causalTrace.episodes.slice(0, 3).map((episode, episodeIndex) => ( + + ))} +
+ )} + {causalTrace.truncated ? ( +
+ Bounded result. Narrow the selector or request the next ledger page for more evidence. +
+ ) : null} +
+ ); +} + +function EntityEvolutionPanel({ entityEvolution }: { entityEvolution: HistoryEntityEvolution }) { + return ( +
+
+ + First indexed:{' '} + + {entityEvolution.first_seen?.revision_sha.slice(0, 8) ?? 'unknown'} + + + + Last changed:{' '} + + {entityEvolution.last_changed?.revision_sha.slice(0, 8) ?? 'unknown'} + + + + Last present:{' '} + + {entityEvolution.last_present?.revision_sha.slice(0, 8) ?? 'unknown'} + + + {entityEvolution.occurrences.length} indexed checkpoints +
+ {entityEvolution.lineage.length > 0 ? ( +
+ {entityEvolution.lineage.slice(0, 12).map((edge) => ( + + {edge.relation} · {edge.trust} + {edge.candidates.length > 0 ? ` · ${edge.candidates.length + 1} candidates` : ''} + + ))} +
+ ) : null} + {entityEvolution.coverage_gap ? ( +
{entityEvolution.coverage_gap}
+ ) : null} +
+ ); +} + +function EntityFacetCard({ facet }: { facet: HistoryFacetPacket['facets'][number] }) { + return ( +
+
+ + {facet.name} + + + {facet.status.replace('_', ' ')} + +
+

{facet.summary}

+
+ trust: {facet.trust} + {facet.sources.length > 0 + ? ` · ${facet.sources + .slice(0, 2) + .map((source) => `${source.path}${source.start_line ? `:${source.start_line}` : ''}`) + .join(', ')}` + : ' · no source anchor'} +
+
+ ); +} + +function EntityExplanationAside({ + entityExplanation, + entityEvolution, + causalTrace, + annotations, + revision, + openEvidenceSource, + annotationAuthor, + setAnnotationAuthor, + annotationBody, + setAnnotationBody, + annotationDecision, + setAnnotationDecision, + annotationSaving, + saveAnnotation, +}: { + entityExplanation: HistoryFacetPacket; + entityEvolution: HistoryEntityEvolution | null; + causalTrace: HistoryCausalTrace | null; + annotations: HistoryAnnotation[]; + revision: HistoryRevision; + openEvidenceSource: (path: string) => void; + annotationAuthor: string; + setAnnotationAuthor: (value: string) => void; + annotationBody: string; + setAnnotationBody: (value: string) => void; + annotationDecision: HistoryAnnotationDecision; + setAnnotationDecision: (value: HistoryAnnotationDecision) => void; + annotationSaving: boolean; + saveAnnotation: () => void; +}) { + return ( +