#!/usr/bin/env node import fs from "node:fs/promises"; import path from "node:path"; const args = new Set(process.argv.slice(2)); const getArg = (name, fallback = null) => { const i = process.argv.indexOf(name); if (i >= 0 && process.argv[i + 1]) return process.argv[i + 1]; return fallback; }; const DIST_DIR = getArg("--dist", "dist"); const BASELINE = getArg("--baseline", path.join("tests", "anchors-baseline.json")); const UPDATE = args.has("--update"); const THRESHOLD = Number(getArg("--threshold", process.env.ANCHORS_THRESHOLD ?? "0.2")); const MIN_PREV = Number(getArg("--min-prev", process.env.ANCHORS_MIN_PREV ?? "10")); const pct = (x) => (Math.round(x * 1000) / 10).toFixed(1) + "%"; async function walk(dir) { const out = []; const entries = await fs.readdir(dir, { withFileTypes: true }); for (const ent of entries) { const p = path.join(dir, ent.name); if (ent.isDirectory()) out.push(...(await walk(p))); else if (ent.isFile() && ent.name.endsWith(".html")) out.push(p); } return out; } // Contrat : .reading p[id^="p-"] function extractIds(html) { if (!html.includes('class="reading"')) return []; const ids = []; const re = /
]*\sid="(p-[^"]+)"/g; let m; while ((m = re.exec(html))) ids.push(m[1]); const seen = new Set(); const uniq = []; for (const id of ids) { if (seen.has(id)) continue; seen.add(id); uniq.push(id); } return uniq; } async function buildSnapshot() { const absDist = path.resolve(DIST_DIR); const files = await walk(absDist); const snap = {}; for (const f of files) { const rel = path.relative(absDist, f).replace(/\\/g, "/"); const html = await fs.readFile(f, "utf-8"); const ids = extractIds(html); if (ids.length === 0) continue; snap[rel] = ids; } const ordered = {}; for (const k of Object.keys(snap).sort()) ordered[k] = snap[k]; return ordered; } async function readJson(p) { const s = await fs.readFile(p, "utf-8"); return JSON.parse(s); } async function writeJson(p, obj) { await fs.mkdir(path.dirname(p), { recursive: true }); await fs.writeFile(p, JSON.stringify(obj, null, 2) + "\n", "utf-8"); } function diffPage(prevIds, curIds) { const prev = new Set(prevIds); const cur = new Set(curIds); const added = curIds.filter((x) => !prev.has(x)); const removed = prevIds.filter((x) => !cur.has(x)); return { added, removed }; } (async () => { const snap = await buildSnapshot(); if (UPDATE) { await writeJson(BASELINE, snap); const pages = Object.keys(snap).length; const total = Object.values(snap).reduce((a, xs) => a + xs.length, 0); console.log(`OK baseline updated -> ${BASELINE}`); console.log(`Pages: ${pages}, Total paragraph IDs: ${total}`); process.exit(0); } let base; try { base = await readJson(BASELINE); } catch { console.error(`Baseline missing: ${BASELINE}`); console.error(`Run: node scripts/check-anchors.mjs --update`); process.exit(2); } const allPages = new Set([...Object.keys(base), ...Object.keys(snap)]); const pages = Array.from(allPages).sort(); let failed = false; let changedPages = 0; for (const p of pages) { const prevIds = base[p] || null; const curIds = snap[p] || null; if (!prevIds && curIds) { console.log(`+ PAGE ${p} (new) ids=${curIds.length}`); continue; } if (prevIds && !curIds) { console.log(`- PAGE ${p} (missing now) prevIds=${prevIds.length}`); failed = true; continue; } const { added, removed } = diffPage(prevIds, curIds); if (added.length === 0 && removed.length === 0) continue; changedPages += 1; const prevN = prevIds.length || 1; const churn = (added.length + removed.length) / prevN; const line = `~ ${p} prev=${prevIds.length} now=${curIds.length}` + ` +${added.length} -${removed.length} churn=${pct(churn)}`; console.log(line); if (removed.length) { 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 && removed.length / prevN > THRESHOLD) failed = true; } console.log(`\nSummary: pages compared=${pages.length}, pages changed=${changedPages}`); if (failed) { console.error(`FAIL: anchor churn above threshold (threshold=${pct(THRESHOLD)} minPrev=${MIN_PREV})`); process.exit(1); } console.log("OK: anchors stable within threshold"); })();