Point 4 (READ THE WORD) printed Mark 5:1-20 in full at 14px, the longest
and smallest text in the packet. It now prints THE HEAVENLY FATHER'S
HEART: one heading, one framing line and seven short readings (Noah,
Lot, Rahab, the Gerasene, Cornelius, Lydia, the jailer), each a
household the Father saved through one person. The names come from each
locale's block; the addresses stay canonical English in the data and the
book is named in the packet's language, like every other reference
(Genesis and Joshua added to the 11 books tables).
That room goes to the twelve points: body 17 -> 22px, heading 19 -> 24px,
references 14 -> 17px, and 13 -> 16px between points. The page still fits
one sheet in all 11 locales (the tightest, de, ends 59px above the
bottom).
The gates that asserted the full passage (packet-render, packet-browser,
poster-prints) now assert the list: a .p4-heart block with its seven
readings. Rebuilt: my-upg-100-list, country-posters, regions-dashboard,
doxa-research-map, upg-neighbor; the tree and SRI regenerated.
At two posters per row on a phone (a ~162px card) the print row wraps to
three lines, and because it is a sticker pinned over the banner's bottom
edge that grows downward, it hung about 45px below the card's white box.
Each card is now fitted to its own row: a ResizeObserver on every .pfs
writes --pfs-room on its card when the row hangs past the white box (the
part below the banner, plus 8px of daylight, that the 26px bottom padding
does not cover). A row that fits writes 0, so every card that already
contained its row is unchanged.
check:poster-card-fits asserts it: a print row may not end below its own
card (below-card N). Rebuilt: my-upg-100-list, country-posters,
regions-dashboard; the tree and SRI regenerated.
The gate began from keys like 'adm0:IND', so it never ran resolveTerritory
on what the map hands the click handler, and it passed while every tapped
country resolved as a province. Section 4 feeds the resolver three real
tapestry shapes: a country (shapeName + iso3, no listId), an India state
(listId + shapeName + iso3), and a country drawn inside an Asia zone
(zone.adm0).
Two-sided: on the previous resolver it fails three ways (Sudan and Nepal
filed adm1 with null counts); on the fixed one it passes.
The bold header on a poster -- the UPG count and population set large, a
bigger map, and no "Jesus' Strategy to Feed the 5000" / "What is a Prayer List"
columns -- is the SPARSE layout, the "thumbnail-worthy" design. The `posters`
script generates with SPARSE=1, so every stored thumbnail has it. But the env
handed to the generator in the browser carried only FORMAT and POSTER_DATE, so
EVERY sheet drawn at the click -- UPG-100 and country alike -- fell back to the
dense layout.
MEASURED on FINAL (two UPG-100 zones, Northwest and South-Central India): the
thumbnail shows the SPARSE header; the print the reader actually gets shows the
small facts table, both vision columns and a smaller map. That is the reported
"the country poster looks worse than the UPG-100 poster" -- the comparison was a
SPARSE thumbnail against a dense sheet.
(a) posterSheets.js passes SPARSE for every POSTER format and NOT for 'packet'.
That is exactly the split the two build scripts make: `posters` sets
SPARSE=1, `packets` does not. The renderer's formats are 3x4, 3x3, letter and
packet, so 'packet' alone keeps the full layout its pages were built around.
(b) check:poster-browser-equiv's poster case now runs the Node side with
SPARSE=1 as well. It used to compare dense with dense, so it passed while no
reader ever saw that layout on a card. The packet case stays dense on both
sides. PASS: country-npl 909026 bytes on each; asia-india-gujarat (packet)
835047 on each.
The stored UPG-100 thumbnails are untouched by this. What changes is the PRINT,
which now matches them.
Found in the max-effort re-read of 1ab8524cf. countryZone now colours the sheet
from the region, and this gate's zone omitted it -- so it rendered the FALLBACK
teal and logged '[posterSheets] region undefined is not in POSTER_REGIONS' on
every run. The gate still passed, because what it pins is the served generator's
integrity rather than the colour, but it was exercising the path that does not
ship and shouting about it.
Chad is Africa in library/data/country-posters.json. The warn-once is doing
exactly its job here: it found a caller that had not been updated.
From studio's review of 87f10ceda. configDest swallowed a JSON parse error, so with a malformed
vite.doxa-maps-build-config.json (one trailing comma from a hand edit), --if-present read it as "no
destination configured" and exited 0. The build went green and never refreshed the site. The parse
error is now recorded when the file EXISTS. Under --if-present or --from-config the command exits 1
with "build config unreadable: <file>: <error>", even with DOXA_DROPIN=off. A config file that is
absent, or one with no dropin.dest, still skips.
Tested 7 ways:
1. dest configured, host absent: skip, exit 0.
2. host present: 1,208 files copied, exit 0.
3. no dest key: skip, exit 0.
4. no dest key, plain --from-config: exit 1.
5. trailing comma: exit 1, "build config unreadable".
6. trailing comma with DOXA_DROPIN=off: exit 1.
7. no config file: skip, exit 0.
The config was restored byte for byte (cmp).
Found re-reading 2a4c3ddb3. With dropin.dest removed from the config (a site that doesn't use DX Life)
and no DOXA_DROPIN_DEST, the full build's last step printed the usage line and exited 1, after a build
that had passed. The host is optional, so --if-present now skips there as well. Without --if-present a
missing destination is still refused.
Tested: no dest configured with --if-present skips (exit 0); the plain command still exits 1; a configured
but absent host still skips (exit 0). The config was restored byte for byte after the test.
Only the printed percentage rounds. A large list at 0.004% is tens of
thousands of people, which printed as 0 when the total was computed from
the rounded figure; a share below the last printed digit now reads
"<0.01%" beside its real total. check:evangelical-share asserts it.
The page reused the life calendar's grid and counted its shaded boxes in
weeks ("0.61% · 32 weeks"), under a week axis and a year axis, with a lede
calling it "one human life in weeks". On this page a box is an equal share
of the list's PEOPLE, so the same 5,200 boxes are now drawn with no week or
year axis, and the legend states each side in people: how many follow
Jesus and how many do not yet, each with its share (the two percentages
sum to 100 as printed), plus how many people one box stands for. Numbers
are formatted in the reader's locale.
The labels become "Followers of Jesus" / "Not yet following Jesus" and the
lede drops the weeks, in all eleven languages; the served strings file is
re-pinned. check:evangelical-share asserted the weeks figure; it now
asserts the people totals, the bare percentage and the absence of weeks
and axes, with the life page (which keeps its axes) as the negative
control. check:packet-variants found the grid by an exact class string
and now matches the class prefix.
The Deep Research page publishes the rail as Dashboards (Big Picture, Simple Research, Regions),
Prayer Mobilization (Prayer Progress, UPG-100 List), Gospel Research (Cities with Internet, Gospel
Resources), then the Experimental folder. Only the Prayer Mobilization pair moves. Big Picture now
says "desktop only" on every width, not only once a phone has disabled it.
check:spa-tab-order asserted the old four-name order with UPG-100 first; it now asserts all seven
published rows in order (still relative, with tab 1 positional). It needs a browser: rega runs it.
Three defects on country-map/country-map, which is production -- DX Life's
country pages. Found by auditing the element rather than from a report.
1. ANY profile-config change refetched all 2,106 records. The watch getter
returned a NEW ARRAY literal, and Vue compares a watch value with Object.is,
so a fresh array always reads as changed; both computeds read the injected
cfg, so the effect re-ran on every config change and the callback then fired
unconditionally. MEASURED with real Vue: flipping isDark refetched the whole
endpoint and redrew the map, and so did a host adding pinHint. A joined
STRING compares by value, so only a real language or country change refetches
-- which must still refetch, because the API localises the names.
This was latent waste until the composite re-forward made config writes
routine. It is now on every upstream locale switch, so the two changes are
complementary rather than independent.
2. A SLOW EARLIER RESPONSE OVERWROTE A NEWER ONE. loadPoints is async and had no
sequencing at all -- 0 abort or generation tokens in the file -- so two runs
could be in flight and the one that STARTED first could FINISH last. Both
assigned points, so the map showed the previous country's pins with no error
and nothing for the reader to notice. Every run now takes a ticket and a stale
ticket writes nothing: not the points, not loadFailed, not the spinner the
current run is still using. fetch is left to complete rather than aborted,
because the response is cached for the next switch back.
3. `loading` WAS WRITE-ONLY: assigned in three places and read in none, zero
template references. A reader on a country page waited on an empty map -- no
outline, no pins, no message -- which is indistinguishable from a country that
genuinely has no people groups, and this element fetches the whole list before
it can draw one pin. "Loading results..." was already in the catalogue in all
eleven locales, so saying so cost no new string and no parity debt.
check:country-map-refetch guards all three. It drives the REAL reactive chain
with real Vue and takes the watch key expression OUT OF THE SHIPPED SFC, because
bug 1 is entirely a question of what Vue considers changed and is invisible to
any test that fakes reactivity or re-types the watch.
Mutation-tested, 5 mutations, all caught by name. The first version of the gate
MISSED one: a bare /if \(!current\(\)\) return/ grep matched the guard in the
catch block, so removing only the guard before the points write still passed.
The assertions are positional now -- the guard must sit immediately before the
assignment it protects -- and the catch guard is asserted separately. That hole
existed because the behavioural half of the test reproduced the guard's shape
inside the gate instead of reading the component, which is the self-consistent
oracle this repo's other geometry gate explicitly warns against.
Registered in scripts and gates:pre, verified by re-parsing: one key, 107
checks. Bundle rebuilt (BUNDLE=country-map), then tree, then fix:sri.
- research-map.vue's openPacket read and wrote _packetBusy, a name 8ab3dc0f5 replaced with the
reactive packetBusy ref. Reading an undeclared name throws, so the packet export threw a
ReferenceError on every click, in doxa-research-map and in research-map-plus-grid, the production
map that compiles it. It uses packetBusy.value now (check:no-free-identifiers).
- check:list-prop-unit and check:optional-t-equivalence read every entry of library/i18n/locales as
a locale, and the folder has held a README.md since bba33e783. They read locale FOLDERS now.
- people-groups-grid's regions-look instance previewed two props (filters-toggle, backdrop) of a grid
merge that is not on development, so the page showed nothing different and
check:instance-config-keys failed. It returns with that merge.
`bun run build` now ends with `sync-dropin --from-config --if-present`. It copies public/dx-maps into the
ONE configured host path (vite.doxa-maps-build-config.json dropin.dest, default ../doxa-life/public, or
DOXA_DROPIN_DEST). No site at that path: it prints "skipped" and exits 0. Nothing else on the filesystem
is searched for repos or sites. DOXA_DROPIN=off skips it even when the site is there. Without
--if-present, a missing host is still an error, as before.
Tested: no host skips (exit 0); a host gets 1,209 files; DOXA_DROPIN=off copies nothing; the plain
command still refuses a missing host. The container's /cdn build (build-cdn.sh) does not run this chain.
On this box ../doxa-life does not exist beside the doxa-maps checkouts (DX Life lives in root-dx-life), so the step is a no-op for the fleet's builds.
- check:no-orphan-gates stopped at internal/check-grid-merge-defaults.mjs, which had no script. It
asserts defaults of three grid props that are not on development (the grid merge has not landed),
so it is registered ON DEMAND with that reason, not chained red. Past that first arm, its chain arm
found check:grid-image-loading and check:retry-fetch run by no chain; both depend only on code and
pass in under 300 ms, so they join gates:pre.
- A tracked vite.config.js.timestamp-*.mjs (395 KB, eight absolute paths) failed check:no-abs-paths.
Removed, and ignored at the source: vite writes one beside its config while it bundles it.
- check:library-index: documentation/reference-library-index.md regenerated for retryFetch.js.
- run-parallel printed a failed step's output only with --verbose: the unbraced else bound to the
inner if. Braced; a failing step now prints why.
- The Pages workflow ran bun run build without BUILD_ALLOW=1, so build-lock refused it with exit 75
and the workflow ran no gate at all. A CI runner is the only builder on its machine; it opts in.
sr asked to adopt usePaneConfig for research-map-plus-grid rather than write a
second fix, and found the one shape it did not cover: people-groups-grid takes
its filters as THREE separate attributes (wagf-region, religion-family,
primary-language) instead of one profile-config blob, so that composite has one
pane of each kind. The helper hardcoded 'profile-config' and gave the grid
nothing.
registerAttrs is the sibling for that shape. It shares the same entries list,
the same watchEffect, the same prune and the same write-only-on-change -- one
mechanism for all four composites rather than a fork per adopter. A helper with
a single caller has not been shown to generalise at all; this is the second
shape that showed it.
Two decisions worth recording. Each attribute is compared and written
independently, so a getter whose region changed does not rewrite the two that
did not and re-trigger the child. And the attribute NAME LIST is pinned at
registration: a later getter that omits a key CLEARS that attribute rather than
leaving a stale filter silently narrowing the grid, which is the same class of
lie as the frozen config this helper exists to fix.
The gate runs the shipped helper with real Vue, not a re-typed copy, because the
dependency-tracking hole found in review is invisible to any test that fakes
reactivity. 28 cases over both shapes. Its subject is the measurement on
development 92ca8b6e6: the outer element reached locale ar by +1.5s while every
nested host carried NO locale key at boot and still none at +15s, across four
composites -- so case 1b asserts that an ABSENT key is still a change.
Case 7 is the adopter's property rather than mine: a re-forward triggered by a
LOCALE change must produce the SAME primary-language string. gridLanguage() can
fall back to a display label, and the grid compares whole normalised values, so
a name fetched in one language matches no row in another. It does not bite today
because the surviving fallback is a value the grid itself produced, but the gate
asserts the class, not today's instance.
Mutation-tested, 7 mutations, all caught: plain array, no write-on-change guard,
skip-instead-of-prune, escaping getter throw, no per-attribute compare, omitted
key left stale, and the label-instead-of-code regression. The first two crashed
rather than naming a case, so cfgOf is null-safe now -- a gate that dies reports
"something went wrong" instead of naming the pane that stopped following.
Registered in package.json scripts and in gates:pre, verified by re-parsing the
file: one key, 104 checks. gates:pre PASS 103 FAIL 0 NM 1.
useShadowStyles keys its <style> by the label and overwrites an existing
tag with the same key, so two components sharing a label keep only the
last one's CSS in a shared shadow root. The regions list lost its sheet
this way to the region card, which had kept the list's label when it was
extracted.
check:shadow-style-labels-unique (gates:pre) fails any label two calls
share, globally rather than per shadow root, so a copied or forked
component is caught when it is copied. It reads the real parsers,
classifies every reference to the function (a rebinding, .call, a
re-export or a namespace import fails instead of hiding a call),
cross-checks the AST against the token stream, and covers the other
ways two calls share a key: unlabelled calls under the dev server's
per-component key, and a fixed label over CSS that varies per caller.
countries-map.vue, a fork of research-map.vue, carried the same label;
it is now 'countries-map', with the country-posters bundle rebuilt and
the tree regenerated. The composable's JSDoc called the label a
debugging name; it now says it is the key.
The same-origin engine copy collided with the CDN preloads that landed meanwhile: map
leaves now preload https://cdn.jsdelivr.net/.../maplibre-gl.js (leaf hints), and composite
snippets preload the same URL. With the engine loaded from <tree>/vendor/ instead, those
preloads would never be reused and every map page would download the engine twice.
Re-pointing the preloads is design work, which the handoff does not start. The change
stays in history (fc7259e06, studio/vendor-maplibre 275cdc807), and it belongs to the
HANDOFF-BRANCHES-RECORDED-AS-MERGED-WITHOUT-THEIR-CONTENT card.
The gate hashed each component's entire emitted CSS, so any unrelated style change
in these four files (Regions giving three :host rules position:relative) failed it,
and could pass only by re-baselining a guard that exists to prove something else.
It now hashes what library/styles/portedGlobals.js emits, found in the executed CSS:
each cyrillicHeading and strongMedium rule with the selectors in front of it, and
each scrollbar-gutter declaration with its rule's selector.
Re-baselined on the tree the old gate passed on. Proven both ways: re-scoping
GoalCard's strong rule, changing the module's Cyrillic letter-spacing and dropping
a file's ported rules each fail it; --write refuses to baseline an empty block.
UNVERIFIED: carried into the handoff without a browser run on this base.
MapLibre GL came from cdn.jsdelivr.net: a second origin to connect to before a
map could start. generate-tree now copies the pinned maplibre-gl JS and CSS from
node_modules into <tree>/vendor/maplibre-gl@5.24.0/ and stops the build if their
bytes do not match the integrity pin. A built bundle loads the engine from its
own tree with that same pin, one cached copy for every bundle, and falls back to
jsdelivr if the copy is missing. Mapbox GL stays on api.mapbox.com.
UNVERIFIED: written, not yet run end to end; the handoff stopped it here.
The rebuild runs in the working tree, so it can read what no commit carries: an
untracked locale a glob picks up, a VITE_* key from an untracked root .env that
the code reads (apiBaseUrl.js reads VITE_API_BASE_URL), a build env var such as
DOXA_QA. Bytes built from those prove nothing about freshness, and the landing
precondition caches a PASS per tree id. Build env vars are now scrubbed from each
rebuild; vite.config.js writes down every file a build read when DOXA_BUILD_INPUTS
is set (only this gate sets it); a bundle whose rebuild read an untracked file or
such a key is NOT MEASURED (exit 75), naming the files.
Measured before the stop: an untracked .env.local with an unreferenced VITE_ key
leaves all-people-groups byte-identical (cf536c63), so only keys the code reads
are held against a bundle.
The gate hashed each component's entire emitted CSS, so any unrelated style change
in these four files (Regions giving three :host rules position:relative) failed it,
and could pass only by re-baselining a guard that exists to prove something else.
It now hashes what library/styles/portedGlobals.js emits, found in the executed CSS:
each cyrillicHeading and strongMedium rule with the selectors in front of it, and
each scrollbar-gutter declaration with its rule's selector.
Re-baselined on the tree the old gate passed on. Proven both ways: re-scoping
GoalCard's strong rule, changing the module's Cyrillic letter-spacing and dropping
a file's ported rules each fail it; --write refuses to baseline an empty block.
Ported from rega's snipgate and mbxcheck probes (card GATE-EVERY-PUBLISHED-SNIPPET-DRAWS-A-MAP). Two scripts so the keyless arm never waits on a token: check:snippets-draw pastes each production embed.script and requires a MapLibre map to draw (a hooked instance with its style loaded via a setter trap on the engine global, a sized canvas, tiles 2xx, no failure note, 0 page errors); check:snippets-draw:mapbox does the same over embed.scriptMapbox with only the token filled from MAP_TOKEN, NOT MEASURED without one and never printing it. Composites that open on a list (regions-mfe, upg-100-mfe) are pressed to their map view under the settle rule first, since a hidden pane still draws a canvas. The browser slot is checked before any server starts, and the tree server is spawned --exit-with-parent. Registered and declared on demand until rega's first run; it joins verify-browser-all after.
On 2026-09-29, 41 serve-strict processes had outlived the gates that started them: 279 MB resident,
the oldest 5.3 days, spread across eight worktrees. A gate that crashes, is killed, or is refused by the
browser-slot guard's process.exit cannot run its own srv.kill(), and _serve-reaped's handlers cover
signals and normal exit but not a SIGKILLed parent.
serve-strict takes --exit-with-parent: it records the pid that started it and exits within 2 s once
that process is gone. Opt-in, so a server someone deliberately left running is never taken down.
Every internal/ gate and tool that spawns it now passes the flag (14 files, plus _serve-reaped, the
shared helper). Proven: a parent that spawns the server with the flag and exits abruptly (as the
guard does) leaves it alive for a moment and gone within 3 s.
The orphans already running belong to other worktrees' runs and are left to their owners; plugin-local
scripts that spawn serve-strict should pass the flag too.
Seven Driver citations came back with the merged branches. Each keeps its
engineering reason and its date, and loses the speaker: the overlap report, the
poster's legend rule, the network-change defect, the chart's provenance.
check:plural-forms crashed rather than failed. It built its locale list by
listing the catalogue directory, so a README.md moved in beside the locale
folders became a 'locale', passed the exists check because a file does exist,
and crashed readdirSync on a non-directory. It now filters to directories. The
gate on development has the same line and only survives because that README is
not there yet -- so this is a fix for development, not a merge artefact.
Both found by running the full 103-gate chain; the shortened chain the merge
produced ran neither.