A pair walked in both directions is a 2-cycle in the edge set; the
longest-path relaxation loops through it and inflates every downstream
depth toward the node-count cap, dragging other relays hard against the
ends (a one-hop relay landed at x=0.09 instead of its natural 0.33).
Rank balanced_x over the graph with each such pair merged to one
representative — the acyclic flow the picture really is; the pair, drawn
as a single vertical, rightly shares an x anyway.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A relay a balanced rank pins near a canvas edge centred its long label
off-canvas, placed nothing, and — unlike an endpoint — had no inward-slide
or side-placement fallback, so its marker went silently unlabelled. Give
every node the endpoint's inward-anchor clamp and marker_label fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every node now sits on a level platform: an edge leaves level, shifts
once, arrives level, so an off-lane relay reads as a flat-topped
trapezium instead of a pointed peak or valley. The shift itself is a
cubic-bezier S-curve with horizontal tangents at both ends, easing out
of one platform and settling into the next with no corner anywhere.
Every edge is keyed by its unordered node pair and drawn exactly once,
killing the doubled/offset lines that came from a pair walked in both
directions. That bidirectional pair is the sole edge drawn as a bare
straight segment (near-vertical when the layout stacks the markers) —
the one place an up-and-down line is the honest picture.
A direction arrow embeds in the trunk just before us, so the whole
graph reads as a directed run node → us.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The contact page's route graph fanned every path as a free lane from the two
endpoints and averaged shared relays together, so a well-connected node drew as
an acute tangle of near-overlapping lines. Worse, the same physical node could
appear twice: a 1-byte trace hop (27) standing beside its own wide contact id
(27d4396a2967) whenever the short hash was too ambiguous to widen — YUL-Poly
drew YUL-UpperSalaberry as two separate markers.
Redraw the route graph as a layered left-to-right graph (pathgraph.py):
* longest-path ranks put every node in the column of its hop distance, so each
edge steps strictly forward and a relay shared by several routes is drawn once;
* an edge that skips a column routes through invisible waypoints, bending around
the nodes between its ends instead of slicing through them;
* nodes are ordered to cut crossings (barycentre sweeps) then spread down the
full height with their ys pulled toward their neighbours' (PAVA packing), which
straightens each route into a lane and opens the branch angles up;
* first-appearance ordering keeps the layout identical on every repaint.
Kill the double-drawn node at two levels. build_topology now coalesces an
under-specified id into the one longer node it can only be (27 -> 27d4396a2967),
folding their links — fixing the atlas and the scenario ranker too, not just this
graph — while leaving a genuinely ambiguous stub (c5 -> two nodes) alone. The
widget then closes the same gap locally across its own layers, catching a hash
that is ambiguous against the whole contact list but unmistakable among these
paths (be -> be1d1c1dbc4b).
On the node detail page specifically: name every node, not just the two ends
(a relay reads by its contact name, unknown ones keep the honest hash byte), and
draw the target with its own map glyph (repeater, room, sensor) to match the
header. us stays on the left, the target on the right.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fan-lane route graph clipped every label to a fixed 12-cell budget, so
node names past twelve characters ellipsized even with room to spare. Cap the
label at the physical canvas width instead: a name is kept whole unless it is
wider than the whole canvas (nowhere for the cells to go), and the placement
pass — which already slides an endpoint's label inward or drops a crowded
relay's — decides where it lands. Message-paths endpoints now read in full.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both trace tools now feed the boards: on a successful trace the session
derives the walk's spec, route, and stats from the reply and offers it to
every discipline, and a walk that sets a record surfaces a ★ note under the
run's aggregates (the walk still records nothing extra — it's the same one
trace per transmission). Scoring is defensive: a hiccup never breaks a
trace.
Trace path can open armed on a given spec, so the trophy case's Trace this
path reopens a record's exact route ready to walk again. The shared route
graph is unchanged; its docstring just drops the pathfinder example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Message paths dialog's braille route graph becomes ui/pathgraph.py —
layered paths with per-layer colour and draw priority, caller-supplied
glyph/label callbacks — so the Pathfinder can draw best-ever/session/attempt
routes through the same widget. Endpoint labels lose their beside-the-marker
restriction: they place above/below like relay labels (with an edge-clamped
anchor so a name at the canvas margin still lands), falling back beside only
as a last resort.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>