Commit graph

857 commits

Author SHA1 Message Date
doxa-maps-88655c70
29e4b153a4 List what is missing, in a form you can take away
Every table in this bundle lists what EXISTS. The map can filter to the gap and rank its
40 largest, but 40 is a preview and a map is not something you hand to a prayer group or
work through in a spreadsheet. There was no exportable list of what is missing.

"Where Nothing Was Found" is that list: 3,457 people groups, 347,030,459 people, 183 of
them speaking a sign language. Largest first, with CSV and JSON export already provided by
the shared table.

IT IS NOT THE EXISTING "GAPS & OPPORTUNITIES" TABLE and could not have been folded into
it. That one is one row per group across the curated 2,106-record set, so it structurally
cannot see these 3,457. It keeps its own job — bridge-language analysis for the groups it
does cover — and this sits beside it.

EVERY ROW CARRIES ITS OWN PROVENANCE: checked_pools and unchecked_pools travel with the
data, so an exported CSV cannot be read as a finding about the world. Rows with an EMPTY
"pools checked" are groups whose language was in no resource table at all, and they say so
rather than looking identical to a group that was searched and came back empty. The meta
states outright that audio has never been searched for any group here.

THE COUNTRY CLICK NOW REACHES IT. The deep-link filter applied only to the resource table,
so a country click could show what a country HAS and never what it lacks — the half a
mobiliser came for. Chad: 94 of 3,457. Nepal: 54. Both match the map's own country sheet
exactly, which is the check that the two surfaces are describing the same groups.

180 ROWS PRINTED A RAW ISO3 IN THE COUNTRY COLUMN — "CHE", plus Canada, Taiwan,
Timor-Leste, Mexico, Argentina. gospel-by-country knows 170 countries and the boundaries
file 162, while the pins span 242. Names now come from people-groups-slim, the source the
pins were built from, so coverage matches by construction rather than by luck; the curated
spellings still win where they exist. 180 -> 1. The one left is Kosovo, whose ISO coding is
genuinely contested across datasets, and its row is named "Deaf of Kosovo" so no reader is
misled — one honest code beats starting a hand-written country table.

Verified: the tab renders 3,457 rows with all eight columns and both export buttons, the
country filter isolates Chad and Nepal correctly, and the coverage gate plus the table and
deep-link probes all pass.
2026-09-24 08:08:59 +00:00
doxa-maps-88655c70
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 d154a76 — PosterWall fetches posters/<id>.html and rasterises
it in the browser. The PRINT path did not: it handed an <img> a url it never
checked, so on a default drop the SAME poster downloaded fine and PRINTED as
"Poster not available yet." That is the bad first impression
IDEA-POSTER-EXPORT-404-ON-DROPPED-FOLDER-TRADEOFF called out by name.

Three things were missing; all three are here:

1. printPoster now climbs the same ladder as the download path — probe the webp,
   and only on a miss fetch the .html, rasterise, and verify PIXELS before opening
   the dialog (an SVG in an <img> is an isolated document: a lost stylesheet prints
   a black square with no error). Blob urls are held until unmount, not revoked at
   print time — the preview <img> is still reading from them.

2. publish-html.mjs wrote into public/assets/posters, which is a GENERATED MIRROR
   that every `bun run tree` rmSync's. Its output never survived, which is a large
   part of why the fallback still had no input months after it shipped. Retargeted
   at data/posters, the tracked source the mirror is made from.

3. bundlePosters is one boolean over a directory holding 40.9MB of rasters AND the
   28 sources, so it could only ever ship both or neither — and "neither" is what
   made both full-size paths 404. posterSources:true ships the sources without the
   rasters, the combination that was previously inexpressible. DEFAULT UNCHANGED:
   28 sources are 27.9MB raw (6.9MB packed, measured with pack-objects), real
   weight in a published tree that needs a deployment decision, not a silent one.

VERIFIED AT THE RENDERED LAYER, not by reading source. check:poster-print-fallback
clicks the real control with every posters/*.webp ABORTED at the network layer —
the default-drop condition forced rather than hoped for — and asserts the HUD
enters "generating the poster" and settles clean. Mutation-tested: removing the
fallback produces 4 failures including "no posters/*.html was fetched". The other
branch is verified too — with a webp present it is used directly, zero html
fetches, no rasterise — so the extra probe is paid only on a miss.

★ THE GATE WAS WRONG THREE TIMES FIRST, each time blaming the product for a
harness fault, which is why it now asserts the HUD instead of an internal:
  - it matched /print/i, but the control reads "UPG prayer poster" — the product
    names the ARTEFACT, not the action;
  - it patched Document.prototype in the PARENT realm while openPrintFrame writes
    into the iframe's own;
  - openPrintFrame calls doc.open() AFTER append, and open() RESETS the document,
    discarding a write patched at append time.
Through all three the .webp/.html request counters looked corroborating, because
the wall loads both by itself. The HUD is what a user sees, survives a refactor of
the print internals, and cannot silently miss.

The 28 poster sources are committed because the fallback has no input without
them. city-* posters are deliberately NOT covered: they fetch OSM street tiles at
generation time, so they are not reproducible offline and their rasters must stay.
2026-09-24 08:07:22 +00:00
doxa-maps-88655c70
b771fa622d Make a people group sendable, not just findable
Search made a group findable last tick; nothing made it sendable. A colleague could be
pointed at the map but not at the Fulani group you had just spent five minutes locating —
the Share button emitted the same URL no matter what was open.

It now carries the selection: ?code=<iso639-3>&country=<iso3>&name=<people group>, and the
map reopens that group on load, flying to it.

★ KEYED ON code+country+name, NEVER ON THE PIN INDEX. The index is a position in a
generated table and shifts every time the pins are rebuilt — which has happened eight
times this flight. An index-based link would keep working and silently open a DIFFERENT
people group after the next data refresh. A link that rots into a wrong answer is far
worse than one that breaks, and this map's whole discipline is not handing someone the
resources for the wrong people.

The query rides on ShareButton's embedPath rather than teaching that component about
parameters: it has four other consumers and none of them need this. Zero blast radius.

Verified end to end on the served tree, in a FRESH page rather than the one that made the
link: opening German emits
?code=deu&country=BEL&name=German, and loading that URL reopens German (deu) at zoom 4.2.
NEGATIVE CONTROL: ?code=zzq&country=ZZZ&name=Nobody opens no group at all, so the resolver
discriminates rather than falling back to whatever is first.

The repo's own smoke:share-all passes this bundle — share → reopened <gospel-resources-map>,
canvas:y. It reports one failure elsewhere, my-upg-100-list/upg-100-list "no Share button
found", which predates this flight: commit ca4343c "UPG-100 drops its duplicate share" is
an ancestor of my first commit. Not mine, and left alone.
2026-09-24 08:01:05 +00:00
doxa-maps-88655c70
4034fa9d6a Let someone find the group they came for
12,379 dots and no way to reach a named one. Everything built so far answers "show me the
gap" — the filter, the ranked list, the country view — and none of it answers the question
a person actually arrives with: where is Fulani, where is Uzbek. Finding one meant hunting
the map by eye.

The legend now opens with a search that matches a people-group NAME, its LANGUAGE, or its
ISO 639-3 code, because a user turns up knowing whichever of the three they happen to
know. Results carry the tier dot, so you can see before clicking whether that group has
anything. Tapping one flies there and opens it.

SUBSTRING, NOT FUZZY, and that is the load-bearing choice. A fuzzy match over 12,379 rows
returns plausible WRONG groups, and handing someone the resources for a similarly-named
people is precisely the failure this map is careful about everywhere else — it is why the
language join is an exact ISO code and never a name. A search that quietly suggests the
wrong Fulani is worse than one that finds nothing.

Sorted by population, because "Fulani" matches dozens of groups and the largest is the one
most likely meant. Capped at 8 with a count of the rest, so a broad word does not bury the
legend.

Built inline rather than importing library SearchBar: the shared component injects its own
styles, and whether that lands inside THIS shadow root was unverified — an unstyled search
box would have been a worse outcome than eleven lines of CSS in the palette already here.

Verified on the served tree: "fulani" returns 8 rows led by Adamawa and Sokoto Fulani,
the ISO code "deu" returns German and Austrian, a nonsense string returns 0 — so the
search discriminates rather than always matching — and clicking Uzbek moves the map from
zoom 1.3 to 4.2 and opens Uzbek, 28,470,000 people. Coverage gate, the map gate, and the
pins, filter, gap-list and scope probes all pass.
2026-09-24 07:51:37 +00:00
doxa-maps-88655c70
ff8a318e51 Let the map gate start its own server, so it will actually run
Registering a gate is not enough to make it run. verify:browser-all is invoked NOWHERE in
this repo; build:unlocked runs only `bun run verify`, and verify-all.mjs skips anything
marked browser:true unless --full is passed. That is precisely how two gates rotted here
before — "both had rotted unnoticed precisely because the build chain has never run them".
A gate whose precondition is "remember to start a server first" has a precondition nobody
remembers.

So the precondition is gone. If nothing answers, the gate spawns serve-strict on an
ephemeral port — ephemeral so two gates running at once cannot collide on 4173 — and shuts
it down afterwards. `bun run verify:gospel-map` now works from a clean shell with no setup.

An explicitly named BASE_URL or argv is honoured and never auto-served: a caller naming a
host means it, and quietly pointing at a different tree would be worse than skipping.
Verified — a dead explicit URL SKIPs and starts nothing.

★ THE FAILURE PATH LEAKED A PROCESS AND THE PASS PATH DID NOT. process.exit(1) inside the
try terminates before the finally block's cleanup runs, so a failing run left its spawned
server alive: measured 0 strays after a pass, 1 after a fail. A gate that leaks only when
it fails leaks exactly when someone is re-running it to see what broke. The exit code is
now set, not thrown, and the process exits after cleanup. Re-measured from a clean
baseline: 0 strays after pass, after fail, and after restore.

One scare worth recording: a stray server DID show up mid-test and it was the orphan from
the pre-fix run, six minutes older than the code being tested. Checking its start time
rather than its existence is what told them apart.
2026-09-24 07:44:26 +00:00
doxa-maps-88655c70
f47ee3dfbe Refresh the gallery's baked SRI after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
The rebuild that follows a rebase re-hashes the bundles, and the gallery bakes
those integrity hashes into its page. A stale hash is the silent failure this
repo has already paid for: the browser refuses the script and the page mounts
nothing, with no error a reader would see.
2026-09-24 07:43:31 +00:00
doxa-maps-88655c70
d06bf88195 Stop committing 80.7 MB of generated mirrors of data/
public/assets/<name> is not a source. generate-tree.mjs:1353 rmSync's it and
copies data/<name> over it on EVERY `bun run tree`, so five directories were a
byte-identical copy of a tracked source and BOTH copies were committed:

  posters        378 files  40.9 MB
  people-groups    2 files  23.0 MB
  tables          19 files  14.2 MB
  legacy           4 files   2.0 MB
  vendor           3 files   0.6 MB

Verified identical, not assumed: same file set and matching hashes in all five.
And proven regenerable rather than argued — deleting public/assets/posters and
running `bun run tree` restored all 378 files with laos.webp md5 ea65d2f1, the
same hash the committed copy carried.

This is the Driver's "80mb of images in a public folder" almost exactly, and it
is the cheapest possible version of it: a publish was already rejected once at a
74 MB asset tree, and half that weight was a duplicate of something already in
the same repo. No feature changes, no size tradeoff, nothing regenerates
differently.

★ previews/ IS DELIBERATELY STILL TRACKED. It has no data/ source — make-previews.py
derives it from the full posters — and it is what the poster WALL renders from.
bundlePosters defaults false, so a dropped folder ships previews and NOT posters;
untracking previews would blank every wall. Treating "public/assets" as one
homogeneous pile would have removed the one directory that must stay.

The root .gitignore said "what actually ships is the exported subset in
public/assets/ (see export-public.mjs)" and BOTH HALVES were stale — which is why
the duplication looked intentional. export-public.mjs writes a SIBLING OF THE REPO
(../../../../upg-100-posters or $DOXA_PUBLIC), stating in its own header that this
is "NOT inside public/, because capacitor packages public/ whole"; and
public/assets/ is now generate-tree's mirror of data/. Comment corrected in the
same change, because a stale rationale is what restores a deleted duplicate.

Build green with the mirrors untracked: 7/7 pre, 23/23 bundles, 21/21 post.
2026-09-24 07:39:51 +00:00
doxa-maps-88655c70
3d3586b5f9 Commit the map check, so the browser path survives this flight
Every browser check written for this bundle lived in _qa-cycle/, which is gitignored: a
dozen probes proving the pins render, the sheet opens with real links and the deep link
filters the table — all of which disappear with the working tree. check:coverage-consistent
guards the DATA and cannot see the map at all. The repo has carded orphaned gates twice;
this is the same loss one step earlier, a gate that was never committed.

It asserts, against the SHIPPED tree through the production-parity server: the element
mounts and exposes its state handle (a stale-SRI leaf silently refuses to execute, which
looks identical to a mount failure); every pin in the source is RENDERED, not merely
loaded; a tap opens a group with a NAMED language; the table deep-link carries that
group's own ISO code; and the gap filter leaves only tier 0.

★ IT TOOK THREE TRIES TO MAKE IT WORTH TRUSTING, and each failure is the point.

FIRST it was FLAKY — 1 run in 3. Clause 4 waited a fixed 900ms after clicking the filter,
but setFilter is applied asynchronously, so it queried mid-transition and still saw tier 1
and 2. It now waits for the map's own idle event. 5 of 5 runs clean. A gate that fails a
third of the time on correct code is a gate that gets switched off.

SECOND its doctrine check had a SAMPLE SIZE OF 1. It validated the two URLs of the one
group it happened to open — 1 of 2,685 codes — so a jw.org link planted anywhere else
passed cleanly. MEASURED: the injection did not fail the gate until the scan covered the
whole index. It now checks all 3,550 links, and fails if the index carries none at all,
because a scan over nothing passes vacuously.

THIRD, adversarially tested end to end: with jw.org injected on code `tel` the gate exits
1 naming the publisher; restored, it exits 0; the file's md5 is unchanged either side.

Registered in package.json and in verify-browser-all's GATES array — check:no-orphan-gates
confirms it, and check:quoted-scripts passes. Without a server it SKIPS loudly with the
command to start one, rather than passing quietly.
2026-09-24 07:31:01 +00:00
doxa-maps-88655c70
2392fe0319 Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 07:30:43 +00:00
doxa-maps-88655c70
e5c2c77561 all-people-groups can be shared: its own button, verified by a round-trip
Some checks are pending
build-gate / build (push) Waiting to run
It was published, embeddable and reachable with NO sharing path by any mechanism —
no ShareButton, no shareUrl config, no composite parent overriding one on its
behalf — while every other published map had exactly one. A reader who landed on
it could not send it to anyone, which is the point of an embeddable jamstack map.
check:share-policy's known debt falls from 2 published maps to 1.

The button is in a MapToolbar (pure layout shell: position, z-index, mobile
offsets) rather than hand-positioned, so it sits where every other map's controls
sit. has-geocoder=false because this profile mounts no search pill. Only the share
control is added — zoom/location/fullscreen each need a live mapRef this bundle
does not expose, and the card asks for a sharing path, not a toolbar.

NOT gated on appReady, unlike this bundle's legend. Sharing a map does not require
its data to have loaded, and gating it would remove the affordance exactly when a
slow or failed load makes a reader most likely to want the link.

★ WHY A RENDERED CHECK WAS NEEDED. check:share-policy counts <ShareButton> in
SOURCE. Mutation-tested by gating the toolbar off: the policy gate still read
"1 button / own-button" and stayed GREEN while the button was absent from the DOM.
A source count cannot tell a rendered button from an unrendered one.

So this is verified by the EXISTING share-roundtrip-smoke, not a new gate. I wrote
one, then deleted it on finding that smoke does strictly more — it fetches the URL
and REOPENS it, asserting the same custom element renders, which catches SPA
fallbacks and hash-stripping that a link-text check cannot. A second share checker
would be the fourth-print-path mistake this repo has already paid for once.
Result: share URL resolves 200 text/html and reopens as <all-people-groups> with a
rendered canvas.

i18n verified at the rendered layer: under ?lang= the button title reads Share /
Compartir / مشاركة / साझा करें / 分享, so the new affordance is translated in every
locale, RTL included. buttons.share is one of the keys restored to parity earlier.

ALSO FIXED, found by running the suite: smoke:share-all was testing the LEAF urls
of the two 'header'-policy bundles, whose button lives in the MFE shell by design,
so it reported 10/12 with two failures that were the policy working as intended.
Swapped country-posters/countries-map for country-posters-mfe and dropped the stale
my-upg-100-list leaf (upg-100-mfe was already listed). Now 11/11 pass, 0 fail.
check-share-policy records the coupling so the list cannot drift from the policy
again.

prayer-walk-planner stays debt-no-share, deliberately. It builds a personal walking
route from polygons the reader draws; a link carrying none of that arrives as an
empty planner, and a button that reliably produces a blank map is worse than none
because it looks like sharing works. That is a semantics decision, not an
implementation one.
2026-09-24 07:25:19 +00:00
doxa-maps-88655c70
0e702e5a34 Merge main into bundler-1/work (regenerate the tree, re-hash) 2026-09-24 07:24:05 +00:00
doxa-maps-88655c70
ca13f04bd2 Regions: delete a dead prop, after auditing what the probes can actually prove
The source change is small: countryName was declared on UupgsList and
passed by region-dashboards but never read — residue of the invented
count line removed earlier. Rule 3 prefers deleting a workaround to
keeping one, so both the prop and its call site are gone. Audits for
the two shapes that revealed real gaps earlier — a prop declared and
never used, and state read but never written — now come back clean
across all four components, and every reachable prop their CountryDetail
passes is implemented.

The substantial finding is about the probes themselves, and it lands in
_qa-cycle rather than here. Scanning all 42 for "no ok() calls AND
never exits non-zero" returned 23, including both numbers reported every
tick: region-vs-live's 79/79 and country-vs-live's 32/32 printed
"N IDENTICAL · M differ" and returned success either way, so the
headline parity was read from text rather than enforced.

_probe-standalone-app.mjs had already drifted for exactly that reason:
it printed its table, counted gaps, and exited 0 regardless. Read
properly it was 9 of 10, not the 10/10 reported for several ticks. The
gap was "one h1 on the page: h1 count 2", and measuring resolved it
rather than silencing it — the composite is a scroll column, both panes
are genuinely on the page (both h1s compute hiddenBy null at 1200x75),
so they are two cloned pages in one document. "One h1 per document"
collides with clone parity, since region-vs-live and list-vs-live both
diff an h1 selector; the clone requirement wins and the property now
states "one h1 per pane (scroll column)" with that measurement
attached.

All three now exit non-zero on a real failure, each proven by a
negative test. Two of those tests were wrong first: one swapped a
selector on BOTH sides so they stayed identical, and one read $? after
a pipe, which reports tail's exit rather than node's.

Also made the chooser's pin assertion wait for the condition instead of
a stopwatch. It had read queryRenderedFeatures at a fixed 4200ms and
measured 0 once while consecutive runs gave 1991 and 2107 — a flaky
check, which is worse than a missing one because it teaches you to
ignore a red. Three runs now return 2107 every time.

Regression: 18 suites green, 9 gates green.
2026-09-24 07:22:38 +00:00
doxa-maps-88655c70
02351725ba Gate the shape of last tick's bug, not just the instance
70 story sets were typed as Scripture for seven ticks because NOTHING compared a row's
label to where it points. Clause H, added last tick, catches that exact mislabel. It would
not have caught the same mistake made anywhere else.

Clause I asserts the general shape: Film only on jesusfilm.org or arc.gt, Bible Stories
only on git.door43.org and only under that name, Scripture never on a film host. Audited
across all 3,550 rows first — 0 violations, plus 0 duplicate (language, url) pairs — so
this ships as a guard rather than a fix. The 8 languages carrying more than one Scripture
URL were checked and are correct: Cebuano really does have an eBible ULB alongside Door43's
ULB and UDB, which is three translations, not three copies.

THE PASS LINE NOW NAMES EVERY CLAUSE. It listed four of nine, which reads as a narrower
check than it is, and the next reader calibrates their trust on that line rather than on
the source. Nine clauses, nine names, and --poison still fails 9/9 on demand.

The table also states its scope breakdown where a reader meets it — 855 New Testament, 254
Portions of both Testaments, 136 Full Bible, 14 Old Testament, 13 Portions — with the
reason it is counted from each publication's own book list rather than its subject label.
"How much Scripture exists" is the question under every other number in that table and it
was previously visible only by sorting a column.

Verified: gate passes, self-test 9/9, the table renders its new Scope column, and pins,
scope and deep-link probes all pass.
2026-09-24 07:06:21 +00:00
doxa-maps-88655c70
596b5fe5e8 Rebuild the served tree after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
The rebase conflicted in nine public/ files — build OUTPUT, not source. Neither
side of such a conflict is authoritative, so the resolution is to rebuild rather
than hand-pick hunks: these nine differ from what the manual resolution produced,
which is precisely why the rebuild was required before pushing.

No source file conflicted except package.json, where upstream had added
check:maplibre-drops and this branch check:packets-all. Both are kept, and
gates:post is upstream's 20 steps plus this branch's one addition — not either
side's version of the list.
2026-09-24 06:59:37 +00:00
doxa-maps-88655c70
25405d571a Scope the Door43 Scripture, and stop 70 story sets being typed as Scripture
Door43 entries carry their own book list. Each ingredient is categorised bible-ot or
bible-nt, so a Door43 Scripture entry can say how much Scripture it is, exactly as an
eBible one does — and it is already in the response we fetch, so this costs no extra
requests. 35 entries scoped: 15 New Testament, 13 Portions, 4 Full Bible, 3 Portions of
both Testaments.

THE SUBJECT LABEL IS NOT THE SCOPE, which is why it is counted from the data. An entry
filed under "Bible" can be 27 NT books and nothing else. Calling that a Bible because of
its folder name is the same error as calling Open Bible Stories Scripture.

★ AND THAT ERROR WAS ALREADY SHIPPING, 70 ROWS OF IT, BY MY OWN HAND. Tick 15 added owner
disambiguation so two same-named entries could be told apart — "Open Bible Stories
(OBS-TLF)". The type test was `d.s === 'Open Bible Stories'`, an exact match, so every
disambiguated label fell through to Scripture. 70 story sets have been typed as Scripture
for seven ticks: Bhojpuri, Maithili and 68 more told a reader they had a translation when
they have 50 illustrated narratives. Found only because the type counts moved when this
tick changed the test to a prefix.

An exact match against a label that can grow a suffix is the whole defect, and the fix is
not enough on its own — so clause H now asserts that nothing named "Open Bible Stories" is
typed as Scripture, poisoned with the rest. 8/8 clauses fail on demand.

The Door43 chip in the sheet had the same exact-match and now reads "Bible Stories
(OBS-TLF) · Door43 · <scope>". The one `===` left in the fetcher is correct and was
checked rather than blanket-replaced: it compares the RAW subject from the API, which
never carries an owner suffix.

Every Scripture row on the map now has a scope. The 2,278 blanks reconcile exactly to
2,087 film plus 191 Bible Stories — none of which is a portion of the canon.

Verified: gate passes, its self-test fails 8/8 on demand, and the Door43, pins, table,
scope and deep-link probes all pass.
2026-09-24 06:58:12 +00:00
doxa-maps-88655c70
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.
2026-09-24 06:55:58 +00:00
doxa-maps-88655c70
6d29493e84 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 06:53:50 +00:00
doxa-maps-88655c70
a32097751b Regions: their second filters panel was missing, and it carries two of their bugs
The expanded filters panel was the largest interactive surface never
compared to theirs, and comparing it found a whole panel absent.
Measured on their live country page: TWO .filters__panel blocks and
THREE .filter-toggle buttons — Adopted, Engaged, Exact Search — against
our one panel and zero toggles. The dropdowns already matched exactly,
four of them, with their country dropdown correctly hidden by
lockCountryCode.

A tell I had missed: the search already read active.exact for the Fuse
threshold, so Exact Search's BEHAVIOUR was ported while the control
that sets it never was.

Ported their panel, their three toggles, their toggle semantics (a
second click removes the filter), their activeFilters shape so each
toggle also renders as a chip, and .filter-toggle styled from the
MEASURED computed values of their live button rather than by chasing
tokens. The labels needed no new keys — Adopted and Engaged were
already translated in all eight locales.

Two of their bugs are cloned deliberately, both measured on their live
/regions/india at 855 rows:

- ENGAGED RETURNS ZERO. Their branch filters on u.people_praying, and
  people_praying is not in the field list they request — they fetch
  people_committed. The field is always undefined, so the filter
  matches nothing: their page goes 855 to 0. Reading people_committed
  instead would make us differ, and the ruling is that what exists on
  DX-life must match. The upstream fix is one word in their file.
- THE ACTIVE STATE IS WHITE ON WHITE. Their rule sets background-color
  var(--color-surface-brand-lighter, rgba(0,80,160,0.05)) with color
  var(--color-white), and --color-surface-brand-lighter is not defined
  in their token files. Their active button computes background
  rgb(255,255,255) and color rgb(255,255,255) — an invisible label,
  now visible in our screenshot too. Defining that one token upstream
  fixes both sides.

Both are flagged for the Driver rather than quietly corrected: a bar
chart was already removed under this ruling for the opposite reason,
being in ours and not theirs.

Proof: _probe-filter-toggles.mjs, all assertions pass — two panels,
three toggles, nine measured style properties, Adopted narrowing 24 to
2 and appearing as a chip, a second click restoring, Engaged emptying
to 0 exactly as theirs does, Exact Search changing nothing alone.

Regression: 22 suites green.
2026-09-24 06:53:22 +00:00
doxa-maps-88655c70
91dc9e6b33 Name and scope every Scripture link, including the 24 on an older edition
Last tick left 547 Scripture links with no scope and 24 with no name. Both had a cause
worth finding rather than papering over.

★ THE 547 WERE A PATCH THAT SILENTLY DID NOTHING. Two code paths create an eBible entry.
The edit that added `ss` matched a string containing httpsify(ee.u); the second path reads
`s: ee.u` without it, so the replacement found no match, reported success, and changed
nothing. 523 entries kept a title and a full-Bible flag while losing their scope — a
half-applied change is harder to see than a failed one, because every field you happen to
check is present. That branch now sets every field the other branch sets.
Scope coverage 690 -> 1,213.

THE 24 POINT AT AN OLDER EDITION than the catalogue's current pick — tel2010 where the
catalogue says tel2017, ben2006 where it says benobcv. BOTH RESOLVE, checked 8 of 8 at
200, so the curated choice is a choice and fill-never-overwrite keeps it. What was missing
was only the description, and the language index cannot supply it: it answers "the best
translation for this code", not "what is THIS link". The catalogue is now also indexed BY
URL, 1,545 entries, so a link is described by the edition it actually points to. Telugu
keeps ebible.org/tel2010/ and is now named "Telugu Easy Reading Version — Full Bible".

RESULT: 1,237 of 1,237 Scripture links carry a real translation name and a scope. Zero
rows still read "Scripture in this language". 840 New Testament, 251 Portions of both
Testaments, 132 Full Bible, 14 Old Testament.

The 2,311 blank scopes in the table reconcile exactly — 2,087 film, 121 Bible Stories and
103 Door43 Scripture entries, which are not eBible publications and have no book counts to
scope. Blank there is the honest answer, not a gap.

Verified: the scope line renders on a real group and is ABSENT on a group with no
Scripture, so it still discriminates; coverage gate passes; pins, table and deep-link
probes pass.
2026-09-24 06:47:23 +00:00
doxa-maps-88655c70
2484f26304 facet gate: say "I cannot measure this engine", not "the map is broken"
Some checks are pending
build-gate / build (push) Waiting to run
Run against ?provider=maplibre the gate reported

    FAIL — the map never populated (no rd-bubbles features after 90s)

about a dashboard that visibly renders all 2107 dots with a working
choropleth. It reads the source's live GeoJSON off `_data`, a private Mapbox
internal. MEASURED on one dashboard, one build, only the engine differing,
by calling the app's own setData() to shrink 2107 features to 5:

    mapbox    _data 2107 -> 5      _options.data 2107 -> 2107   (_data is LIVE)
    maplibre  _data never an FC    _options.data 2107 -> 2107   (NEITHER is live)

So on MapLibre no private field tracks setData and the wait can never
succeed. It now diagnoses the engine and SKIPs with the reason. Same
false-FAIL shape removed from check:filter-width last tick: a control that
accuses correct code is worse than no control.

★ DELIBERATELY THE SMALL FIX. Re-expressing the assertion on the public
querySourceFeatures() would change the gate's reported counts, and the two
earlier attempts at that made things worse -- one failed BOTH engines by
preferring the construction-time _options.data, the other called a
node-side helper from inside a page-evaluated string where it is simply
undefined. Every line of page code added here is self-contained for exactly
that reason.

THREE CONTROLS:
  maplibre     -> SKIP, exit 0, naming why it cannot count
  mapbox       -> PASS, exit 0
  --self-test  -> gate REJECTED the reverted build, catching
                  'circle-color is the flat literal "#ef6c00"'
The third is the one that matters: the gate still catches the bug it exists
for rather than having been quietened.

This removes the LIE, not the GAP. MapLibre dashboards remain unverified by
this gate; BUG-FACET-GATE-CANNOT-MEASURE-MAPLIBRE stays open, downgraded to
P3 and re-scoped to the coverage question.
2026-09-24 06:41:38 +00:00
doxa-maps-88655c70
cc350514f3 Say which Scripture it is, and how much of it
Every Scripture row on this map said "Scripture". The catalogue knows the translation's
actual name for 1,213 of the 1,237 languages that have one, and the difference it hides is
the one a mobiliser most needs: "Ahirani Matthew and Mark" is two Gospels, "Bhadrawahi
Bible" is a Bible. The table said "Scripture in this language" on all 1,237 rows and the
sheet showed a bare chip.

The sheet now names the translation and its scope — "German ELBBK Bible — Full Bible" —
and the table carries the real name in its Resource column plus a new Scope column: 101
Full Bible, 461 New Testament, 122 Portions of both Testaments, 6 Old Testament. Scope is
its own column rather than folded into Type, so Type stays a clean three-value filter.

SCOPE DESCRIBES THE TRANSLATION WE ACTUALLY LINK TO. A curated scripture_url pointing at
the same eBible page inherits it; one pointing elsewhere does NOT, because the catalogue's
book counts would then be describing a different publication. That distinction recovered
96 entries and correctly leaves 547 blank rather than guessing.

EBIBLE LINKS WERE THE TICK'S ACTUAL QUESTION and they came back clean: ebible.org/cmn2006/
renders the Mandarin Bible with its full book list in-script, png.bible/aai/ renders
Minaifia NT from Matthew to Revelation. They land where a person can read, which is what
the film links did not do. The scope work came out of reading those pages.

★ THE DEEP-LINK PROBE FAILED ON THIS COMMIT AND THE PRODUCT WAS FINE. It read the ISO code
from cell index 4; inserting the Scope column moved it to 5, so it reported a filter
failure while the filter was returning exactly the right 2 rows. It now finds the column
by its HEADER. A column index is a position, a header is a contract — the same mistake the
tooltip probe made two ticks ago by asserting wording instead of the rule.

Verified: the scope line renders "German ELBBK Bible — Full Bible" on a real group and is
ABSENT on a group with no scope, so it discriminates. Coverage gate passes, its self-test
still fails 7/7 on demand, and pins, table, deep-link, tooltip and sign probes all pass.
2026-09-24 06:40:37 +00:00
doxa-maps-88655c70
ad6787e617 Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 06:34:45 +00:00
doxa-maps-88655c70
0951b91875 Merge main into bundler-2/work 2026-09-24 06:34:31 +00:00
doxa-maps-88655c70
f03b900ccc filter-width: an absent server is not a width regression
The gate printed "FAIL — 2 host(s)" when it had measured NOTHING —
ERR_CONNECTION_REFUSED on :4173, "UNMEASURED" twice, then a failure. That
accuses the product of a regression on the strength of a missing process,
and it already cost one wrong report in this flight: the gate was reported
as failing when it had simply never run.

★ THIS DOES NOT REVERSE THE AUTHOR'S RULE. "An unmeasured host is a failure.
A gate that quietly drops a host reports success about something it never
looked at" is correct for a STRUCTURAL miss — the page loads and the trigger
or tab cannot be found, meaning the gate has been defeated — and that path
is kept, comment and all. Only connection-level errors are separated out.

It is also not a judgement call, because the house already decided: four
sibling browser gates skip on an unreachable host and say so in the same
words — check-fits-the-screen, interop-harness, check-catalog-identity and
check-legend-matches-source all print "SKIP — ... unreachable right now.
Not a product finding." This gate was the outlier.

THREE CONTROLS, ALL FIRED:
  no server                      -> SKIP, exit 0
  server + healthy code          -> PASS, exit 0
  real regression, the original
  flex RANGE put back on
  .filter-dropdown               -> FAIL, exit 1 ("the row grew 369px")
The third is the one that matters: it proves the gate can still catch the
bug it exists for, rather than having been quietened.

Before believing any of those browser measurements I ran `bun run tree` and
confirmed check:sri 0 — a single-bundle build leaves stale SRI, the browser
refuses the script, and the page then measures clean for the wrong reason.
That trap made a negative control vacuous earlier in this flight.
2026-09-24 06:34:29 +00:00
doxa-maps-88655c70
81cb8ad235 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 06:30:46 +00:00
doxa-maps-88655c70
a5628171ea Merge main into bundler-1/work (regenerate the tree, re-hash) 2026-09-24 06:30:29 +00:00
doxa-maps-88655c70
cf33a79c32 Merge origin/main into the studio branch
Some checks failed
build-gate / build (push) Has been cancelled
The two repos carry the same work under different commit hashes, so this
branch had drifted 30 commits behind while looking 8 ahead. Pushing it would
have DELETED 315 lines of newer upstream work, including
check-maplibre-layer-drops.mjs entirely and the current versions of six
library/ files.

Both conflicts were textual, not semantic: package.json's script map is
key-for-key identical to remote's (verified before resolving, not assumed),
and check-packet-render.mjs is byte-identical on both sides — add/add
artifacts of two repos that built the same file in separate histories.

This also retires the divergence finding committed a moment ago. It was
measured against this repo's STALE checkout rather than against main; the
merge brings library/ to byte-identical with wt-packet and
check:poster-prints — which failed with "printPacket is not a function" —
now passes. The durable part of that finding is the absolute-path bug, and
that is already recorded where it matters, in the gates themselves.

check:i18n-parity, check:locale-reachable, check:poster-prints,
check:tabs-localized and check:no-abs-paths all pass on the merged tree.
2026-09-24 06:29:57 +00:00
doxa-maps-88655c70
9dfcf3551b Correct a wrong finding: Scripture Earth's links are not dead, its shelves are empty
Two ticks ago this repo recorded that Scripture Earth was rejected because its JESUS Film
links are dead, on the evidence that the /watch/jesus.html/<slug>.html scheme 404s —
english.html included, so not merely rare languages. That was wrong, and it was published
where the next agent would inherit it.

It 404s TO A SERVER-SIDE FETCH ONLY. Loaded in a browser, jesus.html/english.html and the
Ghotuo link both render "FEATURE FILM JESUS · 128 min FHD", and a bogus slug renders
"404 PAGE NOT FOUND". The scheme is alive. The lesson was already in this file for
?languageId= and I applied it inconsistently: no jesusfilm.org URL can be graded by
fetching, because the site is client-routed — a fetch 404s pages that work and returns a
shell for pages that do not.

THE REJECTION STANDS, for the reason that actually holds. MEASURED on a random sample of
20 of the 730 tier-0 languages with a Scripture Earth directory: 19 are EMPTY — no video
files, mostly no subdirectories at all. A code directory is a roster row, not a resource.
Perhaps ~5% hold something real, recoverable only by crawling all 730, which is now
recorded as a small-return future option rather than a dead end.

This is the difference between rejecting a source and rejecting it for a reason that is
true. The first would have been re-litigated the moment someone opened one of those links
and watched it play.

ALSO VERIFIED, since the tick started there: all 33 arc.gt links we ship resolve to a real
JESUS Film page — every one, not a sample. Arclight returns those shortlinks as the
shareUrl for some languages, notably American, Russian and Iranian Sign Language, which
matters because sign languages are the emptiest part of this map. A bogus keyword answers
with JSON, {"error":"Keyword not found or invalid"}, NOT a 404 page — so the probe's
control had been grading the bogus case as alive and failing for the wrong reason. Fixed,
and the string recorded so the next check knows what a dead arc.gt looks like.
2026-09-24 06:29:52 +00:00
doxa-maps-88655c70
51a5636601 Make the last two gates repo-relative; report the library divergence they expose
check-locale-reachable and check-poster-prints defaulted REPO to a hardcoded
wt-packet path, so like check-i18n-parity before them they graded the OTHER
repo's files and printed PASS for a tree they never opened. These two are the
third and fourth instance of that pattern. Now repo-relative, so
check:no-abs-paths — which DOES gate this repo's build — stays green.

Reading its own tree, check-poster-prints now fails honestly:
usePosterPrints.js here has no printPacket. That is real, and it is NOT fixed
here, because the fix that worked for the locale catalogs would be destructive
for this code. All six diverged library/ files have lines that exist only in
this repo — two-way divergence, not staleness — so a wholesale copy would
delete studio-side work. What made the catalog sync safe was the measurement
that nothing would be lost, not the resemblance.

Recorded in internal/FINDING-library-diverged-from-wt-packet.md with the
per-file line counts and the decision a resolver needs to make. None of these
four gates run in this repo's gates:post, so nothing is newly blocked; they are
runnable by hand and now honest, which beats passing by reading another repo.
2026-09-24 06:27:48 +00:00
doxa-maps-88655c70
52e6f9ca2e Merge main into bundler-1/work (regenerate the tree, re-hash)
Six conflicts, all generated artifacts, resolved by rebuilding the
PosterWall consumers and regenerating the tree.

Main brought two things that land on this branch's work: another
builder made check:i18n-parity's DOXA_REPO default portable, which is
the wt-packet default this branch flagged last tick, so the gate now
measures the repo it lives in; and a new check:tabs-localized proves a
catalog is actually REACHED rather than merely symmetric. That gate
covers SemanticTreeLegend's tabs.json, not this element's SELECT VIEW
strip, so the strip's three strings remain uncovered and untranslated.
2026-09-24 06:26:43 +00:00
doxa-maps-88655c70
e8e2be1498 Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	package.json
#	public/doxa-maps/all-people-groups/all-people-groups.js
#	public/doxa-maps/all-people-groups/all-people-groups/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-24 06:25:29 +00:00
doxa-maps-88655c70
2ddb09b42c Regions: RTL kept three rules on the wrong side, and the tab strip never localised
Every vs-live diff so far ran at LTR, so RTL was the next coverage gap —
and it is where layout actually breaks, because a physical `left`
silently survives a direction flip while a logical
`inset-inline-start` does not. Their CSS uses the logical form; ours had
physical `left` in three direction-sensitive places.

The new probe compares each element's offset from the INLINE-START edge
— the left edge in LTR, the right edge in RTL — which is the one number
that goes wrong when a physical property survives. Measured against
their live /ar/regions/cameroon:

- .search-icon sat 566px from the inline-start edge against their
  19.5px: it had stayed on the physical left, which is the inline END in
  Arabic. Replaced with their rule verbatim (blocks/_form.scss:119 —
  inset-inline-start:1em, top:0/bottom:0, margin:auto 0, 1em square),
  which also fixed a second difference, since our fixed 18px square
  could not track the input's type where theirs is 1em. Ours now
  measures their exact 19.5px.
- The country bullet kept left:-16px, so in RTL it detached on the
  wrong side — computed right 256.938px instead of a -16px overhang.
- The chooser's region legend was pinned to the physical left. That one
  is a sanctioned addition rather than a clone, but a legend stuck on
  the physical left in an RTL layout reads as broken whoever designed
  it.

Reading the Arabic screenshot then showed every heading, strapline and
map note in Arabic and RTL while the SELECT VIEW strip still read
"Regions List" — the one piece of chrome that never localised, because
regions-mfe hardcoded its labels and imported no i18n at all. Now
routed through t(), with the legend going through ViewSelect's existing
prop so the shared component and its three consumers stay untouched.

No catalogue keys were added, deliberately. Their common.json has
"Regions" and "View" but none of the composites, so there is nothing to
copy; translating our own additions is not this agent's call; and
check:i18n-parity tracks a per-locale debt ceiling — it/ro/zh sit at
exactly 94 missing because DX-life translates 8 of our 11 locales — so
English-only keys would push them to 97 and fail it. The shim falls
back to the key, so today's rendering is byte-identical. Proven rather
than assumed: a sentinel injected into ar/regions.json rendered on tab
1 at locale=ar while tab 2 stayed English, then was reverted.

Two of my own assertions were wrong first. The probe set
profile-config.locale on every custom element, which on the composite
means three regions-dashboard elements — host and both panes — and
writing a fresh {locale:'ar'} onto a pane that had inherited its config
by provide() clobbered it, leaving every root LTR; I nearly "fixed" RTL,
which works on all three leaves. And checking inset-inline-start in an
LTR context is meaningless, since it merely mirrors left.

Worth flagging: check:i18n-parity's DOXA_REPO defaults to wt-packet,
another worktree, so it must be pointed at this repo to mean anything.

Regression: 22 suites green.
2026-09-24 06:24:45 +00:00
doxa-maps-88655c70
9056b9329c All 12,379 pins were missing on MapLibre, and now a gate says so
Having found that MapLibre drops a whole layer for one unrecognised paint
property, I swept all 17 isMap leaves under ?provider=maplibre listening to
the map's own error event -- the only place this class surfaces. One more
real instance:

    layers.apg-pins.paint.circle-sort-key: unknown property

circle-sort-key is a LAYOUT property, written into paint. Mapbox tolerated
the misplacement; MapLibre rejected the layer, so every pin on
all-people-groups was absent under the keyless engine while the page looked
perfectly healthy. Moved to `layout`; the sort order itself is unchanged.
Screenshots read on both engines: 12,379 pins, identical.

NEW GATE check:maplibre-drops loads every isMap leaf under maplibre and
fails on the layer/source-dropping error family. It skips cleanly when the
server is unreachable, because an UNMEASURED check is not a FAILED one --
the flaw already filed against check:filter-width.

★ THE NEGATIVE CONTROL WAS VACUOUS TWICE BEFORE IT WAS REAL, and that is
worth more than the fix. The first poison renamed the layout block, which
does not reproduce the bug -- the property simply vanishes, no error. The
second poison was faithful, circle-sort-key back in paint, and the gate
STILL printed PASS. Cause: I rebuilt with `BUNDLE=... vite build` and did
not run `bun run tree`, so the leaf carried stale SRI, the browser REFUSED
the script, no map mounted, and the gate saw zero errors. A check that ran
correctly and measured nothing, reading as green -- the first entry in
AGENT-BRIEF's troubleshooting table, walked into anyway.

After `bun run tree` (check:sri 0) the gate FAILED on the real bug, naming
the exact property, and PASSED again once restored. Both directions seen on
this codebase, which is the only thing that makes its green mean anything.

gates:pre 0, gates:post 17/18 (pre-existing verify/tables-complete),
check:doc-scripts 0, check:no-orphan-gates 0.
2026-09-24 06:24:40 +00:00
doxa-maps-88655c70
b0a267c894 Sync the locale tree forward from wt-packet; stop grading another repo's locales
This repo's check-i18n-parity hardcoded an absolute path to wt-packet, so it
read THAT repo's locales and reported PASS on a tree it never opened. Making it
repo-relative revealed this tree was 103 keys behind in it/ro/zh and short in
every other locale too — including buttons.json:share and shareSteps, the exact
orphan the gate's own header describes it as having been written to catch. The
bug it was built for had reappeared in a second repo, invisible.

The tree is a strict subset of wt-packet's: zero keys exist only here, zero
translations disagree. So this is a lossless forward copy, not a merge. All 11
locales now carry 10 of 11 areas; regions.json in it/ro/zh is the only debt.

Adds check-tabs-localized, which renders the tab labels and asserts they CHANGE
with the locale — key-parity alone cannot distinguish a shipped translation from
a dead catalog.
2026-09-24 06:23:39 +00:00
doxa-maps-88655c70
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.
2026-09-24 06:23:30 +00:00
doxa-maps-88655c70
7b27bf1b83 Every link https, and a gate so it stays that way
1,237 of the URLs we ship were plain http — all of eBible.org, png.bible and alkitab.pw,
with the host spelled `eBible.org` on six of them. Every one of those hosts serves https
directly (sampled 12/12 at 200) and redirects http there anyway, so the plain link bought
nothing but a redirect hop and a downgraded scheme in a field worker's browser, on a page
that may itself be embedded over https.

NORMALISED WHERE A URL ENTERS THE INDEX, not per fetcher. Doing it in the eBible fetcher
first left 120 links behind — the curated scripture_url values, which fill-never-overwrite
preserves and which therefore never pass through a fetcher at all. One httpsify at every
emit point takes it to 3,548 https and 0 http.

Scheme and HOST only. The path is left exactly as published, because a path is
case-sensitive and "tidying" it would break the link rather than improve it.

★ THE GATE CAUGHT MY OWN DRIFT, ten seconds after I added the clause. Clause G failed —
but as clause E, "1237 table URL(s) exist in no pin record": I had rebuilt the pins and
not the table, so the two surfaces disagreed about every eBible link in the corpus. That
is precisely the class this gate was written for last tick, and it found it on its first
real use rather than three ticks later by hand.

Clause G is poisoned in the self-test alongside the rest: 7/7 clauses now fail on demand.

Verified: 0 plain-http URLs remain, six upgraded links sampled live all answer 200 over
https, the coverage gate passes, and pins, table, deep-link, tooltip and list probes all
still pass.
2026-09-24 06:15:58 +00:00
doxa-maps-88655c70
799345cba2 Give the film links a destination: 2,049 real per-language deep links
Last tick established that ?languageId= is ignored by the site — a real id and a bogus id
render the same generic library — so 2,087 groups had a film link that did not take them
to the film. Arclight publishes the missing piece: media-components/1_jf-0-0/languages/<id>
returns a shareUrl, and that IS a deep link — jesusfilm.org/watch/jesus/<language-slug>.

2,082 of 2,087 languages now carry one; 304 existing links were upgraded in place and the
rest were written as deep links from the start. The 5 with no shareUrl keep the old URL
and their tooltip says so, rather than every group getting the same hedge.

FETCHED, NOT DERIVED, and that was the judgement call. The slug looks derivable — "Pashto,
Southern" -> pashto-southern, and 8 of 8 sampled names transformed correctly. Eight is not
2,087, and a wrong slug is not a broken link but a link to SOMEONE ELSE'S language, which
is the worst failure available here. So the API was asked 2,087 times, rate-limited at
250ms, cached and resumable so it is asked once. 5 missing, 0 wrong.

★ AND THIS CHECK CAN FAIL, which none of the previous jesusfilm checks could. A real slug
returns a page carrying the media component id `1_jf-0-0`; a bogus slug returns 0 of them.
Sampled 8 deep links: 8 real, 0 bad, and the bogus control returns 0 hits. In a browser
the Bhilali link renders "FEATURE FILM JESUS · Watch now · 128 min FHD" while the bogus
slug renders "404 PAGE NOT FOUND". That is the first time a jesusfilm URL has been
verified rather than assumed in either direction.

THE TOOLTIP PROBE FAILED ON THIS COMMIT, correctly: it asserted the old caveat wording, so
fixing the links broke the test that described the defect. It now asserts the CONTRACT —
a deep link must NOT carry the caveat and a legacy link MUST — which survives the next
change to the words.

SOURCES-CHECKED-AND-REJECTED.md records the shareUrl endpoint and the `1_jf-0-0`
discriminator, so the next agent inherits the answer instead of the dead end.

Verified: coverage gate, pins, table and deep-link probes all pass; the film chip on
German renders the real deep link with the accurate tooltip.
2026-09-24 06:09:11 +00:00
doxa-maps-88655c70
66c4d3163b Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
Six conflicts, all generated artifacts under public/, resolved by
rebuilding the three PosterWall consumers and regenerating the tree
rather than hand-merging built files.

Main brought the i18n catalogues to parity, and another builder has
translated this branch's three poster-dropdown strings into all seven
non-English locales — so the 'en only' caveat on that feature is now
closed. Main's two new gates, check:i18n-parity and
check:locale-reachable, both pass against the merged catalogue.
2026-09-24 06:01:41 +00:00
doxa-maps-88655c70
0fffb90cca test(i18n): ratchet down again — seven locales must now stay complete
Some checks are pending
build-gate / build (push) Waiting to run
344 -> 282 keys. de and hi reached zero and join ar/es/fr/pt/ru as entries
DELETED from the DEBT map: the gate treats an absent locale as "must be
complete", so removal is what makes a finished translation permanent.

The three that remain (it, ro, zh) are each missing WHOLE catalog files rather
than stray keys, which is worth stating in the file — it tells the next person
these are real translation work, not the propagation gaps the last two ticks
closed.

Confirmed the guarantee before relying on it: deleting one Hindi tab key reports
"hi is missing 1/176 key(s) and has no DEBT entry" rather than passing.
2026-09-24 05:58:55 +00:00
doxa-maps-88655c70
10f83a943b Regions: links we render as buttons had lost their anchor styling
Found by a COVERAGE AUDIT, not a screenshot. region-vs-live diffs
/regions/africa and country-vs-live diffs only UupgsList's nine
elements, so the country page's head and the region card had no
property diff at all. Adding both produced four defects sharing one
root cause: DX-life renders these as <a>, we render them as <button>
because this element has no router, and a button inherits neither the
UA underline nor their `a` rule (_globals.scss:132 — color
var(--color-brand-light), font-weight medium,
text-decoration-thickness 2px, text-underline-offset 4px).

- The dashboard back-link was #4e594f with no underline against their
  #cbc5b9 underlined. Their .light-link recolours to
  --color-brand-secondary = $color-beige-300 = #cbc5b9, exactly the
  measured rgb(203, 197, 185). Wrong on BOTH our region and country
  pages.
- The region-card link was font-weight 400 against their 500.
- Its icon spacing came from gap:10px where theirs comes from
  .with-icon .icon.right { margin-inline-start: 1rem } — the same 10px
  on screen, but their rule would never have applied to our markup, so
  the mechanism is now theirs.
- The country entries were rgb(42, 51, 42), our body ink, against
  their rgb(78, 89, 79) = $color-green-400 = --color-brand-light.
  Their country links carry no class at all — they simply take the `a`
  rule, which is why '.country-link' matched nothing on their side and
  the element read as unmeasured until the selector became
  '.countries-grid a'.

Coverage added so none of this can come back: a new
_probe-country-head-vs-live.mjs at 29/29 (back-link, h1, strapline, map
note, search-box container); the back-link added to region-vs-live,
which went 73 to 79 IDENTICAL; the search-box CONTAINER added to
country-vs-live, 28 to 32, so the 50ch cap cannot regress; the region
card added to list-vs-live at 30/30.

Three of my own assertions were wrong before the code was: I omitted
fontWeight and the underline geometry from the back-link spec while
their `a` sets all three; I compared `gap` on an element where theirs
never sets it; and my sub-pixel tolerance used Number('246.938px'),
which is NaN, so NaN <= 0.5 was false and it silently never fired.

Regression: 22 suites green, 9 gates green.
2026-09-24 05:58:43 +00:00
doxa-maps-88655c70
68963c5e1e test(i18n): ratchet down again — seven locales must now stay complete
344 -> 282 keys. de and hi reached zero and join ar/es/fr/pt/ru as entries
DELETED from the DEBT map: the gate treats an absent locale as "must be
complete", so removal is what makes a finished translation permanent.

The three that remain (it, ro, zh) are each missing WHOLE catalog files rather
than stray keys, which is worth stating in the file — it tells the next person
these are real translation work, not the propagation gaps the last two ticks
closed.

Confirmed the guarantee before relying on it: deleting one Hindi tab key reports
"hi is missing 1/176 key(s) and has no DEBT entry" rather than passing.
2026-09-24 05:58:25 +00:00
doxa-maps-88655c70
7580274ed4 feat(i18n): German and Hindi reach complete — 7 of 11 locales done
Some checks are pending
build-gate / build (push) Waiting to run
Continues paying the debt the parity gate measured, cheapest first.

German and Hindi carried an IDENTICAL 22-key gap — the same legend and search
keys, added to English after those two catalogs were last touched. One batch of
work closed German entirely and took Hindi to 18, which was its whole missing
tabs.json. Both are now complete: 344 -> 282 keys.

Terminology follows each catalog's OWN existing vocabulary rather than inventing
a second one for the same concepts — de keeps Volk/Sprachfamilien, hi keeps
जनसमूह/भाषा परिवार. Tab labels stay SHORT because they sit in a narrow rail:
Spanish already uses "Pueblos" rather than a literal rendering, so Hindi does
the same. UUPG, WAGF and DOXA remain acronyms, as in every locale.

Seven locales (ar es fr pt ru de hi) are now DELETED from the gate's DEBT map
rather than set to zero — an absent entry means "must be complete", so deletion
is what converts finished work into a permanent guarantee. Verified: removing
one Hindi tab key reports "hi is missing 1/176 key(s) and has no DEBT entry".

Verified at the RENDERED layer: "View country posters" comes back as
"Länderposter ansehen" (de) and "देश के पोस्टर देखें" (hi), zero page errors —
which also confirms Devanagari shapes correctly in the APP, not only in the
prayer packet where the font was first vendored.

The three remaining (it, ro, zh at 94 each) are missing WHOLE FILES —
regions.json and tabs.json — so they are real translation work rather than the
propagation gaps closed here.
2026-09-24 05:58:16 +00:00
doxa-maps-88655c70
b4902a7fed test(i18n): ratchet down — five locales now must stay complete
Some checks are pending
build-gate / build (push) Waiting to run
The ceilings drop as the debt is paid: 429 -> 344 keys, and ar/es/fr/pt/ru
reached zero.

Those five are REMOVED from the DEBT map rather than set to 0. The gate already
treats a locale with no entry as "must be complete", so deletion is what
converts a finished translation into a permanent guarantee — setting 0 would
read the same but leaves a line inviting someone to raise it.

Confirmed the guarantee works before relying on it: deleting one Spanish key
reports "es is missing 1/176 key(s) and has no DEBT entry" rather than passing.
2026-09-24 05:53:31 +00:00
doxa-maps-88655c70
8cef804bae test(i18n): ratchet down — five locales now must stay complete
The ceilings drop as the debt is paid: 429 -> 344 keys, and ar/es/fr/pt/ru
reached zero.

Those five are REMOVED from the DEBT map rather than set to 0. The gate already
treats a locale with no entry as "must be complete", so deletion is what
converts a finished translation into a permanent guarantee — setting 0 would
read the same but leaves a line inviting someone to raise it.

Confirmed the guarantee works before relying on it: deleting one Spanish key
reports "es is missing 1/176 key(s) and has no DEBT entry" rather than passing.
2026-09-24 05:53:03 +00:00
doxa-maps-88655c70
a487e48cbf feat(i18n): five locales reach complete — popup.json and six orphaned keys
Some checks are pending
build-gate / build (push) Waiting to run
Working the 429-key debt the parity gate surfaced last tick, cheapest first.

popup.json existed ONLY in English and French — the country popup's four labels
(WAGF Region, WAGF Bloc, Country, UUPG Count), which is what a reader sees when
they click a country on the map. Now in all 11. WAGF and UUPG stay as acronyms
in every language, following what French already did: they are proper nouns of
the dataset, not words to translate.

Then six keys that were missing from EVERY non-English locale identically —
added to English at some point and never propagated: legend header.default,
search sections.within/allData, and the three country-poster strings in
regions.json. The {selection} placeholder is preserved.

Result: ar, es, fr, pt and ru are now COMPLETE, and 429 -> 344 keys of debt.
Those five are deliberately REMOVED from the gate's DEBT map rather than set to
zero — a locale with no entry must be complete, so none of them can silently
regress. Verified: deleting one Spanish key reports "es is missing 1/176 key(s)
and has no DEBT entry".

Verified at the RENDERED layer, not by counting files: "View country posters"
comes back as "Ver los carteles del país" under ?lang=es and "Показать плакаты
страны" under ?lang=ru, zero page errors.

★ One key I translated turned out to be DEAD: legend.header.default is consumed
by nothing — my first probe appeared to test it and was actually matching the
substring "Legend" inside this profile's hardcoded LEGEND_TITLE ("My UPG 100
List — Legend"). A probe that matches an artifact proves nothing; the assertion
has to be a string only the translated key can produce.
2026-09-24 05:52:53 +00:00
doxa-maps-88655c70
ce750f5b55 test(i18n): a key in English must exist in every locale — ratcheted
Some checks are pending
build-gate / build (push) Waiting to run
The failure here is silent BY DESIGN. vue-i18n falls back to English for a
missing key, which is the right runtime behaviour and exactly why catalogs drift
unnoticed: the app looks fine, nothing errors, and a reader who chose Spanish
just gets English words with no signal anything is missing.

Measured when this was written: buttons.json had 21 keys in English and 19 in
every other locale. The orphans were `share` and `shareSteps` — the Share
button's title and its explainer, rendered on EVERY map. Ten locales had been
shipping an English Share button since those keys landed.

★ IT FOUND MUCH MORE THAN THE BUG THAT PROMPTED IT: 429 missing keys across ten
locales, with popup.json existing only in English and it/ro/zh also lacking
regions.json and tabs.json entirely — 101 of 176 keys each.

So it RATCHETS rather than blocks. Each locale has a ceiling; the gate fails
when one gets WORSE and prints a nudge when one gets better so the ceiling can
be lowered. Failing outright on 429 keys of debt this gate did not create would
block every build, and a gate everyone disables protects nothing. A locale with
no ceiling entry must be complete, so a NEW locale cannot be added half-done.

Keys are compared FLATTENED, so a nested object or an array is checked
key-by-key rather than counted as one present key.

Confirmed to fail before acceptance: deleting two French keys reports
"fr regressed: 8 missing key(s), was 6" and names them.
2026-09-24 05:46:13 +00:00
doxa-maps-88655c70
e69c76f5d4 chore(i18n): regenerate the tree after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
Generated output only — three bundles rebuilt and the tree regenerated so the
SRI hashes match the shipped scripts. Re-verified rather than assumed:
check:i18n-parity and check:deck-click both pass afterwards.
2026-09-24 05:45:57 +00:00
doxa-maps-88655c70
984f86d10b Say that the film link is not language-specific, because it is not
A tick spent on Scripture Earth ended by turning the check on our own links, and they do
not do what the chip implies.

MEASURED IN A BROWSER: https://www.jesusfilm.org/watch/?languageId=<id> renders the same
generic library page for id 529, for id 1304, and for id 999999999. languageId is a query
parameter on a client-rendered page and the current site ignores it. The film is real —
the JESUS Film Project's own API confirms a feature film in that language — but the URL
does not carry you to it, and the tooltip said nothing about that. A field worker follows
the link and has to find the language themselves. The chip now says so.

★ THE URL CANNOT BE VERIFIED BY FETCHING, IN EITHER DIRECTION, and both wrong answers are
available. The server returns a byte-identical 867,227-byte shell for every id: grading by
STATUS reports ALIVE for everything, including a bogus id; grading by CONTENT reports DEAD
for everything, because "WatchNotFound" lives in an i18n bundle present on every page. I
ran the content check first and it returned 10 of 10 of our own links DEAD — Fon and
Southern Pashto among them, which is not believable, and that implausibility is the only
thing that caught it. Reported as-is it would have ripped out 2,087 correct links.

No constructible scheme works either: the legacy /watch/jesus.html/<slug>.html form is
retired (english.html 404s, not just rare languages) and /watch/<slug>.html/videos — a
shape the live page itself renders — also 404s to a fetch, resolving client-side. The
Arclight API is the only thing that discriminates, and it stays the basis for the claim.

SCRIPTURE EARTH IS REJECTED, with the reasoning kept next to the code so the next agent
does not re-walk it. Its /data/ index looks like the largest win available — 3,477 ISO
639-3 directories, 730 of them languages we currently show as "nothing found" — and it is
not coverage: aab/video and aac/video are EMPTY, its JESUS Film links use the retired
scheme, and it files Ghotuo under `aaa` where the publisher files it under `ikh`. Shipping
from it would have added ~730 dead links to the groups that most need real ones.

SOURCES-CHECKED-AND-REJECTED.md records that, plus what is credential-blocked and why,
plus that sign.bible is a parked domain answering 200. It sits beside the bundle rather
than in _qa-cycle, which is gitignored — a note the next agent cannot read is not a note.

Verified: the film chip's title renders the honest text on a real group (German), and the
pins, country and table probes still pass.
2026-09-24 05:44:55 +00:00
doxa-maps-88655c70
7fd1c6bcd1 feat(i18n): the poster wall's own labels now translate
Last tick made the 11 locales selectable. That immediately exposed what was
sitting behind the door: the wall's labels — Poster Map, Prayer List, Card Deck,
Print Options, Show on map, Close — were hardcoded English, so a reader who
chose Spanish got Spanish page furniture around English buttons.

All six are now keys in buttons.json, translated across all 11 locales, plus the
aria-label which interpolates the item name rather than concatenating it.

★ AND IT CLOSED TWO OLDER ORPHANS. buttons.json had 21 keys in English and 19
everywhere else: `share` and `shareSteps` — the Share button's title and its
five-step explainer, rendered on EVERY map. Ten locales had been shipping an
English Share button since those keys were added, invisibly, because vue-i18n
falls back to English rather than erroring. Both are translated now.

Verified in a real browser across four languages: the wall's buttons read
"Lista de oración | Mapa del cartel" under ?lang=es, "Liste de prière | Carte de
l'affiche" under ?lang=fr, and the Arabic text renders with dir=rtl — zero page
errors in each. The Share button's title follows too: Compartir, Поделиться.

The wall's `t` and `locale` come from the SAME useI18n instance, so a label and
a printed card deck can never disagree about which language the reader chose.
2026-09-24 05:44:06 +00:00
doxa-maps-88655c70
986b83ee77 test(i18n): a key in English must exist in every locale — ratcheted
The failure here is silent BY DESIGN. vue-i18n falls back to English for a
missing key, which is the right runtime behaviour and exactly why catalogs drift
unnoticed: the app looks fine, nothing errors, and a reader who chose Spanish
just gets English words with no signal anything is missing.

Measured when this was written: buttons.json had 21 keys in English and 19 in
every other locale. The orphans were `share` and `shareSteps` — the Share
button's title and its explainer, rendered on EVERY map. Ten locales had been
shipping an English Share button since those keys landed.

★ IT FOUND MUCH MORE THAN THE BUG THAT PROMPTED IT: 429 missing keys across ten
locales, with popup.json existing only in English and it/ro/zh also lacking
regions.json and tabs.json entirely — 101 of 176 keys each.

So it RATCHETS rather than blocks. Each locale has a ceiling; the gate fails
when one gets WORSE and prints a nudge when one gets better so the ceiling can
be lowered. Failing outright on 429 keys of debt this gate did not create would
block every build, and a gate everyone disables protects nothing. A locale with
no ceiling entry must be complete, so a NEW locale cannot be added half-done.

Keys are compared FLATTENED, so a nested object or an array is checked
key-by-key rather than counted as one present key.

Confirmed to fail before acceptance: deleting two French keys reports
"fr regressed: 8 missing key(s), was 6" and names them.
2026-09-24 05:43:32 +00:00