doxa-maps/internal/verify-gospel-map.mjs
doxa-maps-88655c70 e0fd61eb3e Gospel presentations in 94 languages, from Cru's API rather than its parked domain
Every pool here so far is Scripture, film, Bible video or a course. None is a GOSPEL
PRESENTATION — the short translated thing a person actually opens with someone. Cru
publishes those, in the same organisation that makes the JESUS Film, so the doctrine
question is settled the same way the brief settles it for the film. Only their gospel
tracts are included, from a fixed allow-list; the lessons and survey tools are not.

IT CLOSES NO GAP AND SAYS SO. All 94 of these languages already have something, so this
is depth rather than coverage and moves no tier. Tiers are byte-identical at
2901/4767/4711. It reaches 3,270 pins and 3.97bn people.

godtools.org, the obvious host, IS A PARKED DOMAIN. It answers 200 to everything with a
114-byte body, and rendered, every URL — including /zz/nosuchtool-xyzzy — reads
"godtools.org is parked free, courtesy of GoDaddy.com". Shipping links there would have
sent people to a GoDaddy ad.

The real front-end is knowgod.com, and it does serve per-language tracts: /en/kgp reads
"Knowing God Personally", /sw/kgp reads "Kumjua Mungu Kibinafsi". But ONLY once
rendered. By fetch it is a uniform 3,864-byte shell for every URL valid or not — the
third host in this bundle with that shape, after jesusfilm's old ?languageId= and
live.bible.is.

So existence is proved from Cru's own API and never from the page: a (language, tool)
pair exists exactly when the resource's latest-translations intersect that language's
translations. Checked against the browser before being trusted — en/kgp and sw/kgp both
intersect and both render real localised text, and the bogus pair does not intersect. In
band, the verifier's controls are now 7 negative all non-verified and 7 positive all
verified, with a bogus knowgod pair among the negatives. All 94 URLs verified.

Clause I keeps its closed set: Gospel presentation only on knowgod.com, and knowgod.com
only that type. Poison self-test still 12/12. Screenshot read: German shows six distinct
resource types, including "Gospel presentation - Knowing God Personally".
2026-09-24 13:51:56 +00:00

237 lines
12 KiB
JavaScript

#!/usr/bin/env node
/**
* verify-gospel-map.mjs — the gospel-resources map must render its pins and answer a tap.
*
* WHY THIS IS A GATE AND NOT A SCRATCH PROBE. Every browser check written for this bundle
* lived in _qa-cycle/, which is gitignored: a dozen probes that proved the pins render, the
* sheet opens with real links, and the deep link filters the table — all of which vanish
* with the working tree. check:coverage-consistent guards the DATA and cannot see the map.
* The repo has carded orphaned gates twice; this is the same loss one step earlier, a gate
* that was never committed at all.
*
* WHAT IT ASSERTS, against the SHIPPED tree through the production-parity server:
* 1. the element mounts and exposes window.__grState — a stale-SRI leaf silently refuses
* to execute its script, which looks exactly like a mount failure;
* 2. every pin in the source is RENDERED, not merely loaded;
* 3. tapping a pin opens a group whose language is NAMED and whose links are real URLs
* belonging to the screened hosts — never a fabricated or off-list link;
* 4. the gap filter actually filters: fewer features, and only tier 0 left;
* 5. the sheet's "every resource" link carries the group's own ISO code.
*
* ★ WAIT FOR IDLE BEFORE COUNTING. queryRenderedFeatures before first paint returns 0,
* which is indistinguishable from a layer that is not painting — the first version of this
* check reported a false negative on itself for exactly that reason.
*
* ★ AND THE HOST LIST IS THE DOCTRINE BOUNDARY, enforced here rather than trusted: only
* eBible/PNG.Bible/Door43/JESUS Film may appear. A link to an excluded publisher would be
* a doctrinal failure, not a broken link, so it fails this gate loudly.
*
* ★ IT STARTS ITS OWN SERVER. Browser gates in this repo rot because they need a server
* someone else remembered to start: verify:browser-all is invoked NOWHERE, build:unlocked
* runs only `bun run verify`, and verify-all.mjs skips anything marked browser:true unless
* --full is passed. The repo has already lost two gates that way — "both had rotted
* unnoticed precisely because the build chain has never run them". A gate with a manual
* precondition is a gate that will not run, so this one removes the precondition: if
* nothing answers, it spawns serve-strict on an ephemeral port and shuts it down after.
*
* An explicit BASE_URL or argv[2] is honoured as-is and never auto-served — a caller
* naming a host means it, and silently pointing at a different tree would be worse than
* failing.
*
* USAGE: node internal/verify-gospel-map.mjs [baseUrl] (no setup needed)
* Skips loudly (exit 0) only if the tree is not built, the way CI sees it.
*/
import { chromium } from 'playwright'
import { servesThisTree } from './_serves-this-tree.mjs'
import { spawn } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const BASE = (process.env.BASE_URL || process.argv[2] || 'http://127.0.0.1:4173').replace(/\/+$/, '')
const GL = ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl', '--no-sandbox']
// The screened publishers. Anything else is a doctrine failure, not a typo.
const ALLOWED = /^(https:\/\/)?([a-z0-9-]+\.)*(ebible\.org|png\.bible|alkitab\.pw|git\.door43\.org|jesusfilm\.org|arc\.gt|zume\.training|bible\.is|knowgod\.com)(\/|$)/i
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '..')
const EXPLICIT = !!(process.env.BASE_URL || process.argv[2])
let base = BASE
let server = null
// ADOPTING A SERVER REQUIRES PROVING IT SERVES *THIS* TREE.
// This used to accept anything that answered 200 on the default port, and that is not the
// same question. Measured: port 4173 was held by an unrelated python3 process belonging to
// another worktree, the gate adopted it, and it spent three runs reporting on a build that
// was not this one — first a PASS, then a mount timeout that looked exactly like a
// regression in code that had nothing to do with it.
// The fingerprint is the built bundle itself: if the bytes served differ from the bytes on
// disk, it is a different tree and must not be used.
const alive = async (b) => {
const r = await fetch(`${b}/doxa-maps/gospel-resources/resources-map/index.html`).catch(() => null)
if (!(r && r.ok)) return false
const tree = await servesThisTree(b, REPO)
if (tree.ok) return true
console.log(`GOSPEL-MAP: not adopting ${b} — ${tree.why}`)
return false
}
if (!(await alive(base))) {
if (EXPLICIT) {
console.log(`GOSPEL-MAP: SKIP — nothing served at ${base} (you named it explicitly, so none was started).`)
process.exit(0)
}
if (!existsSync(resolve(REPO, 'public/doxa-maps/gospel-resources/resources-map/index.html'))) {
console.log('GOSPEL-MAP: SKIP — no built tree. Run `bun run tree` (cheap) or a full build first.')
process.exit(0)
}
// Ephemeral port so two gates running at once cannot collide on 4173.
const port = 5100 + Math.floor(Math.random() * 800)
base = `http://127.0.0.1:${port}`
server = spawn(process.execPath, [resolve(HERE, 'serve-strict.mjs'), '--port', String(port), '--directory', resolve(REPO, 'public')], { stdio: 'ignore', detached: false })
const until = Date.now() + 15000
while (Date.now() < until && !(await alive(base))) await new Promise((r) => setTimeout(r, 250))
if (!(await alive(base))) {
try { server.kill() } catch (_) {}
console.error('GOSPEL-MAP: FAIL — could not start serve-strict for the check.')
process.exit(1)
}
console.log(`GOSPEL-MAP: started serve-strict on ${port} for this run.`)
}
const PAGE_URL = `${base}/doxa-maps/gospel-resources/resources-map/index.html`
const fails = []
let linkCount = 0
let exitCode = 0
const browser = await chromium.launch({ headless: true, args: GL })
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
const pageErrors = []
page.on('pageerror', (e) => pageErrors.push(String(e)))
try {
await page.goto(PAGE_URL, { waitUntil: 'networkidle', timeout: 60000 })
// 1 — mounted. A stale-SRI leaf never executes, and that reads as "did not mount".
await page.waitForFunction(() => typeof window.__grState === 'function', { timeout: 45000 })
// ★ idle FIRST: counting before first paint returns 0 and looks like a dead layer.
await page.evaluate(() => new Promise((r) => {
const m = window.__grMap
m.loaded() ? r() : m.once('idle', r)
setTimeout(r, 8000)
}))
const st = await page.evaluate(() => window.__grState())
if (!st.hasLayer || !st.hasSource) fails.push('pin layer or source missing')
// 2 — rendered, not merely loaded
if (!st.pins || st.rendered !== st.pins) {
fails.push(`${st.rendered} of ${st.pins} pins rendered`)
}
// 3 — a tap answers, with a named language and real links
const opened = await page.evaluate(() => {
const f = window.__grMap.getSource('gr-pin-src')._data.features
for (const x of f) {
window.__grOpenPin(x.properties.i)
const g = window.__grState().group
if (g && (g.scripture_url || g.film_url)) {
return { pg: g.people_group, lang: g.language, iso: g.iso639_3, s: g.scripture_url, f: g.film_url }
}
}
return null
})
if (!opened) fails.push('no pin opened a group carrying a link')
else if (!String(opened.lang || '').trim()) fails.push(`${opened.pg} opened with no language name`)
// ★ EVERY link in the index the page loaded, not just the one group we opened. The
// first version checked only the opened group's two URLs — a sample of 1 out of 2,685
// codes — so an excluded publisher injected anywhere else passed cleanly. MEASURED: a
// jw.org URL planted in the served data did NOT fail this gate until the scan covered
// the whole index. The doctrine boundary is the one thing here that must never be
// spot-checked: a single off-list link is a doctrinal failure, not a broken link.
const idxRes = await fetch(`${base}/doxa-maps/assets/tables/people-group-pins.json`)
if (!idxRes.ok) fails.push(`pin index not served (${idxRes.status})`)
else {
const idx = await idxRes.json()
const bad = []
let scanned = 0
for (const [c, v] of Object.entries(idx.codes || {})) {
for (const u of [v.s, v.f, v.z, v.za && v.za.u, v.au && v.au.u, v.bv && v.bv.u, v.gp && v.gp.u, ...(v.d || []).map((d) => d.u)]) {
if (!u) continue
scanned++
if (!ALLOWED.test(u)) bad.push(`${c} -> ${u}`)
}
}
if (!scanned) fails.push('pin index carries no links at all — this check would pass vacuously')
if (bad.length) fails.push(`${bad.length} link(s) on an unscreened publisher (e.g. ${bad[0]})`)
linkCount = scanned
}
// 5 — the table deep link carries this group's own code
await page.waitForTimeout(400)
const href = await page.evaluate(() => {
const walk = (r) => {
for (const el of r.querySelectorAll('*')) {
const h = el.getAttribute && el.getAttribute('href')
if (el.tagName === 'A' && h && /data-tables/.test(h)) return h
if (el.shadowRoot) { const f = walk(el.shadowRoot); if (f) return f }
}
return null
}
return walk(document)
})
if (opened?.iso && (!href || !href.includes(`iso=${opened.iso}`))) {
fails.push(`table link does not carry iso=${opened.iso} (got ${href})`)
}
// 4 — the gap filter filters
const filtered = await page.evaluate(async () => {
const click = (i) => {
const walk = (r) => {
const b = [...r.querySelectorAll('.gm-kbtn')]
if (b[i]) { b[i].click(); return true }
for (const el of r.querySelectorAll('*')) if (el.shadowRoot && walk(el.shadowRoot)) return true
return false
}
return walk(document)
}
const m = window.__grMap
const before = m.queryRenderedFeatures({ layers: ['gr-pins'] }).length
click(2)
// ★ WAIT FOR IDLE, not a fixed sleep. setFilter is applied asynchronously and the
// layer re-renders after it; querying on a 900ms timer read the layer MID-TRANSITION
// and still saw tier 1 and 2 features. MEASURED at 1 failure in 3 runs — a gate that
// fails a third of the time on correct code is a gate that gets switched off, which
// is worse than not having written it. The file's own header says to wait for idle
// before counting; clause 4 was not doing it.
await new Promise((r) => { m.once('idle', r); setTimeout(r, 6000) })
const after = m.queryRenderedFeatures({ layers: ['gr-pins'] })
return { before, after: after.length, tiers: [...new Set(after.map((f) => f.properties.tier))] }
})
if (!(filtered.after > 0 && filtered.after < filtered.before)) {
fails.push(`gap filter did not reduce the layer (${filtered.before} -> ${filtered.after})`)
}
if (filtered.tiers.length !== 1 || filtered.tiers[0] !== 0) {
fails.push(`gap filter left tiers ${JSON.stringify(filtered.tiers)}, expected only [0]`)
}
if (pageErrors.length) fails.push(`page error: ${pageErrors[0].slice(0, 120)}`)
if (fails.length) {
console.error(`GOSPEL-MAP: FAIL — ${fails.length} problem(s):`)
for (const f of fails) console.error(` ${f}`)
// ★ NOT process.exit() here. Calling it inside the try terminates the process before
// the finally block's cleanup runs, so a FAILING run left its spawned server alive
// while a passing run did not. Measured: 0 strays after a pass, 1 after a fail — a
// gate that leaks only when it fails leaks exactly when someone is re-running it.
exitCode = 1
} else {
console.log(`GOSPEL-MAP: PASS — ${st.pins} pins rendered; "${opened.pg}" (${opened.lang}) opens with`)
console.log(` a named language; all ${linkCount} links in the index are on screened publishers;`)
console.log(` the table link carries iso=${opened.iso}; the gap filter`)
console.log(` cuts ${filtered.before} features to ${filtered.after}, all tier 0.`)
}
} finally {
await browser.close()
// Never leave a stray server behind, on any exit path.
if (server) { try { server.kill('SIGTERM') } catch (_) {} }
}
process.exit(exitCode)