A host page can define a tag name before our bundle loads, and the first definition wins. DX Life's own
/js/doxa-simple-map.js defines <doxa-map> and <doxa-simple-map>, so on those pages our guarded define was
skipped and the page drew the host's element under our name.
- The doxa-simple-map bundle now defines <dxm-simple-map>, DX Maps' own name, first. The manifest, the leaf
pages and the gallery mount it (generate-tree reads the first define).
- <doxa-map> and <doxa-simple-map> are defined only when the host asks: data-legacy-tags on the bundle's
<script>, or window.DXM_LEGACY_TAGS = true before it loads.
The library creates the language-family pin layer at circle-opacity 1, but the three
paths that clear a filter (no filter key, an unknown key, and the aggregate search
filter) restored it to 0.9, so after selecting and deselecting a legend row the pins
stayed slightly faded. They now restore PIN_REST_OPACITY (1), so a cleared map looks
like a fresh one. Rebuilt doxa-simple-map, then the tree and SRI.
Comments only. Eleven comments in doxa-simple-map.vue carried an agent name, a
"user request" / "user feedback" attribution, or a dated QA tag; each now keeps
its reasoning and drops the attribution and the date.
The built bundle changes only in the component's scope id (Vue derives it from the
source in production builds); every other token is identical, so behaviour is
unchanged. Source only: the assembler rebuilds.
Naming it would ship every map's strategy text inside this bundle (the text shares one catalogue
area). Embeds show the intro and the note; the strategy follows once the catalogue loads per locale.
MapInfo gains a `strategyKey` prop: a catalogue key prefix whose .bottom is the bottom line and .p1, .p2 … the
points, read in order until the first missing one (the catalogue decides how many). The popup shows the intro
(the lead), then:
- a map with examples AND a strategy gets two tabs, Examples first, then Strategy (44px targets, reset to
Examples on every open);
- a map with only a strategy goes straight to it, under a Strategy heading;
- a map with neither is unchanged.
The takeaway is not shown beside a strategy, which supersedes it. Labels come from mapInfo.tabExamples and
mapInfo.tabStrategy. Prayer Progress's own info (for embeds) names its strategy key too.
The ⓘ panel now states that a dot marks where a people is found, not a town or a boundary (noteKey
pins.approximate, the catalogue string the map's pin note used, in all 11 locales), so the caveat
stays available to a reader who asks once the phone legend stops stating it.
Also rebuilds every bundle the MapInfoButton / MapInfo change reaches, as the landing precondition
names them: research-dashboard, dashboard-plus-grid, cities-internet, gospel-resources and
doxa-research-map (their behaviour is unchanged: standalone is opt-in), plus doxa-simple-map for the
note. Then tree + SRI.
On a partner page or a standalone preset page the ⓘ control rendered nothing, because only the
gallery answered it and the ⓘ's text lived in the gallery's settings. Now the map carries its info
(plugin-designs/doxa-simple-map/src/mapInfo.js) and a host only adds placement:
- MapInfoButton gains an opt-in `standalone` prop. When no host answers and the map carries info, it
asks the map for a popup of its own (the 'standalone' event). If a real host appears later, the
map's popup is dropped and the host takes over. Never inside the gallery (its panes carry
data-doxa-gallery): there the gallery alone decides, its own info:false included.
- MapInfo gains `popupOnly` (with trigger 'control'): no floating button, since the map's control is
the only button; it answers that control from inside the map, points at it once per page load,
and opens the same popup (the bottom sheet on phones). It announces itself as local, so the
control can tell it from a real host.
- Prayer Progress mounts that popup beside its control column, only when asked.
Per preset: the text describes the prayer map, so a map that shows the Prayer tab has info (the full
profile and the prayer preset); the engagement-only and adoption-only presets have none and show no
ⓘ, as before. A host's profile-config info:false removes the ⓘ. Every other MapInfoButton consumer
is unchanged (standalone is off by default).
doxa-simple-map grows by 20,590 bytes raw (5,699 gzipped), for compiling MapInfo. Rebuilt
doxa-simple-map and doxa-maps-spa, then tree + SRI.
Prayer Progress draws its control column only once the style and the people-group data are in,
which on a slow phone can take longer than any fixed wait, and then the gallery's floating ⓘ
appeared and was swapped for the control. Now the map announces at mount that a control is on its
way ('dxmaps:mapinfo-pending'), and MapInfo waits for the map instead of a clock.
- A known failure (the map reports an error) or an unmount before the map is ready withdraws the
announcement, and the floating ⓘ shows at once.
- While a control is pending, the 30 s wait becomes a 90 s backstop (time on screen), so an
unreported failure still gets a floating ⓘ.
- A map that never announces anything keeps the 30 s wait, and pages with no MapInfo are unchanged.
Rebuilt doxa-simple-map and doxa-maps-spa, then tree + SRI.
The control column goes back to the library's round, spaced buttons at every width; the square
grouped card read too compact, on phones most of all. The ⓘ stays the top control.
In control mode, MapInfo no longer shows its floating button before the map's control has
announced itself. The map draws its column only once it is ready (style and people-group data),
so the floating ⓘ appeared for a moment and was then replaced by the control. The wait is bounded
at 30 s of time on screen, counted only while the pane is visible and not inert: measured column
arrival times run from 7 to 19 s on a local serve, so a shorter wait expired inside a normal load
and flashed late. If no control arrives, the floating button shows, so there is still always a way
to open the popup. Once a control has announced itself, measurement alone decides, as before.
MapInfo's plain <script> block (the page-load pointer memory) now sits above <script setup>, so
check:setup-order no longer reads it as used before its definition; its explanation moves into
<script setup> so the library index keeps the component's own description. Compiled output is
unchanged by the move.
Rebuilt doxa-simple-map and doxa-maps-spa (the only bundle that compiles MapInfo.vue), then
tree + SRI.
- Prayer Progress's column becomes the engine's square grouped card (controls="square"). Its first
control is MapInfoButton, which opens the gallery's MapInfo for the tab. The gallery's tabInfo
'prayer-map' entry now uses trigger 'control', so the floating button hides while that control is
on screen, and shows again (fail-open) whenever it is not.
- MapInfo, control mode: the first-visit pointer now points at the control. Its place is measured
against the pane (which scrolls) and goes on the control's physical inner side, so an Arabic page,
whose column mirrors to the left, gets the pointer on the right with the arrow reversed. It shows at
every width: the under-1200px rule was measured for the floating pointer crossing each tab's top
strip, and does not apply here.
- MapInfo, pointerKeep (prayer-map only): the popup offers "Keep tooltip activated" instead of the
every-map "don't point again" box. It is off by default, and opening the popup retires the pointer.
Checked, the pointer keeps showing on every load. It is stored per map.
- mapInfo.keepPointer in all 11 locales.
- Rebuilt doxa-simple-map and doxa-maps-spa, then tree and fix:sri.
Prayer Progress is a heavy gallery tab: switching away empties its slot. If the map
is still starting, initializeMap returns silently (useMapInstance's liveness
checks) and leaves map.value null. onMounted then logged "[doxa-simple-map] Map
failed to initialize, check token and container": a console error that blamed
the token for a tab switch.
Measured on development's tip, served from its committed tree, on the shared
browser, a phone in Arabic (pp-fastswitch.mjs): switched away 150 and 500 ms after
selecting the tab, 2 runs in 10 logged it. The control runs, switched away after a
full start or never switched away, logged nothing.
Same shape as the research map's quiet cancel (0c263fb60): when the map's
container is gone or detached, both failure paths return quietly. A mounted pane
that really fails still logs.
The research page now gives this map three tabs (engagement, prayer,
adoption). With two or more tabs the bar was a row of plain buttons: a screen
reader heard three buttons, with nothing saying they are tabs or which one is
showing. The bar is now role=tablist, each button is role=tab with
aria-selected, and the buttons are type=button. A one-tab instance shows no
bar, so it is unchanged.
21 lines in seven of exp's bundles (upg-neighbor 4, prayer-walk-planner 8, doxa-simple-map 2, pplr-data-maps 2, wall-of-people-groups 1, doxa-vision-card 1, all-people-groups 3): each keeps its engineering reason, its dates and its measurements; the speaker and the quotes go (a quote becomes a plain statement of the defect). Comments only. Five bundles rebuild byte-identical; doxa-simple-map and pplr-data-maps change only in their Vue scoped-style ids, which hash the SFC source.
Goal (c), and it turned into a goal (d) fix. Every profile element is
defineCustomElement(ProfileLoader, { configureApp }), and the configureApp has four
steps. The fourth, app.provide('appI18n', i18n), is the one that gets dropped.
ProfileLoader injects that instance (ProfileLoader.vue:176) to drive its locale from
profile-config. Without the provide the watcher holds null and the drive NEVER RUNS --
while setRuntimeLocale() on the next line still fires, so tStatic strings DO follow the
host. The element renders a convincing MIXTURE of the reader's language and English, and
nothing throws, because the t() shim falls back to its key. It reads as "a few strings
not translated yet".
SHIPPING THAT TODAY: country-posters, key-cities, gospel-resources, cities-internet and
my-upg-100-list. regions-plugin censused all 19 entry files independently and found the
same five and no sixth. The measurement is regions-dashboard's, from when they hit it:
a Spanish catalogue present, es set, and every t() string still English.
All sixteen entries now call library/elements/defineMapElement.js, so the five are fixed
by construction rather than by five separate edits. Behaviour is otherwise identical --
same options, same element names, same per-plugin extras. wall-of-people-groups needed
the helper to hand its pinia back, because it takes a store off its own instance and a
helper that hid pinia would have given it a second one: two stores and a silent state
split.
★ AND THE CENSUS THAT RECOMMENDED THIS SAW FOUR OF SIXTEEN. My detector hashed function
BODIES; eleven sites are not `function buildXElement()` at all, they are
`const El = defineCustomElement(...)`. I wrote "every number here is a lower bound" in
the census as a caveat and it was the headline within the hour -- a 4x undercount of the
item it recommended. So this gate greps for the SHAPE, not for a known list: a check
written around the four that matched would have passed the twelve that did not.
check-element-bootstrap has two arms. No entry may build a profile element by hand, and
the helper itself must provide appI18n -- PLANTED as a negative case, so the regression
cannot return by editing library instead of a plugin. It also fails if it finds fewer
than ten entries, because a detection that stops matching reports the same green as a
tree with no problems.
THE PLANT CAUGHT TWO BUGS IN THIS GATE BEFORE IT CAUGHT ANYTHING ELSE. The predicate
matched the helper's DOCSTRING, so it would have passed a helper that only mentions the
provide in prose -- comments are stripped now, because a check that reads comments is
reading the author's intentions rather than the program. And the plant itself used /m
instead of /gm, so it deleted the documentation and left the code, then reported that it
could not detect an absence that was not there.
The legend and search column were placed with left:10px and styled only under Mapbox's
.mapboxgl-ctrl-top-left, so on the keyless engine they kept MapLibre's defaults and in
Arabic stayed on the left. Both engines' classes now take inset-inline-start (the same
fix country-posters carries), and the help close button and the detail labels use
logical sides.
countries-map, country-poster-wall and doxa-simple-map carried label:null, so the Studio
rail and the gallery showed their ids. Each now has a label in its meta; types are
unchanged (doxa-simple-map stays parameterized).
The Driver: a public plugin that is not yet a production map shows under Experimental until it is. A
profile's .meta.json declares "stage": "production" | "experimental" (default experimental; a bundle is
production if any profile says so) and "heavy": true for a live WebGL owner. generate-tree carries both into
manifest.json, so the Studio and the gallery read the same field and neither keeps a list.
The seven production bundles are marked: dashboard-plus-grid, research-map-plus-grid, regions-dashboard,
my-upg-100-list, doxa-simple-map, cities-internet, gospel-resources. The Studio rail keeps its capability
sections for them and puts every experimental bundle (gospel-internet among them) in one final Experimental
section. The gallery's auto-discovered Experimental folder skips a production bundle. gospel-internet-board
joins HEAVY_ELEMENTS (it can own two WebGL contexts). check:studio-rail asserts the split.
With no token the engine is MapLibre and the basemap is OpenFreeMap fiord, slate-blue
everywhere: sea and land 13 units apart, blue-grey borders. The Mapbox Prayer map is
Standard at night: near-black navy sea, slate land, rose borders. The keyless map now
recolours fiord to the colours measured on the Mapbox render of this leaf (ocean
#03112d, land #323f52, border #c95d74), on every style load, on MapLibre and the dark
basemap only. The Mapbox path is untouched.
Every one of these bundle entries carried its own copy of
if (window.mapboxgl?.setRTLTextPlugin) { setRTLTextPlugin(url, null, true) }
which is a strict SUBSET of what library/maps/rtl-text.js already does, so the
copies could not be covering anything the library misses:
- useMapInstance calls installRtlTextPlugin(gl) at line 450, before it builds a
map at 537/567 -- registration still precedes construction, the one ordering
Mapbox cares about.
- the call is identical in effect: same v0.3.0 URL, same null callback, same
lazy flag.
- the block is mapboxgl-only. The library installs on MapLibre too, so every
keyless embed was covered by the library and never by these blocks.
- the single raw `new gl.Map` site (upg-100-list.vue:1894) installs for itself
at 1835 and never depended on its entry.
I first justified this as deleting DEAD code, on the theory that the engine is
injected lazily so window.mapboxgl would not exist when an entry runs. That is
wrong and the measurement says so: every leaf index.html loads mapbox-gl.js as a
blocking <head> script while ./index.js is a deferred module, so the guard did
pass and the blocks were live. The reason to delete them is redundancy, which is
the weaker and truer claim.
A three-line pointer replaces each block so the next reader does not re-add a
Mapbox-only copy.
SPEC-LIBRARY-CURATION.md's first migration, in the order the shepherd approved.
Source only; studio rebuilds (rule 5B — library/ plus eight other owners' plugins).
THE ASYMMETRY, which is why 12 sites each solved it or failed at it separately:
Mapbox getProjection() -> { name } setProjection('mercator')
MapLibre getProjection() -> { type } | undefined setProjection({ type: 'mercator' })
Passing the Mapbox shape to MapLibre THROWS. Measured on regions-dashboard, where
the throw aborted the camera fly so a region dashboard never framed its region.
WHAT THE 12 SITES WERE DOING:
· NINE passed a bare string, so on MapLibre the projection silently never changed.
Eight swallowed the throw in a try/catch; doxa-simple-map.vue:1244 did NOT, so
there it threw uncaught.
· TWO had solved it independently and differently: pplr-data-maps'
setProjectionFor() (instance-tested — the correct one, and the shape adopted)
and regions-dashboard's local pair (config-derived, which is weaker: it trusts
what the page was configured with rather than what the map IS).
· ONE probed by exception, trying the object form and falling back on throw.
The helper reads the engine from the INSTANCE, never from config, because a page can
hold a map built by the other engine. Both functions return something observable —
setProjectionOn returns whether it applied, projectionOf returns null rather than
throwing — on upg100-plugin's argument that a precondition which bails without
recording it is indistinguishable from the request never arriving.
pplrData.setProjectionFor stays as a delegating named export so its three views need
no change.
GATE: _chk-c2-projection-helper-equivalence.mjs, 20 assertions. It runs all THREE old
patterns and the helper against the same stub maps and compares outcomes, rather than
testing the helper alone. Controls prove pattern A really does fail on MapLibre and
leave the projection untouched, and that the bare string really throws — so the
"equivalence" claim has something to be equivalent to. A NEG control catches a helper
that reports success it did not achieve.
★ I DESTROYED 984 LINES OF ANOTHER TEAM'S CODE GETTING HERE, and only the build caught
it. My import-inserting script did `s[:at] + IMPORT` and never appended `s[at:]`, so
every file where the import was ADDED rather than extended was truncated at its last
import line: MapCell.vue -458, FlowerClusters.vue -334, lang-cluster.vue -192.
check:no-free-identifiers PASSED on all three, because a truncated script block is
still valid script. Restored from HEAD and redone with an assertion that a migration
must change the line count by exactly +1 and never remove a line — which is the check
that would have caught it in the first second.
Verified after: all 9 affected bundles build, both gates green, and a per-file line
delta on all 13 changed files with the only negative being the 2 lines I deliberately
deleted from RegionCountryMap when its local pair became an import.
Every map showed DOXA's people groups, and only those. The same maps must serve other projects'
lists: Project 7000 (unreached), 4700 (frontier), 700 (frontier of 500,000 or more), 2000 (DOXA).
The list is now chosen the way the engine is:
profile-config `list` > ?list= > window.MAP_LIST > 'doxa' (exactly today's behaviour)
with ids or project aliases ('700', 'project-7000').
- library/lists/lists.json: each list is a FILTER over the IMB / peoplegroups.org dataset already
in the repo (Lostness Priority, Population, Engagement Status), never a hand-kept roster.
- library/scripts/people-groups/build-lists.mjs (`bun run lists`): writes each list's DOXA members
to data/people-groups/lists/<id>.json, joined the way the Vision Card funnel joins, so the
counts agree with it: unreached 7,341 (2,082 DOXA), frontier 4,754 (1,745), frontier-500k 737
(71), unengaged 2,982 (1,929).
- library/lists/peopleGroupList.js: resolvePeopleGroupList and applyPeopleGroupList(rows, cfg).
The default list returns the same array, unfetched. A list whose membership cannot load shows no
groups (fails closed) rather than DOXA's whole list under another list's name.
- Tier A, every production map: DataSourceManager applies it (and the maps that build one pass
their profileConfig), and so does every direct people-group fetch: people-groups-grid, the
Big Picture header, research-dashboard (data, CSV export; the DOXA-wide /statistics baselines
are skipped under another list), Regions (list, dashboards, their table), UPG-100 (4 fetches +
the poster wall), Gospel's roster, and the Experimental upg-neighbor and Wall. The composites
thread `list` to their children like `provider`.
Gates: check:universal-props gains a `list` column (LIST-FETCH, LIST-DSM, LIST-KEYS; its
self-test now plants 9 bypasses) and passes across 18 bundles, with all-people-groups allowed:
it draws the IMB dataset, so its list is Tier B. New check:list-prop-unit (Node, 17 cases, in
gates:post); poisoning the ladder order or deleting a membership file fails it. Documented in
documentation/reference-universal-props.md section 5.
Headless: all 14 touched bundles build; check:composites-mount 20/20 and check:composite-interop
17 links pass on this tree; no-orphan-gates, script-flags, quoted-scripts, doc-scripts pass. Not
rendered in a browser.
The Driver's localization list, item 4. doxa-simple-map already took its language from profile-config
locale/lang and ?lang= (ProfileLoader) and set dir=rtl for Arabic, but its own strings were English
literals: "Loading map…", "The map could not be loaded." plus a technical init message, the legend
title 'Legend', the default "Prayer" tab label, and the whole "About This Map" help modal. They now
read the catalogs: legend.titleBar.legend, tabs.<id>, allPeopleGroups.loadError, buttons.aboutThisMap,
buttons.close (the close button had no accessible name), and a new simpleMap area in all 11 locales.
A tab label a HOST passes in profileConfig.tabs is still shown as given. The map formats no numbers
itself; the legend, popups and detail panel are library components already translated.
check:i18n-parity PASS; the bundle builds. A library catalog change, so studio lands it (rule 5B).
needs eyes: Prayer Progress with ?lang=ar: the loading line, the tab, the legend title and RTL layout.
(cherry picked from commit c35bc84794)
Moved out (kept outside the repo, in the team's internal tracker):
- AGENT-BRIEF.md: operating rules for the team's own agents
- documentation/slide-deck-outline.md, documentation/README-longform.md:
internal pitch material; the README and why.md cover the same ground
- plugin-designs/gospel-resources/SOURCES-CHECKED-AND-REJECTED.md: research log
- .claude/ (an editor status line), .forgejo/ (a CI workflow that never ran),
_archive/ (a retired staging page; already gitignored but still tracked)
Seven code comments that named these files now describe the rule instead.
Fixed five dead links left from the docs/ -> documentation/ move, including
the one in template-bundle that every new plugin inherits.
The delegation diagram now shows the whole model: the Plugin Studio with the
shared library and the merge queue into main, seven production plugins with
capability badges and the Experimental shelf, one agent per plugin (a roaming
agent for the shelf), the shepherd's oversight ring, a legend, and the five
ways a build ships.
The column header ("Prayer Status", "Engagement Status", …) was hard-coded
English, so an Arabic, Chinese, Russian or Hindi embed showed it in English
above translated rows. It now reads legend.colLabel.*, which all 11 catalogs
already translate.
Measured on a served built tree with the host page on another origin: in ar, zh,
ru and hi the only text still identical to English is the Mapbox attribution
and "UUPGs", which every catalog keeps in Latin script.
Driver: 'if I search the name of the country and add n to it, like
Austrian or Canadian, I would like for there to be a selection of that
country -- the table under the map shows the result for that country.'
Three changes, one ladder:
1. resolveFuzzyValues(): WHOLE-VALUE Levenshtein against the DISTINCT
values of every search key -- 'Sudann' is a misspelling of the
VALUE 'Sudan', so the rows returned are exactly Sudan's, identical
to picking it as a Place. The windowed matcher stays only as the
final rescue tier.
2. Best match wins: only the minimal-distance value(s) resolve. The
under-the-cap union admitted 47 rows for 'Canadian'.
3. Both fuzzy tiers cap at 2 edits absolute (the did-you-mean bound);
threshold*len alone reached 3 errors at 8+ chars, which is where
the cross-facet noise lived.
Measured (dashboard scope / grid count agree): Sudann -> 105/105
Sudan only; Indonesa -> 36/36 Indonesia; Banladesh -> 3/3 Bangladesh;
Bhutann -> 13/13 Bhutan; exact terms unchanged (tier 1).
NOTE: Canada/Austria themselves have NO rows in this dataset (it
holds only the 175 countries with unreached people groups), so
'Canadian'/'Austrian' cannot select them -- a handful of genuine
2-edit neighbours return instead.
Also ships @map/composables/retryableAsyncComponent.js wired at every
defineAsyncComponent site (ProfileLoader chokepoint + 8 lazy
components): a transient ERR_NETWORK_CHANGED no longer permanently
kills a lazy plugin (3 retries, growing backoff). Live fault-injection
proof pending; the wrapper is the documented Vue onError contract.
More than half the catalog had an empty description in manifest.json, so the
plugin-studio — the surface a Doxa inheritor browses to learn what exists —
showed flagship plugins as a bare "bundle - <tag>" with no explanation, while
a demo bundle explained itself fully.
Cause: the generator reads a JSDoc @description TAG, and these files carried
only prose. Every description added here is a summary of what that file's own
header already documented, not new invention. Claims were checked against the
artifacts: doxa-simple-map's "three presets" was verified against
simple-map.instances.json (prayer, engagement, adoption).
The two cap-5 shells needed a second pass. Their headers are HTML comments
with no leading asterisk, and the real TAG_RE requires `* @tag value`, so the
first attempt extracted nothing.
Result: 26/26 profiles described, 0 empty; the built gallery went from 15
empty / 11 populated to 0 / 26. Verified in a browser, not just in the
manifest. Embed suite 40/40.
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.
docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.
The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.
Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
- join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
- tauri frontendDist '../public' -> '../../public', since src-tauri is now one
level deeper; check:tauri caught this exactly as the card predicted
- the WordPress contract test's dirname(__DIR__,2) -> 3, same reason
Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.
Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.