The host page of every full-page map (and the Plugin Studio) sized the
element to the viewport in a way that tracks the phone's address bar:
- the built fixed-viewport leaf used position:fixed with inset:0, which
follows the DYNAMIC viewport, so the map and its legend sheet resized
on every scroll that moved the bar;
- the Plugin Studio page and the dev server's leaf and studio pages used
100vh, the LARGE viewport, taller than the view while the bar shows, so
the page itself scrolled by the bar's height and the app seemed to
resize.
All four now use height:100svh (100% first as the fallback where svh is
unknown), anchored top-left, and set --profile-min-h:0 so ProfileLoader's
100dvh floor cannot grow the element when the bar hides. On a desktop svh
equals the viewport, so nothing moves there. Scroll-page leaves (the
cap-4 stacks) are unchanged.
Regenerated the tree: 51 leaf pages and the Plugin Studio page change
only this rule and its comment; no bundle changes.
The README says a plugin folder holds "an index.html for previewing it", and a fresh reader
followed that to /plugin-designs/<plugin>/index.html under `bun run dev`. leafRewrite reads any
<a>/<b>/index.html as <bundle>/<profile>, so the request reached the leaf route as bundle
"plugin-designs" and answered only {"error":"unknown bundle"}, which pointed nowhere.
The 404 now carries a hint: for a folder that is a plugin, its own page
(/__leaf/all-people-groups/all-people-groups, taken from the same registry the gallery uses);
otherwise the /__leaf/<plugin>/<profile> shape and the manifest that lists every plugin. The
status and the error stay as they were. The README now names the page as well.
Every reference to the Driver in internal/ is reworded the same way as in
library/: the reason stays, the speaker and the quote go, an open decision
reads "provisional, pending a product decision", and agent names and decision-
sheet rows are dropped. Where a gate printed the word, its message now names
the reviewer or the ruling instead.
Three spots were code, not comments, and changed with their callers:
- the element census's allowlist field `driver` is now `ruling`, in the gate
and both presets (same validation: match, ruling, date and card required);
- check:i18n-never-translated's DRIVER-GATED marker is DECISION-PENDING, in
the entries and in the regex that counts them (its output is otherwise
byte-identical);
- the package.json description of the census says "ruling".
public-hygiene-check keeps its two patterns for the word: they are what keep
it out of the served folder.
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 copyable script snippet 404'd on bun run dev for every production map, while
the iframe snippet worked through leafRewrite. /<b>/<b>.js and /<tree>/<b>/<b>.js
now serve the built IIFE from public/<tree>/; a bundle not built yet gets a plain
404 that says to build it, never the HTML fallback.
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.
A shared dev server follows main, so what it serves changes under whoever checks against it; a check
that cannot name the commit it measured is not evidence. /__dev-sha answers "<sha>" (or "<sha>+dirty"),
read at request time. internal/dev-server.mjs: url(), devSha(), waitForSha(sha) (resolves once the
served commit contains it) and a CLI, for headless checks against the shared server on 127.0.0.1:5190
(DOXA_DEV_URL overrides). Its header says when NOT to use it: an unlanded change, and release/embed
checks, which stay on the built tree.
(cherry picked from commit e43310e479)
Reported from a laptop running `bun run dev`: under the Plugin Studio, maps drew with no boundary
polygons and UPG-100 had no posters. The built tree was fine, and every gate measured the built tree.
Cause: a dev leaf page is /__leaf/<bundle>/<profile>. A plugin that falls back to the page-relative
'./assets' asked for /__leaf/<bundle>/assets/…, and the leaf route answered with its own HTML at
HTTP 200, so JSON.parse failed and the map drew without our data. /<tree>/assets/ was not served
in dev at all. The built leaf never hits this because its page passes assetsBase "../../assets".
- dev-plugin serves the pool at /__leaf/…/assets/ and /<tree>/assets/ too, and the dev leaf page
passes assetsBase "/assets" in its profile-config, as the built leaf passes its own.
- resolveAssetsBase's last rung is '/assets' under the dev server (import.meta.env.DEV, false in
every build); the built tree keeps './assets'.
- check:dev-assets (headless): starts a short-lived dev server for this checkout, requires
boundaries/previews/fonts/images/tables to answer 200 and not HTML at every prefix a dev page can
produce, and requires the dev leaf to carry the root-absolute assetsBase. It fails on the parent
commit (4 prefixes and both leaves) and passes here.
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.
Two gaps found by opening every plugin in the studio under the dev
server:
- key-cities is gated (vite.doxa-maps-build-config.json gatedProfiles)
out of every public surface the build emits, but the dev manifest
listed it anyway, so the dev studio and the Research Gallery's
Experimental shelf showed a plugin the built tree deliberately hides.
The dev model now applies the same gate: 21 bundles, 33 profiles, as
in the built manifest. readBuildConfig() does not carry gatedProfiles,
so the gate is read from the config file, as generate-tree does.
- golden-path-check takes its tree base from a tree-base attribute, not
from profile-config. The studio only passed the latter, so inside the
studio the check guessed the tree from the page URL, fetched
/manifest.json, got a 404 and showed an empty self-test. The studio
now sets tree-base on every element it mounts; elements that do not
read it ignore it.
Dev sweep after: all 31 listed plugins mount with content, no page
errors, no failed requests. Rebuilt the plugin-studio bundle and
re-baked its SRI.
Opening every plugin in the studio under the dev server on a fresh
worktree, 13 of them mounted with empty data: every table, boundary and
people-groups file 404'd. Two causes.
- The dev asset lookup read public/assets (a mirror generate-tree
writes, absent on a fresh clone) and plugin-designs/*/data, but never
data/, the tracked source that mirror is copied from. It now falls
back to data/<rel>, which is what DX gets when they clone.
- Plugins nested in the studio resolve assets against the tree base the
studio hands them (/__plugin-gallery/), as they do against <tree>/assets
in the built tree, but only /assets/ was served. Both prefixes are
served now.
After: the same sweep shows the data 404s gone from all 13.
The rail now has two labelled parts. "Every plugin, by capability" keeps
one node per plugin in collapsible CAP 5 -> CAP 1 sections. "What each
composite is made of" adds one collapsible section per composite, headed
by the composite, with every element nested into it drawn as an indented
tree at full depth, ordered by capability at each level. A child repeats
in every composite that nests it, as specified.
Repeats are aliases, not new nodes: clicking people-groups-grid under
big-picture selects the same canonical entry as clicking it in CAP 2 and
highlights every copy, so each plugin still has exactly one name. Arrow
keys walk only the canonical rows. A declared child that is published
nowhere (big-picture's bp-research-header) shows as a "part" row, so the
header/grid split inside Big Picture can be named.
The dev server's manifest never carried `composes` edges, so under
`bun x vite` the Composed-of panel was empty for every composite. It now
publishes them from the same meta.json source as the built manifest.
Also: bare sibling profile refs (gospel-resources composes "data-tables")
now resolve, and rail rows use local SVG icons per capability instead of
emoji that rendered as empty boxes without an emoji font.
VerticalTabsLayout gains two optional row fields (`selects`, `depth`) and
shows badge/count on folder headings; existing consumers set neither.
A copied embed URL — <bundle>/<profile>[/<instance>]/index.html, the shape the
manifest advertises and a person actually pastes — had no route on the dev
server, which served its leaf stand-in only at /__leaf/<bundle>/<profile>.
It failed silently, which is the expensive part: with no route, Vite's SPA
fallback answers an unknown path with the studio's HTML at status 200. The
reader asks for one map and receives a different, working page, with no 404 and
nothing in the console.
leafRewrite() maps the embed shape onto the existing /__leaf/ renderer before
any branch runs, so there is one renderer, not two that can drift — the point of
the route is that one URL renders the same thing in dev and in the built tree.
Both the bare tree-relative form and the /<treeName>/-prefixed form are accepted;
the tree name is read from the build config rather than hardcoded. Traversal,
encoded traversal and malformed escapes fall through untouched, and an unknown
bundle keeps the leaf route's 404 JSON rather than becoming HTML at 200.
Verified: /doxa-simple-map/doxa-simple-map/index.html and
/__leaf/doxa-simple-map/doxa-simple-map return byte-identical bodies (742 B);
the prayer URL mounts doxa-map at 1280x800 with a live canvas in a real browser.
Five lanes had 465 insertions across 18 files sitting uncommitted, some for
15 minutes. That is the state that silently lost the shell-scroll fix earlier
today: a verified fix existed only in the working tree, a second agent's edit
to the same file overwrote it, and the reverted version got committed and
described as the fix.
Nothing here is finished and no card is being closed on it. This is a
snapshot so the work survives an overwrite, taken at a moment when every file
had been quiet 15s or more, so no agent was mid-write.
Verified coherent before staging rather than committed blind: template-literal,
mjs-parse, sfc-parse, no-orphan-gates, kpi-unit, composite-reg, script-flags,
quoted-scripts and backlog-status all PASS. check:synced-css fails on
PeopleGroupDetail.vue and doxa-simple-map.vue only -- the same two files and
the same counts as before the swarm started, both untouched by any agent, both
already carded.
Two untracked files included because both are load-bearing: internal/
check-facet-one-field.mjs is wired into package.json with a self-test, and
library/utils/viewportScrim.js is imported by DataTable, SemanticTreeLegend
and PosterDialog -- a shared helper for the modal-scrim fix rather than six
copies of the same workaround.
Pins keep their navy but gain a white ring and a radius that shrinks as you zoom out, so
neighbouring dots separate instead of merging into one blue mass. The Driver asked for
rings rather than clustering, explicitly to avoid more logic.
The top header no longer carries selection state. That belongs under the filter toggles,
in the second header, and nowhere else.
The filter menu is anchored to its trigger again. It had been moved to fixed positioning
to escape a scroller that was clipping it out of existence, and viewport coordinates go
stale in ways that put the menu somewhere unrelated to the box that opened it.
A gallery row now carries its identity as an attribute. A Vue key is a reconciliation
hint and is never rendered, so the smoke check read undefined and reported a broken deep
link on a page that works — measured zero occurrences of that attribute in both the
generator and the built page. Asserting on the visible label instead would have coupled
a machine check to display text.
That repaired gate immediately earned its keep: it caught the gallery asking for assets
one directory too deep. The relative asset base was correct while the gallery was the
root page and silently wrong once it moved aside for the studio, and the failure surfaced
as a JSON parse error rather than a 404, because an unknown path answers with HTML.
The built leaf pages set window.MAP_APP_API_URL; the dev pages set only the Mapbox
token. Every dashboard in dev therefore fell back to a relative /api/people-groups/list,
which Vite answers with SPA-fallback HTML at HTTP 200 — so the fetch "succeeds", the
parse yields nothing, and the facet rail renders zero rows with no console error and no
error card. One studio sweep produced eight such requests. It reads as a broken
component and is really a missing global.
Both dev routes now read the value from the same build config the built leaves use, so
the two surfaces agree by construction rather than through a second hard-coded copy, and
each route resolves the token and the API base from a single config read so they cannot
disagree about their source. Measured after: the research dashboard renders 66 facet
rows and 216 table cells with zero 4xx.
EVERY TAB FAILED TO LOAD. Introducing tree-base for the dev server prefixed it onto
URLs that were already rooted, producing /__plugin-gallery//plugin-designs/<b>/index.js
and a 404 on all 29 tabs. Both capability-5 shells had byte-identical resolve()
implementations, so both carried the same defect; both now return an already-absolute
ref untouched and join through exactly one slash. The built tree hands relative refs
and keeps its base, so this is one rule for both trees rather than a dev-only branch.
THE STUDIO NO LONGER FRAMES ANYTHING. Map elements were nested in iframes pointing at
their standalone leaf pages, to dodge a one-shot customElements.define collision. That
collision cannot occur here: 19 bundles claim 19 distinct tags, none shared, and two map
elements were proven to coexist in one document with the first unaffected by the second.
Every pane now mounts the plugin's real custom element, which is what makes a row both
reviewable and nestable.
Panes keep two lifetimes, and the difference is the WebGL context budget rather than
taste. Light rows stay mounted so switching back is instant and state survives. Map rows
are destroyed on deactivate, because a hidden map holds its GPU context: the browser
caps live contexts, then silently reclaims the OLDEST, so a map visited ten tabs ago
would go blank with no error anywhere. getContext never returns null and contextlost
never fires on the new canvas, which is what makes that failure so hard to attribute.
The dev server serves a leaf route for one element with no chrome, and passes the tree
base to nested shells so they discover the live tree instead of a baked fallback.
THE DEV SERVER LANDED ON THE WRONG PAGE. `bun run dev` served only the staging
gallery at /, and had no route for a bundle leaf, so there was no URL at which the
capability-5 plugin studio could be seen at all. Opening / showed plugins wrapped in
per-bundle frames behind a wall of explanatory prose, which reads as "none of the
elements are working" when in fact the elements were fine and the page was a chooser.
/ now mounts <plugin-studio> directly: a 605-byte host page with no chrome and no
prose, the element filling the viewport, and the Mapbox token baked as the token
ladder's first rung so maps work without anyone appending ?tk=. The gallery is not
deleted — it still generates the built index and is load-bearing for the manifest —
it simply moves to an explicit /__gallery URL and stops being the landing surface.
tree-base is passed explicitly, without which the studio mounts EMPTY. It builds its
tab list by fetching <base>manifest.json and falls back to a path relative to its own
script URL, which in dev resolves to a file that does not exist. The dev server serves
the same manifest-shaped model on its own route, so dev now shows the same tab list as
production: 19 bundles, 30 profiles, five capability groups.
PLUGIN FRAMES SHOWED THE GALLERY. The studio nests map profiles in a lazy iframe
pointed at that profile's leaf page, and dev had no such page, so each frame fell
through to the SPA fallback and rendered / — the gallery, recursively, once per map
tab. A /__leaf/<bundle>/<profile> route now serves the dev equivalent of the built
tree's leaf: one element, its bundle entry, the token, and nothing else.
- internal/playground → internal/plugin-gallery; playground-smoke → plugin-gallery-smoke;
generate-spa → generate-gallery (scripts: gallery, smoke:gallery; smoke:playground stays as alias)
- generate-gallery exports renderGallery(model, { mode }) — the dev plugin serves the same page
at / from the discovered registry (dev mode imports the dev entries, ladder popovers on);
the build writes public/index.html from the manifest (byte-identical to before)
- Docs and probes use the gallery vocabulary