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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Working the 429-key debt the parity gate surfaced last tick, cheapest first.
popup.json existed ONLY in English and French — the country popup's four labels
(WAGF Region, WAGF Bloc, Country, UUPG Count), which is what a reader sees when
they click a country on the map. Now in all 11. WAGF and UUPG stay as acronyms
in every language, following what French already did: they are proper nouns of
the dataset, not words to translate.
Then six keys that were missing from EVERY non-English locale identically —
added to English at some point and never propagated: legend header.default,
search sections.within/allData, and the three country-poster strings in
regions.json. The {selection} placeholder is preserved.
Result: ar, es, fr, pt and ru are now COMPLETE, and 429 -> 344 keys of debt.
Those five are deliberately REMOVED from the gate's DEBT map rather than set to
zero — a locale with no entry must be complete, so none of them can silently
regress. Verified: deleting one Spanish key reports "es is missing 1/176 key(s)
and has no DEBT entry".
Verified at the RENDERED layer, not by counting files: "View country posters"
comes back as "Ver los carteles del país" under ?lang=es and "Показать плакаты
страны" under ?lang=ru, zero page errors.
★ One key I translated turned out to be DEAD: legend.header.default is consumed
by nothing — my first probe appeared to test it and was actually matching the
substring "Legend" inside this profile's hardcoded LEGEND_TITLE ("My UPG 100
List — Legend"). A probe that matches an artifact proves nothing; the assertion
has to be a string only the translated key can produce.
The failure here is silent BY DESIGN. vue-i18n falls back to English for a
missing key, which is the right runtime behaviour and exactly why catalogs drift
unnoticed: the app looks fine, nothing errors, and a reader who chose Spanish
just gets English words with no signal anything is missing.
Measured when this was written: buttons.json had 21 keys in English and 19 in
every other locale. The orphans were `share` and `shareSteps` — the Share
button's title and its explainer, rendered on EVERY map. Ten locales had been
shipping an English Share button since those keys landed.
★ IT FOUND MUCH MORE THAN THE BUG THAT PROMPTED IT: 429 missing keys across ten
locales, with popup.json existing only in English and it/ro/zh also lacking
regions.json and tabs.json entirely — 101 of 176 keys each.
So it RATCHETS rather than blocks. Each locale has a ceiling; the gate fails
when one gets WORSE and prints a nudge when one gets better so the ceiling can
be lowered. Failing outright on 429 keys of debt this gate did not create would
block every build, and a gate everyone disables protects nothing. A locale with
no ceiling entry must be complete, so a NEW locale cannot be added half-done.
Keys are compared FLATTENED, so a nested object or an array is checked
key-by-key rather than counted as one present key.
Confirmed to fail before acceptance: deleting two French keys reports
"fr regressed: 8 missing key(s), was 6" and names them.
Generated output only — three bundles rebuilt and the tree regenerated so the
SRI hashes match the shipped scripts. Re-verified rather than assumed:
check:i18n-parity and check:deck-click both pass afterwards.
Last tick made the 11 locales selectable. That immediately exposed what was
sitting behind the door: the wall's labels — Poster Map, Prayer List, Card Deck,
Print Options, Show on map, Close — were hardcoded English, so a reader who
chose Spanish got Spanish page furniture around English buttons.
All six are now keys in buttons.json, translated across all 11 locales, plus the
aria-label which interpolates the item name rather than concatenating it.
★ AND IT CLOSED TWO OLDER ORPHANS. buttons.json had 21 keys in English and 19
everywhere else: `share` and `shareSteps` — the Share button's title and its
five-step explainer, rendered on EVERY map. Ten locales had been shipping an
English Share button since those keys were added, invisibly, because vue-i18n
falls back to English rather than erroring. Both are translated now.
Verified in a real browser across four languages: the wall's buttons read
"Lista de oración | Mapa del cartel" under ?lang=es, "Liste de prière | Carte de
l'affiche" under ?lang=fr, and the Arabic text renders with dir=rtl — zero page
errors in each. The Share button's title follows too: Compartir, Поделиться.
The wall's `t` and `locale` come from the SAME useI18n instance, so a label and
a printed card deck can never disagree about which language the reader chose.
The failure here is silent BY DESIGN. vue-i18n falls back to English for a
missing key, which is the right runtime behaviour and exactly why catalogs drift
unnoticed: the app looks fine, nothing errors, and a reader who chose Spanish
just gets English words with no signal anything is missing.
Measured when this was written: buttons.json had 21 keys in English and 19 in
every other locale. The orphans were `share` and `shareSteps` — the Share
button's title and its explainer, rendered on EVERY map. Ten locales had been
shipping an English Share button since those keys landed.
★ IT FOUND MUCH MORE THAN THE BUG THAT PROMPTED IT: 429 missing keys across ten
locales, with popup.json existing only in English and it/ro/zh also lacking
regions.json and tabs.json entirely — 101 of 176 keys each.
So it RATCHETS rather than blocks. Each locale has a ceiling; the gate fails
when one gets WORSE and prints a nudge when one gets better so the ceiling can
be lowered. Failing outright on 429 keys of debt this gate did not create would
block every build, and a gate everyone disables protects nothing. A locale with
no ceiling entry must be complete, so a NEW locale cannot be added half-done.
Keys are compared FLATTENED, so a nested object or an array is checked
key-by-key rather than counted as one present key.
Confirmed to fail before acceptance: deleting two French keys reports
"fr regressed: 8 missing key(s), was 6" and names them.
My previous commit changed the REPO fallback to
resolve(dirname(fileURLToPath(import.meta.url)), '..') but did not add the
fileURLToPath import, so the gate died at load with
ReferenceError: fileURLToPath is not defined
and I pushed it. node --check passed because that is a RUNTIME error, not a
syntax one, and check:no-abs-paths went green because the offending literal
really was gone -- so both of the checks I ran said yes while the gate
itself could no longer run at all. The script's own exit code printed 1 in
the same output I pushed from, and I did not stop on it.
Verified this time by running the gate, not around it:
check:locale-reachable 0, check:no-abs-paths 0.
Fourth instance of one pattern in this flight, after check-zone-census,
check-share-policy and check-poster-prints: a new internal/ gate hardcodes
its author's worktree (/home/.../wt-packet) as the REPO fallback, and
check:no-abs-paths -- which runs inside build:unlocked -- turns red for
every other builder on the next merge.
DOXA_REPO still wins and is untouched; only the default becomes portable,
via the idiom check-no-absolute-paths.mjs prints in its own failure text.
Verified both ways: check:no-abs-paths 0, check:locale-reachable 0 on the
derived default.
Four occurrences is a template problem, not four accidents.
The region fills did not paint under MapLibre, and three ticks of
instrumenting addLayer() found nothing because the failure never travels
through addLayer's return path. Listening to the map's own `error` EVENT --
the only place it surfaces -- gave it in one run:
layers.regions-fill.paint.fill-emissive-strength: unknown property
layers.regions-border.paint.line-emissive-strength: unknown property
*-emissive-strength is a Mapbox GL v3 lighting property. MapLibre rejects a
layer whose paint carries an unknown property and DROPS THE WHOLE LAYER:
addLayer() accepts the call, emits an error event, and silently omits the
layer. getLayer() then returns undefined and the map renders without it, so
it reads as "the fills do not paint" rather than as a failure.
★ THE REPO ALREADY KNEW THIS, FOR CIRCLES ONLY. useMapProvider.emissivePaint()
exists and its own docstring says "the whole layer is dropped -> invisible
pins" -- but it was hardcoded to 'circle', while the fill, line and
clustering layers still wrote the literal and kept being dropped for exactly
the documented reason. Generalised to emissivePaint(map, kind) and applied
at all five literal sites: useMapLayers x3, useCountryOutline,
useMapClustering. One helper, one rule, no sixth copy.
A second defect was hiding behind the first: regions-border still declared
'source-layer' on the geojson source ("Source layer country_boundaries does
not exist on source"), which only became visible once the paint error
stopped masking it. Fixed the same way as regions-fill.
MEASURED on the built tree: 0 error events, both layers present, and the
screenshot shows the WAGF fills painting under MapLibre -- green, blue,
purple, red, orange -- matching the Mapbox control, which is itself
re-shot and unchanged. Provider sweep 15/17 (the 2 render no map at all),
gates:pre 0, gates:post 17/18 with only the pre-existing
verify/tables-complete failure.
The bug this gates is unreachability, not incorrectness. The packet shipped 11
complete translations while detectLocale() read only <html lang>, which every
generated leaf page hardcodes to "en" — so ten of them could not be reached by
any reader on the static tree. Nothing was wrong; nothing was visible.
Two assertions, because they fail separately:
RESOLUTION ?lang= / ?locale= selects a supported locale, an unsupported
value falls back to English, a region tag (pt-BR) resolves to its
base, and with no param the host page still wins.
REACHABILITY the packet's language list and SUPPORTED_LOCALES cannot drift
apart, and the leaf pages still declare a lang — so if someone
later drops that attribute, the host-page path does not silently
become unavailable while the tests stay green.
★ It evaluates the SHIPPED detectLocale rather than reimplementing it. A gate
that reimplements the thing it tests only ever proves the copy agrees with
itself — which is how a fix can be reverted in the source while the gate keeps
passing.
Confirmed to fail before acceptance: removing the URL branch reports all four
param cases resolving to "en".
Generated output only. Regenerating rather than hand-merging keeps the tree a
true function of the merged source — and refreshes the SRI hashes, which is not
optional: a stale hash makes the browser refuse the script and the page mounts
NOTHING, silently. Re-verified afterwards rather than assumed: check:locale-reachable
and check:deck-click both pass.
The packet ships in 11 languages, every string translated. detectLocale() read
ONLY document.documentElement.lang — and EVERY generated leaf page in this repo
hardcodes lang="en" (measured: 20 of 20, later 34 of 34 after the tree grew).
So on the built static tree — the jamstack case these bundles exist for — ten of
the eleven translations could not be reached by any reader. Correct, complete,
and invisible. Several ticks of translation work landed behind a door with no
handle.
?lang= / ?locale= is now consulted FIRST, then the host page, then English. A
query parameter is the smallest thing that makes them reachable: no per-language
page, linkable and shareable, and the WordPress/Polylang path through <html lang>
is untouched — a gate assertion keeps the leaf pages declaring a lang so that
path cannot silently become the only way in. Both spellings are accepted because
both are in use here (`lang` in profile configs, `locale` in the i18n layer) and
guessing wrong is a silent no-op.
⚠ AND MAKING ARABIC REACHABLE EXPOSED THE NEXT PROBLEM. Only ONE component reads
RTL_LOCALES today, so selecting ar would have given Arabic TEXT inside a
left-to-right layout everywhere else — readable glyphs in the wrong place, which
reads as a styling quirk rather than a bug. createAppI18n now reflects an RTL
locale onto document.dir. It only ever SETS it, never clears it: a host page that
already declares dir owns that decision, and stomping it to "ltr" for an English
map embedded in an Arabic page would break the page around us.
Verified: ?lang=ar sets dir=rtl in a real browser; ?lang=zz correctly falls
through to English. detectLocale is exercised against its SHIPPED source across
7 cases including pt-BR -> pt.
★ One thing this tick taught me the hard way: rebuilding a bundle WITHOUT
regenerating the tree leaves the leaf page carrying the OLD SRI hash, so the
browser refuses the script and NOTHING MOUNTS — silently. I spent three probes
reading an empty page before checking the integrity attribute.
The bug this gates is unreachability, not incorrectness. The packet shipped 11
complete translations while detectLocale() read only <html lang>, which every
generated leaf page hardcodes to "en" — so ten of them could not be reached by
any reader on the static tree. Nothing was wrong; nothing was visible.
Two assertions, because they fail separately:
RESOLUTION ?lang= / ?locale= selects a supported locale, an unsupported
value falls back to English, a region tag (pt-BR) resolves to its
base, and with no param the host page still wins.
REACHABILITY the packet's language list and SUPPORTED_LOCALES cannot drift
apart, and the leaf pages still declare a lang — so if someone
later drops that attribute, the host-page path does not silently
become unavailable while the tests stay green.
★ It evaluates the SHIPPED detectLocale rather than reimplementing it. A gate
that reimplements the thing it tests only ever proves the copy agrees with
itself — which is how a fix can be reverted in the source while the gate keeps
passing.
Confirmed to fail before acceptance: removing the URL branch reports all four
param cases resolving to "en".
Six conflicts, all in generated artifacts under public/, including this
bundle's own — another builder's tree regeneration had rebuilt it.
Regenerated rather than hand-merged: the three PosterWall consumers
rebuilt, then the tree and the hashes.
PosterWall.vue itself auto-merged with both sides intact — this
branch's embedded/sectionHeads props and main's usePosterPrints
refactor — and the four suites that exercise it through the regions
poster dropdown were re-run green after the merge, since their change
lands inside the component this feature renders.
The packet is the product's core artefact, and it moved into the shared
composable this tick. The gate now drives printPacket and asserts what came out:
4 sheets for 2 groups (map one-pager + one people page + prayer points + prayer
wheel), 12 wheel segments, Mark 5:1-20 present. Confirmed to fail before
acceptance — removing the wheel page reports 3 sheets and 0 segments.
⚠ THE HARNESS HAD TO RESOLVE THE RENDERERS, and the first run exposed why. The
composable imports them by RELATIVE path; under a blob URL there is no directory
to resolve '../packet/...' against, so the action failed with "Failed to resolve
module specifier" and printed nothing — reported as 0 sheets, which reads like a
product bug. It is not: Vite resolves them and the built bundle inlines both
renderers (verified in the shipped my-upg-100-list.js). The harness now
substitutes blob URLs for packet-render, card-render, qr and the strings catalog,
so the gate exercises the REAL renderers rather than a stub.
Continues the consolidation. The card deck moved to usePosterPrints last tick;
the PACKET export in upg-100-list.vue was still a second hand-maintained copy of
the same shape — its own renderer import, its own strings fetch, its own
language resolution. Two copies of one fact drift.
printPacket now lives in the composable, so the browser export and the
build-time generator share one renderer AND one calling convention. What stays
in the bundle is what genuinely cannot be shared:
packetRecords who is in this list — the people-groups API, the selector
evaluator, and the pg_slugs special case that 12 of the 27
lists need (the evaluator intentionally THROWS on that type)
packetMapSvg where this bundle's posters are served from. Absent is fine:
the packet prints a cover without the map rather than failing
⚠ AND THE MIGRATION WOULD HAVE MADE THE PACKET PRINT SILENTLY. The composable
reports through its own exportState; this profile has no HUD for that and shows
`status` instead — exportState appears ZERO times in the file. Without a bridge
there would be no "building…", and a failure would vanish with nothing said: the
mechanism working while the feedback does not, which is the exact trap the wall
consolidation hit from the other direction. A watcher now maps the composable's
phases onto `status`.
Verified: check:poster-prints drives the shared action and asserts the packet's
SHAPE — 4 sheets for 2 groups (map + people page + points + wheel), 12 wheel
segments, Mark 5:1-20 present — not merely that bytes were produced. Confirmed
to fail before acceptance by removing the wheel page (3 sheets, 0 segments). The
build-time generator still passes check:packet-render at 8/8, and
check:deck-click still prints 116 cards through the real button.
The packet is the product's core artefact, and it moved into the shared
composable this tick. The gate now drives printPacket and asserts what came out:
4 sheets for 2 groups (map one-pager + one people page + prayer points + prayer
wheel), 12 wheel segments, Mark 5:1-20 present. Confirmed to fail before
acceptance — removing the wheel page reports 3 sheets and 0 segments.
⚠ THE HARNESS HAD TO RESOLVE THE RENDERERS, and the first run exposed why. The
composable imports them by RELATIVE path; under a blob URL there is no directory
to resolve '../packet/...' against, so the action failed with "Failed to resolve
module specifier" and printed nothing — reported as 0 sheets, which reads like a
product bug. It is not: Vite resolves them and the built bundle inlines both
renderers (verified in the shipped my-upg-100-list.js). The harness now
substitutes blob URLs for packet-render, card-render, qr and the strings catalog,
so the gate exercises the REAL renderers rather than a stub.
Measured first, as the Oswald lesson requires, and this one was real:
their UupgsList root carries worldmap.svg inline (UupgsList.vue:451),
and it genuinely renders on their live country page — HTTP 200, 62,740
bytes over the wire, computed background-size 60%, position 50% 0%,
repeat no-repeat, min-height 400px. Confirmed by screenshotting the
ELEMENT, not the viewport: the watermark sits below the fold, so a page
screenshot shows nothing at all.
public/assets/images/worldmap.svg is their file byte-for-byte — 311KB on
disk, small against this tree's existing assets, which run 644KB to
42MB. The inline background-image takes its base from a new imagesBase
prop, since a dropped bundle cannot assume their absolute /assets path.
generate-tree's staged allowlist needed 'images' added — the same trap
that silently broke the vendored fonts. A dir absent from the allowlist
404s, and an unfetched background-image is quieter than the font
failure was: nothing paints and nothing reports. So the probe asserts
the REQUEST returned 200 rather than that the CSS names the file.
Then, reading the screenshot after every number was already green, two
more defects:
- A CLASS WITHOUT ITS RULE. The search label uses their class name,
sr-only — the right instinct — but their rule
(utilities/_visibility.scss:2) was never ported, so the class matched
nothing and the label rendered as a visible line of text above the
input, duplicating its own placeholder. Borrowing a class name only
works if its declarations travel with it, and a class matching no rule
is invisible to a property diff. Now 1x1px and clipped, still in the
DOM and still announced.
- A PIXEL CAP WHERE THEIRS IS RELATIVE. .search-box was capped at a
hardcoded max-width:480px; theirs composes .max-width-md, which is
max-width:50ch (utilities/_sizes.scss:11). Wrong value and wrong unit —
ch tracks the input's font, px does not — and ours truncated the
placeholder mid-word. 50ch now resolves to exactly 600px, their
measured width.
Regression: 18 suites green, including a new _probe-watermark.mjs at
15/15.
Every other packet/card gate drives the RENDERERS. This one drives the UI: it
finds the Card Deck button in the shadow root, clicks it, and captures the
document that reaches the print frame. That is the only path proving the wiring
— records provider, stock picker, language, shared composable, print frame — is
connected end to end. A renderer can be perfect while the button calling it
passes the wrong stock, or nothing at all.
⚠ It intercepts the printed HTML rather than reading the iframe back.
openPrintFrame calls window.print(), which BLOCKS in headless Chromium, and the
frame self-removes on afterprint — reading it back measures a frame that has
already gone and reports 0 cards for a correct deck.
Measured through the real UI: 116 cards at 2.5in 3.5in with 116 QR codes, and
3.5in 2.5in when the stock picker is switched to landscape. Confirmed to fail on
a wrong expected page size.
Also updates the share policy: country-posters moves from 'own-button' to
'header'. Its two-tab MFE now owns sharing, exactly as my-upg-100-list's does —
the gate caught the bundle briefly carrying two affordances after the MFE
landed, which is the regression it was written for.
Generated output only — built bundles, SRI hashes, leaf pages and the manifest —
conflicted because another agent's work landed in the same files. Resolving
those by hand yields a tree matching neither agent's source, so this takes
main's versions and regenerates. Re-verified rather than assumed: check:deck-click
and check:mfe-two-tab both pass afterwards.
Third instance of one pattern in this flight, after check-zone-census and
check-share-policy: a new internal/ gate authored in one worktree hardcodes
THAT worktree (/home/.../wt-packet) as its REPO fallback, and
check:no-abs-paths -- which runs inside build:unlocked -- turns red for
every other builder on the next merge.
Same minimal fix each time: DOXA_REPO still wins and is untouched; only the
default becomes portable, via the idiom check-no-absolute-paths.mjs prints
in its own failure text. Verified both directions: check:no-abs-paths 0, and
check:poster-prints itself 0 on the derived default.
Three occurrences is a pattern, not bad luck. Whoever is generating these
gates should take the portable default as the template, or the next one
lands red too.
Finishes the half this card left open last tick. The shared print composable
existed but the 903-line 100-list wall did not use it, so the card deck lived in
TWO places — I wrote both this flight, and they could already drift.
Two duplicates removed:
printCardDeck now delegates to usePosterPrints, sharing THIS wall's
exportState so the HUD keeps rendering one surface. The bundle
still owns what cannot be shared — fetchListRecords knows the
people-groups API, the selector evaluator and the pg_slugs
special case — and hands it over as the `records` provider. The
preview-only "no API configured" hint is preserved, because the
shared action cannot know about it.
openPrintWindow was a local reimplementation of printInPlace.js — same
off-screen iframe, same doc.write, same autoprint script,
differing only in an unused `title` argument. A FOURTH copy of
one mechanism is how a fix to the blank-first-page behaviour
reaches one caller and not the others. Now a one-line adapter.
★ AND check:share-policy CAUGHT A REGRESSION I HAD CAUSED. Adding the
country-posters two-tab MFE last tick gave that bundle a second ShareButton —
the BUG-UPG100-DOUBLE-SHARE state the 100-list already fixed once. Fixed the
same way the 100-list fixed it: the MAP's button is REMOVED and the MFE header
owns sharing. Removed, not flag-gated — a `v-if="false"` button is dead code
that still reads as an affordance to every reader and every counter, which is
exactly why the gate counts tags rather than renders.
Verified by clicking the REAL button in a browser, not by driving the renderer:
116 cards at 2.5in 3.5in with 116 QR codes, and 3.5in 2.5in when the stock
picker is switched to landscape — so records provider, picker, language, shared
composable and print frame are connected end to end. check:mfe-two-tab still
reports exactly one share affordance on the country-posters MFE.
Every other packet/card gate drives the RENDERERS. This one drives the UI: it
finds the Card Deck button in the shadow root, clicks it, and captures the
document that reaches the print frame. That is the only path proving the wiring
— records provider, stock picker, language, shared composable, print frame — is
connected end to end. A renderer can be perfect while the button calling it
passes the wrong stock, or nothing at all.
⚠ It intercepts the printed HTML rather than reading the iframe back.
openPrintFrame calls window.print(), which BLOCKS in headless Chromium, and the
frame self-removes on afterprint — reading it back measures a frame that has
already gone and reports 0 cards for a correct deck.
Measured through the real UI: 116 cards at 2.5in 3.5in with 116 QR codes, and
3.5in 2.5in when the stock picker is switched to landscape. Confirmed to fail on
a wrong expected page size.
Also updates the share policy: country-posters moves from 'own-button' to
'header'. Its two-tab MFE now owns sharing, exactly as my-upg-100-list's does —
the gate caught the bundle briefly carrying two affordances after the MFE
landed, which is the regression it was written for.
main added country-posters-mfe (isMap=true). It inherits the engine swap
correctly -- 14 -> 4 requests to api.mapbox.com, maplibregl loaded -- but
threw
Fetch API cannot load mapbox://mapbox.country-boundaries-v1.
URL scheme "mapbox" is not supported
A new map arriving is exactly what the rollout has to cover, so this was
worth chasing rather than noting.
TWO shared library sites, not one, which only measuring found. Fixing
useCountryOutline.js (consumers: doxa-simple-map, countries-map,
research-map) cleaned two of the four leaves and left countries-map -- and
the new element nesting it -- still throwing, from a SECOND site:
useMapLayers.js addRegionsLayer. Both now branch per engine, using the
keyless assets/boundaries/world-adm0.geojson the tree already ships, with
iso3 normalised once to iso_3166_1_alpha_3 so every downstream expression
keeps reading the name it always read, and 'source-layer' set for the
vector-tile path alone.
The engine is read off the LIVE map -- its container carries
maplibregl-map -- rather than from config, because both composables are
called with nothing but a map getter.
MEASURED: 0 mapbox:// errors across country-posters-mfe, countries-map,
doxa-simple-map, research-map and demo-map; the adm0 fetch confirmed firing;
provider sweep 15/17. The Mapbox path is unchanged -- its control screenshot
still shows the coloured WAGF region fills.
Also fixes check:descriptions, which the new profile red-gated by shipping
an empty description. Written from the component's own header, not a
placeholder: 34/34 profiles described.
NOT FIXED, and recorded in the ledger rather than glossed: under MapLibre
countries-map still paints NO coloured region fills. The error is gone and
the geojson loads, but the fill does not appear. That is a separate defect
from the source failing, and claiming this commit finishes it would be
false.
The Experimental folder now presents most-impactful first per the
finalized roundup ranking (EXPERIMENTAL-MAPS-ROUNDUP.md): Prayer Walk
Planner, the rescued legacy set (entry: send-map), All People Groups,
Country Posters, Vision Card -- rank is editorial curation and lives
beside TAB_DEF; unranked bundles still discover and sort A-Z after.
Verified in the built shell rail.
Exclusions tightened: demo-map + golden-path-check (scaffolding /
authoring tooling -- the studio keeps them), doxa-research-map (the
part already composed into Simple Research), stack-demo (broken and
deferred; a broken tab on the giveaway shelf reads as damage).
Wrapper description corrected: the legacy IIFE holds FIVE maps.
Gates usePosterPrints.js — the print actions now shared by every poster wall —
by driving them in a browser with a fake records provider, so the assertion is
about the DOCUMENTS rather than any bundle's API being reachable.
★ BOTH DIRECTIONS, because "the button is there" and "the button works" are
different claims and this flight has repeatedly found the first true while the
second was false. With a provider: a real table, correct row count, the prayer
checkbox column present. WITHOUT one: phase "error" naming the missing provider,
not a silent no-op and not a throw.
⚠ IT CAPTURES THE PRINTED HTML INSTEAD OF READING THE IFRAME BACK, and finding
that out cost a wrong first result. openPrintFrame calls window.print() under
autoprint, which BLOCKS in headless Chromium, and the frame self-removes on
afterprint — so reading it back measured a frame that had already gone and
reported 0 rows for a perfectly correct document. Intercepting what the
composable hands to the frame measures what actually gets printed.
Confirmed to fail before being accepted: deleting the checkbox column from the
row template reports "the list print has no prayer checkbox column".
The rebase conflicted only in GENERATED output — the built bundles, their SRI
hashes, the leaf pages and the manifest — because another agent's regions work
landed in the same files. My source file applied cleanly.
Resolving generated files by hand produces a tree matching neither agent's
source, so this takes main's versions and REGENERATES: four bundle builds plus
`npm run tree`. Re-verified rather than assumed — check:poster-prints and
check:mfe-two-tab both pass after the regeneration.
The poster wall was generalised into library/components/poster/PosterWall.vue
and two bundles migrated onto it — but the ORIGINAL, my-upg-100-list's 903-line
wall, was left on its own copy. So the one wall carrying the real print actions
is the one wall nothing else benefits from, and the shared component's packet
path was still a "coming soon" stub. Every print fix made in the 100-list this
flight (the card deck, the stock picker, the image_url fetch) was invisible to
the three bundles on the shared component, and vice versa.
★ THE SEAM IS WHAT MADE THIS SHAREABLE AT ALL. The PRINT ACTIONS are common; the
RECORD FETCHING is not. fetchListRecords in the 100-list knows the people-groups
API, the selector evaluator and the pg_slugs special case; Key Cities and Country
Posters have entirely different data shapes. usePosterPrints.js therefore takes a
`records(item)` PROVIDER and owns everything after it — the HUD state machine,
the in-place print frame, and the documents.
That also keeps the bundles honest. A wall that CAN supply rows passes the prop
and gets the list + card-deck prints; one that cannot omits it, the Card Deck
button is not rendered, and Prayer List keeps its truthful "coming soon" instead
of a button that fails at click time. Country Posters shows exactly that today:
175 cards, Prayer List and Poster Map, no Card Deck — because it has no provider
yet, not because something broke.
⚠ ONE exportState, not two. The composable owns a ref by default, and a wall
rendering its OWN state while these actions wrote to a private one would show no
progress and swallow every error — the mechanism working and the feedback not,
which is the failure mode this flight keeps finding. PosterWall hands the
composable its own ref so both write to the surface the reader is looking at.
Prints IN PLACE via the shared openPrintFrame, never window.open() — the
mechanism the Driver asked for by name: "not supposed to open up a new browser,
but should open up the print dialog directly in the browser".
NOT DONE, and deliberately: the 100-list wall is NOT yet migrated onto the shared
component. Its template is 107 lines against the shared 72 with a different
structure, and it is a Driver-reviewed surface — that is a rewrite whose risk
does not fit in one tick alongside this. The shared half lands first and is
verified; the migration stays open on the card with this composable now waiting
for it.
Verified: all three bundles on the shared wall build; country-poster-wall renders
175 cards with zero page errors; check:poster-prints drives the composable in a
browser and asserts BOTH directions.
The one conflict was in public/doxa-maps/index.html, which generate-tree
writes. Built files are regenerated, not hand-merged: country-posters
was rebuilt because main changed its source (a new two-tab MFE),
regions-dashboard because this branch did, then the tree and the hashes.
Two findings, both from measuring at locale=ru instead of trusting the
plan.
DO NOT VENDOR OSWALD. The work list called for it for Russian headings.
Their rule is [lang="ru-RU"] on h1/h2/h3 (_globals.scss:40) — but the
live document's lang is "ru", so the selector never matches. It is dead
code on their own site. Oswald never loads there either:
document.fonts reports it "loading"/"unloaded" while Bebas, Poppins and
Brandon all report "loaded", and the glyph oracle agrees — Oswald
renders "HAMBURGEFONS" at 855 against sans-serif's 856.36 (0.2%, noise),
where Bebas moves it to 486 (43.2%). Bebas itself has no Cyrillic (501
vs 501.42), so their Russian headings fall back to the system face at
letter-spacing normal. Vendoring Oswald would have made us DIFFER.
_probe-ru-parity.mjs now asserts that parity instead of assuming it:
10/10, both sides falling back identically at 444.88.
The list tab was half-localised, and three of the four defects were
invisible in English:
- .rl-title forced text-transform:uppercase. Their h1 is none on both
locales. Invisible in English because Bebas is an all-caps face, and
wrong in Russian, where theirs reads "Регионы" and ours read
"РЕГИОНЫ". This file's own comment already recorded the correct
finding for .region-card__title; this rule was missed.
- summaryLine() was a hardcoded English template where theirs is
t('%count% people groups in %countries% countries'). The Russian
string was already in our catalogue — copied, never wired.
- dashboardLink() likewise: their ru value is "Открыть панель: %region%",
a different word order no English template could be patched into.
- Nothing refetched on a locale change. The region and country labels
come from the API's ?lang=, and their site serves Азия/Африка/
Азербайджан on /ru/regions. region-dashboards.vue already had
watch(languageCode, loadRegions); this tab did not, so a locale switch
translated the chrome and left every label English.
Why none of it was caught: _probe-region-vs-live.mjs does compare
textTransform and reports 73/73 — but it diffs the DASHBOARD tab's
.rd-title. Nothing ever property-diffed the LIST tab's head against
/regions. The gap was coverage, not the property list.
_probe-list-vs-live.mjs closes it at 8/8.
The loading state now uses their own 'Loading results...' key rather
than new English, since that string is already translated in all eight
locales.
Regression: 18 suites green.
Gates BUG-UPG100-IGNORES-THE-PROVIDER-SWITCH. my-upg-100-list builds its map RAW
with `new gl.Map` instead of going through useMapInstance, so the page-level
switch — which resolves inside that composable — could not reach it. The card
measured 19 requests to api.mapbox.com at ?provider=maplibre while five sibling
leaves swapped cleanly.
★ IT COUNTS NETWORK REQUESTS BY HOST, not globals. `window.mapboxgl` can be
present because the leaf page preloads it while the map is actually DRAWN by
maplibre, and the reverse is equally possible. What the switch exists for is not
loading a proprietary engine and not calling its servers, and only the request
log shows that.
The allowance is DERIVED from the leaf's own HTML — count the distinct
api.mapbox.com urls the generated page declares, plus one for a stylesheet that
gets fetched twice — rather than hardcoded. Asserting merely "fewer than the
default" would pass a half-fix at 18 of 19; hardcoding 5 would pass a leaf that
preloads 4 and leaks 1 runtime call.
★ THE GATE'S OWN FIRST VERSION MEASURED NOTHING AND PASSED. `const URL =
process.argv[2]` shadowed the global URL constructor, so every
`new URL(request.url())` threw, the host tally stayed empty, and it cheerfully
reported "0 mapbox requests" on a page making 19. It now names the variable LEAF,
counts what it could not parse, and FAILS if the default run records no requests
at all — because a silently empty tally is indistinguishable from success.
Passing on three leaves today: upg-100-list 19 -> 5, doxa-simple-map 21 -> 4,
countries-map 24 -> 4, with every residual being the page's own <link>/<script>
preloads. Confirmed to fail when the allowance is tightened by one.