refactor(stammbaum): single source of truth for findAc3Candidates (#361)

Extracts the AC3 revisit-trigger predicate into a plain .mjs module both
the Node-run capture script and the TypeScript validator import directly.
Removes the line-for-line duplicate (and its "keep both in sync" comment)
that Felix + Markus flagged in cycle-3 review.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-28 21:15:02 +02:00
parent 2c18cb8b0d
commit 585f28cd23
3 changed files with 66 additions and 83 deletions

View File

@@ -11,6 +11,8 @@ import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { randomUUID } from 'node:crypto';
import { findAc3Candidates } from '../src/lib/person/genealogy/__fixtures__/findAc3Candidates.mjs';
const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:8080';
const EMAIL = process.env.CAPTURE_EMAIL;
const PASSWORD = process.env.CAPTURE_PASSWORD;
@@ -59,9 +61,10 @@ const FIXTURE_PATH = `${HERE}/../src/lib/person/genealogy/__fixtures__/stammbaum
// 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.
// substantially. The matching gates in
// src/lib/person/genealogy/__fixtures__/validateFixture.ts cover unit-test
// coverage; the floors are intentionally duplicated as numeric constants
// (the AC3 revisit predicate, by contrast, now lives in one shared module).
const MIN_NODES = 50;
const MIN_GENERATIONS = 5;
const MIN_SPOUSE_OF_EDGES = 1;
@@ -161,47 +164,6 @@ function countMultiSpousePersons(spouseEdges) {
return count;
}
// AC3 revisit-trigger predicate (ADR-026 §Notes). Returns the IDs of every
// captured person matching "unseeded loose spouse whose parents are in graph".
// A non-empty result is the documented revisit trigger for the dagre
// deferral decision — emitted as a soft-warn (does not fail capture) so a
// human-in-the-loop notices when the AC3 layout branch becomes reachable.
// Mirrors the implementation in src/lib/person/genealogy/__fixtures__/
// validateFixture.ts findAc3Candidates(); keep both in sync.
function findAc3Candidates(network) {
const nodes = Array.isArray(network.nodes) ? network.nodes : [];
const edges = Array.isArray(network.edges) ? network.edges : [];
const generationById = new Map();
for (const n of nodes) {
if (n.id) generationById.set(n.id, n.generation);
}
const hasSpouseEdge = new Set();
const seededParentByChild = new Map();
for (const e of edges) {
if (!e.personId || !e.relatedPersonId) continue;
if (e.relationType === 'SPOUSE_OF') {
hasSpouseEdge.add(e.personId);
hasSpouseEdge.add(e.relatedPersonId);
continue;
}
if (e.relationType === 'PARENT_OF') {
const parentGen = generationById.get(e.personId);
if (parentGen != null) seededParentByChild.set(e.relatedPersonId, true);
}
}
const matches = [];
for (const n of nodes) {
if (!n.id) continue;
if (n.generation != null) continue;
if (!seededParentByChild.get(n.id)) continue;
if (!hasSpouseEdge.has(n.id)) continue;
matches.push(n.id);
}
return matches;
}
function mapAddToSet(map, key, value) {
const s = map.get(key);
if (s) s.add(value);