Files
archicratie-edition/scripts/audit-glossary-navigation.mjs
Archicratia afa4d84997
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 38s
CI / build-and-anchors (pull_request) Successful in 40s
chore(glossaire): auditer la convergence effective du graphe
2026-04-30 10:37:07 +02:00

232 lines
6.4 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 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");
}