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:
parent
08dca05f63
commit
c3d1df30dc
2 changed files with 93 additions and 54 deletions
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue