Files
ARCADIA-Studio/harness/experiments/auto-layout-20260925/routing-experiment-plan.md
T

11 KiB

#32 next routing experiment: distinguish shared terminals from ambiguous trunks

Local source/receipt diagnosis only; no builds, formatters, GUI or product edits. Input: completed e7deea70 probe .local/auto-layout-checks-20260925/01-probe/command/output.log; implementation read from .local/auto-layout-20260925/crates/oa-core/src/routing.rs. The probe uses core estimated label bounds, not renderer glyph measurement. No new candidate module is justified until the required private routing hook is agreed.

Exact observed geometry

The layered diamond has 82 units of pairwise shared length: 36 classified as common terminal stubs, 46 nonterminal. Edges ending 009 and 00c share the physical output (264,106), its 18-unit stub to (282,106), then an avoidable extra horizontal run to (328,106). Edge 00c then turns down to (328,286) and enters (346,286). A candidate branching immediately at (282,106), going to (282,286), then right to the target would separate that extra 46-unit trunk while preserving the port and stub. This is a geometric proposal, not a verified collision-free final result; the same obstacle and label checks must evaluate it.

The layered crossed-bipartite case has 344 shared units: 72 common-terminal, 272 nonterminal. Its opposite diagonal routes both use x=328, y=106..286, an exact 180-unit coincident vertical run. Top and bottom horizontal portions add 92 nonterminal shared units. The source stubs are at x=282; target stubs at x=328. Proper crossings are reported as zero, but these coincident routes do not provide unambiguous independent traces. Do not treat the zero as a solved crossing problem.

Source evidence: a local scoring asymmetry precedes penalty tuning

crossing_cost charges min(shared_length * 0.12, 80) per previously occupied segment, multiplied by segment count. A proper interior crossing costs24. BEND_COST is24. RoutingSpace::new uses obstacle boundary coordinates; fast-path candidates add the start/end midpoint, and grid coordinates add start/end. Neither systematically adds lanes offset from previously occupied trunks.

More importantly, fast_path(start, end, space, previous, incoming) includes the turn from the preceding source-side segment, but has no outgoing direction parameter. route_edge chooses this local path and only later appends the final target attachment point b.point; complete path_cost is then compared across endpoint choices, not across the discarded alternative paths for that same endpoint pair. With fixed semantic ports there may be no alternate endpoint choice to recover the discarded route. grid_path is only a fallback when fast_path returns None, not a better-cost search after a valid fast path.

For the diamond, horizontal-then-vertical has one internal bend and no incoming bend; its 46-unit shared portion costs5.52. Vertical-then-horizontal has one internal bend plus an incoming bend, costing24 extra locally. But appending the target segment adds the missing final bend to horizontal-then-vertical. Complete paths have equal bend counts; the local comparison prefers the shared route because it omitted one terminal join. The 180-unit bipartite overlap costs21.6, also below one turn. This explains a plausible bias, not proof that correcting it resolves every route.

Minimal discriminating comparison (proposal to parent)

Freeze layered node positions, labels' text, attachments, port identities/anchors, edge order and work budgets. Compare a small fixed sequence, not an open parameter search:

  1. J: symmetric terminal-join cost. Supply an optional outgoing point/direction to fast-path scoring. For the final segment to b.stub, use b.point; include turn_cost(penultimate, end, outgoing) both in actual candidate score and admissible pruning estimates where applicable. Intermediate authored pins have no predetermined outgoing direction; do not invent one. If grid routing supports this experiment too, target state selection must account for the terminal turn before returning the best result. Preserve baseline behavior for absent outgoing context. This isolates the suspected bias without stronger penalties or enlarged diagrams.
  2. JP: J plus stronger shared-run penalty. One declared test coefficient, e.g.1.0 per unit with the existing80 cap, compared with0.12. Keep the proper crossing cost24 unchanged so the comparison exposes whether penalizing overlap merely substitutes crossings. Do not choose this variant solely by shared length. Preserve all hard endpoint constraints.
  3. JPL: JP plus bounded lane candidates. For occupied horizontal/vertical trunks, offer parallel offsets of CLEARANCE on either side where applicable; validate obstacle clearance and fixed terminal stubs. Use a strict deterministic global cap on added coordinates/candidates and reuse existing work exhaustion handling. This tests whether absent geometric alternatives, rather than insufficient penalty, is the blocker. No persisted waypoint authoring and no unconstrained port movement.
  4. Spacing control. Only if J/JP fail, compare the unchanged router with one fixed additional inter-layer gap (e.g.24 world units). This is a control, not the assumed remedy: larger gaps may lengthen shared trunks and enlarge diagrams while retaining identical topological ambiguity. Do not mix spacing and router changes in the first comparison.

The first J variant is the smallest informative next experiment. If it removes diamond's extra trunk while retaining invariants, stop and inspect results before implementing JP/JPL. A clean shared terminal fan-out need not eliminate its unavoidable initial18-unit stub. Conversely, sharing a node identity does not exempt arbitrary later overlap, and different physical ports on the same node must not be collapsed into one endpoint exemption.

Required hook and ownership boundary

The candidate module can call public route_diagram, but cannot change private crossing_cost, fast_path, grid_path or candidate generation. Returning a changed Project with temporary authored waypoints would test a different policy and would violate the generated-elbows-remain-derived experiment. Copying the entire router into an example introduces unnecessary divergence.

Therefore propose a parent-owned isolated routing experiment hook, with no default production behavior change, or a separate disposable source variant controlled by the existing runner. Evaluator needs an optional routing provider returning RoutedView for a fixed Project, rather than only candidate -> Project. It must reuse that provider for every metric, capture, and repeat check and label the variant explicitly. Until parent approves that hook, no module/interface changes were made. The current original/reset/layered comparisons remain reproducible.

Falsifiers and risks

Evaluate both raw and terminal-excluded shared length, proper crossings, T/bend contacts, route lengths/turns, unrelated-node/header penetration, label collisions, warnings and runtime/work counts. A positive overlap moved onto a bend is not a successful reduction in intersections. The crossed K2,2 fixture with fixed port ordering may require a genuine visible crossing or an outer detour; do not demand zero overlap AND zero crossings without a fixed-attachment feasible witness. Separate forced terminal contacts from independently traceable route interiors.

Keep diamond, crossed-bipartite, chain and long-label fixtures; add a fixed shared-output fan-out, two distinct ports on one node, opposite-direction connections, and a narrow obstacle corridor. Verify exact endpoint coordinates and metadata; preserve ordinary authored pins and preview/settled agreement if the routing change advances beyond this isolated experiment. Stable-ID order can privilege earlier routes, so rerun the same graph under a fixed edge-ID permutation and disclose sensitivity instead of relabeling edges to improve the score.

Labels are placed after paths settle; lane changes can worsen label collisions even when node positions stay fixed. Parent's eventual GUI inspection should trace each diagonal from its source label/port through the corridor to its intended target at normal zoom. No geometry metric alone establishes that user outcome. This note selects a falsifiable experiment, not an accepted product fix.

Exact J patch plan, held for parent's separate candidate pin

No edits to routing.rs are authorized yet. After parent publishes the first experiment evidence, the narrowly proposed change is:

  • Add outgoing: Option<Position> beside incoming in private fast_path and grid_path only. At route_edge's loop over authored pins followed by b.stub, pass Some(b.point) only when index == pins.len(); pass None for every intermediate waypoint. The preceding settled path still supplies incoming exactly as now. No public API or metadata change.
  • In fast_path, extend its shared join(path) closure with the terminal turn from path[path.len()-2] through end to outgoing, when two path points exist. Both estimate(path) and actual scoring already call join; change both consistently through that closure. Keep the two-bend lower-bound early exit: Manhattan distance plus two bend costs remains a lower bound for two-bend alternatives because terminal penalties are nonnegative. Keep final whole-path path_cost unchanged; local scoring does not persist a cost, so it does not double-charge the returned path.
  • In grid_path, add the terminal turn during relaxation when next == last, using turn_cost(a, b, outgoing). Include it in costs[state] and the queue priority before the target state can be popped. Do not merely add a penalty after returning the first reached target: that would fail to compare alternative arrival directions. The existing Manhattan heuristic remains admissible because the new terminal penalty is nonnegative. The initial incoming-turn logic remains unchanged.
  • start == end has no locally selectable segment. Keep the degenerate result, and rely on whole-path scoring for the junction between preceding/following points. Do not fabricate a direction from an absent penultimate point. Waypoint positions/order remain exact; this patch does not claim globally optimal look-ahead across several hard-pin legs.

Focused tests before comparing full fixtures: (1) fast-path diamond geometry with one occupied horizontal trunk, fixed start/target stubs, asserting the equal-complete-bend alternative branches earlier; (2) directly exercise grid routing with two clear arrivals where adding the target join changes the preferred total, compare complete cost to an enumerated feasible alternative; (3) mirrored/rotated terminal geometry, including straight continuation and reversal costs; (4) outgoing=None preserves established intermediate-waypoint behavior; (5) full route with one/multiple authored pins retains exact pins and terminal attachments, plus start=end leg; (6) preview versus settled geometry remains consistent under the same baseline/budget. Use private unit tests for fast/grid so a passing public route cannot accidentally avoid the grid test. Then rerun the unchanged probe corpus and report the complete quality vector, especially any newly visible crossings. No expected overall improvement is asserted before execution.