Commit graph

314 commits

Author SHA1 Message Date
doxa-maps-88655c70
b089f9effb Reach Global Recordings Network, and lift 147 more groups
GRN records the gospel as AUDIO for languages with no writing, which is
most of what is left in the gap — and every earlier attempt recorded it
as unreachable. globalrecordings.net sits behind a proof-of-work bot
shield that challenges even robots.txt; that is an access control and
is not worked around. Their own delivery site, 5fish.org, is not
shielded, allows what this reads, and resolves by ISO 639-3 directly.

First 180 gap codes swept: 74 hold gospel audio, a 41% hit rate, and
147 people groups that had nothing now have the gospel in a form they
can receive. Tier 0 falls 2,873 to 2,746.

The host NEVER 404s. A nonsense path returns HTTP 200 and a plausible
geo-located page, so reading 200 as existence would have marked every
code on earth as covered. Existence is read from the page title, and a
negative and a positive control run in band on every run — the fetcher
aborts if a nonsense code ever reads as a real language. Their
robots.txt asks for a ten-second crawl delay, so the sweep is
sequential, capped per run and resumable. Do not add concurrency.

Three sign languages are deliberately left out. GRN's catalogue is
audio; 5fish holding a page titled "Pakistan Sign Language" says they
index the language, not that they have signed video. Attaching it told
20 Deaf groups — 11.5 million people — that gospel audio was available
to them. Unverified, and for that audience useless.

Six places across three surfaces still told the reader GRN answers 403
to everything and had never been searched. That is now false, and a
stale caveat misleads exactly as much as a stale number.

Also here, because adding a source is what exposed it: the charts
profile decided "this group has nothing" a second time, by listing the
resource fields by hand. That list went stale every time a source was
added — 7 languages once, 23 when Scripture Earth landed — so the chart
drew groups as having nothing while the map offered them a working
link. It now reads the tier the pin index already computed, and a new
gate refuses any surface that re-derives it. The gate was checked
against the real pre-fix file, not only a synthetic one.
2026-09-24 18:55:28 +00:00
doxa-maps-88655c70
4d79c08fd4 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 17:48:14 +00:00
doxa-maps-88655c70
cc6a5ea901 legend: a phone can now export the packet — it was a prop, not a design call
Some checks are pending
build-gate / build (push) Waiting to run
Last tick I found the export unreachable on mobile and recorded it as
Driver-gated: "WHERE the control goes on a phone is a design call, the sheet has
no titlebar". Both halves of that were wrong, and checking them was this tick.

LegendMobile RENDERS SemanticTreeLegend itself — titlebar and all — and the 0x0
elements my measurement found belonged to the OTHER, hidden desktop instance
mounted below the breakpoint. That geometry made it look as though the sheet had
nowhere to put the control. A Driver decision was never needed.

Two gaps, not one.

The sheet passed neither :export-enabled nor @export, and exportEnabled defaults
to false, so the control's v-if was never satisfied. Fixed via provide/inject,
matching onSemanticTreeSelect immediately above it, because the sheet renders
several levels down where an emit would not reach. Opt-in by construction:
enabled only when a profile provides a handler. The other three LegendMobile
consumers provide none and are verified untouched.

Even then the export did nothing, and that second gap is the one that mattered.
The profile passed :nodes but no :tabs, so hideTabs was true and the sheet showed
a single flat Region tree with no tab strip — no UPG-100 Lists tab, no selectable
zone, and onLegendExport bails on `if (!zoneId) return`. Passing :tabs fixed it.
A :default-tab attribute was added and then removed: defaultTab is not a prop on
LegendMobile, so it would have been inert.

Verified end to end at 390x844: the tab strip appears, a real zone selects,
Export opens, Packet clicks, and one print frame is produced. A packet on a
phone. The gate written last tick — which failed on mobile by design — now
passes at both widths. The newly visible mobile tabs are fully localized in
en/es/zh, so catalog work from earlier ticks now reaches a surface that never
showed tabs at all.

Four of my own probe errors were caught by measuring rather than concluding.
Clicking a REGION row left selectedKey empty, so the handler correctly bailed
and it read as "export does nothing" — the same zero on DESKTOP is what proved
it was the probe and not a regression.

country-posters shows the same mobile gap on its own surface. Not taken; its
owner's call.
2026-09-24 17:47:26 +00:00
doxa-maps-88655c70
bea4a4ff2d Rename the app-shell to Research Gallery
"DX-Maps SPA" named the technology; "Research Gallery" names what the
thing is to someone opening it — the Big Picture plus every research
map behind one vertical tab rail.

Display labels only: the profile folder, bundle name and custom-element
tag are untouched, since changing those breaks every existing embed.
The in-shell header moves with the other two — it is the most visible
label of the three, and leaving it reading "DX-Maps" would have made
the rename look half-finished to the only people who see it.
2026-09-24 17:46:35 +00:00
doxa-maps-88655c70
e1df0d5a7c Add Scripture Earth, and lift 28 groups off "nothing found"
SIL and its translation partners publish Scripture for small languages
the big catalogues skip, which is exactly the population left in the
gap: 2,901 groups had nothing in eBible, Arclight, Door43 or FCBH.
921 codes now carry a Scripture Earth link and 28 groups that had
nothing at all — 339,550 people — have Scripture in their own language
for the first time on this map. Tier 0 falls 2,901 to 2,873.

Most of this source is not what it appears to be, and the fetcher
throws away both false positives rather than shipping them. Their open
index lists 3,477 language directories, but 764 of the ones we can join
are empty stubs: trusting the listing alone would have handed 716
people groups a link to an empty folder. And a "video" directory holds
JESUSFilm-<iso>.txt metadata, not playable film — 1,588 codes have
nothing else, so video alone never qualifies a code. Only PDF, audio,
Scripture App Builder, study notes and SFM text count.

All 921 URLs verified HTTP 200 before shipping, and the artefacts were
opened by hand first: Baka's is 14.4 MB of application/pdf, Foodo's
40.7 MB, and a nonexistent sibling 404s, so a 200 here means a file.

The coverage gate, the verifier and the links table each had to be
taught the new field. Leaving any one of them ignorant would have been
worse than not adding the source: the gate caught all 28 tier changes
as inconsistent, and the links table filed 921 rows under the "other"
publisher — the fallback that exists to make an unscreened domain
impossible to hide. The sheet's "checked" line now names the pool too,
since it is searched.
2026-09-24 17:46:27 +00:00
doxa-maps-88655c70
1cfb0578d9 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/gospel-resources/charts/index.html
#	public/doxa-maps/gospel-resources/data-tables/index.html
#	public/doxa-maps/gospel-resources/gospel-dashboard/index.html
#	public/doxa-maps/gospel-resources/gospel-resources.js
#	public/doxa-maps/gospel-resources/resources-list/index.html
#	public/doxa-maps/gospel-resources/resources-map/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-24 17:04:21 +00:00
doxa-maps-88655c70
4f5136211c Show the groups with nothing found and nobody praying
213 people groups have no Scripture, audio, film, Bible video or Door43
resource in any pool checked AND zero people praying on doxa.life's
counter — 4.67M people, largest Songhai (BFA, 259,000), Kuka (TCD,
250,000), Kimr (SDN, 221,000). That intersection is the sharpest thing
in the data and nothing surfaced it.

The gap sheet gains a fourth tab whose rows are links straight to each
group's prayer page, so the answer to "nobody is praying" is one tap,
not a search. The sheet says plainly that "nobody praying" means zero
on doxa.life's counter, not a claim nobody in the world prays for them.

Two fixes found while proving it: the tab is built from the lazily
fetched prayer index, so opening the gap list now loads that index —
without it the tab only ever appeared if you had clicked a pin first.
And the new branch opened its own v-if chain, which left the old
"largest groups" list rendering underneath it, 80 rows where 40 belong.
2026-09-24 16:54:14 +00:00
doxa-maps-88655c70
a53953423b Merge main and regenerate the served tree
Some checks are pending
build-gate / build (push) Waiting to run
public/doxa-maps/index.html conflicted; generated, so resolved by regenerating rather than hand-merging. fix:sri reports all 113 hashes already in agreement.
2026-09-24 16:46:46 +00:00
doxa-maps-88655c70
be33bac508 Carry the region colours onto the landmasses, in the chooser only
The pins already paint by region; the polygons underneath were flat brand
grey. They now take the same seven hues, so the chooser reads as seven
regions at a glance instead of one grey world with coloured dots.

The shared expression could not be reused as-is: it matches on a region
property that belongs to our own geojson, while the boundary tileset's
features carry iso_3166_1_alpha_3 and know nothing about WAGF regions. So
this bridges country code to region to the same palette — no second palette,
which is what the one-palette gate exists to prevent. Their fill is 0.12,
near-invisible, which is right for a single highlighted country; the chooser
is ours and carries 0.42 so the colours read. Cloned pages keep 0.12 and the
flat fill exactly, asserted both ways.
2026-09-24 16:44:45 +00:00
doxa-maps-88655c70
ee4b7ed4e9 Vendor Figtree in place of Brandon Grotesque, chosen by measurement
Brandon Grotesque is commercial and must not ship in a public giveaway, so
the two rules that named it now ask for Figtree. It is vendored the way the
other three families are — woff2 per subset under public/assets/fonts, an
OFL licence beside it, registered at document level because @font-face is
document-scoped.

The choice is measured, not a matter of taste. Their live page really does
load Brandon_Grotesque_medium.woff2, and Brandon sets "FULL PROFILE" at
128.00px at 500/19.5312px. Nineteen libre grotesques were measured the same
way: Figtree is 126, Nunito Sans 130, Barlow 125, Inter 134, Archivo 135,
Poppins 119, Montserrat 148. Figtree is the closest at two pixels.

Worth recording because the old scorecard has it backwards: 134.33 is the
generic sans fallback, not Brandon. The number to match was 128 all along.
2026-09-24 16:44:44 +00:00
doxa-maps-88655c70
7fd8e4545c Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 16:33:20 +00:00
doxa-maps-88655c70
9bc2b5ea48 Gospel resources: MapLibre was dropping every pin on the map
check:maplibre-drops went red on gospel-resources/resources-map and the new
gospel-dashboard leaf: layers.gr-pins.paint.circle-sort-key: unknown property.

circle-sort-key is a LAYOUT property. Mapbox tolerates it in `paint`; MapLibre
rejects the whole layer for one misplaced property and does NOT throw — so under
the keyless engine all 12,379 dots were absent while the page still rendered its
basemap, its choropleth and its legend, and looked fine. That is the same defect
that cost all-people-groups its pins, so the fix is the same: move the key into
`layout` and leave the z-order comment with it, since the reason (a group with
nothing found must win the z-fight) is about the sort, not about paint.

MEASURED after, both engines: gr-pins present, circle-sort-key in layout, pins
drawn. Screenshot read on maplibre — dots and choropleth both there.
check:maplibre-drops PASS 17/17 (it was 15 ok / 2 dropping).

The gate earned its keep here: this arrived with incoming work, not with mine,
and nothing in the page's own appearance would have told anyone.
2026-09-24 16:33:11 +00:00
doxa-maps-88655c70
a9b717c246 Merge main and regenerate the served tree
Some checks are pending
build-gate / build (push) Waiting to run
public/doxa-maps/index.html conflicted again; it is generated, so it was resolved by regenerating the pipeline rather than hand-merging. fix:sri reports all 113 hashes already in agreement.
2026-09-24 16:24:42 +00:00
doxa-maps-88655c70
487e3c9aa2 Give the regions map the shared scroll-past edge strips
The Driver asked for the affordance every other map here already has. The
flag alone was not enough: useEdgeScrollAffordance does not create the strips,
it scans the wrapper for them and returns a no-op when it finds none, so
flipping showEdgeBorders left the page with zero strips. The markup and its
CSS are per-consumer in all five existing users; only the behaviour is
shared. Both are ported from upg-100-list rather than written fresh —
including touch-action:pan-y, which is the mechanism that lets a vertical
drag scroll the page while Mapbox keeps the centre.

The reason the flag was originally false is kept in the file rather than
deleted, because it is the thing to re-examine if the result looks wrong:
this map sits inside a scrolling dashboard, where the gutters can read as
page chrome rather than map chrome. The reference maps own their viewport.
2026-09-24 16:23:11 +00:00
doxa-maps-88655c70
22712eb43a The map said "someone still needs prayer" and gave you no way to pray
Its toast has always read "Tap a coloured dot — someone still needs prayer". Tapping one
showed Scripture, film, audio, video, stories, tracts and training, and no way to pray.
doxa.life publishes a page per people group and sibling plugins in this repo already link
to it; this map simply never did. 1,723 groups now carry a "Pray for <group>" action with
the number already praying beside it.

THIS IS THE ONE JOIN HERE THAT IS NOT BY LANGUAGE, and it needed justifying rather than
assuming. Every other source keys on ISO 639-3 because a translation serves everyone who
speaks the language; prayer does not work that way — you pray for a people, in a place. So
this keys on (group name, country), which is a NAME join, the thing this map refuses
everywhere else because names collide.

Allowed only because the key was tested first: on the API side (name, country) is
perfectly unique — 2,107 entries, 2,107 distinct keys, 0 collisions — and on the pin side
12,379 pins give 12,369 distinct keys. The ambiguous ones are DROPPED, not guessed. One
entry was lost that way, and 383 more have no pin here at all.

pray.doxa.life is the fourth host in this bundle that answers 200 with identical bytes for
any URL: /abai-sungai and /zzz-not-a-real-group are both 3,903 bytes. Rendered it does
discriminate — a real slug titles the page "Abai Sungai - App" with 1,863 characters, a
bogus one titles it "Doxa.Life" with zero — but existence comes from the API listing,
which is the publisher's own statement and costs one request rather than 1,723 renders.

The lookup is COMPUTED, not baked into groupAt. groupAt runs the instant a pin is clicked,
so a value read there is whatever had arrived by then and never updates when the lazy
fetch lands — the same shape as the link-verification state that used to hang off
openPin's country branch.

Verified with a negative control: a group in the index renders an anchor whose href
matches the expected slug URL exactly, and German, which is not in it, renders nothing.
My probe reported FAIL twice against correct code by waiting a guessed 900ms; it now waits
for the data. Screenshot read — the Lati, 2,900 people with nothing found in any pool, can
now be prayed for.
2026-09-24 16:06:01 +00:00
doxa-maps-88655c70
f89e5eb370 The bridge table was recommending bridges on stale resource flags
languages-by-country is the bridge view: one row per country and language, ranked by
prevalence, flagging which languages could carry a resource to groups that lack one. Its
curated work — prevalence_rank, bridge_candidate, groups_speaking — is the reason it
exists and is untouched.

Its resource flags were not curated, and they had drifted like the other two legacy
tables: 75 rows wrong about Scripture, 39 wrong about whether the language carries
anything at all, 1 about film. That matters more here than elsewhere, because this table
is consulted to CHOOSE a bridge: a language flagged as carrying nothing when it carries
audio Scripture is a bridge somebody was told not to use.

Refreshed from the pin index, which is the same source the map draws. 0 of 613 rows now
disagree, and all 613 still carry their curated columns.

full_bible is deliberately NOT refreshed. The pin index knows a scope only for eBible
rows, so downgrading a curated "full Bible" on that basis would lose information rather
than correct it.

Also recorded rather than acted on: this table covers 175 countries while the map has
242 with pins, so 67 countries and 1,537 people groups have no bridge analysis at all.
That is a gap in curated coverage, not a contradiction — the rows that exist are now
correct — and filling it means doing the prevalence work, not copying numbers across.
2026-09-24 15:55:58 +00:00
doxa-maps-88655c70
ffa49709ac The gap by country is now a table you can export, not just a sheet you can read
gospel-by-country carries 242 countries with real per-country counts, and the map uses
them for the choropleth and the "where the gap concentrates" sheet. The tables profile
had no country table at all, so those numbers lived only inside the map — readable, not
exportable — while every other tab there offers CSV and JSON. A mobiliser deciding where
to send a team cannot work from a sheet they have to screenshot.

Derived, not a second source: every row is read out of gospel-by-country, which is itself
refreshed from the pin index, so the table and the map cannot disagree.

Three columns because they rank differently and each is somebody's question — groups with
nothing, people with nothing, and the share of the country each represents. The default
sort is by groups, and the share column is what makes rows like Vanuatu legible: 79 groups
with nothing out of 114, 69.3%, which ranks ninth by count and first by share. Region is
carried through so the table can be grouped the same way the map's region tab ranks.

Curated columns come through as-is and stay null where the curated analysis does not cover
the country — null, never zero, because zero is a claim this build cannot make.

Verified against the PIN INDEX rather than against the table being tested: 242 countries
with pins, 2,901 groups with nothing, worst is PNG at 469. The rendered tab reports 242
rows and its first row reads Papua New Guinea / PNG / Oceania / 831 / 469 / 56.4, with
650 real cells. No page errors. Screenshot read.
2026-09-24 15:43:13 +00:00
doxa-maps-88655c70
04c8a816d0 Oceania is 51% unreached by group, and nothing on this map said so
region was empty on all 242 country rows, so the map had no way to answer "what share of
this part of the world is unreached" — only how many groups a country holds and how many
people live in them.

The source carries UNm49Region, and it was verified before being trusted: only 2, 9, 19,
142 and 150 appear — exactly the five UN M49 top-level regions — every country maps to
exactly ONE of them with 0 conflicts, and the five sum to 12,379, the whole corpus. So
the code resolves to a real name. It is backfilled on every row, not just the new ones,
and all 242 now carry a region with none blank.

The rate is a genuinely different answer from the two already on offer:

  Oceania   51.2% of its 1,325 groups have nothing —   679 groups —   3.1M people
  Africa    30.0% of 3,074                         —   923         —  68.2M
  Asia      19.3% of 5,073                         —   978         — 198.7M
  Americas  11.4% of 2,117                         —   241         —   6.1M
  Europe    10.1% of   790                         —    80         —  11.6M

Oceania is half unreached by group and the smallest population of the five; Asia is the
reverse on both. Ranked by rate, because count and population are what the other two tabs
already say and a third copy of either would be noise. All three are offered rather than
one picked for the reader.

The rows are plain divs, not buttons: a region is not somewhere this map can fly to, and
a control that looks clickable and does nothing is worse than a row that does not pretend.

Verified against an offline computation over the same index: all five regions match
exactly on rate, group count and population. The sheet header is now three-way rather
than showing the country prose in region mode. All five profiles still mount clean.
2026-09-24 15:39:50 +00:00
doxa-maps-88655c70
6062b080f7 60 countries carrying pins had no country row at all
Set out to verify the map's second mode, "Resources found", which had never been tested.
It works: the legend switches, the arithmetic is an exact complement of the gap view — 0
of 170 countries disagreed — and the two modes genuinely repaint, 98/46/12/6 polygons by
band in gap against 85/66/9/2 in coverage.

Proving that took three attempts and the third one found the real defect. Comparing the
fill expression proved nothing, because it is ["get","color"] in BOTH modes — the colour
is baked onto each feature and switching modes rewrites the source data. Comparing the
features instead showed 157 coloured and 5 transparent against 170 rows in the table,
and that mismatch was the thread worth pulling.

The previous refresh of this table only UPDATED rows that already existed, and the table
came from the curated 2,106-record set. So 60 countries carrying pins had no row at all —
150 people groups with nothing found, invisible to every country-level surface. Canada
20, Ghana 15, Timor-Leste 13, Australia 7. 72 countries are now added from the pins, and
0 countries with bare groups are missing; all 242 rows agree with the pins on groups and
no_resource.

Curated columns on the new rows are null, never zero: this build does not know a growth
rate for Canada, and zero is a claim. They are flagged from_pins_only so the different
basis is visible rather than implied. Names come from people-groups-slim's CountryName,
because a row labelled "CAN" renders "CAN" as the sheet's title. UNm49Region was
deliberately NOT used for region — it is a numeric code, and "19" is worse than blank.
One row stays a bare code: Kosovo, whose non-standard KOS has no name in the source, and
inventing one for a single row is not worth it.

The boundary file turns out to hold only 162 polygons and has no Canada, Australia,
Timor-Leste or Taiwan, so this paints five previously-transparent countries and fixes the
rest where it actually shows: clicking a pin in Canada now opens a sheet titled "Canada"
with 304 groups, matching the pins exactly, where before it fell through to a bare ISO
code with no statistics.
2026-09-24 15:39:49 +00:00
doxa-maps-88655c70
a3780fc06a Three buttons said "Show all" in the same composite and meant different things
Checked whether a reader can undo the dashboard filter and tell why the table changed.
They can: the banner names the selection and offers an escape, clearing works, and
re-selecting the same country refilters rather than sitting cleared — the filter object
carries a timestamp precisely so an identical selection is still a change.

The ambiguity is in the labels. Stacked under the map there were THREE controls reading
exactly "Show all": two in the map's group lists, which reveal the rest of a country's
people groups, and one under the table, which clears the resource filter. Nothing on
screen distinguished them.

That is not a theoretical complaint. The first probe written to verify this clicked one
of the map's, saw the filter unchanged, and reported "Show all does not clear the filter
in composed mode" — a defect that did not exist. If the ambiguity can mislead a probe
reading the DOM, it can mislead a reader looking at the screen.

The map's now read "Show all groups", the table's "Show all resources". One word each.

Verified after the change: the three candidates are distinguishable by text alone, and
the full cycle passes — filter to 400 of 6,498 for Indonesia, clear back to no banner,
re-select the same country and return to 400, no page errors. Screenshot read.

Fourth probe in this flight to produce a wrong verdict from an assumption about the DOM,
and the second where a composite made a pattern ambiguous that is unambiguous on a
single page.
2026-09-24 15:39:49 +00:00
doxa-maps-88655c70
1a410ff8ef Country click now filters the dashboard's tables too, instead of leaving the page
The language link was fixed last change; the country link had the identical defect and
answers the brief's SECOND requirement. "Every resource used in Indonesia" was an
<a href="../data-tables/index.html?tab=links&country=IDN">, so inside the composite,
clicking a country to see its resources navigated away from the composite to do it.

Both controls now split the same way: a button that filters the tables below when they
are on screen, an anchor when they are not.

Verified against an offline expectation rather than read off the page. Counting the
links table for rows whose country_codes contain IDN gives 400. In the dashboard the
control is a BUTTON, the URL stays on gospel-dashboard/index.html, and the banner reads
"Showing 400 of 6,498 resources for Indonesia" — the same 400. Standalone it is still an
<a> to data-tables/index.html?tab=links&country=IDN&label=Indonesia, so nothing changed
for the map on its own page. No page errors on either.

Screenshot read: the filtered rows are real Indonesian-language resources across five
sources — JESUS Film, Faith Comes By Hearing audio and Bible video, Cru's Four Spiritual
Laws, eBible and Zume — with a "Show all" escape beside the count.
2026-09-24 15:39:49 +00:00
doxa-maps-88655c70
c63d92542c In the dashboard, "every resource in this language" now filters the tables below it
The composite puts the map above its own tables, and the brief's third requirement is
that clicking a pin filters that table. It did not. "Every resource in German, Standard"
was an <a href="../data-tables/index.html?...">, which is exactly right when the map is
alone — and inside the dashboard it navigated AWAY, to the standalone tables page. The
filter applied correctly, 6 of 6,498, and the map was gone. Measured: the URL changed
from gospel-dashboard/index.html to data-tables/index.html.

The two halves now talk to each other. The dashboard provides a shared filter; the map
injects it and, when present, turns that link into a button that sets the filter and
scrolls the tables into view; the tables inject it and apply it in place, switching to
the links tab because a language filter means nothing on the others.

PROVIDED, NOT PROPPED, because both children must still work standalone. A child that
finds nothing injected keeps its old behaviour exactly — verified rather than assumed:
on resources-map/index.html the control is still an <a> pointing at
data-tables/index.html?tab=links&iso=deu.

Composed: the control is a BUTTON, the URL stays on gospel-dashboard/index.html, the
table banner reads "Showing 6 of 6,498 resources for German, Standard", and 6 rows with
66 cells render. Arrow changed from → to ↓ because the destination is now down the page
rather than away from it.

My first probe read "12 of 25" here and was wrong. Both halves share ONE shadow root in
the composite, so the map's own "Showing 8 of 25" matched the same pattern as the
table's banner. The probe now matches the table's specific wording and asserts rendered
rows and cells beside it. Same class of mistake as the three already recorded in
AGENT-BRIEF — a pattern that is unambiguous on one page can be ambiguous in a composite.

All five profiles still mount clean; gospel-dashboard 68,459 chars, 54 links, 0 errors.
2026-09-24 15:39:48 +00:00
doxa-maps-88655c70
0c64fdd856 A caveat naming three pools sat directly under rows from eight
The dashboard is the one profile never opened on a phone. Structurally it holds up at
390x844 — no horizontal overflow anywhere in either shadow tree, canvas 390x675, the
table half renders 20 real rows and 60 cells, no page errors. What does not hold up is
what it says.

Under the table, the Pools Caveat read "Pools checked: eBible.org, JESUS Film (Arclight),
Door43/unfoldingWord". On the same screen, visible immediately above it, were rows typed
Audio Scripture, Bible video, Zume Training and Gospel presentation — four pools the
caveat denied having checked. It is the most direct version of a mistake this bundle has
now hit repeatedly: a sentence written by hand goes stale the moment a source is added,
and this one had gone stale four times over.

It is no longer written by hand. The checked list is now counted off the rows themselves,
so it cannot disagree with the table it sits under: "Cru (GodTools), Door43
(unfoldingWord), Faith Comes By Hearing, JESUS Film, PNG.Bible, Zume Training,
eBible.org, eBible.org (mirror)". The NOT-checked half stays prose, because it names
things that are absent and no row can testify to those — GRN, api.bible, the Digital
Bible Library and Progress Bible, each needing a credential or an agreement.

The page subtitle had the same shape of error, offering "scripture, film or audio" over
tables carrying seven media. It now names the question rather than a list that will go
stale again.

Verified on the phone viewport after the change: table half still 20 rows / 60 cells, no
overflow, no page errors, and the rendered caveat lists all eight sources. Screenshot
read.
2026-09-24 15:39:48 +00:00
doxa-maps-88655c70
4f202cf8fd Merge main and regenerate the served tree
public/doxa-maps/index.html conflicted again. It is generated, so it was
resolved by regenerating rather than by hand-merging two machine-written
files: build the bundle, rebuild the tree, re-hash. fix:sri then reports all
113 hashes already in agreement, which is the evidence the tree is coherent
rather than merely conflict-free.
2026-09-24 14:37:48 +00:00
doxa-maps-88655c70
9a05d5218e Make Back retrace the whole chain, and Forward work at all
Drilling All Regions to Africa to Angola and then pressing Back: the first
press returned to Africa, the second stayed on Africa, and Forward never did
anything. history.length sat at 4 the whole time, which is the tell — every
Back was pushing a replacement entry, discarding the forward stack and
pinning the position.

The guard against that could never have worked. onPopState set a flag,
assigned the selection and cleared the flag synchronously, while the watcher
it guards is flush:'post' and runs after the DOM update — by which time the
flag was already false. So the flag is gone rather than patched: writeUrl now
compares the URL it would write against the one the address bar already
holds and returns early when they match. After a popstate there is nothing to
write, so no entry is created.

A one-step check could not have caught this. The existing assertion picks a
region, clears it, and presses Back once — and one step is exactly the depth
at which the bug still looks correct.
2026-09-24 14:34:10 +00:00
doxa-maps-88655c70
9484a98bbd On a phone the gap workflow was hiding behind a label that said "notes"
Some checks are pending
build-gate / build (push) Waiting to run
The pin-hint fix covered the group sheet; the other two full-height sheets had not been
exercised. They are fine — the country sheet and both gap views hide the hint, fit 390px
with no horizontal overflow, and throw no page errors.

Getting that answer took two attempts, and the first attempt is the finding. The probe
clicked the pin filter and the "see the largest" button, got three states back, and
passed. All three were the SAME state: the country sheet, three times. Nothing had been
clicked, because at phone width those controls are inside the collapsed legend and are
not reachable until it is opened.

Which is the real defect. The panel is labelled "Key & notes", but it also holds the pin
FILTERS — deliberately, because the key doubles as the filter, and the component says so:
"a reader who has just understood what a colour means is exactly the reader who wants to
see only that colour". On a phone that design puts the entire gap workflow behind a
label advertising documentation. Reaching the ranked gap list means opening "notes"
first, and a reader with no reason to open notes never finds it.

It now reads "Key & filters" / "Hide key & filters".

Verified after the fix with the legend actually opened: the three sheets are now three
distinct states — "Indonesia...", "Largest groups with nothing found...", "Where the gap
concentrates..." — each with the hint hidden, docWidth equal to viewport width, no
overflowing child and no page errors. Screenshot read at 390px.
2026-09-24 14:28:39 +00:00
doxa-maps-88655c70
4565ecfec1 Merge bundler-3's newest two commits; regenerate the touched bundle and tree 2026-09-24 14:24:20 +00:00
doxa-maps-88655c70
9dea551586 On a phone, the pin hint was sitting on top of the sheet it invited you to open
Seven chip types and two long notes have been added to the group sheet this flight and
none of it had been looked at on a phone. At 390x844 the layout itself holds up — no
horizontal overflow, chips wrap, the sheet scrolls its 1,338px of content in a 559px
window. What does not hold up is the shared pin hint.

"Tap a coloured dot — someone still needs prayer" is pinned near the bottom of the map,
which on a phone is exactly where the bottom sheet is. Measured: it covered "Also speak
German, Standard (25)" and the first group row under it. Once a sheet is open the
invitation is also redundant — the reader has already tapped a dot.

Hidden while a sheet is open, at phone widths only, and scoped to this bundle. NOT
out-stacked: the hint is a library component with z-index 2147482000 and a deliberate
"only the reader closes it" rule, which is the right behaviour for a bare map, and
raising this sheet above that number would be a z-index arms race with shared code that
every other map would inherit. The hint renders inside this component's own shadow root,
so a media-query rule here costs nothing and changes nothing for anyone else. Desktop is
untouched — there the sheet is a 330px corner card and the two never collide.

Verified in both directions rather than one: with the sheet open the hint computes to
hidden, and after pressing Close it is visible again. Screenshot read at 390px — the
"Also speak" heading and its rows are now legible.
2026-09-24 14:20:10 +00:00
doxa-maps-88655c70
445131cf94 Merge main into bundler-3/work — settle the 21-file conflict, keep both campaigns
Only 2 files conflicted in source: package.json and its CLI doc, both taken to
main's refactored gate pipeline (gates:pre/gates:post/build:bundles) after
verifying every check the old inline chain named still runs there. The other 19
were generated: public/assets/tables mirrors keep main's deliberate deletion,
and public/doxa-maps is regenerated by the build that follows.
2026-09-24 14:16:47 +00:00
doxa-maps-88655c70
38f8d8b1d6 The charts were drawing four of the seven resource types
A health check across all five profiles found them all mounting cleanly, and then found
the charts telling a much smaller story than the data holds.

Bible video reaches 1,449 of the 5,067 languages here — it would be the third-largest
bar on the page — and was not drawn at all. Neither were gospel presentations (85) nor
discipleship training (51). A reader taking away "what exists, by resource type" was
missing three of the seven types, including one larger than Open Bible Stories, audio
and Full Bible combined.

"Scripture (any)" also read only eBible's field, counting 1,237 languages where 1,729
have a text Scripture. Faith Comes By Hearing publishes Bible text, and the list profile
had already been corrected for exactly this undercount; the chart had not.

Two caveats had gone stale in the way this bundle keeps finding. The hero said "four
resource pools, including all audio, were never queried" — audio has been searched at
FCBH since. And the pools footer unioned two different provenance vocabularies into
nonsense: "arclight (14 sign languages, all captured), fcbh (none), ebible, arclight,
door43, fcbh", the sign-language sentence spliced into the general list with arclight and
fcbh appearing twice. Sign languages carry a different provenance because text and audio
do not reach a Deaf group, so the two are now reported as the separate claims they are.

MY FIRST PROBE FAILED AGAINST A CORRECT CHART. It compared the bars to counts over all
5,563 catalogue codes, but the chart counts languages SPOKEN BY A PIN — 5,067 of them.
The chart was right and the expectation was wrong. Corrected, all four bars match
exactly: Scripture 1,729, Bible video 1,449, gospel presentation 85, training 51. The
denominator is part of the claim, and a probe that gets it wrong invents a defect.

All five profiles verified mounting with no page errors. Screenshot read.
2026-09-24 14:09:28 +00:00
doxa-maps-88655c70
a4d8363179 Where the gap concentrates: the same 2,901 groups, ranked by country
The ranked gap list answered "which single group is largest". A mobiliser also asks
"where would a team go", and that answer concentrates hard: the top 10 countries hold
56.6% of all 2,901 groups with nothing, and Papua New Guinea alone holds 469 of them.

The gap sheet now switches between the two. Country rows show groups AND people, because
the two rankings disagree and the disagreement is the useful part — Papua New Guinea has
469 groups with nothing and 2.2M people; China has 125 groups and 150.3M. Which number
matters depends on whether you are sending translators or broadcasters, and the sheet
says so rather than picking for the reader.

Verified against an offline count from the same index: the rendered first row reads
"Papua New Guinea 469 groups with nothing · 2,187,150 people", matching exactly, with the
By country tab active.

Two sources researched this tick and both recorded as rejected rather than shipped:

WordProject is real and permitted — robots.txt allows everything except /database/ and
/scripts/, and it lists 78 Bible languages. But only 7 of its directory codes are
literally an ISO 639-3 this map carries, ZERO of those reach a group with nothing, and
matching the other 71 would mean joining on the English language name, which this map
refuses everywhere else because names collide across unrelated languages. Taking that
risk to duplicate Scripture links that already exist is a bad trade.

GodTools, added last change, reaches 0 tier-0 groups of its 105 ISO-joined languages.

Both are depth for languages already served. The remaining 2,901 groups are not short of
a source nobody has looked at — GRN, YouVersion, api.bible, DBL, Deaf Bible Society and
DOOR all need a credential or an agreement, and everything open has now been mined.
2026-09-24 13:58:42 +00:00
doxa-maps-88655c70
e0fd61eb3e Gospel presentations in 94 languages, from Cru's API rather than its parked domain
Every pool here so far is Scripture, film, Bible video or a course. None is a GOSPEL
PRESENTATION — the short translated thing a person actually opens with someone. Cru
publishes those, in the same organisation that makes the JESUS Film, so the doctrine
question is settled the same way the brief settles it for the film. Only their gospel
tracts are included, from a fixed allow-list; the lessons and survey tools are not.

IT CLOSES NO GAP AND SAYS SO. All 94 of these languages already have something, so this
is depth rather than coverage and moves no tier. Tiers are byte-identical at
2901/4767/4711. It reaches 3,270 pins and 3.97bn people.

godtools.org, the obvious host, IS A PARKED DOMAIN. It answers 200 to everything with a
114-byte body, and rendered, every URL — including /zz/nosuchtool-xyzzy — reads
"godtools.org is parked free, courtesy of GoDaddy.com". Shipping links there would have
sent people to a GoDaddy ad.

The real front-end is knowgod.com, and it does serve per-language tracts: /en/kgp reads
"Knowing God Personally", /sw/kgp reads "Kumjua Mungu Kibinafsi". But ONLY once
rendered. By fetch it is a uniform 3,864-byte shell for every URL valid or not — the
third host in this bundle with that shape, after jesusfilm's old ?languageId= and
live.bible.is.

So existence is proved from Cru's own API and never from the page: a (language, tool)
pair exists exactly when the resource's latest-translations intersect that language's
translations. Checked against the browser before being trusted — en/kgp and sw/kgp both
intersect and both render real localised text, and the bogus pair does not intersect. In
band, the verifier's controls are now 7 negative all non-verified and 7 positive all
verified, with a bogus knowgod pair among the negatives. All 94 URLs verified.

Clause I keeps its closed set: Gospel presentation only on knowgod.com, and knowgod.com
only that type. Poison self-test still 12/12. Screenshot read: German shows six distinct
resource types, including "Gospel presentation - Knowing God Personally".
2026-09-24 13:51:56 +00:00
doxa-maps-88655c70
5000355237 Reference the chevron once, the way their markup references it
Their chevron is a single <use href="/assets/icons/arrow-chevron.svg#chevron-up">.
We had copied its path into three call sites and never vendored the file.

I assumed the platform forced that, and it does not: an external
<use href="file.svg#id"> renders in Chromium, renders inside a shadow root,
and a local <symbol> plus <use href="#id"> renders too — 410x234 in all
three. What is forced is the external href, not the mechanism. A dropped
bundle resolves assets against whatever tree it sits in and the tree
generator stages an allowlist with no icons directory, so an external
reference would 404 in silence and the chevron would simply be absent. That
is the same reason worldmap.svg arrives through a prop. So: their path,
verbatim, as one <symbol>, referenced the way they reference it.

The first attempt blanked all 21 chevrons — the regex rewrote the path inside
the sprite it had just inserted, so the symbol referenced itself. Worth
recording how that was caught, because the CSS fingerprint called the broken
page identical: a <use> resolving nothing keeps exactly the same computed
styles. Geometry caught it, and so did looking at the page. The fingerprint
now says so in its own header, along with the fact that its keys are
ordinal-sensitive for unclassed elements.
2026-09-24 13:48:54 +00:00
doxa-maps-88655c70
b46ce03a8b The country colours were drawn from a different dataset than the dots on top of them
gospel-by-country supplies band and no_resource, which fill every country polygon on the
map. Those were computed on 2026-08-01 over the curated 2,106-record set. The pins drawn
ON TOP of that fill come from the 12,379-pin index. The two disagreed for 135 of 170
countries, and the worst cases are not close:

  PNG  coloured "critical" from ONE curated group, under 831 pins   (56.4% bare -> high)
  USA  coloured "covered"  from three groups,      under 623 pins   (7.1%  bare -> partial)
  PHL  coloured "covered"  from one group,         under 156 pins   (17.9% bare -> partial)
  IDN  coloured "critical" from 36 groups,         under 669 pins   (48.0%      -> high)

A reader sees the colour first and the dots second. Telling them a country is covered
while plotting bare groups across it, or critical on the evidence of a single group, is
the map arguing with itself - and the fill is the louder half.

groups, no_resource, pop_no_resource, scripture, film, pct_no_resource and band are now
counted from the pins, under the same band rule as before (critical >=60%, high >=30%,
partial >0, covered 0). growing_unreached, deaths_day and growth are NOT derivable from
the pins - they come from the curated country analysis over a narrower set, the country
sheet already labels them as a different basis, and they are left exactly as they were.

Verified: 0 of 170 countries still disagree with the pins on groups, no_resource or
band. Bands are now 8 critical / 52 high / 100 partial / 10 covered.

The legend was making a claim that stopped being true two changes ago: "Audio is
unchecked for every group here". Faith Comes By Hearing has been searched since. It now
names all five pools that were checked and isolates GRN as the one that cannot be -
403 on every request including its own robots.txt - which is the honest version of the
same warning.
2026-09-24 13:35:46 +00:00
doxa-maps-88655c70
85a3d61780 Two curated tables stopped disagreeing with the map about what exists
gaps-and-opportunities and people-group-resources predate everything added this flight.
Their curated columns — the bridge-language analysis, internet access, messaging
channels — are the reason they exist and are untouched. Their resource booleans had
drifted into saying things the map disproves:

  80 rows     heart_has_any=false while the index has a resource   (6M people)
  317 rows    "no scripture" while the index has TEXT Scripture
  277 rows    the same, for the bridge language
  5 rows      "no film" while the index has one
  2,106 rows  has_audio = the string "unknown", written when no audio pool had been
              searched. Faith Comes By Hearing has been searched since.

Only the facts the pin index is authoritative for are rewritten. Rebuilding these tables
would have destroyed curated work to fix a few columns.

I GOT THE DERIVED FIELDS WRONG FIRST AND CAUGHT IT BEFORE COMMITTING. `gap` reads
naturally as "no resource in the heart language", and defining it that way changed 416
rows. The table's own meta defines it as "nothing found in the heart language AND no
in-country bridge language carries anything" — my version would have silently discarded
half its meaning while looking like a correction. Every formula is now taken from the
table's documented meaning and was verified to reproduce the existing column on all
2,106 rows BEFORE anything was rewritten: gap, opportunity (which also requires the
bridge to be spoken by more than one group in the country), aligned, and status. With
the right formula, gap moves 12 rows, not 416.

has_audio stays THREE-STATE and never false: true where FCBH publishes audio, "unknown"
otherwise. GRN is the main audio pool and answers 403 to everything, so "no audio
exists" is a claim this build cannot make. 1,297 true, 809 unknown.

Verified after the fact, not assumed: zero remaining contradictions between either
table and the pin index, and all four derived formulas still reproduce their columns
2,106/2,106. Both tables written through the snapshot guard. Tables tab still renders
real cells.
2026-09-24 13:25:14 +00:00
doxa-maps-88655c70
271c7c387b Give the spacing and type numbers dx-life's own names
The three regions components carried 119 inlined values — 23.5264px,
19.5312px and sixteen others — with nothing to tell a reader that those two
are --spacing-lg and --font-size-md on the site being cloned. They now say so.

The values are unchanged. Live declares them in rem against a 10px root, but
rem resolves against the document root even inside a shadow root, and these
components render inside whatever page embeds them, so rem would scale every
space by 1.6 against a default 16px host. That is why they were resolved to
px originally, and the tokens stay px. Nor are they re-derived from live's
fuller precision: naming numbers and changing numbers are separate changes,
and keeping them separate is what makes this one provably inert.

They live in the plugin folder rather than library/styles because the count
says so — 121 occurrences here, none in any other plugin, none in library —
and shared placement wants a second consumer. The file says to move it when
one arrives.

Verified by fingerprinting every element the bundle renders: four pages at two
widths, 2,650 elements, 27 computed properties each, baseline taken from the
previous source. Zero differences, nothing gone, nothing new.
2026-09-24 13:24:54 +00:00
doxa-maps-88655c70
78e9f3cc2f The Deaf Gap tab was missing more than half the Deaf gap
Two tables behind the brief's resources view were describing a different dataset from
the map beside them.

"Where Nothing Was Found" still listed 3,457 groups. The map draws 2,901. Its builder
already used the current tier; it simply had not been re-run since audio Scripture and
Bible video started lifting groups out of tier 0. A mobiliser exporting that tab would
have worked through 556 groups who now have a resource. Regenerated, and it matches the
map exactly.

"The Deaf Gap" was worse, and structurally so. It came from the curated 2,106-record
set and reported 73 sign languages, 120 people groups, 19.35M people. The map draws
150 sign languages, 234 groups, 84.2M people. The tab named for the Deaf gap was
missing more than half of it. Rebuilt from the pins - the same index the map draws, so
the two cannot disagree - and it now measures 150 languages, 234 groups, 84,159,050
people, of which 136 languages / 183 groups / 55,199,840 people have nothing found.

Kept deliberately: the bridge rule, verbatim, because it is the most important sentence
in the table - a sign language is bridged ONLY by another sign language, and text or
audio in a spoken language does not reach a Deaf community. That is also why the
related-variety reasoning added for spoken macrolanguages must never be applied to
these rows. Private-use codes are counted rather than hidden: 40 of these languages
carry qaa-qtz codes that no resource pool indexes, so "nothing found" for them is a
statement about the join as much as about the world, and each such row says so.

MY FIRST VERSION OF THIS SHIPPED AN EMPTY TABLE AND MY PROBE PASSED IT. DataTable
renders a { meta, columns, rows } document and the rebuild omitted `columns`, so the
tab drew its header, its "234 rows" caption and five pages of pagination over a
completely empty body. The probe had asserted the caption - a number the page prints
about itself, which is not evidence it drew anything. It now counts rendered rows AND
cells, and is proven against the defect: with `columns` removed it reports 50 row
shells and 0 cells and FAILS; restored, it passes. The row count alone would still have
missed it.

Verified: both tabs' captions match offline counts from the same index (2,901 and 234),
both render real cells, no page errors. Screenshot read.
2026-09-24 13:18:51 +00:00
doxa-maps-88655c70
455c872c82 The list's headline counted 2,162 people groups who do have Scripture
Its gap number read only eBible and PNG.Bible for text. Faith Comes By Hearing
publishes Bible TEXT on bible.is, and since that landed the page had been telling 766
languages - 2,162 people groups, 1.37bn speakers - that no Scripture was found when
there is text Scripture in their language. The headline falls from 7,106 to 4,944.

This is not the change an earlier note refused. That note kept the pill meaning the
SCRIPTURE gap rather than "no resources at all", because widening it would say
something false about a language that has a film. That reasoning stands and is
untouched. This is the same claim finally counted in full: FCBH text is text Scripture.

full_bible stays eBible-only, because FCBH's row carries no book scope and "full Bible"
is a claim this build cannot make for it. Such a row reads "partial" - Scripture
exists, without overstating how much.

The pill also stopped saying "none found". It always meant the text gap, which was
unambiguous while text was all a row could show; now a row can carry audio Scripture
and a 27-part Bible video and still be pilled "none found". Arabic Moroccan does
exactly that - 29.2M people, a JESUS Film link, an audio Scripture and Matthew to
Revelation on video, labelled "none found". It now reads "no text", which is the same
honest claim and cannot be misread as "nothing exists for these people". The filter tab
and hero line follow.

charts.vue had the smaller version of the same blind spot: its "none" series ignored
Bible video and training, drawing 7 codes as having nothing while the map showed them a
real link.

Verified against expectations computed offline from the same index the pages load: the
list renders exactly 3,338 "no text" pills against an offline count of 3,338, down from
4,031; the country probe still splits Indonesia 348/321. No page errors on either.
Screenshots read.
2026-09-24 13:07:16 +00:00
doxa-maps-88655c70
73444655c9 Click a country and you now see every medium, not two of five
The country view answered the brief's second requirement with only Scripture and film.
Everything added since - audio Scripture, Bible video, Zume training - was invisible
there, and the predicate deciding which section a group landed in never learned about
them either.

So the same map contradicted itself depending on how you opened it: 559 people groups
covering 61M people sat under "No resource found" in a country sheet while their own
pin sheet showed a live audio Scripture link. Worst affected were Indonesia (63),
Nigeria (47), China (37) and Cameroon (37).

hasAny now counts all five media, and each group row chips audio, Bible video and
training alongside Scripture and film - keeping the type distinctions the rest of the
map insists on, so Bible video still says it is not the JESUS Film and an audio link
still names which media the fileset carries.

The summary above the list was hiding the largest one. It listed "Have scripture" and
"Have film" and stopped, so for Indonesia it showed 77 and 265 while 269 groups there
have audio Scripture - the single biggest medium in that country, absent from its own
summary while the rows underneath displayed it.

Verified against an expectation computed offline from the same index the page loads,
not read off the page: Indonesia splits 348 with / 321 without, matching exactly, and
"without" falls from 384, which is the 63 groups measured beforehand. Chip kinds now
rendered include Film, Scripture, "Scripture, audio, text", "Scripture, audio, text,
video", Bible video and Training. No page errors. Screenshot read.
2026-09-24 12:59:42 +00:00
doxa-maps-88655c70
77ac7e3436 The two largest gaps on this map were Chinese varieties of a resourced language
Read top-down, the gap list sent you to "Chinese, Jinyu - 60,310,000 people - nothing
found", then "Chinese, Gan - 41,280,000". Both statements are true about those ISO
codes and both are misleading as priorities: ISO 639-3 files them under the
macrolanguage zho beside cmn, Mandarin, which has Scripture, film, audio and training
here. Measured across the whole map: 84 pins covering 140M people sit at "nothing
found" while a sibling variety of their own ISO macrolanguage carries resources.

Those groups now carry a related-variety note, on the pin sheet and inline in the
ranked list, because the list is the thing that gets read top-down. 63 codes have one.

WHAT IT DOES NOT SAY. It is not a claim of mutual intelligibility, and the wording
refuses to imply one - spoken Jinyu and spoken Mandarin are not mutually intelligible;
what they share is a written standard. Only ISO's own relation is asserted, the reader
is told exactly that, and it changes NO tier and closes NO gap. Tiers are byte-identical
at 2901/4767/4711. The chips still read "none found", because that is still true.

The macrolanguage table is fetched from SIL rather than transcribed, and written
through the snapshot guard like every other catalogue.

Also corrected: the gap list header still said "Audio has never been searched for any
of them". That stopped being true when Faith Comes By Hearing was added, and a stale
caveat misleads exactly as much as a stale number. It now says audio HAS been searched
and these groups have none there either, with GRN still named as the one pool nobody
can look in.

And a gate defect that produced three runs of false signal. verify-gospel-map adopted
any server answering 200 on the default port. Port 4173 turned out to be held by an
unrelated python3 process from another worktree, so the gate reported on a build that
was not this one - first a PASS, then a mount timeout that looked exactly like a
regression in code that had nothing to do with it. Answering 200 is not the same
question as serving this tree. It now fingerprints the served bundle against the bytes
in public/ and refuses to adopt a server whose build differs, starting its own instead.
Proven: it now prints "serves a DIFFERENT build than public/ - not adopting it",
starts its own, and passes.

Screenshot read: the ranked list shows "related: Chinese, Mandarin has resources"
under Jinyu, Gan and Min Bei, and correctly nothing under Deaf Chinese or Deaf
Pakistani, which have no macrolanguage sibling.
2026-09-24 12:44:24 +00:00
doxa-maps-88655c70
f7a7813817 Say what the search shim actually costs, instead of calling it Fuse-parity
The import note claimed "the repo's Fuse-parity shim ... a wiring job, not a
new matcher." Nothing had ever measured that. Diffed against live
/regions/cameroon, the shim matches their Fuse exactly on an exact term —
"Cameroon" gives the same 8 rows and the same 9 highlight spans — and both
tolerate a typo of it, and both return nothing for a term with no match.

Where it differs: "islam" returns 5 rows against their 7. The two we omit are
Majera and Osatu, and Osatu's religion is "Ethnoreligion - Animism" with
"islam" appearing in none of the seven searched keys. They are Fuse's fuzzy
reach at threshold 0.4, not rows the shim failed to find; its first tier
returns the exact set and stops. The shortfall is always on the fuzzy side,
never an invented row.

The substitution is forced rather than chosen: fuse.js is neither in
package.json nor installed, new dependencies are out, and reimplementing
bitap scoring would be the from-scratch lookalike that copying exists to
avoid. The shim also has four other consumers, so widening its contract to
chase Fuse would change their search too. The note now carries those numbers
so the next reader inherits the measurement rather than the claim.
2026-09-24 12:41:54 +00:00
doxa-maps-88655c70
f129294609 Clause L: a verification record that stops being re-run now fails the gate
Eleven clauses checked that the surfaces AGREE with each other. None checked that any
of it was still true. "Link checked" is a claim about the present tense, and a table
that quietly stops being re-run keeps making that claim on evidence that could be a
year old while every consistency clause still passes. A dead link is the failure a
field worker actually hits, so this is not bookkeeping.

Clause L fails when a shipped URL carries no check date at all, or when the oldest
shipped link was last checked more than 45 days ago, and the message names the exact
command to fix it. Re-running is cheap and resumable now, so the failure is an
instruction rather than an obstacle.

Proven against real data, not only the poison harness: deleting the check date from
one shipped URL is caught ("1 shipped URL(s) carry no check date"), and backdating one
by 120 days is caught ("last checked 120 days ago, past the 45-day limit"). Both
restore clean. Poison self-test now 12/12.

Also checked and deliberately NOT changed: the Arclight harvest keeps a language only
when counts.featureFilm >= 1, and it was worth asking whether that discards coverage.
Of 2,330 catalogue languages, 2,309 already have a feature film. Only 15 carry just a
shortFilm or a series, and exactly one of those - nmg Kwasio, 2 pins, 60k people - is
spoken by a group here with nothing else. Widening the filter would mean typing a
short film as "Film" beside the JESUS Film for a single language. The filter stays.

The registered description still claimed "6/6 clauses must fail"; corrected to 12.
2026-09-24 12:31:36 +00:00
doxa-maps-88655c70
270fae1a02 Verification survives being killed, and every record now means the same thing
A full pass is ~6,500 requests over a quarter of an hour, and it was killed three
times by system memory pressure - twice at around 2,500 and once at 1,000. Results
lived only in memory until the final write, so each kill threw away every request it
had made. The file on disk survived, which is correct, but the pass never landed and
the bandwidth was spent for nothing.

Every record now carries `at`, the moment it was checked, and the table is written
after each batch. A killed run loses at most one batch, and rerunning with
VERIFY_MAX_AGE_H skips anything already stamped. Writing mid-pass is safe because a
checkpoint only ADDS to or REFRESHES records - it can never shrink the table, which is
the failure the snapshot guard exists for elsewhere.

Proven rather than asserted: killed mid-pass, 300 stamped records survived on disk;
the restart reported "resuming - 600 URL(s) already checked within 24h, 5876 left to
do" and skipped them. The fourth memory kill then arrived unplanned during the real
run and cost 500 records instead of 1,900.

With the pass finally complete, the point of it: bible.is verification switched from a
body-size heuristic to their API two changes ago, but only the URLs added since had
been re-checked. 2,609 of 2,868 records still asserted "verified" on the older basis.
All 2,708 now come from /api/bibles, and none from the heuristic.

The last 160 were neither shipped nor broken - leftovers from filesets no build has
referenced since the FCBH ranking changed, quietly carrying the old method. A full
pass now prunes any record for a URL that nothing ships and nothing proved dead, so
the table means exactly one thing: every URL this map currently ships, plus the
history of everything ever proven dead. Broken records are never pruned; forgetting a
dead link is how it gets shipped again.

That prune sat behind an early exit that fired whenever nothing was stale, so the
first run to reach it reported success and pruned nothing. Removed.

Table: 6,476 records, 6,397 verified, 74 broken and dropped, 5 uncheckable, 0
unstamped, 0 on the old method.
2026-09-24 12:28:48 +00:00
doxa-maps-88655c70
e99399c87c The Deaf gap is not a search problem, and the map now says so
136 of the 150 sign languages here have no resource at all, across 234 people groups
and 84,159,050 people. This tick searched for more rather than assuming, and the answer
is that there are none to find in the open catalogues:

  Arclight / JESUS Film   14 sign-language entries across 13 ISO codes - every one of
                          them was ALREADY in this index
  Faith Comes By Hearing  none at all, in 3,602 rows
  deaf.bible              200, but a 3,894-byte app shell
  deafbible.com           the same 3,894-byte shell
  door.international      200, 114 bytes

So the gap closes through an agreement with Deaf Bible Society or DOOR International,
not through more searching, and every sign-language group now says that in those words
instead of leaving a reader to assume somebody simply had not looked yet.

Two real honesty defects went with it. The provenance line claimed "ebible, arclight,
door43, fcbh" as CHECKED for a Deaf group - eBible and Door43 are text and FCBH is
audio, none of which serves a Deaf group - and named GRN, an AUDIO pool, as the thing
still unchecked, while omitting the two organisations that actually publish for the
Deaf. A sign language now reads "arclight (14 sign languages, all captured), fcbh
(none)" as checked and "deaf_bible_society, door_international - both key-gated" as not.

And the sheet showed a Deaf group an "Audio - none at FCBH, GRN unsearchable" chip:
noise about a medium they cannot use, pushing the one line that matters further down.
It is suppressed for sign languages. Where FCBH does hold something for them it is
text, and that link still shows.

Verified against the data rather than the prose: 150 sign languages, 13 with a film, 0
with a text Scripture, 0 with both - so the standing claim that none yet has both is
still true - and 84,159,050 is the population the page renders. Screenshot read: Deaf
Nicaraguan shows the exhausted-catalogue paragraph naming both ministries, with no
audio chip.

Jehovah's Witnesses publish extensively in sign language and remain excluded. The size
of a gap is not a reason to relax the doctrinal filter.
2026-09-24 12:08:27 +00:00
doxa-maps-88655c70
3e7df7ecce Mirror the region chevrons in Arabic, the way their own rotate-90 utility does
In Arabic the chevrons moved to the correct side of the label but kept
pointing right, so "forward" pointed backwards. Measured on the live site,
the same selector in both directions: .region-card__link svg is
matrix(0, 1, -1, 0, 0, 0) at /regions and matrix(0, -1, 1, 0, 0, 0) at
/ar/regions. Ours was the LTR matrix in both.

Their utilities/_shapes.scss already answers this:

  .rotate-90 { transform: rotate(90deg);
    [dir="rtl"] & { transform: rotate(-90deg); } }

and their .rotate-180 pointedly has no such variant. That distinction is the
rule worth keeping: a chevron meaning "forward" mirrors, a disclosure caret
meaning "open or closed" does not. So of the three rotations here only the
chevron gains the variant; both 180-degree carets were already right.

The open-state rule beside it is load-bearing rather than decorative. The new
[dir="rtl"] selector outweighs the --open modifier, and without its own RTL
rule the poster caret would have frozen mid-turn in Arabic.
2026-09-24 12:00:20 +00:00
doxa-maps-88655c70
0e0b142719 A half-finished catalogue fetch can no longer overwrite a good one
The FCBH fetcher read per-fileset video scope for 1,666 languages and that file was
committed. A later run, while live.bible.is was rate-limiting, produced 1,103 and
overwrote the good file with the degraded one. Nothing failed, nothing warned, and the
artefact silently lost a third of its content. It was caught only because git status
happened to be read, and the fix was a git checkout.

That is the third time this shape has appeared here:
  - a VERIFY_LIMIT smoke run replaced the full verification table with 30 rows;
  - dropping a dead link removed it from the corpus, so the next run forgot it was
    dead and the build put it straight back;
  - this one.
Every snapshot in this bundle is built from a network read, and a network read has a bad
day. The dangerous case is never the run that throws - that leaves the old file alone -
it is the run that half-succeeds and writes anyway.

scripts/_snapshot-guard.mjs now stands in front of every catalogue write. A snapshot may
grow freely and may shrink a little, but losing more than a tenth of the previous run is
treated as a failed fetch: the existing file is left untouched and the script exits 1.
It is deliberately not an equality check, because catalogues do genuinely lose entries
when a publisher retires something, and a gate nobody can satisfy gets switched off.
SNAPSHOT_FORCE=1 is the one loud way past it.

Proven by replaying the exact failure: feeding it the 1,103-entry version refuses the
write, names the real numbers (563 fewer, 13.8%, below the 90% floor) and leaves the
file byte-identical to the good one. SNAPSHOT_FORCE=1 writes it. An equal write passes.

All six fetchers are wired: ebible, arclight, arclight-share-urls, door43, zume, fcbh.
Re-running zume, door43 and ebible for real passes the guard at 52, 208 and 1,237 codes
and produces byte-identical data, so the guard perturbs nothing.

The one exception is the checkpoint write in the resumable share-urls fetcher, which
starts from the existing file as a cache and can only grow. It must now declare itself
with a CHECKPOINT-WRITE-OK marker, so the exemption is a statement rather than an
oversight.

New gate check:snapshot-guard fails if any fetch-*.mjs writes OUT directly without that
marker, or never calls writeSnapshot at all - a guard that is easy to bypass is a guard
that will be bypassed. It refuses to run against an empty file list rather than passing
vacuously, and --poison injects a raw write and requires the check to catch it. Chained
into build:unlocked and its doc copy kept in sync.
2026-09-24 11:59:48 +00:00
doxa-maps-88655c70
c4fbc469ac Bible video from FCBH, with the real book scope on every link
The previous change left video_date out because the column does not say what the video
IS. Establishing it took measurement, and the first samples were misleading. Three
video-only filesets all rendered ...S2DV pages opening on "Intro & Garden of Eden" with
Drama/Non-Drama controls, which suggested one Old Testament series. Reading
GET /api/bibles/<bible_id>, which returns the exact books array in one call, for all
1,669 video languages says otherwise:

    209  27 parts, Matthew to Revelation          (the whole New Testament)
    116  27 parts, San Mateo to Apocalipsis       (same, Spanish editions)
    110  12 parts, Intro & Garden of Eden ...     (Covenant, Old Testament)
     27  Mark      26  Acts      23  Luke
    1,006 distinct scopes in all

Generalising from three samples would have described the bulk of it exactly backwards.
Every link therefore ships its own scope rather than a shared description, and the 3
languages whose book list could not be read were dropped: an undescribed video is not
shipped.

It is typed "Bible video", never "Film". Film on this map means the JESUS Film, and
bible.is itself treats the two as different - its pages call a separate Arclight
endpoint, /api/arclight/jesus-film/languages?iso=..., to find it. So Bible video lifts a
group off "nothing found", which is true, and contributes nothing to the Scripture+film
tier. 1,666 languages carry it: 287 as their own link reaching 4,375 pins and 5.76bn
people, and 1,379 merged into the Scripture link whose media list now reads "audio,
text, video" instead of advertising half of what the page holds.

Verification for bible.is switched from a body-size heuristic to their own API. The page
answers 200 for any id, so the first version compared body sizes against a shell
measured at run time. That was a heuristic on a number the site controls, and it stopped
working when their WAF began answering HEAD with 403 and a 520-byte body for real and
bogus alike. The run-time calibration caught exactly that and ABORTED rather than mark
thousands of links verified - which is the whole reason it was measured and not
hard-coded. The API needs no heuristic: /api/bibles/OKEBSN is 200, /api/bibles/ZZZZZZ is
404. It is a tenth of the weight, it describes the same fileset the page renders, and
GET is used because HEAD is what the WAF blocks.

Also fixed: a fetch with no timeout hung the fetcher for thirteen minutes with no
output. A stall looks exactly like slow work, so every fetch in it now carries a
timeout, and the scope lookups run with bounded concurrency and 403 backoff.

6,404 links screened, 6,557 verified, 74 broken and dropped, 5 uncheckable. Screenshot
read: German shows "Bible video - 12 parts, Intro & Garden of Eden to After the Forty
Years / End Credits" as its own chip, distinct from "Film - link checked".
2026-09-24 11:47:37 +00:00
doxa-maps-88655c70
172af79f60 Don't tell the reader pins are coloured by prayer on the map that colours them by region
The no-selection chooser paints its pins by WAGF region — that is the whole
point of the state, and the region key beneath the map is what explains it.
Above that map sat DX-life's own sentence: "Each pin is coloured by how many
people are praying for that people group." True on their region and country
pages, where the pins really are prayer-coloured. Flatly false here.

That sentence is a single translated key, so it cannot be split (a ". " split
breaks on Chinese and Arabic punctuation) and a replacement clause would be
copy invented in eleven languages. The chooser therefore drops the claim
rather than restating it: the subtitle already gives both counts, and the key
below the map names every colour. Their own pages keep the sentence verbatim.
2026-09-24 11:33:31 +00:00
doxa-maps-88655c70
659ca6e244 FCBH text as well as audio, and 207 text-only filesets stop claiming to be audio
The FCBH integration used only audio_date. Their rows also carry text_date, and it is
unambiguous - they publish Bible text on bible.is. 698 codes carry text where this map
had no text Scripture at all, reaching 2,176 pins and 1.38bn people. Codes go 2,192 ->
2,400 and the gap falls 2,927 -> 2,908.

ONE LINK PER LANGUAGE, NOT ONE PER MEDIUM. A bible.is fileset is a single page carrying
whatever media it has, so two chips pointing at the same URL would be noise. The link
now states what it carries: 1,465 codes audio+text, 677 audio only, 207 text only.

That last number was a live falsehood. Because the builder never copied the media field
across, all 2,349 bible.is rows were typed "Audio Scripture" - including 207 filesets
with no recording at all. A group with text-only Scripture was being sent to look for a
recording that does not exist. Both the chip and the table type now follow the media.

Ranking is by media count, then AUDIO PRESENT, then recency. The middle term is not
decoration: without it a newer text-only fileset outranks an audio-only one on date
alone and the language silently loses its recording, which is the medium an
oral-preference group actually needs. Measured while it was missing: 3 codes lost audio.

video_date is deliberately left out. The column does not say what the video IS - a Luke
film, a sign-language edition, something else - and counting it as "film" beside the
JESUS Film would assert something nobody here has checked. 400 codes with no film today
carry one, so it is worth establishing later, but not by guessing.

Clause I now accepts BOTH Audio Scripture and Scripture on bible.is, because a fileset
really can be either, while keeping the set closed and keeping Audio Scripture wrong on
every other host. Proven by poisoning both directions: Film on bible.is and Audio
Scripture on ebible.org are each caught. Poison self-test 11/11.

Also verified, not assumed: the brief's requirement that a pin or country click filters
the table. iso=deu shows 4 of 5,889, country=IND shows 502 of 5,889. It works; an
earlier reading of mine that suggested otherwise was measuring the wrong thing.

All 6,117 links screened and verified. Screenshot read: Ninigo (ssg) shows "Scripture,
text - 2012 Wycliffe Bible Translators, Inc." where it would have claimed audio.
2026-09-24 11:26:10 +00:00
doxa-maps-88655c70
a11e19a6b0 Audio Scripture in 2,121 languages: every pin used to say "not searched"
Every pin on this map has read "Audio - not searched (GRN)" for its whole life, because
Global Recordings Network sits behind Bunny Shield and answers 403 to everything, its
own robots.txt included. Re-probed this tick: still 403. But GRN was never the only
audio pool.

Faith Comes By Hearing needed no key. Their 4.dbt.io API wants one and answers 422,
which is where the earlier note stopped. Their own recordings-database page does not
use that API - it is client-rendered and pulls a STATIC public file from their CDN,
bible_recordings.json, 3,602 rows of { iso, lang_name, bible_name, bible_id, country,
video_date, audio_date, text_date }, already keyed by ISO 639-3. 2,192 codes carry a
dated audio recording; 2,121 survive verification.

Audio lifts a group off tier 0, unlike Zume, because a people group with an audio Bible
in their own language does not have "nothing" - for an oral-preference group it may be
the most useful thing on the list. It does not create tier 2, which stays reserved for
text Scripture together with film. Measured: 533 pins and 59.5M people leave the gap,
which falls 3,460 -> 2,927. Coverage 72.0% -> 76.4%. 8,094 pins now carry audio.

A 200 FROM live.bible.is PROVES NOTHING and this almost shipped wrong. It answers 200
for a fileset that does not exist, returning a fixed-size not-found shell, so the
discriminator is BODY SIZE and HEAD gives content-length for free. The shell size is
measured at run time, never hard-coded: it was 7,961 bytes on one path and 7,860 on
another within the same hour. Both fetcher and verifier abort if a bogus id ever
returns the same size as a known-good one. It caught 73 dead filesets in FCBH's own
data, all dropped.

The first full pass collected 466 HTTP 403s from bible.is - rate limiting, not dead
links; the same URL answered 200 when retried alone. Requests are now paced per host
and 403/429 retried with backoff, which cut those to 48, and a VERIFY_RETRY_UNKNOWN
pass resolved all 48. Final: 5,882 verified, 73 broken, 5 uncheckable.

Three bugs found and fixed on the way:
- The dead-link drop ran BEFORE the audio merge, so 71 dead filesets attached after the
  cleanup and shipped anyway while the pass reported "dropped 0". A cleanup that runs
  before the thing it cleans is worse than none, because its count measures an empty
  room. Moved after every source attaches.
- A VERIFY_LIMIT smoke run overwrote the real table, destroying the broken-link history
  the previous change added. A limit run is now a smoke test that writes nothing.
- The browser gate screened 3,768 of 5,889 links because its harvest never learned the
  new field.

Added VERIFY_MISSING=1 so adding one resource costs one request, not a quarter-hour
pass over 5,960 URLs.

Charts, list and map were corrected together: all three still asserted audio had never
been searched, and charts deliberately drew it as a hatched "not measured" row. That
bar is now a real count, decided by hasField() from the data itself. The checked-pools
caveat on every group now names fcbh.

Screenshot read: Uab Meto (aoz) - no Scripture, no film, no stories, not in Zume - now
shows a verified audio link and the caveat lists fcbh as checked.
2026-09-24 11:16:30 +00:00