Commit graph

2385 commits

Author SHA1 Message Date
Doxa Maps
cb72f1b786 Rebuild country-posters, and regenerate the tree
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 07:27:12 +00:00
Doxa Maps
97903f756a Country posters: pre-warm skips only paints that already read the feature
An array is not the same as data-driven: a zoom-only interpolate is an array and
still compiles a variant on the first gesture. The warm-up now leaves alone only values
that read a feature property, and wraps constants and zoom-only expressions.
2026-09-28 07:26:59 +00:00
Doxa Maps
4b941be4bf Country posters: the packet prints the selected groups, not the first fifty loaded
Choosing "Asia" in the legend printed the same first 50 groups as any other row,
Sudan included. The packet now takes the pins the selection shows, through the shared
library/maps/pin-selection.js (reading pins as drawn, not raw rows). Selected pins
that match no rows fall back to the full list with a warning; an empty selection
reaches the printer's own error; over 50, the cover says '50 of N'.
2026-09-28 07:26:58 +00:00
Doxa Maps
16b2e7aeaf Country posters: the first country highlight no longer freezes on a shader compile
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.
2026-09-28 07:26:58 +00:00
Doxa Maps
7c360016fa Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: dashboard-plus-grid
doxa-maps-spa
doxa-research-map
gospel-resources
plugin-studio
research-map-plus-grid
stack-demo. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-28 07:16:22 +00:00
Doxa Maps
e020e68bca Skip a paint that is already data-driven, not one that merely looks like an expression
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.
2026-09-28 07:07:34 +00:00
Doxa Maps
f83b3a195d The pre-warm was repainting the borders it was only supposed to warm
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.
2026-09-28 07:07:34 +00:00
Doxa Maps
35955a4db6 check:one-palette sees a bundle's private src/colors/ palettes; Rudder's offline hint states Go 1.22+ and the setup
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.
2026-09-28 07:07:33 +00:00
Doxa Maps
4ce8604d42 Both library layouts load nested bundles through the shared embedScript, which fixes VerticalTabsLayout on the dev server
VerticalTabsLayout kept a private script loader without the dev-server ES-module
rule, so a bundle nested through it under bun run dev loaded as a classic script,
reported success, and never defined its tag. It and StackLayout now call
library/utils/embedScript.js loadScriptOnce; StackLayout keeps its own
resolveBundleSrc step first. loadScriptOnce gains an empty-url no-op (a
<script src=""> re-fetched the page) and carries StackLayout's stack-demo
measurement beside the gallery's. One copy of the pattern and one cache.
2026-09-28 07:07:33 +00:00
Doxa Maps
d0939c0492 library: one embedScript for the app shells, replacing four private copies
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.
2026-09-28 07:07:33 +00:00
Doxa Maps
15b8cb4756 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: doxa-research-map. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-28 06:55:58 +00:00
Doxa Maps
2bbabaa72b A region's countries come from the regions dataset, not from whichever ones happen to have pins
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.
2026-09-28 06:44:14 +00:00
Doxa Maps
c0c1a72851 The write compare was comparing two orderings of the same set
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.
2026-09-28 06:44:14 +00:00
Doxa Maps
16c98acfe9 A comment of mine said this dropped jspdf from the bundle; it never did
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.
2026-09-28 06:44:14 +00:00
Doxa Maps
c73d713b43 Delete useMapPacket.js: nothing imports it since both packets print through the shared HTML renderer
research-map and countries-map moved to the shared printer, leaving the
composable with no importer on main. jsPDF stays: useMapPoster.toPdf,
posterSizes and my-upg-100-list still reach it. The file map row goes and the
library index is regenerated.
2026-09-28 06:44:14 +00:00
Doxa Maps
6451a2b158 bun run desktop:build <plugin> packages a desktop plugin as an app; check:targets keeps non-web plugins out of the web build
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.
2026-09-28 06:44:14 +00:00
Doxa Maps
0d21354251 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: country-posters
dashboard-plus-grid
doxa-research-map
doxa-simple-map
research-dashboard. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-28 06:36:49 +00:00
Doxa Maps
2ea3c53e0f The packet prints what the reader selected, and the question "which pins is that" becomes shared
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.
2026-09-28 06:29:49 +00:00
Doxa Maps
4f4dc9f3e4 Country posters: the manifest names the tag the two-tab element really registers
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-28 06:13:32 +00:00
Doxa Maps
ddd5211be0 Rebuild country-posters, and regenerate the tree
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 06:06:09 +00:00
Doxa Maps
d8ee9e9787 Country posters: the packet prints through the shared renderer instead of downloading a PDF
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.
2026-09-28 06:05:04 +00:00
Doxa Maps
d8c67ca28a Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: doxa-research-map. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-28 05:53:01 +00:00
Doxa Maps
f8f81b9bcb One writer for the region polygons, and its idle state compiles the shader at layer-add
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.
2026-09-28 05:46:36 +00:00
Doxa Maps
f7dbac3731 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-internet
gospel-resources
my-upg-100-list
plugin-studio
pplr-data-maps
prayer-walk-planner
regions-dashboard
research-dashboard
template-bundle
upg-neighbor. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-28 05:40:30 +00:00
Doxa Maps
e53df81cb5 check:poster-action-print also pins PosterDialog's print default and its download opt-ins 2026-09-28 05:30:58 +00:00
Doxa Maps
0a713df497 The packet card prints the religion's NAME, not its code
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.
2026-09-28 05:30:58 +00:00
Doxa Maps
7568c12610 The packet prints too, through the shared renderer this map was quietly duplicating
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.
2026-09-28 05:30:58 +00:00
Doxa Maps
cc6e7cb6c4 PosterDialog prints by default instead of downloading, like PosterWall
The poster dialog had only a download path: its Render button saved a file, the
same trap PosterWall's old default was (gospel's Driver-lens audit, card 7).
A new action prop defaults to 'print': a PNG poster opens the print dialog in
place (printImageBlob, sized to the chosen page), a PDF opens in the browser's
viewer, and a blocked print frame or popup falls back to the file. 'download'
stays an explicit opt-in.
2026-09-28 05:30:58 +00:00
Doxa Maps
47cfc6ed34 Legend: "Go up to …" is read in the reader's language
The parent crumb's title and aria-label were typed in English inside the library
legend; they now read legend.a11y.goUp, in all eleven languages.
2026-09-28 05:30:58 +00:00
Doxa Maps
5f053d992c The galaxy basemap was localizable all along: its label keys use an underscore, not a colon
exp-shepherd found research-dashboard rendering ENDONYMS in every locale — 中国 and Türkiye on a Russian
page — and fetching a glyph range for every script on screen. The reading was that its basemap carries no
localized names and the choice was to swap the basemap or give up on its labels. Measured against the live
tiles, neither is necessary: the names are there, under keys this chain never tried.

The galaxy style is VersaTiles Colorful on the SHORTBREAD schema, whose label keys are name_ar / name_de /
name_en, not name:ar. This coalesce only tried the colon spelling, so all 43 of that style's label layers
fell through to `name` — the local name — while the localizer ran and reported success. A running fix that
cannot reach its data looks exactly like a missing feature.

MEASURED, six z3/z4 tiles across Europe, Asia and Africa, identical key sets on every one: name_ar,
name_de, name_el, name_en, name_es, name_fr, name_it, name_nl, name_pl, name_pt, name_uk. Of the eleven
locales we ship that is SEVEN in their own language — ar, de, en, es, fr, it, pt — with no change to the
basemap the Driver chose and no change to how the map looks.

hi, ro, ru and zh are genuinely absent from those tiles, so they now take name_en: one Latin script rather
than mixed CJK and Devanagari endonyms, which is also the single-script glyph saving. That is the same role
name:latin already plays for the OpenFreeMap styles, so it sits in the same position in the chain rather
than becoming a special case.

NOTHING CHANGES FOR THE OPENFREEMAP STYLES, by construction: the colon spelling stays first, so their
name:<lang> or name:latin still wins and the underscore entries are never reached. The order is also what
keeps the already-rewritten sentinel working — it tests for a chain beginning ["coalesce",["get","name:,
which this still is. Verified both, rather than assumed.

WHAT I DID NOT DO: swap the basemap. galaxy resolves to VersaTiles Colorful because the Driver asked for
blue oceans like the Mapbox map and that style's water measured closest (#bfd9f2; OpenFreeMap Liberty is
#9ebdff, noticeably stronger). Trading a look he chose for four locales' labels is his call, not mine, and
with this change those four get readable Latin labels anyway.

Verified: gates:pre 12/12. NOT measured here: glyph KB per locale, which needs a browser — rega has the
instrument and the before numbers.
2026-09-28 05:30:58 +00:00
Doxa Maps
daffaa6ef5 Dev server: a pull no longer reloads the page in a burst, and Rudder's recording pill keeps a fixed width
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.
2026-09-28 05:30:58 +00:00
Doxa Maps
f3e29d026b UPG-100: test handles are also keyed by instance
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-28 05:27:38 +00:00
Doxa Maps
7b7b8a4ac3 README: a catalyst or mobilizer can also find gospel resources for the people they reach (Gospel × Internet)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 05:22:54 +00:00
Doxa Maps
6c46b7317e UPG-100: people-group data follows the reader's language, not English
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-28 05:11:14 +00:00
Doxa Maps
11f0f4f5fc packet: country mode needs the country zone source, not just the wording
exp-shepherd measured on main: SERIES=country ONLY=country-yem made a POSTER,
and the matching packet.mjs run died with 'no zone matches ONLY=country-yem'.
The packet half of SERIES=country could not run for any country.

Cause: allZones() built zones from clusters.json -- the UPG-100 zoning -- where a
country id never appears. generate.mjs got its country zone source from
countryMode in the previous commit; packet.mjs did not. The same 'one concept,
two switches' defect I had just removed from one file survived in the other.

MY OWN TEST MISSED IT BECAUSE IT WAS NOT END TO END. I ran the country packet
against a UPG-100 zone id (ONLY=west-africa), which exercised the WORDING and
never the zone source, then reported the parameter verified end to end. Running
the actual pairing is what found it, and exp-shepherd did that, not me.

allZones() now returns the country zones under countryMode, built to match
generate.mjs's country branch exactly -- same ids, labels, adm0 and the PLURAL
country_codes selector shape -- so a poster and its packet cannot describe
different zones. Same two-path country-list lookup and the same error naming the
command that writes the file.

Verified: country-yem builds 8 groups / 6 pages, SERIES-ISOLATION PASS with 9
country markers and zero UPG-100 framing; the UPG-100 default is unchanged at 12
pages and 13 markers; all four packet gates PASS.
2026-09-28 05:10:15 +00:00
Doxa Maps
11129455e4 posters: SERIES is the one switch; header wording joins it; country mode fixed
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
rega read the rendered page and found a fourth leak site my gate had passed: a
SERIES=country poster still printed 'UNENGAGED PEOPLE GROUPS', 'Unengaged of West
Africa', 'Series: List of 100 UPG's · map 11 of 28' and a side box calling it one
of a 30-map set. I had reported that PASS to three agents as proof the render was
clean. It was not proof: the gate's vocabulary was a HAND LIST of the phrases I
had already found, so it could not see framing I never enumerated.

ONE SWITCH (dx-shepherd's call). SERIES=country now implies the country zone
source, the series name and the header strings. COUNTRIES=1 is kept as an ALIAS
that resolves to SERIES=country, so runs that predate series.json keep working
and now get the matching packet too. Two parameters for one concept is exactly
what let rega's render take the new packet path with a UPG-100 header.

The five header strings move into series.json: kicker, title qualifier, series
name, side-box heading and body. One file owns the wording.

THE GATE NO LONGER DEPENDS ON WHAT I HAPPENED TO KNOW. Its vocabulary is DERIVED
from series.json -- every string a series declares becomes that series' own
fingerprint, so adding wording polices it from that moment. Strings shared
between series are dropped (they identify nothing) and a 12-character floor keeps
short phrases from matching innocent prose. The hand list is kept and merged for
phrases no series declares, like 'Project 7000'.
It also strips HTML and CSS comments before matching: generate.mjs ships design
notes into the SVG that QUOTE these strings, and a correct country poster failed
on five of them. A detector that does not know the comment syntax of what it
reads attacks the documentation of the thing it protects.

FOUND WHILE WIRING IT, a pre-existing break: COUNTRY MODE HAS NOT RUN AT ALL.
generate.mjs read plugin-designs/country-posters/data/countries.json, but
ddde518a8 moved that data to library/data/country-posters.json and the reader was
never updated -- no file at that path on main, so every COUNTRIES=1 run died on
ENOENT. Both paths are now tried, newest first, with an error naming the command
that writes it. Country mode builds 173 countries for the first time.
(node --check passed on the fix while existsSync was still unimported -- a parse
check is not a will-it-run check, and the free variable sat in the one branch my
tests had never reached.)

Verified: country header now reads 'Unreached People Groups / Pray for Republic
of The Gambia / What is a Country Prayer Map?'; the UPG-100 default header is
unchanged; the stale poster still fails as country with 11 real hits, so the
comment strip did not blind the check; COUNTRIES=1 alias renders correctly; all
four packet gates PASS.
2026-09-28 04:57:44 +00:00
Doxa Maps
70cb1b3ed3 packet: refuse to build around a poster of a different series
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The ordering rule 'regenerate the poster with SERIES=country BEFORE the packet'
was a fact about the build living in a human's head. Documentation does not hold
that kind of fact: the same bet already failed for 'regenerating the tree alone
ships a stale .js', which is written down in the brief and still happened.

The hazard is silent and I walked into it while building the series parameter.
The three below-map blocks are DRAWN INTO the poster's SVG by generate.mjs, and
the packet lifts that SVG whole, so the text reaches the packet through the
IMAGE -- no string file mentions it and no grep of packet-strings.json can see
it. A country packet built against a leftover UPG-100 poster prints 'Thirty
disciples' on page 1 with no error, the right file size, and a generator
reporting success. It looks exactly like the SERIES parameter does not work.

So the build asserts it instead of remembering it. The packet reads the poster's
series out of the artwork it is about to lift -- fingerprinted by the NETWORKS
headings, which are the one marker that survives every stage of the pipeline into
the artefact -- and exits 1 with the remedy command when it disagrees with its
own SERIES. Suggested by regions-plugin, whose two near-misses today were the
same class: assert against the ARTIFACT with a marker that survives the pipeline.

Verified in all four directions: country packet vs upg100 poster FAILS, upg100
packet vs country poster FAILS, and both matching combinations build clean. An
unrecognised poster is deliberately not an error -- that is the existing 'no
<svg>' branch's failure, not this one's.
2026-09-28 04:51:50 +00:00
Doxa Maps
373b7979b1 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: country-posters
doxa-research-map
key-cities
my-upg-100-list
regions-dashboard. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-28 04:47:19 +00:00
Doxa Maps
5673c209d1 The map was undoing its own selection: an empty external value cleared what the same payload had just lit
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.
2026-09-28 04:41:00 +00:00
Doxa Maps
6778a0828f A poster button opens the print dialog by default; downloading is an explicit, explained opt-in
Driver rule 2026-09-28: an export-poster button opens the print dialog with a
preview, never a direct download. PosterWall defaulted posterAction to
'download', so country-posters and key-cities, which set nothing, downloaded.
The default is 'print'. check:poster-action-print (gates:pre) keeps the default
and fails a consumer that opts into 'download' without a
"posterAction-download: <why>" comment. PDF packets and CSV exports are not
poster buttons and stay downloads.
2026-09-28 04:41:00 +00:00
Doxa Maps
fe88f90f49 Cities with Internet: people-group dots are dark slate, not red
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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).
2026-09-28 04:38:32 +00:00
Doxa Maps
b23ecd4f64 Manifest: the Gospel x Internet label
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 04:31:15 +00:00
Doxa Maps
a57dec789d Gospel x Internet: the Studio rail names it 'Gospel × Internet' 2026-09-28 04:31:15 +00:00
Doxa Maps
ded58aa2cc Cloud container: subdomain apps, a published plugin as its own public site
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
SUBDOMAIN_APPS=host=bundle/profile makes e.g. research-gallery.<domain> serve the /cdn build of
doxa-maps-spa at its root. A separate server on its own port answers only those hosts and only with
published /cdn files (no admin, PocketBase or dev server behind it), so no sign-in is skipped anywhere
else. The root page gets a <base href> at the plugin's real folder, so its relative links resolve as on /cdn.
2026-09-28 04:28:03 +00:00
Doxa Maps
12d7f218f3 posters: a SERIES parameter, so a country poster stops rendering the UPG-100 packet
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The country posters shipped carrying UPG-100 framing: the UNENGAGED PEOPLE
GROUPS kicker, 'This packet is 1 of {n} UPG-100 lists', the Project
2000/7000/4700/700 narrowing funnel, and a below-map block telling the reader to
divide the set among THIRTY disciples -- coherent for ~30 UPG-100 zones and
incoherent for ~170 countries. Reported by the Driver.

Not a mislabel: there was NO series parameter anywhere, so 'the packet' and 'the
UPG-100 packet' were one thing and nothing could disagree.

series.json declares each series' funnel flag, string overrides and the three
below-map NETWORKS blocks. Both packet.mjs and generate.mjs read it, because the
leak was in TWO independent places -- the packet strings and the poster's own
furniture, which is not a packet string at all, so fixing either alone leaves the
other printing. SERIES absent or unknown resolves to upg100.

The funnel page drops for country: it narrows every people group on earth to THIS
list, which is not a description of a country. Reusing the existing
missing-vision-funnel path means one way for the page to be absent, not two.

Four packet keys are overridden, not seventeen: 12 of the 17 are funnel_* and go
with the page; the 12 prayer points are invariant, because they are about a
PEOPLE and a country poster still prays for the peoples within it. Overrides
apply to every locale on purpose -- an English country kicker is visibly awaiting
translation, whereas inheriting the translated UPG-100 kicker prints the wrong
series fluently.

The country English is a DRAFT from the Driver's dictated shape, flagged
_draft_awaiting_driver; no translations until he signs off.

check:series-isolation asserts the property, with a self-test over 6 known-bad
inputs. It caught a real leak this commit would otherwise have claimed fixed: the
packet's page 1 lifts the whole SVG from the pre-rendered poster, so a country
packet built against a stale UPG-100 poster still printed 'Thirty disciples'.
Verified at the rendered layer -- country 11 pages, upg100 12, the delta exactly
the funnel page; upg100 networks byte-identical to the text they replaced.
2026-09-28 04:19:27 +00:00
Doxa Maps
20de483544 Rebuild pplr-data-maps, and regenerate the tree
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 04:10:19 +00:00
Doxa Maps
fa494dbac0 PPLR maps: the legend tabs are named in the reader's language
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.
2026-09-28 04:06:20 +00:00
Doxa Maps
5d67a3969d PPLR maps: the legend and the instance store are the library's, not drifted copies
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.
2026-09-28 03:58:06 +00:00
Doxa Maps
7077c55502 Remove a kill switch the code does not have
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
file-map.md documented `SOW_EXPORT_BUTTON=false` as a hidden global switch that "hides the
fully-wired Export menu across ALL bundles". SOW_ appears nowhere in this repo, so anyone setting
it would get silence and conclude the Export menu was broken. Docs describe the code as it is, so
the claim is gone.

The per-component prop it mentioned is real and untouched: LegendMobile.vue passes
:export-enabled. Only the global env override was fictional. MAX_LIVE went with the staging.js row
it lived in, and neither name appears in README.md or documentation/ now.

If a global export switch is wanted, that is a card, not a doc edit.
2026-09-28 03:40:55 +00:00
Doxa Maps
9b6bcaa0fa Doc audit: five stale claims a newcomer would have acted on
Audited README.md and all 35 documentation/*.md against main. Script names and markdown links
were already clean — 59 documented scripts all exist in package.json, 0 broken links. What was
wrong was file-map.md describing files that no longer exist, and three paths that had moved.

REMOVED, all archived in afb998f17 or earlier and still documented as current:
  internal/staging.js — the repo-root staging page; the plugin studio is the only entry now
  index.html — the same page's HTML
  library/composables/useDoxaSearch.js — deleted in ea0c47b27, and dropped from a prose list too
REPOINTED: the three colour strategies documented under plugin-designs/doxa-research-map/src/colors/
live in library/colors/. THIRD-PARTY.md's two vendored files live under data/vendor/.
embeddability.md's reference to the staging script now says it was archived and where the entry is.

WHAT I DID NOT "FIX", because it was not broken: ~20 relative shorthand paths in the architecture
docs (`composables/useMapData.js` inside a document about library/) read correctly in context, and
three paths name things that are not repo files at all — a runtime endpoint (public/api/maps/token.json),
a generated artifact (out/_records.json) and a placeholder (./bundle/bundle.js). Reporting those as
defects would have buried the five real ones in 23 false positives.

TWO MEASUREMENT TRAPS I fell into on the way, both corrected before reporting: my first env-var scan
grepped only internal/, plugin-designs/ and library/ and declared 13 documented env vars unread —
11 of them are read in internal/build-lock.sh, .env.example, deployment/cloud/Dockerfile and the Go
backend. And an earlier sweep of my own data files reported six "rega" hits that were all the word
"aggregates".
2026-09-28 03:40:55 +00:00