The click-sheet verifier had not run in months, and was aimed at the wrong files

It crashed on ENOENT — app/doxa-maps/gospel-resources/assets/tables/, a tree
deleted when data moved out of public/. Nothing noticed, because nothing ran it:
it had no npm script, so it was neither discoverable nor wired anywhere.

The dead path was the smaller half. Its other input was a private by-country
copy under plugin-designs/, and its people-group table was the curated 2,106-row
subset that resources-map.vue stopped fetching some time ago — it says so in its
own comments; the country sheet is built from the 12,379-pin index now. Repaired
in place it would have gone green about a surface it was no longer looking at.

Both inputs are the live tables. The country join is ISO3 instead of a substring
test against a comma-joined name blob, which the file's own comment admitted was
under-counting because country names contain commas. It reads 242 of 242
countries resolving to a pin and 8,435 sheet links, through the same flattener
the sheet renders from, and its self-test now requires four corruptions to go
red rather than two.

Also gives it and the stale-sweep gate an npm script, and corrects the link-rot
entry, which still told readers that jesusfilm.org and fivefish.org can never be
status-checked — the thing that gate stopped believing two commits ago.
This commit is contained in:
doxa-maps-88655c70 2026-09-25 03:33:31 +00:00
parent 08dca05f63
commit c3d1df30dc
2 changed files with 93 additions and 54 deletions

View file

@ -159,9 +159,11 @@
"check:snapshot-guard:poison": "node internal/check-snapshot-guard.mjs --poison",
"check:coverage-consistent": "node internal/check-gospel-coverage-consistent.mjs # the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
"check:coverage-consistent:poison": "node internal/check-gospel-coverage-consistent.mjs --poison",
"check:click-sheet": "node plugin-designs/gospel-resources/scripts/verify-gospel-click-sheet.mjs # the COUNTRY-CLICK SHEET without a GPU: every country in the roster resolves to >=1 pin, every link the sheet would render is absolute, and has_audio stays the STRING \"unknown\" where the pool was never checked (collapsing it to false would read as \"no audio exists\"). It had been CRASHING on a path deleted when data moved out of public/, and its two inputs were a private by-country copy and the curated 2,106-row subset the sheet stopped reading — so it was unrunnable AND aimed at files the surface no longer uses. Joins by ISO3 now, not by country-name substring. Carries its own 4-case self-test. Static, sub-second.",
"check:stale-sweep-claim": "node internal/check-no-stale-sweep-claim.mjs # a caveat that has stopped being true is a wrong number. The gospel surfaces carry a paragraph saying which pools were searched; while the GRN sweep ran they truthfully said part of it was unread, the sweep finished, and the sentence stayed for four more commits. The DATA decides here: undecided gap codes mean the claim is allowed, all-decided means it must go, so it re-arms itself for the next slow pool. Does NOT flag a claim rendered under a v-if on the pending count — that form expires on its own. Self-test: `--poison`. Static, sub-second.",
"check:surfaces-read-the-tier": "node internal/check-surfaces-read-the-tier.mjs # a surface may READ the tier the pin index computed, never re-derive 'this group has nothing' from the raw resource fields. That hand-rolled field list went stale every time a source was added: 7 codes drawn as having nothing while the map showed them a link, then 28 when Scripture Earth landed. Static, sub-second. Self-test: `bun run check:surfaces-read-the-tier:poison` re-injects the old formula into every surface and each must be caught.",
"check:surfaces-read-the-tier:poison": "node internal/check-surfaces-read-the-tier.mjs --poison",
"check:link-rot": "node internal/check-link-rot-sample.mjs # re-tests a stratified sample of links we already VERIFIED, because nothing else ever re-tests one: a publisher can retire a page and the map keeps offering it, still labelled 'link checked'. Only hosts whose HTTP status is real evidence are sampled — jesusfilm.org, live.bible.is, knowgod.com and fivefish.org never 404, so status-checking them would report perfect health forever. Three impossible URLs run in band every time; if one answers 2xx it reports PROVED NOTHING instead of a pass. ~60s, network.",
"check:link-rot": "node internal/check-link-rot-sample.mjs # re-tests a stratified sample of links we already VERIFIED, because nothing else ever re-tests one: a publisher can retire a page and the map keeps offering it, still labelled 'link checked'. THREE BANDS, each measured rather than assumed, each with its controls in band every run: plain STATUS where a status is evidence (jesusfilm.org among them — it does 404 on the /watch/jesus/<slug> shape the map ships, and the old exclusion was inherited from the retired ?languageId= URL); API-MAPPED for live.bible.is, whose page answers 200 for any fileset while /api/bibles/<id> answers 404; and REDIRECT-LANDING for fivefish.org, where a published code lands on its numbered language page and an unknown one is swept to /regions. If a control misbehaves it reports PROVED NOTHING rather than a pass. Only knowgod.com is left out — byte-identical page for any URL; the verifier proves those through Cru's API. ~3 minutes, network.",
"check:embed-basemap": "node internal/check-embed-basemap-paints.mjs # the gospel map's BASEMAP must paint inside a genuinely cross-origin iframe on the token the page BAKES, not one a probe forces on it. Every other browser gate here forces a restricted token (u=doxalife) that 403s on the basemap, then counts PINS — which come from our own GeoJSON and paint with no token at all — so 'the map renders' had never been measured. Runs a negative control every time by rewriting the token on the wire; if the control passes it reports PROVED NOTHING rather than a pass. ~60s, needs a browser.",
"check:synced-css:self-test": "node internal/check-synced-css-copies.mjs --self-test",
"serve:permissive": "python3 -m http.server 4173 --bind 127.0.0.1 --directory public # LOOPBACK ONLY. This was --bind 0.0.0.0, which published the built tree on every interface of whatever machine ran a gate; measured on the build VPS it answered HTTP 200 on the box's PUBLIC IP while a browser gate was running. Every consumer (verify-browser-all, check:a11y-button-names, check:nav-reachable, smoke:share-all) already defaults to 127.0.0.1, so nothing needed the wider bind. Override with BASE_URL/BASE if you deliberately want to reach it from another host.",

View file

@ -1,6 +1,6 @@
// verify-gospel-click-sheet.mjs — prove the COUNTRY-CLICK SHEET is correct without a GPU.
//
// node plugin-designs/gospel-resources/scripts/verify-gospel-click-sheet.mjs # exits 1 on any failure
// node plugin-designs/gospel-resources/scripts/verify-gospel-click-sheet.mjs # exits 1 on failure
//
// ═══ WHY THIS EXISTS ════════════════════════════════════════════════════════
// Clicking a country on the gospel map needs Mapbox TILES (WebGL), which headless
@ -8,70 +8,107 @@
// But the click is two separate things:
// 1. THE SHEET CONTENT what a click RESOLVES TO <- verifiable, and this does it
// 2. THE TAP -> OPEN the GPU-rendered gesture <- needs a real device
// Leaving BOTH unverified because one is impossible is the lazy read (same split as
// verify-map-paint.mjs). This closes half the gap and NAMES the half that remains.
// Leaving BOTH unverified because one is impossible is the lazy read. This closes half
// the gap and NAMES the half that remains.
//
// WHAT IT ASSERTS, for every country the map can paint:
// - the iso3 the click handler looks up EXISTS in the country roster (byIso hit)
// - that country resolves to >=1 people-group row (the sheet would not be empty)
// - every resource link rendered is a real absolute URL, never fabricated
// - has_audio is the STRING 'unknown' where the pool was never checked - never
// ═══ IT HAD STOPPED RUNNING AT ALL, AND THEN IT WAS CHECKING THE WRONG FILE ═
// Found 2026-09-25, by running it. It crashed with ENOENT on
// app/doxa-maps/gospel-resources/assets/tables/people-group-resources.json
// a tree deleted when data moved out of public/. A gate that cannot start proves nothing,
// and nothing noticed, because nothing runs it.
//
// The dead path was the smaller half. Its other input was a private copy of by-country
// under plugin-designs/, and its people-group table was the CURATED 2,106-row subset —
// which resources-map.vue stopped fetching some time ago, saying so in its own comments:
// the country sheet is built from the PIN INDEX now, 12,379 groups. So even repaired, it
// would have verified two files the sheet no longer reads, and reported green about a
// surface it was no longer looking at. Both inputs are now the live ones.
//
// ═══ THE JOIN IS ISO3, NOT A NAME ═══════════════════════════════════════════
// The old country match was a SUBSTRING test against a comma-joined name blob, with a
// comment explaining that country names contain commas so splitting under-counted. That
// is the shape of bug this bundle refuses everywhere else: names collide, ISO codes do
// not. The pin index carries iso3 per pin, so the join is exact.
//
// WHAT IT ASSERTS:
// - every country in the roster resolves to >=1 pin (the sheet would not be empty)
// - every URL the sheet would render for those groups is absolute — never fabricated
// - has_audio stays the STRING 'unknown' where the pool was never checked, never
// collapsed to false, which would read as "no audio exists" (persecution-critical)
//
// PROVEN TO GO RED — see the SELF-TEST at the bottom: it corrupts each invariant in
// memory and asserts the check fails. A check that has never failed proves nothing.
// PROVEN TO GO RED — the SELF-TEST corrupts each invariant in memory and requires the
// check to fail. A check that has never failed proves nothing.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { resourceRows } from '../gospel-resource-rows.js'
const __dirname = dirname(fileURLToPath(import.meta.url));
const A = join(__dirname, '..', '..', '..', 'app', 'doxa-maps', 'gospel-resources', 'assets', 'tables');
const P = join(__dirname, '..', '..', '..', 'plugin-designs', 'gospel-resources', 'data');
const HERE = dirname(fileURLToPath(import.meta.url))
const T = join(HERE, '..', '..', '..', 'data', 'tables')
const load = (p) => JSON.parse(readFileSync(p, 'utf8'));
const countries = (() => { const d = load(join(P, 'by-country.json')).countries; return Array.isArray(d) ? d : Object.values(d); })();
const pgRows = load(join(A, 'people-group-resources.json')).rows;
const load = (p) => JSON.parse(readFileSync(join(T, p), 'utf8'))
const roster = load('gospel-by-country.json').countries
const { codes, pins } = load('people-group-pins.json')
/* Still shipped as the "Resources by People Group" TAB, so its URLs are still worth
checking — but it is the table, not the sheet, and this no longer confuses the two. */
const legacyRows = load('people-group-resources.json').rows
function check(rows, roster) {
const fail = [];
// NOTE: `countries` is a COMMA-joined string, but country NAMES themselves contain
// commas ("Micronesia, Federated States of"). Splitting on comma UNDER-COUNTS and
// reported a false 167/170. Match by SUBSTRING against the whole field instead —
// that is also what a UI filter would do. (pm-dx 2026-08-02, caught by cross-check.)
const blob = rows.map((r) => String(r.countries || ''));
let painted = 0, resolved = 0;
for (const c of roster) {
if (!c.iso3) { fail.push(`country with no iso3: ${c.country}`); continue; }
painted++;
if (blob.some((b) => b.includes(c.country))) resolved++;
else fail.push(`country paints but resolves to ZERO people groups: ${c.country} (${c.iso3})`);
const ABS = (v) => v == null || v === '' || /^https?:\/\//.test(String(v))
function check(rosterIn, pinsIn, codesIn, legacyIn) {
const fail = []
const byIso = new Set()
for (const p of pinsIn) if (p[3]) byIso.add(p[3])
let painted = 0, resolved = 0
for (const c of rosterIn) {
if (!c.iso3) { fail.push(`country with no iso3: ${c.country}`); continue }
painted++
if (byIso.has(c.iso3)) resolved++
else fail.push(`country paints but resolves to ZERO pins: ${c.country} (${c.iso3})`)
}
for (const r of rows) {
for (const k of ['scripture_url', 'film_url']) {
const v = r[k];
if (v != null && v !== '' && !/^https?:\/\//.test(String(v)))
fail.push(`${r.people_group}: ${k} is not an absolute URL: ${JSON.stringify(v)}`);
/* Every link the sheet would actually render, through the one flattener the sheet uses. */
let links = 0
for (const [code, c] of Object.entries(codesIn)) {
for (const r of resourceRows(c)) {
links++
if (!ABS(r.url)) fail.push(`${code}: ${r.type} link is not an absolute URL: ${JSON.stringify(r.url)}`)
}
if (r.has_audio === false && r.checked_pools && !String(r.checked_pools).includes('GRN'))
fail.push(`${r.people_group}: has_audio===false although GRN was never checked (must be the STRING 'unknown')`);
}
return { painted, resolved, fail };
for (const r of legacyIn) {
for (const k of ['scripture_url', 'film_url']) {
if (!ABS(r[k])) fail.push(`${r.people_group}: ${k} is not an absolute URL: ${JSON.stringify(r[k])}`)
}
if (r.has_audio === false && r.checked_pools && !/\bgrn\b/i.test(String(r.checked_pools))) {
fail.push(`${r.people_group}: has_audio===false although GRN was never checked (must be the STRING 'unknown')`)
}
}
return { painted, resolved, links, fail }
}
const res = check(pgRows, countries);
console.log(`[click-sheet] countries in roster: ${res.painted}`);
console.log(`[click-sheet] countries resolving to >=1 people group: ${res.resolved}`);
console.log(`[click-sheet] link/honesty violations: ${res.fail.length}`);
res.fail.slice(0, 5).forEach((f) => console.log(' -', f));
const res = check(roster, pins, codes, legacyRows)
console.log(`[click-sheet] countries in roster: ${res.painted}`)
console.log(`[click-sheet] countries resolving to >=1 pin: ${res.resolved}`)
console.log(`[click-sheet] resource links the sheet would render: ${res.links}`)
console.log(`[click-sheet] link/honesty violations: ${res.fail.length}`)
res.fail.slice(0, 5).forEach((f) => console.log(' -', f))
// ── SELF-TEST: the guard must go RED on each corrupted invariant ──────────────
const bad1 = check([{ ...pgRows[0], scripture_url: 'not-a-url' }], countries);
const bad2 = check([{ ...pgRows[0], has_audio: false, checked_pools: 'eBible' }], countries);
const redOk = bad1.fail.length > 0 && bad2.fail.length > 0;
console.log(`[click-sheet] SELF-TEST (guard goes RED on corruption): ${redOk ? 'PASS' : 'FAIL'}`);
const firstCode = Object.keys(codes)[0]
const bad = [
['relative sheet link', check(roster, pins, { x: { l: 'X', s: 'not-a-url' } }, [])],
['legacy relative url', check(roster, pins, {}, [{ ...legacyRows[0], scripture_url: 'not-a-url' }])],
['audio collapsed to false', check(roster, pins, {}, [{ ...legacyRows[0], has_audio: false, checked_pools: 'ebible' }])],
['country resolving to no pin', check([...roster, { iso3: 'ZZZ', country: 'Nowhere' }], pins, {}, [])],
]
const redOk = bad.every(([, r]) => r.fail.length > 0)
for (const [what, r] of bad) if (!r.fail.length) console.log(` ! SELF-TEST did not catch: ${what}`)
console.log(`[click-sheet] SELF-TEST (guard goes RED on ${bad.length} corruptions): ${redOk ? 'PASS' : 'FAIL'}`)
const green = res.fail.length === 0 && res.resolved > 0 && redOk;
console.log(`[click-sheet] ${green ? 'GREEN' : 'RED'}`);
console.log('[click-sheet] NOT VERIFIED HERE: the tap->open gesture on a GPU-rendered map. Needs a real device.');
process.exit(green ? 0 : 1);
const green = res.fail.length === 0 && res.resolved > 0 && res.links > 0 && redOk
console.log(`[click-sheet] ${green ? 'GREEN' : 'RED'} (first code sampled: ${firstCode})`)
console.log('[click-sheet] NOT VERIFIED HERE: the tap->open gesture on a GPU-rendered map. Needs a real device.')
process.exit(green ? 0 : 1)