Repository navigation
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: a6dd7dd7e74b4328a9bec47d8a8ce344
Branch: dev/2-forward-wire-factory-pitch
Base: 96e24ef
At: 2026-11-02T12:55:38Z
What goes wrong
#2161 (merged as 96e24ef307e034f3be2706ac60a6631cc8db0e24, 12:54:38Z) made a row-crossing wire measure its corridor from the row's lowest measured bottom. In the layout the app actually produces, a row's cards can be taller than the row pitch, so that bottom is already below the next row's top: the corridor gets clamped to zero and lands inside the cards of the row below.
Concretely, with the factory pitch (plugins/workflow/src/graph.js:10 GAP_Y = 108, layoutNodes places a second row at y = 108):
| card | position | height |
|---|---|---|
a |
(0, 0) | 48 |
b |
(188, 0) | 120 |
c |
(0, 108) | 72 |
d |
(188, 108) | 72 |
Edge a → c:
- at
0bfaf525(before#2161):M 0 48 L 0 108— straight down, clean; - at
96e24ef3(now):M 0 48 L 0 120 L -96 120 L -96 108 L 0 108— the run aty = 120sits insidec(its body is 108–180), and the final approach ridesc's top border.
Same layout, edge a → d: one card crossed before (b, which is what #2161 set out to fix), three now (b, c, d). Heights of 140 and 160 behave the same; at 108 the corridor collapses onto the target's top border.
Cause: rowBottom = max(bottom of boxes whose top is within 1.5px of the source's top) is 120 here, while to.top is 108, so gap = min(WIRE_GAP, max(0, (to.top − rowBottom) / 2)) is 0 and yA = rowBottom. #2161's two tests place the second row at y = 120 / y = 400 (a 280 pitch), so they never exercise a row that starts above the previous row's tallest bottom.
Reproduction
Build the graph above with normalizeGraph / the page's own layout, then read the wire's d for a → c: it contains a horizontal run at y = 120, inside c. Same with h = 140 / 160; h = 108 puts the run on c's top border.
Expected
- A forward wire never enters or rides the border of a non-endpoint card in either row, including when the next row begins above the source row's tallest bottom.
- The
#2161case still works: with a 96–120px card beside a 48px source and a row far enough below (the 280 pitch used by its tests), the wire leaves above the neighbourhood and does not cross the neighbour. - No layout that was clean before
#2161becomes dirty: for every graph, the number of cards a wire crosses must not increase relative to0bfaf525. - The same-row (loop/skip) and back-edge geometries keep their current
dstrings verbatim (#215110 cases,#21386 cases).
Suggested fix shape
When the row's lowest bottom leaves no room for a corridor — to.top − rowBottom < 2 × WIRE_GAP — fall back to the pre-#2161 corridor (source-only, clamped to stay above to.top), so factory-pitch graphs render exactly as they did before; keep the row-measured corridor only when there is room. The guard must also make yA > yB unrepresentable.
Scope
core/src/client/workflows.js, core/test/workflows-page.test.js (one test on the factory 188/108 pitch with a 120px card, not a hand-picked 280 pitch), core/client.js + patches/ds-bot/0008-client-bundle.patch + the two patches/ds-bot/files/ mirrors via build + generate, docs/subsystems/core-client.md sentence, changelog fragment.
Duplicate check
row pitch wire (0), GAP_Y (2: this regression's own PR #2161, closed, and an unrelated panel issue #1513), "next row" wire in titles (0). No other issue covers the factory-pitch collapse; this is a follow-up to #2160, not a duplicate of it.
Activity
HugoXK commentedon Nov 2, 2026
Release
Written-by: FlyBot Lead
Released: a6dd7dd7e74b4328a9bec47d8a8ce344
Reason: merged as 7d06d01 (#2172)
At: 2026-11-02T14:16:57Z