The build writes a locale's sha384 only for a file it actually emitted, so
the pin's presence is the one reliable statement that there is something to
fetch. loadLocale keyed on messages[c] instead, which is not the same
question: a bundle that includes no areas inlines nothing, so messages is
empty with the flag OFF too and the preload requested locales/<lc>.json on
every non-English page — a 404 per reader, fetched with no integrity
because there was no hash to pass.
Keying on the pin makes the flag-off build inert and the flag-on build fail
closed to English. The integrity argument is now unconditional: reaching the
fetch at all means a hash exists.
Found by studio in review. Verified both ways: flag off is byte-identical
at 284.61 kB gzip with no locale pin map in the output at all, flag on is
unchanged at 165.23 kB with all ten hashes present.
Every leaf inlines all eleven catalogues, so a reader who opens the map in
one language downloads the other ten. Measured on doxa-simple-map: the ten
unused locales are 113 KB gzipped, 40% of what a reader actually fetches.
With the flag on, the build inlines en — it is the fallbackLocale, so it
has to stay — and emits the other ten as <bundle>/locales/<lc>.json holding
exactly the areas that bundle uses. Same areasForBundle() answer the area
narrowing already uses, so the two dimensions cannot drift apart, and
emitted from the same plugin so composite-freshness, fix:sri and the
.gated/ split cover them with no new step. Off by default: the build is
byte-identical until someone sets it.
Not Vite's dynamic import. That was tried in April and GREW the bundle
~22 KB, because IIFE output needs inlineDynamicImports, which pulls every
chunk back in and adds a wrapper closure each. This is a plain fetch.
The base URL is captured as the script evaluates. Rollup shims
import.meta.url to document.currentScript, which is null once the script
has run, so a base read at mount would resolve against document.baseURI —
right on our own pages, wrong on every host that serves the bundle from
another path. deriveTreeBase() carries that same warning but returns the
tree root; these assets sit one level deeper, in the bundle's own
directory.
Each emitted file's sha384 is baked into the bundle and passed to
fetch({ integrity }), hashed from the same bytes the emit writes, so a
swapped catalogue fails closed to English instead of injecting text.
The fetch starts at module evaluation from the detected locale, so it
overlaps the bundle's own startup, and localeChromeReady() lets an RTL
locale hold its text chrome for up to 1.5s. For ar the flash is a layout
flip, not a text swap, which is worth a short hold; the map never waits,
and the timeout means a failed fetch degrades to English rather than to a
blank panel. loadLocale() runs after the instance exists, never inside
createI18n, because setLocaleMessage touches no effect scope while
createGlobal is the call that throws inside a stopped one. It also installs
into each instance that asks, since vue-i18n copies messages at
construction and a catalogue arriving later would otherwise reach only the
instances built after it.
Measured, doxa-simple-map: 284.6 KB gzip -> 165.2 KB, with ar as a 13 KB
asset. Ten assets emitted, every hash matching, ar no longer inlined, en
inlined.
The treeshake locale arm reads the narrowed build correctly (reports "10
asset") and fails when an asset is deleted. Its length floor is now per
script: a 25-character Chinese string is a whole sentence, so a
Latin-calibrated floor found no zh in a composite carrying short labels and
called it absent.
An English _note key sat in gospel.json in all eleven locales, so every
Arabic reader downloaded an English paragraph about how the file is
organised. It also fooled a gate: because it was text present in ar and
absent from en, it satisfied a probe asking "does this bundle contain
Arabic translations?" for a bundle with every Arabic character stripped
out. A value no reader is meant to see is a value no check should be able
to count.
The reasoning it carried is real and is kept — which pools were searched,
why a mistranslated caveat there would read as authoritative, why Open
Bible Stories is not Scripture — moved to a README beside the catalogues
along with the rule that nothing in those files is a comment.
Adding that README turned check:i18n-parity red, which is the second half
of this commit: it enumerated locales with existsSync, true for a file as
well as a directory, so the README read as a locale missing all 1,685
keys. Documentation belongs next to what it documents; the filter was
wrong. Locales are directories now.
2.7 KB out of every bundle, and gospel.prose.english — the one translated
line that says the prose stays English — is untouched.
The area arm asks whether every area a bundle uses is present. It names
locales nowhere, so it is equally green whether a bundle carries eleven
locales or one. That is fine while the glob inlines them all, and stops
being fine the moment the build narrows by locale: a per-locale file that
failed to emit would fall back to English silently, with no error and no
gate. Written now, while the answer is still "all inlined", so it is
already guarding when the build changes underneath it.
A locale counts as reachable if it is inlined in the IIFE or emitted as
<bundle>/locales/<lc>.json beside it, which is the shape the design
proposes. The list comes from SUPPORTED_LOCALES, not from the folders on
disk: the folders are what happens to exist, while SUPPORTED_LOCALES is
what the runtime will actually try to resolve, and a locale added to the
list without a catalogue is precisely the failure to catch.
Three things it had to stop reporting, each a correct absence:
Bundles that include no areas carry no catalogue at all. Composite hosts
mount leaf bundles at runtime and render no catalogue text of their own —
stack-demo makes zero translator calls in its own source, and failing the
host would send someone looking for a bug in the wrong file. And the probe
was asking bundles for text from areas the build deliberately left out,
which reported five bundles as broken when none of them were.
The probe requires a locale's own script for ar, ru, hi and zh. "Not in
en" is not the same as "is a translation": the ar catalogue holds an
English developer note that en does not, and that one string was enough to
report ar as present in a bundle with every Arabic character stripped —
the check voting on a string no reader will ever see.
Proven both ways on real output. Stripping ar from a built bundle fails
naming ar; restoring passes. Simulating the design's end state — ar not
inlined, ar.json emitted — passes with the asset present and fails when it
is deleted.
gospel's resources-list maps the resource titles that arrive in the data to
the i18n keys that render them:
const RES_KEY = { 'Bible video': 'gospel.res.bibleVideo', ... }
title: RES_KEY[r.title] ? tr(RES_KEY[r.title]) : r.title
The English on the left is matched against a value from the data; what the
reader sees is the key on the right. Wrapping the left side in t() would
make every lookup miss and fall through to the raw data value, so the count
would drop while the display broke — the worst shape a gate finding can
have. Found by gospel, who triaged it rather than acting on it.
Exempt only when the mapped key really resolves in every locale, the same
way a fallback has to earn its exemption. A table pointing at a key nobody
wrote still counts: the lookup succeeds and hands t() a key the catalogue
has never heard of. A value that is a plain category rather than a key
counts too — the gate can only vouch for what it can resolve.
Self-test covers all three arms, using the real RES_KEY case. Poisoning the
exemption to skip the resolution check trips it.
gospel 153 -> 146; fleet 159 -> 152.
poster-wall translates its section headings by looking the key up by the
English: one table maps a zone id to a heading, a second maps that heading
to a catalogue key, and the helper returns the raw English when the second
table has no entry. Two headings reachable from four zone ids had no key, so
they printed in English in all eleven locales. upg100 found and fixed that
one; this is so the next table cannot repeat it.
The hardcoded-English scan cannot see this class. There the English is a
lookup index rather than a rendered label, and the key is computed, so a
scan that reads keys written literally at the call site has nothing to
check. Resolving arbitrary key expressions is a static-analysis problem;
this does the decidable part — two object literals in one file where the
values of the first are the keys of the second, and coverage is a set
difference. The table list is declared, not discovered, because a heuristic
hunting for related-looking objects would be guessing about code it has
never read.
Two things the check has to get right and could quietly get wrong. It
compares the VALUES of the producing table, not its keys — the ids are
never translated, and reading them finds nothing while looking like a
check. And a label passed straight to the helper as a literal counts as
produced: three of the nine headings never appear in the table at all, and
"Other" is exactly the catch-all that gets added last and keyed never.
A renamed table fails loudly rather than comparing two empty sets and
calling it agreement. The self-test covers a covered table, a gap, a gap
reachable only through a literal argument, and the renamed case; poisoning
either parser or the object matcher trips it.
Verified against both real states: development's poster-wall fails naming
Americas and Europe, and upg100's fix passes.
Eighteen keys across eleven locales for text that was written in English in
the components: the rail and filter toggles, the selection count, the CSV
export toast, the drawer's prayer and information links, the dashboard
title, the map's failure message, and the four search placeholders that name
the tiers of whichever tab is open.
The export toast is interpolated rather than concatenated. Its counts keep
going through fmt(), which groups digits the way each locale does, and only
the words around them come from the catalogue — the order of "rows",
"fields" and the outcome is not the same in every language, so a sentence
built by joining fragments cannot be translated at all. It uses the shared
fillParams, which leaves a placeholder visible when a value is missing
instead of rendering the word "null".
The map's failure message no longer names the token and the container: that
is for the console line, which stays English for whoever is debugging. What
the reader sees says what to do.
Three more shapes the scan could not see, each of them correct code: tStatic
called directly rather than through a wrapper; `config || t(key) || English`,
where the translator is not the immediate left operand because || is
left-associative; and the licence credits, media queries and product name
that must not be translated at all.
Own leaks 91 -> 1, the last being a tab label behind a key that resolves in
all eleven locales. Fleet 249 -> 159. check:i18n-parity,
never-translated, treeshake and stopped-scope all pass.
The legend emits 'packet:full' or 'packet:lightweight' — it widened its
existing event rather than adding a second one — and this router still
tested the whole string for 'packet'. Neither submenu item matched any
branch, so the Packet export opened nothing, logged nothing, and looked
like a menu item nobody had wired. Found by upg100, who had already hit
the same thing in its own copy.
The kind is split before it is compared, the way upg-100-list does it, and
a bare 'packet' still means the full packet so an older emitter keeps
working. The variant then has to survive the call as well: openPacket
ignored it and printPacket defaults to full, so lightweight would have
printed the full packet even once the menu fired.
The gate asserts the seam rather than one file — it reads the kinds the
legend actually emits and requires every consumer to split before
comparing, because this bug cannot be seen by reading either side alone.
It currently fails on country-posters/countries-map.vue, which has the
identical dead menu and is not mine to change.
Two fixes to the check itself while writing it: a fixed-size window read
only part of a long router and reported correct code as broken, so the
function's extent is found by brace depth; and a trailing \b after a
quoted literal never matches, which made the assertion fire on every
consumer including the working ones.
tStatic is the library's translator for code outside a component's setup, and
it was missing from the set of names the scan knows. Anywhere it was called
directly rather than through a wrapper, correct guarded code read as a leak.
The two-sided rule still applies to it: a tStatic fallback is exempt only
when its key resolves, and the self-test asserts both halves.
Three allow shapes, each for a string that must not be translated:
A matchMedia query. Rendering "(prefers-reduced-motion: reduce)" in another
language matches nothing, so motion safety would switch off for exactly the
readers who asked for it, and nobody would see an error.
Tile licence attribution. The credit a data licence asks for is the one
string that stays as the licensor wrote it in every language. These render
only when the live sources cannot be read, which is the case a licence never
allows to come out empty.
The product name, and bare CSS position keywords.
Each shape is anchored rather than fuzzy: prose that resembles them —
"Start end of the list", "center the map on your region", "Doxa Maps is a
prayer tool for the unreached", a copyright line with a real sentence in it —
is still reported. 190 -> 178.
The legend gives each tab a small "i" button explaining what that tier
means — what an Affinity Block is, how a people group differs from a people
group in a country. SemanticTreeLegend already read that text from
legend.tabInfo.<id> behind a te() guard, and not one of the ten keys the
research map needs had ever been written. The tab chips were translated in
all eleven locales and the explanations beside them were not, so the reader
least able to guess what "PGIC" means was the one reader who got it in
English.
The keys are written for all eleven locales, following the terminology each
catalogue already uses for these same tiers — People Group as Volksgruppe,
Peuple, Pueblo, 民族, Народ, जनसमूह — with WAGF and UPG left untransliterated
as the corpus leaves them. The reader-facing text drops the source field
names and row counts that the English carried; those are notes for whoever
maintains the tables, so they stay in the source as a comment.
With the keys in place the English in the tab tables can never render, and
a second copy of a string drifts from the catalogue without anyone noticing,
so it is removed. `info: true` stays because the legend gates the "i" button
on that field being set — dropping it would have removed the affordance
instead of translating it — and tabInfo now treats only a string as a
fallback, so the flag cannot reach the popover as the word "true".
Verified every changed tab renders a real label and a real explanation in
all eleven locales. check:i18n-parity, check:i18n-never-translated and
check:i18n-treeshake pass; hardcoded-english 205 -> 190.
T('dashboard.map_loading', 'Research map loading…') is the idiom these
components use: the English is what shows if the catalogue ever loses the
key, not what the reader gets. Forty-four findings were code of exactly
that shape, and triaging them meant reading catalogue entries that were
already complete in all eleven locales.
Two things were wrong. Wrapper aliases were not recognised as translators
at all — only `const X = t` was, never `const T = (key, en) => ...` — so
every fallback in those components read as a leak. And the rule that
exempted translator arguments exempted all of them, including the fallback,
which left the gate structurally unable to see a typo'd key: the English
then renders forever while the call site still looks correct.
So the key argument stays exempt and the fallback argument has to earn it,
by resolving to a real value in every shipped locale. A missing key still
counts. An identical rendering counts only when the English has three or
more translatable words: measured against this catalogue, the identical
entries are cognates (Normal in de, Auto in fr), proper nouns, and
format-only strings like "{label} · {count} · {pct}%", and failing on those
would report correct translations as leaks.
Bound expressions are parsed on their own, so they could not see the
translator declared in the script block; the declarations are carried in as
stubs. The stubs deliberately contain no || — written the natural way they
match the te()-guard shape and exempt every fallback in the file, resolved
or not.
Self-test covers both sides: a resolved fallback is quiet in template and
script, an unresolved one counts in both, and the catalogue rule is
asserted directly. 249 -> 205 across the fleet, all 44 in this owner's
plugins; detection elsewhere is unchanged.
Triage meant grepping the reported string to find its site, and the first
textual match is very often a comment that discusses the string rather than
the code that renders it. Two findings were sited to prose that way, and a
count built on those sites was wrong.
Each finding now carries the line. <script> and <script setup> are scanned
separately rather than joined, because concatenating them made every line in
the second block wrong by the length of the first — a confident wrong site
sends the reader further off than no site at all. A bound expression is
re-parsed on its own, so its findings take the attribute's line instead of
the fragment's own numbering.
The self-test reads the reported line back out of the fixture and requires
it to contain the string, so an offset error fails rather than passing with
a plausible number.
Detection is unchanged: 249 before and after.
check-hardcoded-english judges each quasi of a template literal on its own, which
is correct -- an allow-listed fragment must not drag a whole literal in with it.
But splitting `url(${base}/images/worldmap.svg)` leaves "url(" and
"/images/worldmap.svg)", and `repeat(${n}, minmax(0, 1fr))` leaves "repeat(" and
", minmax(0, 1fr))". The whole string is obviously CSS and the existing CSS shape
matches it; no individual fragment carries a brace or a colon-value pair, so each
one fell through and was reported as user-visible English.
Colon-terminated prefixes failed the same way. The id rule accepts a trailing
hyphen, because `id="country-${code}"` leaves "country-", but a langTree id is
`fam:${x}` / `dial:${x}` and a localStorage key is `pgg-cards-per-row:${profile}`.
Those leave "dial:" and "pgg-cards-per-row:", which no shape described.
Two shapes, both narrow: a CSS function fragment, and a prefix that is one
lowercase token ending at a colon. The fleet total goes 257 to 249 -- five of
mine, three in other owners' plugins -- and every real string still flags, which
I checked in the same breath rather than afterwards: "Search people groups…",
"Show filters", "selected" and "Research map loading…" are all still caught.
Worth saying plainly because the number was about to be routed as work: these
eight were never translation defects. Wrapping "dial:" in t() would have been a
bug, and someone would have done it on the strength of a count.
rega at 390 px: the tab strip ran past the right edge with no scroll cue, and the flat legend's collapse button measured 19×15 with an English title in every locale. On a phone the tabs wrap (the brand steps aside); the button is 24 px and named by the shared aria keys.
rega measured the header at 390 px: one row, so the title wrapped to five lines and the poster-size select (x=393) and the Bless/Social toggle (x=553) ran off the edge, unreachable. On a phone the title takes one short line and the controls wrap onto rows.
The caption and its eleven locale keys reach the bundle, so this is a real
rebuild rather than a docs-only change. fix:sri reports 127 hashes already
agreed, 0 rewritten; manifest churn is this bundle and the spa manifest's own
self-reference.
The README described fit and fill-all qualitatively but never said what choosing
one costs, which is the number a reader deciding between them needs: on the
default list, fit draws 931 of 2,074 groups and reports the other 1,143, while
fill-all draws all 2,074 with none outside its band and none under a label.
It also records the evidence for WHY this is a mode rather than a setting: 931 at
separation 7, 1,369 at 5, and 1,369 again at 4. That plateau is what shows the
band's depth is binding and not the spacing, and without it the next reader will
reasonably try to tune the number that cannot help.
Every figure here was re-measured against the shipped tables through the
component's own join before being written down, because a stale number in a
README is worse than no number: it reads as fact and nobody re-checks it.
Seven comments across six files pointed at SPEC-GOSPEL-INTERNET-DASHBOARD, which
lives only in _qa-cycle: never cloned, never published. A reader of the public
source met a citation they could not follow, which is worse than no citation —
it implies the reasoning is written down somewhere they may look for.
The durable part is now plugin-designs/gospel-internet/README.md, written as
BUILT rather than as specced: the views and their shared URL state, the list
model and its fail-closed rule, the two set charts (rings, bands, the label
keep-out, fit vs fill-all and why band depth is the binding constraint), why the
charts fit their box, the data joins, the columns, and the gates that guard each
of those. Section 13's honesty rules are carried over close to verbatim, because
they are constraints on the product and not notes: pins are approximate, never
"reached", never who works where, internet figures are the country's, public data
only.
Two citations did not become README links, deliberately. The facet rail's quoted
a design INSTRUCTION ("comes from the kit; do not re-implement"), so the
instruction is kept and only the unreachable document name dropped — inventing a
README section to host it would be citing myself. board.meta.json's and the
instances file's now point at the README section that actually describes the
URL-state behaviour they depend on.
The instances file is edited through the JSON parser: my first attempt did string
surgery and put unescaped double quotes inside a string value, which JSON.parse
caught before it could ship.
STAGED, NOT FLIPPED. SET_FILL_MODE in GiCenteredSetMap is 'fit' — today's
behaviour, unchanged — with the alternative named beside it so the ruling is one
line and a rebuild. 'fit' is not a placeholder: it is the honest mode for a page
that cannot zoom, so the POSTER should keep it whatever the screen chooses.
The binding constraint is recorded there because it is not obvious from the
numbers: separation can never fall below twice the pin floor (5.2), and an
18.5-unit band therefore holds at most three rows, so no minSep reaches ring 2's
658 groups. minSep 7 places 931 of 2,074; minSep 5 places 1,369; minSep 4 places
1,369 — identical, which is what proves depth rather than separation is binding.
'fill-all' places all 2,074 with none under a label and ring 2 using 18 rows.
THE CAPTION states the TRUE total — the groups that have a ring — not the number
of pins currently drawn. In the dense mode the same groups read as an arc of
colour zoomed out and as separate pins zoomed in, so a caption counting what is
visible would change as the reader moved: true at every instant and misleading
overall. It joins the paragraph that already accounts for the centre, the
no-relation groups and the unmatched ones, so those four numbers reconcile
instead of competing.
centered.shown added in all eleven locales; parity and keys-exist pass, and
hardcoded-english did not rise (257, and that fall is studio's, not mine).
check:composite-freshness found stale on main: none. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
Bless mode is the printable poster (a verse under every group, sizes, Print HD poster) and Social mode the shared prayer wall; the texts use each locale's own button names. UPG Neighbor's entry is removed: it has no hosted "?" (its own How it works is its explainer), so the entry showed nowhere and could only drift.
It had a script but no description and sat in no gate list, so a map fetching the people-group list
around the library (upg-neighbor's photo lookup) stayed red on development until someone ran it by
hand. With that lookup allowed by name (the commit before), it passes; it goes into gates:post, where
it reads the shipped bundles too, and now carries its description.
Its header cited research notes in a folder that is not part of the repository, named an internal
directive, and quoted a reader's complaint. It keeps the reasoning and the list of what was copied.
landing-preconditions verified EVERY bundle whenever vite.doxa-maps-build-config.json changed, but two
of its keys build nothing: dropin (where sync-dropin copies to) and the "//" comment keys. Changing
dropin.dest cost a full verification, about 10 minutes, for no possible staleness. The config now counts
as a build change only when another key differs. Checked on real history both ways: the dropin.dest fix
is not a build change; the posterSources flip is.
useSelectionBus and useDashboardLayout cited sections of a design spec that lives outside the repository.
The contract as built is now library/dashboards/README.md, so both point there. Comments only.
Four places in the repo (the registry, the resolver, the packet renderer and a gate's allowance) cited a
research spec that lives outside the repository, so a reader of the code could not follow the reference.
library/lists/README.md now documents the list as built: how a list is chosen, the registry and its five
lists with their measured counts, where membership is generated, the two tiers (narrowing DOXA's rows,
failing closed; IMB rows per map), the data sources and their terms, and the questions still open. The
four citations point at it.
The deployment section named four hosting options, three of them CDN-shaped (Cloudflare Pages,
jsDelivr, Netlify). dx-maps now ships two routes, and the section says so in that order:
- The easiest path: drop-in. The folder goes into your own site's public/ and your site's server serves
every map; bun run build && bun run dropin. One server, no PocketBase. It is the route the DX Life
branches were proven on.
- Subdomain: the dx-maps container with PocketBase, for maps that need a backend of their own; the
Dockerfile and compose.yaml at the repo root.
The notes that still hold stay (relative links, iframes, the one CORS header for JS embeds). The
quick start's own drop-in steps become a pointer to the route, and the cloud dev server is described
as following development, not main.
packet.mjs looked for page 1's map sheet only in
library/scripts/posters/out/posters/, which exists only after
"bun run posters". On a fresh checkout every offline packet printed the
missing-map notice, and check:packets-all failed each fixture packet
with map=NO.
The same 28 zone poster sources are tracked in data/posters/ and ship
with the tree. The generator now looks in out/posters first (a local
run's fresh sheets win), then in data/posters. With out/posters absent,
all four fixture packets now carry their map. Messages that named the
sheet give its real path.
The drop-in was proven on a laptop with one server: the three DX Life branches rendered their maps,
7 of 7 checks passed, with internal/sync-dropin.mjs --from-config --dest <doxa-life>/public. This
makes that the default path:
- dropin.dest is ../doxa-life/public. The old ../doxa-life/public/embed/doxa-maps-build was stale.
- bun run dropin = sync-dropin --from-config; deploy:dev (build + drop-in) now uses it.
- The README gains "The easiest path": the studio for the work, dropin.dest at your site's public/,
then build and drop in.
One hazard came with the new destination, and it is closed here. --prune deletes every file in the
destination outside the drop-in's allowlist, which was right while that was a folder of its own. Into
a site's public/, it would delete the site. deploy:dev passed --prune, so it no longer does, and
sync-dropin refuses --prune into any folder named public/. Measured in a scratch site: --prune into
site/public exits 1 with the reason; a plain drop-in copies dx-maps/ and leaves the site's own
file in place.
On a narrow screen the list used to be display:none, so a walker could not see, select, rename or delete a zone there. It is now a drawer over the map from the reading-end edge (right in English, left in Arabic), behind a ☰ Zones (n) button that leads the toolbar; a tap outside, × or Escape closes it and focus returns to the button. On a wide screen it is the sidebar as before, its border now on the reading side. Two words in the planner's own 11-locale strings: zones, closeZones.
The feature-state approach left an invisible twin per DOXA group (2,616 circles drawn at alpha 0, and counted by queryRenderedFeatures). When the DOXA set arrives the base is re-sent once, slim (1.25 MB, not the old 3.7 MB), without the DOXA rows; the black pins stay on their own top layer. Popup fields and counts unchanged.
check:composite-freshness found stale on main: none. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
check:descriptions failed on development: country-map.vue had a rich prose header
and no @description TAG, which extracts to an empty string. My header explained
at length why the bundle exists and never said, in one line, what it IS — so the
plugin-studio, the gallery and the SPA's rail had nothing to show a reader.
The tag matters more than the prose: generate-tree matches
/^\s*\*\s*@([\w-]+)\s+(.+?)\s*$/gm, so a paragraph with no asterisked tag is
invisible to it. Verified the text reaches the BUILT manifest, not just the
source, since the manifest is what the gate and every consumer read.
37 of 37 profiles now described. country-map rebuilt, tree and SRI refreshed.
One sha instead of two, per the shepherd: both branches fixed the SAME
regions-list rule, because a gate is useless on a tree where its poison case is
still live. Landing them separately would have applied that fix twice.
★ I HAD CLAIMED THE TWO COPIES WERE IDENTICAL. They were not — I checked before
folding rather than after, and the hashes differed. The CODE line is the same in
both (`cyrillicHeading('.rl-title')`, one occurrence each); only the explanatory
comment differed. I kept the css-dangling wording because it names the arm that
now catches the bug, which is what the next reader needs. The emitted CSS is
unchanged either way — comments are not CSS — and the byte-compare baseline still
passes, which is the assertion that would have caught me if I were wrong.
WHAT THIS SHA CARRIES, both halves verified after combining rather than assumed:
THE MIGRATION (was 4a4bc96fc). The harness stub's `.value` is now the empty
string, so an injected-config read and a locale interpolation both behave as
they do in the browser. Measured components 2 -> 4, no NOT MEASURED left.
region-dashboards is byte-identical (a pure de-duplication); regions-list
changed because its old bytes were the bug, so its baseline was re-taken.
strongMedium() deliberately NOT adopted — the helper emits bare `strong{...}`
where region-dashboards scopes `.rd-root strong{...}`, and swapping would widen
the rule.
THE GATE (was 023ca2d08). check:shadow-css-parses now fails on a selector that
never receives a block, gated on `endsLiteral` — the LAST fragment — rather than
on `complete`, which is false for nearly every sheet here and therefore
suppressed the very bug the arm was written for. Four self-tests live in the
file: the exact regions-list string is caught, a last rule WITH a block is not,
a trailing comment is not, whitespace is not.
Re-verified on the combined tree: ported-globals 4/4 byte-identical, shadow-css
80/80, the poison case still produces exactly 1 dangling hit when reintroduced,
gates:pre 56/56, and the broken form is absent from the shipped minified bundle.
Railway, DigitalOcean and `docker build` use a root Dockerfile with no setting, and it is
Coolify's and Dokploy's default path. The file moves; nothing is copied.
- VOLUME /data is gone. Railway rejects the line, and a named volume is needed anyway
(VOLUME alone gives each new container a fresh anonymous one). The platforms mount /data.
- The baked PocketBase gate's comment named the old path. It now says the repo-root
Dockerfile, and the gate's SHA-256 pin moves with it (2987055b... -> 2eceef03...). The
hook's code is unchanged: the exact RUN passes `sha256sum -c`, and the old block still
passes its old pin.
- .dockerignore excludes everything: the Dockerfile copies nothing from the build context,
so no platform sends the whole repository to the builder.
- compose.yaml: the same image with its /data volume and memory limit; settings from .env.
- deployment/cloud/README.md gains "Where it runs": Coolify, Dokploy, Railway, a server with
Docker, Kubernetes, and why not DigitalOcean App Platform (no persistent volumes).
DEPLOY.md and two comments follow the move.
gates:post had gone red on development in three places that no landing runs, because landings run
gates:pre. The library index and the capability-exemplars table are derived documents: regenerated,
they now list the country-map bundle, BackToTopButton and useHostedHelp. check:no-free-identifiers
flagged the Wall's new Path2D (a canvas built-in every browser has) as never bound; it knows the
canvas built-ins now. No code changes.
Both were registered but in no gate list, because each was red on development: wiring either in
would have failed every landing. They are clean now (the plugin-studio widget release fixed the
first, the country map's legend rows the second), so they go into gates:pre, where a regression
fails the landing instead of waiting for someone to run them by hand.
upg100's harness recorded my two files as NOT MEASURED because its stubs threw on
them. Both were FIDELITY gaps — the stub modelled states that cannot occur — so
they are fixed by making the stub more like the browser, never by relaxing a
comparison:
· `.value` was undefined, so a component reading an INJECTED ref threw
"Cannot read properties of undefined" (region-dashboards.vue:199,
cfg.value.regionSlug — ProfileLoader always provides that object).
· the stub threw on ANY string coercion, because a Proxy over a function has no
primitive conversion. That alone produced regions-list's "Cannot convert
object to primitive value".
`.value` is now the EMPTY STRING, which is the one shape that serves both real
cases honestly: a locale ref's .value IS a string, so `${locale.value || 'en'}`
correctly yields 'en'; and reading a property off '' returns undefined instead of
throwing, so an injected-config read falls through its own `|| ''` exactly as in
the browser. Symbol.toPrimitive makes any remaining stub render empty.
Measured components: 2 -> 4, with no NOT MEASURED left.
★ AND THE MIGRATION FOUND A REAL BUG, which is the part worth reading.
regions-list's Cyrillic heading rule ended with a trailing comma and NO
declaration block, so the concatenation emitted:
[lang="ru"] .rl-title, [lang="ru-RU"] .rl-title,regions-list
A selector list with nothing to apply — the Russian heading font was silently
never applied — AND it swallowed useShadowStyles' style-id argument as CSS,
leaving that sheet unlabelled. Braces stayed balanced, so
check:shadow-css-parses could not see it, and it passed 80/80 both before and
after. cyrillicHeading('.rl-title') replaces it and emits a real rule.
SO THE BYTE-COMPARE SPLITS, deliberately, and I did not paper over it:
region-dashboards.vue IDENTICAL — cyrillicHeading('.rd-title','.rd-h2')
emits exactly the literal it replaced, verified
character-for-character before the swap. A pure
de-duplication.
regions-list.vue CHANGED, and the change IS the fix. The old bytes were
the bug, so its baseline is re-taken rather than
preserved. Preserving it would have pinned a broken
rule forever, which is the one thing a byte-identical
gate must not be used for.
NOT MIGRATED, on purpose: strongMedium(). The helper emits bare `strong{...}`
while region-dashboards emits `.rd-root strong{...}` — a narrower selector.
Swapping it would widen the rule to every <strong> in the shadow root, a
behaviour change disguised as a de-duplication. scrollbarGutter() is likewise
left inline: both files carry it inside a larger .rd-scroll/.rl-scroll rule, not
as a standalone declaration the helper could replace.
Both files now gated (owned:true) instead of recorded-only, so a future edit that
changes their emitted CSS fails rather than being noted.
Two reported mobile bugs (DRV1650 #3 and #4), both settled by rega's
measurements at 390x844 on dx-life /research rather than by reading source.
★ #3 — THERE WAS NO BACK-TO-TOP AT ALL, and I had reported otherwise. I said
VerticalTabsLayout already rendered one targeting the active .vtl-host. rega:
ZERO .vtl-host elements in the live build, button.btt ABSENT from the DOM. The
chain: VTL renders .vtl-host only for `mountedElementRows`, which filters rows
carrying an `element` key — and this SPA passes :groups whose rows carry none,
mounting its own .dms-pane panes imperatively. So activeHostEl is null, and the
layout's button correctly renders nothing. That button is for layouts hosting
their own element rows; it is inert for the composition the gallery uses. I had
read the source and not the configuration.
The SPA now renders its own, bound to the ACTIVE .dms-pane — the scroller rega
measured (clientHeight 712, scrollHeight 12,373, overflow-y:auto) while the
document does not scroll at all. The gallery owns both the circle and the
scroller, so no shadow-root walking is needed. Placement reads --map-fab-beside,
published by the layout only while its circle renders, so the arrow sits BESIDE
the circle and falls back to the plain inset where there is none.
BackToTopButton already refuses to appear when its target does not scroll.
The label goes in all ELEVEN of the SPA's locales. Its translate() falls back to
the KEY, so a missing entry would have rendered the literal string "backToTop"
to every reader — en/ar/zh/ru/hi verified, and the Arabic and Chinese strings
are present in the built bundle.
★ #4 — THE REGIONS ATTRIBUTION SITS UNDER THE CIRCLE, and the geometry was not
what I assumed. rega: at 390 the attribution is details.maplibregl-ctrl-attrib in
the BOTTOM-RIGHT container, expanded to 347x44 at (22,791) — nearly the full map
width — so its LEFT END runs under the 44px fab at (12,788), 1,394 px² overlap.
The bottom-left container is EMPTY (0x0), so a bottom-left-only fix would have
changed nothing.
Both bottom containers are lifted off the published --map-fab-clearance under a
767px query, so a standalone leaf or a <doxa-country-map> with no gallery around
it reads the 0px fallback and nothing moves. The existing z-index:3 cannot help:
the circle lives in the layout, outside this shadow root, so the two are
different stacking contexts. Attribution is a LICENCE TERM of the basemap, which
is why this is a fix and not a polish item.
REBASED ONTO 0428f279c BY REPLAYING SOURCE, not by resolving a conflict: the
rebase collided only in generated bundles, where a conflict is never resolvable
by hand. Three files applied clean; RegionCountryMap.vue had MOVED to
library/components/maps in that same round (my own country-map work), so its rule
was re-applied at the new path by hand — a straight patch would have failed with
"No such file or directory", which is exactly what studio would have hit.
country-map, regions-dashboard and doxa-maps-spa rebuilt; both fixes verified in
the shipped minified bundles, including the new country-map bundle that now
inherits the lift.
NOT PROVEN HERE: the headless DOM proof needs a browser (rega's seat). What I
verified is that the button, its scoped CSS and its ar/zh labels are in the
SHIPPED doxa-maps-spa.js, and the fab lift in both map bundles.
countries-map's print path set the poster's legend from legend.rows, but useLegendData returns items,
never rows, so the renderer got an empty list and page 1's live map printed with no legend. It now
takes the items, with their child tiers flattened in, as research-map's poster does since A1.
check:legend-data-contract FAILED on this file and PASSES now; it goes into gates:pre with this round.
key-cities is a gated profile: generate-tree leaves it out of the manifest and the pages. Its
bundle file was still in public/dx-maps/key-cities/key-cities.js, committed, and the landing
freshness pass rebuilt it into public/ every time it looked "stale", so any host that serves or
drops in the tree folder whole shipped it.
- vite.config.js builds a gated bundle into .gated/<tree>/ (ignored by git) instead of public/.
- check:composite-freshness skips gated bundles: they have no committed bytes to be fresh against.
- The committed public/dx-maps/key-cities/ is removed. History is not rewritten.
- generate-tree removes a gated folder it finds in the tree (an older build, or a landing that kept the
tree's side of a conflict), so the tree it writes is clean whatever came before. Measured: a planted
public/dx-maps/key-cities/key-cities.js fails the gate, and one regeneration removes it.
- The gated list is read in one place, internal/_tree-name.mjs, beside the tree name.
- check:gated-not-in-tree (gates:pre) fails if a gated name has a folder in the tree, a manifest
entry, or a page referencing its script, and proves each arm on a planted leak first. It FAILED
on the tree before this change (key-cities/ is in the tree) and PASSES after.
The data tables are left alone on purpose: key-cities.json also feeds the production Cities with
Internet map, and whether that dataset should be public is a separate decision.
doxa-maps-spa marks the pane of every tab that has explainer text with data-doxa-help, listens for
doxa:help, and opens the same what/how dialog for the active tab. Closing returns focus to the
exact "?" that opened it (composedPath reaches into the map's shadow root). The top-centre button
and its CSS are gone; a comment for the device-rule toast that sat above the old button's rule
is back on its own rule.
check:help-reachable (gates:pre) keeps it reachable: every map with text has a hosted "?" or a
named help control of its own; the app listens, marks its panes and has no top-centre button; and
the composable, executed on a small fake DOM, dispatches doxa:help bubbling and composed and finds
the host across a shadow root. Six planted defects, each caught.
Map controls: prayer-walk-planner (under fullscreen), gospel-internet (the map and the language
tree, the board's default view), country-posters (the country map, its entry profile) and
all-people-groups get the library HelpButton with hosted. Own bars, where there is no control
column: the Wall (after the Bless/Social toggle), the PPLR hub (the end of its view bar) and the
vision card (its top corner, at the end of the reading direction so Arabic mirrors it) use
useHostedHelp. The vision card's label is the library's own "About this map" in each of the
eleven locales, copied into vision.json rather than translated again. UPG Neighbor is left as it
is: its bar already has an About button that opens the full explainer.
Each "?" stays hidden until a host marks its pane, so nothing changes until the app answers.