Commit graph

3125 commits

Author SHA1 Message Date
Doxa Maps
5e99195fdf rudder-voice README: the desktop build is no longer "not yet" on Linux
"Not yet" still listed the desktop build with the engine as a Tauri sidecar.
On Linux it builds and runs; what is still not done is macOS and Windows
(the engine cross-compiles for both and has never run there).
2026-09-29 14:40:11 +00:00
Doxa Maps
9227ad8eb7 README: show Rudder running as a desktop app
Output 3 showed only the map app as a desktop app. Rudder is the desktop
example the README describes, and it has now been built with
`bun run desktop:build rudder-voice` and run on Linux: a screenshot of the
running app goes under the map app's, with one line on how to build it.

The PNG carries no text chunks (the original recorded its creation time
and timezone); the pixels are unchanged.

HELD since 2026-09-29 01:32Z for a condition from the Driver whose message was cut off ("before you add it to the readme you nee"). It is merged now on the 14:38Z merge-all order. Drop this commit if that condition is not met.
2026-09-29 14:40:11 +00:00
Doxa Maps
dbee30e827 Rebuild pplr-data-maps upg-neighbor, and regenerate the tree 2026-09-29 14:30:26 +00:00
Doxa Maps
a92f03dd43 Key Cities: a row's languages read as a list in the page's grammar, and a shortened one says so
The row meta joined its first three languages with English commas and cut the rest silently, so a truncated list read as complete. It now goes through formatList (Intl.ListFormat) as a conjunction whose LAST item states the cut, from keyCities.list.moreLanguages ("{n} more", landed in R29): "Hindi, Urdu, Bengali, and 2 more" / "Hindi وUrdu وBengali و2 أخرى" / "Hindi、Urdu、Bengali和另外2种". Three or fewer languages show as they are. Not 'unit': in zh it joins names with no separator, and in ar and hi it still adds a conjunction (studio, measured).
2026-09-29 14:29:20 +00:00
Doxa Maps
1d78d83a98 PPLR and UPG Neighbor: two lists read in the page's own list grammar, through formatList
join(', ') is English list grammar. The two user-facing sites now go through the library's formatList (Intl.ListFormat, R29), as conjunctions because each is the FULL list:
- PPLR's Rosetta popup, "Not yet cross-mapped in PPLR: {ids}";
- UPG Neighbor's bridge-language cards, "Believers in: {list}".
Key Cities' shortened language list follows once its "{n} more" key (keyCities.list.moreLanguages) is on development. The joins that stay are not reader lists: a thrown error's details, the bridge-languages CSV columns, and a gate's console line.
2026-09-29 14:29:19 +00:00
Doxa Maps
2dd05a58bf Rebuild my-upg-100-list, and regenerate the tree 2026-09-29 14:22:38 +00:00
Doxa Maps
a6482b9f5d Join the UPG-100 map's name lists the reader's way, not with ', '
Two reader-visible lists were joined with a hardcoded ', ': the ADM1 names
in the selection error ('no people-group data for {names}') and the
selected territories in the batch HUD. Both use formatList now (R29), so
de reads 'Gujarat, Rajasthan und Odisha', zh 'Gujarat、Rajasthan和Odisha',
ar 'Gujarat وRajasthan وOdisha'. The locale is passed from the i18n ref
(packetLang), not left to getRuntimeLocale, so the HUD's list re-renders
on a runtime language switch. formatClipboard's join stays: its only
caller is a test handle.
2026-09-29 14:21:57 +00:00
Doxa Maps
f342590e0d Rebuild dashboard-plus-grid, which compiles the same dashboard
dashboard-plus-grid.vue imports research-dashboard.vue directly, so the list
formatter reaches a second bundle through that import and the composite was
left holding a dashboard that no longer matched its source.

+362 bytes, the same delta as the dashboard's own bundle -- which is what
identifies this as the same change arriving by a shared import rather than
unrelated drift. The formatter appears once in these bytes too, with the two
list types at the counts the source asks for.

Found by the landing precondition, not by inspection: it reported the composite
stale on a commit that named none of its files. The first reading of that was
that it could not be mine. It is: a composite that imports a plugin's component
is downstream of every change to it, and a range that touches no file of the
composite can still be the reason it is stale.
2026-09-29 14:15:25 +00:00
Doxa Maps
d387c6a456 Rebuild research-dashboard for the localised filter lists
The list formatter is a library function the bundle had no reference to before,
so the source change does not reach a reader until the bundle carries it.

+362 bytes. Confirmed in the built output rather than inferred from the delta:
Intl.ListFormat appears exactly once, which is the single formatter
implementation and not a copy per call site, and the two list types appear at
the counts the source asks for. A byte delta alone cannot tell a shipped
conversion from a duplicated helper; counting the shipped strings can.

The tree is regenerated, not only re-hashed. fix:sri rewrote the integrity in
each leaf page and in the manifest's embed snippet, but NOT in the Mapbox
variant of that snippet beside it, which the tree generates and fix:sri does
not touch. The landing precondition refused the commit for exactly that: a
manifest that regenerating the tree would change. After the regeneration the
current bundle hash appears in all three places it belongs, and check:sri
passes across all 65 leaves.
2026-09-29 14:08:04 +00:00
Doxa Maps
090b4eeb16 Research dashboard: filter lists read as lists in the reader's language
Three places joined names with ", ", which is a list only in English. Arabic
separates with a word, Chinese uses a different mark entirely and no comma at
any position, and German and French put a word before the last item. The
separator and the final connector are the grammar, so no catalogue key can fix
them from outside.

Two of the three carried a second, worse error. A multi-value filter holds
ALTERNATIVES -- the bus matches a row if ANY of its values matches, in
useSelectionBus with some() and in the data table with includes() -- so the
chip means "Islam or Hinduism". Joined with a comma it reads as "and" in every
language with an explicit connector, which states the opposite of what the
filter does. Those two become a disjunction.

The third is the screen-reader live region, which names the facets currently
applied. Those hold together and narrow the set in turn, so it is a
conjunction. It is also the one string here that some readers rely on entirely,
having no chips to glance at.

The composable is the single owner: every surface that turns a filter into
words comes through it, so the chip bar, the research-filter chips and the CSV
provenance line now say the same thing in the same language.

Left alone deliberately: five joins in this plugin's gate scripts. Those are
failure messages read in a terminal by whoever runs the gate, not text shown to
a reader, and formatting them per locale would help nobody.

Source only. This needs the bundle rebuilt before it ships.
2026-09-29 14:08:04 +00:00
Doxa Maps
9db9f519d4 Rebuild cities-internet, gospel-internet and gospel-resources
Regenerated on this base rather than carried over: BUNDLE=<name> bun x vite
build for each, then bun run tree and fix:sri.

Three integrity hashes move, one per bundle. The MapLibre and Mapbox CDN pins
are byte-identical on both sides, and the only bundle paths in any changed
manifest line are ./cities-internet/, ./gospel-internet/ and ./gospel-resources/.

Verified in the BUILT output, not only in source: the shipped
cities-internet.js carries position:relative on :host, which is the declaration
the partner-page overlap depends on.
2026-09-29 14:03:14 +00:00
Doxa Maps
c5e2131fc0 Two lists joined with an English comma in every locale
formatList (R29) replaces join(', ') at the two sites that a reader sees. The
separator was never a missing key — no key can fix punctuation between
interpolated values — which is why both survived every i18n gate.

connectivity-map.vue:365, CONJUNCTION. The Indian states with no boundary are
ALL missing one. The names land inside a translated sentence at :595, and the ar
message reads "لا توجد بعد حدود ولاية لـ {names}: …", so English commas were
being printed inside Arabic prose.

GiCombos.vue:49, DISJUNCTION, decided from the matcher and not from what reads
better: useSelectionBus.js:299 tests cur.includes(value) and toggleFilterValue
adds to the list, so two values on one facet WIDEN the result set. "Africa and
Asia" would state the opposite of what the filter does. This changes the English
too — "Africa, Asia" becomes "Africa or Asia" — which is a gain, because a comma
silently reads as ALL.

Measured, the same three names and two facet values through each type:
  en  "Ladakh, Puducherry, and Chandigarh"   "Africa or Asia"
  ar  "Ladakh وPuducherry وChandigarh"        "Africa أو Asia"
  zh  "Ladakh、Puducherry和Chandigarh"        "Africa或Asia"
  was "Ladakh, Puducherry, Chandigarh"        "Africa, Asia"
ar joins with و and no comma at all; zh uses 、. Neither is reachable by keying.

Not converted, and deliberately: useBoard.js:126 is a console.warn for
developers, and rudder-voice.vue:971 is in a component with zero i18n imports and
no catalogue area — English by construction, so it converts with the rest of
Rudder if Rudder is ever localized.

Source only. The rebuilds are regenerated after this rebases onto landed R29.
2026-09-29 14:01:38 +00:00
Doxa Maps
60cb4ff184 The embedded map still painted 478px below its host box
73bc270bc gave :host a height and stopped the ELEMENT overflowing. It did not
stop the MAP, because it never made the host a containing block.

.gm is position:absolute;inset:0, which resolves against the nearest POSITIONED
ancestor. :host had display:block and a height but no position, so on a partner
page .gm resolved against whatever the PARTNER had positioned and the map sized
itself to that. rega measured it in the hostbox run on ca07fc3c8: the element
correct at 420px inside a 420px box, the canvas inside it still 900px, and the
"Tap a coloured dot" pill and the attribution painting 478px below the box —
538px in a 360px box — over the partner's own text, whose top sits at y=464/404.

position:relative on :host is the whole fix, and it is the house pattern:
prayer-walk-planner.vue:628 carries the identical shape with an absolute child.

Worth recording, because the first fix passed every check: a host box that is
the right size proves nothing about what is drawn inside it. Only a partner-page
shot with content BELOW the box can see this, which is why rega's hostbox run
found it and my source reading did not.
2026-09-29 14:01:38 +00:00
Doxa Maps
c9caf68e44 Public Studio: the raw plugin registry is refused too, so the gated bundle's name stays private
In ACCESS=public mode the guard refused a gated bundle's source and its /__leaf/ pages, but not
/__plugin-gallery/registry.json. That file is the raw discovery list for the smoke tools, and it still
names key-cities, its tag and its two profiles to any visitor. No data, but a gated plugin must not reach a
public surface. The Studio reads only manifest.json, which already leaves gated bundles out, so the
registry is now refused in public mode as well. Dev mode is unchanged, and the smoke tools that read the
registry run against a dev server.
Measured with the cloud config run locally:
- before: public mode served registry.json (200) naming key-cities;
- after: public mode answers 404, including with a query string and a percent-encoded name;
- manifest.json and / still answer 200 in public mode, and dev mode still serves registry.json.
2026-09-29 13:54:30 +00:00
Doxa Maps
004d1d3f51 Rebuild the consumers main left stale (build owner, README rule 5)
check:composite-freshness found stale on main: none. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-29 13:45:44 +00:00
Doxa Maps
f96576fdf7 check:optional-t-equivalence registers ApproxPinNote on the {t, te, locale} form
ApproxPinNote moved from useOptionalT to useOptionalI18n (which wraps it) so its formatList names follow the sentence's own locale. The per-site register still asserted the old helper and failed. The row now names useOptionalI18n, as SemanticTreeLegend, PosterWall and all-people-groups already do, and still asserts the site passes its own APPROX_FALLBACK.
2026-09-29 13:34:37 +00:00
Doxa Maps
5a73e4d37d Guard the pan's control clearance, which a cut-height check cannot see
The popup auto-pan had no gate. Its founding bug was invisible to the obvious
one: measured at 390 on a 366x732 map, the pan fitted the popup inside the
container, the cut really was zero, and a real click on Full Profile still did
nothing, because the top element at the button's centre was
div.maplibregl-ctrl-attrib-inner on one pin and span.pin-hint-pill on the other.
Two pins that looked fixed and were not.

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

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

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

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

Mutation-tested three ways, each failing the case written for it: ignoring
bottomInset (the pre-fix behaviour) fails 15 cases; reserving only half the band
fails; dropping the Math.max clamp fails exactly the negative-inset case.
24 cases, registered in all three places, verified by re-reading the parsed file.
2026-09-29 13:31:24 +00:00
Doxa Maps
46266a7ecc The popup pan clears the map's bottom controls, not just the clip
Clearing the clip is not the same as being clickable. The attribution and the
pin hint are painted over the canvas and take pointer events, so a popup that
ends fully inside the frame can still have its buttons underneath them.

Measured at 390 on a 366x732 map: the pan cleared the clip exactly (cut 0) and
a real click on Full Profile still did nothing, because at y~701 the top element
at that point was div.maplibregl-ctrl-attrib-inner; on the sibling pin it was
span.pin-hint-pill. Both pins panned, both looked fixed, neither was.

Neither the popup nor the control is at fault. Attribution is a licence term of
the basemap, so raising the popup over it is not available to us. The usable
frame is simply shorter than the container, and the fit test has to say so.

panPlanFor now takes a bottomInset and fits against containerH - bottomInset.
On the measured case the plan changes from -77 (pan up, popup below the pin,
buttons under the attribution) to +119 (pan down, popup above the pin, clear of
the band) -- the planner flips direction because with the band reserved, fitting
above became the smaller move. That is the existing "pick the smaller move" rule
doing its job on a corrected frame, not a new rule.

The inset is MEASURED from the live controls rather than assumed. A constant
would be wrong on the next control we add, at a width where the attribution
wraps to two lines, or when the gallery's clearance transform lifts the band.
Only controls that intercept pointer events are measured: the corner container
is pointer-events:none and spans nearly the full map width at 390, so measuring
the container would over-reserve the whole strip.

Unchanged where it was already right: a pin whose anchor fits still returns 0
(rega measured pan 0 on those in both arms), a popup taller than the usable
frame still refuses rather than jumping the camera, and bottomInset 0 is
byte-identical to the previous behaviour.

7 probe cases, with the post-pan fit asserted from the geometry rather than by
re-calling the planner -- a self-consistent oracle cannot see an error that
shifts both sides.
2026-09-29 13:31:24 +00:00
Doxa Maps
79b02f962a Key Cities keys: the "and N more" that ends a shortened language list, in each language's own list form 2026-09-29 13:31:15 +00:00
Doxa Maps
dd0428823c exp's four headless guards are registered, described and chained in gates:pre
check:wall-wysiwyg (0.9 s), check:wall-print-watch (0.7 s), check:wall-no-bg-photos (13 s) and check:template-i18n (9.7 s), each run once on this round's tip before chaining (all PASS) and each two-sided on its known-bad (dx-base poisoned all four independently). Placement read, not assumed: the two Vite runs use middlewareMode and bind no port, and wall-no-bg-photos serves a committed fixture with fetch stubbed, so no answer depends on the network, the machine or the moment. The orphan arm now counts 219 of 330 reached.
2026-09-29 13:31:14 +00:00
Doxa Maps
201baa3d60 A filter chip with several values says "or", in the reader's language
With no resolver configured, the kit chip bar joined a multi-value filter with ', ', so "Africa,
Asia" read as ALL to most people and carried English commas in every locale. The selection bus
matches several values on one facet with includes() (useSelectionBus isFilterActive), so adding a
value WIDENS the result: they are alternatives. The chip now uses formatList(value, { type:
'disjunction' }): "Africa or Asia", "Africa أو Asia", "Africa或Asia". gospel traced the matcher
and made the call as the only consumer's owner (gospel-internet's GiCombos). The English on this
chip changes from a comma to "or".

check:legend-mirrors lifts this exact expression and drives it with no resolver. It now lifts
formatList from library/i18n the same way (the shipped grammar, not a copy) and expects
"Islam or Hinduism". Poisoned with the old join it fails; restored it passes. formatList's doc now
says to choose the type by what the list MEANS, and never 'unit' for names.
2026-09-29 13:29:19 +00:00
Doxa Maps
62c8a2120c The first-run superuser URL itself reaches the terminal, not only the lines around it
PocketBase's link is /_/#/pbinstall/<token> (two l's). The forward rule matched \bpbinstal\b, which cannot match 'pbinstall', so the terminal showed 'Launch the URL below' with no URL below it (repo, testing live with PocketBase 0.40.4). The rule is now pbinstall?, tested on the line as PocketBase prints it rather than on a guessed spelling: the instruction, the URL and the CLI alternative are shown, the banner and dashboard URL stay quiet.
2026-09-29 13:29:19 +00:00
Doxa Maps
c5723f2b15 The shared-point list follows its sentence's locale, and formatList never throws outside a browser
check:approx-pin-note server-renders ApproxPinNote in Node, and it crashed: with no locale argument
and no runtime locale, formatList fell through to detectLocale(), which reads `document`. The page
lookup is now a guarded last resort. ApproxPinNote also passes its sentence's own locale (the
host's vue-i18n when installed, else '' so the runtime/page chain applies), so a host with its own
i18n can never get an Arabic sentence with a list in another language.

The gate now expects the new English ("Koraga and Maila", the list grammar this change exists for)
and adds the Arabic case: two names joined with و, never an English comma inside Arabic prose.
Poisoned by putting join(', ') back, both of those cases fail; restored, it passes.
2026-09-29 13:29:18 +00:00
Doxa Maps
64debb7f5b The shared-point note lists its groups in the reader's language
ApproxPinNote built pins.sharedPoint's {names} with join(', '), so the Arabic sentence carried English commas between the group names: the case gospel measured. It now uses formatList, which joins by the locale's own grammar (ar 'A وB وC', zh 'A、B和C', en 'A, B, and C').
2026-09-29 13:29:18 +00:00
Doxa Maps
57e3a11e68 The first-run superuser link still reaches the terminal
With the backend's output going to .pb/logs, PocketBase's one-time 'create your first superuser' link and its command-line alternative went only to the log, so the HOWTO's admin screen at /api/pb/_/ was unusable on a fresh .pb without knowing to look there (repo found it testing the change live). Those lines are a step a person must take, so they are forwarded like errors and 'address already in use'. The banner and URLs stay quiet. Tested against the regex read from the source.
2026-09-29 13:29:18 +00:00
Doxa Maps
f06751f3a8 formatList: a list of items in the reader's language, since join(', ') is wrong in every locale but English
join(', ') builds "A, B, C" for every reader. Arabic joins with و and no comma, Chinese with 、,
German and Russian with their own final conjunction, and no catalogue key can fix it, because the
separator and conjunction ARE the grammar. gospel measured it inside a fully keyed sentence
(pins.sharedPoint in Arabic still carried English commas) and counted 31 such joins across library
and plugins, with no Intl.ListFormat and no helper anywhere in the tree.

library/i18n now exports formatList(items, { type, style, locale }), backed by Intl.ListFormat.
It resolves the locale the way tStatic does (explicit argument, then the host's runtime locale,
then the page's), caches one formatter per locale and options, and falls back to ', ' only where
Intl.ListFormat does not exist. Proven from the shipped source, not a copy: en "A, B, and C",
ar "A وB وC", zh "A、B和C", de "A, B und C", disjunction "A or B", one item, none, and the runtime
locale when no argument is given. The call sites stay with their owners to migrate.
2026-09-29 13:29:18 +00:00
Doxa Maps
0d147b275c bun run dev keeps Vite's address in view: the backend's full log goes to a file
PocketBase prints about 230 lines of startup SQL, and the dev plugin forwarded every line to the
terminal, so Vite's "Local:" address scrolled out of view right after it appeared. The fourth fresh
reader of the README saw it happen.

Each dev backend's output now goes whole to .pb/logs/<name>.log (gitignored), and the terminal gets
only the lines a person must act on: errors, warnings, panics, and "address already in use", the
line the README and HOWTO tell a reader to watch for before setting DX_PB_PORT. The started line
names the log file. Tested against representative lines (in use, WARN, panic shown; SQL and the
banner quiet). The live check rides the next fresh-reader run.
2026-09-29 13:29:18 +00:00
Doxa Maps
6a1860539c Chain the region-paint gate now that both maps take the library's expression
The gate asserts the regions idle paint has one source. It could not be chained
while only one of the two maps had adopted the library export, because it was
legitimately red on the other plugin owner's unlanded work, and chaining a red
gate holds the shared run for everyone.

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

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

Verified on the combined tree: the gate passes, the orphan guard passes with it
counted among the chained scripts rather than the declared ones, and poisoning
the gate still fails it.
2026-09-29 13:29:18 +00:00
Doxa Maps
8793d47f0e countries-map paints idle regions with the library's regionIdle, not its own copy
useLegendMapBridge.js now exports regionIdle (sr, pair 1). The local
function is deleted and the name imported beside the bridge's other
exports; all 15 calls keep their values, the dynamic regionIdle(v)
included. Built bundle: the idle expression occurs exactly once before
and after (the local copy before, the library's after), 1 byte apart;
the country-posters composite still mounts. check:region-paint-single-
source PASS. Also drops a comment left behind when clearCountryHighlight
moved to the same library module.
2026-09-29 13:29:18 +00:00
Doxa Maps
1fde4b9cc4 The regions idle paint expression moves to the library that already painted with it
useLegendMapBridge took the idle paint helper as an injected dependency and
called it five times, while each consuming map defined its own copy of the
one-line body and handed it over. The library owned the paint logic and not the
expression it painted with, so the copies could drift apart silently and the
two maps would render the same idle state differently.

Export it from the composable instead, and take it from there in this map. The
injected parameter is deliberately KEPT for one round: inside the composable
the parameter shadows the module export, so what the bridge paints with is
still exactly what its callers pass, and this change cannot move behaviour. It
is a deletion in the plugin and an addition in the library, nothing more. The
parameter goes when the second map has adopted the export too.

This map held a THIRD copy under a different name -- a function-scoped helper
in the paint prewarm, byte-identical body, used once. It now refers to the
library helper.

Verified rather than assumed: the library version and both original plugin
versions produce identical output for every value the maps actually pass,
including the dynamic one. check:region-paint-single-source is green for this
map and stays red for the other until it adopts the export, which is the state
the gate is meant to report.

The second map is the other plugin's file and is not touched here; its owner
takes that half.
2026-09-29 13:29:18 +00:00
Doxa Maps
4ae1bbf4f5 Gate: the regions idle paint must have one source, not a copy per map
Two production maps paint the doxa-regions layers directly -- 35
setPaintProperty('regions-fill'|'regions-border') calls between them, 10 in one
file and 25 in the other. Twenty-one pass their value through a one-line helper
that wraps it in the same ['case', ['has','iso_3166_1_alpha_3'], v, v]
expression, and each map defined its OWN copy of that helper while already
passing it into the library composable that paints with it. The library owned
the paint logic and not the expression, so the two copies could be edited apart
and the same "idle" would render differently on the two maps, with nothing
failing. No existing check covers this: the nearest one reads a different
component entirely.

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

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

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

Building it found a third copy nobody had counted, under a different name, and
two bugs in the gate's own first draft: a block-comment stripper that ate the
declarations it was meant to find (so every single-source assertion passed
while both files plainly defined the helper), and an exemption for lines
mentioning the helper, which hid a one-line definition that mentions it. Both
were caught by the rule that a gate must fail when poisoned.
2026-09-29 13:29:18 +00:00
Doxa Maps
f50b641b28 The Wall's and the template's proofs become guards: four two-sided headless checks, in the plugins' own scripts/
Until now these ran from _qa-cycle, so they proved a landing once and guarded nothing after it. Each now lives with its plugin, locates the repo from its own path, and fails on its known-bad:
- wall-of-people-groups/scripts/check-wall-wysiwyg.mjs: the screen poster and the print are one layout on every preset (sheets, 2,107 card rects to 0.002 in, name size and wrap, reference size, chrome blocks). Planted: a card moved 0.01 in, a name a point larger, the footer missing on screen. About 0.6 s.
- wall-of-people-groups/scripts/check-wall-print-watch.mjs: the HD print counts its photos while they load, counts the ones that could not load, finishes at load or the 10-minute ceiling, and ends when its frame is gone. Planted copies of printWatch.js now go to the OS temp folder, never into the tree. About 0.3 s.
- wall-of-people-groups/scripts/check-wall-no-bg-photos.mjs + fixtures/pray-list-sample.json: the real store (through Vite) never requests a photo on its own, a group with no photo URL gets none, and one with a URL keeps it. Planted: the old background test, added through the store's $onAction. The store from before L12 (d9e9aa448) fails both arms: 12 photo requests in 3.5 s and 3 groups on the 404 placeholder. The fixture is a trimmed sample of the live list: 200 rows (120 with has_photo, 77 without, 3 synthetic with no image_url), only the fields the check reads, 66 KB. About 10 s.
- template-bundle/scripts/check-template-i18n.mjs: profile B renders from template.json in en, ar and zh, one sentence in each language's own order with both <code> islands, and no raw key. Planted: the English catalogue served as Arabic. About 4 s.

All four are headless: no browser seat.
2026-09-29 13:14:46 +00:00
Doxa Maps
a4622781a2 Key Cities: all nine region headings read in the page's language, and the region stays the key
This corrects my own "Other" fix (9d12e59b6), which could never fire. gospel's cities-internet analysis (87a168195) showed the same shape here: key-cities.json has no row with an empty region (1,646 rows), "Other" (2) and "N/A" (19) are region VALUES, and every section heading printed its raw English data string in every locale. The fallback I translated also put a label into the grouping key.

Now, as in cities-internet: the raw region string stays the grouping key (sections, :key, collapse state), with 'Other' as the data guard; the heading is projected at render by regionLabel.js from the keys the data's strings already are the English values of (dashboard.wagfRegion.*, dashboard.other, wall.na), written out in full so the build ships those areas. The poster wall, whose PosterWall prints its section name as given, groups by that projected heading, as country-posters' wall does. keyCities.posters.otherRegion is no longer used.

Headless (_qa-cycle/exp-shepherd/check-keycities-regions.mjs): every one of the nine region values maps to its catalogue heading in ar, zh, es and ru, and an unknown value passes through as data. key-cities is gated, so nothing in public/ changes.
2026-09-29 13:14:46 +00:00
Doxa Maps
f0192342eb Rebuild gospel-resources for the translated pin-sheet labels
BUNDLE=gospel-resources bun x vite build, then bun run tree and fix:sri.

Regenerated rather than rebased: development ca07fc3c8 rebuilt this bundle too,
so my earlier rebuild commit conflicted on the built artifacts. Hand-merging
generated files would invent bytes neither build produced, so that commit was
dropped and the bundle rebuilt from the merged source instead — which is why the
integrity hash moves from ca07fc3c8's U6FmpP… rather than from the one my first
build replaced.

One hash changes; the MapLibre and Mapbox CDN pins are byte-identical on both
sides, and ./gospel-resources/ is the only bundle path in any changed line.
2026-09-29 13:10:16 +00:00
Doxa Maps
b1c4c03956 Four resource labels on the pin sheet read English in every locale
The chips a reader sees on a group's sheet — "Scripture,", "Bible video",
"Training", "Gospel presentation" — were plain template text. All four already
had keys with real ar and zh translations, so this routes them through
gospel.res.* and adds nothing: scripture, bibleVideo, training,
gospelPresentation.

WHAT IS DELIBERATELY LEFT ALONE, because it is not a label:
- g.audio.m keeps its raw value and its 'audio' default. That is the table's own
  media descriptor, data rather than UI text.
- gospel-resource-rows.js is already correct. add(title, type, …) translates
  title through tx() and keeps type as the category identifier the rows are
  grouped by; the gate flags those type strings, but translating them would
  break the grouping — the same value/label trap as the region headings.
- d.s.replace('Open Bible Stories', 'Bible Stories') at :2754 rewrites a DATA
  value, not a label.
- data-tables.vue:122 already carries labelKey; its `en:` is the fallback beside
  it, by design.
- The caveat prose (Driver row 80) and the title= tooltips (row 120) are
  untouched and stay that way until those rows are answered.

check:hardcoded-english on gospel-resources: 91 -> 87, which is these four.
2026-09-29 13:08:53 +00:00
Doxa Maps
ca07fc3c8c Rebuild the consumers main left stale (build owner, README rule 5)
check:composite-freshness found stale on main: none. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-29 12:59:05 +00:00
Doxa Maps
204282b780 bun.lock records the capacitor specifiers package.json declares, so a fresh install leaves the checkout clean
The lockfile's workspace section had "7" for @capacitor/android, cli and core, where package.json says "^7.6.9". --frozen-lockfile accepts that (7.6.9 satisfies both), but a plain `bun install`, as the HOWTO shows, rewrote the three lines, so a new contributor's checkout was dirty before their first edit and their first push would carry the lockfile. Regenerated with bun 1.3.14: exactly those three lines change, installs report no changes, and --frozen-lockfile still passes. Found by the fourth fresh reader.
2026-09-29 12:44:25 +00:00
Doxa Maps
6a0aade5c1 The browser/Node sheet equivalence check can measure on a fresh checkout
check:poster-browser-equiv is chained in gates:pre and exited 75 NOT MEASURED
every run. Its input, out/_records.json, is gitignored and written only by
generate.mjs against a live API, so on CI and on any box without credentials the
gate never looked at anything. The runner reported that honestly rather than as
a pass, but an identical NOT MEASURED every run is indistinguishable from an
unwired gate -- and this is the gate that proves a sheet generated in the browser
is byte-identical to the one Node writes.

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

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

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

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

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

Two-sided, on this tree: with the fixture it PASSES byte-identical on both arms
(834,174 and 921,522 bytes). Dropping the province boundaries at
posterSheets.js:206 -- the defect this gate's header records, where a country
beside India or China printed without them -- makes the country arm 251,576
against Node's 921,522 and the gate exits 1. Restored, it passes again.
2026-09-29 12:44:25 +00:00
Doxa Maps
9061065558 The two composites that draw a map get the map snippets: a MapLibre preload and a Mapbox variant
research-map-plus-grid and regions-mfe draw a map through a child element, so their isMap is false,
and their published snippets had no engine preload and no scriptMapbox. A pasted composite still
draws on Mapbox given provider "mapbox" and a token (measured), so this is the snippet's
convenience, not a broken map.

generate-tree now decides after every profile's isMap is known: a composite (nested-elements-mfe)
whose OWN composes reach a map profile gets the map form of the default snippet and the Mapbox
variant. It reads the profile's own composes, never the bundle's hoisted list, which the bundle's
plain leaves (charts, data-tables, the poster wall) share while drawing nothing. The gallery shell
is left out until its passing of provider to its tabs is measured. Regenerated: exactly those two
profiles' embed changes, in both manifests; nothing internal leaks into the output.

check:snippet-engine asserts the same rule on the built manifest (21 map snippets, 19 maps + 2
composites). Its self-test adds a composite stripped of its variant, which fails. Against the
manifest as it stood before this change, the widened gate fails on exactly these two composites;
on the regenerated one it passes.
2026-09-29 12:44:25 +00:00
Doxa Maps
b899e6887a The reference baseline reaches 0: the last two lines were a hygiene checker's own patterns
Every owner's sweep has landed. The two remaining lines were public-hygiene-check.mjs's pattern list, which must spell the role word in order to catch it on the way out, as this gate's own file must. It is exempt by name in a one-file list with its reason, not by any wider rule. The baseline is now empty: from here, every line that names the role is a new reference and fails.
2026-09-29 12:44:25 +00:00
Doxa Maps
3daa602741 One pin click should call the click handler once, not twice
The pin click handler was registered on two layers that always overlap: the
visible pins and the invisible hitbox drawn over them. A layer-scoped click
fires once per registered layer with a feature under the point, and every
visible pin has a hitbox twin at its own point -- same source, larger radius,
added alongside it so taps are easier to land. So every pin click ran the
handler twice, 3ms apart. Measured on the leaf at 1440, on two different pins,
both reporting both layers at the click point; exactly twice, never more.

That already shipped as a visible bug once: the second call overwrote the
legend state the first had saved, so closing a people-group detail reopened a
legend the reader had deliberately collapsed. The store is now idempotent, so
that particular symptom cannot return, but the duplicate call was the cause and
is still here -- anything that counts, toggles, appends or reports from a pin
click is wrong by a factor of two, and nothing in the code warns the next
author.

Bind the click to the hitbox only. It covers every click the visible layer
would catch: both layers are created together by addLanguageFamilyLayer, from
the same source, and the two places that filter the hitbox mirror the pin
filter rather than narrowing it, so the two layers always carry the same
features. The handler reads only properties and geometry, which are identical
between them. Cursor handlers stay on both layers -- mouseenter and mouseleave
are idempotent, so they were never part of this.

Checked before relying on it: exactly one addLayer creates each of the two
layers, both inside addLanguageFamilyLayer as consecutive statements with no
branch between them; no flag gates the hitbox; no removeLayer anywhere touches
either; and the only layout writes on the pin layer set circle-sort-key. The
invariant is now stated at the binding, because it is not visible from the call
site: a pins layer added without a hitbox would get no clicks.

Also fixes a dead trap in the same file. The people-groups-pins branch called
the handler with no argument, so it fell back to the language-family pins and
bound the wrong layer. Nothing creates that layer today -- the id occurs only
in this file -- so the branch never runs, but whoever first adds it would have
got a duplicate binding, or no click handler at all if the language-family
layer were absent.
2026-09-29 12:44:25 +00:00
Doxa Maps
cb71051cdd A table row's detail sheet names itself in the reader's language when its first column is empty
DataTable's sheet heading fell back to a hardcoded English 'Detail', one word and so outside check:hardcoded-english's reach. It now reads aria.rowDetail ('Row detail'), which this component already uses for the dialog's own label: translated in all 11 locales, and it brings no new catalogue area into any bundle (a dashboard.* key would have added the whole dashboard area to every bundle that shows a table).
2026-09-29 12:44:25 +00:00
Doxa Maps
a2e6184d3b check:poster-print-fallback asserts the outcome: the source loaded and a print frame opened
A poison run answered 404 to posters/<id>.html for the browser only, and the gate still PASSED.
It counted the .html REQUEST, and its failure pattern (not available|failed|blank|error) never
matched what the wall then shows, "poster not found (404)"; the HUD reader did not even collect
that text. So it could not fail on the one path it exists for.

It now requires the .html to ANSWER 200 and a print frame to actually OPEN (counted at creation
with the appendChild hook the degrade gate already uses, since the frame removes itself after
printing). "not found" is read as the failure it is, in the HUD reader, the poll and the verdict.
Its output reports both outcomes. The browser runs are rega's: the positive arm must still pass,
and the 404'd source must now fail.
2026-09-29 12:44:25 +00:00
Doxa Maps
cc9508da7c Rebuild wall-of-people-groups, and regenerate the tree 2026-09-29 12:36:05 +00:00
Doxa Maps
488d34836a Wall of People Groups: the level-of-detail hook reports the zoom it actually draws at
rega's L12 photo probe read host.__wallLod().cardPx as 0 after 46 wheel ticks: the field was set once when the cards were first laid out and never again, since the canvas renderer replaced the LOD component that used to update it. The canvas now records one card's on-screen width at every frame it draws (stats.cardPx, both the grid and the poster), and the hook returns that. The container's header comment no longer claims Font Awesome is loaded: the canvas draws its own icons, and d3 is pinned and kept off window.d3 (src/wallD3.js).
2026-09-29 12:35:22 +00:00
Doxa Maps
ef83e0ab9b Rebuild cities-internet for the translated region headings
The source change needs its bundle: main-capture scan refused the landing with
COMPOSITE-FRESHNESS, committed 26db9ec7f664 against a rebuild of 6a08c67b57e4.

BUNDLE=cities-internet bun x vite build, then bun run tree and fix:sri. One
integrity hash moves, nijTwn… to I39zqe…, in the cities-internet rows of both
manifests. The four MapLibre and Mapbox CDN hashes are untouched, and no other
bundle's path appears in any changed line.
2026-09-29 12:30:32 +00:00
Doxa Maps
87a1681956 Nine region headings read English in every locale, not just "Other"
rega saw "Other" in ar and zh. The reported cause — `c.region || 'Other'` at
:65 — is not one: of 175 rows in connectivity.json NONE has a falsy region, so
that fallback never fires. The "Other" section is one row (United States) whose
region IS the string "Other". Patching :65 would have changed nothing on screen.

The render site is `{{ sec.name }}`, and it printed the raw data value for ALL
NINE regions, so this was nine English headings rather than one.

All nine already had translated keys — dashboard.wagfRegion.* for the seven WAGF
regions, dashboard.other, wall.na — in all 11 locales. None needed adding.

THE LABEL IS PROJECTED AT RENDER, NEVER AT GROUPING. sec.name keys the Map, the
:key, and the collapse state, so translating it where the rows are grouped would
reset every open section on a locale switch and filter on a value no row carries.
That is the value/label trap useFacetAggregation.js documents at OTHER_KEY; this
follows the same shape.

The name->key map is built by asking the ENGLISH catalogue what each key says,
rather than writing the English phrases here: the data's region strings are the
catalogue's own English values, so English is the join and never has to be
duplicated. Written the other way it added two check:hardcoded-english findings
to a file that had none — an English phrase used as a lookup key reads exactly
like a user-visible string.

tStatic(path, 'en'), not t(). t()'s signature is (key, params, plural), so
t(k, 'en') passes 'en' as params and returns the RUNTIME locale's value: the map
would be built from Arabic values and match no region at all.

The keys are written out in full because internal/i18n-areas.mjs bundles an area
only when its name appears literally in a reached file. cities-internet shipped
14 areas and neither dashboard nor wall; assembling these at runtime would have
left all nine unresolved in the shipped bundle while working in the dev tree.
Measured both ways — written out, the bundle takes 16.

`|| 'Other'` stays as a data guard so a future row with no region cannot key the
Map on undefined, and 'Other' is the sentinel dashboard.other projects.

9 of 9 headings translate in ar and zh, verified against the real catalogues.
2026-09-29 12:24:45 +00:00
Doxa Maps
50bdc54c16 Rebuild pplr-data-maps key-cities, and regenerate the tree 2026-09-29 12:20:31 +00:00
Doxa Maps
53ed778d71 PPLR Data Maps: the fullscreen button is named in the page's language
rega's L15 (e7f343d4c, ar + zh): every PPLR map view's fullscreen button read "Toggle fullscreen" in English, title and aria-label. It is PPLR's own control (FullscreenToggleControl in src/utils/pplrMap.js, not MapLibre's), and it wrote the English itself. It now takes the shared buttons.toggleFullscreen, already translated in all 11 locales, when the map is built.
2026-09-29 12:20:03 +00:00
Doxa Maps
9d12e59b60 Key Cities: a row with no region is headed in the reader's language, not "Other"
cities-list.vue grouped region-less rows under a hardcoded 'Other' section heading, a single English word, which is the hardcoded-english gate's blind spot. It now reuses keyCities.posters.otherRegion (all 11 locales, landed in R25). The same literal in cities-internet's connectivity-list, where rega's R25 look found it in ar and zh, is gospel's and goes to gospel. The SSR check (check-keycities-i18n.mjs) passes; key-cities is gated, so nothing in public/ changes.
2026-09-29 12:20:03 +00:00