The prayer packet prints its map: the dx-maps tree ships the 28 poster sources

Page 1 of every UPG-100 prayer packet is the zone map, drawn from
<tree>/assets/posters/<id>.html. The tree shipped no posters/ at all:
generate-tree gates the folder behind posterSources and bundlePosters, both
default false, so the fetch 404'd and the packet, which rightly keeps printing
without its map, printed page 1 blank.

The dx-maps config now sets posterSources: the 28 sources, without the 41 MB of
.webp rasters. Measured: 27 MB raw, 7 MB gzipped. The copies are byte-identical
to the tracked data/posters/*.html, so git stores each blob once and a push adds
no new pack bytes. The code default stays false for other deployments.

check:poster-print-fallback now RUNS by default instead of skipping. Part 1 needs
no browser and runs in every wave: when the config ships sources, the built tree
must carry every data/posters/*.html, byte-identical (a stale copy prints an old
map just as quietly). It FAILs 28 of 28 on the tree before this change and PASSes
after. Part 2 clicks the real print control and is NOT MEASURED (75) outside the
browser slot; with no url it serves this checkout and opens the UPG-100 wall. The
skips that exited 0, which a wave reads as a pass, now exit 75. Registered in
gates:post.
This commit is contained in:
Doxa Maps 2026-09-29 00:15:19 +00:00
parent 490dfd1f8c
commit e57f64b64b
4 changed files with 68 additions and 24 deletions

View file

@ -22,31 +22,67 @@
// request for posters/<id>.webp is failed at the network layer, which is exactly
// the default-drop condition, and the .html is left reachable.
//
// Usage: node internal/check-poster-print-fallback.mjs <wallUrl> [zoneId]
import { chromium } from './lib/playwright-guarded.mjs'
import { requireThisTree } from './_serves-this-tree.mjs'
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
// Usage: node internal/check-poster-print-fallback.mjs [wallUrl] [zoneId]
// With no wallUrl it serves THIS checkout's public/ on a free port and opens the UPG-100 poster wall.
import { chromium, requireBrowserSlot, NOT_MEASURED } from './lib/playwright-guarded.mjs'
import { requireThisTree, serveThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { join, resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
import { fileURLToPath as _fileURLToPath } from 'node:url'
// SKIP, never fail, when the preconditions are absent. This gate needs a SERVED tree
// that carries posters/<id>.html (posterSources:true or bundlePosters:true). The
// default tree ships neither, and a clean checkout has no server at all — failing the
// build on that would only teach people to disable the gate. It says what is missing.
const WALL = process.argv[2] || process.env.POSTER_WALL_URL
const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
// ★ PART 1, NO BROWSER, SO IT RUNS IN EVERY WAVE: the BUILT TREE carries the poster sources its build
// config ships. The prayer packet's first page (the zone map) and this print fallback both fetch
// <tree>/assets/posters/<id>.html. A tree without them printed page 1 BLANK: the packet swallows a
// failed map fetch so the other pages still print, so nothing said why. Byte-identical, not merely
// present: a stale copy prints an old map just as quietly.
const CFG = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
if (CFG.posterSources !== true && CFG.bundlePosters !== true) {
console.log('POSTER-PRINT-FALLBACK: NOT MEASURED — this build config ships no poster sources (posterSources and')
console.log(' bundlePosters are both off), so a tree built from it has nothing to rasterise.')
process.exit(NOT_MEASURED)
}
const SRC_DIR = join(ROOT, 'data', 'posters')
const TREE_DIR = join(ROOT, 'public', TREE_NAME, 'assets', 'posters')
const sources = existsSync(SRC_DIR) ? readdirSync(SRC_DIR).filter((f) => f.endsWith('.html')).sort() : []
const missing = sources.filter((f) => !existsSync(join(TREE_DIR, f)))
const stale = sources.filter((f) => !missing.includes(f)
&& !readFileSync(join(TREE_DIR, f)).equals(readFileSync(join(SRC_DIR, f))))
if (!sources.length || missing.length || stale.length) {
console.error('POSTER-PRINT-FALLBACK: FAIL — the built tree does not carry the poster sources its config ships.')
if (!sources.length) console.error(' - data/posters/ holds no <id>.html at all')
if (missing.length) console.error(` - ${missing.length} of ${sources.length} missing from public/${TREE_NAME}/assets/posters/: ${missing.slice(0, 6).join(', ')}`)
if (stale.length) console.error(` - ${stale.length} differ from data/posters/ (the tree is older than its source): ${stale.slice(0, 6).join(', ')}`)
console.error(' Regenerate the tree (node internal/generate-tree.mjs) and commit public/.')
process.exit(1)
}
console.log(` · the tree carries all ${sources.length} poster sources, byte-identical to data/posters/`)
// PART 2, THE BROWSER: only on the browser seat, inside the slot. Anywhere else this is NOT MEASURED
// (75), never a pass: part 1 above is what an ordinary wave can vouch for.
requireBrowserSlot('POSTER-PRINT-FALLBACK')
// No url given: serve THIS checkout and open its own UPG-100 wall, so the gate RUNS by default. It
// used to SKIP with exit 0 for want of an argument, which a wave reads as a pass.
let WALL = process.argv[2] || process.env.POSTER_WALL_URL || ''
if (!WALL) {
const own = await serveThisTree(join(ROOT, 'public'))
if (!own) {
console.log('POSTER-PRINT-FALLBACK: NOT MEASURED — could not start a server for this checkout.')
process.exit(NOT_MEASURED)
}
process.once('exit', () => own.stop())
WALL = `${own.origin}/${TREE_NAME}/my-upg-100-list/poster-wall/index.html`
}
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port
// belongs to whichever server started first — measured 2026-09-24 on :4173, where browser gates
// reported confidently about a build nobody here had touched. 75 (EX_TEMPFAIL), not 1: the wrong
// tree is a precondition, not a product defect, and the wave runner reports 75 as ⊘ rather than ✗.
// A file:// target passes straight through — there is no server there to be wrong about.
await requireThisTree(WALL, ROOT, 'POSTER-PRINT-FALLBACK')
if (!WALL) {
console.log('POSTER-PRINT-FALLBACK: SKIP — no wall url given.')
console.log(' Pass one, or set POSTER_WALL_URL. Needs a tree built with posterSources:true')
console.log(' and a server: `node library/scripts/posters/publish-html.mjs && bun run tree`.')
process.exit(0)
}
// The poster SOURCE must be reachable before any of this means anything. Without it
// the fallback has nothing to rasterise and every assertion below would report a
@ -62,13 +98,13 @@ const srcUrl = new URL(`../../assets/posters/${probeId}.html`, WALL).href
try {
const r = await fetch(srcUrl)
if (!r.ok) {
console.log(`POSTER-PRINT-FALLBACK: SKIP — ${srcUrl} is ${r.status}.`)
console.log(' The tree was not built with posterSources:true, so there is no poster source to rasterise.')
process.exit(0)
console.log(`POSTER-PRINT-FALLBACK: NOT MEASURED — ${srcUrl} is ${r.status}.`)
console.log(' The SERVED tree lacks this poster source (another build, or one made with posterSources off).')
process.exit(NOT_MEASURED)
}
} catch {
console.log(`POSTER-PRINT-FALLBACK: SKIP — cannot reach ${srcUrl} (no server?).`)
process.exit(0)
console.log(`POSTER-PRINT-FALLBACK: NOT MEASURED — cannot reach ${srcUrl} (no server?).`)
process.exit(NOT_MEASURED)
}
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] })

View file

@ -1818,13 +1818,14 @@ function main() {
// <id>.webp 378 files, 40.9 MB — the print-resolution rasters. This is the weight
// that pushed the tree to 80MB and got the push rejected.
// <id>.html 28 files, the poster SOURCE the browser rasterises when no .webp is
// present (library/packet/poster-raster.js). ~0.3 MB each.
// present (library/packet/poster-raster.js). 27 MB raw, 7 MB gzipped.
// "Neither" is what a default drop got, and it is why BOTH full-size paths 404'd:
// export fell back to fetching <id>.html and that was not shipped either, so the
// fallback had nothing to fetch. posterSources ships the sources WITHOUT the rasters,
// which is the only combination the old boolean could not express.
// Default stays FALSE — 28 sources are ~27.9 MB raw, real weight that needs a
// deployment decision, not a silent default. This makes the choice available.
// The code default stays FALSE (real weight is a per-deployment decision); the dx-maps config
// turns it ON, because the prayer packet's map page fetches these and printed blank without them.
// The copies are byte-identical to the tracked data/posters/*.html, so git stores each once.
const posterExts = DEST_CONFIG.bundlePosters ? null // everything
: (DEST_CONFIG.posterSources ? ['.html'] : []) // sources only / nothing
const assetDirs = (posterExts === null || posterExts.length)

View file

@ -299,7 +299,7 @@
"dev": "bun x vite",
"fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:poster-print-fallback",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:gospel-whiteboard",
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",

View file

@ -23,6 +23,13 @@
"key-cities"
],
"qaWidget": false,
"posterSources": true,
"//posterSources": [
"Ship posters/<id>.html (the 28 poster SOURCES, 27 MB raw, 7 MB gzipped) in <tree>/assets/posters/, without",
"the 41 MB of .webp rasters. The prayer packet's first page (the zone map) and the wall's print fallback",
"fetch these; without them the map page printed blank and the fetch failure was swallowed. The copies",
"are byte-identical to data/posters/, so git stores each file once. check:poster-print-fallback asserts it."
],
"//token": [
"THE PUBLIC MAPBOX KEY, BAKED ON PURPOSE (Driver ruling 2026-09-21): 'all maps can use the pk",
"from the api, no problem \u2014 if they want to set a mapbox sk they can'. This is token-ladder",