doxa-maps/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs

334 lines
27 KiB
JavaScript

#!/usr/bin/env node
// check:country-sheet-one-path — a country's map sheet is drawn through ONE path, so one country
// cannot print two different posters.
//
// WHY: two callers draw a country sheet (the Country Posters wall and the Regions tab's regions-list).
// Each once had its own wrapper around generateCountrySheet, and they drifted twice: the Regions tab
// printed a country in flat teal while the wall printed it in its region's hue, and the Regions tab
// printed India's packet page 1 as "map 1 of 0" while the wall said "map 1 of 1". Both now go through
// @map/packet/countrySheet.js (makeCountrySheet + seriesFor).
//
// ARM A (structure, over the TREE): every plugin file whose CODE (parsed, so comments cannot count)
// names generateCountrySheet is a violation; every caller of makeCountrySheet passes `countries`, so
// its "map N of M" is computed from the same list; and at least the two known callers are found (a
// census that finds fewer is NOT MEASURED, never a pass).
// ARM B (behaviour, over the real library/data/country-posters.json): seriesFor gives named answers:
// India (sheet null) is 1 of 1, every 3x3 country is N of the 3x3 tier with no repeated N, a code
// absent from the data gets no series.
//
// Exit 0 pass, 1 fail, 75 not measured. `--self-test` plants each defect and requires its arm to fail.
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
import { join, relative, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse as parseSfc, babelParse } from '@vue/compiler-sfc'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
const PD = join(ROOT, 'plugin-designs')
const KNOWN_CALLERS = ['plugin-designs/country-posters/country-poster-wall.vue']
// The Regions list may draw a country sheet itself OR delegate to <dxm-poster-section> (whose library type draws it).
// If it still calls makeCountrySheet it must pass arms F and G like the wall; either way the library type must.
const REGIONS_LIST = 'plugin-designs/regions-dashboard/regions-list.vue'
const COUNTRY_TYPE = 'library/components/poster/types/countryPosterType.js'
// ARM H: the library 'country' type (the element's caller) withholds like the wall: rows reject with the sentence,
// a withheld poster's card shows the sentence.
export function armH(typeSrc) {
const fails = []
if (!/makeCountrySheet\(/.test(typeSrc)) fails.push(`${COUNTRY_TYPE}: no longer draws through makeCountrySheet`)
if (!/rowsWithheld\(c\) \? Promise\.reject\(keyedError\('messages\.countryPosterTooMany'/.test(typeSrc)) fails.push(`${COUNTRY_TYPE}: withheld ROWS are not withheld (the element would print India's list)`)
if (!/posterWithheld\(c\) \|\|[^?]*\? t\('messages\.countryPosterTooMany'\)/.test(typeSrc)) fails.push(`${COUNTRY_TYPE}: a withheld POSTER's card does not show the sentence`)
return fails
}
// India's states and union territories draw through their own ONE path; inside this plugin only that helper may
// call the generator directly.
const STATE_HELPER = 'plugin-designs/country-posters/src/indiaStateSheet.js'
const STATE_CALLER = 'plugin-designs/country-posters/country-poster-wall.vue'
function scriptsOf(file, src) {
if (!file.endsWith('.vue')) return [src]
const { descriptor, errors } = parseSfc(src, { filename: file })
if (errors.length) throw new Error(`${file}: SFC parse error: ${errors[0].message || errors[0]}`)
return [descriptor.script, descriptor.scriptSetup].filter(Boolean).map((b) => b.content)
}
function walk(node, visit) {
if (!node || typeof node.type !== 'string') return
visit(node)
for (const k of Object.keys(node)) {
if (k === 'loc' || k === 'start' || k === 'end' || k === 'leadingComments' || k === 'trailingComments') continue
const v = node[k]
if (Array.isArray(v)) v.forEach((c) => c && typeof c.type === 'string' && walk(c, visit))
else if (v && typeof v.type === 'string') walk(v, visit)
}
}
// -> { names: Set of identifier names used in code, makeCalls: [{ hasCountries }] }
function codeFacts(file, src) {
const names = new Set()
const makeCalls = []
for (const code of scriptsOf(file, src)) {
const ast = babelParse(code, { sourceType: 'module', plugins: ['typescript'] })
walk(ast.program, (n) => {
if (n.type === 'Identifier') names.add(n.name)
if (n.type === 'CallExpression' && n.callee.type === 'Identifier' && n.callee.name === 'makeCountrySheet') {
const arg = n.arguments[0]
const hasCountries = !!(arg && arg.type === 'ObjectExpression'
&& arg.properties.some((p) => p.key && (p.key.name === 'countries' || p.key.value === 'countries')))
makeCalls.push({ hasCountries })
}
})
}
return { names, makeCalls }
}
function listSources(dir, out = []) {
for (const e of readdirSync(dir)) {
if (e === 'node_modules' || e.startsWith('.')) continue
const p = join(dir, e)
const st = statSync(p)
if (st.isDirectory()) listSources(p, out)
else if (/\.(vue|js|mjs)$/.test(e) && !/\/scripts\//.test(p)) out.push(p)
}
return out
}
// ARM A over a { relPath: source } map. Returns failure strings; `callers` gets the files that call makeCountrySheet.
export function armA(files, callers = []) {
const fails = []
for (const [rel, src] of Object.entries(files)) {
if (!/generateCountrySheet|makeCountrySheet|generateSheet|makeIndiaStateSheet/.test(src)) continue // a text prefilter; the parse decides
const { names, makeCalls } = codeFacts(rel, src)
if (names.has('generateCountrySheet')) fails.push(`${rel}: draws a country sheet directly (generateCountrySheet in code); use makeCountrySheet from @map/packet/countrySheet.js`)
if (rel.startsWith('plugin-designs/country-posters/') && rel !== STATE_HELPER && names.has('generateSheet')) fails.push(`${rel}: calls generateSheet directly; a state sheet goes through makeIndiaStateSheet (${STATE_HELPER})`)
if (rel === STATE_CALLER && names.has('makeIndiaStateSheet')) callers.push(`${rel}#state`)
if (makeCalls.length) callers.push(rel)
makeCalls.forEach((c, i) => { if (!c.hasCountries) fails.push(`${rel}: makeCountrySheet call #${i + 1} passes no \`countries\`, so its "map N of M" is not the wall's`) })
}
return fails
}
// ARM B: named expectations for seriesFor over the real data.
export function armB(seriesFor, countries, sheetFormatFor = null) {
const fails = []
const by = (iso) => countries.find((c) => c.iso3 === iso)
const ind = by('IND')
const s = ind && seriesFor(countries, ind)
// THE LARGE-SHEET FORMAT RULE: India prints 60x96 ONLY (any request but a packet page gets it); a packet page stays a
// page; any other country gets exactly what it asked for.
if (sheetFormatFor) {
const want = [[ind, '60x96', '60x96'], [ind, '66x66', '60x96'], [ind, '36x60', '60x96'], [ind, 'letter', '60x96'], [ind, '3x4', '60x96'], [ind, 'packet', 'packet'],
[by('NPL'), '3x3', '3x3'], [by('NPL'), 'letter', 'letter'], [by('CHN'), '3x4', '3x4'], [by('NPL'), '66x66', '66x66']]
for (const [item, ask, exp] of want) { const got = sheetFormatFor(item, ask); if (got !== exp) fails.push(`sheetFormatFor(${item?.iso3}, ${ask}) = ${got}, expected ${exp}`) }
}
const oversized = countries.filter((c) => (c.sheet ?? null) === null)
if (oversized.length > 1) fails.push(`${oversized.length} countries have no sheet size (${oversized.map((c) => c.iso3).join(', ')}): they now share ONE oversized series, so India prints "map 1 of ${oversized.length}". Decide that deliberately, then update this expectation.`)
if (!ind) fails.push('India is missing from country-posters.json')
else if (!s || s.index !== 1 || s.totalLists !== 1 || s.totalGroups !== ind.count) fails.push(`India (sheet ${JSON.stringify(ind.sheet)}) should be map 1 of 1 with ${ind.count} groups; got ${JSON.stringify(s)}`)
const t3 = countries.filter((c) => c.sheet === '3x3')
const seen = new Set()
for (const c of t3) {
const r = seriesFor(countries, c)
if (!r || r.totalLists !== t3.length || r.index < 1 || r.index > t3.length) { fails.push(`${c.iso3} (3x3) should be N of ${t3.length}; got ${JSON.stringify(r)}`); break }
seen.add(r.index)
}
if (seen.size && seen.size !== t3.length) fails.push(`3x3 tier: ${seen.size} distinct indexes for ${t3.length} countries`)
const absent = seriesFor(countries, { id: 'country-zzz', iso3: 'ZZZ', label: 'ZZZ', count: 3 })
if (absent !== undefined) fails.push(`a country absent from the data should get no series; got ${JSON.stringify(absent)}`)
if (seriesFor(null, ind) !== undefined) fails.push('seriesFor with no list should return undefined')
return fails
}
// ARM C: the state posters over the SERVED table and the renderer's own ADM1 file (named expectations).
export function armC(H, states, adm1Ind) {
const fails = []
const before = JSON.stringify((adm1Ind.features || []).map((f) => f.properties && f.properties.listId)) // BEFORE any call
const withGroups = H.statesWithPosters(states)
const zero = (states || []).filter((st) => !st.count).map((st) => st.name)
if ((states || []).length !== 36) fails.push(`the state table has ${(states || []).length} divisions, expected 36 (28 states + 8 union territories)`)
if (withGroups.length + zero.length !== (states || []).length) fails.push('every division is either postered or named as having none')
const idx = withGroups.map((st) => (H.stateSeries(states, st) || {}).index)
if (new Set(idx).size !== withGroups.length || Math.min(...idx) !== 1 || Math.max(...idx) !== withGroups.length) fails.push(`state series indexes are not 1..${withGroups.length}, unique: ${idx.join(',')}`)
const noShape = withGroups.filter((st) => !H.stateGeometry(H.stateZone(st, '#000'), adm1Ind, st.shapeName)).map((st) => st.name)
if (noShape.length) fails.push(`states with people groups but no outline in gb-IND-ADM1.geojson (their poster would not draw): ${noShape.join(', ')}`)
withGroups.forEach((st) => H.stateGeometry(H.stateZone(st, '#000'), adm1Ind, st.shapeName))
if (JSON.stringify((adm1Ind.features || []).map((f) => f.properties && f.properties.listId)) !== before) fails.push('stateGeometry mutated the shared ADM1 collection')
return fails
}
// ARM D: the state cards' localized names cover EXACTLY the table's divisions in every locale (a runtime-built key,
// `countryPosters.stateNames.${iso}`, that the i18n call-site gates cannot see), English is the table's own name, and
// the generated names carry their source licence.
const NAME_LOCALES = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ro', 'ru', 'ar', 'hi', 'zh']
export function armD(states, areas, meta) {
const fails = []
const want = new Set((states || []).map((st) => st.iso))
for (const l of NAME_LOCALES) {
const names = (areas[l] && areas[l].stateNames) || {}
const have = new Set(Object.keys(names))
const missing = [...want].filter((x) => !have.has(x)), extra = [...have].filter((x) => !want.has(x))
const empty = Object.entries(names).filter(([, v]) => typeof v !== 'string' || !v.trim()).map(([k]) => k)
if (missing.length || extra.length || empty.length) fails.push(`${l} countryPosters.stateNames: missing [${missing}] extra [${extra}] empty [${empty}]`)
}
const en = (areas.en && areas.en.stateNames) || {}
const drift = (states || []).filter((st) => en[st.iso] !== st.name).map((st) => st.iso)
if (drift.length) fails.push(`English state names differ from the table's: ${drift.join(', ')} (re-run emit-india-state-names.mjs)`)
if (!meta || !/CC0/.test(String(meta.licence || ''))) fails.push('india-state-names.meta.json must record the source licence (CC0)')
return fails
}
// ARM F — A DESKTOP-ONLY CARD ON A PHONE (India): in EACH caller (the wall, the Regions list) its rows REJECT with
// poster.phoneLimitAll before any record is fetched, never resolve [] (the print path reads [] as "that list has no
// people groups to print", on a card that has 855), and the card shows the same full-block notice.
export function armF(srcs) {
const fails = []
for (const [name, src] of Object.entries(srcs)) {
if (!/phoneBlocked\((?:c|item)\) \? (?:desktopOnly\(\)|Promise\.reject\(keyedError\('poster\.phoneLimitAll')/.test(src)) fails.push(`${name}: a phone-blocked card's rows do not reject with poster.phoneLimitAll`)
if (/phoneBlocked\((?:c|item)\) \? Promise\.resolve\(\[\]\)/.test(src)) fails.push(`${name}: a phone-blocked card's rows resolve [] (printed as "no people groups")`)
if (/desktopOnly\(\)/.test(src) && !/const desktopOnly = \(\) => Promise\.reject\(keyedError\('poster\.phoneLimitAll'/.test(src)) fails.push(`${name}: desktopOnly() does not reject with poster.phoneLimitAll`)
if (!/phoneBlocked\(c\) \? t(?:Key)?\('poster\.phoneLimitAll'\)/.test(src)) fails.push(`${name}: a phone-blocked card does not show poster.phoneLimitAll`)
}
return fails
}
// ARM E: India is its OWN section, FIRST, counted ONCE. The India country card, the state cards and the none-listed
// cards group together; the section's total is India's own count (not the card sum, which counts 855 twice); on that
// true total it outranks every regional section. PosterWall's default (no sectionTotal) must still be the card sum.
// ARM G — WHAT OF A WHOLE COUNTRY IS WITHHELD (countrySheet.js WITHHELD), and the PREVIEW guard. India:
// (a) BY DEFAULT (every public host: DX Life, the research landing, any embed) its whole-country POSTER is withheld
// (the sentence; largeSheet() null) and its ROWS are withheld;
// (b) only in a PREVIEW context (the Vite dev server: the Plugin Studio on the pilot) the poster prints, 60x96 ONLY; its rows stay withheld. A state card is never withheld.
// Each caller shows the sentence for a withheld POSTER and rejects the rows for withheld ROWS; Country Posters' India
// tab exists only under the preview flag.
export async function armG(cs, srcs, mfeSrc = '') {
const fails = []
const ind = { id: 'country-ind', iso3: 'IND', label: 'India' }
// (a) by default: outside the Vite dev server (here: no import.meta.env.DEV) the preview is shut EVEN IF a page or a
// console sets the global; (b) an open preview (passed explicitly) prints the poster at 66x66.
const was = globalThis.DXM_PREVIEW_UNRELEASED
try {
globalThis.DXM_PREVIEW_UNRELEASED = true
if (cs.previewUnreleased()) fails.push('(a) the preview opens outside the dev server from a global alone: a page/console could print the withheld poster from production')
if (!cs.posterWithheld(ind) || cs.largeSheet(ind)) fails.push("(a) India's whole-country poster is NOT withheld by default: it would ship to every host")
} finally { if (was === undefined) delete globalThis.DXM_PREVIEW_UNRELEASED; else globalThis.DXM_PREVIEW_UNRELEASED = was }
if (!cs.rowsWithheld(ind)) fails.push("India's rows are not withheld: its list/packet/cards (~1,000 pages) would print")
// (b) the open preview itself is proved under Vite (import.meta.env.DEV true) by check:poster-section; here, the size:
const big = cs.LARGE_SHEETS && cs.LARGE_SHEETS.IND
if (!big || big.format !== '60x96' || big.formats.join() !== '60x96') fails.push(`India's whole-country poster is not 60x96 ONLY (${big && big.formats})`)
if (cs.posterWithheld.length > 1 || cs.largeSheet.length > 1) fails.push('posterWithheld()/largeSheet() take a second argument again: a production caller could open the withheld poster')
if (cs.sheetWithheld(ind) !== cs.posterWithheld(ind)) fails.push('sheetWithheld() no longer means the poster')
if (cs.rowsWithheld({ iso3: 'IND', indiaState: true, id: 'india-state-gujarat' })) fails.push('an India STATE card has withheld rows (only the whole country may)')
for (const [name, src] of Object.entries(srcs)) {
if (!/posterWithheld\((?:c|item)\) \? t(?:Key)?\('messages\.countryPosterTooMany'\)/.test(src)) fails.push(`${name}: a withheld POSTER's card does not show messages.countryPosterTooMany`)
if (!/rowsWithheld\((?:c|item)\) \? (?:withheld\(\)|Promise\.reject\(keyedError\('messages\.countryPosterTooMany')/.test(src)) fails.push(`${name}: withheld ROWS are not withheld (its list/packet/cards would print)`)
if (/sheetWithheld\(/.test(src)) fails.push(`${name}: still reads sheetWithheld() (ambiguous: poster or rows?)`)
}
if (mfeSrc && !/\.\.\.\(import\.meta\.env && import\.meta\.env\.DEV && globalThis\.DXM_PREVIEW_UNRELEASED === true && previewOptedIn\(\) \? \[INDIA_PANE\] : \[\]\)/.test(mfeSrc)) fails.push('country-posters-mfe: the India tab is not gated on the DEV-only preview read AND the explicit opt-in (previewOptedIn())')
// the BUILT tree never mentions the flag (every read is compiled away)
try {
const pub = join(ROOT, 'public/dx-maps')
for (const b of readdirSync(pub)) { const f = join(pub, b, `${b}.js`); if (existsSync(f) && readFileSync(f, 'utf8').includes('DXM_PREVIEW_UNRELEASED')) fails.push(`the built ${b}.js mentions DXM_PREVIEW_UNRELEASED (a preview read survived the build)`) }
} catch (_) { /* no built tree here */ }
return fails
}
export function armE(countries, states, wallSrc, posterWallSrc, H) {
const fails = []
if (!/:section-total=/.test(wallSrc)) fails.push('country-poster-wall.vue does not pass :section-total, so India is counted twice')
if (!/String\(c\.iso3[^)]*\)\.toUpperCase\(\) === 'IND'/.test(wallSrc)) fails.push("the wall's isIndia() no longer puts India's country card in the India section")
if (!/props\.sectionTotal \? props\.sectionTotal\(name, items\) : items\.reduce\(\(n, x\) => n \+ \(x\.count \|\| 0\), 0\)/.test(posterWallSrc)) fails.push("PosterWall's default section total is no longer the card sum (every other wall would change)")
const ind = countries.find((c) => c.iso3 === 'IND')
const stateCards = [...H.statesWithPosters(states).map((st) => ({ count: st.count, indiaState: true })), ...states.filter((st) => !st.count).map(() => ({ count: 0, indiaState: true }))]
const groups = new Map()
for (const c of [...countries, ...stateCards]) {
const g = c.indiaState || c.iso3 === 'IND' ? 'India' : (c.region || 'Other')
if (!groups.has(g)) groups.set(g, []); groups.get(g).push(c)
}
const total = (g, items) => (g === 'India' ? ind.count : items.reduce((n, x) => n + (x.count || 0), 0))
const order = [...groups].map(([g, items]) => [g, total(g, items)]).sort((a, b) => b[1] - a[1])
if (order[0][0] !== 'India') fails.push(`India is not the first section: ${order.slice(0, 3).map(([g, t]) => `${g} ${t}`).join(', ')}`)
if (order.find(([g]) => g === 'India')[1] !== ind.count) fails.push(`the India section counts ${order.find(([g]) => g === 'India')[1]}, not India's ${ind.count}`)
return { fails, order }
}
async function main() {
const selfTest = process.argv.includes('--self-test')
const CS = await import(join(ROOT, 'library/packet/countrySheet.js'))
const { seriesFor, sheetFormatFor } = CS
const data = JSON.parse(readFileSync(join(ROOT, 'library/data/country-posters.json'), 'utf8'))
const countries = data.countries || []
if (countries.length < 170) { console.log(`NOT MEASURED: country-posters.json parsed ${countries.length} countries (floor 170)`); process.exitCode = 75; return }
const H = await import(join(ROOT, STATE_HELPER))
const states = JSON.parse(readFileSync(join(ROOT, 'public/dx-maps/assets/people-groups/india-states.json'), 'utf8')).states || []
const adm1Ind = JSON.parse(readFileSync(join(ROOT, 'public/dx-maps/assets/boundaries/gb-IND-ADM1.geojson'), 'utf8'))
if (states.length < 30 || (adm1Ind.features || []).length < 30) { console.log(`NOT MEASURED: state table ${states.length} / ADM1 ${(adm1Ind.features || []).length} features (floor 30)`); process.exitCode = 75; return }
const areas = Object.fromEntries(NAME_LOCALES.map((l) => [l, JSON.parse(readFileSync(join(ROOT, `library/i18n/locales/${l}/countryPosters.json`), 'utf8'))]))
const meta = JSON.parse(readFileSync(join(ROOT, 'plugin-designs/country-posters/data/india-state-names.meta.json'), 'utf8'))
const files = {}
for (const p of listSources(PD)) files[relative(ROOT, p)] = readFileSync(p, 'utf8')
const REG = readFileSync(join(ROOT, 'plugin-designs/regions-dashboard/regions-list.vue'), 'utf8')
const MFE = readFileSync(join(ROOT, 'plugin-designs/country-posters/country-posters-mfe.vue'), 'utf8')
const TYPE = readFileSync(join(ROOT, COUNTRY_TYPE), 'utf8')
const regIsCaller = /makeCountrySheet\(/.test(REG)
const lists = (wallSrc, regSrc = REG) => (regIsCaller ? { wall: wallSrc, regions: regSrc } : { wall: wallSrc })
if (selfTest) {
const wall = KNOWN_CALLERS[0]
const cases = [
['direct generateCountrySheet call', () => armA({ ...files, [wall]: files[wall].replace('<script setup>', "<script setup>\nimport { generateCountrySheet } from '@map/packet/posterSheets.js'\ngenerateCountrySheet({})") })],
['makeCountrySheet without countries', () => armA({ ...files, [wall]: files[wall].replace(/countries: data\.countries \|\| \[\],/, '') })],
['India skipped (the old guard)', () => armB((l, it) => (!it || !it.sheet ? undefined : seriesFor(l, it)), countries)],
['absent code given a position', () => armB((l, it) => seriesFor(l, it) || { index: 1, totalLists: 1, totalGroups: 0 }, countries)],
['a second no-sheet country joins India', () => armB(seriesFor, countries.map((c) => (c.iso3 === 'CHN' ? { ...c, sheet: null } : c)))],
['the wall calls generateSheet directly', () => armA({ ...files, [wall]: files[wall].replace('<script setup>', "<script setup>\nimport { generateSheet } from '@map/packet/posterSheets.js'\ngenerateSheet({})") })],
['a state with groups loses its outline', () => armC(H, states.map((st) => (st.id === 'india-gj' ? { ...st, shapeName: 'Nowhere' } : st)), adm1Ind)],
['a division silently dropped', () => armC(H, states.filter((st) => st.id !== 'india-mz'), adm1Ind)],
['the wall stops passing sectionTotal (India counted twice)', () => armE(countries, states, files[wall].replace(':section-total=', ':x-section-total='), readFileSync(join(ROOT, 'library/components/poster/PosterWall.vue'), 'utf8'), H).fails],
['India country card left in Asia', () => armE(countries, states, files[wall].replace("=== 'IND'", "=== 'XXX'"), readFileSync(join(ROOT, 'library/components/poster/PosterWall.vue'), 'utf8'), H).fails],
['India offered 66x66 again', () => armB(seriesFor, countries, (it, f) => (it?.iso3 === 'IND' && f === '66x66' ? '66x66' : sheetFormatFor(it, f)))],
['a packet page becomes a large sheet', () => armB(seriesFor, countries, (it, f) => (it?.iso3 === 'IND' ? '60x96' : f))],
['the wall resolves [] for a phone-blocked card', () => armF({ wall: files[wall].replace("const desktopOnly = () => Promise.reject(keyedError('poster.phoneLimitAll'", "const desktopOnly = () => Promise.resolve([]) || (keyedError('poster.phoneLimitAll'"), regions: REG })],
// Only while the Regions list draws its own sheets (regIsCaller): a list that delegates to <dxm-poster-section> has
// no phone-block code of its own to poison, and lists() leaves it out of arm F, so this case would read as MISSED.
...(regIsCaller ? [['the Regions list resolves [] for a phone-blocked card', () => armF(lists(files[wall], REG.replace("phoneBlocked(item) ? Promise.reject(keyedError('poster.phoneLimitAll'", "phoneBlocked(item) ? Promise.resolve([]) || (keyedError('poster.phoneLimitAll'")))]] : []),
['a locale loses a state name', () => armD(states, { ...areas, ro: { ...areas.ro, stateNames: Object.fromEntries(Object.entries(areas.ro.stateNames).filter(([k]) => k !== 'IN-GJ')) } }, meta)],
['the names lose their licence line', () => armD(states, areas, { ...meta, licence: '' })],
['India\'s poster released by default', async () => armG({ ...CS, posterWithheld: () => false, largeSheet: () => CS.LARGE_SHEETS.IND }, lists(files[wall]), MFE)],
['an opener argument comes back', async () => armG({ ...CS, posterWithheld: (it, open) => CS.posterWithheld(it) }, lists(files[wall]), MFE)],
['the preview read without DEV (a global opens production)', async () => armG({ ...CS, previewUnreleased: () => globalThis.DXM_PREVIEW_UNRELEASED === true }, lists(files[wall]), MFE)],
['India\'s rows un-withheld', async () => armG({ ...CS, rowsWithheld: () => false }, lists(files[wall]), MFE)],
['the India tab shown without the preview flag', async () => armG(CS, lists(files[wall]), MFE.replace('...(import.meta.env && import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true && previewOptedIn() ? [INDIA_PANE] : []),', 'INDIA_PANE,'))],
['the India tab shown on the pilot by dev mode alone', async () => armG(CS, lists(files[wall]), MFE.replace(' && previewOptedIn() ? [INDIA_PANE]', ' ? [INDIA_PANE]'))],
['the wall prints the list of a country whose rows are withheld', async () => armG(CS, lists(files[wall].replace('rowsWithheld(c) ? withheld() : ', '')))],
['the library type prints withheld rows', () => armH(TYPE.replace('rowsWithheld(c) ? ', 'false ? '))],
['the library type shows a withheld poster', () => armH(TYPE.replace('posterWithheld(c) ||', 'false ||'))],
['a helper that mutates the shared ADM1', () => armC({ ...H, stateGeometry: (z, a, n) => { (a.features || []).forEach((f) => { if (f.properties.shapeName === n) f.properties.listId = z.id }); return H.stateGeometry(z, a, n) } }, states, JSON.parse(JSON.stringify(adm1Ind)))],
]
let bad = 0
if (!regIsCaller) console.log('n/a the Regions list resolves [] for a phone-blocked card (the list delegates to <dxm-poster-section>; its withholding is arm H, the library type)')
for (const [name, run] of cases) {
const f = await run()
console.log(`${f.length ? '✓ caught' : '✗ MISSED'} ${name}${f.length ? ` -> ${f[0]}` : ''}`)
if (!f.length) bad++
}
// The negative control: a comment naming the forbidden call must NOT fail.
const commented = armA({ ...files, [wall]: files[wall].replace('<script setup>', '<script setup>\n// generateCountrySheet(x) is not called here') })
console.log(`${commented.length ? '✗ FALSE RED' : '✓ ignored'} a comment naming generateCountrySheet`)
if (commented.length) bad++
process.exitCode = bad ? 1 : 0
return
}
const callers = []
const E = armE(countries, states, files['plugin-designs/country-posters/country-poster-wall.vue'], readFileSync(join(ROOT, 'library/components/poster/PosterWall.vue'), 'utf8'), H)
const G = [...await armG(CS, lists(files['plugin-designs/country-posters/country-poster-wall.vue']), MFE), ...armH(TYPE)]
const fails = [...armA(files, callers), ...armB(seriesFor, countries, sheetFormatFor), ...armC(H, states, adm1Ind), ...G, ...E.fails, ...armD(states, areas, meta), ...armF(lists(files['plugin-designs/country-posters/country-poster-wall.vue']))]
const missing = [...KNOWN_CALLERS, `${STATE_CALLER}#state`].filter((k) => !callers.includes(k))
if (missing.length) fails.push(`known callers not found calling makeCountrySheet: ${missing.join(', ')}`)
if (fails.length) { fails.forEach((f) => console.log(`FAIL ${f}`)); process.exitCode = 1; return }
const ind = seriesFor(countries, countries.find((c) => c.iso3 === 'IND'))
console.log(`PASS check:country-sheet-one-path — ${Object.keys(files).length} plugin sources parsed-or-skipped, makeCountrySheet callers: ${callers.join(', ')}; seriesFor over ${countries.length} countries (India ${ind.index} of ${ind.totalLists}); ${H.statesWithPosters(states).length} state posters, series 1..${H.statesWithPosters(states).length}, every outline found; sections ${E.order.slice(0, 3).map(([g, t]) => `${g} ${t}`).join(' > ')}; state names in ${NAME_LOCALES.length} locales x ${states.length}`)
}
main().catch((e) => { console.error(`FAIL ${e.stack || e}`); process.exitCode = 1 })