diff --git a/documentation/reference-library-index.md b/documentation/reference-library-index.md index 934addb27..6e7b27ff2 100644 --- a/documentation/reference-library-index.md +++ b/documentation/reference-library-index.md @@ -270,6 +270,8 @@ and a change here can break other maps. `import … from '@map/utils/languageRequest.js'` - **loadPinnedUmd** — loadPinnedUmd — load a PINNED, integrity-checked UMD library and capture it WITHOUT touching the host `import … from '@map/utils/loadPinnedUmd.js'` +- **peopleGroupName** — peopleGroupName.js — a people group's name in the reader's language, or English if it was machine-translated. + `import … from '@map/utils/peopleGroupName.js'` - **peopleGroupPoints** — peopleGroupPoints.js — SHARED (@map/utils/peopleGroupPoints.js). `import … from '@map/utils/peopleGroupPoints.js'` - **scrollport** — findScrollportAncestor — the nearest ancestor that actually scrolls. diff --git a/internal/check-gospel-tables-staged.mjs b/internal/check-gospel-tables-staged.mjs index d5625685d..146b488b2 100644 --- a/internal/check-gospel-tables-staged.mjs +++ b/internal/check-gospel-tables-staged.mjs @@ -64,6 +64,44 @@ for (const prefix of dirLike) { for (const file of named.filter((f) => f.endsWith(ext))) wanted.push(prefix + file) } +/** + * ⚠ AN INTERPOLATED FILE NAME IS NOT A FILE EITHER. The field card fetches + * `${assetsBase.value}/tables/card-${cardShard(cardSlug)}.json`, so the parse above captures "tables/card-" and + * stops at the interpolation; read as a file it failed as "not staged" while all its shards were there. Its + * expected set comes from the SOURCE, as the rule above says, never from the tree: every roster row the card + * builder keeps (data/tables/doxa-people-groups.json, the same filter as build-field-cards.mjs) hashed with the + * leaf's own cardShard. This file carries a copy of that function and fails if the leaf's copy changes, so the + * two cannot drift apart; any other interpolated name is reported as unresolved, never passed. + */ +const SHARD_FN = 'let h = 5381 for (let i = 0; i < slug.length; i++) h = (((h << 5) + h) ^ slug.charCodeAt(i)) & 0xffffffff return ((h >>> 0) & 0xff).toString(16).padStart(2, \'0\')' +const cardShard = (slug) => { + let h = 5381 + for (let i = 0; i < slug.length; i++) h = (((h << 5) + h) ^ slug.charCodeAt(i)) & 0xffffffff + return ((h >>> 0) & 0xff).toString(16).padStart(2, '0') +} +function checkInterpolatedName(rel, missing) { + if (rel !== 'tables/card-') { missing.push(`${rel} — an interpolated file name this check cannot resolve`); return } + const leafFn = (src.match(/const cardShard = \(slug\) => \{([\s\S]*?)\n\}/) || [])[1] + if (!leafFn || leafFn.replace(/\s+/g, ' ').trim() !== SHARD_FN) { + missing.push('tables/card-*.json — the leaf\'s cardShard changed; update this check\'s copy (and build-field-cards.mjs) to match') + return + } + let rows = [] + try { rows = JSON.parse(readFileSync(resolve(ROOT, 'data/tables/doxa-people-groups.json'), 'utf8')).rows || [] } + catch (e) { missing.push(`tables/card-*.json — could not read the roster: ${e.message}`); return } + const keys = [...new Set(rows.filter((r) => r && r.slug && r.mapped && r.lat != null && r.lng != null).map((r) => cardShard(r.slug)))].sort() + if (keys.length < 20) { missing.push(`tables/card-*.json — the roster gave only ${keys.length} shard key(s); the roster changed shape`); return } + let ok = 0, bytes = 0 + for (const k of keys) { + const p = resolve(POOL, `tables/card-${k}.json`) + if (!existsSync(p)) { missing.push(`tables/card-${k}.json — not staged`); continue } + const b = statSync(p).size + if (b < 200) { missing.push(`tables/card-${k}.json — staged but only ${b} bytes`); continue } + ok++; bytes += b + } + console.log(` ${ok === keys.length ? 'ok ' : 'FAIL'} ${'tables/card-*.json'.padEnd(42)} ${ok}/${keys.length} shard(s) the roster needs, ${bytes} bytes`) +} + const missing = [] for (const rel of wanted) { if (rel.endsWith('/')) { @@ -73,6 +111,7 @@ for (const rel of wanted) { if (!found.length) missing.push(`${rel} — a dynamic fetch this check could not resolve to a file`) continue } + if (!/\.(?:geojson|json)$/.test(rel)) { checkInterpolatedName(rel, missing); continue } const p = resolve(POOL, rel) if (!existsSync(p)) { missing.push(`${rel} — not staged`); continue } const bytes = statSync(p).size diff --git a/internal/check-pg-name-gate.mjs b/internal/check-pg-name-gate.mjs new file mode 100644 index 000000000..671183b29 --- /dev/null +++ b/internal/check-pg-name-gate.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node +// check-pg-name-gate — a people group's localized name is shown only when it is a transliteration, never a word +// that a machine translated (Balantak → "Balance", Balo → "Sac à dos"). The rule lives in ONE place, +// library/utils/peopleGroupName.js; this gate holds it there and proves it on the REAL table. Headless. Asserts: +// A. the table loads (data/pplr/pg-names.json, the file the tree serves as assets/pplr/pg-names.json); +// B. REAL ROWS: Balantak, Balo, Abaga and Abai Sungai (translated as words) show English in every locale; +// Balangao Bontoc (a transliteration) shows its own names; it and ro, which have no column, show English; +// C. TWO-SIDED on planted rows, AT the threshold: EXACTLY two far Latin locales falls back, one is kept (the rule +// is "two or more"); a row with only one Latin name to check is not trusted (no evidence → English); +// D. the whole table falls back between 30% and 45% (measured 38.2%, 8,172 of 21,419). A similarity that +// always says "same" (0%) or always "different" (100%) fails here even if B and C were tuned around; +// E. THE ONE-READER RULE: a file in library/ or plugin-designs/ that names pg-names must take its names through +// peopleGroupName.js, and must not index its rows itself (`.names[`) even when it imports the helper. +// pplr-data-maps is the carded exception (its coverage views read raw rows on purpose). NEGATIVE CONTROLS: a +// planted raw reader, and a planted file that imports the helper AND indexes rows, must both be caught. +import { readFileSync, readdirSync, statSync } from 'node:fs' +import { resolve, dirname, join, relative } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const P = (...a) => join(REPO, ...a) +const fails = [], notes = [] +const ok = (cond, msg, note) => { if (!cond) fails.push(msg); else if (note) notes.push(` ok ${note}`) } + +let H, table +try { + H = await import(pathToFileURL(P('library/utils/peopleGroupName.js')).href) + table = JSON.parse(readFileSync(P('data/pplr/pg-names.json'), 'utf8')) +} catch (e) { + console.error(`PG-NAME-GATE: NOT MEASURED (exit 75) — an input did not load: ${e.message}`) + process.exit(75) +} +const { locales, names } = table +const name = (code, lang) => H.peopleGroupName(names[code], locales, lang) + +// ── A ── +ok(Array.isArray(locales) && locales.includes('en') && Object.keys(names).length > 20000, + `the table has ${Object.keys(names || {}).length} rows and locales ${JSON.stringify(locales)}`, + `the table loads: ${Object.keys(names).length} groups × ${locales.length} locales`) + +// ── B: real rows ── +const TRANSLATED = { 220002: 'Balantak', 220017: 'Balo', 220044: 'Abaga', 220045: 'Abai Sungai' } +const wrong = [] +for (const [code, en] of Object.entries(TRANSLATED)) { + if (!names[code] || names[code][locales.indexOf('en')] !== en) { wrong.push(`${code} is not ${en} in this table`); continue } + for (const l of ['fr', 'es', 'de', 'ar', 'hi', 'ru', 'zh']) if (name(code, l) !== en) wrong.push(`${en} in ${l} shows "${name(code, l)}"`) +} +ok(!wrong.length, `translated rows leak: ${wrong.slice(0, 5).join('; ')}`, + `the 4 known translated rows (${Object.values(TRANSLATED).join(', ')}) show English in fr es de ar hi ru zh`) +const bb = names[220001], at = (l) => bb[locales.indexOf(l)] +ok(bb && name(220001, 'ar') === at('ar') && /[\u0600-\u06FF]/.test(name(220001, 'ar')) && name(220001, 'zh') === at('zh_CN') && name(220001, 'hi') === at('hi'), + `Balangao Bontoc lost its own names: ar "${bb && name(220001, 'ar')}", zh "${bb && name(220001, 'zh')}"`, + `Balangao Bontoc keeps its own names (ar "${name(220001, 'ar')}", zh "${name(220001, 'zh')}")`) +ok(name(220001, 'it') === 'Balangao Bontoc' && name(220001, 'ro') === 'Balangao Bontoc' && H.peopleGroupName(bb, locales, 'ar', 'Bontoc, Balangao') === at('ar') + && H.peopleGroupName(names[220002], locales, 'fr', 'Balantak (Sulawesi)') === 'Balantak (Sulawesi)', + 'it/ro or the caller\'s English name are not honoured', 'it and ro show English; a fallback shows the CALLER\'s English name') + +// ── C: planted rows, two-sided ── +const planted = (fn) => { const r = bb.slice(); fn(r); return r } +const twoFar = planted((r) => { r[locales.indexOf('fr')] = 'Sac à dos'; r[locales.indexOf('de')] = 'Rucksack' }) +const oneFar = planted((r) => { r[locales.indexOf('fr')] = 'Sac à dos' }) +const oneLatin = planted((r) => { for (const l of H.LATIN_LOCALES) if (l !== 'fr') r[locales.indexOf(l)] = '' }) +const kept = (r) => (H.isTransliterated(r, locales) ? 'kept' : 'English') +ok(kept(twoFar) === 'English' && H.peopleGroupName(twoFar, locales, 'ar') === 'Balangao Bontoc' && kept(oneFar) === 'kept' + && kept(oneLatin) === 'English' && H.peopleGroupName(oneLatin, locales, 'ar', 'Balangao Bontoc') === 'Balangao Bontoc', + `planted rows: exactly two far → ${kept(twoFar)}, one far → ${kept(oneFar)}, one Latin name only → ${kept(oneLatin)}`, + 'planted, at the threshold: exactly two far Latin locales → English (ar too); one far → kept; only one Latin name to check → English') + +// ── D: the whole table ── +let back = 0 +for (const row of Object.values(names)) if (!H.isTransliterated(row, locales)) back++ +const share = back / Object.keys(names).length +ok(share > 0.30 && share < 0.45, `${back} rows (${(share * 100).toFixed(1)}%) fall back; expected 30-45% (measured 38.2%)`, + `${back} of ${Object.keys(names).length} rows (${(share * 100).toFixed(1)}%) fall back to English`) + +// ── E: one reader ── +const EXEMPT = ['plugin-designs/pplr-data-maps/'] +const rawReaders = (files) => files.filter(({ path, src }) => /pg-names/.test(src) && !EXEMPT.some((x) => path.startsWith(x)) + && (!/peopleGroupName(\.js)?['"]/.test(src) || /\.names\s*\[/.test(src))) +const walk = (dir, out = []) => { + for (const f of readdirSync(dir)) { + if (f === 'node_modules' || f.startsWith('.')) continue + const p = join(dir, f), s = statSync(p) + if (s.isDirectory()) walk(p, out) + else if (/\.(m?js|vue)$/.test(f)) out.push({ path: relative(REPO, p).split('\\').join('/'), src: readFileSync(p, 'utf8') }) + } + return out +} +const sources = [...walk(P('library')), ...walk(P('plugin-designs'))].filter((f) => f.path !== 'library/utils/peopleGroupName.js') +const PLANT_RAW = "fetch(base + '/pplr/pg-names.json').then((t) => t.names[code][3])" +const PLANT_BOTH = "import { peopleGroupName } from '@map/utils/peopleGroupName.js'\n" + PLANT_RAW +const PLANT_GOOD = "import { peopleGroupName } from '@map/utils/peopleGroupName.js'\nfetch(base + '/pplr/pg-names.json').then((t) => peopleGroupName(t.names && row, t.locales, lang, name))" +ok(rawReaders([{ path: 'plugin-designs/x/a.vue', src: PLANT_RAW }, { path: 'plugin-designs/x/b.vue', src: PLANT_BOTH }, { path: 'plugin-designs/x/c.vue', src: PLANT_GOOD }]).map((f) => f.path).join() === 'plugin-designs/x/a.vue,plugin-designs/x/b.vue', + 'NEGATIVE CONTROL: the one-reader scan missed a planted raw reader (or flagged a helper-only one)', + 'negative controls: a raw reader, and one that imports the helper AND indexes rows, are both caught; a helper-only reader passes') +const raw = rawReaders(sources).map((f) => f.path) +ok(!raw.length, `pg-names read outside peopleGroupName.js: ${raw.join(', ')}`, + `no file outside peopleGroupName.js reads pg-names rows (${sources.length} sources; pplr-data-maps carded)`) + +for (const n of notes) console.log(n) +if (fails.length) { + console.error('PG-NAME-GATE: FAIL') + for (const f of fails) console.error(` ✗ ${f}`) + process.exit(1) +} +console.log('PG-NAME-GATE: PASS — a localized people-group name is shown only when it is a transliteration, decided in one place.') diff --git a/library/utils/peopleGroupName.js b/library/utils/peopleGroupName.js new file mode 100644 index 000000000..18493e0dc --- /dev/null +++ b/library/utils/peopleGroupName.js @@ -0,0 +1,83 @@ +/** + * peopleGroupName.js — a people group's name in the reader's language, or English if it was machine-translated. + * + * The source is assets/pplr/pg-names.json: { locales: [17 codes], names: { : [17 names in that order] } }, + * 21,419 groups. About a third of its rows were MACHINE-TRANSLATED AS WORDS instead of transliterated: + * 220002 Balantak fr "Balance" es "Balanza" de "Waage" (scales) + * 220017 Balo fr "Sac à dos" es "Mochila" de "Rucksack" (backpack) + * 220045 Abai Sungai fr "Ignorer la rivière" de "Ignoriere den Fluss" + * A proper name rendered as a dictionary word is worse than English: the reader cannot tell it is wrong. + * + * THE GATE, per row. A real transliteration of a proper name stays close to the English spelling in every + * Latin-script locale; a translated word does not. So each Latin locale's name is compared with the English one + * (bigram Dice similarity, accents folded). When TWO OR MORE of them fall below 0.5, the row is a translation, and + * EVERY locale of that group shows English, because the non-Latin names (ar, hi, ru, zh) came from the same + * process and cannot be checked directly. + * Measured on the whole table: 8,055 of 21,419 rows (37.6%) are translations, and 117 more have fewer than two + * Latin names to check, so they are not trusted either: 8,172 (38.2%) show English. Balantak, Balo, Abaga and Abai + * Sungai fall back; Balangao Bontoc keeps its names. The 0.4-0.5 band (1,406 rows) is about half wrong (Bawaria → + * "Bavière", Ager → "Agriculteur"), so 0.5 is the safer side: a wrongly kept translation is the worse error. + * + * ★ THE ONE-READER RULE. This file is the only code that interprets a pg-names row. A caller fetches the table + * (it is 7.9 MB, served, never bundled) and passes the row in; check:pg-name-gate fails on any other reader, with + * pplr-data-maps as the carded exception (its coverage views read the raw rows on purpose). + * + * PURE: no fetch, no JSON import, no i18n framework. It takes the locale string the caller already has. + */ + +// The Latin-script locales of pg-names.json: the ones whose spelling can be compared with English. +export const LATIN_LOCALES = ['fr', 'pt', 'es', 'tl', 'id', 'de'] +export const MIN_SIMILARITY = 0.5 +export const MAX_FAR_LOCALES = 1 // two or more far locales → the row is a translation +export const MIN_LATIN_EVIDENCE = 2 // fewer Latin names than this → no evidence either way → English + +// The app's locales → pg-names.json's. it and ro have no column, so they show English. +const LOCALE_COLUMN = { en: 'en', es: 'es', fr: 'fr', pt: 'pt', de: 'de', ru: 'ru', ar: 'ar', hi: 'hi', zh: 'zh_CN' } + +const fold = (s) => String(s || '').normalize('NFKD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z]/g, '') + +/** Bigram Dice similarity of two names, 0..1, accents and punctuation ignored. */ +export function nameSimilarity(a, b) { + const x = fold(a), y = fold(b) + if (!x || !y) return 0 + if (x === y) return 1 + if (x.length < 2 || y.length < 2) return 0 + const grams = (s) => { const m = new Map(); for (let i = 0; i < s.length - 1; i++) { const g = s.slice(i, i + 2); m.set(g, (m.get(g) || 0) + 1) } return m } + const gx = grams(x), gy = grams(y) + let shared = 0 + for (const [g, n] of gx) shared += Math.min(n, gy.get(g) || 0) + return (2 * shared) / (x.length - 1 + y.length - 1) +} + +/** + * True when the row's names are transliterations (keep them), false when they were translated as words. A row with + * fewer than MIN_LATIN_EVIDENCE Latin names cannot be checked (117 rows), so it is NOT trusted: English. + */ +export function isTransliterated(row, locales) { + if (!Array.isArray(row) || !Array.isArray(locales)) return false + const en = row[locales.indexOf('en')] + if (!en) return false + let far = 0, seen = 0 + for (const l of LATIN_LOCALES) { + const v = row[locales.indexOf(l)] + if (!v) continue + seen++ + if (nameSimilarity(v, en) < MIN_SIMILARITY) far++ + } + return seen >= MIN_LATIN_EVIDENCE && far <= MAX_FAR_LOCALES +} + +/** + * The name to show for a group: its `lang` name when the row is a transliteration and has that column, else + * `english`. ★ CALLERS ALWAYS PASS `english` (the record's own name): it is what a fallback shows, and a row the + * table does not have (row undefined) has no English column to fall back to, so without it the result is ''. + */ +export function peopleGroupName(row, locales, lang, english) { + const en = english || (Array.isArray(row) && Array.isArray(locales) ? row[locales.indexOf('en')] : '') || '' + const col = LOCALE_COLUMN[String(lang || 'en').toLowerCase().split(/[-_]/)[0]] + if (!col || col === 'en' || !isTransliterated(row, locales)) return en + const v = row[locales.indexOf(col)] + return (typeof v === 'string' && v.trim()) ? v : en +} + +export default { peopleGroupName, isTransliterated, nameSimilarity, LATIN_LOCALES, MIN_SIMILARITY, MAX_FAR_LOCALES, MIN_LATIN_EVIDENCE } diff --git a/package.json b/package.json index 60db4ecc6..fd8b4a2f3 100644 --- a/package.json +++ b/package.json @@ -240,6 +240,7 @@ "check:ported-globals-identical": "node internal/check-ported-globals-identical.mjs", "check:one-scroll-container": "node internal/check-one-scroll-container.mjs", "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:poster-action-print": "node internal/check-poster-action-print.mjs", "check:popup-href-escaped": "node internal/check-popup-href-escaped.mjs", @@ -350,7 +351,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: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", + "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: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", "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", @@ -585,6 +586,7 @@ "check:pinned-umd": "library/utils/loadPinnedUmd.js (d3 for PPLR and the Wall) loads a pinned UMD with SRI and never touches the host page's global of the same name: a host's own d3 gains no keys, a failed load leaves it in place. Headless.", "check:popup-escaping": "the library's pin and region popups render hostile data (an , a