Merge pull request 'chore(glossaire): renforcer l'audit de gouvernance du graphe' (#345) from chore/audit-gouvernance-graphe-v2 into main
Reviewed-on: #345
This commit was merged in pull request #345.
This commit is contained in:
@@ -3,65 +3,154 @@ import path from "path";
|
|||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
const ROOT = "src/content/glossaire";
|
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 = [];
|
const files = fs.readdirSync(ROOT).filter((f) => f.endsWith(".md"));
|
||||||
let edges = {};
|
const slugs = new Set(files.map((f) => f.replace(".md", "")));
|
||||||
let incoming = {};
|
|
||||||
|
const entries = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const full = path.join(ROOT, file);
|
const full = path.join(ROOT, file);
|
||||||
const raw = fs.readFileSync(full, "utf-8");
|
const raw = fs.readFileSync(full, "utf-8");
|
||||||
|
|
||||||
if (!raw.startsWith("---")) continue;
|
|
||||||
|
|
||||||
const data = yaml.load(raw.split("---")[1]) || {};
|
|
||||||
const slug = file.replace(".md", "");
|
const slug = file.replace(".md", "");
|
||||||
|
|
||||||
if (!data.navigation) {
|
if (!raw.startsWith("---")) {
|
||||||
missingNavigation.push(slug);
|
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) {
|
if (next) {
|
||||||
edges[slug] = next;
|
edges[slug] = next;
|
||||||
incoming[next] = (incoming[next] || 0) + 1;
|
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
|
const seenPairs = new Set();
|
||||||
if (missingNavigation.length > 0) {
|
|
||||||
console.log("\n❌ Missing navigation:");
|
|
||||||
missingNavigation.forEach(s => console.log(" -", s));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔍 2. Cycles directs
|
|
||||||
console.log("\n🔍 Direct cycles:");
|
|
||||||
for (const [a, b] of Object.entries(edges)) {
|
for (const [a, b] of Object.entries(edges)) {
|
||||||
if (edges[b] === a) {
|
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:");
|
console.log("\n📊 Top hubs:");
|
||||||
Object.entries(incoming)
|
Object.entries(incoming)
|
||||||
.sort((a,b) => b[1]-a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.slice(0,10)
|
.slice(0, 10)
|
||||||
.forEach(([slug, n]) => {
|
.forEach(([slug, n]) => {
|
||||||
if (n > 5) console.log(`⚠️ ${slug}: ${n}`);
|
if (n > HUB_LIMIT) console.log(`⚠️ ${slug}: ${n}`);
|
||||||
else console.log(` ${slug}: ${n}`);
|
else console.log(` ${slug}: ${n}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔍 4. Slugs morts
|
|
||||||
console.log("\n🔗 Checking dead primaryNext:");
|
console.log("\n🔗 Checking dead primaryNext:");
|
||||||
for (const [a,b] of Object.entries(edges)) {
|
if (deadPrimaryNext.length) deadPrimaryNext.forEach((x) => console.log("❌", x));
|
||||||
if (!slugs.has(b)) {
|
else console.log(" (none)");
|
||||||
console.log(`❌ ${a} → ${b} (missing)`);
|
|
||||||
}
|
if (missingDefaults.length) {
|
||||||
|
console.log("\n❌ Families without defaults:");
|
||||||
|
missingDefaults.forEach((f) => console.log(" -", f));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("\n✅ Audit done");
|
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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user