diff --git a/internal/check-upg100-poster-type.mjs b/internal/check-upg100-poster-type.mjs new file mode 100644 index 000000000..a1ef7b432 --- /dev/null +++ b/internal/check-upg100-poster-type.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +// check-upg100-poster-type — UPG-100's region sections are ONE section, whether the poster wall draws them or +// 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 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 .') diff --git a/package.json b/package.json index 35005a00e..6eaf375ec 100644 --- a/package.json +++ b/package.json @@ -242,6 +242,7 @@ "check:list-pattern-doc": "node internal/check-list-pattern-doc.mjs", "check:pg-name-gate": "node internal/check-pg-name-gate.mjs", "check:pin-selection": "node internal/check-pin-selection.mjs", + "check:upg100-poster-type": "node internal/check-upg100-poster-type.mjs", "check:poster-action-print": "node internal/check-poster-action-print.mjs", "check:popup-href-escaped": "node internal/check-popup-href-escaped.mjs", "check:popup-pan-controls": "node internal/check-popup-pan-clears-controls.mjs", @@ -351,7 +352,7 @@ "fix:sri": "node internal/fix-sri.mjs", "gallery": "node internal/generate-gallery.mjs", "gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test", - "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate", + "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:upg100-poster-type", "gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs", "internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs", "library-index": "node internal/generate-library-index.mjs", @@ -589,6 +590,7 @@ "check:popup-escaping": "the library's pin and region popups render hostile data (an , a