diff --git a/documentation/README-SECTION-3-cli-commands.md b/documentation/README-SECTION-3-cli-commands.md index b3525873f..0f0cc1f64 100644 --- a/documentation/README-SECTION-3-cli-commands.md +++ b/documentation/README-SECTION-3-cli-commands.md @@ -49,7 +49,7 @@ The scripts these commands are grounded in (from this branch's `package.json`): "build": "bash internal/build-lock.sh", "build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery", "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:wp-embed check:doc-scripts", -"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source check:backlog-refs check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary", +"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary", "build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time "preview": "bun x vite preview", "serve": "node internal/serve-strict.mjs", // 404s a directory, like production 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/browser-slot.sh b/internal/browser-slot.sh index bb3efd0e5..3f9738ec3 100755 --- a/internal/browser-slot.sh +++ b/internal/browser-slot.sh @@ -34,6 +34,25 @@ BUSY=75 MAX_HOLD="${BROWSER_MAX_HOLD:-900}" KILL_AFTER="${BROWSER_KILL_AFTER:-15}" +# ★ A GATE THAT IS LEGITIMATELY LONG DECLARES ITS OWN LIMIT, in its header: @browser-slot-max-hold 2700 +# check:host-locale-contract loads every leaf twice and hit the 900 s default three times on full +# runs (NOT MEASURED, not a verdict). An env prefix in its package.json script could not fix that — +# this wrapper reads its limit before the script's own environment exists — so the gate carries the +# number itself and it holds however the gate is invoked (node , or bun run `) }).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`