A fleet-wide census with this gate as it stood would have been worth nothing, and
worse than nothing once mailed to seven owners. Three things had to be true
first.
IT HAD TO SEE OTHER PEOPLE'S CALL SITES. The first version matched tStatic/$t/t
and found four missing keys while sailing past the ten it was written for. Adding
`T` by hand took it to 22 — and that fix was the same mistake one level up. A
census of the tree finds FOUR wrapper names, not one: `const T` x16, `const L`
x6, `const tr` x1 (library/layouts/VerticalTabsLayout.vue) and `const v` x1. A
gate that knows about T and not tr is blind to whichever bundle used the other
name and reports it CLEAN. So the names are now read out of each file, and the
next author's alias is covered the day they write it. Negative-tested by renaming
T to Tz in a file: all six of its call sites are still found.
IT HAD TO RESOLVE THE PREFIX HELPERS IT WAS DECLINING TO. All six `L`
declarations carry a static prefix — four are `tStatic('dashboard.' + key)`, two
are a ladder `tStatic('controls.' + key) || tStatic('dashboard.' + key)`. Those
are ordinary keys with a known prefix, not runtime expressions, and counting them
as unresolved left most of this gate's coverage on the floor: 49 call sites moved
from unchecked to checked, and the unresolved count fell from 80 to 28. A key
resolves if ANY prefix in the ladder holds it, because that is what the ladder
does at runtime.
★ AND THE FIRST RUN OF THAT RESOLUTION INVENTED A DEFECT. BpResearchHeader
renders `L('reset_all', '') || tStatic('controls.reset_all') || 'Reset'` — the
author saying "if this prefix has nothing, fall to the next rung", and
controls.reset_all does hold it. Reported as missing, that is a gate manufacturing
a problem out of a working ladder — the precise failure I was about to mail seven
owners about. An empty English fallback is the author's own signal that the miss
is intended, and is now honoured.
IT HAD TO MEASURE A TREE IT IS NOT STANDING IN. A census has to be taken against
current main; every bundle but mine is stale in my worktree, so a local scan would
report keys another owner fixed an hour ago. A second checkout was not available
either (the box is at 97% disk), so `--rev` reads the files as blobs out of the
object store: one ls-tree and one cat-file --batch, parsed by length rather than
by scanning for a delimiter that could occur inside a file. No worktree, no temp
copy, no writes.
AND IT HAD TO STOP ITS OWN SILENCE BEING READ AS A PASS. This gate asks whether
the keys a bundle NAMES exist. A bundle that names none has nothing to be missing
and passes without a word — and seven do, including two of mine whose composites
render no chrome of their own. Reporting only the misses would let "0 missing
keys" be read as "fully localized", which is the opposite of true for exactly
those bundles. Participation is now counted and printed beside the result, and
the two are never merged into one number.
THE CENSUS, at origin/main 1a761741: 16 missing keys, and all sixteen are in this
lane — research-dashboard 14, big-picture 1, doxa-research-map 1. Every other
owner is clean. All sixteen are already fixed by this branch's parent, which
PASSES under `--rev`.
SPEC-DASHBOARD-KIT D1. filterSet[key] may hold an array meaning "any of these";
keys still AND together. The array support is four lines. The reason this is a
landing rather than a patch is the other half.
`a(r) === filterSet[k]` WAS WRITTEN OUT SEVEN TIMES — the shell's filteredRows,
the facet rail twice, the map panel, the table, the charts rail and the CSV
exporter. Seven copies of a one-character rule are survivable while the rule
stays one character. The moment it grows an array case, six of the seven keep
comparing an array with ===, every one of them quietly matches NOTHING, and the
panels disagree with each other about what is on screen. That is not abstract:
the facet rail computes the COUNTS a reader acts on before clicking, so a rail
still using === would print counts for a scope the map and table no longer show.
So the predicate now lives in the bus and every panel calls it. The
de-duplication is what makes the four lines safe.
WHAT A CLICK DOES IS UNCHANGED, deliberately. A plain click still REPLACES. D1
gives the bus the capability; making a rail row additive on an unmodified click
would change what every existing click does to the flagship, which is a product
decision and not this one. The array path is reached programmatically today.
AND THAT PATH IS THE PAYOFF: "Arabic" now narrows the grid. The language tier
covers 22 ISO codes, so no single value expressed it, and the composite had to
send nothing and leave the axis unfiltered — honest, and visibly incomplete,
because the map narrowed and the grid did not. The whole code set now crosses and
the grid ORs it. Verified per tier on the committed fixture rather than on one
example: for all 805 tiers the joined set selects exactly the rows the map
selected.
★ THE SEPARATOR IS `|`, AND MY FIRST DRAFT GOT THAT WRONG IN A WAY WORTH
RECORDING. I joined on `,` and broke label matching outright: 120 of the 864 live
language labels are comma-inverted ("Arabic, Mesopotamian"), so a selection made
BY LABEL was chopped in half and matched nothing. A separator that can also occur
inside a value is not a separator — the rule the URL hash states one file over,
which I had just written and then violated. check:filter-multivalue caught it
before it left this machine, and it caught it because it asserts a behaviour I
was KEEPING (label matching) and not only the one I was adding. Asserting only
the new behaviour would have shipped it.
EMPTY IS NOT A FILTER. An empty array clears the key rather than matching
nothing — a UI deselecting its last chip produces exactly that array, and
treating it as "narrow to zero" is the silent-empty-screen failure the hierarchy
contract already exists to prevent. A one-element array collapses to the scalar
it means, so the hash, the chips and every adapter keep their existing shape
until somebody actually picks two.
THE SURFACES THAT CANNOT SHOW A LIST SAY SO HONESTLY. labelFor resolves each
value and joins them, at the one owner, so the chip bar, the research-filter
chips and the CSV provenance line cannot drift apart. A dropdown shows NO
selection when the key holds several: ticking the first of three would state
something false about the board, which is worse than stating nothing, and the
chip still names every value so the selection is never invisible. The URL hash
carries a list and drops the display slug for it — the slug is one human name for
one code, and there is no honest slug for twenty-two.
check:filter-multivalue runs the real bus, greps the panels for surviving copies
of the predicate, round-trips the hash including a value containing a comma, and
lifts the grid's valueMatches to prove both ends OR the same way. No browser.
Green: check:filter-multivalue, check:language-code-link, check:facet-cascade and
its self-test, check:kpi-unit, check:csv-shape, check:no-free-identifiers,
check:sfc-parse, check:mjs-parse, check:descriptions, check:no-orphan-gates,
check:doc-scripts, check:script-flags, check:quoted-scripts.
SOURCE ONLY. Stacked on kit landing 1, which moved the bus into library/.
STUDIO: needs a rebuild of research-dashboard, dashboard-plus-grid,
doxa-research-map, research-map-plus-grid and people-groups-grid. package.json is
main's copy plus two lines beside check:fits-screen.
A localized CALL SITE is not a localized STRING, and nothing in this tree was
watching the join between the two.
The dashboard's five resize handles read their names through
T('aria.resizeFiltersRail', 'Resize filters rail'). Reviewed, that looks
finished: the call goes to the catalogue and the English is only a fallback.
aria.json held eleven keys and not one of them was a resize key. Every handle
had been announcing itself in English in all eleven locales, and the fallback —
which is right to exist, because a missing key must degrade to readable English
rather than to "aria.resizeFiltersRail" on screen — is exactly what kept it
silent.
No existing gate can see this. check:i18n-parity is satisfied because the key is
missing from ALL locales equally. check:english-leaks and
check:composite-locale-leaks both need the catalogue to HOLD the string before
its appearance counts as a leak, and it does not. check:ui-strings-have-keys
reads templates for typed English. Each is looking at a different end of one
wire. check:i18n-keys-exist asks the question none of them asks: does the key
this code names exist?
It found TWENTY-TWO on its first run, all in this lane — the detail drawer's
five rows, the table's two selection toggles and their hints, the map's loading
line, the breadcrumb, the charts rail and the map legend's aria labels, the
empty-search message, and big-picture's footer. Six of them are the resize keys
on this branch's parent; seventeen are added here, English wording kept exactly
as each call site already reads it, in all eleven locales.
★ THE FIRST DRAFT OF THE GATE WOULD HAVE MISSED THE DEFECT IT WAS WRITTEN FOR.
It matched tStatic/$t/t and reported four missing keys while sailing past the ten
resize keys, because the house pattern is a file-local alias —
const T = (key, en) => tStatic(key) || en — and almost every call site in the
dashboard reads T(...). Matching T( took it from 4 to 22. Prefix helpers like
BpResearchHeader's L(), and any concatenated or template-literal key, are COUNTED
as unresolved and never guessed at: inventing the key set a runtime expression
might produce is how a gate starts reporting keys nobody wrote.
AND BIG-PICTURE, the last HOST-LOCALE failure fleet-wide, gets the two halves it
was still missing. Its title was a hardcoded English literal with no key at all —
the largest text on the element, sitting above an Arabic grid. And it never
reflected `dir`, alone among its siblings, so a host asking for Arabic got Arabic
WORDS in a left-to-right layout: a bug that reads as a styling quirk, which is
why it outlived every locale fix around it. The attribute goes on the
custom-element HOST, because `dir` takes effect through the inherited CSS
`direction` and inheritance crosses shadow boundaries — one attribute covers this
element's chrome and both nested elements beneath it. Only ever set, never
cleared: a host page that declares its own dir owns that decision.
The gate is green with this branch and its parent together, and red without
either. Negative-tested: deleting a key that exists fails naming the key and the
call site; a first argument whose area is not a catalogue file is correctly not
reported.
STUDIO: no rebuild needed for the catalogue, but big-picture must be rebuilt.
package.json adds two lines beside check:i18n-never-translated — a different
region from sr/dashboard-filter-a11y and sr/language-code-link, so the three
merge.
- check:dashboard-geocoder-provider: 0 search bars on BOTH engines. Its default leaf (research-map) hides
the bar under MapLibre on purpose, and the tokenless Mapbox run could not draw. It now defaults to
doxa-simple-map (a bar on both engines), and the Mapbox run gets a host token: TEST_MAPBOX_TOKEN, or a
well-formed fake whose token-carrying requests are aborted, never sent.
- check:composite-embed-knobs: every tk case SKIPPED (no baked token since the auto engine), so every
provider case went UNMEASURED and the gate still PASSed. It takes TEST_MAPBOX_TOKEN, and a run where no
case measured anything exits 75 NOT MEASURED instead of PASS.
- check:linked-vocabulary: it attached its groups-filtered listener after goto and could miss the grid's
first emit ("the grid never reported a total"). The totals are now recorded from page start
(addInitScript).
Not browser-run here (browsers are rega's): rega's next gates:post pass is the proof.
The shared approximate-pin note (pins.approximate) turned gospel-resources' own hardcoded copy of the
same sentence into 'has a key but typed in'. Recorded as known debt for gospel-plugin to switch to
ApproxPinNote; no string was added.
Third and final batch of the shared-library strings, in the shepherd's order:
SemanticTreeLegend (4), PosterPreview (2), PosterWall (2) and
VerticalTabsLayout (2), plus PosterWall's "people groups" which already had a
key and was still typed in.
These are all LOADING and OVERFLOW states — "Loading data…", "Showing top 500 of
2,107", "use the geocoder above to find the rest", "Loading map snapshot…",
"preview unavailable", "Could not load X". Text a reader meets exactly when
something is slow or has gone wrong, which is the worst moment to be handed a
language they do not read. PosterWall's own source called one of them "i18n
DEBT" in a comment; it is paid now.
The counted ones take a placeholder rather than being split around an
interpolation — legend.showingTop is "Showing top {n} of {m}" — so a translator
can move the numbers within the sentence. Languages do not agree about where a
count goes, and a sentence assembled from fragments cannot be reordered at all.
VerticalTabsLayout needed a different shape: it is an options-API component, so
a module-level helper compiles fine and then renders nothing, because its
template can only see what the component returns. T is exposed from setup().
★ THE NEW GATE CAUGHT SOMETHING I WOULD HAVE MISSED, on its first real batch.
Adding these keys flipped two strings in
plugin-designs/pplr-data-maps/src/components/SemanticTreeLegend.vue from "no
catalogue entry" to "has a key and is still hardcoded" — that file is a COPY of
the library component I just fixed, carrying the same debt. It is another lane's
plugin so I have not touched it, and it is reported rather than quietly
baselined away. The baseline is refreshed to today's truth so the gate guards
the new line: 285 → 273 without a key, and the two copies recorded as known debt
rather than as a regression.
check:composite-locale-leaks asks a good question — "does the catalogue
translate this string, and is the page showing English anyway?" — but its
criterion is that the catalogue already HOLDS the string, so a string nobody
ever added is invisible to it by construction. And it runs in a browser, so it
only ever sees what happens to be on one screen.
That blind spot was measured, not guessed. After both composites passed the leak
gate with zero findings, a source audit of the same two trees found 31 more
user-visible strings. Most were aria-labels, titles and tooltips: text only a
screen-reader user or a hovering mouse ever meets. Our localization verification
had been "what can I see", and the blind spot was exactly the readers who most
need the words.
TWO CLASSES, because they are different defects with different fixes:
NO KEY wording must be written and translated — sometimes the Driver's call.
HAS A KEY the translation already ships in eleven locales and the reader is
shown English anyway. The cheapest defect in the catalogue: the
work was done and wasted. Nobody was looking for this one; it only
appeared because the report was split in two.
★ WARN-ONLY AGAINST A BASELINE, DELIBERATELY. Failing on the whole backlog — 285
without a key, 29 hardcoded-with-a-key across 126 components — would make it
unlandable and it would be switched off within a day. It fails on what is NEW, so
the count can only fall; --update refreshes the baseline when a batch lands.
QaCorner is excluded BY NAME, with the reason in the file: an opt-in developer
widget that studio is making host-neutral or removing. Eleven locales for a
corner only we open is not worth the translations, and a judgement like that is
better written down than left as a silent gap.
NEGATIVE-TESTED ON BOTH CLASSES, because a gate never seen to fail is not
evidence: a new text node and a new title attribute are both caught and named;
an aria-label hardcoded to "Reset all" — which already ships in every locale — is
caught as the second class with the fix stated. Restored, it passes.
Localizing the grid's data (ae5e27c3) would have broken the language link
silently, and the shepherd asked me to prove the coupling with a test. Measuring
it first showed the coupling should not exist.
MEASURED against the live API, en vs ar, 2,107 records:
primary_language LABEL 864 names in en, 864 in ar, ZERO shared → localized
primary_language VALUE 864 codes, ALL 864 shared → stable
religion / wagf_region VALUE also identical across locales
So the fix is not "make both halves fetch the same locale and test that they do".
It is to link on the form that does not move. Region already links on a slug and
religion on a family letter; language was the one facet still travelling as a
display name.
The map now emits `languageCode` alongside `language`, the composite carries
{value, label} for language as it always has for region, the GRID is driven by
the code (valueMatches compares the normalized value as well as the label, so a
code matches in every locale) and the MAP is still driven by the name, because
its legend selects by name. Each half gets the form it can actually use, and
neither depends on the other's locale.
★ THIS TREE HAD ALREADY PAID FOR THIS ONCE. deriveLanguageFamily in useMapData
carries a comment about the 2026-06-22 locale colour bug: deriving a family from
the localized label collapsed every pin onto one bucket, and the fix was to use
primary_language.value. Same trap, one facet over, three months later.
check:linked-vocabulary now runs per LOCALE as well as per provider — en as the
control, ar as the case that would have broken in silence — and asserts the
language facet with a poison case (a language that cannot exist must NOT narrow
the grid). It is a browser gate, so rega runs it; I have not.
★ AND check:no-free-identifiers CAUGHT MY OWN BUG IN THIS COMMIT on its first
outing: I wrote `rows.find(...)` in the emitter where no `rows` was in scope. The
gate named the file and line before anything was built. That is the class of
mistake that broke both composites yesterday, found in seconds this time.
I broke both composites on main. Editing them with a pattern-match script, I
replaced an import line by matching text that had already changed, so the
replacement silently matched nothing and three names — RTL_LOCALES,
getRuntimeLocale, detectLocale — were used and never imported. Setup threw a
ReferenceError and both elements rendered nothing until a hotfix landed.
★ IT PASSED EVERY GATE, AND THAT IS THE INTERESTING PART. It compiled, because a
free identifier is valid JavaScript until it RUNS. It passed my grep of the
built bundle for the new code, because the code was genuinely there. It passed
template-literal, sfc-parse, composite-freshness and SRI, because not one of
them evaluates a setup function. The evidence I had could not distinguish a
working composite from a dead one, and I called it "verified headless".
So this asks the one question those gates cannot: is every name a component uses
actually bound — imported, declared, a parameter, or a known global? It reads
the same source the compiler reads, tracks real scopes (module, function, block,
catch, class), and runs in milliseconds with no browser. Fleet-wide, over every
.vue in plugin-designs/ and library/, because the mistake was a text-editing
habit rather than anything about one plugin.
WRITTEN BY RUNNING IT, which is the only way an allowlist gets honest. The first
run reported 4,259 findings — every one a false positive from a real bug in my
own walker: a helper that takes a node TYPE was being handed the NODE, so the
function branch never fired and every parameter read as unbound. The second run
found the globals this tree genuinely uses (d3, customElements, ShadowRoot, CSS,
Function, self, the typed arrays) and vite's build-time defines. The third found
a real gap in the gate: an SFC may have BOTH a plain <script> and a <script
setup>, and they share a scope — LegendTools exports a function from one and
uses it in the other, which I was reporting as free. Reading only one block
would have taught the fleet to ignore this gate on its first run.
NEGATIVE CONTROL, because a check that has never been seen to fail is not
evidence: restoring the broken import makes it FAIL naming all three names and
their lines; restored, it passes across 127 components.
It is the cheap half of a pair — it proves a name is bound, not that the thing
runs. The expensive half is a check that actually mounts a composite.
BUILD-INTEROP-CONTRACT-GRID-UNDER-MAP-BOTH-PROVIDERS asks for a provider matrix:
the same assertions, once per engine, with identical counts. Interoperability
that holds on one provider and not the other is not interoperability.
It is not a formality here. The map half of this link resolves a religion family
through its own legend, and the legend is drawn by the map engine — MapLibre
drops an entire layer in silence for one misplaced paint property, which would
surface as a link delivering nothing while the Mapbox run stayed green.
So the gate now runs the whole measurement under ?provider=mapbox and
?provider=maplibre, and then asserts the two agree: the same starting total, the
same rows for the same family, and the same rows over the composite's wire. A
per-engine pass with different numbers would mean the same choice narrows to
different peoples depending on which engine a partner embedded.
MEASURED, identical to the row on both:
mapbox 2,107 groups · Buddhism 83 direct · 83 linked · mutant 0
maplibre 2,107 groups · Buddhism 83 direct · 83 linked · mutant 0
The mutant still runs first on each engine, so a green here still means the gate
can tell a right mapping from a wrong one — twice over now.
The research verify-* scripts and research-map-plus-grid's two gates hardcoded 'doxa-maps' (11
literals); they now import TREE_NAME from internal/_tree-name.mjs like the rest of the tooling.
A reader on an Arabic page saw a handful of English words among the Arabic:
the table's "People Groups" title, its count line, the Engagement column, the
Gospel Access chart title, and the side drawer's close button. Every one of them
had a correct Arabic entry sitting in the catalogue; nothing looked it up.
THE CHECK CAME FIRST, AND IT REPLACES A SWEEP THAT PASSED A BROKEN TREE.
The old method sorted every rendered string into third-party / data / chrome and
counted only chrome. That split is a guess, and it guessed wrong exactly where
the defect lived: single words like "Region", "Prayer" and "Country" are as
plausible as values as they are as labels, so they scored as data and the sweep
reported zero on a page that was visibly leaking.
check:composite-locale-leaks asks a question with an answer instead:
the English catalogue holds this exact string as a value, AND
this locale holds a DIFFERENT value for that key, AND
the page still renders the English one.
No judgement about what kind of word it is — if the translation exists and the
reader is not getting it, that is the defect. The two surfaces a lookup cannot
judge (vendor map chrome, and table/facet cells carrying API values) are muted
by NAME with a stated reason, never by word, so muting one can never hide the
next real leak that happens to reuse it. The gate failed on this tree naming 9
strings before any of them were fixed.
Also threaded here: both composites now accept `lang` / `locale` in
profile-config and pass it to the children they mount, alongside `tk` and
`provider`. A partner writing {"lang":"ar"} reached nothing before — the knob
was documented and read by nobody.
The prayer facet's label in DashboardResearchFilters is now a catalogue getter
too. Measured before touching it: dashboard.config.js does define prayer_level
and FACETS spreads the config last, so that literal never rendered for this
config — it is a default for a config that omits the facet, and it stays, with
the merge order written down so the next reader need not re-derive it.
Four strings remain English with no catalogue entry at all — "Top countries",
"Select all {n}", the table dock's title, and the singular "group". They need
wording in ten languages that cannot be composed from existing entries, so they
are a DECIDE card for the Driver rather than translations invented here.
A reader who picks a religion family on the map now sees the grid narrow to the
same peoples, and a language selection travels the same way. Region already
linked; these two did not, because the two halves do not name a religion the
same way and nothing yet proved what the honest translation was.
The map filters religion through its legend's flat tab, keyed by the FIRST
LETTER of the religion code ('B'); the grid filters through a derived axis that
runs library/colors/religion.js forward per row ('BLA' -> 'Buddhism'). One
shared table, read in opposite directions, copied by neither half — so the wire
carries the family NAME and each side resolves it against its own data.
Two measurements changed the design on the way:
- mapStore.filters.religions, which the map's filter panel writes, is read by
NOTHING: the mapLayers.applyFilters() it was meant to reach is called and
never implemented anywhere in the tree. A link routed through it would have
set state, passed a smoke test and dimmed no pin.
- the map's legend selections are mutually exclusive by design, so it can
light exactly one of region/religion/language. The composite mirrors that
rather than letting the grid hold three facets the map cannot show.
Religion links at the FAMILY and not at the code, deliberately: a map cannot
distinguish 'BLA' from 'BMA' — both dim as Buddhism — so linking at the code
would promise a precision one half cannot keep.
check:linked-vocabulary proves it by measurement rather than by reading: it
compares the total reached over the composite's wire against the total the grid
gives for the same facet on its own (83 of 2,107 both ways), and runs the MUTANT
first — the legend key 'B' instead of 'Buddhism' — failing as NO-TEETH if the
wrong mapping scores the same as the right one. Measured: mutant 0, real 83.
The CLI doc's apk:debug quote was realigned in passing; package.json had gained
app:start-page and the doc still described the older chain.