Commit graph

464 commits

Author SHA1 Message Date
Doxa Maps
323283a43c Merge origin/development (32715a5cc) into upg100/india-states; package.json keeps both sides' gates; rebuild my-upg-100-list from the merged source, and regenerate the tree and SRI 2026-09-30 22:17:44 +00:00
Doxa Maps
3f23274a30 Merge origin/development (f8de00edf) into upg100/india-states; package.json keeps both sides' gates; rebuild my-upg-100-list from the merged source, and regenerate the tree and SRI 2026-09-30 22:11:30 +00:00
Doxa Maps
ffbb07e4a4 Country Posters wall draws its sheets through makeCountrySheet
The wall had its own inline wrapper around generateCountrySheet, a second
copy of @map/packet/countrySheet.js, which the Regions tab uses. Two copies
of one wrapper drifted twice (a teal Regions print, then India's series). The
wall now calls makeCountrySheet with the same country list, still handing it
the data's own English record rather than the localized card.

Behaviour: identical sheets for every country with records; a country whose
records come back empty now yields no sheet (the wall falls through to its
stored preview), as the Regions tab already did.

check:country-sheet-one-path (in gates:pre, with a self-test) parses every
plugin source: a direct generateCountrySheet call fails, every
makeCountrySheet call must pass the series list, both known callers must be
found, and seriesFor gives named answers over the real data (India 1 of 1,
each 3x3 country N of 173, an absent code no series). It fails on
development c4655072a for all three defects.
2026-09-30 21:57:25 +00:00
Doxa Maps
47a856c144 Merge origin/development (939db5b6c: P0 landed, posterverse, csv-guard) into upg100/india-states; package.json keeps both sides' gates; rebuild my-upg-100-list from the merged source, and regenerate the tree and SRI 2026-09-30 21:49:47 +00:00
Doxa Maps
f8de00edf7 Merge remote-tracking branch 'origin/development' into wall-of-people-groups-plugin/work
# Conflicts:
#	package.json
2026-09-30 21:47:28 +00:00
Doxa Maps
939db5b6c7 Merge origin/development (e9805a5c8: csv-guard, posterverse) into upg100/batch-select; package.json keeps both sides' gates; rebuild my-upg-100-list from the merged source, and regenerate the tree and SRI 2026-09-30 21:42:50 +00:00
Doxa Maps
39ded339fa Merge upg100/batch-select (P0 batch selection, 2bf2607fc) into upg100/india-states: P0b's India detection builds on P0's selectionPeople. package.json keeps both new gates 2026-09-30 21:33:24 +00:00
Doxa Maps
5a6b226a19 Posterverse: a capability-5 app shell for every poster plugin
<posterverse-app> is a vertical-tab rail of whole poster plugins, each mounted
as its real element from the manifest: UPG 100 List Posters (upg-100-mfe),
Country Posters (country-posters-mfe), Poster Design, and an Experimental
folder of Wall of People Groups, All People Groups, UPG Neighbor and Bible
Posters. It reuses VerticalTabsLayout, loadScriptOnce/whenTagDefined and the
Research Gallery's profile-config contract (urlSync:false, mapTitle, token
ladder, treeBase, assetsBase).

The whole rail is an allow-list (src/tabs.js), so a gated plugin such as
key-cities can never appear. Rows whose bundle is not in the tree are pruned;
Poster Design and Bible Posters carry provisional ids until their bundles land.
meta.json declares nested-mfe-spa and composes the five rows that exist, so
the Plugin Studio lists it under capability 5.

check:posterverse-shelf (in gates:pre) asserts the allow-list, the key-cities
exclusion, composite-not-leaf resolution, composes/rail agreement and
11-locale string parity; --self-test plants one defect per arm.
2026-09-30 21:32:51 +00:00
Doxa Maps
db1e6c978f Wall of People Groups: the wall pans and zooms with the pointer, 1:1
d3-zoom on a canvas reports its transform in the canvas's own CSS px, but the renderer applied it inside
the "meet" fit (a leftover from the SVG, whose viewBox made it world units). Every pan therefore moved
the wall by only the fit scale: a 200 px mouse drag panned 34 px at 1440, a 120 x 200 px finger drag
14 x 23 px on a phone, and a wheel zoom drifted about 90 px off the pointer. The transform now applies
after the fit, in Bless and Social mode alike.

Also: a trackpad pinch (ctrl-wheel) zooms the wall instead of falling through to a browser page zoom,
with a capped step; and a fling can no longer leave an empty table (the wall always covers the centre
of the view). check:wall-pan (gates:pre) runs the real WallCanvas methods with d3's transform
arithmetic and catches the old formulas.
2026-09-30 21:29:56 +00:00
Doxa Maps
f88a7bbe69 India's states and union territories: register check:india-states in gates:pre (mutation-tested: a dropped slug, a slug in two states, a stale input, a tampered served copy each FAIL), and the table's README (what reads it, its shape, how it is regenerated) 2026-09-30 21:29:11 +00:00
Doxa Maps
dd913f7269 UPG-100 batch selection: recolour, Copy and Export CSV are back. The colour swatches return; Copy puts the territory line apply-grouping parses first, then the selection's people groups as CSV; Export CSV downloads the same CSV. One detection feeds every action, prefetched when batch mode opens so a tap copies from cache. New gate check:batch-selection-export (gates:pre) 2026-09-30 20:53:19 +00:00
Doxa Maps
f3fdf4bf37 Mobile apps from plugins: a how-to, with Deep Research as the worked example
documentation/how-to-build-mobile-apps-from-plugins/README.md explains why Deep Research
(doxa-maps-spa, its app preset) is the plugin to wrap first: the seven production maps in one
phone-ready shell, token-free on MapLibre. It then walks the whole path: tools, the wiring check,
one command, what each step does, another plugin as the app, making it your own, and fixes.

The one command needed two small changes; the default app is unchanged:
- internal/set-app-start-page.mjs takes APP_START=<plugin>/<screen>[/<preset>] and opens that page
  instead of the UPG-100 list. Unset, it behaves and marks the page exactly as before. A malformed
  value, a page missing from the build, or links that would not resolve are refused, as before.
- package.json: phone:from-tree is sync, start page, verify, assemble, install from the committed
  tree, with no full rebuild (bun run build refuses without BUILD_ALLOW=1).
  phone:deep-research is phone:from-tree with Deep Research as the start page.

how-to.md links the guide, and two of its lines are corrected: webDir is public/dx-maps/, and
apk:debug's first step is the full build, which asks for BUILD_ALLOW=1.

Checked, with no Android tools: the start page written for doxa-maps-spa/doxa-maps-spa/app, then
the app root served and loaded in the shared browser at 390 and 1280. It showed the gallery
element, 15 tabs (the seven maps and the experimental ones) and a drawn map, with 0 console errors
and 0 failed requests. --check passed for APP_START and failed for the default until it was
restored; the default run still writes the UPG-100 page with its original marker. The Gradle and
adb steps are those bun run phone already uses; they were not run here.
2026-09-30 10:04:34 +00:00
Doxa Maps
fa69209035 The gallery's published section order: Prayer Progress before the UPG-100 List
The Deep Research page publishes the rail as Dashboards (Big Picture, Simple Research, Regions),
Prayer Mobilization (Prayer Progress, UPG-100 List), Gospel Research (Cities with Internet, Gospel
Resources), then the Experimental folder. Only the Prayer Mobilization pair moves. Big Picture now
says "desktop only" on every width, not only once a phone has disabled it.

check:spa-tab-order asserted the old four-name order with UPG-100 first; it now asserts all seven
published rows in order (still relative, with tab 1 positional). It needs a browser: rega runs it.
2026-09-29 20:00:36 +00:00
Doxa Maps
2b210b78f0 Merge remote-tracking branch 'origin/development' into regions/country-map-refetch
# Conflicts:
#	package.json
2026-09-29 19:36:16 +00:00
Doxa Maps
656503a28d Merge remote-tracking branch 'origin/development' into regions/pane-config-land
# Conflicts:
#	package.json
2026-09-29 19:12:46 +00:00
Doxa Maps
e643e8a30e country-map: key the refetch by value, let only the newest response win, show the wait
Three defects on country-map/country-map, which is production -- DX Life's
country pages. Found by auditing the element rather than from a report.

1. ANY profile-config change refetched all 2,106 records. The watch getter
   returned a NEW ARRAY literal, and Vue compares a watch value with Object.is,
   so a fresh array always reads as changed; both computeds read the injected
   cfg, so the effect re-ran on every config change and the callback then fired
   unconditionally. MEASURED with real Vue: flipping isDark refetched the whole
   endpoint and redrew the map, and so did a host adding pinHint. A joined
   STRING compares by value, so only a real language or country change refetches
   -- which must still refetch, because the API localises the names.
   This was latent waste until the composite re-forward made config writes
   routine. It is now on every upstream locale switch, so the two changes are
   complementary rather than independent.

2. A SLOW EARLIER RESPONSE OVERWROTE A NEWER ONE. loadPoints is async and had no
   sequencing at all -- 0 abort or generation tokens in the file -- so two runs
   could be in flight and the one that STARTED first could FINISH last. Both
   assigned points, so the map showed the previous country's pins with no error
   and nothing for the reader to notice. Every run now takes a ticket and a stale
   ticket writes nothing: not the points, not loadFailed, not the spinner the
   current run is still using. fetch is left to complete rather than aborted,
   because the response is cached for the next switch back.

3. `loading` WAS WRITE-ONLY: assigned in three places and read in none, zero
   template references. A reader on a country page waited on an empty map -- no
   outline, no pins, no message -- which is indistinguishable from a country that
   genuinely has no people groups, and this element fetches the whole list before
   it can draw one pin. "Loading results..." was already in the catalogue in all
   eleven locales, so saying so cost no new string and no parity debt.

check:country-map-refetch guards all three. It drives the REAL reactive chain
with real Vue and takes the watch key expression OUT OF THE SHIPPED SFC, because
bug 1 is entirely a question of what Vue considers changed and is invisible to
any test that fakes reactivity or re-types the watch.

Mutation-tested, 5 mutations, all caught by name. The first version of the gate
MISSED one: a bare /if \(!current\(\)\) return/ grep matched the guard in the
catch block, so removing only the guard before the points write still passed.
The assertions are positional now -- the guard must sit immediately before the
assignment it protects -- and the catch guard is asserted separately. That hole
existed because the behavioural half of the test reproduced the guard's shape
inside the gate instead of reading the component, which is the self-consistent
oracle this repo's other geometry gate explicitly warns against.

Registered in scripts and gates:pre, verified by re-parsing: one key, 107
checks. Bundle rebuilt (BUNDLE=country-map), then tree, then fix:sri.
2026-09-29 19:11:37 +00:00
Doxa Maps
e7962e4ef7 Merge origin/development into studio/hygiene 2026-09-29 18:47:01 +00:00
Doxa Maps
2a4c3ddb3d The full build also copies the maps into the sibling DX Life site, when it is there
`bun run build` now ends with `sync-dropin --from-config --if-present`. It copies public/dx-maps into the
ONE configured host path (vite.doxa-maps-build-config.json dropin.dest, default ../doxa-life/public, or
DOXA_DROPIN_DEST). No site at that path: it prints "skipped" and exits 0. Nothing else on the filesystem
is searched for repos or sites. DOXA_DROPIN=off skips it even when the site is there. Without
--if-present, a missing host is still an error, as before.
Tested: no host skips (exit 0); a host gets 1,209 files; DOXA_DROPIN=off copies nothing; the plain
command still refuses a missing host. The container's /cdn build (build-cdn.sh) does not run this chain.

On this box ../doxa-life does not exist beside the doxa-maps checkouts (DX Life lives in root-dx-life), so the step is a no-op for the fleet's builds.
2026-09-29 18:39:36 +00:00
Doxa Maps
cc65f40c1e Gate hygiene: three gates:post reds on development, and two things that hid failures
- check:no-orphan-gates stopped at internal/check-grid-merge-defaults.mjs, which had no script. It
  asserts defaults of three grid props that are not on development (the grid merge has not landed),
  so it is registered ON DEMAND with that reason, not chained red. Past that first arm, its chain arm
  found check:grid-image-loading and check:retry-fetch run by no chain; both depend only on code and
  pass in under 300 ms, so they join gates:pre.
- A tracked vite.config.js.timestamp-*.mjs (395 KB, eight absolute paths) failed check:no-abs-paths.
  Removed, and ignored at the source: vite writes one beside its config while it bundles it.
- check:library-index: documentation/reference-library-index.md regenerated for retryFetch.js.
- run-parallel printed a failed step's output only with --verbose: the unbraced else bound to the
  inner if. Braced; a failing step now prints why.
- The Pages workflow ran bun run build without BUILD_ALLOW=1, so build-lock refused it with exit 75
  and the workflow ran no gate at all. A CI runner is the only builder on its machine; it opts in.
2026-09-29 18:35:37 +00:00
Doxa Maps
9ccec24d29 Generalise the pane re-forward to attribute-configured panes, and gate it
sr asked to adopt usePaneConfig for research-map-plus-grid rather than write a
second fix, and found the one shape it did not cover: people-groups-grid takes
its filters as THREE separate attributes (wagf-region, religion-family,
primary-language) instead of one profile-config blob, so that composite has one
pane of each kind. The helper hardcoded 'profile-config' and gave the grid
nothing.

registerAttrs is the sibling for that shape. It shares the same entries list,
the same watchEffect, the same prune and the same write-only-on-change -- one
mechanism for all four composites rather than a fork per adopter. A helper with
a single caller has not been shown to generalise at all; this is the second
shape that showed it.

Two decisions worth recording. Each attribute is compared and written
independently, so a getter whose region changed does not rewrite the two that
did not and re-trigger the child. And the attribute NAME LIST is pinned at
registration: a later getter that omits a key CLEARS that attribute rather than
leaving a stale filter silently narrowing the grid, which is the same class of
lie as the frozen config this helper exists to fix.

The gate runs the shipped helper with real Vue, not a re-typed copy, because the
dependency-tracking hole found in review is invisible to any test that fakes
reactivity. 28 cases over both shapes. Its subject is the measurement on
development 92ca8b6e6: the outer element reached locale ar by +1.5s while every
nested host carried NO locale key at boot and still none at +15s, across four
composites -- so case 1b asserts that an ABSENT key is still a change.

Case 7 is the adopter's property rather than mine: a re-forward triggered by a
LOCALE change must produce the SAME primary-language string. gridLanguage() can
fall back to a display label, and the grid compares whole normalised values, so
a name fetched in one language matches no row in another. It does not bite today
because the surviving fallback is a value the grid itself produced, but the gate
asserts the class, not today's instance.

Mutation-tested, 7 mutations, all caught: plain array, no write-on-change guard,
skip-instead-of-prune, escaping getter throw, no per-attribute compare, omitted
key left stale, and the label-instead-of-code regression. The first two crashed
rather than naming a case, so cfgOf is null-safe now -- a gate that dies reports
"something went wrong" instead of naming the pane that stopped following.

Registered in package.json scripts and in gates:pre, verified by re-parsing the
file: one key, 104 checks. gates:pre PASS 103 FAIL 0 NM 1.
2026-09-29 18:34:49 +00:00
Doxa Maps
99816626af Every shadow stylesheet gets its own label, and a gate keeps it that way
useShadowStyles keys its <style> by the label and overwrites an existing
tag with the same key, so two components sharing a label keep only the
last one's CSS in a shared shadow root. The regions list lost its sheet
this way to the region card, which had kept the list's label when it was
extracted.

check:shadow-style-labels-unique (gates:pre) fails any label two calls
share, globally rather than per shadow root, so a copied or forked
component is caught when it is copied. It reads the real parsers,
classifies every reference to the function (a rebinding, .call, a
re-export or a namespace import fails instead of hiding a call),
cross-checks the AST against the token stream, and covers the other
ways two calls share a key: unlabelled calls under the dev server's
per-component key, and a fixed label over CSS that varies per caller.

countries-map.vue, a fork of research-map.vue, carried the same label;
it is now 'countries-map', with the country-posters bundle rebuilt and
the tree regenerated. The composable's JSDoc called the label a
debugging name; it now says it is the key.
2026-09-29 17:42:44 +00:00
Doxa Maps
de6d60378e Merge origin/development into studio/integration 2026-09-29 16:38:05 +00:00
Doxa Maps
2ff7d2f17b Merge origin/development into studio/integration 2026-09-29 16:01:38 +00:00
Doxa Maps
0417e46fbd Merge remote-tracking branch 'origin/development' into bundler-2/handoff
# Conflicts:
#	internal/check-poster-layout.mjs
#	library/composables/poster/useMapPoster.js
#	package.json
2026-09-29 16:00:18 +00:00
Doxa Maps
212ec20816 Merge remote-tracking branch 'origin/development' into bundler-2/handoff
# Conflicts:
#	package.json
2026-09-29 15:54:16 +00:00
Doxa Maps
14096e19c2 Merge remote-tracking branch 'origin/development' into upg100/merge
# Conflicts:
#	package.json
2026-09-29 15:54:05 +00:00
Doxa Maps
8eccda7752 Merge origin/development into studio/integration 2026-09-29 15:37:45 +00:00
Doxa Maps
9d00aa3db4 check:snippets-draw: every published snippet, pasted verbatim on another origin, must draw (awaiting its first run)
Ported from rega's snipgate and mbxcheck probes (card GATE-EVERY-PUBLISHED-SNIPPET-DRAWS-A-MAP). Two scripts so the keyless arm never waits on a token: check:snippets-draw pastes each production embed.script and requires a MapLibre map to draw (a hooked instance with its style loaded via a setter trap on the engine global, a sized canvas, tiles 2xx, no failure note, 0 page errors); check:snippets-draw:mapbox does the same over embed.scriptMapbox with only the token filled from MAP_TOKEN, NOT MEASURED without one and never printing it. Composites that open on a list (regions-mfe, upg-100-mfe) are pressed to their map view under the settle rule first, since a hidden pane still draws a canvas. The browser slot is checked before any server starts, and the tree server is spawned --exit-with-parent. Registered and declared on demand until rega's first run; it joins verify-browser-all after.
2026-09-29 14:56:52 +00:00
Doxa Maps
65729d608d Register and chain check:list-pattern-doc
The list-pattern gate (internal/check-list-pattern-doc.mjs) came in with
its doc but was never registered, which check:no-orphan-gates refuses.
Registered, and chained into gates:pre: it is headless and re-derives the
7 numeric claims in PATTERN-A-LIST-OF-ZONES.md from the data files.
2026-09-29 14:54:47 +00:00
Doxa Maps
023afcb55e Restore the gate chains after merging eleven branches
Merging the branch backlog cut gates:pre to 49 entries of 102 and gates:post to
86 of 108: package.json holds each gate on its own line, but the chains are
single long strings, so a line-wise union takes one side wholesale while every
definition survives. A run then passes and means almost nothing.

Both chains come from development, with the one gate these merges add appended
rather than replacing them. Every gate named in a chain is defined: checked,
not assumed.
2026-09-29 14:51:12 +00:00
Doxa Maps
bdb74ff69f Merge remote-tracking branch 'origin/development' into bundler-2/handoff 2026-09-29 14:50:51 +00:00
Doxa Maps
4d65d399b1 Merge origin/development into the handoff integration 2026-09-29 14:50:47 +00:00
Doxa Maps
6156895e54 Merge branch 'bundler-2/driver-1650' into bundler-2/handoff
# Conflicts:
#	internal/check-embed-url-absolute.mjs
#	internal/check-legend-top-offset.mjs
#	internal/check-map-menu-anchor.mjs
#	internal/check-mercator-slice.mjs
#	internal/check-one-scroll-container.mjs
#	library/components/map-controls/MapControlMenu.vue
#	library/composables/poster/useMapPoster.js
#	library/composables/usePosterPrints.js
#	library/maps/mercator-slice.js
#	library/utils/embedBase.js
#	package.json
#	plugin-designs/doxa-research-map/research-map.vue
2026-09-29 14:50:44 +00:00
Doxa Maps
336a1e0a18 Merge branch 'bundler-2/script-registry' into bundler-2/handoff
# Conflicts:
#	internal/check-legend-data-contract.mjs
#	internal/check-map-control-overlap.mjs
#	library/composables/poster/useMapPoster.js
#	library/composables/useMapInstance.js
#	package.json
#	plugin-designs/doxa-research-map/research-map.vue
#	plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue
2026-09-29 14:50:42 +00:00
Doxa Maps
85252c8d47 Merge branch 'bundler-2/he-sr' into bundler-2/handoff
# Conflicts:
#	internal/check-hardcoded-english.mjs
#	internal/check-label-key-coverage.mjs
#	internal/check-packet-variants.mjs
#	library/ProfileLoader.vue
#	library/i18n/locales/ar/search.json
#	library/i18n/locales/de/search.json
#	library/i18n/locales/en/search.json
#	library/i18n/locales/es/search.json
#	library/i18n/locales/fr/search.json
#	library/i18n/locales/hi/search.json
#	library/i18n/locales/it/search.json
#	library/i18n/locales/pt/search.json
#	library/i18n/locales/ro/search.json
#	library/i18n/locales/ru/search.json
#	library/i18n/locales/zh/search.json
#	package.json
#	plugin-designs/doxa-research-map/research-map.vue
#	plugin-designs/research-dashboard/src/components/dashboards/DashboardResearchFilters.vue
2026-09-29 14:50:40 +00:00
Doxa Maps
97ffa4e8df Restore the full gate chains after the merge took an older package.json
The union resolution kept every script DEFINITION but took the older branch's
gates:pre and gates:post chains, which are single long strings rather than one
line per gate -- so a line-wise union picked one side wholesale. That silently
dropped 95 gates from the pre chain and 30 from post: every definition present,
almost nothing actually run.

Both chains are restored from development, with any gate this merge adds
appended rather than replacing them. Caught by counting the gates in the chain
against development, not by the run, which passed 7 of 7 and looked green.
2026-09-29 14:43:13 +00:00
Doxa Maps
bb66d4a03b Merge branch 'bundler-2/p0-setup-order' into bundler-2/handoff
# Conflicts:
#	internal/check-setup-order.mjs
#	package.json
#	plugin-designs/doxa-research-map/research-map.vue
2026-09-29 14:40:37 +00:00
Doxa Maps
8fcd6ad834 Merge branch 'bundler-2/poster-layout-optin' into bundler-2/handoff
# Conflicts:
#	package.json
2026-09-29 14:40:36 +00:00
doxa-maps
fcebf2c589 A9 lever 1: engine-aware resource hints on map leaves
A cold map ran serially: leaf HTML, then the bundle (250-470 KiB gz, parsed),
then only the engine (from cdn.jsdelivr.net or api.mapbox.com), then the style
host, tiles, and data. Four origins discovered one after another, none
pre-connected. That waterfall is the shape behind A9's 3-5 s cold map.

generate-tree now emits, in each MAP leaf's <head>, a preconnect for the
engine origin, the style host and the data origin, and -- only on the 'auto'
leaf, which carries no engine <script> today -- a preload of the engine, so
DNS/TLS and the engine download start alongside the bundle instead of after
it. A non-map leaf (grid/card/chart-table/app-shell/composite) gets nothing.

The engine is decided at build time from the SAME inputs the runtime ladder
reads (profile-config.provider, then a baked token or a token endpoint; 'auto'
with no token path is MapLibre keyless), so the hint and the ladder cannot
disagree. Every URL, integrity and crossorigin comes from
library/constants/cdn.js, so a preload cannot drift from ensureEngineScript's
tag. The one case a static hint cannot foresee -- an explicit ?provider= or
window.MAP_PROVIDER overriding the build default at runtime -- costs one unused
preconnect.

New gate check:leaf-hints (gates:post): a map leaf without hints fails, a
non-map leaf carrying a map-engine hint fails, and a hint origin must be one
the code uses. PASS: 36 map leaves carry hints, 28 non-map leaves carry none.
Poison-tested both sides.

Unlanded (dx-base/a9-leaf-hints); the bundled-vs-shared-vs-today trade-off
report and any landing wait on rega's timing and regions' tiles.
2026-09-29 14:40:27 +00:00
Doxa Maps
4b1bfdb23c Merge branch 'bundler-2/poster-attribution' into bundler-2/handoff
# Conflicts:
#	library/composables/poster/useMapPoster.js
#	package.json
2026-09-29 14:40:26 +00:00
Doxa Maps
4571596cf1 Merge branch 'bundler-2/grid-lazy-images' into bundler-2/handoff 2026-09-29 14:39:13 +00:00
Doxa Maps
1b19b7478a Register the control-inset gate, now that it has been run and proven
verify-browser-all's rule is that every gate listed there was run before it was
listed, so this waited for a real run rather than being registered on the
strength of the code reading correctly.

Measured, on the browser seat: the positive arm PASSES 9/9, and the mutation arm
-- the maplibre selector inside bottomControlInset broken to a name that matches
nothing -- FAILS on cases 1 and 5, the two whose pages carry only a maplibre
attribution. Cases 2, 3, 6 and 7 pass under the mutation by design: they assert
the inset is 0, so a selector that stops matching cannot make them fail. They
guard the opposite error, reserving space that is not there.

Registered in all three places: the script, its //scripts description, and
verify-browser-all's list. Deliberately NOT in gates:pre -- every assertion rests
on getBoundingClientRect, so it needs a browser and exits 75 NOT MEASURED
without a slot, which is never a pass.
2026-09-29 14:35:29 +00:00
Doxa Maps
12078103d1 check:no-orphan-gates sees gate FILES outside internal/: a plugin's own gate that nothing runs fails
The file walk read internal/ only, and the chain arm counts package.json NAMES, so a plugin-local
check-*.mjs that nobody registered was invisible to both: on 2026-09-29 six were run by nothing
(dx-shepherd's census; exp's four have since been registered). Every tracked gate-shaped file under
plugin-designs/ or library/ must now be run by PATH from a script or another file's code, carry
@one-shot, or sit in the shrink-only `files` baseline for its owner.

Two rules keep it honest. Comments are stripped before searching, since a file mentioned in prose is
not run by it. And a basename counts only where no other gate file shares it:
library/scripts/research/verify-map-paint.mjs (199 lines, run by nothing) shares its name with
internal/verify-map-paint.mjs (342 lines, what verify:map-paint runs), so a name match let one file's
registration vouch for the other. Self-tested on planted cases (unregistered, comment-only, shared
basename). On the real tree it finds 4, now baselined for their owners:
  library/scripts/research/verify-map-paint.mjs            likely a stale copy
  plugin-designs/doxa-research-map/scripts/check-reduced-motion-jumps.mjs
  plugin-designs/gospel-resources/scripts/verify-translator-org-links.mjs   a data tool? @one-shot or register
  plugin-designs/research-map-plus-grid/scripts/check-ui-strings-have-keys.mjs
The chain arm's writer now keeps every key of the shared baseline file.
2026-09-29 14:35:28 +00:00
Doxa Maps
5a73e4d37d Guard the pan's control clearance, which a cut-height check cannot see
The popup auto-pan had no gate. Its founding bug was invisible to the obvious
one: measured at 390 on a 366x732 map, the pan fitted the popup inside the
container, the cut really was zero, and a real click on Full Profile still did
nothing, because the top element at the button's centre was
div.maplibregl-ctrl-attrib-inner on one pin and span.pin-hint-pill on the other.
Two pins that looked fixed and were not.

So this asserts the property that matters -- where the popup ENDS UP relative to
the band that takes the click -- rather than whether it was clipped.

It runs the SHIPPED panPlanFor, read out of the SFC with new Function because it
is not exported, the same way check:optional-t-equivalence reaches useOptionalT.
A re-typed copy of the arithmetic would pass forever while the component drifted,
which is the one failure a geometry gate must not have. POPUP_OFFSET is read from
the source too rather than assumed.

Each case states the resulting geometry independently instead of re-deriving the
expected number from the same formula: a self-consistent oracle cannot see an
error that shifts both sides, which is how a 12px offset hid here once already.

The band is measured at runtime, never hard-coded, so the rule is swept across
0-120px rather than pinned to one number -- 120 is far beyond anything observed
and is there to prove the rule, not the value. Also swept across every width the
component renders at, since below 768px the container is 2x width.

Mutation-tested three ways, each failing the case written for it: ignoring
bottomInset (the pre-fix behaviour) fails 15 cases; reserving only half the band
fails; dropping the Math.max clamp fails exactly the negative-inset case.
24 cases, registered in all three places, verified by re-reading the parsed file.
2026-09-29 13:31:24 +00:00
Doxa Maps
dd0428823c exp's four headless guards are registered, described and chained in gates:pre
check:wall-wysiwyg (0.9 s), check:wall-print-watch (0.7 s), check:wall-no-bg-photos (13 s) and check:template-i18n (9.7 s), each run once on this round's tip before chaining (all PASS) and each two-sided on its known-bad (dx-base poisoned all four independently). Placement read, not assumed: the two Vite runs use middlewareMode and bind no port, and wall-no-bg-photos serves a committed fixture with fetch stubbed, so no answer depends on the network, the machine or the moment. The orphan arm now counts 219 of 330 reached.
2026-09-29 13:31:14 +00:00
Doxa Maps
6a1860539c Chain the region-paint gate now that both maps take the library's expression
The gate asserts the regions idle paint has one source. It could not be chained
while only one of the two maps had adopted the library export, because it was
legitimately red on the other plugin owner's unlanded work, and chaining a red
gate holds the shared run for everyone.

Both halves are now in: this map took the export, and the other adopted it too,
so the gate reports all ten assertions green and can run with the rest.

This replaces the on-demand declaration rather than following it. Declaring a
gate as run-by-hand is a decision that costs a reader something to understand,
and it is only worth making while it is true -- the second half landed sooner
than the declaration assumed, so the honest end state is a chained gate and no
entry at all, not an entry added in one commit and deleted in the next.

Verified on the combined tree: the gate passes, the orphan guard passes with it
counted among the chained scripts rather than the declared ones, and poisoning
the gate still fails it.
2026-09-29 13:29:18 +00:00
Doxa Maps
4ae1bbf4f5 Gate: the regions idle paint must have one source, not a copy per map
Two production maps paint the doxa-regions layers directly -- 35
setPaintProperty('regions-fill'|'regions-border') calls between them, 10 in one
file and 25 in the other. Twenty-one pass their value through a one-line helper
that wraps it in the same ['case', ['has','iso_3166_1_alpha_3'], v, v]
expression, and each map defined its OWN copy of that helper while already
passing it into the library composable that paints with it. The library owned
the paint logic and not the expression, so the two copies could be edited apart
and the same "idle" would render differently on the two maps, with nothing
failing. No existing check covers this: the nearest one reads a different
component entirely.

The gate asserts the idle paint is single-sourced: the library exports the
helper, neither map keeps a local definition, and neither inlines the
expression. It deliberately does NOT require all 35 writes to route through the
library. The raw multi-line writes are highlight states and outline resets that
differ by design and by map; demanding uniformity there would make this red on
the day it was written, which is how a gate earns being ignored. It also does
not assert how OFTEN each map repaints -- one repaints the idle state more
than the other and has a dynamic call the other lacks. That is a real
difference in when, and flattening it is how a map goes blank. This checks that
the maps agree on what idle looks like, never on how often they say it.

Two-sided. Poisoning it un-exports the library helper and re-inlines a local
copy in a consumer; three assertions flip from ok to FAIL, each catching its
own planted regression, and the run exits 1.

Registered but NOT chained yet: it is legitimately red until the second map
adopts the library helper, and chaining a red gate would break the shared run
for everyone else. It joins the chain with that change.

Building it found a third copy nobody had counted, under a different name, and
two bugs in the gate's own first draft: a block-comment stripper that ate the
declarations it was meant to find (so every single-source assertion passed
while both files plainly defined the helper), and an exemption for lines
mentioning the helper, which hid a one-line definition that mentions it. Both
were caught by the rule that a gate must fail when poisoned.
2026-09-29 13:29:18 +00:00
Doxa Maps
6a0aade5c1 The browser/Node sheet equivalence check can measure on a fresh checkout
check:poster-browser-equiv is chained in gates:pre and exited 75 NOT MEASURED
every run. Its input, out/_records.json, is gitignored and written only by
generate.mjs against a live API, so on CI and on any box without credentials the
gate never looked at anything. The runner reported that honestly rather than as
a pass, but an identical NOT MEASURED every run is indistinguishable from an
unwired gate -- and this is the gate that proves a sheet generated in the browser
is byte-identical to the one Node writes.

An equivalence check needs the SAME input on both sides, not fresh input, so a
committed fixture loses nothing it was measuring. The fixture is the 124 public
records the two arms actually consume: Nepal's 34 for the country arm, and the
90 slugs the Gujarat zone selects for the zone arm. Same fields the served maps
show; no other data.

Verified equivalent to the full 2,106-record cache: identical zone pins in the
same order, identical Nepal set, count 90, pop 3,164,770, series {2209, 28, 1}.

Record ORDER is load-bearing -- it drives draw and label order -- so the fixture
keeps the cache's order. A first cut sorted it: every aggregate matched and the
pin SET matched, while the order differed. Sorting a fixture would have quietly
changed the sheet it produces.

generate.mjs gains RECORDS_FILE so a caller can name the offline input instead of
only the API-written cache. It still fails closed without an API when no offline
file is given. The gate passes it the very file the browser arm read, so both
sides are fed the same records by construction.

The gate now says which input it measured, because the full cache widens coverage
to all 175 countries where it exists and the fixture is the floor everywhere else.

Two-sided, on this tree: with the fixture it PASSES byte-identical on both arms
(834,174 and 921,522 bytes). Dropping the province boundaries at
posterSheets.js:206 -- the defect this gate's header records, where a country
beside India or China printed without them -- makes the country arm 251,576
against Node's 921,522 and the gate exits 1. Restored, it passes again.
2026-09-29 12:44:25 +00:00
Doxa Maps
7ae80e3810 The coverage gate's poison self-test runs beside its chained arm
check:coverage-consistent:chained sets the link-age threshold out of reach. The proof that this narrows its scope and does not disarm it is the poison arm: its clock is computed RELATIVE to the threshold, so all 15 clauses (A-O, L included) still fail when poisoned at any setting (regions measured it at 99999). That proof was an orphan; it now runs on every landing, like the other gates' self-tests. 1.6 s.
2026-09-29 11:56:47 +00:00
Doxa Maps
b142fe1178 Two more gates join gates:pre honestly: dxlife walks only the clones, and the calendar stays on demand
check:dxlife walked the whole root-dx-life umbrella, so git worktrees of doxa-life (wt-rg-*) and
a dangling one (research-map-clean, whose .git points at a path that no longer exists) read as
+4,524 "added" files and held it red while the clones were untouched. It now skips any directory
whose .git is a FILE beginning `gitdir:` (keyed on that file, not on `git worktree list`, which
cannot see the dangling one) and .cache with the other generated dirs, and compares the baseline
only within that scope. Result against the 2026-09-20 baseline: 2,844 clone files, +0 -0 ~0. So the
law has held since that date, and the baseline stays as it is: re-taking it would reset the evidence.
Nothing is deleted; research-map-clean may hold the only copy of its work. With no clone on the
machine it reports NOT MEASURED (75) instead of exit 2, so chaining it cannot fail other machines.

check:coverage-consistent fails when the oldest verified link is over 45 days old: its verdict
depends on the calendar, and chained it would turn gates:pre red on 2026-11-08 with no change
(regions measured the margin). Every other clause depends on code and data and is worth running
on every landing, so gates:pre runs check:coverage-consistent:chained (the age knob set out of
reach). The plain script is declared on demand as the freshness run, with that reason. Its poison
self-test still proves every clause can fail.

gates:pre 94/0/2 of 96. The orphan baseline shrinks to 105; 7 checks are declared on demand.
2026-09-29 11:56:47 +00:00