Commit Graph

11 Commits

Author SHA1 Message Date
Marcel
10249c33be fix(stammbaum): raise dimmed opacity to 0.45 and bind tests to the constant (#703)
Bump DIMMED_OPACITY 0.4 -> 0.45 so dimmed outlines/labels stay legible
against bg-surface in both themes (dark mode dims already-light mint, the
riskier case). Import the constant into StammbaumTree.svelte.test.ts so the
node-opacity assertions track it instead of a hard-coded '0.4'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 19:13:49 +02:00
Marcel
7a655ce6f4 feat(stammbaum): add lineage highlight traversal module (#703)
Pure, DOM-free traversal over the family graph. Given the relationship
edges and a selected root, highlightLineage returns the active id set
(root + full pedigree upward + full descendant tree downward + every
spouse of those blood people, as active leaves) and a connector
predicate active only when both joined people are active.

The walk is guarded by the accumulating visited set, so cyclic PARENT_OF
data terminates (REQ-STAMMBAUM-04 / AC10). SIBLING_OF and social
relation types are ignored, so collaterals never enter the active set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 16:26:24 +02:00
Marcel
89bb0b5d65 test(stammbaum): assert no node sits between AC2 spouses on same y (#361)
Cycle-2 follow-up from Sara. The existing assertion
`Math.abs(posA2.x - posB2.x) === NODE_W + COL_GAP` proves adjacency in
the current integer-slot packer but would silently pass if a future
refactor moved to fractional offsets with a third node squatting at a
non-slot x between the spouses. The added loop closes that contract.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:55:23 +02:00
Marcel
9bdd9fb3a5 refactor(stammbaum): extract computeViewBox() helper from buildLayout (#361)
@Felix + @Markus on PR #693: viewBox computation is self-contained
(reads only positions + the MIN/PAD constants). Lift it out so buildLayout
ends with a readable two-line orchestration.

Pure refactor under green tests — no behaviour change, no test diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:43:25 +02:00
Marcel
52e48a6b8c refactor(stammbaum): extract assignRanks() helper from buildLayout (#361)
@Felix + @Markus on PR #693: buildLayout was a 367-line orchestrator
doing five sequential phases. assignRanks() is one of the two
self-contained phases that reads top-down on its own.

Pure refactor under green tests — no behaviour change, no test diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:42:14 +02:00
Marcel
fd624f6ec8 test(stammbaum): assert no canonical SPOUSE_OF carries fromYear (#361)
@Sara on PR #693: canonical_fixture_multi_spouse_falls_through_to_displayName
_when_no_fromYear asserts the *fallback* branch of the multi-spouse sort
(NULLS LAST, then displayName). It only exercises the name branch while
every SPOUSE_OF row in the fixture has fromYear=undefined. The day a year
gets backfilled in canonical import, the test would silently start
asserting year-order with no notice.

Add a precondition at the head of the test that fails fast with a clear
maintainer message ("update or split into year-branch / name-branch")
when any canonical SPOUSE_OF row gains a fromYear.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:41:17 +02:00
Marcel
652100a9c2 test+feat(stammbaum): merge sibling blocks across same-rank spouse edge (#361)
AC2 — intra-family marriage. When two parented persons at the same
imported generation are spouses but live in separate sibling blocks
(each under their own parent), the block-packer used to leave them
split, drawing a long spouse line that crossed through any intervening
siblings. The new step 3.5 detects that case, moves the focal members
to the join boundary (A's spouse rightmost in A's block, B's spouse
leftmost in B's), and concatenates B's members onto A's; the combined
block centres on the average of the two parents' midpoints.

Latent against today's data (no intra-family marriage in the canonical
fixture); covered by a synthetic two-family scenario in
buildLayout.test.ts. Packer growth stays comfortably under Markus's
80-LoC extraction threshold, so packBlocks.ts is not yet warranted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:18:23 +02:00
Marcel
557f37be54 test+feat(stammbaum): order multi-spouses by fromYear then displayName (#361)
Replaces the alternating-side insertOnRight rule with a sort-and-splice
that places every loose spouse to the right of the parented focal in
(fromYear ASC NULLS LAST, displayName ASC) order. Mirrored in step 3 for
the all-loose chained merge so Albert de Gruyter's four marriages land
in deterministic alphabetical order today (no fromYear populated in the
canonical dataset) and switch automatically to year-order as the
transcription pipeline backfills marriage years.

PersonNodeDTO carries only displayName, not parsed first/last names, so
the tiebreaker uses displayName rather than the (lastName, firstName)
key in the original UX brief. The canonical alphabetical order matches
in both schemes — the rule activates the moment a multi-spouse case has
mixed display-name patterns.

Retires the temporary commit-3 scaffold
`attaches_loose_multi_spouse_to_parented_partner_when_edge_order_clobbers`
which became position-arithmetic-equivalent under the new right-of-focal
rule; the two new sort tests are stronger discriminators for the same
behaviour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:14:23 +02:00
Marcel
2a462d0a7c test+feat(stammbaum): preserve all SPOUSE_OF edges in layout (#361)
Switches spousePairs from Map<string, string> to Map<string, Set<string>>
so multi-spouse persons (canonical case: Albert de Gruyter, 4 marriages)
keep every partner instead of losing the earlier .set() values.

The behavioural discriminator (now exercised by
attaches_loose_multi_spouse_to_parented_partner_when_edge_order_clobbers)
is a loose person with both a parented and a loose spouse: the old map
clobbered to whichever edge landed last, so the loose-placement step could
miss the parented partner and merge the focal node into the wrong block.

Also closes the robustness gap NullX flagged: SPOUSE_OF edges referencing
IDs outside allNodes are dropped at ingestion instead of leaking into the
spouse-pulldown loop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:03:52 +02:00
Marcel
cb8c85a742 feat(stammbaum): seed layout rank from imported generation (#689)
buildLayout switches to a two-stage assignment:

1. Seed — every node with node.generation != null is locked at that
   rank. The fallback heuristic never moves a locked rank, and the
   spouse-pulldown never pulls a locked rank.
2. Fallback — for unseeded nodes, rank = max(parent rank) + 1 reading
   parents from the same unified rank map, so an unseeded child of a
   seeded G 2 parent correctly inherits rank 3. Spouse-pulldown ties
   unseeded spouses to their deeper partner exactly as before.
3. Normalise — if any rank is negative (future G −1 ancestor), shift
   the whole map so min(rank) == 0. No-op for today's data.

Fixes the Herbert Cram pattern from #361's review: two parented
spouses with imported G 3 now render on the same y row. Existing
StammbaumTree tests still pass byte-for-byte because every test node
has node.generation undefined, so the heuristic runs unchanged.

Refs #689

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:43:58 +02:00
Marcel
1cb05697cc refactor(stammbaum): extract buildLayout to pure module
Move the layout function out of StammbaumTree.svelte (lines 47-275) into a
new pure TypeScript module at frontend/src/lib/person/genealogy/layout/
buildLayout.ts so it can be exercised by direct unit tests. Drops the
eslint-disable svelte/prefer-svelte-reactivity blanket; switches the
remaining scope-local Maps/Sets in parentLinks to SvelteMap/SvelteSet to
satisfy the rule per-call-site. No behaviour change — existing
StammbaumTree tests must pass byte-for-byte.

Refs #689

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:17:18 +02:00