Commit graph

728 commits

Author SHA1 Message Date
Doxa Maps
cf0b23a4d1 check:mjs-parses parses with node even when the gates run under bun
Under bun, process.execPath is bun, and bun --check runs the script instead of
parsing it; a cloud build hung inside check-link-rot-sample.mjs. The gate now
uses node under bun and fails plainly if node is missing.
2026-09-27 22:10:35 +00:00
Doxa Maps
533ef04a9f Rudder transcribes on a server: the page streams its own microphone to the engine
With RUDDER_AUDIO=browser the engine records no local microphone. The page
captures the reader's mic, converts it to 16 kHz mono PCM in an AudioWorklet,
and streams it over a WebSocket at /api/rudder/stream into the same speech
session a local recorder feeds. The words come back on that socket to that
reader only, never on the shared event stream. One session at a time; a second
reader is told the engine is busy. The engine honours RUDDER_HOME, accepts the
public host named in RUDDER_ALLOW_HOST, and refuses a socket from another
origin. The dev proxy now carries WebSockets to backends.
2026-09-27 22:10:35 +00:00
Doxa Maps
30a02d2782 Rudder's corner shows a live mic when collapsed, and dev backends survive Vite restarts
Two gaps found while checking the corner widget (1775f5ac: "Needs eyes: open, HMR, collapse" — all
three verified in Firefox against the running Plugin Studio: opens, keeps the SAME element across a
Studio hot update, collapses).

- LIVE MIC WHEN COLLAPSED: rudder-voice emits `recording` (true/false); the Studio's corner button
  turns red with "● " while it listens, so closing the panel never hides that the mic is on.
- DEV BACKENDS SURVIVE VITE RESTARTS: Vite restarts itself when vite.config.js or anything it imports
  changes. The backend was stopped on every restart and a new one started at once; the old process
  still held the port, the new one died "address already in use", and Rudder showed "engine offline"
  until `bun run dev` was restarted by hand (measured twice). Backends now live in a process-wide
  registry: reused across restarts, stopped only when the dev process exits, restarted if they crash.
  The "engine not running, quietly" answers (89ff2238) are unchanged and read the same registry.
2026-09-27 20:09:03 +00:00
Doxa Maps
2044081798 Rebuild the served tree after merging main
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-27 19:26:55 +00:00
Doxa Maps
37aaf69c24 rega's four notes on the card, all four mine
1 THE CHIPS SAID "unchecked" ON VERIFIED LINKS. Card mode never fetches the verification table,
so every chip understated the evidence behind it — caught on central-sama, whose Scripture and
film are both verified. Each shard now carries the states for its OWN urls, keyed by url exactly
as linkVerify is, so vchip() and vtitle() work unchanged. Shards grew 8.0 KB to 14.0 KB mean,
29 KB worst; against 6,051 KB for the map that is a price worth paying for not lying about
evidence.

2 MY GATE'S FALSE NEGATIVE, which cost rega a round: it asserted
text.includes(slug.split('-')[0]) — "abai" against a rendered "Abai Sungai". A slug fragment is
not a name. It compares the roster's name, case-folded, anywhere in the card.

3 THE EMPTY GREY BAND. The sheet is a bottom panel capped at 62% of the map; in card mode there
is no map under it, so a phone showed 285px of grey above it. Full height in card mode, and the
border radius goes with it.

4 THE CLOSE BUTTON with nothing behind it — hidden in card mode, both instances of it.

gates:pre 11/11, LINK-COVERAGE 12,292 unchanged, SFC-PARSE PASS. rega spot-checks after landing
per dx-shepherd; the render claim is still not mine to make.
2026-09-27 19:26:29 +00:00
Doxa Maps
e79d6dc8f4 A shared link reads a list separated by , or |, so partner links already published keep working
Links on partner pages write band=yes|partial, and the reader split on ',' only,
so such a link read back as one value and matched no row. Reading now accepts
either separator; writing stays ','. A literal '|' in a value is percent-encoded
before any join, so every separator left in a URL is a real one.
2026-09-27 19:10:08 +00:00
Doxa Maps
f68cb24e78 2,001 groups had readable Scripture and were told they did not
go-int cross-checked 20 roster languages with "no New Testament found in the pools" against
open-data signals. Thirteen of the twenty had readable text all along — on a record the tier
could not see — and chasing that found one rule written four times and wrong in two of them.

THE BUG. tierOfCode tested `!!c.s`, one field, while this build's own hasReadableScripture
tested four sources. Tier 2 means "Scripture AND film", so a language whose text is on the
Faith Comes By Hearing record (au.m names the media it holds), on Scripture Earth, or in a
Door43 translation was scored 1 — "some resource found" — with its own text and the JESUS
Film both in hand. Measured: 602 codes and 2,001 of 12,379 groups move 1 -> 2. Tier 0 does
not move at all, 1,592 before and after, so nothing about who has NOTHING changes: the gap
lists, the 369 zero-access groups and the bridge-reach table are untouched.

THE SAME BUG DOWNSTREAM, and this is the one a catalyst would have felt: resourceRows typed
an FCBH record "audio, text, video" as Audio Scripture only, so the access table's
byType.Scripture never saw it. 288 people groups were published as "watch or hear, cannot
read" while their own readable Scripture sat behind that link. Access now reads 1,129 "read
and watch" (was 869), 518 "cannot read" (was 806), 85 "read" (was 57), 369 nothing found and
6 stories only — unchanged where it should be, and 2,107 in total either way. Typing the row
by its text would have dropped "can hear", so the access builder now counts a Scripture row
whose scope names audio as hearable too.

ONE OWNER NOW. hasReadableScripture is exported from gospel-resource-rows.js and imported by
the pin builder and by check-gospel-coverage-consistent, whose arm F had its own narrow copy
and therefore reported 2,001 disagreements against the corrected tier — a gate defending the
bug it exists to catch. A rule with four copies has no owner; this one has.

`sx` ships alongside: the readable text with its source named, for the 887 codes whose text is
not at c.s, so a surface can LINK what the tier now counts instead of claiming Scripture and
showing a film. Wiring it into the sheet and the field card is the next commit; this one is
data and rules only, and public/ is deliberately NOT rebuilt because rega is rendering this
worktree.
2026-09-27 18:53:52 +00:00
Doxa Maps
e2cebd9130 Shared links keep partner param names, and a CSV export matches multi-value filters
A shared table link can now be written as one query param per facet, and a
param name may map to a different bus key, so links already published on
partner pages keep working. The CSV export matched no rows once a reader picked
a second value in a filter; the default row match now reads a multi-value filter
as OR, and the gate no longer exempts that line.
2026-09-27 18:40:47 +00:00
Doxa Maps
655bbdc561 A filtered table becomes a link anyone can open, and the link now carries the typed search
Card BUILD-SHAREABLE-FILTERED-TABLES, for researchers and media-to-movements teams: narrow the table to
the peoples you are working on, then give that exact view to a teammate or reopen it on a phone.

TWO GAPS, both measured on main before building.

1. IT WAS NOT REUSABLE. The share hash was a PRIVATE function inside
   plugin-designs/research-dashboard/src/composables/useDashboardShell.js, so exactly one dashboard in
   the fleet could produce a link. go-int's board had none — grepped, zero matches — and every future
   kit dashboard would have had none either. Nothing in it was research-specific except its address.
   It is now library/composables/useShareableView.js, parameterized: the bus and labelFor are passed
   in, the filter and search param names are options, and no dashboard is named inside it. The
   research dashboard supplies the one genuinely research thing — labelFor, for the display-only slug.

2. THE LINK WAS INCOMPLETE, AND SILENTLY SO. The hash serialized filterSet ONLY. Typing in the search
   box is the commonest way this persona narrows a table, so a shared link reopened with MORE ROWS than
   the sender saw, with nothing to tell the recipient their set was wider. For population data passed
   between teams that is worse than a link that plainly fails. `q` is appended only when a term is
   present, so a dashboard with an empty search box writes byte-identically the hash it wrote before.

`&` joins params now, so it is reserved in values alongside `;` `:` `*` `,` `%` — otherwise a search for
"rice & beans" would truncate the link at the ampersand.

check:share-policy grows a DRIVEN arm: the two pure functions are lifted from the kit source and RUN,
so nine guarantees are measured rather than asserted — the old format byte-for-byte on an empty search,
slug still display-only, multi-value still dropping the slug, the typed search reaching the link and
coming back, a literal comma in a people-group name surviving, an ampersand in the search surviving, and
both legacy shapes (JSON hashes, and old #f= links with no q) still restoring. No browser needed to
prove a string round-trips.

Source only. Rebuild: research-dashboard, dashboard-plus-grid.

check:filter-multivalue arm C follows the file: it lifted escapeValue/unescapeValue from the shell to
drive the hash round-trip, and said so plainly when they moved ("the hash format moved and this gate is
blind to it") rather than passing vacuously. Repointed to the kit module.
2026-09-27 18:25:01 +00:00
Doxa Maps
c3332e3d85 The field card rendered blank, because I never rendered it
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
rega found ?card= throwing "Cannot read properties of null (reading 'country')" at the back
button. Card mode had no `selected` — the country — and the sheet reads selected.* in
FOURTEEN places, so guarding the one line rega pointed at would have left thirteen. The card
already knows its country, so `selected` is populated from it and every existing read is
satisfied; the curated-analysis block stays hidden on its own hasCurated test because a card
carries none of those fields. The back button is also hidden in card mode: it would have
opened a country sheet with no pins behind it.

The lesson is mine and worth the line. I shipped this having measured the shards, the byte
saving, the addressing and the card contents — 400 slugs resolving, a tier-0 and a tier-2
card correct — and reported it as done. Every one of those checks passed on a feature that
rendered nothing, because I cannot launch a browser and did not say that the render was
therefore unverified. Data being right is not the feature working.

So the gate grows the arm that would have caught it: a headless mount of ?card=<slug> from
the roster, asserting the page renders more than 400 characters and names the group, plus an
unknown slug that must still render. rega runs it (browser gates are rega's), which is
exactly why it belongs in the gate rather than in my own ad-hoc runs.
2026-09-27 18:14:38 +00:00
Doxa Maps
89ff223846 Rudder with no engine running is quiet in dev: the dev backend answers offline instead of 500
dx-shepherd (R500): with no rudder-engine process the dev proxy returned 500 ECONNREFUSED for
/api/rudder/{devices,events}, which showed as console errors. While a backend is down, backendDevPlugin now
answers its routes itself: /events streams one status event (engine offline, 'The desktop engine is not
running.'), which Rudder renders as its own state, and other routes return an empty JSON list. Running
engines are proxied as before.
2026-09-27 10:05:45 +00:00
Doxa Maps
e53495dfcf The dev server's manifest carries stage, heavy and widget, so Rudder's corner mounts under bun run dev
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
rega (7a7ca67b): on the dev server the corner widget never mounted, because /__plugin-gallery/manifest.json
built its profiles without the .meta.json fields only generate-tree read. Both now read them through one
module, internal/profile-meta.mjs (stage, heavy, widget per profile; bundle stage = production if any
profile says so), so the dev Studio and the built Studio cannot disagree. Verified: the dev model lists the
same 7 production bundles and rudder-voice:bottom-right; the built manifest is unchanged.
2026-09-27 09:41:26 +00:00
Doxa Maps
b22df1381e Mark 5 in five more languages, each a cited public-domain translation
rega measured the Arabic packet (row 83) and found page 8 still English. dx-shepherd's rule, which this
follows exactly: NEVER machine-translate Scripture — use a published public-domain translation per locale,
cited with its name (END-DXS-MK5GO).

  ar  Arabic Van Dyck Bible        ru  Russian Synodal Bible     es  Reina Valera 1909
  de  German Luther Bible 1912     fr  French Ostervald Bible

Fetched from bible.helloao.org, which ships licenceUrl + website + sha256 per translation and points at
ebible.org's own licence pages. Full provenance — translation id, name, licence URL, sha256 — is recorded in
data/people-groups/scripture-sources.json, because a credit line on the page can carry a NAME but not a
verifiable source, and the two are different claims.

★ VERSE NUMBERING VERIFIED AGAINST THE ENGLISH, not assumed. All five extract exactly verses 1-20 of 43 in
the chapter; verse 1 matches the WEB's "they came to the other side of the sea, into the country of the
Gadarenes" and verse 20 ends at "Decapolis ... everyone marveled". Lengths land within 1,689-2,399 chars
against the English 2,141, which is the spread you would expect across languages rather than a truncation.
The English passage carries no verse numbers, so these are joined as continuous prose the same way.

⚠ AND I DID NOT WRITE ANY OF IT FROM MEMORY. Scripture reproduced from recall is machine translation with
extra confidence — the same failure the rule forbids, one step removed. Every character came from the API
and was checked against the English before storing.

hi/zh/pt/it/ro STAY ENGLISH and stay in DECLARED_ABSENT, which is now narrowed rather than deleted: PD
candidates exist (Hindi 1851 Serampore-lineage, Chinese Union Version 1919, Almeida 1819, Diodati 1649,
Cornilescu 1924) but their licence status varies by jurisdiction and I could not confirm it. The entry
shrinks one locale at a time as each is cited.

VERIFIED: check:packet-strings-parity PASS, and the ar and de packets both render 9 sheets PASS with the
passage and its credit on page 8.
2026-09-27 09:07:17 +00:00
Doxa Maps
1775f5acb5 Corner widgets: a plugin can declare a fixed corner of the Plugin Studio, and Rudder takes bottom-right
BUILD-CORNER-WIDGET-CLASS-RUDDER-IN-PLUGIN-STUDIO (design approved by dx-shepherd). A profile's .meta.json
declares "widget": { "corner": "bottom-right" | ... }; generate-tree carries it into manifest.json, so the
Studio keeps no list. The Studio mounts each widget once in a fixed corner host (z-index above everything),
outside the rail and the pane, collapsed to a button, with its open state remembered. Picking it in the rail
opens the corner instead of mounting a second copy in a pane. The host and the mounted element live in a
page-level singleton on document.body, so a Studio hot update or remount re-attaches the same element and
Rudder keeps its live stream. rudder-voice declares bottom-right. Needs eyes: open, HMR, collapse.
2026-09-27 09:07:17 +00:00
Doxa Maps
a4e8b8c058 Add Rudder (rudder-voice): offline streaming voice-to-text, a desktop-target plugin
Rudder is the first plugin that isn't a map: it shows that one plugin-designs/ folder, developed live
in the Plugin Studio, can ship as a desktop app with its own local backend.

- plugin-designs/rudder-voice: <rudder-voice> element. Words stream into the end of a text area while
  you type/paste anywhere (typing holds the stream; nothing duplicated or lost). Mic picker with
  hot-plug, 3-second mic test, plain-language errors, info panel, privacy-safe debug report.
  meta.json declares "targets": ["desktop"].
- backend/rudder-engine (Go): supervises NVIDIA NeMo-Speech.cpp (ggml, CPU) with the Nemotron 3.5 ASR
  Streaming GGUF over a loopback WebSocket; records with parec/pw-record only while recording; recovers
  dead Bluetooth mic links (reopen -> CVSD voice profile -> optional RUDDER_BT_RESET) and starts
  headsets on CVSD. No telemetry: traced with strace, zero sockets/DNS while transcribing.
- Privacy: log lines redact MAC addresses and the home folder, transcripts are never logged, Host check
  against DNS rebinding, CORS only for the desktop webview, random API key for the speech server.
- internal/backend-dev-plugin.mjs + vite.config.js: `bun run dev` starts built plugin backends and
  proxies /api/<name> same-origin, so a plugin calls the same API in the studio and on desktop.
- backend/rudder-engine/setup-runtime.sh: fetches the runtime and model pinned by SHA-256.

Not yet: the Tauri desktop build (sidecar) and the PocketBase word-correction table.
2026-09-27 08:23:17 +00:00
Doxa Maps
524e5ebfae Translate the new packet pages into all eleven locales
The gate caught what I shipped: rendering the Tibet packet at PACKET_LANG=ar failed with "the funnel page
carries Latin-script prose in a ar packet: 'This packet is', 'UPG-', 'lists … How each number is reached'".
Exactly right — my new strings were English-only, and two of them PRINT on a page whose siblings
(funnel_title, funnel_intro, funnel_people) are fully translated. English text on an otherwise Arabic page
is not a graceful fallback, it is a half-translated page.

So all 17 new keys are now genuinely translated across es fr pt de it ro ru ar hi zh — 150 values — and
DECLARED_ABSENT is back to its original two entries (the Mark 5 passage and its credit). The declaration
mechanism was the right call while they were untranslated and the wrong place to leave them.

⚠ 'UPG-' IS ALLOWLISTED, AND THE ORDER MATTERS. It is the Latin run left by "UPG-100", the product name every
locale keeps verbatim — the same reason 'Project' is already in LATIN_OK. But it is allowed only AFTER the
real translations landed: an allowlist entry added when the failure first appeared would have silenced the
gate on fifteen genuinely untranslated strings. The allowlist is for names, not for work not yet done.

VERIFIED at the rendered layer in BOTH locales: en and ar each PASS with 9 sheets, map painted, content
intact. The Arabic packet carries its life-calendar title, christian-share title, both legend labels, the
three prayer columns and the "1 of 28" tip in Arabic, with 2 grids and 32 shaded cells — the same numbers as
the English one, which is the point: the arithmetic is language-independent and only the words change.
check:packet-strings-parity PASS.
2026-09-27 07:37:26 +00:00
Doxa Maps
e214cc2b7b LOC-ENUS counts the named constant, so folding the literal is not mistaken for fixing it
Folding eight en-US call sites onto library/utils/formatCount.js took the matrix from 13 warns to 3,
and that looked like progress. It was not: the rule matches a LITERAL 'en-US', the helper calls
toLocaleString(COUNT_LOCALE), so my eight sites went INVISIBLE while the behaviour was untouched — the
same false-green as a precondition that skips the file under test. The audit had stopped seeing a
decision that is still in force.

The rule now also matches the constant's DEFINITION. One site, attributed to every bundle that imports
it, which is what "held in one place" should read as: 5 warns —
  library/utils/formatCount.js:26   against research-dashboard and people-groups-grid
  RegionCountryMap.vue:394          regions' held Driver decision
  pplr-coverage.vue:128, pplr-pins.vue:60
regions' catch-fallback discrimination is untouched and its self-test stays green (rc=0).

Gate only, no product code, no rebuild.
2026-09-27 07:37:26 +00:00
Doxa Maps
f3b7e25e49 check:experimental-shelf strips the Mapbox preload under MapLibre again
generate-tree's preload comment now ends "...the map draws with MapLibre and this line must go", but the
gate still looked for "remove this line", so under MapLibre it kept the preload and every arm reported a
Mapbox request that no real MapLibre host makes (rega, 1173a992). It matches either wording now.
2026-09-27 07:37:26 +00:00
Doxa Maps
de243597db The prayer packet the Driver asked for: life calendar, the same grid as a percentage, a triangle
Driver, 2026-09-27, in his own page order. Nine pages now, was seven.

  1  map            the zone's vertical one-pager (unchanged)
  2  LIFE CALENDAR  100 years x 52 weeks = 5,200 boxes — a whole human life on one page
  3  THE SAME GRID  shaded by the evangelical share of these groups' POPULATION
  4  funnel         now an UPSIDE-DOWN TRIANGLE with an equation sidebar and a "1 of 28" tip
  5..n groups        20 per page with photos AND three prayer tick columns
  then the 12 prayer points and the prayer wheel (both unchanged)

★ THE 12 PRAYER POINTS WERE NOT LOST. The Driver believed they had gone; all 12 are intact in
packet-strings.json with his rhythms and sub-titles — praise/Two-Cross Gospel, wait/Holy Spirit &
deliverance, confess, read/the Father's love, ask/baptisms, intercede/laborers & shepherds,
pray_word/Persons & Houses of Peace, thank/rest, sing, meditate/communion, listen/accountability,
praise_end/great-greater-greatest — plus the Mark 5 passage and the Metacamp attribution.

★ PAGES 2 AND 3 ARE ONE GRID READ TWICE, which is the whole idea. 5,200 is the Driver's own arithmetic
(100 x 52), not a layout choice, so both pages derive from it and cannot drift into two grids that merely
look alike. A percentage drawn as a bar is an abstraction; drawn on the grid the reader has just
understood as their own life, 32 shaded boxes of 5,200 is the same fact made unavoidable.

★ AND PAGE 3's NUMBER IS POPULATION-WEIGHTED, because he asked for "% of Christian population vs lost".
That matters: by GROUP COUNT 69.1% of the 2,091 matched DOXA groups have no known evangelicals, but by
POPULATION the evangelical share is 0.61% of 209,079,159 people. The group-count version is a materially
milder picture of the same data. Computed in data/people-groups/christian-share.json, joined IMB
CSV ROP3Code+ISOAlpha3 to DOXA's snapshot — the same join the Vision Card uses — and READ here, never
re-derived, so one fact has one source.
⚠ THE BAND MIDPOINT IS AN ASSUMPTION AND THE PAGE SAYS SO. peoplegroups.org publishes EvangelicalLevel
as a BAND ("Less than 2%", "No Known Evangelicals"), never a number, so any percentage requires choosing
a value inside each band. The file records the method, the page prints it, and 0.61 + 99.39 = 100 exactly
— my first formatter rounded the large side to "99%", which loses the asymmetry that IS the point and
leaves a reader checking the arithmetic to find the page wrong.

⚠ GEOMETRY MEASURED, NOT EYEBALLED: square cells would need 26.5px x 100 rows = 2,654px of grid in
1,521px of printable height, overflowing the sheet 1.7x. 13px rows put 100 rows in 1,498px with 23px
spare. Caught by computing the height before rendering.
⚠ AND THE TIP SAID "1 of 27". listCount spread the three cluster keys and omitted `deaf`, whose two
deaf_lists entries buildLegendNodes collapses into one leaf — the same omission allZones() had before
3ef164c0, and this was its last copy. It prints on the page, so a 27 there is a visible wrong fact.

THE GATE IS TAUGHT, NOT TOLERANT: the two new sheets are DETECTED (.sheet-life/.sheet-christian) and
added as known terms, exactly as the funnel was, because that assertion exists to catch silently dropped
people groups and a ±1 tolerance is the slack a lost list page hides in. New assertions: 5,200 cells per
grid, the shaded count neither 0 nor 5,200 (either means the percentage never arrived), and exactly
3 x cards prayer boxes. Probes scoped to .lc-grid — the legend swatches share the cell class and made the
count 5,201 on the first run.
The 17 new strings are English-only and DECLARED_ABSENT with reasons rather than back-filled: English
copied into ten locale blocks would read as translated to this gate and to a reader, and the per-key
fallback already renders English without the lie.

VERIFIED: check-packet-render PASS (9 sheets, map painted, content intact) and
check:packet-strings-parity PASS on the Tibet fixture.
2026-09-27 07:37:25 +00:00
Doxa Maps
8d739edfcd Unblock studio's landing 13: drop the dead fallback, make the zoning gate measure on a clean tree
Both halves of studio's L13U refusal were correct and both are mine.

1. THE LEGACY upg100WallIntro FALLBACK IS GONE. It existed for exactly one window — the three
   plain-text keys were landing in studio's catalogue while this consumer was already written, so the
   branch rendered the old markup-bearing key until they arrived. They are on main in all eleven
   locales, so the condition can no longer be true: it is DEAD CODE that keeps a key alive, because
   studio cannot delete upg100WallIntro while a line still reads it. A fallback whose condition cannot
   fire is not safety, it is a reference.
   The three remaining mentions in this file are COMMENTS, not code — verified that no live call site
   anywhere in plugin-designs/ or library/ reads it, so it is now safe to delete.

2. THE GATE DEFAULTED TO A GITIGNORED FILE. check:zoning-declared read
   library/scripts/posters/out/_records.json, which .gitignore:94 excludes, so on a clean checkout it
   exited 75 — NOT MEASURED forever, indistinguishable from an unwired gate. studio measured exactly
   that and correctly refused to wire it into gates:post. This tree has been here before:
   check:packets-all SKIPped on every build for weeks for the same reason.
   ★ THE FIX IS A COMMITTED FIXTURE, NOT A LOOSER GATE. data/people-groups/zoning-records.json carries
   slug plus every field a selector actually reads — 2,107 records, ~300 KB. country_code alone would
   not do: the deaf zone reads rop1 and five African/European zones read wagf_block/wagf_region, which
   is the same omission that made six zones silently resolve to nothing in the packet generator.
   The live cache is still PREFERRED when present, so a local run measures the freshest data and CI
   measures the committed one.
   VERIFIED on a genuinely clean tree (git archive of the staged tree, no cache, no network):
   PASS — 28 zones, 2,107 records, 2,096 covered, all five assertions green, exit 0.

check:i18n-keys-exist PASS, check:sfc-parse PASS.
2026-09-27 06:57:00 +00:00
Doxa Maps
d87ee852fe check:composite-interop: two known-issue entries outlived the bug they described
The gate failed on main 12a64702 with two LATE_KNOWN entries that no longer match anything:
dashboard-plus-grid.vue#reflectDir and research-map-plus-grid.vue#reflectDir. Landing 11 fixed both —
reflectDir now captures the instance in setup, so a host switching lang to ar after mount does get
dir=rtl — and the exemptions stayed behind.

That is the gate working as designed: it refuses an exemption that has stopped describing anything,
because a stale entry is a defect it would quietly stop reporting. Both entries are deleted, and the
note left in their place says why rather than leaving a bare gap.

Gate only, no product code, no rebuild. composite-interop rc=0 after.
2026-09-27 06:42:55 +00:00
Doxa Maps
d024b855bd check:composite-embed-knobs: a composite with no map is N/A, not unmeasured
bp-big-picture could not be measured "because there's no canvas under a minimal config", and that was
the honest answer to a question it should never have been asked. Big Picture nests NO MAP: it composes
a research header and a people-groups grid, and its built bundle contains zero occurrences of
mapbox-gl or maplibre-gl. There is no canvas to draw and no Mapbox half to prove.

The target list was wrong, not the gate. A profile qualifies if `composes` names a map bundle OR the
type is nested-elements-mfe; that second arm exists because country-posters-mfe bakes its children and
names nothing in `composes`, but it is loose enough to admit composites that nest no map at all.

Now a non-`composes` candidate must also SHIP an engine, decided on the BUILT artefact rather than the
manifest, because whether a bundle compiled a map engine is a fact about what it built. Map-less
composites are printed as N/A so they stay visible instead of being silently dropped.

MEASURED: exactly 2 excluded — bp-big-picture (big-picture) and stack-demo — both genuinely map-less.
No other owner's map target is affected, so the set goes from 9 targets with 2 permanently unmeasured
to 7 real targets plus 2 declared N/A.

Gate only, no product code, no rebuild. Still needs a browser and a token to run: rega's, not mine.
2026-09-27 06:42:55 +00:00
Doxa Maps
c4bcb1407d check:composite-embed-knobs opens country-posters-mfe on its map view
country-posters-mfe now reads profile-config view (exp-shepherd, 66837586); it opens on posters, so the gate
passes view:"map" to have a canvas to measure. Browser gate: rega confirms the unmeasured cases drop.
2026-09-27 06:42:54 +00:00
Doxa Maps
57b0200bb6 The built tree ships every people-group list, so ?list= draws pins
generate-tree's asset allow-list staged people-groups-slim and -points but not people-groups/lists/*.json,
so on the built tree every non-default list (frontier-500k, frontier, unreached, unengaged) 404'd and the map
drew 0 pins (go-int / rega at a71007f1). The lists folder now ships whole, like tables/: the fetch name is the
list id. Verified on a regenerated tree: all four files staged; check:list-prop-unit PASS.
2026-09-27 06:04:09 +00:00
Doxa Maps
24b9ada48e Plugin Studio and the gallery split production from experimental, from each bundle's own stage
The Driver: a public plugin that is not yet a production map shows under Experimental until it is. A
profile's .meta.json declares "stage": "production" | "experimental" (default experimental; a bundle is
production if any profile says so) and "heavy": true for a live WebGL owner. generate-tree carries both into
manifest.json, so the Studio and the gallery read the same field and neither keeps a list.

The seven production bundles are marked: dashboard-plus-grid, research-map-plus-grid, regions-dashboard,
my-upg-100-list, doxa-simple-map, cities-internet, gospel-resources. The Studio rail keeps its capability
sections for them and puts every experimental bundle (gospel-internet among them) in one final Experimental
section. The gallery's auto-discovered Experimental folder skips a production bundle. gospel-internet-board
joins HEAVY_ELEMENTS (it can own two WebGL contexts). check:studio-rail asserts the split.
2026-09-27 06:04:08 +00:00
Doxa Maps
65faa2d971 Keep the square panel together on a phone
A media query adds no specificity. The mobile block's
`.map-toolbar { gap: 6px }` is (0,1,0) exactly like
`.map-toolbar--square { gap: 0 }`, and it comes later in the sheet, so it
won: on every phone a square toolbar fell back to separate buttons inside
one ringed panel. The shape broke precisely where the Driver is most likely
to look at it.

The --no-geocoder pair is (0,2,0) and outranked the variant whatever the
order, so it needs the two-class answer.

This is the same lesson as the goal cards' gap-vs-margin: a ported mechanism
is only ported if it still WINS, and specificity is the part that decides
whether it does. Found by following the phone thread rather than by looking
at the variant again.

check:square-map-controls now asserts the override is inside the mobile
block, that the two-class form answers --no-geocoder, and -- as a control --
that the base rule really does come first, which is why the override is
needed at all. Removing it turns the gate red.

The CSS comment explaining it cost one more mistake: I wrote the selectors
in backticks, inside the template literal that holds the stylesheet. A
backtick ends the literal, which is the trap check:sfc-parse names in its own
failure text, and it caught me with it.
2026-09-27 06:04:08 +00:00
Doxa Maps
ef9e1775d9 check:universal-props: a bundle's source is not only its own folder any more
THE MATRIX WAS UNDER-REPORTING, AND MY OWN KIT LANDING IS WHY. This gate scanned
plugin-designs/<bundle>/** and nothing else, which was correct while every panel lived inside its
plugin. The dashboard-kit extraction moves panels into library/components/dashboards/ and the bundle
still COMPILES them, so a source rule stopped firing the moment the file moved, with the code
unchanged. Measured: kit landing 4c-3 moved DashboardFacetRail.vue, which holds three
toLocaleString('en-US') calls, and the matrix went from 13 warns to 10. Nothing improved; the audit
stopped looking. LOC-ENUS has no `shipped` matcher, so the built-bundle pass could not catch them
either.

This is not three lines. Every kit extraction, and every future one, silently shrinks this audit —
the more code becomes shared, the less of it this gate sees. So each bundle is now audited over the
library files it actually IMPORTS, resolved from its own `@map/...` specifiers and followed
transitively. Attribution stays per-bundle, which is what keeps the matrix actionable: a library file
compiled into four bundles is reported against all four, because all four ship it.

★ MY FIRST VERSION OF THIS WOULD HAVE TURNED THE BUILD RED WITH FOUR FALSE POSITIVES, and finding
that is the reason I measure a gate's blast radius before handing it over:
- library/api/sources.json got swept in and LIST-FETCH reported it three times as "a file that
  fetches the list". It is the endpoint REGISTRY — a .json file declares endpoints and fetches
  nothing. The per-bundle walk has an extension filter; my sweep did not. Now it does.
- library/api/DataSourceManager.js:464 was reported for LIST-DSM, on the line
  `const dataSourceManager = new DataSourceManager()` — the singleton the module exports. That rule
  exists to catch a CALLER constructing one without profileConfig. A module is not a bypass of its
  own contract, and reporting it told three owners the library was broken because the gate read a
  definition as a use.

AFTER, on main: 13 warns (was 10), 0 bypasses, PASS, and 19 bundles audited instead of 18 — following
imports gave one more bundle a row. Every restored warn is LOC-ENUS, which is warn-only by the
Driver's held numerals decision, so nothing here asks anyone to change a number.

Gate only. No product code, no rebuild. check:universal-props is not in gates:pre or gates:post, so
this cannot turn a build red on its own; it is run directly and by check:studio-all.
2026-09-27 06:04:08 +00:00
Doxa Maps
833cc691a2 The locale warn was flagging the code that honours the locale
LOC-ENUS flagged any en-US format literal. Two of regions-dashboard's three
warns were the 'en-US' inside a CATCH, whose try formats with
`${locale}-u-nu-latn` -- a verbatim clone of DX-life's RegionDetail.vue:16,
Latin digits with the reader's grouping. The fallback exists because a bad
locale tag throws RangeError in toLocaleString and blanking the page is
worse than one wrong separator.

So the matrix's locale column said "ignores the locale" about code that
honours it, which is the kind of false warn that teaches people to stop
reading the column.

Measured before changing the rule: of the NINE LOC-ENUS sites in the tree,
exactly TWO are such fallbacks. The other seven are genuinely operative and
still flagged, so the tree goes 10 warns -> 8 and only those two move.

The exemption needs BOTH halves -- a catch AND a locale-derived primary --
because a lone catch must not excuse a format that never consulted the
locale. That is the one rule here that can widen by accident, so four
adversarial shapes are pinned as a self-test: the real pattern is skipped,
while a catch around a hardcoded primary, a catch whose primary never
consulted a locale, and a plain operative call are all still caught.
Loosening the exemption to a bare catch makes the self-test exit 3.

regions-dashboard keeps ONE warn: RegionCountryMap's popup, fixed en-US on
purpose, carded and evidenced by DX-life's own convention. I did not add an
ALLOW for it -- ALLOW is keyed per BUNDLE, so it would also blind regions to
a future operative en-US, and a visible warn on a decision already taken is
cheaper than a blind spot.
2026-09-27 06:04:08 +00:00
Doxa Maps
d888b20d8b Gate the square controls against the engine's own stylesheet
check:square-map-controls, registered with a gates:post token.

It reads node_modules/maplibre-gl/dist/maplibre-gl.css and compares our
ported values against THAT FILE rather than against a remembered number,
which is possible only because the look the Driver asked for is the engine
default: DX-life adds NavigationControl and overrides nothing, so the target
is published.

It asserts the three deliberate deviations as deviations -- the engine's
29px is not adopted, there is no overflow:hidden, and a forced-colors block
ships -- so each stays a stated choice rather than decaying into a mistake
nobody can see.

And it asserts the blast radius: round is the default and none of the nine
MapToolbar consumers passes controls=. Opting demo-map in turns it red,
which is what makes the silence mean something.

Its CSS assertions read the text passed to useShadowStyles, not the file.
The first run went red on "we do not adopt 29px" because the prop's own doc
comment quotes the engine's rule, 29px and all -- a checker that cannot tell
a comment from a declaration keeps finding the documentation it stands on.
2026-09-27 06:04:08 +00:00
Doxa Maps
56fd4fba79 check:universal-props: key-cities no longer needs its engine exemption
key-cities now resolves its engine through the library (exp-shepherd, 71af822a, on main at c2784f5a), so the
'key-cities:ENG-STYLE' allowance is stale. Removed; the gate passes against main's key-cities (18 bundles),
and it fails on the pre-change file, so this commit must land on or after c2784f5a.
2026-09-27 06:04:07 +00:00
Doxa Maps
e9e1adc0d6 check:shadow-css-parses reads the style id: it must be a plain identifier
gospel-plugin's trap (COMMON.md): a CSS rule moved past the first argument of useShadowStyles concatenates
onto the style id ('@media(...){...}doxa-gospel-map'). That is valid JS, so the build stays green and the rule
is silently lost; the brace-balance scan never sees it because the id is not CSS. The walker now folds the
second argument too, and a literal id must match /^[a-z0-9-]+$/ (a computed id is left alone). Measured on
the current tree first: 0 offenders, so this breaks no owner. Self-tests cover the trap and a plain id.
From dx-shepherd (END-DXS-CSSID).
2026-09-27 06:04:07 +00:00
Doxa Maps
d5a95e97b8 merge main (gospel a37ea4ff) into packet/vertical
Six public/ conflicts, zero in source. They take main's side here and are regenerated from the merged
source in the next commit — a bundle is an output, and a hand-reconciled middle version is wrong by
construction.
2026-09-27 05:56:25 +00:00
Doxa Maps
7b6d02a568 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	package.json
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-27 05:46:57 +00:00
Doxa Maps
4ee5c02c8b merge main (landing 10 + the web-sized boundaries) into packet/vertical 2026-09-27 05:07:45 +00:00
Doxa Maps
d4bd75f4ba Library index: a data file is described by its own _description or _meta, not its first line
generate-library-index took the first line of every file as its description, which for a .json file is data:
the Data section read '"_meta": {', '"value": "AFG",' and a run of ISO codes (flagged by simpleresearch).
A .json file now uses _description, then _meta.description, then _meta.purpose, and otherwise says
'(no description)'. iso3-to-alpha2 now reads as what it is.
2026-09-27 05:05:04 +00:00
Doxa Maps
8c7a52ed6f Every shadow root honours prefers-reduced-motion
A page's own reduced-motion reset never crosses a shadow boundary, so library components rendered inside
custom elements slid, faded and spun for readers who asked for less motion: thirteen of them, including the
legends and map controls (regions-plugin found it on ProgressBar and named the class).

useShadowStyles now injects one prefers-reduced-motion reset into each shadow root it styles, which covers
every component styled through it, including later ones. PosterDialog, FilterDropdown and SearchFilterHeader
style themselves with <style> blocks and carry their own guard.

check:shadow-reduced-motion (in gates:pre): useShadowStyles must inject the reset, and every animating
library component must be covered. It fails without the reset (negative-tested) and found the three
<style>-block components on its first run.
2026-09-27 05:05:04 +00:00
Doxa Maps
a267b89583 The Research Gallery loads behind a login gate: same-origin fetches keep their credentials
Behind any login gate (basic auth, a session cookie, OIDC) the gallery said "Could not load Big Picture":
doxa-maps-spa fetched its same-origin manifest, and plugin-studio its token URL, with credentials:'omit', so
the gate's cookie never went and the request was refused (found on the new dev container; dx-shepherd).
Both now use 'same-origin', which never sends credentials cross-origin, so CDN and CORS behaviour is
unchanged.

check:no-credentials-omit (+ :self-test, in gates:pre) fails any credentials:'omit' in library/ or
plugin-designs/ unless the line says why (// credentials-omit: <reason>). Measured before: exactly these two
sites; it fails on the old code and passes on the fix.
2026-09-27 05:05:03 +00:00
Doxa Maps
0997252432 check:setup-order arm C: getCurrentInstance() read from a watcher, and the early-out that hid it
Arm C catches the live RTL defect in the two plus-grid composites: a top-level function that reads
getCurrentInstance() and is handed to a watcher or an addEventListener runs with no current instance,
so the read returns null.

IT MODELS VUE RATHER THAN BANNING THE CALL. Inside setup, and inside a lifecycle-hook callback, the
call is CORRECT — Vue sets the instance for the duration of a hook — so research-map-plus-grid.vue's
onMounted(() => { getCurrentInstance()... }) is deliberately not reported. A gate that flags working
code gets ignored, and this one has already had to drop five such hits.

★ AND ITS OWN EARLY-OUT MADE IT USELESS FIRST, WHICH IS THE SECOND TIME TODAY FOR THIS EXACT HOLE.
The file filter only ran the arms on sources containing `immediate: true` or `async` — arm A's and
arm B's triggers. Neither of the two files carrying the RTL bug contains either word, so both were
skipped before a single arm ran, and the gate reported the tree clean while the self-test stayed
green. A fixture is text and never meets the file filter, so no self-test could have caught it; only
running the gate against the un-fixed code did, which is why dx-shepherd asked for exactly that.

An hour earlier I diagnosed the same shape in check:i18n-keys-exist — it skipped any file that never
wrote the word "tStatic", which is how a planted poison key passed — and wrote up the rule that a
cheap early-out is part of a gate's COVERAGE, not an optimisation. Then I wrote the hole again here.
So the fix is structural rather than a correction: the precondition is now DERIVED from a per-arm
ARM_TRIGGERS table, so adding a fourth arm forces you to extend the filter in the same edit.

One more false positive the self-test caught before it shipped: `watch(textDir, reflectDir)` has no
inline callback, so brace-matching forward from the call walked past it into the NEXT statement's
onMounted body and reported a getCurrentInstance() nowhere near a watcher. A call's callback is now
bounded by that call's own parenthesis span.

Verified both directions, on the real files and not only on fixtures: FAILS on the shipped code
naming both composites, PASSES with the fix, and does not flag the legal onMounted read.

Gate only, no product code, no rebuild — rule 5B because it lives in internal/.
2026-09-27 05:05:03 +00:00
Doxa Maps
4edd97407a Keep the square panel together on a phone
A media query adds no specificity. The mobile block's
`.map-toolbar { gap: 6px }` is (0,1,0) exactly like
`.map-toolbar--square { gap: 0 }`, and it comes later in the sheet, so it
won: on every phone a square toolbar fell back to separate buttons inside
one ringed panel. The shape broke precisely where the Driver is most likely
to look at it.

The --no-geocoder pair is (0,2,0) and outranked the variant whatever the
order, so it needs the two-class answer.

This is the same lesson as the goal cards' gap-vs-margin: a ported mechanism
is only ported if it still WINS, and specificity is the part that decides
whether it does. Found by following the phone thread rather than by looking
at the variant again.

check:square-map-controls now asserts the override is inside the mobile
block, that the two-class form answers --no-geocoder, and -- as a control --
that the base rule really does come first, which is why the override is
needed at all. Removing it turns the gate red.

The CSS comment explaining it cost one more mistake: I wrote the selectors
in backticks, inside the template literal that holds the stylesheet. A
backtick ends the literal, which is the trap check:sfc-parse names in its own
failure text, and it caught me with it.
2026-09-27 04:58:24 +00:00
Doxa Maps
4f607198c1 The locale warn was flagging the code that honours the locale
LOC-ENUS flagged any en-US format literal. Two of regions-dashboard's three
warns were the 'en-US' inside a CATCH, whose try formats with
`${locale}-u-nu-latn` -- a verbatim clone of DX-life's RegionDetail.vue:16,
Latin digits with the reader's grouping. The fallback exists because a bad
locale tag throws RangeError in toLocaleString and blanking the page is
worse than one wrong separator.

So the matrix's locale column said "ignores the locale" about code that
honours it, which is the kind of false warn that teaches people to stop
reading the column.

Measured before changing the rule: of the NINE LOC-ENUS sites in the tree,
exactly TWO are such fallbacks. The other seven are genuinely operative and
still flagged, so the tree goes 10 warns -> 8 and only those two move.

The exemption needs BOTH halves -- a catch AND a locale-derived primary --
because a lone catch must not excuse a format that never consulted the
locale. That is the one rule here that can widen by accident, so four
adversarial shapes are pinned as a self-test: the real pattern is skipped,
while a catch around a hardcoded primary, a catch whose primary never
consulted a locale, and a plain operative call are all still caught.
Loosening the exemption to a bare catch makes the self-test exit 3.

regions-dashboard keeps ONE warn: RegionCountryMap's popup, fixed en-US on
purpose, carded and evidenced by DX-life's own convention. I did not add an
ALLOW for it -- ALLOW is keyed per BUNDLE, so it would also blind regions to
a future operative en-US, and a visible warn on a decision already taken is
cheaper than a blind spot.
2026-09-27 04:54:30 +00:00
Doxa Maps
724ec0b474 Gate the square controls against the engine's own stylesheet
check:square-map-controls, registered with a gates:post token.

It reads node_modules/maplibre-gl/dist/maplibre-gl.css and compares our
ported values against THAT FILE rather than against a remembered number,
which is possible only because the look the Driver asked for is the engine
default: DX-life adds NavigationControl and overrides nothing, so the target
is published.

It asserts the three deliberate deviations as deviations -- the engine's
29px is not adopted, there is no overflow:hidden, and a forced-colors block
ships -- so each stays a stated choice rather than decaying into a mistake
nobody can see.

And it asserts the blast radius: round is the default and none of the nine
MapToolbar consumers passes controls=. Opting demo-map in turns it red,
which is what makes the silence mean something.

Its CSS assertions read the text passed to useShadowStyles, not the file.
The first run went red on "we do not adopt 29px" because the prop's own doc
comment quotes the engine's rule, 29px and all -- a checker that cannot tell
a comment from a declaration keeps finding the documentation it stands on.
2026-09-27 04:51:19 +00:00
Doxa Maps
ff15e86ab7 Merge main (f4871401) after landing 10
All of batch 2, the CSS-syntax gate, both country-code fixes and the CLDR
country-name work are on main, byte-identical -- verified by comparing the
files, since studio re-applies rather than merges and my shas are not
ancestors.

Six conflicts were GENERATED artifacts: my 807d68d0 rebuild against the
build owner's. Took main's for all six. That commit was always
provisional -- studio said their landing rebuilds every consumer -- and a
generated file is the one kind you resolve by regenerating, never by
reading.

package.json needed two unions rather than a side. gates:pre: main brought
check:setup-order and check:package-json-unique-keys, I brought nothing new,
so main's 8 tokens stand. The docs hunk looked like a contest but was a
POSITIONAL clash between two DIFFERENT keys -- my religion entry and main's
check:setup-order -- so both are kept; picking a side would have deleted a
description nobody was arguing about.

Then I introduced the exact defect I had reported an hour earlier: keeping
main's gen:iso3-to-alpha2 line when my side already carried it, twice over,
in the scripts block and the docs block. main's NEW check:package-json-
unique-keys caught both and named the object. That gate did not exist when I
filed the duplicate-key finding; it does now, and it graded me first.

reference-library-index.md regenerated, 116 items. 11 gates green.
2026-09-27 04:38:19 +00:00
Doxa Maps
63bd15713b 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-27 04:37:06 +00:00
Doxa Maps
873d73f5eb 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-27 04:17:14 +00:00
Doxa Maps
f1b33af772 Validate the CSS we build as JS strings
check:sfc-parse proves a .vue script block parses. A broken stylesheet
inside a valid string literal is still a valid string literal, so it
passed a sheet with 9 declarations stranded outside any rule and braces
at 12 open / 21 close. Nothing else in the tree validated CSS syntax.

check:shadow-css-parses reads every useShadowStyles() argument and scans
the result as CSS: brace balance, declarations stranded at depth 0, empty
selectors. 72 stylesheets across library/ and plugin-designs/.

It reads the arguments off the @babel/parser AST the build already uses,
because three hand-rolled lexers each produced a confident FALSE RED on a
file that was fine: a /* CSS comment */ read as a stranded declaration; an
apostrophe in a .vue prose header opening a phantom string literal, so a
</div> from the template was reported as CSS; and a regex literal
containing a quote, which cannot be lexed without resolving regex-vs-
division. The CSS scan itself stays hand-written, because CSS is the thing
being checked -- and it consumes CSS comments and quoted values as their
own token types, so content:'a{b' opens no rule.

9 self-tests run inline on every invocation, including the exact damage
above, so the gate cannot quietly stop being able to fail. An argument
built partly from an imported constant is scanned for its literal
segments and labelled, rather than skipped -- skipping it would have
excluded region-dashboards.vue, which builds its sheet from the token
layer plus literals.
2026-09-27 04:14:32 +00:00
Doxa Maps
812e1029e4 check:package-json-unique-keys: a duplicated key in package.json fails
JSON.parse keeps the last of two equal keys and says nothing, so a duplicate silently kills the first entry
while every gate that reads the parsed object passes. "check:religion-label-not-value" sat twice in
//scripts after a merge (found by regions-plugin; only Bun's parser warned). A small scanner tracks keys
per object; its self-test runs on every invocation, and a planted duplicate is caught. In gates:pre.
The landing tooling's package.json union now refuses to write a result with a repeated key.
2026-09-27 04:14:31 +00:00
Doxa Maps
25c102f5cd Kit 4c-1: the counting half, and the bucket that said "Other" in every locale
useDashboardAggregations has a split personality, and only half of it could ever
move. `lbl` and `aggregate` are pure counting — eleven lines, a Map, a sort, a
topN rollup, no facet name and no colour. Everything else in that file is
research: `colorFor` with its hardcoded religion / wagf_region / affinity_bloc
branches, the religion palettes, the donut and bar builders. Those stay, and
DashboardChartsRail keeps importing them.

The generic pair was what blocked kit landing 4c: the facet rail needs only that
half, and while it lived beside the palettes the rail could not follow it into the
library. Same split, same one-cycle re-export as 4a. Bodies verbatim.

★ AND THE ROLLUP BUCKET SAID "Other" IN EVERY LOCALE. aggregate() pushes
`{ key: 'Other', other: true }` for the tail, and that English word was rendered
directly by the facet rail and the charts rail. check:i18n-keys-exist could not
see it, because there was no T() call to inspect — the string was never a lookup,
it was a KEY being used as a LABEL. I found it while ASSESSING this landing rather
than while doing it, and it is fixed here rather than later for one reason: the
moment this module becomes kit code, an unfixed bug in it is exported to every
dashboard built from the kit.

THE TWO HALVES PULL IN OPPOSITE DIRECTIONS, which is the whole difficulty. The KEY
must stay English: the rail tests it to make the row inert, and the charts rail
refuses to filter on it, so a localized key would make the bucket CLICKABLE in
Arabic and filter on a value no row carries — an empty board, reading as a data
problem. The LABEL must not stay English. So OTHER_KEY is the sentinel,
otherLabel() reads dashboard.other, and they live next to the function that
produces the bucket so they cannot drift. Eleven locales, catalogue first.

This is the value/label trap for the third time — religion families, the language
tiers, and now a bucket in generic code. The third is the worst place for it,
which is why the rule now travels WITH the code that creates the value rather than
being remembered at each render site.

check:legend-mirrors grows arm 4, asserting both directions: translate the
sentinel and it fails; render the key instead of the label and it fails. Both
mutants run.

check-one-palette now scans the new module as well. Its subject — private palette
declarations — stayed in the plugin half, so nothing moved; adding the library
path means a palette smuggled into kit code later is caught by the same rule
instead of escaping into the library.

Library index regenerated in-commit. Source only. STUDIO: needs a rebuild of
research-dashboard and dashboard-plus-grid. The internal/ hunk is four lines in
check-one-palette.mjs.
2026-09-27 04:14:13 +00:00
Doxa Maps
2c5d9f47ec Two setup-time defects: a TDZ that blanked the map, and a cleanup that never ran
Both came from the Driver's console in the gallery's Simple Research tab, and both come from one
gap: where a line SITS in a setup block is not WHEN it runs.

research-map.vue — "Cannot access '_lastFitKey' before initialization", thrown during setup, which
left the map blank. The externalFilters watcher is immediate:true, so it runs while setup is still
on the stack and calls applyFitSlugs() 350 lines below it. The CALL resolved, because a function
declaration is hoisted; its constants are not, so the first read hit their temporal dead zone. A
hoisted function promises that its NAME resolves early, not that its BODY can run early, and that
asymmetry is invisible from both ends: the watcher looks like it only touches the store, and the
constants look like they sit right beside the function that reads them. They and the guard now sit
above the watcher, with a note saying why they cannot live next to applyFitSlugs().

DashboardMapPanel.vue — "onBeforeUnmount is called when there is no active component instance".
A lifecycle registrar closes over Vue's current instance, which exists only while setup is on the
stack, so a hook registered after "await initializeMap()" is silently dropped: a ResizeObserver on
document.body plus a window resize listener outlived the component. Nothing threw and the feature
worked, so it showed only as a studio that slowed down the more tabs a reader had visited. The fix
is not to move the line — the code that creates those handles is past the await either way. The
handles move to setup scope and are torn down in the hook this file already registers correctly,
which is the pattern it was using three lines away for containerRO and attribRO.

research-map.vue held a second copy of the same leak, its scroll-zoom resync listener. The new
gate found that one; reading the file had not.

check:setup-order covers both classes. Its own false positives are the instructive part: 6 hits
down to 1 over three rounds, and every one it dropped was prose or shadowing read as code — an
onBeforeUnmount inside a console.error STRING, the word "table" inside a new Error(...) template
literal in four files, and a destructured byCountry shadowing a computed 220 lines away. So it
blanks comments, string text and template text while keeping ${...}, which is code, and subtracts
every name a span declares for itself. It self-tests on both shipped defects, both fixes and all
three decoys, and exits 3 if an arm goes blind rather than passing quietly.

Source only. doxa-research-map, research-dashboard and dashboard-plus-grid need a rebuild.
2026-09-27 04:00:31 +00:00
Doxa Maps
d378b38653 Merge main (23976fb0) after landing 9
Batch 1's content is on main as de24e633, studio's re-application rather
than my cf8cc929, so the commit is not an ancestor and only the CONTENT
test says it landed: RegionSectionCard.vue on origin/main is byte-identical
to mine.

Three conflicts, none resolved by choosing a side:

  gates:post — both sides added tokens. Mine brought
  check:shadow-css-parses, main brought check:no-silent-engine and
  check:i18n-stopped-scope. Taking either side would have dropped the
  other's gates, so the tokens are unioned: 81, none lost.

  check-regions-uses-library.mjs — add/add. Mine is the superset; the only
  two lines unique to main's copy are ones batch 2 deliberately replaced,
  generalising the import assertion to @map/components/<dir>/<name> so
  ProgressBar in charts/ resolves.

  reference-library-index.md — derived, so regenerated rather than merged
  by hand. 113 items.

Verified after merging by content and by authorship, not by reading a diff:
all four batch-2 components, the CLDR vendor and the gate survive; the
plugin still renders them and still ships no .rd-card CSS; SJM -> SJ and
NOR -> NO hold. git log over the merge base shows only my own commit
touched region-dashboards.vue, so nothing of another agent's was at risk
there. 12 gates green.
2026-09-27 03:49:21 +00:00
Doxa Maps
e5398c072f One gate owns the source rule; this one keeps the six measurements
check:no-silent-engine is in gates:post as of main 23976fb0 and scans every plugin for
a cross-engine fallback, so this leaf's own static arm is deleted. Two gates owning one
source-level rule meant my leaf was checked twice while every other leaf was checked
once, and the narrower copy is always the one that drifts.

What stays is the part no source read can do: 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, and the engine identified from the canvas class rather than
a debug handle. All six still pass after the deletion.

--poison now NAMES where the falsifiable arm went instead of exiting 0 as though it had
self-tested — and it names it correctly: the tree-wide gate's flag is --self-test, not
--poison, which I checked in its source rather than assuming the convention my own gates
use. Its package.json description no longer claims a static arm this file does not have.
2026-09-27 03:49:05 +00:00
Doxa Maps
a681fd3c65 A zoning gate that checks a list against what its author declared, not against my intuition
Part 1 of making UPG-100 a reusable pattern (Driver via repo; dx-shepherd UPGPAT). Any project list —
7000 / 4700 / 3000 / 2000 / 700 — will be zoned by an agent who did not write this code. Zoning is human
judgement (geography first, balanced with language), so a gate cannot decide whether a zoning is GOOD. It
can decide whether it still matches what the author SAID, which turns "did I zone this right?" from a
browser question into an exit code.

★ EVERY OBVIOUS RULE I WOULD HAVE WRITTEN FAILED THE REFERENCE IMPLEMENTATION. Measured against the live
28 zones and 2,107 records BEFORE writing the assertions, which is the only reason they are right:
  "every group in exactly one zone"  → 112 groups are in two. 110 are the DEAF BLOC, and that is
                                       design-approved (legend-tree.js:151, slice 08): an affinity bloc's
                                       members also live in a geographic zone. So the rule is "at most one
                                       GEOGRAPHIC zone", with bloc zones declared as a second partition.
  "no group is unzoned"              → 11 are, all JPN/CHN/MNG, plausibly deliberate. A gate cannot tell a
                                       deliberate exclusion from a silent drop; only a declaration can.
  "no two zones overlap"             → 3 non-bloc overlaps exist, because "Scattered / Diaspora"
                                       (country_codes AND,CCK,GUF,USA) overlaps the regions it scatters
                                       across.
A gate that fails on the only correct example of the thing it checks teaches people to disable it.

⚠ AND THE GATE FOUND A THIRD OVERLAP MY OWN HAND ANALYSIS HAD MISSED: deaf-cocos-islanders, in both
oceania-indonesia and uncovered-scatter. I had filtered out every A017 group as "the design-approved deaf
double-count" — but its TWO GEOGRAPHIC claims (CCK is in both zones' country lists) are a SEPARATE fact
from its bloc membership. Conflating them is exactly what A2's bloc/geographic split exists to prevent,
and the gate was right where I was wrong. Best argument for it existing.

THE FIVE ASSERTIONS, and two would have caught this cycle's real defects before a browser did:
  A1 no zone resolves to zero groups     ← `deaf` + the 5 wagf zones were in exactly this state, found
                                           only by generating all 28 and reading the log
  A2 at most one GEOGRAPHIC zone/group   ← blocs declared; found the overlap above
  A3 orphan set EQUALS the declared set  ← and a RESOLVED orphan fails too, deliberately: leaving a
                                           now-zoned group listed means the next real drop passes
                                           silently. An unmaintained declaration stops being evidence.
  A4 every selector type is supported    ← the evaluator THROWS on an unknown type, at render time, on one
                                           zone's page; better as a build-time failure on the whole set
  A5 no zone over the declared budget    ← a wildly oversized zone is usually a selector matching too much

DECLARATIONS pre-filled with today's actuals and marked PENDING DRIVER, per UPGPAT: the 11 orphans and
the 3 overlaps are recorded so the gate is green on arrival and any CHANGE fails. Both are in
DRIVER-DECISIONS-DIGEST and neither membership was touched.

VERIFIED: --self-test PASSes 12 cases, each assertion firing on a violation AND staying quiet on the
declared exception (including the negative control that A2's bloc exemption is doing the work). Poison
test on REAL data: removing ainu-japan from the declaration makes A3 fail with that slug, so a green run
is meaningful rather than vacuous.

⚠ AND A package.json MISTAKE WORTH RECORDING. My first insertion put all three keys into the //scripts
DOC block, because re.finditer over the whole file matched check:zone-census there (//scripts comes later
in the file) — producing duplicate keys that json.load silently resolved last-wins, so the script COUNT
WENT DOWN and `npm run check:zoning-declared` reported "Missing script". Fixed by scoping each insertion
to its own block span. The diff is now exactly 3 added lines and reorders nothing: an earlier attempt that
re-serialised the sorted object moved gospel's check:embed-contract and check:engine-contract, which is
not mine to reshuffle in a shared file.
2026-09-27 03:36:19 +00:00
Doxa Maps
f728831d77 The interactive maps fetch the web-sized boundaries
Seven consumers switch to the -web copies exp-shepherd built (5410842a: every feature and island kept, about
a fifth of the bytes): useCountryOutline, useMapLayers, regions RegionCountryMap, research-dashboard
DashboardMapPanel, cities-internet connectivity-map (world + India states), gospel resources-map (world +
India provinces) and my-upg-100-list (world + UPG-100's India partition; China unchanged). generate-tree
stages the three -web files beside the full ones. Posters, print and every build-time point-in-polygon keep
the full files, as REPORT-WEB-BOUNDARIES.md requires: no runtime code tests membership on these.
2026-09-27 03:25:59 +00:00
Doxa Maps
4c49b46f29 Validate the CSS we build as JS strings
check:sfc-parse proves a .vue script block parses. A broken stylesheet
inside a valid string literal is still a valid string literal, so it
passed a sheet with 9 declarations stranded outside any rule and braces
at 12 open / 21 close. Nothing else in the tree validated CSS syntax.

check:shadow-css-parses reads every useShadowStyles() argument and scans
the result as CSS: brace balance, declarations stranded at depth 0, empty
selectors. 72 stylesheets across library/ and plugin-designs/.

It reads the arguments off the @babel/parser AST the build already uses,
because three hand-rolled lexers each produced a confident FALSE RED on a
file that was fine: a /* CSS comment */ read as a stranded declaration; an
apostrophe in a .vue prose header opening a phantom string literal, so a
</div> from the template was reported as CSS; and a regex literal
containing a quote, which cannot be lexed without resolving regex-vs-
division. The CSS scan itself stays hand-written, because CSS is the thing
being checked -- and it consumes CSS comments and quoted values as their
own token types, so content:'a{b' opens no rule.

9 self-tests run inline on every invocation, including the exact damage
above, so the gate cannot quietly stop being able to fail. An argument
built partly from an imported constant is scanned for its literal
segments and labelled, rather than skipped -- skipping it would have
excluded region-dashboards.vue, which builds its sheet from the token
layer plus literals.
2026-09-27 03:15:25 +00:00
Doxa Maps
73782bbbb5 Merge main into packet/vertical
Six public/ conflicts, ZERO in source — checked before the lock rather than discovered inside it, and it
held: the only overlap between main and this branch is my own bundle, its three leaf HTMLs and the two
manifests, because main rebuilt them and so did I.

They take main's side HERE and are regenerated from the merged source in the next commit. A bundle is an
OUTPUT: the only correct content is whatever the build emits from the merged inputs, and any
hand-reconciled middle version is wrong by construction even when the diff looks plausible.

studio's landing 9 (19682d2c, carrying my ee0bf338 by identical blob 9bdc45a9) has not landed, so mine
goes in first and theirs becomes the no-op. Both orders were agreed safe with studio and dx-shepherd
because the content is byte-identical either way.
2026-09-27 03:10:05 +00:00
Doxa Maps
60e5fdcf42 The map learned to name peoples, not just their languages
?codes= filters by LANGUAGE, which is what a partner's list usually is and the wrong
filter for a list of peoples. The reach table measured the gap in its own rows: its
Mandarin row counts 36 groups, those 36 speak 30 codes, and 55 Doxa groups speak those
30 — so a link from a 36-group row drew 55 pins. Akha's 33 drew 57; SUN's 93 drew 116.
The extra groups are real and also have nothing; they belong to a different row.

So the map takes ?groups=<slug list> and filters by people group. The slug is
doxa.life's own — carried from the roster into the pin rows, and read in the builder
off the prayer URL their API built from it, never constructed from a name, because a
slug this build invented could name somebody else's people. All 369 of those rows
carry a prayer URL, measured before relying on it.

Row and map now agree by construction: 0 of 35 rows disagree, and the Mandarin link
renders "36 / 2,107" on the map against a row claiming 36. The gate asserts exact
equality for a three-slug link and 0 for an unknown slug — the failure that would
quietly turn a partner's 40-group embed into the whole roster.

The longest link is 1,577 characters, SUN's 93 slugs; well inside what browsers carry,
but worth knowing before anyone adds a row with a thousand groups.
2026-09-27 02:40:12 +00:00
Doxa Maps
3f4ad2d85c Merge commit '3f6e29eb' into bundler-1/work
# Conflicts:
#	internal/check-optional-t-equivalence.mjs
2026-09-27 02:35:10 +00:00
Doxa Maps
5ec50a8124 Every Studio gallery tab after the first threw from createI18n: create it in a detached scope
The Driver's console, P0: in Plugin Studio's gallery, switching tabs threw "SyntaxError: Unexpected error"
from createI18n in createAppI18n. vue-i18n's createGlobal() runs inside scope.run() and throws
UNEXPECTED_ERROR when that returns undefined; Vue 3.5 creates a new EffectScope INACTIVE when the current
one is stopped; and a custom element's connectedCallback -> configureApp -> createAppI18n ran while the
released tab's stopped scope was still current. Root cause traced by dx-shepherd.

The i18n instance and its locale watcher are now created in effectScope(true), which never inherits the
ambient scope. The install wrapper also makes a second provide('appI18n') of the same instance a no-op, so
the bundle entries that still provide it themselves stop warning "App already provides property".

check:i18n-stopped-scope (in gates:post): a bare createI18n() inside a stopped scope must throw (control),
createAppI18n() there must succeed, translate and keep its locale watcher live, and a second provide must
not warn. On the previous index.js it fails with the Driver's exact error.
2026-09-27 02:32:07 +00:00
Doxa Maps
a5586e3ae2 check:universal-props reads the gospel-internet bundle
The bundle list is hard-coded, so a new bundle passed as '0 bundles'
until it is named here.
2026-09-27 02:17:35 +00:00
Doxa Maps
db2b24c753 A bare-host embed under ?lang=ar now renders Arabic here too
dx-shepherd's ruling: the locale-honouring fallback everywhere, carrying the
Driver's goal (d) — a bare-host embed with ?lang=ar must render Arabic — now
recorded in COMMON.md as the embed contract.

all-people-groups passed its inline EN map as the fallback, which served ENGLISH to
exactly the hosts that cannot install vue-i18n: the seven bundles here that install
none. It now takes the catalogue default, so those readers get their own language
with English still the last resort. The EN map is deleted — with the catalogue
complete it could rescue nothing, and a second copy of the English is only a place
for the two to drift.

THE RULING'S CONDITION WAS MEASURED BEFORE THE SWITCH, not after: all eleven keys
exist in all eleven locales, and ar differs from en on 11 of 11, so the change is
observable rather than cosmetic.

★ AND THE CONDITION IS NOW KEPT TRUE RATHER THAN MEASURED ONCE. A hand census
protects the day it is taken. Section 9 of check:optional-t-equivalence re-derives
it every run, because with no local fallback an uncatalogued key renders as a raw
dotted path to a reader — the exact failure upg100 described, and the reason they
had advised keeping a fallback at all.

★ THAT CENSUS UNDER-COUNTED TWICE BEFORE IT WAS RIGHT, both caught by checking
instead of assuming:
 - literal tr('key') extraction found 6 of 11. Four keys are category ids reached
   only through tr(cat.id) / tr(c.id); the census now extracts the CATS ids too, and
   asserts the dynamic call sites still look the way it assumes.
 - the eleventh, 'pray', was lost to codeOnly: it sits in a template literal whose
   `https://` was read as a line comment, because the scanner had already lost
   template state further up a 1000-line SFC. The census now reads RAW source.
   The asymmetry is the point — over-including a commented-out key makes the check
   STRICTER (a cheap false red); under-including a real one makes it claim
   completeness it does not have. strip-code.mjs carries that rule now, so the next
   author picks raw or stripped for a reason rather than by habit.
2026-09-27 02:17:35 +00:00
Doxa Maps
a7c9e9557f The prayer map's legends see the pins it draws
rega, row 46 at 835ec1ac: on the prayer map neither the shared-point sentence nor the standing pin note
appeared. research-map and countries-map provide normalizedPeopleGroups to their legends; the prayer map
did not, so its legends fell back to dataStore's cache, which holds the RAW API rows: field names the
co-location index cannot read, and no GeoJSON geometry for the standing-note detector to find.

useMapData now provides the normalized rows it draws, so every map built on it offers them. LegendMobile
and SemanticTreeLegend read them first (the cache stays the fallback), and the detector accepts a row with a
name and a position as well as a GeoJSON point. check:approx-pin-note asserts the provide and the
normalized-row match; the provide check fails on the previous useMapData.
2026-09-27 02:17:35 +00:00
Doxa Maps
bdff90ce82 Merge commit '9df086e5' into bundler-1/work
# Conflicts:
#	internal/check-optional-t-equivalence.mjs
2026-09-27 02:00:00 +00:00
Doxa Maps
ee0bf338d1 My own gate was accusing the one agent who fixed the defect
check:no-silent-engine reported gospel-resources/resources-map.vue as the last remaining offender at
main 9df086e5. It is not, and has not been since their landing: line 1513 reads
`const gl = engineGlobal(grProvider)` with no `||` at all. I relayed the red to dx-shepherd as fact
before looking at the line, and the line was PROSE.

★ WHAT IT WAS MATCHING. gospel documented what they removed, in a /* */ block whose second line begins
with a BACKTICK:
      `engineGlobal(grProvider) || window.mapboxgl`. engineGlobal already defaults to mapbox, so
The skip asked only whether a line STARTS with //, * or /*. That is not what a comment is: it misses
every CONTINUATION line of a block comment whose text begins with something else. So the gate now
tracks block-comment state instead, and the --self-test carries gospel's exact shape as a regression
case — plus its inverse, a real offender after a closed block, because fixing a false positive by
skipping MORE is only correct if the true positives still fail. Both assert in the same run.

★ AND THIS IS THE WORST FAILURE MODE A GATE CAN HAVE, which is why it gets a commit of its own rather
than a quiet one-line patch. It told the one agent who did the right thing that they had done the wrong
thing, and the cheapest way to make it green was to DELETE the explanation — so the gate actively
attacked the reasoning it exists to protect. This file's own header quotes the bad pattern for exactly
that reason; the skip was there from the start and was simply too shallow to cover the form someone
else actually used.

⚠ AND IT MADE A STALE CARD LOOK LIVE. The offender's line number moved 1490 -> 1505 -> 1509 across
three landings purely because their file grew, so each re-read produced a plausible new number for a
defect that had already been fixed. A card that names file:line ages into a false claim silently; the
gate has to be the source of truth, and it was lying too.

Tree-wide result now: 6 files call engineGlobal(), PASS — every one keeps the engine its provider asked
for. The gate is finally green on a green tree, which means it can be wired into gates:post. That wiring
is shared config (rule 5B) so it is NOT in this commit: prepared for studio separately.
2026-09-27 01:59:11 +00:00
Doxa Maps
9df086e5f0 merge main (gospel) into packet/vertical
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-27 01:44:55 +00:00
Doxa Maps
2f260ab700 Merge main (studio landing 6) into packet/vertical
★ THE TWO INSTRUCTIONS I WAS GIVEN CONTRADICTED EACH OTHER ON ONE LINE, so this records how it was
resolved rather than leaving the next reader to guess. UPGHK said delete humanizeKey; UPGW3 said merge
1bd5f109 and "keep regions' version" — and regions' version KEEPS humanizeKey
(`useOptionalT('', { fallback: (k) => humanizeKey(k) })`). Read as structure-vs-fallback they agree:
UPGHK's reasoning is about what a reader SEES ("a real English or translated string beats a title-cased
key segment"), so "keep regions' version" is about the SHAPE of the call. Kept theirs in every respect
but the fallback:
  - useOptionalT('') with the EMPTY namespace, theirs, not my 'messages'/'tabs'. Verified to be a
    behavioural no-op here (every key this code passes is already fully qualified, so qualify() returns
    it unchanged either way) — and '' is the more honest of two identical options, because it says
    "these keys arrive qualified" instead of implying a prefix is being added.
  - their "one behaviour gain" note, which is a real gain I had not documented: the old path was the
    bare useI18n().t, so on a host WITH i18n a missing key rendered the raw dotted key.
  - the fallback deleted, per UPGHK.

AND THE REGISTER KEEPS BOTH WAVES. Their side of the conflict added a wave-3d row for
all-people-groups that mine would have dropped — it stays, with its own reading intact, which is the
OPPOSITE of the call taken for my two files (its inline EN map stays because the file is UNOWNED and an
unowned file is the wrong place to make a product decision by default). Both readings are right for
their own file; that is the point.

⚠ AND I FIXED A FALSE SENTENCE THE GATE WAS ABOUT TO PRINT. With a null third column the register said
"takes the CATALOGUE fallback, as it always did" — true for the two composables that always did, false
for my two, which used humanizeKey until today. That branch already existed to stop exactly this for
gospel's four; my files needed the same treatment, so the "moved" list now names both owners and says
what each moved FROM. A register can be right about its assertion and still teach the next reader
something untrue in the line it prints, and a gate message is the one place a reader has no reason to
doubt.

Verified: check:optional-t-equivalence PASS on all sites, including gospel's unchanged message.
2026-09-27 01:39:28 +00:00
Doxa Maps
f822a410ba Merge commit 'dd369848' into bundler-1/work
# Conflicts:
#	internal/check-optional-t-equivalence.mjs
#	package.json
#	plugin-designs/all-people-groups/all-people-groups.vue
2026-09-27 01:38:41 +00:00
Doxa Maps
1f186070a5 Register the two wave-3c sites, with the third column their code actually has
The rows studio removed when 4c05e589 was not in their landing go back in — but NOT as
regions-plugin wrote them. 4c05e589 had the third column as humanizeKey(k), which was correct for
the migration it was written against and became wrong the moment dx-shepherd took the product call
(UPGHK) to DELETE that fallback. A register row asserting a fallback the code no longer passes fails
by design, which is the same hole regions flagged to me in the opposite direction.

So both sites register with null: no fallback, resolve through the catalogue. The gate then asserts
!/fallback:/ rather than a fragment, which is exactly what the code now does.
2026-09-27 01:37:45 +00:00
Doxa Maps
0ded1a4e1b A bare-host embed under ?lang=ar now renders Arabic here too
dx-shepherd's ruling: the locale-honouring fallback everywhere, carrying the
Driver's goal (d) — a bare-host embed with ?lang=ar must render Arabic — now
recorded in COMMON.md as the embed contract.

all-people-groups passed its inline EN map as the fallback, which served ENGLISH to
exactly the hosts that cannot install vue-i18n: the seven bundles here that install
none. It now takes the catalogue default, so those readers get their own language
with English still the last resort. The EN map is deleted — with the catalogue
complete it could rescue nothing, and a second copy of the English is only a place
for the two to drift.

THE RULING'S CONDITION WAS MEASURED BEFORE THE SWITCH, not after: all eleven keys
exist in all eleven locales, and ar differs from en on 11 of 11, so the change is
observable rather than cosmetic.

★ AND THE CONDITION IS NOW KEPT TRUE RATHER THAN MEASURED ONCE. A hand census
protects the day it is taken. Section 9 of check:optional-t-equivalence re-derives
it every run, because with no local fallback an uncatalogued key renders as a raw
dotted path to a reader — the exact failure upg100 described, and the reason they
had advised keeping a fallback at all.

★ THAT CENSUS UNDER-COUNTED TWICE BEFORE IT WAS RIGHT, both caught by checking
instead of assuming:
 - literal tr('key') extraction found 6 of 11. Four keys are category ids reached
   only through tr(cat.id) / tr(c.id); the census now extracts the CATS ids too, and
   asserts the dynamic call sites still look the way it assumes.
 - the eleventh, 'pray', was lost to codeOnly: it sits in a template literal whose
   `https://` was read as a line comment, because the scanner had already lost
   template state further up a 1000-line SFC. The census now reads RAW source.
   The asymmetry is the point — over-including a commented-out key makes the check
   STRICTER (a cheap false red); under-including a real one makes it claim
   completeness it does not have. strip-code.mjs carries that rule now, so the next
   author picks raw or stripped for a reason rather than by habit.
2026-09-27 01:36:30 +00:00
Doxa Maps
4ca2dcf912 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-27 01:35:26 +00:00
Doxa Maps
ff0c19e79a merge main (studio landing 5) into packet/vertical 2026-09-27 01:31:40 +00:00
Doxa Maps
0fa6d1fe63 Record why humanizeKey stays, since the reason in circulation was wrong
upg100-plugin advised keeping humanizeKey on the grounds that their wall/poster keys
were incompletely catalogued, and I carried that forward as a measured reason. They
retracted it themselves after enumerating, and I verified the retraction rather than
just accepting it: those two files use exactly seven keys, all string literals, and
all seven are present in all eleven locales. Zero gaps.

Their deeper correction matters more than the count, and it is right: humanizeKey
does not guard a MISSING KEY at all. It guards a host that never installed vue-i18n,
where useI18n() throws — seven bundles here install none. The catalogue was never
the question.

So the row now carries the only true reason, which is the one useOptionalT's own
header already gave: switching these to tStatic would change what seven BARE-HOST
bundles render, turning strings that are English today into strings that follow the
reader's locale. That is a decision about the embed contract, not about a catalogue,
and it is undecided. upg100 now recommends dropping humanizeKey on the merits and has
put that to dx-shepherd; when the contract owner agrees, this row's third field
becomes null and the fallback goes.

The row's assertion is unchanged — it always described bare-host rendering, which was
correct. What was wrong was the justification I repeated for it downstream.
2026-09-27 01:29:47 +00:00
Doxa Maps
18d70de5dd The coverage line counted posters against packets and contradicted itself
Three defects in one report, all of them the kind that makes a number read as verified when it is not.

1. THE DENOMINATOR CAME FROM A DIFFERENT POPULATION THAN THE NUMERATOR. It counted
   out/posters/*.html — POSTER files — while the numerator counts PACKET zones, and the two are not
   the same set: 28 posters against 27 packet-able zones, because `deaf` had a poster and no
   obtainable packet (fixed in 3ef164c0). So "2 of 28" was not 26 to go, it was 25 to go plus one
   that no command could produce. Both sides now read the SAME seam — leafZones(buildLegendNodes(...)),
   which is what the generator itself enumerates — so they cannot drift apart again.

2. out/posters IS ITSELF GENERATED, so on a clean checkout the denominator was null and the
   shortfall warning silently vanished. The one line whose job was to stop an overclaim could not
   fire precisely when nothing had been rendered at all. Its absence is now stated out loud.

3. THE COUNT AND THE LIST DISAGREED ON SIGHT — my own bug, caught by reading the output rather than
   trusting it: "21 zone(s)" printed above a list of 26. zones[] holds every FILE stem, which
   includes things that are not zones (the four fixture-* packets and the <zone>.frontier ?list=
   variant), so it overcounted coverage against a real-zone denominator and made "7 of 28" out of
   2 real zones — a 3.5x overstatement. Count and list are now one derived set, and the non-zone
   packets are reported separately as what they are: coverage of the RENDERER, not of a zone's data
   shape.

And the warning now NAMES the missing zones instead of counting them. "26 zone(s) have no packet"
tells a reader the size of the hole and not what to type, which is why nobody ever closed it; the
named list is what surfaced `deaf` as unreachable rather than merely ungenerated.

Coverage now measured at 22 of 28, every one PASSing with --expect-funnel, up from 2 of 28. The six
still open are named in the output (central-africa, deaf, east-africa, europe,
southern-africa-islands, west-africa) and all six need a records source carrying the selector fields
3ef164c0 added to the query.
2026-09-27 01:28:02 +00:00
Doxa Maps
c790b39840 Every shipped link has evidence, and 1,412 of them did not
Three findings, one thread: rule 4 says verify a URL before shipping it, and the
check I wrote for it yesterday was measuring one file.

1. THE TABLES PAGE WAS SHIPPING DEAD FORM. people-group-resources.json — the
"Resources by People Group" tab, which a reader clicks — carried 2,294 URL cells, of
which 1,412 were jesusfilm.org/watch/?languageId=<id>. That form is IGNORED by the
site: a real id and a bogus one render the same generic library page, which is why
the map migrated onto Arclight shareUrls. The migration never reached this table, so
the booleans beside those links were being refreshed while the links themselves went
stale for a year of builds. 1,417 film and 882 scripture URLs re-derived from the
index, which is authoritative for both and whose URLs are the verified ones.

2. TWO ARCLIGHT LANGUAGES, ONE ISO CODE — resolved, with evidence rather than a
ruling. jos was SKIPPED here, and the note said resolving it meant deciding whether
the code or the publisher's name was authoritative, "a data question for someone who
can check it". Checked: media-languages?iso3=jos returns 7138 "Jordanian Sign
Language" and 139080 "Lebanese Sign Language", and 7138's shareUrl redirects to
.../jordanian-sign-language.html while a bogus arc.gt code 404s. So the code stays
authoritative for the join and the NAME breaks the tie the code cannot. The old note
also had a fact wrong — it called Lebanese "a different language (lbs)", but lbs is
LIBYAN Sign Language and Lebanese has no code of its own, so this is two Levantine
varieties sharing one code, not a mis-filing. Three Deaf groups now reach the JESUS
Film in their own sign language through a link that resolves; 0 retired-form links
remain in the index.

3. THE GATE'S SCOPE WAS THE BUG. It walked the pin index alone, which is how item 1
hid from it. It now reads every table the SURFACES read, taken from the surfaces, and
resolves each URL against four records: the verification table, the org contacts, the
SUN pages (proved by content against a catch-all control) and doxa.life's own listing
for prayer pages, which cannot be proved by fetch because pray.doxa.life answers 200
for any slug. Provenance fields are excluded by path — a citation is not an offer.
8,445 links measured yesterday; 12,292 shipped links today, 0 unrecorded, 0 broken,
0 that no status code can prove.

Reading every STAGED file instead reported 6,115 unrecorded and 53 broken, all of
them correct behaviour: API endpoints in source fields, and build caches that still
hold the 53 share URLs the verifier proved dead — which is exactly why the pin
builder drops them. A gate that goes red on correct behaviour gets switched off.
2026-09-27 01:26:50 +00:00
Doxa Maps
8999e7ff01 check:composite-embed-knobs hands each composite its own profile
rega's run at 1a761741 measured 8 cases and left 10 UNMEASURED: gospel-resources, my-upg-100-list,
regions-dashboard and two more drew no map under a knob-only profile-config, because a multi-profile
element must be told which profile is the composite. The target's nested-elements-mfe profile now rides
with the knob under test (the knob still wins on any clash). Browser gate: needs a DOXA_BROWSER=1 seat.
2026-09-27 01:16:52 +00:00
Doxa Maps
15820f1432 Dashboard kit, landing 3: the camera and the CSV exporter
Both are zero-domain in the sense the spec meant: they import only `vue` and read
rows through accessors. Bodies are what research-dashboard shipped apart from the
two parameters the spec named and one policy object, and the old paths re-export
for one cycle.

★ THE FIRST STEP FOUND TWO GATES THAT READ THESE FILES BY PATH, which is why it is
now the first step. Landing 1 nearly disarmed check-facet-cascade the same way.
  internal/check-csv-export-shape.mjs does not import useCsvExport; it READS the
  source and extracts esc() out of it. Left at the plugin path it would have read
  the one-line shim, found no esc(), and failed with "could not extract esc()" — a
  gate accusing a working exporter because its subject moved.
  check-filter-multivalue.mjs — MINE — lists the same file in its PANELS array.
  Left at the plugin path it would have read the shim, found no predicate to
  object to, and PASSED VACUOUSLY. That is worse: a gate going quiet about the
  thing it guards. Both move in this commit.

THE COORDINATE IS A PARAMETER; WHAT MAKES A COORDINATE VALID IS NOT. `coords`
replaces only the READ of row.longitude / row.latitude, which is pray-tools'
spelling and not a universal one. Every guard stays shared: Number() on API
strings, 0 accepted per axis, the exact 0,0 null-island sentinel rejected, the
out-of-range check. A kit that let each caller re-implement that validation would
hand the next dashboard the same four bugs to rediscover.

★ AND THE OPTION WAS DEAD WHEN I FIRST WROTE IT. `coords` was accepted in the
signature and never passed to selectionBounds, so a caller could set it,
type-check clean, build clean, and watch it do nothing. This tree has paid for
that shape before — a defineProps on a component the custom element never binds —
and it is worse than a missing feature because it READS as supported. Nobody files
a bug against a knob they assume they misused. check:kit-options-reachable now
asserts, across all four shared composables, that every option accepted is used.

THE CAMERA'S EIGHT CONSTANTS ARE ONE DECISION, so CAMERA_POLICY exports them
together. The failure that prevents is a second dashboard re-picking them one at a
time and arriving at a camera that feels different from Big Picture's for no reason
anybody wrote down. It is read-only and deliberately not yet overridable: making
each a per-caller option means threading eight of them through every use, and no
consumer has asked for a different camera. Exporting the decision so it can be
cited is the half with a use today; saying so is better than shipping eight
options nobody passes.

THE EXPORTER GAINS exportLabel and sources. "Research Dashboard" was this
dashboard's name, and the provenance line said nothing about where the data came
from — which is what a provenance line is for. Both default to today's output, to
the byte.

★ AND THE NEW GATE HAD THE COMMENT HOLE TWICE IN ONE SITTING. First it searched
the raw body, and the comment beside the fixed call site says "`coords` IS PASSED
HERE" — so the mutant that removed the argument left it green. Then, stripping
comments only per-part, it split the signature's comment PROSE on commas and
reported that useCsvExport accepts `and`. Comments are text; every text search has
to say so, in both directions. That is the fourth variant of one mistake this
week, and all four were caught by running the mutant rather than by reading.

Library index regenerated in the same commit, 103 items. Source only.
STUDIO: needs a rebuild of research-dashboard and dashboard-plus-grid. The
internal/ hunk is one line in check-csv-export-shape.mjs.
2026-09-27 01:16:52 +00:00
Doxa Maps
1bd5f109d8 C4 wave 3c, my-upg-100-list: two humanizeKey guards become one call
poster-wall.vue and upg-100-mfe.vue both had the same two lines — a humanizeKey
fallback, then `try { t = useI18n().t } catch` — and now call
useOptionalT('', { fallback: (k) => humanizeKey(k) }).

No namespace: callers pass FULL keys here, which is what the bare useI18n().t they
used to get required.

humanizeKey stays as the fallback rather than taking the catalogue default, so a
bare host still reads a title-cased last segment. gospel-plugin chose the
locale-honouring default for their bundle; the same one-word choice is upg100's to
make for theirs.

★ ONE BEHAVIOUR GAIN, stated rather than smuggled: the old i18n path was the raw
useI18n().t, so on a host WITH i18n a missing key rendered the dotted key itself.
It now falls through to humanizeKey — the fallback a bare host already got. Nothing
that resolves today changes; a key that is missing reads better.

Register now covers 14 migrated sites.
2026-09-27 01:16:51 +00:00
Doxa Maps
ccc9a44995 Register the all-people-groups row, which the previous commit missed
The insertion in ef8b8602 failed on a bad anchor and the commit went ahead anyway,
so the migration landed with no row asserting it. The gates all passed, because a
register that does not mention a file cannot fail on it -- a green run that proved
nothing about the thing just changed.

The row asserts the useOptionalI18n form and that its inline EN map is still passed
as the fallback.
2026-09-27 01:16:51 +00:00
Doxa Maps
f13075b1d4 Decouple the tooling: no host named in internal/ or the build config
Six tools defaulted to the live host's URL. They now read the published tree's location from
DOXA_PUBLISHED_BASE (environment, or the gitignored .env / .env.local) through internal/_published-base.mjs,
and with nothing set they report NOT MEASURED (exit 75) instead of measuring a host they were never told
about: check:catalog-currency, check:catalog-identity, check:published, verify:5-caps, verify:map-paint. An
explicit BASE / --base / PUBLISHED_BASE still wins. check:embed-base only needed an example URL for its
offline resolution table.

The public-hygiene leak detector builds the platform name from parts, so the file that catches it does not
carry it. Comments and the two build-config notes say the host platform or the live test host. From
repo's DECOUPLE round 2 list.
2026-09-27 01:16:50 +00:00
Doxa Maps
78a37c1b5f Gate rule 4, and find the 16 links my own measurement missed
"Verify a URL resolves before shipping it" had a verifier and a record, and nothing
asserting the record covers what the build actually ships — so the coverage number
was something a person went and measured, and I measured it earlier this flight as
"8,445 distinct URLs, 100% covered". That was 16 links short, and short for the
reason this bundle keeps relearning: I walked a LIST OF FIELDS.

This check recurses the pin index and treats any http string as a shipped link. It
found 8,461, and the 16 extra were real: the 15 translator-organisation homepages
and the SUN block's three pages, none of which the verification table has ever heard
of. They are not exempt — they are verified in their OWN records, by the scripts that
added them, and the check resolves against all three. SUN's evidence is the stronger
kind: sun.bible answers 200 for any path, so its pages are proved by content against
a catch-all control that returns 904 characters where the New Testament returns
22,043.

Two more things the run corrected rather than confirmed. The status-blind exemption
is a URL SHAPE, not a host: jesusfilm.org/watch/jesus/<slug> does discriminate and
2,049 film links were migrated onto it for exactly that reason, so exempting the host
would have re-blinded all of them — only the older /watch/?languageId= form is blind,
and one such URL remains. And the poison arms now derive their failures from real
entries instead of a synthetic push, because an arm that manufactures its own failure
proves the assertion runs, not that it can catch anything.
2026-09-27 01:14:33 +00:00
Doxa Maps
3c0ddc7e5e Merge commit '835ec1ac' into bundler-1/work
# Conflicts:
#	internal/check-optional-t-equivalence.mjs
#	package.json
2026-09-27 01:12:12 +00:00
Doxa Maps
d8e50de16b cities-internet takes the locale-honouring fallback, and drops its English copy
exp-shepherd's call as the plugin's owner, on the reasoning gospel-plugin gave
first: the bundle already carries all 11 catalogues, so a partner embedding it
without vue-i18n being served English is a bug rather than a decision.

So the three files pass no fallback and resolve through tStatic — the runtime
locale first, English as the last resort. Their enLookup was English-only, which is
exactly what served English to those hosts, so it goes, and the per-area
connectivity.json import goes with it: tStatic reads the same area out of the
catalogue the bundle already ships. Three fewer copies of a lookup, three fewer
imports of bytes that were already there.

Two entries added to the register as DELIBERATE absences, so a later reader cannot
"complete" C4 by converting them:
  · pplr-data-maps/src/components/SemanticTreeLegend.vue keeps its hand-rolled
    guard at exp-shepherd's request — it is carded for retirement
    (RETIRE-PPLR-SEMANTICTREELEGEND) in favour of the library component, and a
    migration whose value expires before it lands is not worth the diff;
  · country-posters-mfe.vue and key-cities/cities-map.vue are their owner's to cut,
    so that country-posters stays one landing and key-cities stays gated.
2026-09-26 22:06:59 +00:00
Doxa Maps
ef5ea1840a C4 wave 3b, cities-internet: three identical guards become one call
All three files carried the same block byte-for-byte — enConnectivity + enLookup +
fill + try/catch — and now call useOptionalT('connectivity', { fallback: enLookup }).
English is unchanged on a bare host, because each passes its own enLookup rather
than taking the catalogue default.

gospel-plugin chose the catalogue default for their bundle, on the argument that a
partner host without vue-i18n is served English while the bundle already ships all
11 catalogues. The same one-word choice is open to cities-internet's owner; it is
not a migration's to take, so this preserves what they have.

Their local fill() used `k in params`, so the shared fillParams also removes a way
for these three to render the literal string "null" into a sentence.

Also records, in the stripper itself, the two things codeOnly does NOT strip —
CSS comments inside a JS template literal, and HTML comments in a .vue template.
Both have already cost a real measurement here: a specificity read of (0,5,53),
and a bare-useI18n census that read 3 where the code had 2. gospel-plugin's
generalisation is the one worth keeping: any <!-- --> that quotes an identifier
will keep doing that to you. The stripper must match the LANGUAGE of the text.
2026-09-26 22:06:59 +00:00
Doxa Maps
9816a4244f gospel takes the locale-honouring fallback, and it is their call that made it
gospel-plugin's decision, in their words: "most partners will drop a custom
element or an iframe and never install vue-i18n ... an Arabic ministry site
embedding this map serves its readers English while the bundle already carries all
11 catalogues. That is a bug, not a decision."

So all four gospel translators drop `{ fallback: enLookup }` and take the
helper's catalogue default, which resolves through tStatic: the reader's locale
first, English as the last resort. Their three conditions are met and each is
asserted, not assumed:
  1. resources-map passes NO namespace — its keys span gospel.* and pins.*;
  2. the fallback walks the catalogue ROOT, which tStatic does by construction;
  3. params survive, via the null-safe fillParams.

★ THE ROW THEY ASKED FOR, and it is the only thing here that could catch the
regression this migration risked: a BARE host under ar must render
pins.approximate in ARABIC — not the key, not gospel.pins.approximate, not the
English. It reads the SHIPPED ar/ and en/ pins.json and rebuilds tStatic's own
resolution order over them, so it measures the real catalogue rather than a stub.
Three negatives beside it, plus a control that both catalogues really carry the
key, so the comparison is measuring a difference that exists.

enLookup is deleted from all four, and the EN_MESSAGES import from three —
data-tables keeps it, because it still builds a map from EN_MESSAGES.gospel.res at
:182. Checked before deleting rather than after: check:no-free-identifiers would
have caught a wrong removal, since removing something still used creates a name
that is valid JS until it runs.

Two corrections that came from other agents reading this work:
 - studio: the JSDoc carried <area>.<key>-shaped examples, which
   check:i18n-keys-exist reads as real keys and reports missing. They reworded it
   twice because my file copy restored it. Reworded, with a standing note in the
   file so a third copy cannot bring them back.
 - the register said each site "takes the CATALOGUE fallback, as it always did".
   True of the two composables, FALSE of gospel's four, which were moved here by
   their owner. A false statement inside a gate message is worse than no message,
   because a reader has no reason to doubt it. Now accurate per row.

check:i18n-keys-exist is red on messages.engineNotLoadedDetail. NOT mine and not
touched here: it is in cities-internet/connectivity-map.vue, whose own comment says
the key "arrives with upg100's ade12e4c", and the call already guards for its
absence.
2026-09-26 22:06:59 +00:00
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
4ec83ac7e2 Register the all-people-groups row, which the previous commit missed
The insertion in ef8b8602 failed on a bad anchor and the commit went ahead anyway,
so the migration landed with no row asserting it. The gates all passed, because a
register that does not mention a file cannot fail on it -- a green run that proved
nothing about the thing just changed.

The row asserts the useOptionalI18n form and that its inline EN map is still passed
as the fallback.
2026-09-26 18:56:08 +00:00
Doxa Maps
e80c38c045 cities-internet takes the locale-honouring fallback, and drops its English copy
exp-shepherd's call as the plugin's owner, on the reasoning gospel-plugin gave
first: the bundle already carries all 11 catalogues, so a partner embedding it
without vue-i18n being served English is a bug rather than a decision.

So the three files pass no fallback and resolve through tStatic — the runtime
locale first, English as the last resort. Their enLookup was English-only, which is
exactly what served English to those hosts, so it goes, and the per-area
connectivity.json import goes with it: tStatic reads the same area out of the
catalogue the bundle already ships. Three fewer copies of a lookup, three fewer
imports of bytes that were already there.

Two entries added to the register as DELIBERATE absences, so a later reader cannot
"complete" C4 by converting them:
  · pplr-data-maps/src/components/SemanticTreeLegend.vue keeps its hand-rolled
    guard at exp-shepherd's request — it is carded for retirement
    (RETIRE-PPLR-SEMANTICTREELEGEND) in favour of the library component, and a
    migration whose value expires before it lands is not worth the diff;
  · country-posters-mfe.vue and key-cities/cities-map.vue are their owner's to cut,
    so that country-posters stays one landing and key-cities stays gated.
2026-09-26 18:52:21 +00:00
Doxa Maps
4c05e58933 C4 wave 3c, my-upg-100-list: two humanizeKey guards become one call
poster-wall.vue and upg-100-mfe.vue both had the same two lines — a humanizeKey
fallback, then `try { t = useI18n().t } catch` — and now call
useOptionalT('', { fallback: (k) => humanizeKey(k) }).

No namespace: callers pass FULL keys here, which is what the bare useI18n().t they
used to get required.

humanizeKey stays as the fallback rather than taking the catalogue default, so a
bare host still reads a title-cased last segment. gospel-plugin chose the
locale-honouring default for their bundle; the same one-word choice is upg100's to
make for theirs.

★ ONE BEHAVIOUR GAIN, stated rather than smuggled: the old i18n path was the raw
useI18n().t, so on a host WITH i18n a missing key rendered the dotted key itself.
It now falls through to humanizeKey — the fallback a bare host already got. Nothing
that resolves today changes; a key that is missing reads better.

Register now covers 14 migrated sites.
2026-09-26 18:48:41 +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
f5cef180ae C4 wave 3b, cities-internet: three identical guards become one call
All three files carried the same block byte-for-byte — enConnectivity + enLookup +
fill + try/catch — and now call useOptionalT('connectivity', { fallback: enLookup }).
English is unchanged on a bare host, because each passes its own enLookup rather
than taking the catalogue default.

gospel-plugin chose the catalogue default for their bundle, on the argument that a
partner host without vue-i18n is served English while the bundle already ships all
11 catalogues. The same one-word choice is open to cities-internet's owner; it is
not a migration's to take, so this preserves what they have.

Their local fill() used `k in params`, so the shared fillParams also removes a way
for these three to render the literal string "null" into a sentence.

Also records, in the stripper itself, the two things codeOnly does NOT strip —
CSS comments inside a JS template literal, and HTML comments in a .vue template.
Both have already cost a real measurement here: a specificity read of (0,5,53),
and a bare-useI18n census that read 3 where the code had 2. gospel-plugin's
generalisation is the one worth keeping: any <!-- --> that quotes an identifier
will keep doing that to you. The stripper must match the LANGUAGE of the text.
2026-09-26 18:29:32 +00:00
Doxa Maps
2fb0f740d0 gospel takes the locale-honouring fallback, and it is their call that made it
gospel-plugin's decision, in their words: "most partners will drop a custom
element or an iframe and never install vue-i18n ... an Arabic ministry site
embedding this map serves its readers English while the bundle already carries all
11 catalogues. That is a bug, not a decision."

So all four gospel translators drop `{ fallback: enLookup }` and take the
helper's catalogue default, which resolves through tStatic: the reader's locale
first, English as the last resort. Their three conditions are met and each is
asserted, not assumed:
  1. resources-map passes NO namespace — its keys span gospel.* and pins.*;
  2. the fallback walks the catalogue ROOT, which tStatic does by construction;
  3. params survive, via the null-safe fillParams.

★ THE ROW THEY ASKED FOR, and it is the only thing here that could catch the
regression this migration risked: a BARE host under ar must render
pins.approximate in ARABIC — not the key, not gospel.pins.approximate, not the
English. It reads the SHIPPED ar/ and en/ pins.json and rebuilds tStatic's own
resolution order over them, so it measures the real catalogue rather than a stub.
Three negatives beside it, plus a control that both catalogues really carry the
key, so the comparison is measuring a difference that exists.

enLookup is deleted from all four, and the EN_MESSAGES import from three —
data-tables keeps it, because it still builds a map from EN_MESSAGES.gospel.res at
:182. Checked before deleting rather than after: check:no-free-identifiers would
have caught a wrong removal, since removing something still used creates a name
that is valid JS until it runs.

Two corrections that came from other agents reading this work:
 - studio: the JSDoc carried <area>.<key>-shaped examples, which
   check:i18n-keys-exist reads as real keys and reports missing. They reworded it
   twice because my file copy restored it. Reworded, with a standing note in the
   file so a third copy cannot bring them back.
 - the register said each site "takes the CATALOGUE fallback, as it always did".
   True of the two composables, FALSE of gospel's four, which were moved here by
   their owner. A false statement inside a gate message is worse than no message,
   because a reader has no reason to doubt it. Now accurate per row.

check:i18n-keys-exist is red on messages.engineNotLoadedDetail. NOT mine and not
touched here: it is in cities-internet/connectivity-map.vue, whose own comment says
the key "arrives with upg100's ade12e4c", and the call already guards for its
absence.
2026-09-26 18:24:04 +00:00
Doxa Maps
7138109f07 Re-cut gospel's C4 wave on main: no namespace, because its keys span two areas
studio rejected the first cut for the right reason, and it was a SEMANTIC conflict
rather than a textual one — the kind a clean auto-merge hides.

On main, gospel's resources-map.vue reads keys in TWO areas: gospel.* and pins.*
(the shared people-group-pin wording), and its enLookup deliberately walks the
catalogue ROOT so it can see both. My first cut passed useOptionalT('gospel', …),
whose qualify() would have turned pins.approximate into gospel.pins.approximate on
the translated path, and sent the fallback looking under EN_MESSAGES.gospel where
pins.* does not exist. The pins sentence would have regressed. It now passes '',
so every key reaches the catalogue exactly as the caller wrote it, and the gate
asserts that empty namespace explicitly so a later edit cannot quietly re-add one.

The other three files are unchanged from the earlier cut: main did not touch their
enLookup, which still strips ^gospel. against EN_MESSAGES.gospel, so 'gospel' is
the right namespace there. The namespace is a PER-FILE fact, not a per-plugin one.

fill() stays: it has a third caller, tOr(), that this migration does not touch.
Checked before deleting it as dead.

★ AND THE COUNTING BUG THE RE-CUT EXPOSED IN MY OWN GATE. The register asserts how
many bare useI18n() calls a file may keep. It read 3 for resources-map where the
code has 2, because codeOnly() is a JAVASCRIPT stripper and the third "call" is
inside an HTML <!-- --> comment in the template. The stripper has to match the
LANGUAGE of the text it reads — the same lesson as the CSS-in-a-template-literal
case that gave a specificity of (0,5,53). Fixed by stripping HTML comments before
counting.

fillParams' null-safe rule is included here and is independently confirmed by
main: gospel's own fill() on 4d18a6f3 uses `params[k] == null ? m : …`, the same
rule, arrived at separately.
2026-09-26 18:20:01 +00:00
Doxa Maps
36e608f54f Merge commit '4d18a6f3' into bundler-1/work
# Conflicts:
#	plugin-designs/gospel-resources/resources-map.vue
2026-09-26 18:17:12 +00:00
Doxa Maps
e04121f16f C4 wave 3a, gospel-resources: four guards share one, English unchanged
All four of gospel's translators had the same shape — enLookup + a try/catch
around useI18n() + `v === key ? fallback : v` — and all four now call
useOptionalT('gospel', { fallback: enLookup }).

enLookup STAYS as each site's fallback rather than taking the helper's catalogue
default. The difference is real and it is not mine to decide: enLookup resolves
against the SHIPPED ENGLISH, while the catalogue default resolves through tStatic,
which honours the runtime locale. Adopting the default would make a bare host
render Arabic under ?lang=ar where it renders English today. That is arguably
better, and it belongs to gospel-plugin.

★ AND ONE FIX THAT CAME OUT OF READING THEIR CODE BEFORE REPLACING IT.
charts.vue's interpolate() substitutes only when a value is NOT NULL; my
fillParams substituted whenever the KEY WAS PRESENT. With { count: null } mine
would have rendered the string "null" into a sentence where theirs leaves
`{count}` visible. Theirs is right — a visible placeholder is a bug you can see,
"null" reads as content — so fillParams now matches, with assertions for the null
case and for the 0 case a truthiness test would have dropped.

resources-map.vue KEEPS two bare useI18n() calls, deliberately: they read the
LOCALE (`useI18n().locale.value`) for isEnglish and the engine's language tag, not
text. `locale` arrives as a ref or a plain string depending on the host —
PosterWall records the same variance — so normalising it belongs in a considered
helper change, not here. The register asserts EXACTLY two, so a third bare call
still fails.

check:no-free-identifiers earned its keep on this batch: replacing that file's
vue-i18n import while two calls still referenced useI18n left a name that is valid
JS until it runs. Caught before commit, both imports now present.

The per-site register in check:optional-t-equivalence gains four rows (59
assertions).
2026-09-26 18:14:36 +00:00
Doxa Maps
63ab6719d2 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 18:07:44 +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
Doxa Maps
e3d25ff6db The people-group list is a universal prop: the same maps can show any project's list
Every map showed DOXA's people groups, and only those. The same maps must serve other projects'
lists: Project 7000 (unreached), 4700 (frontier), 700 (frontier of 500,000 or more), 2000 (DOXA).
The list is now chosen the way the engine is:
  profile-config `list` > ?list= > window.MAP_LIST > 'doxa' (exactly today's behaviour)
with ids or project aliases ('700', 'project-7000').

- library/lists/lists.json: each list is a FILTER over the IMB / peoplegroups.org dataset already
  in the repo (Lostness Priority, Population, Engagement Status), never a hand-kept roster.
- library/scripts/people-groups/build-lists.mjs (`bun run lists`): writes each list's DOXA members
  to data/people-groups/lists/<id>.json, joined the way the Vision Card funnel joins, so the
  counts agree with it: unreached 7,341 (2,082 DOXA), frontier 4,754 (1,745), frontier-500k 737
  (71), unengaged 2,982 (1,929).
- library/lists/peopleGroupList.js: resolvePeopleGroupList and applyPeopleGroupList(rows, cfg).
  The default list returns the same array, unfetched. A list whose membership cannot load shows no
  groups (fails closed) rather than DOXA's whole list under another list's name.
- Tier A, every production map: DataSourceManager applies it (and the maps that build one pass
  their profileConfig), and so does every direct people-group fetch: people-groups-grid, the
  Big Picture header, research-dashboard (data, CSV export; the DOXA-wide /statistics baselines
  are skipped under another list), Regions (list, dashboards, their table), UPG-100 (4 fetches +
  the poster wall), Gospel's roster, and the Experimental upg-neighbor and Wall. The composites
  thread `list` to their children like `provider`.

Gates: check:universal-props gains a `list` column (LIST-FETCH, LIST-DSM, LIST-KEYS; its
self-test now plants 9 bypasses) and passes across 18 bundles, with all-people-groups allowed:
it draws the IMB dataset, so its list is Tier B. New check:list-prop-unit (Node, 17 cases, in
gates:post); poisoning the ladder order or deleting a membership file fails it. Documented in
documentation/reference-universal-props.md section 5.

Headless: all 14 touched bundles build; check:composites-mount 20/20 and check:composite-interop
17 links pass on this tree; no-orphan-gates, script-flags, quoted-scripts, doc-scripts pass. Not
rendered in a browser.
2026-09-26 05:16:02 +00:00
Doxa Maps
65017fefa4 The prayer packet shows how its list was narrowed out of all the earth
Page 2 of every packet is now the funnel: 12,379 people groups on earth, 7,341
unreached, 4,754 frontier, 2,107 on this prayer network's list, and N of those
in the reader's hand. A partner holding one list can see where it sits in the
whole, which is the argument the packet is making.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Negative-tested on 521f8b18, and each of these fails: a receiver renamed in
source, a built bundle out of step, an unmapped composite, a new late
getCurrentInstance (one-line function, arrow, multi-line), and a stale KNOWN
entry. check:no-orphan-gates, check:script-flags and check:quoted-scripts pass.
It is not wired into gates:post; that is the build owner's call.
2026-09-26 04:21:49 +00:00