Compare commits
8 Commits
chore/gouv
...
chore/audi
| Author | SHA1 | Date | |
|---|---|---|---|
| afa4d84997 | |||
| bf0683bbb2 | |||
| c486a5c5eb | |||
| 82e0d5ba78 | |||
| 7910a3964a | |||
| d4df080f1d | |||
| d0d5e03afb | |||
| c3065e7116 |
31
docs/GLOSSARY-GRAPH-GOVERNANCE.md
Normal file
31
docs/GLOSSARY-GRAPH-GOVERNANCE.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Gouvernance du graphe du glossaire
|
||||
|
||||
## Lois actuelles vérifiées automatiquement
|
||||
|
||||
- Aucune fiche sans navigation.
|
||||
- Aucun `primaryNext` mort.
|
||||
- Aucun cycle direct.
|
||||
- Aucun `primaryNext` vers soi-même.
|
||||
- Aucune famille sans defaults.
|
||||
- Aucun hub `primaryNext` au-dessus du seuil 5.
|
||||
- Les paths effectifs tiennent compte des defaults famille.
|
||||
|
||||
## Liens `primaryNext` à surveiller qualitativement
|
||||
|
||||
- `contractualisme-hobbesien → droit-naturel-et-propriete`
|
||||
- `exception-souveraine → droit-naturel-et-propriete`
|
||||
- `regime-de-co-viabilite → gouvernance-des-communs`
|
||||
- `memoire-symbolique-et-instantaneite-computationnelle → meta-regime`
|
||||
- `scene-darchicration → co-viabilite`
|
||||
|
||||
## Critère de décision
|
||||
|
||||
Un `primaryNext` est bon s’il produit au moins l’un des effets suivants :
|
||||
|
||||
- déplier la notion ;
|
||||
- changer de niveau ;
|
||||
- rendre opératoire ;
|
||||
- mettre en tension ;
|
||||
- concrétiser.
|
||||
|
||||
Un `primaryNext` est faible s’il est seulement voisin, décoratif ou encyclopédique.
|
||||
@@ -3,65 +3,230 @@ 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 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 = [];
|
||||
let edges = {};
|
||||
let incoming = {};
|
||||
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");
|
||||
|
||||
if (!raw.startsWith("---")) continue;
|
||||
|
||||
const data = yaml.load(raw.split("---")[1]) || {};
|
||||
const slug = file.replace(".md", "");
|
||||
|
||||
if (!data.navigation) {
|
||||
missingNavigation.push(slug);
|
||||
if (!raw.startsWith("---")) {
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🔍 1. Fiches sans navigation
|
||||
if (missingNavigation.length > 0) {
|
||||
console.log("\n❌ Missing navigation:");
|
||||
missingNavigation.forEach(s => console.log(" -", s));
|
||||
}
|
||||
const seenPairs = new Set();
|
||||
|
||||
// 🔍 2. Cycles directs
|
||||
console.log("\n🔍 Direct cycles:");
|
||||
for (const [a, b] of Object.entries(edges)) {
|
||||
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:");
|
||||
Object.entries(incoming)
|
||||
.sort((a,b) => b[1]-a[1])
|
||||
.slice(0,10)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
.forEach(([slug, n]) => {
|
||||
if (n > 5) console.log(`⚠️ ${slug}: ${n}`);
|
||||
if (n > HUB_LIMIT) console.log(`⚠️ ${slug}: ${n}`);
|
||||
else console.log(` ${slug}: ${n}`);
|
||||
});
|
||||
|
||||
// 🔍 4. Slugs morts
|
||||
console.log("\n🔗 Checking dead primaryNext:");
|
||||
for (const [a,b] of Object.entries(edges)) {
|
||||
if (!slugs.has(b)) {
|
||||
console.log(`❌ ${a} → ${b} (missing)`);
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user