diff --git a/internal/check-upg-zone-joins.mjs b/internal/check-upg-zone-joins.mjs new file mode 100644 index 000000000..62329351f --- /dev/null +++ b/internal/check-upg-zone-joins.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +/** + * check-upg-zone-joins.mjs — the two UPG-100 zone joins, each pinned WITH ITS COST. + * + * WHY THIS IS A REGISTERED CHECK AND NOT A SCRIPT I RAN ONCE. Both joins below were + * verified by hand while they were written, and that verification lived nowhere: not + * in a file, not in a gate, only in a terminal. A probe proves something once. The two + * defects it guards are both SILENT — neither throws, neither renders an error, and + * both return a well-formed answer that is simply smaller than the truth: + * + * 1. ADMIN NAMES NEED THEIR DIACRITICS FOLDED. admin-to-list.json writes `gujarat`; + * gospel-by-province-groups writes `Gujarāt` (also Karnātaka, Tamil Nādu, + * Uttarākhand, Jammu and Kashmīr, Haryāna). Lowercase alone joins 514 of 1009 + * rows across 9 zones; folding joins 1009 across 12. The 50.9% is the dangerous + * number — every surviving zone still looks populated, just smaller. + * + * 2. THE SELECTOR EVALUATOR WANTS FLAT RECORDS. It reads `r.country_code === want`, + * a plain string, while the source shape is `{ value, label }` before normalize.js + * flattens it. Passing the object form matches nothing and returns an empty Set + * for EVERY zone, which reads as "these zones have no groups". + * + * EACH ARM IS CHECKED IN BOTH DIRECTIONS. The negative arms are the point: they record + * what the fold and the flattening are BUYING, so a later reader asking "is this still + * needed?" gets a number instead of a green tick. A check that can only go green when + * the guard is present, and never demonstrates the guard's absence, is not evidence. + */ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' +import evaluate from '../plugin-designs/my-upg-100-list/data/selector-evaluator.js' +import { zonesByGroup, zonesBySelector, toSelectorRecord } from '../plugin-designs/gospel-internet/src/upgZones.js' + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') +const read = (p) => JSON.parse(readFileSync(join(ROOT, p), 'utf8')) +const D = 'plugin-designs/my-upg-100-list/data/' + +const fail = [] +const ok = (label, got, want) => { + if (got === want) console.log(` ✓ ${label}: ${got}`) + else { console.log(` ✗ ${label}: got ${got}, expected ${want}`); fail.push(label) } +} + +console.log('UPG-ZONE-JOINS') + +// ── 1. the province join, with and without the fold ───────────────────────── +const a2l = read(D + 'admin-to-list.json') +const prov = read('data/tables/gospel-by-province-groups.json').rows +const folded = zonesByGroup(a2l, prov) +ok('province join, folded — rows', folded.size, prov.length) +ok('province join, folded — zones', new Set(folded.values()).size, 12) + +// the negative arm: what lowercase-only would cost, so the fold's value is recorded +const naive = new Map(Object.entries(a2l).map(([k, v]) => [k.trim().toLowerCase(), v])) +const naiveHits = prov.filter((r) => naive.get(String(r.province).trim().toLowerCase())).length +ok('province join, UNFOLDED — rows (the fold buys the difference)', naiveHits, 514) +if (naiveHits >= prov.length) { + console.log(' ✗ the unfolded path no longer loses rows — this check can no longer fail; re-derive it') + fail.push('fold arm is inert') +} + +// ── 2. the selector join, flat records vs the object shape ────────────────── +const clusters = read(D + 'clusters.json') +const zoneLists = [...(clusters.lists || []), ...(clusters.deaf_lists || []), ...(clusters.asia_lists || [])] +const nn = read('data/tables/near-neighbors.json') +const ix = Object.fromEntries(nn.cols.map((c, i) => [c, i])) +const dec = (r, c) => (nn.dict[c] ? nn.dict[c][r[ix[c]]] : r[ix[c]]) +const records = nn.rows.map((r, i) => toSelectorRecord( + { iso3: dec(r, 'iso3'), name: dec(r, 'name'), langName: dec(r, 'langName'), bloc: dec(r, 'bloc') }, `nn-${i}`)) + +const flat = zonesBySelector(zoneLists, records, evaluate) +ok('selector join, FLAT — zones with members', [...flat.members.values()].filter((s) => s.size).length, 8) +ok('selector join — zones the evaluator rejects by design (pg_slugs)', flat.unanswerable.size, 12) + +// the negative arm: the object shape must collapse to nothing, loudly recorded +const objShaped = nn.rows.map((r, i) => ({ slug: `nn-${i}`, country_code: { value: dec(r, 'iso3') } })) +const obj = zonesBySelector(zoneLists, objShaped, evaluate) +ok('selector join, OBJECT-shaped — zones with members (must be 0)', + [...obj.members.values()].filter((s) => s.size).length, 0) + +if (fail.length) { + console.log(`\nUPG-ZONE-JOINS: FAIL — ${fail.length} assertion(s): ${fail.join(', ')}`) + process.exit(1) +} +console.log('\nUPG-ZONE-JOINS: PASS — both joins pinned in both directions') diff --git a/package.json b/package.json index b5b22adcb..8436a9d4c 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "check:experimental-shelf": "node internal/check-experimental-shelf.mjs", "check:export-poster-prints": "node plugin-designs/doxa-research-map/scripts/check-export-poster-prints.mjs", "check:export-poster-prints:maplibre": "node plugin-designs/doxa-research-map/scripts/check-export-poster-prints.mjs --provider maplibre", + "check:upg-zone-joins": "node internal/check-upg-zone-joins.mjs", "check:facet-cascade": "node internal/check-facet-cascade.mjs", "check:facet-cascade:offline": "node internal/check-facet-cascade.mjs --offline", "check:facet-cascade:self-test": "node internal/check-facet-cascade.mjs --self-test", @@ -267,7 +268,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: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:frozen-labels", - "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", + "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", "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",