Commit graph

728 commits

Author SHA1 Message Date
Doxa Maps
0114ec45cb Merge landing 2 and rebuild against the 50 library files it moved
studio's landing 2 (1d569edd) changed 50 files under library/, so the rebuild was
required — checked with `git diff --name-only 85a41726 1d569edd | grep '^library/'`
rather than assumed, which is the test that decides this every merge and has come
back both empty and non-empty on different rounds.

The only conflict was the built bundle, a derived artifact: resolved to main's
version and then regenerated from the merged source, because picking a side of a
generated file is meaningless. Zero source conflicts.

VERIFIED BY HASHING, not by fix:sri's summary: upg-100-list, upg-100-mfe and
poster-wall all MATCH the rebuilt bundle, whole-tree sweep 57 integrity attributes
with 0 mismatches.

AND VERIFIED MY OWN WORK SURVIVED, because a 50-file library merge is exactly where
a plugin change quietly disappears: flashRef present, messages.engineNotLoaded
wired, tokenUrl still forwarded in the composite, the new gate intact.
The gate is the better witness than a grep, and it now reports what it should:
1 offender, gospel-resources/resources-map.vue:1490, and NONE in my plugin. Before
this merge it named mine too, because ade12e4c was unlanded. check:i18n-parity PASS.
2026-09-26 17:30:33 +00:00
Doxa Maps
dcd25a0a4f Register check:provider-for
The gate proves providerFor(map) answers the same as the raw window.maplibregl sniffs it replaced (C3), on
both engines and on the ambiguous inputs where the originals fell through to mapbox. It also proves the
obvious isMapboxMap() rewrite would have inverted that fall-through. Registered in scripts (alphabetical,
with its //scripts note) and gates:post; source-only. From regions-plugin 332dcf74; package.json edited by
hand, not taken whole.
2026-09-26 17:28:56 +00:00
Doxa Maps
50a29b463d Dashboard kit, landing 1: the selection bus becomes library code
The first piece of Big Picture to be shared, and it went first for one reason:
useSelectionBus has ZERO domain references and imports nothing but `vue`, so it
is the only piece that could move with no adapter and no behaviour change. The
body is byte-for-byte what main shipped — verified by diffing everything from the
first import onward — and only the file's header block is new. A move that also
edits is a move nobody can verify, and nine of these have to be verifiable in a
row.

THE OLD PATH STILL WORKS, as a re-export, for one cycle. That is not politeness;
it is what keeps this landing small enough to check. A landing that moved the
file AND rewrote every import would be two changes whose failures look identical
from the outside. The shim re-exports and does not duplicate: a second copy would
drift silently while both halves kept passing their gates, which is the failure
check:no-cross-plugin-imports and check:shared-reached exist to stop. It is
deleted in the next landing.

★ THE GATE HAD TO MOVE WITH IT, and this is the part a move like this hides.
check-facet-cascade reads the bus from a PATH (BUS_SRC) and, for its self-test,
copies that file and poisons the copy to prove it can still catch a reverted
cascade. Pointed at the shim, it would have copied and poisoned a RE-EXPORT: the
mutation would match nothing, the "broken" copy would behave correctly, and the
self-test would report that the gate no longer catches anything. One line, and
without it landing 1 would have silently disarmed the only check that proves the
hierarchy contract. Verified after the move: the gate PASSES at the new path and
the self-test still catches a reverted cascade (13 assertions).

The library index is regenerated in the same commit, per SPEC-LIBRARY-CURATION
section 4: check:library-index compares REGENERATED BYTES, so a new file under
composables/ turns it red until `bun run library-index` is re-run. Per-landing
regeneration rather than one pass at the end, because nine landings with a
knowingly-red staleness gate would train nine readings of "ignore that one".
useSelectionBus now appears in the generated menu — which was the whole argument
for the existing taxonomy over a new top-level area.

NOTHING TO REGISTER IN package.json: the gate that guards this file,
check:facet-cascade, was already wired, and check:no-orphan-gates confirms it.
This commit does not touch package.json at all, so it cannot conflict with the
three landings of mine that do.

STILL COMING, deliberately not here: SPEC-DASHBOARD-KIT D1 makes filterSet
multi-value. That changes this file's CONTENTS; this changes only its LOCATION.
Keeping them apart is what makes "Big Picture is unchanged" provable.

SOURCE ONLY — not built, by instruction. Green here on main's tree:
check:facet-cascade and its self-test, check:library-index, check:no-orphan-gates,
check:no-cross-plugin-imports, check:shared-reached, check:no-free-identifiers,
check:mjs-parse, check:sfc-parse, check:descriptions.
2026-09-26 17:28:46 +00:00
Doxa Maps
5739818ecc Never hand a keyless page the engine that needs a token
resources-map resolved its engine as `engineGlobal(provider) || window.mapboxgl`.
engineGlobal already defaults to mapbox, so that fallback could only ever fire for
provider 'maplibre' — and then it returned MAPBOX. A page that chose the keyless
engine silently got the one that needs a token, and because the profile only
assigns accessToken when the provider is not maplibre, the substitute had no token
either: it would have 401'd on every tile. The fix is the deletion. The requested
engine or a named failure, with a console warning for whoever is debugging and the
reader's line through messages.* (English kept inline until the key lands).

check:engine-contract measures it: two providers across engine present, engine
blocked and both blocked, with the CDN blocked by request routing the way a proxy
or an offline kiosk does it. Three of those six arms first failed for reasons that
were mine, not the product's, and each is written down in the gate: Mapbox cannot
mount at all on a static tree without a token, 11s was not long enough for a leaf
that fetches 4 MB before the map exists, and engine attribution read from a debug
handle scored Mapbox-with-a-bad-token as "engine null" — the canvas class cannot
lie about who drew it.

Also in this landing: the pin caveat is translated in 11 locales from the shared
pins.* area, read through this leaf's own guarded t() rather than by mounting
library ApproxPinNote.vue — that component calls useI18n() unguarded, and this
bundle is embeddable in hosts that never called app.use(i18n), where an unguarded
call throws during setup and the shadow boundary swallows it. Same wording either
way. Both "Show all groups" buttons now use the key that already exists in 11
locales, and the engine's own control labels get the provider arm.
2026-09-26 17:21:22 +00:00
Doxa Maps
d70f5d4538 A gate for the negative case the three engine gates never asserted
ade12e4c removed one silent engine substitution. This asserts that nobody
re-adds it, and it found the two I had no business fixing myself:
  cities-internet/connectivity-map.vue:365   falls back to mapboxgl
  gospel-resources/resources-map.vue:1424    falls back to mapboxgl
and ZERO hits in my own plugin, which is the fail-then-pass proof — on real code
rather than a synthetic mutation.

★ WHY THIS IS NOT A FOURTH DUPLICATE, which I checked before writing it because I
nearly built a redundant check:sri earlier today. Three engine gates exist:
  check:leaf-engine-per-provider — a leaf loads the engine its provider asks for
  check:provider-switch          — the page switch reaches a raw-built map
  check:auto-engine              — 'auto' = Mapbox with a token, MapLibre without
All three assert the HAPPY PATH: what happens when engines load. This defect only
appears when one does NOT load, so all three were green for as long as it existed.
The property nobody asserted is the negative one — "when the requested engine is
absent, no other engine is substituted".

IT IS A SOURCE CHECK ON PURPOSE. Reproducing this live means serving a leaf with
MapLibre deliberately blocked: a browser slot spent deciding what a grep decides.
The fallback either appears in the file or it does not.

⚠ AND IT REFUSES TO JUDGE RATHER THAN PASS VACUOUSLY: exit 75 when no file calls
engineGlobal at all, because "I found no files" is not "I found nothing wrong" —
a green run on an empty scan is the exact shape of a gate that has quietly stopped
measuring anything.

--self-test proves the pattern still fires, since a source gate that has never
matched is indistinguishable from a broken regex. It matches all three real forms
(window-prefixed, bare, whitespace-padded) and ignores a comment quoting the bad
pattern, a `|| null` fallback and an unrelated `||` token chain. Comment lines are
skipped deliberately: a gate that fails on its own explanation teaches people to
delete the explanation.

check:no-silent-engine + :self-test wired in alphabetical position, with the
reasoning in //scripts.
2026-09-26 17:07:37 +00:00
Doxa Maps
c51233182d Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-26 16:43:37 +00:00
Doxa Maps
6273ae18e6 P8: a partner's own list is an address
This map ships Doxa's roster, and the point of a public corpus is that other lists
can stand on it. A partner could already embed the map; their reader then had to
go hunting for their own people among 2,107 dots.

  ?codes=kfr,cmn,npi   draw only those languages, framed on them
  ?country=IND         open that country's sheet — this did NOTHING before unless
                       a ?code came with it, so the brief's second requirement had
                       no address, which is exactly what a partner embeds
  ?view=zero           unchanged

The join is ISO 639-3, so a partner's list needs no agreement with ours — only the
codes their own data already carries. It filters and never invents: the notice
reads "74 / 2,107" and is itself the button back to the whole map, labelled with a
key that already exists in 11 locales. A link matching nothing shows an empty map
reading "0 / 2,107", because showing the full map instead would tell a partner
their broken list works.

check:embed-contract gates the promise. Written against the pin index first, it
demanded "12 / 12,379" of a map correctly saying "0 / 2,107" — the gate was wrong
and the map was right, so its numbers and its probe codes now come from the file
the leaf itself reads.
2026-09-26 13:36:15 +00:00
Doxa Maps
6c804159c3 Three browser gates stop passing hollow or racing (rega's gates:post run at ded4b3c1)
- check:dashboard-geocoder-provider: 0 search bars on BOTH engines. Its default leaf (research-map) hides
  the bar under MapLibre on purpose, and the tokenless Mapbox run could not draw. It now defaults to
  doxa-simple-map (a bar on both engines), and the Mapbox run gets a host token: TEST_MAPBOX_TOKEN, or a
  well-formed fake whose token-carrying requests are aborted, never sent.
- check:composite-embed-knobs: every tk case SKIPPED (no baked token since the auto engine), so every
  provider case went UNMEASURED and the gate still PASSed. It takes TEST_MAPBOX_TOKEN, and a run where no
  case measured anything exits 75 NOT MEASURED instead of PASS.
- check:linked-vocabulary: it attached its groups-filtered listener after goto and could miss the grid's
  first emit ("the grid never reported a total"). The totals are now recorded from page start
  (addInitScript).
Not browser-run here (browsers are rega's): rega's next gates:post pass is the proof.
2026-09-26 13:35:57 +00:00
Doxa Maps
1f77810450 check:share-policy: record the template-bundle, which became a map
22b92ae0 made the starter's first screen a real map (CRITIC-11), so the gate
found a map bundle with no share decision. It is recorded as 'none': the starter
is not published, and a copy adds a ShareButton when it becomes a real map. The
gate enforces that assumption: if template-bundle is ever published, 'none'
fails it. Now PASS, 12 map bundles.
2026-09-26 13:04:16 +00:00
Doxa Maps
1228cd1f1a check:facet-cascade: read the facet rail by brace depth, so getter titles stop blinding it
The gate reads facetRail out of dashboard.config.js with /\{[^{}]*\}/, which
matches only objects with no braces inside. 53e2c662 translated the rail titles
as getters (get title() { return tStatic(...) || 'People' }), so the four drill
sections stopped parsing. The bus was then built with no hierarchy, and the gate
reported 21 'orphaned child' failures in a dashboard that cascades correctly
(the real one reads the live config object, never this parser). Red on main
since 53e2c662 (bisected by swapping the gate's only two inputs: 7f700935 OK,
53e2c662 FAIL).

The parser now splits the array into top-level objects by brace depth, skipping
quoted strings, and throws if a section has neither a drill nor a facet. So a
parse miss is an error, never a false pass or a false fail.

Headless: it passes on main's real files. With a poisoned bus (configureHierarchy
installs no hierarchy) it FAILS; restored, it passes.
2026-09-26 13:04:16 +00:00
Doxa Maps
9888c4bcc9 gates:post reports three buckets, PASS / FAIL / NOT MEASURED, and exits non-zero only on FAIL
A skipped or unmeasurable gate and a failing one shared one exit code, so real reds hid among skips
(regions measured 15 of 67 red at d3d3c3f8, mixed with gates that simply could not look).
- Exit 75 (EX_TEMPFAIL) from ANY step is NOT MEASURED, as is a gate that exits 0 announcing SKIP or NOT
  MEASURED. Only any other non-zero exit is a FAIL. The summary line is
  "PASS n · FAIL n · NOT MEASURED n", then the unmeasured by name, then each failure in full.
- Browser gates run only on the browser seat (DOXA_BROWSER=1; the project rule that browsers are one
  agent's). Elsewhere they are NOT MEASURED without being started, instead of failing to launch.
- `verify` is graded static: verify-all runs its browser guards only with --full, so the plain step
  no longer goes through the browser slot, where it printed "ALL GUARDS PASS (browser guards skipped)"
  and still exited non-zero.
2026-09-26 13:04:15 +00:00
Doxa Maps
1ac32fe3d0 People-group pins say what they are: one shared note, and the groups that share a point are named
A people-group pin is a representative point, not an address, and some pins ARE the same point: on the
Doxa roster 2,107 groups sit on 2,048 coordinates, 108 of them on a point shared with another (gospel-
plugin's measurement). Every map with people-group pins must say so, in one wording.

- library/components/legends/ApproxPinNote.vue: "The dot marks where this people is found, not a town or a
  boundary." always; when the selected pin's point is shared, "It sits on the same point as <names> — <n>
  groups share this coordinate…". Never prints the raw coordinate. Wording: gospel-plugin's P5 proposal,
  in a new pins catalogue (11 locales).
- library/utils/colocation.js: the co-location index, built from the roster THE MAP draws (rows or
  GeoJSON features), never a hardcoded count.
- LegendDesktop and LegendMobile (research map, simple map, country posters, cities, UPG-100) show the
  note under the legend, and under the detail card with the shared names.
- check:approx-pin-note (headless, gates:post): the index, the two sentences, no coordinate, Arabic.
2026-09-26 13:04:15 +00:00
Doxa Maps
205a871d44 check:i18n-never-translated: CSV and JSON are file-format names, the same in every locale by design 2026-09-26 12:23:49 +00:00
Doxa Maps
1795fb4740 Plugin Studio rail laid out like plugin-designs/: one folder per bundle, its elements inside
The Driver's ask: one folder per BUNDLE with its ELEMENTS inside, bundles sorted by capability, highest
first. The rail listed every element flat under its own capability, so a bundle's elements were
scattered (gospel-resources' five screens sat in three sections). Now each capability section (5 → 1)
holds the bundles whose HIGHEST capability it is, in name order; a bundle with several elements is one
folder (country-posters, gospel-resources, my-upg-100-list, regions-dashboard, cities-internet), and a
single-element bundle is shown as its element. Composites keep their folded composition trees.

StudioTree identifies a plugin's row by a `canonical` flag rather than "depth 0", so auto-reveal opens
the bundle folder above an element chosen by URL or restored tab. New folder icon; 'elementsCount' and
'elements' in all 11 studio catalogues.
check:studio-rail (headless, gates:post) runs the studio's real rail code on the built manifest and
asserts the layout and one canonical node per plugin (21 bundles, 33 elements); it fails on the previous
rail. Also: poster.json:dpi is declared identical on purpose in check:i18n-never-translated (a unit
symbol kept as-is by project decision), which had turned that gate red on main.
2026-09-26 12:23:49 +00:00
Doxa Maps
0f136d2513 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-26 11:42:26 +00:00
Doxa Maps
54df0de2a8 The map credit names the basemap that was actually drawn: OpenFreeMap / OpenMapTiles under MapLibre
addMapAttribution always credited "Mapbox · © OpenStreetMap". Under MapLibre the tiles are OpenFreeMap's
(OpenMapTiles on OpenStreetMap), so cities-internet, key-cities and gospel-resources credited Mapbox,
which served nothing, and omitted the OpenMapTiles credit (rega, row 28 at ccc2b1bb). The basemap credit
is now read from the map INSTANCE (instanceof maplibregl.Map, as setProjectionOn does): OpenFreeMap +
© OpenMapTiles + © OpenStreetMap contributors under MapLibre, Mapbox + © OpenStreetMap under Mapbox.
attributionText(provider) gives the print/PDF text per engine; ATTRIBUTION_TEXT is unchanged for
existing importers. check:map-attribution (headless, gates:post): 7 cases.
2026-09-26 08:51:38 +00:00
Doxa Maps
9d38a8156f The funnel page must be in the reader's script, and now that is asserted
THE GAP rega NAMED AND I LEFT OPEN: "the gate passed: it counts rows, not the
row's language." It was right. When the list's label was English among Devanagari,
every assertion on that page was green — sheets, rows, bars, share line,
zero-populations. I asserted the page's STRUCTURE and nothing about whether its
prose reached the reader, which is my own standing rule turned on my own gate.

IT ONLY WORKS IN A NON-LATIN-SCRIPT LOCALE, and that is exactly why it is worth
having rather than aspiring to a general "is this translated?" test, which is not
decidable. On a hi/ar/ru/zh sheet a run of Latin letters is a proper name, a
number, or an untranslated string — three possibilities, two of them enumerable.
In de/fr/es an untranslated string looks like ordinary text and this measures
nothing, so those locales are NOT covered and the gate says so rather than
implying coverage it does not have. rega offered to chase the de/fr/es case; I
declined it for that reason, not for cost.

MEASURED FIRST, ASSERTED SECOND. Across every non-Latin packet in out/ (hi, ar,
ru, zh) exactly TWO Latin runs appear, both the data source's name. That is what
makes a floor of "nothing else" defensible instead of a guess.

THE ALLOWLIST IS THREE ENTRIES AND ALL THREE ARE DECISIONS, NOT BOOKMARKS:
  IMB, peoplegroups.org — the publisher and its domain. A reader following the
    citation meets the English name at the destination, so translating it would
    hide the thing it identifies. Same reasoning the i18n allowlist uses for
    "Open Bible Stories" and "Scripture Earth".
  Project — the campaign designation. Digits break a run, so "Project 4700"
    surfaces as "Project" and the numeral is never part of the match; the WORD is a
    name, while the description beside it ("frontier people groups") is prose and
    must be translated. That is the line check-i18n-never-translated draws around
    "UUPGs", applied in the other direction.
A fourth entry must state whether it is settled or deferred: an allowlist that
does not distinguish the two turns an open defect into a verdict.

MUTATION-TESTED AGAINST THE REAL DEFECT, not a synthetic one. Restoring the
pre-5c254970 English list label on the hi sheet reports ['Project', 'frontier
people groups']; replacing a translated key with English reports ['THIS packet'];
the current sheets report clean. So it fails on the bug it was written for.

Verified: all 18 packets in out/ pass sheets, rows==bars, zero-population and
this new script check — 6 non-Latin packets actively checked, 12 correctly out of
scope. The gate also prints the runs it found, so a reader can see what it
measured rather than trusting a silent pass.
2026-09-26 08:39:54 +00:00
Doxa Maps
6d6b656721 A packet says which list it holds, in the reader's language
TWO FINDINGS FROM rega'S ?list= RENDER, and the first one is a hazard I created
in the same commit where I argued against it.

I MADE PACKET_LIST FAIL CLOSED because "a packet that showed the wrong list is
printed and handed to someone" — and then wrote the frontier packet to the SAME
FILENAME as the default-list packet of that zone and language. rega measured the
result: out/packets/ held frontier packets under the DEFAULT names, beside an `ar`
file that was still the default list, and nothing in the name could tell them
apart. Guarding the input while leaving the output ambiguous protects against
asking for the wrong list, not against being unable to tell what you got.
  Fixed both ways. The list is in the FILENAME
(asia-china-tibet-autonomous-region.frontier.hi.html) and STAMPED INTO THE
DOCUMENT as data-packet-list, because a filename can be renamed, copied or
emailed while the packet itself should be able to say what it is. Same reasoning
as data-packet-build: out/ is gitignored, so a sweep grades whatever files happen
to be there, and a packet that cannot describe its own provenance makes an
accidental set indistinguishable from a current one. The gate prints it too.

THE LIST'S LABEL WAS ENGLISH IN EVERY NON-ENGLISH PACKET. "Project 4700: frontier
people groups" sat among Devanagari labels in a Latin serif face, because it came
verbatim from library/lists/lists.json — which carries ONE label per list and has
no locale dimension at all. The gate passed because it counts rows, not the row's
language.
  It is translated in packet-strings.json rather than by restructuring the shared
lists.json: that file is the packet's OWN catalogue, where a new language is a new
block and never a code change, and it degrades per key like every other string. An
unknown list still falls back to the data's English label.
  THE NUMERIC DESIGNATION STAYS — "Project 4700" / "مشروع 4700" / "4700 计划" —
because it names the campaign, while "frontier people groups" is prose. That is
the same line check-i18n-never-translated draws around "UUPGs": translate what a
bilingual reader would not dispute, keep what is an identity.

rega'S MEASUREMENTS, all confirming predictions I could only compute: 6 pages, 37
of 43 records, rows 12,379 -> 7,341 -> 4,754 -> 2,107 -> 1,745 -> 37, bars
1080/640/415/184/152/26 identical in en and hi, share "2.1% of the 1,745" with no
"1 of 27 lists", provenance-only footnote, fail-closed rc=1 on an unknown list.
And the number I most wanted: share-to-footnote clearance 314px en, 222px hi — so
hi+frontier, the tightest combination that exists (128px rows, gap down to 147px),
still clears by 222px and no label wraps.

Verified: all 18 packets in out/ pass sheets, rows==bars and zero-population.
The 11 laos packets FAILED the zero-population assertion until regenerated — they
predated the fix, which is the gate distinguishing stale artefacts from current
ones exactly as data-packet-build was added to do. Default path unchanged (43
groups, 7 sheets, 5 rows). frontier renders 6/6 in en, ar and hi with the label in
each script. check:packet-strings-parity PASS with the 5 new keys.
2026-09-26 08:39:54 +00:00
Doxa Maps
9c683f1310 README shots wait on the map, not a clock: style + tiles loaded and our layers actually drawn (cap 60 s)
A fixed 7 s wait shot the upg-100-list counter over a bare basemap: that leaf paints 9.5-16.5 s after
load (rega, 3 cold loads at fc2fa2e4). Every map a shot's page or frames create is now recorded (an init
script on the engine global's Map.prototype.setStyle, as in rega's probe), and the capture waits until
every visible map has its style and tiles loaded and has drawn features on OUR layers (GeoJSON sources;
the basemap's are vector tiles), capped at 60 s with a WARN naming the unready map. Each shot's `wait`
becomes a 3 s floor, and the full fallback for a surface with no map. Not browser-run here (browsers are
rega's): the next shot run is the proof.
2026-09-26 08:39:54 +00:00
Doxa Maps
3113344e6a readme-shots: wait 20 s for the UPG-100 list leaf, which paints at 9.5-16.5 s (its counter shows before the paint)
rega measured it at fc2fa2e4 (3 cold loads, tokenless, judged by rendered features on the leaf's own
layers): first paint +9.5 to +14.3 s, idle +12.1 to +16.5 s. The 7 s wait then network-quiet plus
1.5 s made the English shot a coin toss; the Arabic shot of the same leaf (12 s) only won it. Both go
to 20 s. A stopgap: the lasting fix is to wait on the map (style + tiles loaded, rendered features)
rather than a timer.
2026-09-26 08:39:53 +00:00
Doxa Maps
de5a3fdf80 check:no-cross-plugin-imports: a plugin may not reach into another plugin's private folder
research-dashboard and dashboard-plus-grid imported '../doxa-research-map/src/colors/…' — another plugin's
private code — which is how two copies of the same palette drifted (C1a moves them to library/colors).
The gate refuses any relative import that resolves into plugin-designs/<other>/ below that bundle's root;
a composite mounting another bundle's PUBLIC profile .vue (the capability-4 pattern) stays allowed. It
FAILs on main (the 4 palette imports C1a removes) and PASSes with C1a; --self-test in gates:post.
2026-09-26 08:39:53 +00:00
Doxa Maps
1c7104e90b List names speak the reader's language: listLabel and listDefinition
library/lists/lists.json carried one English label per list, so every surface
that showed a list's name printed English in every locale (reported by upg100,
which worked around it inside the packet's own strings).

- A `lists` catalogue area, library/i18n/locales/<lc>/lists.json: a label and a
  definition for each of the five lists, in all 11 locales.
- listLabel(id, t) / listDefinition(id, t) in peopleGroupList.js resolve
  lists.<id>.label through the caller's translator (useI18n's t, tStatic, or any
  key -> string), accept ids and aliases ('700'), and fall back to the
  registry's English. The module still imports no i18n runtime, so it stays pure
  and testable in Node.
- The registry's labels are now documented as the English fallback only, and the
  doxa list gains the definition its catalogue entry has.
- check:list-prop-unit (24 cases, was 17): the Arabic and German catalogues come
  through, a missing translator or a raw key falls back to English, the English
  catalogue must equal the registry word for word (a drifted registry label fails
  it, naming the list), and every locale must carry both fields for every list.
- documentation/reference-universal-props.md: read names through listLabel.

Headless: list-prop-unit, i18n-parity, i18n-never-translated,
no-free-identifiers and universal-props pass. No consumer calls listLabel yet
(the packet has its own strings), so nothing renders differently until one does.
2026-09-26 08:39:52 +00:00
Doxa Maps
07644ffdbe India by state, from India's own states
The province table and the map's province layer both read gb-IND-ADM1.geojson,
which is the UPG-100 list's curated 34-unit partition — not a country's states.
Eleven groups came out with no province because they were standing in states that
partition does not draw. Both now read the complete ADM1 that landed with the
internet work (36 states, geoBoundaries via DataMeet India): 998 assigned becomes
1,005, 44 province rows become 46, and the rows name Indian states.

Four groups are still unassigned and each one is legible: Great Andamanese and the
Sentinelese sit on islands whose simplified coastline their point falls just
outside, and Jing and Pana are on China's Vietnam and Laos borders, outside its
twelve-province subset. They are listed with their coordinates, not dropped.

The staged-assets gate now also asserts the map and the table read the SAME
geometry. Staging could never catch this: both files were staged.
2026-09-26 08:37:10 +00:00
Doxa Maps
a2c2ae4722 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
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-26 08:26:24 +00:00
Doxa Maps
b9f4492752 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work 2026-09-26 08:14:48 +00:00
Doxa Maps
127a2e0930 The funnel states the list it was actually drawn from
A LIVE WRONG NUMBER, not a future risk. The universal `list` prop is already on
main (e3d25ff6) and this profile already honours it — applyPeopleGroupList()
filters the rows at all four fetch sites, packetRecords included — and the
membership files are on disk. So under ?list=frontier-500k the packet's rows were
the 71 network groups that are Project 700, while the funnel took its denominator
from the last step of vision-funnel.json and printed "of the 2,107". Off by a
factor of 30, with nothing on the page looking broken.
Measured from data/people-groups/lists/*.json (counts.doxa, what a Tier A map
shows): frontier-500k 71 · frontier 1,745 · unengaged 1,929 · unreached 2,082 ·
the default 2,107.

THREE NUMBERS WERE WRONG, NOT ONE, because all three descended from one total:
the share's denominator; the footnote's "N are in no list" (total minus assigned);
and "1 of 27 lists", which is a property of the UPG-100 ZONE CATALOGUE, not of the
active list — the zones partition the default list and nothing else.
A non-default list now gets its own step, labelled with the list's own label, so
the narrowing does not jump from one list's total to another list's packet.

AND EACH CLAUSE IS SUPPRESSED SEPARATELY, because they are provable separately.
A first pass dropped the whole share line whenever listCount was absent, throwing
away a true statement ("5.4% of the 1,745") to avoid an unprovable one. The
footnote splits the same way: provenance holds for every list, coverage does not.
⚠ The footnote is NOT split by parsing the translated sentence — hi ends in "।"
and zh in "。", and reading translated prose for structure is the trap
check-packet-render's own comment warns about. It is a second key.
⚠ And `funnel.unassigned ?? 0` would have printed "0 of the 1,745 are not yet in
any list" — a confident falsehood whose zero reads as a completeness claim.

── AND THREE FINDINGS FROM rega'S RENDER, none of which I could see ──────────
"0 people" PRINTED ON THE READER'S ROW OF EVERY PACKET. Number(null) is 0 (only
Number(undefined) is NaN), so `Number.isFinite(Number(x))` admitted the
deliberate `population: null` marker as the number zero. I asked "is this a
finite number?" when the question was "did anyone give me a population?" — the
same presence-vs-value confusion as inferring "did it work" from a field that
answers "did it isolate". Zero is suppressed too: a people group with no people
is missing data wearing a number's clothes.

THE SHARE LINE READ AS PART OF THE FOOTNOTE. Measured: 218px below the reader's
row, 42px above the footnote, because the last row kept the full inter-row gap.
Proximity is the only thing grouping them, so an even gap grouped the share with
the wrong element. The last row's margin is now 14px: share 14px under its row,
288px clear of the footnote.

THE GATE ASSERTED PAGE 2 AND PRINTED NOTHING ABOUT IT. rega: "a reader cannot see
WHAT it asserted." A silent pass is the mirror of a printed-but-unasserted number
— the check is real and the evidence invisible. It now reports rows, bars and the
share line, and gained two assertions: every row must have a BAR (the reader's
step had none, so the funnel never visibly narrowed to "yours"), and no row may
print a zero population.

Verified: all five list cases render their own true numbers — doxa keeps the full
claim, unreached/frontier/unengaged get their own step and a percentage with no
false "1 of K", frontier-500k reads "100% of the 71" with the duplicate row
suppressed (a step that repeats its neighbour is not a step). The four fixtures
are byte-for-byte unchanged on the default path, so the list-awareness is purely
additive. check:packet-strings-parity PASS with the three new keys. Both new gate
assertions mutation-tested in en and ar: removing a bar is DETECTED, injecting a
zero population is DETECTED. 5 rows = 5 bars, 0 zero-population rows, "0 people"
absent. Six-row layout recomputed: fits with 318px headroom, 206px if every label
wraps. BUNDLE=my-upg-100-list vite build clean.

NOT re-rendered in a browser: rega measured the previous state and these are
responses to it, so the new geometry wants one more look.
2026-09-26 08:13:34 +00:00
Doxa Maps
462d12e3b0 Something in CI actually requires page 2
THE HALF I OWED. check-packet-render gained --expect-funnel last commit so a
packet with no funnel page can fail — but nothing passed the flag, and I said so
to regions-plugin while advising them not to reuse the pattern as though
declarable meant covered: "I have traded an invisible defect for a visible
obligation, which is better but not the same as fixed." This is the obligation.

check-packets-all now passes --expect-funnel for every packet it sweeps.

WHY IT CAN BE UNCONDITIONAL. Every packet in out/ is produced by
library/scripts/posters/packet.mjs, which always passes FUNNEL to buildPacket. So
a packet there without page 2 was either generated before the funnel existed or
by a caller that dropped the argument, and this sweep should say both out loud.
Verified across all 16 packets in out/ — the 4 fixtures, the Arabic tibet, and
all 11 laos locales, which I regenerated for this: 95 groups, 9 sheets each (was
8), 95 photos, page 2 present in every one. So the requirement matches a property
the pipeline actually holds rather than one it aspires to.

THE ESCAPE HATCH IS DELIBERATELY AWKWARD: PACKETS_ALL_NO_FUNNEL=1, which prints a
warning. A legitimate funnel-less host is a real state and deserves to be
recordable, but it must be a decision someone takes rather than a default that
quietly tolerates a missing page.

Verified: node --check clean; the arg construction exercised both ways (flag
present by default, absent under the env var). All 16 packets satisfy the sheet
arithmetic with 0 failures. Hindi laos re-checked as the longest-label locale —
5 rows, 5 bars, 0 Devanagari digits, longest label 51 chars so the wrap slack is
not even consumed.

NOT re-run end to end: check-packets-all spawns check-packet-render per packet,
which launches Chromium, and the Playwright policy reserves browsers to rega.
That is also how I learned this wrapper starts browsers — I ran it earlier
without realising and reported it.
2026-09-26 08:13:34 +00:00
Doxa Maps
c33610938a The funnel narrows to the reader, and a missing page 2 can now go red
TWO FIXES FROM A BROWSER RUN I COULD NOT DO MYSELF. rega measured page 2 in
print media and found the opposite of the defect I feared: no overflow, but the
four global steps ended at y~330 of 1056 while "43 · are in THIS packet" sat
alone at y~965, leaving the middle ~60% of the sheet empty so the reader's own
step read as a detached footer. And it had NO BAR, so the funnel never visibly
narrowed to "yours" — a funnel whose final term is not drawn as part of the
funnel fails at the one thing it is for.

So the five steps are now ONE list with the reader's included, and it has a bar
like the others: 1080 -> 640 -> 415 -> 184 -> 26px, the last two tinted so the
network's list and the reader's list read as one thought. The 26px floor is what
keeps the smallest list a visible sliver rather than a hairline.

THE ROW RHYTHM IS DERIVED FROM MEASURED ROW HEIGHTS, not guessed. A first
attempt subtracted a magic 118px as "about one row" and left 418px of dead space
at the foot — rega's emptiness, just redistributed. Summing the real heights
closes it to ~298px.
AND IT KEEPS DELIBERATE SLACK, because two runtime facts are unknowable at
layout time: whether a step carries a population line, and how many LINES its
translated label wraps to. Arabic and Hindi labels are far longer than the
English, so LINE_SLACK reserves one extra wrapped line per row. Computed: 298px
headroom normally, 196px even if EVERY label wraps an extra line. Tight-packing
this page would trade "60% empty" for an overflow in the locales I cannot eyeball.

── AND A GATE HOLE A NEAR-MISS EXPOSED ──────────────────────────────────────
regions-plugin reported, unprompted, that a merge resolved with
`git checkout --ours` silently deleted the printPacket call site supplying the
funnel data — 60 lines — AND EVERY GATE STAYED GREEN. They filed it as merge
discipline. It is also gate design, and that half is mine: the funnel page is
OPTIONAL by design (buildPacket omits it when the arg is null), so a packet
without page 2 is not malformed, just smaller. My own choice made the deletion
invisible.

--expect-funnel closes it. The wrong fix is to always require the page: the 11
pre-funnel laos packets and any host with no funnel data would go red, and a
false red costs as much as a false green. Instead the CALLER, which knows whether
this packet was built with funnel data, declares it. An absence is then either
DECLARED (no flag, fine) or a FAILURE (flag passed, page missing) — never a
silent nothing.

Verified: all 16 generated packets satisfy the sheet arithmetic, 0 failures,
including the 11 laos packets that have no funnel page. fixture-tibet renders 5
rows with 5 bars ending in 43. Arabic regenerated: 7 sheets, dir="rtl", 5 rows,
5 bars, ZERO Eastern-Arabic digits on the sheet. Mutation-tested --expect-funnel
three ways: baseline+flag PASS, funnel-sheet-deleted+flag FAILS, deleted without
the flag PASSES. Flag filtered from the positional read, so a bare
--expect-funnel is not eaten as the packet path (that trap has bitten this repo
three times). BUNDLE=my-upg-100-list vite build clean, +141 bytes vs main.

NOT re-verified in a browser: check:packet-render needs Chromium, reserved to
rega. The geometry above is computed, not rendered — the print-media layout is
exactly what rega's eyes caught that my arithmetic did not, so this wants one
more look.
2026-09-26 08:13:33 +00:00
Doxa Maps
9d56943fae A complete India ADM1 (36 states) is staged beside UPG-100's 34-unit partition
The served gb-IND-ADM1.geojson is the 100-list's curated partition (Mizoram and Nagaland excluded,
listId-tagged), so a map colouring every state had data for 36 and shapes for 34. data/boundaries/
gb-IND-ADM1-full.geojson is all 36 from geoBoundaries gbOpen (boundaryID IND-ADM1-1811400, DataMeet /
Election Commission of India, CC BY 2.5 IN), with source and licence in gb-IND-ADM1-full.meta.json;
generate-tree stages both, and the dev server serves data/ directly. Reproducible with
library/scripts/boundaries/source-full-adm1.mjs (coordinates rounded to 3 decimals: 1.84 MB). The curated
file is untouched.
2026-09-26 08:13:33 +00:00
Doxa Maps
06ea24c98a check:one-palette reads only the PALETTE export's values, not every hex to the end of the file
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
It claimed to read "only the PALETTE export" but matched every hex from `export const PALETTE` to the
END OF THE FILE, comments included. A later note such as "REJECTED #AF7AC5, scored 2.95, under the 3.0
floor" (regions' affinity-block.js, being prepared) would have obliged every dashboard to ship a colour
rejected on WCAG grounds. paletteHexes() now slices to the export's matching closing brace and reads
hexes only inside string literals. --self-test (check:one-palette:self-test, gates:post) plants a hex
in an inline comment, a block comment, a trailing comment and a later export, and requires only the
two real values back. The verdict on main is unchanged (PASS, 6 palettes).
2026-09-26 08:03:23 +00:00
Doxa Maps
20c29442e8 Landing precondition 0: no file in the commit, public/ included, may hold a merge-conflict marker
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
d6bf410a (a merge) committed conflict markers into 10 built files under public/ (big-picture,
dashboard-plus-grid, research-map, research-dashboard, both manifests) and every precondition passed:
a built bundle with markers throws on load while its SRI hash still matches. Step 0 now scans the
COMMIT's tree with git grep (1.3 s, every file, whatever the working tree holds) and refuses.
Run on d6bf410a it names exactly those 10 files; --markers-self-test builds a throwaway commit object
holding a textbook conflict under public/ and requires the scan to refuse it (PASS).
2026-09-26 07:58:06 +00:00
Doxa Maps
c9b3d62111 check:composite-interop: drop the emitFilterState exemption, now that 9384c1ba fixed it
9384c1ba (simpleresearch) captures the instance in setup, so research-map.vue's
map-filtered is dispatched again, and the gate correctly reported the leftover
LATE_KNOWN entry as stale. It failed on main 688ba6cf with exactly that one
finding. With the entry removed, the gate passes and enforces the rule for
research-map.vue again. The two reflectDir entries stay: those sites are
unchanged.
2026-09-26 07:56:40 +00:00
Doxa Maps
a44a3a41a1 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-26 07:47:05 +00:00
Doxa Maps
27bc18a6e1 Name the translator, and let a mobiliser write to them
The credit line said who translated the Scripture and called them "the likeliest
people to ask", then stopped at a name. 838 of the 929 credited groups now reach
that organisation's own homepage from the sheet.

The URL does not come from eBible's homeDomain field, which is still dropped: it
comes from a hand-screened list of 16 organisations, every one fetched on the run
that wrote the table. Where a credit names two, the one it LEADS with wins, so
"Pioneer Bible Translators and The Word for the World International" reaches
Pioneer and not the other way round.

National Bible Societies hold 89+ credits and are deliberately linked to none of
them: several UBS members publish editions carrying the deuterocanonicals, so
whether a Society contact sits inside the 66-book filter is a ruling to be made,
not one to assume. They keep the name without the link, and the count says so.

A contact is screened as somewhere to write to, not as a publisher this map reads
text from — two separate lists, so wycliffe.org appearing as a scripture link is
still a failure. Four organisations answered a datacentre IP with a bot challenge
while serving browsers normally; those are proved by robots.txt and the table
records which method proved which.
2026-09-26 07:43:58 +00:00
Doxa Maps
06209dc45e One pinned-UMD loader for PPLR and the Wall: library/utils/loadPinnedUmd.js
pplr-data-maps and the Wall each carried the same ~40-line d3 loader (pinned, SRI, and a trap that keeps
the UMD from writing into a host page's own window.d3). It is now one library helper, fed from the
CDN registry. check:pinned-umd (headless, gates:post): the tag carries the pinned integrity, a host's d3
gains no keys, and a failed load rejects leaving the host's d3 in place. Both bundles build.
2026-09-26 07:24:25 +00:00
Doxa Maps
a4b79f6875 Each bundle carries only the translation areas it uses (people-groups-grid 611 KB -> 210 KB)
library/i18n/index.js globbed every catalogue area into every bundle: 25 areas x 11 locales, other
plugins' strings included, and each new area grew every bundle (CRITIC-10).

- internal/i18n-areas.mjs walks a bundle's SOURCE import graph (relative, @map/, dynamic import,
  import.meta.glob) and keeps an area when its name appears as a key prefix or a bare string literal
  anywhere reached; runtime-fetched data carries no i18n keys (measured), and every rule errs toward
  including.
- vite.config.js: in a BUNDLE build, a pre-transform narrows index.js's glob to those areas; dev keeps all.
- check:i18n-treeshake (gates:post): every area-prefixed key literal in each BUILT bundle belongs to an
  included area. Poisoned (one area dropped) it fails.
Measured, raw (gzip): people-groups-grid 611,595 -> 210,308 (203,176 -> 72,658); wall 659,876 -> 332,644;
doxa-simple-map 902,907 -> 686,910; research-dashboard 843,625 -> 637,940.
2026-09-26 07:24:25 +00:00
Doxa Maps
0d8d1b3302 Gospel map: provinces for India and China, fetched only when someone looks
P1 rendering. Opening India or China now draws its provinces, coloured by the
same ladder as the country beneath them, and tapping one gives its groups, its
people, how many have nothing, the largest of those and the way in.

It arrives on intent rather than on load. India's ADM1 geometry is 1,628 KB —
nearly the size of the whole world ADM0 file — on a leaf that already loads about
4 MB and is meant to be embedded on thousands of partner sites. Loading it for
the 173 countries nobody is looking at would tax every reader for the benefit of
two. Opening one of the two IS the signal; China is 54 KB, and both are cached
after the first open.

The counts come from the measured table, never recomputed in the leaf. The
point-in-polygon belongs in the build, and a second implementation here is how
two surfaces come to disagree. Provinces repaint when the mode changes, because a
country saying one thing while the province on top of it says another is worse
than having no provinces at all.

The panel repeats that placement is approximate, and that the way in was chosen
for the COUNTRY — which is exactly the limitation going below the border exposed.

Also fixes the gate I added for this, twice. The province fetch interpolates its
filename, so the parse captured the literal prefix "boundaries/" — a real
directory of 4,096 bytes that always exists, proving nothing. The first fix
expanded that prefix against the filesystem, which was worse: unstaging a real
geojson simply shortened the list and the gate stayed green on nine assets
instead of ten, exit 0. The expectation now comes from the filenames the leaf
names, so a file the map asks for and the tree lacks fails. Poison-tested both
ways.
2026-09-26 06:53:22 +00:00
Doxa Maps
91c060974e Map popups show data as text: a people-group name or country field can no longer run script
The library's pin and region popups (useMapEvents) interpolated data fields raw into the HTML handed to
setHTML(); a name like <img src=x onerror=…> from an API or a partner's list would run in the host page
(CRITIC-3). library/utils/html.js adds escapeHtml and safeUrl (http(s) only); every field of both popups
goes through them. check:popup-escaping (headless, gates:post) mounts the real composable, renders both
popups from hostile data and requires no markup, handler or javascript: URL to survive: it FAILs on the
previous code (both popups) and PASSes here. check:cdn-pins and check:no-eval join gates:post too.
2026-09-26 05:50:24 +00:00
Doxa Maps
f14c397758 No bundle needs CSP 'unsafe-eval': translations compile without eval, and the partner CSP is documented
vue-i18n's default message compiler builds every message with `new Function`, so 20 of 25 bundles needed
'unsafe-eval' and a partner with a strict Content-Security-Policy got no translated text (CRITIC-4).
vite.config.js defines __INTLIFY_JIT_COMPILATION__ (vue-i18n 9.14: messages compile to an AST, no eval),
and the bundler drops the eval path: rebuilt doxa-simple-map carries 0 `new Function` (1 before).
check:no-eval (gates:post): no built bundle contains new Function / eval, and vue-i18n in JIT mode renders
real en/ar catalogue messages with Function and eval blocked. documentation/how-to.md lists the CSP an
embedding page needs (style-src 'unsafe-inline' for the shadow <style>, worker-src blob:, tile hosts).
2026-09-26 05:50:24 +00:00
Doxa Maps
4a12b009aa Every runtime CDN script and stylesheet is an exact version with an integrity hash
maplibre-gl@5 and maplibre-gl-geocoder@1 were floating ranges, and no runtime loader set `integrity`,
so a CDN could serve any bytes to every partner's visitors unnoticed (CRITIC-5).

- library/constants/cdn.js is the one registry: mapbox-gl 3.24.0, maplibre-gl 5.24.0 (the release the
  @5 range already resolved to), both geocoders (1.9.4 / 5.0.0) and d3 7.9.0, each with its sha384.
  Every host answers Access-Control-Allow-Origin: *, so integrity + crossorigin work cross-origin.
- ensureEngineScript / ensureEngineCSS, the geocoder loaders (library and PPLR) and the generator's
  static MapLibre tags set integrity + crossorigin from it; the embed snippet's engine preload matches.
- check:cdn-pins (gates:post): no floating CDN url in shipped code, and every one is in the registry;
  --online re-fetches each and compares the hash (all 9 match today).

MapLibre stays on 5.x knowingly. GHSA-jrc7-96c5-q579 (DOM.sanitize, reached through a source's
attribution string) affects <= 6.4.0, and 6.x is ESM-only (no UMD global), so the upgrade is a loader
migration of its own. The attribution strings rendered today come from our configured basemap styles
and our own sources; the note in cdn.js says who is exposed until then.
2026-09-26 05:50:24 +00:00
Doxa Maps
4f47486105 check:profiles-mount: every profile and preset instance mounts in en and ar, not only composites
check:composites-mount gains --all-profiles: every .vue profile of every bundle, and every preset instance
with its props (62 x en/ar, 124 mounted clean, ~38 s), in gates:post. Its entry defines are now recorded
once per bundle per locale (a bundle's second profile read an empty record). Scope stated honestly: this
is a SOURCE mount; an error that exists only in the built bundle (a minified name hoisted out of order)
needs a mount of the build and is not covered here.
2026-09-26 05:16:02 +00:00
Doxa Maps
e3d25ff6db The people-group list is a universal prop: the same maps can show any project's list
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.
2026-09-26 05:16:02 +00:00
Doxa Maps
65017fefa4 The prayer packet shows how its list was narrowed out of all the earth
Page 2 of every packet is now the funnel: 12,379 people groups on earth, 7,341
unreached, 4,754 frontier, 2,107 on this prayer network's list, and N of those
in the reader's hand. A partner holding one list can see where it sits in the
whole, which is the argument the packet is making.

★ IT RENDERS NUMBERS IT DID NOT COMPUTE. Every count is read from
data/people-groups/vision-funnel.json, which build-vision-funnel.mjs generates
from the IMB CSV and the live network list and which already drives the Vision
Card. Re-deriving them in the packet would give one fact two sources; they would
drift, and two internally-consistent surfaces disagreeing is worse than one
wrong number because a reader cannot tell which to trust.

K AND THE SHARE ARE MEASURED AT RENDER TIME, NEVER TYPED, and this mattered:
the brief said "~30 lists of 50-100" and the catalogue says K=27 with counts
from 8 to 118 (west-africa 118, asia-india-rajasthan 116, uncovered-scatter 8).
Only about half sit in that band, so no copy claims it. The page states what the
data can defend -- "1 of 27 lists" and "N of the 2,107" -- and a footnote states
plainly that 21 of the 2,107 are in no list, because a partner adding up rows
would find the gap anyway. Reported to the Driver; K=27 pending his ruling on
whether the two residual buckets count.

A SHARE MUST NEVER ROUND TO ZERO. One decimal gave the single-group fixture
"0.0%", which reads as "this packet covers none of them". Verified exhaustively
over all 2,108 possible list sizes against 2,107: no size prints 0 and none is
malformed. 1 group -> 0.05%, 20 -> 0.95%, 21 -> 1%, 43 -> 2%, 118 -> 5.6%.

NUMERALS STAY WESTERN, as required. num() is toLocaleString(lang), and CLDR's
default numbering system for the bare ar/hi tags is latn -- verified 12,379 and
2,107 in both. Reusing num() rather than writing a second formatter is also what
keeps the funnel's digits identical to every other page of the packet.

PAGE COUNT NOW COUNTS THE ARTIFACT. packet.mjs computed
`1 + ceil(n/PER_PAGE) + 2` under a comment claiming the number was "asked of the
renderer, not recomputed here". It WAS recomputed, and the funnel page silently
made it wrong by one -- exactly the drift the comment warned of. It now counts
the emitted sheets.

THE GATE IS TAUGHT, NOT LOOSENED. check-packet-render went red on every fixture
("43 cards should lay out as 6 sheets, but the packet has 7") -- correct
behaviour on a structure it had not been told about. The wrong fix is to accept
N or N+1: that assertion exists to catch silently dropped people groups, and a
±1 tolerance is the slack a lost list page hides in. So the funnel is DETECTED
via .funnel-title and added as a known term, and a funnel page that renders its
title but no share figure is now its own failure -- a number printed but not
asserted on is decoration.

The page is OPTIONAL: buildPacket omits it when no funnel data was supplied, so
every existing caller keeps working and a host without the data still gets a
correct packet. Better a 4-section packet than one asserting figures it could
not read. A multi-list batch selection keeps the steps but drops "1 of 27",
which is false for a custom selection, while "N of the 2,107" stays exactly true.

Verified: check:packet-strings-parity PASS (31 string keys x 11 locales, my 10
new ones present and translated in all 10 non-English blocks, 0 declared-absent
additions). A partial locale with 3 of 10 funnel keys translated renders those 3
translated and the other 7 in English, nothing blank. Arabic packet generated:
dir="rtl", funnel is sheet 2 of 7, all four steps carry Western digits. All 16
generated packets satisfy the corrected sheet arithmetic, including the 11 older
laos packets that have no funnel page. Mutation-tested: dropping a list sheet is
caught by the independent page-header witness (20 DROPPED), a funnel page with no
share figure is caught by the new assertion, and removing the funnel page passes.
BUNDLE=my-upg-100-list vite build 319 modules clean.

NOT verified in a browser: check:packet-render launches Chromium, which the
Playwright policy reserves to rega. The simulations above parse the same
attributes from the same generated HTML.
2026-09-26 04:38:59 +00:00
Doxa Maps
67730457f6 A map leaf must have our data on it, not just a basemap
verify-map-paint.mjs:293 asserts shadow && glContext && attribution &&
!asksForToken && cssOk && no-real-errors. Every one of those is TRUE of a
flawless basemap carrying none of our zones, polygons or pins, so a data-layer
regression lands green. Measured by starve-probe (abort our
/assets/{boundaries,tables,people-groups}/ and people-groups API requests,
leave the basemap untouched, then read exactly what verify-map-paint reads):
NINE of its thirteen leaves still report a live canvas, live GL, attribution
and ZERO page errors with none of our data on them. The hole is the majority of
the suite. The other four lose their canvas entirely, so the existing gate
already fails them and they are deliberately not listed here -- a leaf list is
a claim about where a failure would otherwise go unseen, not a trophy cabinet.

TWO MEASURES, PASS ON EITHER, because each alone produces a false red on a real
leaf. dashboard-plus-grid's map is a 686x340 pane whose basemap already
contributes ~846 distinct colours, so removing a choropleth deletes AREA
(26.4% of pixels) while barely moving the palette (ratio 0.788 -- I nearly
reported that as a defect). demo-map is the mirror: palette collapses to 0.196
while only 5.0% of pixels change. A map with NO data moves NEITHER.
A global floor cannot work here at all: check:map-paints requires 40 distinct
colours and a starved upg-100-list scores 306.

THE INSTRUMENT MATTERS MORE THAN THE THRESHOLD, three findings:
 - drawImage/readPixels straight off a GL canvas returns an EMPTY buffer
   (preserveDrawingBuffer:false). Three leaves read colours:1, nonzero:0 while
   visibly full of colour. page.screenshot({clip}) goes through the compositor.
 - elementHandle.screenshot() waits for the element to be STABLE, which an
   animating dashboard canvas never becomes; it timed out on three leaves.
 - A FIXED WAIT MAKES THE GATE FLAKY AND THE FLAKINESS READS AS A DEFECT. With
   waitForTimeout(10000): countries-map measured 1868 distinct colours then 54,
   and research-dashboard PASSED at 19.1% then timed out, on identical code
   minutes apart. The fix is a readiness predicate, not a longer clock --
   isStyleLoaded() && areTilesLoaded(), reachable on ANY leaf without a
   per-profile handle via host()._instance.provides.mapStore. countries-map
   then reported 1862->194 / 46.6% twice, settling in 12.7s and 10.5s: the old
   10s wait was 2.7s short, which a clock cannot tell you and the map can.

IT REFUSES TO JUDGE RATHER THAN GUESS, exit 75 (never 0): no canvas, a starve
that aborted 0 requests, images that cannot be compared, a readiness that could
not be measured, or the gate itself throwing. That last one is not theoretical
-- the first run died on a Playwright arity error and exited 1, which a wave
runner reads as "these maps have no data on them". Three leaves are EXCLUDED
with their measured reasons rather than left to emit a permanent no-measurement
that reads as coverage.

--poison starves BOTH runs so neither measure can move and every leaf must go
red; it is mandatory because "either measure moved" is a weaker claim than
either half, and a widened assertion that still passes everything reads as
coverage while proving nothing.
2026-09-26 04:38:59 +00:00
Doxa Maps
924a185aaa "MST" clustering draws again: a true spanning tree in under a second instead of a 12-second freeze
Choosing MST on the research map built and sorted every pair of pins (~2.2 million edges for the
2,107-pin roster), ~12 s on the main thread, and then did it all again for a log line. The page froze,
and check:clustering-modes read the map before any line existed: "MST drew no cluster lines".

- MSTClusteringUtil runs Prim over the implicit complete graph: O(n²) time, O(n) memory, no edge list.
  Starting a new tree when the nearest outside point is beyond maxConnectionDistance gives the same
  spanning forest as before.
- useMapClustering no longer recomputes every tree for its stats log line.
- check:mst-clustering (headless, in gates:post): same edge count and total length as a reference
  all-pairs Kruskal on 2-400 pins, capped and uncapped; 2,107 pins in 872 ms here (budget 3 s).
  On the previous code it FAILs at 11,206 ms.
2026-09-26 04:38:59 +00:00
Doxa Maps
1dfd63b631 Maps stop flying for readers who ask for reduced motion: useMapFly decides it for every camera move
A reader whose OS asks for reduced motion (prefers-reduced-motion: reduce) still got flying and easing
maps: callers passed { duration: 0 } themselves, and flyToCoords took no options argument, so theirs was
silently dropped. useMapFly now routes every camera move (flyTo, easeTo, fitBounds: 12 call sites)
through one guard: jumpTo, or fitBounds with animate:false, under reduced motion. flyToCoords takes and
honours caller options. check:map-fly-reduced-motion runs the real composable against a recording map,
headless: 7/7 PASS here; 5 of them FAIL on the previous code.
2026-09-26 04:38:59 +00:00
Doxa Maps
0ca857ff08 readme-shots: tokenless by default, so the README shows the MapLibre look keyless embeds get
The engine default is now auto (c8fee416): no token means MapLibre, which is what every keyless
partner embed shows, and the Driver wants the README pictures to show that. The script no longer
reads a baked token or fetches doxa.life's, and appends no ?tk=. SHOTS_WITH_TOKEN=1 restores the
old token ladder for Mapbox shots.

Checked headless: vite.doxa-maps-build-config.json tokenUrl is empty and the in-process server has
no /api/maps/token, so a leaf's token ladder ends empty and generate-tree's auto branch mounts
MapLibre with no warning banner (TOKEN_FREE, no 'Token fetch failed' text). node --check passes.
2026-09-26 04:38:13 +00:00
Doxa Maps
4717193961 Flower Mesh opens on an SVG lens: the leaf-engine gate stops expecting a canvas there
upg-neighbor/flower-mesh is a preset of the upg-neighbor element that opens on
its Flower Mesh lens, which is SVG and has no GL map. The leaf is still a map
element: the view-switcher reaches Send Map and the other GL lenses, so it must
keep isMap (engine peer, token ladder). Marking it isMap:false would break those
lenses. check:leaf-engine-per-provider judged it as a drawing map and failed it
on both engines ("drew: none"), before and after the auto engine alike.

- upg-neighbor.instances.json: the flower-mesh preset says "opensOnMap": false.
- generate-tree.mjs: an instance's manifest entry carries opensOnMap:false when
  it is set (absent otherwise, so every other entry is byte-identical).
- check-leaf-engine-per-provider.mjs: such a leaf is not judged as a drawing
  map. Its Mapbox-request count under ?provider=maplibre is still measured.

Headless: after bun run tree, the manifest marks only
upg-neighbor/upg-neighbor/flower-mesh with opensOnMap:false. The gate's leaf
selection, replayed over it, judges 28 of 56 leaves as drawing maps, with
flower-mesh excluded and its five siblings unchanged. No browser run.
2026-09-26 04:21:49 +00:00
Doxa Maps
9fa3b1e484 Composites must mount and their links must exist on both ends, in every landing
check:composites-mount and check:composite-interop join gates:post, and composites-mount is also a landing
precondition (3b, ~20-45 s): 08f89f55 blanked Big Picture and Simple Research while every existing check
passed, so a landing that blanks a composite is now refused before it reaches main.
2026-09-26 04:21:49 +00:00
Doxa Maps
ad61981395 check:composite-interop: a composite's links must exist on both ends, in source and in the built bundles
A composite is a composite because its children talk, and every link is a name
written in two files that nothing ties together. Rename it on one side and the
build stays green while the link silently disappears. The interop matrix also
found the other silent way a link dies: research-map.vue looked up its host with
getCurrentInstance() inside a watcher callback, where Vue returns null, so
map-filtered was never sent, and the only proof had used a synthetic event.

Headless, in about a second:
- A: each link's send pattern is in the sender and its listen pattern in the
  receiver (17 links across 7 composites).
- B: the literal is in every built bundle that ships an end of the link.
- C: every capability-4 composite is mapped, or states why it has no links
  (stack-demo).
- D: no getCurrentInstance() inside a plain function in plugin-designs/ or
  library/. The three routed sites are printed as KNOWN, and an entry left
  behind after its fix fails.

Negative-tested on 521f8b18, and each of these fails: a receiver renamed in
source, a built bundle out of step, an unmapped composite, a new late
getCurrentInstance (one-line function, arrow, multi-line), and a stale KNOWN
entry. check:no-orphan-gates, check:script-flags and check:quoted-scripts pass.
It is not wired into gates:post; that is the build owner's call.
2026-09-26 04:21:49 +00:00
Doxa Maps
3735f27296 check:composites-mount: a composite whose setup throws can no longer look green
08f89f55 shipped Big Picture and Simple Research calling getRuntimeLocale,
detectLocale and RTL_LOCALES with no import. The build was green and every
structural gate passed, but setup threw a ReferenceError in every locale and
both composites rendered nothing.

This gate runs the part that failed, with no browser. Vite loads each
capability-4/5 composite the way its bundle entry defines it (configureApp,
plus ProfileLoader's provides where the entry mounts ProfileLoader), and
@vue/runtime-core mounts it on a no-op renderer in en and ar. Any error from
setup, a watcher's first run or a mounted hook fails it, naming the composite.

Measured on 75c4b54e: it fails exactly the two broken composites in both
locales. On 521f8b18 all 20 mounts pass. Throws poisoned into a directly
defined element and into a ProfileLoader profile both fail. It takes about
20 s. check:no-orphan-gates, check:script-flags and check:quoted-scripts pass.
2026-09-26 04:21:49 +00:00
Doxa Maps
be0a9efecf Token gates follow the auto engine: no token ships, and an explicit engine beats an available token
The build config no longer bakes a Mapbox token, and the default engine is 'auto' (Mapbox only when a
token is available). Two gates still assumed the old contract.

- check:nested-token: with the build config's token empty (the shipped default), no leaf and no bundle
  may carry a pk. token, and every leaf must carry the runtime engine ladder (EXPLICIT / HAS_TOKEN),
  so with no token it draws MapLibre, keyless. A builder who bakes their own token gets the original
  check. Fails on the pre-rebuild tree, which still has baked tokens.
- check:provider-switch: the Mapbox baseline is now ?provider=mapbox with a fake host token (its
  requests are aborted, never sent); the switch run adds ?provider=maplibre to the same token (an
  explicit engine must win); a third run requires the no-token default to make no Mapbox requests.
2026-09-26 03:37:53 +00:00
Doxa Maps
0b9b068b44 Gates: pin-dim-slider runs again, heartbeat follows the translated panel, and the new engine checks are registered
- check:pin-dim-slider: its description sat in "scripts" under the same key as
  its command, so the prose won and bash failed on the apostrophe. It moves to
  "//scripts".
- check💓 the panel's bin labels now call T(key, en), defined above the
  slice the gate evaluates, so the gate threw "T is not defined". It now passes
  the same shape, answering in English.
- check:map-props-unit (Node, 26 cases) and check:auto-engine (browser) had no
  script, so check:no-orphan-gates failed. Both are registered; map-props-unit
  joins gates:post. auto-engine stays a browser run, outside gates:post.
2026-09-26 03:35:18 +00:00
Doxa Maps
6e54c394b8 Dev server: listens on this machine only, never serves .git or .env, and shows data/ edits at once
(cherry picked from commit d01e19c90c)
2026-09-26 03:34:31 +00:00
Doxa Maps
d2afa3f153 The dev server says which commit it serves (/__dev-sha), and a helper finds the shared one
A shared dev server follows main, so what it serves changes under whoever checks against it; a check
that cannot name the commit it measured is not evidence. /__dev-sha answers "<sha>" (or "<sha>+dirty"),
read at request time. internal/dev-server.mjs: url(), devSha(), waitForSha(sha) (resolves once the
served commit contains it) and a CLI, for headless checks against the shared server on 127.0.0.1:5190
(DOXA_DEV_URL overrides). Its header says when NOT to use it: an unlanded change, and release/embed
checks, which stay on the built tree.

(cherry picked from commit e43310e479)
2026-09-26 03:34:30 +00:00
Doxa Maps
0c7f4af47b Engine and token props: a token-service URL is honoured, Clear clears, and a pending token is awaited
(cherry picked from commit 6e4ddceced)
2026-09-26 03:34:15 +00:00
Doxa Maps
3711c590c9 One browser slot on this machine: a second browser run queues instead of racing
(cherry picked from commit 928d38d445)
2026-09-26 03:34:13 +00:00
Doxa Maps
ab17c9b6aa The default engine is auto: Mapbox when a token is available, MapLibre when none is
(cherry picked from commit 3cb89eae52)
2026-09-26 03:34:12 +00:00
Doxa Maps
6989e30f44 check:universal-props (draft): every plugin reads engine, token and locale through the library
Headless static gate for the contract in documentation/reference-universal-props.md: scans each
production and experimental bundle's source (and the shipped bundle for strings the minifier keeps)
for a bypass. The bypasses: a mapbox:// style in setStyle or a raw new gl.Map (a style handed to
useMapInstance is translated there and passes), a bare mapboxgl constructor, a literal provider pinned
into useMapInstance, a window.DOXA_TOKEN read, a pk. token literal, and toLocaleString('en-US')
(warn-only until the Driver sets a numerals policy). Documented design exceptions are ALLOW-listed
with their reason. A self-test plants each bypass and one clean case; a blind or over-eager rule
exits 3. Today: FAIL, 7 bypasses in 4 bundles (routed as defects), 12 locale warnings.
2026-09-26 02:38:06 +00:00
Doxa Maps
5f1db2350f Landing precondition: one freshness pass per tree, so the merge-queue lock is held for minutes
The freshness precondition rebuilds every bundle in its scope (~10 min for a library change) and ran
inside the lock, so a build-owner landing held main for 30+ minutes while other landings timed out
behind it. Its verdict depends only on the committed tree and the scope, so a PASS is now recorded
against both (in the repo's git dir). The landing runs the precondition before taking the lock; the
scan inside the lock finds the record and skips the rebuild. A different tree or a wider scope has no
record and is checked in full, as before.
2026-09-26 02:38:05 +00:00
Doxa Maps
57bf76422f Only a page we own gets its direction set
An RTL locale reflected onto document.documentElement whenever it
resolved — so a reader following a ?lang=ar link to a PARTNER'S page
flipped the partner's whole <html>: their headings, their nav, their
footer. The existing comment already worried about the host owning that
decision, but it only guarded against CLEARING dir, never against
setting it.

A generated leaf is the only thing that knows a document belongs to the
plugin, so it says so: window.MAP_OWNS_PAGE, written into all three leaf
templates. library/i18n reads it before touching the document. Nothing
else sets it, so the DEFAULT IS "not ours" — a partner page that
declares nothing is never touched. Absence has to mean restraint for a
flag like this.

The element's own direction was never this code's business: ProfileLoader
binds dir on .profile-loader from effectiveLocale (0f6d5049), so the
plugin mirrors correctly whether or not it owns the page.

★ THE OLD GATE COULD NOT HAVE SEEN THIS, which is why it needed a new
case rather than a re-run. check-embedded-rtl drove the locale through
profile-config — the channel a HOST uses. This defect fires on the URL
channel, which is what a READER uses, and detectLocale() reads the URL
FIRST. Added a ?lang=ar case on the partner page:

  before  profile-config cases  partner html unset / body ltr   pass
          ?lang=ar              partner html RTL / body RTL     FAIL
  after   ?lang=ar              partner html unset / body ltr   pass
          plugin root stays rtl throughout — it still mirrors

Standalone leaves are unchanged and verified: ownsPage true, html
dir="rtl", plugin root rtl. The page we own still mirrors completely.
2026-09-26 02:38:05 +00:00
Doxa Maps
d2a643d0aa gates:pre green again: the tree generator's quote-escaping regex no longer desyncs the template-literal check
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
template-literal-check reads source with a simple state machine that does not know regex literals.
The .replace(/"/g, …) in esc() looked to it like an open string, and it stayed "in a string" until a
later quote. Its state happened to re-sync on a `//` inside the forge page template; when 87ebbace
removed that line (the static api.mapbox.com preconnect), the check lost track and flagged a
backtick in an ordinary JSDoc comment (generate-tree.mjs:1084) as a stray backtick in a template.
gates:pre failed for everyone.

The regex is now /\u0022/g: the same character, so escaping is unchanged (verified: 'a"b' → a&quot;b),
and the tree regenerates byte-identical.
2026-09-26 02:24:56 +00:00
Doxa Maps
b76cf8f74f The partner-embed check becomes something a person can open
Under the Playwright policy only rega runs a browser here, so this
check's result has to come from someone else's eyes. But the partner
page only ever existed INSIDE check-embedded-rtl — two createServer calls
on ephemeral ports — so "open a partner page at a foreign origin" was not
a thing anyone but the gate could do. NEEDS-EYES rows 3, 4 and 6 name a
runnable probe or an exact URL; row 7 was mine, and it named a construct
that lived in my own process.

--emit writes the same pages to disk, with the two-terminal recipe and
what to look at, so the item is reproducible by whoever has the browser.

⚠ THEY GO IN documentation/, NOT _qa-cycle/. I wrote them there first and
then checked: _qa-cycle is gitignored (.gitignore:113), so the files
could never have travelled to the person who needs them — which is the
entire point. documentation/ is tracked and already carries
README-embed-example.md.

⚠ AND THE ?lang=ar FIXTURE CARRIES NO locale IN ITS profile-config —
1100 bytes against 1114. The two channels reach the document-direction
code by different routes and detectLocale() reads the URL FIRST, so a
config that also supplied the locale would let a reviewer believe the URL
had done it when it had not. The first emit produced all three files
BYTE-IDENTICAL, which would have handed someone three pages they could
not tell apart; I only caught it by checking the emitted sizes rather
than trusting that the flag had taken effect.

The CASES array now carries the ?lang= row itself, so this stands alone:
the emit produces every fixture committed beside it. It previously
depended on the library/ commit for that row, which left the repo with a
fixture the tool would not regenerate — worse than either half.

Also: --emit was being consumed as the baseUrl positional ("SKIP —
nothing served at --emit"). Flags are filtered out of it now. That is the
same bug, in the same shape, that check-leaf-english-leaks had three days
ago — worth knowing it recurs in any gate that takes both a positional
and flags.
2026-09-26 00:47:12 +00:00
Doxa Maps
8f5bd5efb4 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work 2026-09-25 23:57:06 +00:00
Doxa Maps
0b54b0354e Gospel checks: one dev-assets gate, and cover the gap it leaves
The gospel-only dev-assets check is gone in favour of the shared one, which is
better at the job: it tests the /__leaf/<bundle>/<profile> prefix — the one that
actually broke — across all seven map bundles, and confirms each leaf hands its
element the base it claims. The gospel version never looked at that prefix, so
keeping both would have meant two implementations and the weaker one still green
while the map was broken.

What the shared gate samples is the POOL, the files every map shares. It cannot
know this leaf also fetches gospel tables by name, so a table that stopped being
staged would leave it green and this map fetching a 404. That is the only gap
left, and this covers exactly it.

The wanted list is parsed out of resources-map.vue rather than written down,
which immediately earned itself: the hand-written list it replaces was already
two short — pray-index and gospel-link-verification were being fetched and were
not in it. A list maintained by hand is the field-list-goes-stale bug, and this
bundle has produced that three times in one day.

Poison-tested by unstaging a real table: FAIL, exit 1, then restored.
2026-09-25 23:50:31 +00:00
Doxa Maps
87ebbace9b Leaf pages load only the engine their provider asks for: no Mapbox requests under MapLibre
A generated map leaf chose its engine at BUILD time and hard-linked it in <head>: mapbox-gl.css,
the geocoder css, mapbox-gl.js and the geocoder js from api.mapbox.com. The provider is chosen at
RUNTIME (profile-config.provider → ?provider= → window.MAP_PROVIDER), so a partner embedding a leaf
with ?provider=maplibre still had every visitor's browser make 4 requests to Mapbox.

- generate-tree: no static engine tags on map leaves; the element's own runtime loaders
  (ensureEngineScript / ensureEngineCSS, the geocoder's on-demand loader) load exactly the engine
  asked for, the path composite leaves already ran on. The forge page drops its api.mapbox.com
  preconnect. A leaf opened with provider=maplibre (same ladder as resolveMapProvider) is token-free,
  so the public copy no longer asks a MapLibre visitor to paste a Mapbox token.
- check:leaf-engine-per-provider (new, browser): every leaf at ?provider=maplibre makes 0 requests
  to *.mapbox.com and still draws; at its default every map leaf draws with the engine it drew with
  before (--baseline <tree> compares against another tree).
- check:dashboard-geocoder-provider is wired into gates:post alongside it; it had been held out
  because this generator's tags made it red.
- verify-map-paint: optional GATE_VIEWPORT for a phone pass.

Measured before this landing (browser runs made before the headless rule): leaf-engine vs main's
tree PASS, 56 leaves, 0 Mapbox requests under MapLibre, same default engine. Pins, region popup,
PPLR search and key-cities PASS on both engines with 0 Mapbox requests under MapLibre. RTL shaping
PASS on both engines wherever RTL labels rendered. Paint under MapLibre: the same 4 leaves fail as
on main (country-posters-mfe, upg-100-mfe, regions-mfe, flower-mesh). Pre-existing, not caused here.
Poster "Render" is missing on country-posters and doxa-research-map under BOTH engines. Pre-existing
on main, read from source: countries-map.vue renders <PosterDialog v-if="showPoster"> without
v-model or getMap, and PosterDialog shows only when modelValue is true.
Headless, on this commit's regenerated tree: 57 leaves, 0 carry api.mapbox.com engine tags (main's
doxa-simple-map leaf carried 5), 56 carry the runtime provider check, check:sri PASS.
2026-09-25 23:44:04 +00:00
Doxa Maps
3ab1d7aad5 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work 2026-09-25 23:41:23 +00:00
Doxa Maps
17dc7bad87 check:dev-assets: every leaf of the seven studio maps, with bytes that parse and a bogus-path control
Each leaf's dev page must hand its element assetsBase "/assets"; a boundary file and a table under
that base must answer 200 with bytes that JSON.parse (never an HTML fallback), and a bogus file
under it must be a real 404.
2026-09-25 23:22:27 +00:00
Doxa Maps
9dc4ce5ecf Gospel map: a headless check that the dev server holds the assets the map fetches
Every gate for this bundle measured the built tree, and the map was found broken
where none of them look: the dev server. Boundary polygons were missing there on
five maps at once, this one included, while every built-tree gate stayed green —
the thing that differs is how a leaf resolves its asset base, not what the build
produces.

Measured without a browser, which is also why it can run on every change rather
than when a slot is free: /assets/boundaries/world-adm0.geojson answers 404 in
dev while /dx-maps/assets/boundaries/world-adm0.geojson answers 200 with 1.9 MB.
The file is there; the base is wrong.

It asserts bytes and JSON, not just status, because a dev server that answers 200
with an HTML fallback for a missing .geojson is the shell trap this repo has been
caught by twice. A bogus asset path is requested as a control: if that also
answers 200 the gate reports NOT MEASURED, since a server answering everything
cannot tell a present asset from a missing one.

It does not prove the leaf asks for the right path. That fallback lives in
library/ and belongs to studio; this is the other half — whether there is
anything at the end of the base once the base is right.
2026-09-25 23:18:50 +00:00
Doxa Maps
c43a8b793c Dev server: every plugin reaches the asset pool again (no polygons, previews, fonts or tables in dev)
Reported from a laptop running `bun run dev`: under the Plugin Studio, maps drew with no boundary
polygons and UPG-100 had no posters. The built tree was fine, and every gate measured the built tree.

Cause: a dev leaf page is /__leaf/<bundle>/<profile>. A plugin that falls back to the page-relative
'./assets' asked for /__leaf/<bundle>/assets/…, and the leaf route answered with its own HTML at
HTTP 200, so JSON.parse failed and the map drew without our data. /<tree>/assets/ was not served
in dev at all. The built leaf never hits this because its page passes assetsBase "../../assets".

- dev-plugin serves the pool at /__leaf/…/assets/ and /<tree>/assets/ too, and the dev leaf page
  passes assetsBase "/assets" in its profile-config, as the built leaf passes its own.
- resolveAssetsBase's last rung is '/assets' under the dev server (import.meta.env.DEV, false in
  every build); the built tree keeps './assets'.
- check:dev-assets (headless): starts a short-lived dev server for this checkout, requires
  boundaries/previews/fonts/images/tables to answer 200 and not HTML at every prefix a dev page can
  produce, and requires the dev leaf to carry the root-absolute assetsBase. It fails on the parent
  commit (4 prefixes and both leaves) and passes here.
2026-09-25 23:17:35 +00:00
Doxa Maps
58326f2d82 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	internal/check-gospel-rtl-shaping.mjs
2026-09-25 22:55:49 +00:00
Doxa Maps
40d5d8e6bf The docs stop handing the next reader a 404
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Last tick I fixed the one executable literal the tree rename broke, and
wrote that the remaining mentions "will mislead the next reader but
break nothing." That second half was wrong, and worth correcting rather
than leaving.

Six of them are RUNNABLE COMMANDS — usage lines, a --leaf example, two
PROBE_PATH invocations, a BASE= export. Copy-pasting one aims a probe at
a folder that no longer exists, and the failure is silent: a 404 loads
as an empty document and every gate then reports whatever "nothing"
looks like in its own vocabulary. That is exactly how I produced three
confident fake findings in a single sitting last tick — a frame probe
reporting "0 images, no wall" on two different leaves, and a basemap
gate reporting "map never finished its style". A comment that names a
path a reader will then USE is not inert documentation.

So the six say <TREE_NAME> now, which is what the code reads and what
_tree-name.mjs exists to make authoritative.

WHAT IS DELIBERATELY LEFT ALONE, because rewriting it would falsify the
record: 19 mentions are historical narrative — "public/doxa-maps was…",
the census that motivated _tree-name.mjs, and check-catalog-identity's
citation of https://nuxtbase.app/app/doxa-maps/manifest.json returning a
200 sign-in page. That URL is EVIDENCE of a past false-200, not an
instruction; its code already reads TREE_NAME.

Live literals across internal/ are now zero, except public-hygiene's
doxa-life/doxa-maps, which is the private REPO name and correctly
unrelated to the tree.
2026-09-25 22:49:37 +00:00
Doxa Maps
d753998e0f Three gates read the tree's name instead of assuming it
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The output tree was renamed doxa-maps → dx-maps. The name has always
been a config knob, and _tree-name.mjs exists precisely so a rename is
one line — but three gates still built their URLs from the literal, so
they fetched a folder that no longer exists.

THE FAILURE MODE IS WHY THIS MATTERS MORE THAN A PATH FIX. A gate
pointed at a missing tree does not error; it loads a 404, finds an empty
document, and reports whatever "nothing" looks like in its own terms.
Measured in one sitting, all three of these were artifacts of the rename
rather than findings about the product:

  a frame probe returned 0 images and no wall, twice, on two different
  leaves — which I first read as "the composite mounts the wall in a
  second pane";
  check-basemap-labels-localized returned NOT MEASURED, "map never
  finished its style";
  and every leaf under my plugin answered 404 while the card-fits gate
  had been passing, because IT was already migrated and my probes were
  not.

Only the refuse-to-judge guards kept those from becoming filed defects.

WHAT WAS ACTUALLY LIVE, after separating comments from code: exactly ONE
executable literal across the 27 files that still mention the old name —
the --leaf default in check-gospel-rtl-shaping, which I added last tick
when parameterising that gate for the other raw-map owners. The rest are
prose, plus one in public-hygiene-check that is the private REPO name
(doxa-life/doxa-maps) and correctly unrelated. So the fleet-wide
migration was essentially complete and this closes it.

check-leaf-english-leaks now imports TREE_NAME for both its reachability
probe and its leaf URLs; check-poster-card-fits was already migrated in
code and only its comment still described the old path.
2026-09-25 22:27:09 +00:00
doxa-maps-88655c70
dca7d83b53 Freshness covers every bundle a landing can have staled; SRI repairs keep the gallery's manifest copy
Two gaps in the build-owner tooling, found by rega while landing 13f5f3a3:
- check:composite-freshness judged only COMPOSITES. A library change (bd75f609's Arabic catalogue:
  library/i18n eager-loads every locale into every bundle that uses it) left 12 of 25 bundles stale on main,
  most of them not composites; my consumer rebuild d1af039a saw none of it. The check takes --all and
  --bundles a,b; the landing precondition scopes it by what the landing changed (shared code -> every
  bundle; otherwise composites + the bundles whose own folder changed); the build-owner landing always
  uses --all. Proven: at d1af039a it names prayer-walk-planner STALE (0b695f70 vs 0042dda2, rega's numbers);
  on main 13f5f3a3 --all finds the 12.
- fix:sri repaired manifest.json but not the Research Gallery's byte-identical copy
  (doxa-maps-spa/manifest.json, read first on a foreign origin), so an embedded gallery refused a rebuilt
  bundle; it now rewrites the copy too (tested: a changed bundle -> both repaired, identical, check:sri
  PASS). And generate-tree now writes the front page's integrity itself, so `bun run tree` after fix:sri no
  longer strips it (tree alone: 115 agreed, 0 added).
2026-09-25 22:09:40 +00:00
doxa-maps-88655c70
2ff0ecba35 README screenshots retaken after d1af039a; freshness-gate comments name the tree, not the old folder
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-25 22:05:39 +00:00
doxa-maps-88655c70
d38488d799 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	internal/check-gospel-rtl-shaping.mjs
#	public/dx-maps/assets/tables/country-languages.json
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-25 21:38:20 +00:00
doxa-maps-88655c70
56dffd1af8 Gospel map: ask what the country speaks, not what the unreached speak
The bridge candidate — the "possibly a way in" offered to a group with nothing —
was chosen from Doxa's own pins: the biggest language among the UNREACHED groups
in that country that has Scripture and the film. That is not the language a
community shares with its neighbours. For Syenara Senufo in Mali it proposed
Minianka, one people group. Mali's vehicular language is Bambara.

The number beside it was worse than the pick. "8.8% of this country's mapped
population" is a share of Doxa's unreached pins, and a reader takes a percentage
for a share of the country. A percentage measured against a denominator nobody
can see is not a caveat, it is a wrong number.

CLDR publishes what each territory speaks and by what share, joined to ISO 639-3
through SIL's own Part1 column, and pinned rather than tracked from a moving
branch. 1,161 of 1,175 candidates now come from it; the remaining 14 keep the old
proxy and say on the row that their count is of this map's groups and not of the
country. Mali now reads French at 46% and official.

Where two languages are within five points the map names BOTH and says it will
not choose. Mali is exactly that: French 46% and official, Bambara 46% and not —
one is the administrative language and the other is what people share in a
market, and which bridges better depends on whether you are handing over a
booklet or starting a conversation. A tie-break would have quietly answered a
question nobody asked it. Both carry their own links, for the same reason the
first one does.

Two field lists were a field short, the same shape twice. The candidate filter
and the link resolver both read c.s for "has a readable Scripture" and forgot
that Faith Comes By Hearing carries text on the audio record — which excluded
Bambara from Mali's candidates entirely, and then, once it was named, offered it
the film while a full 66-book text sat one field away. There is now one
predicate.

The RTL gate read a hardcoded tree path. It reads the build config instead, and
was re-run against its baseline before the result was trusted.
2026-09-25 21:34:38 +00:00
doxa-maps-88655c70
b862c7d61a Export gate reads the tree name from the build config
After the dx-maps rename, its public/doxa-maps literal made it SKIP (exit 2) without measuring.
Validated against its baseline on the renamed tree: 4/4 one-click exports, 1700x2200 PNGs,
0 errors, as before the rename. Also: usage examples and present-tense comments in the
gallery and planner name /dx-maps/ or the configured tree; both bundles rebuild byte-identical.
2026-09-25 21:32:35 +00:00
doxa-maps-88655c70
ca548b37ba Gospel map: the Deaf groups get their way in on the map, not only in the table
SUN reached the access table and the CSV first, and the map — the surface a
mobiliser actually opens — still showed Deaf groups a panel that explained their
situation honestly and then left them with nothing to do. That is the largest
population on this map with nothing: 93 groups, 17.5 million people, and
structurally so, because a spoken bridge cannot reach a Deaf community and there
was no other kind.

Clicking Deaf Beninese now offers the SUN New Testament, the SUN Bible stories
and the language itself, and says that SUN is already being taught in Benin by
Mission Évangélique des Sourds — a named local Deaf church rather than a
resource with nobody attached to it. It is offered only where the group has
nothing, because a group with Scripture in its own sign language does not need
to learn a second one, and it says on its face that it is a bridge which has to
be learnt and closes no gap in their own language.

Carried in the pin index beside the spoken bridges rather than fetched by the
leaf, so the sheet and the access table cannot disagree about what exists. It is
one small object, not a per-language join, because SUN is one signed language
for all of them.

The doctrine scan reads it too. Those URLs live in a new top-level block that the
per-language loop cannot see, so they were added to the scan in the same change
rather than found missing later, the way the access table was.
2026-09-25 21:00:44 +00:00
doxa-maps-88655c70
a3f50075a6 check:element-census-vs-live: sanctioned PAIRS, so an allowed difference leaves no phantom omission
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
A sanctioned entry only excused OUR string, so the other half of the same control — their "Show
Filters" beside our sanctioned "Hide Filters" (our panel is open on load) — still counted as an
omission. An entry may now carry `theirs` (+ `theirsFlags`): their string for the same control, under
the same Driver citation. It is excused only while OUR half is on the page; if our control vanishes,
their string is an omission again, so a pair can never hide a real gap.

Measured on the live research page with simpleresearch's preset (people-groups-grid): as committed,
THEIRS-ONLY 12 including "Show Filters"; with `"theirs": "^Show Filters$"` added to the Hide Filters
entry, THEIRS-ONLY 11 and "Show Filters" reported as the other half of the sanctioned pair. The preset
itself is simpleresearch's to update.
2026-09-25 20:57:39 +00:00
doxa-maps-88655c70
da016d8b1d The tree-identity guard can check the DATA a gate judges, not just the bundles
servesThisTree fingerprints manifest.json, which describes BUNDLES. MEASURED 2026-09-25 (gospel):
verify:gospel-map adopted another worktree's server and reported PASS, reading 16,867 links, 93
short of this tree — the manifests were byte-identical because the change under test was a data
asset (doxa-gospel-access.json); a planted jw.org link went undetected.

- servesThisTree(base, repo, { assets }) / requireThisTree(base, repo, label, { assets }): a gate
  declares the tree-relative data files it judges, and their served bytes must equal disk too.
  A mismatch or a missing file is a DIFFERENT build -> requireThisTree exits 75 NOT MEASURED,
  naming the file.
- verify-gospel-map converges onto it (its per-gate copy of the check is gone): the tables are
  declared in its adopt check, so a same-manifest/different-data server is not adopted and its own
  server is started instead; a server you NAMED that is another tree is now NOT MEASURED (75), not
  "SKIP — nothing served" (exit 0).

Negative-tested: a copy of this tree with an identical manifest and one planted link in
doxa-gospel-access.json — without assets the guard says "same build" (the incident); with assets it
refuses, naming the file; requireThisTree exits 75; this tree passes; verify:gospel-map named at the
copy exits 75.
2026-09-25 20:57:39 +00:00
doxa-maps-88655c70
405db45f19 browser-slot: a disk floor, and an infrastructure crash is NOT MEASURED, not a red gate
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-25 20:52:19 +00:00
doxa-maps-88655c70
db92d79d0a Landing precondition: judge public/ as COMMITTED, and point stale consumers at the build owner
- Check 2 read public/ from the filesystem, so a gitignored, untracked leftover (a pre-rename public/assets/
  in an older worktree) REFUSED repo's clean README-only landing. It now reads the commit being landed
  (git ls-tree HEAD public/). Tested: an ignored leftover passes; a committed extra folder is still refused.
- The stale-consumer refusal now states README rule 5 (dx-shepherd, 2026-09-25): one agent owns the full
  build. A change that touches library/ or anything compiled into other plugins is handed to studio to land
  with the consumer rebuild; a change stale only in the author's own plugin rebuilds just that bundle.
2026-09-25 20:40:31 +00:00
doxa-maps-88655c70
acb837ff02 Gospel charts: the titles and the counts inside them are translatable
The three chart titles, subtitles and axis units were the last inline English in
this leaf. The subtitles carry a count, so they are held whole in the catalogue
with a placeholder and the number arrives as a parameter — a sentence built by
gluing a number between two fragments cannot be translated, because word order
differs and several of these locales put the number where English does not.

The fallback interpolates too. Without that, a host that never installed i18n
would render the placeholder literally on screen, which is worse than the English
sentence the fallback exists to protect.

The leak gate could not have caught any of this. Whole-label equality is the
right unit for a fixed label and blind to a sentence carrying a number: the
catalogue holds "Across all {n} languages in the corpus." and the screen shows
"Across all 5,067 languages in the corpus." Those are never equal, so three
subtitles could have shipped untranslated in eleven locales with the gate
reporting PASS. A placeholder is now a wildcard and those keys are compared by
pattern; everything else stays equality, which is what keeps a word inside a
sentence from impersonating a label. Comparison went from 2,100 pairs to 2,244.
2026-09-25 20:39:19 +00:00
doxa-maps-88655c70
6d919169c9 A landing is refused unless the commit is fresh, one-folder and SRI-consistent
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 SEVEN composites stale on main (the committed bundle was not what its
source builds). Cause: d49b6c80 rebuilt all 25 bundles in a worktree that had not merged the dx-maps
rename — it wrote the OLD folder and left dx-maps/ stale — and every landing involved passed its own
checks. A check BESIDE the push can be skipped; this makes it a PRECONDITION of the push.

- internal/landing-preconditions.mjs, run through .main-capture/prepush on the exact commit about to
  land: (1) the working tree IS that commit, (2) public/ = README.md + the configured tree only — a build
  under another tree name writes another folder and is refused, (3) check:sri, (4) every composite
  rebuilds to its committed bytes.
- `main-capture scan` (the Driver's harness) runs a repo's .main-capture/prepush, when present, after its
  merge scan is clean; non-zero refuses the landing. Repos without the file are unaffected.

Proven with main-capture scan: (A) main as it was, 7 stale -> REFUSED naming all seven; (B) the rebuilt
main 7c96759d -> PASS; (C) a people-groups-grid string changed and committed WITHOUT a rebuild ->
REFUSED (big-picture, dashboard-plus-grid stale); (D) a public/doxa-maps/ folder beside dx-maps/ ->
REFUSED ("extra: doxa-maps"). Scratch commits were reset, never pushed.
2026-09-25 20:25:47 +00:00
doxa-maps-88655c70
90910f32f6 public/ is one folder again: drop the stale doxa-maps/ bundles a rebuild re-added, and make that a hard failure
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
d49b6c80 rebuilt four bundles under the OLD tree name after the rename to dx-maps (09014f8a), putting
public/doxa-maps/{big-picture,doxa-research-map,research-map-plus-grid,wall-of-people-groups}.js back
beside public/dx-maps/. Nothing references them (the live copies are under dx-maps/). Removed.

check:public-hygiene only WARNED about a root entry outside the drop-in contract unless run --strict, so
the build stayed green. public/ holding only the output folder + README.md is a Driver decision
(BUILD-OUTPUT-IS-ONE-FOLDER), so it is now always a finding. Proven both ways: with the orphans the gate
FAILS ('doxa-maps/ — outside the drop-in contract (dx-maps, README.md only)'); without them it passes.
2026-09-25 19:54:59 +00:00
doxa-maps-88655c70
a7c65c2bb9 check:experimental-shelf looks at the map: paint arm with its own poisons, and settled counts
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Every earlier assertion counted FEATURES, and queryRenderedFeatures answers happily over a white
rectangle (gospel's incident, 0574fe7f). Each map surface now also passes two looks:
  - COLOURS read off the map's own WebGL canvas inside its 'render' event (floor 40), so a legend
    or panel over the map cannot inflate the count and no per-plugin clip is needed;
  - FILL = the map's height over the plugin ELEMENT's (floor 70%), not canvas over container,
    which stayed 1.0 when a stylesheet collapsed both together.
Each arm has its own self-test poison and must be the one that catches it: 'blank' (every layer's
opacity 0) -> "only 1 colour(s) on its canvas ... while 12204 features still count as drawn";
'collapse' (container squeezed to 300px) -> "fills 38% of <all-people-groups>'s height".
Drawn counts are now read SETTLED (equal on two consecutive polls): a first-non-zero read took
1752 of 2107 mid-fit on Cities. The PASS line names only the surfaces that ran.
Measured: 7 bundles, 60 rows PASS, 3 n/a; lowest paint across the 40 map rows 264 colours, 92%.
2026-09-25 19:54:41 +00:00
doxa-maps-88655c70
9fb4f8350a The leak gate proves its exclusions do not blind it
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The numeric-sibling rule shipped last tick as a rider on the RTL commit
and was never confirmed by a run — both attempts returned RC=75. Live,
plausible and unmeasured is the riskiest state a change can be in, and
a GENERAL exclusion makes it worse: "an element whose own text sits
beside a sibling rendering a number is enumerating data" can go green by
excluding too much, which is the same thing as never checking.

So --self-test INJECTS a real chrome leak in the position the rule is
most likely to swallow: an English catalogue value in a label that HAS a
numeric sibling, plus a control with none. Both must still be caught.
The injected value is read from the catalogue rather than invented, so
the self-test exercises the same comparison the real run does.

  SELF-TEST: PASS — 6 of 6 injected leaks caught, including the one
  beside a number. The data, facet and enumerated-value exclusions
  narrow the population without blinding the gate.

  NORMAL: PASS — 8 of 8 combinations in ar/zh/ru/hi, 74-83 comparable
  keys each, so no combination was vacuous. dashboard-plus-grid is clean
  in all four locales, which closes Big Picture cell E.

⚠ THE SELF-TEST'S OWN FIRST RUN REPORTED NOT MEASURED, and that is worth
keeping: every non-flag argv is a target, so `--self-test` was consumed
as a leaf path and all four locales 404'd. The "no combination
rendered" guard caught my harness instead of reporting 0 leaks caught
out of 0 expected, which I could have read as a pass. Flags are now
filtered out of the target list.
2026-09-25 19:42:51 +00:00
doxa-maps-88655c70
2509a6e347 README: three outputs, not five methods; a Languages section; screenshot gates see every shot again
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- Deployment is now organised around what the build makes: (1) one web
  folder (536 files, 76 MiB), embedded by iframe and JavaScript (proven) and
  WordPress (just for fun); (2) the mobile app (Capacitor, built); (3) the
  desktop app (Tauri, proof of concept). A hosting table says where the folder
  can go: dx-life's public/, Cloudflare Pages drag-and-drop (fits its 1,000-file
  / 25 MiB limits), GitHub + jsDelivr (JS embeds only: measured, jsDelivr serves
  HTML as text/plain), or any web server, with the one _headers line JS embeds
  need. No Docker. The diagram and the map-cruncher picture say three outputs.
- Languages: 11 locales via ?lang=, Arabic right to left, with an Arabic shot of
  UPG-100 (Big Picture still leaks English in Arabic; reported). The partner
  profile-config locale is deliberately not documented until it is fixed.
- check-shot-freshness and check-shot-distinctness parsed only '...' paths, so
  after 44ef31b2 turned them into `/${TREE_NAME}/...` templates they saw 2 of 26
  shots: distinctness failed, freshness passed vacuously and hid 22 stale shots.
  Both now accept either form and resolve TREE_NAME, and exit 75 (NOT MEASURED)
  unless every `await shot(` call is parsed (the one dynamic dx-life call is
  marked). Negative-tested. The 22 shots are retaken and checked by eye.
- how-to: the shell no longer overwrites the Studio (it now lives in the output
  folder); README serve line points at doxa-maps/index.html.
- Docs follow the rename to dx-maps (09014f8a): every output-folder path and URL in
  README, how-to, reference, CONTRIBUTING, the CLI section and the develop guide now says
  public/dx-maps/. The repo name, the config file name, the WordPress plugin folder and
  the doxa-* tag names are unchanged (tags are a separate landing).
- Hosting: a live example, the dx-life branch research-gallery, which drops the folder
  into dx-life public/ and fills /research with the Research Gallery.
2026-09-25 19:40:03 +00:00
doxa-maps-88655c70
ebf43e6001 Gospel map: give the bridge language its own links
Both bridge panels named a language and stopped there. For a group with nothing,
the only link on the sheet was "Every resource in their own language" — which is,
by definition, the empty table. Clicking a gap pin showed it plainly: Syenara
Senufo offered five "none found" chips, named Minianka as the way in, and the one
thing to click led to nothing. The sheet told a mobiliser where to go and then
made them go and find it themselves.

Nothing new had to be fetched. Both bridge records already carried the bridge's
ISO 639-3 code and the pin index already held that language's verified links, on
the same screened publishers and through the same doctrine scan as every other
link on the sheet. Minianka is ebible.org/myk/ and jesusfilm.org/watch/jesus/
minianka, and both answer 200 while a made-up sibling path 404s. Joined on the
code, never the name.

All 1,298 bridge records resolve to a language that has at least one link, so no
named bridge becomes a dead chip; the markup renders only what the index holds,
so that stays true if a pool ever loses one.

Also records the ruling on publisher names as settled rather than as this gate's
own judgement: organisation and product names are proper nouns and stay English,
scoped to the Source column and product names. The structural cell rule is what
lets the same words be a name in a Source cell and a translated label in the
chrome at the same time.
2026-09-25 19:27:23 +00:00
doxa-maps-88655c70
09014f8a03 Rename the output folder to dx-maps
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Driver decision: no embeddable URL carries "doxa" in its folder. The tree's name is one line in
vite.doxa-maps-build-config.json ("name": "dx-maps"); everything else follows it.

- __DOXA_TREE_NAME__ (the vite define rega landed, from MFE_NAME = internal/_tree-name.mjs) bakes the name into the
  bundles for the runtime code that must recognise the tree in a URL. StackLayout.vue's
  resolveBundleSrc anchored on a literal /doxa-maps/ and would have broken every composite's nested
  bundles on a renamed tree; it now anchors on the define. The Research Gallery's dev fallback uses
  it (one reader: rega's define, not a second one).
- The native shells follow via internal/sync-app-shells.mjs: capacitor webDir public/dx-maps, Tauri
  frontendDist ../../public/dx-maps (its window url is tree-relative since the one-folder landing).
- The WordPress contract test reads the name from config instead of carrying three literals.
- check:locale-reachable extracted detectLocale alone and died with "localeFromUrl is not defined"
  after 09d03c60 factored a helper out; it now follows the calls it makes (already red on main).
- generate-tree removes its temp asset pool even when it crashes (a failed build leaked ~50 MB into
  /tmp each time; measured on a full disk).
- Capability exemplars regenerated for the new paths.

Proof: rm -rf public && bun run build -> public/ = README.md + dx-maps/; the 25 built bundles carry
zero 'doxa-maps/' paths (scanned); check:sri, check:drop, check:parse, check:public-hygiene PASS.
Tag names (doxa-* -> dx-*) are the next, separate landing.
2026-09-25 19:26:14 +00:00
doxa-maps-88655c70
82d7f21e85 The raw map shapes Arabic, Persian and Hebrew place names
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
upg-100-list builds its map RAW (`new gl.Map`) rather than through
useMapInstance, so it inherits none of the bundle entry's side effects —
including the RTL text plugin. It appeared to work only because the leaf
head PRELOADED the Mapbox peer, which let an entry-time registration
land in time. studio is removing that preload.

⚠ IT IS NOT A UI-LANGUAGE FEATURE, which is why it matters more than it
looks. A basemap draws each place in its OWN script, so over the Middle
East, Iran and Israel MOST labels are Arabic, Persian or Hebrew for
EVERY viewer in EVERY UI language. Unshaped they render reversed — a
WRONG NAME for exactly the groups this map is about — and it fails
silently: no error, no warning, just mirrored text that reads as a font
problem to anyone who does not know the script.

MEASURED with gospel's gate, which PANS to Arabia before judging because
the shaper registers lazily and reads 'deferred' at a world view:

  before   mapbox    ok    58/61 RTL labels shaped
           maplibre  FAIL  62 RTL labels on screen, shaper 'requested'
  after    mapbox    ok    58/61
           maplibre  ok    63/63 shaped

★ THE MAPBOX ARM PASSING BEFOREHAND IS THE FINDING. It is the preload
rescuing an entry-time call, so mapbox looks fine today and breaks the
moment that preload goes. MAPLIBRE ALREADY FAILED, with no preload to
rescue it — this was live for every keyless viewer, not only a future
regression. Measuring just the mapbox arm would have read "already
fine".

The install goes after the engine global resolves and before
new gl.Map: with the engine injected lazily, an index.js call runs
before `gl` exists and does nothing at all.

THE GATE IS PARAMETERISED, NOT FORKED. dx-shepherd routed it to the
other raw-map owners (cities-internet, key-cities, RegionCountryMap), so
it takes --leaf instead of hardcoding gospel's. Copying it would have
given each owner a drifting copy of the pan-first logic and the poison
arm — the two parts that needed corrections to get right.

Also carries the leak gate's third correction: the survivors of the
data and facet exclusions were in .rd-lt inside .rd-li and .rd-bl inside
.rd-bar — DashboardChartsRail renders `{{ s.key }}` beside `{{ s.value }}`
over chart.geometry, the same shape as a facet row. Chasing one class
name at a time is how that went wrong three times, so the rule is now
general: an element whose own text sits beside a sibling rendering a
NUMBER is enumerating data, not naming the UI. Section headings and
column titles have no numeric sibling and stay in scope.
2026-09-25 19:05:59 +00:00
doxa-maps-88655c70
ac6347aa15 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/doxa-maps/doxa-maps-spa/manifest.json
#	public/doxa-maps/gospel-resources/charts/index.html
#	public/doxa-maps/gospel-resources/data-tables/index.html
#	public/doxa-maps/gospel-resources/gospel-dashboard/index.html
#	public/doxa-maps/gospel-resources/gospel-resources.js
#	public/doxa-maps/gospel-resources/resources-list/index.html
#	public/doxa-maps/gospel-resources/resources-map/index.html
#	public/doxa-maps/manifest.json
2026-09-25 19:04:56 +00:00
doxa-maps-88655c70
ac66728a69 Gospel charts: the chart vocabulary comes from the catalogue
Every bar label, tab and legend key in the charts leaf was written inline in
English and never wired to i18n — a surface whose headings were translated while
the words inside the picture were not. Eight of them already had catalogue
entries and were simply not used; the other fourteen are new keys, translated in
all eleven locales from each catalogue's own vocabulary rather than invented
beside it.

The tab strip had to become computed. A plain array is evaluated once at setup,
so it would have kept whichever language the leaf first rendered in and never
followed a locale change; the legend arrays live in the template and already
re-evaluate on render.

Adding a key for "Full Bible" then exposed what the leak gate cannot tell apart.
It read table CELLS as if they were chrome, and a cell whose entire text equals
an English term is indistinguishable from a label by text alone. Both findings it
produced were the language being correct: Source cells carry organisation names —
JESUS Film, Faith Comes By Hearing, Door43 — and Scope cells describe what a
resource contains, one of whose 114 values is the words "Full Bible" while most
of the rest are already in the target language. Translating either would be the
defect. The boundary is structural, so the DOM now draws it.

That leaves the Type column, the one cell column carrying catalogue labels, with
its own arm. Asked the obvious way round it was wrong: requiring every translated
type to be present failed on Gospel audio in all four locales, because 1,027 rows
carry it and none was on the first page of fifty — pagination reported as a
translation defect. It asks instead that no cell equal one of the eight
enumerated type strings, which stays silent for a type that is off-screen and
fails for one that is on-screen and English.
2026-09-25 19:03:44 +00:00
doxa-maps-88655c70
ffef2b761e Raw maps shape Arabic, Persian and Hebrew place names: cities-internet and key-cities
These two maps build `new gl.Map` directly, so they never got useMapInstance's RTL install, and
their bundle entries registered nothing either. The basemap draws each place in its OWN script,
so over the Middle East most labels are Arabic, Persian or Hebrew for EVERY viewer: they were
drawn as disconnected glyphs in the wrong order. installRtlTextPlugin(gl) now runs after the
engine global resolves and before new gl.Map (an entry-time call cannot work once the engine is
injected lazily).

Gate: check:rtl-raw (+ :poison), gospel's check-gospel-rtl-shaping method (pan to Arabia first;
no RTL label on screen is NOT MEASURED; one defineProperty per global for hook + poison) over
cities-internet on both engines and key-cities (Mapbox-only, gated, no leaf: mounted on a
same-origin host page). The shared gate's --leaf argument can replace the cities rows once it
lands; the key-cities host page still needs this one.
Measured: before, cities-internet mapbox 58 RTL labels with the shaper 'unavailable' and maplibre
62 stuck at 'requested', both unshaped; after, shaper 'loaded' on both, and key-cities 56 RTL
labels shaped. Poison: 3/3 rows caught.
2026-09-25 18:59:31 +00:00
doxa-maps-88655c70
32b4e5ec6c Gospel tables: translate the resource types, and let the leak gate see every locale
The gate could only ever read Arabic. Its rule — English prose declares itself
with dir="ltr" — is right about the prose and wrong about the root: the embedded
RTL fix sets direction on the plugin's own root element, so under ar the root was
rtl and the walk ran, while under zh, ru and hi the root matched the prose test
and the entire leaf was skipped. Measured on identical DOM in both locales: 1,359
elements walked and 589 carrying text either way, of which 1 sat in a prose
subtree under ar and all 1,359 did under zh.

It then reported that silence as "rendered no labels at all", which reads as a
product failure and is not one. The Chinese leaf renders correctly, title and
all. Silence is now NOT MEASURED, reported separately, and never counted as a
pass. dir is honoured below the root and ignored on it; the comparison went from
488 key/locale pairs to 1,876.

What that exposed was a translation that gave up halfway. Type is an enumerated
label whose eight values the catalogue already translates, and the heading above
it was translated while the column beneath stayed English. The map is derived
from the catalogue rather than listed in the component, so a ninth resource type
is covered the moment it is added; a hand-written list would have been one entry
short and nothing would have failed. Both code paths localise, including the
filtered view a pin or country click produces.

Two values had no key because the catalogue spelled them differently from the
data — Audio Scripture and Bible Stories, now translated in all eleven locales.
Open Bible Stories stays English: it is a product name, as are the organisations
in the Source column beside it.
2026-09-25 18:24:26 +00:00
doxa-maps-88655c70
38b1963d4e Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work 2026-09-25 17:47:32 +00:00
doxa-maps-88655c70
cac041da42 Gospel access: a bridge for the Deaf, and screen the links nobody was screening
Of the 369 Doxa groups with nothing in their language, 94 had no bridge either,
and 93 of those were Deaf — 17.5 million people, the largest untouched number on
the map. That was structural, not bad luck: every bridge here is a spoken
language widely used in the same country, and a spoken bridge is worthless to a
Deaf community, so the build correctly refused to offer one and then had nothing
else to say.

SUN is a signed language built to be learnt across sign languages, with the Old
Testament, New Testament and Bible stories in it. It is carried the way a spoken
bridge is — something to learn, no substitute for Scripture in a group's own
sign language — in its own columns, never folded into the access rung, which
stays the honest "nothing found in their language". 28 countries where it is
already being taught come with the named local Deaf church or organisation
doing the teaching, which is the difference between a resource and a lead.

Only URLs verified by their content ship. sun.bible answers 200 for
/sun/<anything> and /resource/<anything>, so a per-language URL built by pattern
would look valid and could be empty; the fetcher re-measures that catch-all on
every run rather than trusting a note. Hosts left out are listed with the reason,
including three that are reachable by a person but not by a script.

Doctrine was checked harder here, not less. The Deaf are the most heavily
targeted group in this dataset by publishers this build must never carry, and
the thinner the pickings the stronger the pull. SUN is Wycliffe Associates'.

Screening the result exposed the bigger hole. The doctrine scan read the pin
index only, and within it skipped the Words of Life and Scripture Earth fields,
so the access table — the one the page renders and the one exported to CSV for
posters and QR codes — was never screened at all. The least supervised links in
the build were the ones most likely to be printed and handed to a stranger.
Scanning both, and both fully, takes the screened count from 6,497 to 16,960.

The gate also now proves the server holds these tables. It adopted a server from
another worktree on the default port and reported PASS on it: all three
manifests are byte-identical, because a manifest describes bundles and the
change was to a data asset it never mentions. A planted excluded-publisher link
went undetected, which is the worst possible failure for this particular check.
2026-09-25 17:47:00 +00:00
doxa-maps-88655c70
79bd6d2ac3 public/ is one folder: the output tree plus a README, with the Plugin Studio as its front page
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Driver decisions (cards BUILD-OUTPUT-IS-ONE-FOLDER, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE).

- public/ holds only <tree>/ and a generated README.md. The build no longer writes public/index.html,
  public/assets/ or public/__gallery/; `rm -rf public && bun run build` recreates exactly these two.
- The Plugin Studio is <tree>/index.html (tree-base "."), replacing the staging-forge chooser; nothing
  is written above the tree. check:staging-lang, which tested that chooser, is retired with it.
- Assets are gathered in a temp directory and the fetched allowlist staged into <tree>/assets/ as
  before. fonts/, images/, brand/ and legacy/ existed ONLY in public/assets: they are sources and move
  to data/ (git mv). Poster previews keep their tracked sources in plugin-designs/*/assets/previews
  (all 378 verified present).
- __gallery/ is archived out of public/ (a copy kept in the umbrella's _qa-cycle/archive) and out of
  the build; generate-gallery renders only on request, into a directory you name, never public/.
- Native shells load the output folder: capacitor webDir public/<tree> (cap copy needs an index.html
  there), Tauri frontendDist + window url relative to it. internal/sync-app-shells.mjs keeps both in
  step with the tree's name on every build, so the coming rename stays one config line; the Capacitor
  start page resolves its leaf tree-relative.
- dropin.include now lists only extras (README.md): the tree is always shipped and never named twice;
  excludes are tree-relative ({tree}/...). clean.mjs removes the old root entries from existing
  checkouts; the build lock and public/.gitignore's rules move out of public/.
- check:wp-embed read the BUILT manifest from gates:pre, before the build, so a from-scratch build
  could never pass; it runs in gates:post and reads the tree name from config.
- Stale bundles refreshed by the clean build (key-cities, template-bundle, wall-of-people-groups);
  the CLI doc's quoted scripts re-synced; capability exemplars regenerated; the '#' note leaves
  check:stale-sweep-claim's command (it was already in //scripts).

Proof: rm -rf public && bun run build -> public/ = README.md + doxa-maps/; check:sri, check:drop,
check:parse, check:public-hygiene, check:quoted-scripts, check:capability-exemplars PASS; all 57 pages
(the front page + 56 leaves) mount under /doxa-maps/ on this tree. check:facet-cascade is red as on main.
2026-09-25 17:41:56 +00:00
doxa-maps-88655c70
2a2ea36e98 A leak check that can be pointed at any leaf, and two false positives it taught me
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Cell E verification for Big Picture and Simple Research needed the
gospel leak gate's method on two other bundles. That gate hardcodes its
four leaves, its element tag and its single catalogue file, so pointed
elsewhere it finds no shadow root and silently compares nothing. This
one takes the leaf, the tag and the locales as arguments and compares
against EVERY catalogue area — a COMPOSITE has no t() calls of its own,
so its chrome comes from the children it nests.

SIMPLE RESEARCH CELL E IS CLEAN, independently measured:
research-map-plus-grid in ar/zh/ru/hi, tree identity checked first, no
leaks, 0 page errors, and 86/34/27/43 comparable keys — so the
comparison was non-vacuous in every locale rather than "0 leaks over
nothing". A second instrument agreeing with the owner's reading is what
the verifier role is for.

★ TWO FALSE POSITIVES, BOTH OF WHICH LOOKED LIKE TEXTBOOK DEFECTS. The
first run reported 3 leaks x 4 locales on Big Picture, and all three
keys ARE translated in every catalogue, so the pages really were showing
English where a translation existed.

  DATA CELLS. "Unengaged" rendered in .rd-td inside .rd-grid — table
  data. people-groups-grid.vue:150 records the field as
  {value:'unengaged', label:'Unengaged'}, 100% populated, and
  dashboard.config.js:132 counts 2,081 Unengaged / 27 Engaged. It is an
  API VALUE that happens to equal a catalogue string, which is the
  population rega's method excludes by name. Whole-label matching stops
  a substring impersonating a label; it cannot stop a data cell
  rendering the same text.

  FACET VALUES. "Other" rendered in span.fr-lbl in the facet rail, which
  LOOKS like chrome. The template is
  `<span class="fr-lbl">{{ r.label }}</span>` with `{{ r.count }}`
  beside it, and the component's own comment calls it "the 'Other'
  bucket". The string comes from row data, not from t('regions.Other').

Both are excluded BY ROLE rather than by class. The facet exclusion keys
on the row carrying a COUNT, so the rail's own section headings stay in
scope and a real leak there is still caught — excluding .fr-lbl
wholesale would have blinded it.

⚠ NEITHER IS GOSPEL-SPECIFIC. Any bundle with a data grid or a facet
rail produces these, and whether "Unengaged" and "Other" as DATA should
be localised is a genuine product question — but a different one from
cell E, and filing it as a leak would send someone to fix correct code.

⚠ BIG PICTURE CELL E IS NOT CLAIMED CLEAN. The confirming run after the
facet exclusion came back RC=75 (no browser slot within 900s), and the
run before it still reported regions.Other. Whether the exclusion takes
effect is unverified, so the cell stays open rather than being marked
on an unconfirmed reading.
2026-09-25 17:26:29 +00:00
doxa-maps-88655c70
b9dd5545c8 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/doxa-maps/doxa-maps-spa/manifest.json
#	public/doxa-maps/gospel-resources/charts/index.html
#	public/doxa-maps/gospel-resources/data-tables/index.html
#	public/doxa-maps/gospel-resources/gospel-dashboard/index.html
#	public/doxa-maps/gospel-resources/gospel-resources.js
#	public/doxa-maps/gospel-resources/resources-list/index.html
#	public/doxa-maps/gospel-resources/resources-map/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 17:12:17 +00:00
doxa-maps-88655c70
d633f496d7 Gospel map: shape Arabic, Hebrew and Persian place names
The map is built raw, for the provider switch, so it never installed the RTL
text plugin — the same opt-out that cost it the engine stylesheet. Without the
plugin those labels draw as disconnected glyphs in the wrong order, which is
unreadable rather than merely ugly.

This was never an Arabic-locale bug, which is why it went unseen. The leaf does
not localise basemap labels, so every place is drawn in its own script: over
Arabia, 57 of 61 rendered symbols are Arabic for every viewer whatever their UI
language. An English reader sees broken Arabic they cannot evaluate.

It installs after the engine global resolves rather than at bundle entry. This
bundle injects its engine lazily, so at entry time there is no engine to install
into and the call would silently do nothing.

The gate pans to Arabia before judging. Registration alone proves little, since
the plugin is registered lazily and a freshly loaded map reports "deferred"
whether or not the shaper can actually be fetched; only real RTL text on screen
turns it "loaded". Asserting without panning would have failed a working map.
2026-09-25 17:10:04 +00:00
doxa-maps-88655c70
30a11a175c Merge main into bundler-2/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/doxa-maps/doxa-maps-spa/manifest.json
#	public/doxa-maps/gospel-resources/charts/index.html
#	public/doxa-maps/gospel-resources/data-tables/index.html
#	public/doxa-maps/gospel-resources/gospel-dashboard/index.html
#	public/doxa-maps/gospel-resources/gospel-resources.js
#	public/doxa-maps/gospel-resources/resources-list/index.html
#	public/doxa-maps/gospel-resources/resources-map/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 17:09:33 +00:00
doxa-maps-88655c70
93ace277df Match the live page's wording where we differed only by case
The census against doxa.life/research found our grid saying "Full profile" and
"Load more" where the page we clone says "Full Profile" and "Load More". Case,
nothing else — the kind of difference that is invisible in review and obvious
side by side. Fixed in the English catalogue and in the fallback literals; the
other ten locales are untouched, since none of them has a capital to drift.

The filter toggle's "Hide Filters" is now SANCTIONED in the preset rather than
fixed, with the Driver's words, the date and the card: our filter panel is open
on load because the Driver reported not being able to find the dropdowns, so our
toggle offers to hide where theirs offers to show. Same control, other state.

Everything else the census found went to the Driver as
DECIDE-RESEARCH-CENSUS-DIFFERENCES rather than into the allowlist — the legend's
prayer-band wording, our four additions, and their page title.

★ AND THE CENSUS TURNED UP A REAL DEFECT WHILE CONFIRMING THE "DATA DRIFT".
The drift is OURS. Measured against pray.doxa.life/api/people-groups/list with
this repo's own thresholds (prayer-progress.js: 0 or null = Needs Prayer):

    API         Needs Prayer 1,162 (58.8M)    Has Prayer 945 (147.5M)
    doxa.life   Needs Prayer 1,162 (58.8M)    — matches the API exactly
    our legend  Needs Prayer 1,417 (69.1M)    Has Prayer 690 (137.1M)

254 people groups the API says have someone praying are drawn and counted by our
map as having nobody. Their snapshot is right and we are wrong. Recorded on the
card with a candidate cause (useMapLayers.js:226 reads only the camelCase
peoplePraying and defaults to 0, where getPrayerLevel also accepts the snake
_raw.people_praying) and passed to the shepherd for its own card rather than
fixed inside a wording commit.
2026-09-25 17:08:16 +00:00
doxa-maps-88655c70
1870efa5c9 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/doxa-maps/gospel-resources/charts/index.html
#	public/doxa-maps/gospel-resources/data-tables/index.html
#	public/doxa-maps/gospel-resources/gospel-dashboard/index.html
#	public/doxa-maps/gospel-resources/gospel-resources.js
#	public/doxa-maps/gospel-resources/resources-list/index.html
#	public/doxa-maps/gospel-resources/resources-map/index.html
2026-09-25 16:39:46 +00:00
doxa-maps-88655c70
03a01f99bf Merge main into bundler-2/work
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-25 16:37:11 +00:00
doxa-maps-88655c70
78021cd854 Census Simple Research against the live research page it clones
check:element-census-vs-live now has a preset for this lane, so the question
"what is on our page and not on theirs, and the reverse" has an answer that runs
instead of an opinion.

★ THE LEAF IS THE COMPOSITE, NOT THE GRID ALONE. people-groups-grid is a copy of
DX-life's UupgsList.vue, which is the BODY of doxa.life/research — but that page
is a map plus that body. MEASURED: censusing the grid leaf against /research
reports 45 omissions, and every one of them is the research map's chrome
(Clustering, Off/Native/MST/Network, the tab strip, the legend) — nothing to do
with the grid. A list that long and that wrong teaches a reader to ignore the
gate. research-map-plus-grid is map + grid, which is what /research IS, and it
lines up 98 strings against their 95.

The negative control is the pin hint ("someone still needs prayer"), and the run
confirms the census SEES it as ours-only — without that, every count it prints
would be worthless.

The sanctioned list is deliberately EMPTY. The first run's 14 additions and 14
omissions have gone to the shepherd as findings; an allowlist written by the
owner of the code to make its own gate green is not evidence. Most of the
omissions look like wording rather than absence — their "Full Profile" against
our "Full profile", their "Load More" against our "Load more", their legend's
"1+ People Praying" against our "Has Prayer" — but that is exactly the call the
Driver makes and not the one this file should quietly make for them.
2026-09-25 16:37:10 +00:00
doxa-maps-88655c70
e52150b500 Map paint gate: say which tree it measured
The gate defaults to the deployed site, so a failure line reads as "this code
is broken" when it means "this code is not published yet". That misread cost
two wrong diagnoses in one day: the resources map and the connectivity map both
reported the stylesheet absent while the fix for exactly that sat verified on
disk. Served locally, the same gate passes all thirteen leaves.

The identity guard cannot close this. It fingerprints the manifest to catch the
wrong worktree on a localhost port and deliberately passes a remote target,
because lagging behind a checkout is a deployment's normal state. The output
was the thing at fault: the one number that tells the two failures apart was
never on screen.
2026-09-25 16:36:54 +00:00
doxa-maps-88655c70
3c3e357068 check-gospel-map-paints reads the tree's name from the build config too
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
It landed on main after the tree-name codemod ran and carried one 'doxa-maps' literal.
2026-09-25 16:26:03 +00:00
doxa-maps-88655c70
456b920fb6 browser-slot: a long gate declares its own hold limit; check:host-locale-contract takes 2700 s
A full check:host-locale-contract run loads every leaf twice and hit the 900 s default three times
(exit 75, NOT MEASURED). An env prefix in its package.json script cannot help: the wrapper reads its
limit before the script's environment exists. So a gate declares it in its header
(@browser-slot-max-hold <s>); the wrapper reads it for 'node <file>' and 'bun run <script>' alike.
The caller's BROWSER_MAX_HOLD still wins, a declaration is capped at 3600 s, and every other
command keeps the 900 s default. Tested on a private slot dir: declared 3 s -> killed at 3.1 s;
caller override 1 s -> 1.1 s; bun run form -> 3.2 s; 99999 -> capped to 3600.
2026-09-25 16:25:22 +00:00
doxa-maps-88655c70
44ef31b2c6 Tooling reads the tree's name from the build config, in one place
The output tree's name has always been configurable (vite.doxa-maps-build-config.json "name"),
but 61 tooling files ignored it and hardcoded 'doxa-maps' (181 literals), so a rename would have
moved the build while the gates kept reading the old folder.

- internal/_tree-name.mjs reads it once (validated as one URL-safe path segment; falls back to the
  historical name) and exports TREE_NAME / TREE_NAME_RE; `node internal/_tree-name.mjs` prints it
  for shell scripts.
- A codemod rewrote 171 literals in internal/ to TREE_NAME; the regex literals, messages with
  backticks, atomic-public-swap.sh (fails loudly if the name cannot be read), build-lock.sh and
  vite.config.js (its duplicate readMfeName is gone) were done by hand.
- The name is unchanged: old and new tooling produce all 1408 files under public/ byte for byte.
  Code lines still naming the tree in internal/ + vite.config.js: 2, both intended (the helper's
  fallback, the private-repo-name pattern in public-hygiene-check).

This renames nothing visible; the eventual rename becomes a one-line config change.
2026-09-25 16:25:22 +00:00
doxa-maps-88655c70
577149afb4 check:experimental-shelf: test the partner embeds too, from another origin
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Two surfaces join the leaf and the Gallery, each built from the manifest's own snippets so a
broken snippet fails as well:
  B  a host on a different origin iframes the leaf; the plugin must draw its data and the
     host's title, URL and scroll must not move.
  C  a host on a different origin pastes embed.script with its own token and provider. Under
     maplibre: 0 api.mapbox.com requests. Under mapbox, where the plugin draws a Mapbox map, a
     second run with a fake host token must show that token, and no other, in tile requests.
     MapLibre-only plugins take no token and report n/a.
The self-test now also poisons the bundle under the script embed. Rows print their seconds;
--surfaces A,B,C,D splits the ~26 min run. Measured: 7 bundles, 60 rows PASS, 3 n/a.
B does not assert 0 api.mapbox.com: every generated leaf head links Mapbox JS/CSS whatever
the provider, which is a template property reported separately.
2026-09-25 16:23:49 +00:00
doxa-maps-88655c70
7c9e3d9fc4 Take main
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-25 15:54:57 +00:00
doxa-maps-88655c70
0574fe7ffa A gate that looks at the map, because every other one passed over a blank rectangle
Injecting mapbox-gl.css collapsed this profile's canvas to a white strip, and
every assertion I owned went green over it: verify-gospel-map counted 2,107 of
2,107 pins "rendered", leaves-mount found a full shadow root, the i18n gates
found every label translated and nothing clipped. queryRenderedFeatures does not
care whether anything is visible. I caught it by opening the screenshot, which is
not a system.

So: screenshot the map region and count distinct colours — a basemap has
hundreds, a blank rectangle one or two — and check the map still fills its
element.

THE POISON ARM CORRECTED THIS GATE TWICE, which is the argument for having one.

First it hid the canvas, and the region still showed 316 colours: the sample was
taken over the whole custom element, and the legend card is itself full of
colour. The sample is now clipped to the right of the legend, so it looks at map
alone.

Then it collapsed the container, and the gate still passed — because I was
comparing the canvas to its own container, and in the real incident BOTH
collapsed together, leaving that ratio at 1.0. The check would have passed the
exact defect it was written for. The ratio that matters is map-to-ELEMENT, and
the poison now reproduces the incident rather than approximating it: 1440x300
inside a 1440x900 host, caught at 33%.

Measured after: 644 distinct colours, map filling 100% of the element, and the
poison arm red.
2026-09-25 15:54:03 +00:00
doxa-maps-88655c70
737059467d The never-translated gate goes green on what it has settled
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
It reported four failures and could not be run in CI, so a real
regression would have landed behind an already-red gate. Two of the four
are not defects at all, and two are open in a way code cannot close.

PROPER NOUNS, now allowlisted with reasons. "Open Bible Stories" and
"Scripture Earth" are the names of the organisations that publish these
resources, not descriptions of them: a reader following the link meets
the English name on the destination site, so translating it would hide
the thing it identifies. They sit in the same res.* block as
`jesusFilm: 'JESUS Film'`, which the product already treats as a brand,
and resources-list.vue:234 maps the literal string 'Scripture Earth'
back to this key — it is an identity, not a label.

DRIVER-GATED, and allowlisted as a BOOKMARK rather than a verdict.
legend.columns.upgs and prayerLaps.upgs really are an English acronym
wedged into an otherwise localized row, in Arabic and Chinese included,
while `population` beside them translates normally. That stays true; it
is simply not resolvable here. Measured this tick: the column's own
tooltip (legend.sortByUpgs) is ALREADY translated in all 11 with only
the acronym left English — "Ordenar por UUPG", "按 UUPG 排序",
"ترتيب حسب UUPG" — and the widest candidate phrase is 28% of the 380px
panel (zh costs 9px). So neither engineering nor space is the blocker:
it is terminology only, and an invented acronym looks AUTHORITATIVE to a
reader who cannot check it.

★ "ALLOWED" IS NOT ONE THING, and collapsing the two would make this
gate lie. A proper noun is SETTLED — correct English forever. A
driver-gated key is an OPEN DEFECT parked so the gate can still catch
the next regression. Calling it "identical on purpose" would turn a
deferral into a verdict, which is exactly how an open item stops being
visible. The gate now separates them: the PASS line itself reads
"2 deferred pending a ruling" and names both keys above it.

MUTATION TESTED — making a NEW key identical in all 11 locales
(buttons.prayerList) still fails it, exit 1, naming the key. The
allowlist exempts without blinding.

The priced options are on
DECIDE-UUPGS-COLUMN-HEADER-IS-ENGLISH-IN-ALL-ELEVEN-LOCALES; these two
lines come out when the Driver rules.
2026-09-25 15:49:47 +00:00
doxa-maps-88655c70
0f6d504915 An RTL locale turns the plugin, not the partner's page
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Arabic text was rendering inside a left-to-right layout: readable glyphs
in the wrong place, which reads as a styling quirk rather than the bug
it is.

TWO DEFECTS IN ONE BLOCK (library/i18n/index.js, createAppI18n):

  TARGET — it wrote document.documentElement.dir. An EMBEDDED widget
  doing that flips the PARTNER'S WHOLE PAGE: their headings, their nav,
  their footer. The existing comment already worried about the host
  owning that decision, but guarded only against CLEARING dir, never
  against setting it.

  TIMING — it ran at createAppI18n() construction, reading
  detectLocale(), which sees only the URL and <html lang>. A locale
  arriving later through profile-config — the channel a HOST uses —
  never reached it. Measured across the previous landing: htmlDir null
  on every host-channel run, including leaves whose chrome was fully
  Arabic.

Direction now binds on ProfileLoader's own root, driven by
effectiveLocale — the locale actually in force AFTER profile-config, so
it re-evaluates when a host changes the language. This element cannot
tell "I am the page" from "I am a widget", and guessing wrong is exactly
the failure above, so it no longer touches the document at all.

MEASURED on a partner page at a FOREIGN ORIGIN with
profile-config.locale='ar' (same gate, before and after):

  before   upg-100-mfe   plugin root ltr, arabic 1765   FAIL
           poster-wall   plugin root ltr, arabic 1738   FAIL
  after    upg-100-mfe   plugin root RTL, arabic 1765   pass
           poster-wall   plugin root RTL, arabic 1738   pass
  partner <html>/<body>/<h1> stayed ltr throughout.

Standalone leaves are unchanged and still get <html dir="rtl"> from
?lang=ar — verified, both profiles — because a leaf page DOES own its
document. createAppI18n's document write therefore stays for now;
replacing it needs a page-ownership signal that touches every generated
leaf, which is its own change rather than a rider on this one.

⚠ THE GATE REFUSES TO JUDGE WHAT IT CANNOT SEE, and that guard earned
its place twice before this was measurable. Its first two runs reported
"the element never mounted" — a HARNESS gap, not the product: a partner
embed uses crossorigin="anonymous" (SRI requires it), so the tree must
send Access-Control-Allow-Origin, which `bun run serve` does not. The
gate now proxies the tree through its own CORS-enabled port. Without
that guard those runs would have read as fails-first evidence for a
defect nothing had actually exercised. It also fails if the locale
produced zero Arabic chrome, because direction cannot be assessed on a
page whose language never applied.
2026-09-25 15:14:20 +00:00
doxa-maps-88655c70
069d75dba7 check:element-census-vs-live: what is on one side and not the other, ours vs the live page we clone
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Every clone-fidelity gate compares properties of NAMED selectors, so none of them can see an element
that exists on only one side; regions found an extra control and an invented chart by eye, past eight
green gates. This executes the Driver's binary scope test (promoted from regions' probe).

- Harvests every visible text node through every shadow root on both sides and diffs the sets:
  OURS-ONLY unaccounted (scope violation) and THEIRS-ONLY (omission) fail; SANCTIONED entries must
  carry the Driver's words, date and card or the preset is refused; same-label digit drift is
  reported, not failed.
- The negative control is mandatory: a known ours-only string must land in the ours-only set, or the
  run prints CENSUS IS BLIND and exits 75. An unreachable live page is also 75.
- Presets live in internal/element-census/<name>.json; ad hoc via --leaf/--live/--neg.
- Measured: regions passes (list 229 vs 228, the only difference its sanctioned poster toggle; Africa
  97 vs 97 with 3 live-vs-snapshot figures). With the allowlist removed the toggle FAILS as
  unaccounted; a negative-control string not on the page gives CENSUS IS BLIND, exit 75.
2026-09-25 14:15:19 +00:00
doxa-maps-88655c70
c7c2050dd3 Installing i18n also provides it, so a host can set the locale
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
A host embeds these elements and sets profile-config.locale.
ProfileLoader applies it by driving the app's i18n directly, but reaches
it with inject('appI18n', null) — and the default is NULL. So a bundle
whose entry does app.use(createAppI18n()) without a matching
app.provide('appI18n', i18n) turned that watchEffect into a SILENT
no-op: no error, no warning, no translation. A partner embedding the
element simply could not choose its language.

FAILS-FIRST, measured with the fix stashed so this is a true before:
  my-upg-100-list/upg-100-mfe   profile-config.locale='ar'    0 Arabic chars
  regions-dashboard/regions-list (provides appI18n, CONTROL) 178 Arabic chars
Same harness, same locale present in both configs. The control is what
makes it evidence rather than a broken probe: 178 vs 0 under one harness
rules out "the config never arrived".

★ WHY HERE AND NOT IN EIGHTEEN index.js FILES. The fix is one line per
bundle, which is exactly why it decays: one fact stored in eighteen
places, and the nineteenth bundle written next month re-introduces it
with nothing to catch it. A vue-i18n instance IS a Vue plugin, so
wrapping its own install() makes the provide a CONSEQUENCE of
installing rather than a step beside it. A bundle cannot forget.
Bundles that already provide the same instance are unaffected —
providing an identical value twice is a no-op, and the control's reading
is unchanged at exactly 178 after.

MEASURED with studio's check:host-locale-contract, which compares each
leaf's host-channel reading against THAT LEAF'S own ?lang=ar reference,
so a leaf with sparse Arabic chrome is not judged against a constant:

  parent   18 FAIL
  here      7 FAIL

  my-upg-100-list/upg-100-mfe      0 -> 3418  (ref 3418, exact parity)
  my-upg-100-list/poster-wall      0 -> 2685  (ref 2685)
  my-upg-100-list/upg-100-list     0 -> 688   (ref 688)
  country-posters/country-poster-wall  0 -> 11025
  plus all five gospel-resources leaves and cities-internet

⚠ BIG PICTURE IS NOT FIXED BY THIS, and should not be marked so. It
reads 0 against ref 273 with the wrapper active and locale 'ar' present
in its config, so its failure is a different mechanism — nested children
following the parent's locale — not the missing provide. Six other
composites remain FAIL for the same reason.

The poster-wall gate grew hi and ru (five scripts: Latin, RTL, CJK,
Devanagari, Cyrillic) plus a guard for a locale that cannot detect the
defect at all: if a catalogue ever translates a key to exactly its
humanized fallback, that locale renders identically whether i18n worked
or threw and would pass forever. It now exits 2 NOT MEASURED naming the
key. Its PASS line also states its scope, because a green line pasted
into a scoreboard is where "translations render" silently becomes
"localisation works".
2026-09-25 13:49:58 +00:00
doxa-maps-88655c70
544819624f browser-slot: a slot is held for at most BROWSER_MAX_HOLD seconds, and only by the wrapper
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
A probe that awaited map.once('load') with no timeout held slot 1 for 89 minutes on 3 s of CPU,
and every other browser gate on the box queued behind it and timed out as NOT MEASURED.

- The command now runs as a child under `timeout --kill-after` (default 900 s, BROWSER_MAX_HOLD);
  timeout signals the whole process group, so the chrome a probe spawned dies with it. 124/137 are
  reported as 75 NOT MEASURED, naming the command.
- fd 9 (the slot lock) is closed for the child. With exec, the command and every grandchild
  inherited it, so an orphaned process kept the slot locked after its parent exited — measured
  with the previous wrapper: the next caller got 75 after its wait.
- Proven on a private slot dir (1 slot, 5 s limit): sleep 99999 freed at 5.07 s with 75 and the next
  waiter ran at 5.58 s; a TERM-ignoring child killed at 7.1 s; after a command that leaves an orphan
  the slot is free in 261 ms; exit codes and stderr pass through unchanged.
2026-09-25 13:46:33 +00:00
doxa-maps-88655c70
b0c7b52243 Take main
# Conflicts:
#	package.json
#	public/doxa-maps/index.html
2026-09-25 12:38:20 +00:00
doxa-maps-88655c70
d26e416d03 Translate the list and the table headers; correct a claim I had not checked
resources-list's chrome is translated in all eleven — the three filter tabs, the
generic resource names, the people-group count — and the table's column headers
are localised at render in data-tables. resources-map, data-tables and charts now
report zero leaks by the corrected gate; what remains is in resources-list's
data-driven rows.

A CLAIM I MADE AND HAD NOT MEASURED. The previous commit said the CSV export
"keeps the data file's English field names". It does not: Tabulator exports the
column titles it is given, so a Russian reader's CSV heads "Народ","Страна",
"Регион". I checked only after asserting the opposite. Left translated on
purpose — this is a gift for people to use and someone who exports what is on
their screen should get what was on their screen — with the stable machine
contract being data/tables/ itself, which is untouched and English.

THE GATE NEEDED A THIRD CORRECTION, and this one mattered most. Substring
matching over the rendered text reported keys that were never rendered at all:
"Language" matched inside the DATA — the people-group language "Sign Language" —
and "Region" inside a region name. No length filter separates a label from a
word sitting inside one. The unit is now an element's OWN label, and a leak is a
label that EQUALS the English value while a translation exists. A language name
can no longer impersonate a UI string.

Scripts are alphabetised per the fleet convention, so parallel landings stop
colliding on the last line of package.json.
2026-09-25 12:36:41 +00:00
doxa-maps-88655c70
cfdbea7f9c Host locale: gate the profile-config contract, forward ?lang= into staging embeds, stop losing gate stderr
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- check:host-locale-contract: every leaf loaded with ONLY profile-config.locale="ar" must draw at
  least half the Arabic the same page draws at ?lang=ar; regions-list is the positive control.
  Fails on this commit for 18 leaves (bundles that never provide appI18n, and every composite).
- check:staging-lang: the staging page at ?lang=ar embeds its leaf with lang=ar and the Gospel map
  renders gospel.mode.gap in Arabic. staging-forge.js (iframe + open link) and
  staging-reference-v2.html now forward ?lang= / ?locale= via withLang(); fails on the parent.
- browser-slot.sh: `exec 9>f 2>/dev/null` redirected the whole script's stderr, and the gate
  exec'd after it, to /dev/null — FAIL lines and "not measuring" messages were being discarded.
  The redirection is now scoped to the open.
- verify-map-paint: the two regions map leaves join the default list (both paint, local and live).
- Vision card reads ?lang= / ?locale= like the rest of the library.
2026-09-25 12:18:26 +00:00
doxa-maps-88655c70
d78d67f197 Merge remote-tracking branch 'origin/main' into exp-shepherd/shelf-gate
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	package.json
2026-09-25 11:48:59 +00:00
doxa-maps-88655c70
07e2e96ec4 Take main
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	package.json
2026-09-25 11:44:59 +00:00
doxa-maps-88655c70
299adaa9e3 A leak gate that had to be corrected twice before it could be trusted
rega's method: a string is a LEAK when the catalogue has a different value for
the locale and the page still shows the English one — a render fault, not a
missing translation. This measures that across four leaves and four locales.

IT TOOK THREE VERSIONS AND THE FIRST TWO WERE WRONG IN INSTRUCTIVE WAYS.

One: it failed with no explanation. process.exit(1) terminates before pending
stderr flushes, so the run returned 1 with every reason line missing. A gate that
cannot say why it failed is barely better than no gate. It sets exitCode now and
lets the process end.

Two: it read the whole shadow root and reported 113 leaks, nearly all false.
"JESUS Film" and "Scripture" appear inside the explanatory paragraphs this bundle
deliberately keeps in English, and short entries like "for" match inside ordinary
sentences. Crying 113 when a handful are real is worse than silence — someone
would have gone and "fixed" strings that were already correct. Every English
prose block here declares itself dir="ltr", so the check now walks the shadow
root and skips those subtrees, and ignores catalogue entries under six
characters.

WHAT IT FINDS, now that it is trustworthy: 70 real leaks, and every one is in
resources-list. resources-map, data-tables and charts are clean in ar, zh, ru and
hi. That is exactly the debt I declared two commits ago — that leaf got its title
translated and nothing else — now measured instead of estimated.

Scope is the leaf on purpose. Six keys were reported leaking in the gallery and
the leaf is clean for all six: the gallery builds its iframe src with no lang
parameter, so the embed never learns the locale and correctly falls back to
English. Embedder defect, not a bundle one, and a check that conflated them would
send someone to translate what is already translated.
2026-09-25 11:43:16 +00:00
doxa-maps-88655c70
ff36d0dfcc Merge remote-tracking branch 'origin/main' into exp-shepherd/shelf-gate
# Conflicts:
#	package.json
#	public/doxa-maps/doxa-maps-spa/manifest.json
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
#	public/doxa-maps/prayer-walk-planner/prayer-walk-planner.js
#	public/doxa-maps/prayer-walk-planner/prayer-walk-planner/index.html
2026-09-25 11:36:00 +00:00
doxa-maps-88655c70
b1e0c7fa61 check:experimental-shelf: every experimental plugin, measured working, and two label fonts that 404'd
The Driver asked for "all experimental plugins working"; this makes it a
measurement. The gate derives the shelf from the Research Gallery's own rules
(manifest minus curated tabs and NOT_EXPERIMENTAL), then checks every bundle on
its leaf page and on its Gallery tab, under mapbox and maplibre. It requires
content inside the plugin's own element, 0 page errors, 0 failed requests, no
loading text, and for each map 'load' fired AND its own GeoJSON data drawn. An
empty source does not count. Maps are found by wrapping the engine's Map class
in an init script, so no bundle changes for it. It serves this checkout on an
OS-assigned port, and self-tests first: a deliberately broken bundle must fail,
or it exits 3.

Prayer Walk Planner's data is the user's own (zones in localStorage), so the
gate seeds one zone and requires it drawn, printing the seed and reason on every
run rather than exempting the bundle.

It found two real defects on the way. Both are fixed here, and both failed the
gate before the fix:
- UPG Neighbor: every label layer asked MapLibre's default fonts (or Mapbox's
  DIN) from OpenFreeMap, which 404s, so no label drew under maplibre. labelFont()
  now picks Noto Sans under MapLibre; Mapbox is unchanged.
- Prayer Walk Planner (always MapLibre): zone titles never drew, for the same
  reason. The label layer now names Noto Sans Regular.

Result: 28/28 PASS across all 7 shelf bundles.
2026-09-25 11:35:18 +00:00
doxa-maps-88655c70
a2be2396b9 README: the Experimental shelf, with Wall of People Groups and the Prayer Walk Planner
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
A new "Experimental shelf" section pictures the two newest shelf plugins, each
shot from its own page after it finished loading: the Wall of People Groups
(all 2,107 groups as photo cards on one zoomable prayer wall, 7fec7426) and the
Prayer Walk Planner (full screen from any embed and a sharp HD print,
30e6e569). The Research Gallery tree now lists all seven shelf plugins
(adds pplr-data-maps and wall-of-people-groups). Shots staled by 92a81ed2 and
later rebuilds are retaken.
2026-09-25 11:22:52 +00:00
doxa-maps-88655c70
4cc4a1284b The last browser scripts refuse to measure a tree or dev server that is not this checkout
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
BUG-BROWSER-GATES-CAN-MEASURE-ANOTHER-WORKTREES-TREE, the remainder. A
static scan of every internal/ script that drives a browser at a local
target found eight with no identity guard (a9550858 and 59f497eb
guarded the rest):
  built tree  check-spa-tab-order -> requireThisTree
  dev server  long-frames-probe, mobile-sheet-probe, theme-flash-probe,
              plugin-gallery-smoke, share-roundtrip-smoke (--plugin-gallery),
              smoke-studio, legend-columns-probe (only when aimed at
              /plugin-designs/; its default target is the DX-life site
              it compares against on purpose)

A dev server has no built manifest, so the new
requireDevServesThisCheckout checks the source it serves. It fetches
three library files with ?raw and compares them byte for byte with
this checkout. Another tree's dev server answers differently or 403s,
and the gate exits 75 (NOT MEASURED). Vite answers a plain fetch with
the file's text and an import-style request with `export default "…"`;
both shapes are accepted, since the first version parsed only the
second and refused this checkout's own server.

Proven: against a dev server running from 319064a1's parent and a
built tree whose manifest differs by one byte, all eight exit 75 and
name the differing file (useMapInstance.js). Against this checkout,
check-spa-tab-order and smoke-studio PASS, and plugin-gallery-smoke
gets past the guard. A re-scan finds no unguarded script left.
2026-09-25 11:10:34 +00:00
doxa-maps-88655c70
6356cc3876 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 10:51:41 +00:00
doxa-maps-88655c70
30e6e5699b Prayer Walk Planner: full screen from any embed, and a print proven sharp by resolution
The Driver asked for full-screen, and for "a print that stays sharp when
zoomed".

Full screen. The planner mounts the shared FullscreenButton (the library's
Fullscreen-API control, with its CSS pseudo-fullscreen fallback for iOS
Safari) as a map overlay. It full-screens the whole planner, toolbar and
map, not just the canvas. Measured from a 520px embed in the Research
Gallery on a foreign page: the planner took 1280x800 and the map grew 459
→ 739px. The label is translated (Arabic "تبديل ملء الشاشة"), because the
planner now keeps the shared vue-i18n instance on its own locale. Before,
Share and Full screen took the PAGE language even when the gallery sent
another.

Print resolution, measured, not asserted. The printed map lays out at
7.53 x 8.4 in on the letter sheet (printable width = 8.5in minus the two
12mm margins; emulateMedia alone does not apply @page margins, which
misread it once as 8.48in). Its image is 2400x2688 px, so 319 x 320 DPI.
A 1:1 crop of the zoomed print was read: small street names (Sancaktar
Yokuşu Sokağı, Baki Dede Sokağı), individual building outlines and
footpaths are crisp, because the map is rendered from vector tiles at
pixelRatio 2, not upscaled from the screen. One PDF page.

check:prayer-walk-planner gains both: full screen from a 520px embed must
take the viewport, and the printed map must measure >= 300 DPI. 10/10
PASS.
2026-09-25 10:51:21 +00:00
doxa-maps-88655c70
cd53582ea4 The shared poster wall's locale is asserted, in locales that can show a leak
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
UPG-100 carried a ❌ for a PosterWall i18n leak: useI18n() said to throw
where the wall mounts, leaving the print buttons English in every
locale on upg-100-mfe, poster-wall and country-poster-wall.

IT DOES NOT REPRODUCE. Measured on the current build, three leaves x
ro/ar/zh, tree identity checked against the served manifest first: zero
English leaks, zero page errors. ro "Listă de rugăciune / Harta afișului
/ Pachet de cartonașe", ar "قائمة الصلاة / خريطة الملصق", zh "禱告名單 /
海报地图", legend "Opțiuni de tipărire" / "خيارات الطباعة" / "列印選項".
The diagnosis does not fit either: this bundle installs i18n per element
via configureApp, and createI18n is legacy:false, so useI18n() resolves
for every descendant including the wall.

THE REAL DEFECT WAS THE MISSING ASSERTION, and it is worse than the bug
would have been: nothing in the repo checked the shared wall's RENDERED
locale, so a ❌ could sit on the scoreboard with nothing able to confirm
or refute it.

★ AND IT CANNOT BE CHECKED IN ENGLISH. The wall's guard HUMANIZES the
key when useI18n() throws - buttons.prayerList becomes "Prayer List" -
which is deliberate, so a bare host shows readable English rather than a
raw key. It also means "the button says Prayer List" is true both when
i18n worked and when it threw. An en-only check passes forever while
every other locale leaks. This gate therefore runs ONLY in non-English
locales, where the translated string differs from the humanized key and
seeing the key IS the defect.

It covers FIVE consumer leaves, not the three the report named:
key-cities and regions-dashboard mount the same shared wall and were
never checked. A gate that copies a report's sampling inherits its
blind spot.

TWO WAYS IT COULD HAVE PASSED WITHOUT CHECKING ANYTHING, both real on
the first run and both now hard skips rather than green ticks: a wall
with no cards renders no chrome, and regions-list rendered 7 cards with
ZERO wall chrome because that profile mounts the wall without the print
row. Skips are named in the output with their reason - a summary that
counts only the green ones reads as full coverage.

MUTATION TESTED by reproducing the reported condition exactly - forcing
the useI18n() guard to throw - rather than corrupting a string: the gate
goes red with 28-33 leaked strings on every measured combination, which
proves both that it detects the defect and that the defect is genuinely
absent from the unmutated build. Restored, 9 combinations PASS, 6
reported as not covered.

No source change: there was nothing to fix.
2026-09-25 10:50:46 +00:00
doxa-maps-88655c70
c77207b53a check:basemap-labels: the basemap's place names follow the reader's language on both engines
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
BUILD-LOCALIZATION-SURVIVES-THE-PROVIDER-SWAP asked for a check; the
behaviour landed in 319064a1 (library/maps/localize-labels.js) without
one. This reads the labels the map actually DRAWS, at ?lang=en and
?lang=ru on the same view, under both providers:
  Mapbox    the Standard basemap's 'place-labels' featureset (a plain
            queryRenderedFeatures cannot see the imported basemap):
            Germany→Германия, Ukraine→Украина, Egypt→Египет… must flip.
  MapLibre  each country symbol's evaluated text-field: under ru it must
            be the feature's name:ru, under en not. English there
            restores OSM's own label, so no English word is expected.
It serves this checkout on an OS-assigned port. --dir points it at
another tree for the negative control.

Negative-tested: doxa-simple-map built from 319064a1's parent (no
localize-labels) and served with --dir FAILS on both engines (Mapbox:
68 country labels, none flip; MapLibre: 0/49 in Russian). This tree
PASSES on both (5 known countries flip on Mapbox; 49/49 on MapLibre,
e.g. United Kingdom → Великобритания). A first version counted the
pre-fix Mapbox run as NOT MEASURED instead of FAIL, since it saw no
English names. "Measured" now means enough country labels were drawn.
2026-09-25 10:48:42 +00:00
doxa-maps-88655c70
c73b775b6d Take main
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-25 10:19:42 +00:00
doxa-maps-88655c70
59e9695aaa Correct the record: the dead bundle was my merge, not a duplicate import
dx-shepherd's verifier traced it. Merge 52ab6089 from this worktree committed
public/doxa-maps/gospel-resources/gospel-resources.js with 21 conflict markers —
both sides concatenated, 1.13 MB — and that is what threw "Identifier Ge has
already been declared" and killed every gospel leaf.

My diagnosis last commit was wrong. I blamed a module imported twice and
re-routed the import; that change was harmless but it was not the cause. The
REBUILD that came with it is what repaired the file. A plausible cause that a fix
appears to confirm is the easiest kind of wrong answer to believe.

THE ACTUAL CAUSE IS A LANDING SCRIPT I WROTE. On a conflict it resolves the one
file it expects — public/doxa-maps/index.html — and then runs "git add -A
public", which stages every OTHER file under public/ including the ones still
holding markers. One line of convenience turning a conflict git correctly refused
to guess at into a committed artefact.

So: a check that names the real condition. It scans every tracked file for an
unresolved marker, is built from character codes so it can never trip itself, and
carries a poison arm. It runs FIRST in check:gospel-all, because there is no
point measuring anything else about a tree that is mid-merge.

Also verifies the 8 links the GRN recheck recovered, which check:gospel-all
caught as shipped-but-unverified on its very first run — psi, alk, nos, jle, lma
and three more, all now confirmed landing on their own 5fish language pages.
2026-09-25 10:19:07 +00:00
doxa-maps-88655c70
413fa30d19 isMap per profile: a map-less leaf no longer bakes, or preloads, a map engine it never uses
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
generate-tree decided a single leaf's map-ness as "its own .vue
references an engine OR anything under the BUNDLE's src/ does", so a
map-less profile inherited it from a sibling. regions-dashboard/
regions-list shows a list and no map, but src/RegionCountryMap.vue made
it isMap=true. Its leaf loaded the Mapbox engine for nothing, and since
4a6886d2 its embed snippet also preloaded it.

A leaf's map-ness now follows the RELATIVE imports reachable from its
own .vue, bounded to its bundle, so a map several imports deep
(research-dashboard's DashboardMapPanel) still counts. An explicit
boolean isMap in <profile>.meta.json wins. Composites keep their
separate rule, since they do mount their siblings.

Measured: across all 34 profiles exactly one changed (regions-list,
true -> false). 18 map profiles keep isMap and their preload; 0 non-map
snippets carry one. On a served built tree regions-list makes 0 engine
requests and 0 api.mapbox.com requests and renders its list (673
elements, no errors), where before it downloaded the 1.75 MB engine.
regions-mfe still loads the engine and draws its map. The regenerated
files are only the regions-list leaf, the two manifest copies (still
byte-identical), the staging page and the gallery; check:sri passes.
2026-09-25 10:17:33 +00:00
doxa-maps-88655c70
da8bc790b8 check:composite compares bytes, not mtimes: rebuild each composite and diff it with the committed bundle
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The gate compared a child's newest source mtime with the parent
bundle's mtime. A git merge or checkout rewrites sources with
checkout-time mtimes, so rega's publish of beb13da3 stopped on
"doxa-maps-spa / plugin-studio / stack-demo STALE", although a rebuild
reproduced the committed bytes exactly. Two of those parents mount
their children by tag and compile none of them in. It was also
gameable the other way: checking out the parents makes them pass.

It now rebuilds each composite into a temp dir and compares sha256
with the committed bundle. Builds are deterministic (two runs give
identical bytes), so a difference is a real source change the
committed parent predates. The new vite.config.js DOXA_OUT_ROOT
redirects that build away from public/; unset, nothing changes. A bare
sibling-profile entry in composes (gospel-resources' resources-map,
data-tables) now resolves instead of being reported unresolved.

Proven: an edit to people-groups-grid.vue with no rebuild fails
big-picture, which compiles the grid, and leaves plugin-studio and
research-map-plus-grid fresh. Touching every child and library source
with no content change still passes.

On today's main: doxa-maps-spa, plugin-studio and stack-demo (the
false alarm) are fresh. country-posters, dashboard-plus-grid and
regions-dashboard are really stale and need a rebuild. About 2 minutes
for all 10 composites; use --only to narrow.
2026-09-25 10:10:08 +00:00
doxa-maps-88655c70
85bd7ed1e9 README: Simple Research and the Research Gallery join the phone row; stale shots refreshed
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
simpleresearch fixed the phone toast (2221d11b): at 390x844 the "Tap a
coloured dot" prompt now sits above the Progress sheet and the legend reads in
full (checked by eye). The phone row now shows seven maps in two rows of four;
Big Picture keeps its note (desktop by design; on a phone the Research Gallery
opens on Simple Research). Gospel Resources shots retaken after its bundle was
repaired on main (57fecfd0 replaced a bundle committed with merge-conflict
markers in 52ab6089). Other stale shots refreshed from the latest builds.
2026-09-25 10:09:12 +00:00
doxa-maps-88655c70
59f497eb67 Six more browser gates measure THIS build or nothing
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
a9550858 guarded twenty gates; these six were still unguarded. Each
now exits 75 (NOT MEASURED) rather than grading another tree:
- check-legend-export-reachable, check-vision-card-numbers --url:
  requireThisTree on the URL they are given.
- check-embed-basemap-paints: it picks a RANDOM port, not an
  OS-assigned one, so if another agent holds it, its own server fails
  to start and it would measure theirs. It now checks the leaf server's
  identity, stops its own servers and exits 75. Its "a server did not
  come up" path exited 1 and now exits 75.
- check-tabs-localized, check-poster-prints read library/ from
  DOXA_REPO. Pointing that at another checkout now needs
  DOXA_REPO_MEASURE_OTHER=1 (new requireThisRepo helper).
- check-packet-render refuses a packet file inside a DIFFERENT git
  checkout (new requirePathInThisCheckout helper); a file outside any
  checkout is allowed with a note.

Proven by pointing each at a wrong build: a copy of this public/ whose
manifest differs by one byte, served on its own port, and another
worktree for the file and DOXA_REPO gates. All 8 (these 6, plus
shell-scroll and upg-map-data) exit 75. On this tree they measure:
legend-export, vision-card, upg-map-data, tabs-localized and
poster-prints PASS. packet-render gets past the guard and fails on a
non-packet file, as intended. embed-basemap measures and FAILS: "the
page does NOT paint a basemap on its own baked token". That is a
product finding, not the guard, and is reported to its owner.
2026-09-25 10:03:16 +00:00
doxa-maps-88655c70
2a1292b3ad verify-shell-scroll asserts the Driver's ruling: a wheel over the map zooms, below it scrolls
Step 2 asserted that a wheel OVER the map must scroll the page. That
predates the Driver's 2026-09-23 ruling, recorded in
DashboardMapPanel.vue: "wheel OVER the map zooms; wheel BELOW it
scrolls". It kept the gate red on correct behaviour (dx-shepherd ruled
it stale). It now asserts the ruling: a scroll-down wheel over the map
moves the page 0 px, a zoom-in wheel over it raises the zoom, and a
wheel below the map scrolls the page until the grid is on screen. It
locates the map canvas and wheels at its centre and below it, not at
fixed coordinates.

Proven both ways on a served built tree: PASS (over the map 0 px and
zoom 0.50 -> 1.30; below it 2502 px). With SHELL_SCROLL_NEGATIVE=1,
which disables scrollZoom as before the ruling, it FAILS: the page
moved 300 px. It also gains the tree-identity guard (exit 75 on another
build).
2026-09-25 10:03:16 +00:00
doxa-maps-88655c70
be3620ef1c browser-slot.sh: stop overwriting the caller's BASE, and let a waiter take any slot that frees
Two defects, both reproduced before the fix and re-measured after.

1. It assigned BASE for its lock path. A variable the caller exported
   stays exported in bash, so every gate run through the slot saw
   BASE=/tmp/doxa-maps-browser-gate.lock. Seven gates read
   process.env.BASE; check:upg-map-data-painted printed "SKIP — nothing
   served at /tmp/doxa-maps-browser-gate.lock/…" and exited 0. Renamed
   to SLOT_BASE. Proof: with BASE=/x set, a child printed the lock path
   before and /x after.
2. With every slot busy, waiters queued on slot 1 alone, so a slot that
   freed was never taken and two slots behaved as one queue. It now
   polls every slot with flock -n every 2 s until the deadline and
   still exits 75 on timeout. Proof, in a private slot dir with slot 1
   held 12 s and slot 2 freed at ~3 s: a caller waited 11.5 s before and
   4.3 s after. A free slot is still taken at once (66 ms).

check-upg-map-data-painted: its unreachable-server SKIP exited 0 and
its wrong-tree refusal exited 2. Both now exit 75, the repo's "not
measured" code. Its existing tree-identity check is what makes the
:4173 default safe, since another agent's tree there is refused.
2026-09-25 10:03:16 +00:00
doxa-maps-88655c70
6c1c543a92 Take main
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 09:52:42 +00:00
doxa-maps-88655c70
57fecfd0da A gate that asks whether anything is there — and it caught a dead bundle
Wrote this after shipping a page that returned 200 for every asset, reported
zero console errors, and was completely dead: a throw during setup dies inside
the custom-element boundary. Zero errors is not the same as working, so this
does not trust their absence — it looks for PRESENCE, on all five leaves, in
five steps that each name their own failure: the tag is defined, the host is in
the document, a shadowRoot is attached, it has children, it renders text.

Its first real run went red on all five, and it was right. Every gospel leaf was
dead with "Identifier 'Ge' has already been declared" — while the SRI hash
still matched and every request still returned 200. My own doing: four profiles
imported en/gospel.json directly for an English fallback, and library/i18n
already pulls that exact file in through an eager glob, so the IIFE carried two
instances of one module.

The fix is one module: library/i18n now exports EN_MESSAGES and the profiles
read it from there. The duplicate is gone.

Measured after: all five leaves mount with 3,228 to 233,307 characters of
rendered text, verify-gospel-map passes with 2,107 pins and the gap cutting to
370, and --poison confirms the check goes red on a page with no element at all.

The lesson is the gate's whole reason to exist. I had rich assertions about pin
counts and filters and none of them ran on data-tables, charts or the list, so a
bundle-level failure could take out every leaf while the one gate I did run was
pointed elsewhere. Cheap and universal beats rich and narrow for THIS question.
2026-09-25 09:52:25 +00:00
doxa-maps-88655c70
319064a11b Map place names in the reader's language, on both engines
Every map translated its own chrome but left the basemap's country, city
and street names in the style default (English on Mapbox, Latin + native
on OpenFreeMap) in all eleven locales. A reader in Arabic saw an Arabic
legend over an English map.

library/maps/localize-labels.js uses each engine's supported mechanism:
- Mapbox: map.setLanguage(lang). It also localizes the Standard style's
  IMPORTED basemap, which a layer rewrite cannot reach.
- MapLibre: basemap symbol layers point at
  ['coalesce', name:<lang>, name:latin, name]. English restores each
  layer's original expression, re-captured on every style load so a theme
  switch never restores the previous style's labels.
Only layers whose text-field reads an OSM name field are touched. A
plugin's own label layers (zone titles, group names) are never rewritten.

useMapInstance applies it on every style.load and when the reader's
language changes (ProfileLoader's 'lang', else profile-config locale, else
?lang / <html lang>). The Prayer Walk Planner drops its private copy for
the shared one.

Measured on the Prayer Progress map, ar/zh/ru/hi:
- Mapbox getLanguage() == locale in all four.
- MapLibre: 13/14 symbol layers localized, and the plugin's own label
  layer untouched.
Read in the Arabic screenshots: القاهرة, الإسكندرية, السويس, الأردن on
both engines. check:gallery-embed (+ :maplibre) and
check:prayer-walk-planner pass. The shared change reaches all 11 bundles
that compile initializeMap, and all are rebuilt here. check:shared-reached
marker basemap-labels-localized: 10/10 consumers.
2026-09-25 09:46:21 +00:00
doxa-maps-88655c70
8e6fde19ae README: the production maps on a phone
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
A row of phone screenshots (390x844, taken after each map finished loading)
under the production gallery: Regions, UPG-100 List, Prayer Progress, Cities
with Internet and Gospel Resources. Big Picture is desktop-only by ruling, and
Simple Research is left out until its phone layout is finished: its "Tap a
coloured dot" prompt still covers the Progress legend at 390px. The README
says both plainly.
2026-09-25 09:12:16 +00:00
doxa-maps-88655c70
60b3650304 The phone app opens the UPG-100 list, not the plugin studio
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
repo reported it: the Capacitor app launches public/index.html, which
mounts <plugin-studio>. A recipient opening the phone app meets the DEV
STUDIO rather than the map the README promises. Reproduced in the synced
Android assets before touching anything.

RULED (Kingdom-agent, card DECISION-MOBILE-EXAMPLE-IS-UPG100-NOT-SIMPLE-
RESEARCH, done): the ONE mobile-app showcase is the UPG-100 list.

⚠ THE CARD'S OWN BLUF SAYS "Simple Research" AND IS STALE. It predates
that ruling, which is recorded DONE in the same backlog and says Simple
Research "was never mobile-friendly". Building to the card's wording
would have shipped the app the ruling rejects, so this builds to the
ruling and the card is annotated with why.

WHY IT RUNS AFTER cap sync AND WRITES ONLY INTO THE ANDROID ASSETS.
public/index.html has THREE consumers, which capacitor.config.json
documents: Railway/CDN, Capacitor, and local staging. The studio is
CORRECT for the CDN root and for `bun run serve` — only the phone build
wants a different landing page. Rewriting the shared root would fix the
app by breaking the other two. cap sync copies webDir into
assets/public/, so overwriting the COPY changes what the app opens and
leaves the served tree untouched. The studio is kept at studio.html
rather than deleted; a developer with the apk still wants it.

WHY IT COPIES THE BUILT LEAF RATHER THAN HAND-WRITING A HOST PAGE.
The leaf carries an SRI integrity hash. A hand-written page needs its
own correct hash, and a stale one makes the browser REFUSE the script
with no console error: the element never upgrades and the app is a blank
screen. Copying the artifact the build already verified means there is
no second copy of that hash to drift — confirmed, the hash still
matches after the rewrite. Only the relative depth changes, and the
script asserts every rewritten reference resolves to a file that exists
rather than trusting its own regex.

MEASURED by serving the ANDROID assets tree (not public/, which is the
whole point) on a private port, with the app-root marker checked before
any number was read:
  390x844   mounted+upgraded, studio absent, 27 poster cards, 0 4xx, 0 errors
  1280x900  same
check:app-start-page is mutation-tested: restoring the studio page as
the app root makes it exit 1 naming <plugin-studio>.
2026-09-25 09:11:01 +00:00
doxa-maps-88655c70
a7d086619b Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 08:56:25 +00:00
doxa-maps-88655c70
4a6886d2c9 Embed snippets preload the Mapbox engine, so a partner page fetches it alongside the bundle
The card asked for Vue to be bundled inline to make maps faster. It
already is: vite.config.js externalises only jspdf, and every IIFE
carries its Vue runtime. The measurement (internal/measure-map-load.mjs,
cold, 10 Mbps / 60 ms, served built tree) found one serial runtime
fetch before first paint, on partner pages only. A page that pastes the
embed snippet has no leaf page to fetch mapbox-gl.js (~480 KB gz), so the
bundle requested it after it had downloaded, run and mounted. Leaf pages
already load it in parallel.

Map profiles' script snippets now start with a preload for the engine,
at the URL read from useMapInstance.js ENGINES.mapbox.js so the two can
never differ. It has no crossorigin attribute because
ensureEngineScript injects a plain <script>, and a mismatched preload is
not reused. Non-map profiles get no preload. A comment tells a partner
who switches to provider "maplibre" to delete the line.

Partner embed, before -> after (2 cold runs):
  doxa-simple-map  engine start 1.06 -> 0.20 s, done 2.26 -> 0.95 s,
                   style start 2.73 -> 1.94 s
  regions-mfe      engine start 2.67 -> 0.11 s, done 3.25 -> 1.13 s
                   (style start unchanged: the composite mounts its map late)
  gospel resources engine start 0.81 -> 0.27 s
One engine request per run: the preload is reused, never a double
download. The time until tiles are loaded is dominated by tiles, data
and drawing, and moved within noise.

Held, per dx-shepherd: no shared Vue, no inlined mapbox-gl, and lighter
basemaps are for the Driver to decide.
2026-09-25 08:56:09 +00:00
doxa-maps-88655c70
27151f39a6 Research Gallery: boot on a partner page under the CORS the live server actually sends
The doxa.life script embed was blocked on a server change: the live root
manifest.json answers without Access-Control-Allow-Origin, and the gallery
fetch()es it cross-origin, so no tab could mount. Measured on nuxtbase,
though, the same static layer DOES send ACAO on every path inside a folder
of the tree (doxa-maps-spa/<anything>, even a 404; assets/…).

So generate-tree writes a byte-identical manifest copy at
doxa-maps-spa/manifest.json in the same step as the root one (they cannot
diverge). The gallery reads it FIRST when the tree is on another origin (no
CORS error in the host's console) and the root first when same-origin.
Script URLs still resolve against the tree root, never the copy's folder.

check:gallery-embed:live-cors serves the tree with the live rule
(serve-strict LIVE_CORS=1: no ACAO on the root manifest only).
- The current gallery under it: "did not boot within 60s" (negative
  control, reproducing doxa.life).
- With this change: js 14, iframe 14, neighbour 14, linked 4. The default
  gate also passes all phases.
check:embed-targets, check:drop and check:public-hygiene pass with the copy
present.

The server-side CORS rule (serving/traefik-embed-headers.yaml) is still the
right long-term fix, and it needs server access. This unblocks the embed
without it once the tree is published.
2026-09-25 08:55:52 +00:00
doxa-maps-88655c70
2a062c1ca0 README: the 7 production maps up front, one capabilities x deploy table, the library tree and rule
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- A gallery of the seven production maps (Big Picture, Simple Research,
  Regions, UPG-100 List, Prayer Progress, Cities with Internet, Gospel
  Resources) plus the Research Gallery, each shot from its own standalone page
  after its map finished loading. Five new shot definitions in readme-shots.
- One 5 x 5 table: every capability ships every way (iframe, mobile and JS
  embed today; WordPress and desktop proven, not yet in production).
- The "map cruncher" converge/diverge picture, folded.
- Repository layout shows the plugin-designs and shared library trees, and a
  folded "plugin or library?" section states the rule: write your app in its
  own plugin; put code in the library only if it is reusable and parameterized.
- Stale shots retaken from the latest builds.
Closes the README parts of DOCS-README-FOR-A-VISIONARY and BUILD-README-COMPLETE-HANDOFF.
2026-09-25 08:45:04 +00:00
doxa-maps-88655c70
3d4cd4004e Plugin studio: host attributes stay on the host; map-load measurement probe
inheritAttrs:false on the studio root, so a host's attributes (style,
class, id) stay on the <plugin-studio> element instead of also landing
on the layout inside it: the trap that made the Prayer Walk Planner
76px tall in the gallery. Layout verified unchanged in four hosts on the
built bundle: its own page (shell 1280x900, body 836), a bare partner
snippet (85vh default), a host that sets its own inline height (the
style stays on the host; the shell root has no style attribute), and
the app-shell mounter from BUG-EMBEDDED-PLUGIN-STUDIO-BLACK-SCREEN,
display:block forced on the host: body 656px, rail and tree rendered.
That bug no longer reproduces on the dx-maps side.

internal/measure-map-load.mjs: per production map, the cold, throttled
waterfall (bundle, engine script, style, data, basemap up, visible
tiles loaded) on a served built tree, from the leaf page or, with
--embed, from a partner page that pastes only the embed snippet. It
showed that Vue is already inlined in every bundle and that the engine
waits on the bundle only on partner pages.
2026-09-25 08:34:52 +00:00
doxa-maps-88655c70
efb19637fd Merge main into bundler-2/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
# Conflicts:
#	public/doxa-maps/dashboard-plus-grid/dashboard-plus-grid.js
#	public/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/default/index.html
#	public/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 08:26:59 +00:00
doxa-maps-88655c70
e67d718603 Merge main into bundler-2/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 08:22:13 +00:00
doxa-maps-88655c70
4fb8176b00 Shape Arabic, Hebrew and Persian map labels on MapLibre maps too
library/maps/rtl-text.js installed the RTL text plugin for MAPBOX only
(window.mapboxgl, from api.mapbox.com). So every MapLibre map, i.e. every
keyless embed, drew RTL place names as disconnected, reversed glyphs.
Measured on the Prayer Walk Planner's printed Arabic sheet.

Under it was a second gap. Bundle entries call the installer at LOAD
time, before useMapInstance has lazily injected a MapLibre engine, so even
an engine-aware installer found nothing to install into.

- rtl-text.js is engine-aware. Mapbox is unchanged (same URL, same lazy
  call). MapLibre gets the same plugin package from jsDelivr, keyless and
  not api.mapbox.com, so keyless pages still make zero Mapbox requests.
  New ensureRtlTextPlugin(gl) waits until it is LOADED, for callers that
  capture a single frame (print).
- useMapInstance installs it the moment an engine global exists, the one
  point every map passes through.
- The planner drops its private copy for the shared helper.

Measured: a print map of downtown Cairo (MapLibre, keyless) shows shaped
Arabic street and place names (شارع التحرير, مسجد الطباخ …); plugin
status "loaded", fetched from jsDelivr. check:gallery-embed passes js,
iframe, neighbour and linked (14 tabs); :maplibre passes;
check:prayer-walk-planner passes.

The shared change reaches all 11 bundles that compile initializeMap, and
all are rebuilt here. check:shared-reached gains an rtl-text-maplibre
marker (10/10 consumers carry it; cities-internet, gospel-resources and
my-upg-100-list boot maps another way and are not affected).

Also: the new Experimental bundle pplr-data-maps loads the Mapbox geocoder
under maplibre (pplrData.js:38). It is an owned, printed MAPBOX_KNOWN entry
routed to its owner; delete it in their fix commit.
2026-09-25 08:20:12 +00:00
doxa-maps-88655c70
a9550858ed Browser gates: measure THIS build, or measure nothing
About thirty browser gates could pass while describing another worktree's build.
This machine runs several checkouts and a default port belongs to whichever
server started first — measured 2026-09-24, :4173 was another worktree's, and an
hour of probing reported confidently on code nobody here had touched. check:sri
cannot see it: a stale tree and its stale leaf agree perfectly.

Twenty gates now call requireThisTree() right after they resolve their target and
exit 75 on a mismatch — EX_TEMPFAIL, the code this repo already uses for "not
measured", which the wave runner reports as ⊘ rather than ✗. Exiting 1 would turn
a mis-pointed probe into a red product gate, which is the failure this whole
family of checks exists to prevent.

★ NARROWER THAN THE PROBLEM STATEMENT, ON PURPOSE. Three classes do NOT get it,
because there it would be wrong rather than missing:
  self-serving gates  start their own server on an OS-assigned port
  file:// gates       read this checkout directly — no server to be wrong about
  remote targets      verify-map-paint defaults to the LIVE site, and "does
                      production match what we built" is a deploy question asked
                      deliberately. MEASURED mid-rollout: it reported NOT
                      MEASURED against the live host, which was the guard being
                      wrong, not the gate. It now enforces on localhost only.

TWO THINGS THE ROLLOUT TAUGHT, both caught by running rather than reading:
  · node --check passes a file that will die on ReferenceError. Adding a ROOT
    constant to sixteen gates that do not import resolve/dirname/fileURLToPath
    parsed perfectly and crashed on first run — the same shape that broke
    check-locale-reachable twice. The imports are added under aliased names so
    they cannot collide with a gate's own.
  · Five gates take their URL from argv with no default and are left for a
    per-file pass rather than patched blind.

CONTROLS: against another worktree's server a guarded gate prints NOT MEASURED
naming the mismatch and exits 75; against this checkout's server it runs and
reports normally; against the live site it proceeds.
2026-09-25 08:20:11 +00:00
doxa-maps-88655c70
9eb65faa99 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 07:55:26 +00:00
doxa-maps-88655c70
2043384435 Merge remote-tracking branch 'origin/main' into exp-shepherd/pplr
# Conflicts:
#	internal/generate-tree.mjs
2026-09-25 07:40:35 +00:00
doxa-maps-88655c70
145e3c0df8 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed 2026-09-25 07:40:09 +00:00
doxa-maps-88655c70
a9a44c3eb1 Merge main into bundler-2/work
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-25 07:38:11 +00:00
doxa-maps-88655c70
430c06d978 Browser gates: two slots with a memory floor, not one mutex
The single rendezvous made every agent's browser proof wait behind one chrome —
the queue sat 10-19 deep for hours. Exclusivity was never the real constraint:
MEMORY is, at roughly 300-500MB per headless chrome. So the rendezvous becomes a
counting semaphore.

internal/browser-slot.sh takes the first free slot with flock -n, in a fixed
order, and queues on slot 1 only when all are busy. Before taking any slot it
requires MemAvailable above a floor, because a free slot on an out-of-memory box
is an invitation to be killed mid-measurement — which then reads as a flaky gate
rather than as the machine it actually was. Default 2 slots, 2500MB floor, both
overridable.

It keeps the exit-code contract that the last commit established: 75 is
EX_TEMPFAIL, "not measured", distinct from 0 and from any gate's own 1. The
runner already reports 75 as ⊘ and excludes it from the failure count.

CONTROLS, all fired:
  two takers      both run concurrently
  a third         waits, and takes a slot the moment one frees
  short wait      exits 75 rather than 1, so a full queue is never a red gate
  floor above RAM exits 75 immediately, naming the MemAvailable it saw
  inner exit 3    passes through as 3 — the wrapper never rewrites a verdict
  through a wave  two browser gates, 3.5s wall for 3.9s of work: overlapping

The legacy single lock is deliberately no longer consulted: taking it alongside
the slots would collapse two slots back into the one queue this replaces. Agents'
own wrappers should call internal/browser-slot.sh so everyone shares one counter
— and it lives in the repo precisely because the old shared path carried a name
check:no-codename forbids us to ship.
2026-09-25 07:37:26 +00:00
doxa-maps-88655c70
b32795aa0e Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	package.json
#	public/doxa-maps/index.html
2026-09-25 07:22:18 +00:00
doxa-maps-88655c70
b78e7d1b52 Prayer Walk Planner: fill the page, and print a real walking map
The Driver: "the prayer walk planner is still a very tiny map, not
expanding the full page", and the printed map was a ~10px sliver.

Tiny map. Measured: the leaf filled (1003x739), but inside the Research
Gallery the map was 747x76 on desktop and 390x0 on a phone. The planner
mounts its SFC as the custom-element root (that is what puts its client
API on the element). So Vue's attribute fallthrough copied the gallery's
inline host style onto the root .pwp, and that inline display:block beat
.pwp{display:flex}. inheritAttrs:false keeps host attributes on the host.
The map now fills at 1280x800 and 390x844, in the leaf and in the gallery.
The gallery gets the same guard: its .vtl had survived only through
display:grid !important.

Sliver print. Print used to snapshot the LIVE map, so the page got that
map's on-screen size and shape. Each zone now renders in its own
temporary MapLibre map:
- page-shaped (1200x1344 at pixelRatio 2, a 2400x2688 image, ~320 dpi on
  the 7.5x8.4in frame)
- keyless OpenFreeMap liberty, with streets and building footprints
- fitted to the zone, with the zone outlined on top
- destroyed once its pixels are taken, and the reader's map never moves
One zone = one letter page, with the map filling it.

Localized. The planner read no language at all. It now follows
language → profile-config locale → ?lang → <html lang>, in all 11
locales (private src/strings.js): toolbar, list, status, default zone
names, and the printed sheet (lang/dir set, so Arabic prints RTL).
Street and place labels prefer the OSM name:<lang>. Two Arabic defects
were found by reading the printed sheet, and fixed:
- coordinates were bidi-scrambled → isolated LTR
- map labels were unshaped glyphs → the RTL text plugin is installed
  for MapLibre, from jsDelivr, and awaited before the print frame
  (library/maps/rtl-text.js only does it for Mapbox)

renderZoneMap(id) joins the client API: the print PNG without the dialog.

New gate check:prayer-walk-planner:
- 4 size cells (leaf/gallery × desktop/phone)
- a real zone in Balat, Istanbul renders a >=2000px non-blank print map
- a Spanish toolbar
- 0 page errors
Both gates now prove the served tree is this checkout's (manifest equals
disk) before measuring, and exit 75 NOT MEASURED otherwise.
check:gallery-embed's Big Picture link now asserts that the grid shows
exactly the facet's own count.
2026-09-25 07:21:42 +00:00
doxa-maps-88655c70
33bd7ca387 All People Groups loads fast on a light map; the Vision Card becomes a true funnel from data
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
ALL PEOPLE GROUPS. The Driver: "not even loading, takes a very long
time", and its DOXA pins were dark on a dark map.
- It downloaded the 7.5 MB people-groups-slim.json (22 columns) to draw
  ten fields. It now fetches people-groups-points.json: the same rows in
  column order, repeated strings in dictionaries, coordinates at 3
  decimals. 0.74 MB, generated deterministically by
  library/scripts/people-groups/build-points.mjs and staged into the tree
  by generate-tree. It falls back to the old file on an older tree.
- Pins no longer wait for the live prayer API. Every pin draws by GSEC
  from the file, and the DOXA overlay recolours when the API answers. The
  DOXA count reads "…" until then, not 0.
- Named style 'paper' (Mapbox light-v11 / MapLibre positron) instead of
  'ink'. The four categories keep their meaning; yellow and green are a
  step darker, and pins get a thin dark ring, so they read on near-white.
- Its words are in all 11 locales (allPeopleGroups.json), with a guarded
  useI18n and values passed to vue-i18n so {n} is not blanked.
Measured on a served built tree, 3 fresh runs each, median time to first
rendered pins: 8.3 s before, 5.2 s after, on localhost. This box renders
GL in software, so most of the remaining time is map start-up; the 10x
smaller download matters more on a real connection.

DOXA VISION CARD. The Driver: "the data doesn't make sense … super false
information". The card rotated four typed-in figures with no source.
It is now a proportional funnel, every number computed from data by
library/scripts/people-groups/build-vision-funnel.mjs:
  12,379 people groups   (IMB rows: one group in one country)
   7,341 unreached       (IMB LostnessPriority 0-2, < 2% evangelical)
   4,754 frontier        (LostnessPriority 0, < 0.1% evangelical)
   2,107 DOXA groups     (the DOXA prayer list, snapshot saved in data/;
                          1,745 of them frontier)
The first three nest; DOXA groups do not all sit inside frontier, so the
card says how many do. The rules are on the card ("How these are
counted") and in HOW-THE-NUMBERS-ARE-COUNTED.md, which also explains why
the total is not the 16-17 thousand of Joshua Project's count. Material
light, all 11 locales, right-to-left in Arabic, sources and dates shown.
The old fake stats are removed from the instances file, the frontier
page, the manifest, the staging page and the gallery.

CHECKS.
- check:vision-card-numbers recomputes the funnel independently (own CSV
  reader, the published rules) and compares with vision-funnel.json and,
  with --url, the rendered bars. Negative-tested: a corrupted count exits
  1. On the built tree: bars 12,379 / 7,341 / 4,754 / 2,107 and "1,745 of
  them are frontier", all matching.
- check:map-embed-cells measures PRODUCTION-MATRIX cells B and C for the
  seven gallery maps on a foreign origin; C proves the host's token is
  used by handing the host a distinct token and checking every Mapbox
  request. 14 of 14 pass.
2026-09-25 07:18:30 +00:00
doxa-maps-88655c70
5516c157bb Merge main into bundler-2/work
# Conflicts:
#	public/doxa-maps/country-posters/countries-map/index.html
#	public/doxa-maps/country-posters/country-poster-wall/index.html
#	public/doxa-maps/country-posters/country-posters-mfe/index.html
#	public/doxa-maps/country-posters/country-posters.js
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 07:14:11 +00:00
doxa-maps-88655c70
066b9b0cec Gate runner: a busy lock is unmeasured, not a failing gate
Adding the second rendezvous made browser gates queue against every agent's
probes — and then report the queue as a product defect. MEASURED in the first
wave after that change: five browser gates "failed" at exactly 1200064ms, which
is the flock -w 1200 timeout to the millisecond. flock exits 1 on timeout, the
same code a failing gate uses, so `verify`, check:tabs-localized,
check:packet-browser, check:provider-switch and check:composite-embed-knobs all
printed ✗ having never run.

That is the oldest trap in this repo wearing new clothes: a signal that cannot
distinguish "did not run" from "found something". It is the reason gates grade by
exit code rather than by grepping output, and I reintroduced it by adding a lock
without giving its timeout a code of its own.

Both locks now pass -E 75 (EX_TEMPFAIL, distinct from 0 and 1), and the runner
treats that code as UNMEASURED: ⊘ rather than ✗, excluded from the failure count,
with the reason printed next to it. The wave still exits 0, because a lock it
could not get says nothing about the product.

CONTROLS, on isolated lock files so live contention could not confuse them:
  nested success        → 0
  inner command fails   → 1   (not swallowed into 75)
  lock held by another  → 75  → ⊘ "could not take the browser lock within Ns"
and the wave's own exit stays 0 in that last case.

Also fixed the line that disagreed with its own summary: a lock-busy step printed
✗ on its row while the footer counted it as skipped.
2026-09-25 07:11:08 +00:00
doxa-maps-88655c70
89bd9c5d79 Poster wall: the banner gets stickers, top and bottom
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The card is a BANNER that displays a map, the way a label displays a
design on a bottle. A banner taped up gets a sticker at each end, and
each sticker straddles the edge it tapes: part on the banner's own white
space, part overhanging. Neither one touches the map.

MEASURED at 1440px before this change:
  banner 240-426, map 241-425  ->  1px of white space at each end
  header sticker  35px above the banner's top edge, -1px below
  footer sticker   1px above the bottom edge, 67px below

So the map filled the banner edge to edge and there was no banner
surface for a sticker to sit on. The footer row could only hang off into
space, which is what shipped, and every assertion in the gate was green
through all of it.

The banner now reserves white space at both ends (--banner-pad-top /
--banner-pad-bottom on .main), and each sticker centres on its edge:

  header sticker  17px on the banner / 17px above it
  footer sticker  35px on the banner / 33px overhanging
  map covered by either: 0px

The print row also moved INSIDE .main. It was a sibling, so top:100%
resolved against the CARD, 27px past the banner - the re-parenting and
07d3341c's centring transform are one fix, and neither works alone.
That commit's per-row localStorage preference is preserved.

Card Deck is no longer a peer button with a 162px <select> beside it.
The stock picker is a disclosure: press Card Deck, choose a size, and
that choice prints. A native dropdown in the row collides with the host
page's own dropdowns, which is why it is buttons rather than a <select>.

Print-button labels WRAP instead of clipping. With the per-row default
now 2, a 390px viewport halves the card to ~170px, where Romanian's
"Pachet de cartonașe" cannot share a line: measured 54 of 81 controls
clipped at ro/390px, en and zh clean. The ruling on this wall is that a
long label wraps; nowrap was forcing a clip.

Also fixes the transparent strip above a pinned section header. Sticky
pins to the scroll container's PADDING box, so with .wall's 22px
padding-top the header rested 22px down and posters scrolled visibly
through the gap. A ::before extends the header's opaque fill upward by
exactly that padding, read from the same var so the two cannot drift.
Measured by sampling the strip's pixels: 1 colour, 100% header
background, where before it was 1338-2831 colours of poster artwork.

The MFE shell's own chrome is translated, in all 11 locales. Measured
with ?lang=ar: "Select View", "Views", "Prayer Resources" and "Map"
rendered English while the poster wall's buttons inside the panes
translated correctly, because each pane installs i18n and the wrapper
around them never did - the chrome a reader meets FIRST was the
untranslated part. The wall's own heading, tag and intro go through t()
for the same reason, with the <strong> inside the translated string so
each language emphasises its own words.

The gate grew four assertions, each mutation-tested to fail on the
defect it was written for: off-frame (row floated off the banner),
head-off (header sticker not straddling), map-covered (a sticker on the
poster), and selects (a native dropdown back in the row).

Includes a rebuild of the four PosterWall consumers, which also picks up
studio's shared useMapInstance unmount guard and exp-shepherd's
translated LegendMobile labels.
2026-09-25 06:41:56 +00:00
doxa-maps-88655c70
e1d847028d PPLR data maps (WIP): move the ten PPLR research views into plugin-designs
Not for main yet. All ten views mount on the dev server with real data and no
page errors. Left: the coverage view's status still says 'sample of 6,000',
its choropleth needs checking, and the bundle is not built or tree-verified.
2026-09-25 06:33:49 +00:00
doxa-maps-88655c70
8e5addd9d5 locale gate: read the function it names, not the gap between two landmarks
It sliced library/i18n/index.js from `export function detectLocale` to `export
function createAppI18n` and evaluated whatever lay between. That is a promise
about the ORDER OF A FILE the gate does not own, and it has now been broken twice
the same way: someone exports a helper between those two lines, the slice carries
a bare `export` into new Function(), and the gate dies with "Unexpected token
'export'" — not failing the product, just refusing to start.

Both times the helper was mine. The first fix was to move my helper elsewhere,
which repaired the symptom and left the trap armed for the next person; this time
it caught me again from main while the corrected file sat unlanded in my branch.

Now it extracts detectLocale by matching its own braces and strips any surviving
export keyword, so neighbours are irrelevant. If the function is ever renamed the
gate FAILS by name rather than passing quietly, because a harness that cannot
find its subject has stopped testing anything.

CONTROLS, both fired: adding an exported neighbour between the old landmarks —
the exact crash — now passes; poisoning detectLocale to return 'zz' fails with
0/7 resolution cases. Restored, green.
2026-09-25 06:33:04 +00:00
doxa-maps-88655c70
961d9811db Take main
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-25 06:32:31 +00:00
doxa-maps-88655c70
6be1395d1f Cell C has a gate now: the composite must filter, not navigate
The claim worth testing about the composed dashboard is not that it renders. It
is that it keeps the reader INSIDE it. Standalone, "Every resource in
<language>" is a link to the tables page; composed, it must be a button that
filters in place, because navigating away throws out the map the reader just
positioned and the group they just found. Same words, two affordances, one right
in each context.

Six assertions, each a way the composite can look fine and be broken: the
control is a <button> not an <a>, the URL does not change, the table's row count
goes DOWN, it does not go to zero, the pin count survives, and nothing 4xx'd or
threw.

It picks its subject from the live source rather than by position, and that is
not a detail. The first run opened pin 0 — Abai Sungai, a group with nothing
found — so the filter correctly produced an empty table and the check failed a
working composite. The test was wrong, not the product. It now opens a tier-2
pin, so "narrowed but not emptied" is a question the subject can answer.

Measured: 2,107 pins and 50 rows before, then Telugu narrows the tables to 13
with the map intact and the URL unchanged.
2026-09-25 06:32:02 +00:00
doxa-maps-88655c70
9a18dee1c2 Merge main into bundler-2/work
# Conflicts:
#	package.json
#	public/doxa-maps/country-posters/countries-map/index.html
#	public/doxa-maps/country-posters/country-poster-wall/index.html
#	public/doxa-maps/country-posters/country-posters-mfe/index.html
#	public/doxa-maps/country-posters/country-posters.js
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 06:12:10 +00:00
doxa-maps-88655c70
cb6ee50e7a check:shared-reached: a bundle that never compiles the changed code is not "missing" the fix
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
ensureEngineCSS is only called by initializeMap. A bundle that imports
pieces of useMapInstance (ensureEngineScript, engineGlobal…) but never
initializeMap has ensureEngineCSS tree-shaken away, so the guard can
never appear in it and the race cannot happen there. The check counted
those bundles as missing the fix. upg100 caught it on my-upg-100-list.

The map-unmount-guard entry now also declares the unguarded shape. A
bundle is missing the fix only if it still compiles ensureEngineCSS
without the guard. With neither shape it is reported as not affected.
Entries without a staleRe behave exactly as before.

On today's tree: 7 bundles carry it, 2 are really stale
(doxa-research-map, research-dashboard), and 3 are not affected
(cities-internet, gospel-resources, my-upg-100-list). Before this, all
three were reported as stale.
2026-09-25 06:11:17 +00:00
doxa-maps-88655c70
ce1bce582a Gate runner: one rendezvous for everyone, and a load ceiling that opens
TWO LOCKS, FIXED ORDER. Browser gates queued on a product-named lock while every
agent's hand-run probe queued on a differently-named one, so gates and probes
were not serialised against each other at all — two queues, one machine, and the
dogpile the lock exists to prevent. The runner now takes BOTH, always product
first: flock can exec flock, so nesting is free, and a fixed order means two
processes can never take them in opposite orders and deadlock. Either file alone
still serialises everything that uses it.

The repo cannot simply adopt the other name: it carries an internal code name and
check:no-codename refuses to let a repo we are handing over contain one. Taking
both paths is how the rendezvous becomes shared without the repo spelling a word
it must not ship. VERIFIED in ps while a wave held both, another agent's probe
was waiting on the same file.

THE LOAD CEILING WAS UNREACHABLE, so the guard only ever cost time. It was set at
12 when the risk was seven agents starting chrome at once — but the FLOCK is what
prevents that; the load check is belt-and-braces on a belt. Measured today with
several agents working: 1-min load sits at 36-50 all day, so every browser gate
waited out its full 900s cap and ran anyway. That is a 15-minute tax per wave for
a guard that never opens, and its real effect is that people stop running browser
gates. Now cpus().length * 5 (40 here), scaled because 40 means something
different on 8 cores than on 64, with a 120s cap. Both still override by env.

Also: check:stale-sweep-claim advertised --poison inside an inline comment it can
never receive; the flag gets its own entry and the parent stops advertising it.

gates:pre 7/7, gates:post 42/44 with the two known precondition skips.
2026-09-25 06:10:56 +00:00
doxa-maps-88655c70
b4a0387495 Translate the chrome that was English in all eleven locales
Measured under ?lang=ru on both composites: every Latin-script string left on the
page was ours. The map above was fully translated; underneath it the grid, the
map's own side menu and filter panel, and the whole research dashboard were
English in Arabic, Chinese, Hindi, Russian and everything else.

Three surfaces, three reasons, none of them a missing translation:

  the grid          read labels from a host-supplied `t` object that nothing ever
                    filled, with an English literal at each call site as fallback
  side menu +
  filter panel      plain string literals
  dashboard         labels declared as literals in dashboard.config.js — facet
                    rail, chart titles, KPI strip, table headers, segmented
                    control, search placeholder

New catalogues for all 11 locales (grid, controls, dashboard), REUSING the
catalogue's existing wording wherever the word already existed — Регион, Блок
родства, Население, Вовлечён, Графики — so two panels never name one thing two
ways, and nothing is coined that the project already has a word for.

Read through a new plugin-free tStatic() rather than useI18n, because these
components are also mounted by entries that install no i18n plugin. An unguarded
useI18n() throws and a thrown setup() removes the subtree with no build error —
the defect that had just blanked people-groups-grid everywhere (6603dc17).

★ THE CONFIG LABELS ARE GETTERS, AND A SPREAD WOULD HAVE LOOKED IDENTICAL.
dashboard.config.js is imported once at load; the locale is only known later. My
first version spread a getter object, and a spread COPIES THE RESOLVED VALUE, not
the accessor — every label would have frozen at whichever locale loaded first,
and it would have looked perfect in English. `get label() {…}` resolves on each
read.

MEASURED after, served built tree, identity checked first: Big Picture ru/ar and
Simple Research ru/zh have ZERO Latin-script chrome strings. What remains is
Mapbox's licence attribution (never translate), country and people-group names
(data), and 'UUPGs' — which every locale file deliberately keeps as the acronym.

Three gates went red on the way, two of them mine and worth recording:
  check:locale-reachable  slices this i18n module between two markers and EVALS
                          the slice; I had put tStatic between them, so the gate
                          hit an `export`. Moved to the end of the file.
  check:library-index     my import+helper above a component's docblock became
                          that component's DERIVED description. Moved below it.
  check:kpi-unit          its parser assumed single-brace entries, so getters made
                          it find ZERO pct KPIs — and it failed rather than
                          printing a green PASS about nothing, which is the gate
                          working. Parse now understands getters AND checks the
                          translated labels, so a '%' reintroduced in any of the
                          11 catalogues fails by name. Both controls fired.
2026-09-25 06:10:27 +00:00
doxa-maps-88655c70
40750a153e Screenshots wait for the maps to finish loading
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
readme-shots captured on a fixed timer once the page's elements existed, so
maps were caught mid-load: the Big Picture globe came out as a bare white ball
and the UPG-100 map without its resources. Every request is now tracked, and
the capture waits until nothing has been in flight for 4 s (capped at 60 s,
with a warning if the cap is hit), then 1.5 s for the last tiles to paint.

Retaken with the new wait and checked by eye: Big Picture (country shading and
people-group dots on the globe), the research map, the three prayer /
engagement / adoption presets, all people groups, the Plugin Studio and the
iframe host. The UPG-100 shots are deliberately NOT retaken yet: its owner is
still resizing that map.
2026-09-25 05:52:09 +00:00
doxa-maps-88655c70
ce46d92998 Composite pages hand a ?tk= or pasted token to their nested maps, and ask when there is none
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
A composite's nested Mapbox maps read window.MAP_TOKEN, not the page's
profile-config. In a tree with no baked token (the public copy), the token-free
research-map-plus-grid page mounted before reading ?tk= or a stored token, so
its map stayed blank with no prompt. Now every leaf publishes the token it
resolves to window.MAP_TOKEN, and a token-free composite with no token renders
its content plus the paste-once prompt, then reloads with the token.

Verified in a browser against a tree regenerated with no token:
  research-map-plus-grid  no token: prompt shown | ?tk=: map drawn, Mapbox 6 OK / 0 errors
  dashboard-plus-grid     no token: prompt shown | ?tk=: map drawn, Mapbox 6 OK / 0 errors
Negative control (unpatched generator): research-map-plus-grid with ?tk= never
received the token (0 Mapbox requests) and showed no prompt without one.

Also: removed a personal health note the page template wrote into every leaf
page, and the Mapbox account names from three comments. Pages regenerated on
the latest main; nested-token, sri, hygiene, drop, parse, embed-targets pass.
2026-09-25 05:35:46 +00:00
doxa-maps-88655c70
31ddd2eb87 Merge commit 'dc11cdbb6a536c7797651009851e357a532b6224' into bundler-1/work
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-25 05:19:05 +00:00
doxa-maps-88655c70
dc11cdbb6a check:shared-reached: pin the unmount-guard marker to ensureEngineCSS only
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The first regex for map-unmount-guard also matched useShadowStyles as
the current source compiles it:
  if(!n)return;const i=n.getRootNode();if(!(i instanceof ShadowRoot)||…
The freshly built plugin-studio bundle, which has no useMapInstance at
all, matched it. So any consumer rebuilt against today's useShadowStyles
would have been counted as carrying the fix whether or not it did.

The marker now requires ensureEngineCSS's early exit, "…ShadowRoot))return",
where useShadowStyles continues with "||". Proven both ways on a rebuilt
doxa-simple-map: built from the guarded source it matches, built with
the guard stripped it does not. The studio bundle no longer matches.
2026-09-25 05:17:07 +00:00
doxa-maps-88655c70
4cfe8ec5e2 Merge commit 'd989d598f18227e677f762c15c21df27013a57ad' into bundler-1/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 05:16:03 +00:00
doxa-maps-88655c70
4d773d6ee8 Move internal files out of the repo; fix five dead links; a fuller delegation diagram
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-25 05:14:10 +00:00
doxa-maps-88655c70
0c1a91cd02 useMapInstance: a map unmounted mid-initialise returns quietly instead of throwing
Some checks are pending
build-gate / build (push) Waiting to run
initializeMap is async. If its component unmounts while it waits (a
studio tab switch, a composite swapping views), Vue nulls containerRef
and the next dereference threw "Cannot read properties of null (reading
'getRootNode')" from ensureEngineCSS. check:studio-all hit it about 1
run in 4 (gospel-dashboard); gospel-plugin found a second site one await
later (containerRef.value.clientWidth after the animation frame).

initializeMap now checks the ref after each of its three awaits and
returns silently when it is gone, and ensureEngineCSS returns on a null
element. An unmounted pane is normal, not an error, so nothing is
logged. The ref is the only liveness signal: requiring isConnected too
would silently stop maps that mount into a subtree before it is
attached.

Proof, deterministic in both directions:
- check:map-unmount-mid-init holds mapbox-gl.js, opens a map plugin in
  the studio, switches away (destroying the pane), then releases the
  script. Before: FAIL 2/2 runs (research-map, doxa-simple-map threw
  the production error). After: PASS 2/2 across research-map,
  gospel-dashboard and doxa-simple-map.
- check:map-unmount-race (gospel-plugin's browser-free test, adopted):
  the guarded ensureEngineCSS resolves; with the guard stripped it
  throws the exact message.
- No regression: check:studio-all passes 31/31, and 19 plugins still
  create a live map canvas, as before.

check:shared-reached now tracks this change with a regex marker (a
control-flow fix has no literal that survives minification). It
reports 12 consumer bundles still to rebuild. Verified: rebuilding
doxa-simple-map moves it to "carries it".
2026-09-25 05:01:13 +00:00
doxa-maps-88655c70
32700c538d check:studio-all: print the top of the stack for a page error
Some checks are pending
build-gate / build (push) Waiting to run
A failure read "Cannot read properties of null (reading 'getRootNode')",
which names no caller, and the owner could not act on it without
knowing which call site threw. The gate now keeps the first four stack
frames with each page error. That output located the intermittent
gospel-dashboard error: useMapInstance.js ensureEngineCSS, called from
initializeMap after its pane had already been unmounted.
2026-09-25 04:42:37 +00:00
doxa-maps-88655c70
e6f328b40b check:studio-all: open every plugin in the studio, not a sample
Some checks are pending
build-gate / build (push) Waiting to run
smoke:studio samples 4 tabs. On 2026-09-25 a sweep of all of them found
people-groups-grid rendering 2 DOM nodes behind a page error, and 13
plugins mounting with their data 404ing on a fresh clone. The sample
had passed.

  bun run check:studio-all -- --url <studio page>

Opens every plugin row in the studio's tree and fails a plugin on the
studio's error overlay, a page error or failed request while it is
active (Mapbox hosts excluded), or a near-empty mount. Graded by exit
code. --url is required, because hardcoded :5173 gates have measured
another worktree's tree. The package.json entry carries no inline
comment: a `# …` in a script string swallows the arguments bun passes.

Negative-tested: re-introducing the unguarded useI18n() in
FilterDropdown fails it, naming people-groups-grid, research-map-plus-grid
and stack-demo. Against today's dev server: PASS, 31 of 31, in 3 of 4
runs; the fourth caught an intermittent gospel-dashboard page error,
reported to its owner.
2026-09-25 04:30:09 +00:00
doxa-maps-88655c70
47ced6d6b2 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 04:23:12 +00:00
doxa-maps-88655c70
c8b13286f2 Research Gallery: speak the host page's language, and prove the composites' links
Language. The gallery fell back to the browser's language and forced it
onto every nested map through profile-config.locale, while its own rail
stayed English. Measured: a Spanish page viewed in a German browser showed
German maps under an English rail. It now follows the ladder every nested
map already uses: the `language` attribute, then ?lang=, then <html lang>
(watched live, so a host that switches language switches the gallery), then
English. The browser only counts once a visitor has used the gallery's own
switcher.

Its own words (rail, sections, hints, badges, notice) now ship in all 11
locales, in a private src/strings.js that reuses the library's terms.
Rail sweep, 11 locales at 1400px and 390px: 21 of 22 clean. The one clip,
Italian 'Avanzamento della preghiera' (191px in a 176px slot), now uses the
rail form 'Avanzamento preghiera'. A live <html lang> switch from en to
pt-BR re-renders the rail.

Interop (matrix cell D). check:gallery-embed gains a `linked` phase that
drives each composite's real control INSIDE a gallery tab:
- Big Picture: a facet shows "Showing 1,309" in the grid.
- Simple Research: a grid search narrows 2107 → 3. Its halves are
  independent by design.
- Regions: clicking "Africa" in the list retitles the dashboard "Africa".
- UPG-100: "Show on map" brings the map.
UPG-100's missing zone selection is an owned, printed LINKED_KNOWN entry.
Full gate: js 13, iframe 13, neighbour 13, linked 4.
2026-09-25 04:22:54 +00:00
doxa-maps-88655c70
3ad7bb7438 Merge commit '8836182db264adc34b251ea3cc9298d7d31eed09' into bundler-1/work
# Conflicts:
#	internal/check-research-gallery-embed.mjs
#	public/doxa-maps/index.html
2026-09-25 04:20:26 +00:00
doxa-maps-88655c70
6e85370797 Regions honours urlSync:false, so a nested tab stops rewriting its host's URL
rega's embed sweep found region-dashboards.vue:499 URL_SYNC unconditional.
Nested in the Research Gallery this element rewrote the HOST page's #anchor
through writeUrl() and retitled the host's tab through syncTitle(). Those two
capabilities are ours when we ARE the page and not ours when we are one tab
inside someone else's. The gallery already passes profile-config.urlSync:false
for exactly this.

writeUrl, syncTitle and popstate now check it. The guard is a COMPUTED, not a
second module-level const, because the injected profile-config arrives after
this module evaluates -- a snapshot taken beside URL_SYNC would read undefined
and sync anyway. That is the same late-injection trap that already made every
value read off the string prop fall back to its default in this file.

DELIBERATELY NOT GUARDED: slugFromHash(), the boot-time READ. The complaint is
about writing to a host we do not own, and refusing to read would change which
subject a standalone deep link opens. The residual is narrow and stated in the
comment: on a host whose own #anchor happens to equal one of their slugs, a
nested Regions opens that subject instead of the chooser. Say so rather than
quietly widen the change.

Deletes both routed NEIGHBOUR_KNOWN entries in check:gallery-embed --
regions-dashboard:url and regions-dashboard:title -- in this same commit, so
main never carries a stale exemption for a bug that is fixed.
2026-09-25 04:19:09 +00:00
doxa-maps-88655c70
193127e257 Stop publishing the template scaffold's placeholder pages
Some checks are pending
build-gate / build (push) Waiting to run
template-bundle is the scaffold DX copies to start a plugin: its source
ships, but it is not a plugin. The dev manifest already skips it by
name (discover.mjs), while the build published it, so the built studio
listed template-map-a and template-map-b as two blank-looking plugins
that the dev studio never showed.

generate-tree now skips it by the same name rule, so the built
manifest lists exactly what dev lists: 21 bundles, 33 profiles. The
committed tree is brought in line without a full regeneration: the
entry is removed from manifest.json and from the staging page's baked
copy, the two leaf pages are deleted, and the archived gallery is
regenerated from the manifest. It is not added to gatedProfiles, which
is the private-data list.

Verified: check:sri and check-manifest-covers-discovery pass, the
staging page renders with no errors, and a sweep of the built studio
shows 31 plugins and no template rows.
2026-09-25 04:05:59 +00:00
doxa-maps-88655c70
344990d186 Research map: centre the active tab without scrolling the host page
On mount the map centred its active tab with

    btn.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' })

and 'nearest' does not mean "do not scroll the page". It means "scroll every
scrollable ancestor by the least amount needed", and on a partner page the HOST
is an ancestor. Measured by the embed sweep: a page carrying this map below the
fold jumps to y≈640 the moment it mounts — on a phone, where Simple Research is
the default tab. The reader asked for nothing and the page moved under them.

The tab strip now scrolls ITSELF: compute the offset that centres the active
button inside its own box, clamp it to the strip's own scroll range, and write
scrollLeft. Same shape as SemanticTreeLegend's bringRowToTop, which scrolls the
container it found rather than asking the platform to walk ancestors. An element
that only ever writes its own scrollLeft cannot move anybody's page.

The exemption goes in the SAME commit, per that gate's contract — main never
carries a stale exemption or a red gate on someone else's bug. Both
NEIGHBOUR_KNOWN entries (research-map, research-map-plus-grid) are deleted here.

MEASURED after, with the exemption gone: check:gallery-embed PASS, and the
neighbour phase now reports "research-map leaves the host page alone" and
"research-map-plus-grid leaves the host page alone" on its own evidence — js 13
tabs, iframe 13 tabs, neighbour 13 tabs. The two KNOWN lines still printed belong
to regions-dashboard and are theirs to clear.
2026-09-25 03:57:20 +00:00
doxa-maps-88655c70
580dff6369 Studio first run: dev lists what the build lists, and the self-test finds its manifest
Some checks are pending
build-gate / build (push) Waiting to run
Two gaps found by opening every plugin in the studio under the dev
server:

- key-cities is gated (vite.doxa-maps-build-config.json gatedProfiles)
  out of every public surface the build emits, but the dev manifest
  listed it anyway, so the dev studio and the Research Gallery's
  Experimental shelf showed a plugin the built tree deliberately hides.
  The dev model now applies the same gate: 21 bundles, 33 profiles, as
  in the built manifest. readBuildConfig() does not carry gatedProfiles,
  so the gate is read from the config file, as generate-tree does.
- golden-path-check takes its tree base from a tree-base attribute, not
  from profile-config. The studio only passed the latter, so inside the
  studio the check guessed the tree from the page URL, fetched
  /manifest.json, got a 404 and showed an empty self-test. The studio
  now sets tree-base on every element it mounts; elements that do not
  read it ignore it.

Dev sweep after: all 31 listed plugins mount with content, no page
errors, no failed requests. Rebuilt the plugin-studio bundle and
re-baked its SRI.
2026-09-25 03:43:38 +00:00
doxa-maps-88655c70
2dce74ce67 Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 03:37:50 +00:00
doxa-maps-88655c70
86fb6e750b Research Gallery: a good guest on the partner's page
Embedded below the fold on a phone-width host page, the gallery disturbed
the page it was a guest on:

- The host's inherited typography flowed into the gallery. Shadow DOM
  isolates selectors, not inheritance, so a host with body{font:20px/3;
  letter-spacing:2px} re-flowed every rail row and legend (root measured at
  20px / 60px / 2px). :host now resets the inherited text properties to the
  gallery's own defaults (16px / normal / normal). Direction still inherits,
  so RTL hosts stay RTL.
- Nested maps wrote the host's URL and title. The gallery now tells every
  child urlSync:false. Regions needs to honour it (routed).

check:gallery-embed gains a good-neighbour phase: phone width, gallery
below the fold, hostile host CSS, the host's own #anchor and <title>. After
load and after every tab it asserts no host scroll, an unchanged URL, an
unchanged title, and no inherited typography. Two nested-plugin violations
are listed, owned and printed in NEIGHBOUR_KNOWN until their owners fix
them (same contract as MAPBOX_KNOWN):
- research-map.vue:647's scrollIntoView jumps the host to y=640 on load on
  a phone.
- Regions' unconditional URL/title sync.

A boot timeout is now a named FAIL instead of an uncaught crash. The gate
caught a real one on the way: a rebuilt bundle whose leaf page still
pinned the old SRI hash.
2026-09-25 03:37:26 +00:00
doxa-maps-88655c70
bf7e0e6464 Dev server: serve assets from data/ and under the tree base, so a fresh clone's plugins get their data
Some checks are pending
build-gate / build (push) Waiting to run
Opening every plugin in the studio under the dev server on a fresh
worktree, 13 of them mounted with empty data: every table, boundary and
people-groups file 404'd. Two causes.

- The dev asset lookup read public/assets (a mirror generate-tree
  writes, absent on a fresh clone) and plugin-designs/*/data, but never
  data/, the tracked source that mirror is copied from. It now falls
  back to data/<rel>, which is what DX gets when they clone.
- Plugins nested in the studio resolve assets against the tree base the
  studio hands them (/__plugin-gallery/), as they do against <tree>/assets
  in the built tree, but only /assets/ was served. Both prefixes are
  served now.

After: the same sweep shows the data 404s gone from all 13.
2026-09-25 03:27:29 +00:00
doxa-maps-88655c70
e32034039d poster cards: assert the preview's alt, which is the card's only name
Some checks are pending
build-gate / build (push) Waiting to run
MEASURED while sweeping my own gates for the count-only pattern: a poster card's
TEXT reads "116 UPG · Show on map" — a count and an action, no zone. The
identity lives entirely in the image's alt ("Northwest India — prayer poster…").

So the wall is CORRECT AS BUILT — all 27 previews carry a descriptive alt, and
sighted and screen-reader readers both learn which poster is which. No defect.
But that alt was doing load-bearing work nothing asserted: remove it and the
wall becomes 27 anonymous pictures for a screen-reader user while every
geometry, clipping, hover and reachability assertion here stays green.

★ I ALMOST ASSERTED THE WRONG THING. The obvious move on seeing "no zone name in
the card text" is to require one. That would have been me inventing a
requirement nobody stated — the same line I drew on the card-stock labels and
the UUPGs header. Measuring first showed the identity was already there, in the
attribute rather than the text, so the right assertion protects what exists
instead of mandating what I would have preferred.

MUTATION-TESTED: emptying the alt binding fails all 15 width x locale
combinations with "27 poster preview(s) have no alt text", exit 1. Restored,
gate green, SRI consistent.
2026-09-25 03:18:31 +00:00
doxa-maps-88655c70
2087074e9d Stop telling readers the audio sweep is unfinished — it finished
Some checks are pending
build-gate / build (push) Waiting to run
The legend's caveat is the paragraph a reader uses to decide what "nothing
found" is worth. It said GRN was being swept a batch at a time and part of the
pool was still unread. That was true while the crawl ran and false the moment it
ended, and it stayed on the page through four commits that knew better. Found by
reading a screenshot, which is the only thing that could have found it.

Corrected on all three surfaces, and scoped honestly rather than upgraded: the
sweep finished for every one of the 1,192 languages in the gap, so "nothing
found" no longer rests on an unread pool — but codes that already carry a
resource were not all asked, because the sweep was spent on the gap first. Both
halves are now on the page.

A gate holds it. The DATA decides whether the claim is allowed: while codes are
genuinely undecided it passes, and it starts failing the moment they are all
decided, so it re-arms itself for the next slow pool. It does not flag a claim
rendered under a v-if on the pending count, because that form expires on its own
and is the one to copy. Its first version asked about every code on the map
instead of the gap, measured 3,202 undecided and would have let the stale
sentence stand.
2026-09-25 02:57:50 +00:00
doxa-maps-88655c70
8a3d836e44 packets: assert a people-group card actually NAMES someone
Some checks are pending
build-gate / build (push) Waiting to run
The gate counted .card elements and checked the photo/typographic split. A card
with a photo and a BLANK NAME satisfied all of it: the total held, the split
held, the page arithmetic held. Counting proves the element exists; the spec
says pages 2..n carry "the people groups, 20 per page, with photos", and a slot
that names nobody is not an entry in a prayer list.

★ MY FIRST VERSION FAILED ITS OWN MUTATION TEST, and the reason is the useful
part. It tested card.textContent. Blanking every name still left 18 characters
per card, because the typographic fallback ("no photo available") lives INSIDE
.card — so the card was not empty, it just said nothing about a people group.
The assertion passed a packet where all 95 names were gone.

Now it measures .nm, the name line the renderer writes (packet-render.js:229).
Same lesson as the nophoto count one level up, and as the consumersOf substring
test: match the thing you mean, not a container that happens to include it.

MEASURED on laos: 95 names, 0 blank, shortest "1. Aheu" — a threshold of 3 sits
far below any real entry and above an empty one.

MUTATION-TESTED against the mutation that defeated the first version: blanking
<div class="nm"> fails with "95 of 95 people-group card(s) have no NAME",
exit 1. Restored, and check:packets-all stays green across all five shapes.
2026-09-25 02:51:50 +00:00
doxa-maps-88655c70
43e3102f47 Check 5fish links too, and say why YouVersion stays out
The rot sampler now covers 5fish by where its redirect LANDS — a published code
goes to its numbered language page, one they do not carry is swept to /regions —
the same rule the verifier uses, with both arms proved in band. Six links a run,
ten seconds apart, because their robots.txt asks for it. That leaves 94 of 8,510
shipped links outside every re-check, down from 5,760 this morning.

YouVersion is the largest catalogue the brief names and it is now recorded as
rejected, with the measurement: nodejs.bible.com serves it without a key but its
robots.txt is Disallow: / , www.bible.com answers even robots.txt with a bot
wall, and the unauthenticated slice returns 2 English versions against the dozens
that exist — so taking it would be both discourteous and wrong. It is named in
the unchecked list on 5,413 codes instead of being silently absent, because a
reader deciding that a group has nothing is entitled to know which pools nobody
searched.
2026-09-25 02:44:37 +00:00
doxa-maps-88655c70
4e20a98d12 Judge a bible.is link the way the verifier already does, by its API
The band shipped an hour ago compared the page against an empty shell measured
at run time. It separates cleanly today, but verify-gospel-links.mjs had already
dropped that rule for this host and recorded why: their WAF began answering HEAD
with a 403 of the same size for real and bogus filesets, and the rule agreed
that everything was dead. /api/bibles/<id> answers 200 or a 75-byte 404.

Also corrects two claims in the same file. jesusfilm.org was excluded on the
strength of the retired ?languageId= shape, not the shareUrl path it ships now;
and knowgod.com is not unverifiable — Cru's API proves those pairs, the verifier
checks them that way, and this file simply has not reproduced it yet.
2026-09-25 02:44:36 +00:00
doxa-maps-88655c70
86ca935ed9 Re-check the links nothing was re-checking: 4,685 more, in two measured bands
The rot check exempted four hosts as "never 404". Measured on the shapes the
map actually ships: jesusfilm.org does 404, and live.bible.is separates a real
page from an empty shell by better than ten times its size. That was 69% of the
corpus outside every check; it is now 13%, and the two that stay out carry the
measurement that keeps them out.
2026-09-25 02:44:36 +00:00
doxa-maps-88655c70
51a3abe482 packets: render the MIXED photo case, and assert the fallback says something
Some checks are pending
build-gate / build (push) Waiting to run
Every packet ever produced was all-photo (laos: 95 photos, 0 typographic) or
all-typographic (every fixture: 0 photos, N typographic). THE MIXED CASE HAD
NEVER BEEN RENDERED — and it is the realistic one. fetchPhotos already degrades
per-photo on a failed fetch, so any zone where a few images 404 puts exactly
that layout in front of a reader.

PHOTOS_EVERY=N keeps only every Nth photo, deterministic by INDEX rather than
random so the same command always yields the same packet — a fixture that
differs run to run cannot be a gate. The build stamp gains `mixedphotos`,
because a deliberately mixed packet and a partly-failed photo fetch deserve
opposite verdicts and nothing could tell them apart.

★ IT IS ALSO AN I18N SURFACE NOBODY HAD SEEN. The fallback card renders
t('no_photo'), translated in all 11 catalogs, and the gate COUNTED .nophoto
elements without ever looking inside one. A blank grey box passed every
assertion: `photos + typographic === cards` held, the page count held, nothing
was broken. Counting proves the element exists; it says nothing about whether
the reader sees a label or a hole.

MEASURED on the first mixed packet (PHOTOS_EVERY=3 over laos, 31 photos and 64
typographic): all 64 read "no photo available", 0 blank, all 64 with a real box.
In Spanish "sin fotografía disponible", in Arabic "لا توجد صورة متاحة" — 63 and
64 cards, 0 blank in both.

MUTATION-TESTED: emptying the fallback to `<div class="nophoto"></div>` fails
the gate with "63 of 63 typographic card(s) render an EMPTY box", exit 1.
Restored, and check:packets-all stays green across all five shapes.

One trap worth recording: the language variable is PACKET_LANG, not LANG. The
file warns about it — process.env.LANG is the shell's POSIX locale
("en_US.UTF-8") — and I still lost a run to it, regenerating three full-photo
packets that proved nothing.
2026-09-25 02:42:04 +00:00
doxa-maps-88655c70
a4c9c0a6c7 gates: refuse to measure a tree that is not this checkout
Some checks are pending
build-gate / build (push) Waiting to run
"Answers 200" and "is serving this build" are different questions, and my two
browser gates only ever asked the first. Point either at a stale server and it
measures the OLD build and reports PASS, with nothing in the output naming which
tree it saw.

★ THAT IS NOT HYPOTHETICAL — I DID IT FOR SEVENTEEN TICKS. The published site was
many merges behind while I ran these gates against it and reported the results
as live verification. They were confident measurements of a tree that was not
mine. The country-posters defect they eventually caught was only visible once
the publish made live and local agree; before that, a stale copy would have
passed either way and I would not have known the difference.

bundler-2 found this class first (BUG-BROWSER-GATES-CAN-MEASURE-ANOTHER-WORKTREES-TREE:
port 4173 on this box was held by a different worktree, and a gate reported three
confident runs about code nobody had changed) and wrote _serves-this-tree.mjs,
which hashes the served manifest.json against the one on disk. Eight gates use
it; mine did not. This adds it to the two of mine that drive a browser.

⚠ NOT MEASURED, NOT SKIP, and the distinction carries the meaning. A SKIP reads
as "nothing to check here". This is "I could not attribute what I saw", which is
a different and louder fact. Exit 2, so a caller can tell it from a pass (0) and
from a real failure (1).

A useful side effect: these gates now refuse to measure live whenever the
publish is stale, turning the silent failure mode I lived with into a loud one
at the point of use.

VERIFIED BOTH DIRECTIONS on both gates: against this checkout they measure and
pass as before (15 width x locale combinations; 195 polys · 27/27 zones · 2107
pins on both leaves); against a host serving a different build they print NOT
MEASURED and exit 2 instead of passing.

The other 26 browser gates that lack this belong to bundler-2's chain, which
owns them — reported, not converted.
2026-09-25 02:26:03 +00:00
doxa-maps-88655c70
852310d05e embedBase: the asset pool is INSIDE the tree, not beside it
Some checks are pending
build-gate / build (push) Waiting to run
resolveAssetsBase computed `<treeRoot>/../assets` -- one path segment too high.
This reached the live site. MEASURED by publishing to the catalog and reverting:

  leaf  .../doxa-maps/my-upg-100-list/upg-100-list/index.html + '../../assets'
                            -> /plugin-assets/doxa-maps/assets   200
  code  root '/plugin-assets/doxa-maps' + '/../assets'
                            -> /plugin-assets/assets             404

The UPG-100 map went from "195 polys · 27/27 zones · 2107 pins" to
"⚠ boundary fetch 404 world-adm0.geojson" -- a basemap with none of our data on
it. Reverted within minutes; live is healthy.

★ THE COMMENT JUSTIFIED THE OFF-BY-ONE IN PROSE, which is how it survived. It
said the pool is "a SIBLING of the tree -- <root>/../assets -- which is why the
leaf page's generated value is '../../assets' (two up from
<tree>/<bundle>/<profile>/)". Two up from <tree>/<bundle>/<profile>/ is <tree>,
not the tree's parent. The arithmetic was wrong and the sentence made it read
deliberate. generate-tree stages assets INSIDE the tree on purpose -- "staged N
runtime asset(s) -> doxa-maps/assets", so a dropped folder is self-contained.

⚠ WHY FIFTEEN TICKS OF GREEN GATES MISSED IT, three independent reasons:
  1. INVISIBLE LOCALLY. The dev web-root has BOTH public/assets (a sibling) and
     public/doxa-maps/assets (inside), each holding the boundaries -- so the
     wrong path RESOLVES on 127.0.0.1. The catalog publishes app/doxa-maps only;
     no sibling exists there. No local run can tell the answers apart.
  2. THE BYTE GATES PASSED. check:published and check:catalog-currency both went
     GREEN on the broken publish -- 22 of 22 byte-identical, 0 stale. They prove
     the bytes match, not that the page works.
  3. NO GATE REFERENCED THE FUNCTION, while 10+ profiles across 6 bundles call
     it.

So the fix ships with check:embed-base, which is PURE ARITHMETIC -- no browser,
no network, no served tree, so it cannot SKIP itself into irrelevance. It
computes what a leaf's own '../../assets' means and asserts the resolver agrees:
the oracle is what the generated HTML already says, not an opinion about where
assets belong. It also pins the explicit ladder rungs, so a narrow fix cannot
swallow a host-supplied assetsBase.

MUTATION-TESTED: restoring the original '/../assets' fails all four tree roots
(live catalog, dev, doxa.life drop-in, deep subpath) in milliseconds.
2026-09-25 01:30:56 +00:00
doxa-maps-88655c70
953fe79002 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 01:23:35 +00:00
doxa-maps-88655c70
92d236106d Re-test links we already called checked
The verification table records that a URL resolved on the day it was
checked, and nothing ever re-tests one that passed. A publisher can
retire a page and this map will go on offering it indefinitely, still
labelled "link checked". Clause L guards the freshness of the record;
it cannot notice the web moving underneath it.

This samples 153 links — 25 per host at a fixed stride, so the same
run is repeatable — and today every one of them still resolves.

THE PART WORTH KEEPING is which hosts it refuses to check. Four of the
eleven never 404: jesusfilm.org, live.bible.is and knowgod.com serve
an identical shell for any path, and fivefish.org answers a nonsense
path with a plausible geo-located page at HTTP 200. Re-checking those
by status would report perfect health forever, which is worse than not
checking them, because it would look like evidence. They are verified
by publisher API or page title in the main verifier instead.

Three impossible URLs run in band every time. If one answers 2xx the
check reports PROVED NOTHING and exits non-zero rather than passing.
That mattered immediately: the first version sampled one link per host
instead of 25 and still said PASS — a green that meant almost nothing,
caught only by reading the count rather than the verdict.
2026-09-25 01:09:08 +00:00
doxa-maps-88655c70
bc368dadda gate: a key copied into all 11 catalogs is not a translated key
Some checks are pending
build-gate / build (push) Waiting to run
check:i18n-parity asserts every locale carries every key -- and a key can pass
that perfectly while holding the English string in all eleven files, which is
what "translated" was supposed to mean. Presence and translatedness are
different properties, and only the first was measured.

Swept every catalog: exactly THREE keys are byte-identical across all locales,
out of 210 compared. Finding three rather than three hundred is what makes this
worth gating -- it is signal, not nagging.

★ THE DEFECT IT FOUND. legend.columns.upgs is the literal string "UUPGs" in all
11 catalogs, while `population` sitting beside it IN THE SAME HEADER ROW
translates normally -- Población / 人口 / Население / السكان. The legend renders
a fully localized row with one English acronym wedged into it, and prayerLaps
carries the same string.

AND THE PRODUCT HAS ALREADY RULED ON THE CONCEPT, which is what makes this a
defect rather than a preference: packet-strings.json translates the same idea in
prose in every locale -- "unreached people groups" becomes grupos étnicos no
alcanzados / 未得之民 / неохваченных народов. The legend disagrees with the packet
the legend generates. Two honest surfaces, one vocabulary, no agreement.

⚠ IT DOES NOT PROPOSE TRANSLATIONS, and that restraint is deliberate. Drafting
candidates here produced invented initialisms -- PNA, UEVG, ННГ -- assembled
from translated phrases with no source establishing any of them as the field's
term. A wrong term looks AUTHORITATIVE to a reader who cannot check it, which is
worse than an honest English acronym. The gate names what is untranslated; a
human who knows the vocabulary supplies the words.

⚠ AND IT CARRIES AN ALLOWLIST, because some keys are correctly identical: pure
interpolation formats, brand names, ISO designations. The sweep found one --
legend.progress.segment is "{label} · {count} · {pct}%", all placeholders. Each
entry needs a REASON, so the list reads as a record of decisions rather than a
silencer.

MUTATION-TESTED THREE WAYS: making ONE locale match English is correctly NOT
flagged (it requires all of them, so a genuinely translated key cannot trip it);
making a key identical in ALL eleven IS flagged; and allowlisting a real finding
moves it from the failure list to the allowed list, 2 down to 1.
2026-09-25 01:06:08 +00:00
doxa-maps-88655c70
2e8f303a43 poster cards: measure the density the Driver asked about
Some checks are pending
build-gate / build (push) Waiting to run
The Driver asked for "around 4 per row on desktop, like Instagram". The grid is
ONE rule for every width -- repeat(auto-fill, minmax(210px,1fr)) -- so the count
falls out of the viewport rather than being chosen, and nothing measured it. The
card's own verify says so: "perRow is currently printed nowhere and asserted
nowhere. A number printed but not asserted on is decoration."

MEASURED, all five widths:
    390px   max 1/row @ 354px
    768px   max 3/row @ 224px
   1024px   max 4/row @ 225px   <- already on target
   1440px   max 5/row @ 212px
   1920px   max 5/row @ 212px
The miss is only at 1440 and 1920, and the card width explains it: the grid
packs a fifth card by SHRINKING to 212px rather than growing toward 225px,
because auto-fill adds a column the moment 5x210 fits.

⚠ REPORTED, NOT ASSERTED, deliberately. The desktop target is settled (~4); the
MOBILE one is not -- 3 per row at 390px is a ~110px card, less than half the
224px that currently holds a readable print footer at 768px. Asserting a number
the Driver has not ruled on would encode my taste as a requirement. Measuring it
is what makes the ruling answerable, and it is the precondition for tuning:
you cannot tune toward a target you are not measuring.

★ IT IS THE MAX ACROSS ROWS, NOT THE FIRST ROW. Grouping every card by top edge
at 1440px gives [5,5,5,3,3,2,2,1,1] -- the short rows are the REMAINDERS of
collapsible sections, not a density failure. Reading the first row alone is
right here by luck and wrong on a wall whose first section holds one poster.

MUTATION-TESTED so the number cannot be decoration itself: widening the track to
minmax(260px,1fr) moves it to 1/2/3/4/4 per row at 354/351/309/273/273px. That
also prices wp1 -- 260px lands 1440 and 1920 on exactly 4 with BIGGER cards
(273px vs 212px), which is what "a bit bigger, more room for the footer" asked
for, at the cost of dropping 768px to 2/row. Which is why that card says "add a
desktop breakpoint" rather than "change the number". Not shipped: it changes
what readers see, and the card is gated.
2026-09-25 00:44:45 +00:00
doxa-maps-88655c70
6090e8e7da Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-25 00:32:03 +00:00
doxa-maps-88655c70
9d1a0acd9e Hold the gap table to the split it publishes
Last tick the map and the exported gap table bucketed the same 1,845
groups differently — the table tested sign language first, the map
tested private-use first, so 47 Deaf groups landed in different
buckets and the Deaf share read 136 in one place and 183 in the other.
Both were internally consistent and told different stories, which is
worse than one being plainly wrong: a reader comparing them cannot
tell which to believe. I fixed it by hand, which fixes today only.

Clause O holds three things a reader depends on: every tier-0 group
appears in the table exactly once, every row carries a kind, and a
sign language is ALWAYS labelled as one rather than as an unindexable
code — because being Deaf is the actionable fact, and every
private-use code in this gap belongs to a sign language anyway.

Checked against the real defect, not a synthetic one: putting the
precedence back the wrong way round makes it fail with "47
sign-language row(s) are not labelled as sign languages (e.g. qcd,
qiq, qns) — the Deaf share of the gap will read low". Poison is 15/15.
2026-09-25 00:24:29 +00:00
doxa-maps-88655c70
8c3576b4ed gate: a consumer is one that IMPORTS the component, not one that mentions it
Some checks are pending
build-gate / build (push) Waiting to run
The consumer list was derived with a plain substring test, and a substring
cannot tell an import from a comment ABOUT the component.

MEASURED: it reported research-dashboard as a stale consumer of
SemanticTreeLegend for three ticks, and I routed that "drift" to its owner as
work to do. It was a FALSE POSITIVE. research-dashboard imports the component
ZERO times and mentions it four times in prose -- DashboardFacetRail.vue:3 says
"the floating @map SemanticTreeLegend is DROPPED", followed by the root cause it
was dropped for. A bundle that deliberately removed a component is CORRECT to
lack its translation keys. The gate was reading the comment explaining the
removal as evidence of use, and the more carefully that removal was documented,
the more certain the false positive became.

★ A GATE THAT MANUFACTURES WORK FOR ANOTHER AGENT IS WORSE THAN ONE THAT MISSES
DRIFT. A missed regression is found by the next reader; an invented one costs
someone a tick discovering there was nothing to fix, and it discredits the gate
that raised it.

Now requires real import syntax -- `import ... from '<path>'` or a dynamic
`import('<path>')` -- matching either the @map path or any path ending in
/<Name>.vue.

MUTATION-TESTED THREE WAYS, because narrowing a detector can quietly blind it:
  false positive gone  -- research-dashboard no longer listed; legend-a11y goes
                          4 of 5 to 4 of 4, and every genuine consumer is
                          unchanged (PosterWall still finds all four)
  real drift caught    -- stripping the marker from doxa-simple-map's built
                          bundle fails it, exit 1
  new consumer found   -- injecting a real import into big-picture lists it
                          immediately as missing the keys

The second and third are the ones that matter: it would have been easy to fix
the false positive by narrowing until the gate saw nothing at all.
2026-09-25 00:24:26 +00:00
doxa-maps-88655c70
8fbc9beacc gate: the tree we publish has a weight, and nothing was watching it
Some checks are pending
build-gate / build (push) Waiting to run
A publish was ALREADY BLOCKED once at a 74MB asset tree -- Forgejo rejected the
pack. Nothing has measured it since. public/assets is now 132MB, against the
94MB recorded when BUILD-POSTERS-GENERATE-ON-DEMAND-DROP-52MB was written. It
grew ~40MB and no one noticed, because size only becomes visible when something
refuses to accept it.

★ IT REPORTS THE SPLIT, NOT A TOTAL, because a total invites deleting the wrong
half:
    city-*.webp     176 files  22.0MB   NOT reproducible -- needs OSM tiles
    country-*.webp  174 files  13.2MB
    zone *.webp      28 files   5.7MB   reproducible
    zone *.html      28 files  27.9MB   reproducible
The city posters are simultaneously the heaviest single group and the one that
cannot be regenerated offline, which is why publish-html.mjs deliberately skips
them. "132MB" on its own points an optimiser straight at them.

⚠ AND RAW SIZE IS NOT THE SHIPPING COST -- the gate says so in its own output.
publish-html.mjs already measured this with real git packs: swapping the 28 zone
rasters (5.0MB raw, ~5.0MB packed; webp is already compressed and git cannot
pack it further) for their HTML sources (21.4MB raw, 5.78MB packed; repetitive
text) is about +0.8MB PACKED. A net LOSS. Its comment says "do not ship that
swap and call it a saving", and a gate reporting raw bytes without that caveat
would have re-argued for exactly the swap that file talks you out of.

The ceiling is 160MB -- roughly 20% headroom over today, far above the current
tree and far below a surprise repeat of the 74MB rejection. Deliberately not
pinned to today's value: a gate that fails on the next legitimate asset teaches
people to raise the number instead of reading it.

MUTATION-TESTED BOTH WAYS: a ceiling below the current tree fails it (exit 1)
with the publish-rejection history in the message; a generous ceiling passes
(exit 0). The poster buckets were checked exhaustive -- 406 of 406 files
categorised, so nothing can hide between them.
2026-09-25 00:20:02 +00:00
doxa-maps-88655c70
b8d57be8d7 poster cards: assert a reader can actually REACH the last poster
Some checks are pending
build-gate / build (push) Waiting to run
Every assertion in this gate measured cards that are ALREADY laid out -- which
is satisfied perfectly by a wall whose bottom half is unreachable. The
Kingdom-agent ruled UPG-100 the one mobile-app showcase, promising an
"Instagram-style scroll", and nothing asserted that the scroll works.

MUTATION-TESTED, and the result is the argument. Capping the wall's height with
overflow:hidden leaves every existing number perfect -- overflow 0, neighbour 0,
over-preview 0, 108 controls, 0 too small, 0 clipped, at all five widths -- while
20 of 27 posters become unreachable. Only the new assertion fires:
"scrolling to the bottom does NOT bring the last poster into view (scrolled 0 of
0)". A reader on the showcase would see the top of the wall and never learn the
rest existed, with the whole suite green.

⚠ DO NOT ASK document.documentElement.scrollHeight. MEASURED at 390x844 on
upg-100-mfe: it reports 844 -- exactly one viewport -- while the wall holds
13,032px of cards across two nested scrollers. The composite scrolls an INNER
element, so the document is not the thing that moves, and a document-level check
would call a healthy wall empty. It finds the element that genuinely overflows,
picking the largest so a small inner scroller (the legend's own list) cannot be
mistaken for the wall.

It SCROLLS rather than computing. "The last card is at 13001px" is not the same
claim as "scrolling brings it into view with working controls", and only the
second one is what a reader gets. Having arrived, it also checks that card's
controls are present, unclipped and pressable -- the far end of a long list is
where a layout gives out, and every previous sample was taken near the top.

Side effect worth noting: scrolling to the bottom triggers the lazy previews, so
the 390px run now reports 17 painted rather than 8. The images were always fine;
nothing had ever scrolled far enough to load them.
2026-09-25 00:08:59 +00:00
doxa-maps-88655c70
2f96f04b49 packets: fixtures at the PAGE BOUNDARY, which nothing had ever rendered
Some checks are pending
build-gate / build (push) Waiting to run
check:packets-all reported "every generated packet renders, in every locale
present" and passed on 11 packets. All 11 were the SAME ZONE (laos) in 11
locales. It proved one zone's layout eleven times.

The answer is not more zones -- 28 zones x 11 locales is a network fetch per
zone and ~100MB of artefacts. It is more SHAPES, chosen where the layout can
actually break, built from committed records with no network.

★ THE SHAPE THAT MATTERS IS THE PAGE BOUNDARY. packet-render.js:76 sets
PER_PAGE = 20 and paginates `for (start = 0; start < groups.length; start +=
PER_PAGE)`. laos has 95 groups and tibet 43 -- both mid-range remainder cases,
so the boundary ITSELF had never been rendered. Three fixtures, each a slice of
the committed tibet records, so they are byte-deterministic and need no API:
  exactly 20 -- one full page; an off-by-one yields a second, empty one
  21         -- two pages, the second holding a single card
  1          -- the minimum a zone can have

MUTATION-TESTED, and this is the argument for the whole change. Flipping the
page loop to `start <= groups.length` -- an off-by-one that only shows when the
count is a multiple of PER_PAGE:

  fixture-edge-exactly-20   FAIL   <- the only one of 15
  fixture-edge-21-groups    pass
  fixture-edge-one-group    pass
  fixture-tibet             pass
  laos x 11 locales         pass

A real pagination bug would have shipped completely green under the old suite.
The failure is specific too: "20 people cards should lay out as 4 sheets but the
packet has 5" and "list pages disagree on the zone total: 20 vs 21".

⚠ EACH BUILD OVERWROTE THE LAST, which is why this is a script and not a line of
shell. packet.mjs names its output for the ZONE, not the fixture, so four
fixture builds against one zone leave ONE file -- and check:packets-all would go
on seeing a single shape while reporting success. Each output is renamed to its
fixture's own name so all of them coexist and every one is evaluated.

packet:ci now builds all four. check:packets-all goes from 11 packets in 1 zone
to 15 across 5 shapes; it still reports "5 of 28" and names what it does not
cover.
2026-09-24 23:53:07 +00:00
doxa-maps-88655c70
a3729129b1 poster cards: the fits gate sweeps LOCALES, not just widths
Some checks are pending
build-gate / build (push) Waiting to run
The Driver's requirement was "full words visible AND support localization".
This gate only ever ran in English, which is the shortest of the eleven -- so
the half of the requirement about translation had never been measured.

MEASURED at 1440px, the three print labels:
  en  Prayer List 76px · Card Deck 72px · Poster Map 79px
  ro  Listă de rugăciune 122px · Pachet de cartonașe 133px · Harta afișului 92px
  zh  禱告名單 56px · 禱告卡組 56px · 海报地图 56px

★ THE WORST CASE IS RENDERED WIDTH, NOT STRING LENGTH, and picking by the wrong
one would have picked the wrong locale. Chinese has the FEWEST characters of the
three and is nowhere near the widest, because each glyph is near double-width;
Romanian is +75% on English. The gate asserts scrollWidth > clientWidth, which
is pixels, so the sweep is pixels too. en is the control, ro and zh the measured
extremes; CARD_FIT_LANG overrides.

MUTATION-TESTED, and the result is the whole argument for this change.
Restoring the ellipsis the Driver rejected, at 1440px:
  en   0 clipped   <- passes
  ro  54 clipped   <- fails
  zh   0 clipped   <- passes
A regression that clips half the controls for Romanian readers would have
shipped completely green under the English-only gate. Restore the wrap and all
15 combinations pass.

Failures now name the locale ("ro 768px: 54 of 108 clipped"), because "768px"
alone is a report nobody can reproduce.

The PASS line reports widths x locales rather than their product -- it used to
say "at 15 width(s)" for 5 widths in 3 languages, and a summary that overstates
its own coverage is how a green line stops meaning anything.

Also verified, separately from the sweep: a PARTIAL translation degrades per key.
German renders translated (Gebetsliste, Kartenset, Posterkarte); an unknown
locale falls back to English; both with 0 blank labels, 0 raw i18n keys visible
and 0 clipped.
2026-09-24 23:41:09 +00:00
doxa-maps-88655c70
6f8ff0ebb7 gate: a shared fix is only as deployed as the last rebuild of each consumer
Some checks are pending
build-gate / build (push) Waiting to run
library/ is the single source of truth, but EVERY BUNDLE SHIPS ITS OWN COMPILED
COPY of the components it imports. So a one-line fix in a shared component is
live in whichever bundles happened to be rebuilt afterwards and silently absent
from the rest -- and nothing detected it, because every other gate measures the
RENDERED BEHAVIOUR OF ONE LEAF, and a leaf whose bundle is stale renders the old
behaviour perfectly.

Three independent instances in one week, which is what makes it a gate rather
than a note:
  - the PosterWall hover outline (a7f57c1) reached my-upg-100-list and none of
    the other three consumers;
  - the SemanticTreeLegend legend.a11y keys reached four bundles and NOT
    research-dashboard, so a legend there still ships nameless carets while
    check:a11y-button-names passes 791/791 -- that gate only sees the caret on
    leaves where an expandable branch happens to render. Nobody was working on
    it. It had just been sitting there;
  - commit 3cfe93c6, "Rebuild the fleet so the translated hint actually reaches
    it" -- someone already hit this and fixed it by hand. The remedy was known;
    the detection was not.

★ IT ASSERTS A MARKER STRING, NOT A TIMESTAMP OR A HASH. Comparing mtimes says
"older" and goes noisy on every unrelated rebuild. Comparing hashes cannot work
at all: a bundle legitimately differs from the library source it compiled. The
answerable question is narrower -- does the BUILT bundle contain THIS change? --
and a distinctive literal answers exactly that.

Consumers are DERIVED from the imports, never listed. A hardcoded list goes
stale the moment someone adopts a shared component, and a gate that silently
stops covering a new consumer is worse than none.

A marker that vanishes from SOURCE is reported as a stale ENTRY, not as failing
bundles -- otherwise reverting a change would blame every consumer for it.

MUTATION-TESTED IN BOTH DIRECTIONS, because a gate that has only ever failed is
indistinguishable from a broken one: rebuilding key-cities flipped its line from
✗ to ✓ and the count from 1 to 2 of 4, live; a positive control over a
long-standing marker reaches the PASS branch with all four consumers green; and
pointing an entry at a marker absent from source produces the stale-entry notice
rather than four false failures.

key-cities is rebuilt here because it was the mutation subject and the rebuild
is the fix the gate prescribes. The two bundles still red -- country-posters and
regions-dashboard -- belong to regions, and research-dashboard's legend drift is
older than this work; all three are reported, not rebuilt for them.
2026-09-24 23:22:18 +00:00
doxa-maps-88655c70
a7f57c117d poster cards: a blue outline on hover, and a gate that can see it
Some checks are pending
build-gate / build (push) Waiting to run
The card asks for the Pinterest/Instagram behaviour: hovering a poster outlines
it in blue so a reader knows which one their click is about to act on.

★ THIS IS NOT A RESTORED BORDER. Several reports asked to bring back "the blue
outline around the poster", and MEASURED 2026-09-24 there has never been one:
all 136 elements painting a frame on this wall compute outline:none, and every
border is grey (.card #e3e6ea, .main #dfe3e7). Nothing regressed -- the
affordance was never built. The reports kept recurring because a hover state is
invisible to a screenshot, so each check confirmed "no outline" and concluded
the sighting was mistaken.

outline, NOT border, and that is load-bearing. An outline paints OUTSIDE the
box and takes no space, so it cannot move anything. The rest of this wall's gate
asserts where the print row sits at five widths; a hover that grew a border
would re-lay-out the card and push that row, so a cosmetic state could break a
gate about print controls.

:focus-within carries it to the keyboard, and @media (hover:none) drops the
whole state on touch -- with no pointer it would either never appear or stick
after a tap, leaving a card looking permanently selected.

THE GATE NOW MOVES THE POINTER. Everything it measured before was of a loaded,
untouched page, which is precisely why this affordance could go unbuilt while
being repeatedly requested.

⚠ AND ONE OF MY OWN ASSERTIONS WAS DECORATION UNTIL I MUTATION-TESTED IT. I
first asserted the card's OUTER width is unchanged by hover. It can't change:
the card is a grid item, so the track fixes its width and a border grown on
hover eats inward. MEASURED with border-width:4px on hover -- outer 224->224px,
unfalsifiable, while the CONTENT box went 222->216px. It now asserts clientWidth,
which is what actually repositions the print row.

MUTATION-TESTED: remove the outline -> FAIL at all four pointer widths (exit 1);
grow a border alongside it -> FAIL naming the content-box shift (exit 1);
restore -> PASS (exit 0), reporting "hover 2px rgb(47, 111, 219)" at 768-1920
and "no outline (touch, correct)" at 390.

The print/open controls the card also asks for are already there and were
verified clickable in the same pass: Prayer List, Card Deck, the stock select
and Poster Map, all four with full labels and real handlers, 0 clipped.
2026-09-24 23:17:42 +00:00
doxa-maps-88655c70
31b934bdf5 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 23:01:56 +00:00
doxa-maps-88655c70
f2b667b41c gate: what readers are SERVED must be what we BUILT
Some checks are pending
build-gate / build (push) Waiting to run
Every gate in this repo measures the LOCAL build. Nothing measured the
PUBLISHED one. So the whole board stays green while readers are served a
months-old bundle, and the only detector is a human noticing something looks
wrong.

Running it today: 17 of 22 published bundles are NOT what we built, 3 more
return 404 and are not published at all, and 2 match. my-upg-100-list: readers
get 474KB, we built 663KB.

★ THE EXPENSIVE PART IS NOT THE STALENESS, IT IS THE PHANTOM BUGS. A stale
publish manufactures reports that cannot be reproduced. Three this week --
print buttons truncated to "Pr...", the legend reading "Generation 1/2/3"
instead of the real tab names, the pin hint saying "red dot" instead of
"coloured dot" -- were ALL fixed in main and ALL still visible live. Each one
read as a regression against a card marked done. Two cost most of a tick to
diagnose, and the diagnosis was the same sentence every time.

It compares the sha384 the manifest already carries, not the byte size: two
different builds can coincide in length, and a size check would call that a
match. Size is reported only to make a mismatch legible.

One finding the hash surfaced that a size check would have blurred:
gospel-resources is published 331KB LARGER than we build it. That is not
"behind", it is a different lineage -- published from a tree that has since
dropped code. And doxa-vision-card and golden-path-check DO match, so this is
not "the host is stale", it is a partial publish, which is a more specific
thing to go fix.

MUTATION-TESTED THREE WAYS: a matching bundle passes (exit 0); appending five
bytes to a matching local file fails it (exit 1); an unreachable host SKIPs
rather than failing, because a network gate that goes red on flaky wifi is a
gate people learn to ignore -- and the SKIP says it is not a pass.
2026-09-24 22:56:56 +00:00
doxa-maps-88655c70
054ff6059d Make a gate's dev server die with the gate
Twelve gates here spawn serve-strict and eleven reap it only on the
happy path — srv.kill() at the end of the script. That holds until the
end is not reached. When the box ran low on memory it killed gate
processes mid-run, and every server those gates had spawned was
orphaned: ten in one worktree, four in another. Each orphan holds
memory, which makes the next kill likelier, which orphans more. The
leak feeds the condition that causes it.

Both halves measured rather than argued. A script using the old spawn
was sent SIGTERM and its server was still answering afterwards —
orphan reproduced on demand. The same script using this helper left
nothing behind.

The helper registers the child against SIGTERM, SIGINT, SIGHUP and
normal exit, and unregisters when stopped deliberately, so it is safe
in a finally block. Nothing survives SIGKILL and nothing can, which is
why the port is returned too — a sweep needs something to look for.

This gate adopts it; it still passes, and its negative control still
discriminates. The other ten are other agents' files, so they get the
one-line change offered rather than applied.
2026-09-24 22:54:11 +00:00
doxa-maps-88655c70
96bd068bfc packets: the all-packets gate says how many ZONES it covered
Some checks are pending
build-gate / build (push) Waiting to run
"PACKETS-ALL: PASS — every generated packet renders, in every locale present"
is true and reads like "every packet is fine". It counts FILES. Only the zones
someone generated are ever in that directory.

MEASURED 2026-09-24: 11 packets, all of them the SAME zone (laos) in 11
locales, against 28 poster zones in out/posters. The suite proved one zone's
layout eleven times and had never rendered the other 27 -- any of which can
carry a different data shape: fewer groups, a missing photo, a map that does
not resolve. The packet gate itself is strong; the claim about its reach was
not.

It now prints "zones covered : N of M" with the zone names, and names the
shortfall. Running it today reports 2 of 28.

NOT a failure, deliberately. Generating 28 zones x 11 locales means a network
fetch per zone and ~100MB of artefacts; checking only what exists is a real
trade. The problem was never the cap, it was that the cap was silent -- a
summary line that overclaims is how a green board stops meaning anything. The
PASS text now ends with what it does NOT cover, and points at packet:ci, which
builds a second zone from a committed fixture with no network.
2026-09-24 22:49:23 +00:00
doxa-maps-88655c70
840830ebc4 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 22:41:49 +00:00
doxa-maps-88655c70
0ae551fdf0 gate: assert the UPG-100 map carries our polygons, zones and pins
Some checks are pending
build-gate / build (push) Waiting to run
The Driver reported "the polygons for the map and map colors and pins are not
showing up" and no gate could answer it. verify-map-paint asserts a live GL
context, zero console errors and that Mapbox emitted attribution -- every one
of which is true of a perfectly-painted basemap with none of our data on it.
"The engine booted and tiles are loading" and "my data is on it" are different
properties, and only the first was measured.

The profile already counts this, at the moment it adds the layers, and prints
it: upg-100-list.vue:1849 writes "N polys · R/T zones · P pins" into
.upg-status. Three numbers printed by the product and asserted on by nothing.
This gate reads them.

★ THE TWO OBVIOUS APPROACHES ARE BOTH WRONG, and I shipped one before measuring.
Counting DOM .mapboxgl-marker elements returns 0 on a HEALTHY map -- mapbox
draws these pins as GL geometry inside the canvas, so there is no element to
count; I carried "markers: 0" across three probes as proof of breakage, and it
was proof of a wrong selector. Sampling the canvas returns an empty buffer,
because a WebGL drawing buffer is cleared after compositing: 0 of 13362 sampled
pixels had any alpha, on a map visibly full of colour. The product's own count
is authoritative in a way a proxy is not.

It asserts the zone RATIO, not just presence. "27/27 zones" and "1/27 zones"
both contain polygons and only one of them is the product, so the threshold is
what separates "some data arrived" from "the data arrived". Floors rather than
exact counts, well below the real values, so a data refresh does not fail the
gate and teach people to edit the expectation instead of reading it.

MUTATION-TESTED BOTH WAYS: raise the three thresholds above the real values and
each assertion fires independently with its own message (exit 1); restore them
and it passes (exit 0). Blocking the boundary geojson also fails it, via the
unparsed-status branch.

MEASURED, local AND live, both leaves: 195 polys · 27/27 zones · 2107 pins.
The Driver's symptom does not reproduce in that form on either tree.
2026-09-24 22:39:12 +00:00
doxa-maps-88655c70
d5fff78ccd upg-100: ask the registry for the stylesheet URL, and let the gate hear mapbox
Some checks are pending
build-gate / build (push) Waiting to run
A map inside a SHADOW ROOT has to inject its own copy of mapbox-gl.css, because
a document stylesheet does not cross a shadow boundary. This profile did that
by writing the URL out a second time, and the second copy drifted: it pinned
v3.3.0 while every production loader -- generate-tree's baked <script>,
useMapInstance, generate-gallery -- had moved to v3.24.0. So the map ran new JS
against old CSS.

MEASURED on the leaf: mapboxgl.version 3.24.0, the only mapbox stylesheet
reaching the shadow root v3.3.0, and mapbox-gl emitting its own "missing CSS
declarations" warning. The file's own header already says the engine loader is
imported rather than re-declared "because a second copy of one fact drifts".
The stylesheet URL was that second copy. It now comes from the same registry
via engineStylesheetUrl().

★ THE GATE COULD NOT SEE ANY OF IT, which is the more useful half of this
commit. verify-map-paint asserts a live GL context, zero console ERRORS, and
that mapbox emitted attribution -- all true of a map whose stylesheet never
applied, since the basemap paints fine while the chrome around it does not.
Mapbox itself says so, but as a WARNING, and the listener filtered to
type()==='error'. It now listens for _detectMissingCSS's verdict and fails on
it.

MUTATION-TESTED BOTH WAYS on the same leaf: restore v3.3.0 -> FAIL, exit 1;
restore the registry lookup -> PASS, exit 0.

Two things I had wrong on the way, recorded so the next reader does not repeat
them. I first asserted position:absolute on .mapboxgl-canvas-container -- but
v3.24.0 declares only cursor and touch-action there, so static is its CORRECT
value and that assertion would have failed every healthy map. I also read
"0 .mapboxgl-marker elements" as missing pins; mapbox draws pins as GL geometry
in the canvas, so zero is expected. A screenshot showed the polygons, the
region colours and hundreds of pins all rendering. Both were properties I
assumed rather than verified, and the canary's background-color is the one
mapbox actually defines -- it is the test _detectMissingCSS uses.

The canary alone would still have been a false green here: that rule is
identical in v3.3.0 and v3.24.0, so it is salmon under both. It catches an
ABSENT stylesheet; the warning catches a MISMATCHED one. The gate asserts both.
2026-09-24 22:27:52 +00:00
doxa-maps-88655c70
cc3ab8b822 Check the shipped answer, not the code that wrote it
The same drift has now appeared six times, each spelled differently:
negated fields on a line naming "none", a positive hasAny over a
mapped row, a predicate inside a build script, and — found this tick —
a pair of columns no pass had ever touched. A source-level pattern has
to guess how the next one will be written.

Clause N does not guess. It compares the shipped ANSWER against the
pin tier, so it catches the seventh however it is phrased. Measured
when written: people-group-resources.any_resource_found disagreed with
the tier on 194 rows, and its no_resource_found pair contradicted
ITSELF on 7 more, because neither column was in the refresh at all.
Both now move together, 195 and 202 corrected, and the table agrees
with the tier on all 2,103 judgeable rows.

A code with no pin is SKIPPED, not guessed. The tier lives on pins, so
an unpinned code cannot be judged this way — judging it anyway
reported ajp as a defect on the first run, when the row was right and
the check was wrong. That is the failure this repo keeps meeting, so
it is written into the clause rather than left as a comment.

Also corrected there: the table's own note still told readers GRN
answers 403 to every request and had never been searched.
2026-09-24 22:24:24 +00:00
doxa-maps-88655c70
337a1a863b Tell the curated tables that GRN and Scripture Earth exist
The legacy reconciler reported "nothing to change" and was confidently
wrong: its hasAny predicate never learned the two newest pools, so 122
curated rows — 8.9 million people — still carried heart_has_any=false
for groups the pin index can now reach. A reconciler that cannot see a
source is not reconciling, it is confirming.

Corrected exactly the 122 predicted, and the derived columns moved
with them: status 122, opportunity 93, lbc_carries_resource 69,
aligned 59, gap 29. Gap moves least because it means "nothing in the
heart language AND no in-country bridge either" — most of these have a
bridge, so they were already opportunities rather than gaps, and that
documented meaning is intact.

This was the fifth occurrence of one bug, and the gate written for it
had never looked at scripts — only .vue. Extending it to the build
scripts immediately found a sixth, in the Deaf gap table, which is
latent today because no sign language carries either field: GRN is
deliberately skipped for them and Scripture Earth has none. Fixed
while it is still cheap.

The rule now differs by file because the truth does: a surface must
READ the tier, while a build script COMPUTES it and cannot, so a
script must instead name every pool. Both halves verified against the
real pre-fix lines rather than synthetic ones.
2026-09-24 22:15:53 +00:00
doxa-maps-88655c70
90f0c37ddf Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 22:12:10 +00:00
doxa-maps-88655c70
de3b34beb5 Stop the country sheet calling 729 reached groups unreached
The country sheet split a country's groups into "with resources" and
"no resource found" using its own hand-listed field set, which never
learned about Scripture Earth or GRN. Measured: 729 people groups —
77.9 million people — were filed under "no resource found" while this
same map's pin sheet offered them a working link. Chad alone had 107
groups with something and showed fewer.

It now reads the tier the pin index computed, carried onto the mapped
row. Chad reports 150 groups, 107 with a resource, 43 without, which
is what the tier says.

This is the FOURTH time this bundle has re-derived that decision
somewhere, and the gate written for it missed this one: the gate
looked for negated c.* fields on a line naming "none", and this was a
positive predicate named hasAny over the mapped row.

Widening it took three attempts and the first two were worse than the
bug. "Three OR-ed resource fields" flags canHear(), which ORs media
precisely because it must exclude text. "Spans text and media" flags
`!!c.s || !!(c.au && /text/…)`, because c.au is text or audio
depending on a qualifier and a `|| ''` default inflates the operator
count. Both were confident false findings — the failure this repo has
now met in a forced token, a swallowed flag and a class extractor, and
a gate that cries wolf gets switched off.

So it asks one question it can actually answer: a predicate NAMED for
"has anything" is deciding the tier by definition and must read it.
Verified against the real pre-fix line, not a synthetic one.
2026-09-24 22:10:03 +00:00
doxa-maps-88655c70
66519288dc README: UPG-100 is the mobile app, shot at its owner's "ready at 88a566f"
Some checks are pending
build-gate / build (push) Waiting to run
Deployment method 2 now shows the UPG-100 prayer lists and posters at phone
size: one poster per row, print options wrapping in full. Shot from the
rebuilt bundle at 88a566f, not from source. The capability-4 shots are
retaken from the same build, so they show the print-option fix too.
2026-09-24 22:03:50 +00:00
doxa-maps-88655c70
608ef3dd56 poster cards: print options wrap to full words instead of an ellipsis
The Driver rejected the ellipsis by name: the print options must LINE WRAP so
every label stays readable, because a button that no longer says what it does
has not been fixed.

Three measurements drove the CSS:

  - a <fieldset> takes min-width:min-content from the UA stylesheet, which
    OVERRIDES width and max-width. .pfs read width:82% while computing 431px
    inside a 212px card. min-width:0 goes on the fieldset ITSELF, not only on
    its children.
  - the row is anchored at its TOP (top:100%;margin-top:-14px), not its bottom.
    At bottom:-14px every extra line grew UPWARD over the preview image, which
    is how the first overflow fix broke the card it was meant to repair.
  - wrapping took the row from 35px to 78px, so the grid's row gap moves 64px
    to 88px. The old gap was tuned when the row was a single line.

No text-overflow anywhere in .pfs. min-width:0 alone had let three 76-79px
labels share 172px, showing 40px each.

The gate gains the assertion that would have caught the ellipsis: a control is
clipped when scrollWidth exceeds clientWidth. Mutation-tested -- restoring
text-overflow:ellipsis reproduces 81 of 108 clipped and exits 1. Without that
assertion the gate passed a wall whose buttons all read "Pra...".

Measured at 390/768/1024/1440/1920px: 0 clipped, 0 overflow, 0 over-preview,
0 neighbour intrusions, 108 controls.
2026-09-24 21:59:34 +00:00
doxa-maps-88655c70
bf7fc46dcd check:shots compares bundle content, not dates; retake the stale shots
Some checks are pending
build-gate / build (push) Waiting to run
A retake that came out byte-identical left the PNG unchanged, git dated it by
its old commit, and the gate called a correct, just-taken picture stale. No
clock can see that case. readme-shots now writes shot-bundles.json (shot ->
sha256 of the bundle it was taken from), and check:shots compares that hash
with the bundle built now; dates are only the fallback for unrecorded shots.

Negative-tested: one byte appended to the doxa-simple-map bundle fails exactly
the three cap2 shots; restoring it passes.

Also retook the eight shots a rebuild had made stale since e2beec9.
2026-09-24 21:54:08 +00:00
doxa-maps-88655c70
e2beec9a11 README: reshoot the Plugin Studio at f63d75e — the Explorer-tree rail
Some checks are pending
build-gate / build (push) Waiting to run
readme-shots now clicks the rail's first plugin row (.st-row[role=treeitem],
not a section heading), keeping .vtl-tab as a fallback. Without that, the walk
fell through to the nested gallery's own tabs.
2026-09-24 21:42:37 +00:00
doxa-maps-88655c70
f63d75ed80 Plugin studio: an Explorer tree for the rail, imitating VS Code
Some checks are pending
build-gate / build (push) Waiting to run
The studio's rail was hand-rolled vertical tabs stretched into a tree.
It is now an Explorer tree modelled on the VS Code Explorer, keyboarded
per the WAI-ARIA tree pattern:

- CAP 5 -> CAP 1 sections with sticky headers; one-line rows with the
  name and a muted element tag; twisty on the left; indent guides.
- Composites fold; their parts hang beneath, one level at a time, each
  part opening the plugin's one canonical entry.
- A filter box on top shows matches together with the path to them.
- One view action, Collapse all.
- Auto-reveal: opening a plugin by click, URL hash or remembered tab
  scrolls its row into view and opens its section.
- Arrow keys walk the tree, Right/Left open, close and move between
  parent and child, Enter opens, a letter jumps to the next name.
- The slim rail becomes an activity bar with one icon per section.
- Fold state is remembered per page.

The tree lives in plugin-designs/plugin-studio/src/StudioTree.vue and
fills VerticalTabsLayout's new #rail slot, so the layout still owns the
collapse toggle, mobile drawer and slim strip. The alias, depth and
branch row fields added to the layout earlier today are removed again:
nothing else used them.

The studio host now has a default height (85vh, min 480px), as
doxa-maps-spa does. Pasting the published embed snippet into a plain
page mounted a complete studio 0px tall. A page's own height or
--plugin-studio-height still wins.

The studio gates that click tabs by text now also match tree rows.
Rebuilt the plugin-studio bundle and re-baked its SRI.
2026-09-24 21:40:48 +00:00
doxa-maps-88655c70
53308c546c Delegation diagram as rings of circles; stop showing Simple Research as the mobile app
Some checks are pending
build-gate / build (push) Waiting to run
- The diagram is redrawn as concentric rings of uniform circles, not filled
  bands: the Plugin Studio ring labels itself, the seven plugins sit on the
  plugin ring, one agent per plugin sits outside it joined by a line and an
  arrow ("each plugin has one agent responsible for building it"), and one
  shepherd sits outside the agent ring. The race framing is gone. SVG source
  updated with the PNG.
- Deployment method 2: Simple Research was never built for phones, so it is
  no longer the mobile example. UPG-100 is; its screenshot lands when its
  phone layout is finished. The shot is removed from readme-shots, with the
  reason and the planned replacement in a comment.
- Retook nine screenshots that the latest bundle rebuilds had made stale.
2026-09-24 21:08:52 +00:00
doxa-maps-88655c70
25e3edec8f upg-100: no mapbox stylesheet under maplibre, and assert the preview exists
Some checks are pending
build-gate / build (push) Waiting to run
Two things, both found by other agents' reports rather than by my own gates.

rega found upg-100-list.vue:1464 injecting mapbox-gl.css v3.3.0 into the shadow
root unconditionally, so a keyless maplibre embed still pulled a stylesheet from
api.mapbox.com. Token-free and harmless to render, but wrong-engine leakage on a
page whose point is having no Mapbox dependency. Gated on isMaplibre, which
already existed at line 119.

This commit is ATOMIC by design: the fix, the rebuilt bundle, and the deletion
of rega's MAPBOX_KNOWN exemption land together. Either half alone leaves a
window — delete first and main red-gates on its own known bug; fix first and a
stale exemption sits there hiding the next one.

Proven before deleting, because the exemption is keyed on a URL STRING while the
defect is a network REQUEST: the built bundle still contains the v3.3.0 literal
(the constant remains; the guard stops the fetch), so a source-grep gate would
have been misled. rega's gate measures requests, and it reported
"provider=maplibre made 0 request(s) to api.mapbox.com" in BOTH js and iframe
modes with no KNOWN line printed at all — i.e. the exemption was already dead
code. Re-proven after deletion: still 0 requests, still PASS.

The leaf-head preloads are explicitly NOT covered and the reason is recorded in
the gate: mapbox-gl.css v3.24.0 and mapbox-gl-geocoder v5.0.0 are a different
version and a different owner (generate-tree, bundler-1's lane), already tracked
separately.

Second: the Driver's report had two halves — "it is supposed to show the
previews" and the print options overlapping. I fixed and gated the overlap and
left the first unasserted. The over-preview check was written as `if (img)`,
which means a card with NO preview satisfies it perfectly: nothing can overlap
an image that is not there.

The gate now counts previews. Mutation-tested by aborting every previews/**
request: the product degrades well — all 27 cards swap to a "poster not
generated yet" placeholder with no broken-image icons and no <img> at all, which
is precisely why an image-health check cannot see it. So the fallback itself is
asserted, and all-cards-placeholder now fails.

A first version of that assertion was wrong and the reason is recorded in the
code: it called 18 lazy-loaded previews broken. Every img carries loading=lazy,
the first unpainted one sat 3926px down an 844px viewport, and no HTTP response
failed. Deferred is not broken; it now judges only in-viewport images.
2026-09-24 20:28:45 +00:00
doxa-maps-88655c70
c276a2158e Gate the embed contract: a composite must pass tk and provider down
Four elements shipped with this broken and nothing could see it, because every
existing gate loads a composite on its OWN generated leaf — and that leaf bakes a
token and assigns window.MAP_TOKEN, supplying the very thing under test. The
failure exists only where there is no leaf: the partner page the published embed
snippet is written for.

The gate serves the built tree on one free port and a partner page on another, so
CORS is exercised rather than assumed, and asserts per composite:

  tk        given only {"tk": <the token this tree bakes>}, a canvas appears and
            api.mapbox.com actually serves it
  provider  given only {"provider":"maplibre"} and NO token, a canvas appears and
            there are ZERO requests to api.mapbox.com

Zero is the assertion, not "it rendered": a build that quietly fell back to
mapbox with a baked key renders perfectly and is exactly the regression this
exists to catch.

★ IT FAILS ONLY WHERE THE EVIDENCE POINTS AT THE KNOB. The first draft failed
NINE composites for "no map canvas under a minimal config" — an accusation it had
not measured, since several of those elements simply need a `profile` key to
choose which profile to show. It now distinguishes: a token WAS supplied and the
element still says there is none, or maplibre was asked for and mapbox tiles
still loaded — those are findings. No canvas and no complaint is UNMEASURED, and
says so with the reason. A gate that red-lights other people's plugins over a
claim it cannot support is worse than no gate.

COST, because a slow gate stops being run: a flat 14s-per-case wait became a poll
for the canvas, and the provider case short-circuits when that element's tk case
drew no map — there is nothing to compare against, so a second page load was pure
waste. 5m38 -> 3m15.

GREEN on the landed tree: dashboard-plus-grid, research-map-plus-grid, stack-demo
and plugin-studio all pass both knobs, every provider case at ZERO api.mapbox.com
requests. plugin-studio only reached zero once the cap-5 shell started setting
window.MAP_PROVIDER — profile-config alone reaches the child a composite writes
config for, while the global rung reaches a map nested at any depth, which the
studio-inside-SPA-inside-composite chain needed.

gates:post is 44 steps; the quoted CLI doc is updated to match.
2026-09-24 20:26:33 +00:00
doxa-maps-88655c70
dd2443b7de Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 19:57:53 +00:00
doxa-maps-88655c70
3a4f065ec5 Research Gallery: a host that asks for maplibre gets maplibre in every tab
The shell forwarded tk, tokenUrl, locale and treeBase to each nested map,
but not the engine. So <doxa-maps-spa provider="maplibre"> still loaded
mapbox-gl and Mapbox styles. On a keyless host, that is the embed failing.

The shell now forwards `provider` in each child's profile-config, validated
to the two known engines. That fixed the simple map tabs, but not the
composites: they mount their inner maps without passing the provider on.
Measured with one tab per clean page: Big Picture made 19 api.mapbox.com
requests, Regions 17, UPG-100 15, Simple Research 14. So when a host asks
for an engine, the shell also sets window.MAP_PROVIDER (only if the page
hasn't). That is rung 3 of every map's own ladder, so it reaches maps
nested at any depth. After it, all four composites make 0 Mapbox requests.

check:gallery-embed:maplibre runs the cross-origin gate with a keyless
provider and fails on any api.mapbox.com request, attributed to the tab
that was open. Before this change, js mode failed with 20 URLs. It has one
named, printed exemption: upg-100-list.vue pins a token-free mapbox-gl.css.
That belongs to the upg100 plugin and is routed there.
2026-09-24 19:57:23 +00:00
doxa-maps-88655c70
a5a3c6b9d1 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 19:56:32 +00:00
doxa-maps-88655c70
d1fc658de4 README: screenshots first — one per capability, three deployments shown live, the studio last
Some checks are pending
build-gate / build (push) Waiting to run
- The five capabilities each lead with a current screenshot; the Research
  Gallery's composition tree folds into a <details> block.
- Deployment: (1) the prayer map in a host page's iframe, (2) the Simple
  Research dashboard as the phone app, (3) the whole Research Gallery
  JS-embedded on dx-life's research page. WordPress/Disciple Tools and Tauri
  are stated as proven proofs of concept, not production-ready.
- The Plugin Studio closes the README with its current screenshot, the
  four-command start and the command table.
- 4,151 -> 1,330 words. Every image retaken against the current build.
- readme-shots: deploy2 shoots research-map-plus-grid at phone size; deploy3
  shoots a running dx-life (DXLIFE_URL=...), skipped when it is not set;
  an http(s) path is used as-is. Retired four images nothing shows any more.
2026-09-24 19:55:24 +00:00
doxa-maps-88655c70
65e959a08f Say which pools were really searched for THIS language
The sheet's "checked" line is what tells a reader whether "nothing
found" means anything, and it was wrong in both directions. 5,131
codes omitted Scripture Earth, which had in fact been swept for every
one of them. And 110 codes named GRN as checked when the sweep has
never looked at them — GRN is crawled a batch at a time at the delay
their robots.txt asks for, so 2,100 codes are still unread.

The cause was ordering, the same lesson this file already carries for
the dead-link drop: the pass that normalises provenance ran before the
Scripture Earth and GRN fetchers, so the codes those fetchers create
kept the raw constant. A tidying pass that runs before the thing it
tidies reports a confident answer about a room still filling up. It
now runs after every source attaches, and the count is exact — 282
codes name GRN, 282 have been decided, and none that was decided goes
unnamed.

Scripture Earth is claimed unconditionally because that sweep really
did cover every code this map uses: a code absent from their index is
a checked negative. GRN is claimed per code, and the rest carry
"grn (not yet swept)" in the unchecked list instead.

Clause M of the coverage gate now fails if any code names a pool the
sweep never decided for it. Poison is 13/13.
2026-09-24 19:55:12 +00:00
doxa-maps-88655c70
b026c3ef83 poster cards: the print row belongs below the preview, and a fieldset ignores width
Some checks are pending
build-gate / build (push) Waiting to run
The Driver caught my own fix breaking the poster section, in the same session I
landed it. flex-wrap wrapped the print row onto three lines; the row is anchored
at the card BOTTOM, so growing it 35px to 78px pushed its TOP up from 216px to
173px in a 238px card — straight over the preview image and the outline. I
traded a horizontal overflow for a vertical one.

My gate measured only left and right edges, so it reported PASS on a visibly
broken card. Same instruments-lie family the flock banked today: a green that
measures the wrong thing.

The real cause of the ORIGINAL overflow, which both the old CSS and my first fix
missed: a <fieldset> gets min-width:min-content from the UA stylesheet, and that
OVERRIDES width and max-width. The rule said width:82% while the element
computed 431px inside a 212px card. That is why "it has a width, looks fine"
survived two rounds of this bug — the declaration was there and inert.

Fixed with min-width:0 on the fieldset itself, flex-wrap:nowrap, and flex:1 1 0
on the stock <select>, which sizes to its widest option and was taking 188px of
a 198px row.

Measured through all three states: 431x35 at top 216 overflowing, then 78px tall
at top 173 covering the preview, now 172x35 at top 216 — one line, below the
184x184 preview, inside the 212px card. Clean at 390/768/1024/1440/1920px with
all 108 controls full size.

The gate now measures the vertical axis and sweeps five widths. Mutation-tested
against my own regression: restoring flex-wrap fails with "27 print rows COVER
the preview image (37px)" at four of the five widths. 390px stays clean, which
is exactly why single-width testing would have missed it a second time.
2026-09-24 19:54:43 +00:00
doxa-maps-88655c70
53498c392b Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed 2026-09-24 19:24:32 +00:00
doxa-maps-88655c70
67de5b11fd check:gallery-embed: let the mode flag reach the gate, and count tabs per mode
The package.json entry ended in an inline # comment that named --mode and
--only. So `bun run check:gallery-embed --mode iframe` put the flag inside
the comment, the shell dropped it, and the gate ran its default. That made
check:script-flags red on main.

The mode now has its own entries, check:gallery-embed:js and
check:gallery-embed:iframe, and the base comment names no flags.

The default run always covered both modes (13 tabs each, recorded in the
original run). Only a caller-supplied flag was lost. So nothing can pass as
"both modes" again without running them, the summary now prints the tab
count per mode, and a requested mode that drove zero tabs is a FAIL.
Negative-tested: a filter that matches no tab exits 1.
2026-09-24 19:24:26 +00:00
doxa-maps-88655c70
7ae1572c94 poster cards: the print-options row stays inside its own card
Some checks are pending
build-gate / build (push) Waiting to run
The repo-hygiene agent found this in a fresh 1440px screenshot, which is the
whole lesson: .pfs already had width:82%, so reading the CSS said it was fine.
Flex items default to min-width:auto and refuse to shrink below their content
width, so four controls — Prayer List, Card Deck, the stock select, Poster Map —
pushed the row open regardless of what the container declared. A constrained box
does not constrain flex children, and only geometry shows that.

Measured at 1440px on cap4-upg100-mfe: 27 of 27 cards overflowing, worst 110px
past a 212px card, 36 intrusions into a neighbouring card's box. After
flex-wrap:wrap plus min-width:0 on the children: 0 and 0, with all 108 print
controls still a usable size.

That second number is asserted deliberately. "Nothing overflows" is trivially
satisfied by a row whose buttons collapsed to zero width, which would be a worse
product than the overflow it replaced. check:poster-card-fits holds both, and is
mutation-tested: reverting the fix reproduces the reporter's exact figures.

Also adds check:backlog-refs — a card may not depend on a card that does not
exist, because a dangling depends_on can never be done, so the card can never
become ready and nothing says so.

Its first run disproved the finding that motivated it. I had censused three
dangling references, including the PRINCIPLE card the loop prompt lists as open
work. The gate came back PASS, and chasing the discrepancy rather than assuming
the gate was broken showed my census was wrong: it read only the builder
backlog, and all three cards exist in 01-ideation-research. Cross-phase
references are legitimate, and a single-backlog census structurally cannot see
them. Reporting three dangling refs would have sent someone hunting for cards
that were never lost.

It fails only for depends_on on an OPEN card; a dangling parent is reported and
passes, because failing on filing debt teaches people to delete the reference
rather than restore the card. Both severities mutation-tested.
2026-09-24 19:19:02 +00:00
doxa-maps-88655c70
f0c46022ee Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 19:15:28 +00:00
doxa-maps-88655c70
a5ab04ec75 Measure the basemap, in a real embed, on the token the page ships
"The map renders" had never been measured. Every browser gate here
forces a Mapbox token onto the page from a shared file; that token is
restricted and 403s on the basemap, while the leaf bakes a different
one that works. The gates then counted PINS — which are drawn from our
own GeoJSON and paint with no token at all — so they passed on a
basemap the probe itself had broken.

This gate forces nothing. It serves the leaf on one port and a host
page on another so the iframe is genuinely cross-origin, loads it as a
stranger's browser would, and takes evidence two ways: Mapbox responses
counted by status, and colour variety in a slice of the canvas. The
gospel map passes — 9 tile responses, none denied, and the screenshot
shows coastlines, ocean shading and country labels.

The negative control runs every time, and the check reports PROVED
NOTHING rather than a pass if the control succeeds. That is not
decoration: the first two attempts at the control did nothing at all.
A page-level init script never reaches a cross-origin iframe, and even
at context level it was ignored, because this leaf injects `tk`
straight into the element and only falls back to window.MAP_TOKEN —
so it is immune to the forced-token hazard by construction. The
control had to move to the wire. With the token rewritten there, 6 of
8 tile requests are denied and the basemap vanishes, leaving only the
pins and choropleth: the picture that shows exactly why a pin count
was never evidence of a map.
2026-09-24 19:13:46 +00:00
doxa-maps-88655c70
6ab560e42f Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 19:06:54 +00:00
doxa-maps-88655c70
3a2cf0e8f2 Research Gallery: embed on someone else's page, in both modes
The gallery is going onto the DX-life research page, and nothing had tried
it on a page it didn't generate. From a second origin, a bare
<script> + <doxa-maps-spa> drew over the host's header, and 8 of 13 tabs
showed an error card.

Three causes, each fixed in the shell:

- No :host box. The layout root is position:absolute, so it anchored to the
  viewport instead of the element. :host is now a positioned block with a
  default height the host can override.
- A foreign page sets none of the globals a leaf page bakes. Every data call
  hit the host's own origin and got HTML back. The manifest now publishes a
  `runtime` block (apiBaseUrl + the public token every leaf already carries).
  The shell fills window.MAP_APP_API_URL / window.MAP_TOKEN only when the page
  hasn't set them, and forwards tk in profile-config.
- Nested elements resolved assets to <tree>/../assets, a sibling pool the
  publisher doesn't ship (404 live). The shell now forwards an absolute
  <tree>/assets. This also fixes the iframe mode, where Cities with Internet
  and Gospel Resources were failing the same way.

Also adds a `tab` attribute (plus ?tab=) to deep-link into one map, and a
`tab-change` event for the host.

check:gallery-embed drives every tab in both modes from a second origin. It
passes here and fails on the previous tree (7 findings).

EMBED.md documents the contract. The Traefik template now sends CORS for the
whole tree: live manifest.json answers without Access-Control-Allow-Origin.
2026-09-24 19:03:37 +00:00
doxa-maps-88655c70
b089f9effb Reach Global Recordings Network, and lift 147 more groups
GRN records the gospel as AUDIO for languages with no writing, which is
most of what is left in the gap — and every earlier attempt recorded it
as unreachable. globalrecordings.net sits behind a proof-of-work bot
shield that challenges even robots.txt; that is an access control and
is not worked around. Their own delivery site, 5fish.org, is not
shielded, allows what this reads, and resolves by ISO 639-3 directly.

First 180 gap codes swept: 74 hold gospel audio, a 41% hit rate, and
147 people groups that had nothing now have the gospel in a form they
can receive. Tier 0 falls 2,873 to 2,746.

The host NEVER 404s. A nonsense path returns HTTP 200 and a plausible
geo-located page, so reading 200 as existence would have marked every
code on earth as covered. Existence is read from the page title, and a
negative and a positive control run in band on every run — the fetcher
aborts if a nonsense code ever reads as a real language. Their
robots.txt asks for a ten-second crawl delay, so the sweep is
sequential, capped per run and resumable. Do not add concurrency.

Three sign languages are deliberately left out. GRN's catalogue is
audio; 5fish holding a page titled "Pakistan Sign Language" says they
index the language, not that they have signed video. Attaching it told
20 Deaf groups — 11.5 million people — that gospel audio was available
to them. Unverified, and for that audience useless.

Six places across three surfaces still told the reader GRN answers 403
to everything and had never been searched. That is now false, and a
stale caveat misleads exactly as much as a stale number.

Also here, because adding a source is what exposed it: the charts
profile decided "this group has nothing" a second time, by listing the
resource fields by hand. That list went stale every time a source was
added — 7 languages once, 23 when Scripture Earth landed — so the chart
drew groups as having nothing while the map offered them a working
link. It now reads the tier the pin index already computed, and a new
gate refuses any surface that re-derives it. The gate was checked
against the real pre-fix file, not only a synthetic one.
2026-09-24 18:55:28 +00:00
doxa-maps-88655c70
f9146d5105 Plugin studio: every plugin by capability, and a composition tree per composite
Some checks are pending
build-gate / build (push) Waiting to run
The rail now has two labelled parts. "Every plugin, by capability" keeps
one node per plugin in collapsible CAP 5 -> CAP 1 sections. "What each
composite is made of" adds one collapsible section per composite, headed
by the composite, with every element nested into it drawn as an indented
tree at full depth, ordered by capability at each level. A child repeats
in every composite that nests it, as specified.

Repeats are aliases, not new nodes: clicking people-groups-grid under
big-picture selects the same canonical entry as clicking it in CAP 2 and
highlights every copy, so each plugin still has exactly one name. Arrow
keys walk only the canonical rows. A declared child that is published
nowhere (big-picture's bp-research-header) shows as a "part" row, so the
header/grid split inside Big Picture can be named.

The dev server's manifest never carried `composes` edges, so under
`bun x vite` the Composed-of panel was empty for every composite. It now
publishes them from the same meta.json source as the built manifest.

Also: bare sibling profile refs (gospel-resources composes "data-tables")
now resolve, and rail rows use local SVG icons per capability instead of
emoji that rendered as empty boxes without an emoji font.

VerticalTabsLayout gains two optional row fields (`selects`, `depth`) and
shows badge/count on folder headings; existing consumers set neither.
2026-09-24 18:49:20 +00:00
doxa-maps-88655c70
42c2279ee3 UPG Neighbor: build the five neighbor maps from source, not from a frozen IIFE in an iframe
Send Map, the Bloom cockpit, Flower Mesh, Lang Cluster and Flowers ran in
the Experimental shelf as a 976 KB prebuilt legacy bundle inside a sandboxed
iframe. Their original source survived in the old app-profiles/doxa-upg-neighbor
bundle, and it only imports library pieces that still exist, so it moves here
as-is: the unified hub becomes upg-neighbor.vue and the lenses go in src/.

The hub now picks its opening lens at setup rather than on mount. Switching
on mount tore Send Map down mid-initialisation and threw on every other lens.
The five instance ids stay the same, so existing embed URLs still work, and
the old legacyElement key still maps to a lens.

The legacy sandbox (data/legacy/upg-neighbor and its staging rows) is gone
because nothing loads it any more.
2026-09-24 18:47:14 +00:00