feat(stammbaum): add familyForest.pickStructuralOwner (#724)

Structural-owner rule for couples: earlier birth year wins, missing year sorts
last, ties break on stable id. The single definition reused by the cross-link,
cycle and intra-family paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-04 13:13:26 +02:00
committed by marcel
parent fc5c837d2c
commit 7e8b90c8ee
2 changed files with 57 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { pickStructuralOwner } from './familyForest';
type Person = { id: string; birthYear?: number };
describe('pickStructuralOwner', () => {
const p = (id: string, birthYear?: number): Person => ({ id, birthYear });
it('picks the earlier-born spouse as structural owner', () => {
expect(pickStructuralOwner(p('a', 1900), p('b', 1920))).toBe('a');
expect(pickStructuralOwner(p('a', 1920), p('b', 1900))).toBe('b');
});
it('sorts a missing birthYear last (the dated spouse owns)', () => {
expect(pickStructuralOwner(p('a'), p('b', 1900))).toBe('b');
expect(pickStructuralOwner(p('a', 1900), p('b'))).toBe('a');
});
it('breaks ties on stable id when birth years match or are both missing', () => {
expect(pickStructuralOwner(p('zzz', 1900), p('aaa', 1900))).toBe('aaa');
expect(pickStructuralOwner(p('zzz'), p('aaa'))).toBe('aaa');
});
});

View File

@@ -0,0 +1,34 @@
// Domain-aware construction of the genealogy "family forest" (#724).
//
// This module owns every piece of genealogy knowledge the layout needs —
// spouse runs, birth-year ordering, structural-owner selection, intra-family
// marriage resolution and cross-links — and flattens it into the abstract
// { id, width, children } nodes that the domain-agnostic tidyTree.ts packs.
// buildLayout.ts orchestrates the two and maps run positions back to persons.
import type { components } from '$lib/generated/api';
type PersonNodeDTO = components['schemas']['PersonNodeDTO'];
/**
* Choose the structural owner of a couple: the spouse who keeps the bloodline
* (hierarchy) position. Earlier birth year wins; a missing birth year sorts
* last; ties break on the stable id. Shared by the cycle, cross-link and
* intra-family paths so the rule is defined exactly once.
*/
export function pickStructuralOwner(
a: { id: string; birthYear?: number | null },
b: { id: string; birthYear?: number | null }
): string {
const ra = ownerRank(a);
const rb = ownerRank(b);
if (ra !== rb) return ra < rb ? a.id : b.id;
return a.id <= b.id ? a.id : b.id;
}
// Lower is "more owning". Missing birth year → +Infinity (sorts last).
function ownerRank(p: { birthYear?: number | null }): number {
return p.birthYear == null ? Number.POSITIVE_INFINITY : p.birthYear;
}
export type { PersonNodeDTO };