From d0d5e03afbb2a7b16911abb0a00cc049be59bf62 Mon Sep 17 00:00:00 2001 From: Archicratia Date: Tue, 28 Apr 2026 21:33:24 +0200 Subject: [PATCH] chore(glossaire): renforcer l'audit de gouvernance du graphe --- scripts/audit-glossary-navigation.mjs | 149 ++++++++++++++++++++------ 1 file changed, 119 insertions(+), 30 deletions(-) diff --git a/scripts/audit-glossary-navigation.mjs b/scripts/audit-glossary-navigation.mjs index 098000f..f106917 100644 --- a/scripts/audit-glossary-navigation.mjs +++ b/scripts/audit-glossary-navigation.mjs @@ -3,65 +3,154 @@ import path from "path"; import yaml from "js-yaml"; const ROOT = "src/content/glossaire"; +const DEFAULTS_FILE = "src/lib/glossary-navigation-defaults.ts"; +const HUB_LIMIT = 5; -const files = fs.readdirSync(ROOT).filter(f => f.endsWith(".md")); +const defaultsRaw = fs.readFileSync(DEFAULTS_FILE, "utf-8"); -const slugs = new Set(files.map(f => f.replace(".md", ""))); +const defaultFamilies = new Set( + [...defaultsRaw.matchAll(/^\s{4}"?([a-z0-9-]+)"?\s*:/gm)].map((m) => m[1]), +); -let missingNavigation = []; -let edges = {}; -let incoming = {}; +const files = fs.readdirSync(ROOT).filter((f) => f.endsWith(".md")); +const slugs = new Set(files.map((f) => f.replace(".md", ""))); + +const entries = []; for (const file of files) { const full = path.join(ROOT, file); const raw = fs.readFileSync(full, "utf-8"); - - if (!raw.startsWith("---")) continue; - - const data = yaml.load(raw.split("---")[1]) || {}; const slug = file.replace(".md", ""); - if (!data.navigation) { - missingNavigation.push(slug); + if (!raw.startsWith("---")) { + entries.push({ slug, data: {}, noFrontmatter: true }); + continue; } - const next = data?.navigation?.primaryNext; + const frontmatter = raw.split("---", 3)[1]; + const data = yaml.load(frontmatter) || {}; + entries.push({ slug, data, noFrontmatter: false }); +} + +const missingNavigation = []; +const missingReason = []; +const weakPaths = []; +const selfLoops = []; +const deadPrimaryNext = []; +const directCycles = []; +const edges = {}; +const incoming = {}; +const families = new Set(); + +for (const { slug, data, noFrontmatter } of entries) { + if (noFrontmatter) continue; + + if (data.family) families.add(data.family); + + const nav = data.navigation; + + if (!nav) { + missingNavigation.push(slug); + continue; + } + + const next = nav.primaryNext; + if (next) { edges[slug] = next; incoming[next] = (incoming[next] || 0) + 1; + + if (next === slug) selfLoops.push(slug); + if (!slugs.has(next)) deadPrimaryNext.push(`${slug} → ${next}`); + + if (!nav.primaryReason) missingReason.push(slug); } + + const paths = nav.paths || {}; + const pathCount = ["understand", "deepen", "compare", "apply"].filter( + (key) => Array.isArray(paths[key]) && paths[key].length > 0, + ).length; + + if (pathCount < 2) weakPaths.push(slug); } -// šŸ” 1. Fiches sans navigation -if (missingNavigation.length > 0) { - console.log("\nāŒ Missing navigation:"); - missingNavigation.forEach(s => console.log(" -", s)); -} +const seenPairs = new Set(); -// šŸ” 2. Cycles directs -console.log("\nšŸ” Direct cycles:"); for (const [a, b] of Object.entries(edges)) { if (edges[b] === a) { - console.log(` - ${a} <-> ${b}`); + const pair = [a, b].sort().join(" <-> "); + if (!seenPairs.has(pair)) { + seenPairs.add(pair); + directCycles.push(`${a} <-> ${b}`); + } } } -// šŸ” 3. Hubs +const missingDefaults = [...families].filter((f) => !defaultFamilies.has(f)); + +const bigHubs = Object.entries(incoming) + .filter(([, count]) => count > HUB_LIMIT) + .sort((a, b) => b[1] - a[1]); + +console.log("\nšŸ” Glossary navigation audit"); + +if (missingNavigation.length > 0) { + console.log("\nāŒ Missing navigation:"); + missingNavigation.forEach((s) => console.log(" -", s)); +} + +console.log("\nšŸ” Direct cycles:"); +if (directCycles.length) directCycles.forEach((c) => console.log(" -", c)); +else console.log(" (none)"); + console.log("\nšŸ“Š Top hubs:"); Object.entries(incoming) - .sort((a,b) => b[1]-a[1]) - .slice(0,10) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) .forEach(([slug, n]) => { - if (n > 5) console.log(`āš ļø ${slug}: ${n}`); + if (n > HUB_LIMIT) console.log(`āš ļø ${slug}: ${n}`); else console.log(` ${slug}: ${n}`); }); -// šŸ” 4. Slugs morts console.log("\nšŸ”— Checking dead primaryNext:"); -for (const [a,b] of Object.entries(edges)) { - if (!slugs.has(b)) { - console.log(`āŒ ${a} → ${b} (missing)`); - } +if (deadPrimaryNext.length) deadPrimaryNext.forEach((x) => console.log("āŒ", x)); +else console.log(" (none)"); + +if (missingDefaults.length) { + console.log("\nāŒ Families without defaults:"); + missingDefaults.forEach((f) => console.log(" -", f)); } -console.log("\nāœ… Audit done"); \ No newline at end of file +if (bigHubs.length) { + console.log(`\nāš ļø Hubs above limit (${HUB_LIMIT}):`); + bigHubs.forEach(([slug, n]) => console.log(` - ${slug}: ${n}`)); +} + +if (missingReason.length) { + console.log("\nāš ļø Missing primaryReason:"); + missingReason.forEach((s) => console.log(" -", s)); +} + +if (weakPaths.length) { + console.log("\nāš ļø Weak path coverage (<2):"); + weakPaths.forEach((s) => console.log(" -", s)); +} + +if (selfLoops.length) { + console.log("\nāŒ Self-referencing primaryNext:"); + selfLoops.forEach((s) => console.log(" -", s)); +} + +const hardFailures = + missingNavigation.length + + directCycles.length + + deadPrimaryNext.length + + missingDefaults.length + + selfLoops.length; + +if (hardFailures > 0) { + console.log(`\nāŒ Audit failed: ${hardFailures} hard issue(s)`); + process.exitCode = 1; +} else { + console.log("\nāœ… Audit done"); +} \ No newline at end of file