Each chart's title is its question, its subtitle says what a bar is and
counts, and a computed takeaway answers it in words. 1: what they have in
their own language. 2: the largest people groups with nothing in their own
language, by name (7 of the 8 largest on the roster are Deaf communities).
3: of those with nothing, how many live where the country is online.
4: which languages most need the New Testament, by people who speak them
(736 languages, 153 million people). The stacked chart, the social
platform chart and the app chart are gone: no readable answer, or not a
gap.
The rail was sized to the map row alone (~300px), and its cards, children
of a flex column, shrank instead of scrolling: the measure toggle
collapsed to a 2px line and a caveat card was cut in half (rega, the
Driver). The rail now spans the map and the table rows, its children
never shrink, it opens at 360px, and chart labels get 130px so no band
name truncates.
Both halves of studio's L13U refusal were correct and both are mine.
1. THE LEGACY upg100WallIntro FALLBACK IS GONE. It existed for exactly one window — the three
plain-text keys were landing in studio's catalogue while this consumer was already written, so the
branch rendered the old markup-bearing key until they arrived. They are on main in all eleven
locales, so the condition can no longer be true: it is DEAD CODE that keeps a key alive, because
studio cannot delete upg100WallIntro while a line still reads it. A fallback whose condition cannot
fire is not safety, it is a reference.
The three remaining mentions in this file are COMMENTS, not code — verified that no live call site
anywhere in plugin-designs/ or library/ reads it, so it is now safe to delete.
2. THE GATE DEFAULTED TO A GITIGNORED FILE. check:zoning-declared read
library/scripts/posters/out/_records.json, which .gitignore:94 excludes, so on a clean checkout it
exited 75 — NOT MEASURED forever, indistinguishable from an unwired gate. studio measured exactly
that and correctly refused to wire it into gates:post. This tree has been here before:
check:packets-all SKIPped on every build for weeks for the same reason.
★ THE FIX IS A COMMITTED FIXTURE, NOT A LOOSER GATE. data/people-groups/zoning-records.json carries
slug plus every field a selector actually reads — 2,107 records, ~300 KB. country_code alone would
not do: the deaf zone reads rop1 and five African/European zones read wagf_block/wagf_region, which
is the same omission that made six zones silently resolve to nothing in the packet generator.
The live cache is still PREFERRED when present, so a local run measures the freshest data and CI
measures the committed one.
VERIFIED on a genuinely clean tree (git archive of the staged tree, no cache, no network):
PASS — 28 zones, 2,107 records, 2,096 covered, all five assertions green, exit 0.
check:i18n-keys-exist PASS, check:sfc-parse PASS.
At 390px the legend card covered most of the map on open, and its close
button was a 16px glyph (rega, row 75). Both legends (map and language
tree) now start folded to their Legend button below 520px, and the close
button is a 28px box.
The P1 that emptied Big Picture's Religion and Region charts was a free identifier:
useDashboardAggregations.js read NEUTRAL five times and never imported it. This gate exists for exactly
that class and did not fire, because it walked only .vue files. A bundle compiles .js and .vue alike,
so the gate was seeing half of what ships.
Two things were needed, and the first attempt got only one of them. Adding .js to the walker is not
enough: a .js module was then handed to Vue's SFC parser, which found no <script>, and acorn reported
"Unexpected token (2:1)" — the second line of the file's own doc comment. A .js file is its own script
block, so it now parses directly, with offset 0 so reported line numbers are the file's real ones.
VERIFIED BOTH DIRECTIONS, which is the only way to know a gate works:
- clean main: rc=0, no false positives
- NEUTRAL import removed again: rc=1, naming useDashboardAggregations.js:173 and :216
So it would have caught the P1 before it reached the Driver's console.
Blast radius measured on main first: exactly 2 findings, both false — DOMException and
createImageBitmap, standard browser globals missing from the known-globals list. Added. No other
owner's file is implicated, and nothing was changed outside this gate.
Two certain defects from source. What I could NOT settle without a browser is stated at the end
rather than guessed at.
1. THE PROBE READ THE WRONG NODE. The sign has to match the element the GRID lays out with. Both
probes read the SHELL; the handle is a grid child, so whatever direction the grid resolves the
handle inherits — and the shell is one node further out. A host that sets dir between the two, or
a shellRef that is not the grid itself, yields a sign that disagrees with the mirroring the reader
can see. Both the pointer and the keyboard path now try the handle first and fall back to the
shell, then to ltr, so a detached handle cannot throw mid-gesture.
2. research-dashboard's two handles used PHYSICAL margins (margin-right / margin-left) while the grid
they sit in mirrors — grid-template-columns and justify-self both follow the inline direction — so
in Arabic each handle's 4px overhang sat on the wrong side of its own column. go-int's board
already used the logical pair; this makes them agree.
WHAT WAS ALREADY RIGHT, so nobody re-fixes it: the xSign inversion itself is correct for a mirroring
grid and is DEPLOYED — 2 of 2 rtl checks present in the built gospel-internet, dashboard-plus-grid and
research-dashboard bundles, so this was never a stale-rebuild problem.
MEASUREMENT I NEED (rega, one line): during a drag in ar on the Gospel x Internet board, log
getComputedStyle on BOTH the handle and the shell and report each .direction. If the handle says rtl
and the shell says ltr, item 1 above is the whole bug. If both say ltr while the grid is visibly
mirrored, the mirroring comes from somewhere my probe cannot see and I will need that.
Source only. Rebuild: research-dashboard, dashboard-plus-grid, gospel-internet (library change).
On a phone the brief covers the bottom half, and the selected group and its neighbors
were framed behind it; both views now fit them into the visible strip. The prayer
map's "Tap a coloured dot" coach-mark, which covered the brief, is turned off here.
country-posters-mfe reads profile-config `view` ("map" or "wall") to choose its
opening pane, so an embed can start on the map.
Five lenses answered one question five ways on a 40-country snapshot, and the default
one silently dropped 27% of its senders. They are replaced by two views over one
dataset built from peoplegroups.org (7,307 unreached groups):
Mobilize pick an unreached group; see its reached neighbors at E-1 (same language),
E-2 (near culture) and E-3 (nearby, other culture), its P-level, what
resources exist in its language, and export them as audience segments.
Campaigns bridge languages spoken by believers and by unreached groups, ranked by
the unreached population one campaign in that language reaches: where to
run it, whom it reaches, exportable.
The dataset is fetched (bundle 1.27 MB -> 0.55 MB); the old view names open the new
views so existing embeds keep working. 65 new strings in all eleven languages.
The tapestry is laid out at half its former size in the map's world square,
so from the opening view the reader can zoom out two levels instead of
one. Every zoom-dependent stop (dot sizes, label entry zooms, framing
limits) moves up one level with it, so the opening view looks the same.
profile-config tk, then the token ProfileLoader provides (a tokenUrl
token-service answer or the page's shared token), then window.MAP_TOKEN.
It read only the first and last, so a host using tokenUrl got no Mapbox
token on either of the board's maps.
stage:experimental puts it on the Plugin Studio's Experimental shelf until it
works as a production map; heavy:true makes the Studio unmount it when
left, since it holds up to two WebGL contexts (map + language tree).
The Driver reported the list page being slow and guessed the posters. The
posters are behind a v-if and never load until a region is opened. The cost
was the mount fetch: it requested the full PEOPLE_GROUP_LIST_FIELDS for every
people group, and this page renders none of it -- a region's label, group
count, country count and country names all come from country_code and
wagf_region alone. Verified by running summarizeRegions on two-field rows:
the four values this page binds all come out.
SIZE OF THE SAVING: NOT MEASURED, and I will not put a number on it. Six of
eight fields are dropped per row across the whole dataset, including two
coordinates, a population integer and three status/count fields, so the
reduction is real -- but sizing it needs the live endpoint, which I cannot
call from here. My first attempt quoted a proxy off
public/assets/people-groups/people-groups-slim.json; that file is the IMB
dataset in PascalCase and carries NONE of the eight field names, so both
halves of that comparison were measuring empty objects. The figure was
meaningless and is removed rather than rounded.
PEOPLE_GROUP_LIST_FIELDS is untouched: the dashboard tab needs all eight for
its map pins and stat cards.
It owns up to two WebGL contexts (its map, plus the language-tree
whiteboard once opened). Kept mounted by v-show, it spent the context
budget that silently drops the oldest map. go-int's edit here was
dropped at landing.
82 lines, one importer (useDashboardShell), and every import already @map/ — so it moves as-is with
no adapter and no shim. Its inject('profileConfig') stays inside its try/catch: it can be called from
outside setup, and that is the property the move had to preserve.
Source only. research-dashboard and dashboard-plus-grid need a rebuild.
Landing 4a extracted buildSearchAccessors / makeSearchMatcher / filterRowsBySearch into
library/composables/useDashboardSearch.js. It left them DEFINED in useDashboardShell.js as well —
4.3KB of code-identical copies, including the Fuse.js matcher. I compared all three function bodies
with comments and whitespace stripped: identical, byte for byte.
Nothing had diverged, so nothing failed, and that is exactly why it survived: the facet rail ran the
library copy while the shell's KPIs and three panels ran the shell's. The comment that used to sit
above those definitions said the point of ONE declaration is that "the searchable text can never
drift from what the panels show" — and there were two. This tree has already paid for that shape once,
at Jaccard 0.82, with useNeighborT and usePplrT.
The shell now imports them like every other consumer, and DashboardChartsRail, DashboardDataTable and
DashboardMapPanel read '@map/composables/useDashboardSearch.js' instead of reaching through the shell.
No re-export shim: all four consumers are repointed in this commit, so there is nothing for a shim to
serve, and an unretired shim is how three older ones sat in the tree unnoticed until this afternoon.
check:filter-multivalue grows arm E, deliberately beside arm B. Arm B exists because a copied FILTER
predicate lets one panel disagree with the others about what is on screen; a copied SEARCH is the same
hazard with a different name. Arm E asserts the library module declares all three, that the shell
declares none of them, and that no file anywhere imports them from the shell. Its importer sweep walks
plugin-designs and library rather than a hardcoded list, because a hardcoded list of "files that might
import this" is the assumption that let 4a's copies survive.
Groundwork for 4d-4: this removes 4.3KB from the file that has to be split into a generic kernel and a
research composition root, and it removes three exports from the surface that split has to preserve.
Source only. research-dashboard and dashboard-plus-grid need a rebuild.
The Driver: a public plugin that is not yet a production map shows under Experimental until it is. A
profile's .meta.json declares "stage": "production" | "experimental" (default experimental; a bundle is
production if any profile says so) and "heavy": true for a live WebGL owner. generate-tree carries both into
manifest.json, so the Studio and the gallery read the same field and neither keeps a list.
The seven production bundles are marked: dashboard-plus-grid, research-map-plus-grid, regions-dashboard,
my-upg-100-list, doxa-simple-map, cities-internet, gospel-resources. The Studio rail keeps its capability
sections for them and puts every experimental bundle (gospel-internet among them) in one final Experimental
section. The gallery's auto-discovered Experimental folder skips a production bundle. gospel-internet-board
joins HEAVY_ELEMENTS (it can own two WebGL contexts). check:studio-rail asserts the split.
The numerals policy is HELD — whether counts follow the reader's locale is the Driver's decision — so
this changes nothing anyone can see. Every caller renders exactly the digits and separators it
rendered before. What changes is how many places that decision is written down: thirteen call sites
across four bundles becomes one, library/utils/formatCount.js, and eight of the thirteen (mine) now
read it.
That is the whole value while the policy is held. If the answer becomes "follow the locale" it is a
one-line edit in formatCount.js instead of thirteen edits by four owners, each free to pick a
different locale source — and the first divergence in that world would be invisible: two panels in
one dashboard, same number, different separators.
★ IT FORMATS. IT DOES NOT DECIDE WHAT EMPTY MEANS, and that is the bug this fold would otherwise have
shipped. I told dx-shepherd these three helpers were "byte-identical". They are not:
DashboardDataTable null/undefined -> '' and '' falls through to Number('') === 0, so "0"
DashboardDetailDrawer null/undefined/'' -> '—'
useDashboardShell null/undefined -> '—' and '' likewise becomes "0"
Three different answers to "what does a missing count look like", inside one dashboard. Folding them
onto one empty policy would have silently changed two of the three — a table cell going from blank to
an em dash, or an em dash where a zero used to be. So each caller KEEPS its own guard and hands
formatCount a value it has already established is real. The locale decision folds; the empty policy
does not, and the difference is written into the helper's header so the next person does not re-merge
them.
Folded: DashboardDataTable, DashboardDetailDrawer, useDashboardShell (its exported fmt keeps working
for the KPI strip), DashboardFacetRail's three inline template calls, people-groups-grid's two.
NOT folded, because they are other owners': regions-dashboard (3) and pplr-data-maps (2). The helper
is theirs to import when they want it; nothing forces them, and no gate demands it — a gate requiring
the helper everywhere would fail on their files today, which is not a thing to land on someone else.
Source only. research-dashboard, dashboard-plus-grid and people-groups-grid need a rebuild.
THE GROUP WAS SCHEDULED AS ONE LANDING AND DELIVERED AS TWO. Section 8 of the spec puts the facet
rail, the chip bar and the BREADCRUMB in a single landing. The rail moved in 4c-3 and the chips in
4c-2; the breadcrumb was left behind, and nothing failed, because it is mounted by nobody. A leftover
like that is invisible unless someone re-reads the spec against the tree, which is what this landing
came out of.
The breadcrumb gets exactly the inversion the chip bar got: no useTreeLegendLabels import, the
resolver read from dashboardConfig.legend, and the SAME unconfigured default — identity, joining an
array. Identical on purpose rather than by copy-paste: the breadcrumb and the chips render the same
filterSet in the same band, so if their defaults differed a half-configured dashboard would show one
value resolved and the other raw, side by side. Its dead `dataStore` / `searchSourceId` injects go
too; they existed only to build the composable that is no longer imported.
★ AND THREE ONE-CYCLE SHIMS FROM LANDINGS 2 AND 3 WERE NEVER RETIRED. plugin-designs/research-
dashboard/src/composables/ still held useDashboardLayout.js (11 lines), useSelectionCamera.js (11) and
useCsvExport.js (14), each a pure re-export of the library module. MEASURED before deleting: ZERO
importers. Every consumer — including DashboardMapPanel, DashboardDataTable, DashboardDetailDrawer,
useDashboardShell, gospel-internet's board and research-dashboard.vue — already reads
'@map/composables/…' directly, and a tree-wide grep for a relative import of any of the three returns
nothing. So they were dead code that made the plugin look like it still owned three library modules.
I wrote "a one-cycle shim that nobody retires is permanent" into the previous landing's gate comment
while three older ones were sitting in the tree unnoticed. They were not found by a gate; they were
found by listing what is still in plugin-designs and comparing it against the spec.
check:legend-mirrors arm 5 now guards three moved names instead of two, so a re-export of the
breadcrumb creeping back into the plugin barrel is caught the same way.
Source only. research-dashboard and dashboard-plus-grid need a rebuild.
The chip bar (4c-2) and the facet rail (4c-3) each moved into library/components/dashboards/ and were
re-exported from the plugin barrel for ONE cycle, so that moving a file and changing its importers
were separate, reviewable landings. That cycle is over.
BLAST RADIUS, measured on main rather than assumed: exactly ONE real importer. research-dashboard.vue
imported DashboardFacetRail from './src/components/dashboards'; it now reads
'@map/components/dashboards/DashboardFacetRail.vue'. DashboardFilterChips had ZERO importers, so its
shim served nobody and is simply gone. Everything else that named either component was a gate already
pointing at the library path, or prose.
check:legend-mirrors arm 5 INVERTS, and the inversion is the point rather than an edit. While the
shim existed, the risk was an importer failing to resolve, so the arm REQUIRED the re-export. With the
shim retired the risk is the opposite and larger: a re-export creeping back makes the plugin barrel
look like the owner of a library component, which is exactly how check:filter-multivalue once ended up
reading a shim instead of the panel it was written to guard, and passing vacuously. A one-cycle shim
that nobody retires is permanent, so the gate now holds the window closed — it fails on a returning
re-export AND on any file still importing either name through the barrel or its old path.
★ THE THREE FILES MUST LAND TOGETHER. Measured: the new gate produces 3 arm-5 failures against
unmodified main, because main still has both shims and the barrel import. check:legend-mirrors is in
gates:post, so landing the gate without the source changes turns every build red. All three failures
are in the two files this commit fixes, so nothing outside it is affected and no other owner is
blocked.
Two false positives of my own, both caught by running the gate rather than reading it:
- The importer test used `import … <Name> … [\s\S]{0,200}? from '…'`, and that span CROSSED INTO THE
NEXT STATEMENT: after the fix, research-dashboard.vue has the new single-name import immediately
followed by a braced import ending `from './src/components/dashboards'`, so the regex started on the
corrected line and found the barrel path two statements later. It reported the file it had just been
fixed in. Now bounded to one statement with a negative lookahead for the next `import`.
- That is the third unbounded-span false positive I have written today — the others walked a brace
match past a callback-less watch() and read a chip's internal name as prose. The shape is the
lesson, so it is written down beside the fix: never let a span cross a statement boundary.
Verified on main + these changes: check:legend-mirrors, check:sfc-parse, check:no-free-identifiers,
check:filter-multivalue, check:no-cross-plugin-imports, check:composites-mount, check:library-index and
check:setup-order all pass. Arm 5 negative-tested both ways: a returning barrel re-export and a
reverted importer are each caught, and the clean state passes.
Source only. research-dashboard and dashboard-plus-grid need a rebuild.
Six public/ conflicts, zero in source. They take main's side here and are regenerated from the merged
source in the next commit — a bundle is an output, and a hand-reconciled middle version is wrong by
construction.
A vector SVG of the current selection, one sheet of the chosen size,
printed in place (library printInPlace) or downloaded for a print shop.
Every label is placed so that none overlaps another or covers a circle:
families on top of their dish, leaf names running outward like spokes,
the rest beside their dot, and a name that cannot fit is left off and
counted on the sheet. The poster has its own layout (build step), with
slots for languages only, because resources are catalogued per language:
671 of 858 languages named at 3 ft, 779 at 4 ft (all families), 0 overlaps.
check:language-tree measures both sizes.
A strip under the title shows what is selected, as the kit's removable
filter chips (the board is DashboardFilterChips' second consumer: it
provides selectionBus and dashboardConfig), with a language-tree node
named as the node, not its language codes. Beside it, seven combinations
a mobiliser would run, each with its live count for the current list:
nothing in their language in a country 60%+ online (220), hear but cannot
read in an online country, nothing in their language offline, in a
WhatsApp or a Telegram country, where a Deaf bridge exists, and India and
China. A combination is a filter set on the one bus, so the map, the
language tree, the charts and both tables all follow it.
P1, rega on main a71007f1: "NEUTRAL is not defined", both engines, both languages, and the two charts
render nothing.
Kit landing 4c-1 moved NEUTRAL into library/composables/useFacetAggregation.js and updated the
RE-EXPORT line in useDashboardAggregations.js, but not the IMPORT line beside it — so the module
re-exported a name it could not itself read. colorFor() and buildCountryRegionColors() read NEUTRAL
five times as the "this facet has no palette for that key" colour, and every read threw.
One line: NEUTRAL joins the import. Nothing else changes.
The wall opened at the whole-wall view, where a card is 11-15 CSS px, under wallLod's 48 px photo
threshold, so every card showed its empty image slot: the README picture read as a blank grid (the
Driver: "zoomed too far out... hard to see the people groups"). It now opens at the top-left corner,
where the store puts the biggest clusters, with cards ~96 px wide. Checked headless with lodPlan on
the component's own geometry (2,107 groups, 53x40): desktop 1440x820 k=7.5, 77 photos; 1280x720
k=8.6, 60; phone 390x700 k=20 (max), 15 photos, all 'hd' and far under the 300 cap, so the memory
win holds. Reset Zoom still shows the whole wall. Rows were 3 units apart against 8 between columns
and left a 200-unit empty band at the bottom of the 4ft x 3ft canvas; 8 fills it exactly.
Built with BUNDLE=wall-of-people-groups only, then the tree; check:sri PASS.
The tapestry is laid out at half its former size in the map's world square,
so from the opening view the reader can zoom out two levels instead of
one. Every zoom-dependent stop (dot sizes, label entry zooms, framing
limits) moves up one level with it, so the opening view looks the same.
PRODUCTION-MATRIX upg-100-mfe composite -> map (doxa:select-zone), the last red cell in the 7.
CAUSE, and it is one missing line rather than the delivery race it looked like: applySelectZone sets
_pendingZoneId on every FAILING path and clears it on the no-op path, but the SUCCEEDING path stamped
data-doxa-select-zone-state=applied and returned true WITHOUT clearing it. So a request retained once stayed
outstanding forever, and ensureLayers() drained it again on every subsequent call. Re-entering
onLegendSelect for an already-isolated zone TOGGLES the isolation back OFF, which is the reported symptom:
the Map view opens, the zone is selected, and then it is silently lost.
The retain/drain machinery (the attribute-as-state, the MutationObserver, the layer-readiness signal) was
already correct from the earlier fixes; only the release was missing. Not the dispatch-before-listener race
the report suspected -- the composite already writes select-zone as an ATTRIBUTE before dispatching, so
delivery survives a late listener.
check:sfc-parse PASS, check:no-free-identifiers PASS.
profile-config tk, then the token ProfileLoader provides (a tokenUrl
token-service answer or the page's shared token), then window.MAP_TOKEN.
It read only the first and last, so a host using tokenUrl got no Mapbox
token on either of the board's maps.
stage:experimental puts it on the Plugin Studio's Experimental shelf until it
works as a production map; heavy:true makes the Studio unmount it when
left, since it holds up to two WebGL contexts (map + language tree).
iso3-to-iso2 mapped SJM to NO, so every research fetch keyed through it filed
Norway's figures under Svalbard. With SJM -> SJ on main, the four fetched tables now
hold what the pipeline produces for Svalbard itself, re-fetched for SJ through each
script's own code:
network-operators 60 Norwegian networks -> none (RIPEstat: 0 ASNs for SJ)
platform-reachability 15 rows of Norway's OONI -> 15 not-measured (no SJ probes)
social-platforms 8 rows of Norway, 2026-07 -> 4 rows of Svalbard, 2026-09
messaging-channels 3 rows from Norway's store -> none (no SJ storefront)
Norway is not added: it is not in the DX country spine these tables are built over,
so no build produces Norway rows here. Svalbard's own people group rows elsewhere
are untouched, and every other country's rows are unchanged and in order.
Behind any login gate (basic auth, a session cookie, OIDC) the gallery said "Could not load Big Picture":
doxa-maps-spa fetched its same-origin manifest, and plugin-studio its token URL, with credentials:'omit', so
the gate's cookie never went and the request was refused (found on the new dev container; dx-shepherd).
Both now use 'same-origin', which never sends credentials cross-origin, so CDN and CORS behaviour is
unchanged.
check:no-credentials-omit (+ :self-test, in gates:pre) fails any credentials:'omit' in library/ or
plugin-designs/ unless the line says why (// credentials-omit: <reason>). Measured before: exactly these two
sites; it fails on the old code and passes on the fix.
goal (d). Both composites reflect the reader's direction onto their custom-element host with
reflectDir(), and reflectDir read `getCurrentInstance()` at the moment it ran. That call answers only
while setup or a lifecycle hook is on the stack. reflectDir is called from BOTH:
onMounted(reflectDir) -> Vue has set the current instance; this worked
watch(textDir, reflectDir) -> nothing has; the read returned null and nothing was reflected
So a host that declared lang=ar BEFORE mount got dir=rtl correctly, and a host that SWITCHED to ar
afterwards got nothing at all. Arabic then rendered in a left-to-right layout — readable glyphs in the
wrong places, which the comment above this code already names as reading like "a styling quirk rather
than the bug it is".
THAT SPLIT IS WHY IT SURVIVED REVIEW, and it is the part worth remembering: the function is not wrong
and neither caller is wrong; the pairing is. Either line read alone is correct code, and first paint —
the thing anyone actually checks — was right. Only the later switch was broken.
The instance is now captured in setup as _selfInstance, which is the fix research-map.vue already
carries for the same failure in emitFilterState (measured there under Vue 3.5.42).
TWO THINGS DELIBERATELY NOT CHANGED, so nobody has to wonder:
- reflectDir still only ever SETS dir and never clears it. That is documented directly above it — a
host page that declares its own dir owns that decision — so switching ar -> en leaving the attribute
is intended, not a second bug.
- research-map-plus-grid.vue's onMounted(() => { getCurrentInstance()... }) at :422 is CORRECT and is
left alone. Vue sets the current instance for the duration of a lifecycle hook, so that read
resolves. Changing it would be churn, and flagging it would be a gate crying wolf on working code.
When the map failed to construct (Mapbox with no token, for one), every map view
called map.value.on('load') on null and threw "Cannot read properties of null" in
its mounted hook. All seven map views now stop there and show the shared, translated
"Map unavailable" in their status line.
It loaded and constructed Mapbox GL whatever the page asked for, so under
provider=maplibre Mapbox warned about missing CSS and an empty style. The engine now
comes from the library's provider resolution (auto = Mapbox only with a token at hand,
including the injected one), the engine's stylesheet goes into the shadow root, and the
style is the registry pair connectivity-map uses: ink (dark-v11 / OpenFreeMap dark) or
paper (light-v11 / positron). No silent fallback to Mapbox; the map stays gated.
The element declares an instanceId PROP and also exposed an instanceId member,
so Vue warned "Exposed property 'instanceId' already exists on custom element".
The exposed member is the RESOLVED id (the prop, or a generated one), so it is now
resolvedInstanceId. The window.PrayerWalkPlanner registry keeps keying on it,
unchanged, and no reader used the exposed name.
With no token the engine is MapLibre and the basemap is OpenFreeMap fiord, slate-blue
everywhere: sea and land 13 units apart, blue-grey borders. The Mapbox Prayer map is
Standard at night: near-black navy sea, slate land, rose borders. The keyless map now
recolours fiord to the colours measured on the Mapbox render of this leaf (ocean
#03112d, land #323f52, border #c95d74), on every style load, on MapLibre and the dark
basemap only. The Mapbox path is untouched.
The Research Gallery keeps main's source and bundle: per its owner (rega,
relaying the Driver) the board appears under Experimental, which the
gallery discovers from the manifest with no gallery change.
studio landed upg100WallIntroA / Emph / B in all eleven locales (landing 10, f4871401) and kept the legacy
upg100WallIntro, so this consumer can land without a flag day. dx-shepherd UPGHTML / studio END-STU-WALLINTRO.
★ THE CATALOGUE NO LONGER CARRIES MARKUP. vue-i18n warned "[intlify] Detected HTML in 'Hand away the prayer
task…'" twice per tab pass, and it was right: I measured every en catalogue area and upg100WallIntro was 1 of
1 — the only key in the whole tree with a tag in it. Not a convention I was following; my outlier.
⚠ THE OBVIOUS FIXES DID NOT APPLY, which is why this took a design rather than a one-liner. "Markup in the
template" and "<i18n-t> interpolation" both assume this profile renders the sentence. It does not: `intro` is
a STRING PROP on the library PosterWall, rendered there with v-html (PosterWall.vue:955), and that component
has no slots. So the markup must arrive inside the string; the only question was who builds it. Building it
here keeps the catalogue clean with no library change.
★ AND IT SHRINKS WHAT v-html TRUSTS, which matters more than the warning. v-html on a whole catalogue string
trusts the CATALOGUE: any tag a translator or a bad merge puts in any of the eleven files renders as markup.
Composing from parts escapes each part and emits exactly one tag I control, so the trusted surface goes from
"every translation of this key" to this line. Verified: a tag planted in a part comes out as <img …>,
not as an element.
THREE PARTS, NOT A HEAD+EMPHASIS WRAPPER, because the emphasis is not last in every language. All eleven have
exactly one <strong> and no other tags, but de, hi and zh carry real text AFTER it (" beten.",
" प्रार्थना करते हुए।", "為他們禱告。") where the others end on a full stop. A two-key split would have
silently dropped those tails.
RENDERS IDENTICALLY BEFORE AND AFTER: while the three keys were absent it fell through to the legacy single
key, unescaped, exactly as before — escaping THAT would have printed a literal "<strong>" to the reader, a
visible regression traded for a console warning. Verified against main's catalogue: all eleven locales
recompose A + <strong>Emph</strong> + B to the current upg100WallIntro BYTE-FOR-BYTE, which is the same check
studio said they would run before committing, run independently here rather than taken on trust.
check:sfc-parse PASS. check:i18n-keys-exist still reports the three keys missing IN THIS WORKTREE, which is
correct and expected — they are on main and this branch has not merged it yet; the merge in my landing window
resolves it.
The folded explainer justifies showing a COUNT per country rather than a percentage with
"106 of these 175 countries hold one or two groups". Audited today against the roster:
both exactly right. They were also typed in, which is how every stale claim I have fixed
this flight began, so they are computed from the same pin rows the map draws and the
sentence cannot outlive the data. Renders 106 of these 175, unchanged.
Measured at 390x844: the Gap / Resources toggle rendered 48x26 and 129x26, and the
legend's fold button 316x32. None of them FAILED anything — WCAG 2.5.8 puts the AA floor
at 24x24 — but they are small for a thumb on a map that is mostly drag and pinch, where a
missed tap pans the map instead of doing what you asked. They are 54x44 and 316x44 now,
narrow screens only: on a desktop they sit in a 290px card beside a pointer and 26px is
the right density. The legend grows 124px to 154px of an 844px screen, which it can afford.
TWO MISTAKES WORTH RECORDING, both mine, both caught by measuring rather than by reading:
1. Written above .gm-mode's own rule, the media query changed the font size and nothing
else. A media query adds NO specificity and that rule sets min-height:26px, so the
later plain selector won. Reading the computed style is what told me — min-height
resolved to 26px while my font-size had applied, so the rule was live and merely
losing. It has to come last, and the comment says so.
2. Moving it "to the end" put it after the CSS argument, where it concatenated onto the
style element's own id — so the stylesheet lost the rule and the id became
"@media(...){...}doxa-gospel-map". The build passed: it is valid JavaScript. Only
re-measuring the rendered button showed it, which is the second time today a green
build meant nothing about the thing I had changed.
Still 32x32 on a phone: the five .mcb toolbar buttons, which are the shared library
control and every map's, not this leaf's. Reported rather than patched here — one leaf
quietly disagreeing with the fleet about control size is worse than the size.
TWO ITEMS, both found by checking claims rather than accepting them.
★ 1. THE MAPBOX "missing CSS declarations" WARNING (rega END-REGA-SRV / dx-shepherd END-DXS-U100C). The
routed hypothesis was "the leaf doesn't load mapbox-gl.css". It does not — and neither does
cities-internet's leaf, which does NOT warn, so that was not the difference. The real difference, measured:
connectivity-map.vue:398-399 calls `engineStylesheetUrl(provider)` then `await ensureEngineCSS(...)`. This
profile called NEITHER; it hand-rolled its own injection, which failed two ways:
(a) it ran only when the map element was inside a ShadowRoot, so on a standalone BUILT leaf — light DOM
— no stylesheet was ensured at all; and
(b) even in the shadow case it fired and forgot, so `new gl.Map` could run before the sheet parsed.
Mapbox checks its declarations AT CONSTRUCTION, which is exactly when the warning appears.
ensureEngineCSS covers both DOM cases and AWAITS load/error with a 2s cap. It also takes the PROVIDER's own
URL, where the old line asked for mapbox's stylesheet even under MapLibre behind an !isMaplibre guard — so
a maplibre page now gets maplibre's sheet or none, and never touches api.mapbox.com, which is the keyless
contract the maplibre-embed gate enforces. The shadow injection stays as a belt for a partner embedding the
custom element with no wrapper <link>; both paths check by href, so neither duplicates the other.
★ 2. THE SELECTION PACKET'S COVER WAS HARDCODED ENGLISH. Found while checking whether batch selection was
already done (END-DXS-U100B — it is, and render-verified). selectionAsList built its label with
`names.join(' + ')` and `` `+${n} more` ``, printed onto page 1 and into the print dialog title. So a reader
choosing three territories under ?lang=ar got their names in Arabic joined by an English tail.
zone_join / zone_join_more already exist in all eleven packet-strings blocks — translated once for exactly
this shape when the zone-title generator landed — so this is reuse, not new strings. The block is resolved
with per-KEY fallback to English, as packet.mjs and packet-render already do it, so a half-translated locale
cannot blank a key that en has.
check:sfc-parse PASS, check:no-free-identifiers PASS.
⚠ THE 0×0 CONTAINER IS NOT MINE AND NOT FIXED HERE — diagnosed and routed to studio. It is
doxa-maps-spa's HEAVY_ELEMENTS set: the gallery computes `heavy: HEAVY_ELEMENTS.has(tag) ||
HEAVY_ELEMENTS.has(id)`, the set contains `my-upg-100-list-map` (the inner MAP element) and the gallery
mounts the COMPOSITE, so neither the mfe tag nor the bundle id `my-upg-100-list` matches. A non-heavy tab
mounts under `v-show` + `:hidden`, i.e. display:none, and a map built into a display:none container is 0×0
by construction. The gallery's own comment says heavy means "anything that owns OR NESTS a map" — the
composite nests one, so it qualifies and is simply missing from the list. regions-dashboard/regions-mfe has
the same shape and the same gap.
NOT caused by my inert change, despite the timing pointing there (absent at a1c8f3f6, present at 5d83febd):
`inert` sets no layout at all and nothing in the tree keys CSS on [inert], so it cannot collapse a box. The
window contained seven candidate commits and the timing alone could not distinguish them.
The table said Jing and Pana were "outside the twelve-province subset". Both provinces
are IN that file — Guangxi and Yunnan. Measured: Jing sits 8.7 km outside Guangxi's
outline and Pana 5.4 km outside Yunnan's. They are across the border, which is a fact
about one coordinate standing for a whole people, not about a missing province. I shipped
that wrong explanation twice, in the meta and in a commit message.
Sourcing China's COMPLETE 34-unit ADM1 from geoBoundaries changes neither distance by a
metre — same source geometry, 22 more provinces, none of them theirs. So better data is
not the answer here, the downloaded file is deleted rather than staged for nothing, and
the finding is written down so nobody repeats the experiment.
A point within 15 km of exactly ONE province now joins it, carries near_border_km on its
row, and is counted in meta as placed by nearest province. Two provinces inside the
tolerance leave it unassigned: a near-miss is a hint, and a hint between two answers is
not one. 1,007 assigned becomes 1,009 and unassigned 2 becomes 0 — with the two
exceptions visible per row rather than dissolved into the total, because gospel-internet
reads this file and should be able to tell a point inside a province from one 9 km out.