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.
check:public-hygiene is the white-label gate for the giveaway, and translator-org-links.json
shipped an internal role noun: the held-Bible-Society reason read "...is the Driver's ruling, not
this script's". A recipient reading our data files should not meet our process vocabulary. It now
reads "a product ruling", which keeps the meaning exactly — the point was always that this build
will not decide a doctrine question on its own.
Fixed in the script that writes the table, not the artifact, or the next build would put it back;
the same phrase in that file's header comment went too, rather than waiting to be flagged
separately now studio has closed the comment half of this class.
Regenerated by re-running the verifier: 16 of 16 organisations answered again, so the table is
current rather than hand-edited. My published tables now carry no internal vocabulary — checked
with word boundaries across all of them, which is also how I learned the six "rega" hits reported
in my sweep were the word "aggregates".
The join table's never_read note said 'pending the Driver'; it now says
'pending a product decision' (check:public-hygiene, the giveaway's
white-label gate). Fixed in the builder that writes it.
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.
At 390px the toolbar was one sideways-scrolling row with no scroll cue, so Delete
selected, Select all, Clear selection and the count ran past the edge. On a phone
Draw, Print, More and the count now fit one row, and More opens the three list
actions as a row beneath; desktop is unchanged. "More" in all eleven languages.
Two status lines formatted their counts with an inline en-US; they now call
formatCount, the one place counts are formatted (fixed en-US while numerals are
held for the Driver), so the only remaining LOC-ENUS site is the library's own.
When the data arrived while the map was still loading tiles, the paint was skipped
and never retried, so Mobilize opened on an empty map (and Campaigns could miss its
dots the same way). A skipped paint now retries once the map is idle.
A server where Rudder is laptop-only (RUDDER_ENGINE=0) no longer starts a
leftover engine binary. If an engine that records its own machine's microphone
is reached from another machine anyway, Rudder shows the laptop notice and
disables Record instead of recording the server.
The dev backend answers /status with the same offline status as /events, and
Rudder reads it on mount and whenever the event stream drops, so a proxy that
buffers the stream no longer leaves it saying "Runs on your computer" on a
server that should run the engine. The local-microphone notice no longer shows
off this computer. The legend progress bar paints its grey band as a rounded
strip and rounds the first and last segment, which the 24px touch targets had
squared off.
The dev backend reports expected=true while an engine the server is set up to
run is down (RUDDER_ENGINE=1 or RUDDER_AUDIO=browser). Rudder then reads "Not
running yet" and says the engine is starting or failed to set up; "Runs on
your computer" remains for a server with no engine at all.
generate-tree derives each profile's tier from its composes edges (composing
anything is capability 4; composing a capability-4 plugin is capability 5). An
explicit type is checked against the derivation and a contradiction fails the
build naming both. check:capability-derived asserts every profile is 1-5 and
agrees. documentation/PRESET-PLUGIN-STANDARD.md describes the one
*.instances.json file a preset plugin declares.
Every plugin is capability 1 to 5, so the rail files each bundle under its
highest capability again. A bundle not yet in production says Experimental on
its own rows. check:studio-rail now asserts there is no section outside 1-5.
The Experimental section is a stage, not a tier, so its heading showed CAP 0
and the Research Gallery (capability 5) looked like a capability-0 plugin. The
heading now reads EXP and each of its rows names its own capability. Clicking a
plugin opens it without unfolding its folder or its parts; only the twisty
folds and unfolds, and a closed folder that stands for the plugin stays closed.