From 0b54b0354e86a2523198d2f75eecbab11b4e5525 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Fri, 25 Sep 2026 23:50:31 +0000 Subject: [PATCH] Gospel checks: one dev-assets gate, and cover the gap it leaves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gospel-only dev-assets check is gone in favour of the shared one, which is better at the job: it tests the /__leaf// prefix — the one that actually broke — across all seven map bundles, and confirms each leaf hands its element the base it claims. The gospel version never looked at that prefix, so keeping both would have meant two implementations and the weaker one still green while the map was broken. What the shared gate samples is the POOL, the files every map shares. It cannot know this leaf also fetches gospel tables by name, so a table that stopped being staged would leave it green and this map fetching a 404. That is the only gap left, and this covers exactly it. The wanted list is parsed out of resources-map.vue rather than written down, which immediately earned itself: the hand-written list it replaces was already two short — pray-index and gospel-link-verification were being fetched and were not in it. A list maintained by hand is the field-list-goes-stale bug, and this bundle has produced that three times in one day. Poison-tested by unstaging a real table: FAIL, exit 1, then restored. --- internal/check-gospel-all.mjs | 1 + internal/check-gospel-dev-assets.mjs | 107 ------------------------ internal/check-gospel-tables-staged.mjs | 60 +++++++++++++ package.json | 2 +- 4 files changed, 62 insertions(+), 108 deletions(-) delete mode 100644 internal/check-gospel-dev-assets.mjs create mode 100644 internal/check-gospel-tables-staged.mjs diff --git a/internal/check-gospel-all.mjs b/internal/check-gospel-all.mjs index cf3be1fbe..f5e52b9d0 100644 --- a/internal/check-gospel-all.mjs +++ b/internal/check-gospel-all.mjs @@ -36,6 +36,7 @@ const STATIC = [ ['check:stale-sweep-claim', 'no caveat outliving its truth'], ['check:click-sheet', 'the country sheet resolves, links are absolute'], ['check:snapshot-guard', 'no fetcher writes around the guard'], + ['check:gospel-tables-staged', 'every asset the map fetches is in the shipped tree'], ] const BROWSER = [ ['check:leaves-mount', 'all five leaves actually mount'], diff --git a/internal/check-gospel-dev-assets.mjs b/internal/check-gospel-dev-assets.mjs deleted file mode 100644 index 43e340b4c..000000000 --- a/internal/check-gospel-dev-assets.mjs +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env node -/** - * check-gospel-dev-assets — every asset the gospel map fetches must be reachable in the DEV - * SERVER, not only in the built tree. - * - * WHY THIS EXISTS. Every other gate for this bundle measures public/ — the built tree — and the - * Driver found the map broken where none of them look: `bun run dev`. Boundary polygons were - * missing there on five maps at once, this one included, and every built-tree gate stayed green - * throughout, because the thing that differs is how the leaf RESOLVES its asset base, not what - * the build produces. - * - * MEASURED against the dev server, headless, with curl-equivalent fetches: - * /assets/boundaries/world-adm0.geojson 404 - * /dx-maps/assets/boundaries/world-adm0.geojson 200, 1,911,385 bytes - * The file is there; the base is wrong. A map with no boundaries draws no countries at all, and - * a country is half of what this map is for. - * - * NO BROWSER, deliberately. Browsers on this box are rationed to one agent, and none is needed: - * the question is whether a URL answers, which fetch settles. That also makes this gate cheap - * enough to run on every change rather than when a slot happens to be free. - * - * ⚠ IT ASSERTS BYTES, NOT JUST STATUS. A dev server that answers 200 with an HTML fallback for a - * missing .geojson is the SPA-shell trap this repo has already been caught by twice, so each - * asset must come back as the KIND of thing it claims to be. - * - * ⚠ WHAT THIS DOES *NOT* PROVE, said plainly so nobody reads a green line as the all-clear. - * It proves the assets ARE SERVED at //assets in dev. It does NOT prove the leaf ASKS for - * that path — and the reported defect is exactly that: resolveAssetsBase falls back to './assets' - * in dev, so the map fetches /assets/… and gets a 404 while the file sits at //assets/… - * untouched. That fix is in library/ and belongs to studio. This gate is the other half: once the - * base is right, it says whether there is anything at the end of it, and it will keep saying so - * when a table is renamed or a boundary file is dropped from the staged pool. - * - * Usage: node internal/check-gospel-dev-assets.mjs [--base http://127.0.0.1:5399] - * Start the server yourself first: bun x vite --port 5399 --strictPort - */ -import { TREE_NAME } from './_tree-name.mjs' - -const argBase = process.argv.indexOf('--base') -const BASE = (argBase > -1 && process.argv[argBase + 1]) || process.env.DEV_BASE || 'http://127.0.0.1:5399' - -/* Exactly what resources-map.vue fetches, by the same paths, so this cannot drift into testing - files the map does not actually want. */ -const ASSETS = [ - { path: 'boundaries/world-adm0.geojson', kind: 'json', min: 100000 }, - { path: 'tables/people-group-pins.json', kind: 'json', min: 100000 }, - { path: 'tables/gospel-by-country.json', kind: 'json', min: 1000 }, - { path: 'tables/doxa-people-groups.json', kind: 'json', min: 1000 }, - { path: 'tables/country-languages.json', kind: 'json', min: 1000 }, -] - -let reachable = false -try { - const r = await fetch(`${BASE}/${TREE_NAME}/assets/${ASSETS[0].path}`, { signal: AbortSignal.timeout(20000) }) - reachable = r.ok -} catch { /* precondition below */ } -if (!reachable) { - console.log(`DEV-ASSETS: SKIP — no dev server answering at ${BASE}.`) - console.log(' Start one on your own port first: bun x vite --port 5399 --strictPort') - process.exit(0) -} - -const fails = [] -for (const a of ASSETS) { - const url = `${BASE}/${TREE_NAME}/assets/${a.path}` - let res, body - try { - res = await fetch(url, { signal: AbortSignal.timeout(60000) }) - body = await res.text() - } catch (e) { - fails.push(`${a.path} — ${String(e.message).slice(0, 60)}`) - continue - } - if (!res.ok) { fails.push(`${a.path} — HTTP ${res.status}`); continue } - /* The shell trap: 200 with an HTML page instead of the asset. */ - if (/^\s*/ prefix, which + * is the one that actually broke, and which mine never looked at. One implementation. + * + * What it samples, though, is the POOL: world-adm0 and country-connectivity, the files common to + * every map. It cannot know that this leaf also fetches four gospel tables by name, so a table + * that stopped being staged would leave that gate green and this map fetching a 404. That is the + * only gap left once the duplicate is gone, so this covers exactly it and nothing else. + * + * Read from the SOURCE OF TRUTH rather than a second list: the fetch() calls in resources-map.vue + * are parsed out of the file, so adding a table to the leaf adds it here with no edit. A list + * maintained by hand is the field-list-goes-stale bug, which this bundle has now produced three + * times in one day. + * + * Headless, filesystem only. No server, no browser. + */ +import { readFileSync, existsSync, statSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { TREE_NAME } from './_tree-name.mjs' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const LEAF = resolve(ROOT, 'plugin-designs/gospel-resources/resources-map.vue') +const POOL = resolve(ROOT, 'public', TREE_NAME, 'assets') + +const src = readFileSync(LEAF, 'utf8') +/* `${assetsBase.value}/tables/x.json` and `${assetsBase.value}/boundaries/y.geojson` */ +const wanted = [...new Set( + [...src.matchAll(/\$\{assetsBase\.value\}\/([A-Za-z0-9_\-./]+)/g)].map((m) => m[1]), +)] + +if (!wanted.length) { + console.error('GOSPEL-TABLES-STAGED: NOT MEASURED — no asset fetches found in resources-map.vue.') + console.error(' The leaf changed shape; this check would pass vacuously, which is worse than failing.') + process.exit(2) +} + +const missing = [] +for (const rel of wanted) { + const p = resolve(POOL, rel) + if (!existsSync(p)) { missing.push(`${rel} — not staged`); continue } + const bytes = statSync(p).size + /* A staged-but-empty file is the same outage with a friendlier face. */ + if (bytes < 200) { missing.push(`${rel} — staged but only ${bytes} bytes`); continue } + console.log(` ok ${rel.padEnd(42)} ${String(bytes).padStart(9)} bytes`) +} + +if (missing.length) { + console.error(`GOSPEL-TABLES-STAGED: FAIL — ${missing.length} of ${wanted.length} asset(s) the map fetches are not in the tree:`) + for (const m of missing) console.error(` ${m}`) + console.error(` Looked under public/${TREE_NAME}/assets. Run \`bun run tree\` after building this bundle.`) + process.exitCode = 1 +} else { + console.log(`GOSPEL-TABLES-STAGED: PASS — all ${wanted.length} asset(s) resources-map.vue fetches are staged.`) +} diff --git a/package.json b/package.json index c34a9ebe9..da273cc8b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "check:csv-shape": "node internal/check-csv-export-shape.mjs", "check:csv-shape:self-test": "node internal/check-csv-export-shape.mjs --self-test", "check:deck-click": "node internal/check-deck-click.mjs", - "check:dev-assets": "node internal/check-gospel-dev-assets.mjs", "check:dep-names": "node internal/check-dependency-names.mjs", "check:descriptions": "node internal/check-profile-descriptions.mjs", "check:dev-assets": "node internal/check-dev-assets.mjs", @@ -81,6 +80,7 @@ "check:gallery-embed:live-cors": "node internal/check-research-gallery-embed.mjs --live-cors", "check:gallery-embed:maplibre": "node internal/check-research-gallery-embed.mjs --provider maplibre", "check:gospel-all": "node internal/check-gospel-all.mjs", + "check:gospel-tables-staged": "node internal/check-gospel-tables-staged.mjs", "check:heartbeat": "node internal/check-heartbeat-threshold.mjs", "check:heartbeat:self-test": "node internal/check-heartbeat-threshold.mjs --self-test", "check:host-locale-contract": "node internal/check-host-locale-contract.mjs",