The Driver wants MapLibre available without breaking Mapbox: keep every Mapbox
component working, add the option beside it, start with this dashboard.
The change is deliberately additive. provider defaults to mapbox, so with no
config the behaviour is what it was and nothing Mapbox-shaped can regress. A host
opts in through dashboardConfig, which is how every other dashboard setting
already arrives, so the cap-4 composites can pass it down with no new mechanism.
The plumbing already existed and was simply unreachable from here: useMapInstance
takes a provider and loads the matching engine, and the style registry maps every
named style to both engines with the MapLibre side pointing at keyless URLs. This
call passed neither provider nor style, so it silently took the default.
The token stays wired when MapLibre is selected. MapLibre ignores it, and keeping
it means switching back is one word with nothing else to restore, which is what
hot-swappable has to mean to be worth anything.
Two composites nested a child by tag and URL without a profile-config. Those
children do not render a component directly — their bundle entry mounts
ProfileLoader, which reads profile-config.profile to choose which view to load.
With none, the element still mounts and still reports its tag, so every
structural check said both children were present, while the loader rendered
"missing or invalid profile-config prop" where the map should be. The grid half
of each page worked, which is how it survived.
Found by reading a regenerated README screenshot, not by any gate. The mobile
screenshot showed an error card and an empty pane; the fixed one shows the real
research map with its data, controls and progress figures.
research-map-plus-grid was mine, shipped that way. stack-demo predates it and had
the same defect against research-dashboard. Both now pass the shape their own
generated leaf pages use, so nested and standalone mounts ask for the same thing.
check:composite-srcs now requires a profile-config on any child whose bundle
mounts ProfileLoader, derived from which entries import it rather than a list.
The first version of that check tested for the word and passed on its own
explanatory comment; it now matches the quoted attribute key, and a negative test
that mutates only the key fails as it should.
Also regenerates three derived artifacts that had drifted behind this flight's
changes: the library index, the capability exemplars table, and all twelve README
screenshots.
The file header said tab 1 was bp-big-picture, mounted directly via its real tag.
Both halves are false: tab 1 is dashboard-plus-grid, and every tab is an iframe —
there is no direct-tag mount left in this shell. A reader trusting that text would
look for a mount path that does not exist and maintain the wrong bundle believing
it was the one users see. The big-picture bundle is still a valid element; it is
simply one of the parts already composed into the tab-1 composite.
The locale switcher also poked slot zero by hand, writing a profile-config named
big-picture onto what is now an iframe, which ignores it. Harmless, and that is
why it survived: the effectiveLocale watcher was doing the real work all along,
branching on the tab's mode and re-pointing the iframe src that carries the
locale. Removed rather than repaired, since a second hardcoded locale path is one
more thing to keep in sync for nothing. Verified by driving the switcher: the
active frame moves from locale=en to locale=es.
The direct-tag branch in the template is kept. It is generic, keyed on the tab's
mode rather than a bundle name, and remains the right way to nest a future
token-free element. Only the comment claiming tab 1 uses it was wrong.
The Driver's ordering: desktop opens on the Big Picture dashboard; mobile opens
on the research map and does not show Big Picture at all. Both capability-4
composites already existed, so this is the shell choosing between them. A row in
the nav model can now declare onlyWhen desktop or mobile; everything below the
first rung is identical on both, which is why only the first two rows carry it.
One nav model rather than two, so the teaching order lives in one place. Two
parallel models would be two things to keep in sync and the failure would be
silent: a row added to one and forgotten in the other simply never appears.
Crossing the breakpoint can remove the row that is currently active, which would
leave the user on a mounted but unlisted tab with no highlight. The shell now
snaps to the first surviving row, and establishes the viewport class before
opening the first tab so a phone never opens Big Picture for a frame.
Also fixes a pre-existing bug this exposed: with no header there was no way to
open the rail on mobile. The drawer sits off-canvas, the only toggle lives inside
the header, and the collapse control is inside the drawer itself — measured
off-screen at x:-244. Every tab except the one that happened to open was
unreachable. A floating button now appears only when narrow, headerless and shut,
so no existing consumer can see a change.
posterUrl() said .png. There are 378 poster .webp files and no matching .png, so
the helper could only ever have produced a 404 — but it was never called, because
the export and print paths each built the same URL inline. A dead helper carrying
a wrong constant is worse than no helper: it is primed to break whoever tidies up
by routing a call through it. Point it at .webp and route both callers through
it, so the format lives in one place.
The file header still said posters "DO NOT EXIST YET. Nothing has been generated."
378 previews are generated, shipped and served live, and the wall paints from
them. A reader following that line would go build a generator that already ran.
Replace it with what actually ships: previews by default, full-resolution posters
excluded because bundling them pushed the tree past what the catalog would accept.
The desktop composite pairs the research dashboard with the grid. This is its
mobile sibling: the monolithic 7-tab research map on top, the same grid beneath,
stacked to scroll. Which one a shell shows is an aspect-ratio choice made at the
SPA level, so neither plugin knows about the other and both stay embeddable.
The grid's own search and filters are ON here. The desktop composite turns them
off because the dashboard header owns those controls and drives the grid through
props; the research map has no such header, so switching them off would leave the
grid with no way to be searched at all.
Composes its children by tag and bundle URL rather than importing their .vue
files. Importing the research map directly would bypass its bundle entry, which
is where its three bundle-private colour strategies, its profile registry, its
Pinia and i18n pair and the RTL text plugin are installed — and losing those is a
runtime defect in a green build, because a thrown setup() removes the subtree
with an empty error.
Add check:composite-srcs, which resolves every nested src against the built tree
and asserts every nested tag is registered. A wrong relative depth is a 404, and
the stack layout swallows a failed section on purpose so one broken child cannot
take the whole stack down — which means the cost of that resilience is a silently
empty band. Negative-tested both ways: breaking the depth and renaming a tag each
fail with the file and path named; restoring returns to pass.
The north star is that an admin agent can author a plugin, build it, publish
it and see it live. Every step was documented and individually verified, but
never walked in one pass by following the doc as written. This plugin is that
walk, authored from the documented three-file minimum.
It stays useful afterwards: it reads the manifest.json it was published
beside and reports what the catalog actually contains -- bundle and profile
counts, how many profiles describe themselves, and whether it is listed. If
you can see it answering, the path worked. Deliberately dependency-free: no
map engine, no token, no data API, so it can mount when everything else is
broken, which is when someone reaches for it.
The walk found a real defect in my own plugin, which is the point of doing
it. The first version wrapped the element in ProfileLoader (copied from a MAP
bundle) without createPinia. ProfileLoader builds three stores in setup(), so
setup threw -- and a thrown setup removes the subtree silently. Build green,
manifest listed it, nothing complained. Only smoke:embeds caught it: "shadow
DOM nearly empty (1 nodes)". That is exactly what Step 3b exists for and why
the doc puts it before publish.
The fix is the cap-1 pattern doxa-vision-card already uses: mount the root
SFC directly. ProfileLoader earns its cost for a map plugin; it is the wrong
wrapper for an element that draws no tiles and takes no token.
Verified: 17 -> 18 bundles, 26 -> 27 profiles, auto-discovered with no config
edit; renders 19 shadow nodes reporting live catalog state; embed suite 41/41.
More than half the catalog had an empty description in manifest.json, so the
plugin-studio — the surface a Doxa inheritor browses to learn what exists —
showed flagship plugins as a bare "bundle - <tag>" with no explanation, while
a demo bundle explained itself fully.
Cause: the generator reads a JSDoc @description TAG, and these files carried
only prose. Every description added here is a summary of what that file's own
header already documented, not new invention. Claims were checked against the
artifacts: doxa-simple-map's "three presets" was verified against
simple-map.instances.json (prayer, engagement, adoption).
The two cap-5 shells needed a second pass. Their headers are HTML comments
with no leading asterisk, and the real TAG_RE requires `* @tag value`, so the
first attempt extracted nothing.
Result: 26/26 profiles described, 0 empty; the built gallery went from 15
empty / 11 populated to 0 / 26. Verified in a browser, not just in the
manifest. Embed suite 40/40.
Both country-posters profiles had a prose header but no @description tag, and
the generator reads the TAG, not the prose. It emitted an empty description
into manifest.json, so the cap-5 SPA fell back to its generic label and the
Experimental rail showed a nameless tile — the only bundle in the app that
did. A Doxa inheritor browsing the SPA met "experimental bundle".
Added @description (and the @element tag both files were also missing, which
validates against the build's choke-point rule) to countries-map.vue and
country-poster-wall.vue, with text grounded in what each profile does.
Verified at the symptom, not the intermediate: the rendered rail now shows
the real description and the placeholder count is 0. Embed suite 40/40.
Two Driver-reported defects.
1. No more "Basemap tiles blocked" popup on the map. The banner reported an
operator problem (one Mapbox account setting on one deployment origin) to
the end user, who can do nothing about it, on every map and every load. The
console diagnosis stays, because it reaches the person who can act.
2. Selections in the dashboard now filter the grid beneath it. The two halves
were never connected: research-dashboard had no defineEmits and its bus is a
per-instance factory that is never provided, while dashboard-plus-grid passed
the grid no filter props. The grid's receiving end already worked. Added the
emit, plus a translation layer between the dashboard's snake_case facet keys
and the grid's camelCase props, mirroring big-picture.
Proven live: typing "Turk" takes the grid from 24 cards to 22 with wholly
different rows. Only 4 of 12 facets map; the rest are dropped rather than
guessed, because religion_family carries a family name while the grid's
religion prop matches a code, and mapping them would filter to zero rows.
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.
The comment claimed a three-part fix including 'a thin white inner ring for
contrast'. No such property was ever added, and a mapbox circle layer has a
single stroke, so no inner ring is possible on this layer. A reader trusting
the comment would look for a second layer that does not exist.
Verified live against the running map rather than the source: the upg-pins
layer reports stroke-color rgba(15,20,25,0.9), opacity 1, and stroke-width
interpolating 1.2 -> 2 across zoom. The fix is real; only its description
was wrong.
A previous sweep found six dark matches, correctly ruled five of them unreachable, and
fixed the one real violation in doxa-simple-map. Re-running it with a luminance test
rather than a hex prefix found more, because the earlier pattern also matched the green
accent #10b981 and so buried the genuine surfaces in noise.
Three further unconditional violations, all the same shape as the one already fixed: the
loading overlays in all-people-groups, country-posters and doxa-research-map each render
on every load via v-if="!appReady", so a near-black flash was the first thing a visitor
saw. All four now use the palette surface with muted text.
The template-bundle starter was also dark in both profiles. That one matters more than a
leaf: the documented recipe is to copy template-bundle and rename it, so every future
plugin would have inherited a surface the house style forbids. The two profiles stay
visibly distinct — a tinted surface with an accent border, b white with a neutral one.
Verified by reading the shipped rule out of each built bundle: 4 of 4 found, all
#f3f3f1 at luminance 243. The found-count is reported so a run that matches nothing
cannot read as a pass — an earlier version of that check found zero rules and printed
ALL LIGHT.
Deriving the denominator from perZone.size fixed the impossible 27/26 but left a
permanent 27/28, which reads as "one zone is broken". I carded it as a bug on exactly
that reading. It is not a bug.
resolveAllZones deliberately excludes the deaf zone from the region-fill tapestry and
records zero for it, because its A017 affinity selector double-stamps land already owned
by the 13 geographic zones — 16 of oceania-indonesia's 18 Pacific ISO3 codes alone. That
double-stamp showed deaf twice and underlapped every other region. Deaf stays complete in
the list, the legend and the pins; only the polygon fill is withheld.
So a by-design exclusion was being displayed as a shortfall. Counting only zones that are
meant to have geometry makes the badge report 27/27 when everything is healthy, and fall
below its total only when a zone genuinely fails to resolve.
Verified: 28 leaves minus 1 deaf is 27 expected, 27 resolve, and the leaf now shows
195 polys · 27/27 zones · 2107 pins. The composite does not display this badge at all,
which a first probe misread as a failure.
The Driver asked for the top-header search bar to be more rounded. It is now 16px, which
is exactly half its measured 32px height, so the ends are true semicircles. Any larger
value renders identically because the browser clamps to height/2, making this the actual
maximum rather than a number picked by eye.
Scoped to the top header only. The card's scope field named the shared SearchBar
component, but that component's own 10px is not what renders here: measuring the computed
style showed 6px, because DashboardTopBar overrides it. Editing the library would also
have changed people-groups-grid and big-picture, which was not asked for.
Verified 6px to 16px at two viewports, confirmed as a full pill, with the magnifier icon
still clear of the curve at a 10px inset.
The Driver rejected v1, which added the hint as its own paragraph above the grid: "you
dont have to add a SECOND HEADER to make this show up... it could be CENTRED on the same
header where it says total 2107." So it now rides the bar that is already there, beside
the live count.
Centred by absolute positioning against the bar, not by flex spacers. The first attempt
here used a spacer on each side and measured 55px off centre at both 1440px and 1280px,
because spacers split the leftover space rather than the bar, and the left cluster
(dock, title, count, selection controls) and right cluster (toast, Export CSV) differ in
width. Absolute at left:50% with translateX(-50%) is relative to the bar itself, so the
clusters cannot drag it, and being out of flow it adds no height by construction.
The bar gains position:relative to be that containing block. pointer-events:none keeps
the hint from intercepting a click meant for a control beneath it, and it hides below
1100px where the clusters leave no free middle.
Measured against a pre-change baseline of 32px: 32px at all three viewports, one header
bar, and centre offset 0 at 1440px and 1280px.
The badge read "27/26 zones" — a count exceeding its own total — in two shipped README
screenshots. 26 was the leaf count before slice 09b-6 added the Frontier-Asia uncovered
rows; the literal was never updated, so it drifted the moment the data grew. The
denominator is now derived from perZone.size, which tracks the zone list automatically
and cannot disagree again. Measured from clusters.json: 13 regional + 12 asia + 2
uncovered + 1 deaf = 28. The corrected badge also surfaces 1 genuinely unresolved zone
that the stale denominator was masking.
legend-tree.js asserted "33 leaves" and "7 Frontier-Asia uncovered" in a comment; the
shipped data carries 28 and 2. Corrected to point at the arrays instead of a count.
README showed "List — page 1" and "List — page 2" as the same picture. The shot loaded
one URL twice and tried to reach a page 2 by scrolling and clicking Next; neither exists,
the list has no pagination, and every attempt sat in try/catch so the miss was silent.
The PNGs differed by 0.16% render noise, so no checksum would have caught them. Mean-
while poster-wall — a real manifest-listed profile — had no screenshot at all. Shoot the
real third thing rather than fake a fourth.
check:shot-distinctness gates this, blocking in CI. It keys on url+viewport, so the
deploy2 mobile shot of the same SPA stays legitimate rather than a false positive.