Commit graph

537 commits

Author SHA1 Message Date
Doxa Maps
dffae0502c A clicked pin finds the groups that share its point
rega, row 46: on a point four groups share, the note never said so. A click hands the legend the RENDERED
feature, whose geometry the engine rebuilt from tile coordinates (about 0.001 degree off at low zoom), and the
co-location index matched on the exact coordinate. The pin is now resolved to its roster row by name, and to
the nearest of those rows when a name repeats across countries; only a name the roster does not know falls
back to the pin's own coordinate. check:approx-pin-note adds both cases; the tile-rounded one fails on the old
code.
2026-09-26 22:06:58 +00:00
Doxa Maps
a94a85cb8c C4 wave 3a, gospel-resources: four guards share one, English unchanged
From regions-plugin e04121f1 re-cut as 7138109f on main 4d18a6f3. charts, data-tables and resources-list call
useOptionalT('gospel', { fallback: enLookup }); resources-map calls useOptionalT('') because its keys span two
areas (gospel.* and pins.*) and its enLookup walks the catalogue root, so a namespace would have turned
pins.approximate into gospel.pins.approximate. Each keeps the English fallback it already had.

useOptionalT's fillParams substitutes only non-null values (the rule main's own fill() already used), so
{ count: null } leaves the placeholder visible instead of printing "null". check:optional-t-equivalence
(59 assertions) asserts the empty namespace for resources-map and strips HTML comments before counting its
bare useI18n() calls.
2026-09-26 22:06:58 +00:00
Doxa Maps
09aba6dc92 C4 wave 2: five library components share the guard, and keep their own English
The five are NOT one shape, and treating them as one would have changed what
readers see. Measured before touching them:

  ApproxPinNote, FilterDropdown   fall back to a literal map
  SemanticTreeLegend, PosterWall  fall back to humanizeKey() — 'legend.columns.name' renders 'Name'
  PinHint                         falls back to a one-key map
  SemanticTreeLegend, PinHint     also capture i18n.te (used 3x and 1x)
  PosterWall                      also captures i18n.locale

So useOptionalT grew an optional `fallback` hook, and useOptionalI18n was added
for the callers that need te and locale. Every site passes the fallback it
already had, so not one rendered string changes. Routing them through the
catalogue instead would have altered what seven bare-host bundles print, and —
because tStatic honours the runtime locale — would have started translating
strings that are English today. That may be an improvement; it is not this
commit's to make. Changing a default is not a migration.

te keeps its existing contract too: FALSE on a bare host, never "true because the
catalogue has it". PinHint and SemanticTreeLegend both record why — te is how
their callers know to use their own defaults.

THE CHECK NOW LIVES ON MAIN, which is the point of this commit as much as the
migration is. dx-shepherd: the 18-assertion version was in _qa-cycle, so it would
never have run again. internal/check-optional-t-equivalence.mjs is registered as
check:optional-t-equivalence — script line, //scripts note and gates:post token,
all inserted by hand rather than by re-serialising package.json. It carries 46
assertions: the helper's contract across all three host states, a branch-for-
branch equivalence against the old hand-written code, and a PER-SITE REGISTER
that every later C4 wave appends a row to. Negative-tested from outside: strip one
site's own fallback and it fails naming that site.

check:library-index went red on the new composable and is green again after
`bun run library-index`. That is the staleness gate working, not a flake.
2026-09-26 22:06:57 +00:00
Doxa Maps
2449098a68 The legend mirrors in Arabic: logical insets instead of left/right
The shared legend was placed with physical left/right, while the map controls use inset-inline-end. In
Arabic the controls mirrored to the left, onto a legend that stayed left: on Simple Research, Prayer Progress
and UPG-100 (rega's RTL survey at 52eeb0db; traced by upg100-plugin and repo).

SemanticTreeLegend: the panel, its reopen button, the tree-guide hairlines and the export menu use
inset-inline-start/end; the closed panel slides off to the right and its collapse arrow flips under
:dir(rtl) (additive overrides, as VerticalTabsLayout does). LegendDesktop, LegendMobile, LegendRows,
LegendTools and LegendProgressBar: the card, reopen, detail close/collapse buttons, the name gutters that
clear them, indents and spacing are logical; LegendTools re-centres under RTL. LTR renders the same.

check:legend-logical-insets (with :self-test, both in gates:post) fails on any physical left/right in the
legend components unless it is symmetric, an auto reset, or allowed with a reason.
2026-09-26 22:06:57 +00:00
Doxa Maps
29f4ab7097 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 22:04:34 +00:00
Doxa Maps
175ba52cf3 Merge main (studio landing 4) into packet/vertical
Landing 4 carries the nine messages keys this branch's packet and poster
funnel call (4be260a0), so the eleven catalogue conflicts resolve to MAIN'S
side in full: measured first, not assumed — my side added exactly those nine
keys to each of the eleven locales, changed no existing key, and all nine are
byte-identical on main. Whole-file take-theirs therefore loses nothing, which
is the only condition under which taking a whole shared catalogue is safe.

The six public/ conflicts are build artifacts and are NOT hand-merged: they
take main's side here and are regenerated from the merged source in the next
commit. A hand-merged bundle is what produced three consecutive SRI
mismatches, where leaf HTML pinned a hash for a bundle that no longer existed
and the browser refused the script — a blank page in 6/6 runs.
2026-09-26 21:45:06 +00:00
Doxa Maps
32792c0fb3 Click a province, get its row in the table
The brief's third requirement at the grain the province work added: clicking a place
filters the table to it. Same split as the country and language links — a filter when
the tables are already beneath the map, a link when they are not.

The province key is a NAME, not a code: geoBoundaries' shapeName is what the table
joins on. So it is matched with diacritics folded, because a link that only works for
whoever copied it off the map is not an address — ?province=Arunachal Pradesh finds
"Arunāchal Pradesh". An unknown province matches nothing and says 0, rather than
falling back to every row.

The embed gate grew two arms for it and the first one failed for a reason that was
mine: it sliced the page text to 300 characters and then asked whether the province
name was in it. The row was on screen, 400 characters down, and the gate called the
product broken. A slice belongs in the message, never in the predicate.
2026-09-26 18:35:46 +00:00
Doxa Maps
64931139a1 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 17:55:20 +00:00
Doxa Maps
376ecaee25 Prayer as a move at the grain a team is sent to
A country is not where anyone is sent; a state is. Each province now carries the
same two prayer numbers the country sheet does and ends in the same move — the
largest group there with nothing and nobody praying, one tap from its prayer page,
ranked need-first so drilling from country to province never hands the reader a
second answer. Andhra Pradesh: 64 of its 76 groups with a prayer page have nobody
praying, 3.8M people.

Always N of M. doxa.life publishes a page for 1,723 of the roster, so nobody-praying
can be said of the groups that HAVE one and never of the province; both numbers
ship together so no surface can quote the first alone.

The join here is name+country, not language — you pray for a people in a place, and
that is doxa.life's own key. It is the one join in this build where a name match is
correct rather than forbidden, and the slug is always theirs: pray.doxa.life answers
200 for any path, so a constructed one would look valid and be empty.

The doctrine screen now scans the province table too. It is the SIXTH place a URL
lives in this bundle, and a table added after the scan was written is exactly how
the access table went unscreened for weeks — read from disk, so it is checked even
when the province layer was never opened.
2026-09-26 17:54:39 +00:00
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