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.
112 lines
6.3 KiB
JavaScript
112 lines
6.3 KiB
JavaScript
#!/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`)
|