Make the zone-join assertions travel, and record what each guard buys

Both joins were verified while they were written, and that verification lived in
a terminal. A probe proves something once; only a registered check keeps
proving it. Collected by gates:pre, where it runs as check:upg-zone-joins.

Both defects it guards are silent. Neither throws, neither renders an error, and
both return a well-formed answer that is merely smaller than the truth: admin
names whose diacritics are not folded join 514 of 1009 rows instead of 1009, and
records passed in the pre-normalize { value, label } shape match nothing at all,
so every zone reports as having no groups.

Each arm is checked in both directions, and the negative arms are the point. The
check records that the fold is worth 495 rows and that the object shape resolves
zero zones, so a later reader asking whether a guard is still needed gets a
number rather than a green tick. It also fails if the unfolded path ever stops
losing rows, because at that moment the assertion would no longer be able to
fail and would stop being evidence.
This commit is contained in:
Doxa Maps 2026-09-28 13:42:16 +00:00
parent 04bb5b264c
commit 6b6c745dbe
2 changed files with 86 additions and 1 deletions

View file

@ -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')

View file

@ -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",