diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index a3643970b..727b28260 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -83,6 +83,21 @@ jobs: - name: Install dependencies (locked) run: bun install --frozen-lockfile + # ★ BUILD ONE PACKET BEFORE THE GATES, so check:packets-all has something to check. + # library/scripts/posters/out/ is gitignored — both the records cache and every + # generated packet — so a clean CI checkout had NONE, that sweep SKIPped on every + # single build, and every packet assertion in this tree (the 12 prayer points, the + # wheel's 12 labels, page 1's geography, the print layer) was never once evaluated + # here. A SKIP that is always taken is indistinguishable from an unwired gate. + # It builds from a 20KB COMMITTED fixture: no API, no network, no photo fetch. The + # packet stamps itself "fixture nomap" (CI has no poster sources either), which the + # gate reads and accepts as declared — while still asserting everything that does + # not depend on live data. It runs BEFORE build:unlocked because gates:post is + # inside that script. + - name: Build one packet from the committed fixture + run: bun run packet:ci + + # THE AUTHORITATIVE BUILD. build:unlocked is the real chain (clean -> token # -> per-bundle vite -> tree -> drop/parse checks -> verify -> gallery). We # call :unlocked, NOT `bun run build`, because CI IS the single builder now — diff --git a/documentation/README-SECTION-3-cli-commands.md b/documentation/README-SECTION-3-cli-commands.md index d144255e3..958e61f31 100644 --- a/documentation/README-SECTION-3-cli-commands.md +++ b/documentation/README-SECTION-3-cli-commands.md @@ -49,7 +49,7 @@ The scripts these commands are grounded in (from this branch's `package.json`): "build": "bash internal/build-lock.sh", "build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery", "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:wp-embed check:doc-scripts", -"gates:post": "node internal/run-parallel.mjs --label=post -- 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: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", +"gates:post": "node internal/run-parallel.mjs --label=post -- 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: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", "build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time "preview": "bun x vite preview", "serve": "node internal/serve-strict.mjs", // 404s a directory, like production diff --git a/internal/check-packet-render.mjs b/internal/check-packet-render.mjs index 1c450e7b2..f2139d203 100644 --- a/internal/check-packet-render.mjs +++ b/internal/check-packet-render.mjs @@ -346,7 +346,9 @@ if (r.cards && r.imgs + r.nophoto !== r.cards) { } // The packet's own stamp is authoritative; the env var stays as an override for // artefacts generated before data-packet-build existed. -const expectPhotos = process.env.PACKET_EXPECT_PHOTOS !== '0' && !/\bnophotos\b/.test(build) +// `fixture` implies no photos: a fixture packet is built from a committed records subset +// with image_url stripped precisely so CI never fetches ~43 images from someone else's CDN. +const expectPhotos = process.env.PACKET_EXPECT_PHOTOS !== '0' && !/\b(nophotos|fixture)\b/.test(build) if (r.cards && expectPhotos && r.imgs === 0) { fails.push(`${r.cards} people cards and NOT ONE photo — the packet spec is "20 per page ` + 'WITH IMAGES", and every source record carries an image_url, so this is a skipped or ' diff --git a/internal/generate-packet-fixture.mjs b/internal/generate-packet-fixture.mjs new file mode 100644 index 000000000..b29a0fd1c --- /dev/null +++ b/internal/generate-packet-fixture.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +// generate-packet-fixture — a small COMMITTED records subset, so CI can build a packet. +// +// ★ WHY THIS EXISTS. library/scripts/posters/out/ is gitignored — both the records cache +// and every generated packet — so on a clean checkout check:packets-all finds nothing and +// SKIPs. MEASURED 2026-09-24: CI runs that sweep on every build through gates:post, and it +// has SKIPped every time. Every packet assertion in this tree — the 12 prayer points, the +// wheel's 12 labels, page 1's geography paths, the print layer, the build stamp — has never +// once been evaluated in CI. 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. +// +// ★ WHY A FIXTURE RATHER THAN A LIVE FETCH. Building a real packet needs 2,107 records +// (887KB, live data that goes stale) and, with photos, ~95 CDN requests. Neither belongs in +// every CI run. This carries ONE zone's records — the smallest, Tibet at 43 slugs, 20KB — +// with image_url stripped so it can never reach for a photo. +// +// ★ AND IT IS DERIVED, NOT HAND-MAINTAINED. Membership comes from clusters.json, which IS +// committed, so this can be regenerated and content-compared (check:packet-fixture) exactly +// like the capability-exemplar table. A fixture nobody can verify is a fixture that rots. +// +// ⚠ IT IS NOT A SUBSTITUTE FOR REAL PACKETS. PACKET_RECORDS is opt-in and never the default: +// a fixture packet proves the RENDERER and the GATE work end to end, not that today's live +// data is sound. The 11-locale sweep against real records stays the real verification. +// +// Usage: node internal/generate-packet-fixture.mjs [--check] +// (reads the live records cache; run after a normal `node library/scripts/posters/packet.mjs`) +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const CACHE = join(ROOT, 'library/scripts/posters/out/_records.json') +const CLUSTERS = join(ROOT, 'plugin-designs/my-upg-100-list/data/clusters.json') +const OUT_DIR = join(ROOT, 'library/scripts/posters/fixtures') +const OUT = join(OUT_DIR, 'records-tibet.json') +// The smallest zone by slug count, so the fixture stays small. Its membership lives in +// clusters.json; if that zone is ever renamed this script fails loudly rather than +// silently emitting an empty fixture. +const ZONE = 'asia-china-tibet-autonomous-region' + +const CHECK = process.argv.includes('--check') + +if (!existsSync(CLUSTERS)) { + console.log('PACKET-FIXTURE: SKIP — no clusters.json.') + process.exit(0) +} +const clusters = JSON.parse(readFileSync(CLUSTERS, 'utf8')) +const zones = [...(clusters.lists || []), ...(clusters.asia_lists || []), ...(clusters.uncovered_lists || [])] +const zone = zones.find((z) => z && z.id === ZONE) +if (!zone || !zone.selector || zone.selector.type !== 'pg_slugs') { + console.error(`PACKET-FIXTURE: FAIL — ${ZONE} is not a pg_slugs zone in clusters.json.`) + console.error(' The fixture is keyed to that zone; pick another and update ZONE here.') + process.exit(1) +} +const want = new Set(zone.selector.values || []) + +// ── CHECK MODE: the committed fixture must still match clusters.json ──────── +if (CHECK) { + if (!existsSync(OUT)) { + console.error(`PACKET-FIXTURE: FAIL — ${OUT.slice(ROOT.length + 1)} does not exist.`) + console.error(' Fix: node internal/generate-packet-fixture.mjs (then commit it)') + process.exit(1) + } + const have = JSON.parse(readFileSync(OUT, 'utf8')) + const haveSlugs = new Set(have.map((r) => r.slug)) + const missing = [...want].filter((s) => !haveSlugs.has(s)) + const extra = [...haveSlugs].filter((s) => !want.has(s)) + const withPhotos = have.filter((r) => r.image_url).length + console.log(`PACKET-FIXTURE: ${have.length} record(s) for ${ZONE}.`) + const fails = [] + if (missing.length) fails.push(`${missing.length} slug(s) in clusters.json are absent: ${missing.slice(0, 4).join(', ')}`) + if (extra.length) fails.push(`${extra.length} record(s) are no longer in the zone: ${extra.slice(0, 4).join(', ')}`) + // image_url must stay stripped, or CI starts fetching ~43 photos from someone else's CDN. + if (withPhotos) fails.push(`${withPhotos} record(s) still carry image_url — the fixture must never fetch photos`) + if (fails.length) { + console.error(`\nPACKET-FIXTURE: FAIL — ${fails.length} problem(s)`) + for (const f of fails) console.error(' - ' + f) + console.error('\n Fix: node internal/generate-packet-fixture.mjs (then commit it)') + process.exit(1) + } + console.log('PACKET-FIXTURE: PASS — the committed fixture matches clusters.json and fetches no photos.') + process.exit(0) +} + +// ── GENERATE ──────────────────────────────────────────────────────────────── +if (!existsSync(CACHE)) { + console.error('PACKET-FIXTURE: FAIL — no records cache to derive from.') + console.error(` Expected ${CACHE.slice(ROOT.length + 1)}.`) + console.error(' Run a normal packet build first: node library/scripts/posters/packet.mjs') + process.exit(1) +} +const recs = JSON.parse(readFileSync(CACHE, 'utf8')) +const subset = (Array.isArray(recs) ? recs : recs.records || []).filter((r) => r && want.has(r.slug)) +if (subset.length !== want.size) { + console.error(`PACKET-FIXTURE: FAIL — matched ${subset.length} of ${want.size} slug(s) in the cache.`) + console.error(' The cache is stale or partial; re-run a full packet build and try again.') + process.exit(1) +} +// STRIP image_url. A fixture that carries photo URLs would make CI fetch ~43 images from +// Joshua Project's CDN on every build, which is both slow and rude; the packet degrades to +// typographic cards, which is exactly what a fixture should exercise. +const lean = subset.map((r) => { + const c = { ...r } + delete c.image_url + c.has_photo = false + return c +}) +mkdirSync(OUT_DIR, { recursive: true }) +writeFileSync(OUT, JSON.stringify(lean)) +const kb = (JSON.stringify(lean).length / 1024).toFixed(0) +console.log(`[packet-fixture] wrote ${OUT.slice(ROOT.length + 1)} — ${lean.length} records for ${ZONE}, ${kb}KB`) diff --git a/library/scripts/posters/fixtures/records-tibet.json b/library/scripts/posters/fixtures/records-tibet.json new file mode 100644 index 000000000..c780d503f --- /dev/null +++ b/library/scripts/posters/fixtures/records-tibet.json @@ -0,0 +1 @@ +[{"slug":"adi","name":"Adi","country_code":"CHN","population":"1400","religion":"E","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://flickr.com/photos/ramesh_lalwani/8169468855/","text":"Ramesh Lalwani - Flickr"}],"latitude":"29.24961200","longitude":"95.17182400","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion"},{"slug":"ainu-china","name":"Ainu","country_code":"CHN","population":"11000","religion":"MSN","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"38.93020500","longitude":"76.16959600","country_code_label":"People's Republic of China","religion_label":"Islam - Sunni"},{"slug":"alxa-mongols","name":"Alxa Mongols","country_code":"CHN","population":"35500","religion":"MSN","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"39.20798700","longitude":"101.67522000","country_code_label":"People's Republic of China","religion_label":"Islam - Sunni"},{"slug":"bogar-lhoba","name":"Bogar Lhoba","country_code":"CHN","population":"5300","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By bojum"}],"latitude":"28.74470000","longitude":"93.64580000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"bogol","name":"Bogol","country_code":"CHN","population":"1400","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"48.46892600","longitude":"124.48821300","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"boyu-tibetans","name":"Boyu Tibetans","country_code":"CHN","population":"5400","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"33.36236100","longitude":"104.58044400","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"bunan","name":"Bunan","country_code":"CHN","population":"2500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"31.79607000","longitude":"78.90344000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"buryat","name":"Buryat","country_code":"CHN","population":"170000","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://commons.wikimedia.org/wiki/File:Me_and_a_Buryat.jpg","text":"Telaneo - Wikimedia"}],"latitude":"48.75926000","longitude":"117.78630000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"butuo-nosu","name":"Butuo Nosu","country_code":"CHN","population":"337000","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Anonymous"}],"latitude":"27.60452900","longitude":"102.87518600","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"cona-monba","name":"Cona Monba","country_code":"CHN","population":"58500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of Joshua Project by Anonymous"}],"latitude":"27.89404900","longitude":"92.20691400","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"darang-deng","name":"Darang Deng","country_code":"CHN","population":"12500","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"28.37980000","longitude":"97.02930000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"enger-yugur","name":"Enger Yugur","country_code":"CHN","population":"5500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://flickr.com/photos/mke1963/154213183/","text":"Mark mke1963 - Flickr"}],"latitude":"37.98815000","longitude":"101.76214000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"ersu","name":"Ersu","country_code":"CHN","population":"48000","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"29.23558500","longitude":"102.35591600","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"geman-deng","name":"Geman Deng","country_code":"CHN","population":"16500","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"28.47324000","longitude":"97.04437000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"heishui","name":"Heishui","country_code":"CHN","population":"48500","religion":"BLA","has_photo":false,"picture_credit":null,"latitude":"32.19164300","longitude":"103.03860100","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"henan-mongols","name":"Henan Mongols","country_code":"CHN","population":"43000","religion":"BLA","has_photo":false,"picture_credit":null,"latitude":"34.73230000","longitude":"101.60860000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"keji","name":"Keji","country_code":"CHN","population":"2600","religion":"MSN","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.paulnoll.com/China/Minorities/China-Nationalities.html","text":"Paul Noll"}],"latitude":"29.65355100","longitude":"91.12095900","country_code_label":"People's Republic of China","religion_label":"Islam - Sunni"},{"slug":"kyrgyz","name":"Kyrgyz","country_code":"CHN","population":"195000","religion":"MOF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://commons.wikimedia.org/wiki/File:Kyrgyz_women_offering_butter_and_salt.jpg","text":"Theklan - Wikimedia"}],"latitude":"38.01774000","longitude":"76.09630000","country_code_label":"People's Republic of China","religion_label":"Islam - Folk"},{"slug":"luzu","name":"Luzu","country_code":"CHN","population":"1600","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"28.33754000","longitude":"101.35823900","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"manyak","name":"Manyak","country_code":"CHN","population":"3000","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"29.67090000","longitude":"101.59510000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"medog-monba","name":"Medog Monba","country_code":"CHN","population":"11000","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"29.46300000","longitude":"95.74060000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"miguba","name":"Miguba","country_code":"CHN","population":"100","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"29.29621200","longitude":"95.31208900","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"ming","name":"Ming","country_code":"CHN","population":"15500","religion":"NAT","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"31.53976000","longitude":"103.68679500","country_code_label":"People's Republic of China","religion_label":"Unaffiliated - Atheism"},{"slug":"minyak","name":"Minyak","country_code":"CHN","population":"35000","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.pesquisas.org.br/","text":"COMIBAM / Sepal"}],"latitude":"29.65400600","longitude":"101.52975300","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"naju","name":"Naju","country_code":"CHN","population":"2500","religion":"EAN","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"27.67805600","longitude":"101.46272000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Animism"},{"slug":"namuyi","name":"Namuyi","country_code":"CHN","population":"11500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"28.59085000","longitude":"101.89760000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"nghari-tibetans","name":"Nghari Tibetans","country_code":"CHN","population":"82500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Peoples of the Buddhist World, Asia Harvest"}],"latitude":"30.97696000","longitude":"81.21100500","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"olot","name":"Olot","country_code":"CHN","population":"2400","religion":"ESM","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"47.87897700","longitude":"124.33623400","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Shamanism"},{"slug":"ongkor","name":"Ongkor","country_code":"CHN","population":"25","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://english.cri.cn/811/2005/08/24/48@13403.htm","text":"CRIENGLISH"}],"latitude":"43.98811800","longitude":"81.23009100","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"puroik","name":"Puroik","country_code":"CHN","population":"450","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Anonymous"}],"latitude":"28.42033000","longitude":"93.10505600","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"saman","name":"Saman","country_code":"CHN","population":"2600","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"47.46389900","longitude":"124.11668900","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"saragh-yugur","name":"Saragh Yugur","country_code":"CHN","population":"10500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"38.97160000","longitude":"99.50310000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"sarikoli-tajiks","name":"Sarikoli Tajiks","country_code":"CHN","population":"20500","religion":"MIS","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Anonymous"}],"latitude":"37.90501000","longitude":"75.14486000","country_code_label":"People's Republic of China","religion_label":"Islam - Shia Imami Ismaili"},{"slug":"shixing","name":"Shixing","country_code":"CHN","population":"4100","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Christopher Timmons"}],"latitude":"28.28800000","longitude":"100.65700000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"suodi-nosu","name":"Suodi Nosu","country_code":"CHN","population":"305000","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"26.86570200","longitude":"102.37629200","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"tongren-bonan","name":"Tongren Bonan","country_code":"CHN","population":"8900","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://flickr.com/photos/ding_zhou/15612978268/","text":"Joe Tym - Flickr"}],"latitude":"35.14500000","longitude":"102.18300000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"uzbeks","name":"Uzbeks","country_code":"CHN","population":"12500","religion":"MSN","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://commons.wikimedia.org/wiki/File:Uzbek_people_(4934182297).jpg","text":"Fulvio Spada - Wikimedia"}],"latitude":"44.14145000","longitude":"90.29497000","country_code_label":"People's Republic of China","religion_label":"Islam - Sunni"},{"slug":"wakhi-china","name":"Wakhi","country_code":"CHN","population":"9600","religion":"MSH","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://flickr.com/photos/100415065@N06/50312622062","text":"Robertino Radovix - Flickr"}],"latitude":"37.38000000","longitude":"75.40200000","country_code_label":"People's Republic of China","religion_label":"Islam - Shia"},{"slug":"western-xibe","name":"Western Xibe","country_code":"CHN","population":"35500","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Epel - Shutterstock"}],"latitude":"43.80236300","longitude":"80.79151000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"wutun","name":"Wutun","country_code":"CHN","population":"3800","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"35.57300000","longitude":"102.05400000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"yidu-lhoba","name":"Yidu Lhoba","country_code":"CHN","population":"12000","religion":"ESM","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Mow Sunil"}],"latitude":"28.97230000","longitude":"96.63810000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Shamanism"},{"slug":"yinuo-nosu","name":"Yinuo Nosu","country_code":"CHN","population":"810000","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Anonymous"}],"latitude":"28.04378200","longitude":"102.44427000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"za","name":"Za","country_code":"CHN","population":"3400","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"28.47990000","longitude":"97.07920000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"}] \ No newline at end of file diff --git a/library/scripts/posters/packet.mjs b/library/scripts/posters/packet.mjs index 1699f5a57..f51825f17 100644 --- a/library/scripts/posters/packet.mjs +++ b/library/scripts/posters/packet.mjs @@ -52,8 +52,14 @@ const LANG = process.env.PACKET_LANG || 'en'; const ONLY = process.env.ONLY || ''; const NOPHOTOS = process.env.NOPHOTOS === '1'; const API = process.env.VITE_API_BASE_URL || process.env.MAP_APP_API_URL; -if (!API) { - console.error('[packet] No API base — set VITE_API_BASE_URL. Failing closed.'); +// FAIL CLOSED without an API — EXCEPT when records come from a committed fixture, which +// is the one case where no API is needed at all. Measured 2026-09-24: this guard runs +// BEFORE loadRecords() ever looks at PACKET_RECORDS, so `bun run packet:ci` exited 1 on a +// tree that had everything it needed. It only passed in hand-testing because I had been +// supplying a dummy host — the real invocation path is what exposed it. +if (!API && !process.env.PACKET_RECORDS) { + console.error('[packet] No API base — set VITE_API_BASE_URL, or PACKET_RECORDS for a fixture build.'); + console.error(' Failing closed.'); process.exit(1); } @@ -109,6 +115,21 @@ const { evaluate } = await import(join(DATA, 'selector-evaluator.js')); const { normalizeRecords, extractRecords } = await import(join(DATA, 'normalize.js')); async function loadRecords() { + // ★ A COMMITTED FIXTURE, so CI can build a packet at all. out/ is gitignored — both the + // records cache AND every generated packet — so on a clean checkout check:packets-all + // finds nothing and SKIPs. MEASURED 2026-09-24: CI runs the sweep on EVERY build via + // gates:post and it has SKIPped every single time, which means every packet assertion in + // this tree (the 12 points, the wheel labels, page 1's geography, the print layer, the + // build stamp) has never once been evaluated there. A SKIP that is always taken is + // indistinguishable from an unwired gate, and the wave-membership census cannot see the + // difference because membership measures INVOCATION, not EVALUATION. + // PACKET_RECORDS points at a small committed subset instead of the live API or the + // gitignored cache. It is deliberately NOT the default: real packets must use real data. + const fixture = process.env.PACKET_RECORDS; + if (fixture) { + if (!existsSync(fixture)) throw new Error(`PACKET_RECORDS set but missing: ${fixture}`); + return JSON.parse(readFileSync(fixture, 'utf8')); + } const cache = join(__dirname, 'out', '_records.json'); if (process.env.OFFLINE && existsSync(cache)) return JSON.parse(readFileSync(cache, 'utf8')); const fields = 'slug,name,country_code,population,religion,image_url,has_photo,picture_credit,latitude,longitude'; @@ -232,7 +253,13 @@ for (const z of zones) { // session, packet-strings.json was 303s newer than the packets while being byte-identical // to HEAD, because a probe restored it from backup. Record the INTENT, which is a fact // about the build, not the clock. - const provenance = [NOPHOTOS ? 'nophotos' : null, mapMiss ? 'nomap' : null] + // `fixture` is a build fact like the others: a fixture packet is built from a committed + // records subset with image_url stripped, so it legitimately has no photos and its + // records are not today's live data. Without this the gate reports "every source record + // carries an image_url, so this is a skipped or failed photo fetch" — a true sentence + // about real records and a false one about this packet. + const provenance = [process.env.PACKET_RECORDS ? 'fixture' : null, + NOPHOTOS ? 'nophotos' : null, mapMiss ? 'nomap' : null] .filter(Boolean).join(' ') || 'full'; const stamped = html.replace('.html for page 1's map, which `bun run posters` writes — without it the packet still builds and page 1 is simply BLANK, so the script now warns per-zone AND in its summary. PACKET_LANG= for one language, ONLY= for one zone, NOPHOTOS=1 to skip the photo fetch.", + "packet-fixture": "node internal/generate-packet-fixture.mjs # regenerate the committed records subset (smallest zone, image_url stripped) that lets CI build a packet at all — out/ is gitignored, so a clean checkout has neither the records cache nor any packet.", + "check:packet-fixture": "node internal/generate-packet-fixture.mjs --check # the committed fixture must still match clusters.json and must never carry image_url (CI would fetch 43 photos from someone else's CDN).", + "packet:ci": "sh -c 'F=\"$PWD/library/scripts/posters/fixtures/records-tibet.json\"; PACKET_RECORDS=\"$F\" ONLY=asia-china-tibet-autonomous-region node library/scripts/posters/packet.mjs' # build ONE packet from the committed fixture, no network. This is what makes check:packets-all evaluate something in CI instead of SKIPping, as it had on every build.", "token": "node internal/write-token.mjs", "smoke": "node internal/smoke.mjs", "smoke:embeds": "node internal/embed-smoke.mjs", @@ -154,7 +157,7 @@ "check:doc-scripts:self-test": "node internal/check-doc-quotes-scripts.mjs --self-test", "check:legend-source": "node internal/check-legend-matches-source.mjs # the numbers the maps SHOW must match the numbers the live SOURCE has. Every other gate checks INTERNAL consistency — a page can agree with itself perfectly while its legend has stopped describing reality. Offline SKIPs (exit 0); only a reachable endpoint that disagrees fails.", "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:wp-embed check:doc-scripts", - "gates:post": "node internal/run-parallel.mjs --label=post -- 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: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", + "gates:post": "node internal/run-parallel.mjs --label=post -- 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: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", "build:bundles": "node internal/run-parallel.mjs --bundles", "check:maplibre-drops": "node internal/check-maplibre-layer-drops.mjs", "check:tabs-localized": "node internal/check-tabs-localized.mjs # a chosen locale must CHANGE the rendered tab text. check:i18n-parity compares catalogs to each OTHER, so it cannot tell a shipped translation from a dead one; tabs.json is reached only via a DYNAMIC key (SemanticTreeLegend.vue:40), invisible to any grep. Renders it and reads the result.",