From 64fed43449bb667a7b2d2b35bd3ced3af0fb3d26 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:21:45 +0000 Subject: [PATCH] packet: stamp how a packet was built, into the packet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit out/packets/ is gitignored, so check:packets-all grades whatever .html files happen to be sitting there — an accidental set, not a current one. On this machine that was 11 freshly generated packets. In doxa-maps-plugin-studio, which shares main with this worktree, it was two NOPHOTOS=1 layout probes of mine from earlier ticks, and the sweep was RED there for anyone who ran the wave. The gate was honest about each file it checked; the set it checked was accidental. The underlying problem is that a fast layout-iteration build and a shippable one were byte-indistinguishable. The packet carried lang and dir and nothing about its own provenance, so the only way to tell them apart was for the person running the gate to remember and pass PACKET_EXPECT_PHOTOS=0. packet.mjs now stamps data-packet-build with the flags it generated under, and the gate reads it. The GENERATOR writes it rather than the gate inferring it: had the gate guessed "no photos present, so probably a probe", it would have excused genuine photo-fetch failures. Tested explicitly — a packet stamped "full" whose map svg is then stripped STILL FAILS. The stamp records intent and cannot excuse a defect the build did not declare. Deliberately not a timestamp. An mtime staleness rule was considered and rejected on evidence from this same session: packet-strings.json was 303s newer than the packets while byte-identical to HEAD, because a probe had restored it from backup, so an mtime rule would have failed a correct tree — the exact mistake fixed two commits ago in the library-index gate. Content comparison is not available here (regenerating hits a live API and takes minutes), so the build records its intent instead of the clock. The studio sweep goes from "2 checked, 0 passed" to "1 checked, 1 passed", with the packet self-declaring build="nophotos nomap". Env vars stay as an override for pre-stamp artefacts, and the declared-deliberate note now names whichever source actually declared it rather than always crediting the env var. All 11 locales stamp "full". i18n re-verified with a partial block (1 string, 3/12 points, 6/12 rhythms): wheel 6 translated and 6 English, 12 point rows with 6 translated, 0 blanks. Wave 34/34, packets-all 11/11 here and 1/1 in the studio. --- internal/check-packet-render.mjs | 17 ++++++++++++++--- library/scripts/posters/packet.mjs | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/internal/check-packet-render.mjs b/internal/check-packet-render.mjs index 0ff999784..1c450e7b2 100644 --- a/internal/check-packet-render.mjs +++ b/internal/check-packet-render.mjs @@ -53,6 +53,14 @@ await page.goto(pathToFileURL(file).href, { waitUntil: 'networkidle' }) // Resolve the rhythm words for THIS packet's language from the shared catalog. // Per-key fallback to English, the same contract the renderer honours. const lang = await page.evaluate(() => document.documentElement.lang || 'en') +// ★ WHAT THE PACKET SAYS ABOUT ITS OWN BUILD. packet.mjs stamps data-packet-build with the +// flags it was generated under ("full", "nophotos", "nomap", or a combination). Without it +// the only way to tell a fast layout-iteration build from a shippable one was for the PERSON +// RUNNING THE GATE to remember and pass PACKET_EXPECT_PHOTOS=0 — which is why the sweep sat +// RED in doxa-maps-plugin-studio on two probe artifacts nobody intended to ship. +// The ARTEFACT declares its own intent; the env vars below remain as an override for packets +// generated before this stamp existed. +const build = await page.evaluate(() => document.documentElement.getAttribute('data-packet-build') || '') // RTL languages must actually be laid out RTL. Without dir="rtl" an Arabic packet // renders readable glyphs LEFT-aligned — it looks like a styling quirk rather than // the bug it is, and no other assertion here would notice. @@ -336,7 +344,9 @@ if (r.cards && r.imgs + r.nophoto !== r.cards) { fails.push(`${r.cards} people cards but ${r.imgs} photo + ${r.nophoto} typographic = ` + `${r.imgs + r.nophoto} — every card must be exactly one of the two`) } -const expectPhotos = process.env.PACKET_EXPECT_PHOTOS !== '0' +// 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) 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 ' @@ -389,7 +399,7 @@ if (r.metacamp !== 2) fails.push(`${r.metacamp} metacamp.org attributions, expec // poster html files against wt-packet's 28, so every packet generated there is degraded // this way and nothing said so. // Default to FAILING, so the degradation has to be DECLARED rather than discovered. -const expectMap = process.env.PACKET_EXPECT_MAP !== '0' +const expectMap = process.env.PACKET_EXPECT_MAP !== '0' && !/\bnomap\b/.test(build) if (r.mapFaults.length && expectMap) { fails.push(`map: ${r.mapFaults.join('; ')}` + (r.mapGeoPaths === -1 @@ -562,6 +572,7 @@ if (r.ranges.length) { + 'cross-checks the card count against is missing') } +console.log(` build : ${build || '(unstamped — generated before data-packet-build)'}`) console.log(` sheets/PDF pages : ${r.sheets} / ${pages}`) console.log(` printed artefact : ${(pdfBytes / 1024 / 1024).toFixed(1)}MB, ${pdfFonts} embedded font(s), ` + `${pdfImages} image(s) for ${r.distinctImgSrc} distinct source(s)`) @@ -572,7 +583,7 @@ console.log(` Mark 5 in full : ${r.mark5Len} chars` + `${scriptureIsEnglish ? ' ⚠ UNTRANSLATED (English fallback)' : ''}`) console.log(` metacamp.org : ${r.metacamp}`) console.log(` map (page 1) : ${r.mapFaults.length - ? `${r.mapFaults.join('; ')}${expectMap ? '' : ' [declared deliberate: PACKET_EXPECT_MAP=0]'}` + ? `${r.mapFaults.join('; ')}${expectMap ? '' : ` [declared deliberate: ${/\bnomap\b/.test(build) ? 'data-packet-build="' + build + '"' : 'PACKET_EXPECT_MAP=0'}]`}` : `${r.mapGeoPaths} geography paths, palette resolves, not black`}`) console.log(` page size : ${boxes[0] ? boxes[0].join(' x ') : '?'} pt` + `${boxes.length ? ` (all ${boxes.length} pages, asserted US Letter)` : ''}`) diff --git a/library/scripts/posters/packet.mjs b/library/scripts/posters/packet.mjs index e599f4c26..1699f5a57 100644 --- a/library/scripts/posters/packet.mjs +++ b/library/scripts/posters/packet.mjs @@ -219,7 +219,23 @@ for (const z of zones) { const { html, pageCount, withPhoto } = await buildPacket(z, groups, mapSvg); const name = LANG === 'en' ? `${z.id}.html` : `${z.id}.${LANG}.html`; - writeFileSync(join(OUT, name), html); + // ★ STAMP HOW THIS PACKET WAS BUILT, INTO THE PACKET. out/packets/ is gitignored, so the + // sweep gate (check:packets-all) grades whatever .html files happen to be sitting there — + // an ACCIDENTAL set, not a current one. MEASURED 2026-09-24: doxa-maps-plugin-studio, which + // shares main with this worktree, had two packets from a NOPHOTOS=1 probe days old and the + // sweep was RED there, on artifacts nobody intended to ship. A fast layout-iteration build + // and a shippable one were byte-indistinguishable: the packet carried lang and dir and + // nothing else about its own provenance. + // The GENERATOR stamps this rather than the renderer, because packet-render.js is shared + // with the browser export path and must not know about CLI flags. + // ⚠ NOT A TIMESTAMP. An mtime rule would fail a correct tree — measured in this very + // 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] + .filter(Boolean).join(' ') || 'full'; + const stamped = html.replace('