maplibre-gl@5 and maplibre-gl-geocoder@1 were floating ranges, and no runtime loader set `integrity`,
so a CDN could serve any bytes to every partner's visitors unnoticed (CRITIC-5).
- library/constants/cdn.js is the one registry: mapbox-gl 3.24.0, maplibre-gl 5.24.0 (the release the
@5 range already resolved to), both geocoders (1.9.4 / 5.0.0) and d3 7.9.0, each with its sha384.
Every host answers Access-Control-Allow-Origin: *, so integrity + crossorigin work cross-origin.
- ensureEngineScript / ensureEngineCSS, the geocoder loaders (library and PPLR) and the generator's
static MapLibre tags set integrity + crossorigin from it; the embed snippet's engine preload matches.
- check:cdn-pins (gates:post): no floating CDN url in shipped code, and every one is in the registry;
--online re-fetches each and compares the hash (all 9 match today).
MapLibre stays on 5.x knowingly. GHSA-jrc7-96c5-q579 (DOM.sanitize, reached through a source's
attribution string) affects <= 6.4.0, and 6.x is ESM-only (no UMD global), so the upgrade is a loader
migration of its own. The attribution strings rendered today come from our configured basemap styles
and our own sources; the note in cdn.js says who is exposed until then.
utilities/_typography.scss gives .font-size-4xl a clamp:
font-size: clamp(var(--font-size-2xl), 6vw, var(--font-size-4xl))
and ours was a flat var(--font-size-4xl). Identical at 1024 and above, where both
sit on the ceiling, but below that ours never shrank. Their resolved values against
ours:
390px theirs 38.15 ours 59.60 ours 21.46 too large
768px theirs 46.08 ours 59.60 ours 13.52 too large
1024px+ theirs 59.60 ours 59.60 identical
★ I REPORTED THIS AS UNCHANGEABLE ONE COMMIT AGO, AND THAT WAS BACKWARDS. I said it
interacted with the documented nowrap decision — a figure must never split across
lines in a 372px card, which is why the flat value was chosen. But the clamp makes
the figure SMALLER exactly where the wrap was observed: in a 372px container 6cqw is
22.3px, below the 2xl floor, so it resolves to 38.15px against the 59.60px that was
breaking. The clamp helps the very problem the flat value was picked to avoid. I had
assumed a conflict instead of computing it, and one line of arithmetic settled it.
The nowrap stays as a belt.
cqw rather than vw, consistent with every other clamp in this element (the
container-vs-viewport substitution the Driver is still ruling on). At 768 that lands
42.4 against their 46.08 — 3.68 off instead of 13.52.
_chk-clamp-floors-at-phone-and-tablet.mjs picks the new clamp up on its own: 14
clamps now, and 12/12 token-bounded ones still pin to their floor at 390, so the
"exact parity at phone width, provable without a container measurement" result now
covers the big number too.
THE EIGHTH GLOBAL RULE OF THEIRS FOUND MISSING, and the same class as the
box-sizing reset: _globals.scss:14 is `body { line-height: var(--line-height-normal) }`
with $line-height-normal: 1.5, and a document-level rule does not cross a shadow
boundary. dxlife-tokens.js declared every spacing and font token and NO line-height
at all, so every text element in the clone inherited whatever the EMBEDDING PAGE
set, while theirs is deterministically 1.5.
.rd-big carried line-height:1.1 to compensate. At --font-size-4xl (59.6046px) that
is a 65.6px line box against their 89.4px — a 23.8px difference. rega measured the
consequence to the pixel: our goal cards were 24px shorter than theirs (571 vs 595),
their big number sitting ~12px lower and its label ~24px lower, the extra leading
splitting roughly half above and half below the glyphs. Theirs is
`<p class="font-size-4xl font-weight-medium">` and the .font-size-4xl utility sets
ONLY a font-size, so their number simply inherits body's 1.5.
So :host now carries their line-height and its tokens, and .rd-big's override is
gone. Their own tighter cases stay explicit and already agreed: .region-stat__value
is line-height:1 and so is our .rd-stat__value.
★ I APPLIED THIS HALF-WAY FIRST. The :host patch failed on a string mismatch while
the .rd-big patch succeeded, which left the number's line box inheriting from the
HOST PAGE — worse than the wrong-but-deterministic 1.1 it replaced. Caught because
the two patches reported separately instead of as one result.
CORRECTION TO 9009dd62, owed and measured against me: that commit claimed our goal
line "floated at three different heights" where theirs bottom-aligned. rega measured
both sides — the goal line sits 42px above the card's bottom edge on BOTH, in all
three cards, with identical gaps to the element above (0 / 73 / 120px). So margin:0
already reproduced their margin-top:auto at 1440, and the visible defect I described
did not exist. The change stands as a verbatim port of their declaration (RULE 1),
and it matters only where a card has free space — but I asserted a consequence I had
not measured, and the measurement says otherwise.
STILL DIFFERENT, reported not changed: their .font-size-4xl utility is
`clamp(var(--font-size-2xl), 6vw, var(--font-size-4xl))` where ours is a flat
var(--font-size-4xl). Identical at 1440, divergent below — at 768 theirs is 46.08px
and ours stays 59.6px. It interacts with the documented nowrap decision (a figure
must never split across lines in a 372px card), so it goes out for measurement
rather than into this commit.
Drawn verification is rega's: card heights 571 -> 595 and the two gaps around the
big number. A base line-height touches every text element in the element, and I
cannot measure layout headlessly.
Two rounds of diagnosis went on guessing which step failed, and both guesses were
wrong in the same direction: I assumed delivery.
rega's timeline settled delivery. On main 031b9b38: all three elements exist at
t=893, the map pane's legend rows are populated at t=1017, the click lands at
t=4054, the select-zone attribute is written at t=4106, and the listener first
appears at t=4763. So the attribute IS written, and main ALREADY reads it at
registration (upg-100-list.vue:2140). A headless test then ruled out the lookup:
buildLegendNodes over the real clusters.json yields 37 keys and
findLegendNode('asia-india-rajasthan') resolves to "Northwest India".
So the attribute is read, the node IS found, onLegendSelect IS called -- and
nothing appears. The failure is DOWNSTREAM of the apply, and it is invisible:
nothing throws, nothing logs, and "selected but not visibly selected" is
indistinguishable at the rendered layer from "never selected". That is the third
time this session that a defect has hidden in that gap.
So the apply now writes its own outcome onto the host, where rega's existing 50 ms
poll can read it with no CDP and no new instrument:
data-select-zone-state = applied | applied-but-no-selection | unresolved | cleared
data-select-zone-applied = <the zone id>
'applied-but-no-selection' is the discriminating case: the path was reached and the
EFFECT was lost. It reads selectedKey, the component's own record of the isolation.
★ AND THE DIAGNOSTIC ITSELF NEARLY LIED. I first wrote selectedZoneKey, which does
not exist in this file; the try/catch around it would have swallowed the
ReferenceError and reported 'applied' every time. A diagnostic that cannot report
failure is worse than none -- the exact fault it was added to expose. Caught by
reading :163 for the real name (selectedKey) instead of trusting the one I
expected.
This does not claim to fix upg-100. It makes the next re-shoot decisive instead of
another guess.
rega re-shot the first click at main 031b9b38. country-posters HELD (India fits in
~1 s, bounds cover its bbox). upg-100 FAILED: the view switches, every list paints,
and 12 s later it is still the world view. Not a stale build — freshness PASS and
8 'select-zone' references in the shipped bundle, so the retained request ships and
does not reach the apply path.
WHY THE TWO DIVERGED, which is the useful part. Both got the same pattern, but they
drain differently. country-posters drains on isMapReady, which flips AFTER the
request arrives. upg-100 drained on legendNodes and only from the REMEMBERED id --
so it silently assumed an order: the attribute was read, or the observer fired, and
legendNodes changed afterwards. If legendNodes was already populated when the click
came, that watcher never fires again and the request sits in _pendingZoneId forever.
Nothing throws. It is the same shape as the bug being fixed, one level up: state
recorded correctly and then never applied.
So the drain no longer consults its own bookkeeping. It watches BOTH readiness
signals (legendNodes and mapRef) and on either one RE-READS THE ATTRIBUTE off the
host. The attribute is the durable record; re-reading it is what makes the outcome
independent of whether any single hook fired at the right moment, which was the
entire reason for choosing state over an event.
THIS HALF IS A HYPOTHESIS, NOT A PROVEN FIX, and I would rather say so than imply
otherwise: I can show the old drain COULD strand a request, not that it did. rega
has offered to instrument the timeline and that is the measurement that will settle
it — element <my-upg-100-list> in the map pane, attribute select-zone.
Also fixed, and this one rega proved: DESELECT DID NOTHING. country-posters'
applySelectCountry(null) cleared the flag and returned, so a second press left the
camera on India. PosterWall's button IS a toggle — it tracks lastShown and sends
listId: null on the second press — so a control you can turn on but not off reads
as broken even when the first press worked. It now flies back to the configured
opening view, the same center/zoom useMapInstance booted with.
A real clone defect, found by reading their markup after rega's live spacing probe
landed NEAR it and measured the pair as EQUAL.
Theirs is RegionDetail.vue:24 `.region-goal { margin-top: auto }`, and the goal
paragraph is the last child of `<div class="stack stack--lg | card | text-center">`.
composition/_stack.scss makes .stack a flex COLUMN with
`> * + * { margin-top: var(--stack-spacing) }`, so that auto OVERRIDES the child
margin and pushes the goal line to the BOTTOM of the card. Ours was margin:0, which
left it sitting directly under the text above it.
The visible consequence is the whole point for a clone: the three goal cards hold
different amounts of copy, so theirs align their goal lines flush along the cards'
bottom edge while ours floated at three different heights. That is exactly the kind
of thing the Driver means by "proper css spacing" — nothing is off-scale, and it
still looks wrong.
WHY THE PROBE COULD NOT SEE IT, which is worth more than the fix. rega measured
.rd-goal against .region-goal as "same", and they were right: at 1440 the card has
no free space, so margin-top:auto resolves to 0 and the two computed values agree.
An equal number hid a real difference. Their probe was correct AND incomplete, and
only their markup settled it.
My own spacing check could not see it either, for a different reason: it verifies
every value we SHIP is on their scale and is structurally blind to a declaration we
OMIT. That is "a property diff cannot see an addition", inverted. It now carries
hand-written assertions for the two omissions found this way (this one and
.region-stat__label's margin-top), since there is no general way to enumerate what
is missing.
★ AND I APPENDED THOSE TWO ASSERTIONS BELOW process.exit() at first, so they were
dead code: they printed nothing while the check still reported success. A check you
can add assertions to without them running is the same defect as a check that
cannot fail. Moved above the exit.
Also corrected, for the record: two of the nine selector pairs I gave rega were
wrong. .rd-big/.rd-big-label are the GOAL CARD's number and label, not their
.region-stat__* pair — our real counterparts, .rd-stat__value and .rd-stat__label,
already matched theirs exactly. And .rd-switcher's 112.5px side margins are the
container rule on .rd-scroll > * (90%+16px capped at 1200, centred at 1440), not a
spacing difference; theirs lives on a separate .container ancestor. I built that
pair list from greps instead of from the provenance comments in the file, which
document the true counterparts.
Every map showed DOXA's people groups, and only those. The same maps must serve other projects'
lists: Project 7000 (unreached), 4700 (frontier), 700 (frontier of 500,000 or more), 2000 (DOXA).
The list is now chosen the way the engine is:
profile-config `list` > ?list= > window.MAP_LIST > 'doxa' (exactly today's behaviour)
with ids or project aliases ('700', 'project-7000').
- library/lists/lists.json: each list is a FILTER over the IMB / peoplegroups.org dataset already
in the repo (Lostness Priority, Population, Engagement Status), never a hand-kept roster.
- library/scripts/people-groups/build-lists.mjs (`bun run lists`): writes each list's DOXA members
to data/people-groups/lists/<id>.json, joined the way the Vision Card funnel joins, so the
counts agree with it: unreached 7,341 (2,082 DOXA), frontier 4,754 (1,745), frontier-500k 737
(71), unengaged 2,982 (1,929).
- library/lists/peopleGroupList.js: resolvePeopleGroupList and applyPeopleGroupList(rows, cfg).
The default list returns the same array, unfetched. A list whose membership cannot load shows no
groups (fails closed) rather than DOXA's whole list under another list's name.
- Tier A, every production map: DataSourceManager applies it (and the maps that build one pass
their profileConfig), and so does every direct people-group fetch: people-groups-grid, the
Big Picture header, research-dashboard (data, CSV export; the DOXA-wide /statistics baselines
are skipped under another list), Regions (list, dashboards, their table), UPG-100 (4 fetches +
the poster wall), Gospel's roster, and the Experimental upg-neighbor and Wall. The composites
thread `list` to their children like `provider`.
Gates: check:universal-props gains a `list` column (LIST-FETCH, LIST-DSM, LIST-KEYS; its
self-test now plants 9 bypasses) and passes across 18 bundles, with all-people-groups allowed:
it draws the IMB dataset, so its list is Tier B. New check:list-prop-unit (Node, 17 cases, in
gates:post); poisoning the ladder order or deleting a membership file fails it. Documented in
documentation/reference-universal-props.md section 5.
Headless: all 14 touched bundles build; check:composites-mount 20/20 and check:composite-interop
17 links pass on this tree; no-orphan-gates, script-flags, quoted-scripts, doc-scripts pass. Not
rendered in a browser.
The Wall's pinned-d3 loader had the flaw rega found in PPLR's: d3's UMD build
writes its exports INTO an existing window.d3, so restoring the host's object
afterwards was too late. Ours had already been written into it. Same fix as
pplr-data-maps (60faff1a): while our script executes (document.currentScript
is our element), window.d3 is a slot of ours; any other code always sees the
host's own. A non-configurable `var d3` global is emptied for the load instead.
Headless, the real d3@7.9.0 (sha384 matches the pinned SRI) in a vm realm:
before host owns window.d3: keys 2 -> 579, version -> 7.9.0
after host owns window.d3: keys 2 -> 2, version host-7.0.0; the Wall gets its own d3 7.9.0
after no host d3: window.d3 absent afterwards, as before
rega re-ran PPLR's identical loader in a browser in all three variants, including a real
non-configurable `var d3`: PASS.
The Driver settled the call this was parked on: "the only changes are to the map to
make it look better, and to add the all-regions map ... which needs to be
intuitive". This is the map half, on the sanctioned addition only.
Their .country-map flips to aspect-ratio 1/2 under 768px and we port that verbatim
on .rcm — right for the CLONE pages, which frame one region or country in portrait.
The chooser renders under the GLOBE projection, so its subject is a CIRCLE whose
diameter cannot exceed the frame's short side. In a 1:2 frame that circle covers at
most W / 2W = 50% of the height: at 390px wide, 390px of globe in a 780px box with
the rest empty.
So this is geometry, not a preference between two acceptable options. A square is
the ONLY frame a circle fills on both axes, and every other ratio is provably worse
for a circle:
1/2 frame height 50% area 39.3%
1/1 frame height 100% area 78.5% (pi/4, the maximum in any rectangle)
Vertical coverage doubles.
SCOPED TO THE ADDITION, and that is the part worth checking rather than trusting.
The rule is .rcm--chooser, bound to colorMode === 'region'. I verified that value
is chooser-only instead of assuming it: mapColorMode returns 'in-country' for a
country and 'prayer' for a region, so 'region' is only the fall-through — the
no-selection state. If that ever stopped being true, this rule would silently
change cloned geometry, which is exactly what the scope ruling forbids, so the
check asserts it.
The existing globe-fill formula needs no change: it bisects the zoom that fills the
frame's SHORT side, which a square leaves well defined.
_chk-chooser-square-on-mobile-clone-untouched.mjs asserts both halves — the chooser
changed AND the clone did not — computes the coverage rather than restating it, and
carries a NEG control showing a 1/3 frame scores worse than 1/2, so the metric
discriminates between ratios instead of blessing whatever it is given.
395a66c9 translated the lens and put `const nt = useNeighborT()` at line 387,
but setup reads nt() at line 68 (statusMsg's first text). A const read before
its line is a TDZ ReferenceError, so setup threw "Cannot access 'nt' before
initialization" and the lens rendered nothing, map included, on both engines
(rega, check:leaf-engine-per-provider on main c8fee416).
The translator is now defined above its first use. Of the other files that
commit touched, only flower-mesh uses nt() above its definition, and there it
is inside a hover handler that runs after setup, so it is safe.
Headless (_qa-cycle/exp-shepherd/lc-probe.mjs: the check:composites-mount
harness, mounting the lens under ProfileLoader's provides):
main lang-cluster: setup THREW, ReferenceError: Cannot access 'nt' before initialization
fix lang-cluster: setup ran clean (send-map, which draws, also runs clean)
4c45d76c put the host's window.d3 back after loading the pinned d3, but d3's UMD
build writes its exports INTO an existing window.d3 ((t = globalThis).d3 =
t.d3 || {}). By the time the host's object was restored, ours had been written
into it: rega measured 577 added keys and version '7.9.0' on a host's own d3.
A host on d3 v6 would get v7 functions.
While our script executes (document.currentScript === the element we
appended), window.d3 now reads and writes a fresh slot of ours. Any other code,
at every moment, reads and writes the host's own value, so nothing is blanked
while the file loads. The host's value is then restored as a plain data
property. A non-configurable global (a page-level `var d3`) cannot be trapped,
so it is emptied for the load instead.
Headless, with the real d3@7.9.0 file (its sha384 matches the pinned SRI), run
in a vm realm with document.currentScript set as a browser does:
before host owns window.d3: keys 2 -> 579, version -> 7.9.0 (rega's finding)
after host owns window.d3: keys 2 -> 2, version host-7.0.0, same object;
PPLR gets its own d3 7.9.0; the host sees its own d3 during the load
after no host d3: window.d3 absent afterwards, as before
The vm realm makes `var d3` configurable, so the non-configurable fallback was
not exercised there. The probe is kept at _qa-cycle/exp-shepherd/d3-probe/.
The critic fact-check (CRITIC-3) found PPLR's popups interpolating data raw into setHTML():
popupForPG (pins, coverage, rosetta), the country popup in coverage, the local popup builders in
religion, rop-tree and language-families, and the search box's suggestion renderer. That last one
renders place names that come from the Mapbox geocoding API, so it is external data. All of them now
go through escHtml(); the dash placeholder for empty values is unchanged.
Headless proof: a hostile-input test (an <img onerror> name, a <script> country, quotes and
ampersands) 5/5: no live tag or handler survives and the text shows as text. BUNDLE=pplr-data-maps
builds. Plugin-only.
The Driver's instruction: the regions dashboard must look like a clone of dx-life
"with proper CSS spacing", the only sanctioned deviations being the map and the
all-regions chooser. So spacing stops being a matter of taste and becomes a
measurement: every padding, margin and gap we ship should be a value THEIR CSS can
produce.
Measured that way, one value was ours and off-system: .rd-status / .rl-status
padding:28px, the app-only empty and loading blocks ("No regions found.",
"Loading region dashboards…") that their server-rendered page cannot be in. The
ELEMENT is legitimately ours; the NUMBER had no business being invented. Now
var(--spacing-xl). Result: 39 var(--spacing-*) references and 34 px literals, all
traceable to their CSS, and ZERO off-scale values.
★ TWO DENSITIES, AND I HAD CONFLATED THEM. tokens/semantic/_typography.scss sets
$font_density 1.25; tokens/semantic/_spacing.scss sets $density = $major-third =
1.33. An earlier check used 1.25 for BOTH, so every spacing floor it reported was
wrong. The conclusion survived — 11/11 token-bounded clamps still pin at 390px —
but one case came within 0.13px of flipping, which is the margin a wrong constant
hides. The scale is now DERIVED in the check from their own file, with a control
that fails if their file stops saying 1.33, so it cannot drift back.
The check's first premise was also too strong and would have produced three false
findings. It flagged 15px, 25px and 3.5px as off-system when clamp(15px, 6%, 25px)
is _progress-bar.scss:14 verbatim and gap:0.35rem is .prayer-coverage-legend's own
— correct ports under RULE 1. A value is only off-system if it appears NOWHERE in
their CSS, so the reference set is harvested from their real stylesheets and
unioned with the token scale. .sr-only's margin:-1px is excluded for a different
reason: it is the screen-reader clip idiom, where 1px is a hairline that keeps the
element non-zero-sized for assistive tech, not a spacing choice.
What this check CANNOT tell you, stated so nobody over-reads it: that a value is in
the right PLACE. It proves every number is on their system, not that the right
number is on the right selector. Per-selector parity still needs the live diff.
A builder previewing a plugin full screen can hide the studio header (the props panel: language, map
engine, Mapbox token). The header's new "Hide header" button collapses it, as the Research Gallery runs
headerless. A "Show header" button then stays pinned in the top corner, above every plugin, so the
props are always one click away. The choice is remembered in this browser (plugin-studio:header-hidden).
Strings in all 11 studio catalogues. BUNDLE=plugin-studio builds.
Page 2 of every packet is now the funnel: 12,379 people groups on earth, 7,341
unreached, 4,754 frontier, 2,107 on this prayer network's list, and N of those
in the reader's hand. A partner holding one list can see where it sits in the
whole, which is the argument the packet is making.
★ IT RENDERS NUMBERS IT DID NOT COMPUTE. Every count is read from
data/people-groups/vision-funnel.json, which build-vision-funnel.mjs generates
from the IMB CSV and the live network list and which already drives the Vision
Card. Re-deriving them in the packet would give one fact two sources; they would
drift, and two internally-consistent surfaces disagreeing is worse than one
wrong number because a reader cannot tell which to trust.
K AND THE SHARE ARE MEASURED AT RENDER TIME, NEVER TYPED, and this mattered:
the brief said "~30 lists of 50-100" and the catalogue says K=27 with counts
from 8 to 118 (west-africa 118, asia-india-rajasthan 116, uncovered-scatter 8).
Only about half sit in that band, so no copy claims it. The page states what the
data can defend -- "1 of 27 lists" and "N of the 2,107" -- and a footnote states
plainly that 21 of the 2,107 are in no list, because a partner adding up rows
would find the gap anyway. Reported to the Driver; K=27 pending his ruling on
whether the two residual buckets count.
A SHARE MUST NEVER ROUND TO ZERO. One decimal gave the single-group fixture
"0.0%", which reads as "this packet covers none of them". Verified exhaustively
over all 2,108 possible list sizes against 2,107: no size prints 0 and none is
malformed. 1 group -> 0.05%, 20 -> 0.95%, 21 -> 1%, 43 -> 2%, 118 -> 5.6%.
NUMERALS STAY WESTERN, as required. num() is toLocaleString(lang), and CLDR's
default numbering system for the bare ar/hi tags is latn -- verified 12,379 and
2,107 in both. Reusing num() rather than writing a second formatter is also what
keeps the funnel's digits identical to every other page of the packet.
PAGE COUNT NOW COUNTS THE ARTIFACT. packet.mjs computed
`1 + ceil(n/PER_PAGE) + 2` under a comment claiming the number was "asked of the
renderer, not recomputed here". It WAS recomputed, and the funnel page silently
made it wrong by one -- exactly the drift the comment warned of. It now counts
the emitted sheets.
THE GATE IS TAUGHT, NOT LOOSENED. check-packet-render went red on every fixture
("43 cards should lay out as 6 sheets, but the packet has 7") -- correct
behaviour on a structure it had not been told about. The wrong fix is to accept
N or N+1: that assertion exists to catch silently dropped people groups, and a
±1 tolerance is the slack a lost list page hides in. So the funnel is DETECTED
via .funnel-title and added as a known term, and a funnel page that renders its
title but no share figure is now its own failure -- a number printed but not
asserted on is decoration.
The page is OPTIONAL: buildPacket omits it when no funnel data was supplied, so
every existing caller keeps working and a host without the data still gets a
correct packet. Better a 4-section packet than one asserting figures it could
not read. A multi-list batch selection keeps the steps but drops "1 of 27",
which is false for a custom selection, while "N of the 2,107" stays exactly true.
Verified: check:packet-strings-parity PASS (31 string keys x 11 locales, my 10
new ones present and translated in all 10 non-English blocks, 0 declared-absent
additions). A partial locale with 3 of 10 funnel keys translated renders those 3
translated and the other 7 in English, nothing blank. Arabic packet generated:
dir="rtl", funnel is sheet 2 of 7, all four steps carry Western digits. All 16
generated packets satisfy the corrected sheet arithmetic, including the 11 older
laos packets that have no funnel page. Mutation-tested: dropping a list sheet is
caught by the independent page-header witness (20 DROPPED), a funnel page with no
share figure is caught by the new assertion, and removing the funnel page passes.
BUNDLE=my-upg-100-list vite build 319 modules clean.
NOT verified in a browser: check:packet-render launches Chromium, which the
Playwright policy reserves to rega. The simulations above parse the same
attributes from the same generated HTML.
A new writer's first copy used to draw a coloured box. template-map-a.vue now
draws the points in data/places.geojson on a basemap with one translated title,
through the library's own pieces (useMapInstance, resolveMapProvider, labelFont,
emissivePaint). It follows the page's engine, locale (right-to-left included) and
token, and with no token it runs on keyless MapLibre, so it works with no account.
template-map-b.vue stays as the second-screen example, so the README, the tutorial
and the plugin-designer skill (rename a, delete b) stay true. The README gains the
map's pieces table and the dx-maps paths.
Library change: library/i18n/locales/<11>/template.json (key template.title) is
eagerly globbed into every bundle, so consumers need rebuilding (rule 5B).
upg-neighbor/flower-mesh is a preset of the upg-neighbor element that opens on
its Flower Mesh lens, which is SVG and has no GL map. The leaf is still a map
element: the view-switcher reaches Send Map and the other GL lenses, so it must
keep isMap (engine peer, token ladder). Marking it isMap:false would break those
lenses. check:leaf-engine-per-provider judged it as a drawing map and failed it
on both engines ("drew: none"), before and after the auto engine alike.
- upg-neighbor.instances.json: the flower-mesh preset says "opensOnMap": false.
- generate-tree.mjs: an instance's manifest entry carries opensOnMap:false when
it is set (absent otherwise, so every other entry is byte-identical).
- check-leaf-engine-per-provider.mjs: such a leaf is not judged as a drawing
map. Its Mapbox-request count under ?provider=maplibre is still measured.
Headless: after bun run tree, the manifest marks only
upg-neighbor/upg-neighbor/flower-mesh with opensOnMap:false. The gate's leaf
selection, replayed over it, judges 28 of 56 leaves as drawing maps, with
flower-mesh excluded and its five siblings unchanged. No browser run.
I broke both composites on main. Editing them with a pattern-match script, I
replaced an import line by matching text that had already changed, so the
replacement silently matched nothing and three names — RTL_LOCALES,
getRuntimeLocale, detectLocale — were used and never imported. Setup threw a
ReferenceError and both elements rendered nothing until a hotfix landed.
★ IT PASSED EVERY GATE, AND THAT IS THE INTERESTING PART. It compiled, because a
free identifier is valid JavaScript until it RUNS. It passed my grep of the
built bundle for the new code, because the code was genuinely there. It passed
template-literal, sfc-parse, composite-freshness and SRI, because not one of
them evaluates a setup function. The evidence I had could not distinguish a
working composite from a dead one, and I called it "verified headless".
So this asks the one question those gates cannot: is every name a component uses
actually bound — imported, declared, a parameter, or a known global? It reads
the same source the compiler reads, tracks real scopes (module, function, block,
catch, class), and runs in milliseconds with no browser. Fleet-wide, over every
.vue in plugin-designs/ and library/, because the mistake was a text-editing
habit rather than anything about one plugin.
WRITTEN BY RUNNING IT, which is the only way an allowlist gets honest. The first
run reported 4,259 findings — every one a false positive from a real bug in my
own walker: a helper that takes a node TYPE was being handed the NODE, so the
function branch never fired and every parameter read as unbound. The second run
found the globals this tree genuinely uses (d3, customElements, ShadowRoot, CSS,
Function, self, the typed arrays) and vite's build-time defines. The third found
a real gap in the gate: an SFC may have BOTH a plain <script> and a <script
setup>, and they share a scope — LegendTools exports a function from one and
uses it in the other, which I was reporting as free. Reading only one block
would have taught the fleet to ignore this gate on its first run.
NEGATIVE CONTROL, because a check that has never been seen to fail is not
evidence: restoring the broken import makes it FAIL naming all three names and
their lines; restored, it passes across 127 components.
It is the cheap half of a pair — it proves a name is bound, not that the thing
runs. The expensive half is a check that actually mounts a composite.
rega measured two broken interop links and read them as one timing bug. They are
two different bugs, and neither is the one hypothesised (that the event fires
before the map pane has mounted -- both shells create and append their pane
elements in their own onMounted, so the element exists at click time).
country-posters: THE FORWARDING WAS NEVER WRITTEN. onShowOnMap() was a guard,
go('map'), return. No dispatch, no attribute, no property -- nothing carried the
selection to the map at all, which is why the camera stayed at world view. Not a
race, and no amount of waiting would have fixed it. The shell also created its
pane elements and dropped them on the same line, so there was no handle to address
the map pane with even if it had wanted to.
upg-100: forwarding existed but was fire-and-forget, losable two ways. The listener
is attached at the END of an async onMounted, after 5 awaits and only `if (host)`,
and an element dispatch has no replay. And once attached, the zone id was resolved
against legendNodes and applied by `if (node) onLegendSelect(node)` with NO else --
so an id arriving before the data was discarded in silence. rega's second-click
measurement settled which one fires: the first request dies before readiness.
ONE PATTERN, and it is deliberately not "attach the listener sooner" -- that would
fix one of the two losses and leave the other. The request is RECORDED as an
attribute on the pane element and only then announced by event. An attribute is
state: it survives custom-element upgrade, and it is still readable when the pane
arrives, and when the pane's data arrives later still. The event remains the live
path for a map that is already up. Both triggers end in one apply function, so
there is one apply path with two triggers rather than two ways to select.
Unreadiness is retained; an UNRESOLVABLE id is not. Retaining a typo'd id would
spin on every readiness change forever, so it is dropped.
The drain runs on nextTick, and that is not caution. upg100-plugin flagged from
experience that watch([batchMode, isDark]) marks legend rows on nextTick, so a
pre-flush drain would apply the selection against rows about to be replaced by the
re-render, and the highlight could be stomped. That failure is invisible in the
worst possible way: "the selection happened but you cannot see it" is
indistinguishable at the rendered layer from "the selection never happened" --
the exact bug being fixed. The fix would have impersonated the defect.
SOURCE ONLY, deliberately. Both bundles were built to prove the source compiles,
then the built artifacts were reverted: these are not my bundles, and rule 5 keeps
rebuilds with the build owner. country-posters and my-upg-100-list both need one.
Verified by _chk-showonmap-selection-is-not-retained.mjs, 30 assertions. It asserts
the ONE APPLY PATH property rather than just the functions' presence, on
upg100-plugin's suggestion -- a later third trigger that bypassed the apply path
would otherwise fail nothing and reproduce the invisible-selection symptom.
P2. The list of groups with nothing already existed and could only be reached by
knowing which control to press. It is the question this map is for, so it gets an
address: ?view=zero opens it with the map already filtered to those pins. That
address is also what a partner embeds and what a QR code points at.
Each row now carries its way in. This was the only surface showing groups with
nothing that did not say what to do about them — the pin sheet and the country
panel both do — and it is the one a mobiliser reads top-down.
Building it exposed the same flag under-reporting for the third time. `sg` comes
from the sign-language catalogues and asf (Auslan) carries none, so reading it
alone this leaf missed Cocos Deaf, Deaf Christmas Islanders and Deaf Norfolk
Islanders: no SUN panel on their pin sheet, and no way in on the list, while the
access table gave all three SUN. The pins builder and the access builder were
each corrected the same way earlier; this puts one Deaf test in the leaf so the
four surfaces agree. Measured after: 369 of 369 zero-access rows carry a way in,
none blank.
The revert left fmtShort and the table's row count going through fmt() instead of
calling toLocaleString() directly. Behaviour was identical, because fmt is that
bare call again — but the acceptance measure for holding number format is a
literal count of the pre-change state, and 6 is not 8. An "identical in effect"
argument is the wrong currency when the point is that this map must be byte-for
-byte what it was until the Driver rules.
Measured against the build at 2eac0fb3^: numberingSystem latn calls 0, bare
toLocaleString() 8, both exact. The number handling in all four leaves now
differs from that build only in comments.
The shepherd's call: translating wording that already exists is not a product
decision. So every string keeps the English it reads today and gains a key, in
all eleven locales.
Big Picture's four first, since it is the localization priority: Export CSV, the
detail drawer's title and its empty state, and the legend's "heartbeat zone"
bin. Then Top countries, Select all, the table dock's Expand/Collapse titles,
and the singular "group" whose plural was already translated — a half-translated
plural is its own small defect.
Full screen / Exit full screen now read buttons.toggleFullscreen, which already
shipped in every locale, so that one needed no new wording at all. The arrow
glyph still carries the state.
Three more went in that were not on the list, because leaving them would have
been worse than the defect being fixed: "Deselect all" is the other half of a
toggle whose first half we just translated, and the two row-selection aria
labels are the same control speaking to a screen reader. A control that changes
language halfway through is harder to use than one that never changed at all.
★ ONE STRING I AM NOT CONFIDENT IN, AND IT SHOULD BE CHECKED BY A SPEAKER.
"heartbeat zone" is the Driver's own coinage, not standard UI vocabulary, and a
metaphor is exactly where a translation I cannot read can go quietly wrong. I
translated it literally in all ten languages. The rest are ordinary interface
terms; that one is a judgement, and I would rather flag it than have it
discovered on the gift.
VERIFIED HEADLESS: i18n-parity passes with zero debt; three bundles build; the
keys AND their Arabic values are present in the built output (تصدير CSV,
منطقة نبض القلب, اختر شعبًا لعرض التفاصيل, and the header's placeholder).
Numerals are untouched, per the hold — this is text only.
(cherry picked from commit 67568c0509)
The critic fact-check (CRITIC-9) found the Wall injecting Font Awesome 6.5.1 into the HOST
page's <head> (global .fa-* rules that can restyle a partner's own icons) and loading d3@7 (a
floating range, no SRI) as window.d3, overwriting a host's own d3.
- The card's heart / comment / share icons are now plain SVG paths in the card itself, not
Font Awesome <i> tags in three foreignObject/xhtml divs per card (~6,300 on the wall: lighter
too). The dialog hearts are inline SVG; the source-picker info icons are a text glyph. No
Font Awesome stylesheet is injected anywhere.
- src/wallD3.js loads d3@7.9.0 exactly, with integrity + crossorigin, keeps it in the module
(wallD3()) and puts the host's window.d3 back (or leaves none).
- The heart used `self.$store`; `self` is the browser's window, so a prayer made in this
session never lit it. It now reads this.$store.
Headless proof: a stubbed-DOM test of the d3 loader, 6/6 (the host's d3 restored, none leaked,
exact version + integrity + crossorigin, a failure rejects). The bundle builds and contains 0 Font
Awesome references.
needs eyes: the Wall: hearts, comments and share icons render on the cards; a card whose group has
prayers shows a red heart; a host page with its own d3 or Font Awesome is unaffected.
(cherry picked from commit 28575d6013)
The last bundle of the L1 pass: key-cities had no i18n at all though its entry installs it. A new
keyCities area (38 keys, all 11 locales) carries the map title, band legend, caveats, the city
sheet's labels, the list's intro, filters and section lines, the data tables' title, intro, tabs and
loading line, and the poster wall's heading, tag, nouns and "coming soon" message. Unnamed places
keep showing coordinates, now inside a translated phrase. Counts use a "label: n" form outside English
and numbers keep their existing format (numerals held). cities-map's script message goes through a
guarded useI18n with the English catalog as fallback.
check:i18n-parity PASS; placeholders match English in every locale; the bundle builds.
A library catalog change: studio lands it (rule 5B).
needs eyes: key-cities (gated) with ?lang=ar: the map legend, a city sheet, and the list.
(cherry picked from commit 7d4da13721)
The wall gave all ~2,100 card photos their full href at once, so every photo was fetched and
decoded even with the whole 4000x3000 wall on screen and each photo a few pixels wide. Now:
- photo URLs are parked in data-hd; zoomed out (a card under 48 px on screen) the wall shows
only the light image slots, with no photos loaded;
- zoomed in, updateLod() (rAF-throttled on every zoom/pan) gives HD photos only to the cards in
view plus one card of margin, and REMOVES them from cards that leave the view; a 300-image
cap keeps any view light;
- "Print HD poster" loads every photo, waits for them, prints, then returns to level of detail;
it replaces the old "coming later" alert (new key wall.printPoster, all 11 locales).
The geometry is a pure module (src/wallLod.js), so it is tested without a browser.
MEASURED, headless: the real 2,107-card layout at 1280x800. Sampled photos are ~200x250 (23 real
image URLs from the live API): ~0.22 MB each decoded, ~45 KB compressed. Loading all of them, as
before, is ~466 MB decoded + ~94 MB downloaded. With LOD: zoom 1-4x = 0 photos; 6x = 150 (~33 MB);
8x = 80 (~18 MB); 12x = 63 (~14 MB). lodPlan matches a brute-force visibility oracle at every zoom.
Photos are the part of the 1.4 GB that can be measured without a browser; the rest (DOM,
rasterisation) needs a browser heap snapshot: needs eyes.
needs eyes: Wall of People Groups: DevTools memory before/after; zoomed out = no photos, zoom in =
photos appear only where you look; "Print HD poster" prints every photo.
(cherry picked from commit ef9646f8f5)
The universal-props matrix flagged upg-neighbor as a locale BYPASS: no t() in any of its ten
components. By the agreed scope (dx-shepherd, 2026-09-26), this localizes the CHROME in all 11 locales
through a new neighbor area (103 keys): the brand, view tabs and hints, lens titles, search
placeholders, select/aria labels, loading and error lines, legends, stat labels, tooltips, chart-strip
controls, list actions and copy feedback. Research terms, provenance footers and proper nouns stay in
English as named sources: SEND, doxa-2000, Bounded set, JP, PPLR, IN/CN, the 4-D join notes.
Templates use the globally injected $t; script strings use useNeighborT() (guarded, English fallback).
Stat lines use a "label: n" layout in every locale so no language needs plural rules. Country names
in the lookup tables are data and unchanged.
check:i18n-parity PASS; every locale's placeholders match English; the bundle builds.
A library catalog change: studio lands it (rule 5B).
needs eyes: My UPG Neighbors with ?lang=ar: the view tabs, the Send Map legend and the Overview chart strip.
(cherry picked from commit 090f64fbe3)
The universal-props matrix flagged the Wall as a locale BYPASS: no t() anywhere. A new wall area
(library/i18n/locales/<lc>/wall.json, all 11 locales, 39 keys) now carries the header, orientation
and zoom controls, the loading and offline notice, each card's prayer count and "View prayers", and
both dialogs (Prayer Tools, People Group Chats), with aria-labels on the close buttons, which had none.
The components use the globally injected $t (createAppI18n, globalInjection). The prayer count uses
vue-i18n's plural choice in English and a neutral "label: n" form elsewhere, so no language needs
plural rules. Status values from the API are data and stay as they come.
Headless proof: vue-i18n against the real catalogs, 8/8 (en 0/1/4 plural, ar neutral form, ar title,
de label form); check:i18n-parity PASS; the bundle builds. Library catalog change: studio lands it.
needs eyes: Wall of People Groups with ?lang=ar: the header, a card's prayer line, and both dialogs.
(cherry picked from commit 7c1c60975e)
The universal-props matrix flagged pplr-data-maps as a locale BYPASS: every tab label and every
loading or ready line was an English literal. A new pplr area (library/i18n/locales/<lc>/pplr.json,
all 11 locales) carries the 10 tab labels and 20 status lines; usePplrT() reads them through a
guarded useI18n with the English catalog as fallback. Counts go in as parameters, and their number
formatting is unchanged pending the Driver's numerals decision. The views' own legend and panel text
is the next pass.
check:i18n-parity PASS (all 11 complete); the bundle builds. A library catalog change (the i18n index
globs every area into every i18n bundle), so studio lands it (rule 5B).
needs eyes: PPLR Data Maps with ?lang=ar: the tab bar and the loading and ready lines.
(cherry picked from commit 4a16f0538b)
The Driver's localization list, item 4. doxa-simple-map already took its language from profile-config
locale/lang and ?lang= (ProfileLoader) and set dir=rtl for Arabic, but its own strings were English
literals: "Loading map…", "The map could not be loaded." plus a technical init message, the legend
title 'Legend', the default "Prayer" tab label, and the whole "About This Map" help modal. They now
read the catalogs: legend.titleBar.legend, tabs.<id>, allPeopleGroups.loadError, buttons.aboutThisMap,
buttons.close (the close button had no accessible name), and a new simpleMap area in all 11 locales.
A tab label a HOST passes in profileConfig.tabs is still shown as given. The map formats no numbers
itself; the legend, popups and detail panel are library components already translated.
check:i18n-parity PASS; the bundle builds. A library catalog change, so studio lands it (rule 5B).
needs eyes: Prayer Progress with ?lang=ar: the loading line, the tab, the legend title and RTL layout.
(cherry picked from commit c35bc84794)
08f89f55 gave both composites a text-direction computed that calls
getRuntimeLocale(), detectLocale() and RTL_LOCALES, but imported none of them.
The build does not check free names, so the bundles on main reference three
undefined globals. watch(textDir, reflectDir) evaluates the computed during
setup, so setup throws a ReferenceError in every locale, and Vue renders
nothing for the whole composite: the map, the dashboard and the grid.
Reproduced headless with the same computed + watch shape under
@vue/runtime-core: lang '' fails with "getRuntimeLocale is not defined", and
lang 'ar' with "RTL_LOCALES is not defined"; neither renders.
After this import, both bundles build and neither contains a free reference
to the three names.
922a8b66 formatted counts with toLocaleString(<reader's locale>), which switches Arabic to
Arabic-Indic digits. Numerals are held for the Driver (dx-shepherd, 2026-09-26), so num() goes back
to the browser-default toLocaleString() these views used before. Only the text is translated.
The bundle builds.
country-posters-mfe labelled its panes with English literals ('Country Posters', 'Map') while the
panes inside translated: the chrome a reader sees first (universal-props matrix, cell L1). They now
read tabs.countryPosters (new, in all 11 locales, worded as the Research Gallery's own rail) and
the existing tabs.map, through a guarded useI18n with the English catalog as the fallback.
check:i18n-parity PASS; the bundle builds. Library catalog change: studio lands it (rule 5B).
Both views were English-only with no t() (the universal-props matrix, cell L1): titles, intro,
count chips, section and row lines, band pills, the source note, the five table tabs and the
loading line. They now read connectivity.list.* and connectivity.tables.* in all 11 locales, with
the English catalog as the fallback where no i18n is installed (the connectivity-map pattern), and
format numbers in the reader's locale instead of the browser's. Count chips use a "label: N" form
outside English so no language needs plural rules. Region and country names are data, unchanged.
check:i18n-parity PASS; every new key's placeholders match English in every locale; the bundle builds.
Touches library/i18n/locales (eagerly globbed into every i18n bundle), so studio lands it with the
consumer rebuild (rule 5B).
needs eyes: Cities with Internet > list and Cities Data tabs with ?lang=ar and ?lang=zh.
NUMBER FORMAT REVERTED, on jurisdiction rather than on merit. Formatting with the
locale i18n resolved is almost certainly right, but it decides number format for
this map alone and that decision belongs to every map at once. The finding stays
in the source so nobody re-derives it: toLocaleString() with no argument formats
for the JS runtime's locale, so the same page shows different numbers to readers
on different browsers; and pinning latn would change digits only for region tags,
since the eleven tags this bundle ships already resolve to latn.
MAPLIBRE. Reported against the built tree: under ?provider=maplibre this map drew
the basemap and nothing else — no fills, no pins, queryRenderedFeatures returning
0 while the source held all 2,107 features — while ?provider=mapbox drew 170
fills and 2,107 pins, with no page errors either way. Everything this map draws is
added inside one map.once('load') callback, so a load that has already fired
leaves exactly the basemap, which is the reported symptom; the two engines
resolve their styles on different schedules, so the race lands differently on
each. Draw immediately when the style is already loaded, keep once('load')
otherwise.
This is the cause the evidence indicts, not a verified repair. Browsers here
belong to one agent, so it needs re-running before anyone believes it.
The critic fact-check (CRITIC-9 / CRITIC-5): the ROP Sunburst loaded d3@7 (a floating range, no
SRI) as window.d3, overwriting a host's own d3, or running on whatever d3 the host already had.
ensureD3() now loads d3@7.9.0 exactly with integrity + crossorigin, keeps it in the module
(pplrD3()) and puts the host's window.d3 back (or leaves none). SemanticSunburst reads pplrD3(),
and if a caller forgot to await ensureD3 it loads it and builds instead of asking for a refresh.
Headless proof: the stubbed-DOM loader test, 6/6 (host d3 restored, none leaked, exact version +
integrity + crossorigin, a failure rejects). BUNDLE=pplr-data-maps builds. Plugin-only.
needs eyes: PPLR > ROP Sunburst draws; on a host page with its own window.d3 it is untouched.
Two things, and the second was found by the first disagreeing with itself.
The CSV is what a team prints onto a poster or turns into a QR code, and it was
the only surface that did not say when the bridge language carries the Zume
course. 179 rows now do. Same URL the sheet already shows and the verification
already proved; nothing new is fetched.
"Candidate groups" is removed rather than renamed. It counted people groups
speaking the candidate language, which only the old pins-derived proxy could
produce; since candidates came from CLDR it has shipped as 0 on all 108 candidate
rows, and an empty column in an exported CSV reads as missing data rather than as
one that stopped applying. CLDR does give whether the language is official there,
which is a real fact about how likely it already is to be taught, so that takes
its place.
THE BUG. 182 rows had a bridge carrying Zume and only 179 could export one. The
three were Deaf groups speaking Auslan, and codes.asf has no sg flag — it is set
from the sign-language catalogues and asf is in none of them. So the pin index
built a SPOKEN bridge for them and the map offered it, because the map's is_sign
test reads that same flag. The access table refused it, because it also matches
the group name. A spoken bridge is not a weak bridge for a Deaf community, it is
not a bridge; this is the residue of the finding that already removed 89.
Suppressed on either signal now, so the two builders agree and the map cannot
offer what the table refuses.
Still 369 with nothing and still none of them without a way in.
app/assets/styles/base/_reset.scss:3-8 applies border-box to every element on
DX-life. Our leaf document carries the identical rule already -- but these
components render inside a SHADOW ROOT, and a document-level `*` selector does not
cross that boundary. So every element in here was content-box while every element
of theirs was border-box, and no scoped component style could reveal it. That is
how it survived seven global-rule fixes, and it is why this codebase had accrued
seven separate per-element box-sizing:border-box patches: each was a local
workaround for the reset that never arrived.
What it cost, measured rather than described: their legend swatch is width:10px
with a 1px border -- 10px under border-box, 12px under content-box. Two swatch
columns of 1px each are exactly the 2px by which our legend panel measured wider
than theirs (167 vs 165) after the rest of the legend was aligned to the pixel.
The same label measured 129px on both sides in the same Poppins, so the text was
never the cause.
The port is a geometric NO-OP and that is why it is safe to do in one move: the
blast radius was measured FIRST, by injecting this exact rule into every shadow
root and diffing every element's rect. 0 of 431 elements moved on
region-dashboards, 0 of 678 on regions-list, 301 already border-box by their own
declarations. The detector was negative-tested, because a zero from a check that
cannot fail proves nothing. So the whole value of this commit is the DELETION --
six per-element patches plus the interim `.rcm *` subtree rule, which the global
reset strictly contains.
Placement is plugin-local, not library/useShadowStyles.js. Putting it there would
fix all 25 bundles in one line and is tempting for exactly that reason, but the
blast radius above was measured on THESE two pages only; 23 other bundles would
have their geometry changed on no evidence. RULE 2 says default to the plugin
folder until a second consumer is named.
One module rather than three copies: three roots inject into the shared shadow
root and any of them can be the one that mounts, so all three need it.
The rows under "No resource found" name the way in. For 182 of the 369 groups
with nothing, that bridge language also carries the Zume discipleship course, and
the row did not say so — which is the difference between "there is a way in" and
"there is a way in and the course to train the first believers already exists in
it". A mobiliser scanning a country is deciding where to start, and that is the
deciding fact.
The bridge's own name stays first, because the language is the decision and the
training is what makes it actionable. It reads "way in: French · Discipleship
training", with the label coming from the catalogue so it follows the reader's
locale like every other label on the row.
Nothing new is fetched or linked: it reads the same Zume URL already verified and
already on the bridge panel, so the doctrine screen is unchanged at 16,963.