C4 — TimeTravel text ⇄ graph view
Design + guide for C4: give the viz ⏳ Time Travel tab's dependencies reel a graph view alongside its existing text edge-list, so you can watch the cross-repo dependency graph as it was at any point in time scroll by — with direct deps coloured distinctly from transitive ones.
1. What it is
Time Travel pins three parallel reels — versions, dependencies, benches — to one release as you step the cursor (⏮ ◀ ▶ ⏭, scrub slider, play/pause) through release_order. Before C4 the dependencies reel was a flat text edge-list (from → to via: crate, …). C4 adds a text ⇄ graph toggle: flip it and the deps reel renders the laid-out dependency graph instead, pinned to the cursor's release — i.e. the graph as it was at that moment in history.
⏱ pinned to release <rid> [📄 deps: text | 🔗 deps: graph]
The graph reel is the same renderer as the 🔗 Dep Graph tab — it reuses, never duplicates, the pure-data layout core.
2. Reuse, not duplication
The graph is laid out by the existing pure-data core src/viz/depgraph_layout.rs (DepGraphLayout), which already does the deep transitive closure and the direct vs transitive edge classification, and is painted by the existing src/viz/graph.rs (draw_dep_graph / DepGraphView). C4 wires those into the Time Travel reel:
draw_timetravel (cursor → rid)
└─[graph mode ON]─▶ reel_dependencies (graph=true)
└─▶ draw_dep_graph(ui, tl, Some(rid), …, dep_view)
└─▶ DepGraphLayout::build (transitive closure + direct/transitive classes)
└─▶ rectangular nodes + violet=direct / dashed-blue=transitive
TimeTravelStategainsgraph: bool(the toggle), aDepGraphView(so the deep/pan/zoom/collapse navigation comes for free), and adep_selected_repo.reel_dependencies(.., graph, dep_view, dep_selected_repo)short-circuits todraw_dep_graph(ui, tl, Some(rid), dep_selected_repo, dep_view)when graph mode is on — passing the cursor'sridas theselected_release, so the snapshot resolved is the one pinned at that release (snapshot_for(rid)), not the latest.- Direct deps paint in the Dep Graph tab's
DIRECT_COLOR(violet, solid arrow); transitive deps inTRANSITIVE_COLOR(cooler blue, dashed,⤳ N hops). The legend + deep toggle from the Dep Graph toolbar come along unchanged.
3. Introspection (LAW #6)
state_json()["timetravel"] carries the cursor and — in graph mode — the time-pinned laid-out graph (the same DepGraphLayout::state_json blob the Dep Graph tab exposes):
{
"idx": 0,
"release_count": 1,
"pinned_release": "4e6f…0001",
"playing": false,
"view": "graph",
"graph": {
"deep": true, "node_count": 3,
"direct_edges": 2, "transitive_edges": 1,
"nodes": [{"repo":"A","col":0,"row":0}, …],
"edges": [{"from":"A","to":"B","class":"direct","hops":1,"via":["b_crate"]},
{"from":"A","to":"C","class":"transitive","hops":2,"via":[]}]
}
}
In text view, graph is null and view is "text".
$NORNIR_VIZ_TRACE stream
On every cursor move or view flip (edge-triggered, never per-frame), TimeTravelState::trace_if_changed emits the matching tab pattern:
timetravel.viewevent — the user flipped the toggle (text/graph).timetravel.renderin — the requested cursor (idx,view, pinned release).timetravel.renderout — the full time-pinned graph data (nodes, positions, classified edges).timetravel.renderend — the summary (view,node_count,direct_edges,transitive_edges).
So an agent following the trace reads back exactly the graph the reel painted, without a screenshot.
4. Inject-and-assert test (LAW #1)
tests/viz_snapshots.rs :: viz_timetravel_graph_view_inject_assert:
-
Injects the A→B→C chain (no direct A→C) as a single release, pins the cursor to it (
set_timetravel_for_test(0, graph=true, deep=true)). -
Asserts through
state_json()["timetravel"]:view == "graph"andpinned_release== the injected release id,- A→B and B→C are classified direct, A→C is the transitive edge the closure synthesises at 2 hops (
direct_edges==2,transitive_edges==1).
-
LAW #4: renders the coloured, time-pinned graph to a golden PNG (
tests/snapshots/viz_timetravel_graph.png) + a manifest entry.
Real values in, real graph structure + classification out — never "didn't panic".
5. CLI / GUI parity
The graph reel exposes nothing the data layer can't print: its full structure is in state_json()["timetravel"]["graph"] and the trace stream, the same JSON the 🔗 Dep Graph tab already surfaces — so the time-pinned graph is fully readable headless, not GUI-only.