The word is a military term, and the Driver asked for "prayer movement(s)" / "mobilization movement"
instead. The five user-facing strings in all eleven locales (55; the translated word too: Kampagne,
campaña, حملة, अभियान, 宣传活动 …) now say movement, in each language's natural church usage: Russian
"движение" with "вдохновить" rather than "мобилизовать", Arabic "حركة" with "تحفيز" rather than "تعبئة",
Chinese "宣教动员" rather than 宣传 (propaganda) or 运动; the Portuguese lines move to Brazilian forms like
the rest of the catalogue. The instance title and the meta note follow, and the CSV exports' column
headers become bridge_language / bridge_language_code (a user sees them in every spreadsheet).
Identifiers stay: the "view":"campaigns" profile-config value (embeds pass it), the i18n key names,
file and function names, and the campaigns-server references.
Every country poster printed from the Regions tab read "map 1 of 0" and "This
map is one of a 0-map set called the Country Maps" -- nonsense on a printed
poster, on the demo's showcase flow (Regions -> a country -> Poster Map). Found
by the served check on FINAL.
WHY: makeCountrySheet called generateCountrySheet with no series, so
generateSheet fell back to its default { totalGroups:0, totalLists:0, index:1 }.
NOT A REGRESSION from the one-definition change, and worth recording because it
was first reported as one. The plugin-local copy regions-list used before that
change passed no series either; across it regions-list.vue changed by exactly
one line, the import. The defect has been in the Regions print since 168b1f1bd.
The Country Posters wall was never affected because it computes its own.
THE FIX computes the series exactly as the wall does (country-poster-wall.vue):
the tier is every country printed at the same sheet size, the total is that
tier's length, and the index is the country's position in it by id. It lives in
ONE place, seriesFor() in the library, rather than as a second inline copy of the
wall's formula -- the same duplication this branch's parent removed for the sheet
itself. regions-list passes the FULL country list, not a region's sublist:
indexing a region's own list would print "map 3 of 12" beside a thumbnail that
says "map 57 of 173".
PROVED, rendered with the values seriesFor returns:
Sudan index 1 of 173 -> "map 1 of 173" | "one of a 170-map set"
Chad index 4 of 173 -> "map 4 of 173" | "one of a 170-map set"
which is exactly the text the wall prints for the same two countries on FINAL,
measured on the served page, and what their thumbnails say. A row with no sheet
tier gets undefined, keeping the renderer's own default rather than a fabricated
position.
KNOWN, NOT CHANGED: China is the only 3x4 sheet, so its tier has one member and
it prints "map 1 of 1" / "a 0-map set" -- here AND on the wall, identically,
because this matches the wall by design. Its thumbnail says "174 of 174". Fixing
it means choosing whole-series numbering over per-tier, which is the carded
174-vs-173 question, not a freeze-night change.
The poster walls' card THUMBNAILS are stored WebP files, not generated at the
click, and all 174 were last shot on 09-25 -- teal. Since the region hue
landed, the PRINT has been hued, so both Country Posters and the Regions tab
showed a teal thumbnail above a hued poster. The Experimental section shows on
Deep Research, so that mismatch was visible.
Shot in one pass on the shared browser: 174 of 174, 0 failed, 0 retries,
23:00:37-23:10:07Z, one engine, DSF 2. Measured by the shooter: none blank (at
least 1,000 distinct colours each), and the count of sheets by their --zc hue
matches the region census exactly -- 41 Africa, 27 Asia, 27 Europe, 21 Middle
East, 18 Other, 17 Oceania, 14 North America, 9 Latin America. Asia is 27, not
28, because India has no sheet.
★ ALL 174 CHANGE, and that is the check that matters here. make-previews used to
write one directory too high (library/plugin-designs/..., library/data/posters)
while reporting success, so a reshoot could complete, look complete, and change
no served file at all. With that fixed, 174 of 174 source thumbnails, 174 of
174 full-size sources and 174 of 174 served previews differ from development,
with 0 untracked strays.
China was regenerated first with the whole-series count: 'map 174 of 174' and
'one of a 170-map set', where it had printed 'map 1 of 1' and 'a 0-map set'.
Prayer Progress is a heavy gallery tab: switching away empties its slot. If the map
is still starting, initializeMap returns silently (useMapInstance's liveness
checks) and leaves map.value null. onMounted then logged "[doxa-simple-map] Map
failed to initialize, check token and container": a console error that blamed
the token for a tab switch.
Measured on development's tip, served from its committed tree, on the shared
browser, a phone in Arabic (pp-fastswitch.mjs): switched away 150 and 500 ms after
selecting the tab, 2 runs in 10 logged it. The control runs, switched away after a
full start or never switched away, logged nothing.
Same shape as the research map's quiet cancel (0c263fb60): when the map's
container is gone or detached, both failure paths return quietly. A mounted pane
that really fails still logs.
At top 46px the button lay in the empty end of Big Picture's filter area, but that bar is a
positioned layer (z-index 55) painted over the button (z-index 20): nothing opened on click (rega,
1440, en and ar). The filter area wraps with width, so no spot below it is free at every width (at
1024 the stats row moves under a fixed offset). pos.raise lifts the BUTTON to z-index 60; the one-time
pointer stays under the bar, so no filter is covered even visually. Measured on the built tree: no
interactive element or text under the button, the button topmost at its centre, and a click opens the
popup, at 1440, 1024 and 800, en and ar.
When the reader switches tab while the research map is still starting, the gallery releases the
pane, and initializeMap returns quietly with no map. The handler logged that as "Map failed to
initialize — check token and container", so the console error landed in whichever tab was then open
and blamed the token. A released container is now a quiet cancel.
At 390 Cities' legend spans the same width as Gospel's (344px), and side 56px put the button on
the end of the legend title's first line (gospel's measurement; a probe that also counts visible
text under the button reports the same, 236px2). narrow side 4px, as Gospel.
The grid is mounted inside Big Picture as a component, so it shares Big Picture's host, and its
direction is written onto Big Picture itself. Its profile-config carried only the list, so its
language fell through to ?lang= on the page. An English page opened with ?lang=ar therefore laid
the whole of Big Picture out right-to-left, English included.
The grid now receives the host's language under both keys, locale and lang.
UPG-100's info popup lists its Middle East Core poster, then the full packet's 12 pages and the 70-card
deck (upg100's pre-generated files). Simple Research shows the same 12 pages, captioned "Example
packet: Middle East Core": same renderer, method and 'full' variant, but a UPG-100 zone's data, so it
is labelled as an example rather than passed off as a Simple Research selection.
Twelve packet pages and seventy prayer cards, rendered from the current
renderers (people-based knows-Christ page, page-filling grids and charts,
sharp card photos) and shot in the fleet's shared browser: pages at 1500px
reduced to 1200px wide, cards at 480x672, WebP q80, 2.8 MB. They ship under
previews/examples/middle-east-core/, which the existing previews staging
copies into the tree, and the popup's pages and deck examples read them.
Middle East Core is the packet covering the most people in unengaged groups
(IMB GSEC 0-1).
At 390 the desktop offsets landed on each composite's own top chrome: Regions' and UPG-100's view
selector and Gospel's search box (probe, en and ar). A tab's pos can now carry `narrow`, laid over
it while the shell is narrow (max-width:760px): Regions and UPG-100 put the button at the START of
their strip, which is empty on a phone; Gospel takes the corner above its zoom column. The same
probe then reports no interactive element under the button on any tab, 1440 and 390, en and ar.
The one-time pointer still lies over the selector or search box until first opened; it takes no
clicks (pointer-events:none).
The stored poster (data/posters) and its wall preview predated the ink pin
badge: 57 pin numbers drawn white on the zone hue, 1.44:1. They are what the
UPG-100 info popup shows and opens ("Open full size"). Regenerated from the
current poster-render in its stored 3x4 format (1500x2000) and published;
the pin numbers are now white on #1c1c17, 17.10:1. The preview and the
1500px download are made from a 3000px shot of the regenerated sheet with
make-previews' own sizes and qualities (a 3x4 sheet now makes a 420x560
thumbnail; the wall letterboxes it). The other 27 stored zone posters are
fallback-only and follow after the demo.
A geometry probe (every visible interactive element in every shadow root, intersected with the
button's box, 7 tabs in en and ar at 1440) found the default position covering each composite's Share
button exactly, which no screenshot showed. They take side 56px like the map tabs. With that, the
probe reports no element under the button on any tab in either direction; before this branch it
reported Zoom in/out under it on Prayer Progress, Cities and Gospel and Share on Big Picture.
At the default 12px from the top and the end of the line, the button covered the zoom-in button on
Prayer Progress, Cities with Internet and Gospel Resources (a control nobody could click), Big
Picture's header buttons, and Simple Research's tab bar, in English and, mirrored, in Arabic
(rega's 1440 captures, exp's second read). Each tab now carries its placement in src/tabInfo.js,
passed as MapInfo's own --map-info-top/--map-info-side: 56px from the end beside a zoom column,
46px down below Big Picture's header bar, 56px down below Simple Research's tab bar. Regions and
UPG-100 keep the default. MapInfo itself now places by the logical end of the line (upg100), so the
gallery passes no direction.
exp's second read of 1ab8524cf found a real defect on the demo path, and a census
of the tree found the cause was worse than the symptom.
THE SYMPTOM: the Regions tab printed a TEAL country poster and logged a warn,
while Country Posters printed the same country HUED. That is precisely the "two
different posters for one country" the hue change exists to refuse.
THE CAUSE: regions-list.vue:73 imported a plugin-local copy,
src/country-sheet-print.js, byte-identical to library/packet/countrySheet.js
except for the region key. The region forwarding was added to the LIBRARY file,
so countryRegionColor(undefined) returned the old flat teal and warned.
★ AND THE LIBRARY FILE HAD NO IMPORTERS AT ALL. Its own comment claimed two
consumers -- the country-poster wall and <doxa-country-map> -- and a census found
NEITHER was true: regions-list imported the local copy, and
country-poster-wall.vue:94 defines its own inline countrySheet that calls
generateCountrySheet directly (passing the whole record, which is why THAT path
was correctly hued). So the module was an orphan wearing a comment that said
otherwise, and the region key was added where nothing read it. A move that was
never finished, and the duplicate stayed invisible until one copy was edited.
regions-list now imports the library module, the copy is deleted, and the
comment says what is true. Not re-copied: a duplicate of that file is
indistinguishable from it until one side is edited, and then it is a silent
divergence.
CHAIN VERIFIED TO SOURCE, since the hue is only right if the region survives
every hop: POSTER_BY_ISO3 holds country-posters.json records, which all carry a
region; localisedPoster SPREADS the record in both its branches, so the key
survives a locale switch; postersFor passes it; makeCountrySheet forwards it.
ARTIFACT PROOF: the region forwarding in the rebuilt regions-dashboard bundle
goes from 1 occurrence to 2, and POSTER_REGIONS plus the full region-name list
are present in it.
The no-poster fallback branch in postersFor still builds an object without a
region, deliberately: it has no sheet to draw, and it already carries
unsupportedReason.
NOTE ON HISTORY, not rewritten: f6a3f85a2 is titled "Rebuild the three
consumers…" but it also carries SOURCE changes -- the ink .pin-num-bg in the
shared renderer and the warn-once in posterSheets.js. The title understates it.
It is pushed, so it stands; recorded here instead.
The four headers are copied verbatim, in all eleven locales, from dashboard.json into mapInfo.col
(peopleGroup, country, region, population); no string is retranslated. Measured with areasForBundle:
doxa-maps-spa carried [buttons, dashboard, grid, mapInfo, messages] and now carries [buttons, grid,
mapInfo, messages]. The bundle drops from 420,172 to 371,064 bytes (136,998 to 124,005 gzipped).
The clear-only-ours fix recorded the host's dir lazily, inside the rtl branch,
the first time it wrote. That is correct only while nothing else has written a
direction first. If anything set dir="rtl" on the host before that moment, the
element recorded rtl as "the page's own declaration" and then faithfully
restored it on every switch away from Arabic -- so the direction never cleared,
which is the defect this whole change exists to remove.
Measured on the Big Picture leaf: ar left rtl, and zh put rtl straight back,
while its three siblings with the same code passed. The difference was not the
code; it was that something writes rtl on that host before the element does.
Reading the attribute on the FIRST call instead, before any write of ours, is
the only moment it is certainly the page's and not ours. A page that genuinely
declares dir still owns that decision and still gets it back.
Five cases driven against the real function in all four files: no dir at boot,
a page declaring ltr, a page declaring rtl, an element mounting while already
Arabic, and -- the case that reproduces the failure -- a FOREIGN writer setting
rtl after mount and before ours. Restoring the lazy capture to one file fails
that fifth case alone and leaves the other four passing.
The push wrote `lang`. Every reader takes `locale` FIRST and only then falls
back to `lang`: ProfileLoader.vue:189, and people-groups-grid.vue at :273 and
:380. So writing one key is correct exactly while no child config carries the
other, and stops working silently the day one does -- a `locale` left by a boot
copy or a host-list path would shadow every fresh `lang` we push, for the rest
of the session, with both values sitting in the same object.
Writing both keeps them in step whatever put them there.
Not broken today: the nested configs were measured carrying no locale key at
all. This closes the class rather than a symptom, which is worth one line.
Shown by driving the real function: with a stale {lang:'en', locale:'en'} the
push now yields ar in both keys, and a config carrying only `locale` switches
too. Reverting to the lang-only write leaves {lang:'ar', locale:'en'} -- the
exact shadowing above, where the map reads English while holding a fresh lang.
Every element that sets a reading direction did it one way: if the locale was
RTL it wrote dir="rtl", and there was no else. So a reader who moved from an
RTL locale to an LTR one kept the right-to-left layout for the rest of the
session -- Chinese laid out backwards, which reads as a broken page rather than
a stale setting.
The reason the clear was missing is sound and is kept: a host page that
declares its own dir owns that decision, and blindly removing the attribute
would take that with it. So each element now remembers what it found before it
first wrote, restores that value verbatim when the locale stops being RTL, and
removes the attribute only when there was none to begin with. It clears its
own, never the host's.
FOUR elements, not the three the report named: big-picture.vue,
research-map-plus-grid.vue and people-groups-grid.vue all carry it, and so does
dashboard-plus-grid.vue -- the Big Picture composite, same shape, found by
checking the neighbours of the named lines rather than only the lines
themselves.
The comment above each one said "Only ever SET, never cleared". Left alone it
would now describe the opposite of the code, so it says what the code does.
Verified two-sided against all four shipped files, driving the real extracted
function: it clears what it set, preserves a host's own dir, and still sets rtl.
Restoring the one-way body to one file fails exactly the first two and leaves
the third passing, with the other three files unaffected.
Big Picture's export is a spreadsheet, so its example is a kind:'table' excerpt (sr): the six largest
groups by population, of 2,107, with the live table's own column keys (dashboard.people_group,
.country, .region, .population), so the popup header and the table's header cannot drift.
Cost, measured: those keys bring the 'dashboard' area into the gallery bundle, which grows from
369,635 to 420,172 bytes (123,536 to 136,998 gzipped, +13.5 KB). The alternative, header keys
duplicated into 'mapInfo' in eleven locales, is a translation job for after the demo.
The Deep Research page publishes the rail as Dashboards (Big Picture, Simple Research, Regions),
Prayer Mobilization (Prayer Progress, UPG-100 List), Gospel Research (Cities with Internet, Gospel
Resources), then the Experimental folder. Only the Prayer Mobilization pair moves. Big Picture now
says "desktop only" on every width, not only once a phone has disabled it.
check:spa-tab-order asserted the old four-name order with UPG-100 first; it now asserts all seven
published rows in order (still relative, with tab 1 positional). It needs a browser: rega runs it.
The composite re-pushed its FILTER state to the map and the grid on every
change, and nothing else. So a host switching language moved the composite's own
chrome and stopped there: the outer element received the new profile-config,
the push re-parsed each child's EXISTING attribute, and the boot `lang` survived
every write. Measured on a live en to ar switch: the map and the grid both stayed
English while the frame around them changed.
The panes were never at fault. Each child's own chain is fully reactive -- it is
reactive to its own prop, and its prop was frozen.
The map now carries the locale in the same push that carries the filters. The
grid needs its own line, because its three linked facets travel as separate
attributes while its language travels in profile-config, which this composite
set only when a host supplied a list, and never again. And a locale change now
triggers a push at all: the existing watchers cover filter state only.
The grid write is conditional so an unchanged locale writes nothing. That
matters here more than it looks: setting a pane's attribute makes it re-filter
and emit, and the composite reads those emissions -- which is what the `pushing`
guard above exists for, after a measured round trip once cancelled a selection
the composite had just applied.
Verified two-sided by driving the real pushToChildren against stub elements:
the locale reaches both panes, and a repeat at the same locale adds no write.
Removing the map's line fails that assertion alone and leaves the grid's passing.
Each production tab reads mapInfo.tab.<tabId>.lead and .takeaway, and Regions also its note, with
the India/China reasoning beside it. Only files already in the tree are listed as examples (the
UPG-100 poster); the rest join when their shots land. A tab's button renders only when its lead
key resolves in the bundled catalogue, so a key that has not landed yet hides the button instead of
opening a popup of raw key names.
Three defects on country-map/country-map, which is production -- DX Life's
country pages. Found by auditing the element rather than from a report.
1. ANY profile-config change refetched all 2,106 records. The watch getter
returned a NEW ARRAY literal, and Vue compares a watch value with Object.is,
so a fresh array always reads as changed; both computeds read the injected
cfg, so the effect re-ran on every config change and the callback then fired
unconditionally. MEASURED with real Vue: flipping isDark refetched the whole
endpoint and redrew the map, and so did a host adding pinHint. A joined
STRING compares by value, so only a real language or country change refetches
-- which must still refetch, because the API localises the names.
This was latent waste until the composite re-forward made config writes
routine. It is now on every upstream locale switch, so the two changes are
complementary rather than independent.
2. A SLOW EARLIER RESPONSE OVERWROTE A NEWER ONE. loadPoints is async and had no
sequencing at all -- 0 abort or generation tokens in the file -- so two runs
could be in flight and the one that STARTED first could FINISH last. Both
assigned points, so the map showed the previous country's pins with no error
and nothing for the reader to notice. Every run now takes a ticket and a stale
ticket writes nothing: not the points, not loadFailed, not the spinner the
current run is still using. fetch is left to complete rather than aborted,
because the response is cached for the next switch back.
3. `loading` WAS WRITE-ONLY: assigned in three places and read in none, zero
template references. A reader on a country page waited on an empty map -- no
outline, no pins, no message -- which is indistinguishable from a country that
genuinely has no people groups, and this element fetches the whole list before
it can draw one pin. "Loading results..." was already in the catalogue in all
eleven locales, so saying so cost no new string and no parity debt.
check:country-map-refetch guards all three. It drives the REAL reactive chain
with real Vue and takes the watch key expression OUT OF THE SHIPPED SFC, because
bug 1 is entirely a question of what Vue considers changed and is invisible to
any test that fakes reactivity or re-types the watch.
Mutation-tested, 5 mutations, all caught by name. The first version of the gate
MISSED one: a bare /if \(!current\(\)\) return/ grep matched the guard in the
catch block, so removing only the guard before the points write still passed.
The assertions are positional now -- the guard must sit immediately before the
assignment it protects -- and the catch guard is asserted separately. That hole
existed because the behavioural half of the test reproduced the guard's shape
inside the gate instead of reading the component, which is the self-consistent
oracle this repo's other geometry gate explicitly warns against.
Registered in scripts and gates:pre, verified by re-parsing: one key, 107
checks. Bundle rebuilt (BUNDLE=country-map), then tree, then fix:sri.
A key in a map's own area either renders blank in the gallery or, named literally, pulls that map's
whole catalogue into the gallery bundle (internal/i18n-areas.mjs). Measured with areasForBundle:
development's doxa-maps-spa carries buttons, grid, messages; this branch carries buttons, grid,
mapInfo, messages, and no upgMap. The UPG-100 entry reads mapInfo.tab.upg-100.lead and .takeaway,
which upg100 adds with every tab's strings.
The library MapInfo (an info button, a one-time pointer and a popup of real, pre-generated output)
is mounted in each pane whose tab has an entry in src/tabInfo.js, with the gallery's live locale and
its assets base. UPG-100 is the first entry, with its poster; its packet pages and card deck join
when their files are in the tree, because a missing file would show "not available" to the audience.
Regions and the other tabs follow as their owners' keys land. profile-config info:false removes the
buttons.
The research page now gives this map three tabs (engagement, prayer,
adoption). With two or more tabs the bar was a row of plain buttons: a screen
reader heard three buttons, with nothing saying they are tabs or which one is
showing. The bar is now role=tablist, each button is role=tab with
aria-selected, and the buttons are type=button. A one-tab instance shows no
bar, so it is unchanged.
The gallery re-drove the visible tab by re-setting its profile-config, which works only for a map
that re-reads its whole config live. Two production maps do not: Big Picture resolves its tStatic
strings once (2 of 352 changed on en to ar, measured) and Simple Research copies its config into its
panes once (0 of 43, 0 of 105). A fresh element reads the new locale at mount, so the visible tab is
now rebuilt in the new language whatever the map does inside, the way a provider change rebuilds.
View state is lost on a language switch; that is accepted.
Hidden tabs that are still mounted (a light tab stays mounted when hidden, and select() never
remounts a mounted tab) are reset too, so their next open mounts in the new language. The old comment
said they picked it up lazily; for light tabs they did not. Tabs are reset in place, keeping the slot
ref, because a light pane renders once and releaseTab's deleted ref would never come back.
resources-map.meta.json carried NO stage, so the manifest defaulted it to
experimental — while doxa-maps-spa, the gallery that becomes Deep Research,
mounts it: doxa-maps-spa.meta.json composes "gospel-resources/resources-map"
and TAB_DEFS names it as the Gospel Resources tab. The record disagreed with
what ships, and the record was the part that was wrong.
WHAT THIS CHANGES: the manifest stage, and nothing a reader sees. Verified —
the diff moves one "experimental" to one "production" and ZERO sha384 hashes.
No bundle is rebuilt and no byte of any map changes.
The stricter production gates now apply to this profile and it passes them:
gates:pre 103 PASS / 0 FAIL / 1 NOT MEASURED of 104. That is the part worth
having, because marking something production is only honest if it then has to
meet the production bar.
NOT a decision about which profile the Deep Research page SHOULD show.
gospel-dashboard is the production composite and remains the better end state;
it is not switched to today because its .gd-map is sized 80svh — a viewport
unit inside a pane that is not the viewport — which against rega's measured
panes gives the map 91% of the pane at 1440x900 and 95% at 390x844, and
because it has never been shot inside the gallery. That fix is written and
held local for after the deadline.