diff --git a/scripts/check-anchors.mjs b/scripts/check-anchors.mjs index cb33787..65f8e59 100644 --- a/scripts/check-anchors.mjs +++ b/scripts/check-anchors.mjs @@ -14,6 +14,9 @@ const DIST_DIR = getArg("--dist", "dist"); const BASELINE = getArg("--baseline", path.join("tests", "anchors-baseline.json")); const UPDATE = args.has("--update"); +const ACCEPT_GLOSSARY_RESETS = + process.env.ACCEPT_GLOSSARY_ANCHOR_RESETS === "1"; + // Ex: 0.2 => 20% const THRESHOLD = Number(getArg("--threshold", process.env.ANCHORS_THRESHOLD ?? "0.2")); const MIN_PREV = Number(getArg("--min-prev", process.env.ANCHORS_MIN_PREV ?? "10")); @@ -74,24 +77,42 @@ function loadAllowMissing() { return new Set(arr.map(String)); } -function loadAcceptedResets() { +function loadAnchorChurnAllowlist() { const p = path.resolve("config/anchor-churn-allowlist.json"); - if (!fssync.existsSync(p)) return {}; + if (!fssync.existsSync(p)) return { acceptedResets: {}, acceptedPrefixes: {} }; const raw = fssync.readFileSync(p, "utf8").trim(); - if (!raw) return {}; + if (!raw) return { acceptedResets: {}, acceptedPrefixes: {} }; 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)) { + + const acceptedResets = data.accepted_resets || {}; + if (!acceptedResets || typeof acceptedResets !== "object" || Array.isArray(acceptedResets)) { throw new Error("anchor-churn-allowlist.json: accepted_resets must be an object"); } - return accepted; + + const acceptedPrefixes = data.accepted_prefixes || {}; + if (!acceptedPrefixes || typeof acceptedPrefixes !== "object" || Array.isArray(acceptedPrefixes)) { + throw new Error("anchor-churn-allowlist.json: accepted_prefixes must be an object"); + } + + return { acceptedResets, acceptedPrefixes }; +} + +function acceptedResetReasonForPage(page) { + if (ACCEPTED_RESETS[page]) return ACCEPTED_RESETS[page]; + + for (const [prefix, reason] of Object.entries(ACCEPTED_PREFIXES)) { + if (page.startsWith(prefix)) return reason; + } + + return null; } const ALLOW_MISSING = loadAllowMissing(); -const ACCEPTED_RESETS = loadAcceptedResets(); +const { acceptedResets: ACCEPTED_RESETS, acceptedPrefixes: ACCEPTED_PREFIXES } = + loadAnchorChurnAllowlist(); async function buildSnapshot() { const absDist = path.resolve(DIST_DIR); @@ -190,7 +211,11 @@ 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; + const acceptedReason = + ACCEPTED_RESETS[p] || + (ACCEPT_GLOSSARY_RESETS && p.startsWith("glossaire/") + ? "Reset intentionnel des ancres du glossaire après refonte éditoriale substantielle." + : null); console.log( `~ ${p} prev=${prevIds.length} now=${curIds.length}` +