Page 2 of every packet is now the funnel: 12,379 people groups on earth, 7,341
unreached, 4,754 frontier, 2,107 on this prayer network's list, and N of those
in the reader's hand. A partner holding one list can see where it sits in the
whole, which is the argument the packet is making.
★ IT RENDERS NUMBERS IT DID NOT COMPUTE. Every count is read from
data/people-groups/vision-funnel.json, which build-vision-funnel.mjs generates
from the IMB CSV and the live network list and which already drives the Vision
Card. Re-deriving them in the packet would give one fact two sources; they would
drift, and two internally-consistent surfaces disagreeing is worse than one
wrong number because a reader cannot tell which to trust.
K AND THE SHARE ARE MEASURED AT RENDER TIME, NEVER TYPED, and this mattered:
the brief said "~30 lists of 50-100" and the catalogue says K=27 with counts
from 8 to 118 (west-africa 118, asia-india-rajasthan 116, uncovered-scatter 8).
Only about half sit in that band, so no copy claims it. The page states what the
data can defend -- "1 of 27 lists" and "N of the 2,107" -- and a footnote states
plainly that 21 of the 2,107 are in no list, because a partner adding up rows
would find the gap anyway. Reported to the Driver; K=27 pending his ruling on
whether the two residual buckets count.
A SHARE MUST NEVER ROUND TO ZERO. One decimal gave the single-group fixture
"0.0%", which reads as "this packet covers none of them". Verified exhaustively
over all 2,108 possible list sizes against 2,107: no size prints 0 and none is
malformed. 1 group -> 0.05%, 20 -> 0.95%, 21 -> 1%, 43 -> 2%, 118 -> 5.6%.
NUMERALS STAY WESTERN, as required. num() is toLocaleString(lang), and CLDR's
default numbering system for the bare ar/hi tags is latn -- verified 12,379 and
2,107 in both. Reusing num() rather than writing a second formatter is also what
keeps the funnel's digits identical to every other page of the packet.
PAGE COUNT NOW COUNTS THE ARTIFACT. packet.mjs computed
`1 + ceil(n/PER_PAGE) + 2` under a comment claiming the number was "asked of the
renderer, not recomputed here". It WAS recomputed, and the funnel page silently
made it wrong by one -- exactly the drift the comment warned of. It now counts
the emitted sheets.
THE GATE IS TAUGHT, NOT LOOSENED. check-packet-render went red on every fixture
("43 cards should lay out as 6 sheets, but the packet has 7") -- correct
behaviour on a structure it had not been told about. The wrong fix is to accept
N or N+1: that assertion exists to catch silently dropped people groups, and a
±1 tolerance is the slack a lost list page hides in. So the funnel is DETECTED
via .funnel-title and added as a known term, and a funnel page that renders its
title but no share figure is now its own failure -- a number printed but not
asserted on is decoration.
The page is OPTIONAL: buildPacket omits it when no funnel data was supplied, so
every existing caller keeps working and a host without the data still gets a
correct packet. Better a 4-section packet than one asserting figures it could
not read. A multi-list batch selection keeps the steps but drops "1 of 27",
which is false for a custom selection, while "N of the 2,107" stays exactly true.
Verified: check:packet-strings-parity PASS (31 string keys x 11 locales, my 10
new ones present and translated in all 10 non-English blocks, 0 declared-absent
additions). A partial locale with 3 of 10 funnel keys translated renders those 3
translated and the other 7 in English, nothing blank. Arabic packet generated:
dir="rtl", funnel is sheet 2 of 7, all four steps carry Western digits. All 16
generated packets satisfy the corrected sheet arithmetic, including the 11 older
laos packets that have no funnel page. Mutation-tested: dropping a list sheet is
caught by the independent page-header witness (20 DROPPED), a funnel page with no
share figure is caught by the new assertion, and removing the funnel page passes.
BUNDLE=my-upg-100-list vite build 319 modules clean.
NOT verified in a browser: check:packet-render launches Chromium, which the
Playwright policy reserves to rega. The simulations above parse the same
attributes from the same generated HTML.
|
||
|---|---|---|
| .. | ||
| data | ||
| fixtures | ||
| snapshots | ||
| build-gallery.mjs | ||
| build-hd-gallery.mjs | ||
| emit-countries.mjs | ||
| export-public.mjs | ||
| generate.mjs | ||
| make-previews.py | ||
| osm-basemap.mjs | ||
| packet-strings.json | ||
| packet.mjs | ||
| poster-regions.mjs | ||
| publish-html.mjs | ||
| README.md | ||
| serve-persist.sh | ||
| serve.sh | ||
| shoot.mjs | ||
| verify-posters.mjs | ||
library/scripts/posters/ — UPG-100 poster generator
Renders one 3ft × 3ft print poster per zone (28 today) for the my-upg-100-list profile, from the profile's own data. It ships inside the repo alongside that data on purpose — reaching for the data via an env var once silently rendered stale posters (Nepal, Bangladesh, Bhutan vanished).
bun run posters -> posters:shoot -> (WebP previews) -> bun run build
HTML per zone PNG @3000² assets/previews/ app shows them
generate.mjsis the template;shoot.mjsscreenshots to PNG (Firefox only — privacy-first policy).- A template change is not visible in the app until previews are regenerated and the app rebuilt.
- Exported PNGs go to a sibling folder (
../../../upg-100-posters/), neverpublic/— Capacitor packagespublic/wholesale. snapshots/holds frozen template versions (v5 is the approved current); each has its own README.
Full reference — files, env vars, gotchas, measurement discipline: ../../../documentation/architecture/poster-generation.md.