A pin's position is generalised, so it does not tell a reader the locality or where a group is
centred.
- pins.sharedPoint in all 11 locales and ApproxPinNote.vue's fallback: the groups sharing the point
are named and counted ("{n} groups share this dot"); the locality claim is gone.
- keyCities map.note, map.sheetNote and list.caveat.row in all 11 locales: a cluster is of
people-group pins, not centroids, and each pin marks a people group, not a town.
- The key-cities and languages-per-city caveats in build-tabulator-tables.mjs and in every committed
copy of those two tables (only that one field changes), derive-city-candidates.mjs's caveat, and
the matching comments in key-cities, name-clusters and the poster generator.
- check-approx-pin-note's pass line no longer calls pins localities. Two UI-strings baseline entries
whose strings no longer exist in their files are dropped.
Locations are generalised, so a dot's position is not a claim about where a people group lives.
The pin notes now say only what is certain: a dot marks a people group, not a town or a boundary,
and its position is approximate.
- pins.approximate in all 11 locales, and ApproxPinNote.vue's fallback and header.
- gospelInternet notice.pin / notice.pins / notice.standing / facet.provinceNote in all 11 locales,
their T() fallbacks (GiDrawer, GiMap, GiFacetRail, board) and the README line. notice.pin no longer
takes {country}. The shared-point and internet-figure clauses are unchanged.
- gospel-resources: the province sheet's English sentence, and the `approximate` notes that
build-gospel-by-province.mjs writes into the shipped tables. Both tables and gospel-internet.json
are regenerated: only those notes and the generated dates change, every row is identical.
- Comments and the embed README say the same. The UI-strings baseline follows the new sentence.
India's card is no longer "too many to fit": it prints as the 60x96 side-lane sheet (all 855 groups named beside
the map, crossing-free leaders, real photos), whatever paper the wall asks for, until the shared size picker
offers 66x66 and 36x60 too. Its facts row reads "36 states and union territories" (English, from the wall
head's plural key). As ruled, the whole-India card offers no prayer list, packet or cards; the state cards keep
theirs. Its card preview is the real sheet.
check:side-lanes (+ :self-test, gates:pre): the three side-lane formats on a seeded worst case (855 groups,
stacked and clustered) through the real renderer: every name once, 0 crossings re-measured from the SVG, no
overflow, photos only where real; without the lane clamp the same data crosses 36 times, and the arm fails.
Each state's sheet drawn from live records with the browser's own inputs (SPARSE 3x3, the indiaState series, the
served boundaries and places), shot in the shared browser at 3000 px, and reduced by make-previews.py to the 560 px
card thumbnail and the 1500 px download, as the country previews are. Every record pinned; every layout octilinear.
Comments in library/ and internal/ that quoted a person, attributed a decision to one, used first person,
named fleet agents or carried private host names now state the same technical fact in neutral words:
poster-render (including the comments it writes into generated posters), packet-render, the poster and
research scripts, PosterWall, DataTable, the legends, the layouts and the gates. The network-policy note
states the policy itself. Behaviour is unchanged: a few printed gate messages and reason labels drop an
agent name, the poster-render integrity pin follows its comment-only edit, and series.json changes only in
its _meta and _why notes. The historical poster design snapshots (library/scripts/posters/snapshots/),
which nothing reads, are removed.
The country poster previews were the dense layout -- small facts table, both
vision columns, a smaller map -- while every UPG-100 thumbnail is SPARSE. Now
that the click-time print is SPARSE too, the stored country previews have to be,
or the card and the print disagree.
Generated with SPARSE=1 and checked BEFORE any browser time:
· the generator's own layout guards: 173 of 173 ok, octilinear, 0 diagonals.
· SPARSE on all 173: 0 sheets draw a dense-table element; all 173 carry the
bold "N UPG's" count.
· real values: 0 sheets show 0, NaN or blank population. Worst cases by UPG
count: Sudan 105 / ~20.7M, Brazil 103 / ~63K, Laos 95 / ~968K.
Shot on the fleet's shared Chromium (no private browser), one page at a time:
173 of 173, 0 failed, all 3000x3000, none blank. Smoke-tested and looked at
first: Sudan shows the bold facts row, no vision columns, "1 of ~170 maps".
ALL 173 CHANGE: 173 source thumbnails, 173 full-size and 173 served previews
differ from development, 0 strays, nothing written to a stray library/ path.
China is deliberately NOT here: its thumbnail stays the dense "map 174 of 174"
it has tonight. Its print becomes SPARSE with "map 1 of 1" via the small-total
guard. The remaining numbering difference is R-CHN-SERIES-TIER, a product call
after the demo.
The poster walls' card THUMBNAILS are stored WebP files, not generated at the
click, and all 174 were last shot on 09-25 -- teal. Since the region hue
landed, the PRINT has been hued, so both Country Posters and the Regions tab
showed a teal thumbnail above a hued poster. The Experimental section shows on
Deep Research, so that mismatch was visible.
Shot in one pass on the shared browser: 174 of 174, 0 failed, 0 retries,
23:00:37-23:10:07Z, one engine, DSF 2. Measured by the shooter: none blank (at
least 1,000 distinct colours each), and the count of sheets by their --zc hue
matches the region census exactly -- 41 Africa, 27 Asia, 27 Europe, 21 Middle
East, 18 Other, 17 Oceania, 14 North America, 9 Latin America. Asia is 27, not
28, because India has no sheet.
★ ALL 174 CHANGE, and that is the check that matters here. make-previews used to
write one directory too high (library/plugin-designs/..., library/data/posters)
while reporting success, so a reshoot could complete, look complete, and change
no served file at all. With that fixed, 174 of 174 source thumbnails, 174 of
174 full-size sources and 174 of 174 served previews differ from development,
with 0 untracked strays.
China was regenerated first with the whole-series count: 'map 174 of 174' and
'one of a 170-map set', where it had printed 'map 1 of 1' and 'a 0-map set'.
The stored poster (data/posters) and its wall preview predated the ink pin
badge: 57 pin numbers drawn white on the zone hue, 1.44:1. They are what the
UPG-100 info popup shows and opens ("Open full size"). Regenerated from the
current poster-render in its stored 3x4 format (1500x2000) and published;
the pin numbers are now white on #1c1c17, 17.10:1. The preview and the
1500px download are made from a 3000px shot of the regenerated sheet with
make-previews' own sizes and qualities (a 3x4 sheet now makes a 420x560
thumbnail; the wall letterboxes it). The other 27 stored zone posters are
fallback-only and follow after the demo.
The page says each box is a share of the people in this list. It
printed one DOXA-wide figure - 0.6133% of 209,079,159 people - in
every packet, true of no single list.
That figure had no builder in the repo. build-evangelical-share.mjs
makes its method reproducible, per group: the IMB CSV joined to the
DOXA snapshot by ROP3 + country, each group's EvangelicalLevel band at
its midpoint (0 for No Known Evangelicals, the midpoint read from the
band's own wording), weighted by IMB population. It writes
evangelical-by-slug.json and refuses to write unless the table, summed
over every DOXA group, reproduces christian-share.json exactly: 2,091
matched, 209,079,159 people, 1,282,178 evangelical, 0.6133%. Change a
single band's midpoint rule and it refuses.
Not the API's imb_gsec: GSEC puts 225 groups at "no evangelicals" where
EvangelicalLevel puts 1,444, because its "less than 2%" band also
carries church-planting status. A share from it sits on a different
scale from the method the page's own footnote states.
evangelicalShare.js sums a packet's own groups - pure, zero imports, so
the browser print and the offline generator run the same sum. Across
the 28 zones the page now carries 28 different figures, from 0.182%
(Deaf) to 0.922%. A host that supplies its own figure still wins.
A list with no published levels prints a dash and no number. The old
guard, Number.isFinite(Number(x)), passes null as 0 and would have
printed "0% evangelical" for no data. Where some of a list's groups
lack a level, the footnote says what the figure is computed over; it
says nothing when coverage is whole. That clause is in all eleven
languages.
check-evangelical-share (gates:pre): the table reproduces the committed
figure; different lists differ; no data is a dash; coverage appears
only when partial. Mutation-tested four ways, each caught - the fourth
only once it corrupted an existing group rather than adding one no sum
reads.
Seven changes to two plugins, re-applied onto current main as ONE source patch
rather than rebased, because the rebase conflicted in the generated bundles —
where a conflict is never resolvable by hand, since neither side matches what
either source builds. Source re-applied clean, everything rebuilt from it.
people-groups-grid
· the saved column count is restored before first paint, not after, so a
returning reader no longer watches the grid snap from auto to their choice.
It re-reads if the profile-config key arrives late, and never over a choice
the reader has since made by hand.
· the cards-per-row control is inset from the card edge it was sitting level
with (Driver round 2, #5).
· the window vocabulary handle is released on unmount, identity-checked so the
first of two grids on a page cannot strip the second's live handle.
regions-dashboard
· the four stat cards no longer let Population stretch across a tablet row.
auto-fit collapsed the empty tracks; auto-fill with the track minimum capped
at a quarter of the row gives at most four columns and never a stretched
orphan. Measured at nine widths.
· the dashboard is printable at all. It had no @media print rule, so the app
shell's absolute root and inner scroller meant the browser printed one
screenful; the legend stayed on top of the map; interactive chrome printed.
· both window globals are released on unmount, not just the remembered one,
each identity-checked and on its own line.
shared asset
· the worldmap watermark is 242 KB rather than 311 KB — same 394 paths, same
drawing, coordinate precision trimmed to what the render resolves.
The move was authored from a copy that predated fe54c303, so madhya-pradesh, uttarakhand and
west-bengal lost their region again and check:list-selection named all three. Restored from main.
GOSPEL'S LEGEND RENDERS IN THE BROWSER and needs these zones at runtime; a
build-time import path would not reach the bundle. data/tables/ is the pool the
tree copies into public/dx-maps/assets/tables/, which is how near-neighbors.json
already reaches a browser, so this makes the zone data a single source the same
way (dx-shepherd, 2026-09-28).
admin-to-list.json deliberately does NOT move: it waits on the Driver's row 87,
the "zones fold" regeneration.
★ TWO ROOTS IN internal/check-list-selection.mjs, NOT ONE. Its readJson helper
reads all three files through a single DATA constant, and only two moved.
Collapsing them to one root silently breaks whichever file is not where you
pointed — so the helper now routes admin-to-list.json to the plugin dir and the
other two to data/tables/, with the reason in the comment.
★ THE ASSET-POOL COPY IS MANUAL, AND IT HAS ALREADY DRIFTED. There is no
automated copier: `git grep` finds no rule in vite.config.js or internal/ that
writes public/dx-maps/assets/tables/, and exp's rings note says the same ("only
the tree copies tables"). Measured before copying: data/tables has 309 json
files, the asset pool 304 — and the five missing were groupings.json,
zone-regions.json AND THREE THAT PREDATE THIS COMMIT (index.json,
open-doors.json, platform-reachability.json). So these two would have joined an
existing gap rather than created one. Both are copied here and verified with
cmp; the other three are reported separately, not silently fixed in a move
commit.
Paths verified to resolve, not assumed: P(GROUPINGS) and the REGIONS join both
resolve to existing files. Gates green after the move: check:list-selection,
check:selector-fields, check:describe-group, region-hue-proof.
upg100-plugin ran the per-country fetch (304a4c120): 162 queries against the
geoBoundaries endpoint, values recorded verbatim, 0 failures, one spot-check
re-verified live. 130 of 162 countries carry a duty; only 32 are duty-free.
ATTRIBUTION now says the world-adm0 duties are DISCHARGEABLE and points at the
single authority for them -- their PROVENANCE.json -- rather than copying the
table, because one table in two places drifts. Their words.
★ THE DUTIES ARE RECORDED AS CLASSES, NOT AS ONE THING. They separated the 7
explicit ShareAlike countries because share-alike is a stronger duty than
attribution, which is right; by the same reasoning the class holds 65, because
Open Data Commons ODbL 1.0 also requires a derivative database to be offered
under ODbL. 58 ODbL + 7 CC-SA. This repository already acted on that reading:
Natural Earth's 1:10m roads were deliberately not shipped over an ODbL
obligation. Whether "licensed alike" binds a giveaway is a legal question, not
a measurement, and it is recorded as open rather than resolved here.
The CCK line is gone. It read as an outstanding gap; CCK is absent from
world-adm0 entirely, so that 404 was a fetch for a country that never shipped.
The India ADM1 boundary is Creative Commons Attribution (CC BY 2.5 IN,
sourced from DataMeet and the Election Commission of India), so the credit
has to reach whoever receives the data. It did not: 6.1 MB of boundary
GeoJSON shipped with no human-readable attribution, and a sidecar for one
file of eight.
data/boundaries/ATTRIBUTION now ships beside the data. Every licence value in
it is verbatim from the geoBoundaries API, queried per boundary -- CC BY 2.5
IN for India, Public Domain for China. No licence wording is ours.
★ IT TOOK TWO ALLOWLIST EDITS, and the first one silently did nothing. The
boundary staging is TWO-STAGE: a pair list gathers into the build pool, and a
separate path list decides what reaches the tree. I added the file to the pair
list, rebuilt, and it still did not ship. An entry in one stage of a two-stage
pipeline is not an entry.
WORLD-ADM0 IS CORRECTED, not closed. This register first guessed Natural
Earth, public domain. It is geoBoundaries gbOpen assembled per country: its
features carry geoBoundaries' own iso3/shapeGroup/shapeName, and the plugin's
data/adm0/PROVENANCE.json names the source and endpoint. That file also states
the real problem -- gbOpen passes each country's UPSTREAM licence through, so
one file carries up to 162 licences and up to 162 attribution duties -- and
its licences table is EMPTY. Which of the 162 are CC BY is unestablished.
Stated in ATTRIBUTION as an undischarged duty rather than papered over.
pplr's row is exp-shepherd's evidence: the PPLR ArcGIS org aggregating IMB,
Joshua Project, ROP and WCD identifiers, publishing no licence at all, and
WCD is a proprietary Brill database.
Both are MIT, which asks for the permission notice to be included with the
software. Tabulator's minified .js carried only "(c) Oliver Folkerd 2025" and
its .css carried nothing; Vue's banner names @license MIT but omits the
permission text. Each now has its upstream LICENSE vendored verbatim beside
it -- fetched, not authored, with the URL and sha256 recorded.
★ AND THE ALLOWLIST IS WHY IT WOULD NOT HAVE WORKED. generate-tree stages
vendor/ by an explicit PATH LIST, so the licences sat in data/vendor and never
reached the tree: staged count 0. Vendoring a notice the staging step drops is
not compliance, it is paperwork. Both LICENSE paths are now listed.
documentation/THIRD-PARTY.md is the register the sweep produced: every
non-original thing dx-maps ships, its source, its licence, whether the licence
or attribution actually travels, and the gap. Compiled by inspecting the
SHIPPED tree rather than the source lists, because the question is what a
recipient receives.
Four rows are left OPEN and marked as such. Two are unverified and need a look
(world-adm0's provenance, the pplr upstream). Two need a decision, not a fetch:
people-groups and the 304 derived tables carry data whose redistribution terms
are not established, 36 MB of it. Attaching a licence I have not confirmed
would be worse than the gap, so those go to the Driver. geoBoundaries is
closable by work rather than judgment -- CC BY needs attribution, and the
.meta.json sidecar pattern already exists here for one file of eight.
Two of the five shipped without one. bebas-neue/LICENSE was 24 lines and
poppins/LICENSE 29 -- our own notes files named LICENSE, pointing at
openfontlicense.org instead of carrying the text. The OFL requires the licence
to travel WITH the font, and these fonts go out in the repo we intend to give
away.
Both now carry their FAMILY'S OWN upstream OFL, fetched verbatim from
google/fonts, which is what brings the real copyright line with it -- Dharma
Type for Bebas Neue, the Poppins Project Authors for Poppins. No licence text
was authored here. Source URL and sha256 are recorded. figtree keeps its OFL
and loses only the six lines of ours that sat above it.
Our prose moved to documentation/vendored-fonts.md, OUTSIDE the served tree,
and that placement is the whole lesson. My first attempt put a NOTES.md beside
each LICENSE, which check:public-hygiene rejected as "documentation in a served
folder" -- three new hard findings, plus a fourth for an internal folder name I
had relocated with the text. The old bebas-neue LICENSE had said exactly this
in its own words, "prose does not belong in a public served folder"; I quoted
that line and then did it anyway. The licence travels with the font; the
explanation travels with the repo.
Whole shipped tree, measured before and after rather than sampled:
role nouns 6 -> 0, hygiene warnings 18 -> 14, findings 1 -> 1 (the pre-existing
assets/ contract one, not mine).
check:public-hygiene is the white-label gate for the giveaway, and translator-org-links.json
shipped an internal role noun: the held-Bible-Society reason read "...is the Driver's ruling, not
this script's". A recipient reading our data files should not meet our process vocabulary. It now
reads "a product ruling", which keeps the meaning exactly — the point was always that this build
will not decide a doctrine question on its own.
Fixed in the script that writes the table, not the artifact, or the next build would put it back;
the same phrase in that file's header comment went too, rather than waiting to be flagged
separately now studio has closed the comment half of this class.
Regenerated by re-running the verifier: 16 of 16 organisations answered again, so the table is
current rather than hand-edited. My published tables now carry no internal vocabulary — checked
with word boundaries across all of them, which is also how I learned the six "rega" hits reported
in my sweep were the word "aggregates".
The join table's never_read note said 'pending the Driver'; it now says
'pending a product decision' (check:public-hygiene, the giveaway's
white-label gate). Fixed in the builder that writes it.
dx-shepherd caught what I had just shipped: "full Bible", "New Testament" and "N parts" were
being generated in the BUILDER, so English was baked into data that no locale can translate and
no catalogue owns. The same mistake I have been fixing in other people's surfaces all day, made
one commit earlier by me.
The builder now ships FCBH's part count and nothing else — 27 on 400 codes, 66 on 41, another
count on 82, none on 367 — and the leaf names it through tOr(), so it localises the moment
studio adds three keys: gospel.res.newTestament, gospel.res.fullBible and gospel.res.nParts
with {n}. English inline until then, as always.
A count that is neither 27 nor 66 is still reported as itself and never rounded up to a
Testament, and no count still names only the source.
The Scripture chip named its source ("Faith Comes By Hearing") and stopped there, for 887
codes. FCBH publishes its own verse scope in the same record — au.vsc, "N parts, FIRST to
LAST" — and 567 of those 766 carry one: 400 New Testaments, 41 full Bibles, the rest partial.
A catalyst standing with a group needs to know whether that link is a whole Bible or two
books, and the data said so all along.
The chip now reads "Scripture · checked (New Testament — Faith Comes By Hearing)". 27 and 66
are named as Testament and Bible; anything else reports the part count FCBH claims and is never
rounded up. 367 sx codes still carry no scope — Scripture Earth's 104 and Door43's 17 publish
none in this record, and 47 FCBH rows omit it — and those say only the source, which is the
honest thing to say when the scope is unknown.
Found because go-int asked whether the FCBH text is a New Testament. It is a fair question to
ask of a map that had the answer in a field it never rendered. Their tree now reads the same
field and their green count went 128 to 195 across the roster.
Still FCBH's claim, cited, not counted book by book here — the same standard as the rest of
this corpus, and it is on the chip rather than buried in a builder.
A ?tree= link applied its node as soon as the tree file loaded; if that was
before the rows, the node's on-board languages were the empty set and the
grid restored to 0 (rega F1). The node now applies once both have loaded.
The New Testament rule also reads FCBH's own verse scope (people-group-pins
codes[iso].au.vsc, 27 or 66 parts), gospel-plugin's pointer: green 128 ->
195. Kyrgyz (33 parts) and three languages with no scope stay yellow.
The access sub-ladder follows the corrected pools (read and watch 869 ->
1,129, watch or hear cannot read 806 -> 518, read 57 -> 85). Nothing found
(369), own/bridge/zero and the band headline are unchanged.
1 THE CHIPS SAID "unchecked" ON VERIFIED LINKS. Card mode never fetches the verification table,
so every chip understated the evidence behind it — caught on central-sama, whose Scripture and
film are both verified. Each shard now carries the states for its OWN urls, keyed by url exactly
as linkVerify is, so vchip() and vtitle() work unchanged. Shards grew 8.0 KB to 14.0 KB mean,
29 KB worst; against 6,051 KB for the map that is a price worth paying for not lying about
evidence.
2 MY GATE'S FALSE NEGATIVE, which cost rega a round: it asserted
text.includes(slug.split('-')[0]) — "abai" against a rendered "Abai Sungai". A slug fragment is
not a name. It compares the roster's name, case-folded, anywhere in the card.
3 THE EMPTY GREY BAND. The sheet is a bottom panel capped at 62% of the map; in card mode there
is no map under it, so a phone showed 285px of grey above it. Full height in card mode, and the
border radius goes with it.
4 THE CLOSE BUTTON with nothing behind it — hidden in card mode, both instances of it.
gates:pre 11/11, LINK-COVERAGE 12,292 unchanged, SFC-PARSE PASS. rega spot-checks after landing
per dx-shepherd; the render claim is still not mine to make.
Third report of a blank card, and the root cause was never a missing field — it was two code
paths producing "the same" object. build-field-cards.mjs copied groupAt()'s 39 fields by hand
and missed door43, which exists only in that normalizer, so the sheet's
`!selectedGroup.door43.length` threw on undefined. I fixed a value, then a timing, then would
have added door43 and waited for the fourth.
The shard now ships the RAW INPUTS the map itself holds — the pin row, the whole code record,
the country name and the two bridge records — and the leaf's groupFrom() normalizes both
paths. groupAt(i) is a three-line wrapper over it. A field added to the sheet reaches the card
with no second edit, and forgetting one is no longer possible. The code record is passed WHOLE
rather than picked over, because picking fields is exactly the mistake above. Shards got
smaller doing it: mean 8.0 KB against 9.1.
Two sheet-level sources the card fetches nothing for are seeded from the shard: the prayer page
(prayIndex) and the SUN block. Both matter most to this persona — a catalyst with a Deaf group,
or one handing someone a prayer page — and both were silently absent while the card "worked".
Verified statically, since I cannot render: every one of the 28 fields the sheet reads off
selectedGroup is produced by groupFrom, so the two paths cannot disagree about any of them.
rega has the render.
The sheet tested scripture_url alone, so for every code whose readable text is NOT at c.s it
rendered "Scripture · none found". 887 codes keep theirs elsewhere — on the Faith Comes By
Hearing record, on Scripture Earth, or in a Door43 translation — and 332 field cards carry
one. A catalyst reading that chip was told there is nothing to read while the link existed.
The chip now links `sx` and NAMES its source, because a reader deserves to know they are
being sent to an audio site to read: "Scripture · checked (Faith Comes By Hearing)". The
verification state and title come from the same record as any other link, so nothing here is
claimed that the link record does not already support. The card carries sx for the same
reason — the tier that promises Scripture and the panel that shows it must agree.
Two placement mistakes of mine, both caught by checking the DATA rather than the diff: the
first edit matched `credit: c.cr || null` in the bridge helper instead of the card object, and
a str.replace with no match is a silent no-op that reports success. The card had no sx and
said so when I looked for it, which is why the second attempt asserted its anchor.
go-int cross-checked 20 roster languages with "no New Testament found in the pools" against
open-data signals. Thirteen of the twenty had readable text all along — on a record the tier
could not see — and chasing that found one rule written four times and wrong in two of them.
THE BUG. tierOfCode tested `!!c.s`, one field, while this build's own hasReadableScripture
tested four sources. Tier 2 means "Scripture AND film", so a language whose text is on the
Faith Comes By Hearing record (au.m names the media it holds), on Scripture Earth, or in a
Door43 translation was scored 1 — "some resource found" — with its own text and the JESUS
Film both in hand. Measured: 602 codes and 2,001 of 12,379 groups move 1 -> 2. Tier 0 does
not move at all, 1,592 before and after, so nothing about who has NOTHING changes: the gap
lists, the 369 zero-access groups and the bridge-reach table are untouched.
THE SAME BUG DOWNSTREAM, and this is the one a catalyst would have felt: resourceRows typed
an FCBH record "audio, text, video" as Audio Scripture only, so the access table's
byType.Scripture never saw it. 288 people groups were published as "watch or hear, cannot
read" while their own readable Scripture sat behind that link. Access now reads 1,129 "read
and watch" (was 869), 518 "cannot read" (was 806), 85 "read" (was 57), 369 nothing found and
6 stories only — unchanged where it should be, and 2,107 in total either way. Typing the row
by its text would have dropped "can hear", so the access builder now counts a Scripture row
whose scope names audio as hearable too.
ONE OWNER NOW. hasReadableScripture is exported from gospel-resource-rows.js and imported by
the pin builder and by check-gospel-coverage-consistent, whose arm F had its own narrow copy
and therefore reported 2,001 disagreements against the corrected tier — a gate defending the
bug it exists to catch. A rule with four copies has no owner; this one has.
`sx` ships alongside: the readable text with its source named, for the 887 codes whose text is
not at c.s, so a surface can LINK what the tier now counts instead of claiming Scripture and
showing a film. Wiring it into the sheet and the field card is the next commit; this one is
data and rules only, and public/ is deliberately NOT rebuilt because rega is rendering this
worktree.
Second failure on the same feature, and the cause was timing rather than values: the sheet
mounted on cardMode ALONE, so it read selected.country before the shard fetch had set it.
My previous fix put the right value in the right place and left it arriving too late.
The sheet now appears when there is DATA — v-else-if="selected || showGapList", with cardMode
removed from that condition entirely, because a card that has arrived sets both. A wait box
stands in until then and shows the error if the shard 404s, so a loading card looks like one
and a failed card says so instead of showing nothing.
And the card now carries the BRIDGE's own links. In card mode the pin index is never fetched,
so bridgeResources() found nothing and a tier-0 card would have named a way in with no way to
follow it — the single most important thing on that group's card. Derived in the builder
exactly as the leaf derives it, FCBH text fallback included, because a group's readable
Scripture is sometimes only on the audio record. Verified in the data: abai-sungai's card
carries Indonesian with its Scripture, film and Zume links.
NOT LANDING THIS until rega has rendered it on the branch. I have now shipped this feature
blank twice on checks that passed, and the missing check was always the same one: that it
renders. I cannot run a browser, so the claim is not mine to make.
BUILD-FIELD-RESOURCE-CARD. For the catalyst standing with the group on a phone. Seeing ONE
group used to cost the engine, the world boundaries, the basemap tiles and the 2.3 MB pin
index before a single link appeared.
?card=<slug> renders the group's sheet alone — no engine, no boundaries, no tiles, no pin
index. Measured from the served files: the map transfers 7,942 KB plus a ~700 KB engine and
unbounded tiles; the card transfers 678 KB worst case, of which 661 KB is the bundle itself
and 17 KB the largest shard. 92% fewer bytes before tiles are counted at all. The bundle is
now the floor, and splitting it is a separate job I have not done.
SHARDED BY A STABLE HASH, after two measured failures: first two letters of the slug gave a
120 KB worst shard because every "Deaf <people>" lands in "de"; three letters left 110 KB
because those slugs all begin "dea". djb2 low byte gives 255 even shards, mean 9 KB, max
17 KB, and the leaf recomputes the key so there is no index and one request. The identical
function lives in both files: if either drifts the card 404s rather than serving somebody
else's group.
Flat filenames, card-<key>.json, because internal/generate-tree.mjs stages tables/*.json as
files and throws EISDIR on a subdirectory — shared build machinery, not mine to change for
one plugin.
Share uses navigator.share where it exists (it offers whatever the catalyst already has) and
falls back to the clipboard, with a cancelled share treated as the non-event it is. The link
carries doxa.life's own slug, so what arrives reopens this card rather than a search — the
slug now reaches the sheet from a tapped pin too, so sharing works from the map as well.
Honesty unchanged: 1,738 cards carry something in the group's own language, 369 carry
nothing and say so with the way in. Checked 400 roster rows resolve to a card, 0 missing;
a tier-0 and a tier-2 card both carry what they should.
For an AI or Bible-translation team choosing where a first New Testament
draft is feasible: per language, five named open signals, each cited and
licensed: Glottolog's most extensive description (CC BY 4.0), Scripture
books found (eBible), a language Wikipedia (CC BY-SA 4.0), Common Voice
validated speech hours (CC0) and FLORES+ (CC BY-SA 4.0). A dated snapshot
the build reads; no live fetch. The board shows 'Open data (of 5)', a
sortable column (with the signals in the CSV), a drawer section with each
signal and its source, and a line on the tree card. It is a count of named
facts, not a score. Codes that could not be mapped are listed (6 Wikipedia
codes), and the 31 languages Glottolog lacks read as unknown, never none.
Of the 736 languages with no New Testament found: 20 have 3+ signals.
rega measured the Arabic packet (row 83) and found page 8 still English. dx-shepherd's rule, which this
follows exactly: NEVER machine-translate Scripture — use a published public-domain translation per locale,
cited with its name (END-DXS-MK5GO).
ar Arabic Van Dyck Bible ru Russian Synodal Bible es Reina Valera 1909
de German Luther Bible 1912 fr French Ostervald Bible
Fetched from bible.helloao.org, which ships licenceUrl + website + sha256 per translation and points at
ebible.org's own licence pages. Full provenance — translation id, name, licence URL, sha256 — is recorded in
data/people-groups/scripture-sources.json, because a credit line on the page can carry a NAME but not a
verifiable source, and the two are different claims.
★ VERSE NUMBERING VERIFIED AGAINST THE ENGLISH, not assumed. All five extract exactly verses 1-20 of 43 in
the chapter; verse 1 matches the WEB's "they came to the other side of the sea, into the country of the
Gadarenes" and verse 20 ends at "Decapolis ... everyone marveled". Lengths land within 1,689-2,399 chars
against the English 2,141, which is the spread you would expect across languages rather than a truncation.
The English passage carries no verse numbers, so these are joined as continuous prose the same way.
⚠ AND I DID NOT WRITE ANY OF IT FROM MEMORY. Scripture reproduced from recall is machine translation with
extra confidence — the same failure the rule forbids, one step removed. Every character came from the API
and was checked against the English before storing.
hi/zh/pt/it/ro STAY ENGLISH and stay in DECLARED_ABSENT, which is now narrowed rather than deleted: PD
candidates exist (Hindi 1851 Serampore-lineage, Chinese Union Version 1919, Almeida 1819, Diodati 1649,
Cornilescu 1924) but their licence status varies by jurisdiction and I could not confirm it. The entry
shrinks one locale at a time as each is cited.
VERIFIED: check:packet-strings-parity PASS, and the ar and de packets both render 9 sheets PASS with the
passage and its credit on page 8.
Driver, 2026-09-27, in his own page order. Nine pages now, was seven.
1 map the zone's vertical one-pager (unchanged)
2 LIFE CALENDAR 100 years x 52 weeks = 5,200 boxes — a whole human life on one page
3 THE SAME GRID shaded by the evangelical share of these groups' POPULATION
4 funnel now an UPSIDE-DOWN TRIANGLE with an equation sidebar and a "1 of 28" tip
5..n groups 20 per page with photos AND three prayer tick columns
then the 12 prayer points and the prayer wheel (both unchanged)
★ THE 12 PRAYER POINTS WERE NOT LOST. The Driver believed they had gone; all 12 are intact in
packet-strings.json with his rhythms and sub-titles — praise/Two-Cross Gospel, wait/Holy Spirit &
deliverance, confess, read/the Father's love, ask/baptisms, intercede/laborers & shepherds,
pray_word/Persons & Houses of Peace, thank/rest, sing, meditate/communion, listen/accountability,
praise_end/great-greater-greatest — plus the Mark 5 passage and the Metacamp attribution.
★ PAGES 2 AND 3 ARE ONE GRID READ TWICE, which is the whole idea. 5,200 is the Driver's own arithmetic
(100 x 52), not a layout choice, so both pages derive from it and cannot drift into two grids that merely
look alike. A percentage drawn as a bar is an abstraction; drawn on the grid the reader has just
understood as their own life, 32 shaded boxes of 5,200 is the same fact made unavoidable.
★ AND PAGE 3's NUMBER IS POPULATION-WEIGHTED, because he asked for "% of Christian population vs lost".
That matters: by GROUP COUNT 69.1% of the 2,091 matched DOXA groups have no known evangelicals, but by
POPULATION the evangelical share is 0.61% of 209,079,159 people. The group-count version is a materially
milder picture of the same data. Computed in data/people-groups/christian-share.json, joined IMB
CSV ROP3Code+ISOAlpha3 to DOXA's snapshot — the same join the Vision Card uses — and READ here, never
re-derived, so one fact has one source.
⚠ THE BAND MIDPOINT IS AN ASSUMPTION AND THE PAGE SAYS SO. peoplegroups.org publishes EvangelicalLevel
as a BAND ("Less than 2%", "No Known Evangelicals"), never a number, so any percentage requires choosing
a value inside each band. The file records the method, the page prints it, and 0.61 + 99.39 = 100 exactly
— my first formatter rounded the large side to "99%", which loses the asymmetry that IS the point and
leaves a reader checking the arithmetic to find the page wrong.
⚠ GEOMETRY MEASURED, NOT EYEBALLED: square cells would need 26.5px x 100 rows = 2,654px of grid in
1,521px of printable height, overflowing the sheet 1.7x. 13px rows put 100 rows in 1,498px with 23px
spare. Caught by computing the height before rendering.
⚠ AND THE TIP SAID "1 of 27". listCount spread the three cluster keys and omitted `deaf`, whose two
deaf_lists entries buildLegendNodes collapses into one leaf — the same omission allZones() had before
3ef164c0, and this was its last copy. It prints on the page, so a 27 there is a visible wrong fact.
THE GATE IS TAUGHT, NOT TOLERANT: the two new sheets are DETECTED (.sheet-life/.sheet-christian) and
added as known terms, exactly as the funnel was, because that assertion exists to catch silently dropped
people groups and a ±1 tolerance is the slack a lost list page hides in. New assertions: 5,200 cells per
grid, the shaded count neither 0 nor 5,200 (either means the percentage never arrived), and exactly
3 x cards prayer boxes. Probes scoped to .lc-grid — the legend swatches share the cell class and made the
count 5,201 on the first run.
The 17 new strings are English-only and DECLARED_ABSENT with reasons rather than back-filled: English
copied into ten locale blocks would read as translated to this gate and to a reader, and the per-key
fallback already renders English without the lie.
VERIFIED: check-packet-render PASS (9 sheets, map painted, content intact) and
check:packet-strings-parity PASS on the Tibet fixture.
Both halves of studio's L13U refusal were correct and both are mine.
1. THE LEGACY upg100WallIntro FALLBACK IS GONE. It existed for exactly one window — the three
plain-text keys were landing in studio's catalogue while this consumer was already written, so the
branch rendered the old markup-bearing key until they arrived. They are on main in all eleven
locales, so the condition can no longer be true: it is DEAD CODE that keeps a key alive, because
studio cannot delete upg100WallIntro while a line still reads it. A fallback whose condition cannot
fire is not safety, it is a reference.
The three remaining mentions in this file are COMMENTS, not code — verified that no live call site
anywhere in plugin-designs/ or library/ reads it, so it is now safe to delete.
2. THE GATE DEFAULTED TO A GITIGNORED FILE. check:zoning-declared read
library/scripts/posters/out/_records.json, which .gitignore:94 excludes, so on a clean checkout it
exited 75 — NOT MEASURED forever, indistinguishable from an unwired gate. studio measured exactly
that and correctly refused to wire it into gates:post. This tree has been here before:
check:packets-all SKIPped on every build for weeks for the same reason.
★ THE FIX IS A COMMITTED FIXTURE, NOT A LOOSER GATE. data/people-groups/zoning-records.json carries
slug plus every field a selector actually reads — 2,107 records, ~300 KB. country_code alone would
not do: the deaf zone reads rop1 and five African/European zones read wagf_block/wagf_region, which
is the same omission that made six zones silently resolve to nothing in the packet generator.
The live cache is still PREFERRED when present, so a local run measures the freshest data and CI
measures the committed one.
VERIFIED on a genuinely clean tree (git archive of the staged tree, no cache, no network):
PASS — 28 zones, 2,107 records, 2,096 covered, all five assertions green, exit 0.
check:i18n-keys-exist PASS, check:sfc-parse PASS.