Commit graph

827 commits

Author SHA1 Message Date
Doxa Maps
7f3435cc1b Delete the selection-bus shim: its one cycle is over
Landing 1 left plugin-designs/research-dashboard/src/composables/useSelectionBus.js
re-exporting the moved file, so the move and its consumers did not have to land
together. Both consumers moved in that same landing — useDashboardShell's import
and check-facet-cascade's BUS_SRC — so the shim has had zero importers ever
since. Measured before deleting: a search across plugin-designs, library and
internal finds two references to useSelectionBus and both name the LIBRARY path.

A re-export with no importers is not harmless. It is a second entry point that
reads like a supported one, and the next author who finds it has no way to tell
that it is scaffolding rather than the plugin-local home of the bus.

★ HOLD THIS COMMIT UNTIL 2bb90852 IS ON MAIN. It is deliberately the tip of the
stack and touches nothing else, so it can be dropped or deferred on its own. If
landing 1 is not in main, the file this deletes is the only thing making the old
import path resolve.

Landing 2's own shim (useDashboardLayout) STAYS for one more cycle, on the same
rule that earned this one its deletion: it is one landing old, and it gets the
same measured check before it goes.
2026-09-26 18:38:19 +00:00
Doxa Maps
8fc7e2d62a Dashboard kit, landing 2: the layout engine, and the key it was sharing
useDashboardLayout is the second zero-domain piece: it imports nothing but `vue`
and reads no row. The four mentions of a people-groups grid in it are comments
naming an example of what a host stacks underneath. Body verbatim apart from the
one change the spec required before it could be shared, and the same one-cycle
re-export at the old path.

THE KEY WAS THE REASON THIS COULD NOT JUST BE MOVED. The engine persists the four
divider positions to localStorage under a single constant, so every dashboard
wrote to the same slot. That was invisible while there was exactly one dashboard
— and SHARING THE ENGINE IS PRECISELY WHAT MAKES A SECOND ONE POSSIBLE. Moving it
without fixing this would have shipped a collision into the kit's first reuse:
drag a divider in one dashboard, reload, and the other has moved. Nothing errors,
neither is wrong on its own, and the reader has no reason to suspect the other
element on the page. SPEC-DASHBOARD-KIT section 8 named it as this landing's risk;
the fix belongs in the commit that shares the engine, not in the one that first
trips over it. The key now carries the profile's own id, so a second dashboard
gets its own slot without its author knowing this key exists.

AND NAMESPACING MUST NOT SILENTLY DISCARD A LAYOUT. Every reader who has dragged
a divider has a value under the old shared key; a new key would simply orphan it
and reset them for no reason they could see. The engine reads the old key once
when the new one is absent. It never writes or deletes it: a migration that also
deletes cannot be rolled back, and leaving the old value costs one localStorage
entry.

check:layout-key-namespaced guards all three. It EVALUATES the storageKey
expression the shell actually passes, with two different dashboard ids, rather
than retyping the rule — a gate carrying its own copy of the thing it checks
passes while the product is broken, which is the lesson from
check:linked-vocabulary. Negative-tested: a bare literal fails A, an expression
that ignores the id fails B, and writing or removing the legacy key fails C.

★ THE BUS_SRC LESSON WAS APPLIED BEFORE MOVING, NOT AFTER. Landing 1 nearly
disarmed check:facet-cascade because that gate reads its subject from a path. I
checked first this time: no gate reads useDashboardLayout by path, and the
storage-key literal appears in exactly two places, both of them in this diff.

The library index is regenerated here, per landing, for the same reason as
landing 1 — 101 items now.

SOURCE ONLY. Stacked on D1. STUDIO: needs a rebuild of research-dashboard and
dashboard-plus-grid. package.json is the parent's copy plus two lines beside
check:leaf-engine-per-provider, so D1's registration is preserved.
2026-09-26 18:38:19 +00:00
Doxa Maps
5643981601 Cities with Internet: the basemap's place names follow the reader's language
Under ar, Cities with Internet showed an Arabic legend over English country and
city names, while Prayer Progress localized its basemap. The helper already
exists in the library: localizeBasemapLabels (library/maps/localize-labels.js),
which is Mapbox setLanguage, and on MapLibre points each OSM-name label layer
at name:<lang>, then name:latin, then name. useMapInstance applies it for every
map built through it; connectivity-map builds its map raw (new gl.Map), so it
never ran here. It now runs on 'load' with the same reader-language ladder the
engine UI labels use. No library change: the helper was already shared.

Headless, the real helper against engine-shaped maps: under MapLibre ar, the 2
basemap label layers are rewritten and the plugin's own label (['get','iso3'])
is untouched; en restores the originals exactly. Under Mapbox ar-EG,
setLanguage('ar') is called. Build OK; no-free-identifiers, universal-props and
profiles-mount pass.
2026-09-26 18:26:38 +00:00
Doxa Maps
009795a6c5 Cities with Internet: the engine-missing error uses the shared key that landed
626e7229 looked up messages.engineNotLoadedDetail, the name upg100's ade12e4c
used on its branch; the key that reached main (studio's landing 3, 52eeb0db) is
messages.engineMissing ('The {engine} map engine did not load.', 11 locales).
With the old name the error always fell back to English. It now reads the
shared key. Build OK; no-free-identifiers passes.
2026-09-26 18:26:38 +00:00
Doxa Maps
4d18a6f3b4 The legend stops sitting under the controls in Arabic
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Driver-found, rega-measured: under ar the five map controls mirror to the LEFT
while `left:8px` kept the legend physically left, so the legend sat underneath all
five of them. cities-internet shares the gm-legend class and passed for one reason —
it anchors inline. Every direction-dependent physical property in this stylesheet is
logical now: the legend anchor, the search clear button, the accent borders, and the
header's push-right.

Measured at 1440x900 against six controls: ar legend x=1052 with 0 overlaps, en
unchanged at x=8 with 0 overlaps.

Then the Latin rega saw. Once the explainer is folded away, exactly one sentence was
left in English, and it is the legend's only piece of NEWS — that 366 of the 369
groups with nothing have somewhere to start. It goes through tOr() with its numbers
as parameters, so it renders the English today and switches when the key lands. The
caveat trailing it ("a way in is not the gospel in their own language") moved into
the fold, where the rest of the basis lives: it qualifies the news rather than being
it. What is left in an Arabic legend outside the fold is now exactly the two strings
whose keys I have asked studio for, and nothing else.
2026-09-26 18:10:44 +00:00
Doxa Maps
64931139a1 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-26 17:55:20 +00:00
Doxa Maps
376ecaee25 Prayer as a move at the grain a team is sent to
A country is not where anyone is sent; a state is. Each province now carries the
same two prayer numbers the country sheet does and ends in the same move — the
largest group there with nothing and nobody praying, one tap from its prayer page,
ranked need-first so drilling from country to province never hands the reader a
second answer. Andhra Pradesh: 64 of its 76 groups with a prayer page have nobody
praying, 3.8M people.

Always N of M. doxa.life publishes a page for 1,723 of the roster, so nobody-praying
can be said of the groups that HAVE one and never of the province; both numbers
ship together so no surface can quote the first alone.

The join here is name+country, not language — you pray for a people in a place, and
that is doxa.life's own key. It is the one join in this build where a name match is
correct rather than forbidden, and the slug is always theirs: pray.doxa.life answers
200 for any path, so a constructed one would look valid and be empty.

The doctrine screen now scans the province table too. It is the SIXTH place a URL
lives in this bundle, and a table added after the scan was written is exactly how
the access table went unscreened for weeks — read from disk, so it is checked even
when the province layer was never opened.
2026-09-26 17:54:39 +00:00
Doxa Maps
a76eb43419 Arunachal Pradesh is no longer offered Telugu
This table carried its own worst case as a caveat: the ways in were chosen for the
COUNTRY, so Arunachal Pradesh — in the far north-east — was offered TELUGU, a South
Indian language, because the curated bridge table is keyed by country and a curated
bridge outranks a computed one. Manipur and Kerala got Telugu too.

The province answers for itself now. Candidates are the languages of groups
STANDING IN IT that already have Scripture AND film, ranked by the people here who
already hold them. Arunachal Pradesh gets Hindi and Assamese, Manipur gets Meitei,
Kerala gets Malayalam — each one a measured neighbour rather than a national
average. 41 of 46 provinces can answer locally; the other 5 keep the country answer
and the new Basis column says which is which, because "no language here has both
Scripture and film" is itself worth reading. The country-level answer stays in its
own column so the two can be compared rather than swapped silently.

It is a NEIGHBOUR, not a recommendation: whether a group actually speaks it is a
question for someone on the ground, and the table says so.
2026-09-26 17:46:32 +00:00
Doxa Maps
b493f5e1b1 Wire the two strategic strings, English until their keys arrive
The fold's summary is a CONTROL — a reader in Arabic could not tell what was behind
it — and "This is a sign language." is the sentence that stops eBible's silence
being read as an absence of Scripture for 150 sign languages and 84.2M people.
Both go through tOr(), so they render today's English until exp-shepherd's keys
land and switch to the catalogue with no further change here.
2026-09-26 17:30:48 +00:00
Doxa Maps
ca7144a57b The gate learns the aliases instead of being told them, and can read main
A fleet-wide census with this gate as it stood would have been worth nothing, and
worse than nothing once mailed to seven owners. Three things had to be true
first.

IT HAD TO SEE OTHER PEOPLE'S CALL SITES. The first version matched tStatic/$t/t
and found four missing keys while sailing past the ten it was written for. Adding
`T` by hand took it to 22 — and that fix was the same mistake one level up. A
census of the tree finds FOUR wrapper names, not one: `const T` x16, `const L`
x6, `const tr` x1 (library/layouts/VerticalTabsLayout.vue) and `const v` x1. A
gate that knows about T and not tr is blind to whichever bundle used the other
name and reports it CLEAN. So the names are now read out of each file, and the
next author's alias is covered the day they write it. Negative-tested by renaming
T to Tz in a file: all six of its call sites are still found.

IT HAD TO RESOLVE THE PREFIX HELPERS IT WAS DECLINING TO. All six `L`
declarations carry a static prefix — four are `tStatic('dashboard.' + key)`, two
are a ladder `tStatic('controls.' + key) || tStatic('dashboard.' + key)`. Those
are ordinary keys with a known prefix, not runtime expressions, and counting them
as unresolved left most of this gate's coverage on the floor: 49 call sites moved
from unchecked to checked, and the unresolved count fell from 80 to 28. A key
resolves if ANY prefix in the ladder holds it, because that is what the ladder
does at runtime.
★ AND THE FIRST RUN OF THAT RESOLUTION INVENTED A DEFECT. BpResearchHeader
renders `L('reset_all', '') || tStatic('controls.reset_all') || 'Reset'` — the
author saying "if this prefix has nothing, fall to the next rung", and
controls.reset_all does hold it. Reported as missing, that is a gate manufacturing
a problem out of a working ladder — the precise failure I was about to mail seven
owners about. An empty English fallback is the author's own signal that the miss
is intended, and is now honoured.

IT HAD TO MEASURE A TREE IT IS NOT STANDING IN. A census has to be taken against
current main; every bundle but mine is stale in my worktree, so a local scan would
report keys another owner fixed an hour ago. A second checkout was not available
either (the box is at 97% disk), so `--rev` reads the files as blobs out of the
object store: one ls-tree and one cat-file --batch, parsed by length rather than
by scanning for a delimiter that could occur inside a file. No worktree, no temp
copy, no writes.

AND IT HAD TO STOP ITS OWN SILENCE BEING READ AS A PASS. This gate asks whether
the keys a bundle NAMES exist. A bundle that names none has nothing to be missing
and passes without a word — and seven do, including two of mine whose composites
render no chrome of their own. Reporting only the misses would let "0 missing
keys" be read as "fully localized", which is the opposite of true for exactly
those bundles. Participation is now counted and printed beside the result, and
the two are never merged into one number.

THE CENSUS, at origin/main 1a761741: 16 missing keys, and all sixteen are in this
lane — research-dashboard 14, big-picture 1, doxa-research-map 1. Every other
owner is clean. All sixteen are already fixed by this branch's parent, which
PASSES under `--rev`.
2026-09-26 17:28:56 +00:00
Doxa Maps
67f5740475 A dialect is called "Arabic, Sudanese", and one click is one event
rega, row 43 case 3, both engines and both locales: clicking a dialect emitted
the right languageCode every time and narrowed the grid in only ONE of four runs,
while the name it reported read ", Afro-Asiatic__Arabic__Sudanese". Two defects
in one click, and both are mine.

THE NAME WAS A TREE NODE ID. selectedDialect.key is family__baseLang__dialect,
and the watcher fifteen lines above the offending template says so in as many
words — I read that comment and then used the key as a display name anyway.
selectedLanguage is also empty by the time a dialect is active, so the template
produced a leading comma in front of an internal identifier: a string no reader
could make sense of, in the one slot meant for a human name. The dialect's
originalLabels are the API labels the legend grouped ("Arabic, Sudanese"), which
are both the right words and already in the reader's locale. When a dialect
groups several labels all of them are reported, because that is what the
selection covers.

THE GRID LOTTERY WAS A BURST MEETING A GUARD. Clicking a dialect does not change
one store value; it clears the language, sets the family and sets the dialect,
across ticks. So the watcher fired three or four times and this element
dispatched three or four map-filtered events for ONE click. The composite ignores
anything arriving while it is pushing state into its children — a guard it needs,
because the grid re-emits during a push and the round trip would otherwise cancel
the selection just made — and that window is 1.2 seconds. Whether the LAST event,
the one carrying the settled state, arrived inside or outside the window was
timing. Three runs lost it; one did not, which is exactly the 1-in-4 rega
measured.

The fix belongs in this file rather than the other end: the guard is correct, and
the burst is what this element should not have been sending. One click, one
event, nothing to drop. A short settle window rather than nextTick because the
cascade crosses ticks (the legend bridge does async work); 60ms is far below the
1.2s guard and below what a reader notices.

I am NOT touching the composite's 1.2s window in this commit. It is now only
reachable by a genuinely rapid second click, and changing a guard that exists to
stop a self-cancelling round trip is not something to do in the same commit as
the bug it was masking.

check:language-code-link grows a section E for both, asserted against the source
because both are shapes. Three mutants, and the SECOND one found a hole in my own
check: searching the whole file for `emitFilterStateSettled()` is satisfied by the
helper's own declaration, so reverting the WATCHER left the gate green while the
burst came back. It now reads the watcher line specifically. A check its own
subject can satisfy is not a check.

Also corrects 22 to 23. rega counted 23 codes under the "Arabic" tier and was
right: 22 came from a cruder comma-split grouping of mine, while the tier's real
three-arm rule selects 23. The gate's computed numbers were always right; only my
prose was wrong. Two more comments still saying 22 live on sr/kit-d1-multivalue-filters,
which is already in studio's queue — I have not rewritten a branch someone is
landing; they need the same one-word change once it lands.
2026-09-26 17:28:46 +00:00
Doxa Maps
f79d8f48e2 D1: a filter may be several values, and one function decides what matches
SPEC-DASHBOARD-KIT D1. filterSet[key] may hold an array meaning "any of these";
keys still AND together. The array support is four lines. The reason this is a
landing rather than a patch is the other half.

`a(r) === filterSet[k]` WAS WRITTEN OUT SEVEN TIMES — the shell's filteredRows,
the facet rail twice, the map panel, the table, the charts rail and the CSV
exporter. Seven copies of a one-character rule are survivable while the rule
stays one character. The moment it grows an array case, six of the seven keep
comparing an array with ===, every one of them quietly matches NOTHING, and the
panels disagree with each other about what is on screen. That is not abstract:
the facet rail computes the COUNTS a reader acts on before clicking, so a rail
still using === would print counts for a scope the map and table no longer show.
So the predicate now lives in the bus and every panel calls it. The
de-duplication is what makes the four lines safe.

WHAT A CLICK DOES IS UNCHANGED, deliberately. A plain click still REPLACES. D1
gives the bus the capability; making a rail row additive on an unmodified click
would change what every existing click does to the flagship, which is a product
decision and not this one. The array path is reached programmatically today.

AND THAT PATH IS THE PAYOFF: "Arabic" now narrows the grid. The language tier
covers 22 ISO codes, so no single value expressed it, and the composite had to
send nothing and leave the axis unfiltered — honest, and visibly incomplete,
because the map narrowed and the grid did not. The whole code set now crosses and
the grid ORs it. Verified per tier on the committed fixture rather than on one
example: for all 805 tiers the joined set selects exactly the rows the map
selected.

★ THE SEPARATOR IS `|`, AND MY FIRST DRAFT GOT THAT WRONG IN A WAY WORTH
RECORDING. I joined on `,` and broke label matching outright: 120 of the 864 live
language labels are comma-inverted ("Arabic, Mesopotamian"), so a selection made
BY LABEL was chopped in half and matched nothing. A separator that can also occur
inside a value is not a separator — the rule the URL hash states one file over,
which I had just written and then violated. check:filter-multivalue caught it
before it left this machine, and it caught it because it asserts a behaviour I
was KEEPING (label matching) and not only the one I was adding. Asserting only
the new behaviour would have shipped it.

EMPTY IS NOT A FILTER. An empty array clears the key rather than matching
nothing — a UI deselecting its last chip produces exactly that array, and
treating it as "narrow to zero" is the silent-empty-screen failure the hierarchy
contract already exists to prevent. A one-element array collapses to the scalar
it means, so the hash, the chips and every adapter keep their existing shape
until somebody actually picks two.

THE SURFACES THAT CANNOT SHOW A LIST SAY SO HONESTLY. labelFor resolves each
value and joins them, at the one owner, so the chip bar, the research-filter
chips and the CSV provenance line cannot drift apart. A dropdown shows NO
selection when the key holds several: ticking the first of three would state
something false about the board, which is worse than stating nothing, and the
chip still names every value so the selection is never invisible. The URL hash
carries a list and drops the display slug for it — the slug is one human name for
one code, and there is no honest slug for twenty-two.

check:filter-multivalue runs the real bus, greps the panels for surviving copies
of the predicate, round-trips the hash including a value containing a comma, and
lifts the grid's valueMatches to prove both ends OR the same way. No browser.

Green: check:filter-multivalue, check:language-code-link, check:facet-cascade and
its self-test, check:kpi-unit, check:csv-shape, check:no-free-identifiers,
check:sfc-parse, check:mjs-parse, check:descriptions, check:no-orphan-gates,
check:doc-scripts, check:script-flags, check:quoted-scripts.

SOURCE ONLY. Stacked on kit landing 1, which moved the bus into library/.
STUDIO: needs a rebuild of research-dashboard, dashboard-plus-grid,
doxa-research-map, research-map-plus-grid and people-groups-grid. package.json is
main's copy plus two lines beside check:fits-screen.
2026-09-26 17:28:46 +00:00
Doxa Maps
5d82899d18 Three maps asked the window which engine drew them; now they ask the map
C3, buckets A and B. Each site was sniffing `window.maplibregl.Map` directly,
which breaks the moment a leaf stops loading a static engine peer: the global is
simply absent and the sniff answers about whatever else the page happened to load.

A needed a helper that did not exist. Both A sites want a provider NAME from a
live map. engineFor() returns the engine NAMESPACE, and resolveMapProvider()
reads a CONFIG rather than a map, so on a dual-engine page it answers about the
wrong thing. Hence providerFor(map) -> 'mapbox' | 'maplibre' | undefined, with two
named consumers on arrival.

★ WHY providerFor RETURNS undefined INSTEAD OF A DEFAULT. The obvious rewrite was
in terms of the existing isMapboxMap(). It is wrong in a way no per-engine test
catches: both originals test for MAPLIBRE and fall through to mapbox/false, so an
isMapboxMap() version inverts the fall-through to maplibre/true. On a stub, a
destroyed map, or a page where neither global loaded, that changes what a
diagnostic field reports AND which attribution a poster prints -- while agreeing
on both real engines. Returning undefined forces each call site to keep its own
default visible:

    pplrData.js      engine: providerFor(map) || 'mapbox'
    research-map.vue const onMaplibre = providerFor(map.value) === 'maplibre'

Changing a default is not a curation migration.

B is engineGlobal(mapProvider), which already defaults to Mapbox for anything
unrecognised -- exactly what the ternary's else branch did.

All three files now carry zero raw window.mapboxgl|maplibregl references in code.

Verified by _qa-cycle/02-builder-cycle/_chk-c3-provider-helper-equivalence.mjs:
27 assertions, each old pattern against its replacement on both engines and on
{}, null and undefined, plus an executable proof of the isMapboxMap near-miss and
three negative controls.
2026-09-26 17:28:46 +00:00
Doxa Maps
50a29b463d Dashboard kit, landing 1: the selection bus becomes library code
The first piece of Big Picture to be shared, and it went first for one reason:
useSelectionBus has ZERO domain references and imports nothing but `vue`, so it
is the only piece that could move with no adapter and no behaviour change. The
body is byte-for-byte what main shipped — verified by diffing everything from the
first import onward — and only the file's header block is new. A move that also
edits is a move nobody can verify, and nine of these have to be verifiable in a
row.

THE OLD PATH STILL WORKS, as a re-export, for one cycle. That is not politeness;
it is what keeps this landing small enough to check. A landing that moved the
file AND rewrote every import would be two changes whose failures look identical
from the outside. The shim re-exports and does not duplicate: a second copy would
drift silently while both halves kept passing their gates, which is the failure
check:no-cross-plugin-imports and check:shared-reached exist to stop. It is
deleted in the next landing.

★ THE GATE HAD TO MOVE WITH IT, and this is the part a move like this hides.
check-facet-cascade reads the bus from a PATH (BUS_SRC) and, for its self-test,
copies that file and poisons the copy to prove it can still catch a reverted
cascade. Pointed at the shim, it would have copied and poisoned a RE-EXPORT: the
mutation would match nothing, the "broken" copy would behave correctly, and the
self-test would report that the gate no longer catches anything. One line, and
without it landing 1 would have silently disarmed the only check that proves the
hierarchy contract. Verified after the move: the gate PASSES at the new path and
the self-test still catches a reverted cascade (13 assertions).

The library index is regenerated in the same commit, per SPEC-LIBRARY-CURATION
section 4: check:library-index compares REGENERATED BYTES, so a new file under
composables/ turns it red until `bun run library-index` is re-run. Per-landing
regeneration rather than one pass at the end, because nine landings with a
knowingly-red staleness gate would train nine readings of "ignore that one".
useSelectionBus now appears in the generated menu — which was the whole argument
for the existing taxonomy over a new top-level area.

NOTHING TO REGISTER IN package.json: the gate that guards this file,
check:facet-cascade, was already wired, and check:no-orphan-gates confirms it.
This commit does not touch package.json at all, so it cannot conflict with the
three landings of mine that do.

STILL COMING, deliberately not here: SPEC-DASHBOARD-KIT D1 makes filterSet
multi-value. That changes this file's CONTENTS; this changes only its LOCATION.
Keeping them apart is what makes "Big Picture is unchanged" provable.

SOURCE ONLY — not built, by instruction. Green here on main's tree:
check:facet-cascade and its self-test, check:library-index, check:no-orphan-gates,
check:no-cross-plugin-imports, check:shared-reached, check:no-free-identifiers,
check:mjs-parse, check:sfc-parse, check:descriptions.
2026-09-26 17:28:46 +00:00
Doxa Maps
44a1afba04 Decouple prose: no go-base / nuxtbase / nuxtinator / doxa-base names in plugin comments and docs
Comments and prose only, no code change (DECOUPLE-go-base-mentions.md sections B and C, outside
library/ and README.md): stack-demo's page-flow note becomes 'a normal host page', the share-link
and CORS comments say 'the live test host', embeddability.md's Traefik note says 'a self-hosted
reverse proxy' and drops the internal proxy file path, and how-to's hosting option becomes 'a Git
host you run (for example Forgejo)'. stack-demo.vue's @description feeds the generated tree, so the
landing regenerates the tree. Section A (QA widget), library/ and package.json are left to studio.
2026-09-26 17:28:45 +00:00
Doxa Maps
5739818ecc Never hand a keyless page the engine that needs a token
resources-map resolved its engine as `engineGlobal(provider) || window.mapboxgl`.
engineGlobal already defaults to mapbox, so that fallback could only ever fire for
provider 'maplibre' — and then it returned MAPBOX. A page that chose the keyless
engine silently got the one that needs a token, and because the profile only
assigns accessToken when the provider is not maplibre, the substitute had no token
either: it would have 401'd on every tile. The fix is the deletion. The requested
engine or a named failure, with a console warning for whoever is debugging and the
reader's line through messages.* (English kept inline until the key lands).

check:engine-contract measures it: two providers across engine present, engine
blocked and both blocked, with the CDN blocked by request routing the way a proxy
or an offline kiosk does it. Three of those six arms first failed for reasons that
were mine, not the product's, and each is written down in the gate: Mapbox cannot
mount at all on a static tree without a token, 11s was not long enough for a leaf
that fetches 4 MB before the map exists, and engine attribution read from a debug
handle scored Mapbox-with-a-bad-token as "engine null" — the canvas class cannot
lie about who drew it.

Also in this landing: the pin caveat is translated in 11 locales from the shared
pins.* area, read through this leaf's own guarded t() rather than by mounting
library ApproxPinNote.vue — that component calls useI18n() unguarded, and this
bundle is embeddable in hosts that never called app.use(i18n), where an unguarded
call throws during setup and the shadow boundary swallows it. Same wording either
way. Both "Show all groups" buttons now use the key that already exists in 11
locales, and the engine's own control labels get the provider arm.
2026-09-26 17:21:22 +00:00
Doxa Maps
7f9b0d8983 Twenty-two keys the code asks for that no catalogue has ever had
A localized CALL SITE is not a localized STRING, and nothing in this tree was
watching the join between the two.

The dashboard's five resize handles read their names through
T('aria.resizeFiltersRail', 'Resize filters rail'). Reviewed, that looks
finished: the call goes to the catalogue and the English is only a fallback.
aria.json held eleven keys and not one of them was a resize key. Every handle
had been announcing itself in English in all eleven locales, and the fallback —
which is right to exist, because a missing key must degrade to readable English
rather than to "aria.resizeFiltersRail" on screen — is exactly what kept it
silent.

No existing gate can see this. check:i18n-parity is satisfied because the key is
missing from ALL locales equally. check:english-leaks and
check:composite-locale-leaks both need the catalogue to HOLD the string before
its appearance counts as a leak, and it does not. check:ui-strings-have-keys
reads templates for typed English. Each is looking at a different end of one
wire. check:i18n-keys-exist asks the question none of them asks: does the key
this code names exist?

It found TWENTY-TWO on its first run, all in this lane — the detail drawer's
five rows, the table's two selection toggles and their hints, the map's loading
line, the breadcrumb, the charts rail and the map legend's aria labels, the
empty-search message, and big-picture's footer. Six of them are the resize keys
on this branch's parent; seventeen are added here, English wording kept exactly
as each call site already reads it, in all eleven locales.

★ THE FIRST DRAFT OF THE GATE WOULD HAVE MISSED THE DEFECT IT WAS WRITTEN FOR.
It matched tStatic/$t/t and reported four missing keys while sailing past the ten
resize keys, because the house pattern is a file-local alias —
const T = (key, en) => tStatic(key) || en — and almost every call site in the
dashboard reads T(...). Matching T( took it from 4 to 22. Prefix helpers like
BpResearchHeader's L(), and any concatenated or template-literal key, are COUNTED
as unresolved and never guessed at: inventing the key set a runtime expression
might produce is how a gate starts reporting keys nobody wrote.

AND BIG-PICTURE, the last HOST-LOCALE failure fleet-wide, gets the two halves it
was still missing. Its title was a hardcoded English literal with no key at all —
the largest text on the element, sitting above an Arabic grid. And it never
reflected `dir`, alone among its siblings, so a host asking for Arabic got Arabic
WORDS in a left-to-right layout: a bug that reads as a styling quirk, which is
why it outlived every locale fix around it. The attribute goes on the
custom-element HOST, because `dir` takes effect through the inherited CSS
`direction` and inheritance crosses shadow boundaries — one attribute covers this
element's chrome and both nested elements beneath it. Only ever set, never
cleared: a host page that declares its own dir owns that decision.

The gate is green with this branch and its parent together, and red without
either. Negative-tested: deleting a key that exists fails naming the key and the
call site; a first argument whose area is not a catalogue file is correctly not
reported.

STUDIO: no rebuild needed for the catalogue, but big-picture must be rebuilt.
package.json adds two lines beside check:i18n-never-translated — a different
region from sr/dashboard-filter-a11y and sr/language-code-link, so the three
merge.
2026-09-26 17:10:06 +00:00
Doxa Maps
dc226c2e01 Register the embedder-key gate so it can actually run
check:embedder-keys asserts regions-mfe forwards the documented embedder keys to its panes, and that
profile and instanceId are NOT forwarded (the host's would overwrite each pane's own and collapse both tabs
into one view). Negative-tested both ways: removing dataSource fails naming it; adding profile fails naming
the collapsed-tabs risk.

The gate lives in the plugin's scripts/ folder, as nine check: entries already do. Its comment stripper is
committed beside it (strip-code.mjs), because a committed gate cannot import from the gitignored QA folder.
Registered in scripts (alphabetical, with its //scripts note) and in gates:post; source-only, ~0.2s.
2026-09-26 17:10:06 +00:00
Doxa Maps
c193621bc2 My UPG Neighbors: the cockpit, list, flower and send panels speak the reader's language
The universal-props re-measure (PRODUCTION-MATRIX.md, main dee9f9c9) found
typed-in English still left in upg-neighbor after its first i18n pass. This
translates the rest of the INTERFACE text, under the approved scope (interface
text, not research jargon):
- ListCell: the sender count, "Build send list", the pinned card's People
  group / Country / Language / Population / Religion, and the empty and
  placeholder states (no senders, N hidden, no reached neighbours yet, pick a
  red target, cluster/sender counts, tap a bubble);
- bloom-cockpit: the view switch's aria-label, the "unreached target" legend
  key, the loading overlay;
- FlowerCell: the empty-state instruction;
- send-map: the target's UPG / sender counts and its no-senders message;
- FlowerClusters: the API-down status reuses neighbor.lc.apiDown.

20 new keys (neighbor.list.*, neighbor.cockpit.*, neighbor.flower.*,
neighbor.sm.*) in all 11 locales. Every existing key is unchanged (checked per
locale). Method vocabulary (Bridge, Adjacency, Code, the 4-D SEND panel) and
the scripture / method footers stay English by the scope ruling. Numbers keep
their current toLocaleString (numerals held). No code branches on any of
these strings (swept).

Headless: build OK; no-free-identifiers, i18n-parity, i18n-never-translated
and profiles-mount pass. Library change (catalogues): consumers need
rebuilding.
2026-09-26 17:10:06 +00:00
Doxa Maps
5cdb5cdc3c A partner's dataSource never reached either Regions pane
regions-mfe forwards page-scoped config to its nested panes by an explicit key
list, because a cap-4 composite mounts its children in THIS document and a pane
has no leaf page of its own to hand it anything. Two keys were missing.

dataSource is the one that was actually broken. PRODUCTION-MATRIX's own table for
embedders lists it -- "wrong/partial data ... Regions uses pray-tools" -- and
nothing pinned it in spec.config or either pane.config, so a partner setting it on
<regions-mfe> had it silently dropped and both panes fell back to sources.json's
activeSource. Unlike the token there is no page-wide publish to rescue it.

tokenUrl is latent rather than broken, and it is worth being exact about which.
exp-shepherd's universal-props re-measure named it absent here, and it is; but
this composite sits under a ProfileLoader that already watches tokenUrl, fetches
it, and publishes the answer page-wide, so the panes do hold a token. What they
lacked was the URL, so a pane could never re-ask the token service itself and was
stuck with whatever the page had published.

Of the four keys that table documents, assetsBase was already forwarded, tk needs
no forwarding (ProfileLoader publishes a token it is handed page-wide), and
profile must NOT be inherited -- the host's would overwrite each pane's own and
collapse both tabs into one view. The new gate asserts that exclusion too, so a
future well-meaning addition fails instead of quietly merging the tabs.
2026-09-26 17:10:06 +00:00
Doxa Maps
40a6d48e0f Cities with Internet: a page that asks for MapLibre never silently gets Mapbox
connectivity-map took its engine as engineGlobal(provider) || window.mapboxgl.
engineGlobal already answers for Mapbox, so the fallback fired only when
MapLibre was asked for and failed to load, and it handed back the token-gated
engine on a keyless page (regions' C3 read; upg100's ade12e4c is the same fix).

The fallback is deleted. A missing engine now warns in the console and shows the
map's existing error state, naming the engine, through
messages.engineNotLoadedDetail when that key lands with ade12e4c, and in English
until then. upg100's second defect (the token gated on the engine) does not
apply here: accessToken is passed in the constructor options on both engines.

Headless, the engine pick over both providers x loaded / failed with the other
engine present / failed with nothing: the only change is 'MapLibre failed,
Mapbox on the page', which went from MAPBOX to the error; the other five are
unchanged. Build OK; no-free-identifiers, universal-props and profiles-mount
pass.
2026-09-26 17:01:36 +00:00
Doxa Maps
28c1ed2e7b Country posters: the map's loading and error lines speak the reader's language
countries-map typed in 'Research map loading…' and 'The map could not be
loaded.' (universal-props re-measure, main dee9f9c9). Both now reuse existing
keys through the component's own t(): messages.loadingNamed and
messages.couldNotLoadNamed, with the name tabs.map. That is 11/11 locales with
no new keys, so no library change. Headless: build OK, no-free-identifiers and
profiles-mount pass.
2026-09-26 16:55:14 +00:00
Doxa Maps
56c2da26c5 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-26 16:46:48 +00:00
Doxa Maps
c51233182d Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-26 16:43:37 +00:00
Doxa Maps
2195b2aceb Country posters: a partner's dataSource and tokenUrl reach the panes
country-posters-mfe forwards the host's page keys to its two panes by an
explicit list, and dataSource and tokenUrl were not on it. The same gap regions
fixed in f0aa5ab3, found by dx-shepherd on this composite.
- dataSource was LIVE: countries-map reads it from its own profile-config
  (countries-map.vue:392, :622), nothing pinned it, and nothing publishes it
  page-wide, so <country-posters-mfe profile-config='{"dataSource":…}'> fell
  back to sources.json's activeSource.
- tokenUrl was LATENT: the outer ProfileLoader already publishes the fetched
  token page-wide, but the panes never got the URL itself.

Both join INHERITED_PAGE_KEYS. profile and instanceId are still never inherited
(pane.config is spread last).

Headless: replaying paneElement's forwarding on a partner config, main passes
{provider, lang} and drops dataSource and tokenUrl; after, the map pane gets
dataSource and tokenUrl too, and profile stays countries-map. Build OK;
no-free-identifiers, universal-props and composites-mount pass.
2026-09-26 16:42:10 +00:00
Doxa Maps
6273ae18e6 P8: a partner's own list is an address
This map ships Doxa's roster, and the point of a public corpus is that other lists
can stand on it. A partner could already embed the map; their reader then had to
go hunting for their own people among 2,107 dots.

  ?codes=kfr,cmn,npi   draw only those languages, framed on them
  ?country=IND         open that country's sheet — this did NOTHING before unless
                       a ?code came with it, so the brief's second requirement had
                       no address, which is exactly what a partner embeds
  ?view=zero           unchanged

The join is ISO 639-3, so a partner's list needs no agreement with ours — only the
codes their own data already carries. It filters and never invents: the notice
reads "74 / 2,107" and is itself the button back to the whole map, labelled with a
key that already exists in 11 locales. A link matching nothing shows an empty map
reading "0 / 2,107", because showing the full map instead would tell a partner
their broken list works.

check:embed-contract gates the promise. Written against the pin index first, it
demanded "12 / 12,379" of a map correctly saying "0 / 2,107" — the gate was wrong
and the map was right, so its numbers and its probe codes now come from the file
the leaf itself reads.
2026-09-26 13:36:15 +00:00
Doxa Maps
c683d49716 The dashboard now says out loud what a filter just did
Cross-filtering is the one interaction whose whole point is that acting on ONE
control changes a number somewhere ELSE. Pick "Islam" in the left rail and 2,107
becomes 693 across the KPI strip, the map, the table and the charts. A sighted
user watches that happen. A screen-reader user heard nothing at all: this
element carried exactly one role="status" (the table's selection pill) and zero
aria-live regions. The filter worked; the feedback did not exist.

ONE polite live region in the shell, not one per panel. Six surfaces change on
every brush, and polite announcements QUEUE — six regions would make the user
wait through five repeats before reaching the next control. It is always
rendered and starts empty, because a live region inserted already holding its
text is routinely not announced; it is visually hidden rather than display:none,
which would take it out of the accessibility tree along with the pixels. It is
debounced at 700ms for the same reason the selection camera is debounced: typing
re-derives the row set per keystroke, and an un-debounced region would queue one
announcement per character. It stays silent on arrival and only ever reports a
change. It names the count, the total, the active facets and the search term,
formatted by the shell's own formatter so it can never read as a different
number from the KPI strip beside it.

ONE RESIZE HANDLE ANNOUNCED ITSELF IN ENGLISH in all 11 locales. Four of the five
read their title and aria-label through T(); the map/table one held raw string
literals. It survived review because each half looks correct on its own — the
ternary is right, the strings are right, only the missing T( is wrong. Both now
resolve through the catalogue. (The keys themselves are on
sr/dashboard-a11y-strings: aria.json held eleven keys and not one was a resize
key, so ALL five handles have been falling back to English. The T() call sites
were right the whole time.)

THE CHIP ✕ IS NOW BIG ENOUGH TO HIT. The live strip's remove button was a
~13x16px glyph — the smallest control in the product, and the only way out of a
narrowed board, where a miss costs the reader the whole view. The row height
cannot grow: three comment blocks above it record a measured layout-shift bug and
the Driver's "a lot more subtle" instruction that pinned it to 16px. So the HIT
AREA grows and the BOX does not, via an absolutely-positioned 24x24 ::after that
contributes nothing to the flex row's intrinsic size. Nothing moves by a pixel.
2.5.8 is a TARGET SIZE criterion, not a touch one, so it is unconditional. A
focus-visible ring too, since the chips live in an overflow-x strip where being
scrolled into view is not the same as being visible.

The kit's own DashboardFilterChips were spans with click delegation — not
focusable, no click from Enter or Space. They are buttons now, with an aria-label
naming the ACTION rather than the filter ("Religion: Islam" is a good name and a
useless action), type="button" so a partner's form is not submitted by a chip, a
focus ring and a 24px floor. The reset-all chip deliberately gets NO aria-label:
its visible words already are the action, and overriding them breaks voice
control.

Recording what this does NOT fix, because the measurement that prompted it named
the wrong file. DashboardFilterChips is exported from the kit and mounted by
NOBODY — round 1 of BUG-DUPLICATE-FILTER-BADGES removed it, and the strip on
screen belongs to SearchFilterHeader, whose remove button was already a real
<button type="button"> with a localized aria-label. So the keyboard half was a
latent defect closed before the next dashboard mounts it, not a live one. The
live defect in that same control was its size, above.

check:dashboard-filter-a11y guards all of it, on the kit component AND the strip
that actually ships. It walks the parsed template AST rather than grepping,
because grep cannot tell a bound :aria-label from a literal one, cannot tell a
button from a span named like one, and cannot see a live region hiding inside a
v-show'd cell. It also reads the built bundle, since source-correct plus
bundle-stale is a real repeat failure here. Fourteen mutants were run against it;
every one fails, naming the file and the reason. No browser — that is rega's.

STUDIO: needs a rebuild of research-dashboard and dashboard-plus-grid (the
composite bakes the shell in). Part D of the gate fails until you do, by design.
2026-09-26 13:35:57 +00:00
Doxa Maps
f3fcce9386 "Arabic" emptied the grid, because no people group speaks "Arabic"
rega, on MapLibre, in BOTH locales: Languages > Language > "Arabic" sent
map-filtered with {language:"Arabic", languageCode:""}, the composite handed the
grid an empty code, and 2,107 rows became 0. An empty grid reads as "no
results" — the worst shape a wiring bug can take, because the reader believes
the data.

ONE LINE, AND IT WAS MINE. The emitter looked for a row whose language LABEL
equalled the selected name. A language tier's label is not a label any row
carries: the legend builds family > language > dialect and derives the language
tier by splitting the API label at the comma. Measured on the live source —
2,107 rows, every one carrying primary_language as {value,label} — there is NO
row whose label is "Arabic". There is "Arabic, Sudanese", "Arabic, Levantine",
"Arabic, Gulf", twenty-two codes in all. The exact match could never hit, in any
locale, which is why it failed identically in en and ar and why the locale fix
that prompted this code was right about the disease and wrong about this line.

This is the derived-value trap one tier down from where I last wrote it up: a
value that crosses a component boundary has to exist on a row. "Arabic" is a
grouping the legend invented; primary_language.label is a field.

So the rows are now matched the way the legend's own node matches pins — base,
"base," prefix, " base" suffix — rather than by a fourth hand-typed spelling of
a rule that already exists in two places. 61 tiers resolved an empty code
before; "Arabic" was one of them.

AND ONE CODE ONLY WHEN THERE IS ONE. "Arabic" means 22 ISO codes, so no single
value expresses it. Emitting one would filter the grid to a fraction of the
selection while looking correct; emitting the tier NAME is what showed zero. The
honest answer is to emit nothing for that axis and leave the grid unfiltered —
the map is narrowed, the grid is not, and the reader can see both. languageCodes
carries the whole set alongside, so this becomes a real narrowing the day
filterSet goes multi-value (SPEC-DASHBOARD-KIT D1) rather than a rewrite.
Measured on the fixture: 764 tiers now resolve one code and filter the grid; 41
cover several and abstain.

A DIALECT IS A LANGUAGE SELECTION TOO, and it emitted nothing at all — the
watcher never observed selectedDialect, so drilling from "Arabic" to "Arabic,
Sudanese" changed the map and left the grid reporting the tier above. Its
originalLabels are exact API labels, so it is the one unambiguously resolvable
case.

check:language-code-link, headless, no network at run time. It LIFTS the
emitter's match rule and the composite's gridLanguage() out of the shipping
source rather than carrying its own copy — a gate with its own copy of the rule
passes while the product is broken, which is exactly how this survived a green
check:linked-vocabulary. It proves the emitter's predicate and the legend's
filter expression agree on every (label, tier) pair, that every tier selects at
least one row, and that an ambiguous tier narrows nothing. The fixture is the
864 distinct label/value pairs from the live source, deduplicated rather than
sampled so the counts above are about the whole set. Four mutants; reverting the
emitter to the exact-label rule reproduces rega's finding exactly — 61 tiers
selecting zero rows, "Arabic" named first.

STUDIO: needs a rebuild of doxa-research-map and research-map-plus-grid.
2026-09-26 13:35:57 +00:00
Doxa Maps
f42daf2802 The geocoder check failed a contract it should have been reading
rega: "0 search bars on both engines". The product is right and the check was
wrong, twice, and both mistakes are mine.

UNDER MAPLIBRE, NO SEARCH BAR IS THE CONTRACT. research-map.vue says
`hasGeocoder = mapProvider === 'mapbox'`, and research-map.instances.json says it
in words: "Under maplibre the search bar is deliberately ABSENT —
GeocoderComponent's maplibre path still geocodes through Mapbox, so shipping it
would bill the very key this instance exists to avoid." The previous revision had
just been taught that a bar-less leaf is a hollow pass, and applied that lesson
to the one engine where absence is the point. An absent bar is in fact the
STRONGEST form of this gate's own assertion: a search that cannot be typed into
cannot send anyone's IP to Mapbox.
The rule is conditional now rather than hardcoded, because a keyless geocoder
may ship one day and this gate should not need rewriting when it does: no bar
under maplibre is recorded and passes; a bar under maplibre must work and still
make zero Mapbox requests. Either way the zero is what is asserted.

THE MAPBOX RUN WAS NEVER GIVEN A TOKEN, so it could not have rendered a bar
either. The leaf bakes BAKED_TOKEN = "" and nothing else supplies one, so
mapbox-gl cannot construct a Map, `map` stays null, and the bar's
`v-if="appReady && map && hasGeocoder"` never fires. Note the mechanism, because
it is not the obvious one: an explicit ?provider=mapbox does NOT fall back to
maplibre — explicitMapProvider reads the URL first — so the engine is Mapbox, it
simply has no key. A gate that fails a product for a key the gate did not
provide is measuring its own environment.
It takes MAPBOX_TOKEN from the env, or `token` from the build config — the same
ladder check:experimental-shelf uses — and injects it as window.MAP_TOKEN the way
check:auto-engine and check:a11y-button-names do. Only for the mapbox run: a
token present during the maplibre run is the very variable this gate holds at
zero. With no token the mapbox half is NOT MEASURED, reported through the SKIP
line run-parallel reads, so the wave prints it as unmeasured rather than counting
it as a pass. The maplibre half is measured either way, keyless being the whole
point of that engine.

Also separates "the bar was removed" from "the map never constructed" by probing
for a canvas, since those are different bugs with one symptom — and the second is
how this check misread itself.

The token is never printed, only its presence.
2026-09-26 13:35:57 +00:00
Doxa Maps
c377e19925 Drop my geocoder-check edit in favour of simpleresearch's contract-reading rewrite (7d3a2199) 2026-09-26 13:35:57 +00:00
Doxa Maps
1c63481832 Six entries hand-rolled the RTL text plugin; the library already installs it
Every one of these bundle entries carried its own copy of

    if (window.mapboxgl?.setRTLTextPlugin) { setRTLTextPlugin(url, null, true) }

which is a strict SUBSET of what library/maps/rtl-text.js already does, so the
copies could not be covering anything the library misses:

  - useMapInstance calls installRtlTextPlugin(gl) at line 450, before it builds a
    map at 537/567 -- registration still precedes construction, the one ordering
    Mapbox cares about.
  - the call is identical in effect: same v0.3.0 URL, same null callback, same
    lazy flag.
  - the block is mapboxgl-only. The library installs on MapLibre too, so every
    keyless embed was covered by the library and never by these blocks.
  - the single raw `new gl.Map` site (upg-100-list.vue:1894) installs for itself
    at 1835 and never depended on its entry.

I first justified this as deleting DEAD code, on the theory that the engine is
injected lazily so window.mapboxgl would not exist when an entry runs. That is
wrong and the measurement says so: every leaf index.html loads mapbox-gl.js as a
blocking <head> script while ./index.js is a deferred module, so the guard did
pass and the blocks were live. The reason to delete them is redundancy, which is
the weaker and truer claim.

A three-line pointer replaces each block so the next reader does not re-add a
Mapbox-only copy.
2026-09-26 13:35:57 +00:00
Doxa Maps
6c804159c3 Three browser gates stop passing hollow or racing (rega's gates:post run at ded4b3c1)
- check:dashboard-geocoder-provider: 0 search bars on BOTH engines. Its default leaf (research-map) hides
  the bar under MapLibre on purpose, and the tokenless Mapbox run could not draw. It now defaults to
  doxa-simple-map (a bar on both engines), and the Mapbox run gets a host token: TEST_MAPBOX_TOKEN, or a
  well-formed fake whose token-carrying requests are aborted, never sent.
- check:composite-embed-knobs: every tk case SKIPPED (no baked token since the auto engine), so every
  provider case went UNMEASURED and the gate still PASSed. It takes TEST_MAPBOX_TOKEN, and a run where no
  case measured anything exits 75 NOT MEASURED instead of PASS.
- check:linked-vocabulary: it attached its groups-filtered listener after goto and could miss the grid's
  first emit ("the grid never reported a total"). The totals are now recorded from page start
  (addInitScript).
Not browser-run here (browsers are rega's): rega's next gates:post pass is the proof.
2026-09-26 13:35:57 +00:00
Doxa Maps
6bdfa59437 PPLR legend: its loading, empty and overflow lines speak the reader's language
PPLR's SemanticTreeLegend (a diverged copy of the library's; retiring it is card
RETIRE-PPLR-SEMANTICTREELEGEND) still typed in five strings: "Loading {label}…",
"Loading data…", "No data at this level", "Showing top {n} of {m}" and "use the
geocoder above to find the rest". The keys exist in all 11 locales since
d4359163 (legend.loadingRows / loadingData / noDataAtThisLevel / showingTop /
useGeocoderHint), which the library copy uses.

They now go through the component's existing guarded translator (ta), with the
English added to its fallback table, so a host without i18n still reads
English, never a key. Numbers keep their current toLocaleString (numerals held).

Headless: build OK, and check:no-free-identifiers, i18n-parity and
profiles-mount pass. (check:ui-strings-have-keys, which found this, is not on
main yet.)
2026-09-26 13:24:08 +00:00
Doxa Maps
85a4172644 Merge APPROXPIN and rebuild against the library it moved
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
studio's APPROXPIN (dee9f9c9) moved library/ — legends, PinHint, useMapInstance,
i18n — so the rebuild was certain rather than conditional this time. Checked, as
always, with `git diff --name-only <base> <main> | grep '^library/'` rather than
assumed: that test was EMPTY on one earlier merge and NON-EMPTY on the next two,
and it is the only thing that decides whether the bundle must follow.

All conflicts were DERIVED artifacts again — my two leaf HTMLs and the manifests,
zero source conflicts. Resolved to main's fresh build, then regenerated from the
merged source, because picking a side of a generated file is meaningless.

VERIFIED BY HASHING, the step whose absence shipped a blank page: fix:sri rewrote
the two manifests, and I then recomputed sha384 over the actual bundle —
upg-100-list, upg-100-mfe and poster-wall all MATCH, whole-tree sweep 57 integrity
attributes with 0 mismatches. fix:sri, not tree: studio had already regenerated the
pages and the gate itself warns that tree re-reads .vue source on a shared branch.
2026-09-26 13:20:45 +00:00
Doxa Maps
420c36a405 ui-strings baseline: gospel's resources-map types the pin note's first sentence, which now has a key
The shared approximate-pin note (pins.approximate) turned gospel-resources' own hardcoded copy of the
same sentence into 'has a key but typed in'. Recorded as known debt for gospel-plugin to switch to
ApproxPinNote; no string was added.
2026-09-26 13:04:16 +00:00
Doxa Maps
fef8c7b0fd The last of the shared-library English: legend, poster preview and tab loading states
Third and final batch of the shared-library strings, in the shepherd's order:
SemanticTreeLegend (4), PosterPreview (2), PosterWall (2) and
VerticalTabsLayout (2), plus PosterWall's "people groups" which already had a
key and was still typed in.

These are all LOADING and OVERFLOW states — "Loading data…", "Showing top 500 of
2,107", "use the geocoder above to find the rest", "Loading map snapshot…",
"preview unavailable", "Could not load X". Text a reader meets exactly when
something is slow or has gone wrong, which is the worst moment to be handed a
language they do not read. PosterWall's own source called one of them "i18n
DEBT" in a comment; it is paid now.

The counted ones take a placeholder rather than being split around an
interpolation — legend.showingTop is "Showing top {n} of {m}" — so a translator
can move the numbers within the sentence. Languages do not agree about where a
count goes, and a sentence assembled from fragments cannot be reordered at all.

VerticalTabsLayout needed a different shape: it is an options-API component, so
a module-level helper compiles fine and then renders nothing, because its
template can only see what the component returns. T is exposed from setup().

★ THE NEW GATE CAUGHT SOMETHING I WOULD HAVE MISSED, on its first real batch.
Adding these keys flipped two strings in
plugin-designs/pplr-data-maps/src/components/SemanticTreeLegend.vue from "no
catalogue entry" to "has a key and is still hardcoded" — that file is a COPY of
the library component I just fixed, carrying the same debt. It is another lane's
plugin so I have not touched it, and it is reported rather than quietly
baselined away. The baseline is refreshed to today's truth so the gate guards
the new line: 285 → 273 without a key, and the two copies recorded as known debt
rather than as a regression.
2026-09-26 13:04:16 +00:00
Doxa Maps
e9f5edf79d A gate for English typed into a template, so the ground we have won stays won
check:composite-locale-leaks asks a good question — "does the catalogue
translate this string, and is the page showing English anyway?" — but its
criterion is that the catalogue already HOLDS the string, so a string nobody
ever added is invisible to it by construction. And it runs in a browser, so it
only ever sees what happens to be on one screen.

That blind spot was measured, not guessed. After both composites passed the leak
gate with zero findings, a source audit of the same two trees found 31 more
user-visible strings. Most were aria-labels, titles and tooltips: text only a
screen-reader user or a hovering mouse ever meets. Our localization verification
had been "what can I see", and the blind spot was exactly the readers who most
need the words.

TWO CLASSES, because they are different defects with different fixes:
  NO KEY      wording must be written and translated — sometimes the Driver's call.
  HAS A KEY   the translation already ships in eleven locales and the reader is
              shown English anyway. The cheapest defect in the catalogue: the
              work was done and wasted. Nobody was looking for this one; it only
              appeared because the report was split in two.

★ WARN-ONLY AGAINST A BASELINE, DELIBERATELY. Failing on the whole backlog — 285
without a key, 29 hardcoded-with-a-key across 126 components — would make it
unlandable and it would be switched off within a day. It fails on what is NEW, so
the count can only fall; --update refreshes the baseline when a batch lands.

QaCorner is excluded BY NAME, with the reason in the file: an opt-in developer
widget that studio is making host-neutral or removing. Eleven locales for a
corner only we open is not worth the translations, and a judgement like that is
better written down than left as a silent gap.

NEGATIVE-TESTED ON BOTH CLASSES, because a gate never seen to fail is not
evidence: a new text node and a new title attribute are both caught and named;
an aria-label hardcoded to "Reset all" — which already ships in every locale — is
caught as the second class with the fix stated. Restored, it passes.
2026-09-26 13:04:15 +00:00
Doxa Maps
bb7a6d869f Cities with Internet: the map's own labels speak the reader's language
The connectivity map is built raw (new gl.Map), not through useMapInstance, so
it never handed the engine a UI locale: the canvas's "Map" name, "Zoom in",
"Toggle attribution" and the gesture hints stayed English under ar on both
engines (rega, row 34, _qa-cycle/rega/rows31-34-0f136d25/).

It now passes locale: engineUiLocale(readerLang(), provider) at construction,
with the same reader-language ladder useMapInstance uses (ProfileLoader's lang,
profile-config, ?lang=, <html lang>). The provider argument is ready for
studio's per-engine logo label; today's engineUiLocale ignores it.

Headless: the bundle builds and carries the engine key map (Map.Title...), the
ar catalogue has mapui.map = "الخريطة", and check:no-free-identifiers,
profiles-mount and universal-props pass. Not rendered in a browser.
2026-09-26 12:54:15 +00:00
Doxa Maps
49992a5ddd Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-26 12:50:53 +00:00
Doxa Maps
1f1edb6c4d The engine's own controls speak the reader's language
This leaf calls `new gl.Map` directly rather than going through useMapInstance, so
it never inherited the locale map the shared composable passes: under Arabic every
string we own was Arabic and the engine's own were not — "Toggle attribution", and
the canvas's accessible name, which is what a screen reader announces the map as.

The strings were already in the bundle; nothing was handing them to the engine.
Measured that way: the Arabic mapui catalogue is present in the built bundle both
before and after, while ENGINE_UI_KEYS — the map from the engine's key names to
ours — appears only after. Set at construction, the only time either engine reads
it. Needs eyes to confirm on screen.

The one English legend tooltip now goes through t(), with the English kept in the
template as the fallback so it renders exactly as it does today until the key
lands, rather than showing "gospel.legend.hnrTip" in a tooltip.
2026-09-26 12:50:05 +00:00
Doxa Maps
a6273dc905 Merge studio/mirror and rebuild: library/ changed, so the bundle had to follow
studio/mirror (c85eeee7) rebuilt every bundle and touched library/ —
PinHint, LegendDesktop, LegendTools, MapControlMenu, ShareButton, PosterDialog
and others — so unlike the previous merge this one genuinely required rebuilding
my bundle rather than just re-hashing it. Checked rather than assumed: the
decision of whether to rebuild turns on `git diff --name-only <base> <main> |
grep '^library/'`, and last time that was empty and this time it is not.

ALL SIX CONFLICTS WERE DERIVED ARTIFACTS — my bundle, my three leaf HTMLs and the
two manifests — and ZERO source files conflicted. Resolved to main's freshly built
versions and then regenerated from the merged source, because picking a side of a
generated file is meaningless: neither side is authoritative, the source is.

VERIFIED BY HASHING, which is the step whose absence shipped a blank page three
commits ago. fix:sri rewrote files this time (it reported nothing to do last time
only because `tree` had already fixed them), so the summary alone would have been
even less informative. Recomputed sha384 over the actual bundle: upg-100-list,
upg-100-mfe and poster-wall all MATCH, and a whole-tree sweep is 57 integrity
attributes with 0 mismatches.
Used fix:sri and NOT tree, per the gate's own advice — tree re-reads .vue source
and on a shared branch can bake in another agent's half-finished work. studio had
already regenerated the pages, so there was nothing to regenerate.

BLAST RADIUS: the only bundle differing from main is mine. Pins-first source
confirmed intact after the merge (boundaryPromises, paintPinsEarly, the deferred
boundary await).
2026-09-26 12:38:51 +00:00
Doxa Maps
1795fb4740 Plugin Studio rail laid out like plugin-designs/: one folder per bundle, its elements inside
The Driver's ask: one folder per BUNDLE with its ELEMENTS inside, bundles sorted by capability, highest
first. The rail listed every element flat under its own capability, so a bundle's elements were
scattered (gospel-resources' five screens sat in three sections). Now each capability section (5 → 1)
holds the bundles whose HIGHEST capability it is, in name order; a bundle with several elements is one
folder (country-posters, gospel-resources, my-upg-100-list, regions-dashboard, cities-internet), and a
single-element bundle is shown as its element. Composites keep their folded composition trees.

StudioTree identifies a plugin's row by a `canonical` flag rather than "depth 0", so auto-reveal opens
the bundle folder above an element chosen by URL or restored tab. New folder icon; 'elementsCount' and
'elements' in all 11 studio catalogues.
check:studio-rail (headless, gates:post) runs the studio's real rail code on the built manifest and
asserts the layout and one canonical node per plugin (21 bundles, 33 elements); it fails on the previous
rail. Also: poster.json:dpi is declared identical on purpose in check:i18n-never-translated (a unit
symbol kept as-is by project decision), which had turned that gate red on main.
2026-09-26 12:23:49 +00:00
Doxa Maps
ff99c2d27e PPLR data maps: the map boots in the projection its button says
The three map views ask for projection:'mercator' at construction, but on
Mapbox the style's own projection (globe) wins, so the map booted as a globe
while the view's state and button said Mercator, and the first "Switch to
Globe" click did nothing (rega, C2 at 0f136d25: pre/post-mapbox logs, "proj
globe -> globe"). The existing setProjectionFor ran only after a basemap
switch, never at boot.

Each view's 'load' handler (religion, rop-tree, language-families) now applies
the state's projection first; the style is ready there on both engines.
Headless, against a map whose style projection is globe: before, the map is
globe while the state is mercator; after, both are mercator.
2026-09-26 12:14:42 +00:00
Doxa Maps
64568ba685 My UPG Neighbors: the flat projection is applied on MapLibre too
Four lenses (send-map, lang-cluster, the cockpit's MapCell and FlowerClusters)
called setProjectionOn(m, 'mercator') straight after initializeMap(). MapLibre
throws "Style is not done loading" from setProjection before the style loads;
setProjectionOn swallows it and returns false, so on MapLibre the projection was
never set (rega, C2 at 0f136d25). Mapbox accepts it early, which is why only
one engine showed it.

A small plugin helper, src/whenStyleReady.js, runs the call at once if the style
has loaded, else on 'style.load'. Headless, against a MapLibre-shaped map that
throws before load, with the real setProjectionOn: before, it returns false and
the projection stays null; after, it is mercator, both when the style loads
later and when it is already loaded.
2026-09-26 12:14:41 +00:00
Doxa Maps
afb34488cc A country click ends in a move you can make now
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Every number on the country sheet is work for somebody else to fund, staff or
translate. The one thing the reader can do before closing the tab was reachable
only by guessing which pin to tap, so the sheet now names it: the largest group in
this country with nothing and nobody praying, one tap from its prayer page.

Need first, then size, and India shows the cost of that order — of its 601 groups
with nobody praying only five have nothing at all, so it offers Malai Pandaram at
8,700 people ahead of Badhai at 851,000, who already have Scripture and film. The
sub-label says which rung the reader was handed.

The count above it reads "601 / 779", never a bare 601: doxa.life publishes a page
for 1,723 of the 12,379 groups here, so nobody-praying can be said of the groups
that HAVE a page and never of the country. Zero still means zero on doxa.life's
counter, not a claim about the world.

Costs 1.05 kB: the prayer pages and the tabs catalogue were already shipped.
2026-09-26 11:54:39 +00:00
Doxa Maps
91d9edba72 Merge main and rebuild through the whole recipe, not just the first step
MY LAST THREE COMMITS SHIPPED A PAGE THAT COULD NOT RUN, and rega caught it: the
committed bundle hashed to sha384-k1mPDt9L… while every leaf's integrity attribute
still pinned sha384-GEhDNkbg… — the PREVIOUS bundle. The browser refuses the script
outright, so 6 of 6 of their runs loaded only the html and the js and never created
a map. A blank page.

I RAN `vite build` FOUR TIMES AND NEVER RAN THE OTHER TWO STEPS. dx-shepherd's
recipe is build -> tree -> fix:sri, and I ran the first third of it and reported
success each time because vite exited clean. It did — the BUILD was fine. What was
broken is the ARTIFACT PAIR: index.html and my-upg-100-list.js are two files that
must agree about a hash, and "vite exited 0" says nothing about whether they do.
That is the same measure-the-artifact-not-the-property mistake I have spent this
whole session finding in other places, committed four times in a row in my own
build step.

FIXED PROPERLY THIS TIME:
  · merged main (d3d3c3f8). rega also flagged the stale base — the branch was on
    c2af6b16 and missing main's catalogue trimming, which is why the bundle was
    1,026,943 B against main's 811,632. After the merge it builds to 792 kB.
  · the ONLY merge conflict was public/dx-maps/my-upg-100-list/my-upg-100-list.js,
    a DERIVED artifact — so neither side is authoritative and picking one would be
    wrong on principle. Regenerated it from the merged source instead.
  · then `bun run tree`, then `bun run fix:sri`.

VERIFIED BY HASHING, not by trusting the tool's own summary. fix:sri reported "115
already agreed, rewrote 0", which is only reassuring if `tree` had already fixed
them — so I recomputed sha384 over the actual bundle and compared it against every
leaf that loads it: upg-100-list, upg-100-mfe and poster-wall all MATCH. Then swept
the whole tree: 57 integrity attributes checked, zero mismatches.
AND CHECKED THE BLAST RADIUS, because `tree` regenerates every leaf's HTML: the
other bundles under public/ (all-people-groups.js, big-picture.js) are byte-identical
to main — the merge brought main's versions, my rebuild did not touch them. Only my
own bundle differs, as it should.

NOT SIMPLIFIED YET, for the record: the merge brought gb-IND-ADM1-full.geojson
(1,796 KB) as an ADDED higher-detail variant. The SERVED files are unchanged
(world-adm0 1,866 KB, gb-IND-ADM1 1,628 KB), so the 3.6 MB parse cost still stands
and the boundary-simplification work is still pending elsewhere.
2026-09-26 11:46:13 +00:00
Doxa Maps
0f136d2513 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-26 11:42:26 +00:00
Doxa Maps
aba2dfb710 Drop the select-zone compatibility fallback: the applier now answers for itself
The cleanup I promised when the fallback went in, done now that both halves are on
main (verified BY CONTENT at ccc2b1bb, not by sha).

onLegendSelect returns an EXPLICIT boolean on all four of its paths — false when the
tapestry layer is not up, true for batch mode, true for the un-isolate, true on
success. No path yields undefined, so the `typeof applied === 'boolean'` capability
check can never fall through and the inference behind it is unreachable. Three lines
become one.

WHY THE CHECK WAS WRITTEN THAT WAY, since the reasoning outlives the code: upg100-plugin
warned that a function with no explicit return yields undefined, which is FALSY — so
adopting their boolean before it landed would have read every success as a failure and
retried it. Testing the CAPABILITY rather than the version meant this worked before
their change, after it, and in any bundle built from either, with no flag day.

AND WHY NOT TO REINTRODUCE THE INFERENCE, which is the part worth keeping: I had read
!selectedKey.value as "it failed". That is a proxy for "did it ISOLATE", not for "did
it work", and the two come apart in exactly two paths — batch mode feeds the selection
union and the un-isolate clears it, and both leave selectedKey unset while having
succeeded. Retaining on that would have re-applied the zone and toggled it back OUT of
the union. upg100-plugin found it prospectively and added the boolean so the applier
could be asked instead of guessed at.

Source only; my-upg-100-list needs the rebuild.
2026-09-26 08:51:50 +00:00
Doxa Maps
370446390f C1b: the resource palette moves to library/colors/, and the local colour globs go with it
Completes C1. Source only; studio rebuilds (rule 5B). Cut on top of C1a because it
depends on that move. Rebuild scope: country-posters, doxa-research-map,
research-dashboard, dashboard-plus-grid, gospel-resources.

resource.js existed as TWO identical plugin-local copies with no library original,
while library composables resolved it through the registry — so the library depended
on a plugin registering a module the library itself documented. Now it lives in
library/colors/ like the other eight, both copies are gone, and src/colors/ holds no
.js in either plugin. The local `registerStrategies(import.meta.glob('./src/colors/*.js'))`
in both index.js files is therefore DELETED: it matched nothing after the move, and a
glob that matches nothing reads as a deliberate override. Their now-unused
registerStrategies imports went with them. That also completes the shepherd's original
C1 instruction, which C1a had to defer for exactly this reason.

★ MY OWN PREDICTION HERE WAS WRONG, and the measurement is the only reason this is
clean. I predicted check:one-palette would FAIL after the move, reasoning that the
dashboards carry 6 of resource's 8 hexes (because 6 are SHARED with affinity-block and
language-family, which they legitimately ship) while the 2 unique ones — #20c997
jesusFilm and #2c3e50 noResources — are absent, and the gate fails a PARTIAL palette.
The gate PASSES: 9 strategies, 18 pairs. Once the module is in library/colors/,
_registry.js bundles it into every consumer, so all 8 hexes are present BY CONSTRUCTION.
I had reasoned about the pre-move bundle and assumed the rebuild would not change it.

That also corrects the diagnosis I gave earlier. simpleresearch was right that there
was no disagreement, but not that the 6 were coincidental substrings in minified
output: they are real hexes, genuinely present, shared across three strategies. The
gate cannot attribute a hex to a strategy — which is a real limitation worth knowing,
just not one that bites here.

Stale comments corrected rather than left to rot: five references across
useLegendData, researchLegendOptions, useWagfRegionsLegendData and my own
wagf-region.js header all claimed these palettes live in a plugin folder. One
parenthetical keeps the old path deliberately, as history.

GATES: check:one-palette PASS 9/18 (baselined 6/12 before C1a). check:library-index
PASS after regenerating the derived menu. check:no-free-identifiers PASS. All six
affected bundles build; their artifacts reverted.
2026-09-26 08:51:39 +00:00
Doxa Maps
303b712469 Draw the pins first: they need 68 KB, not 3.6 MB
rega's BASELINE TRACE OVERTURNED MY OWN PROFILE, and this commit is built on their
measurement rather than my arithmetic. Baseline at main ccc2b1bb, tokenless
MapLibre, software GL: every key byte arrives by 1.2-2.9 s, and the first territory
draws 5.6-6.4 s LATER. Only ~2.6 s of that gap is main-thread long tasks. The rest
is MapLibre's WORKERS parsing and tiling ~3.6 MB of boundary GeoJSON.

I HAD PROFILED FILE SIZES AND REQUEST ORDER — the artifact — and concluded the
download was the cost. On localhost those 3.6 MB arrive in ~230 ms. Reordering
requests was never going to reach 3 s, because PARSING is the bottleneck, and my
own two-pass-zones plan would have been just as blocked: resolving even the 15
record-free zones still needs world-adm0 parsed. dx-shepherd dropped it in favour
of pins-first (ENDTOKEN-DXS-UPG-PINSFIRST). Right call.

THE PINS NEED NONE OF THE GEOMETRY, verified in the code rather than assumed: they
are built from `records` (lat/lng) plus slugToZone, and the evaluate() in the pin
build maps slugs to zone keys without touching a boundary file. 68 KB, arriving at
~2.9 s cold.

SO THE BOOT ORDER IS NOW: records (68 KB) -> build pins -> PAINT PINS -> await the
boundaries (3.6 MB parse) -> tapestry -> search index -> full repaint.
Three things make that safe rather than clever:
  · all three boundary downloads are still started BEFORE any await, so nothing is
    serialised — they fetch and parse while the pins are being drawn;
  · paintAll now paints WHAT IS READY instead of refusing until everything is. Its
    old guard `if (!map || !tapestry) return` was what forced the pins to wait on
    the parse. Every step below it is idempotent and guards its own source AND
    layers, so calling it twice adds each thing exactly once — which is only true
    because of ea90e9b2;
  · `tapestry` STAYS WHOLE-OR-NULL. Nothing paints a partial tapestry; the pins
    simply stop depending on one. boundsFor(), the search index and the status
    telemetry keep their invariant, so a reader can never search or fly-to against
    a half-built index. That was the correctness constraint dx-shepherd and I
    agreed before rega's trace, and it survives the change of plan.
The early paint is FIRE-AND-FORGET: it uses the same style-readiness rule as the
boot gate, and if the style is slow the pins simply appear later while the boot
gate repaints everything regardless. It must not delay the boundary await.

⚠ NOT MEASURED BY ME. Every claim about timing here is rega's; I have only verified
that the code order now matches the plan and that the build is clean. Sending them
branch@sha for the same cold trace, and the honest comparison is baseline-cold vs
this-cold in THEIR environment — software GL inflates rendering, so the absolute
numbers are not the point.
ALSO CONFIRMED BY THEIR TRACE, about the previous commit: the list API sends
`public, max-age=14400`, so dropping cache:'no-store' is a real win and not a
guess — the warm reload was still refetching 68 KB over the network, which fits a
client-side no-store exactly.

BUNDLE=my-upg-100-list vite build clean; check:packet-strings-parity PASS.
2026-09-26 08:40:53 +00:00