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} + +
A partner's own site
${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. --> `) }).listen(HP, 'localhost') await new Promise(r => setTimeout(r, 800)) @@ -44,10 +45,10 @@ await new Promise(r => setTimeout(r, 800)) // pass or a fail — the same code the shared browser locks use for "busy, re-run later". { const { readFileSync } = await import('node:fs') - const disk = readFileSync(resolve(ROOT, 'public/doxa-maps/manifest.json'), 'utf8') + const disk = readFileSync(resolve(ROOT, `public/${TREE_NAME}/manifest.json`), 'utf8') let served = '' for (let i = 0; i < 20 && !served; i++) { - try { const r = await fetch(`http://127.0.0.1:${TP}/doxa-maps/manifest.json`); if (r.ok) served = await r.text() } catch { /* not up yet */ } + try { const r = await fetch(`http://127.0.0.1:${TP}/${TREE_NAME}/manifest.json`); if (r.ok) served = await r.text() } catch { /* not up yet */ } if (!served) await new Promise(r => setTimeout(r, 300)) } if (served !== disk) { diff --git a/internal/check-profile-descriptions.mjs b/internal/check-profile-descriptions.mjs index 872cc978c..d715ee3e0 100644 --- a/internal/check-profile-descriptions.mjs +++ b/internal/check-profile-descriptions.mjs @@ -34,9 +34,10 @@ import { readFileSync, existsSync } 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') // A description shorter than this is a placeholder, not an explanation. Calibrated against the // REAL corpus rather than a guess (measured 2026-09-22): shortest genuine description 62 chars, @@ -52,7 +53,7 @@ const MIN_CHARS = 25 const MIN_EXPECTED_PROFILES = 20 if (!existsSync(MANIFEST)) { - console.error('PROFILE-DESCRIPTIONS: FAIL — no manifest.json at public/doxa-maps/.') + console.error(`PROFILE-DESCRIPTIONS: FAIL — no manifest.json at public/${TREE_NAME}/.`) console.error(' This gate reads the BUILT manifest (the artifact consumers actually read).') console.error(' Run `bun run build` first; an absent manifest is a failure, not a pass.') process.exit(1) diff --git a/internal/check-provider-switch.mjs b/internal/check-provider-switch.mjs index ff023e0de..686252160 100644 --- a/internal/check-provider-switch.mjs +++ b/internal/check-provider-switch.mjs @@ -16,6 +16,7 @@ import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { chromium } from 'playwright' import { servesThisTree, serveThisTree } from './_serves-this-tree.mjs' +import { TREE_NAME } from './_tree-name.mjs' const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..') @@ -30,7 +31,7 @@ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..') // executed shape check:packets-all had. Default to the upg-100 leaf (the profile this // gate was written for) and SKIP with a named verdict when nothing is served there. const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173') - + '/doxa-maps/my-upg-100-list/upg-100-list/index.html' + + `/${TREE_NAME}/my-upg-100-list/upg-100-list/index.html` let LEAF = process.argv[2] || DEFAULT_LEAF let OWN_SERVER = null { diff --git a/internal/check-published-matches-build.mjs b/internal/check-published-matches-build.mjs index 9b2fa335a..7bc873b7b 100644 --- a/internal/check-published-matches-build.mjs +++ b/internal/check-published-matches-build.mjs @@ -37,12 +37,13 @@ import { readFileSync, existsSync, statSync } 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 HERE = dirname(fileURLToPath(import.meta.url)) const REPO = process.env.DOXA_REPO || resolve(HERE, '..') const PUBLISHED_BASE = (process.env.PUBLISHED_BASE - || 'https://nuxtbase.app/plugin-assets/doxa-maps').replace(/\/$/, '') -const TREE = resolve(REPO, 'public/doxa-maps') + || `https://nuxtbase.app/plugin-assets/${TREE_NAME}`).replace(/\/$/, '') +const TREE = resolve(REPO, `public/${TREE_NAME}`) const MANIFEST = join(TREE, 'manifest.json') if (!existsSync(MANIFEST)) { @@ -133,7 +134,7 @@ if (stale.length) { console.error(' "red dot" pin hint) were all fixed in main and all still visible live, and each') console.error(' one read as a regression against a card marked done.') console.error('\n FIX: publish. That is a COORDINATOR action — deploy:dev runs a full build whose') - console.error(' first step deletes public/doxa-maps/ and tears the tree out from under every') + console.error(` first step deletes public/${TREE_NAME}/ and tears the tree out from under every`) console.error(' other agent, which is why the build guard refuses it without BUILD_ALLOW=1.') process.exit(1) } diff --git a/internal/check-research-gallery-embed.mjs b/internal/check-research-gallery-embed.mjs index ca728a89d..e609563dc 100644 --- a/internal/check-research-gallery-embed.mjs +++ b/internal/check-research-gallery-embed.mjs @@ -40,6 +40,7 @@ import { spawn } from 'node:child_process' import { existsSync } from 'node:fs' 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) @@ -101,10 +102,10 @@ const MAPBOX_KNOWN = { const freePort = () => new Promise(ok => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) }) 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}` -if (!existsSync(resolve(ROOT, 'public/doxa-maps/doxa-maps-spa/doxa-maps-spa.js'))) { - console.log('SKIP check-research-gallery-embed: no built gallery at public/doxa-maps/doxa-maps-spa/') +if (!existsSync(resolve(ROOT, `public/${TREE_NAME}/doxa-maps-spa/doxa-maps-spa.js`))) { + console.log(`SKIP check-research-gallery-embed: no built gallery at public/${TREE_NAME}/doxa-maps-spa/`) process.exit(2) } let chromium @@ -139,10 +140,10 @@ await new Promise(r => setTimeout(r, 800)) // pass or a fail — the same code the shared browser locks use for "busy, re-run later". { const { readFileSync } = await import('node:fs') - const disk = readFileSync(resolve(ROOT, 'public/doxa-maps/manifest.json'), 'utf8') + const disk = readFileSync(resolve(ROOT, `public/${TREE_NAME}/manifest.json`), 'utf8') let served = '' for (let i = 0; i < 20 && !served; i++) { - try { const r = await fetch(`http://127.0.0.1:${TREE_PORT}/doxa-maps/manifest.json`); if (r.ok) served = await r.text() } catch { /* not up yet */ } + try { const r = await fetch(`http://127.0.0.1:${TREE_PORT}/${TREE_NAME}/manifest.json`); if (r.ok) served = await r.text() } catch { /* not up yet */ } if (!served) await new Promise(r => setTimeout(r, 300)) } if (served !== disk) { diff --git a/internal/check-share-policy.mjs b/internal/check-share-policy.mjs index 8b6c8a5a5..951d998e9 100644 --- a/internal/check-share-policy.mjs +++ b/internal/check-share-policy.mjs @@ -43,6 +43,7 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs' import { resolve, join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' +import { TREE_NAME } from './_tree-name.mjs' // Same fix as check-zone-census.mjs, same reason. DOXA_REPO still wins; only the FALLBACK moved // off an absolute path into one author's worktree. check:no-abs-paths rejects those outright — "a @@ -80,7 +81,7 @@ const POLICY = { } const isMap = (src) => /mapboxgl|maplibregl|useMapInstance/.test(src) -const manifest = JSON.parse(readFileSync(join(REPO, 'public', 'doxa-maps', 'manifest.json'), 'utf8')) +const manifest = JSON.parse(readFileSync(join(REPO, 'public', TREE_NAME, 'manifest.json'), 'utf8')) const published = new Set(manifest.bundles.map((b) => b.bundle)) const fails = [] diff --git a/internal/check-shared-component-reached-bundles.mjs b/internal/check-shared-component-reached-bundles.mjs index ae956d6fb..e1a2a158f 100644 --- a/internal/check-shared-component-reached-bundles.mjs +++ b/internal/check-shared-component-reached-bundles.mjs @@ -35,10 +35,11 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs' import { resolve, dirname, join } from 'node:path' import { fileURLToPath } 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, '..') -const TREE = resolve(REPO, 'public/doxa-maps') +const TREE = resolve(REPO, `public/${TREE_NAME}`) // Each entry: a change that landed in a SHARED component, and a string that proves a built bundle // contains it. `source` is where the marker must also still exist — if it does not, the marker is diff --git a/internal/check-shot-freshness.mjs b/internal/check-shot-freshness.mjs index 3fb100eab..c727cffb0 100644 --- a/internal/check-shot-freshness.mjs +++ b/internal/check-shot-freshness.mjs @@ -46,11 +46,12 @@ import { execFileSync } from 'node:child_process' import { createHash } from 'node:crypto' import { resolve, dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' +import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const SHOTS_SRC = join(ROOT, 'internal', 'readme-shots.mjs') const OUT = join(ROOT, 'documentation', 'readme-images') -const BUILT = join(ROOT, 'public', 'doxa-maps') +const BUILT = join(ROOT, 'public', TREE_NAME) if (!existsSync(SHOTS_SRC) || !existsSync(OUT)) { console.log('SHOT-FRESHNESS: SKIP — readme-shots.mjs or documentation/readme-images/ not present.') @@ -64,7 +65,7 @@ const recorded = existsSync(SHOT_BUNDLES) ? JSON.parse(readFileSync(SHOT_BUNDLES const git = (...a) => execFileSync('git', ['-C', ROOT, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) let dirty = null try { - dirty = new Set(git('status', '--porcelain', '--', 'public/doxa-maps', 'documentation/readme-images') + dirty = new Set(git('status', '--porcelain', '--', `public/${TREE_NAME}`, 'documentation/readme-images') .split('\n').filter(Boolean).map((l) => resolve(ROOT, l.slice(3).replace(/^"|"$/g, '')))) } catch { /* not a git checkout */ } function stamp(file) { @@ -88,8 +89,8 @@ for (const { name, path } of shots) { const png = join(OUT, name + '.png') if (!existsSync(png)) { missing.push(name); continue } - const m = path.match(/^\/doxa-maps\/([^/]+)\//) - if (!m) { skipped.push(`${name} (path ${path} is not a /doxa-maps/ leaf)`); continue } + const m = path.match(new RegExp(`^/${TREE_NAME_RE}/([^/]+)/`)) + if (!m) { skipped.push(`${name} (path ${path} is not a /${TREE_NAME}/ leaf)`); continue } const bundle = join(BUILT, m[1], `${m[1]}.js`) if (!existsSync(bundle)) { skipped.push(`${name} (bundle ${m[1]} not built)`); continue } diff --git a/internal/check-spa-tab-order.mjs b/internal/check-spa-tab-order.mjs index aef0c7a91..88cf72228 100644 --- a/internal/check-spa-tab-order.mjs +++ b/internal/check-spa-tab-order.mjs @@ -35,9 +35,10 @@ import { chromium } from 'playwright' import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs' import { fileURLToPath as __f2p } from 'node:url' import { dirname as __dn, resolve as __rs } from 'node:path' +import { TREE_NAME } from './_tree-name.mjs' const __ROOT = __rs(__dn(__f2p(import.meta.url)), '..') -const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173/doxa-maps' +const BASE = process.env.BASE_URL || `http://127.0.0.1:4173/${TREE_NAME}` // Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs). await __treeGuard(BASE, __ROOT, 'SPA-TAB-ORDER') diff --git a/internal/check-staging-forwards-lang.mjs b/internal/check-staging-forwards-lang.mjs index 6995becac..b3d71ffce 100644 --- a/internal/check-staging-forwards-lang.mjs +++ b/internal/check-staging-forwards-lang.mjs @@ -24,6 +24,7 @@ import { createServer } from 'node:http' import { readFileSync } from 'node:fs' 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) @@ -36,13 +37,13 @@ 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 stop = () => { 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/index.html`).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}/index.html`).then((r) => r.ok).catch(() => false) } if (!up) { stop(); console.log(`STAGING-LANG: NOT MEASURED — could not serve ${DIR}`); process.exit(75) } const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) const done = async (code, msg) => { console.log(msg); await browser.close(); stop(); process.exit(code) } const page = await browser.newPage({ viewport: { width: 1400, height: 900 } }) -await page.goto(`http://127.0.0.1:${port}/doxa-maps/index.html?lang=ar`, { waitUntil: 'load' }) +await page.goto(`http://127.0.0.1:${port}/${TREE_NAME}/index.html?lang=ar`, { waitUntil: 'load' }) const node = page.locator(`.node[data-key="${KEY}"]`) if (!(await node.count())) await done(75, `STAGING-LANG: NOT MEASURED — no ${KEY} node on the staging page`) await node.first().click() diff --git a/internal/check-upg-map-data-painted.mjs b/internal/check-upg-map-data-painted.mjs index 325b4273d..feeb82523 100644 --- a/internal/check-upg-map-data-painted.mjs +++ b/internal/check-upg-map-data-painted.mjs @@ -35,10 +35,11 @@ import { chromium } from 'playwright' 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.env.BASE || 'http://127.0.0.1:4173/doxa-maps' +const BASE = process.env.BASE || `http://127.0.0.1:4173/${TREE_NAME}` const DEFAULT = [ 'my-upg-100-list/upg-100-list/index.html', 'my-upg-100-list/upg-100-mfe/index.html', @@ -62,7 +63,7 @@ if (!reachable) { // "SKIP — nothing served at /tmp/doxa-maps-browser-gate.lock/…" (the slot had overwritten BASE) // and EXITED 0, so a wave counted a gate that measured nothing as green. console.log(`UPG-MAP-DATA: NOT MEASURED — nothing served at ${urls[0]}.`) - console.log(' Serve THIS checkout (internal/serve-strict.mjs on your own port) and pass BASE=http://127.0.0.1:/doxa-maps') + console.log(` Serve THIS checkout (internal/serve-strict.mjs on your own port) and pass BASE=http://127.0.0.1:/${TREE_NAME}`) process.exit(75) } @@ -76,7 +77,7 @@ if (!reachable) { // ⚠ NOT MEASURED, exit 75 — the repo's EX_TEMPFAIL for "not measured" (a9550858), which the wave // runner reports as ⊘. It was exit 2 here, a code nothing else in the repo reads that way. // This check is also what makes the :4173 default safe: another agent's tree there is refused. -const TREE_BASE = String(urls[0]).split('/doxa-maps/')[0] +const TREE_BASE = String(urls[0]).split(`/${TREE_NAME}/`)[0] const treeCheck = await servesThisTree(TREE_BASE, ROOT) if (!treeCheck.ok) { console.error('UPG-MAP-DATA: NOT MEASURED — the server is not serving this checkout.') @@ -125,7 +126,7 @@ try { console.log('UPG-MAP-DATA') const fails = [] for (const r of results) { - const name = r.url.split('/doxa-maps/')[1] || r.url + const name = r.url.split(`/${TREE_NAME}/`)[1] || r.url if (r.error) { fails.push(`${name}: navigation failed — ${r.error}`); console.log(` ${name} — ERROR`); continue } if (!r.text) { // A MISSING STATUS LINE IS A FAILURE, NOT A SKIP. If the element never rendered its own diff --git a/internal/clean.mjs b/internal/clean.mjs index 01f0ee67f..2ae4edd02 100644 --- a/internal/clean.mjs +++ b/internal/clean.mjs @@ -10,16 +10,17 @@ import { readFileSync, rmSync, mkdirSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' +import { TREE_NAME } from './_tree-name.mjs' const __dirname = dirname(fileURLToPath(import.meta.url)) function config() { try { const cfg = JSON.parse(readFileSync(resolve(__dirname, '..', 'vite.doxa-maps-build-config.json'), 'utf8')) - const name = (typeof cfg?.name === 'string' && cfg.name.trim()) ? cfg.name.trim() : 'doxa-maps' + const name = (typeof cfg?.name === 'string' && cfg.name.trim()) ? cfg.name.trim() : TREE_NAME const paths = Array.isArray(cfg?.paths) ? cfg.paths.filter(p => typeof p === 'string' && p.trim()) : [] return { name, paths } - } catch { return { name: 'doxa-maps', paths: [] } } + } catch { return { name: TREE_NAME, paths: [] } } } const { name, paths } = config() diff --git a/internal/drop-portability-check.mjs b/internal/drop-portability-check.mjs index 7fa428d6d..fdf75b055 100644 --- a/internal/drop-portability-check.mjs +++ b/internal/drop-portability-check.mjs @@ -18,13 +18,14 @@ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; import { join, relative, dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { TREE_NAME } from './_tree-name.mjs' const here = dirname(fileURLToPath(import.meta.url)); const bundlerRoot = resolve(here, '..'); let treeDir = process.argv[2]; if (!treeDir) { - let name = 'doxa-maps'; + let name = TREE_NAME; const cfgPath = join(bundlerRoot, 'vite.doxa-maps-build-config.json'); if (existsSync(cfgPath)) { try { name = JSON.parse(readFileSync(cfgPath, 'utf8')).name || name; } catch {} diff --git a/internal/embed-smoke.mjs b/internal/embed-smoke.mjs index e55fcdc57..c71d5b697 100644 --- a/internal/embed-smoke.mjs +++ b/internal/embed-smoke.mjs @@ -22,6 +22,7 @@ import { readFileSync, existsSync, statSync, createReadStream } from 'node:fs' import { resolve, join, extname } from 'node:path' import { fileURLToPath } from 'node:url' import { chromium } from 'playwright' +import { TREE_NAME } from './_tree-name.mjs' const __dirname = fileURLToPath(new URL('.', import.meta.url)) const PUB = resolve(__dirname, '..', 'public') @@ -46,16 +47,16 @@ const server = createServer((req, res) => { }) // ── the page list: every profile page in the manifest ─────────────────────── -const manifest = JSON.parse(readFileSync(join(PUB, 'doxa-maps', 'manifest.json'), 'utf8')) +const manifest = JSON.parse(readFileSync(join(PUB, TREE_NAME, 'manifest.json'), 'utf8')) const targets = [] for (const b of manifest.bundles) for (const p of b.profiles) { // iframeUrl is ./-relative to the tree root - const url = p.iframeUrl.replace(/^\.\//, '/doxa-maps/') + const url = p.iframeUrl.replace(/^\.\//, `/${TREE_NAME}/`) targets.push({ bundle: b.bundle, profile: p.profile, element: p.element, url }) // Preconfigured instances are embeddable pages too — verify them the same way. for (const inst of (p.instances || [])) { targets.push({ bundle: b.bundle, profile: `${p.profile}/${inst.id}`, element: inst.element || p.element, - url: inst.iframeUrl.replace(/^\.\//, '/doxa-maps/') }) + url: inst.iframeUrl.replace(/^\.\//, `/${TREE_NAME}/`) }) } } const list = GREP ? targets.filter(t => (t.bundle + '/' + t.profile).includes(GREP)) : targets diff --git a/internal/fix-sri.mjs b/internal/fix-sri.mjs index 7ed08e5d8..5a10a9301 100644 --- a/internal/fix-sri.mjs +++ b/internal/fix-sri.mjs @@ -76,6 +76,7 @@ import { createHash } from 'node:crypto' import { tmpdir } from 'node:os' import { spawnSync } from 'node:child_process' import { fileURLToPath } from 'node:url' +import { TREE_NAME } from './_tree-name.mjs' const HERE = dirname(fileURLToPath(import.meta.url)) const CHECKER = join(HERE, 'sri-consistency-check.mjs') @@ -269,7 +270,7 @@ if (SELF_TEST) { selfTest() } else { let code = 0 - for (const root of (TREES.length ? TREES : ['public/doxa-maps'])) { + for (const root of (TREES.length ? TREES : [`public/${TREE_NAME}`])) { code = report(root, fixTree(root)) || code } process.exit(code) diff --git a/internal/generate-capability-exemplars.mjs b/internal/generate-capability-exemplars.mjs index 99f89365c..0c5c9011b 100644 --- a/internal/generate-capability-exemplars.mjs +++ b/internal/generate-capability-exemplars.mjs @@ -28,9 +28,10 @@ import { readFileSync, writeFileSync, existsSync } 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') const OUT = join(ROOT, 'documentation', 'reference-capability-exemplars.md') if (!existsSync(MANIFEST)) { @@ -68,7 +69,7 @@ lines.push('> **Do not hand-edit.** Regenerate with `bun run capability-exemplar lines.push('> check:capability-exemplars` fails if this file falls behind the manifest.') lines.push('>') lines.push('> This answers one question authoritatively: **which bundles are the capability-N') -lines.push('> examples right now?** It is derived from `public/doxa-maps/manifest.json`, which is what') +lines.push(`> examples right now?** It is derived from \`public/${TREE_NAME}/manifest.json\`, which is what`) lines.push('> the build computed from each folder\'s real shape — so it cannot drift from what ships.') lines.push('>') lines.push('> **Cite this file rather than remembering.** The same citation has been wrong in three') diff --git a/internal/generate-gallery.mjs b/internal/generate-gallery.mjs index e428564a3..3e7601e69 100644 --- a/internal/generate-gallery.mjs +++ b/internal/generate-gallery.mjs @@ -50,6 +50,7 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from import { join, resolve, dirname } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { PROFILE_TIER, TYPE_DISPLAY, detectProfileType } from './plugin-gallery/discover.mjs' +import { TREE_NAME } from './_tree-name.mjs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') @@ -99,8 +100,8 @@ export function readBuildConfig() { } catch { /* fall through */ } } return { - name: str('name') || 'doxa-maps', - title: str('displayTitle') || 'doxa-maps', + name: str('name') || TREE_NAME, + title: str('displayTitle') || TREE_NAME, tokenUrl: tokenUrl || 'window.MAP_TOKEN_URL', token: str('token'), apiBaseUrl: str('apiBaseUrl').replace(/\/+$/, ''), @@ -185,7 +186,7 @@ const LADDER_JS = String.raw`