Funnel — the live, growing plan-DAG graph
While a user funnels ideas, a graph must render and grow in real time: each idea insert adds a few nodes to the plan-DAG and the user watches it build. From the original ask — "draw a graph in the funnel pane so one can see the DAG growing."
Current state
| Surface | Graph? | Grows on insert? | Layout |
nornir funnel tab (src/viz/funnel_tab.rs) |
✅ egui-snarl 2D DAG (+ funnel3d.rs) |
✅ yes, live | CentralPanel = the DAG canvas · SidePanel::right("funnel_history") = intake history |
dwarves funnel pane (nornir-funnel-embed) |
✅ facett-plandag DAG |
✅ live auto-poll (~1.5 s) + submit strip | submit strip (idea|error) atop the DAG; ⟳ Refresh + ●live/⏸ toggle; growth-history trail (history · N samples · peak M · +K grown + node-count → trail) under the frame |
How nornir grows it live (the real path — reuse this)
Submit button → submit_intake() → invalidate() (loaded=false, view=None, built_for=None). Next frame: load_view() re-reads the funnel from the warehouse, runs promote_ready() (derives the Pending→Ready frontier), rebuilds the graph via build_snarl(&plan) when the plan changed. Net: every submit / generate_plan re-derives and re-renders the DAG — the mother idea node appears on submit, the child task nodes appear on generate_plan, and readiness lights up. That is the "grows as you funnel" behaviour, already working in nornir.
Target layout (both surfaces)
┌───────────────────────────────┬───────────────────────────┐
│ intake strip │ LIVE plan-DAG │
│ ( 💡 idea / 🐞 error + Submit)│ (grows per insert; │
│ history list (newest first) │ new nodes light up) │
│ │ ⤷ mini-graph inset when │
│ │ space is tight │
└───────────────────────────────┴───────────────────────────┘
- Intake + history on the left; the live plan-DAG on the right (or a mini-graph inset while funnelling). nornir today puts the DAG central + history right — a layout swap makes the graph the persistent right-hand companion; keep the full-canvas mode too.
Grow-on-insert
- Each submitted idea → planner adds a few nodes/edges (
IdeaSubmitted/PlanCreated/NodeAdded/EdgeAdded) → the graph animates the new nodes lighting up (reusefunnel-core::plan_dag::DagStatus→facett-plandagOpStatus). - nornir: already re-derives on
invalidate()— no change needed beyond the layout. - dwarves: ✅ live — auto-polls
Funnel.Show(~1.5 s, ●live/⏸ toggle) so the embedded DAG grows without a click, and a submit strip (idea|error →Funnel.SubmitIdea) lets you grow it from inside the pane; a submit triggers an immediate refresh. Manual⟳ Refreshkept as an override. Offline → clean state (unavailable / submit-error), no panic.
The plan-DAG points INTO the system-DAG (the super-graph = the warehouse)
A plan node is work to do on something that exists. So each plan node should anchor into the system-DAG — the SCIP/deps graph (symbol_facts/call_edges) — with a badge or edge to the anchored symbol (funnel/decompose.rs::CodeIndexRetriever already produces those Anchors for the model-free planner; the --llm planner's anchors are still empty — the "LLM proposes → RAG anchors" merge, tracked in the-two-dags.md).
The super-graph that spans binary/function → module → system → infrastructure is the warehouse itself (working name Yggdrasil — the world-tree spanning realms, cosmos to root). plan-DAG (what to build) sits on top of system-DAG (what exists), both in the one historized warehouse. If a layer of that span is missing from the warehouse, it must be added — see the-two-dags.md and the nornir warehouse schema. *(Naming
- any missing-layer audit is out of scope for this doc — flagged for a warehouse pass.)*
EXISTS vs GAP
-
✅ nornir: live-growing plan-DAG (snarl), readiness lights, history side-panel.
-
✅ dwarves: auto-poll (~1.5 s ●live/⏸) grows live + submit strip (idea|error →
Funnel.SubmitIdea) grows it from the pane; offline-safe; robot + testmatrix in code. -
✅ dwarves: growth-history trail ("addera historik") —
FunnelHistoryrecords the DAG's(node,edge)size on every SUCCESSFUL poll, collapsing consecutive-identical polls so the trail is a de-duped list of the moments the DAG changed (grew), capped atHISTORY_CAP(64). Rides instate_json.{history,history_len,history_peak_nodes,node_growth}and renders as apeak / +grown / node-count → trailstrip;{"clear_history":true}over the seam (or theclearbutton) resets it. A transient offline blip never records, so it can't wipe the trajectory. Red-when-broken tests + matrix cellfunnel_history_addera_historik. -
✅ dwarves: run history ("addera historik", the run dimension) —
FunnelRunLogrecords a FULL DAG snapshot (the capturedPlanModel) per SUCCESSFUL poll whose plan changed (node/edge count or outcome differs), de-duped + capped atRUN_CAP(32). A run-picker (◷ latest (live)- one entry per past run, newest first) lets the user select a prior run to re-draw its final DAG through the SAME
facett-plandagview (no second renderer, L5); a per-run summary showsrun #seq · N nodes / M edges · outcome(outcome = Running/Done/Failed from the node statuses). The live view stays the default/"latest". Rides instate_json.{runs,run_count,selected_run}; driven over{"select_run":<seq>|null}/{"clear_runs":true}. Red-when-broken unit test + egui_kittest render leg; matrix cellfunnel_run_history_picker_and_bottleneck. Seefunnel-run-history-bottleneck-design.md.
- one entry per past run, newest first) lets the user select a prior run to re-draw its final DAG through the SAME
-
✅ dwarves: bottleneck annotation — a "clear next · <title>" hint over the DAG showing WHERE the funnel is constrained as it grows, CONSUMED from
facett-plandag's constraint analysis (PlanDagView.state_json.top_bottleneck— the node that unblocks the most downstream work, sources filtered) + the node titles; never recomputed dwarves-side (L5). Applies to whichever DAG is shown (live or a selected past run). Rides instate_json.{bottleneck,bottleneck_title,critical_path}. -
✅ dwarves + funnel-core: EVENT-SOURCED run history ("addera historik", the real ledger) —
Event::RunRecordedused to be a validate-and-drop arm (it checked the plan/node refs, bumpednext_run, and discarded every payload field), so run history was not recoverable from a replayedFunneland any consumer wanting it had to keep its own store. funnel-core now folds runs intoFunnel::runswith aruns()/runs_for_node()/runs_for_plan()read API and aruns_to_json()projection (newest first); a duplicaterun_idis rejected so a replayed or spliced log cannot double-count. The pane surfaces it asstate_json.{event_runs, event_run_count}and paints aruns · N recordedstrip (✔/✖/⊘ per outcome, hover = run/node/actor). This is the state engine's own history — not a parallel store. Distinct fromFunnelRunLogbelow, which snapshots the rendered DAG per poll for the run PICKER: a view concern, and the only history a remoteFunnel.Showcan support (that RPC returns the current funnel with no run ledger on the wire, soevent_runsis empty-but-present in thin mode). Robot scenariofunnel-event-run-history+ matrix cellsfunnel_event_sourced_run_history/funnel_event_run_history_robot. -
✅ dwarves: Claude credential visibility — the pane takes the host's resolved credential SOURCE over
{"claude_source": "…"}(a label, never a key) and warns "no Claude credential — configure it in Settings to expand ideas" when none is configured, so a funnel that cannot expand ideas says so. Seeclaude-token-path.md. -
⚠️ dwarves: intake side-list (server
Funnel.History— the per-idea event log, distinct from this size trajectory) — the P1 tail. This trail answers "how big did the DAG get, when"; the side-list would answer "which ideas/errors were funnelled, in order". -
⚠️ both: right-side / mini-graph layout while funnelling.
-
⚠️ both: anchor badge/edge from plan node → system-DAG symbol.
Two more interactions
1. Click-to-Yggdrasil linking
Clicking a plan-DAG node draws connective lines from that node down into the super-graph (Yggdrasil = the system-DAG) and highlights the node's anchored symbols — its RAG Anchors (funnel/decompose.rs::Anchor → symbol_facts/call_edges files & functions). Effect: "this task touches THESE functions/files." Reuse the anchor data already carried on plan nodes; light the anchored symbols in the DepGraph/CallGraph/Metro view and draw plan-node → symbol edges.
- Open dependency: LLM-planned nodes currently have empty anchors, so this is only meaningful once the LLM→RAG-anchor merge lands (tracked in
the-two-dags.md); model-freeCodeIndexRetrievernodes already carry anchors and work today. - Works in both surfaces: nornir funnel tab (snarl 2D +
funnel3d) and the dwarves funnel pane (facett-plandag) — the pane emits a "node selected → anchor ids" signal the host uses to highlight/link into its system-DAG view.
2. Idle auto-rotate (screensaver)
After N seconds of no interaction, the graph slowly auto-rotates to show the DAG off; any input stops it immediately. funnel3d.rs already has an auto-rotate toggle ("Auto-rotate (drag the cloud to grab the yaw)") — extend it to be idle-triggered (reset an idle timer on any pointer/key event; start rotating past the threshold, halt on the next event). Applies to the 3D cloud; the 2D snarl can gently pan/orbit its layout as the mini equivalent.