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 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(); 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", ""))); 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(); 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; 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; addEffectiveEdge(slug, next); if (next === slug) selfLoops.push(slug); if (!slugs.has(next)) deadPrimaryNext.push(`${slug} → ${next}`); if (!nav.primaryReason) missingReason.push(slug); } const explicitPaths = nav.paths || {}; const familyDefaults = defaultPathKeysByFamily.get(data.family) || new Set(); const familyDefaultTargets = defaultTargetsByFamily.get(data.family) || new Map(); 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(); 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)); } 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 + 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"); }