gate readme screenshots against the bundle they show; wire the generator
|
Before Width: | Height: | Size: 2 MiB After Width: | Height: | Size: 2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 977 KiB After Width: | Height: | Size: 977 KiB |
|
Before Width: | Height: | Size: 1,002 KiB After Width: | Height: | Size: 1 MiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 328 KiB |
86
internal/check-shot-freshness.mjs
Normal 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.')
|
||||
|
|
@ -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.",
|
||||
|
|
|
|||