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.
The desktop card was placed with right:14px while the legend sits at the inline start. In a
right-to-left page the legend mirrors to the right and the card stayed there, so opening any
people group or country covered the legend at every desktop width. The card now sits at the
inline end, as Cities' copy of the same rule already did. English is unchanged.
Rebuilds gospel-resources only, with its five pages and the two manifests.
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.
- research-map.vue's openPacket read and wrote _packetBusy, a name 8ab3dc0f5 replaced with the
reactive packetBusy ref. Reading an undeclared name throws, so the packet export threw a
ReferenceError on every click, in doxa-research-map and in research-map-plus-grid, the production
map that compiles it. It uses packetBusy.value now (check:no-free-identifiers).
- check:list-prop-unit and check:optional-t-equivalence read every entry of library/i18n/locales as
a locale, and the folder has held a README.md since bba33e783. They read locale FOLDERS now.
- people-groups-grid's regions-look instance previewed two props (filters-toggle, backdrop) of a grid
merge that is not on development, so the page showed nothing different and
check:instance-config-keys failed. It returns with that merge.
DX Life publishes the gallery shell on a hidden page named Deep Research, so that is now the
default heading (profile-config.heading or the heading prop still override it), and the meta,
instance and demo page say the same.
profile-config experimental:false (or "hidden") hides the Experimental folder: the manifest is not
mined for experimental tabs, the folder has no rows, and a tab= request for one falls back to the
first visible tab. It is off only on request, so every other host keeps the folder as before.
useShadowStyles keys its <style> by the label and overwrites an existing
tag with the same key, so two components sharing a label keep only the
last one's CSS in a shared shadow root. The regions list lost its sheet
this way to the region card, which had kept the list's label when it was
extracted.
check:shadow-style-labels-unique (gates:pre) fails any label two calls
share, globally rather than per shadow root, so a copied or forked
component is caught when it is copied. It reads the real parsers,
classifies every reference to the function (a rebinding, .call, a
re-export or a namespace import fails instead of hiding a call),
cross-checks the AST against the token stream, and covers the other
ways two calls share a key: unlabelled calls under the dev server's
per-component key, and a fixed label over CSS that varies per caller.
countries-map.vue, a fork of research-map.vue, carried the same label;
it is now 'countries-map', with the country-posters bundle rebuilt and
the tree regenerated. The composable's JSDoc called the label a
debugging name; it now says it is the key.
Studio's 5B review found a dependency-tracking hole by testing the helper with
real Vue rather than reading it. entries was a plain array, so the watchEffect
tracked only what the FIRST registered getter read: with two panes reading
different refs, changing pane 2's source produced ZERO writes, and pane 2 caught
up only when pane 1's source happened to change. That is this helper's own bug
reappearing for any adopter whose panes read different sources. regions-mfe is
safe today because every getter reads the same cfg and spec -- which is exactly
why a reading could not find it.
entries is now shallowReactive, so register() re-runs the effect and every
getter is tracked. Also from the review:
- stale entries are PRUNED, not skipped: an adopter that rebuilds its pane
column would otherwise keep detached elements and re-run their getters on
every pass forever;
- a getter that throws is reported once per entry instead of being swallowed,
since silence recreates the symptom this helper exists to fix;
- the unused `tag` parameter is gone -- a documented check that does not exist
misleads the next adopter;
- the host argument now documents that it must be in the SAME tree as the
panes, because Element.contains() does not cross a shadow boundary and
passing the custom element would silently skip every write.
regions-dashboard rebuilt so public/ is servable at this sha for the browser
A/B. It is the only bundle that compiles the helper.
regions-mfe built each pane by creating the child element and writing its
profile-config once, inside onMounted, and nothing rewrote it. The pane is handed
a JSON STRING rather than a reactive object, so a later change to the OUTER
element's profile-config updated the composite's own cfg and reached nothing
below it. regions-dashboard is kept alive by v-show rather than remounted, so the
one code path that would have rewritten the attribute never runs again and the
panes stay in the old language for the whole session.
The leaf was never at fault: ProfileLoader's config is a computed over its prop,
lang is a computed over config, and a watchEffect drives appI18n on every change.
Every link is reactive -- it simply never receives a new prop.
library/composables/usePaneConfig.js owns the missing half: a composite registers
each pane element with a getter for its config, and a watchEffect re-forwards
whenever any reactive source that getter reads changes. It writes only when the
serialised config actually differs, because setting profile-config re-parses it
in the child and can remount what it drives -- an unconditional write per tick
would be a rerender loop. Stale elements (a rebuilt pane column) are skipped.
It is a library helper rather than three copies because regions-mfe, upg-100-mfe
and country-posters-mfe all assemble a config and set the attribute once; they
differ only in which keys they inherit, so each keeps its own
INHERITED_PAGE_KEYS and hands the result to the helper. This commit adopts it in
regions-mfe only; the other two are upg100's to adopt.
Two-sided, headlessly: a check that fails when a composite writes profile-config
on a child without re-forwarding names all THREE composites on development, and
on this branch regions-dashboard reports "re-forwarding=usePaneConfig" while the
other two still fail.
NOT VERIFIED IN A BROWSER YET. rega's LIVEDIAG is the falsifier: the nested
[profile-config] should show the OLD locale on development and the NEW one here.
country-map.vue's poster-wall wiring put the poster table, peopleGroupRecords
and the sheet code into the bundle of EVERY DX Life country page, for a wall
that is hidden by default. Measured: country-map.js 391,475 bytes on
development against 683,401 here -- +291,926, a 74% increase -- and with it
removed the bundle is 391,625, back within 150 bytes of development.
Making the three imports lazy does not help, and I measured that rather than
assuming it: with all three behind await import() and the table fetched at
mount, the bundle grew to 684,700. vite.config.js:282 sets
inlineDynamicImports, so a dynamic import is flattened straight back in and only
the wrapper is added. Removing the weight needs the pinned-served-asset pattern
that poster-render.js already uses -- card
PERF-LOAD-THE-PRINTING-SUBSYSTEM-ON-DEMAND.
library/packet/countrySheet.js stays: it is unreached at runtime (0 hits in the
built bundle) so it adds nothing, and it is the library half this element will
use once the printing code loads on demand.
ProfileLoader's comment now names both populations -- 20 of the 25 bundles are
single-profile, 10 of the 17 that actually mount this loader, which are the only
ones the default affects.
Resolving package.json conflicts by unioning unique lines is right for a file
that holds one entry per line. Applied to ordinary source it duplicates
declarations, because both sides of a hunk keep their copy and nothing
de-duplicates across the join.
Eleven plugin entry points ended with the same import twice, and the poster
renderer with two identical `const sheet`. Each is a SyntaxError: every one of
those bundles would have failed to build, and the poster export would not run.
Found by parse-checking every changed .js against the merge base, not by the
gates -- gates:pre passed 101 of 102 with all twelve files broken, because its
parse gates do not cover plugin entry points.
One measurement trap worth recording. `node --check` on a .js file parses it as
CommonJS, so a bare `import` is a syntax error and ALL 28 entry points appear
broken, on development too. That reading says the tree is on fire and is simply
wrong. Copying each file to a .mjs and checking that parses it as a module,
which separates the eleven that are genuinely broken here from the rest that
are fine.
The handoff merges brought in three references that name the Driver in shipped
text: two comments in CenteredSetTarget.vue and one _why in the people-groups-grid
instances. Reworded to state the engineering reason without the speaker -- an
open decision now reads as pending a product decision, which is what the rule
asks for. The instances file still parses as JSON.
Three lines brought in with the list-pattern doc cited the Driver; the
reference gate refuses them. The rule, the scope and the open exception
stay as stated, without the attribution.
Merging gospel/a5-cpu and gospel/panel-followup brought back two comments that
quote the Driver directly, and check:no-new-driver-references failed on them —
correctly. Development had already swept these; the merge reintroduced them from
an older base.
Both keep their reasoning and lose the attribution: a recording pill that
changes width on every word reads as instability, and on a phone the header can
take most of the panel, leaving the draft unscrollable and its last lines
unreachable. Those are the facts the rules encode; who said them is not.
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.
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.
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.
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)
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)
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)
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
<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.
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.
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.
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.
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.
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.
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.
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.
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/.
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.
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.
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.
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.
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).
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.
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.
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.
"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.
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.
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.
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.
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.
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.
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.
<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.
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.
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.
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)
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.