Files
archicratie-edition/scripts/audit-glossary-navigation.mjs
Archicratia d0d5e03afb
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 38s
CI / build-and-anchors (pull_request) Successful in 38s
chore(glossaire): renforcer l'audit de gouvernance du graphe
2026-04-28 21:33:24 +02:00

156 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}