Commit graph

857 commits

Author SHA1 Message Date
doxa-maps-88655c70
24a4520bc0 Fix the import I omitted in the locale-reachable portability fix
Some checks are pending
build-gate / build (push) Waiting to run
My previous commit changed the REPO fallback to
resolve(dirname(fileURLToPath(import.meta.url)), '..') but did not add the
fileURLToPath import, so the gate died at load with

    ReferenceError: fileURLToPath is not defined

and I pushed it. node --check passed because that is a RUNTIME error, not a
syntax one, and check:no-abs-paths went green because the offending literal
really was gone -- so both of the checks I ran said yes while the gate
itself could no longer run at all. The script's own exit code printed 1 in
the same output I pushed from, and I did not stop on it.

Verified this time by running the gate, not around it:
check:locale-reachable 0, check:no-abs-paths 0.
2026-09-24 05:40:58 +00:00
doxa-maps-88655c70
05bc4c45c5 locale-reachable: default to this repo, not one author's worktree
Some checks are pending
build-gate / build (push) Waiting to run
Fourth instance of one pattern in this flight, after check-zone-census,
check-share-policy and check-poster-prints: a new internal/ gate hardcodes
its author's worktree (/home/.../wt-packet) as the REPO fallback, and
check:no-abs-paths -- which runs inside build:unlocked -- turns red for
every other builder on the next merge.

DOXA_REPO still wins and is untouched; only the default becomes portable,
via the idiom check-no-absolute-paths.mjs prints in its own failure text.
Verified both ways: check:no-abs-paths 0, check:locale-reachable 0 on the
derived default.

Four occurrences is a template problem, not four accidents.
2026-09-24 05:40:06 +00:00
doxa-maps-88655c70
27e397225e Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# 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
#	public/doxa-maps/regions-dashboard/region-dashboards/index.html
#	public/doxa-maps/regions-dashboard/regions-dashboard.js
#	public/doxa-maps/regions-dashboard/regions-list/index.html
#	public/doxa-maps/regions-dashboard/regions-mfe/index.html
2026-09-24 05:39:04 +00:00
doxa-maps-88655c70
b2f441b075 MapLibre drops a layer whose paint it does not recognise
The region fills did not paint under MapLibre, and three ticks of
instrumenting addLayer() found nothing because the failure never travels
through addLayer's return path. Listening to the map's own `error` EVENT --
the only place it surfaces -- gave it in one run:

    layers.regions-fill.paint.fill-emissive-strength: unknown property
    layers.regions-border.paint.line-emissive-strength: unknown property

*-emissive-strength is a Mapbox GL v3 lighting property. MapLibre rejects a
layer whose paint carries an unknown property and DROPS THE WHOLE LAYER:
addLayer() accepts the call, emits an error event, and silently omits the
layer. getLayer() then returns undefined and the map renders without it, so
it reads as "the fills do not paint" rather than as a failure.

★ THE REPO ALREADY KNEW THIS, FOR CIRCLES ONLY. useMapProvider.emissivePaint()
exists and its own docstring says "the whole layer is dropped -> invisible
pins" -- but it was hardcoded to 'circle', while the fill, line and
clustering layers still wrote the literal and kept being dropped for exactly
the documented reason. Generalised to emissivePaint(map, kind) and applied
at all five literal sites: useMapLayers x3, useCountryOutline,
useMapClustering. One helper, one rule, no sixth copy.

A second defect was hiding behind the first: regions-border still declared
'source-layer' on the geojson source ("Source layer country_boundaries does
not exist on source"), which only became visible once the paint error
stopped masking it. Fixed the same way as regions-fill.

MEASURED on the built tree: 0 error events, both layers present, and the
screenshot shows the WAGF fills painting under MapLibre -- green, blue,
purple, red, orange -- matching the Mapbox control, which is itself
re-shot and unchanged. Provider sweep 15/17 (the 2 render no map at all),
gates:pre 0, gates:post 17/18 with only the pre-existing
verify/tables-complete failure.
2026-09-24 05:38:29 +00:00
doxa-maps-88655c70
39ec4b42bd test(i18n): every packet language must be SELECTABLE, not merely present
Some checks are pending
build-gate / build (push) Waiting to run
The bug this gates is unreachability, not incorrectness. The packet shipped 11
complete translations while detectLocale() read only <html lang>, which every
generated leaf page hardcodes to "en" — so ten of them could not be reached by
any reader on the static tree. Nothing was wrong; nothing was visible.

Two assertions, because they fail separately:

  RESOLUTION    ?lang= / ?locale= selects a supported locale, an unsupported
                value falls back to English, a region tag (pt-BR) resolves to its
                base, and with no param the host page still wins.
  REACHABILITY  the packet's language list and SUPPORTED_LOCALES cannot drift
                apart, and the leaf pages still declare a lang — so if someone
                later drops that attribute, the host-page path does not silently
                become unavailable while the tests stay green.

★ It evaluates the SHIPPED detectLocale rather than reimplementing it. A gate
that reimplements the thing it tests only ever proves the copy agrees with
itself — which is how a fix can be reverted in the source while the gate keeps
passing.

Confirmed to fail before acceptance: removing the URL branch reports all four
param cases resolving to "en".
2026-09-24 05:36:45 +00:00
doxa-maps-88655c70
e2a47b1366 chore(i18n): regenerate the tree after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
Generated output only. Regenerating rather than hand-merging keeps the tree a
true function of the merged source — and refreshes the SRI hashes, which is not
optional: a stale hash makes the browser refuse the script and the page mounts
NOTHING, silently. Re-verified afterwards rather than assumed: check:locale-reachable
and check:deck-click both pass.
2026-09-24 05:36:30 +00:00
doxa-maps-88655c70
f7f4b8c1e8 fix(i18n): let a reader actually CHOOSE one of the 11 locales
The packet ships in 11 languages, every string translated. detectLocale() read
ONLY document.documentElement.lang — and EVERY generated leaf page in this repo
hardcodes lang="en" (measured: 20 of 20, later 34 of 34 after the tree grew).

So on the built static tree — the jamstack case these bundles exist for — ten of
the eleven translations could not be reached by any reader. Correct, complete,
and invisible. Several ticks of translation work landed behind a door with no
handle.

?lang= / ?locale= is now consulted FIRST, then the host page, then English. A
query parameter is the smallest thing that makes them reachable: no per-language
page, linkable and shareable, and the WordPress/Polylang path through <html lang>
is untouched — a gate assertion keeps the leaf pages declaring a lang so that
path cannot silently become the only way in. Both spellings are accepted because
both are in use here (`lang` in profile configs, `locale` in the i18n layer) and
guessing wrong is a silent no-op.

⚠ AND MAKING ARABIC REACHABLE EXPOSED THE NEXT PROBLEM. Only ONE component reads
RTL_LOCALES today, so selecting ar would have given Arabic TEXT inside a
left-to-right layout everywhere else — readable glyphs in the wrong place, which
reads as a styling quirk rather than a bug. createAppI18n now reflects an RTL
locale onto document.dir. It only ever SETS it, never clears it: a host page that
already declares dir owns that decision, and stomping it to "ltr" for an English
map embedded in an Arabic page would break the page around us.

Verified: ?lang=ar sets dir=rtl in a real browser; ?lang=zz correctly falls
through to English. detectLocale is exercised against its SHIPPED source across
7 cases including pt-BR -> pt.

★ One thing this tick taught me the hard way: rebuilding a bundle WITHOUT
regenerating the tree leaves the leaf page carrying the OLD SRI hash, so the
browser refuses the script and NOTHING MOUNTS — silently. I spent three probes
reading an empty page before checking the integrity attribute.
2026-09-24 05:35:11 +00:00
doxa-maps-88655c70
d0ecd2f780 test(i18n): every packet language must be SELECTABLE, not merely present
The bug this gates is unreachability, not incorrectness. The packet shipped 11
complete translations while detectLocale() read only <html lang>, which every
generated leaf page hardcodes to "en" — so ten of them could not be reached by
any reader on the static tree. Nothing was wrong; nothing was visible.

Two assertions, because they fail separately:

  RESOLUTION    ?lang= / ?locale= selects a supported locale, an unsupported
                value falls back to English, a region tag (pt-BR) resolves to its
                base, and with no param the host page still wins.
  REACHABILITY  the packet's language list and SUPPORTED_LOCALES cannot drift
                apart, and the leaf pages still declare a lang — so if someone
                later drops that attribute, the host-page path does not silently
                become unavailable while the tests stay green.

★ It evaluates the SHIPPED detectLocale rather than reimplementing it. A gate
that reimplements the thing it tests only ever proves the copy agrees with
itself — which is how a fix can be reverted in the source while the gate keeps
passing.

Confirmed to fail before acceptance: removing the URL branch reports all four
param cases resolving to "en".
2026-09-24 05:34:47 +00:00
doxa-maps-88655c70
ba6b9229f3 Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
Six conflicts, all in generated artifacts under public/, including this
bundle's own — another builder's tree regeneration had rebuilt it.
Regenerated rather than hand-merged: the three PosterWall consumers
rebuilt, then the tree and the hashes.

PosterWall.vue itself auto-merged with both sides intact — this
branch's embedded/sectionHeads props and main's usePosterPrints
refactor — and the four suites that exercise it through the regions
poster dropdown were re-run green after the merge, since their change
lands inside the component this feature renders.
2026-09-24 05:33:32 +00:00
doxa-maps-88655c70
c2046be7ec Gate the agreement between the surfaces, after finding the same drift three times
Three defects this flight were the same defect, and none of them threw. Every page
rendered; only comparing two surfaces caught them:

  the country sheet listed 36 groups for Indonesia while the map drew 669 pins, because
  the sheet read a curated subset and the map read the pins;

  the resource table silently dropped 506 real resources across 377 languages, because
  its generator skipped any code no pin speaks — in a table titled "every link";

  resources attached to `und` told 14 people groups there was Scripture in a language
  the record explicitly does not identify.

A fourth is a matter of time, so the comparison becomes a gate rather than a habit.

SIX CLAUSES, all static, 60ms, no browser and no server: no ISO reserved code carries a
resource; every resourced code has a language name; the table's reach columns match an
independent recount off the pins; every resourced code appears in the table; no table URL
exists that the pin index does not hold; and each pin's tier agrees with its code's URLs.
Each one is a defect that was actually shipped, not a hypothetical.

★ IT REFUSES TO PASS VACUOUSLY. Five of six clauses assert a NEGATIVE, which is exactly
the shape that reads green against empty or mis-keyed input, so an empty corpus is a FAIL
rather than a clean run, and a missing input SKIPS loudly with the command to fix it.

★ AND IT SELF-TESTS. `--poison` injects one defect per clause in memory and requires all
six to fail. Clause D is included deliberately: it is the one that would have caught the
506 dropped resources, and the first version of this self-test left it unproven — the
most valuable clause was the one clause never shown to fail. 6/6 now.

Registered in package.json and in build:unlocked, because a gate nothing runs is not a
gate — this repo has carded that twice. check:no-orphan-gates went from FAIL to PASS on
registration, which is the check confirming itself. The CLI doc quotes the build chain
verbatim, so it was updated to match: package.json is what runs, the doc is the copy.

No product file changed, so nothing is rebuilt. check:quoted-scripts, check:doc-scripts,
check:no-orphan-gates and check:mjs-parse all pass.
2026-09-24 05:26:34 +00:00
doxa-maps-88655c70
9e8d53bf3b test(poster): assert the shared PACKET action's shape, not just its bytes
Some checks are pending
build-gate / build (push) Waiting to run
The packet is the product's core artefact, and it moved into the shared
composable this tick. The gate now drives printPacket and asserts what came out:
4 sheets for 2 groups (map one-pager + one people page + prayer points + prayer
wheel), 12 wheel segments, Mark 5:1-20 present. Confirmed to fail before
acceptance — removing the wheel page reports 3 sheets and 0 segments.

⚠ THE HARNESS HAD TO RESOLVE THE RENDERERS, and the first run exposed why. The
composable imports them by RELATIVE path; under a blob URL there is no directory
to resolve '../packet/...' against, so the action failed with "Failed to resolve
module specifier" and printed nothing — reported as 0 sheets, which reads like a
product bug. It is not: Vite resolves them and the built bundle inlines both
renderers (verified in the shipped my-upg-100-list.js). The harness now
substitutes blob URLs for packet-render, card-render, qr and the strings catalog,
so the gate exercises the REAL renderers rather than a stub.
2026-09-24 05:23:05 +00:00
doxa-maps-88655c70
117467f7e7 Stop claiming Scripture in a language recorded as "Undetermined"
Reading the list profile's own screenshot turned up a row for "Undetermined (und)" —
14 people groups — offering two Open Bible Stories links. ISO 639-3 reserves und, mul,
mis, zxx and qaa-qtz for exactly the case where the language is NOT known, so a resource
attached to one is attached to nothing. Telling 14 groups there is Scripture in their
language when the record says the language is undetermined is the single worst thing this
map can do, and it took looking at the page to see it. Door43 publishes 4 entries under
und; 16 pins sit on a special code. All are now excluded at the index.

COVERAGE GOES DOWN, 8,935 -> 8,921 pins with a real URL. That is the right direction: the
14 were false.

The same screenshot showed Dogri rendering "Open Bible Stories  Open Bible Stories" —
two links a reader cannot tell apart. 40 languages carry more than one entry under the
same subject, because a language can have more than one independent translation effort.
The owner now disambiguates, and ONLY where it is needed: Dogri reads "Open Bible Stories
(translationCore-Create-BCS)" and "(OBS-TLF)", while a language with one entry is
unchanged.

The list profile also gained the pool it predated. It bundles its own resources.json at
build time, so Door43 never reached it: 49 languages, 63 links. Checked before writing —
120 of its 862 languages already carry a scripture link and those are exactly the 120 the
enriched index has, so nothing there needed correcting, and its curated 862-language scope
is this profile's subject rather than staleness. The enrichment is idempotent: it drops
what a previous run added before re-adding, or a stale title would survive the index
learning to disambiguate.

NOT DONE, DELIBERATELY: the 506 zero-reach rows still never match a COUNTRY filter.
Arclight carries a primaryCountryId that could attribute them, but the only alpha-2 to
alpha-3 mapping in this repo disagrees with itself on 18 countries and cannot resolve 51
of the 168 codes. A wrong attribution would point a real link at the wrong field, so the
behaviour is documented in the table meta instead of guessed at.

Verified: und/mul/mis/zxx absent from the index, the list renders 49 Door43 anchors where
it rendered none, three sampled new links answer 200, and pins, table, deep-link, country
and sign-language probes all still pass.
2026-09-24 05:22:59 +00:00
doxa-maps-88655c70
a0910bcdef refactor(packet): the legend's packet export uses the shared action
Some checks are pending
build-gate / build (push) Waiting to run
Continues the consolidation. The card deck moved to usePosterPrints last tick;
the PACKET export in upg-100-list.vue was still a second hand-maintained copy of
the same shape — its own renderer import, its own strings fetch, its own
language resolution. Two copies of one fact drift.

printPacket now lives in the composable, so the browser export and the
build-time generator share one renderer AND one calling convention. What stays
in the bundle is what genuinely cannot be shared:

  packetRecords  who is in this list — the people-groups API, the selector
                 evaluator, and the pg_slugs special case that 12 of the 27
                 lists need (the evaluator intentionally THROWS on that type)
  packetMapSvg   where this bundle's posters are served from. Absent is fine:
                 the packet prints a cover without the map rather than failing

⚠ AND THE MIGRATION WOULD HAVE MADE THE PACKET PRINT SILENTLY. The composable
reports through its own exportState; this profile has no HUD for that and shows
`status` instead — exportState appears ZERO times in the file. Without a bridge
there would be no "building…", and a failure would vanish with nothing said: the
mechanism working while the feedback does not, which is the exact trap the wall
consolidation hit from the other direction. A watcher now maps the composable's
phases onto `status`.

Verified: check:poster-prints drives the shared action and asserts the packet's
SHAPE — 4 sheets for 2 groups (map + people page + points + wheel), 12 wheel
segments, Mark 5:1-20 present — not merely that bytes were produced. Confirmed
to fail before acceptance by removing the wheel page (3 sheets, 0 segments). The
build-time generator still passes check:packet-render at 8/8, and
check:deck-click still prints 116 cards through the real button.
2026-09-24 05:22:29 +00:00
doxa-maps-88655c70
248c2cc6d6 test(poster): assert the shared PACKET action's shape, not just its bytes
The packet is the product's core artefact, and it moved into the shared
composable this tick. The gate now drives printPacket and asserts what came out:
4 sheets for 2 groups (map one-pager + one people page + prayer points + prayer
wheel), 12 wheel segments, Mark 5:1-20 present. Confirmed to fail before
acceptance — removing the wheel page reports 3 sheets and 0 segments.

⚠ THE HARNESS HAD TO RESOLVE THE RENDERERS, and the first run exposed why. The
composable imports them by RELATIVE path; under a blob URL there is no directory
to resolve '../packet/...' against, so the action failed with "Failed to resolve
module specifier" and printed nothing — reported as 0 sheets, which reads like a
product bug. It is not: Vite resolves them and the built bundle inlines both
renderers (verified in the shipped my-upg-100-list.js). The harness now
substitutes blob URLs for packet-render, card-render, qr and the strings catalog,
so the gate exercises the REAL renderers rather than a stub.
2026-09-24 05:22:17 +00:00
doxa-maps-88655c70
63fad20739 Regions: their worldmap watermark, and two defects it uncovered
Measured first, as the Oswald lesson requires, and this one was real:
their UupgsList root carries worldmap.svg inline (UupgsList.vue:451),
and it genuinely renders on their live country page — HTTP 200, 62,740
bytes over the wire, computed background-size 60%, position 50% 0%,
repeat no-repeat, min-height 400px. Confirmed by screenshotting the
ELEMENT, not the viewport: the watermark sits below the fold, so a page
screenshot shows nothing at all.

public/assets/images/worldmap.svg is their file byte-for-byte — 311KB on
disk, small against this tree's existing assets, which run 644KB to
42MB. The inline background-image takes its base from a new imagesBase
prop, since a dropped bundle cannot assume their absolute /assets path.

generate-tree's staged allowlist needed 'images' added — the same trap
that silently broke the vendored fonts. A dir absent from the allowlist
404s, and an unfetched background-image is quieter than the font
failure was: nothing paints and nothing reports. So the probe asserts
the REQUEST returned 200 rather than that the CSS names the file.

Then, reading the screenshot after every number was already green, two
more defects:

- A CLASS WITHOUT ITS RULE. The search label uses their class name,
  sr-only — the right instinct — but their rule
  (utilities/_visibility.scss:2) was never ported, so the class matched
  nothing and the label rendered as a visible line of text above the
  input, duplicating its own placeholder. Borrowing a class name only
  works if its declarations travel with it, and a class matching no rule
  is invisible to a property diff. Now 1x1px and clipped, still in the
  DOM and still announced.

- A PIXEL CAP WHERE THEIRS IS RELATIVE. .search-box was capped at a
  hardcoded max-width:480px; theirs composes .max-width-md, which is
  max-width:50ch (utilities/_sizes.scss:11). Wrong value and wrong unit —
  ch tracks the input's font, px does not — and ours truncated the
  placeholder mid-word. 50ch now resolves to exactly 600px, their
  measured width.

Regression: 18 suites green, including a new _probe-watermark.mjs at
15/15.
2026-09-24 05:15:06 +00:00
doxa-maps-88655c70
bc18753985 Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 05:14:55 +00:00
doxa-maps-88655c70
694f3859d6 List every link, including the 377 languages no pin speaks
The table is titled "every link" and was silently dropping 506 real resources across 377
languages, because the generator skipped any code with no people group on this map. They
are not edge cases: Ahirani has Scripture, a film and Open Bible Stories; Pahari Mahasui
has Scripture, a film and five Door43 resources. Someone searching for that language
still needs to find it, and the reach columns reading 0 is the true answer to "how many
groups on THIS map speak it" rather than a reason to hide the resource.

3,043 -> 3,549 rows, 2,307 -> 2,684 languages. The meta says what a zero-reach row means
so the column is not read as "this resource reaches nobody".

THE INCLUSION EXPOSED A BLANK. Those codes are named nowhere in the people-group dataset
— it only names languages some group speaks — so their rows rendered a bare ISO code with
an empty language column, which tells a reader looking for a language precisely nothing.
The eBible index now carries languageNameInEnglish alongside the translation title (two
different things), and a final pass names any remaining code from whichever catalogue
knows it. Codes carrying a resource with no language name: 377 -> 0.

An audit of the reach columns against an independent recount off the pins found 0
disagreements on people_groups and 0 on speakers, so the table and the map agree about
who speaks what.

Verified: a seeded sample of 12 newly-exposed rows — 9 resolved 200, 3 skipped as
unverifiable, because jesusfilm.org/watch answers 200 for any id and a status check there
proves nothing. Three Door43 links first returned 000, which is curl failing to CONNECT
rather than an HTTP status; retried with spacing they all answer 200, so the failure was
my own request rate and not a dead link. Table, deep-link, pins, gap-list and country
probes all still pass, and the country sheet still never fetches the retired 1.4MB file.
2026-09-24 05:13:57 +00:00
doxa-maps-88655c70
e263991dc5 test(deck): click the REAL button and read what it prints
Some checks are pending
build-gate / build (push) Waiting to run
Every other packet/card gate drives the RENDERERS. This one drives the UI: it
finds the Card Deck button in the shadow root, clicks it, and captures the
document that reaches the print frame. That is the only path proving the wiring
— records provider, stock picker, language, shared composable, print frame — is
connected end to end. A renderer can be perfect while the button calling it
passes the wrong stock, or nothing at all.

⚠ It intercepts the printed HTML rather than reading the iframe back.
openPrintFrame calls window.print(), which BLOCKS in headless Chromium, and the
frame self-removes on afterprint — reading it back measures a frame that has
already gone and reports 0 cards for a correct deck.

Measured through the real UI: 116 cards at 2.5in 3.5in with 116 QR codes, and
3.5in 2.5in when the stock picker is switched to landscape. Confirmed to fail on
a wrong expected page size.

Also updates the share policy: country-posters moves from 'own-button' to
'header'. Its two-tab MFE now owns sharing, exactly as my-upg-100-list's does —
the gate caught the bundle briefly carrying two affordances after the MFE
landed, which is the regression it was written for.
2026-09-24 05:13:03 +00:00
doxa-maps-88655c70
571670f1d3 chore(poster): regenerate the tree after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
Generated output only — built bundles, SRI hashes, leaf pages and the manifest —
conflicted because another agent's work landed in the same files. Resolving
those by hand yields a tree matching neither agent's source, so this takes
main's versions and regenerates. Re-verified rather than assumed: check:deck-click
and check:mfe-two-tab both pass afterwards.
2026-09-24 05:12:41 +00:00
doxa-maps-88655c70
09460e1777 poster-prints: default to this repo, not one author's worktree
Third instance of one pattern in this flight, after check-zone-census and
check-share-policy: a new internal/ gate authored in one worktree hardcodes
THAT worktree (/home/.../wt-packet) as its REPO fallback, and
check:no-abs-paths -- which runs inside build:unlocked -- turns red for
every other builder on the next merge.

Same minimal fix each time: DOXA_REPO still wins and is untouched; only the
default becomes portable, via the idiom check-no-absolute-paths.mjs prints
in its own failure text. Verified both directions: check:no-abs-paths 0, and
check:poster-prints itself 0 on the derived default.

Three occurrences is a pattern, not bad luck. Whoever is generating these
gates should take the portable default as the template, or the next one
lands red too.
2026-09-24 05:11:10 +00:00
doxa-maps-88655c70
6c32623856 refactor(poster): the 100-list wall stops carrying its own copies
Finishes the half this card left open last tick. The shared print composable
existed but the 903-line 100-list wall did not use it, so the card deck lived in
TWO places — I wrote both this flight, and they could already drift.

Two duplicates removed:

  printCardDeck   now delegates to usePosterPrints, sharing THIS wall's
                  exportState so the HUD keeps rendering one surface. The bundle
                  still owns what cannot be shared — fetchListRecords knows the
                  people-groups API, the selector evaluator and the pg_slugs
                  special case — and hands it over as the `records` provider. The
                  preview-only "no API configured" hint is preserved, because the
                  shared action cannot know about it.

  openPrintWindow was a local reimplementation of printInPlace.js — same
                  off-screen iframe, same doc.write, same autoprint script,
                  differing only in an unused `title` argument. A FOURTH copy of
                  one mechanism is how a fix to the blank-first-page behaviour
                  reaches one caller and not the others. Now a one-line adapter.

★ AND check:share-policy CAUGHT A REGRESSION I HAD CAUSED. Adding the
country-posters two-tab MFE last tick gave that bundle a second ShareButton —
the BUG-UPG100-DOUBLE-SHARE state the 100-list already fixed once. Fixed the
same way the 100-list fixed it: the MAP's button is REMOVED and the MFE header
owns sharing. Removed, not flag-gated — a `v-if="false"` button is dead code
that still reads as an affordance to every reader and every counter, which is
exactly why the gate counts tags rather than renders.

Verified by clicking the REAL button in a browser, not by driving the renderer:
116 cards at 2.5in 3.5in with 116 QR codes, and 3.5in 2.5in when the stock
picker is switched to landscape — so records provider, picker, language, shared
composable and print frame are connected end to end. check:mfe-two-tab still
reports exactly one share affordance on the country-posters MFE.
2026-09-24 05:10:44 +00:00
doxa-maps-88655c70
394741bf7a test(deck): click the REAL button and read what it prints
Every other packet/card gate drives the RENDERERS. This one drives the UI: it
finds the Card Deck button in the shadow root, clicks it, and captures the
document that reaches the print frame. That is the only path proving the wiring
— records provider, stock picker, language, shared composable, print frame — is
connected end to end. A renderer can be perfect while the button calling it
passes the wrong stock, or nothing at all.

⚠ It intercepts the printed HTML rather than reading the iframe back.
openPrintFrame calls window.print(), which BLOCKS in headless Chromium, and the
frame self-removes on afterprint — reading it back measures a frame that has
already gone and reports 0 cards for a correct deck.

Measured through the real UI: 116 cards at 2.5in 3.5in with 116 QR codes, and
3.5in 2.5in when the stock picker is switched to landscape. Confirmed to fail on
a wrong expected page size.

Also updates the share policy: country-posters moves from 'own-button' to
'header'. Its two-tab MFE now owns sharing, exactly as my-upg-100-list's does —
the gate caught the bundle briefly carrying two affordances after the MFE
landed, which is the regression it was written for.
2026-09-24 05:10:17 +00:00
doxa-maps-88655c70
916ef35ee8 Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# 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-24 05:09:42 +00:00
doxa-maps-88655c70
fc69adbbd5 Country boundaries load on MapLibre in the shared library, not per plugin
main added country-posters-mfe (isMap=true). It inherits the engine swap
correctly -- 14 -> 4 requests to api.mapbox.com, maplibregl loaded -- but
threw

    Fetch API cannot load mapbox://mapbox.country-boundaries-v1.
    URL scheme "mapbox" is not supported

A new map arriving is exactly what the rollout has to cover, so this was
worth chasing rather than noting.

TWO shared library sites, not one, which only measuring found. Fixing
useCountryOutline.js (consumers: doxa-simple-map, countries-map,
research-map) cleaned two of the four leaves and left countries-map -- and
the new element nesting it -- still throwing, from a SECOND site:
useMapLayers.js addRegionsLayer. Both now branch per engine, using the
keyless assets/boundaries/world-adm0.geojson the tree already ships, with
iso3 normalised once to iso_3166_1_alpha_3 so every downstream expression
keeps reading the name it always read, and 'source-layer' set for the
vector-tile path alone.

The engine is read off the LIVE map -- its container carries
maplibregl-map -- rather than from config, because both composables are
called with nothing but a map getter.

MEASURED: 0 mapbox:// errors across country-posters-mfe, countries-map,
doxa-simple-map, research-map and demo-map; the adm0 fetch confirmed firing;
provider sweep 15/17. The Mapbox path is unchanged -- its control screenshot
still shows the coloured WAGF region fills.

Also fixes check:descriptions, which the new profile red-gated by shipping
an empty description. Written from the component's own header, not a
placeholder: 34/34 profiles described.

NOT FIXED, and recorded in the ledger rather than glossed: under MapLibre
countries-map still paints NO coloured region fills. The error is gone and
the geojson loads, but the fill does not appear. That is a separate defect
from the source failing, and claiming this commit finishes it would be
false.
2026-09-24 05:09:12 +00:00
doxa-maps-88655c70
a1ccc683a6 Make a country describe the same people groups the map draws
The country sheet was built from people-group-resources.json, a curated 2,106-record
subset, while the map draws 12,379 pins. The two described different universes and the
sheet silently undercounted. MEASURED before this change: Indonesia listed 36 groups with
669 pins under your finger, Nigeria 38 against 433, India 899 against 2,146. A map that
contradicts itself on tap is worse than one that says less.

Matching was also done by PARSING a comma-separated country-name string against a
longest-first vocabulary — a second way to lose rows, since a name that does not match
the vocabulary exactly drops out with no error. Pins carry ISO3, so the index is now an
exact key rather than a text search.

ONE SHAPE, BUILT IN ONE PLACE. openPin used to assemble its own group object while the
country list built another, so a field added to one path silently did not exist on the
other — which is how is_sign and the Door43 list would have gone missing from a group
opened out of a country. groupAt(i) is now the only constructor.

THE HEADER HAD TO FOLLOW, and reading the screenshot is what caught it. Moving the list
to the pins left the stat block still reading the curated table, so the card contradicted
itself inside one view: "People groups 36" sitting directly above "Gospel resources found
(282)". The stats are now derived from the same groupsForCountry the list renders.
Growing-and-unreached and deaths/day are NOT derivable from pins — they come from the
curated analysis — so they are marked and footnoted with how many groups that analysis
actually covers, rather than sitting unlabelled among numbers with a different basis.

RETIRES A 1.4MB FETCH. people-group-resources.json is no longer requested at all: the
pins are already in memory and carry richer resource data than that file ever did. Only
the 136KB link-verification fetch remains, and it is still allowed to fail.

Verified on the served tree: for IDN, NGA, IND, NPL and TCD the sheet's group count now
equals the layer's pin count exactly (669/669, 433/433, 2146/2146, 171/171, 150/150), and
with + without partitions the total in every case. The network log confirms
people-group-resources.json is never fetched. Indonesia reads 669 groups, 387 with
nothing, 282 with a resource — and 669 - 387 = 282. All seven browser probes still pass.
2026-09-24 05:07:31 +00:00
doxa-maps-88655c70
7f8b75d661 Experimental shelf: Kingdom-impact order + curated exclusions
Some checks are pending
build-gate / build (push) Waiting to run
The Experimental folder now presents most-impactful first per the
finalized roundup ranking (EXPERIMENTAL-MAPS-ROUNDUP.md): Prayer Walk
Planner, the rescued legacy set (entry: send-map), All People Groups,
Country Posters, Vision Card -- rank is editorial curation and lives
beside TAB_DEF; unranked bundles still discover and sort A-Z after.
Verified in the built shell rail.

Exclusions tightened: demo-map + golden-path-check (scaffolding /
authoring tooling -- the studio keeps them), doxa-research-map (the
part already composed into Simple Research), stack-demo (broken and
deferred; a broken tab on the giveaway shelf reads as damage).
Wrapper description corrected: the legacy IIFE holds FIVE maps.
2026-09-24 05:02:21 +00:00
doxa-maps-88655c70
449adfe515 test(poster): the shared print actions must produce real documents
Some checks are pending
build-gate / build (push) Waiting to run
Gates usePosterPrints.js — the print actions now shared by every poster wall —
by driving them in a browser with a fake records provider, so the assertion is
about the DOCUMENTS rather than any bundle's API being reachable.

★ BOTH DIRECTIONS, because "the button is there" and "the button works" are
different claims and this flight has repeatedly found the first true while the
second was false. With a provider: a real table, correct row count, the prayer
checkbox column present. WITHOUT one: phase "error" naming the missing provider,
not a silent no-op and not a throw.

⚠ IT CAPTURES THE PRINTED HTML INSTEAD OF READING THE IFRAME BACK, and finding
that out cost a wrong first result. openPrintFrame calls window.print() under
autoprint, which BLOCKS in headless Chromium, and the frame self-removes on
afterprint — so reading it back measured a frame that had already gone and
reported 0 rows for a perfectly correct document. Intercepting what the
composable hands to the frame measures what actually gets printed.

Confirmed to fail before being accepted: deleting the checkbox column from the
row template reports "the list print has no prayer checkbox column".
2026-09-24 04:59:45 +00:00
doxa-maps-88655c70
5c192ea0e2 chore(poster): regenerate the tree after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
The rebase conflicted only in GENERATED output — the built bundles, their SRI
hashes, the leaf pages and the manifest — because another agent's regions work
landed in the same files. My source file applied cleanly.

Resolving generated files by hand produces a tree matching neither agent's
source, so this takes main's versions and REGENERATES: four bundle builds plus
`npm run tree`. Re-verified rather than assumed — check:poster-prints and
check:mfe-two-tab both pass after the regeneration.
2026-09-24 04:59:23 +00:00
doxa-maps-88655c70
04f165efd3 feat(poster): share the print actions, keep the record fetching per bundle
The poster wall was generalised into library/components/poster/PosterWall.vue
and two bundles migrated onto it — but the ORIGINAL, my-upg-100-list's 903-line
wall, was left on its own copy. So the one wall carrying the real print actions
is the one wall nothing else benefits from, and the shared component's packet
path was still a "coming soon" stub. Every print fix made in the 100-list this
flight (the card deck, the stock picker, the image_url fetch) was invisible to
the three bundles on the shared component, and vice versa.

★ THE SEAM IS WHAT MADE THIS SHAREABLE AT ALL. The PRINT ACTIONS are common; the
RECORD FETCHING is not. fetchListRecords in the 100-list knows the people-groups
API, the selector evaluator and the pg_slugs special case; Key Cities and Country
Posters have entirely different data shapes. usePosterPrints.js therefore takes a
`records(item)` PROVIDER and owns everything after it — the HUD state machine,
the in-place print frame, and the documents.

That also keeps the bundles honest. A wall that CAN supply rows passes the prop
and gets the list + card-deck prints; one that cannot omits it, the Card Deck
button is not rendered, and Prayer List keeps its truthful "coming soon" instead
of a button that fails at click time. Country Posters shows exactly that today:
175 cards, Prayer List and Poster Map, no Card Deck — because it has no provider
yet, not because something broke.

⚠ ONE exportState, not two. The composable owns a ref by default, and a wall
rendering its OWN state while these actions wrote to a private one would show no
progress and swallow every error — the mechanism working and the feedback not,
which is the failure mode this flight keeps finding. PosterWall hands the
composable its own ref so both write to the surface the reader is looking at.

Prints IN PLACE via the shared openPrintFrame, never window.open() — the
mechanism the Driver asked for by name: "not supposed to open up a new browser,
but should open up the print dialog directly in the browser".

NOT DONE, and deliberately: the 100-list wall is NOT yet migrated onto the shared
component. Its template is 107 lines against the shared 72 with a different
structure, and it is a Driver-reviewed surface — that is a rewrite whose risk
does not fit in one tick alongside this. The shared half lands first and is
verified; the migration stays open on the card with this composable now waiting
for it.

Verified: all three bundles on the shared wall build; country-poster-wall renders
175 cards with zero page errors; check:poster-prints drives the composable in a
browser and asserts BOTH directions.
2026-09-24 04:56:31 +00:00
doxa-maps-88655c70
c1ab013f9e Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
The one conflict was in public/doxa-maps/index.html, which generate-tree
writes. Built files are regenerated, not hand-merged: country-posters
was rebuilt because main changed its source (a new two-tab MFE),
regions-dashboard because this branch did, then the tree and the hashes.
2026-09-24 04:54:42 +00:00
doxa-maps-88655c70
c268d13a42 Regions: the list tab was half-localised, and Oswald must not be vendored
Two findings, both from measuring at locale=ru instead of trusting the
plan.

DO NOT VENDOR OSWALD. The work list called for it for Russian headings.
Their rule is [lang="ru-RU"] on h1/h2/h3 (_globals.scss:40) — but the
live document's lang is "ru", so the selector never matches. It is dead
code on their own site. Oswald never loads there either:
document.fonts reports it "loading"/"unloaded" while Bebas, Poppins and
Brandon all report "loaded", and the glyph oracle agrees — Oswald
renders "HAMBURGEFONS" at 855 against sans-serif's 856.36 (0.2%, noise),
where Bebas moves it to 486 (43.2%). Bebas itself has no Cyrillic (501
vs 501.42), so their Russian headings fall back to the system face at
letter-spacing normal. Vendoring Oswald would have made us DIFFER.
_probe-ru-parity.mjs now asserts that parity instead of assuming it:
10/10, both sides falling back identically at 444.88.

The list tab was half-localised, and three of the four defects were
invisible in English:

- .rl-title forced text-transform:uppercase. Their h1 is none on both
  locales. Invisible in English because Bebas is an all-caps face, and
  wrong in Russian, where theirs reads "Регионы" and ours read
  "РЕГИОНЫ". This file's own comment already recorded the correct
  finding for .region-card__title; this rule was missed.
- summaryLine() was a hardcoded English template where theirs is
  t('%count% people groups in %countries% countries'). The Russian
  string was already in our catalogue — copied, never wired.
- dashboardLink() likewise: their ru value is "Открыть панель: %region%",
  a different word order no English template could be patched into.
- Nothing refetched on a locale change. The region and country labels
  come from the API's ?lang=, and their site serves Азия/Африка/
  Азербайджан on /ru/regions. region-dashboards.vue already had
  watch(languageCode, loadRegions); this tab did not, so a locale switch
  translated the chrome and left every label English.

Why none of it was caught: _probe-region-vs-live.mjs does compare
textTransform and reports 73/73 — but it diffs the DASHBOARD tab's
.rd-title. Nothing ever property-diffed the LIST tab's head against
/regions. The gap was coverage, not the property list.
_probe-list-vs-live.mjs closes it at 8/8.

The loading state now uses their own 'Loading results...' key rather
than new English, since that string is already translated in all eight
locales.

Regression: 18 suites green.
2026-09-24 04:53:36 +00:00
doxa-maps-88655c70
5e54e601aa Say that audio was never searched, on the groups whose need it is
MEASURED: has_audio is 'unknown' for all 2,106 source records and therefore for all
12,379 pins. Not one group on this map has had its audio checked, because GRN — the main
audio-gospel pool — is named as unchecked on every record. The map was nonetheless
showing 3,444 groups as "nothing found", covering 347,125,659 people, and for an oral
culture audio is not one resource among several, it is the one that matters.

So the claim was overstated in the place it is least affordable. Three changes, all copy,
no data:

The audio chip said "unknown", which did no work — it is unknown for EVERY group, so it
read as a property of that group rather than of our search. It now says "not searched
(GRN)" and names the pool in its tooltip. A shrug becomes something a reader can act on.

The gap list now leads with it: 3,444 groups, 347,125,659 people, and "Audio has never
been searched for any of them" before the ranked rows rather than after.

The legend caveat names the pool and what its absence costs, instead of the passing
"GRN audio is unchecked".

NOT ADDED, DELIBERATELY: Arclight publishes an audioPreview for 2,074 of these codes and
it is a real, resolving URL. It is also ~24.4KB of AAC — MEASURED at a uniform 11.8
seconds — a language-identification sample, not Scripture. Shipping it under "Audio ·
found" would be the same error as calling Open Bible Stories a Bible, so it is left out.
Real audio still needs GRN or FCBH credentials.

Verified on the served tree: the legend carries the warning, a group sheet reads "Audio ·
not searched (GRN)" with no bare "unknown" anywhere in it, the gap panel carries the
sentence, and the people-count renders 347,125,659 — the same number the build measures.
Pins, filter, gap-list and sign-language probes all still pass.
2026-09-24 04:48:45 +00:00
doxa-maps-88655c70
e1553df458 test(provider): ?provider=maplibre must swap the ENGINE, measured by request host
Some checks are pending
build-gate / build (push) Waiting to run
Gates BUG-UPG100-IGNORES-THE-PROVIDER-SWITCH. my-upg-100-list builds its map RAW
with `new gl.Map` instead of going through useMapInstance, so the page-level
switch — which resolves inside that composable — could not reach it. The card
measured 19 requests to api.mapbox.com at ?provider=maplibre while five sibling
leaves swapped cleanly.

★ IT COUNTS NETWORK REQUESTS BY HOST, not globals. `window.mapboxgl` can be
present because the leaf page preloads it while the map is actually DRAWN by
maplibre, and the reverse is equally possible. What the switch exists for is not
loading a proprietary engine and not calling its servers, and only the request
log shows that.

The allowance is DERIVED from the leaf's own HTML — count the distinct
api.mapbox.com urls the generated page declares, plus one for a stylesheet that
gets fetched twice — rather than hardcoded. Asserting merely "fewer than the
default" would pass a half-fix at 18 of 19; hardcoding 5 would pass a leaf that
preloads 4 and leaks 1 runtime call.

★ THE GATE'S OWN FIRST VERSION MEASURED NOTHING AND PASSED. `const URL =
process.argv[2]` shadowed the global URL constructor, so every
`new URL(request.url())` threw, the host tally stayed empty, and it cheerfully
reported "0 mapbox requests" on a page making 19. It now names the variable LEAF,
counts what it could not parse, and FAILS if the default run records no requests
at all — because a silently empty tally is indistinguishable from success.

Passing on three leaves today: upg-100-list 19 -> 5, doxa-simple-map 21 -> 4,
countries-map 24 -> 4, with every residual being the page's own <link>/<script>
preloads. Confirmed to fail when the allowance is tightened by one.
2026-09-24 04:47:54 +00:00
doxa-maps-88655c70
45354c95ec Say what "none found" means for a Deaf group, because it does not mean the same thing
A sign language cannot be served by a text translation at all. eBible is a text pool, so
its silence for a Deaf group describes OUR SEARCH, not their need — and the sheet was
reporting it as "Scripture · none found" exactly as it does for a spoken language. That
is the map telling a true-sounding thing that misleads.

Sign languages are now flagged in the pin build and the sheet says so: what counts for
them is Scripture FILMED in that sign language, and the text pool was never the right
place to look. Counts are computed, not typed — 150 sign languages on the map covering
84,159,050 people, and the copy reads "none yet has both" only while that is true, since
it is generated from the same pins the layer draws.

THE SCALE: 234 pins on 150 sign languages, 183 of them with nothing found, 0 with both a
film and a text Scripture. 13 have a JESUS Film through Arclight and 1 has Open Bible
Stories through Door43; everything else is empty.

★ A NOTE LEFT IN THE BUILD FOR WHOEVER FILLS THIS GAP. The largest producer of
sign-language Bible content by a wide margin is the Watchtower/JW organisation, which is
excluded by standing rule — added canon. Anyone researching this will meet jw.org first
and it will look like the obvious answer. The legitimate partners are the Deaf Bible
Society and DOOR International, and both need credentials this build does not have:
deafbible.com is a key-gated app, doorinternational.org answers 403. sign.bible is NOT a
source — it is a parked domain listed for sale, which a status-code check would have
happily accepted as live.

No sign-language resource is added here, because none could be verified. What changed is
that the map stops implying the absence was measured against the right pool.

Verified on the served tree with a negative control: the note renders for Deaf Nicaraguan
(Nicaraguan Sign Language, ncs) and does NOT render for German, so it discriminates
rather than always firing. The rendered counts match the build's own tally exactly. Pins,
deep-link, filter, gap-list and language-naming probes all still pass.
2026-09-24 04:39:10 +00:00
doxa-maps-88655c70
c9ed3b4e9a test(mfe): a two-tab element must mount both panes AND occupy space
Some checks are pending
build-gate / build (push) Waiting to run
Gates the composite MFEs at the rendered layer. The failure it exists for is
BUG-UPG100-NESTED-ELEMENTS-ZERO-HEIGHT: every layer inside these elements is
position:absolute/height:100%, so the chain resolves to ZERO unless :host is
sized. The header renders, the tab strip paints, the panes mount — the element
looks alive and occupies no space. A DOM-presence check passes it.

★ THE HOST HEIGHT IS THE LOAD-BEARING ASSERTION, and finding that out cost a
wrong first attempt. I wrote the gate to check PANE heights, which is the
obvious reading of "zero-height collapse" — then measured it: squashing the host
to 0px leaves its panes still reporting 843px, because they are absolutely
positioned and escape the collapsed box. A pane-height check alone does NOT
catch the bug. Only measuring the host does. The pane check stays as a secondary
signal with a note saying why it cannot be trusted alone.

Also asserts exactly one share affordance (two on one surface shipped once as
BUG-UPG100-DOUBLE-SHARE) and that the strip has the expected number of tabs.

Tabs are selected by role=tab, not by class. '.viewsel .seg' matched the tablist
WRAPPER as well as its buttons, reporting 3 tabs for a 2-tab strip with
concatenated text ("Country PostersMap") — the gate finding a bug in itself.

Passing on all three composites today: upg-100-mfe, regions-mfe and the new
country-posters-mfe. Confirmed to FAIL before being accepted.
2026-09-24 04:35:59 +00:00
doxa-maps-88655c70
7edcda368b chore(country-posters): regenerate the tree after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
The rebase conflicted only in GENERATED output — the built bundle, its SRI
hashes, the leaf pages and the manifest — because another agent's work landed in
the same files. My two SOURCE files applied cleanly.

Resolving generated files by hand would have produced a tree that matches
neither agent's source. Taking main's versions and REGENERATING is the only
resolution that makes the output a true function of the merged source, so that
is what this is: one `vite build` plus `npm run tree` after the rebase.

Re-verified after regeneration rather than assumed: check:mfe-two-tab passes on
country-posters-mfe and upg-100-mfe alike.
2026-09-24 04:33:22 +00:00
doxa-maps-88655c70
2d8361a4a0 feat(country-posters): two-tab element, reusing the shared view switcher
Driver: "Country Posters is supposed to have two tabs, structured like the
UPG-100 list."

★ THE CARD'S REAL POINT WAS THE CONTROL, and it was already settled: this is the
THIRD consumer of the SELECT VIEW switcher, and library/components/ViewSelect.vue
already exists precisely because a third hand-rolled copy was imminent — its own
header says a fourth divergent copy is not what the library is for. So this
element imports the shared component rather than adding one, and the whole
feature is one new profile following an established shape.

country-posters-mfe.vue mirrors upg-100-mfe deliberately (index.js already
records that this bundle is "the same shape as my-upg-100-list"): the
pane-wrapper column, each pane under its own ProfileLoader/Pinia, one
ShareButton in the header sharing the COMPOSITE leaf — not a child, which is the
BUG-SHARE-FROM-CAP4-COMPOSITE bug this pattern already paid for.

LIST FIRST, MAP SECOND, per the Driver's principle that maps are a secondary
view: the poster wall is the concrete artefact a newcomer can act on, the map is
where it gets placed. Pane order lives in at[0] and in the array, because
buildPaneColumn sorts by at[0] while ViewSelect renders the array verbatim —
desynchronising them is a recorded trap.

It also CLOSES A DEAD SEAM. country-poster-wall has dispatched
'doxa-country-posters:show-on-map' since it was written, but only the app shell
listened — so in a dropped embed or a nested composite the action fired into a
document with no handler and did nothing. This shell owns the event and swaps to
the map tab, which is what makes the two tabs one element rather than two pages
that share a bundle.

Verified in a browser by the new check:mfe-two-tab: host 900px, tabs
"Country Posters | Map", both panes mounted at 1280x843, exactly one share
affordance, zero page errors. The same gate passes unchanged on upg-100-mfe and
regions-mfe, so the three composites are now held to one contract.
2026-09-24 04:32:05 +00:00
doxa-maps-88655c70
f68ffc33ba Rank the gap, and name the languages that have nothing
The filter shows WHERE the gap is; it could not say which part of it is biggest. The
3,444 groups with nothing were reachable only by tapping dots until a large one turned
up, which is not how anyone chooses a field.

"See the 40 largest" now opens a ranked list in the sheet — the slot the country and
group views already use, so no new geometry and no new collision with the control rail.
Sorted by population, because choosing where to go is choosing how many people are
behind the decision. Tapping a row flies there and opens that group. Fly first, THEN
open: opening sets `selected`, which swaps the sheet away from the list, so the other
order loses the map movement behind a re-render.

THE LIST EXPOSED A REAL DEFECT. The largest gaps are exactly the groups with no entry
in any pool, so they had no record to carry a language name and the sheet rendered
"Language: (blank) cjy" for a 60-million-speaker group. Every language is now named
from the people-group dataset, creating the entry where no pool has one: 2,251 codes
are named-only — the language is known, nothing was found for it anywhere.

That makes "has a code entry" ~100% by construction, so it is relabelled in the build
output as naming and NOT coverage. Coverage is the line under it and still reads 8,935
of 12,379, 72.2%. A metric that reaches 100% because its meaning changed is worse than
no metric.

Verified on the served tree: 40 rows, populations strictly descending 60,310,000 ->
974,000 so "largest" is not a claim taken on trust; tapping row 1 moves zoom 1.3 -> 4.2
and opens Han Chinese Jinyu, whose scripture_url and film_url are empty and whose
Door43 list is empty — so the list is not mislabelling groups that have something. 200
sampled zero-resource groups all name their language, where the screenshot before this
change shows that field blank. Pins, table, deep-link and filter probes all still pass,
and the full a11y sweep passes 810 of 810 buttons across 21 leaf pages.
2026-09-24 04:30:11 +00:00
doxa-maps-88655c70
61dcd9b28e Merge main into bundler-1/work (regenerate the built tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
The nine conflicts were all in generated artifacts under public/. Built
files are not hand-merged: every bundle whose source either side touched
was rebuilt, the tree regenerated against main's new generate-tree, and
the hashes re-baked. Source merged clean — main never touched
PosterWall.vue or en/regions.json, they only appeared in a two-dot diff
because this branch changed them.
2026-09-24 04:28:25 +00:00
doxa-maps-88655c70
ddde518a85 Regions: expand a region to its country posters, reusing the shared wall
Driver request: "a toggle or carrot added to the bottom of the region
section that says view country posters, which will let you view country
posters for each of the countries in the regions" — with View-on-map and
Print, "the same reusable code from upg 100 list". This is a third
sanctioned addition alongside the tab strip and the no-selection chooser.

Almost nothing was built. library/components/poster/PosterWall.vue was
already the shared, fully parameterised grid, and it already owned the
card layout, the "Show on map" header button, the PRINT OPTIONS footer
and the client-side raster. regions-list.vue supplies only data and a
toggle — the same division country-poster-wall.vue states in its own
header.

- A "View country posters" toggle at the bottom of every region section,
  carrying aria-expanded and aria-controls, using the same caret SVG the
  header and footer links already use, rotated when open.
- Opening a region renders that region's countries. Measured: 41 cards
  for Africa and all 41 previews LOAD (naturalWidth>0 — a 404 still
  yields an <img>, so the load is the real check). No assets were
  copied: public/assets/previews already holds 174 country-*.webp and
  generate-tree already stages them.
- "Show Chad on the map" reaches the Region Dashboards tab with title
  "Chad" and hash "#chad", by mapping PosterWall's CustomEvent listId
  back through the country-selected the tabs already exchange.
- India prints its own reason with the exact number from the data; China
  renders a real poster. Only India is unsupported — 855 groups and no
  sheet, against China's 154 on a 3x4.

Two defects the screenshot caught and no assertion did:

- PosterWall's root is position:absolute;inset:0, because in its own
  bundles it IS a pane. Embedded in a region card it painted 1440x1100
  over the region's country list. Fixed by parameterising the shared
  component rather than overriding its CSS locally: new `embedded` and
  `sectionHeads` props, both defaulting to today's behaviour, so
  country-posters and key-cities are unchanged by construction. Both
  re-checked. Re-measured: root static, 1200px wide, panel below the
  grid, no overlap.
- With one unnamed group the wall printed a section head reading the
  literal "Other" plus a count duplicating the region card's own
  summary. Suppressed by sectionHeads=false.

The dataset moved to library/data/country-posters.json now that it has a
second consumer, one import line changed in country-posters, and
emit-countries.mjs was retargeted so the next generator run cannot
recreate the duplicate.

The three new strings do not exist on DX-life, so there was nothing to
copy and nothing was invented in another language: en only, with the
other seven locales falling back to the key.

Regression: 15 suites green, including region-vs-live 73/73,
country-vs-live 28/28, mobile 44/44 and 26/26, and a new
_probe-region-posters.mjs. check:library-index drifted for real this
time (3 new library files) and is regenerated here.
2026-09-24 04:26:29 +00:00
doxa-maps-88655c70
fc04f14a52 Let the key filter the map, so the gap can be looked at on its own
The map could say what one group has, on tap. It could not answer the question it
exists for — show me only the ones with nothing — so the 3,444 groups that still have
no resource were spread through 12,379 dots and could not be seen as a pattern.

THE KEY IS THE FILTER. A reader who has just worked out what a colour means is exactly
the reader who wants to see only that colour, so the legend entries became the controls
rather than adding a second row beneath them. Each carries its live count: 4,711 with
Scripture and film, 4,224 with some resource, 3,444 with nothing — summing to 12,379,
which is the check that the three buckets partition the set rather than overlap it.

FILTERED ON THE LAYER, NOT THE SOURCE. setFilter is a GL-side predicate, so all 12,379
features stay on the GPU and nothing is re-parsed or re-uploaded on a toggle. Rebuilding
the FeatureCollection per click would re-upload a megabyte to redraw a subset it already
holds.

Clicking an active filter clears it, and the state line names what is hidden with a
"Show all 12,379" out — a filtered map that does not say it is filtered is how a reader
concludes the data is missing.

Verified on the served tree: rendered features 12,379 -> 3,444 on the "nothing found"
filter, getFilter returns ["==",["get","tier"],0], and the only tier visible afterwards
is 0. All three buttons expose an accessible name and aria-pressed. The filtered view
shows the remaining gap concentrated in the Sahel, the Horn, north India and Nepal,
Myanmar, Indonesia and the Philippines.
2026-09-24 04:22:40 +00:00
doxa-maps-88655c70
716e7c84eb Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 04:22:01 +00:00
doxa-maps-88655c70
86a3c3bb99 Experimental roundup: FIVE legacy maps resurrected, keyless, in Experimental
Some checks are pending
build-gate / build (push) Waiting to run
The pre-bundler upg-neighbor IIFE holds five lost experimental maps --
send-map, upg-neighbors (cockpit), lang-cluster, bloom-cockpit,
flower-mesh. One wrapper bundle now rescues all five as preset
instances, each mounted in a sandboxed iframe whose host page:
  - loads our self-hosted MapLibre and aliases it as the mapboxgl
    global with a patched Map constructor that rewrites the one
    mapbox:// style to keyless OpenFreeMap Positron -- the whole set
    runs WITHOUT a Mapbox token;
  - shims relative /api fetches to the live API and sets
    MAP_APP_API_URL (the cockpit fails closed without it);
  - passes the legacy profile-config contract (the elements share the
    modern loader lineage).

Wrapper reads profile-config as a PROP (direct-defined element --
inject('profileConfig') is null outside ProfileLoader; the send-map
default masked the miss because send-map is instance #1). The
vocabulary patch rides in the shipped copy ('Lower pressure').

Verified by screenshots read: send-map renders the full 4-D SEND JOIN
app; flower-mesh blooms (India, 2,040 UPGs); the cockpit renders map,
charts and bloom pane. Auto-flows into the SPA's Experimental section
via manifest discovery.
2026-09-24 04:20:24 +00:00
doxa-maps-88655c70
d229034a2a share-policy: default to this repo, not one author's worktree
Identical defect to check-zone-census.mjs, landed minutes apart: line 34
fell back to resolve('/home/lk10x/.../wt-packet'), an absolute path into one
author's worktree. check:no-abs-paths rejects those by design -- "a clone
must work on any machine" -- and it runs inside build:unlocked, so main went
red for every builder again.

Same minimal fix: DOXA_REPO still wins and is untouched; only the FALLBACK
moved to resolve(dirname(fileURLToPath(import.meta.url)), '..'), the idiom
check-no-absolute-paths.mjs prints in its own failure text and every sibling
script here already uses.

VERIFIED both ways: check:no-abs-paths 0, and check:share-policy itself 0
running on the derived default rather than erroring on a missing directory.

This is now the second instance of one pattern in a single tick, so it is
recorded in the rollout ledger as fleet-wide debt rather than just fixed
twice: a new internal/ gate authored in one worktree hardcodes THAT worktree
as its default, and lands red for everyone else.
2026-09-24 04:19:59 +00:00
doxa-maps-88655c70
acc875d444 Merge main into bundler-2/work 2026-09-24 04:18:34 +00:00
doxa-maps-88655c70
b45d35267e zone-census: default to this repo, not one author's worktree
check:no-abs-paths started failing the moment check-zone-census.mjs arrived
from main: line 24 fell back to
resolve('/home/lk10x/.../wt-packet') -- an absolute path into one author's
worktree. That gate's whole rule is "a clone must work on any machine", and
it sits inside build:unlocked, so main went red for every builder rather
than only for whoever lacked that directory.

The override was never the problem: DOXA_REPO still wins and is untouched.
Only the FALLBACK moved, to the idiom check-no-absolute-paths.mjs names in
its own failure text and every sibling script here already uses --
resolve(dirname(fileURLToPath(import.meta.url)), '..'). The data it reads,
plugin-designs/my-upg-100-list/data, is present in this repo, so the default
now resolves for anyone who clones it, and a caller pointing at a different
worktree still exports DOXA_REPO exactly as before.

VERIFIED both directions: check:no-abs-paths 0, and check:zone-census itself
0 running on the derived default rather than skipping.
2026-09-24 04:18:24 +00:00
doxa-maps-88655c70
8e8e659556 test(share): one decided rule for which maps carry a share button
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "Some maps need a map-level share button and some do not... Decide +
record the per-map rule, then apply it."

The BEHAVIOUR was already right on both maps the card names — Prayer Progress
(an instance of doxa-simple-map) has its button, and UPG-100 deliberately omits
one because upg-100-mfe.vue's header owns sharing, with a comment saying so.
What did not exist was the RULE. Nothing stopped the next agent adding a second
button to a surface that already had one, and nothing noticed a published map
with no way to be shared at all.

Surveying all 11 map bundles found FOUR patterns in use, so the policy is
written as the four it actually needs rather than a binary:

  own-button   the map IS the product surface      6 bundles
  header       a composite header owns sharing     my-upg-100-list
  composite    a parent overrides shareUrl so the
               PARENT is shared, not the child     research-map-plus-grid
  none         unpublished, nothing to share yet   key-cities

★ IT FOUND TWO REAL GAPS the card did not anticipate: all-people-groups and
prayer-walk-planner are PUBLISHED and have no sharing path by ANY mechanism —
no button, no shareUrl, no composite parent. They are recorded as
'debt-no-share' so the gate passes on today's reality while NAMING the gap, and
so the count cannot drift without someone deciding. Silently passing them would
have buried the finding; failing on them would block a green build over
pre-existing debt this card never scoped.

A map bundle with no POLICY entry FAILS, so adding a map forces the sharing
decision instead of deferring it. Both negative cases confirmed to fire before
this was accepted: a second <ShareButton> reports "two affordances on one
surface" (that shipped once already), and removing an entry reports the bundle
as undecided.
2026-09-24 04:15:59 +00:00
doxa-maps-88655c70
5d5f162b1c Merge main into bundler-2/work 2026-09-24 04:15:36 +00:00
doxa-maps-88655c70
3e16f67899 Move the vendored font's README out of the served folder
check:public-hygiene went from PASS to FAIL across a merge, with two
findings -- "documentation in a served folder" for
public/assets/fonts/noto-serif-devanagari/README.md and for the copy
generate-tree stages into the tree. It arrived with 467b7f0, "Hindi
unblocked with a vendored Devanagari face". public-hygiene sits inside
build:unlocked, so main was red-gating every builder, not just this branch.

NOT deleted, because the obvious move would have been the wrong one. The
folder also carries LICENSE (SIL OFL 1.1). That file is a DISTRIBUTION
OBLIGATION and has to stay beside the woff2 -- removing it to satisfy a
hygiene gate would trade a lint finding for a licence breach. The README is
developer rationale, and it answers something the files cannot: why the font
is vendored at all, and why the 400 and 700 faces resolve to one variable
file. So it moved to documentation/, verbatim, with a header recording where
it came from and why.

check:public-hygiene 0 after. gates:pre 0, gates:post 17/18 -- the remaining
failure is the pre-existing verify/tables-complete break from the visa-leak
fix, still filed and still not ours.

Also in this tick, after main moved 7 commits including "Regions: one
rollup, ported from theirs" (the same file carrying the MapLibre boundary
branch): merged, rebuilt, re-verified. The branch is intact and the
boundaries still load -- 400 features on mapbox, 467 on maplibre, 0 console
errors -- and the full provider sweep still reads 14/16 with no regression.
2026-09-24 04:15:26 +00:00
doxa-maps-88655c70
f9808d0c8b Harvest the whole Arclight catalogue: film coverage 306 -> 2,087 languages
Same shape as the eBible finding, same cause. Arclight was ALREADY a checked pool and
only 306 language codes carried a film link, while the catalogue lists 2,330 media
languages with 2,087 carrying a feature film. Being inside a pool is not the same as
having been read.

COVERAGE: pins with a real URL 6,985 -> 8,935, 56.4% -> 72.2%. Language codes 2,068 ->
3,196. 1,950 pins lift off "nothing found" and 1,214 move to Scripture AND film. The
resource table grows 1,497 -> 3,043 rows across 2,307 languages.

★ THE VERIFICATION HAD TO BE REDONE, and that is the part worth keeping. The obvious
check is to sample watch URLs and confirm 200. Ten of ten passed. The check was
worthless: https://www.jesusfilm.org/watch/?languageId=<id> is a single-page app and
answers 200 for EVERY id — measured, `0` and `abcdef` both return 200. A check that
cannot fail proves nothing, and that one would have certified 1,781 links on a status
code that is always 200.

The API discriminates where the page does not: media-languages/<id> returns a real
counts.featureFilm for a genuine language and 404 for a bogus one. So existence is
established from the API and the watch URL is the deep link for a language already
confirmed. Re-verified that way: 10/10 sampled languages carry featureFilm >= 1, and
the negative control — a bogus id — returns featureFilm 0, so the check can fail and
does, on the case that should fail.

featureFilm >= 1 IS THE COVERAGE TEST, not roster membership: 21 of the 2,330 listed
media languages have no feature film and are dropped. Only featureFilm is taken;
Arclight also carries short films and other media that were not screened.

FILL, NEVER OVERWRITE, as with eBible — a curated film_url stands, and the catalogue
only supplies a link where there was none. 1,695 of the codes had no row in the
resources table at all.

Verified: all browser probes pass — pins, table, deep link — 12,379 pins rendered, and
the map now reads green across Papua New Guinea, Indonesia, Brazil and Central Africa
where it was red, leaving the remaining red concentrated in the Sahel, the Horn, north
India and Central Asia. The pin table is 1.1MB but 281KB gzipped; the four repeated
pool strings compress 4:1, so interning them would buy nothing on the wire.
2026-09-24 04:08:30 +00:00