From 577149afb498fb9863db9800baddb04db8728568 Mon Sep 17 00:00:00 2001
From: doxa-maps-88655c70
<88627970+doxa-maps-88655c70@users.noreply.github.com>
Date: Fri, 25 Sep 2026 16:23:43 +0000
Subject: [PATCH 1/5] check:experimental-shelf: test the partner embeds too,
from another origin
Two surfaces join the leaf and the Gallery, each built from the manifest's own snippets so a
broken snippet fails as well:
B a host on a different origin iframes the leaf; the plugin must draw its data and the
host's title, URL and scroll must not move.
C a host on a different origin pastes embed.script with its own token and provider. Under
maplibre: 0 api.mapbox.com requests. Under mapbox, where the plugin draws a Mapbox map, a
second run with a fake host token must show that token, and no other, in tile requests.
MapLibre-only plugins take no token and report n/a.
The self-test now also poisons the bundle under the script embed. Rows print their seconds;
--surfaces A,B,C,D splits the ~26 min run. Measured: 7 bundles, 60 rows PASS, 3 n/a.
B does not assert 0 api.mapbox.com: every generated leaf head links Mapbox JS/CSS whatever
the provider, which is a template property reported separately.
---
internal/check-experimental-shelf.mjs | 111 ++++++++++++++++++++++----
1 file changed, 96 insertions(+), 15 deletions(-)
diff --git a/internal/check-experimental-shelf.mjs b/internal/check-experimental-shelf.mjs
index e7221dd99..38dd9d31a 100644
--- a/internal/check-experimental-shelf.mjs
+++ b/internal/check-experimental-shelf.mjs
@@ -7,8 +7,18 @@
* Both sets are read from plugin-designs/doxa-maps-spa/doxa-maps-spa.vue, so a bundle that joins or
* leaves the shelf changes what this gate measures without an edit here.
*
- * PER BUNDLE, PER ENGINE (mapbox, maplibre), on TWO surfaces: its own leaf page (A) and its tab
- * inside the Research Gallery (D). A surface passes when:
+ * PER BUNDLE, PER ENGINE (mapbox, maplibre), on FOUR surfaces:
+ * A its own leaf page
+ * B a host page on a DIFFERENT ORIGIN that iframes the leaf, built from the manifest's own
+ * embed.iframe snippet; the host's title, URL and scroll must not change
+ * C a host page on a different origin that pastes the manifest's own embed.script snippet with
+ * the HOST's token and provider filled in. Under maplibre it must make 0 api.mapbox.com
+ * requests; under mapbox, where the plugin draws a Mapbox map, a second run with a fake host
+ * token must show that token in its tile requests and no other (the host's token is used,
+ * not one baked into the bundle). A MapLibre-only plugin takes no token: reported n/a
+ * D its tab inside the Research Gallery
+ * B and C test exactly what a partner pastes, so a broken snippet fails here too.
+ * A surface passes when:
* - the element mounted with content (its shadow tree has real nodes),
* - 0 page errors and 0 failed requests (4xx/5xx; Mapbox telemetry and favicons excluded),
* - no loading text is left on screen (Loading / Adding / Computing),
@@ -36,12 +46,16 @@ import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawn } from 'node:child_process'
import { createServer } from 'node:net'
+import { createServer as createHttpServer } from 'node:http'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2)
const arg = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
const ENGINES = arg('--engines', 'mapbox,maplibre').split(',')
const ONLY = arg('--only', '') ? arg('--only', '').split(',') : null
+// --surfaces A,B,C,D (default all). The full shelf is ~60 page loads, ~20 min under SwiftShader:
+// run it with BROWSER_MAX_HOLD raised, or split it by --only / --surfaces.
+const SURFACES = arg('--surfaces', 'A,B,C,D').split(',')
const SELF_TEST = !argv.includes('--no-self-test')
const WAIT_MS = Number(process.env.SHELF_WAIT_MS || 75000)
@@ -84,6 +98,39 @@ process.on('exit', reap); for (const s of ['SIGINT', 'SIGTERM']) process.on(s, (
const BASE = `http://127.0.0.1:${port}/doxa-maps`
for (let i = 0; i < 40; i++) { try { if ((await fetch(BASE + '/manifest.json')).ok) break } catch (_) {} await new Promise(r => setTimeout(r, 250)) }
+// ── the PARTNER: a host site on a different origin (localhost vs 127.0.0.1) ────────────────
+// Its pages are built from the manifest's own embed snippets, placeholders filled the way the
+// snippet's comments tell a partner to.
+const byBundle = Object.fromEntries(manifest.bundles.map(x => [x.bundle, x]))
+const snippet = (bundle, profile, kind) => ((byBundle[bundle].profiles.find(x => x.profile === profile) || {}).embed || {})[kind] || ''
+function hostPage(kind, s, eng, token) {
+ let body
+ if (kind === 'B') {
+ const leafUrl = `${BASE}/${s.bundle}/${s.profile}/index.html?provider=${eng}` + (eng === 'mapbox' ? `&tk=${token}` : '')
+ body = snippet(s.bundle, s.profile, 'iframe').replace(/src="[^"]*"/, `src="${leafUrl}&utm_source=shelf"`).replace(/height="\d+"/, 'height="720"')
+ } else {
+ body = snippet(s.bundle, s.profile, 'script')
+ .replaceAll('src="./', `src="${BASE}/`).replaceAll('href="./', `href="${BASE}/`)
+ .replaceAll('YOUR_MAPBOX_TOKEN', token).replaceAll('YOUR_SOURCE', 'shelf').replaceAll('YOUR_TREE_URL', BASE)
+ if (eng === 'maplibre') body = body.replace(/\s* ]*>\s*/i, '')
+ .replace(/profile-config='\{/, `profile-config='{"provider":"maplibre",`)
+ }
+ return `
PARTNER ${kind}
+
+${body} `
+}
+const FAKE_TK = 'pk.SHELFHOSTTOKEN' + 'x'.repeat(20)
+const hostSrv = createHttpServer((req, res) => {
+ const m = req.url.match(/^\/(B|C)\/([a-z0-9-]+)\/(mapbox|maplibre)(\/fake)?/)
+ const s = m && shelf.find(x => x.bundle === m[2])
+ if (!s) { res.writeHead(404); return res.end('no') }
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
+ res.end(hostPage(m[1], s, m[3], m[4] ? FAKE_TK : TK))
+})
+const hostPort = await new Promise(ok => hostSrv.listen(0, '127.0.0.1', () => ok(hostSrv.address().port)))
+const HOST = `http://localhost:${hostPort}`
+process.on('exit', () => { try { hostSrv.close() } catch (_) {} })
+
// ── every Map the page builds, recorded without touching the bundle ───────────
const RECORD_MAPS = () => {
window.__shelfMaps = []
@@ -121,14 +168,14 @@ const PROBE = (tag) => `(() => {
const layers = (st.layers || []).filter(l => geoIds.includes(l.source)).map(l => l.id)
if (layers.length) drawn = m.queryRenderedFeatures({ layers }).length
} catch (_) {}
- return { loaded: !!(m.__shelfLoaded || (m.isStyleLoaded && m.isStyleLoaded())), geo, drawn }
+ return { loaded: !!(m.__shelfLoaded || (m.isStyleLoaded && m.isStyleLoaded())), geo, drawn, mapbox: !!(window.mapboxgl && m instanceof window.mapboxgl.Map) }
})
return { own, sized, host: !!host, busy, maps }
})()`.replace('__TAG__', tag)
const b = await chromium.launch({ headless: true, args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl', '--no-sandbox'] })
-async function measure(url, s, poisonBundle) {
+async function measure(url, s, poisonBundle, opts = {}) {
const p = await b.newPage({ viewport: { width: 1280, height: 800 } })
await p.addInitScript(RECORD_MAPS)
const seed = SEED[s.bundle]
@@ -137,27 +184,41 @@ async function measure(url, s, poisonBundle) {
Storage.prototype.getItem = function (k) { const v = get.call(this, k); return (v == null && String(k).startsWith(keyPrefix)) ? value : v }
}, seed)
if (poisonBundle) await p.route(new RegExp(`/${poisonBundle}/${poisonBundle}\\.js(\\?|$)`), r => r.fulfill({ status: 200, contentType: 'text/javascript', body: 'throw new Error("shelf self-test: deliberately broken bundle")' }))
- const errs = [], fails = []
+ const errs = [], fails = [], mapboxReqs = [], tokens = new Set()
+ p.on('request', r => { const u = r.url(); if (/api\.mapbox\.com/.test(u)) mapboxReqs.push(u.replace(/\?.*/, '').slice(-60)); const t = u.match(/access_token=([^&]+)/); if (t) tokens.add(t[1]) })
p.on('pageerror', e => errs.push(e.message.slice(0, 140)))
p.on('response', r => { if (r.status() >= 400 && !/events\.mapbox\.com|favicon/.test(r.url())) fails.push(r.status() + ' ' + r.url().replace(/\?.*/, '').slice(-80)) })
await p.goto(url)
+ const host0 = opts.frame ? await p.evaluate(() => ({ title: document.title, href: location.href, y: scrollY })) : null
+ // B: the plugin lives in the iframe whose document comes from the tree
+ const ctx = () => (opts.frame ? p.frames().find(f => f.url().startsWith(BASE)) : p.mainFrame()) || p.mainFrame()
let st = null
const t0 = Date.now()
- while (Date.now() - t0 < WAIT_MS) {
+ // provenance needs one tile request, not a drawn map: a fake token cannot draw anything
+ while (Date.now() - t0 < (opts.provenanceOnly ? Math.min(WAIT_MS, 25000) : WAIT_MS)) {
await p.waitForTimeout(2500)
- st = await p.evaluate(PROBE(s.tag))
+ try { st = await ctx().evaluate(PROBE(s.tag)) } catch (_) { st = null; continue }
const mapsReady = st.maps.every(m => m.loaded && m.drawn > 0)
- if (st.own > 8 && st.sized && st.busy === 0 && mapsReady && Date.now() - t0 > 6000) break
+ if (opts.provenanceOnly ? tokens.has(FAKE_TK) : (st.own > 8 && st.sized && st.busy === 0 && mapsReady && Date.now() - t0 > 6000)) break
}
+ const host1 = opts.frame ? await p.evaluate(() => ({ title: document.title, href: location.href, y: scrollY })) : null
await p.close()
+ if (opts.provenanceOnly) {
+ // C provenance: with a FAKE host token the tiles cannot load, so only one thing is asserted:
+ // the requests carry the HOST's token, not a token baked into the bundle.
+ const used = tokens.has(FAKE_TK), baked = [...tokens].filter(t => t !== FAKE_TK)
+ return { pass: used && !baked.length, why: used ? (baked.length ? [`a token other than the host's was also used (${baked[0].slice(0, 14)}…)`] : []) : ['the host token never reached a tile request'], maps: 0 }
+ }
const why = []
if (!st || !st.host) why.push(`<${s.tag}> never appeared`)
else if (st.own <= 8 || !st.sized) why.push(`<${s.tag}> mounted empty (${st.own} node(s)${st.sized ? '' : ', not sized'})`)
if (st && st.busy) why.push(`${st.busy} loading label(s) left`)
if (st) st.maps.forEach((m, i) => { if (!m.loaded) why.push(`map ${i + 1}: 'load' never fired`); else if (!m.drawn) why.push(`map ${i + 1}: no own data drawn (${m.geo} geojson source(s))`) })
+ if (host0 && host1 && (host0.title !== host1.title || host0.href !== host1.href || host0.y !== host1.y)) why.push(`the host page changed (title/URL/scroll): ${JSON.stringify(host0)} -> ${JSON.stringify(host1)}`)
+ if (opts.noMapbox && mapboxReqs.length) why.push(`${mapboxReqs.length} api.mapbox.com request(s) under maplibre, e.g. ${mapboxReqs[0]}`)
if (errs.length) why.push(`page error: ${errs[0]}`)
if (fails.length) why.push(`failed request: ${fails[0]}`)
- return { pass: !why.length, why, maps: st ? st.maps.length : 0 }
+ return { pass: !why.length, why, maps: st ? st.maps.length : 0, mapboxMaps: st ? st.maps.filter(m => m.mapbox).length : 0 }
}
const leaf = (s, eng) => `${BASE}/${s.bundle}/${s.profile}/index.html?provider=${eng}` + (eng === 'mapbox' ? `&tk=${TK}` : '')
@@ -168,18 +229,38 @@ try {
if (SELF_TEST) {
const s = shelf[0]
const a = await measure(leaf(s, 'maplibre'), s, s.bundle), d = await measure(gallery(s, 'maplibre'), s, s.bundle)
- if (a.pass || d.pass) { console.error(`SELF-TEST FAILED: a deliberately broken ${s.bundle} still passed (${a.pass ? 'leaf' : ''} ${d.pass ? 'gallery' : ''}). This gate cannot see a broken bundle.`); exit = 3 }
- else console.log(`self-test: a deliberately broken ${s.bundle} fails as it must (leaf: ${a.why[0]}; gallery: ${d.why[0]})`)
+ const c = await measure(`${HOST}/C/${s.bundle}/maplibre`, s, s.bundle, { noMapbox: true })
+ if (a.pass || d.pass || c.pass) { console.error(`SELF-TEST FAILED: a deliberately broken ${s.bundle} still passed (${a.pass ? 'leaf ' : ''}${d.pass ? 'gallery ' : ''}${c.pass ? 'script-embed' : ''}). This gate cannot see a broken bundle.`); exit = 3 }
+ else console.log(`self-test: a deliberately broken ${s.bundle} fails as it must (leaf: ${a.why[0]}; script-embed: ${c.why[0]}; gallery: ${d.why[0]})`)
}
if (exit !== 3) {
for (const [k, v] of Object.entries(SEED)) if (shelf.some(s => s.bundle === k)) console.log(`SEEDED ${k}: ${v.why}`)
console.log(`EXPERIMENTAL SHELF — ${shelf.length} bundle(s): ${shelf.map(s => s.bundle).join(', ')} · engines: ${ENGINES.join(', ')}`)
- for (const s of shelf) for (const eng of ENGINES) for (const [surf, url] of [['A leaf', leaf(s, eng)], ['D gallery', gallery(s, eng)]]) {
- const r = await measure(url, s)
+ // B does NOT assert 0 api.mapbox.com: the iframe loads the LEAF page, and every generated leaf
+ // links Mapbox's JS/CSS statically whatever the provider (generate-tree's template).
+ // That is a fleet-wide leaf property, reported separately; C is where a partner chooses.
+ const row = (r, s, eng, surf, t0) => {
if (!r.pass) exit = 1
- console.log(`${r.pass ? 'PASS' : 'FAIL'} ${s.bundle.padEnd(24)} ${eng.padEnd(9)} ${surf.padEnd(10)} ${r.maps ? r.maps + ' map(s)' : 'no map'}${r.pass ? '' : ' — ' + r.why.join('; ')}`)
+ console.log(`${r.pass ? 'PASS' : 'FAIL'} ${s.bundle.padEnd(24)} ${eng.padEnd(9)} ${surf.padEnd(10)} ${(r.maps ? r.maps + ' map(s)' : 'no map').padEnd(9)} ${String(Math.round((Date.now() - t0) / 1000)).padStart(3)}s${r.pass ? '' : ' — ' + r.why.join('; ')}`)
}
- console.log(exit ? 'EXPERIMENTAL-SHELF: FAIL' : `EXPERIMENTAL-SHELF: PASS — every experimental plugin loads on its own page and in the Gallery, under ${ENGINES.join(' and ')}`)
+ for (const s of shelf) for (const eng of ENGINES) for (const [surf, url, opts] of [
+ ['A leaf', leaf(s, eng), {}],
+ ['B iframe', `${HOST}/B/${s.bundle}/${eng}`, { frame: true }],
+ ['C script', `${HOST}/C/${s.bundle}/${eng}`, { noMapbox: eng === 'maplibre' }],
+ ['D gallery', gallery(s, eng), {}],
+ ].filter(x => SURFACES.includes(x[0][0]))) {
+ let t0 = Date.now()
+ const r = await measure(url, s, null, opts)
+ row(r, s, eng, surf, t0)
+ // C host-tk: only where the script embed really made a MAPBOX map. A MapLibre-only plugin
+ // (prayer-walk-planner) takes no token at all, so there is nothing to prove.
+ if (surf === 'C script' && eng === 'mapbox' && r.pass) {
+ if (!r.mapboxMaps) { console.log(`n/a ${s.bundle.padEnd(24)} ${eng.padEnd(9)} C host-tk — no Mapbox map on this plugin, it takes no token`); continue }
+ t0 = Date.now()
+ row(await measure(`${HOST}/C/${s.bundle}/mapbox/fake`, s, null, { provenanceOnly: true }), s, eng, 'C host-tk', t0)
+ }
+ }
+ console.log(exit ? 'EXPERIMENTAL-SHELF: FAIL' : `EXPERIMENTAL-SHELF: PASS — every experimental plugin works on its own page, iframed and script-embedded on another site, and in the Gallery, under ${ENGINES.join(' and ')}`)
}
} finally { await b.close(); reap() }
process.exit(exit)
From 44ef31b2c66d724ad5e830d817c1d64d421d8ec0 Mon Sep 17 00:00:00 2001
From: doxa-maps-88655c70
<88627970+doxa-maps-88655c70@users.noreply.github.com>
Date: Fri, 25 Sep 2026 15:42:10 +0000
Subject: [PATCH 2/5] Tooling reads the tree's name from the build config, in
one place
The output tree's name has always been configurable (vite.doxa-maps-build-config.json "name"),
but 61 tooling files ignored it and hardcoded 'doxa-maps' (181 literals), so a rename would have
moved the build while the gates kept reading the old folder.
- internal/_tree-name.mjs reads it once (validated as one URL-safe path segment; falls back to the
historical name) and exports TREE_NAME / TREE_NAME_RE; `node internal/_tree-name.mjs` prints it
for shell scripts.
- A codemod rewrote 171 literals in internal/ to TREE_NAME; the regex literals, messages with
backticks, atomic-public-swap.sh (fails loudly if the name cannot be read), build-lock.sh and
vite.config.js (its duplicate readMfeName is gone) were done by hand.
- The name is unchanged: old and new tooling produce all 1408 files under public/ byte for byte.
Code lines still naming the tree in internal/ + vite.config.js: 2, both intended (the helper's
fallback, the private-repo-name pattern in public-hygiene-check).
This renames nothing visible; the eventual rename becomes a one-line config change.
---
internal/_serves-this-tree.mjs | 5 +-
internal/_tree-name.mjs | 45 +++++++++++++++
internal/acceptance/interop-harness.mjs | 5 +-
internal/atomic-public-swap.sh | 23 +++++---
internal/build-lock.sh | 2 +-
internal/check-a11y-button-names.mjs | 7 ++-
internal/check-aria-labels-localized.mjs | 3 +-
internal/check-basemap-labels-localized.mjs | 5 +-
internal/check-capability-citations.mjs | 3 +-
internal/check-capability-exemplars.mjs | 3 +-
internal/check-catalog-currency.mjs | 13 +++--
internal/check-catalog-identity.mjs | 5 +-
internal/check-composite-embed-knobs.mjs | 7 ++-
internal/check-composite-freshness.mjs | 7 ++-
internal/check-composite-srcs.mjs | 3 +-
internal/check-element-census-vs-live.mjs | 5 +-
internal/check-embed-base-resolution.mjs | 9 +--
internal/check-embed-basemap-paints.mjs | 3 +-
internal/check-embedded-rtl.mjs | 7 ++-
internal/check-experimental-shelf.mjs | 5 +-
internal/check-filter-width-stable.mjs | 3 +-
internal/check-fits-the-screen.mjs | 11 ++--
internal/check-fixed-anchoring.mjs | 3 +-
internal/check-gospel-composed-filters.mjs | 3 +-
internal/check-gospel-english-leaks.mjs | 3 +-
internal/check-gospel-leaves-mount.mjs | 5 +-
internal/check-host-locale-contract.mjs | 9 +--
internal/check-legend-export-reachable.mjs | 3 +-
internal/check-legend-info-localized.mjs | 3 +-
internal/check-locale-reachable.mjs | 3 +-
internal/check-manifest-covers-discovery.mjs | 5 +-
internal/check-map-embed-cells.mjs | 7 ++-
internal/check-maplibre-layer-drops.mjs | 5 +-
internal/check-nav-reachable-mobile.mjs | 5 +-
internal/check-nested-token-delivery.mjs | 3 +-
internal/check-no-internal-codename.mjs | 3 +-
internal/check-one-palette.mjs | 5 +-
internal/check-poster-card-fits.mjs | 5 +-
internal/check-poster-wall-localised.mjs | 5 +-
internal/check-prayer-walk-planner.mjs | 9 +--
internal/check-profile-descriptions.mjs | 5 +-
internal/check-provider-switch.mjs | 3 +-
internal/check-published-matches-build.mjs | 7 ++-
internal/check-research-gallery-embed.mjs | 11 ++--
internal/check-share-policy.mjs | 3 +-
...check-shared-component-reached-bundles.mjs | 3 +-
internal/check-shot-freshness.mjs | 9 +--
internal/check-spa-tab-order.mjs | 3 +-
internal/check-staging-forwards-lang.mjs | 5 +-
internal/check-upg-map-data-painted.mjs | 9 +--
internal/clean.mjs | 5 +-
internal/drop-portability-check.mjs | 3 +-
internal/embed-smoke.mjs | 7 ++-
internal/fix-sri.mjs | 3 +-
internal/generate-capability-exemplars.mjs | 5 +-
internal/generate-gallery.mjs | 11 ++--
internal/generate-tree.mjs | 7 ++-
internal/inline-parse-check.mjs | 3 +-
internal/measure-map-load.mjs | 9 +--
internal/plugin-gallery/dev-plugin.mjs | 7 ++-
internal/public-hygiene-check.mjs | 3 +-
internal/readme-shots-fix.mjs | 3 +-
internal/readme-shots.mjs | 55 ++++++++++---------
internal/serve-strict.mjs | 9 +--
internal/set-app-start-page.mjs | 5 +-
internal/settled-tree-gate.mjs | 3 +-
internal/smoke.mjs | 5 +-
internal/sri-consistency-check.mjs | 5 +-
internal/sync-dropin.mjs | 3 +-
internal/verify-5-capabilities.mjs | 3 +-
internal/verify-browser-all.mjs | 9 +--
internal/verify-gospel-map.mjs | 9 +--
internal/verify-map-paint.mjs | 5 +-
internal/verify-shell-scroll.mjs | 5 +-
vite.config.js | 21 +++----
75 files changed, 320 insertions(+), 204 deletions(-)
create mode 100644 internal/_tree-name.mjs
diff --git a/internal/_serves-this-tree.mjs b/internal/_serves-this-tree.mjs
index 34133c4eb..8d4bc86e1 100644
--- a/internal/_serves-this-tree.mjs
+++ b/internal/_serves-this-tree.mjs
@@ -22,8 +22,9 @@
import { readFileSync, existsSync, realpathSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { resolve, dirname, join } from 'node:path'
+import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
-const MANIFEST = 'doxa-maps/manifest.json'
+const MANIFEST = `${TREE_NAME}/manifest.json`
/**
* @param {string} base e.g. http://127.0.0.1:4173 (a trailing /doxa-maps/ is tolerated)
@@ -37,7 +38,7 @@ export async function servesThisTree(base, repo) {
} catch {
return { ok: false, why: `no local public/${MANIFEST} to compare against — build the tree first` }
}
- const root = String(base).replace(/\/+$/, '').replace(/\/doxa-maps$/, '')
+ const root = String(base).replace(/\/+$/, '').replace(new RegExp(`/${TREE_NAME_RE}$`), '')
const r = await fetch(`${root}/${MANIFEST}`).catch(() => null)
if (!r || !r.ok) return { ok: false, why: `nothing serving ${MANIFEST} at ${root}` }
const served = createHash('sha256').update(Buffer.from(await r.arrayBuffer())).digest('hex')
diff --git a/internal/_tree-name.mjs b/internal/_tree-name.mjs
new file mode 100644
index 000000000..eeefde608
--- /dev/null
+++ b/internal/_tree-name.mjs
@@ -0,0 +1,45 @@
+/**
+ * _tree-name.mjs — the ONE place the output tree's name is read.
+ *
+ * The built tree lives at public// and every embeddable URL carries it
+ * (…////index.html). Its name has always been configurable —
+ * vite.doxa-maps-build-config.json `name` — but a census on 2026-09-25 found 61 tooling files
+ * that ignored the knob and hardcoded 'doxa-maps' (181 literals), so changing the config renamed
+ * the build while those gates kept reading the old folder. Every tool now imports TREE_NAME from
+ * here, which makes renaming the tree a one-line config change (see
+ * _qa-cycle/03-deployment-cycle/qa-loops/RENAME-IMPACT-dx.md, mechanism A).
+ *
+ * import { TREE_NAME, BUILD_CONFIG_FILE } from './_tree-name.mjs'
+ * TREE=$(node internal/_tree-name.mjs) # from a shell script
+ *
+ * The fallback, used only when the config is missing or has no name, is the historical name,
+ * so a checkout without the config behaves exactly as before.
+ */
+import { readFileSync } from 'node:fs'
+import { resolve, dirname } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
+export const BUILD_CONFIG_FILE = resolve(ROOT, 'vite.doxa-maps-build-config.json')
+const FALLBACK = 'doxa-maps'
+
+function read() {
+ let name = ''
+ try {
+ const cfg = JSON.parse(readFileSync(BUILD_CONFIG_FILE, 'utf8'))
+ if (cfg && typeof cfg.name === 'string') name = cfg.name.trim()
+ } catch { /* no config: the fallback */ }
+ if (!name) return FALLBACK
+ // A tree name is ONE path segment that also appears in public URLs: refuse anything that could
+ // escape public/ or need escaping in a URL, rather than build into a surprising place.
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(name) || name.includes('..')) {
+ throw new Error(`[_tree-name] build config "name" must be a single URL-safe path segment, got ${JSON.stringify(name)}`)
+ }
+ return name
+}
+
+export const TREE_NAME = read()
+// The name escaped for use inside a RegExp source (a '.' in a future name must not match any char).
+export const TREE_NAME_RE = TREE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+
+if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) process.stdout.write(TREE_NAME + '\n')
diff --git a/internal/acceptance/interop-harness.mjs b/internal/acceptance/interop-harness.mjs
index 91ce100e4..71fec1c68 100644
--- a/internal/acceptance/interop-harness.mjs
+++ b/internal/acceptance/interop-harness.mjs
@@ -68,6 +68,7 @@ import { setTimeout as sleep } from 'node:timers/promises'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { execFileSync } from 'node:child_process'
+import { TREE_NAME } from '../_tree-name.mjs'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const ROOT = resolve(__dirname, '..', '..')
@@ -178,7 +179,7 @@ async function readGrid(page) {
async function runDesktop(browser) {
const PAIR = 'desktop dashboard-plus-grid'
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } })
- const url = `${BASE}/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html`
+ const url = `${BASE}/${TREE_NAME}/dashboard-plus-grid/dashboard-plus-grid/index.html`
console.log(`\n=== ${PAIR} — ${url} ===`)
try {
await page.goto(url, { waitUntil: 'load', timeout: 60000 })
@@ -344,7 +345,7 @@ async function runDesktop(browser) {
async function runMobile(browser) {
const PAIR = 'mobile research-map-plus-grid'
const page = await browser.newPage({ viewport: { width: 420, height: 900 } })
- const url = `${BASE}/doxa-maps/research-map-plus-grid/research-map-plus-grid/index.html`
+ const url = `${BASE}/${TREE_NAME}/research-map-plus-grid/research-map-plus-grid/index.html`
console.log(`\n=== ${PAIR} — ${url} ===`)
try {
await page.goto(url, { waitUntil: 'load', timeout: 60000 })
diff --git a/internal/atomic-public-swap.sh b/internal/atomic-public-swap.sh
index 65c976448..b99331145 100755
--- a/internal/atomic-public-swap.sh
+++ b/internal/atomic-public-swap.sh
@@ -34,11 +34,16 @@ DRY_RUN="${DRY_RUN:-0}"
die() { echo "❌ atomic-public-swap: $*" >&2; exit 1; }
-[ -n "$STAGE_DIR" ] || die "usage: atomic-public-swap.sh (dir holding a built doxa-maps/ + index.html)"
+# The tree's name comes from the build config (internal/_tree-name.mjs), never a literal: this script
+# renames live folders, so it must fail rather than swap the wrong one if the name cannot be read.
+TREE="$(node "$(dirname "$0")/_tree-name.mjs")" || die "could not read the tree name (internal/_tree-name.mjs)"
+[ -n "$TREE" ] || die "empty tree name from internal/_tree-name.mjs"
+
+[ -n "$STAGE_DIR" ] || die "usage: atomic-public-swap.sh (dir holding a built $TREE/ + index.html)"
[ -d "$STAGE_DIR" ] || die "STAGE_DIR '$STAGE_DIR' is not a directory"
[ -d "$PUBLIC_DIR" ] || die "PUBLIC_DIR '$PUBLIC_DIR' is not a directory (run from the studio root)"
-STAGE_TREE="$STAGE_DIR/doxa-maps"
+STAGE_TREE="$STAGE_DIR/$TREE"
STAGE_INDEX="$STAGE_DIR/index.html"
# --- GATE: the stage must be a COMPLETE, verified tree before we touch the live web-root ---------------------
@@ -81,12 +86,12 @@ stage_dev=$(stat -c '%d' "$STAGE_DIR")
pub_dev=$(stat -c '%d' "$PUBLIC_DIR")
[ "$stage_dev" = "$pub_dev" ] || die "STAGE_DIR and PUBLIC_DIR are on different filesystems ($stage_dev != $pub_dev) — rename() would not be atomic. Stage under the same volume as public/."
-OLD_TREE="$PUBLIC_DIR/doxa-maps.old.$$"
+OLD_TREE="$PUBLIC_DIR/$TREE.old.$$"
if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN — plan (no changes):"
- echo " 1. mv $PUBLIC_DIR/doxa-maps -> $OLD_TREE"
- echo " 2. mv $STAGE_TREE -> $PUBLIC_DIR/doxa-maps"
+ echo " 1. mv $PUBLIC_DIR/$TREE -> $OLD_TREE"
+ echo " 2. mv $STAGE_TREE -> $PUBLIC_DIR/$TREE"
echo " 3. cp $STAGE_INDEX -> $PUBLIC_DIR/index.html"
echo " 4. rm -rf $OLD_TREE"
echo " (NEVER touched: $PUBLIC_DIR/assets/ $PUBLIC_DIR/.gitignore $PUBLIC_DIR/.build.lock)"
@@ -94,11 +99,11 @@ if [ "$DRY_RUN" = "1" ]; then
fi
# --- THE SWAP (scoped; rename-based; public/ itself and the lock never move) ---------------------------------
-if [ -d "$PUBLIC_DIR/doxa-maps" ]; then
- mv "$PUBLIC_DIR/doxa-maps" "$OLD_TREE" # 1. instant; old tree still fully valid under new name
+if [ -d "$PUBLIC_DIR/$TREE" ]; then
+ mv "$PUBLIC_DIR/$TREE" "$OLD_TREE" # 1. instant; old tree still fully valid under new name
fi
-mv "$STAGE_TREE" "$PUBLIC_DIR/doxa-maps" # 2. instant; new tree (built+verified) becomes live
+mv "$STAGE_TREE" "$PUBLIC_DIR/$TREE" # 2. instant; new tree (built+verified) becomes live
cp "$STAGE_INDEX" "$PUBLIC_DIR/index.html" # 3. fresh root gallery page
rm -rf "$OLD_TREE" # 4. drop the superseded tree
-echo "🔀 atomic swap complete — $PUBLIC_DIR/doxa-maps is the freshly-built tree; assets/ + .build.lock untouched."
+echo "🔀 atomic swap complete — $PUBLIC_DIR/$TREE is the freshly-built tree; assets/ + .build.lock untouched."
diff --git a/internal/build-lock.sh b/internal/build-lock.sh
index be1ed19fd..70c89bab1 100755
--- a/internal/build-lock.sh
+++ b/internal/build-lock.sh
@@ -51,7 +51,7 @@ mkdir -p "$(dirname "$LOCK_FILE")"
# that distinguishes coordinator from subagent; anything claiming to is an untested guess.
if [ "${BUILD_ALLOW:-}" != "1" ]; then
echo "🚫 FULL BUILD REFUSED — a full build is almost never what you want here, and its FIRST" >&2
- echo " step deletes public/doxa-maps/ entirely, which tears" >&2
+ echo " step deletes public/$(node "$(dirname "$0")/_tree-name.mjs" 2>/dev/null || echo "")/ entirely, which tears" >&2
echo " the tree out from under every other agent reading it (measured 3+ times, incl. a" >&2
echo " 461-file phantom deletion and several false 'element did not mount' diagnoses)." >&2
echo "" >&2
diff --git a/internal/check-a11y-button-names.mjs b/internal/check-a11y-button-names.mjs
index c765453cd..0c0dc46e8 100644
--- a/internal/check-a11y-button-names.mjs
+++ b/internal/check-a11y-button-names.mjs
@@ -35,10 +35,11 @@ import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright'
import { servesThisTree } from './_serves-this-tree.mjs'
+import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
-const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
-const BASE = process.env.BASE || 'http://127.0.0.1:4173/doxa-maps/'
+const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
+const BASE = process.env.BASE || `http://127.0.0.1:4173/${TREE_NAME}/`
const ALL = process.env.ALL === '1'
if (!existsSync(MANIFEST)) {
@@ -55,7 +56,7 @@ if (!existsSync(MANIFEST)) {
if (!tree.ok) {
console.error('A11Y-BUTTON-NAMES: NOT MEASURED — the server is not serving this checkout.')
console.error(` ${tree.why}`)
- console.error(' Point BASE at your own server (BASE=http://127.0.0.1:PORT/doxa-maps/).')
+ console.error(` Point BASE at your own server (BASE=http://127.0.0.1:PORT/${TREE_NAME}/).`)
process.exit(2)
}
}
diff --git a/internal/check-aria-labels-localized.mjs b/internal/check-aria-labels-localized.mjs
index c67bd36bf..0ae75d9a2 100644
--- a/internal/check-aria-labels-localized.mjs
+++ b/internal/check-aria-labels-localized.mjs
@@ -30,6 +30,7 @@ import { chromium } from 'playwright'
import { requireThisTree } from './_serves-this-tree.mjs'
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
import { fileURLToPath as _fileURLToPath } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const URL_ = process.argv[2] || process.env.ARIA_URL
const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
@@ -43,7 +44,7 @@ await requireThisTree(URL_, ROOT, 'ARIA-LABELS-LOCALIZED')
if (!URL_) {
console.log('ARIA-LABELS-LOCALIZED: SKIP — no leaf url given.')
console.log(' Pass one, or set ARIA_URL. Serve the TREE ROOT (public/) and point at a leaf,')
- console.log(' e.g. doxa-maps/my-upg-100-list/upg-100-list/index.html')
+ console.log(` e.g. ${TREE_NAME}/my-upg-100-list/upg-100-list/index.html`)
process.exit(0)
}
const UNDER_TEST = (process.argv[3] || 'es,ar,zh').split(',').map((s) => s.trim()).filter(Boolean)
diff --git a/internal/check-basemap-labels-localized.mjs b/internal/check-basemap-labels-localized.mjs
index fc7d887c3..79ac7eda5 100644
--- a/internal/check-basemap-labels-localized.mjs
+++ b/internal/check-basemap-labels-localized.mjs
@@ -30,6 +30,7 @@ import { spawn } from 'node:child_process'
import { createServer } from 'node:http'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2)
@@ -41,13 +42,13 @@ const PAIRS = { Germany: 'Германия', Ukraine: 'Украина', Turkey:
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
await new Promise((r) => setTimeout(r, 900))
-const up = await fetch(`http://127.0.0.1:${port}/doxa-maps/${LEAF}/index.html`).then((r) => r.ok).catch(() => false)
+const up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/${LEAF}/index.html`).then((r) => r.ok).catch(() => false)
if (!up) { srv.kill(); console.log(`BASEMAP-LABELS: NOT MEASURED — could not serve ${DIR}`); process.exit(2) }
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] })
async function labels(provider, lang) {
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
- await page.goto(`http://127.0.0.1:${port}/doxa-maps/${LEAF}/index.html?lang=${lang}${provider === 'maplibre' ? '&provider=maplibre' : ''}`, { waitUntil: 'networkidle' })
+ await page.goto(`http://127.0.0.1:${port}/${TREE_NAME}/${LEAF}/index.html?lang=${lang}${provider === 'maplibre' ? '&provider=maplibre' : ''}`, { waitUntil: 'networkidle' })
const FIND = `(() => { const f = (r) => { for (const e of r.querySelectorAll('*')) { if (e.__doxaMap) return e.__doxaMap; if (e.shadowRoot) { const m = f(e.shadowRoot); if (m) return m } } return null }; return f(document) })()`
let ok = false
for (let i = 0; i < 60 && !ok; i++) { await page.waitForTimeout(500); ok = await page.evaluate(`(() => { const m = ${FIND}; return !!(m && m.isStyleLoaded && m.isStyleLoaded()) })()`).catch(() => false) }
diff --git a/internal/check-capability-citations.mjs b/internal/check-capability-citations.mjs
index bbff70299..e6bd30e17 100644
--- a/internal/check-capability-citations.mjs
+++ b/internal/check-capability-citations.mjs
@@ -34,10 +34,11 @@
import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const README = join(ROOT, 'README.md')
-const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
+const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
// ★ WIDENED 2026-09-22 TO documentation/ AS WELL AS README.md.
//
diff --git a/internal/check-capability-exemplars.mjs b/internal/check-capability-exemplars.mjs
index 9d237101b..55e72dbf9 100644
--- a/internal/check-capability-exemplars.mjs
+++ b/internal/check-capability-exemplars.mjs
@@ -22,11 +22,12 @@ import { readFileSync, existsSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const OUT = join(ROOT, 'documentation', 'reference-capability-exemplars.md')
const GEN = join(ROOT, 'internal', 'generate-capability-exemplars.mjs')
-const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
+const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) {
console.log('CAPABILITY-EXEMPLARS: SKIP — no manifest.json (nothing built yet).')
diff --git a/internal/check-catalog-currency.mjs b/internal/check-catalog-currency.mjs
index 588d55e85..d5f8c98f4 100644
--- a/internal/check-catalog-currency.mjs
+++ b/internal/check-catalog-currency.mjs
@@ -33,18 +33,19 @@ import { readFileSync, existsSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const args = process.argv.slice(2)
const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
-const BASE = (argOf('--base', 'https://nuxtbase.app/plugin-assets/doxa-maps')).replace(/\/+$/, '')
+const BASE = (argOf('--base', `https://nuxtbase.app/plugin-assets/${TREE_NAME}`)).replace(/\/+$/, '')
const TIMEOUT = Number(argOf('--timeout', 25000))
const sha384 = (buf) => 'sha384-' + createHash('sha384').update(buf).digest('base64')
-const localManifestPath = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
+const localManifestPath = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(localManifestPath)) {
- console.log('CATALOG-CURRENCY: SKIP — no local build (public/doxa-maps/manifest.json missing). Run `bun run build`.')
+ console.log(`CATALOG-CURRENCY: SKIP — no local build (public/${TREE_NAME}/manifest.json missing). Run \`bun run build\`.`)
process.exit(0)
}
const local = JSON.parse(readFileSync(localManifestPath, 'utf8'))
@@ -125,7 +126,7 @@ for (const b of local.bundles || []) {
const profile = b.profiles?.[0]?.profile
if (!profile) continue
const rel = `${b.bundle}/${profile}/index.html`
- const localPath = join(ROOT, 'public', 'doxa-maps', rel)
+ const localPath = join(ROOT, 'public', TREE_NAME, rel)
if (!existsSync(localPath)) continue
const { buf, err } = await fetchBuf(`${BASE}/${rel}`)
// Same MISSING-vs-UNREACHABLE split as the bundle loop above: a 4xx is the server telling us the
@@ -157,7 +158,7 @@ if (missing.length) {
'\n Something was LOST between the build and the catalog — the merge dropped it, or the publish\n' +
' never included it. This is the guarantee Card 1 Step 1 asks for ("merge all bundles so nothing\n' +
' is lost"). Re-run the publish and assert the resulting bundle SET, not just the changed files:\n' +
- ' rsync -a --delete public/doxa-maps/ /app/doxa-maps/ # --delete mirrors removals\n' +
+ ` rsync -a --delete public/${TREE_NAME}/ /app/${TREE_NAME}/ # --delete mirrors removals\n` +
' then confirm every local bundle name appears live before pushing.'
)
process.exit(1)
@@ -167,7 +168,7 @@ if (stale.length) {
for (const s of stale) console.error(` ✗ ${s.bundle}\n local ${s.want}…\n live ${s.got}…`)
console.error('\n The live catalog is BEHIND this build. Publish per')
console.error(' _qa-cycle/03-deployment-cycle/delegation/RUNBOOK-publish-bundles-to-catalog.md')
- console.error(' (rsync public/doxa-maps/ into the catalog, then push with -c http.postBuffer=524288000).')
+ console.error(` (rsync public/${TREE_NAME}/ into the catalog, then push with -c http.postBuffer=524288000).`)
process.exit(1)
}
console.log('CATALOG-CURRENCY: PASS — every local bundle is present live and matches this build.')
diff --git a/internal/check-catalog-identity.mjs b/internal/check-catalog-identity.mjs
index 59e08205e..f6110859f 100644
--- a/internal/check-catalog-identity.mjs
+++ b/internal/check-catalog-identity.mjs
@@ -34,8 +34,9 @@
import { readFileSync, existsSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { join } from 'node:path'
+import { TREE_NAME } from './_tree-name.mjs'
-const BASE = (process.env.BASE || 'https://nuxtbase.app/plugin-assets/doxa-maps/').replace(/\/?$/, '/')
+const BASE = (process.env.BASE || `https://nuxtbase.app/plugin-assets/${TREE_NAME}/`).replace(/\/?$/, '/')
const CATALOG = process.env.CATALOG || ''
const sha384 = (buf) => 'sha384-' + createHash('sha384').update(buf).digest('base64')
@@ -64,7 +65,7 @@ try {
console.error('CATALOG-IDENTITY: FAIL — the manifest URL did not return JSON.')
console.error(` ${BASE}manifest.json served ${liveManifestBuf.length} bytes that do not parse.`)
console.error(' This is what the LOGIN WALL looks like: HTTP 200 with an HTML sign-in page.')
- console.error(' The public door is /plugin-assets/doxa-maps/ — NOT /app/doxa-maps/.')
+ console.error(` The public door is /plugin-assets/${TREE_NAME}/ — NOT /app/${TREE_NAME}/.`)
process.exit(1)
}
diff --git a/internal/check-composite-embed-knobs.mjs b/internal/check-composite-embed-knobs.mjs
index 69da686ce..09d68150b 100644
--- a/internal/check-composite-embed-knobs.mjs
+++ b/internal/check-composite-embed-knobs.mjs
@@ -49,9 +49,10 @@ import { spawn } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
-const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
+const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) {
console.log('COMPOSITE-EMBED-KNOBS: SKIP — no built manifest (run `bun run tree` first).')
@@ -95,7 +96,7 @@ const bakedToken = (() => {
for (const b of manifest.bundles || []) {
for (const p of b.profiles || []) {
if (!p.isMap || !p.iframeUrl) continue
- const leaf = join(ROOT, 'public', 'doxa-maps', String(p.iframeUrl).replace(/^\.\//, ''))
+ const leaf = join(ROOT, 'public', TREE_NAME, String(p.iframeUrl).replace(/^\.\//, ''))
if (!existsSync(leaf)) continue
const m = readFileSync(leaf, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/)
if (m) return m[1]
@@ -109,7 +110,7 @@ const freePort = () => new Promise((ok) => {
})
const TREE_PORT = await freePort()
const HOST_PORT = await freePort()
-const TREE = `http://127.0.0.1:${TREE_PORT}/doxa-maps`
+const TREE = `http://127.0.0.1:${TREE_PORT}/${TREE_NAME}`
let pageHtml = 'nothing yet'
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' })
diff --git a/internal/check-composite-freshness.mjs b/internal/check-composite-freshness.mjs
index 27e68e402..f618ec3f0 100644
--- a/internal/check-composite-freshness.mjs
+++ b/internal/check-composite-freshness.mjs
@@ -41,16 +41,17 @@ import { spawnSync } from 'node:child_process'
import { tmpdir } from 'node:os'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const DESIGNS = join(ROOT, 'plugin-designs')
-const TREE = join(ROOT, 'public', 'doxa-maps')
+const TREE = join(ROOT, 'public', TREE_NAME)
const argv = process.argv.slice(2)
const oi = argv.indexOf('--only')
const ONLY = oi >= 0 && argv[oi + 1] ? new Set(argv[oi + 1].split(',')) : null
if (!existsSync(join(TREE, 'manifest.json'))) {
- console.log('COMPOSITE-FRESHNESS: SKIP — no built tree (public/doxa-maps/manifest.json).')
+ console.log(`COMPOSITE-FRESHNESS: SKIP — no built tree (public/${TREE_NAME}/manifest.json).`)
process.exit(0)
}
const manifest = JSON.parse(readFileSync(join(TREE, 'manifest.json'), 'utf8'))
@@ -81,7 +82,7 @@ try {
for (const e of composes) if (!resolveEntry(e, bundle)) unresolved.push(`${bundle} composes "${e}" — resolves to no bundle`)
composites++
const committed = join(TREE, bundle, `${bundle}.js`)
- if (!existsSync(committed)) { failures.push(`${bundle}: no committed bundle at public/doxa-maps/${bundle}/${bundle}.js`); continue }
+ if (!existsSync(committed)) { failures.push(`${bundle}: no committed bundle at public/${TREE_NAME}/${bundle}/${bundle}.js`); continue }
const out = join(tmp, bundle)
const r = spawnSync('bun', ['x', 'vite', 'build'], { cwd: ROOT, env: { ...process.env, BUNDLE: bundle, DOXA_OUT_ROOT: out }, encoding: 'utf8' })
const built = join(out, bundle, `${bundle}.js`)
diff --git a/internal/check-composite-srcs.mjs b/internal/check-composite-srcs.mjs
index 64afa73d2..08ffec380 100644
--- a/internal/check-composite-srcs.mjs
+++ b/internal/check-composite-srcs.mjs
@@ -33,6 +33,7 @@
import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
@@ -51,7 +52,7 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// printed: "checked against public/doxa-maps/" looked identical whether the name came from the
// config or from the fallback. Printing WHICH means a future regression shows up in normal output
// instead of waiting for a rename to expose it.
-let treeName = 'doxa-maps'
+let treeName = TREE_NAME
let treeNameSource = 'built-in default (build config unreadable)'
try {
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
diff --git a/internal/check-element-census-vs-live.mjs b/internal/check-element-census-vs-live.mjs
index 86f1ac34b..f327de6fa 100644
--- a/internal/check-element-census-vs-live.mjs
+++ b/internal/check-element-census-vs-live.mjs
@@ -43,6 +43,7 @@ import { createServer } from 'node:http'
import { readFileSync, readdirSync, existsSync } from 'node:fs'
import { resolve, dirname, join, basename } from 'node:path'
import { fileURLToPath } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const PRESET_DIR = join(ROOT, 'internal', 'element-census')
@@ -94,7 +95,7 @@ const port = await new Promise((ok) => { const s = createServer().listen(0, '127
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
const stopSrv = () => { try { srv.kill() } catch { /* already gone */ } }
let up = false
-for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/doxa-maps/manifest.json`).then((r) => r.ok).catch(() => false) }
+for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/manifest.json`).then((r) => r.ok).catch(() => false) }
if (!up) { stopSrv(); console.log(`ELEMENT-CENSUS: NOT MEASURED — could not serve ${DIR}`); process.exit(NOT_MEASURED) }
const THIRD_PARTY = /mapbox|openstreetmap|improve this map|maxar|geoboundaries|©/i
@@ -141,7 +142,7 @@ for (const [name, p] of presets) {
const oursByLeaf = new Map()
let negSeenInOursOnly = null
for (const pg of p.pages) {
- const ours = await read(`http://127.0.0.1:${port}/doxa-maps/${pg.leaf}`)
+ const ours = await read(`http://127.0.0.1:${port}/${TREE_NAME}/${pg.leaf}`)
oursByLeaf.set(pg.leaf, ours)
const theirs = await read(pg.live)
const needH1 = p.requireH1 !== false
diff --git a/internal/check-embed-base-resolution.mjs b/internal/check-embed-base-resolution.mjs
index d27a321be..8be0ca31d 100644
--- a/internal/check-embed-base-resolution.mjs
+++ b/internal/check-embed-base-resolution.mjs
@@ -36,6 +36,7 @@
*/
import { resolve, dirname } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
@@ -53,10 +54,10 @@ try {
// The leaf's own arithmetic is the oracle — computed here rather than hardcoded, so the
// expectation cannot drift from what the generated HTML actually means.
const TREES = [
- ['live catalog', 'https://nuxtbase.app/plugin-assets/doxa-maps'],
- ['dev tree', 'http://127.0.0.1:4173/doxa-maps'],
- ['doxa.life drop-in', 'https://doxa.life/js/doxa-maps-build/doxa-maps'],
- ['deep subpath', 'https://cdn.example.org/a/b/c/doxa-maps'],
+ ['live catalog', `https://nuxtbase.app/plugin-assets/${TREE_NAME}`],
+ ['dev tree', `http://127.0.0.1:4173/${TREE_NAME}`],
+ ['doxa.life drop-in', `https://doxa.life/js/doxa-maps-build/${TREE_NAME}`],
+ ['deep subpath', `https://cdn.example.org/a/b/c/${TREE_NAME}`],
]
const fails = []
diff --git a/internal/check-embed-basemap-paints.mjs b/internal/check-embed-basemap-paints.mjs
index 92137d9cf..3c15e7b66 100644
--- a/internal/check-embed-basemap-paints.mjs
+++ b/internal/check-embed-basemap-paints.mjs
@@ -33,12 +33,13 @@ import { writeFileSync, mkdtempSync, readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
+import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '..')
const PUBLIC = resolve(REPO, 'public')
-const LEAF = 'doxa-maps/gospel-resources/resources-map/index.html'
+const LEAF = `${TREE_NAME}/gospel-resources/resources-map/index.html`
// 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))
diff --git a/internal/check-embedded-rtl.mjs b/internal/check-embedded-rtl.mjs
index 1fdfbec97..4a3ca56a0 100644
--- a/internal/check-embedded-rtl.mjs
+++ b/internal/check-embedded-rtl.mjs
@@ -25,13 +25,14 @@ import { createServer } from 'node:http'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { servesThisTree } from './_serves-this-tree.mjs'
+import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const BASE = process.argv[2] || process.env.BASE_URL || 'http://127.0.0.1:4173'
const HOST_PORT = Number(process.env.RTL_HOST_PORT || 47393)
let reachable = false
-try { const r = await fetch(`${BASE}/doxa-maps/manifest.json`, { signal: AbortSignal.timeout(4000) }); reachable = r.ok } catch { /* precondition */ }
+try { const r = await fetch(`${BASE}/${TREE_NAME}/manifest.json`, { signal: AbortSignal.timeout(4000) }); reachable = r.ok } catch { /* precondition */ }
if (!reachable) { console.log(`EMBEDDED-RTL: SKIP — nothing served at ${BASE}.`); process.exit(0) }
const treeCheck = await servesThisTree(BASE, ROOT)
if (!treeCheck.ok) { console.error(`EMBEDDED-RTL: NOT MEASURED — ${treeCheck.why}`); process.exit(2) }
@@ -52,11 +53,11 @@ const hostHtml = (c) => `
renders zero cards and the locale has nothing to translate. The first run of this gate
omitted it and reported "never mounted" — a harness gap, not the RTL defect. -->