Development record
by-links-layout-publishes-overlapping-positions
Summary
Makes the record-graph's by-links arrangement come to rest and makes the overlap gate measure it. The by-links layout settles islands with a spring layout that has no collision pass and packs two rim rows onto a circle whose circumference is smaller than the sum of the record arc-widths, so it publishes overlapping positions by construction; and the overlap gate counts only the by-date coil, so the overlap ships green. This spec adds one post-settle collision-resolution pass covering islands, pairs and rim under a single packing rule, and widens the gate to measure the by-links layout too.
Scope
In:
internal/core/site/layout.go— thebyLinksbuilder (lines 366–469): a post-settle collision pass after island/pair/rim placement, and the rim-row packing so the circumference matches the summed arc-widths.- The overlap counter, today computed only inside
coil()(layout.go:332–339): measurea.Linksas well, in one coordinate space. internal/core/site/build.go(theOverlapsresult field, lines 299–301,- and the human-facing print in
internal/surface/cli/site.go:197.
- and the human-facing print in
- Tests in
layout_test.goandbuild_test.go.
Out:
- The renderer half is already fixed (iss-2608231243286557 — the collision
resolver no longer demands 1.5 units of padding); the JS chart emitted from
graphpage.gois unchanged. - The by-date coil (
coil(), layout.go:264–360) already settles and is not touched beyond sharing the widened overlap counter.
Approach
The two causes. byLinks (layout.go:366–469) settles islands via
springLayout (called line 418, Fruchterman-Reingold at 511–587) then
decompress/scale/place (419–422) with no node-radius collision pass — cause
one. The two rim rows (445–468, radii rimRow0=0.905/rimRow1=0.97) sum each
record's arc-width w = 2*a.Radius[i] + coilGap into total (456) and map each
onto a circle at radius rr, but the summed widths exceed the circumference at
that radius — overpacked by construction, cause two. Prior local patches
(relaxing linked records alone; a rim-row growth rule that pushed the outer
radius from 0.97 to 1.83) were measured and reverted because they changed the
picture without settling it.
One packing rule. After island, pair and rim placement (after line 468), run
a single post-settle collision-resolution pass over all by-links nodes
together — the coil already demonstrates the forbidden-interval idea
(layout.go:296–317). The pass iteratively separates any pair whose centre
distance is less than the sum of their published radii (plus the same epsilon the
coil uses), nudging both along their separation vector, bounded by an iteration
cap, until no pair overlaps. Because islands, pairs and rim are resolved in the
same pass and the same coordinate space, no region is packed against a promise a
later region breaks. The rim rows are seeded so their circumference is at least
the summed arc-widths (the row radius is derived from total, not fixed), so the
pass starts from a near-feasible configuration rather than an over-packed one and
converges without inflating the outer radius the way the reverted growth rule
did.
Widen the gate, one coordinate space. The overlap count lives only in coil()
(layout.go:332–339). Extract the counting loop into a helper
countOverlaps(points []Point, radii []float64) int and call it for both a.Coil
and a.Links, summing into a.Overlaps (Arrangements struct, layout.go:99–137).
The known trap is that node positions are unit-disk-normalised while radii are in
reference pixels (the mismatch that produced spurious overlaps in
iss-2608231322321751); the helper normalises both into one space before
comparing, so the count is real for by-links as it already is for by-date.
build.go (299–301, 465) and the CLI print (site.go:197) surface the combined
count as they do today.
How it satisfies each acceptance criterion
- By-links overlaps after the spring settle are separated; the published layout
reports zero overlaps — the post-settle pass over all nodes. Test: over the
record corpus, build the by-links layout and assert
countOverlaps(a.Links, a.Radius) == 0(a newTestByLinksNeverOverlapsmirroringTestCoilNeverOverlaps, layout_test.go:40). - The overlap gate measures the by-links layout, not only the by-date coil —
a.Overlapsnow sums both arrangements. Test:TestBuildLayoutDoesNotOverlap(build_test.go:322) is extended to assert the count reflectsa.Links. - A by-links layout that still contains overlaps flags red — because the gate
now counts
a.Links, a residual overlap makesa.Overlaps != 0, which the CLI prints as a non-zero/red result (site.go:197). Test: inject an overlapping by-links fixture and asserta.Overlaps > 0. - The by-date arrangement continues to pass — the coil's forbidden-interval
packing is unchanged; the shared counter measures it exactly as before. Test:
TestCoilNeverOverlapsstays green.
Decisions
One packing rule over all node classes, chosen over per-region patches. The two measured local fixes each moved the picture without settling it; the intent resolved that a correct fix is a design of the arrangement, not a patch. The single post-settle pass in one coordinate space is that design. The rim-row radius becomes derived from the summed arc-widths rather than fixed, so the pass converges without the outer-radius inflation the reverted growth rule caused.
Frontmatter
| id | spc-50 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-50-by-links-layout-publishes-overlapping.md
open on GitHub ↗ · commit history ↗
References1
- itd-157The by-links arrangement's non-settling has a LAYOUT half that is still open, and it is a redesign rather than a fix. The renderer half is fixed (the collision resolver no longer demands 1.5 units of padding the published positions never promised, iss-2608231243286557), but the layout itself still publishes overlapping positions, so the chart still never comes to rest on that arrangement — measured 2026-08-23: by date comes to rest, by links does not. Two causes found and NOT fixed, because fixing them changes the picture: the islands are settled by a spring layout with no collision pass at all, and the two rim rows map each record's arc-width onto a circle whose circumference is smaller than the sum of those widths, so the rows are overpacked by construction. Attempted fixes measured: relaxing the linked records alone left 600 overlaps (the rest are on the rim); adding a rim-row growth rule left 367 and pushed the outermost radius from 0.97 to 1.83, visibly changing the picture without settling it. Both reverted. A correct fix places every record — islands, pairs and rim — under one packing rule, which is a design of the arrangement, not a patch to it.implements
mentions1
- iss-2608231322321751The site check's overlapping-bubbles gate measures the COIL arrangement only: the counter lives inside the coil packer and never sees the by-links layout, so a links arrangement can ship with overlapping bubbles while the gate reports zero. Widening it is not a one-line change — the published positions are normalised to the unit disk while the radii are quoted in reference pixels, so a correct check has to compare both in one space (an attempt that compared them across spaces reported 159,600 overlaps on a 400-node fixture). Found while diagnosing why the by-links arrangement never settles (iss-2608231243286557). The renderer-side half of that defect IS fixed: the collision resolver no longer adds padding the layout never promised.