docs(adr): ADR-026 in-house Stammbaum layout, dagre deferred (#361)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m32s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m56s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 25s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m32s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m56s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 25s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
Records the decision to keep Stammbaum layout in-house, with the in-house fixes from commits 1-6 of #361 as the implementation, and a UX-signal-only stop trigger as the dagre re-evaluation criterion. Captures the deferred acceptance criteria (AC3, AC6, AC7) with explicit revisit triggers so future maintainers do not silently inherit unbounded scope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
129
docs/adr/026-stammbaum-layout-in-house.md
Normal file
129
docs/adr/026-stammbaum-layout-in-house.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# ADR-026 — In-House Stammbaum Layout, dagre Evaluated and Deferred
|
||||
|
||||
**Date:** 2026-05-28
|
||||
**Status:** Accepted
|
||||
**Issue:** #361
|
||||
**Supersedes:** _none_
|
||||
**Supersedes-on-trigger:** A future ADR-027 if any acceptance criterion below stops converging in-house.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
After #689 shipped the seeded-rank invariant — `buildLayout.ts` treats imported
|
||||
`persons.generation` as a strict row anchor and the iterative heuristic only
|
||||
runs for unseeded nodes — the question "should we adopt
|
||||
[@dagrejs/dagre](https://www.npmjs.com/package/@dagrejs/dagre) for Stammbaum
|
||||
layout?" had to be re-evaluated.
|
||||
|
||||
dagre's headline value is **rank assignment** via `network-simplex` /
|
||||
`longest-path`. That value is now mostly redundant: curated import data already
|
||||
pins ranks for the family graph, and the residual heuristic only places
|
||||
unseeded nodes (today: family members imported without a `generation` column,
|
||||
spouses with no parents in the graph).
|
||||
|
||||
What remains are **position-within-rank** problems:
|
||||
|
||||
1. Multi-spouse persons (canonical case: Albert de Gruyter, 4 marriages) whose
|
||||
secondary marriages were silently dropped by a `Map<string, string>` shape.
|
||||
2. Intra-family marriages — two persons in different sibling blocks at the
|
||||
same rank who marry each other (latent; zero cases in current data).
|
||||
3. Unseeded loose spouses whose parents are also in the graph (latent; zero
|
||||
cases — 0 of 942 unseeded persons match the predicate in the May-2026
|
||||
snapshot).
|
||||
|
||||
Six persona walkthroughs on #361 (Leonie/UX, Felix/Dev, Markus/Architect,
|
||||
Nora/Security, Sara/QA, Tobias/DevOps, Elicit/Requirements) converged on the
|
||||
same recommendation: try the in-house fix path first, against the canonical
|
||||
dataset, with quantitative exit triggers — adopt dagre only if any acceptance
|
||||
criterion fails to converge.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
**Keep Stammbaum layout in-house. Do not adopt dagre at this time.**
|
||||
|
||||
The fix path lands as six commits on #361:
|
||||
|
||||
1. Spec geometry reconcile (`NODE_W=160, NODE_H=56` matches `buildLayout.ts`)
|
||||
and an explicit seeded-rank-invariant Layout-rules line.
|
||||
2. Canonical `/api/network` fixture capture script + pinned snapshot for
|
||||
structural assertions in `buildLayout.test.ts`.
|
||||
3. `spousePairs: Map<string, string>` → `Map<string, Set<string>>`. Preserves
|
||||
all marriages; closes Nora's robustness gap (edges referencing IDs outside
|
||||
`allNodes` are guarded at ingestion).
|
||||
4. Multi-spouse ordering: `(fromYear ASC NULLS LAST, displayName ASC)`,
|
||||
inserted to the right of the parented focal — matches Leonie's UX rule.
|
||||
5. Intra-family-marriage block merge across same-rank parented sibling blocks
|
||||
(AC2) — adjacent placement at the join boundary.
|
||||
6. Marriage-line midpoint dot enlarged from `r=4.5` to `r=6` (WCAG 1.4.11
|
||||
informational contrast — the dot disambiguates stacked marriages and is
|
||||
no longer decorative).
|
||||
|
||||
The block-packer + AC2 merge stays well under Markus's 80-LoC extraction
|
||||
threshold, so `packBlocks.ts` is **not** yet warranted.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Accepted today
|
||||
|
||||
- **AC1 (multi-spouse preservation)** is now a property of `buildLayout`, verified
|
||||
by both synthetic and canonical fixture tests.
|
||||
- **AC2 (intra-family marriage)** ships latent but covered by a synthetic
|
||||
two-family regression test.
|
||||
- **AC4 (seeded-rank invariant)** preserved end-to-end by every `buildLayout.test.ts`
|
||||
case from #689.
|
||||
- **AC5 (spec ↔ code geometry)** reconciled in commit 1.
|
||||
|
||||
### Deferred with revisit triggers
|
||||
|
||||
- **AC3 — Unseeded loose spouse with parents-in-graph.** Database verification:
|
||||
0 of 942 unseeded persons match the predicate today. Structurally, every
|
||||
realistic case maps to a **curation/import gap** (P's parents were imported
|
||||
with `generation`, P themselves was not) and belongs in the canonical import
|
||||
sheet rather than `buildLayout`. **Revisit trigger:** first canonical fixture
|
||||
containing a parented unseeded spouse — at which point this ADR is updated
|
||||
in place or superseded by an ADR-027.
|
||||
- **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()`
|
||||
permanently dropped (high running cost, speculative coverage). The axe-core
|
||||
3:1 contrast check for the enlarged marriage dot is verified one-shot at
|
||||
PR time, not committed; the permanent contrast/breakpoint gate lands with
|
||||
#692 (mobile pan/zoom epic) alongside the breakpoint visual-regression
|
||||
infrastructure.
|
||||
|
||||
### UX-signal-only stop trigger for dagre adoption
|
||||
|
||||
There is **no LoC cap** on the in-house path. The only divergence signal that
|
||||
warrants reopening the dagre decision is a **UX failure against the canonical
|
||||
fixture** — specifically, Albert de Gruyter's 4 marriages failing the read
|
||||
test ("can a 67-year-old researcher unambiguously see all four spouses?").
|
||||
If that ever happens, Felix posts a divergence-evidence comment on #361 (or
|
||||
the equivalent successor issue), the team re-runs brainstorming with the
|
||||
dagre option on the table, and adoption proceeds under the supply-chain
|
||||
controls already documented in #361's body (`@dagrejs/dagre` exact-pinned, no
|
||||
auto-merge, try/catch fallback with structured log, deterministic input sort).
|
||||
|
||||
### Operational
|
||||
|
||||
- **No CI, image, or compose changes.** Pure frontend layout work; standard
|
||||
frontend rebuild covers the deploy.
|
||||
- **No service topology changes.** No new env vars, ports, resource limits.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- `frontend/scripts/capture-network-fixture.mjs` is a **local-only developer
|
||||
utility**, never invoked from CI. Re-run intentionally; commit the resulting
|
||||
JSON in one atomic commit when a new structural case appears (new edge type,
|
||||
new marriage configuration, new generation range).
|
||||
- The canonical fixture contains real family names. Repository is private;
|
||||
scrubbing is a single-commit migration if it ever opens.
|
||||
- Brand-mint enforcement on SVG strokes (Leonie's "all connectors render in
|
||||
brand-navy, hierarchy comes from shape") stays a **code-review check at PR
|
||||
time**. No CI grep, no custom ESLint rule.
|
||||
Reference in New Issue
Block a user