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