From b2fc0e4ddaa3d32e2edebc40de9a070cc1ceac44 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:55:43 +0000 Subject: [PATCH] Make the shell-scroll gate runnable, and catch orphan gates verify-shell-scroll.mjs was written and negative-tested, then left with no package.json entry and no importer, so nothing could ever run it again. It also defaulted to localhost:8899 -- an ad-hoc port from an earlier session -- so it failed with ERR_CONNECTION_REFUSED for anyone else and read as a broken gate rather than a missing server. Now check:shell-scroll, defaulting to BASE_URL or 127.0.0.1:4173 like every other browser gate, and listed in verify-browser-all's GATES array. check:no-orphan-gates fails any gate-shaped file in internal/ that nothing can invoke. The repo carded this once already; it recurs because writing a gate and registering one are separate acts and only the first feels like work. Not every unreferenced file is a lost gate, so the rule is not "everything must be wired": theme-flash-probe is a dated diagnostic for two fixed bugs, worth keeping as a reproduction recipe. It now carries an @one-shot tag saying so, which makes the author state which kind of thing they wrote. Two self-referential defects in the first version of that gate, both from reading evidence out of documentation: - it cited theme-flash-probe.mjs in its own header as an example, then grepped for importers, found itself, and declared the orphan reachable. The gate written to catch orphans made an orphan pass by mentioning it. A gate must never be its own witness, so it now excludes its own file. - it matched @one-shot anywhere in prose, including its own paragraph explaining the tag, and reported "1 tagged" when nothing had been tagged. The tag must now be a marker at line start. Negative-tested: removing check:shell-scroll from package.json fails by name. The scroll fix itself is NOT done. contain:layout and .vtl-host overflow-y are confirmed in the built bundle, and a real wheel gesture still moves the grid 0px with 2586px mounted and unreachable. The gate is what caught that; the diff looked correct. --- internal/check-no-orphan-gates.mjs | 113 +++++++++++++++++++++++++ internal/theme-flash-probe.mjs | 6 ++ internal/verify-browser-all.mjs | 5 ++ internal/verify-shell-scroll.mjs | 130 +++++++++++++++++++++++++++++ package.json | 8 +- 5 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 internal/check-no-orphan-gates.mjs create mode 100644 internal/verify-shell-scroll.mjs diff --git a/internal/check-no-orphan-gates.mjs b/internal/check-no-orphan-gates.mjs new file mode 100644 index 000000000..21fcfbed0 --- /dev/null +++ b/internal/check-no-orphan-gates.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node +/** + * check-no-orphan-gates.mjs — a gate that nothing can run is not a gate. + * + * THE DEFECT THIS CATCHES. `internal/verify-shell-scroll.mjs` was written, negative-tested, and + * proved a real Driver-reported bug with hard numbers (2586px of mounted grid, a real wheel gesture + * moving 0px). Then it sat unreachable: no `package.json` entry, no importer. It would never have + * run again. The repo has already carded this once — + * BUILD-BROWSER-GATES-WERE-ALL-ORPHANS-ONE-COMMAND-RUNS-THEM — and it recurred anyway, because + * writing a gate and REGISTERING a gate are two separate acts and only the first feels like work. + * + * ★ THE DISTINCTION THIS GATE HAS TO GET RIGHT, or it becomes noise nobody reads: not every + * unreferenced file in internal/ is a lost gate. + * - `vite-plugin-strip-shadow-css-comments.mjs` is imported by vite.config.js — it runs on every + * build. Reachable. + * - `long-frames-probe.mjs` is imported by two library composables. Reachable. + * - `mobile-sheet-probe.mjs`, `theme-flash-probe.mjs`, `readme-shots-fix.mjs` are DATED ONE-SHOT + * DIAGNOSTICS (Sept 17/21) written to answer one question about a bug that is now fixed. They + * are history, not decay — deleting them would throw away a reproduction recipe, and wiring them + * up would add three checks nobody wants run. + * So the rule is not "everything must be wired". The rule is: a file that PRESENTS ITSELF as a gate + * must be runnable, and a file that is a one-shot probe must SAY SO. Anything else is ambiguous, and + * ambiguity is exactly how a real gate hides among abandoned scripts. + * + * HOW A FILE DECLARES ITS INTENT (either is enough): + * - be referenced from package.json scripts, or imported by something that is; or + * - carry `@one-shot` in its header comment, naming it a diagnostic that is not expected to run + * again. The tag is cheap to add and makes the author state which kind of thing they wrote. + * + * WHAT IT ASSERTS: every internal/*.mjs whose name or header marks it as a gate (check-*, verify-*, + * smoke-*, or a header containing "GATE") is either reachable or explicitly tagged @one-shot. + * + * NEGATIVE-TESTED: removing the check:shell-scroll entry from package.json makes this fail by name. + */ +import { readFileSync, readdirSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { execSync } from 'node:child_process' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const pkgRaw = readFileSync(join(ROOT, 'package.json'), 'utf8') + +const files = readdirSync(join(ROOT, 'internal')).filter((f) => f.endsWith('.mjs')) + +const SELF = 'check-no-orphan-gates.mjs' + +/** + * A file is reachable if package.json names it, or if some OTHER source file imports it. + * + * ★ THE EXCLUSION OF THIS FILE IS LOad-BEARING, not tidiness. First version of this gate listed + * `theme-flash-probe.mjs` in its own header as an example of a dated diagnostic — then grepped for + * importers, found ITSELF, and declared the file reachable. The gate written to catch orphans made + * an orphan pass, by citing it. A gate must never be its own witness. + * + * Same family as the `@one-shot` bug found in the same run: the tag-detection matched this file's + * own prose explaining what the tag means, so the gate reported "1 tagged @one-shot" when no file + * had ever been tagged. Both defects come from reading EVIDENCE out of DOCUMENTATION. Where a gate + * must describe the thing it checks, it has to exclude its own text from the check. + */ +function reachable(base) { + if (pkgRaw.includes(base)) return 'package.json' + try { + const hits = execSync( + `grep -rl -- ${JSON.stringify(base.replace(/\.mjs$/, ''))} --include=*.mjs --include=*.js --include=*.ts . 2>/dev/null | grep -v node_modules | grep -v "internal/${base}" | grep -v "internal/${SELF}" || true`, + { cwd: ROOT, encoding: 'utf8' } + ).trim() + if (hits) return hits.split('\n')[0] + } catch {} + return null +} + +const orphans = [] +let checked = 0 +let tagged = 0 + +for (const f of files) { + const src = readFileSync(join(ROOT, 'internal', f), 'utf8') + const header = src.slice(0, 2600) + + // Does this file present itself as a GATE (something meant to run repeatedly)? + const looksLikeGate = /^(check|verify|smoke)-/.test(f) || /\bGATE\b|\bgate for\b/i.test(header) + if (!looksLikeGate) continue + checked++ + + // The tag must be a MARKER on its own line (` * @one-shot `), not the string appearing + // anywhere in prose. This gate's own header discusses `@one-shot` at length, and a loose test + // counted that discussion as a tag — reporting "1 tagged" when nothing had ever been tagged. + // Requiring line-start-plus-comment-prefix makes the tag a deliberate act instead of a word. + if (/^\s*(?:\*|\/\/)\s*@one-shot\b/m.test(header) && f !== SELF) { tagged++; continue } + const via = reachable(f) + if (!via) orphans.push(f) +} + +if (orphans.length) { + console.error(`NO-ORPHAN-GATES: FAIL — ${orphans.length} gate(s) exist but nothing can run them.\n`) + for (const f of orphans) { + console.error(` internal/${f}`) + } + console.error('\n A gate nobody can invoke is a file, not a check. Pick one:') + console.error(' 1. add a package.json script (and, if it needs a browser, an entry in the GATES') + console.error(' array of internal/verify-browser-all.mjs so the one-command suite runs it);') + console.error(' 2. or add `@one-shot` to its header if it was a diagnostic for a single bug and') + console.error(' is kept only as a reproduction recipe.') + console.error('\n This already happened once (card BUILD-BROWSER-GATES-WERE-ALL-ORPHANS). It recurs') + console.error(' because writing a gate and registering it are separate acts, and only the first') + console.error(' one feels like the work.') + process.exit(1) +} + +console.log(`NO-ORPHAN-GATES: PASS — ${checked} gate-shaped file(s); all reachable (${tagged} tagged @one-shot).`) +console.log(' Reachable means named in package.json or imported by something that is. The @one-shot') +console.log(' tag exists so a dated diagnostic can be KEPT as a reproduction recipe without either') +console.log(' masquerading as a live check or being deleted for tidiness.') diff --git a/internal/theme-flash-probe.mjs b/internal/theme-flash-probe.mjs index dc246588b..4aaadf52f 100644 --- a/internal/theme-flash-probe.mjs +++ b/internal/theme-flash-probe.mjs @@ -4,6 +4,12 @@ * when it is supposed to be pinned DARK (useMapTheme THEME_TOGGLE=false → * lightPreset 'night'). Gate for DXM-007 (simple map) + DXM-010 (research map). * + * @one-shot Written 2026-09-17 to prove DXM-007/DXM-010 (a LIGHT basemap frame flashing + * before useMapTheme pinned it dark). Both are fixed. Kept as the reproduction recipe + * — it measures first-paint colour through the real map pipeline, which is expensive + * to rebuild — but it is NOT a standing check and is not expected to run again + * unless that class of bug returns. + * * WHY: the Driver sees (a) a light→dark FLASH on load and (b) the basemap flipping * FULLY light after a legend click / tab click. "It looks dark to me" is not * evidence — this tool turns the basemap's brightness into NUMBERS sampled over diff --git a/internal/verify-browser-all.mjs b/internal/verify-browser-all.mjs index c9edeb66c..ecd1d67ed 100644 --- a/internal/verify-browser-all.mjs +++ b/internal/verify-browser-all.mjs @@ -75,6 +75,11 @@ const GATES = [ // gate above can tell those apart — the defect lives in the host's ancestor chain, not the // component. See internal/check-fixed-anchoring.mjs. { script: 'check:fixed-anchoring', why: 'the filter dropdown stays anchored in BOTH hosts, not just standalone' }, + // Same family as the gate above, and found the same week: the cap-5 shell's pane geometry breaks a + // plugin that is perfect standalone. Here .vtl-host was inset:0 with overflow:visible, so 2586px of + // mounted grid was unreachable and a real wheel gesture moved 0px. Asserts with a WHEEL, not with + // scrollHeight — scrollHeight > innerHeight was the FALSE PASS that closed this bug twice. + { script: 'check:shell-scroll', why: 'a wheel gesture in the cap-5 shell actually reaches the grid below the dashboard' }, ] // ── PRECONDITION 1: a built tree ──────────────────────────────────────────── diff --git a/internal/verify-shell-scroll.mjs b/internal/verify-shell-scroll.mjs new file mode 100644 index 000000000..52314267c --- /dev/null +++ b/internal/verify-shell-scroll.mjs @@ -0,0 +1,130 @@ +#!/usr/bin/env node +/** + * verify-shell-scroll.mjs — THE GATE FOR BUG-STILL-CANNOT-SCROLL-DOWN-TO-THE-GRID. + * + * ★ WHAT IT ASSERTS, and why each assertion exists rather than the obvious one. + * + * The obvious assertion — `scrollHeight > innerHeight` — is a FALSE PASS and was used twice to + * close this bug while the Driver could still not scroll. In the cap-5 shell the document measures + * scrollHeight 900 === clientHeight 900 while the composite element measures 3380/769: the + * overflow is real, but it belongs to an element with `overflow:visible` that no scroller owns. + * Geometry said "tall"; the user's wheel moved nothing. + * + * So this gate asserts the ONLY thing the Driver actually cares about: that a REAL WHEEL GESTURE + * over the map area moves the grid up the screen and brings it into view. It measures the grid's + * position before and after, in the SHELL, which is the surface he uses — not the standalone leaf, + * which passed all along and is why the two previous fixes looked green. + * + * ★ NEGATIVE-TESTED. Reverting the fix (restoring `.ps-pane`/`.vtl-host` to `position:absolute; + * inset:0` with no overflow, and `.vtl-content` to `contain:layout paint`) makes this exit 1 with + * "grid did not move". A gate nobody has broken is a gate nobody knows works. + * + * Usage: node internal/verify-shell-scroll.mjs [baseUrl] + * (defaults to http://localhost:8899; serve ./public) + */ +import { chromium } from 'playwright' + +// Default matches every other browser gate in this repo: `bun run serve` on loopback 4173. +// It was `localhost:8899` — one of the ad-hoc ports a previous session happened to have running, +// which meant the gate failed with ERR_CONNECTION_REFUSED for anyone else and looked like a broken +// gate rather than a missing server. Prefer BASE_URL (env) over a positional argument, because +// `bun run