Last tick fixed 15 of 26 profiles that shipped with an empty description.
Without a gate that fix has a half-life: the north star is "develop a plugin
and see it live", so new plugins arrive often and each one is a fresh chance
to regress this silently. The repo already validates @element, but only
against a MALFORMED value — an ABSENT @description fails quietly, the
generator writes an empty string, every gate passes, and the defect only
surfaces to a human who opens the studio and reads.
Reads the BUILT manifest, not the .vue sources, on purpose. Re-deriving the
description here would mean reimplementing generate-tree's TAG_RE — which is
exactly what hid two of the fifteen last tick, when a hand-written copy of
that regex reported success while the real rule extracted nothing. Check the
artifact the consumer reads, never a reimplementation of how it was produced.
MIN_CHARS is calibrated against the real corpus, not guessed: shortest
genuine description 62 chars, median 81, longest 192, so the 25-char floor
sits clear of every legitimate entry while still catching a stub.
Negative-tested four ways, including end to end: deleting a @description tag
from a .vue source FAILS THE WHOLE BUILD (exit 1) naming the profile and the
file to edit; restoring it returns the build to green. Also fails on a short
stub and on a near-empty manifest (the vacuous-pass guard), rather than
passing for lack of anything to check.
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.
Eight of the twelve named styles resolved their MapLibre URL to
basemaps.cartocdn.com. Those are keyless today -- verified live, style JSON
and a real vector tile and a font all 200 with no token -- but CARTO's own
docs reserve the right to extend the key requirement to vector tiles. That
is a future silent outage already shipped, and the same single-point-of-
failure shape we are trying to escape by leaving the URL-restricted Mapbox
token. A dependency that is free today and gated tomorrow is a deferred one.
dark-matter -> openfreemap dark (midnight-glow, dusk, ink, wayfinder-night)
positron -> openfreemap positron (paper)
voyager -> openfreemap liberty (satellite, hybrid, wayfinder-day)
Each replacement verified before the swap: 200 keyless, zero keyed URLs in
sources/glyphs/sprite, real tiles serve. Built output now has zero CARTO
references. Embed suite 40/40 pages mount cleanly.
The header records the bar for adding a style here: keyless, commercial use
permitted, self-hostable. MapTiler and Stadia free tiers fail the second.
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.
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.
Found by the folder-scan agent and verified by simulation. This gate resolves
ROOT at depth 2 while all 40 sibling gates use depth 1, because the backlogs it
reads live in _qa-cycle/ - agentic scratch OUTSIDE the repo, tracked by no git
repository and never cloned with the studio.
On this machine _qa-cycle happens to sit beside the repo, so it passes. An
inheritor who clones only the studio got something worse than a failure: it
printed "0 card(s) across 3 backlog(s), 0 done ... PASS" and exited 0. A
vacuously green gate certifies nothing while looking healthy.
Now it detects the absent directory and SKIPs with an explanation, still
exiting 0 so it cannot break an inheritor's build. Negative-tested both ways:
274 cards still checked here, honest SKIP in a bare clone.
ROOT CAUSE, proven by interception rather than inference. A COMPOSITE leaf whose
own root paints no tiles is generated TOKEN_FREE, so its page calls mount('') and
the "tk: token" assignment never runs. Intercepting setAttribute BEFORE Vue could
consume it showed profile-config arriving with keys profile/dataSource/
instanceId/assetsBase and NO tk. ProfileLoader then computed '' and provided it
to every child, so a map nested INSIDE the composite got an empty accessToken and
logged "No Mapbox access token provided" - the error reported at
dashboard-plus-grid.js:605.
The generator assumed a composite's nested maps each render in their own iframe
leaf running its own token ladder. That is false for dashboard-plus-grid: zero
iframes, and ResearchDashboard / PeopleGroupsGrid are imported as COMPONENTS, so
the nested map runs inside the shell page and needs the token the shell withholds.
ProfileLoader now falls back to window.MAP_TOKEN when config.tk is absent.
STRICTLY ADDITIVE: 10 components inject mapboxToken and today all receive ''
whenever tk is missing; now they receive the real token. An empty string becoming
a valid token cannot break a map that already worked.
Verified: the nested map inside dashboard-plus-grid now carries a real
accessToken instead of ''. Negative-tested both ways - an explicit ?tk= still
WINS over the global (per-instance precedence intact), and with no token anywhere
the code still logs "No Mapbox access token provided" rather than silently
pretending. Full build ALL GUARDS PASS, SRI consistent.
This does not make the generator's classifier correct; see
BUG-COMPOSITE-LEAVES-ARE-TOKEN-FREE-BUT-NEST-REAL-MAPS for the deeper fix.
A console message only helps someone who already suspects a problem and opens
devtools. The reported experience was "I clicked the map and saw nothing", so
the page itself must say why.
The leaf's own #status node is not usable here: done() removes it the moment the
element mounts SUCCESSFULLY, which is exactly what happens in this failure -
mount succeeds, only the tiles 403 afterwards. So the composable creates its own
small banner instead.
A CORNER banner, never a full-cover overlay: the data layer, legend and labels
all render correctly and stay useful. Hiding a working map to report a missing
basemap would trade one blank screen for another.
TOP-LEFT was chosen by LOOKING at a screenshot, not by geometry. A first attempt
at bottom-left rendered underneath the legend panel and was unreadable, while
every numeric check still passed - 442x70px, correct palette, 3% coverage.
The other corners are taken: controls top-right, attribution bottom-right, hint
pill bottom-centre.
Material light only: #fff surface, #3b463d text, #d1d4cd border, #6d766e muted.
No red - this is a configuration notice, not a crash. pointer-events:none so it
can never block map interaction, and the container is only set position:relative
when it is still static, so a profile that positions its own container keeps it.
A URL-restricted Mapbox token returns 200 for the style document AND the fonts,
and 403 only for tilesets. So the element mounts at full height, the legend and
labels paint, the data layer paints - and the basemap silently never arrives.
It reads as a rendering bug in our code. It is not: it is one account setting.
Nothing listened for it. GL-JS fires an "error" event carrying e.status on every
failed tile request, so the information was always there - no one asked. That gap
cost real debugging hours this flight and is the first thing an inheritor
deploying to a NEW domain will hit.
The listener attaches after both engine branches converge, so it covers Mapbox
and MapLibre with one hook, and is wrapped in try/catch so an engine without an
.on() method can never break map creation.
ONE MESSAGE PER MAP, not per tile: a blank basemap 403s every tile in the
viewport, dozens per second while panning, and an unguarded console.error would
bury the page and train the reader to ignore it.
Verified both directions: a restricted map produced 9 tile 403s and exactly 1
diagnostic; the token-free maplibre profile mounted 800px and emitted 0.
Full build ALL GUARDS PASS, SRI consistent.
The Tauri README said the bundles 'self-host mapbox-gl from public/assets/vendor/
(the no-CDN rule)'. Measured: vendor/ contains only tabulator, and every leaf
carries src=https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js. MapLibre
likewise loads from cdn.jsdelivr.net. The ENGINES registry in
library/composables/useMapInstance.js is the authority.
This mattered because the sentence sat next to CSP guidance: a script-src
written from the old claim would have blocked the engine and produced a blank
map - the exact symptom being debugged elsewhere.
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.
check:catalog-identity hashes each live bundle against the CATALOG'S OWN
manifest - deliberately, per its header. That proves the served tree is
internally consistent; it can never prove it is current. A catalog three
publishes behind is perfectly self-consistent and passes clean.
Measured today: after baking the Mapbox token, catalog-identity reported '17 of
17 identity-proven - PASS' while every live leaf still served BAKED_TOKEN = ''
and every map on the platform was broken.
The two gates answer different questions and neither subsumes the other:
identity is the live catalog consistent WITH ITSELF? (tamper/corruption)
currency is the live catalog the SAME BUILD as ours? (staleness)
It checks BOTH bundles and leaf pages, because a bundle-only version of this
gate also passed: baking the token changed 37 leaf HTML pages and ZERO .js
bundles. generate-tree owns the leaves, vite owns the bundles, and a publish can
change either independently.
Current reading: bundles 17 current, leaves 17 STALE.
Negative-tested: unreachable catalog SKIPs (offline must not fail a build), a
manifest yielding 0 integrity values FAILs rather than passing vacuously, and a
missing local build SKIPs.
The generated leaf pages carry BAKED_TOKEN, so they change whenever the
build-config token changes. public/doxa-maps is committed as part of the
product, so shipping the config without the regenerated leaves would leave the
tree inconsistent with its own source.
26 of 26 map leaves now carry the pk; the rest are the staging and SPA pages
the generator writes the same block into.
Staged with 'git add -u' so only TRACKED modifications are included: a broad
'git add public/doxa-maps' swept in 378 untracked poster webp files and 376k
insertions, which is the separate untracked-build-outputs question and not this
change. That commit was reset and redone.
Driver 2026-09-21: 'all the maps need to have their mapbox token wired
properly according to the references - all the maps are not working yet until
that fix', then 'all maps can use the pk from the api, no problem - if they
want to set a mapbox sk they can'.
Every leaf already implemented a 6-rung token ladder; rungs 1-3 are used
directly with NO network call. On nuxtbase.app rungs 4-6 all failed: the
same-origin /api/maps/token 404s and the absolute doxa.life fallback sends ZERO
CORS headers. Rungs 1-3 were unset, hence 'No Mapbox access token provided' at
dashboard-plus-grid.js:605.
Setting build-config.token (rung 3) inlines the key into all 26 leaves, so a
map works on ANY host with no endpoint and no CORS.
This required splitting the hygiene rule, which lumped pk with sk/tk. A pk. is
a PUBLIC key: designed to ship in client code and protected by URL
restrictions, not secrecy (measured - the same pk. serves tiles 200 with a
doxa.life Referer and 403s with nuxtbase.app). Flagging it made rung 3
unbuildable, since that gate runs inside build:unlocked. sk. and tk. remain
flagged and were negative-tested: sk CAUGHT, tk CAUGHT, GitHub PAT CAUGHT, pk
allowed.
Verified: full build ALL GUARDS PASS, hygiene clean, SRI consistent across 58
leaf pages; 26 of 26 leaves carry the baked pk; and three maps mount with NO
token supplied by the harness - 0 'no token' errors.
Last tick I named this narrow and deliberately left it, reasoning that public/
is a tree we GENERATE so the only realistic leak is the Mapbox token the build
writes. That reasoning was wrong: 855 files under public/ are TRACKED, 95 of
them text - boundaries, people-group data, brand assets, leaf pages - curated
and committed by hand. A hand-committed file is exactly where a pasted
credential lands, and this tree is rsynced to a PUBLIC catalog.
Measured before fixing: a planted ghp_ PAT and an AKIA AWS key both passed as
'clean - 0 findings'.
Now covers GitHub PATs, Slack tokens, AWS access keys, private-key blocks,
literal Bearer tokens and Coolify token assignments alongside the original
Mapbox JWT. Each entry matches a KNOWN credential FORMAT rather than guessing
from entropy - an entropy rule over this tree would fire on every SRI hash and
base64 image and be ignored.
Negative-tested all 7: each plants its credential in public/ and is CAUGHT by
name; the clean tree still passes with 0 findings across 855 files; the file is
byte-identical after testing. The gate runs inside build:unlocked, so every
build and CI run enforces it.
Also caught before shipping: (?i:...) is Python/PCRE, not JavaScript - written
inline it throws 'Invalid regular expression' and takes the whole gate down.
Uses a trailing /i flag instead.
tauri build produced target/release/doxa-maps-desktop (65 MB ELF) and a 58 MB
.deb (Package: doxa-maps, Depends: libwebkit2gtk-4.1-0 libgtk-3-0). tauri info
reports the environment fully green and correctly detects frontendDist
../public, Vue.js, Vite.
The web tree is compiled INTO the binary rather than shipped as files, which is
why the .deb holds only 6 entries - confirmed by 19 distinct embedded
doxa-maps/... asset paths in the ELF and by size (a logic-free shell is 65 MB
only because the 132 MB public/ tree is compressed inside it).
bundle.targets narrowed from all to deb: rpmbuild and linuxdeploy are absent
here, so all would fail two of three formats at the END of a ~6-minute release
build.
The CLI is a devDependency (@tauri-apps/cli, prebuilt binary) rather than
cargo install tauri-cli, which compiles from source.
Status stays BUILT, NOT SHOT. The GUI launch is unverified because this box is
headless - the binary panics in tao with 'Failed to initialize GTK', the
absence of a display rather than a defect, and no Xvfb is installed.
Three docs said the config lives at the repo root - true when written, false
since the move, and exactly the real-but-wrong-target shape: a reader follows
the path, finds nothing, and cannot tell whether the feature exists.
Status upgraded from 'rustc parses main.rs' to what was actually measured:
cargo check finishes with 0 errors and 0 warnings across 400+ crates. Still
'built, not shot' - no packaged binary has been launched.
Yesterday's scaffold was never compiled - only parsed. Running the real
toolchain found three defects the config-only gate could not see.
1. tauri.conf.json was at the REPO ROOT. tauri-build reads it from src-tauri/
only; cargo compiled 228 crates before dying with 'unable to read Tauri
config file ... entity not found'. Moved, and check:tauri now asserts the
canonical location AND fails on a stray root copy (two configs where edits
to one silently do nothing).
2. The '//'-prefixed comment keys were FATAL. capacitor.config.json documents
itself that way because Capacitor ignores unknown keys; Tauri's parser is
strict and rejected '//THE_TOKEN_CAVEAT' outright. The config is now
schema-pure and the rationale moved to src-tauri/README.md, which also
records why it cannot go back.
3. generate_context! requires icons/icon.png even when bundle.icon is omitted -
my comment claimed omitting it AVOIDED an error; it caused one. Reused the
Android launcher icon (192x192 RGBA) so both native shells carry one
identity, matching the shared appId.
Result: cargo check finishes clean, 0 errors 0 warnings, 400+ crates including
wry and tauri 2.11.6. Status stays BUILT, NOT SHOT - no packaged binary has
been launched. gen/ and target/ are gitignored as generated output.
Five places across three docs still said method 5 was aspirational / not yet
scaffolded / a deferred POC. All were true this morning and all are now false:
tauri.conf.json, src-tauri/ and check:tauri exist.
The honest-ledger table moves from 2 proven / 2 built-not-shot / 1 aspirational
to 2 / 3 / 0, and its closing sentence is corrected - with nothing aspirational
left, 'the two lower states' no longer described the table.
Status is deliberately BUILT, NOT SHOT and never 'proven': proven is reserved
for methods 1 and 3, which have live artifacts. No desktop binary has been
emitted here. Section 3 now carries the runnable contract command, the
one-build-four-consumers framing, and the token/CORS caveat up front.
Method 5 was the last aspirational entry in the 5-deploy-method model: 0
tauri.conf.json, 0 Cargo.toml, 0 .rs. It is now scaffolded against the v2
config schema, mirroring the Capacitor pattern that already works.
frontendDist points at the SAME public/ tree Capacitor and the CDN serve, so
one build still feeds every consumer - now four instead of three. The crate
carries no application logic on purpose: the product is the web tree, this is
a window around it.
check:tauri is the analogue of cap:verify and exists for the same reason
capacitor.config.json warns about in capitals: a wrong frontendDist or a window
url that does not exist launches a BLANK WHITE WINDOW, not an error. It runs
with no Rust toolchain and no Tauri CLI, so CI can run it. Negative-tested five
ways - wrong frontendDist, missing window url, identifier drift from the
Capacitor appId, empty windows array, missing crate file - each exits 1, and
the config was restored byte-identical after every test.
Status stays BUILT, NOT SHOT. rustc parses main.rs (it reaches line 14 and
fails only on the uninstalled tauri crate, a dependency error not a syntax
one), but producing a binary needs the Tauri CLI plus system webview libs.
Also recorded the token/CORS caveat up front: a Tauri webview serves from
tauri://localhost, so the token endpoint must send CORS for that origin -
the same surprise Capacitor documents for https://localhost.
Step 2 told an author to create 'exactly four files'. Measured across the 18
shipped bundles, the folder holds 3 to 8 files and NOT ONE has exactly four.
all-people-groups is the true minimum at 3 (root .vue + index.js + index.html)
and is a fully working, manifest-listed plugin. 9 of 18 carry no .meta.json at
all, because the single and prop-table tiers are INFERRED rather than declared.
The table now marks each file required or opt-in, and adds the two files real
bundles actually use that the doc never mentioned: <name>.instances.json (which
is what makes a bundle prop-table / cap-2, per discover.mjs) and src/.
Verified the one claim that matters by testing it: moving index.html out of
demo-map made the build error and dropped the bundle from the buildable list
(18 -> 17); restoring it returned a clean build, byte-identical to HEAD.
Capability inference confirmed against the built manifest: all-people-groups
type=single, demo-map and doxa-vision-card type=parameterized.
Step 6 told an author that surfacing a plugin means adding a TAB_DEFS entry and
a NAV_MODEL row. That overstates the work. The cap-5 SPA reads the built
manifest at runtime and auto-discovers every bundle that is neither curated nor
on the NOT_EXPERIMENTAL exclusion list, placing it in the Experimental folder
with its label and description from the manifest.
Verified in a browser rather than from the comment that claims it: the rail
rendered 11 rows - 6 curated, then 5 auto-discovered (all-people-groups,
country-posters, demo-map, doxa-vision-card, stack-demo), each carrying the
flask marker.
The manual edit is still documented, reframed as what it actually is: optional
promotion OUT of Experimental into the curated teaching order. Also recorded
the one-row-per-bundle rule and the fail-soft behaviour, and corrected the
closing summary, which omitted the new mount step and implied surfacing was
manual work.
The golden path took an author from author -> build -> PUBLISH TO THE LIVE
CATALOG without ever opening the element in a browser. Every guard in Step 3 is
static: it checks that files exist, parse, carry a correct SRI hash and resolve.
None of them renders anything.
Proved the gap rather than asserting it: injecting a throw into a component's
setup() produced a SUCCESSFUL build (206 kB emitted, exit 0) with check:parse
PASS and check:drop PASS, while smoke:embeds reported 0/2 pages mounting. After
restoring, the same command reports 2/2.
The step documents the token requirement because its absence is loud and
misleading: without it every map profile fails to mount (27/40) and reads like
13 broken plugins. It also records why this is not a CI step - CI is the single
authoritative builder but installs no browser binary.
All three quoted commands were run as written before being documented.
Without a token every map profile failed the mount assertion and the gate
reported 13 'mount timeout' failures that read as broken plugins. They were
not broken: the same profiles mount immediately once a token is supplied
(doxa-simple-map went 0/4 -> 4/4 with canvas; the full suite 27/40 -> 40/40).
Both channels are set because they are not interchangeable: ?tk= reaches only
the top-level element's profile-config, while nested children read
window.MAP_TOKEN. A composite mounts with ?tk= alone but its children stay
blank.
No token is not a failure - the run prints an explicit warning and degrades to
DOM-only coverage rather than reporting map profiles as broken. Negative-tested
in both directions.
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.
Three places told a plugin author their work compiles to public/<name>/. That directory
does not exist. The build emits public/doxa-maps/<bundle>/<bundle>.js, where the
doxa-maps segment is the output tree name from vite.doxa-maps-build-config.json — not the
plugin's name and not this repo's folder name. The doc conflated two different <name>s
and sent a receiver looking somewhere empty.
One of the three lines carried its own contradiction: it wrote public/<name>/ and
public/doxa-maps/manifest.json in the same sentence.
Left the reference.md occurrences alone. Both say "output tree" explicitly, so <name>
correctly means the configurable tree name there rather than a plugin.
The layout section now also states the contract plainly, since that is what a hand-off
reader needs: write inside plugin-designs/<your-map>/, run the build, and it appears in
the output tree with no registration step. Every concrete path in the README was then
resolved, and both template forms checked against a real bundle.
The Capacitor record claimed "21 bundles staged". That number matches nothing measurable
today: the tree has 18 built bundles, 17 of them in the manifest since key-cities is
withheld by gatedProfiles, and 19 subdirectories under public/doxa-maps, one of which is
assets and not a bundle at all.
More to the point, bundles was never the unit cap sync operates on — it copies webDir
wholesale. A count that names the wrong noun goes stale the moment the tree changes and
cannot be re-checked against anything, which is how the original figure survived several
sweeps unnoticed. The line now states what was actually verified.
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.
Driver: "the sidebar is supposed to look like Material UI, like what people are used to...
people are used to the YouTube layout, Google layout is just way better. We do not need
to create custom styles people are not used to."
Measured before: app bar 51px, nav rows 44px with an 8px radius, collapsed rail 60px.
Now app bar 64px desktop and 56px mobile, rows 48px, a full pill active row, and a 72px
mini-variant rail. Nav width was already 256px and is unchanged.
Nothing vendored. The card's law is to imitate the metrics in our own CSS rather than add
Quasar or MUI, which is the same clone-don't-edit rule the library follows elsewhere.
Two traps worth recording. box-sizing:border-box is load-bearing on the app bar: without
it the element is content-box, so min-height:64px measured 81px live once padding and
border were added. And the rail collapse is a translate rather than a width change, so
four numbers are coupled — the nav translate, the content pane's left, and two padding
compensations that keep icons inside the visible strip.
Verified live: app bar 64 desktop and 56 mobile, rows 48 with a 999px radius, rail 256
expanded to 72 collapsed with the content pane starting at exactly 72.
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 card recorded a hypothesis and said not to assume it: that Firefox's deepFind resolved
only the outermost shadow root. Measured and disproved — both engines find exactly one
shadow host at depth 0, so shadow traversal is identical. The real difference is WebGL.
Chromium reports webglSupported true with 157 elements in the shadow root and the toolbar
present; Firefox reports false with 31 elements, no toolbar, and "[doxa-simple-map] Map
failed to initialize".
Confirmed as an environment property, not a page one: Firefox returns null from getContext
on a blank page here, and firefoxUserPrefs for software WebGL do not change it. Chromium
only works because launch() passes SwiftShader args. So the Share button is never rendered
because the map never initialises, and the suite was blaming the product for that.
Rather than quietly defaulting away from the Driver's house rule, the suite now aborts with
the measurement and the exact working command, and exits 2 so a caller can tell "could not
test" from "test failed". Restating the rule stays the Driver's call.
It also had no token wiring, the third gate found missing it. My first attempt set only
window.MAP_TOKEN, assuming a page global would survive into the reopened page. Measured:
?tk= only mounts, global only does NOT mount, both mount. The global is read by nested
children of a composite, not by a top-level element. So the host URL carries ?tk= and the
share URL is navigated verbatim, because appending a token there would fake the very thing
under test.
The remaining failure is now honest and in the right place: the host page renders, and the
share URL cannot, because a pasteable link deliberately strips query params. Only the token
endpoint fixes that.
Both gates barred on zero console errors, which fails a perfectly rendering map whenever
the environment is imperfect. Measured on all-people-groups with a valid token: the
element mounts at 900px with a live GL canvas at 1440x900 and 309 characters of real
text, and the gate said FAIL over 20 console errors that were all one external cause.
Three classes, each observed rather than imagined. Ladder fallthrough is the token ladder
doing exactly what it is designed to do, trying rungs in order until one answers. Mapbox
403s come from the shared token being URL-restricted to doxa.life, and Mapbox then throws
its own internal errors because of them, which is why one restriction produces twenty
lines. Terminal "No Mapbox access token provided" means the ladder is exhausted and the
map genuinely cannot paint, so that still fails.
This is not "ignore errors": anything unrecognised still fails by name, and the exempted
count is printed per tier so the exemption's blast radius stays visible. A silent
exemption is how a gate quietly stops testing anything.
verify:map-paint also gained the token wiring verify:5-caps already had. Classifying was
necessary but not sufficient there — without a token the element never mounts at all, and
a gate that can never pass is still a gate that gets ignored.
verify:5-caps now reports 5/5 with a token and still fails correctly without one, naming
the terminal cause. verify:map-paint reports 4/4 painting with live GL and attribution.
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.
Last tick's fix covered the shared map-control base. Sweeping the rendered DOM of all 17
bundles found three more unnamed buttons — the same "Collapse legend" control in three
map bundles, again from one shared component. Its only content is an svg caret, so with
title alone its accessible name was empty.
It is a disclosure control, so it now reports aria-expanded as well. Verified by toggling
it: the label flips Collapse/Expand and aria-expanded flips true/false.
The gate measures the rendered DOM through shadow roots, because a source grep cannot
answer this question. There are 132 <button> hits here and most have visible text, which
IS a valid accessible name; aria-label is often on a following line. Only the DOM knows
which buttons ended up nameless. It counts aria-label, a resolved aria-labelledby, or
visible text, and deliberately does not count title, which is the defect it was written
for. Hidden buttons are skipped since assistive tech cannot reach them either.
The sweep went 319/322 before the legend fix to 322/322 after, which is the evidence it
detects real defects rather than passing vacuously.
The zoom buttons render an icon through a slot and carried only `title`, so their
accessible name was empty: a screen reader announced "button" with no indication of what
it does. title is a tooltip, not an accessible name — unreliably announced, and never
surfaced to touch users.
Found while proving the research-dashboard zoom controls actually work. They do: a click
triggered six tile requests at zoom level 2, so the open-work item is function-verified,
not just present. The probe that matched aria-label found nothing while the probe that
fell back to title found both buttons, and that discrepancy was the defect.
One binding in the shared MapControlButton reaches all eight control buttons — zoom in
and out, fullscreen, location, help, share, hamburger, theme — across seven map bundles,
because every one composes this base. No caller changes: the component does not set
inheritAttrs:false, so a caller passing its own aria-label still wins through normal
fallthrough, and undefined omits the attribute when there is no title to mirror.
Verified across all seven bundles: 34 control buttons, 0 without an accessible name.
Card 1 asks that one public Forgejo repo be the live catalog source and that the merge
lose nothing. Both halves were marked done-verified on 2026-09-19, two publishes ago, so
this re-measures rather than trusting the field: the catalog is still a zero-diff match
for the freshly-built tree, all 17 live bundles hash to their own manifest integrity, and
the live public manifest is byte-identical to the catalog's.
An HTTP status cannot make that claim. /app/doxa-maps/manifest.json returns 200 and is a
2,872-byte HTML sign-in page, so a status check reads the login wall as healthy. Every
profile already carries an integrity hash the build computed for an unrelated reason, so
comparing it against the live bytes is a cryptographic identity proof with no new source
of truth to keep in sync.
Network-dependent, so not in build:unlocked and not in CI. It distinguishes "could not
reach" from "reached and wrong" and only the latter fails, because a gate that fails on a
network blip gets switched off.
Negative-tested four ways: the login wall fails with the reason, an unreachable host
skips at exit 0, the catalog-comparison mode confirms the sha256 match, and the baseline
proves 17 of 17.
"Which bundle is the capability-N example?" has been answered wrongly in three separate
artifacts, each fixed in isolation while the others kept the bug: verify-5-capabilities
cited doxa-vision-card as cap-1, the README made the identical claim, and the
/plugin-designer skill still cites both that and unified-research-dashboard, a bundle
that does not exist.
The skill cannot be gated from this repo. It lives in the session config directory, in
no git repo, so a checker pointed at it would pass here and skip silently everywhere
else — the gate-that-cannot-fail shape. So instead of policing the skill, give the repo
an authoritative generated answer that prose can cite and a gate can verify.
The table derives from manifest.json, which the build computed from each folder's real
shape, and reference.md now points at it with the reason: a folder's apparent simplicity
does not decide its tier. doxa-vision-card looks like the lightest thing in the repo and
is capability 2, because it ships an instances file.
check:capability-exemplars regenerates and content-compares rather than checking mtimes,
because a full build rewrites manifest.json every time and a gate that cries wolf after
every build gets switched off.
The cap-4 composite shipped the coach-mark sliced through the middle of its sentence.
Measured, same component and viewport: the leaf renders the pill at bottom 864 of 900,
the composite at 919 — clipped by 17px, because the composite adds a ~53px header above
its nested child's stage and findMapBox() finds no positioned container to re-parent into.
The component's header promises that case degrades to "stay put — a degraded anchor, never
a crash". A graceful degradation that degrades to invisible is a failure, not a fallback,
so the clamp measures the rendered rect and only ever lifts a pill that is actually clipped.
Two wrong attempts are recorded in the code because the reasons matter. Setting
style.transform would have destroyed the wrapper's translateX(-50%) centring, and the
pill's computed transform also carries the bob animation, so a computed reading is never
just my offset. Adjusting the bottom offset composes with the attribution clearance
instead of fighting it. Clamping only at nextTick also missed: the composite's layout has
not settled then, so the clamp now runs from syncAttribClearance, which already re-fires
from a ResizeObserver, plus a second observer on the pill's own layout parent.
Verified on six surfaces: the composite lifts 30px to bottom 888, every other surface
reports lift 0 and is byte-identical in behaviour, and all six keep centreDelta 0.
The CAP 1 slot shot doxa-vision-card, which is not a single element: it ships
doxa-vision-card.instances.json with two instances, so discover.mjs types it
parameterized and manifest.json records "parameterized" — capability 2.
The same error had already been found and fixed in verify-5-capabilities.mjs, whose own
comment records that it had been sampling cap-2 twice and cap-1 never. That fix landed in
the gate and never reached the README: two artifacts encoding one fact, with nothing
forcing agreement. Fixing one copy was not fixing the fact.
all-people-groups is a genuine cap-1 — one .vue, no instances file, manifest type single.
The README now also says what decides the tier, since the folder's apparent simplicity is
the trap: doxa-vision-card looks like the lightest thing in the repo.
check:capability-citations compares every README capability claim against the manifest,
blocking in CI. Its first matcher used \Z, which is not JavaScript, so it matched zero
sections while still reporting a citation count — a gate reporting a number it had not
verified. It now splits on headings and the count reflects real matches.
No bundle .js changed — the rebuild was deterministic and the catalog needs no publish.
The images move only because check:shots compares each PNG against its bundle mtime, and
a full build was required to clear the composite staleness my negative test introduced.
Reading the cap-2 screenshots found all three doxa-simple-map shots rendering data dots
on an empty dark field: no coastlines, no country fills, no labels. Every shot reported
OK, because the page loads, the element mounts, and the legend, controls and data layer
are all correct.
The reason no signal can see it: the Mapbox STYLE DOCUMENT returns 200 from every origin,
so the map genuinely boots. Only the TILESETS are restricted — mapbox-streets-v8, which
is the basemap itself, plus bathymetry-v2 and country-boundaries-v1, all 403 off
doxa.life and 200 with its referer. A booted map showing its own UI and its own data
looks like a working map; only the geography is absent.
This also explains a near-misdiagnosis. cap4-upg100-list paints perfectly beside four
blank maps, which looked like a per-bundle bug. It is not: my-upg-100-list ships its own
world-adm0.geojson and draws geography from self-hosted data. Three bundles do that and
survive; seven do not.
The dark basemap in those shots is deliberate, not a theme violation — useMapTheme's
lookForStrategy keeps prayer, adoption and engagement on the night preset by design.
Negative-tested: marking every affected bundle as self-hosted removes the finding, and
removing the markers brings it back.
A previous reader measured ZERO <canvas> on research-dashboard and concluded the surface
owns no map, recording it as an architectural fact and relaxing the shot's readiness
check to match. The measurement was real; the inference inverted it. The page imports
useMapInstance, injects mapboxToken and adds a vector source, and re-measured live it
builds a mapboxgl-canvas at 878x445. Zero canvas meant the map failed to initialise.
The cost was not the blank image. A weakened assertion that makes a failing check pass
promotes a defect to a design note, and the README's deploy-3 proof has shown a blank
grey map pane since.
Why it is blank, measured: the shared pk. token is URL-restricted to doxa.life. Against
country-boundaries-v1, Referer doxa.life returns 200 on three attempts, 127.0.0.1 and no
referer return 403. The style endpoint returns 200 from every origin, which is why most
maps paint here and only the tileset-backed choropleth blanks. A first reading of that
asymmetry looked like a missing tileset scope; re-testing with the doxa.life referer
disproved it.
check:shot-fidelity now reports this class alongside the missing emoji font, and no
longer exits early after the first finding.
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.
README drew the two cap-5 products apart in prose and even warned that captioning a
studio shot as the product SPA is a documented mistake — then filled its plugin-studio
slot with cap5-doxa-maps-spa.png. Both cap-5 figures showed the same app and no
plugin-studio screenshot existed anywhere in the repo.
A slot filled with the wrong real image looks exactly like a correct one; nothing 404s.
So the fix is to shoot the missing artifact, not re-caption around the gap. Both cap-5
slots now use generated, freshness-gated images.
Also adds check:shot-fidelity. The shot run reported 13x OK while every emoji rendered
as a tofu box: this machine has no emoji font. Measured rather than assumed — the DOM
carries 27 correct codepoints, so the markup is right and the defect belongs to the shot
machine. Reports by default because installing a font is a Driver call; --strict blocks
once one is present.
README-SECTION-3 reproduces the scripts block verbatim and carries its own plea to
re-transcribe after every package.json edit. It drifted anyway, twice: cap:sync still
showed the bunx form that does not exist here, and build:unlocked was missing
check:public-hygiene and check:label-joins.
The second is the dangerous shape. An omission in a quoted list has no wrong character
to notice, so a reader auditing what the build verifies concludes the secret scan is not
in the chain. The warning was already there and did not work, because re-transcribing by
hand is what gets skipped when the edit felt unrelated.
Blocking in CI: it diffs two files already in the checkout, so a failure is always real.
Negative-tested both ways, including a matcher that matches too little failing rather
than passing vacuously.
The top-level layouts/ directory does not exist; the wrappers live in library/layouts/.
Six references across four docs named the phantom path, including a copy-pasteable
cd command and a README-longform link that resolved to docs/layouts/README.md (a 404).
how-to.md taught bunx for the iOS scaffold. bunx is not on PATH here, which is the
exact failure package.json already documents in cap:sync; point both at the local bin.
useMapProvider.js's own header listed three resolution rungs, omitting ?provider= and
misnumbering window.MAP_PROVIDER. The function implements four. how-to.md was correct.