diff --git a/config/anchor-churn-allowlist.json b/config/anchor-churn-allowlist.json new file mode 100644 index 0000000..544a465 --- /dev/null +++ b/config/anchor-churn-allowlist.json @@ -0,0 +1,5 @@ +{ + "accepted_resets": { + "archicrat-ia/chapitre-1/index.html": "Reset intentionnel des ancres après révision doctrinale substantielle du chapitre 1. Site neuf, sans annotations ni compatibilité descendante à préserver." + } +} \ No newline at end of file diff --git a/scripts/check-anchors.mjs b/scripts/check-anchors.mjs index 99ae5bc..cb33787 100644 --- a/scripts/check-anchors.mjs +++ b/scripts/check-anchors.mjs @@ -74,7 +74,24 @@ function loadAllowMissing() { return new Set(arr.map(String)); } +function loadAcceptedResets() { + const p = path.resolve("config/anchor-churn-allowlist.json"); + if (!fssync.existsSync(p)) return {}; + const raw = fssync.readFileSync(p, "utf8").trim(); + if (!raw) return {}; + const data = JSON.parse(raw); + if (!data || typeof data !== "object" || Array.isArray(data)) { + throw new Error("anchor-churn-allowlist.json must be an object"); + } + const accepted = data.accepted_resets || {}; + if (!accepted || typeof accepted !== "object" || Array.isArray(accepted)) { + throw new Error("anchor-churn-allowlist.json: accepted_resets must be an object"); + } + return accepted; +} + const ALLOW_MISSING = loadAllowMissing(); +const ACCEPTED_RESETS = loadAcceptedResets(); async function buildSnapshot() { const absDist = path.resolve(DIST_DIR); @@ -139,6 +156,7 @@ function diffPage(prevIds, curIds) { let failed = false; let changedPages = 0; + let acceptedPages = 0; for (const p of pages) { const prevIds = base[p] || null; @@ -172,6 +190,7 @@ function diffPage(prevIds, curIds) { const prevN = prevIds.length || 1; const churn = (added.length + removed.length) / prevN; const removedRatio = removed.length / prevN; + const acceptedReason = ACCEPTED_RESETS[p] || null; console.log( `~ ${p} prev=${prevIds.length} now=${curIds.length}` + @@ -182,11 +201,23 @@ function diffPage(prevIds, curIds) { console.log(` removed: ${removed.slice(0, 20).join(", ")}${removed.length > 20 ? " …" : ""}`); } - if (prevIds.length >= MIN_PREV && churn > THRESHOLD) failed = true; - if (prevIds.length >= MIN_PREV && removedRatio > THRESHOLD) failed = true; + const exceeds = + (prevIds.length >= MIN_PREV && churn > THRESHOLD) || + (prevIds.length >= MIN_PREV && removedRatio > THRESHOLD); + + if (exceeds && acceptedReason) { + acceptedPages += 1; + console.log(` ✅ accepted reset: ${acceptedReason}`); + continue; + } + + if (exceeds) failed = true; } - console.log(`\nSummary: pages compared=${pages.length}, pages changed=${changedPages}`); + console.log( + `\nSummary: pages compared=${pages.length}, pages changed=${changedPages}, accepted resets=${acceptedPages}` + ); + if (failed) { console.error(`FAIL: anchor churn above threshold (threshold=${pct(THRESHOLD)} minPrev=${MIN_PREV})`); process.exit(1);