import fs from "fs"; 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 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 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"); const slug = file.replace(".md", ""); if (!raw.startsWith("---")) { entries.push({ slug, data: {}, noFrontmatter: true }); continue; } 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); } const seenPairs = new Set(); for (const [a, b] of Object.entries(edges)) { if (edges[b] === a) { const pair = [a, b].sort().join(" <-> "); if (!seenPairs.has(pair)) { seenPairs.add(pair); directCycles.push(`${a} <-> ${b}`); } } } 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) .forEach(([slug, n]) => { if (n > HUB_LIMIT) console.log(`⚠️ ${slug}: ${n}`); else console.log(` ${slug}: ${n}`); }); console.log("\n🔗 Checking dead primaryNext:"); 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)); } 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"); }