The graph subsystem — reasoning
Class:
reasoning. This file holds the why, the trade-offs, and every design considered and rejected. The descriptive shape actually built is ingraph-design.md, which by rule mentions none of this. If you want to know "why not X?" — X is here. Folds the rationale previously spread acrossfacett-graphview.md,graph3d-orgasmic.md, andfacett-syschart.md's "why a new component" section.
f0 — why five crates instead of one "graph" crate
The obvious tidy version is one facett-graph crate with every capability behind feature flags. It was rejected in favour of five crates split by what kind of host wants them:
- Dependency weight.
facett-graphview's scalable engine pulls invello/vello_cpu(and, behindgpu,wgpu/pollster/bytemuck). A host that just wants a draggable box-and-line canvas (facett-graphpan) should not have to compile that tree. Splitting the crates makes the dependency graph tell the truth:facett-graphpan/Cargo.tomlhas no vello dependency at all. facett-graph3d's GPU lane is genuinely different work (instanced emissive rendering + bloom,graph_gpu.rs) fromfacett-graphview's vello seam — folding them into one crate would mean one crate owning two unrelated GPU pipelines.facett-syschartis not a graph-layout problem at all — its whole point is that positions are given (normalized, host-chosen), not computed. Cramming it into a "graph" crate that's mostly about layout algorithms would blur what it actually does.
The cost of five crates is duplicated small pieces — downstream_of (forward-edge BFS) is defined independently in both facett-graphview::decorated_view and facett-graphpan; Decorations/NodeDecoration are independently declared (not re-exported) in both crates. That duplication was accepted rather than adding a sixth "graph-common" crate, because each copy is ~20 lines and pulling a shared crate in would re-couple exactly the dependency trees the split was meant to keep apart.
f1 — why GraphView and DepGraphView stay on core Scene, not GraphModel
facett-graph predates facett-graphview's GraphModel/Decorations shape. When facett-graphview was built, GraphView/DepGraphView were not migrated onto it. Reasons this was left alone rather than "fixed":
GraphViewdraws throughfacett_core::draw(ui, &scene, layout, hint)— a battle-tested, already-themed core routine. Re-platforming it ontofacett-graphview's CPU/GPU rasterizer would trade a working, simple path for a spike-stage engine, for no user-visible gain (this is the simple graph viewer; it was never meant to scale to the sizesfacett-graphviewtargets).DepGraphView's drill-down panel and toposort-into-columns layout are purpose-built for the "click a node, see deps/dependents" interaction. That interaction doesn't need a pixel-level rasterizer at all — it's a plain egui painter routine. Building it againstfacett-graphview::depgraph_layoutinstead (the pure, egui-free port of the same idea) was considered, butDepGraphViewpredatesdepgraph_layoutand rewriting a working, tested facet purely to deduplicate a layout algorithm was not judged worth the churn.
The result is two independent implementations of "layered dependency DAG" — facett-graph::depgraph::DepGraphView (egui-painting Facet) and facett-graphview::depgraph_layout::DepGraphLayout (pure layout data, with direct/deep modes and collapse that DepGraphView does not have). This is a known duplication, not an oversight — see graph-design.md f1/f2 for what each actually offers.
f2 — why vello, and why "GPU-only" was rejected
The engine's founding constraint was: a headless CI box, a server with no GPU driver, and a laptop with a flaky one must all still render. A GPU-only design (wgpu hard dependency, no fallback) was rejected outright — it would make the engine untestable in CI and unusable on half the fleet.
vello_cpu was chosen as the CPU fallback specifically because it already ships transitively inside epaint 0.34 — every consumer already builds it, so the CPU path costs nothing extra and is the proven rasterizer (not a degraded "backup" mode nobody exercises). The GPU arm (vello 0.9 on wgpu 29) was chosen to be version-aligned with the egui-wgpu 0.34 stack already pinned elsewhere in the workspace, specifically to avoid a version war when the GPU seam eventually lands inside a live facett host.
Why both Backend arms currently rasterize through the CPU path (even Wgpu): building a live GPU texture-render-and-readback needs a real wgpu::Device/Queue + a render target + a paint-callback wiring into whatever host owns the frame. Forcing that to exist now, before a real host consumes it, would make the spike's own tests flaky on any CI box without a usable adapter. The chosen trade: prove the architecture (the version-aligned seam compiles, gpu::build_scene produces a real vello::Scene) and ship the proven CPU pixels under both dispatch arms, deferring the live readback to the actual consuming host's integration.
Why the L0 kernel adoption (l0.rs) is a separate, non-default path rather than replacing cpu::render: the L0 vocabulary is SDF circles/markers and straight capsules; the reference path draws rounded-rects and cubic Béziers. Forcing pixel parity would mean either downgrading the reference path's visual quality or building a rounded-rect-with-border SDF + cubic tessellation into the shared core kernel before this crate could adopt it. Neither was judged worth blocking on — l0.rs exists to prove the routing (a graph really does flow through the shared facett_core::render::Canvas the map skins use), left as a documented, deliberately non-parity alternative lane.
Roadmap items documented but not built — LOD/viewport-culling, and the "wire community/fdeb/gpu_layout into the default render path" step — are called out as stubs with TODOs in the source (facett-graphview/src/lib.rs:31-34, community.rs:12-19, fdeb.rs:18-25, gpu_layout.rs:26-28) rather than half-built in-tree, because each is substantial enough (a real Barnes-Hut quadtree for gpu_layout, a GPU port of fdeb's force loop, recursive multi-level Louvain) that landing the foundation clean and tested first was prioritized over a partially-wired feature.
f3 — why graphpan exists alongside graphview's DecoratedGraphView
facett-graphview::DecoratedGraphView already does pan/zoom/select/decorations as an owning Facet. facett-graphpan was built anyway, as the minimal, borrowed counterpart, because:
- It is a direct adoption of nornir's existing
src/viz/graph_render.rsdraw_graph— the exact routine already driving nornir's 🏛 Architecture board and 🧬 release dashboard. Rewriting that call site againstfacett-graphview's owning-Facetshape would mean nornir giving up its own caller-ownedGraphViewtransform (which it needs to persist per-tab state across frames the way it already does) and pulling infacett-graphview's vello/wgpu dependency tree for a feature nornir's board doesn't use (there is no GPU rasterizer need for a static wiring board). content_extent+draw_graph_scrolled's honest-scrollbar behaviour has no equivalent infacett-graphview—DecoratedGraphViewalways paints intoui.available_size(), appropriate for a resizable pane but wrong for a board that should scroll like a real oversized document.
The two are kept complementary, not merged: graphview is "the consolidated render engine" (CPU/GPU rasterizer + a Facet on top); graphpan is "the minimal paint-this-model-and-tell-me-what-was-clicked routine" a caller drives directly.
f4 — why graph3d's beauty is theme-gated, not always-on
The "orgasmic" neon/glow/bloom treatment (DecorPolicy::Full) was designed as the holger sellpoint — a stunning 3D repo/dependency graph — with one hard constraint: the beauty must live in facett, not be hand-rolled per-host, so holger, nornir, and korp all inherit it for free from one change.
Rejected: making Full decor the unconditional default. A device/airgap console (the None-effects LookTheme preset) needs to stay calm and legible — a strobing crimson node on a monitoring console read by an operator under stress is a liability, not a feature. So decor is driven by the active theme's EffectsPolicy by default (DecorPolicy::FromTheme), with with_decor as an explicit override for a host (or a test) that wants to force a specific level regardless of theme. This mirrors the look-and-feel.md LAW that effects are theme-controlled, never forced.
Why every effect is a pure function of one injected clock (determinism LAW): re-rendering the same state + clock must be bit-identical, so a headless screenshot test can pin the clock and assert byte-stable PNGs. An ambient, wall-clock-driven animation (the obvious simpler approach) was rejected because it makes every golden-image test flaky by construction — the frame would differ every run.
Why the click → shockwave + dim behaviour dims to DIM_ALPHA ≈ 0.1 rather than hiding unrelated nodes entirely: hiding would destroy the "neighbour subgraph in context" read — the whole point of the neon-dashboard focus is to see the lit subgraph against the rest of the system, not in isolation.
Why the GPU bloom lane (graph_gpu.rs) is headless-only today: it is T3.4 of a staged rollout — proving the instanced-emissive + bloom pipeline renders correctly offscreen (assertable as DATA: mean_luma/luma_at) was sequenced before wiring it into a live egui_wgpu paint callback, the same staging logo3d::gpu went through first. Building the live wiring before the offscreen proof existed would have made a rendering bug indistinguishable from a paint-callback wiring bug.
f5 — why syschart is a new component, not a reuse of GraphView/DepGraphView
Considered and rejected: extending facett_core::draw/facett-graph::GraphView or DepGraphView to cover the "monitoring map of a few peer services" shape (a PKI/OIDC/Nexus triangle for holger's mannequin GUI).
GraphView/coreScenepaints a non-interactive scene — no per-node badge, no click-select, no inline detail panel. Adding all three would turn a simple, static viewer into something it was never meant to be, for a use case (a handful of peer nodes) that doesn't needScene's generality.DepGraphViewis a layered DAG with a fixed toposort-into-columns layout and a fixed deps/dependents drill-down shape. A system map of 3-10 peer services wants free, host-chosen positions (a literal triangle, not columns) and a free-form detail panel the host fills with arbitrary text — neither fits the layered-DAG assumptionDepGraphView's layout algorithm is built around.
Building a new, small, purpose-fit component (SystemChart) was judged cheaper and clearer than bolting a third, incompatible layout mode and a badge/detail system onto either existing viewer. The trade: syschart duplicates the "circle node + click-select + AccessKit hit node" pattern rather than reusing DepGraphView's box-node painter — accepted because the visual shape (circles at free positions vs. boxes in columns) is different enough that sharing the paint code would have meant threading a shape parameter through code that was written assuming boxes.
Why SysNode::pos is normalized (0.0..=1.0) rather than world-space pixels/units (as f1–f4 all use): syschart's whole reason to exist is a small, fixed topology a host lays out once by hand (a PKI/OIDC/Nexus triangle is not something you want computed by a force layout). Normalized coordinates make that hand-placed layout resolution-independent — the same (0.12, 0.15) reads correctly whether the canvas is a phone-sized panel or a wide monitor, which matters for SystemChart's deterministic-screenshot testing story in a way it wouldn't for f2's force-laid-out graphs (which are pixel/world-space because their positions come from a layout algorithm, not a human).