doxa-maps/internal/check-upg100-poster-type.mjs
Doxa Maps 07b28e405e UPG-100: region sections are the shared poster section, collapsible, and an 'upg100-list' poster type
UPG-100's poster wall groups its zones into regions (India, China + Mongolia,
Africa, Middle East, Central Asia, Southeast Asia + Oceania, Americas, Europe).
Those sections are now collapsible like Country Posters' and start open. They
are also available on their own as <dxm-poster-section type="upg100-list"
scope="africa">.

- data/upg100-regions.js (pure): the region table (REGION_OF, moved verbatim
  from poster-wall.vue), regionOf(id) and each region's scope slug.
- data/upg100-poster-type.js: the poster-type adapter (scopes, items, title,
  actions), built on the wall's own zones (upg100Zones), headings (regionTitle)
  and print path (the records provider with this plugin's evaluator, and
  zone-sheet-print.js).
- index.js registers the type ('upg100-list') and the element. The element
  registration is a no-op if another bundle on the page registered it first.
- poster-wall.vue groups through the same regionOf, draws the same zones, and
  passes collapsible + collapsedBy (all open). Its other props are unchanged.

check:upg100-poster-type (gates:pre) checks:
- every zone sits in a named region, with a negative control;
- the 8 sections exist, and the scopes are unique and round-trip;
- the headings exist ×11;
- the wiring holds.
Three mutants each fail it.
No library code changed.
2026-10-02 07:58:49 +00:00

84 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
// check-upg100-poster-type — UPG-100's region sections are ONE section, whether the poster wall draws them or
// <dxm-poster-section type="upg100-list" scope="…"> does. Headless. Asserts:
// A. every zone the wall shows (clusters.json: lists + asia_lists + uncovered_lists) belongs to a NAMED region,
// never 'Other', and the sections the wall promises exist (India, China + Mongolia, Africa, Middle East,
// Central Asia, Southeast Asia + Oceania, Americas, Europe). NEGATIVE CONTROL: a planted unknown zone falls to
// 'Other', so this check can fail;
// B. each region's scope (its slug) is unique and round-trips from the English name, so a scope can't name two
// sections;
// C. each region's heading has its poster.region_* key in all 11 locales;
// D. the wiring: the bundle registers the 'upg100-list' type and the element; the type's adapter has the four
// parts a section reads; the wall groups through the SAME regionOf and draws the SAME zones (upg100Zones), and
// its sections are collapsible.
import { readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const P = (...a) => join(REPO, ...a)
const PLUGIN = 'plugin-designs/my-upg-100-list'
const fails = [], notes = []
const ok = (cond, msg, note) => { if (!cond) fails.push(msg); else if (note) notes.push(` ok ${note}`) }
const LANGS = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ro', 'ru', 'ar', 'hi', 'zh']
let R, clusters, REGION_KEY
try {
R = await import(pathToFileURL(P(PLUGIN, 'data/upg100-regions.js')).href)
clusters = JSON.parse(readFileSync(P(PLUGIN, 'data/clusters.json'), 'utf8'))
// REGION_KEY is a plain table in legend-tree.js; read it from the source rather than importing the module (it
// pulls JSON the node loader cannot take without import attributes).
const src = readFileSync(P(PLUGIN, 'data/legend-tree.js'), 'utf8')
REGION_KEY = Object.fromEntries([...src.slice(src.indexOf('export const REGION_KEY'), src.indexOf('};', src.indexOf('export const REGION_KEY'))).matchAll(/'([^']+)': '([^']+)'/g)].map((m) => [m[1], m[2]]))
} catch (e) {
console.error(`UPG100-POSTER-TYPE: NOT MEASURED (exit 75) — an input did not load: ${e.message}`)
process.exit(75)
}
// ── A ──
const ids = [...(clusters.lists || []), ...(clusters.asia_lists || []), ...(clusters.uncovered_lists || [])].map((l) => l.id).filter(Boolean)
const others = ids.filter((id) => R.regionOf(id) === 'Other')
const used = new Set(ids.map((id) => R.regionOf(id)))
const PROMISED = ['India', 'China + Mongolia', 'Africa', 'Middle East', 'Central Asia', 'Southeast Asia + Oceania', 'Americas', 'Europe']
ok(R.regionOf('a-zone-nobody-placed') === 'Other', 'NEGATIVE CONTROL: an unknown zone did not fall to Other, so this check cannot see a missing region',
'negative control: an unplaced zone falls to Other')
ok(ids.length > 20 && !others.length && PROMISED.every((r) => used.has(r)),
`zones without a region: ${others.join(', ') || '(none)'}; sections missing: ${PROMISED.filter((r) => !used.has(r)).join(', ') || '(none)'}`,
`${ids.length} zones, every one in a named region; all ${PROMISED.length} sections present`)
// ── B ──
const scopes = R.REGIONS.map(R.regionScope)
ok(new Set(scopes).size === scopes.length && R.REGIONS.every((r) => R.regionScope(R.regionScope(r)) === R.regionScope(r)) && R.regionScope('Southeast Asia + Oceania') === 'southeast-asia-oceania',
`scopes are not unique or do not round-trip: ${scopes.join(', ')}`, `scopes unique and stable: ${scopes.join(', ')}`)
// ── C ──
const missingKeys = []
for (const r of R.REGIONS) {
const key = REGION_KEY[r]
if (!key) { missingKeys.push(`${r}: no REGION_KEY`); continue }
for (const l of LANGS) { const cat = JSON.parse(readFileSync(P('library/i18n/locales', l, 'poster.json'), 'utf8')); if (typeof cat[key] !== 'string' || !cat[key].trim()) missingKeys.push(`${l} poster.${key}`) }
}
ok(!missingKeys.length, `region headings missing: ${missingKeys.slice(0, 6).join(', ')}`, `every region heading exists in all ${LANGS.length} locales`)
// ── D ──
const index = readFileSync(P(PLUGIN, 'index.js'), 'utf8')
const type = readFileSync(P(PLUGIN, 'data/upg100-poster-type.js'), 'utf8')
const wall = readFileSync(P(PLUGIN, 'poster-wall.vue'), 'utf8')
const wired = {
'the bundle registers the upg100-list type': /registerPosterType\('upg100-list', \(\) => import\('\.\/data\/upg100-poster-type\.js'\)\)/.test(index),
'the bundle registers the element': /\nregisterPosterSectionElement\(\)/.test(index),
'the adapter has scopes, items, title and actions': ['scopes:', 'items(scope, locale)', 'title(scope, t)', 'actions({'].every((s) => type.includes(s)),
'the wall groups through the same regionOf': /const regionFor = \(id\) => regionTitle\(regionOf\(id\), t\)/.test(wall) && !/const REGION_OF = /.test(wall),
'the wall draws the same zones': /return upg100Zones\(profileConfig, locale\)/.test(wall),
'the wall sections are collapsible': /:collapsible="true"/.test(wall) && /:collapsed-by="allOpen"/.test(wall),
}
const missingWires = Object.entries(wired).filter(([, v]) => !v).map(([k]) => k)
ok(!missingWires.length, `missing: ${missingWires.join('; ')}`, 'the wall and <dxm-poster-section type="upg100-list"> share one region table, one zone list and the collapsible section')
for (const n of notes) console.log(n)
if (fails.length) {
console.error('UPG100-POSTER-TYPE: FAIL')
for (const f of fails) console.error(` ✗ ${f}`)
process.exit(1)
}
console.log('UPG100-POSTER-TYPE: PASS — UPG-100\'s region sections are one section, on the wall and as <dxm-poster-section type="upg100-list">.')