check:packets-all — skip stale artifacts, and follow the series on the funnel

Two defects, both found by the done-card sweep rather than by looking for them.

1. A STALE ARTIFACT WAS GRADED AS A DEFECT, which is this gate's own recorded bug
   recurring. BUG-PACKET-SWEEP-GRADED-AN-ACCIDENTAL-SET-OF-GITIGNORED-ARTIFACTS is
   marked DONE, but the card was closed without the mechanism changing: the gate
   still readdirSync'd every .html with no freshness test. It went red again on 43
   packets left from a run three days earlier -- reporting "0 prayer tick boxes"
   about files generated before tick boxes existed, so they could not have had
   them.
   The directory is gitignored (.gitignore:94), so nothing in it reaches main or
   another agent; it is one developer's scratch. The gate already knew an EMPTY
   directory is not a failure ("a clean checkout has none"); a directory of
   LEFTOVERS is the same fact wearing different clothes. A packet older than the
   renderer that produces it is now SKIPPED, loudly, with the command to
   regenerate it. The renderer's mtime is the reference because that is what
   decides the shape a packet should have -- comparing against "today" would
   re-fail every morning.

2. THE UNCONDITIONAL --expect-funnel STOPPED BEING TRUE. The comment above it says
   every packet comes from packet.mjs "which always passes FUNNEL". That held
   until SERIES landed: a country packet DROPS the funnel page deliberately,
   because the funnel narrows every people group on earth down to THIS UPG-100
   list, which is not a description of a country. So the gate failed a correct
   packet.
   The expectation now follows the series that produced the packet, identified by
   the series' own NETWORKS text surviving into the artifact, and read from
   series.json rather than hardcoded -- adding a series with funnel:false must not
   require editing this gate. A packet whose series cannot be determined keeps the
   strict expectation, because the safe default for "I do not know" is the
   stricter check.

Verified: PACKETS-ALL PASS with 43 stale skipped and the country packet accepted;
and a POISON TEST inside the tree (a upg100 packet with its funnel-title removed)
is still caught -- so the relaxation discriminates rather than passing everything.
The first poison attempt ran outside the checkout and failed on a missing strings
catalogue, proving nothing; re-run in place.

internal/ is the build owner's under rule 5B -- source sha to studio, not landed
here.
This commit is contained in:
Doxa Maps 2026-09-28 07:09:33 +00:00
parent fbc517c94a
commit 78eb6bdcb8

View file

@ -17,7 +17,7 @@
// SKIP (exit 0) only when the directory is genuinely absent or empty: packets are
// generated on demand and a clean checkout has none. A skip says so out loud, with the
// count, so "0 packets checked" can never read as "all packets passed".
import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
// pathToFileURL, not a bare path: a dynamic import() of an absolute POSIX path works by luck on
// linux and throws ERR_UNSUPPORTED_ESM_URL_SCHEME the moment anyone runs this on windows.
@ -38,9 +38,33 @@ if (!existsSync(DIR)) {
process.exit(0)
}
const files = readdirSync(DIR).filter((f) => f.endsWith('.html')).sort()
// ★ A STALE ARTIFACT IS NOT A DEFECT, and grading one as a defect is this gate's own recorded bug
// (BUG-PACKET-SWEEP-GRADED-AN-ACCIDENTAL-SET-OF-GITIGNORED-ARTIFACTS). The card was closed without
// the mechanism changing, and the gate went red again on 2026-09-28 over 43 packets left on disk
// from a run three days earlier — before the prayer tick-boxes existed at all, so it reported
// "0 prayer tick boxes" about a file that could not have had them.
//
// This whole directory is GITIGNORED (.gitignore:94 `library/scripts/*/out/**`), so nothing here
// ever reaches main or another agent: it is one developer's scratch. The gate already knows that an
// EMPTY directory is not a failure ("a clean checkout has none"); a directory of LEFTOVERS is the
// same fact wearing different clothes.
//
// So a packet older than the renderer that produces it is SKIPPED, loudly, rather than failed. The
// renderer's own mtime is the reference because that is what decides the shape a packet should have
// — comparing against "today" would re-fail every morning, and comparing against nothing is what
// produced this bug twice.
const RENDERER = resolve(HERE, '..', 'library/packet/packet-render.js')
const rendererMtime = existsSync(RENDERER) ? statSync(RENDERER).mtimeMs : 0
const all = readdirSync(DIR).filter((f) => f.endsWith('.html')).sort()
const stale = all.filter((f) => statSync(join(DIR, f)).mtimeMs < rendererMtime)
const files = all.filter((f) => !stale.includes(f))
if (stale.length) {
console.log(`PACKETS-ALL: skipping ${stale.length} packet(s) older than the renderer — regenerate`)
console.log(` them (ONLY=<id> node library/scripts/posters/packet.mjs) or delete them; this`)
console.log(` directory is gitignored scratch, so a leftover is not a defect.`)
}
if (!files.length) {
console.log(`PACKETS-ALL: SKIP — ${DIR} contains no .html packets`)
console.log(`PACKETS-ALL: SKIP — ${DIR} contains no .html packets newer than the renderer`)
process.exit(0)
}
@ -67,9 +91,30 @@ if (!EXPECT_FUNNEL) {
console.log('PACKETS-ALL: ⚠ PACKETS_ALL_NO_FUNNEL=1 — page 2 is NOT required this run.')
}
// ⚠ THE UNCONDITIONAL ABOVE STOPPED BEING TRUE, and this is the correction. The comment says every
// packet comes from packet.mjs "which always passes FUNNEL" — that held until SERIES landed
// (2026-09-28). A `country` packet DROPS the funnel page deliberately: the funnel narrows every
// people group on earth down to THIS UPG-100 list, which is not a description of a country, and a
// page of true numbers about the wrong thing is worse than no page.
// So the expectation follows the SERIES that produced the packet, read from series.json rather than
// hardcoded here — adding a series with funnel:false must not require editing this gate.
// A packet whose series cannot be determined keeps the old unconditional expectation, because the
// safe default for "I do not know" is the stricter check.
const SERIES_PATH = resolve(HERE, '..', 'library/scripts/posters/series.json')
const SERIES_DEFS = existsSync(SERIES_PATH) ? JSON.parse(readFileSync(SERIES_PATH, 'utf8')) : {}
const NO_FUNNEL_MARKERS = Object.entries(SERIES_DEFS)
.filter(([id, def]) => !id.startsWith('_') && def && def.funnel === false)
.flatMap(([, def]) => (def.networks || []).map(([head]) => head).filter(Boolean))
/** True when this packet's own text identifies it as a series that has no funnel page. */
function expectsFunnel(html) {
if (!NO_FUNNEL_MARKERS.length) return EXPECT_FUNNEL
return !NO_FUNNEL_MARKERS.some((m) => html.includes(m)) && EXPECT_FUNNEL
}
const failed = []
for (const f of files) {
const args = EXPECT_FUNNEL ? [gate, join(DIR, f), '--expect-funnel'] : [gate, join(DIR, f)]
const wantFunnel = expectsFunnel(readFileSync(join(DIR, f), 'utf8'))
const args = wantFunnel ? [gate, join(DIR, f), '--expect-funnel'] : [gate, join(DIR, f)]
const res = spawnSync(process.execPath, args, { encoding: 'utf8' })
const ok = res.status === 0
console.log(` ${ok ? '✓' : '✗'} ${f}`)