#!/usr/bin/env node /** * Consistency check: confirms the number of upcoming meetups reported by the * live /events page, the /events.md mirror, and the /llms.txt summary all match. * * Catches the exact "confidently wrong" drift this check exists to prevent: * a mirror or summary going stale relative to the rendered page. * * Usage: * node scripts/check-consistency.mjs [baseUrl] * BASE_URL=https://belgianbitcoinembassy.org node scripts/check-consistency.mjs * * Exits 0 if all three agree, 1 on any mismatch or fetch failure. */ const baseUrl = ( process.argv[2] || process.env.BASE_URL || "http://localhost:3000" ).replace(/\/$/, ""); async function getText(path) { const url = `${baseUrl}${path}`; const res = await fetch(url, { headers: { "User-Agent": "bbe-consistency-check" }, }); if (!res.ok) { throw new Error(`GET ${url} -> ${res.status}`); } return res.text(); } /** /llms.txt: "There are currently N upcoming meetup(s) scheduled." */ function parseLlmsTxt(text) { const m = text.match(/currently\s+(\d+)\s+upcoming meetup/i); if (!m) throw new Error("llms.txt: could not find upcoming-meetup count line"); return Number(m[1]); } /** /events.md: count "### " headings within the "## Upcoming" section. */ function parseEventsMd(text) { const lines = text.split("\n"); let inUpcoming = false; let count = 0; let sawUpcoming = false; for (const line of lines) { if (/^##\s+Upcoming\b/.test(line)) { inUpcoming = true; sawUpcoming = true; continue; } if (/^##\s+/.test(line) && inUpcoming) inUpcoming = false; // next section if (inUpcoming && /^###\s+/.test(line)) count += 1; } if (!sawUpcoming) throw new Error("events.md: no '## Upcoming' section found"); return count; } /** /events: data-upcoming-count attribute rendered into the HTML. */ function parseEventsHtml(text) { const m = text.match(/data-upcoming-count="(\d+)"/); if (!m) throw new Error("events: data-upcoming-count attribute not found in HTML"); return Number(m[1]); } async function main() { const [llmsTxt, eventsMd, eventsHtml] = await Promise.all([ getText("/llms.txt"), getText("/events.md"), getText("/events"), ]); const counts = { "/llms.txt": parseLlmsTxt(llmsTxt), "/events.md": parseEventsMd(eventsMd), "/events": parseEventsHtml(eventsHtml), }; const values = Object.values(counts); const allMatch = values.every((v) => v === values[0]); console.log(`Consistency check against ${baseUrl}`); for (const [name, value] of Object.entries(counts)) { console.log(` ${name.padEnd(12)} upcoming = ${value}`); } if (!allMatch) { console.error( `\nāœ— MISMATCH: upcoming-meetup counts disagree (${values.join(", ")}).`, ); process.exit(1); } console.log(`\nāœ“ All sources agree: ${values[0]} upcoming meetup(s).`); } main().catch((err) => { console.error(`āœ— Consistency check failed: ${err.message}`); process.exit(1); });