public/ is one folder: the output tree plus a README, with the Plugin Studio as its front page
Driver decisions (cards BUILD-OUTPUT-IS-ONE-FOLDER, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE).
- public/ holds only <tree>/ and a generated README.md. The build no longer writes public/index.html,
public/assets/ or public/__gallery/; `rm -rf public && bun run build` recreates exactly these two.
- The Plugin Studio is <tree>/index.html (tree-base "."), replacing the staging-forge chooser; nothing
is written above the tree. check:staging-lang, which tested that chooser, is retired with it.
- Assets are gathered in a temp directory and the fetched allowlist staged into <tree>/assets/ as
before. fonts/, images/, brand/ and legacy/ existed ONLY in public/assets: they are sources and move
to data/ (git mv). Poster previews keep their tracked sources in plugin-designs/*/assets/previews
(all 378 verified present).
- __gallery/ is archived out of public/ (a copy kept in the umbrella's _qa-cycle/archive) and out of
the build; generate-gallery renders only on request, into a directory you name, never public/.
- Native shells load the output folder: capacitor webDir public/<tree> (cap copy needs an index.html
there), Tauri frontendDist + window url relative to it. internal/sync-app-shells.mjs keeps both in
step with the tree's name on every build, so the coming rename stays one config line; the Capacitor
start page resolves its leaf tree-relative.
- dropin.include now lists only extras (README.md): the tree is always shipped and never named twice;
excludes are tree-relative ({tree}/...). clean.mjs removes the old root entries from existing
checkouts; the build lock and public/.gitignore's rules move out of public/.
- check:wp-embed read the BUILT manifest from gates:pre, before the build, so a from-scratch build
could never pass; it runs in gates:post and reads the tree name from config.
- Stale bundles refreshed by the clean build (key-cities, template-bundle, wall-of-people-groups);
the CLI doc's quoted scripts re-synced; capability exemplars regenerated; the '#' note leaves
check:stale-sweep-claim's command (it was already in //scripts).
Proof: rm -rf public && bun run build -> public/ = README.md + doxa-maps/; check:sri, check:drop,
check:parse, check:public-hygiene, check:quoted-scripts, check:capability-exemplars PASS; all 57 pages
(the front page + 56 leaves) mount under /doxa-maps/ on this tree. check:facet-cascade is red as on main.
11
.gitignore
vendored
|
|
@ -134,3 +134,14 @@ _qa-cycle/
|
||||||
# Private datasets never live in this repo (see DOXA_PRIVATE_DIR in
|
# Private datasets never live in this repo (see DOXA_PRIVATE_DIR in
|
||||||
# library/scripts/research/build-tabulator-tables.mjs). Guard against re-creation.
|
# library/scripts/research/build-tabulator-tables.mjs). Guard against re-creation.
|
||||||
data-private/
|
data-private/
|
||||||
|
|
||||||
|
# ── public/ is the OUTPUT FOLDER + README.md only (Driver decision, BUILD-OUTPUT-IS-ONE-FOLDER, 2026-09-25) ──
|
||||||
|
# These rules used to live in public/.gitignore, which was itself a tracked file INSIDE the generated
|
||||||
|
# folder (so `rm -rf public && bun run build` deleted it). Local-only files stay untracked:
|
||||||
|
/public/token.local.js
|
||||||
|
# the build mutex's stamp (internal/build-lock.sh) — runtime state, now at the repo root
|
||||||
|
/.build.lock
|
||||||
|
# what the OLD layout wrote beside the tree; internal/clean.mjs removes any a checkout still has
|
||||||
|
/public/assets/
|
||||||
|
/public/__gallery/
|
||||||
|
/public/index.html
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,20 @@
|
||||||
"appId": "life.doxa.maps",
|
"appId": "life.doxa.maps",
|
||||||
"appName": "Doxa Maps",
|
"appName": "Doxa Maps",
|
||||||
"//webDir": [
|
"//webDir": [
|
||||||
"webDir = ./public \u2014 THE CDN WEB-ROOT. It is a POINTER, NOT A COPY.",
|
"webDir = public/<tree> \u2014 THE OUTPUT FOLDER, written by the build (internal/sync-app-shells.mjs keeps it",
|
||||||
"public/ is the single SERVED SURFACE, so the bundler IS the CDN: it serves its own bundles AND",
|
"in step with the tree's name in vite.doxa-maps-build-config.json; do not edit it by hand).",
|
||||||
"non-bundle assets (brand/logos, vendored libs, fonts) that belong to no single bundle.",
|
"public/ holds ONLY the output folder + README.md (Driver decision, BUILD-OUTPUT-IS-ONE-FOLDER). The folder is",
|
||||||
" public/doxa-maps/<bundle>/ built bundles (vite outDir app/ + copied here by the build config's paths)",
|
"self-contained: its index.html is the front page (the Plugin Studio), <bundle>/ holds each plugin, and",
|
||||||
" public/assets/brand/ logos and brand imagery",
|
"assets/ carries the data, fonts and images the plugins fetch.",
|
||||||
" public/assets/vendor/ self-hosted libs (e.g. mapbox-gl) \u2014 no CDN, per the no-CDN rule",
|
"ONE build, THREE consumers, all reading THIS folder:",
|
||||||
" public/assets/fonts/ Bebas Neue / Poppins (both SIL OFL, so self-hostable)",
|
" 1. Railway / CDN \u2014 serve it statically; partner sites embed from stable URLs",
|
||||||
"ONE build, THREE consumers, all reading THIS root:",
|
" 2. Capacitor \u2014 this webDir; `cap sync` copies it into deployment/capacitor/android/app/src/main/assets/public/,",
|
||||||
" 1. Railway / CDN \u2014 serve public/ statically; partner sites embed from stable URLs",
|
" then `bun run app:start-page` makes the UPG-100 list the app's start page",
|
||||||
" 2. Capacitor \u2014 this webDir; `cap sync` copies public/ into deployment/capacitor/android/app/src/main/assets/public/",
|
" 3. local staging \u2014 `bun run serve` (public/ on :4173; open /<tree>/index.html)",
|
||||||
" 3. local staging \u2014 `bun run serve` (public/ on :4173)",
|
|
||||||
"NOTE the drop-portability gate still governs what goes INSIDE a bundle: bundle-internal refs must stay",
|
|
||||||
"RELATIVE. public/ is the HOST root, which is a different layer \u2014 putting shared assets here does not",
|
|
||||||
"make a bundle root-relative, so the gate stays green.",
|
|
||||||
"\u26a0\ufe0f SOFT FAILURE: a wrong webDir yields an EMPTY assets/public and a BLANK WHITE SCREEN at launch, not an",
|
"\u26a0\ufe0f SOFT FAILURE: a wrong webDir yields an EMPTY assets/public and a BLANK WHITE SCREEN at launch, not an",
|
||||||
"error. After every sync, verify deployment/capacitor/android/app/src/main/assets/public/index.html exists (`bun run cap:verify`)."
|
"error. After every sync, verify deployment/capacitor/android/app/src/main/assets/public/index.html exists (`bun run cap:verify`)."
|
||||||
],
|
],
|
||||||
"webDir": "public",
|
"webDir": "public/doxa-maps",
|
||||||
"server": {
|
"server": {
|
||||||
"//": [
|
"//": [
|
||||||
"Capacitor defaults are deliberately kept: iosScheme 'capacitor', androidScheme 'https', hostname",
|
"Capacitor defaults are deliberately kept: iosScheme 'capacitor', androidScheme 'https', hostname",
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,26 @@
|
||||||
# data/ — the bundler's source-of-truth data
|
# data/ — the bundler's source-of-truth data
|
||||||
|
|
||||||
**The law: data lives here; `public/` is generated.** Every folder in `data/` is copied into the
|
**The law: data lives here; `public/` is generated.** `public/` holds only the output folder
|
||||||
served web-root by the build (`internal/generate-tree.mjs`), exactly like the per-profile data in
|
(`public/<tree>/`, named by `name` in `vite.doxa-maps-build-config.json`) and a generated README. The
|
||||||
`plugin-designs/*/data/`. You should be able to `rm -rf public/` and get it all back with `bun run build`.
|
build (`internal/generate-tree.mjs`) copies what the plugins FETCH at runtime from here into
|
||||||
|
`<tree>/assets/`, alongside the per-profile data in `plugin-designs/*/data/`. You should be able to
|
||||||
|
`rm -rf public/` and get it all back with `bun run build`.
|
||||||
|
|
||||||
| Folder | Served at | What it is |
|
| Folder | Served at | What it is |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `people-groups/` | `/assets/people-groups/` | People-group datasets (CSV/JSON) the maps read at runtime |
|
| `people-groups/` | `<tree>/assets/people-groups/` (the fetched cuts only) | People-group datasets (CSV/JSON) the maps read at runtime |
|
||||||
| `vendor/` | `/assets/vendor/` | Third-party runtime assets (e.g. table libraries) |
|
| `vendor/` | `<tree>/assets/vendor/` | Third-party runtime assets (e.g. table libraries) |
|
||||||
| `posters/` | `/assets/posters/` | Rendered poster webp set (heavy pipeline output; poster walls fetch these) |
|
| `posters/` | `<tree>/assets/posters/` (opt-in: `bundlePosters` / `posterSources`) | Rendered poster set (heavy pipeline output; the export button fetches these) |
|
||||||
| `tables/` | `/assets/tables/` | The MERGED served table set (its index.json is the merge; per-profile `plugin-designs/*/data/tables` are build-time sources) |
|
| `tables/` | `<tree>/assets/tables/` | The MERGED served table set (its index.json is the merge; per-profile `plugin-designs/*/data/tables` are build-time sources) |
|
||||||
| `external-bundles/` | not served | Prebuilt map IIFEs vendored from sibling projects (not built here); only the optional `layouts/` shell mounted them, so the build no longer copies them into `public/` |
|
| `pplr/` | `<tree>/assets/pplr/` (the fetched files only) | The PPLR data maps' derived datasets |
|
||||||
|
| `fonts/` | `<tree>/assets/fonts/` | Vendored typefaces (SIL OFL), e.g. Bebas Neue for the regions clone |
|
||||||
|
| `images/` | `<tree>/assets/images/` | Images the plugins fetch (e.g. the regions list's world-map watermark; see its PROVENANCE) |
|
||||||
|
| `brand/` | not served | Logos and brand imagery (sources; the optional `library/layouts/app-wrapper-material` shell references them) |
|
||||||
|
| `legacy/` | not served | Archived standalone builds kept for reference (e.g. the original upg-neighbor page) |
|
||||||
|
| `external-bundles/` | not served | Prebuilt map IIFEs vendored from sibling projects (not built here); only the optional `layouts/` shell mounted them |
|
||||||
|
|
||||||
|
`fonts/`, `images/`, `brand/` and `legacy/` moved here from `public/assets/` on 2026-09-25 (card
|
||||||
|
BUILD-OUTPUT-IS-ONE-FOLDER): they were sources that existed only in the generated folder.
|
||||||
|
|
||||||
Don't edit anything under `public/` by hand — change it here (or in `plugin-designs/*/data/`,
|
Don't edit anything under `public/` by hand — change it here (or in `plugin-designs/*/data/`,
|
||||||
`layouts/*/assets/`) and rebuild.
|
`layouts/*/assets/`) and rebuild.
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 422 KiB |
|
Before Width: | Height: | Size: 304 KiB After Width: | Height: | Size: 304 KiB |
|
|
@ -4,7 +4,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "life.doxa.maps",
|
"identifier": "life.doxa.maps",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../../public",
|
"frontendDist": "../../public/doxa-maps",
|
||||||
"beforeBuildCommand": ""
|
"beforeBuildCommand": ""
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"minWidth": 900,
|
"minWidth": 900,
|
||||||
"minHeight": 600,
|
"minHeight": 600,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"url": "doxa-maps/doxa-maps-spa/doxa-maps-spa/index.html"
|
"url": "doxa-maps-spa/doxa-maps-spa/index.html"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
* Say "contract-proven, not WP-shot" and nothing stronger.
|
* Say "contract-proven, not WP-shot" and nothing stronger.
|
||||||
*
|
*
|
||||||
* Usage: php wordpress/tests/wp-embed-contract-test.php (exit 0 pass / 1 fail / 2 no target)
|
* Usage: php wordpress/tests/wp-embed-contract-test.php (exit 0 pass / 1 fail / 2 no target)
|
||||||
* Requires: php CLI + a BUILT tree (public/doxa-maps/manifest.json).
|
* Requires: php CLI + a BUILT tree (public/<tree>/manifest.json; <tree> = the build config's name).
|
||||||
*/
|
*/
|
||||||
require __DIR__.'/wp-stubs.php';
|
require __DIR__.'/wp-stubs.php';
|
||||||
$PLUGIN = dirname(__DIR__).'/doxa-maps-embed';
|
$PLUGIN = dirname(__DIR__).'/doxa-maps-embed';
|
||||||
|
|
@ -30,7 +30,13 @@ require $PLUGIN.'/includes/class-doxa-maps-embed.php';
|
||||||
// Read the AUTHORITATIVE contract from the built catalog manifest.
|
// Read the AUTHORITATIVE contract from the built catalog manifest.
|
||||||
// dirname(__DIR__,3) — THREE levels, not two: wordpress/ moved under deployment/ on
|
// dirname(__DIR__,3) — THREE levels, not two: wordpress/ moved under deployment/ on
|
||||||
// 2026-09-21, so tests/ is now deployment/wordpress/tests and the repo root is 3 up.
|
// 2026-09-21, so tests/ is now deployment/wordpress/tests and the repo root is 3 up.
|
||||||
$mf = json_decode(file_get_contents(dirname(__DIR__,3).'/public/doxa-maps/manifest.json'), true);
|
// The output folder's name comes from the build config (vite.doxa-maps-build-config.json "name"), never a literal:
|
||||||
|
// the tree is renamed by that one line, and a hardcoded 'doxa-maps' here would silently test a missing folder.
|
||||||
|
$cfg = json_decode(@file_get_contents(dirname(__DIR__,3).'/vite.doxa-maps-build-config.json'), true);
|
||||||
|
$tree = (is_array($cfg) && !empty($cfg['name']) && preg_match('/^[a-z0-9][a-z0-9._-]*$/i', $cfg['name'])) ? $cfg['name'] : 'doxa-maps';
|
||||||
|
$mfPath = dirname(__DIR__,3).'/public/'.$tree.'/manifest.json';
|
||||||
|
if(!is_file($mfPath)){ fwrite(STDERR,"no built tree at public/$tree/manifest.json -- build first (this gate runs in gates:post)\n"); exit(2); }
|
||||||
|
$mf = json_decode(file_get_contents($mfPath), true);
|
||||||
$bundles = $mf['bundles'];
|
$bundles = $mf['bundles'];
|
||||||
$target = null;
|
$target = null;
|
||||||
foreach($bundles as $b){ foreach(($b['profiles']??[]) as $p){ if(!empty($p['element']) && !empty($p['integrity'])){ $target=['bundle'=>$b['name']??$b['bundle'],'profile'=>$p,'b'=>$b]; break 2; } } }
|
foreach($bundles as $b){ foreach(($b['profiles']??[]) as $p){ if(!empty($p['element']) && !empty($p['integrity'])){ $target=['bundle'=>$b['name']??$b['bundle'],'profile'=>$p,'b'=>$b]; break 2; } } }
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,9 @@ The scripts these commands are grounded in (from this branch's `package.json`):
|
||||||
// claiming to be ground truth, and this one had silently stopped being it.
|
// claiming to be ground truth, and this one had silently stopped being it.
|
||||||
"dev": "bun x vite",
|
"dev": "bun x vite",
|
||||||
"build": "bash internal/build-lock.sh",
|
"build": "bash internal/build-lock.sh",
|
||||||
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery",
|
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post",
|
||||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:wp-embed check:doc-scripts",
|
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts",
|
||||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary",
|
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed",
|
||||||
"build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time
|
"build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time
|
||||||
"preview": "bun x vite preview",
|
"preview": "bun x vite preview",
|
||||||
"serve": "node internal/serve-strict.mjs", // 404s a directory, like production
|
"serve": "node internal/serve-strict.mjs", // 404s a directory, like production
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
> bundle `parameterized` (cap-2) no matter how light it looks, and a `meta.json` `type`
|
> bundle `parameterized` (cap-2) no matter how light it looks, and a `meta.json` `type`
|
||||||
> declares tiers 3/4/5.
|
> declares tiers 3/4/5.
|
||||||
|
|
||||||
Generated from 21 bundle(s).
|
Generated from 23 bundle(s).
|
||||||
|
|
||||||
### One canonical exemplar per tier (generated — quote these)
|
### One canonical exemplar per tier (generated — quote these)
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ Generated from 21 bundle(s).
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 — single-element | `single` | `all-people-groups` |
|
| 1 — single-element | `single` | `all-people-groups` |
|
||||||
| 2 — preset-element (prop-table) | `parameterized` | `doxa-simple-map` |
|
| 2 — preset-element (prop-table) | `parameterized` | `doxa-simple-map` |
|
||||||
| 3 — multi-view-element (monolithic) | `monolithic` | `research-dashboard` |
|
| 3 — multi-view-element (monolithic) | `monolithic` | `pplr-data-maps` |
|
||||||
| 4 — composite-element (nested-element SPA) | `nested-elements-mfe` | `big-picture` |
|
| 4 — composite-element (nested-element SPA) | `nested-elements-mfe` | `big-picture` |
|
||||||
| 5 — app-shell (nested-SPA) | `nested-mfe-spa` | `doxa-maps-spa` |
|
| 5 — app-shell (nested-SPA) | `nested-mfe-spa` | `doxa-maps-spa` |
|
||||||
|
|
||||||
|
|
@ -67,8 +67,10 @@ Generated from 21 bundle(s).
|
||||||
| Bundle | Profile | Machine key |
|
| Bundle | Profile | Machine key |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `doxa-research-map` | `research-map` | `monolithic` |
|
| `doxa-research-map` | `research-map` | `monolithic` |
|
||||||
|
| `pplr-data-maps` | `pplr-data-maps` | `monolithic` |
|
||||||
| `research-dashboard` | `research-dashboard` | `monolithic` |
|
| `research-dashboard` | `research-dashboard` | `monolithic` |
|
||||||
| `upg-neighbor` | `upg-neighbor` | `monolithic` |
|
| `upg-neighbor` | `upg-neighbor` | `monolithic` |
|
||||||
|
| `wall-of-people-groups` | `wall-of-people-groups` | `monolithic` |
|
||||||
|
|
||||||
## Capability 4 — composite-element (nested-element SPA)
|
## Capability 4 — composite-element (nested-element SPA)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@
|
||||||
# half-tree):
|
# half-tree):
|
||||||
# 1. rename public/doxa-maps -> public/doxa-maps.old (instant; old still fully valid)
|
# 1. rename public/doxa-maps -> public/doxa-maps.old (instant; old still fully valid)
|
||||||
# 2. rename STAGE/doxa-maps -> public/doxa-maps (instant; new fully valid, built+verified)
|
# 2. rename STAGE/doxa-maps -> public/doxa-maps (instant; new fully valid, built+verified)
|
||||||
# 3. cp STAGE/index.html -> public/index.html (root gallery page)
|
# 3. cp STAGE/README.md -> public/README.md (the one other entry public/ holds)
|
||||||
# 4. rm -rf public/doxa-maps.old
|
# 4. rm -rf public/doxa-maps.old
|
||||||
# public/assets/, public/.gitignore, and public/.build.lock are NEVER touched — the flock handle stays valid.
|
# public/api/ (write-token) is NEVER touched — the flock handle stays valid.
|
||||||
#
|
#
|
||||||
# This is the LOCAL form of the atomic-publish discipline; the catalog MERGE (into platform/mfe-index) has its
|
# This is the LOCAL form of the atomic-publish discipline; the catalog MERGE (into platform/mfe-index) has its
|
||||||
# own hard-gate runbook. This script only makes the LOCAL public/ swap safe.
|
# own hard-gate runbook. This script only makes the LOCAL public/ swap safe.
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
# USAGE:
|
# USAGE:
|
||||||
# internal/atomic-public-swap.sh <STAGE_DIR>
|
# internal/atomic-public-swap.sh <STAGE_DIR>
|
||||||
# <STAGE_DIR> must contain a fully-built <STAGE_DIR>/doxa-maps/ tree (with manifest.json) and an
|
# <STAGE_DIR> must contain a fully-built <STAGE_DIR>/doxa-maps/ tree (with manifest.json) and an
|
||||||
# <STAGE_DIR>/index.html. Typically produced by a build with BUILD_STAGE_DIR pointed here.
|
# <STAGE_DIR>/README.md. Typically produced by a build with BUILD_STAGE_DIR pointed here.
|
||||||
# Env:
|
# Env:
|
||||||
# PUBLIC_DIR (default: public) the served web-root to swap into
|
# PUBLIC_DIR (default: public) the served web-root to swap into
|
||||||
# DRY_RUN=1 print the plan, make no changes
|
# DRY_RUN=1 print the plan, make no changes
|
||||||
|
|
@ -39,18 +39,18 @@ die() { echo "❌ atomic-public-swap: $*" >&2; exit 1; }
|
||||||
TREE="$(node "$(dirname "$0")/_tree-name.mjs")" || die "could not read the tree name (internal/_tree-name.mjs)"
|
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 "$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)"
|
[ -n "$STAGE_DIR" ] || die "usage: atomic-public-swap.sh <STAGE_DIR> (dir holding a built $TREE/ + README.md)"
|
||||||
[ -d "$STAGE_DIR" ] || die "STAGE_DIR '$STAGE_DIR' is not a directory"
|
[ -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)"
|
[ -d "$PUBLIC_DIR" ] || die "PUBLIC_DIR '$PUBLIC_DIR' is not a directory (run from the studio root)"
|
||||||
|
|
||||||
STAGE_TREE="$STAGE_DIR/$TREE"
|
STAGE_TREE="$STAGE_DIR/$TREE"
|
||||||
STAGE_INDEX="$STAGE_DIR/index.html"
|
STAGE_README="$STAGE_DIR/README.md" # public/ holds ONLY the output folder + README.md (BUILD-OUTPUT-IS-ONE-FOLDER)
|
||||||
|
|
||||||
# --- GATE: the stage must be a COMPLETE, verified tree before we touch the live web-root ---------------------
|
# --- GATE: the stage must be a COMPLETE, verified tree before we touch the live web-root ---------------------
|
||||||
# (This is the same "verify is a GATE that aborts, never chained after the destructive op" lesson.)
|
# (This is the same "verify is a GATE that aborts, never chained after the destructive op" lesson.)
|
||||||
[ -d "$STAGE_TREE" ] || die "stage tree missing: $STAGE_TREE"
|
[ -d "$STAGE_TREE" ] || die "stage tree missing: $STAGE_TREE"
|
||||||
[ -f "$STAGE_TREE/manifest.json" ] || die "stage tree has no manifest.json — refusing to swap an incomplete build"
|
[ -f "$STAGE_TREE/manifest.json" ] || die "stage tree has no manifest.json — refusing to swap an incomplete build"
|
||||||
[ -f "$STAGE_INDEX" ] || die "stage index.html missing: $STAGE_INDEX"
|
[ -f "$STAGE_DIR/$TREE/index.html" ] || die "stage front page missing: $STAGE_DIR/$TREE/index.html"
|
||||||
|
|
||||||
# a complete tree has >=15 bundle dirs (the known catalog size floor); guard against a mid-churn/empty stage.
|
# a complete tree has >=15 bundle dirs (the known catalog size floor); guard against a mid-churn/empty stage.
|
||||||
tree_dirs=$(find "$STAGE_TREE" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
|
tree_dirs=$(find "$STAGE_TREE" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
|
||||||
|
|
@ -79,7 +79,7 @@ PY
|
||||||
)
|
)
|
||||||
[ -z "$missing" ] || die "stage bundles missing a .js: $missing — refusing to swap a broken tree"
|
[ -z "$missing" ] || die "stage bundles missing a .js: $missing — refusing to swap a broken tree"
|
||||||
|
|
||||||
echo "✅ stage verified: $tree_dirs bundle dirs + manifest.json + index.html, all bundles resolve."
|
echo "✅ stage verified: $tree_dirs bundle dirs + manifest.json + the front page, all bundles resolve."
|
||||||
|
|
||||||
# --- GUARD: stage and public must be on the SAME filesystem for rename() to be atomic ------------------------
|
# --- GUARD: stage and public must be on the SAME filesystem for rename() to be atomic ------------------------
|
||||||
stage_dev=$(stat -c '%d' "$STAGE_DIR")
|
stage_dev=$(stat -c '%d' "$STAGE_DIR")
|
||||||
|
|
@ -92,7 +92,7 @@ if [ "$DRY_RUN" = "1" ]; then
|
||||||
echo "DRY_RUN — plan (no changes):"
|
echo "DRY_RUN — plan (no changes):"
|
||||||
echo " 1. mv $PUBLIC_DIR/$TREE -> $OLD_TREE"
|
echo " 1. mv $PUBLIC_DIR/$TREE -> $OLD_TREE"
|
||||||
echo " 2. mv $STAGE_TREE -> $PUBLIC_DIR/$TREE"
|
echo " 2. mv $STAGE_TREE -> $PUBLIC_DIR/$TREE"
|
||||||
echo " 3. cp $STAGE_INDEX -> $PUBLIC_DIR/index.html"
|
echo " 3. cp $STAGE_README -> $PUBLIC_DIR/README.md (if present)"
|
||||||
echo " 4. rm -rf $OLD_TREE"
|
echo " 4. rm -rf $OLD_TREE"
|
||||||
echo " (NEVER touched: $PUBLIC_DIR/assets/ $PUBLIC_DIR/.gitignore $PUBLIC_DIR/.build.lock)"
|
echo " (NEVER touched: $PUBLIC_DIR/assets/ $PUBLIC_DIR/.gitignore $PUBLIC_DIR/.build.lock)"
|
||||||
exit 0
|
exit 0
|
||||||
|
|
@ -103,7 +103,7 @@ if [ -d "$PUBLIC_DIR/$TREE" ]; then
|
||||||
mv "$PUBLIC_DIR/$TREE" "$OLD_TREE" # 1. instant; old tree still fully valid under new name
|
mv "$PUBLIC_DIR/$TREE" "$OLD_TREE" # 1. instant; old tree still fully valid under new name
|
||||||
fi
|
fi
|
||||||
mv "$STAGE_TREE" "$PUBLIC_DIR/$TREE" # 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
|
[ -f "$STAGE_README" ] && cp "$STAGE_README" "$PUBLIC_DIR/README.md" # 3. the generated README
|
||||||
rm -rf "$OLD_TREE" # 4. drop the superseded tree
|
rm -rf "$OLD_TREE" # 4. drop the superseded tree
|
||||||
|
|
||||||
echo "🔀 atomic swap complete — $PUBLIC_DIR/$TREE 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."
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
# BUILD_LOCK_WAIT=<seconds> (default 0 = fail-fast/nonblock; set e.g. 300 to QUEUE behind a running build).
|
# BUILD_LOCK_WAIT=<seconds> (default 0 = fail-fast/nonblock; set e.g. 300 to QUEUE behind a running build).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
LOCK_FILE="${BUILD_LOCK_FILE:-public/.build.lock}"
|
# At the repo root, NOT in public/: public/ holds only the output folder + README.md (BUILD-OUTPUT-IS-ONE-FOLDER).
|
||||||
|
LOCK_FILE="${BUILD_LOCK_FILE:-.build.lock}"
|
||||||
WAIT="${BUILD_LOCK_WAIT:-0}"
|
WAIT="${BUILD_LOCK_WAIT:-0}"
|
||||||
mkdir -p "$(dirname "$LOCK_FILE")"
|
mkdir -p "$(dirname "$LOCK_FILE")"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,13 @@
|
||||||
import { existsSync, statSync, readdirSync } from 'node:fs'
|
import { existsSync, statSync, readdirSync } from 'node:fs'
|
||||||
import { resolve, dirname, join } from 'node:path'
|
import { resolve, dirname, join } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { TREE_NAME } from './_tree-name.mjs'
|
||||||
|
|
||||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||||
const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
|
const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
|
||||||
const ASSETS = resolve(REPO, 'public/assets')
|
// The published weight is the OUTPUT FOLDER's assets (public/ holds only the output folder since
|
||||||
|
// BUILD-OUTPUT-IS-ONE-FOLDER; the old web-root public/assets is gone).
|
||||||
|
const ASSETS = resolve(REPO, `public/${TREE_NAME}/assets`)
|
||||||
|
|
||||||
if (!existsSync(ASSETS)) {
|
if (!existsSync(ASSETS)) {
|
||||||
console.log(`TREE-WEIGHT: SKIP — no asset tree at ${ASSETS}`)
|
console.log(`TREE-WEIGHT: SKIP — no asset tree at ${ASSETS}`)
|
||||||
|
|
@ -74,7 +77,7 @@ const total = top.reduce((s, d) => s + d.bytes, 0)
|
||||||
const mb = (b) => (b / MB).toFixed(1)
|
const mb = (b) => (b / MB).toFixed(1)
|
||||||
|
|
||||||
console.log('TREE-WEIGHT')
|
console.log('TREE-WEIGHT')
|
||||||
console.log(` public/assets : ${mb(total)}MB (ceiling ${MAX_MB}MB)`)
|
console.log(` public/${TREE_NAME}/assets : ${mb(total)}MB (ceiling ${MAX_MB}MB)`)
|
||||||
for (const d of top.slice(0, 6)) console.log(` ${d.name.padEnd(14)} ${mb(d.bytes).padStart(6)}MB`)
|
for (const d of top.slice(0, 6)) console.log(` ${d.name.padEnd(14)} ${mb(d.bytes).padStart(6)}MB`)
|
||||||
|
|
||||||
// The posters split is the actionable part — see the header. Reported whenever it exists.
|
// The posters split is the actionable part — see the header. Reported whenever it exists.
|
||||||
|
|
@ -105,7 +108,7 @@ if (existsSync(posters)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (total > MAX_MB * MB) {
|
if (total > MAX_MB * MB) {
|
||||||
console.error(`\nTREE-WEIGHT: FAIL — public/assets is ${mb(total)}MB, over the ${MAX_MB}MB ceiling.`)
|
console.error(`\nTREE-WEIGHT: FAIL — public/${TREE_NAME}/assets is ${mb(total)}MB, over the ${MAX_MB}MB ceiling.`)
|
||||||
console.error(' A publish was already blocked once at a 74MB asset tree (Forgejo rejected the pack),')
|
console.error(' A publish was already blocked once at a 74MB asset tree (Forgejo rejected the pack),')
|
||||||
console.error(' and nothing has measured it since; it grew from 94MB to 134MB unnoticed.')
|
console.error(' and nothing has measured it since; it grew from 94MB to 134MB unnoticed.')
|
||||||
console.error(' Before deleting anything, read the split above: the city posters are the heavy half')
|
console.error(' Before deleting anything, read the split above: the city posters are the heavy half')
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* check-staging-forwards-lang — a staging page opened at ?lang=ar embeds its leaves in Arabic.
|
|
||||||
*
|
|
||||||
* THE BUG (found by gospel 2026-09-25). The tree's staging page (public/doxa-maps/index.html, built
|
|
||||||
* from internal/staging-forge.js) built each embedded leaf's URL as withProvider(withTk(href)) — the
|
|
||||||
* token and the engine rode along, the language did not. The leaf pages bake <html lang="en">, so a
|
|
||||||
* maintainer or reviewer reading the tree at ?lang=ar saw every map in English, and the leaf was
|
|
||||||
* right to do so: nothing told it otherwise.
|
|
||||||
*
|
|
||||||
* WHAT IS ASSERTED, on the served tree:
|
|
||||||
* 1. /doxa-maps/index.html?lang=ar, gospel-resources/resources-map selected: the stage iframe's src
|
|
||||||
* AND the "open ↗" link both carry lang=ar;
|
|
||||||
* 2. inside that iframe the Gospel map's mode toggle reads gospel.mode.gap in Arabic ("الفجوة",
|
|
||||||
* from library/i18n/locales/ar/gospel.json) — the language actually arrived, not just the param.
|
|
||||||
*
|
|
||||||
* USAGE node internal/check-staging-forwards-lang.mjs [--dir <tree root>] (through browser-slot.sh)
|
|
||||||
* EXIT 0 forwarded and rendered · 1 not · 75 not measured.
|
|
||||||
* NEGATIVE-TESTED 2026-09-25: on the tree built before withLang() existed it FAILS at step 1.
|
|
||||||
*/
|
|
||||||
import { chromium } from 'playwright'
|
|
||||||
import { spawn } from 'node:child_process'
|
|
||||||
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)
|
|
||||||
const arg = (k, d) => { const i = argv.indexOf(k); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
|
|
||||||
const DIR = resolve(arg('--dir', resolve(ROOT, 'public')))
|
|
||||||
const KEY = 'gospel-resources/resources-map'
|
|
||||||
const GAP_AR = JSON.parse(readFileSync(resolve(ROOT, 'library/i18n/locales/ar/gospel.json'), 'utf8')).mode.gap
|
|
||||||
|
|
||||||
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' })
|
|
||||||
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}/${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}/${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()
|
|
||||||
await page.waitForSelector('#stage iframe', { timeout: 15000 }).catch(() => {})
|
|
||||||
const src = await page.locator('#stage iframe').first().getAttribute('src').catch(() => null)
|
|
||||||
const pop = await page.locator('#stage a.popout').first().getAttribute('href').catch(() => null)
|
|
||||||
if (!src) await done(75, 'STAGING-LANG: NOT MEASURED — the stage never rendered an iframe')
|
|
||||||
const carries = (u) => new URL(u, 'http://x/').searchParams.get('lang') === 'ar'
|
|
||||||
console.log(` iframe src ${carries(src) ? 'carries lang=ar' : 'NO lang'} ${src}`)
|
|
||||||
console.log(` open ↗ ${pop && carries(pop) ? 'carries lang=ar' : 'NO lang'} ${pop}`)
|
|
||||||
if (!carries(src) || !pop || !carries(pop)) await done(1, 'STAGING-LANG: FAIL — the staging page does not forward ?lang= to the leaf it embeds.')
|
|
||||||
|
|
||||||
const frame = page.frameLocator('#stage iframe')
|
|
||||||
let label = null
|
|
||||||
for (let i = 0; i < 60 && label === null; i++) {
|
|
||||||
await page.waitForTimeout(500)
|
|
||||||
label = await frame.locator('body').evaluate(() => {
|
|
||||||
// the toggle lives inside the element's shadow root (possibly nested) — walk every root
|
|
||||||
const f = (r) => {
|
|
||||||
const b = [...r.querySelectorAll('button.gm-mode')]
|
|
||||||
if (b.length) return b.map((x) => x.textContent.trim())
|
|
||||||
for (const e of r.querySelectorAll('*')) if (e.shadowRoot) { const m = f(e.shadowRoot); if (m) return m }
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return f(document)
|
|
||||||
}).catch(() => null)
|
|
||||||
}
|
|
||||||
if (!label) await done(75, 'STAGING-LANG: NOT MEASURED — the Gospel map\'s mode toggle never rendered in the iframe')
|
|
||||||
console.log(` mode toggle ${JSON.stringify(label)} (expect "${GAP_AR}")`)
|
|
||||||
if (!label.includes(GAP_AR)) await done(1, `STAGING-LANG: FAIL — lang=ar reached the iframe but the Gospel map did not render gospel.mode.gap in Arabic.`)
|
|
||||||
await done(0, 'STAGING-LANG: PASS — ?lang=ar is forwarded to the embedded leaf and the Gospel map renders in Arabic.')
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* otherwise renaming the tree would leave a stale folder behind. Runs BEFORE the
|
* otherwise renaming the tree would leave a stale folder behind. Runs BEFORE the
|
||||||
* per-bundle vite builds (which emit into public/<name>/); generate-tree.mjs runs after.
|
* per-bundle vite builds (which emit into public/<name>/); generate-tree.mjs runs after.
|
||||||
*/
|
*/
|
||||||
import { readFileSync, rmSync, mkdirSync } from 'node:fs'
|
import { readFileSync, rmSync, mkdirSync, existsSync } from 'node:fs'
|
||||||
import { dirname, resolve } from 'node:path'
|
import { dirname, resolve } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { TREE_NAME } from './_tree-name.mjs'
|
import { TREE_NAME } from './_tree-name.mjs'
|
||||||
|
|
@ -36,8 +36,7 @@ const { name, paths } = config()
|
||||||
// LESSON: a clean step must clear EVERY output destination the build writes to.
|
// LESSON: a clean step must clear EVERY output destination the build writes to.
|
||||||
// Cleaning only the canonical outDir is a silent, compounding leak.
|
// Cleaning only the canonical outDir is a silent, compounding leak.
|
||||||
// The build writes into public/<name>/ directly (public/ is the committed served
|
// The build writes into public/<name>/ directly (public/ is the committed served
|
||||||
// web-root). Only that ONE subfolder is cleaned — never public/ itself, which also
|
// web-root). That ONE subfolder is reset — never public/ itself (write-token's public/api/ lives there).
|
||||||
// holds the shell and the pre-generated posters that are committed source.
|
|
||||||
const roots = [
|
const roots = [
|
||||||
resolve(__dirname, '..', 'public', name),
|
resolve(__dirname, '..', 'public', name),
|
||||||
...paths.map(p => resolve(__dirname, '..', p, name)),
|
...paths.map(p => resolve(__dirname, '..', p, name)),
|
||||||
|
|
@ -48,3 +47,13 @@ for (const root of roots) {
|
||||||
mkdirSync(root, { recursive: true })
|
mkdirSync(root, { recursive: true })
|
||||||
console.log(`[clean] reset ${root}`)
|
console.log(`[clean] reset ${root}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ONE FOLDER (Driver decision, card BUILD-OUTPUT-IS-ONE-FOLDER, 2026-09-25): public/ now holds only the
|
||||||
|
// output folder + a generated README.md. These are the entries the OLD layout wrote beside the tree;
|
||||||
|
// removing them here lets a checkout that still has them converge on the new layout at its next build
|
||||||
|
// instead of carrying them forever. (public/api/ is left alone: write-token owns it, and only when a
|
||||||
|
// MAPBOX_TOKEN is supplied.)
|
||||||
|
for (const legacy of ['index.html', 'assets', '__gallery']) {
|
||||||
|
const p = resolve(__dirname, '..', 'public', legacy)
|
||||||
|
if (existsSync(p)) { rmSync(p, { recursive: true, force: true }); console.log(`[clean] removed public/${legacy} (no longer part of the output)`) }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -676,10 +676,24 @@ function main() {
|
||||||
const nProfiles = baked.reduce((n, b) => n + b.p.length, 0)
|
const nProfiles = baked.reduce((n, b) => n + b.p.length, 0)
|
||||||
const nInstances = baked.reduce((n, b) => n + b.p.reduce((k, p) => k + (p.in ? p.in.length : 0), 0), 0)
|
const nInstances = baked.reduce((n, b) => n + b.p.reduce((k, p) => k + (p.in ? p.in.length : 0), 0), 0)
|
||||||
const nOverrides = baked.reduce((n, b) => n + b.p.reduce((k, p) => k + (p.em ? 1 : 0) + (p.in || []).filter(x => x.em).length, 0), 0)
|
const nOverrides = baked.reduce((n, b) => n + b.p.reduce((k, p) => k + (p.em ? 1 : 0) + (p.in || []).filter(x => x.em).length, 0), 0)
|
||||||
const dir = join(ROOT, 'public', '__gallery')
|
// ARCHIVED OUT OF public/ (Driver decision, card BUILD-OUTPUT-IS-ONE-FOLDER): public/ holds only the
|
||||||
|
// output folder + README.md, and the studio at <tree>/index.html is the front page. The last
|
||||||
|
// generated copy is kept in the umbrella's _qa-cycle/archive/. This still renders on request, into a
|
||||||
|
// directory you name — never into public/.
|
||||||
|
const oi = process.argv.indexOf('--out')
|
||||||
|
const outDir = oi >= 0 && process.argv[oi + 1] ? resolve(process.argv[oi + 1]) : null
|
||||||
|
if (!outDir) {
|
||||||
|
console.log('[generate-gallery] ARCHIVED — not written. The gallery is no longer part of the build or of public/;')
|
||||||
|
console.log(' the Plugin Studio (<tree>/index.html) is the front page. To render it anyway: bun run gallery -- --out <dir>')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (outDir === resolve(ROOT, 'public') || outDir.startsWith(resolve(ROOT, 'public') + '/')) {
|
||||||
|
console.error('[generate-gallery] refusing to write into public/ — it holds only the output folder.'); process.exit(1)
|
||||||
|
}
|
||||||
|
const dir = outDir
|
||||||
mkdirSync(dir, { recursive: true })
|
mkdirSync(dir, { recursive: true })
|
||||||
writeFileSync(join(dir, 'index.html'), html)
|
writeFileSync(join(dir, 'index.html'), html)
|
||||||
console.log(`[generate-gallery] public/__gallery/index.html (ARCHIVED; \`/\` is the studio) — ${(html.length / 1024).toFixed(1)} KB · ${baked.length} bundle(s) · ${nProfiles} profile(s) · ${nInstances} instance(s) · ${nOverrides} embed snippet(s) baked verbatim (the rest re-derive)`)
|
console.log(`[generate-gallery] ${dir}/index.html (ARCHIVED rendering) — ${(html.length / 1024).toFixed(1)} KB · ${baked.length} bundle(s) · ${nProfiles} profile(s) · ${nInstances} instance(s) · ${nOverrides} embed snippet(s) baked verbatim (the rest re-derive)`)
|
||||||
}
|
}
|
||||||
// Runs only as a script; importing it (the dev plugin does) has no side effects.
|
// Runs only as a script; importing it (the dev plugin does) has no side effects.
|
||||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main()
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main()
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,9 @@
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, statSync,
|
readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, statSync,
|
||||||
cpSync, rmSync,
|
cpSync, rmSync, mkdtempSync,
|
||||||
} from 'node:fs'
|
} from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
import { dirname, resolve, join } from 'node:path'
|
import { dirname, resolve, join } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
|
|
@ -43,6 +44,7 @@ import { createHash } from 'node:crypto'
|
||||||
// consumer (plugin-studio) mis-tiered every profile as cap-1 (single). Single source of truth.
|
// consumer (plugin-studio) mis-tiered every profile as cap-1 (single). Single source of truth.
|
||||||
import { detectProfileType } from './plugin-gallery/discover.mjs'
|
import { detectProfileType } from './plugin-gallery/discover.mjs'
|
||||||
import { TREE_NAME as CONFIGURED_TREE_NAME } from './_tree-name.mjs'
|
import { TREE_NAME as CONFIGURED_TREE_NAME } from './_tree-name.mjs'
|
||||||
|
import { syncAppShells } from './sync-app-shells.mjs'
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
const APP_PROFILES = resolve(__dirname, '..', 'plugin-designs')
|
const APP_PROFILES = resolve(__dirname, '..', 'plugin-designs')
|
||||||
|
|
@ -692,9 +694,12 @@ function rootHtml(bundles, manifest) {
|
||||||
* "the plugin works"; a mounted element with no data is the failure that looks like success.
|
* "the plugin works"; a mounted element with no data is the failure that looks like success.
|
||||||
*/
|
*/
|
||||||
function studioRootHtml(bakedToken) {
|
function studioRootHtml(bakedToken) {
|
||||||
// Relative, so the served root stays position-independent (drop-portability): the same file
|
// THE TREE'S FRONT PAGE (<tree>/index.html) — Driver decision, card
|
||||||
// works at a domain root, under a foreign subpath, and from file://.
|
// BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE: the output is ONE folder, and opening it opens the
|
||||||
const src = `./${TREE_NAME}/plugin-studio/plugin-studio.js`
|
// studio. It used to sit one level ABOVE the tree (public/index.html) with the staging-forge chooser
|
||||||
|
// at <tree>/index.html; both are replaced by this. Relative paths, so the folder stays
|
||||||
|
// position-independent (drop-portability): a domain root, a foreign subpath, or file://.
|
||||||
|
const src = `./plugin-studio/plugin-studio.js`
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -708,7 +713,7 @@ ${bakedToken ? ` <script>window.MAP_TOKEN=${JSON.stringify(bakedToken)};</scrip
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<plugin-studio tree-base="./${TREE_NAME}"></plugin-studio>
|
<plugin-studio tree-base="."></plugin-studio>
|
||||||
<script src="${src}"></script>
|
<script src="${src}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1359,16 +1364,10 @@ function writeManifestAndHtml(root, bundles, tokenUrlExpr, bakedToken) {
|
||||||
if (existsSync(join(root, 'doxa-maps-spa'))) {
|
if (existsSync(join(root, 'doxa-maps-spa'))) {
|
||||||
writeFileSync(join(root, 'doxa-maps-spa', 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n')
|
writeFileSync(join(root, 'doxa-maps-spa', 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n')
|
||||||
}
|
}
|
||||||
writeFileSync(join(root, 'index.html'), rootHtml(bundles, manifest))
|
// THE FRONT PAGE IS THE STUDIO (see studioRootHtml). The staging-forge chooser that used to be
|
||||||
|
// written here (rootHtml) is retired with it; nothing is written above the tree any more.
|
||||||
|
writeFileSync(join(root, 'index.html'), studioRootHtml(bakedToken))
|
||||||
|
|
||||||
// ── THE WEB-ROOT PAGE = THE STUDIO ──────────────────────────────────────────────────────────
|
|
||||||
// One level ABOVE the tree (public/index.html, beside public/<tree>/ and public/assets/), which
|
|
||||||
// is what `bun run serve` serves at `/`. Written here rather than by generate-gallery.mjs
|
|
||||||
// because the studio host page is part of the TREE's contract: it names the tree folder and
|
|
||||||
// loads a bundle out of it, so it belongs with the code that knows the tree's shape.
|
|
||||||
// Previously this slot held the 57 KB staging gallery; that page is now archived at
|
|
||||||
// public/__gallery/index.html by generate-gallery.mjs.
|
|
||||||
writeFileSync(join(root, '..', 'index.html'), studioRootHtml(bakedToken))
|
|
||||||
|
|
||||||
bundles.forEach((b) => {
|
bundles.forEach((b) => {
|
||||||
const bundleDir = join(root, b.bundle)
|
const bundleDir = join(root, b.bundle)
|
||||||
|
|
@ -1403,6 +1402,25 @@ function writeManifestAndHtml(root, bundles, tokenUrlExpr, bakedToken) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publicReadme(bundles) {
|
||||||
|
const profiles = bundles.reduce((n, b) => n + b.profiles.length, 0)
|
||||||
|
return `# public/ — the built output
|
||||||
|
|
||||||
|
This folder is GENERATED by \`bun run build\`; do not edit it by hand. It holds exactly one thing:
|
||||||
|
|
||||||
|
- **\`${TREE_NAME}/\`** — the output folder: ${bundles.length} plugin bundles and ${profiles} pages, self-contained.
|
||||||
|
Copy this one folder to any web host (or open it from disk) and it works.
|
||||||
|
- \`${TREE_NAME}/index.html\` — the front page: the Plugin Studio, every plugin in one place.
|
||||||
|
- \`${TREE_NAME}/<plugin>/<plugin>.js\` — a plugin's bundle (a custom element; load it with a \`<script>\` tag).
|
||||||
|
- \`${TREE_NAME}/<plugin>/<page>/index.html\` — a ready-made page for one plugin view (embed it in an \`<iframe>\`).
|
||||||
|
- \`${TREE_NAME}/manifest.json\` — every plugin, page and embed snippet, machine-readable.
|
||||||
|
- \`${TREE_NAME}/assets/\` — the data, fonts and images the plugins fetch at runtime.
|
||||||
|
|
||||||
|
The folder's name comes from \`name\` in \`vite.doxa-maps-build-config.json\`. Sources live in
|
||||||
|
\`plugin-designs/\`, \`library/\` and \`data/\`; \`rm -rf public/ && bun run build\` recreates all of this.
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const bundles = scanBundles()
|
const bundles = scanBundles()
|
||||||
// SUBRESOURCE INTEGRITY is computed inside scanBundles() (each bundle record carries
|
// SUBRESOURCE INTEGRITY is computed inside scanBundles() (each bundle record carries
|
||||||
|
|
@ -1426,7 +1444,13 @@ function main() {
|
||||||
// They must be COPIED here or a clean build drops them and the wall falls back
|
// They must be COPIED here or a clean build drops them and the wall falls back
|
||||||
// to text placeholders while the map fails to resolve its boundaries.
|
// to text placeholders while the map fails to resolve its boundaries.
|
||||||
{
|
{
|
||||||
const pubAssets = resolve(__dirname, '..', 'public', 'assets')
|
// STAGED IN A TEMP DIRECTORY, NOT public/assets (Driver decision, card BUILD-OUTPUT-IS-ONE-FOLDER:
|
||||||
|
// public/ holds only the output folder + README.md). This step still gathers the sources
|
||||||
|
// (data/, plugin previews, the upg-100 boundaries) into one pool and copies the fetched
|
||||||
|
// allowlist into <tree>/assets below; the pool itself is a build intermediate. The web-root
|
||||||
|
// sibling it used to be was also a trap: it made a sibling-relative asset path resolve locally
|
||||||
|
// while it 404'd live (library/utils/embedBase.js records that incident).
|
||||||
|
const pubAssets = mkdtempSync(join(tmpdir(), `${TREE_NAME}-assets-`))
|
||||||
// EVERY profile's previews, not just the 100-list's. This was hardcoded to
|
// EVERY profile's previews, not just the 100-list's. This was hardcoded to
|
||||||
// my-upg-100-list, so country-posters shipped with zero thumbnails and its
|
// my-upg-100-list, so country-posters shipped with zero thumbnails and its
|
||||||
// whole wall fell back to "poster not generated yet" placeholders even though
|
// whole wall fell back to "poster not generated yet" placeholders even though
|
||||||
|
|
@ -1445,7 +1469,7 @@ function main() {
|
||||||
cpSync(src, d, { recursive: true })
|
cpSync(src, d, { recursive: true })
|
||||||
copied += readdirSync(src).length
|
copied += readdirSync(src).length
|
||||||
}
|
}
|
||||||
if (copied) console.log(`[generate-tree] copied ${copied} poster previews → public/assets/previews`)
|
if (copied) console.log(`[generate-tree] gathered ${copied} poster previews`)
|
||||||
}
|
}
|
||||||
const bDir = join(pubAssets, 'boundaries')
|
const bDir = join(pubAssets, 'boundaries')
|
||||||
mkdirSync(bDir, { recursive: true })
|
mkdirSync(bDir, { recursive: true })
|
||||||
|
|
@ -1455,7 +1479,7 @@ function main() {
|
||||||
[join(dataDir, 'gb-IND-ADM1.geojson'), 'gb-IND-ADM1.geojson'],
|
[join(dataDir, 'gb-IND-ADM1.geojson'), 'gb-IND-ADM1.geojson'],
|
||||||
[join(dataDir, 'gb-CHN-ADM1.geojson'), 'gb-CHN-ADM1.geojson'],
|
[join(dataDir, 'gb-CHN-ADM1.geojson'), 'gb-CHN-ADM1.geojson'],
|
||||||
]) if (existsSync(from)) cpSync(from, join(bDir, to))
|
]) if (existsSync(from)) cpSync(from, join(bDir, to))
|
||||||
console.log('[generate-tree] copied boundary geojson → public/assets/boundaries')
|
console.log('[generate-tree] gathered boundary geojson')
|
||||||
|
|
||||||
// DATA LIVES IN data/, THE WEB-ROOT IS GENERATED (the D1 reorganization):
|
// DATA LIVES IN data/, THE WEB-ROOT IS GENERATED (the D1 reorganization):
|
||||||
// source-of-truth folders that used to sit ONLY inside public/ now live in
|
// source-of-truth folders that used to sit ONLY inside public/ now live in
|
||||||
|
|
@ -1475,6 +1499,10 @@ function main() {
|
||||||
['tables', join(pubAssets, 'tables')],
|
['tables', join(pubAssets, 'tables')],
|
||||||
// pplr — the PPLR data maps' derived datasets (plugin-designs/pplr-data-maps/scripts/build-data.mjs)
|
// pplr — the PPLR data maps' derived datasets (plugin-designs/pplr-data-maps/scripts/build-data.mjs)
|
||||||
['pplr', join(pubAssets, 'pplr')],
|
['pplr', join(pubAssets, 'pplr')],
|
||||||
|
// fonts/ + images/ — vendored faces and images the leaves fetch (see the staging notes
|
||||||
|
// below). They used to live ONLY in public/assets; they are sources, so they live in data/.
|
||||||
|
['fonts', join(pubAssets, 'fonts')],
|
||||||
|
['images', join(pubAssets, 'images')],
|
||||||
]) {
|
]) {
|
||||||
const src = join(repoData, srcName)
|
const src = join(repoData, srcName)
|
||||||
if (!existsSync(src)) continue
|
if (!existsSync(src)) continue
|
||||||
|
|
@ -1482,7 +1510,7 @@ function main() {
|
||||||
mkdirSync(destRel, { recursive: true })
|
mkdirSync(destRel, { recursive: true })
|
||||||
cpSync(src, destRel, { recursive: true })
|
cpSync(src, destRel, { recursive: true })
|
||||||
}
|
}
|
||||||
console.log('[generate-tree] copied data/ sources → web-root/assets (people-groups, vendor, posters, tables, pplr)')
|
console.log('[generate-tree] gathered data/ sources into the build pool (people-groups, vendor, posters, tables, pplr, fonts, images)')
|
||||||
|
|
||||||
// ── THE TREE IS THE SHIPPING UNIT: stage the fetched assets INSIDE it ──────
|
// ── THE TREE IS THE SHIPPING UNIT: stage the fetched assets INSIDE it ──────
|
||||||
// BUG-DATA-ASSETS-NOT-SHIPPED-TREE-NOT-PORTABLE (2026-09-20). The web-root copy
|
// BUG-DATA-ASSETS-NOT-SHIPPED-TREE-NOT-PORTABLE (2026-09-20). The web-root copy
|
||||||
|
|
@ -1648,8 +1676,12 @@ function main() {
|
||||||
console.log('[generate-tree] posters/ NOT bundled — the wall renders from previews; export and print need posterSources:true, bundlePosters:true, or an absolute assetsBaseUrl')
|
console.log('[generate-tree] posters/ NOT bundled — the wall renders from previews; export and print need posterSources:true, bundlePosters:true, or an absolute assetsBaseUrl')
|
||||||
}
|
}
|
||||||
console.log(`[generate-tree] staged ${staged} runtime asset(s) → ${TREE_NAME}/assets (tree is self-contained)`)
|
console.log(`[generate-tree] staged ${staged} runtime asset(s) → ${TREE_NAME}/assets (tree is self-contained)`)
|
||||||
|
rmSync(pubAssets, { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// public/README.md — the one other entry public/ holds: what this folder is, in plain words.
|
||||||
|
writeFileSync(join(DEFAULT_ROOT, '..', 'README.md'), publicReadme(bundles))
|
||||||
|
|
||||||
for (const dest of dests) {
|
for (const dest of dests) {
|
||||||
if (dest === DEFAULT_ROOT) continue
|
if (dest === DEFAULT_ROOT) continue
|
||||||
rmSync(dest, { recursive: true, force: true })
|
rmSync(dest, { recursive: true, force: true })
|
||||||
|
|
@ -1658,6 +1690,9 @@ function main() {
|
||||||
console.log(`[generate-tree] copied ${TREE_NAME}/ → ${dest}`)
|
console.log(`[generate-tree] copied ${TREE_NAME}/ → ${dest}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The native app shells load THIS folder; keep their paths in step with the tree's name.
|
||||||
|
for (const r of syncAppShells()) if (r.changed) console.log(`[generate-tree] app shell ${r.file}: ${r.notes.join('; ')}`)
|
||||||
|
|
||||||
console.log(`[generate-tree] ${bundles.length} bundle(s), ` +
|
console.log(`[generate-tree] ${bundles.length} bundle(s), ` +
|
||||||
`${bundles.reduce((n, b) => n + b.profiles.length, 0)} profile(s) → ${dests.length} destination(s)`)
|
`${bundles.reduce((n, b) => n + b.profiles.length, 0)} profile(s) → ${dests.length} destination(s)`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@ const DEV_ASSET_MIME = {
|
||||||
}
|
}
|
||||||
export function resolveDevAsset(rel, root = REPO_ROOT) {
|
export function resolveDevAsset(rel, root = REPO_ROOT) {
|
||||||
const isFile = (p) => existsSync(p) && statSync(p).isFile()
|
const isFile = (p) => existsSync(p) && statSync(p).isFile()
|
||||||
const shipped = resolve(root, 'public', 'assets', rel)
|
// The built copy lives INSIDE the output folder (public/<tree>/assets) — public/ holds nothing else
|
||||||
|
// since BUILD-OUTPUT-IS-ONE-FOLDER. The data/ fallback below keeps a fresh clone working unbuilt.
|
||||||
|
const shipped = resolve(root, 'public', TREE_NAME, 'assets', rel)
|
||||||
if (isFile(shipped)) return shipped
|
if (isFile(shipped)) return shipped
|
||||||
// data/<rel> is the TRACKED source of truth public/assets mirrors (see .gitignore): generate-tree
|
// data/<rel> is the TRACKED source of truth public/assets mirrors (see .gitignore): generate-tree
|
||||||
// writes the mirror, so on a fresh clone — which is what DX receives — public/assets holds almost
|
// writes the mirror, so on a fresh clone — which is what DX receives — public/assets holds almost
|
||||||
|
|
@ -260,7 +262,7 @@ export function autoSpaDevPlugin(token = '') {
|
||||||
try { rel = decodeURIComponent(path.slice(assetPrefix.length)) } catch { rel = '' }
|
try { rel = decodeURIComponent(path.slice(assetPrefix.length)) } catch { rel = '' }
|
||||||
if (!rel || rel.split('/').includes('..')) { res.statusCode = 400; return res.end('bad asset path') }
|
if (!rel || rel.split('/').includes('..')) { res.statusCode = 400; return res.end('bad asset path') }
|
||||||
const file = resolveDevAsset(rel)
|
const file = resolveDevAsset(rel)
|
||||||
if (!file) return json(res, { error: 'asset not found', asset: rel, looked: ['public/assets/', 'data/', 'plugin-designs/*/data/'] }, 404)
|
if (!file) return json(res, { error: 'asset not found', asset: rel, looked: [`public/${TREE_NAME}/assets/`, 'data/', 'plugin-designs/*/data/'] }, 404)
|
||||||
res.statusCode = 200
|
res.statusCode = 200
|
||||||
res.setHeader('Content-Type', DEV_ASSET_MIME[extname(file).toLowerCase()] || 'application/octet-stream')
|
res.setHeader('Content-Type', DEV_ASSET_MIME[extname(file).toLowerCase()] || 'application/octet-stream')
|
||||||
res.setHeader('Cache-Control', 'no-cache')
|
res.setHeader('Cache-Control', 'no-cache')
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,10 @@ for (const f of files) {
|
||||||
if (ext === '.map') hit(findings, 'debug', f.rel, 'source map file')
|
if (ext === '.map') hit(findings, 'debug', f.rel, 'source map file')
|
||||||
// NOTE: 'docs' here is the FINDING-CATEGORY name documented in this file's header, not a
|
// NOTE: 'docs' here is the FINDING-CATEGORY name documented in this file's header, not a
|
||||||
// folder path. It deliberately did NOT change when docs/ was renamed to documentation/.
|
// folder path. It deliberately did NOT change when docs/ was renamed to documentation/.
|
||||||
if (ext === '.md' || ext === '.markdown' || ext === '.txt') hit(findings, 'docs', f.rel, 'documentation in a served folder')
|
// The ONE exception is public/README.md: the build generates it (generate-tree publicReadme) to explain
|
||||||
|
// the folder, per the Driver's decision (BUILD-OUTPUT-IS-ONE-FOLDER: "only the output folder + a short
|
||||||
|
// README.md"). A doc anywhere else — inside the tree included — is still a finding.
|
||||||
|
if ((ext === '.md' || ext === '.markdown' || ext === '.txt') && f.rel !== 'README.md') hit(findings, 'docs', f.rel, 'documentation in a served folder')
|
||||||
const isText = TEXT_EXT.has(ext) || TEXT_EXT.has(base)
|
const isText = TEXT_EXT.has(ext) || TEXT_EXT.has(base)
|
||||||
if (!isText || f.size > 40 * 1024 * 1024) continue
|
if (!isText || f.size > 40 * 1024 * 1024) continue
|
||||||
const t = readFileSync(f.abs, 'utf8')
|
const t = readFileSync(f.abs, 'utf8')
|
||||||
|
|
@ -168,7 +171,9 @@ for (const f of files) {
|
||||||
for (const d of notes) hit(STRICT ? findings : warnings, 'meta', f.rel, d)
|
for (const d of notes) hit(STRICT ? findings : warnings, 'meta', f.rel, d)
|
||||||
}
|
}
|
||||||
// staging surfaces: <tree>/index.html and <tree>/<bundle>/index.html (leaf pages are deeper)
|
// staging surfaces: <tree>/index.html and <tree>/<bundle>/index.html (leaf pages are deeper)
|
||||||
const staging = files.filter(f => f.rel === `${TREE}/index.html` || /^[^/]+\/[^/]+\/index\.html$/.test(f.rel) && f.rel.startsWith(`${TREE}/`)).map(f => f.rel)
|
// <tree>/index.html is NOT a staging page any more: it is the Plugin Studio, the output's front page
|
||||||
|
// (Driver decision, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE). A <tree>/<bundle>/index.html chooser still would be.
|
||||||
|
const staging = files.filter(f => /^[^/]+\/[^/]+\/index\.html$/.test(f.rel) && f.rel.startsWith(`${TREE}/`)).map(f => f.rel)
|
||||||
for (const s of staging) hit(STRICT ? findings : warnings, 'staging', s, 'preview/staging page (internal surface) — exclude from a public mirror or accept knowingly')
|
for (const s of staging) hit(STRICT ? findings : warnings, 'staging', s, 'preview/staging page (internal surface) — exclude from a public mirror or accept knowingly')
|
||||||
// The drop-in contract: only the entries vite.doxa-maps-build-config.json `dropin.include` declares
|
// The drop-in contract: only the entries vite.doxa-maps-build-config.json `dropin.include` declares
|
||||||
// may sit at the root. Strict = every other root entry is a finding; otherwise a warning.
|
// may sit at the root. Strict = every other root entry is a finding; otherwise a warning.
|
||||||
|
|
@ -185,15 +190,16 @@ const CONTRACT = (() => {
|
||||||
try {
|
try {
|
||||||
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
|
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
|
||||||
const inc = cfg?.dropin?.include
|
const inc = cfg?.dropin?.include
|
||||||
if (Array.isArray(inc) && inc.length) return new Set(inc)
|
// The output folder is always part of the contract (it is the product); include lists the extras.
|
||||||
|
if (Array.isArray(inc)) return new Set([TREE, ...inc])
|
||||||
} catch { /* fall through */ }
|
} catch { /* fall through */ }
|
||||||
return new Set(['index.html', 'assets', TREE])
|
return new Set(['README.md', TREE])
|
||||||
})()
|
})()
|
||||||
const rootEntries = [...new Set(files.map(f => f.rel.split('/')[0]))].sort()
|
const rootEntries = [...new Set(files.map(f => f.rel.split('/')[0]))].sort()
|
||||||
for (const e of rootEntries) {
|
for (const e of rootEntries) {
|
||||||
if (CONTRACT.has(e)) continue
|
if (CONTRACT.has(e)) continue
|
||||||
const label = files.some(f => f.rel.startsWith(`${e}/`)) ? `${e}/` : e
|
const label = files.some(f => f.rel.startsWith(`${e}/`)) ? `${e}/` : e
|
||||||
hit(STRICT ? findings : warnings, 'contract', label, `outside the drop-in contract (index.html, assets/, ${TREE}/ only)`)
|
hit(STRICT ? findings : warnings, 'contract', label, `outside the drop-in contract (${[...CONTRACT].join(', ')} only)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// inventory
|
// inventory
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,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
|
// 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
|
// 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.
|
// finding. Anything outside the bundle tree is unaffected.
|
||||||
const TREE = join(DIR, TREE_NAME)
|
const TREE = join(DIR, TREE_NAME)
|
||||||
const MANIFEST = join(TREE, 'manifest.json')
|
const MANIFEST = join(TREE, 'manifest.json')
|
||||||
function treeIsTorn() {
|
function treeIsTorn() {
|
||||||
|
|
@ -135,4 +135,5 @@ createServer((req, res) => {
|
||||||
}).listen(PORT, HOST, () => {
|
}).listen(PORT, HOST, () => {
|
||||||
console.log(`serve-strict: http://${HOST}:${PORT} -> ${DIR.replace(ROOT + '/', '')}`)
|
console.log(`serve-strict: http://${HOST}:${PORT} -> ${DIR.replace(ROOT + '/', '')}`)
|
||||||
console.log(' directory requests return 404 (matching production). Ask for /index.html explicitly.')
|
console.log(' directory requests return 404 (matching production). Ask for /index.html explicitly.')
|
||||||
|
console.log(` front page (the Plugin Studio): http://${HOST}:${PORT}/${TREE_NAME}/index.html`)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const APP_PUBLIC = join(ROOT, 'deployment/capacitor/android/app/src/main/assets/
|
||||||
const APP_ROOT = join(APP_PUBLIC, 'index.html')
|
const APP_ROOT = join(APP_PUBLIC, 'index.html')
|
||||||
// The showcase leaf, inside the served tree.
|
// The showcase leaf, inside the served tree.
|
||||||
const TREE = TREE_NAME
|
const TREE = TREE_NAME
|
||||||
const LEAF_REL = `${TREE}/my-upg-100-list/upg-100-mfe/index.html`
|
const LEAF_REL = `my-upg-100-list/upg-100-mfe/index.html` // the app's web root IS the tree (webDir public/<tree>)
|
||||||
const LEAF = join(APP_PUBLIC, LEAF_REL)
|
const LEAF = join(APP_PUBLIC, LEAF_REL)
|
||||||
|
|
||||||
// A marker so --check can tell the app page apart from the studio page without parsing HTML.
|
// A marker so --check can tell the app page apart from the studio page without parsing HTML.
|
||||||
|
|
@ -78,7 +78,7 @@ if (CHECK) {
|
||||||
// `../my-upg-100-list.js`. At the app root that same relative path resolves ABOVE the web root.
|
// `../my-upg-100-list.js`. At the app root that same relative path resolves ABOVE the web root.
|
||||||
// Every leaf-relative reference gains the leaf's own directory prefix; absolute and protocol URLs
|
// Every leaf-relative reference gains the leaf's own directory prefix; absolute and protocol URLs
|
||||||
// (the Mapbox CDN tags) are left alone.
|
// (the Mapbox CDN tags) are left alone.
|
||||||
const LEAF_DIR = `${TREE}/my-upg-100-list/upg-100-mfe/`
|
const LEAF_DIR = `my-upg-100-list/upg-100-mfe/`
|
||||||
const html = readFileSync(LEAF, 'utf8')
|
const html = readFileSync(LEAF, 'utf8')
|
||||||
const rebased = html.replace(
|
const rebased = html.replace(
|
||||||
/\b(src|href)=(["'])(?!https?:|data:|#|\/)([^"']+)\2/g,
|
/\b(src|href)=(["'])(?!https?:|data:|#|\/)([^"']+)\2/g,
|
||||||
|
|
|
||||||
71
internal/sync-app-shells.mjs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* sync-app-shells.mjs — point the native app shells at the OUTPUT FOLDER, from the tree's name.
|
||||||
|
*
|
||||||
|
* public/ holds only the output folder (Driver decision, card BUILD-OUTPUT-IS-ONE-FOLDER), so the app
|
||||||
|
* shells load THAT folder as their web root:
|
||||||
|
* capacitor.config.json "webDir": "public/<tree>"
|
||||||
|
* `cap copy` hard-fails unless webDir holds an index.html; the tree's front page is its
|
||||||
|
* index.html (the Plugin Studio), and set-app-start-page then installs the app's start page.
|
||||||
|
* deployment/src-tauri/tauri.conf.json build.frontendDist "../../public/<tree>"
|
||||||
|
* windows[].url relative to that folder
|
||||||
|
* The window url used to carry the tree's name ("doxa-maps/doxa-maps-spa/…"), so renaming the
|
||||||
|
* tree opened a BLANK desktop window (check:tauri caught it in the zz-maps rename proof).
|
||||||
|
*
|
||||||
|
* Both files are hand-formatted JSON, so only those values are replaced in place — nothing else
|
||||||
|
* moves. Idempotent. generate-tree runs this after writing the tree, so a rename stays ONE line
|
||||||
|
* (vite.doxa-maps-build-config.json "name").
|
||||||
|
*
|
||||||
|
* USAGE node internal/sync-app-shells.mjs [--check] --check: exit 1 if a file is out of date.
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync, 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)), '..')
|
||||||
|
|
||||||
|
/** Returns [{ file, changed: bool, notes: [] }]; writes unless check. */
|
||||||
|
export function syncAppShells({ check = false } = {}) {
|
||||||
|
const out = []
|
||||||
|
// ── Capacitor ──
|
||||||
|
const cap = resolve(ROOT, 'capacitor.config.json')
|
||||||
|
if (existsSync(cap)) {
|
||||||
|
const src = readFileSync(cap, 'utf8')
|
||||||
|
const want = `public/${TREE_NAME}`
|
||||||
|
const next = src.replace(/("webDir"\s*:\s*)"[^"]*"/, `$1"${want}"`)
|
||||||
|
out.push({ file: 'capacitor.config.json', changed: next !== src, notes: [`webDir -> ${want}`] })
|
||||||
|
if (next !== src && !check) { JSON.parse(next); writeFileSync(cap, next) }
|
||||||
|
}
|
||||||
|
// ── Tauri ──
|
||||||
|
const tauri = resolve(ROOT, 'deployment', 'src-tauri', 'tauri.conf.json')
|
||||||
|
if (existsSync(tauri)) {
|
||||||
|
const src = readFileSync(tauri, 'utf8')
|
||||||
|
const conf = JSON.parse(src)
|
||||||
|
const want = `../../public/${TREE_NAME}`
|
||||||
|
let next = src.replace(/("frontendDist"\s*:\s*)"[^"]*"/, `$1"${want}"`)
|
||||||
|
const notes = [`frontendDist -> ${want}`]
|
||||||
|
// A window url that still starts with a tree folder (any name that is a sibling of the bundles, i.e.
|
||||||
|
// the first segment is not a bundle) is made relative to the tree.
|
||||||
|
for (const w of (conf.app && conf.app.windows) || []) {
|
||||||
|
if (!w.url || /^[a-z]+:/i.test(w.url)) continue
|
||||||
|
const segs = w.url.split('/')
|
||||||
|
if (segs.length > 1 && !existsSync(resolve(ROOT, 'public', TREE_NAME, segs[0])) && existsSync(resolve(ROOT, 'public', TREE_NAME, ...segs.slice(1)))) {
|
||||||
|
const rel = segs.slice(1).join('/')
|
||||||
|
next = next.replace(`"url": "${w.url}"`, `"url": "${rel}"`)
|
||||||
|
notes.push(`window url ${w.url} -> ${rel}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push({ file: 'deployment/src-tauri/tauri.conf.json', changed: next !== src, notes })
|
||||||
|
if (next !== src && !check) { JSON.parse(next); writeFileSync(tauri, next) }
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
|
const check = process.argv.includes('--check')
|
||||||
|
const res = syncAppShells({ check })
|
||||||
|
for (const r of res) console.log(` ${r.changed ? (check ? 'STALE' : 'wrote') : 'ok '} ${r.file} (${r.notes.join('; ')})`)
|
||||||
|
if (check && res.some((r) => r.changed)) { console.log('APP-SHELLS: FAIL — run node internal/sync-app-shells.mjs (generate-tree does it on every build).'); process.exit(1) }
|
||||||
|
console.log(`APP-SHELLS: ${check ? 'PASS — both shells load' : 'synced to'} public/${TREE_NAME}/`)
|
||||||
|
}
|
||||||
|
|
@ -102,11 +102,13 @@ if (args.includes('--only')) {
|
||||||
try { cfg = JSON.parse(readFileSync(CONFIG, 'utf8')) } catch (e) { console.error(`[sync-dropin] cannot read ${CONFIG}: ${e.message}`); process.exit(1) }
|
try { cfg = JSON.parse(readFileSync(CONFIG, 'utf8')) } catch (e) { console.error(`[sync-dropin] cannot read ${CONFIG}: ${e.message}`); process.exit(1) }
|
||||||
const inc = cfg?.dropin?.include
|
const inc = cfg?.dropin?.include
|
||||||
if (!Array.isArray(inc) || !inc.length || !inc.every(s => typeof s === 'string')) { console.error(`[sync-dropin] --from-config: ${norm(relative(ROOT, CONFIG))} declares no "dropin": { "include": [ …path prefixes relative to public/… ] }`); process.exit(1) }
|
if (!Array.isArray(inc) || !inc.length || !inc.every(s => typeof s === 'string')) { console.error(`[sync-dropin] --from-config: ${norm(relative(ROOT, CONFIG))} declares no "dropin": { "include": [ …path prefixes relative to public/… ] }`); process.exit(1) }
|
||||||
ALLOW = inc.map(normPrefix).filter(Boolean)
|
// The OUTPUT FOLDER is always shipped: it is the product, and naming it here would put the tree's
|
||||||
|
// name in a second place in the config (a rename must stay one line). include lists the EXTRAS.
|
||||||
|
ALLOW = [TREE, ...inc.map((s) => s.replaceAll('{tree}', TREE))].map(normPrefix).filter(Boolean)
|
||||||
ALLOW_FROM = `${norm(relative(ROOT, CONFIG))} · dropin.include`
|
ALLOW_FROM = `${norm(relative(ROOT, CONFIG))} · dropin.include`
|
||||||
const exc = cfg?.dropin?.exclude
|
const exc = cfg?.dropin?.exclude
|
||||||
if (exc !== undefined && (!Array.isArray(exc) || !exc.every(s => typeof s === 'string'))) { console.error(`[sync-dropin] --from-config: "dropin.exclude" must be an array of path prefixes relative to public/`); process.exit(1) }
|
if (exc !== undefined && (!Array.isArray(exc) || !exc.every(s => typeof s === 'string'))) { console.error(`[sync-dropin] --from-config: "dropin.exclude" must be an array of path prefixes relative to public/`); process.exit(1) }
|
||||||
NOT_SHIPPED = (exc || []).map(normPrefix).filter(Boolean)
|
NOT_SHIPPED = (exc || []).map((s) => s.replaceAll('{tree}', TREE)).map(normPrefix).filter(Boolean)
|
||||||
if (NOT_SHIPPED.some(p => p.split('/').includes('..'))) { console.error(`[sync-dropin] dropin.exclude climbs out of ${norm(relative(ROOT, FROM))}/: ${JSON.stringify(NOT_SHIPPED)}`); process.exit(1) }
|
if (NOT_SHIPPED.some(p => p.split('/').includes('..'))) { console.error(`[sync-dropin] dropin.exclude climbs out of ${norm(relative(ROOT, FROM))}/: ${JSON.stringify(NOT_SHIPPED)}`); process.exit(1) }
|
||||||
}
|
}
|
||||||
if (ALLOW && (!ALLOW.length || ALLOW.some(p => p.split('/').includes('..')))) { console.error(`[sync-dropin] allowlist is empty or climbs out of ${norm(relative(ROOT, FROM))}/: ${JSON.stringify(ALLOW)}`); process.exit(1) }
|
if (ALLOW && (!ALLOW.length || ALLOW.some(p => p.split('/').includes('..')))) { console.error(`[sync-dropin] allowlist is empty or climbs out of ${norm(relative(ROOT, FROM))}/: ${JSON.stringify(ALLOW)}`); process.exit(1) }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ the app ships.
|
||||||
|
|
||||||
python3 make-previews.py
|
python3 make-previews.py
|
||||||
-> plugin-designs/my-upg-100-list/assets/previews/<zone>.webp 560px card thumbnails
|
-> plugin-designs/my-upg-100-list/assets/previews/<zone>.webp 560px card thumbnails
|
||||||
-> public/assets/posters/<zone>.webp 1500px the download
|
-> data/posters/<zone>.webp 1500px the download
|
||||||
|
|
||||||
WHY TWO SIZES
|
WHY TWO SIZES
|
||||||
The wall card shows a small thumbnail; tapping "Poster Map" must hand the user
|
The wall card shows a small thumbnail; tapping "Poster Map" must hand the user
|
||||||
|
|
@ -37,7 +37,7 @@ REPO = os.path.abspath(os.path.join(HERE, '..', '..'))
|
||||||
PROFILE = os.environ.get('PROFILE', 'my-upg-100-list')
|
PROFILE = os.environ.get('PROFILE', 'my-upg-100-list')
|
||||||
ONLY = os.environ.get('ONLY_PREFIX', '')
|
ONLY = os.environ.get('ONLY_PREFIX', '')
|
||||||
THUMB = os.path.join(REPO, 'plugin-designs', PROFILE, 'assets', 'previews')
|
THUMB = os.path.join(REPO, 'plugin-designs', PROFILE, 'assets', 'previews')
|
||||||
FULL = os.path.join(REPO, 'public', 'assets', 'posters')
|
FULL = os.path.join(REPO, 'data', 'posters') # a SOURCE: the build stages it into <tree>/assets/posters on request
|
||||||
|
|
||||||
os.makedirs(THUMB, exist_ok=True)
|
os.makedirs(THUMB, exist_ok=True)
|
||||||
os.makedirs(FULL, exist_ok=True)
|
os.makedirs(FULL, exist_ok=True)
|
||||||
|
|
@ -63,4 +63,4 @@ for p in pngs:
|
||||||
|
|
||||||
print(f'[make-previews] {len(pngs)} posters')
|
print(f'[make-previews] {len(pngs)} posters')
|
||||||
print(f' thumbnails -> plugin-designs/.../assets/previews {tkb/1024:5.1f} MB (inlined in the bundle)')
|
print(f' thumbnails -> plugin-designs/.../assets/previews {tkb/1024:5.1f} MB (inlined in the bundle)')
|
||||||
print(f' full size -> public/assets/posters {fkb/1024:5.1f} MB (served + shipped in the APK)')
|
print(f' full size -> data/posters {fkb/1024:5.1f} MB (served + shipped in the APK)')
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ const SCRIPT_FONT_FILE = {
|
||||||
let fontData = null;
|
let fontData = null;
|
||||||
const fontRel = SCRIPT_FONT_FILE[LANG];
|
const fontRel = SCRIPT_FONT_FILE[LANG];
|
||||||
if (fontRel) {
|
if (fontRel) {
|
||||||
const fp = join(__dirname, '..', '..', '..', 'public', 'assets', 'fonts', fontRel);
|
const fp = join(__dirname, '..', '..', '..', 'data', 'fonts', fontRel); // fonts are sources in data/ (BUILD-OUTPUT-IS-ONE-FOLDER)
|
||||||
if (existsSync(fp)) {
|
if (existsSync(fp)) {
|
||||||
fontData = readFileSync(fp).toString('base64');
|
fontData = readFileSync(fp).toString('base64');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import { dirname, join, resolve } from 'node:path';
|
||||||
import { TREE_NAME } from '../../../internal/_tree-name.mjs'
|
import { TREE_NAME } from '../../../internal/_tree-name.mjs'
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const ASSETS = resolve(__dirname, '..', '..', '..', 'public', 'assets');
|
const ASSETS = resolve(__dirname, '..', '..', '..', 'public', TREE_NAME, 'assets'); // the served assets live inside the output folder
|
||||||
|
|
||||||
const BANDS = {
|
const BANDS = {
|
||||||
gospel: { critical: '#b42318', high: '#e07a2b', partial: '#e8c15a', covered: '#10b981' },
|
gospel: { critical: '#b42318', high: '#e07a2b', partial: '#e8c15a', covered: '#10b981' },
|
||||||
|
|
|
||||||
10
package.json
|
|
@ -12,7 +12,7 @@
|
||||||
"build": "bash internal/build-lock.sh",
|
"build": "bash internal/build-lock.sh",
|
||||||
"build:bundle": "bun x vite build",
|
"build:bundle": "bun x vite build",
|
||||||
"build:bundles": "node internal/run-parallel.mjs --bundles",
|
"build:bundles": "node internal/run-parallel.mjs --bundles",
|
||||||
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery",
|
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post",
|
||||||
"build:watch": "bun x vite build --watch",
|
"build:watch": "bun x vite build --watch",
|
||||||
"cap:sync": "./node_modules/.bin/cap sync android",
|
"cap:sync": "./node_modules/.bin/cap sync android",
|
||||||
"cap:verify": "test -f deployment/capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)",
|
"cap:verify": "test -f deployment/capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)",
|
||||||
|
|
@ -149,8 +149,7 @@
|
||||||
"check:snapshot-guard:poison": "node internal/check-snapshot-guard.mjs --poison",
|
"check:snapshot-guard:poison": "node internal/check-snapshot-guard.mjs --poison",
|
||||||
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs",
|
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs",
|
||||||
"check:sri": "node internal/sri-consistency-check.mjs",
|
"check:sri": "node internal/sri-consistency-check.mjs",
|
||||||
"check:staging-lang": "node internal/check-staging-forwards-lang.mjs",
|
"check:stale-sweep-claim": "node internal/check-no-stale-sweep-claim.mjs",
|
||||||
"check:stale-sweep-claim": "node internal/check-no-stale-sweep-claim.mjs # a caveat that has stopped being true is a wrong number. The gospel surfaces carry a paragraph saying which pools were searched; while the GRN sweep ran they truthfully said part of it was unread, the sweep finished, and the sentence stayed for four more commits. The DATA decides here: undecided gap codes mean the claim is allowed, all-decided means it must go, so it re-arms itself for the next slow pool. Does NOT flag a claim rendered under a v-if on the pending count — that form expires on its own. Self-test: `the check:stale-sweep-claim:poison entry`. Static, sub-second.",
|
|
||||||
"check:studio-all": "node internal/check-studio-every-plugin.mjs",
|
"check:studio-all": "node internal/check-studio-every-plugin.mjs",
|
||||||
"check:surfaces-read-the-tier": "node internal/check-surfaces-read-the-tier.mjs",
|
"check:surfaces-read-the-tier": "node internal/check-surfaces-read-the-tier.mjs",
|
||||||
"check:surfaces-read-the-tier:poison": "node internal/check-surfaces-read-the-tier.mjs --poison",
|
"check:surfaces-read-the-tier:poison": "node internal/check-surfaces-read-the-tier.mjs --poison",
|
||||||
|
|
@ -173,8 +172,8 @@
|
||||||
"dev": "bun x vite",
|
"dev": "bun x vite",
|
||||||
"fix:sri": "node internal/fix-sri.mjs",
|
"fix:sri": "node internal/fix-sri.mjs",
|
||||||
"gallery": "node internal/generate-gallery.mjs",
|
"gallery": "node internal/generate-gallery.mjs",
|
||||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary",
|
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed",
|
||||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:wp-embed check:doc-scripts",
|
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts",
|
||||||
"library-index": "node internal/generate-library-index.mjs",
|
"library-index": "node internal/generate-library-index.mjs",
|
||||||
"packet-fixture": "node internal/generate-packet-fixture.mjs",
|
"packet-fixture": "node internal/generate-packet-fixture.mjs",
|
||||||
"packet:ci": "node internal/build-packet-fixtures.mjs",
|
"packet:ci": "node internal/build-packet-fixtures.mjs",
|
||||||
|
|
@ -301,7 +300,6 @@
|
||||||
"check:shots": "a README screenshot that predates the bundle it shows is a picture telling a lie — and it looks IDENTICAL to a current one. Compares each shot's PNG against the BUILT BUNDLE it renders (not raw sources: an unbuilt edit is not yet in any leaf). Fix: node internal/readme-shots.mjs",
|
"check:shots": "a README screenshot that predates the bundle it shows is a picture telling a lie — and it looks IDENTICAL to a current one. Compares each shot's PNG against the BUILT BUNDLE it renders (not raw sources: an unbuilt edit is not yet in any leaf). Fix: node internal/readme-shots.mjs",
|
||||||
"check:snapshot-guard": "every gospel-resources catalogue fetcher must write through scripts/_snapshot-guard.mjs, which refuses a write that loses more than a tenth of the previous snapshot. Written after a rate-limited FCBH run silently replaced 1,666 video scopes with 1,103. Static, sub-second. Self-test: `bun run check:snapshot-guard:poison` injects a raw write and the check must fail on it.",
|
"check:snapshot-guard": "every gospel-resources catalogue fetcher must write through scripts/_snapshot-guard.mjs, which refuses a write that loses more than a tenth of the previous snapshot. Written after a rate-limited FCBH run silently replaced 1,666 video scopes with 1,103. Static, sub-second. Self-test: `bun run check:snapshot-guard:poison` injects a raw write and the check must fail on it.",
|
||||||
"check:spa-tab-order": "the cap-5 SPA must open on Big Picture and keep the Driver's order (Big Picture, UPG-100, Prayer Progress, Gospel Resources) — asserted as PRESENCE + tab-1 + RELATIVE order on the RENDERED rail, because the rail gained three sections and now renders 12 rows with others between them. ⚠ NOT WAVE-WIRED: it FAILS rather than skips when the SPA is unreachable, deliberately (\"a gate that cannot reach its subject has not passed, it has not run\") — correct standalone, but gates:post runs on checkouts with no server. Needs `bun run serve` on 4173.",
|
"check:spa-tab-order": "the cap-5 SPA must open on Big Picture and keep the Driver's order (Big Picture, UPG-100, Prayer Progress, Gospel Resources) — asserted as PRESENCE + tab-1 + RELATIVE order on the RENDERED rail, because the rail gained three sections and now renders 12 rows with others between them. ⚠ NOT WAVE-WIRED: it FAILS rather than skips when the SPA is unreachable, deliberately (\"a gate that cannot reach its subject has not passed, it has not run\") — correct standalone, but gates:post runs on checkouts with no server. Needs `bun run serve` on 4173.",
|
||||||
"check:staging-lang": "the tree's staging page (public/doxa-maps/index.html, from internal/staging-forge.js) opened at ?lang=ar forwards lang=ar to the leaf it embeds (iframe src AND the open link), and the Gospel map inside renders gospel.mode.gap in Arabic. Negative-tested: the tree built before withLang() FAILS. --dir <tree>. Through browser-slot.sh.",
|
|
||||||
"check:stale-sweep-claim": "a caveat that has stopped being true is a wrong number. The gospel surfaces carry a paragraph saying which pools were searched; while the GRN sweep ran they truthfully said part of it was unread, the sweep finished, and the sentence stayed for four more commits. The DATA decides here: undecided gap codes mean the claim is allowed, all-decided means it must go, so it re-arms itself for the next slow pool. Does NOT flag a claim rendered under a v-if on the pending count — that form expires on its own. Self-test: `--poison`. Static, sub-second.",
|
"check:stale-sweep-claim": "a caveat that has stopped being true is a wrong number. The gospel surfaces carry a paragraph saying which pools were searched; while the GRN sweep ran they truthfully said part of it was unread, the sweep finished, and the sentence stayed for four more commits. The DATA decides here: undecided gap codes mean the claim is allowed, all-decided means it must go, so it re-arms itself for the next slow pool. Does NOT flag a claim rendered under a v-if on the pending count — that form expires on its own. Self-test: `--poison`. Static, sub-second.",
|
||||||
"check:surfaces-read-the-tier": "a surface may READ the tier the pin index computed, never re-derive 'this group has nothing' from the raw resource fields. That hand-rolled field list went stale every time a source was added: 7 codes drawn as having nothing while the map showed them a link, then 28 when Scripture Earth landed. Static, sub-second. Self-test: `bun run check:surfaces-read-the-tier:poison` re-injects the old formula into every surface and each must be caught.",
|
"check:surfaces-read-the-tier": "a surface may READ the tier the pin index computed, never re-derive 'this group has nothing' from the raw resource fields. That hand-rolled field list went stale every time a source was added: 7 codes drawn as having nothing while the map showed them a link, then 28 when Scripture Earth landed. Static, sub-second. Self-test: `bun run check:surfaces-read-the-tier:poison` re-injects the old formula into every surface and each must be caught.",
|
||||||
"check:tabs-localized": "a chosen locale must CHANGE the rendered tab text. check:i18n-parity compares catalogs to each OTHER, so it cannot tell a shipped translation from a dead one; tabs.json is reached only via a DYNAMIC key (SemanticTreeLegend.vue:40), invisible to any grep. Renders it and reads the result.",
|
"check:tabs-localized": "a chosen locale must CHANGE the rendered tab text. check:i18n-parity compares catalogs to each OTHER, so it cannot tell a shipped translation from a dead one; tabs.json is reached only via a DYNAMIC key (SemanticTreeLegend.vue:40), invisible to any grep. Renders it and reads the result.",
|
||||||
|
|
|
||||||
38
public/.gitignore
vendored
|
|
@ -1,38 +0,0 @@
|
||||||
token.local.js
|
|
||||||
|
|
||||||
# The build MUTEX's stamp file (internal/build-lock.sh writes it under flock).
|
|
||||||
# It is pure runtime state — a pid, a timestamp and `by=$(whoami)` — rewritten on
|
|
||||||
# every `bun run build`. It was tracked, so each rebuild committed the BUILDING
|
|
||||||
# MACHINE'S USERNAME into the repo. Nothing reads it but the lock itself, and a
|
|
||||||
# clone that inherits a stale pid is at best noise.
|
|
||||||
.build.lock
|
|
||||||
|
|
||||||
# ── GENERATED MIRRORS OF data/ — 80.7 MB committed TWICE ─────────────────────
|
|
||||||
# generate-tree.mjs:1353 copies data/<name> -> public/assets/<name>, rmSync'ing
|
|
||||||
# the destination first, on EVERY `bun run tree`. So these five directories are
|
|
||||||
# not a source: they are a byte-identical copy of a tracked source, and both
|
|
||||||
# copies were committed.
|
|
||||||
#
|
|
||||||
# MEASURED 2026-09-24, all five verified identical file-set and hash:
|
|
||||||
# posters 378 files 40.9 MB
|
|
||||||
# people-groups 2 files 23.0 MB
|
|
||||||
# tables 19 files 14.2 MB
|
|
||||||
# legacy 4 files 2.0 MB
|
|
||||||
# vendor 3 files 0.6 MB
|
|
||||||
# Proven regenerable, not assumed: deleting public/assets/posters and running
|
|
||||||
# `bun run tree` restored all 378 files with laos.webp md5 ea65d2f1 — the same
|
|
||||||
# hash the committed copy had.
|
|
||||||
#
|
|
||||||
# This is the "80mb of images in a public folder" directly: a publish was already
|
|
||||||
# rejected once at a 74 MB asset tree, and half of that weight was a duplicate of
|
|
||||||
# something already in the same repo.
|
|
||||||
#
|
|
||||||
# ⚠ previews/ IS NOT LISTED AND MUST STAY TRACKED. It has no data/ source — it is
|
|
||||||
# generated by make-previews.py from the full posters — and it is what the poster
|
|
||||||
# WALL renders from, so a dropped folder needs it (bundlePosters defaults false,
|
|
||||||
# which ships previews and not posters). Untracking it would blank every wall.
|
|
||||||
/assets/posters/
|
|
||||||
/assets/people-groups/
|
|
||||||
/assets/tables/
|
|
||||||
/assets/pplr/
|
|
||||||
/assets/vendor/
|
|
||||||
14
public/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# public/ — the built output
|
||||||
|
|
||||||
|
This folder is GENERATED by `bun run build`; do not edit it by hand. It holds exactly one thing:
|
||||||
|
|
||||||
|
- **`doxa-maps/`** — the output folder: 23 plugin bundles and 35 pages, self-contained.
|
||||||
|
Copy this one folder to any web host (or open it from disk) and it works.
|
||||||
|
- `doxa-maps/index.html` — the front page: the Plugin Studio, every plugin in one place.
|
||||||
|
- `doxa-maps/<plugin>/<plugin>.js` — a plugin's bundle (a custom element; load it with a `<script>` tag).
|
||||||
|
- `doxa-maps/<plugin>/<page>/index.html` — a ready-made page for one plugin view (embed it in an `<iframe>`).
|
||||||
|
- `doxa-maps/manifest.json` — every plugin, page and embed snippet, machine-readable.
|
||||||
|
- `doxa-maps/assets/` — the data, fonts and images the plugins fetch at runtime.
|
||||||
|
|
||||||
|
The folder's name comes from `name` in `vite.doxa-maps-build-config.json`. Sources live in
|
||||||
|
`plugin-designs/`, `library/` and `data/`; `rm -rf public/ && bun run build` recreates all of this.
|
||||||
1
public/assets/boundaries/gb-CHN-ADM1.geojson
vendored
1
public/assets/boundaries/gb-IND-ADM1.geojson
vendored
1
public/assets/boundaries/world-adm0.geojson
vendored
BIN
public/assets/previews/andean-caribbean.webp
vendored
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 51 KiB |
BIN
public/assets/previews/asia-india-gujarat.webp
vendored
|
Before Width: | Height: | Size: 50 KiB |
BIN
public/assets/previews/asia-india-karnataka.webp
vendored
|
Before Width: | Height: | Size: 52 KiB |
BIN
public/assets/previews/asia-india-kerala.webp
vendored
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 51 KiB |
BIN
public/assets/previews/asia-india-odisha.webp
vendored
|
Before Width: | Height: | Size: 51 KiB |
BIN
public/assets/previews/asia-india-rajasthan.webp
vendored
|
Before Width: | Height: | Size: 51 KiB |
BIN
public/assets/previews/asia-india-tamil-nadu.webp
vendored
|
Before Width: | Height: | Size: 54 KiB |
BIN
public/assets/previews/asia-india-uttarakhand.webp
vendored
|
Before Width: | Height: | Size: 51 KiB |
BIN
public/assets/previews/asia-india-west-bengal.webp
vendored
|
Before Width: | Height: | Size: 53 KiB |
BIN
public/assets/previews/brazil.webp
vendored
|
Before Width: | Height: | Size: 53 KiB |
BIN
public/assets/previews/central-africa.webp
vendored
|
Before Width: | Height: | Size: 50 KiB |
BIN
public/assets/previews/city-ago--8.8484-13.2535.webp
vendored
|
Before Width: | Height: | Size: 27 KiB |
BIN
public/assets/previews/city-bfa-9.7792--4.2879.webp
vendored
|
Before Width: | Height: | Size: 17 KiB |
BIN
public/assets/previews/city-brn-4.9234-114.9209.webp
vendored
|
Before Width: | Height: | Size: 35 KiB |
BIN
public/assets/previews/city-btn-27.75-91.2637.webp
vendored
|
Before Width: | Height: | Size: 25 KiB |
BIN
public/assets/previews/city-cod-3.5049-24.4533.webp
vendored
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB |
BIN
public/assets/previews/city-ind-10.1396-77.7883.webp
vendored
|
Before Width: | Height: | Size: 24 KiB |
BIN
public/assets/previews/city-ind-10.3796-78.8202.webp
vendored
|
Before Width: | Height: | Size: 31 KiB |
BIN
public/assets/previews/city-ind-10.5285-76.2146.webp
vendored
|
Before Width: | Height: | Size: 34 KiB |
BIN
public/assets/previews/city-ind-10.7853-78.7051.webp
vendored
|
Before Width: | Height: | Size: 31 KiB |
BIN
public/assets/previews/city-ind-10.7855-76.6516.webp
vendored
|
Before Width: | Height: | Size: 32 KiB |
BIN
public/assets/previews/city-ind-10.787-79.1378.webp
vendored
|
Before Width: | Height: | Size: 29 KiB |
BIN
public/assets/previews/city-ind-10.8469-76.2633.webp
vendored
|
Before Width: | Height: | Size: 26 KiB |
BIN
public/assets/previews/city-ind-11.0049-76.9626.webp
vendored
|
Before Width: | Height: | Size: 34 KiB |
BIN
public/assets/previews/city-ind-11.0546-78.6369.webp
vendored
|
Before Width: | Height: | Size: 22 KiB |
BIN
public/assets/previews/city-ind-11.06-76.0816.webp
vendored
|
Before Width: | Height: | Size: 26 KiB |
BIN
public/assets/previews/city-ind-11.0991-77.2807.webp
vendored
|
Before Width: | Height: | Size: 26 KiB |
BIN
public/assets/previews/city-ind-11.1271-78.6565.webp
vendored
|
Before Width: | Height: | Size: 23 KiB |
BIN
public/assets/previews/city-ind-11.253-76.2299.webp
vendored
|
Before Width: | Height: | Size: 29 KiB |
BIN
public/assets/previews/city-ind-11.3548-77.711.webp
vendored
|
Before Width: | Height: | Size: 26 KiB |
BIN
public/assets/previews/city-ind-11.4517-76.7145.webp
vendored
|
Before Width: | Height: | Size: 29 KiB |
BIN
public/assets/previews/city-ind-11.6234-92.7265.webp
vendored
|
Before Width: | Height: | Size: 25 KiB |
BIN
public/assets/previews/city-ind-11.6765-76.1741.webp
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
public/assets/previews/city-ind-11.8745-75.3695.webp
vendored
|
Before Width: | Height: | Size: 25 KiB |
BIN
public/assets/previews/city-ind-11.9182-79.8085.webp
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
public/assets/previews/city-ind-11.967-75.3663.webp
vendored
|
Before Width: | Height: | Size: 21 KiB |