doxa-maps/internal/check-shot-distinctness.mjs
doxa-maps-88655c70 9dc4dda8cf
Some checks are pending
build-gate / build (push) Waiting to run
consolidate docs into documentation/ and deployment shells into deployment/
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.

docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.

The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.

Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
  - join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
  - tauri frontendDist '../public' -> '../../public', since src-tauri is now one
    level deeper; check:tauri caught this exactly as the card predicted
  - the WordPress contract test's dirname(__DIR__,2) -> 3, same reason

Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.

Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
2026-09-21 23:23:23 +00:00

121 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
/**
* check-shot-distinctness.mjs — two README slots must not show the same thing.
*
* THE BUG THIS EXISTS TO PREVENT (real, 2026-09-21)
* README displayed cap4-upg100-list-page1.png and -page2.png under the captions "List — page 1"
* and "List — page 2". They were the SAME VIEW. The generator loaded the same URL twice and tried
* to reach a "page 2" by scrolling to the bottom and clicking a Next control —
*
* ★ NEITHER EXISTS. my-upg-100-list has NO pagination: no Next button, no pages. And
* window.scrollTo is a no-op because the map fills the viewport.
*
* Every attempt sat inside `try {} catch {}`, so the misses were SILENT and the shot reported OK.
* The two PNGs differed by 0.16% — pure render noise — so they were NOT byte-identical and no
* checksum comparison would have caught them either.
*
* ★ WHILE A REAL PROFILE WENT UNSHOT. The bundle ships THREE manifest-listed profiles
* (upg-100-mfe, upg-100-list, poster-wall) and poster-wall had no screenshot anywhere. The slot
* was faking a fourth view while a genuine one was missing — the same shape as the cap-5 studio
* mislabel found the day before. A missing artifact wearing a plausible one.
*
* WHAT IT CHECKS, AND THE ONE LEGITIMATE EXCEPTION
* Two shots of the SAME URL at the SAME VIEWPORT cannot differ except by noise, so that pair is
* always a defect. But the same URL at a DIFFERENT viewport is a real, intended figure:
* deploy2-mobile-spa shoots doxa-maps-spa at 390x844 to show the Capacitor mobile surface, while
* cap5-doxa-maps-spa shoots it at 1440x900 (2880x1800 vs 1170x2532 on disk). So the key is
* (url + viewport), not url alone — a url-only rule would have a false positive on day one and
* get switched off.
*
* WHY THIS AND check:shots BOTH EXIST
* check:shots proves a PNG is newer than the bundle it shows — freshness. This proves two slots
* are not the same picture — distinctness. A duplicate can be perfectly fresh.
*
* NEGATIVE-TESTED: re-adding a second shot of upg-100-list at the default viewport makes this
* FAIL naming both; removing it returns PASS. The real deploy2/cap5 pair does NOT trip it.
*/
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 SRC = join(ROOT, 'internal', 'readme-shots.mjs')
const OUT = join(ROOT, 'documentation', 'readme-images')
if (!existsSync(SRC)) {
console.log('SHOT-DISTINCTNESS: SKIP — readme-shots.mjs not present.')
process.exit(0)
}
const src = readFileSync(SRC, 'utf8')
// Each `await shot('name', '/path', { ...opts })` — capture the opts blob so we can read `vp`.
const RE = /shot\(\s*'([^']+)'\s*,\s*'([^']*)'\s*(?:,\s*(\{[\s\S]*?\}))?\s*\)/g
const shots = []
for (const m of src.matchAll(RE)) {
const [, name, path, opts = ''] = m
const vp = opts.match(/vp:\s*\{\s*width:\s*(\d+)\s*,\s*height:\s*(\d+)/)
shots.push({ name, path, vp: vp ? `${vp[1]}x${vp[2]}` : 'default' })
}
// A matcher that finds almost nothing reads exactly like a clean pass — see check-quoted-scripts.
const MIN_EXPECTED = 8
console.log(`SHOT-DISTINCTNESS: ${shots.length} shot(s) declared.`)
if (shots.length < MIN_EXPECTED) {
console.error(`\nSHOT-DISTINCTNESS: FAIL — only ${shots.length} shots parsed (expected >= ${MIN_EXPECTED}).`)
console.error(' The MATCHER is broken, not the shot list. Fix the regex before trusting this.')
process.exit(1)
}
const bySignature = new Map()
for (const s of shots) {
const key = `${s.path} @${s.vp}`
if (!bySignature.has(key)) bySignature.set(key, [])
bySignature.get(key).push(s.name)
}
const collisions = [...bySignature.entries()].filter(([, names]) => names.length > 1)
// Report the same-url/different-viewport pairs as intentional, so they stay visible.
const sameUrlDiffVp = new Map()
for (const s of shots) {
if (!sameUrlDiffVp.has(s.path)) sameUrlDiffVp.set(s.path, new Set())
sameUrlDiffVp.get(s.path).add(s.vp)
}
for (const [path, vps] of sameUrlDiffVp) {
if (vps.size > 1) console.log(` i SAME URL, DIFFERENT VIEWPORTS (intended): ${path} @ ${[...vps].join(', ')}`)
}
if (collisions.length) {
console.error(`\nSHOT-DISTINCTNESS: FAIL — ${collisions.length} slot pair(s) capture the SAME view:`)
for (const [key, names] of collisions) {
console.error(` ✗ ${names.join(' and ')}\n both load ${key}`)
}
console.error(
'\n Two shots of one URL at one viewport cannot differ except by render noise, so they are\n' +
' not byte-identical and a checksum will not catch them. If a slot is meant to show a\n' +
' DIFFERENT state, drive it with an assertion that FAILS when the control is absent —\n' +
' a click wrapped in try/catch makes a missing feature look like a successful shot.\n' +
' And check first whether the state exists at all: the original defect was a "page 2"\n' +
' for a list that has no pagination, while a real third profile went unshot.'
)
process.exit(1)
}
// Belt and braces: identical bytes are always wrong, whatever the declared opts say.
if (existsSync(OUT)) {
const seen = new Map()
for (const s of shots) {
const p = join(OUT, s.name + '.png')
if (!existsSync(p)) continue
const sig = `${statSync(p).size}`
if (!seen.has(sig)) seen.set(sig, [])
seen.get(sig).push(s.name)
}
const sameSize = [...seen.values()].filter((v) => v.length > 1)
for (const group of sameSize) {
console.log(` i identical file size (check by eye if unexpected): ${group.join(', ')}`)
}
}
console.log('SHOT-DISTINCTNESS: PASS — every slot captures a distinct url+viewport.')