facett-helix — the TIME-HELIX DAG view
A plan / dependency DAG rendered as a helix that advances ALONG a time axis. Time is one axis; the DAG winds forward through time as a modern rounded helix (not a spin-in-place z-rotation). Nodes are plan items — idea / task / test / crate — each tied to a system (its angular lane / strand). Edges are soft rounded rubberband splines. Coils can be wound onto a pin, many coils coexist in one scene with inter-helix links, and an animated traverse flies a time-cursor along the axis. GPU-rendered with emissive bloom.
The model (what a host builds)
Scene { coils: Vec<Coil>, links: Vec<InterEdge>, tension } // a multi-helix scene
Coil { dag: PlanDag, origin, axis, wind } // one helix + placement
PlanDag { nodes: Vec<Node> }
Node { id, label, t, system, status, kind, deps, events }
Dep { on, level } // SCIP-style dependency depth (1 = direct, 2.. = transitive)
Event { t, kind, label } // a per-node timeline marker the traverse sweeps through
Edge { from, to, level } // intra-coil, derived from deps
InterEdge { from: HelixRef, to: HelixRef, level } // cross-coil system links
t→ distance ALONG the central (time) axis.system→ the angular lane (which strand the node winds on).wind∈ [0,1] → reel a coil onto its central pin (1.0) or splay it along time (0.0).kind/EventKindare open enums (Other(String)), so a host adds tags without editing the model.
The "add ideas" API (what the owner asked for)
use facett_helix::model::{PlanDag, Kind, Status, EventKind};
let mut dag = PlanDag::new();
let idea = dag.add_idea("helix view", "facett", 0.0, Kind::Idea); // -> NodeId
let task = dag.add_idea("layout math", "facett", 1.0, Kind::Task);
let test = dag.add_idea("spline test", "facett", 2.0, Kind::Test);
dag.add_dep(task, idea); // task depends on idea (level 1, direct)
dag.add_dep_at(test, idea, 2); // test transitively depends on idea (level 2)
dag.set_status(test, Status::Active);
dag.add_event(task, 1.5, EventKind::Cve, "RUSTSEC-…");
// …or the fluent builder:
let mut b = PlanDag::builder();
let a = b.idea("a", "core", 0.0, Kind::Idea);
let c = b.idea("c", "core", 1.0, Kind::Task);
b.dep(c, a);
let dag = b.build();
Wrap it in a view: HelixView::from_dag("plan", dag) (single coil) or build a Scene of many Coils + Scene::add_link(...) for cross-coil system links.
Helix layout math (layout.rs, pure + deterministic)
A node's coil-local point is [along, r·cos θ, r·sin θ] (time axis = local X):
u = (t − t_min) / (t_max − t_min)— normalised time across the coil's span.along = u · axis_len · (1 − wind)— distance along the (wound) time axis.θ = u · turns · TAU + lane_offset(system)— the winding angle; it advances witht, so the strand spirals forward through time, andlane_offsetseparates systems.r = radius + lane · lane_spread.
The coil's Frame (an orthonormal basis around axis, translated to origin) maps that local point into world space, so coils point anywhere and many coexist.
Edges follow the helix: control points are sampled by interpolating u, the lane offset and the radius while re-evaluating the winding term at every sample, so a link spirals along the tube instead of cutting a straight chord. The controls run through a Catmull-Rom chain (rounded, passes through every control point). With tension > 0 the spline becomes an elastic rubberband: interior controls are displaced transverse to the chord by a bell-shaped, tension-scaled amount driven by an injected easing curve (the view feeds facett_core::effects::easing::elastic), so the edge wobbles and settles — and is a pure function of the clock (determinism).
Inter-helix links bridge two coils' world points with a bowed arc, rendered with the same elastic spline.
The traverse (the key ask: advance THROUGH time, not spin)
The view holds a time-cursor u ∈ [0,1]; the camera pans the world time-axis (X) to keep the cursor centred (the "now" plane). Msg::ToggleTraverse(true) auto-advances it from the clock; Msg::Seek(u) pins it. A secondary spin only rotates the tube's cross-section — the primary animation moves the camera along the axis. With an injected clock (set_clock / Msg::Tick) the cursor, the spin and the elastic wobble are all pure functions of it (FC-7), so the frame + AccessKit are reproducible.
Portability contract (why this is nornir-free, how it moves to ordning)
model.rs + layout.rs depend on nothing but serde + std — no egui, no facett, no nornir, no modgunn. They are the seam that git mvs into ordning:
- The view (
lib.rs,HelixView) is the only part bound to facett — it implementsfacett_core::Facet, paints with egui, and reusesfacett-graph3d'sgraph_gpuoffscreen+bloom lane (featurewgpu). When this crate moves,model+layoutgo to ordning as-is; ordning supplies its own host binding (or reusesHelixView). - Hosts map INTO the model; the model never reaches back. A host appends ideas via
PlanDag::add_ideaand links coils viaScene::add_link. - The elastic easing is injected (a
fn(f32) -> f32), so even the rubberband reuse of facett-core stays at the call site, not baked into the portable core.
Adapters (hosts populate the generic model)
These live in the host repos (keeping facett-helix host-free). Sketches:
nornir funnel → PlanDag (agent/nornir-helix-feed, follow-up)
Map nornir's funnel decompose-DAG (src/funnel) onto the model:
// for each funnel node, in decompose order:
let id = dag.add_idea(node.title, node.anchor_crate /*RAG-anchored file/crate = system*/,
order_index as f64 /* t */,
match node.role { Decompose => Kind::Idea, Implement => Kind::Task,
Prove => Kind::Test, _ => Kind::Task });
for upstream in node.requires { dag.add_dep(id, ids[upstream]); }
// route the "add idea" path straight through PlanDag::add_idea.
modgunn → Scene (one coil per crate)
Map a SCIP/cargo dependency graph: one Coil per crate, version-time on the axis, sub-modules as lanes, dependency depth as Dep.level (the view's level filter = "direct deps only"), CVEs/version bumps as Events the traverse sweeps, and inter-helix links for cross-crate edges:
let h = scene.add_coil(Coil::new(crate_dag).at(stacked_offset(i))); // crate i's coil
// inter-crate dep B → A (system-level):
scene.add_link(HelixRef { helix: hb, node: b_node }, HelixRef { helix: ha, node: a_node },
dep_depth);
// a CVE landing on a crate at its vuln-introducing version-time:
crate_dag.add_event(node, vuln_t, EventKind::Cve, advisory_id);
FC compliance
- state_json — every node's helix coordinates (
along,theta,lane,system,t,kind,status) + wind / level / cursor / tension /winds_forward. The picture as DATA. Msg+update—Seek·ToggleTraverse·SetWind·SetLevel·SetTension·Select·Tick/SetClock. The robot-/CLI surface.- AccessKit — each node is a queryable
Buttoncarrying its timetas the value. - Injected clock — traverse + elastic wobble are pure functions of it.
- GPU bloom —
gpu_scene(featurewgpu) reuses graph3d's emissive additive offscreen + bright→blur→composite; render-proofs intests/.