Commit graph

1867 commits

Author SHA1 Message Date
Doxa Maps
ead8afa817 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 02:24:51 +00:00
Doxa Maps
fbf1e115ca Gospel x Internet: the Platforms view
Which apps would carry a bridge-language resource into the countries in
scope: the top messaging app (Apple top-free, iOS only, a download rank)
and the top social platforms (StatCounter referrals, which cannot see
messaging) per country, never on one scale, each with its own source's
caveats. A country outside Apple's feed is 'not measured', never dropped.
The preset opens on the 220 groups with nothing in their own language in
countries 60% or more online.
2026-09-27 01:53:16 +00:00
Doxa Maps
8da570f5da i18n: gospelInternet gains the Platforms view's twelve strings
In all 11 locales, before the code that calls them. Keys sorted.
2026-09-27 01:53:15 +00:00
Doxa Maps
61e1d417f6 Gospel x Internet reads each group's province from gospel-plugin
gospel-by-province-groups.json replaces this builder's own point-in-polygon
(which agreed with it group for group: 0 province changes on rebuild). One
assertion stays: the aggregate's groups sum to the per-group rows, so a
stale copy of one file beside a fresh other refuses to build. The rebuild
also inherits gospel-plugin's fix for 3 retired film links.
2026-09-27 01:46:01 +00:00
Doxa Maps
83c25a3204 Merge main into go-int/work 2026-09-27 01:42:03 +00:00
Doxa Maps
48866948af Gospel x Internet board: coded labels, its own gates, the built bundle
Every label built from a code falls back to readable English. check:gospel-
internet runs the model rules against the real table and the kit bus, then
every catalogue key the bundle names in all 11 locales; each half has a
poison arm. Built, tree regenerated, SRI in agreement.
2026-09-27 01:40:31 +00:00
Doxa Maps
b888eb49d7 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 01:40:30 +00:00
Doxa Maps
da23daa263 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 01:40:30 +00:00
Doxa Maps
4ca2dcf912 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-27 01:35:26 +00:00
Doxa Maps
bbd7eaa1f0 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 01:29:39 +00:00
Doxa Maps
756eb67920 Merge main into go-int/work 2026-09-27 01:29:34 +00:00
Doxa Maps
802cccd66e Gospel x Internet board: a new plugin on the dashboard kit
One row per people group joining what the gospel sources hold in its own
language to how much of its country (or Indian state) is online. The join
builder refuses to write if any join drops a row or if its province
placement disagrees with gospel-by-province. Board, zero-access and
India/China views over one selection bus; facets, map on both engines,
charts, grid, drawer and CSV export.
2026-09-27 01:29:34 +00:00
Doxa Maps
c790b39840 Every shipped link has evidence, and 1,412 of them did not
Three findings, one thread: rule 4 says verify a URL before shipping it, and the
check I wrote for it yesterday was measuring one file.

1. THE TABLES PAGE WAS SHIPPING DEAD FORM. people-group-resources.json — the
"Resources by People Group" tab, which a reader clicks — carried 2,294 URL cells, of
which 1,412 were jesusfilm.org/watch/?languageId=<id>. That form is IGNORED by the
site: a real id and a bogus one render the same generic library page, which is why
the map migrated onto Arclight shareUrls. The migration never reached this table, so
the booleans beside those links were being refreshed while the links themselves went
stale for a year of builds. 1,417 film and 882 scripture URLs re-derived from the
index, which is authoritative for both and whose URLs are the verified ones.

2. TWO ARCLIGHT LANGUAGES, ONE ISO CODE — resolved, with evidence rather than a
ruling. jos was SKIPPED here, and the note said resolving it meant deciding whether
the code or the publisher's name was authoritative, "a data question for someone who
can check it". Checked: media-languages?iso3=jos returns 7138 "Jordanian Sign
Language" and 139080 "Lebanese Sign Language", and 7138's shareUrl redirects to
.../jordanian-sign-language.html while a bogus arc.gt code 404s. So the code stays
authoritative for the join and the NAME breaks the tie the code cannot. The old note
also had a fact wrong — it called Lebanese "a different language (lbs)", but lbs is
LIBYAN Sign Language and Lebanese has no code of its own, so this is two Levantine
varieties sharing one code, not a mis-filing. Three Deaf groups now reach the JESUS
Film in their own sign language through a link that resolves; 0 retired-form links
remain in the index.

3. THE GATE'S SCOPE WAS THE BUG. It walked the pin index alone, which is how item 1
hid from it. It now reads every table the SURFACES read, taken from the surfaces, and
resolves each URL against four records: the verification table, the org contacts, the
SUN pages (proved by content against a catch-all control) and doxa.life's own listing
for prayer pages, which cannot be proved by fetch because pray.doxa.life answers 200
for any slug. Provenance fields are excluded by path — a citation is not an offer.
8,445 links measured yesterday; 12,292 shipped links today, 0 unrecorded, 0 broken,
0 that no status code can prove.

Reading every STAGED file instead reported 6,115 unrecorded and 53 broken, all of
them correct behaviour: API endpoints in source fields, and build caches that still
hold the 53 share URLs the verifier proved dead — which is exactly why the pin
builder drops them. A gate that goes red on correct behaviour gets switched off.
2026-09-27 01:26:50 +00:00
Doxa Maps
3179c24561 Publish which province each group falls in
gospel-internet was running the same point-in-polygon over the same two geometry
files and refusing to write unless its per-province counts matched this table's —
which is good discipline and should not be necessary. The assignment ships now: one
row per placed group, 1,005 of them, with the language code, population and tier.

The two files are reconciled BEFORE either is written — per-province counts and the
assigned total must agree or the build refuses — because they are published
separately (the map fetches only the aggregate, lazily, and has no use for a
thousand rows) and separate files can drift. The failing direction of that guard is
not proven, only the passing one: 46 provinces agree, 1,005 = 1,005.
2026-09-27 01:26:31 +00:00
Doxa Maps
dd369848f6 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
big-picture
dashboard-plus-grid
my-upg-100-list
people-groups-grid
regions-dashboard
research-dashboard. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-27 01:24:59 +00:00
Doxa Maps
8999e7ff01 check:composite-embed-knobs hands each composite its own profile
rega's run at 1a761741 measured 8 cases and left 10 UNMEASURED: gospel-resources, my-upg-100-list,
regions-dashboard and two more drew no map under a knob-only profile-config, because a multi-profile
element must be told which profile is the composite. The target's nested-elements-mfe profile now rides
with the knob under test (the knob still wins on any clash). Browser gate: needs a DOXA_BROWSER=1 seat.
2026-09-27 01:16:52 +00:00
Doxa Maps
e8b87b8187 package.json: the //scripts note for check:text-highlight
It was registered in scripts and gates:post without its note.
2026-09-27 01:16:52 +00:00
Doxa Maps
15820f1432 Dashboard kit, landing 3: the camera and the CSV exporter
Both are zero-domain in the sense the spec meant: they import only `vue` and read
rows through accessors. Bodies are what research-dashboard shipped apart from the
two parameters the spec named and one policy object, and the old paths re-export
for one cycle.

★ THE FIRST STEP FOUND TWO GATES THAT READ THESE FILES BY PATH, which is why it is
now the first step. Landing 1 nearly disarmed check-facet-cascade the same way.
  internal/check-csv-export-shape.mjs does not import useCsvExport; it READS the
  source and extracts esc() out of it. Left at the plugin path it would have read
  the one-line shim, found no esc(), and failed with "could not extract esc()" — a
  gate accusing a working exporter because its subject moved.
  check-filter-multivalue.mjs — MINE — lists the same file in its PANELS array.
  Left at the plugin path it would have read the shim, found no predicate to
  object to, and PASSED VACUOUSLY. That is worse: a gate going quiet about the
  thing it guards. Both move in this commit.

THE COORDINATE IS A PARAMETER; WHAT MAKES A COORDINATE VALID IS NOT. `coords`
replaces only the READ of row.longitude / row.latitude, which is pray-tools'
spelling and not a universal one. Every guard stays shared: Number() on API
strings, 0 accepted per axis, the exact 0,0 null-island sentinel rejected, the
out-of-range check. A kit that let each caller re-implement that validation would
hand the next dashboard the same four bugs to rediscover.

★ AND THE OPTION WAS DEAD WHEN I FIRST WROTE IT. `coords` was accepted in the
signature and never passed to selectionBounds, so a caller could set it,
type-check clean, build clean, and watch it do nothing. This tree has paid for
that shape before — a defineProps on a component the custom element never binds —
and it is worse than a missing feature because it READS as supported. Nobody files
a bug against a knob they assume they misused. check:kit-options-reachable now
asserts, across all four shared composables, that every option accepted is used.

THE CAMERA'S EIGHT CONSTANTS ARE ONE DECISION, so CAMERA_POLICY exports them
together. The failure that prevents is a second dashboard re-picking them one at a
time and arriving at a camera that feels different from Big Picture's for no reason
anybody wrote down. It is read-only and deliberately not yet overridable: making
each a per-caller option means threading eight of them through every use, and no
consumer has asked for a different camera. Exporting the decision so it can be
cited is the half with a use today; saying so is better than shipping eight
options nobody passes.

THE EXPORTER GAINS exportLabel and sources. "Research Dashboard" was this
dashboard's name, and the provenance line said nothing about where the data came
from — which is what a provenance line is for. Both default to today's output, to
the byte.

★ AND THE NEW GATE HAD THE COMMENT HOLE TWICE IN ONE SITTING. First it searched
the raw body, and the comment beside the fixed call site says "`coords` IS PASSED
HERE" — so the mutant that removed the argument left it green. Then, stripping
comments only per-part, it split the signature's comment PROSE on commas and
reported that useCsvExport accepts `and`. Comments are text; every text search has
to say so, in both directions. That is the fourth variant of one mistake this
week, and all four were caught by running the mutant rather than by reading.

Library index regenerated in the same commit, 103 items. Source only.
STUDIO: needs a rebuild of research-dashboard and dashboard-plus-grid. The
internal/ hunk is one line in check-csv-export-shape.mjs.
2026-09-27 01:16:52 +00:00
Doxa Maps
1bd5f109d8 C4 wave 3c, my-upg-100-list: two humanizeKey guards become one call
poster-wall.vue and upg-100-mfe.vue both had the same two lines — a humanizeKey
fallback, then `try { t = useI18n().t } catch` — and now call
useOptionalT('', { fallback: (k) => humanizeKey(k) }).

No namespace: callers pass FULL keys here, which is what the bare useI18n().t they
used to get required.

humanizeKey stays as the fallback rather than taking the catalogue default, so a
bare host still reads a title-cased last segment. gospel-plugin chose the
locale-honouring default for their bundle; the same one-word choice is upg100's to
make for theirs.

★ ONE BEHAVIOUR GAIN, stated rather than smuggled: the old i18n path was the raw
useI18n().t, so on a host WITH i18n a missing key rendered the dotted key itself.
It now falls through to humanizeKey — the fallback a bare host already got. Nothing
that resolves today changes; a key that is missing reads better.

Register now covers 14 migrated sites.
2026-09-27 01:16:51 +00:00
Doxa Maps
ccc9a44995 Register the all-people-groups row, which the previous commit missed
The insertion in ef8b8602 failed on a bad anchor and the commit went ahead anyway,
so the migration landed with no row asserting it. The gates all passed, because a
register that does not mention a file cannot fail on it -- a green run that proved
nothing about the thing just changed.

The row asserts the useOptionalI18n form and that its inline EN map is still passed
as the fallback.
2026-09-27 01:16:51 +00:00
Doxa Maps
d882f4ec7c C4 wave 3d, all-people-groups: the {t, te, locale} form, English preserved
This site needed more than `t`, so it is the first to take useOptionalI18n: it used
te() to choose between the catalogue and its inline EN map, and i18n.locale to pick
a number format for toLocaleString.

EN stays the fallback, passed in. It is an inline English map rather than the
catalogue, so routing it through tStatic would change what a bare host renders.
gospel-plugin and exp-shepherd each CHOSE the locale-honouring default for their own
bundles; all-people-groups has no owner to ask — studio agreed to treat it as
unowned — so the conservative reading wins and it renders exactly what it rendered.
An unowned file is the wrong place to make a product decision by default.

Two equivalences worth stating rather than assuming:
 - the old code asked te() first and only then t(); the helper calls t() and falls
   back when it returns the key unchanged. Same outcome either way — present key
   gives the translation, absent key gives EN — and it no longer depends on te()
   being implemented by the host.
 - keys arrive BARE here ('hint', 'pray'), and qualify() prefixes 'allPeopleGroups.'
   exactly as the hand-written `const full = 'allPeopleGroups.' + key` did.

locale needs unwrapping: it is a REF when the host installed i18n and a plain string
when it did not. PosterWall.vue records the same variance, which is why the helper
hands it back as-is rather than pretending one shape.
2026-09-27 01:16:51 +00:00
Doxa Maps
02b2e9d59a The second consumer drops its copy of the highlighter
people-groups-grid is the file whose duplicate earned useTextHighlight its place in
library/ under RULE 2, and studio agreed to treat it as unowned and asked for it to
be cut. Its hand-rolled range walk is replaced by highlightRanges(value,
match.indices), and its local escapeHtml goes with it: once the loop moved, that
function had ZERO callers left.

★ ONE CORRECTION TO THE STATED REASON. studio approved this with "removing the
null-throw is reason enough". Measured before accepting it: that escaper has exactly
three callers, all inside `if (value && typeof value === 'string')`, so the throw
was NOT REACHABLE here. The library escaper's null guard is still an improvement in
principle, but it fixes nothing that could fire in this file, and I would rather
carry the smaller true reason than the larger convenient one.

The reason that does hold is the original one: the same construction existed twice,
character for character apart from variable names. One fact, one copy.

The gate flips with it — check:text-highlight now asserts this file routes through
the helper, that its escapeHtml is gone, and that the hand-rolled range walk is
really removed rather than merely shadowed by the new import.
2026-09-27 01:16:50 +00:00
Doxa Maps
29bbab3967 Two plugins built the same search highlighter; now there is one
Dispatch work item (5) asked for useTextHighlight in library/. RULE 2 wants a named
SECOND consumer before anything moves there, and it exists: regions-dashboard's
UupgsList and people-groups-grid both consume the same Fuse-compatible shim, both
read match.indices, both wrap hits in <span class="search-highlight">, and both
append the tail only on the last range. Line for line the same construction, apart
from variable names.

So library/composables/useTextHighlight.js exports highlightRanges(value, indices),
and UupgsList calls it. Its local escapeHtml goes too: every caller was inside the
highlight body that moved.

★ ESCAPING NOW COMES FROM library/utils/html.js, AND THAT IS A CHANGE FOR BOTH
CALLERS, stated rather than smuggled. Three implementations existed and differed:
  library/utils/html.js  escapes & < > " ' AND backtick; returns '' for null
  UupgsList              no backtick; guarded null via String(v ?? '')
  people-groups-grid     no backtick; NO NULL GUARD — s.replace(null) THROWS
Both deltas move in the safe direction — one more character escaped, one crash
removed — and neither changes the output of any value that renders today. The gate
pins both, with controls proving each delta is a real difference rather than a
no-op: the old copy really does leave a backtick unescaped, and their escaper
really does throw on null.

people-groups-grid is NOT migrated here: it is another owner's file, so it is
handed over as source. The gate asserts it still builds its own highlighter, so
this check states the truth rather than implying the job is finished.

check:text-highlight is registered (script line, gates:post token, hand-inserted)
and negative-tested: an implementation that escaped nothing fails it, and the
output is asserted to depend on the indices.
2026-09-27 01:16:50 +00:00
Doxa Maps
f13075b1d4 Decouple the tooling: no host named in internal/ or the build config
Six tools defaulted to the live host's URL. They now read the published tree's location from
DOXA_PUBLISHED_BASE (environment, or the gitignored .env / .env.local) through internal/_published-base.mjs,
and with nothing set they report NOT MEASURED (exit 75) instead of measuring a host they were never told
about: check:catalog-currency, check:catalog-identity, check:published, verify:5-caps, verify:map-paint. An
explicit BASE / --base / PUBLISHED_BASE still wins. check:embed-base only needed an example URL for its
offline resolution table.

The public-hygiene leak detector builds the platform name from parts, so the file that catches it does not
carry it. Comments and the two build-config notes say the host platform or the live test host. From
repo's DECOUPLE round 2 list.
2026-09-27 01:16:50 +00:00
Doxa Maps
78a37c1b5f Gate rule 4, and find the 16 links my own measurement missed
"Verify a URL resolves before shipping it" had a verifier and a record, and nothing
asserting the record covers what the build actually ships — so the coverage number
was something a person went and measured, and I measured it earlier this flight as
"8,445 distinct URLs, 100% covered". That was 16 links short, and short for the
reason this bundle keeps relearning: I walked a LIST OF FIELDS.

This check recurses the pin index and treats any http string as a shipped link. It
found 8,461, and the 16 extra were real: the 15 translator-organisation homepages
and the SUN block's three pages, none of which the verification table has ever heard
of. They are not exempt — they are verified in their OWN records, by the scripts that
added them, and the check resolves against all three. SUN's evidence is the stronger
kind: sun.bible answers 200 for any path, so its pages are proved by content against
a catch-all control that returns 904 characters where the New Testament returns
22,043.

Two more things the run corrected rather than confirmed. The status-blind exemption
is a URL SHAPE, not a host: jesusfilm.org/watch/jesus/<slug> does discriminate and
2,049 film links were migrated onto it for exactly that reason, so exempting the host
would have re-blinded all of them — only the older /watch/?languageId= form is blind,
and one such URL remains. And the poison arms now derive their failures from real
entries instead of a synthetic push, because an arm that manufactures its own failure
proves the assertion runs, not that it can catch anything.
2026-09-27 01:14:33 +00:00
Doxa Maps
25c3d10abd Rebuild country-posters, and regenerate the tree
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-27 01:11:39 +00:00
Doxa Maps
81ca86e39f Country posters: a religion search names the family in the reader's language
The legend row it selects is keyed on the family letter, so only the shown label changes.
2026-09-27 01:10:49 +00:00
Doxa Maps
165983c2c8 Country posters: the geocoder column is styled on MapLibre too
countries-map styled only .mapboxgl-ctrl-top-left. MapLibre names its
container .maplibregl-ctrl-top-left, so under MapLibre the column got no
position, width or margin rule at all. The three rules (desktop column, its
controls, and the mobile full-width row) now name both engines' classes.

Headless: build OK, and the shipped CSS carries the maplibregl selectors.
Visual check: rega, countries-map on ?provider=maplibre (en and ar).
2026-09-26 22:19:56 +00:00
Doxa Maps
e4f2be71c4 Country posters: the geocoder column follows the reading direction
countries-map pinned its geocoder column with a physical left:8px, so under ar
the search bar stayed on the left while the legend (studio's fixed legend) moved
right, and the two no longer stacked as one column. It now uses
inset-inline-start:8px, which is 8px from the start edge in either direction.
The pinned card's label spacing (margin-right) becomes margin-inline-end too.
The mobile rule already sets both left and right, so it is symmetric and
unchanged.

Headless: build OK, and the shipped CSS carries inset-inline-start:8px. Visual
check needs a browser (ar, both engines).
2026-09-26 22:19:56 +00:00
Doxa Maps
835ec1ac28 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
big-picture
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
people-groups-grid
pplr-data-maps
regions-dashboard
research-dashboard
research-map-plus-grid
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-26 22:14:55 +00:00
Doxa Maps
d8e50de16b cities-internet takes the locale-honouring fallback, and drops its English copy
exp-shepherd's call as the plugin's owner, on the reasoning gospel-plugin gave
first: the bundle already carries all 11 catalogues, so a partner embedding it
without vue-i18n being served English is a bug rather than a decision.

So the three files pass no fallback and resolve through tStatic — the runtime
locale first, English as the last resort. Their enLookup was English-only, which is
exactly what served English to those hosts, so it goes, and the per-area
connectivity.json import goes with it: tStatic reads the same area out of the
catalogue the bundle already ships. Three fewer copies of a lookup, three fewer
imports of bytes that were already there.

Two entries added to the register as DELIBERATE absences, so a later reader cannot
"complete" C4 by converting them:
  · pplr-data-maps/src/components/SemanticTreeLegend.vue keeps its hand-rolled
    guard at exp-shepherd's request — it is carded for retirement
    (RETIRE-PPLR-SEMANTICTREELEGEND) in favour of the library component, and a
    migration whose value expires before it lands is not worth the diff;
  · country-posters-mfe.vue and key-cities/cities-map.vue are their owner's to cut,
    so that country-posters stays one landing and key-cities stays gated.
2026-09-26 22:06:59 +00:00
Doxa Maps
ef5ea1840a C4 wave 3b, cities-internet: three identical guards become one call
All three files carried the same block byte-for-byte — enConnectivity + enLookup +
fill + try/catch — and now call useOptionalT('connectivity', { fallback: enLookup }).
English is unchanged on a bare host, because each passes its own enLookup rather
than taking the catalogue default.

gospel-plugin chose the catalogue default for their bundle, on the argument that a
partner host without vue-i18n is served English while the bundle already ships all
11 catalogues. The same one-word choice is open to cities-internet's owner; it is
not a migration's to take, so this preserves what they have.

Their local fill() used `k in params`, so the shared fillParams also removes a way
for these three to render the literal string "null" into a sentence.

Also records, in the stripper itself, the two things codeOnly does NOT strip —
CSS comments inside a JS template literal, and HTML comments in a .vue template.
Both have already cost a real measurement here: a specificity read of (0,5,53),
and a bare-useI18n census that read 3 where the code had 2. gospel-plugin's
generalisation is the one worth keeping: any <!-- --> that quotes an identifier
will keep doing that to you. The stripper must match the LANGUAGE of the text.
2026-09-26 22:06:59 +00:00
Doxa Maps
9816a4244f gospel takes the locale-honouring fallback, and it is their call that made it
gospel-plugin's decision, in their words: "most partners will drop a custom
element or an iframe and never install vue-i18n ... an Arabic ministry site
embedding this map serves its readers English while the bundle already carries all
11 catalogues. That is a bug, not a decision."

So all four gospel translators drop `{ fallback: enLookup }` and take the
helper's catalogue default, which resolves through tStatic: the reader's locale
first, English as the last resort. Their three conditions are met and each is
asserted, not assumed:
  1. resources-map passes NO namespace — its keys span gospel.* and pins.*;
  2. the fallback walks the catalogue ROOT, which tStatic does by construction;
  3. params survive, via the null-safe fillParams.

★ THE ROW THEY ASKED FOR, and it is the only thing here that could catch the
regression this migration risked: a BARE host under ar must render
pins.approximate in ARABIC — not the key, not gospel.pins.approximate, not the
English. It reads the SHIPPED ar/ and en/ pins.json and rebuilds tStatic's own
resolution order over them, so it measures the real catalogue rather than a stub.
Three negatives beside it, plus a control that both catalogues really carry the
key, so the comparison is measuring a difference that exists.

enLookup is deleted from all four, and the EN_MESSAGES import from three —
data-tables keeps it, because it still builds a map from EN_MESSAGES.gospel.res at
:182. Checked before deleting rather than after: check:no-free-identifiers would
have caught a wrong removal, since removing something still used creates a name
that is valid JS until it runs.

Two corrections that came from other agents reading this work:
 - studio: the JSDoc carried <area>.<key>-shaped examples, which
   check:i18n-keys-exist reads as real keys and reports missing. They reworded it
   twice because my file copy restored it. Reworded, with a standing note in the
   file so a third copy cannot bring them back.
 - the register said each site "takes the CATALOGUE fallback, as it always did".
   True of the two composables, FALSE of gospel's four, which were moved here by
   their owner. A false statement inside a gate message is worse than no message,
   because a reader has no reason to doubt it. Now accurate per row.

check:i18n-keys-exist is red on messages.engineNotLoadedDetail. NOT mine and not
touched here: it is in cities-internet/connectivity-map.vue, whose own comment says
the key "arrives with upg100's ade12e4c", and the call already guards for its
absence.
2026-09-26 22:06:59 +00:00
Doxa Maps
0d0ee3cbed SemanticTreeLegend says a dot is a locality, on every map with people-group pins
The Driver's card BUILD-APPROXIMATE-PIN-NOTICE: every map that shows people-group pins must say so plainly.
The standing APPROXPIN note existed only under LegendRows and in the pin card, so maps whose legend is
SemanticTreeLegend (the prayer map's flat tabs among them) never showed it (rega, row 46).

It now renders under the tree when the map's own data holds named Point features (the injected dataStore);
maps without people-group pins show nothing. A pinNote prop overrides for a leaf that already says it.
2026-09-26 22:06:58 +00:00
Doxa Maps
8ec207c662 My coalescing broke the echo guard it ran past
rega, row 50: a region row click emitted {region:"asia"} and then an EMPTY event,
identically on both engines. The grid stayed narrowed only because the
composite's 1.2 second push window happened to swallow the second one — and a
pass that depends on a swallow is not a pass. rega said so, and was right to
refuse to score it green.

THE CAUSE IS MINE AND IT IS NOT A TIMING QUIRK. emitFilterState has always opened
with `if (applyingExternal) return`: when the HOST pushes state DOWN through
externalFilters the store changes, and reporting that back up is an echo of the
host's own instruction. That guard worked because the emit ran SYNCHRONOUSLY
inside the watcher, while the flag was still set. My coalescing moved the emit
60ms later, and applyingExternal is released on nextTick — so by the time the
emit ran the flag was false and the echo went out.

I fixed a burst and broke a guard that depended on the burst's timing. Changing
WHEN something runs is a change to everything that depended on when, and I did
not re-read what else was standing on that synchrony.

So the decision moves to schedule time, where the flag still means what it says:
a change that arrived from the host is not a change the reader made, and is not
reported back. The check inside emitFilterState stays as a second line of
defence.

This is the same shape as the dialect case on the branch below, and it is why
that one is a guard rather than a longer timeout: both are "the emit is no longer
where it thought it was".

★ AND THE GATE CAUGHT THE SAME HOLE IN ITSELF FOR THE THIRD TIME. The assertion
searched the scheduler for `if (applyingExternal) return`, and the comment
explaining the fix QUOTES that line — so deleting the code left the gate green.
It strips comments before asserting now. A check its own documentation can
satisfy is not a check, and I have now written that mistake three different ways:
a probe that yields zero when its assumption is wrong, a mutation aimed at one of
three identical bindings, and this.

Source only, stacked on sr/dialect-empty-cascade. STUDIO: rebuild
doxa-research-map (the same rebuild that branch already needs).
2026-09-26 22:06:58 +00:00
Doxa Maps
dffae0502c A clicked pin finds the groups that share its point
rega, row 46: on a point four groups share, the note never said so. A click hands the legend the RENDERED
feature, whose geometry the engine rebuilt from tile coordinates (about 0.001 degree off at low zoom), and the
co-location index matched on the exact coordinate. The pin is now resolved to its roster row by name, and to
the nearest of those rows when a name repeats across countries; only a name the roster does not know falls
back to the pin's own coordinate. check:approx-pin-note adds both cases; the tile-rounded one fails on the old
code.
2026-09-26 22:06:58 +00:00
Doxa Maps
087b005350 Big Picture: the live count speaks, and the locale reaches the children
rega, row 47 at 52eeb0db. Two findings, both mine, and the first is the smaller
and more certain of the two.

"2107 shown" STAYED ENGLISH IN BOTH CHANNELS, because it was never a lookup:
BpResearchHeader rendered the count and then the bare word. It reads the
catalogue now, with the English wording kept exactly as it was — "{n} shown" —
per the standing rule that translating existing wording is not a product
decision. Eleven locales.

THE CHILDREN WERE HANDED ONLY THE LIST. big-picture built its children's
profile-config from `list` alone, so the one documented channel a partner has for
asking a nested element to speak Arabic arrived empty of any locale.
setRuntimeLocale covers everything that reads tStatic, which is why the headline
and much of the chrome were already correct through both channels; it does not
cover a child that reads its own profile-config. Threading the locale is what
ProfileLoader would have done had one existed beneath this composite — and the
reason none does is the (C) failure mode this element is the last example of: a
composite that BAKES its children in has no loader under it, so it must do that
job itself.

★ THIS IS ONE IDENTIFIED GAP, NOT A CLAIM TO HAVE CLOSED THE MEASURED ONE.
check:host-locale-contract reports 402 against 888 for this leaf, and 31 Arabic
text nodes against 79. I can see that the children were given no locale and that
giving it to them is correct regardless; I cannot see from here how much of that
difference it accounts for, and I am not going to assert a number I have not
measured. The gate is a browser gate and rega owns the re-run.

Source only. STUDIO: needs a rebuild of big-picture.
2026-09-26 22:06:58 +00:00
Doxa Maps
c53d356d3f The one event was the cascade's empty moment, not the selection
rega, row 43 case 3 at 52eeb0db: the coalescing landed — one event per click,
exactly as intended — and in 3 of 4 runs that one event was EMPTY,
{language:"", languageCode:"", languageCodes:[]}, so the grid stayed at 2,107.
The name fix works whenever it fires; the event just carried nothing to name.

THE CASCADE PASSES THROUGH A CLEARED STATE. mapStore.selectLanguage() sets
selectedDialect = null BEFORE it sets the language, so a dialect click walks the
store through "nothing selected" on its way to the answer. The settle timer fired
inside that window and read exactly what was there. The composite then did the
right thing with what it was told: an all-empty report IS how this element says
"the reader cleared everything".

MY FIRST FIX WAS A TIMEOUT, AND A TIMEOUT WAS THE WRONG KIND OF ANSWER. 60ms was
enough in my seat and not under headless swiftshader. That is the third time I
have tuned a duration on one machine and called it settled, so this one does not
get lengthened — lengthening moves the failure rather than removing it.

The guard is SEMANTIC instead. An all-empty read arriving straight after a filled
one is the one genuinely ambiguous case — it is what a mid-cascade read looks
like AND what a real clear looks like, and nothing in the payload separates them.
So it is re-taken rather than believed: the transient fills in and is reported
correctly, while a real clear stays empty through every re-take and is reported a
few tens of ms later, which no reader can perceive. The re-takes are BOUNDED
(3 x 60ms) so a clear can only ever be delayed, never swallowed, and the whole
budget stays far below the composite's 1.2s push window so the two cannot
collide. The budget resets on every fresh user action.

Note what is NOT assumed: the retries re-read the store on their own timer rather
than waiting for another watcher fire. In the failing runs the watcher fired only
ONCE, so a fix that depended on a second fire would not have worked.

check:language-code-link grows a section G for it, asserting both halves — the
guard exists, and it is bounded. Without the first the transient ships; without
the second a clear can never be reported at all. Both negative-tested.

Source only. STUDIO: needs a rebuild of doxa-research-map.
2026-09-26 22:06:58 +00:00
Doxa Maps
a94a85cb8c C4 wave 3a, gospel-resources: four guards share one, English unchanged
From regions-plugin e04121f1 re-cut as 7138109f on main 4d18a6f3. charts, data-tables and resources-list call
useOptionalT('gospel', { fallback: enLookup }); resources-map calls useOptionalT('') because its keys span two
areas (gospel.* and pins.*) and its enLookup walks the catalogue root, so a namespace would have turned
pins.approximate into gospel.pins.approximate. Each keeps the English fallback it already had.

useOptionalT's fillParams substitutes only non-null values (the rule main's own fill() already used), so
{ count: null } leaves the placeholder visible instead of printing "null". check:optional-t-equivalence
(59 assertions) asserts the empty namespace for resources-map and strips HTML comments before counting its
bare useI18n() calls.
2026-09-26 22:06:58 +00:00
Doxa Maps
f9020e4910 The legends mirror now, and their labels never became their values
rega's Arabic survey on main 52eeb0db: the research-dashboard legends sat on the
wrong edge with English text, 70 of 85 letters Latin, in Big Picture and in the
Research Gallery. Two faults that look like one, and they fail in opposite
directions if you fix them carelessly.

A PHYSICAL INSET DOES NOT MIRROR. right:8px pins a box to the physical right edge
in every locale, so in Arabic it stays where an English reader expects it while
everything around it flips. Both surfaces the survey named are logical now, and
inset-inline-start:auto travels with inset-inline-end as a pair rather than as
decoration: it cancels a physical inset inherited from a control container, which
otherwise stretches the box across the map once the anchored edge moves.
The second surface is research-map's geocoder column, which studio found in its
own sweep. That column exists ONLY to share an x-edge with the legend below it —
the alignment is recorded in the file — and the legend already mirrored while the
search bar did not, so under ar the pair came apart and the bar was stranded on
the side the legend had vacated. A shared edge that one of the two honours is
worse than no alignment: it reads as a broken layout rather than as a choice.

THE ENGLISH WAS NOT CHROME, AND THAT CHANGES THE FIX ENTIRELY. The legend renders
a facet VALUE, and for the religion donut that value is a family name DERIVED in
English by library/colors/religion.js — the same table people-groups-grid
re-derives per row so the two ends byte-match. Translating those strings is the
obvious fix and the dangerous one: it would work in English and match nothing in
Arabic, SILENTLY, because an empty intersection reads as "no results". That is
the language-link defect one facet over, and it cost a card to learn the first
time.

So the value stays canonical English and only the visible text is projected, the
same rule the shareable URL follows, where the code is canonical and the slug is
display-only. religionFamilyLabel() lives beside the table it projects, so the
next consumer finds it rather than inventing a second spelling, and the table
itself now carries a comment saying these are values and must not be translated.
Ten family names in eleven locales, English wording held.

check:legend-mirrors guards both halves — and its own negative test found a hole
in it. A mutation aimed at the legend's data-key silently hit a DIFFERENT
:data-key, the donut segment, and the gate stayed green while a projection sat on
a wire. There are three such bindings and it was guarding one. It now checks
every :data-key and :data-facet in the file; all three mutants are caught.

★ AND I BROKE THE FILE'S PARSE ON THE WAY, worth recording because the rule is
already written down. I used backticks around CSS property names in a comment
that lives INSIDE a shadow-CSS template literal, where a backtick ends the
literal and everything after it is read as code. check:sfc-parse and
check:no-free-identifiers both caught it immediately. The comment now says so, in
the file, without backticks.

Source only. STUDIO: needs a rebuild of research-dashboard, dashboard-plus-grid,
doxa-research-map and research-map-plus-grid.
2026-09-26 22:06:58 +00:00
Doxa Maps
c38efae49c gospel.guide: the legend's way-in sentence, in 11 locales
wayInSome ("{n} of those {total}"), wayInAll ("Every one of those {n}") and wayInWhat (the sentence that
follows), requested by gospel-plugin: the last English sentence in an Arabic legend. The resources map already
reads them through tOr() with its parameters, so it switches on its own once they land.
2026-09-26 22:06:58 +00:00
Doxa Maps
a693ca6832 Decouple deployment/: no go-base / nuxtbase / doxa-base names in the Tauri README, the WordPress how-to and its contract test
Prose: the Tauri README's token note says 'the test host', the WordPress how-to's hosting option says
'a Forgejo host you run'. The WordPress contract test's catalog base was the live host as a fixture;
it is now https://cdn.example.org/plugin-assets/ in all three places (the passed base, the expected
script src, and the negative test), which keeps the test self-consistent: check:wp-embed PASS before
and after.
2026-09-26 22:06:57 +00:00
Doxa Maps
09aba6dc92 C4 wave 2: five library components share the guard, and keep their own English
The five are NOT one shape, and treating them as one would have changed what
readers see. Measured before touching them:

  ApproxPinNote, FilterDropdown   fall back to a literal map
  SemanticTreeLegend, PosterWall  fall back to humanizeKey() — 'legend.columns.name' renders 'Name'
  PinHint                         falls back to a one-key map
  SemanticTreeLegend, PinHint     also capture i18n.te (used 3x and 1x)
  PosterWall                      also captures i18n.locale

So useOptionalT grew an optional `fallback` hook, and useOptionalI18n was added
for the callers that need te and locale. Every site passes the fallback it
already had, so not one rendered string changes. Routing them through the
catalogue instead would have altered what seven bare-host bundles print, and —
because tStatic honours the runtime locale — would have started translating
strings that are English today. That may be an improvement; it is not this
commit's to make. Changing a default is not a migration.

te keeps its existing contract too: FALSE on a bare host, never "true because the
catalogue has it". PinHint and SemanticTreeLegend both record why — te is how
their callers know to use their own defaults.

THE CHECK NOW LIVES ON MAIN, which is the point of this commit as much as the
migration is. dx-shepherd: the 18-assertion version was in _qa-cycle, so it would
never have run again. internal/check-optional-t-equivalence.mjs is registered as
check:optional-t-equivalence — script line, //scripts note and gates:post token,
all inserted by hand rather than by re-serialising package.json. It carries 46
assertions: the helper's contract across all three host states, a branch-for-
branch equivalence against the old hand-written code, and a PER-SITE REGISTER
that every later C4 wave appends a row to. Negative-tested from outside: strip one
site's own fallback and it fails naming that site.

check:library-index went red on the new composable and is green again after
`bun run library-index`. That is the staleness gate working, not a flake.
2026-09-26 22:06:57 +00:00
Doxa Maps
1a17d3a211 One guarded translator, and the fallback catalogue nobody needed
C4, first wave. Twenty places each stored the same fact — how to translate when
the host never installed vue-i18n — and two of them, usePplrT.js and
useNeighborT.js, were whole-file near-duplicates at Jaccard 0.82. That pair was
the curation audit's evidence that the guard was being COPIED rather than shared.
Both now call library/composables/useOptionalT.js and keep their public names, so
no call site changes.

★ THE SPEC ASKED FOR (namespace, fallbackCatalogue) AND THE CATALOGUE IS
UNNECESSARY. Every copy imported its own English JSON and hand-walked it after
stripping the namespace prefix. But library/i18n builds `messages[locale][area]`
keyed by the JSON's own filename, and tStatic() already walks that tree and
already falls back to English. So `import enPplr from '.../en/pplr.json'` was a
second copy of bytes the bundle already had, and the helper takes one argument.

★ AND IT IS NOT A SIZE REGRESSION, which was the one good reason the narrow
import might have existed. The build NARROWS library/i18n's catalogue glob per
bundle to the areas internal/i18n-areas.mjs finds in that bundle's source graph
(check:i18n-treeshake). A bundle that says 'pplr.' gets the pplr area, not the
other thirty. Verified before writing the helper, because "the obvious dedup
makes every bundle carry every locale" would have killed it.

Resolution order, each step earning its place: the host's own vue-i18n when it
installed one, so a host managing locale keeps control; then tStatic, which
honours the runtime locale and falls back to English; then the key itself, so a
missing string is visible rather than blank. The middle step also covers i18n
being present but empty — i18n.t() returns the key unchanged, which every copy
of this pattern already treated as "not found".

Verified by _qa-cycle/02-builder-cycle/_chk-c4-optional-t-equivalence.mjs: 18
assertions across all three host states, with a branch-for-branch equivalence
check against the old hand-written implementation and two negative controls. It
extracts and executes the module source, because useOptionalT imports vue-i18n
and an i18n index that calls import.meta.glob, which plain Node cannot run.
2026-09-26 22:06:57 +00:00
Doxa Maps
c2987e8134 PinHint shows only while its map is on screen
It gated on a map canvas EXISTING. A composite (pane-wrapper) keeps the map pane mounted while the reader
scrolls to another pane, so on UPG-100's phone view the pill was clamped into view over the poster pane,
where there is no map to tap (Driver, README picture; traced by upg100-plugin and repo).

An IntersectionObserver on the map box now decides: under a fifth of the map in view hides the pill, and
scrolling back shows it again. Only the reader's close button dismisses it for the visit, as before. Without
IntersectionObserver the old behaviour stands.
2026-09-26 22:06:57 +00:00
Doxa Maps
2449098a68 The legend mirrors in Arabic: logical insets instead of left/right
The shared legend was placed with physical left/right, while the map controls use inset-inline-end. In
Arabic the controls mirrored to the left, onto a legend that stayed left: on Simple Research, Prayer Progress
and UPG-100 (rega's RTL survey at 52eeb0db; traced by upg100-plugin and repo).

SemanticTreeLegend: the panel, its reopen button, the tree-guide hairlines and the export menu use
inset-inline-start/end; the closed panel slides off to the right and its collapse arrow flips under
:dir(rtl) (additive overrides, as VerticalTabsLayout does). LegendDesktop, LegendMobile, LegendRows,
LegendTools and LegendProgressBar: the card, reopen, detail close/collapse buttons, the name gutters that
clear them, indents and spacing are logical; LegendTools re-centres under RTL. LTR renders the same.

check:legend-logical-insets (with :self-test, both in gates:post) fails on any physical left/right in the
legend components unless it is symmetric, an auto reset, or allowed with a reason.
2026-09-26 22:06:57 +00:00
Doxa Maps
29f4ab7097 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# 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-26 22:04:34 +00:00
Doxa Maps
d17950086a Rebuild my-upg-100-list from the merged source, and re-pin its three leaves
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The six public/ files the merge left holding main's side are regenerated here
rather than merged, because a bundle is an OUTPUT: the only correct content is
whatever `BUNDLE=my-upg-100-list vite build` emits from the merged source, and
any hand-reconciled middle version is wrong by construction even when it looks
plausible in a diff.

fix:sri then rewrote seven stale hashes across five files. That summary is NOT
the evidence: the three leaf integrity attributes were re-derived independently
here by recomputing sha384 over the emitted bundle and comparing to what each
HTML claims — 3 recomputed, 0 mismatches. This is the check that was skipped
when three consecutive commits shipped leaf HTML 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's own output had reported success.
2026-09-26 21:46:07 +00:00
Doxa Maps
175ba52cf3 Merge main (studio landing 4) into packet/vertical
Landing 4 carries the nine messages keys this branch's packet and poster
funnel call (4be260a0), so the eleven catalogue conflicts resolve to MAIN'S
side in full: measured first, not assumed — my side added exactly those nine
keys to each of the eleven locales, changed no existing key, and all nine are
byte-identical on main. Whole-file take-theirs therefore loses nothing, which
is the only condition under which taking a whole shared catalogue is safe.

The six public/ conflicts are build artifacts and are NOT hand-merged: they
take main's side here and are regenerated from the merged source in the next
commit. A hand-merged bundle is what produced three consecutive SRI
mismatches, where leaf HTML pinned a hash for a bundle that no longer existed
and the browser refused the script — a blank page in 6/6 runs.
2026-09-26 21:45:06 +00:00
Doxa Maps
78b4b81e1f 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-maps-spa
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
plugin-studio
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-26 18:50:12 +00:00