Skip to content

feat(flow): remove dagre — SGCR is the only layout engine - #301

Draft
ivanmkc wants to merge 3 commits into
masterfrom
feat/sgcr-only-engine
Draft

feat(flow): remove dagre — SGCR is the only layout engine#301
ivanmkc wants to merge 3 commits into
masterfrom
feat/sgcr-only-engine

Conversation

@ivanmkc

@ivanmkc ivanmkc commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Remove @dagrejs/dagre dependency entirely (~14KB gzip bundle savings)
  • Replace all dagre calls with SGCR equivalents (inter-zone packing, swimlane global rank, ungrouped layout, measured re-pass)
  • Add grouped SGCR pipeline: two-level layout (per-zone → zone-packing → orthogonal edge routing) with container boxes
  • Remove bestLayout direction search (SGCR is deterministic)
  • Remove analyzeFlowGeometry heuristic lint (replaced by checkInvariants exact arithmetic)
  • Geometry lint uses sgcrReport for ALL flows (grouped and ungrouped)
  • engine:"dagre" accepted for backward compat but silently ignored

SGCR guarantees zero node overlaps (P1) and zero edge-over-node (P2) by construction — invariants dagre never provided. Verified on the 30K-graph fuzz corpus + all shipped examples.

Test plan

  • tsc clean (zero errors)
  • 703 vitest tests pass (48 files)
  • 30 SGCR tests including 8 new grouped layout tests
  • All shipped flow/panes examples produce zero SGCR invariant violations
  • Server test updated (flow-too-many-nodes replaces edge-over-node as the trigger)
  • Visual regression check on grouped boards in the viewer
  • Bundle size verification (dagre ~14KB gzip removed)

@ivanmkc
ivanmkc force-pushed the feat/sgcr-only-engine branch from 41f4c5b to 2cca2d2 Compare August 12, 2026 06:38
Replace all dagre calls with SGCR equivalents across the flow layout
pipeline: inter-zone packing, swimlane global rank, ungrouped flat layout,
and the measured re-pass. SGCR guarantees zero node overlaps (P1) and zero
edge-over-node (P2) by construction — invariants dagre never provided.

- Add grouped SGCR pipeline: two-level layout (per-zone → zone-packing →
  orthogonal edge routing) with container boxes for zone rendering
- Remove @dagrejs/dagre dependency (~14KB gzip bundle savings)
- Remove bestLayout direction search (SGCR is deterministic, no search needed)
- Remove analyzeFlowGeometry heuristic lint (replaced by checkInvariants)
- Geometry lint now uses sgcrReport for ALL flows (grouped and ungrouped)
- engine:"dagre" accepted for backward compat, silently ignored
- 703 tests pass, tsc clean
…floor

Grouped flows (tiers/lanes/zones) were skipped by the measured re-pass,
relying entirely on estSize estimates which under-predicted ChangeNode's
rendered width (estSize minW=120 vs CSS minWidth=140 + cell fill). This
caused node overlaps in the architecture-zones tiered layout.

Two fixes:
- Raise estSize minW from 120 to 172 (matches the ChangeNode's rendered
  floor when it fills its SGCR cell via width:100%)
- Enable the measured re-pass for grouped flows: re-run layoutFlow with
  DOM-measured node sizes, updating both member positions and container
  styles. Previously skipped because "dagre would ignore zones" — now
  layoutFlow handles groups natively via SGCR.
The measured re-pass calls setNodes with the recomputed positions but never
setEdges, and for grouped flows the layout hands back EXPLICIT edge polylines in
data.points. So the nodes moved to their measured-size arrangement while every
edge kept the geometry computed from estimated sizes.

The old dagre re-pass returned positions only and let React Flow derive each
path from the live node positions, which is why this never bit before: this PR
switched the re-pass to a layout that owns its edge routes without giving the
component a way to publish them. The setEdges slot was literally discarded at
the useEdgesState call.

Also carry the edge `type`, since an edge the first pass left un-routed only
draws its polyline once it is switched to the "sgcr" edge component.

This removes the ex_infra-topology regression, and ex_architecture-zones and
syn_groups_long come out clean too — but syn_swimlane then breaks in their
place, because the cross-zone router is a fixed 4-point dogleg with no obstacle
avoidance. Which board loses is incidental geometry, so this is a real fix for a
real bug but not sufficient on its own. See the review comment on the PR.
@ivanmkc
ivanmkc force-pushed the feat/sgcr-only-engine branch from a0ffa44 to 4623ad6 Compare August 12, 2026 07:01
@ivanmkc

ivanmkc commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

Review: holding this until the P2 claim matches the code

Rebased onto master (resolved the Finding union conflict against #294/#296) and pushed one real bug fix, but this shouldn't merge yet. The blocker is that the property justifying the whole change isn't implemented for the flows that need it.

The guarantee doesn't hold for grouped flows

SGCR guarantees zero node overlaps (P1) and zero edge-over-node (P2) by construction […] Verified on the 30K-graph fuzz corpus + all shipped examples.

Cross-zone edges in layoutGrouped are routed by a fixed 4-point dogleg:

// Cross-zone or fallback: simple orthogonal polyline
const my = (sy + ty) / 2;
points = [{x: sx, y: sy}, {x: sx, y: my}, {x: tx, y: my}, {x: tx, y: ty}];

There is no obstacle test anywhere in that path — nothing consults the other nodes. For grouped flows P2 isn't "guaranteed by construction", it's left to chance, and "all shipped examples produce zero violations" is not true as written: six boards violate it on this branch.

Board count hid what actually changed

25/31 vs master's 24/31 reads like an improvement. Per-board it is not:

board note
fixed ex_data-lineage, ex_k8s-topology genuinely clean now
regressed ex_infra-topology was clean on master
worse ex_architecture-zones workerworker,kafka,redis
worse ex_dataeng-etl kafkakafka,s3

Two fixed, one newly broken, two degraded. #302 landed a baseline for this guard (it had been red since the SGCR migration and was gating nothing), so CI now names the regression directly instead of drowning it in a count, and violations print in full rather than truncated to three.

One real bug fixed here — but it isn't sufficient

4623ad6 fixes something worth having independently. The measured re-pass called setNodes with recomputed positions but never setEdges, and grouped layouts return explicit edge polylines in data.points. Nodes moved to their measured-size arrangement while every edge kept its estimated-size geometry. The old dagre re-pass returned positions only and let React Flow derive paths from live node positions, so the failure mode didn't exist before this PR; the setEdges slot was literally discarded at the useEdgesState call.

That clears ex_infra-topology, and ex_architecture-zones and syn_groups_long come out clean too — but then syn_swimlane breaks instead. Which board loses is incidental geometry, which is the same point as above: without obstacle avoidance the failures move, they don't resolve.

What I'd want before merging

  1. Give the cross-zone router real obstacle avoidance, or drop the P2 claim and baseline the boards it can't route.
  2. Rewrite the summary to what's true — the current text asserts a guarantee the code doesn't implement, and that's what made the regression easy to miss.
  3. Keep engine: "dagre" working for one release rather than accepting-and-ignoring it. Silently ignoring a layout option leaves anyone hitting a bad grouped layout with no way back, which is the risk of removing the fallback while the replacement still violates its own invariant.
  4. The two unchecked boxes in the test plan (visual regression on grouped boards, bundle size) are exactly the two that would have caught this.

One retraction

While tracing this I thought every grouped edge was rendering in the wrong coordinate space — node and edge screen coordinates don't line up in a direct DOM measurement. That reproduces identically on master, so it's an artifact of how I was measuring, not a regression here. Disregard; the overlap guard's own numbers above are the reliable signal.


Reviewed by CloudCode — session ses_00b81f4a5ffe8Nab7Wg7Ca2r2J.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants