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.
This commit is contained in:
doxa-maps-88655c70 2026-09-25 15:42:10 +00:00
parent 577149afb4
commit 44ef31b2c6
75 changed files with 320 additions and 204 deletions

View file

@ -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')

45
internal/_tree-name.mjs Normal file
View file

@ -0,0 +1,45 @@
/**
* _tree-name.mjs — the ONE place the output tree's name is read.
*
* The built tree lives at public/<TREE_NAME>/ and every embeddable URL carries it
* (…/<TREE_NAME>/<bundle>/<profile>/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')

View file

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

View file

@ -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 <STAGE_DIR> (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 <STAGE_DIR> (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."

View file

@ -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 "<tree>")/ 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

View file

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

View file

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

View file

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

View file

@ -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.
//

View file

@ -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).')

View file

@ -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/ <catalog>/app/doxa-maps/ # --delete mirrors removals\n' +
` rsync -a --delete public/${TREE_NAME}/ <catalog>/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.')

View file

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

View file

@ -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 = '<!doctype html><html><body>nothing yet</body></html>'
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' })

View file

@ -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`)

View file

@ -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'))

View file

@ -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

View file

@ -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 = []

View file

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

View file

@ -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) => `<!DOCTYPE html>
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. -->
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";<\/script>
<script src="${TREE}/doxa-maps/${c.id}/${c.id}.js" crossorigin="anonymous"><\/script>
<script src="${TREE}/${TREE_NAME}/${c.id}/${c.id}.js" crossorigin="anonymous"><\/script>
<script>
var el = document.createElement(${JSON.stringify(c.tag)});
el.setAttribute('profile-config', JSON.stringify({ profile: ${JSON.stringify(c.profile)}, locale: 'ar',
assetsBase: ${JSON.stringify(TREE + '/doxa-maps/assets')} }));
assetsBase: ${JSON.stringify(TREE + `/${TREE_NAME}/assets`)} }));
el.style.cssText = 'display:block;width:100%;height:900px';
document.getElementById('w').appendChild(el);
<\/script></body></html>`

View file

@ -47,6 +47,7 @@ import { fileURLToPath } from 'node:url'
import { spawn } from 'node:child_process'
import { createServer } from 'node:net'
import { createServer as createHttpServer } from 'node:http'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2)
@ -60,7 +61,7 @@ const SELF_TEST = !argv.includes('--no-self-test')
const WAIT_MS = Number(process.env.SHELF_WAIT_MS || 75000)
const { chromium } = await import(resolve(ROOT, 'node_modules/playwright/index.mjs'))
const manifest = JSON.parse(readFileSync(resolve(ROOT, 'public/doxa-maps/manifest.json'), 'utf8'))
const manifest = JSON.parse(readFileSync(resolve(ROOT, `public/${TREE_NAME}/manifest.json`), 'utf8'))
const spa = readFileSync(resolve(ROOT, 'plugin-designs/doxa-maps-spa/doxa-maps-spa.vue'), 'utf8')
const cfg = JSON.parse(readFileSync(resolve(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
const TK = process.env.MAPBOX_TOKEN || cfg.token || ''
@ -95,7 +96,7 @@ const port = await new Promise(ok => { const s = createServer().listen(0, '127.0
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', 'public'], { cwd: ROOT, stdio: 'ignore' })
const reap = () => { try { srv.kill() } catch (_) {} }
process.on('exit', reap); for (const s of ['SIGINT', 'SIGTERM']) process.on(s, () => { reap(); process.exit(130) })
const BASE = `http://127.0.0.1:${port}/doxa-maps`
const BASE = `http://127.0.0.1:${port}/${TREE_NAME}`
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) ────────────────

View file

@ -40,9 +40,10 @@ import { dirname, resolve } 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 REPO = resolve(dirname(fileURLToPath(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}`
// ⚠ "ANSWERS 200" IS NOT "IS THIS BUILD". This machine runs several worktrees of one repo and
// the default port belongs to whichever server started first — measured 2026-09-24: :4173 was

View file

@ -63,6 +63,7 @@ import { spawn, execFileSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { readFileSync, existsSync } from 'node:fs'
import { TREE_NAME } from './_tree-name.mjs'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const ROOT = resolve(__dirname, '..')
@ -109,7 +110,7 @@ if (!apiReachable) {
}
// ── 1. MANIFEST-DERIVED TARGETS (never hardcode which bundle is "the" cap-5 shell) ────────────
const MANIFEST = resolve(ROOT, 'public', 'doxa-maps', 'manifest.json')
const MANIFEST = resolve(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) {
console.log('FITS-THE-SCREEN: SKIP — no manifest.json (nothing built).')
process.exit(0)
@ -183,7 +184,7 @@ const browser = await chromium.launch({ args: GL_ARGS })
async function runComposite() {
const TARGET = 'dashboard-plus-grid'
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
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`
await page.goto(url, { waitUntil: 'load', timeout: 60000 }).catch((e) => { throw new Error(`cannot reach ${TARGET}: ${e.message}`) })
await page.waitForTimeout(6000)
await reinject(page)
@ -613,7 +614,7 @@ async function runShellBestEffort() {
return
}
for (const shell of shellLeaves) {
const url = shell.iframeUrl ? `${BASE}/doxa-maps/${shell.iframeUrl.replace(/^\.\//, '')}` : null
const url = shell.iframeUrl ? `${BASE}/${TREE_NAME}/${shell.iframeUrl.replace(/^\.\//, '')}` : null
if (!url) continue
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
try {
@ -651,8 +652,8 @@ async function runShellBestEffort() {
let runError = null
try {
await runComposite()
await runResponsiveSweep(`${BASE}/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html`, 'dashboard-plus-grid')
await runResponsiveSweep(`${BASE}/doxa-maps/research-dashboard/research-dashboard/index.html`, 'research-dashboard')
await runResponsiveSweep(`${BASE}/${TREE_NAME}/dashboard-plus-grid/dashboard-plus-grid/index.html`, 'dashboard-plus-grid')
await runResponsiveSweep(`${BASE}/${TREE_NAME}/research-dashboard/research-dashboard/index.html`, 'research-dashboard')
await runShellBestEffort()
} catch (e) {
runError = e

View file

@ -37,8 +37,9 @@ 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 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 ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port

View file

@ -30,6 +30,7 @@ import { serveReaped } from './_serve-reaped.mjs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { requireThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const PUBLIC = resolve(HERE, '../public')
@ -43,7 +44,7 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// tree is a precondition, not a product defect, and the wave runner reports 75 as ⊘ rather than ✗.
// A file:// target passes straight through — there is no server there to be wrong about.
await requireThisTree(BASE, ROOT, 'GOSPEL-COMPOSED-FILTERS')
const PATHNAME = '/doxa-maps/gospel-resources/gospel-dashboard/index.html'
const PATHNAME = `/${TREE_NAME}/gospel-resources/gospel-dashboard/index.html`
let srv = null
if (!process.env.BASE_URL) srv = serveReaped(PUBLIC, PORT)

View file

@ -27,6 +27,7 @@ import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { serveReaped } from './_serve-reaped.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '..')
@ -53,7 +54,7 @@ try {
for (const lc of LOCALES) {
const loc = Object.fromEntries(flat(cat(lc)))
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
await page.goto(BASE + '/doxa-maps/gospel-resources/' + leaf + '/index.html?lang=' + lc,
await page.goto(BASE + `/${TREE_NAME}/gospel-resources/` + leaf + '/index.html?lang=' + lc,
{ waitUntil: 'networkidle', timeout: 90000 })
await page.waitForTimeout(8000)
/* ═══ READ ONLY THE CHROME, NOT THE DECLARED-ENGLISH PROSE ═══════════════════════

View file

@ -31,6 +31,7 @@ import { chromium } from 'playwright'
import { serveReaped } from './_serve-reaped.mjs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const PUBLIC = resolve(HERE, '../public')
@ -80,7 +81,7 @@ try {
if (POISON) {
/* A URL that serves no element at all must fail EVERY leaf-shaped assertion. */
const r = await probe(page, `${BASE}/doxa-maps/index.html`)
const r = await probe(page, `${BASE}/${TREE_NAME}/index.html`)
const f = failuresFor('poison', r)
if (!f.length) {
console.error('LEAVES-MOUNT: SELF-TEST FAIL — a page with no gospel element passed as mounted.')
@ -93,7 +94,7 @@ try {
const bad = []
for (const leaf of LEAVES) {
const r = await probe(page, `${BASE}/doxa-maps/gospel-resources/${leaf}/index.html`)
const r = await probe(page, `${BASE}/${TREE_NAME}/gospel-resources/${leaf}/index.html`)
const f = failuresFor(leaf, r)
bad.push(...f)
console.log(` ${f.length ? 'FAIL' : 'ok '} ${leaf.padEnd(18)} defined=${r.defined} shadow=${r.hasShadow} children=${r.shadowChildren} text=${r.visibleTextLength}`)

View file

@ -38,6 +38,7 @@ import { createServer } from 'node:http'
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 argv = process.argv.slice(2)
@ -48,14 +49,14 @@ const CONTROL = 'regions-dashboard/regions-list'
const MIN = 20 // Arabic characters a page must show under ?lang=ar before it can be judged
const CONCURRENCY = 3
const manifestPath = join(DIR, 'doxa-maps', 'manifest.json')
const manifestPath = join(DIR, TREE_NAME, 'manifest.json')
if (!existsSync(manifestPath)) { console.log(`HOST-LOCALE: NOT MEASURED — no ${manifestPath}`); process.exit(75) }
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
const leaves = []
for (const b of manifest.bundles || []) for (const p of b.profiles || []) {
const id = `${b.bundle}/${p.profile}`
if (ONLY && !ONLY.has(id) && id !== CONTROL) continue
if (existsSync(join(DIR, 'doxa-maps', id, 'index.html'))) leaves.push(id)
if (existsSync(join(DIR, TREE_NAME, id, 'index.html'))) leaves.push(id)
}
if (!leaves.includes(CONTROL)) { console.log(`HOST-LOCALE: NOT MEASURED — the control ${CONTROL} is not in this tree`); process.exit(75) }
@ -63,7 +64,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 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/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) { stop(); console.log(`HOST-LOCALE: NOT MEASURED — could not serve ${DIR}`); process.exit(75) }
const COUNT = `(() => {
@ -85,7 +86,7 @@ const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable
async function measure(id, mode) {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } })
const page = await ctx.newPage()
const url = `http://127.0.0.1:${port}/doxa-maps/${id}/index.html${mode === 'ref' ? '?lang=ar' : ''}`
const url = `http://127.0.0.1:${port}/${TREE_NAME}/${id}/index.html${mode === 'ref' ? '?lang=ar' : ''}`
let injected = mode === 'ref'
if (mode === 'host') {
await page.route(url, async (route) => {

View file

@ -31,9 +31,10 @@ import { chromium } from 'playwright'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { requireThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173')
+ '/doxa-maps/my-upg-100-list/upg-100-list/index.html'
+ `/${TREE_NAME}/my-upg-100-list/upg-100-list/index.html`
const LEAF = process.argv[2] || DEFAULT_LEAF
let reachable = false

View file

@ -34,6 +34,7 @@ import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { requireThisTree } from './_serves-this-tree.mjs'
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
import { TREE_NAME } from './_tree-name.mjs'
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..')
const LOCALES = join(REPO, 'library/i18n/locales')
@ -49,7 +50,7 @@ const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
await requireThisTree(URL_, ROOT, 'LEGEND-INFO-LOCALIZED')
if (!URL_) {
console.log('LEGEND-INFO-LOCALIZED: SKIP — no leaf url given.')
console.log(' Pass one, or set LEGEND_URL. Serve the TREE ROOT (public/doxa-maps) and point')
console.log(` Pass one, or set LEGEND_URL. Serve the TREE ROOT (public/${TREE_NAME}) and point`)
console.log(' at a profile whose legend carries per-tab info buttons, e.g.')
console.log(' my-upg-100-list/upg-100-list/index.html')
process.exit(0)

View file

@ -17,6 +17,7 @@
import { readFileSync, readdirSync, existsSync } from 'node:fs'
import { resolve, join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const REPO = process.env.DOXA_REPO
// FOURTH instance of this pattern in one flight (check-zone-census, check-share-policy,
@ -95,7 +96,7 @@ if (unreachable.length) {
}
// The host-page path must survive: leaf pages still declare a lang.
const leafRoot = join(REPO, 'public/doxa-maps')
const leafRoot = join(REPO, `public/${TREE_NAME}`)
let leaves = 0
let withLang = 0
if (existsSync(leafRoot)) {

View file

@ -35,13 +35,14 @@ import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { discoverSpaRegistry } from './plugin-gallery/discover.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 MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
const MIN_BUNDLES = Number(process.env.MIN_BUNDLES || 10)
if (!existsSync(MANIFEST)) {
console.error('MANIFEST-COVERS: FAIL — public/doxa-maps/manifest.json does not exist.')
console.error(`MANIFEST-COVERS: FAIL — public/${TREE_NAME}/manifest.json does not exist.`)
console.error(' Run `bun run build` first; there is nothing to compare against.')
process.exit(1)
}

View file

@ -26,6 +26,7 @@ import { createServer } from 'node:http'
import { readFileSync, existsSync } from 'node:fs'
import { join, dirname, resolve } 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)
@ -35,14 +36,14 @@ const DEFAULT = JSON.parse(readFileSync(join(ROOT, 'plugin-designs/doxa-maps-spa
const ONLY = (argOf('--only', '') ? argOf('--only', '').split(',') : DEFAULT).map((s) => s.trim()).filter(Boolean)
const CELLS = argOf('--cell', '') ? [argOf('--cell', '').toUpperCase()] : ['B', 'C']
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) { console.log('MAP-EMBED-CELLS: SKIP — no built tree'); process.exit(2) }
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
const bakedToken = (() => {
for (const b of manifest.bundles || []) for (const p of b.profiles || []) {
if (!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]
@ -67,7 +68,7 @@ for (const ref of ONLY) {
const freePort = () => new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const port = s.address().port; s.close(() => ok(port)) }) })
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 = '<!doctype html><title>host</title><body>nothing</body>'
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' })
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); r.end(pageHtml) }).listen(HOST_PORT, 'localhost')

View file

@ -41,11 +41,12 @@ import { readFileSync, existsSync } from 'node:fs'
import { resolve, join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { servesThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
// Portable by construction — see check-no-absolute-paths.mjs. Never hardcode a worktree.
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const BASE = String(process.env.BASE_URL || 'http://127.0.0.1:4173').replace(/\/+$/, '')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
const DROP_RE = /unknown property|does not exist|is required|Expected|invalid/i
@ -118,7 +119,7 @@ let unreachable = 0
for (const [bundle, profile] of leaves) {
const page = await browser.newPage({ viewport: { width: 1100, height: 760 } })
await page.addInitScript(INIT)
const url = `${BASE}/doxa-maps/${bundle}/${profile}/index.html?provider=maplibre`
const url = `${BASE}/${TREE_NAME}/${bundle}/${profile}/index.html?provider=maplibre`
try {
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 })
await page.waitForTimeout(9000)

View file

@ -38,10 +38,11 @@ import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright'
import { requireThisTree } 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}/`
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees of this repo run on one machine and
// a default port belongs to whichever server started first — measured 2026-09-24, :4173 was another

View file

@ -37,6 +37,7 @@
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)), '..')
@ -46,7 +47,7 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// correct only because the default happens to equal the config's current name. A dead read is
// INVISIBLE when only the result is printed, so the source is now part of the output: a future
// regression shows up on every run instead of waiting for a tree 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'))

View file

@ -46,6 +46,7 @@ import { execSync } from 'node:child_process'
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)), '..')
@ -128,7 +129,7 @@ if (hits.length) {
for (const h of hits) console.error(` ✗ ${h.rel}:${h.line} ${h.label}\n ${h.text}`)
console.error(
'\n This repo is handed to the Doxa team; an internal code name means nothing to them and is a\n' +
' hand-off defect. Remove it, or rewrite the sentence with the product name — doxa-maps, or\n' +
` hand-off defect. Remove it, or rewrite the sentence with the product name — ${TREE_NAME}, or\n` +
' "the plugin studio". Do not invent a replacement brand. If the match is a unix path, use a\n' +
' repo-relative path instead and check:no-abs-paths will stop reporting it too.'
)

View file

@ -50,13 +50,14 @@
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 COLORS = join(ROOT, 'library/colors')
const BUILT = join(ROOT, 'public/doxa-maps')
const BUILT = join(ROOT, `public/${TREE_NAME}`)
if (!existsSync(BUILT)) {
console.error('ONE-PALETTE: SKIP — no built tree at public/doxa-maps. Run `bun run build` first.')
console.error(`ONE-PALETTE: SKIP — no built tree at public/${TREE_NAME}. Run \`bun run build\` first.`)
console.error(' Reported as a SKIP with a non-silent message rather than a pass: a gate that')
console.error(' reports success when it never ran is worse than no gate.')
process.exit(0)

View file

@ -24,10 +24,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 DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173')
+ '/doxa-maps/my-upg-100-list/upg-100-mfe/index.html'
+ `/${TREE_NAME}/my-upg-100-list/upg-100-mfe/index.html`
const LEAF = process.argv[2] || DEFAULT_LEAF
// ⚠ SWEEP WIDTHS. The report named 1440px, so this first guarded 1440px — but the defect is a
// FLEX-SHRINK failure, and those are width-dependent: a narrower viewport gives the controls less
@ -79,7 +80,7 @@ if (!reachable) {
// so a caller can tell it apart from both a pass (0) and a real failure (1).
// The helper wants the tree's BASE, not a leaf URL — it fetches <base>/doxa-maps/manifest.json.
// LEAF is `<base>/doxa-maps/<bundle>/<profile>/index.html`, so cut at the /doxa-maps/ segment.
const TREE_BASE = LEAF.split('/doxa-maps/')[0]
const TREE_BASE = LEAF.split(`/${TREE_NAME}/`)[0]
const treeCheck = await servesThisTree(TREE_BASE, ROOT)
if (!treeCheck.ok) {
console.error('POSTER-CARD-FITS: NOT MEASURED — the server is not serving this checkout.')

View file

@ -44,6 +44,7 @@ import { readFileSync } from 'node:fs'
import { resolve, dirname, join } 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'
@ -102,7 +103,7 @@ if (blind.length) {
}
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(`POSTER-WALL-LOCALISED: SKIP — nothing served at ${BASE}.`)
console.log(' Serve the built tree: bun run serve (or set BASE_URL).')
@ -127,7 +128,7 @@ for (const leaf of LEAVES) {
page.on('pageerror', (e) => errs.push(String(e).slice(0, 120)))
let r = null
try {
const res = await page.goto(`${BASE}/doxa-maps/${leaf}/index.html?lang=${lang}`,
const res = await page.goto(`${BASE}/${TREE_NAME}/${leaf}/index.html?lang=${lang}`,
{ waitUntil: 'load', timeout: 45000 })
if (!res || !res.ok()) { rows.push({ leaf, lang, skipped: `HTTP ${res ? res.status() : '?'}` }); await page.close(); continue }
await page.waitForTimeout(13000)

View file

@ -26,13 +26,14 @@ import { writeFileSync } from 'node:fs'
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)), '..')
if (!existsSync(resolve(ROOT, 'public/doxa-maps/prayer-walk-planner/prayer-walk-planner.js'))) { console.log('SKIP check-prayer-walk-planner: no built planner'); process.exit(2) }
if (!existsSync(resolve(ROOT, `public/${TREE_NAME}/prayer-walk-planner/prayer-walk-planner.js`))) { console.log('SKIP check-prayer-walk-planner: no built planner'); process.exit(2) }
let chromium
try { ({ chromium } = await import('playwright')) } catch { console.log('SKIP check-prayer-walk-planner: playwright not installed'); process.exit(2) }
const OUT = process.env.OUT || '/tmp'
const free = () => new Promise(ok => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
const TP = await free(), HP = await free(), TREE = `http://127.0.0.1:${TP}/doxa-maps`
const TP = await free(), HP = await free(), TREE = `http://127.0.0.1:${TP}/${TREE_NAME}`
const srv = spawn('node', ['internal/serve-strict.mjs', '--port', String(TP)], { cwd: ROOT, stdio: 'ignore' })
const host = createServer((q, r) => { const l = new URL(q.url, 'http://x').searchParams.get('l') || 'en'; r.writeHead(200, { 'content-type': 'text/html' }); r.end(`<!doctype html><html lang="${l}"><body style="margin:0"><script src="${TREE}/doxa-maps-spa/doxa-maps-spa.js"></script><doxa-maps-spa tab="prayer-walk-planner" style="height:100vh"></doxa-maps-spa></body></html>`) }).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) {

View file

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

View file

@ -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
{

View file

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

View file

@ -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) {

View file

@ -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 = []

View file

@ -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

View file

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

View file

@ -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')

View file

@ -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()

View file

@ -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:<port>/doxa-maps')
console.log(` Serve THIS checkout (internal/serve-strict.mjs on your own port) and pass BASE=http://127.0.0.1:<port>/${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

View file

@ -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()

View file

@ -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 {}

View file

@ -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

View file

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

View file

@ -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')

View file

@ -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`<script>
ex: '&lt;doxa-map profile-config=&#39;{&quot;profile&quot;:&quot;doxa-simple-map&quot;}&#39;&gt;&lt;/doxa-map&gt;' },
{ title: 'App profile (mounting file)',
def: 'One flat .vue file in plugin-designs/&lt;bundle&gt;/ that defines a whole map app — the main component of that map. The settings written on the tag (profile-config) name which app profile to load, and the shared ProfileLoader picks it. In class-and-object terms: the element class is the class, each tag on a page is an instance, and the app profile is the screen that instance loads.',
how: 'Most repos need one such main file per app. doxa-maps builds hundreds of maps from one shared library, so every map gets its own app profile — the folder plugin-designs/ is where they sit. The five kinds on this ladder are the five shapes those files come in.',
how: 'Most repos need one such main file per app. ${TREE_NAME} builds hundreds of maps from one shared library, so every map gets its own app profile — the folder plugin-designs/ is where they sit. The five kinds on this ladder are the five shapes those files come in.',
ex: 'plugin-designs/doxa-simple-map/doxa-simple-map.vue' }
];
var LADDER = [
@ -246,7 +247,7 @@ const LADDER_JS = String.raw`<script>
* config fields every leaf page bakes (readBuildConfig)
*/
export function renderGallery(model, opts = {}) {
const { mode = 'build', title = 'doxa-maps', tree = 'doxa-maps', tokenUrl = 'window.MAP_TOKEN_URL', token = '', apiBaseUrl = '', assetsBaseUrl = '', qaWidget = true, tierMap = {} } = opts
const { mode = 'build', title = TREE_NAME, tree = TREE_NAME, tokenUrl = 'window.MAP_TOKEN_URL', token = '', apiBaseUrl = '', assetsBaseUrl = '', qaWidget = true, tierMap = {} } = opts
const dev = mode === 'dev'
const baked = compact(model, true, tierMap)
const json = (v) => JSON.stringify(v).replace(/</g, '\\u003c')
@ -301,7 +302,7 @@ code{font-family:ui-monospace,Menlo,monospace}
${dev ? LADDER_CSS : ''}</style>
</head>
<body>
<!-- The doxa-maps nested-mfe-spa (capability 5, the reserved app-shell): a Vue single-page app
<!-- The ${TREE_NAME} nested-mfe-spa (capability 5, the reserved app-shell): a Vue single-page app
whose sidebar is the composability ladder rendered as vertical tabs (capability 5 → 1),
each row mounting that plugin's custom element in the one slot. This SAME page is the
served studio root (public/index.html) AND the Capacitor mobile-app entry (webDir: public).

View file

@ -42,6 +42,7 @@ import { createHash } from 'node:crypto'
// without a separate tierMap. Previously the manifest carried no `type`, so a manifest-fed
// consumer (plugin-studio) mis-tiered every profile as cap-1 (single). Single source of truth.
import { detectProfileType } from './plugin-gallery/discover.mjs'
import { TREE_NAME as CONFIGURED_TREE_NAME } from './_tree-name.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const APP_PROFILES = resolve(__dirname, '..', 'plugin-designs')
@ -56,7 +57,7 @@ const SOURCES = resolve(__dirname, '..', 'library/api/sources.json')
function readBuildConfig() {
try {
const cfg = JSON.parse(readFileSync(BUILD_CONFIG_FILE, '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() : CONFIGURED_TREE_NAME
const paths = Array.isArray(cfg.paths) ? cfg.paths.filter((p) => typeof p === 'string' && p.trim()) : []
// Local, NON-committed sync override: `DOXA_SYNC_PATHS=/abs/dir[,/abs/dir2]` ALSO copies the
// built tree into extra destinations (e.g. a local doxa-life checkout's public folder) without
@ -103,7 +104,7 @@ function readBuildConfig() {
const posterSources = cfg.posterSources === true || bundlePosters
return { name, paths, embedBaseUrl, tokenUrl, token, assetsBaseUrl, apiBaseUrl, gatedProfiles, qaWidget, bundlePosters, posterSources }
} catch {
return { name: 'doxa-maps', paths: [], embedBaseUrl: '', tokenUrl: '', token: '', assetsBaseUrl: '', apiBaseUrl: '', gatedProfiles: [], qaWidget: true, bundlePosters: false, posterSources: false }
return { name: CONFIGURED_TREE_NAME, paths: [], embedBaseUrl: '', tokenUrl: '', token: '', assetsBaseUrl: '', apiBaseUrl: '', gatedProfiles: [], qaWidget: true, bundlePosters: false, posterSources: false }
}
}
const DEST_CONFIG = readBuildConfig()
@ -138,7 +139,7 @@ function assetsBaseFor(depth) {
// title is the PRODUCT title (config displayTitle, default org-free), never the deployment TREE_NAME
// (TREE_NAME stays in URLs/paths where it is a folder name, not branding).
const DISPLAY_TITLE = (typeof DEST_CONFIG.displayTitle === 'string' && DEST_CONFIG.displayTitle.trim())
? DEST_CONFIG.displayTitle.trim() : 'doxa-maps'
? DEST_CONFIG.displayTitle.trim() : TREE_NAME
// Base URL every intra-tree ref (manifest iframeUrl/scriptUrl) is built against.
// With embedBaseUrl set (e.g. "https://doxa.life/js"), refs are ABSOLUTE — the
// build-time override for known hosts / partner embeds. WITHOUT it, refs are

View file

@ -17,12 +17,13 @@ import { join, relative, dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execFileSync } from 'node:child_process'
import { tmpdir } from 'node:os'
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 cfg = join(bundlerRoot, 'vite.doxa-maps-build-config.json')
if (existsSync(cfg)) { try { name = JSON.parse(readFileSync(cfg, 'utf8')).name || name } catch {} }
treeDir = join(bundlerRoot, 'public', name)

View file

@ -16,6 +16,7 @@ import { chromium } from 'playwright'
import { spawn } from 'node:child_process'
import { createServer } from 'node:http'
import { readFileSync } from 'node:fs'
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
const argv = process.argv.slice(2)
const arg = (k, d) => { const i = argv.indexOf(k); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
@ -25,7 +26,7 @@ const RUNS = Number(arg('--runs', 3)), MBPS = Number(arg('--mbps', 10)), RTT = N
const EMBED = argv.includes('--embed')
const DEFAULT = JSON.parse(readFileSync('plugin-designs/doxa-maps-spa/doxa-maps-spa.meta.json', 'utf8')).composes
const ONLY = arg('--only', '') ? arg('--only', '').split(',') : DEFAULT
const manifest = JSON.parse(readFileSync('public/doxa-maps/manifest.json', 'utf8'))
const manifest = JSON.parse(readFileSync(`public/${TREE_NAME}/manifest.json`, 'utf8'))
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, ['internal/serve-strict.mjs', '--port', String(port)], { stdio: 'ignore' })
@ -33,10 +34,10 @@ const hostPort = await new Promise((ok) => { const s = createServer().listen(0,
let hostHtml = ''
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); r.end(hostHtml) }).listen(hostPort, 'localhost')
await new Promise((r) => setTimeout(r, 900))
const T = `http://127.0.0.1:${port}/doxa-maps`
const T = `http://127.0.0.1:${port}/${TREE_NAME}`
const kind = (u) => /mapbox-gl(\.min)?\.js|maplibre-gl\.js/.test(u) ? 'engine' : /mapbox-gl\.css|maplibre-gl\.css/.test(u) ? 'engine-css'
: /\/styles\/v1\/|tiles\.openfreemap\.org\/styles/.test(u) ? 'style' : /\/doxa-maps\/[^/]+\/[^/]+\.js$/.test(u) ? 'bundle'
: /\/styles\/v1\/|tiles\.openfreemap\.org\/styles/.test(u) ? 'style' : new RegExp(`/${TREE_NAME_RE}/[^/]+/[^/]+\\.js$`).test(u) ? 'bundle'
: /pray\.doxa\.life\/api/.test(u) ? 'api' : /\/assets\/.*\.(json|geojson)/.test(u) ? 'data' : /\.pbf|\.mvt|vector/.test(u) ? 'tile' : 'other'
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] })
@ -48,7 +49,7 @@ for (const ref of ONLY) {
if (!p) { rows.push({ ref, missing: true }); continue }
let url = `${T}/${String(p.iframeUrl).replace(/^\.\//, '')}`
if (EMBED) {
const tok = (readFileSync(`public/doxa-maps/${String(p.iframeUrl).replace(/^\.\//, '')}`, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/) || [])[1] || ''
const tok = (readFileSync(`public/${TREE_NAME}/${String(p.iframeUrl).replace(/^\.\//, '')}`, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/) || [])[1] || ''
const snip = String((p.embed && p.embed.script) || '').replace(/src="\.\//g, `src="${T}/`).replace(/YOUR_MAPBOX_TOKEN/g, tok)
hostHtml = `<!doctype html><html><head><meta charset="utf-8"><title>Partner</title></head><body style="margin:0"><div style="position:relative;height:900px">${snip}</div></body></html>`
url = `http://localhost:${hostPort}/partner.html`

View file

@ -14,6 +14,7 @@ import { resolve, extname } from 'node:path'
import { readdirSync, statSync, existsSync, readFileSync } from 'node:fs'
import { REPO_ROOT, discoverSpaRegistry } from './discover.mjs'
import { readBuildConfig, renderGallery } from '../generate-gallery.mjs'
import { TREE_NAME } from '../_tree-name.mjs'
// ─── DEV plugin gallery ──────────────────────────────────────────────────────
// `bun x vite` (dev) serves the plugin gallery at `/` — the same page `bun run build`
@ -172,7 +173,7 @@ export function galleryModel(reg) {
//
// Returns the rewritten `/__leaf/…` path, or '' when the URL is not an embed leaf URL (the common
// case — every other request must fall through untouched).
export function leafRewrite(path, treeName = 'doxa-maps') {
export function leafRewrite(path, treeName = TREE_NAME) {
if (!path.endsWith('/index.html')) return ''
let segs = path.slice(0, -'/index.html'.length).split('/').filter(Boolean)
if (segs[0] === treeName) segs = segs.slice(1) // tolerate the tree-prefixed form
@ -241,7 +242,7 @@ export function autoSpaDevPlugin(token = '') {
// The rewrite is applied to `path` (which every branch reads) and to `req.url` (so anything
// downstream sees a consistent request); never via server.middlewares.handle(), which
// replays this same middleware and would loop.
let treeName = 'doxa-maps'
let treeName = TREE_NAME
try { treeName = readBuildConfig()?.name || treeName } catch { /* keep the default */ }
const rewritten = leafRewrite(rawPath, treeName)
const path = rewritten || rawPath
@ -405,7 +406,7 @@ export function autoSpaDevPlugin(token = '') {
'<!DOCTYPE html>',
'<html lang="en"><head><meta charset="UTF-8" />',
'<meta name="viewport" content="width=device-width, initial-scale=1.0" />',
'<title>doxa-maps plugin studio</title>',
`<title>${TREE_NAME} plugin studio</title>`,
'<link rel="icon" href="data:,">',
// The token ladder's baked rung, exactly as the built leaves receive it, so maps paint
// in dev without anyone appending ?tk=.

View file

@ -36,13 +36,14 @@
import { readdirSync, statSync, readFileSync, existsSync, writeFileSync } from 'node:fs'
import { join, relative, resolve, dirname, extname, sep } 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 VALUE_FLAGS = new Set(['--tree', '--json', '--root'])
const DIR = resolve(argOf('--root', '') || args.find((a, i) => !a.startsWith('--') && !VALUE_FLAGS.has(args[i - 1])) || join(ROOT, 'public'))
const TREE = argOf('--tree', 'doxa-maps')
const TREE = argOf('--tree', TREE_NAME)
const STRICT = args.includes('--strict')
const JSON_OUT = argOf('--json', '')
if (!existsSync(DIR) || !statSync(DIR).isDirectory()) { console.error(`[hygiene] not a directory: ${DIR}`); process.exit(2) }

View file

@ -7,6 +7,7 @@ import { createServer } from 'node:http'
import { readFile, stat } from 'node:fs/promises'
import { join, extname, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(HERE, '..', 'public')
@ -58,7 +59,7 @@ async function tryShot(name, path, { vp = { width: 1440, height: 900 }, wait = 5
// cap1: vision-card DEFAULT instance page (TOKEN_FREE=true -> renders the card instantly, no token error).
// Long wait so the aurora backdrop + rotating stat cards fully paint (they animate in).
await tryShot('cap1-single-element', '/doxa-maps/doxa-vision-card/doxa-vision-card/default/index.html',
await tryShot('cap1-single-element', `/${TREE_NAME}/doxa-vision-card/doxa-vision-card/default/index.html`,
{ wait: 9000 })
// cap5: SPA deep-linked to the prayer map instance so a real painted map shows in the right pane.

View file

@ -17,6 +17,7 @@ import { createHash } from 'node:crypto'
import { existsSync, readFileSync } from 'node:fs'
import { join, extname, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(HERE, '..', 'public')
@ -205,8 +206,8 @@ async function shot(name, path, {
console.log(' OK', name + '.png', `<- ${path} (${ready} elements)`)
// Record WHICH BUNDLE this picture is of, by content hash, so check:shots can tell a current
// picture from a stale one without trusting any clock (see SHOT_BUNDLES below).
const lm = path.match(/^\/doxa-maps\/([^/]+)\//)
const bundleJs = lm && join(ROOT, 'doxa-maps', lm[1], `${lm[1]}.js`)
const lm = path.match(new RegExp(`^/${TREE_NAME_RE}/([^/]+)/`))
const bundleJs = lm && join(ROOT, TREE_NAME, lm[1], `${lm[1]}.js`)
if (bundleJs && existsSync(bundleJs)) {
shotBundles[name] = { bundle: lm[1], sha256: createHash('sha256').update(readFileSync(bundleJs)).digest('hex') }
}
@ -234,22 +235,22 @@ async function shot(name, path, {
//
// all-people-groups is a genuine cap-1: ONE .vue, NO instances.json, manifest type "single".
// It DOES own a map, so it needs a token and longer to settle than the token-free vision card did.
await shot('cap1-single-element', '/doxa-maps/all-people-groups/all-people-groups/index.html',
await shot('cap1-single-element', `/${TREE_NAME}/all-people-groups/all-people-groups/index.html`,
{ wait: 8000, minElements: 25 })
// ---- CAP2: prop-table -> 3 shots from ONE root.vue (prayer/engagement/adoption) ----
await shot('cap2-prayer', '/doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html', { waitFor: CANVAS, wait: 8000 })
await shot('cap2-engagement', '/doxa-maps/doxa-simple-map/doxa-simple-map/engagement/index.html', { waitFor: CANVAS, wait: 8000 })
await shot('cap2-adoption', '/doxa-maps/doxa-simple-map/doxa-simple-map/adoption/index.html', { waitFor: CANVAS, wait: 8000 })
await shot('cap2-prayer', `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/prayer/index.html`, { waitFor: CANVAS, wait: 8000 })
await shot('cap2-engagement', `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/engagement/index.html`, { waitFor: CANVAS, wait: 8000 })
await shot('cap2-adoption', `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/adoption/index.html`, { waitFor: CANVAS, wait: 8000 })
// ---- CAP3: monolithic research map ----
await shot('cap3-monolithic-research', '/doxa-maps/doxa-research-map/research-map/index.html', { waitFor: CANVAS, wait: 9000 })
await shot('cap3-monolithic-research', `/${TREE_NAME}/doxa-research-map/research-map/index.html`, { waitFor: CANVAS, wait: 9000 })
// ---- CAP4: nested-element SPA. The bundle ships THREE profiles, one shot each:
// upg-100-mfe (the cap-4 composite) · upg-100-list (the list) · poster-wall.
// There are no "list pages" — see the note below the second shot. ----
await shot('cap4-upg100-mfe', '/doxa-maps/my-upg-100-list/upg-100-mfe/index.html', { wait: 8000 })
await shot('cap4-upg100-list', '/doxa-maps/my-upg-100-list/upg-100-list/index.html', { wait: 7000 })
await shot('cap4-upg100-mfe', `/${TREE_NAME}/my-upg-100-list/upg-100-mfe/index.html`, { wait: 8000 })
await shot('cap4-upg100-list', `/${TREE_NAME}/my-upg-100-list/upg-100-list/index.html`, { wait: 7000 })
// ---- CAP4's THIRD profile: the poster wall ----
// ★ REPLACED 'cap4-upg100-list-page2' 2026-09-21. That shot loaded the SAME URL as page 1 and
@ -263,7 +264,7 @@ await shot('cap4-upg100-list', '/doxa-maps/my-upg-100-list/upg-100-list/index.ht
// ★ THE SLOT WAS CHASING UI THAT DOES NOT EXIST while a REAL profile went unshot: this bundle
// ships THREE manifest-listed profiles (upg-100-mfe, upg-100-list, poster-wall) and poster-wall
// had no screenshot anywhere. Shoot the real third thing rather than fake a fourth.
await shot('cap4-upg100-poster-wall', '/doxa-maps/my-upg-100-list/poster-wall/index.html', { wait: 8000 })
await shot('cap4-upg100-poster-wall', `/${TREE_NAME}/my-upg-100-list/poster-wall/index.html`, { wait: 8000 })
// ---- CAP5: nested SPA — the doxa-maps-spa ELEMENT, not the gallery page ----
// ★ CORRECTED 2026-09-20. This shot previously pointed at /index.html, the AUTO-GENERATED
@ -274,7 +275,7 @@ await shot('cap4-upg100-poster-wall', '/doxa-maps/my-upg-100-list/poster-wall/in
// mapboxReferer:false — a cap-5 shell nests EVERY tab in an iframe, so the context-wide Referer
// (see shot()) breaks its content exactly as it broke deploy1-iframe. The shell loads no Mapbox
// tiles of its own; each nested leaf is its own document and fetches its own.
await shot('cap5-doxa-maps-spa', '/doxa-maps/doxa-maps-spa/doxa-maps-spa/index.html',
await shot('cap5-doxa-maps-spa', `/${TREE_NAME}/doxa-maps-spa/doxa-maps-spa/index.html`,
{ wait: 10000, mapboxReferer: false })
// ---- CAP5 (the OTHER one): the plugin-studio ELEMENT — the DEVELOPER gallery ----
@ -300,7 +301,7 @@ await shot('cap5-doxa-maps-spa', '/doxa-maps/doxa-maps-spa/doxa-maps-spa/index.h
// Clicks the first rail row (cap-5 → doxa-maps-spa), so the picture shows what the studio is FOR:
// a nested SPA rendering inside the shell that lists every element by capability. Fails soft — if
// the row is not found the shot still captures, and the readiness guard below still applies.
await shot('cap5-plugin-studio', '/doxa-maps/plugin-studio/plugin-studio/index.html', {
await shot('cap5-plugin-studio', `/${TREE_NAME}/plugin-studio/plugin-studio/index.html`, {
// Same iframe-nesting reason as the other cap-5 shell above.
mapboxReferer: false,
wait: 12000,
@ -337,7 +338,7 @@ await shot('cap5-plugin-studio', '/doxa-maps/plugin-studio/plugin-studio/index.h
// exited 0, so deploy1-iframe.png has been a screenshot of a 404 page since 2026-09-18.
// The host page is SERVED FROM MEMORY rather than written into public/: anything written there
// is agentic scratch in the studio tree and would be destroyed by the next build's clean.
const IFRAME_SRC = '/doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html'
const IFRAME_SRC = `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/prayer/index.html`
+ '?tk=' + encodeURIComponent(TK)
virtualPages.set('/__readme-iframe-host.html', `<!doctype html>
<html><head><meta charset="utf-8"><title>Embedding a Doxa map</title>
@ -369,31 +370,31 @@ await shot('deploy1-iframe', '/__readme-iframe-host.html',
// ---- THE 7 PRODUCTION MAPS: one shot each, for the README's opening gallery ----
// Each is the plugin's own standalone leaf page — the same element the Research Gallery nests.
// (UPG-100 List and Prayer Progress reuse cap4-upg100-mfe and cap2-prayer.)
await shot('prod-big-picture', '/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html', { wait: 10000, minElements: 300 })
await shot('prod-simple-research', '/doxa-maps/research-map-plus-grid/research-map-plus-grid/index.html', { wait: 10000, minElements: 200 })
await shot('prod-regions', '/doxa-maps/regions-dashboard/regions-mfe/index.html', { wait: 10000, minElements: 100 })
await shot('prod-cities-internet', '/doxa-maps/cities-internet/connectivity-map/index.html', { wait: 10000, minElements: 60 })
await shot('prod-gospel-resources', '/doxa-maps/gospel-resources/resources-map/index.html', { wait: 10000, minElements: 100 })
await shot('prod-big-picture', `/${TREE_NAME}/dashboard-plus-grid/dashboard-plus-grid/index.html`, { wait: 10000, minElements: 300 })
await shot('prod-simple-research', `/${TREE_NAME}/research-map-plus-grid/research-map-plus-grid/index.html`, { wait: 10000, minElements: 200 })
await shot('prod-regions', `/${TREE_NAME}/regions-dashboard/regions-mfe/index.html`, { wait: 10000, minElements: 100 })
await shot('prod-cities-internet', `/${TREE_NAME}/cities-internet/connectivity-map/index.html`, { wait: 10000, minElements: 60 })
await shot('prod-gospel-resources', `/${TREE_NAME}/gospel-resources/resources-map/index.html`, { wait: 10000, minElements: 100 })
// ---- THE PRODUCTION MAPS ON A PHONE (390x844) — for the README's phone row ----
// Big Picture is desktop-only by ruling (on phones the Research Gallery disables it and Simple Research is
// the dashboard), so it has no phone shot. UPG-100's phone shot is deploy2-mobile-upg100.
await shot('prod-simple-research-phone', '/doxa-maps/research-map-plus-grid/research-map-plus-grid/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-research-gallery-phone', '/doxa-maps/doxa-maps-spa/doxa-maps-spa/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-regions-phone', '/doxa-maps/regions-dashboard/regions-mfe/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-prayer-phone', '/doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-cities-internet-phone', '/doxa-maps/cities-internet/connectivity-map/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-gospel-resources-phone', '/doxa-maps/gospel-resources/resources-map/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-simple-research-phone', `/${TREE_NAME}/research-map-plus-grid/research-map-plus-grid/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-research-gallery-phone', `/${TREE_NAME}/doxa-maps-spa/doxa-maps-spa/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-regions-phone', `/${TREE_NAME}/regions-dashboard/regions-mfe/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-prayer-phone', `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/prayer/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-cities-internet-phone', `/${TREE_NAME}/cities-internet/connectivity-map/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-gospel-resources-phone', `/${TREE_NAME}/gospel-resources/resources-map/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
// ---- EXPERIMENTAL SHELF: the plugins the README pictures there ----
await shot('exp-wall-of-people-groups', '/doxa-maps/wall-of-people-groups/wall-of-people-groups/index.html', { wait: 12000, minElements: 40 })
await shot('exp-prayer-walk-planner', '/doxa-maps/prayer-walk-planner/prayer-walk-planner/index.html', { wait: 12000, minElements: 40 })
await shot('exp-wall-of-people-groups', `/${TREE_NAME}/wall-of-people-groups/wall-of-people-groups/index.html`, { wait: 12000, minElements: 40 })
await shot('exp-prayer-walk-planner', `/${TREE_NAME}/prayer-walk-planner/prayer-walk-planner/index.html`, { wait: 12000, minElements: 40 })
// ---- DEPLOY2: mobile — the UPG-100 list as the phone app ----
// The mobile example is UPG-100 (ruled 2026-09-24; Simple Research was retired as the example because
// it was never built for phones). Shot once its owner marked the phone layout ready at 88a566f:
// posters one per row, the legend a bottom sheet flush with the map, print options wrapping.
await shot('deploy2-mobile-upg100', '/doxa-maps/my-upg-100-list/upg-100-mfe/index.html',
await shot('deploy2-mobile-upg100', `/${TREE_NAME}/my-upg-100-list/upg-100-mfe/index.html`,
{ vp: { width: 390, height: 844 }, wait: 10000, dsf: 3 })
// ---- DEPLOY3: JS embed — the Research Gallery on a real host site ----

View file

@ -40,6 +40,7 @@ import { createReadStream, statSync } from 'node:fs'
import { resolve, join, extname, normalize } from 'node:path'
import { dirname } 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 argv = process.argv.slice(2)
@ -79,7 +80,7 @@ const send = (res, code, body, headers = {}) => {
// A 503 is deliberate, not a 404: 404 means "this is not here", which a gate may reasonably report
// as a product defect. 503 with a named cause means "ask again later" and cannot be mistaken for a
// finding. Static assets outside the bundle tree (assets/, __gallery/) are unaffected.
const TREE = join(DIR, 'doxa-maps')
const TREE = join(DIR, TREE_NAME)
const MANIFEST = join(TREE, 'manifest.json')
function treeIsTorn() {
try { return !statSync(MANIFEST).isFile() } catch { return true }
@ -89,8 +90,8 @@ createServer((req, res) => {
let pathname
try { pathname = decodeURIComponent(new URL(req.url, 'http://x').pathname) } catch { return send(res, 400, 'bad request\n') }
if (pathname.startsWith('/doxa-maps/') && treeIsTorn()) {
return send(res, 503, 'tree is mid-build — public/doxa-maps/manifest.json is absent.\n' +
if (pathname.startsWith(`/${TREE_NAME}/`) && treeIsTorn()) {
return send(res, 503, `tree is mid-build — public/${TREE_NAME}/manifest.json is absent.\n` +
'A build rm -rf\'s this tree at its start, so anything measured now is a phantom.\n' +
'Wait for the build to finish and retry; do NOT record this as a product finding.\n',
{ 'retry-after': '30' })
@ -128,7 +129,7 @@ createServer((req, res) => {
// ACAO on every path INSIDE a folder of the tree, NOT on the tree-root manifest.json. Off by
// default; check:gallery-embed --live-cors uses it to prove the gallery still boots cross-origin
// under the rule partners actually get.
...(process.env.LIVE_CORS === '1' && /^\/doxa-maps\/manifest\.json$/.test(pathname) ? {} : { 'access-control-allow-origin': '*' }),
...(process.env.LIVE_CORS === '1' && new RegExp(`^/${TREE_NAME_RE}/manifest\\.json$`).test(pathname) ? {} : { 'access-control-allow-origin': '*' }),
})
createReadStream(file).pipe(res)
}).listen(PORT, HOST, () => {

View file

@ -31,6 +31,7 @@
import { readFileSync, writeFileSync, existsSync, copyFileSync } 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 CHECK = process.argv.includes('--check')
@ -39,12 +40,12 @@ const CHECK = process.argv.includes('--check')
const APP_PUBLIC = join(ROOT, 'deployment/capacitor/android/app/src/main/assets/public')
const APP_ROOT = join(APP_PUBLIC, 'index.html')
// The showcase leaf, inside the served tree.
const TREE = 'doxa-maps'
const TREE = TREE_NAME
const LEAF_REL = `${TREE}/my-upg-100-list/upg-100-mfe/index.html`
const LEAF = join(APP_PUBLIC, LEAF_REL)
// A marker so --check can tell the app page apart from the studio page without parsing HTML.
const MARKER = '<!-- doxa-maps: mobile app start page (UPG-100 list) -->'
const MARKER = `<!-- ${TREE_NAME}: mobile app start page (UPG-100 list) -->`
if (!existsSync(APP_PUBLIC)) {
console.log('APP-START-PAGE: SKIP — no synced assets tree.')

View file

@ -40,8 +40,9 @@
import { readdirSync, statSync, existsSync } from 'node:fs'
import { join } from 'node:path'
import { execSync } from 'node:child_process'
import { TREE_NAME } from './_tree-name.mjs'
const TREE = process.env.TREE || 'public/doxa-maps'
const TREE = process.env.TREE || `public/${TREE_NAME}`
const QUIET_SECONDS = Number(process.env.QUIET_SECONDS || 45)
// ★ RAISED 2026-09-23 from 17 to 21, on this gate's OWN advice. It had been printing a NOTE on
// every green run — "the tree has outgrown its floors ... they still catch a CATASTROPHICALLY gutted

View file

@ -43,6 +43,7 @@ import { createServer } from 'node:http'
import { readFile, stat } from 'node:fs/promises'
import { join, extname, 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)), '..', 'public')
const TYPES = { '.html':'text/html', '.js':'text/javascript', '.mjs':'text/javascript',
@ -122,11 +123,11 @@ try {
// A generated profile page — the surface a partner iframes. Picked from the
// manifest rather than hardcoded, so renaming or removing a bundle cannot leave
// this silently checking a page that no longer exists.
const manifest = JSON.parse(await readFile(join(ROOT, 'doxa-maps', 'manifest.json'), 'utf8'))
const manifest = JSON.parse(await readFile(join(ROOT, TREE_NAME, 'manifest.json'), 'utf8'))
const first = manifest.bundles.find(b => b.profiles?.length)
if (!first) fail.push('manifest declares no bundle with a profile — nothing to iframe')
else {
const path = `/doxa-maps/${first.bundle}/${first.profiles[0].profile}/`
const path = `/${TREE_NAME}/${first.bundle}/${first.profiles[0].profile}/`
const p2 = await browser.newPage()
const err2 = [], failed2 = []
p2.on('pageerror', e => err2.push(e.message.split('\n')[0]))

View file

@ -52,8 +52,9 @@
import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { join, dirname, resolve } from 'node:path'
import { createHash } from 'node:crypto'
import { TREE_NAME } from './_tree-name.mjs'
const TREE = process.argv[2] || 'public/doxa-maps'
const TREE = process.argv[2] || `public/${TREE_NAME}`
if (!existsSync(TREE)) {
console.error('SRI-CONSISTENCY: tree not found: ' + TREE)
process.exit(2)
@ -119,7 +120,7 @@ if (bad.length) {
console.error(' …then re-run this check. Do NOT publish until it passes.')
console.error('')
console.error(' ★ DO NOT reach for `bun run build` here. This message used to prescribe exactly')
console.error(' that, and it is the destructive remedy: its first step DELETES public/doxa-maps/,')
console.error(` that, and it is the destructive remedy: its first step DELETES public/${TREE_NAME}/,`)
console.error(' tearing the tree out from under every other agent reading it (measured 3+ times,')
console.error(' incl. a 461-file phantom deletion and several false "element did not mount"')
console.error(' diagnoses). A full rebuild cannot fix anything here that rehashing does not —')

View file

@ -51,6 +51,7 @@ import { readdirSync, statSync, lstatSync, mkdirSync, copyFileSync, rmSync, exis
import { join, relative, resolve, dirname, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createHash } from 'node:crypto'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const CONFIG = join(ROOT, 'vite.doxa-maps-build-config.json')
@ -73,7 +74,7 @@ const isDoc = (f) => /\.(md|markdown|txt)$/i.test(f)
// Staging/preview pages are INTERNAL surfaces: <tree>/index.html and <tree>/<bundle>/index.html
// (the leaf pages partners embed live one level deeper). A public mirror carries bundles, leaf
// pages and the data they fetch — not previews. --keep-staging opts back in.
const TREE = String(argOf('--tree', 'doxa-maps')).replace(/\/+$/, '')
const TREE = String(argOf('--tree', TREE_NAME)).replace(/\/+$/, '')
const KEEP_STAGING = args.includes('--keep-staging')
const isStaging = (f) => !KEEP_STAGING && (f === `${TREE}/index.html` || new RegExp(`^${TREE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/[^/]+/index\\.html$`).test(f))
// Built data nothing shipped fetches: "dropin.exclude" in the build-config (read with --from-config,

View file

@ -36,9 +36,10 @@ 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 GL=['--use-gl=swiftshader','--enable-unsafe-swiftshader','--ignore-gpu-blocklist','--enable-webgl','--no-sandbox']
const B=(process.env.BASE || 'https://nuxtbase.app/plugin-assets/doxa-maps/').replace(/\/?$/,'/')
const B=(process.env.BASE || `https://nuxtbase.app/plugin-assets/${TREE_NAME}/`).replace(/\/?$/,'/')
const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port

View file

@ -57,6 +57,7 @@ import { servesThisTree } from './_serves-this-tree.mjs'
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 BASE = process.env.BASE_URL || 'http://127.0.0.1:4173'
@ -91,9 +92,9 @@ const GATES = [
]
// ── PRECONDITION 1: a built tree ────────────────────────────────────────────
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) {
console.error('BROWSER-ALL: SETUP — no public/doxa-maps/manifest.json. Run `bun run build` first.')
console.error(`BROWSER-ALL: SETUP — no public/${TREE_NAME}/manifest.json. Run \`bun run build\` first.`)
process.exit(2)
}
let bundleCount = 0
@ -107,7 +108,7 @@ if (bundleCount < 2) {
// Every gate here fetches over HTTP. Without this check a missing server yields six identical
// timeouts and looks like six broken gates.
try {
const r = await fetch(`${BASE}/doxa-maps/manifest.json`, { signal: AbortSignal.timeout(5000) })
const r = await fetch(`${BASE}/${TREE_NAME}/manifest.json`, { signal: AbortSignal.timeout(5000) })
// ★ 503 IS A DIFFERENT DIAGNOSIS FROM "no server", and saying so is the whole point of the code
// below. serve-strict.mjs returns 503 while public/doxa-maps/manifest.json is absent, because a
// build rm -rf's that tree at its START — so a gate running then reads an EMPTY tree and reports
@ -117,7 +118,7 @@ try {
// server, sending the reader to start one that was already running.
if (r.status === 503) {
console.error('BROWSER-ALL: NOT MEASURED — the built tree is mid-build.')
console.error(` ${BASE} answered 503: public/doxa-maps/manifest.json is absent, which is what a`)
console.error(` ${BASE} answered 503: public/${TREE_NAME}/manifest.json is absent, which is what a`)
console.error(' build looks like from the outside (clean rm -rf\'s the tree before rebuilding it).')
console.error(' Anything measured now is a PHANTOM. Wait for the build and re-run; do NOT record')
console.error(' this as a product finding. Check with: bun run check:settled')

View file

@ -47,6 +47,7 @@ import { spawn } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const BASE = (process.env.BASE_URL || process.argv[2] || 'http://127.0.0.1:4173').replace(/\/+$/, '')
const GL = ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl', '--no-sandbox']
@ -68,7 +69,7 @@ let server = null
// The fingerprint is the built bundle itself: if the bytes served differ from the bytes on
// disk, it is a different tree and must not be used.
const alive = async (b) => {
const r = await fetch(`${b}/doxa-maps/gospel-resources/resources-map/index.html`).catch(() => null)
const r = await fetch(`${b}/${TREE_NAME}/gospel-resources/resources-map/index.html`).catch(() => null)
if (!(r && r.ok)) return false
const tree = await servesThisTree(b, REPO)
if (tree.ok) return true
@ -81,7 +82,7 @@ if (!(await alive(base))) {
console.log(`GOSPEL-MAP: SKIP — nothing served at ${base} (you named it explicitly, so none was started).`)
process.exit(0)
}
if (!existsSync(resolve(REPO, 'public/doxa-maps/gospel-resources/resources-map/index.html'))) {
if (!existsSync(resolve(REPO, `public/${TREE_NAME}/gospel-resources/resources-map/index.html`))) {
console.log('GOSPEL-MAP: SKIP — no built tree. Run `bun run tree` (cheap) or a full build first.')
process.exit(0)
}
@ -98,7 +99,7 @@ if (!(await alive(base))) {
}
console.log(`GOSPEL-MAP: started serve-strict on ${port} for this run.`)
}
const PAGE_URL = `${base}/doxa-maps/gospel-resources/resources-map/index.html`
const PAGE_URL = `${base}/${TREE_NAME}/gospel-resources/resources-map/index.html`
const fails = []
let linkCount = 0
@ -147,7 +148,7 @@ try {
// jw.org URL planted in the served data did NOT fail this gate until the scan covered
// the whole index. The doctrine boundary is the one thing here that must never be
// spot-checked: a single off-list link is a doctrinal failure, not a broken link.
const idxRes = await fetch(`${base}/doxa-maps/assets/tables/people-group-pins.json`)
const idxRes = await fetch(`${base}/${TREE_NAME}/assets/tables/people-group-pins.json`)
if (!idxRes.ok) fails.push(`pin index not served (${idxRes.status})`)
else {
const idx = await idxRes.json()

View file

@ -33,6 +33,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'
// The SwiftShader launch. Without these a headless Chromium here has NO GL context at all.
const GL_ARGS = [
@ -43,7 +44,7 @@ const GL_ARGS = [
'--no-sandbox',
]
const BASE = process.env.BASE || 'https://nuxtbase.app/plugin-assets/doxa-maps'
const BASE = process.env.BASE || `https://nuxtbase.app/plugin-assets/${TREE_NAME}`
const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port
@ -147,7 +148,7 @@ const splitErrors = (lines) => {
let bad = 0
for (const url of urls) {
const name = url.split('/doxa-maps/')[1] || url
const name = url.split(`/${TREE_NAME}/`)[1] || url
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } })
const errors = []
// ⚠ THE STYLESHEET COMPLAINT IS A WARNING, NOT AN ERROR, so filtering to type()==='error' above

View file

@ -26,6 +26,7 @@ import { chromium } from 'playwright'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { requireThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
// Default matches every other browser gate in this repo: `bun run serve` on loopback 4173.
// It was `localhost:8899` — one of the ad-hoc ports a previous session happened to have running,
@ -39,11 +40,11 @@ const BASE = process.env.BASE_URL || process.argv[2] || 'http://127.0.0.1:4173'
// gate passed for a whole flight on URLs that do not work in production. `bun run serve` is now
// internal/serve-strict.mjs, which 404s a directory exactly as production does, and it caught these
// two the first time it ran. The manifest already ships 28/28 iframeUrl values in this form.
const SHELL = '/doxa-maps/plugin-studio/plugin-studio/index.html'
const SHELL = `/${TREE_NAME}/plugin-studio/plugin-studio/index.html`
// Measure THIS build, or measure nothing: a default port on this machine can belong to another
// worktree, and a stale studio there would be graded as ours. Exit 75 = NOT MEASURED (see a9550858).
await requireThisTree(BASE, resolve(dirname(fileURLToPath(import.meta.url)), '..'), 'SHELL-SCROLL')
const LEAF = '/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html'
const LEAF = `/${TREE_NAME}/dashboard-plus-grid/dashboard-plus-grid/index.html`
// Shadow DOM DOES NOT PIERCE. A single-level querySelector lies about every composite, so every
// lookup here is a recursive walk with a seen-Set, re-injected after each interaction (a page

View file

@ -26,22 +26,17 @@ import { autoSpaDevPlugin } from './internal/plugin-gallery/dev-plugin.mjs'
import { stripShadowCssCommentsPlugin } from './internal/vite-plugin-strip-shadow-css-comments.mjs'
import { fileURLToPath } from 'node:url'
import { resolve, dirname } from 'node:path'
import { readdirSync, statSync, existsSync, readFileSync } from 'node:fs'
import { readdirSync, statSync, existsSync } from 'node:fs'
import { TREE_NAME } from './internal/_tree-name.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const profilesDir = resolve(__dirname, 'plugin-designs')
// The multi-MFE output folder name is configured in vite.doxa-maps-build-config.json (`name`),
// so you can name the output tree (e.g. "doxa-maps") independently of this bundler
// folder's own name. Falls back to "doxa-maps" if the file is missing/unset.
function readMfeName() {
try {
const cfg = JSON.parse(readFileSync(resolve(__dirname, 'vite.doxa-maps-build-config.json'), 'utf8'))
if (cfg && typeof cfg.name === 'string' && cfg.name.trim()) return cfg.name.trim()
} catch { /* fall through to default */ }
return 'doxa-maps'
}
const MFE_NAME = readMfeName()
// so you can name the output tree independently of this bundler folder's own name. It is read in
// ONE place, internal/_tree-name.mjs, which every tool shares — so renaming the tree is a one-line
// config change rather than a hunt through the gates.
const MFE_NAME = TREE_NAME
// Default output tree. generate-tree.mjs reads the same name + copies it onward.
// DOXA_OUT_ROOT redirects a build's output WITHOUT touching public/: check:composite rebuilds each
@ -74,7 +69,7 @@ export default defineConfig(({ command, mode }) => {
if (isBuild && !target) {
throw new Error(
`[doxa-maps] Set BUNDLE=<name> for build, or use \`npm run build\` (loops all).\n` +
`[${MFE_NAME}] Set BUNDLE=<name> for build, or use \`npm run build\` (loops all).\n` +
`Available: ${allNames.join(', ') || '(none)'}`
)
}
@ -84,7 +79,7 @@ export default defineConfig(({ command, mode }) => {
// entry pair (e.g. a half-authored map, or a folder still being scaffolded). Fail with
// a clear, actionable message instead of a `Cannot read properties of undefined` stack.
throw new Error(
`[doxa-maps] Bundle "${target}" is not buildable: plugin-designs/${target}/ is missing ` +
`[${MFE_NAME}] Bundle "${target}" is not buildable: plugin-designs/${target}/ is missing ` +
`index.html and/or index.js (the custom-element entry pair every bundle needs).\n` +
`Buildable bundles: ${allNames.join(', ') || '(none)'}`
)