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.
A media query adds no specificity. The mobile block's
`.map-toolbar { gap: 6px }` is (0,1,0) exactly like
`.map-toolbar--square { gap: 0 }`, and it comes later in the sheet, so it
won: on every phone a square toolbar fell back to separate buttons inside
one ringed panel. The shape broke precisely where the Driver is most likely
to look at it.
The --no-geocoder pair is (0,2,0) and outranked the variant whatever the
order, so it needs the two-class answer.
This is the same lesson as the goal cards' gap-vs-margin: a ported mechanism
is only ported if it still WINS, and specificity is the part that decides
whether it does. Found by following the phone thread rather than by looking
at the variant again.
check:square-map-controls now asserts the override is inside the mobile
block, that the two-class form answers --no-geocoder, and -- as a control --
that the base rule really does come first, which is why the override is
needed at all. Removing it turns the gate red.
The CSS comment explaining it cost one more mistake: I wrote the selectors
in backticks, inside the template literal that holds the stylesheet. A
backtick ends the literal, which is the trap check:sfc-parse names in its own
failure text, and it caught me with it.
THE MATRIX WAS UNDER-REPORTING, AND MY OWN KIT LANDING IS WHY. This gate scanned
plugin-designs/<bundle>/** and nothing else, which was correct while every panel lived inside its
plugin. The dashboard-kit extraction moves panels into library/components/dashboards/ and the bundle
still COMPILES them, so a source rule stopped firing the moment the file moved, with the code
unchanged. Measured: kit landing 4c-3 moved DashboardFacetRail.vue, which holds three
toLocaleString('en-US') calls, and the matrix went from 13 warns to 10. Nothing improved; the audit
stopped looking. LOC-ENUS has no `shipped` matcher, so the built-bundle pass could not catch them
either.
This is not three lines. Every kit extraction, and every future one, silently shrinks this audit —
the more code becomes shared, the less of it this gate sees. So each bundle is now audited over the
library files it actually IMPORTS, resolved from its own `@map/...` specifiers and followed
transitively. Attribution stays per-bundle, which is what keeps the matrix actionable: a library file
compiled into four bundles is reported against all four, because all four ship it.
★ MY FIRST VERSION OF THIS WOULD HAVE TURNED THE BUILD RED WITH FOUR FALSE POSITIVES, and finding
that is the reason I measure a gate's blast radius before handing it over:
- library/api/sources.json got swept in and LIST-FETCH reported it three times as "a file that
fetches the list". It is the endpoint REGISTRY — a .json file declares endpoints and fetches
nothing. The per-bundle walk has an extension filter; my sweep did not. Now it does.
- library/api/DataSourceManager.js:464 was reported for LIST-DSM, on the line
`const dataSourceManager = new DataSourceManager()` — the singleton the module exports. That rule
exists to catch a CALLER constructing one without profileConfig. A module is not a bypass of its
own contract, and reporting it told three owners the library was broken because the gate read a
definition as a use.
AFTER, on main: 13 warns (was 10), 0 bypasses, PASS, and 19 bundles audited instead of 18 — following
imports gave one more bundle a row. Every restored warn is LOC-ENUS, which is warn-only by the
Driver's held numerals decision, so nothing here asks anyone to change a number.
Gate only. No product code, no rebuild. check:universal-props is not in gates:pre or gates:post, so
this cannot turn a build red on its own; it is run directly and by check:studio-all.
LOC-ENUS flagged any en-US format literal. Two of regions-dashboard's three
warns were the 'en-US' inside a CATCH, whose try formats with
`${locale}-u-nu-latn` -- a verbatim clone of DX-life's RegionDetail.vue:16,
Latin digits with the reader's grouping. The fallback exists because a bad
locale tag throws RangeError in toLocaleString and blanking the page is
worse than one wrong separator.
So the matrix's locale column said "ignores the locale" about code that
honours it, which is the kind of false warn that teaches people to stop
reading the column.
Measured before changing the rule: of the NINE LOC-ENUS sites in the tree,
exactly TWO are such fallbacks. The other seven are genuinely operative and
still flagged, so the tree goes 10 warns -> 8 and only those two move.
The exemption needs BOTH halves -- a catch AND a locale-derived primary --
because a lone catch must not excuse a format that never consulted the
locale. That is the one rule here that can widen by accident, so four
adversarial shapes are pinned as a self-test: the real pattern is skipped,
while a catch around a hardcoded primary, a catch whose primary never
consulted a locale, and a plain operative call are all still caught.
Loosening the exemption to a bare catch makes the self-test exit 3.
regions-dashboard keeps ONE warn: RegionCountryMap's popup, fixed en-US on
purpose, carded and evidenced by DX-life's own convention. I did not add an
ALLOW for it -- ALLOW is keyed per BUNDLE, so it would also blind regions to
a future operative en-US, and a visible warn on a decision already taken is
cheaper than a blind spot.
check:square-map-controls, registered with a gates:post token.
It reads node_modules/maplibre-gl/dist/maplibre-gl.css and compares our
ported values against THAT FILE rather than against a remembered number,
which is possible only because the look the Driver asked for is the engine
default: DX-life adds NavigationControl and overrides nothing, so the target
is published.
It asserts the three deliberate deviations as deviations -- the engine's
29px is not adopted, there is no overflow:hidden, and a forced-colors block
ships -- so each stays a stated choice rather than decaying into a mistake
nobody can see.
And it asserts the blast radius: round is the default and none of the nine
MapToolbar consumers passes controls=. Opting demo-map in turns it red,
which is what makes the silence mean something.
Its CSS assertions read the text passed to useShadowStyles, not the file.
The first run went red on "we do not adopt 29px" because the prop's own doc
comment quotes the engine's rule, 29px and all -- a checker that cannot tell
a comment from a declaration keeps finding the documentation it stands on.
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.
The Driver asked for square controls by name. The fact that shaped this:
the square look is not a DX-life design at all. Their CountryMap.vue adds
NavigationControl + FullscreenControl and overrides nothing, so what he
admired is the ENGINE'S default grouped card -- which makes the target
published and exact instead of a matter of taste. Every value here is read
from node_modules/maplibre-gl/dist/maplibre-gl.css, and the check compares
ours against that file rather than against my memory of it.
SQUARE IS NOT A RADIUS SWAP, which is why it lives on the TOOLBAR. Round
controls are separate floating circles with a gap and a shadow each; the
engine's square form is ONE panel that owns the background, the 4px corners
and the 2px ring while its buttons own nothing and are divided by 1px lines.
Setting border-radius:0 on the buttons alone gives squares floating 8px
apart -- neither shape. So MapToolbar takes controls="round"|"square", and
MapControlButton keeps a shape prop only for a button used outside a
toolbar, where a lone control gets the engine's 4px.
THE 29px IS DELIBERATELY NOT PORTED. Ours stay --map-btn-size, capped to
32px in this toolbar. 29px would cut the touch target below what this
project has already ruled on twice, so the variant takes the shape and
leaves the size. It is therefore not pixel-identical to the engine, and
that is a choice, stated.
No overflow:hidden. Clipping was the quick way to round the panel, but it
also clips a focus ring on any inner button, and the engine does not do it
-- it rounds the first and last buttons instead. Ported as their mechanism.
MapControlMenu teleports out of the toolbar, so clipping would not have
hidden a menu; the focus ring was the real cost, and the invisible kind.
Forced colours ship with it, which the round buttons never had: an rgba()
ring and a #ddd divider both vanish under a replaced palette, leaving one
undifferentiated block where the controls were.
BLAST RADIUS: round is the default and none of the nine MapToolbar
consumers passes controls=, so every map renders exactly as before.
Asserted, not claimed -- opting one consumer in turns the check red.
The regions dashboard is NOT switched. Its map already uses the engine's
native controls, ported from DX-life, so it already looks like this;
replacing them with ours would risk clone parity to arrive at the same
picture.
One near-miss, recorded because of how it hid: the first cut added a second
:class to a <button> that already had one, and Vue rejects a duplicate
attribute, so the SFC did not parse. check:sfc-parse said so IMMEDIATELY --
and I had piped it through `head -2`, which printed its counting line and
cut off the FAIL beneath. The block count fell 133 -> 132 in that same
output, I noticed, and then checked script TAGS rather than parseability and
talked myself out of it. Two separate signals, both read past. The two
bindings are merged into one; the count is 133 again.
key-cities now resolves its engine through the library (exp-shepherd, 71af822a, on main at c2784f5a), so the
'key-cities:ENG-STYLE' allowance is stale. Removed; the gate passes against main's key-cities (18 bundles),
and it fails on the pre-change file, so this commit must land on or after c2784f5a.
rega measured it on all 40 dev pages: on a cold start every page's first load got "504 (Outdated Optimize
Dep)" on vue.js and pinia.js, Vite re-optimised, and nothing mounted until a reload. optimizeDeps.include now
names every bare runtime import in library/ and plugin-designs/ (vue, vue-i18n, pinia) plus jspdf, loaded
lazily on poster export. The map engines and d3 are pinned CDN scripts, not imports. Needs eyes: a cold
start plus one page load (rega).
gospel-plugin's trap (COMMON.md): a CSS rule moved past the first argument of useShadowStyles concatenates
onto the style id ('@media(...){...}doxa-gospel-map'). That is valid JS, so the build stays green and the rule
is silently lost; the brace-balance scan never sees it because the id is not CSS. The walker now folds the
second argument too, and a literal id must match /^[a-z0-9-]+$/ (a computed id is left alone). Measured on
the current tree first: 0 offenders, so this breaks no owner. Self-tests cover the trap and a plain id.
From dx-shepherd (END-DXS-CSSID).
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.
One real conflict, in package.json's description map: main added check:no-credentials-omit
and its self-test beside a line both sides carry. Took main's side, which is a superset;
my own script and description entries are elsewhere in the file and untouched. Verified by
parsing the result rather than by eye — mine and main's entries both present.
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.
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.
886.14 kB, up 53 kB from 833. ★ THE GROWTH IS OTHER AGENTS CATALOGUE WORK, not a leak, and I chased
four wrong explanations before measuring it: not the new CLDR codeMappings.json (0 hits in the bundle,
nothing imports countryName.js yet), not the 183 kB -web geojson the merge added to my data folder (getJSON
fetches it at runtime by filename, so it is never bundled), and only 3 new keys landed in messages.json
itself. The delta is the SHARED areas my bundle legitimately reaches: gospel.json +5134, pplr.json +1976,
gospelInternet.json +1656, neighbor.json +789 chars in EN alone, x11 locales.
check:i18n-treeshake PASS across 26 bundles — each carries only the areas its code asks for — so this is
correct behaviour, not the import.meta.glob leak it resembles.
Leaf integrity re-derived by hand: 3 recomputed, 0 mismatches.
generate-library-index took the first line of every file as its description, which for a .json file is data:
the Data section read '"_meta": {', '"value": "AFG",' and a run of ISO codes (flagged by simpleresearch).
A .json file now uses _description, then _meta.description, then _meta.purpose, and otherwise says
'(no description)'. iso3-to-alpha2 now reads as what it is.
A page's own reduced-motion reset never crosses a shadow boundary, so library components rendered inside
custom elements slid, faded and spun for readers who asked for less motion: thirteen of them, including the
legends and map controls (regions-plugin found it on ProgressBar and named the class).
useShadowStyles now injects one prefers-reduced-motion reset into each shadow root it styles, which covers
every component styled through it, including later ones. PosterDialog, FilterDropdown and SearchFilterHeader
style themselves with <style> blocks and carry their own guard.
check:shadow-reduced-motion (in gates:pre): useShadowStyles must inject the reset, and every animating
library component must be covered. It fails without the reset (negative-tested) and found the three
<style>-block components on its first run.
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.
Arm C catches the live RTL defect in the two plus-grid composites: a top-level function that reads
getCurrentInstance() and is handed to a watcher or an addEventListener runs with no current instance,
so the read returns null.
IT MODELS VUE RATHER THAN BANNING THE CALL. Inside setup, and inside a lifecycle-hook callback, the
call is CORRECT — Vue sets the instance for the duration of a hook — so research-map-plus-grid.vue's
onMounted(() => { getCurrentInstance()... }) is deliberately not reported. A gate that flags working
code gets ignored, and this one has already had to drop five such hits.
★ AND ITS OWN EARLY-OUT MADE IT USELESS FIRST, WHICH IS THE SECOND TIME TODAY FOR THIS EXACT HOLE.
The file filter only ran the arms on sources containing `immediate: true` or `async` — arm A's and
arm B's triggers. Neither of the two files carrying the RTL bug contains either word, so both were
skipped before a single arm ran, and the gate reported the tree clean while the self-test stayed
green. A fixture is text and never meets the file filter, so no self-test could have caught it; only
running the gate against the un-fixed code did, which is why dx-shepherd asked for exactly that.
An hour earlier I diagnosed the same shape in check:i18n-keys-exist — it skipped any file that never
wrote the word "tStatic", which is how a planted poison key passed — and wrote up the rule that a
cheap early-out is part of a gate's COVERAGE, not an optimisation. Then I wrote the hole again here.
So the fix is structural rather than a correction: the precondition is now DERIVED from a per-arm
ARM_TRIGGERS table, so adding a fourth arm forces you to extend the filter in the same edit.
One more false positive the self-test caught before it shipped: `watch(textDir, reflectDir)` has no
inline callback, so brace-matching forward from the call walked past it into the NEXT statement's
onMounted body and reported a getCurrentInstance() nowhere near a watcher. A call's callback is now
bounded by that call's own parenthesis span.
Verified both directions, on the real files and not only on fixtures: FAILS on the shipped code
naming both composites, PASSES with the fix, and does not flag the legal onMounted read.
Gate only, no product code, no rebuild — rule 5B because it lives in internal/.
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.