gate readme screenshots against the bundle they show; wire the generator

This commit is contained in:
doxa-maps-88655c70 2026-09-21 07:05:00 +00:00
parent 7276ade0e6
commit 32e6c1231e
13 changed files with 88 additions and 0 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 977 KiB

After

Width:  |  Height:  |  Size: 977 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,002 KiB

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 328 KiB

View file

@ -0,0 +1,86 @@
#!/usr/bin/env node
/**
* check-shot-freshness.mjs — catch a README screenshot that no longer shows the shipped UI.
*
* THE BUG THIS EXISTS TO PREVENT (real, 2026-09-21)
* The 12 PNGs in docs/readme-images/ were dated Sep 20 while FIVE .vue sources were newer —
* including two I had changed that same flight (people-groups-grid's visible-filters fix and
* doxa-simple-map's light loading overlay). The README showed a UI THAT NO LONGER EXISTED and
* nothing in the routine noticed, because A STALE SCREENSHOT LOOKS EXACTLY LIKE A CURRENT ONE.
* There is no error, no 404, no failing test — just a picture quietly telling a lie to whoever
* the project is handed to.
*
* WHAT IT COMPARES, AND WHY THAT IS THE RIGHT PAIR
* Each shot captures a BUILT LEAF, so the honest comparison is the BUNDLE it renders versus the
* PNG. Comparing raw .vue sources instead would be both noisy (an unrelated plugin's edit would
* fail every shot) and WRONG — a source edit that has not been built is not yet in any leaf, so
* the screenshot is still accurate. Using the bundle also covers NESTED children for free: a
* composite's bundle is rebuilt whenever a child changes (that is what check:composite enforces),
* so cap-4/cap-5 shots go stale exactly when their children do.
*
* DERIVED, NOT HARDCODED
* The shot->bundle mapping is parsed out of readme-shots.mjs itself: every `await shot('name',
* '/doxa-maps/<bundle>/...')` names its bundle in the URL. Add a shot and this follows; a
* hardcoded table would drift silently, which is the failure mode the whole file is about.
* Shots whose path is not a /doxa-maps/ leaf (the synthetic iframe host) are REPORTED as skipped
* rather than dropped — an unchecked shot must be visible, not invisible.
*
* NEGATIVE-TESTED: `touch public/doxa-maps/doxa-simple-map/doxa-simple-map.js` makes this FAIL
* naming the three cap2-* shots; re-running readme-shots.mjs returns it to PASS.
*/
import { readFileSync, existsSync, statSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const SHOTS_SRC = join(ROOT, 'internal', 'readme-shots.mjs')
const OUT = join(ROOT, 'docs', 'readme-images')
const BUILT = join(ROOT, 'public', 'doxa-maps')
if (!existsSync(SHOTS_SRC) || !existsSync(OUT)) {
console.log('SHOT-FRESHNESS: SKIP — readme-shots.mjs or docs/readme-images/ not present.')
process.exit(0)
}
// Parse `await shot('name', '/path/...')` — the generator is the source of truth for its own map.
const src = readFileSync(SHOTS_SRC, 'utf8')
const shots = [...src.matchAll(/shot\(\s*'([^']+)'\s*,\s*'([^']*)'/g)].map(m => ({ name: m[1], path: m[2] }))
const stale = [], missing = [], skipped = []
let checked = 0
for (const { name, path } of shots) {
const png = join(OUT, name + '.png')
if (!existsSync(png)) { missing.push(name); continue }
const m = path.match(/^\/doxa-maps\/([^/]+)\//)
if (!m) { skipped.push(`${name} (path ${path} is not a /doxa-maps/ leaf)`); continue }
const bundle = join(BUILT, m[1], `${m[1]}.js`)
if (!existsSync(bundle)) { skipped.push(`${name} (bundle ${m[1]} not built)`); continue }
checked++
const bMs = statSync(bundle).mtimeMs, pMs = statSync(png).mtimeMs
if (bMs > pMs) {
stale.push(`${name}.png is OLDER than the bundle it shows (${m[1]}) — ` +
`${Math.round((bMs - pMs) / 1000)}s behind`)
}
}
console.log(`SHOT-FRESHNESS: ${shots.length} shot(s) declared, ${checked} compared against their bundle.`)
for (const s of skipped) console.log(` i SKIPPED ${s}`)
if (missing.length || stale.length) {
if (missing.length) {
console.error(`\nSHOT-FRESHNESS: FAIL — ${missing.length} declared shot(s) have NO png:`)
for (const m of missing) console.error(` ✗ ${m}.png`)
}
if (stale.length) {
console.error(`\nSHOT-FRESHNESS: FAIL — ${stale.length} screenshot(s) predate the UI they show:`)
for (const s of stale) console.error(` ✗ ${s}`)
}
console.error('\n Fix: `node internal/readme-shots.mjs` (it fetches its own token server-side).')
console.error(' A stale screenshot looks exactly like a current one — that is why this is a gate.')
process.exit(1)
}
console.log('SHOT-FRESHNESS: PASS — every screenshot is newer than the bundle it shows.')

View file

@ -49,6 +49,8 @@
"verify:map-paint": "node internal/verify-map-paint.mjs",
"check:sri": "node internal/sri-consistency-check.mjs",
"check:composite": "node internal/check-composite-freshness.mjs # a COMPOSITE is a COPY taken at its own build time: fails if a nested child's source is newer than the parent's bundle. NOT in build:unlocked on purpose \u2014 there everything was just built, so it could never catch the real case (a PARTIAL build). Run it before PUBLISH.",
"check:shots": "node internal/check-shot-freshness.mjs # a README screenshot that predates the bundle it shows is a picture telling a lie \u2014 and it looks IDENTICAL to a current one. Compares each shot's PNG against the BUILT BUNDLE it renders (not raw sources: an unbuilt edit is not yet in any leaf). Fix: node internal/readme-shots.mjs",
"shots": "node internal/readme-shots.mjs # regenerate docs/readme-images/*.png (fetches its own Mapbox token SERVER-SIDE, so the browser CORS block does not apply). Was UNREACHABLE \u2014 present but invoked by no script, which is how the 12 PNGs silently rotted a day behind the UI.",
"check:done-evidence": "node internal/check-done-evidence.mjs # a card may not reach status:done with NO bluf AND NO fields \u2014 the Doxa team inherits this board and an evidence-less 'done' cannot be reconciled against the code. 10 pre-existing are baselined by id; the list should only ever SHRINK.",
"check:wp-embed": "php wordpress/tests/wp-embed-contract-test.php",
"check:mobile": "bun run cap:sync && bun run cap:verify # deploy-2 CONTRACT check: needs NO JDK/SDK/device. cap sync is a file copy + plugin wiring, so it runs anywhere; cap:verify then asserts assets/public/index.html exists. Guards the SOFT FAILURE capacitor.config.json warns about: a wrong webDir yields an EMPTY assets/public and a BLANK WHITE SCREEN at launch, not an error.",