Files
ARCADIA-Studio/harness/release-routing-contract.md

12 KiB

Editable connection routing — first slice, 2026-09-05

Status: the per-diagram route-list contract is implemented in core, source/API, shared geometry and desktop interaction. Core/source/router checkpoints are committed; desktop integration passed code and native acceptance at 23535b4. Native OA/SA routing, save/reopen, SVG and default/minimum window trials passed. 65fa9ab resolves the six reproduced default SA label overlap pairs, with automated, independent and native follow-up. Final native UI review found no material unresolved finding; see release-routing-verification.md for exact evidence, the 119 ms dense settled-solve limitation and release boundaries. State gesture design is preserved separately in release-state-gesture-design.md.

Problem, scope and chosen representation

Before this slice, architecture canvas, hit testing and SVG independently regenerated a fixed cubic between endpoints without stored edge geometry. Fit and SVG bounds included only nodes. This prevented authored detours and could conceal lines under cards and headers.

Use routing metadata keyed logically by (diagram, existing relation/trace ID). One semantic edge is projected at most once per current diagram; the same edge can have independent routing in different diagrams. A separately identified EdgePlacement would add aliases, identity lifecycle and source declarations without a current multiple-representation requirement. Reject desktop-only coordinates because they lose text/API/save parity. A literal UUID-keyed map needs a source reference-key grammar or special serializer; use a unique-keyed list that fits existing typed records instead.

// All types derive Debug, Clone, PartialEq, Serialize, Deserialize.
// Records deny unknown fields; missing optional/default fields are accepted.
pub struct EdgeLabel {
    pub fraction: f32,       // [0, 1] of routed polyline arc length
    pub offset: Position,   // diagram-space displacement from that point
}
pub struct EdgeRoute {
    pub edge: Id,           // existing Relation or Trace, never a placement UUID
    pub waypoints: Vec<Position>, // default []; pinned points in traversal order
    pub label: Option<EdgeLabel>, // default None = automatic label placement
}
// Diagram gains: #[serde(default)] pub routes: Vec<EdgeRoute>;
Command::SetEdgeRoute { diagram: Id, route: EdgeRoute }
Command::RemoveEdgeRoute { diagram: Id, edge: Id }

Set upserts by edge, preserving unrelated route entries/order; Remove is idempotent for an existing diagram/edge. Neither creates/reidentifies a semantic object. An empty route with no label is equivalent to the automatic default; UI uses Remove for Reset routing. Preserve explicit empty input during roundtrip. List order has no routing meaning. Store only pinned points, not computed elbows, endpoints, or generated label coordinates. Route edits remain one history entry.

routes [
    { edge @report_exchange;
      waypoints [ { x 420; y 260; }, { x 690; y 260; } ];
      label { fraction 0.55; offset { x 0; y -16; } };
    }
];

This is part of the ordinary diagram declaration. Add edge to language and graphical reference catalogs; route points/label use existing typed numbers and lists. Keep the diagram in its existing views source file. No second source grammar or semantic relationship is introduced. Full Project JSON carries the same list; existing POST /api/v1/projects/{id}/transactions accepts the two new Command variants with existing conditional ETag behavior. Update OpenAPI and normative reST. No new REST route is necessary.

Shared deterministic projection and default router

Add a pure Rust core routing module, separate from semantic validation. It takes Project, Diagram and the already projected nodes/ports/edges, returning for each edge its full polyline, label anchor, effective pinned points and routing warnings. Recommended seam: route_diagram(p, d, view) -> RoutedView, with RoutedEdge keyed by edge ID and carrying points: Vec<Position>, label: EdgeLabelGeometry and warnings: Vec<RouteWarning>. Return total bounds including paths/labels. Desktop draw, edge/label hit tests, drag preview, Fit and SVG consume this result. Delete the duplicate cubic and midpoint calculations after parity passes.

Default routing is orthogonal. Authored waypoints are hard points visited in order; the router may add intermediate orthogonal elbows. Generated elbows are not silently persisted. UI distinguishes pinned handles from computed elbows; dragging an automatic segment/elbow pins the chosen point. Removing a pinned point reroutes the adjacent spans; Reset removes all pins. Segment crossing and shared runs are soft costs, node/header intersections are forbidden when a valid route exists. Do not claim a guarantee of zero crossings for arbitrary graphs.

Use a bounded rectilinear visibility/grid search over endpoint stubs, obstacle boundaries and pins, with Manhattan length plus bend penalty. Deterministic tie breaking uses coordinates/direction and stable edge ID order, never random/hash iteration. First try direct and one-/two-bend clear paths; search only as needed. Reuse obstacle preparation within one routed view. Bound search work and report a visible fallback warning instead of hanging or silently drawing through a card. Cache by diagram/model revision and transient preview revision in the desktop; pan/zoom alone does not change world geometry. Measure before adding a routing framework or dependency. Single-edge previews retain their settled per-edge search budget and preserve unrelated routes. A label-only edit preserves the settled polyline and routing warnings; these invariants are implemented and regression-tested at 30c7db1.

Endpoints, hierarchy and obstacles

Actual projected ports are hard endpoints. Use PortAnchorSide's outward normal for a short orthogonal lead-out/lead-in (initial constants: 18 diagram units, 12 units clearance); a target is approached from outside along that normal. Never move, invent or switch a function/component port to simplify routing. Arrowheads use the final nonzero segment and existing oriented flag; Standard unoriented exchanges stay unoriented. For non-port edges choose a clear side of the projected owner rectangle, evaluating alternate sides if the cheapest is blocked. Geometry never changes semantic direction or endpoint references.

Visible leaf cards and unrelated container interiors are obstacles. Ancestors of either endpoint are traversable enclosures, but their header rectangles remain obstacles; their children remain obstacles independently. The endpoint owner can be crossed only by its own lead segment. A functional exchange may cross an ancestor boundary without inventing a component port/allocation. Preserve the current distinction between semantic containment and allocation projection.

If collapsing an ancestor promotes either endpoint to another visible node, temporarily route that edge automatically. Preserve its complete stored route and show that custom routing resumes on expansion; disable bend editing for this promoted representation. This avoids old interior pins dragging a folded edge through invisible child coordinates. Do not expose fictitious ports on the collapsed container. Unrelated folding may reroute around the new obstacle but does not suspend otherwise unchanged endpoint routes. An edge hidden because both endpoints fold into one anchor retains its stored metadata.

Movement, lifecycle and validation

Waypoints are absolute diagram coordinates. Moving one endpoint leaves pins in place and reconnects the endpoint span automatically. When MoveNode moves a group and both actual endpoint-owner placements belong to that group, translate all its stored pins by the same delta, including hidden descendants. Label offset stays a relative vector; arc-length anchoring moves the label with the route. Use endpoint-owner placement lookup before visibility promotion, not the visible collapsed anchor, to decide group membership. Other diagrams remain unchanged.

One shared pure placement-delta helper supplies this calculation to command movement and desktop preview. Reflow currently emits multiple EditNode commands (diagram.rs:30); append translated-route updates for edges whose endpoints move by the same delta. Raw EditNode/text coordinate edits remain exact record edits and do not implicitly move unrelated route points. Layout preserves pins and translates them where both actual endpoint placements share a movement delta; Reset routing is an explicit separate action instead of erasing authored work. Reconnect/port-contract edits preserve stored pins but compute fresh endpoints; blocked pinned geometry is reported as a recoverable layout warning.

Deleting a relation/trace removes its routes from every diagram in the same transaction. Reidentify remaps EdgeRoute.edge without changing coordinates. Deleting a diagram naturally removes its route list. Hiding/relocating a node, changing projection or folding does not delete route metadata; it resumes if the same semantic edge is shown again. Source/API may store routes for currently hidden edges, so visibility is not a validity requirement.

Structural validation rejects duplicate edge entries, references to nonexistent or non-edge objects, nonfinite/out-of-range coordinates, invalid label fraction, and excessive point counts. Use existing MAX_COORDINATE. Adopt 128 pinned points per edge for this slice; count route records in admission totals and retain existing aggregate payload/source limits. A point inside an obstacle or an impossible path is a located layout warning, not semantic corruption or permission to erase the point. Route defaults/missing fields must be the same in UI, source, JSON and REST. V1/V2 historical readers/goldens stay frozen; migration produces empty routes. Existing schema3 files without routes load with an empty list; this is an additive extension of the unreleased schema3 model.

UI slice, partition and acceptance

Select a connection to reveal route handles; double-click a segment or use a visible Add bend action, drag a handle, select a handle and Remove bend, or Reset routing. Drag the label separately. Endpoint handles are not bend handles and cannot detach ports. Use fixed screen-space hit radii (approximately 8 px divided by zoom), explicit nearest-hit precedence and segment distances from the actual polyline. Node/label/handle hit ordering must work inside nested enclosures. Capture the initiating diagram/revision; Escape cancels; release emits one command; stale gestures or invalid source/property drafts cannot mutate a different model. Use the release cursor as the final gesture position even if no final motion event was delivered; short/coalesced native drags must commit their displacement.

Core schema/lifecycle owns model.rs, session.rs, references.rs, validation.rs and focused tests. Independent router owns new routing.rs plus geometry tests after seam freeze. Source/API worker owns language/reST/OpenAPI and round-trip/HTTP tests after schema freeze; coordinate with current source-project work. Lead owns diagram.rs, app.rs, views/forms, actual CUA and integration. Core/source/router workers have released their implementation ownership; the lead owns final native integration. Continue partitioning any follow-up edits to avoid concurrent writers.

Automated gates: independent routes for one edge in two views; add/move/remove and one-step exact undo; deletion/reidentify; malformed/duplicate/huge inputs; V1/V2/schema3-default loading; source/JSON/HTTP equivalence; obstacle clearance, port normals, nested header avoidance, deterministic routing, promoted endpoints, group movement and bounds. Test actual pointer gestures at 50%, 100% and 150%. Test SVG polyline/arrow/label coordinates against shared routing output.

Native gate: a nested SA architecture with function and Standard/Flow component ports, three participants and enough exchanges to force detours. Add/move/remove pins, drag a label, move one endpoint and a whole container, fold/unfold, undo, save/reopen and export SVG at 1440x900 and 1100x700. Inspect crossings, headers, labels, handles and typed port attachment. Measure interactive latency on a 100-node/150-edge fixture before claiming smoothness; initial target is below 100 ms for settled routing and no multi-second stalls during drag. Record actual times/results, not forecasts. Code/native acceptance and exact performed scopes are recorded in release-routing-verification.md. The measured release full solve is 119 ms, so the initial settled target is not claimed as passed. Current Linux verification and broader release acceptance remain separate from the completed routing workflows.