Commit graph

506 commits

Author SHA1 Message Date
doxa-maps-88655c70
79dd00a12c Merge main: no source conflicts, artifacts rebuilt rather than merged
Every conflict was a build artifact — another agent had pushed a full rebuild of
all 23 bundles. Minified .js and baked-SRI leaf pages are generated files: merging
their text is meaningless and can produce a bundle that matches neither side, so
they are resolved by taking one side and then rebuilding from the merged source.

The incoming diff carries 68 source files, among them library/composables/
useMapInstance.js and the dashboard/gospel catalogues in 10 locales, so the
rebuild is all 23 bundles and not only the ones I touched — regenerating the tree
without rebuilding has shipped a stale .js here before.
2026-09-25 08:42:13 +00:00
doxa-maps-88655c70
0adf6c3021 Take main
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-25 08:40:42 +00:00
doxa-maps-88655c70
8313f766b6 Bridge candidates as evidence, and no spoken bridge for a Deaf community
The curated bridge covers 168 of Doxa's 369 with nothing, because it comes from
a curated subset rather than from the data. A candidate IS computable for the
rest: the largest language in their country carrying both Scripture and the film.

WHAT I COULD NOT COMPUTE, AND THEREFORE DO NOT CLAIM. Ranking those candidates
needs a notion of "is this a lingua franca", and neither available proxy
survives contact. By GROUP COUNT, Oman picks Swahili — two groups — for an
Arabic-speaking people. By SHARE OF PINNED POPULATION, a 10% floor REFUSES Urdu
in Pakistan at 7.3%, the national language, because few Pakistani groups are
pinned as Urdu speakers. That is the same bad-denominator mistake this map
already fixed on the choropleth. So no strength is invented: the candidate ships
with its group count and its share, under column names that say "candidate",
and a reader who knows Pakistan can see Urdu at 7.3% and know what to do with it.
A score would have thrown it away on their behalf.

AND THE ERROR CAUGHT BEFORE SHIPPING. The candidate logic was offering 89 Deaf
groups a SPOKEN language — Deaf Afghans to Turkmen, Deaf Arubans to Dutch. That
is not a weak bridge, it is not a bridge, and a mobiliser acting on it would be
sent to do something that cannot reach the people. This build already refuses to
search GRN for sign languages because GRN publishes audio; the candidate logic
had inherited the gap. Now: 92 Deaf groups are offered none, and their gap is
named as the access problem it is.

Of the 369 with nothing: 168 curated bridge, 107 computed candidate, 92 Deaf,
2 with neither.
2026-09-25 08:40:25 +00:00
doxa-maps-88655c70
3d4cd4004e Plugin studio: host attributes stay on the host; map-load measurement probe
inheritAttrs:false on the studio root, so a host's attributes (style,
class, id) stay on the <plugin-studio> element instead of also landing
on the layout inside it: the trap that made the Prayer Walk Planner
76px tall in the gallery. Layout verified unchanged in four hosts on the
built bundle: its own page (shell 1280x900, body 836), a bare partner
snippet (85vh default), a host that sets its own inline height (the
style stays on the host; the shell root has no style attribute), and
the app-shell mounter from BUG-EMBEDDED-PLUGIN-STUDIO-BLACK-SCREEN,
display:block forced on the host: body 656px, rail and tree rendered.
That bug no longer reproduces on the dx-maps side.

internal/measure-map-load.mjs: per production map, the cold, throttled
waterfall (bundle, engine script, style, data, basemap up, visible
tiles loaded) on a served built tree, from the leaf page or, with
--embed, from a partner page that pastes only the embed snippet. It
showed that Vue is already inlined in every bundle and that the engine
waits on the bundle only on partner pages.
2026-09-25 08:34:52 +00:00
doxa-maps-88655c70
afe04bc835 Take main
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-25 08:27:09 +00:00
doxa-maps-88655c70
efb19637fd Merge main into bundler-2/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/doxa-maps/dashboard-plus-grid/dashboard-plus-grid.js
#	public/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/default/index.html
#	public/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 08:26:59 +00:00
doxa-maps-88655c70
01d768fdb6 Put the way in next to the gap: bridge languages in the Doxa export
The access table could say "nothing found" and stop. For 168 of Doxa's 369
that is not the whole truth: a language already spoken widely in their country
has Scripture and the film. Afitti in Sudan has nothing of their own; Sudanese
Arabic, spoken by 65 groups around them, has both. That is the difference
between no way in and a way in that is not their heart language, and it is the
next action a mobiliser can take — which is why it belongs in the EXPORT and not
only in the map's sheet, where it has been sitting unexportable.

Four columns: the bridge language, its ISO code, the strength, and how many
groups in that country speak it. Keyed by code|country, because the same
language bridges differently in different places.

IT CLOSES NO GAP AND THE DEFINITION SAYS SO. A bridge is a way in, not a
translation. These 168 are still counted in "nothing found", because they still
have nothing in the language they think in, and the first believers among them
still cannot be trained from a text that does not exist.

The other 201 have nothing AND no bridge. That number is worth having on its own:
it is the isolated set, where neither their own language nor anything spoken
around them carries the gospel.

Verified end to end: the CSV exports 2,107 rows carrying Bridge language, Bridge
ISO, Bridge strength and Groups speaking it, and the Afitti row reads Arabic,
Sudanese.
2026-09-25 08:26:52 +00:00
doxa-maps-88655c70
e67d718603 Merge main into bundler-2/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 08:22:13 +00:00
doxa-maps-88655c70
cc09fd6184 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 08:21:21 +00:00
doxa-maps-88655c70
4fb8176b00 Shape Arabic, Hebrew and Persian map labels on MapLibre maps too
library/maps/rtl-text.js installed the RTL text plugin for MAPBOX only
(window.mapboxgl, from api.mapbox.com). So every MapLibre map, i.e. every
keyless embed, drew RTL place names as disconnected, reversed glyphs.
Measured on the Prayer Walk Planner's printed Arabic sheet.

Under it was a second gap. Bundle entries call the installer at LOAD
time, before useMapInstance has lazily injected a MapLibre engine, so even
an engine-aware installer found nothing to install into.

- rtl-text.js is engine-aware. Mapbox is unchanged (same URL, same lazy
  call). MapLibre gets the same plugin package from jsDelivr, keyless and
  not api.mapbox.com, so keyless pages still make zero Mapbox requests.
  New ensureRtlTextPlugin(gl) waits until it is LOADED, for callers that
  capture a single frame (print).
- useMapInstance installs it the moment an engine global exists, the one
  point every map passes through.
- The planner drops its private copy for the shared helper.

Measured: a print map of downtown Cairo (MapLibre, keyless) shows shaped
Arabic street and place names (شارع التحرير, مسجد الطباخ …); plugin
status "loaded", fetched from jsDelivr. check:gallery-embed passes js,
iframe, neighbour and linked (14 tabs); :maplibre passes;
check:prayer-walk-planner passes.

The shared change reaches all 11 bundles that compile initializeMap, and
all are rebuilt here. check:shared-reached gains an rtl-text-maplibre
marker (10/10 consumers carry it; cities-internet, gospel-resources and
my-upg-100-list boot maps another way and are not affected).

Also: the new Experimental bundle pplr-data-maps loads the Mapbox geocoder
under maplibre (pplrData.js:38). It is an owned, printed MAPBOX_KNOWN entry
routed to its owner; delete it in their fix commit.
2026-09-25 08:20:12 +00:00
doxa-maps-88655c70
7f700935c5 Simple Research + Big Picture: the host's style stops at the box, and the last English goes
TWO FIXES THAT BOTH SHOW UP ONLY WHEN THESE PLUGINS ARE SOMEBODY ELSE'S GUEST.

1. ATTRIBUTE FALLTHROUGH. These SFCs mount AS custom elements, so a style the
   host puts on the tag lands on the component's own root element. MEASURED with
   a host style of "display:flex; position:relative; height:100%": that exact
   string appeared verbatim on the inner root div of people-groups-grid,
   stack-demo, research-map-plus-grid and dashboard-plus-grid. The gallery sets
   one on every tab, and the same fallthrough collapsed the Prayer Walk Planner
   to 76px. inheritAttrs:false on each root — a host styling the TAG is styling
   the box, not our internals.
   AFTER: the inner root carries no inline style on any of the four, and each
   still renders full height (2997-4010px in the harness), so the guard did not
   become the collapse it prevents.
   Worth knowing for the next probe: shadowRoot.firstElementChild is a <style>
   node — Vue injects styles first — so a probe that trusts it measures nothing
   and reports "no fallthrough, nothing to fix". Take the first non-style child.

2. THE RESIDUAL CHROME the in-gallery re-measure found: the dashboard title,
   Align Dashboard and its tooltip, World, Vertical Legend, groups in scope, the
   per-row aria templates (Filter by / Remove filter / groups), and the map
   drawer's aria label. Catalogued in all 11 locales and read through tStatic,
   reusing existing wording where the catalogue already had the word.

The aria TEMPLATES matter more than their size suggests: they repeat per row, so
one untranslated template is dozens of English strings in a screen-reader's ear.
2026-09-25 08:19:56 +00:00
doxa-maps-88655c70
e941d642ce Take main
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 08:18:26 +00:00
doxa-maps-88655c70
77f24ae5ab The last two profiles: chrome translated, prose kept English on purpose
resources-list and charts turned out to be almost entirely explanatory prose.
Their actual chrome is four strings — two page titles, the loading line and
Close — and those are now in all eleven locales. The blanket dir=ltr is off both
roots, so Arabic gets its layout, and each English prose block declares its own
direction where it sits.

That completes option (b) across all five gospel profiles. What stays English is
what carries doctrinal and statistical precision: which pools were searched, what
"nothing found" does and does not claim, why Open Bible Stories is not
Scripture. A machine translation of those would read as authoritative while
being wrong, and one translated line in each locale says so in the reader's own
language.

Verified on a served built tree whose identity I proved first — the Russian title
appears once served and once on disk. en/ar/ru on both leaves: heading
translated, 0 clipped, dir=rtl only for Arabic, no page errors and nothing 4xx.
2026-09-25 08:17:54 +00:00
doxa-maps-88655c70
0fee065212 Composites forward provider, so a MapLibre partner gets MapLibre
A composite shell copies a whitelist of keys from its own profile-config down to
each map it nests. `provider` was not on any of the three lists, so a nested pane
read it as undefined and fell back to Mapbox: a partner who asked for MapLibre,
and who holds no Mapbox token, got Mapbox maps in every inner pane. Same shape as
the missing `urlSync` here before it -- an inner map cannot honour a key the shell
never passed down.

The report named country-posters-mfe. All three composites had it: regions-mfe and
upg-100-mfe were missing `provider` too, so fixing only the one named would have
left two thirds of the defect in place.

Verification is a probe, not an inspection, and it needs a control. "0 requests to
api.mapbox.com" passes trivially on a page where nothing mounted -- the most likely
way this measurement goes wrong -- so each composite is loaded twice: once with
provider=maplibre expecting zero, once with provider omitted expecting more than
zero. The second run is what proves the first measured anything; a run that
renders no canvas is reported UNPROVEN rather than passing.

It also cannot be measured on a leaf page. Every leaf loads its map peer in its
own <head> (regions-mfe/index.html line 13 is a hardcoded api.mapbox.com script),
so on a leaf zero mapbox requests is unreachable whatever provider says. The
question only has meaning on a host that picks the peer itself, which is what a
partner embedding the .js actually does.
2026-09-25 08:12:20 +00:00
doxa-maps-88655c70
edb66d2805 Correct the record: there is no RTL column-clipping bug
The last commit recorded "in RTL the leftmost data column clips against the
edge" as known debt. Measured rather than eyeballed, that is wrong. Counting
cells falling outside the table holder: English and Arabic are IDENTICAL — 13 of
20 columns off-screen in both, scrollWidth 3146 against clientWidth 1400 either
way.

A twenty-column table is wider than 1,400px, and RTL mirrors which edge the
overflow sits on. What I saw in the Arabic screenshot was English's right-edge
overflow appearing on the left, which is what mirroring means. I read a picture
instead of measuring, and filed the result as debt — which would have sent the
next person chasing a defect that does not exist.

Whether twenty columns is right for a 1,400px screen is a real question. It is a
design question, and it is the same question in English.
2026-09-25 07:59:22 +00:00
doxa-maps-88655c70
9eb65faa99 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 07:55:26 +00:00
doxa-maps-88655c70
2043384435 Merge remote-tracking branch 'origin/main' into exp-shepherd/pplr
# Conflicts:
#	internal/generate-tree.mjs
2026-09-25 07:40:35 +00:00
doxa-maps-88655c70
ba057df9c9 PPLR maps: country polygons came back after PPLR renamed its fields
PPLR renamed the aCountry fields upstream (GENCalpha3 -> GENCAlpha3, CtryNm -> Name,
ctryNmDisp -> DisplayName, ISOalpha2 -> ISOAlpha2). The query asked for the old
names, ArcGIS answered HTTP 200 with { error }, and the loop read that as "no
features". Every view that draws country polygons silently lost them. The
query now asks for the new names and gives each feature the old ones back, and an
{ error } body throws.

Paging also stopped after one page: f=geojson carries exceededTransferLimit under
data.properties, so only 200 of 251 countries loaded. A full page now means ask
for more.

Coverage: the status no longer claims a 6,000-row sample, and a __pplrCoverage()
handle reports how many polygons matched a coverage row.
2026-09-25 07:40:08 +00:00
doxa-maps-88655c70
b32795aa0e Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	package.json
#	public/doxa-maps/index.html
2026-09-25 07:22:18 +00:00
doxa-maps-88655c70
b78e7d1b52 Prayer Walk Planner: fill the page, and print a real walking map
The Driver: "the prayer walk planner is still a very tiny map, not
expanding the full page", and the printed map was a ~10px sliver.

Tiny map. Measured: the leaf filled (1003x739), but inside the Research
Gallery the map was 747x76 on desktop and 390x0 on a phone. The planner
mounts its SFC as the custom-element root (that is what puts its client
API on the element). So Vue's attribute fallthrough copied the gallery's
inline host style onto the root .pwp, and that inline display:block beat
.pwp{display:flex}. inheritAttrs:false keeps host attributes on the host.
The map now fills at 1280x800 and 390x844, in the leaf and in the gallery.
The gallery gets the same guard: its .vtl had survived only through
display:grid !important.

Sliver print. Print used to snapshot the LIVE map, so the page got that
map's on-screen size and shape. Each zone now renders in its own
temporary MapLibre map:
- page-shaped (1200x1344 at pixelRatio 2, a 2400x2688 image, ~320 dpi on
  the 7.5x8.4in frame)
- keyless OpenFreeMap liberty, with streets and building footprints
- fitted to the zone, with the zone outlined on top
- destroyed once its pixels are taken, and the reader's map never moves
One zone = one letter page, with the map filling it.

Localized. The planner read no language at all. It now follows
language → profile-config locale → ?lang → <html lang>, in all 11
locales (private src/strings.js): toolbar, list, status, default zone
names, and the printed sheet (lang/dir set, so Arabic prints RTL).
Street and place labels prefer the OSM name:<lang>. Two Arabic defects
were found by reading the printed sheet, and fixed:
- coordinates were bidi-scrambled → isolated LTR
- map labels were unshaped glyphs → the RTL text plugin is installed
  for MapLibre, from jsDelivr, and awaited before the print frame
  (library/maps/rtl-text.js only does it for Mapbox)

renderZoneMap(id) joins the client API: the print PNG without the dialog.

New gate check:prayer-walk-planner:
- 4 size cells (leaf/gallery × desktop/phone)
- a real zone in Balat, Istanbul renders a >=2000px non-blank print map
- a Spanish toolbar
- 0 page errors
Both gates now prove the served tree is this checkout's (manifest equals
disk) before measuring, and exit 75 NOT MEASURED otherwise.
check:gallery-embed's Big Picture link now asserts that the grid shows
exactly the facet's own count.
2026-09-25 07:21:42 +00:00
doxa-maps-88655c70
0c7d324b5f Take main
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 07:20:35 +00:00
doxa-maps-88655c70
db317925d7 Translate the tables profile's chrome, and name what is still English
All twelve tab labels, the page title and the filter chip come from the
catalogue in eleven locales. The blanket dir=ltr is gone from this profile's
root, so Arabic gets the RTL layout it should have; the English prose blocks —
the subtitle, the hero sentence, the footnote and the two caveats in the chip —
declare dir=ltr where they sit, the same split the map profile uses.

The translator is named tr(), not t(), and that is load-bearing: the tab loop
binds v-for="t in TABS", so a t() here would be shadowed inside the one block
that needs it most and every label would render as whatever a tab object
stringifies to.

Verified on a served built tree whose identity I proved first — the Russian
string I had just added appears once in the SERVED bundle and once on disk, and
the port owner's cwd is this worktree. en/ar/zh/ru/hi: heading and tabs
translated, 50 rows rendering, 0 clipped chrome, dir=rtl only for Arabic.

STILL ENGLISH, and not claimed otherwise. The table's own column headers and
title come from the DATA FILE's columns[].title and meta.title, not from this
component, so translating them means translating the table schemas — a separate
job. And in RTL the leftmost data column clips against the edge; my clip check
only measured the chrome, so it did not catch it. Both are named here rather
than left for someone to discover.
2026-09-25 07:20:16 +00:00
doxa-maps-88655c70
33bd7ca387 All People Groups loads fast on a light map; the Vision Card becomes a true funnel from data
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
ALL PEOPLE GROUPS. The Driver: "not even loading, takes a very long
time", and its DOXA pins were dark on a dark map.
- It downloaded the 7.5 MB people-groups-slim.json (22 columns) to draw
  ten fields. It now fetches people-groups-points.json: the same rows in
  column order, repeated strings in dictionaries, coordinates at 3
  decimals. 0.74 MB, generated deterministically by
  library/scripts/people-groups/build-points.mjs and staged into the tree
  by generate-tree. It falls back to the old file on an older tree.
- Pins no longer wait for the live prayer API. Every pin draws by GSEC
  from the file, and the DOXA overlay recolours when the API answers. The
  DOXA count reads "…" until then, not 0.
- Named style 'paper' (Mapbox light-v11 / MapLibre positron) instead of
  'ink'. The four categories keep their meaning; yellow and green are a
  step darker, and pins get a thin dark ring, so they read on near-white.
- Its words are in all 11 locales (allPeopleGroups.json), with a guarded
  useI18n and values passed to vue-i18n so {n} is not blanked.
Measured on a served built tree, 3 fresh runs each, median time to first
rendered pins: 8.3 s before, 5.2 s after, on localhost. This box renders
GL in software, so most of the remaining time is map start-up; the 10x
smaller download matters more on a real connection.

DOXA VISION CARD. The Driver: "the data doesn't make sense … super false
information". The card rotated four typed-in figures with no source.
It is now a proportional funnel, every number computed from data by
library/scripts/people-groups/build-vision-funnel.mjs:
  12,379 people groups   (IMB rows: one group in one country)
   7,341 unreached       (IMB LostnessPriority 0-2, < 2% evangelical)
   4,754 frontier        (LostnessPriority 0, < 0.1% evangelical)
   2,107 DOXA groups     (the DOXA prayer list, snapshot saved in data/;
                          1,745 of them frontier)
The first three nest; DOXA groups do not all sit inside frontier, so the
card says how many do. The rules are on the card ("How these are
counted") and in HOW-THE-NUMBERS-ARE-COUNTED.md, which also explains why
the total is not the 16-17 thousand of Joshua Project's count. Material
light, all 11 locales, right-to-left in Arabic, sources and dates shown.
The old fake stats are removed from the instances file, the frontier
page, the manifest, the staging page and the gallery.

CHECKS.
- check:vision-card-numbers recomputes the funnel independently (own CSV
  reader, the published rules) and compares with vision-funnel.json and,
  with --url, the rendered bars. Negative-tested: a corrupted count exits
  1. On the built tree: bars 12,379 / 7,341 / 4,754 / 2,107 and "1,745 of
  them are frontier", all matching.
- check:map-embed-cells measures PRODUCTION-MATRIX cells B and C for the
  seven gallery maps on a foreign origin; C proves the host's token is
  used by handing the host a distinct token and checking every Mapbox
  request. 14 of 14 pass.
2026-09-25 07:18:30 +00:00
doxa-maps-88655c70
ad81d16511 Merge main into bundler-2/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/doxa-maps/index.html
2026-09-25 07:15:27 +00:00
doxa-maps-88655c70
5516c157bb Merge main into bundler-2/work
# Conflicts:
#	public/doxa-maps/country-posters/countries-map/index.html
#	public/doxa-maps/country-posters/country-poster-wall/index.html
#	public/doxa-maps/country-posters/country-posters-mfe/index.html
#	public/doxa-maps/country-posters/country-posters.js
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 07:14:11 +00:00
doxa-maps-88655c70
01baaf4475 Take main
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 07:14:01 +00:00
doxa-maps-88655c70
b197055bf0 Print a UPG-100 list straight from the map legend
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The Driver, as a first-time user: "I select one and it gives me print
options at the top right of the legend ... that's the only way that map
would be usable."

The affordance already existed - SemanticTreeLegend renders an export
menu gated on a selection, the plugin passes :export-enabled, and
@export was wired. What the handler did with it was the problem: the
packet branch delegated to the shared composable, and the poster branch
hand-rolled an <a download> of posters/<id>.webp. That is wrong twice.
It downloads a file silently, which was ruled out on 2026-09-23 ("no
silent file download as the default action"), and it has no fallback, so
for the lists whose .webp is not shipped it saved a 404.

The wall had a correct poster print the whole time, private to
PosterWall.vue, so this surface could not reach it. It moves into
usePosterPrints.printPoster and both surfaces now call the one copy -
what the card asked for: "reuse the existing three print actions rather
than growing a fourth implementation." Two guards moved with it, each
learned the hard way: a THROWN fetch is a miss rather than an error (or
the rasterise fallback is skipped entirely), and a blank raster fails
loudly (an SVG in an <img> is an isolated document, so a missing
stylesheet paints black with no error).

Also in this commit, both from measurement rather than review:

THE STICKER OVERLAP IS NOW A CONSTANT, NOT HALF A VARIABLE. Centring the
print row on the banner edge put half its height on the banner - and
that height is content-dependent: 68px in en, 54px in zh, taller when ro
wraps at 390px. Measured ro/390px: 27 of 27 stickers on the map by 4px
while en and zh were clean at the same padding. No fixed padding can
clear half of a variable. The row's top is now pinned --tape-overlap
above the edge and extra height grows downward into the row gap, so the
amount on the banner is identical in all 11 locales at every width.
15 width x locale combinations now pass with map-covered 0.

THE PER-ROW PREFERENCE IS CLAMPED TO WHAT THE VIEWPORT HOLDS. It renders
as an inline grid-template-columns, and inline beats the stylesheet, so
the Driver's default of 2 overrode minmax(210px,1fr) at every width -
including 390px, where two columns give a ~162px card. Reported from a
390x844 phone shot. The preference is for desktop density, not an
instruction to overflow a phone; it survives, clamped by the same 210px
minimum the stylesheet already uses.

THE ROW GAP WAS CLEARING A DISTANCE NOTHING OCCUPIES. 88px was tuned
when the print row hung fully below its card. It now straddles the
banner edge, so the same shot showed a ~180px empty band between rows.
56px still clears a two-line row in the long-label locales.

And the composite shell re-dispatches doxa:select-zone, so a list chosen
in the poster wall is ISOLATED on the map rather than merely scrolled
to. The map half was already built and listening on its own element; the
shell owed it one re-dispatch and nothing here re-implements selection.
2026-09-25 07:10:23 +00:00
doxa-maps-88655c70
b84beaed9e Wire the corrected GRN sweep, and translate the three lines that make the map usable
THE RECHECK, reported at its real size. Re-asking all 293 Doxa codes the old
title catch-all had defaulted to "none" recovered EIGHT: the rule was wrong
2.7% of the time, not catastrophically. That moves 12 Doxa people groups out of
the gap — 381 to 369 — and each now carries a working Words of Life link. Modest,
and worth saying plainly rather than dressing up: the bug was real, its blast
radius was small, and the fix is that the classifier measures instead of
defaulting.

THE THREE LINES. rega's in-gallery count was right that these are chrome, not
prose: what a dot is, what "none found" does not mean, and that a country is
tappable. They are what a reader needs to use the map AT ALL, so leaving them
English made it unusable rather than merely untranslated. Translated in all
eleven; the long explanatory paragraphs around them stay English and are pinned
dir=ltr where they sit, which is option (b) unchanged.
2026-09-25 07:06:33 +00:00
doxa-maps-88655c70
89bd9c5d79 Poster wall: the banner gets stickers, top and bottom
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The card is a BANNER that displays a map, the way a label displays a
design on a bottle. A banner taped up gets a sticker at each end, and
each sticker straddles the edge it tapes: part on the banner's own white
space, part overhanging. Neither one touches the map.

MEASURED at 1440px before this change:
  banner 240-426, map 241-425  ->  1px of white space at each end
  header sticker  35px above the banner's top edge, -1px below
  footer sticker   1px above the bottom edge, 67px below

So the map filled the banner edge to edge and there was no banner
surface for a sticker to sit on. The footer row could only hang off into
space, which is what shipped, and every assertion in the gate was green
through all of it.

The banner now reserves white space at both ends (--banner-pad-top /
--banner-pad-bottom on .main), and each sticker centres on its edge:

  header sticker  17px on the banner / 17px above it
  footer sticker  35px on the banner / 33px overhanging
  map covered by either: 0px

The print row also moved INSIDE .main. It was a sibling, so top:100%
resolved against the CARD, 27px past the banner - the re-parenting and
07d3341c's centring transform are one fix, and neither works alone.
That commit's per-row localStorage preference is preserved.

Card Deck is no longer a peer button with a 162px <select> beside it.
The stock picker is a disclosure: press Card Deck, choose a size, and
that choice prints. A native dropdown in the row collides with the host
page's own dropdowns, which is why it is buttons rather than a <select>.

Print-button labels WRAP instead of clipping. With the per-row default
now 2, a 390px viewport halves the card to ~170px, where Romanian's
"Pachet de cartonașe" cannot share a line: measured 54 of 81 controls
clipped at ro/390px, en and zh clean. The ruling on this wall is that a
long label wraps; nowrap was forcing a clip.

Also fixes the transparent strip above a pinned section header. Sticky
pins to the scroll container's PADDING box, so with .wall's 22px
padding-top the header rested 22px down and posters scrolled visibly
through the gap. A ::before extends the header's opaque fill upward by
exactly that padding, read from the same var so the two cannot drift.
Measured by sampling the strip's pixels: 1 colour, 100% header
background, where before it was 1338-2831 colours of poster artwork.

The MFE shell's own chrome is translated, in all 11 locales. Measured
with ?lang=ar: "Select View", "Views", "Prayer Resources" and "Map"
rendered English while the poster wall's buttons inside the panes
translated correctly, because each pane installs i18n and the wrapper
around them never did - the chrome a reader meets FIRST was the
untranslated part. The wall's own heading, tag and intro go through t()
for the same reason, with the <strong> inside the translated string so
each language emphasises its own words.

The gate grew four assertions, each mutation-tested to fail on the
defect it was written for: off-frame (row floated off the banner),
head-off (header sticker not straddling), map-covered (a sticker on the
poster), and selects (a native dropdown back in the row).

Includes a rebuild of the four PosterWall consumers, which also picks up
studio's shared useMapInstance unmount guard and exp-shepherd's
translated LegendMobile labels.
2026-09-25 06:41:56 +00:00
doxa-maps-88655c70
e1d847028d PPLR data maps (WIP): move the ten PPLR research views into plugin-designs
Not for main yet. All ten views mount on the dev server with real data and no
page errors. Left: the coverage view's status still says 'sample of 6,000',
its choropleth needs checking, and the bundle is not built or tree-verified.
2026-09-25 06:33:49 +00:00
doxa-maps-88655c70
cd24bda42a Merge commit '8caf2dde90f44b26dea67df511b343b582ae52bc' into bundler-1/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 06:31:59 +00:00
doxa-maps-88655c70
d61819b553 Forward urlSync to the nested panes: the guard was unreachable without it
rega measured that a Regions nested in the Research Gallery STILL rewrote the
host's #anchor and tab title after my urlSync guard landed, and named the cause
exactly: regions-mfe.vue INHERITED_PAGE_KEYS lacked `urlSync`.

They were right, and the lesson is mine. Guarding writeUrl, syncTitle and
popstate in region-dashboards.vue was necessary and NOT sufficient: a nested
pane has no leaf page of its own, so it only ever sees what this composite
forwards. Without the key the panes read cfg.urlSync as undefined and the guard
never fired. A guard you cannot reach is not a guard, and I had verified only
the half I could see from my own file.

It is the same lesson this file already records for cap-4 asset paths: page
scoped config must be forwarded explicitly. The existing filter keeps `false`,
which is the value that matters here -- it rejects undefined, null and '' only.

VERIFIED ON THE WHOLE PATH, composite -> pane -> guard, on a foreign origin
(tree on 127.0.0.1, host page on localhost) with urlSync:false and a host
#anchor: 868 nodes and 2 nested pane roots rendered, and the host's
#host-anchor-must-survive and its tab title both came through untouched.

The probe asserts the embed RENDERED before it believes the URL result, and
that guard earned itself twice: a first run used <regions-mfe> as a tag, which
is a profile of <regions-dashboard> and not an element, so nothing mounted --
7 inert nodes, with the host URL "untouched" only because no code had run.
2026-09-25 06:31:36 +00:00
doxa-maps-88655c70
93dc5d14a2 Merge remote-tracking branch 'origin/main' into exp-shepherd/cities-dots
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 06:13:08 +00:00
doxa-maps-88655c70
9a18dee1c2 Merge main into bundler-2/work
# Conflicts:
#	package.json
#	public/doxa-maps/country-posters/countries-map/index.html
#	public/doxa-maps/country-posters/country-poster-wall/index.html
#	public/doxa-maps/country-posters/country-posters-mfe/index.html
#	public/doxa-maps/country-posters/country-posters.js
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 06:12:10 +00:00
doxa-maps-88655c70
03b838e16d Simple Research: one filter state, so the two halves move together
The composite showed a map and a grid that ignored each other: filtering the grid
to Africa moved its own count 2,107 → 315 while the map beside it kept every pin.
The composite now owns ONE filter state; the children report theirs upward as DOM
events and accept ours downward as attributes, so each stays independently
embeddable — a child mounted alone hears nothing and is handed nothing.

MEASURED on a served built tree, region facet:
  grid → map   2,107 → 315, and the map's own state follows (externalRegion and
               selectedRegion both 'africa'); clearing returns both
  map → grid   the map's map-filtered contract → the grid's list 2,107 → 315;
               clearing returns it
  standalone   grid alone 24 rows / Total 2,107 / 0 errors; map alone canvas 1 /
               0 errors — unchanged
  MUTATION     disabling the composite's push leaves the grid filtering itself
               (315) while the map stays empty, and kills the reverse direction:
               the check fails when the link is broken

Four things this cost, each of which first looked like it worked:

1. StackLayout applies `attrs` ONCE at create — its documented contract. The
   first version recomputed `sections`; the composite adopted the filter and the
   map's attribute in the DOM never changed. A computed nobody re-reads is not a
   channel, so the state is now PUSHED onto the mounted children.
2. The echo cancelled itself. Composite pushes → the grid re-filters → it emits
   groups-filtered with activeFilters still EMPTY → the composite adopts that as
   "the reader cleared it" and wipes what it just applied. Both handlers were
   correct; the round trip undid itself in under a second. A push is not a user
   action, so the push window ignores children.
3. The grid ignored host filters BY DESIGN when its own dropdowns are visible:
   activeFilters picks local OR props, never both. Right for the desktop
   composite, which hides them; silently wrong here. A host facet now seeds the
   LOCAL state, so the map's selection appears as a chip in the grid's own bar
   and the reader can clear it there.
4. That seeding called lbl(), which is local to another computed — "lbl is not
   defined", watcher dead, attribute set, list unchanged. Visible only after the
   probe was widened to console.error. A silent watcher failure looks exactly
   like a feature that was never wired.

SCOPE, DELIBERATE: region only. The map filters religion and language by the
canonical strings its own data carries; the grid filters them by slugs from a
different table. Linking those means inventing a mapping, and a wrong link is
worse than an honest gap.

The map also gains a verification handle (window.__researchMapFilterState), the
same contract as __rdashMap and __grMap: a linked filter is a STATE claim, and
every DOM proxy for it was either tab-specific or a selector guess.
2026-09-25 06:10:43 +00:00
doxa-maps-88655c70
b4a0387495 Translate the chrome that was English in all eleven locales
Measured under ?lang=ru on both composites: every Latin-script string left on the
page was ours. The map above was fully translated; underneath it the grid, the
map's own side menu and filter panel, and the whole research dashboard were
English in Arabic, Chinese, Hindi, Russian and everything else.

Three surfaces, three reasons, none of them a missing translation:

  the grid          read labels from a host-supplied `t` object that nothing ever
                    filled, with an English literal at each call site as fallback
  side menu +
  filter panel      plain string literals
  dashboard         labels declared as literals in dashboard.config.js — facet
                    rail, chart titles, KPI strip, table headers, segmented
                    control, search placeholder

New catalogues for all 11 locales (grid, controls, dashboard), REUSING the
catalogue's existing wording wherever the word already existed — Регион, Блок
родства, Население, Вовлечён, Графики — so two panels never name one thing two
ways, and nothing is coined that the project already has a word for.

Read through a new plugin-free tStatic() rather than useI18n, because these
components are also mounted by entries that install no i18n plugin. An unguarded
useI18n() throws and a thrown setup() removes the subtree with no build error —
the defect that had just blanked people-groups-grid everywhere (6603dc17).

★ THE CONFIG LABELS ARE GETTERS, AND A SPREAD WOULD HAVE LOOKED IDENTICAL.
dashboard.config.js is imported once at load; the locale is only known later. My
first version spread a getter object, and a spread COPIES THE RESOLVED VALUE, not
the accessor — every label would have frozen at whichever locale loaded first,
and it would have looked perfect in English. `get label() {…}` resolves on each
read.

MEASURED after, served built tree, identity checked first: Big Picture ru/ar and
Simple Research ru/zh have ZERO Latin-script chrome strings. What remains is
Mapbox's licence attribution (never translate), country and people-group names
(data), and 'UUPGs' — which every locale file deliberately keeps as the acronym.

Three gates went red on the way, two of them mine and worth recording:
  check:locale-reachable  slices this i18n module between two markers and EVALS
                          the slice; I had put tStatic between them, so the gate
                          hit an `export`. Moved to the end of the file.
  check:library-index     my import+helper above a component's docblock became
                          that component's DERIVED description. Moved below it.
  check:kpi-unit          its parser assumed single-brace entries, so getters made
                          it find ZERO pct KPIs — and it failed rather than
                          printing a green PASS about nothing, which is the gate
                          working. Parse now understands getters AND checks the
                          translated labels, so a '%' reintroduced in any of the
                          11 catalogues fails by name. Both controls fired.
2026-09-25 06:10:27 +00:00
doxa-maps-88655c70
502a4186dc Cities with Internet and the phone legend speak the page's language
Cities with Internet was English end to end: the legend title, the four bands,
the World Bank caveat, the new people-group row and the whole country sheet. Its
chrome now comes from a connectivity catalog area in all eleven locales, following
gospel-resources: i18n is optional, English from the shipped catalog is the
fallback, and carrier and platform names stay as data. The legend and the sheets
are anchored with logical insets, so in Arabic the legend sits opposite the control
rail instead of under the zoom buttons.

LegendMobile hard-coded its column labels ("Prayer Status", "Engagement Status",
…) and titles ("Legend", "Languages", …) in English. They now read
legend.colLabel.* and legend.titleBar.*, which every catalog already translates.
English is byte-identical, so other consumers are unchanged until they rebuild.

Measured in the Research Gallery tab on a served built tree, at 1280 and 390, in
en/ar/zh/ru/hi: Cities shows no English UI string (only the Mapbox and
geoBoundaries attribution); ar is right-to-left with 0 clipped and the legend
clear of the control rail.
2026-09-25 06:05:46 +00:00
doxa-maps-88655c70
405db377d8 Take main
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-25 05:51:48 +00:00
doxa-maps-88655c70
c31df9f349 IBT is the best lead on this map, and recording why it is not a pool
36 Doxa groups carry an IMB "audio Scripture exists" flag while every pool here
says nothing. Asking why rather than shrugging: most are Dagestani and Caucasus
languages — Akhvakh, Andi, Bagvalal, Botlikh, Chamalal — which the
English-centred catalogues simply do not cover. The Institute for Bible
Translation in Moscow does, and their /languages page lists 54+ projects
including every one of those, with an ordinary robots.txt that allows it.

It still cannot be a pool. The page gives a native-script name, a region and a
language family and nothing else: no ISO code, no per-language page, no
publication URL. Joining it means matching "Akhvakh (ашвалӏи мицӏцІи)" to a
code by NAME, which is the guess this build removed two commits ago when Doxa's
own API made it unnecessary — the same guess that would have handed Akha in Laos
one of three sub-varieties.

What it is worth is the other thing the Driver asked for: the people who do the
translation work, so a group with nothing can be handed to a team that already
has the linguistic work and the relationships. For the Caucasus cluster, IBT is
that team, by name. That belongs in a leads table with a human confirming each
code, never auto-joined into the index.
2026-09-25 05:51:26 +00:00
doxa-maps-88655c70
6d59645b37 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 05:48:29 +00:00
doxa-maps-88655c70
3b8e8e21e7 Research Gallery: name the Experimental maps, in every locale
The Experimental folder's rail rows were the one part of the gallery's own
chrome still in English in every locale. They were also machine-titleized
manifest ids, so even English read "Upg Neighbor".

The six Experimental bundles known today now have proper rail names in all
11 locales (Prayer Walk Planner, UPG Neighbor, Key Cities, All People
Groups, Country Posters, Doxa Vision Card). A future bundle that isn't
listed still appears, under its manifest title. Outside English its
English manifest description is replaced by the translated generic
subtitle.

Measured on the served built tree with the folder opened (7 → 12 rows):
translated in it/de/hi/ru/ar, en "UPG Neighbor", and 0 clipped. The rail
clip sweep with the folder open (11 locales × 1400/390) is 22/22 clean.
check:spa-tab-order passes on my own served tree. check:gallery-embed:
js 13, iframe 13, neighbour 13, linked 4.
2026-09-25 05:48:21 +00:00
doxa-maps-88655c70
c980d4e576 The Deaf gap is larger than this map was treating it as, and the route is a person
Cross-checking Doxa's roster against IMB's own access flags: 122 of their 2,107
people groups are DEAF — 17.5 million people — and 92 have nothing in this index.
For 33 of those 92 IMB records that material EXISTS, much of it dated 2021 at
"Selections" or "Stories": Armenian, Bulgarian, Myanmar, Cambodian and Cuban
sign languages among them. That is a coordinated body of work this build cannot
see.

Re-probed all three hosts so nobody spends another tick on them:

  doorinternational.org   the REAL DOOR International. Cloudflare 403 "Just a
                          moment..." on every path INCLUDING robots.txt. A bot
                          shield, and this repo does not circumvent shields — it
                          reached GRN through their own public site rather than
                          around it. Out on access, never on doctrine.
  deafbible.com           still the same 3,894-byte app shell for every URL,
                          /sitemap.xml included. robots allows everything; there
                          is simply nothing served to a fetch. api.deafbible.com
                          answers 525, so their own app's API is down too.
  door.international      NOT theirs. Parked for sale on GoDaddy, and it now
                          publishes an llms.txt saying exactly that. It looked
                          like a lead precisely because it advertises an LLM
                          policy. Their real domain is the first entry above.

This is the largest identified-but-unreachable set on the map. The route is
someone asking them for access, not another agent probing the same three hosts.
2026-09-25 05:46:52 +00:00
doxa-maps-88655c70
8423411757 The GRN sweep was defaulting to "none", not measuring it
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Cross-checking IMB's own access flags against this index turned up 112 Doxa
groups where they say a resource exists and we found nothing. Nineteen of those
were gospel recordings — GRN, a pool this build had already swept — so one of the
two was wrong. Asking 5fish directly about six of them, with a real code and an
impossible one as controls: FOUR have recordings. alk Alak, nos Nisu Eastern,
jle Ngile, lma Limba East, each landing on its own numbered language page.

The fault is here. classify() read the page TITLE and its last rule was a
catch-all: any title matching neither known pattern became "none". That is a
default wearing the clothes of a finding, and it was silently losing every page
whose title was phrased in a way nobody had anticipated.

The landing decides now and the title only names. A published code lands on
/<id>?language=<name>; one they do not carry is swept to /regions — the same
discriminator already proved in band in the verifier and the rot sampler. A
request that errors leaves the code UNDECIDED rather than "none", because
recording a failed request as an absence is how this started.

GRN_RECHECK=1 re-asks the codes the old rule defaulted, and a hit can now take a
code back OUT of the none list — a recheck that could only add would be unable to
correct the thing it exists for. Proved on the six: 4 recovered, 2 confirmed
empty, controls passing.
2026-09-25 05:42:16 +00:00
doxa-maps-88655c70
60347ec0b7 Take main
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-25 05:35:33 +00:00
doxa-maps-88655c70
26cd737587 All 2,107 of Doxa's groups, placed and searchable, from Doxa's own record
The map got coordinates and a language code by matching each group's NAME
against the pin dataset. That left 285 unplaceable and, worse, unsearchable: no
ISO code is no key to look them up by in any pool, so the access table could only
report them as "not searched" — nobody had been able to ask.

Doxa's own per-group endpoint carries both, and their robots.txt disallows
nothing. /api/people-groups/detail/<slug> gives primary_language and latitude and
longitude for every one: Akha to ahk, Akeu to byo, Alxa Mongols to xal, Ajja to
krs — four the name join had failed. So the name matching is not improved, it is
REMOVED. It was always a guess wearing a join's clothes: Akha in Laos sat beside
Akha Jipea, Akha Jaweuy and Akha Piso in the pins, and any pick among them would
have invented a language for a people. The publisher saying which language their
group speaks ends the guessing.

2,107 of 2,107 placed. Zero without a language code. The "not searched" rung is
gone from the access ladder because nothing is unsearched any more.

The gap grows 281 to 381, and that is the point: those 100 groups were always
without the gospel in their language, and the map could not say so because it did
not know what their language was.

The fetcher runs its controls in band both ways before trusting a record — a real
slug must return a language and coordinates, an impossible one must not — so a
site answering 200 with an empty body could not fill this table with silence.
IMB's own access and engagement fields come along beside ours, never merged into
them, because where two sources disagree that disagreement is worth seeing.
2026-09-25 05:35:11 +00:00
doxa-maps-88655c70
5bbbcc61f4 Plugin studio: mount the element the bundle defines, not the one a profile's JSDoc names
A profile's `element` in the manifest is copied from its @element JSDoc,
and that can name a tag nothing registers. country-posters-mfe's says
<country-posters-mfe>, but country-posters' index.js defines only
<country-posters-map>, so the built studio showed "never registered"
on a working plugin (gospel-dashboard had the same shape until
5418a79c). The studio now uses a profile's element only when the
bundle's `elements` (what index.js actually registers) includes it, and
otherwise the bundle's defined element.

Across all 33 manifest profiles this is the only mismatch, and its
published embed snippet already uses the right tag. Verified on the
built tree: country-posters-mfe mounts <country-posters-map> with
content (2,025 elements).
2026-09-25 05:34:16 +00:00
doxa-maps-88655c70
ca79ccb91c Merge remote-tracking branch 'origin/main' into exp-shepherd/cities-dots
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 05:23:30 +00:00
doxa-maps-88655c70
d6496bd87d Rebuild my six bundles to carry the useMapInstance unmount guard (0c1a91cd)
doxa-simple-map, cities-internet, all-people-groups, upg-neighbor,
prayer-walk-planner and demo-map compile useMapInstance in, so 0c1a91cd reaches
them only through a rebuild. check:shared-reached reports all five that use it as
carrying the guard.

cities-internet builds its map directly and never calls ensureEngineCSS, so it gets
its own guard: onMounted stops before new gl.Map if the pane unmounted during its
awaits. Removing the element 150, 400 or 900 ms after mount did not throw on either
build, so this is defensive, not a measured fix.
2026-09-25 05:23:08 +00:00
doxa-maps-88655c70
4cfe8ec5e2 Merge commit 'd989d598f18227e677f762c15c21df27013a57ad' into bundler-1/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 05:16:03 +00:00
doxa-maps-88655c70
4d773d6ee8 Move internal files out of the repo; fix five dead links; a fuller delegation diagram
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Moved out (kept outside the repo, in the team's internal tracker):
- AGENT-BRIEF.md: operating rules for the team's own agents
- documentation/slide-deck-outline.md, documentation/README-longform.md:
  internal pitch material; the README and why.md cover the same ground
- plugin-designs/gospel-resources/SOURCES-CHECKED-AND-REJECTED.md: research log
- .claude/ (an editor status line), .forgejo/ (a CI workflow that never ran),
  _archive/ (a retired staging page; already gitignored but still tracked)
Seven code comments that named these files now describe the rule instead.

Fixed five dead links left from the docs/ -> documentation/ move, including
the one in template-bundle that every new plugin inherits.

The delegation diagram now shows the whole model: the Plugin Studio with the
shared library and the merge queue into main, seven production plugins with
capability badges and the Experimental shelf, one agent per plugin (a roaming
agent for the shelf), the shepherd's oversight ring, a legend, and the five
ways a build ships.
2026-09-25 05:14:10 +00:00