A host that builds its own tree (maps-dev's /cdn build inherits the container env) can bake
absolute embed snippet URLs without a committed config change, the same pattern as
DOXA_SYNC_PATHS. The value is the base without the tree name; a trailing slash is trimmed.
check-embed-targets-exist reads the same effective base. Unset, the tree is byte-identical.
RQ-RGPRINT, standalone /regions-dashboard/region-dashboards/ on a035408e2,
Chromium print media: ONE page, Letter. The map DID draw — basemap, region
fills and dots all rasterised. But the document under print media was 900px
tall, one viewport, while the content continued to y=1753. The sheet showed the
stats and the map, then blank to the bottom, and the region list ending at
"Oceania" was simply absent.
The cause is the leaf, not the plugin: `#slot, <tag> { position:fixed; inset:0 }`.
A fixed element paints once, on the first page. Every leaf taller than a screen
has been printing its top screenful and dropping the rest, and no shadow-root
rule could reach it — a shadow root cannot restyle its host's document rule,
which is why the dashboard's own print CSS landed and changed nothing here.
min-height rather than a bare release: a full-bleed map leaf takes its SIZE from
that fixed rule, so height:auto alone can leave a zero-height box and print an
empty page — a worse failure than the one being fixed. The obvious knob is the
wrong one: isMap is true for region-dashboards, which is a scrolling dashboard,
not a full-bleed map. The floor keeps a definite box for the map case while
letting a taller document flow onto page two.
Still to be looked at, and asked for: one printed full-bleed map leaf, to
confirm the floor holds a canvas that was sized by the fixed rule. The
dashboard case is measured; that one is reasoned.
No generated leaf page has ever carried an @media print rule — measured across
the tree: zero, out of 63. Two consequences, both on every plugin:
The page background is #0a0e15. Browsers drop background colours by default, so
this is invisible until a reader ticks "Background graphics" to get the MAP on
paper — and that reader, the one who most wants the printout, gets the sheet
flooded black around it.
#status is the loading overlay: position:fixed, inset:0, above everything. It
is emptied once the element mounts, but it is still a full-sheet layer, and a
page printed before or during load prints "loading…" over content that did in
fact load. It is hidden only while it is NOT an error: an error still prints,
because a blank sheet with the explanation suppressed is worse than a sheet
that says what went wrong.
Deliberately NOT in this commit: releasing the leaf's position:fixed. The bare
leaf pins the element with `#slot, <tag> { position:fixed; inset:0 }`, which is
why a standalone dashboard still prints one screenful even after its own print
rules landed — a shadow root cannot restyle its host's document rule. Releasing
it would make a full-bleed map leaf resolve its height against auto and could
print an empty box, so the discriminator is not `isMap` (region-dashboards is
isMap:true and is a scrolling dashboard, not a full-bleed map) and I will not
guess one from source. It needs one printed page looked at first.
The platform dashboard is moving off one shared index onto each team's own
/cdn/dx-maps/manifest.json, so this manifest now has to answer a question it
never carried: may another team list this plugin. Per bundle and per profile:
"team" listed only in the owning team's library
"public" also listed in every other team's library, read-only from our CDN
absent team
Emitted ONLY when public, so every manifest in the tree keeps the bytes it has
and nothing becomes visible by upgrading the generator — verified by rebuilding
the tree and comparing: byte-identical.
Three things this encodes rather than states.
`stage` is NOT this field. It sits directly beside it, reads like a fit, and is
maturity rather than audience — using it would hide every experimental plugin
from its own team and publish every production one to everybody, wrong in both
directions at once. The header says so where the next reader will be standing.
An unrecognised value is REFUSED, not defaulted. Coercing "Public" to team is
the safe direction and that is what makes it dangerous: the author has written
their opt-in, the manifest says private, and the plugin never appears anywhere,
with no error to search for. The generator exits, naming the profile and the
value, at the build that would have published it. Both halves were mutation
tested: "public" reaches the manifest at bundle and profile level; "Public"
exits 1; absent leaves the file byte-identical.
A bundle is public if ANY of its profiles is, because a bundle is one IIFE.
Profiles share a single .js, so publishing one publishes the file the others
compile into; there is no shape of this field that makes half a script
fetchable. The per-profile key is still emitted, so a reader can list only the
profiles that opted in while loading the one bundle.
The built manifest and the dev server's both go through profile-meta.mjs, so
the dev Studio and the built Studio cannot disagree about this the way they
once did about "widget".
The snippet is the only thing a partner ever reads, and before this none of the
36 mentioned provider at all. So when a token stopped selecting Mapbox, the
hosts affected were exactly the ones who had pasted a real token into this
snippet — and they had no way to see the lever that would give it back.
Emitting "provider":"maplibre" restates the new default, which looks redundant
until you ask what the snippet is for. Stating the default is what makes the
alternative visible. A one-line HTML comment above the element names the opt-in.
Generated, so the change is in the generator; the 36 files follow from a tree
run. Verified 36 of 36 carry both the key and the note.
⚠ THIS CHANGES WHAT TWO BROWSER GATES MEASURE, and they are not mine to edit.
check-map-embed-cells and check-experimental-shelf both fill YOUR_MAPBOX_TOKEN
into this snippet to exercise the Mapbox path. With provider pinned to maplibre
they will get MapLibre and make no api.mapbox.com requests at all — so an
assertion like "every api.mapbox.com request carries the host's token" stops
failing because it stops having anything to check. Each needs to swap the
provider to mapbox in the snippet it builds, the way the shelf gate already does
for its leaf URL with ?provider=. Flagged rather than silently left to surface
as a green run that measured nothing.
A plugin's <profile>.meta.json targets (web, mobile, desktop, widget; absent = web)
now decide where it is built. internal/plugin-targets.mjs is the one reading.
desktop:build builds a desktop plugin into .desktop/<plugin>/ with a page that
names its engine, builds its backend as a Tauri sidecar for this machine, and runs
tauri build with a --config override; tauri.conf.json and public/ are untouched.
The shell starts the bundled rudder-engine (RUDDER_AUDIO=browser, RUDDER_HOME in the
app data folder) and stops it on exit. A plugin whose targets leave out web
(Rudder: desktop + widget) is no longer built for the web tree: run-parallel,
generate-tree and check-composite-freshness skip it, and its public/ folder is
removed; the dev server still gives the Studio its corner. check:targets
(gates:pre, with a self-test of five planted violations) validates the values,
widget corners and backend folders and fails if a non-web plugin reaches the built
tree. Measured: desktop:build rudder-voice on Linux produced Rudder_0.1.0_amd64.deb
(8.8 MB) with the app and the engine side by side; the app has not been launched.
A preset page baked only the instance's props, so every default in the profile's
.meta.json config that the instance did not repeat was lost (go-int: upg-neighbor's
five presets dropped pinHint:false). instanceHtml now takes the meta config first
and the props over it. check:preset-config-merge (gates:post) reads every built
preset page and fails on a lost default; the rule is written into
PRESET-PLUGIN-STANDARD.md. Affected before the fix: upg-neighbor only; no
production preset has profile defaults to lose.
The India ADM1 boundary is Creative Commons Attribution (CC BY 2.5 IN,
sourced from DataMeet and the Election Commission of India), so the credit
has to reach whoever receives the data. It did not: 6.1 MB of boundary
GeoJSON shipped with no human-readable attribution, and a sidecar for one
file of eight.
data/boundaries/ATTRIBUTION now ships beside the data. Every licence value in
it is verbatim from the geoBoundaries API, queried per boundary -- CC BY 2.5
IN for India, Public Domain for China. No licence wording is ours.
★ IT TOOK TWO ALLOWLIST EDITS, and the first one silently did nothing. The
boundary staging is TWO-STAGE: a pair list gathers into the build pool, and a
separate path list decides what reaches the tree. I added the file to the pair
list, rebuilt, and it still did not ship. An entry in one stage of a two-stage
pipeline is not an entry.
WORLD-ADM0 IS CORRECTED, not closed. This register first guessed Natural
Earth, public domain. It is geoBoundaries gbOpen assembled per country: its
features carry geoBoundaries' own iso3/shapeGroup/shapeName, and the plugin's
data/adm0/PROVENANCE.json names the source and endpoint. That file also states
the real problem -- gbOpen passes each country's UPSTREAM licence through, so
one file carries up to 162 licences and up to 162 attribution duties -- and
its licences table is EMPTY. Which of the 162 are CC BY is unestablished.
Stated in ATTRIBUTION as an undischarged duty rather than papered over.
pplr's row is exp-shepherd's evidence: the PPLR ArcGIS org aggregating IMB,
Joshua Project, ROP and WCD identifiers, publishing no licence at all, and
WCD is a proprietary Brill database.
Both are MIT, which asks for the permission notice to be included with the
software. Tabulator's minified .js carried only "(c) Oliver Folkerd 2025" and
its .css carried nothing; Vue's banner names @license MIT but omits the
permission text. Each now has its upstream LICENSE vendored verbatim beside
it -- fetched, not authored, with the URL and sha256 recorded.
★ AND THE ALLOWLIST IS WHY IT WOULD NOT HAVE WORKED. generate-tree stages
vendor/ by an explicit PATH LIST, so the licences sat in data/vendor and never
reached the tree: staged count 0. Vendoring a notice the staging step drops is
not compliance, it is paperwork. Both LICENSE paths are now listed.
documentation/THIRD-PARTY.md is the register the sweep produced: every
non-original thing dx-maps ships, its source, its licence, whether the licence
or attribution actually travels, and the gap. Compiled by inspecting the
SHIPPED tree rather than the source lists, because the question is what a
recipient receives.
Four rows are left OPEN and marked as such. Two are unverified and need a look
(world-adm0's provenance, the pplr upstream). Two need a decision, not a fetch:
people-groups and the 304 derived tables carry data whose redistribution terms
are not established, 36 MB of it. Attaching a licence I have not confirmed
would be worse than the gap, so those go to the Driver. geoBoundaries is
closable by work rather than judgment -- CC BY needs attribution, and the
.meta.json sidecar pattern already exists here for one file of eight.
The capability gate reported five composes refs as "resolve to no known bundle/profile" —
bp-research-header, cities-internet-map, gospel-resources-map, my-upg-100-list-map,
research-dashboard-map. All five were correct data. `composes` accepts a third form besides
'<bundle>/<profile>' and a bare '<bundle>': a registered ELEMENT TAG. Nothing needed renaming and no
author's meta.json was wrong; the resolver was short. 5 unresolved -> 0.
Two things the element form forced, both measured:
· A SELF-REFERENCE is an internal child, not a nested plugin. big-picture composes
bp-research-header, its own element, which resolved back to big-picture (cap-4) and promoted the
bundle to cap-5. Excluding self keeps "nests other plugins" from meaning "has parts".
· A name covering several profiles resolves to the HIGHEST tier among them, and both non-slashed
forms now resolve the same way. A bare bundle name and that bundle's element tag both mean "this
bundle", so taking the first profile for one and the highest for the other made the answer depend
on discovery order. gospel-resources shows it: four single leaves and one cap-4 composite in one
bundle, and losing the 4 silently demotes whoever composes it.
The rule now lives in internal/_composed-tiers.mjs, imported by the generator that WRITES the tier and
the gate that CHECKS it. It was duplicated, and a rule this fiddly does not stay identical in two files;
the drift would have shown up as a gate passing a manifest the generator would refuse to write. The
data stays independent — the gate rebuilds the edges from the <profile>.meta.json files on disk and
checks the published manifest against them.
With the count at 0, an unresolved ref FAILS in both. While five were outstanding a warning was right:
they were correct data the resolver could not read, and failing would have blamed five authors for a
gap in the tooling. At zero, a typo and a genuine plain child still look identical from here, so the
one thing that must not happen is quietly reading one as the other.
Verified on the tree as it stands: 37 profiles, 25 bundles, 10 declaring composes edges — 10 agree,
0 disagree, 0 unresolved. Each fail arm negative-tested: a one-letter typo in a ref fails; a published
tier that disagrees with its own edges fails; the same composes shape pointing at another bundle's
cap-4 instead of its own element fails, which is what proves self-exclusion is load-bearing rather
than incidental.
generate-tree derives each profile's tier from its composes edges (composing
anything is capability 4; composing a capability-4 plugin is capability 5). An
explicit type is checked against the derivation and a contradiction fails the
build naming both. check:capability-derived asserts every profile is 1-5 and
agrees. documentation/PRESET-PLUGIN-STANDARD.md describes the one
*.instances.json file a preset plugin declares.
rega (7a7ca67b): on the dev server the corner widget never mounted, because /__plugin-gallery/manifest.json
built its profiles without the .meta.json fields only generate-tree read. Both now read them through one
module, internal/profile-meta.mjs (stage, heavy, widget per profile; bundle stage = production if any
profile says so), so the dev Studio and the built Studio cannot disagree. Verified: the dev model lists the
same 7 production bundles and rudder-voice:bottom-right; the built manifest is unchanged.
BUILD-CORNER-WIDGET-CLASS-RUDDER-IN-PLUGIN-STUDIO (design approved by dx-shepherd). A profile's .meta.json
declares "widget": { "corner": "bottom-right" | ... }; generate-tree carries it into manifest.json, so the
Studio keeps no list. The Studio mounts each widget once in a fixed corner host (z-index above everything),
outside the rail and the pane, collapsed to a button, with its open state remembered. Picking it in the rail
opens the corner instead of mounting a second copy in a pane. The host and the mounted element live in a
page-level singleton on document.body, so a Studio hot update or remount re-attaches the same element and
Rudder keeps its live stream. rudder-voice declares bottom-right. Needs eyes: open, HMR, collapse.
generate-tree's asset allow-list staged people-groups-slim and -points but not people-groups/lists/*.json,
so on the built tree every non-default list (frontier-500k, frontier, unreached, unengaged) 404'd and the map
drew 0 pins (go-int / rega at a71007f1). The lists folder now ships whole, like tables/: the fetch name is the
list id. Verified on a regenerated tree: all four files staged; check:list-prop-unit PASS.
The Driver: a public plugin that is not yet a production map shows under Experimental until it is. A
profile's .meta.json declares "stage": "production" | "experimental" (default experimental; a bundle is
production if any profile says so) and "heavy": true for a live WebGL owner. generate-tree carries both into
manifest.json, so the Studio and the gallery read the same field and neither keeps a list.
The seven production bundles are marked: dashboard-plus-grid, research-map-plus-grid, regions-dashboard,
my-upg-100-list, doxa-simple-map, cities-internet, gospel-resources. The Studio rail keeps its capability
sections for them and puts every experimental bundle (gospel-internet among them) in one final Experimental
section. The gallery's auto-discovered Experimental folder skips a production bundle. gospel-internet-board
joins HEAVY_ELEMENTS (it can own two WebGL contexts). check:studio-rail asserts the split.
Seven consumers switch to the -web copies exp-shepherd built (5410842a: every feature and island kept, about
a fifth of the bytes): useCountryOutline, useMapLayers, regions RegionCountryMap, research-dashboard
DashboardMapPanel, cities-internet connectivity-map (world + India states), gospel resources-map (world +
India provinces) and my-upg-100-list (world + UPG-100's India partition; China unchanged). generate-tree
stages the three -web files beside the full ones. Posters, print and every build-time point-in-polygon keep
the full files, as REPORT-WEB-BOUNDARIES.md requires: no runtime code tests membership on these.
Six tools defaulted to the live host's URL. They now read the published tree's location from
DOXA_PUBLISHED_BASE (environment, or the gitignored .env / .env.local) through internal/_published-base.mjs,
and with nothing set they report NOT MEASURED (exit 75) instead of measuring a host they were never told
about: check:catalog-currency, check:catalog-identity, check:published, verify:5-caps, verify:map-paint. An
explicit BASE / --base / PUBLISHED_BASE still wins. check:embed-base only needed an example URL for its
offline resolution table.
The public-hygiene leak detector builds the platform name from parts, so the file that catches it does not
carry it. Comments and the two build-config notes say the host platform or the live test host. From
repo's DECOUPLE round 2 list.
The served gb-IND-ADM1.geojson is the 100-list's curated partition (Mizoram and Nagaland excluded,
listId-tagged), so a map colouring every state had data for 36 and shapes for 34. data/boundaries/
gb-IND-ADM1-full.geojson is all 36 from geoBoundaries gbOpen (boundaryID IND-ADM1-1811400, DataMeet /
Election Commission of India, CC BY 2.5 IN), with source and licence in gb-IND-ADM1-full.meta.json;
generate-tree stages both, and the dev server serves data/ directly. Reproducible with
library/scripts/boundaries/source-full-adm1.mjs (coordinates rounded to 3 decimals: 1.84 MB). The curated
file is untouched.
maplibre-gl@5 and maplibre-gl-geocoder@1 were floating ranges, and no runtime loader set `integrity`,
so a CDN could serve any bytes to every partner's visitors unnoticed (CRITIC-5).
- library/constants/cdn.js is the one registry: mapbox-gl 3.24.0, maplibre-gl 5.24.0 (the release the
@5 range already resolved to), both geocoders (1.9.4 / 5.0.0) and d3 7.9.0, each with its sha384.
Every host answers Access-Control-Allow-Origin: *, so integrity + crossorigin work cross-origin.
- ensureEngineScript / ensureEngineCSS, the geocoder loaders (library and PPLR) and the generator's
static MapLibre tags set integrity + crossorigin from it; the embed snippet's engine preload matches.
- check:cdn-pins (gates:post): no floating CDN url in shipped code, and every one is in the registry;
--online re-fetches each and compares the hash (all 9 match today).
MapLibre stays on 5.x knowingly. GHSA-jrc7-96c5-q579 (DOM.sanitize, reached through a source's
attribution string) affects <= 6.4.0, and 6.x is ESM-only (no UMD global), so the upgrade is a loader
migration of its own. The attribution strings rendered today come from our configured basemap styles
and our own sources; the note in cdn.js says who is exposed until then.
upg-neighbor/flower-mesh is a preset of the upg-neighbor element that opens on
its Flower Mesh lens, which is SVG and has no GL map. The leaf is still a map
element: the view-switcher reaches Send Map and the other GL lenses, so it must
keep isMap (engine peer, token ladder). Marking it isMap:false would break those
lenses. check:leaf-engine-per-provider judged it as a drawing map and failed it
on both engines ("drew: none"), before and after the auto engine alike.
- upg-neighbor.instances.json: the flower-mesh preset says "opensOnMap": false.
- generate-tree.mjs: an instance's manifest entry carries opensOnMap:false when
it is set (absent otherwise, so every other entry is byte-identical).
- check-leaf-engine-per-provider.mjs: such a leaf is not judged as a drawing
map. Its Mapbox-request count under ?provider=maplibre is still measured.
Headless: after bun run tree, the manifest marks only
upg-neighbor/upg-neighbor/flower-mesh with opensOnMap:false. The gate's leaf
selection, replayed over it, judges 28 of 56 leaves as drawing maps, with
flower-mesh excluded and its five siblings unchanged. No browser run.
An RTL locale reflected onto document.documentElement whenever it
resolved — so a reader following a ?lang=ar link to a PARTNER'S page
flipped the partner's whole <html>: their headings, their nav, their
footer. The existing comment already worried about the host owning that
decision, but it only guarded against CLEARING dir, never against
setting it.
A generated leaf is the only thing that knows a document belongs to the
plugin, so it says so: window.MAP_OWNS_PAGE, written into all three leaf
templates. library/i18n reads it before touching the document. Nothing
else sets it, so the DEFAULT IS "not ours" — a partner page that
declares nothing is never touched. Absence has to mean restraint for a
flag like this.
The element's own direction was never this code's business: ProfileLoader
binds dir on .profile-loader from effectiveLocale (0f6d5049), so the
plugin mirrors correctly whether or not it owns the page.
★ THE OLD GATE COULD NOT HAVE SEEN THIS, which is why it needed a new
case rather than a re-run. check-embedded-rtl drove the locale through
profile-config — the channel a HOST uses. This defect fires on the URL
channel, which is what a READER uses, and detectLocale() reads the URL
FIRST. Added a ?lang=ar case on the partner page:
before profile-config cases partner html unset / body ltr pass
?lang=ar partner html RTL / body RTL FAIL
after ?lang=ar partner html unset / body ltr pass
plugin root stays rtl throughout — it still mirrors
Standalone leaves are unchanged and verified: ownsPage true, html
dir="rtl", plugin root rtl. The page we own still mirrors completely.
template-literal-check reads source with a simple state machine that does not know regex literals.
The .replace(/"/g, …) in esc() looked to it like an open string, and it stayed "in a string" until a
later quote. Its state happened to re-sync on a `//` inside the forge page template; when 87ebbace
removed that line (the static api.mapbox.com preconnect), the check lost track and flagged a
backtick in an ordinary JSDoc comment (generate-tree.mjs:1084) as a stray backtick in a template.
gates:pre failed for everyone.
The regex is now /\u0022/g: the same character, so escaping is unchanged (verified: 'a"b' → a"b),
and the tree regenerates byte-identical.
A generated map leaf chose its engine at BUILD time and hard-linked it in <head>: mapbox-gl.css,
the geocoder css, mapbox-gl.js and the geocoder js from api.mapbox.com. The provider is chosen at
RUNTIME (profile-config.provider → ?provider= → window.MAP_PROVIDER), so a partner embedding a leaf
with ?provider=maplibre still had every visitor's browser make 4 requests to Mapbox.
- generate-tree: no static engine tags on map leaves; the element's own runtime loaders
(ensureEngineScript / ensureEngineCSS, the geocoder's on-demand loader) load exactly the engine
asked for, the path composite leaves already ran on. The forge page drops its api.mapbox.com
preconnect. A leaf opened with provider=maplibre (same ladder as resolveMapProvider) is token-free,
so the public copy no longer asks a MapLibre visitor to paste a Mapbox token.
- check:leaf-engine-per-provider (new, browser): every leaf at ?provider=maplibre makes 0 requests
to *.mapbox.com and still draws; at its default every map leaf draws with the engine it drew with
before (--baseline <tree> compares against another tree).
- check:dashboard-geocoder-provider is wired into gates:post alongside it; it had been held out
because this generator's tags made it red.
- verify-map-paint: optional GATE_VIEWPORT for a phone pass.
Measured before this landing (browser runs made before the headless rule): leaf-engine vs main's
tree PASS, 56 leaves, 0 Mapbox requests under MapLibre, same default engine. Pins, region popup,
PPLR search and key-cities PASS on both engines with 0 Mapbox requests under MapLibre. RTL shaping
PASS on both engines wherever RTL labels rendered. Paint under MapLibre: the same 4 leaves fail as
on main (country-posters-mfe, upg-100-mfe, regions-mfe, flower-mesh). Pre-existing, not caused here.
Poster "Render" is missing on country-posters and doxa-research-map under BOTH engines. Pre-existing
on main, read from source: countries-map.vue renders <PosterDialog v-if="showPoster"> without
v-model or getMap, and PosterDialog shows only when modelValue is true.
Headless, on this commit's regenerated tree: 57 leaves, 0 carry api.mapbox.com engine tags (main's
doxa-simple-map leaf carried 5), 56 carry the runtime provider check, check:sri PASS.
Two gaps in the build-owner tooling, found by rega while landing 13f5f3a3:
- check:composite-freshness judged only COMPOSITES. A library change (bd75f609's Arabic catalogue:
library/i18n eager-loads every locale into every bundle that uses it) left 12 of 25 bundles stale on main,
most of them not composites; my consumer rebuild d1af039a saw none of it. The check takes --all and
--bundles a,b; the landing precondition scopes it by what the landing changed (shared code -> every
bundle; otherwise composites + the bundles whose own folder changed); the build-owner landing always
uses --all. Proven: at d1af039a it names prayer-walk-planner STALE (0b695f70 vs 0042dda2, rega's numbers);
on main 13f5f3a3 --all finds the 12.
- fix:sri repaired manifest.json but not the Research Gallery's byte-identical copy
(doxa-maps-spa/manifest.json, read first on a foreign origin), so an embedded gallery refused a rebuilt
bundle; it now rewrites the copy too (tested: a changed bundle -> both repaired, identical, check:sri
PASS). And generate-tree now writes the front page's integrity itself, so `bun run tree` after fix:sri no
longer strips it (tree alone: 115 agreed, 0 added).
Driver decision: no embeddable URL carries "doxa" in its folder. The tree's name is one line in
vite.doxa-maps-build-config.json ("name": "dx-maps"); everything else follows it.
- __DOXA_TREE_NAME__ (the vite define rega landed, from MFE_NAME = internal/_tree-name.mjs) bakes the name into the
bundles for the runtime code that must recognise the tree in a URL. StackLayout.vue's
resolveBundleSrc anchored on a literal /doxa-maps/ and would have broken every composite's nested
bundles on a renamed tree; it now anchors on the define. The Research Gallery's dev fallback uses
it (one reader: rega's define, not a second one).
- The native shells follow via internal/sync-app-shells.mjs: capacitor webDir public/dx-maps, Tauri
frontendDist ../../public/dx-maps (its window url is tree-relative since the one-folder landing).
- The WordPress contract test reads the name from config instead of carrying three literals.
- check:locale-reachable extracted detectLocale alone and died with "localeFromUrl is not defined"
after 09d03c60 factored a helper out; it now follows the calls it makes (already red on main).
- generate-tree removes its temp asset pool even when it crashes (a failed build leaked ~50 MB into
/tmp each time; measured on a full disk).
- Capability exemplars regenerated for the new paths.
Proof: rm -rf public && bun run build -> public/ = README.md + dx-maps/; the 25 built bundles carry
zero 'doxa-maps/' paths (scanned); check:sri, check:drop, check:parse, check:public-hygiene PASS.
Tag names (doxa-* -> dx-*) are the next, separate landing.
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.
The output tree's name has always been configurable (vite.doxa-maps-build-config.json "name"),
but 61 tooling files ignored it and hardcoded 'doxa-maps' (181 literals), so a rename would have
moved the build while the gates kept reading the old folder.
- internal/_tree-name.mjs reads it once (validated as one URL-safe path segment; falls back to the
historical name) and exports TREE_NAME / TREE_NAME_RE; `node internal/_tree-name.mjs` prints it
for shell scripts.
- A codemod rewrote 171 literals in internal/ to TREE_NAME; the regex literals, messages with
backticks, atomic-public-swap.sh (fails loudly if the name cannot be read), build-lock.sh and
vite.config.js (its duplicate readMfeName is gone) were done by hand.
- The name is unchanged: old and new tooling produce all 1408 files under public/ byte for byte.
Code lines still naming the tree in internal/ + vite.config.js: 2, both intended (the helper's
fallback, the private-repo-name pattern in public-hygiene-check).
This renames nothing visible; the eventual rename becomes a one-line config change.
generate-tree decided a single leaf's map-ness as "its own .vue
references an engine OR anything under the BUNDLE's src/ does", so a
map-less profile inherited it from a sibling. regions-dashboard/
regions-list shows a list and no map, but src/RegionCountryMap.vue made
it isMap=true. Its leaf loaded the Mapbox engine for nothing, and since
4a6886d2 its embed snippet also preloaded it.
A leaf's map-ness now follows the RELATIVE imports reachable from its
own .vue, bounded to its bundle, so a map several imports deep
(research-dashboard's DashboardMapPanel) still counts. An explicit
boolean isMap in <profile>.meta.json wins. Composites keep their
separate rule, since they do mount their siblings.
Measured: across all 34 profiles exactly one changed (regions-list,
true -> false). 18 map profiles keep isMap and their preload; 0 non-map
snippets carry one. On a served built tree regions-list makes 0 engine
requests and 0 api.mapbox.com requests and renders its list (673
elements, no errors), where before it downloaded the 1.75 MB engine.
regions-mfe still loads the engine and draws its map. The regenerated
files are only the regions-list leaf, the two manifest copies (still
byte-identical), the staging page and the gallery; check:sri passes.
The card asked for Vue to be bundled inline to make maps faster. It
already is: vite.config.js externalises only jspdf, and every IIFE
carries its Vue runtime. The measurement (internal/measure-map-load.mjs,
cold, 10 Mbps / 60 ms, served built tree) found one serial runtime
fetch before first paint, on partner pages only. A page that pastes the
embed snippet has no leaf page to fetch mapbox-gl.js (~480 KB gz), so the
bundle requested it after it had downloaded, run and mounted. Leaf pages
already load it in parallel.
Map profiles' script snippets now start with a preload for the engine,
at the URL read from useMapInstance.js ENGINES.mapbox.js so the two can
never differ. It has no crossorigin attribute because
ensureEngineScript injects a plain <script>, and a mismatched preload is
not reused. Non-map profiles get no preload. A comment tells a partner
who switches to provider "maplibre" to delete the line.
Partner embed, before -> after (2 cold runs):
doxa-simple-map engine start 1.06 -> 0.20 s, done 2.26 -> 0.95 s,
style start 2.73 -> 1.94 s
regions-mfe engine start 2.67 -> 0.11 s, done 3.25 -> 1.13 s
(style start unchanged: the composite mounts its map late)
gospel resources engine start 0.81 -> 0.27 s
One engine request per run: the preload is reused, never a double
download. The time until tiles are loaded is dominated by tiles, data
and drawing, and moved within noise.
Held, per dx-shepherd: no shared Vue, no inlined mapbox-gl, and lighter
basemaps are for the Driver to decide.
The doxa.life script embed was blocked on a server change: the live root
manifest.json answers without Access-Control-Allow-Origin, and the gallery
fetch()es it cross-origin, so no tab could mount. Measured on nuxtbase,
though, the same static layer DOES send ACAO on every path inside a folder
of the tree (doxa-maps-spa/<anything>, even a 404; assets/…).
So generate-tree writes a byte-identical manifest copy at
doxa-maps-spa/manifest.json in the same step as the root one (they cannot
diverge). The gallery reads it FIRST when the tree is on another origin (no
CORS error in the host's console) and the root first when same-origin.
Script URLs still resolve against the tree root, never the copy's folder.
check:gallery-embed:live-cors serves the tree with the live rule
(serve-strict LIVE_CORS=1: no ACAO on the root manifest only).
- The current gallery under it: "did not boot within 60s" (negative
control, reproducing doxa.life).
- With this change: js 14, iframe 14, neighbour 14, linked 4. The default
gate also passes all phases.
check:embed-targets, check:drop and check:public-hygiene pass with the copy
present.
The server-side CORS rule (serving/traefik-embed-headers.yaml) is still the
right long-term fix, and it needs server access. This unblocks the embed
without it once the tree is published.
ALL PEOPLE GROUPS. The Driver: "not even loading, takes a very long
time", and its DOXA pins were dark on a dark map.
- It downloaded the 7.5 MB people-groups-slim.json (22 columns) to draw
ten fields. It now fetches people-groups-points.json: the same rows in
column order, repeated strings in dictionaries, coordinates at 3
decimals. 0.74 MB, generated deterministically by
library/scripts/people-groups/build-points.mjs and staged into the tree
by generate-tree. It falls back to the old file on an older tree.
- Pins no longer wait for the live prayer API. Every pin draws by GSEC
from the file, and the DOXA overlay recolours when the API answers. The
DOXA count reads "…" until then, not 0.
- Named style 'paper' (Mapbox light-v11 / MapLibre positron) instead of
'ink'. The four categories keep their meaning; yellow and green are a
step darker, and pins get a thin dark ring, so they read on near-white.
- Its words are in all 11 locales (allPeopleGroups.json), with a guarded
useI18n and values passed to vue-i18n so {n} is not blanked.
Measured on a served built tree, 3 fresh runs each, median time to first
rendered pins: 8.3 s before, 5.2 s after, on localhost. This box renders
GL in software, so most of the remaining time is map start-up; the 10x
smaller download matters more on a real connection.
DOXA VISION CARD. The Driver: "the data doesn't make sense … super false
information". The card rotated four typed-in figures with no source.
It is now a proportional funnel, every number computed from data by
library/scripts/people-groups/build-vision-funnel.mjs:
12,379 people groups (IMB rows: one group in one country)
7,341 unreached (IMB LostnessPriority 0-2, < 2% evangelical)
4,754 frontier (LostnessPriority 0, < 0.1% evangelical)
2,107 DOXA groups (the DOXA prayer list, snapshot saved in data/;
1,745 of them frontier)
The first three nest; DOXA groups do not all sit inside frontier, so the
card says how many do. The rules are on the card ("How these are
counted") and in HOW-THE-NUMBERS-ARE-COUNTED.md, which also explains why
the total is not the 16-17 thousand of Joshua Project's count. Material
light, all 11 locales, right-to-left in Arabic, sources and dates shown.
The old fake stats are removed from the instances file, the frontier
page, the manifest, the staging page and the gallery.
CHECKS.
- check:vision-card-numbers recomputes the funnel independently (own CSV
reader, the published rules) and compares with vision-funnel.json and,
with --url, the rendered bars. Negative-tested: a corrupted count exits
1. On the built tree: bars 12,379 / 7,341 / 4,754 / 2,107 and "1,745 of
them are frontier", all matching.
- check:map-embed-cells measures PRODUCTION-MATRIX cells B and C for the
seven gallery maps on a foreign origin; C proves the host's token is
used by handing the host a distinct token and checking every Mapbox
request. 14 of 14 pass.
Not for main yet. All ten views mount on the dev server with real data and no
page errors. Left: the coverage view's status still says 'sample of 6,000',
its choropleth needs checking, and the bundle is not built or tree-verified.
A composite's nested Mapbox maps read window.MAP_TOKEN, not the page's
profile-config. In a tree with no baked token (the public copy), the token-free
research-map-plus-grid page mounted before reading ?tk= or a stored token, so
its map stayed blank with no prompt. Now every leaf publishes the token it
resolves to window.MAP_TOKEN, and a token-free composite with no token renders
its content plus the paste-once prompt, then reloads with the token.
Verified in a browser against a tree regenerated with no token:
research-map-plus-grid no token: prompt shown | ?tk=: map drawn, Mapbox 6 OK / 0 errors
dashboard-plus-grid no token: prompt shown | ?tk=: map drawn, Mapbox 6 OK / 0 errors
Negative control (unpatched generator): research-map-plus-grid with ?tk= never
received the token (0 Mapbox requests) and showed no prompt without one.
Also: removed a personal health note the page template wrote into every leaf
page, and the Mapbox account names from three comments. Pages regenerated on
the latest main; nested-token, sri, hygiene, drop, parse, embed-targets pass.
template-bundle is the scaffold DX copies to start a plugin: its source
ships, but it is not a plugin. The dev manifest already skips it by
name (discover.mjs), while the build published it, so the built studio
listed template-map-a and template-map-b as two blank-looking plugins
that the dev studio never showed.
generate-tree now skips it by the same name rule, so the built
manifest lists exactly what dev lists: 21 bundles, 33 profiles. The
committed tree is brought in line without a full regeneration: the
entry is removed from manifest.json and from the staging page's baked
copy, the two leaf pages are deleted, and the archived gallery is
regenerated from the manifest. It is not added to gatedProfiles, which
is the private-data list.
Verified: check:sri and check-manifest-covers-discovery pass, the
staging page renders with no errors, and a sweep of the built studio
shows 31 plugins and no template rows.
The gallery is going onto the DX-life research page, and nothing had tried
it on a page it didn't generate. From a second origin, a bare
<script> + <doxa-maps-spa> drew over the host's header, and 8 of 13 tabs
showed an error card.
Three causes, each fixed in the shell:
- No :host box. The layout root is position:absolute, so it anchored to the
viewport instead of the element. :host is now a positioned block with a
default height the host can override.
- A foreign page sets none of the globals a leaf page bakes. Every data call
hit the host's own origin and got HTML back. The manifest now publishes a
`runtime` block (apiBaseUrl + the public token every leaf already carries).
The shell fills window.MAP_APP_API_URL / window.MAP_TOKEN only when the page
hasn't set them, and forwards tk in profile-config.
- Nested elements resolved assets to <tree>/../assets, a sibling pool the
publisher doesn't ship (404 live). The shell now forwards an absolute
<tree>/assets. This also fixes the iframe mode, where Cities with Internet
and Gospel Resources were failing the same way.
Also adds a `tab` attribute (plus ?tab=) to deep-link into one map, and a
`tab-change` event for the host.
check:gallery-embed drives every tab in both modes from a second origin. It
passes here and fails on the previous tree (7 findings).
EMBED.md documents the contract. The Traefik template now sends CORS for the
whole tree: live manifest.json answers without Access-Control-Allow-Origin.
Send Map, the Bloom cockpit, Flower Mesh, Lang Cluster and Flowers ran in
the Experimental shelf as a 976 KB prebuilt legacy bundle inside a sandboxed
iframe. Their original source survived in the old app-profiles/doxa-upg-neighbor
bundle, and it only imports library pieces that still exist, so it moves here
as-is: the unified hub becomes upg-neighbor.vue and the lenses go in src/.
The hub now picks its opening lens at setup rather than on mount. Switching
on mount tore Send Map down mid-initialisation and threw on every other lens.
The five instance ids stay the same, so existing embed URLs still work, and
the old legacyElement key still maps to a lens.
The legacy sandbox (data/legacy/upg-neighbor and its staging rows) is gone
because nothing loads it any more.
posters/*.webp is not shipped by default (42MB; bundling pushed the tree to 80MB,
git-packed to 57MiB, and the catalog rejected the push). The DOWNLOAD path has
handled that since d154a76 — PosterWall fetches posters/<id>.html and rasterises
it in the browser. The PRINT path did not: it handed an <img> a url it never
checked, so on a default drop the SAME poster downloaded fine and PRINTED as
"Poster not available yet." That is the bad first impression
IDEA-POSTER-EXPORT-404-ON-DROPPED-FOLDER-TRADEOFF called out by name.
Three things were missing; all three are here:
1. printPoster now climbs the same ladder as the download path — probe the webp,
and only on a miss fetch the .html, rasterise, and verify PIXELS before opening
the dialog (an SVG in an <img> is an isolated document: a lost stylesheet prints
a black square with no error). Blob urls are held until unmount, not revoked at
print time — the preview <img> is still reading from them.
2. publish-html.mjs wrote into public/assets/posters, which is a GENERATED MIRROR
that every `bun run tree` rmSync's. Its output never survived, which is a large
part of why the fallback still had no input months after it shipped. Retargeted
at data/posters, the tracked source the mirror is made from.
3. bundlePosters is one boolean over a directory holding 40.9MB of rasters AND the
28 sources, so it could only ever ship both or neither — and "neither" is what
made both full-size paths 404. posterSources:true ships the sources without the
rasters, the combination that was previously inexpressible. DEFAULT UNCHANGED:
28 sources are 27.9MB raw (6.9MB packed, measured with pack-objects), real
weight in a published tree that needs a deployment decision, not a silent one.
VERIFIED AT THE RENDERED LAYER, not by reading source. check:poster-print-fallback
clicks the real control with every posters/*.webp ABORTED at the network layer —
the default-drop condition forced rather than hoped for — and asserts the HUD
enters "generating the poster" and settles clean. Mutation-tested: removing the
fallback produces 4 failures including "no posters/*.html was fetched". The other
branch is verified too — with a webp present it is used directly, zero html
fetches, no rasterise — so the extra probe is paid only on a miss.
★ THE GATE WAS WRONG THREE TIMES FIRST, each time blaming the product for a
harness fault, which is why it now asserts the HUD instead of an internal:
- it matched /print/i, but the control reads "UPG prayer poster" — the product
names the ARTEFACT, not the action;
- it patched Document.prototype in the PARENT realm while openPrintFrame writes
into the iframe's own;
- openPrintFrame calls doc.open() AFTER append, and open() RESETS the document,
discarding a write patched at append time.
Through all three the .webp/.html request counters looked corroborating, because
the wall loads both by itself. The HUD is what a user sees, survives a refactor of
the print internals, and cannot silently miss.
The 28 poster sources are committed because the fallback has no input without
them. city-* posters are deliberately NOT covered: they fetch OSM street tiles at
generation time, so they are not reproducible offline and their rasters must stay.
Measured first, as the Oswald lesson requires, and this one was real:
their UupgsList root carries worldmap.svg inline (UupgsList.vue:451),
and it genuinely renders on their live country page — HTTP 200, 62,740
bytes over the wire, computed background-size 60%, position 50% 0%,
repeat no-repeat, min-height 400px. Confirmed by screenshotting the
ELEMENT, not the viewport: the watermark sits below the fold, so a page
screenshot shows nothing at all.
public/assets/images/worldmap.svg is their file byte-for-byte — 311KB on
disk, small against this tree's existing assets, which run 644KB to
42MB. The inline background-image takes its base from a new imagesBase
prop, since a dropped bundle cannot assume their absolute /assets path.
generate-tree's staged allowlist needed 'images' added — the same trap
that silently broke the vendored fonts. A dir absent from the allowlist
404s, and an unfetched background-image is quieter than the font
failure was: nothing paints and nothing reports. So the probe asserts
the REQUEST returned 200 rather than that the CSS names the file.
Then, reading the screenshot after every number was already green, two
more defects:
- A CLASS WITHOUT ITS RULE. The search label uses their class name,
sr-only — the right instinct — but their rule
(utilities/_visibility.scss:2) was never ported, so the class matched
nothing and the label rendered as a visible line of text above the
input, duplicating its own placeholder. Borrowing a class name only
works if its declarations travel with it, and a class matching no rule
is invisible to a property diff. Now 1x1px and clipped, still in the
DOM and still announced.
- A PIXEL CAP WHERE THEIRS IS RELATIVE. .search-box was capped at a
hardcoded max-width:480px; theirs composes .max-width-md, which is
max-width:50ch (utilities/_sizes.scss:11). Wrong value and wrong unit —
ch tracks the input's font, px does not — and ours truncated the
placeholder mid-word. 50ch now resolves to exactly 600px, their
measured width.
Regression: 18 suites green, including a new _probe-watermark.mjs at
15/15.
The pre-bundler upg-neighbor IIFE holds five lost experimental maps --
send-map, upg-neighbors (cockpit), lang-cluster, bloom-cockpit,
flower-mesh. One wrapper bundle now rescues all five as preset
instances, each mounted in a sandboxed iframe whose host page:
- loads our self-hosted MapLibre and aliases it as the mapboxgl
global with a patched Map constructor that rewrites the one
mapbox:// style to keyless OpenFreeMap Positron -- the whole set
runs WITHOUT a Mapbox token;
- shims relative /api fetches to the live API and sets
MAP_APP_API_URL (the cockpit fails closed without it);
- passes the legacy profile-config contract (the elements share the
modern loader lineage).
Wrapper reads profile-config as a PROP (direct-defined element --
inject('profileConfig') is null outside ProfileLoader; the send-map
default masked the miss because send-map is instance #1). The
vocabulary patch rides in the shipped copy ('Lower pressure').
Verified by screenshots read: send-map renders the full 4-D SEND JOIN
app; flower-mesh blooms (India, 2,040 UPGs); the cockpit renders map,
charts and bloom pane. Auto-flows into the SPA's Experimental section
via manifest discovery.
MEASURED before: the region dashboard matched live doxa.life on 61 of 68
computed properties, and every one of the 7 differences traced to a single
cause — Bebas Neue could not be loaded. Three were the font-family itself, and
four were workarounds it forced: an uppercase hack imitating Bebas's caps, and
two font-sizes stepped down because a wider fallback face overflowed their card
widths. So it was never seven problems, it was one with six knock-ons.
DX-life does not ship the file either — @nuxt/fonts downloads and self-hosts it
for them, which is why their computed family reads "Bebas Neue", "Bebas Neue
Fallback: ...". We could not @font-face from their origin: their file is served
without CORS headers, so a cross-origin face fails silently. The font is a
Google Font under the OFL, so it is vendored here from Google Fonts — both
subsets, their own unicode-ranges (css2 v16) — self-hosted, never a CDN, the
same rule public/assets/vendor/tabulator follows.
library/composables/useDocumentFont.js is shared because @font-face is
DOCUMENT-scoped: a face declared inside a shadow root is ignored, so a custom
element cannot register its own webfont through useShadowStyles. That asymmetry
is the reason this is a separate composable rather than a flag on that one.
Idempotent per document, so a composite registers one face for both panes; both
profiles call it because each has a standalone leaf page where the other is
absent.
With the real face loaded, the workarounds are DELETED rather than kept: the
goal-card heading is back to their 59.6046px, the headline figure back to
59.6046px, and text-transform:uppercase and font-stretch:condensed are gone —
Bebas is already caps and already condensed, and stacking those changed the
glyphs. The three-across switcher still holds at 372/372/372 with no overflow,
which is the geometry that forced the step-downs in the first place.
★ THE CATCH THAT MATTERED. With the font vendored the diff said 68 of 68
IDENTICAL, 0 differ — and it was WRONG. document.fonts reported our face in
"error" status: generate-tree stages an ALLOWLIST of asset dirs into the tree,
fonts/ was not in it, so the leaf resolved assets/fonts/... to a 404 and every
heading silently rendered on the fallback. Both sides were agreeing on a
FALLBACK, which is not a match. A font that 404s is invisible to any check that
reads font-family instead of asking the engine whether the family is usable, so
the probe now calls document.fonts.check() and the staging list carries fonts/.
After the fix: usable true, face "loaded", and the headings render in Bebas.
Two honest notes. The fallback TAIL still differs — theirs carries @nuxt/fonts'
generated metric-fallback names we do not produce — so the probe compares the
FIRST family (the face actually used) and reports the tail as an accepted
difference rather than counting it either way. And document.fonts.check()
returns false on THEIR page, which contradicts it visibly rendering in Bebas,
so that check is trusted in one direction only: it proves ours loaded, it does
not prove parity.
The licence ships beside the font, extensionless: check:public-hygiene refuses
*.txt in a served folder and is right to, while the OFL requires the licence to
accompany redistributed font files. The conventional name satisfies both.
Driver-reported console text:
Could not load <doxa-research-map> from ../../doxa-research-map/doxa-research-map.js
A composite declares children as src: '../../<bundle>/<bundle>.js'. That literal
is right from a one-deep leaf and wrong from a two-deep one, and the generator
emits both:
.../research-map-plus-grid/index.html ../../ -> /doxa-maps/ 200
.../research-map-plus-grid/default/index.html ../../ -> /doxa-maps/rmpg/ 404
So the path encoded an assumption about the host's depth that the host does not
control and cannot see. It survived because every gate exercised the shallow
leaf, which is genuinely fine.
StackLayout now derives the tree root from the document's own pathname (up to
and including the /doxa-maps/ segment) and rebuilds the path from there --
depth-independent by construction, so a third leaf depth cannot reintroduce it.
Absolute, protocol-relative and root-relative srcs pass through untouched, and
a missing /doxa-maps/ segment returns the original string, so this can only fix
a path, never break a working one.
Measured, both leaves, real browser with live data:
one-deep mapEl=true mapH=792 canvasH=856 0 4xx
two-deep mapEl=true mapH=792 canvasH=856 0 4xx (was 404, map absent)
Also carries source that had been left behind by its own built output:
the cities/gospel legend bounding (shipped in 5008a6f as output only) and the
per-profile config carrier in generate-tree.mjs, plus a new fits-screen clause
that wheels inside every column rather than one.
Bundles that declare composition in their profile meta.json (composes:
[...]) now carry that array on the manifest's bundle object, so a
consumer like the plugin studio can render the composition tree without
re-deriving it. 7 bundles affected: big-picture, dashboard-plus-grid,
research-map-plus-grid, my-upg-100-list, stack-demo, doxa-maps-spa,
plugin-studio. Omitted for bundles with no composed children.
Checkpointing thirteen files of agent work that were sitting uncommitted while three
agents wrote to a live tree the Driver was watching through hot reload. Uncommitted work
is work at risk, and a tree being edited under an observer oscillates between states that
look like regressions and are really just partial saves.
The second capability-5 shell now mounts its tabs as real elements too, so no iframe
remains in either shell. It was the last holdout: eight tabs, all iframe mode, each
pointing at a hardcoded built-tree leaf that does not exist under the dev server.
The facet rail keeps its shape when a filter narrows it. Selecting one place took the
rail from sixty-six rows to twenty-one — forty-six rows deleted, none dimmed — so the row
a user was about to click vanished under the cursor, which is what "the buttons went
dead" actually was. Rows whose count falls to zero are now re-admitted at their original
slot and depth, dimmed and inert, which also restores the component's own promise to lose
nothing against the tree it replaced.
Expanding a branch and filtering by it are now separate, visible affordances: the whole
row filters, and expand is a real button with its own hit area and label. Rows that were
never clickable are unmistakably inert rather than silently ignored.
Three more facet axes reach the grid — affinity bloc, engagement status and primary
language — each verified to share a field with an existing prop rather than being mapped
on name resemblance. Two derived facets remain unmapped on purpose: they have no matching
row field, and mapping them would filter confidently to the wrong rows.
generate-gallery.mjs:542 requests assets/vendor/vue/vue.global.prod.js, and
generate-tree.mjs staged only vendor/tabulator -- so that request has 404'd
on every build there has ever been.
It survived because loadVue() degrades in three rungs: self-hosted, then the
jsDelivr CDN, then a plain-DOM render. On any networked machine rung 2
silently covers for rung 1, and the only symptom was a 404 in `bun run
smoke`, a gate nobody was running. Good degradation hid a missing build step.
It mattered anyway: the comment above loadVue promises "self-hosted first
(portable / offline / no-CDN)". Measured in a browser with no outbound
network, the gallery fell all the way to the plain-DOM degrade -- usable, but
not the Vue shell, and the offline claim was false. A tree dropped on a
foreign host or opened over file:// is exactly what that rung is for.
Vendored from node_modules/vue 3.5.42, placed in data/vendor/ (the real
source -- generate-tree rmSyncs public/assets/vendor and re-copies from
data/), tracked in both places exactly as vendor/tabulator already is.
Verified: build exit 0 with zero missing-asset warnings; the gallery now
loads Vue 3.5.42 FROM THE VENDORED COPY (not the CDN), the Vue app mounts,
nav renders 5 rows. `bun run smoke` went from 4 failures to 2.
A correction en route: I first blamed `bun run clean` for deleting the file.
clean.mjs only wipes public/doxa-maps, never public/assets -- the real cause
was generate-tree.mjs:1189 rmSyncing public/assets/vendor and re-copying it
from data/vendor, which is why tabulator persists and a hand-placed file
does not.
Two defects in generate-tree.mjs left the only two map-owning composite
leaves marked TOKEN_FREE, so they mounted with no token in profile-config
and survived only on a window.MAP_TOKEN fallback that yields to any
pre-existing global.
- isMapLeaf scans one flat file. research-dashboard.vue has zero engine
references and is the only .vue in its folder, while the real map lives in
src/components/dashboards/DashboardMapPanel.vue. New ownsAMapDeeply walks
src/ for .vue/.js/.mjs and is wired into both the composite path and the
non-composite branch.
- composes entries dropped the bundle segment, so a child from a sibling
bundle was looked up in the wrong directory and silently missed.
Both fixes were needed; resolving cross-bundle alone still reports map=false
because the map is a directory deeper.
Negative-tested before wiring in: across every bundle, research-dashboard is
the only profile whose answer changes. Post-build check confirms the seven
genuinely map-free profiles stay token-free. embed suite 40/40, exit 0.
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.
docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.
The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.
Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
- join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
- tauri frontendDist '../public' -> '../../public', since src-tauri is now one
level deeper; check:tauri caught this exactly as the card predicted
- the WordPress contract test's dirname(__DIR__,2) -> 3, same reason
Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.
Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.