docs(stammbaum): ADR-030 tidy-tree layout, supersede ADR-026 packer, refresh glossary (#724)
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
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
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>
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
# ADR-026 — In-House Stammbaum Layout, dagre Evaluated and Deferred
|
||||
|
||||
**Date:** 2026-05-28
|
||||
**Status:** Accepted
|
||||
**Status:** Accepted — superseded in part by [ADR-030](./030-stammbaum-bloodline-tidy-tree-layout.md)
|
||||
**Issue:** #361
|
||||
**Supersedes:** _none_
|
||||
**Supersedes-on-trigger:** A future ADR-027 if any acceptance criterion below stops converging in-house.
|
||||
**Superseded-by:** ADR-030 (#724) replaces the **per-generation block packer** below with
|
||||
a bottom-up tidy-tree after its position-within-rank model stranded ancestors and smeared
|
||||
bloodlines across the canvas — the UX stop-trigger named in this ADR. The **in-house /
|
||||
no-dagre** decision and the seeded-rank invariant (#689) are retained.
|
||||
**Supersedes-on-trigger:** _(triggered)_ The UX stop-trigger fired; see ADR-030.
|
||||
|
||||
---
|
||||
|
||||
@@ -117,6 +121,7 @@ threshold, so `packBlocks.ts` is **not** yet warranted.
|
||||
is the source-of-truth probe against live data; the function is the
|
||||
capture-time and fixture-time signal that the predicate's count crossed
|
||||
zero.
|
||||
|
||||
- **AC6 — Bundle-impact gate (≤ 40 kB gzipped on `/stammbaum`).** Moot under
|
||||
this ADR; reactivates only under ADR-027 (dagre adoption).
|
||||
- **AC7 — Visual regression at 320 / 768 / 1440.** `toHaveScreenshot()`
|
||||
|
||||
110
docs/adr/030-stammbaum-bloodline-tidy-tree-layout.md
Normal file
110
docs/adr/030-stammbaum-bloodline-tidy-tree-layout.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# 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"
|
||||
(Reingold–Tilford / 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.
|
||||
Reference in New Issue
Block a user