diff --git a/internal/check-country-names.mjs b/internal/check-country-names.mjs new file mode 100644 index 000000000..6961a0694 --- /dev/null +++ b/internal/check-country-names.mjs @@ -0,0 +1,70 @@ +/** + * check-country-names.mjs — every ISO3 our data uses resolves to a localized country name. + * + * Guards the join, not the library. Intl.DisplayNames is the platform's; what can rot here is + * OURS: library/data/iso3-to-alpha2.json (the alpha-3 -> alpha-2 bridge) drifting from + * library/data/countries.json (the codes the data actually uses). A country added to one and not + * the other renders as a bare code — a silent defect, because a code still looks like a value. + * + * ★ WHY IT ASSERTS THE ARMS THAT MUST FAIL TOO. A coverage gate that only counts hits passes just + * as happily on a table of 242 correct entries as on one where every lookup quietly returns its + * input. So the last block feeds it a code that does not exist and a code that is structurally + * invalid, and FAILS if either is reported as resolved. Without that, this file proves nothing + * about the 242 above it. + */ +import { readFileSync } from 'node:fs' + +const read = (p) => JSON.parse(readFileSync(new URL(p, import.meta.url), 'utf8')) +const table = read('../library/data/iso3-to-alpha2.json').map +const countries = read('../library/data/countries.json') + +// ar and zh are the two the brief names, and they are the two worth naming: ar is our only RTL +// locale and zh uses a script where an untranslated Latin code is unmistakable on sight. +const LOCALES = ['en', 'ar', 'zh'] +const fail = [] + +// ── 1. the bridge covers every code the data uses, in both directions ────────────────────────── +const dataCodes = countries.map((c) => c.value) +const missing = dataCodes.filter((c) => !table[c]) +const orphans = Object.keys(table).filter((c) => !dataCodes.includes(c)) +if (missing.length) fail.push(`${missing.length} code(s) in countries.json with no alpha-2: ${missing.slice(0, 8).join(', ')}`) +if (orphans.length) fail.push(`${orphans.length} code(s) in the table that the data never uses: ${orphans.slice(0, 8).join(', ')}`) + +// ── 2. every code resolves to a real NAME, in every locale we check ──────────────────────────── +const dn = {} +for (const l of LOCALES) { + try { dn[l] = new Intl.DisplayNames([l], { type: 'region' }) } catch { fail.push(`Intl.DisplayNames unavailable for '${l}'`) } +} +let checked = 0 +const unresolved = [] +for (const iso3 of dataCodes) { + const a2 = table[iso3] + if (!a2) continue + for (const l of LOCALES) { + if (!dn[l]) continue + let name = null + try { name = dn[l].of(a2) } catch { name = null } + // An unknown-but-well-formed region comes back as the INPUT. That is not a name. + if (!name || name === a2) unresolved.push(`${iso3}->${a2} @${l}`) + checked++ + } +} +if (unresolved.length) fail.push(`${unresolved.length} unresolved: ${unresolved.slice(0, 8).join(', ')}`) + +// ── 3. THE ARMS THAT MUST FAIL — without these, everything above is unfalsifiable ────────────── +const mustNotResolve = [ + ['ZZZ', 'a code that is well-formed but assigned to nothing'], + ['QQ', 'a two-letter code with no country behind it'], +] +for (const [code, why] of mustNotResolve) { + let name = null + try { name = dn.en ? dn.en.of(code) : null } catch { name = null } + if (name && name !== code) fail.push(`NEGATIVE ARM BROKEN: '${code}' (${why}) resolved to "${name}" — this gate cannot distinguish a hit from a miss`) +} + +if (fail.length) { + console.error('COUNTRY-NAMES: FAIL') + for (const f of fail) console.error(` ✗ ${f}`) + process.exit(1) +} +console.log(`COUNTRY-NAMES: PASS — ${dataCodes.length} code(s) resolve in ${LOCALES.join('/')} (${checked} lookups), and both negative arms still fail.`) diff --git a/package.json b/package.json index 39bccc42e..af9687b73 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "check:composite-srcs": "node internal/check-composite-srcs.mjs", "check:composite-token-url": "node internal/check-composite-token-url.mjs", "check:composites-mount": "node internal/check-composites-mount.mjs", + "check:country-names": "node internal/check-country-names.mjs", "check:coverage-consistent": "node internal/check-gospel-coverage-consistent.mjs", "check:coverage-consistent:poison": "node internal/check-gospel-coverage-consistent.mjs --poison", "check:csv-shape": "node internal/check-csv-export-shape.mjs", @@ -301,7 +302,7 @@ "dev": "bun x vite", "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:done-evidence check:script-flags check:backlog-status 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:backlog-refs 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: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: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:poster-print-fallback", + "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:done-evidence check:script-flags check:backlog-status 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:backlog-refs 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: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: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:poster-print-fallback check:country-names", "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:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist 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:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin", "gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs", "internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs", @@ -377,6 +378,7 @@ "check:composite-locale-leaks": "a composite rendered in a non-English locale must not show a string the catalogue already translates. The criterion is a LOOKUP, not a category: English catalogue holds the string as a value, this locale holds a different value for that key, the page still renders English. The previous sweep split strings into chrome/data/third-party and scored single words like 'Region' and 'Prayer' as data, so 28 leaks on Big Picture and 41 on Simple Research counted as zero. Surfaces that a lookup cannot judge - vendor map chrome, table DATA cells, facet-rail values - are suppressed by NAME with a stated reason, never by word.", "check:composite-token-url": "a composite embedded BARE (no outer ProfileLoader) must honour a tokenUrl-only config: it reads tokenUrl, forwards it, and asks the token service only when no token is given. The expectation is derived from ProfileLoader, not restated. Headless; in gates:pre.", "check:composites-mount": "every capability-4/5 composite's OWN setup() and mounted hooks run without throwing, in en and ar, headless (no browser): vite loads each composite as its bundle entry defines it (configureApp, and ProfileLoader's provides where the entry mounts ProfileLoader), and @vue/runtime-core mounts it on a no-op renderer. Exists because 08f89f55 shipped two composites calling getRuntimeLocale/detectLocale/RTL_LOCALES with no import: the build was green, setup threw a ReferenceError in every locale, and Big Picture and Simple Research rendered nothing. Proves setup runs; cannot prove the children render.", + "check:country-names": "every ISO 3166-1 alpha-3 our data uses must resolve to a localized country name. Guards the JOIN, not Intl: library/data/iso3-to-alpha2.json drifting from library/data/countries.json makes a country render as a bare code, which still looks like a value. Checks en/ar/zh (ar is our only RTL locale; zh makes an untranslated Latin code unmistakable) and asserts BOTH directions of drift. Carries two negative arms — an unassigned code and a malformed one — and FAILS if either resolves, because a coverage count cannot otherwise tell a hit from a lookup that returns its input.", "check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.", "check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.", "check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a