From 59f497eb67f395cee881a56ebc9cfb4cbca52132 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:03:12 +0000 Subject: [PATCH] Six more browser gates measure THIS build or nothing a9550858 guarded twenty gates; these six were still unguarded. Each now exits 75 (NOT MEASURED) rather than grading another tree: - check-legend-export-reachable, check-vision-card-numbers --url: requireThisTree on the URL they are given. - check-embed-basemap-paints: it picks a RANDOM port, not an OS-assigned one, so if another agent holds it, its own server fails to start and it would measure theirs. It now checks the leaf server's identity, stops its own servers and exits 75. Its "a server did not come up" path exited 1 and now exits 75. - check-tabs-localized, check-poster-prints read library/ from DOXA_REPO. Pointing that at another checkout now needs DOXA_REPO_MEASURE_OTHER=1 (new requireThisRepo helper). - check-packet-render refuses a packet file inside a DIFFERENT git checkout (new requirePathInThisCheckout helper); a file outside any checkout is allowed with a note. Proven by pointing each at a wrong build: a copy of this public/ whose manifest differs by one byte, served on its own port, and another worktree for the file and DOXA_REPO gates. All 8 (these 6, plus shell-scroll and upg-map-data) exit 75. On this tree they measure: legend-export, vision-card, upg-map-data, tabs-localized and poster-prints PASS. packet-render gets past the guard and fails on a non-packet file, as intended. embed-basemap measures and FAILS: "the page does NOT paint a basemap on its own baked token". That is a product finding, not the guard, and is reported to its owner. --- internal/_serves-this-tree.mjs | 49 +++++++++++++++++++++- internal/check-embed-basemap-paints.mjs | 15 ++++++- internal/check-legend-export-reachable.mjs | 5 +++ internal/check-packet-render.mjs | 5 ++- internal/check-poster-prints.mjs | 2 + internal/check-tabs-localized.mjs | 2 + internal/check-vision-card-numbers.mjs | 3 ++ 7 files changed, 76 insertions(+), 5 deletions(-) diff --git a/internal/_serves-this-tree.mjs b/internal/_serves-this-tree.mjs index 13c11e1cf..ebc13b188 100644 --- a/internal/_serves-this-tree.mjs +++ b/internal/_serves-this-tree.mjs @@ -19,9 +19,9 @@ * Deliberately NOT fatal on its own: the caller decides whether to start its own server * instead, or to skip. What it must never do is quietly continue. */ -import { readFileSync } from 'node:fs' +import { readFileSync, existsSync, realpathSync } from 'node:fs' import { createHash } from 'node:crypto' -import { resolve } from 'node:path' +import { resolve, dirname, join } from 'node:path' const MANIFEST = 'doxa-maps/manifest.json' @@ -123,3 +123,48 @@ export async function requireThisTree(base, repo, label) { } return verdict } + + +/** + * The same guard for gates that READ A CHECKOUT instead of talking to a server. + * + * check-tabs-localized and check-poster-prints read library/ source from DOXA_REPO, which exists + * so a gate can be pointed at ANOTHER repo on purpose. That is also exactly how a leaked env var + * from someone else's shell makes a gate grade their tree while reporting as ours. So pointing + * elsewhere is an explicit opt-in (DOXA_REPO_MEASURE_OTHER=1); without it the gate exits 75. + * + * @param {string} repoPath the checkout the gate is about to read + * @param {string} repo this gate's own repo root + * @param {string} label the gate's name, for the message + */ +export function requireThisRepo(repoPath, repo, label) { + const real = (p) => { try { return realpathSync(p) } catch { return String(p) } } + if (real(repoPath) === real(repo)) return { ok: true, why: 'this checkout' } + if (process.env.DOXA_REPO_MEASURE_OTHER === '1') { + console.log(`${label}: measuring ANOTHER checkout on purpose (DOXA_REPO_MEASURE_OTHER=1): ${repoPath}`) + return { ok: true, why: 'explicit opt-in' } + } + console.log(`${label}: NOT MEASURED — DOXA_REPO points at ${repoPath}, not this checkout (${repo}).`) + console.log(' Unset DOXA_REPO, or set DOXA_REPO_MEASURE_OTHER=1 if measuring that checkout is the intent.') + process.exit(75) +} + +/** + * For a gate handed a FILE to render: refuse a file that lives inside a DIFFERENT git checkout. + * A file outside any checkout (a /tmp or _qa-cycle output) cannot be attributed and is allowed, + * with a note; one inside another worktree is that worktree's build, not ours. + */ +export function requirePathInThisCheckout(file, repo, label) { + const real = (p) => { try { return realpathSync(p) } catch { return String(p) } } + let dir = dirname(real(file)) + while (dir && dir !== dirname(dir)) { + if (existsSync(join(dir, '.git'))) { + if (real(dir) === real(repo)) return { ok: true, why: 'inside this checkout' } + console.log(`${label}: NOT MEASURED — ${file} belongs to another checkout (${dir}), not this one (${repo}).`) + process.exit(75) + } + dir = dirname(dir) + } + console.log(`${label}: note — ${file} is outside any checkout, so which build produced it cannot be checked.`) + return { ok: true, why: 'outside any checkout' } +} diff --git a/internal/check-embed-basemap-paints.mjs b/internal/check-embed-basemap-paints.mjs index 949793f66..92137d9cf 100644 --- a/internal/check-embed-basemap-paints.mjs +++ b/internal/check-embed-basemap-paints.mjs @@ -28,6 +28,7 @@ */ import { chromium } from 'playwright' import { serveReaped } from './_serve-reaped.mjs' +import { servesThisTree } from './_serves-this-tree.mjs' import { writeFileSync, mkdtempSync, readFileSync } from 'node:fs' import { resolve, dirname } from 'node:path' import { tmpdir } from 'node:os' @@ -38,7 +39,9 @@ const REPO = resolve(HERE, '..') const PUBLIC = resolve(REPO, 'public') const LEAF = 'doxa-maps/gospel-resources/resources-map/index.html' -const leafPort = 5810 + Math.floor(Math.random() * 60) +// EMBED_BASEMAP_PORT pins the port — used to prove the identity guard below (occupy it with another +// tree's server first; the gate must refuse rather than measure that tree). +const leafPort = Number(process.env.EMBED_BASEMAP_PORT) || (5810 + Math.floor(Math.random() * 60)) const hostPort = leafPort + 1 /** Mapbox endpoints that actually draw the map. Telemetry is not evidence of anything. */ @@ -82,7 +85,15 @@ const stop = () => { if (!(await waitFor(`http://127.0.0.1:${leafPort}/${LEAF}`)) || !(await waitFor(`http://127.0.0.1:${hostPort}/index.html`))) { console.error('EMBED-BASEMAP: NOT MEASURED — a server did not come up.') stop() - process.exit(1) + process.exit(75) // a precondition, not a product failure (was 1, which read as a red gate) +} +// ★ THE PORT IS RANDOM, NOT OS-ASSIGNED: if another agent's server already holds it, ours fails to +// start and waitFor() above succeeds against THEIRS. Prove the leaf server is this build. +// Measure THIS build, or measure nothing (exit 75 = NOT MEASURED; see a9550858 / _serves-this-tree.mjs). +{ + // servesThisTree (not requireThisTree) so this gate stops ITS OWN two servers before exiting. + const t = await servesThisTree(`http://127.0.0.1:${leafPort}`, REPO) + if (/DIFFERENT build/.test(t.why)) { console.log('EMBED-BASEMAP: NOT MEASURED — ' + t.why); stop(); process.exit(75) } } const browser = await chromium.launch({ diff --git a/internal/check-legend-export-reachable.mjs b/internal/check-legend-export-reachable.mjs index 97cd56e2b..5b814d6da 100644 --- a/internal/check-legend-export-reachable.mjs +++ b/internal/check-legend-export-reachable.mjs @@ -28,6 +28,9 @@ // Usage: node internal/check-legend-export-reachable.mjs [leafUrl] // SKIPs when nothing is served — this needs the built tree. import { chromium } from 'playwright' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { requireThisTree } from './_serves-this-tree.mjs' const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173') + '/doxa-maps/my-upg-100-list/upg-100-list/index.html' @@ -43,6 +46,8 @@ if (!reachable) { console.log(' Serve the built tree: python3 -m http.server 4173 --directory public (or set BASE_URL)') process.exit(0) } +// Measure THIS build, or measure nothing (exit 75 = NOT MEASURED; see a9550858 / _serves-this-tree.mjs). +await requireThisTree(LEAF, resolve(dirname(fileURLToPath(import.meta.url)), '..'), 'LEGEND-EXPORT-REACHABLE') const WALK = `(root, out) => { out = out || []; const rec = (r) => { for (const e of r.querySelectorAll('*')) { out.push(e); if (e.shadowRoot) rec(e.shadowRoot) } }; rec(root); return out }` diff --git a/internal/check-packet-render.mjs b/internal/check-packet-render.mjs index df8f27f6a..3038f5306 100644 --- a/internal/check-packet-render.mjs +++ b/internal/check-packet-render.mjs @@ -29,7 +29,8 @@ import { chromium } from 'playwright' import { existsSync, readFileSync } from 'node:fs' import { resolve, dirname } from 'node:path' -import { pathToFileURL } from 'node:url' +import { pathToFileURL, fileURLToPath } from 'node:url' +import { requirePathInThisCheckout } from './_serves-this-tree.mjs' const target = process.argv[2] || process.env.PACKET_HTML if (!target) { @@ -43,6 +44,8 @@ if (!existsSync(file)) { console.error(`PACKET-RENDER: FAIL — no such packet: ${file}`) process.exit(1) } +// Measure THIS build, or measure nothing (exit 75 = NOT MEASURED; see a9550858 / _serves-this-tree.mjs). +requirePathInThisCheckout(file, resolve(dirname(fileURLToPath(import.meta.url)), '..'), 'PACKET-RENDER') const browser = await chromium.launch() const page = await browser.newPage({ viewport: { width: 1500, height: 1941 } }) diff --git a/internal/check-poster-prints.mjs b/internal/check-poster-prints.mjs index e5962470e..0ef468efb 100644 --- a/internal/check-poster-prints.mjs +++ b/internal/check-poster-prints.mjs @@ -29,6 +29,8 @@ const REPO = process.env.DOXA_REPO // other builder. DOXA_REPO still wins; only the default is made portable, using the idiom // check-no-absolute-paths.mjs prints in its own failure text. || resolve(dirname(fileURLToPath(import.meta.url)), '..') +// Measure THIS build, or measure nothing (exit 75 = NOT MEASURED; see a9550858 / _serves-this-tree.mjs). +{ const { requireThisRepo } = await import('./_serves-this-tree.mjs'); requireThisRepo(REPO, resolve(dirname(fileURLToPath(import.meta.url)), '..'), 'POSTER-PRINTS') } const src = readFileSync(resolve(REPO, 'library/composables/usePosterPrints.js'), 'utf8') const frame = readFileSync(resolve(REPO, 'library/composables/printInPlace.js'), 'utf8') // The composable dynamically imports its renderers by RELATIVE path. Under a blob diff --git a/internal/check-tabs-localized.mjs b/internal/check-tabs-localized.mjs index 1148a38b5..efcee808c 100644 --- a/internal/check-tabs-localized.mjs +++ b/internal/check-tabs-localized.mjs @@ -47,6 +47,8 @@ import { chromium } from 'playwright' const REPO = process.env.DOXA_REPO || resolve(dirname(fileURLToPath(import.meta.url)), '..') +// Measure THIS build, or measure nothing (exit 75 = NOT MEASURED; see a9550858 / _serves-this-tree.mjs). +{ const { requireThisRepo } = await import('./_serves-this-tree.mjs'); requireThisRepo(REPO, resolve(dirname(fileURLToPath(import.meta.url)), '..'), 'TABS-LOCALIZED') } const LOCALES = join(REPO, 'library/i18n/locales') // The tab ids research-map renders, in order, with the KEY each resolves through. diff --git a/internal/check-vision-card-numbers.mjs b/internal/check-vision-card-numbers.mjs index 939482642..7b37d4e79 100644 --- a/internal/check-vision-card-numbers.mjs +++ b/internal/check-vision-card-numbers.mjs @@ -47,6 +47,9 @@ for (const k of Object.keys(expect)) { const i = process.argv.indexOf('--url') if (i > -1) { + // Measure THIS build, or measure nothing (exit 75 = NOT MEASURED; see a9550858 / _serves-this-tree.mjs). + const { requireThisTree } = await import('./_serves-this-tree.mjs') + await requireThisTree(process.argv[i + 1], ROOT, 'VISION-CARD-NUMBERS') const { chromium } = await import('playwright') const b = await chromium.launch() const p = await b.newPage({ viewport: { width: 900, height: 1000 } })