Measured at ?lang=ar: the tabs render in Arabic and then the heading above the table
reads "Gospel Resources — every link, and who it reaches" in English, over English
column headers. The headers have had a lookup since cell E and it holds 13 of the 128
titles the 14 tables use; the TITLE had no lookup at all, so it came straight out of
the data file in every locale.
It is keyed by the tab's ID, not by its English text like the columns are: a title is a
sentence, and keying on it would break the day a builder rewords its own meta. The
English stays the fallback, so this renders exactly what it renders now until each key
lands.
The first version of this fix was wrong in a way worth recording: there are TWO paths —
the unfiltered table returns through localise(), the filtered one builds its own object
— and I patched the filtered path while measuring the bug on an unfiltered table. That
would have shipped a fix for the state a reader is rarely in. One resolver now, called
from both.
The exact key list is in _qa-cycle/03-deployment-cycle/KEY-REQUEST-gospel-tables.md for
studio: 14 titles and 113 headers, ordered by how many tables use each so a partial
batch still covers the most surfaces. Standards are excluded — ISO 639-3, ISO3, JSON,
CSV, ROP1 and SUN read the same in every locale.
?codes= filters by LANGUAGE, which is what a partner's list usually is and the wrong
filter for a list of peoples. The reach table measured the gap in its own rows: its
Mandarin row counts 36 groups, those 36 speak 30 codes, and 55 Doxa groups speak those
30 — so a link from a 36-group row drew 55 pins. Akha's 33 drew 57; SUN's 93 drew 116.
The extra groups are real and also have nothing; they belong to a different row.
So the map takes ?groups=<slug list> and filters by people group. The slug is
doxa.life's own — carried from the roster into the pin rows, and read in the builder
off the prayer URL their API built from it, never constructed from a name, because a
slug this build invented could name somebody else's people. All 369 of those rows
carry a prayer URL, measured before relying on it.
Row and map now agree by construction: 0 of 35 rows disagree, and the Mandarin link
renders "36 / 2,107" on the map against a row claiming 36. The gate asserts exact
equality for a three-slug link and 0 for an unknown slug — the failure that would
quietly turn a partner's 40-group embed into the whole roster.
The longest link is 1,577 characters, SUN's 93 slugs; well inside what browsers carry,
but worth knowing before anyone adds a row with a thousand groups.
A row reading "36 peoples" that cannot show you which 36 is a statistic, so each row
now carries the map's own ?codes= address — the partner-embed filter doing a second
job.
It would have contradicted its own row. The map filters by LANGUAGE and these rows
count GROUPS: Mandarin's 36 groups speak 30 codes, and 55 Doxa groups speak those 30.
Akha claims 33 and the link draws 57; SUN 93 and 116. The extra groups also have
nothing — they are offered a different bridge because they are in a different country.
So both numbers ship beside the link, the column is named for languages rather than
peoples, and the meta says why. Labelling a language filter as a people filter would
have been the easy version of this and would have made the map call the table a liar.
An exact per-group filter is a change to the map's URL contract and belongs there, not
faked in a builder.
Every surface here answered "who has nothing", then group by group "what is the way
in for THIS one". A mobiliser with a budget asks the inverse and had nowhere to ask
it: if I resource ONE language, how many peoples does that reach? Over the 369 groups
with nothing of their own the answer is 35 rows — SUN is the way in for 93 Deaf groups
across 93 countries, Mandarin for 36, Akha for 33, Indonesian for 28. That is a
different shape of decision from a list of 369, and it is the one a sending church or
a translation agency actually makes.
The three kinds are never summed. A CURATED bridge was checked for those groups; a
CANDIDATE is computed from published CLDR data about what the country speaks, which is
a fact about the country and not about these people; SIGNED is SUN, which has to be
learned. A language offered as both a checked bridge and a computed candidate appears
once per kind, because those are different claims — which is why there are 35 rows for
32 languages.
Every row carries "still nothing of their own" and the word bridge, because
aggregating makes that easier to forget, not harder. No new link enters the map: a
bridge language's Scripture, film and training are the ones the pin index already
holds for that ISO code, and all 80 URL cells resolve to links the verification record
has already proved — measured, not assumed.
The tab carries its own English until its key lands. tr() falls back to the key, which
on a tab would read "gospel.tables.t_bridgereach" — worse than the English it replaces.
Three findings, one thread: rule 4 says verify a URL before shipping it, and the
check I wrote for it yesterday was measuring one file.
1. THE TABLES PAGE WAS SHIPPING DEAD FORM. people-group-resources.json — the
"Resources by People Group" tab, which a reader clicks — carried 2,294 URL cells, of
which 1,412 were jesusfilm.org/watch/?languageId=<id>. That form is IGNORED by the
site: a real id and a bogus one render the same generic library page, which is why
the map migrated onto Arclight shareUrls. The migration never reached this table, so
the booleans beside those links were being refreshed while the links themselves went
stale for a year of builds. 1,417 film and 882 scripture URLs re-derived from the
index, which is authoritative for both and whose URLs are the verified ones.
2. TWO ARCLIGHT LANGUAGES, ONE ISO CODE — resolved, with evidence rather than a
ruling. jos was SKIPPED here, and the note said resolving it meant deciding whether
the code or the publisher's name was authoritative, "a data question for someone who
can check it". Checked: media-languages?iso3=jos returns 7138 "Jordanian Sign
Language" and 139080 "Lebanese Sign Language", and 7138's shareUrl redirects to
.../jordanian-sign-language.html while a bogus arc.gt code 404s. So the code stays
authoritative for the join and the NAME breaks the tie the code cannot. The old note
also had a fact wrong — it called Lebanese "a different language (lbs)", but lbs is
LIBYAN Sign Language and Lebanese has no code of its own, so this is two Levantine
varieties sharing one code, not a mis-filing. Three Deaf groups now reach the JESUS
Film in their own sign language through a link that resolves; 0 retired-form links
remain in the index.
3. THE GATE'S SCOPE WAS THE BUG. It walked the pin index alone, which is how item 1
hid from it. It now reads every table the SURFACES read, taken from the surfaces, and
resolves each URL against four records: the verification table, the org contacts, the
SUN pages (proved by content against a catch-all control) and doxa.life's own listing
for prayer pages, which cannot be proved by fetch because pray.doxa.life answers 200
for any slug. Provenance fields are excluded by path — a citation is not an offer.
8,445 links measured yesterday; 12,292 shipped links today, 0 unrecorded, 0 broken,
0 that no status code can prove.
Reading every STAGED file instead reported 6,115 unrecorded and 53 broken, all of
them correct behaviour: API endpoints in source fields, and build caches that still
hold the 53 share URLs the verifier proved dead — which is exactly why the pin
builder drops them. A gate that goes red on correct behaviour gets switched off.
gospel-internet was running the same point-in-polygon over the same two geometry
files and refusing to write unless its per-province counts matched this table's —
which is good discipline and should not be necessary. The assignment ships now: one
row per placed group, 1,005 of them, with the language code, population and tier.
The two files are reconciled BEFORE either is written — per-province counts and the
assigned total must agree or the build refuses — because they are published
separately (the map fetches only the aggregate, lazily, and has no use for a
thousand rows) and separate files can drift. The failing direction of that guard is
not proven, only the passing one: 46 provinces agree, 1,005 = 1,005.
check:composite-freshness found stale on main: all-people-groups
big-picture
dashboard-plus-grid
my-upg-100-list
people-groups-grid
regions-dashboard
research-dashboard. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
check:composite-freshness found stale on main: all-people-groups
big-picture
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
people-groups-grid
pplr-data-maps
regions-dashboard
research-dashboard
research-map-plus-grid
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
The six public/ files the merge left holding main's side are regenerated here
rather than merged, because a bundle is an OUTPUT: the only correct content is
whatever `BUNDLE=my-upg-100-list vite build` emits from the merged source, and
any hand-reconciled middle version is wrong by construction even when it looks
plausible in a diff.
fix:sri then rewrote seven stale hashes across five files. That summary is NOT
the evidence: the three leaf integrity attributes were re-derived independently
here by recomputing sha384 over the emitted bundle and comparing to what each
HTML claims — 3 recomputed, 0 mismatches. This is the check that was skipped
when three consecutive commits shipped leaf HTML pinning a hash for a bundle
that no longer existed; the browser refused the script and every one of those
pages was blank, while the build's own output had reported success.
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-maps-spa
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
plugin-studio
pplr-data-maps
regions-dashboard
research-dashboard
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
The brief's third requirement at the grain the province work added: clicking a place
filters the table to it. Same split as the country and language links — a filter when
the tables are already beneath the map, a link when they are not.
The province key is a NAME, not a code: geoBoundaries' shapeName is what the table
joins on. So it is matched with diacritics folded, because a link that only works for
whoever copied it off the map is not an address — ?province=Arunachal Pradesh finds
"Arunāchal Pradesh". An unknown province matches nothing and says 0, rather than
falling back to every row.
The embed gate grew two arms for it and the first one failed for a reason that was
mine: it sliced the page text to 300 characters and then asked whether the province
name was in it. The row was on screen, 400 characters down, and the gate called the
product broken. A slice belongs in the message, never in the predicate.
Driver-found, rega-measured: under ar the five map controls mirror to the LEFT
while `left:8px` kept the legend physically left, so the legend sat underneath all
five of them. cities-internet shares the gm-legend class and passed for one reason —
it anchors inline. Every direction-dependent physical property in this stylesheet is
logical now: the legend anchor, the search clear button, the accent borders, and the
header's push-right.
Measured at 1440x900 against six controls: ar legend x=1052 with 0 overlaps, en
unchanged at x=8 with 0 overlaps.
Then the Latin rega saw. Once the explainer is folded away, exactly one sentence was
left in English, and it is the legend's only piece of NEWS — that 366 of the 369
groups with nothing have somewhere to start. It goes through tOr() with its numbers
as parameters, so it renders the English today and switches when the key lands. The
caveat trailing it ("a way in is not the gospel in their own language") moved into
the fold, where the rest of the basis lives: it qualifies the news rather than being
it. What is left in an Arabic legend outside the fold is now exactly the two strings
whose keys I have asked studio for, and nothing else.
A country is not where anyone is sent; a state is. Each province now carries the
same two prayer numbers the country sheet does and ends in the same move — the
largest group there with nothing and nobody praying, one tap from its prayer page,
ranked need-first so drilling from country to province never hands the reader a
second answer. Andhra Pradesh: 64 of its 76 groups with a prayer page have nobody
praying, 3.8M people.
Always N of M. doxa.life publishes a page for 1,723 of the roster, so nobody-praying
can be said of the groups that HAVE one and never of the province; both numbers
ship together so no surface can quote the first alone.
The join here is name+country, not language — you pray for a people in a place, and
that is doxa.life's own key. It is the one join in this build where a name match is
correct rather than forbidden, and the slug is always theirs: pray.doxa.life answers
200 for any path, so a constructed one would look valid and be empty.
The doctrine screen now scans the province table too. It is the SIXTH place a URL
lives in this bundle, and a table added after the scan was written is exactly how
the access table went unscreened for weeks — read from disk, so it is checked even
when the province layer was never opened.
This table carried its own worst case as a caveat: the ways in were chosen for the
COUNTRY, so Arunachal Pradesh — in the far north-east — was offered TELUGU, a South
Indian language, because the curated bridge table is keyed by country and a curated
bridge outranks a computed one. Manipur and Kerala got Telugu too.
The province answers for itself now. Candidates are the languages of groups
STANDING IN IT that already have Scripture AND film, ranked by the people here who
already hold them. Arunachal Pradesh gets Hindi and Assamese, Manipur gets Meitei,
Kerala gets Malayalam — each one a measured neighbour rather than a national
average. 41 of 46 provinces can answer locally; the other 5 keep the country answer
and the new Basis column says which is which, because "no language here has both
Scripture and film" is itself worth reading. The country-level answer stays in its
own column so the two can be compared rather than swapped silently.
It is a NEIGHBOUR, not a recommendation: whether a group actually speaks it is a
question for someone on the ground, and the table says so.
The fold's summary is a CONTROL — a reader in Arabic could not tell what was behind
it — and "This is a sign language." is the sentence that stops eBible's silence
being read as an absence of Scripture for 150 sign languages and 84.2M people.
Both go through tOr(), so they render today's English until exp-shepherd's keys
land and switch to the catalogue with no further change here.
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
pplr-data-maps
research-dashboard
template-bundle
wall-of-people-groups; also rebuilt: regions-dashboard big-picture upg-neighbor. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
resources-map resolved its engine as `engineGlobal(provider) || window.mapboxgl`.
engineGlobal already defaults to mapbox, so that fallback could only ever fire for
provider 'maplibre' — and then it returned MAPBOX. A page that chose the keyless
engine silently got the one that needs a token, and because the profile only
assigns accessToken when the provider is not maplibre, the substitute had no token
either: it would have 401'd on every tile. The fix is the deletion. The requested
engine or a named failure, with a console warning for whoever is debugging and the
reader's line through messages.* (English kept inline until the key lands).
check:engine-contract measures it: two providers across engine present, engine
blocked and both blocked, with the CDN blocked by request routing the way a proxy
or an offline kiosk does it. Three of those six arms first failed for reasons that
were mine, not the product's, and each is written down in the gate: Mapbox cannot
mount at all on a static tree without a token, 11s was not long enough for a leaf
that fetches 4 MB before the map exists, and engine attribution read from a debug
handle scored Mapbox-with-a-bad-token as "engine null" — the canvas class cannot
lie about who drew it.
Also in this landing: the pin caveat is translated in 11 locales from the shared
pins.* area, read through this leaf's own guarded t() rather than by mounting
library ApproxPinNote.vue — that component calls useI18n() unguarded, and this
bundle is embeddable in hosts that never called app.use(i18n), where an unguarded
call throws during setup and the shadow boundary swallows it. Same wording either
way. Both "Show all groups" buttons now use the key that already exists in 11
locales, and the engine's own control labels get the provider arm.
check:composite-freshness found stale on main: all-people-groups
big-picture
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
pplr-data-maps
regions-dashboard
research-dashboard
research-map-plus-grid
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
This map ships Doxa's roster, and the point of a public corpus is that other lists
can stand on it. A partner could already embed the map; their reader then had to
go hunting for their own people among 2,107 dots.
?codes=kfr,cmn,npi draw only those languages, framed on them
?country=IND open that country's sheet — this did NOTHING before unless
a ?code came with it, so the brief's second requirement had
no address, which is exactly what a partner embeds
?view=zero unchanged
The join is ISO 639-3, so a partner's list needs no agreement with ours — only the
codes their own data already carries. It filters and never invents: the notice
reads "74 / 2,107" and is itself the button back to the whole map, labelled with a
key that already exists in 11 locales. A link matching nothing shows an empty map
reading "0 / 2,107", because showing the full map instead would tell a partner
their broken list works.
check:embed-contract gates the promise. Written against the pin index first, it
demanded "12 / 12,379" of a map correctly saying "0 / 2,107" — the gate was wrong
and the map was right, so its numbers and its probe codes now come from the file
the leaf itself reads.
studio's APPROXPIN (dee9f9c9) moved library/ — legends, PinHint, useMapInstance,
i18n — so the rebuild was certain rather than conditional this time. Checked, as
always, with `git diff --name-only <base> <main> | grep '^library/'` rather than
assumed: that test was EMPTY on one earlier merge and NON-EMPTY on the next two,
and it is the only thing that decides whether the bundle must follow.
All conflicts were DERIVED artifacts again — my two leaf HTMLs and the manifests,
zero source conflicts. Resolved to main's fresh build, then regenerated from the
merged source, because picking a side of a generated file is meaningless.
VERIFIED BY HASHING, the step whose absence shipped a blank page: fix:sri rewrote
the two manifests, and I then recomputed sha384 over the actual bundle —
upg-100-list, upg-100-mfe and poster-wall all MATCH, whole-tree sweep 57 integrity
attributes with 0 mismatches. fix:sri, not tree: studio had already regenerated the
pages and the gate itself warns that tree re-reads .vue source on a shared branch.
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-maps-spa
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
plugin-studio
pplr-data-maps
prayer-walk-planner
regions-dashboard
research-dashboard
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
This leaf calls `new gl.Map` directly rather than going through useMapInstance, so
it never inherited the locale map the shared composable passes: under Arabic every
string we own was Arabic and the engine's own were not — "Toggle attribution", and
the canvas's accessible name, which is what a screen reader announces the map as.
The strings were already in the bundle; nothing was handing them to the engine.
Measured that way: the Arabic mapui catalogue is present in the built bundle both
before and after, while ENGINE_UI_KEYS — the map from the engine's key names to
ours — appears only after. Set at construction, the only time either engine reads
it. Needs eyes to confirm on screen.
The one English legend tooltip now goes through t(), with the English kept in the
template as the fallback so it renders exactly as it does today until the key
lands, rather than showing "gospel.legend.hnrTip" in a tooltip.
studio/mirror (c85eeee7) rebuilt every bundle and touched library/ —
PinHint, LegendDesktop, LegendTools, MapControlMenu, ShareButton, PosterDialog
and others — so unlike the previous merge this one genuinely required rebuilding
my bundle rather than just re-hashing it. Checked rather than assumed: the
decision of whether to rebuild turns on `git diff --name-only <base> <main> |
grep '^library/'`, and last time that was empty and this time it is not.
ALL SIX CONFLICTS WERE DERIVED ARTIFACTS — my bundle, my three leaf HTMLs and the
two manifests — and ZERO source files conflicted. Resolved to main's freshly built
versions and then regenerated from the merged source, because picking a side of a
generated file is meaningless: neither side is authoritative, the source is.
VERIFIED BY HASHING, which is the step whose absence shipped a blank page three
commits ago. fix:sri rewrote files this time (it reported nothing to do last time
only because `tree` had already fixed them), so the summary alone would have been
even less informative. Recomputed sha384 over the actual bundle: upg-100-list,
upg-100-mfe and poster-wall all MATCH, and a whole-tree sweep is 57 integrity
attributes with 0 mismatches.
Used fix:sri and NOT tree, per the gate's own advice — tree re-reads .vue source
and on a shared branch can bake in another agent's half-finished work. studio had
already regenerated the pages, so there was nothing to regenerate.
BLAST RADIUS: the only bundle differing from main is mine. Pins-first source
confirmed intact after the merge (boundaryPromises, paintPinsEarly, the deferred
boundary await).
Every number on the country sheet is work for somebody else to fund, staff or
translate. The one thing the reader can do before closing the tab was reachable
only by guessing which pin to tap, so the sheet now names it: the largest group in
this country with nothing and nobody praying, one tap from its prayer page.
Need first, then size, and India shows the cost of that order — of its 601 groups
with nobody praying only five have nothing at all, so it offers Malai Pandaram at
8,700 people ahead of Badhai at 851,000, who already have Scripture and film. The
sub-label says which rung the reader was handed.
The count above it reads "601 / 779", never a bare 601: doxa.life publishes a page
for 1,723 of the 12,379 groups here, so nobody-praying can be said of the groups
that HAVE a page and never of the country. Zero still means zero on doxa.life's
counter, not a claim about the world.
Costs 1.05 kB: the prayer pages and the tabs catalogue were already shipped.
MY LAST THREE COMMITS SHIPPED A PAGE THAT COULD NOT RUN, and rega caught it: the
committed bundle hashed to sha384-k1mPDt9L… while every leaf's integrity attribute
still pinned sha384-GEhDNkbg… — the PREVIOUS bundle. The browser refuses the script
outright, so 6 of 6 of their runs loaded only the html and the js and never created
a map. A blank page.
I RAN `vite build` FOUR TIMES AND NEVER RAN THE OTHER TWO STEPS. dx-shepherd's
recipe is build -> tree -> fix:sri, and I ran the first third of it and reported
success each time because vite exited clean. It did — the BUILD was fine. What was
broken is the ARTIFACT PAIR: index.html and my-upg-100-list.js are two files that
must agree about a hash, and "vite exited 0" says nothing about whether they do.
That is the same measure-the-artifact-not-the-property mistake I have spent this
whole session finding in other places, committed four times in a row in my own
build step.
FIXED PROPERLY THIS TIME:
· merged main (d3d3c3f8). rega also flagged the stale base — the branch was on
c2af6b16 and missing main's catalogue trimming, which is why the bundle was
1,026,943 B against main's 811,632. After the merge it builds to 792 kB.
· the ONLY merge conflict was public/dx-maps/my-upg-100-list/my-upg-100-list.js,
a DERIVED artifact — so neither side is authoritative and picking one would be
wrong on principle. Regenerated it from the merged source instead.
· then `bun run tree`, then `bun run fix:sri`.
VERIFIED BY HASHING, not by trusting the tool's own summary. fix:sri reported "115
already agreed, rewrote 0", which is only reassuring if `tree` had already fixed
them — so I recomputed sha384 over the actual bundle and compared it against every
leaf that loads it: upg-100-list, upg-100-mfe and poster-wall all MATCH. Then swept
the whole tree: 57 integrity attributes checked, zero mismatches.
AND CHECKED THE BLAST RADIUS, because `tree` regenerates every leaf's HTML: the
other bundles under public/ (all-people-groups.js, big-picture.js) are byte-identical
to main — the merge brought main's versions, my rebuild did not touch them. Only my
own bundle differs, as it should.
NOT SIMPLIFIED YET, for the record: the merge brought gb-IND-ADM1-full.geojson
(1,796 KB) as an ADDED higher-detail variant. The SERVED files are unchanged
(world-adm0 1,866 KB, gb-IND-ADM1 1,628 KB), so the 3.6 MB parse cost still stands
and the boundary-simplification work is still pending elsewhere.
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
prayer-walk-planner
regions-dashboard
research-dashboard
template-bundle. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
check:composite-freshness found stale on main: all-people-groups
big-picture
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
people-groups-grid
pplr-data-maps
prayer-walk-planner
regions-dashboard
research-dashboard
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
rega's BASELINE TRACE OVERTURNED MY OWN PROFILE, and this commit is built on their
measurement rather than my arithmetic. Baseline at main ccc2b1bb, tokenless
MapLibre, software GL: every key byte arrives by 1.2-2.9 s, and the first territory
draws 5.6-6.4 s LATER. Only ~2.6 s of that gap is main-thread long tasks. The rest
is MapLibre's WORKERS parsing and tiling ~3.6 MB of boundary GeoJSON.
I HAD PROFILED FILE SIZES AND REQUEST ORDER — the artifact — and concluded the
download was the cost. On localhost those 3.6 MB arrive in ~230 ms. Reordering
requests was never going to reach 3 s, because PARSING is the bottleneck, and my
own two-pass-zones plan would have been just as blocked: resolving even the 15
record-free zones still needs world-adm0 parsed. dx-shepherd dropped it in favour
of pins-first (ENDTOKEN-DXS-UPG-PINSFIRST). Right call.
THE PINS NEED NONE OF THE GEOMETRY, verified in the code rather than assumed: they
are built from `records` (lat/lng) plus slugToZone, and the evaluate() in the pin
build maps slugs to zone keys without touching a boundary file. 68 KB, arriving at
~2.9 s cold.
SO THE BOOT ORDER IS NOW: records (68 KB) -> build pins -> PAINT PINS -> await the
boundaries (3.6 MB parse) -> tapestry -> search index -> full repaint.
Three things make that safe rather than clever:
· all three boundary downloads are still started BEFORE any await, so nothing is
serialised — they fetch and parse while the pins are being drawn;
· paintAll now paints WHAT IS READY instead of refusing until everything is. Its
old guard `if (!map || !tapestry) return` was what forced the pins to wait on
the parse. Every step below it is idempotent and guards its own source AND
layers, so calling it twice adds each thing exactly once — which is only true
because of ea90e9b2;
· `tapestry` STAYS WHOLE-OR-NULL. Nothing paints a partial tapestry; the pins
simply stop depending on one. boundsFor(), the search index and the status
telemetry keep their invariant, so a reader can never search or fly-to against
a half-built index. That was the correctness constraint dx-shepherd and I
agreed before rega's trace, and it survives the change of plan.
The early paint is FIRE-AND-FORGET: it uses the same style-readiness rule as the
boot gate, and if the style is slow the pins simply appear later while the boot
gate repaints everything regardless. It must not delay the boundary await.
⚠ NOT MEASURED BY ME. Every claim about timing here is rega's; I have only verified
that the code order now matches the plan and that the build is clean. Sending them
branch@sha for the same cold trace, and the honest comparison is baseline-cold vs
this-cold in THEIR environment — software GL inflates rendering, so the absolute
numbers are not the point.
ALSO CONFIRMED BY THEIR TRACE, about the previous commit: the list API sends
`public, max-age=14400`, so dropping cache:'no-store' is a real win and not a
guess — the warm reload was still refetching 68 KB over the network, which fits a
client-side no-store exactly.
BUNDLE=my-upg-100-list vite build clean; check:packet-strings-parity PASS.
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 BOOT CHAIN WAS STRICTLY SERIAL and the sizes are the story. Measured
2026-09-26: `await Promise.all([3 boundaries])` and only THEN `await fetch(API)`,
so 3,549 KB of geojson — world-adm0 1,866, gb-IND-ADM1 1,628, gb-CHN-ADM1 54 —
had to download AND JSON.parse before the API request was even issued. rega
measured the first paint at 9.5-16.5 s, which is why a 7 s screenshot wait caught
a blank map.
THREE CHEAP WINS, no restructuring of the paint itself (dx-shepherd
ENDTOKEN-DXS-UPG-PERFGO, cheap first):
1. THE API REQUEST IS ISSUED FIRST, NOT LAST. Nothing needed to build that URL
depends on the boundaries, so the fetch starts immediately and is awaited where
it always was — the rejection still surfaces at the same place in the same
try/catch. Creating it early changes WHEN the request goes out, not where its
failure lands.
2. gb-IND-ADM1 (1,628 KB) IS OFF THE CRITICAL PATH. Only the India pg_slugs zones
need it, and they are resolved well after the first layer could be drawn. It now
downloads alongside the other two and is awaited beside the records.
⚠ AND THE ORDER OF THAT LINE IS LOAD-BEARING. My first version put the
getJSON() under the Promise.all and it overlapped NOTHING: a promise created
after an await does not start until that await resolves, so the code read as
three parallel downloads while behaving exactly as serially as before — a
parallel-looking shape with serial behaviour. Caught by checking the line
numbers rather than the intent.
3. `cache: 'no-store'` DROPPED from the boot fetch. It forced a full ~870 KB
re-download on every reload and could never hit the HTTP cache. This list
changes on the order of days, not within a session, and the default lets the
API's own headers decide — which is the server's call rather than this client's.
⚠ The other three no-store fetches are DELIBERATELY LEFT: packetRecords and
selectionAsList are user-triggered (a reader asking for a packet should get
today's data), and enrichLegendFromRecords runs only on the degraded paths where
no engine or no token means the map pipeline never ran at all — so it is a
fallback, not a duplicate of the boot fetch. Verified both call sites.
The blocking await is now 1,920 KB instead of 3,549 KB plus a serial API round
trip, with three requests in flight from the start.
⚠ NOT MEASURED AT THE RENDERED LAYER. Every number here is file sizes and code
order; the 9.5-16.5 s is rega's. dx-shepherd is having rega take a BASELINE trace
and a second on this branch so the gain is measured rather than inferred, and I
would rather that number came from them than from me. The two-pass paint (15
record-free zones first, then the 12 pg_slugs zones and the pins) is next and
deliberately a separate commit.
BUNDLE=my-upg-100-list vite build clean.
`if (map.getSource('tapestry')) return` returns having added NOTHING whenever the
source outlives the layers — and that state is real, not hypothetical: setStyle()
wipes LAYERS, which is why paintAll is re-armed on 'style.load' at :1625. The
re-add is a genuine code path (the light/dark toggle) and this guard could silently
no-op it, leaving a map with a source, no polygons and no pins.
addPinLayers had the identical bug on getSource('upg'), which is why territories
and pins would disappear together: one mistake, made twice.
ensureSelectionLayers():652 ALREADY HAD THE RIGHT SHAPE — `if (!map ||
!map.getSource('tapestry')) return; if (map.getLayer('upg-sel-fill')) return`. The
correct pattern was sitting in this file and its two siblings did not copy it.
Presence of a source and presence of the layers drawn from it are two properties;
a function whose job is the second must not test the first.
EACH LAYER IS GUARDED SEPARATELY because addLayer THROWS on an id that already
exists — a half-built style is exactly the state this fix is for, so re-adding a
survivor would abort the function and skip everything after it.
⚠ THIS IS NOT THE CAUSE OF THE BLANK ENGLISH README SHOT, and I am recording that
because I diagnosed it as such and was wrong. I had a confident chain — source
survives layers, half-built style, hence en blank while ar painted — and started
fixing it as the cause. rega settled it with the measurement I did not have: the
leaf paints 9.5-16.5 s after load and the screenshot tool waited 7 s. A timeout,
not a defect. The chain explained the symptom without being true of it, and
claiming this commit fixes that shot would be exactly the false attribution I have
been correcting in other people's reports today.
Kept on its own merits. BUNDLE=my-upg-100-list vite build clean.