Files
familienarchiv/docs/adr/030-stammbaum-bloodline-tidy-tree-layout.md
Marcel cfbcc047c4
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m27s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m31s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m4s
docs(stammbaum): ADR-030 tidy-tree layout, supersede ADR-026 packer, refresh glossary (#724)
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>
2026-06-04 14:18:18 +02:00

5.7 KiB
Raw Blame History

ADR-030 — Stammbaum Bloodline-Contiguous Tidy-Tree Layout

Date: 2026-06-04 Status: Accepted Issue: #724 Supersedes: ADR-026 (in part — the per-generation block-packer decision and its position-within-rank fix path; the in-house / no-dagre decision and the seeded-rank invariant from #689 are retained)


Context

ADR-026 kept Stammbaum horizontal placement in-house with a per-generation block packer and pre-committed a successor ADR "if any acceptance criterion stops converging in-house." Its single UX stop-trigger was Albert de Gruyter's marriages failing the read test.

The block packer hit a worse, structural failure: it placed each generation independently, centring sibling blocks under already-placed parents and only ever shoving right on collision. Two consequences followed — a deep branch that could not fit at its ideal centre dragged everything downstream rightward and stranded the ancestor at the left edge of its own descendants; and a parent placed before its descendants existed could never be re-centred over them. Extreme symptom: Albert de Gruyter (G0) far left, a great-great-grandchild far right — one bloodline smeared across the full canvas. That is exactly the "UX failure against the canonical fixture" ADR-026 named as the trigger to revisit the layout.

Decision

Replace the per-generation block packer with a bottom-up "tidy tree" (ReingoldTilford / Walker contour pack), still in-house, no new dependency.

The horizontal x rewrite is split into three reviewable, unit-tested modules (mirroring the panZoom.ts / panZoomGestures.ts / animateView.ts split):

  • layout/tidyTree.ts — domain-agnostic contour packer over abstract { id, width, children, level? } nodes, zero generated-API imports. Contours are indexed by absolute generation level, not tree depth, so unrelated roots at different generations share x-columns instead of smearing the forest wide.
  • layout/familyForest.ts — all genealogy semantics: the unit model (a bloodline-carrying primary plus the spouse(s) absorbed into its run), pickStructuralOwner (lower birthYear, then stable id), loose-spouse absorption, multi-spouse runs (#361), sibling/branch order (birthYear ASC NULLS LAST → displayName → id), intra-family resolution, and cross-link classification.
  • layout/buildLayout.ts — orchestrates forest → tidy-pack → per-person positions. assignRanks (y from rank, #689 seeding), the generations map, and computeViewBox are reused unchanged; x comes from structure, y from rank.

Two decisions taken during implementation and confirmed with the maintainer:

  1. Intra-family marriage = hybrid. A couple is always exactly adjacent in the owner's run. When the two spouses' parents sit at the same structural level the displaced parent edge renders as a normal solid connector (the "adjacency" case); when they are cross-level (e.g. the canonical Clara⚭Herbert, where one parent is nested under Albert and the other hangs off a separate root), the structural owner keeps the hierarchy edge and the other parent→spouse edge renders as a distinct cross-link.
  2. Cross-link is rendered with a distinct 2 6 dash at 0.7 opacity in StammbaumConnectors.svelte — never the 4 4 ended-marriage cadence. Geometry still lands on the correct child top, so meaning is carried redundantly (WCAG 1.4.1); the 0.7 opacity clears the WCAG 1.4.11 3:1 non-text floor.

Consequences

Accepted

  • Ancestor centring — every unit is centred over its child-units' span (named-bug guard great_great_grandparent_is_not_stranded_left_of_descendants + a fixture-wide loop over canonical and synthetic trees).
  • Bloodline contiguity — each bloodline is one band with no foreign node interleaved. Albert de Gruyter's bloodline shrank from a full ~4860px smear to ~960px.
  • #361 / #689 preserved — multi-spouse runs in marriage-year order, seeded ranks, spouse pull-down; the existing buildLayout.test.ts cases stay green.
  • Determinism — every comparator ends in a stable id; a seeded permutation of nodes/edges yields byte-identical positions.
  • Fail-closed on cyclesassignRanks' iteration ceiling plus a forest structure (each unit has ≤1 hierarchy parent, cycles are unreachable from roots) guarantee a finite layout with every node placed exactly once.

Trade-off — total canvas width replaces the ADR-026 width assumption

Centring every ancestor inherently makes a forest of ~24 root-bands wider overall than the old per-generation left-packer that interleaved everyone into compact shared rows (canonical: ~7960px vs the old ~4860px). Total canvas width is therefore the wrong success metric; per-bloodline span is. The width regression test asserts each contiguous bloodline stays far under the old full-canvas smear. The wider canvas is navigated by the pan/zoom from #692 (ADR-027) and is an accepted trade-off for readability.

Operational

  • No CI, image, compose, or dependency change. Pure frontend layout. The d3-flextree escape hatch from #724 was not needed.

Deferred (follow-ups, per #724)

  • Connector legibility at 320px — the issue's "open verification"; a manual /stammbaum pass, with a connector-clarity issue spun only if drops/cross-links tangle.
  • Polished cross-link routing + a relationship tooltip.

Notes

  • ADR-026's retained parts: the no-dagre / in-house decision and the seeded-rank invariant (#689) still hold — this ADR changes only position-within / across rank, not rank assignment, and adds no dependency.
  • The validateFixture sanity gates and the AC3 revisit probe from ADR-026 are unchanged.