Files
parenthese/tools/layout-bench/perturb.mjs
T
IliesAllaliandClaude Fable 5.1 3be95b1a0f Banc du placement v2 : invariants, défauts sur le tracé réel, archétypes en texte, stabilité
Le banc mesure maintenant le code de production (computeBlockLayout), sans
écran, en quatre étages lus dans l'ordre et jamais additionnés :

- invariants qui doivent valoir zéro (chevauchement, couple non adjacent,
  fratrie coupée ou hors de l'ordre choisi, rangée différente de la génération
  de parenté recalculée ici, années de naissance qui redescendent) ;
- défauts comptés sur la géométrie vraiment dessinée (renvois, lignes de
  fratrie au-dessus d'un portrait étranger, croisements, lignes et foyers plus
  larges qu'un écran, proches de la référence visibles dans un écran) ;
- quatorze archétypes (ordre d'une fratrie, issue #67, branche courte, Marc et
  Nadia, petit conjoint, fratrie de douze, remariage, cousins, oncle et nièce,
  parent seul, adoption, deux familles, belle-famille, trois familles
  enchaînées) rendus en texte, témoins dans __snapshots__/archetypes, testés
  par vitest ; deux échecs connus (belle-famille, trois-belles-familles) en
  attendant la règle « le sang de la référence garde son enfant » ;
- stabilité : retirer une feuille ne devrait déplacer que son foyer.

Les modèles ELK écartés en septembre, elk-common et l'ancienne mesure sur
segments droits sont supprimés, avec la dépendance elkjs. Le générateur
d'arbres fictifs (gen-fr.mjs) entre dans le dépôt ; les arbres réels restent
dans fixtures/, ignoré par git.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 14:14:03 +02:00

37 lines
2.0 KiB
JavaScript

// Stabilité : retirer une feuille (personne sans enfant) ne devrait déplacer que son foyer.
// On retire tour à tour quelques feuilles prises à des positions réparties, on recalcule, et on compte
// les nœuds qui bougent de plus de 40 px une fois la translation d'ensemble retirée.
const SEUIL = 40
export function perturbation(layoutFn, graph, referenceId = null, { picks = 5 } = {}) {
const base = layoutFn(graph, referenceId)
const before = new Map(base.children.map((n) => [n.id, n.x]))
const hasKids = new Set()
for (const e of base.edges) {
const u = base.children.find((n) => n.id === e.sources[0])
if (u?._data?.p1Key) { hasKids.add(u._data.p1Key); hasKids.add(u._data.p2Key) }
}
const leaves = base.children.filter((n) => n._type === 'person' && !n._compact && !hasKids.has(n.id))
if (!leaves.length) return { feuilles: 0, max_bouges: 0, max_deplacement_px: 0 }
const chosen = [...new Set(Array.from({ length: picks }, (_, i) => leaves[Math.floor((i / Math.max(1, picks - 1)) * (leaves.length - 1))]))]
let maxMoved = 0, maxShift = 0
for (const leaf of chosen) {
const pid = leaf._data.id
const g = {
...graph,
persons: graph.persons.filter((p) => p.id !== pid),
unions: graph.unions.filter((u) => u.partner1Id !== pid && u.partner2Id !== pid),
filiations: graph.filiations.filter((f) => f.childId !== pid && f.parentId !== pid),
}
const after = layoutFn(g, referenceId === pid ? null : referenceId)
const common = after.children.filter((n) => before.has(n.id))
if (!common.length) continue
const shift = common.reduce((s, n) => s + (n.x - before.get(n.id)), 0) / common.length
const moved = common.filter((n) => Math.abs(n.x - before.get(n.id) - shift) > SEUIL).length
const far = Math.max(...common.map((n) => Math.abs(n.x - before.get(n.id) - shift)))
maxMoved = Math.max(maxMoved, moved)
maxShift = Math.max(maxShift, far)
}
return { feuilles: chosen.length, max_bouges: maxMoved, max_deplacement_px: Math.round(maxShift) }
}