Wrote this after shipping a page that returned 200 for every asset, reported
zero console errors, and was completely dead: a throw during setup dies inside
the custom-element boundary. Zero errors is not the same as working, so this
does not trust their absence — it looks for PRESENCE, on all five leaves, in
five steps that each name their own failure: the tag is defined, the host is in
the document, a shadowRoot is attached, it has children, it renders text.
Its first real run went red on all five, and it was right. Every gospel leaf was
dead with "Identifier 'Ge' has already been declared" — while the SRI hash
still matched and every request still returned 200. My own doing: four profiles
imported en/gospel.json directly for an English fallback, and library/i18n
already pulls that exact file in through an eager glob, so the IIFE carried two
instances of one module.
The fix is one module: library/i18n now exports EN_MESSAGES and the profiles
read it from there. The duplicate is gone.
Measured after: all five leaves mount with 3,228 to 233,307 characters of
rendered text, verify-gospel-map passes with 2,107 pins and the gap cutting to
370, and --poison confirms the check goes red on a page with no element at all.
The lesson is the gate's whole reason to exist. I had rich assertions about pin
counts and filters and none of them ran on data-tables, charts or the list, so a
bundle-level failure could take out every leaf while the one gate I did run was
pointed elsewhere. Cheap and universal beats rich and narrow for THIS question.
The table offered 107 groups a next step the map did not show. The candidate is
now computed once, in the pin build beside the curated bridge, and both surfaces
read it — the same rule that put the tier in one place. 1,167 candidates across
the research index; 107 of them reach Doxa groups the curated bridge misses.
The map's sheet renders it as its own weaker panel, and the words carry the
limit: "Possibly a way in — worth checking locally", with the language, how
many groups there speak it and what share of the country they are, and "not
ranked and not recommended". No strength, because none can honestly be
computed. Never for a sign language.
A REGRESSION I INTRODUCED AND ALMOST SHIPPED. The edit that wired this used
pinCandidates without declaring it — a string replace that matched nothing and
said nothing. The ReferenceError died inside the custom-element boundary, so the
map never mounted AND the page reported zero errors. My probe then failed with a
30-second timeout that I had caused separately, by passing playwright's timeout
as the waitForFunction ARG rather than its options, which had been silently
using the default in eleven scripts. Two faults stacked: a broken page that
looked error-free, and a probe too impatient to say so.
Zero page errors is not the same as working, and that is the only reason this
was caught: __grState never appeared, which no assertion about pins could have
explained away. Verified after the fix: 2,107 pins, gap cuts to 370, the
candidate panel renders for Abai Sungai naming Malay, and 25 Deaf sheets still
show no bridge at all.
The curated bridge covers 168 of Doxa's 369 with nothing, because it comes from
a curated subset rather than from the data. A candidate IS computable for the
rest: the largest language in their country carrying both Scripture and the film.
WHAT I COULD NOT COMPUTE, AND THEREFORE DO NOT CLAIM. Ranking those candidates
needs a notion of "is this a lingua franca", and neither available proxy
survives contact. By GROUP COUNT, Oman picks Swahili — two groups — for an
Arabic-speaking people. By SHARE OF PINNED POPULATION, a 10% floor REFUSES Urdu
in Pakistan at 7.3%, the national language, because few Pakistani groups are
pinned as Urdu speakers. That is the same bad-denominator mistake this map
already fixed on the choropleth. So no strength is invented: the candidate ships
with its group count and its share, under column names that say "candidate",
and a reader who knows Pakistan can see Urdu at 7.3% and know what to do with it.
A score would have thrown it away on their behalf.
AND THE ERROR CAUGHT BEFORE SHIPPING. The candidate logic was offering 89 Deaf
groups a SPOKEN language — Deaf Afghans to Turkmen, Deaf Arubans to Dutch. That
is not a weak bridge, it is not a bridge, and a mobiliser acting on it would be
sent to do something that cannot reach the people. This build already refuses to
search GRN for sign languages because GRN publishes audio; the candidate logic
had inherited the gap. Now: 92 Deaf groups are offered none, and their gap is
named as the access problem it is.
Of the 369 with nothing: 168 curated bridge, 107 computed candidate, 92 Deaf,
2 with neither.
The access table could say "nothing found" and stop. For 168 of Doxa's 369
that is not the whole truth: a language already spoken widely in their country
has Scripture and the film. Afitti in Sudan has nothing of their own; Sudanese
Arabic, spoken by 65 groups around them, has both. That is the difference
between no way in and a way in that is not their heart language, and it is the
next action a mobiliser can take — which is why it belongs in the EXPORT and not
only in the map's sheet, where it has been sitting unexportable.
Four columns: the bridge language, its ISO code, the strength, and how many
groups in that country speak it. Keyed by code|country, because the same
language bridges differently in different places.
IT CLOSES NO GAP AND THE DEFINITION SAYS SO. A bridge is a way in, not a
translation. These 168 are still counted in "nothing found", because they still
have nothing in the language they think in, and the first believers among them
still cannot be trained from a text that does not exist.
The other 201 have nothing AND no bridge. That number is worth having on its own:
it is the isolated set, where neither their own language nor anything spoken
around them carries the gospel.
Verified end to end: the CSV exports 2,107 rows carrying Bridge language, Bridge
ISO, Bridge strength and Groups speaking it, and the Afitti row reads Arabic,
Sudanese.
resources-list and charts turned out to be almost entirely explanatory prose.
Their actual chrome is four strings — two page titles, the loading line and
Close — and those are now in all eleven locales. The blanket dir=ltr is off both
roots, so Arabic gets its layout, and each English prose block declares its own
direction where it sits.
That completes option (b) across all five gospel profiles. What stays English is
what carries doctrinal and statistical precision: which pools were searched, what
"nothing found" does and does not claim, why Open Bible Stories is not
Scripture. A machine translation of those would read as authoritative while
being wrong, and one translated line in each locale says so in the reader's own
language.
Verified on a served built tree whose identity I proved first — the Russian title
appears once served and once on disk. en/ar/ru on both leaves: heading
translated, 0 clipped, dir=rtl only for Arabic, no page errors and nothing 4xx.
The last commit recorded "in RTL the leftmost data column clips against the
edge" as known debt. Measured rather than eyeballed, that is wrong. Counting
cells falling outside the table holder: English and Arabic are IDENTICAL — 13 of
20 columns off-screen in both, scrollWidth 3146 against clientWidth 1400 either
way.
A twenty-column table is wider than 1,400px, and RTL mirrors which edge the
overflow sits on. What I saw in the Arabic screenshot was English's right-edge
overflow appearing on the left, which is what mirroring means. I read a picture
instead of measuring, and filed the result as debt — which would have sent the
next person chasing a defect that does not exist.
Whether twenty columns is right for a 1,400px screen is a real question. It is a
design question, and it is the same question in English.
All twelve tab labels, the page title and the filter chip come from the
catalogue in eleven locales. The blanket dir=ltr is gone from this profile's
root, so Arabic gets the RTL layout it should have; the English prose blocks —
the subtitle, the hero sentence, the footnote and the two caveats in the chip —
declare dir=ltr where they sit, the same split the map profile uses.
The translator is named tr(), not t(), and that is load-bearing: the tab loop
binds v-for="t in TABS", so a t() here would be shadowed inside the one block
that needs it most and every label would render as whatever a tab object
stringifies to.
Verified on a served built tree whose identity I proved first — the Russian
string I had just added appears once in the SERVED bundle and once on disk, and
the port owner's cwd is this worktree. en/ar/zh/ru/hi: heading and tabs
translated, 50 rows rendering, 0 clipped chrome, dir=rtl only for Arabic.
STILL ENGLISH, and not claimed otherwise. The table's own column headers and
title come from the DATA FILE's columns[].title and meta.title, not from this
component, so translating them means translating the table schemas — a separate
job. And in RTL the leftmost data column clips against the edge; my clip check
only measured the chrome, so it did not catch it. Both are named here rather
than left for someone to discover.
THE RECHECK, reported at its real size. Re-asking all 293 Doxa codes the old
title catch-all had defaulted to "none" recovered EIGHT: the rule was wrong
2.7% of the time, not catastrophically. That moves 12 Doxa people groups out of
the gap — 381 to 369 — and each now carries a working Words of Life link. Modest,
and worth saying plainly rather than dressing up: the bug was real, its blast
radius was small, and the fix is that the classifier measures instead of
defaulting.
THE THREE LINES. rega's in-gallery count was right that these are chrome, not
prose: what a dot is, what "none found" does not mean, and that a country is
tappable. They are what a reader needs to use the map AT ALL, so leaving them
English made it unusable rather than merely untranslated. Translated in all
eleven; the long explanatory paragraphs around them stay English and are pinned
dir=ltr where they sit, which is option (b) unchanged.
36 Doxa groups carry an IMB "audio Scripture exists" flag while every pool here
says nothing. Asking why rather than shrugging: most are Dagestani and Caucasus
languages — Akhvakh, Andi, Bagvalal, Botlikh, Chamalal — which the
English-centred catalogues simply do not cover. The Institute for Bible
Translation in Moscow does, and their /languages page lists 54+ projects
including every one of those, with an ordinary robots.txt that allows it.
It still cannot be a pool. The page gives a native-script name, a region and a
language family and nothing else: no ISO code, no per-language page, no
publication URL. Joining it means matching "Akhvakh (ашвалӏи мицӏцІи)" to a
code by NAME, which is the guess this build removed two commits ago when Doxa's
own API made it unnecessary — the same guess that would have handed Akha in Laos
one of three sub-varieties.
What it is worth is the other thing the Driver asked for: the people who do the
translation work, so a group with nothing can be handed to a team that already
has the linguistic work and the relationships. For the Caucasus cluster, IBT is
that team, by name. That belongs in a leads table with a human confirming each
code, never auto-joined into the index.
Cross-checking Doxa's roster against IMB's own access flags: 122 of their 2,107
people groups are DEAF — 17.5 million people — and 92 have nothing in this index.
For 33 of those 92 IMB records that material EXISTS, much of it dated 2021 at
"Selections" or "Stories": Armenian, Bulgarian, Myanmar, Cambodian and Cuban
sign languages among them. That is a coordinated body of work this build cannot
see.
Re-probed all three hosts so nobody spends another tick on them:
doorinternational.org the REAL DOOR International. Cloudflare 403 "Just a
moment..." on every path INCLUDING robots.txt. A bot
shield, and this repo does not circumvent shields — it
reached GRN through their own public site rather than
around it. Out on access, never on doctrine.
deafbible.com still the same 3,894-byte app shell for every URL,
/sitemap.xml included. robots allows everything; there
is simply nothing served to a fetch. api.deafbible.com
answers 525, so their own app's API is down too.
door.international NOT theirs. Parked for sale on GoDaddy, and it now
publishes an llms.txt saying exactly that. It looked
like a lead precisely because it advertises an LLM
policy. Their real domain is the first entry above.
This is the largest identified-but-unreachable set on the map. The route is
someone asking them for access, not another agent probing the same three hosts.
Cross-checking IMB's own access flags against this index turned up 112 Doxa
groups where they say a resource exists and we found nothing. Nineteen of those
were gospel recordings — GRN, a pool this build had already swept — so one of the
two was wrong. Asking 5fish directly about six of them, with a real code and an
impossible one as controls: FOUR have recordings. alk Alak, nos Nisu Eastern,
jle Ngile, lma Limba East, each landing on its own numbered language page.
The fault is here. classify() read the page TITLE and its last rule was a
catch-all: any title matching neither known pattern became "none". That is a
default wearing the clothes of a finding, and it was silently losing every page
whose title was phrased in a way nobody had anticipated.
The landing decides now and the title only names. A published code lands on
/<id>?language=<name>; one they do not carry is swept to /regions — the same
discriminator already proved in band in the verifier and the rot sampler. A
request that errors leaves the code UNDECIDED rather than "none", because
recording a failed request as an absence is how this started.
GRN_RECHECK=1 re-asks the codes the old rule defaulted, and a hit can now take a
code back OUT of the none list — a recheck that could only add would be unable to
correct the thing it exists for. Proved on the six: 4 recovered, 2 confirmed
empty, controls passing.
The map got coordinates and a language code by matching each group's NAME
against the pin dataset. That left 285 unplaceable and, worse, unsearchable: no
ISO code is no key to look them up by in any pool, so the access table could only
report them as "not searched" — nobody had been able to ask.
Doxa's own per-group endpoint carries both, and their robots.txt disallows
nothing. /api/people-groups/detail/<slug> gives primary_language and latitude and
longitude for every one: Akha to ahk, Akeu to byo, Alxa Mongols to xal, Ajja to
krs — four the name join had failed. So the name matching is not improved, it is
REMOVED. It was always a guess wearing a join's clothes: Akha in Laos sat beside
Akha Jipea, Akha Jaweuy and Akha Piso in the pins, and any pick among them would
have invented a language for a people. The publisher saying which language their
group speaks ends the guessing.
2,107 of 2,107 placed. Zero without a language code. The "not searched" rung is
gone from the access ladder because nothing is unsearched any more.
The gap grows 281 to 381, and that is the point: those 100 groups were always
without the gospel in their language, and the map could not say so because it did
not know what their language was.
The fetcher runs its controls in band both ways before trusting a record — a real
slug must return a language and coordinates, an impossible one must not — so a
site answering 200 with an empty body could not fill this table with silence.
IMB's own access and engagement fields come along beside ours, never merged into
them, because where two sources disagree that disagreement is worth seeing.
Moved out (kept outside the repo, in the team's internal tracker):
- AGENT-BRIEF.md: operating rules for the team's own agents
- documentation/slide-deck-outline.md, documentation/README-longform.md:
internal pitch material; the README and why.md cover the same ground
- plugin-designs/gospel-resources/SOURCES-CHECKED-AND-REJECTED.md: research log
- .claude/ (an editor status line), .forgejo/ (a CI workflow that never ran),
_archive/ (a retired staging page; already gitignored but still tracked)
Seven code comments that named these files now describe the rule instead.
Fixed five dead links left from the docs/ -> documentation/ move, including
the one in template-bundle that every new plugin inherits.
The delegation diagram now shows the whole model: the Plugin Studio with the
shared library and the merge queue into main, seven production plugins with
capability badges and the Experimental shelf, one agent per plugin (a roaming
agent for the shelf), the shepherd's oversight ring, a legend, and the five
ways a build ships.
The Driver's question, in their words, is which of THEIR people groups have
gospel resources and which do not. Every tab here answered it for the research
corpus — 12,379 groups across 4,231 languages — which is the right scope for
finding what exists and the wrong one for working a list of 2,107. This one is
one row per group on Doxa's roster, it leads the tabs, and it exports.
The access ladder says what a person can DO, because that is what decides what
you send: read and watch 785 · watch or hear but cannot read 698 · read 53 ·
stories only 5 · nothing found 281. The second rung is the translation brief —
those groups can already receive the gospel and cannot yet study it, so the
first believers among them cannot be trained from a text that does not exist.
NO LANGUAGE CODE IS ITS OWN RUNG. The first run collapsed it into "nothing
found" and reported 566, when 281 had been searched and come back empty and 285
had never been searchable at all — no ISO 639-3, so no pool has a key to look
them up by. That would have told a mobiliser 285 peoples have no gospel in their
language when the truth is nobody has been able to ask. It is an identification
problem before it is a translation one.
CSV carries the filter and the roster: the whole table exports 2,107 rows, China
exports 154, and German exports a header and nothing else because German is not
one of Doxa's groups.
383 of the roster had no coordinates — 18% of Doxa's people invisible on the map
and missing from every count on it. Reading why, rather than reaching for fuzzy
matching, one class turns out to be a spelling convention: the roster puts the
qualifier first and the pin dataset puts it last, around a comma.
Acre Arara <-> Arara, Acre Ashkuni Nuristani <-> Nuristani Ashkuni
Banka Samogho <-> Samogho, Banka Butuo Nosu <-> Nosu, Butuo
Same words, same country, different order. Matching on the sorted token set
absorbs exactly that: 94 recovered, plus 4 that only needed diacritics folded,
and every one resolved to a SINGLE pin inside its own country. Spot-checked
against the languages they landed on — Ashkuni Nuristani to Ashkun [ask] in
Afghanistan, Banka Samogho to Bankagooma [bxw] in Mali — and all 94 carry a
language code, so the resource join holds.
What it refuses is the point. Doxa lists Akha in Laos; the pins carry Akha
Jipea, Akha Jaweuy and Akha Piso — a parent against three sub-varieties, and
picking one would invent a language for a people. Doxa lists Akeu in China; the
nearest pin is Ake, a different language. Two candidates is a refusal, not a
coin toss. 285 still have no candidate at all and stay unplaced, because they
need a source this build does not have rather than a looser string match.
1,822 placed now. The gap grows 268 to 281, which is the join telling the truth.
The map drew 12,379 people groups. Doxa works 2,107. It now draws theirs — 1,724
placed, 268 of them with nothing found, which is the number this map exists to
answer for. Every other group stays in people-group-pins.json as the research
record and the resource index; nothing was deleted, it stopped being dumped on
the map.
Countries are a COUNT of Doxa's groups with nothing, never a share. 106 of their
175 countries hold one or two groups and the United States holds three, so one
group without a resource read as 33% and turned the whole country amber. That is
what made the map look mainly yellow, and it was arithmetic rather than palette —
a prettier ramp over the same division would have been a prettier lie. A country
Doxa does not work is now left UNCOLOURED, because "no finding" is not "zero".
The ramp is sage through clay to oxblood, on the house light palette. No yellow:
it has the least luminance contrast of any hue against #f3f3f1, which is why the
old map looked jaundiced over a pale basemap.
Pin tiers stopped indexing the band array. A band is now a count of groups in a
country and a tier is what one group HAS — indexing one by the other would have
painted "nothing found" in the colour of "1-4 groups".
Type is up about three points throughout, 10-16px to 13-19px, because this is
meant to be usable by people who are not squinting at it.
The map was built on the full 12,379-group dataset. Doxa works the groups on
pray.doxa.life, so a map of everyone buries the ones they can reach and paints
countries by a gap that is not theirs.
The roster is now its own table, from their own API: 2,107 groups across 175
countries, 206 million people. 1,724 join a pin by (name, country) and every one
of those carries an ISO 639-3 code, so the resource join is intact. The other 383
are kept with mapped:false rather than dropped — they are Doxa's groups whether
or not this build can place them yet.
The measurement that settles the colour complaint: 106 of those 175 countries
hold one or two Doxa groups, and the United States holds three. The choropleth
paints a country by the SHARE of its groups with nothing found, so one group of
three turns the whole United States amber. That is the mainly-yellow map, and
underneath it is not a palette problem — it is a percentage over a denominator
of three being drawn as though it meant something. A share is only meaningful in
the 35 countries holding eight or more.
Nothing is lost: every index, verified link and sweep result stays where it is.
Clicking a pin is the first thing this map is for, and its sheet was still
English under every locale. Both halves of every resource chip now come from the
catalogue — the resource name and the link state — along with Pray for, Every
resource in, and Also speak. Proper nouns stay Latin with the frame translated:
Open Bible Stories, Scripture Earth, Zume, FCBH and GRN are names, not strings.
The bridge panel was English prose without declaring it, so Arabic threw its
sentence-ending periods to the start of every line — ".Not their language". It
declares dir=ltr like the other prose blocks now.
Verified on a served built tree in en, ar, zh, ru and hi: seven chips each,
nothing clipped, and the first chip reads Scripture · none found, الكتاب المقدس
· لا شيء, 圣经 · 一无所获, Писание · ничего не найдено, पवित्र शास्त्र · कुछ नहीं मिला.
The gospel map rendered identical English under every locale — measured, 3,018
words in en and in ar alike and not one non-Latin character. Its short UI labels
now come from library/i18n/locales/<lc>/gospel.json: the two mode buttons, the
search placeholders, the legend title and its eight colour bands, the four tier
filters, the five gap tabs, the twelve country and group stat rows, the sheet
controls and the dot tally. Key parity across all eleven.
The explanatory prose stays English on purpose and is marked dir=ltr where it
sits. It carries doctrinal and statistical precision — which pools were
searched, what "nothing found" does and does not claim — and a machine
translation there would read as authoritative while being wrong. One translated
line says so in the reader's own language, and it is absent in English, where
saying it would be noise.
Two things Arabic taught. Numeric ranges needed explicit LTR isolates: 30-59%
rendered as 59%-30 inside RTL text. And the dot-tally sentence was pinned LTR
first, which pushed it out of the panel and clipped "with a link" against the
left edge — it is a count, not doctrine, so it went into the catalogue and the
bidi problem stopped existing.
createAppI18n sets document.documentElement.dir=rtl for Arabic, which is right
for a translated app. Every string in these five profiles is hard-coded English
— measured at ?lang=ar: identical word count to ?lang=en and zero non-Latin
characters — so the flip gained nothing and cost a great deal. On the built
tree it clipped the legend title's first letter against the left edge, turned
30-59% into 59%-30, moved every colour swatch to the far side of its label,
threw sentence-ending periods to the START of lines, and dropped the zoom
controls on top of the search box. Not untranslated: scrambled.
The five profile roots declare dir=ltr until their strings are translated, and
the comment says to take it off on that day. The host page's own dir is left
alone, as createAppI18n already intends.
Arabic now renders exactly as English does: 12,379 of 12,379 pins, full
choropleth, nothing clipped.
gospel-dashboard.vue declared @element gospel-resources-dashboard. index.js
registers exactly one custom element, gospel-resources-map, and every profile is
selected through its profile-config — which is why all five generated leaves
embed gospel-resources-map and work. The manifest reads that JSDoc line, so the
catalogue offered consumers a tag that resolves to nothing: copy the snippet
into a page and get an empty box. Invisible in dev, where the leaf HTML is what
loads, and only findable on a built tree.
Also retires the numbers shipped alongside it, which described a different
dataset: 3,457 groups with nothing became 1,613 and 347 million people became
208 million when the GRN sweep finished, and the tables profile still called
itself Eight tables over 3,550 resources when it carries eleven over 8,437
across 4,231 languages. The manifest is what a consumer of this map reads first.
Five codes still carried the retired ?languageId= URL, which nothing can verify.
Asking Arclight what each language actually has:
ars Arabic, Najdi The Savior (8 groups; film was their ONLY resource)
gjk Kachi Koli Magdalena - Director's Cut
mzn Mazanderani Magdalena - Director's Cut
lia Limba, West-Central no feature at all
jos Jordanian Sign Lang left alone, see below
counts.featureFilm >= 1 says a feature exists, not that it is JESUS. Three of
these had a real JESUS Film Project film in their own language under a different
title, and the map called all of them the JESUS Film — the same class of lie as
calling Open Bible Stories Scripture. They now ship under their own names, with
verified deep links; lia's film is dropped, because no component carries it and
a link nobody can check is worse than none.
jos is left exactly as it was. Arclight files iso3 jos under a row whose own
name reads Lebanese Sign Language, while the JESUS Film sits at a different id
named Jordanian Sign Language. The ISO join and the publisher's label disagree,
and resolving that is a data question, not a guess to make inside a fetcher.
It crashed on ENOENT — app/doxa-maps/gospel-resources/assets/tables/, a tree
deleted when data moved out of public/. Nothing noticed, because nothing ran it:
it had no npm script, so it was neither discoverable nor wired anywhere.
The dead path was the smaller half. Its other input was a private by-country
copy under plugin-designs/, and its people-group table was the curated 2,106-row
subset that resources-map.vue stopped fetching some time ago — it says so in its
own comments; the country sheet is built from the 12,379-pin index now. Repaired
in place it would have gone green about a surface it was no longer looking at.
Both inputs are the live tables. The country join is ISO3 instead of a substring
test against a comma-joined name blob, which the file's own comment admitted was
under-counting because country names contain commas. It reads 242 of 242
countries resolving to a pin and 8,435 sheet links, through the same flattener
the sheet renders from, and its self-test now requires four corruptions to go
red rather than two.
Also gives it and the stale-sweep gate an npm script, and corrects the link-rot
entry, which still told readers that jesusfilm.org and fivefish.org can never be
status-checked — the thing that gate stopped believing two commits ago.
Click a country on the map, follow through to the table, export: you get 264 of
8,438 rows, correctly filtered, in a file called
gospel-resources-every-link-and-who-it-reaches.csv. Do it for a second country
and you get the same filename again, so the second download overwrites the first
or lands as "(1)". The rows were right and the file was unidentifiable, which
is the whole failure for the person this export is for — someone pulling a list
per country to hand to a team.
The filter now travels in meta.title, which is what the download is named from,
so China arrives as -people-s-republic-of-china.csv and German as
-german-standard.csv. Done here rather than by teaching the shared DataTable a
new prop, keeping the decision this file already made about a component with
other consumers, and it fixes the heading in the same move: it read "every
link, and who it reaches" over a table showing one language's six.
The two curated figures — growing & unreached, deaths a day — sit under a
footnote saying how much narrower their basis is than the pins. It read "covers
391 of these 391 groups — a narrower set than the pins" for every one of the 242
countries, because the script that recomputes country rows from the pins
overwrote the curated group count with the pin count and the footnote went on
reading it. Always false, and false about the one thing it existed to qualify.
The real numbers were still in the commit that introduced the overwrite and are
restored on the rows: China 158 of 391, India 856 of 2,146. They are recorded
once now and never rewritten, because the pins change every build and a curated
analysis does not.
The same block also drew both figures for the 72 countries that have no curated
row at all — "Est. deaths/day 0", which is not a measurement. It is hidden for
them instead. A first pass at the fix handed those 72 a basis equal to their pin
count, inventing the number the footnote exists to qualify; null now means there
is no curated set here.
Open a group in the gap and the sheet listed five pools that found nothing —
Scripture, film, FCBH audio, Bible Stories, Zume — and said nothing at all about
Global Recordings Network or Scripture Earth. For 1,220 groups GRN is the only
resource there is, so silence about it at the moment of decision does not read
as "searched and empty", it reads as "nobody asked". Both now have an absent
case, with the pool named.
The GRN chip asks the data whether the pool was really searched, and that is the
part worth keeping. The sweep ran against the gap first and stopped there, so
3,202 codes that already carry resources have never been asked — the first
version of this chip told German, which certainly does have Words of Life, that
none was found. The build had recorded the truth per code all along: a swept
code lists grn under checked, an unswept one carries "grn (not yet swept)".
The chip reads that instead of assuming the sweep was universal.
The legend's caveat is the paragraph a reader uses to decide what "nothing
found" is worth. It said GRN was being swept a batch at a time and part of the
pool was still unread. That was true while the crawl ran and false the moment it
ended, and it stayed on the page through four commits that knew better. Found by
reading a screenshot, which is the only thing that could have found it.
Corrected on all three surfaces, and scoped honestly rather than upgraded: the
sweep finished for every one of the 1,192 languages in the gap, so "nothing
found" no longer rests on an unread pool — but codes that already carry a
resource were not all asked, because the sweep was spent on the gap first. Both
halves are now on the page.
A gate holds it. The DATA decides whether the claim is allowed: while codes are
genuinely undecided it passes, and it starts failing the moment they are all
decided, so it re-arms itself for the next slow pool. It does not flag a claim
rendered under a v-if on the pending count, because that form expires on its own
and is the one to copy. Its first version asked about every code on the map
instead of the gap, measured 3,202 undecided and would have let the stale
sentence stand.
The rot sampler now covers 5fish by where its redirect LANDS — a published code
goes to its numbered language page, one they do not carry is swept to /regions —
the same rule the verifier uses, with both arms proved in band. Six links a run,
ten seconds apart, because their robots.txt asks for it. That leaves 94 of 8,510
shipped links outside every re-check, down from 5,760 this morning.
YouVersion is the largest catalogue the brief names and it is now recorded as
rejected, with the measurement: nodejs.bible.com serves it without a key but its
robots.txt is Disallow: / , www.bible.com answers even robots.txt with a bot
wall, and the unauthenticated slice returns 2 English versions against the dozens
that exist — so taking it would be both discourteous and wrong. It is named in
the unchecked list on 5,413 codes instead of being silently absent, because a
reader deciding that a group has nothing is entitled to know which pools nobody
searched.
Batch fourteen closed it — 2,347 language codes decided, 1,022 carrying Words
of Life. Forty groups leave the gap: 1,653 with nothing becomes 1,613, and the
gap breakdown drops from four situations to three, because "GRN not yet swept"
no longer describes anybody. What is left is 1,429 groups where every pool this
build can reach has been searched, 183 sign languages behind key-gated sources,
and one group with no language code at all.
Two things the sweep exposed. 5fish answers 200 for any path, so its 982 links
were being verified by a status that means nothing there; they are now judged by
where the redirect LANDS — a real code goes to its numbered language page, one
they do not carry is swept to /regions — with both arms proved in band before
any link is classified. And country-gap reads a file that another script
refreshes from the pins, so running them out of order publishes the previous
build's numbers: it said 1,653 while the map said 1,613. It now refuses to write
a table that disagrees with the index the map draws.
The breakdown hardcoded "Four different situations share it" and gave
every line an unconditional row. The GRN sweep has 302 codes left, so
within an hour or two that bucket empties — and the sheet would have
rendered "· 0 (0) — Global Recordings Network has not been swept for
them yet" beside a sentence insisting there were four. Two falsehoods
at once: the count, and the implication that some remain unswept.
Each line is now conditional and the opening counts the situations
that actually have rows.
Tested by simulating completion in the data the page really loads:
with every code marked swept it renders "Three different situations",
the GRN line is gone, and the real translation gap absorbs them,
1,220 → 1,531. Restored afterwards and both asset copies verified
identical and back to 311.
Worth recording from that test: the page reads public/assets/tables/,
NOT public/doxa-maps/assets/tables/. Editing the second one changes
nothing, which cost two browser runs to discover — the first
"simulation" verified the file it edited and still watched the old
number render, which looked like a broken guard rather than a wrong
path.
Two defects, both found by looking at the rendered page rather than
the data behind it.
The footer read "Pools CHECKED: … grn. Pools NOT checked: …, grn (not
yet swept)." GRN is swept a batch at a time, so some codes carry it as
checked and the rest as unchecked, and unioning across the corpus
printed it on both sides. To a reader that is a flat contradiction,
and it is the kind that quietly discredits every other number on the
page. It is neither fully checked nor untouched, so it now says so
once: "grn (partly — the sweep is still running)", on the checked side
only.
And three of the eleven bars — Audio, Gospel presentation,
Discipleship training — are drawn in a green the legend never named,
so a reader had no key for them at all. The legend now carries it.
Neither was visible in the markup or the numbers. The pool strings are
assembled from per-code provenance, and the legend was three entries
that all looked reasonable until you counted the colours actually on
screen against the colours explained.
A footnote under every country sheet still said "No resource found
means not present in eBible or the JESUS Film catalogue; audio pools
are key-blocked and unchecked". Both halves stopped being true long
ago: eight pools are checked, not two, and audio is searched at Faith
Comes By Hearing and at GRN — which needed no key at all.
That is not a harmless leftover. It understates what the map knows, to
a reader deciding whether "nothing found" means anything, and it is
the single caveat most likely to be quoted back.
The file's own header was wrong in three ways and is corrected with
it: the band is not "no scripture and no film" but no resource of ANY
kind, since it is computed from the tier; the pools are named in full
rather than as "eBible + Arclight"; and GRN is not key-blocked.
Verified in the browser rather than in the markup — the footnote is
inside the country sheet, which only appears after a pin and a back
button, so a grep would not have told me it renders.
The map explains that "1,845 groups have nothing" is four situations
wearing one number. An exported copy of the gap table could not say
that at all, which is the copy a translation organisation actually
works from. Each row now carries its kind:
1,001 every reachable pool checked — the real translation gap
660 GRN not yet swept — the crawl is still running
183 sign language — pools are key-gated, an access problem
1 no language code — no pool can index it
Adding it exposed that the two surfaces bucketed differently. The
table tests sign language first; the map tested private-use first, so
47 Deaf groups were filed under "unindexable code" and the Deaf share
read 136 instead of 183. Both were internally honest and they told
different stories, which is worse than either being wrong on its own.
The map now uses the table's order, because being Deaf is the more
actionable fact about those rows than the shape of their code, and it
still says 47 of them are doubly stuck. Both surfaces report 1,001 /
660 / 183 / 1, verified against each other rather than separately.
The meta warns that filtering to sign_language alone is now the whole
Deaf figure and not a subset, since the private-use rows are the same
people.
The breakdown I shipped last tick implied two separate populations:
136 sign languages whose pools are key-gated, and 48 groups with a
private-use code that no pool can index. Measured: ALL 47 of the
private-use groups are sign languages too. Read as written, a reader
would put the Deaf share of this gap at 136 groups and 50.6 million
people, when it is 183 groups and 55.2 million — the largest blocked
population on this map, and one whose gap is an agreement away rather
than a translation away.
Both lines stay, because the reasons they are stuck genuinely differ,
but the sheet now says they overlap and gives the combined figure.
Found while researching whether anything open exists for them, which
is recorded next to the other rejected pools so nobody re-probes it:
deafbible.com serves the same 3,894-byte app shell for every URL,
door.international returns 114 bytes, and Deaf Missions is a real
ministry with real Scripture but ASL only — and not one of these 183
groups speaks ASL. Watchtower publish a great deal of sign-language
material and are excluded on doctrine, without exception.
"1,913 people groups have nothing" is four different situations
wearing one number, and they call for four different responses. The
sheet now breaks it down, computed from the pins and each code's own
"checked" line rather than typed:
887 groups, 149.7M people — every pool this build can reach was
checked and found nothing. This is the real translation gap, and
the only quarter a translator rather than an administrator can
close.
842 groups, 1.1M — Global Recordings Network has not been swept for
them yet. The crawl runs a batch at a time at the delay they ask
for, so these resolve themselves.
136 groups, 50.6M — sign languages, whose pools are key-gated. Fifty
million people whose gap is an ACCESS problem, not a translation
one, and reading them as untranslated would send help to the
wrong place entirely.
48 groups, 6.9M — a private-use code or none at all, which no pool
can index. Searching harder cannot move these.
The four sum to 1,913 and the rendered figures were checked against
the same count made offline. Also fixes a missing space that ran two
sentences together — "people.These are the 40 largest" — which the
screenshot caught and the markup did not.
At 390px the placeholder read "Find a people group, language or ISO
cod" — losing the one word that tells a reader an ISO code is accepted
at all, on the control most likely to be used first. Measured: 40
characters of text in 279px of room.
The short form keeps all three things the box accepts and fits with
room to spare, 185px of 279. The full phrase still shows at desktop
width, and the aria-label stays long either way, because a screen
reader has no width to run out of.
Found by checking phone layout after a flight of additions to this
sheet — five gap tabs, a filter, new chips and the bridge panel all
arrived without anyone looking at 390px. The rest held up: no
horizontal overflow, the chips wrap, the sheet scrolls, and the bridge
panel renders at 358px.
For 230 people groups the map said "nothing found" and stopped, when
that was not the whole truth: a language already spoken widely in
their country does have Scripture or the film. Kimr in Sudan has
nothing in Assangori; Sudanese Arabic, spoken by 65 groups around
them, has both. That is the difference between no way in and a way in
that is not their heart language.
WORDED SO IT CANNOT BE SKIMMED AS "THEY HAVE SCRIPTURE". The heading
says "Not their language", the body names whose language it is and how
widely it is spoken, the strength is quoted from the source, and the
last line says a second language is a way in and not a translation.
The chips above are untouched and still read "none found" five times,
and the tier is unchanged: 2,001 groups still have nothing of their
own, exactly as before.
Two exclusions, both measured rather than assumed. Fourteen of these
rows offer a DEAF people group a SPOKEN bridge — Gawri "has a film" is
no use to Deaf Pakistanis, the same error as offering them recorded
audio — so sign languages are out. Sixty-five carry the curated
strength "weak — may not be a real lingua franca"; a bridge the source
itself doubts is not an answer, so those are out too.
Placed below the whole chip row after a first pass split it, leaving
the Zume chip stranded under the panel.
Two claims in shipped metadata had gone false, and both were the kind
a reader trusts precisely because it looks specific.
people-group-resources listed Global Recordings Network under "not
included: requires an API key". Wrong on both halves: GRN is included,
583 codes of it, and it needed no key — 5fish resolves by ISO 639-3
and asks only for a crawl delay. A caveat naming a pool as missing
misleads exactly as much as a stale number when the pool is there. The
refresh now moves it out of that list and records how it was reached,
so the list cannot drift again.
The links table said "the 506 zero-reach rows" and "51 of the 168
codes". The true figures are 1,038 and 492: every source added rows
for languages no pin on this map speaks, and a sentence that counts
something has to be recounted when the something changes. Both are
computed now rather than typed.
Found by sweeping every shipped table's metadata for baked numbers
instead of waiting to trip over one. The surfaces came back clean —
their prose is already computed — so the stale claims were all in the
table meta, which is the part nobody looks at and a CSV carries away.
"Can hear it, no text" is a list of 2,556 groups. A team is not sent to
a group, it is sent to a country, and nothing aggregated that set —
the existing country table counts the groups with NOTHING, which is a
different population and does not contain one of these rows, because
none of them is tier 0.
154 countries, with two columns that disagree on purpose. Nigeria and
the DRC lead on LANGUAGES, 221 and 103, which is the translation
workload. China and Pakistan lead on PEOPLE, 218 and 195 million,
across far fewer languages. Which number matters depends on whether
you are funding translators or broadcasters, so both are shown and
neither is ranked for the reader.
The aggregate reconciles with its source exactly — 2,556 groups and
1,329,044,699 people both ways — and the basis travels as a column, so
a CSV that outlives this page still says "no text found in the pools
checked" rather than "no text exists".
Built without a browser: the box was low on memory this tick and a
gate that needs chromium was the wrong thing to add to it.
The same drift has now appeared six times, each spelled differently:
negated fields on a line naming "none", a positive hasAny over a
mapped row, a predicate inside a build script, and — found this tick —
a pair of columns no pass had ever touched. A source-level pattern has
to guess how the next one will be written.
Clause N does not guess. It compares the shipped ANSWER against the
pin tier, so it catches the seventh however it is phrased. Measured
when written: people-group-resources.any_resource_found disagreed with
the tier on 194 rows, and its no_resource_found pair contradicted
ITSELF on 7 more, because neither column was in the refresh at all.
Both now move together, 195 and 202 corrected, and the table agrees
with the tier on all 2,103 judgeable rows.
A code with no pin is SKIPPED, not guessed. The tier lives on pins, so
an unpinned code cannot be judged this way — judging it anyway
reported ajp as a defect on the first run, when the row was right and
the check was wrong. That is the failure this repo keeps meeting, so
it is written into the clause rather than left as a comment.
Also corrected there: the table's own note still told readers GRN
answers 403 to every request and had never been searched.
The legacy reconciler reported "nothing to change" and was confidently
wrong: its hasAny predicate never learned the two newest pools, so 122
curated rows — 8.9 million people — still carried heart_has_any=false
for groups the pin index can now reach. A reconciler that cannot see a
source is not reconciling, it is confirming.
Corrected exactly the 122 predicted, and the derived columns moved
with them: status 122, opportunity 93, lbc_carries_resource 69,
aligned 59, gap 29. Gap moves least because it means "nothing in the
heart language AND no in-country bridge either" — most of these have a
bridge, so they were already opportunities rather than gaps, and that
documented meaning is intact.
This was the fifth occurrence of one bug, and the gate written for it
had never looked at scripts — only .vue. Extending it to the build
scripts immediately found a sixth, in the Deaf gap table, which is
latent today because no sign language carries either field: GRN is
deliberately skipped for them and Scripture Earth has none. Fixed
while it is still cheap.
The rule now differs by file because the truth does: a surface must
READ the tier, while a build script COMPUTES it and cannot, so a
script must instead name every pool. Both halves verified against the
real pre-fix lines rather than synthetic ones.
The country sheet split a country's groups into "with resources" and
"no resource found" using its own hand-listed field set, which never
learned about Scripture Earth or GRN. Measured: 729 people groups —
77.9 million people — were filed under "no resource found" while this
same map's pin sheet offered them a working link. Chad alone had 107
groups with something and showed fewer.
It now reads the tier the pin index computed, carried onto the mapped
row. Chad reports 150 groups, 107 with a resource, 43 without, which
is what the tier says.
This is the FOURTH time this bundle has re-derived that decision
somewhere, and the gate written for it missed this one: the gate
looked for negated c.* fields on a line naming "none", and this was a
positive predicate named hasAny over the mapped row.
Widening it took three attempts and the first two were worse than the
bug. "Three OR-ed resource fields" flags canHear(), which ORs media
precisely because it must exclude text. "Spans text and media" flags
`!!c.s || !!(c.au && /text/…)`, because c.au is text or audio
depending on a qualifier and a `|| ''` default inflates the operator
count. Both were confident false findings — the failure this repo has
now met in a forced token, a swallowed flag and a class extractor, and
a gate that cries wolf gets switched off.
So it asks one question it can actually answer: a predicate NAMED for
"has anything" is deciding the tier by definition and must read it.
Verified against the real pre-fix line, not a synthetic one.
Three audio-Scripture sources were investigated this tick and none is
shippable. Writing down WHY, beside the unchecked-pool list, so the
next agent does not spend a tick rediscovering it — each was reachable
and doctrinally sound, and the reason is always coverage or the join
key, never suspicion.
AudioTreasure publishes 9 languages and every one is major; zero
overlap with the gap. Davar is a ministry site rather than a
catalogue, with no per-language page and so no URL a worker could be
sent to. Talking Bibles is the interesting one: 177 real audio Bibles,
open robots.txt, but the player keys languages by an internal numeric
id and publishes no ISO code, so a join could only be by NAME — which
this map refuses, because names collide across unrelated languages. It
turned out not to matter: of the 81 whose names match a language we
carry, ZERO are in the gap, so even a perfect ISO join would have
added no coverage.
GRN is the contrast worth keeping next to them — equally name-shy at
first glance, but 5fish resolves by ISO 639-3 directly, which is why
it is in and these are not.
The map could select the groups that can already hear the gospel but
have no text Scripture. A filter cannot be sorted, exported, or handed
to a committee, and this is precisely the list that gets handed to a
committee. None of these groups appears in "Where Nothing Was Found"
either, because none of them is tier 0 — it is the opposite question.
2,355 groups across 1,224 languages, largest first, with CSV and JSON
export. Each row names what the group DOES have and links to it, so it
is actionable without the map: Western Punjabi, 97 million, "audio
Scripture, the JESUS Film". The pools checked and the pools NOT checked
are columns on every row, because an exported spreadsheet strips the
caveat first and the caveat is the part that keeps this honest.
Fixing the map to agree exposed a real defect in the filter shipped
last tick: it counted 49 SIGN languages among its 2,289. "Can hear the
gospel" is the wrong sentence about a Deaf people group, and recorded
audio is neither what they lack nor what they need — this map already
suppresses the audio chip and the GRN attach for them for that reason.
Both surfaces now exclude them and both report 2,355.
The map could select the 2,289 groups that can hear the gospel but
have no text Scripture; it could not say which of them to weigh first,
and 2,289 dots cannot be read top-down. The gap sheet gains a fifth
tab that ranks them by population and, on each row, names what the
group DOES have — "has audio Scripture, the JESUS Film — no text
found" — so a reader can see whether they are being sent to a
recording or a film before they go.
The tab describes a different population from the other four, so it
does not inherit their sentences: the nothing-found opener, the audio
caveat and the macrolanguage note are all scoped away from it. Its own
paragraph states the bound twice, because the top rows are Western
Punjabi at 97 million and Wu Chinese at 92.5 million, and a reader
skimming those numbers must not come away thinking no text exists in
any variety they speak.
GRN batch four: 170 more codes, 90 with gospel audio. 343 codes now
carry a Words of Life link and tier 0 falls 2,488 to 2,373 — from
3,457 when this work began. 689 of the gap's codes are decided, and
provenance stays exact at 588 claiming GRN, 588 decided, none
decided-but-unnamed.
The pin filter could separate nothing / something / both, which are
tiers. It could not ask the question a translation team actually
starts from: who can already HEAR the gospel — a recording, the JESUS
Film, a Bible video, Words of Life — but has no text Scripture in
their language? That set cuts across all three tiers, so no tier
button could select it, and it is the largest actionable group on this
map: 2,289 people groups across 1,147 languages.
The claim is bounded in the button, the caption and the tooltip: no
text Scripture FOUND IN THE POOLS CHECKED, never a claim that none
exists. It matters here more than usual, because the largest entries
are Western Punjabi, Shanghainese and Egyptian Arabic — varieties
where a recording plainly exists and a text in that same variety is
genuinely contested.
The flag is baked into the pin features so the filter stays a GL-side
predicate like the others; recomputing it per toggle would re-upload
12,379 features to answer a yes/no already known at build time. The
button carries no colour swatch, because it names a question rather
than one of the three colours.
Measured in the browser: 12,379 pins rendered, 2,289 after the filter,
matching the offline count exactly.
The sheet's "checked" line is what tells a reader whether "nothing
found" means anything, and it was wrong in both directions. 5,131
codes omitted Scripture Earth, which had in fact been swept for every
one of them. And 110 codes named GRN as checked when the sweep has
never looked at them — GRN is crawled a batch at a time at the delay
their robots.txt asks for, so 2,100 codes are still unread.
The cause was ordering, the same lesson this file already carries for
the dead-link drop: the pass that normalises provenance ran before the
Scripture Earth and GRN fetchers, so the codes those fetchers create
kept the raw constant. A tidying pass that runs before the thing it
tidies reports a confident answer about a room still filling up. It
now runs after every source attaches, and the count is exact — 282
codes name GRN, 282 have been decided, and none that was decided goes
unnamed.
Scripture Earth is claimed unconditionally because that sweep really
did cover every code this map uses: a code absent from their index is
a checked negative. GRN is claimed per code, and the rest carry
"grn (not yet swept)" in the unchecked list instead.
Clause M of the coverage gate now fails if any code names a pool the
sweep never decided for it. Poison is 13/13.
The list built its links from c.s and c.d, then appended film, audio
and Bible video by hand. Scripture Earth, GRN, gospel presentations
and Zume were never added, so 190 languages rendered with no link at
all while the map offered a working one for the same language — Naba
in Chad, 753,000 people, had a Words of Life recording and the list
showed nothing.
Both surfaces now derive their rows from one helper in this plugin's
own folder. It is private on purpose: nothing outside gospel-resources
has a codes[] entry, so publishing it to library/ would advertise a
signature only one bundle can call.
Two things this nearly got wrong. The link kind was hardcoded to
"scripture" for the first rows, which was harmless while the list held
only text Scripture and became a doctrinal error the moment it held
gospel audio — Words of Life is gospel messages with songs, and
styling it as Scripture tells a reader they have a translation they do
not have. The kind now comes from the row's own type. And the eBible
translation count on the page reduced over the same array, so widening
it would have overstated how many translations exist under a sentence
that names eBible.org; it counts text Scripture rows only.
The gap pill is untouched and still reads "no text" for Naba, because
that remains true.
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.