diff --git a/internal/check-packet-render.mjs b/internal/check-packet-render.mjs index 766365d92..56c739c50 100644 --- a/internal/check-packet-render.mjs +++ b/internal/check-packet-render.mjs @@ -163,9 +163,33 @@ const r = await page.evaluate(() => { // Whole-map property, NOT one sampled element — see header note 2. const mapFaults = (() => { - const svg = document.querySelector('.sheet svg') - if (!svg) return ['no map svg found'] + // ⚠ SCOPE TO PAGE 1. This was `document.querySelector('.sheet svg')` — "the first svg + // ANYWHERE" — which is a FALLBACK SELECTOR, and a fallback selector turns an absence + // into a pass. Measured 2026-09-24: deleting the map svg from page 1 entirely left this + // gate reporting "map: palette resolves, not black — PASS, map painted", because the + // query silently retargeted to the PRAYER WHEEL's svg two sheets later. The wheel has no + // CSS variables to leave unresolved and no black fills, so both checks passed vacuously + // while page 1 — "the map one-pager", the first line of the Driver's packet spec — was + // blank. The sheet count stayed 8 because the empty sheet still exists. + // The packet's page order is the product spec and does not vary: sheet 0 IS the map page. + const sheet0 = document.querySelector('.sheet') + const svg = sheet0 ? sheet0.querySelector('svg') : null + if (!svg) return ['no map svg on page 1 — the map one-pager rendered without its map'] const faults = [] + // ⚠ AND ASSERT IT PAINTS GEOGRAPHY. Palette-resolves + not-black are both properties a + // map with ZERO country shapes satisfies perfectly. Geography is counted as paths whose + // `d` is long enough to be a coastline rather than an icon or a chart tick. MEASURED + // across zones: laos 13, brazil 18, middle-east-core 55 — it varies with how many + // countries a zone spans, so the floor is well under the smallest rather than an exact + // count. Zones whose groups do not resolve are SKIPPED by the generator and never reach + // this gate ("europe: no groups resolved — skipped"), so a zero here is a real fault. + const geoPaths = [...svg.querySelectorAll('path')] + .filter((e) => (e.getAttribute('d') || '').length > 200).length + if (geoPaths < 5) { + faults.push(`only ${geoPaths} geography path(s) on the map page — the map one-pager is ` + + 'blank or drew nothing but furniture (a correct palette and no black fills are both ' + + 'satisfied by a map with no countries on it)') + } const names = [...new Set((svg.outerHTML.match(/var\((--[a-z0-9-]+)/g) || []).map((v) => v.slice(4)))] const cs = getComputedStyle(svg) for (const n of names) if (!cs.getPropertyValue(n).trim()) faults.push(`unresolved ${n}`) @@ -235,6 +259,14 @@ const r = await page.evaluate(() => { })(), metacamp: (body.match(/metacamp\.org/g) || []).length, mapFaults, + // Printed beside the verdict: a reader should see the number the assertion rests on, + // not just the word "painted". + mapGeoPaths: (() => { + const sheet0 = document.querySelector('.sheet') + const svg = sheet0 ? sheet0.querySelector('svg') : null + if (!svg) return -1 + return [...svg.querySelectorAll('path')].filter((e) => (e.getAttribute('d') || '').length > 200).length + })(), // The per-page "{from}-{to} of {count}" headers, as DIGIT TRIPLES rather than text. // Read from the rendered DOM (not the source template) so what is checked is what a // reader would see. Locale digits are normalised to ASCII first: several catalogs @@ -329,7 +361,24 @@ if (r.points !== POINTS_EXPECT) { + 'carries its "12 Prayer Points" heading') } if (r.metacamp !== 2) fails.push(`${r.metacamp} metacamp.org attributions, expected exactly 2`) -if (r.mapFaults.length) fails.push(`map: ${r.mapFaults.join('; ')}`) +// THE MAP PAGE IS OPT-OUT-ABLE, THE SAME WAY THE PHOTOS ARE — and for the same reason. +// packet.mjs lifts the map from out/posters/.html and, when that file is absent, +// deliberately "still builds (cover page without the map) rather than failing". That soft +// fail is intentional, but it is also INVISIBLE: measured 2026-09-24, a packet built with +// no poster sources has 8 sheets and an present (the prayer wheel's) and carries no +// marker at all, so it is structurally indistinguishable from a good one while page 1 +// promises a map one-pager and delivers an empty sheet. doxa-maps-plugin-studio ships 0 +// 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' +if (r.mapFaults.length && expectMap) { + fails.push(`map: ${r.mapFaults.join('; ')}` + + (r.mapGeoPaths === -1 + ? ' — build the poster sources first (node library/scripts/posters/generate.mjs), or set ' + + 'PACKET_EXPECT_MAP=0 to declare this a deliberately map-less packet' + : '')) +} if (fontStatus.declared && fontStatus.loaded !== fontStatus.declared) { fails.push(`a declared @font-face did not load (${fontStatus.names.join(', ')}) — ` + 'text in that script will print as empty boxes') @@ -478,7 +527,9 @@ console.log(` wheel segments : ${r.wheelSegs} (${r.wheelLabels}/${WHEEL_LABE console.log(` Mark 5 in full : ${r.mark5Len} chars` + `${scriptureIsEnglish ? ' ⚠ UNTRANSLATED (English fallback)' : ''}`) console.log(` metacamp.org : ${r.metacamp}`) -console.log(` map : ${r.mapFaults.length ? r.mapFaults.join('; ') : 'palette resolves, not black'}`) +console.log(` map (page 1) : ${r.mapFaults.length + ? `${r.mapFaults.join('; ')}${expectMap ? '' : ' [declared deliberate: 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)` : ''}`) console.log(` layout : ${r.cards} cards / ${PER_PAGE} per page ` @@ -494,4 +545,8 @@ if (fails.length) { for (const f of fails) console.error(` - ${f}`) process.exit(1) } -console.log(`\nPACKET-RENDER: PASS — ${r.sheets} sheets, ${r.sheets} pages, portrait, map painted, content intact.`) +// ⚠ THE VERDICT MUST NOT CLAIM MORE THAN WAS CHECKED. It read "map painted" unconditionally, +// so a packet passing under PACKET_EXPECT_MAP=0 — one with NO map on page 1 — was summarised +// as having a painted map. The summary line is the part a reader actually quotes. +console.log(`\nPACKET-RENDER: PASS — ${r.sheets} sheets, ${r.sheets} pages, portrait, ` + + `${r.mapFaults.length ? 'NO map (declared)' : 'map painted'}, content intact.`)