Commit graph

1844 commits

Author SHA1 Message Date
Doxa Maps
29bbab3967 Two plugins built the same search highlighter; now there is one
Dispatch work item (5) asked for useTextHighlight in library/. RULE 2 wants a named
SECOND consumer before anything moves there, and it exists: regions-dashboard's
UupgsList and people-groups-grid both consume the same Fuse-compatible shim, both
read match.indices, both wrap hits in <span class="search-highlight">, and both
append the tail only on the last range. Line for line the same construction, apart
from variable names.

So library/composables/useTextHighlight.js exports highlightRanges(value, indices),
and UupgsList calls it. Its local escapeHtml goes too: every caller was inside the
highlight body that moved.

★ ESCAPING NOW COMES FROM library/utils/html.js, AND THAT IS A CHANGE FOR BOTH
CALLERS, stated rather than smuggled. Three implementations existed and differed:
  library/utils/html.js  escapes & < > " ' AND backtick; returns '' for null
  UupgsList              no backtick; guarded null via String(v ?? '')
  people-groups-grid     no backtick; NO NULL GUARD — s.replace(null) THROWS
Both deltas move in the safe direction — one more character escaped, one crash
removed — and neither changes the output of any value that renders today. The gate
pins both, with controls proving each delta is a real difference rather than a
no-op: the old copy really does leave a backtick unescaped, and their escaper
really does throw on null.

people-groups-grid is NOT migrated here: it is another owner's file, so it is
handed over as source. The gate asserts it still builds its own highlighter, so
this check states the truth rather than implying the job is finished.

check:text-highlight is registered (script line, gates:post token, hand-inserted)
and negative-tested: an implementation that escaped nothing fails it, and the
output is asserted to depend on the indices.
2026-09-27 01:16:50 +00:00
Doxa Maps
f13075b1d4 Decouple the tooling: no host named in internal/ or the build config
Six tools defaulted to the live host's URL. They now read the published tree's location from
DOXA_PUBLISHED_BASE (environment, or the gitignored .env / .env.local) through internal/_published-base.mjs,
and with nothing set they report NOT MEASURED (exit 75) instead of measuring a host they were never told
about: check:catalog-currency, check:catalog-identity, check:published, verify:5-caps, verify:map-paint. An
explicit BASE / --base / PUBLISHED_BASE still wins. check:embed-base only needed an example URL for its
offline resolution table.

The public-hygiene leak detector builds the platform name from parts, so the file that catches it does not
carry it. Comments and the two build-config notes say the host platform or the live test host. From
repo's DECOUPLE round 2 list.
2026-09-27 01:16:50 +00:00
Doxa Maps
25c3d10abd Rebuild country-posters, and regenerate the tree
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-27 01:11:39 +00:00
Doxa Maps
81ca86e39f Country posters: a religion search names the family in the reader's language
The legend row it selects is keyed on the family letter, so only the shown label changes.
2026-09-27 01:10:49 +00:00
Doxa Maps
165983c2c8 Country posters: the geocoder column is styled on MapLibre too
countries-map styled only .mapboxgl-ctrl-top-left. MapLibre names its
container .maplibregl-ctrl-top-left, so under MapLibre the column got no
position, width or margin rule at all. The three rules (desktop column, its
controls, and the mobile full-width row) now name both engines' classes.

Headless: build OK, and the shipped CSS carries the maplibregl selectors.
Visual check: rega, countries-map on ?provider=maplibre (en and ar).
2026-09-26 22:19:56 +00:00
Doxa Maps
e4f2be71c4 Country posters: the geocoder column follows the reading direction
countries-map pinned its geocoder column with a physical left:8px, so under ar
the search bar stayed on the left while the legend (studio's fixed legend) moved
right, and the two no longer stacked as one column. It now uses
inset-inline-start:8px, which is 8px from the start edge in either direction.
The pinned card's label spacing (margin-right) becomes margin-inline-end too.
The mobile rule already sets both left and right, so it is symmetric and
unchanged.

Headless: build OK, and the shipped CSS carries inset-inline-start:8px. Visual
check needs a browser (ar, both engines).
2026-09-26 22:19:56 +00:00
Doxa Maps
835ec1ac28 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: all-people-groups
big-picture
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
people-groups-grid
pplr-data-maps
regions-dashboard
research-dashboard
research-map-plus-grid
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-26 22:14:55 +00:00
Doxa Maps
d8e50de16b cities-internet takes the locale-honouring fallback, and drops its English copy
exp-shepherd's call as the plugin's owner, on the reasoning gospel-plugin gave
first: the bundle already carries all 11 catalogues, so a partner embedding it
without vue-i18n being served English is a bug rather than a decision.

So the three files pass no fallback and resolve through tStatic — the runtime
locale first, English as the last resort. Their enLookup was English-only, which is
exactly what served English to those hosts, so it goes, and the per-area
connectivity.json import goes with it: tStatic reads the same area out of the
catalogue the bundle already ships. Three fewer copies of a lookup, three fewer
imports of bytes that were already there.

Two entries added to the register as DELIBERATE absences, so a later reader cannot
"complete" C4 by converting them:
  · pplr-data-maps/src/components/SemanticTreeLegend.vue keeps its hand-rolled
    guard at exp-shepherd's request — it is carded for retirement
    (RETIRE-PPLR-SEMANTICTREELEGEND) in favour of the library component, and a
    migration whose value expires before it lands is not worth the diff;
  · country-posters-mfe.vue and key-cities/cities-map.vue are their owner's to cut,
    so that country-posters stays one landing and key-cities stays gated.
2026-09-26 22:06:59 +00:00
Doxa Maps
ef5ea1840a C4 wave 3b, cities-internet: three identical guards become one call
All three files carried the same block byte-for-byte — enConnectivity + enLookup +
fill + try/catch — and now call useOptionalT('connectivity', { fallback: enLookup }).
English is unchanged on a bare host, because each passes its own enLookup rather
than taking the catalogue default.

gospel-plugin chose the catalogue default for their bundle, on the argument that a
partner host without vue-i18n is served English while the bundle already ships all
11 catalogues. The same one-word choice is open to cities-internet's owner; it is
not a migration's to take, so this preserves what they have.

Their local fill() used `k in params`, so the shared fillParams also removes a way
for these three to render the literal string "null" into a sentence.

Also records, in the stripper itself, the two things codeOnly does NOT strip —
CSS comments inside a JS template literal, and HTML comments in a .vue template.
Both have already cost a real measurement here: a specificity read of (0,5,53),
and a bare-useI18n census that read 3 where the code had 2. gospel-plugin's
generalisation is the one worth keeping: any <!-- --> that quotes an identifier
will keep doing that to you. The stripper must match the LANGUAGE of the text.
2026-09-26 22:06:59 +00:00
Doxa Maps
9816a4244f gospel takes the locale-honouring fallback, and it is their call that made it
gospel-plugin's decision, in their words: "most partners will drop a custom
element or an iframe and never install vue-i18n ... an Arabic ministry site
embedding this map serves its readers English while the bundle already carries all
11 catalogues. That is a bug, not a decision."

So all four gospel translators drop `{ fallback: enLookup }` and take the
helper's catalogue default, which resolves through tStatic: the reader's locale
first, English as the last resort. Their three conditions are met and each is
asserted, not assumed:
  1. resources-map passes NO namespace — its keys span gospel.* and pins.*;
  2. the fallback walks the catalogue ROOT, which tStatic does by construction;
  3. params survive, via the null-safe fillParams.

★ THE ROW THEY ASKED FOR, and it is the only thing here that could catch the
regression this migration risked: a BARE host under ar must render
pins.approximate in ARABIC — not the key, not gospel.pins.approximate, not the
English. It reads the SHIPPED ar/ and en/ pins.json and rebuilds tStatic's own
resolution order over them, so it measures the real catalogue rather than a stub.
Three negatives beside it, plus a control that both catalogues really carry the
key, so the comparison is measuring a difference that exists.

enLookup is deleted from all four, and the EN_MESSAGES import from three —
data-tables keeps it, because it still builds a map from EN_MESSAGES.gospel.res at
:182. Checked before deleting rather than after: check:no-free-identifiers would
have caught a wrong removal, since removing something still used creates a name
that is valid JS until it runs.

Two corrections that came from other agents reading this work:
 - studio: the JSDoc carried <area>.<key>-shaped examples, which
   check:i18n-keys-exist reads as real keys and reports missing. They reworded it
   twice because my file copy restored it. Reworded, with a standing note in the
   file so a third copy cannot bring them back.
 - the register said each site "takes the CATALOGUE fallback, as it always did".
   True of the two composables, FALSE of gospel's four, which were moved here by
   their owner. A false statement inside a gate message is worse than no message,
   because a reader has no reason to doubt it. Now accurate per row.

check:i18n-keys-exist is red on messages.engineNotLoadedDetail. NOT mine and not
touched here: it is in cities-internet/connectivity-map.vue, whose own comment says
the key "arrives with upg100's ade12e4c", and the call already guards for its
absence.
2026-09-26 22:06:59 +00:00
Doxa Maps
0d0ee3cbed SemanticTreeLegend says a dot is a locality, on every map with people-group pins
The Driver's card BUILD-APPROXIMATE-PIN-NOTICE: every map that shows people-group pins must say so plainly.
The standing APPROXPIN note existed only under LegendRows and in the pin card, so maps whose legend is
SemanticTreeLegend (the prayer map's flat tabs among them) never showed it (rega, row 46).

It now renders under the tree when the map's own data holds named Point features (the injected dataStore);
maps without people-group pins show nothing. A pinNote prop overrides for a leaf that already says it.
2026-09-26 22:06:58 +00:00
Doxa Maps
8ec207c662 My coalescing broke the echo guard it ran past
rega, row 50: a region row click emitted {region:"asia"} and then an EMPTY event,
identically on both engines. The grid stayed narrowed only because the
composite's 1.2 second push window happened to swallow the second one — and a
pass that depends on a swallow is not a pass. rega said so, and was right to
refuse to score it green.

THE CAUSE IS MINE AND IT IS NOT A TIMING QUIRK. emitFilterState has always opened
with `if (applyingExternal) return`: when the HOST pushes state DOWN through
externalFilters the store changes, and reporting that back up is an echo of the
host's own instruction. That guard worked because the emit ran SYNCHRONOUSLY
inside the watcher, while the flag was still set. My coalescing moved the emit
60ms later, and applyingExternal is released on nextTick — so by the time the
emit ran the flag was false and the echo went out.

I fixed a burst and broke a guard that depended on the burst's timing. Changing
WHEN something runs is a change to everything that depended on when, and I did
not re-read what else was standing on that synchrony.

So the decision moves to schedule time, where the flag still means what it says:
a change that arrived from the host is not a change the reader made, and is not
reported back. The check inside emitFilterState stays as a second line of
defence.

This is the same shape as the dialect case on the branch below, and it is why
that one is a guard rather than a longer timeout: both are "the emit is no longer
where it thought it was".

★ AND THE GATE CAUGHT THE SAME HOLE IN ITSELF FOR THE THIRD TIME. The assertion
searched the scheduler for `if (applyingExternal) return`, and the comment
explaining the fix QUOTES that line — so deleting the code left the gate green.
It strips comments before asserting now. A check its own documentation can
satisfy is not a check, and I have now written that mistake three different ways:
a probe that yields zero when its assumption is wrong, a mutation aimed at one of
three identical bindings, and this.

Source only, stacked on sr/dialect-empty-cascade. STUDIO: rebuild
doxa-research-map (the same rebuild that branch already needs).
2026-09-26 22:06:58 +00:00
Doxa Maps
dffae0502c A clicked pin finds the groups that share its point
rega, row 46: on a point four groups share, the note never said so. A click hands the legend the RENDERED
feature, whose geometry the engine rebuilt from tile coordinates (about 0.001 degree off at low zoom), and the
co-location index matched on the exact coordinate. The pin is now resolved to its roster row by name, and to
the nearest of those rows when a name repeats across countries; only a name the roster does not know falls
back to the pin's own coordinate. check:approx-pin-note adds both cases; the tile-rounded one fails on the old
code.
2026-09-26 22:06:58 +00:00
Doxa Maps
087b005350 Big Picture: the live count speaks, and the locale reaches the children
rega, row 47 at 52eeb0db. Two findings, both mine, and the first is the smaller
and more certain of the two.

"2107 shown" STAYED ENGLISH IN BOTH CHANNELS, because it was never a lookup:
BpResearchHeader rendered the count and then the bare word. It reads the
catalogue now, with the English wording kept exactly as it was — "{n} shown" —
per the standing rule that translating existing wording is not a product
decision. Eleven locales.

THE CHILDREN WERE HANDED ONLY THE LIST. big-picture built its children's
profile-config from `list` alone, so the one documented channel a partner has for
asking a nested element to speak Arabic arrived empty of any locale.
setRuntimeLocale covers everything that reads tStatic, which is why the headline
and much of the chrome were already correct through both channels; it does not
cover a child that reads its own profile-config. Threading the locale is what
ProfileLoader would have done had one existed beneath this composite — and the
reason none does is the (C) failure mode this element is the last example of: a
composite that BAKES its children in has no loader under it, so it must do that
job itself.

★ THIS IS ONE IDENTIFIED GAP, NOT A CLAIM TO HAVE CLOSED THE MEASURED ONE.
check:host-locale-contract reports 402 against 888 for this leaf, and 31 Arabic
text nodes against 79. I can see that the children were given no locale and that
giving it to them is correct regardless; I cannot see from here how much of that
difference it accounts for, and I am not going to assert a number I have not
measured. The gate is a browser gate and rega owns the re-run.

Source only. STUDIO: needs a rebuild of big-picture.
2026-09-26 22:06:58 +00:00
Doxa Maps
c53d356d3f The one event was the cascade's empty moment, not the selection
rega, row 43 case 3 at 52eeb0db: the coalescing landed — one event per click,
exactly as intended — and in 3 of 4 runs that one event was EMPTY,
{language:"", languageCode:"", languageCodes:[]}, so the grid stayed at 2,107.
The name fix works whenever it fires; the event just carried nothing to name.

THE CASCADE PASSES THROUGH A CLEARED STATE. mapStore.selectLanguage() sets
selectedDialect = null BEFORE it sets the language, so a dialect click walks the
store through "nothing selected" on its way to the answer. The settle timer fired
inside that window and read exactly what was there. The composite then did the
right thing with what it was told: an all-empty report IS how this element says
"the reader cleared everything".

MY FIRST FIX WAS A TIMEOUT, AND A TIMEOUT WAS THE WRONG KIND OF ANSWER. 60ms was
enough in my seat and not under headless swiftshader. That is the third time I
have tuned a duration on one machine and called it settled, so this one does not
get lengthened — lengthening moves the failure rather than removing it.

The guard is SEMANTIC instead. An all-empty read arriving straight after a filled
one is the one genuinely ambiguous case — it is what a mid-cascade read looks
like AND what a real clear looks like, and nothing in the payload separates them.
So it is re-taken rather than believed: the transient fills in and is reported
correctly, while a real clear stays empty through every re-take and is reported a
few tens of ms later, which no reader can perceive. The re-takes are BOUNDED
(3 x 60ms) so a clear can only ever be delayed, never swallowed, and the whole
budget stays far below the composite's 1.2s push window so the two cannot
collide. The budget resets on every fresh user action.

Note what is NOT assumed: the retries re-read the store on their own timer rather
than waiting for another watcher fire. In the failing runs the watcher fired only
ONCE, so a fix that depended on a second fire would not have worked.

check:language-code-link grows a section G for it, asserting both halves — the
guard exists, and it is bounded. Without the first the transient ships; without
the second a clear can never be reported at all. Both negative-tested.

Source only. STUDIO: needs a rebuild of doxa-research-map.
2026-09-26 22:06:58 +00:00
Doxa Maps
a94a85cb8c C4 wave 3a, gospel-resources: four guards share one, English unchanged
From regions-plugin e04121f1 re-cut as 7138109f on main 4d18a6f3. charts, data-tables and resources-list call
useOptionalT('gospel', { fallback: enLookup }); resources-map calls useOptionalT('') because its keys span two
areas (gospel.* and pins.*) and its enLookup walks the catalogue root, so a namespace would have turned
pins.approximate into gospel.pins.approximate. Each keeps the English fallback it already had.

useOptionalT's fillParams substitutes only non-null values (the rule main's own fill() already used), so
{ count: null } leaves the placeholder visible instead of printing "null". check:optional-t-equivalence
(59 assertions) asserts the empty namespace for resources-map and strips HTML comments before counting its
bare useI18n() calls.
2026-09-26 22:06:58 +00:00
Doxa Maps
f9020e4910 The legends mirror now, and their labels never became their values
rega's Arabic survey on main 52eeb0db: the research-dashboard legends sat on the
wrong edge with English text, 70 of 85 letters Latin, in Big Picture and in the
Research Gallery. Two faults that look like one, and they fail in opposite
directions if you fix them carelessly.

A PHYSICAL INSET DOES NOT MIRROR. right:8px pins a box to the physical right edge
in every locale, so in Arabic it stays where an English reader expects it while
everything around it flips. Both surfaces the survey named are logical now, and
inset-inline-start:auto travels with inset-inline-end as a pair rather than as
decoration: it cancels a physical inset inherited from a control container, which
otherwise stretches the box across the map once the anchored edge moves.
The second surface is research-map's geocoder column, which studio found in its
own sweep. That column exists ONLY to share an x-edge with the legend below it —
the alignment is recorded in the file — and the legend already mirrored while the
search bar did not, so under ar the pair came apart and the bar was stranded on
the side the legend had vacated. A shared edge that one of the two honours is
worse than no alignment: it reads as a broken layout rather than as a choice.

THE ENGLISH WAS NOT CHROME, AND THAT CHANGES THE FIX ENTIRELY. The legend renders
a facet VALUE, and for the religion donut that value is a family name DERIVED in
English by library/colors/religion.js — the same table people-groups-grid
re-derives per row so the two ends byte-match. Translating those strings is the
obvious fix and the dangerous one: it would work in English and match nothing in
Arabic, SILENTLY, because an empty intersection reads as "no results". That is
the language-link defect one facet over, and it cost a card to learn the first
time.

So the value stays canonical English and only the visible text is projected, the
same rule the shareable URL follows, where the code is canonical and the slug is
display-only. religionFamilyLabel() lives beside the table it projects, so the
next consumer finds it rather than inventing a second spelling, and the table
itself now carries a comment saying these are values and must not be translated.
Ten family names in eleven locales, English wording held.

check:legend-mirrors guards both halves — and its own negative test found a hole
in it. A mutation aimed at the legend's data-key silently hit a DIFFERENT
:data-key, the donut segment, and the gate stayed green while a projection sat on
a wire. There are three such bindings and it was guarding one. It now checks
every :data-key and :data-facet in the file; all three mutants are caught.

★ AND I BROKE THE FILE'S PARSE ON THE WAY, worth recording because the rule is
already written down. I used backticks around CSS property names in a comment
that lives INSIDE a shadow-CSS template literal, where a backtick ends the
literal and everything after it is read as code. check:sfc-parse and
check:no-free-identifiers both caught it immediately. The comment now says so, in
the file, without backticks.

Source only. STUDIO: needs a rebuild of research-dashboard, dashboard-plus-grid,
doxa-research-map and research-map-plus-grid.
2026-09-26 22:06:58 +00:00
Doxa Maps
c38efae49c gospel.guide: the legend's way-in sentence, in 11 locales
wayInSome ("{n} of those {total}"), wayInAll ("Every one of those {n}") and wayInWhat (the sentence that
follows), requested by gospel-plugin: the last English sentence in an Arabic legend. The resources map already
reads them through tOr() with its parameters, so it switches on its own once they land.
2026-09-26 22:06:58 +00:00
Doxa Maps
a693ca6832 Decouple deployment/: no go-base / nuxtbase / doxa-base names in the Tauri README, the WordPress how-to and its contract test
Prose: the Tauri README's token note says 'the test host', the WordPress how-to's hosting option says
'a Forgejo host you run'. The WordPress contract test's catalog base was the live host as a fixture;
it is now https://cdn.example.org/plugin-assets/ in all three places (the passed base, the expected
script src, and the negative test), which keeps the test self-consistent: check:wp-embed PASS before
and after.
2026-09-26 22:06:57 +00:00
Doxa Maps
09aba6dc92 C4 wave 2: five library components share the guard, and keep their own English
The five are NOT one shape, and treating them as one would have changed what
readers see. Measured before touching them:

  ApproxPinNote, FilterDropdown   fall back to a literal map
  SemanticTreeLegend, PosterWall  fall back to humanizeKey() — 'legend.columns.name' renders 'Name'
  PinHint                         falls back to a one-key map
  SemanticTreeLegend, PinHint     also capture i18n.te (used 3x and 1x)
  PosterWall                      also captures i18n.locale

So useOptionalT grew an optional `fallback` hook, and useOptionalI18n was added
for the callers that need te and locale. Every site passes the fallback it
already had, so not one rendered string changes. Routing them through the
catalogue instead would have altered what seven bare-host bundles print, and —
because tStatic honours the runtime locale — would have started translating
strings that are English today. That may be an improvement; it is not this
commit's to make. Changing a default is not a migration.

te keeps its existing contract too: FALSE on a bare host, never "true because the
catalogue has it". PinHint and SemanticTreeLegend both record why — te is how
their callers know to use their own defaults.

THE CHECK NOW LIVES ON MAIN, which is the point of this commit as much as the
migration is. dx-shepherd: the 18-assertion version was in _qa-cycle, so it would
never have run again. internal/check-optional-t-equivalence.mjs is registered as
check:optional-t-equivalence — script line, //scripts note and gates:post token,
all inserted by hand rather than by re-serialising package.json. It carries 46
assertions: the helper's contract across all three host states, a branch-for-
branch equivalence against the old hand-written code, and a PER-SITE REGISTER
that every later C4 wave appends a row to. Negative-tested from outside: strip one
site's own fallback and it fails naming that site.

check:library-index went red on the new composable and is green again after
`bun run library-index`. That is the staleness gate working, not a flake.
2026-09-26 22:06:57 +00:00
Doxa Maps
1a17d3a211 One guarded translator, and the fallback catalogue nobody needed
C4, first wave. Twenty places each stored the same fact — how to translate when
the host never installed vue-i18n — and two of them, usePplrT.js and
useNeighborT.js, were whole-file near-duplicates at Jaccard 0.82. That pair was
the curation audit's evidence that the guard was being COPIED rather than shared.
Both now call library/composables/useOptionalT.js and keep their public names, so
no call site changes.

★ THE SPEC ASKED FOR (namespace, fallbackCatalogue) AND THE CATALOGUE IS
UNNECESSARY. Every copy imported its own English JSON and hand-walked it after
stripping the namespace prefix. But library/i18n builds `messages[locale][area]`
keyed by the JSON's own filename, and tStatic() already walks that tree and
already falls back to English. So `import enPplr from '.../en/pplr.json'` was a
second copy of bytes the bundle already had, and the helper takes one argument.

★ AND IT IS NOT A SIZE REGRESSION, which was the one good reason the narrow
import might have existed. The build NARROWS library/i18n's catalogue glob per
bundle to the areas internal/i18n-areas.mjs finds in that bundle's source graph
(check:i18n-treeshake). A bundle that says 'pplr.' gets the pplr area, not the
other thirty. Verified before writing the helper, because "the obvious dedup
makes every bundle carry every locale" would have killed it.

Resolution order, each step earning its place: the host's own vue-i18n when it
installed one, so a host managing locale keeps control; then tStatic, which
honours the runtime locale and falls back to English; then the key itself, so a
missing string is visible rather than blank. The middle step also covers i18n
being present but empty — i18n.t() returns the key unchanged, which every copy
of this pattern already treated as "not found".

Verified by _qa-cycle/02-builder-cycle/_chk-c4-optional-t-equivalence.mjs: 18
assertions across all three host states, with a branch-for-branch equivalence
check against the old hand-written implementation and two negative controls. It
extracts and executes the module source, because useOptionalT imports vue-i18n
and an i18n index that calls import.meta.glob, which plain Node cannot run.
2026-09-26 22:06:57 +00:00
Doxa Maps
c2987e8134 PinHint shows only while its map is on screen
It gated on a map canvas EXISTING. A composite (pane-wrapper) keeps the map pane mounted while the reader
scrolls to another pane, so on UPG-100's phone view the pill was clamped into view over the poster pane,
where there is no map to tap (Driver, README picture; traced by upg100-plugin and repo).

An IntersectionObserver on the map box now decides: under a fifth of the map in view hides the pill, and
scrolling back shows it again. Only the reader's close button dismisses it for the visit, as before. Without
IntersectionObserver the old behaviour stands.
2026-09-26 22:06:57 +00:00
Doxa Maps
2449098a68 The legend mirrors in Arabic: logical insets instead of left/right
The shared legend was placed with physical left/right, while the map controls use inset-inline-end. In
Arabic the controls mirrored to the left, onto a legend that stayed left: on Simple Research, Prayer Progress
and UPG-100 (rega's RTL survey at 52eeb0db; traced by upg100-plugin and repo).

SemanticTreeLegend: the panel, its reopen button, the tree-guide hairlines and the export menu use
inset-inline-start/end; the closed panel slides off to the right and its collapse arrow flips under
:dir(rtl) (additive overrides, as VerticalTabsLayout does). LegendDesktop, LegendMobile, LegendRows,
LegendTools and LegendProgressBar: the card, reopen, detail close/collapse buttons, the name gutters that
clear them, indents and spacing are logical; LegendTools re-centres under RTL. LTR renders the same.

check:legend-logical-insets (with :self-test, both in gates:post) fails on any physical left/right in the
legend components unless it is symmetric, an auto reset, or allowed with a reason.
2026-09-26 22:06:57 +00:00
Doxa Maps
29f4ab7097 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 22:04:34 +00:00
Doxa Maps
d17950086a Rebuild my-upg-100-list from the merged source, and re-pin its three leaves
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The six public/ files the merge left holding main's side are regenerated here
rather than merged, because a bundle is an OUTPUT: the only correct content is
whatever `BUNDLE=my-upg-100-list vite build` emits from the merged source, and
any hand-reconciled middle version is wrong by construction even when it looks
plausible in a diff.

fix:sri then rewrote seven stale hashes across five files. That summary is NOT
the evidence: the three leaf integrity attributes were re-derived independently
here by recomputing sha384 over the emitted bundle and comparing to what each
HTML claims — 3 recomputed, 0 mismatches. This is the check that was skipped
when three consecutive commits shipped leaf HTML pinning a hash for a bundle
that no longer existed; the browser refused the script and every one of those
pages was blank, while the build's own output had reported success.
2026-09-26 21:46:07 +00:00
Doxa Maps
175ba52cf3 Merge main (studio landing 4) into packet/vertical
Landing 4 carries the nine messages keys this branch's packet and poster
funnel call (4be260a0), so the eleven catalogue conflicts resolve to MAIN'S
side in full: measured first, not assumed — my side added exactly those nine
keys to each of the eleven locales, changed no existing key, and all nine are
byte-identical on main. Whole-file take-theirs therefore loses nothing, which
is the only condition under which taking a whole shared catalogue is safe.

The six public/ conflicts are build artifacts and are NOT hand-merged: they
take main's side here and are regenerated from the merged source in the next
commit. A hand-merged bundle is what produced three consecutive SRI
mismatches, where leaf HTML pinned a hash for a bundle that no longer existed
and the browser refused the script — a blank page in 6/6 runs.
2026-09-26 21:45:06 +00:00
Doxa Maps
78b4b81e1f Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-maps-spa
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
plugin-studio
pplr-data-maps
regions-dashboard
research-dashboard
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-26 18:50:12 +00:00
Doxa Maps
6aa4cb2ed8 The selection packet names the territories in the order the reader picked them
rega measured it (NEEDS-EYES row 45, main 52eeb0db): selecting Laos, then West
Africa, then Europe produced a packet headed "West Africa + Laos + Europe".

selectedLists() was `allLists().filter((l) => want.has(l.id))` — it used the Set
only for MEMBERSHIP and returned the lists in clusters.json order, discarding the
order the reader chose. west-africa is catalogue index 0 and laos is index 4,
which reproduces rega's heading exactly from the data alone.

The ordering information was never missing: selectedTerritories is documented at
:186 as "an ORDERED list … (insertion order)". The function simply threw it away.

IT MATTERS BECAUSE THE LABEL'S WHOLE JOB IS TO NAME WHAT THEY CHOSE. The comment
on selectionAsList says the cover should "name the territories they actually chose
rather than inventing a title for their selection" — and silently reordering a
reader's own choices is inventing a title, just quietly enough that nobody notices
until someone measures it.

Now maps the selected ids through a byId lookup, preserving insertion order.
Verified against rega's exact case: old -> "West Africa + Laos + Europe",
new -> "Laos + West Africa + Europe".
Checked all three consumers of selectedLists() before changing it: the only
index access is lists[0], and it is guarded by `lists.length === 1`, where order
cannot matter. BUNDLE=my-upg-100-list vite build clean.
2026-09-26 18:43:38 +00:00
Doxa Maps
c58db47ac3 "Arabic" is 23 ISO codes, not 22
rega counted 23 while measuring the language link, and was right. My 22 came
from a cruder comma-split grouping of the labels; the language tier's actual
rule has three arms — exact, "base," prefix, " base" suffix — and selects 23.

Only the prose was ever wrong. Every computed number in the gate and in the
product came from the real rule and has always been 23; these are two comments
explaining it, in two files, one word each. I did not correct them at the time
because the branch carrying them was already in studio's queue, and rewriting a
branch somebody is landing is a worse habit than a stale comment.

A comment that states a measurement is a claim like any other, and this is one a
reader cannot check without redoing the measurement.
2026-09-26 18:38:20 +00:00
Doxa Maps
e35a86253c useSelectionBus: its header no longer promises a shim deletion and a D1 that have both landed
Comment only. The research-dashboard shim is deleted and multi-value filterSet (D1) is in; the header
said both were still to come.
2026-09-26 18:38:19 +00:00
Doxa Maps
4f3a954b9a Gate 9 was accusing a working map: ask the engine, not the DOM
rega's gates:post run reported this gate failing its mapbox half — "NO map
constructed", 21 api.mapbox.com requests, 0 bars. Two theories followed and both
were wrong, mine included. rega then MEASURED the leaf on both token rungs: 1
Mapbox map, 2 GL canvases, 3 visible search inputs, 0 page errors, via the host
global AND via ?tk=. The product was never at fault. This gate's detection was.

THE MAP PROBE WALKED THE DOM FOR A CANVAS. That is a chain of assumptions — every
shadow root open, the canvas carrying the class I expected, existing by the time
I looked — and when any link breaks the probe reports ABSENCE, which is
indistinguishable from a broken product. It now hooks the engine's own
Map.prototype.setStyle through both globals, which cannot be wrong about the DOM
because it never looks at it. Both globals, because the leaf loads only the
engine its provider asks for; the property is defined with a setter so the hook
survives the engine script loading afterwards, and the whole thing is wrapped so
a hook can never be the reason a page fails. The canvas walk stays as a second
opinion, so an engine that stops routing through setStyle degrades to the old
behaviour rather than accusing the product again.

THE BAR SELECTOR MATCHED ONE CLASS. input.mapboxgl-ctrl-geocoder--input, and
nothing else — so it reported 0 bars on a page carrying three visible search
inputs. Whether the miss was the class or the shadow walk cannot be told from
here, and that is exactly the problem: a selector narrow enough that one wrong
assumption yields ZERO will accuse the product of being broken when it is only
wrong about a name. It now counts what a reader would call a search box.

Both probes are borrowed from rega's tokrung.mjs, which measured this leaf
correctly while this gate did not. Where a positive control exists that measures
correctly, copy the control.

IT STILL MEASURES BOTH TOKEN RUNGS, per studio. The ?tk= theory turned out to be
wrong, but the two cases are worth keeping on their own merit: the host global is
the rung an embedding partner reaches for, and if the two ever diverge the gate
now says which one failed instead of leaving a channel switch to look like a fix.
Rejected api.mapbox.com responses are recorded too, so "requests but no map" can
be told apart from a refused key — a 401 says the key, not the code.

THE LESSON, recorded in the header because this is the second time in two days
that a probe of mine accused a working product: a measurement that yields ZERO
when its own assumption is wrong will always blame the thing it is measuring.

Source only; this commit edits one gate and changes no product code. I cannot run
it — Playwright is rega's.
2026-09-26 18:38:19 +00:00
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
4be260a0c0 messages: the nine strings the UPG 100 packet and poster funnel needs, in 11 locales
packetFailed, selectionPacketFailed, posterOneTerritory, selectionPosterFailed, mapUnavailable, copied,
copyFailed, engineNotLoaded, engineNotLoadedDetail. Wording and translations are upg100-plugin's
(packet/vertical 0114ec45), added before its code lands: a key missing from every locale, English
included, renders as the raw key and warns nowhere. Additions only; no existing key changes.
2026-09-26 18:38:19 +00:00
Doxa Maps
32792c0fb3 Click a province, get its row in the table
The brief's third requirement at the grain the province work added: clicking a place
filters the table to it. Same split as the country and language links — a filter when
the tables are already beneath the map, a link when they are not.

The province key is a NAME, not a code: geoBoundaries' shapeName is what the table
joins on. So it is matched with diacritics folded, because a link that only works for
whoever copied it off the map is not an address — ?province=Arunachal Pradesh finds
"Arunāchal Pradesh". An unknown province matches nothing and says 0, rather than
falling back to every row.

The embed gate grew two arms for it and the first one failed for a reason that was
mine: it sliced the page text to 300 characters and then asked whether the province
name was in it. The row was on screen, 400 characters down, and the gate called the
product broken. A slice belongs in the message, never in the predicate.
2026-09-26 18:35:46 +00:00
Doxa Maps
fa1ab52a0a Drop the two RTL rules the library now owns, keep the three it does not
studio's c79aae95 converts BOTH physical insets in SemanticTreeLegend to logical
ones — :619 .stl-panel and :925 .stl-reopen are now `inset-inline-start: 8px` — so
the panel and its reopen button mirror for EVERY profile rather than just this one.
My overrides for those two are deleted.

Two mechanisms doing one job is worse than either: the next reader cannot tell
which is load-bearing, and deleting the wrong one looks safe right up until an
Arabic page is opened.

THREE RULES REMAIN, and each is something c79aae95 demonstrably does not do —
verified by reading it rather than trusting the summary:
  · .upg-detail-panel is THIS file's own CSS (:449), not a legend class. The
    library cannot reach it, and it must swap or it collides with the
    now-mirrored legend on the opposite side.
  · direction:rtl on the panel's rows, and unicode-bidi:isolate on the counts.
    c79aae95 has ZERO occurrences of direction:rtl or unicode-bidi. It mirrors the
    panel's POSITION; these set its CONTENT direction — a different property. repo
    reported BOTH symptoms ("the panel is on the left" AND "the rows stay LTR"),
    so dropping these would fix the first and silently restore the second.

⚠ THE SECOND PAIR MAY YET BE REDUNDANT, and the comment says so rather than
implying they are settled. studio's argument is that dir on .profile-loader
(ProfileLoader.vue:282) inherits into the legend, since `direction` is inherited
and crosses the shadow boundary. Plausible, and they declined to add a library
rule without a measurement — correctly. rega will read the rows on Simple Research
and Prayer Progress, which carry NO override, after landing 5. If they read RTL
there, these two go too.

Verified after the cut: 3 [dir=rtl] rules in the built bundle, and the specificity
that matters still holds — `[dir=rtl] .stl-panel .stl-count` is (0,3,0) against
`[dir=rtl] .stl-panel *` at (0,2,0), so the universal direction:rtl does not
override the digit isolation. Had that inverted, numbers in the Arabic panel would
reorder and the fix would look correct while being wrong exactly where a reader
checks. BUNDLE=my-upg-100-list vite build clean.
2026-09-26 18:33:15 +00:00
Doxa Maps
91ea64f856 Rebuild cities-internet, and regenerate the tree
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 18:27:03 +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
acc1ebbdc8 A comment that warned about a hazard someone else fixed
The template told a future reader not to mount library ApproxPinNote.vue because it
resolved its catalogue without the guard this leaf needs on hosts that never
installed vue-i18n. C4 wave 2 fixed exactly that, so the warning pointed at a
component that is now safe — and the next reader might have duplicated wording to
avoid a hazard that no longer existed.

The decision is unchanged and the real reason is now written down: the component
carries its own inline 11px and #5b6470 while every other note in this sheet is
.gm-note at 14px inheriting the sheet's ink, so mounting it would render one note in
a different size and an off-palette grey. The wording is shared either way — both
read pins.* from the same catalogue.

It also cost regions-plugin a measurement: their bare-call gate counted the comment's
mention of the i18n composable as a third call site, because a JavaScript stripper
does not know an HTML comment. The rewrite does not name the composable.
2026-09-26 18:25:32 +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
998a75352d The Arabic legend mirrors to the right instead of under the controls
Driver-reported via repo from the README's Arabic picture: under ?lang=ar the
legend panel stayed on the LEFT and the map controls sat on top of its top-left
corner, with the rows still reading left-to-right.

★ THE CAUSE IS ONE COMPONENT BEING RTL-AWARE AND ITS NEIGHBOUR NOT:
    MapToolbar.vue:42           inset-inline-end: 3px       LOGICAL — mirrors to the left under rtl
    SemanticTreeLegend.vue:619  position:absolute; left:8px PHYSICAL — ignores dir entirely
So the toolbar moves left, the panel never leaves, and they land on top of each
other. The overlap is not a z-index or a spacing bug; it is two coordinate systems
disagreeing about which edge "start" means.

Fixed here, scoped to [dir="rtl"] so the LTR layout is untouched — ProfileLoader
binds dir on the .profile-loader wrapper from RTL_LOCALES, so it activates exactly
when the document says so:
  · the legend mirrors to the right;
  · the detail panel swaps to the left — it is the legend's mirror image (top-RIGHT
    in LTR), so without this the two would simply trade places and collide on the
    other side;
  · the panel's rows get direction:rtl, because dir on a wrapper does not reach
    content that sets its own direction — that is why the picture shows a panel
    which mirrors while its contents stay LTR;
  · counts and percentages get unicode-bidi:isolate, since a run that STARTS with a
    digit is reordered by the bidi algorithm inside an RTL line. Same fix the
    packet's scripture references needed.

⚠ THIS IS A WORKAROUND FOR A LIBRARY DEFECT AND THE COMMENT SAYS SO RATHER THAN
PAPERING OVER IT. The real fix is one property in SemanticTreeLegend —
`left:8px` -> `inset-inline-start:8px` — which would mirror for every profile
automatically. That file is library/, so under rule 5B it is studio's and it is
reported. Until they land it, every OTHER profile with an RTL locale has this same
collision; this override fixes only upg-100-list.

NOT FIXED HERE, deliberately: the phone "Tap a coloured dot" hint over the poster
view. PinHint.vue:27 self-gates on "a map <canvas> exists", which on the composite
is true but irrelevant — pane-wrapper.js:42 stacks panes with position:absolute
inset:0, not display:none, so the map pane is mounted and laid out while the
reader is on Prayer Resources. The gate asks whether a canvas EXISTS when the
property it needs is whether the map view is SHOWING. library/, so studio's.

BUNDLE=my-upg-100-list vite build clean.
2026-09-26 17:57:33 +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
52eeb0db5c Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: all-people-groups
big-picture
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-maps-spa
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
people-groups-grid
plugin-studio
pplr-data-maps
regions-dashboard
research-dashboard
research-map-plus-grid
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-26 17:43:35 +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
0114ec45cb Merge landing 2 and rebuild against the 50 library files it moved
studio's landing 2 (1d569edd) changed 50 files under library/, so the rebuild was
required — checked with `git diff --name-only 85a41726 1d569edd | grep '^library/'`
rather than assumed, which is the test that decides this every merge and has come
back both empty and non-empty on different rounds.

The only conflict was the built bundle, a derived artifact: resolved to main's
version and then regenerated from the merged source, because picking a side of a
generated file is meaningless. Zero source conflicts.

VERIFIED BY HASHING, not by fix:sri's summary: upg-100-list, upg-100-mfe and
poster-wall all MATCH the rebuilt bundle, whole-tree sweep 57 integrity attributes
with 0 mismatches.

AND VERIFIED MY OWN WORK SURVIVED, because a 50-file library merge is exactly where
a plugin change quietly disappears: flashRef present, messages.engineNotLoaded
wired, tokenUrl still forwarded in the composite, the new gate intact.
The gate is the better witness than a grep, and it now reports what it should:
1 offender, gospel-resources/resources-map.vue:1490, and NONE in my plugin. Before
this merge it named mine too, because ade12e4c was unlanded. check:i18n-parity PASS.
2026-09-26 17:30:33 +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
eaecc62047 gospel: keys for the fold's summary control and the Deaf panel's opening sentence
gospel-plugin's ruling on the typed-in English in resources-map (from the
universal-props re-measure): two of the strings are debt, the rest are
deliberate English-only explainer prose.
- gospel.guide.howNumbersMade 'How these numbers are made': the <summary> of
  the fold, a CONTROL. A reader who cannot read it cannot tell what is behind it.
- gospel.guide.isSignLanguage 'This is a sign language.': the opening sentence
  of the Deaf panel, the one that stops a reader taking eBible's silence as no
  Scripture for 150 sign languages.
Both in all 11 locales, in the existing guide group next to the other
translated BLUF lines. gospel-plugin wires them (one line each). i18n-parity
and i18n-never-translated pass.
2026-09-26 17:28:56 +00:00