Commit graph

1541 commits

Author SHA1 Message Date
Doxa Maps
7869f28ebc Merge branch 'regions/grid-merge' into regions/handoff-integration
# Conflicts:
#	package.json
#	plugin-designs/people-groups-grid/people-groups-grid.vue
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/people-groups-grid/people-groups-grid.js
#	public/dx-maps/people-groups-grid/people-groups-grid/driven/index.html
#	public/dx-maps/people-groups-grid/people-groups-grid/index.html
#	public/dx-maps/people-groups-grid/people-groups-grid/people-groups-grid/index.html
2026-09-29 14:53:55 +00:00
Doxa Maps
844fc60d0e Merge upg100/group-set-standard into development (handoff)
# Conflicts:
#	library/scripts/posters/packet.mjs
2026-09-29 14:53:36 +00:00
Doxa Maps
bf07e0eb61 Merge branch 'regions/country-map-posters' into regions/handoff-integration
# Conflicts:
#	plugin-designs/regions-dashboard/regions-list.vue
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/regions-dashboard/region-dashboards/index.html
#	public/dx-maps/regions-dashboard/regions-dashboard.js
#	public/dx-maps/regions-dashboard/regions-list/index.html
#	public/dx-maps/regions-dashboard/regions-mfe/index.html
2026-09-29 14:53:10 +00:00
Doxa Maps
ed1aa635ce Merge development into the gospel handoff branch 2026-09-29 14:51:04 +00:00
Doxa Maps
bdb74ff69f Merge remote-tracking branch 'origin/development' into bundler-2/handoff 2026-09-29 14:50:51 +00:00
Doxa Maps
506b42fc31 Merge repo/readme-rudder-shot: the Driver's Rudder desktop screenshot in the README (HELD since 01:32Z for his cut-off condition "before you add it to the readme you nee"; merged on the merge-all order) 2026-09-29 14:50:44 +00:00
Doxa Maps
c9ffa34e93 Merge branch 'bundler-2/provide-warnings' into bundler-2/handoff
# Conflicts:
#	internal/check-element-bootstrap.mjs
#	internal/check-provide-shadowing.mjs
#	library/elements/defineMapElement.js
#	library/elements/provide-own.js
#	package.json
#	plugin-designs/all-people-groups/index.js
#	plugin-designs/cities-internet/index.js
#	plugin-designs/country-posters/index.js
#	plugin-designs/doxa-research-map/index.js
#	plugin-designs/gospel-internet/index.js
#	plugin-designs/gospel-resources/index.js
#	plugin-designs/key-cities/index.js
#	plugin-designs/my-upg-100-list/index.js
#	plugin-designs/pplr-data-maps/index.js
#	plugin-designs/regions-dashboard/index.js
#	plugin-designs/upg-neighbor/index.js
2026-09-29 14:50:37 +00:00
Doxa Maps
59345a0aaa Merge gospel/board768 for the handoff (superseded; development's side kept)
# Conflicts:
#	plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue
#	plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue
#	plugin-designs/gospel-internet/src/setChartFeatures.js
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-internet/board/board/index.html
#	public/dx-maps/gospel-internet/board/index.html
#	public/dx-maps/gospel-internet/board/india-china/index.html
#	public/dx-maps/gospel-internet/board/language-tree/index.html
#	public/dx-maps/gospel-internet/board/platforms/index.html
#	public/dx-maps/gospel-internet/board/project-700/index.html
#	public/dx-maps/gospel-internet/board/zero/index.html
#	public/dx-maps/gospel-internet/gospel-internet.js
#	public/dx-maps/manifest.json
2026-09-29 14:50:21 +00:00
Doxa Maps
d6f2f10355 Merge gospel/a5-cpu for the handoff (superseded; development's side kept)
# Conflicts:
#	internal/desktop-build.mjs
2026-09-29 14:50:19 +00:00
Doxa Maps
2e72d59f35 Keep the reasons, drop the speaker, in the two merged branches
The consolidation brought back two Driver citations from older branches that
the reference sweep had already cleared from development. Both keep their
engineering reason and lose the speaker: a gate's rationale for existing, and
the mechanism of the print collapse.

Caught by check:no-new-driver-references, which is the point of running the
full chain rather than the seven gates the merged package.json had left.
2026-09-29 14:46:07 +00:00
Doxa Maps
964b4c4ccf Merge branch 'bundler-2/rm-plugin-only' into bundler-2/handoff
# Conflicts:
#	plugin-designs/doxa-research-map/research-map.vue
2026-09-29 14:39:50 +00:00
Doxa Maps
4571596cf1 Merge branch 'bundler-2/grid-lazy-images' into bundler-2/handoff 2026-09-29 14:39:13 +00:00
Doxa Maps
5ff4da6460 The three Regions roots resolve against their own host box, not the page
Each of these shadow roots is position:absolute; inset:0, while its :host had
display:block with no position. An absolutely positioned element resolves against
its nearest POSITIONED ancestor, and an unpositioned host is not one -- so the
root fell through to whatever the partner page happened to have, or to the
viewport. It only looked right standalone, where the page fills the viewport
anyway, and inside a composite's .pane, which is itself positioned. A partner
embedding the element directly gets it painted outside its box.

All three are the shadow root's own root element, with only comments between
<template> and them, so none of them has a positioned ancestor to fall back on:
  regions-list.vue:408      .rl-root  at :409
  region-dashboards.vue:733 .rd-root  at :734
  regions-mfe.vue:194       .rmfe     at :199

dx-shepherd's census named the first as confirmed and the other two as
candidates to check. Checked: both are roots, not children of something already
positioned, so all three are real.

The fix is the house pattern -- position:relative on :host -- matching the
already-fixed cities-internet/connectivity-map byte for byte in shape, and
prayer-walk-planner.vue:628.

One line changed per file. Verified in the ARTIFACT rather than the source: the
rebuilt regions-dashboard.js carries all three corrected host rules (two at
min-height 320px, one at 420px). check:sfc-parse, check:synced-css and
check:one-scroll-container pass; the manifest diff is integrity hashes only,
0 other lines.
2026-09-29 14:38:56 +00:00
Doxa Maps
0ab2fe7c87 The production Country Map honours the host's people-group list
country-map.vue fetched /api/people-groups/list and mapped the posts straight to
points, without applyPeopleGroupList(). Every other consumer of that endpoint
calls it -- DataSourceManager, useDashboardData, useCsvExport, peopleGroupRecords
and the people-groups grid. So a host that asked for one curated list got the
WHOLE endpoint drawn on its country map, and nothing said so: the pins looked
like data rather than like the wrong list.

The list is applied to the raw posts, before they become points, which is where
every other consumer applies it. That is what keeps this element and a dashboard
showing the same country from disagreeing about which groups are in it -- the
same reason the country filter beside it is client-side.

The default list returns the rows untouched and makes no fetch, so an
unconfigured host pays nothing. A list whose membership cannot be loaded yields
NO groups rather than the wrong ones: applyPeopleGroupList's own contract, and
the safer failure for a prayer surface.

Measured three ways, with a temporary copy of check:universal-props that adds
country-map to its BUNDLES list (the reason the gate never saw this: 19 bundles
listed, this one absent):
  - RED on the base: BYPASS country-map LIST-FETCH country-map.vue:98, exit 1;
  - GREEN here: 20 bundles read engine, token, locale and list through the
    library, exit 0, the gate's 9 planted-bypass self-tests still caught;
  - a real list value changes the drawn count, through the shipped
    applyPeopleGroupList and the served membership files: Nepal draws 34 points
    on the default list, 29 on "frontier", 0 on "frontier-500k". That last zero
    is data, not a failed load: the same membership matches 2 India rows.

Rebuilt on this base rather than carrying the earlier bundle, because
development rebuilt the same files in between and generated output is
regenerated, never merged. The list-membership URL is present in the built
bundle; the manifest diff is integrity hashes only, 0 other lines.
2026-09-29 14:38:56 +00:00
Doxa Maps
ce40a1eb87 Rudder's corner panel kept its notices above the draft, and they never yielded
Two defects, one cause: on a short panel the engine-offline notice took the room
the draft needed.

FIRST, A PHONE RULE THAT NEVER RAN. bac2431d0 (19:59) put notices below the
draft "so the draft stays in view". 4e94e9216 (21:58) added the corner widget
with `.compact .notice { order: 0 }`. A media query adds NO specificity, so
`.compact .notice` (0,2,0) beat `@media { .notice }` (0,1,0) at every width and
the phone rule has been dead since the day it landed. Ordering the compact rule
to 1 fixes the widget at every width, not only under 480px, because the corner
panel is short regardless of the viewport.

SECOND, ORDER ALONE WAS NOT ENOUGH, which I only found by doing the arithmetic
rather than trusting the first fix. .notice is flex:0 1 auto and .compact
.compose is flex:1 1 0, so the draft yields whatever the notice's content asks
for no matter where it sits. The offline notice is ~132px at 375 and ~179px at
390 — reference text a reader needs once, holding a third of a short panel
forever. Capping it at 96px with its own scroll gives the draft 58px at 375x450
and 124px at 390x560, against rega's measured 22px and 41px.

The panel is 60vh (plugin-studio.vue:466), NOT the viewport. Sizing off the
viewport is what made my earlier estimate wrong by 58px; the model that uses the
real panel reproduces rega's 22px exactly.

No floor anywhere: a floor is what clipped the draft to begin with.

(cherry picked from commit 79cbb41bc3)
2026-09-29 14:38:53 +00:00
Doxa Maps
4dbeb257ab The gospel data table escaped its host box when embedded alone
Same defect as cities-internet's list and table: no :host rule at all, so the
element falls back to display:inline with no height while .gt is
position:absolute;inset:0 — which then resolves against whatever the partner
page has positioned.

TWO MOUNTS, ONE RULE, and they do not collide:
- STANDALONE (data-tables.meta.json) this is its own custom element, so :host
  applies and .gt is finally contained.
- COMPOSED, gospel-dashboard renders <DataTables/> as an inline Vue component
  with no shadow host of its own, so :host matches nothing there. Its
  '.gd-tables .gt{position:static;…}' override (gospel-dashboard.vue:81) keeps
  working, which is what lets the stacked section take its natural height and the
  page scroll. That override records a MEASURED failure — docScrollHeight 900
  against clientHeight 900, a wheel gesture moving 0px — and must not return.

min-height, not height alone: with no height, .gt's inset:0 would give the table
0px in an auto-height box, so it would stop escaping by becoming invisible. A
floor gives an auto host something to grow from; a definite-height slot still
decides through height:100%.

NOT the ProfileLoader 100dvh floor (ProfileLoader.vue:410), which is the separate
upstream cause of the connectivity-map overflow and is studio's card. .gt is not
that wrapper, so this fix stands on its own.

(cherry picked from commit 10f2a5c13f8d08c0086ada8eafa4ad819863bd0a)
2026-09-29 14:38:46 +00:00
Doxa Maps
eec16a8fd8 The list and the table painted thousands of pixels below their host box
Neither element had a :host rule at all, so each fell back to the custom-element
default — display:inline, no height of its own — while its root div carried
position:absolute;inset:0. Absolute positioning resolves against the nearest
POSITIONED ancestor, so on a partner page both resolved against the PARTNER's
layout rather than against this element.

rega measured it on a partner page with a 420px host box: connectivity-list
painted to y=16,799 and data-tables to y=1,698 — 16,355px and 1,254px below the
box, over whatever the partner had underneath.

The same fix as connectivity-map's (60cb4ff18), for the same reason:
  :host{display:block;position:relative;width:100%;height:100%;min-height:320px}
display:block gives the element a box; position:relative makes that box the
containing block; height:100% lets a definite-height slot decide; min-height is
the floor so an auto-height host grows to fit instead of collapsing.

NO NEW SCROLL CONTAINER WAS NEEDED. .ci and .ct already carry overflow-y:auto —
the scroller existed, it simply had no host to be bounded by. Once contained,
the list and the table scroll inside the box, which is what an embed should do
in a definite-height slot.

min-height is 320px here, not the map's 420px: regions-list.vue:408 is the house
pattern for list profiles, and a list is legible shorter than a map is.

(cherry picked from commit 08ad95fa86e64dfe043befb156e358c91319e511)
2026-09-29 14:38:46 +00:00
Doxa Maps
1208fdce71 The production Country Map honours the host's people-group list
country-map.vue fetched /api/people-groups/list and mapped the posts straight to
points, without applyPeopleGroupList(). Every other consumer of that endpoint
calls it -- DataSourceManager, useDashboardData, useCsvExport, peopleGroupRecords
and the people-groups grid. So a host that asked for one curated list got the
WHOLE endpoint drawn on its country map, and nothing said so: the pins looked
like data rather than like the wrong list.

The list is applied to the raw posts, before they become points, which is where
every other consumer applies it. That is what keeps this element and a dashboard
showing the same country from disagreeing about which groups are in it -- the
same reason the country filter beside it is client-side.

The default list returns the rows untouched and makes no fetch, so an
unconfigured host pays nothing. A list whose membership cannot be loaded yields
NO groups rather than the wrong ones: applyPeopleGroupList's own contract, and
the safer failure for a prayer surface.

Measured three ways, with a temporary copy of check:universal-props that adds
country-map to its BUNDLES list (the reason the gate never saw this: 19 bundles
listed, this one absent):
  - RED on development: BYPASS country-map LIST-FETCH country-map.vue:98, exit 1;
  - GREEN here: 20 bundles read engine, token, locale and list through the
    library, exit 0, the gate's 9 planted-bypass self-tests still caught;
  - a real list value changes the drawn count, through the shipped
    applyPeopleGroupList and the served membership files: Nepal draws 34 points
    on the default list, 29 on "frontier", 0 on "frontier-500k".

Rebuilt, tree regenerated, SRI refreshed. The list-membership URL is present in
the built bundle; the manifest diff is integrity hashes only, 0 other lines.
2026-09-29 14:35:28 +00:00
Doxa Maps
a92f03dd43 Key Cities: a row's languages read as a list in the page's grammar, and a shortened one says so
The row meta joined its first three languages with English commas and cut the rest silently, so a truncated list read as complete. It now goes through formatList (Intl.ListFormat) as a conjunction whose LAST item states the cut, from keyCities.list.moreLanguages ("{n} more", landed in R29): "Hindi, Urdu, Bengali, and 2 more" / "Hindi وUrdu وBengali و2 أخرى" / "Hindi、Urdu、Bengali和另外2种". Three or fewer languages show as they are. Not 'unit': in zh it joins names with no separator, and in ar and hi it still adds a conjunction (studio, measured).
2026-09-29 14:29:20 +00:00
Doxa Maps
1d78d83a98 PPLR and UPG Neighbor: two lists read in the page's own list grammar, through formatList
join(', ') is English list grammar. The two user-facing sites now go through the library's formatList (Intl.ListFormat, R29), as conjunctions because each is the FULL list:
- PPLR's Rosetta popup, "Not yet cross-mapped in PPLR: {ids}";
- UPG Neighbor's bridge-language cards, "Believers in: {list}".
Key Cities' shortened language list follows once its "{n} more" key (keyCities.list.moreLanguages) is on development. The joins that stay are not reader lists: a thrown error's details, the bridge-languages CSV columns, and a gate's console line.
2026-09-29 14:29:19 +00:00
Doxa Maps
a6482b9f5d Join the UPG-100 map's name lists the reader's way, not with ', '
Two reader-visible lists were joined with a hardcoded ', ': the ADM1 names
in the selection error ('no people-group data for {names}') and the
selected territories in the batch HUD. Both use formatList now (R29), so
de reads 'Gujarat, Rajasthan und Odisha', zh 'Gujarat、Rajasthan和Odisha',
ar 'Gujarat وRajasthan وOdisha'. The locale is passed from the i18n ref
(packetLang), not left to getRuntimeLocale, so the HUD's list re-renders
on a runtime language switch. formatClipboard's join stays: its only
caller is a test handle.
2026-09-29 14:21:57 +00:00
Doxa Maps
090b4eeb16 Research dashboard: filter lists read as lists in the reader's language
Three places joined names with ", ", which is a list only in English. Arabic
separates with a word, Chinese uses a different mark entirely and no comma at
any position, and German and French put a word before the last item. The
separator and the final connector are the grammar, so no catalogue key can fix
them from outside.

Two of the three carried a second, worse error. A multi-value filter holds
ALTERNATIVES -- the bus matches a row if ANY of its values matches, in
useSelectionBus with some() and in the data table with includes() -- so the
chip means "Islam or Hinduism". Joined with a comma it reads as "and" in every
language with an explicit connector, which states the opposite of what the
filter does. Those two become a disjunction.

The third is the screen-reader live region, which names the facets currently
applied. Those hold together and narrow the set in turn, so it is a
conjunction. It is also the one string here that some readers rely on entirely,
having no chips to glance at.

The composable is the single owner: every surface that turns a filter into
words comes through it, so the chip bar, the research-filter chips and the CSV
provenance line now say the same thing in the same language.

Left alone deliberately: five joins in this plugin's gate scripts. Those are
failure messages read in a terminal by whoever runs the gate, not text shown to
a reader, and formatting them per locale would help nobody.

Source only. This needs the bundle rebuilt before it ships.
2026-09-29 14:08:04 +00:00
Doxa Maps
c5e2131fc0 Two lists joined with an English comma in every locale
formatList (R29) replaces join(', ') at the two sites that a reader sees. The
separator was never a missing key — no key can fix punctuation between
interpolated values — which is why both survived every i18n gate.

connectivity-map.vue:365, CONJUNCTION. The Indian states with no boundary are
ALL missing one. The names land inside a translated sentence at :595, and the ar
message reads "لا توجد بعد حدود ولاية لـ {names}: …", so English commas were
being printed inside Arabic prose.

GiCombos.vue:49, DISJUNCTION, decided from the matcher and not from what reads
better: useSelectionBus.js:299 tests cur.includes(value) and toggleFilterValue
adds to the list, so two values on one facet WIDEN the result set. "Africa and
Asia" would state the opposite of what the filter does. This changes the English
too — "Africa, Asia" becomes "Africa or Asia" — which is a gain, because a comma
silently reads as ALL.

Measured, the same three names and two facet values through each type:
  en  "Ladakh, Puducherry, and Chandigarh"   "Africa or Asia"
  ar  "Ladakh وPuducherry وChandigarh"        "Africa أو Asia"
  zh  "Ladakh、Puducherry和Chandigarh"        "Africa或Asia"
  was "Ladakh, Puducherry, Chandigarh"        "Africa, Asia"
ar joins with و and no comma at all; zh uses 、. Neither is reachable by keying.

Not converted, and deliberately: useBoard.js:126 is a console.warn for
developers, and rudder-voice.vue:971 is in a component with zero i18n imports and
no catalogue area — English by construction, so it converts with the rest of
Rudder if Rudder is ever localized.

Source only. The rebuilds are regenerated after this rebases onto landed R29.
2026-09-29 14:01:38 +00:00
Doxa Maps
60cb4ff184 The embedded map still painted 478px below its host box
73bc270bc gave :host a height and stopped the ELEMENT overflowing. It did not
stop the MAP, because it never made the host a containing block.

.gm is position:absolute;inset:0, which resolves against the nearest POSITIONED
ancestor. :host had display:block and a height but no position, so on a partner
page .gm resolved against whatever the PARTNER had positioned and the map sized
itself to that. rega measured it in the hostbox run on ca07fc3c8: the element
correct at 420px inside a 420px box, the canvas inside it still 900px, and the
"Tap a coloured dot" pill and the attribution painting 478px below the box —
538px in a 360px box — over the partner's own text, whose top sits at y=464/404.

position:relative on :host is the whole fix, and it is the house pattern:
prayer-walk-planner.vue:628 carries the identical shape with an absolute child.

Worth recording, because the first fix passed every check: a host box that is
the right size proves nothing about what is drawn inside it. Only a partner-page
shot with content BELOW the box can see this, which is why rega's hostbox run
found it and my source reading did not.
2026-09-29 14:01:38 +00:00
Doxa Maps
201baa3d60 A filter chip with several values says "or", in the reader's language
With no resolver configured, the kit chip bar joined a multi-value filter with ', ', so "Africa,
Asia" read as ALL to most people and carried English commas in every locale. The selection bus
matches several values on one facet with includes() (useSelectionBus isFilterActive), so adding a
value WIDENS the result: they are alternatives. The chip now uses formatList(value, { type:
'disjunction' }): "Africa or Asia", "Africa أو Asia", "Africa或Asia". gospel traced the matcher
and made the call as the only consumer's owner (gospel-internet's GiCombos). The English on this
chip changes from a comma to "or".

check:legend-mirrors lifts this exact expression and drives it with no resolver. It now lifts
formatList from library/i18n the same way (the shipped grammar, not a copy) and expects
"Islam or Hinduism". Poisoned with the old join it fails; restored it passes. formatList's doc now
says to choose the type by what the list MEANS, and never 'unit' for names.
2026-09-29 13:29:19 +00:00
Doxa Maps
8793d47f0e countries-map paints idle regions with the library's regionIdle, not its own copy
useLegendMapBridge.js now exports regionIdle (sr, pair 1). The local
function is deleted and the name imported beside the bridge's other
exports; all 15 calls keep their values, the dynamic regionIdle(v)
included. Built bundle: the idle expression occurs exactly once before
and after (the local copy before, the library's after), 1 byte apart;
the country-posters composite still mounts. check:region-paint-single-
source PASS. Also drops a comment left behind when clearCountryHighlight
moved to the same library module.
2026-09-29 13:29:18 +00:00
Doxa Maps
1fde4b9cc4 The regions idle paint expression moves to the library that already painted with it
useLegendMapBridge took the idle paint helper as an injected dependency and
called it five times, while each consuming map defined its own copy of the
one-line body and handed it over. The library owned the paint logic and not the
expression it painted with, so the copies could drift apart silently and the
two maps would render the same idle state differently.

Export it from the composable instead, and take it from there in this map. The
injected parameter is deliberately KEPT for one round: inside the composable
the parameter shadows the module export, so what the bridge paints with is
still exactly what its callers pass, and this change cannot move behaviour. It
is a deletion in the plugin and an addition in the library, nothing more. The
parameter goes when the second map has adopted the export too.

This map held a THIRD copy under a different name -- a function-scoped helper
in the paint prewarm, byte-identical body, used once. It now refers to the
library helper.

Verified rather than assumed: the library version and both original plugin
versions produce identical output for every value the maps actually pass,
including the dynamic one. check:region-paint-single-source is green for this
map and stays red for the other until it adopts the export, which is the state
the gate is meant to report.

The second map is the other plugin's file and is not touched here; its owner
takes that half.
2026-09-29 13:29:18 +00:00
Doxa Maps
f50b641b28 The Wall's and the template's proofs become guards: four two-sided headless checks, in the plugins' own scripts/
Until now these ran from _qa-cycle, so they proved a landing once and guarded nothing after it. Each now lives with its plugin, locates the repo from its own path, and fails on its known-bad:
- wall-of-people-groups/scripts/check-wall-wysiwyg.mjs: the screen poster and the print are one layout on every preset (sheets, 2,107 card rects to 0.002 in, name size and wrap, reference size, chrome blocks). Planted: a card moved 0.01 in, a name a point larger, the footer missing on screen. About 0.6 s.
- wall-of-people-groups/scripts/check-wall-print-watch.mjs: the HD print counts its photos while they load, counts the ones that could not load, finishes at load or the 10-minute ceiling, and ends when its frame is gone. Planted copies of printWatch.js now go to the OS temp folder, never into the tree. About 0.3 s.
- wall-of-people-groups/scripts/check-wall-no-bg-photos.mjs + fixtures/pray-list-sample.json: the real store (through Vite) never requests a photo on its own, a group with no photo URL gets none, and one with a URL keeps it. Planted: the old background test, added through the store's $onAction. The store from before L12 (d9e9aa448) fails both arms: 12 photo requests in 3.5 s and 3 groups on the 404 placeholder. The fixture is a trimmed sample of the live list: 200 rows (120 with has_photo, 77 without, 3 synthetic with no image_url), only the fields the check reads, 66 KB. About 10 s.
- template-bundle/scripts/check-template-i18n.mjs: profile B renders from template.json in en, ar and zh, one sentence in each language's own order with both <code> islands, and no raw key. Planted: the English catalogue served as Arabic. About 4 s.

All four are headless: no browser seat.
2026-09-29 13:14:46 +00:00
Doxa Maps
a4622781a2 Key Cities: all nine region headings read in the page's language, and the region stays the key
This corrects my own "Other" fix (9d12e59b6), which could never fire. gospel's cities-internet analysis (87a168195) showed the same shape here: key-cities.json has no row with an empty region (1,646 rows), "Other" (2) and "N/A" (19) are region VALUES, and every section heading printed its raw English data string in every locale. The fallback I translated also put a label into the grouping key.

Now, as in cities-internet: the raw region string stays the grouping key (sections, :key, collapse state), with 'Other' as the data guard; the heading is projected at render by regionLabel.js from the keys the data's strings already are the English values of (dashboard.wagfRegion.*, dashboard.other, wall.na), written out in full so the build ships those areas. The poster wall, whose PosterWall prints its section name as given, groups by that projected heading, as country-posters' wall does. keyCities.posters.otherRegion is no longer used.

Headless (_qa-cycle/exp-shepherd/check-keycities-regions.mjs): every one of the nine region values maps to its catalogue heading in ar, zh, es and ru, and an unknown value passes through as data. key-cities is gated, so nothing in public/ changes.
2026-09-29 13:14:46 +00:00
Doxa Maps
b1c4c03956 Four resource labels on the pin sheet read English in every locale
The chips a reader sees on a group's sheet — "Scripture,", "Bible video",
"Training", "Gospel presentation" — were plain template text. All four already
had keys with real ar and zh translations, so this routes them through
gospel.res.* and adds nothing: scripture, bibleVideo, training,
gospelPresentation.

WHAT IS DELIBERATELY LEFT ALONE, because it is not a label:
- g.audio.m keeps its raw value and its 'audio' default. That is the table's own
  media descriptor, data rather than UI text.
- gospel-resource-rows.js is already correct. add(title, type, …) translates
  title through tx() and keeps type as the category identifier the rows are
  grouped by; the gate flags those type strings, but translating them would
  break the grouping — the same value/label trap as the region headings.
- d.s.replace('Open Bible Stories', 'Bible Stories') at :2754 rewrites a DATA
  value, not a label.
- data-tables.vue:122 already carries labelKey; its `en:` is the fallback beside
  it, by design.
- The caveat prose (Driver row 80) and the title= tooltips (row 120) are
  untouched and stay that way until those rows are answered.

check:hardcoded-english on gospel-resources: 91 -> 87, which is these four.
2026-09-29 13:08:53 +00:00
Doxa Maps
488d34836a Wall of People Groups: the level-of-detail hook reports the zoom it actually draws at
rega's L12 photo probe read host.__wallLod().cardPx as 0 after 46 wheel ticks: the field was set once when the cards were first laid out and never again, since the canvas renderer replaced the LOD component that used to update it. The canvas now records one card's on-screen width at every frame it draws (stats.cardPx, both the grid and the poster), and the hook returns that. The container's header comment no longer claims Font Awesome is loaded: the canvas draws its own icons, and d3 is pinned and kept off window.d3 (src/wallD3.js).
2026-09-29 12:35:22 +00:00
Doxa Maps
87a1681956 Nine region headings read English in every locale, not just "Other"
rega saw "Other" in ar and zh. The reported cause — `c.region || 'Other'` at
:65 — is not one: of 175 rows in connectivity.json NONE has a falsy region, so
that fallback never fires. The "Other" section is one row (United States) whose
region IS the string "Other". Patching :65 would have changed nothing on screen.

The render site is `{{ sec.name }}`, and it printed the raw data value for ALL
NINE regions, so this was nine English headings rather than one.

All nine already had translated keys — dashboard.wagfRegion.* for the seven WAGF
regions, dashboard.other, wall.na — in all 11 locales. None needed adding.

THE LABEL IS PROJECTED AT RENDER, NEVER AT GROUPING. sec.name keys the Map, the
:key, and the collapse state, so translating it where the rows are grouped would
reset every open section on a locale switch and filter on a value no row carries.
That is the value/label trap useFacetAggregation.js documents at OTHER_KEY; this
follows the same shape.

The name->key map is built by asking the ENGLISH catalogue what each key says,
rather than writing the English phrases here: the data's region strings are the
catalogue's own English values, so English is the join and never has to be
duplicated. Written the other way it added two check:hardcoded-english findings
to a file that had none — an English phrase used as a lookup key reads exactly
like a user-visible string.

tStatic(path, 'en'), not t(). t()'s signature is (key, params, plural), so
t(k, 'en') passes 'en' as params and returns the RUNTIME locale's value: the map
would be built from Arabic values and match no region at all.

The keys are written out in full because internal/i18n-areas.mjs bundles an area
only when its name appears literally in a reached file. cities-internet shipped
14 areas and neither dashboard nor wall; assembling these at runtime would have
left all nine unresolved in the shipped bundle while working in the dev tree.
Measured both ways — written out, the bundle takes 16.

`|| 'Other'` stays as a data guard so a future row with no region cannot key the
Map on undefined, and 'Other' is the sentinel dashboard.other projects.

9 of 9 headings translate in ar and zh, verified against the real catalogues.
2026-09-29 12:24:45 +00:00
Doxa Maps
53ed778d71 PPLR Data Maps: the fullscreen button is named in the page's language
rega's L15 (e7f343d4c, ar + zh): every PPLR map view's fullscreen button read "Toggle fullscreen" in English, title and aria-label. It is PPLR's own control (FullscreenToggleControl in src/utils/pplrMap.js, not MapLibre's), and it wrote the English itself. It now takes the shared buttons.toggleFullscreen, already translated in all 11 locales, when the map is built.
2026-09-29 12:20:03 +00:00
Doxa Maps
9d12e59b60 Key Cities: a row with no region is headed in the reader's language, not "Other"
cities-list.vue grouped region-less rows under a hardcoded 'Other' section heading, a single English word, which is the hardcoded-english gate's blind spot. It now reuses keyCities.posters.otherRegion (all 11 locales, landed in R25). The same literal in cities-internet's connectivity-list, where rega's R25 look found it in ar and zh, is gospel's and goes to gospel. The SSR check (check-keycities-i18n.mjs) passes; key-cities is gated, so nothing in public/ changes.
2026-09-29 12:20:03 +00:00
Doxa Maps
c00b0b4b5d Gospel x Internet uses the library's whiteboard map instead of its own copy
boardEngine.js was createWhiteboardMap under another name: the bodies are
identical line for line, and every import it made already came from library.
It is deleted and the four components import @map/maps/whiteboard-map.js.

The copies were NOT interchangeable, which is the part worth recording. Their
DEFAULTS are opposites, because the library file was extracted for the
whiteboard case and boardEngine served both map kinds:

  boardEngine  whiteboard:false boundaries:true  centre [60,20] zoom 1.4
  library      whiteboard:true  boundaries:false centre [0,0]   zoom 1

Three of the four callers pass all four options explicitly, so they were never
at risk. GiMap — the geographic map — passed only a token and took every
default. Swapping its import alone would have set whiteboard:true, hiding every
basemap layer, dropping boundary attribution and moving the camera: a blank map
in production, from a change that reads like pure de-duplication.

So GiMap now states its four options. Every effective option is unchanged from
today's values, verified field by field, and the call reads the same against
either module.
2026-09-29 11:56:46 +00:00
Doxa Maps
5095d798b8 research-dashboard, research-map, big-picture: keep the reasons, drop the speaker
Twenty-six references in capitals, which the earlier case-sensitive pass
could not see. Every requirement, date and measurement is kept: the 2026-09-23
instruction that removed the engagement and status filters, the MapLibre ask
with its Mapbox-compatibility condition, the blue-water-black-galaxy style,
the red ramp and the bin that collided with the heartbeat zone, the white
rings around the blue dots, the zoom that did not work inside the bounded map.

Two that are not quotes and were handled differently. The reduced-motion gate
opens with a medical reason — a reader of this map is photosensitive, and a
camera that sweeps the globe on every filter is a strobe — which is the whole
justification for the gate, so it stays as a fact about a reader rather than
about a person. And BpResearchHeader's "CAT-1 DRIVER CONTRACT" was a code
term; it is now the CAT-1 HEADER CONTRACT, checked for other references first.

Eleven lines broke and were found by re-reading every changed line, not by the
count. Removing an attribution left seven continuation lines holding the
closing half of a quotation that no longer opened, and four sentences carried
a pronoun for the speaker who had just been removed. The reference count was
correct at every point while the prose was wrong.

grep -wi driver across the three plugins: 0. The widened gate reports 40
swept, 14 still baselined. All six SFCs parse.
2026-09-29 11:33:39 +00:00
Doxa Maps
62c3bb27e7 PPLR Data Maps: the chrome a reader operates comes from the catalogue
Chrome only, as scoped: the controls, status lines and navigation, not the data labels or the research notes. 192 English strings → 155; the 155 left are data and notes by that decision:
- the Semantic Trees reference (85: sample tree rows, API field names, taxonomy levels);
- the data-source panel entries (layer names, FeatureServer paths);
- the language family names and the tree level definitions;
- the index tiles' descriptions and source line;
- the cross-system ID names;
- five CSS/SVG values the scanner reads as prose.

What changed:
- The ten view names were written twice, English beside their tKey. The dead English goes, and the bar's brand is pplr.title.
- The theme and projection buttons on the three map views said "Theme: Dark (click to toggle)" / "Switch to Globe". They now say what they do, from the shared keys buttons.switchTo{Light,Dark}Mode and mapui.{enable,disable}Globe, as both title and aria-label.
- The sunburst's status is a key in state (pplr.status.d3Failed / readyD3), translated where it shows, so it follows a language change. Its count uses formatCount like the other views.
- The name-locales view's pill (pplr.pill.translations) and its data-sources button and header (pplr.ui.dataSources) are keys.
- The coverage scale's "0 locales" stop reads 0, like its other stops; the legend title names the unit.
- The index's title and help line are keys, and its tiles are named by their views' own names. Its old help line pointed at an Overview button that no longer exists.
2026-09-29 10:59:04 +00:00
Doxa Maps
70b64e9608 Key Cities: every string on screen comes from the catalogue
22 English strings outside the catalogue. 10 were dead duplicates: the filter, tab and band labels were already translated from keyCities.* in the templates, so the English copies go. The rest now come from keys:
- the list's caveat is three translated lines, and its long sentence is ONE key rendered with useSentence, with the named-centre count and "Nominatim/OpenStreetMap" passed in as values (the name as its own left-to-right island, never translated);
- a row's group count reuses keyCities.list.peopleGroups, and the unknown internet pill reuses keyCities.map.unknown;
- a load failure reads "The data could not be loaded: {detail}" in the reader's language, with the server's own words as the detail (list and tables);
- the poster wall's intro and its "Other" region come from keys.

Headless, SSR with the real catalogue (_qa-cycle/exp-shepherd/check-keycities-i18n.mjs): the caveats render from keyCities in en, ar and zh, the gazetteer's name stays a <bdi dir="ltr"> island, no raw key or placeholder. The experimental English scan: key-cities 22 → 0. key-cities stays gated (dev only); it builds.
2026-09-29 10:59:04 +00:00
Doxa Maps
10275c36f8 Template bundle: every string on screen is a key, and the template shows the three habits that keep it translated
The starter every new plugin copies had 7 English strings outside the catalogue: profile A's "map engine not loaded" and all of profile B. Now profile A keeps its error's KEY in state (errorKey) and translates it in the template, with the engine's own message passed in as a value; profile B's heading, its note line and its sentence are keys, and "Served from {file} in the {bundle} bundle." is ONE key rendered with useSentence, the file and bundle names passed in as their own <code dir="ltr"> islands so each language orders the sentence itself. t() is called in the template throughout, never frozen into a constant at setup. The README's pieces table and "Your own text" section say the same three habits.

Headless, SSR with the real catalogue (_qa-cycle/exp-shepherd/check-template-i18n.mjs): profile B renders in en, ar and zh from template.json, with the sentence in each language's own order (zh puts the bundle first), both islands intact, and no raw key or placeholder. The experimental English scan: template-bundle 7 → 0.
2026-09-29 10:59:04 +00:00
88312a24b8 Keep the reason, drop the speaker, in the four capitalised comments
check:no-new-driver-references matched "Driver" case-sensitively, so lines in
capitals passed unseen -- and my own greps were blind the same way, which is why
I reported this plugin clean twice. A case-insensitive sweep finds four, and one
of them carries a verbatim quote.

All four keep their engineering reason and lose the speaker:
 · the reuse check states the negotiating position rather than whose it is;
 · the substituted button font is blocked on a licensing decision, not a person;
 · the code, not just the screenshot, is required to match;
 · the table's ring and gridlines live in a global stylesheet, which is why
   reading RegionDetail.vue alone could not show them -- the misspelt verbatim
   quote is gone, the finding it pointed at is not.

Verified with grep -wi rather than the grep that missed them: 0 remain across
regions-dashboard and country-map. The four reasons are each still present by
their own text. sfc-parse, mjs-parse and no-new-driver-references all pass.
2026-09-29 10:51:10 +00:00
Doxa Maps
2b70cc1aa5 Seven more references, in capitals, that a case-sensitive grep could not see
The gate matched "Driver" only, so "THE DRIVER'S ASK" and "HELD FOR THE DRIVER"
passed unseen — and so did my own verification, which used the same
case-sensitive pattern and reported zero. I checked the thing the gate checked
rather than the thing that mattered.

Seven lines across six files: five banner headings, one quoted requirement, and
the row-130 marker. Same rule as the first sweep — keep the reason, drop the
speaker — with the verbatim quote in build-doxa-gospel-access.mjs restated as
the question the map answers, since its content is the requirement and its
framing was attribution.

Comment-only: 0 non-comment lines changed, tree regenerates identically.
Case-insensitive grep across all three plugins now returns nothing.
2026-09-29 10:45:01 +00:00
Doxa Maps
792f25c8c8 No 'driver' in any case: the capitals lines and code terms keep their meaning in other words
check:no-new-driver-references matched 'Driver' only, so lines in capitals
passed unseen (and so did my own grep -w Driver). In my-upg-100-list:
'BATCH-UNION DRIVER' and 'gate drivers' are test handles, so they say so;
'in its driver' is generate.mjs's main routine; the zoning declarations'
'PENDING DRIVER' and 'In DRIVER-DECISIONS-DIGEST' become a pending product
decision and the product-decisions digest. grep -rni driver across both
plugins: 0.
2026-09-29 10:38:56 +00:00
Doxa Maps
cfeadca398 Delete the legend's never-returned parent nodes, and point its proof at the real parents
Since the regional grouping, buildLegendNodes assembles the tree from the
LEAVES under the poster page's nine region sections. The old India, China,
"Asia (admin-1)" and "Frontier Asia (uncovered)" parents were still built
and never returned: dead code whose English labels the call-site triage
read as reader text. They are deleted, with sumCount, which only they used.
The legend's output is byte-identical for all four configurations
(with/without records, with/without the localizing options; 2,107 records).

The L15 shape assertion now composes its text inside the throw: the
offending leaves are collected as data and formatted in new Error(...), so
the message is visibly developer text. Same message, checked on bad input.

pop-proof.mjs had been failing on every run, because section 3 looked up
those same dead parents, and section 1 expected their count (32). It now
rolls up the nine region parents the tree actually has (each pop = the sum
of its children, and there must be at least one) and expects 28 leaves + 9
regions = 37. ALL PASS, identically against the previous legend-tree.js.
2026-09-29 10:38:56 +00:00
Doxa Maps
2cde9c0918 people-groups-grid: keep the reasons, drop the speaker
Fourteen references, each rewritten to state the requirement directly. Every
date, measurement and pixel coordinate is kept — the y=1827/y=1865 redundancy
that prompted the count line, the 2026-09-22 interoperability report behind
the Language and Status props, the 220x130 letterbox the square image
replaced, the dev-server path that showed "view more information" reaching
the wrong page, and the India click that left the grid unchanged.

Thirteen were found by a case-sensitive search and a fourteenth was not: a
line in capitals, carrying a verbatim quote, which `grep -w Driver` cannot
see. The check that catches both is `grep -wi driver`, and it now reports 0.

Two sentences broke in the first pass and were found by re-reading every
changed line rather than by the reference count: one left "the control the"
with its object removed, and one left a quotation with an unclosed quote when
the surrounding attribution went. The count was correct while the prose was
wrong, which is why the count is not the check.

check:no-new-driver-references passes. research-map-plus-grid and
dashboard-plus-grid both rebuild byte-identical.
2026-09-29 10:30:59 +00:00
Doxa Maps
3ad1a8fc37 UPG Neighbor on a phone: the attribution sits above the sheet, a pick raises the sheet, and the hover tip stays on screen
rega at 390 (b07fc59e0, en + ar, both views): the sheet lowers and raises as it should, but
- the map's attribution control sat under the sheet, lowered or raised, in all four cases. On a phone the map's bottom control corners now sit above the sheet: 55% + 6 px when it is up, 62 px when it is down;
- a pick with the list lowered moved the map but showed no detail, because the detail is in the sheet. A pick (a group, a neighbour dot, a bridge language, a group opened from Campaigns) now raises the sheet FIRST, so the frame is computed for the sheet that shows the detail;
- the hover tip was cut off at the right edge in ar ("Ko…"): it opened to the right of the point wherever the point was, and after a tap it stayed where the tap had been while the map flew to the pick. It now opens toward the side with more room, is at most 45% of the map wide, and clears when the map moves.
2026-09-29 10:22:12 +00:00
Doxa Maps
404b2c866e Fourteen English strings in a .js module reached readers in every locale
gospel-resource-rows.js builds each resource row's display text, and its
fallbacks were English literals: a record with no name of its own showed
"Scripture in this language", "Audio Scripture", "Listen in a browser" to an
Arabic or Chinese reader — precisely the rows whose source data was least
complete. check:hardcoded-english does not read .js files, which is why the
bundle's sweep never saw them.

Thirteen keys in eleven locales, and the translator is PASSED IN rather than
imported: the module is a pure function with no component context, and it stays
optional, so a caller without one degrades to the English written at the call
site instead of throwing.

Three publisher names stay verbatim — JESUS Film, Scripture Earth, Zume — but
where a name sits inside an ordinary phrase the phrase is keyed and the name is
INTERPOLATED, so "Zume discipleship training" translates around a brand a
translator never retypes. SE_KIND's values are displayed and take keys; its keys
are the upstream feed's codes and must not be touched.

Verified by running the module both ways: without a translator every row reads as
it does today; with an Arabic one the labels and scopes are Arabic while
"Scripture Earth" and "Zume" are unchanged.

ROUTED TO STUDIO, not self-landed: the eleven locale files make this a library
change. The route test says 11, and the last time I judged a landing by which
plugin the work was for rather than which directories it touched, the scan ran
against most of the fleet and timed out twice.
2026-09-29 10:07:29 +00:00
Doxa Maps
75ea0adec0 doxa-maps-spa: its comments say what the code does and why, not who asked for it
The shell's comments carried 23 of the reference baseline's lines, one more written in capitals that
the baseline's pattern never saw, and four internal card ids. Each is reworded to keep the engineering
reason: the 404s, the grey pane, the scroll trap, the device rule and the curated order all stay
explained. Quotes became the rule they stated.

Comments only. The script (esbuild, whitespace and syntax only, no renaming), the template (compiled
without comments) and the styles (comments removed) are identical before and after. The baseline
shrinks by the 23 lines that are gone.
2026-09-29 10:07:28 +00:00
81d57f1954 A country map with no centre starts, instead of blaming the token
<doxa-country-map> sent center: null when the host set none, and null is not
absent. Vue applies a prop default only for undefined, so an explicit null
REPLACED RegionCountryMap's [20, 10] default rather than falling back to it. The
null reached the Map constructor, and the engine's LngLat.convert threw before a
style or a tile was ever requested.

So any host that gave countryCodes without a centre got a dead map -- and the
page told them it was "a map-token or connection problem", which blamed the
wrong thing entirely and sent two rounds of investigation after providers and
tokens. Both center and bounds are documented OPTIONAL in this element's
contract, and the manifest's own embed snippet sends neither, so a partner
following our published snippet got nothing.

MEASURED on a host page across twelve cases: MapLibre selected correctly in all
of them, 0 requests to mapbox.com, 0 responses >= 400, 0 failed, 0 unanswered --
and no request to the tile host at all, because the map never got as far as
asking. The console carried the real cause the whole time.

The element now sends undefined, which is what "the host set none" means. That
alone restores the default and starts the map; the defensive guards inside
RegionCountryMap go separately as a library change, because they protect a
different caller rather than this one.

Probe reads the shipping source and states Vue's own rule independently, since
that rule is what makes this a bug. It reports whether the library guards are
present rather than requiring them, so it stays honest on either tree.
2026-09-29 09:54:07 +00:00
dd7230a029 Name the country cards, and give India's reason, in the reader's language
Two untranslated strings on a production print surface, both reported by the
Country Posters plugin after it fixed the same pair on its own wall.

THE CARD NAMES. postersFor returned the country-posters.json record unchanged,
and that record carries its label in English only -- so every card with a poster
was named in English in every locale. label is not decoration: it is the card's
accessible name, the print HUD's title, and the title on the printed list, deck
and packet. It now goes through countryName(iso3, locale) for a non-English
reader. English keeps the data's own names, because Intl's English differs from
them for 32 of these 175 countries and that swap is a recorded product decision.
The sheet generator still receives the original record; the poster itself is
English by the same decision. The locale REF is read, not a snapshot, so a
runtime language switch renames the cards.

INDIA'S REASON. posterUnsupported returned the record's own English note, so one
untranslated sentence sat on the wall in every locale. The record still decides
WHETHER a country is unsupported; only the wording now comes from the catalogue.
The key needs no number in it because the card shows the count directly above.

★ AND IT NEEDED A SECOND TRANSLATOR, which is the part that would have shipped
broken. This page's t() is useRegionsI18n's: it resolves DX-life's English
sentences as keys inside the `regions` namespace, because that is what the page
is cloned from. messages.countryPosterTooMany lives in another area, so t()
would have missed and rendered the literal string "messages.countryPosterTooMany"
on the wall. tKey is vue-i18n's own lookup, added for keys outside this page's
namespace.

Verified in the built bundle rather than the source: DisplayNames present,
countryPosterTooMany present. A probe over the real alpha-2 table confirms the
localisation across ar/ru/zh/de/fr and that an unknown iso3 falls back instead
of throwing. gates:pre 75 PASS / 0 FAIL / 1 NOT MEASURED of 76; i18n-parity and
i18n-keys-exist both pass. Cost +244 B.
2026-09-29 09:54:06 +00:00
Doxa Maps
e028682dd1 The draft's last line is unreachable when a notice makes the header tall
FIX-RUDDER-PANEL-DRAFT-LAST-LINE-UNREACHABLE-WITH-TALL-HEADER.

.compact .compose carried min-height:120px, so the column refused to shrink and
whatever the header took was pushed past the panel's clip. The draft's own
scrollbar cannot help: the overflow is OUTSIDE the scrollable area, which is why
a wheel over it reaches the end of its content while the last line stays hidden.

Measured in the corner widget with the engine offline, header 237px:
  390x560 — panel leaves 99px, the floor forces 120, overflow 21px.
  375x450 — panel leaves 33px, overflow 87px.
Both reproduced independently, and both are exactly 120 minus the room available,
which is what makes the floor the mechanism rather than a coincidence.

Pre-existing: present on plain development as well as on my earlier branch, so
this is its own defect and not something a previous round introduced.

The floor is removed unconditionally. A viewport-height guard would not work —
what consumes the room is the HEADER, and a notice can make it 237px at any
viewport; an earlier version of this fix guarded above 520px and would have left
the 560px case still broken.

Cut clean onto development rather than carried on the branch it was written on:
that branch also held two hunks that were deliberately withdrawn after an A/B
showed they changed nothing measurable, and landing it would have quietly
reinstated them.
2026-09-29 09:41:48 +00:00
Doxa Maps
07eb74c33c Move four identical bodies into the library
From the fleet census: 620 function bodies indexed across 29 plugin trees,
16 identical in two or more. These are the four cheapest — none closes over
anything beyond its own parameters, so each is a file move plus an import
rather than a new dependency to thread.

hostElement resolves the custom element a component is mounted inside, one
hop out through the shadow root. It was identical in three plugins, and it
is the piece worth sharing for a reason other than line count: every plugin
ships through defineCustomElement, a composite nests two shadow roots, and
code that forgets the hop operates on the wrong node while looking correct.
A document-level query for a node inside a shadow root returns null on a
working build, which reads as a broken feature rather than a wrong query.
Three copies were three chances to write that.

findScrollportAncestor walks up to the nearest ancestor that actually
scrolls, crossing shadow boundaries on the way — parentElement stops at a
root, so a plugin in a custom element would otherwise find no scrollport and
conclude the page does not scroll.

findNodeInTree and buildFlatLegendMatchExpr join useLegendMapBridge, which
already holds four functions from the same seam. They sit at module level
rather than inside the factory because they close over nothing: a pure
function hidden in a closure can only be reached by constructing the whole
bridge.

Call sites keep their old names through a one-line local binding, so no
call changed and the diff is the declaration plus an import.

Rebuild-and-compare, all five affected bundles, before -> after:
  regions-dashboard   1,098,469 -> 1,098,479   +10
  country-posters     1,484,053 -> 1,484,028   -25
  doxa-research-map   1,354,051 -> 1,354,061   +10
  dashboard-plus-grid   915,660 ->   915,644   -16
  big-picture           366,907 ->   366,719  -188
  net -209, and country-posters gains 25 bytes against the 1.5 MB ceiling.
Every behaviour-bearing token counted, not set-compared, and unchanged in
all three map bundles: regions-fill, peoplePraying, engagementStatus,
adoptionStatus, overflowY, scrollHeight, getRootNode, _flat_filter, religion.

Owners cleared their own files first: regions for regions-list.vue, upg100
for countries-map.vue. check:selection-kinds, check:legend-matches-source,
check:legend-data-contract, check:packet-variants, check:no-dead-library and
check:no-cross-plugin-imports all pass.
2026-09-29 09:19:04 +00:00
Doxa Maps
f96fb4290f A desktop-only composite blocks on a phone too: the phone rule is a composable its root can call
rega measured dashboard-plus-grid's own leaf at 390px: the page carried
"mobile": "desktop-only" in its profile-config, and the dashboard, the grid
and a MapLibre canvas mounted anyway. The rule lived in ProfileLoader, and
this composite defines its element without ProfileLoader (it bakes its
children in), so nothing on that page read the flag.

- library/composables/useScreenGate.js holds the rule (narrow screen, not
  inside the Gallery, dismissed for the session); ProfileLoader uses it,
  unchanged in behaviour.
- dashboard-plus-grid calls it: on a narrow screen the desktop-only message
  replaces the element, and the dashboard and grid never mount.
- check:mobile-gating (g): every flagged profile's element must mount
  through ProfileLoader (an import in its index.js, not a mention in a
  comment) or call useScreenGate itself. The pre-fix composite fails it.
2026-09-29 09:18:55 +00:00
Doxa Maps
5e81b05281 check:no-free-identifiers reads internal/ and .mjs, which is where two gates had crashed on load
A commit on 09-25 gave some 30 browser gates `const ROOT = …(_fileURLToPath(…))`.
Two never imported it, and crashed with a ReferenceError the moment they
loaded: check-legend-info-localized (found by hand after four days, fixed
this morning) and library/scripts/posters/verify-posters.mjs, found now by
this change and fixed here. Both run only by hand, so nothing noticed.

The gate read only .vue and .js under plugin-designs/ and library/. It now
reads .mjs as well, and internal/. Scanning them surfaced standard globals
the list lacked (eval, EvalError, Bun, and the window members that
page.evaluate() callbacks use), which are added. A file tagged @one-shot
is skipped: it is kept as a reproduction recipe, not run.
2026-09-29 09:18:26 +00:00
Doxa Maps
77889d3df9 Name each Country Posters card in the reader's language
item.label is the card's accessible name, the print HUD's title and the
title of the printed list, card deck and packet, and country-posters.json
carries it in English only, so every locale printed "Chad" and "India".
Cards are now named through countryName (Intl, joined from our ISO-3):
all 175 countries localize in ar, zh and ru. English keeps the data's
own names; Intl's English differs for 32 of them, and countryName.js
records that swap as a product decision. The list reads the i18n locale
ref, so a runtime language switch renames the cards.

The generated poster sheet still receives the data's own English record
(the poster surface is English by recorded decision), and its series
index is found by id, because a language switch rebuilds the cards.
check:poster-browser-equiv still passes, byte for byte.
2026-09-29 09:10:27 +00:00
Doxa Maps
2067233553 Re-render the UPG-100 legend and wall names on a runtime language switch
ProfileLoader switches the i18n locale mid-session, and two things the i18n
batch localized were resolved once and then kept:
- the legend tree, built through legendOpts() and stored, so every region
  and zone name stayed in the boot language (as did the flat zone list and
  the search feed built from its labels);
- poster-wall's card names, a computed that read only getRuntimeLocale(),
  a plain getter, so it never re-ran.
check:frozen-labels cannot see either: t() runs inside a function, and the
computed has no t() in it at all.

One buildLegend() now serves all three build sites and remembers the last
records; a watch on the locale rebuilds the tree, relabels the flat zone
list in place (colours and selectors unchanged) and rebuilds the search
feed. The wall's computed reads the i18n locale ref, so it re-runs.

The first-status watch (ac5a14ffa) now watches packetLang() rather than packetLocale:
the host may install i18n with a plain-string locale, and watch() on a non-ref never fires.
2026-09-29 09:10:27 +00:00
Doxa Maps
538e2fb782 Keep the reason, drop the speaker, in the UPG-100 and Country Posters plugins
The 40 baselined references in my-upg-100-list (32) and country-posters (8),
plus 3 unbaselined ones in data/*.js, each wrapped an engineering reason in
an attribution. The reason stays; the speaker, the quotation marks and the
pronouns go. Dates and measurements are kept where they were ("decided
2026-09-24", "Reported 2026-09-24: the UPG-100 list legend on the map
overflowed beyond the height of the map"). Where a quote carried a
requirement, the requirement is now stated directly.

Comment-only, one line for one line (73 against 73), including the
meta.json note, which does not reach the manifest. Both bundles build
byte-identical to the base. Route test: 0 files under library/ or
internal/.
2026-09-29 09:10:26 +00:00
Doxa Maps
4ea988a4c9 Ten facet labels froze in the boot language
GiCombos built its facets object inside provide() at setup, so T() ran once and
the result was handed to the consumer as a literal. After a live switch to Arabic
every facet name stayed in English — the labels were keyed, the keys resolved,
and the reader still saw the wrong language, because what was wrong was WHEN they
were read rather than whether they were translated.

check:frozen-labels cannot see this: every call site passes a real key and every
key exists in all eleven locales. The defect is a lifetime, not a string.

The facets are a computed now, provided through a getter so a consumer reading
.facets re-reads it rather than taking a snapshot. Demonstrated rather than
asserted: with a locale ref driven en -> ar, the old shape keeps returning
"Access" and the new one returns "الوصول".

Audited all 179 t() call sites across the three plugins by enclosing scope, not
by grep. This was the only frozen one; the six GiDrawer hits my first pass
flagged are inside a computed 60 lines up, which is why a lookback window is the
wrong tool for a lifetime question.
2026-09-29 09:06:28 +00:00
Doxa Maps
6baf6dba65 Keep the reason, drop the speaker, across all three plugins
The 36 baselined references in gospel-internet (23), gospel-resources (9) and
cities-internet (4) each wrapped a real engineering reason around an attribution.
The reason is what a reader needs; the speaker is process vocabulary that does not
belong in shipped text.

Every one is rewritten in place, keeping the measurement and the date where they
were there: "NUMERALS ARE HELD fleet-wide (2026-09-26)", "(2026-09-27: one more
level of zoom-out was needed)", "Reported from use: it overlapped the bottom of
the page". Where a quote carried the requirement, the requirement is now stated
directly rather than quoted.

Comment-only: 36 insertions against 36 deletions, one line for one line, and no
built text changes. Route test passes — 0 files under library/ or internal/, so
this self-lands rather than going through studio.

Two things I nearly shipped by accident, both caught before committing:
`git add -A` swept in a 1,961-line vite.config timestamp artefact from my own
build, and editing the JSON meta through a parser reformatted "composes" from one
line to three. The artefact is deleted and the JSON is edited as a string, so the
diff is only what I meant to change.
2026-09-29 09:06:28 +00:00
Doxa Maps
c2473d0e41 exp's maps keep the reason and drop the speaker: no comment quotes or cites the Driver
21 lines in seven of exp's bundles (upg-neighbor 4, prayer-walk-planner 8, doxa-simple-map 2, pplr-data-maps 2, wall-of-people-groups 1, doxa-vision-card 1, all-people-groups 3): each keeps its engineering reason, its dates and its measurements; the speaker and the quotes go (a quote becomes a plain statement of the defect). Comments only. Five bundles rebuild byte-identical; doxa-simple-map and pplr-data-maps change only in their Vue scoped-style ids, which hash the SFC source.
2026-09-29 08:56:44 +00:00
Doxa Maps
7e309d1777 Wall of People Groups: the poster print counts its photos while they load, and says how many could not load
The HD poster prints on its frame's 'load', which waits for every photo: up to 2,107 photos, 132 MB, which can take minutes on a slow connection, and a photo that fails prints as a grey box without a word. The print button now reads "Preparing the poster: n of 2,107 photos" while the frame loads, and if any photo could not load, a toast says how many print as grey boxes. The counting is src/printWatch.js (it never decides when to print; the frame's own 'load' does). It finishes at 'load', at the frame's 10-minute ceiling, or at once if the frame is gone (closed), so the busy button never outlives its frame. The busy state's fallback moves from 120 s to that ceiling, because the frame is now kept until it has loaded (printInPlace, exp-shepherd/print-floor).

Headless, fake timers and a fake frame (_qa-cycle/exp-shepherd/check-wall-print-watch.mjs): progress climbs as photos load; 3 photos with no pixels are counted as failed; a load that never ends finishes once at the ceiling and not before; a frame removed at 60 s ends the watch within a tick; 4 planted defects (failures not counted, no ceiling, no progress, a removed frame holding the watch) each caught.
2026-09-29 08:56:44 +00:00
Doxa Maps
ac5a14ffaf The UPG-100 map's first status re-resolves on a language switch: it was set once at setup, which check:frozen-labels refuses 2026-09-29 08:39:07 +00:00
Doxa Maps
a3e58ac7ca Serve the packet text at print time instead of bundling all 11 languages
A single-file build inlines every import, dynamic ones included, so
packet-strings.json (~115 KB minified: every language's prayer points,
captions and headings) rode in every bundle that can print a packet, list
or card deck. With the wall's print messages translated, country-posters
crossed the 1.5 MB ceiling (1,502,226 bytes).

The table is now served at <assets>/lib/packet-strings.json and fetched at
the first print by library/packet/packetStrings.js, the same move as the
sheet generator and ne-places.json:

- Pinned like them: PACKET_STRINGS_SHA384 in poster-render.integrity.js
  (pin-poster-render.mjs); the text is hashed before it is parsed, and an
  unmatched table is not used. These strings are laid into the print
  document, so an unverified copy never reaches it.
- English is bundled as the fallback (a named import of the JSON, so only
  the en block ships): with no asset base, on file://, on a network error or
  an integrity mismatch the print comes out in English and the console says
  why. A print never fails for want of this file.
- generate-tree copies it into the pool AND the served allowlist; verified
  by regenerating the tree (the served copy is byte-identical to source).
- check:poster-render-pinned, two-sided: the pin matches source and served
  copy, the genuine copy loads all 11 languages, a tampered copy falls back
  to English (mutation-tested: disabling the hash check fails it), and an
  unserved one falls back rather than failing.
- sha384 is one shared helper (library/packet/sha384.js).

Sizes: country-posters 1,378,463 (was 1,484,053), my-upg-100-list 1,143,456
(was 1,243,509), regions-dashboard 973,802 (was 1,079,448).
2026-09-29 08:33:44 +00:00
Doxa Maps
03839179b7 Speak the reader's language on the UPG-100 map, its wall and the print HUD
The legend's region sections, its Deaf row and its one-country zones, the
wall's card names, the print progress and error messages, the map's status
badge and the Population column all rendered English in every locale. None
of them was visible to check:hardcoded-english: they were single words, .js
modules, Error() arguments, library components or number formatting.

- Legend: region rows through one REGION_KEY table shared with the wall
  (poster.region_*, new region_deaf); zones through zoneTitle (tier A by
  country, the Deaf row by its bloc name); node ids stay English-derived.
- Wall: card labels through zoneTitle, so the Show-on-map name, alt text,
  tooltip, HUD title and printed title agree with the legend and packet.
- Errors a reader sees are keyedError(key, English, params);
  errorText(e, t) shows them translated, and an unkeyed error still shows
  its own message. Developer detail for a failed data load goes to the
  console; the badge says upgMap.mapDataFailed.
- Population: Intl compact notation in the app's locale, same precision.
- Country Posters: the unsupported card's reason is
  messages.countryPosterTooMany, not the data file's English note.
- Strings only the UPG-100 map shows move to a new upgMap area, so other
  bundles do not carry them (country-posters 1,490,907 bytes).
- check:hardcoded-english judges keyedError's English as a fallback
  (quiet when the key resolves everywhere, a finding when it does not);
  check:label-key-coverage follows REGION_KEY to legend-tree.js and covers
  the legend's table and its regionFor producer. Both self-tests two-sided.
2026-09-29 08:33:43 +00:00
Doxa Maps
2795455688 The legend toggle, the chart titles and "no code" were never translated
Found while classifying the remainder rather than by the gate pointing at them:
the search box's aria-label was hardcoded English — screen-reader text, the same
class as the resize-handle bug that check:i18n-keys-exist documents — along with
the legend's open/close label, the three gap-chart titles, "unnamed language",
the unchecked-link notice, and "no code", which appears at six call sites and is
now one key.

Nine keys, sixteen call sites. 102 -> 88.

These were hiding inside the 102 I had described as mostly-deliberate. The
classification was worth doing for its own sake: four of the five classes really
are deliberate, but the fifth had real defects in it, and I would not have found
them by reading the count.
2026-09-29 08:11:50 +00:00
Doxa Maps
04b38560d1 The loading lines and the caveat lead-ins get their own keys
Five more keys: the table's loading line, the language sheet's population and
countries line, and the two bold lead-ins that introduce the caveat prose in
resources-list. The caveat prose itself stays inside its dir="ltr" island, which
the gate already marks deliberate — only the heading that introduces it is
language.

Not touched, and not oversights: "· GRN", "· Zume" and "· Door43" are a separator
plus a publisher's name, on lines whose translatable half is ALREADY keyed. A
brand name after a middot is not a sentence.

106 -> 102.
2026-09-29 08:11:50 +00:00
Doxa Maps
bfefb87d4f The list sheets count their own rows in one key
"Showing {shown} of {total}." appears twice, for the with-resources and
without-resources lists, and is ONE key used twice. The population suffix and the
related-language line join it; "way in:" reuses the key the gap list already has
rather than authoring a second one for the same words.

Three new keys, five call sites.
2026-09-29 08:11:50 +00:00
Doxa Maps
f24a9e2531 The data-tables hero is one key too
Four fragments around three counts, two bold words and a bdi island — "people
groups are", "people,", "an estimated", "deaths per day." — become one message
with named slots, the same shape as the charts hero.

Verified through a real createI18n: en renders as today, ar carries the clause
order Arabic needs, zh reorders too. The counts and the quoted phrase go in as
values, so the bold and the dir="ltr" survive without a translator reproducing
markup.

115 -> 111.
2026-09-29 08:11:50 +00:00
Doxa Maps
e08d0552d3 The resource sheet's lines are sentences carrying their counts
Twelve more keys across resources-map: the two resource chips ("Scripture,
{medium} · {source}" and "Bible video · {source}", where the type word is
language and the source is data), the sheet headings that carry a count, the
country line, the pray count, and the loading notice.

The separators stay out of the catalogue. A "·" between a type and a source is
structure, not prose, and a translator asked to reproduce it would eventually get
it wrong in a language that does not use it — so the punctuation lives in the
message and only the words vary.

124 -> 115 findings. The ratio is back to roughly one finding per key now the
duplicated spans are gone, which is what I said would happen once the one-off
sentences began.
2026-09-29 08:11:50 +00:00
Doxa Maps
6914f44b2e The gap list's lines are sentences, so each is one key
resources-map's gap list read as fragments around its values: "{count} people ·
{iso3}", "way in: {via}", "has {what} — no text found", and a "Showing only …"
whose bold phrase came from a four-branch conditional. Each was a sentence broken
around the data it carries, which is the shape that cannot be reordered.

Nine keys, and one of them does three jobs: the population line appears three
times in near-identical sibling spans, so it is ONE key used three times rather
than three keys — the duplication was in the template, not in the language.

The four filter descriptions are their own keys and slot into the sentence, so a
translator can move "Showing only" around the phrase rather than being handed two
halves to join in English order.

144 -> 124 findings for eleven keys. The ratio is better than the proving case
because the duplicates collapsed; it will worsen again on the one-off sentences.
2026-09-29 08:11:50 +00:00
Doxa Maps
c7aa670426 The gap sentence is one key, so a translator can move its clauses
charts.vue's hero read as six fragments concatenated around three counts and a
bdi island: "of", "languages have", "— the languages of", "people." Arabic does
not put those clauses in that order, so translating the fragments would have
given Arabic words in English order — grammatically broken, and worse than
leaving it English, because it looks translated and nobody re-checks it.

Now one message with named placeholders through useSentence. The counts and the
quoted phrase go in as VALUES, so the bold and the dir="ltr" survive without a
translator ever reproducing markup, and keep() hands the placeholders back to t()
intact — without it the lookup blanks them and the sentence renders with no
values at all.

Proven through a real createI18n rather than in isolation, which is the lesson
from the keep() trap: en renders as it does today, and ar and ru both LEAD with
the object and put the counts after — the reordering that fragment-by-fragment
translation cannot express. zh reorders too.

The gate moves 144 -> 142 for this one sentence. That is the honest number: six
fragments collapse into one key, but the gate counts findings, not fragments, so
the count falls more slowly than the work proceeds. ~26 fragments remain.
2026-09-29 08:11:50 +00:00
Doxa Maps
592880ac75 Teach the gate the two classes gospel classified by hand
gospel found 12 real defects — including a search box's aria-label, which is
screen-reader text — inside 88 findings they had already described as mostly
deliberate. None were visible from the count. They classified every remaining
finding by hand and named the two shapes the gate miscounts.

A string compared with === against table data is a value, not a label.
`scope === 'Full Bible'` tests a cell of the shipped resource table; the
reader never sees that literal, and the row it selects renders through a key.
Translating it makes the comparison miss and the feature stop, while the
count falls and it looks like progress — the same inversion as a lookup-table
key, which is why gospel classed them together.

A separator followed by a brand is not a sentence. `{{ t('…') }} · GRN`
splits into a keyed half and the text node "· GRN", and only the second
reaches this scan.

The names are LISTED, not patterned. "Punctuation then a capitalised word"
would swallow "· Not found in any pool", a real caveat, and that is the class
gospel's 12 defects were hiding in. A name earns its place by being a
publisher or standard that is the same word in every language.

Each quiet case is paired in the self-test with the nearest thing that must
stay loud: a caveat after the same separator, prose that merely begins with a
brand, a real English sentence used as a table key, and an .includes()
argument. Poisoning the name list into a punctuation rule fails.

Poisoning the operator check did NOT fail, which is why there is now a
concatenation case: `greeting + 'Welcome to the prayer map'` is a
BinaryExpression too, so accepting any operator would silence a sentence
assembled for the reader. Only an equality test means "compared against
data".

Also the last finding in this owner's plugins. All seven main tabs resolve
through tabs.<id> in all eleven locales, so their English labels could never
render — a second copy of each name that could only drift. The sub-tab tables
lost theirs the same way.

Fleet 152 -> 143. This owner's plugins: 1 -> 0.
2026-09-29 08:11:49 +00:00
168b1f1bd9 Country posters print their list, deck and packet, with the map drawn at the click
The regions list showed a poster per country and could print the poster alone.
Supplying a records provider is what makes the Prayer List, Card Deck and Prayer
Packet buttons appear at all -- PosterWall hides them when a wall cannot supply
rows, rather than failing at click time -- and this wall never supplied any.

Records come from the shared provider, not a copy of anyone's fetch. It caches
one request per language and every country in every region shares it, so opening
five regions is still one request, and the same rows feed the sheet, so the
printed map and the printed list cannot disagree about which groups are in a
country.

Page 1 is GENERATED, because there is nothing to load. No country sheet ships:
data/posters holds 28 .html and all 28 are zones, and emit-countries.mjs writes
only country-posters.json, whose `sheet` field is a paper size rather than a
filename. So the packet's default lift asked for posters/country-<iso3>.html and
404ed on all 175. country-sheet-print.js draws it instead, in the same shape as
the zone sheet next door so the two surfaces cannot draw the same sheet two
ways, and returns '' when there is no country or no rows so the caller falls
through rather than printing an empty country.

There is ONE loading path for the generator, not two: this calls
generateCountrySheet, and posterSheets resolves the served, sha384-pinned
poster-render.js itself. Nothing here fetches or imports the generator directly.

MEASURED against the development baseline, both built here: 1044 -> 1061 KB,
+17 KB. An earlier measurement of this same wiring on the pre-pinned base read
+208 KB; almost all of that was the printing subsystem that is now served rather
than bundled, so the cost fell by about 190 KB without this file changing.
createPosterGenerator does not appear in the bundle.

item-count-key replaces nothing: the noun pair still names the section, and the
count now goes through the plural-aware key.
2026-09-29 08:07:36 +00:00
0bedb889ba PARKED: the country map's own poster surface, opt-in, waiting on on-demand print code
<doxa-country-map> gains the poster, prayer list, card deck and packet that the
regions wall has, for the one country it draws. The surface is opt-in through
profile-config (showPosters, read only as literal true or 'true'), off by
default, behind v-if so an embed that never asks for it emits no markup, and
loaded through retryableAsyncComponent rather than a static import.

NOT LANDABLE YET, and the measurement is why. Built element, default OFF:

  baseline          528,901 B
  with the surface  930,692 B
  delta            +401,791 B

The lazy load does not help. This repo builds each bundle as a single IIFE with
inlineDynamicImports, which flattens an async component exactly as it flattens a
plain import(): PosterWall, packet-render and createPosterGenerator all appear in
the built element with the flag off. my-upg-100-list's lazy detail panel shows
the same pattern, so this is the build shape rather than this wiring.

So a country page that never turns the surface on would still carry ~400 KB of
printing code. This element exists precisely so such a page need not load the
regions dashboard, so that trade is the wrong way round. It waits for the print
subsystem to be served on demand, the same change that took the regions wiring
from +208 KB to +17 KB once the sheet generator moved to a pinned asset.

countrySheet.js moves to library/packet/ HERE rather than in the wiring commit:
its second consumer is this parked surface, so until this lands the file has one
live consumer and stays plugin-local. The wiring lands plugin-only.

Holds either way: records and sheet come from the shared providers, never a copy;
the single poster item resolves from country-posters.json, with an
unsupported-reason row for a code that has none; poster-action is print
throughout; no host is switched on.
2026-09-29 07:48:18 +00:00
Doxa Maps
c0ffb13f86 check:no-free-identifiers counts __DX_LOCALE_SRI__ as build-bound: the locale plugin prepends it, and its one read is typeof-guarded 2026-09-29 07:31:17 +00:00
Doxa Maps
155c774b22 Share the legend-map bridge between the two 811-line twins
countries-map.vue and research-map.vue share 1,756 lines across 60
contiguous blocks, and 78% of that is one concern: the wiring between a
semantic-tree legend and the map beside it. The two files already import
the same 41 library modules and the same five legend-data composables, so
the data layer was never the duplication. The glue was, and glue is where
a fix lands in one copy and not the other: the packet kind-split, the
poster attribution reading and the packet page-1 map hand-off each shipped
fixed in one twin and broken in the other.

Only the four functions that are BYTE-IDENTICAL in both files move:
clearCountryHighlight, _flatItemsToStlNodes, onLegendRowClick and
_bridgeSelection. Measured, not assumed — applyDimFilter (185 lines against
170) and switchTab (49 against 50) genuinely differ and stay where they
are. Sharing code that merely looks similar is how a refactor picks one
plugin's behaviour for both.

A composable rather than a component: the legend markup already lives in
SemanticTreeLegend and its two shells, and a component would have forced
both plugins to give up the template where their real differences live.
Nothing is injected or reached for, so a second dashboard can adopt it
without inheriting research-map's provide tree.

Both calls sit after countryOutline and pplrInstance. The declarations they
replace were hoisted and ran above those consts; a composable call is not
hoisted, and placed too early it would read undefined at the first call
rather than at import — the legend would stop highlighting with no error.
The gate asserts both the dependency list and that ordering, for every
caller.

Rebuild-and-compare on both bundles: research-map 1,237,385 -> 1,237,709
bytes, country-posters 1,550,988 -> 1,551,317, and every behaviour-bearing
token identical in both — regions-fill, regions-border, fill-opacity,
fill-outline-color, line-color, line-width, visibility,
doxa:legend-row-click, _flat_filter, iso_3166_1_alpha_3. Counted rather
than set-compared, because minified output renames freely.

104 lines out of the plugins, shared lines 1,756 -> 1,702.
2026-09-29 07:31:17 +00:00
Doxa Maps
ae6d60780b Send the research map's live map to packet page 1 through the mapImage seam
Page 1 keeps the live map rather than a fetched sheet, and it was handed
across as a bare <img> in mapSvg — raw markup the renderer interpolates.
The page-1 redesign no longer draws that full page, so the map arrived
shrunk inside a layout built for a stored sheet. It reads as a styling
quirk rather than a wiring mistake, which is why it survived here after
country-posters was fixed. The URL now goes through mapImage, which places
it as the whole page.

This is the third defect of the same shape between these two files: a fix
lands in one twin and not the other, and nothing reports it. So the gate
asserts the seam for every host that rasterises a live map, rather than for
one file.

Two fixes to the check itself, both found by poisoning it rather than by
reading it. `[^)]*` cannot cross the arrow function's own parens, so
`useMapPoster(() => map.value).render(` never matched and the new section
skipped both files — it passed while testing nothing. And an older
assertion pinned the exact literal `{ mapSvg, variant }`, so moving to the
seam failed a check that was still describing the shape it replaced; it now
asserts that the variant travels, not the argument list it travelled in. A
gate that names one spelling of a call turns every refactor into a false
alarm, and a false alarm is how a real one gets waved through.

Both twins now use the seam. The render is skipped for a card deck, which
has no map page; this map's legend menu only asks for full or lightweight,
so that arm guards a future caller rather than a path taken today.
2026-09-29 07:31:17 +00:00
Doxa Maps
c0085d7336 Retire the last one-cycle re-export in the dashboard composables
useDashboardAggregations re-exported lbl, aggregate, OTHER_KEY, otherLabel
and NEUTRAL from @map/composables/useFacetAggregation.js, so that callers
could migrate after kit landing 4c-1 moved the generic half to the library.
Five files were still importing through it. They now import from the library
directly and the re-export line is gone; what stays in this module — colorFor
and its facet branches, the religion palettes, the donut and bar builders —
is research-specific.

DashboardChartsRail keeps its three builders from here and takes the other
four symbols from the library, so the split is by what each symbol is, not
by which file is nearer.

NEUTRAL is still imported: colorFor and buildCountryRegionColors read it five
times as the "no palette for this key" colour. Kit landing 4c-1 updated the
re-export line and not the import beside it, and every read threw
ReferenceError until that was fixed — deleting a re-export is not the same as
deleting the import next to it, so the import stays and the now-unused lbl
and aggregate come out of it.

Rebuild-and-compare on dashboard-plus-grid: 821.50 kB before and after,
899,237 bytes both, and every string literal appears the same number of times
in each. The builds differ only in which short names the minifier assigned,
which is what a change of module order looks like.

(cherry picked from commit f2cdcd0b62)
2026-09-29 07:31:16 +00:00
Doxa Maps
51ef6f7ec4 One phone flag per map, and the leaf page follows it as well as the Gallery
A map says how it behaves on a narrow screen once, in its <profile>.meta.json:
"mobile": "desktop-only" (blocked on a phone) or "larger-screens" (opens,
under a dismissible notice). Absent means phone-ready. Until now the rule
lived only in the Research Gallery, as two hard-coded id sets, so a map
opened standalone on its own leaf page on a phone got neither the block nor
the notice.

- profile-meta.mjs knows the vocabulary; generate-tree and the dev manifest
  carry the flag into each profile's manifest entry, and generate-tree into
  the leaf page's BASE_CONFIG (with the map's name for the desktop-only
  sentence).
- ProfileLoader mounts library/components/LargerScreenNotice.vue on a screen
  under 640px: for desktop-only the profile is never mounted and a centred
  message says why; for larger-screens a slim in-flow bar sits above the map
  (it cannot cover the map's controls), dismissible per profile for the
  session, in the element's own locale and direction. Inside the Gallery it
  stays quiet: every Gallery pane now carries data-doxa-gallery.
- One text: the Gallery's two sentences moved into the library catalogue as
  messages.largerScreenNotice / desktopOnlyNotice (+ noticeDismiss), all 11
  locales moved as they were, and the Gallery reads them from there.
- The Gallery derives its rules from the manifest's flags. On a phone it
  mounts nothing until the manifest has landed: its baked first tab (Big
  Picture) would otherwise mount there before the flags say it is
  desktop-only.
- dashboard-plus-grid is desktop-only and gospel-internet/board is
  larger-screens, the two sets the Gallery held; nothing changes for either.
- check:mobile-gating now asserts the vocabulary, that each flagged leaf page
  carries its flag, the Gallery's derivation and its closed-before-manifest
  rule, that Big Picture stays desktop-only, the 11 locales, and the
  notice's mounting. Its self-test plants one failure per rule.
2026-09-29 07:31:16 +00:00
e4aac1cd65 Country posters print their list, deck and packet, with the map drawn at the click
The regions list showed a poster per country and could print the poster alone.
Supplying a records provider is what makes the Prayer List, Card Deck and Prayer
Packet buttons appear at all -- PosterWall hides them when a wall cannot supply
rows, rather than failing at click time -- and this wall never supplied any.

Records come from the shared provider, not a copy of anyone's fetch. It caches
one request per language and every country in every region shares it, so opening
five regions is still one request, and the same rows feed the sheet, so the
printed map and the printed list cannot disagree about which groups are in a
country.

Page 1 is GENERATED, because there is nothing to load. No country sheet ships:
data/posters holds 28 .html and all 28 are zones, and emit-countries.mjs writes
only country-posters.json, whose `sheet` field is a paper size rather than a
filename. So the packet's default lift asked for posters/country-<iso3>.html and
404ed on all 175. country-sheet-print.js draws it instead, in the same shape as
the zone sheet next door so the two surfaces cannot draw the same sheet two
ways, and returns '' when there is no country or no rows so the caller falls
through rather than printing an empty country.

There is ONE loading path for the generator, not two: this calls
generateCountrySheet, and posterSheets resolves the served, sha384-pinned
poster-render.js itself. Nothing here fetches or imports the generator directly.

MEASURED against the development baseline, both built here: 1044 -> 1061 KB,
+17 KB. An earlier measurement of this same wiring on the pre-pinned base read
+208 KB; almost all of that was the printing subsystem that is now served rather
than bundled, so the cost fell by about 190 KB without this file changing.
createPosterGenerator does not appear in the bundle.

item-count-key replaces nothing: the noun pair still names the section, and the
count now goes through the plural-aware key.
2026-09-29 07:27:50 +00:00
Doxa Maps
32e3a20ae4 Wall of People Groups: the store no longer downloads every photo in the background, and the dead source picker and old card are gone
Measured on the live list (2026-09-29): after loading, the store tested every photo by loading it, three at a time with a second between batches: 932 photos, 132 MB by their Content-Length (the 77 IMB photos alone are 115 MB), for as long as the page stayed open, whether or not anyone zoomed in. All 939 photo URLs answered 200, and the canvas already loads only the photos in view and draws its grey box for any that fail, so the test is removed. A headless run of the real store with a counting Image stub: 12 photo requests in the first 3.5 s before, 0 after.

A group with no photo URL now gets none, instead of the stock "no image" photo, which answers 404. Today every group has a URL (a regional photo when it has none of its own), so nothing changes on screen.

The source picker only showed with two sources or more, and there has been one since the WordPress API and the CSV were dropped. It goes, with the two loaders, the CSV parser, their CSS, and the card component nothing imports. The bundle is 9.6 kB smaller (2.5 kB gzip).
2026-09-29 07:22:57 +00:00
Doxa Maps
a00c9af009 UPG Neighbor: on a phone the list can be lowered to a bar and raised again
rega at 390 px: the Mobilize list, a bottom sheet over 54% of the map, had no way to put it down. A handle at the top of the sheet (both views, one shared choice) lowers it to a 56 px bar and raises it again, and the map frames the selection above whichever height it has.
2026-09-29 07:22:57 +00:00
7b06dc95a7 No country codes means no pins, not every pin on earth
country-map filtered its fetched groups to the countries it was given, and fell
through to the UNFILTERED list when it was given none. So a host that omits the
attribute, misspells it, or passes a value that is neither an array nor a
comma-string got the entire global list drawn on an element whose whole contract
is one country.

Measured against the shipped country data: 2,106 groups across 175 countries,
where a typical country has 1 and the largest has 855. And the child's fill
expression matches nothing on an empty list, so those pins arrive with no
country outline at all -- a wall of worldwide dots that looks like data rather
than like the misconfiguration it is.

Empty now draws nothing, and says why in the console, addressed to whoever
embedded the element: this is a missing attribute rather than a data state, so
it does not belong in a translated sentence on the page. It sits beside the
fetch error that was already logged there.

Verified in the built bundle, not the source: the minified output carries
`E.size||console.error(...)` and the empty branch compiles to [].

Probe in _qa-cycle reads the filter expression out of the component and
exercises it. Mutation-tested: restoring the fall-through makes it report 3 pins
where it wants 0, and it passes only with the fix.
2026-09-29 07:07:48 +00:00
c6d77b9351 The country map's failure message says people groups, not regions
country-map reused the regions dashboard's load-failure sentence. That wording
is right on both regions surfaces and wrong here: this element has no region, it
has one or more countries, and what it fetches is people groups. A reader whose
country map failed to load was told the region data failed.

It was wrong in all eleven locales, not just English -- ar reads "بيانات
المناطق", ru "данные по регионам", de "Regionsdaten", all of them regions.

The people-groups sentence was already in the catalogue and already translated
in all eleven, so this adds no string and moves no parity: it is the same
sentence the grid and the other consumers already show when this fetch fails.

Verified in the built bundle rather than the source, because the minifier
rewrites the call: the component's own render now calls the people-groups key,
and the remaining "Region data" occurrences are catalogue entries, which ship
for every key regardless of which one is called. check:i18n-keys-exist passes,
so the key resolves. check:hardcoded-english is unchanged at 257 with
country-map contributing 0, measured before and after.
2026-09-29 07:07:47 +00:00
Doxa Maps
7d278c3e2a check:no-free-identifiers knows atob and btoa, the two base64 globals every browser and Node 16+ define 2026-09-29 06:32:21 +00:00
Doxa Maps
6c80284b0b The generated sheets fetch their capitals and cities at print time, instead of every page bundling them
Generating map sheets in the browser inlined Natural Earth's capitals and cities (170 KB) into every
bundle that can print one: single-file builds inline lazy imports, so the whole page paid for a table
only a reader who prints needs. The tree now serves it beside the boundaries (boundaries/
ne-places.json, one line in generate-tree's copy list; public domain, no credit to carry) and
posterSheets fetches it at the first print, with the boundaries. No asset base, or a failed fetch,
draws no city dots — the generator's own rule for absent data. The Node generator reads the same file
from its source.

Measured: my-upg-100-list 482 -> 432 KB gzipped, country-posters 547 -> 496 KB. check:poster-browser-
equiv still passes byte-identical through makeZoneSheet and generateCountrySheet, which now fetch the
served table — so the fetch path draws the same dots Node does. gates:pre 73/73.
2026-09-29 06:23:20 +00:00
Doxa Maps
7067c0ae96 Fix what an adversarial review of the print standard confirmed: 21 defects, each re-checked against the code
Ten agents read today's branch (five finders, one skeptic per area who had to confirm each finding
against the current code); 21 distinct defects survived. All are fixed here.

Generator and its proof
- generateCountrySheet fed only ADM0, so a country beside India or China printed without their
  provinces (Nepal: 251 KB against Node's 921 KB). It loads the full boundary set, as the driver does.
- check:poster-browser-equiv rebuilt its own feed, so it could not see that. It now calls what the
  browser calls (makeZoneSheet, generateCountrySheet — Nepal), deletes the target before the Node run,
  runs Node with a clean environment, and FAILS (not "not measured") when the Node generator breaks.
  Proven two-sided: it fails with the ADM1 fix reverted.
- The facts table's country list sorted in the reader's browser locale; pinned to en-US (Node's
  default, so Node's output still hashes to the baseline).
- zone-sheet-print.js imports the library relatively, so bun, Node and the bundler resolve it alike.

Print seams and wiring
- The wall's Poster Map button used its own stored-file fetch and never the generated sheet, so every
  country poster 404'd on the published tree; it now prints the generated sheet first.
- Country Posters wall: card stocks passed (its Card Deck could never print), iso3-names passed (zh/ar
  page 1 kept English names), the stored fallback reads previews/ (where the tree serves them).
- Card-stock labels render at render time, so a locale change relabels them.
- The records provider caches by language AND list, and does not keep a failed membership load.
- The UPG-100 legend's Card Deck returns quietly on a parent row instead of an evaluator error; the
  selection fetch uses PRINT_FIELDS.

Packet
- The centered-set caption counts what is DRAWN (a ring too full to separate reports its remainder).
- Each ring's number lane is sized to its own radius (ring 1 put a dot under its badge).
- Per-box line keeps a decimal below 10 people ("≈ 0" for small lists); worksheet rows name the country;
  the localized poster kicker is fitted against the poster's real 19px, not 22.

Plurals and gates
- regions.countryCount / regionCount were authored in English zero|one|other order in de, es, fr, hi, it
  and pt, so CLDR picked "27 Land", "1 países". Rewritten in each locale's own order; check:plural-forms
  now fails a string in English order (proven on the old German string).
- check:optional-t-equivalence (gates:post) crashed on the new plural fallback's imports; it injects the
  real cldrChoiceIndex and asserts the plural path (ru 21/3/5, en 0/1/7).
- check:packets-all and check-packet-render counted people as cards only, so every lightweight packet
  read as "a packet of nobody"; worksheet rows count, and the browser gate checks the grid's top fill
  (the renamed .lc-frame) instead of the retired scatter. check:packet-variants now reads the grid itself
  — 5,200 boxes, Jesus' 1,716 from the top, the % shading from the top — proven: it fails on a renderer
  that shades the last boxes.
gates:pre 68/68, check:optional-t-equivalence PASS, both bundles build.
2026-09-29 06:23:19 +00:00
Doxa Maps
b95c40e0f9 Map sheets are generated in the browser at the click, by the same generator the Node script runs
The Driver's print standard: every export is generated when it is asked for, and only previews are
stored. The zone and country map sheets — the poster, and the map-only sheet on a packet's page 1 —
existed only if library/scripts/posters/generate.mjs (Node) had written them ahead of time; no country
had a print-resolution sheet at all.

- library/packet/poster-render.js is generate.mjs's drawing layer (2,645 lines) as a pure factory,
  createPosterGenerator({ env, SERIES_DEF, ISO3_NAME, adm0, adm1Ind, adm1Chn, nePlaces, series }). Moved
  VERBATIM (template literals carry their whitespace into the output), `process.env` read through a
  local shadow; its inputs are its free variables, found by a scope-aware parse, not a grep (a grep
  missed adm0, and the proof caught it). generate.mjs is now its Node driver.
- PROVEN BYTE-IDENTICAL: all 118 sheets of a pinned offline matrix — the 28 zones in 3x4, 3x3, letter
  and packet, and a country sample at both sheet sizes — hash the same before and after.
- data/zone-sheet.js is which group pins in which zone (first claim wins), now ONE function both the
  Node driver and the browser call, so a group cannot pin in one zone on paper and another on screen.
- library/packet/posterSheets.js feeds the factory in the browser: the served full-resolution
  boundaries, the shared series and places tables, a generator per call (caching by format pinned the
  first caller's data on every later sheet). generateCountrySheet does a country end to end;
  data/zone-sheet-print.js a UPG-100 zone. ISO3_NAME moves to library/data (territory-counts re-exports).
- usePosterPrints gains opts.sheet: the poster print is the generated letter sheet as a VECTOR page,
  and the packet's page 1 the generated map-only sheet (item.mapId honoured); stored images remain the
  fallback when generation fails. PosterWall forwards it; the Country Posters wall, the UPG-100 wall and
  the UPG-100 map pass it. The UPG-100 wall and map drop their own record fetches for the shared
  provider, and the map's legend and selection HUD offer the card deck (production).
- check:poster-browser-equiv: the browser feed's sheet is byte-identical to the Node generator's for an
  India zone (packet) and a country — proven two-sided (it fails when the feed drops the city dots).
  check:selection-kinds reads the handler's branches instead of a fixed poster/packet pair, anchored on
  `if (` (proven: it fails with the cards branch deleted). library/packet/README.md: the standard.
gates:pre 68/68; both bundles build.
2026-09-29 05:26:27 +00:00
Doxa Maps
50b9f9a8a2 Two gates:post reds from this landing's own commits, fixed at the source
check:optional-t-equivalence evaluates useOptionalT.js on its own, with its
imports stripped and tStatic and useI18n handed in. The composable now also
imports getRuntimeLocale and cldrChoiceIndex, so every section died with a
ReferenceError. The gate now hands in a bare host's getRuntimeLocale and the
shipped cldrChoiceIndex, lifted from library/i18n/index.js's own text; its
import control accepts tStatic among several names, but still only from
'@map/i18n/index.js' (a catalogue import of the module's own still fails).

check:frozen-labels caught the research filters' new search placeholder:
tStatic inside a computed that reads no locale never re-runs, so the
placeholder stayed in the boot language. It now reads and passes
locale.value, like the search chip in the same file.
2026-09-29 05:26:27 +00:00
Doxa Maps
8890377993 The Country Posters map's legend Packet export works, and the card deck rolls out behind one switch
- The legend's packet submenu emits 'packet:full' | 'packet:lightweight', and the country map compared
  against a bare 'packet', so its legend Packet export did nothing at all. It splits the kind first, as
  the UPG-100 map's handler does (the research map carries the same three lines; reported to its owner).
- The selection's rows come from the print standard's records provider by slug (full print fields,
  the reader's language); the rows mapped by hand put the country's NAME in country_code and carried no
  language, so every profile description ran short. They remain as the fallback. The live map's raster
  goes in through the mapImage seam, so page 1 is the map full page.
- The legend gains an OPT-IN Card Deck export (exportCards; provide('legendExportCards') for the mobile
  sheet), so a map that does not handle it never shows a dead item.
- library/print/experimentalExports.js is the ONE switch for rolling the standard's exports out on
  maps that are not production print surfaces (production: the UPG 100 list, the Regions dashboard's
  country posters, the Country Posters wall). This map's card deck sits behind it; off — per page, per
  visit (?exports=off), per site or everywhere — restores the menu it had before.
- The records provider returns coordinates (a generated map's pins), exposes the unselected set
  (recordsFor.all) and sorts in the reader's collation.
2026-09-29 05:26:27 +00:00
Doxa Maps
8d8b38a4b2 Translate the dashboard chrome and the map's tab-scoped search
Eighteen keys across eleven locales for text that was written in English in
the components: the rail and filter toggles, the selection count, the CSV
export toast, the drawer's prayer and information links, the dashboard
title, the map's failure message, and the four search placeholders that name
the tiers of whichever tab is open.

The export toast is interpolated rather than concatenated. Its counts keep
going through fmt(), which groups digits the way each locale does, and only
the words around them come from the catalogue — the order of "rows",
"fields" and the outcome is not the same in every language, so a sentence
built by joining fragments cannot be translated at all. It uses the shared
fillParams, which leaves a placeholder visible when a value is missing
instead of rendering the word "null".

The map's failure message no longer names the token and the container: that
is for the console line, which stays English for whoever is debugging. What
the reader sees says what to do.

Three more shapes the scan could not see, each of them correct code: tStatic
called directly rather than through a wrapper; `config || t(key) || English`,
where the translator is not the immediate left operand because || is
left-associative; and the licence credits, media queries and product name
that must not be translated at all.

Own leaks 91 -> 1, the last being a tab label behind a key that resolves in
all eleven locales. Fleet 249 -> 159. check:i18n-parity,
never-translated, treeshake and stopped-scope all pass.
2026-09-29 05:26:26 +00:00
Doxa Maps
181f723bf8 The legend's Packet export did nothing on the research map
The legend emits 'packet:full' or 'packet:lightweight' — it widened its
existing event rather than adding a second one — and this router still
tested the whole string for 'packet'. Neither submenu item matched any
branch, so the Packet export opened nothing, logged nothing, and looked
like a menu item nobody had wired. Found by upg100, who had already hit
the same thing in its own copy.

The kind is split before it is compared, the way upg-100-list does it, and
a bare 'packet' still means the full packet so an older emitter keeps
working. The variant then has to survive the call as well: openPacket
ignored it and printPacket defaults to full, so lightweight would have
printed the full packet even once the menu fired.

The gate asserts the seam rather than one file — it reads the kinds the
legend actually emits and requires every consumer to split before
comparing, because this bug cannot be seen by reading either side alone.
It currently fails on country-posters/countries-map.vue, which has the
identical dead menu and is not mine to change.

Two fixes to the check itself while writing it: a fixed-size window read
only part of a long router and reported correct code as broken, so the
function's extent is found by brace depth; and a trailing \b after a
quoted literal never matches, which made the assertion fire on every
consumer including the working ones.
2026-09-29 05:26:26 +00:00
Doxa Maps
126ee06619 Translate the research map's legend tab explanations
The legend gives each tab a small "i" button explaining what that tier
means — what an Affinity Block is, how a people group differs from a people
group in a country. SemanticTreeLegend already read that text from
legend.tabInfo.<id> behind a te() guard, and not one of the ten keys the
research map needs had ever been written. The tab chips were translated in
all eleven locales and the explanations beside them were not, so the reader
least able to guess what "PGIC" means was the one reader who got it in
English.

The keys are written for all eleven locales, following the terminology each
catalogue already uses for these same tiers — People Group as Volksgruppe,
Peuple, Pueblo, 民族, Народ, जनसमूह — with WAGF and UPG left untransliterated
as the corpus leaves them. The reader-facing text drops the source field
names and row counts that the English carried; those are notes for whoever
maintains the tables, so they stay in the source as a comment.

With the keys in place the English in the tab tables can never render, and
a second copy of a string drifts from the catalogue without anyone noticing,
so it is removed. `info: true` stays because the legend gates the "i" button
on that field being set — dropping it would have removed the affordance
instead of translating it — and tabInfo now treats only a string as a
fallback, so the flag cannot reach the popover as the word "true".

Verified every changed tab renders a real label and a real explanation in
all eleven locales. check:i18n-parity, check:i18n-never-translated and
check:i18n-treeshake pass; hardcoded-english 205 -> 190.
2026-09-29 05:26:26 +00:00
Doxa Maps
f8744925e4 Every country prints its prayer list, card deck and packet, from a records provider any map can use
The country wall passed no records, so each of its 175 cards offered only the poster and said the
prayer list was "coming soon", while the people groups were one request away. library/lists/
peopleGroupRecords.js is the records half of the print standard: one people-groups fetch per language
(normalised the way the prints read rows, list membership applied), then a SELECTOR picks the set — a
country by ISO3, explicit slugs, any field, or a host's own selector language. A selector nobody
understands selects nothing rather than every group on earth, and a failed fetch is not cached.

The country wall uses it by ISO3, so every country now prints the list, the deck (all five stocks) and
the full or lightweight packet. The packet's page 1 is, for now, the country's stored poster when it is
really there; the in-browser map generator will replace that.

check:people-group-records asserts the selection two-sided with an injected fetch; registered in
gates:pre.
2026-09-29 05:26:26 +00:00
Doxa Maps
ee87f8e9db Plugin Studio keeps the reason and drops the speaker: its 15 references to the Driver reworded, comments only 2026-09-29 05:26:26 +00:00
Doxa Maps
c067a5d069 Dictation fell behind because our own build took every core
Rudder was measured slowing after about 1,400 words while a Tauri build
ran. The word count is incidental — it is a proxy for how long the build had
been going. The speech model runs on the CPU (backend/rudder-engine starts it
with --device cpu), and a Rust build saturates every core, so at equal priority
it starves the one process whose latency a person can hear.

Three changes, and the direction of the first is forced rather than chosen:

1. internal/desktop-build.mjs nices the whole build tree to 10, through the one
   run() helper every step goes through, so cargo, rustc and the linker inherit
   it. We nice the COMPETITOR because an unprivileged process cannot raise its
   own priority on Linux — a negative nice needs CAP_SYS_NICE — so "promote the
   ASR" would fail with EPERM. If nice(1) is missing the build still runs and
   says so, because a silent fallback would look exactly like the fix working.

2. cargo is capped at cores-1. nice decides who wins a CONTESTED core; it does
   not stop the build taking all of them, and a fully subscribed machine still
   adds scheduling latency to the audio path. An explicit CARGO_BUILD_JOBS is
   honoured rather than overridden.

3. yieldCPU reports its error instead of discarding it. It was
   `_ = syscall.Setpriority(...)`, which cannot fail visibly — a call that
   silently does nothing is indistinguishable from one that worked, and this one
   guards something audible.

Also, one real O(n) in the widget, found while checking studio's answer that the
collapsed pill already shows the last words: sendActivity split the ENTIRE draft
on every activity tick to take five words. Measured at 3,000 words: 1.61ms per
tick against 0.051ms for a bounded tail, 31x, with byte-identical output.

Verify the priority work with `ps -o ni=` on the running cargo processes, never
by any of these calls returning.
2026-09-29 05:26:25 +00:00
Doxa Maps
cc3406cfe4 The UPG-100 list prints its packets through the shared lift, not a second copy of it
The list kept its own page-1 map lift (packetMapSvg + packetLabelFor) beside the one in
usePosterPrints, and the two drifted: the list's relabelled country names, the wall's did not, which
is the zh finding rega made in the app. With the composable now taking iso3Names, the list passes it
and deletes its copy, so the list, the wall and any other map print page 1 through one path.

The selection packet borrowed one list's sheet when it held exactly one list. That is now a general
seam: item.mapId names the sheet when it is not the item's own id, and mapId: null says the item has
none, so a multi-zone selection fetches nothing and page 1 says the map is missing. Measured two-sided
through the real printPacket. gates:pre 66/66.
2026-09-29 05:26:23 +00:00
Doxa Maps
c83a9827ff Country Posters and the UPG-100 wall read in the page's language, and walls count in each language's plural forms
The i18n + RTL audit of Country Posters (the hardcoded-English gate only reads production plugins,
so it had never read this one; its scanner was run on the plugin directly and every hit triaged for
reachability, since the country map mounts one tab):
- 9 strings a reader sees were English in every locale: the search placeholder, the packet title's
  "— 50 of N", the map-failed message, the wall's heading, tag, intro, soon message and item nouns,
  and the view switcher's legend and label. All read from the catalogue now, reusing the existing
  keys where they exist (tabs.selectView/views, the UPG-100 wall's tag and emphasis).
- The wall's section headings are the WAGF region labels in the data, English everywhere; they now
  resolve through the Regions dashboard's own WAGF table, which gains its two missing regions.
- The three WAGF tier tooltips had no keys in any locale (legend.tabInfo.region/block/country).
- 0 physical-CSS findings.

PosterWall counted "N countries" as a number plus a 2-form noun pair and defaulted the plural to
noun + "s" — wrong for Russian and Arabic whatever strings it was given. It takes itemCountKey, a
plural catalogue key rendered with the count; useOptionalT forwards a plural count to vue-i18n (the
CLDR map picks ru 4 forms, ar 6, zh 1), and a host without i18n picks the English form by
vue-i18n's own rule. The people-group counts on the section heads and placeholder cards use
wall.peopleGroupCount the same way ("people groups" was hardcoded English on the cards).

The UPG-100 wall: its Americas and Europe section headings had no key (single words, invisible to
the gate), and it counts its lists with wall.listCount.

Checked: plural-forms, i18n-parity, keys-exist, never-translated, and i18n-treeshake against both
bundles built (26 bundles carry every area they ask for); gates:pre 66/66.
2026-09-29 05:26:23 +00:00
Doxa Maps
ff0c433fce A packet printed from the wall names page 1's countries in its own language
The list's own print relabelled the map sheet (mapLabels.js); the wall's print, through
usePosterPrints, lifted the sheet with no relabeller, so a zh packet printed from the wall kept
INDIA, CHINA … on page 1. The generator's ISO3 -> name table is plugin data the library cannot
import, so the host now supplies it: PosterWall iso3Names -> usePosterPrints iso3Names, and the
UPG-100 wall passes its territory-counts table lazily. A host that passes nothing prints exactly as
before. Measured two-sided through the real printPacket on a tracked sheet: without the table the
page carries CHINA and no 中国; with it, 中国 and no CHINA.
2026-09-29 05:26:23 +00:00
Doxa Maps
7160126831 Wall of People Groups: the verse file's comments name the brief and the chosen footer, not a person 2026-09-29 05:17:38 +00:00
Doxa Maps
a812056e71 PPLR Data Maps: on a phone the ten views are one select, and the prayer map's coach-mark stays off this research tool
rega at 390 px: wrapped, the view tabs took five rows (160 px of 844), and the shared 'Tap a coloured dot, someone still needs prayer' hint sat on top of the view's own bottom bar and attribution. On a phone the views are one select in one row (the tabs stay on a wide screen); pinHint is off, as for UPG Neighbor and the Wall.
2026-09-29 05:17:38 +00:00
Doxa Maps
042803ab3a Wall of People Groups: photos are kept as small copies, text is measured a little at a time, and off-screen sheets skip rendering
Measured on the live list: 77 photos are 1024 x 1024 (4 MB each decoded) and drawImage downscaled each of them on every frame it was visible. A loaded photo is now kept at 320 px on its long side (createImageBitmap with resize, or one drawImage into a small canvas where that is missing), and the original is released; the print frame loads the originals itself. Below text zoom no card is measured (the photo box uses an estimate), and at text zoom at most 150 new cards are measured per frame. The overlay's sheets use content-visibility:auto. The host hook reports the counts (photos loaded, thumbnails, fallbacks, cards measured) for the performance probes.
2026-09-29 05:17:38 +00:00
Doxa Maps
daf8a335ea Wall of People Groups: the poster comes in 3 by 4 ft (the default), 2 by 3 ft and 5 by 8 ft, and every size keeps its scripture readable
The brief's three sizes. At 3 x 4 and 2 x 3 ft a verse under each of ~2,100 cards would print at 3 pt and 2 pt, so those sizes use the strip layout: cards of name and photo, and one blessing verse per row on a strip under it at 9 pt (33 and 34 verses). 5 x 8 ft keeps a verse under every card at 6 pt. The footer's height now comes from its text (the whole passages), with the journey never below 6 pt; the screen shows the strips as it shows the rest of the poster.
2026-09-29 05:17:38 +00:00
Doxa Maps
9e82ac8dd1 Wall of People Groups: the poster's footer quotes the whole passages, Numbers 6:22-27 and the journey's seven, and the chart cites the passages its verses come from
The priestly blessing is Numbers 6:22-27 in full: the Lord's word to Moses and Aaron in small type, the three blessing lines large, and the promise that closes it. Each station of the journey quotes the passage the brief lists: Genesis 12:1-4, Genesis 15:5-6, Galatians 3:13-14, 1 Peter 2:4-8 (the cornerstone, at the centre), Galatians 3:5-7 and 28-29, 1 Peter 2:9-10, Luke 19:9-10. The chart's New Testament verses name their passages (Matthew 21:21, from 21:18-22; Matthew 8:26, from 8:23-27). All WEB, verbatim, checked by script against ebible.org.
2026-09-29 05:17:38 +00:00
90613f9c88 Translate the three poster-wall nouns the regions list was passing in English
sr's census found item-noun="country", item-noun-plural="countries" and
unit="UPG" passed as literals. All three reach the DOM: PosterWall renders the
noun pair at its section count (n === 1 ? itemNoun : plural) and the unit beside
each row's count, so in Arabic the chrome translated and these stayed English.

The noun pair now goes through t(). Country and Countries were already in the
catalogue in all eleven locales -- Land/Länder, الدولة/الدول, Страна/Страны --
so this adds no strings, and PosterWall keeps making its own singular/plural
choice with the caller supplying both forms, which is what its props are for.

unit stays "UPG" on purpose. It is DX-life's term of art, and their own Arabic
common.json carries the literal "UPG" rather than translating it; matching them
is the point of the port. It is the one entry the census still reports here.

A fourth literal the census did not flag went with them: the prayer-list
"Coming soon" sentence, which reaches exportState.message and so renders too.
That one had no key, so it lands as an English-source key -- the convention
useRegionsI18n exists to serve -- translated in all eleven locales, 77 keys
each, parity green.

MEASURED, this plugin's entries in check:hardcoded-english: 3 before, 1 after.
2026-09-29 04:51:30 +00:00
Doxa Maps
d2fd5452d5 Keep the reason, drop the speaker, in the poster comment
check:no-new-driver-references went red on development with my landing: the
comment I wrote when removing the SVG download named who set the rule. The gate's
own advice is the fix — the engineering reason is what a reader needs, and it
stands without attribution.

Comment-only: 0 non-comment lines changed, and the minifier strips comments, so
the shipped bundle is byte-unaffected and needs no rebuild. Verified rather than
assumed: the phrase greps 0 in the built artefact either way.

Found by regions, who proved it on bare ced1eb73c before their own cherry-pick
touched anything, and who held their push rather than landing on a red.
2026-09-29 04:44:37 +00:00
Doxa Maps
7c1cfc8596 The language tree could write an SVG to disk; now it only prints
regions' fleet audit found the one poster path in the tree that downloaded a
file. The Driver's rule is that a poster action opens the print dialog and never
downloads, and there is no case for an exception here: printPoster builds the
same poster from the same makePoster(), so the download was a second route to one
artefact rather than a capability only it could offer. A reader who needs a file
prints to PDF — the system's own dialog, not a silent write.

The button, the function and the now-unreferenced tree.posterSvg key in all
eleven locales are gone. The comment left in its place names check:poster-action-
print, which regions is extending with this file as its poison case, so a
download added here again fails a gate rather than shipping quietly.

The two CSV export paths in GiGrid and GiResources are deliberately untouched: a
table has no print equivalent, the export is a documented feature that writes a
header line of sources with the data, and the rule — and that gate's own
description — is about POSTER buttons. Flagged to the shepherd rather than left
for the extended gate to trip over.
2026-09-29 04:28:09 +00:00
Doxa Maps
98fd04c86d The centered set's legend covered the chart before anyone asked for it
A4.3. GiMap and GiTree already default their legends closed and read the host's
`legend: 'open'` knob; the centered set hardcoded ref(true), so it was the one
view whose decoder sat over the drawing from the first frame — on the chart where
seeing the target matters most. That was an inconsistency, not a considered
difference.

Changing the ref alone would have shipped NOTHING: applyWidth ran on mount and
set legendOpen back to `!m.matches`, so at any desktop width it reopened
immediately. The bug was in the width rule, not the initial value. Width now only
ever CLOSES the legend — a wide window is not a request to cover the chart, it is
just the absence of the reason to force it shut — and a choice the reader has
made still wins outright.

Checked as a state machine over the cases that matter: desktop with no host knob
closed, desktop with legend:'open' open, phone closed, phone→desktop stays
closed, and a reader who opens it keeps it through a rotation and a resize.
2026-09-29 04:28:08 +00:00
Doxa Maps
7f8199a5b7 The recording pill kept resizing, because a character count is not a width
The Driver: "it shows some words … but it keeps RESIZING, kind of distracting …
it should stay the same width and stop resizing".

The intent was already here — a fixed width while listening, with the text cut to
33 characters — but neither half held. The bubble is inline-flex, so it sizes to
its content wherever `width` loses, and it carries a max-width that the <=480px
query overrides, which is exactly the phone case he was looking at. Truncating by
CHARACTER cannot stabilise it either: 33 characters is about 133px of "illi" and
about 344px of "MMM WOW", so the same count is a 200px swing.

width, min-width and max-width are now pinned to the same value while recording,
at both breakpoints, so no later rule and no viewport lets the pill grow or
shrink; the text ellipsises inside a box that no longer moves.

studio's file: sent as a hunk for them to land, not landed here.
2026-09-29 04:06:41 +00:00
Doxa Maps
8b7114942c On a phone the transcript could not be scrolled, because a minimum refused to shrink
The Driver, expanded on a phone: "a lot of header space … fills the entire page …
I can't scroll down in the text area and see everything".

The panel is capped at 60vh here and .draft carried min-height:180px (240px
elsewhere). A MINIMUM is the wrong tool for that: it makes the box refuse to
shrink, so whatever the header takes is pushed off the bottom and the draft's own
scrollbar never appears — the text is there and unreachable. An earlier attempt
had already lowered 240 to 180, which only moved the threshold rather than
removing it, and is why the symptom survived.

The column now owns the height and the draft takes what is left. min-height:0 is
the load-bearing part: without it a flex child will not shrink below its content,
which is the same reason the old minimum failed. The draft scrolls inside its
share, and the header gives back space — tighter row gap, smaller name and status
pill — since "a lot of header space" was the other half of what he saw.
2026-09-29 04:06:40 +00:00
Doxa Maps
2b655e25ff Dictation fell behind because our own build took every core
The Driver measured Rudder slowing "after about 1,400 words while a Tauri build
ran". The word count is incidental — it is a proxy for how long the build had
been going. The speech model runs on the CPU (backend/rudder-engine starts it
with --device cpu), and a Rust build saturates every core, so at equal priority
it starves the one process whose latency a person can hear.

Three changes, and the direction of the first is forced rather than chosen:

1. internal/desktop-build.mjs nices the whole build tree to 10, through the one
   run() helper every step goes through, so cargo, rustc and the linker inherit
   it. We nice the COMPETITOR because an unprivileged process cannot raise its
   own priority on Linux — a negative nice needs CAP_SYS_NICE — so "promote the
   ASR" would fail with EPERM. If nice(1) is missing the build still runs and
   says so, because a silent fallback would look exactly like the fix working.

2. cargo is capped at cores-1. nice decides who wins a CONTESTED core; it does
   not stop the build taking all of them, and a fully subscribed machine still
   adds scheduling latency to the audio path. An explicit CARGO_BUILD_JOBS is
   honoured rather than overridden.

3. yieldCPU reports its error instead of discarding it. It was
   `_ = syscall.Setpriority(...)`, which cannot fail visibly — a call that
   silently does nothing is indistinguishable from one that worked, and this one
   guards something audible.

Also, one real O(n) in the widget, found while checking studio's answer that the
collapsed pill already shows the last words: sendActivity split the ENTIRE draft
on every activity tick to take five words. Measured at 3,000 words: 1.61ms per
tick against 0.051ms for a bounded tail, 31x, with byte-identical output.

Verify the priority work with `ps -o ni=` on the running cargo processes, never
by any of these calls returning.
2026-09-29 04:00:29 +00:00
Doxa Maps
59c2d1b838 Keep the reason, drop the speaker
One hundred and forty-three comments across the research dashboard, the research
map, Big Picture and the two composites attributed a decision to a person: by
name, by quotation, by date, or as "per Driver". Shipped code should carry the
reason a thing is the way it is, not who said so. A reader of the giveaway repo
has no way to ask that person anything, and the name tells them nothing the
reason does not.

So a quotation becomes the requirement it was quoting, a ruling becomes the
ruling, "the Driver reported" becomes what was reported, and a decision still
waiting on an answer now reads as provisional and pending a product decision
rather than as somebody's pending word. Where a comment already stated the
reason -- most of them did -- only the attribution went.

Two things were removed rather than rewritten. A personal medical fact appeared
twice ("the Driver is photosensitive"), once in a comment and once in a gate's
console output where it printed on every run; the motion-safety requirement
stands on its own and now reads that readers may be photosensitive. Internal card
ids that named nobody stayed, since they point at this repo's own history.

The bulk went in three batched passes of meaning-preserving substitutions, and
that is where the risk was: a blind replacement produces sentences that are
grammatically wrong but still look plausible in a diff. Re-reading every changed
line found thirteen -- "the the report describes", "including the two that were
had just complained about", "because was asked for WHITE rings" -- each repaired
individually. The count of remaining references caught the tail three separate
times, when a file type, an escaped apostrophe, and a scripts/ path each fell
outside a filter I had assumed was total.

Comment bodies and two console strings only. All twenty-one files parse, both
meta.json files are still valid JSON, and no declaration moved.
2026-09-29 03:46:37 +00:00
Doxa Maps
a4360f6661 Say what the research found, instead of pointing at where it is filed
Five comments in the shipped research dashboard cited _qa-cycle documents by
path: three RESEARCH write-ups, one REFERENCE standard, and one scratch probe.
Those files do not ship. A reader of the built plugin gets a path they cannot
open, and the reasoning it was standing in for is lost to them.

In every case the durable finding was already written out directly above the
citation, which was only ever a trailing "and here is where that came from". So
each now ends on the finding itself and the path is gone: that a comfort-tuned
palette anaesthetises the response the map exists to provoke; that a casing which
scales with zoom is what stops dense low-zoom rings swallowing their own fill;
that the real fix for pin contrast is the lightness-conditional stroke, which
holds whichever ramp is selected; and that the palette values come from the live
site rather than being re-invented, which is what stops an embedded dashboard
reading cooler than the page hosting it.

The scratch probe became the METHOD rather than a filename, since a probe is
worth citing only for what it does: contrast every ramp stop against all three
pin targets under both CVD simulations, and re-solve composited over the real
basemap. Compositing over pure white is what made two handed-over candidates
look like they passed, and that is the part a future reader needs.

Comment-only: no declaration moved, and all three files still parse.
2026-09-29 03:46:37 +00:00
Doxa Maps
51056bac65 Keep the engineering reason, drop the speaker: 31 Driver references to 0
The rule: never quote or cite the Driver in shipped code. Every one of these
comments had a real engineering reason attached, and the reason is what a future
reader needs — so the reason stays and the attribution goes.

COUNT IS THE CHECK: 31 before, 0 after, across 8 files.

I was pointed at 18 in region-dashboards. That was one file. Sweeping my own area
as instructed found 31 in eight files: region-dashboards 7, RegionCountryMap 13,
regions-list 5, UupgsList 2, plus regions-mfe.vue, regions-mfe.meta.json,
index.js and check-regions-uses-library.mjs with one each. Reporting the wider
number rather than the one I was given.

HOW EACH WAS REWRITTEN, by kind:
  · a REPORTED SYMPTOM keeps the symptom, loses the reporter — "the pins were
    reported too small at rest", "the list page was reported slow, with the
    posters suspected", "instead of flying to the region, which is exactly the
    reported symptom".
  · a REQUIREMENT becomes a requirement — the region key is "a map that shows the
    regions and countries AND lets a reader select one" rather than a quotation;
    the missing fullscreen and zoom controls are "part of the cloned surface"
    rather than someone's complaint about a full clone.
  · an OPEN DECISION becomes provisional. Two of these: the stat-card divergence
    from RegionDetail.vue:176 is now "carried back as a product question", and
    the numeral-grouping parity reads "PROVISIONAL, pending a product decision"
    instead of waiting on a named person's ruling.
  · a CARD ID goes with the name: BUILD-REGIONS-MAP-RESTORE-EDGE-SCROLL-STRIPS
    and the round-2 test numbering are internal handles that mean nothing to a
    reader of a published bundle.
  · a SCOPE RULING is stated as scope: "improve the map's LOOK and add the
    all-regions map, which must be intuitive" — the constraint survives, the
    quotation does not.

Nothing lost that a reader needs: every measurement, pixel value, file:line
citation into DX-life's source and engine-specific caveat is untouched. What went
is a name, four dates and two internal ids.

SAME PROOF AS THE CITATION COMMIT, because a prose sweep across eight files is
exactly where a stray edit hides:
  · the only non-comment changes are an HTML comment in a template and the "note"
    field of regions-mfe.meta.json — and `note` is read by nothing: generate-tree
    never touches it, and the string does not appear in the built manifest.
  · meta.json still parses.
  · rebuilding regions-dashboard and country-map reproduces the committed bytes
    EXACTLY, and a full `bun run tree` leaves public/ clean — so NO bundle needs
    rebuilding for this.
  · gates:pre 66/66.
2026-09-29 03:46:37 +00:00
Doxa Maps
565907adea State what the probes found; drop the paths into the scratch area
gospel's citation rule, applied to mine: a comment citing a PROBE or MEASUREMENT
(_probe-*, _chk-*) states the FINDING and drops the path. A reader cannot open
_qa-cycle from a published bundle, and the path outlives the file — so the number
has to be in the comment or it is lost.

FOUR CITATIONS, all of the probe kind, so all get the same treatment. Every one
already carried its finding; only the dead path is removed:

  RegionCountryMap:36   three engineFor implementations "decide identically in
                        every engine combination" — now says WHICH combinations
                        were checked (mapbox-only, maplibre-only, both globals
                        present) rather than naming the script that checked them.
  RegionCountryMap:462  the globe-fill measurement keeps its numbers (zoom 1.229
                        -> 390.61px = 50.1% of the frame, half the frame dead
                        space) and loses the probe path.
  RegionCountryMap:552  the fit-at-construction comparison keeps both readings
                        (bounds-at-construction zoom 3.210 / mercator at t=0 vs
                        fitBounds zoom 2.777 / globe) and the engine version.
  region-dashboards:522 "4 of 10 app properties OK", with the five failures that
                        were one missing thing already spelled out below it.

I did NOT only fix the one line I was pointed at: I swept my own plugin files,
the six library files I own or moved this session, and the seven gates I wrote.
Four citations total, all in these two files; the gates and the other library
files carry none.

COMMENTS ONLY, and proved rather than asserted: the diff filtered to non-comment
lines is empty; check:ported-globals-identical still reports 4/4 byte-identical
emitted CSS; and rebuilding regions-dashboard and country-map reproduces the
committed bytes exactly — comments are stripped at build, so NO bundle needs
rebuilding for this and public/ stays untouched. gates:pre 66/66.

scripts/strip-code.mjs:5 is deliberately left alone: per the same ruling it
explains why a committed gate cannot import from the scratch area, which is a
fact about the boundary rather than a citation across it.
2026-09-29 03:46:37 +00:00
Doxa Maps
9996bb6fde Comments state their findings instead of citing files outside the repo
Seven comments in the library and the app pointed at probes, checks, a report or research notes that
live outside the repository, where a reader of the code cannot follow them. Each now states what was
found and drops the path: the engine-predicate equivalence, the chooser's globe-fill measurement, the
fit-at-construction probe, the worldview clause, the web-boundaries sizes (1,866 KB to 383 KB, every
feature and part kept). The QA corner and the packet generator lose a dead pointer. The app's nav
comment states the three sections and the third dashboard directly instead of quoting a transcript.
2026-09-29 03:46:37 +00:00
Doxa Maps
88cd390b27 PPLR Data Maps: on a phone every view tab is on screen, and the legend's collapse button is a real target in the reader's language
rega at 390 px: the tab strip ran past the right edge with no scroll cue, and the flat legend's collapse button measured 19×15 with an English title in every locale. On a phone the tabs wrap (the brand steps aside); the button is 24 px and named by the shared aria keys.
2026-09-29 03:38:57 +00:00
Doxa Maps
cc5d592b02 Wall of People Groups: on a phone every control is on screen
rega measured the header at 390 px: one row, so the title wrapped to five lines and the poster-size select (x=393) and the Bless/Social toggle (x=553) ran off the edge, unreachable. On a phone the title takes one short line and the controls wrap onto rows.
2026-09-29 03:38:57 +00:00
Doxa Maps
26a351757a Prayer Progress: the basemap colours' comment states the measurement instead of citing a folder outside the repo 2026-09-29 03:38:57 +00:00
Doxa Maps
cfdb0fc606 Say what the two fill modes actually cost on the shipped data
The README described fit and fill-all qualitatively but never said what choosing
one costs, which is the number a reader deciding between them needs: on the
default list, fit draws 931 of 2,074 groups and reports the other 1,143, while
fill-all draws all 2,074 with none outside its band and none under a label.

It also records the evidence for WHY this is a mode rather than a setting: 931 at
separation 7, 1,369 at 5, and 1,369 again at 4. That plateau is what shows the
band's depth is binding and not the spacing, and without it the next reader will
reasonably try to tune the number that cannot help.

Every figure here was re-measured against the shipped tables through the
component's own join before being written down, because a stale number in a
README is worse than no number: it reads as fact and nobody re-checks it.
2026-09-29 03:34:23 +00:00
Doxa Maps
c73a0be9da Shipped code cited a document nobody outside this machine can open
Seven comments across six files pointed at SPEC-GOSPEL-INTERNET-DASHBOARD, which
lives only in _qa-cycle: never cloned, never published. A reader of the public
source met a citation they could not follow, which is worse than no citation —
it implies the reasoning is written down somewhere they may look for.

The durable part is now plugin-designs/gospel-internet/README.md, written as
BUILT rather than as specced: the views and their shared URL state, the list
model and its fail-closed rule, the two set charts (rings, bands, the label
keep-out, fit vs fill-all and why band depth is the binding constraint), why the
charts fit their box, the data joins, the columns, and the gates that guard each
of those. Section 13's honesty rules are carried over close to verbatim, because
they are constraints on the product and not notes: pins are approximate, never
"reached", never who works where, internet figures are the country's, public data
only.

Two citations did not become README links, deliberately. The facet rail's quoted
a design INSTRUCTION ("comes from the kit; do not re-implement"), so the
instruction is kept and only the unreachable document name dropped — inventing a
README section to host it would be citing myself. board.meta.json's and the
instances file's now point at the README section that actually describes the
URL-state behaviour they depend on.

The instances file is edited through the JSON parser: my first attempt did string
surgery and put unescaped double quotes inside a string value, which JSON.parse
caught before it could ship.
2026-09-29 03:34:22 +00:00
Doxa Maps
334b3d6fa1 Stage the row 130 flip, and caption the chart with a total that does not move
STAGED, NOT FLIPPED. SET_FILL_MODE in GiCenteredSetMap is 'fit' — today's
behaviour, unchanged — with the alternative named beside it so the ruling is one
line and a rebuild. 'fit' is not a placeholder: it is the honest mode for a page
that cannot zoom, so the POSTER should keep it whatever the screen chooses.

The binding constraint is recorded there because it is not obvious from the
numbers: separation can never fall below twice the pin floor (5.2), and an
18.5-unit band therefore holds at most three rows, so no minSep reaches ring 2's
658 groups. minSep 7 places 931 of 2,074; minSep 5 places 1,369; minSep 4 places
1,369 — identical, which is what proves depth rather than separation is binding.
'fill-all' places all 2,074 with none under a label and ring 2 using 18 rows.

THE CAPTION states the TRUE total — the groups that have a ring — not the number
of pins currently drawn. In the dense mode the same groups read as an arc of
colour zoomed out and as separate pins zoomed in, so a caption counting what is
visible would change as the reader moved: true at every instant and misleading
overall. It joins the paragraph that already accounts for the centre, the
no-relation groups and the unmatched ones, so those four numbers reconcile
instead of competing.

centered.shown added in all eleven locales; parity and keys-exist pass, and
hardcoded-english did not rise (257, and that fall is studio's, not mine).
2026-09-29 03:34:22 +00:00
Doxa Maps
2eafd15e1f Experimental shelf: the Wall's "?" describes the Bless poster and Social mode, in every language
Bless mode is the printable poster (a verse under every group, sizes, Print HD poster) and Social mode the shared prayer wall; the texts use each locale's own button names. UPG Neighbor's entry is removed: it has no hosted "?" (its own How it works is its explainer), so the entry showed nowhere and could only drift.
2026-09-29 03:22:40 +00:00
Doxa Maps
4f52889692 The Plugin Studio's tree says why it imitates VS Code without pointing outside the repo
Its header cited research notes in a folder that is not part of the repository, named an internal
directive, and quoted a reader's complaint. It keeps the reasoning and the list of what was copied.
2026-09-29 03:22:40 +00:00
Doxa Maps
7b5de8bcfa Prayer Walk Planner: on a phone the zone list opens from a ☰ Zones button instead of being hidden
On a narrow screen the list used to be display:none, so a walker could not see, select, rename or delete a zone there. It is now a drawer over the map from the reading-end edge (right in English, left in Arabic), behind a ☰ Zones (n) button that leads the toolbar; a tap outside, × or Escape closes it and focus returns to the button. On a wide screen it is the sidebar as before, its border now on the reading side. Two words in the planner's own 11-locale strings: zones, closeZones.
2026-09-29 02:58:03 +00:00
Doxa Maps
cf962e4065 All People Groups: no DOXA group is drawn twice; the base is re-sent slim without them
The feature-state approach left an invisible twin per DOXA group (2,616 circles drawn at alpha 0, and counted by queryRenderedFeatures). When the DOXA set arrives the base is re-sent once, slim (1.25 MB, not the old 3.7 MB), without the DOXA rows; the black pins stay on their own top layer. Popup fields and counts unchanged.
2026-09-29 02:53:16 +00:00
Doxa Maps
1226cbeb0a Say what <doxa-country-map> is, so the gate and the gallery both have an answer
check:descriptions failed on development: country-map.vue had a rich prose header
and no @description TAG, which extracts to an empty string. My header explained
at length why the bundle exists and never said, in one line, what it IS — so the
plugin-studio, the gallery and the SPA's rail had nothing to show a reader.

The tag matters more than the prose: generate-tree matches
/^\s*\*\s*@([\w-]+)\s+(.+?)\s*$/gm, so a paragraph with no asterisked tag is
invisible to it. Verified the text reaches the BUILT manifest, not just the
source, since the manifest is what the gate and every consumer read.

37 of 37 profiles now described. country-map rebuilt, tree and SRI refreshed.
2026-09-29 02:35:36 +00:00
Doxa Maps
38e36e7de4 Fold the dangling-selector gate into the portedGlobals migration
One sha instead of two, per the shepherd: both branches fixed the SAME
regions-list rule, because a gate is useless on a tree where its poison case is
still live. Landing them separately would have applied that fix twice.

★ I HAD CLAIMED THE TWO COPIES WERE IDENTICAL. They were not — I checked before
folding rather than after, and the hashes differed. The CODE line is the same in
both (`cyrillicHeading('.rl-title')`, one occurrence each); only the explanatory
comment differed. I kept the css-dangling wording because it names the arm that
now catches the bug, which is what the next reader needs. The emitted CSS is
unchanged either way — comments are not CSS — and the byte-compare baseline still
passes, which is the assertion that would have caught me if I were wrong.

WHAT THIS SHA CARRIES, both halves verified after combining rather than assumed:

  THE MIGRATION (was 4a4bc96fc). The harness stub's `.value` is now the empty
  string, so an injected-config read and a locale interpolation both behave as
  they do in the browser. Measured components 2 -> 4, no NOT MEASURED left.
  region-dashboards is byte-identical (a pure de-duplication); regions-list
  changed because its old bytes were the bug, so its baseline was re-taken.
  strongMedium() deliberately NOT adopted — the helper emits bare `strong{...}`
  where region-dashboards scopes `.rd-root strong{...}`, and swapping would widen
  the rule.

  THE GATE (was 023ca2d08). check:shadow-css-parses now fails on a selector that
  never receives a block, gated on `endsLiteral` — the LAST fragment — rather than
  on `complete`, which is false for nearly every sheet here and therefore
  suppressed the very bug the arm was written for. Four self-tests live in the
  file: the exact regions-list string is caught, a last rule WITH a block is not,
  a trailing comment is not, whitespace is not.

Re-verified on the combined tree: ported-globals 4/4 byte-identical, shadow-css
80/80, the poison case still produces exactly 1 dangling hit when reintroduced,
gates:pre 56/56, and the broken form is absent from the shipped minified bundle.
2026-09-29 02:35:36 +00:00
Doxa Maps
9b4db40443 Three post-landing gates green again: the library index, the capability table, Path2D
gates:post had gone red on development in three places that no landing runs, because landings run
gates:pre. The library index and the capability-exemplars table are derived documents: regenerated,
they now list the country-map bundle, BackToTopButton and useHostedHelp. check:no-free-identifiers
flagged the Wall's new Path2D (a canvas built-in every browser has) as never bound; it knows the
canvas built-ins now. No code changes.
2026-09-29 02:35:35 +00:00
Doxa Maps
fc3b64bbe6 Migrate the regions pages onto portedGlobals, and fix a Cyrillic rule that was inert
upg100's harness recorded my two files as NOT MEASURED because its stubs threw on
them. Both were FIDELITY gaps — the stub modelled states that cannot occur — so
they are fixed by making the stub more like the browser, never by relaxing a
comparison:

  · `.value` was undefined, so a component reading an INJECTED ref threw
    "Cannot read properties of undefined" (region-dashboards.vue:199,
    cfg.value.regionSlug — ProfileLoader always provides that object).
  · the stub threw on ANY string coercion, because a Proxy over a function has no
    primitive conversion. That alone produced regions-list's "Cannot convert
    object to primitive value".

`.value` is now the EMPTY STRING, which is the one shape that serves both real
cases honestly: a locale ref's .value IS a string, so `${locale.value || 'en'}`
correctly yields 'en'; and reading a property off '' returns undefined instead of
throwing, so an injected-config read falls through its own `|| ''` exactly as in
the browser. Symbol.toPrimitive makes any remaining stub render empty.
Measured components: 2 -> 4, with no NOT MEASURED left.

★ AND THE MIGRATION FOUND A REAL BUG, which is the part worth reading.
regions-list's Cyrillic heading rule ended with a trailing comma and NO
declaration block, so the concatenation emitted:

    [lang="ru"] .rl-title, [lang="ru-RU"] .rl-title,regions-list

A selector list with nothing to apply — the Russian heading font was silently
never applied — AND it swallowed useShadowStyles' style-id argument as CSS,
leaving that sheet unlabelled. Braces stayed balanced, so
check:shadow-css-parses could not see it, and it passed 80/80 both before and
after. cyrillicHeading('.rl-title') replaces it and emits a real rule.

SO THE BYTE-COMPARE SPLITS, deliberately, and I did not paper over it:
  region-dashboards.vue  IDENTICAL — cyrillicHeading('.rd-title','.rd-h2')
                         emits exactly the literal it replaced, verified
                         character-for-character before the swap. A pure
                         de-duplication.
  regions-list.vue       CHANGED, and the change IS the fix. The old bytes were
                         the bug, so its baseline is re-taken rather than
                         preserved. Preserving it would have pinned a broken
                         rule forever, which is the one thing a byte-identical
                         gate must not be used for.

NOT MIGRATED, on purpose: strongMedium(). The helper emits bare `strong{...}`
while region-dashboards emits `.rd-root strong{...}` — a narrower selector.
Swapping it would widen the rule to every <strong> in the shadow root, a
behaviour change disguised as a de-duplication. scrollbarGutter() is likewise
left inline: both files carry it inside a larger .rd-scroll/.rl-scroll rule, not
as a standalone declaration the helper could replace.

Both files now gated (owned:true) instead of recorded-only, so a future edit that
changes their emitted CSS fails rather than being noted.
2026-09-29 02:35:35 +00:00
Doxa Maps
65810f872f A back-to-top beside the view-list circle, and lift the Regions attribution clear of it
Two reported mobile bugs (DRV1650 #3 and #4), both settled by rega's
measurements at 390x844 on dx-life /research rather than by reading source.

★ #3 — THERE WAS NO BACK-TO-TOP AT ALL, and I had reported otherwise. I said
VerticalTabsLayout already rendered one targeting the active .vtl-host. rega:
ZERO .vtl-host elements in the live build, button.btt ABSENT from the DOM. The
chain: VTL renders .vtl-host only for `mountedElementRows`, which filters rows
carrying an `element` key — and this SPA passes :groups whose rows carry none,
mounting its own .dms-pane panes imperatively. So activeHostEl is null, and the
layout's button correctly renders nothing. That button is for layouts hosting
their own element rows; it is inert for the composition the gallery uses. I had
read the source and not the configuration.

The SPA now renders its own, bound to the ACTIVE .dms-pane — the scroller rega
measured (clientHeight 712, scrollHeight 12,373, overflow-y:auto) while the
document does not scroll at all. The gallery owns both the circle and the
scroller, so no shadow-root walking is needed. Placement reads --map-fab-beside,
published by the layout only while its circle renders, so the arrow sits BESIDE
the circle and falls back to the plain inset where there is none.
BackToTopButton already refuses to appear when its target does not scroll.

The label goes in all ELEVEN of the SPA's locales. Its translate() falls back to
the KEY, so a missing entry would have rendered the literal string "backToTop"
to every reader — en/ar/zh/ru/hi verified, and the Arabic and Chinese strings
are present in the built bundle.

★ #4 — THE REGIONS ATTRIBUTION SITS UNDER THE CIRCLE, and the geometry was not
what I assumed. rega: at 390 the attribution is details.maplibregl-ctrl-attrib in
the BOTTOM-RIGHT container, expanded to 347x44 at (22,791) — nearly the full map
width — so its LEFT END runs under the 44px fab at (12,788), 1,394 px² overlap.
The bottom-left container is EMPTY (0x0), so a bottom-left-only fix would have
changed nothing.

Both bottom containers are lifted off the published --map-fab-clearance under a
767px query, so a standalone leaf or a <doxa-country-map> with no gallery around
it reads the 0px fallback and nothing moves. The existing z-index:3 cannot help:
the circle lives in the layout, outside this shadow root, so the two are
different stacking contexts. Attribution is a LICENCE TERM of the basemap, which
is why this is a fix and not a polish item.

REBASED ONTO 0428f279c BY REPLAYING SOURCE, not by resolving a conflict: the
rebase collided only in generated bundles, where a conflict is never resolvable
by hand. Three files applied clean; RegionCountryMap.vue had MOVED to
library/components/maps in that same round (my own country-map work), so its rule
was re-applied at the new path by hand — a straight patch would have failed with
"No such file or directory", which is exactly what studio would have hit.
country-map, regions-dashboard and doxa-maps-spa rebuilt; both fixes verified in
the shipped minified bundles, including the new country-map bundle that now
inherits the lift.

NOT PROVEN HERE: the headless DOM proof needs a browser (rega's seat). What I
verified is that the button, its scoped CSS and its ar/zh labels are in the
SHIPPED doxa-maps-spa.js, and the fab lift in both map bundles.
2026-09-29 02:35:35 +00:00
Doxa Maps
b8166218b9 The country map's live poster prints its legend: it read rows the legend composable never had
countries-map's print path set the poster's legend from legend.rows, but useLegendData returns items,
never rows, so the renderer got an empty list and page 1's live map printed with no legend. It now
takes the items, with their child tiers flattened in, as research-map's poster does since A1.
check:legend-data-contract FAILED on this file and PASSES now; it goes into gates:pre with this round.
2026-09-29 02:35:35 +00:00
Doxa Maps
dcd1c13f55 The app answers each map's own ? and drops the one at the top centre
doxa-maps-spa marks the pane of every tab that has explainer text with data-doxa-help, listens for
doxa:help, and opens the same what/how dialog for the active tab. Closing returns focus to the
exact "?" that opened it (composedPath reaches into the map's shadow root). The top-centre button
and its CSS are gone; a comment for the device-rule toast that sat above the old button's rule
is back on its own rule.

check:help-reachable (gates:pre) keeps it reachable: every map with text has a hosted "?" or a
named help control of its own; the app listens, marks its panes and has no top-centre button; and
the composable, executed on a small fake DOM, dispatches doxa:help bubbling and composed and finds
the host across a shadow root. Six planted defects, each caught.
2026-09-29 02:35:35 +00:00
Doxa Maps
807c78cc64 Every Experimental map carries its own hosted ?, where its controls already are
Map controls: prayer-walk-planner (under fullscreen), gospel-internet (the map and the language
tree, the board's default view), country-posters (the country map, its entry profile) and
all-people-groups get the library HelpButton with hosted. Own bars, where there is no control
column: the Wall (after the Bless/Social toggle), the PPLR hub (the end of its view bar) and the
vision card (its top corner, at the end of the reading direction so Arabic mirrors it) use
useHostedHelp. The vision card's label is the library's own "About this map" in each of the
eleven locales, copied into vision.json rather than translated again. UPG Neighbor is left as it
is: its bar already has an About button that opens the full explainer.

Each "?" stays hidden until a host marks its pane, so nothing changes until the app answers.
2026-09-29 02:35:35 +00:00
Doxa Maps
63a2e09bde All People Groups: the map worker gets a third of the data, built once, so the map stays light
Each of the 12,379 pins carries only its row's index and one category number (1.25 MB of GeoJSON, was 3.7 MB); the popup reads the row by id, and shows the same fields (checked on 18 pins, 10 of them DOXA, and the four counts). The DOXA recolour no longer rebuilds and re-tiles all 12,379 pins: DOXA pins are a small source of their own on the top layer, and their base twins are hidden by feature-state.
2026-09-29 02:30:22 +00:00
Doxa Maps
87c59e3a1f Name the map's countries on page 1 in the packet's language
The map sheets are generated once, in English, so in an Arabic or
Chinese packet page 1's map labelled every country in Latin capitals,
and its privacy line was English.

The labels are now replaced as the sheet is lifted into the packet.
They are recognised by the generator's own rule rather than a list:
generate.mjs draws each label as esc(ISO3_NAME[iso3].toUpperCase())
from the zone data's ISO3_NAME table, so inverting that table under
that transform names the country behind every label it can draw. The
inverse is unambiguous (162 labels, no two countries sharing one), so a
label is known exactly or left as drawn. The map's privacy line is word
for word the packet's privacy_note, so it takes this language's.

A width guard: the generator placed every label and then dropped any
that would collide, measuring the English name, so a translation is
fitted into that same box - estimated wider, it shrinks in proportion
and cannot reach a neighbour the English name did not. On the Sudan
poster, six Arabic names shrink (the longest, Saudi Arabia, from 15px
to 10px); none of the narrower Chinese ones do. Capitals' tracking is
dropped for scripts without capitals, where it breaks Arabic joining.

English is untouched: its relabeller is null, and the lifted sheet is
byte-identical with and without it. No sheet or poster is regenerated -
the change is in the lift, so the tracked 3x4 posters page 1 now uses
are relabelled as they are.

Both print paths supply the relabeller: the UPG 100 profile from its
own ISO3_NAME and the library's country-name resolver, the offline
generator from the same table and the injected ISO-3 to ISO-2 table.

check-map-labels (gates:pre), on the tracked Sudan poster: English
byte-identical; in ar and zh no known English name left, halo matching
fill, privacy localized; the guard fires for Arabic, not Chinese, and
grows nothing; Arabic tracking off, Spanish kept; an unknown label left
alone. Mutation-tested five ways, each caught.
2026-09-29 02:16:33 +00:00
Doxa Maps
6af71bce45 Name the Deaf packet in its reader's language, not by its internal label
The Deaf zone printed "Deaf (affinity bloc A017)" as its title - on
page 1, at the head of every profile page and in the document's own
title. That is the legend's internal label: the unified Deaf row is an
affinity bloc spanning every region, so there is no country to name it
and zone-title.js declined it, leaving the label to print.

An affinity-bloc zone has a name of its own even though it has no
country. zone-title.js now reads it from the packet strings, keyed by
bloc (zone_affinity_A017: "Deaf people groups", in all eleven
languages), so another affinity bloc a list adds is named with one
string and no code. With no strings it still declines and the label
stays - it never invents a name. Both print paths now pass the
language's strings, English per key, the same fallback the renderer
applies.

check-zone-title-tier-a covers the unified Deaf zone, which it never
saw (it enumerates clusters.json entries, where Deaf is two older
lists): the translated name in en/es/ar, never internal text, and a
decline without strings. It now tests with the real packet strings,
which carry the multi-country joiner, so a broken one-country guard
would generate a title and be caught.
2026-09-29 02:16:32 +00:00
Doxa Maps
b2ad4607ee Title each packet in its reader's language where the title can be generated
Every packet was titled with its zone's hand-written English label, so
a Spanish Sudan packet said "Sudan" and an Arabic one printed Latin
script on page 1, on every profile page's header and in the document's
own title.

A one-country zone's title is that country's name, and Intl supplies it
in every language: the packets now carry it - Sudán, السودان, सूडान,
苏丹 - through zone-title.js's tier A, on both print paths. Every other
zone keeps its hand-written label, since a region's name is not a list
of its countries (tier B and C are a pending product decision). The
selection packet names its cover the same way.

In the browser the title uses the library's country-name resolver; the
offline generator cannot load that module under Node, so it reads the
ISO-3 to ISO-2 table and injects it, the path zone-title.js was built
for. A data set without zone-title.js keeps its labels.

check:zone-title-tier-a was committed unregistered and so never ran; it
is now in gates:pre. Its comments, and zone-title.js's, no longer cite
people or dates.
2026-09-29 02:16:30 +00:00
Doxa Maps
a4330484a4 Generated zone titles ship for one-country zones only
Measured all 29 zones in en/es/ar with a real country-name supplier
before wiring anything, and the result split cleanly.

A one-country zone generates a correct title in every locale at zero
translation cost: Sudan / Sudán / السودان.

A multi-country zone generates a title WORSE than the hand-written
label it would replace, in English too, so this was never a
localization trade-off:

  "Middle East Core"      would become  "Iran + Yemen + 11 more"
  "Scattered / Diaspora"  would become  "Andorra + Cocos (Keeling)
                                         Islands + 2 more"

and no maxParts rescues it -- 2, 3, 4, 13 just walks toward a full
13-country enumeration. A multi-country zone has a name that is not a
list of its members, so no assembly of country names can produce it.
The generator is correct; the approach does not fit that tier.

The guard is in zone-title.js, not at the caller, because six product
call sites supply this across three surfaces (the legend three times,
posters, packets). A caller-side test would be repeated six times and
would drift the first time one was missed, silently regressing one
surface while the others stayed right.

Tier B now keeps its label through the same null-means-keep-yours path
tier C already used, so this widens an existing contract rather than
adding a new one.

check-zone-title-tier-a.mjs is two-sided on purpose: a gate that only
checked "the 3 generate" would pass a build that also regressed the
other 26. It asserts the 3 produce non-code titles that differ across
locales, AND that every other entry returns null so its label survives
byte-identical -- by value, not by count, since "26 declined" would
pass even if the wrong 26 declined.

Shown capable of failing four ways: removing the guard (the forbidden
regression), declining tier A too, a supplier returning raw codes, and
a supplier ignoring locale. The third exits 75 NOT MEASURED rather than
failing, because a broken supplier makes every zone decline and would
otherwise look like a clean pass.
2026-09-29 02:16:29 +00:00
Doxa Maps
2367c2ef64 Build the prayer packet page by page to its specified structure
FULL DETAIL is four front pages, the profiles, four back pages:
  map, triangle, life calendar, % evangelical, profiles,
  bounded set, centered set, prayer wheel, 12 prayer points.
LIGHTWEIGHT is two front, the profiles, two back:
  map, triangle, profiles, prayer wheel, 12 prayer points.
Both are generated from data, in every packet language, from the
browser and from the offline generator.

PAGE 1 - the map, on a vertical page. One lift for every caller
(packet-render.js liftMap): the browser composable, the UPG 100
profile and the offline generator each carried an identical copy of
the old wrapper (250px down, scale 0.62), and three copies of a layout
decision is how page 1 drifts between print paths. The lift prefers
the map-only vertical sheet (mapSheetFiles: <zone>.packet.html, then
the poster) and places it so its names land on the same 60px margin
every other page keeps; a poster falls back fitted under the title at
0.81 rather than 0.62. The title block is drawn after the map, since a
map-only sheet begins with a full-page background. Anything that is not
a map sheet - an icon-sized <svg> from a host that answers a missing
file with a 200 page - is refused, and page 1 says the map is missing.
Page 1 now states which set it belongs to ("One of 28 UPG 100 prayer
lists"), only when the caller supplies the count.

PAGE 2 - one inverted triangle in the Vision Card's look. The card's
steps are the top (every people group, the unreached, the frontier,
the network's list), narrowing to the UPG 100 packets and, at the tip,
this packet. The card's greens, darkening as it narrows, walked
through its own colour stops; every numeral picks white or ink by the
WCAG ratio (all at least 3.4:1, large bold type). Tier widths are
geometric, not proportional - 2,107, 28 and ~100 against 12,379 would
be hairlines - so where two sets truly nest the caption states the
step removed, and the network's list, which does not sit inside
frontier, gets the card's "N of them are frontier" note instead of a
false subtraction. The packets tier and its count appear only when the
caller supplies one.

BACK PAGES - the bounded and centered sets get a page each at twice the
width, under the one page key, so lightweight drops the pair together.

THE WHEEL - the Metacamp Module 1 prayer wheel: its twelve colours,
sampled from the fill operators in the source PDF's content stream; its
orientation, the opening PRAISE at nine o'clock and READ THE WORD at
twelve; white labels with a shadow and a faint outline, so a print
path that drops SVG filters still leaves them legible on the palest
segments; "5 minutes" badges; the attribution with the website. The
labels stay the packet's own rhythm names, translated in every
language and matching the 12 points page.

The offline generator makes both packets (PACKET_VARIANT=lightweight;
the full packet keeps its names, lightweight is <zone>.lightweight and
stamped with its variant).

STRINGS: four new keys, in all eleven blocks - each block's own words
for "packet" and "prayer list", the frontier note aligned to the word
the frontier tier uses on the same page.

GATES: check-packet-a1-structure (registered in gates:pre) reads every
sheet's CONTENT for the order of both variants, page 1's lift in all
four cases, the triangle's tiers and steps, the wheel's colours and
orientation, and all 28 zones. Mutation-tested eleven ways, each caught
by its own message. check-packet-variants now asserts both sets pages
and a four-sheet difference. The browser gate check-packet-render
counted funnel rows and bars by inline-style substrings, which an SVG
triangle has none of - both would have read zero and passed - so it
now counts .funnel-n against .funnel-tier, its sheet arithmetic counts
the sets pages, and its Latin allowlist carries the renamed product
name. check-packet-browser used a copy of the old wrapper; it now uses
the product's lift.
2026-09-29 02:16:28 +00:00
Doxa Maps
502d7107e3 Every zone a reader can pick resolves to its groups, Deaf included
The legend offers 28 zones; every place that turned a picked id back
into a zone spread clusters.json's three geographic buckets, which is
27. The missing one is Deaf, whose two deaf lists the legend collapses
into a single affinity-bloc row. So in the browser, silently:

  - the Prayer packet for Deaf returned on "if (!zone) return";
  - the Deaf poster titled itself by its slug;
  - a selection containing Deaf dropped it in .filter(Boolean);
  - the packet count read 27 while 28 packets exist, and that count
    prints on page 1 and at the tip of page 2's triangle.

The packet fetch also lacked rop1, the field the Deaf selector reads,
so even a found Deaf zone would have resolved to nobody.

The fix is one enumeration. legend-tree.js now exports DEAF_SELECTOR
(the unified row's selector, which exists nowhere in clusters.json) and
zonesWithSelectors(clusters): the legend's leaves with their selectors
attached. The plugin's printableZones() keeps the 27 catalogue objects
exactly as they were and adds only what the catalogue lacks, so the
packet, poster, selection and count all see the same 28. rop1 joins
both packet fetches.

The three hand-typed copies of the Deaf literal (the map, the poster
generator, the offline packet generator) now import the one
definition; the offline generator's own id-to-selector table is
replaced by zonesWithSelectors. The copies left are frozen snapshots and
proof scripts, which keep independent literals on purpose.

The offline sweep gate's coverage had the same 27: it never required a
Deaf packet. It now counts through the legend.

Verified: the live API carries exactly 123 A017 rows, matching the
legend's Deaf count; the default 3x4 posters for sudan, kerala, deaf
and west-africa are byte-identical before and after.
2026-09-29 02:16:27 +00:00
Doxa Maps
a40fa017b4 The set charts fit their box, carry a cross, and draw the target — combined
One landing because it is one bundle: the ring band layout wiring (already on
this branch) plus the three set-fit commits, replayed as SOURCE only. The
generated tree is rebuilt below rather than merged — a previous cherry-pick of
mine swept a conflicted generated file into a commit, and hand-resolving build
output is how that happens.

From set-fit: extentFraction 0.17 -> 0.095 on both charts, so the drawing fits
the height the engine's zoom floor leaves it; crossFeature restored from history
and called by BOTH maps through one shared addCrossLayer; and the ResizeObserver
fit restored, which rega found missing because zoomAtOpen read 1.00 in all eight
chart/layout combinations — too clean to be a fit result.

Two things this merge had to get right, and neither was automatic:
- Taking GiCenteredSetMap.vue wholesale from set-fit silently REMOVED the ring
  wiring — bandFill, curvedRingLabels and openAndFrame all went to zero while
  every gate still passed. Both feature sets are now in the file together.
- Taking package.json wholesale would have deleted studio's check:ring-bands
  registration, including its gates:pre entry, landing a gate nothing runs. It is
  rebuilt from development's, with only check:gospel-set-fit added — in all three
  places this time: script, gates:pre, and //scripts.
2026-09-29 02:11:28 +00:00
Doxa Maps
4e00ec4aa1 A pin on the bounded set opens its people group too
The same interaction as the centered set and as GiTree, so a reader who learns
one chart has learned all three: click a group, its drawer opens and the camera
frames it; reduced motion jumps instead of easing. The centre dish is not a
people group, so it is excluded by tier rather than by id — an id test would
have to know the dish's private name and would break the day it changed.

This chart takes no band layout: its groups are inside or outside one boundary,
not on rings, so bandFill has nothing to spread. Only the interaction is shared.
2026-09-29 02:11:28 +00:00
Doxa Maps
3a06772a27 The centered set draws as a target, and a pin opens its people group
Wires ringBands into GiCenteredSetMap. layoutCenteredSet still decides WHICH
ring a group belongs on — that model is unchanged and still the source of the
ring numbers — and bandFill then spreads the groups THROUGH each ring's band
instead of placing them all on its circle. Measured on a 90-group sample: ring 5
went from 1 distinct radius (the single line of dots the Driver reported) to 9,
with every group placed and every ring drawing.

The ring labels and the band fill come from one source: curvedRingLabels returns
each label's keep-out arc and bandFill refuses to place a pin inside it, so pins
cannot overlap a label by construction rather than by tuning.

A click on a GROUP now opens that people group and frames it; a click elsewhere
on a ring still selects the ring. The Driver asked for "exactly like the Language
tree", so this is GiTree's path rather than a second version of it: the same
board.openGroup() call, and a camera that mirrors GiTree.frame()'s degenerate-
point branch — a single dot has no extent, so fitBounds would ask for an infinite
zoom; easeTo at the zoom the group tier becomes readable at, and jumpTo under
reduced motion. Both the drawer call and the camera are wrapped: the drawer is
what the reader asked for, and a camera that throws must not take the board down
after it has already opened.

check:ring-bands, gospel-set-charts, gospel-whiteboard, shadow-css-parses and
i18n-keys-exist all pass; both SFCs parse with 0 errors.
2026-09-29 02:11:28 +00:00
Doxa Maps
fcbdbdd349 template-bundle: each screen carries its meta.json, so a copy of the starter is listed in the Plugin Studio
A copy of the starter built but never appeared in the Studio, which lists a plugin only when it has a meta.json or an instances.json. The template itself stays off the Studio by its folder NAME (generate-tree SCAFFOLD_BUNDLES, discover.mjs), not by a flag a copy would inherit. The README's copy steps now name the meta.json beside each .vue.
2026-09-29 02:01:33 +00:00
Doxa Maps
5b6ed0d0f8 UPG Neighbor: print a prayer packet of a reached group's unreached neighbours, on the shared packet renderer
From a green group's brief: the same list as the map, the CSV and the letter (this E-level, closest first, the closest 60, as the cover says), printed by library/composables/usePosterPrints.printPacket with the renderer UPG-100 uses, not a copy. The group set is upg100's pg_slugs shape. Agreed with upg100: the lightweight variant (the %-evangelical page is DOXA-wide today), a triangle of its own (all, unreached, these N; not DOXA's 2,107, which these are not a subset of), and one explicit record adapter (ISO3 country code, the language as a plain label, IMB's GSEC for the how-reached sentence, photos joined from the doxa list by slug when an API is configured).
2026-09-29 01:54:37 +00:00
Doxa Maps
e2ac092277 RegionCountryMap moved to the library, and its gate, its numerals allowance and a comment follow it
country-map (the previous commit) moves RegionCountryMap.vue from regions-dashboard/src to
library/components/maps so a page can mount the country map alone. check:region-fill-repaint read the
old path and crashed on the move; it reads the library file now and still PASSes, with the repaint
after setData intact (the move carried the hardening that landed in 608e8ec29). check:universal-props
held the file's fixed en-US population format by its old per-bundle path; the same held decision now
sits on the library file for every bundle that imports it (regions-dashboard and country-map), which
the allowlist permits only for library/. One comment still pointed at src/useRegionsI18n.js.
2026-09-29 01:38:08 +00:00
Doxa Maps
c99fe68c38 A country map you can drop on one page, without the dashboard around it
DX-life's country pages render nothing where a map belongs. The map they need
has been in this repo since the regions clone landed — it was just unreachable
without loading the whole regions dashboard, a bundle that fetches the people
group list, builds four stat cards, a region chooser and a table, none of which
a country page wants. So this is not a new map. It is the same component, given
its own element and its own bundle.

  <doxa-country-map profile-config='{"countryCodes":["TCD"],"colorByPrayer":true}'>

RegionCountryMap and useRegionsI18n move to library/ because they now have a
second consumer, which is the only thing that earns a place there. Six import
sites updated in this commit; nothing else moved.

THE CONTRACT arrives through profile-config, the channel every element here
uses, so a generated page and a hand-written host page are configured the same
way: countryCodes, center, zoom, bounds, colorByPrayer, researchUrl,
prayBaseUrl. Two translations happen at the edge rather than by widening the
component — colorByPrayer is a boolean here and colorMode a string inside, and
countryCodes accepts an array OR "TCD,NER,MLI", because an HTML attribute is a
string and JSON-inside-an-attribute is the shape people get wrong.

THE ELEMENT FETCHES ITS OWN PINS, and that is what makes prayBaseUrl part of the
contract rather than decoration. An element handed a country code, a base URL
and no way to fetch would draw a country outline with nothing inside it — the
blank page it exists to fix, in a new disguise. Same endpoint, same fields, same
mapping as the dashboard; it asks for a country instead of the world.

THE FIELD LIST TRAVELS WITH THE MAPPING, in peopleGroupPoints.js. The obvious
version of this commit asks for PEOPLE_GROUP_LIST_FIELDS — the list the stats
need — and every pin still draws, every popup comes up empty, and nothing logs
anything. The popup half is what makes it a map you can read, so the two are
one export and neither can be requested without the other. The dashboard's own
request is unchanged: the concatenation produces the identical string.

countryUrl is additive, not in the confirmed contract. The component's country
link needs a base and DX-life's country pages live under /regions/; a host that
sets nothing gets that documented default.
2026-09-29 01:38:07 +00:00
Doxa Maps
9924e056b5 Plugin Studio releases its corner-widget hosts when it unmounts
window.__doxaStudioWidgets keeps the corner hosts across a hot update so a live widget keeps its
state, but a real unmount never released it, so the hosts and every widget element in them stayed
on the page. It is now released in beforeUnmount, except during a hot update.
2026-09-29 01:38:06 +00:00
Doxa Maps
1fb2ea9a25 UPG Neighbor: every row carries IMB's GSEC level (imb_gsec, 0-6), so a prayer packet's description can say how reached each group is 2026-09-29 01:38:05 +00:00
Doxa Maps
7e6d3324ec On a phone the research map offered no export at all
The desktop legend gets :export-enabled and @export from this file's template, so
a reader at a desk can print a poster or a packet. LegendMobile does not read
props from here. It reaches for an injected handler and decides whether to offer
the control at all by whether one exists -- mobileExportEnabled is
`typeof onLegendExport === 'function'`. Nothing here provided it, so the phone
showed no export while the same map on a desktop showed both.

That is why it went unreported for so long: it is an ABSENCE, not a broken
button. Nobody clicks a control that was never drawn, and the desktop, where this
kind of thing gets checked, was always correct.

The provide carries EXPORT_FEATURE_ENABLED rather than the bare function. The
desktop already passes that flag, so providing unconditionally would hand a phone
an affordance the desktop deliberately hides when the flag is off; and because
"is there a handler" IS the mobile switch, passing null when it is off is exactly
what makes the sheet hide the control. countries-map fixed this first and its
comment records the same two causes on my-upg-100-list, so this is the third
instance of one pattern rather than a new bug.

Measured before building: the sibling item in the same diff, the RTL-safe CSS the
research map was said to owe the country map, is already done -- zero physical
declarations remain once comments are excluded, and the one apparent hit is prose
describing why a padding exists.

closes #15 of COUNTRY-RESEARCH-MAP-DIFF
2026-09-29 01:33:28 +00:00