Every map showed DOXA's people groups, and only those. The same maps must serve other projects'
lists: Project 7000 (unreached), 4700 (frontier), 700 (frontier of 500,000 or more), 2000 (DOXA).
The list is now chosen the way the engine is:
profile-config `list` > ?list= > window.MAP_LIST > 'doxa' (exactly today's behaviour)
with ids or project aliases ('700', 'project-7000').
- library/lists/lists.json: each list is a FILTER over the IMB / peoplegroups.org dataset already
in the repo (Lostness Priority, Population, Engagement Status), never a hand-kept roster.
- library/scripts/people-groups/build-lists.mjs (`bun run lists`): writes each list's DOXA members
to data/people-groups/lists/<id>.json, joined the way the Vision Card funnel joins, so the
counts agree with it: unreached 7,341 (2,082 DOXA), frontier 4,754 (1,745), frontier-500k 737
(71), unengaged 2,982 (1,929).
- library/lists/peopleGroupList.js: resolvePeopleGroupList and applyPeopleGroupList(rows, cfg).
The default list returns the same array, unfetched. A list whose membership cannot load shows no
groups (fails closed) rather than DOXA's whole list under another list's name.
- Tier A, every production map: DataSourceManager applies it (and the maps that build one pass
their profileConfig), and so does every direct people-group fetch: people-groups-grid, the
Big Picture header, research-dashboard (data, CSV export; the DOXA-wide /statistics baselines
are skipped under another list), Regions (list, dashboards, their table), UPG-100 (4 fetches +
the poster wall), Gospel's roster, and the Experimental upg-neighbor and Wall. The composites
thread `list` to their children like `provider`.
Gates: check:universal-props gains a `list` column (LIST-FETCH, LIST-DSM, LIST-KEYS; its
self-test now plants 9 bypasses) and passes across 18 bundles, with all-people-groups allowed:
it draws the IMB dataset, so its list is Tier B. New check:list-prop-unit (Node, 17 cases, in
gates:post); poisoning the ladder order or deleting a membership file fails it. Documented in
documentation/reference-universal-props.md section 5.
Headless: all 14 touched bundles build; check:composites-mount 20/20 and check:composite-interop
17 links pass on this tree; no-orphan-gates, script-flags, quoted-scripts, doc-scripts pass. Not
rendered in a browser.
★ THE CAMERA DID NOT JUMP, AND THE DRIVER IS PHOTOSENSITIVE.
rega measured it in a real browser: with prefers-reduced-motion set, the
grid→map framing still animated for ~5.0s on MapLibre and ~6.7s on Mapbox —
indistinguishable from no preference at all, where a jump is ~0ms. The
preference was read correctly here and thrown away one layer down: useMapFly's
`flyToCoords(coords, zoom)` takes NO options argument, so the `{ duration: 0 }`
being handed to it went nowhere. Its neighbour `fitBounds(bounds, padding,
options)` does accept one — two functions in one file, different signatures, and
nothing at the call site to show it.
So the reduced-motion branch now drives the engine directly (jumpTo, or
fitBounds with duration 0) instead of depending on whether a helper forwards an
option. The eased paths are unchanged and still go through mapFly, so ordinary
motion keeps one owner. The library asymmetry is reported separately; it affects
every reduced-motion caller of flyToCoords, not just this one.
check:reduced-motion-jumps proves the DECISION headlessly — it lifts the real
camera block out of the SFC and runs it against a mocked matchMedia and a
recording map, over four shapes of selection:
several peoples reduce=true → map.fitBounds(duration 0) false → mapFly.fitBounds
one people reduce=true → map.jumpTo false → mapFly.flyToCoords
tight cluster reduce=true → map.jumpTo false → mapFly.flyToCoords
globe-spanning reduce=true → map.jumpTo false → mapFly.flyToCoords
It cannot prove the engine honours duration 0 — that stays rega's half. It
proves we ASK for a jump, which is the half that was wrong, and it runs in
milliseconds instead of six seconds of watching movestart→moveend.
★ AND THE SCREEN-READER BLIND SPOT. A source audit of both my maps found 31
user-visible strings that no check could see: 24 with no catalogue entry at all
(invisible to check:composite-locale-leaks, whose criterion is that the
catalogue already holds the string) and 7 that HAVE a translation in eleven
languages and were still typed into the template. Most are 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.
All 31 are wired now; the audit reports 0/0 for this lane. Two were duplicates
resolved by REUSING the existing key rather than adding a second: "Export CSV ↓"
→ dashboard.export_csv with the arrow kept as a glyph, and the chip's "Clear
all" → controls.reset_all, since clearing every filter is the action that button
already names. Two keys for one action is how a control ends up saying two
different things in two languages.
The keys themselves live in library/i18n and are a separate rule-5B handoff, so
until they land every one of these falls back to the English it reads today.
The shepherd's call: translating wording that already exists is not a product
decision. So every string keeps the English it reads today and gains a key, in
all eleven locales.
Big Picture's four first, since it is the localization priority: Export CSV, the
detail drawer's title and its empty state, and the legend's "heartbeat zone"
bin. Then Top countries, Select all, the table dock's Expand/Collapse titles,
and the singular "group" whose plural was already translated — a half-translated
plural is its own small defect.
Full screen / Exit full screen now read buttons.toggleFullscreen, which already
shipped in every locale, so that one needed no new wording at all. The arrow
glyph still carries the state.
Three more went in that were not on the list, because leaving them would have
been worse than the defect being fixed: "Deselect all" is the other half of a
toggle whose first half we just translated, and the two row-selection aria
labels are the same control speaking to a screen reader. A control that changes
language halfway through is harder to use than one that never changed at all.
★ ONE STRING I AM NOT CONFIDENT IN, AND IT SHOULD BE CHECKED BY A SPEAKER.
"heartbeat zone" is the Driver's own coinage, not standard UI vocabulary, and a
metaphor is exactly where a translation I cannot read can go quietly wrong. I
translated it literally in all ten languages. The rest are ordinary interface
terms; that one is a judgement, and I would rather flag it than have it
discovered on the gift.
VERIFIED HEADLESS: i18n-parity passes with zero debt; three bundles build; the
keys AND their Arabic values are present in the built output (تصدير CSV,
منطقة نبض القلب, اختر شعبًا لعرض التفاصيل, and the header's placeholder).
Numerals are untouched, per the hold — this is text only.
(cherry picked from commit 67568c0509)
The shepherd's call: translating wording that already exists is not a product
decision. So every string keeps the English it reads today and gains a key, in
all eleven locales.
Big Picture's four first, since it is the localization priority: Export CSV, the
detail drawer's title and its empty state, and the legend's "heartbeat zone"
bin. Then Top countries, Select all, the table dock's Expand/Collapse titles,
and the singular "group" whose plural was already translated — a half-translated
plural is its own small defect.
Full screen / Exit full screen now read buttons.toggleFullscreen, which already
shipped in every locale, so that one needed no new wording at all. The arrow
glyph still carries the state.
Three more went in that were not on the list, because leaving them would have
been worse than the defect being fixed: "Deselect all" is the other half of a
toggle whose first half we just translated, and the two row-selection aria
labels are the same control speaking to a screen reader. A control that changes
language halfway through is harder to use than one that never changed at all.
★ ONE STRING I AM NOT CONFIDENT IN, AND IT SHOULD BE CHECKED BY A SPEAKER.
"heartbeat zone" is the Driver's own coinage, not standard UI vocabulary, and a
metaphor is exactly where a translation I cannot read can go quietly wrong. I
translated it literally in all ten languages. The rest are ordinary interface
terms; that one is a judgement, and I would rather flag it than have it
discovered on the gift.
VERIFIED HEADLESS: i18n-parity passes with zero debt; three bundles build; the
keys AND their Arabic values are present in the built output (تصدير CSV,
منطقة نبض القلب, اختر شعبًا لعرض التفاصيل, and the header's placeholder).
Numerals are untouched, per the hold — this is text only.
TWO DEFECTS FROM THE UNIVERSAL-PROPS MATRIX, and they are the same defect at two
levels: a component can be perfectly localized and still show English, and a
page can be perfectly translated and still be laid out backwards.
★ THE HEADER HAD NO i18n AT ALL. Every label at the top of Big Picture was an
English literal passed as a PROP — FilterDropdown translates nothing, it renders
what the caller hands it — so Region, Bloc, People Group and Religion sat in
English above an Arabic page. They now read the catalogue, grid namespace first
because this header drives the cap-2 grid and its sibling reads the same keys.
Reset uses controls.reset_all, so its English becomes "Reset all": an existing
entry rather than invented wording, and arguably more accurate, since it clears
the search too.
Its search placeholder and two aria-labels have no usable entry — the nearest,
grid.search, drops "or countries", which this search actually covers — so they
are on the DECIDE card rather than quietly reworded. They are invisible to
check:composite-locale-leaks by design: its criterion is that the catalogue
already holds the string, and it does not.
★ RTL IS A DIRECTION, NOT A TRANSLATION. createAppI18n reflects an RTL locale
onto document.documentElement, which is right for a leaf page it owns and
useless for an element dropped into somebody else's page — the case these
composites exist for. A host asking for Arabic got Arabic WORDS in a
left-to-right layout: readable glyphs in the wrong place, which reads as a
styling quirk rather than the bug it is.
Both composites now set `dir` on their custom-element HOST rather than on the
layout inside it, and that choice is the fix: `dir` takes effect through the
inherited CSS `direction`, and inheritance crosses shadow boundaries, so one
attribute covers the composite's own chrome AND every nested element beneath it
— including the grid half the audit found bare. Only ever SET, never cleared: a
host page that declares its own dir owns that decision.
VERIFIED HEADLESS: three bundles build; setAttribute("dir","rtl") is present in
both composites and controls.reset_all in big-picture; freshness and SRI clean,
nothing else stale; gates:pre 6/6. NEEDS EYES: Big Picture and Simple Research
at ?lang=ar — header labels in Arabic, and the layout actually mirrored.
Embedded with profile-config {"locale":"ar"} this leaf drew no Arabic at all,
while the same page reached with ?lang=ar drew 273 characters of it.
Its two siblings were fixed two different ways and neither applies here. This
bundle's entry calls defineCustomElement with no configureApp: there is no i18n
instance for install-time provide to reach, and no ProfileLoader beneath it to
drive one. Everything this element renders comes through tStatic — the grid's
`t` proxy, the header's labels — and tStatic resolves the locale from the URL
and the document, neither of which a host's profile-config touches.
So the only thing missing was somebody to say which language this page is being
read in, which is the same six lines the other two composites already carry.
MEASURED with check:host-locale-contract:
big-picture/big-picture host 0 of 273 -> host 273 of 273
whole fleet 1 failing -> PASS, 32 leaves
The composite's own header renders "<N> shown" from @groups-filtered, and the
nested grid rendered its own "Total: <N>" line 139px below it. Measured live:
"2107 shown" and "Total: 2,107" — the same number twice, both above the fold.
That is the redundant-header shape the standing rule forbids (the hint lives in
the EXISTING header, never a second one). show-total=false here matches the
pattern already applied in dashboard-plus-grid.vue:279 for the same reason.
The grid keeps its own total when embedded ALONE, where it is the only count on
screen — the prop is host-controlled, not removed.
Work started by a delegated agent that hit its session limit mid-task; verified
the cited sibling pattern exists before committing. check:sfc-parse and
check:template-literal pass.