The province table and the map's province layer both read gb-IND-ADM1.geojson,
which is the UPG-100 list's curated 34-unit partition — not a country's states.
Eleven groups came out with no province because they were standing in states that
partition does not draw. Both now read the complete ADM1 that landed with the
internet work (36 states, geoBoundaries via DataMeet India): 998 assigned becomes
1,005, 44 province rows become 46, and the rows name Indian states.
Four groups are still unassigned and each one is legible: Great Andamanese and the
Sentinelese sit on islands whose simplified coastline their point falls just
outside, and Jing and Pana are on China's Vietnam and Laos borders, outside its
twelve-province subset. They are listed with their coordinates, not dropped.
The staged-assets gate now also asserts the map and the table read the SAME
geometry. Staging could never catch this: both files were staged.
The India layer drew on UPG-100's curated partition (34 of 36 units), so
Mizoram and Nagaland had figures but no geometry. studio staged the complete
ADM1 beside it (data/boundaries/gb-IND-ADM1-full.geojson: gbOpen
IND-ADM1-1811400, DataMeet / ECI, CC BY 2.5 IN, with its .meta.json). The
curated file is untouched.
- connectivity-map.vue fetches boundaries/gb-IND-ADM1-full.geojson.
- The crosswalk maps Mizoram -> "Mizoram" and Nagaland -> "Nāgāland" (the
boundary file's spelling) instead of null. The generator validates against the
full file, and the table's meta names it with its licence.
- The regenerated table has 36 areas drawn and none missing, so the legend's
"no boundary yet" line no longer shows (it renders only when that list is
non-empty).
Headless: build and tree OK; both boundary files are staged; the fill
expression (maplibre-gl-style-spec) colours 36 of 36 states (Mizoram women
67.6, Nagaland women 49.9).
The served gb-IND-ADM1.geojson is the 100-list's curated partition (Mizoram and Nagaland excluded,
listId-tagged), so a map colouring every state had data for 36 and shapes for 34. data/boundaries/
gb-IND-ADM1-full.geojson is all 36 from geoBoundaries gbOpen (boundaryID IND-ADM1-1811400, DataMeet /
Election Commission of India, CC BY 2.5 IN), with source and licence in gb-IND-ADM1-full.meta.json;
generate-tree stages both, and the dev server serves data/ directly. Reproducible with
library/scripts/boundaries/source-full-adm1.mjs (coordinates rounded to 3 decimals: 1.84 MB). The curated
file is untouched.
Country-level internet data is not good enough for India. This adds the first
sub-national layer, from the one open, machine-readable source verified for it:
NFHS-5 (2019-21, IIPS / MoHFW), "women / men aged 15-49 who have ever used the
internet (%)", urban / rural / total, for every state and UT (CSV transcription
of the state fact sheets, CC BY 4.0; SPEC-GRANULAR-INTERNET.md).
- data/internet/: the two NFHS-5 internet rows (with provenance, licence and
upstream commit), plus a strict crosswalk from source names to the shipped
gb-IND-ADM1 shapeNames.
- plugin-designs/cities-internet/scripts/build-adm1-internet.mjs (`bun run
internet:adm1`) writes data/tables/internet-by-adm1.json. It is GENERIC, one
row per iso3 x adm1 x measure x sex x area, with meta naming measures,
sources, boundary files and the national figures, so go-int and other plugins
can reuse it. It fails on an unmatched name or a value outside 0-100: a name
is never guessed.
- The map draws India's states in a blue ramp (0-25-40-55-70%+), leading with
women, with a Women / Men toggle. A tapped state shows both halves, urban /
rural / total. Its own legend block names the measure, survey, years and ages,
and says it counts people, not subscriptions, so it is never read as the
World Bank country bands. India's national band is taken out under its states.
- The shipped boundary file is the UPG-100 partition (34 of 36 units): Mizoram
and Nagaland have data but no geometry. They stay in the table and the legend
names them.
- 8 new strings under connectivity.adm1, in all 11 locales (library change:
consumers need rebuilding).
Headless: build and tree OK, and the table and gb-IND-ADM1 are staged under
assets/. The fill expression validates with maplibre-gl-style-spec, and all 34
shipped states get a colour (e.g. Bihar women 20.6 -> 0-25, Delhi men 85.2 ->
70+). A crosswalk poisoned to a wrong shapeName fails the generator. Passing:
universal-props, no-free-identifiers, i18n-parity, i18n-never-translated,
profiles-mount, composites-mount, no-orphan-gates, script-flags, quoted-scripts.
Not rendered in a browser.
The credit line said who translated the Scripture and called them "the likeliest
people to ask", then stopped at a name. 838 of the 929 credited groups now reach
that organisation's own homepage from the sheet.
The URL does not come from eBible's homeDomain field, which is still dropped: it
comes from a hand-screened list of 16 organisations, every one fetched on the run
that wrote the table. Where a credit names two, the one it LEADS with wins, so
"Pioneer Bible Translators and The Word for the World International" reaches
Pioneer and not the other way round.
National Bible Societies hold 89+ credits and are deliberately linked to none of
them: several UBS members publish editions carrying the deuterocanonicals, so
whether a Society contact sits inside the 66-book filter is a ruling to be made,
not one to assume. They keep the name without the link, and the count says so.
A contact is screened as somewhere to write to, not as a publisher this map reads
text from — two separate lists, so wycliffe.org appearing as a scripture link is
still a failure. Four organisations answered a datacentre IP with a bot challenge
while serving browsers normally; those are proved by robots.txt and the table
records which method proved which.
Every map showed DOXA's people groups, and only those. The same maps must serve other projects'
lists: Project 7000 (unreached), 4700 (frontier), 700 (frontier of 500,000 or more), 2000 (DOXA).
The list is now chosen the way the engine is:
profile-config `list` > ?list= > window.MAP_LIST > 'doxa' (exactly today's behaviour)
with ids or project aliases ('700', 'project-7000').
- library/lists/lists.json: each list is a FILTER over the IMB / peoplegroups.org dataset already
in the repo (Lostness Priority, Population, Engagement Status), never a hand-kept roster.
- library/scripts/people-groups/build-lists.mjs (`bun run lists`): writes each list's DOXA members
to data/people-groups/lists/<id>.json, joined the way the Vision Card funnel joins, so the
counts agree with it: unreached 7,341 (2,082 DOXA), frontier 4,754 (1,745), frontier-500k 737
(71), unengaged 2,982 (1,929).
- library/lists/peopleGroupList.js: resolvePeopleGroupList and applyPeopleGroupList(rows, cfg).
The default list returns the same array, unfetched. A list whose membership cannot load shows no
groups (fails closed) rather than DOXA's whole list under another list's name.
- Tier A, every production map: DataSourceManager applies it (and the maps that build one pass
their profileConfig), and so does every direct people-group fetch: people-groups-grid, the
Big Picture header, research-dashboard (data, CSV export; the DOXA-wide /statistics baselines
are skipped under another list), Regions (list, dashboards, their table), UPG-100 (4 fetches +
the poster wall), Gospel's roster, and the Experimental upg-neighbor and Wall. The composites
thread `list` to their children like `provider`.
Gates: check:universal-props gains a `list` column (LIST-FETCH, LIST-DSM, LIST-KEYS; its
self-test now plants 9 bypasses) and passes across 18 bundles, with all-people-groups allowed:
it draws the IMB dataset, so its list is Tier B. New check:list-prop-unit (Node, 17 cases, in
gates:post); poisoning the ladder order or deleting a membership file fails it. Documented in
documentation/reference-universal-props.md section 5.
Headless: all 14 touched bundles build; check:composites-mount 20/20 and check:composite-interop
17 links pass on this tree; no-orphan-gates, script-flags, quoted-scripts, doc-scripts pass. Not
rendered in a browser.
P1, data layer. 855 of Doxa's 2,107 groups are in India and 154 in China — 47.9%
of everything this map draws, rendered until now as two flat fills. No other
country is close; the third is Sudan at 105. Each group is placed into a province
by point-in-polygon on its coordinate: 44 provinces with groups, 998 assigned,
13 provinces holding groups with nothing.
China's ADM1 file carries 12 provinces, not 31+, and I expected that to lose
groups. Measured instead: 152 of 154 Chinese groups fall inside them and all
twelve are used. The file was cut to the provinces where the unreached peoples
are, so the subset is the right subset.
The 11 groups that land outside every polygon are COUNTED, not dropped — nine
Indian and two Chinese, every one of them coastal or an island. That is the
predicted failure of placing a people by a single point, and it belongs in the
output rather than in a silence.
Every row is flagged approximate, because a province here comes from a coordinate
that marks where a people is found rather than a settlement, and 108 groups in
the roster share a coordinate with another group.
Going below the border also exposed something the country view was hiding:
Arunachal Pradesh, in the far north-east, is offered TELUGU as its way in,
because the curated bridge table names it and a curated bridge outranks the
computed one. India's computed default is Hindi, defensible nationally and still
wrong for much of the north-east. This table does not fix that. It records it, and
says to read the ways-in column as "a language with Scripture and film somewhere
in this country" until a bridge can be chosen per province.
Two things, and the second was found by the first disagreeing with itself.
The CSV is what a team prints onto a poster or turns into a QR code, and it was
the only surface that did not say when the bridge language carries the Zume
course. 179 rows now do. Same URL the sheet already shows and the verification
already proved; nothing new is fetched.
"Candidate groups" is removed rather than renamed. It counted people groups
speaking the candidate language, which only the old pins-derived proxy could
produce; since candidates came from CLDR it has shipped as 0 on all 108 candidate
rows, and an empty column in an exported CSV reads as missing data rather than as
one that stopped applying. CLDR does give whether the language is official there,
which is a real fact about how likely it already is to be taught, so that takes
its place.
THE BUG. 182 rows had a bridge carrying Zume and only 179 could export one. The
three were Deaf groups speaking Auslan, and codes.asf has no sg flag — it is set
from the sign-language catalogues and asf is in none of them. So the pin index
built a SPOKEN bridge for them and the map offered it, because the map's is_sign
test reads that same flag. The access table refused it, because it also matches
the group name. A spoken bridge is not a weak bridge for a Deaf community, it is
not a bridge; this is the residue of the finding that already removed 89.
Suppressed on either signal now, so the two builders agree and the map cannot
offer what the table refuses.
Still 369 with nothing and still none of them without a way in.
A pool nobody here had tried, and the reason it stays out is worth writing down
so the next tick does not spend itself on the same site.
It is reachable and it 404s properly, so unlike the shells that defeated deaf
sources, its answers can be trusted. Its index publishes 118 languages with no
pagination and no stated total.
Out on coverage first: of the 294 languages in the Doxa gap, exactly ONE name
matches a folder there. 82 of the 118 match a language this index carries
somewhere, so the catalogue is real and does overlap us — just not the part with
nothing, which is the only part a new pool is worth adding for.
Out on the join key second: no ISO code is published anywhere, in the index or on
a story page. The folder IS the language name, so joining would mean matching by
name, the guess this build removed when Doxa's own API made it unnecessary, and
the guess that would have handed Akha in Laos one of three sub-varieties. The
single gap match above is a name match and worth nothing on its own.
Out on doctrine third, and that word is chosen carefully: no statement of faith
is served at any of the usual paths. This is unverified, not suspected. The brief
says leave a source out when its doctrine is unclear and say why, so that is what
this is, and it is not a finding against them.
No resource, link or count changes; the index differs only by its generated date.
The sheet names who translated the Scripture, and a team printing posters or
building QR codes exports the CSV and never opens the sheet. A lead that only
exists on screen is a lead that does not reach the people doing the work, so
"Translated by" and the year are columns now: 864 of the 2,107 rows carry a named
body across 33 organisations.
Left empty rather than filled where the catalogue names a licence instead of a
translator. The column says who to ask; "public domain" answers a different
question and would read as an answer to this one.
Two pools were considered and deliberately not attributed. Faith Comes By
Hearing puts the producer inside a free-text version name — "Okpe, Delta - Bible
Society of Nigeria" has it, "Malkus Mawyon" does not — and splitting on the dash
would invent an organisation for the rows that have none; its own API, which
would answer properly, needs credentials this build does not have. Door43's URL
owner IS authoritative but is frequently a catalogue or a language namespace
rather than a body, so "translated by Door43-Catalog" would be false for 116
entries. A name nobody can act on is worse than a blank.
The brief asks for real leads — real translators, the real organisations that do
the work — and the map had only URLs. A mobiliser who finds a neighbouring
language already has Scripture wants to know who produced it, because that
organisation has the linguists, the relationships and very often a team already
in that country. The link answers where; this answers who to ask.
eBible publishes its catalogue with a copyright line on every row and the join is
exact: 964 of the 964 eBible links in this index match a catalogue row by
translation id, none unmatched. 929 resolve to a named body across 137
organisations — Wycliffe Bible Translators, The Word for the World, Biblica,
Pioneer Bible Translators, the Seed Company, World Bible Translation Center. It
shows on the group sheet and on both bridges, where "who do I ask about this
language" is the sharpest question on the page.
NAMES ONLY, NO NEW LINKS. The catalogue also carries each publisher's domain, and
shipping it would put hosts into this map that the doctrine screen has never
seen — the screen is an allowlist precisely so a new publisher cannot arrive as a
side effect of a data refresh. Measured after the change: 16,963 links scanned,
the same count as before, and not one credit contains a URL.
A licence is not a translator. 35 rows reduce to "public domain", which says how
the text may be used and nothing about who made it; answering "who to ask" with a
legal status would be worse than saying nothing, so those are dropped and counted
separately from the rows that have no catalogue entry at all.
Gazan Arabs — 2,315,000 people, the second largest population this map draws —
was painted red as "nothing found" while the index held a Scripture Earth PDF and
a Zume course in South Levantine Arabic, and the access table beside it already
said "read". Because of that stale zero it was also the one group on the map
shown as having nothing AND no way in.
The cause is structural, not a typo. The tier existed only ON A PIN, and `codes`
is keyed from the resource pools while `pins` comes from the people-group
dataset, so a language can be fully researched and carry no pin at all: zero pins
speak ajp. With no tier to read, the map fell back to the roster's stored copy,
captured when that file was written and never refreshed since.
So the tier is computed for every code the index knows and published as `tiers`,
from one definition that also tiers the pins. The map reads it. It does not
re-derive a tier from the resource fields — that is the drift the surfaces gate
exists to stop, and a roster keeping its own copy is the same drift with extra
steps.
The gap is 369, not 370, and not one of them is left without a way in — so the
legend now says "every one of those 369" rather than "369 of those 369", which
read like a coincidence. Both halves still hold: every group has somewhere to
start, and none of them has the gospel in their own language.
Five countries fell back to the weak pins proxy, and Saudi Arabia fell back to
URDU. The reason is a join asking for a code no publisher uses: CLDR reports
Arabic (ara) at 100% of Saudi Arabia, 81% of Oman, 74% of Libya and 100% of
Kuwait, but ara is a MACROLANGUAGE and nothing is published under it — the
Scripture and the film sit under arb. Taiwan had the same shape: zho empty, the
resources under cmn. The five countries this hit are among the least reached on
earth, which is where a wrong bridge costs the most.
Resolved through SIL's own macrolanguage membership, never by name, reusing the
table already read for the related-variety note. Every one of the 1,175
candidates now comes from published language data and none from the proxy; 211
resolve through a macrolanguage.
The share keeps the name it was measured under. CLDR measured Arabic, not
Standard Arabic, so the sheet says Arabic is spoken by 100% of this country and
that Scripture and the JESUS Film are published in Standard Arabic. Claiming the
percentage for the variety would be a figure CLDR never produced.
"Nothing found 370" is where the legend stopped, and it reads as a wall. Every
one of those groups but a single uncoded row now has somewhere to start — a
curated bridge, a language their own country actually speaks, or SUN for a Deaf
group — and none of it was visible without opening 370 pins one at a time.
It says "a possible way in", not "reached". A bridge has to be learnt, it is not
Scripture in their language, and the red dot means exactly what it meant before.
Counted in the same pass as the tier numbers, from the same rows, so the two can
never drift apart.
Also drops a link-pruning pass added earlier in this change and then removed. It
was written on the belief that 74 links recorded broken were still being served;
they are not — the builder has pruned them from the index for some time, and it
reports the count on every run. The measurement that looked alarming was a
verification record listing URLs that had already left the index. Nothing was
wrong, so nothing needed fixing, and a second implementation of a working rule is
worse than none.
The bridge candidate — the "possibly a way in" offered to a group with nothing —
was chosen from Doxa's own pins: the biggest language among the UNREACHED groups
in that country that has Scripture and the film. That is not the language a
community shares with its neighbours. For Syenara Senufo in Mali it proposed
Minianka, one people group. Mali's vehicular language is Bambara.
The number beside it was worse than the pick. "8.8% of this country's mapped
population" is a share of Doxa's unreached pins, and a reader takes a percentage
for a share of the country. A percentage measured against a denominator nobody
can see is not a caveat, it is a wrong number.
CLDR publishes what each territory speaks and by what share, joined to ISO 639-3
through SIL's own Part1 column, and pinned rather than tracked from a moving
branch. 1,161 of 1,175 candidates now come from it; the remaining 14 keep the old
proxy and say on the row that their count is of this map's groups and not of the
country. Mali now reads French at 46% and official.
Where two languages are within five points the map names BOTH and says it will
not choose. Mali is exactly that: French 46% and official, Bambara 46% and not —
one is the administrative language and the other is what people share in a
market, and which bridges better depends on whether you are handing over a
booklet or starting a conversation. A tie-break would have quietly answered a
question nobody asked it. Both carry their own links, for the same reason the
first one does.
Two field lists were a field short, the same shape twice. The candidate filter
and the link resolver both read c.s for "has a readable Scripture" and forgot
that Faith Comes By Hearing carries text on the audio record — which excluded
Bambara from Mali's candidates entirely, and then, once it was named, offered it
the film while a full 66-book text sat one field away. There is now one
predicate.
The RTL gate read a hardcoded tree path. It reads the build config instead, and
was re-run against its baseline before the result was trusted.
SUN reached the access table and the CSV first, and the map — the surface a
mobiliser actually opens — still showed Deaf groups a panel that explained their
situation honestly and then left them with nothing to do. That is the largest
population on this map with nothing: 93 groups, 17.5 million people, and
structurally so, because a spoken bridge cannot reach a Deaf community and there
was no other kind.
Clicking Deaf Beninese now offers the SUN New Testament, the SUN Bible stories
and the language itself, and says that SUN is already being taught in Benin by
Mission Évangélique des Sourds — a named local Deaf church rather than a
resource with nobody attached to it. It is offered only where the group has
nothing, because a group with Scripture in its own sign language does not need
to learn a second one, and it says on its face that it is a bridge which has to
be learnt and closes no gap in their own language.
Carried in the pin index beside the spoken bridges rather than fetched by the
leaf, so the sheet and the access table cannot disagree about what exists. It is
one small object, not a per-language join, because SUN is one signed language
for all of them.
The doctrine scan reads it too. Those URLs live in a new top-level block that the
per-language loop cannot see, so they were added to the scan in the same change
rather than found missing later, the way the access table was.
Of the 369 Doxa groups with nothing in their language, 94 had no bridge either,
and 93 of those were Deaf — 17.5 million people, the largest untouched number on
the map. That was structural, not bad luck: every bridge here is a spoken
language widely used in the same country, and a spoken bridge is worthless to a
Deaf community, so the build correctly refused to offer one and then had nothing
else to say.
SUN is a signed language built to be learnt across sign languages, with the Old
Testament, New Testament and Bible stories in it. It is carried the way a spoken
bridge is — something to learn, no substitute for Scripture in a group's own
sign language — in its own columns, never folded into the access rung, which
stays the honest "nothing found in their language". 28 countries where it is
already being taught come with the named local Deaf church or organisation
doing the teaching, which is the difference between a resource and a lead.
Only URLs verified by their content ship. sun.bible answers 200 for
/sun/<anything> and /resource/<anything>, so a per-language URL built by pattern
would look valid and could be empty; the fetcher re-measures that catch-all on
every run rather than trusting a note. Hosts left out are listed with the reason,
including three that are reachable by a person but not by a script.
Doctrine was checked harder here, not less. The Deaf are the most heavily
targeted group in this dataset by publishers this build must never carry, and
the thinner the pickings the stronger the pull. SUN is Wycliffe Associates'.
Screening the result exposed the bigger hole. The doctrine scan read the pin
index only, and within it skipped the Words of Life and Scripture Earth fields,
so the access table — the one the page renders and the one exported to CSV for
posters and QR codes — was never screened at all. The least supervised links in
the build were the ones most likely to be printed and handed to a stranger.
Scanning both, and both fully, takes the screened count from 6,497 to 16,960.
The gate also now proves the server holds these tables. It adopted a server from
another worktree on the default port and reported PASS on it: all three
manifests are byte-identical, because a manifest describes bundles and the
change was to a data asset it never mentions. A planted excluded-publisher link
went undetected, which is the worst possible failure for this particular check.
Driver decisions (cards BUILD-OUTPUT-IS-ONE-FOLDER, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE).
- public/ holds only <tree>/ and a generated README.md. The build no longer writes public/index.html,
public/assets/ or public/__gallery/; `rm -rf public && bun run build` recreates exactly these two.
- The Plugin Studio is <tree>/index.html (tree-base "."), replacing the staging-forge chooser; nothing
is written above the tree. check:staging-lang, which tested that chooser, is retired with it.
- Assets are gathered in a temp directory and the fetched allowlist staged into <tree>/assets/ as
before. fonts/, images/, brand/ and legacy/ existed ONLY in public/assets: they are sources and move
to data/ (git mv). Poster previews keep their tracked sources in plugin-designs/*/assets/previews
(all 378 verified present).
- __gallery/ is archived out of public/ (a copy kept in the umbrella's _qa-cycle/archive) and out of
the build; generate-gallery renders only on request, into a directory you name, never public/.
- Native shells load the output folder: capacitor webDir public/<tree> (cap copy needs an index.html
there), Tauri frontendDist + window url relative to it. internal/sync-app-shells.mjs keeps both in
step with the tree's name on every build, so the coming rename stays one config line; the Capacitor
start page resolves its leaf tree-relative.
- dropin.include now lists only extras (README.md): the tree is always shipped and never named twice;
excludes are tree-relative ({tree}/...). clean.mjs removes the old root entries from existing
checkouts; the build lock and public/.gitignore's rules move out of public/.
- check:wp-embed read the BUILT manifest from gates:pre, before the build, so a from-scratch build
could never pass; it runs in gates:post and reads the tree name from config.
- Stale bundles refreshed by the clean build (key-cities, template-bundle, wall-of-people-groups);
the CLI doc's quoted scripts re-synced; capability exemplars regenerated; the '#' note leaves
check:stale-sweep-claim's command (it was already in //scripts).
Proof: rm -rf public && bun run build -> public/ = README.md + doxa-maps/; check:sri, check:drop,
check:parse, check:public-hygiene, check:quoted-scripts, check:capability-exemplars PASS; all 57 pages
(the front page + 56 leaves) mount under /doxa-maps/ on this tree. check:facet-cascade is red as on main.
dx-shepherd's verifier traced it. Merge 52ab6089 from this worktree committed
public/doxa-maps/gospel-resources/gospel-resources.js with 21 conflict markers —
both sides concatenated, 1.13 MB — and that is what threw "Identifier Ge has
already been declared" and killed every gospel leaf.
My diagnosis last commit was wrong. I blamed a module imported twice and
re-routed the import; that change was harmless but it was not the cause. The
REBUILD that came with it is what repaired the file. A plausible cause that a fix
appears to confirm is the easiest kind of wrong answer to believe.
THE ACTUAL CAUSE IS A LANDING SCRIPT I WROTE. On a conflict it resolves the one
file it expects — public/doxa-maps/index.html — and then runs "git add -A
public", which stages every OTHER file under public/ including the ones still
holding markers. One line of convenience turning a conflict git correctly refused
to guess at into a committed artefact.
So: a check that names the real condition. It scans every tracked file for an
unresolved marker, is built from character codes so it can never trip itself, and
carries a poison arm. It runs FIRST in check:gospel-all, because there is no
point measuring anything else about a tree that is mid-merge.
Also verifies the 8 links the GRN recheck recovered, which check:gospel-all
caught as shipped-but-unverified on its very first run — psi, alk, nos, jle, lma
and three more, all now confirmed landing on their own 5fish language pages.
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.
ALL PEOPLE GROUPS. The Driver: "not even loading, takes a very long
time", and its DOXA pins were dark on a dark map.
- It downloaded the 7.5 MB people-groups-slim.json (22 columns) to draw
ten fields. It now fetches people-groups-points.json: the same rows in
column order, repeated strings in dictionaries, coordinates at 3
decimals. 0.74 MB, generated deterministically by
library/scripts/people-groups/build-points.mjs and staged into the tree
by generate-tree. It falls back to the old file on an older tree.
- Pins no longer wait for the live prayer API. Every pin draws by GSEC
from the file, and the DOXA overlay recolours when the API answers. The
DOXA count reads "…" until then, not 0.
- Named style 'paper' (Mapbox light-v11 / MapLibre positron) instead of
'ink'. The four categories keep their meaning; yellow and green are a
step darker, and pins get a thin dark ring, so they read on near-white.
- Its words are in all 11 locales (allPeopleGroups.json), with a guarded
useI18n and values passed to vue-i18n so {n} is not blanked.
Measured on a served built tree, 3 fresh runs each, median time to first
rendered pins: 8.3 s before, 5.2 s after, on localhost. This box renders
GL in software, so most of the remaining time is map start-up; the 10x
smaller download matters more on a real connection.
DOXA VISION CARD. The Driver: "the data doesn't make sense … super false
information". The card rotated four typed-in figures with no source.
It is now a proportional funnel, every number computed from data by
library/scripts/people-groups/build-vision-funnel.mjs:
12,379 people groups (IMB rows: one group in one country)
7,341 unreached (IMB LostnessPriority 0-2, < 2% evangelical)
4,754 frontier (LostnessPriority 0, < 0.1% evangelical)
2,107 DOXA groups (the DOXA prayer list, snapshot saved in data/;
1,745 of them frontier)
The first three nest; DOXA groups do not all sit inside frontier, so the
card says how many do. The rules are on the card ("How these are
counted") and in HOW-THE-NUMBERS-ARE-COUNTED.md, which also explains why
the total is not the 16-17 thousand of Joshua Project's count. Material
light, all 11 locales, right-to-left in Arabic, sources and dates shown.
The old fake stats are removed from the instances file, the frontier
page, the manifest, the staging page and the gallery.
CHECKS.
- check:vision-card-numbers recomputes the funnel independently (own CSV
reader, the published rules) and compares with vision-funnel.json and,
with --url, the rendered bars. Negative-tested: a corrupted count exits
1. On the built tree: bars 12,379 / 7,341 / 4,754 / 2,107 and "1,745 of
them are frontier", all matching.
- check:map-embed-cells measures PRODUCTION-MATRIX cells B and C for the
seven gallery maps on a foreign origin; C proves the host's token is
used by handing the host a distinct token and checking every Mapbox
request. 14 of 14 pass.
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.
Not for main yet. All ten views mount on the dev server with real data and no
page errors. Left: the coverage view's status still says 'sample of 6,000',
its choropleth needs checking, and the bundle is not built or tree-verified.
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.
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 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.
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.
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.
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.
170 more codes, 57 with gospel audio. 979 codes carry a Words of Life
link and 1,220 people groups whose ONLY resource in any pool is that
recording — 79.4 million people — can hear the gospel in their own
language. Tier 0 is 1,653, from 3,457 when this began.
2,215 of the gap's 2,456 codes are decided. The "not yet swept" bucket
is down to 133 groups, so one more batch ends it, and the breakdown
will then read three situations rather than four — which the sheet was
taught to handle last tick, before it could print "0 groups still
unswept" beside a claim that four situations remained.
Provenance exact: 2,079 name GRN as checked, 2,079 decided, none
decided goes unnamed. All 57 new URLs verified before shipping.
170 more codes, 52 with gospel audio — 31%, against 43% over the first
nine batches. The sweep runs largest-population first, so the tail is
the small and rare languages where GRN is least likely to have
reached; the falling rate is the sweep working through the hard part.
922 codes carry a Words of Life link and 1,158 people groups whose
ONLY resource in any pool is that recording — 79.4 million people —
can hear the gospel in their own language. Tier 0 is 1,715, from 3,457
when this began. 2,045 of the gap's 2,456 codes are decided, 83%.
The shape keeps telling the same story: "GRN not yet swept" has fallen
842 → 660 → 484 → 311, while the real translation gap has risen 887 →
1,220. Groups are not getting worse off; they are moving from a bucket
that meant "we have not looked" into one that means "we looked
everywhere we can reach". That is the number a translator should plan
against, and it only became sayable once the two were separated.
Provenance exact: 1,910 name GRN as checked, 1,910 decided, none
decided goes unnamed. All 52 new URLs verified before shipping.
170 more codes, 73 with gospel audio. 870 codes carry a Words of Life
link and 1,104 people groups whose ONLY resource in any pool is that
recording — 79.3 million people — can hear the gospel in their own
language.
Tier 0 is 1,769, from 3,457 when this began. 1,876 of the gap's 2,456
codes are decided, 76% of the sweep, and the "GRN not yet swept"
bucket has fallen from 842 groups to 484: that share of the gap is
evaporating as the crawl proceeds rather than because anything was
translated, which is why the sheet names it separately.
The real translation gap barely moves by comparison — 1,001 to 1,101
— because groups leaving the unswept bucket mostly land there. That is
the honest shape of this work: the sweep is not shrinking the gap so
much as telling the truth about which part of it is real.
Provenance exact: 1,744 name GRN as checked, 1,744 decided, none
decided goes unnamed. All 73 new URLs verified before shipping.
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.
170 more codes, 62 with gospel audio. 797 codes carry a Words of Life
link and 1,028 people groups whose ONLY resource in any pool is that
recording — 79.2 million people — can hear the gospel in their own
language.
Tier 0 is 1,845, from 3,457 when this began: 1,612 groups moved out of
"nothing found" on verified links. 1,707 of the gap's 2,456 codes are
decided, 70% of the sweep.
The hit rate is falling, and that is the sweep working rather than
failing: 36% this batch against 43% over the first nine. The codes are
probed largest-population first, so the tail is the small and rare
languages where GRN is least likely to have reached — which is exactly
where the remaining gap lives.
Provenance exact — 1,578 name GRN as checked, 1,578 decided, none
decided goes unnamed — and all 62 new URLs verified before shipping.
170 more codes, 78 with gospel audio. 735 codes carry a Words of Life
link, and 960 people groups whose ONLY resource in any pool is that
recording — 79.0 million people — can hear the gospel in their own
language.
Tier 0 is 1,913. It was 3,457 when this work began, so 1,544 people
groups have moved out of "nothing found" on real, verified links.
1,537 of the gap's 2,456 codes are decided, 63% of the sweep, and the
hit rate holds at 43% across nine batches.
Provenance exact — 1,412 name GRN as checked, 1,412 decided, none
decided goes unnamed — and all 78 new URLs verified before shipping.
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.
170 more codes, 74 with gospel audio. 657 codes carry a Words of Life
link and 872 people groups whose ONLY resource in any pool is that
recording — 78.7 million people — can hear the gospel in their own
language.
Tier 0 is 2,001, from 3,457 when this started: 1,456 groups have been
moved out of "nothing found" by real, verified links. 1,367 of the
gap's 2,456 codes are decided, 56% of the sweep, and the hit rate is
steady at 43% over eight batches.
Provenance exact — 1,246 name GRN as checked, 1,246 decided, none
decided goes unnamed — and all 74 new URLs verified before shipping.
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.
170 more codes probed, 72 with gospel audio. 583 codes carry a Words
of Life link and 785 people groups whose ONLY resource in any pool is
that recording — 78.3 million people — can hear the gospel in their
own language.
Tier 0 falls 2,172 to 2,088, from 3,457 when this began: the gap has
shrunk by 40%. 1,197 of the gap's 2,456 codes are decided, so the
sweep is now about half done, and the hit rate has settled around 42%
across seven batches.
Provenance exact through the change — 1,078 codes name GRN as checked,
1,078 decided, none decided goes unnamed — and all 72 new URLs
verified before shipping.
"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.