30 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3c17245c73 |
packets: give CI something to check, instead of a sweep that always SKIPs
Some checks are pending
build-gate / build (push) Waiting to run
check:packets-all is in gates:post. CI runs it on every build. It has SKIPped every single time, because library/scripts/posters/out/ is gitignored — both the records cache and every generated packet — so a clean checkout has none. Every packet assertion in this tree has therefore never once been evaluated there: the 12 prayer points, the wheel's 12 labels, page 1's geography paths, the print layer, the build stamp. Several ticks of work accumulating on a gate that had only ever graded files in my own working tree. A SKIP that is always taken is indistinguishable from an unwired gate — and the wave-membership census cannot tell them apart, because membership measures invocation, not evaluation. That census is how check:packets-all was created in the first place, which makes this the same class recurring one level up. I raised this at the end of the last tick and deferred it on the premise that closing it "adds a live-API call to every build". The premise was wrong. packet.mjs already had an OFFLINE mode, and a fixture needs no network at all. The fixture is the smallest zone — Tibet, 43 slugs, 20KB — with image_url stripped so CI can never fetch 43 photos from someone else's CDN. It is DERIVED, not hand-maintained: membership comes from the committed clusters.json, so check:packet-fixture regenerates and content-compares it. Re-adding an image_url fails; dropping a record fails naming the slug. A fixture packet stamps itself "fixture", and "fixture nomap" in CI, which has no poster sources either. `fixture` implies nophotos in the gate — without that it reported "every source record carries an image_url", a true sentence about real records and a false one about this packet. A real bug in my own change, caught only by running the script through its REAL invocation path: packet.mjs fails closed on a missing VITE_API_BASE_URL BEFORE loadRecords() ever looks at PACKET_RECORDS, so `bun run packet:ci` exited 1 on a tree that had everything it needed. Every hand-test had passed because I was supplying a dummy host. The guard now exempts fixture builds and still fails closed otherwise. From a genuinely clean state — the whole out/ directory moved away — the sweep now reports "1 packet(s) checked, 1 passed". i18n verified through that same path: a partial block (1 string, 5/12 points, 3/12 rhythms) renders 12 points, 12/12 wheel labels, wheel 3 translated and 9 English, 0 blanks. check:packet-fixture joins gates:post (34 -> 35), README quote re-synced in the same commit. Wave 35/35, packets-all 11/11 here and 2/2 in the studio. |
||
|
|
64fed43449 |
packet: stamp how a packet was built, into the packet
Some checks are pending
build-gate / build (push) Waiting to run
out/packets/ is gitignored, so check:packets-all grades whatever .html files happen to be sitting there — an accidental set, not a current one. On this machine that was 11 freshly generated packets. In doxa-maps-plugin-studio, which shares main with this worktree, it was two NOPHOTOS=1 layout probes of mine from earlier ticks, and the sweep was RED there for anyone who ran the wave. The gate was honest about each file it checked; the set it checked was accidental. The underlying problem is that a fast layout-iteration build and a shippable one were byte-indistinguishable. The packet carried lang and dir and nothing about its own provenance, so the only way to tell them apart was for the person running the gate to remember and pass PACKET_EXPECT_PHOTOS=0. packet.mjs now stamps data-packet-build with the flags it generated under, and the gate reads it. The GENERATOR writes it rather than the gate inferring it: had the gate guessed "no photos present, so probably a probe", it would have excused genuine photo-fetch failures. Tested explicitly — a packet stamped "full" whose map svg is then stripped STILL FAILS. The stamp records intent and cannot excuse a defect the build did not declare. Deliberately not a timestamp. An mtime staleness rule was considered and rejected on evidence from this same session: packet-strings.json was 303s newer than the packets while byte-identical to HEAD, because a probe had restored it from backup, so an mtime rule would have failed a correct tree — the exact mistake fixed two commits ago in the library-index gate. Content comparison is not available here (regenerating hits a live API and takes minutes), so the build records its intent instead of the clock. The studio sweep goes from "2 checked, 0 passed" to "1 checked, 1 passed", with the packet self-declaring build="nophotos nomap". Env vars stay as an override for pre-stamp artefacts, and the declared-deliberate note now names whichever source actually declared it rather than always crediting the env var. All 11 locales stamp "full". i18n re-verified with a partial block (1 string, 3/12 points, 6/12 rhythms): wheel 6 translated and 6 English, 12 point rows with 6 translated, 0 blanks. Wave 34/34, packets-all 11/11 here and 1/1 in the studio. |
||
|
|
e130cb672a |
packet generator: say when page 1 has no map, instead of printing a tick
Some checks are pending
build-gate / build (push) Waiting to run
`✓ laos 95 groups 8 pages 0 photos` — for a packet whose first page is blank. The page count stayed 8 because a blank sheet is still a sheet. The map one-pager is lifted from out/posters/<zone>.html, which a separate manual step writes. When it is absent packet.mjs deliberately still builds, and that fallback is right — but it was silent. The empty-groups branch three lines above already warns and skips; the map branch had the same shape and no warning. A gate catching this is not the same as the tool reporting it. The gate added last tick runs later, optionally, and only when someone invokes it. The generator prints its tick at the moment of production, which is the one output an operator is guaranteed to see. Now it prints ⚠ with the reason inline and repeats it in an end-of-run summary naming every affected zone and the fix — a per-line mark scrolls off in a 27-zone run, and this is the one fault that cannot be seen by counting pages. Both silent branches are covered, distinctly: a missing poster reports "no out/posters/laos.html"; a poster that exists but carries no <svg> — a truncated generate run — reports "out/posters/laos.html has no <svg>". An operator can tell "never generated" from "generation broke". Also adds the `packets` script. Its absence is why the posters -> packets dependency was never expressible anywhere: packet generation was only ever a hand-typed node invocation, so nothing could state what it reads. Measured in both repos. doxa-maps-plugin-studio has 0 generated posters against wt-packet's 28 — out/ is gitignored in both, so posters are build artifacts and this is a missing build step, not a repo defect. A fresh studio run now warns; wt-packet prints a clean tick. The size difference corroborates: 0.4 MB with the map, 0.1 MB without. i18n re-verified end to end since this touches the generator: a partial locale block (2 strings, 3/12 points, 4/12 rhythms) renders the wheel 4 translated and 8 English, 12 point rows with 4 translated, 0 blanks, clean tick. All 11 locales regenerated with photos — packets-all 11/11, each 12/12 wheel labels, 12/12 points, 13 geography paths. Wave 34/34. |
||
|
|
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. |
||
|
|
3a468c2bc3 |
The poster print preview rasterises too, and a deployment can ship sources without rasters
Some checks are pending
build-gate / build (push) Waiting to run
posters/*.webp is not shipped by default (42MB; bundling pushed the tree to 80MB,
git-packed to 57MiB, and the catalog rejected the push). The DOWNLOAD path has
handled that since
|
||
|
|
4cb04480cc |
Localize tab labels for it/ro/zh, and prove the catalog is actually read
Some checks are pending
build-gate / build (push) Waiting to run
120 keys across three locales: legend.json (12 each), search.json kinds (10
each) and tabs.json (18 each, previously absent entirely). Each locale follows
its own established vocabulary rather than a parallel one — Popolo/Popor/民族,
matching the terms already used in that catalog's detail and legend strings.
zh stays Simplified here, as the rest of this tree is, even though the prayer
packet's zh block is Traditional: a reader sees 民族 next to 祷告进度, never
the two files side by side.
Ceilings drop 94 -> 54 per locale. regions.json is the only i18n debt left in
the app: 54 keys of full sentences, three locales.
WHY THE NEW GATE. check:i18n-parity compares catalogs TO EACH OTHER, so it
certifies a file no t() call ever reaches — perfect symmetry, zero consumers,
green forever. Grepping for t('tabs. found ZERO call sites and tabs.json was
nearly recorded as dead. It is not: the only consumer builds the key
dynamically (SemanticTreeLegend.vue:40, 'tabs.' + (tab.i18nKey || tab.id)),
which no literal-prefix grep can see. The reverse error appeared in the same
pass — regions. and options. matched only poster-regions.mjs and a JS options
parameter. Static reachability is unavailable in both directions, so
check:tabs-localized renders the labels and reads them.
It asserts DIFFERENCE from English, not mere presence: te() falls back to the
hardcoded English label, so "shows some text" passes on the exact failure
being tested. Mutation-tested three ways — missing catalog FAILs, an all-English
catalog FAILs (first draft PASSed it, taking a per-pair exemption seven times),
one legitimate collision still passes with a note.
Both i18n gates now run in gates:post; neither was wired into the build before.
Also fixed, found by the build rather than sought:
- check-i18n-parity hardcoded an absolute path, so the studio copy silently
graded wt-packet's locales and reported PASS on a tree it never read. Now
repo-relative. That exposed the studio's own locale tree as a strict subset,
103 keys behind including buttons.json:share — the very orphan this gate's
header says it was written to catch, reappeared unseen in a second repo.
Synced forward: zero studio-only keys, zero text conflicts, lossless.
- index.json still declared field-access-visas with rows:1146, a leftover from
the commit that removed that table as private. The generator already filters
private tables; the committed artifact predated the filter. Removing the
declaration also removes the row-count leak. Copying the file in would have
reopened the leak.
- check:sfc-parse needs an installed node_modules and is the first pre-gate to
fail without one, so a dependency-less worktree reads as a parse error.
Documented, with the reason switching it to bun only appears to fix it.
|
||
|
|
61dcd9b28e |
Merge main into bundler-1/work (regenerate the built tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
The nine conflicts were all in generated artifacts under public/. Built files are not hand-merged: every bundle whose source either side touched was rebuilt, the tree regenerated against main's new generate-tree, and the hashes re-baked. Source merged clean — main never touched PosterWall.vue or en/regions.json, they only appeared in a two-dot diff because this branch changed them. |
||
|
|
ddde518a85 |
Regions: expand a region to its country posters, reusing the shared wall
Driver request: "a toggle or carrot added to the bottom of the region section that says view country posters, which will let you view country posters for each of the countries in the regions" — with View-on-map and Print, "the same reusable code from upg 100 list". This is a third sanctioned addition alongside the tab strip and the no-selection chooser. Almost nothing was built. library/components/poster/PosterWall.vue was already the shared, fully parameterised grid, and it already owned the card layout, the "Show on map" header button, the PRINT OPTIONS footer and the client-side raster. regions-list.vue supplies only data and a toggle — the same division country-poster-wall.vue states in its own header. - A "View country posters" toggle at the bottom of every region section, carrying aria-expanded and aria-controls, using the same caret SVG the header and footer links already use, rotated when open. - Opening a region renders that region's countries. Measured: 41 cards for Africa and all 41 previews LOAD (naturalWidth>0 — a 404 still yields an <img>, so the load is the real check). No assets were copied: public/assets/previews already holds 174 country-*.webp and generate-tree already stages them. - "Show Chad on the map" reaches the Region Dashboards tab with title "Chad" and hash "#chad", by mapping PosterWall's CustomEvent listId back through the country-selected the tabs already exchange. - India prints its own reason with the exact number from the data; China renders a real poster. Only India is unsupported — 855 groups and no sheet, against China's 154 on a 3x4. Two defects the screenshot caught and no assertion did: - PosterWall's root is position:absolute;inset:0, because in its own bundles it IS a pane. Embedded in a region card it painted 1440x1100 over the region's country list. Fixed by parameterising the shared component rather than overriding its CSS locally: new `embedded` and `sectionHeads` props, both defaulting to today's behaviour, so country-posters and key-cities are unchanged by construction. Both re-checked. Re-measured: root static, 1200px wide, panel below the grid, no overlap. - With one unnamed group the wall printed a section head reading the literal "Other" plus a count duplicating the region card's own summary. Suppressed by sectionHeads=false. The dataset moved to library/data/country-posters.json now that it has a second consumer, one import line changed in country-posters, and emit-countries.mjs was retargeted so the next generator run cannot recreate the duplicate. The three new strings do not exist on DX-life, so there was nothing to copy and nothing was invented in another language: en only, with the other seven locales falling back to the key. Regression: 15 suites green, including region-vs-live 73/73, country-vs-live 28/28, mobile 44/44 and 26/26, and a new _probe-region-posters.mjs. check:library-index drifted for real this time (3 new library files) and is regenerated here. |
||
|
|
ec974b4c63 |
fix(posters): ask the legend which zones are reachable, not clusters.json
Some checks are pending
build-gate / build (push) Waiting to run
publish-html.mjs enumerated the raw cluster keys under a comment that said "the zone ids the app can actually reach" — which was exactly what it did not do. It then warned that deaf-old-world and deaf-new-world-asia-pacific were MISSING on every run, when nothing was missing. Those two are SUPERSEDED by the unified `deaf` row (affinity bloc A017), not absent. Verified against live records instead of trusting the comment that says so: 63 + 60 = 123 = A017 exactly, zero groups on either side of the difference. They stay in clusters.json deliberately — three isolation-proof scripts assert deaf_lists.length === 2 — but the legend does not expose them, so no user can select them and a poster for them is unreachable by construction. It now derives zone ids from leafZones(buildLegendNodes(...)), the same source the app itself uses, so the two can never disagree again. Result: 28 posters published, zero warnings, matching the legend's 28 leaves. records:null, not records:[] — buildLegendNodes treats an empty array as truthy and then demands population fields that are not available here. A recurring warning that is always wrong is worse than no warning: it trains everyone to ignore the one run where something really is missing. |
||
|
|
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. |
||
|
|
41847076a2 |
feat(packet): ship the prayer packet in Spanish, French and Portuguese
Some checks are pending
build-gate / build (push) Waiting to run
The app ships 11 locales; the packet shipped 1. A Spanish-speaking reader got a
Spanish interface and then an entirely English prayer packet. The per-key
fallback machinery was built and verified several ticks ago — the catalog was
simply never populated, so i18n was functionally unshipped.
es, fr and pt now carry all 21 translatable keys: the page furniture, the twelve
prayer points with their sub-headings, the eleven prayer rhythms, both Metacamp
attributions and the privacy note. Terminology follows the app's OWN existing
locales rather than inventing a parallel vocabulary — "grupo étnico" for people
group and "no evangelizado" for unengaged come straight from
library/i18n/locales/es.
★ point4_passage IS DELIBERATELY OMITTED FROM EVERY LANGUAGE BLOCK, and that is
the point, not an oversight. It is Mark 5:1-20 printed in full, and the file's
own guidance already required "a public-domain or properly-licensed version for
that language". Translating scripture myself would put unlicensed and
unverified Bible text into a printed prayer packet. Omitting the key makes it
fall back to the licensed English WEB text with its credit line intact —
honest, and visibly English, which invites a real translation rather than
hiding the gap. The note in _meta now says this outright so the next translator
does not "complete" the block.
Also recorded, because the render made it visible: scripture REFERENCES stay in
English in every language ("Matthew 27:27" on a French page), so a reader can
find the passage in their own Bible. That is a deliberate tradeoff and it does
look foreign next to fully translated prose — _meta now names it and says how to
add localised book names properly if a language wants them.
Verified at the rendered layer in all four languages: 8 sheets -> 8 letter
portrait pages, 95 people cards, 12 wheel segments, map painted, zero
overflowing sheets, and Mark 5 correctly falling back to English with its
credit. The French prayer-points page was read as a picture, not just asserted
on: all twelve points translated, Matt 27:27 intact at point 1, "un lieu sûr" at
point 8.
|
||
|
|
cfea0c8e70 |
fix(posters): measure the full-version header instead of guessing 460
Some checks are pending
build-gate / build (push) Waiting to run
Four zones had NO POSTER AT ALL. measureHeader() opened with
`if (!SPARSE) return HEADER_BASE` — a flat 460 — so the per-zone measurement its
own comments describe ("HEADER IS PER-ZONE, NOT A SHARED CONSTANT...
measureHeader() predicts the height from the zone's OWN content") only ever ran
in SPARSE mode. Outside it, any zone whose header was taller than 460 tripped
the fail-loud guard and rendered nothing:
oceania-indonesia 504 andean-caribbean 504
asia-china-tibet-autonomous-region 481 deaf 619
Raising the constant was not the fix. HEADER is subtracted from the map height,
so 460 -> 620 would have stolen 160px of map from the 25 healthy sheets to
rescue 4 — the exact tradeoff the per-zone measurement exists to avoid, and the
one its own comment records (tuned to North India, it overflowed west-africa;
tuned to west-africa, it stole from everything else).
MEASURED the real header on six zones instead:
HEAD_BOTTOM = (headY + 34) + 118 exact on all six
headY = 191 + 23 * tableLines exact on all four probed
The 118 is the two vision columns — a constant 104, because that copy is fixed
and wraps identically on every sheet — plus the 14px trailing gap. ALL per-zone
variation is the facts table, which advances 23px per wrapped line. So this is
arithmetic on the same inputs the renderer wraps, not an estimate, and it needs
no calibration history like the SPARSE model's +24/+52/+66/-8/+6 above it.
★ THE ERROR WORTH RECORDING: my first attempt derived the constant by READING
the code — tracing headY's assignments to 150 + 23n - 12 — and was 53px short on
every zone, because the scripture block between the table and the vision columns
also advances the cursor and I had not traced it. Every zone then failed,
including ones that worked before. Deriving it from four measured renders got it
exact. Reading selectively is not measuring.
Result: 28 posters render, zero failures. And the map GREW on the healthy
sheets, because they no longer pay for the worst case — laos' map frame starts
at y=443 against the old flat 460, while deaf takes the 627 it genuinely needs.
Still missing after this: deaf-old-world and deaf-new-world-asia-pacific, which
never reach the render at all (silent skip, exit 0 — they are `deaf_predicate`
selectors that leafZones() does not return). Different cause, carded separately.
|
||
|
|
52b7a110d8 |
fix(posters): one bad zone no longer costs the other 28
Some checks are pending
build-gate / build (push) Waiting to run
generate.mjs threw on the first zone whose header overflowed, so a single bad sheet ended the run: 16 posters written, the remaining 13 silently unbuilt, and a non-obvious short count as the only symptom. Failures are now collected, the run continues, and it exits 1 at the end with a census naming every zone that has no poster. The guard stays loud — an overflowing header is a real defect and must not be downgraded to a warning — but it fails the RUN rather than truncating it. That change immediately surfaced a FOURTH broken zone the abort had been hiding. All four overflow the same way: oceania-indonesia 504 vs HEADER=460 (+44px) andean-caribbean 504 vs HEADER=460 (+44px) asia-china-tibet-autonomous-region 481 vs HEADER=460 (+21px) deaf 619 vs HEADER=460 (+159px) Root cause, found but NOT patched here: measureHeader() returns a fixed HEADER_BASE when SPARSE is off instead of measuring the zone's own content — so the per-zone measurement its own comments describe only runs in SPARSE mode. That is a separate fix with its own layout risk; carded rather than bolted on. Also adds publish-html.mjs, the missing half of the browser-rasterise fallback. That fallback fetches posters/<id>.html and NOTHING published it: out/ is gitignored and public/ had zero .html, so a zone with no image 404s today. The script writes zone poster HTML into public/assets/posters/, skips city-* posters (they need OSM tiles to regenerate, so their images must stay), and warns loudly about zones with no HTML instead of quietly writing fewer files. ★ IT IS NOT RUN AS PART OF THIS COMMIT, and the reason is worth recording. Measured with real git packs: webp 5.0 MB raw -> ~5.0 MB packed (already compressed; git cannot pack it) HTML 21.4 MB raw -> 5.78 MB packed (text, repetitive across posters) Swapping zone webp for zone HTML is about +0.8 MB PACKED — a net LOSS, not the 42 MB win the headline implies. The raw-byte comparison that made it look like a win (21.4 vs 5.0) and the gzip comparison that made it look better still (5.8 vs 5.0) both mislead; what git stores is the number that decides it. The real 22 MB is 176 city-* posters, which cannot be replaced this way at all. So the deletion this was leading to is deliberately NOT done: it would have made the repo bigger while being reported as a saving. |
||
|
|
4475581c1b |
Close the visa-data leak at its factory + harden the gate
Some checks are pending
build-gate / build (push) Waiting to run
The field-access-visas table (580 KB -- how workers enter closed countries) was found PUBLIC in three copies and LIVE-SERVED at 200: the key-cities bundle data dir, the research out/ dir, and a stale staging tree inside the served catalog. Root cause: emit() enforced PRIVATE_TABLES but the ROUTE bundle-copy loop did not -- a privacy rule enforced at one of two writers is not a rule. The loop now skips PRIVATE_TABLES; the redundant source copies are deleted (data-private/ keeps the one canonical file); and the hygiene gate flags the filename and visa_ fields as HARD findings in every mode -- this data class never rides the strict-tier softening. All three public copies and the live URL were purged first and verified 404 by anonymous fetch before this commit. |
||
|
|
7bba1c0771 |
feat(upg-100): the prayer card deck prints as CARDS, not as paper
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "the print dialog that opens up needs to show all the cards not in a
paper format but a CARD format, the aspect ratio that is used in the card
generator."
The deck set @page{size:letter portrait} and gave each card height:100%, so 95
cards printed as 95 mostly-empty 8.5x11 sheets — and with NO photo, because
image_url was never in the fetch. Measured: 95 of 95 groups DO have a portrait,
so the omission cost every card the one thing that makes it a prayer card.
Now the PAGE IS THE CARD. @page takes the stock's own inches, so one card is
exactly one page: 95 cards -> 95 pages at 180x252pt (2.5x3.5in). Print on
cardstock and cut, or send the PDF to a card printer. The stock is a user
choice, per the Driver's ask to "choose the layout of the print dialog to choose
the aspect ratio": poker 2.5x3.5in (default), postcard 4x6in, A6.
⚠ @page SIZE MUST BE EXPLICIT INCHES. Chromium SILENTLY falls back to letter for
a named size it does not know: `size: A6 portrait` yields 612x792pt with no
error of any kind, while `size: 2.5in 3.5in` yields exactly 180x252pt. The gate
therefore asserts the MEASURED MediaBox — a check that grepped the stylesheet
would pass a deck that prints as paper.
Card content follows doxa-life/promotional-material-generation, the Python data
pipeline feeding the InDesign template: per language it emits Description,
Religion, Area, Country, a QR to that group's page, and the URL. Ported to
JavaScript here so the deck generates in the map app on click. Description and
Area are NOT available from this API — measured 0 of 2107 records carry either —
so the card is built from what genuinely exists: portrait, name, country,
population, language, religion. An empty block reserved for data that never
arrives is worse than a card designed around the data there is.
QR is hand-rolled (library/packet/qr.js, ~200 lines): byte mode, level M,
smallest fitting version, mask 0. A symbol is pure computation, so it costs one
small file instead of a package and runs identically under Node and in a tab —
the standing constraint is "without adding a lot of dependencies on the
internet". A QR that does not scan looks exactly like one that does, so it is
verified by DECODING: an independent reverse-path decoder re-derives the data
placement, unmasks, de-interleaves and parses the header. 5/5 round-trip,
including version selection (v2 for a short URL, v3 for real ones).
I18N: the QR encodes the LOCALISED profile URL, so a Spanish card scans through
to the Spanish page — verified, pray.doxa.life/es/aheu. Strings fall back per
key; checked with a partial Spanish block ("Ora por ellos" rendering while
Language/Religion stayed English).
★ ONE BUG THE NUMERIC GATE PASSED, worth recording. The first deck rendered
"LAO", "thm", "EAN" — raw codes on a card a person is meant to pray from —
because normalizeRecords() flattens each { value, label } dict to its VALUE and
parks the label under <field>_label. A gate counting "4 rows rendered" passed
it: the rows were there, just unreadable. The gate now asserts the values are
HUMAN-READABLE, not merely present.
Verified by check:card-deck across all three stocks: 95 cards / 95 pages, 0
broken portraits, 95 QR codes with real symbols, 0 raw codes, 0 overflow.
|
||
|
|
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.
|
||
|
|
fd2077625a |
feat(posters): capitals and major cities on the country maps
Some checks are pending
build-gate / build (push) Waiting to run
The Driver asked for map detail: "a typical map poster needs details on the map that is not hard to see, but not so much that it becomes too complicated — cities and capitals makes sense." Natural Earth 1:10m, PUBLIC DOMAIN: no attribution to carry onto a printed packet, vendored so a build is reproducible offline with no runtime dependency. Trimmed at vendor time from 4.9 MB / 7,342 places to 170 KB / 2,573 (national + admin-1 capitals, plus cities >= 250k) because a poster cannot legibly carry 7,342 dots and the packet is downloaded. The layer is deliberately SUBORDINATE. It answers "where am I?"; the people-group pins answer "who do I pray for?", and they keep the only saturated colour on the sheet. Capitals are ringed dots (the cartographic convention), cities plain dots — shape and value carry the rank so no extra hue is spent. Reading order stays country > province > city > people group. Three things had to be right, each found by measuring rather than by looking: 1. RANK BY RELEVANCE TO THE SUBJECT, not by global importance. Sorting by capital-rank then population is the obvious choice and is wrong: the neighbours are usually bigger. On the Laos sheet that sort spent the budget on 12 Vietnamese and 8 Thai cities and printed exactly ONE Laotian place while 12 more sat available — a poster of Laos labelled with other countries' cities. Subject countries now win the budget. 2. DERIVE THE SUBJECT FROM THE PINS, not from the zone. zone.adm0 exists only on generated city/country zones; the hand-authored lists carry a selector in SEVEN shapes (pg_slugs, country_code, country_codes, wagf_block, wagf_region, union, deaf_predicate) and the most common names no country at all. Reading zone.adm0 produced an empty set and the whole sort silently did nothing. The pins always know their own country. 3. AVOID THE PINS USING WHERE THEY ARE DRAWN. Two distinct errors here. Testing the label's ANCHOR is wrong for a right-aligned label, whose glyphs run leftward from it — so the box is tested, not the point. And the de-overlap relaxation MOVES badges after projection, so proj(pin.lng,pin.lat) is not where the badge lands; this block therefore runs AFTER layoutLabels and reads laid[].px/py. With the naive tests, 7 of 25 labels sat within 14px of a pin and Saravan landed 1.4px away while the check reported it clear. Now: 291 labels across 16 posters, ZERO overlaps. A city that cannot fit clear of a pin is dropped — a missing city costs nothing, an obscured people group costs the poster its purpose. ⚠ ROADS WERE REQUESTED AND DELIBERATELY NOT SHIPPED. Natural Earth's 10m roads layer was fetched and measured: ZERO features for LAO, MMR, KHM, VNM, THA, NPL, BTN, TCD, NER, MLI, SOM, AFG, YEM, IDN, IND and CHN. Its coverage is essentially USA/CAN/MEX. It would draw a dense network on a US map and nothing on every country these packets are for — a broken-looking map exactly where it matters. Real road coverage needs an OSM extract: a much larger dependency with an ODbL attribution obligation, so it is carded rather than half-shipped. See data/README.md. Note: zone oceania-indonesia fails generation with a header-overflow error. Verified pre-existing on a clean tree — unrelated to this change. |
||
|
|
1c24759e21 |
feat(posters): UPG-100 prayer packet generator, letter portrait, i18n
Some checks are pending
build-gate / build (push) Waiting to run
The packet as specified: page 1 the map one-pager, pages 2..n the people
groups 20-per-page with photos, then the 12 prayer points, then the prayer
wheel. Every string is externalised so a new language is a new block in
packet-strings.json, not a code change; fallback is PER KEY, so a partial
translation renders what it has and leaves the rest in English rather than
blanking. Verified with a partial es block before removing the stub.
Three bugs worth recording, each of which rendered silently wrong:
1. zoom, NOT transform:scale(), for print. transform is paint-time, so the
sheet still RESERVES its 1941px layout box and the paginator breaks it
across two pages, inking only the top of the first: 8 sheets came out as
16 PDF pages. Negative margins do not help — they collapse the outer
space, not the fragmentation box. zoom is layout-time, so pagination sees
the scaled box. Measured: transform 2 pages/sheet, transform+margins 2,
zoom 1.
2. The map SVG carries almost no presentation attributes — generate.mjs
styles it from the poster document's stylesheet. Lifting the <svg> alone
left those rules behind, and SVG fill falls back to its initial value,
which is BLACK: 670 elements painted solid black with no error, no broken
image, nothing any existing assertion could see. Fixed by carrying the
poster's stylesheet minus its document-level rules, so a palette change
in generate.mjs still follows the packet automatically.
3. LANG is the shell's POSIX locale ("en_US.UTF-8"), not a BCP-47 tag, and
throws in localeCompare. PACKET_LANG is the only source.
The verification lesson is the bigger one. The probe PRINTED "PDF pages: 16"
for several runs while reporting the packet verified, because the bad counter
never looked at that value — a number that is displayed but not asserted on
is decoration. Then the first fix for the black map asserted on the first
<rect>, the one element just repaired, and went green while 647 others were
still black. Both assertions now test the property rather than a proxy: one
sheet must be exactly one page, and the map as a whole must not be
predominantly black-filled. Both were confirmed to FAIL on the known-broken
input before being accepted.
|
||
|
|
839b0c453d |
A letter-portrait sheet, an honest credit, and a privacy line
Some checks are pending
build-gate / build (push) Waiting to run
FORMAT=letter. The same poster on paper a home printer takes. Width stays 1500 for the reason the 3x4 note already gives -- every width-derived measurement (the 330 gutter, the name columns, the header budget) was calibrated there and carries over untouched. Only height moves: 8.5x11 at width 1500 is 1941. Layout, text and spacing are unchanged; the extra height goes to the label columns, which is where 95 names wanted it. PRINTS AS ONE PAGE -- and this fixes the SQUARE sheet too. The stage is sized in CSS pixels while @page declares a physical size, so a browser mapped 1500px to 15.6in and TILED it: measured, the 3x3 sheet printed as FOUR pages. It went unnoticed because these posters were always screenshotted by shoot.mjs, never printed from a browser. A scale computed at generate time (page width in px over stage width) makes one sheet equal one page on every format. Verified via page.pdf: letter 612x792pt x1 page, square 216x216pt x1 page (was 4). ATTRIBUTION CORRECTED. The credit said "(c) Mapbox (c) OpenStreetMap". This file's own header says NO Mapbox, NO token, NO external fetch at render time, and the basemap is bundled GeoJSON -- so it credited a vendor contributing nothing, which is the kind of line that makes a later reader add a token "to comply". The real source is geoBoundaries (the adm0/adm1 files carry its shapeGroup/shapeName fields), CC BY 4.0, which REQUIRES attribution. OSM is named only when OSM tiles are actually fetched (BASEMAP=1). PRIVACY LINE on the sheet. People-group coordinates arrive already generalised from the publisher -- deliberately not precise locations. A printed sheet travels past the person who made it, so it now carries that fact itself. Worded as provenance, not as a claim about processing we do not perform. It gets its own row: sharing the credit row was a measured collision (516-984px against a stamp ending at 543), and it is a sentence to READ, not a provenance stamp. Also: a backtick inside a CSS comment inside a JS template literal broke generation outright -- the trap check:template-literal exists for. |
||
|
|
3a7913df64 |
Record why the table generator keys by ISO code and never by name
Extending last tick's external-source check from legends to the baked tables turned up a real discrepancy: gaps-and-opportunities.json has 2,106 rows against a 2,107-row corpus, with five name mismatches. The table says "Jerba" and "Tmagourt" where the live API now says "Amazigh of Djerba" and "Amazigh, Matmata" — the same Tunisian peoples, relabelled upstream since the snapshot. It is harmless, and the reason is the point. Nothing joins on the name: the generator keys everything by iso639_3, and the consumer does the same at resources-map.vue:194, whose comment already states the rule and its stake — "Never a name match: language names collide and drift ... a fuzzy hit here would put the wrong scripture in someone's hands." The generator followed that rule in four places and never said why. So the decision lived only in the consumer and in my head, and a future author tidying this file toward a name join would find nothing to stop them. They would silently drop three groups and orphan two rows: no error, no empty state, just fewer people on a map about people nobody has reached. That is precisely what check-label-joins.mjs was written to prevent. Verified rather than asserted before writing it down: the join key is clean on every row — heart_iso639_3 present and well-formed on all 2,106, zero blank, zero malformed, 862 distinct codes; bridge_iso639_3 blank on 461, which is expected because not every group has a bridge language. gospel-resources/ data-tables renders 50 rows by 21 cells with zero console errors and zero responses >= 400. Both citations in the new comment were checked against the files they name. Comment only; no behaviour change. Five gates pass, including check:label-joins itself and check:legend-source from last tick. |
||
|
|
159acac940 |
Point the app-tables guard at pages that exist, and let it count rows it can see
verify:full runs two browser guards that `bun run verify` skips, and both were failing. Neither was a product defect; both had rotted unnoticed precisely because verify-all.mjs marks them `browser: true` and skips them unless --full is passed, so the build chain has never run them. verify-app-tables was broken four ways, each of which alone made it useless: Its default port was 8917, which nothing in this repo has ever bound. Now 4173, the port `bun run serve` binds, so it runs with the same server as every other browser gate. APP_BASE still overrides. It navigated to `index.html#key-city-posters` and two sibling fragments, resolved against the PROFILES object in layouts/app-wrapper-material. That shell is not what `/` serves — as of 2026-09-22 the root page mounts <plugin-studio>, and the built root contains ZERO of those three keys. The list is now three real profile paths taken from manifest.json. key-city-posters is dropped rather than renamed to something that merely looks similar: no such profile ships. Its "are we on our page" check matched the shell title "Doxa Maps". A leaf titles itself "<profile> · <bundle>". My first replacement matched an absolute script path and was equally wrong, because a leaf references its bundle relatively (src="../gospel-resources.js") — measured, on a page that was fine. It now matches the SRI integrity attribute, which this build guarantees by construction and no other server emits. Its row counter only counted rows ONE level inside a shadow host, with no seen set. A row in the light DOM, or one shadow level deeper, counted zero. Now walks every depth. The last defect is the one worth reading twice. The app header printed near the END of the loop body, after the row check, so every verdict appeared ABOVE the app it described and the whole report was offset by one. That made a PASSING app (gospel-resources/data-tables, 40 rows) look like the failure and the real failure look like it had rendered rows. I chased the wrong app through three rounds of instrumentation before reading the output in order. A report that is correct but misordered costs a wrong diagnosis, not just time. Read correctly, the guard now says: cities-internet/data-tables 40 rows and 1 table fetched; gospel-resources/data-tables 40 rows and 1 table fetched; and gospel-resources/resources-list fetches NOTHING and renders nothing. That last one is the already-carded gospel-resources data gap, waiting on the three Driver decisions — the component is built and correctly showing an empty result for data it was never given. So the guard is reporting one real, known, Driver-gated absence, which is what it is for. Everything else in it was already sound and is untouched: the recursive shadow-root walk for row counting, fail-closed on zero fetches, and third-party request detection, which the file rightly calls a persecution risk rather than a performance note. |
||
|
|
9dc4dda8cf |
consolidate docs into documentation/ and deployment shells into deployment/
Some checks are pending
build-gate / build (push) Waiting to run
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.
docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.
The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.
Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
- join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
- tauri frontendDist '../public' -> '../../public', since src-tauri is now one
level deeper; check:tauri caught this exactly as the card predicted
- the WordPress contract test's dirname(__DIR__,2) -> 3, same reason
Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.
Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
|
||
|
|
2d64e21fbf | Complete app-profiles->plugin-designs sweep across comments/docs; build green; layouts kept at root (load-bearing) | ||
|
|
1a3925c6a6 | Rename app-profiles -> plugin-designs; build green under new naming (load-bearing paths updated, guards pass) | ||
|
|
b7d91e9aca |
Public data ships without working notes; unused data files stay out of the drop-in
- data/ tables and the people-groups file: meta blocks reduced to provenance (source, url, downloaded, rows, grain, attribution); the link-verification table keeps only state and detail per link; generators write the same shape - dropin.exclude in the build config: people_groups.csv, open-doors.json, platform-reachability.json and tables/index.json are not shipped (nothing loads them); sync-dropin honours it - public-hygiene --strict flags note-style meta keys in shipped data - Rebuilt bundles and gallery |
||
|
|
3fee91a193 |
Release governance: field-access data is private, never shipped
data-private/ is born: datasets whose publication could help close doors stay out of the web-root and out of public repos, permanently. field-access-visas moved there; the tables index no longer lists it; the key-cities Field Access tab is removed from the public build; the table generator routes it (and any future PRIVATE_TABLES entry) to data-private/ and excludes it from the index. Public-good data (internet gaps, gospel-resource gaps, people-group pairings) ships as before. |
||
|
|
994b8d7dcf |
Prepare repo to the vision: BLUF vision-cast README, library warning, SRI injection, term scrub
- README rewritten as a BLUF-first vision cast for vibe coders (key-terms glossary, origin story, features, requirements, data caveat, developer's note). - library/components/WARNING.md: prototype in your app-profile; only proven, parameterized components graduate into the shared library. - generate-tree.mjs: inject Subresource Integrity (sha384) + crossorigin into the standalone page <script> tags and the copy-paste embed snippets (manifest = registry). - Scrubbed identity-tied phrasing from comments/docs, keeping the technical rationale and the mission content. |
||
|
|
35ac1c5607 | docs: simplify all folder READMEs (what's here + graphic + link); migrate complex reference to docs/architecture/{data-sources,i18n,components,app-wrappers,poster-generation}.md | ||
|
|
d5ba21fa76 |
doxa-maps — portable bundler for embeddable JAMstack maps
Standalone repo that builds embeddable JAMstack map bundles (web components), composes them into a lazy-loaded SPA via layouts/, and optionally wraps a Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs. History reset to a single clean commit; no PII, IPs, or internal terms. |