A retake that came out byte-identical left the PNG unchanged, git dated it by
its old commit, and the gate called a correct, just-taken picture stale. No
clock can see that case. readme-shots now writes shot-bundles.json (shot ->
sha256 of the bundle it was taken from), and check:shots compares that hash
with the bundle built now; dates are only the fallback for unrecorded shots.
Negative-tested: one byte appended to the doxa-simple-map bundle fails exactly
the three cap2 shots; restoring it passes.
Also retook the eight shots a rebuild had made stale since e2beec9.
Inside a code span a backslash renders literally, so the sentence read
"| tail" with a stray backslash. The escape inside the table is kept:
there a bare pipe would split the cell.
f63d75e (plugin studio's Explorer tree) rewrote library/layouts/
VerticalTabsLayout.vue, which is compiled into doxa-maps-spa.js, and did not
rebuild the gallery. This rebuilds it. check:gallery-embed and the maplibre
variant both pass, js 13 + iframe 13, with the new layout.
readme-shots now clicks the rail's first plugin row (.st-row[role=treeitem],
not a section heading), keeping .vtl-tab as a fallback. Without that, the walk
fell through to the nested gallery's own tabs.
e3df467 changed library/layouts/VerticalTabsLayout.vue, which is compiled
into doxa-maps-spa.js, but did not rebuild the gallery. So the bundle on main
was stale: new source, old bytes. check:sri cannot see this, because it only
proves the leaf page and the bundle agree with each other.
Measured, not assumed. Rebuilding fd95518's source reproduces the landed
bundle byte-for-byte (sha256 2856a1e2…; the build is deterministic). So the
gallery landings themselves were current. After this rebuild the bundle
contains the new layout code (branchOpen: new 1, landed 0).
check:gallery-embed and its maplibre variant both pass, js 13 + iframe 13.
The studio's rail was hand-rolled vertical tabs stretched into a tree.
It is now an Explorer tree modelled on the VS Code Explorer, keyboarded
per the WAI-ARIA tree pattern:
- CAP 5 -> CAP 1 sections with sticky headers; one-line rows with the
name and a muted element tag; twisty on the left; indent guides.
- Composites fold; their parts hang beneath, one level at a time, each
part opening the plugin's one canonical entry.
- A filter box on top shows matches together with the path to them.
- One view action, Collapse all.
- Auto-reveal: opening a plugin by click, URL hash or remembered tab
scrolls its row into view and opens its section.
- Arrow keys walk the tree, Right/Left open, close and move between
parent and child, Enter opens, a letter jumps to the next name.
- The slim rail becomes an activity bar with one icon per section.
- Fold state is remembered per page.
The tree lives in plugin-designs/plugin-studio/src/StudioTree.vue and
fills VerticalTabsLayout's new #rail slot, so the layout still owns the
collapse toggle, mobile drawer and slim strip. The alias, depth and
branch row fields added to the layout earlier today are removed again:
nothing else used them.
The studio host now has a default height (85vh, min 480px), as
doxa-maps-spa does. Pasting the published embed snippet into a plain
page mounted a complete studio 0px tall. A page's own height or
--plugin-studio-height still wins.
The studio gates that click tabs by text now also match tree rows.
Rebuilt the plugin-studio bundle and re-baked its SRI.
Three audio-Scripture sources were investigated this tick and none is
shippable. Writing down WHY, beside the unchecked-pool list, so the
next agent does not spend a tick rediscovering it — each was reachable
and doctrinally sound, and the reason is always coverage or the join
key, never suspicion.
AudioTreasure publishes 9 languages and every one is major; zero
overlap with the gap. Davar is a ministry site rather than a
catalogue, with no per-language page and so no URL a worker could be
sent to. Talking Bibles is the interesting one: 177 real audio Bibles,
open robots.txt, but the player keys languages by an internal numeric
id and publishes no ISO code, so a join could only be by NAME — which
this map refuses, because names collide across unrelated languages. It
turned out not to matter: of the 81 whose names match a language we
carry, ZERO are in the gap, so even a perfect ISO join would have
added no coverage.
GRN is the contrast worth keeping next to them — equally name-shy at
first glance, but 5fish resolves by ISO 639-3 directly, which is why
it is in and these are not.
regions landed the pin hint's i18n — useI18n, messages.pinHint.tapDot in 11
locales, English fallback, pinHintLabel first. Thirteen bundles embed that
component and only ONE of them had been rebuilt, so twelve maps still shipped the
old hardcoded English literal in their served .js.
check:sri passed the whole time, and correctly: every leaf matched the bundle it
loads. A stale bundle and its stale leaf agree perfectly — consistency is not
freshness, and no gate in the chain compares either against source.
MEASURED after rebuilding the eleven that were stale (country-posters and
regions-dashboard were already current):
es Toca un punto de color: alguien todavía necesita oración
fr Touchez un point coloré — quelqu'un a encore besoin de prière
ru Нажмите на цветную точку — кому-то всё ещё нужна молитва
Every one of those was the English sentence before this commit.
The same rebuild carried the legend's a11y fix to the two bundles that had no
owner watching them. All four bundles that embed SemanticTreeLegend now carry
legend.a11y.collapseRow; on doxa-research-map the row carets read "Expand Asia
(Region)", "Expand Afro-Asiatic", "Expand Arab World" — 14, 92 and 34 named
controls across the three tree tabs, with ZERO raw i18n keys, which is the defect
the commit before it fixed. The nine bundles WITHOUT that key were checked
separately and embed no such legend, so their zero means not-applicable rather
than stale.
Verified not-stale the only way that works here: every one of the thirteen now
contains both current source markers (the tapDot key, and the sheet-clearance
logic — 71 hits in the four that also embed the mobile legend). A build that had
failed silently would show tapDot=0, which is exactly how the original
twelve-of-thirteen gap was found.
gates:pre 7/7, gates:post 42/44 with the two known precondition skips.
A pipeline's exit status is its last command's, so a build piped
through tail reports success when the build failed — and the chain
runs on, publishing a stale bundle with new source and an old .js.
Every gate stays green afterwards, because the SRI check proves the
leaf and the bundle agree with each other, not with the source.
Measured all three forms on this machine rather than repeating the
warning: the piped form reports success on a deliberately broken
build, set -o pipefail catches it, and redirecting instead of piping
catches it because the status is then the build's own.
The same shape already bit a merge here: "if ! git merge ... | tail"
tested the pipe, so a conflicted merge read as clean and the
resolution step was skipped.
Both tab components' file headers said the region grouping was a COPY of
DX-life's countries-meta.ts "not an import", one of them attributing it to the
other tab. That was true when it was written — their file is a .ts module with
a Nuxt-only COUNTRIES import that cannot resolve outside that app — and it
stopped being true when the file was ported to library/utils/countries-meta.js.
Both tabs import summarizeRegions and PEOPLE_GROUP_LIST_FIELDS from there now,
and grep finds exactly one definition in the repo.
Worth the edit because a comment that still describes the old shape is worse
than no comment: the next reader would have duplicated the rollup a third time
on its authority, and one of the headers told them to copy it from the other
tab. "Kept field-for-field identical so both tabs roll up the same way" is also
no longer a discipline anyone has to maintain — it is the same function.
A dropdown every localised surface renders had no t() at all. Its two visible
strings — the clear button and the empty state — were English literals, and one
of them, "No options found", was ALREADY catalogued in eleven locales elsewhere
in this repo: the translation existed and the component was ignoring it. The
poster wall's export modal had the same shape, reassuring the reader that
nothing is uploaded in English only.
Three keys in buttons.json across eleven locales, and the literals replaced.
Both bundles that render these components — regions and country-posters,
whose cards are routed to me — are rebuilt. check:i18n-parity passes with no
locale regressed.
AND A BUILD HAZARD IN MY OWN COMMAND SHAPE, worth writing down because it is
exactly the failure the brief warns about. I ran
BUNDLE=x bun x vite build 2>&1 | tail -2 && bun run tree && bun run fix:sri
The vite build FAILED on a syntax error — I had put a const inside
defineProps — and tree and fix:sri ran anyway, because the exit status of a
pipeline is the LAST command's, and `tail` succeeded. So the chain regenerated
the tree and re-hashed against a bundle that had not been rebuilt: the "tree
alone ships a stale .js" trap, opened by the pipe I added to keep the output
short. Caught because the next build's byte count moved, not because the chain
complained. Any agent using `| tail` after a build needs pipefail or a separate
status check.
170 more gap codes, 84 with gospel audio. 427 codes now carry a Words
of Life link; 603 people groups whose only resource in any pool is
that recording — 76.4 million people — can hear the gospel in their
own language. Tier 0 falls 2,373 to 2,270, from 3,457 when this began.
859 of the gap's codes decided, provenance exact at 752 claiming GRN,
752 decided, none decided-but-unnamed.
Also measured, rather than assumed: the map really does filter the
table beneath it. Composed on the dashboard, opening Foodo and
pressing "Every resource in Foodo ↓" cuts the table from 7,762 rows to
1 — Foodo has exactly one resource, the Scripture Earth PDF. That was
claimed by the brief and had never been checked end to end.
The translation-priority table tracks the sweep: 2,458 groups now,
up from 2,355, because every code GRN reaches moves a group out of
"nothing found" and into "can hear it, no text".
On any third-party page the whole clone rendered in a fallback face, and this
is why. useDocumentFont injects a <style> into the HOST document, so a RELATIVE
src resolves against the HOST PAGE rather than against this bundle. The leaf's
generated config says "../../assets" and the font loader was handed that value
raw. On our own leaf it is invisible — the leaf really is two directories deep,
so it resolves to /doxa-maps/assets and works. On a partner page it resolves
against the partner's URL, the file is not there, and every face fails.
resolveAssetsBase exists for exactly this. Its own header documents this bug
class in these words: "'./assets' or '../../assets' is the ambiguous input whose
meaning depends on the host, which is the whole problem." The poster previews
next door have used it all along. The font loader was the last consumer still
taking the raw value, and fonts are the one consumer where it cannot be merely
safer, because the style lands in a document this element does not own.
TWO WRONG HYPOTHESES DIED FIRST, and both are worth keeping.
A missing Access-Control-Allow-Origin. Plausible: @font-face is
document-scoped, the woff2 is cross-origin, and a font fetch is a CORS fetch. I
built a proxy that forwards the same bytes and adds only that header so the
claim would stand or fall on a pair — and with the header it still failed.
Then I checked what the asset host actually sends, which I should have done
first: serve-strict already answers
content-type: font/woff2
access-control-allow-origin: *
So the header was never missing, and the proxy was proving nothing about a
condition that was already satisfied.
A check made too early. document.fonts.check answers "can I render with this
right now", and fonts load lazily, so the probe had to await
document.fonts.ready — the suite's older diff has done that since the day it
was written. Fixed, and still failing.
What settled it was asking the document what it actually held instead of
proposing a third cause: every FontFace on the partner page read status
"error" while the responses were 200. A 200 that is then discarded is not a
network problem and not a timing problem — it is the wrong URL, fetched
successfully from somewhere that has no font at it.
The embed probe now records each font response's ORIGIN and its
access-control-allow-origin header, because "200 bebas-neue-latin.woff2" does
not say which host served it, and in a proxy phase that is the entire question.
A log that cannot distinguish the proxy from the bare host cannot test a claim
about the proxy.
TWO SHARED-COMPONENT DEFECTS, both routed to me, both a step away from where
the routing pointed.
THE 14 NAMELESS BUTTONS ARE ONE COMPONENT. check:a11y-button-names reported 14
visible buttons with no accessible name across two country-posters leaves. The
routing named countries-map.vue, which contains exactly one <button> and it has
visible text — the gate prints the LEAF, so the file was an inference. Running
the gate and then a probe that prints what a developer can grep for — class,
innerHTML, box size, the custom-element host chain — named the real one:
library/components/legends/SemanticTreeLegend.vue:1228, seven
<button class="stl-caret">, 14x14, a 9x9 chevron and nothing else. The 14 is
those 7 counted twice, because country-posters-mfe composes countries-map;
worth saying, or the next reader hunts a second defect that does not exist.
"(no title either)" in the gate's output is the diagnostic half: it rules out
the title-only control this gate was built for, and it rules out Mapbox's own
controls, which ship aria-labels. These were ours and they were empty —
assistive technology said "button" once per expandable branch.
Fixed at the source, which is why it lives in library/: an aria-label naming
the branch the caret discloses, and aria-expanded, because a name tells a
screen-reader user what the control is for and still not whether the branch is
open. Three keys across eleven locales rather than a hardcoded label. And the
same defect one element over, latent: .stl-x-chip carried only a `title` —
precisely what this gate exists to reject — and hardcoded English besides. It
escaped the sweep because it renders only while a row is selected.
Seven surfaces render that legend, so one fix reaches all of them. The
artifacts are per-bundle though: country-posters is rebuilt here because its
cards are routed to me, and my own bundle with it. The other five stay stale
until their owners rebuild, and I have not touched them.
AND THE PIN HINT HAD NEVER BEEN TRANSLATED. The ask was to audit eleven locales
for a stale "red dot". There is nothing stale, because there is nothing: no
locale file contains "Tap a", "pinHint" or "coloured dot"; PinHint.vue imports
no useI18n; the sentence is a JavaScript string literal. So every map that
renders the hint has been showing an English sentence to readers in Arabic,
Russian and Chinese.
Which is worth noticing about the fix that came before it. The colour half was
found and fixed carefully — the comment above it records the Driver reporting it
five or more times, and why a shared component must not name a colour it cannot
know. All correct. Nobody asked whether the sentence was in the catalogue. Same
shape as the Russian headings next door: wrong only for readers who do not file
bug reports in English, and so invisible to everyone who could.
Eleven locales, every one saying coloured and never red. The pinHintLabel
override still comes first, so a profile that does want to name its colour can.
And the key falls back to the English sentence when a locale lacks it, so a
missing translation degrades to today's behaviour rather than rendering the
literal "messages.pinHint.tapDot" — the failure mode of a naive t() swap.
Flagged and not taken, because they are shared components with owners:
FilterDropdown renders a hardcoded English "Clear selection" and "No options
found" (the latter already exists translated in the regions catalogue, so the
component is ignoring a string that is right there), and PosterWall's export
modal renders its reassurance line untranslated.
Shot from the rebuilt studio bundle the studio agent marked stable (0740a94),
not from source: the redesign was on main for half an hour before its bundle was.
The map could select the groups that can already hear the gospel but
have no text Scripture. A filter cannot be sorted, exported, or handed
to a committee, and this is precisely the list that gets handed to a
committee. None of these groups appears in "Where Nothing Was Found"
either, because none of them is tier 0 — it is the opposite question.
2,355 groups across 1,224 languages, largest first, with CSV and JSON
export. Each row names what the group DOES have and links to it, so it
is actionable without the map: Western Punjabi, 97 million, "audio
Scripture, the JESUS Film". The pools checked and the pools NOT checked
are columns on every row, because an exported spreadsheet strips the
caveat first and the caveat is the part that keeps this honest.
Fixing the map to agree exposed a real defect in the filter shipped
last tick: it counted 49 SIGN languages among its 2,289. "Can hear the
gospel" is the wrong sentence about a Deaf people group, and recorded
audio is neither what they lack nor what they need — this map already
suppresses the audio chip and the GRN attach for them for that reason.
Both surfaces now exclude them and both report 2,355.
The served studio (public/doxa-maps/plugin-studio) was built before the
rail folded its composites and before the shell was called Research
Gallery, so the built tree still showed the old studio. Rebuilt that one
bundle and re-baked its SRI in the manifest and its two leaf pages;
check:sri passes on all 57 pages. No other bundle was rebuilt.
- The diagram is redrawn as concentric rings of uniform circles, not filled
bands: the Plugin Studio ring labels itself, the seven plugins sit on the
plugin ring, one agent per plugin sits outside it joined by a line and an
arrow ("each plugin has one agent responsible for building it"), and one
shepherd sits outside the agent ring. The race framing is gone. SVG source
updated with the PNG.
- Deployment method 2: Simple Research was never built for phones, so it is
no longer the mobile example. UPG-100 is; its screenshot lands when its
phone layout is finished. The shot is removed from readme-shots, with the
reason and the planned replacement in a comment.
- Retook nine screenshots that the latest bundle rebuilds had made stale.
Their heading face has no Cyrillic glyphs. That is why base/_globals.scss:35-44
switches every h1/h2/h3 to --font-family-cyrillic under [lang="ru-RU"], with
letter-spacing -0.02em — without it a Russian heading leaves the family
entirely and lands on whatever condensed system face the browser picks. We ship
a `ru` locale and had neither the rule nor the font, so that is exactly what
every Russian heading in this clone was doing.
Four subsets vendored, Google's own unicode-range values carried verbatim so
the browser fetches only what a heading needs: cyrillic 6700B and cyrillic-ext
7688B are the point, latin 12248B and latin-ext 11624B come too because a
Russian heading routinely contains Latin characters — a country name, a figure,
an ampersand — and a Cyrillic-only face drops those back to the fallback
mid-word, which looks worse than not switching. Each file verified to begin
with the wOF2 magic after download, because a fetch that returns an HTML error
page also "succeeds" and a font that is really a 404 fails at render time with
no network error left to find.
Two things the rule needed beyond the font. [lang] cannot cross a shadow
boundary, so the attribute has to be INSIDE the root for a selector in the
root's own stylesheet to see it — our roots now carry :lang for the same reason
they already carried :dir, and it is correct HTML besides. And the selector
accepts both `ru` and `ru-RU`: their site tags the document ru-RU while our
i18n locale is `ru`, and a host page embedding this element may set either, so
a selector matching only one would work on their site and fail on ours.
Only the headings, as theirs. Their .region-stat__value is a <p>, so their
ru-RU rule does not touch it and it keeps the heading family even in Russian —
switching everything would be wrong in a way no Russian reader would report and
no English reader could see.
_probe-cyrillic-headings.mjs checks the mechanism and the result separately, so
a rule that is present but inert cannot pass: the attribute is on the root, the
Oswald subset returned 200, the face is usable once document.fonts.ready
resolves, the h1 and h2 switched, the stat value did not, and English is
untouched in both family and letter-spacing. It awaits fonts.ready before
asking about a font — not doing so is what made a sibling probe report a
cross-origin font failure that did not exist.
Two fixes and a correction, all three found by turning other agents' lessons on
my own already-green work.
THE TOTAL ROW DISAPPEARED WHILE LOADING. Theirs never removes it: it swaps the
count for a visibility:hidden placeholder so the row holds its line and the
results below do not jump down when the number arrives. Ours v-if-ed the whole
row away, so the layout shifted on every load. It surfaced only after the
class-coverage sweep's extractor was widened to read the whole file instead of
just the template — the class is in markup my template scan reached, but the
sweep had never seen the NAME, so nobody had asked what it was for. Widening it
took that sweep from 22 to 58 classes seen on their UupgsList and from 103 to
134 checked overall, inside a run I had already reported as complete. Of the
ten new names, eight are their useSelectCard variant this clone does not render
and each is recorded with that reason and its line number; the ninth,
line-height-tight, was already correct because we ported the resolved value
rather than the class.
AND A CORRECTION TO MY OWN LOCALISATION CLAIM. I reported zero hardcoded
English in the regions components. That was wrong, and my own instrument is
why: the scan bounded matches at sixty characters, and three failure messages
are longer than that —
Region data could not be loaded. This is a connection problem, not a filter.
People groups could not be loaded. This is a connection problem, not a filter.
The map could not start. This is a map-token or connection problem, not a
filter — the figures above are unaffected.
Re-scanned with no upper bound and across line breaks, translated into all
eleven locales, wired through t(). A scan with a bound can only find what fits
the bound, and I quoted its zero as though it covered everything.
Verified against live after all of it: the region card diff is now 37 of 37
identical (it grew by the icon's own margin, which nothing had read), the
bullet 11 of 11 exact, the region page 107 of 107, the country page 32 of 32.
The map could select the 2,289 groups that can hear the gospel but
have no text Scripture; it could not say which of them to weigh first,
and 2,289 dots cannot be read top-down. The gap sheet gains a fifth
tab that ranks them by population and, on each row, names what the
group DOES have — "has audio Scripture, the JESUS Film — no text
found" — so a reader can see whether they are being sent to a
recording or a film before they go.
The tab describes a different population from the other four, so it
does not inherit their sentences: the nothing-found opener, the audio
caveat and the macrolanguage note are all scoped away from it. Its own
paragraph states the bound twice, because the top rows are Western
Punjabi at 97 million and Wu Chinese at 92.5 million, and a reader
skimming those numbers must not come away thinking no text exists in
any variety they speak.
GRN batch four: 170 more codes, 90 with gospel audio. 343 codes now
carry a Words of Life link and tier 0 falls 2,488 to 2,373 — from
3,457 when this work began. 689 of the gap's codes are decided, and
provenance stays exact at 588 claiming GRN, 588 decided, none
decided-but-unnamed.
The studio listed the app shell by its profile id, doxa-maps-spa. It now
shows the Driver's name for it, Research Gallery, in the rail, in every
composition tree and in the title bar. Display only: the row key, the URL
hash, the remembered tab and the <doxa-maps-spa> tag under the name are
unchanged.
Rows now carry the real profile id separately from the label, because the
mount handed the label to the element as its profile; a display name
there would have mounted the shell with a profile it does not have.
THE DRIVER RULED ON THE PALETTE. He compared the two screens himself: "when i
look on teh color ffor th regiosn tab of the simpel research map i don tsee the
you using htes colors on the reigosn dashbarod hwen you shoudl". Two surfaces
disagreeing about what Africa's colour means is a defect whichever palette is
prettier, and he chose which one wins. library/colors/wagf-region.js now
carries doxa-research-map/src/colors/doxa-region.js's values.
The blast radius turned out to be nil, and my earlier escalation overstated it:
wagf-region.js is imported by exactly two files, both mine. I had reported it as
needing a cross-agent decision on the grounds that seven surfaces would be
repainted. They would not have been — nobody else imports it.
WHAT THE CHANGE COST, measured with the same validator and surface the previous
palette was stepped against:
lightness band FAIL #1ee05f .793 · #FFEB3B .928 · #00e0bd .810 sit
above the .43–.77 band
chroma floor PASS all 7 >= .1
CVD separation PASS worst adjacent #FFEB3B/#1ee05f dE 8.7 protan
normal vision PASS worst adjacent dE 21.0
contrast WARN under 3:1 vs surface: #1ee05f 1.72 · #FFEB3B 1.19
· #00e0bd 1.65 · #ff7f11 2.47
The palette this replaced passed all five. The contrast warning is survivable
here for a reason and not by dismissal: it obligates relief through a secondary
encoding, and both surfaces already carry one — the chooser renders a legend of
labelled chips in these colours, the dashboard renders the same seven as a
table. Identity never rests on hue alone. The lightness failure has no such
relief: three of these read faintly as pins on a light basemap. Darkening those
three by about 10% L would enter the band with the hue still recognisable, and
that is the Driver's call, so it is asked rather than done quietly.
_chk-region-palette-agrees.mjs is what stops the two drifting apart again. It
compares them value by value and names both files on a disagreement. Ours is
imported; theirs is parsed, because doxa-region.js imports a Vite alias and node
refuses the module — so the parse asserts its own shape first, requiring the
eight entries that file declares, and fails rather than comparing against an
empty object if anyone refactors it to computed values. Negative-tested: flip
one hex and it names that region and both paths.
AND ONE UNIT IN THIS PORT WAS NOT PX. The chevron's gap was a literal 1rem,
ported from their `.with-icon .icon.right { margin-inline-start: 1rem }` — 10px
at their 10px root. `rem` resolves against the document root wherever the
element lives, which is the whole reason dxlife-tokens.js resolves their scale
to px, so ours rendered 16px on our own leaf and would render 10px by accident
on a host with the common 62.5% reset. Pinned to 10px. No diff caught it: the
list diff's link spec reads colour, decoration, weight, display and alignItems,
and never a margin. It reads the icon's margin now.
With every composition tree open, the rail was too long to find your way
around. Each composite now carries a chevron and starts folded, so the
rail first reads as the 35 plugins by capability, each composite saying
how many parts it has. Open one and you see its direct parts; a part that
is itself a composite folds again, so doxa-maps-spa opens to its tabs and
each tab opens to what it is made of, one level at a time. Choosing a
composite also opens it; only the chevron closes it. The same child folds
independently wherever it appears.
VerticalTabsLayout gains two optional row fields: `branch` makes a row
foldable, `ancestors` hides a row until every branch above it is open.
Rows are wrapped so the chevron can sit beside the tab rather than
inside it. doxa-maps-spa sets neither field and renders unchanged.
The pin filter could separate nothing / something / both, which are
tiers. It could not ask the question a translation team actually
starts from: who can already HEAR the gospel — a recording, the JESUS
Film, a Bible video, Words of Life — but has no text Scripture in
their language? That set cuts across all three tiers, so no tier
button could select it, and it is the largest actionable group on this
map: 2,289 people groups across 1,147 languages.
The claim is bounded in the button, the caption and the tooltip: no
text Scripture FOUND IN THE POOLS CHECKED, never a claim that none
exists. It matters here more than usual, because the largest entries
are Western Punjabi, Shanghainese and Egyptian Arabic — varieties
where a recording plainly exists and a text in that same variety is
genuinely contested.
The flag is baked into the pin features so the filter stays a GL-side
predicate like the others; recomputing it per toggle would re-upload
12,379 features to answer a yes/no already known at build time. The
button carries no colour swatch, because it names a question rather
than one of the three colours.
Measured in the browser: 12,379 pins rendered, 2,289 after the filter,
matching the offline count exactly.
Apply a filter on the country page and the button you just pressed went white
text on 5%-alpha blue over a white ground — an invisible label. Theirs is white
on #6d766e. We had copied their DEAD FALLBACK:
background-color: var(--color-surface-brand-lighter, rgba(0,80,160,0.05));
color: var(--color-white);
and --color-surface-brand-lighter is defined, at tokens/semantic/_colors.scss
:64, as $color-green-300. The fallback never applies on their site.
What kept it alive was a comment of mine sitting right above the rule. It said
their active state is white-on-white on purpose, that the token "is NOT DEFINED
in their token files", and that the white-on-white had been "MEASURED on their
live page". The first two are contradicted by a file in the read-only clone.
The third could not have been what it claimed: getComputedStyle returns the
specified colour and never the composite, so neither candidate can serialise as
white — the token is #6d766e, the fallback is rgba(0,80,160,0.05). The only
rule on that button that yields white is .input's var(--color-surface-white),
which is the inactive state. The measurement had read a button without
data-active set.
Measured properly, by clicking the Adopted filter on both sides: theirs is
background rgb(109,118,110), colour rgb(255,255,255). Ours now matches, and so
does Clear All — theirs is `.button compact link`, which blocks/_buttons.scss
:64-69 gives --button-text-color: var(--color-brand) = #3b463d, where ours had
used the body ink #2a332a. 30 of 30 properties identical to live, 0 differ.
That second one was found twice over, independently: by this diff, and by a
static sweep of their markup's classes that flagged `.link` as a rule our CSS
had no equivalent for at all. The sweep is worth reusing — list every class and
tag a component's markup uses, resolve each against their scoped block and
their global sheet, then check ours has something for it.
The probe exists because _probe-country-vs-live.mjs printed one line everybody
had read past: "NOT MEASURED (absent on both sides, so neither identical nor
different): chip". A filter chip only exists once a filter is applied. Absent
on both sides is not a pass. It is the second diff here that clicks something
before measuring, after the pin popup, and its own first run was a probe bug
worth recording: it waited thirty seconds for the three filter buttons and then
reported that Adopted could not be found. The panel is collapsed by default on
both sides and the buttons sit behind a v-if, so they did not exist yet — a
probe failure dressed as a product finding.
Two things, both found by other agents' reports rather than by my own gates.
rega found upg-100-list.vue:1464 injecting mapbox-gl.css v3.3.0 into the shadow
root unconditionally, so a keyless maplibre embed still pulled a stylesheet from
api.mapbox.com. Token-free and harmless to render, but wrong-engine leakage on a
page whose point is having no Mapbox dependency. Gated on isMaplibre, which
already existed at line 119.
This commit is ATOMIC by design: the fix, the rebuilt bundle, and the deletion
of rega's MAPBOX_KNOWN exemption land together. Either half alone leaves a
window — delete first and main red-gates on its own known bug; fix first and a
stale exemption sits there hiding the next one.
Proven before deleting, because the exemption is keyed on a URL STRING while the
defect is a network REQUEST: the built bundle still contains the v3.3.0 literal
(the constant remains; the guard stops the fetch), so a source-grep gate would
have been misled. rega's gate measures requests, and it reported
"provider=maplibre made 0 request(s) to api.mapbox.com" in BOTH js and iframe
modes with no KNOWN line printed at all — i.e. the exemption was already dead
code. Re-proven after deletion: still 0 requests, still PASS.
The leaf-head preloads are explicitly NOT covered and the reason is recorded in
the gate: mapbox-gl.css v3.24.0 and mapbox-gl-geocoder v5.0.0 are a different
version and a different owner (generate-tree, bundler-1's lane), already tracked
separately.
Second: the Driver's report had two halves — "it is supposed to show the
previews" and the print options overlapping. I fixed and gated the overlap and
left the first unasserted. The over-preview check was written as `if (img)`,
which means a card with NO preview satisfies it perfectly: nothing can overlap
an image that is not there.
The gate now counts previews. Mutation-tested by aborting every previews/**
request: the product degrades well — all 27 cards swap to a "poster not
generated yet" placeholder with no broken-image icons and no <img> at all, which
is precisely why an image-health check cannot see it. So the fallback itself is
asserted, and all-cards-placeholder now fails.
A first version of that assertion was wrong and the reason is recorded in the
code: it called 18 lazy-loaded previews broken. Every img carries loading=lazy,
the first unpainted one sat 3926px down an 844px viewport, and no HTTP response
failed. Deferred is not broken; it now judges only in-viewport images.
Four elements shipped with this broken and nothing could see it, because every
existing gate loads a composite on its OWN generated leaf — and that leaf bakes a
token and assigns window.MAP_TOKEN, supplying the very thing under test. The
failure exists only where there is no leaf: the partner page the published embed
snippet is written for.
The gate serves the built tree on one free port and a partner page on another, so
CORS is exercised rather than assumed, and asserts per composite:
tk given only {"tk": <the token this tree bakes>}, a canvas appears and
api.mapbox.com actually serves it
provider given only {"provider":"maplibre"} and NO token, a canvas appears and
there are ZERO requests to api.mapbox.com
Zero is the assertion, not "it rendered": a build that quietly fell back to
mapbox with a baked key renders perfectly and is exactly the regression this
exists to catch.
★ IT FAILS ONLY WHERE THE EVIDENCE POINTS AT THE KNOB. The first draft failed
NINE composites for "no map canvas under a minimal config" — an accusation it had
not measured, since several of those elements simply need a `profile` key to
choose which profile to show. It now distinguishes: a token WAS supplied and the
element still says there is none, or maplibre was asked for and mapbox tiles
still loaded — those are findings. No canvas and no complaint is UNMEASURED, and
says so with the reason. A gate that red-lights other people's plugins over a
claim it cannot support is worse than no gate.
COST, because a slow gate stops being run: a flat 14s-per-case wait became a poll
for the canvas, and the provider case short-circuits when that element's tk case
drew no map — there is nothing to compare against, so a second page load was pure
waste. 5m38 -> 3m15.
GREEN on the landed tree: dashboard-plus-grid, research-map-plus-grid, stack-demo
and plugin-studio all pass both knobs, every provider case at ZERO api.mapbox.com
requests. plugin-studio only reached zero once the cap-5 shell started setting
window.MAP_PROVIDER — profile-config alone reaches the child a composite writes
config for, while the global rung reaches a map nested at any depth, which the
studio-inside-SPA-inside-composite chain needed.
gates:post is 44 steps; the quoted CLI doc is updated to match.
170 more gap codes probed, 89 with gospel audio — a 52% hit rate,
steady across three batches now. 253 codes carry a Words of Life link
and 385 people groups whose only resource in any pool is that
recording — 71.6 million people — can hear the gospel in their own
language. Tier 0 falls 2,610 to 2,488, from 3,457 when this work
started.
520 of the gap's 2,456 codes are decided. The provenance stays exact
through the change: 434 codes name GRN as checked, 434 have really
been decided, and none that was decided goes unnamed.
THE POPUP WAS A STUB. Click a pin on their region page and you get a card: the
people group's photo, the name, "country · primary ethnolinguistic group", a
definition list of Population / Religion / Intercessors-out-of-100, up to three
clamped lines of location description, and two buttons — Pray, which is the
point of the entire site, and Full Profile. Click one on ours and you got four
lines of hardcoded English with en-US number grouping in all eleven locales.
No diff here could ever have found it. Every vs-live probe reads the page at
rest, and a popup only exists after a click. So there is a new one, and the
popup now matches theirs on 37 of 37 measured properties with the same stat
labels, the same two actions, and their ?source=doxalife attribution on the
Pray link — which is invisible on screen and exactly what a re-implementation
drops silently.
Noticing it was not enough to fix it: the DATA was not on the page. Their
PIN_FIELDS asks for rop1, religion, location_description, image_url and
has_photo; ours asked for none of them, so a faithful popup would have rendered
empty rows. Five more columns on the same single request. Their row mapping
comes with it, including the has_photo gate on the image — a row can carry a
placeholder image_url and they only show it when the flag says there is a real
photo — as does their fuller escape, which also escapes both quote characters,
and that matters because these strings are interpolated into href and alt.
AND THAT FILE HAD NO t() AT ALL. Its prayer legend rendered four hardcoded
English strings on a page whose every other word is translated into eleven
languages — and three of those four were already in the catalogue, translated
for the Prayer Coverage card and hardcoded here anyway.
LOCALISATION, re-measured honestly. The old 97.4% was counting strings
identical to English as untranslated, which is wrong: es/fr/pt/ro share
"Total", and French shares "Population" and "Religion". Those are cognates. The
real question is coverage of their surfaces, so: every t() argument in
RegionDetail.vue, pages/regions/index.vue, CountryDetail.vue and CountryMap.vue
— 35 strings — checked against our catalogue. Two were missing, both
CountryMap's: "Outside this region" and "Pray". Added in all eleven, the eight
DX-life translates copied from their own common.json and it/ro/zh composed from
words already attested in those locales' own files rather than invented. Then
the reverse direction: a scan for capitalised text nodes outside {{ }} found
two of our own strings left in English — the dashboard's loading state and the
list's empty state, neither of which DX-life has an equivalent for because
their page is server-rendered. Both translated. The scan now returns zero in
all three components, and coverage of their surfaces is 35/35.
Verified alongside: all seven regions at 107 properties identical to live each
(not just Africa), mobile parity still 44/0 at 390 and 768 after the CSS
surgery, and first paint of the list measured at 3766ms with 1863KB of it a map
engine that leaf never calls — carded to the build machine, not worked around.
MEASURED at 390x844 on doxa-research-map: the hint pill sat at y=700 and the open
legend sheet begins at y=603, so "Tap a coloured dot — someone still needs
prayer" printed straight across the Progress rows. 1,418 Needs Prayer and the
Full Prayer Coverage line were unreadable underneath it, in the phone screenshot
the README showcases.
Nothing was clipped, which is why the existing below-the-fold clamp had nothing
to say: the pill was fully visible and fully in the way. So the sheet becomes a
FLOOR, exactly as the attribution strip already is — this component has always
treated the credit line as something to clear rather than cover, and the sheet is
the same kind of obstacle. If the pill would cross the sheet's top edge, it lifts
by the overlap; only when the two really intersect horizontally, and only ever
upward, so a surface with no sheet is unchanged.
TWO THINGS THE FIRST ATTEMPT GOT WRONG, both found by re-measuring rather than by
reading the diff:
· The sheet does not exist when the pill mounts — LegendMobile renders after the
map's first canvas. A mount-time querySelector found nothing, so no observer
was bound and the clamp never ran again: the pill moved 0px and the "fix" was
a no-op that looked plausible. The observer is now bound lazily by the first
clamp that SEES a sheet, with three settle re-measures after mount to make
sure one of them does.
· The sheet also changes height as the reader drags or collapses it, and each
height is a different floor. The existing host observer watches the MAP, which
a sheet drag does not resize, so the sheet needed its own.
MEASURED AFTER — 390, 360 and 414px wide: no overlap between the pill and any
painting part of the sheet; screenshot read at 390, where Progress now shows 689
/ 136.7M, 1,418 / 69.6M and Full Prayer Coverage in full. Desktop 1280x900
unchanged: pill still at the map's bottom edge, no lift applied. The two other
hint consumers (country-posters-mfe, regions-mfe) show no overlap either.
One probe note worth keeping: .legend-sheet-root spans the whole map with
pointer-events:none and paints nothing, so an overlap check that counts it reports
a collision no reader can see. The obstacle is the sheet, not the box it lives in.