Same gap as my-upg-100-list. countries-map.vue provided onSemanticTreeSelect to
the mobile sheet but not onLegendExport, so its embedded SemanticTreeLegend
rendered with exportEnabled defaulting to false. Fixed the same way, gated on
the profile's own EXPORT_FEATURE_ENABLED so the phone never gets an affordance
the desktop deliberately hides.
Mutation-tested, which is what shows the provide is load-bearing: without it
mobile reports 3 .stl-export elements with 0 having a real box, and fails. With
it, 6 present and 3 boxed, and passes.
I deferred this last tick as "another agent's profile". That was my assumption,
not a recorded rule. The swarm lane assignment maps FILES, and
countries-map.vue appears in no lane — while SemanticTreeLegend.vue, which I
have been editing throughout, is in one. Treating profile directories as
ownership boundaries is a reasonable default, but it is not what the swarm
recorded, and it cost a known two-line fix a full tick.
A second defect turned up in my own gate. check:legend-export-reachable clicked
the legend toggle unconditionally. That is right for my-upg-100-list, whose
sheet starts closed, and exactly wrong for country-posters, whose sheet opens
expanded: 8 rows visible before the click, 0 after. The gate was closing the
panel it came to inspect and then reporting "no selectable row" — which reads as
a product defect and was purely the probe. It now toggles only when nothing is
selectable yet, and both profiles pass.
Also re-verified two cards already marked done, after the previous tick found
part (a) of the legend card shipped-but-broken. Legend part (b), "remove or
demote Zone", holds: zero occurrences of "zone" in the rendered legend. The
per-map share policy holds at the rendered layer — upg-100 has no share button,
prayer-progress has exactly one — with smoke:share-all green across all twelve
published bundles and its coverage set checked against the policy's own list.
The regions poster wall was downloading a .webp. UPG-100 opens a print dialog
over the page, and the Driver asked for the same UX with no difference.
This is one prop, not a second implementation: PosterWall's posterAction
defaults to 'download', UPG-100 passes "print", and regions passed nothing.
deliverPoster() branches on it and routes both the shipped-.webp and the
browser-rasterised path through the shared openPrintFrame, so the two walls
now print through exactly the same code.
The probe that covered this had been asserting the wrong thing. Its own
header said the Driver asked for print, but the assertion accepted "delivers
a file or says why" and then checked the file was a .webp — which a download
satisfies. It now asserts nothing is downloaded and that the shared print
frame is created.
Catching that took two attempts, because the frame is transient by design:
print() returns immediately in headless, afterprint fires, and the frame
removes itself within milliseconds. Polling the DOM afterwards found nothing
and looked like a broken print path. Subscribing to frameattached before the
click observes it without racing the cleanup.
Measured at the rendered layer: the legend opened on REGION at desktop and
mobile. That is the Driver's first complaint about this legend word for word —
"it shouldn't have me sitting there on Regions, I don't know what to do, it's
confusing" — and the card recorded that part as shipped.
Every source signal said it worked. LEGEND_DEFAULT_TAB = 1 existed, defaultTab
was a declared prop, it seeded a local ref, and it was named in a watch fallback.
The root cause is one layer down. SemanticTreeLegend resolves its tab as
`instance ? instance.activeTab.value : localActiveTab.value`, and
usePplrInstance initialises activeTab to ref(0). Zero is indistinguishable from
"the user chose tab 0", so for every profile sharing an instance store the
defaultTab prop was dead — read on every render, never able to win. An
activeTabTouched sentinel fixes it: the store only wins once something actually
chose a tab, meaning a tab click or a node selection that jumps to that node's
generation. A sentinel rather than a new default, so no other profile's landing
tab moves as a side effect.
And a regression of my own, from the previous commit. There was only one
:default-tab left in the profile and it was on <LegendMobile>, where defaultTab
is not a prop. Last tick I "dropped the inert attribute" with a string replace
that matched the DESKTOP block first, deleting the working one and leaving the
inert copy. A dropped attribute is silent, so the build stayed green. Restored
to SemanticTreeLegend, and LegendMobile now has a real defaultTab prop so the
phone honours it too.
Both widths open on "UPG-100 Lists", and on ?lang=es both open on "Listas
UPG-100" — locale-independent. A user's own choice still sticks: clicking Region
holds at 0, and selecting a row does not snap back, so the sentinel is not a
trap.
check:legend-export-reachable still passes and now selects a real zone rather
than a region, because the legend opens on the Lists tab. The two fixes compound:
a phone user lands on the lists and can export the one they pick.
Last tick I found the export unreachable on mobile and recorded it as
Driver-gated: "WHERE the control goes on a phone is a design call, the sheet has
no titlebar". Both halves of that were wrong, and checking them was this tick.
LegendMobile RENDERS SemanticTreeLegend itself — titlebar and all — and the 0x0
elements my measurement found belonged to the OTHER, hidden desktop instance
mounted below the breakpoint. That geometry made it look as though the sheet had
nowhere to put the control. A Driver decision was never needed.
Two gaps, not one.
The sheet passed neither :export-enabled nor @export, and exportEnabled defaults
to false, so the control's v-if was never satisfied. Fixed via provide/inject,
matching onSemanticTreeSelect immediately above it, because the sheet renders
several levels down where an emit would not reach. Opt-in by construction:
enabled only when a profile provides a handler. The other three LegendMobile
consumers provide none and are verified untouched.
Even then the export did nothing, and that second gap is the one that mattered.
The profile passed :nodes but no :tabs, so hideTabs was true and the sheet showed
a single flat Region tree with no tab strip — no UPG-100 Lists tab, no selectable
zone, and onLegendExport bails on `if (!zoneId) return`. Passing :tabs fixed it.
A :default-tab attribute was added and then removed: defaultTab is not a prop on
LegendMobile, so it would have been inert.
Verified end to end at 390x844: the tab strip appears, a real zone selects,
Export opens, Packet clicks, and one print frame is produced. A packet on a
phone. The gate written last tick — which failed on mobile by design — now
passes at both widths. The newly visible mobile tabs are fully localized in
en/es/zh, so catalog work from earlier ticks now reaches a surface that never
showed tabs at all.
Four of my own probe errors were caught by measuring rather than concluding.
Clicking a REGION row left selectedKey empty, so the handler correctly bailed
and it read as "export does nothing" — the same zero on DESKTOP is what proved
it was the probe and not a regression.
country-posters shows the same mobile gap on its own surface. Not taken; its
owner's call.
The repo is built by several people at once against a single branch,
and nothing in the README said so. The new section carries the
concentric-rings diagram and states the rule that makes it work: a
builder captures main before pushing, resolves every conflict while
holding it, then releases.
Placed before "Repository layout" — it answers the question a reader
has after seeing the studio and before reading a folder list.
"DX-Maps SPA" named the technology; "Research Gallery" names what the
thing is to someone opening it — the Big Picture plus every research
map behind one vertical tab rail.
Display labels only: the profile folder, bundle name and custom-element
tag are untouched, since changing those breaks every existing embed.
The in-shell header moves with the other two — it is the most visible
label of the three, and leaving it reading "DX-Maps" would have made
the rename look half-finished to the only people who see it.
SIL and its translation partners publish Scripture for small languages
the big catalogues skip, which is exactly the population left in the
gap: 2,901 groups had nothing in eBible, Arclight, Door43 or FCBH.
921 codes now carry a Scripture Earth link and 28 groups that had
nothing at all — 339,550 people — have Scripture in their own language
for the first time on this map. Tier 0 falls 2,901 to 2,873.
Most of this source is not what it appears to be, and the fetcher
throws away both false positives rather than shipping them. Their open
index lists 3,477 language directories, but 764 of the ones we can join
are empty stubs: trusting the listing alone would have handed 716
people groups a link to an empty folder. And a "video" directory holds
JESUSFilm-<iso>.txt metadata, not playable film — 1,588 codes have
nothing else, so video alone never qualifies a code. Only PDF, audio,
Scripture App Builder, study notes and SFM text count.
All 921 URLs verified HTTP 200 before shipping, and the artefacts were
opened by hand first: Baka's is 14.4 MB of application/pdf, Foodo's
40.7 MB, and a nonexistent sibling 404s, so a 200 here means a file.
The coverage gate, the verifier and the links table each had to be
taught the new field. Leaving any one of them ignorant would have been
worse than not adding the source: the gate caught all 28 tier changes
as inconsistent, and the links table filed 921 rows under the "other"
publisher — the fallback that exists to make an unscreened domain
impossible to hide. The sheet's "checked" line now names the pool too,
since it is searched.
Two problems, one change.
LOAD. Seven agents each running a wave put 7+ chrome-headless renders on one box;
measured today it reached load 47 with 8GB of swap, and a browser gate that is
swapping measures the swap, not the product. The wave runner now holds browser
steps while 1-min load is at or above LOAD_MAX (12) and takes a coarse flock
before each one, so they queue instead of dogpiling. Thresholds, cadence and the
20-minute cap are bundler-3's wait-for-quiet.sh verbatim, and BROWSER_GATE_LOCK
selects the rendezvous, so every agent lands on one lock whichever entry point
they use. Static gates are untouched: in gates:post that is 38 free, 4 queued.
The load wait is taken ONCE per wave, not once per gate. Per-step it multiplied
the same hold by the number of browser gates — an hour of a wave doing nothing,
which is how a load fix becomes a reason to stop running gates. The flock is
what serializes; the wait only decides when to join the queue.
Browser steps are identified from the gate FILE (a playwright import), not a
hand-kept list that goes stale the first time someone adds a gate — the one
missing from such a list is exactly the one that rejoins the dogpile. `verify` is
caught too: it imports no playwright itself but spawns three gates that render,
and it already marks them browser:true, so that declaration is honoured rather
than duplicated.
COVERAGE. Yesterday's guard made check:provider-switch and check:packet-browser
SKIP in every wave, because their default ports (4173, 4898) belong to other
worktrees on this machine. Skipping was honest but a gate that skips every time
is a gate nobody runs, so on a mismatch they now start a short-lived server over
THIS checkout and measure that instead — the point was never the port, it was
being able to prove which build answered.
MEASURED: a served tree whose manifest differs by one key but has all the same
pages -> "serves a DIFFERENT build", then "serving this checkout on :28384
instead", then a real verdict (16 -> 5 mapbox requests). packet-browser likewise,
once it re-navigates to the new origin — a dynamic import across origins is
refused by a plain static server, which cost one failed run to learn.
gates:post 40/42, the only skips being the two that genuinely have no input
(packets-all, zone-census); it was 4 before this.
The lock path default is product-named. The shared file several shells export
carries an internal code name, and check:no-codename refuses to let a repo we
hand over carry one — it caught this commit's first draft.
The Export affordance lives in SemanticTreeLegend, which upg-100-list.vue mounts
for desktop only — its own comment says "Hidden below the mobile breakpoint,
where <LegendMobile>...". LegendMobile, the phone bottom-sheet, contains the
string "export" zero times. So the whole export path is desktop-only.
The packet IS the product for this profile. The Driver asked for "print options
at the top right of the legend... that's the only way that map would be usable".
A reader on a phone gets the browsing half and none of the printing half, with
no error and nothing to click — the failure is invisible rather than broken,
which is why it survived.
Measured at 390x844 against 1500x1000. Desktop: selecting a row takes
.stl-export from 0 to 3 elements, all with a real box, labelled "Export".
Mobile: the same 3 elements exist and measure 0x0 at (0,0), with display:block
and visibility:visible, inside an overflow:hidden parent — the desktop panel
rendered without being laid out.
Two earlier readings of mine were wrong, and the gate now encodes against both.
"No export button at either width" — I had not selected a row, and the control
is gated on selectedNode. "No legend rows on mobile" — 20 rows exist with 0
visible, because the sheet starts closed. Both were true statements about the
wrong surface, and both would have been reported as product defects.
The gate asserts a real box rather than a CSS property, for the same reason: the
mobile elements report visibility:visible while measuring nothing.
It FAILS on mobile by design and is deliberately not wave-wired. WHERE the
control goes on a phone is a design call — the sheet has no titlebar to put a
top-right action in, and SemanticTreeLegend is shared by four profiles — so the
gate names the gap instead of assuming a fix. Mutation-tested: flipping
SHOW_EXPORT_BUTTON off drops desktop to zero and the message distinguishes "no
element at all" from "exists but 0x0".
Five gates took their measurement from whatever answered a default localhost
port. On this machine that port belongs to whichever worktree started first:
:4173 is wt-packet's server, :4898 is its repo root. Measured today — an hour of
probing reported on a build that was not on disk here, and the mistake was only
caught because a marker string was in the file and absent from the response.
"Answers 200" and "is serving this checkout" are different questions, and only
the second one licenses a verdict. _serves-this-tree.mjs already asked it (it
exists because the gospel-resources gate hit this and spent three runs reporting
on the wrong tree) but only three callers used it. Wired into the five that
measure a served page and had no such check:
check-provider-switch request tally per host — the wrong tree's tally
check-maplibre-layer-drops silent layer drops — invisible by nature
check-filter-width-stable width divergence — would accuse untouched CSS
check-facet-changes-map data flow on facet change
check-packet-browser imports the renderer FROM the server
Only a genuine MISMATCH skips. "No manifest to compare" is a different
precondition (an unbuilt checkout) that each gate already speaks for, and
turning that into a skip would silence these on the builds they should run on.
A mismatch exits 0, like the unreachable case, because it is a precondition and
not a product finding — but it prints which, so nobody reads it as a pass.
CONTROLS, each fired, per gate: correct server -> PASS with a real number
(provider-switch 19 -> 5 mapbox requests, drops 17/17, filter-width identical,
facet 2107 -> 1209 dots, packet-browser headings match); a served tree whose
manifest differs by one key -> SKIP naming "serves a DIFFERENT build"; no server
-> the pre-existing unreachable SKIP, unchanged.
One correction found by running it rather than by reading it: the first wiring
passed the LEAF PAGE URL where the helper wants the ORIGIN, so it looked for the
manifest under the page's own path and skipped every run for the wrong reason.
node --check cannot see that; only running both controls can.
KNOWN COST, not hidden: in a wave with no server of this tree running,
check:provider-switch and check:packet-browser now SKIP where they used to
report a green about another worktree. That is the honest reading, not a
regression, and the runner prints "evaluated nothing" for them. Next increment
is to let these gates serve this checkout themselves so they measure again.
Red since the rail gained its three-section structure on 2026-09-23. The Driver
said "cap-5 tab order (Big Picture, UPG-100, Prayer Progress, Gospel
Resources)". That names an ORDER, not a row count, and nothing in it forbids a
row between. The gate quoted him correctly and then asserted those four names at
positions 1-4 of a flat rail.
Measured at the rendered layer: the rail is Big Picture, Simple Research,
Regions, UPG-100 List, Prayer Progress, Cities with Internet, Gospel Resources,
plus five lab-badged Experimental rows when that folder is open. The Driver's
four sit at 1/4/5/7, all present, relative order intact. The instruction is
honoured.
Provenance rather than timestamps was the tiebreaker. My first instinct was
"source is newer, so the gate is stale" — but this gate cites a Driver
instruction verbatim, so I checked what actually moved. The suspect commit
turned out not to touch the main rail at all; the restructure was two days
earlier.
A second defect in the same gate: '.vtl-tab' matched nested elements, returning
35 hits for 7 real rows. The old code's "first four" were therefore all
fragments of rows 1 and 2 — which is why it reported row 2 as "Simple Research"
when Simple Research is genuinely row 2 of seven. Now keeps only outermost
matches.
The expectation is presence, plus tab 1 positional, plus relative order for the
rest. Tab 1 stays positional because that is exactly what the Driver claimed:
the landing surface. Mutation-tested both kinds — reversing the order fails
naming the rows, expecting a different landing surface fails naming tab 1.
PASS now prints the whole rail with the Driver's four starred, so a reader sees
the rows between them. A gate printing only its own four would hide the
structure that made the old assertion wrong.
Still not wave-wired, and the reason is on the script: it FAILS rather than
skips when the SPA is unreachable, deliberately — "a gate that cannot reach its
subject has not passed, it has not run" — which is right standalone and
incompatible with gates:post running on server-less checkouts.
213 people groups have no Scripture, audio, film, Bible video or Door43
resource in any pool checked AND zero people praying on doxa.life's
counter — 4.67M people, largest Songhai (BFA, 259,000), Kuka (TCD,
250,000), Kimr (SDN, 221,000). That intersection is the sharpest thing
in the data and nothing surfaced it.
The gap sheet gains a fourth tab whose rows are links straight to each
group's prayer page, so the answer to "nobody is praying" is one tap,
not a search. The sheet says plainly that "nobody praying" means zero
on doxa.life's counter, not a claim nobody in the world prays for them.
Two fixes found while proving it: the tab is built from the lazily
fetched prayer index, so opening the gap list now loads that index —
without it the tab only ever appeared if you had clicked a pin first.
And the new branch opened its own v-if chain, which left the old
"largest groups" list rendering underneath it, 80 rows where 40 belong.
public/doxa-maps/index.html conflicted; generated, so resolved by regenerating rather than hand-merging. fix:sri reports all 113 hashes already in agreement.
The pins already paint by region; the polygons underneath were flat brand
grey. They now take the same seven hues, so the chooser reads as seven
regions at a glance instead of one grey world with coloured dots.
The shared expression could not be reused as-is: it matches on a region
property that belongs to our own geojson, while the boundary tileset's
features carry iso_3166_1_alpha_3 and know nothing about WAGF regions. So
this bridges country code to region to the same palette — no second palette,
which is what the one-palette gate exists to prevent. Their fill is 0.12,
near-invisible, which is right for a single highlighted country; the chooser
is ours and carries 0.42 so the colours read. Cloned pages keep 0.12 and the
flat fill exactly, asserted both ways.
Brandon Grotesque is commercial and must not ship in a public giveaway, so
the two rules that named it now ask for Figtree. It is vendored the way the
other three families are — woff2 per subset under public/assets/fonts, an
OFL licence beside it, registered at document level because @font-face is
document-scoped.
The choice is measured, not a matter of taste. Their live page really does
load Brandon_Grotesque_medium.woff2, and Brandon sets "FULL PROFILE" at
128.00px at 500/19.5312px. Nineteen libre grotesques were measured the same
way: Figtree is 126, Nunito Sans 130, Barlow 125, Inter 134, Archivo 135,
Poppins 119, Montserrat 148. Figtree is the closest at two pixels.
Worth recording because the old scorecard has it backwards: 134.33 is the
generic sans fallback, not Brandon. The number to match was 128 all along.
Re-ran the unwired-gate census from last tick. check:facet-changes-map failed
with a clean verdict line, which looked like a real finding. It was not — it is
flaky: PASS, FAIL, FAIL, FAIL across four consecutive runs, failing with "the
map never populated after 90s".
Against the BUILT tree it passes: twice in the first check, then three more
times through the new script entry. Five for five. The product is fine.
A flaky gate is worse than a failing one. It teaches you to re-run until green,
which is how a real red later gets dismissed as "just the flake".
The cause is the default target. A dev page load pulls 88 unbundled module
requests against the built leaf's 3, and Vite compiles them on demand, so with
several agents working the same box the wait this gate ALREADY retries three
times at 90s can still lose. Raising the timeout is the wrong fix for exactly
that reason: it is not one slow moment, it is contention.
The dev default is NOT changed. Its header gives a sound reason — the dev
surface always reflects current source — and another agent may be relying on it
for hand-running uncommitted work. Instead the flakiness, its cause and the
deterministic invocation are recorded on the script, and
check:facet-changes-map:built makes the built-tree run a command rather than
something to remember. Also recorded why this gate is not wave-wired: a step
green 1 run in 4 would train everyone to ignore the wave.
One dead end worth recording. I tried to reverse-engineer the wait by locating
the map instance myself and got "never found" on BOTH surfaces — my selector was
wrong, not the product. The gate has working discovery of its own; I stopped
probing and used its result, which was already decisive.
check:maplibre-drops went red on gospel-resources/resources-map and the new
gospel-dashboard leaf: layers.gr-pins.paint.circle-sort-key: unknown property.
circle-sort-key is a LAYOUT property. Mapbox tolerates it in `paint`; MapLibre
rejects the whole layer for one misplaced property and does NOT throw — so under
the keyless engine all 12,379 dots were absent while the page still rendered its
basemap, its choropleth and its legend, and looked fine. That is the same defect
that cost all-people-groups its pins, so the fix is the same: move the key into
`layout` and leave the z-order comment with it, since the reason (a group with
nothing found must win the z-fight) is about the sort, not about paint.
MEASURED after, both engines: gr-pins present, circle-sort-key in layout, pins
drawn. Screenshot read on maplibre — dots and choropleth both there.
check:maplibre-drops PASS 17/17 (it was 15 ok / 2 dropping).
The gate earned its keep here: this arrived with incoming work, not with mine,
and nothing in the page's own appearance would have told anyone.
public/doxa-maps/index.html conflicted again; it is generated, so it was resolved by regenerating the pipeline rather than hand-merging. fix:sri reports all 113 hashes already in agreement.
The Driver asked for the affordance every other map here already has. The
flag alone was not enough: useEdgeScrollAffordance does not create the strips,
it scans the wrapper for them and returns a no-op when it finds none, so
flipping showEdgeBorders left the page with zero strips. The markup and its
CSS are per-consumer in all five existing users; only the behaviour is
shared. Both are ported from upg-100-list rather than written fresh —
including touch-action:pan-y, which is the mechanism that lets a vertical
drag scroll the page while Mapbox keeps the centre.
The reason the flag was originally false is kept in the file rather than
deleted, because it is the thing to re-examine if the result looks wrong:
this map sits inside a scrolling dashboard, where the gutters can read as
page chrome rather than map chrome. The reference maps own their viewport.
Merged rather than rebased: this branch already carries an ours-merge recording an
earlier pre-rebase tip, so replaying it onto main would duplicate that ancestry. A merge
takes main's one new commit cleanly and leaves both histories intact.
Conflicts, if any, are generated public/ output and are resolved by taking main's and
regenerating from source.
Typing into the research-map's search bar inside a composite (the Simple
Research dashboard) wiped the box instantly and never queried. Standalone, the
same map searched fine — which is the shape of the bug, not a coincidence.
mapbox-gl-geocoder unwraps exactly ONE shadow level to find the input:
var e = t.target && t.target.shadowRoot ? t.target.shadowRoot.activeElement : t.target
if (!(e ? e.value : "")) { this.fresh = !0, 9 !== t.keyCode && this.clear(t), ... }
Its handler is debounced, so it reads event.target after dispatch has ended, by
which time the target is the OUTERMOST host. One hop is therefore exactly right
for a map one custom element deep and exactly wrong for a map inside another
one. Measured, same build, same keystroke:
doxa-research-map (1 deep) target=DOXA-RESEARCH-MAP -> unwrap = the input -> _geocode("Bhu")
research-map-plus-grid (2) target=RESEARCH-MAP-PLUS-GRID -> unwrap = <doxa-research-map> -> clear()
An element with no .value reads as an empty box, so the vendor cleared what you
had just typed. Walk the activeElement chain to the bottom instead of one step
and hand the vendor an event whose target is that element; nesting deeper cannot
break it again, and at one level it resolves to the same input as before.
Two things worth recording. The receiver is passed explicitly: the Mapbox build
binds this method in its constructor and the MapLibre build does not, and an
unbound call landed on `this === undefined`, which swallowed the keystroke AND
the query ("Cannot read properties of undefined (reading
'_hideGeolocateButton')") — green-looking text with no search behind it. And
neither suspect on the card was the cause: the clear came from inside the vendor
bundle, not from _geoBeingCleared, and SearchBar is not involved.
MEASURED on the built tree, both engines' mapbox mode, typing "Bhutan":
fix off leaf ok (1 geocoding request) / composite value="" and 0 requests
fix on both keep "Bhutan", both fire the query, both list 19 results
including People Cluster, People Group + Country, Country and Place
Screenshot read, not just the log. gates:pre 7/7, gates:post 34/34.
Its toast has always read "Tap a coloured dot — someone still needs prayer". Tapping one
showed Scripture, film, audio, video, stories, tracts and training, and no way to pray.
doxa.life publishes a page per people group and sibling plugins in this repo already link
to it; this map simply never did. 1,723 groups now carry a "Pray for <group>" action with
the number already praying beside it.
THIS IS THE ONE JOIN HERE THAT IS NOT BY LANGUAGE, and it needed justifying rather than
assuming. Every other source keys on ISO 639-3 because a translation serves everyone who
speaks the language; prayer does not work that way — you pray for a people, in a place. So
this keys on (group name, country), which is a NAME join, the thing this map refuses
everywhere else because names collide.
Allowed only because the key was tested first: on the API side (name, country) is
perfectly unique — 2,107 entries, 2,107 distinct keys, 0 collisions — and on the pin side
12,379 pins give 12,369 distinct keys. The ambiguous ones are DROPPED, not guessed. One
entry was lost that way, and 383 more have no pin here at all.
pray.doxa.life is the fourth host in this bundle that answers 200 with identical bytes for
any URL: /abai-sungai and /zzz-not-a-real-group are both 3,903 bytes. Rendered it does
discriminate — a real slug titles the page "Abai Sungai - App" with 1,863 characters, a
bogus one titles it "Doxa.Life" with zero — but existence comes from the API listing,
which is the publisher's own statement and costs one request rather than 1,723 renders.
The lookup is COMPUTED, not baked into groupAt. groupAt runs the instant a pin is clicked,
so a value read there is whatever had arrived by then and never updates when the lazy
fetch lands — the same shape as the link-verification state that used to hang off
openPin's country branch.
Verified with a negative control: a group in the index renders an anchor whose href
matches the expected slug URL exactly, and German, which is not in it, renders nothing.
My probe reported FAIL twice against correct code by waiting a guessed 900ms; it now waits
for the data. Screenshot read — the Lati, 2,900 people with nothing found in any pool, can
now be prayed for.
languages-by-country is the bridge view: one row per country and language, ranked by
prevalence, flagging which languages could carry a resource to groups that lack one. Its
curated work — prevalence_rank, bridge_candidate, groups_speaking — is the reason it
exists and is untouched.
Its resource flags were not curated, and they had drifted like the other two legacy
tables: 75 rows wrong about Scripture, 39 wrong about whether the language carries
anything at all, 1 about film. That matters more here than elsewhere, because this table
is consulted to CHOOSE a bridge: a language flagged as carrying nothing when it carries
audio Scripture is a bridge somebody was told not to use.
Refreshed from the pin index, which is the same source the map draws. 0 of 613 rows now
disagree, and all 613 still carry their curated columns.
full_bible is deliberately NOT refreshed. The pin index knows a scope only for eBible
rows, so downgrading a curated "full Bible" on that basis would lose information rather
than correct it.
Also recorded rather than acted on: this table covers 175 countries while the map has
242 with pins, so 67 countries and 1,537 people groups have no bridge analysis at all.
That is a gap in curated coverage, not a contradiction — the rows that exist are now
correct — and filling it means doing the prevalence work, not copying numbers across.
Censused the 67 gate scripts that are in no wave. The framing I had been using —
"coverage we are not getting" — was wrong in the more serious direction. Two of
them were FAILING on main right now. The gates exist, work, and had found
something; because nothing ran them the findings sat silently.
check:legend-source, a real product finding: the live corpus moved 2108 -> 2107
and one group crossed Unengaged -> Engaged, so the legend baseline was
describing a dataset that no longer exists. Confirmed 2107 independently against
the live endpoint rather than trusting the gate's own reading.
Updated the baseline following the procedure its header requires, repeated
rather than assumed: grepping the six figures across plugin-designs/ and
library/ finds them only in explanatory comments, so no shipping code hardcodes
a corpus total and only the baseline lagged. Mutation-tested — an off-by-8
baseline fails. Recorded that this is the third reading, that it drifts in both
directions, and that it must NOT be widened into a range: the noise is the
signal that the numbers on the page are live rather than asserted.
check:verification-tags asks whether a human ever looked at a done card. 98
could not answer. 24 of those were mine, and they are now tagged agent/state —
honestly agent rather than human, and state rather than visual, because every
one was verified by measuring rendered DOM, PDF bytes and request tallies, never
by a human looking at pixels. The other 74 belong to other agents and the
Driver; guessing their provenance would defeat the gate, so it stays unwired.
Six further gates were green and self-contained and are now wired: locale
reachability, url resolution, manifest coverage, the palette registry, and the
legend/source comparison. gates:post 37 -> 42; wave membership 43 -> 50 of 106.
One I nearly wired wrong. check:dxlife passes standalone but exits 2 by
documented contract — 0 unchanged, 1 drift, 2 clone not found — and the wave
runner grades any non-zero as failure, so a checkout without the sibling clone
would go red on a precondition. Caught in the fresh-checkout run, unwired, with
the reason recorded on the script so the next census does not re-add it. "Can it
run" and "is it wave-safe" are different questions.
42/42 with servers up; 31/42 passed and 7 SKIPPED on a fresh checkout, every
skip named.
The seven gospel-resources commits were rebased onto main after other agents landed, so
the branch tip and its rebased copies have different hashes with identical content. This
merge records the old tip as an ancestor so the branch advances by fast-forward instead
of a force push, which this repo forbids.
Verified before recording: the source trees at af6f343 and at the rebased HEAD are
identical under plugin-designs/, internal/ and data/.
gospel-by-country carries 242 countries with real per-country counts, and the map uses
them for the choropleth and the "where the gap concentrates" sheet. The tables profile
had no country table at all, so those numbers lived only inside the map — readable, not
exportable — while every other tab there offers CSV and JSON. A mobiliser deciding where
to send a team cannot work from a sheet they have to screenshot.
Derived, not a second source: every row is read out of gospel-by-country, which is itself
refreshed from the pin index, so the table and the map cannot disagree.
Three columns because they rank differently and each is somebody's question — groups with
nothing, people with nothing, and the share of the country each represents. The default
sort is by groups, and the share column is what makes rows like Vanuatu legible: 79 groups
with nothing out of 114, 69.3%, which ranks ninth by count and first by share. Region is
carried through so the table can be grouped the same way the map's region tab ranks.
Curated columns come through as-is and stay null where the curated analysis does not cover
the country — null, never zero, because zero is a claim this build cannot make.
Verified against the PIN INDEX rather than against the table being tested: 242 countries
with pins, 2,901 groups with nothing, worst is PNG at 469. The rendered tab reports 242
rows and its first row reads Papua New Guinea / PNG / Oceania / 831 / 469 / 56.4, with
650 real cells. No page errors. Screenshot read.
region was empty on all 242 country rows, so the map had no way to answer "what share of
this part of the world is unreached" — only how many groups a country holds and how many
people live in them.
The source carries UNm49Region, and it was verified before being trusted: only 2, 9, 19,
142 and 150 appear — exactly the five UN M49 top-level regions — every country maps to
exactly ONE of them with 0 conflicts, and the five sum to 12,379, the whole corpus. So
the code resolves to a real name. It is backfilled on every row, not just the new ones,
and all 242 now carry a region with none blank.
The rate is a genuinely different answer from the two already on offer:
Oceania 51.2% of its 1,325 groups have nothing — 679 groups — 3.1M people
Africa 30.0% of 3,074 — 923 — 68.2M
Asia 19.3% of 5,073 — 978 — 198.7M
Americas 11.4% of 2,117 — 241 — 6.1M
Europe 10.1% of 790 — 80 — 11.6M
Oceania is half unreached by group and the smallest population of the five; Asia is the
reverse on both. Ranked by rate, because count and population are what the other two tabs
already say and a third copy of either would be noise. All three are offered rather than
one picked for the reader.
The rows are plain divs, not buttons: a region is not somewhere this map can fly to, and
a control that looks clickable and does nothing is worse than a row that does not pretend.
Verified against an offline computation over the same index: all five regions match
exactly on rate, group count and population. The sheet header is now three-way rather
than showing the country prose in region mode. All five profiles still mount clean.
Set out to verify the map's second mode, "Resources found", which had never been tested.
It works: the legend switches, the arithmetic is an exact complement of the gap view — 0
of 170 countries disagreed — and the two modes genuinely repaint, 98/46/12/6 polygons by
band in gap against 85/66/9/2 in coverage.
Proving that took three attempts and the third one found the real defect. Comparing the
fill expression proved nothing, because it is ["get","color"] in BOTH modes — the colour
is baked onto each feature and switching modes rewrites the source data. Comparing the
features instead showed 157 coloured and 5 transparent against 170 rows in the table,
and that mismatch was the thread worth pulling.
The previous refresh of this table only UPDATED rows that already existed, and the table
came from the curated 2,106-record set. So 60 countries carrying pins had no row at all —
150 people groups with nothing found, invisible to every country-level surface. Canada
20, Ghana 15, Timor-Leste 13, Australia 7. 72 countries are now added from the pins, and
0 countries with bare groups are missing; all 242 rows agree with the pins on groups and
no_resource.
Curated columns on the new rows are null, never zero: this build does not know a growth
rate for Canada, and zero is a claim. They are flagged from_pins_only so the different
basis is visible rather than implied. Names come from people-groups-slim's CountryName,
because a row labelled "CAN" renders "CAN" as the sheet's title. UNm49Region was
deliberately NOT used for region — it is a numeric code, and "19" is worse than blank.
One row stays a bare code: Kosovo, whose non-standard KOS has no name in the source, and
inventing one for a single row is not worth it.
The boundary file turns out to hold only 162 polygons and has no Canada, Australia,
Timor-Leste or Taiwan, so this paints five previously-transparent countries and fixes the
rest where it actually shows: clicking a pin in Canada now opens a sheet titled "Canada"
with 304 groups, matching the pins exactly, where before it fell through to a bare ISO
code with no statistics.
Checked whether a reader can undo the dashboard filter and tell why the table changed.
They can: the banner names the selection and offers an escape, clearing works, and
re-selecting the same country refilters rather than sitting cleared — the filter object
carries a timestamp precisely so an identical selection is still a change.
The ambiguity is in the labels. Stacked under the map there were THREE controls reading
exactly "Show all": two in the map's group lists, which reveal the rest of a country's
people groups, and one under the table, which clears the resource filter. Nothing on
screen distinguished them.
That is not a theoretical complaint. The first probe written to verify this clicked one
of the map's, saw the filter unchanged, and reported "Show all does not clear the filter
in composed mode" — a defect that did not exist. If the ambiguity can mislead a probe
reading the DOM, it can mislead a reader looking at the screen.
The map's now read "Show all groups", the table's "Show all resources". One word each.
Verified after the change: the three candidates are distinguishable by text alone, and
the full cycle passes — filter to 400 of 6,498 for Indonesia, clear back to no banner,
re-select the same country and return to 400, no page errors. Screenshot read.
Fourth probe in this flight to produce a wrong verdict from an assumption about the DOM,
and the second where a composite made a pattern ambiguous that is unambiguous on a
single page.
The language link was fixed last change; the country link had the identical defect and
answers the brief's SECOND requirement. "Every resource used in Indonesia" was an
<a href="../data-tables/index.html?tab=links&country=IDN">, so inside the composite,
clicking a country to see its resources navigated away from the composite to do it.
Both controls now split the same way: a button that filters the tables below when they
are on screen, an anchor when they are not.
Verified against an offline expectation rather than read off the page. Counting the
links table for rows whose country_codes contain IDN gives 400. In the dashboard the
control is a BUTTON, the URL stays on gospel-dashboard/index.html, and the banner reads
"Showing 400 of 6,498 resources for Indonesia" — the same 400. Standalone it is still an
<a> to data-tables/index.html?tab=links&country=IDN&label=Indonesia, so nothing changed
for the map on its own page. No page errors on either.
Screenshot read: the filtered rows are real Indonesian-language resources across five
sources — JESUS Film, Faith Comes By Hearing audio and Bible video, Cru's Four Spiritual
Laws, eBible and Zume — with a "Show all" escape beside the count.
The composite puts the map above its own tables, and the brief's third requirement is
that clicking a pin filters that table. It did not. "Every resource in German, Standard"
was an <a href="../data-tables/index.html?...">, which is exactly right when the map is
alone — and inside the dashboard it navigated AWAY, to the standalone tables page. The
filter applied correctly, 6 of 6,498, and the map was gone. Measured: the URL changed
from gospel-dashboard/index.html to data-tables/index.html.
The two halves now talk to each other. The dashboard provides a shared filter; the map
injects it and, when present, turns that link into a button that sets the filter and
scrolls the tables into view; the tables inject it and apply it in place, switching to
the links tab because a language filter means nothing on the others.
PROVIDED, NOT PROPPED, because both children must still work standalone. A child that
finds nothing injected keeps its old behaviour exactly — verified rather than assumed:
on resources-map/index.html the control is still an <a> pointing at
data-tables/index.html?tab=links&iso=deu.
Composed: the control is a BUTTON, the URL stays on gospel-dashboard/index.html, the
table banner reads "Showing 6 of 6,498 resources for German, Standard", and 6 rows with
66 cells render. Arrow changed from → to ↓ because the destination is now down the page
rather than away from it.
My first probe read "12 of 25" here and was wrong. Both halves share ONE shadow root in
the composite, so the map's own "Showing 8 of 25" matched the same pattern as the
table's banner. The probe now matches the table's specific wording and asserts rendered
rows and cells beside it. Same class of mistake as the three already recorded in
AGENT-BRIEF — a pattern that is unambiguous on one page can be ambiguous in a composite.
All five profiles still mount clean; gospel-dashboard 68,459 chars, 54 links, 0 errors.
The dashboard is the one profile never opened on a phone. Structurally it holds up at
390x844 — no horizontal overflow anywhere in either shadow tree, canvas 390x675, the
table half renders 20 real rows and 60 cells, no page errors. What does not hold up is
what it says.
Under the table, the Pools Caveat read "Pools checked: eBible.org, JESUS Film (Arclight),
Door43/unfoldingWord". On the same screen, visible immediately above it, were rows typed
Audio Scripture, Bible video, Zume Training and Gospel presentation — four pools the
caveat denied having checked. It is the most direct version of a mistake this bundle has
now hit repeatedly: a sentence written by hand goes stale the moment a source is added,
and this one had gone stale four times over.
It is no longer written by hand. The checked list is now counted off the rows themselves,
so it cannot disagree with the table it sits under: "Cru (GodTools), Door43
(unfoldingWord), Faith Comes By Hearing, JESUS Film, PNG.Bible, Zume Training,
eBible.org, eBible.org (mirror)". The NOT-checked half stays prose, because it names
things that are absent and no row can testify to those — GRN, api.bible, the Digital
Bible Library and Progress Bible, each needing a credential or an agreement.
The page subtitle had the same shape of error, offering "scripture, film or audio" over
tables carrying seven media. It now names the question rather than a list that will go
stale again.
Verified on the phone viewport after the change: table half still 20 rows / 60 cells, no
overflow, no page errors, and the rendered caveat lists all eight sources. Screenshot
read.
gospel-by-country carries 242 countries with real per-country counts, and the map uses
them for the choropleth and the "where the gap concentrates" sheet. The tables profile
had no country table at all, so those numbers lived only inside the map — readable, not
exportable — while every other tab there offers CSV and JSON. A mobiliser deciding where
to send a team cannot work from a sheet they have to screenshot.
Derived, not a second source: every row is read out of gospel-by-country, which is itself
refreshed from the pin index, so the table and the map cannot disagree.
Three columns because they rank differently and each is somebody's question — groups with
nothing, people with nothing, and the share of the country each represents. The default
sort is by groups, and the share column is what makes rows like Vanuatu legible: 79 groups
with nothing out of 114, 69.3%, which ranks ninth by count and first by share. Region is
carried through so the table can be grouped the same way the map's region tab ranks.
Curated columns come through as-is and stay null where the curated analysis does not cover
the country — null, never zero, because zero is a claim this build cannot make.
Verified against the PIN INDEX rather than against the table being tested: 242 countries
with pins, 2,901 groups with nothing, worst is PNG at 469. The rendered tab reports 242
rows and its first row reads Papua New Guinea / PNG / Oceania / 831 / 469 / 56.4, with
650 real cells. No page errors. Screenshot read.
check:provider-switch is a good gate — it counts requests BY HOST rather than
reading window globals (which stay defined after a swap), and it allows exactly
the api.mapbox.com URLs the generated leaf declares in its own head, so a
half-fix at 18 requests cannot pass.
It had a package.json entry and was in no wave, because it REQUIRED a URL
argument and exited 2 without one. So it only ever ran when somebody typed a
URL — the same present-but-never-executed shape check:packets-all had before it
was given a fixture.
Wiring it as-is would have turned every server-less build red rather than
silently green, which is the less common half of that failure. It now defaults
to the upg-100 leaf and SKIPs in the form the wave runner recognises, so the
skip is counted and named instead of grading as a pass or an error.
gates:post 36 -> 37, README re-synced. Verified both ways: 37/37 with a server,
and 27/37 passed / 6 SKIPPED on a fresh checkout with none.
Found while re-verifying BUG-UPG100-IGNORES-THE-PROVIDER-SWITCH from scratch
rather than trusting its done status. The card holds: ?provider=maplibre drops
Mapbox traffic 19 -> 5 on the upg-100 leaf, and all 5 are the page's own
declared preloads, so zero runtime calls remain. The same run independently
reproduced the four residual URLs in BUG-LEAF-PAGES-PRELOAD-MAPBOX-EVEN-UNDER-
MAPLIBRE, which stays untaken — its fix is in generate-tree, the tree verb
bundler-1 owns. When those preloads go, this gate's allowance drops to 0 and it
holds the new floor with no edit.
region was empty on all 242 country rows, so the map had no way to answer "what share of
this part of the world is unreached" — only how many groups a country holds and how many
people live in them.
The source carries UNm49Region, and it was verified before being trusted: only 2, 9, 19,
142 and 150 appear — exactly the five UN M49 top-level regions — every country maps to
exactly ONE of them with 0 conflicts, and the five sum to 12,379, the whole corpus. So
the code resolves to a real name. It is backfilled on every row, not just the new ones,
and all 242 now carry a region with none blank.
The rate is a genuinely different answer from the two already on offer:
Oceania 51.2% of its 1,325 groups have nothing — 679 groups — 3.1M people
Africa 30.0% of 3,074 — 923 — 68.2M
Asia 19.3% of 5,073 — 978 — 198.7M
Americas 11.4% of 2,117 — 241 — 6.1M
Europe 10.1% of 790 — 80 — 11.6M
Oceania is half unreached by group and the smallest population of the five; Asia is the
reverse on both. Ranked by rate, because count and population are what the other two tabs
already say and a third copy of either would be noise. All three are offered rather than
one picked for the reader.
The rows are plain divs, not buttons: a region is not somewhere this map can fly to, and
a control that looks clickable and does nothing is worse than a row that does not pretend.
Verified against an offline computation over the same index: all five regions match
exactly on rate, group count and population. The sheet header is now three-way rather
than showing the country prose in region mode. All five profiles still mount clean.
This header said the actions/runners endpoints 404 on this Forgejo version, so
runner state "was not directly measurable". Only the LISTING endpoint 404s. Both
registration-token endpoints return 200 and issue a 40-char token on demand:
/api/v1/repos/doxa-life/doxa-maps/actions/runners/registration-token
/api/v1/admin/runners/registration-token
That distinction matters more than it looks. "Not measurable" reads as "nothing
to try", and this workflow has sat designed-but-never-run for weeks while the
one action that would activate it was API-reachable the whole time.
The header's load-bearing claim is unaffected and re-verified today: still
total_count 0, still never run.
It also cited a handoff doc twice — in this header, and beside "the ONE Driver
action that registers it" — and that file did not exist. It does now, at the
path cited, written from measured server facts rather than generic Forgejo
advice: version 9.0.3+gitea-1.22.0, has_actions true, the label the runner must
carry (ubuntu-latest, or every job queues forever looking exactly like no
runner), the github.com reachability the setup-bun action needs, and the
confirmation step that distinguishes "registered" from "actually ran".
Note this commit touches only .forgejo/, which paths-ignore excludes — so it
will not trigger a build even once a runner exists. That is intended.
Set out to verify the map's second mode, "Resources found", which had never been tested.
It works: the legend switches, the arithmetic is an exact complement of the gap view — 0
of 170 countries disagreed — and the two modes genuinely repaint, 98/46/12/6 polygons by
band in gap against 85/66/9/2 in coverage.
Proving that took three attempts and the third one found the real defect. Comparing the
fill expression proved nothing, because it is ["get","color"] in BOTH modes — the colour
is baked onto each feature and switching modes rewrites the source data. Comparing the
features instead showed 157 coloured and 5 transparent against 170 rows in the table,
and that mismatch was the thread worth pulling.
The previous refresh of this table only UPDATED rows that already existed, and the table
came from the curated 2,106-record set. So 60 countries carrying pins had no row at all —
150 people groups with nothing found, invisible to every country-level surface. Canada
20, Ghana 15, Timor-Leste 13, Australia 7. 72 countries are now added from the pins, and
0 countries with bare groups are missing; all 242 rows agree with the pins on groups and
no_resource.
Curated columns on the new rows are null, never zero: this build does not know a growth
rate for Canada, and zero is a claim. They are flagged from_pins_only so the different
basis is visible rather than implied. Names come from people-groups-slim's CountryName,
because a row labelled "CAN" renders "CAN" as the sheet's title. UNm49Region was
deliberately NOT used for region — it is a numeric code, and "19" is worse than blank.
One row stays a bare code: Kosovo, whose non-standard KOS has no name in the source, and
inventing one for a single row is not worth it.
The boundary file turns out to hold only 162 polygons and has no Canada, Australia,
Timor-Leste or Taiwan, so this paints five previously-transparent countries and fixes the
rest where it actually shows: clicking a pin in Canada now opens a sheet titled "Canada"
with 304 groups, matching the pins exactly, where before it fell through to a bare ISO
code with no statistics.
Checked whether a reader can undo the dashboard filter and tell why the table changed.
They can: the banner names the selection and offers an escape, clearing works, and
re-selecting the same country refilters rather than sitting cleared — the filter object
carries a timestamp precisely so an identical selection is still a change.
The ambiguity is in the labels. Stacked under the map there were THREE controls reading
exactly "Show all": two in the map's group lists, which reveal the rest of a country's
people groups, and one under the table, which clears the resource filter. Nothing on
screen distinguished them.
That is not a theoretical complaint. The first probe written to verify this clicked one
of the map's, saw the filter unchanged, and reported "Show all does not clear the filter
in composed mode" — a defect that did not exist. If the ambiguity can mislead a probe
reading the DOM, it can mislead a reader looking at the screen.
The map's now read "Show all groups", the table's "Show all resources". One word each.
Verified after the change: the three candidates are distinguishable by text alone, and
the full cycle passes — filter to 400 of 6,498 for Indonesia, clear back to no banner,
re-select the same country and return to 400, no page errors. Screenshot read.
Fourth probe in this flight to produce a wrong verdict from an assumption about the DOM,
and the second where a composite made a pattern ambiguous that is unambiguous on a
single page.
The legend's Export > Prayer packet action is the path a reader clicks. The
build-time file is the one nobody clicks. The reader's packet was the less
verified of the two.
check:packet-render gained six assertion families over recent ticks — the 12
prayer points, the wheel's 12 LABELS rather than just its 12 segments, page 1's
geography, the print layer, the image census, the build stamp. Each was found by
mutation and each catches a real "looks fine, is empty" shape. None existed
here.
It also printed "Mark 5 in full : undefined" beside a PASS for as long as it has
existed: the measurement is r.mark5Len and the print read r.mark5, which is not
a key on r. The assertion always read the right key, so the requirement was
protected — but undefined next to a PASS reads as a gate that does not know what
it is doing. That is the inverse of this tree's usual failure, where a number is
printed and never asserted on.
A mistake caught before it became a false report. The new geography assertion
went red immediately with "no map svg on page 1", which would have read as a
serious product defect. It was the harness: this gate called buildPacket with an
EMPTY mapSvg, while the real export path fetches posters/<zone>.html and lifts
its <svg>. The shortcut was not just skipping an assertion — it meant the gate
never exercised mapCss() in a browser at all, which is the one thing it exists
to prove. It now fetches the map the way the product does and reports 13
geography paths, the same number check:packet-render gives for laos. ISO3 and
ZONE are also different strings ("lao" vs "laos.html"), so even a naive fetch
would have missed; PACKET_ZONE names it.
Mutation-tested: blanking the wheel labels gives 0/12 labelled and fails; hiding
content at print time gives 0 embedded fonts and fails.
i18n verified on the browser path for the first time. A partial locale with 3 of
12 points renders 12/12 points — the per-key merge holds here too — with the
points heading translated, the wheel heading falling back to English, Mark 5 at
2220 chars, and nothing blank.
Also made wave-safe: without a server it threw an unhandled navigation error and
dumped a stack trace, which is why it could never join a wave. It now SKIPs
cleanly naming its precondition. gates:post 35 -> 36, README re-synced. Verified
both ways — 36/36 with a server, and "0/1 passed, 1 SKIPPED" without one, which
is visible only because of the runner change in the previous commit.
The language link was fixed last change; the country link had the identical defect and
answers the brief's SECOND requirement. "Every resource used in Indonesia" was an
<a href="../data-tables/index.html?tab=links&country=IDN">, so inside the composite,
clicking a country to see its resources navigated away from the composite to do it.
Both controls now split the same way: a button that filters the tables below when they
are on screen, an anchor when they are not.
Verified against an offline expectation rather than read off the page. Counting the
links table for rows whose country_codes contain IDN gives 400. In the dashboard the
control is a BUTTON, the URL stays on gospel-dashboard/index.html, and the banner reads
"Showing 400 of 6,498 resources for Indonesia" — the same 400. Standalone it is still an
<a> to data-tables/index.html?tab=links&country=IDN&label=Indonesia, so nothing changed
for the map on its own page. No page errors on either.
Screenshot read: the filtered rows are real Indonesian-language resources across five
sources — JESUS Film, Faith Comes By Hearing audio and Bible video, Cru's Four Spiritual
Laws, eBible and Zume — with a "Show all" escape beside the count.
run-parallel grades on the exit code, and a gate that declines to evaluate
exits 0. So "35/35 passed" read the same whether 35 gates graded something or
31 did, and the summary line — the one people quote — was the most misleading
part of the output.
This is the generalisation of the packet-sweep fix two commits ago. That closed
ONE always-skipping gate; the reason nobody could see it for months was that the
runner reported it green. 18 of the 35 steps in gates:post CAN skip, every one
on a precondition a fresh checkout does not have, so the gap is structural
rather than specific to packets.
Measured on a genuinely fresh checkout — `git archive HEAD | tar -x`, which is
what CI sees. Before: "31/35 passed". After: "28/35 passed, 4 SKIPPED", naming
check:packets-all, check:zone-census, check:done-evidence, check:backlog-status.
All four are legitimate, and I checked rather than assumed. packets-all skips
until packet:ci runs, which CI does first. zone-census needs generated posters.
The two backlog gates read _qa-cycle, which lives outside any repo by the
deliberate decision that PM markdown never sits inside a code repo — and both
already say so in their own output. So the finding is the REPORTING, not a
broken gate.
Exit semantics are deliberately unchanged, and verified: a wave whose only step
skips still exits 0. Failing on a SKIP would fail every CI build on the backlog
gates and teach people to disable the reporting.
The detector is mutation-tested against seven real output shapes. It matches
"ZONE-CENSUS: SKIP — …" and the indented variant, and correctly does not match
prose mentioning skip, nor "SHARE-POLICY: PASS — 12 buttons, 0 skipped".
A correction to my own method along the way: I first censused skips in
doxa-maps-plugin-studio as a proxy for a fresh checkout. It is not one — it sits
many commits behind main with a 21-step gates:post that lacks 14 of the steps
main has, so the two censuses were of different waves. The fresh-archive test
replaced it.
Also documents check:zone-census's precondition, the one conditional gate in the
wave that had none — including why it cannot be fixtured the way packets-all
was: it asserts coverage of real generated output, so a fixture would only
assert that the fixture is complete.