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

nordisk/facett

.nornir/graph-reasoning.md

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 in graph-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 across facett-graphview.md, graph3d-orgasmic.md, and facett-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:

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":

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:

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

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