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

207 lines
12 KiB
Markdown

# 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.
```rust
// 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.
```text
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.