Commit graph

1927 commits

Author SHA1 Message Date
Doxa Maps
183bb21589 PPLR maps: a map that cannot be built says so instead of throwing
When the map failed to construct (Mapbox with no token, for one), every map view
called map.value.on('load') on null and threw "Cannot read properties of null" in
its mounted hook. All seven map views now stop there and show the shared, translated
"Map unavailable" in their status line.
2026-09-27 04:38:17 +00:00
Doxa Maps
3734cf3882 Key cities: the map honours provider=maplibre instead of always building Mapbox GL
It loaded and constructed Mapbox GL whatever the page asked for, so under
provider=maplibre Mapbox warned about missing CSS and an empty style. The engine now
comes from the library's provider resolution (auto = Mapbox only with a token at hand,
including the injected one), the engine's stylesheet goes into the shadow root, and the
style is the registry pair connectivity-map uses: ink (dark-v11 / OpenFreeMap dark) or
paper (light-v11 / positron). No silent fallback to Mapbox; the map stays gated.
2026-09-27 04:38:17 +00:00
Doxa Maps
54d04fc388 Prayer Walk Planner: the resolved instance id is exposed under its own name
The element declares an instanceId PROP and also exposed an instanceId member,
so Vue warned "Exposed property 'instanceId' already exists on custom element".
The exposed member is the RESOLVED id (the prop, or a generated one), so it is now
resolvedInstanceId. The window.PrayerWalkPlanner registry keeps keying on it,
unchanged, and no reader used the exposed name.
2026-09-27 04:38:17 +00:00
Doxa Maps
053bdb41b1 Prayer map without a key: crisp rose borders and light mixed-case country names, as on the reference 2026-09-27 04:38:16 +00:00
Doxa Maps
d677a36a83 Prayer map without a key: the night sea, slate land and rose borders of the Mapbox look
With no token the engine is MapLibre and the basemap is OpenFreeMap fiord, slate-blue
everywhere: sea and land 13 units apart, blue-grey borders. The Mapbox Prayer map is
Standard at night: near-black navy sea, slate land, rose borders. The keyless map now
recolours fiord to the colours measured on the Mapbox render of this leaf (ocean
#03112d, land #323f52, border #c95d74), on every style load, on MapLibre and the dark
basemap only. The Mapbox path is untouched.
2026-09-27 04:38:16 +00:00
Doxa Maps
f487140172 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-maps-spa
doxa-research-map
doxa-simple-map
gospel-resources
key-cities
my-upg-100-list
people-groups-grid
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-27 04:26:57 +00:00
Doxa Maps
30c42b5f46 Library index: regenerated once for everything studio landing 10 adds to library/ 2026-09-27 04:14:32 +00:00
Doxa Maps
f1b33af772 Validate the CSS we build as JS strings
check:sfc-parse proves a .vue script block parses. A broken stylesheet
inside a valid string literal is still a valid string literal, so it
passed a sheet with 9 declarations stranded outside any rule and braces
at 12 open / 21 close. Nothing else in the tree validated CSS syntax.

check:shadow-css-parses reads every useShadowStyles() argument and scans
the result as CSS: brace balance, declarations stranded at depth 0, empty
selectors. 72 stylesheets across library/ and plugin-designs/.

It reads the arguments off the @babel/parser AST the build already uses,
because three hand-rolled lexers each produced a confident FALSE RED on a
file that was fine: a /* CSS comment */ read as a stranded declaration; an
apostrophe in a .vue prose header opening a phantom string literal, so a
</div> from the template was reported as CSS; and a regex literal
containing a quote, which cannot be lexed without resolving regex-vs-
division. The CSS scan itself stays hand-written, because CSS is the thing
being checked -- and it consumes CSS comments and quoted values as their
own token types, so content:'a{b' opens no rule.

9 self-tests run inline on every invocation, including the exact damage
above, so the gate cannot quietly stop being able to fail. An argument
built partly from an imported constant is scanned for its literal
segments and labelled, rather than skipped -- skipping it would have
excluded region-dashboards.vue, which builds its sheet from the token
layer plus literals.
2026-09-27 04:14:32 +00:00
Doxa Maps
458ca5ff59 The prayer-progress cards become shared components too
The three goal cards, the stat row and their bars were the regions
dashboard's own markup. They are library components now, and the plugin
renders them instead of holding a copy:

  GoalCard      title + figure + a goal line pinned to the card's bottom,
                with a slot for whatever sits between
  StatCard      one figure and its caption
  ProgressBar   a single-value track, or a segmented one with a legend
  useCardSurface  the card surface both cards share, injected once per
                root under a stable label instead of copied into each

The bar and its legend now read from ONE list of segments, so a width and
the number printed beside it come from the same figure and cannot drift
apart; before, each was computed separately. A segment's colour is a prop
carrying the same token its .rd-seg--full/--partial/--none class used to
set, so a shared bar needs no vocabulary about prayer and those three
rules are deleted rather than left unreachable.

ProgressBar carries its own reduced-motion guard. The plugin's reset is
scoped to .rd-root and would not have followed the component anywhere
else, which would have reintroduced the one-second sliding bar their own
site suppresses.

The rendered DOM is unchanged for all four instances, measured by
server-rendering the real components and comparing against the markup
that was deleted, including that the bar widths equal the percentOfSubject()
they replace.

check:regions-uses-library grows to cover all four, asserting both halves
again: the plugin imports and renders them, and no longer ships their
markup or CSS. The .rd-root strong rule STAYS in the plugin -- UupgsList
renders a <strong> in the same shadow root and depends on it, which a
file-scoped read would have called dead.
2026-09-27 04:14:31 +00:00
Doxa Maps
812e1029e4 check:package-json-unique-keys: a duplicated key in package.json fails
JSON.parse keeps the last of two equal keys and says nothing, so a duplicate silently kills the first entry
while every gate that reads the parsed object passes. "check:religion-label-not-value" sat twice in
//scripts after a merge (found by regions-plugin; only Bun's parser warned). A small scanner tracks keys
per object; its self-test runs on every invocation, and a planted duplicate is caught. In gates:pre.
The landing tooling's package.json union now refuses to write a result with a repeated key.
2026-09-27 04:14:31 +00:00
Doxa Maps
c38c66f957 gospel: the tables page's 14 titles and 112 column headers, in 11 locales
gospel.tableTitles.<tab id> and gospel.columns.<English title>, requested by gospel-plugin
(KEY-REQUEST-gospel-tables.md): under ar the tabs were Arabic and every heading and header above the
table was English. Standards stay as they are (ISO 639-3, ISO3, JSON, CSV, ROP1, SUN; a bare "ISO" header
gets no key and keeps its English). JESUS Film uses each locale's existing title; "Heart language" uses
each locale's own-language wording.

Reviewed against the data by gospel-plugin before insertion, which corrected five wrong-sense readings:
ALIGNED (conditions met, not layout), Band (a coverage level), Basis (the level the answer came from),
Code unlookupable (no source indexes it, not an invalid code), Scope (extent). Machine-drafted; not
native-reviewed.
2026-09-27 04:14:31 +00:00
Doxa Maps
be77ce3aa3 messages: the UPG-100 wall intro as three plain parts, in 11 locales
upg100WallIntroA / Emph / B split the one catalogue string that carried markup (a <strong>), which vue-i18n
flags as "Detected HTML" on every mount. Values from upg100-plugin's split, each re-asserted here to
recompose byte for byte (A + <strong>Emph</strong> + B) in every locale; three parts because de, hi and zh
carry text after the emphasis. upg100WallIntro stays until upg100's consumer, which composes the parts and
escapes each, lands.
2026-09-27 04:14:31 +00:00
Doxa Maps
880f5b677e Say where the country-name helper must not be used
It has no consumer yet, and that is deliberate rather than unfinished.
Intl's English names differ from the canonical names in
library/data/countries.json for 42 of 242 countries -- "Antigua & Barbuda"
vs "Antigua and Barbuda", "Cape Verde" vs "Cabo Verde", "Hong Kong SAR
China" vs "Hong Kong", "Myanmar (Burma)" vs "Myanmar".

So it is not a drop-in replacement for a name already on screen. The
regions surfaces prefer the API's localized label on purpose
(region-dashboards.vue:370) and are measured against live doxa.life;
swapping the source would change 42 English names and break clone parity.
Which name is canonical is a product decision, not a refactor.

Its real gap is the case where only an alpha-3 is in hand, or where a
label arrives in English whatever the reader's locale. Recorded in the
file because the failure mode is silent -- the wrong names still look
like names.
2026-09-27 04:14:31 +00:00
Doxa Maps
725467a9de Country names in the reader's language, for all 242
Our data speaks ISO alpha-3. Intl.DisplayNames('region') takes alpha-2 or
UN M49 and NOT alpha-3, so every country name was stuck in English for want
of the join.

Vendored CLDR cldr-core 48.2.2 (Unicode-3.0, 36KB, sha256 and source URL
recorded in library/vendor/cldr/README.md). No npm dependency: a package
would pull the whole supplemental tree and make a shipped data file depend
on an install step, where a vendored copy lets the generator reproduce the
map offline -- which is the point of vendoring rather than merely having a
copy.

generate-iso3-to-alpha2.mjs joins it against the 242 codes in
countries.json and GRADES ITS OWN OUTPUT, writing nothing on failure: 242
coverage, injectivity, two-letter well-formedness, and that each documented
exception is still needed. KOS -> XK is the only exception -- Kosovo has no
ISO 3166-1 assignment, so no refresh will ever supply it, and XK is the
user-assigned code CLDR itself uses in locale ids. It is recorded with its
reason instead of patched into the vendored bytes.

countryName(iso3, locale) falls back localized name -> canonical English
name -> the code. Intl.DisplayNames THROWS on a missing or malformed code
rather than returning null, so the guard is checked before asking; the
probe demonstrates that throw rather than asserting it in a comment. An
unassigned-but-well-formed region returns the input unchanged, so that case
falls back instead of rendering "DE" as a name.

THE RESEARCH SPINE MAP IS NOT TOUCHED. iso3-to-iso2.json is a different
file with a different job -- the 176-country spine that drives the data
fetches -- and expanding it is a Driver decision, not a side effect of
adding country names. The generator cross-checks it and REPORTS
disagreements without rewriting. There are none: CLDR independently agrees
with all 176 values, including the two corrected this session.

Also fixes a duplicate "check:religion-label-not-value" key in
package.json, left by a merge. JSON keeps the last, so one description was
silently dead and check:doc-scripts could not see it; bun's parser warned.
The surviving entry absorbs the only fact the dead one carried, the two
library sites by name.
2026-09-27 04:14:31 +00:00
Doxa Maps
8f79a3af0f Norway gets into the country table at all
NOR was absent from iso3-to-iso2, so Norway appeared in no research table:
consumers filter on ISO3_2[iso3], which makes a missing code a silent
omission rather than an error. Its absence is also why SJM -> NO read as
injective -- "NO" had exactly one claimant, the wrong one.

NOR -> NO, from ISO 3166-1, which is what CLDR's codeMappings encodes. The
full CLDR vendor still comes with the 242-country table and will re-derive
this alongside the other 66 missing codes; this is the one line
exp-shepherd needs first to re-key the mislabelled Svalbard rows onto
Norway.

NOR joins the named spot-check, and the completeness floor rises 175 -> 176
with it. Left at 175 that check would have called a regression back to the
pre-NOR map clean, which is the opposite of what it is for. Removing NOR
now fires both guards; injectivity still reports "all 175 codes map to a
DISTINCT ISO2" while it does, for the third time in this defect.
2026-09-27 04:14:14 +00:00
Doxa Maps
1eae1d8c10 Svalbard gets its own country code
SJM mapped to NO, which is Norway. Every research fetch keyed to Svalbard
and Jan Mayen therefore asked the APIs about Norway. SJM -> SJ.

Injectivity could not see this, and that is the part worth keeping: the
check passes as long as no two ISO3 codes claim the same ISO2, and NOR is
absent from the map entirely, so "NO" had exactly one claimant and read as
clean. A uniqueness test cannot tell a right code from a wrong one nobody
else wanted. So SJM joins the named spot-check beside PSE and HKG, which
were wrong the same way -- mapped to their administrator's code. Reverting
the value now names it; before, the verifier still printed "all 175 codes
map to a DISTINCT ISO2".

The mapping is the half that stops a future fetch being wrong. The rows
already fetched are still Norway's, keyed to Svalbard: 60 network
operators (Telenor Norge AS, GlobalConnect AS, Marlink AS), 15 platform
reachability rows and 8 social-platform rows, in the shipped tables under
public/assets/tables/. Those belong to other plugins, so they are routed
rather than rewritten here.
2026-09-27 04:14:14 +00:00
Doxa Maps
e514dd8544 check:i18n-keys-exist was blind to a translator that arrives from a composable
go-int planted T('poison_key') behind "const T = useGiT()" and this gate reported the tree clean.
That is the one failure a check is not forgiven: a red gate gets fixed, a green gate that should be
red gets believed, and this is the gate that guards goal (d).

Three independent blindnesses were stacked, and any one alone hid the key:
  1. the file never writes the word tStatic, so the precondition skipped it before anything ran;
  2. useGiT is not tStatic, so the per-file alias discovery found no alias at all;
  3. the alias is PREFIXED, because useOptionalT carries a namespace, while its call sites pass BARE
     keys -- which the full-key pattern cannot match, because it requires a dot.

A translator bound from a composable is now resolved like any other, and the namespace is READ from
the factory rather than listed here -- useOptionalT and useOptionalI18n directly, plus any
"export function useXxxT() { return useOptionalT('ns') }", which is how useNeighborT and usePplrT
are written. That keeps the next author's factory covered the day they write it, which is the same
reason the wrapper NAMES were already read per file instead of hardcoded. Because qualify() prefixes
a bare key and leaves an already-qualified one alone, one binding registers as both shapes; the two
patterns are disjoint, so nothing is counted twice.

MEASURED before and after, with the same poison on disk: the previous version PASSES it, this one
fails it as "neighbor.zzz_not_a_real_key (via nt())". Coverage went 683 -> 743 full-key and 49 -> 56
prefixed call sites, so 67 call sites were being scored as clean without being read. The tree itself
is still clean: no real key was hiding behind those 67, so there is nothing to route to any owner.

A self-test asserts all three fixes SEPARATELY and exits 3 if any one goes blind, because testing
them together would not say which was fixed -- and because this gate had already been trusted once
while lying.

Empty-namespace bare keys stay outside the model and are COUNTED rather than assumed away. Measured
at zero today: all five such sites pass fully dotted keys, so every one is checked by the full-key
pass. The counter stays because the shape is writable.

Gate and description only. No product code, no rebuild.
2026-09-27 04:14:14 +00:00
Doxa Maps
7e04a94c34 Kit landing 4c-3: the facet rail follows the chip bar into the library
The rail's rendering is unchanged. What changed is where its last two plugin imports point. 4b had
already removed the research COLOUR and LABEL tables -- those stay in plugin-designs and arrive on
dashboardConfig.legend -- which left exactly two, and both now read the module that OWNS the
function rather than a re-export:
  useDashboardAggregations -> @map/composables/useFacetAggregation.js   (extracted by 4c-1)
  useDashboardShell        -> @map/composables/useDashboardSearch.js    (extracted by 4a)
Pointing at the shim instead would have been invisible and wrong in a way this tree has already
measured: check:filter-multivalue carries the note from landing 3 that a gate left at the plugin path
read the shim, found no predicate to object to, and PASSED VACUOUSLY.

The dead injects go too. `dataStore` and `searchSourceId` were here only to construct
useTreeLegendColors and useTreeLegendLabels, and 4b removed both constructions. An unused inject is a
claim about the host: a kit component that appears to need a research dataStore is one a new
dashboard will dutifully provide before finding out it was never read. The chip bar shed the same
pair in 4c-2.

check:filter-multivalue and check:legend-mirrors follow the file rather than being left pointing at a
path that no longer exists -- the multivalue gate fails loudly on a missing panel, which is what it
is for, and it is how I would have caught forgetting.

UNLIKE THE CHIP BAR, THIS ONE IS ON SCREEN. 4c-2's component is mounted by nobody, so its gate was
the whole story. The rail is mounted by research-dashboard.vue and is 770 lines of tree, search and
drag behaviour: "the imports resolve" says nothing about whether it still opens four trees by
default or whether its search still narrows. It needs eyes.

The barrel re-exports from the library for one cycle, so no importer's line changes in the same
landing that moves the file. documentation/reference-library-index.md regenerated.
2026-09-27 04:14:14 +00:00
Doxa Maps
d54ece3b25 Kit landing 4c-2: the chip bar moves into the library, and its unconfigured default gets a voice
DashboardFilterChips.vue moves to library/components/dashboards/. Nothing about the rendering
changed. What made the move possible was 4b, which inverted the one dependency that blocked it: this
file imported useTreeLegendLabels — the People CODE -> NAME tables, research-specific and staying in
plugin-designs — and a library component importing a plugin composable is what
check:no-cross-plugin-imports refuses. The resolver now arrives on dashboardConfig.legend.

THREE THINGS BEYOND THE MOVE, all found by assessing the file rather than by moving it:

1. THE UNCONFIGURED DEFAULT NOW JOINS A LIST. 4b's fallback returned the value untouched, so a
   multi-value filter (spec D1) printed String(['A012','A013']) = "A012,A013", comma-jammed, while a
   dashboard that DID supply a resolver got "South Asian Peoples, Bengali Peoples". Configured and
   unconfigured should differ in whether CODES RESOLVE, never in punctuation — the second difference
   reads as a rendering bug rather than as missing configuration, and go-int is the consumer who will
   see the unconfigured state first.

2. THE DEAD INJECTS ARE GONE. `dataStore` and `searchSourceId` existed only to construct
   useTreeLegendLabels and have been unused since 4b. Removed rather than left in place: an unused
   inject is a claim about the host, and a kit component that appears to need a research dataStore is
   one a new dashboard will dutifully provide before discovering it is never read.

3. AN EXPLICIT EXCEPTION TO library/components/WARNING.md, recorded in the file's own header. That
   file says a component graduates into the library only once MORE THAN ONE map uses it; this one is
   used by ZERO, because the topbar dedup pass unmounted it when SearchFilterHeader became the single
   source of truth for showing and clearing filters. It is here because the kit's promise runs the
   other way round — a NEW dashboard mounts kit components, so waiting for a second consumer before
   providing the first one is a deadlock. The honest consequence is written down too: a passing gate
   means "it mounts and degrades", not "it looks right", and nobody has SEEN this component since the
   dedup. Whoever wires it in is its first real reader, and must not mount it beside a
   SearchFilterHeader that is already showing chips.

check:legend-mirrors grows arm 5, and it asserts the two things a "move" gets wrong that reading the
new file cannot show: that the OLD copy is actually gone (a move that leaves the original behind is a
copy, and two chip bars over one filterSet drift the moment one is edited — this tree has already
paid that with useNeighborT/usePplrT at Jaccard 0.82), and that the UNCONFIGURED behaviour is right,
which no dashboard here would reveal because they all supply a resolver. The default is LIFTED out of
the source and RUN, the way arm 3 drives the rail's fallbacks — not mounted, because a no-op renderer
cannot show a chip and the rendered result is rega's. Five mutants, all caught: old copy left behind,
the join reverted to 4b's bare value, a dead inject re-added, the supplied resolver ignored, and a
relative import restored.

The barrel re-exports from the library for one cycle, so no consumer's import line changes in the
same landing that moves the file. documentation/reference-library-index.md regenerated, per landing.

PARENTED ON a285e06c (4b), not on main, because 4b edits this same file — landing them the other way
round would revert 4b's inversion. Source only: research-dashboard and dashboard-plus-grid need a
rebuild.
2026-09-27 04:14:14 +00:00
Doxa Maps
25c102f5cd Kit 4c-1: the counting half, and the bucket that said "Other" in every locale
useDashboardAggregations has a split personality, and only half of it could ever
move. `lbl` and `aggregate` are pure counting — eleven lines, a Map, a sort, a
topN rollup, no facet name and no colour. Everything else in that file is
research: `colorFor` with its hardcoded religion / wagf_region / affinity_bloc
branches, the religion palettes, the donut and bar builders. Those stay, and
DashboardChartsRail keeps importing them.

The generic pair was what blocked kit landing 4c: the facet rail needs only that
half, and while it lived beside the palettes the rail could not follow it into the
library. Same split, same one-cycle re-export as 4a. Bodies verbatim.

★ AND THE ROLLUP BUCKET SAID "Other" IN EVERY LOCALE. aggregate() pushes
`{ key: 'Other', other: true }` for the tail, and that English word was rendered
directly by the facet rail and the charts rail. check:i18n-keys-exist could not
see it, because there was no T() call to inspect — the string was never a lookup,
it was a KEY being used as a LABEL. I found it while ASSESSING this landing rather
than while doing it, and it is fixed here rather than later for one reason: the
moment this module becomes kit code, an unfixed bug in it is exported to every
dashboard built from the kit.

THE TWO HALVES PULL IN OPPOSITE DIRECTIONS, which is the whole difficulty. The KEY
must stay English: the rail tests it to make the row inert, and the charts rail
refuses to filter on it, so a localized key would make the bucket CLICKABLE in
Arabic and filter on a value no row carries — an empty board, reading as a data
problem. The LABEL must not stay English. So OTHER_KEY is the sentinel,
otherLabel() reads dashboard.other, and they live next to the function that
produces the bucket so they cannot drift. Eleven locales, catalogue first.

This is the value/label trap for the third time — religion families, the language
tiers, and now a bucket in generic code. The third is the worst place for it,
which is why the rule now travels WITH the code that creates the value rather than
being remembered at each render site.

check:legend-mirrors grows arm 4, asserting both directions: translate the
sentinel and it fails; render the key instead of the label and it fails. Both
mutants run.

check-one-palette now scans the new module as well. Its subject — private palette
declarations — stayed in the plugin half, so nothing moved; adding the library
path means a palette smuggled into kit code later is caught by the same rule
instead of escaping into the library.

Library index regenerated in-commit. Source only. STUDIO: needs a rebuild of
research-dashboard and dashboard-plus-grid. The internal/ hunk is four lines in
check-one-palette.mjs.
2026-09-27 04:14:13 +00:00
Doxa Maps
22dc3ed770 Kit landing 4b: the rail stops reaching for research colours
The facet rail and the chip bar imported useTreeLegendColors and
useTreeLegendLabels — the DOXA palettes and the People code tables. Those are
research-specific by this kit's own reckoning and they STAY in plugin-designs, so
that import is precisely what stopped the rail from being able to move into the
kit at all: a library component importing a plugin composable is what
check:no-cross-plugin-imports refuses, and rightly.

So the dependency inverts rather than the file moving. The panels declare what
they need — two functions — and the SHELL supplies them as
dashboardConfig.legend. The shell is the right supplier because it is the only
thing that already holds the config, the dataStore and the source id those
composables require, and because a composable cannot be called at config-module
scope.

ON dashboardConfig, NOT A NEW INJECT KEY, because section 5.1 of the spec says a
panel needing a new capability gets a new CONFIG key and never a new channel. It
would have been easy to add a seventh inject key and quietly exempt myself from a
rule I wrote; the rule is better than the exception. Merged rather than assigned,
so a profile supplying its own legend wins.

THE NO-LEGEND STATE IS HONEST, NOT A CRASH. A dashboard that supplies nothing
gets uncoloured swatches and raw values — which is what go-int sees before it
wires its palette, and a half-configured dashboard should look PLAIN rather than
broken. The kit's neutral is its OWN constant and deliberately not the research
palette's NEUTRAL, which happens to hold the same value today: importing it to
save a literal would re-create the dependency this landing removes, and the
research palette must stay free to change its own neutral without reaching in
here.

Big Picture is unchanged because the fallbacks fire only when legend is ABSENT,
and for Big Picture the shell always supplies it. Every hex still comes from the
same strategy modules the map's pins paint with, so the legend still cannot drift
from the map — it is handed the strategies instead of fetching them.

check:legend-mirrors grows arm 3: the research imports are gone, cfg.legend is
read, and the two no-legend fallbacks are LIFTED OUT OF THE SFC AND DRIVEN, so
"neutral swatch and raw value" is measured rather than asserted. Three mutants,
all caught. What it deliberately does NOT do is mount the component — a no-op
renderer cannot show a colour, so a mount would prove only "it did not throw" at
the cost of a dev server in a gate that runs in milliseconds. The rendered result
is rega's.

★ AND MY OWN CHECK FAILED ON MY OWN COMMENT, FOR THE FIFTH TIME. The comment
beside the kit's neutral explains the decision by NAMING the research NEUTRAL, so
testing raw source reported a coupling that does not exist. I had already fixed
that shape four times in other gates and still wrote the raw-source version here,
which says the fix belongs in a habit rather than in each gate: every text search
over source strips comments first, without thinking about it.

Source only. STUDIO: needs a rebuild of research-dashboard and dashboard-plus-grid.
2026-09-27 04:14:13 +00:00
Doxa Maps
281f9d1def The search chip was the one chip still in English, and the string for it already existed
DashboardResearchFilters builds the :active-chips array the live strip renders, and its search chip
read `Search: ${term}` — a raw English label sitting beside six localized facet chips in all ten
non-English locales. dashboard.search_live was already in the catalogue, in all eleven locales,
written for that exact line and never wired.

The punctuation is why it has to come from the catalogue rather than be assembled here: French is
"Recherche : {term}", with a narrow space before the colon, and Chinese is "搜索:{term}" with a
full-width one. Translating the word and composing the colon in code gets both of those wrong even
with a correct translation. tStatic takes no params, so {term} is filled at the call site.

Also closes a LATENT gap beside it: an array value is a multi-value filter (D1), labelFor() joins one
for display at its single owner, but the prayer arm skips labelFor deliberately — its values are
already human labels — so it skipped the join too and a two-value pick would have read "A,B" with no
spacing. Nothing in this plugin calls toggleFilterValue, so no caller produces a prayer array today;
it is closed because the next caller that does would not think to look in a chip label for the reason.

check:dashboard-filter-a11y gains arm A13, and the arm is the part worth reading. Everything else in
that gate checks the chips where they are RENDERED; neither of those files is where the WORDS come
from, which is how an English label survived a gate written for this strip. A13 reads the computed
that builds the labels and allows an English literal only as the fallback of a catalogue read.

It also took two rounds, both caught by negative-testing rather than by review:
  1. it flagged '__search__', the chip's internal name — reporting an identifier as untranslated
     prose, which is the crying-wolf failure arriving from the other direction. Now scoped to the
     `label:` expression, by brace-balanced scan rather than by line.
  2. it then PASSED the real defect, because the English was in a TEMPLATE literal and the arm only
     read '…' strings. A chip label is exactly where a template literal is natural, so the one shape
     the defect actually takes was the one shape the arm could not see.
Verified failing on the restored defect ("Search: ") and passing on the fix.
2026-09-27 04:14:13 +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
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
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
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
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