Commit graph

407 commits

Author SHA1 Message Date
doxa-maps-88655c70
cfdbea7f9c Host locale: gate the profile-config contract, forward ?lang= into staging embeds, stop losing gate stderr
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- check:host-locale-contract: every leaf loaded with ONLY profile-config.locale="ar" must draw at
  least half the Arabic the same page draws at ?lang=ar; regions-list is the positive control.
  Fails on this commit for 18 leaves (bundles that never provide appI18n, and every composite).
- check:staging-lang: the staging page at ?lang=ar embeds its leaf with lang=ar and the Gospel map
  renders gospel.mode.gap in Arabic. staging-forge.js (iframe + open link) and
  staging-reference-v2.html now forward ?lang= / ?locale= via withLang(); fails on the parent.
- browser-slot.sh: `exec 9>f 2>/dev/null` redirected the whole script's stderr, and the gate
  exec'd after it, to /dev/null — FAIL lines and "not measuring" messages were being discarded.
  The redirection is now scoped to the open.
- verify-map-paint: the two regions map leaves join the default list (both paint, local and live).
- Vision card reads ?lang= / ?locale= like the rest of the library.
2026-09-25 12:18:26 +00:00
doxa-maps-88655c70
d78d67f197 Merge remote-tracking branch 'origin/main' into exp-shepherd/shelf-gate
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	package.json
2026-09-25 11:48:59 +00:00
doxa-maps-88655c70
07e2e96ec4 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
# Conflicts:
#	package.json
2026-09-25 11:44:59 +00:00
doxa-maps-88655c70
299adaa9e3 A leak gate that had to be corrected twice before it could be trusted
rega's method: a string is a LEAK when the catalogue has a different value for
the locale and the page still shows the English one — a render fault, not a
missing translation. This measures that across four leaves and four locales.

IT TOOK THREE VERSIONS AND THE FIRST TWO WERE WRONG IN INSTRUCTIVE WAYS.

One: it failed with no explanation. process.exit(1) terminates before pending
stderr flushes, so the run returned 1 with every reason line missing. A gate that
cannot say why it failed is barely better than no gate. It sets exitCode now and
lets the process end.

Two: it read the whole shadow root and reported 113 leaks, nearly all false.
"JESUS Film" and "Scripture" appear inside the explanatory paragraphs this bundle
deliberately keeps in English, and short entries like "for" match inside ordinary
sentences. Crying 113 when a handful are real is worse than silence — someone
would have gone and "fixed" strings that were already correct. Every English
prose block here declares itself dir="ltr", so the check now walks the shadow
root and skips those subtrees, and ignores catalogue entries under six
characters.

WHAT IT FINDS, now that it is trustworthy: 70 real leaks, and every one is in
resources-list. resources-map, data-tables and charts are clean in ar, zh, ru and
hi. That is exactly the debt I declared two commits ago — that leaf got its title
translated and nothing else — now measured instead of estimated.

Scope is the leaf on purpose. Six keys were reported leaking in the gallery and
the leaf is clean for all six: the gallery builds its iframe src with no lang
parameter, so the embed never learns the locale and correctly falls back to
English. Embedder defect, not a bundle one, and a check that conflated them would
send someone to translate what is already translated.
2026-09-25 11:43:16 +00:00
doxa-maps-88655c70
ff36d0dfcc Merge remote-tracking branch 'origin/main' into exp-shepherd/shelf-gate
# Conflicts:
#	package.json
#	public/doxa-maps/doxa-maps-spa/manifest.json
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
#	public/doxa-maps/prayer-walk-planner/prayer-walk-planner.js
#	public/doxa-maps/prayer-walk-planner/prayer-walk-planner/index.html
2026-09-25 11:36:00 +00:00
doxa-maps-88655c70
b1e0c7fa61 check:experimental-shelf: every experimental plugin, measured working, and two label fonts that 404'd
The Driver asked for "all experimental plugins working"; this makes it a
measurement. The gate derives the shelf from the Research Gallery's own rules
(manifest minus curated tabs and NOT_EXPERIMENTAL), then checks every bundle on
its leaf page and on its Gallery tab, under mapbox and maplibre. It requires
content inside the plugin's own element, 0 page errors, 0 failed requests, no
loading text, and for each map 'load' fired AND its own GeoJSON data drawn. An
empty source does not count. Maps are found by wrapping the engine's Map class
in an init script, so no bundle changes for it. It serves this checkout on an
OS-assigned port, and self-tests first: a deliberately broken bundle must fail,
or it exits 3.

Prayer Walk Planner's data is the user's own (zones in localStorage), so the
gate seeds one zone and requires it drawn, printing the seed and reason on every
run rather than exempting the bundle.

It found two real defects on the way. Both are fixed here, and both failed the
gate before the fix:
- UPG Neighbor: every label layer asked MapLibre's default fonts (or Mapbox's
  DIN) from OpenFreeMap, which 404s, so no label drew under maplibre. labelFont()
  now picks Noto Sans under MapLibre; Mapbox is unchanged.
- Prayer Walk Planner (always MapLibre): zone titles never drew, for the same
  reason. The label layer now names Noto Sans Regular.

Result: 28/28 PASS across all 7 shelf bundles.
2026-09-25 11:35:18 +00:00
doxa-maps-88655c70
a2be2396b9 README: the Experimental shelf, with Wall of People Groups and the Prayer Walk Planner
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
A new "Experimental shelf" section pictures the two newest shelf plugins, each
shot from its own page after it finished loading: the Wall of People Groups
(all 2,107 groups as photo cards on one zoomable prayer wall, 7fec7426) and the
Prayer Walk Planner (full screen from any embed and a sharp HD print,
30e6e569). The Research Gallery tree now lists all seven shelf plugins
(adds pplr-data-maps and wall-of-people-groups). Shots staled by 92a81ed2 and
later rebuilds are retaken.
2026-09-25 11:22:52 +00:00
doxa-maps-88655c70
4cc4a1284b The last browser scripts refuse to measure a tree or dev server that is not this checkout
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
BUG-BROWSER-GATES-CAN-MEASURE-ANOTHER-WORKTREES-TREE, the remainder. A
static scan of every internal/ script that drives a browser at a local
target found eight with no identity guard (a9550858 and 59f497eb
guarded the rest):
  built tree  check-spa-tab-order -> requireThisTree
  dev server  long-frames-probe, mobile-sheet-probe, theme-flash-probe,
              plugin-gallery-smoke, share-roundtrip-smoke (--plugin-gallery),
              smoke-studio, legend-columns-probe (only when aimed at
              /plugin-designs/; its default target is the DX-life site
              it compares against on purpose)

A dev server has no built manifest, so the new
requireDevServesThisCheckout checks the source it serves. It fetches
three library files with ?raw and compares them byte for byte with
this checkout. Another tree's dev server answers differently or 403s,
and the gate exits 75 (NOT MEASURED). Vite answers a plain fetch with
the file's text and an import-style request with `export default "…"`;
both shapes are accepted, since the first version parsed only the
second and refused this checkout's own server.

Proven: against a dev server running from 319064a1's parent and a
built tree whose manifest differs by one byte, all eight exit 75 and
name the differing file (useMapInstance.js). Against this checkout,
check-spa-tab-order and smoke-studio PASS, and plugin-gallery-smoke
gets past the guard. A re-scan finds no unguarded script left.
2026-09-25 11:10:34 +00:00
doxa-maps-88655c70
6356cc3876 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 10:51:41 +00:00
doxa-maps-88655c70
30e6e5699b Prayer Walk Planner: full screen from any embed, and a print proven sharp by resolution
The Driver asked for full-screen, and for "a print that stays sharp when
zoomed".

Full screen. The planner mounts the shared FullscreenButton (the library's
Fullscreen-API control, with its CSS pseudo-fullscreen fallback for iOS
Safari) as a map overlay. It full-screens the whole planner, toolbar and
map, not just the canvas. Measured from a 520px embed in the Research
Gallery on a foreign page: the planner took 1280x800 and the map grew 459
→ 739px. The label is translated (Arabic "تبديل ملء الشاشة"), because the
planner now keeps the shared vue-i18n instance on its own locale. Before,
Share and Full screen took the PAGE language even when the gallery sent
another.

Print resolution, measured, not asserted. The printed map lays out at
7.53 x 8.4 in on the letter sheet (printable width = 8.5in minus the two
12mm margins; emulateMedia alone does not apply @page margins, which
misread it once as 8.48in). Its image is 2400x2688 px, so 319 x 320 DPI.
A 1:1 crop of the zoomed print was read: small street names (Sancaktar
Yokuşu Sokağı, Baki Dede Sokağı), individual building outlines and
footpaths are crisp, because the map is rendered from vector tiles at
pixelRatio 2, not upscaled from the screen. One PDF page.

check:prayer-walk-planner gains both: full screen from a 520px embed must
take the viewport, and the printed map must measure >= 300 DPI. 10/10
PASS.
2026-09-25 10:51:21 +00:00
doxa-maps-88655c70
cd53582ea4 The shared poster wall's locale is asserted, in locales that can show a leak
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
UPG-100 carried a ❌ for a PosterWall i18n leak: useI18n() said to throw
where the wall mounts, leaving the print buttons English in every
locale on upg-100-mfe, poster-wall and country-poster-wall.

IT DOES NOT REPRODUCE. Measured on the current build, three leaves x
ro/ar/zh, tree identity checked against the served manifest first: zero
English leaks, zero page errors. ro "Listă de rugăciune / Harta afișului
/ Pachet de cartonașe", ar "قائمة الصلاة / خريطة الملصق", zh "禱告名單 /
海报地图", legend "Opțiuni de tipărire" / "خيارات الطباعة" / "列印選項".
The diagnosis does not fit either: this bundle installs i18n per element
via configureApp, and createI18n is legacy:false, so useI18n() resolves
for every descendant including the wall.

THE REAL DEFECT WAS THE MISSING ASSERTION, and it is worse than the bug
would have been: nothing in the repo checked the shared wall's RENDERED
locale, so a ❌ could sit on the scoreboard with nothing able to confirm
or refute it.

★ AND IT CANNOT BE CHECKED IN ENGLISH. The wall's guard HUMANIZES the
key when useI18n() throws - buttons.prayerList becomes "Prayer List" -
which is deliberate, so a bare host shows readable English rather than a
raw key. It also means "the button says Prayer List" is true both when
i18n worked and when it threw. An en-only check passes forever while
every other locale leaks. This gate therefore runs ONLY in non-English
locales, where the translated string differs from the humanized key and
seeing the key IS the defect.

It covers FIVE consumer leaves, not the three the report named:
key-cities and regions-dashboard mount the same shared wall and were
never checked. A gate that copies a report's sampling inherits its
blind spot.

TWO WAYS IT COULD HAVE PASSED WITHOUT CHECKING ANYTHING, both real on
the first run and both now hard skips rather than green ticks: a wall
with no cards renders no chrome, and regions-list rendered 7 cards with
ZERO wall chrome because that profile mounts the wall without the print
row. Skips are named in the output with their reason - a summary that
counts only the green ones reads as full coverage.

MUTATION TESTED by reproducing the reported condition exactly - forcing
the useI18n() guard to throw - rather than corrupting a string: the gate
goes red with 28-33 leaked strings on every measured combination, which
proves both that it detects the defect and that the defect is genuinely
absent from the unmutated build. Restored, 9 combinations PASS, 6
reported as not covered.

No source change: there was nothing to fix.
2026-09-25 10:50:46 +00:00
doxa-maps-88655c70
c77207b53a check:basemap-labels: the basemap's place names follow the reader's language on both engines
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
BUILD-LOCALIZATION-SURVIVES-THE-PROVIDER-SWAP asked for a check; the
behaviour landed in 319064a1 (library/maps/localize-labels.js) without
one. This reads the labels the map actually DRAWS, at ?lang=en and
?lang=ru on the same view, under both providers:
  Mapbox    the Standard basemap's 'place-labels' featureset (a plain
            queryRenderedFeatures cannot see the imported basemap):
            Germany→Германия, Ukraine→Украина, Egypt→Египет… must flip.
  MapLibre  each country symbol's evaluated text-field: under ru it must
            be the feature's name:ru, under en not. English there
            restores OSM's own label, so no English word is expected.
It serves this checkout on an OS-assigned port. --dir points it at
another tree for the negative control.

Negative-tested: doxa-simple-map built from 319064a1's parent (no
localize-labels) and served with --dir FAILS on both engines (Mapbox:
68 country labels, none flip; MapLibre: 0/49 in Russian). This tree
PASSES on both (5 known countries flip on Mapbox; 49/49 on MapLibre,
e.g. United Kingdom → Великобритания). A first version counted the
pre-fix Mapbox run as NOT MEASURED instead of FAIL, since it saw no
English names. "Measured" now means enough country labels were drawn.
2026-09-25 10:48:42 +00:00
doxa-maps-88655c70
c73b775b6d 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 10:19:42 +00:00
doxa-maps-88655c70
59e9695aaa Correct the record: the dead bundle was my merge, not a duplicate import
dx-shepherd's verifier traced it. Merge 52ab6089 from this worktree committed
public/doxa-maps/gospel-resources/gospel-resources.js with 21 conflict markers —
both sides concatenated, 1.13 MB — and that is what threw "Identifier Ge has
already been declared" and killed every gospel leaf.

My diagnosis last commit was wrong. I blamed a module imported twice and
re-routed the import; that change was harmless but it was not the cause. The
REBUILD that came with it is what repaired the file. A plausible cause that a fix
appears to confirm is the easiest kind of wrong answer to believe.

THE ACTUAL CAUSE IS A LANDING SCRIPT I WROTE. On a conflict it resolves the one
file it expects — public/doxa-maps/index.html — and then runs "git add -A
public", which stages every OTHER file under public/ including the ones still
holding markers. One line of convenience turning a conflict git correctly refused
to guess at into a committed artefact.

So: a check that names the real condition. It scans every tracked file for an
unresolved marker, is built from character codes so it can never trip itself, and
carries a poison arm. It runs FIRST in check:gospel-all, because there is no
point measuring anything else about a tree that is mid-merge.

Also verifies the 8 links the GRN recheck recovered, which check:gospel-all
caught as shipped-but-unverified on its very first run — psi, alk, nos, jle, lma
and three more, all now confirmed landing on their own 5fish language pages.
2026-09-25 10:19:07 +00:00
doxa-maps-88655c70
413fa30d19 isMap per profile: a map-less leaf no longer bakes, or preloads, a map engine it never uses
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
generate-tree decided a single leaf's map-ness as "its own .vue
references an engine OR anything under the BUNDLE's src/ does", so a
map-less profile inherited it from a sibling. regions-dashboard/
regions-list shows a list and no map, but src/RegionCountryMap.vue made
it isMap=true. Its leaf loaded the Mapbox engine for nothing, and since
4a6886d2 its embed snippet also preloaded it.

A leaf's map-ness now follows the RELATIVE imports reachable from its
own .vue, bounded to its bundle, so a map several imports deep
(research-dashboard's DashboardMapPanel) still counts. An explicit
boolean isMap in <profile>.meta.json wins. Composites keep their
separate rule, since they do mount their siblings.

Measured: across all 34 profiles exactly one changed (regions-list,
true -> false). 18 map profiles keep isMap and their preload; 0 non-map
snippets carry one. On a served built tree regions-list makes 0 engine
requests and 0 api.mapbox.com requests and renders its list (673
elements, no errors), where before it downloaded the 1.75 MB engine.
regions-mfe still loads the engine and draws its map. The regenerated
files are only the regions-list leaf, the two manifest copies (still
byte-identical), the staging page and the gallery; check:sri passes.
2026-09-25 10:17:33 +00:00
doxa-maps-88655c70
da8bc790b8 check:composite compares bytes, not mtimes: rebuild each composite and diff it with the committed bundle
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The gate compared a child's newest source mtime with the parent
bundle's mtime. A git merge or checkout rewrites sources with
checkout-time mtimes, so rega's publish of beb13da3 stopped on
"doxa-maps-spa / plugin-studio / stack-demo STALE", although a rebuild
reproduced the committed bytes exactly. Two of those parents mount
their children by tag and compile none of them in. It was also
gameable the other way: checking out the parents makes them pass.

It now rebuilds each composite into a temp dir and compares sha256
with the committed bundle. Builds are deterministic (two runs give
identical bytes), so a difference is a real source change the
committed parent predates. The new vite.config.js DOXA_OUT_ROOT
redirects that build away from public/; unset, nothing changes. A bare
sibling-profile entry in composes (gospel-resources' resources-map,
data-tables) now resolves instead of being reported unresolved.

Proven: an edit to people-groups-grid.vue with no rebuild fails
big-picture, which compiles the grid, and leaves plugin-studio and
research-map-plus-grid fresh. Touching every child and library source
with no content change still passes.

On today's main: doxa-maps-spa, plugin-studio and stack-demo (the
false alarm) are fresh. country-posters, dashboard-plus-grid and
regions-dashboard are really stale and need a rebuild. About 2 minutes
for all 10 composites; use --only to narrow.
2026-09-25 10:10:08 +00:00
doxa-maps-88655c70
85bd7ed1e9 README: Simple Research and the Research Gallery join the phone row; stale shots refreshed
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
simpleresearch fixed the phone toast (2221d11b): at 390x844 the "Tap a
coloured dot" prompt now sits above the Progress sheet and the legend reads in
full (checked by eye). The phone row now shows seven maps in two rows of four;
Big Picture keeps its note (desktop by design; on a phone the Research Gallery
opens on Simple Research). Gospel Resources shots retaken after its bundle was
repaired on main (57fecfd0 replaced a bundle committed with merge-conflict
markers in 52ab6089). Other stale shots refreshed from the latest builds.
2026-09-25 10:09:12 +00:00
doxa-maps-88655c70
59f497eb67 Six more browser gates measure THIS build or nothing
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
a9550858 guarded twenty gates; these six were still unguarded. Each
now exits 75 (NOT MEASURED) rather than grading another tree:
- check-legend-export-reachable, check-vision-card-numbers --url:
  requireThisTree on the URL they are given.
- check-embed-basemap-paints: it picks a RANDOM port, not an
  OS-assigned one, so if another agent holds it, its own server fails
  to start and it would measure theirs. It now checks the leaf server's
  identity, stops its own servers and exits 75. Its "a server did not
  come up" path exited 1 and now exits 75.
- check-tabs-localized, check-poster-prints read library/ from
  DOXA_REPO. Pointing that at another checkout now needs
  DOXA_REPO_MEASURE_OTHER=1 (new requireThisRepo helper).
- check-packet-render refuses a packet file inside a DIFFERENT git
  checkout (new requirePathInThisCheckout helper); a file outside any
  checkout is allowed with a note.

Proven by pointing each at a wrong build: a copy of this public/ whose
manifest differs by one byte, served on its own port, and another
worktree for the file and DOXA_REPO gates. All 8 (these 6, plus
shell-scroll and upg-map-data) exit 75. On this tree they measure:
legend-export, vision-card, upg-map-data, tabs-localized and
poster-prints PASS. packet-render gets past the guard and fails on a
non-packet file, as intended. embed-basemap measures and FAILS: "the
page does NOT paint a basemap on its own baked token". That is a
product finding, not the guard, and is reported to its owner.
2026-09-25 10:03:16 +00:00
doxa-maps-88655c70
2a1292b3ad verify-shell-scroll asserts the Driver's ruling: a wheel over the map zooms, below it scrolls
Step 2 asserted that a wheel OVER the map must scroll the page. That
predates the Driver's 2026-09-23 ruling, recorded in
DashboardMapPanel.vue: "wheel OVER the map zooms; wheel BELOW it
scrolls". It kept the gate red on correct behaviour (dx-shepherd ruled
it stale). It now asserts the ruling: a scroll-down wheel over the map
moves the page 0 px, a zoom-in wheel over it raises the zoom, and a
wheel below the map scrolls the page until the grid is on screen. It
locates the map canvas and wheels at its centre and below it, not at
fixed coordinates.

Proven both ways on a served built tree: PASS (over the map 0 px and
zoom 0.50 -> 1.30; below it 2502 px). With SHELL_SCROLL_NEGATIVE=1,
which disables scrollZoom as before the ruling, it FAILS: the page
moved 300 px. It also gains the tree-identity guard (exit 75 on another
build).
2026-09-25 10:03:16 +00:00
doxa-maps-88655c70
be3620ef1c browser-slot.sh: stop overwriting the caller's BASE, and let a waiter take any slot that frees
Two defects, both reproduced before the fix and re-measured after.

1. It assigned BASE for its lock path. A variable the caller exported
   stays exported in bash, so every gate run through the slot saw
   BASE=/tmp/doxa-maps-browser-gate.lock. Seven gates read
   process.env.BASE; check:upg-map-data-painted printed "SKIP — nothing
   served at /tmp/doxa-maps-browser-gate.lock/…" and exited 0. Renamed
   to SLOT_BASE. Proof: with BASE=/x set, a child printed the lock path
   before and /x after.
2. With every slot busy, waiters queued on slot 1 alone, so a slot that
   freed was never taken and two slots behaved as one queue. It now
   polls every slot with flock -n every 2 s until the deadline and
   still exits 75 on timeout. Proof, in a private slot dir with slot 1
   held 12 s and slot 2 freed at ~3 s: a caller waited 11.5 s before and
   4.3 s after. A free slot is still taken at once (66 ms).

check-upg-map-data-painted: its unreachable-server SKIP exited 0 and
its wrong-tree refusal exited 2. Both now exit 75, the repo's "not
measured" code. Its existing tree-identity check is what makes the
:4173 default safe, since another agent's tree there is refused.
2026-09-25 10:03:16 +00:00
doxa-maps-88655c70
6c1c543a92 Take main
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 09:52:42 +00:00
doxa-maps-88655c70
57fecfd0da A gate that asks whether anything is there — and it caught a dead bundle
Wrote this after shipping a page that returned 200 for every asset, reported
zero console errors, and was completely dead: a throw during setup dies inside
the custom-element boundary. Zero errors is not the same as working, so this
does not trust their absence — it looks for PRESENCE, on all five leaves, in
five steps that each name their own failure: the tag is defined, the host is in
the document, a shadowRoot is attached, it has children, it renders text.

Its first real run went red on all five, and it was right. Every gospel leaf was
dead with "Identifier 'Ge' has already been declared" — while the SRI hash
still matched and every request still returned 200. My own doing: four profiles
imported en/gospel.json directly for an English fallback, and library/i18n
already pulls that exact file in through an eager glob, so the IIFE carried two
instances of one module.

The fix is one module: library/i18n now exports EN_MESSAGES and the profiles
read it from there. The duplicate is gone.

Measured after: all five leaves mount with 3,228 to 233,307 characters of
rendered text, verify-gospel-map passes with 2,107 pins and the gap cutting to
370, and --poison confirms the check goes red on a page with no element at all.

The lesson is the gate's whole reason to exist. I had rich assertions about pin
counts and filters and none of them ran on data-tables, charts or the list, so a
bundle-level failure could take out every leaf while the one gate I did run was
pointed elsewhere. Cheap and universal beats rich and narrow for THIS question.
2026-09-25 09:52:25 +00:00
doxa-maps-88655c70
319064a11b Map place names in the reader's language, on both engines
Every map translated its own chrome but left the basemap's country, city
and street names in the style default (English on Mapbox, Latin + native
on OpenFreeMap) in all eleven locales. A reader in Arabic saw an Arabic
legend over an English map.

library/maps/localize-labels.js uses each engine's supported mechanism:
- Mapbox: map.setLanguage(lang). It also localizes the Standard style's
  IMPORTED basemap, which a layer rewrite cannot reach.
- MapLibre: basemap symbol layers point at
  ['coalesce', name:<lang>, name:latin, name]. English restores each
  layer's original expression, re-captured on every style load so a theme
  switch never restores the previous style's labels.
Only layers whose text-field reads an OSM name field are touched. A
plugin's own label layers (zone titles, group names) are never rewritten.

useMapInstance applies it on every style.load and when the reader's
language changes (ProfileLoader's 'lang', else profile-config locale, else
?lang / <html lang>). The Prayer Walk Planner drops its private copy for
the shared one.

Measured on the Prayer Progress map, ar/zh/ru/hi:
- Mapbox getLanguage() == locale in all four.
- MapLibre: 13/14 symbol layers localized, and the plugin's own label
  layer untouched.
Read in the Arabic screenshots: القاهرة, الإسكندرية, السويس, الأردن on
both engines. check:gallery-embed (+ :maplibre) and
check:prayer-walk-planner pass. The shared change reaches all 11 bundles
that compile initializeMap, and all are rebuilt here. check:shared-reached
marker basemap-labels-localized: 10/10 consumers.
2026-09-25 09:46:21 +00:00
doxa-maps-88655c70
8e6fde19ae README: the production maps on a phone
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
A row of phone screenshots (390x844, taken after each map finished loading)
under the production gallery: Regions, UPG-100 List, Prayer Progress, Cities
with Internet and Gospel Resources. Big Picture is desktop-only by ruling, and
Simple Research is left out until its phone layout is finished: its "Tap a
coloured dot" prompt still covers the Progress legend at 390px. The README
says both plainly.
2026-09-25 09:12:16 +00:00
doxa-maps-88655c70
60b3650304 The phone app opens the UPG-100 list, not the plugin studio
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
repo reported it: the Capacitor app launches public/index.html, which
mounts <plugin-studio>. A recipient opening the phone app meets the DEV
STUDIO rather than the map the README promises. Reproduced in the synced
Android assets before touching anything.

RULED (Kingdom-agent, card DECISION-MOBILE-EXAMPLE-IS-UPG100-NOT-SIMPLE-
RESEARCH, done): the ONE mobile-app showcase is the UPG-100 list.

⚠ THE CARD'S OWN BLUF SAYS "Simple Research" AND IS STALE. It predates
that ruling, which is recorded DONE in the same backlog and says Simple
Research "was never mobile-friendly". Building to the card's wording
would have shipped the app the ruling rejects, so this builds to the
ruling and the card is annotated with why.

WHY IT RUNS AFTER cap sync AND WRITES ONLY INTO THE ANDROID ASSETS.
public/index.html has THREE consumers, which capacitor.config.json
documents: Railway/CDN, Capacitor, and local staging. The studio is
CORRECT for the CDN root and for `bun run serve` — only the phone build
wants a different landing page. Rewriting the shared root would fix the
app by breaking the other two. cap sync copies webDir into
assets/public/, so overwriting the COPY changes what the app opens and
leaves the served tree untouched. The studio is kept at studio.html
rather than deleted; a developer with the apk still wants it.

WHY IT COPIES THE BUILT LEAF RATHER THAN HAND-WRITING A HOST PAGE.
The leaf carries an SRI integrity hash. A hand-written page needs its
own correct hash, and a stale one makes the browser REFUSE the script
with no console error: the element never upgrades and the app is a blank
screen. Copying the artifact the build already verified means there is
no second copy of that hash to drift — confirmed, the hash still
matches after the rewrite. Only the relative depth changes, and the
script asserts every rewritten reference resolves to a file that exists
rather than trusting its own regex.

MEASURED by serving the ANDROID assets tree (not public/, which is the
whole point) on a private port, with the app-root marker checked before
any number was read:
  390x844   mounted+upgraded, studio absent, 27 poster cards, 0 4xx, 0 errors
  1280x900  same
check:app-start-page is mutation-tested: restoring the studio page as
the app root makes it exit 1 naming <plugin-studio>.
2026-09-25 09:11:01 +00:00
doxa-maps-88655c70
a7d086619b Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 08:56:25 +00:00
doxa-maps-88655c70
4a6886d2c9 Embed snippets preload the Mapbox engine, so a partner page fetches it alongside the bundle
The card asked for Vue to be bundled inline to make maps faster. It
already is: vite.config.js externalises only jspdf, and every IIFE
carries its Vue runtime. The measurement (internal/measure-map-load.mjs,
cold, 10 Mbps / 60 ms, served built tree) found one serial runtime
fetch before first paint, on partner pages only. A page that pastes the
embed snippet has no leaf page to fetch mapbox-gl.js (~480 KB gz), so the
bundle requested it after it had downloaded, run and mounted. Leaf pages
already load it in parallel.

Map profiles' script snippets now start with a preload for the engine,
at the URL read from useMapInstance.js ENGINES.mapbox.js so the two can
never differ. It has no crossorigin attribute because
ensureEngineScript injects a plain <script>, and a mismatched preload is
not reused. Non-map profiles get no preload. A comment tells a partner
who switches to provider "maplibre" to delete the line.

Partner embed, before -> after (2 cold runs):
  doxa-simple-map  engine start 1.06 -> 0.20 s, done 2.26 -> 0.95 s,
                   style start 2.73 -> 1.94 s
  regions-mfe      engine start 2.67 -> 0.11 s, done 3.25 -> 1.13 s
                   (style start unchanged: the composite mounts its map late)
  gospel resources engine start 0.81 -> 0.27 s
One engine request per run: the preload is reused, never a double
download. The time until tiles are loaded is dominated by tiles, data
and drawing, and moved within noise.

Held, per dx-shepherd: no shared Vue, no inlined mapbox-gl, and lighter
basemaps are for the Driver to decide.
2026-09-25 08:56:09 +00:00
doxa-maps-88655c70
27151f39a6 Research Gallery: boot on a partner page under the CORS the live server actually sends
The doxa.life script embed was blocked on a server change: the live root
manifest.json answers without Access-Control-Allow-Origin, and the gallery
fetch()es it cross-origin, so no tab could mount. Measured on nuxtbase,
though, the same static layer DOES send ACAO on every path inside a folder
of the tree (doxa-maps-spa/<anything>, even a 404; assets/…).

So generate-tree writes a byte-identical manifest copy at
doxa-maps-spa/manifest.json in the same step as the root one (they cannot
diverge). The gallery reads it FIRST when the tree is on another origin (no
CORS error in the host's console) and the root first when same-origin.
Script URLs still resolve against the tree root, never the copy's folder.

check:gallery-embed:live-cors serves the tree with the live rule
(serve-strict LIVE_CORS=1: no ACAO on the root manifest only).
- The current gallery under it: "did not boot within 60s" (negative
  control, reproducing doxa.life).
- With this change: js 14, iframe 14, neighbour 14, linked 4. The default
  gate also passes all phases.
check:embed-targets, check:drop and check:public-hygiene pass with the copy
present.

The server-side CORS rule (serving/traefik-embed-headers.yaml) is still the
right long-term fix, and it needs server access. This unblocks the embed
without it once the tree is published.
2026-09-25 08:55:52 +00:00
doxa-maps-88655c70
2a062c1ca0 README: the 7 production maps up front, one capabilities x deploy table, the library tree and rule
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- A gallery of the seven production maps (Big Picture, Simple Research,
  Regions, UPG-100 List, Prayer Progress, Cities with Internet, Gospel
  Resources) plus the Research Gallery, each shot from its own standalone page
  after its map finished loading. Five new shot definitions in readme-shots.
- One 5 x 5 table: every capability ships every way (iframe, mobile and JS
  embed today; WordPress and desktop proven, not yet in production).
- The "map cruncher" converge/diverge picture, folded.
- Repository layout shows the plugin-designs and shared library trees, and a
  folded "plugin or library?" section states the rule: write your app in its
  own plugin; put code in the library only if it is reusable and parameterized.
- Stale shots retaken from the latest builds.
Closes the README parts of DOCS-README-FOR-A-VISIONARY and BUILD-README-COMPLETE-HANDOFF.
2026-09-25 08:45:04 +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
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
e67d718603 Merge main into bundler-2/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 08:22:13 +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
a9550858ed Browser gates: measure THIS build, or measure nothing
About thirty browser gates could pass while describing another worktree's build.
This machine runs several checkouts and a default port belongs to whichever
server started first — measured 2026-09-24, :4173 was another worktree's, and an
hour of probing reported confidently on code nobody here had touched. check:sri
cannot see it: a stale tree and its stale leaf agree perfectly.

Twenty gates now call requireThisTree() right after they resolve their target and
exit 75 on a mismatch — EX_TEMPFAIL, the code this repo already uses for "not
measured", which the wave runner reports as ⊘ rather than ✗. Exiting 1 would turn
a mis-pointed probe into a red product gate, which is the failure this whole
family of checks exists to prevent.

★ NARROWER THAN THE PROBLEM STATEMENT, ON PURPOSE. Three classes do NOT get it,
because there it would be wrong rather than missing:
  self-serving gates  start their own server on an OS-assigned port
  file:// gates       read this checkout directly — no server to be wrong about
  remote targets      verify-map-paint defaults to the LIVE site, and "does
                      production match what we built" is a deploy question asked
                      deliberately. MEASURED mid-rollout: it reported NOT
                      MEASURED against the live host, which was the guard being
                      wrong, not the gate. It now enforces on localhost only.

TWO THINGS THE ROLLOUT TAUGHT, both caught by running rather than reading:
  · node --check passes a file that will die on ReferenceError. Adding a ROOT
    constant to sixteen gates that do not import resolve/dirname/fileURLToPath
    parsed perfectly and crashed on first run — the same shape that broke
    check-locale-reachable twice. The imports are added under aliased names so
    they cannot collide with a gate's own.
  · Five gates take their URL from argv with no default and are left for a
    per-file pass rather than patched blind.

CONTROLS: against another worktree's server a guarded gate prints NOT MEASURED
naming the mismatch and exits 75; against this checkout's server it runs and
reports normally; against the live site it proceeds.
2026-09-25 08:20:11 +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
145e3c0df8 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed 2026-09-25 07:40:09 +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
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
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
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
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
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
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
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
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