Files
familienarchiv/docs/adr/030-stammbaum-bloodline-tidy-tree-layout.md
Marcel 4d1a5862d0
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m33s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 3m34s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 22s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
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:55:10 +02:00

111 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 cycles** — `assignRanks`' 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.