Conflicts resolved by taking the pilot's side: upg100's
my-upg-100-list/poster-wall.meta.json (its new label) and the generated
files (regenerated). upg100's posterDesign block is re-added in the next
commit.
Seventeen lines in 15 files. Six quoted short fragments from feedback; eleven used "design note" where a
person once stood ("design note selects 2+ rows", "a design note design call"). Each now states the
reason in neutral terms and keeps the technical content and dates.
All edits are comments, except one string: the safety_note text in
library/scripts/research/derive-city-candidates.mjs, a research script that is not bundled and whose
output is not committed. No logic changes.
Each country's region is set to the live wagf_region its groups carry (file
label spelling); only the region field changes (stats, bbox, count, India row
untouched). The stale 'Other' bucket (18 countries) empties out and each moves
to its real region: USA and the non-Spanish Caribbean + the Guianas to North
America & Non-Spanish Caribbean; Andorra/Monaco/San Marino to Europe; Cocos,
Christmas, Niue, Norfolk, Tokelau, Wallis & Futuna to Oceania. Europe goes
27 -> 30, matching the live Regions dashboard.
check:poster-section drops 'other' from its covering scopes (the bucket is now
empty, so the /regions/other scope resolves to zero countries); the seven real
regions still cover all 175 countries exactly once.
Region-only diff (18 region lines); B25 India withheld unchanged
(check:country-sheet-one-path, poster-section, poster-browser-equiv,
poster-design-catalogue all PASS).
previewUnreleased() is import.meta.env && import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true, and the
India tab uses the same expression inline, so every BUILT bundle compiles both away. A page, an embed or a reader's
console setting the global can no longer print the withheld poster from production; only the Vite dev server (the
pilot's Plugin Studio, whose element bootstrap sets it) previews it. posterWithheld()/largeSheet() take the preview
state as an optional argument, so a gate can test an open preview directly.
check:country-sheet-one-path arm G: a global alone does NOT open the preview outside DEV; withheld by default; an
open preview prints 66x66; the India tab is gated on the DEV read; and the built tree mentions the flag 0 times
(the stale ungated build failed this check, as it should). A poison for each.
It is not released yet, and the pilot syncs to DX Life (and the partner handoff) within minutes. So WITHHELD.IND is
{ poster: true, rows: true, preview: true }: posterWithheld() is false ONLY when the host page sets
globalThis.DXM_PREVIEW_UNRELEASED === true (the Plugin Studio on the pilot, where it is reviewed); never by a URL
parameter. Every other host keeps the withheld sentence on India's card, and Country Posters' India tab exists only
under that flag. The rows (list, packet, cards) stay withheld everywhere.
check:country-sheet-one-path arm G proves (a) withheld by default and (b) printing at 66x66 with the flag (only
=== true opens it), rows withheld in both, the India tab gated, with a poison for each; check:poster-section arm C
expects the sentence by default.
countrySheet.js WITHHELD now says WHAT of a whole country is withheld: { IND: { poster: false, rows: true } }, with
posterWithheld() and rowsWithheld() (sheetWithheld() keeps meaning the poster, the name Poster Design reads).
- India's whole-country POSTER prints again, 66x66 by default (60x96 and 36x60 on request); largeSheet() names it.
- Its ROWS (the prayer list, packet and cards: about a thousand pages) stay withheld with the same sentence, in the
wall, the Regions list and the 'country' poster type. India's state posters are unchanged.
- Country Posters gains an INDIA tab: the same wall filtered to India, labelled with India's own name in the
reader's language (Intl), between the posters and the map.
check:country-sheet-one-path arm G checks the split (poster live at 66x66, rows withheld, state cards never
withheld, both callers read the right half), with poisons for un-withheld rows and a wall that prints them;
check:poster-section arm C follows. The by-state, leaderless India layout follows as its own PR.
The section the Country Posters tab draws (a collapsible region head + its poster cards) is now usable on its
own, for any poster set, by parameters:
- library/components/poster/posterTypes.js: a TYPE REGISTRY. A type is an adapter (items for a scope, the
section's title, the print actions). The library registers 'country' (library data + the shared sheet path,
so a withheld whole-country poster stays withheld); plugins register their own types from their own code
(UPG 100 lists, regions, continents later). No plugin code in the library.
- 'country' scopes: a region key in any spelling. DX Life's /regions slug ('latin-america-and-caribbean'),
the API's value, the wall's slug and the English name all reduce to one canonical key.
- PosterSectionView.vue: one section, drawn by a PosterWall in a new SECTION-ONLY mode (no wall head; off by
default, so every existing wall renders as before), embedded so the page scrolls it.
- <dxm-poster-section type scope collapsible collapsed locale assets-base api-base-url is-dark>: the
attributes map onto the standard element's profile-config (posterSectionConfig.js, pure), so it keeps the
per-instance i18n and store. registerPosterSectionElement() from any bundle; Country Posters ships it first.
- country-posters/poster-section: a page showing two sections (Africa open, Oceania collapsed).
- poster.sectionNone (11 locales): an unknown type or an empty scope says so, never a blank.
check:poster-section (gates:pre) + self-test: the registry, DX Life's 8 scopes covering every country once,
withheld India, the attribute mapping, and PosterSection.vue injecting every name its template uses; 3 poisons.
The known collapse-while-scrolled landing is left exactly as it is.
India's whole-country poster does not ship yet; a solution is in progress. countrySheet.js gains WITHHELD (India)
and sheetWithheld(item):
- makeCountrySheet draws nothing for a withheld country, in any format, before fetching a single record.
- largeSheet() is null for it, so the Country Posters wall and the Regions list show their existing
messages.countryPosterTooMany ("Too many people groups to fit on one poster legibly — a solution is in
progress.", all eleven locales) on the card instead of a poster.
- Both callers also withhold its list, packet and cards with the same sentence (about a thousand pages, offered
when the poster is).
- India's STATE and union-territory posters are separate cards (indiaState) and are untouched; so are the India
section and its first place.
- sheetFormatFor keeps the large-sheet size rule itself (it returns with the poster).
check:country-sheet-one-path arm G: India stays withheld (no sheet in any of 7 formats, 0 records fetched, its
state cards never withheld, both callers show the sentence and reject its rows), with a poison for un-withholding
it and one for a wall that prints its list.
Measured on the shipped India sheets in the browser (getBBox, Lato loaded): 74 of 151 photos on the 60x96 and
32 on the 66x66 overlapped their own name, by up to 7 px (half a letter at 20.7 pt). The photo was placed from
an ESTIMATED text width, and the real width runs wider.
Now the photo sits in a fixed slot on the LEADER side of its name, and the name moves out by the slot. A
side-lane name is anchored at its leader-side end (left column: text-anchor end; right: start), so its glyphs
lie wholly on the far side of that anchor and the photo wholly on the near side: they cannot overlap whatever
width the font draws. Only a name with a photo takes the slot, so the columns and the type keep their size
(India 60x96 stays 20.7 pt).
Measured after: India 60x96 and 66x66 have 0 photos on, and 0 touching, their name. 0 proper crossings on all
three India formats. 36x60 (no photos) is byte-identical. Re-pinned.
- check:side-lanes: every photo on its name's anchor side, at the 4 px gap (a source twin, always measured).
A self-test poison restores the estimated-width placement: 167 photos off their side, caught.
- check:side-lane-photo-pixels (browser): every photo box clear of its name's real drawn box (getBBox), with
the same poison in its self-test.
countryPosters.stateNames x11, keyed by ISO 3166-2, for the India section's CARD labels (the wall, the print HUD, the
packet and list titles). The printed sheet stays English by recorded decision, like every country poster.
Generated, not typed: scripts/emit-india-state-names.mjs reads Wikidata labels by ISO 3166-2 (P300), dissolved items
excluded; Wikidata is CC0 (source, licence, query, date and every fallback in data/india-state-names.meta.json).
- Four codes ISO has renamed (IN-CT/OR/TG/UT -> CG/OD/TS/UK) are queried under the current code.
- English is the table's own name. Where the table is briefer than its item ("Delhi" vs "National Capital Territory
of Delhi"), the same-named item's labels win, so hi/ru match the English.
- Reviewed by eye (hi, ar, zh): two explicit overrides with their evidence (ar Odisha = the ar.wikipedia title,
not the pre-2011 label; zh Dadra and Nagar Haveli and Daman and Diu = Simplified, the only label being
Traditional). A zh name from a plain "zh" label with no reviewed override FAILS the generator (poisoned: it did).
The India section retries a failed table fetch at 5 s, 20 s and 60 s (bounded, cancelled on unmount), instead of
staying hidden until the filter changes.
check:country-sheet-one-path ARM D: every locale's stateNames == the table's 36 codes (a runtime-built key the i18n
call-site gates cannot see), English == the table, and the licence recorded; its two poisons join the self-test.
On a phone, India's Prayer List (Full detail, Lightweight) reported "that list has no people groups to print" on a
card of 855: its rows resolved [] and the print path read that as an empty list. Now, in the wall and the Regions
list alike, a desktop-only card's rows REJECT with the notice before any record is fetched, and the card shows the
same words: a new poster.phoneLimitAll ("This needs a desktop or laptop computer.", all eleven locales). The
existing "the largest sizes are off" stays for cards that are only partly blocked.
check:country-sheet-one-path arm F asserts it for both callers, with a poison for each.
Measured on the 60x96 print: every photo beside a name rasterised as the browser's broken-image glyph. An SVG
rasterised through <img> is an isolated document that fetches no external resource, and none of the people-group
photo hosts (media.joshuaproject.net, joshuaproject.net, images.peoplegroups.org and its resizer,
peoplegroupspublic.blob.core.windows.net, s3.doxa.life) sends CORS, so a photo cannot be inlined as data: in the
browser either.
- poster-raster.js: rasterPoster drops every <image> whose href is not data: (the basemap tiles are data: and stay),
so a download shows the paper, never the glyph. hasExternalImages() says when a sheet carries such photos.
- PosterWall: printing a generated sheet that carries photos prints THE SHEET ITSELF through printDocument, where
the photos load, on its own @page (60in 96in), instead of a raster shrunk onto letter. Same HUD steps.
- check:side-lanes: photo arms (0 external images reach the raster; the wall prints such a sheet as a document),
each with a poison in its self-test.
- check:side-lane-photo-pixels: the pixel side, a browser gate (a data: photo draws red; 99/99 external slots stay
flat; without the strip 0/99 do). Declared on-demand until its first browser-seat run.
India can print at 60x96 (the default), 66x66 or 36x60. makeCountrySheet used to map any non-packet request to the
default, so a 66x66 or 36x60 request drew the 60x96 sheet. sheetFormatFor(item, format) (exported, pure) now keeps
a requested size the country can print at, falls back to its default otherwise, keeps a packet page a page, and
returns exactly what was asked for every other country.
check:country-sheet-one-path asserts that table (India 66x66/36x60/60x96/letter/3x4/packet; Nepal 3x3/letter;
China 3x4), with two poisons: India ignoring a requested 66x66, and a packet page turned into a large sheet.
5B fixes on the side-lane mode:
- The side-lane styles' template slot sat on its own line, so every OTHER format's page gained one newline. It is
now joined onto </style>, emitting nothing at all off side lanes.
- A photo is drawn only over https from the hosts that serve people-group photos (media.joshuaproject.net,
joshuaproject.net, images.peoplegroups.org, peoplegroupspublic.blob.core.windows.net), never the generic
placeholder; anything else is dropped. Re-pinned.
check:side-lanes' seeded data now also carries photos on a host that is NOT allowed; none may be drawn.
CORRECTS the previous commit's "no prayer list, packet or cards for whole India": the current direction is that
whole India's poster, list, packet and cards (about a thousand pages) are offered DESKTOP-ONLY, and a phone shows
the existing poster.phoneLimit sentence instead of failing.
countrySheet.js now exports LARGE_SHEETS ({ IND: 60x96, desktopOnly }) and largeSheet(item), and makeCountrySheet
itself prints such a country in its large format (a packet page stays a page) with its "Administrative Areas"
cell from the shared state table ("36 states and union territories"). So every caller gets the same sheet: the
Regions list's India no longer has to disagree with Country Posters (regions-list's own lines follow, from regions).
The wall drops its India-only code and follows the same rule.
India's card is no longer "too many to fit": it prints as the 60x96 side-lane sheet (all 855 groups named beside
the map, crossing-free leaders, real photos), whatever paper the wall asks for, until the shared size picker
offers 66x66 and 36x60 too. Its facts row reads "36 states and union territories" (English, from the wall
head's plural key). As ruled, the whole-India card offers no prayer list, packet or cards; the state cards keep
theirs. Its card preview is the real sheet.
check:side-lanes (+ :self-test, gates:pre): the three side-lane formats on a seeded worst case (855 groups,
stacked and clustered) through the real renderer: every name once, 0 crossings re-measured from the SVG, no
overflow, photos only where real; without the lane clamp the same data crosses 36 times, and the arm fails.
The India section's cards are India itself, its 34 state and union territory posters and the two with none listed,
so "· 37 countries" was untrue. Through PosterWall's sectionItemCountKey, that section alone counts its state cards
(36, from the data) with countryPosters.indiaItemCount; every other section keeps the countries count.
The key in all 11 locales, in each locale's own plural forms (the same form counts as regions.countryCount).
Each state's sheet drawn from live records with the browser's own inputs (SPARSE 3x3, the indiaState series, the
served boundaries and places), shot in the shared browser at 3000 px, and reduced by make-previews.py to the 560 px
card thumbnail and the 1500 px download, as the country previews are. Every record pinned; every layout octilinear.
India's country card, its 34 state and union territory posters and the two with none listed now share one section,
headed by India's name and counted once (855, through PosterWall's sectionTotal), which puts it above every regional
section. One line at the top of the wall says why India is set apart. Asia keeps every other Asian country.
check:country-sheet-one-path ARM E: the wall passes sectionTotal, India's card is in the India section, PosterWall's
default is still the card sum, and over the real data India comes first with 855 (India 855 > Asia 454 > Africa 314).
The census on the assembled branches still found eleven comments that attributed a choice to a dated
request, quoted it, or spoke in the first person: pplr-data-maps (geocoder, pin card, pin indicator, tree,
families, religion, semantic trees), research-dashboard's ramp note, upg-100-list's funnel note, and two
dated QA tags each in countries-map and research-map. Comments only; behaviour unchanged.
Comments only; behaviour unchanged. Each rewritten line keeps its technical fact and drops the attribution:
- design-note and brief quotations restated as plain design statements (country-poster-wall.vue, index.js,
countries-map.vue);
- "(qa: <date> …)" tags and "user feedback / user reported" attributions removed (countries-map.vue);
- fleet agent names replaced by what was measured (countries-map.vue, country-posters-mfe.vue);
- series.json's India-state note: the approval date is gone; the wording stays the editable default.
The unfiltered wall gains an "India — states and union territories" section: 34 cards, one per state or union
territory with people groups listed (from the shared india-states table, fetched and sha-checked only when the
section is shown), each with its poster, prayer packet and cards. Nagaland and Mizoram get a card saying no
unreached people groups are listed there, so no division silently disappears. A host's filtered wall
(countries/region) is unchanged and never loads the table.
src/indiaStateSheet.js draws a state's sheet through ONE path: its records by slug, its outline from the
ADM1 boundaries the renderer already loads (matched on shapeName; the shared collection is copied, never
mutated), India's hue, and the indiaState series ("India State Maps", map N of 34). Show on map for a state
frames India, since the countries map draws countries.
check:country-sheet-one-path grows the state arms: inside this plugin only the helper calls generateSheet,
the wall must use makeIndiaStateSheet, and over the served table + ADM1 file: 36 divisions, the 34 with groups
numbered 1..34 each with an outline, the rest named, the ADM1 collection unmutated. Self-test 10/10.
Rendered headlessly through poster-render for Gujarat (90 pins), Lakshadweep (1), Andaman and Nicobar (9):
the state's name as title, "India State Maps", its series position, no country-series text, no "Province".
A second country without a sheet size would silently join India's oversized
series and print India as "map 1 of 2". The comment now says so, and
check:country-sheet-one-path fails when more than one country has no sheet,
naming them, so that change is a decision rather than drift. Its self-test
plants a second one (China) and requires the arm to fail.
The wall had its own inline wrapper around generateCountrySheet, a second
copy of @map/packet/countrySheet.js, which the Regions tab uses. Two copies
of one wrapper drifted twice (a teal Regions print, then India's series). The
wall now calls makeCountrySheet with the same country list, still handing it
the data's own English record rather than the localized card.
Behaviour: identical sheets for every country with records; a country whose
records come back empty now yields no sheet (the wall falls through to its
stored preview), as the Regions tab already did.
check:country-sheet-one-path (in gates:pre, with a self-test) parses every
plugin source: a direct generateCountrySheet call fails, every
makeCountrySheet call must pass the series list, both known callers must be
found, and seriesFor gives named answers over the real data (India 1 of 1,
each 3x3 country N of 173, an absent code no series). It fails on
development c4655072a for all three defects.
profile-config gains two optional keys for the country-poster-wall profile: `countries` (ISO-3 codes, or
{code, name, count} records, in the host's order) and `region` (one WAGF region as the data names it).
With either, the wall is flat: one section, nothing collapsed, two columns unless `columns` says otherwise,
no heading unless `heading` is given, because it sits under the host's own region heading. A requested
country with no poster keeps its card and says so in the regions catalogue's words. With neither key,
every binding is what it was, so the plain wall is unchanged. PosterWall itself is untouched; only the
country-posters bundle rebuilds. The usage lines in index.js and the profile's meta note name the keys.
The country poster previews were the dense layout -- small facts table, both
vision columns, a smaller map -- while every UPG-100 thumbnail is SPARSE. Now
that the click-time print is SPARSE too, the stored country previews have to be,
or the card and the print disagree.
Generated with SPARSE=1 and checked BEFORE any browser time:
· the generator's own layout guards: 173 of 173 ok, octilinear, 0 diagonals.
· SPARSE on all 173: 0 sheets draw a dense-table element; all 173 carry the
bold "N UPG's" count.
· real values: 0 sheets show 0, NaN or blank population. Worst cases by UPG
count: Sudan 105 / ~20.7M, Brazil 103 / ~63K, Laos 95 / ~968K.
Shot on the fleet's shared Chromium (no private browser), one page at a time:
173 of 173, 0 failed, all 3000x3000, none blank. Smoke-tested and looked at
first: Sudan shows the bold facts row, no vision columns, "1 of ~170 maps".
ALL 173 CHANGE: 173 source thumbnails, 173 full-size and 173 served previews
differ from development, 0 strays, nothing written to a stray library/ path.
China is deliberately NOT here: its thumbnail stays the dense "map 174 of 174"
it has tonight. Its print becomes SPARSE with "map 1 of 1" via the small-total
guard. The remaining numbering difference is R-CHN-SERIES-TIER, a product call
after the demo.
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'.
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.
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.
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.
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.