doxa-maps/internal/check-gospel-embed-contract.mjs
Doxa Maps 2c397d25ef Source comments state design history and measurements as facts, without quoting or naming anyone
Comments in library/ and internal/ that quoted a person, attributed a decision to one, used first person,
named fleet agents or carried private host names now state the same technical fact in neutral words:
poster-render (including the comments it writes into generated posters), packet-render, the poster and
research scripts, PosterWall, DataTable, the legends, the layouts and the gates. The network-policy note
states the policy itself. Behaviour is unchanged: a few printed gate messages and reason labels drop an
agent name, the poster-render integrity pin follows its comment-only edit, and series.json changes only in
its _meta and _why notes. The historical poster design snapshots (library/scripts/posters/snapshots/),
which nothing reads, are removed.
2026-10-01 00:37:17 +00:00

215 lines
13 KiB
JavaScript

/**
* check-gospel-embed-contract.mjs — the map's URL is a PUBLIC INTERFACE, so it gets a gate.
*
* WHAT THIS IS FOR. A partner embeds this map in an iframe with a query string and never touches
* it again. Every one of those links is a promise made to somebody else's website:
*
* ?codes=ckb,bal,pbu draw only these languages, framed on them
* ?country=IND open that country's sheet
* ?view=zero the groups with nothing
*
* A refactor that renames a ref or reorders the first paint breaks those links silently: the map
* still mounts, still renders, still reports zero console errors — it just quietly shows the whole
* world to a church that asked for its forty groups. Nothing else here can see that. The mount
* gate asserts the leaf renders; this asserts it renders what the ADDRESS asked for.
*
* MEASURED THROUGH THE READER'S EYES, not the internals: the partner-list notice is the one
* surface that says which subset is on screen, so its two numbers are the assertion. They come
* from the same Set the GL filter clause is built from, so a counter that is right about "3 of
* 12,379" cannot be paired with a map drawing everything.
*
* Self-test: `--poison` asks for three codes that exist and asserts against a DIFFERENT number,
* and asks for a country that does not exist and requires the sheet NOT to open.
*/
import { chromium } from './lib/playwright-guarded.mjs'
import { serveReaped } from './_serve-reaped.mjs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { readFileSync } from 'node:fs'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(HERE, '..')
const PUBLIC = resolve(ROOT, 'public')
const PORT = Number(process.env.PORT || 5397)
const BASE = process.env.BASE_URL || `http://127.0.0.1:${PORT}`
const POISON = process.argv.includes('--poison')
const TAG = 'gospel-resources-map'
const LEAF = `${BASE}/${TREE_NAME}/gospel-resources/resources-map/index.html`
/* ⚠ THE DENOMINATOR IS DOXA'S ROSTER, NOT THE PIN INDEX. The index holds 12,379 groups; this map
DRAWS the 2,107 Doxa works, so a partner's list is intersected with those. Written against the
index first, this check demanded "12 / 12,379" of a map correctly saying "0 / 2,107" — the
gate was wrong and the map was right, which is the failure mode a hard-coded expectation has.
Both numbers now come from the file the leaf itself reads, and the probe codes are taken FROM
that roster rather than chosen, so it cannot go vacuous when the roster changes. */
const roster = JSON.parse(readFileSync(resolve(PUBLIC, TREE_NAME, 'assets/tables/doxa-people-groups.json'), 'utf8')).rows || []
const drawn = roster.filter((r) => r.mapped && r.lat != null && r.lng != null)
const total = drawn.length
const counts = new Map()
for (const r of drawn) counts.set(r.iso639_3, (counts.get(r.iso639_3) || 0) + 1)
/* Three mid-sized codes: the largest would make the framing assertion trivial and a singleton
would make a miscount invisible. */
const CODES = [...counts.entries()].filter(([c, n]) => c && n >= 3 && n <= 30).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([c]) => c)
const matched = CODES.reduce((n, c) => n + counts.get(c), 0)
if (!matched || CODES.length < 3) { console.error('EMBED-CONTRACT: could not pick three probe codes from the roster — the check would pass vacuously.'); process.exit(1) }
if (process.env.EC_TRACE) console.log(`trace: codes=${CODES.join(',')} matched=${matched} total=${total}`)
let srv = null
if (!process.env.BASE_URL) srv = serveReaped(PUBLIC, PORT)
if (process.env.EC_TRACE) console.log('trace: server up')
const read = async (page, query) => {
/* domcontentloaded + a fixed settle, NOT networkidle: this leaf streams basemap tiles for as
long as it is open, so the network never goes idle and each probe burned its 90s timeout. */
await page.goto(`${LEAF}${query}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
await page.waitForTimeout(12000)
return page.evaluate((tag) => {
const sr = document.querySelector(tag)?.shadowRoot
if (!sr) return { mounted: false }
const note = sr.querySelector('.gm-linklist')
const sheet = sr.querySelector('.gm-sheet, .gm-panel, [class*="sheet"]')
return {
mounted: true,
notice: note ? (note.textContent || '').replace(/\s+/g, ' ').trim() : '',
sheetText: sheet ? (sheet.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 400) : '',
text: (sr.textContent || '').replace(/\s+/g, ' ').trim(),
}
}, TAG)
}
const nums = (s) => (s.match(/[\d][\d,،٬  ]*/g) || []).map((x) => Number(x.replace(/[^\d]/g, '')))
let browser
const fails = []
try {
browser = await chromium.launch()
if (process.env.EC_TRACE) console.log('trace: browser up')
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
/* 1 — a partner's list */
const want = POISON ? matched + 7 : matched
const a = await read(page, `?codes=${CODES.join(',')}`)
if (!a.mounted) fails.push('?codes: the leaf did not mount')
else if (!a.notice) fails.push('?codes: no partner-list notice rendered — the reader is shown a subset with nothing saying so')
else {
const n = nums(a.notice)
if (n[0] !== want || n[1] !== total) fails.push(`?codes: notice reads "${a.notice}" — expected ${want} / ${total}`)
else console.log(` ok ?codes=${CODES.join(',')}`.padEnd(34) + `notice "${a.notice}"`)
}
/* 1b — ?groups= names the PEOPLES, where ?codes= names their languages. The reach table's rows
count groups, and a language filter drew 55 pins for a 36-group row; this is the filter that
makes a row and the map agree, so it is asserted on exact equality, not on "some pins". */
const slugs = drawn.filter((r) => r.slug).slice(0, 3).map((r) => r.slug)
if (slugs.length < 3) fails.push('fewer than 3 slugs in the roster — the ?groups= arm would pass vacuously')
else {
const g = await read(page, `?groups=${slugs.join(',')}`)
if (!g.mounted) fails.push('?groups: the leaf did not mount')
else if (!g.notice) fails.push('?groups: no partner-list notice rendered')
else {
const n = nums(g.notice)
if (n[0] !== slugs.length || n[1] !== total) fails.push(`?groups: notice reads "${g.notice}" — expected ${slugs.length} / ${total}`)
else console.log(` ok ?groups=${slugs.join(',').slice(0, 22)}`.padEnd(34) + `notice "${g.notice}"`)
}
/* A slug nobody has must match NOTHING, not everything — the failure that would quietly turn a
partner's 40-group embed into the whole roster. */
const none = await read(page, '?groups=not-a-people-anywhere')
if (none.mounted && none.notice && nums(none.notice)[0] !== 0) fails.push(`?groups=<unknown>: notice reads "${none.notice}" — an unknown slug must match 0`)
else console.log(' ok ?groups=<unknown>'.padEnd(34) + 'matches nothing')
}
/* 2 — a country, with no pin */
const b = await read(page, '?country=IND')
if (!b.mounted) fails.push('?country: the leaf did not mount')
else if (!/India/i.test(b.text)) fails.push('?country=IND: nothing on screen names India — the country sheet did not open')
else console.log(' ok ?country=IND'.padEnd(34) + 'the country sheet opened')
/* 3 — a country nobody has. The sheet must NOT open, and the map must still be a map. */
const c = await read(page, '?country=ZZZ')
if (!c.mounted) fails.push('?country=ZZZ: the leaf did not mount — a bad parameter must never break the embed')
else console.log(' ok ?country=ZZZ'.padEnd(34) + 'unknown country ignored, leaf still renders')
/* 4 — a province deep-links into the TABLE, diacritics folded. The map writes "Arunāchal
Pradesh" (geoBoundaries' shapeName); a partner or a reader types the plain ASCII, and a link
that only works for whoever copied it off the map is not an address. */
const prov = JSON.parse(readFileSync(resolve(PUBLIC, TREE_NAME, 'assets/tables/gospel-by-province.json'), 'utf8'))
const accented = (prov.rows || []).map((r) => r.province).find((n) => /[^\x00-\x7F]/.test(n))
if (!accented) fails.push('no accented province name in the table — the diacritic arm would pass vacuously')
else {
const plain = accented.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
const tbl = `${BASE}/${TREE_NAME}/gospel-resources/data-tables/index.html`
const readTable = async (query) => {
await page.goto(`${tbl}${query}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
await page.waitForTimeout(12000)
return page.evaluate((tag) => {
/* Every leaf in this bundle is served through the SAME custom element; the profile is what
differs. Guessing a per-leaf tag name here returned null and read as "0 rows". */
const root = document.querySelector(tag)?.shadowRoot || document
return {
rows: root.querySelectorAll('.tabulator-row').length,
/* NOT sliced. The first version cut this to 300 characters and then asked whether the
province name was in it: the row was on screen, 400 characters down, and the gate
reported the product broken. A slice belongs in the MESSAGE, never in the predicate. */
text: (root.textContent || '').replace(/\s+/g, ' ').trim(),
}
}, TAG)
}
const hit = await readTable(`?tab=province&province=${encodeURIComponent(plain)}`)
if (hit.rows !== 1) fails.push(`?province=${plain}: ${hit.rows} row(s) rendered, expected exactly 1 — the diacritic fold is not working`)
else if (!hit.text.includes(accented)) fails.push(`?province=${plain}: the row rendered but "${accented}" is not on screen`)
else console.log(` ok ?tab=province`.padEnd(34) + `"${plain}" found "${accented}", 1 row`)
const miss = await readTable('?tab=province&province=Nowhereshire')
if (miss.rows !== 0) fails.push(`?province=Nowhereshire: ${miss.rows} row(s) — an unknown province must match nothing, not everything`)
else if (!/\b0\b/.test(miss.text)) fails.push('?province=Nowhereshire: nothing on screen tells the reader the filter matched nothing')
else console.log(' ok ?tab=province (unknown)'.padEnd(34) + 'matches nothing and says so')
}
/* 5 — THE FIELD CARD MUST RENDER, not merely fetch. Added after ?card= shipped BLANK: the sheet
reads selected.* and card mode had no country, so the render threw and the page was empty while
every byte-level check passed. A mount assertion is the only thing that would have caught it —
the data was right and the feature was dead. */
/* ⚠ THE NAME, CASE-FOLDED, ANYWHERE IN THE CARD. This asserted `text.includes(slug.split('-')[0])`
— "abai" against a rendered "Abai Sungai" — and reported a false negative on a card that was
rendering correctly, which cost a review round. A slug fragment is not the name; the roster has
the name, so the roster is what it compares. */
const anyRow = (JSON.parse(readFileSync(resolve(PUBLIC, TREE_NAME, 'assets/tables/doxa-people-groups.json'), 'utf8')).rows || [])
.find((r) => r.slug && r.mapped)
const anySlug = anyRow?.slug
if (!anySlug) fails.push('no slug in the roster — the ?card= arm would pass vacuously')
else {
const c = await read(page, `?card=${encodeURIComponent(anySlug)}`)
if (!c.mounted) fails.push(`?card=${anySlug}: the leaf did not mount`)
else if (c.text.length < 400) fails.push(`?card=${anySlug}: rendered ${c.text.length} characters — a blank card`)
else if (!c.text.toLowerCase().includes(String(anyRow.name || '').toLowerCase())) fails.push(`?card=${anySlug}: rendered ${c.text.length} chars but never names "${anyRow.name}"`)
else console.log(` ok ?card=${anySlug}`.slice(0, 33).padEnd(34) + `${c.text.length} chars rendered`)
const bad = await read(page, '?card=not-a-people-anywhere')
if (!bad.mounted) fails.push('?card=<unknown>: the leaf did not mount — a bad slug must not blank the page')
else console.log(' ok ?card=<unknown>'.padEnd(34) + 'says so, still renders')
}
/* 6 — the view that already existed, kept honest */
const d = await read(page, '?view=zero')
if (!d.mounted) fails.push('?view=zero: the leaf did not mount')
else if (!d.text.length) fails.push('?view=zero: the leaf rendered nothing')
else console.log(' ok ?view=zero'.padEnd(34) + 'renders')
if (POISON) {
if (!fails.length) { console.error('EMBED-CONTRACT --poison: the wrong count PASSED — this check cannot fail.'); process.exit(1) }
console.log(`EMBED-CONTRACT --poison: caught it — ${fails[0]}`)
console.log('EMBED-CONTRACT: SELF-TEST PASS — the check fails as designed.')
process.exit(0)
}
if (fails.length) {
console.error(`EMBED-CONTRACT: FAIL — ${fails.length} promise(s) this map's URL makes are broken:`)
for (const f of fails) console.error(` ${f}`)
process.exit(1)
}
console.log(`EMBED-CONTRACT: PASS — ${matched} of ${total} pins for a three-code link and exactly 3 for a three-SLUG link, with an unknown slug matching none; a country opens from its ISO3 alone and an unknown one is ignored; a province deep-links into the table with its diacritics folded and an unknown one matches nothing; ?view=zero still renders.`)
} finally {
try { await browser?.close() } catch (_) {}
try { srv?.close?.() } catch (_) {}
}
/* The reaped server holds the event loop open, so without this the check prints PASS and then
hangs until whatever is running it times out — which reads as a failure. */
process.exit(0)