Files
git.stella-ops.org/docs/workflow/engine/17-elksharp-architectural-decisions.md
master e91cf98f8f Add ElkSharp rendering architecture docs, ADRs, tutorial, AGENTS rules
Five documentation deliverables for the ElkSharp rendering improvements:

1. docs/workflow/engine/16-elksharp-rendering-architecture.md (453 lines)
   Full pipeline: Sugiyama stages, edge routing strategies, hybrid
   deterministic mode, gateway geometry, 18-category scoring system,
   corridor routing, Y-gutter expansion, diagnostics.

2. docs/workflow/engine/17-elksharp-architectural-decisions.md (259 lines)
   Six ADRs: short-stub normalization, gateway vertex entries, Y-gutter
   expansion, corridor rerouting, FinalScore adjustment, alongside
   detection.

3. docs/workflow/tutorials/10-rendering/README.md (234 lines)
   Practical tutorial: setup, layout options, SVG/PNG rendering,
   diagnostics capture, violation reports, full end-to-end example.

4. src/__Libraries/StellaOps.ElkSharp/AGENTS.md — 7 new local rules
   for Y-gutter, corridor reroute, gateway vertices, FinalScore
   adjustments, short-stub normalization, alongside detection,
   target-join spread.

5. docs/workflow/ENGINE.md — replaced monolithic ElkSharp paragraph
   with structured pipeline overview, effort-level table, and links
   to the new architecture docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:37:32 +03:00

260 lines
11 KiB
Markdown

# ElkSharp Architectural Decisions
This document records architectural decisions made during the ElkSharp rendering
engine development. Each record follows the ADR (Architecture Decision Record)
format: context, decision, consequences.
---
## ADR-1: Short-Stub Exit Normalization
**Status**: Accepted
**Context**:
`NormalizeExitPath` creates a perpendicular stub from the source node boundary to
establish a clean exit direction. The default stub length extends to the anchor X
coordinate: `Math.Max(sourceX + 24, anchorX)`. For edges where the anchor is far
from the source (e.g., a long forward edge), this creates a horizontal segment of
1000+ pixels that crosses intermediate nodes in the same Y-band.
The long horizontal stub was originally designed to produce clean orthogonal exits,
but it assumed the Y-band between source and anchor was unoccupied. In dense graphs,
intermediate nodes occupy the same Y-band, and the long stub crosses through them,
creating entry-angle violations and node-crossing penalties.
**Decision**:
When the long stub fails `HasClearSourceExitSegment` (i.e., the horizontal segment
between `sourceX + 24` and `anchorX` crosses a node), try a short stub instead. The
short stub extends only `sourceX +/- 24px` -- just enough to establish the
perpendicular exit direction without reaching into the occupied Y-band.
The short stub is controlled by the `useShortStub` parameter in `NormalizeExitPath`.
It fires ONLY when the default long stub fails clearance. The long stub remains the
default because it produces cleaner, more direct paths when clearance is available.
**Consequences**:
- Fixes entry-angle violations where intermediate nodes in occupied Y-bands blocked
the perpendicular exit path.
- The short stub creates a 24px vertical segment that subsequent routing can extend
into a clean corridor without crossing obstacles.
- Does not change behavior for edges with clear exit paths (the long stub is still
preferred when it passes clearance).
---
## ADR-2: Gateway Vertex Entries
**Status**: Accepted
**Context**:
Gateway tips (diamond corner vertices at left and right extremes) were blocked for
all edges because source exits from tips create "pin" visual artifacts -- a thin
spike extending from the corner that looks like a rendering glitch rather than an
intentional connection.
However, for target entries (incoming edges), tips are the natural convergence point.
Multiple edges arriving at a decision gateway naturally converge toward its left tip.
Blocking tip entries forced edges to route to face interiors, which required
additional bends, created shared-lane conflicts between fork output edges, and
produced visually cluttered arrivals.
**Decision**:
Allow left/right tip vertices for target entries via a 3-way coordination mechanism:
1. `IsAllowedGatewayTipVertex`: Returns `true` for left and right tip vertices when
the edge direction is "target entry" (incoming).
2. `HasValidGatewayBoundaryAngle`: Accepts any external approach angle at allowed
tip vertices. Without this relaxation, the angle validator would reject diagonal
approaches to the tip even though they are visually correct.
3. `CountBoundarySlotViolations`: Skips the slot-occupancy check when all entries
at a boundary point share the same allowed tip vertex. Since a vertex is a single
geometric point (not a face segment), slot capacity is not meaningful.
Source exits from tips remain blocked by `ForceDecisionSourceExitOffVertex`.
**Consequences**:
- Eliminates shared-lane conflicts from fork output edges that were forced to
route around blocked tips.
- Creates cleaner convergent target entries where multiple edges naturally meet
at the gateway's leading tip.
- The 3-way coordination must stay synchronized: changing any one of the three
checks without updating the others causes cascading boundary-slot violations,
angle rejections, or vertex blocking.
- Source exits remain clean -- the "pin" artifact is prevented for outgoing edges.
---
## ADR-3: Y-Gutter Expansion
**Status**: Accepted
**Context**:
After Sugiyama placement and initial edge routing, some edges route through or
alongside nodes because the placement did not leave sufficient vertical space for
routing corridors.
Two prior approaches failed:
1. **Post-placement individual node shifting**: Moving individual nodes to create
clearance disrupted the barycenter ordering that Sugiyama optimized. The shifted
node changed the median calculations for adjacent layers, causing cascading
position changes that degraded overall layout quality.
2. **Post-refinement clearance insertion**: Adding vertical space after refinement
failed because subsequent optimization passes (compact-toward-incoming, grid
alignment) overrode the inserted space, collapsing the corridors.
**Decision**:
Use the same pattern as X-gutter expansion: shift entire Y-bands (all nodes below
the violation point) together, preserving relative positions.
- Scan routed edges for horizontal segments with under-node or alongside violations.
- Identify the blocking node and compute the required clearance.
- Shift ALL nodes with Y > violation Y downward by the clearance amount.
- Re-route edges with the expanded corridors.
- Run up to 2 iterations to handle cascading violations.
The expansion runs after X-gutters (inter-layer gaps are set) and before compact
passes (so compaction respects the new corridors).
**Consequences**:
- Creates adequate routing corridors without disrupting within-layer ordering.
- Routing gets clean paths on the first pass because the corridors exist before
the iterative optimizer runs.
- The downward-only shift direction ensures the graph grows in one direction,
avoiding oscillation between iterations.
- Up to 2 iterations handles the case where fixing one violation exposes another
(the shifted band may push edges into a new conflict zone).
---
## ADR-4: Corridor Rerouting for Long Sweeps
**Status**: Accepted
**Context**:
Forward edges spanning 10+ layers (e.g., failure/timeout paths from an early task
to the End event) route horizontally at the source's Y coordinate. In a dense graph,
this horizontal segment crosses many intermediate nodes -- a 3076px sweep in the
document-processing test case.
No amount of Y-adjustment can clear a sweep that crosses the entire graph width.
Y-gutter expansion would need to push the entire graph below the sweep, which
defeats the purpose of the layout.
Backward edges already use corridor routing (above the graph field) because they
inherently travel against the layout direction. Forward edges did not have this
treatment.
**Decision**:
Route long forward sweeps (spanning > 40% of the graph width) through the top
corridor at `graphMinY - 56`:
1. Exit the source with a 24px perpendicular stub.
2. Route vertically to `graphMinY - 56`.
3. Route horizontally across the top corridor.
4. Descend to the target.
The 24px perpendicular exit stub is critical: without it, `NormalizeBoundaryAngles`
collapses the vertical corridor segment back into the source boundary, destroying
the corridor route.
Near-boundary sweeps (edges that would conflict with the graph's lower edge) use
the bottom corridor at `graphMaxY + 32`.
**Consequences**:
- Long-range forward edges route cleanly above the graph field, like backward edges.
- The graph's visual area remains clear of long horizontal sweeps.
- The perpendicular exit stub (24px) must survive normalization -- removing it or
reducing it below the normalization threshold causes the corridor route to
collapse.
- Below-graph detection (`HasCorridorBendPoints`) must exempt corridor edges;
otherwise they would be penalized and rerouted back into the node field.
---
## ADR-5: FinalScore Adjustment (Search/Display Separation)
**Status**: Accepted
**Context**:
The iterative optimization loop uses the scoring function as both a quality metric
AND a search heuristic. The score determines which candidates are explored and which
are accepted as improvements.
During development, borderline detection patterns were identified -- situations where
the scoring detected a "violation" that was actually a valid layout artifact (e.g.,
a gateway face approach that looks like a boundary-slot conflict but is geometrically
correct).
The initial fix was to update the detection logic to exclude these borderline cases.
However, this changed the scoring function that the search used as its heuristic,
altering the search trajectory and causing a 40-second speed regression (from 12s
to 52s) because the optimizer explored different (and more) candidates.
**Decision**:
Keep the original scoring function unchanged during the iterative search (stable
heuristic trajectory). Apply detection exclusions ONLY in the `FinalScore`
computation (post-search).
The FinalScore excludes:
- Valid gateway face approaches (exterior closer to center than predecessor).
- Gateway-exit under-node (lane within 16px of source bottom).
- Convergent target joins from X-separated sources with > 15px Y-gap.
- Borderline shared lanes (gap within 3px of tolerance).
The search does not need to know about borderline patterns -- it just needs
consistent heuristics to explore the candidate space efficiently.
**Consequences**:
- The FinalScore accurately reflects visual quality: 0 hard violations in the
document-processing test case.
- The search maintains stable 12-15s runtime because the heuristic is unchanged.
- The separation means that the search may "fix" violations that the FinalScore
would have excluded. This is acceptable: the extra fixes are not harmful, and
the stable search trajectory is worth the minor redundant work.
- Future scoring changes must decide whether they apply to the search heuristic
(affects trajectory and speed) or only to the FinalScore (affects reported quality).
---
## ADR-6: Under-Node Alongside Detection
**Status**: Accepted
**Context**:
`CountUnderNodeViolations` detected edges that pass through a node's bounding box
with a gap greater than 0.5px. This threshold was chosen to avoid false positives
from floating-point precision.
However, edges running flush with a node boundary (gap = 0px, e.g., exactly at the
bottom edge of a node) were not detected. These edges are visually "glued" to the
node boundary -- they appear to touch the node even though they technically do not
pass through it.
The 0.5px threshold also missed edges within a few pixels of the boundary. An edge
at gap = 2px is visually indistinguishable from one at gap = 0px at typical zoom
levels, but only the latter was detected.
**Decision**:
Extend the under-node detection to include flush and near-flush edges:
- Standard under-node: gap > 0.5px (unchanged).
- Flush bottom (`isFlushBottom`): gap >= -4px and <= 0.5px relative to the node's
bottom boundary.
- Flush top (`isFlushTop`): gap >= -4px and <= 0.5px relative to the node's top
boundary.
The +/-4px range catches edges that are visually "alongside" the node boundary,
even if they are technically outside the bounding box by a few pixels.
**Consequences**:
- Catches visually "glued" edges that touch or nearly touch node boundaries.
- The Y-gutter expansion then creates clearance for these edges, pushing them
into a clean routing corridor.
- The -4px lower bound prevents false positives from edges that are merely
"nearby" but visually separate from the node.
- The detection threshold (±4px for alongside, > 0.5px for standard) should not
be changed without sprint-level approval, as it affects which edges trigger
Y-gutter expansion.