doxa-maps/internal/check-packet-map-missing-loud.mjs
Doxa Maps 646c23cc0e Put the missing-map sentence in every packet language
The page-1 notice for a missing map existed only in English, which
turns check:packet-strings-parity red: the packet catalogue must be
whole in every language, and English-by-choice is not one of its
declared exceptions (only the licensed Mark 5 passage is).

All eleven blocks now carry the sentence, reusing each block's own
page-1 wording for "prayer list" so the notice reads like the line
above it. The English says "prayer list" rather than "zone": zone is an
internal term, and the page itself calls it a prayer list. The Chinese
block is Traditional script, so the sentence is too, with the Taiwan
term for printing.

The loud gate's note described the old English-only state; it now
reports which locales fall back, or that none do.
2026-09-29 00:59:14 +00:00

104 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
/**
* check-packet-map-missing-loud — page 1 of a prayer packet must SAY when its map is missing.
*
* The packet's first page draws the zone map from `mapSvg`. When the map fetch fails — which every
* build did until the dx-maps config shipped the poster sources (posterSources), and any build that
* turns them off still does — the page used to interpolate an empty string: a title, a subtitle, a
* privacy footnote, and nothing where the map belongs. A failed fetch and a successful render were
* BYTE-INDISTINGUISHABLE in the printed PDF, and the packet read as undeveloped.
*
* ★ TWO-SIDED, because a one-sided test here passes a constant:
* (1) mapSvg EMPTY → the notice appears, in the map's place, with real text (not the key)
* (2) mapSvg PRESENT → the notice does NOT appear, and the SVG is on the page untouched
* A renderer that ALWAYS printed the notice would pass (1) alone; one that never did would pass (2)
* alone. Only both together describe "loud exactly when missing".
*
* Also asserts the per-key fallback: a locale with no translation for the notice (every locale but
* English, by design) prints the English sentence rather than the raw key or nothing. A notice that
* rendered as `map_unavailable` would be louder, but it would be wrong in a different way.
*
* Runs in both variants: lightweight keeps page 1, so it must be loud too.
*
* Exit 0 pass · 1 fail · 75 NOT MEASURED.
*/
import { readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '..')
let createPacketRenderer, strings
try {
;({ createPacketRenderer } = await import(join(REPO, 'library/packet/packet-render.js')))
strings = JSON.parse(readFileSync(join(REPO, 'library/scripts/posters/packet-strings.json'), 'utf8'))
} catch (e) {
console.error(`MAP-MISSING-LOUD: NOT MEASURED (exit 75) — the renderer or its strings did not load: ${e.message}`)
process.exit(75)
}
const EN_NOTICE = strings.en?.map_unavailable
if (!EN_NOTICE) {
console.error('MAP-MISSING-LOUD: FAIL — packet-strings.json en has no map_unavailable sentence, so there is')
console.error(' nothing for page 1 to say. The notice must be English-first; other locales fall back to it.')
process.exit(1)
}
const zone = { id: 'probe-zone', label: 'Probe Zone' }
// A recognisable SVG so side (2) can prove the REAL map survives, not just that the notice is absent.
const SVG = '<svg class="probe-map" viewBox="0 0 10 10"><rect width="10" height="10"/></svg>'
/** The HTML of page 1 only — everything before the second sheet. */
function page1(html) {
const starts = [...html.matchAll(/<div class="sheet[^"]*">/g)].map((m) => m.index)
if (!starts.length) return ''
return html.slice(starts[0], starts[1] ?? html.length)
}
const fails = []
const notes = []
for (const lang of ['en', 'es', 'ar']) {
const R = createPacketRenderer({ strings, lang })
for (const variant of ['full', 'lightweight']) {
const tag = `${lang}/${variant}`
// (1) MISSING → loud.
const empty = page1(R.buildPacket(zone, [], new Map(), '', null, null, variant, null))
if (!empty) { fails.push(`${tag}: no page 1 rendered at all`); continue }
if (!empty.includes('class="map-missing"')) {
fails.push(`${tag}: mapSvg empty, but page 1 has NO missing-map notice — the silent cover this gate exists to prevent`)
} else if (empty.includes('map_unavailable')) {
fails.push(`${tag}: the notice printed the raw KEY "map_unavailable" instead of a sentence`)
} else if (!empty.includes(EN_NOTICE) && lang === 'en') {
fails.push(`${tag}: the notice text is not the English sentence from packet-strings.json`)
}
// (2) PRESENT → quiet, and the real map is on the page.
const full = page1(R.buildPacket(zone, [], new Map(), SVG, null, null, variant, null))
if (full.includes('class="map-missing"')) {
fails.push(`${tag}: mapSvg PRESENT, yet page 1 still prints the missing-map notice — a notice that always shows says nothing`)
}
if (!full.includes('class="probe-map"')) {
fails.push(`${tag}: mapSvg present but the SVG is NOT on page 1 — the fix displaced the map it was guarding`)
}
}
notes.push(` ok ${lang.padEnd(4)} full + lightweight: loud when missing, quiet when present`)
}
// Per-key fallback, stated separately because it is a property of the STRINGS, not the renderer. Every
// block carries its own sentence (check:packet-strings-parity requires it); a block that ever lacks it
// still prints the English one rather than the raw key, which side (1) above asserts.
const missingIn = Object.keys(strings).filter((k) => k !== '_meta' && k !== 'en' && !strings[k].map_unavailable)
notes.push(missingIn.length
? ` ok ${missingIn.length} locale(s) fall back to the English sentence: ${missingIn.join(', ')}`
: ' ok every locale carries its own sentence')
for (const n of notes) console.log(n)
if (fails.length) {
console.error('MAP-MISSING-LOUD: FAIL')
for (const f of fails) console.error(` ✗ ${f}`)
process.exit(1)
}
console.log('MAP-MISSING-LOUD: PASS — page 1 names a missing map and stays quiet when the map is there, in en/es/ar, both variants.')