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 } })