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: c3195c5338ed4be889da8f36b469548b
Branch: dev/4-394-loop-wire
Base: ddb4930
At: 2026-11-02T09:21:42Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
What the user sees
A team adds a loop edge to the workflow graph — the sidebar's Add link offers kind=Loop, and a loop back into a parallel layer (say, a failed deploy returning to Review) is the normal use. The loop is drawn, but its return run passes behind the card of the sibling node: the visible segment looks like the loop goes to the wrong node, and only a short stub with an arrowhead shows at the right edge.
Reproduction
Repository factory graph, no hand-written coordinates:
const graph = normalizeGraph(deliveryWorkflow());
graph.edges.push({ from: 'deployer', to: 'reviewer', loop: true, max: 3 });
wireRoutes(graph.nodes, graph.edges);ranksOf skips loop edges, so reviewer stays on the same rank as tester (world positions: planner(0,0) writer(0,108) coder(0,216) reviewer(-94,324) tester(94,324) deployer(0,432); reviewer box [-178,-10], tester box [10,178], same top 324).
The back-edge route comes out as M 84 468 L 192 468 L 192 360 L -10 360: its horizontal run at y = 360 crosses the tester card box [10,178]×[324,396], hitting at [176,360]. The reverse loop { from: 'reviewer', to: 'coder', loop: true } crosses on its departure run at [12,360] at the default card height ([12,348] at the measured-48 one).
This holds at every card height (authored 72 → y 360; measured ~48 → 348; test 96 → 372) and at every width: fitLayout scales the whole world together (reach 202; inner width 600 → scale 1, 300 → scale 0.743), so the hit is preserved.
Root cause
core/src/client/workflows.js — wireRoutes handles forward edges (to.top >= from.top) with a lane search and the straight guard that consults cutsCard(boxes, x1, y1, x2, y2, from, to). The back-edge branch (to.top < from.top) emits M x1 y1 L lane y1 L lane y2 L x2 y2 with no card check at all, so a horizontal run through a sibling on either rank is never detected.
#2126 fixed the forward-side crossing and its fixture offsets the two same-rank cards by 51px (claim(-94,108) vs forensics(94,159)), so the same-rank case never ran — the rule missed back edges rather than the implementation lagging the rule.
Expected behaviour
A back edge must not pass through a non-endpoint card, the same rule forward edges already obey. Either:
- turn each horizontal run at its own card's right edge, step vertically to just outside the rank band (top − 14 / bottom + 14), and cross ranks there; or
- apply the seam rule
#1662/#1663use for a wire into a wrapped flow layer, if it fits this geometry better.
Scope (XS)
core/src/client/workflows.js: the back-edge branch only.core/test/workflows-page.test.js: two cases —deliveryWorkflow()plus the deployer→reviewer loop, and the reviewer→coder loop — each asserting that sample points along the drawn path fall in no non-endpoint card box (not pixel comparison). Both red on baseddb4930e, green after. The file bundlesworkflows.jsthrough an esbuildstdinentry, so the factory graph is reachable by addingdeliveryWorkflowfromplugins/workflow/src/template.jsto that entry (or by reproducing the same six-node shape explicitly if a cross-package import is unwanted — the factory graph is preferred, since it is what the Product reproduction uses).- Mirror the source change into
patches/ds-bot/and re-rungenerate/checkas usual. changelog.d/<slug>.fixed.md(user-visible rendering fix).
Dedupe (checked before opening)
Open issues: no same topic (back edge sibling card — five hits, all closed and different surfaces; #1662/#1663 are the same class of crossing on the wrapped-layer path, #2125/#2126 are the forward-edge routing change this one regresses). Held backlog: no "back edge crossing" entry.
Activity
Route a loop wire through the gap beside its row (ThinkFlowLab#2138)
A wire in the run flowchart can be neither pointed at nor read (Think…
Route a loop wire through the gap beside its row (ThinkFlowLab#2138)