Commit graph

1216 commits

Author SHA1 Message Date
doxa-maps-88655c70
17931ed031 PPLR data maps: every view loads under MapLibre, and the legend has accessible names
Under provider=maplibre (the keyless gallery) four of the seven map views never
finished loading. They built `new mapboxgl.NavigationControl` in the load
handler, and that global does not exist under MapLibre, so they sat on
"Loading…". Popups and the pin marker had the same fault. Every such call now
goes through glFor(map), the namespace of the engine that built the map. The
views' styles and the Dark/Satellite/Light switch now use registry names (ink,
hybrid, paper), which are the same Mapbox styles under Mapbox and OpenFreeMap
under MapLibre.

The search box loads its geocoder by engine, as GeocoderComponent does. Under
MapLibre it is MaplibreGeocoder over this map's own local index, so the keyless
gallery makes no request to api.mapbox.com at all.

The legend's row caret, clear chip and collapse/reopen buttons carry accessible
names and aria-expanded from legend.a11y.* and aria.* (optional i18n, English
fallback), the keys the library legend uses. check:shared-reached now counts
pplr-data-maps as carrying legend-a11y-keys.

window.__pplrMap() reports what the active view's map did (engine, load fired,
style layers, this bundle's data sources), so "loaded" is asserted, not inferred.

Measured on a served built tree, host page on another origin: all ten views
loaded under MapLibre and under Mapbox (load fired, style layers and data
sources present, canvas not blank, 0 errors), with 0 api.mapbox.com requests
under MapLibre. The same check on the parent build fails four map views under
MapLibre. A control with no provider makes 13 Mapbox requests.
2026-09-25 09:57:17 +00:00
doxa-maps-88655c70
3cd3e7a55f Rebuild the Research Gallery and Prayer Walk Planner against merged main 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-25 07:56:00 +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
7d0b0e763e PPLR data maps: ten research views of the PPLR people-group data join Experimental
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The PPLR-validate-data maps (religion, ROP tree, language families, semantic
trees, PG name locales, ROP sunburst, all pins, cross-system ID rosetta,
translation coverage, index) shipped only as a frozen 339 KB IIFE on the old
dx-life research page, registering <doxa-map> and colliding with doxa-simple-map.
They now build from source as plugin-designs/pplr-data-maps: one monolithic
<pplr-data-maps> with a view switcher. The Research Gallery discovers it into
Experimental.

Data: build-data.mjs derives data/pplr from the source's 78 MB: 7.3 MB of pins
without the 17 locale names, which load from pg-names.json only when a pin's
detail card opens, plus a precomputed coverage table. The tree stages it inside
itself; the web-root mirror is ignored like the other data/ mirrors.

Measured on a served built tree in catalog layout: the leaf page, all ten views
script-embedded on another origin, and the Gallery tab each render with 0 page
errors and 0 failed requests. Coverage shades 241 of 251 countries.
2026-09-25 07:50:10 +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
145e3c0df8 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed 2026-09-25 07:40:09 +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
833670a16b Rebuild the Prayer Walk Planner against merged main and regenerate the tree 2026-09-25 07:40:08 +00:00
doxa-maps-88655c70
a9a44c3eb1 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
2026-09-25 07:38:11 +00:00
doxa-maps-88655c70
430c06d978 Browser gates: two slots with a memory floor, not one mutex
The single rendezvous made every agent's browser proof wait behind one chrome —
the queue sat 10-19 deep for hours. Exclusivity was never the real constraint:
MEMORY is, at roughly 300-500MB per headless chrome. So the rendezvous becomes a
counting semaphore.

internal/browser-slot.sh takes the first free slot with flock -n, in a fixed
order, and queues on slot 1 only when all are busy. Before taking any slot it
requires MemAvailable above a floor, because a free slot on an out-of-memory box
is an invitation to be killed mid-measurement — which then reads as a flaky gate
rather than as the machine it actually was. Default 2 slots, 2500MB floor, both
overridable.

It keeps the exit-code contract that the last commit established: 75 is
EX_TEMPFAIL, "not measured", distinct from 0 and from any gate's own 1. The
runner already reports 75 as ⊘ and excludes it from the failure count.

CONTROLS, all fired:
  two takers      both run concurrently
  a third         waits, and takes a slot the moment one frees
  short wait      exits 75 rather than 1, so a full queue is never a red gate
  floor above RAM exits 75 immediately, naming the MemAvailable it saw
  inner exit 3    passes through as 3 — the wrapper never rewrites a verdict
  through a wave  two browser gates, 3.5s wall for 3.9s of work: overlapping

The legacy single lock is deliberately no longer consulted: taking it alongside
the slots would collapse two slots back into the one queue this replaces. Agents'
own wrappers should call internal/browser-slot.sh so everyone shares one counter
— and it lives in the repo precisely because the old shared path carried a name
check:no-codename forbids us to ship.
2026-09-25 07:37:26 +00:00
doxa-maps-88655c70
9b99e7fa9b README: retake all-people-groups (new light map) and the research map from their latest builds
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 07:24:37 +00:00
doxa-maps-88655c70
c096f35023 README: retake the UPG-100 shots after its phone fix (b197055b): one poster per row on a phone again 2026-09-25 07:24:37 +00:00
doxa-maps-88655c70
a78eb4428a README: retake the three prayer/engagement/adoption preset shots from the latest doxa-simple-map build 2026-09-25 07:24:36 +00:00
doxa-maps-88655c70
0c8244aca1 README: retake the UPG-100 shots after its map resize (89bd9c5d)
Shot from the rebuilt bundle at 89bd9c5d, after its owner marked the resize
done, with the wait-for-loading fix: the map shows every region, all 27 zones
and all 2,107 pins; the composite and phone shots show the new banner cards.
The mobile dropdown no longer claims "one poster per row", which the phone
layout no longer is.
2026-09-25 07:24:36 +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
b60b66a16e Regenerate the gallery index after taking 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 07:20:44 +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
a47b232122 Remove the Vision Card's old fake stats from the manifest and the gallery
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
33bd7ca3 stripped the frontier instance's typed-in stats (1,910 / 2.1B /
0.1%) by text search for '"stats":[', which missed the manifest because
it is pretty-printed ('"stats": ['). The gallery, regenerated from that
manifest, carried them back. Removed by parsing the manifest as JSON,
then regenerated the gallery. No old figure is left in the card's pages,
the manifest, the staging page or the gallery; check:sri passes.
2026-09-25 07:19:23 +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
1486cbb1b9 Regenerate the gallery index after taking 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 07:14:09 +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
066b9b0cec Gate runner: a busy lock is unmeasured, not a failing gate
Adding the second rendezvous made browser gates queue against every agent's
probes — and then report the queue as a product defect. MEASURED in the first
wave after that change: five browser gates "failed" at exactly 1200064ms, which
is the flock -w 1200 timeout to the millisecond. flock exits 1 on timeout, the
same code a failing gate uses, so `verify`, check:tabs-localized,
check:packet-browser, check:provider-switch and check:composite-embed-knobs all
printed ✗ having never run.

That is the oldest trap in this repo wearing new clothes: a signal that cannot
distinguish "did not run" from "found something". It is the reason gates grade by
exit code rather than by grepping output, and I reintroduced it by adding a lock
without giving its timeout a code of its own.

Both locks now pass -E 75 (EX_TEMPFAIL, distinct from 0 and 1), and the runner
treats that code as UNMEASURED: ⊘ rather than ✗, excluded from the failure count,
with the reason printed next to it. The wave still exits 0, because a lock it
could not get says nothing about the product.

CONTROLS, on isolated lock files so live contention could not confuse them:
  nested success        → 0
  inner command fails   → 1   (not swallowed into 75)
  lock held by another  → 75  → ⊘ "could not take the browser lock within Ns"
and the wave's own exit stays 0 in that last case.

Also fixed the line that disagreed with its own summary: a lock-busy step printed
✗ on its row while the footer counted it as skipped.
2026-09-25 07:11:08 +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
28fd8aae70 README: Isaiah 40:3 in bold, matching the public repo (be968a0)
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 06:40:23 +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
d77efea253 Merge commit '961d9811db428fd995e7fe4c70b8bb95ab832987' into bundler-1/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-25 06:33:06 +00:00
doxa-maps-88655c70
8e5addd9d5 locale gate: read the function it names, not the gap between two landmarks
It sliced library/i18n/index.js from `export function detectLocale` to `export
function createAppI18n` and evaluated whatever lay between. That is a promise
about the ORDER OF A FILE the gate does not own, and it has now been broken twice
the same way: someone exports a helper between those two lines, the slice carries
a bare `export` into new Function(), and the gate dies with "Unexpected token
'export'" — not failing the product, just refusing to start.

Both times the helper was mine. The first fix was to move my helper elsewhere,
which repaired the symptom and left the trap armed for the next person; this time
it caught me again from main while the corrected file sat unlanded in my branch.

Now it extracts detectLocale by matching its own braces and strips any surviving
export keyword, so neighbours are irrelevant. If the function is ever renamed the
gate FAILS by name rather than passing quietly, because a harness that cannot
find its subject has stopped testing anything.

CONTROLS, both fired: adding an exported neighbour between the old landmarks —
the exact crash — now passes; poisoning detectLocale to return 'zz' fails with
0/7 resolution cases. Restored, green.
2026-09-25 06:33:04 +00:00
doxa-maps-88655c70
20acc242ff Regenerate after merging main 2026-09-25 06:32:40 +00:00
doxa-maps-88655c70
961d9811db 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 06:32:31 +00:00
doxa-maps-88655c70
6be1395d1f Cell C has a gate now: the composite must filter, not navigate
The claim worth testing about the composed dashboard is not that it renders. It
is that it keeps the reader INSIDE it. Standalone, "Every resource in
<language>" is a link to the tables page; composed, it must be a button that
filters in place, because navigating away throws out the map the reader just
positioned and the group they just found. Same words, two affordances, one right
in each context.

Six assertions, each a way the composite can look fine and be broken: the
control is a <button> not an <a>, the URL does not change, the table's row count
goes DOWN, it does not go to zero, the pin count survives, and nothing 4xx'd or
threw.

It picks its subject from the live source rather than by position, and that is
not a detail. The first run opened pin 0 — Abai Sungai, a group with nothing
found — so the filter correctly produced an empty table and the check failed a
working composite. The test was wrong, not the product. It now opens a tier-2
pin, so "narrowed but not emptied" is a question the subject can answer.

Measured: 2,107 pins and 50 rows before, then Telugu narrows the tables to 13
with the map intact and the URL unchanged.
2026-09-25 06:32:02 +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
f0b1836682 Regenerate the capability exemplars after the merge
The derived table drifted from the manifest when main's bundles arrived — two
rows moved and one fell off the end. It is generated, so the fix is to regenerate
it rather than to hand-edit: 33 rows now match what the build computed.
2026-09-25 06:27:29 +00:00
doxa-maps-88655c70
8caf2dde90 Rebuild Cities with Internet and Prayer Progress on the merged 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 06:14:53 +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
cb6ee50e7a check:shared-reached: a bundle that never compiles the changed code is not "missing" the fix
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
ensureEngineCSS is only called by initializeMap. A bundle that imports
pieces of useMapInstance (ensureEngineScript, engineGlobal…) but never
initializeMap has ensureEngineCSS tree-shaken away, so the guard can
never appear in it and the race cannot happen there. The check counted
those bundles as missing the fix. upg100 caught it on my-upg-100-list.

The map-unmount-guard entry now also declares the unguarded shape. A
bundle is missing the fix only if it still compiles ensureEngineCSS
without the guard. With neither shape it is reported as not affected.
Entries without a staleRe behave exactly as before.

On today's tree: 7 bundles carry it, 2 are really stale
(doxa-research-map, research-dashboard), and 3 are not affected
(cities-internet, gospel-resources, my-upg-100-list). Before this, all
three were reported as stale.
2026-09-25 06:11:17 +00:00
doxa-maps-88655c70
ce1bce582a Gate runner: one rendezvous for everyone, and a load ceiling that opens
TWO LOCKS, FIXED ORDER. Browser gates queued on a product-named lock while every
agent's hand-run probe queued on a differently-named one, so gates and probes
were not serialised against each other at all — two queues, one machine, and the
dogpile the lock exists to prevent. The runner now takes BOTH, always product
first: flock can exec flock, so nesting is free, and a fixed order means two
processes can never take them in opposite orders and deadlock. Either file alone
still serialises everything that uses it.

The repo cannot simply adopt the other name: it carries an internal code name and
check:no-codename refuses to let a repo we are handing over contain one. Taking
both paths is how the rendezvous becomes shared without the repo spelling a word
it must not ship. VERIFIED in ps while a wave held both, another agent's probe
was waiting on the same file.

THE LOAD CEILING WAS UNREACHABLE, so the guard only ever cost time. It was set at
12 when the risk was seven agents starting chrome at once — but the FLOCK is what
prevents that; the load check is belt-and-braces on a belt. Measured today with
several agents working: 1-min load sits at 36-50 all day, so every browser gate
waited out its full 900s cap and ran anyway. That is a 15-minute tax per wave for
a guard that never opens, and its real effect is that people stop running browser
gates. Now cpus().length * 5 (40 here), scaled because 40 means something
different on 8 cores than on 64, with a 120s cap. Both still override by env.

Also: check:stale-sweep-claim advertised --poison inside an inline comment it can
never receive; the flag gets its own entry and the parent stops advertising it.

gates:pre 7/7, gates:post 42/44 with the two known precondition skips.
2026-09-25 06:10:56 +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
e7638747f8 README: retake the Research Gallery shot from the latest doxa-maps-spa build
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:56:22 +00:00
doxa-maps-88655c70
40750a153e Screenshots wait for the maps to finish loading
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
readme-shots captured on a fixed timer once the page's elements existed, so
maps were caught mid-load: the Big Picture globe came out as a bare white ball
and the UPG-100 map without its resources. Every request is now tracked, and
the capture waits until nothing has been in flight for 4 s (capped at 60 s,
with a warning if the cap is hit), then 1.5 s for the last tiles to paint.

Retaken with the new wait and checked by eye: Big Picture (country shading and
people-group dots on the globe), the research map, the three prayer /
engagement / adoption presets, all people groups, the Plugin Studio and the
iframe host. The UPG-100 shots are deliberately NOT retaken yet: its owner is
still resizing that map.
2026-09-25 05:52:09 +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