From afa4d8499795bb30e24333f66a1f3a46a00286e6 Mon Sep 17 00:00:00 2001 From: Archicratia Date: Thu, 30 Apr 2026 10:37:07 +0200 Subject: [PATCH] chore(glossaire): auditer la convergence effective du graphe --- scripts/audit-glossary-navigation.mjs | 92 +++++++++++++++++++++------ 1 file changed, 74 insertions(+), 18 deletions(-) diff --git a/scripts/audit-glossary-navigation.mjs b/scripts/audit-glossary-navigation.mjs index 33a6e39..1db0620 100644 --- a/scripts/audit-glossary-navigation.mjs +++ b/scripts/audit-glossary-navigation.mjs @@ -5,29 +5,43 @@ import yaml from "js-yaml"; const ROOT = "src/content/glossaire"; const DEFAULTS_FILE = "src/lib/glossary-navigation-defaults.ts"; const HUB_LIMIT = 5; +const EFFECTIVE_TOP_LIMIT = 10; +const PATH_KEYS = ["understand", "deepen", "compare", "apply"]; const defaultsRaw = fs.readFileSync(DEFAULTS_FILE, "utf-8"); const defaultFamilies = new Set( - [...defaultsRaw.matchAll(/^\s{4}"?([a-z0-9-]+)"?\s*:/gm)].map((m) => m[1]), - ); - - const defaultPathKeysByFamily = new Map(); - - for (const match of defaultsRaw.matchAll(/^\s{4}"?([a-z0-9-]+)"?\s*:\s*\{([\s\S]*?)^\s{4}\},/gm)) { - const family = match[1]; - const body = match[2]; - const keys = new Set(); - - for (const key of ["understand", "deepen", "compare", "apply"]) { - if (new RegExp(`\\b${key}\\s*:\\s*\\[[^\\]]+\\]`).test(body)) { - keys.add(key); - } - } - - defaultPathKeysByFamily.set(family, keys); + [...defaultsRaw.matchAll(/^\s{4}"?([a-z0-9-]+)"?\s*:/gm)].map((m) => m[1]), +); + +const defaultPathKeysByFamily = new Map(); +const defaultTargetsByFamily = new Map(); + +for (const match of defaultsRaw.matchAll( + /^\s{4}"?([a-z0-9-]+)"?\s*:\s*\{([\s\S]*?)^\s{4}\},/gm, +)) { + const family = match[1]; + const body = match[2]; + const keys = new Set(); + const targetsByKey = new Map(); + + for (const key of PATH_KEYS) { + const pathMatch = body.match(new RegExp(`\\b${key}\\s*:\\s*\\[([^\\]]*)\\]`)); + const targets = pathMatch + ? pathMatch[1] + .split(",") + .map((x) => x.trim().replace(/^["']|["']$/g, "")) + .filter(Boolean) + : []; + + if (targets.length > 0) keys.add(key); + targetsByKey.set(key, targets); } + defaultPathKeysByFamily.set(family, keys); + defaultTargetsByFamily.set(family, targetsByKey); +} + const files = fs.readdirSync(ROOT).filter((f) => f.endsWith(".md")); const slugs = new Set(files.map((f) => f.replace(".md", ""))); @@ -58,6 +72,19 @@ const edges = {}; const incoming = {}; const families = new Set(); +const effectiveOutgoing = new Map(); +const effectiveIncoming = new Map(); + +function addEffectiveEdge(from, to) { + if (!from || !to || from === to || !slugs.has(to)) return; + + if (!effectiveOutgoing.has(from)) effectiveOutgoing.set(from, new Set()); + if (!effectiveIncoming.has(to)) effectiveIncoming.set(to, new Set()); + + effectiveOutgoing.get(from).add(to); + effectiveIncoming.get(to).add(from); +} + for (const { slug, data, noFrontmatter } of entries) { if (noFrontmatter) continue; @@ -75,6 +102,7 @@ for (const { slug, data, noFrontmatter } of entries) { if (next) { edges[slug] = next; incoming[next] = (incoming[next] || 0) + 1; + addEffectiveEdge(slug, next); if (next === slug) selfLoops.push(slug); if (!slugs.has(next)) deadPrimaryNext.push(`${slug} → ${next}`); @@ -84,14 +112,24 @@ for (const { slug, data, noFrontmatter } of entries) { const explicitPaths = nav.paths || {}; const familyDefaults = defaultPathKeysByFamily.get(data.family) || new Set(); + const familyDefaultTargets = defaultTargetsByFamily.get(data.family) || new Map(); - const pathCount = ["understand", "deepen", "compare", "apply"].filter((key) => { + const pathCount = PATH_KEYS.filter((key) => { const explicit = Array.isArray(explicitPaths[key]) && explicitPaths[key].length > 0; const fromDefault = familyDefaults.has(key); return explicit || fromDefault; }).length; if (pathCount < 2) weakPaths.push(slug); + + for (const key of PATH_KEYS) { + const explicitTargets = Array.isArray(explicitPaths[key]) ? explicitPaths[key] : []; + const defaultTargets = familyDefaultTargets.get(key) || []; + + for (const target of [...explicitTargets, ...defaultTargets]) { + addEffectiveEdge(slug, target); + } + } } const seenPairs = new Set(); @@ -161,6 +199,24 @@ if (selfLoops.length) { selfLoops.forEach((s) => console.log(" -", s)); } +console.log(`\nšŸ“Š Effective convergence top ${EFFECTIVE_TOP_LIMIT}:`); +[...effectiveIncoming.entries()] + .map(([slug, sources]) => [slug, sources.size]) + .sort((a, b) => b[1] - a[1]) + .slice(0, EFFECTIVE_TOP_LIMIT) + .forEach(([slug, n]) => { + console.log(` ${n} ${slug}`); + }); + +console.log(`\nšŸ“Š Effective branching top ${EFFECTIVE_TOP_LIMIT}:`); +[...effectiveOutgoing.entries()] + .map(([slug, targets]) => [slug, targets.size]) + .sort((a, b) => b[1] - a[1]) + .slice(0, EFFECTIVE_TOP_LIMIT) + .forEach(([slug, n]) => { + console.log(` ${n} ${slug}`); + }); + const hardFailures = missingNavigation.length + directCycles.length +