Commit graph

172 commits

Author SHA1 Message Date
doxa-maps-88655c70
1cbfb3aadf WIP: Simple Research one-click export (toolbar button, shared poster path, engine-aware hidden map, gate) 2026-09-25 12:15:20 +00:00
doxa-maps-88655c70
cd53582ea4 The shared poster wall's locale is asserted, in locales that can show a leak
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
UPG-100 carried a ❌ for a PosterWall i18n leak: useI18n() said to throw
where the wall mounts, leaving the print buttons English in every
locale on upg-100-mfe, poster-wall and country-poster-wall.

IT DOES NOT REPRODUCE. Measured on the current build, three leaves x
ro/ar/zh, tree identity checked against the served manifest first: zero
English leaks, zero page errors. ro "Listă de rugăciune / Harta afișului
/ Pachet de cartonașe", ar "قائمة الصلاة / خريطة الملصق", zh "禱告名單 /
海报地图", legend "Opțiuni de tipărire" / "خيارات الطباعة" / "列印選項".
The diagnosis does not fit either: this bundle installs i18n per element
via configureApp, and createI18n is legacy:false, so useI18n() resolves
for every descendant including the wall.

THE REAL DEFECT WAS THE MISSING ASSERTION, and it is worse than the bug
would have been: nothing in the repo checked the shared wall's RENDERED
locale, so a ❌ could sit on the scoreboard with nothing able to confirm
or refute it.

★ AND IT CANNOT BE CHECKED IN ENGLISH. The wall's guard HUMANIZES the
key when useI18n() throws - buttons.prayerList becomes "Prayer List" -
which is deliberate, so a bare host shows readable English rather than a
raw key. It also means "the button says Prayer List" is true both when
i18n worked and when it threw. An en-only check passes forever while
every other locale leaks. This gate therefore runs ONLY in non-English
locales, where the translated string differs from the humanized key and
seeing the key IS the defect.

It covers FIVE consumer leaves, not the three the report named:
key-cities and regions-dashboard mount the same shared wall and were
never checked. A gate that copies a report's sampling inherits its
blind spot.

TWO WAYS IT COULD HAVE PASSED WITHOUT CHECKING ANYTHING, both real on
the first run and both now hard skips rather than green ticks: a wall
with no cards renders no chrome, and regions-list rendered 7 cards with
ZERO wall chrome because that profile mounts the wall without the print
row. Skips are named in the output with their reason - a summary that
counts only the green ones reads as full coverage.

MUTATION TESTED by reproducing the reported condition exactly - forcing
the useI18n() guard to throw - rather than corrupting a string: the gate
goes red with 28-33 leaked strings on every measured combination, which
proves both that it detects the defect and that the defect is genuinely
absent from the unmutated build. Restored, 9 combinations PASS, 6
reported as not covered.

No source change: there was nothing to fix.
2026-09-25 10:50:46 +00:00
doxa-maps-88655c70
c77207b53a check:basemap-labels: the basemap's place names follow the reader's language on both engines
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
BUILD-LOCALIZATION-SURVIVES-THE-PROVIDER-SWAP asked for a check; the
behaviour landed in 319064a1 (library/maps/localize-labels.js) without
one. This reads the labels the map actually DRAWS, at ?lang=en and
?lang=ru on the same view, under both providers:
  Mapbox    the Standard basemap's 'place-labels' featureset (a plain
            queryRenderedFeatures cannot see the imported basemap):
            Germany→Германия, Ukraine→Украина, Egypt→Египет… must flip.
  MapLibre  each country symbol's evaluated text-field: under ru it must
            be the feature's name:ru, under en not. English there
            restores OSM's own label, so no English word is expected.
It serves this checkout on an OS-assigned port. --dir points it at
another tree for the negative control.

Negative-tested: doxa-simple-map built from 319064a1's parent (no
localize-labels) and served with --dir FAILS on both engines (Mapbox:
68 country labels, none flip; MapLibre: 0/49 in Russian). This tree
PASSES on both (5 known countries flip on Mapbox; 49/49 on MapLibre,
e.g. United Kingdom → Великобритания). A first version counted the
pre-fix Mapbox run as NOT MEASURED instead of FAIL, since it saw no
English names. "Measured" now means enough country labels were drawn.
2026-09-25 10:48:42 +00:00
doxa-maps-88655c70
78efae4eae Integrate main into bundler-2/work
# Conflicts:
#	public/doxa-maps/dashboard-plus-grid/dashboard-plus-grid.js
#	public/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/default/index.html
#	public/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html
#	public/doxa-maps/doxa-maps-spa/manifest.json
#	public/doxa-maps/doxa-research-map/doxa-research-map.js
#	public/doxa-maps/doxa-research-map/research-map/index.html
#	public/doxa-maps/doxa-research-map/research-map/maplibre/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 10:20:57 +00:00
doxa-maps-88655c70
16629f4906 Link religion and language between the map and the grid, by the one table they already share
A reader who picks a religion family on the map now sees the grid narrow to the
same peoples, and a language selection travels the same way. Region already
linked; these two did not, because the two halves do not name a religion the
same way and nothing yet proved what the honest translation was.

The map filters religion through its legend's flat tab, keyed by the FIRST
LETTER of the religion code ('B'); the grid filters through a derived axis that
runs library/colors/religion.js forward per row ('BLA' -> 'Buddhism'). One
shared table, read in opposite directions, copied by neither half — so the wire
carries the family NAME and each side resolves it against its own data.

Two measurements changed the design on the way:

  - mapStore.filters.religions, which the map's filter panel writes, is read by
    NOTHING: the mapLayers.applyFilters() it was meant to reach is called and
    never implemented anywhere in the tree. A link routed through it would have
    set state, passed a smoke test and dimmed no pin.
  - the map's legend selections are mutually exclusive by design, so it can
    light exactly one of region/religion/language. The composite mirrors that
    rather than letting the grid hold three facets the map cannot show.

Religion links at the FAMILY and not at the code, deliberately: a map cannot
distinguish 'BLA' from 'BMA' — both dim as Buddhism — so linking at the code
would promise a precision one half cannot keep.

check:linked-vocabulary proves it by measurement rather than by reading: it
compares the total reached over the composite's wire against the total the grid
gives for the same facet on its own (83 of 2,107 both ways), and runs the MUTANT
first — the legend key 'B' instead of 'Buddhism' — failing as NO-TEETH if the
wrong mapping scores the same as the right one. Measured: mutant 0, real 83.

The CLI doc's apk:debug quote was realigned in passing; package.json had gained
app:start-page and the doc still described the older chain.
2026-09-25 10:20:27 +00:00
doxa-maps-88655c70
59e9695aaa Correct the record: the dead bundle was my merge, not a duplicate import
dx-shepherd's verifier traced it. Merge 52ab6089 from this worktree committed
public/doxa-maps/gospel-resources/gospel-resources.js with 21 conflict markers —
both sides concatenated, 1.13 MB — and that is what threw "Identifier Ge has
already been declared" and killed every gospel leaf.

My diagnosis last commit was wrong. I blamed a module imported twice and
re-routed the import; that change was harmless but it was not the cause. The
REBUILD that came with it is what repaired the file. A plausible cause that a fix
appears to confirm is the easiest kind of wrong answer to believe.

THE ACTUAL CAUSE IS A LANDING SCRIPT I WROTE. On a conflict it resolves the one
file it expects — public/doxa-maps/index.html — and then runs "git add -A
public", which stages every OTHER file under public/ including the ones still
holding markers. One line of convenience turning a conflict git correctly refused
to guess at into a committed artefact.

So: a check that names the real condition. It scans every tracked file for an
unresolved marker, is built from character codes so it can never trip itself, and
carries a poison arm. It runs FIRST in check:gospel-all, because there is no
point measuring anything else about a tree that is mid-merge.

Also verifies the 8 links the GRN recheck recovered, which check:gospel-all
caught as shipped-but-unverified on its very first run — psi, alk, nos, jle, lma
and three more, all now confirmed landing on their own 5fish language pages.
2026-09-25 10:19:07 +00:00
doxa-maps-88655c70
57fecfd0da A gate that asks whether anything is there — and it caught a dead bundle
Wrote this after shipping a page that returned 200 for every asset, reported
zero console errors, and was completely dead: a throw during setup dies inside
the custom-element boundary. Zero errors is not the same as working, so this
does not trust their absence — it looks for PRESENCE, on all five leaves, in
five steps that each name their own failure: the tag is defined, the host is in
the document, a shadowRoot is attached, it has children, it renders text.

Its first real run went red on all five, and it was right. Every gospel leaf was
dead with "Identifier 'Ge' has already been declared" — while the SRI hash
still matched and every request still returned 200. My own doing: four profiles
imported en/gospel.json directly for an English fallback, and library/i18n
already pulls that exact file in through an eager glob, so the IIFE carried two
instances of one module.

The fix is one module: library/i18n now exports EN_MESSAGES and the profiles
read it from there. The duplicate is gone.

Measured after: all five leaves mount with 3,228 to 233,307 characters of
rendered text, verify-gospel-map passes with 2,107 pins and the gap cutting to
370, and --poison confirms the check goes red on a page with no element at all.

The lesson is the gate's whole reason to exist. I had rich assertions about pin
counts and filters and none of them ran on data-tables, charts or the list, so a
bundle-level failure could take out every leaf while the one gate I did run was
pointed elsewhere. Cheap and universal beats rich and narrow for THIS question.
2026-09-25 09:52:25 +00:00
doxa-maps-88655c70
60b3650304 The phone app opens the UPG-100 list, not the plugin studio
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
repo reported it: the Capacitor app launches public/index.html, which
mounts <plugin-studio>. A recipient opening the phone app meets the DEV
STUDIO rather than the map the README promises. Reproduced in the synced
Android assets before touching anything.

RULED (Kingdom-agent, card DECISION-MOBILE-EXAMPLE-IS-UPG100-NOT-SIMPLE-
RESEARCH, done): the ONE mobile-app showcase is the UPG-100 list.

⚠ THE CARD'S OWN BLUF SAYS "Simple Research" AND IS STALE. It predates
that ruling, which is recorded DONE in the same backlog and says Simple
Research "was never mobile-friendly". Building to the card's wording
would have shipped the app the ruling rejects, so this builds to the
ruling and the card is annotated with why.

WHY IT RUNS AFTER cap sync AND WRITES ONLY INTO THE ANDROID ASSETS.
public/index.html has THREE consumers, which capacitor.config.json
documents: Railway/CDN, Capacitor, and local staging. The studio is
CORRECT for the CDN root and for `bun run serve` — only the phone build
wants a different landing page. Rewriting the shared root would fix the
app by breaking the other two. cap sync copies webDir into
assets/public/, so overwriting the COPY changes what the app opens and
leaves the served tree untouched. The studio is kept at studio.html
rather than deleted; a developer with the apk still wants it.

WHY IT COPIES THE BUILT LEAF RATHER THAN HAND-WRITING A HOST PAGE.
The leaf carries an SRI integrity hash. A hand-written page needs its
own correct hash, and a stale one makes the browser REFUSE the script
with no console error: the element never upgrades and the app is a blank
screen. Copying the artifact the build already verified means there is
no second copy of that hash to drift — confirmed, the hash still
matches after the rewrite. Only the relative depth changes, and the
script asserts every rewritten reference resolves to a file that exists
rather than trusting its own regex.

MEASURED by serving the ANDROID assets tree (not public/, which is the
whole point) on a private port, with the app-root marker checked before
any number was read:
  390x844   mounted+upgraded, studio absent, 27 poster cards, 0 4xx, 0 errors
  1280x900  same
check:app-start-page is mutation-tested: restoring the studio page as
the app root makes it exit 1 naming <plugin-studio>.
2026-09-25 09:11:01 +00:00
doxa-maps-88655c70
27151f39a6 Research Gallery: boot on a partner page under the CORS the live server actually sends
The doxa.life script embed was blocked on a server change: the live root
manifest.json answers without Access-Control-Allow-Origin, and the gallery
fetch()es it cross-origin, so no tab could mount. Measured on nuxtbase,
though, the same static layer DOES send ACAO on every path inside a folder
of the tree (doxa-maps-spa/<anything>, even a 404; assets/…).

So generate-tree writes a byte-identical manifest copy at
doxa-maps-spa/manifest.json in the same step as the root one (they cannot
diverge). The gallery reads it FIRST when the tree is on another origin (no
CORS error in the host's console) and the root first when same-origin.
Script URLs still resolve against the tree root, never the copy's folder.

check:gallery-embed:live-cors serves the tree with the live rule
(serve-strict LIVE_CORS=1: no ACAO on the root manifest only).
- The current gallery under it: "did not boot within 60s" (negative
  control, reproducing doxa.life).
- With this change: js 14, iframe 14, neighbour 14, linked 4. The default
  gate also passes all phases.
check:embed-targets, check:drop and check:public-hygiene pass with the copy
present.

The server-side CORS rule (serving/traefik-embed-headers.yaml) is still the
right long-term fix, and it needs server access. This unblocks the embed
without it once the tree is published.
2026-09-25 08:55:52 +00:00
doxa-maps-88655c70
b32795aa0e Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	package.json
#	public/doxa-maps/index.html
2026-09-25 07:22:18 +00:00
doxa-maps-88655c70
b78e7d1b52 Prayer Walk Planner: fill the page, and print a real walking map
The Driver: "the prayer walk planner is still a very tiny map, not
expanding the full page", and the printed map was a ~10px sliver.

Tiny map. Measured: the leaf filled (1003x739), but inside the Research
Gallery the map was 747x76 on desktop and 390x0 on a phone. The planner
mounts its SFC as the custom-element root (that is what puts its client
API on the element). So Vue's attribute fallthrough copied the gallery's
inline host style onto the root .pwp, and that inline display:block beat
.pwp{display:flex}. inheritAttrs:false keeps host attributes on the host.
The map now fills at 1280x800 and 390x844, in the leaf and in the gallery.
The gallery gets the same guard: its .vtl had survived only through
display:grid !important.

Sliver print. Print used to snapshot the LIVE map, so the page got that
map's on-screen size and shape. Each zone now renders in its own
temporary MapLibre map:
- page-shaped (1200x1344 at pixelRatio 2, a 2400x2688 image, ~320 dpi on
  the 7.5x8.4in frame)
- keyless OpenFreeMap liberty, with streets and building footprints
- fitted to the zone, with the zone outlined on top
- destroyed once its pixels are taken, and the reader's map never moves
One zone = one letter page, with the map filling it.

Localized. The planner read no language at all. It now follows
language → profile-config locale → ?lang → <html lang>, in all 11
locales (private src/strings.js): toolbar, list, status, default zone
names, and the printed sheet (lang/dir set, so Arabic prints RTL).
Street and place labels prefer the OSM name:<lang>. Two Arabic defects
were found by reading the printed sheet, and fixed:
- coordinates were bidi-scrambled → isolated LTR
- map labels were unshaped glyphs → the RTL text plugin is installed
  for MapLibre, from jsDelivr, and awaited before the print frame
  (library/maps/rtl-text.js only does it for Mapbox)

renderZoneMap(id) joins the client API: the print PNG without the dialog.

New gate check:prayer-walk-planner:
- 4 size cells (leaf/gallery × desktop/phone)
- a real zone in Balat, Istanbul renders a >=2000px non-blank print map
- a Spanish toolbar
- 0 page errors
Both gates now prove the served tree is this checkout's (manifest equals
disk) before measuring, and exit 75 NOT MEASURED otherwise.
check:gallery-embed's Big Picture link now asserts that the grid shows
exactly the facet's own count.
2026-09-25 07:21:42 +00:00
doxa-maps-88655c70
33bd7ca387 All People Groups loads fast on a light map; the Vision Card becomes a true funnel from data
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
ALL PEOPLE GROUPS. The Driver: "not even loading, takes a very long
time", and its DOXA pins were dark on a dark map.
- It downloaded the 7.5 MB people-groups-slim.json (22 columns) to draw
  ten fields. It now fetches people-groups-points.json: the same rows in
  column order, repeated strings in dictionaries, coordinates at 3
  decimals. 0.74 MB, generated deterministically by
  library/scripts/people-groups/build-points.mjs and staged into the tree
  by generate-tree. It falls back to the old file on an older tree.
- Pins no longer wait for the live prayer API. Every pin draws by GSEC
  from the file, and the DOXA overlay recolours when the API answers. The
  DOXA count reads "…" until then, not 0.
- Named style 'paper' (Mapbox light-v11 / MapLibre positron) instead of
  'ink'. The four categories keep their meaning; yellow and green are a
  step darker, and pins get a thin dark ring, so they read on near-white.
- Its words are in all 11 locales (allPeopleGroups.json), with a guarded
  useI18n and values passed to vue-i18n so {n} is not blanked.
Measured on a served built tree, 3 fresh runs each, median time to first
rendered pins: 8.3 s before, 5.2 s after, on localhost. This box renders
GL in software, so most of the remaining time is map start-up; the 10x
smaller download matters more on a real connection.

DOXA VISION CARD. The Driver: "the data doesn't make sense … super false
information". The card rotated four typed-in figures with no source.
It is now a proportional funnel, every number computed from data by
library/scripts/people-groups/build-vision-funnel.mjs:
  12,379 people groups   (IMB rows: one group in one country)
   7,341 unreached       (IMB LostnessPriority 0-2, < 2% evangelical)
   4,754 frontier        (LostnessPriority 0, < 0.1% evangelical)
   2,107 DOXA groups     (the DOXA prayer list, snapshot saved in data/;
                          1,745 of them frontier)
The first three nest; DOXA groups do not all sit inside frontier, so the
card says how many do. The rules are on the card ("How these are
counted") and in HOW-THE-NUMBERS-ARE-COUNTED.md, which also explains why
the total is not the 16-17 thousand of Joshua Project's count. Material
light, all 11 locales, right-to-left in Arabic, sources and dates shown.
The old fake stats are removed from the instances file, the frontier
page, the manifest, the staging page and the gallery.

CHECKS.
- check:vision-card-numbers recomputes the funnel independently (own CSV
  reader, the published rules) and compares with vision-funnel.json and,
  with --url, the rendered bars. Negative-tested: a corrupted count exits
  1. On the built tree: bars 12,379 / 7,341 / 4,754 / 2,107 and "1,745 of
  them are frontier", all matching.
- check:map-embed-cells measures PRODUCTION-MATRIX cells B and C for the
  seven gallery maps on a foreign origin; C proves the host's token is
  used by handing the host a distinct token and checking every Mapbox
  request. 14 of 14 pass.
2026-09-25 07:18:30 +00:00
doxa-maps-88655c70
5516c157bb Merge main into bundler-2/work
# Conflicts:
#	public/doxa-maps/country-posters/countries-map/index.html
#	public/doxa-maps/country-posters/country-poster-wall/index.html
#	public/doxa-maps/country-posters/country-posters-mfe/index.html
#	public/doxa-maps/country-posters/country-posters.js
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 07:14:11 +00:00
doxa-maps-88655c70
6be1395d1f Cell C has a gate now: the composite must filter, not navigate
The claim worth testing about the composed dashboard is not that it renders. It
is that it keeps the reader INSIDE it. Standalone, "Every resource in
<language>" is a link to the tables page; composed, it must be a button that
filters in place, because navigating away throws out the map the reader just
positioned and the group they just found. Same words, two affordances, one right
in each context.

Six assertions, each a way the composite can look fine and be broken: the
control is a <button> not an <a>, the URL does not change, the table's row count
goes DOWN, it does not go to zero, the pin count survives, and nothing 4xx'd or
threw.

It picks its subject from the live source rather than by position, and that is
not a detail. The first run opened pin 0 — Abai Sungai, a group with nothing
found — so the filter correctly produced an empty table and the check failed a
working composite. The test was wrong, not the product. It now opens a tier-2
pin, so "narrowed but not emptied" is a question the subject can answer.

Measured: 2,107 pins and 50 rows before, then Telugu narrows the tables to 13
with the map intact and the URL unchanged.
2026-09-25 06:32:02 +00:00
doxa-maps-88655c70
9a18dee1c2 Merge main into bundler-2/work
# Conflicts:
#	package.json
#	public/doxa-maps/country-posters/countries-map/index.html
#	public/doxa-maps/country-posters/country-poster-wall/index.html
#	public/doxa-maps/country-posters/country-posters-mfe/index.html
#	public/doxa-maps/country-posters/country-posters.js
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
2026-09-25 06:12:10 +00:00
doxa-maps-88655c70
ce1bce582a Gate runner: one rendezvous for everyone, and a load ceiling that opens
TWO LOCKS, FIXED ORDER. Browser gates queued on a product-named lock while every
agent's hand-run probe queued on a differently-named one, so gates and probes
were not serialised against each other at all — two queues, one machine, and the
dogpile the lock exists to prevent. The runner now takes BOTH, always product
first: flock can exec flock, so nesting is free, and a fixed order means two
processes can never take them in opposite orders and deadlock. Either file alone
still serialises everything that uses it.

The repo cannot simply adopt the other name: it carries an internal code name and
check:no-codename refuses to let a repo we are handing over contain one. Taking
both paths is how the rendezvous becomes shared without the repo spelling a word
it must not ship. VERIFIED in ps while a wave held both, another agent's probe
was waiting on the same file.

THE LOAD CEILING WAS UNREACHABLE, so the guard only ever cost time. It was set at
12 when the risk was seven agents starting chrome at once — but the FLOCK is what
prevents that; the load check is belt-and-braces on a belt. Measured today with
several agents working: 1-min load sits at 36-50 all day, so every browser gate
waited out its full 900s cap and ran anyway. That is a 15-minute tax per wave for
a guard that never opens, and its real effect is that people stop running browser
gates. Now cpus().length * 5 (40 here), scaled because 40 means something
different on 8 cores than on 64, with a 120s cap. Both still override by env.

Also: check:stale-sweep-claim advertised --poison inside an inline comment it can
never receive; the flag gets its own entry and the parent stops advertising it.

gates:pre 7/7, gates:post 42/44 with the two known precondition skips.
2026-09-25 06:10:56 +00:00
doxa-maps-88655c70
0c1a91cd02 useMapInstance: a map unmounted mid-initialise returns quietly instead of throwing
Some checks are pending
build-gate / build (push) Waiting to run
initializeMap is async. If its component unmounts while it waits (a
studio tab switch, a composite swapping views), Vue nulls containerRef
and the next dereference threw "Cannot read properties of null (reading
'getRootNode')" from ensureEngineCSS. check:studio-all hit it about 1
run in 4 (gospel-dashboard); gospel-plugin found a second site one await
later (containerRef.value.clientWidth after the animation frame).

initializeMap now checks the ref after each of its three awaits and
returns silently when it is gone, and ensureEngineCSS returns on a null
element. An unmounted pane is normal, not an error, so nothing is
logged. The ref is the only liveness signal: requiring isConnected too
would silently stop maps that mount into a subtree before it is
attached.

Proof, deterministic in both directions:
- check:map-unmount-mid-init holds mapbox-gl.js, opens a map plugin in
  the studio, switches away (destroying the pane), then releases the
  script. Before: FAIL 2/2 runs (research-map, doxa-simple-map threw
  the production error). After: PASS 2/2 across research-map,
  gospel-dashboard and doxa-simple-map.
- check:map-unmount-race (gospel-plugin's browser-free test, adopted):
  the guarded ensureEngineCSS resolves; with the guard stripped it
  throws the exact message.
- No regression: check:studio-all passes 31/31, and 19 plugins still
  create a live map canvas, as before.

check:shared-reached now tracks this change with a regex marker (a
control-flow fix has no literal that survives minification). It
reports 12 consumer bundles still to rebuild. Verified: rebuilding
doxa-simple-map moves it to "carries it".
2026-09-25 05:01:13 +00:00
doxa-maps-88655c70
6fe55fdba2 package.json: move every script's inline comment out of its command
Some checks are pending
build-gate / build (push) Waiting to run
89 of 181 scripts ended their command string with a "   # …" note. The
shell treats everything after # as a comment, including the arguments
`bun run <script> -- …` appends, so those scripts could never take a
flag. A gate given --url silently measured its hardcoded default port.

Measured before the change: `bun run smoke:studio -- --url
http://127.0.0.1:5477` ignored the URL and graded :5173, another
agent's server running a days-old studio. After: the command runs as
`node internal/smoke-studio.mjs --url "http://127.0.0.1:5477"` and
measures the requested server. check-script-flags-reachable, which
failed on main for exactly this reason (check:stale-sweep-claim
--poison dropped by its comment), now passes.

The notes are kept, verbatim, in a top-level "//scripts" object keyed
by script name. They are not sibling "//name" keys inside "scripts",
because those would themselves be runnable scripts whose command is
prose. Every command string is otherwise byte-identical.
2026-09-25 04:34:31 +00:00
doxa-maps-88655c70
e6f328b40b check:studio-all: open every plugin in the studio, not a sample
Some checks are pending
build-gate / build (push) Waiting to run
smoke:studio samples 4 tabs. On 2026-09-25 a sweep of all of them found
people-groups-grid rendering 2 DOM nodes behind a page error, and 13
plugins mounting with their data 404ing on a fresh clone. The sample
had passed.

  bun run check:studio-all -- --url <studio page>

Opens every plugin row in the studio's tree and fails a plugin on the
studio's error overlay, a page error or failed request while it is
active (Mapbox hosts excluded), or a near-empty mount. Graded by exit
code. --url is required, because hardcoded :5173 gates have measured
another worktree's tree. The package.json entry carries no inline
comment: a `# …` in a script string swallows the arguments bun passes.

Negative-tested: re-introducing the unguarded useI18n() in
FilterDropdown fails it, naming people-groups-grid, research-map-plus-grid
and stack-demo. Against today's dev server: PASS, 31 of 31, in 3 of 4
runs; the fourth caught an intermittent gospel-dashboard page error,
reported to its owner.
2026-09-25 04:30:09 +00:00
doxa-maps-88655c70
c3d1df30dc The click-sheet verifier had not run in months, and was aimed at the wrong files
It crashed on ENOENT — app/doxa-maps/gospel-resources/assets/tables/, a tree
deleted when data moved out of public/. Nothing noticed, because nothing ran it:
it had no npm script, so it was neither discoverable nor wired anywhere.

The dead path was the smaller half. Its other input was a private by-country
copy under plugin-designs/, and its people-group table was the curated 2,106-row
subset that resources-map.vue stopped fetching some time ago — it says so in its
own comments; the country sheet is built from the 12,379-pin index now. Repaired
in place it would have gone green about a surface it was no longer looking at.

Both inputs are the live tables. The country join is ISO3 instead of a substring
test against a comma-joined name blob, which the file's own comment admitted was
under-counting because country names contain commas. It reads 242 of 242
countries resolving to a pin and 8,435 sheet links, through the same flattener
the sheet renders from, and its self-test now requires four corruptions to go
red rather than two.

Also gives it and the stale-sweep gate an npm script, and corrects the link-rot
entry, which still told readers that jesusfilm.org and fivefish.org can never be
status-checked — the thing that gate stopped believing two commits ago.
2026-09-25 03:33:31 +00:00
doxa-maps-88655c70
852310d05e embedBase: the asset pool is INSIDE the tree, not beside it
Some checks are pending
build-gate / build (push) Waiting to run
resolveAssetsBase computed `<treeRoot>/../assets` -- one path segment too high.
This reached the live site. MEASURED by publishing to the catalog and reverting:

  leaf  .../doxa-maps/my-upg-100-list/upg-100-list/index.html + '../../assets'
                            -> /plugin-assets/doxa-maps/assets   200
  code  root '/plugin-assets/doxa-maps' + '/../assets'
                            -> /plugin-assets/assets             404

The UPG-100 map went from "195 polys · 27/27 zones · 2107 pins" to
"⚠ boundary fetch 404 world-adm0.geojson" -- a basemap with none of our data on
it. Reverted within minutes; live is healthy.

★ THE COMMENT JUSTIFIED THE OFF-BY-ONE IN PROSE, which is how it survived. It
said the pool is "a SIBLING of the tree -- <root>/../assets -- which is why the
leaf page's generated value is '../../assets' (two up from
<tree>/<bundle>/<profile>/)". Two up from <tree>/<bundle>/<profile>/ is <tree>,
not the tree's parent. The arithmetic was wrong and the sentence made it read
deliberate. generate-tree stages assets INSIDE the tree on purpose -- "staged N
runtime asset(s) -> doxa-maps/assets", so a dropped folder is self-contained.

⚠ WHY FIFTEEN TICKS OF GREEN GATES MISSED IT, three independent reasons:
  1. INVISIBLE LOCALLY. The dev web-root has BOTH public/assets (a sibling) and
     public/doxa-maps/assets (inside), each holding the boundaries -- so the
     wrong path RESOLVES on 127.0.0.1. The catalog publishes app/doxa-maps only;
     no sibling exists there. No local run can tell the answers apart.
  2. THE BYTE GATES PASSED. check:published and check:catalog-currency both went
     GREEN on the broken publish -- 22 of 22 byte-identical, 0 stale. They prove
     the bytes match, not that the page works.
  3. NO GATE REFERENCED THE FUNCTION, while 10+ profiles across 6 bundles call
     it.

So the fix ships with check:embed-base, which is PURE ARITHMETIC -- no browser,
no network, no served tree, so it cannot SKIP itself into irrelevance. It
computes what a leaf's own '../../assets' means and asserts the resolver agrees:
the oracle is what the generated HTML already says, not an opinion about where
assets belong. It also pins the explicit ladder rungs, so a narrow fix cannot
swallow a host-supplied assetsBase.

MUTATION-TESTED: restoring the original '/../assets' fails all four tree roots
(live catalog, dev, doxa.life drop-in, deep subpath) in milliseconds.
2026-09-25 01:30:56 +00:00
doxa-maps-88655c70
953fe79002 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-25 01:23:35 +00:00
doxa-maps-88655c70
92d236106d Re-test links we already called checked
The verification table records that a URL resolved on the day it was
checked, and nothing ever re-tests one that passed. A publisher can
retire a page and this map will go on offering it indefinitely, still
labelled "link checked". Clause L guards the freshness of the record;
it cannot notice the web moving underneath it.

This samples 153 links — 25 per host at a fixed stride, so the same
run is repeatable — and today every one of them still resolves.

THE PART WORTH KEEPING is which hosts it refuses to check. Four of the
eleven never 404: jesusfilm.org, live.bible.is and knowgod.com serve
an identical shell for any path, and fivefish.org answers a nonsense
path with a plausible geo-located page at HTTP 200. Re-checking those
by status would report perfect health forever, which is worse than not
checking them, because it would look like evidence. They are verified
by publisher API or page title in the main verifier instead.

Three impossible URLs run in band every time. If one answers 2xx the
check reports PROVED NOTHING and exits non-zero rather than passing.
That mattered immediately: the first version sampled one link per host
instead of 25 and still said PASS — a green that meant almost nothing,
caught only by reading the count rather than the verdict.
2026-09-25 01:09:08 +00:00
doxa-maps-88655c70
bc368dadda gate: a key copied into all 11 catalogs is not a translated key
Some checks are pending
build-gate / build (push) Waiting to run
check:i18n-parity asserts every locale carries every key -- and a key can pass
that perfectly while holding the English string in all eleven files, which is
what "translated" was supposed to mean. Presence and translatedness are
different properties, and only the first was measured.

Swept every catalog: exactly THREE keys are byte-identical across all locales,
out of 210 compared. Finding three rather than three hundred is what makes this
worth gating -- it is signal, not nagging.

★ THE DEFECT IT FOUND. legend.columns.upgs is the literal string "UUPGs" in all
11 catalogs, while `population` sitting beside it IN THE SAME HEADER ROW
translates normally -- Población / 人口 / Население / السكان. The legend renders
a fully localized row with one English acronym wedged into it, and prayerLaps
carries the same string.

AND THE PRODUCT HAS ALREADY RULED ON THE CONCEPT, which is what makes this a
defect rather than a preference: packet-strings.json translates the same idea in
prose in every locale -- "unreached people groups" becomes grupos étnicos no
alcanzados / 未得之民 / неохваченных народов. The legend disagrees with the packet
the legend generates. Two honest surfaces, one vocabulary, no agreement.

⚠ IT DOES NOT PROPOSE TRANSLATIONS, and that restraint is deliberate. Drafting
candidates here produced invented initialisms -- PNA, UEVG, ННГ -- assembled
from translated phrases with no source establishing any of them as the field's
term. A wrong term looks AUTHORITATIVE to a reader who cannot check it, which is
worse than an honest English acronym. The gate names what is untranslated; a
human who knows the vocabulary supplies the words.

⚠ AND IT CARRIES AN ALLOWLIST, because some keys are correctly identical: pure
interpolation formats, brand names, ISO designations. The sweep found one --
legend.progress.segment is "{label} · {count} · {pct}%", all placeholders. Each
entry needs a REASON, so the list reads as a record of decisions rather than a
silencer.

MUTATION-TESTED THREE WAYS: making ONE locale match English is correctly NOT
flagged (it requires all of them, so a genuinely translated key cannot trip it);
making a key identical in ALL eleven IS flagged; and allowlisting a real finding
moves it from the failure list to the allowed list, 2 down to 1.
2026-09-25 01:06:08 +00:00
doxa-maps-88655c70
8fbc9beacc gate: the tree we publish has a weight, and nothing was watching it
Some checks are pending
build-gate / build (push) Waiting to run
A publish was ALREADY BLOCKED once at a 74MB asset tree -- Forgejo rejected the
pack. Nothing has measured it since. public/assets is now 132MB, against the
94MB recorded when BUILD-POSTERS-GENERATE-ON-DEMAND-DROP-52MB was written. It
grew ~40MB and no one noticed, because size only becomes visible when something
refuses to accept it.

★ IT REPORTS THE SPLIT, NOT A TOTAL, because a total invites deleting the wrong
half:
    city-*.webp     176 files  22.0MB   NOT reproducible -- needs OSM tiles
    country-*.webp  174 files  13.2MB
    zone *.webp      28 files   5.7MB   reproducible
    zone *.html      28 files  27.9MB   reproducible
The city posters are simultaneously the heaviest single group and the one that
cannot be regenerated offline, which is why publish-html.mjs deliberately skips
them. "132MB" on its own points an optimiser straight at them.

⚠ AND RAW SIZE IS NOT THE SHIPPING COST -- the gate says so in its own output.
publish-html.mjs already measured this with real git packs: swapping the 28 zone
rasters (5.0MB raw, ~5.0MB packed; webp is already compressed and git cannot
pack it further) for their HTML sources (21.4MB raw, 5.78MB packed; repetitive
text) is about +0.8MB PACKED. A net LOSS. Its comment says "do not ship that
swap and call it a saving", and a gate reporting raw bytes without that caveat
would have re-argued for exactly the swap that file talks you out of.

The ceiling is 160MB -- roughly 20% headroom over today, far above the current
tree and far below a surprise repeat of the 74MB rejection. Deliberately not
pinned to today's value: a gate that fails on the next legitimate asset teaches
people to raise the number instead of reading it.

MUTATION-TESTED BOTH WAYS: a ceiling below the current tree fails it (exit 1)
with the publish-rejection history in the message; a generous ceiling passes
(exit 0). The poster buckets were checked exhaustive -- 406 of 406 files
categorised, so nothing can hide between them.
2026-09-25 00:20:02 +00:00
doxa-maps-88655c70
2f96f04b49 packets: fixtures at the PAGE BOUNDARY, which nothing had ever rendered
Some checks are pending
build-gate / build (push) Waiting to run
check:packets-all reported "every generated packet renders, in every locale
present" and passed on 11 packets. All 11 were the SAME ZONE (laos) in 11
locales. It proved one zone's layout eleven times.

The answer is not more zones -- 28 zones x 11 locales is a network fetch per
zone and ~100MB of artefacts. It is more SHAPES, chosen where the layout can
actually break, built from committed records with no network.

★ THE SHAPE THAT MATTERS IS THE PAGE BOUNDARY. packet-render.js:76 sets
PER_PAGE = 20 and paginates `for (start = 0; start < groups.length; start +=
PER_PAGE)`. laos has 95 groups and tibet 43 -- both mid-range remainder cases,
so the boundary ITSELF had never been rendered. Three fixtures, each a slice of
the committed tibet records, so they are byte-deterministic and need no API:
  exactly 20 -- one full page; an off-by-one yields a second, empty one
  21         -- two pages, the second holding a single card
  1          -- the minimum a zone can have

MUTATION-TESTED, and this is the argument for the whole change. Flipping the
page loop to `start <= groups.length` -- an off-by-one that only shows when the
count is a multiple of PER_PAGE:

  fixture-edge-exactly-20   FAIL   <- the only one of 15
  fixture-edge-21-groups    pass
  fixture-edge-one-group    pass
  fixture-tibet             pass
  laos x 11 locales         pass

A real pagination bug would have shipped completely green under the old suite.
The failure is specific too: "20 people cards should lay out as 4 sheets but the
packet has 5" and "list pages disagree on the zone total: 20 vs 21".

⚠ EACH BUILD OVERWROTE THE LAST, which is why this is a script and not a line of
shell. packet.mjs names its output for the ZONE, not the fixture, so four
fixture builds against one zone leave ONE file -- and check:packets-all would go
on seeing a single shape while reporting success. Each output is renamed to its
fixture's own name so all of them coexist and every one is evaluated.

packet:ci now builds all four. check:packets-all goes from 11 packets in 1 zone
to 15 across 5 shapes; it still reports "5 of 28" and names what it does not
cover.
2026-09-24 23:53:07 +00:00
doxa-maps-88655c70
6f8ff0ebb7 gate: a shared fix is only as deployed as the last rebuild of each consumer
Some checks are pending
build-gate / build (push) Waiting to run
library/ is the single source of truth, but EVERY BUNDLE SHIPS ITS OWN COMPILED
COPY of the components it imports. So a one-line fix in a shared component is
live in whichever bundles happened to be rebuilt afterwards and silently absent
from the rest -- and nothing detected it, because every other gate measures the
RENDERED BEHAVIOUR OF ONE LEAF, and a leaf whose bundle is stale renders the old
behaviour perfectly.

Three independent instances in one week, which is what makes it a gate rather
than a note:
  - the PosterWall hover outline (a7f57c1) reached my-upg-100-list and none of
    the other three consumers;
  - the SemanticTreeLegend legend.a11y keys reached four bundles and NOT
    research-dashboard, so a legend there still ships nameless carets while
    check:a11y-button-names passes 791/791 -- that gate only sees the caret on
    leaves where an expandable branch happens to render. Nobody was working on
    it. It had just been sitting there;
  - commit 3cfe93c6, "Rebuild the fleet so the translated hint actually reaches
    it" -- someone already hit this and fixed it by hand. The remedy was known;
    the detection was not.

★ IT ASSERTS A MARKER STRING, NOT A TIMESTAMP OR A HASH. Comparing mtimes says
"older" and goes noisy on every unrelated rebuild. Comparing hashes cannot work
at all: a bundle legitimately differs from the library source it compiled. The
answerable question is narrower -- does the BUILT bundle contain THIS change? --
and a distinctive literal answers exactly that.

Consumers are DERIVED from the imports, never listed. A hardcoded list goes
stale the moment someone adopts a shared component, and a gate that silently
stops covering a new consumer is worse than none.

A marker that vanishes from SOURCE is reported as a stale ENTRY, not as failing
bundles -- otherwise reverting a change would blame every consumer for it.

MUTATION-TESTED IN BOTH DIRECTIONS, because a gate that has only ever failed is
indistinguishable from a broken one: rebuilding key-cities flipped its line from
✗ to ✓ and the count from 1 to 2 of 4, live; a positive control over a
long-standing marker reaches the PASS branch with all four consumers green; and
pointing an entry at a marker absent from source produces the stale-entry notice
rather than four false failures.

key-cities is rebuilt here because it was the mutation subject and the rebuild
is the fix the gate prescribes. The two bundles still red -- country-posters and
regions-dashboard -- belong to regions, and research-dashboard's legend drift is
older than this work; all three are reported, not rebuilt for them.
2026-09-24 23:22:18 +00:00
doxa-maps-88655c70
f2b667b41c gate: what readers are SERVED must be what we BUILT
Some checks are pending
build-gate / build (push) Waiting to run
Every gate in this repo measures the LOCAL build. Nothing measured the
PUBLISHED one. So the whole board stays green while readers are served a
months-old bundle, and the only detector is a human noticing something looks
wrong.

Running it today: 17 of 22 published bundles are NOT what we built, 3 more
return 404 and are not published at all, and 2 match. my-upg-100-list: readers
get 474KB, we built 663KB.

★ THE EXPENSIVE PART IS NOT THE STALENESS, IT IS THE PHANTOM BUGS. A stale
publish manufactures reports that cannot be reproduced. Three this week --
print buttons truncated to "Pr...", the legend reading "Generation 1/2/3"
instead of the real tab names, the pin hint saying "red dot" instead of
"coloured dot" -- were ALL fixed in main and ALL still visible live. Each one
read as a regression against a card marked done. Two cost most of a tick to
diagnose, and the diagnosis was the same sentence every time.

It compares the sha384 the manifest already carries, not the byte size: two
different builds can coincide in length, and a size check would call that a
match. Size is reported only to make a mismatch legible.

One finding the hash surfaced that a size check would have blurred:
gospel-resources is published 331KB LARGER than we build it. That is not
"behind", it is a different lineage -- published from a tree that has since
dropped code. And doxa-vision-card and golden-path-check DO match, so this is
not "the host is stale", it is a partial publish, which is a more specific
thing to go fix.

MUTATION-TESTED THREE WAYS: a matching bundle passes (exit 0); appending five
bytes to a matching local file fails it (exit 1); an unreachable host SKIPs
rather than failing, because a network gate that goes red on flaky wifi is a
gate people learn to ignore -- and the SKIP says it is not a pass.
2026-09-24 22:56:56 +00:00
doxa-maps-88655c70
0ae551fdf0 gate: assert the UPG-100 map carries our polygons, zones and pins
Some checks are pending
build-gate / build (push) Waiting to run
The Driver reported "the polygons for the map and map colors and pins are not
showing up" and no gate could answer it. verify-map-paint asserts a live GL
context, zero console errors and that Mapbox emitted attribution -- every one
of which is true of a perfectly-painted basemap with none of our data on it.
"The engine booted and tiles are loading" and "my data is on it" are different
properties, and only the first was measured.

The profile already counts this, at the moment it adds the layers, and prints
it: upg-100-list.vue:1849 writes "N polys · R/T zones · P pins" into
.upg-status. Three numbers printed by the product and asserted on by nothing.
This gate reads them.

★ THE TWO OBVIOUS APPROACHES ARE BOTH WRONG, and I shipped one before measuring.
Counting DOM .mapboxgl-marker elements returns 0 on a HEALTHY map -- mapbox
draws these pins as GL geometry inside the canvas, so there is no element to
count; I carried "markers: 0" across three probes as proof of breakage, and it
was proof of a wrong selector. Sampling the canvas returns an empty buffer,
because a WebGL drawing buffer is cleared after compositing: 0 of 13362 sampled
pixels had any alpha, on a map visibly full of colour. The product's own count
is authoritative in a way a proxy is not.

It asserts the zone RATIO, not just presence. "27/27 zones" and "1/27 zones"
both contain polygons and only one of them is the product, so the threshold is
what separates "some data arrived" from "the data arrived". Floors rather than
exact counts, well below the real values, so a data refresh does not fail the
gate and teach people to edit the expectation instead of reading it.

MUTATION-TESTED BOTH WAYS: raise the three thresholds above the real values and
each assertion fires independently with its own message (exit 1); restore them
and it passes (exit 0). Blocking the boundary geojson also fails it, via the
unparsed-status branch.

MEASURED, local AND live, both leaves: 195 polys · 27/27 zones · 2107 pins.
The Driver's symptom does not reproduce in that form on either tree.
2026-09-24 22:39:12 +00:00
doxa-maps-88655c70
c276a2158e Gate the embed contract: a composite must pass tk and provider down
Four elements shipped with this broken and nothing could see it, because every
existing gate loads a composite on its OWN generated leaf — and that leaf bakes a
token and assigns window.MAP_TOKEN, supplying the very thing under test. The
failure exists only where there is no leaf: the partner page the published embed
snippet is written for.

The gate serves the built tree on one free port and a partner page on another, so
CORS is exercised rather than assumed, and asserts per composite:

  tk        given only {"tk": <the token this tree bakes>}, a canvas appears and
            api.mapbox.com actually serves it
  provider  given only {"provider":"maplibre"} and NO token, a canvas appears and
            there are ZERO requests to api.mapbox.com

Zero is the assertion, not "it rendered": a build that quietly fell back to
mapbox with a baked key renders perfectly and is exactly the regression this
exists to catch.

★ IT FAILS ONLY WHERE THE EVIDENCE POINTS AT THE KNOB. The first draft failed
NINE composites for "no map canvas under a minimal config" — an accusation it had
not measured, since several of those elements simply need a `profile` key to
choose which profile to show. It now distinguishes: a token WAS supplied and the
element still says there is none, or maplibre was asked for and mapbox tiles
still loaded — those are findings. No canvas and no complaint is UNMEASURED, and
says so with the reason. A gate that red-lights other people's plugins over a
claim it cannot support is worse than no gate.

COST, because a slow gate stops being run: a flat 14s-per-case wait became a poll
for the canvas, and the provider case short-circuits when that element's tk case
drew no map — there is nothing to compare against, so a second page load was pure
waste. 5m38 -> 3m15.

GREEN on the landed tree: dashboard-plus-grid, research-map-plus-grid, stack-demo
and plugin-studio all pass both knobs, every provider case at ZERO api.mapbox.com
requests. plugin-studio only reached zero once the cap-5 shell started setting
window.MAP_PROVIDER — profile-config alone reaches the child a composite writes
config for, while the global rung reaches a map nested at any depth, which the
studio-inside-SPA-inside-composite chain needed.

gates:post is 44 steps; the quoted CLI doc is updated to match.
2026-09-24 20:26:33 +00:00
doxa-maps-88655c70
dd2443b7de Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 19:57:53 +00:00
doxa-maps-88655c70
3a4f065ec5 Research Gallery: a host that asks for maplibre gets maplibre in every tab
The shell forwarded tk, tokenUrl, locale and treeBase to each nested map,
but not the engine. So <doxa-maps-spa provider="maplibre"> still loaded
mapbox-gl and Mapbox styles. On a keyless host, that is the embed failing.

The shell now forwards `provider` in each child's profile-config, validated
to the two known engines. That fixed the simple map tabs, but not the
composites: they mount their inner maps without passing the provider on.
Measured with one tab per clean page: Big Picture made 19 api.mapbox.com
requests, Regions 17, UPG-100 15, Simple Research 14. So when a host asks
for an engine, the shell also sets window.MAP_PROVIDER (only if the page
hasn't). That is rung 3 of every map's own ladder, so it reaches maps
nested at any depth. After it, all four composites make 0 Mapbox requests.

check:gallery-embed:maplibre runs the cross-origin gate with a keyless
provider and fails on any api.mapbox.com request, attributed to the tab
that was open. Before this change, js mode failed with 20 URLs. It has one
named, printed exemption: upg-100-list.vue pins a token-free mapbox-gl.css.
That belongs to the upg100 plugin and is routed there.
2026-09-24 19:57:23 +00:00
doxa-maps-88655c70
b026c3ef83 poster cards: the print row belongs below the preview, and a fieldset ignores width
Some checks are pending
build-gate / build (push) Waiting to run
The Driver caught my own fix breaking the poster section, in the same session I
landed it. flex-wrap wrapped the print row onto three lines; the row is anchored
at the card BOTTOM, so growing it 35px to 78px pushed its TOP up from 216px to
173px in a 238px card — straight over the preview image and the outline. I
traded a horizontal overflow for a vertical one.

My gate measured only left and right edges, so it reported PASS on a visibly
broken card. Same instruments-lie family the flock banked today: a green that
measures the wrong thing.

The real cause of the ORIGINAL overflow, which both the old CSS and my first fix
missed: a <fieldset> gets min-width:min-content from the UA stylesheet, and that
OVERRIDES width and max-width. The rule said width:82% while the element
computed 431px inside a 212px card. That is why "it has a width, looks fine"
survived two rounds of this bug — the declaration was there and inert.

Fixed with min-width:0 on the fieldset itself, flex-wrap:nowrap, and flex:1 1 0
on the stock <select>, which sizes to its widest option and was taking 188px of
a 198px row.

Measured through all three states: 431x35 at top 216 overflowing, then 78px tall
at top 173 covering the preview, now 172x35 at top 216 — one line, below the
184x184 preview, inside the 212px card. Clean at 390/768/1024/1440/1920px with
all 108 controls full size.

The gate now measures the vertical axis and sweeps five widths. Mutation-tested
against my own regression: restoring flex-wrap fails with "27 print rows COVER
the preview image (37px)" at four of the five widths. 390px stays clean, which
is exactly why single-width testing would have missed it a second time.
2026-09-24 19:54:43 +00:00
doxa-maps-88655c70
53498c392b Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed 2026-09-24 19:24:32 +00:00
doxa-maps-88655c70
67de5b11fd check:gallery-embed: let the mode flag reach the gate, and count tabs per mode
The package.json entry ended in an inline # comment that named --mode and
--only. So `bun run check:gallery-embed --mode iframe` put the flag inside
the comment, the shell dropped it, and the gate ran its default. That made
check:script-flags red on main.

The mode now has its own entries, check:gallery-embed:js and
check:gallery-embed:iframe, and the base comment names no flags.

The default run always covered both modes (13 tabs each, recorded in the
original run). Only a caller-supplied flag was lost. So nothing can pass as
"both modes" again without running them, the summary now prints the tab
count per mode, and a requested mode that drove zero tabs is a FAIL.
Negative-tested: a filter that matches no tab exits 1.
2026-09-24 19:24:26 +00:00
doxa-maps-88655c70
7ae1572c94 poster cards: the print-options row stays inside its own card
Some checks are pending
build-gate / build (push) Waiting to run
The repo-hygiene agent found this in a fresh 1440px screenshot, which is the
whole lesson: .pfs already had width:82%, so reading the CSS said it was fine.
Flex items default to min-width:auto and refuse to shrink below their content
width, so four controls — Prayer List, Card Deck, the stock select, Poster Map —
pushed the row open regardless of what the container declared. A constrained box
does not constrain flex children, and only geometry shows that.

Measured at 1440px on cap4-upg100-mfe: 27 of 27 cards overflowing, worst 110px
past a 212px card, 36 intrusions into a neighbouring card's box. After
flex-wrap:wrap plus min-width:0 on the children: 0 and 0, with all 108 print
controls still a usable size.

That second number is asserted deliberately. "Nothing overflows" is trivially
satisfied by a row whose buttons collapsed to zero width, which would be a worse
product than the overflow it replaced. check:poster-card-fits holds both, and is
mutation-tested: reverting the fix reproduces the reporter's exact figures.

Also adds check:backlog-refs — a card may not depend on a card that does not
exist, because a dangling depends_on can never be done, so the card can never
become ready and nothing says so.

Its first run disproved the finding that motivated it. I had censused three
dangling references, including the PRINCIPLE card the loop prompt lists as open
work. The gate came back PASS, and chasing the discrepancy rather than assuming
the gate was broken showed my census was wrong: it read only the builder
backlog, and all three cards exist in 01-ideation-research. Cross-phase
references are legitimate, and a single-backlog census structurally cannot see
them. Reporting three dangling refs would have sent someone hunting for cards
that were never lost.

It fails only for depends_on on an OPEN card; a dangling parent is reported and
passes, because failing on filing debt teaches people to delete the reference
rather than restore the card. Both severities mutation-tested.
2026-09-24 19:19:02 +00:00
doxa-maps-88655c70
f0c46022ee Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 19:15:28 +00:00
doxa-maps-88655c70
a5ab04ec75 Measure the basemap, in a real embed, on the token the page ships
"The map renders" had never been measured. Every browser gate here
forces a Mapbox token onto the page from a shared file; that token is
restricted and 403s on the basemap, while the leaf bakes a different
one that works. The gates then counted PINS — which are drawn from our
own GeoJSON and paint with no token at all — so they passed on a
basemap the probe itself had broken.

This gate forces nothing. It serves the leaf on one port and a host
page on another so the iframe is genuinely cross-origin, loads it as a
stranger's browser would, and takes evidence two ways: Mapbox responses
counted by status, and colour variety in a slice of the canvas. The
gospel map passes — 9 tile responses, none denied, and the screenshot
shows coastlines, ocean shading and country labels.

The negative control runs every time, and the check reports PROVED
NOTHING rather than a pass if the control succeeds. That is not
decoration: the first two attempts at the control did nothing at all.
A page-level init script never reaches a cross-origin iframe, and even
at context level it was ignored, because this leaf injects `tk`
straight into the element and only falls back to window.MAP_TOKEN —
so it is immune to the forced-token hazard by construction. The
control had to move to the wire. With the token rewritten there, 6 of
8 tile requests are denied and the basemap vanishes, leaving only the
pins and choropleth: the picture that shows exactly why a pin count
was never evidence of a map.
2026-09-24 19:13:46 +00:00
doxa-maps-88655c70
3a2cf0e8f2 Research Gallery: embed on someone else's page, in both modes
The gallery is going onto the DX-life research page, and nothing had tried
it on a page it didn't generate. From a second origin, a bare
<script> + <doxa-maps-spa> drew over the host's header, and 8 of 13 tabs
showed an error card.

Three causes, each fixed in the shell:

- No :host box. The layout root is position:absolute, so it anchored to the
  viewport instead of the element. :host is now a positioned block with a
  default height the host can override.
- A foreign page sets none of the globals a leaf page bakes. Every data call
  hit the host's own origin and got HTML back. The manifest now publishes a
  `runtime` block (apiBaseUrl + the public token every leaf already carries).
  The shell fills window.MAP_APP_API_URL / window.MAP_TOKEN only when the page
  hasn't set them, and forwards tk in profile-config.
- Nested elements resolved assets to <tree>/../assets, a sibling pool the
  publisher doesn't ship (404 live). The shell now forwards an absolute
  <tree>/assets. This also fixes the iframe mode, where Cities with Internet
  and Gospel Resources were failing the same way.

Also adds a `tab` attribute (plus ?tab=) to deep-link into one map, and a
`tab-change` event for the host.

check:gallery-embed drives every tab in both modes from a second origin. It
passes here and fails on the previous tree (7 findings).

EMBED.md documents the contract. The Traefik template now sends CORS for the
whole tree: live manifest.json answers without Access-Control-Allow-Origin.
2026-09-24 19:03:37 +00:00
doxa-maps-88655c70
b089f9effb Reach Global Recordings Network, and lift 147 more groups
GRN records the gospel as AUDIO for languages with no writing, which is
most of what is left in the gap — and every earlier attempt recorded it
as unreachable. globalrecordings.net sits behind a proof-of-work bot
shield that challenges even robots.txt; that is an access control and
is not worked around. Their own delivery site, 5fish.org, is not
shielded, allows what this reads, and resolves by ISO 639-3 directly.

First 180 gap codes swept: 74 hold gospel audio, a 41% hit rate, and
147 people groups that had nothing now have the gospel in a form they
can receive. Tier 0 falls 2,873 to 2,746.

The host NEVER 404s. A nonsense path returns HTTP 200 and a plausible
geo-located page, so reading 200 as existence would have marked every
code on earth as covered. Existence is read from the page title, and a
negative and a positive control run in band on every run — the fetcher
aborts if a nonsense code ever reads as a real language. Their
robots.txt asks for a ten-second crawl delay, so the sweep is
sequential, capped per run and resumable. Do not add concurrency.

Three sign languages are deliberately left out. GRN's catalogue is
audio; 5fish holding a page titled "Pakistan Sign Language" says they
index the language, not that they have signed video. Attaching it told
20 Deaf groups — 11.5 million people — that gospel audio was available
to them. Unverified, and for that audience useless.

Six places across three surfaces still told the reader GRN answers 403
to everything and had never been searched. That is now false, and a
stale caveat misleads exactly as much as a stale number.

Also here, because adding a source is what exposed it: the charts
profile decided "this group has nothing" a second time, by listing the
resource fields by hand. That list went stale every time a source was
added — 7 languages once, 23 when Scripture Earth landed — so the chart
drew groups as having nothing while the map offered them a working
link. It now reads the tier the pin index already computed, and a new
gate refuses any surface that re-derives it. The gate was checked
against the real pre-fix file, not only a synthetic one.
2026-09-24 18:55:28 +00:00
doxa-maps-88655c70
6e958bc22f legend: open on the UPG-100 list, which is what the Driver asked for first
Some checks are pending
build-gate / build (push) Waiting to run
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.
2026-09-24 17:59:57 +00:00
doxa-maps-88655c70
3db1dc1852 legend: on a phone you can browse the UPG-100 lists and cannot export them
Some checks are pending
build-gate / build (push) Waiting to run
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".
2026-09-24 17:27:28 +00:00
doxa-maps-88655c70
d9d4cbd0da spa tab-order gate: the product honoured the Driver, the gate over-read him
Some checks are pending
build-gate / build (push) Waiting to run
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.
2026-09-24 16:56:10 +00:00
doxa-maps-88655c70
5905de8e30 facet gate: green 1 run in 4, and the variance is the dev server's
Some checks are pending
build-gate / build (push) Waiting to run
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.
2026-09-24 16:38:59 +00:00
doxa-maps-88655c70
bfc1e0029b gates: two unwired checks were red on main, and nothing could tell us
Some checks are pending
build-gate / build (push) Waiting to run
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.
2026-09-24 15:53:58 +00:00
doxa-maps-88655c70
baf691e8cf provider-switch gate: run by default, instead of only when someone types a URL
Some checks are pending
build-gate / build (push) Waiting to run
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.
2026-09-24 15:30:35 +00:00
doxa-maps-88655c70
7a649b6845 packet browser gate: verify the packet a reader actually exports
Some checks are pending
build-gate / build (push) Waiting to run
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.
2026-09-24 14:59:25 +00:00
doxa-maps-88655c70
a361da0a2e gate wave: a SKIP is not a pass, and this printed them identically
Some checks are pending
build-gate / build (push) Waiting to run
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.
2026-09-24 14:50:05 +00:00
doxa-maps-88655c70
3c17245c73 packets: give CI something to check, instead of a sweep that always SKIPs
Some checks are pending
build-gate / build (push) Waiting to run
check:packets-all is in gates:post. CI runs it on every build. It has SKIPped
every single time, because library/scripts/posters/out/ is gitignored — both the
records cache and every generated packet — so a clean checkout has none.

Every packet assertion in this tree has therefore never once been evaluated
there: the 12 prayer points, the wheel's 12 labels, page 1's geography paths,
the print layer, the build stamp. Several ticks of work accumulating on a gate
that had only ever graded files in my own working tree.

A SKIP that is always taken is indistinguishable from an unwired gate — and the
wave-membership census cannot tell them apart, because membership measures
invocation, not evaluation. That census is how check:packets-all was created in
the first place, which makes this the same class recurring one level up.

I raised this at the end of the last tick and deferred it on the premise that
closing it "adds a live-API call to every build". The premise was wrong.
packet.mjs already had an OFFLINE mode, and a fixture needs no network at all.

The fixture is the smallest zone — Tibet, 43 slugs, 20KB — with image_url
stripped so CI can never fetch 43 photos from someone else's CDN. It is DERIVED,
not hand-maintained: membership comes from the committed clusters.json, so
check:packet-fixture regenerates and content-compares it. Re-adding an image_url
fails; dropping a record fails naming the slug.

A fixture packet stamps itself "fixture", and "fixture nomap" in CI, which has
no poster sources either. `fixture` implies nophotos in the gate — without that
it reported "every source record carries an image_url", a true sentence about
real records and a false one about this packet.

A real bug in my own change, caught only by running the script through its REAL
invocation path: packet.mjs fails closed on a missing VITE_API_BASE_URL BEFORE
loadRecords() ever looks at PACKET_RECORDS, so `bun run packet:ci` exited 1 on a
tree that had everything it needed. Every hand-test had passed because I was
supplying a dummy host. The guard now exempts fixture builds and still fails
closed otherwise.

From a genuinely clean state — the whole out/ directory moved away — the sweep
now reports "1 packet(s) checked, 1 passed". i18n verified through that same
path: a partial block (1 string, 5/12 points, 3/12 rhythms) renders 12 points,
12/12 wheel labels, wheel 3 translated and 9 English, 0 blanks.

check:packet-fixture joins gates:post (34 -> 35), README quote re-synced in the
same commit. Wave 35/35, packets-all 11/11 here and 2/2 in the studio.
2026-09-24 14:33:36 +00:00
doxa-maps-88655c70
baf302983e Rebuild the served tree after merging main, and unswallow two self-test flags
The merge surfaced what main's new script-flags gate was written for: two
gospel-resources entries advertised --poison inside an inline # comment, where
the shell drops it — a self-test that never runs is a gate certifying itself.
Each flag now has a runnable sibling entry (check:snapshot-guard:poison,
check:coverage-consistent:poison) and the prose points at the entry instead of
the raw flag. Both siblings executed: the injected defects are caught (12/12
clauses). capability-exemplars regenerated; full build green, 34/34 post gates.
2026-09-24 14:23:20 +00:00