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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
A fourth row in the Dashboards section (after Big Picture, Simple Research
and Regions), mounting <gospel-internet-board> from its own bundle like
every other tab, with its label and subtitle in all 11 locales. It leaves
the Experimental shelf, which only lists bundles no section names.
The language tree's source (Glottolog 5.2.1, CC BY 4.0) joins the board's
standing sources line and the CSV provenance, read from the snapshot's own
citation.
Every language family of the board's people groups, drawn as a radial tree
(family, branches, languages, dialects) on a whiteboard, the families
packed like cells in a dish with no overlaps. Green: a New Testament or
full Bible found in the language; yellow: something found but no New
Testament; red: nothing found. A family shows the worst colour inside it;
a dialect carries its language's colour, lighter, because every source
catalogues resources by language. Click a circle to narrow the board to
it. The table under the centre gains a Gospel resources tab listing every
screened link for the languages in scope.
The tree is Glottolog 5.2.1 (CC BY 4.0), snapshotted to the languoids the
roster needs: 833 of 864 codes, the 31 IMB private codes kept under their
own node. The layout is computed at build time and drawn by the same map
engine the board already loads, so zooming out draws less and labels
never overlap.
check:gospel-internet now also measures the tree (complete, coloured by
its rule, no overlapping families) and server-renders every panel with the
real data in en and ar: profiles-mount never renders a profile's children,
and a throw planted in a panel passed 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.
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.
go-int found both by reading the kit before importing it. Both are in landed code
and both reach Big Picture in Arabic, not only a new consumer.
THE DRAG HAD NO READING DIRECTION. facetW and railW were computed from
(clientX - startX), written once, in LTR. Under dir=rtl the grid mirrors — the
facet rail sits on the RIGHT — so dragging its handle toward the map moves clientX
the other way, and in Arabic the facet handle shrank the CHARTS rail. Both handles
fight and neither does what the cursor says. The delta now carries a sign read from
the computed direction per drag, not cached: a host can flip dir between gestures,
because our own composites set it on the custom-element host from the runtime
locale.
The ARROW KEYS take the same sign. If only the pointer mirrored, the two input
methods would disagree about which way widens — and aria-valuenow reports the same
ref for both, so the announced value would move opposite to the key for someone
who cannot see the drag.
AND THE LEGACY READ-THROUGH WAS MINE, FROM KIT LANDING 2. I added it so
namespacing would not discard a reader's saved layout, and applied it
unconditionally. So a caller passing its OWN storageKey — which is the entire point
of namespacing — still inherited Big Picture's rail widths on first load. The
migration written to protect one dashboard's layout was handing that layout to
every other dashboard. It is gated on the default key now, and the default is a
named constant rather than a repeated literal, because a fallback comparing against
a copy of its own default stops matching the day someone bumps the version suffix.
check:layout-key-namespaced grows arms D and E. D asserts BOTH widths take the sign
and that the arrows share it — mirroring one and not the other is worse than
mirroring neither, because then the handles disagree with each other. E asserts the
legacy read is gated. Comments are stripped before both assertions, for the reason
that is now written down four times over.
Source only. STUDIO: needs a rebuild of research-dashboard and dashboard-plus-grid.
NEEDS EYES: the drag is a gesture, so this wants rega in ar — drag the facet handle
toward the map and confirm the FACET rail follows the cursor and the charts rail
does not move, then the same with ArrowLeft/ArrowRight.
★ LANDING 4 AS I SCOPED IT IS NOT POSSIBLE, AND THE SPEC WAS WRONG ABOUT THAT.
go-int asked for the facet rail next and I agreed. Then the first step of a
landing — look before moving — found that DashboardFacetRail imports FOUR
plugin-local modules: useDashboardAggregations, useDashboardShell,
useTreeLegendColors and useTreeLegendLabels. Moving the rail into library/ would
have made a library component import plugin composables, which
check:no-cross-plugin-imports rightly refuses. And two of those four are
research-specific by my own spec's reckoning — useTreeLegendColors carries the
DOXA colour strategies and is supposed to STAY in the plugin — so the rail cannot
become library code by moving alone. Its colour and label dependencies have to be
inverted into injection first, which section 8 named as a risk and then ordered as
if it were free.
So landing 4 is three commits, not one, and this is the first: the piece the rail
stands on.
THE SEARCH WAS NEVER A SHELL CONCERN. buildSearchAccessors, makeSearchMatcher and
filterRowsBySearch were exported from useDashboardShell, and EIGHT files reached
into the shell to get them — the table, the map panel, the charts rail, the facet
rail, the research filters, the top bar, the profile and the config. A module that
eight things import is a module, not a corner of another one. Nothing in it is
domain-specific: the accessors come from config.facets / config.columns /
config.searchKeys, and the matching comes from the already-shared fuzzy.js.
Bodies are verbatim; this is a move. The shell re-exports the three names for one
cycle, so none of the eight importers changes in this commit — a barrel line
rather than a shim file, so there is nothing to delete later, only a line to
remove when the eight point at the library directly.
The now-dead fuzzy.js import is removed from the shell rather than left. An import
nothing uses is the kind of thing that survives three more landings because it
looks load-bearing.
WHAT THIS DELIBERATELY DOES NOT TOUCH: the shell keeps rowMatchesFilters and the
URL-hash escape helpers, which are what check:filter-multivalue reads out of it by
path. I checked that before slicing, so no gate path moves in this commit.
Library index regenerated in-commit. Source only; no rebuild needed beyond the
usual, since every importer still resolves through the shell.
dx-shepherd's ruling: the locale-honouring fallback everywhere, carrying the
Driver's goal (d) — a bare-host embed with ?lang=ar must render Arabic — now
recorded in COMMON.md as the embed contract.
all-people-groups passed its inline EN map as the fallback, which served ENGLISH to
exactly the hosts that cannot install vue-i18n: the seven bundles here that install
none. It now takes the catalogue default, so those readers get their own language
with English still the last resort. The EN map is deleted — with the catalogue
complete it could rescue nothing, and a second copy of the English is only a place
for the two to drift.
THE RULING'S CONDITION WAS MEASURED BEFORE THE SWITCH, not after: all eleven keys
exist in all eleven locales, and ar differs from en on 11 of 11, so the change is
observable rather than cosmetic.
★ AND THE CONDITION IS NOW KEPT TRUE RATHER THAN MEASURED ONCE. A hand census
protects the day it is taken. Section 9 of check:optional-t-equivalence re-derives
it every run, because with no local fallback an uncatalogued key renders as a raw
dotted path to a reader — the exact failure upg100 described, and the reason they
had advised keeping a fallback at all.
★ THAT CENSUS UNDER-COUNTED TWICE BEFORE IT WAS RIGHT, both caught by checking
instead of assuming:
- literal tr('key') extraction found 6 of 11. Four keys are category ids reached
only through tr(cat.id) / tr(c.id); the census now extracts the CATS ids too, and
asserts the dynamic call sites still look the way it assumes.
- the eleventh, 'pray', was lost to codeOnly: it sits in a template literal whose
`https://` was read as a line comment, because the scanner had already lost
template state further up a 1000-line SFC. The census now reads RAW source.
The asymmetry is the point — over-including a commented-out key makes the check
STRICTER (a cheap false red); under-including a real one makes it claim
completeness it does not have. strip-code.mjs carries that rule now, so the next
author picks raw or stripped for a reason rather than by habit.
My half of PILLRISK (studio's PinHint half is 2e375530). `inert` on every pane except the active one,
which is the signal studio's contract asked for (END-STU-PILL / UPGINERT).
★ PinHint'S OBSERVER WAS NOT BROKEN, AND THAT IS THE WHOLE DIAGNOSIS. The panes are
`position:absolute; inset:0` in one scroll-snap column, so the map pane sits STACKED BEHIND the active
view at full size: 100% of its geometry is inside the viewport while none of it is visible. An
IntersectionObserver measures INTERSECTION, not OCCLUSION, and no tuning can separate those two states
— they are identical to it. The missing information was never geometric. Only the composite knows which
pane the reader chose, so only the composite can say it.
⚠ inert, NOT display:none or v-show, and this constraint decides the approach. Hiding the pane would
tear down a live map: a MapLibre/Mapbox canvas in a zero-size container loses its size and
re-initialises on the way back — the cold-start path rega had to verify separately in row 52. inert
leaves the pane mounted, painted and warm and only declares it non-interactive; unlike aria-hidden it
also removes it from tab order and the a11y tree, so a keyboard user cannot tab into a map they cannot
see.
ALL FOUR PATHS THAT CHANGE THE VIEW ARE COVERED, which took reading rather than guessing:
- FRESH LOAD — marked in onMounted from column.current(). This is the case the Driver actually hit:
without it no pane carries the attribute until the reader touches the switcher, so the pill is over
the posters on ARRIVAL. A signal that only fires on transitions never describes the initial state.
- THE SWITCHER — marked inside go(), immediately rather than via the scroll callback, because
scrollTo is SMOOTH and onScroll debounces 120ms: waiting would leave the pill over the posters for
the length of the animation, a shorter version of the same bug.
- A DRAG/SCROLL — marked in onScroll, UNCONDITIONALLY rather than inside the existing
`cur !== view.value` guard. go() has already set view.value by then, so a switcher-driven scroll
takes that early exit and would skip the marking; a scroll-driven one needs it, and a
switcher-driven one is idempotent.
- "Show on map" from the poster wall — routes through go(), so it inherits it.
Plugin-only: upg-100-mfe.vue, no library change. mountEl(paneId) already exposes the container, so the
attribute goes on from my side of the fence.
Correct at EVERY viewport, not just the phone: pane-wrapper renders ONE column with
`scroll-snap-type: y mandatory` and `.pane { height:100% }`, so exactly one pane fills the stage at any
width and there is no v1 side-by-side mode whose second pane would be wrongly marked (pane-wrapper.js:56-59,
:101 — read, not inferred from the phone report).
check:sfc-parse PASS, check:no-free-identifiers PASS. The one-pane-non-inert invariant is proven across
switches including the return trip; the browser assertion is rega's (fresh load: no pill -> Map: pill ->
Prayer Resources: no pill).
rega's row 56b at main 4ca2dcf9 measured Simple Research and Prayer Progress under ?lang=ar with NO
override at all: their tree-legend rows compute direction rtl and their counts keep digit order. So the
library legend is RTL on its own and these two rules are mine to remove (dx-shepherd UPGDEL, studio
END-STU-UPG-56B). Rule 1, .upg-detail-panel, stays — that element is mine, not the library's.
★ BUT THE TWO WERE NOT EQUALLY PROVISIONAL, and the difference is the point of this commit:
'[dir="rtl"] .stl-panel,[dir="rtl"] .stl-panel *{direction:rtl}' was REDUNDANT.
It matched real elements — .stl-panel appears 69 times in SemanticTreeLegend — but the library reaches
the same state without it, using the :dir(rtl) PSEUDO-CLASS (:645-646), which reads the INHERITED
direction instead of needing a declaration. Two other profiles carrying nothing prove it.
'...stl-count,...stl-pop{direction:ltr;unicode-bidi:isolate}' was DEAD CSS, and always was.
The library's count and population cells are `.stl-num` (:1314-1315). `.stl-count` and `.stl-pop` exist
NOWHERE in library/ or any other plugin — git grep finds them only inside my own override. The rule
matched zero elements from the day I wrote it. The digit order rega verified was the library's doing and
never mine, and I had cited this rule as the reason it worked.
★ THE LESSON IS ABOUT EVIDENCE, NOT CSS. This rule was written against a real bidi problem, believed to
have fixed it, and quoted as the fix in a commit message and a card — while matching nothing. A style
rule fails SILENTLY when its selector is wrong: no warning, and the page looks correct if something else
already handles it. Checking that a selector EXISTS is a different act from looking at the rendered page,
and only the first one can tell those two states apart. Both are recorded in the source so nobody
re-adds the dead rule believing it once worked.
check:sfc-parse PASS.
REGREL. Two library sites rendered RELIGION_FAMILIES values as English in every
locale: the Religion legend's rows (useLegendData.js) and the search aggregate that
collapses sub-types into their family (useSearch.js). Both now project the LABEL
through religionFamilyLabel(), which resolves through the catalogue and keeps
English as the last resort.
The half worth a gate is the half that did NOT change. A translated value matches
nothing — the moment a localized string lands in a filter, a bus payload or an
element attribute, the link works in English and fails silently in Arabic. So `key`
and `filterKey` stay the canonical family letter, useSearch's key function stays
bare getReligionFamily(), and the check asserts all of that as well as the
projection. RELIGION_FAMILIES is no longer imported in either file: the label was
its only local use, and religionFamilyLabel reads the canonical map internally.
★ A COMMENT IN useSearch.js ARGUED AGAINST THIS FIX, and it was wrong. It read
"label = family name so selection can resolve the family letter back (see
research-map onGeocoderAggregateResult religion branch)" — presenting an English
label as a REQUIREMENT of selection. Measured: onGeocoderAggregateResult does not
exist anywhere in this repo (three comments name it, nothing defines it), and
selection resolves by value — aggregate() emits `key` and `label` as separate
fields, and the consumer reads detail.expectedValue (countries-map.vue:1611).
Retracted in place with the measurement, not deleted, because a reader who has seen
the old claim needs to know it was tested.
check:religion-label-not-value is registered and negative-tested. It executes
religionFamilyLabel's REAL source against the SHIPPED ar/ and en/ religion.json
rather than a stub, because religion.js imports tStatic through an i18n index that
calls import.meta.glob and cannot load in plain Node. Under ar the label is
المسيحية while RELIGION_FAMILIES.C is still "Christianity", and a control asserts
the two really differ — an English-only projection fails that assertion.
One assertion of mine failed on its own retraction first: it required the stale
phrase to be ABSENT, but the corrected comment quotes it in order to record it.
Requiring the absence of a phrase punishes documenting the error, so it now asserts
the retraction is present instead.
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.
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.
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.
★ THE TWO INSTRUCTIONS I WAS GIVEN CONTRADICTED EACH OTHER ON ONE LINE, so this records how it was
resolved rather than leaving the next reader to guess. UPGHK said delete humanizeKey; UPGW3 said merge
1bd5f109 and "keep regions' version" — and regions' version KEEPS humanizeKey
(`useOptionalT('', { fallback: (k) => humanizeKey(k) })`). Read as structure-vs-fallback they agree:
UPGHK's reasoning is about what a reader SEES ("a real English or translated string beats a title-cased
key segment"), so "keep regions' version" is about the SHAPE of the call. Kept theirs in every respect
but the fallback:
- useOptionalT('') with the EMPTY namespace, theirs, not my 'messages'/'tabs'. Verified to be a
behavioural no-op here (every key this code passes is already fully qualified, so qualify() returns
it unchanged either way) — and '' is the more honest of two identical options, because it says
"these keys arrive qualified" instead of implying a prefix is being added.
- their "one behaviour gain" note, which is a real gain I had not documented: the old path was the
bare useI18n().t, so on a host WITH i18n a missing key rendered the raw dotted key.
- the fallback deleted, per UPGHK.
AND THE REGISTER KEEPS BOTH WAVES. Their side of the conflict added a wave-3d row for
all-people-groups that mine would have dropped — it stays, with its own reading intact, which is the
OPPOSITE of the call taken for my two files (its inline EN map stays because the file is UNOWNED and an
unowned file is the wrong place to make a product decision by default). Both readings are right for
their own file; that is the point.
⚠ AND I FIXED A FALSE SENTENCE THE GATE WAS ABOUT TO PRINT. With a null third column the register said
"takes the CATALOGUE fallback, as it always did" — true for the two composables that always did, false
for my two, which used humanizeKey until today. That branch already existed to stop exactly this for
gospel's four; my files needed the same treatment, so the "moved" list now names both owners and says
what each moved FROM. A register can be right about its assertion and still teach the next reader
something untrue in the line it prints, and a gate message is the one place a reader has no reason to
doubt.
Verified: check:optional-t-equivalence PASS on all sites, including gospel's unchanged message.
Both files hand-rolled the same guard — useI18n() in a try/catch, falling back to humanizeKey() — and
both now route through library's useOptionalT (host i18n -> tStatic -> the key). The consolidation is
incidental; deleting the FALLBACK is the product change, and dx-shepherd took it (UPGHK) under the
Driver's standing goal that the production maps be localized.
WHAT humanizeKey ACTUALLY DID, measured against the catalogues rather than reasoned about:
messages.upg100WallIntro "Upg100 Wall Intro" -> "Hand away the prayer task in clusters of …"
messages.upg100WallHeading "Upg100 Wall Heading" -> "My UPG 100 List — Prayer Lists & Posters"
tabs.selectView "Select View" -> "Select View" (en) / "दृश्य चुनें" (hi)
tabs.prayerResources "Prayer Resources" -> "Prayer Resources" (en) / "प्रार्थना संसाधन" (hi)
Two different defects, and the second is why this survived so long. On the messages keys it printed
GARBAGE — a title-cased identifier where a whole sentence exists, wrong even in English. On the tabs
keys it printed the RIGHT English by coincidence, because the key was named after the word, so it
looked correct forever while every non-English reader silently got English. A fallback that happens to
agree with en is indistinguishable from a working translation until someone changes locale.
★ AND I HAD THE GUARD'S PURPOSE WRONG, which is the part worth recording. I told regions-plugin to keep
humanizeKey because "my wall/poster keys are not fully catalogued". Enumerated: this is SEVEN keys
total across both files, all string literals, no computed keys, and every one present in all ELEVEN
locales — 0 gaps. The premise was false; I asserted a catalogue state from memory and presented it as a
measurement. humanizeKey never guarded a MISSING KEY at all, it guarded a host that never installed
vue-i18n (useI18n() throws; seven bundles here install none). Those are different failures, and
useOptionalT's own header says so. Correction sent to regions, who has unwound the register row.
Verified: check:sfc-parse PASS, check:no-free-identifiers PASS across 128 SFCs (which is what proves
useOptionalT resolves and no orphaned useI18n reference is left behind), check:i18n-keys-exist PASS.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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).
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.
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.
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.
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).
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.
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.