158 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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. |
||
|
|
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". |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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 |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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 ( |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
dd2443b7de |
Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts: # public/doxa-maps/index.html |
||
|
|
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. |
||
|
|
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. |
||
|
|
53498c392b | Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed | ||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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". |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
445131cf94 |
Merge main into bundler-3/work — settle the 21-file conflict, keep both campaigns
Only 2 files conflicted in source: package.json and its CLI doc, both taken to main's refactored gate pipeline (gates:pre/gates:post/build:bundles) after verifying every check the old inline chain named still runs there. The other 19 were generated: public/assets/tables mirrors keep main's deliberate deletion, and public/doxa-maps is regenerated by the build that follows. |
||
|
|
e130cb672a |
packet generator: say when page 1 has no map, instead of printing a tick
Some checks are pending
build-gate / build (push) Waiting to run
`✓ laos 95 groups 8 pages 0 photos` — for a packet whose first page is blank. The page count stayed 8 because a blank sheet is still a sheet. The map one-pager is lifted from out/posters/<zone>.html, which a separate manual step writes. When it is absent packet.mjs deliberately still builds, and that fallback is right — but it was silent. The empty-groups branch three lines above already warns and skips; the map branch had the same shape and no warning. A gate catching this is not the same as the tool reporting it. The gate added last tick runs later, optionally, and only when someone invokes it. The generator prints its tick at the moment of production, which is the one output an operator is guaranteed to see. Now it prints ⚠ with the reason inline and repeats it in an end-of-run summary naming every affected zone and the fix — a per-line mark scrolls off in a 27-zone run, and this is the one fault that cannot be seen by counting pages. Both silent branches are covered, distinctly: a missing poster reports "no out/posters/laos.html"; a poster that exists but carries no <svg> — a truncated generate run — reports "out/posters/laos.html has no <svg>". An operator can tell "never generated" from "generation broke". Also adds the `packets` script. Its absence is why the posters -> packets dependency was never expressible anywhere: packet generation was only ever a hand-typed node invocation, so nothing could state what it reads. Measured in both repos. doxa-maps-plugin-studio has 0 generated posters against wt-packet's 28 — out/ is gitignored in both, so posters are build artifacts and this is a missing build step, not a repo defect. A fresh studio run now warns; wt-packet prints a clean tick. The size difference corroborates: 0.4 MB with the map, 0.1 MB without. i18n re-verified end to end since this touches the generator: a partial locale block (2 strings, 3/12 points, 4/12 rhythms) renders the wheel 4 translated and 8 English, 12 point rows with 4 translated, 0 blanks, clean tick. All 11 locales regenerated with photos — packets-all 11/11, each 12/12 wheel labels, 12/12 points, 13 geography paths. Wave 34/34. |
||
|
|
6c60af555f |
library-index gate: ask whether the menu drifted, not whether a file is older
Some checks are pending
build-gate / build (push) Waiting to run
`touch library/components/legends/SemanticTreeLegend.vue` — zero content change, nothing in git status — turned this gate red. So did a git checkout, a rebase, and any editor that rewrites a file on save. It cried wolf twice in one build session while the regenerated doc was byte-identical both times, and the only available response was to run the generator and commit nothing. An mtime answers a different question than the gate's own name asks. This is the SECOND time this exact class was fixed here. The first was a SCOPE error: the gate walked 2.3x more of library/ than the generator reads, so a comment in a build script turned it red and named that script as the reason the component MENU was stale. Narrowing the scope fixed that instance, but the mechanism was still mtime, so the class survived inside the narrowed scope. The lesson recorded then applied again word for word — a gate that fails for a change it can prove is irrelevant teaches the reader to regenerate on reflex, which is how a staleness check stops being read. It now regenerates into a throwaway tree and diffs the bytes. generate-library-index.mjs hardcodes its output path and derives ROOT from its own location, so it cannot be told to write elsewhere — but copying the SCRIPT into a temp tree whose library/ is a SYMLINK to the real one relocates its ROOT without modifying it. The generator runs unmodified against real sources, writes somewhere disposable, and an interrupted run leaves nothing behind. Verified byte-identical to a real `bun run library-index` before this was written, and the generator was confirmed deterministic first — a content diff would be useless otherwise. Comparing output also makes the scope self-enforcing: a change outside components/, composables/ and utils/ cannot alter one byte of the menu, so the INDEXED_DIRS list the mtime walk needed is gone. Mutation-tested both ways, because a gate that never fires is worse than a noisy one. Deleting a catalogued row fails with the line number and expected content. Adding a real new library component fails — that is the drift this exists to catch. Touching 40 library files, the rebase case, passes. Wave 34/34. Swept the five other internal/*.mjs that reference mtime: only check-shot-freshness also decides on one, and there it is correct — a PNG cannot be cheaply regenerated and diffed, and a rebuilt bundle is meaningfully newer. Left alone deliberately. |
||
|
|
a177f98aff |
legend: the controls no longer read their own i18n key aloud
Some checks are pending
build-gate / build (push) Waiting to run
The legend's toggle button announced the literal string "aria.toggleLegend" to screen readers. Not in translated locales only — in English too. vue-i18n echoes the key when a lookup misses, and `toggleLegend` was absent from every catalog while its two siblings in the same file, backToLegend and closeDetailView, were present. Six call sites across LegendDesktop.vue and LegendMobile.vue, desktop and mobile alike. aria-label is the one string a sighted tester never sees. Every other untranslated string in this tree got caught because somebody looked at the screen; this one is only ever spoken aloud, so it survived every visual QA pass. A second instance turned up in the same sweep with the opposite shape: SemanticTreeLegend hardcoded 'Collapse legend' / 'Expand legend' into both title and aria-label with no t() at all — correct English, and untranslatable. One key missing FROM the catalog, one string never routed TO it. Adds toggleLegend, collapseLegend and expandLegend to all 11 aria.json catalogs and routes the hardcoded pair through t(). check:i18n-parity already guards them. New gate smoke:aria-labels asserts BOTH shapes, because either assertion alone is insufficient: "no raw keys" passes a hardcoded English label forever, and "labels differ across locales" passes a raw key, since a key is identical in every locale. It detects a raw key by SHAPE — dotted, camelCase, no spaces — which is locale-independent and works on the Arabic and Chinese pages where no English word list could. Mutation-tested twice. Removing the catalog key fails with the raw key named, on all four viewport/locale combinations. Hardcoding one label back to English initially PASSED: the differ count dropped 6 to 5 and nothing asserted on it — the same "a number printed but not asserted on is decoration" shape this tree keeps rediscovering — so the gate gained a LOCALIZED_FLOOR ratchet and now fails that too. Verified the fix generalizes beyond the profile it was found on: country-posters reports 182 of 190 accessible names localized, 0 raw keys. Rebuilds the six bundles that embed a legend; check:sri confirms every leaf matches. Wave 34/34. |
||
|
|
6af75895c7 |
legend: the info popovers explain the map in the reader's own language
Some checks are pending
build-gate / build (push) Waiting to run
The Driver asked for the per-tab info buttons to stay — "the tab itself gives
you a little information button that will explain what a UPG-100 list is."
They existed, and they were English-only in every locale.
SemanticTreeLegend rendered `tab.info` RAW in all three places it appears (the
button's title=, and the popover body) while the tab LABEL beside it went
through the catalog. So a Spanish reader saw a translated chip next to an
English explanation of what that chip means — and the explanation is the part a
reader who does not read English actually needs. The three UPG-100 tab labels
were absent from tabs.json entirely, so they fell back to hardcoded English too.
Measured on the served leaf before changing anything: ?lang=es changed 2 of 76
rendered strings ("POPULATION" -> "POBLACIÓN") and left all three tabs and all
three info popovers in English. That positive control is what proves the locale
switch WORKS and the strings were simply never catalogued — without it this
reads identically to a broken ?lang=.
tabInfo() mirrors tabLabel exactly: `legend.tabInfo.<i18nKey|id>`, te() guard,
falling back to the hardcoded English so a tab with no catalog entry renders
unchanged rather than blank. The v-if stays keyed on the raw `tab.info`:
whether the affordance EXISTS is a property of the tab definition, while its
TEXT comes from the catalog.
Catalogued in all 11 locales, labels and info. check:i18n-parity already guards
them — deleting one zh key fails the build by name.
New gate smoke:legend-info renders the REAL served page and clicks each info
button. check:tabs-localized could not see any of this: it tests 7 hardcoded
research-map tabs and REIMPLEMENTS tabLabel inside page.evaluate, so it proves
the mechanism works rather than that a given profile is localized — and it never
looked at `info` at all. Mutation-tested: reverting the popover to the raw prop
fails with "the catalog is present and unreached".
Partial-translation contract verified by deleting 2 of 3 fr info keys and 1 of 3
fr labels: translated keys render French, untranslated fall back to English,
nothing blank, and labels and info resolve INDEPENDENTLY.
Rebuilds the six bundles that embed the legend so the tree stays internally
consistent; check:sri confirms all 55 script references match. Full wave 34/34.
|
||
|
|
f129294609 |
Clause L: a verification record that stops being re-run now fails the gate
Eleven clauses checked that the surfaces AGREE with each other. None checked that any
of it was still true. "Link checked" is a claim about the present tense, and a table
that quietly stops being re-run keeps making that claim on evidence that could be a
year old while every consistency clause still passes. A dead link is the failure a
field worker actually hits, so this is not bookkeeping.
Clause L fails when a shipped URL carries no check date at all, or when the oldest
shipped link was last checked more than 45 days ago, and the message names the exact
command to fix it. Re-running is cheap and resumable now, so the failure is an
instruction rather than an obstacle.
Proven against real data, not only the poison harness: deleting the check date from
one shipped URL is caught ("1 shipped URL(s) carry no check date"), and backdating one
by 120 days is caught ("last checked 120 days ago, past the 45-day limit"). Both
restore clean. Poison self-test now 12/12.
Also checked and deliberately NOT changed: the Arclight harvest keeps a language only
when counts.featureFilm >= 1, and it was worth asking whether that discards coverage.
Of 2,330 catalogue languages, 2,309 already have a feature film. Only 15 carry just a
shortFilm or a series, and exactly one of those - nmg Kwasio, 2 pins, 60k people - is
spoken by a group here with nothing else. Widening the filter would mean typing a
short film as "Film" beside the JESUS Film for a single language. The filter stays.
The registered description still claimed "6/6 clauses must fail"; corrected to 12.
|
||
|
|
e8ad1d56e6 |
packet: a partial translation renders 12 prayer points, not 3
Some checks are pending
build-gate / build (push) Waiting to run
packet-render.js resolved the prayer points with `L.points || EN.points`. That is a whole-block fallback when the key is ABSENT, and TRUNCATING when a translator has done some of the 12 — a 3-element array is truthy. Measured at the rendered layer with a deliberately partial locale block: a page headed "12 Prayer Points" containing THREE, points 4..12 silently gone, including the Mark 5:1-20 passage point 4 prints in full. Nothing blank, no error raised. Invisible in all 11 shipping locales because every one of them is complete. It would have appeared on the next language added — and "a new language is a NEW BLOCK, never a code change" only holds if a PARTIAL block degrades per key, since half-finished is the normal state of a language in progress. Now merges per point and per field against English: English fixes the LENGTH and supplies every missing field, so a point translated for `text` but not `sub` keeps the English `sub` — and keeps its NUMBER, which the merge supplies. The sheet-sum invariant could not see this: a 3-point page is still exactly one sheet, so every number check-packet-render printed stayed correct while nine points were missing. It now measures and asserts the count. Mutation-tested — reverting the fix prints "prayer points : 2 / 12" and fails. All 11 locales regenerated with photos and swept: check:packets-all 11/11, each 12/12 points, including RTL ar and Devanagari hi. Renderer, generator and gate synced to doxa-maps-plugin-studio, which also lacked the .points-title and .wheel-title test hooks the new assertion needs. Also wires check:capability-exemplars into gates:post (33 -> 34 steps). It reads public/doxa-maps/manifest.json, a build artifact, so ordering mattered: `tree` writes the manifest and is &&-chained immediately before gates:post, and the gate SKIPs rather than fails when the manifest is absent. Full wave: 34/34. The quoted gates:post line in the README is re-synced in the same commit, because check:quoted-scripts goes red on the drift this commit creates. |
||
|
|
0e0b142719 |
A half-finished catalogue fetch can no longer overwrite a good one
The FCBH fetcher read per-fileset video scope for 1,666 languages and that file was
committed. A later run, while live.bible.is was rate-limiting, produced 1,103 and
overwrote the good file with the degraded one. Nothing failed, nothing warned, and the
artefact silently lost a third of its content. It was caught only because git status
happened to be read, and the fix was a git checkout.
That is the third time this shape has appeared here:
- a VERIFY_LIMIT smoke run replaced the full verification table with 30 rows;
- dropping a dead link removed it from the corpus, so the next run forgot it was
dead and the build put it straight back;
- this one.
Every snapshot in this bundle is built from a network read, and a network read has a bad
day. The dangerous case is never the run that throws - that leaves the old file alone -
it is the run that half-succeeds and writes anyway.
scripts/_snapshot-guard.mjs now stands in front of every catalogue write. A snapshot may
grow freely and may shrink a little, but losing more than a tenth of the previous run is
treated as a failed fetch: the existing file is left untouched and the script exits 1.
It is deliberately not an equality check, because catalogues do genuinely lose entries
when a publisher retires something, and a gate nobody can satisfy gets switched off.
SNAPSHOT_FORCE=1 is the one loud way past it.
Proven by replaying the exact failure: feeding it the 1,103-entry version refuses the
write, names the real numbers (563 fewer, 13.8%, below the 90% floor) and leaves the
file byte-identical to the good one. SNAPSHOT_FORCE=1 writes it. An equal write passes.
All six fetchers are wired: ebible, arclight, arclight-share-urls, door43, zume, fcbh.
Re-running zume, door43 and ebible for real passes the guard at 52, 208 and 1,237 codes
and produces byte-identical data, so the guard perturbs nothing.
The one exception is the checkpoint write in the resumable share-urls fetcher, which
starts from the existing file as a cache and can only grow. It must now declare itself
with a CHECKPOINT-WRITE-OK marker, so the exemption is a statement rather than an
oversight.
New gate check:snapshot-guard fails if any fetch-*.mjs writes OUT directly without that
marker, or never calls writeSnapshot at all - a guard that is easy to bypass is a guard
that will be bypassed. It refuses to run against an empty file list rather than passing
vacuously, and --poison injects a raw write and requires the check to catch it. Chained
into build:unlocked and its doc copy kept in sync.
|
||
|
|
510f3d30d4 |
The backlog-status enum was too narrow, not the nine cards wrong
Some checks are pending
build-gate / build (push) Waiting to run
This gate keeps the AGGREGATE honest — a prose status makes every open-count wrong
while each card still READS correctly, so the error hides in the total. That matters
to this loop directly: every tick starts by reading status counts to pick a card.
MEASURED across 633 cards: EIGHT distinct status values are in use and EVERY ONE IS
A SINGLE WORD. No prose remains anywhere in the field — which is exactly what the
original 130-card migration achieved. The nine cards this gate was failing carried
legitimate TERMINAL states the enum simply predated: superseded (5), deferred (2),
decided, verified-already-true.
★ REWRITING THEM TO "done" WOULD HAVE DESTROYED REAL DISTINCTIONS to satisfy a list.
A SUPERSEDED card was replaced by another; a DEFERRED one was consciously postponed;
neither was finished. Losing that information to make a checker green is the
opposite of what this gate is for.
OPEN vs CLOSED is the property that matters, and all four additions are CLOSED:
open = todo + doing + in-progress (66), closed = done + these four (507),
blocked (60).
MUTATION-TESTED BOTH WAYS, so widening did not weaken it: a prose status
("done — reconciled against main, see notes") still FAILS by name — the original
defect — AND an unagreed one-word status ("wontfix") ALSO fails, so the set stays
CLOSED rather than becoming "any single word".
Wired into gates:post (32 -> 33) after confirming it reads only backlog JSON. The
doc that quotes gates:post is re-transcribed in the same commit.
Build: 7/7 pre, 23/23 bundles, 33/33 post, exit 0.
|
||
|
|
d4e6d8b044 |
Two --self-test modes could not be selected — a gate certifying itself
Some checks are pending
build-gate / build (push) Waiting to run
A --self-test IS the proof that a gate can fail. verify:interop and
check:fits-screen each advertised extra regression modes that nobody could reach,
so their non-default regressions were never demonstrated. The gate that found this
puts it exactly: "a --self-test that never runs is a gate certifying itself."
THE MECHANISM, PROVEN WITH AN ARGV PROBE rather than argued. Both parent script
values end in an inline # comment, so a mode appended by the caller lands INSIDE
that comment:
dedicated entry (mode hardcoded) -> ARGV ["--self-test","--self-test-clause=b"]
parent-style, caller appends -> ARGV ["--self-test"] clause (none)
The run then exits 0 in DEFAULT mode, indistinguishable from the mode being
selected. Unreachable: camera-freeze (interop) and clauses b-e (fits-screen).
Five dedicated entries now hardcode each mode, and the parent comments point at
them instead of at an append that cannot work. Confirmed end to end:
check:fits-screen:self-test:b fires a real clause-named failure
("[A] FAIL dashboard-plus-grid -> band=2401px vs viewport=900px").
★ MY FIRST ATTEMPT MADE IT WORSE — 2 findings became 6. I appended an explanatory
# comment to each new entry, which re-created the swallowing shape AND named the
flag in prose; the gate scans script VALUES, so describing a flag is
indistinguishable from advertising one. Rewrote the notes naming no flag. Then a
blanket string-replace mangled the fits-screen comment's clause list into
unreadable prose, repaired separately. The gate was right at every step; I was the
one adding the defect.
check:script-flags goes FAIL(2) -> PASS(148 scripts, 0 unreachable), and is wired
into gates:post (31 -> 32) after confirming it reads only package.json.
MUTATION-TESTED: reintroducing an advertised-but-swallowed flag fails by script
name. The doc that quotes gates:post is re-transcribed in the same commit.
Build: 7/7 pre, 23/23 bundles, 32/32 post, exit 0.
|
||
|
|
28ce04f6ac |
check:done-evidence knew one card schema and failed the best-evidenced cards on the board
A done card with no record is the reporting failure this loop exists to avoid — so a gate that mis-identifies WHICH cards lack a record is worse than none: it teaches the next agent to pad the field the checker reads. ★ THE THREE FLAGGED CARDS WERE AMONG THE BEST-EVIDENCED THERE ARE. Each carried one_line, why, and a `verify` block naming the probe, its assertion count and its negative tests — "74 files match byte for byte, all three drift classes exit 1", "9 assertions, all pass", "two new assertions, both negative-tested". The gate looked only for `bluf` or `fields`. MEASURED across 443 done cards: 40 carry `verify`, 41 carry `one_line`, and ALL THREE flagged cards had one or both. `verify` is if anything the STRONGER signal — it names how the claim was checked, which is what evidence means here. Fixing the CARDS by adding a redundant bluf would have been gaming the check. Fixing the GATE to recognise the evidence already there is the honest repair. ★ THE BASELINE IS NOW EMPTY. All ten exemptions reported STALE — every one had since gained evidence — so the list exempted nothing while implying ten cards still owed a record. An EMPTY set is not an unset one: every done card must now carry evidence with NO exemptions, and the gate fails outright for a single card that does not. Keeping ten satisfied ids would mean the same today and quietly permit a regression the moment someone reused an entry. Same reasoning as emptying the i18n DEBT map. MUTATION-TESTED both directions: a done card with NOTHING in any of the four fields FAILS by name; the same card with ONLY a `verify` entry PASSES. Wired into gates:post (30 -> 31) after confirming it reads only backlog JSON, which the build does not mutate. The doc quotes gates:post, so it is re-transcribed in the same commit — the self-referential update noted last tick, handled in one pass this time. Build: 7/7 pre, 23/23 bundles, 31/31 post, exit 0. |
||
|
|
856a350c6a |
The CLI doc quoted 18 gates while 29 ran — sync it, and wire the gate that noticed
Some checks are pending
build-gate / build (push) Waiting to run
Last tick left six documentation-drift gates failing and NAMED on every build. This is the smallest — one finding — and it guards something real: a doc that quotes a config is claiming to be ground truth. THE DRIFT WAS ENTIRELY MINE. Every one of the eleven missing steps is a gate added over the past ticks, all four i18n gates among them. ★ AND IT DRIFTED IN THE UNUSUAL DIRECTION. The doc did not OVERSTATE coverage, it UNDERSTATED it: a reader consulting README-SECTION-3 would believe eleven checks were not in the chain. That is precisely what the gate's own header warns about — "there is no wrong character to notice, just a check the reader believes is not in the chain." ★ THE JUDGEMENT CALL. The surrounding prose said "the 29 gates cost 84.5s" and "31 in, 31 out" — figures from the 2026-09-23 phase refactor, now 36. I DATED them rather than restating them as current. Those numbers are the EVIDENCE that the phase change preserved the step set; rewriting them to 36 would destroy that evidence while looking like an update. The gate compares quoted SCRIPT LINES only, so this prose drift was invisible to it either way. Wired into gates:post (29 -> 30) after confirming it reads only package.json and a markdown file, neither of which the build mutates — the precondition test I got wrong with check:settled last tick, where "passes twice standalone" proved repeatability rather than compatibility with running mid-build. ★ WIRING IT IS SELF-REFERENTIAL and takes two passes: adding the gate to gates:post CHANGES the line the gate quotes, so it failed immediately after being wired and needed one more doc sync. That is the mechanism working, and it is now noted in the doc so the next editor is not surprised by it. MUTATION-TESTED: removing a single step from the DOC only makes it FAIL by name — the invisible-omission case it exists for. Build: 7/7 pre, 23/23 bundles, 30/30 post, exit 0. |
||
|
|
482e6d4ca0 |
Wire the seven gates that could run, and make wave membership visible
Some checks are pending
build-gate / build (push) Waiting to run
MEASURED: 70 of 100 check/verify scripts were in package.json but in NEITHER gates:pre nor gates:post. I had fixed this same "present but unwired" failure three times for individual gates without ever asking how many others were in the state. ★ THE GATE THAT EXISTS TO CATCH THIS WAS ITSELF UNWIRED. check:no-orphan-gates asserts every gate-shaped file is REACHABLE — named in package.json or imported — and it passed, 87/87. But reachable and WIRED are different properties, and conflating them is exactly how 70 scripts sat outside every build. A script entry means a human CAN run it; wave membership means it RUNS. Tested each unwired gate standalone: 7 pass with no precondition and were simply never wired (now in gates:post, 22 -> 29 steps, +1.5s on a ~150s build); 6 FAIL on pre-existing documentation drift nobody sees — check:verification-tags alone lists 61+ untagged cards — which is its own project, and wiring a red gate only gets it disabled; the rest legitimately need a served tree, a device, a token or an argument. ★ ONE I WIRED WRONGLY, AND THE MISTAKE IS THE USEFUL PART. check:settled asserts the tree is QUIET — no builder running, no recent churn — so it fails inside a build BY DESIGN. It passed twice standalone, I wired it on that evidence, and the next authoritative build came back 29/30 with check:settled the only red. Running a gate twice while IDLE proves repeatability, not compatibility with running mid-build. The question for wave membership is not "does it pass" but "is its assertion compatible with a build in flight". Unwired, and the reasoning is recorded in check-no-orphan-gates so nobody re-adds it on the same evidence. check:no-orphan-gates now reports wave membership as a second, distinct question from reachability — "37/93 run in gates:pre or gates:post" — naming the first eight unwired so the debt is visible rather than silent. It REPORTS rather than fails, because most of the remaining 56 are correctly hand-run. Mutation-tested: removing check:zone-census from gates:post moves the count 37 -> 36. Build: 7/7 pre, 23/23 bundles, 29/29 post, exit 0. |
||
|
|
8f7a2f875d |
Gate the default drop: with no poster and no source, nothing prints and the reader is told
check:poster-print-fallback proves the GOOD path — no .webp, .html present, browser
rasterises. On a tree with NEITHER it SKIPs. And neither is the DEFAULT, since
posterSources and bundlePosters both default false, so the behaviour a reader is
most likely to meet was the one behaviour nothing asserted.
BUILD-POSTERS-GENERATE-ON-DEMAND-DROP-52MB claims in prose that export and print
"degrade honestly, with the preview refusing to open rather than printing an error
page". That claim was true and untested. Measured on a real dropped folder: the
wall requests the .webp (404), then the .html fallback (404), then shows "poster
not found (404)" with a Close button. 0 print frames, 0 downloads, 0 page errors.
The failure this guards is NOT a 404. It is a print dialog that OPENS on a poster
that does not exist — a blank sheet, or a low-resolution PREVIEW thumbnail printed
as though it were the 3ft poster. Both look like the feature working.
MUTATION-TESTED, two plausible wrong fixes, each caught distinctly: falling back to
printing the preview ("1 print frame(s) opened with NO poster and NO source"), and
failing silently by clearing exportState ("the reader was told nothing"). The gate
also asserts BOTH paths were attempted, so a wall that gives up before trying to
rasterise fails even while showing the right message.
★ SERVE THE TREE ROOT, NOT public/ — and this is in the gate's header because I got
it wrong first. A leaf resolves assetsBase to '../../assets', which from
<tree>/<bundle>/<profile>/ is <tree>/assets. Serving public/ puts public/assets on
that path, a directory still holding 378 posters in a dev checkout, so the poster
loads with a 200 and the gate tests the opposite of what it claims. The same click
returned 200 under `--directory public` and 404 under `--directory public/doxa-maps`.
My first reading of this was "a print frame opened on a missing poster", which would
have been a serious product defect; it was the harness.
Also audited check-packet-render this tick: all thirteen printed metrics now have
assertions behind them. No decoration left on that surface.
|