Commit graph

697 commits

Author SHA1 Message Date
doxa-maps-88655c70
626603ca3f Rebuild the served tree after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
Another agent pushed during the build, so main-capture's scan refused the push —
working as intended, and the lock released on the ';' as it must. The conflict was
in public/doxa-maps/index.html: build OUTPUT, where neither side is authoritative,
so it is resolved by rebuilding rather than by picking hunks. Ten files differ from
the hand-resolution, which is why that rebuild is not optional.

No source file overlapped: their commits were worldmap.svg provenance, mine is
PosterWall. 22/22 post-gates green after the rebuild.
2026-09-24 09:42:50 +00:00
doxa-maps-88655c70
9f076ffbc8 PosterWall can print the poster instead of downloading it — the Driver-rejected UX it still encoded
★ THE BLOCKER ON MIGRATING THE 100-LIST WAS NEVER ONLY A MISSING CAPABILITY. It was
a Driver-rejected UX baked into the shared component, and migrating as planned would
have shipped it.

my-upg-100-list/poster-wall.vue:280 records the ruling verbatim (2026-09-23): "three
print buttons, each opening an in-browser print dialog, no silent file download as
the default action. A raw 'Download' button and a 'Prayer List: coming soon'
placeholder were exactly the two buttons he objected to." exportPoster(),
downloadPacket() and download() were REMOVED from that wall for that reason.

PosterWall still had both: exportPoster() downloads a .webp unconditionally, at two
call sites, and downloadPacket() renders the "coming soon" placeholder whenever
there is no records provider. Consuming it would have reintroduced the exact two
buttons the Driver named.

Two props close that, both defaulting to today's behaviour so country-posters and
key-cities are untouched:

  posterAction: 'download' | 'print' — 'print' opens the OS print dialog on the
  poster. BOTH delivery paths (the shipped .webp and the browser-rasterised
  fallback) now route through ONE deliverPoster(), so the modes cannot drift or be
  applied to only one of them. Printed blob urls are held until unmount rather than
  revoked at print time: the preview <img> is still reading from them, and revoking
  early prints a blank sheet.

  hidePrayerListWhenSoon — suppress the Prayer List button rather than show it as a
  placeholder. The placeholder stays honest where the feature is genuinely unbuilt;
  it is only wrong on a wall that DOES print the list.

VERIFIED AT THE RENDERED LAYER, both modes, by counting the two outcomes that
actually distinguish them:
  poster-action="print"  -> 1 print frame opened, 0 downloads
  default (prop absent)  -> 0 print frames,       1 download
Counted at appendChild, where openPrintFrame attaches the iframe, rather than by
polling for iframes — the frame SELF-REMOVES on afterprint, so polling finds none
and reads as a failure.

⚠ Two probe mistakes worth recording, because both looked like product bugs: a HUD
selector that matched <script>/<style> text and returned the page's whole token
ladder, and a first click that landed on INDIA — which country-posters marks
`unsupported` (855 groups do not fit a sheet), so it short-circuited with zero
poster requests. Skipping cards showing .ph.is-soon fixed it; the next click fetched
country-chn.webp and completed.
2026-09-24 09:40:31 +00:00
doxa-maps-88655c70
f824329466 One worldmap.svg, at the path that carries its provenance
Some checks are pending
build-gate / build (push) Waiting to run
c43aa39 landed the same 311KB asset at public/assets/images/worldmap.svg --
DX-life's own path -- together with a PROVENANCE record: a full-file scan
for copyright, licence, attribution, author and the usual stock-library
names returns nothing, the ids are design-tool export names, and DX-life
keeps no licence file beside that folder either, so this is internal reuse
of first-party artwork rather than redistribution of a third-party asset.
The contrast it draws is the right one: the vendored typefaces ARE
third-party and do carry their OFL licence in the folder that serves them.

I had ported the same file to assets/brand/ earlier in this flight. MEASURED
identical -- md5 b7af01b31096c265e221a1d4e3f88c8d on both -- so the tree was
shipping 311KB twice, and, worse, keeping the provenance record in one
folder and a copy of the asset in another, where the two can drift apart
without anything noticing.

So the grid now points at the documented copy and the brand/ one is deleted.
Nothing about the rendering changes; only which of two identical files it
asks for.

VERIFIED on the built tree: /assets/brand/worldmap.svg now 404,
/assets/images/worldmap.svg 200, the #filters backdrop still computes
background-size 60% and position 50% 0% with 0 failed requests, check:sri 0,
gates:pre 0, gates:post 0.
2026-09-24 09:37:45 +00:00
doxa-maps-88655c70
d8a3e5992f Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work 2026-09-24 09:26:36 +00:00
doxa-maps-88655c70
c43aa399f7 Record worldmap.svg's provenance, after auditing the licence obligation
Applying the same question as the last three ticks to the standing rule
with real-world weight — "vendored bytes small and LICENSED". Two gaps,
one of them mine.

I vendored worldmap.svg silently: 311KB copied from their repo with no
note of where it came from or why no licence accompanies it. The
judgement was left implicit, which is exactly what a provenance
question should not be.

Checked properly now. A full-file scan for copyright, licence,
attribution, author, creator, <title>, <desc>, <metadata>, rdf:/dc:
nodes and the usual stock-library names — Natural Earth, simplemaps,
amCharts, Freepik, Shutterstock, OSM, Mapbox — returns nothing. It is a
plain vector: 394 <path>, three <g>, zero <image>, zero <text>, with
design-tool export ids (Group_345, data-name="Group 345"), so it was
authored by their designer rather than downloaded with embedded
credits. DX-life keeps no licence beside public/assets/images/ either;
they treat that folder as their own artwork. So this is internal reuse
of a first-party asset within the same organisation, not
redistribution of a third-party one.

public/assets/images/PROVENANCE records that reasoning, the exact
source path, the measured live values, and the contrast with the
typefaces — which ARE third-party and therefore carry their OFL text in
the folder that serves them. Extensionless, following the LICENSE
precedent, since check:public-hygiene refuses a *.txt in a served
folder; hygiene stays clean at 0 findings and the note is staged into
the tree beside the asset it describes.

The second gap needed no source change and lands in the probe suite:
OFL compliance rested on a one-time manual act with nothing checking
it, so a merge that dropped a LICENSE, or a future bundle vendoring a
face without one, would have shipped unlicensed bytes with every gate
green. A new suite walks public/assets for woff2/ttf/otf/eot, asserts a
licence sits beside the fonts, asserts it is real text rather than a
stub, asserts it is not a *.txt, and then repeats the audit inside the
generated tree — because a licence that never reaches the tree does not
travel with what a reader downloads. It immediately covered a third
vendored face I did not know existed, noto-serif-devanagari, already
compliant. Negative-tested twice: a removed licence fails, and a
two-byte stub fails on the real-text assertion.
2026-09-24 09:25:20 +00:00
doxa-maps-88655c70
a501f98e2c PosterWall can offer a card-stock picker — the capability keeping the 100-list on its own copy
Some checks are pending
build-gate / build (push) Waiting to run
The card's first step was a gap analysis: what does the 888-line my-upg-100-list wall
render that the shared component's props cannot express? Diffed both templates. They
are STRUCTURALLY THE SAME — wall/tag/sub, sections with head and count, a card with
"N UPG · Show on map", main with image-or-placeholder, a Print Options fieldset, the
same export modal. Exactly TWO real deltas:

  1. the CARD-STOCK PICKER — closed here;
  2. a clickable preview image (.img.is-clickable) — still open, and small.

Everything else that wall "owns" is data and taxonomy: REGION_OF, sections,
fetchListRecords. That is precisely what country-posters (94 lines) and key-cities
(75) already supply to this same component.

`cardStocks` takes true for every stock card-render.js supports, or an array for a
subset; `cardStock` remains the DEFAULT selection. Default false is today's behaviour
byte-for-byte — neither migrated consumer mentions the prop and both still build.

★ LABELS ARE DERIVED FROM card-render.js STOCK, never hand-written. The 100-list
carries its own DECK_STOCKS array duplicating all five labels, beneath a comment
saying they "come from the renderer's own STOCK table so this list cannot drift".
That was the intent, not the implementation. Deriving them makes it true, and the
migration will delete the duplicate.

⚠ The import is LAZY on purpose. usePosterPrints imports card-render.js dynamically
to keep a 10KB renderer out of every wall's initial bundle; importing it statically
for five labels would undo that for every consumer, including the ones that show no
picker at all. Options fill in when the module resolves and the <select> is not
rendered until then, so there is no flash of unlabelled ids. If the configured
cardStock is not among the offered stocks, the selection falls back to the first that
is, rather than printing at a size the reader was never shown.

buttons.cardStockAria added to ALL ELEVEN locales in the same change:
check:i18n-parity still reports 0 keys of debt. An English-only key would have broken
the ratchet emptied two ticks ago.

VERIFIED: the derivation unit-tested against the real STOCK table — all five stocks,
a subset, a subset EXCLUDING the configured default (falls back correctly), and an
unknown id (dropped, not rendered blank). Not yet exercised in a browser because the
picker is gated on a `records` provider and NO wall passes :records to the shared
component yet — the 100-list still holds its provider in its own file. Opting
country-posters in temporarily gave 0 pickers and 0 errors, which is that gate working
as designed. The migration will exercise it end to end.
2026-09-24 09:24:29 +00:00
doxa-maps-88655c70
20c1c0d325 Delete the 100-list wall's dead hidden-map export, and stop it lying to a gate
Some checks are pending
build-gate / build (push) Waiting to run
renderFor() had ZERO call sites and was the only caller of makeHiddenMap() and
loadPoster(); EXPORT_SPEC was never read. The whole chain dated from when this wall
rendered a poster by spinning up an off-screen map and compositing tiles. Export and
print now take the shipped .webp, or rasterise posters/<id>.html in the browser.
52 lines out, 17 of explanation in: 925 -> 888.

★ IT WAS NOT MERELY UNUSED — IT WAS CORRUPTING A GATE. isMapLeaf() in
generate-tree.mjs is a REGEX over FILE TEXT, so the engine handle inside the
unreachable function flagged this profile isMap=true. That flag made
verify-map-paint.mjs EXCLUDE the profile, with a written justification —
"constructs a real gl.Map ON DEMAND during poster export, off-screen at
left:-9999px" — where every clause was true of the SOURCE and false of the RUNNING
PRODUCT. A profile was excluded from a paint gate on the strength of code that never
ran, and the exclusion read as a measured product fact.

Deleting the chain flips the manifest to isMap=false, which is correct: the poster
wall is a wall of cards, not a map. The gate's comment now says it is absent because
it IS NOT A MAP, rather than because it is a map the gate cannot see.

⚠ The explanation had to be written carefully. Naming the engine handle whole in a
comment RE-TRIPS the same regex and reinstates the bug, so it is spelled in two
pieces on purpose with a note saying why — the same class as asserting Mark 5:1-20
via the English word "Gadarenes": describing a thing in the terms the detector scans
for makes the description itself the defect.

ALSO FIXED, found while verifying: check:poster-print-fallback probed
'../assets/posters/<id>.html', one level up. A leaf lives at
<tree>/<bundle>/<profile>/index.html and resolves assetsBase to '../../assets', two
levels up — so on a tree that DID carry the sources the probe 404'd and the gate
SKIPped with "posterSources:true was not set" when it had been. A precondition check
that is wrong about the precondition silently turns the whole gate off.

VERIFIED AT THE RENDERED LAYER after the deletion, not before: the print path still
rasterises with every .webp aborted ("generating the poster", clean terminal state),
and the card deck still prints 116 cards at 2.5in x 3.5in with 116 QR codes and 0
page errors.
2026-09-24 09:12:17 +00:00
doxa-maps-88655c70
063b7ea460 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 09:11:00 +00:00
doxa-maps-88655c70
32936d731b Regions: delete four dead imports, after checking the slug port is consumed
Continuing last tick's question — is the data I ported actually used? —
applied to the slug half of countries-meta. It is: both tabs resolve
selections through r.slug and c.slug, which summarizeCountries computes
with countrySlug, so the canonical-English rule really reaches the URL.
Verified against the values the live site adjudicated: "United States"
from the API still yields the-united-states, and "DR Congo" yields
the-democratic-republic-of-the-congo.

What the check did turn up is dead weight. slugify was imported by both
tabs and called by neither, and regions-list also carried emptyStats
and toNumber — leftovers from the rewire, since that tab has no chooser
and never needed the zero accumulator or the coordinate parser. All
four removed; a re-audit of every import in all five components now
reports zero dead names. The two exports beyond their surface stay
justified: emptyStats has five uses in region-dashboards and toNumber
two, both in the chooser that needs them.

Also verified rather than assumed: the region legend really does render
in REGION_ORDER. That matters because validate_palette measured
ADJACENT PAIRS for colour-vision separation, so the 8.6 deutan and 30.6
normal-vision figures only hold in that order — sort the legend by
count and pairs the validator never checked become neighbours. Nothing
asserted it, so the chooser suite now pins the dot colours to the
palette's own key order, with the expected sequence read from
library/colors/wagf-region.js rather than retyped. Negative-tested by
reversing the sequence: exit 1.
2026-09-24 09:08:57 +00:00
doxa-maps-88655c70
faa78dfe2c Rebuild the served tree after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
The rebase brought in a tree regeneration, which re-hashes the bundles and rewrites
the gallery's baked SRI. A stale integrity hash is the silent failure this repo has
already paid for: the browser refuses the script and the page mounts nothing.

Both i18n gates re-verified at the rebased HEAD rather than assumed — packet-strings
parity and locale parity are green, and gates:post is 22/22.
2026-09-24 09:02:19 +00:00
doxa-maps-88655c70
6732c8d430 Guard the packet catalog: the 12 prayer points and the wheel had no parity gate
check:i18n-parity covers library/i18n/locales — the APP catalogs. Nothing covered
library/scripts/posters/packet-strings.json, which is a separate file with its own
shape and is where the 12 PRAYER POINTS and the PRAYER WHEEL live. SIX gates read
it and not one checked that a language block is complete.

All ten locales happened to be healthy — 19/21 string keys, 12 points with every
field, 12 rhythms — but nothing held them there. A point dropped from one block
renders that slot through the per-key English fallback: correct runtime behaviour,
invisible defect, on a page the reader is meant to pray through.

★ IT ASSERTS PRESENCE, NOT TRANSLATEDNESS, and that is the load-bearing choice.
check-packet-render printed "19 strings translated" and only ever asserted `!== 0`,
so a locale regressing from 19 to 1 passed silently. The fix is NOT to pin 19:
measured, de translates 18 of the same 19 keys and fr 17, because a word or two
legitimately matches English — a hardcoded count fails two CORRECT catalogs. So
parity asserts every English key EXISTS in every block, a structural fact no
translation choice can change, and the render gate gains a FLOOR of two-thirds
derived from the catalog itself.

point4_passage + point4_passage_credit are DECLARED-ABSENT with the reason on
record (Mark 5:1-20, public-domain licensing per language). Naming the exception
beats tolerating it silently: the day a licensed translation lands, deleting the
entry tightens the gate by itself.

MUTATION-TESTED, five defect classes, each caught with a precise message: a dropped
string key, an 11th-of-12 prayer point, an EMPTY point text, a missing wheel rhythm,
and a block reverted wholesale to English. The floor was tested separately by
reverting de to 3 translated of 19 — "only 3 differ from English (floor 13)".

★ THE TWO GATES ANSWER DIFFERENT QUESTIONS, and a partial language shows both at
once. Swahili at 3 of 21 keys, no points, no rhythms: check-packet-render PASSES —
a partial translation renders, Swahili where translated, English where not, zero
blank slots — while check:packet-strings-parity FAILS and names exactly what is
missing, including that the points and wheel would fall back wholesale. Before this,
only the first question had a gate, so a half-finished block was indistinguishable
from a complete one at build time.

gates:post is now 22 steps; 11/11 packets still pass the sweep.
2026-09-24 08:57:34 +00:00
doxa-maps-88655c70
820a9015ca Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 08:51:21 +00:00
doxa-maps-88655c70
e0f2d522bb All eleven locales complete — the last 162 keys of i18n debt, and the ratchet empties
Some checks are pending
build-gate / build (push) Waiting to run
regions.json for it/ro/zh, 54 keys each. check:i18n-parity now reports "0 key(s) of
known translation debt remain", closing the 429-key gap it found when it was written.

★ THE DEBT MAP IS EMPTY RATHER THAN FULL OF ZEROS, and that is the whole point of
this ratchet: a locale with NO entry must be complete — the gate fails it outright
for a single missing key — so an empty DEBT is the strongest state it can be in, not
an unset one. Writing { it: 0, ro: 0, zh: 0 } would mean the same today and quietly
permit a regression the moment someone "adjusted" a number. Mutation-tested both
ways: removing ONE key fails ("it is missing 1/176 key(s) and has no DEBT entry"),
deleting the whole file fails (54/176).

THE CARD'S ESTIMATE WAS WRONG AND IS CORRECTED. It recorded "54 keys of full
SENTENCES". Measured: 45 short labels (median 14.5 chars) + 9 longer strings, three
carrying %count%/%countries%/%region%/%country% interpolation tokens. The catalog is
keyed by its own English TEXT, not symbolic keys — which is exactly why the gap was
invisible: the fallback renders the English key itself, so key and value are
identical in English and nothing ever looks broken.

PLACEHOLDERS HELD BY A PROGRAMMATIC CHECK, not by care: every %token% in every
English value must appear, same set, in all three translations. 0 mismatches, 0
empty values. Mangling one breaks interpolation at runtime with nothing to catch it.

VERIFIED AT THE RENDERED LAYER on the regions-list leaf, en/it/ro/zh: labels
translated (Regions -> Regioni), the intro sentence translated, and the INTERPOLATED
count sentence correct — "315 people groups in 41 countries" -> "315 popoli in 41
paesi", both placeholders substituted with real numbers. Zero page errors. Each
locale asserted to DIFFER from English, so a silent fallback could not pass.
★ That sentence is the exact bug regions-list.vue:144 records: the Russian string was
"ALREADY in library/i18n/locales/ru/regions.json — copied, never wired", caught by
reading a screenshot rather than by any assertion. Now it is asserted.

zh is SIMPLIFIED, measured not assumed: the existing zh catalogs carry 33
Simplified-only characters against 4 Traditional. Vocabulary follows each locale's
own established terms — Popolo / Popor / 民族, Regione / Regiune / 区域 — and WAGF
stays an acronym, as fr and de already had it.
2026-09-24 08:48:02 +00:00
doxa-maps-88655c70
bfe659be3e Regions: the map was pointed at the wrong part of the planet
A half-finished port of my own, found by asking whether the data I had
ported was actually consumed. countries-meta computes centre, zoom and
bounds per country and per region via viewFor/fitView/lngExtent, and I
reported that port as closing the "maps derive their own frame" item.
It did not: this component still computed its own mapBounds from a
naive min/max over the focus pins, its own mapCenter from their
arithmetic mean, and a hardcoded zoom of 4 or 2 — discarding the ported
values entirely.

The consequence was severe and visible on Oceania, whose pins straddle
the antimeridian:

  ours    [[-175.2,-21.3],[178,-6.3]] — 353.2 degrees, the long way
          round the globe — centre [28.02,-16.06], which is in AFRICA,
          zoom 2 hardcoded
  theirs  [[143.9,-21.3],[187.9,-6.3]] — 44.0 degrees along the short
          Pacific arc — centre [165.9,-13.8], zoom 3.07

A naive min/max has no antimeridian handling. lngExtent exists for
exactly this: it walks the sorted longitudes for the largest gap and
spans the rest, letting east exceed 180 so the range stays continuous.

Why nothing caught it: region-vs-live diffs computed CSS properties and
reports 79/79. A map pointed at the wrong hemisphere is not a CSS
property. The chooser probe covers the no-selection world view, and
nothing had ever read the camera on a selected region page.

The frame now comes from the subject's ported values, with the
chooser's world view preserved as the documented exception — null
bounds, [10,28], zoom 1.25 — because fitBounds across every country
pulls the globe into a crescent, which was itself a measured decision.
Fallbacks use countries-meta's own WORLD_VIEW centre rather than an
invented one.

Proven on the live element: centre longitude 153.6 (Pacific, was 28.0),
span 145.7 degrees (short arc, was 353.2), zoom 2.56 (a real fit), with
west 80.8 and east 226.5 — the east-beyond-180 continuity the port
makes possible. The screenshot shows in-region pins framing the Pacific
with out-of-region pins correctly greyed at the edge.

Also checked and not a defect: the region page renders on the globe
projection at this zoom, which matches theirs — their CountryMap sets
no projection either, and this component already documents the cloned
pages as deliberately left alone, forcing mercator only for the chooser.
2026-09-24 08:47:16 +00:00
doxa-maps-88655c70
95b5586cc5 Assert the packet photos the spec names — "20 per page WITH IMAGES"
Some checks are pending
build-gate / build (push) Waiting to run
check-packet-render printed "95 cards (0 photos, 95 typographic)" and PASSED, in
every locale. The packet spec names the photos explicitly, so that was the last
unasserted number on this surface.

The cause was my own fixtures, not the product: I regenerated the packets with
NOPHOTOS=1 two ticks ago for speed. Regenerating properly gives 95 photos, 3.5MB,
0 broken. But a gate that passes a packet violating a core spec line is the defect
whoever made the artefact, and this one would not have noticed a real photo-fetch
failure either.

★ THE COUNT CANNOT BE PINNED, so this is two invariants rather than "=== 95".
Roughly half the groups genuinely have no photo, the CDN drops some — de came back
94/95 on this very run, which is exactly why a hard count would have been wrong —
and NOPHOTOS=1 is a SUPPORTED dev mode for fast layout iteration. So:
  1. photos + typographic === cards. Every card is exactly one shape; a card that
     is neither means the renderer emitted a third nobody is checking.
  2. a photo-enabled packet must have SOME photos. Zero is acceptable only when the
     run declares itself photo-less via PACKET_EXPECT_PHOTOS=0 — "deliberately
     none" said out loud, rather than indistinguishable from "silently none".

MUTATION-TESTED BOTH. Removing a single .card img yields "95 cards but 94 photo +
0 typographic = 94". The photo-less packet FAILS by default and PASSES under the
opt-out. (My first attempt at the sum mutation silently did nothing — the card
blocks are 11,340 chars and I edited a 1,200-char window — so the assertion only
counts as tested because the second attempt targeted the real .card img.)

All 11 locales regenerated WITH photos and re-swept: 11/11 pass. i18n re-verified
end to end — a partial Swahili block, 3 of 23 keys, added as DATA ONLY with no code
change: translated keys render in Swahili, untranslated fall back per key to
English, zero blank slots.
2026-09-24 08:35:19 +00:00
doxa-maps-88655c70
63ee25d48a Rebuild the served tree after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
The rebase brought in a shared map-controls change (the restored keyboard focus
ring), which re-hashes every bundle that imports them and rewrites the gallery's
baked SRI. A stale integrity hash is the silent failure this repo has paid for:
the browser refuses the script and the page mounts nothing.

The one conflict was in public/doxa-maps/index.html — build OUTPUT, where neither
side is authoritative, so it is resolved by rebuilding rather than by picking hunks.

Re-verified after the rebase rather than assumed: prayer-walk-planner's share url
still resolves 200 text/html and reopens as <prayer-walk-planner> with a rendered
canvas, with the shared control styling those commits touched.
2026-09-24 08:26:21 +00:00
doxa-maps-88655c70
e5fe33b230 prayer-walk-planner can be shared — the last published map with no sharing path
check:share-policy's known-debt line is now GONE: it named two published maps, then
one, now zero. Every published map has exactly one decided sharing path.

★ THE OPEN QUESTION WAS ANSWERED BY THE CODE, NOT BY A RULING. This was held back
asking "is it MEANT to be shareable? a link carrying no route arrives as an empty
planner, and a button that reliably produces a blank map is worse than none". That
framing assumed a design choice existed. It did not: the profile keeps polygons in
localStorage and has NO BACKEND — its own header says so — so a URL cannot carry a
drawn route under any design. The shareable artefact is the PLANNER, exactly as
every other map shares its own page. The failure only exists if the link CLAIMS to
carry the route, so the status strip says once, on click: "This link shares the
planner. Your drawn zones stay on this device." — using the status mechanism the
profile already had rather than adding a surface.

★ ADDING <ShareButton> WAS NOT ENOUGH, AND check:share-policy COULD NOT SEE IT.
The button rendered NOWHERE. This bundle mounts its SFC directly as the custom
element — deliberate, it is what surfaces the defineExpose()d client-side API on the
DOM element ("the only API we can build has to be CLIENT SIDE") — and so installed
no i18n. ShareButton calls useI18n(), which throws in setup(), and index.js's own
header records the cost: "a thrown setup() removes the whole shadow-DOM subtree
silently." Measured: a runtime SyntaxError, five toolbar buttons rendered, the sixth
absent — while the policy gate, which counts <ShareButton> in SOURCE, said 1 button
and PASSED. That blind spot is now written into the gate, pointing at the smoke that
does catch it.

Fixed WITHOUT changing the mounting shape: defineCustomElement takes configureApp
whichever component is the root, so i18n installs and the API stays on the element.
The direct-mount decision and i18n were orthogonal; I had assumed they were coupled.

VERIFIED AT THE RENDERED LAYER: share-roundtrip-smoke resolves the url 200 text/html
and REOPENS it as <prayer-walk-planner> with a rendered canvas. The client-side API
was re-verified after the change — all 7 exposed methods present, addPolygon ->
listPolygons round-trips, 0 page errors. i18n verified in 6 locales: Share /
Compartir / مشاركة / साझा करें / 分享 / Partager. Added to smoke:share-all, now
12/12 pass 0 fail.
2026-09-24 08:23:30 +00:00
doxa-maps-88655c70
26f60dae29 Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 08:19:29 +00:00
doxa-maps-88655c70
e390454d31 Map controls: restore the keyboard focus ring that outline:none removed
The brief says of the no-selection chooser: "Keep the dropdowns as the
keyboard/screen-reader path." Pin clicking is mouse-only by
construction, so that path is load-bearing — and nothing had ever
pressed Tab. There was zero keyboard coverage across 42 probes.

The path itself is sound, and is now asserted: 30 distinct focus stops;
the chooser's region keys are real focusable buttons and Enter on one
selects it ("All Regions" to "Africa") with no mouse involved; the
SELECT VIEW strip carries role=tab on both buttons with exactly one
aria-selected.

The defect that fell out: of those 30 stops, 29 showed a 2px ring and
one showed outline:none — button.mcb, the shared MapControlButton. Its
line 38 sets outline:none deliberately, to kill the webkit focus box on
click/tap alongside -webkit-tap-highlight-color, but it had no
:focus-visible replacement. So a sighted keyboard user tabbing onto ANY
map control in ANY bundle saw nothing at all. The accessible name was
already correct (aria-label mirrors title, fixed 2026-09-21), so screen
readers were never affected — only the people who navigate by eye and
keyboard, which is the very group outline:none was written for.

The fix is a :focus-visible rule plus a .dark variant. :focus-visible
is the correct half of the pair: it never fires for a mouse click, so
the original reason for outline:none still holds. Consumers re-checked
per RULE 2 — ShareButton, ZoomIn, ZoomOut, Fullscreen, Help,
ThemeToggle, Location, map-controls/index.js, my-upg-100-list and
research-dashboard's DashboardDataTable — all render the button
icon-only and none styled focus themselves, so the rule only adds a
ring where there was none. All five affected bundles rebuilt.

Proven both ways: Tab gives solid 2px rgb(26,115,232) with
:focus-visible true and the "Share" label intact; a real mouse click on
a fresh page gives outline:none with :focus-visible false.

Three of my own errors on the way, each caught before it shipped. I
first read a region key's name as "Africa315" and nearly fixed a
non-bug — the button already carries aria-label="Open the Africa
dashboard", which wins over textContent, and the label and count have a
real 7.5px flex gap. I put backticks in a CSS comment inside a template
literal, which ended the string and broke four of five builds;
check:sfc-parse caught it at once. And my mouse assertion failed twice
on correct behaviour: synthetic PointerEvents with a manual focus() do
not update the browser's pointer-modality heuristic, and clicking the
button that had just held keyboard focus leaves the page in keyboard
modality. Only a fresh page with no key ever pressed is a clean test.

Regression: 22 suites green at the time of commit, including new
keyboard and focus-ring suites.
2026-09-24 08:14:01 +00:00
doxa-maps-88655c70
3a468c2bc3 The poster print preview rasterises too, and a deployment can ship sources without rasters
Some checks are pending
build-gate / build (push) Waiting to run
posters/*.webp is not shipped by default (42MB; bundling pushed the tree to 80MB,
git-packed to 57MiB, and the catalog rejected the push). The DOWNLOAD path has
handled that since d154a76 — PosterWall fetches posters/<id>.html and rasterises
it in the browser. The PRINT path did not: it handed an <img> a url it never
checked, so on a default drop the SAME poster downloaded fine and PRINTED as
"Poster not available yet." That is the bad first impression
IDEA-POSTER-EXPORT-404-ON-DROPPED-FOLDER-TRADEOFF called out by name.

Three things were missing; all three are here:

1. printPoster now climbs the same ladder as the download path — probe the webp,
   and only on a miss fetch the .html, rasterise, and verify PIXELS before opening
   the dialog (an SVG in an <img> is an isolated document: a lost stylesheet prints
   a black square with no error). Blob urls are held until unmount, not revoked at
   print time — the preview <img> is still reading from them.

2. publish-html.mjs wrote into public/assets/posters, which is a GENERATED MIRROR
   that every `bun run tree` rmSync's. Its output never survived, which is a large
   part of why the fallback still had no input months after it shipped. Retargeted
   at data/posters, the tracked source the mirror is made from.

3. bundlePosters is one boolean over a directory holding 40.9MB of rasters AND the
   28 sources, so it could only ever ship both or neither — and "neither" is what
   made both full-size paths 404. posterSources:true ships the sources without the
   rasters, the combination that was previously inexpressible. DEFAULT UNCHANGED:
   28 sources are 27.9MB raw (6.9MB packed, measured with pack-objects), real
   weight in a published tree that needs a deployment decision, not a silent one.

VERIFIED AT THE RENDERED LAYER, not by reading source. check:poster-print-fallback
clicks the real control with every posters/*.webp ABORTED at the network layer —
the default-drop condition forced rather than hoped for — and asserts the HUD
enters "generating the poster" and settles clean. Mutation-tested: removing the
fallback produces 4 failures including "no posters/*.html was fetched". The other
branch is verified too — with a webp present it is used directly, zero html
fetches, no rasterise — so the extra probe is paid only on a miss.

★ THE GATE WAS WRONG THREE TIMES FIRST, each time blaming the product for a
harness fault, which is why it now asserts the HUD instead of an internal:
  - it matched /print/i, but the control reads "UPG prayer poster" — the product
    names the ARTEFACT, not the action;
  - it patched Document.prototype in the PARENT realm while openPrintFrame writes
    into the iframe's own;
  - openPrintFrame calls doc.open() AFTER append, and open() RESETS the document,
    discarding a write patched at append time.
Through all three the .webp/.html request counters looked corroborating, because
the wall loads both by itself. The HUD is what a user sees, survives a refactor of
the print internals, and cannot silently miss.

The 28 poster sources are committed because the fallback has no input without
them. city-* posters are deliberately NOT covered: they fetch OSM street tiles at
generation time, so they are not reproducible offline and their rasters must stay.
2026-09-24 08:07:22 +00:00
doxa-maps-88655c70
f47ee3dfbe Refresh the gallery's baked SRI after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
The rebuild that follows a rebase re-hashes the bundles, and the gallery bakes
those integrity hashes into its page. A stale hash is the silent failure this
repo has already paid for: the browser refuses the script and the page mounts
nothing, with no error a reader would see.
2026-09-24 07:43:31 +00:00
doxa-maps-88655c70
d06bf88195 Stop committing 80.7 MB of generated mirrors of data/
public/assets/<name> is not a source. generate-tree.mjs:1353 rmSync's it and
copies data/<name> over it on EVERY `bun run tree`, so five directories were a
byte-identical copy of a tracked source and BOTH copies were committed:

  posters        378 files  40.9 MB
  people-groups    2 files  23.0 MB
  tables          19 files  14.2 MB
  legacy           4 files   2.0 MB
  vendor           3 files   0.6 MB

Verified identical, not assumed: same file set and matching hashes in all five.
And proven regenerable rather than argued — deleting public/assets/posters and
running `bun run tree` restored all 378 files with laos.webp md5 ea65d2f1, the
same hash the committed copy carried.

This is the Driver's "80mb of images in a public folder" almost exactly, and it
is the cheapest possible version of it: a publish was already rejected once at a
74 MB asset tree, and half that weight was a duplicate of something already in
the same repo. No feature changes, no size tradeoff, nothing regenerates
differently.

★ previews/ IS DELIBERATELY STILL TRACKED. It has no data/ source — make-previews.py
derives it from the full posters — and it is what the poster WALL renders from.
bundlePosters defaults false, so a dropped folder ships previews and NOT posters;
untracking previews would blank every wall. Treating "public/assets" as one
homogeneous pile would have removed the one directory that must stay.

The root .gitignore said "what actually ships is the exported subset in
public/assets/ (see export-public.mjs)" and BOTH HALVES were stale — which is why
the duplication looked intentional. export-public.mjs writes a SIBLING OF THE REPO
(../../../../upg-100-posters or $DOXA_PUBLIC), stating in its own header that this
is "NOT inside public/, because capacitor packages public/ whole"; and
public/assets/ is now generate-tree's mirror of data/. Comment corrected in the
same change, because a stale rationale is what restores a deleted duplicate.

Build green with the mirrors untracked: 7/7 pre, 23/23 bundles, 21/21 post.
2026-09-24 07:39:51 +00:00
doxa-maps-88655c70
2392fe0319 Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 07:30:43 +00:00
doxa-maps-88655c70
e5c2c77561 all-people-groups can be shared: its own button, verified by a round-trip
Some checks are pending
build-gate / build (push) Waiting to run
It was published, embeddable and reachable with NO sharing path by any mechanism —
no ShareButton, no shareUrl config, no composite parent overriding one on its
behalf — while every other published map had exactly one. A reader who landed on
it could not send it to anyone, which is the point of an embeddable jamstack map.
check:share-policy's known debt falls from 2 published maps to 1.

The button is in a MapToolbar (pure layout shell: position, z-index, mobile
offsets) rather than hand-positioned, so it sits where every other map's controls
sit. has-geocoder=false because this profile mounts no search pill. Only the share
control is added — zoom/location/fullscreen each need a live mapRef this bundle
does not expose, and the card asks for a sharing path, not a toolbar.

NOT gated on appReady, unlike this bundle's legend. Sharing a map does not require
its data to have loaded, and gating it would remove the affordance exactly when a
slow or failed load makes a reader most likely to want the link.

★ WHY A RENDERED CHECK WAS NEEDED. check:share-policy counts <ShareButton> in
SOURCE. Mutation-tested by gating the toolbar off: the policy gate still read
"1 button / own-button" and stayed GREEN while the button was absent from the DOM.
A source count cannot tell a rendered button from an unrendered one.

So this is verified by the EXISTING share-roundtrip-smoke, not a new gate. I wrote
one, then deleted it on finding that smoke does strictly more — it fetches the URL
and REOPENS it, asserting the same custom element renders, which catches SPA
fallbacks and hash-stripping that a link-text check cannot. A second share checker
would be the fourth-print-path mistake this repo has already paid for once.
Result: share URL resolves 200 text/html and reopens as <all-people-groups> with a
rendered canvas.

i18n verified at the rendered layer: under ?lang= the button title reads Share /
Compartir / مشاركة / साझा करें / 分享, so the new affordance is translated in every
locale, RTL included. buttons.share is one of the keys restored to parity earlier.

ALSO FIXED, found by running the suite: smoke:share-all was testing the LEAF urls
of the two 'header'-policy bundles, whose button lives in the MFE shell by design,
so it reported 10/12 with two failures that were the policy working as intended.
Swapped country-posters/countries-map for country-posters-mfe and dropped the stale
my-upg-100-list leaf (upg-100-mfe was already listed). Now 11/11 pass, 0 fail.
check-share-policy records the coupling so the list cannot drift from the policy
again.

prayer-walk-planner stays debt-no-share, deliberately. It builds a personal walking
route from polygons the reader draws; a link carrying none of that arrives as an
empty planner, and a button that reliably produces a blank map is worse than none
because it looks like sharing works. That is a semantics decision, not an
implementation one.
2026-09-24 07:25:19 +00:00
doxa-maps-88655c70
0e702e5a34 Merge main into bundler-1/work (regenerate the tree, re-hash) 2026-09-24 07:24:05 +00:00
doxa-maps-88655c70
ca13f04bd2 Regions: delete a dead prop, after auditing what the probes can actually prove
The source change is small: countryName was declared on UupgsList and
passed by region-dashboards but never read — residue of the invented
count line removed earlier. Rule 3 prefers deleting a workaround to
keeping one, so both the prop and its call site are gone. Audits for
the two shapes that revealed real gaps earlier — a prop declared and
never used, and state read but never written — now come back clean
across all four components, and every reachable prop their CountryDetail
passes is implemented.

The substantial finding is about the probes themselves, and it lands in
_qa-cycle rather than here. Scanning all 42 for "no ok() calls AND
never exits non-zero" returned 23, including both numbers reported every
tick: region-vs-live's 79/79 and country-vs-live's 32/32 printed
"N IDENTICAL · M differ" and returned success either way, so the
headline parity was read from text rather than enforced.

_probe-standalone-app.mjs had already drifted for exactly that reason:
it printed its table, counted gaps, and exited 0 regardless. Read
properly it was 9 of 10, not the 10/10 reported for several ticks. The
gap was "one h1 on the page: h1 count 2", and measuring resolved it
rather than silencing it — the composite is a scroll column, both panes
are genuinely on the page (both h1s compute hiddenBy null at 1200x75),
so they are two cloned pages in one document. "One h1 per document"
collides with clone parity, since region-vs-live and list-vs-live both
diff an h1 selector; the clone requirement wins and the property now
states "one h1 per pane (scroll column)" with that measurement
attached.

All three now exit non-zero on a real failure, each proven by a
negative test. Two of those tests were wrong first: one swapped a
selector on BOTH sides so they stayed identical, and one read $? after
a pipe, which reports tail's exit rather than node's.

Also made the chooser's pin assertion wait for the condition instead of
a stopwatch. It had read queryRenderedFeatures at a fixed 4200ms and
measured 0 once while consecutive runs gave 1991 and 2107 — a flaky
check, which is worse than a missing one because it teaches you to
ignore a red. Three runs now return 2107 every time.

Regression: 18 suites green, 9 gates green.
2026-09-24 07:22:38 +00:00
doxa-maps-88655c70
596b5fe5e8 Rebuild the served tree after rebasing onto main
Some checks are pending
build-gate / build (push) Waiting to run
The rebase conflicted in nine public/ files — build OUTPUT, not source. Neither
side of such a conflict is authoritative, so the resolution is to rebuild rather
than hand-pick hunks: these nine differ from what the manual resolution produced,
which is precisely why the rebuild was required before pushing.

No source file conflicted except package.json, where upstream had added
check:maplibre-drops and this branch check:packets-all. Both are kept, and
gates:post is upstream's 20 steps plus this branch's one addition — not either
side's version of the list.
2026-09-24 06:59:37 +00:00
doxa-maps-88655c70
e952e01e4c Assert the packet numbers that were only being printed, and run the gate on every locale
check-packet-render's own header states the law it exists for — "never print a number
without asserting on it" — and two of the nine numbers it printed were decoration.

PAGE SIZE. Printed "612 x 792 pt", never compared. The landscape check only catches
h <= w, so a wrong PORTRAIT size sailed through: A4 is 595x842, taller-than-wide like
Letter, and would have passed on a packet printing with wrong margins on every US
printer. Not hypothetical — @page{size:...} silently falls back in Chromium, which this
repo has already paid for once. Now asserts every page's MediaBox against Letter.

CARD COUNT. `if (!r.cards)` passed a packet carrying ONE card. Counts must stay relative
(a per-zone packet has its own group count), but relative does not mean unasserted:
sheets === 1 + ceil(cards/PER_PAGE) + 2 holds for any zone.

★ That was not enough, and the mutation test is what showed it. Deleting a whole list
sheet removed 15 cards AND one sheet, so the invariant still held — 80 cards / 7 sheets
is valid arithmetic — and the packet PASSED while missing 15 people groups. Internal
consistency is not completeness. So it now cross-checks an INDEPENDENT witness: the
{from}-{to} of {count} header the renderer prints on each list page, where count comes
from groups.length and does not move when a page is lost. It also requires the ranges to
TILE [1..count], which catches a missing MIDDLE page that the totals alone cannot see.

Those headers are TRANSLATED and the languages disagree on order — en/ar read "1-20 of
95" while hi reads "95 में से 1–20" and zh "95 之中的 1–20", total FIRST. A positional
regex would read those backwards. Parsed as digits with max() as the total instead, and
locale digit forms normalised to ASCII, so it is grammar-independent. Verified by
running the missing-middle-page mutation in Hindi as well as English.

MARK 5 WAS ASSERTED BY AN ENGLISH WORD. body.includes('Gadarenes') would go RED on a
correctly-translated packet, and passes today only because every locale falls back to
English for that key — the check and the bug were locked together. The renderer now
emits class="p4-passage"/"p4-credit" as test hooks (the block is styled inline and had
no selector at all), and the gate asserts length >= 1200, which holds in any language.

check:packets-all runs the gate over EVERY generated packet rather than one hand-named
file. The single-file gate SKIPs without an argument, so it was in package.json, passing
in all 11 locales, and in NEITHER gates:pre NOR gates:post — it ran only when someone
typed a filename. Now in gates:post (21/21, 11 packets through a real browser). SKIPs
when no packets are generated, so a clean checkout is unaffected.

Mutation-tested: A4 FAILs, a dropped sheet FAILs, a missing middle page FAILs in en and
hi, a truncated passage FAILs, a removed passage FAILs. i18n verified end to end by
adding a PARTIAL new language as a data block only — 3 of 23 keys — and confirming the
translated strings render, untranslated keys fall back to English, and nothing is blank.
2026-09-24 06:55:58 +00:00
doxa-maps-88655c70
6d29493e84 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 06:53:50 +00:00
doxa-maps-88655c70
a32097751b Regions: their second filters panel was missing, and it carries two of their bugs
The expanded filters panel was the largest interactive surface never
compared to theirs, and comparing it found a whole panel absent.
Measured on their live country page: TWO .filters__panel blocks and
THREE .filter-toggle buttons — Adopted, Engaged, Exact Search — against
our one panel and zero toggles. The dropdowns already matched exactly,
four of them, with their country dropdown correctly hidden by
lockCountryCode.

A tell I had missed: the search already read active.exact for the Fuse
threshold, so Exact Search's BEHAVIOUR was ported while the control
that sets it never was.

Ported their panel, their three toggles, their toggle semantics (a
second click removes the filter), their activeFilters shape so each
toggle also renders as a chip, and .filter-toggle styled from the
MEASURED computed values of their live button rather than by chasing
tokens. The labels needed no new keys — Adopted and Engaged were
already translated in all eight locales.

Two of their bugs are cloned deliberately, both measured on their live
/regions/india at 855 rows:

- ENGAGED RETURNS ZERO. Their branch filters on u.people_praying, and
  people_praying is not in the field list they request — they fetch
  people_committed. The field is always undefined, so the filter
  matches nothing: their page goes 855 to 0. Reading people_committed
  instead would make us differ, and the ruling is that what exists on
  DX-life must match. The upstream fix is one word in their file.
- THE ACTIVE STATE IS WHITE ON WHITE. Their rule sets background-color
  var(--color-surface-brand-lighter, rgba(0,80,160,0.05)) with color
  var(--color-white), and --color-surface-brand-lighter is not defined
  in their token files. Their active button computes background
  rgb(255,255,255) and color rgb(255,255,255) — an invisible label,
  now visible in our screenshot too. Defining that one token upstream
  fixes both sides.

Both are flagged for the Driver rather than quietly corrected: a bar
chart was already removed under this ruling for the opposite reason,
being in ours and not theirs.

Proof: _probe-filter-toggles.mjs, all assertions pass — two panels,
three toggles, nine measured style properties, Adopted narrowing 24 to
2 and appearing as a chip, a second click restoring, Engaged emptying
to 0 exactly as theirs does, Exact Search changing nothing alone.

Regression: 22 suites green.
2026-09-24 06:53:22 +00:00
doxa-maps-88655c70
2484f26304 facet gate: say "I cannot measure this engine", not "the map is broken"
Some checks are pending
build-gate / build (push) Waiting to run
Run against ?provider=maplibre the gate reported

    FAIL — the map never populated (no rd-bubbles features after 90s)

about a dashboard that visibly renders all 2107 dots with a working
choropleth. It reads the source's live GeoJSON off `_data`, a private Mapbox
internal. MEASURED on one dashboard, one build, only the engine differing,
by calling the app's own setData() to shrink 2107 features to 5:

    mapbox    _data 2107 -> 5      _options.data 2107 -> 2107   (_data is LIVE)
    maplibre  _data never an FC    _options.data 2107 -> 2107   (NEITHER is live)

So on MapLibre no private field tracks setData and the wait can never
succeed. It now diagnoses the engine and SKIPs with the reason. Same
false-FAIL shape removed from check:filter-width last tick: a control that
accuses correct code is worse than no control.

★ DELIBERATELY THE SMALL FIX. Re-expressing the assertion on the public
querySourceFeatures() would change the gate's reported counts, and the two
earlier attempts at that made things worse -- one failed BOTH engines by
preferring the construction-time _options.data, the other called a
node-side helper from inside a page-evaluated string where it is simply
undefined. Every line of page code added here is self-contained for exactly
that reason.

THREE CONTROLS:
  maplibre     -> SKIP, exit 0, naming why it cannot count
  mapbox       -> PASS, exit 0
  --self-test  -> gate REJECTED the reverted build, catching
                  'circle-color is the flat literal "#ef6c00"'
The third is the one that matters: the gate still catches the bug it exists
for rather than having been quietened.

This removes the LIE, not the GAP. MapLibre dashboards remain unverified by
this gate; BUG-FACET-GATE-CANNOT-MEASURE-MAPLIBRE stays open, downgraded to
P3 and re-scoped to the coverage question.
2026-09-24 06:41:38 +00:00
doxa-maps-88655c70
ad6787e617 Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 06:34:45 +00:00
doxa-maps-88655c70
0951b91875 Merge main into bundler-2/work 2026-09-24 06:34:31 +00:00
doxa-maps-88655c70
f03b900ccc filter-width: an absent server is not a width regression
The gate printed "FAIL — 2 host(s)" when it had measured NOTHING —
ERR_CONNECTION_REFUSED on :4173, "UNMEASURED" twice, then a failure. That
accuses the product of a regression on the strength of a missing process,
and it already cost one wrong report in this flight: the gate was reported
as failing when it had simply never run.

★ THIS DOES NOT REVERSE THE AUTHOR'S RULE. "An unmeasured host is a failure.
A gate that quietly drops a host reports success about something it never
looked at" is correct for a STRUCTURAL miss — the page loads and the trigger
or tab cannot be found, meaning the gate has been defeated — and that path
is kept, comment and all. Only connection-level errors are separated out.

It is also not a judgement call, because the house already decided: four
sibling browser gates skip on an unreachable host and say so in the same
words — check-fits-the-screen, interop-harness, check-catalog-identity and
check-legend-matches-source all print "SKIP — ... unreachable right now.
Not a product finding." This gate was the outlier.

THREE CONTROLS, ALL FIRED:
  no server                      -> SKIP, exit 0
  server + healthy code          -> PASS, exit 0
  real regression, the original
  flex RANGE put back on
  .filter-dropdown               -> FAIL, exit 1 ("the row grew 369px")
The third is the one that matters: it proves the gate can still catch the
bug it exists for, rather than having been quietened.

Before believing any of those browser measurements I ran `bun run tree` and
confirmed check:sri 0 — a single-bundle build leaves stale SRI, the browser
refuses the script, and the page then measures clean for the wrong reason.
That trap made a negative control vacuous earlier in this flight.
2026-09-24 06:34:29 +00:00
doxa-maps-88655c70
81cb8ad235 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
2026-09-24 06:30:46 +00:00
doxa-maps-88655c70
a5628171ea Merge main into bundler-1/work (regenerate the tree, re-hash) 2026-09-24 06:30:29 +00:00
doxa-maps-88655c70
cf33a79c32 Merge origin/main into the studio branch
Some checks failed
build-gate / build (push) Has been cancelled
The two repos carry the same work under different commit hashes, so this
branch had drifted 30 commits behind while looking 8 ahead. Pushing it would
have DELETED 315 lines of newer upstream work, including
check-maplibre-layer-drops.mjs entirely and the current versions of six
library/ files.

Both conflicts were textual, not semantic: package.json's script map is
key-for-key identical to remote's (verified before resolving, not assumed),
and check-packet-render.mjs is byte-identical on both sides — add/add
artifacts of two repos that built the same file in separate histories.

This also retires the divergence finding committed a moment ago. It was
measured against this repo's STALE checkout rather than against main; the
merge brings library/ to byte-identical with wt-packet and
check:poster-prints — which failed with "printPacket is not a function" —
now passes. The durable part of that finding is the absolute-path bug, and
that is already recorded where it matters, in the gates themselves.

check:i18n-parity, check:locale-reachable, check:poster-prints,
check:tabs-localized and check:no-abs-paths all pass on the merged tree.
2026-09-24 06:29:57 +00:00
doxa-maps-88655c70
51a5636601 Make the last two gates repo-relative; report the library divergence they expose
check-locale-reachable and check-poster-prints defaulted REPO to a hardcoded
wt-packet path, so like check-i18n-parity before them they graded the OTHER
repo's files and printed PASS for a tree they never opened. These two are the
third and fourth instance of that pattern. Now repo-relative, so
check:no-abs-paths — which DOES gate this repo's build — stays green.

Reading its own tree, check-poster-prints now fails honestly:
usePosterPrints.js here has no printPacket. That is real, and it is NOT fixed
here, because the fix that worked for the locale catalogs would be destructive
for this code. All six diverged library/ files have lines that exist only in
this repo — two-way divergence, not staleness — so a wholesale copy would
delete studio-side work. What made the catalog sync safe was the measurement
that nothing would be lost, not the resemblance.

Recorded in internal/FINDING-library-diverged-from-wt-packet.md with the
per-file line counts and the decision a resolver needs to make. None of these
four gates run in this repo's gates:post, so nothing is newly blocked; they are
runnable by hand and now honest, which beats passing by reading another repo.
2026-09-24 06:27:48 +00:00
doxa-maps-88655c70
52e6f9ca2e Merge main into bundler-1/work (regenerate the tree, re-hash)
Six conflicts, all generated artifacts, resolved by rebuilding the
PosterWall consumers and regenerating the tree.

Main brought two things that land on this branch's work: another
builder made check:i18n-parity's DOXA_REPO default portable, which is
the wt-packet default this branch flagged last tick, so the gate now
measures the repo it lives in; and a new check:tabs-localized proves a
catalog is actually REACHED rather than merely symmetric. That gate
covers SemanticTreeLegend's tabs.json, not this element's SELECT VIEW
strip, so the strip's three strings remain uncovered and untranslated.
2026-09-24 06:26:43 +00:00
doxa-maps-88655c70
e8e2be1498 Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	package.json
#	public/doxa-maps/all-people-groups/all-people-groups.js
#	public/doxa-maps/all-people-groups/all-people-groups/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-24 06:25:29 +00:00
doxa-maps-88655c70
2ddb09b42c Regions: RTL kept three rules on the wrong side, and the tab strip never localised
Every vs-live diff so far ran at LTR, so RTL was the next coverage gap —
and it is where layout actually breaks, because a physical `left`
silently survives a direction flip while a logical
`inset-inline-start` does not. Their CSS uses the logical form; ours had
physical `left` in three direction-sensitive places.

The new probe compares each element's offset from the INLINE-START edge
— the left edge in LTR, the right edge in RTL — which is the one number
that goes wrong when a physical property survives. Measured against
their live /ar/regions/cameroon:

- .search-icon sat 566px from the inline-start edge against their
  19.5px: it had stayed on the physical left, which is the inline END in
  Arabic. Replaced with their rule verbatim (blocks/_form.scss:119 —
  inset-inline-start:1em, top:0/bottom:0, margin:auto 0, 1em square),
  which also fixed a second difference, since our fixed 18px square
  could not track the input's type where theirs is 1em. Ours now
  measures their exact 19.5px.
- The country bullet kept left:-16px, so in RTL it detached on the
  wrong side — computed right 256.938px instead of a -16px overhang.
- The chooser's region legend was pinned to the physical left. That one
  is a sanctioned addition rather than a clone, but a legend stuck on
  the physical left in an RTL layout reads as broken whoever designed
  it.

Reading the Arabic screenshot then showed every heading, strapline and
map note in Arabic and RTL while the SELECT VIEW strip still read
"Regions List" — the one piece of chrome that never localised, because
regions-mfe hardcoded its labels and imported no i18n at all. Now
routed through t(), with the legend going through ViewSelect's existing
prop so the shared component and its three consumers stay untouched.

No catalogue keys were added, deliberately. Their common.json has
"Regions" and "View" but none of the composites, so there is nothing to
copy; translating our own additions is not this agent's call; and
check:i18n-parity tracks a per-locale debt ceiling — it/ro/zh sit at
exactly 94 missing because DX-life translates 8 of our 11 locales — so
English-only keys would push them to 97 and fail it. The shim falls
back to the key, so today's rendering is byte-identical. Proven rather
than assumed: a sentinel injected into ar/regions.json rendered on tab
1 at locale=ar while tab 2 stayed English, then was reverted.

Two of my own assertions were wrong first. The probe set
profile-config.locale on every custom element, which on the composite
means three regions-dashboard elements — host and both panes — and
writing a fresh {locale:'ar'} onto a pane that had inherited its config
by provide() clobbered it, leaving every root LTR; I nearly "fixed" RTL,
which works on all three leaves. And checking inset-inline-start in an
LTR context is meaningless, since it merely mirrors left.

Worth flagging: check:i18n-parity's DOXA_REPO defaults to wt-packet,
another worktree, so it must be pointed at this repo to mean anything.

Regression: 22 suites green.
2026-09-24 06:24:45 +00:00
doxa-maps-88655c70
9056b9329c All 12,379 pins were missing on MapLibre, and now a gate says so
Having found that MapLibre drops a whole layer for one unrecognised paint
property, I swept all 17 isMap leaves under ?provider=maplibre listening to
the map's own error event -- the only place this class surfaces. One more
real instance:

    layers.apg-pins.paint.circle-sort-key: unknown property

circle-sort-key is a LAYOUT property, written into paint. Mapbox tolerated
the misplacement; MapLibre rejected the layer, so every pin on
all-people-groups was absent under the keyless engine while the page looked
perfectly healthy. Moved to `layout`; the sort order itself is unchanged.
Screenshots read on both engines: 12,379 pins, identical.

NEW GATE check:maplibre-drops loads every isMap leaf under maplibre and
fails on the layer/source-dropping error family. It skips cleanly when the
server is unreachable, because an UNMEASURED check is not a FAILED one --
the flaw already filed against check:filter-width.

★ THE NEGATIVE CONTROL WAS VACUOUS TWICE BEFORE IT WAS REAL, and that is
worth more than the fix. The first poison renamed the layout block, which
does not reproduce the bug -- the property simply vanishes, no error. The
second poison was faithful, circle-sort-key back in paint, and the gate
STILL printed PASS. Cause: I rebuilt with `BUNDLE=... vite build` and did
not run `bun run tree`, so the leaf carried stale SRI, the browser REFUSED
the script, no map mounted, and the gate saw zero errors. A check that ran
correctly and measured nothing, reading as green -- the first entry in
AGENT-BRIEF's troubleshooting table, walked into anyway.

After `bun run tree` (check:sri 0) the gate FAILED on the real bug, naming
the exact property, and PASSED again once restored. Both directions seen on
this codebase, which is the only thing that makes its green mean anything.

gates:pre 0, gates:post 17/18 (pre-existing verify/tables-complete),
check:doc-scripts 0, check:no-orphan-gates 0.
2026-09-24 06:24:40 +00:00
doxa-maps-88655c70
b0a267c894 Sync the locale tree forward from wt-packet; stop grading another repo's locales
This repo's check-i18n-parity hardcoded an absolute path to wt-packet, so it
read THAT repo's locales and reported PASS on a tree it never opened. Making it
repo-relative revealed this tree was 103 keys behind in it/ro/zh and short in
every other locale too — including buttons.json:share and shareSteps, the exact
orphan the gate's own header describes it as having been written to catch. The
bug it was built for had reappeared in a second repo, invisible.

The tree is a strict subset of wt-packet's: zero keys exist only here, zero
translations disagree. So this is a lossless forward copy, not a merge. All 11
locales now carry 10 of 11 areas; regions.json in it/ro/zh is the only debt.

Adds check-tabs-localized, which renders the tab labels and asserts they CHANGE
with the locale — key-parity alone cannot distinguish a shipped translation from
a dead catalog.
2026-09-24 06:23:39 +00:00
doxa-maps-88655c70
4cb04480cc Localize tab labels for it/ro/zh, and prove the catalog is actually read
Some checks are pending
build-gate / build (push) Waiting to run
120 keys across three locales: legend.json (12 each), search.json kinds (10
each) and tabs.json (18 each, previously absent entirely). Each locale follows
its own established vocabulary rather than a parallel one — Popolo/Popor/民族,
matching the terms already used in that catalog's detail and legend strings.
zh stays Simplified here, as the rest of this tree is, even though the prayer
packet's zh block is Traditional: a reader sees 民族 next to 祷告进度, never
the two files side by side.

Ceilings drop 94 -> 54 per locale. regions.json is the only i18n debt left in
the app: 54 keys of full sentences, three locales.

WHY THE NEW GATE. check:i18n-parity compares catalogs TO EACH OTHER, so it
certifies a file no t() call ever reaches — perfect symmetry, zero consumers,
green forever. Grepping for t('tabs. found ZERO call sites and tabs.json was
nearly recorded as dead. It is not: the only consumer builds the key
dynamically (SemanticTreeLegend.vue:40, 'tabs.' + (tab.i18nKey || tab.id)),
which no literal-prefix grep can see. The reverse error appeared in the same
pass — regions. and options. matched only poster-regions.mjs and a JS options
parameter. Static reachability is unavailable in both directions, so
check:tabs-localized renders the labels and reads them.

It asserts DIFFERENCE from English, not mere presence: te() falls back to the
hardcoded English label, so "shows some text" passes on the exact failure
being tested. Mutation-tested three ways — missing catalog FAILs, an all-English
catalog FAILs (first draft PASSed it, taking a per-pair exemption seven times),
one legitimate collision still passes with a note.

Both i18n gates now run in gates:post; neither was wired into the build before.

Also fixed, found by the build rather than sought:
- check-i18n-parity hardcoded an absolute path, so the studio copy silently
  graded wt-packet's locales and reported PASS on a tree it never read. Now
  repo-relative. That exposed the studio's own locale tree as a strict subset,
  103 keys behind including buttons.json:share — the very orphan this gate's
  header says it was written to catch, reappeared unseen in a second repo.
  Synced forward: zero studio-only keys, zero text conflicts, lossless.
- index.json still declared field-access-visas with rows:1146, a leftover from
  the commit that removed that table as private. The generator already filters
  private tables; the committed artifact predated the filter. Removing the
  declaration also removes the row-count leak. Copying the file in would have
  reopened the leak.
- check:sfc-parse needs an installed node_modules and is the first pre-gate to
  fail without one, so a dependency-less worktree reads as a parse error.
  Documented, with the reason switching it to bun only appears to fix it.
2026-09-24 06:23:30 +00:00
doxa-maps-88655c70
66c4d3163b Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
Six conflicts, all generated artifacts under public/, resolved by
rebuilding the three PosterWall consumers and regenerating the tree
rather than hand-merging built files.

Main brought the i18n catalogues to parity, and another builder has
translated this branch's three poster-dropdown strings into all seven
non-English locales — so the 'en only' caveat on that feature is now
closed. Main's two new gates, check:i18n-parity and
check:locale-reachable, both pass against the merged catalogue.
2026-09-24 06:01:41 +00:00
doxa-maps-88655c70
0fffb90cca test(i18n): ratchet down again — seven locales must now stay complete
Some checks are pending
build-gate / build (push) Waiting to run
344 -> 282 keys. de and hi reached zero and join ar/es/fr/pt/ru as entries
DELETED from the DEBT map: the gate treats an absent locale as "must be
complete", so removal is what makes a finished translation permanent.

The three that remain (it, ro, zh) are each missing WHOLE catalog files rather
than stray keys, which is worth stating in the file — it tells the next person
these are real translation work, not the propagation gaps the last two ticks
closed.

Confirmed the guarantee before relying on it: deleting one Hindi tab key reports
"hi is missing 1/176 key(s) and has no DEBT entry" rather than passing.
2026-09-24 05:58:55 +00:00
doxa-maps-88655c70
10f83a943b Regions: links we render as buttons had lost their anchor styling
Found by a COVERAGE AUDIT, not a screenshot. region-vs-live diffs
/regions/africa and country-vs-live diffs only UupgsList's nine
elements, so the country page's head and the region card had no
property diff at all. Adding both produced four defects sharing one
root cause: DX-life renders these as <a>, we render them as <button>
because this element has no router, and a button inherits neither the
UA underline nor their `a` rule (_globals.scss:132 — color
var(--color-brand-light), font-weight medium,
text-decoration-thickness 2px, text-underline-offset 4px).

- The dashboard back-link was #4e594f with no underline against their
  #cbc5b9 underlined. Their .light-link recolours to
  --color-brand-secondary = $color-beige-300 = #cbc5b9, exactly the
  measured rgb(203, 197, 185). Wrong on BOTH our region and country
  pages.
- The region-card link was font-weight 400 against their 500.
- Its icon spacing came from gap:10px where theirs comes from
  .with-icon .icon.right { margin-inline-start: 1rem } — the same 10px
  on screen, but their rule would never have applied to our markup, so
  the mechanism is now theirs.
- The country entries were rgb(42, 51, 42), our body ink, against
  their rgb(78, 89, 79) = $color-green-400 = --color-brand-light.
  Their country links carry no class at all — they simply take the `a`
  rule, which is why '.country-link' matched nothing on their side and
  the element read as unmeasured until the selector became
  '.countries-grid a'.

Coverage added so none of this can come back: a new
_probe-country-head-vs-live.mjs at 29/29 (back-link, h1, strapline, map
note, search-box container); the back-link added to region-vs-live,
which went 73 to 79 IDENTICAL; the search-box CONTAINER added to
country-vs-live, 28 to 32, so the 50ch cap cannot regress; the region
card added to list-vs-live at 30/30.

Three of my own assertions were wrong before the code was: I omitted
fontWeight and the underline geometry from the back-link spec while
their `a` sets all three; I compared `gap` on an element where theirs
never sets it; and my sub-pixel tolerance used Number('246.938px'),
which is NaN, so NaN <= 0.5 was false and it silently never fired.

Regression: 22 suites green, 9 gates green.
2026-09-24 05:58:43 +00:00
doxa-maps-88655c70
68963c5e1e test(i18n): ratchet down again — seven locales must now stay complete
344 -> 282 keys. de and hi reached zero and join ar/es/fr/pt/ru as entries
DELETED from the DEBT map: the gate treats an absent locale as "must be
complete", so removal is what makes a finished translation permanent.

The three that remain (it, ro, zh) are each missing WHOLE catalog files rather
than stray keys, which is worth stating in the file — it tells the next person
these are real translation work, not the propagation gaps the last two ticks
closed.

Confirmed the guarantee before relying on it: deleting one Hindi tab key reports
"hi is missing 1/176 key(s) and has no DEBT entry" rather than passing.
2026-09-24 05:58:25 +00:00
doxa-maps-88655c70
7580274ed4 feat(i18n): German and Hindi reach complete — 7 of 11 locales done
Some checks are pending
build-gate / build (push) Waiting to run
Continues paying the debt the parity gate measured, cheapest first.

German and Hindi carried an IDENTICAL 22-key gap — the same legend and search
keys, added to English after those two catalogs were last touched. One batch of
work closed German entirely and took Hindi to 18, which was its whole missing
tabs.json. Both are now complete: 344 -> 282 keys.

Terminology follows each catalog's OWN existing vocabulary rather than inventing
a second one for the same concepts — de keeps Volk/Sprachfamilien, hi keeps
जनसमूह/भाषा परिवार. Tab labels stay SHORT because they sit in a narrow rail:
Spanish already uses "Pueblos" rather than a literal rendering, so Hindi does
the same. UUPG, WAGF and DOXA remain acronyms, as in every locale.

Seven locales (ar es fr pt ru de hi) are now DELETED from the gate's DEBT map
rather than set to zero — an absent entry means "must be complete", so deletion
is what converts finished work into a permanent guarantee. Verified: removing
one Hindi tab key reports "hi is missing 1/176 key(s) and has no DEBT entry".

Verified at the RENDERED layer: "View country posters" comes back as
"Länderposter ansehen" (de) and "देश के पोस्टर देखें" (hi), zero page errors —
which also confirms Devanagari shapes correctly in the APP, not only in the
prayer packet where the font was first vendored.

The three remaining (it, ro, zh at 94 each) are missing WHOLE FILES —
regions.json and tabs.json — so they are real translation work rather than the
propagation gaps closed here.
2026-09-24 05:58:16 +00:00
doxa-maps-88655c70
b4902a7fed test(i18n): ratchet down — five locales now must stay complete
Some checks are pending
build-gate / build (push) Waiting to run
The ceilings drop as the debt is paid: 429 -> 344 keys, and ar/es/fr/pt/ru
reached zero.

Those five are REMOVED from the DEBT map rather than set to 0. The gate already
treats a locale with no entry as "must be complete", so deletion is what
converts a finished translation into a permanent guarantee — setting 0 would
read the same but leaves a line inviting someone to raise it.

Confirmed the guarantee works before relying on it: deleting one Spanish key
reports "es is missing 1/176 key(s) and has no DEBT entry" rather than passing.
2026-09-24 05:53:31 +00:00