Plan: facett-pipeline, facett-plandag and facett-brokkr draw through facett-flow
Implementation plan authorized by Andreas Persson, creator of facett-flow, on 2026-10-04. The components USE facett-flow; nothing of theirs moves into it. Every addition to facett-flow is a compile-time feature, off by default, so plain flow stays exactly as it is.
Based on facett main a367f9aa (including PR #2 and the latest Falsett optimizations) and the crates that embed these components (knut, nornir, dwarves, korp, ordning).
Implementation and verification status for this checkout is recorded in flow-pipelines-validation.md. Implemented code, local test evidence and outstanding release/hardware gates are distinguished there; the presence of code does not mark all 29 steps complete.
0. Rules every step follows
- Dependency direction. pipeline, plandag, brokkr and brokkrview each depend on facett-flow; facett-flow depends on facett-core. None of their concepts, editor gestures or component-specific layout go into flow.
- Flow only gets small generic additions. Each one sits behind a compile-time cargo feature that is off by default. Section 2 names at least two users for every feature.
- Plain flow stays as Andreas made it. With default features you get the same code paths, the same buffers (
Point16 B andLink16 B atscene.rs:186-202,Uniforms8×vec4 = 128 B atgpu.rs:17-25, 3 bindings atgpu.rs:62-96), the sameflow.wgslbyte for byte, and the same benchmark numbers. - The layered layout goes to facett-core.
- Adapters live in knut, nornir and dwarves.
Facts that shape the plan
Flow internals
- The pane key is
ui.id()(gpu.rs:382). - Panes are only evicted inside some pane's
prepare(gpu.rs:273). If no flow pane is drawn, nothing is ever freed. Modelholds the wholeArc<Graph>(lib.rs:20-31), andstate_jsonis the plain serde of that state (lib.rs:176). Every read serializes every node.- Each new
Scenegets a fresh global generation (scene.rs:209-212). Hosts that rebuild every frame would re-upload the GPU geometry every frame. - The CPU fallback is a "preview": straight 0.5 px lines, capped at 2 000 points / 4 000 links, plus the note "GPU host required" (
view.rs:64-86, 345-356). - The edge shader already uses adaptive curve detail (2–16 segments, 6 vertices per segment). The existing screen-error bound and GPU detail uniform remain the baseline.
- The README says the 1M-node figure is a validation budget, not a measured capacity on Linux.
- Loki baseline (Andreas's prototype, RTX 5080): 10k hub at about 60 fps with GPU pass p50 0.52 ms; 1M raw overview at 32.2 fps with GPU p50 23.2 ms; 1M clusters at 0.12 ms.
Layered layout exists in 7 copies today
facett-graph/src/depgraph.rs:142-165(Kahn, sources-aligned)facett-graphview/src/depgraph_layout.rs:332-371rank_dense(iterative DFS, sinks-aligned)facett-releaseview/src/vault.rs:71-104facett-brokkr/src/lib.rs:1113-1137step_depthsfacett-brokkrview/src/lib.rs:559-573facett-pipeline/src/lib.rs:701-733(rows and bounding box on top ofdag.columns())facett-plandag/src/lib.rs:393-415- nornir's original,
nornir/src/viz/depgraph_layout.rs
Embedders pin current behaviour
korp/tests/robot_demo_dag_layout.rschecks four things onPipelineView::layout_scene_centers: theCOL_GAPcolumn pitch, upstream nodes left of their consumers, the zoom ratio, and that two CPU frames at different clocks differ.- dwarves-embed, dwarves-gui, nornir-funnel-embed,
ordning-gui/src/plan_view.rsand nornir'sfunnel_plandagonly use constructors (new,from_adapter,from_model).
Release-order hazard. knut-pipelines requires facett-pipeline >=0.1.19, <0.3. facett's root [patch.crates-io] facett-pipeline = { path } (facett Cargo.toml, see its comment) only takes effect if that requirement matches 0.3.0. Otherwise the build links two PipelineModel types and knut_facett_matrix breaks.
1. Design rules for the flow features
-
R1. Zero-cost layout:
Node,EdgeandGraphget no new fields. The logicalNode.status,Edge.statusandEdge.rateare separate attribute tables infacett_flow::attr, indexed likegraph.nodes/graph.edges, and each exists only under its feature.- Cargo features unify across one build. A feature-gated field on
Nodewould break everyNode { .. }literal in a crate that does not know about it as soon as another crate enables the feature. - Plain serde output,
state_jsonand the 16 BPoint/Linkstay identical. - Live status and rate changes re-upload a few bytes without rebuilding geometry (pipeline and knut update every tick).
- Cargo features unify across one build. A feature-gated field on
-
R2.
MsgandModelbecome#[non_exhaustive]in 0.3.0. Users only receive these types, so feature-gated variants (Msg::Status,Msg::Rates,Msg::Sizes) and fields cannot break someone's exhaustivematch. This is semver-breaking and is agreed with Andreas in step 1. -
R3. GPU and WGSL.
- Bind group 0,
Point/Link,Uniformsandflow.wgslare untouched. - Features add bindings only in
@group(1)of separate variant pipelines. - The variant shader is built once at install:
flow.wgsl(verbatim) +flow_ext.wgsl(new entry pointsnode_vs_x,edge_vs_x,edge_fs_x,pulse_vs_x,arrow_vs) + one hook file per feature (hooks/<f>_on.wgslorhooks/<f>_off.wgsl). - The CPU chooses plain or variant pipeline per pane: a scene with no attribute tables draws with the plain pipelines even when the features are compiled in. There is never a runtime branch in the shader hot path, and a features-on build pays nothing for plain scenes.
- Tunables such as
MAX_PACKETSare WGSLoverrideconstants.
- Bind group 0,
-
R4. Every feature carries three proofs.
- A benchmark row on loki: plain flow is unchanged (10k and 1M hub, all 4 phases, GPU pass p50), and so is a plain scene in an all-features build.
- The
plain_witnesstest passes in the default build, plus acargo check -p facett-flowwith no features. - facett-build-thing rows build the feature alone (with and without
wgpu), all features together, and all features on wasm32.
2. Flow features: what each adds and who needs it
| feature | adds | users (at least 2) |
host |
Public FlowScene (wraps today's scene::Scene); FlowScene::with_key(graph, u64) combines caller identity with a complete graph digest, including positions and ordering; retain the scene between frames to avoid CPU rebuilds as well as GPU uploads; View{scale,offset}, View::from_navigable, Camera::view(rect,bounds); paint(ui, pane: egui::Id, rect, &FlowScene, &View, &PaintOpts) -> Painted; PaintOpts{seconds, moving, clustered, palette: Option<FlowPalette>, cpu: CpuStyle::{Preview, Full{max_edges}}}; gpu::evict_idle(&RenderState, frame) -> usize |
pipeline, plandag, brokkr, brokkrview. Same shape as facett-graphpan's free draw function against a caller-owned transform (graphpan/src/lib.rs:286-307) |
labels |
Painted.visible_nodes supplies visible geometry independently of labels; Painted.on_screen: Vec<OnScreen{node, rect, px_per_world}> supplies label candidates, bounded by label_budget (default 2 000, the A11Y_BUDGET precedent) and min_px; Painted.truncated; FlowScene::edge_point(i, t) (CPU twin of the WGSL curve()); a label pass in FlowGraph using facett_core::legibility::place_labels |
pipeline (names, status, rows, a11y, throughput labels at edge midpoints), plandag (metric bodies), brokkr and brokkrview, knut and nornir FlowGraph hosts |
boxes |
attr::Sizes{nodes: Vec<[f32;2]>} drawn as rounded-box SDF without the 14 px clamp; edge ends sit on box sides; bounds include box extents |
pipeline (160×56), plandag (184×84), knut table cards, nornir step boxes |
arrows |
Arrowhead at the consumer end (+3 vertices per edge, variant pipeline only) | pipeline, plandag, brokkr, brokkrview. Reduced-motion users get no packets, so direction must still read |
status |
attr::Status{Ok,Slow,Error} (#[non_exhaustive]); StatusTable{nodes, edges: Vec<Option<Status>>}; StatusPalette::from_theme (Ok→th.point, Slow→look::AMBER, Error→look::RED, None→plain kind colour) with a host override; FlowScene::set_status; Msg::Status |
pipeline, plandag, brokkrview, brokkr (validation errors), knut, nornir, dwarves |
rate |
attr::Rates{edges: Vec<Option<f32>>} mapped by facett_flowsim::flow::plan_edges (the single existing implementation; new optional dependency with default-features=false). Rate 0 means a dead edge with no packets; None keeps plain behaviour. set_rates; Msg::Rates |
pipeline (throughput_rows_s), plandag (rows per edge), knut (bifrost rows/s), synthetic-sdp |
layered |
Graph::layered(&mut self, LayeredOpts): positions from facett_core::layered, left to right |
knut, nornir and dwarves adapters, Falsett demo fixture |
compact-state |
An explicit per-instance compact-state setting makes FlowGraph::state_json report {node_count, edge_count, graph_digest, camera, seconds, running, reduced_motion, clustered, selected, render_counts, lane}; feature unification alone changes no instance; Elm::state() is unchanged; the digest is cached at graph replacement |
Falsett tab (mega_matrix and robot_demo read it), knut/nornir/dwarves FlowGraph hosts at 100k |
hit-test |
FlowScene::hit(world, tol) -> Option<Hit::{Node,Edge,Group}> (box-aware; edges by distance to the 16-segment CPU curve); hits_in(rect); Painted.hover. Uses flow's existing 64-unit bucket index; interaction is independent of the label budget. A tree-index alternative is deferred to future_improvements.md |
brokkr (connect, move, delete edge), brokkrview and pipeline/plandag beyond the a11y budget, FlowGraph box-aware selection, knut |
lod |
Retains the existing adaptive 2–16 segment count; adds opt-in packet visibility thresholds without restoring fixed 96-vertex edges | FlowGraph at 1M (23.2 ms today), knut/nornir at 100k, 10k pipelines |
Which features each user enables
| user | flow features |
| pipeline | host, labels, boxes, arrows, status, rate |
| plandag | host, labels, boxes, arrows, status |
| brokkrview | host, labels, arrows, status |
| brokkr | host, labels, boxes, arrows, status, hit-test |
| knut adapter | layered, status, rate, labels, compact-state (+ lod) |
| nornir and dwarves adapters | layered, status, labels, compact-state |
- The umbrella feature
facett/flow-wgpustays as it is; no pass-through features are needed. - Each component crate gets
wgpu = ["facett-flow/wgpu"], following thefacett-graph/wgpuconvention.
3. Steps
Each step lists files, API, what is deleted, tests (red first, then green), gates, size and the definition of done. Steps 4–12 and 25–26 are flow pull requests on Codeberg nordisk/facett with Andreas as reviewer (he may author any of them).
Step 1 — Preparation and agreement with Andreas (S)
Depends on: nothing.
- Andreas authorized implementation of the full plan with subagents in this session. Reviewable code and proof results precede any publication; no duplicate approval issue is needed to start local work.
- Re-measure plain flow on loki on today's main with
flow_lab --benchmark: 10k and 1M hub, 4 phases. The falsett-loki numbers came from his prototype on egui 0.35. - Done: authorization is recorded in the session and fresh baseline rows are recorded in
.nornir/facett-benchmarks.md. Historical prototype numbers are not a current-main measurement.
Step 2 — facett_core::layered (M)
Depends on: 1.
Files: new facett-core/src/layered.rs (pure Rust, no egui, no serde); facett-core/src/lib.rs gets pub mod layered;.
API:
pub enum Align { Sources, Sinks } // as-early-as-possible / as-late-as-possible
pub enum RowOrder { Input, Barycenter { sweeps: u8 } }
pub struct LayeredOpts { pub align: Align, pub rows: RowOrder }
pub struct Layered { pub col: Vec<u32>, pub row: Vec<u32>, pub cols: u32, pub rows_max: u32, pub cyclic: bool }
pub fn ranks(n: usize, edges: &[(u32, u32)], align: Align) -> (Vec<u32>, bool);
pub fn layered(n: usize, edges: &[(u32, u32)], opts: LayeredOpts) -> Layered;
pub fn place(l: &Layered, pitch: [f32; 2], pad: [f32; 2]) -> Vec<[f32; 2]>;
pub fn crossings(l: &Layered, edges: &[(u32, u32)]) -> u64;
- Edge
(a, b)meansais placed left ofb. - The algorithm is graphview's iterative
rank_dense, generalised to both alignments: O(V+E), an on-stack cycle guard, and safe on a 100k-deep chain. RowOrder::Inputkeeps today's rule (declaration order within a column).Barycenteris new.
Tests (facett-core/tests/layered.rs; red because the module does not exist yet):
- chain, diamond, wide-vs-deep and a random 1k DAG
SourcesvsSinksInputorder is stable- barycenter crossings are ≤ input order on 20 seeded DAGs, and strictly fewer on a crafted case
- self-loop, 2-cycle and strongly connected component plus tail all terminate with
cyclic = true - 100k chain without stack overflow
- 100k nodes / 200k edges within a time budget (pattern of
depgraph_layout.rs:583-588) cargo check --target wasm32-unknown-unknown
Gates: facett/tests/no_module_shadowing.rs, clippy. Add a benchmark row for 100k and 1M on dwarf (CPU).
Step 3 — Move graph, graphview and releaseview onto core (M)
Depends on: 2.
-
3a. facett-graph.
DepGraphView::columns()(depgraph.rs:142-165) callsranks(n, swapped, Align::Sources); the signature is unchanged.- First commit
facett-graph/tests/columns_frozen.rswith literal expected vectors captured from today's code (green on the old code), then switch to core (still green). - Cyclic fixtures may change; update those expectations deliberately, with a comment.
- First commit
-
3b. facett-graphview.
rank_dense(depgraph_layout.rs:332-371) becomes coreAlign::Sinks. TheDepGraphLayoutAPI and itsstate_jsonare unchanged; existing tests, including the 100k budget, stay green;facett-coderoom-view(lib.rs:500) is untouched. -
3c. facett-releaseview.
vault.rs:71-104uses core; its normalisation is kept. -
Deleted: about 90 lines of duplicate layout.
-
Gates: mega_matrix depgraph cell, robot_demo (depgraph, World coderoom), and a re-measured World coderoom 100k benchmark row (lane G), unchanged within noise.
Step 4 — Proof scaffolding for flow features; no feature yet (M)
Depends on: 1.
-
Plain witness. Unit tests in
scene.rs/gpu.rscompiled only in the default build (#[cfg(all(test, not(any(feature = "host", …))))]). They assert:size_ofPoint=Link= 16 andUniforms= 128- 3 bind-group entries and the pipelines {node, edge, pulse}
Nodeserde keys are exactly {id, label, position, group, kind}- the shader source has a pinned sha256 equal to today's
flow.wgsl
-
Make
MsgandModel#[non_exhaustive](R2). -
Build rows in
facett-build-thing/src/lib.rs:facett-flow-plain(kindLib, no default features) andfacett-flow-wgpu- per feature: alone, and alone plus
wgpu facett-flow-allandfacett-flow-all-wasm32- all emitted into
.nornir/build-variants.toml
-
Guard in
facett-build-thing/tests/declared.rs: every key in facett-flow's[features](excepttestmatrix) has its rows. It goes red when a feature lacks a row. -
Browser gate.
xtask/tests/wgsl_compiles_in_the_browser.rslearns a genericwgsl-compositions.txtmanifest (name: a.wgsl b.wgsl …).- Fragments listed there are judged only inside their compositions, never on their own.
- A deliberately broken fixture composition must be refused (same idea as the existing negative water.wgsl check).
-
flow_labrecords the compiled features and GPU pass p50 in its benchmark JSON. -
Done: witness green, rows emitted, browser gate still 33/33.
Step 5 — feature host (M)
Depends on: 4.
Files:
- new
src/host.rsandsrc/curve.rs(the CPU curve; compiled when any ofhost,labelsorhit-testis on) gpu.rs: the plaindrawpassesui.id()to an inlined private function that also takes an explicit pane id;evict_idleis added underhost+wgpuscene.rs: generation incorporates the complete geometry content and caller key, with distinct raw and clustered identities. Hosts keep scenes between frames; attribute setters do not rebuild geometry.
API: as in section 2.
What changes for hosts:
CpuStyle::Fulldraws cubic curves with packets at the same phase formula as WGSL, does not show the "GPU host required" note, and respectsmax_edges.- This is needed because pipeline and plandag tests run on the CPU kittest renderer, and korp asserts that CPU frames differ by clock.
Tests (tests/host.rs):
- CPU paint draws curves and shows no preview note
View::from_navigableround-tripsCamera::viewequals FlowGraph's own mapping (view.rs:17-22) for 50 random cameras- GPU (
FACETT_TEST_GPU): with the same key, the second frame uploads 0 bytes (lane::note_upload) evict_idlefrees a pane after 2 frames (pattern ofgpu_panes.rs)edge_pointsamples land on lit pixels
Proof: R4 triple.
Step 6 — feature labels (M)
Depends on: 5.
-
Files:
src/labels.rs. TheFlowGraphlabel pass is a separate call compiled only under the feature;view.rs's default path is unchanged. -
Tests:
on_screenis bounded by budget and rect, with stable order and thetruncatedflagedge_point(t=0/1)returns the node positions and matches the WGSL formula on 100 pairs- a 10k FlowGraph shows no label overlap (
place_labelsreport)
-
Proof: R4 triple.
Step 7 — feature boxes and the shader-variant machinery (L)
Depends on: 5.
Files:
src/flow_ext.wgsl,src/hooks/*_on.wgsland*_off.wgsl,src/wgsl-compositions.txtsrc/attr.rsgpu.rs: avariantmodule (feature-gated) with the group-1 layout, variant pipelines built with the same closure (msaa_state(), keeping theone_msaa_writergate green), per-pane plain/variant choice, and attribute buffers with their ownattr_generation
API: attr::Sizes (validated: length, finite, greater than 0); FlowScene::set_sizes; Msg::Sizes.
Tests:
- naga validates every composition in the manifest
- GPU: box pixels appear at the expected rectangles
- in a features-on build, a scene without sizes uses the plain pipelines (witness)
Proof: R4 triple, plus the row "boxes build, plain scene equals default".
Step 8 — feature arrows (S)
Depends on: 7.
- An
arrow_vs/arrow_fsvariant, enabled per scene withFlowScene::set_arrows(true). - Tests: arrow pixels sit at the consumer end; plain scenes have none.
- Proof: R4 triple.
Step 9 — feature status (M)
Depends on: 7.
-
Files:
attr.rsandhooks/status_*.wgsl;Modelgets#[cfg(feature="status")] #[serde(default)] status: Option<Arc<StatusTable>>. -
Tests:
- palette mapping across 3 themes
- a status-less scene in a status build reads back pixel-identical to the default build
- a live update uploads only the attribute bytes
Msg::Statuswith a wrong length is rejected without mutation (pattern ofcontracts.rs:16-39)
-
Proof: R4 triple.
Step 10 — feature rate (M)
Depends on: 7.
-
rate = ["dep:facett-flowsim"]. -
GPU: the edge attribute is
{speed f32, packed u32};pulse_vs_xdraws edges ×MAX_PACKETSinstances and hides packetjwhenj ≥ count. -
CPU:
CpuStyle::Fullusesflowsim::particle_s. -
Tests:
- rate 0 gives no packets: a dead pipeline stays dark, the same claim pair as
pipeline/tests/robot_flowsim.rs:131 - the busiest edge is the fastest
Nonebehaves like plain- the CPU path matches
plan_edges
- rate 0 gives no packets: a dead pipeline stays dark, the same claim pair as
-
Proof: R4 triple.
Step 11 — feature layered (S)
Depends on: 2, 4.
Graph::layered(&mut self, LayeredOpts{align, rows, pitch}).- Tests: positions finite and deterministic, x increases along every DAG edge, 100k within budget.
- Proof: R4 triple.
Step 12 — feature compact-state (S)
Depends on: 4.
- The feature exposes an explicit per-instance compact-state setting; ordinary instances retain the full existing schema.
graph_digestis cached on graph replacement and covers the full graph, with no new dependency. - Tests:
state_jsonat 100k is under 2 KB with all keys present;Elm::state()still round-trips. Under this feature thecontracts.rs:5-14test serializesflow.state()instead ofstate_json; the default build keeps the test as it is. - Proof: R4 triple.
Step 13 — plandag draws through flow (M)
Depends on: 2, 5–9.
Cargo: drop facett-graph; add facett-flow with host, labels, boxes, arrows, status; add wgpu = ["facett-flow/wgpu"].
facett-core change: extract pipeline's private input handling (lib.rs:740-793) into facett_core::nav::gestures(ui, rect, &resp) -> Vec<NavGesture{Pan, Zoom{factor, anchor}, Fit}>. It is used by pipeline, plandag, brokkr and brokkrview; otherwise plandag would copy pipeline's code.
Kept: model.rs unchanged (resolve, bottlenecks, critical path), header controls, detail line, copy, portable_state/load_state, all state_json keys and the a11y labels "{title} · {status}" with heat as value.
State and messages:
PlanDagStategains#[serde(default)] nav: Navigableandfitted.Msggains Pan / Zoom / Fit / RequestRefit and becomes#[non_exhaustive].- Caps add
navigable(). - The scene is rebuilt in
update(not inview) on version or collapse changes, usingFlowScene::with_key. A metric switch only touches attributes.
Mapping to flow:
- Node id is
p{i}; label is the title; position comes from core layered (Sources, producer → consumer). - Spacing constants (240/110) are kept; boxes are 184×84.
- Status: Done→Ok, Failed→Error, Running/Idle→None.
plandag's own overlay (CPU, using visible geometry independently of the label budget):
- heat fill, metric body, Photon ⚡, spill
- borders, downstream ring, bottleneck badge with its a11y node
- click, and double-click to expand a collapsed group
- critical-path strokes sampled with
edge_point
Deleted: lib.rs:393-415 (DepGraphView columns, pos_for, fixed canvas), :430-441 (draw_arrow loop), and the facett-graph dependency.
Behaviour changes:
- Orientation flips to producers on the left (scan on the left, join on the right). Today the sink is on the left because of the
DepEdgeconvention. - plandag gains pan, zoom and fit.
Tests (tests/flow_lane.rs, red first):
- source x < sink x (red on the old code)
- camera messages, and
state_json["nav"] on_screenculling at zoomrender.rsunchanged and green- collapse changes the scene generation; a metric switch does not
- GPU frames > 0
Gates: compliance.rs:233-234 (Caps6 unchanged; add a navigable note) and facett/.nornir/compliance.md; facet_completeness (COVERED); mega_matrix plandag cell; robot_demo :681-685; navgraph_demo ceiling 0.
Step 14 — pipeline draws through flow (L)
Depends on: 2, 5–10, 13 (for the shared gestures).
Cargo: drop facett-graph; add flow with host, labels, boxes, arrows, status, rate; add wgpu = ["facett-flow/wgpu"].
Kept unchanged (all public API):
- Types: Status, StageStatus, UnknownReason, StatusTally,
model,wire, the promote and runa adapters, PipelineNode, JobProgress PipelineState, includingnav: NavigableMsg,Effect, and everyPipelineViewbuilder and getter, includingnav,set_camera,fit_to,zoom_to_cursor,pan_pixels,visible_node_count,layout_scene_centers,take_preview_requestedge_phase,status_glow,EdgeFlow,human_count,human_bytesNODE_W,NODE_H,COL_GAP,ROW_GAP(korp pins the pitch)- the a11y labels at
lib.rs:946-955 state_jsonkeys, including"dag". It is rebuilt fromLayeredas exactly{nodes, edges, columns, selected: null}, the shape atdepgraph.rs:371-378.
Replaced:
- The
dag: DepGraphViewfield (lib.rs:407, 473) becomeslayout: Layeredplus persistentscene: FlowScene(keyed by complete graph content, including positions and ordering) plus status and rate tables. layout_scene(:701-719) uses core placement with the same constants.
Mapping to flow:
- Flow edges are source → consumer, i.e.
(to_idx, from_idx), because pipeline'sedge_endsmean "from reads to". - Rate is the consumer's
throughput_rows_s, the same signal asedge_flow_volume(:1471-1495). - Status: Done→Ok, Failed→Error, Unknown→Slow, Running/Idle→None.
- Throughput labels are drawn at
edge_point(i, 0.5). - The overlay keeps glow, dashed border for Unknown, ⚠ and ?, the text lines, the selection ring, and one a11y node per on-screen node (budget 2 000).
- The injected clock becomes
PaintOpts.seconds.
flowsim overlay:
- When pipeline's flowsim overlay is active, use
Animation::Externalto suppress Falsett packets and animated edge trails. An absent rate table otherwise retains plain animation; it is not an animation-disable flag. with_flowsimandflowsim_mutbecome#[deprecated(since = "0.3.0")]. No caller exists in knut, dwarves, nornir, korp or holger; removal is the owner's call at 0.4.0.
Deleted: paint_dag edge loop :911-927, and paint_flow_edge / edge_spline / EDGE_CURVATURE / EDGE_SEGMENTS :1097-1194 (about 110 lines).
Tests:
- Characterization first:
layout_scene_centersfrozen on fixtures (korp's six-column chain and synthetic-sdp 100), green on the old code. tests/flow_lane.rs: packet direction, status table, rate 0 means no CPU packets, a11y budget, same clock gives identical AccessKit tree, different clocks give different CPU frames, and a per-frame rebuild uploads geometry only once (GPU).- Rewrite the unit test
edges_render_as_smooth_splines…againstedge_point. a11y_determinism,callchain_matrix,robot_callchain,knut_facett_matrixand the flowsim tests stay green.- Run korp's
robot_demo_dag_layout.
Gates: compliance.rs:120-127 and compliance.md:46; mega_matrix pipeline and synthetic-sdp cells; robot_demo :637-647; navgraph_demo.
Step 15 — brokkrview's DAG through flow (S/M)
Depends on: 5, 6, 8, 9, 13.
This is read-only, so it does not need the editor work.
dag_ui(lib.rs:546-630) uses core layered and flowpaint.- Status: passed→Ok, failed→Error, everything else None.
- The Tillsynia colours (
status_color :53-63) are passed as palette overrides so the dwarves look stays. - An overlay shows "{stage} · {status} · {disposition}"; steps become a11y buttons; a camera is added.
state_json["dag"]is kept (tests/headless.rs:61, mega_matrix:1856).- Gates: in
facet_completeness, brokkrview moves from BACKLOG to COVERED with a registry row incompliance.rsandcompliance.md(backlog 52→51).
Step 16 — demo, facet-wrapped and wasm wiring (M)
Depends on: 13–15.
facett-demo:
- No new tab titles:
EXPECTED_FACET_COUNTstays 91/82 (lib.rs:361/366);TAB_GROUPSandLEGENDtitles are unchanged, so the tab strip and the goldens do not move. - Update the LEGEND texts for pipeline, plandag, brokkrview and Falsett.
- The Falsett tab gets a fixture choice: hub, stages, or layered DAG with status and rate (synthetic and labelled as such).
- Add a direct facett-flow dependency with
layered, status, rate, compact-state. - Call
evict_idleonce per frame next to the install inmain.rs:55-56and:138-139. - New robot_demo step for the Falsett fixture.
facet-wrapped:
- The dependency lines for pipeline, plandag and brokkrview get
features = ["wgpu"], identical infacet-wrapped-group/Cargo.toml:97-108(facet-wrapped itself never declares a feature). wasm.rs install_gpu_lanes(:411-418) addsfacett_flow::gpu::install_renderer.- Add LANES rows and the matching rows in
xtask/tests/wasm_bundle_has_gpu_features.rsandwasm_bundle_installs_the_lanes_it_selects.rs. - Update the flow reason in
inventory.rs:55.
Also:
- Record wasm sizes before and after (facet-wrapped 14.7 MB, demo 28.8 MB) in the benchmarks file.
- Gates: roster_gate, pop_ran_list, expected_titles, mega_matrix, robot_demo, navgraph_demo, snapshot, facet-wrapped inventory, the xtask lane tests, and build-thing
declared.rs.
Step 17 — verification on loki (M)
Depends on: 16.
Build on dwarf through the container road and never compile on loki. Use flock ~/runs/loki.gpu.lock, keep the GPU under 85 °C, and do not set VK_LOADER_DRIVERS_SELECT.
-
GPU tests: flow
gpu_panesand all new GPU tests (FACETT_TEST_GPU=discrete). -
Existing goldens: run the demo snapshot and the 7 palette goldens without
UPDATE_SNAPSHOTS. They are expected to stay green because the graph tab is unchanged. If one goes red, re-bless it deliberately and record pixels and region in thesnapshot.rs:94-98comment. -
New goldens: bless the pipeline, plandag and brokkrview flow-lane goldens once (
assert_golden_within(…, 2)). -
Chrome WGSL: headless Chrome with
FACETT_REQUIRE_GPU=1judges plainflow.wgslplus every composition (33 + N). -
Benchmarks: a new section "facett-flow features" in the benchmarks file.
- default, each feature alone with a plain scene, and all features with a plain scene, at 10k and 1M hub
- feature scenes at 10k and 100k
- synthetic-sdp at 100 and a 10k pipeline, plandag at 1k, brokkrview at 500
-
Done: zero reds compared with main's baseline.
Step 18 — docs, cards and credit (S, rolling)
- facett-flow
README.md: an "Optional features" table and an embedding example. The "created by Andreas Persson" line,authors = ["Andreas Persson"]andCREDITS.mdare untouched; any extra credit wording only with his OK. - New card
facett-flow/.nornir/facett-flow.md, plusexclude = [".nornir/"]in its Cargo.toml. - Update the pipeline, plandag, brokkr and brokkrview cards;
.nornir/pipeline-design.md,pipeline-guide.mdandgraph-design.md. - Edit
.nornir/README.md;README.mdis generated from it. - Regenerate
docs/manual.pdf; updatecompliance.mdand the plan rows.
Step 19 — knut: widen the facett-pipeline requirement (S)
Must happen before step 20.
- In
knut-pipelines/Cargo.toml, change facett-pipeline to>=0.1.19, <0.4. Keep knut's git-rev pins (knut/Cargo.toml:128-131) until 0.3.0 is published. - Test: facett's
knut_facett_matrixpasses with the 0.3.0 bump.
Step 20 — release 0.3.0 (M; the owner runs every publish)
Depends on: 17–19.
-
Version. The workspace goes 0.2.0 → 0.3.0 (
Cargo.toml:17; every crate shares one version by repo rule). Breaking: flow#[non_exhaustive], plandag state fields and messages, brokkrview model. -
Publish order. facett-flow moves ahead of pipeline (in 0.2.0 it came after pipeline):
- core → … → flowsim → flow → graph, graphview → releaseview, coderoom-view → pipeline, plandag, brokkrview, brokkr → … → facett umbrella → app, about → demo
-
Same exclusions as 0.2.0; strip the root
[patch]on a temporary commit; use a freshCARGO_TARGET_DIR. -
Publish the next PyPI version (the
python_versions_track_the_workspacegate) and redeploy Pages.
Step 21 — downstream bumps (M)
Depends on: 20.
-
knut: move to the registry 0.3.0 and drop the git pins.
-
nornir:
Cargo.toml:1053-1107. -
dwarves:
- dwarves-embed
:34 - nornir-funnel-embed
:38, :44 - dwarves crate
:790 - dwarves-gui
:134, :146, :484, :562
- dwarves-embed
-
korp: 30 requirements (
Cargo.toml:930-1064, 1797ff). -
Others: edda step-board, holger (umbrella), ordning (plandag).
-
Each GPU host adds
facett_flow::gpu::install_rendererand enableswgpuon the component dependency. -
Run their suites: korp
robot_demo_dag_layoutandfunctional_emit, the dwarves-gui pipeline and panel tests, nornir's funnel plan-DAG pane, ordning-gui'splan_view.
Step 22 — knut adapter (M)
Depends on: 9–12 (develop against a git rev; release after 20).
Files: knut-bifrost/src/flow_adapter.rs, behind a new feature facett-flow = ["dep:facett-flow"]. This follows the precedent of knut-pipelines' facett-adapter feature. Flow features used: layered, status, rate, labels, compact-state.
API:
pub struct DeliveryRoute { pub source: String, pub sink: String, pub table: String }
pub struct FlowParts { pub graph: facett_flow::Graph, pub status: StatusTable, pub rates: Rates }
pub fn delivery_graph(routes: &[DeliveryRoute], outcomes: &BTreeMap<String, RoundOutcome>, report: Option<&RoundReport>) -> FlowParts;
pub fn snapshot_chain(table: &str, snaps: &[SnapshotInfo], cursor: Option<i64>) -> FlowParts;
Mapping:
- Iceberg tables are kind 0; sinks are kind 2.
- Edge rate is
rows_loaded / (read + load)seconds. - A table in
failedis Error;behind > 0is Slow; 0 rows means rate 0. - In the snapshot chain, snapshots up to the cursor are Ok and later ones have no status.
Tests: real RoundOutcome and RoundReport fixtures without network; counts, statuses and rates; validate() passes; positions increase left to right; 100k routes within budget.
Step 23 — nornir adapter (M), plus removing nornir's layout copy (S)
Depends on: 9, 11, 12.
-
nornir/src/viz/build_flow.rs(featureviz):build_plan_graph(&[nornir_build_thing::plugin::BuildPlan], results) -> FlowParts.- A variant node, then the step chain, then the expected artifacts; statuses from forge results.
- Shown in
build_tab.rsas a FlowGraph.
-
nornir/src/viz/depgraph_layout.rsmoves to core.- Current-source adaptation at Nornir
c64abdce: this file already reexports edda'snornir-depgraphlayout. Keep Nornir's public edge facade and route it through Facett's shared-core layout, with DAG/collapse parity tests; do not introduce egui into edda's independent layout leaf.
- Current-source adaptation at Nornir
-
Tests: plan fixtures and nornir's viz robot suite.
Step 24 — dwarves adapter (M)
Depends on: 9, 11, 12.
dwarves/crates/dwarves/src/brokkr/flow_adapter.rsbehind featurefacett-flow.run_graph(&Dag, Option<&RunReport>) -> FlowParts: nodes grouped by stage,need → stepedges, statuses fromBuildRow.- Tests: the
dag.rsfixtures and aRunReportfixture.
Step 25 — feature hit-test (M, ships in 0.3.1)
Depends on: 5.
-
API as in section 2. The plain selection path (
view.rs:221-241) is unchanged. -
Tests:
- box interior, border and outside
- an edge hit at t = 0.5 within tolerance and a miss beyond it
- at 1M, the number of candidates tested is reported as data
- GPU: hit points coincide with lit pixels
-
Proof: R4 triple.
Step 26 — feature lod (M, 0.3.1)
Depends on: 7.
- Preserve the current main CPU screen-error-based segment selection and group-0 detail uniform. Any optional packet culling is additive and explicitly configured; no second segment-count implementation.
- Tests: S increases with zoom; endpoints are exact at every S; at S = 16 the result is within 2 levels of plain.
- Benchmark: compare 1M raw overview to a fresh a367f9aa baseline on the same machine. The 23.2 ms prototype value is historical context, not the optimized baseline.
- Proof: R4 triple.
Step 27 — brokkr converted to Elm, Graph mode drawn through flow (M)
Depends on: 2, 5–9, 13.
PipelineBuilderbecomes an Elm component: a Model holding the current fields plus nav, fitted, selection andlayout_overrides; aMsgfor every existing programmatic edit plus camera.ui(&mut self)and theupdate_jsonstrings stay (lib.rs:1204-1245).- Graph mode uses core layered, flow's paint, and status Error on steps that fail validation.
- Deleted:
step_depths(:1113-1137) and thegraph_uipainter (:837-917). - Tests: existing unit, render, robot and matrix tests green; robot_demo's
modeandvalidkeys; new a11y and camera tests. - Gates: brokkr moves from BACKLOG to COVERED (backlog 51→50) with registry rows in
compliance.rsand.md.
Step 28 — brokkr editor, entirely inside brokkr (L)
Depends on: 25, 27.
Gestures:
- Ports: output port at the right middle and input port at the left middle of each visible node rectangle, independently of the label budget; drawn by brokkr's overlay, with a11y.
- Drag to connect: a straight preview line while dragging; on release,
hit()→Msg::Connect{from, to}. Connections that would create a cycle (checked with core'scyclic) or a self-loop are refused, andlast_refusalis published instate_json. - Moving a node: a ghost box while dragging (no scene rebuild per frame). On release,
Msg::MoveNodewriteslayout_overrides, which are kept in the Model andstate_jsonbut not inbrokkr.toml. - Deleting an edge: click it (
Hit::Edge) and press Delete →Msg::Disconnect. - Undo/redo: an edit stack with inverses, capped at 200, using the existing
look::keymapUndo/Redo actions (keymap.rs:22-23). - Robot seam:
update_jsonaccepts{"connect":[a,b]},{"disconnect":…},{"move":…},"undo","redo".
Why this lives in brokkr and not in flow: these are edits to brokkr's own needs model; flow only supplies generic hit-testing and screen rectangles.
Tests:
- pure model tests: connect, cycle refused, self-loop refused, disconnect, move
- undo/redo property: 1 000 seeded edit sequences restore the TOML byte for byte
- kittest pointer drag from port to node changes the TOML
brokkr_watchreplay- robot_demo connect step
Step 29 — release 0.3.1 and verification (M)
Depends on: 26, 28.
- Loki: Chrome WGSL for the
lodcompositions, GPU tests for hit-test and lod, benchmark rows. - Workspace 0.3.1 is additive; same publish order as 0.3.0.
How pull requests with Andreas work
- Each flow step is one Codeberg PR from
flow/feat-<name>, with Andreas as reviewer. - The PR body states the users of the feature and links the three proofs.
- Commits come as a red/green pair.
- The owner fast-forwards main after Andreas approves and the gates are green (the PR #1 / falsett-finish precedent).
- Component PRs only need Andreas when they expose a need in flow; that becomes a flow PR first.
4. Risks
-
Feature unification. One crate (the demo, korp) turns features on for the whole build. Choosing the plain pipeline per pane plus the "all features, plain scene" benchmark row covers this.
-
Chrome's WGSL compiler may reject a composition that naga accepts (the water.wgsl precedent). Every composition goes through the Chrome gate.
-
CPU-path regressions. Most pipeline and plandag tests, korp's frame-difference check, and no-GPU hosts all run on the CPU.
CpuStyle::Fullhandles this. -
Visible changes:
- pipeline: arcs and marching ants become flow cubics with packets
- plandag: straight arrows become curves, the orientation flips, and it gains a camera
- brokkr and brokkrview: become theme-driven unless palette overrides are passed
Embedders' screenshot tests may notice.
-
Hosts that rebuild every frame (synthetic-sdp, korp's live stream, nornir-funnel-embed). Persistent scenes prevent repeated CPU work; complete geometry keys prevent unnecessary GPU uploads without hiding position or ordering changes.
-
The a11y budget of 2 000 is a new cap; selection and editing remain available independently of the label budget. Prioritize focused and selected nodes and verify keyboard behavior.
-
Publish order and the knut
<0.3requirement — handled by steps 19 and 20. Same-version drift on crates.io: check published sources, not just version numbers. -
Andreas's availability and naming preferences. It is his component.
-
Loki is often off and has thermal limits; never compile there.
-
1M nodes on Linux is unverified. Do not claim it.
-
wasm and wheel size growth, which also forces a PyPI version bump.
5. Summary
| # | step | repo | size | depends on |
| 1 | Preparation, Andreas agreement, baseline | facett | S | — |
| 2 | facett_core::layered |
facett-core | M | 1 |
| 3 | graph, graphview, releaseview onto core | facett | M | 2 |
| 4 | Flow proof scaffolding | facett-flow, build-thing, xtask | M | 1 |
| 5 | Feature host |
facett-flow | M | 4 |
| 6 | Feature labels |
facett-flow | M | 5 |
| 7 | Feature boxes + shader variants |
facett-flow | L | 5 |
| 8 | Feature arrows |
facett-flow | S | 7 |
| 9 | Feature status |
facett-flow | M | 7 |
| 10 | Feature rate |
facett-flow | M | 7 |
| 11 | Feature layered |
facett-flow | S | 2, 4 |
| 12 | Feature compact-state |
facett-flow | S | 4 |
| 13 | plandag through flow | facett-plandag, core | M | 2, 5–9 |
| 14 | pipeline through flow | facett-pipeline | L | 2, 5–10, 13 |
| 15 | brokkrview DAG through flow | facett-brokkrview | S/M | 5, 6, 8, 9, 13 |
| 16 | Demo, facet-wrapped, wasm | facett | M | 13–15 |
| 17 | Loki: GPU, Chrome, goldens, benchmarks | facett | M | 16 |
| 18 | Docs, cards, credit | facett | S | rolling, 17 |
| 19 | knut-pipelines <0.4 |
knut | S | before 20 |
| 20 | Release 0.3.0 (owner publishes) | facett | M | 17–19 |
| 21 | Downstream bumps | knut, nornir, dwarves, korp, edda, holger, ordning | M | 20 |
| 22 | knut adapter | knut | M | 9–12 (release after 20) |
| 23 | nornir adapter + its layout copy | nornir | M | 9, 11, 12 |
| 24 | dwarves adapter | dwarves | M | 9, 11, 12 |
| 25 | Feature hit-test |
facett-flow | M | 5 |
| 26 | Feature lod |
facett-flow | M | 7 |
| 27 | brokkr Elm + Graph through flow | facett-brokkr | M | 2, 5–9, 13 |
| 28 | brokkr editor | facett-brokkr | L | 25, 27 |
| 29 | Release 0.3.1 + verification | facett | M | 26, 28 |