44 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8105b1e76a |
Prayer packet: point 4 prints a short reading list, and the 12 points are larger
Point 4 (READ THE WORD) printed Mark 5:1-20 in full at 14px, the longest and smallest text in the packet. It now prints THE HEAVENLY FATHER'S HEART: one heading, one framing line and seven short readings (Noah, Lot, Rahab, the Gerasene, Cornelius, Lydia, the jailer), each a household the Father saved through one person. The names come from each locale's block; the addresses stay canonical English in the data and the book is named in the packet's language, like every other reference (Genesis and Joshua added to the 11 books tables). That room goes to the twelve points: body 17 -> 22px, heading 19 -> 24px, references 14 -> 17px, and 13 -> 16px between points. The page still fits one sheet in all 11 locales (the tightest, de, ends 59px above the bottom). The gates that asserted the full passage (packet-render, packet-browser, poster-prints) now assert the list: a .p4-heart block with its seven readings. Rebuilt: my-upg-100-list, country-posters, regions-dashboard, doxa-research-map, upg-neighbor; the tree and SRI regenerated. |
||
|
|
190e7665a7 |
The packet's low pages fill the sheet: measured, then re-shaped
Measured in a browser (content's bounding box against each sheet, en/de/ru): the knows-Christ grid ended 72% of the way down its sheet (478px of blank paper below), and the two single-chart sets pages 60% and 69% (702px and 525px). The same 5,200 boxes now lay out 65 x 80 in 14.5px squares, which spends the height on bigger boxes; the sets charts (SVG, so they scale) grow from 960px to 1320px (bounded, nothing below it) and 1240px (centered, room for its ring legend, which widens with it). Now 88%, 79% and 84% of the height, with no page clipped in en, de or ru. The width classes are hung on the row (set-solo-<kind>), not on the page, so no page class name enters the shared stylesheet. |
||
|
|
42537f11c9 |
The knows-Christ grid fills the page: the same 5,200 boxes, 80 x 65
52 columns exist only because a year has 52 weeks; on the knows-Christ page a box is a share of people, and at 52 x 11px the grid used about half the sheet's width. Without axes the same 5,200 boxes are laid out 80 x 65 (both divide 5,200, so the rectangle is whole), the columns stretch to the frame and the cells stay square, still shaded from the top in reading order. The height this frees goes to a larger legend. The life calendar keeps its 52 week columns and its axes. |
||
|
|
7795eba447 |
The knows-Christ totals come from the unrounded share
Only the printed percentage rounds. A large list at 0.004% is tens of thousands of people, which printed as 0 when the total was computed from the rounded figure; a share below the last printed digit now reads "<0.01%" beside its real total. check:evangelical-share asserts it. |
||
|
|
d78e485643 |
"Who among them knows Christ" counts people, not weeks
The page reused the life calendar's grid and counted its shaded boxes in
weeks ("0.61% · 32 weeks"), under a week axis and a year axis, with a lede
calling it "one human life in weeks". On this page a box is an equal share
of the list's PEOPLE, so the same 5,200 boxes are now drawn with no week or
year axis, and the legend states each side in people: how many follow
Jesus and how many do not yet, each with its share (the two percentages
sum to 100 as printed), plus how many people one box stands for. Numbers
are formatted in the reader's locale.
The labels become "Followers of Jesus" / "Not yet following Jesus" and the
lede drops the weeks, in all eleven languages; the served strings file is
re-pinned. check:evangelical-share asserted the weeks figure; it now
asserts the people totals, the bare percentage and the absence of weeks
and axes, with the life page (which keeps its axes) as the negative
control. check:packet-variants found the grid by an exact class string
and now matches the class prefix.
|
||
|
|
7067c0ae96 |
Fix what an adversarial review of the print standard confirmed: 21 defects, each re-checked against the code
Ten agents read today's branch (five finders, one skeptic per area who had to confirm each finding
against the current code); 21 distinct defects survived. All are fixed here.
Generator and its proof
- generateCountrySheet fed only ADM0, so a country beside India or China printed without their
provinces (Nepal: 251 KB against Node's 921 KB). It loads the full boundary set, as the driver does.
- check:poster-browser-equiv rebuilt its own feed, so it could not see that. It now calls what the
browser calls (makeZoneSheet, generateCountrySheet — Nepal), deletes the target before the Node run,
runs Node with a clean environment, and FAILS (not "not measured") when the Node generator breaks.
Proven two-sided: it fails with the ADM1 fix reverted.
- The facts table's country list sorted in the reader's browser locale; pinned to en-US (Node's
default, so Node's output still hashes to the baseline).
- zone-sheet-print.js imports the library relatively, so bun, Node and the bundler resolve it alike.
Print seams and wiring
- The wall's Poster Map button used its own stored-file fetch and never the generated sheet, so every
country poster 404'd on the published tree; it now prints the generated sheet first.
- Country Posters wall: card stocks passed (its Card Deck could never print), iso3-names passed (zh/ar
page 1 kept English names), the stored fallback reads previews/ (where the tree serves them).
- Card-stock labels render at render time, so a locale change relabels them.
- The records provider caches by language AND list, and does not keep a failed membership load.
- The UPG-100 legend's Card Deck returns quietly on a parent row instead of an evaluator error; the
selection fetch uses PRINT_FIELDS.
Packet
- The centered-set caption counts what is DRAWN (a ring too full to separate reports its remainder).
- Each ring's number lane is sized to its own radius (ring 1 put a dot under its badge).
- Per-box line keeps a decimal below 10 people ("≈ 0" for small lists); worksheet rows name the country;
the localized poster kicker is fitted against the poster's real 19px, not 22.
Plurals and gates
- regions.countryCount / regionCount were authored in English zero|one|other order in de, es, fr, hi, it
and pt, so CLDR picked "27 Land", "1 países". Rewritten in each locale's own order; check:plural-forms
now fails a string in English order (proven on the old German string).
- check:optional-t-equivalence (gates:post) crashed on the new plural fallback's imports; it injects the
real cldrChoiceIndex and asserts the plural path (ru 21/3/5, en 0/1/7).
- check:packets-all and check-packet-render counted people as cards only, so every lightweight packet
read as "a packet of nobody"; worksheet rows count, and the browser gate checks the grid's top fill
(the renamed .lc-frame) instead of the retired scatter. check:packet-variants now reads the grid itself
— 5,200 boxes, Jesus' 1,716 from the top, the % shading from the top — proven: it fails on a renderer
that shades the last boxes.
gates:pre 68/68, check:optional-t-equivalence PASS, both bundles build.
|
||
|
|
88f747cf5c |
The bounded set and the centered set each get their own page again, at print size
A second look: the bounded set on one page and the centered set on another. Each chart keeps everything the redesign gave it — its heading, the cross at the centre, the nine numbered target bands with dots spread through them, the full ring legend — as one large square panel (960px) centred on its page, instead of two 660px panels sharing one. The centered set's page carries the legend; both carry the pair's heading so they read as one spread. Also the profile row's photo URL is escaped (esc(src)), the identical hunk regions landed in 1aeafead0, so the two merge cleanly: a quote in an API image_url closed the attribute and injected markup into the printed document. check:packet-a1-structure and check:packet-variants assert the two-page order and the derived lightweight delta (four back pages dropped). |
||
|
|
58e113549c |
The %-evangelical page says how many people one box stands for
Brief A1.4 asks the page to state how many people each cell represents; it printed the share and the
population but never the unit a reader needs to read the grid ("each box ≈ 40,208 people" for the
whole DOXA list). Computed with the screen chart's own peoplePerCell so paper and screen divide the
same way, in all eleven languages, and omitted when the list has no published data.
|
||
|
|
46a53f0b4e |
The carried commits keep the reason and drop the speaker
Thirteen lines arriving in this landing named or quoted the Driver: the packet renderer and its two gates, and the desktop build. check:no-new-driver-references fails on each of them, so they are reworded here the same way as the sweep: the date and the requirement stay, the speaker and the quote go. |
||
|
|
bfc99ff827 |
library/ keeps the reason and drops the speaker: no comment quotes or cites the Driver
Every reference to the Driver in library/ is reworded: the engineering reason stays, the speaker and the quote go, and a decision still open reads "provisional, pending a product decision". Comments only: each changed file is identical after stripping comments (JavaScript, template and CSS, including CSS comments inside the style strings shadow-DOM components inject). Those style-string comments do ship, so several bundles change by exactly those comment bytes. |
||
|
|
0c40927dcf |
The prayer points name their scripture in the packet's language
Every translated packet printed its references in English — "Matthew 27:27" in Chinese, Arabic and the other nine — because refs are kept as canonical addresses and were printed verbatim. The address stays canonical in the data; the BOOK is now named from a per-language table (the ten books the points cite, in each language's common form: 马太福音, متى, Мф., Matthäus …). A localized name in a right-to-left script is isolated rtl so it reads on the right with its chapter:verse intact. Measured: 24 references per packet, 0 English book names left in zh, ar and ru. Point 4's printed passage is untouched: which licensed translation to print per language is still an open decision. |
||
|
|
612d9c3ea8 |
The print standard takes a map the host generates, and says what it takes
A host that draws its map live (the regions country map) has no posters/<id>.html to lift, so its packets printed page 1 with the missing-map notice. usePosterPrints gains opts.mapImage(item): an image or data: URL of the host's own map, placed by the renderer (mapFromImage) exactly as a poster sheet is, the whole page inside the white margin. Order for page 1: an explicit mapSvg, then the generated map, then the stored sheet. PosterWall forwards it as a prop. Measured two-sided through the real printPacket: with mapImage the image is page 1 and no sheet is lifted; without it the stored sheet is. The composable's header now documents the whole contract — every option, which are optional, and the row shape — so a map adopting it reads one comment instead of the wall that uses it. |
||
|
|
4e2685cdad |
The packet follows the printed Module 1 sheets: page 1 is the map alone, the calendar holds Jesus' 33 years, the sets share one page
A review of the full-detail packet, page by page: - Page 1: a poster carries its own title, so the packet's title above it printed two headers and shrank the map to fit beneath them. A poster sheet now fills the page inside the white margin by itself; in another language its kicker and title become the packet's own, fitted to the English width. A map-only sheet keeps the packet's title in the band it leaves empty. - The life calendar is laid out like the Module 1 "Life Calendar in Weeks": square boxes, week numbers across the top, the year down the side, a rule between decades, framed and centred. The first 33 years are shaded as the life of Jesus (1,716 weeks). - The %-evangelical grid fills from the top in reading order instead of scattering. - The flow pages had no page padding at all, so their headings sat on the sheet's edge; they keep the same margin as every other page now. - The bounded and centered sets are one page, two square panels side by side as on the Module 1 list of 100, each headed by name with a cross at its centre. The centered set is a target of nine numbered bands, its dots spread through their bands by the shared ringBands geometry (no overlaps, none on the numbers), and the legend explains every ring with its count. - The lightweight packet lists every group as a worksheet: a short note, one column header per page, numbered rows sized for paper with room to tick, no photos or descriptions. Seven new strings in all eleven languages. check:packet-a1-structure and check:packet-variants assert the new contract two-sided (a poster page 1 has one header, a map-only page 1 still has the packet's; every ring is named, a populated one with its count and an empty one with a dash). |
||
|
|
30db4bb763 |
The people-group list is documented in the repo, library/lists/README.md, not in a file outside it
Four places in the repo (the registry, the resolver, the packet renderer and a gate's allowance) cited a research spec that lives outside the repository, so a reader of the code could not follow the reference. library/lists/README.md now documents the list as built: how a list is chosen, the registry and its five lists with their measured counts, where membership is generated, the two tiers (narrowing DOXA's rows, failing closed; IMB rows per map), the data sources and their terms, and the questions still open. The four citations point at it. |
||
|
|
c8dcb02e97 |
Keep the map artwork left-to-right inside an Arabic packet
Page 1 of an Arabic packet lifts the English 3x4 poster, and the poster inherited the page's dir="rtl": English punctuation jumped to line starts, the header's key and value columns collided, names ran into their numbers, and four text blocks were pushed off the sheet's left edge (rendered and measured in print emulation). The Chinese packet, which is ltr, was intact. The lifted sheet's wrapper now pins dir="ltr". The packet's own title stays rtl; the relabelled Arabic country names are single runs, so they keep their order. check-packet-a1-structure asserts the pin on both kinds of sheet; it fails without it. |
||
|
|
87c59e3a1f |
Name the map's countries on page 1 in the packet's language
The map sheets are generated once, in English, so in an Arabic or Chinese packet page 1's map labelled every country in Latin capitals, and its privacy line was English. The labels are now replaced as the sheet is lifted into the packet. They are recognised by the generator's own rule rather than a list: generate.mjs draws each label as esc(ISO3_NAME[iso3].toUpperCase()) from the zone data's ISO3_NAME table, so inverting that table under that transform names the country behind every label it can draw. The inverse is unambiguous (162 labels, no two countries sharing one), so a label is known exactly or left as drawn. The map's privacy line is word for word the packet's privacy_note, so it takes this language's. A width guard: the generator placed every label and then dropped any that would collide, measuring the English name, so a translation is fitted into that same box - estimated wider, it shrinks in proportion and cannot reach a neighbour the English name did not. On the Sudan poster, six Arabic names shrink (the longest, Saudi Arabia, from 15px to 10px); none of the narrower Chinese ones do. Capitals' tracking is dropped for scripts without capitals, where it breaks Arabic joining. English is untouched: its relabeller is null, and the lifted sheet is byte-identical with and without it. No sheet or poster is regenerated - the change is in the lift, so the tracked 3x4 posters page 1 now uses are relabelled as they are. Both print paths supply the relabeller: the UPG 100 profile from its own ISO3_NAME and the library's country-name resolver, the offline generator from the same table and the injected ISO-3 to ISO-2 table. check-map-labels (gates:pre), on the tracked Sudan poster: English byte-identical; in ar and zh no known English name left, halo matching fill, privacy localized; the guard fires for Arabic, not Chinese, and grows nothing; Arabic tracking off, Spanish kept; an unknown label left alone. Mutation-tested five ways, each caught. |
||
|
|
bc422bb678 |
Each packet's % evangelical page states its own list's share
The page says each box is a share of the people in this list. It printed one DOXA-wide figure - 0.6133% of 209,079,159 people - in every packet, true of no single list. That figure had no builder in the repo. build-evangelical-share.mjs makes its method reproducible, per group: the IMB CSV joined to the DOXA snapshot by ROP3 + country, each group's EvangelicalLevel band at its midpoint (0 for No Known Evangelicals, the midpoint read from the band's own wording), weighted by IMB population. It writes evangelical-by-slug.json and refuses to write unless the table, summed over every DOXA group, reproduces christian-share.json exactly: 2,091 matched, 209,079,159 people, 1,282,178 evangelical, 0.6133%. Change a single band's midpoint rule and it refuses. Not the API's imb_gsec: GSEC puts 225 groups at "no evangelicals" where EvangelicalLevel puts 1,444, because its "less than 2%" band also carries church-planting status. A share from it sits on a different scale from the method the page's own footnote states. evangelicalShare.js sums a packet's own groups - pure, zero imports, so the browser print and the offline generator run the same sum. Across the 28 zones the page now carries 28 different figures, from 0.182% (Deaf) to 0.922%. A host that supplies its own figure still wins. A list with no published levels prints a dash and no number. The old guard, Number.isFinite(Number(x)), passes null as 0 and would have printed "0% evangelical" for no data. Where some of a list's groups lack a level, the footnote says what the figure is computed over; it says nothing when coverage is whole. That clause is in all eleven languages. check-evangelical-share (gates:pre): the table reproduces the committed figure; different lists differ; no data is a dash; coverage appears only when partial. Mutation-tested four ways, each caught - the fourth only once it corrupted an existing group rather than adding one no sum reads. |
||
|
|
2367c2ef64 |
Build the prayer packet page by page to its specified structure
FULL DETAIL is four front pages, the profiles, four back pages:
map, triangle, life calendar, % evangelical, profiles,
bounded set, centered set, prayer wheel, 12 prayer points.
LIGHTWEIGHT is two front, the profiles, two back:
map, triangle, profiles, prayer wheel, 12 prayer points.
Both are generated from data, in every packet language, from the
browser and from the offline generator.
PAGE 1 - the map, on a vertical page. One lift for every caller
(packet-render.js liftMap): the browser composable, the UPG 100
profile and the offline generator each carried an identical copy of
the old wrapper (250px down, scale 0.62), and three copies of a layout
decision is how page 1 drifts between print paths. The lift prefers
the map-only vertical sheet (mapSheetFiles: <zone>.packet.html, then
the poster) and places it so its names land on the same 60px margin
every other page keeps; a poster falls back fitted under the title at
0.81 rather than 0.62. The title block is drawn after the map, since a
map-only sheet begins with a full-page background. Anything that is not
a map sheet - an icon-sized <svg> from a host that answers a missing
file with a 200 page - is refused, and page 1 says the map is missing.
Page 1 now states which set it belongs to ("One of 28 UPG 100 prayer
lists"), only when the caller supplies the count.
PAGE 2 - one inverted triangle in the Vision Card's look. The card's
steps are the top (every people group, the unreached, the frontier,
the network's list), narrowing to the UPG 100 packets and, at the tip,
this packet. The card's greens, darkening as it narrows, walked
through its own colour stops; every numeral picks white or ink by the
WCAG ratio (all at least 3.4:1, large bold type). Tier widths are
geometric, not proportional - 2,107, 28 and ~100 against 12,379 would
be hairlines - so where two sets truly nest the caption states the
step removed, and the network's list, which does not sit inside
frontier, gets the card's "N of them are frontier" note instead of a
false subtraction. The packets tier and its count appear only when the
caller supplies one.
BACK PAGES - the bounded and centered sets get a page each at twice the
width, under the one page key, so lightweight drops the pair together.
THE WHEEL - the Metacamp Module 1 prayer wheel: its twelve colours,
sampled from the fill operators in the source PDF's content stream; its
orientation, the opening PRAISE at nine o'clock and READ THE WORD at
twelve; white labels with a shadow and a faint outline, so a print
path that drops SVG filters still leaves them legible on the palest
segments; "5 minutes" badges; the attribution with the website. The
labels stay the packet's own rhythm names, translated in every
language and matching the 12 points page.
The offline generator makes both packets (PACKET_VARIANT=lightweight;
the full packet keeps its names, lightweight is <zone>.lightweight and
stamped with its variant).
STRINGS: four new keys, in all eleven blocks - each block's own words
for "packet" and "prayer list", the frontier note aligned to the word
the frontier tier uses on the same page.
GATES: check-packet-a1-structure (registered in gates:pre) reads every
sheet's CONTENT for the order of both variants, page 1's lift in all
four cases, the triangle's tiers and steps, the wheel's colours and
orientation, and all 28 zones. Mutation-tested eleven ways, each caught
by its own message. check-packet-variants now asserts both sets pages
and a four-sheet difference. The browser gate check-packet-render
counted funnel rows and bars by inline-style substrings, which an SVG
triangle has none of - both would have read zero and passed - so it
now counts .funnel-n against .funnel-tier, its sheet arithmetic counts
the sets pages, and its Latin allowlist carries the renamed product
name. check-packet-browser used a copy of the old wrapper; it now uses
the product's lift.
|
||
|
|
646c23cc0e |
Put the missing-map sentence in every packet language
The page-1 notice for a missing map existed only in English, which turns check:packet-strings-parity red: the packet catalogue must be whole in every language, and English-by-choice is not one of its declared exceptions (only the licensed Mark 5 passage is). All eleven blocks now carry the sentence, reusing each block's own page-1 wording for "prayer list" so the notice reads like the line above it. The English says "prayer list" rather than "zone": zone is an internal term, and the page itself calls it a prayer list. The Chinese block is Traditional script, so the sentence is too, with the Taiwan term for printing. The loud gate's note described the old English-only state; it now reports which locales fall back, or that none do. |
||
|
|
ef6000c5d7 |
Say so on page 1 when a prayer packet's map is missing
Page 1 of every packet draws the zone map from mapSvg. When the map fetch fails, the page interpolated an empty string: a title, a subtitle, a privacy footnote, and nothing where the map belongs. A failed fetch and a successful render were byte-indistinguishable in the printed PDF, and the packet read as undeveloped. It is not a rare edge. The served asset pool ships boundaries, fonts, images, people-groups, pplr, previews, tables and vendor, and no posters/ directory at all, because generate-tree gates it behind posterSources and bundlePosters, both default false. Over live HTTP against the served tree: previews/sudan.webp 200, boundaries/world-adm0-web.geojson 200, posters/sudan.html 404. So on a default build every packet's page 1 has been blank. The browser path's catch around the poster fetch is right to keep printing -- a missing map must not cost the reader the people pages -- but degrading and degrading silently are different things, and only the renderer knows the map is absent when it draws the page. It now prints one quiet sentence in the map's place. The sentence is English-only on purpose. t() falls back to English per key, so the other ten locales print it in English until a translator supplies theirs, rather than ten invented translations nobody can check. The real fix for the blank map is shipping the poster sources in the asset pool; that is a tree-config change and goes separately. check-packet-map-missing-loud is two-sided: loud when the map is missing AND quiet when it is present with the SVG intact, in en/es/ar, both variants. Shown capable of failing four ways: the silent cover, a notice that always prints, a notice that displaces the map, and the raw key printed instead of a sentence. |
||
|
|
3408088647 |
The packet's pages take the spec's order: triangle second, wheel before points
Diffing the full packet against its card found every PAGE present after tonight's supplier work -- and two of them in the wrong ORDER. The card puts the inverted triangle at page 2, in front of the life calendar, so the packet opens with the argument (twelve thousand narrowing to THIS list) before the meditations; and it closes wheel-then-points, so the last thing in a reader's hands is what to pray rather than how long. The plan card flagged this reorder and would not apply it silently, because the file says the order is the product spec: both changes are the spec's own, confirmed through the shepherd, and the two stale order comments update in this same commit as that card required. The file header now also says which copy of the comment is authoritative, since this is the second time it has gone stale. The variant filter is keyed, never indexed -- the reorder moved lines and could not move meanings, which is exactly the case that comment was written for. Verified in-process on both variants (full: map, funnel, life, christian, profiles, sets, wheel, points; lightweight: map, funnel, profiles, wheel, points) and on the regenerated 28-zone output; the headless packets-all reports 28 of 28 with maps, groups and sets, and every packet gate stays green. Sheet 0 is still the map page, which is the one positional assertion any gate makes. |
||
|
|
c54a11d5aa |
The legend's Export path gets the busy state the HUD already had
Found by the bug-CLASS sweep, which is the point of doing one: the busy indicator was added to the selection HUD after a report that pressing its buttons showed no sign of work, and the legend's Export menu -- Poster and Packet -- had none at all. Same defect, different surface, and I fixed the reported instance without looking for its siblings. Both do the same slow work: two dynamic imports, a map-SVG fetch, a funnel fetch, then a multi-sheet render. onLegendExport now guards re-entry (a second click would run two renders over one print frame), sets busyAction so the control says what it is doing, and clears in finally on every exit path including the early return inside the try. The gate's busy assertion had two holes that let this through, and both are fixed. It counted setters by matching the literal strings 'packet' and 'poster', so a ternary assignment was invisible -- a regex that enumerates the shapes it knows about will always miss the next one, so it now matches the assignment and excludes the clear. And it only asserted a whole-file count, which "some handler somewhere sets it" satisfies; it now asserts PER FUNCTION for all three print paths, so a surface cannot be forgotten while the file as a whole looks fine. Mutation-tested: removing the busy handling from onLegendExport now fails naming that function, and dropping a finally fails naming its own. Also drops a stale "page 3" from a shipped CSS comment -- page order is variant-dependent since lightweight started omitting two sheets, so a fixed page number in the output is wrong even where no reader sees it. |
||
|
|
9609cd84ba |
Say "provisional, pending a product decision" instead of naming a row
Follow-up to 50ebdf89, which kept an internal tracking id on the grounds that a reader needs to know where the answer comes from. The ruling is that in shipped code the id should go too, and it is right: a row number means nothing to someone reading packet-render.js, and it points at a place they cannot open. What the comment has to carry is that the value MAY CHANGE, so a later reader does not treat it as settled and hardcode it somewhere. "Provisional, pending a product decision" says that without pointing anywhere. Three sites, all mine. CenteredSetTarget.vue carries the same reference and is not mine; it belongs to the carded fleet sweep. |
||
|
|
35dfd82b83 |
Drop quotes and transcript references from the packet comments and gates
Per the 17:25Z rule: no quotes, no citations and no transcript references
in strings, commits or code. Studio had to strip these from two of my
commit messages during L64, which is a sign I should be doing it at the
source rather than leaving it to whoever lands the work.
Nine sites across five files, all of them lines I added. The engineering
content is unchanged -- what goes is the attribution, not the reasoning.
"A disabled button is visually indistinguishable from an unresponsive one"
is the durable fact; who observed it is provenance a later reader cannot
verify and that ties shipped code to a private transcript.
Where a decision is genuinely still open I keep the tracking reference
("still open, tracked as row 89") but not the speaker, because a reader
hitting that comment needs to know the value may change and where the
answer will come from.
Scope is deliberately my own unlanded additions. packet-render.js,
PosterWall.vue and upg-100-list.vue carry many more such comments from
before the rule; those are not mine and a fleet-wide sweep is a separate
decision. Flagged rather than silently rewritten.
All seven gates green after the edit, including the two whose own header
text changed.
|
||
|
|
fccf6ee2fe |
Name the rings on the packet's centered set
The figure printed rings without saying what they mean -- honest but thin.
It now names each one with the vocabulary gospel and exp settled, the same
words the screen legend renders, so a printed sheet cannot disagree with
the screen.
COMPOSED FROM band x culture, NOT A FLAT 1..10 TABLE, and that is the
whole design decision. "Same people" in column 1 is provisional on the
Driver's row 89 -- and it appears in rings 1, 4 AND 7. A ten-string table
would need three edits kept in step, which is exactly how two of them end
up saying different things. Composed, it is one string
(ring_culture_same), and if he picks "culture" the ring numbering, the
columns and the colours are all untouched.
Ring 10 stands alone ("Nearby only") because it is not a band-and-culture
reading at all -- it is what is left when no linguistic relation was
found. The gate asserts that separately, and a mutant that composes it
like the other nine fails.
ONLY THE RINGS THAT HOLD SOMEONE. Ten rows of vocabulary beside a figure
is a glossary, not a legend, and a named ring with no dot on it invites
the reader to hunt for something that is not there. ringTally's per-ring
counts drive the list, so it states what this zone actually contains.
Mutation-tested: a wrong band order, listing all ten rings regardless of
count, and composing ring 10 like the others each fail with their own
message. Eight new strings x eleven locales; packet-strings-parity green.
|
||
|
|
3c1d513cc3 |
The packet gets its bounded and centered sets page
Brief A1 back matter: after the profiles, before the wheel, full detail only. The lightweight packet is map, triangle, profiles, wheel, points -- these two figures are the part a reader sits with rather than carries. The layouts are regions' library/components/charts modules, imported rather than injected: unlike describeGroup's country resolver they have zero imports and run under plain node, and packet-render already imports scatterCells from that directory. Verified by importing and CALLING them, not by reading them. They return data, so the SVG is ours. Gospel's non-negotiable is carried through and checked rather than trusted: the bounded set keeps a deliberate gap either side of the boundary because a dot ON the line is the one thing a bounded set cannot express, and print has no hover to disambiguate it. Measured on the real module with 40 groups -- inside max 85.3 of 92 allowed, outside min 119.8 of 112 required. The figure scales through the viewBox, so the gap lives in coordinates the scaling cannot touch; shrink the figure, never the gap. UNKNOWN POINTS ARE STATED, NOT PLOTTED. layoutBoundedSet gives them no x/y, which is the module being right: `reached` undefined means we do not know which side they belong on, so there is no honest place to draw them. My first version mapped them through the same dot() helper and crashed on p.x.toFixed -- a better outcome than silently drawing them at (0,0), which would have put "we don't know" inside the circle. They go in the tally, because on a printed page a category the reader cannot see reads as zero. check-packet-variants' sheet delta goes 2 -> 3, updated in this commit rather than relaxed into a range. It also now SUPPLIES the sets fixture: setsPage returns '' without data, so a build() that omitted `sets` exercised the filter for every page except the newest one, and the gate sat green through the whole rewrite while asserting a delta that had become wrong. A fixture missing an input silently narrows what the whole file covers. Seven strings x eleven locales; packet-strings-parity green. Gospel's own ring names live in gospelInternet.json, a plugin catalogue, so there is no collision with these. |
||
|
|
aedc517cec |
The table row is a border-box, and the offline packet gets its description
Two fixes from measuring what I had only modelled. 1. THE ROW RENDERED 81px AGAINST A 73.25px MODEL. rega measured it: no box-sizing, so the declared height was the CONTENT box and the 6px padding plus 1px border were added outside it. Because `top` still advanced by the modelled height, every row overlapped its successor by the difference. The stack still fitted the sheet -- it ended 8px inside the 60px bottom margin instead of on it -- so nothing clipped, nothing ran off the page, and no headless check could see it. box-sizing:border-box makes the declared height the rendered height. The ladder still fits: the content box is 66.25px and a name plus two lines at the default 17px needs 61.8px. 2. THE OFFLINE GENERATOR WOULD HAVE PRINTED A ONE-CLAUSE DESCRIPTION. Its fetch never asked for primary_language or imb_gsec -- measured on the cached records, 0 of 2107 carried either, while country_code_label was present on all 2107. So the browser packet printed two full sentences and the offline packet printed "The Amahuaca live in Peru." from the same data. Same defect class as the comment already above that field list: it reads as missing DATA and is missing FIELDS. The generator now injects the describer too, deliberately WITHOUT a country resolver: library/utils/countryName.js cannot be imported under node at all (it imports JSON without an import attribute -- measured), which is the constraint that made this seam an injection. It is not needed, because describeGroup falls back to country_code_label and every cached record has one. Verified end to end: 28 packets, 63 sentences on the first page, zero fallbacks, country localized correctly. The gate now budgets against the CONTENT box rather than the declared height, asserts the DEFAULT 17px step fits and not merely the smallest, and pins box-sizing in the source -- mutation-tested: removing border-box fails with its own message. |
||
|
|
4a99453cda |
The packet's list becomes the profile TABLE: picture, description, three boxes
Brief A1 item 1. The 2x10 card grid becomes 20 full-width rows, each one picture | generated localized description | three prayer checkboxes. PER_PAGE stays 20, so this changes the layout SHAPE and not the pagination -- which is what lets check-packet-render's dropped-groups assertion keep passing unrelaxed. That assertion refuses a +/-1 tolerance because slack is exactly where a lost list page hides, so a rewrite needing it loosened would be the wrong rewrite. The description column is ~1036px. An entire card in the old grid was 683px, so the sentence alone is 50% wider than a whole card was; that is the point, since a generated sentence needs width a name and two metadata lines did not. The geometry closes with ZERO SLACK -- 20 rows plus 19 gutters fill the sheet to exactly the bottom margin. That is the finding, not the reassurance: a description wrapping to a third line overflows by ~16px. Hence FIT BEFORE CLAMP: step 17 -> 15 -> 14px, and truncate at a WHOLE WORD only as a last resort, because a mid-word cut reads as a rendering fault rather than an editorial one -- especially inside a people-group's name, where a reader cannot tell it from a misspelling. Deterministic by character count, never a DOM measure, because this module runs under Node for the offline generator as well as in the browser. The 0.52em advance is deliberately wider than Georgia's true ~0.50 so a locale wider than English is absorbed by the estimate rather than by an overflow. Measured against the real catalogue, every shipped locale fits at full size -- German is longest at 178 chars against a 234 budget -- so the ladder is insurance, not routine. describeGroup is INJECTED, not imported: its country resolver reaches a Vite-only alias and imports JSON without an attribute, which would make every `node internal/check-*.mjs` unrunnable. usePosterPrints composes it on the browser path and tolerates its absence, so a packet that cannot resolve a describer still prints, degrading to the metadata line. check-profile-table.mjs was mutation-tested, and two of my own assertions were dead. "The rows close on the bottom margin" is a TAUTOLOGY -- the row height is derived by dividing the page by PER_PAGE, so it holds for any row count; a 21-row mutant sailed through it. It is replaced by the real constraint, that a row stays tall enough for its content (fires at 24 rows). And the word-aware check fed 'word word word', where a mid-word cut is indistinguishable from a clean one; with distinct words it now names the broken token. |
||
|
|
642fc15310 |
Prayer List gets the cards' caret, and starts printing the packet
The Driver asked for the SAME caret the cards already have on the Prayer
List, with two entries -- Full detail and Lightweight -- and said plainly
not to destroy the print options that exist. So nothing is merged, nothing
is removed, and the Poster is untouched.
There was no dropdown COMPONENT to reuse: the cards' caret is an inline
disclosure, one ref plus a sibling tape of plain buttons, deliberately not
a <select> because a native dropdown collides with other dropdowns on the
host page. So the PATTERN is generalized rather than copied. deckOpenFor
now holds a compound `${id}:${which}` key, which keeps "opening one closes
the others" free -- copy/paste would have given two open-states, and two
tapes could then sit open at once and overlap in the grid gap.
While wiring it, a real defect surfaced: THE WALL'S PRAYER LIST BUTTON HAS
NEVER PRINTED A PACKET. downloadPacket calls printList, a columnar table of
name/country/population/religion. The packet -- map, life calendar,
%-evangelical grid, triangle, profiles, wheel, points -- is printPacket,
which every other surface calls and this wall never did. That is a direct
cause of the packet "not following the design that was asked for" here: no
change to the renderer could reach a button that was not calling it.
printList stays exported for any surface that still wants the list.
The two packets are a FILTER over the existing page array, not a second
code path, so they cannot drift apart in layout, i18n, RTL or print scale.
The filter is keyed and never indexed -- an index filter reads identically
today and drops the wrong page the first time the order changes, and that
order is the Driver's. listPages is deliberately outside the filter: a
variant may drop a FIXED page, never one whose count depends on the data,
because that drops people silently. An unknown variant falls back to full.
internal/check-packet-variants.mjs pins all of it, and was mutation-tested
rather than trusted: dropping list pages, reverting to printList, adding a
second open-state ref, and typoing a variant id each fail with their own
message.
|
||
|
|
4f73d83438 |
One scatter implementation, and the gate that could have caught the bar
packet-render's lifeGrid already held a carefully reasoned shading: deterministic so a packet reprints identically, and scattered because a contiguous run of 32 fills most of row 1 of 52 and prints as a solid bar across the header, which is the ordinary chart the life grid exists to avoid. The message is rarity, and the eye has to hunt for the 32 among 5,200. That logic is now in packetCharts.js and packet-render imports it, so the printed packet and the reusable chart cannot drift into two answers. The move is behaviour-preserving: I ran the extracted function against the pre-refactor code over 49 seed and count combinations with zero differences. That probe was one-off, so the durable half is a golden fingerprint here — the exact cells for one seed. If the algorithm is ever tidied, those change and every packet already printed stops being reproducible. The assertion that matters most is the one that did not exist when the bar shipped. The existing check asserts the shaded COUNT is between 0 and 5,200, which is equally true of a bar and of a scatter: a count cannot distinguish a distribution, so no amount of that gate passing was ever evidence. rega's eyes caught it, and nothing else could have. Now the spread is asserted — 32 cells landing on 27 different rows — with the contiguous block as the negative control, so the arm can fail in exactly the shape it exists to reject. The rounding flag upg100 asked to keep reachable stays on the return value, so the packet can print the honest caption when a share is too small to fill one cell rather than inking one and asserting "some". |
||
|
|
35f682f7cb |
The 32 are scattered through the life now, not stacked into a bar
rega's contact sheet showed what no gate could: page 3 read as ONE SOLID GREEN BAR. 32 contiguous cells fill most of row 1 of 52, so the page said "a big green block" where the Driver's intent is "32 boxes lost in 5,200". Rarity is the whole message and a bar cannot carry it. ⚠ AND THE COMMENT I DELETED SAID THE OPPOSITE, WHICH IS THE PART WORTH KEEPING. It read: "Row-major, so the shaded run reads as a contiguous block from the top — a scattered shading of the same COUNT would be honest arithmetic and an unreadable picture." I wrote that reasoning about a picture I had never looked at, and it was exactly backwards. Scattering is what makes the eye hunt for them, which IS the point. DETERMINISTIC, NEVER Math.random(): a 32-bit FNV-1a hash of the ZONE ID feeds a mulberry32 PRNG, so the same packet prints identically every time — two copies of one zone must not disagree — while different zones get different constellations, so a print run is not 28 sheets wearing the same pattern. Verified: two consecutive renders produce byte-identical files (md5 febafc1813c6 both times). Partial Fisher-Yates over the index space rather than shuffling all 5,200, so a large share degrades into an even spread rather than clumping. ★ AND THE NEW ASSERTION IS THE ONE THAT WOULD HAVE CAUGHT THIS. check-packet-render now measures the shaded cells' VERTICAL SPREAD as a fraction of the grid height, at the RENDERED layer via getBoundingClientRect — because a COUNT cannot tell a scatter from a bar. 32 shaded is 32 shaded either way, which is precisely why every existing assertion stayed green on a broken page. POISON-TESTED rather than trusted: rebuilding the same 32 cells as a contiguous run makes it FAIL with "span only 0% of the grid height". The real packet measures 95% (32 cells across 26 distinct rows). The 50% floor is deliberately loose — a genuine scatter spans nearly the whole page, a bar spans ~1%, and anything between deserves a human look. ⚠ The poison test first ran on a copy in /tmp and the gate REFUSED it — "outside any checkout, so which build produced it cannot be checked". A sensible guard, and it meant my first poison run proved nothing; re-ran inside the tree. en and ar both PASS at 9 sheets. Page 2 (the empty life grid reading as blank graph paper) is UNTOUCHED and held for the Driver with the page-order question — shading a reader's life we do not know would be a guess presented as a fact, which is a product decision, not mine. |
||
|
|
f7a7619695 |
Stop shipping an internal role noun in public bundles
check:public-hygiene flags "internal role noun (Driver) — process vocabulary" inside shipped bundles. Three of those reach regions-dashboard.js, and they are not from the regions components: they come from the shared poster and packet code, which every bundle using PosterWall carries. The worst is not a comment at all. packet-strings.json's note_on_scripture is a shipped STRING, so it is data a reader can read. The other two are a JS comment and a CSS comment that both live INSIDE template literals, which makes them string content rather than comments -- the minifier cannot strip what is not a comment, and the shadow-CSS stripper does not reach a print stylesheet. Rewritten to say what the decision was rather than who made it. Three lines, no behaviour, no reformatting. regions-dashboard.js goes from 3 occurrences to 0 after a rebuild. This is the white-label requirement, and it matters most for the artifact we intend to hand over: a recipient reading our bundles should not meet our internal process vocabulary. |
||
|
|
de243597db |
The prayer packet the Driver asked for: life calendar, the same grid as a percentage, a triangle
Driver, 2026-09-27, in his own page order. Nine pages now, was seven.
1 map the zone's vertical one-pager (unchanged)
2 LIFE CALENDAR 100 years x 52 weeks = 5,200 boxes — a whole human life on one page
3 THE SAME GRID shaded by the evangelical share of these groups' POPULATION
4 funnel now an UPSIDE-DOWN TRIANGLE with an equation sidebar and a "1 of 28" tip
5..n groups 20 per page with photos AND three prayer tick columns
then the 12 prayer points and the prayer wheel (both unchanged)
★ THE 12 PRAYER POINTS WERE NOT LOST. The Driver believed they had gone; all 12 are intact in
packet-strings.json with his rhythms and sub-titles — praise/Two-Cross Gospel, wait/Holy Spirit &
deliverance, confess, read/the Father's love, ask/baptisms, intercede/laborers & shepherds,
pray_word/Persons & Houses of Peace, thank/rest, sing, meditate/communion, listen/accountability,
praise_end/great-greater-greatest — plus the Mark 5 passage and the Metacamp attribution.
★ PAGES 2 AND 3 ARE ONE GRID READ TWICE, which is the whole idea. 5,200 is the Driver's own arithmetic
(100 x 52), not a layout choice, so both pages derive from it and cannot drift into two grids that merely
look alike. A percentage drawn as a bar is an abstraction; drawn on the grid the reader has just
understood as their own life, 32 shaded boxes of 5,200 is the same fact made unavoidable.
★ AND PAGE 3's NUMBER IS POPULATION-WEIGHTED, because he asked for "% of Christian population vs lost".
That matters: by GROUP COUNT 69.1% of the 2,091 matched DOXA groups have no known evangelicals, but by
POPULATION the evangelical share is 0.61% of 209,079,159 people. The group-count version is a materially
milder picture of the same data. Computed in data/people-groups/christian-share.json, joined IMB
CSV ROP3Code+ISOAlpha3 to DOXA's snapshot — the same join the Vision Card uses — and READ here, never
re-derived, so one fact has one source.
⚠ THE BAND MIDPOINT IS AN ASSUMPTION AND THE PAGE SAYS SO. peoplegroups.org publishes EvangelicalLevel
as a BAND ("Less than 2%", "No Known Evangelicals"), never a number, so any percentage requires choosing
a value inside each band. The file records the method, the page prints it, and 0.61 + 99.39 = 100 exactly
— my first formatter rounded the large side to "99%", which loses the asymmetry that IS the point and
leaves a reader checking the arithmetic to find the page wrong.
⚠ GEOMETRY MEASURED, NOT EYEBALLED: square cells would need 26.5px x 100 rows = 2,654px of grid in
1,521px of printable height, overflowing the sheet 1.7x. 13px rows put 100 rows in 1,498px with 23px
spare. Caught by computing the height before rendering.
⚠ AND THE TIP SAID "1 of 27". listCount spread the three cluster keys and omitted `deaf`, whose two
deaf_lists entries buildLegendNodes collapses into one leaf — the same omission allZones() had before
|
||
|
|
6d6b656721 |
A packet says which list it holds, in the reader's language
TWO FINDINGS FROM rega'S ?list= RENDER, and the first one is a hazard I created in the same commit where I argued against it. I MADE PACKET_LIST FAIL CLOSED because "a packet that showed the wrong list is printed and handed to someone" — and then wrote the frontier packet to the SAME FILENAME as the default-list packet of that zone and language. rega measured the result: out/packets/ held frontier packets under the DEFAULT names, beside an `ar` file that was still the default list, and nothing in the name could tell them apart. Guarding the input while leaving the output ambiguous protects against asking for the wrong list, not against being unable to tell what you got. Fixed both ways. The list is in the FILENAME (asia-china-tibet-autonomous-region.frontier.hi.html) and STAMPED INTO THE DOCUMENT as data-packet-list, because a filename can be renamed, copied or emailed while the packet itself should be able to say what it is. Same reasoning as data-packet-build: out/ is gitignored, so a sweep grades whatever files happen to be there, and a packet that cannot describe its own provenance makes an accidental set indistinguishable from a current one. The gate prints it too. THE LIST'S LABEL WAS ENGLISH IN EVERY NON-ENGLISH PACKET. "Project 4700: frontier people groups" sat among Devanagari labels in a Latin serif face, because it came verbatim from library/lists/lists.json — which carries ONE label per list and has no locale dimension at all. The gate passed because it counts rows, not the row's language. It is translated in packet-strings.json rather than by restructuring the shared lists.json: that file is the packet's OWN catalogue, where a new language is a new block and never a code change, and it degrades per key like every other string. An unknown list still falls back to the data's English label. THE NUMERIC DESIGNATION STAYS — "Project 4700" / "مشروع 4700" / "4700 计划" — because it names the campaign, while "frontier people groups" is prose. That is the same line check-i18n-never-translated draws around "UUPGs": translate what a bilingual reader would not dispute, keep what is an identity. rega'S MEASUREMENTS, all confirming predictions I could only compute: 6 pages, 37 of 43 records, rows 12,379 -> 7,341 -> 4,754 -> 2,107 -> 1,745 -> 37, bars 1080/640/415/184/152/26 identical in en and hi, share "2.1% of the 1,745" with no "1 of 27 lists", provenance-only footnote, fail-closed rc=1 on an unknown list. And the number I most wanted: share-to-footnote clearance 314px en, 222px hi — so hi+frontier, the tightest combination that exists (128px rows, gap down to 147px), still clears by 222px and no label wraps. Verified: all 18 packets in out/ pass sheets, rows==bars and zero-population. The 11 laos packets FAILED the zero-population assertion until regenerated — they predated the fix, which is the gate distinguishing stale artefacts from current ones exactly as data-packet-build was added to do. Default path unchanged (43 groups, 7 sheets, 5 rows). frontier renders 6/6 in en, ar and hi with the label in each script. check:packet-strings-parity PASS with the 5 new keys. |
||
|
|
75e31e0126 |
The six-row funnel can finally be rendered
ITS ONLY WITNESS WAS MY OWN HARNESS. The browser path honours the universal
`list` prop — upg-100-list.vue calls applyPeopleGroupList() at every fetch site —
but library/scripts/posters/packet.mjs had NO list support at all. So the
non-default-list branch of funnelPage, including the six-row shape where a list's
own total is inserted before the reader's row, could not be produced by the only
pipeline rega can drive. I had unit-tested it through a script I wrote about it,
which is exactly the state I have been objecting to in other people's gates.
rega offered to shoot a ?list= packet and there was no command to give them.
PACKET_LIST=<id> closes that. Rendered for the first time, and it is right:
rows 12,379 -> 7,341 -> 4,754 -> 2,107 -> 1,745 -> 37
bars 1080 / 640 / 415 / 184 / 152 / 26 (six rows, six bars)
label "Project 4700: frontier people groups" between the network's list and
the reader's 37
share "This packet is 2.1% of the 1,745" (no false "1 of 27 lists")
foot provenance only, no coverage claim
row gap auto-adjusted 218 -> 147px for the extra row; last row stays 14px so
the share still reads as its conclusion.
IT READS THE MEMBERSHIP FILE FROM DISK, not through loadListMembership(), which
fetches from the asset pool over HTTP — there is no server in a Node build. Same
file, same slugs, same counts.doxa.
AND IT FAILS CLOSED: an unknown or empty list exits non-zero rather than quietly
producing a packet of every group. peopleGroupList.js makes the same choice for
the same reason ("a map that silently showed all 2,107 while an embed asked for
Project 700 would be showing the wrong list with no sign of it"), and a packet is
worse than a map — it is printed and handed to someone.
⚠ buildFunnel() IS NOW A FUNCTION, NOT A CONSTANT, because it depends on a list
resolved after the records load. It mirrors upg-100-list.vue's packetFunnel()
exactly: the default list is the only one the zone catalogue describes, so it is
the only one that may claim "1 of K lists" and an unassigned count.
── AND A CORRECTION TO MY OWN VERIFICATION ─────────────────────────────────
"Numerals untouched" is about DIGITS, not GROUPING, and I verified the digits and
assumed the rest. rega measured the rendered hi sheet: Indian grouping,
"8,28,85,63,969 लोग" (lakh/crore 2-2-3) rather than Western 3-3-3. That is
CORRECT hi-IN formatting, so changing it would be worse — the real risk was never
the grouping but INCONSISTENCY, the same number printed two ways in one document.
Measured and it is consistent: num() is the only formatter in the renderer, so the
funnel's populations and the card pages' populations group identically — confirmed
against the rendered hi packet (card populations 1,70,000 / 3,37,000 / 8,10,000,
Indian-grouped like the funnel's). Only values at or above 100,000 can distinguish
the systems at all; my first check classified "1,400" and read a mismatch that did
not exist.
Verified: default path byte-identical (43 groups, 7 pages, 5 rows, 5 bars); all
four fixtures PASS with rows==bars; PACKET_LIST=frontier gives 37 of 43 records
and 6 pages; check:packet-strings-parity PASS; vite build clean, bundle unchanged
(the change is Node-side).
|
||
|
|
127a2e0930 |
The funnel states the list it was actually drawn from
A LIVE WRONG NUMBER, not a future risk. The universal `list` prop is already on
main (
|
||
|
|
c33610938a |
The funnel narrows to the reader, and a missing page 2 can now go red
TWO FIXES FROM A BROWSER RUN I COULD NOT DO MYSELF. rega measured page 2 in print media and found the opposite of the defect I feared: no overflow, but the four global steps ended at y~330 of 1056 while "43 · are in THIS packet" sat alone at y~965, leaving the middle ~60% of the sheet empty so the reader's own step read as a detached footer. And it had NO BAR, so the funnel never visibly narrowed to "yours" — a funnel whose final term is not drawn as part of the funnel fails at the one thing it is for. So the five steps are now ONE list with the reader's included, and it has a bar like the others: 1080 -> 640 -> 415 -> 184 -> 26px, the last two tinted so the network's list and the reader's list read as one thought. The 26px floor is what keeps the smallest list a visible sliver rather than a hairline. THE ROW RHYTHM IS DERIVED FROM MEASURED ROW HEIGHTS, not guessed. A first attempt subtracted a magic 118px as "about one row" and left 418px of dead space at the foot — rega's emptiness, just redistributed. Summing the real heights closes it to ~298px. AND IT KEEPS DELIBERATE SLACK, because two runtime facts are unknowable at layout time: whether a step carries a population line, and how many LINES its translated label wraps to. Arabic and Hindi labels are far longer than the English, so LINE_SLACK reserves one extra wrapped line per row. Computed: 298px headroom normally, 196px even if EVERY label wraps an extra line. Tight-packing this page would trade "60% empty" for an overflow in the locales I cannot eyeball. ── AND A GATE HOLE A NEAR-MISS EXPOSED ────────────────────────────────────── regions-plugin reported, unprompted, that a merge resolved with `git checkout --ours` silently deleted the printPacket call site supplying the funnel data — 60 lines — AND EVERY GATE STAYED GREEN. They filed it as merge discipline. It is also gate design, and that half is mine: the funnel page is OPTIONAL by design (buildPacket omits it when the arg is null), so a packet without page 2 is not malformed, just smaller. My own choice made the deletion invisible. --expect-funnel closes it. The wrong fix is to always require the page: the 11 pre-funnel laos packets and any host with no funnel data would go red, and a false red costs as much as a false green. Instead the CALLER, which knows whether this packet was built with funnel data, declares it. An absence is then either DECLARED (no flag, fine) or a FAILURE (flag passed, page missing) — never a silent nothing. Verified: all 16 generated packets satisfy the sheet arithmetic, 0 failures, including the 11 laos packets that have no funnel page. fixture-tibet renders 5 rows with 5 bars ending in 43. Arabic regenerated: 7 sheets, dir="rtl", 5 rows, 5 bars, ZERO Eastern-Arabic digits on the sheet. Mutation-tested --expect-funnel three ways: baseline+flag PASS, funnel-sheet-deleted+flag FAILS, deleted without the flag PASSES. Flag filtered from the positional read, so a bare --expect-funnel is not eaten as the packet path (that trap has bitten this repo three times). BUNDLE=my-upg-100-list vite build clean, +141 bytes vs main. NOT re-verified in a browser: check:packet-render needs Chromium, reserved to rega. The geometry above is computed, not rendered — the print-media layout is exactly what rega's eyes caught that my arithmetic did not, so this wants one more look. |
||
|
|
65017fefa4 |
The prayer packet shows how its list was narrowed out of all the earth
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.
|
||
|
|
e8ad1d56e6 |
packet: a partial translation renders 12 prayer points, not 3
Some checks are pending
build-gate / build (push) Waiting to run
packet-render.js resolved the prayer points with `L.points || EN.points`. That is a whole-block fallback when the key is ABSENT, and TRUNCATING when a translator has done some of the 12 — a 3-element array is truthy. Measured at the rendered layer with a deliberately partial locale block: a page headed "12 Prayer Points" containing THREE, points 4..12 silently gone, including the Mark 5:1-20 passage point 4 prints in full. Nothing blank, no error raised. Invisible in all 11 shipping locales because every one of them is complete. It would have appeared on the next language added — and "a new language is a NEW BLOCK, never a code change" only holds if a PARTIAL block degrades per key, since half-finished is the normal state of a language in progress. Now merges per point and per field against English: English fixes the LENGTH and supplies every missing field, so a point translated for `text` but not `sub` keeps the English `sub` — and keeps its NUMBER, which the merge supplies. The sheet-sum invariant could not see this: a 3-point page is still exactly one sheet, so every number check-packet-render printed stayed correct while nine points were missing. It now measures and asserts the count. Mutation-tested — reverting the fix prints "prayer points : 2 / 12" and fails. All 11 locales regenerated with photos and swept: check:packets-all 11/11, each 12/12 points, including RTL ar and Devanagari hi. Renderer, generator and gate synced to doxa-maps-plugin-studio, which also lacked the .points-title and .wheel-title test hooks the new assertion needs. Also wires check:capability-exemplars into gates:post (33 -> 34 steps). It reads public/doxa-maps/manifest.json, a build artifact, so ordering mattered: `tree` writes the manifest and is &&-chained immediately before gates:post, and the gate SKIPs rather than fails when the manifest is absent. Full wave: 34/34. The quoted gates:post line in the README is re-synced in the same commit, because check:quoted-scripts goes red on the drift this commit creates. |
||
|
|
3519ca3934 |
check:packet-browser asserts its headings, and stops reading them in English
This gate printed nine metrics, folded six into `const bad = a || b || ...` — so a
failure said "FAIL" without naming which — and left three unchecked: page size and
the two headings, which are the very strings its own header claims it covers ("the
gate reports which headings came back translated"). Reports, not asserts.
★ IT LOOKED LIKE IT HAD FOUND A PRODUCT BUG, AND HAD NOT. Under ?lang=es it printed
an EMPTY wheel heading; under hi it blanked BOTH — while the catalog holds "Rueda de
Oración" and "प्रार्थना चक्र". That reads exactly like a broken i18n path, and it
PASSED anyway, because nothing asserted the values.
The cause was the harness. wheelTitle was innerText.match(/^.*Wheel.*$/m) and
pointsTitle /Prayer Points|Puntos/ — neither can match Hindi, and /Wheel/ cannot
match Spanish. The product was correct in every locale all along: verified after the
fix in en/es/hi/ar/zh, RTL and Devanagari included. Had I reported that as a defect
I would have been wrong.
mark5 carried the same fault: innerText.includes('Gadarenes'), an English proper
noun that goes RED the moment point4_passage is translated. Now asserted by
.p4-passage length — the same fix already applied in check-packet-render.
Headings are now compared to the CATALOG entry for the packet's language, with
per-key fallback to English exactly as the renderer resolves it. packet-render.js
gains class="points-title"/"wheel-title" as TEST HOOKS: both were inline-styled with
no selector, which is what forced the English greps in the first place.
MUTATION-TESTED: a renderer forced to ignore the language FAILS with both headings
named and the expected values shown; a renderer emitting an empty heading FAILS
("a blank heading is worse than an English one").
★ ONE MUTATION CORRECTLY PASSED, and it marks a real limit: reverting es
wheel_title to English IN THE CATALOG passes, because rendered and expected then
legitimately agree. This gate cannot tell a translated catalog from an untranslated
one — that is check:packet-strings-parity's job, and it does assert it. Two gates,
two questions.
Partial translation re-verified through the BROWSER path: a 1-of-21-key Swahili
block renders "Mambo 12 ya Maombi" with the wheel heading falling back to "Prayer
Wheel" — translated where present, English where not, neither blank.
That makes four gates with one root cause (tabs-localized, packet-render,
deck-click, and this one twice). Any new gate matching on user-visible text should
resolve it from the catalog.
|
||
|
|
e952e01e4c |
Assert the packet numbers that were only being printed, and run the gate on every locale
check-packet-render's own header states the law it exists for — "never print a number
without asserting on it" — and two of the nine numbers it printed were decoration.
PAGE SIZE. Printed "612 x 792 pt", never compared. The landscape check only catches
h <= w, so a wrong PORTRAIT size sailed through: A4 is 595x842, taller-than-wide like
Letter, and would have passed on a packet printing with wrong margins on every US
printer. Not hypothetical — @page{size:...} silently falls back in Chromium, which this
repo has already paid for once. Now asserts every page's MediaBox against Letter.
CARD COUNT. `if (!r.cards)` passed a packet carrying ONE card. Counts must stay relative
(a per-zone packet has its own group count), but relative does not mean unasserted:
sheets === 1 + ceil(cards/PER_PAGE) + 2 holds for any zone.
★ That was not enough, and the mutation test is what showed it. Deleting a whole list
sheet removed 15 cards AND one sheet, so the invariant still held — 80 cards / 7 sheets
is valid arithmetic — and the packet PASSED while missing 15 people groups. Internal
consistency is not completeness. So it now cross-checks an INDEPENDENT witness: the
{from}-{to} of {count} header the renderer prints on each list page, where count comes
from groups.length and does not move when a page is lost. It also requires the ranges to
TILE [1..count], which catches a missing MIDDLE page that the totals alone cannot see.
Those headers are TRANSLATED and the languages disagree on order — en/ar read "1-20 of
95" while hi reads "95 में से 1–20" and zh "95 之中的 1–20", total FIRST. A positional
regex would read those backwards. Parsed as digits with max() as the total instead, and
locale digit forms normalised to ASCII, so it is grammar-independent. Verified by
running the missing-middle-page mutation in Hindi as well as English.
MARK 5 WAS ASSERTED BY AN ENGLISH WORD. body.includes('Gadarenes') would go RED on a
correctly-translated packet, and passes today only because every locale falls back to
English for that key — the check and the bug were locked together. The renderer now
emits class="p4-passage"/"p4-credit" as test hooks (the block is styled inline and had
no selector at all), and the gate asserts length >= 1200, which holds in any language.
check:packets-all runs the gate over EVERY generated packet rather than one hand-named
file. The single-file gate SKIPs without an argument, so it was in package.json, passing
in all 11 locales, and in NEITHER gates:pre NOR gates:post — it ran only when someone
typed a filename. Now in gates:post (21/21, 11 packets through a real browser). SKIPs
when no packets are generated, so a clean checkout is unaffected.
Mutation-tested: A4 FAILs, a dropped sheet FAILs, a missing middle page FAILs in en and
hi, a truncated passage FAILs, a removed passage FAILs. i18n verified end to end by
adding a PARTIAL new language as a data block only — 3 of 23 keys — and confirming the
translated strings render, untranslated keys fall back to English, and nothing is blank.
|
||
|
|
467b7f01a2 |
feat(packet): all 11 locales — Hindi unblocked with a vendored Devanagari face
Some checks are pending
build-gate / build (push) Waiting to run
The packet now ships in every locale the app ships: en es fr pt de it ro ru ar hi zh. Each carries all 21 translatable keys, the twelve prayer points, the eleven rhythms, both attributions and the privacy note. point4_passage stays omitted everywhere and falls back to the licensed English WEB text, for the reason already recorded — translating scripture here would put unlicensed, unverified Bible text into a printed prayer packet. HINDI WAS BLOCKED ON A FONT, not on a translation. Devanagari renders as tofu boxes in the packet's Georgia/Palatino/serif stack because this machine has no Devanagari font at all. Noto Serif Devanagari (SIL OFL) is now vendored — devanagari subset only, 124 KB, licence and provenance beside it, matching how poppins/ ships latin and latin-ext separately. It is base64-INLINED into the packet, not linked. A packet is a standalone file meant to print offline — the Node generator writes HTML with no server, and the photos are already inlined for exactly this reason — so a URL would break precisely where the packet is most used. Cost: ~165 KB on a 3.5 MB packet, and only for that language. One rule prepends the face to the stack rather than editing 22 hardcoded Georgia declarations, and the browser falls back PER CHARACTER, so Latin scripture references in a Hindi packet still set in Georgia. mr and ne map to the same face; a missing font warns loudly rather than silently producing rectangles. Chinese needed no font work — CJK already shapes in the system stack — only the translation. ★ TWO MEASUREMENTS LIED ABOUT SCRIPT COVERAGE, recorded so nobody automates this cheaply. Comparing shaped width against a missing font reported ar and zh BROKEN and hi FINE — exactly backwards, because Devanagari tofu boxes happen to match real glyph advance. An ink-column-profile heuristic then scored CORRECTLY RENDERED Devanagari the same as tofu (3 vs 2 distinct heights), because its baseline bar gives a uniform profile. Both were abandoned: a detector that blocks working packets is worse than none. Verified: all 11 languages render 8 sheets -> 8 letter portrait pages, map painted, zero overflow. The Hindi prayer-points page was READ AS A PICTURE — conjuncts and matras shaped correctly, no boxes, Latin references still Georgia. |
||
|
|
dd14b5fe76 |
feat(packet): five more languages, and right-to-left support for Arabic
Some checks are pending
build-gate / build (push) Waiting to run
The packet now ships in 9 of the app's 11 locales: en es fr pt de it ro ru ar. Each carries all 21 translatable keys — page furniture, the twelve prayer points with sub-headings, the eleven rhythms, both Metacamp attributions, the privacy note. point4_passage (Mark 5:1-20 in full) stays omitted in every non-en block and falls back to the licensed English WEB text with its credit, for the reason already recorded: translating scripture here would put unlicensed, unverified Bible text into a printed prayer packet. RTL IS NOW REAL, not assumed. The renderer emits dir on <html> from an RTL locale set. Without it an Arabic packet renders readable glyphs LEFT-aligned — which looks like a styling quirk rather than a bug, and no assertion noticed. ★ TWO BIDI FINDINGS, both from reading the render rather than the code: 1. Scripture references are LTR runs inside RTL paragraphs, and the bidi algorithm REORDERS a run that starts with a digit: "1 Peter 5:6-11" rendered as "Peter 5:6-11 1". Each reference is now dir="ltr" + unicode-bidi:isolate. 2. With isolation, the reference LIST reads right-to-left on an RTL page (first item rightmost). That is correct for an RTL reader, not a defect — recorded in _meta so nobody "fixes" it. English source order is unchanged, asserted. ★ HINDI IS DELIBERATELY NOT ADDED. Rendering each script at 48px and READING THE PIXELS: Latin, Cyrillic, Arabic and CJK all shape correctly in the packet's Georgia/serif stack; DEVANAGARI RENDERS AS TOFU BOXES, because this box has zero Devanagari fonts (fc-list). A hi block would ship a page of empty rectangles. It is blocked on vendoring a Devanagari font into public/assets/fonts, which is its own piece of work. ⚠ THE MEASUREMENT THAT LIED. My first test compared rendered text advance against a missing-font baseline and reported ar and zh BROKEN and hi FINE — the exact opposite of the truth, because Devanagari tofu boxes happen to match real glyph advance. One screenshot settled in a glance what two rounds of clever arithmetic got backwards. Recorded in _meta. Verified at the rendered layer in all nine languages: 8 sheets -> 8 letter portrait pages, 95 people cards, 12 wheel segments, map painted, zero overflowing sheets, 19 strings differing from English per locale. The Arabic prayer-points page was read as a picture, not just asserted on. |
||
|
|
301cc38fba |
feat(upg-100): print a prayer packet from the map legend
Some checks are pending
build-gate / build (push) Waiting to run
Driver, as a first-time user: "I select one and it gives me print options at
the top right of the legend... that's the only way that map would be usable."
The affordance was already built and switched OFF. The shared legend emits
'export' with 'poster' | 'packet' for the selected node, the profile wired
@export, and SHOW_EXPORT_BUTTON was held false with the note "until the export
packet is finished: the button currently produces a broken packet". That
precondition is now met, so the switch is on.
ONE RENDERER, TWO CALLERS. The pages move to library/packet/packet-render.js,
imported by BOTH the build-time generator and the browser export, so the printed
packet and the generated one cannot drift. The card asked for exactly this:
"reuse the existing print actions rather than growing a fourth implementation."
The extraction is behaviour-preserving — the generated packet is byte-identical
to before apart from leading whitespace (the code is now inside a factory), and
check:packet-render still passes 8/8.
★ THE PACKET IS GENERATED ON CLICK, NEVER SHIPPED. One packet is ~3.5 MB (photos
dominate) and 28 zones would add ~100 MB to a repo that ALREADY had a publish
blocked at a 74 MB asset tree. Generating client-side costs zero repository
bytes; the photos come from URLs the records already carry. This is the Driver's
instruction: "instead of fetching the poster from the public folder I want the
client side code to generate the poster real time when you click on the button
so it does not take a lot of mb storage." Bundle cost: +21 KB (494 -> 516 KB),
which includes all 12 prayer points and Mark 5:1-20 in full.
Printing goes through the SHARED openPrintFrame — a hidden same-page iframe, not
window.open(). A popup is blockable and drops the reader out of the map.
Two traps the browser path had to handle, both already known to this codebase:
- pg_slugs BYPASSES the shared selector evaluator (it intentionally throws on
that type, and 12 of the 27 lists use it). Same special-case the map and the
poster wall apply, so all three agree about who is in a list.
- evaluate() returns a Set, not an array.
VERIFIED IN A BROWSER, not by reading source (new gate check:packet-browser):
live records -> shared renderer -> page.pdf(), giving 8 sheets -> 8 pages at
612x792pt, 95 people cards, 12 wheel segments, Mark 5 in full, 2 metacamp.org
attributions, 0 page errors. I18n verified with a partial Spanish block: the
translated headings rendered in Spanish while Mark 5 and the attributions fell
back to English, per key, never blank.
Card BUILD-UPG100-MAP-LEGEND-LEADS-WITH-THE-UPG-100-LIST part (d). Parts (a) and
(b) shipped earlier. Part (c) — removing the People Group tab and retabbing the
legend — stays OPEN: that card is explicitly driver_gated on "what is the
legend's tree once Region/Zone/People Group are removed", and the current tabs
are wired to real hierarchy data.
|