highlightCountryByIso makes the regions fill and border paints data-driven on a click,
and the first such click paid a one-off shader-variant compile (rega measured 5.5 s /
11.1 s on the research map). The three paints are now data-driven from the moment the
layer is added, with identical branches so nothing changes on screen.
exp-shepherd's copy of this pre-warm is better than mine in one way and accidentally right in another,
and both are worth taking.
BETTER: it leaves an existing expression alone instead of wrapping it. Mine wrapped regions-border's
line-color — which arrives as a region-coloured match on a feature property — into a case around that
match. No visual change, but a gratuitous rewrite of an expression that was already data-driven and had
no variant left to warm.
ACCIDENTALLY RIGHT: their test is "is it an array". For this layer that coincides with the right answer,
because the only array here is that match. It is not the right test in general — a zoom expression like
['interpolate',['linear'],['zoom'],…] is also an array and is NOT data-driven, so skipping it would
leave that property to compile its variant on the first gesture. That is the partial pre-warm this whole
exercise exists to avoid, and it would measure as a failure with no visible cause.
The test is now whether the expression reads the FEATURE — get, has, or feature-state, at any depth.
Constants and zoom-only expressions get wrapped; anything already keyed on a feature is left exactly as
the layer wrote it. The regions layers have no zoom expression today, so this changes nothing that runs
right now; it means the rule is correct for the reason it is correct.
Verified: gates:pre 13/13. Chained on 790d8838.
Found by reading addRegionsLayer while checking exp-shepherd's copy of this fix, not by anything that
failed. My pre-warm called paintRegionScope(map.value, []) — the CLEARED look: flat grey borders at
width 0.6. The layer does not start there. useMapLayers adds regions-border with line-color set to the
REGION-COLOURED match expression and line-width 1, so the moment the Regions tab was first opened, every
border turned grey and thinner than the layer had defined.
Nothing was going to catch it. The shader compile moved as intended, the write count was unaffected, the
highlight still worked, and the checks in flight were a timing measurement and a write count — a change
of appearance is invisible to both. It would have been found by a person looking at the map, eventually,
and blamed on something else.
prewarmRegionPaint reads each property back and re-writes it wrapped in a case whose two branches are
the value it already had. The appearance is preserved exactly as the layer defined it, and the property
still becomes data-driven, which is all the shader variant depends on. A property the layer does not
define is skipped rather than invented, and one already wrapped is left alone so a second call cannot
nest cases.
paintRegionScope keeps its idle values for the CLEAR path, which is a different thing from the initial
state and always was — conflating the two is what caused this.
Verified: gates:pre 13/13. Chained on 196eca13. Needs eyes: rega — on the first visit to the Regions
tab, the country borders should be REGION-COLOURED at their original width, not grey.
Two findings from upg100's census. check-one-palette read strategy palettes from
library/colors/ only, so a bundle's own src/colors/ strategy (which overrides the
library one for that bundle) was never checked; it now reads both, the bundle's
over the library's, and --self-test plants a private palette. Rudder's
engine-offline notice gave a go build line without saying Go 1.22+ is needed or
that setup-runtime.sh downloads the model; it now says both and points at
bun run desktop:build and the desktop HOWTO.
DEDUP-AUDIT.md (measured today: 253 files, 31 cross-plugin duplicate clusters,
622 duplicated lines) ranks this tenth by raw cost and first by suitability. It
is the only cluster where the copies are identical in code, close over nothing,
and sit in two plugins with one owner between them.
loadScriptOnce (13 lines) and whenTagDefined (11) were byte-identical between
doxa-maps-spa and plugin-studio. deriveTreeBase (9) differed by ONE COMMENT LINE
and not one character of code -- drift in the documentation of a shared function,
which is its own argument for having one copy.
They are one concern, not three: load a bundle's script once, wait for its custom
element to register, and work out the tree base to load it from. That is what a
capability-5 app shell does to nest another plugin, and both plugins here ARE the
capability-5 shells. Any future shell needs the same three.
Verbatim move: no parameterisation, no behaviour change, callers import instead
of declaring. The comments travel with the code because each records a
measurement -- particularly the one explaining why onload is not a usable success
signal for a dev-server ES module.
ONE BEHAVIOUR CHANGE, AND IT IS AN IMPROVEMENT: the _loaded cache is now shared.
Two shells on one page previously kept separate caches and each loaded the same
bundle; the second now reuses the first's promise.
deriveTreeBase reads document.currentScript, so it is only valid AT MODULE
EVALUATION TIME. The call sites did not move -- both entries still call it at
module top level -- and the library file documents the constraint, because a
later call silently returns '' and every nested bundle URL would break.
Verified: both bundles build; each built file contains all three functions
exactly once and still derives currentScript; check:sri, check:series-isolation,
check:studio-rail and check:capability-derived all PASS.
library/utils/embedScript.js is the build owner's under rule 5B. The
plugin-designs and public/ changes are in the same commit because they import it
-- landing them apart breaks the build in between.
Canonicalising the ISO list made the two region painters COMPARABLE. Reading the two sources shows it
cannot make them EQUAL, so the write count would have landed at six rather than three — and the reason
is a real defect underneath, not a tuning problem.
The two sets are different by construction:
· node.isoCodes comes from useWagfRegionsLegendData, which builds its regions by walking the
PEOPLE-GROUP LIST, so it holds only countries that have pins in the current data.
· mapLayers.regionIsoMap comes from regionsData.isoToRegion — every country assigned to the region.
So a region highlight painted from the legend has HOLES wherever a country in that region has no
unreached group in the list, and which of the two a reader saw depended on which writer ran last. The
feature is "highlight the countries belonging to the selected WAGF region", so the regions dataset is
the right answer and the legend's pin-derived list is not.
region: rows therefore stop painting here and defer to the canonical path they already trigger
(selectRegion -> the store watcher -> applyDimFilter). block: and country: rows keep painting from
node.isoCodes, because nothing else paints them — only region: reaches selectRegion — and for those two
the legend's own list IS the scope being asked for.
★ THIS IS A PREDICTION, and it is falsifiable: region clicks should now be 3 writes, not 6. If rega
counts 6, the two sources disagree for some other reason and my reading of them is wrong. If they count
3 but the highlight covers FEWER countries than before, then the pin-derived set was the intended one
and this commit is the thing to revert — the previous behaviour was last-writer-wins, so nobody has
decided which is correct until now.
Verified: gates:pre 13/13. Chained on 1be024ac.
rega's L43 check: three of four pass — the row-click shader compile is 0 ms on both engines (was
3.8 s / 7.5 s), the tab visit is no worse, the highlight stays. The collapse did not: still nine
setPaintProperty calls per row click instead of three.
Their hypothesis is right, and I had the evidence before I wrote the code. The two callers build the
same region from different lists — node.isoCodes and mapLayers.regionIsoMap[slug] — and they do not
agree on ORDER. rega said so in the very first stack trace on this bug: "a case over ['IND',…]" from one
path, "a case over ['MYS',…], a different literal order" from the other. I then wrote a JSON-string
compare, which cannot match two orderings of one set. The pre-warm half worked because that expression
is a shared constant; this half compared text that was never going to be equal.
The list is now canonical before both the compare and the write — de-duplicated, upper-cased, sorted —
so the expression is a function of the SET, which is the thing the two callers actually agree on. Order
is irrelevant to `in` membership, and the case normalisation is a no-op on today's data (both lists come
from the same regions dataset, and the highlight matches now, so they are already upper-case); it is
there so a future source that ships lower-case cannot silently select nothing.
★ IT IS ALSO A TEST, and worth reading as one. If the count drops to 3, the two sources agree about
which countries are in a region. If it does not, they disagree about the CONTENT — a data defect this
compare would otherwise have hidden behind a last-writer-wins repaint, and the next thing to chase.
Verified: gates:pre 13/13. Chained on f7a2f001 (same file). Needs rega: re-count with the same probe.
The claim was wrong when I wrote it and I only checked the built artifact afterwards. jspdf arrives
through useMapPoster.toPdf, which this map still uses for the poster, so the packet path was never the
thing holding it. The bundle in fact GREW about 131 KB (983,264 -> 1,117,687 on L42) when the shared
HTML renderer came in: each bundle is a single .js with no code splitting, so the dynamic imports in
this function are inlined at build time rather than lazy, which also makes the older "keep it off the
critical boot path" note beside them untrue.
Correcting it in the source rather than only in the ledger because the sentence was copied VERBATIM into
country-posters when that map moved to the same renderer — a wrong comment written confidently travels,
and the next person to plan a dependency removal would have started from it.
The change itself stands: the packet prints instead of downloading, and the duplicate implementation is
gone. It cost bytes rather than saving them, and now the file says so.
Chained on 2cf369be because it edits the same function. gates:pre 13/13.
A plugin's <profile>.meta.json targets (web, mobile, desktop, widget; absent = web)
now decide where it is built. internal/plugin-targets.mjs is the one reading.
desktop:build builds a desktop plugin into .desktop/<plugin>/ with a page that
names its engine, builds its backend as a Tauri sidecar for this machine, and runs
tauri build with a --config override; tauri.conf.json and public/ are untouched.
The shell starts the bundled rudder-engine (RUDDER_AUDIO=browser, RUDDER_HOME in the
app data folder) and stops it on exit. A plugin whose targets leave out web
(Rudder: desktop + widget) is no longer built for the web tree: run-parallel,
generate-tree and check-composite-freshness skip it, and its public/ folder is
removed; the dev server still gives the Studio its corner. check:targets
(gates:pre, with a self-test of five planted violations) validates the values,
widget corners and backend folders and fails if a non-web plugin reaches the built
tree. Measured: desktop:build rudder-voice on Linux produced Rudder_0.1.0_amd64.deb
(8.8 MB) with the app and the engine side by side; the app has not been launched.
PACKETSEL, found by exp-shepherd: the prayer packet printed the first 50 of EVERY people group whatever
was selected, so a reader who had narrowed the map to one language family got a packet about somebody
else. I carried that behaviour through verbatim when I moved the packet to the shared printer — it was
already wrong and I preserved it, which is worth saying plainly.
The rows now come from the pins currently SELECTED: the layer's own filter decides which pins render,
`_pinMatchExpr` decides which of those are lit (the affinity tab dims rather than filters), and both
must match — a pin at opacity 0 is not what the reader is looking at.
★ ASKED OF THE MAP'S FEATURES, NOT OF THE ROWS, and this is the part that had to be got right rather
than guessed. Pin properties are DERIVED from rows: useMapLayers fills languageFamily in when the API
returns null for all 2,069 groups, and canonicalizes doxaRegion onto the palette's key forms precisely
so setFilter matches. So evaluating a pin filter against a raw data row is wrong in a way that NEVER
THROWS — it just selects a different, plausible-looking subset. The features the map holds are the only
place the two vocabularies agree.
Answering that question already existed, privately, inside useMapFly: zoomToLegendRow has to solve it to
fit the camera to a legend row, including the reason it cannot use querySourceFeatures (viewport-clipped,
so an off-screen region silently selects nothing). Those three functions move VERBATIM to
library/maps/pin-selection.js and useMapFly imports them, so there is one evaluator rather than two —
and country-posters, which prints the same kind of packet from the same kind of selection, has the same
helper to reach for instead of a second copy.
Two failure modes are distinguished rather than collapsed: pins selected but NO row matching them is an
id mismatch, so it prints the full list and warns; a genuinely empty selection still reaches the shared
printer's own "that list resolved to no records", which is honest. And when the 50-row safety cap bites,
the cover now says "— 50 of 214" instead of quietly being a different packet than the one asked for.
Verified: gates:pre 13/13, check:composite-interop PASS. Needs eyes: rega — select one language family,
print, and check the cards are that family and the cover carries the count.
country-posters-mfe's @element said country-posters-mfe, a tag nothing defines; the
bundle registers country-posters-map, which the embed snippet and leaf page already
use with profile country-posters-mfe. Anything building the tag from the manifest
(check:composite-embed-knobs) mounted an element that never upgraded, so its map
cells could not be measured.
The map's packet built a jsPDF file and downloaded packet.pdf while its poster already
printed. It now goes through usePosterPrints.printPacket, the same path as Simple
Research (8457b5dc): the live map rides in as an image data URL, and an explicit
adapter maps the rows, reading religionName and countryName so a card never prints a
religion code ("MSN") under a people group's name.
Both halves are rega's measurements on main 373b7979b, now that the WAGF selection holds and it is worth
optimising rather than optimising a bug.
NINE WRITES PER CLICK -> THREE. A single "Asia (Region)" click produced nine regions-* paint writes:
fill-opacity, line-color and line-width, three times each inside ~110 ms. Two paths painted the same
highlight from DIFFERENT sources — onSemanticTreeSelect from node.isoCodes, applyDimFilter from
mapLayers.regionIsoMap — each triggering the other through mapStore. Two sources for one visual is not
just wasted work: it is how they drift, and it already cost a debugging cycle, because when the highlight
was being cleared we first had to establish WHICH writer had done it. All four paths (click, watcher,
clear, and the layer-add below) now go through paintRegionScope, which writes a property only when the
value actually differs — asked of the MAP rather than a remembered key, so a layer torn down and re-added
cannot leave a skip holding a paint that is no longer there.
5.5 s AND 11.1 s OF SHADER COMPILE, MOVED OFF THE GESTURE. rega measured the row click compiling a new
variant — 5.5 s MapLibre, 11.1 s Mapbox on SwiftShader — because that click was the first moment these
three properties stopped being constants. Identical to the pins defect and fixed the same way: the idle
value is now ['case', ['has', 'iso_3166_1_alpha_3'], x, x], both branches the same so not one pixel
changes, and it is installed the moment addRegionsLayer returns. The variant therefore compiles on the
tab visit that is already loading a 6.4 MB GeoJSON, not in the middle of a gesture.
That placement is the whole point, and it is what I got wrong on the pins the first time: an idle
expression that is never installed pre-warms nothing. Here it is painted at layer-add, where the layers
first exist.
LEFT ALONE DELIBERATELY: highlightCountryByIso (0.30/0.02) and applyDimFilter's dim path (0.20) are
different states of the same layers, not duplicates of this one, so collapsing them into this writer
would have merged three features into one function to save nothing.
Verified: gates:pre 12/12, check:composite-interop PASS. Needs eyes: rega — expect 3 regions-* writes per
click instead of 9, and no compile on the row click; the compile should appear on the TAB visit instead.
Caught by reading the normalizer instead of waiting for the render: my adapter in f6a9d9d2 read
`g.religion` first, and DataSourceManager normalizes that field to the CODE — so every people-group card
would have printed MSN or BLA under the name, with the readable form sitting unused in `religionName`
one line away (DataSourceManager.js:357-358).
The country line had the same shape of error for the same reason. The renderer prints
flat(g.country_code) as display text next to the population, but the FIELD NAME invites handing it an
ISO code; normalized rows keep the code in countryIso and the label in countryName. Both now read the
readable form first and fall back toward the code, rather than the reverse.
This is the failure the commit before it flagged as the thing that could not be verified headlessly — a
wrongly mapped field prints blank or cryptic rather than throwing. It was findable without a browser
after all: the answer was in the normalizer, not in the rendering.
Verified: gates:pre 12/12. rega's check still stands and is still worth doing — this fixes the two fields
I could prove wrong, not the ones a rendered page would reveal.
Driver-lens audit card 2: the packet button downloaded packet.pdf while its poster sibling, two
functions above it, opened the print dialog. Same map, same menu, two different answers to "export".
The fix is not to wrap that PDF in a dialog. A PDF blob cannot go in the <img> the in-place print frame
uses, and every workaround — embed, object, the browser's own viewer — prints on some engines and
silently does nothing on others, which is the dead-button class this file has already paid for twice.
usePosterPrints.printPacket ALREADY does this: the same packet, authored as HTML with @page rules and
printed in place. This map was running a private second implementation of a shared product feature, with
jspdf behind it. Routing to the shared one deletes the duplicate, drops jspdf from this bundle, and the
packet gains the pages the shared renderer has that the local one never did — the life calendar, the
funnel and the prayer wheel, in the Driver's own page order.
★ AN EXPLICIT ADAPTER, because the mismatch would have been SILENT. The renderer reads slug / name /
population / religion / country_code, and usePosterPrints reads image_url; this map's normalized rows
carry country and imageUrl in camelCase. Handed over unmapped, the packet would have printed with blank
names and missing photos rather than failing — so the mapping sits in one visible place instead of
either side renaming its vocabulary.
Page 1 keeps the LIVE map rather than a fetched poster: this half is the one that knows what is on
screen. The shared renderer interpolates mapSvg as raw markup, so the PNG this map already knows how to
produce rides in as an <img>, as a data URL because the print frame is a separate document and an object
URL created here would be revoked out from under it. If that render fails the packet still prints — a
cover without the map beats no packet, which is the call the shared composable already documents.
Dead code removed with it: the download helper that served packet.pdf, and the lazy-import cache for the
composable this no longer uses. Neither export hands over a file now.
Verified: gates:pre 12/12. NOT verified here, and it needs to be before anyone calls it done: I cannot
see a rendered packet. The specific risk is the adapter — a field I mapped wrongly prints blank rather
than failing. rega should print one and look at the people-group cards before this is released.
The Driver's first laptop run with DX_SYNC_REMOTE: when main moved the page went
white. A landing rewrites dozens of built leaf index.html files under public/, and
Vite answers each changed .html with a full page reload (measured: one edited leaf
page, one full-reload broadcast), so a pull fired a burst of reloads while git was
still writing. The dev server no longer watches public/, which it never serves
through its module graph (measured after: no reload). Rudder's corner pill also
grew with every streamed word; while recording it is now 300px wide and shows the
newest words, cut from the front.
AUDIT34 card 9 (LOW): window.__UPG_MAP__, __UPG_SELECT__ and __UPG_SEARCH__ are
last-writer-wins. Embed two copies of this map on one page and the second mount
overwrites the first, so a gate driving 'the' map silently drives whichever
happened to mount last.
Adds __UPG_MAPS__, __UPG_SELECTS__ and __UPG_SEARCHES__, each keyed by the
embedder's instanceId (the same field poster-wall.vue:55 keys on), falling back
to the profile name.
THE BARE HANDLES ARE KEPT AND STILL POINT AT THE LAST MOUNT, deliberately. Every
existing consumer reads them directly -- internal/check-map-data-painted.mjs plus
~10 call sites across data/visual-gate.mjs and data/palette-preview.mjs -- and
those are browser gates. Removing the bare handles would break them to fix a case
they never hit, since a gate page mounts exactly one copy. This change is purely
additive: nothing reads the registries yet, so no consumer behaviour moves.
The honest limit, stated in the code rather than papered over: two copies with NO
instanceId and the SAME profile still collide, because nothing in the embed
contract distinguishes them. A mount counter would give them unique keys but make
the key unpredictable between mounts, so a gate could not name the instance it
wants -- worse than a key that repeats.
AUDIT34 card 3 (gospel's Driver-lens audit): the on-screen prayer-card fetch
hardcoded &lang=en, so the wall showed English people-group and country names
whatever language the reader picked -- while every word of the interface around
them was translated.
Fixed at three more sites than the card named, all reader-visible:
poster-wall.vue:161 the prayer-card deck (the card's site)
upg-100-list.vue:1983 countryNameMap, which feeds resolveTerritory() -- so
these names are the territory labels and the legend
upg-100-list.vue:2294 the MAIN records fetch: the pins, their popups, the list
The first uses getRuntimeLocale() || detectLocale(), the pattern the sibling
plugins already use (people-groups-grid.vue:276,
research-map-plus-grid.vue:147). The other two use this file's existing
packetLang(), which its two packet fetches already used -- so all four fetches in
that file now read one source.
The printed poster is deliberately NOT changed: generate.mjs fetches lang=en of
its own accord, and whether a print artifact is English-only is an open question
with the Driver (raised by regions-plugin). This is the on-screen surface only.
The Driver's "the research map isn't connecting with the legend", on the WAGF Regions tab. It connected
and then wiped itself 0.4 s later, which is why it read as not connecting at all.
rega's stack trace ended three rounds of my guessing. The highlight paints at +51 ms; at +438 ms
clearAllHighlights runs, reached from applyDimFilter({kind:null}) through the mapStore.selectedRegion
watcher — so selectedRegion had gone FALSY while externalRegion still read "asia". Not the echo guard I
suspected first, not the JS filter scan I suspected second, not the shared legend's nodes reset I fixed
third (that was a real and separate bug). The round trip did it.
A row click emits map-filtered; the composite pushes the state back down as externalFilters; that payload
carries every key it knows — region:"asia" AND language:"". This handler set the region and, one line
later, "cleared" a language that had never been set. mapStore.selectLanguage() nulls selectedRegion by
design (exclusive selection, Bug 13) and does so even when its argument is null, so selectLanguage(null)
is a clear-everything in disguise. The 0.4 s is the debounce plus the trip through the composite and the
grid, which is why it looked like a mysterious timer.
An empty incoming value is now a NO-OP unless that dimension is actually lit. A genuine host clear still
clears — the dimension is lit and the new value is empty — and a genuine host selection still swaps
exclusively, which is what Bug 13 asked for. Guarded on BOTH axes, not just the one that bit: region and
language are symmetric here and fixing one would have left its mirror image.
★ THE SHARP EDGE IS IN THE SHARED STORE, and I am not moving it from here. selectLanguage(null),
selectRegion(null) and their siblings each null the other dimensions unconditionally, so every caller that
means "clear this one axis" is silently clearing all of them. That is load-bearing for other maps and
changing it belongs to whoever owns the store, so this fix is profile-local and the edge is recorded
rather than patched from a plugin lane.
Verified: gates:pre 12/12. Needs eyes: rega — the WAGF "Asia (Region)" click should now still be lit at
5 s, with the legend row keeping its state class.
The Driver: red dots on a mostly green map look wrong. A dark slate dot (#1e293b) with the existing
white ring reads on every fill the map uses, the green/amber/red/grey country bands and India's
pale-to-deep blue states, and it carries no good/bad meaning (red is this map's 'under 25%').
Candidates compared side by side on the world, India and West Africa views before choosing.
Rebuilt cities-internet and regenerated the tree (integrity hashes).
The ROP Tree and Language Families legend tabs (and the 'Go up to …' crumb built
from them) were typed in English; they now read the existing tabs.* catalogue
entries. ROP25 stays as the registry's name.
pplr carried private copies of SemanticTreeLegend and usePplrInstance that had drifted.
The instance copy lacked activeTabTouched, so the legend tab snapped back to its
default after a pick; the legend copy spoke English accessibility labels in every
locale. Both are deleted and the views import the library versions.
The gallery added every experimental bundle as a tab, so Rudder's engine probes
(/api/rudder/status, /devices, /events) logged three 404s on every embed of the
gallery. The manifest now carries each profile's meta targets, and the gallery
skips a bundle whose profiles are all corner widgets or declare targets without
"web". Rudder itself contacts its engine only where one can exist: the dev
server, the desktop app, or an explicit apiBase.
40 console.log calls (fetch progress, grid sizes, sample rows, API stats) ran on every
load; public-hygiene flagged them. Warnings and errors are kept.
70a9ce16 gave the cleared state a data-driven shape and stopped there, which was half a fix. The pins
layer is ADDED with a constant "circle-opacity": 1, and applyPinOpacity is otherwise first reached from
a legend click — so the new expression was only ever the CLEARED state, and the first selection was
still the moment MapLibre needed a new shader variant. The 3.2-3.5 s freeze rega profiled would have
survived the fix meant to remove it.
One line: applyPinOpacity(map.value) in the map-ready path, right after the layer is added. matchExpr is
left UNDEFINED on purpose — "re-apply what is current" — so a selection already restored from a
share-link hash is preserved rather than cleared at boot.
★ HOW IT WAS FOUND, because the method is the point: reading the BUILT bundle, not the source. The
addLayer call with its constant opacity is right there in the shipped JS, next to the pre-warm
expression I had just added — two facts that cannot both be true of a working fix. I had been about to
hand rega a tree to measure and would have read a still-slow first click as "the engine folds the
expression away", which is the wrong lesson and a day of chasing it.
Verified: gates:pre 12/12. Unchanged: this is still zero visual difference — both branches of the
boot-time expression are the same alpha.
The Driver: on Simple Research the research map is not connecting with the legend. It was connecting —
two seconds later, which a reader correctly reads as "nothing happened".
I was wrong about the cause twice, and both wrong answers were cheap to discard because they were
measured. First guess: a late echo through applyDimFilter's 80ms dedupe, whose toggle branch would read
the echo as a second click and clear the highlight. rega instrumented the pin layer every frame for 2 s:
ZERO 'legend:highlight' events on every click and no apply-then-revert anywhere. Second guess: the JS
filter scan in zoomToLegendRow, which walks 2,106 features evaluating ['in',['get','uniqueId'],
['literal', ~700 ids]] linearly. Benchmarked at the real shapes: 13.9 ms per full scan, 4.4 ms with a
memoised Set. Two orders of magnitude short, so nothing there was "optimised".
rega's CPU profile named it. ONE task of 3.2-3.5 s, self time getShaderParameter 3,583 ms +
getProgramParameter 551 ms: the GPU compiling a NEW SHADER VARIANT. The click was the first moment
circle-opacity stopped being a constant and became a per-feature expression, and a data-driven paint
property needs a different program. The second click on the same map costs 205 ms — the program is
cached — which is the signature of a one-time compile and not of anything in our logic. The legend's DOM
was never involved: 34 rows before the click, 34 after.
So the fix is not a smaller expression. It is that the first SELECTION must not be the moment the
variant is first needed. With nothing selected the value is now ['case',['has','uniqueId'], a, a] —
both branches identical, so not one pixel changes — and circle-opacity is data-driven from the first
paint. The program compiles during boot beside every other one, and a selection afterwards only
re-uploads attribute values.
The trade, stated rather than hidden: boot compiles one more program. On a real GPU that is tens of
milliseconds; rega measured seconds only because that box rasterises on the CPU through SwiftShader.
Paying it at load beats freezing the map mid-gesture, and the variant was going to be compiled on the
first interaction anyway.
Second finding from the same profile, fixed here too: circle-opacity was being rewritten 4-8 times for a
single click (+1.52, +1.57, +1.94, +5.17 s) because the selection handler, the glow sync and the slider
watcher all end up in this one function, and every write re-uploads the per-feature attribute. It now
writes only when the value actually differs — asked of the MAP rather than of a remembered key, so a
layer that gets torn down and re-added cannot leave the skip holding a dim that is no longer painted.
Verified: gates:pre 12/12 (check:setup-order included, since this touches module-scope state next to a
function that reads it). Needs eyes: rega, the same affinity click — expect the dim at the first frame
and one long task at BOOT instead.
rega, ?lang=ar on landing 38: "People's Republic of China" and "Lao People's Democratic Republic" were
still English in the top-countries chart. The API labels some countries with their FORMAL name, and
library/data/countries.json does not carry those — it says "China" and "Laos" — so reversing the English
name missed precisely the countries whose names are longest. Measured: both miss the name table, and
both resolve through the row (CHN -> CN -> الصين, LAO -> LA -> لاوس).
Every row already carries both halves: the label the chart aggregates on, and the alpha-3 in
country_code.value — the same accessor the choropleth joins on. Reading the pair out of the data is
exact and needs no alias table. The name table stays as the fallback, so a label whose rows are
currently filtered out still resolves, and a synthetic key ("Other") resolves to neither and renders as
itself.
Deliberately NOT a looser text match. "Democratic Republic of the Congo" and "Republic of the Congo" are
two countries, so stripping formal prefixes is how one would silently become the other — the failure
countryName.js's own header warns about, and the reason that file refuses to guess.
Still English, and not fixable from CLDR: the regions "Middle East" and "North America & Non-Spanish
Caribbean". M49 145 is "Western Asia" and 021 is "Northern America"; neither means the region we draw,
so naming them from CLDR would redraw the region rather than translate it. They need a translation
source, which is the Driver's call — the same class as the affinity blocs and language families.
Verified: check:sfc-parse PASS. Needs eyes: rega, ?lang=ar top-countries.
rega's load waterfall: the engine was requested only after the component tree
had rendered, and the dashboard's 384 KB country boundaries only after the whole
basemap style had loaded, so each waited on the other. The engine download now
starts when the map composable is set up, and the boundaries when the dashboard's
map panel mounts; the style handler awaits that same download.
The rail showed Gospel x Internet as "board": a row's name fell back to its
profile id unless a hand-kept DISPLAY_NAME table had an entry. The manifest now
carries each profile's .meta.json label (or title / displayName), and the rail
shows it without a trailing aside in parentheses; with no label it shows a name
made from the ids ("Gospel Resources · Charts"). DISPLAY_NAME is an override
only and is empty. check:studio-rail fails if any row would show a bare id.
The Driver, 2026-09-28: an export-poster button must open the PRINT DIALOG with a poster preview,
never a direct download. Simple Research was doing exactly the forbidden thing — render a PNG, hand it
to a download helper — so a reader got doxa-prayer-poster.png in their downloads folder with no
preview, no paper size, and no way to print it without opening it somewhere else first.
The dialog IS the preview. printImageBlob() puts the rendered poster on a sheet of exactly the spec's
size (@page 8.5in x 11in, margin 0 — the renderer already composed the margins in) and raises the
dialog over the map. It goes in library/composables/printInPlace.js, which is THE print mechanism here
and already carries the Driver's earlier ruling that killed the pop-up path: an iframe cannot be
blocked and needs no user gesture, so the same call works for a human click and for an agent.
The ellipsis comes back on the label. It was stripped while the button handed over a file directly,
and an ellipsis promises a dialog — which is now what happens.
★ AND THE GATE ASSERTED THE OLD RULING, which is the part worth naming. check-export-one-click passed
while the button did the thing the Driver has now ruled against: it waited for a download and checked
the PNG's dimensions. A gate that encodes a superseded ruling does not just miss the defect, it defends
it — the next person to "fix" this button would have had to break a green gate to do it. It is replaced
by check-export-poster-prints, same four cases (leaf and gallery, en and ar), asserting the new
contract: one click, some frame calls window.print() within 120s, the printing document is the
1700x2200 poster on an 8.5in x 11in sheet, and NO download arrives. window.print is stubbed in every
frame — the native dialog is modal and would hang the run — so this measures everything up to the
dialog and not the browser's own chrome, which is the right boundary for a headless gate.
Why the download half is asserted separately: a button that opens the dialog AND saves a file still
breaks the rule, and a download that lands late is still a download, so it is judged over the whole run
rather than at a moment.
Verified: gates:pre 12/12, check:doc-scripts and check:no-credentials-omit PASS with the rename.
NOT verified here: the new gate needs a browser, and this agent does not start one — rega runs it.
rega's ?lang=ar pass (KA3, 3d64bcad) found English on a localized map in four places, and the census
behind it explains why no gate saw them. Catalogue coverage is COMPLETE: of the 760 keys my bundles
name across the 21 areas they reach, ZERO are missing from any of the ten non-English locales, and the
per-locale English-identical values are numerals, acronyms and cognates rather than debt. Everything
rega actually read was a string no catalogue ever held.
· THE PROJECTION PILL said "🌐 Globe" / "▭ Flat" in all eleven locales. The icon is now the label.
Neither word has a catalogue entry and authoring ten new strings for a two-state toggle to say what
a glyph already says is the wrong trade — but mapui.enableGlobe / mapui.disableGlobe DO ship in all
eleven, as the ACTION, which is what a button should announce. They become the accessible name and
the tooltip. min-width joins min-height on the pill: the 24px used to come free from the text.
· THE TABLE FOOTER said "Σ 2,107 groups". dashboard.groups has shipped in eleven locales all along
(ar "مجموعة") and this one call site never read it. Written with the same singular/plural test as
the header count so the two cannot disagree at one row.
· THE REGION CHART labelled its bars with the prayer API's English region names. Seven regions is a
closed set, so they are now CATALOGUED — five generated from vendored CLDR by
internal/generate-wagf-region-names.mjs, which is the same data countryName.js already trusts.
Generating is what keeps seventy translations out of my hands. middle_east and
north_america_&_non-spanish_caribbean stay English: M49 145 is "Western Asia" and 021 is "Northern
America", and neither means the region we draw. An English label a reader can match to the map beats
a localized one that names the wrong territory. --check verifies the catalogue against CLDR.
· TOP COUNTRIES labelled its bars with the API's English country names. By aggregation time the
alpha-3 is gone, so countryName.js gains iso3ForEnglishName() — reversing its OWN table, exact where
it matches and silent where it does not — which is precisely the ✓ case its header describes.
One thing found while wiring those: the BAR label was the only chart surface still printing the raw
facet key. The donut, its legend and both tooltips already went through legendLabel(); the bar list did
not, which is why these labels stayed English after the donut was fixed for rega's earlier survey. Both
new facets are projected there, so the two charts cannot disagree about a name again.
English is left EXACTLY as authored. Both projections return the key untouched at locale 'en', which
also keeps the 42 countries whose CLDR name differs from ours reading as they do today. Nothing
localizes a VALUE: data-key, isActive() and every click and filter path still carry the canonical
English, the rule the religion donut already followed.
Verified: gates:pre 12/12, check:i18n-keys-exist / parity / never-translated PASS, and the generator's
own --check PASS. Rendering on ?lang=ar goes to rega.
check:public-hygiene reports role nouns in doxa-maps-spa/manifest.json.
studio guessed a meta.json description; it is neither that nor the SPA's.
The manifest generator extracts each element's @description from its SFC
header, and both leaking entries are my files: region-dashboards.vue and
regions-mfe.vue.
Rewritten to describe the element rather than who asked for it. Verified by
regenerating the tree: doxa-maps-spa/manifest.json goes 2 occurrences to 0.
Found the same way as the last two: match the shipped text back to source
rather than reason forward from the bundle name.
regions-plugin challenged the record I had just written, and they were right. I gave the 7 explicitly-named
ShareAlike countries their own field because share-alike is a stronger duty than attribution — and by that
same reasoning the field should hold 65. Open Data Commons ODbL 1.0 is itself a SHARE-ALIKE licence for
databases: alongside attribution it requires a Derivative Database be offered under ODbL, and it carries a
keep-open provision. So my 58 ODbL countries are the same class as my 7, and separating them buried exactly
what the field existed to surface.
★ WHAT CONVINCED ME WAS THIS REPO, NOT THE LICENCE TEXT. regions pointed at
library/scripts/posters/data/README.md:36-50, and I read it: Natural Earth's 1:10m ROADS were requested and
DELIBERATELY NOT SHIPPED because of an ODbL attribution obligation. Someone here already dropped a whole
dataset over this class of duty. An argument from the licence would have been my reading of a clause; an
argument from a decision already taken in this tree is evidence.
THE RECORD NOW SPLITS THREE WAYS, with the iso3 lists so each is checkable:
32 duty-free (28 Public Domain + 4 CC0)
65 attribution-only
65 SHARE-ALIKE CLASS (58 ODbL 1.0 + 7 explicit ShareAlike)
That is a materially different picture from "130 need attribution, 7 are stronger": it says HALF the
duty-bearing countries may constrain how a derivative is licensed, not merely how it is credited.
⚠ AND IT IS EXPLICITLY NOT A LEGAL CONCLUSION. Whether a redistributed boundary file is a "Derivative
Database" under ODbL is precisely the clause a giveaway needs a real answer on, and neither regions-plugin
nor I is qualified to give it. The record says so in _duty_classes._open and the question goes up with the
other licensing items — visible rather than settled by omission, which is the same failure the empty
`licences: {}` was.
The table stays here as the single source of truth; regions' data/boundaries/ATTRIBUTION references it by
path rather than copying it, because one table in two places drifts.
regions-plugin's third-party register (their ATTRIBUTION landing) flagged this row as mine, and they were
right: my own PROVENANCE.json already said "gbOpen passes through each country UPSTREAM licence — this
collection is MIXED-LICENCE with per-country attribution duties" and then left `licences: {}` empty. The
duty was NAMED and not DISCHARGED, which is the worst of both — it reads as handled.
★ THE NUMBERS ARE WHY THIS MATTERED, and I did not expect them to be this lopsided:
130 of 162 countries carry an ATTRIBUTION or SHARE-ALIKE duty
32 are duty-free (Public Domain 28 + CC0 4)
58 Open Data Commons ODbL · 44 CC BY 4.0 · 13 CC BY 3.0 IGO · 7 ShareAlike · 2 OGL v3 · …
The repo is meant to be given away, so those duties travel with boundaries/world-adm0*.geojson. Shipping
the file does not discharge them; naming the source does.
⚠ THE 7 SHARE-ALIKE COUNTRIES GET THEIR OWN FIELD (AGO GAB GEO GIN MMR NER TUR) because ShareAlike is a
STRONGER duty than attribution — a derivative may have to be licensed alike — and a blanket "mostly CC BY"
summary would hide exactly the ones that constrain a giveaway.
METHOD: one query per country against the endpoint the file already documents,
https://www.geoboundaries.org/api/current/gbOpen/<ISO3>/ADM0/, throttled at ~3/s, recording
boundaryLicense / licenseSource / boundaryID / boundarySource verbatim. 162 fetched, 0 failures.
Spot-checked SDN against the live API after writing: byte-identical. No licence is inferred — regions'
point that "guessing a licence is worse than recording none" is exactly right, and every value here came
off the wire.
⚠ THE OLD "CCK: meta HTTP 404" GAP IS RESOLVED AS OBSOLETE, NOT FIXED, and the distinction is recorded in
the file. CCK is not in world-adm0.geojson at all — 0 features, absent from the ISO list — so that 404 was
a fetch for a country that never shipped. It took three checks to establish: `.get('CCK',{})` returned None
and I read it as "null licence", then as "failed fetch", before asking whether the key was there at all.
The gaps list is now genuinely empty rather than empty-because-unmeasured.
The production bar's cell E names en, ar, zh, ru and hi; the render check
measured two. It now server-renders the board in all five and requires
each panel to render with no error and the board's text to be the
locale's, not English.