Gate the country-name join, and record that the wiring was already done
Two cards, BUILD-LOCALIZED-COUNTRY-NAMES-ISO3-TO-ISO2 and
BUILD-ISO3-TABLE-COVERS-ALL-242-COUNTRIES. I measured before building and most
of both already exists: countryName(iso3, lang) is in library/utils, and the
bridge is already 242 entries with an exact correspondence to countries.json —
0 codes missing, 0 orphans, verified in both directions rather than by trusting
the table's own _meta. All 242 resolve in en, ar and zh today.
So what was actually missing is the thing that keeps it true. Writing the table
and keeping it correct are separate acts, and only the first one looks like work
— which is the same reason this repo already has a gate for orphaned gates.
check:country-names asserts both directions of drift (a data code with no
alpha-2, an alpha-2 the data never uses) and that every code resolves to a real
NAME rather than to its own input, which is what Intl returns for a well-formed
code it does not know.
★ IT CARRIES TWO ARMS THAT MUST FAIL. A coverage gate that only counts hits
passes identically on 242 correct entries and on a table where every lookup
silently returns its input, so the last block feeds it an unassigned code and a
malformed one and FAILS if either is reported as resolved. Mutation-tested in
both directions before committing: deleting TCD from the bridge fails with
"1 code(s) with no alpha-2"; pointing TCD at a bogus QQ fails with three
unresolved lookups, one per locale. Table restored byte-identical after.
ar and zh are not decoration: ar is our only RTL locale, and zh uses a script
where an untranslated Latin code is unmistakable on sight.
NOT WIRED INTO THE REGIONS TABLES AND POPUPS, deliberately, against the brief.
All four regions fetches already send &lang=, so the API returns country labels
in the reader's language and summarizeRegions names each country from that
label — those surfaces are already localized. Swapping countryName() in would
replace a correct localized name with a differently-spelled one: Intl's English
differs from our canonical names for 42 of 242 countries ("Cabo Verde" ->
"Cape Verde", "Bonaire, Sint Eustatius, and Saba" -> "Caribbean Netherlands").
I re-measured that 42 rather than trusting the header that claims it. Changing
which name is canonical is a product decision and it would break clone parity
against live doxa.life, so it is not mine to make inside a localization card.
public/README.md's bundle count moves 24 -> 25: generated by bun run tree when
the country-map bundle was added on the parent commit, not hand-edited.
This commit is contained in:
parent
c99fe68c38
commit
1d3a929d51
3 changed files with 74 additions and 2 deletions
70
internal/check-country-names.mjs
Normal file
70
internal/check-country-names.mjs
Normal file
|
|
@ -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.`)
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
"check:composite-srcs": "node internal/check-composite-srcs.mjs",
|
"check:composite-srcs": "node internal/check-composite-srcs.mjs",
|
||||||
"check:composite-token-url": "node internal/check-composite-token-url.mjs",
|
"check:composite-token-url": "node internal/check-composite-token-url.mjs",
|
||||||
"check:composites-mount": "node internal/check-composites-mount.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": "node internal/check-gospel-coverage-consistent.mjs",
|
||||||
"check:coverage-consistent:poison": "node internal/check-gospel-coverage-consistent.mjs --poison",
|
"check:coverage-consistent:poison": "node internal/check-gospel-coverage-consistent.mjs --poison",
|
||||||
"check:csv-shape": "node internal/check-csv-export-shape.mjs",
|
"check:csv-shape": "node internal/check-csv-export-shape.mjs",
|
||||||
|
|
@ -301,7 +302,7 @@
|
||||||
"dev": "bun x vite",
|
"dev": "bun x vite",
|
||||||
"fix:sri": "node internal/fix-sri.mjs",
|
"fix:sri": "node internal/fix-sri.mjs",
|
||||||
"gallery": "node internal/generate-gallery.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",
|
"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",
|
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.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-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: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: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: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: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 <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser (rega's alone).",
|
"check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser (rega's alone).",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
This folder is GENERATED by `bun run build`; do not edit it by hand. It holds exactly one thing:
|
This folder is GENERATED by `bun run build`; do not edit it by hand. It holds exactly one thing:
|
||||||
|
|
||||||
- **`dx-maps/`** — the output folder: 24 plugin bundles and 36 pages, self-contained.
|
- **`dx-maps/`** — the output folder: 25 plugin bundles and 37 pages, self-contained.
|
||||||
Copy this one folder to any web host (or open it from disk) and it works.
|
Copy this one folder to any web host (or open it from disk) and it works.
|
||||||
- `dx-maps/index.html` — the front page: the Plugin Studio, every plugin in one place.
|
- `dx-maps/index.html` — the front page: the Plugin Studio, every plugin in one place.
|
||||||
- `dx-maps/<plugin>/<plugin>.js` — a plugin's bundle (a custom element; load it with a `<script>` tag).
|
- `dx-maps/<plugin>/<plugin>.js` — a plugin's bundle (a custom element; load it with a `<script>` tag).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue