Review follow-up (Markus/Architect): ADR-026 pre-committed a successor ADR if the
in-house layout stopped converging; its UX stop-trigger (Albert smeared across the
canvas) fired. ADR-030 records the bottom-up tidy-tree, the module split, and the two
maintainer-confirmed decisions (hybrid intra-family, per-bloodline width metric),
superseding ADR-026's block-packer in part (no-dagre + seeded-rank retained). GLOSSARY
replaces the deleted sibling-block / parented / anchor-index vocabulary with the new
family-forest model (unit, tidy tree, structural owner, bloodline, cross-link).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-ups:
- Leonie/UX: 0.55 navy on the sand canvas was ~2.6:1, under the WCAG 1.4.11 3:1
non-text floor for senior readers; 0.7 clears it.
- Sara/QA: add a browser test that actually renders a cross-level link and
asserts the distinct 2 6 dash, and that a non-cross-link parent edge stays
solid — the cadence was previously only validated via the structural
crossLinks array, never where it renders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-up (Sara/QA): the empty graph (fresh /stammbaum before data loads)
exercised the positions.size===0 viewBox fallback and the roots.length===0 early
return, both previously untested. Assert no NaN in the viewBox and MIN dimensions,
plus a single isolated node placed once at rank 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An A<->B parent cycle and a founder reaching a re-entrant 3-cycle both return a
finite layout (no frozen $derived) with every node placed exactly once.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seeded Fisher-Yates permutation of nodes and edges yields byte-identical
positions — confirms every comparator ends in a stable id and nothing relies on
Map iteration order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Total canvas width is the wrong metric: centring every ancestor makes a 24-root
forest wider overall (an accepted trade-off, pan/zoom handles navigation). The
actual fix is per-bloodline compactness. Assert every contiguous bloodline's
span stays far under the old full-canvas smear (4860px) — today the widest,
Albert de Gruyter's, is ~960px, down from being smeared across the whole canvas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The canonical graph is a forest of 24 roots spread across generations 0-4.
Packing every root at tree-depth 0 stacked all of them horizontally even when
they sit at different generations (different y), blowing the canvas out to
~9660px. Indexing the contour by absolute level (the rank buildLayout already
passes as level) lets unrelated roots at different generations share x-columns,
and keeps the no-overlap guarantee per-row. level falls back to tree depth when
omitted, so the abstract tidyTree tests are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No node outside a root's structural subtree may intrude into that bloodline's
[minX, maxX] horizontal span — the contiguity guarantee that fixes the smeared
bloodline symptom.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
O(n^2) sweep over canonical + synthetic: any two nodes sharing a y are at least
NODE_W + COL_GAP apart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixture-wide loop over the canonical forest and a synthetic tree: each unit's
run centre is within [min, max] of its child-unit centres — the ancestor
centring invariant, asserted on real data.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 5-generation single bloodline fanning out wide at the bottom: the apex
great-great-grandparent (and every ancestor in the chain) sits at the centre of
the descendant span, the exact symptom the old per-generation packer produced
in reverse (apex pinned to the left edge).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
StammbaumConnectors takes the layout's crossLinks and draws those parent->child
connectors with a 2 6 dash at reduced opacity — deliberately distinct from the
ended-marriage spouse dash (4 4) and from a solid parent drop. Geometry still
lands on the child top, so the meaning is carried redundantly (WCAG 1.4.1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the two spouses' parents sit at different structural levels, the
structural owner keeps its hierarchy edge and the other parent->spouse edge is
recorded in layout.crossLinks (rendered with a distinct dash). The couple still
sits exactly adjacent in the owner's run and B keeps a real position.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the existing adjacency contract: the couple is exactly adjacent in the
run AND, because both parents are roots (same structural level), the displaced
parent edge stays solid — layout.crossLinks is empty for this case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
buildLayout now builds the family forest, packs it bottom-up via tidyTree, and
maps each unit's run x back to per-person positions (x from structure, y from
rank). assignRanks, the generations map, and computeViewBox are reused
unchanged. The unknown-id guard now covers PARENT_OF as well as SPOUSE_OF, and
displaced cross-level edges are exposed as crossLinks for distinct rendering.
The ~210-line block packer (and its block/merge helpers) is gone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Net-new ordering coverage: roots and every unit's children sort by birthYear
ASC (undated last), then displayName, then stable id — so horizontal x never
depends on Map iteration order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assigns every person to one unit: a primary, or a spouse absorbed into the
primary's run (marriage-year order, #361 preserved). Wires the parent/child
hierarchy from each primary's structural-owner parent and records displaced
parent edges as cross-links (classified same-level vs cross-level for later
distinct rendering). Unknown-id guard covers PARENT_OF and SPOUSE_OF.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Structural-owner rule for couples: earlier birth year wins, missing year sorts
last, ties break on stable id. The single definition reused by the cross-link,
cycle and intra-family paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New domain-agnostic bottom-up tidy-tree module (Reingold-Tilford contour pack)
operating on abstract { id, width, children } nodes — zero generated-API
imports. First rung of the TDD ladder: a single leaf lays out at x=0. The full
contour/centring machinery is in place; subsequent commits add tests that
exercise it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The existing node() factory never sets birthYear, but the new sibling/branch
comparator (birthYear ASC NULLS LAST) needs it. Add makeNode(id, name,
{birthYear, generation}) alongside it; unblocks every ordering test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>