Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/timetravel-graph.md

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

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:

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:

  1. 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)).

  2. Asserts through state_json()["timetravel"]:

    • view == "graph" and pinned_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).
  3. 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.