Commit graph

1914 commits

Author SHA1 Message Date
Doxa Maps
6abf71908c Count provinces from the geometry the map actually draws
f728831d switched the interactive maps to the web-sized boundaries — 232 KB against
1.84 MB, which matters on a leaf built for thousands of embeds — and this builder kept
reading the full-precision file. The map would have drawn provinces from one outline
while the table counted them from another. My own staged-assets gate caught it on the
merge, which is exactly the drift it was added for a day ago.

Measured before following the map rather than after: same 36 states, same names, 0 groups
move province, 0 dropped, and 2 GAINED. Great Andamanese and the Sentinelese had been
listed as unassigned because their points fell just outside the detailed coastline of
islands that ARE in the file; on the simplified outline both land inside Andaman and
Nicobar. 1,005 assigned becomes 1,007 and unassigned 4 becomes 2 — only Jing and Pana on
China's Vietnam and Laos borders, outside its twelve-province subset.

A coarser outline is not a worse answer here. It is a more forgiving one at the scale a
single coordinate for a whole people is honest to.

The note that called those two a precision artefact said "Four remain" and named them; it
would have been false the moment this landed, so it says what is true now and records what
the switch measured.
2026-09-27 04:19:40 +00:00
Doxa Maps
873d73f5eb Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-27 04:17:14 +00:00
Doxa Maps
5d83febdcc Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: dashboard-plus-grid
doxa-research-map
research-dashboard. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-27 04:06:38 +00:00
Doxa Maps
2c5d9f47ec Two setup-time defects: a TDZ that blanked the map, and a cleanup that never ran
Both came from the Driver's console in the gallery's Simple Research tab, and both come from one
gap: where a line SITS in a setup block is not WHEN it runs.

research-map.vue — "Cannot access '_lastFitKey' before initialization", thrown during setup, which
left the map blank. The externalFilters watcher is immediate:true, so it runs while setup is still
on the stack and calls applyFitSlugs() 350 lines below it. The CALL resolved, because a function
declaration is hoisted; its constants are not, so the first read hit their temporal dead zone. A
hoisted function promises that its NAME resolves early, not that its BODY can run early, and that
asymmetry is invisible from both ends: the watcher looks like it only touches the store, and the
constants look like they sit right beside the function that reads them. They and the guard now sit
above the watcher, with a note saying why they cannot live next to applyFitSlugs().

DashboardMapPanel.vue — "onBeforeUnmount is called when there is no active component instance".
A lifecycle registrar closes over Vue's current instance, which exists only while setup is on the
stack, so a hook registered after "await initializeMap()" is silently dropped: a ResizeObserver on
document.body plus a window resize listener outlived the component. Nothing threw and the feature
worked, so it showed only as a studio that slowed down the more tabs a reader had visited. The fix
is not to move the line — the code that creates those handles is past the await either way. The
handles move to setup scope and are torn down in the hook this file already registers correctly,
which is the pattern it was using three lines away for containerRO and attribRO.

research-map.vue held a second copy of the same leak, its scroll-zoom resync listener. The new
gate found that one; reading the file had not.

check:setup-order covers both classes. Its own false positives are the instructive part: 6 hits
down to 1 over three rounds, and every one it dropped was prose or shadowing read as code — an
onBeforeUnmount inside a console.error STRING, the word "table" inside a new Error(...) template
literal in four files, and a destructured byCountry shadowing a computed 220 lines away. So it
blanks comments, string text and template text while keeping ${...}, which is code, and subtracts
every name a span declares for itself. It self-tests on both shipped defects, both fixes and all
three decoys, and exits 3 if an arm goes blind rather than passing quietly.

Source only. doxa-research-map, research-dashboard and dashboard-plus-grid need a rebuild.
2026-09-27 04:00:31 +00:00
Doxa Maps
6af6381004 Make the gate's description true — the previous message claimed it already was
e5398c07 said "its package.json description no longer claims a static arm this file
does not have". It did still claim it: the edit that was supposed to do that threw on
an escaping error, I read the gate output that followed and committed anyway. So the
description described a gate with an arm it had just lost, and the commit message said
otherwise — the worse of the two, because a wrong message is what the next reader trusts
instead of looking.

Now actually done: the static-arm clause is replaced by why it was deleted, and the
self-test sentence names --self-test on the tree-wide gate instead of a
check:engine-contract:poison that no longer self-tests anything.
2026-09-27 03:49:43 +00:00
Doxa Maps
e5398c072f One gate owns the source rule; this one keeps the six measurements
check:no-silent-engine is in gates:post as of main 23976fb0 and scans every plugin for
a cross-engine fallback, so this leaf's own static arm is deleted. Two gates owning one
source-level rule meant my leaf was checked twice while every other leaf was checked
once, and the narrower copy is always the one that drifts.

What stays is the part no source read can do: two providers across engine present,
engine blocked and both blocked, with the CDN blocked by request routing the way a proxy
or an offline kiosk does it, and the engine identified from the canvas class rather than
a debug handle. All six still pass after the deletion.

--poison now NAMES where the falsifiable arm went instead of exiting 0 as though it had
self-tested — and it names it correctly: the tree-wide gate's flag is --self-test, not
--poison, which I checked in its source rather than assuming the convention my own gates
use. Its package.json description no longer claims a static arm this file does not have.
2026-09-27 03:49:05 +00:00
Doxa Maps
23976fb0d8 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
pplr-data-maps
regions-dashboard
research-dashboard
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-27 03:39:52 +00:00
Doxa Maps
a1f3bb57bd "The sweep is still running" was true, and misleading
The charts page told every reader "grn (partly — the sweep is still running)". It is
measured by check:no-stale-sweep-claim, which SKIPS rather than fails, because a code
really is undecided — and the number it skips on is ONE code out of 1,184. A reader who
sees that sentence discounts the whole audio column on the strength of 0.08%. A caveat
that is technically true and reads as "do not trust this data" is worse than a stale
number, because nothing flags it.

It states the proportion now: "grn (partly — 1 of 1,184 still to check)". The two
numbers come from the pin index, which publishes them counted exactly as the gate counts
— the codes the caveat SPEAKS FOR, tier 0 with sign languages excluded because GRN
publishes audio for them, against every code the sweep has decided. Reusing the builder's
own grnSeen set rather than rebuilding it, because a second copy could drift from the one
the tiers were computed against.

And it expires itself: at 0 pending the pool stops being described as partial at all,
which is exactly what the gate then demands instead of someone having to remember.
2026-09-27 03:35:31 +00:00
Doxa Maps
de24e633f9 The region card becomes a shared component the plugin consumes
The Driver's ask, and his negotiating point with the DX-life author depends on it
being true: the components cloned from their /regions page must be REUSABLE,
PARAMETERIZED components in the shared library, not private to one plugin. He named
the failure mode himself — someone opens the regions plugin folder and finds the
components are not reusable after all. I checked before building, and for the UI he
named he was right: only the DATA and LOGIC had been promoted.

So library/components/regions/RegionSectionCard.vue takes title, summary, countries
and footerLabel, emits what was clicked, and knows nothing about where regions come
from. The plugin version fetched its own data and owned the layout, which is exactly
what made it unusable anywhere else.

THE DESIGN TRAVELS WITH IT. The card carries the region-card, countries-grid and
country-link CSS lifted unchanged — including the RTL chevron rules, where a
"forward" chevron must mirror in Arabic while a disclosure caret must not, and where
[dir="rtl"] .region-card__icon at 0-2-0 would otherwise beat the 0-1-0 --open
modifier. A consumer gets the look, not a lookalike. It needs dir on an ancestor for
that mirroring, which is documented in the file as a requirement.

★ THE CARD OWNS THE DISCLOSURE ROW, after the first cut got it wrong. I put the
"View country posters" toggle in the consumer's slot — and immediately had to copy
the footer-link styling and the rotating caret back into the plugin, re-creating the
duplication this extraction exists to remove. A disclosure inside a card is a card
feature; only the revealed CONTENT is the consumer's, so PosterWall stays in the
slot with its fourteen props and does not become every consumer's API.

★ THE CHEVRON IS INLINE, NOT A <use href="#id">. The plugin declared the symbol once
and every card referenced it. A shadow-root id is scoped to that root, so the same
card mounted elsewhere would reference a symbol that does not exist — and an
unresolved <use> renders NOTHING, silently. The plugin's own comment warns about
that class for external hrefs; a cross-root id fails the same way.

check:regions-uses-library is registered and asserts BOTH halves: the plugin imports
from @map/ and renders the component, AND no longer holds the markup or the CSS it
delegated. A reusability claim only a human can audit is one that rots; this one
fails the day someone re-inlines a copy. Two negative controls keep it honest — the
plugin still uses its own .rl-root classes, and the other tab is asserted NOT to use
the card yet, so the check describes what is extracted rather than what I hope for.

Two of my own mechanical edits produced valid JS and broken content on the way here,
both caught by checking rather than by the parser: a line-numbered splice that
dropped PosterWall's closing /> (fixed by locating boundaries by CONTENT), and a
line-filtered CSS deletion that left 9 orphaned declarations and braces at 12/21
(fixed by removing rules brace-aware, then re-validating the concatenated stylesheet
rather than trusting check:sfc-parse, which only proves the JS parses).
2026-09-27 03:26:36 +00:00
Doxa Maps
7a420efedb i18n: gospelInternet gains the language tree and resources strings
Thirty keys in all 11 locales, before the code that calls them: the
Map / Language tree switch, the tree's levels, colours and legend, and the
Gospel resources tab.
2026-09-27 03:26:36 +00:00
Doxa Maps
66fbc540a7 gospel.tables.t_bridgereach: the one-language, how-many-peoples tab, in 11 locales
"One Language, How Many Peoples", requested by gospel-plugin for a new gospel tables tab (bundler-3/work
c72b25e5). Its tabLabel() carries the English until the key lands and switches on its own after.
2026-09-27 03:26:35 +00:00
Doxa Maps
9c78d9b1b8 PinHint hides while its map's view is inactive, not only while it is off-screen
On upg-100-mfe at phone width the map pane is stacked (position:absolute; inset:0) BEHIND Prayer Resources,
so the IntersectionObserver, which measures geometry and not occlusion, still reported the map as in view
and the pill floated over the posters (the Driver's report; flagged by repo before the README retake).

The contract: a composite marks an inactive view inert (or hidden / aria-hidden="true"), which is also what
assistive technology needs. The pill hides while any ancestor of its map carries one of those, walking up
through shadow roots, and a MutationObserver on that chain brings it back when the reader picks the map.
2026-09-27 03:26:35 +00:00
Doxa Maps
f716590740 UPG Neighbor cockpit: the view switch, sort buttons, tooltip and legend follow the reader's language
Still typed-in English after the lens pass: MAP / FLOWER, the sender counter, the
sort buttons, the Details tooltip rows (% Christian, Bridge, Adjacency, Code), the
legend ribbon, the flower ring legend and the Flowers cluster-by modes. 21 keys in
all eleven catalogues; the ring legend reuses the Flower Mesh wording already there.

Method names (SEND, 4-D SEND join, Flower-Mesh, Bounded sets), the provenance
footers and the scripture references stay English, per the scope ruling.
2026-09-27 03:26:35 +00:00
Doxa Maps
71826baa3d PPLR maps: the toolbars, legends, pills, popups and pin card follow the reader's language
The chrome around the maps was typed-in English: the search placeholder, Base map and
Data sources, the legend titles, each view's header pill, the popup row labels, the pin
card and the Rosetta legend. 42 keys in all eleven catalogues, reusing the product's
own words for Legend, Close, Choose a view, Religion and Language.

Registry names stay as they are (ROP3, ROP25, PEID, IMB PGID, JP Code, JoshuaProject,
WCD, PGIC GeoID), as do the Data sources provenance labels. The reference pages
(semantic trees, index, name locales) wait on the English-only ruling.
2026-09-27 03:26:35 +00:00
Doxa Maps
b6e04812ec gates:post runs check:no-silent-engine
It asserts the NEGATIVE case beside check:leaf-engine-per-provider's positive one: when a page's engine fails to
load, no other engine is substituted (a keyless page handed the token-gated engine gets 401s on every tile, not
a legible failure). It stayed out of the suite while offenders remained; all three are fixed and it passes
tree-wide. From upg100-plugin.
2026-09-27 03:26:35 +00:00
Doxa Maps
1801bbc20f Messaging channels: a taxi and a food app are no longer a country's top messaging app
The classifier matched "line" as a bare word, so "LINE MAN: Food Delivery & more"
(Thailand) and "Malé Taxi Line" (Maldives) each ranked as the country's top messaging
app, ahead of WhatsApp. LINE now matches only as the whole name or before a colon.

The classifier moves to app-kind.mjs so the fetcher and the table builder share it,
and the builder re-derives each kind from the app name, so a cached fetch made under
an older rule cannot carry its answer forward. The headline count is computed from
the rows instead of typed: WhatsApp (Messenger or Business) is top in 73 countries.
2026-09-27 03:25:59 +00:00
Doxa Maps
f728831d77 The interactive maps fetch the web-sized boundaries
Seven consumers switch to the -web copies exp-shepherd built (5410842a: every feature and island kept, about
a fifth of the bytes): useCountryOutline, useMapLayers, regions RegionCountryMap, research-dashboard
DashboardMapPanel, cities-internet connectivity-map (world + India states), gospel resources-map (world +
India provinces) and my-upg-100-list (world + UPG-100's India partition; China unchanged). generate-tree
stages the three -web files beside the full ones. Posters, print and every build-time point-in-polygon keep
the full files, as REPORT-WEB-BOUNDARIES.md requires: no runtime code tests membership on these.
2026-09-27 03:25:59 +00:00
Doxa Maps
9a955f8f20 Web-sized boundary copies: every feature and island kept, a fifth of the bytes
rega measured ~3.6 MB of boundary GeoJSON costing 5-6 s of MapLibre worker
parsing before the first territory draws, on every partner page. These copies
are for interactive maps; the full files stay for posters, print and
build-time point-in-polygon.

  world-adm0-web.geojson          1,866 KB -> 383 KB (gzip 596 -> 134)  162 features, 572/572 parts
  gb-IND-ADM1-full-web.geojson    1,796 KB -> 227 KB (gzip 451 -> 69)    36 features, 452/452 parts
  gb-IND-ADM1-web.geojson         1,628 KB -> 178 KB (gzip 412 -> 55)    34 features, 40/40 parts, listId kept
  (gb-CHN-ADM1 is 56 KB already: no copy)

library/scripts/boundaries/build-web-boundaries.mjs writes them with mapshaper
(pinned 0.6.121, via npx; not a repo dependency): -explode, then topology-
preserving Visvalingam with keep-shapes, then -dissolve with every other
property copied. The explode makes each island its own shape, so keep-shapes
protects every one: without it, the world drops from 572 to 240 parts. The run
FAILS on a changed feature count, a lost key, a changed property or a dropped
part. Negative-tested (without -explode: 297/572 parts -> FAIL), and a second run
writes identical bytes.

Measured (_qa-cycle/03-deployment-cycle/REPORT-WEB-BOUNDARIES.md): median area
error 0.57% (world), 0.04% (India). Of 14,486 real people-group points, 95.9%
stay in the same country in the world file and 99.9% in the India file; the
misses are archipelago coasts. No runtime code does point-in-polygon on these
files, so this is visual only.

Not wired: consumers still fetch the full files, and generate-tree does not
stage the -web copies yet. Both are in the report's switch table.
2026-09-27 03:25:59 +00:00
Doxa Maps
112c1103bd The table heading was English in every locale
Measured at ?lang=ar: the tabs render in Arabic and then the heading above the table
reads "Gospel Resources — every link, and who it reaches" in English, over English
column headers. The headers have had a lookup since cell E and it holds 13 of the 128
titles the 14 tables use; the TITLE had no lookup at all, so it came straight out of
the data file in every locale.

It is keyed by the tab's ID, not by its English text like the columns are: a title is a
sentence, and keying on it would break the day a builder rewords its own meta. The
English stays the fallback, so this renders exactly what it renders now until each key
lands.

The first version of this fix was wrong in a way worth recording: there are TWO paths —
the unfiltered table returns through localise(), the filtered one builds its own object
— and I patched the filtered path while measuring the bug on an unfiltered table. That
would have shipped a fix for the state a reader is rarely in. One resolver now, called
from both.

The exact key list is in _qa-cycle/03-deployment-cycle/KEY-REQUEST-gospel-tables.md for
studio: 14 titles and 113 headers, ordered by how many tables use each so a partial
batch still covers the most surfaces. Standards are excluded — ISO 639-3, ISO3, JSON,
CSV, ROP1 and SUN read the same in every locale.
2026-09-27 03:24:15 +00:00
Doxa Maps
34b375ee16 Rebuild the three bundles my two joiner strings made stale
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
scan REFUSED the push: country-posters, key-cities and regions-dashboard were all STALE — committed bytes
not what their source builds today.

★ AND THEY WERE STALE BECAUSE OF ME, which is the only reason this commit is mine to make. My first read
was "none of these three are my plugins", which is true and irrelevant. The chain:
    my 22 joiner keys -> library/scripts/posters/packet-strings.json
                      -> library/components/poster/PosterWall.vue imports it
                      -> all three of those bundles import PosterWall (3, 1 and 1 references)
None of the three names packet-strings itself, so grepping them for it says "no" — the dependency is one
hop further out, through a shared component. A bundle's inputs are not the files it mentions; they are the
transitive closure of what it imports, which is exactly what the EVERY-bundle freshness scope exists to
catch and what "shared code changed" in scan's own output was telling me.

Same lesson as the import.meta.glob case earlier this cycle: touching one file under library/ can put
every bundle that reaches it out of date, and the only reliable way to find out is to let the gate that
rebuilds all of them say so.

fix:sri re-pinned country-posters' and regions-dashboard's leaves; key-cities needed none, which I checked
rather than assumed — a changed bundle whose leaf was NOT re-pinned is the precise shape of the mismatch
that shipped blank pages three times. Verified by recomputing sha384 across the WHOLE tree, not just my
own: 35 integrity attributes, 0 mismatches.
2026-09-27 03:19:25 +00:00
Doxa Maps
38dab6bb21 Rebuild my-upg-100-list from the merged source, and re-pin its three leaves
832.06 kB. The six public/ files the merge left on main's side are regenerated here rather than merged,
because a bundle is an output and the only correct content is what the build emits from the merged inputs.

fix:sri rewrote the leaf hashes; that summary is NOT the evidence. All three leaf integrity attributes were
re-derived here by recomputing sha384 over the emitted bundle and comparing to what each HTML claims —
3 recomputed, 0 mismatches. This is the step that was skipped when three consecutive commits shipped leaves
pinning a hash for a bundle that no longer existed: the browser refused the script and every one of those
pages was blank while the build reported success.
2026-09-27 03:10:44 +00:00
Doxa Maps
73782bbbb5 Merge main into packet/vertical
Six public/ conflicts, ZERO in source — checked before the lock rather than discovered inside it, and it
held: the only overlap between main and this branch is my own bundle, its three leaf HTMLs and the two
manifests, because main rebuilt them and so did I.

They take main's side HERE and are regenerated from the merged source in the next commit. A bundle is an
OUTPUT: the only correct content is whatever the build emits from the merged inputs, and any
hand-reconciled middle version is wrong by construction even when the diff looks plausible.

studio's landing 9 (19682d2c, carrying my ee0bf338 by identical blob 9bdc45a9) has not landed, so mine
goes in first and theirs becomes the no-op. Both orders were agreed safe with studio and dx-shepherd
because the content is byte-identical either way.
2026-09-27 03:10:05 +00:00
Doxa Maps
45b07836cb The legend tree accepts a generated title, and keeps its label when there is none
The seam between zone-title.js (84062744) and everything that renders a zone name. buildLegendNodes
already took an opts object, so this is one option — `opts.title(zone)` — consumed at the three places a
leaf takes its name from data: the 13 regional rows, the 12 asia rows, the 2 uncovered rows.

★ NULL MEANS "KEEP YOURS", NOT "NO TITLE", and that is the whole reason this is safe to land while the
Driver's decision on the other 18 zones is still open. 18 of 28 zones decline — their labels are
sub-national ("Malabar Coast") or taxonomic (IMB's wagf blocks), and Intl has no subdivisions — so
falling back to the hand-written label is the NORMAL path here, not an error path. A generated title
replaces a label only where one could actually be composed.

⚠ THIS FILE HAS THREE CONSUMERS, not one: the legend, library/scripts/posters/generate.mjs and
publish-html.mjs both enumerate zones through leafZones(buildLegendNodes(...)), and packet.mjs joined
them in 3ef164c0. So a behaviour change with no option passed would have moved the poster and packet
output too. PROVEN UNCHANGED rather than assumed:
  - no opts.title                      -> 28 leaves, labels byte-identical to before
  - opts.title returning null for all  -> byte-identical again (the decline path is not a special case)
  - opts.title generating for one zone -> exactly that one label changes, nothing else
  - the packet generator still builds  -> Tibet, 43 groups / 7 pages
  - publish-html's enumeration         -> still 28 zone ids

Not yet called with a real resolver anywhere; wiring the legend, the poster header and the packet page-1
title is the next step, and it waits on the caller having records at the reader's locale to build the
country-label index from.
2026-09-27 02:54:25 +00:00
Doxa Maps
84062744ee Zone titles for the 10 zones that can have one, and a null for the 18 that cannot
Driver, 2026-09-27: zone titles must be GENERATED from already-translated parts, because UPG-100 is
becoming a pattern and a hand-translated title is a cost paid again for every project list (7000 / 4700
/ 3000 / 2000 / 700) forever. The Arabic map reads English today because every label is a hand-written
English string.

★ MEASURED BEFORE DESIGNING, and the measurement killed the more attractive half of the idea:
  country_code_label from the records API   all 175 codes, every locale   (formal names)
  Intl.DisplayNames({type:'region'})        all 175, every locale         THROWS on ISO-3
  Intl.DisplayNames({type:'language'})      213 of 2242 = 9.5%            ECHOES THE CODE
LANGUAGE IS OUT. `.of('kfa')` returns the STRING "kfa" rather than throwing, so a miss prints a raw code
onto a printed poster with nothing raising a hand — and the 90.5% miss is structural, not a gap to fill:
unreached peoples speak small languages by definition and CLDR carries the big ones. This module never
names a language.

TIERS, stated in the module because a generator that silently produces a bad title is worse than one
that declines:
  TIER A (3)  country_code   -> one country name.                    GENERATED
  TIER B (7)  country_codes  -> n names + a joiner.                  GENERATED
  TIER C (18) everything else                                         NULL, caller keeps its label
Tier C cannot be done from countries: the 12 India/China zones are pg_slugs whose every record is ONE
country (measured: asia-india-kerala 78/78 IND), so countries would render "India" twelve times and
could not tell "Malabar Coast" from "Northwest India" — those labels are SUB-NATIONAL, and Intl has no
subdivisions. The curated-table-vs-keep-English choice is the Driver's (repo's digest item D). Returning
null until then means nothing regresses.

⚠ AND I HAD THE SOURCE PRIORITY BACKWARDS, corrected by looking at what the two sources actually say.
I preferred the records' label because it is already localized and needs no code mapping — both true,
and it generated "Lao People's Democratic Republic" for the zone whose hand-written label was "Laos": a
generated title WORSE than the one it replaced. 28 of our 175 labels are formal forms like that
("Islamic Republic of Iran", "People's Republic of China"). Intl gives the short common form in every
locale, so Intl is now first and the records are the fallback. The ISO-3→ISO-2 mapper is INJECTED, not
imported, because regions-plugin owns that table (their card) and it has not landed: without it a
3-letter code falls through to the formal name, with it the titles read "Iran + Yemen + 11 more".

THE ONLY NEW TRANSLATED STRINGS ARE TWO: zone_join "{a} + {b}" and zone_join_more "{a} + {n} more", one
pair per locale block in packet-strings.json (22 keys across 11 blocks). That is the entire translation
cost for every project list that ever uses this — a new language stays a NEW BLOCK, never a code change.

VERIFIED against the real 28 zones in en AND ar, with live Arabic records from the API:
  Laos -> لاوس | Middle East Core -> إيران + اليمن + 11 أخرى | Maghreb + Egypt -> الجزائر + ليبيا + 5 أخرى
9 generated, 18 declined, in both locales. Every decline path returns null rather than a code or a
partial title, including a country_codes zone where one of the names it would SHOW is missing — a title
naming two of three countries and dropping the third silently misinforms and cannot be told apart from a
correct one downstream.

NOT WIRED IN YET: this is the generator only. The caller change (legend + poster + packet header) and
the deaf zone — global by construction, which breaks the geography-first rule and which the Driver says
is unsolved — are the next steps on the pattern card.
2026-09-27 02:49:56 +00:00
Doxa Maps
cc1830d79d No "tap a coloured dot" on UPG-100 at all — its map is a poster preview
Driver, 2026-09-27, from the phone screenshots: the pill should not be on this profile, full stop.

★ THIS IS A NARROWER AND MORE CORRECT CLAIM THAN THE ONE I FIXED YESTERDAY. PILLRISK was "the pill shows
over the posters", so I fixed WHERE it shows (inert on the inactive pane, 4642e78d). The Driver's answer
is that it should not show HERE at all: UPG-100's map is a POSTER PREVIEW — the reader is looking at what
will be printed, and there is no per-dot detail to open — so the pill invites a gesture that does
nothing, on a page the screenshots show has no coloured dots on it.

ProfileLoader mounts PinHint by default and self-gates it on a map <canvas> existing. That gate asks a
sound question ("is there a map here?") and it is the WRONG question for this profile: the canvas is
present and the affordance is absent. A self-gate can only test what it can see from inside the library,
and "this map is a preview, not an interactive surface" is not visible from there — it is the profile's
own fact, which is why the opt-out is the right layer.

Uses the contract that already existed — ProfileLoader.vue:150 reads `config.pinHint !== false` — so this
is one config value from my side, not a library change.

The inert signal STAYS, and is no longer what keeps this pill off this app. It is still right for its own
reasons: a stacked pane should not be in tab order or the accessibility tree, and studio's PinHint change
(2e375530) serves the profiles that DO want the pill. Saying so rather than silently leaving two fixes
for one symptom, because the next reader will otherwise assume one of them is redundant and delete the
wrong one.

check:sfc-parse PASS.
2026-09-27 02:40:26 +00:00
Doxa Maps
a1c8f3f6a0 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
pplr-data-maps
prayer-walk-planner
regions-dashboard
research-dashboard
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-27 02:40:15 +00:00
Doxa Maps
60e5fdcf42 The map learned to name peoples, not just their languages
?codes= filters by LANGUAGE, which is what a partner's list usually is and the wrong
filter for a list of peoples. The reach table measured the gap in its own rows: its
Mandarin row counts 36 groups, those 36 speak 30 codes, and 55 Doxa groups speak those
30 — so a link from a 36-group row drew 55 pins. Akha's 33 drew 57; SUN's 93 drew 116.
The extra groups are real and also have nothing; they belong to a different row.

So the map takes ?groups=<slug list> and filters by people group. The slug is
doxa.life's own — carried from the roster into the pin rows, and read in the builder
off the prayer URL their API built from it, never constructed from a name, because a
slug this build invented could name somebody else's people. All 369 of those rows
carry a prayer URL, measured before relying on it.

Row and map now agree by construction: 0 of 35 rows disagree, and the Mandarin link
renders "36 / 2,107" on the map against a row claiming 36. The gate asserts exact
equality for a three-slug link and 0 for an unknown slug — the failure that would
quietly turn a partner's 40-group embed into the whole roster.

The longest link is 1,577 characters, SUN's 93 slugs; well inside what browsers carry,
but worth knowing before anyone adds a row with a thousand groups.
2026-09-27 02:40:12 +00:00
Doxa Maps
5ec50a8124 Every Studio gallery tab after the first threw from createI18n: create it in a detached scope
The Driver's console, P0: in Plugin Studio's gallery, switching tabs threw "SyntaxError: Unexpected error"
from createI18n in createAppI18n. vue-i18n's createGlobal() runs inside scope.run() and throws
UNEXPECTED_ERROR when that returns undefined; Vue 3.5 creates a new EffectScope INACTIVE when the current
one is stopped; and a custom element's connectedCallback -> configureApp -> createAppI18n ran while the
released tab's stopped scope was still current. Root cause traced by dx-shepherd.

The i18n instance and its locale watcher are now created in effectScope(true), which never inherits the
ambient scope. The install wrapper also makes a second provide('appI18n') of the same instance a no-op, so
the bundle entries that still provide it themselves stop warning "App already provides property".

check:i18n-stopped-scope (in gates:post): a bare createI18n() inside a stopped scope must throw (control),
createAppI18n() there must succeed, translate and keep its locale watcher live, and a second provide must
not warn. On the previous index.js it fails with the Driver's exact error.
2026-09-27 02:32:07 +00:00
Doxa Maps
3f6e29ebc9 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
my-upg-100-list
research-dashboard. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-27 02:26:22 +00:00
Doxa Maps
67327e312a i18n: gospelInternet gains the Platforms view's twelve strings
In all 11 locales, before the code that calls them. Keys sorted.
2026-09-27 02:17:36 +00:00
Doxa Maps
b20d16d919 Library index: regenerated once for everything studio landing 8 adds to library/ 2026-09-27 02:17:36 +00:00
Doxa Maps
a5586e3ae2 check:universal-props reads the gospel-internet bundle
The bundle list is hard-coded, so a new bundle passed as '0 bundles'
until it is named here.
2026-09-27 02:17:35 +00:00
Doxa Maps
972927b2d0 i18n: the gospelInternet area, in all 11 locales
The Gospel x Internet board's strings, landed before the code that calls
them. English wording kept; numerals and percent signs as the other
catalogues write them. Coded labels (access ladder, internet band, way in,
resource kinds) are keyed by their stable code.
2026-09-27 02:17:35 +00:00
Doxa Maps
bad6991b03 D3: the bus can point at a record without choosing it
SPEC-DASHBOARD-KIT D3, and the last of the three decided-not-built items. The bus
carried two coordinated channels — filterSet NARROWS scope, selectionSet MARKS
individuals — and was missing the third: hoverKey POINTS at the one record under
the pointer. It is how a reader sweeps a table and watches the map answer without
choosing anything, and kepler.gl treats hover as a configured interaction beside
filters for the same reason.

IT IS A PLAIN ref AND NOT A SET, AND THAT IS THE WHOLE DESIGN. Hover fires on
every mousemove across a 2,107-row table. selectionSet had to become a plain Set
with a version counter precisely because a reactive collection fires per element;
a single ref has none of that problem — one dependency, one write, no iteration.
A repeat is a no-op, so an unchanged value never wakes a panel.

HOVER IS NOT SELECTION, AND MUST NEVER BECOME IT. The cheap implementation is to
put the key in selectionSet "just while hovering", and every consequence of that
is bad while none is obvious at the call site: useSelectionCamera watches
selectionVersion, so a hover would fly the camera at every mousemove — the
resize-strobe bug wearing a new hat, on a Driver who is photosensitive. The CSV
exporter reads selection > filter > all, so a hover would silently change what an
export contains. The table's selection pill and the shell's live region both
report selection counts, and both would narrate the pointer. So a panel may fade
or outline on hover; it may not fly, select or filter.

NO PANEL IS WIRED, deliberately, and I want that on the record rather than left
to be noticed. D3 says panels opt in, and the first opt-in belongs with a
consumer that has a reason: wiring one into Big Picture speculatively would be a
visual change to the flagship that no reader asked for, landed while the kit
stack is paused. That does leave a channel nothing uses — the same criticism I
made of the unmounted filter chips — so the difference has to be earned rather
than asserted: this is a documented, gated contract with its first consumer
named, not a component that was quietly orphaned. check:hover-channel is what
makes it a contract instead of dead code; the first panel to use it inherits the
rules instead of inventing them.

Three mutants, all caught: routing setHover through selectionSet, dropping the
equality guard, and removing hoverKey from the returned surface.

SOURCE ONLY. Stacked on landing 2. No rebuild needed — nothing renders it yet.
2026-09-27 02:17:35 +00:00
Doxa Maps
9b3eb99550 The number's job was wrong, not its value
rega, row 43 case 3 at 835ec1ac: the all-empty guard catches the cascade on
MapLibre and NOT reliably on Mapbox — in ar/mapbox the single event is still
EMPTY and the grid stays at 2,107. Three of four.

THIS IS THE THIRD TIME I HAVE TUNED A NUMBER FOR THIS ONE DEFECT, so the number
does not get another value. 60ms was "long enough for the cascade" and was not.
3 x 60ms was "long enough for the cascade" and is not either. I cannot measure
the cascade from inside the element, and every value I pick is therefore a
measurement of whichever machine I picked it on — which is the lesson already in
COMMON.md, arriving a third time because I applied it to the duration and not to
the DESIGN.

So the number's JOB changes. It was a guess at how long the cascade takes, which
is unknowable from here. It is now a CEILING on how long an empty report waits
before it is believed, which is a choice — and the two fail in opposite
directions. Too short reintroduced the bug three times; too long merely delays a
genuine clear by an amount no reader perceives. The loop now re-reads until the
state is NON-EMPTY, which is the condition that actually matters, and gives up
only at the ceiling.

900ms, chosen to sit under the composite's 1.2s push window so the two cannot
interact, and to be generous rather than tight — the failure mode of generous is
invisible.

Driven through the rule outside the browser: a cascade that fills at 400ms reports
FILLED at 420, one that fills at 800ms reports FILLED at 840, and a genuine clear
reports empty at 960. The old budget gave up at 180.

check:language-code-link's section G already asserted BOUNDEDNESS, and it failed
when I changed the shape — correctly, because that is the invariant it guards. It
now asserts the ceiling instead of the count, and says in the file why the shape
changed while the invariant did not: without SOME bound a genuine clear is
deferred for ever and the grid can never be un-narrowed. Both arms
negative-tested.

Source only. STUDIO: rebuild doxa-research-map.
NEEDS EYES: rega's ar/mapbox run is the one that matters — the other three already
pass, so a green ar/mapbox is the whole result.
2026-09-27 02:17:35 +00:00
Doxa Maps
273785b2e6 The layout drag was LTR-only, and my own migration was too eager
go-int found both by reading the kit before importing it. Both are in landed code
and both reach Big Picture in Arabic, not only a new consumer.

THE DRAG HAD NO READING DIRECTION. facetW and railW were computed from
(clientX - startX), written once, in LTR. Under dir=rtl the grid mirrors — the
facet rail sits on the RIGHT — so dragging its handle toward the map moves clientX
the other way, and in Arabic the facet handle shrank the CHARTS rail. Both handles
fight and neither does what the cursor says. The delta now carries a sign read from
the computed direction per drag, not cached: a host can flip dir between gestures,
because our own composites set it on the custom-element host from the runtime
locale.
The ARROW KEYS take the same sign. If only the pointer mirrored, the two input
methods would disagree about which way widens — and aria-valuenow reports the same
ref for both, so the announced value would move opposite to the key for someone
who cannot see the drag.

AND THE LEGACY READ-THROUGH WAS MINE, FROM KIT LANDING 2. I added it so
namespacing would not discard a reader's saved layout, and applied it
unconditionally. So a caller passing its OWN storageKey — which is the entire point
of namespacing — still inherited Big Picture's rail widths on first load. The
migration written to protect one dashboard's layout was handing that layout to
every other dashboard. It is gated on the default key now, and the default is a
named constant rather than a repeated literal, because a fallback comparing against
a copy of its own default stops matching the day someone bumps the version suffix.

check:layout-key-namespaced grows arms D and E. D asserts BOTH widths take the sign
and that the arrows share it — mirroring one and not the other is worse than
mirroring neither, because then the handles disagree with each other. E asserts the
legacy read is gated. Comments are stripped before both assertions, for the reason
that is now written down four times over.

Source only. STUDIO: needs a rebuild of research-dashboard and dashboard-plus-grid.
NEEDS EYES: the drag is a gesture, so this wants rega in ar — drag the facet handle
toward the map and confirm the FACET rail follows the cursor and the charts rail
does not move, then the same with ArrowLeft/ArrowRight.
2026-09-27 02:17:35 +00:00
Doxa Maps
c2ab779623 Kit landing 4a: the shared search, because the facet rail cannot move yet
★ LANDING 4 AS I SCOPED IT IS NOT POSSIBLE, AND THE SPEC WAS WRONG ABOUT THAT.
go-int asked for the facet rail next and I agreed. Then the first step of a
landing — look before moving — found that DashboardFacetRail imports FOUR
plugin-local modules: useDashboardAggregations, useDashboardShell,
useTreeLegendColors and useTreeLegendLabels. Moving the rail into library/ would
have made a library component import plugin composables, which
check:no-cross-plugin-imports rightly refuses. And two of those four are
research-specific by my own spec's reckoning — useTreeLegendColors carries the
DOXA colour strategies and is supposed to STAY in the plugin — so the rail cannot
become library code by moving alone. Its colour and label dependencies have to be
inverted into injection first, which section 8 named as a risk and then ordered as
if it were free.

So landing 4 is three commits, not one, and this is the first: the piece the rail
stands on.

THE SEARCH WAS NEVER A SHELL CONCERN. buildSearchAccessors, makeSearchMatcher and
filterRowsBySearch were exported from useDashboardShell, and EIGHT files reached
into the shell to get them — the table, the map panel, the charts rail, the facet
rail, the research filters, the top bar, the profile and the config. A module that
eight things import is a module, not a corner of another one. Nothing in it is
domain-specific: the accessors come from config.facets / config.columns /
config.searchKeys, and the matching comes from the already-shared fuzzy.js.

Bodies are verbatim; this is a move. The shell re-exports the three names for one
cycle, so none of the eight importers changes in this commit — a barrel line
rather than a shim file, so there is nothing to delete later, only a line to
remove when the eight point at the library directly.

The now-dead fuzzy.js import is removed from the shell rather than left. An import
nothing uses is the kind of thing that survives three more landings because it
looks load-bearing.

WHAT THIS DELIBERATELY DOES NOT TOUCH: the shell keeps rowMatchesFilters and the
URL-hash escape helpers, which are what check:filter-multivalue reads out of it by
path. I checked that before slicing, so no gate path moves in this commit.

Library index regenerated in-commit. Source only; no rebuild needed beyond the
usual, since every importer still resolves through the shell.
2026-09-27 02:17:35 +00:00
Doxa Maps
db2b24c753 A bare-host embed under ?lang=ar now renders Arabic here too
dx-shepherd's ruling: the locale-honouring fallback everywhere, carrying the
Driver's goal (d) — a bare-host embed with ?lang=ar must render Arabic — now
recorded in COMMON.md as the embed contract.

all-people-groups passed its inline EN map as the fallback, which served ENGLISH to
exactly the hosts that cannot install vue-i18n: the seven bundles here that install
none. It now takes the catalogue default, so those readers get their own language
with English still the last resort. The EN map is deleted — with the catalogue
complete it could rescue nothing, and a second copy of the English is only a place
for the two to drift.

THE RULING'S CONDITION WAS MEASURED BEFORE THE SWITCH, not after: all eleven keys
exist in all eleven locales, and ar differs from en on 11 of 11, so the change is
observable rather than cosmetic.

★ AND THE CONDITION IS NOW KEPT TRUE RATHER THAN MEASURED ONCE. A hand census
protects the day it is taken. Section 9 of check:optional-t-equivalence re-derives
it every run, because with no local fallback an uncatalogued key renders as a raw
dotted path to a reader — the exact failure upg100 described, and the reason they
had advised keeping a fallback at all.

★ THAT CENSUS UNDER-COUNTED TWICE BEFORE IT WAS RIGHT, both caught by checking
instead of assuming:
 - literal tr('key') extraction found 6 of 11. Four keys are category ids reached
   only through tr(cat.id) / tr(c.id); the census now extracts the CATS ids too, and
   asserts the dynamic call sites still look the way it assumes.
 - the eleventh, 'pray', was lost to codeOnly: it sits in a template literal whose
   `https://` was read as a line comment, because the scanner had already lost
   template state further up a 1000-line SFC. The census now reads RAW source.
   The asymmetry is the point — over-including a commented-out key makes the check
   STRICTER (a cheap false red); under-including a real one makes it claim
   completeness it does not have. strip-code.mjs carries that rule now, so the next
   author picks raw or stripped for a reason rather than by habit.
2026-09-27 02:17:35 +00:00
Doxa Maps
a7c9e9557f The prayer map's legends see the pins it draws
rega, row 46 at 835ec1ac: on the prayer map neither the shared-point sentence nor the standing pin note
appeared. research-map and countries-map provide normalizedPeopleGroups to their legends; the prayer map
did not, so its legends fell back to dataStore's cache, which holds the RAW API rows: field names the
co-location index cannot read, and no GeoJSON geometry for the standing-note detector to find.

useMapData now provides the normalized rows it draws, so every map built on it offers them. LegendMobile
and SemanticTreeLegend read them first (the cache stays the fallback), and the detector accepts a row with a
name and a position as well as a GeoJSON point. check:approx-pin-note asserts the provide and the
normalized-row match; the provide check fails on the previous useMapData.
2026-09-27 02:17:35 +00:00
Doxa Maps
3883d55c7f The reach table can put those peoples on a map, and says what it shows
A row reading "36 peoples" that cannot show you which 36 is a statistic, so each row
now carries the map's own ?codes= address — the partner-embed filter doing a second
job.

It would have contradicted its own row. The map filters by LANGUAGE and these rows
count GROUPS: Mandarin's 36 groups speak 30 codes, and 55 Doxa groups speak those 30.
Akha claims 33 and the link draws 57; SUN 93 and 116. The extra groups also have
nothing — they are offered a different bridge because they are in a different country.
So both numbers ship beside the link, the column is named for languages rather than
peoples, and the meta says why. Labelling a language filter as a people filter would
have been the easy version of this and would have made the map call the table a liar.

An exact per-group filter is a change to the map's URL contract and belongs there, not
faked in a builder.
2026-09-27 02:12:39 +00:00
Doxa Maps
f3c19e2f21 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: cities-internet
country-posters
dashboard-plus-grid
doxa-research-map
doxa-simple-map
my-upg-100-list
research-dashboard. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-27 02:09:50 +00:00
Doxa Maps
c72b25e516 One language, how many peoples
Every surface here answered "who has nothing", then group by group "what is the way
in for THIS one". A mobiliser with a budget asks the inverse and had nowhere to ask
it: if I resource ONE language, how many peoples does that reach? Over the 369 groups
with nothing of their own the answer is 35 rows — SUN is the way in for 93 Deaf groups
across 93 countries, Mandarin for 36, Akha for 33, Indonesian for 28. That is a
different shape of decision from a list of 369, and it is the one a sending church or
a translation agency actually makes.

The three kinds are never summed. A CURATED bridge was checked for those groups; a
CANDIDATE is computed from published CLDR data about what the country speaks, which is
a fact about the country and not about these people; SIGNED is SUN, which has to be
learned. A language offered as both a checked bridge and a computed candidate appears
once per kind, because those are different claims — which is why there are 35 rows for
32 languages.

Every row carries "still nothing of their own" and the word bridge, because
aggregating makes that easier to forget, not harder. No new link enters the map: a
bridge language's Scripture, film and training are the ones the pin index already
holds for that ISO code, and all 80 URL cells resolve to links the verification record
has already proved — measured, not assumed.

The tab carries its own English until its key lands. tr() falls back to the key, which
on a tab would read "gospel.tables.t_bridgereach" — worse than the English it replaces.
2026-09-27 02:09:44 +00:00
Doxa Maps
8bea6d3601 Rebuild my-upg-100-list with the inert pane signal
Leaf integrity re-derived by recomputing sha384 over the emitted bundle: 3 recomputed, 0 mismatches.
2026-09-27 02:07:44 +00:00
Doxa Maps
4642e78d66 The pill over the posters: tell the stacked pane it is not the one being read
My half of PILLRISK (studio's PinHint half is 2e375530). `inert` on every pane except the active one,
which is the signal studio's contract asked for (END-STU-PILL / UPGINERT).

★ PinHint'S OBSERVER WAS NOT BROKEN, AND THAT IS THE WHOLE DIAGNOSIS. The panes are
`position:absolute; inset:0` in one scroll-snap column, so the map pane sits STACKED BEHIND the active
view at full size: 100% of its geometry is inside the viewport while none of it is visible. An
IntersectionObserver measures INTERSECTION, not OCCLUSION, and no tuning can separate those two states
— they are identical to it. The missing information was never geometric. Only the composite knows which
pane the reader chose, so only the composite can say it.

⚠ inert, NOT display:none or v-show, and this constraint decides the approach. Hiding the pane would
tear down a live map: a MapLibre/Mapbox canvas in a zero-size container loses its size and
re-initialises on the way back — the cold-start path rega had to verify separately in row 52. inert
leaves the pane mounted, painted and warm and only declares it non-interactive; unlike aria-hidden it
also removes it from tab order and the a11y tree, so a keyboard user cannot tab into a map they cannot
see.

ALL FOUR PATHS THAT CHANGE THE VIEW ARE COVERED, which took reading rather than guessing:
  - FRESH LOAD — marked in onMounted from column.current(). This is the case the Driver actually hit:
    without it no pane carries the attribute until the reader touches the switcher, so the pill is over
    the posters on ARRIVAL. A signal that only fires on transitions never describes the initial state.
  - THE SWITCHER — marked inside go(), immediately rather than via the scroll callback, because
    scrollTo is SMOOTH and onScroll debounces 120ms: waiting would leave the pill over the posters for
    the length of the animation, a shorter version of the same bug.
  - A DRAG/SCROLL — marked in onScroll, UNCONDITIONALLY rather than inside the existing
    `cur !== view.value` guard. go() has already set view.value by then, so a switcher-driven scroll
    takes that early exit and would skip the marking; a scroll-driven one needs it, and a
    switcher-driven one is idempotent.
  - "Show on map" from the poster wall — routes through go(), so it inherits it.

Plugin-only: upg-100-mfe.vue, no library change. mountEl(paneId) already exposes the container, so the
attribute goes on from my side of the fence.
Correct at EVERY viewport, not just the phone: pane-wrapper renders ONE column with
`scroll-snap-type: y mandatory` and `.pane { height:100% }`, so exactly one pane fills the stage at any
width and there is no v1 side-by-side mode whose second pane would be wrongly marked (pane-wrapper.js:56-59,
:101 — read, not inferred from the phone report).
check:sfc-parse PASS, check:no-free-identifiers PASS. The one-pane-non-inert invariant is proven across
switches including the return trip; the browser assertion is rega's (fresh load: no pill -> Map: pill ->
Prayer Resources: no pill).
2026-09-27 02:07:10 +00:00
Doxa Maps
05995927dd Rebuild my-upg-100-list without the two deleted RTL rules
830.97 kB, down from 831.13: the two legend-content rules are gone from the shadow-style literal.
Leaf integrity re-derived by hand — 3 recomputed, 0 mismatches — rather than trusting fix:sri.
2026-09-27 02:04:34 +00:00
Doxa Maps
5a4a0d77e0 Delete the two legend-content RTL rules the library owns — one was dead all along
rega's row 56b at main 4ca2dcf9 measured Simple Research and Prayer Progress under ?lang=ar with NO
override at all: their tree-legend rows compute direction rtl and their counts keep digit order. So the
library legend is RTL on its own and these two rules are mine to remove (dx-shepherd UPGDEL, studio
END-STU-UPG-56B). Rule 1, .upg-detail-panel, stays — that element is mine, not the library's.

★ BUT THE TWO WERE NOT EQUALLY PROVISIONAL, and the difference is the point of this commit:

  '[dir="rtl"] .stl-panel,[dir="rtl"] .stl-panel *{direction:rtl}'   was REDUNDANT.
It matched real elements — .stl-panel appears 69 times in SemanticTreeLegend — but the library reaches
the same state without it, using the :dir(rtl) PSEUDO-CLASS (:645-646), which reads the INHERITED
direction instead of needing a declaration. Two other profiles carrying nothing prove it.

  '...stl-count,...stl-pop{direction:ltr;unicode-bidi:isolate}'      was DEAD CSS, and always was.
The library's count and population cells are `.stl-num` (:1314-1315). `.stl-count` and `.stl-pop` exist
NOWHERE in library/ or any other plugin — git grep finds them only inside my own override. The rule
matched zero elements from the day I wrote it. The digit order rega verified was the library's doing and
never mine, and I had cited this rule as the reason it worked.

★ THE LESSON IS ABOUT EVIDENCE, NOT CSS. This rule was written against a real bidi problem, believed to
have fixed it, and quoted as the fix in a commit message and a card — while matching nothing. A style
rule fails SILENTLY when its selector is wrong: no warning, and the page looks correct if something else
already handles it. Checking that a selector EXISTS is a different act from looking at the rendered page,
and only the first one can tell those two states apart. Both are recorded in the source so nobody
re-adds the dead rule believing it once worked.

check:sfc-parse PASS.
2026-09-27 02:03:54 +00:00
Doxa Maps
ee0bf338d1 My own gate was accusing the one agent who fixed the defect
check:no-silent-engine reported gospel-resources/resources-map.vue as the last remaining offender at
main 9df086e5. It is not, and has not been since their landing: line 1513 reads
`const gl = engineGlobal(grProvider)` with no `||` at all. I relayed the red to dx-shepherd as fact
before looking at the line, and the line was PROSE.

★ WHAT IT WAS MATCHING. gospel documented what they removed, in a /* */ block whose second line begins
with a BACKTICK:
      `engineGlobal(grProvider) || window.mapboxgl`. engineGlobal already defaults to mapbox, so
The skip asked only whether a line STARTS with //, * or /*. That is not what a comment is: it misses
every CONTINUATION line of a block comment whose text begins with something else. So the gate now
tracks block-comment state instead, and the --self-test carries gospel's exact shape as a regression
case — plus its inverse, a real offender after a closed block, because fixing a false positive by
skipping MORE is only correct if the true positives still fail. Both assert in the same run.

★ AND THIS IS THE WORST FAILURE MODE A GATE CAN HAVE, which is why it gets a commit of its own rather
than a quiet one-line patch. It told the one agent who did the right thing that they had done the wrong
thing, and the cheapest way to make it green was to DELETE the explanation — so the gate actively
attacked the reasoning it exists to protect. This file's own header quotes the bad pattern for exactly
that reason; the skip was there from the start and was simply too shallow to cover the form someone
else actually used.

⚠ AND IT MADE A STALE CARD LOOK LIVE. The offender's line number moved 1490 -> 1505 -> 1509 across
three landings purely because their file grew, so each re-read produced a plausible new number for a
defect that had already been fixed. A card that names file:line ages into a false claim silently; the
gate has to be the source of truth, and it was lying too.

Tree-wide result now: 6 files call engineGlobal(), PASS — every one keeps the engine its provider asked
for. The gate is finally green on a green tree, which means it can be wired into gates:post. That wiring
is shared config (rule 5B) so it is NOT in this commit: prepared for studio separately.
2026-09-27 01:59:11 +00:00
Doxa Maps
22cae7fd28 package.json: the //scripts note for check:religion-label-not-value
It was registered in scripts and gates:post without its note.
2026-09-27 01:57:50 +00:00
Doxa Maps
72dc172479 Four pray-tools assumptions the exporter did not know it was making
go-int read useCsvExport before importing it and found four things that are only
visible from outside. Every one is a pray-tools assumption this file made
silently, and I could not have told you they were assumptions while there was one
caller. That is what a second consumer is FOR, and it is worth more than the code
it produced.

  columns — it exported every key of the row, ordered by EXPORT_FIELDS. go-int's
    spec asks for the VISIBLE columns. Their workaround was to project rows before
    handing them over, which loses the ordering this file exists to impose. An
    explicit `columns` is now taken AS GIVEN, order included, and deliberately NOT
    filtered to keys that happen to be present: a column the reader can see must
    appear even when every row in this scope has it empty, or the file's shape
    would depend on the filter, which is the opposite of what a column picker
    promises.
  wide — exportCsvAsync always fetched the pray-tools list endpoint. A
    static-table dashboard has nothing to widen FROM, so that is a wasted network
    call that would also pollute its rows with pray-tools fields. It is skippable,
    and it now also short-circuits when there is no source at all, because that
    case can be recognised rather than configured.
  filename and hookPrefix — 'research-dashboard.csv' and the __rdash* window
    hooks were literals. Both are options; both defaults are the existing strings,
    and the default matters more than the option here: __rdash* names are read
    elsewhere in this tree, so changing it would have broken checks with nothing to
    do with this landing.
  keyOf — a row identified itself as `slug || id`. go-int's rows carry a slug so
    this costs them nothing, but they were right to flag it against
    config.data.primaryKey: two spellings of one idea that nothing forced to agree.

EVERY DEFAULT IS TODAY'S BEHAVIOUR, so Big Picture's file is unchanged to the
byte. check:csv-shape and its self-test pass, and check:kit-options-reachable
passes across 21 options — which is the gate that exists because I shipped a dead
option in the commit below this one.

Source only, stacked on kit landing 3. No rebuild beyond the one that branch
already needs.
2026-09-27 01:57:50 +00:00
Doxa Maps
2525c61151 Religion families follow the reader; the value they filter on does not
REGREL. Two library sites rendered RELIGION_FAMILIES values as English in every
locale: the Religion legend's rows (useLegendData.js) and the search aggregate that
collapses sub-types into their family (useSearch.js). Both now project the LABEL
through religionFamilyLabel(), which resolves through the catalogue and keeps
English as the last resort.

The half worth a gate is the half that did NOT change. A translated value matches
nothing — the moment a localized string lands in a filter, a bus payload or an
element attribute, the link works in English and fails silently in Arabic. So `key`
and `filterKey` stay the canonical family letter, useSearch's key function stays
bare getReligionFamily(), and the check asserts all of that as well as the
projection. RELIGION_FAMILIES is no longer imported in either file: the label was
its only local use, and religionFamilyLabel reads the canonical map internally.

★ A COMMENT IN useSearch.js ARGUED AGAINST THIS FIX, and it was wrong. It read
"label = family name so selection can resolve the family letter back (see
research-map onGeocoderAggregateResult religion branch)" — presenting an English
label as a REQUIREMENT of selection. Measured: onGeocoderAggregateResult does not
exist anywhere in this repo (three comments name it, nothing defines it), and
selection resolves by value — aggregate() emits `key` and `label` as separate
fields, and the consumer reads detail.expectedValue (countries-map.vue:1611).
Retracted in place with the measurement, not deleted, because a reader who has seen
the old claim needs to know it was tested.

check:religion-label-not-value is registered and negative-tested. It executes
religionFamilyLabel's REAL source against the SHIPPED ar/ and en/ religion.json
rather than a stub, because religion.js imports tStatic through an i18n index that
calls import.meta.glob and cannot load in plain Node. Under ar the label is
المسيحية while RELIGION_FAMILIES.C is still "Christianity", and a control asserts
the two really differ — an English-only projection fails that assertion.

One assertion of mine failed on its own retraction first: it required the stale
phrase to be ABSENT, but the corrected comment quotes it in order to record it.
Requiring the absence of a phrase punishes documenting the error, so it now asserts
the retraction is present instead.
2026-09-27 01:57:23 +00:00