Commit graph

2922 commits

Author SHA1 Message Date
Doxa Maps
a70cf9df4f check:sentence-slots runs in gates:pre: it was described and registered as a script, but no gate list ran it 2026-09-29 05:26:26 +00:00
Doxa Maps
8c72b8a309 Cloud README: GET /admin/status is for anyone signed in; only POST /admin/build is admin-only
The route table put both under OIDC_ADMIN_GROUPS. admin.mjs checks the admin group on the
build only; status answers any signed-in team member (commit SHAs and build state, nothing
more). Measured: a designer reads /admin/status (200) and is refused the build (403, "builds
are for team admins").
2026-09-29 05:26:26 +00:00
Doxa Maps
ffebd14377 Cloud container: PocketBase's dashboard renders through /pb, its redirects keep /pb, embed-test reads the data API
Three bugs rega found on a laptop run of the container (2026-09-29):
- /pb/_/ never rendered. admin.mjs proxied with fetch(), which returns the body decompressed,
  but forwarded PocketBase's Content-Encoding: gzip and Content-Length, so the browser tried to
  gunzip plain HTML. The proxy now drops both headers (for /pb and /api-prod).
- PocketBase's own redirects left the mount: /pb/_ went to /_/, and /pb/_/index.html to
  http://127.0.0.1:8090/_/ (the internal address). Root-relative and upstream-absolute
  locations are put back under the mount: /pb/_/.
- The research host's /embed-test set no MAP_APP_API_URL, so the maps asked that static host's
  own /api (404s; dashboard-plus-grid drew 489 nodes, not 1,399). The page now sets it from the
  manifest's runtime.apiBaseUrl before any bundle loads, as every generated page does.

Checked both ways against an upstream that answers like PocketBase (gzipped HTML, redirects
to /_/). The old proxy sends a gzip label on a plain body and /_/ locations; the new one
sends a decodable body and /pb/_/.
2026-09-29 05:26:26 +00:00
Doxa Maps
93ebe64984 New gate check:tree-relative: the committed tree names no host of its own
An absolute embed address works where it was built and breaks everywhere
else, which is how the CDN route failed and was retired. The build config
keeps embedBaseUrl empty, so the committed tree is relative and a deploy
makes it absolute at build time (EMBED_BASE_URL). But nothing stopped a
tree built with EMBED_BASE_URL from being committed: check:drop looks for
root-relative "/..." references and deliberately skips https:// ones.

With embedBaseUrl empty in the config, the manifest must say embedBase "."
and every scriptUrl, iframeUrl and embed snippet src must be relative. Data
addresses (runtime.apiBaseUrl) are not the tree and are not checked. SKIPs
when the config sets embedBaseUrl. In gates:post, after check:drop.

Today's tree: PASS, embedBase "." and 173 embed addresses, all relative.
Poison-tested, each fails: an absolute snippet src, an absolute embedBase,
a protocol-relative iframeUrl, an empty manifest. A real deploy manifest
built with EMBED_BASE_URL fails with 174 findings, as it should.
gates:post: PASS 82, NOT MEASURED 13 (browser/server), FAIL 1: the known
check:packets-all worktree failure (no generated zone packets here).
2026-09-29 05:26:26 +00:00
Doxa Maps
8d8b38a4b2 Translate the dashboard chrome and the map's tab-scoped search
Eighteen keys across eleven locales for text that was written in English in
the components: the rail and filter toggles, the selection count, the CSV
export toast, the drawer's prayer and information links, the dashboard
title, the map's failure message, and the four search placeholders that name
the tiers of whichever tab is open.

The export toast is interpolated rather than concatenated. Its counts keep
going through fmt(), which groups digits the way each locale does, and only
the words around them come from the catalogue — the order of "rows",
"fields" and the outcome is not the same in every language, so a sentence
built by joining fragments cannot be translated at all. It uses the shared
fillParams, which leaves a placeholder visible when a value is missing
instead of rendering the word "null".

The map's failure message no longer names the token and the container: that
is for the console line, which stays English for whoever is debugging. What
the reader sees says what to do.

Three more shapes the scan could not see, each of them correct code: tStatic
called directly rather than through a wrapper; `config || t(key) || English`,
where the translator is not the immediate left operand because || is
left-associative; and the licence credits, media queries and product name
that must not be translated at all.

Own leaks 91 -> 1, the last being a tab label behind a key that resolves in
all eleven locales. Fleet 249 -> 159. check:i18n-parity,
never-translated, treeshake and stopped-scope all pass.
2026-09-29 05:26:26 +00:00
Doxa Maps
181f723bf8 The legend's Packet export did nothing on the research map
The legend emits 'packet:full' or 'packet:lightweight' — it widened its
existing event rather than adding a second one — and this router still
tested the whole string for 'packet'. Neither submenu item matched any
branch, so the Packet export opened nothing, logged nothing, and looked
like a menu item nobody had wired. Found by upg100, who had already hit
the same thing in its own copy.

The kind is split before it is compared, the way upg-100-list does it, and
a bare 'packet' still means the full packet so an older emitter keeps
working. The variant then has to survive the call as well: openPacket
ignored it and printPacket defaults to full, so lightweight would have
printed the full packet even once the menu fired.

The gate asserts the seam rather than one file — it reads the kinds the
legend actually emits and requires every consumer to split before
comparing, because this bug cannot be seen by reading either side alone.
It currently fails on country-posters/countries-map.vue, which has the
identical dead menu and is not mine to change.

Two fixes to the check itself while writing it: a fixed-size window read
only part of a long router and reported correct code as broken, so the
function's extent is found by brace depth; and a trailing \b after a
quoted literal never matches, which made the assertion fire on every
consumer including the working ones.
2026-09-29 05:26:26 +00:00
Doxa Maps
c858ac98e9 Recognise tStatic, and allow licence credits, media queries and the product name
tStatic is the library's translator for code outside a component's setup, and
it was missing from the set of names the scan knows. Anywhere it was called
directly rather than through a wrapper, correct guarded code read as a leak.
The two-sided rule still applies to it: a tStatic fallback is exempt only
when its key resolves, and the self-test asserts both halves.

Three allow shapes, each for a string that must not be translated:

A matchMedia query. Rendering "(prefers-reduced-motion: reduce)" in another
language matches nothing, so motion safety would switch off for exactly the
readers who asked for it, and nobody would see an error.

Tile licence attribution. The credit a data licence asks for is the one
string that stays as the licensor wrote it in every language. These render
only when the live sources cannot be read, which is the case a licence never
allows to come out empty.

The product name, and bare CSS position keywords.

Each shape is anchored rather than fuzzy: prose that resembles them —
"Start end of the list", "center the map on your region", "Doxa Maps is a
prayer tool for the unreached", a copyright line with a real sentence in it —
is still reported. 190 -> 178.
2026-09-29 05:26:26 +00:00
Doxa Maps
126ee06619 Translate the research map's legend tab explanations
The legend gives each tab a small "i" button explaining what that tier
means — what an Affinity Block is, how a people group differs from a people
group in a country. SemanticTreeLegend already read that text from
legend.tabInfo.<id> behind a te() guard, and not one of the ten keys the
research map needs had ever been written. The tab chips were translated in
all eleven locales and the explanations beside them were not, so the reader
least able to guess what "PGIC" means was the one reader who got it in
English.

The keys are written for all eleven locales, following the terminology each
catalogue already uses for these same tiers — People Group as Volksgruppe,
Peuple, Pueblo, 民族, Народ, जनसमूह — with WAGF and UPG left untransliterated
as the corpus leaves them. The reader-facing text drops the source field
names and row counts that the English carried; those are notes for whoever
maintains the tables, so they stay in the source as a comment.

With the keys in place the English in the tab tables can never render, and
a second copy of a string drifts from the catalogue without anyone noticing,
so it is removed. `info: true` stays because the legend gates the "i" button
on that field being set — dropping it would have removed the affordance
instead of translating it — and tabInfo now treats only a string as a
fallback, so the flag cannot reach the popover as the word "true".

Verified every changed tab renders a real label and a real explanation in
all eleven locales. check:i18n-parity, check:i18n-never-translated and
check:i18n-treeshake pass; hardcoded-english 205 -> 190.
2026-09-29 05:26:26 +00:00
Doxa Maps
61d97d8910 A host without i18n picks the same plural form an installed i18n would, in every language
useOptionalT's fallback (the path a bundle with no vue-i18n takes) picked a plural form by English's
rule for every locale, so "21 народ" and Arabic's six forms came out wrong there — regions caught it
before wiring their wall. The fallback now uses the SAME CLDR selection library/i18n/index.js installs
(cldrChoiceIndex, now exported), and English keeps vue-i18n's own rule, so both paths print the same
form. Checked against the real code: ru 1/3/5/21, ar 0/2/5/11, en 0/1/7.
2026-09-29 05:26:26 +00:00
Doxa Maps
f8744925e4 Every country prints its prayer list, card deck and packet, from a records provider any map can use
The country wall passed no records, so each of its 175 cards offered only the poster and said the
prayer list was "coming soon", while the people groups were one request away. library/lists/
peopleGroupRecords.js is the records half of the print standard: one people-groups fetch per language
(normalised the way the prints read rows, list membership applied), then a SELECTOR picks the set — a
country by ISO3, explicit slugs, any field, or a host's own selector language. A selector nobody
understands selects nothing rather than every group on earth, and a failed fetch is not cached.

The country wall uses it by ISO3, so every country now prints the list, the deck (all five stocks) and
the full or lightweight packet. The packet's page 1 is, for now, the country's stored poster when it is
really there; the in-browser map generator will replace that.

check:people-group-records asserts the selection two-sided with an injected fetch; registered in
gates:pre.
2026-09-29 05:26:26 +00:00
Doxa Maps
58e113549c The %-evangelical page says how many people one box stands for
Brief A1.4 asks the page to state how many people each cell represents; it printed the share and the
population but never the unit a reader needs to read the grid ("each box ≈ 40,208 people" for the
whole DOXA list). Computed with the screen chart's own peoplePerCell so paper and screen divide the
same way, in all eleven languages, and omitted when the list has no published data.
2026-09-29 05:26:26 +00:00
Doxa Maps
898f804570 Regenerate the library index for the helpers this landing adds 2026-09-29 05:26:26 +00:00
Doxa Maps
34fd21dd5d The last library reference drops the speaker, and the baseline shrinks to what is left
library/layouts/app-wrapper-material/index.html carried the one library
reference the sweep's file filter missed (it looked at scripts, components
and docs, not .html): reworded the same way, comment only.

The reference baseline then shrinks from 505 to 147 lines, so nothing
reworded in this landing can come back. What remains is in the owners'
plugins, documentation and skills.
2026-09-29 05:26:26 +00:00
Doxa Maps
ee87f8e9db Plugin Studio keeps the reason and drops the speaker: its 15 references to the Driver reworded, comments only 2026-09-29 05:26:26 +00:00
Doxa Maps
46a53f0b4e The carried commits keep the reason and drop the speaker
Thirteen lines arriving in this landing named or quoted the Driver: the
packet renderer and its two gates, and the desktop build. check:no-new-driver-references fails on each of
them, so they are reworded here the same way as the sweep: the date and
the requirement stay, the speaker and the quote go.
2026-09-29 05:26:26 +00:00
Doxa Maps
d314c44043 internal/ keeps the reason and drops the speaker: no gate quotes or cites the Driver
Every reference to the Driver in internal/ is reworded the same way as in
library/: the reason stays, the speaker and the quote go, an open decision
reads "provisional, pending a product decision", and agent names and decision-
sheet rows are dropped. Where a gate printed the word, its message now names
the reviewer or the ruling instead.

Three spots were code, not comments, and changed with their callers:
- the element census's allowlist field `driver` is now `ruling`, in the gate
  and both presets (same validation: match, ruling, date and card required);
- check:i18n-never-translated's DRIVER-GATED marker is DECISION-PENDING, in
  the entries and in the regex that counts them (its output is otherwise
  byte-identical);
- the package.json description of the census says "ruling".

public-hygiene-check keeps its two patterns for the word: they are what keep
it out of the served folder.
2026-09-29 05:26:25 +00:00
Doxa Maps
bfc99ff827 library/ keeps the reason and drops the speaker: no comment quotes or cites the Driver
Every reference to the Driver in library/ is reworded: the engineering reason
stays, the speaker and the quote go, and a decision still open reads
"provisional, pending a product decision". Comments only: each changed file
is identical after stripping comments (JavaScript, template and CSS, including
CSS comments inside the style strings shadow-DOM components inject).

Those style-string comments do ship, so several bundles change by exactly
those comment bytes.
2026-09-29 05:26:25 +00:00
Doxa Maps
474b0b4e8a Prove the sentence helper against a real t(), instead of claiming it
studio measured the trap: t(key) with no params BLANKS every placeholder. On
vue-i18n 9.14.5 "{nNone} of {nLang} have {noScripture}" comes back as " of  have "
— so sentenceParts would find nothing to split on and the sentence would lose its
values, silently, in every locale at once. I reproduced it before building on it.

keep(...names) returns { name: '{name}' } to hand t(), making the lookup a no-op
over the placeholders. Preferred over tm(), which returns the raw string only
while messages are compiled at runtime; this works with precompiled messages and
with plurals, which is the point of the pattern.

The previous commit SAID it works with precompiled messages. It was not measured,
and it was wrong in the way that matters: the helper was right and the call
pattern around it was not, so every arm passed while the real thing would have
rendered a blank sentence. Three new arms run an actual createI18n — English
renders, Arabic reorders the clauses, and the negative control asserts that
WITHOUT keep() the placeholders really are blanked, so if vue-i18n ever stops
doing that we find out rather than carrying keep() by habit.
2026-09-29 05:26:25 +00:00
Doxa Maps
396c8f2ee7 Stop counting a translator fallback whose key really resolves
T('dashboard.map_loading', 'Research map loading…') is the idiom these
components use: the English is what shows if the catalogue ever loses the
key, not what the reader gets. Forty-four findings were code of exactly
that shape, and triaging them meant reading catalogue entries that were
already complete in all eleven locales.

Two things were wrong. Wrapper aliases were not recognised as translators
at all — only `const X = t` was, never `const T = (key, en) => ...` — so
every fallback in those components read as a leak. And the rule that
exempted translator arguments exempted all of them, including the fallback,
which left the gate structurally unable to see a typo'd key: the English
then renders forever while the call site still looks correct.

So the key argument stays exempt and the fallback argument has to earn it,
by resolving to a real value in every shipped locale. A missing key still
counts. An identical rendering counts only when the English has three or
more translatable words: measured against this catalogue, the identical
entries are cognates (Normal in de, Auto in fr), proper nouns, and
format-only strings like "{label} · {count} · {pct}%", and failing on those
would report correct translations as leaks.

Bound expressions are parsed on their own, so they could not see the
translator declared in the script block; the declarations are carried in as
stubs. The stubs deliberately contain no || — written the natural way they
match the te()-guard shape and exempt every fallback in the file, resolved
or not.

Self-test covers both sides: a resolved fallback is quiet in template and
script, an unresolved one counts in both, and the catalogue rule is
asserted directly. 249 -> 205 across the fleet, all 44 in this owner's
plugins; detection elsewhere is unchanged.
2026-09-29 05:26:25 +00:00
Doxa Maps
c7677edd57 Report the line of each hardcoded string, not just the file
Triage meant grepping the reported string to find its site, and the first
textual match is very often a comment that discusses the string rather than
the code that renders it. Two findings were sited to prose that way, and a
count built on those sites was wrong.

Each finding now carries the line. <script> and <script setup> are scanned
separately rather than joined, because concatenating them made every line in
the second block wrong by the length of the first — a confident wrong site
sends the reader further off than no site at all. A bound expression is
re-parsed on its own, so its findings take the attribute's line instead of
the fragment's own numbering.

The self-test reads the reported line back out of the fixture and requires
it to contain the string, so an offset error fails rather than passing with
a plausible number.

Detection is unchanged: 249 before and after.
2026-09-29 05:26:25 +00:00
Doxa Maps
c0ff2d3714 A sentence that carries values is one translatable key
The pattern this replaces built a sentence by concatenating translated fragments
around untranslated text — "languages have", "of", "people." are each a separate
string in gospel-resources today. Arabic does not put those clauses in English
order, so translating the fragments yields Arabic words in English order:
grammatically broken, and worse than leaving it English, because it looks
translated and nobody re-checks it.

library/composables/useSentence.js takes ONE message with named placeholders and
a value per name. It renders sentinels, splits on them and drops the values back
in, so a translator moves the clauses freely and the markup survives as slotted
content: the bold counts and the dir="ltr" island are passed in, never written
into the catalogue. No <i18n-t>, and it works with precompiled messages.

Built to studio's four constraints: the caller passes a whole key literal (the
helper takes a MESSAGE and cannot assemble a key, so the build's area scan still
sees it); t comes from the calling component; a sentence with two counts receives
each already pluralised from its own key, since vue-i18n resolves one plural form
per key; and the sentinels are private-use (U+E000+n) so nothing in the pipeline
may rewrite them.

check:sentence-slots, 14 assertions, every arm with a negative control — and two
of them caught real defects in this change. A slot value of 0 rendered as an
empty string (the bug was in my test's flattener, not the helper — 0 is a real
count and "0 of 7,296" is a sentence a filter legitimately produces). And a
private-use character already IN a message was consumed as slot 0, printing
"a X b X" while the arm still passed because it only checked the value was
present; strays are now stripped before ours are placed, so every remaining mark
is ours by construction.
2026-09-29 05:26:25 +00:00
Doxa Maps
fa696cfd9b check:readme-size runs in gates:post, and the README states the folder with its posters
The gate measures the built folder, so it belongs after the build, in
gates:post, with its description beside the others. It now reads the tree
name from _tree-name.mjs like every other tool: the folder was renamed
once already, and a gate hardcoding the old name kept reading it.

The row now says 1205 files, 154 MiB: the folder as it stands with the
350 city and country posters this landing ships.
2026-09-29 05:26:25 +00:00
Doxa Maps
05d7724bfe README + DEPLOY: no links into private repos; the web folder's size is measured by a gate
dx-base measured two defects in the A8 draft for text that goes public:
- Links into the private DX Life repo answer 404 to anyone without an account. The live
  example is now plain words (README, DEPLOY), and the clone line points at the public repo
  (doxa-open/doxa-maps, as DEPLOY already did). DEPLOY's "three ways" becomes the two routes;
  the CDN route is retired.
- The web folder row said 536 files, 76 MiB. The folder is 855 files, 119.2 MiB. New
  check:readme-size re-measures public/dx-maps and fails when the row is more than 10% off,
  printing the line to paste. It passes at 855/119; the old numbers fail.
2026-09-29 05:26:25 +00:00
Doxa Maps
6038084ea8 UPG Neighbor keys: the phone list can be hidden and shown again 2026-09-29 05:26:25 +00:00
Doxa Maps
c067a5d069 Dictation fell behind because our own build took every core
Rudder was measured slowing after about 1,400 words while a Tauri build
ran. The word count is incidental — it is a proxy for how long the build had
been going. The speech model runs on the CPU (backend/rudder-engine starts it
with --device cpu), and a Rust build saturates every core, so at equal priority
it starves the one process whose latency a person can hear.

Three changes, and the direction of the first is forced rather than chosen:

1. internal/desktop-build.mjs nices the whole build tree to 10, through the one
   run() helper every step goes through, so cargo, rustc and the linker inherit
   it. We nice the COMPETITOR because an unprivileged process cannot raise its
   own priority on Linux — a negative nice needs CAP_SYS_NICE — so "promote the
   ASR" would fail with EPERM. If nice(1) is missing the build still runs and
   says so, because a silent fallback would look exactly like the fix working.

2. cargo is capped at cores-1. nice decides who wins a CONTESTED core; it does
   not stop the build taking all of them, and a fully subscribed machine still
   adds scheduling latency to the audio path. An explicit CARGO_BUILD_JOBS is
   honoured rather than overridden.

3. yieldCPU reports its error instead of discarding it. It was
   `_ = syscall.Setpriority(...)`, which cannot fail visibly — a call that
   silently does nothing is indistinguishable from one that worked, and this one
   guards something audible.

Also, one real O(n) in the widget, found while checking studio's answer that the
collapsed pill already shows the last words: sendActivity split the ENTIRE draft
on every activity tick to take five words. Measured at 3,000 words: 1.61ms per
tick against 0.051ms for a bounded tail, 31x, with byte-identical output.

Verify the priority work with `ps -o ni=` on the running cargo processes, never
by any of these calls returning.
2026-09-29 05:26:25 +00:00
Doxa Maps
80ddec1459 Describe check:posters-exportable beside the other poster gates 2026-09-29 05:26:25 +00:00
Doxa Maps
6c76d42881 New gate check:poster-fetch-guard (in gates:pre)
Two halves, Node only. The helper, against fake responses: a real WebP
is accepted by type or by its bytes; a host's 200 fallback page is
refused as a raster and as a source; HTML labelled octet-stream is
refused; a 404 and a network error are misses for the raster; a network
error on the source is thrown as itself. The callers: neither
usePosterPrints nor PosterWall may fetch posters/ directly again, since
each once carried its own copy and a guard in one did not reach the other.

Poison-tested: restoring the old accept-anything fetch fails 3 checks,
and PosterWall's direct fetches failed the caller check before its switch.
gates:pre: PASS 67, FAIL 0.
2026-09-29 05:26:24 +00:00
Doxa Maps
33299253b0 PosterWall: fetch posters through poster-fetch.js
PosterWall carried its own copy of the poster fetch, so the fallback-page
guard added to usePosterPrints did not reach it: a missing .webp answered
200 text/html by a host was still handed to the print dialog as the image.
It now uses fetchPosterRaster and fetchPosterSource. A thrown .webp fetch
is still a miss that falls through to the source, and a missing source
still reads "poster not found (<status>)".
2026-09-29 05:26:24 +00:00
Doxa Maps
4fc4b8e9c8 Take a poster only if it is one: a host's fallback page is a miss
On a drop-in host's dev server a missing posters/<id>.webp answered
200 text/html (the app's own page), and export handed that page to the
print dialog as the image. The .html source path had the same hole: the
rasteriser lifts the first <svg> on whatever page it is given.

library/packet/poster-fetch.js now fetches both. A raster counts only if
it is an image: by content-type, or by its first bytes when a host labels
a WebP application/octet-stream. A source counts only if it is a poster
document (its class="stage" element); a 200 that is not one reports 404.
A thrown fetch on the raster stays a miss, so the source fallback still
runs; a thrown fetch on the source is still reported as itself.

Measured against a real local HTTP server with a single-page-app fallback:
real WebP accepted (image/webp and octet-stream), missing WebP refused
(200 fallback page and 404), real source accepted, missing source 404
(200 fallback page and 404), unreachable host a miss for the raster and
"fetch failed" for the source.

usePosterPrints uses it. PosterWall follows in its own commit.
2026-09-29 05:26:24 +00:00
Doxa Maps
be220d23ad Rebuild the tree: the 350 city and country posters now ship 2026-09-29 05:26:24 +00:00
Doxa Maps
ccb159f56f Ship every poster once, in its lightest printable form
With posterSources on, the tree shipped only the 28 zone poster sources,
so exporting any of the 176 city or 174 country posters said "poster not
found (404)": 350 of 378 posters could not be printed.

Each poster now ships once: its .html source where one exists, else its
.webp. Generating sources for the 350 was not an option: city posters
need OSM street tiles to regenerate (publish-html.mjs), and a source is
the heavy form (about 1 MB each against about 0.1 MB for a raster).

Measured with check:tree-weight: 102.7 MB before, 137.9 MB after, of
160 MB. The 350 rasters are the tracked data/posters/*.webp: all 350 are
existing git blobs, so the repository gains no new pack bytes.

New gate check:posters-exportable (in gates:post). Every poster id, from
data/posters and the wall's previews, must ship a real WebP or a real
poster source (its class="stage" element), so a placeholder or a host's
fallback page cannot pass. It skips when posters are off by config, which
check:poster-degrades covers. Poison-tested: a missing poster and a fake
image each fail.
2026-09-29 05:26:23 +00:00
Doxa Maps
25828469f3 Card stocks are named in the page's language; their sizes stay as a printer's box prints them
The five stock labels were English literals in every locale ("Card 2.5 × 3.5 in" in Romanian), in a
file whose own header promises every string comes from the catalogue. The DECIDE card asked whether
paper names translate at all; this takes the middle that answers it for both readings: the NOUN
translates (Card, Postcard, landscape — wall.stockCard / stockPostcard / stockLandscape in all eleven
languages) and the size does not, since "2.5 × 3.5 in" and "A6" are what the paper box says in any
language. A6 has no noun and prints as named; English prints exactly as before.
2026-09-29 05:26:23 +00:00
Doxa Maps
cc3406cfe4 The UPG-100 list prints its packets through the shared lift, not a second copy of it
The list kept its own page-1 map lift (packetMapSvg + packetLabelFor) beside the one in
usePosterPrints, and the two drifted: the list's relabelled country names, the wall's did not, which
is the zh finding rega made in the app. With the composable now taking iso3Names, the list passes it
and deletes its copy, so the list, the wall and any other map print page 1 through one path.

The selection packet borrowed one list's sheet when it held exactly one list. That is now a general
seam: item.mapId names the sheet when it is not the item's own id, and mapId: null says the item has
none, so a multi-zone selection fetches nothing and page 1 says the map is missing. Measured two-sided
through the real printPacket. gates:pre 66/66.
2026-09-29 05:26:23 +00:00
Doxa Maps
275ff51523 The tree legend's tabs, info buttons and carets are 24px tap targets
rega measured 13 controls under the 24px minimum in the country map's legend at 390px: the tier tabs
at 22px tall, their info buttons at 18x22, and the row carets. The tabs take a 24px minimum height and
the info buttons a 24px square. The carets grow to 24x24 while a -5px margin returns the extra space,
so the tree's indentation (which shares the caret width) does not move. One library component, so
every map with this legend gets it.
2026-09-29 05:26:23 +00:00
Doxa Maps
c83a9827ff Country Posters and the UPG-100 wall read in the page's language, and walls count in each language's plural forms
The i18n + RTL audit of Country Posters (the hardcoded-English gate only reads production plugins,
so it had never read this one; its scanner was run on the plugin directly and every hit triaged for
reachability, since the country map mounts one tab):
- 9 strings a reader sees were English in every locale: the search placeholder, the packet title's
  "— 50 of N", the map-failed message, the wall's heading, tag, intro, soon message and item nouns,
  and the view switcher's legend and label. All read from the catalogue now, reusing the existing
  keys where they exist (tabs.selectView/views, the UPG-100 wall's tag and emphasis).
- The wall's section headings are the WAGF region labels in the data, English everywhere; they now
  resolve through the Regions dashboard's own WAGF table, which gains its two missing regions.
- The three WAGF tier tooltips had no keys in any locale (legend.tabInfo.region/block/country).
- 0 physical-CSS findings.

PosterWall counted "N countries" as a number plus a 2-form noun pair and defaulted the plural to
noun + "s" — wrong for Russian and Arabic whatever strings it was given. It takes itemCountKey, a
plural catalogue key rendered with the count; useOptionalT forwards a plural count to vue-i18n (the
CLDR map picks ru 4 forms, ar 6, zh 1), and a host without i18n picks the English form by
vue-i18n's own rule. The people-group counts on the section heads and placeholder cards use
wall.peopleGroupCount the same way ("people groups" was hardcoded English on the cards).

The UPG-100 wall: its Americas and Europe section headings had no key (single words, invisible to
the gate), and it counts its lists with wall.listCount.

Checked: plural-forms, i18n-parity, keys-exist, never-translated, and i18n-treeshake against both
bundles built (26 bundles carry every area they ask for); gates:pre 66/66.
2026-09-29 05:26:23 +00:00
Doxa Maps
0c40927dcf The prayer points name their scripture in the packet's language
Every translated packet printed its references in English — "Matthew 27:27" in Chinese, Arabic
and the other nine — because refs are kept as canonical addresses and were printed verbatim. The
address stays canonical in the data; the BOOK is now named from a per-language table (the ten books
the points cite, in each language's common form: 马太福音, متى, Мф., Matthäus …). A localized
name in a right-to-left script is isolated rtl so it reads on the right with its chapter:verse
intact. Measured: 24 references per packet, 0 English book names left in zh, ar and ru.

Point 4's printed passage is untouched: which licensed translation to print per language is still
an open decision.
2026-09-29 05:26:23 +00:00
Doxa Maps
612d9c3ea8 The print standard takes a map the host generates, and says what it takes
A host that draws its map live (the regions country map) has no posters/<id>.html to lift, so its
packets printed page 1 with the missing-map notice. usePosterPrints gains opts.mapImage(item): an
image or data: URL of the host's own map, placed by the renderer (mapFromImage) exactly as a poster
sheet is, the whole page inside the white margin. Order for page 1: an explicit mapSvg, then the
generated map, then the stored sheet. PosterWall forwards it as a prop. Measured two-sided through
the real printPacket: with mapImage the image is page 1 and no sheet is lifted; without it the
stored sheet is.

The composable's header now documents the whole contract — every option, which are optional, and
the row shape — so a map adopting it reads one comment instead of the wall that uses it.
2026-09-29 05:26:23 +00:00
Doxa Maps
4e2685cdad The packet follows the printed Module 1 sheets: page 1 is the map alone, the calendar holds Jesus' 33 years, the sets share one page
A review of the full-detail packet, page by page:
- Page 1: a poster carries its own title, so the packet's title above it printed two headers and
  shrank the map to fit beneath them. A poster sheet now fills the page inside the white margin by
  itself; in another language its kicker and title become the packet's own, fitted to the English
  width. A map-only sheet keeps the packet's title in the band it leaves empty.
- The life calendar is laid out like the Module 1 "Life Calendar in Weeks": square boxes, week
  numbers across the top, the year down the side, a rule between decades, framed and centred. The
  first 33 years are shaded as the life of Jesus (1,716 weeks).
- The %-evangelical grid fills from the top in reading order instead of scattering.
- The flow pages had no page padding at all, so their headings sat on the sheet's edge; they keep
  the same margin as every other page now.
- The bounded and centered sets are one page, two square panels side by side as on the Module 1
  list of 100, each headed by name with a cross at its centre. The centered set is a target of nine
  numbered bands, its dots spread through their bands by the shared ringBands geometry (no overlaps,
  none on the numbers), and the legend explains every ring with its count.
- The lightweight packet lists every group as a worksheet: a short note, one column header per page,
  numbered rows sized for paper with room to tick, no photos or descriptions.

Seven new strings in all eleven languages. check:packet-a1-structure and check:packet-variants
assert the new contract two-sided (a poster page 1 has one header, a map-only page 1 still has the
packet's; every ring is named, a populated one with its count and an empty one with a dash).
2026-09-29 05:26:23 +00:00
Doxa Maps
ff0c433fce A packet printed from the wall names page 1's countries in its own language
The list's own print relabelled the map sheet (mapLabels.js); the wall's print, through
usePosterPrints, lifted the sheet with no relabeller, so a zh packet printed from the wall kept
INDIA, CHINA … on page 1. The generator's ISO3 -> name table is plugin data the library cannot
import, so the host now supplies it: PosterWall iso3Names -> usePosterPrints iso3Names, and the
UPG-100 wall passes its territory-counts table lazily. A host that passes nothing prints exactly as
before. Measured two-sided through the real printPacket on a tracked sheet: without the table the
page carries CHINA and no 中国; with it, 中国 and no CHINA.
2026-09-29 05:26:23 +00:00
Doxa Maps
98277dc30e Splitting a template literal invented eight English strings that were not there
check-hardcoded-english judges each quasi of a template literal on its own, which
is correct -- an allow-listed fragment must not drag a whole literal in with it.
But splitting `url(${base}/images/worldmap.svg)` leaves "url(" and
"/images/worldmap.svg)", and `repeat(${n}, minmax(0, 1fr))` leaves "repeat(" and
", minmax(0, 1fr))". The whole string is obviously CSS and the existing CSS shape
matches it; no individual fragment carries a brace or a colon-value pair, so each
one fell through and was reported as user-visible English.

Colon-terminated prefixes failed the same way. The id rule accepts a trailing
hyphen, because `id="country-${code}"` leaves "country-", but a langTree id is
`fam:${x}` / `dial:${x}` and a localStorage key is `pgg-cards-per-row:${profile}`.
Those leave "dial:" and "pgg-cards-per-row:", which no shape described.

Two shapes, both narrow: a CSS function fragment, and a prefix that is one
lowercase token ending at a colon. The fleet total goes 257 to 249 -- five of
mine, three in other owners' plugins -- and every real string still flags, which
I checked in the same breath rather than afterwards: "Search people groups…",
"Show filters", "selected" and "Research map loading…" are all still caught.

Worth saying plainly because the number was about to be routed as work: these
eight were never translation defects. Wrapping "dial:" in t() would have been a
bug, and someone would have done it on the strength of a count.
2026-09-29 05:26:23 +00:00
Doxa Maps
16fc084d7f README: vibe coders named at the top, a cloud development environment, one line on SRI
- Bottom line up front names the persona dx-maps is for, vibe coders, and links to what
  they get.
- "Cloud development environment": by default bun run dev is fully local. DX_SYNC_REMOTE and
  DX_SYNC_BRANCH in .env.local make it follow a shared branch. It fast-forwards only a clean
  checkout on that branch, and never merges over local work. If your own agents also edit
  the checkout, it stops following, so push small and often or the landings pile into a merge
  conflict.
- The drop-in route's live example pointed at a branch that doesn't exist (research-gallery);
  it is research-gallery-drop-in, proven with every production map passing its embed checks.
- One line on SRI where hosts copy snippets: the integrity hash makes a changed bundle
  refuse to run, so copy the snippet again after each release.
2026-09-29 05:26:23 +00:00
Doxa Maps
35df7410d0 Rebuild wall-of-people-groups pplr-data-maps, and regenerate the tree 2026-09-29 05:19:54 +00:00
Doxa Maps
7160126831 Wall of People Groups: the verse file's comments name the brief and the chosen footer, not a person 2026-09-29 05:17:38 +00:00
Doxa Maps
a812056e71 PPLR Data Maps: on a phone the ten views are one select, and the prayer map's coach-mark stays off this research tool
rega at 390 px: wrapped, the view tabs took five rows (160 px of 844), and the shared 'Tap a coloured dot, someone still needs prayer' hint sat on top of the view's own bottom bar and attribution. On a phone the views are one select in one row (the tabs stay on a wide screen); pinHint is off, as for UPG Neighbor and the Wall.
2026-09-29 05:17:38 +00:00
Doxa Maps
042803ab3a Wall of People Groups: photos are kept as small copies, text is measured a little at a time, and off-screen sheets skip rendering
Measured on the live list: 77 photos are 1024 x 1024 (4 MB each decoded) and drawImage downscaled each of them on every frame it was visible. A loaded photo is now kept at 320 px on its long side (createImageBitmap with resize, or one drawImage into a small canvas where that is missing), and the original is released; the print frame loads the originals itself. Below text zoom no card is measured (the photo box uses an estimate), and at text zoom at most 150 new cards are measured per frame. The overlay's sheets use content-visibility:auto. The host hook reports the counts (photos loaded, thumbnails, fallbacks, cards measured) for the performance probes.
2026-09-29 05:17:38 +00:00
Doxa Maps
daf8a335ea Wall of People Groups: the poster comes in 3 by 4 ft (the default), 2 by 3 ft and 5 by 8 ft, and every size keeps its scripture readable
The brief's three sizes. At 3 x 4 and 2 x 3 ft a verse under each of ~2,100 cards would print at 3 pt and 2 pt, so those sizes use the strip layout: cards of name and photo, and one blessing verse per row on a strip under it at 9 pt (33 and 34 verses). 5 x 8 ft keeps a verse under every card at 6 pt. The footer's height now comes from its text (the whole passages), with the journey never below 6 pt; the screen shows the strips as it shows the rest of the poster.
2026-09-29 05:17:38 +00:00
Doxa Maps
9e82ac8dd1 Wall of People Groups: the poster's footer quotes the whole passages, Numbers 6:22-27 and the journey's seven, and the chart cites the passages its verses come from
The priestly blessing is Numbers 6:22-27 in full: the Lord's word to Moses and Aaron in small type, the three blessing lines large, and the promise that closes it. Each station of the journey quotes the passage the brief lists: Genesis 12:1-4, Genesis 15:5-6, Galatians 3:13-14, 1 Peter 2:4-8 (the cornerstone, at the centre), Galatians 3:5-7 and 28-29, 1 Peter 2:9-10, Luke 19:9-10. The chart's New Testament verses name their passages (Matthew 21:21, from 21:18-22; Matthew 8:26, from 8:23-27). All WEB, verbatim, checked by script against ebible.org.
2026-09-29 05:17:38 +00:00
d74e4d1659 Rebuild every bundle the regions locale reaches, not just my own
The new prayer-list key went into library/i18n/locales, and every bundle inlines
the i18n areas it uses -- so rebuilding regions-dashboard alone left other
consumers shipping the old catalogue. Caught before the landing scan did:
country-posters' committed bundle rebuilt to different bytes, which is the
stale-artifact case the build rule exists for.

build:bundles, then tree, then fix:sri. Nine bundles changed; the rest were
already byte-identical. fix:sri 127 agreed, 0 stale. Verified by rebuilding
country-posters again afterwards and comparing byte-for-byte.
2026-09-29 05:10:23 +00:00
a0ea8b2c66 Rebuild regions-dashboard for the translated poster-wall nouns
regions-list.vue is bundle source, so the tree alone would have shipped a stale
.js with the English literals still in it. Rebuilt the bundle, regenerated the
tree, then fix:sri -- in that order.

Verified in the built artifact rather than the source: the new prayer-list key is
present in the bundle in Arabic, German and Russian, Country/Countries resolve
through t(), and the old item-noun="country" literal is gone (0 occurrences).
fix:sri 127 agreed, 0 stale. The three leaf HTML diffs are only the new
integrity hash following the rebuilt bundle.
2026-09-29 04:51:30 +00:00
90613f9c88 Translate the three poster-wall nouns the regions list was passing in English
sr's census found item-noun="country", item-noun-plural="countries" and
unit="UPG" passed as literals. All three reach the DOM: PosterWall renders the
noun pair at its section count (n === 1 ? itemNoun : plural) and the unit beside
each row's count, so in Arabic the chrome translated and these stayed English.

The noun pair now goes through t(). Country and Countries were already in the
catalogue in all eleven locales -- Land/Länder, الدولة/الدول, Страна/Страны --
so this adds no strings, and PosterWall keeps making its own singular/plural
choice with the caller supplying both forms, which is what its props are for.

unit stays "UPG" on purpose. It is DX-life's term of art, and their own Arabic
common.json carries the literal "UPG" rather than translating it; matching them
is the point of the port. It is the one entry the census still reports here.

A fourth literal the census did not flag went with them: the prayer-list
"Coming soon" sentence, which reaches exportState.message and so renders too.
That one had no key, so it lands as an English-source key -- the convention
useRegionsI18n exists to serve -- translated in all eleven locales, 77 keys
each, parity green.

MEASURED, this plugin's entries in check:hardcoded-english: 3 before, 1 after.
2026-09-29 04:51:30 +00:00