test+fix(stammbaum): capture script floors >= 1 multi-spouse person (#361)

@Markus + @Tobias + @Sara on PR #693: the multi-spouse property is
load-bearing for buildLayout.test.ts (canonical_fixture_assigns_a_position
_to_every_node_with_multiple_spouses + canonical_fixture_multi_spouse
_falls_through_to_displayName_when_no_fromYear). A recapture against a
dataset that lost every multi-spouse person would silently degrade those
tests to vacuous truth.

Add MIN_MULTI_SPOUSE_PERSONS=1 to the capture-script sanity gates. Extract
the validator into a unit-testable TS module next to the fixture; the .mjs
script keeps its inline copy (one-file local utility) but the contract is
now covered by validateFixture.test.ts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-28 20:39:55 +02:00
parent 5167a2ae18
commit 6d8655bad1
3 changed files with 184 additions and 5 deletions

View File

@@ -56,12 +56,16 @@ const HERE = dirname(fileURLToPath(import.meta.url));
const FIXTURE_PATH = `${HERE}/../src/lib/person/genealogy/__fixtures__/stammbaum.json`;
// Sanity floors — calibrated against the canonical dataset (May 2026: 62
// nodes, 5 generations, 28 SPOUSE_OF edges). The point is catching a silently
// empty backend, not strict size validation; raise these only if the
// canonical graph grows substantially.
// nodes, 5 generations, 28 SPOUSE_OF edges, 1 multi-spouse person). The point
// is catching a silently empty backend or a structurally-regressed snapshot,
// not strict size validation; raise these only if the canonical graph grows
// substantially. The same gates are unit-tested at
// src/lib/person/genealogy/__fixtures__/validateFixture.ts — keep both
// definitions in sync.
const MIN_NODES = 50;
const MIN_GENERATIONS = 5;
const MIN_SPOUSE_OF_EDGES = 1;
const MIN_MULTI_SPOUSE_PERSONS = 1;
function parseSetCookies(headers) {
const out = new Map();
@@ -112,6 +116,7 @@ function validate(network) {
const edges = Array.isArray(network.edges) ? network.edges : [];
const spouseEdges = edges.filter((e) => e.relationType === 'SPOUSE_OF');
const generations = new Set(nodes.map((n) => n.generation).filter((g) => g != null));
const multiSpousePersons = countMultiSpousePersons(spouseEdges);
const failures = [];
if (nodes.length < MIN_NODES) {
@@ -123,6 +128,12 @@ function validate(network) {
if (spouseEdges.length < MIN_SPOUSE_OF_EDGES) {
failures.push(`expected >= ${MIN_SPOUSE_OF_EDGES} SPOUSE_OF edges, got ${spouseEdges.length}`);
}
if (multiSpousePersons < MIN_MULTI_SPOUSE_PERSONS) {
failures.push(
`expected >= ${MIN_MULTI_SPOUSE_PERSONS} person with multiple SPOUSE_OF edges, ` +
`got ${multiSpousePersons}`
);
}
if (failures.length > 0) {
throw new Error(`Sanity gates failed:\n - ${failures.join('\n - ')}`);
}
@@ -131,10 +142,31 @@ function validate(network) {
nodes: nodes.length,
edges: edges.length,
spouseEdges: spouseEdges.length,
generations: [...generations].sort((a, b) => a - b)
generations: [...generations].sort((a, b) => a - b),
multiSpousePersons
};
}
function countMultiSpousePersons(spouseEdges) {
const partners = new Map();
for (const e of spouseEdges) {
if (!e.personId || !e.relatedPersonId) continue;
mapAddToSet(partners, e.personId, e.relatedPersonId);
mapAddToSet(partners, e.relatedPersonId, e.personId);
}
let count = 0;
for (const set of partners.values()) {
if (set.size >= 2) count += 1;
}
return count;
}
function mapAddToSet(map, key, value) {
const s = map.get(key);
if (s) s.add(value);
else map.set(key, new Set([value]));
}
function writeFixture(network) {
mkdirSync(dirname(FIXTURE_PATH), { recursive: true });
writeFileSync(FIXTURE_PATH, JSON.stringify(network, null, '\t') + '\n', 'utf8');
@@ -148,7 +180,7 @@ async function main() {
const stats = validate(network);
writeFixture(network);
console.error(
`Wrote ${FIXTURE_PATH}\n nodes: ${stats.nodes}\n edges: ${stats.edges}\n SPOUSE_OF edges: ${stats.spouseEdges}\n generations: G${stats.generations.join(', G')}`
`Wrote ${FIXTURE_PATH}\n nodes: ${stats.nodes}\n edges: ${stats.edges}\n SPOUSE_OF edges: ${stats.spouseEdges}\n multi-spouse persons: ${stats.multiSpousePersons}\n generations: G${stats.generations.join(', G')}`
);
}