SUN reached the access table and the CSV first, and the map — the surface a
mobiliser actually opens — still showed Deaf groups a panel that explained their
situation honestly and then left them with nothing to do. That is the largest
population on this map with nothing: 93 groups, 17.5 million people, and
structurally so, because a spoken bridge cannot reach a Deaf community and there
was no other kind.
Clicking Deaf Beninese now offers the SUN New Testament, the SUN Bible stories
and the language itself, and says that SUN is already being taught in Benin by
Mission Évangélique des Sourds — a named local Deaf church rather than a
resource with nobody attached to it. It is offered only where the group has
nothing, because a group with Scripture in its own sign language does not need
to learn a second one, and it says on its face that it is a bridge which has to
be learnt and closes no gap in their own language.
Carried in the pin index beside the spoken bridges rather than fetched by the
leaf, so the sheet and the access table cannot disagree about what exists. It is
one small object, not a per-language join, because SUN is one signed language
for all of them.
The doctrine scan reads it too. Those URLs live in a new top-level block that the
per-language loop cannot see, so they were added to the scan in the same change
rather than found missing later, the way the access table was.
The three chart titles, subtitles and axis units were the last inline English in
this leaf. The subtitles carry a count, so they are held whole in the catalogue
with a placeholder and the number arrives as a parameter — a sentence built by
gluing a number between two fragments cannot be translated, because word order
differs and several of these locales put the number where English does not.
The fallback interpolates too. Without that, a host that never installed i18n
would render the placeholder literally on screen, which is worse than the English
sentence the fallback exists to protect.
The leak gate could not have caught any of this. Whole-label equality is the
right unit for a fixed label and blind to a sentence carrying a number: the
catalogue holds "Across all {n} languages in the corpus." and the screen shows
"Across all 5,067 languages in the corpus." Those are never equal, so three
subtitles could have shipped untranslated in eleven locales with the gate
reporting PASS. A placeholder is now a wildcard and those keys are compared by
pattern; everything else stays equality, which is what keeps a word inside a
sentence from impersonating a label. Comparison went from 2,100 pairs to 2,244.
Both bridge panels named a language and stopped there. For a group with nothing,
the only link on the sheet was "Every resource in their own language" — which is,
by definition, the empty table. Clicking a gap pin showed it plainly: Syenara
Senufo offered five "none found" chips, named Minianka as the way in, and the one
thing to click led to nothing. The sheet told a mobiliser where to go and then
made them go and find it themselves.
Nothing new had to be fetched. Both bridge records already carried the bridge's
ISO 639-3 code and the pin index already held that language's verified links, on
the same screened publishers and through the same doctrine scan as every other
link on the sheet. Minianka is ebible.org/myk/ and jesusfilm.org/watch/jesus/
minianka, and both answer 200 while a made-up sibling path 404s. Joined on the
code, never the name.
All 1,298 bridge records resolve to a language that has at least one link, so no
named bridge becomes a dead chip; the markup renders only what the index holds,
so that stays true if a pool ever loses one.
Also records the ruling on publisher names as settled rather than as this gate's
own judgement: organisation and product names are proper nouns and stay English,
scoped to the Source column and product names. The structural cell rule is what
lets the same words be a name in a Source cell and a translated label in the
chrome at the same time.
Every bar label, tab and legend key in the charts leaf was written inline in
English and never wired to i18n — a surface whose headings were translated while
the words inside the picture were not. Eight of them already had catalogue
entries and were simply not used; the other fourteen are new keys, translated in
all eleven locales from each catalogue's own vocabulary rather than invented
beside it.
The tab strip had to become computed. A plain array is evaluated once at setup,
so it would have kept whichever language the leaf first rendered in and never
followed a locale change; the legend arrays live in the template and already
re-evaluate on render.
Adding a key for "Full Bible" then exposed what the leak gate cannot tell apart.
It read table CELLS as if they were chrome, and a cell whose entire text equals
an English term is indistinguishable from a label by text alone. Both findings it
produced were the language being correct: Source cells carry organisation names —
JESUS Film, Faith Comes By Hearing, Door43 — and Scope cells describe what a
resource contains, one of whose 114 values is the words "Full Bible" while most
of the rest are already in the target language. Translating either would be the
defect. The boundary is structural, so the DOM now draws it.
That leaves the Type column, the one cell column carrying catalogue labels, with
its own arm. Asked the obvious way round it was wrong: requiring every translated
type to be present failed on Gospel audio in all four locales, because 1,027 rows
carry it and none was on the first page of fifty — pagination reported as a
translation defect. It asks instead that no cell equal one of the eight
enumerated type strings, which stays silent for a type that is off-screen and
fails for one that is on-screen and English.
These two maps build `new gl.Map` directly, so they never got useMapInstance's RTL install, and
their bundle entries registered nothing either. The basemap draws each place in its OWN script,
so over the Middle East most labels are Arabic, Persian or Hebrew for EVERY viewer: they were
drawn as disconnected glyphs in the wrong order. installRtlTextPlugin(gl) now runs after the
engine global resolves and before new gl.Map (an entry-time call cannot work once the engine is
injected lazily).
Gate: check:rtl-raw (+ :poison), gospel's check-gospel-rtl-shaping method (pan to Arabia first;
no RTL label on screen is NOT MEASURED; one defineProperty per global for hook + poison) over
cities-internet on both engines and key-cities (Mapbox-only, gated, no leaf: mounted on a
same-origin host page). The shared gate's --leaf argument can replace the cities rows once it
lands; the key-cities host page still needs this one.
Measured: before, cities-internet mapbox 58 RTL labels with the shaper 'unavailable' and maplibre
62 stuck at 'requested', both unshaped; after, shaper 'loaded' on both, and key-cities 56 RTL
labels shaped. Poison: 3/3 rows caught.
The gate could only ever read Arabic. Its rule — English prose declares itself
with dir="ltr" — is right about the prose and wrong about the root: the embedded
RTL fix sets direction on the plugin's own root element, so under ar the root was
rtl and the walk ran, while under zh, ru and hi the root matched the prose test
and the entire leaf was skipped. Measured on identical DOM in both locales: 1,359
elements walked and 589 carrying text either way, of which 1 sat in a prose
subtree under ar and all 1,359 did under zh.
It then reported that silence as "rendered no labels at all", which reads as a
product failure and is not one. The Chinese leaf renders correctly, title and
all. Silence is now NOT MEASURED, reported separately, and never counted as a
pass. dir is honoured below the root and ignored on it; the comparison went from
488 key/locale pairs to 1,876.
What that exposed was a translation that gave up halfway. Type is an enumerated
label whose eight values the catalogue already translates, and the heading above
it was translated while the column beneath stayed English. The map is derived
from the catalogue rather than listed in the component, so a ninth resource type
is covered the moment it is added; a hand-written list would have been one entry
short and nothing would have failed. Both code paths localise, including the
filtered view a pin or country click produces.
Two values had no key because the catalogue spelled them differently from the
data — Audio Scripture and Bible Stories, now translated in all eleven locales.
Open Bible Stories stays English: it is a product name, as are the organisations
in the Source column beside it.
Of the 369 Doxa groups with nothing in their language, 94 had no bridge either,
and 93 of those were Deaf — 17.5 million people, the largest untouched number on
the map. That was structural, not bad luck: every bridge here is a spoken
language widely used in the same country, and a spoken bridge is worthless to a
Deaf community, so the build correctly refused to offer one and then had nothing
else to say.
SUN is a signed language built to be learnt across sign languages, with the Old
Testament, New Testament and Bible stories in it. It is carried the way a spoken
bridge is — something to learn, no substitute for Scripture in a group's own
sign language — in its own columns, never folded into the access rung, which
stays the honest "nothing found in their language". 28 countries where it is
already being taught come with the named local Deaf church or organisation
doing the teaching, which is the difference between a resource and a lead.
Only URLs verified by their content ship. sun.bible answers 200 for
/sun/<anything> and /resource/<anything>, so a per-language URL built by pattern
would look valid and could be empty; the fetcher re-measures that catch-all on
every run rather than trusting a note. Hosts left out are listed with the reason,
including three that are reachable by a person but not by a script.
Doctrine was checked harder here, not less. The Deaf are the most heavily
targeted group in this dataset by publishers this build must never carry, and
the thinner the pickings the stronger the pull. SUN is Wycliffe Associates'.
Screening the result exposed the bigger hole. The doctrine scan read the pin
index only, and within it skipped the Words of Life and Scripture Earth fields,
so the access table — the one the page renders and the one exported to CSV for
posters and QR codes — was never screened at all. The least supervised links in
the build were the ones most likely to be printed and handed to a stranger.
Scanning both, and both fully, takes the screened count from 6,497 to 16,960.
The gate also now proves the server holds these tables. It adopted a server from
another worktree on the default port and reported PASS on it: all three
manifests are byte-identical, because a manifest describes bundles and the
change was to a data asset it never mentions. A planted excluded-publisher link
went undetected, which is the worst possible failure for this particular check.
Driver decisions (cards BUILD-OUTPUT-IS-ONE-FOLDER, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE).
- public/ holds only <tree>/ and a generated README.md. The build no longer writes public/index.html,
public/assets/ or public/__gallery/; `rm -rf public && bun run build` recreates exactly these two.
- The Plugin Studio is <tree>/index.html (tree-base "."), replacing the staging-forge chooser; nothing
is written above the tree. check:staging-lang, which tested that chooser, is retired with it.
- Assets are gathered in a temp directory and the fetched allowlist staged into <tree>/assets/ as
before. fonts/, images/, brand/ and legacy/ existed ONLY in public/assets: they are sources and move
to data/ (git mv). Poster previews keep their tracked sources in plugin-designs/*/assets/previews
(all 378 verified present).
- __gallery/ is archived out of public/ (a copy kept in the umbrella's _qa-cycle/archive) and out of
the build; generate-gallery renders only on request, into a directory you name, never public/.
- Native shells load the output folder: capacitor webDir public/<tree> (cap copy needs an index.html
there), Tauri frontendDist + window url relative to it. internal/sync-app-shells.mjs keeps both in
step with the tree's name on every build, so the coming rename stays one config line; the Capacitor
start page resolves its leaf tree-relative.
- dropin.include now lists only extras (README.md): the tree is always shipped and never named twice;
excludes are tree-relative ({tree}/...). clean.mjs removes the old root entries from existing
checkouts; the build lock and public/.gitignore's rules move out of public/.
- check:wp-embed read the BUILT manifest from gates:pre, before the build, so a from-scratch build
could never pass; it runs in gates:post and reads the tree name from config.
- Stale bundles refreshed by the clean build (key-cities, template-bundle, wall-of-people-groups);
the CLI doc's quoted scripts re-synced; capability exemplars regenerated; the '#' note leaves
check:stale-sweep-claim's command (it was already in //scripts).
Proof: rm -rf public && bun run build -> public/ = README.md + doxa-maps/; check:sri, check:drop,
check:parse, check:public-hygiene, check:quoted-scripts, check:capability-exemplars PASS; all 57 pages
(the front page + 56 leaves) mount under /doxa-maps/ on this tree. check:facet-cascade is red as on main.
Cell E verification for Big Picture and Simple Research needed the
gospel leak gate's method on two other bundles. That gate hardcodes its
four leaves, its element tag and its single catalogue file, so pointed
elsewhere it finds no shadow root and silently compares nothing. This
one takes the leaf, the tag and the locales as arguments and compares
against EVERY catalogue area — a COMPOSITE has no t() calls of its own,
so its chrome comes from the children it nests.
SIMPLE RESEARCH CELL E IS CLEAN, independently measured:
research-map-plus-grid in ar/zh/ru/hi, tree identity checked first, no
leaks, 0 page errors, and 86/34/27/43 comparable keys — so the
comparison was non-vacuous in every locale rather than "0 leaks over
nothing". A second instrument agreeing with the owner's reading is what
the verifier role is for.
★ TWO FALSE POSITIVES, BOTH OF WHICH LOOKED LIKE TEXTBOOK DEFECTS. The
first run reported 3 leaks x 4 locales on Big Picture, and all three
keys ARE translated in every catalogue, so the pages really were showing
English where a translation existed.
DATA CELLS. "Unengaged" rendered in .rd-td inside .rd-grid — table
data. people-groups-grid.vue:150 records the field as
{value:'unengaged', label:'Unengaged'}, 100% populated, and
dashboard.config.js:132 counts 2,081 Unengaged / 27 Engaged. It is an
API VALUE that happens to equal a catalogue string, which is the
population rega's method excludes by name. Whole-label matching stops
a substring impersonating a label; it cannot stop a data cell
rendering the same text.
FACET VALUES. "Other" rendered in span.fr-lbl in the facet rail, which
LOOKS like chrome. The template is
`<span class="fr-lbl">{{ r.label }}</span>` with `{{ r.count }}`
beside it, and the component's own comment calls it "the 'Other'
bucket". The string comes from row data, not from t('regions.Other').
Both are excluded BY ROLE rather than by class. The facet exclusion keys
on the row carrying a COUNT, so the rail's own section headings stay in
scope and a real leak there is still caught — excluding .fr-lbl
wholesale would have blinded it.
⚠ NEITHER IS GOSPEL-SPECIFIC. Any bundle with a data grid or a facet
rail produces these, and whether "Unengaged" and "Other" as DATA should
be localised is a genuine product question — but a different one from
cell E, and filing it as a leak would send someone to fix correct code.
⚠ BIG PICTURE CELL E IS NOT CLAIMED CLEAN. The confirming run after the
facet exclusion came back RC=75 (no browser slot within 900s), and the
run before it still reported regions.Other. Whether the exclusion takes
effect is unverified, so the cell stays open rather than being marked
on an unconfirmed reading.
The map is built raw, for the provider switch, so it never installed the RTL
text plugin — the same opt-out that cost it the engine stylesheet. Without the
plugin those labels draw as disconnected glyphs in the wrong order, which is
unreadable rather than merely ugly.
This was never an Arabic-locale bug, which is why it went unseen. The leaf does
not localise basemap labels, so every place is drawn in its own script: over
Arabia, 57 of 61 rendered symbols are Arabic for every viewer whatever their UI
language. An English reader sees broken Arabic they cannot evaluate.
It installs after the engine global resolves rather than at bundle entry. This
bundle injects its engine lazily, so at entry time there is no engine to install
into and the call would silently do nothing.
The gate pans to Arabia before judging. Registration alone proves little, since
the plugin is registered lazily and a freshly loaded map reports "deferred"
whether or not the shaper can actually be fetched; only real RTL text on screen
turns it "loaded". Asserting without panning would have failed a working map.
The census against doxa.life/research found our grid saying "Full profile" and
"Load more" where the page we clone says "Full Profile" and "Load More". Case,
nothing else — the kind of difference that is invisible in review and obvious
side by side. Fixed in the English catalogue and in the fallback literals; the
other ten locales are untouched, since none of them has a capital to drift.
The filter toggle's "Hide Filters" is now SANCTIONED in the preset rather than
fixed, with the Driver's words, the date and the card: our filter panel is open
on load because the Driver reported not being able to find the dropdowns, so our
toggle offers to hide where theirs offers to show. Same control, other state.
Everything else the census found went to the Driver as
DECIDE-RESEARCH-CENSUS-DIFFERENCES rather than into the allowlist — the legend's
prayer-band wording, our four additions, and their page title.
★ AND THE CENSUS TURNED UP A REAL DEFECT WHILE CONFIRMING THE "DATA DRIFT".
The drift is OURS. Measured against pray.doxa.life/api/people-groups/list with
this repo's own thresholds (prayer-progress.js: 0 or null = Needs Prayer):
API Needs Prayer 1,162 (58.8M) Has Prayer 945 (147.5M)
doxa.life Needs Prayer 1,162 (58.8M) — matches the API exactly
our legend Needs Prayer 1,417 (69.1M) Has Prayer 690 (137.1M)
254 people groups the API says have someone praying are drawn and counted by our
map as having nobody. Their snapshot is right and we are wrong. Recorded on the
card with a candidate cause (useMapLayers.js:226 reads only the camelCase
peoplePraying and defaults to 0, where getPrayerLevel also accepts the snake
_raw.people_praying) and passed to the shepherd for its own card rather than
fixed inside a wording commit.
check:element-census-vs-live now has a preset for this lane, so the question
"what is on our page and not on theirs, and the reverse" has an answer that runs
instead of an opinion.
★ THE LEAF IS THE COMPOSITE, NOT THE GRID ALONE. people-groups-grid is a copy of
DX-life's UupgsList.vue, which is the BODY of doxa.life/research — but that page
is a map plus that body. MEASURED: censusing the grid leaf against /research
reports 45 omissions, and every one of them is the research map's chrome
(Clustering, Off/Native/MST/Network, the tab strip, the legend) — nothing to do
with the grid. A list that long and that wrong teaches a reader to ignore the
gate. research-map-plus-grid is map + grid, which is what /research IS, and it
lines up 98 strings against their 95.
The negative control is the pin hint ("someone still needs prayer"), and the run
confirms the census SEES it as ours-only — without that, every count it prints
would be worthless.
The sanctioned list is deliberately EMPTY. The first run's 14 additions and 14
omissions have gone to the shepherd as findings; an allowlist written by the
owner of the code to make its own gate green is not evidence. Most of the
omissions look like wording rather than absence — their "Full Profile" against
our "Full profile", their "Load More" against our "Load more", their legend's
"1+ People Praying" against our "Has Prayer" — but that is exactly the call the
Driver makes and not the one this file should quietly make for them.
The gate defaults to the deployed site, so a failure line reads as "this code
is broken" when it means "this code is not published yet". That misread cost
two wrong diagnoses in one day: the resources map and the connectivity map both
reported the stylesheet absent while the fix for exactly that sat verified on
disk. Served locally, the same gate passes all thirteen leaves.
The identity guard cannot close this. It fingerprints the manifest to catch the
wrong worktree on a localhost port and deliberately passes a remote target,
because lagging behind a checkout is a deployment's normal state. The output
was the thing at fault: the one number that tells the two failures apart was
never on screen.
A full check:host-locale-contract run loads every leaf twice and hit the 900 s default three times
(exit 75, NOT MEASURED). An env prefix in its package.json script cannot help: the wrapper reads its
limit before the script's environment exists. So a gate declares it in its header
(@browser-slot-max-hold <s>); the wrapper reads it for 'node <file>' and 'bun run <script>' alike.
The caller's BROWSER_MAX_HOLD still wins, a declaration is capped at 3600 s, and every other
command keeps the 900 s default. Tested on a private slot dir: declared 3 s -> killed at 3.1 s;
caller override 1 s -> 1.1 s; bun run form -> 3.2 s; 99999 -> capped to 3600.
The output tree's name has always been configurable (vite.doxa-maps-build-config.json "name"),
but 61 tooling files ignored it and hardcoded 'doxa-maps' (181 literals), so a rename would have
moved the build while the gates kept reading the old folder.
- internal/_tree-name.mjs reads it once (validated as one URL-safe path segment; falls back to the
historical name) and exports TREE_NAME / TREE_NAME_RE; `node internal/_tree-name.mjs` prints it
for shell scripts.
- A codemod rewrote 171 literals in internal/ to TREE_NAME; the regex literals, messages with
backticks, atomic-public-swap.sh (fails loudly if the name cannot be read), build-lock.sh and
vite.config.js (its duplicate readMfeName is gone) were done by hand.
- The name is unchanged: old and new tooling produce all 1408 files under public/ byte for byte.
Code lines still naming the tree in internal/ + vite.config.js: 2, both intended (the helper's
fallback, the private-repo-name pattern in public-hygiene-check).
This renames nothing visible; the eventual rename becomes a one-line config change.
Two surfaces join the leaf and the Gallery, each built from the manifest's own snippets so a
broken snippet fails as well:
B a host on a different origin iframes the leaf; the plugin must draw its data and the
host's title, URL and scroll must not move.
C a host on a different origin pastes embed.script with its own token and provider. Under
maplibre: 0 api.mapbox.com requests. Under mapbox, where the plugin draws a Mapbox map, a
second run with a fake host token must show that token, and no other, in tile requests.
MapLibre-only plugins take no token and report n/a.
The self-test now also poisons the bundle under the script embed. Rows print their seconds;
--surfaces A,B,C,D splits the ~26 min run. Measured: 7 bundles, 60 rows PASS, 3 n/a.
B does not assert 0 api.mapbox.com: every generated leaf head links Mapbox JS/CSS whatever
the provider, which is a template property reported separately.
Injecting mapbox-gl.css collapsed this profile's canvas to a white strip, and
every assertion I owned went green over it: verify-gospel-map counted 2,107 of
2,107 pins "rendered", leaves-mount found a full shadow root, the i18n gates
found every label translated and nothing clipped. queryRenderedFeatures does not
care whether anything is visible. I caught it by opening the screenshot, which is
not a system.
So: screenshot the map region and count distinct colours — a basemap has
hundreds, a blank rectangle one or two — and check the map still fills its
element.
THE POISON ARM CORRECTED THIS GATE TWICE, which is the argument for having one.
First it hid the canvas, and the region still showed 316 colours: the sample was
taken over the whole custom element, and the legend card is itself full of
colour. The sample is now clipped to the right of the legend, so it looks at map
alone.
Then it collapsed the container, and the gate still passed — because I was
comparing the canvas to its own container, and in the real incident BOTH
collapsed together, leaving that ratio at 1.0. The check would have passed the
exact defect it was written for. The ratio that matters is map-to-ELEMENT, and
the poison now reproduces the incident rather than approximating it: 1440x300
inside a 1440x900 host, caught at 33%.
Measured after: 644 distinct colours, map filling 100% of the element, and the
poison arm red.
It reported four failures and could not be run in CI, so a real
regression would have landed behind an already-red gate. Two of the four
are not defects at all, and two are open in a way code cannot close.
PROPER NOUNS, now allowlisted with reasons. "Open Bible Stories" and
"Scripture Earth" are the names of the organisations that publish these
resources, not descriptions of them: a reader following the link meets
the English name on the destination site, so translating it would hide
the thing it identifies. They sit in the same res.* block as
`jesusFilm: 'JESUS Film'`, which the product already treats as a brand,
and resources-list.vue:234 maps the literal string 'Scripture Earth'
back to this key — it is an identity, not a label.
DRIVER-GATED, and allowlisted as a BOOKMARK rather than a verdict.
legend.columns.upgs and prayerLaps.upgs really are an English acronym
wedged into an otherwise localized row, in Arabic and Chinese included,
while `population` beside them translates normally. That stays true; it
is simply not resolvable here. Measured this tick: the column's own
tooltip (legend.sortByUpgs) is ALREADY translated in all 11 with only
the acronym left English — "Ordenar por UUPG", "按 UUPG 排序",
"ترتيب حسب UUPG" — and the widest candidate phrase is 28% of the 380px
panel (zh costs 9px). So neither engineering nor space is the blocker:
it is terminology only, and an invented acronym looks AUTHORITATIVE to a
reader who cannot check it.
★ "ALLOWED" IS NOT ONE THING, and collapsing the two would make this
gate lie. A proper noun is SETTLED — correct English forever. A
driver-gated key is an OPEN DEFECT parked so the gate can still catch
the next regression. Calling it "identical on purpose" would turn a
deferral into a verdict, which is exactly how an open item stops being
visible. The gate now separates them: the PASS line itself reads
"2 deferred pending a ruling" and names both keys above it.
MUTATION TESTED — making a NEW key identical in all 11 locales
(buttons.prayerList) still fails it, exit 1, naming the key. The
allowlist exempts without blinding.
The priced options are on
DECIDE-UUPGS-COLUMN-HEADER-IS-ENGLISH-IN-ALL-ELEVEN-LOCALES; these two
lines come out when the Driver rules.
Arabic text was rendering inside a left-to-right layout: readable glyphs
in the wrong place, which reads as a styling quirk rather than the bug
it is.
TWO DEFECTS IN ONE BLOCK (library/i18n/index.js, createAppI18n):
TARGET — it wrote document.documentElement.dir. An EMBEDDED widget
doing that flips the PARTNER'S WHOLE PAGE: their headings, their nav,
their footer. The existing comment already worried about the host
owning that decision, but guarded only against CLEARING dir, never
against setting it.
TIMING — it ran at createAppI18n() construction, reading
detectLocale(), which sees only the URL and <html lang>. A locale
arriving later through profile-config — the channel a HOST uses —
never reached it. Measured across the previous landing: htmlDir null
on every host-channel run, including leaves whose chrome was fully
Arabic.
Direction now binds on ProfileLoader's own root, driven by
effectiveLocale — the locale actually in force AFTER profile-config, so
it re-evaluates when a host changes the language. This element cannot
tell "I am the page" from "I am a widget", and guessing wrong is exactly
the failure above, so it no longer touches the document at all.
MEASURED on a partner page at a FOREIGN ORIGIN with
profile-config.locale='ar' (same gate, before and after):
before upg-100-mfe plugin root ltr, arabic 1765 FAIL
poster-wall plugin root ltr, arabic 1738 FAIL
after upg-100-mfe plugin root RTL, arabic 1765 pass
poster-wall plugin root RTL, arabic 1738 pass
partner <html>/<body>/<h1> stayed ltr throughout.
Standalone leaves are unchanged and still get <html dir="rtl"> from
?lang=ar — verified, both profiles — because a leaf page DOES own its
document. createAppI18n's document write therefore stays for now;
replacing it needs a page-ownership signal that touches every generated
leaf, which is its own change rather than a rider on this one.
⚠ THE GATE REFUSES TO JUDGE WHAT IT CANNOT SEE, and that guard earned
its place twice before this was measurable. Its first two runs reported
"the element never mounted" — a HARNESS gap, not the product: a partner
embed uses crossorigin="anonymous" (SRI requires it), so the tree must
send Access-Control-Allow-Origin, which `bun run serve` does not. The
gate now proxies the tree through its own CORS-enabled port. Without
that guard those runs would have read as fails-first evidence for a
defect nothing had actually exercised. It also fails if the locale
produced zero Arabic chrome, because direction cannot be assessed on a
page whose language never applied.
Every clone-fidelity gate compares properties of NAMED selectors, so none of them can see an element
that exists on only one side; regions found an extra control and an invented chart by eye, past eight
green gates. This executes the Driver's binary scope test (promoted from regions' probe).
- Harvests every visible text node through every shadow root on both sides and diffs the sets:
OURS-ONLY unaccounted (scope violation) and THEIRS-ONLY (omission) fail; SANCTIONED entries must
carry the Driver's words, date and card or the preset is refused; same-label digit drift is
reported, not failed.
- The negative control is mandatory: a known ours-only string must land in the ours-only set, or the
run prints CENSUS IS BLIND and exits 75. An unreachable live page is also 75.
- Presets live in internal/element-census/<name>.json; ad hoc via --leaf/--live/--neg.
- Measured: regions passes (list 229 vs 228, the only difference its sanctioned poster toggle; Africa
97 vs 97 with 3 live-vs-snapshot figures). With the allowlist removed the toggle FAILS as
unaccounted; a negative-control string not on the page gives CENSUS IS BLIND, exit 75.
A host embeds these elements and sets profile-config.locale.
ProfileLoader applies it by driving the app's i18n directly, but reaches
it with inject('appI18n', null) — and the default is NULL. So a bundle
whose entry does app.use(createAppI18n()) without a matching
app.provide('appI18n', i18n) turned that watchEffect into a SILENT
no-op: no error, no warning, no translation. A partner embedding the
element simply could not choose its language.
FAILS-FIRST, measured with the fix stashed so this is a true before:
my-upg-100-list/upg-100-mfe profile-config.locale='ar' 0 Arabic chars
regions-dashboard/regions-list (provides appI18n, CONTROL) 178 Arabic chars
Same harness, same locale present in both configs. The control is what
makes it evidence rather than a broken probe: 178 vs 0 under one harness
rules out "the config never arrived".
★ WHY HERE AND NOT IN EIGHTEEN index.js FILES. The fix is one line per
bundle, which is exactly why it decays: one fact stored in eighteen
places, and the nineteenth bundle written next month re-introduces it
with nothing to catch it. A vue-i18n instance IS a Vue plugin, so
wrapping its own install() makes the provide a CONSEQUENCE of
installing rather than a step beside it. A bundle cannot forget.
Bundles that already provide the same instance are unaffected —
providing an identical value twice is a no-op, and the control's reading
is unchanged at exactly 178 after.
MEASURED with studio's check:host-locale-contract, which compares each
leaf's host-channel reading against THAT LEAF'S own ?lang=ar reference,
so a leaf with sparse Arabic chrome is not judged against a constant:
parent 18 FAIL
here 7 FAIL
my-upg-100-list/upg-100-mfe 0 -> 3418 (ref 3418, exact parity)
my-upg-100-list/poster-wall 0 -> 2685 (ref 2685)
my-upg-100-list/upg-100-list 0 -> 688 (ref 688)
country-posters/country-poster-wall 0 -> 11025
plus all five gospel-resources leaves and cities-internet
⚠ BIG PICTURE IS NOT FIXED BY THIS, and should not be marked so. It
reads 0 against ref 273 with the wrapper active and locale 'ar' present
in its config, so its failure is a different mechanism — nested children
following the parent's locale — not the missing provide. Six other
composites remain FAIL for the same reason.
The poster-wall gate grew hi and ru (five scripts: Latin, RTL, CJK,
Devanagari, Cyrillic) plus a guard for a locale that cannot detect the
defect at all: if a catalogue ever translates a key to exactly its
humanized fallback, that locale renders identically whether i18n worked
or threw and would pass forever. It now exits 2 NOT MEASURED naming the
key. Its PASS line also states its scope, because a green line pasted
into a scoreboard is where "translations render" silently becomes
"localisation works".
A probe that awaited map.once('load') with no timeout held slot 1 for 89 minutes on 3 s of CPU,
and every other browser gate on the box queued behind it and timed out as NOT MEASURED.
- The command now runs as a child under `timeout --kill-after` (default 900 s, BROWSER_MAX_HOLD);
timeout signals the whole process group, so the chrome a probe spawned dies with it. 124/137 are
reported as 75 NOT MEASURED, naming the command.
- fd 9 (the slot lock) is closed for the child. With exec, the command and every grandchild
inherited it, so an orphaned process kept the slot locked after its parent exited — measured
with the previous wrapper: the next caller got 75 after its wait.
- Proven on a private slot dir (1 slot, 5 s limit): sleep 99999 freed at 5.07 s with 75 and the next
waiter ran at 5.58 s; a TERM-ignoring child killed at 7.1 s; after a command that leaves an orphan
the slot is free in 261 ms; exit codes and stderr pass through unchanged.
resources-list's chrome is translated in all eleven — the three filter tabs, the
generic resource names, the people-group count — and the table's column headers
are localised at render in data-tables. resources-map, data-tables and charts now
report zero leaks by the corrected gate; what remains is in resources-list's
data-driven rows.
A CLAIM I MADE AND HAD NOT MEASURED. The previous commit said the CSV export
"keeps the data file's English field names". It does not: Tabulator exports the
column titles it is given, so a Russian reader's CSV heads "Народ","Страна",
"Регион". I checked only after asserting the opposite. Left translated on
purpose — this is a gift for people to use and someone who exports what is on
their screen should get what was on their screen — with the stable machine
contract being data/tables/ itself, which is untouched and English.
THE GATE NEEDED A THIRD CORRECTION, and this one mattered most. Substring
matching over the rendered text reported keys that were never rendered at all:
"Language" matched inside the DATA — the people-group language "Sign Language" —
and "Region" inside a region name. No length filter separates a label from a
word sitting inside one. The unit is now an element's OWN label, and a leak is a
label that EQUALS the English value while a translation exists. A language name
can no longer impersonate a UI string.
Scripts are alphabetised per the fleet convention, so parallel landings stop
colliding on the last line of package.json.
- check:host-locale-contract: every leaf loaded with ONLY profile-config.locale="ar" must draw at
least half the Arabic the same page draws at ?lang=ar; regions-list is the positive control.
Fails on this commit for 18 leaves (bundles that never provide appI18n, and every composite).
- check:staging-lang: the staging page at ?lang=ar embeds its leaf with lang=ar and the Gospel map
renders gospel.mode.gap in Arabic. staging-forge.js (iframe + open link) and
staging-reference-v2.html now forward ?lang= / ?locale= via withLang(); fails on the parent.
- browser-slot.sh: `exec 9>f 2>/dev/null` redirected the whole script's stderr, and the gate
exec'd after it, to /dev/null — FAIL lines and "not measuring" messages were being discarded.
The redirection is now scoped to the open.
- verify-map-paint: the two regions map leaves join the default list (both paint, local and live).
- Vision card reads ?lang= / ?locale= like the rest of the library.
rega's method: a string is a LEAK when the catalogue has a different value for
the locale and the page still shows the English one — a render fault, not a
missing translation. This measures that across four leaves and four locales.
IT TOOK THREE VERSIONS AND THE FIRST TWO WERE WRONG IN INSTRUCTIVE WAYS.
One: it failed with no explanation. process.exit(1) terminates before pending
stderr flushes, so the run returned 1 with every reason line missing. A gate that
cannot say why it failed is barely better than no gate. It sets exitCode now and
lets the process end.
Two: it read the whole shadow root and reported 113 leaks, nearly all false.
"JESUS Film" and "Scripture" appear inside the explanatory paragraphs this bundle
deliberately keeps in English, and short entries like "for" match inside ordinary
sentences. Crying 113 when a handful are real is worse than silence — someone
would have gone and "fixed" strings that were already correct. Every English
prose block here declares itself dir="ltr", so the check now walks the shadow
root and skips those subtrees, and ignores catalogue entries under six
characters.
WHAT IT FINDS, now that it is trustworthy: 70 real leaks, and every one is in
resources-list. resources-map, data-tables and charts are clean in ar, zh, ru and
hi. That is exactly the debt I declared two commits ago — that leaf got its title
translated and nothing else — now measured instead of estimated.
Scope is the leaf on purpose. Six keys were reported leaking in the gallery and
the leaf is clean for all six: the gallery builds its iframe src with no lang
parameter, so the embed never learns the locale and correctly falls back to
English. Embedder defect, not a bundle one, and a check that conflated them would
send someone to translate what is already translated.
The Driver asked for "all experimental plugins working"; this makes it a
measurement. The gate derives the shelf from the Research Gallery's own rules
(manifest minus curated tabs and NOT_EXPERIMENTAL), then checks every bundle on
its leaf page and on its Gallery tab, under mapbox and maplibre. It requires
content inside the plugin's own element, 0 page errors, 0 failed requests, no
loading text, and for each map 'load' fired AND its own GeoJSON data drawn. An
empty source does not count. Maps are found by wrapping the engine's Map class
in an init script, so no bundle changes for it. It serves this checkout on an
OS-assigned port, and self-tests first: a deliberately broken bundle must fail,
or it exits 3.
Prayer Walk Planner's data is the user's own (zones in localStorage), so the
gate seeds one zone and requires it drawn, printing the seed and reason on every
run rather than exempting the bundle.
It found two real defects on the way. Both are fixed here, and both failed the
gate before the fix:
- UPG Neighbor: every label layer asked MapLibre's default fonts (or Mapbox's
DIN) from OpenFreeMap, which 404s, so no label drew under maplibre. labelFont()
now picks Noto Sans under MapLibre; Mapbox is unchanged.
- Prayer Walk Planner (always MapLibre): zone titles never drew, for the same
reason. The label layer now names Noto Sans Regular.
Result: 28/28 PASS across all 7 shelf bundles.
A new "Experimental shelf" section pictures the two newest shelf plugins, each
shot from its own page after it finished loading: the Wall of People Groups
(all 2,107 groups as photo cards on one zoomable prayer wall, 7fec7426) and the
Prayer Walk Planner (full screen from any embed and a sharp HD print,
30e6e569). The Research Gallery tree now lists all seven shelf plugins
(adds pplr-data-maps and wall-of-people-groups). Shots staled by 92a81ed2 and
later rebuilds are retaken.
BUG-BROWSER-GATES-CAN-MEASURE-ANOTHER-WORKTREES-TREE, the remainder. A
static scan of every internal/ script that drives a browser at a local
target found eight with no identity guard (a9550858 and 59f497eb
guarded the rest):
built tree check-spa-tab-order -> requireThisTree
dev server long-frames-probe, mobile-sheet-probe, theme-flash-probe,
plugin-gallery-smoke, share-roundtrip-smoke (--plugin-gallery),
smoke-studio, legend-columns-probe (only when aimed at
/plugin-designs/; its default target is the DX-life site
it compares against on purpose)
A dev server has no built manifest, so the new
requireDevServesThisCheckout checks the source it serves. It fetches
three library files with ?raw and compares them byte for byte with
this checkout. Another tree's dev server answers differently or 403s,
and the gate exits 75 (NOT MEASURED). Vite answers a plain fetch with
the file's text and an import-style request with `export default "…"`;
both shapes are accepted, since the first version parsed only the
second and refused this checkout's own server.
Proven: against a dev server running from 319064a1's parent and a
built tree whose manifest differs by one byte, all eight exit 75 and
name the differing file (useMapInstance.js). Against this checkout,
check-spa-tab-order and smoke-studio PASS, and plugin-gallery-smoke
gets past the guard. A re-scan finds no unguarded script left.
The Driver asked for full-screen, and for "a print that stays sharp when
zoomed".
Full screen. The planner mounts the shared FullscreenButton (the library's
Fullscreen-API control, with its CSS pseudo-fullscreen fallback for iOS
Safari) as a map overlay. It full-screens the whole planner, toolbar and
map, not just the canvas. Measured from a 520px embed in the Research
Gallery on a foreign page: the planner took 1280x800 and the map grew 459
→ 739px. The label is translated (Arabic "تبديل ملء الشاشة"), because the
planner now keeps the shared vue-i18n instance on its own locale. Before,
Share and Full screen took the PAGE language even when the gallery sent
another.
Print resolution, measured, not asserted. The printed map lays out at
7.53 x 8.4 in on the letter sheet (printable width = 8.5in minus the two
12mm margins; emulateMedia alone does not apply @page margins, which
misread it once as 8.48in). Its image is 2400x2688 px, so 319 x 320 DPI.
A 1:1 crop of the zoomed print was read: small street names (Sancaktar
Yokuşu Sokağı, Baki Dede Sokağı), individual building outlines and
footpaths are crisp, because the map is rendered from vector tiles at
pixelRatio 2, not upscaled from the screen. One PDF page.
check:prayer-walk-planner gains both: full screen from a 520px embed must
take the viewport, and the printed map must measure >= 300 DPI. 10/10
PASS.
UPG-100 carried a ❌ for a PosterWall i18n leak: useI18n() said to throw
where the wall mounts, leaving the print buttons English in every
locale on upg-100-mfe, poster-wall and country-poster-wall.
IT DOES NOT REPRODUCE. Measured on the current build, three leaves x
ro/ar/zh, tree identity checked against the served manifest first: zero
English leaks, zero page errors. ro "Listă de rugăciune / Harta afișului
/ Pachet de cartonașe", ar "قائمة الصلاة / خريطة الملصق", zh "禱告名單 /
海报地图", legend "Opțiuni de tipărire" / "خيارات الطباعة" / "列印選項".
The diagnosis does not fit either: this bundle installs i18n per element
via configureApp, and createI18n is legacy:false, so useI18n() resolves
for every descendant including the wall.
THE REAL DEFECT WAS THE MISSING ASSERTION, and it is worse than the bug
would have been: nothing in the repo checked the shared wall's RENDERED
locale, so a ❌ could sit on the scoreboard with nothing able to confirm
or refute it.
★ AND IT CANNOT BE CHECKED IN ENGLISH. The wall's guard HUMANIZES the
key when useI18n() throws - buttons.prayerList becomes "Prayer List" -
which is deliberate, so a bare host shows readable English rather than a
raw key. It also means "the button says Prayer List" is true both when
i18n worked and when it threw. An en-only check passes forever while
every other locale leaks. This gate therefore runs ONLY in non-English
locales, where the translated string differs from the humanized key and
seeing the key IS the defect.
It covers FIVE consumer leaves, not the three the report named:
key-cities and regions-dashboard mount the same shared wall and were
never checked. A gate that copies a report's sampling inherits its
blind spot.
TWO WAYS IT COULD HAVE PASSED WITHOUT CHECKING ANYTHING, both real on
the first run and both now hard skips rather than green ticks: a wall
with no cards renders no chrome, and regions-list rendered 7 cards with
ZERO wall chrome because that profile mounts the wall without the print
row. Skips are named in the output with their reason - a summary that
counts only the green ones reads as full coverage.
MUTATION TESTED by reproducing the reported condition exactly - forcing
the useI18n() guard to throw - rather than corrupting a string: the gate
goes red with 28-33 leaked strings on every measured combination, which
proves both that it detects the defect and that the defect is genuinely
absent from the unmutated build. Restored, 9 combinations PASS, 6
reported as not covered.
No source change: there was nothing to fix.
BUILD-LOCALIZATION-SURVIVES-THE-PROVIDER-SWAP asked for a check; the
behaviour landed in 319064a1 (library/maps/localize-labels.js) without
one. This reads the labels the map actually DRAWS, at ?lang=en and
?lang=ru on the same view, under both providers:
Mapbox the Standard basemap's 'place-labels' featureset (a plain
queryRenderedFeatures cannot see the imported basemap):
Germany→Германия, Ukraine→Украина, Egypt→Египет… must flip.
MapLibre each country symbol's evaluated text-field: under ru it must
be the feature's name:ru, under en not. English there
restores OSM's own label, so no English word is expected.
It serves this checkout on an OS-assigned port. --dir points it at
another tree for the negative control.
Negative-tested: doxa-simple-map built from 319064a1's parent (no
localize-labels) and served with --dir FAILS on both engines (Mapbox:
68 country labels, none flip; MapLibre: 0/49 in Russian). This tree
PASSES on both (5 known countries flip on Mapbox; 49/49 on MapLibre,
e.g. United Kingdom → Великобритания). A first version counted the
pre-fix Mapbox run as NOT MEASURED instead of FAIL, since it saw no
English names. "Measured" now means enough country labels were drawn.
dx-shepherd's verifier traced it. Merge 52ab6089 from this worktree committed
public/doxa-maps/gospel-resources/gospel-resources.js with 21 conflict markers —
both sides concatenated, 1.13 MB — and that is what threw "Identifier Ge has
already been declared" and killed every gospel leaf.
My diagnosis last commit was wrong. I blamed a module imported twice and
re-routed the import; that change was harmless but it was not the cause. The
REBUILD that came with it is what repaired the file. A plausible cause that a fix
appears to confirm is the easiest kind of wrong answer to believe.
THE ACTUAL CAUSE IS A LANDING SCRIPT I WROTE. On a conflict it resolves the one
file it expects — public/doxa-maps/index.html — and then runs "git add -A
public", which stages every OTHER file under public/ including the ones still
holding markers. One line of convenience turning a conflict git correctly refused
to guess at into a committed artefact.
So: a check that names the real condition. It scans every tracked file for an
unresolved marker, is built from character codes so it can never trip itself, and
carries a poison arm. It runs FIRST in check:gospel-all, because there is no
point measuring anything else about a tree that is mid-merge.
Also verifies the 8 links the GRN recheck recovered, which check:gospel-all
caught as shipped-but-unverified on its very first run — psi, alk, nos, jle, lma
and three more, all now confirmed landing on their own 5fish language pages.
generate-tree decided a single leaf's map-ness as "its own .vue
references an engine OR anything under the BUNDLE's src/ does", so a
map-less profile inherited it from a sibling. regions-dashboard/
regions-list shows a list and no map, but src/RegionCountryMap.vue made
it isMap=true. Its leaf loaded the Mapbox engine for nothing, and since
4a6886d2 its embed snippet also preloaded it.
A leaf's map-ness now follows the RELATIVE imports reachable from its
own .vue, bounded to its bundle, so a map several imports deep
(research-dashboard's DashboardMapPanel) still counts. An explicit
boolean isMap in <profile>.meta.json wins. Composites keep their
separate rule, since they do mount their siblings.
Measured: across all 34 profiles exactly one changed (regions-list,
true -> false). 18 map profiles keep isMap and their preload; 0 non-map
snippets carry one. On a served built tree regions-list makes 0 engine
requests and 0 api.mapbox.com requests and renders its list (673
elements, no errors), where before it downloaded the 1.75 MB engine.
regions-mfe still loads the engine and draws its map. The regenerated
files are only the regions-list leaf, the two manifest copies (still
byte-identical), the staging page and the gallery; check:sri passes.
The gate compared a child's newest source mtime with the parent
bundle's mtime. A git merge or checkout rewrites sources with
checkout-time mtimes, so rega's publish of beb13da3 stopped on
"doxa-maps-spa / plugin-studio / stack-demo STALE", although a rebuild
reproduced the committed bytes exactly. Two of those parents mount
their children by tag and compile none of them in. It was also
gameable the other way: checking out the parents makes them pass.
It now rebuilds each composite into a temp dir and compares sha256
with the committed bundle. Builds are deterministic (two runs give
identical bytes), so a difference is a real source change the
committed parent predates. The new vite.config.js DOXA_OUT_ROOT
redirects that build away from public/; unset, nothing changes. A bare
sibling-profile entry in composes (gospel-resources' resources-map,
data-tables) now resolves instead of being reported unresolved.
Proven: an edit to people-groups-grid.vue with no rebuild fails
big-picture, which compiles the grid, and leaves plugin-studio and
research-map-plus-grid fresh. Touching every child and library source
with no content change still passes.
On today's main: doxa-maps-spa, plugin-studio and stack-demo (the
false alarm) are fresh. country-posters, dashboard-plus-grid and
regions-dashboard are really stale and need a rebuild. About 2 minutes
for all 10 composites; use --only to narrow.
simpleresearch fixed the phone toast (2221d11b): at 390x844 the "Tap a
coloured dot" prompt now sits above the Progress sheet and the legend reads in
full (checked by eye). The phone row now shows seven maps in two rows of four;
Big Picture keeps its note (desktop by design; on a phone the Research Gallery
opens on Simple Research). Gospel Resources shots retaken after its bundle was
repaired on main (57fecfd0 replaced a bundle committed with merge-conflict
markers in 52ab6089). Other stale shots refreshed from the latest builds.
a9550858 guarded twenty gates; these six were still unguarded. Each
now exits 75 (NOT MEASURED) rather than grading another tree:
- check-legend-export-reachable, check-vision-card-numbers --url:
requireThisTree on the URL they are given.
- check-embed-basemap-paints: it picks a RANDOM port, not an
OS-assigned one, so if another agent holds it, its own server fails
to start and it would measure theirs. It now checks the leaf server's
identity, stops its own servers and exits 75. Its "a server did not
come up" path exited 1 and now exits 75.
- check-tabs-localized, check-poster-prints read library/ from
DOXA_REPO. Pointing that at another checkout now needs
DOXA_REPO_MEASURE_OTHER=1 (new requireThisRepo helper).
- check-packet-render refuses a packet file inside a DIFFERENT git
checkout (new requirePathInThisCheckout helper); a file outside any
checkout is allowed with a note.
Proven by pointing each at a wrong build: a copy of this public/ whose
manifest differs by one byte, served on its own port, and another
worktree for the file and DOXA_REPO gates. All 8 (these 6, plus
shell-scroll and upg-map-data) exit 75. On this tree they measure:
legend-export, vision-card, upg-map-data, tabs-localized and
poster-prints PASS. packet-render gets past the guard and fails on a
non-packet file, as intended. embed-basemap measures and FAILS: "the
page does NOT paint a basemap on its own baked token". That is a
product finding, not the guard, and is reported to its owner.