Every generated leaf <head> preloaded mapbox-gl.js whatever the provider, so code that used the
bare `mapboxgl` global always found one, even on a MapLibre map. Removing that peer (it sends
every visitor's IP to Mapbox under provider=maplibre) turns each such site into an error.
engineFor(map), next to isMapboxMap in useMapProvider.js, asks the map which engine built it:
- useSelectedPin: the selected-pin Marker
- useMapEvents: the pin-fallback and region Popups
- useMapPoster: main already builds the hidden map with the source map's engine (1cbfb3aa);
its choice now goes through this one helper instead of a second inline sniff
- pplr-data-maps: glFor is now this helper; the geocoder awaits ensureEngineScript first
- key-cities: ensureEngineScript('mapbox') before reading the global
Measured with every api.mapbox.com tag stripped from the served leaf (no peer at all for
key-cities), before -> after:
pin select under MapLibre, on cities-internet, country-posters, doxa-research-map and
doxa-simple-map: "reading 'Marker'" -> marker drawn; country-posters region popup: none ->
popup; PPLR search: no input -> suggestions (both engines); key-cities: "mapbox-gl not
loaded" -> map loaded. Mapbox rows unchanged (they passed before and after).
The poster UI could not be driven: PosterDialog never opens in country-posters or
doxa-research-map (they pass props it does not declare and no modelValue/getMap), reported
separately. useMapPoster itself was rendered from a real map with only its engine on the page:
MapLibre "mapboxgl global not found" on the pre-1cbfb3aa library -> a 400x300 PNG with 88 colours.
Driver request, 2026-09-25: "add a spot on the studio for placing a Mapbox token, or setting the URL
for where the Mapbox token API is from, like doxa.life has an API token".
A 🔑 Map token button in the studio header opens a small panel:
- paste a Mapbox PUBLIC token (pk.…) — a secret sk.… token is refused, the page is public;
- or a token-service URL: the studio fetches it (JSON {token|tk|accessToken} or a bare string, the
leaf pages' contract) and uses the pk.… it returns. The maps never read a token URL themselves, so
the studio resolves it and hands them the token;
- Clear returns to the token built into the page. A dot and a status line say which is in use.
Precedence: your token > a token from your URL > the host's tk > the page's built-in token. Kept in
this browser's localStorage only; applying re-mounts the open map and publishes window.MAP_TOKEN for
maps nested inside composites. Labels in all 11 locales (i18n parity PASS).
Tested in a browser on the built studio: shows the built-in source; sk.… refused and not stored; a
pasted pk.… reaches the pane's profile-config and window.MAP_TOKEN and survives a reload; Clear
restores the built-in token; a token URL is fetched and applied. https://doxa.life/api/maps/token is
refused cross-origin from another site — the panel says so and keeps the current token.
I landed glFor as a local copy of pplr-data-maps' helper and flagged the
duplication as a decision on the record rather than drift. It did not need to be
a decision at all: library/composables/useMapProvider.js already exports
isMapboxMap(map) — the same per-instance test, documented for the same reason
("works even on dual-engine pages where both globals are loaded").
So the copy is gone and the part that could drift is delegated. What remains in
the plugin is the namespace choice, one expression with nowhere to drift to.
When exp-shepherd's engineFor(map) lands, that line goes too.
WHAT IS PROVEN, and what is not, because the browser slots have been saturated
for fifteen minutes and RULE 3 says gates green before committing. This is a pure
expression substitution, so its logic is provable without a GPU:
_chk-glfor-equivalence.mjs stubs the globals, builds BOTH implementations, and
compares their decisions across every engine combination — mapbox map with both
globals, maplibre map with both, either alone, and the case the whole fix exists
for: a MapLibre map with no Mapbox global at all. Identical in all of them.
That establishes the refactor cannot have changed the decision, so the pin-click
integration result already measured still applies. It does NOT re-run that click;
the browser re-proof is queued and will run when a slot frees.
check:sri PASS, composite freshness PASS.
Of the 369 Doxa groups with nothing in their language, 94 had no bridge either,
and 93 of those were Deaf — 17.5 million people, the largest untouched number on
the map. That was structural, not bad luck: every bridge here is a spoken
language widely used in the same country, and a spoken bridge is worthless to a
Deaf community, so the build correctly refused to offer one and then had nothing
else to say.
SUN is a signed language built to be learnt across sign languages, with the Old
Testament, New Testament and Bible stories in it. It is carried the way a spoken
bridge is — something to learn, no substitute for Scripture in a group's own
sign language — in its own columns, never folded into the access rung, which
stays the honest "nothing found in their language". 28 countries where it is
already being taught come with the named local Deaf church or organisation
doing the teaching, which is the difference between a resource and a lead.
Only URLs verified by their content ship. sun.bible answers 200 for
/sun/<anything> and /resource/<anything>, so a per-language URL built by pattern
would look valid and could be empty; the fetcher re-measures that catch-all on
every run rather than trusting a note. Hosts left out are listed with the reason,
including three that are reachable by a person but not by a script.
Doctrine was checked harder here, not less. The Deaf are the most heavily
targeted group in this dataset by publishers this build must never carry, and
the thinner the pickings the stronger the pull. SUN is Wycliffe Associates'.
Screening the result exposed the bigger hole. The doctrine scan read the pin
index only, and within it skipped the Words of Life and Scripture Earth fields,
so the access table — the one the page renders and the one exported to CSV for
posters and QR codes — was never screened at all. The least supervised links in
the build were the ones most likely to be printed and handed to a stranger.
Scanning both, and both fully, takes the screened count from 6,497 to 16,960.
The gate also now proves the server holds these tables. It adopted a server from
another worktree on the default port and reported PASS on it: all three
manifests are byte-identical, because a manifest describes bundles and the
change was to a data asset it never mentions. A planted excluded-publisher link
went undetected, which is the worst possible failure for this particular check.
Driver decisions (cards BUILD-OUTPUT-IS-ONE-FOLDER, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE).
- public/ holds only <tree>/ and a generated README.md. The build no longer writes public/index.html,
public/assets/ or public/__gallery/; `rm -rf public && bun run build` recreates exactly these two.
- The Plugin Studio is <tree>/index.html (tree-base "."), replacing the staging-forge chooser; nothing
is written above the tree. check:staging-lang, which tested that chooser, is retired with it.
- Assets are gathered in a temp directory and the fetched allowlist staged into <tree>/assets/ as
before. fonts/, images/, brand/ and legacy/ existed ONLY in public/assets: they are sources and move
to data/ (git mv). Poster previews keep their tracked sources in plugin-designs/*/assets/previews
(all 378 verified present).
- __gallery/ is archived out of public/ (a copy kept in the umbrella's _qa-cycle/archive) and out of
the build; generate-gallery renders only on request, into a directory you name, never public/.
- Native shells load the output folder: capacitor webDir public/<tree> (cap copy needs an index.html
there), Tauri frontendDist + window url relative to it. internal/sync-app-shells.mjs keeps both in
step with the tree's name on every build, so the coming rename stays one config line; the Capacitor
start page resolves its leaf tree-relative.
- dropin.include now lists only extras (README.md): the tree is always shipped and never named twice;
excludes are tree-relative ({tree}/...). clean.mjs removes the old root entries from existing
checkouts; the build lock and public/.gitignore's rules move out of public/.
- check:wp-embed read the BUILT manifest from gates:pre, before the build, so a from-scratch build
could never pass; it runs in gates:post and reads the tree name from config.
- Stale bundles refreshed by the clean build (key-cities, template-bundle, wall-of-people-groups);
the CLI doc's quoted scripts re-synced; capability exemplars regenerated; the '#' note leaves
check:stale-sweep-claim's command (it was already in //scripts).
Proof: rm -rf public && bun run build -> public/ = README.md + doxa-maps/; check:sri, check:drop,
check:parse, check:public-hygiene, check:quoted-scripts, check:capability-exemplars PASS; all 57 pages
(the front page + 56 leaves) mount under /doxa-maps/ on this tree. check:facet-cascade is red as on main.
Cell E verification for Big Picture and Simple Research needed the
gospel leak gate's method on two other bundles. That gate hardcodes its
four leaves, its element tag and its single catalogue file, so pointed
elsewhere it finds no shadow root and silently compares nothing. This
one takes the leaf, the tag and the locales as arguments and compares
against EVERY catalogue area — a COMPOSITE has no t() calls of its own,
so its chrome comes from the children it nests.
SIMPLE RESEARCH CELL E IS CLEAN, independently measured:
research-map-plus-grid in ar/zh/ru/hi, tree identity checked first, no
leaks, 0 page errors, and 86/34/27/43 comparable keys — so the
comparison was non-vacuous in every locale rather than "0 leaks over
nothing". A second instrument agreeing with the owner's reading is what
the verifier role is for.
★ TWO FALSE POSITIVES, BOTH OF WHICH LOOKED LIKE TEXTBOOK DEFECTS. The
first run reported 3 leaks x 4 locales on Big Picture, and all three
keys ARE translated in every catalogue, so the pages really were showing
English where a translation existed.
DATA CELLS. "Unengaged" rendered in .rd-td inside .rd-grid — table
data. people-groups-grid.vue:150 records the field as
{value:'unengaged', label:'Unengaged'}, 100% populated, and
dashboard.config.js:132 counts 2,081 Unengaged / 27 Engaged. It is an
API VALUE that happens to equal a catalogue string, which is the
population rega's method excludes by name. Whole-label matching stops
a substring impersonating a label; it cannot stop a data cell
rendering the same text.
FACET VALUES. "Other" rendered in span.fr-lbl in the facet rail, which
LOOKS like chrome. The template is
`<span class="fr-lbl">{{ r.label }}</span>` with `{{ r.count }}`
beside it, and the component's own comment calls it "the 'Other'
bucket". The string comes from row data, not from t('regions.Other').
Both are excluded BY ROLE rather than by class. The facet exclusion keys
on the row carrying a COUNT, so the rail's own section headings stay in
scope and a real leak there is still caught — excluding .fr-lbl
wholesale would have blinded it.
⚠ NEITHER IS GOSPEL-SPECIFIC. Any bundle with a data grid or a facet
rail produces these, and whether "Unengaged" and "Other" as DATA should
be localised is a genuine product question — but a different one from
cell E, and filing it as a leak would send someone to fix correct code.
⚠ BIG PICTURE CELL E IS NOT CLAIMED CLEAN. The confirming run after the
facet exclusion came back RC=75 (no browser slot within 900s), and the
run before it still reported regions.Other. Whether the exclusion takes
effect is unverified, so the cell stays open rather than being marked
on an unconfirmed reading.
The map is built raw, for the provider switch, so it never installed the RTL
text plugin — the same opt-out that cost it the engine stylesheet. Without the
plugin those labels draw as disconnected glyphs in the wrong order, which is
unreadable rather than merely ugly.
This was never an Arabic-locale bug, which is why it went unseen. The leaf does
not localise basemap labels, so every place is drawn in its own script: over
Arabia, 57 of 61 rendered symbols are Arabic for every viewer whatever their UI
language. An English reader sees broken Arabic they cannot evaluate.
It installs after the engine global resolves rather than at bundle entry. This
bundle injects its engine lazily, so at entry time there is no engine to install
into and the call would silently do nothing.
The gate pans to Arabia before judging. Registration alone proves little, since
the plugin is registered lazily and a freshly loaded map reports "deferred"
whether or not the shaper can actually be fetched; only real RTL text on screen
turns it "loaded". Asserting without panning would have failed a working map.
The census against doxa.life/research found our grid saying "Full profile" and
"Load more" where the page we clone says "Full Profile" and "Load More". Case,
nothing else — the kind of difference that is invisible in review and obvious
side by side. Fixed in the English catalogue and in the fallback literals; the
other ten locales are untouched, since none of them has a capital to drift.
The filter toggle's "Hide Filters" is now SANCTIONED in the preset rather than
fixed, with the Driver's words, the date and the card: our filter panel is open
on load because the Driver reported not being able to find the dropdowns, so our
toggle offers to hide where theirs offers to show. Same control, other state.
Everything else the census found went to the Driver as
DECIDE-RESEARCH-CENSUS-DIFFERENCES rather than into the allowlist — the legend's
prayer-band wording, our four additions, and their page title.
★ AND THE CENSUS TURNED UP A REAL DEFECT WHILE CONFIRMING THE "DATA DRIFT".
The drift is OURS. Measured against pray.doxa.life/api/people-groups/list with
this repo's own thresholds (prayer-progress.js: 0 or null = Needs Prayer):
API Needs Prayer 1,162 (58.8M) Has Prayer 945 (147.5M)
doxa.life Needs Prayer 1,162 (58.8M) — matches the API exactly
our legend Needs Prayer 1,417 (69.1M) Has Prayer 690 (137.1M)
254 people groups the API says have someone praying are drawn and counted by our
map as having nobody. Their snapshot is right and we are wrong. Recorded on the
card with a candidate cause (useMapLayers.js:226 reads only the camelCase
peoplePraying and defaults to 0, where getPrayerLevel also accepts the snake
_raw.people_praying) and passed to the shepherd for its own card rather than
fixed inside a wording commit.
RegionCountryMap built its popup from window.mapboxgl and returned SILENTLY when
that global was absent — the worst shape of failure, because a pin click would
simply do nothing and no error would appear. It has been passing only because
every generated leaf <head> statically loads mapbox-gl, so under provider=maplibre
a MapLibre map was opening a MAPBOX popup. studio is removing those static tags,
since a Mapbox script leaks the visitor's IP to Mapbox even for a partner who
deliberately chose MapLibre; the moment that lands, these popups vanish.
glFor(map) asks which namespace owns the map instance. It checks MapLibre FIRST
with an instanceof test, because a page can legitimately carry both globals and
only the instance knows which one made it.
The controls read is fixed with it. It was `window.mapboxgl || window.maplibregl`,
which happened to work through its fallback but preferred Mapbox even on a
MapLibre map — the identical bug one line away from the same silent failure.
The proof fails first by construction. The probe BLOCKS the Mapbox script, then
asserts window.mapboxgl is genuinely undefined BEFORE clicking, so the pass that
follows is a pass of the thing that used to break: with no peer, MapLibre present,
a pin click opens "Deaf Liechtensteiners … Population 400"; under Mapbox it opens
"Vinza … Population 33,000", unchanged.
glFor is copied, not imported: the same function is in another plugin's folder
(pplr-data-maps/src/utils/pplrData.js:64), and importing across plugin folders
would couple two plugins through a path neither owns. library/ is the right home
the moment a third consumer appears, and both copies should die in that commit.
check:element-census-vs-live now has a preset for this lane, so the question
"what is on our page and not on theirs, and the reverse" has an answer that runs
instead of an opinion.
★ THE LEAF IS THE COMPOSITE, NOT THE GRID ALONE. people-groups-grid is a copy of
DX-life's UupgsList.vue, which is the BODY of doxa.life/research — but that page
is a map plus that body. MEASURED: censusing the grid leaf against /research
reports 45 omissions, and every one of them is the research map's chrome
(Clustering, Off/Native/MST/Network, the tab strip, the legend) — nothing to do
with the grid. A list that long and that wrong teaches a reader to ignore the
gate. research-map-plus-grid is map + grid, which is what /research IS, and it
lines up 98 strings against their 95.
The negative control is the pin hint ("someone still needs prayer"), and the run
confirms the census SEES it as ours-only — without that, every count it prints
would be worthless.
The sanctioned list is deliberately EMPTY. The first run's 14 additions and 14
omissions have gone to the shepherd as findings; an allowlist written by the
owner of the code to make its own gate green is not evidence. Most of the
omissions look like wording rather than absence — their "Full Profile" against
our "Full profile", their "Load More" against our "Load more", their legend's
"1+ People Praying" against our "Has Prayer" — but that is exactly the call the
Driver makes and not the one this file should quietly make for them.
The gate defaults to the deployed site, so a failure line reads as "this code
is broken" when it means "this code is not published yet". That misread cost
two wrong diagnoses in one day: the resources map and the connectivity map both
reported the stylesheet absent while the fix for exactly that sat verified on
disk. Served locally, the same gate passes all thirteen leaves.
The identity guard cannot close this. It fingerprints the manifest to catch the
wrong worktree on a localhost port and deliberately passes a remote target,
because lagging behind a checkout is a deployment's normal state. The output
was the thing at fault: the one number that tells the two failures apart was
never on screen.
The resources map builds its map raw, for the provider switch, and in doing so
opted out of the CSS injection useMapInstance does for every other leaf. That
was fixed here first with a local copy, because nothing shared needed changing
for it. The shared helper now exists for the same defect on the connectivity
map, so this drops the copy and calls it.
Theirs is the better one: it also waits on a link that is present but not yet
loaded, which the local copy skipped straight past.
The prayer API localises its labels from its own lang parameter, so the region
and country NAMES follow whatever languageCode resolves to. That computed read
props and profile-config only — never the URL. So a DIRECT LINK, which is the URL
a person actually sends someone, went through detectLocale() for the chrome and
then asked the API in English: Arabic headings around English region names.
MEASURED, and only visible because the two channels were compared rather than
each checked alone: region-dashboards rendered 597 Arabic chrome characters
through profile-config against 309 through ?lang=ar, and regions-mfe 2709 against
748. The host-locale gate passes both, because it asks host >= ref/2 and cannot
see an asymmetry in that direction. Diffing the two renders element by element
named the whole gap exactly: the seven region labels in the chooser legend
(.rd-regionkey__label) and the seven row links in the table (.rd-row-link).
The reader is EXTRACTED, not copied. library/i18n/index.js already parsed
?lang= / ?locale= inside detectLocale; it is now exported as localeFromUrl() and
used in both places, so the data call and the chrome can never disagree about
what the URL asked for. Re-implementing those two lines in the plugin would have
been a second copy of one fact, which is how the asset-path bug and the
PosterWall fallback table both went wrong.
After: ?lang=ar renders 30 Arabic elements against profile-config's 30 on
region-dashboards, and 42 against 42 on regions-mfe — 0 strings left English on
the shared-link path. All 23 bundles rebuilt because the library file reaches
them; composite freshness PASS, check:sri PASS.
A full check:host-locale-contract run loads every leaf twice and hit the 900 s default three times
(exit 75, NOT MEASURED). An env prefix in its package.json script cannot help: the wrapper reads its
limit before the script's environment exists. So a gate declares it in its header
(@browser-slot-max-hold <s>); the wrapper reads it for 'node <file>' and 'bun run <script>' alike.
The caller's BROWSER_MAX_HOLD still wins, a declaration is capped at 3600 s, and every other
command keeps the 900 s default. Tested on a private slot dir: declared 3 s -> killed at 3.1 s;
caller override 1 s -> 1.1 s; bun run form -> 3.2 s; 99999 -> capped to 3600.
The research verify-* scripts and research-map-plus-grid's two gates hardcoded 'doxa-maps' (11
literals); they now import TREE_NAME from internal/_tree-name.mjs like the rest of the tooling.
The output tree's name has always been configurable (vite.doxa-maps-build-config.json "name"),
but 61 tooling files ignored it and hardcoded 'doxa-maps' (181 literals), so a rename would have
moved the build while the gates kept reading the old folder.
- internal/_tree-name.mjs reads it once (validated as one URL-safe path segment; falls back to the
historical name) and exports TREE_NAME / TREE_NAME_RE; `node internal/_tree-name.mjs` prints it
for shell scripts.
- A codemod rewrote 171 literals in internal/ to TREE_NAME; the regex literals, messages with
backticks, atomic-public-swap.sh (fails loudly if the name cannot be read), build-lock.sh and
vite.config.js (its duplicate readMfeName is gone) were done by hand.
- The name is unchanged: old and new tooling produce all 1408 files under public/ byte for byte.
Code lines still naming the tree in internal/ + vite.config.js: 2, both intended (the helper's
fallback, the private-repo-name pattern in public-hygiene-check).
This renames nothing visible; the eventual rename becomes a one-line config change.
Two surfaces join the leaf and the Gallery, each built from the manifest's own snippets so a
broken snippet fails as well:
B a host on a different origin iframes the leaf; the plugin must draw its data and the
host's title, URL and scroll must not move.
C a host on a different origin pastes embed.script with its own token and provider. Under
maplibre: 0 api.mapbox.com requests. Under mapbox, where the plugin draws a Mapbox map, a
second run with a fake host token must show that token, and no other, in tile requests.
MapLibre-only plugins take no token and report n/a.
The self-test now also poisons the bundle under the script embed. Rows print their seconds;
--surfaces A,B,C,D splits the ~26 min run. Measured: 7 bundles, 60 rows PASS, 3 n/a.
B does not assert 0 api.mapbox.com: every generated leaf head links Mapbox JS/CSS whatever
the provider, which is a template property reported separately.
Its output collided with this branch's during the integration; a build artifact
cannot be merged line by line, so it was rebuilt rather than resolved by hand.
Choosing "Native" in the research map's side menu drew nothing. So did MST, and
so did Network. A control that does nothing is worse than one that is absent:
it tells the reader the map cannot do what it can.
TWO CAUSES, AND FIXING EITHER ALONE WOULD HAVE LOOKED LIKE A FIX.
1. mapStore never declared `clusteringMode`. The picker wrote to a property no
reactive system was watching, so useMapClustering's `mode` kept its own
internal default and the click changed nothing.
2. The legend's Clusters checkbox force-set 'mst' "regardless of what's in the
store", with the mode watcher "kept as a no-op for forwards compat" — its
own words. Even with (1) fixed, one tick of that checkbox would have snapped
Native back to MST: a control that works until you touch the other one.
So the two controls are now two views of one state. clusteringMode IS the state
('off' means disabled); the picker sets it; the checkbox maps onto it, promoting
only 'off' to 'mst' so the checkbox alone behaves exactly as it always has. They
can no longer show different things. The dim-slider's clusterPinOpacity got a
real home in the same pass, for the same reason it was missing.
★ THE GATE ASSERTS ON WHAT IS DRAWN, NOT ON WHAT IS SET. `clusteringMode ===
'mapbox'` would have passed the moment (1) was fixed, while the map still drew
MST lines — state was exactly the evidence that let an inert control ship. The
map now reports, per clustering layer, whether it exists, whether it is visible
and how many features are rendered; the gate clicks the real buttons and reads
that. MEASURED, before and after, on the same tree:
before off 0/0/0 native 0/0/0 mst 0/0/0 network 0/0/0
after off 0/0/0 native 37/37/0 mst 0/0/1700 network 0/0/23896
(circles/counts/lines)
It also asserts no orphan layers between modes, and that MST and Network differ
— two labels for one behaviour is the same defect as a dead option.
One measurement bug caught on the way, worth recording: a fixed 6-second pause
reported MST as drawing nothing while Network drew thousands of lines. MST over
~2,100 pins is quadratic and had simply not finished. The gate now waits for the
drawing to settle rather than guessing a duration — a gate that calls a slow
feature broken is worse than no gate.
Injecting mapbox-gl.css collapsed this profile's canvas to a white strip, and
every assertion I owned went green over it: verify-gospel-map counted 2,107 of
2,107 pins "rendered", leaves-mount found a full shadow root, the i18n gates
found every label translated and nothing clipped. queryRenderedFeatures does not
care whether anything is visible. I caught it by opening the screenshot, which is
not a system.
So: screenshot the map region and count distinct colours — a basemap has
hundreds, a blank rectangle one or two — and check the map still fills its
element.
THE POISON ARM CORRECTED THIS GATE TWICE, which is the argument for having one.
First it hid the canvas, and the region still showed 316 colours: the sample was
taken over the whole custom element, and the legend card is itself full of
colour. The sample is now clipped to the right of the legend, so it looks at map
alone.
Then it collapsed the container, and the gate still passed — because I was
comparing the canvas to its own container, and in the real incident BOTH
collapsed together, leaving that ratio at 1.0. The check would have passed the
exact defect it was written for. The ratio that matters is map-to-ELEMENT, and
the poison now reproduces the incident rather than approximating it: 1440x300
inside a 1440x900 host, caught at 33%.
Measured after: 644 distinct colours, map filling 100% of the element, and the
poison arm red.
The one tree path compiled into any bundle. Built today it is byte-identical (sha 3d5467fd…);
built with the config renamed it reads /public/<new name>/manifest.json.
It reported four failures and could not be run in CI, so a real
regression would have landed behind an already-red gate. Two of the four
are not defects at all, and two are open in a way code cannot close.
PROPER NOUNS, now allowlisted with reasons. "Open Bible Stories" and
"Scripture Earth" are the names of the organisations that publish these
resources, not descriptions of them: a reader following the link meets
the English name on the destination site, so translating it would hide
the thing it identifies. They sit in the same res.* block as
`jesusFilm: 'JESUS Film'`, which the product already treats as a brand,
and resources-list.vue:234 maps the literal string 'Scripture Earth'
back to this key — it is an identity, not a label.
DRIVER-GATED, and allowlisted as a BOOKMARK rather than a verdict.
legend.columns.upgs and prayerLaps.upgs really are an English acronym
wedged into an otherwise localized row, in Arabic and Chinese included,
while `population` beside them translates normally. That stays true; it
is simply not resolvable here. Measured this tick: the column's own
tooltip (legend.sortByUpgs) is ALREADY translated in all 11 with only
the acronym left English — "Ordenar por UUPG", "按 UUPG 排序",
"ترتيب حسب UUPG" — and the widest candidate phrase is 28% of the 380px
panel (zh costs 9px). So neither engineering nor space is the blocker:
it is terminology only, and an invented acronym looks AUTHORITATIVE to a
reader who cannot check it.
★ "ALLOWED" IS NOT ONE THING, and collapsing the two would make this
gate lie. A proper noun is SETTLED — correct English forever. A
driver-gated key is an OPEN DEFECT parked so the gate can still catch
the next regression. Calling it "identical on purpose" would turn a
deferral into a verdict, which is exactly how an open item stops being
visible. The gate now separates them: the PASS line itself reads
"2 deferred pending a ruling" and names both keys above it.
MUTATION TESTED — making a NEW key identical in all 11 locales
(buttons.prayerList) still fails it, exit 1, naming the key. The
allowlist exempts without blinding.
The priced options are on
DECIDE-UUPGS-COLUMN-HEADER-IS-ENGLISH-IN-ALL-ELEVEN-LOCALES; these two
lines come out when the Driver rules.
A hard-coded opening zoom framed the world at one canvas size only. The map now fits the
people-group dots on first paint. A portrait phone cannot zoom out far enough to frame the
world in Mercator (floor 0.72 at 390x844), so when the fit is clamped the view slides to the
longitude window holding the most dots. Measured: desktop 1440x900 still 2107/2107 drawn;
phone 390x844 from 1599 to 1907 of 2107.