doxa-maps/internal/check-list-selection.mjs
Doxa Maps 891f7535d8
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source 53d386262
2026-09-28 14:27:42 +00:00

130 lines
9.6 KiB
JavaScript

#!/usr/bin/env node
/**
* check-list-selection — what the vertical legend emits, and the zones a region tree forgets. Headless.
*
* ★ WHY THIS IS IN internal/ AND NOT internal-tracker/. It began as a internal-tracker probe, which is GITIGNORED
* in this repo (.gitignore:113) — so it ran green on my disk and would NOT have landed with the
* component. studio caught that while staging: "the 13 assertions are not in either commit". I had
* reported them as evidence for code that was about to ship untested, which is the same failure this
* whole file guards against — a green result measured somewhere other than where it matters.
* A probe proves something once. A registered check keeps proving it.
*/
// What the vertical legend emits, and the zones a region tree forgets.
//
// The second half is the one that matters most: four joinable UPG-100 zones are absent from
// zone-regions.json today, one of them the consumer's largest at 100 groups. A region-grouped tree
// drops them with no error and no empty state — just a shorter tree — so this asserts they survive.
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { leavesOf, zoneIdsIn, withUnassigned, selectedId, UNASSIGNED_ID } from '../library/components/legends/listSelection.js'
const DATA = join(dirname(fileURLToPath(import.meta.url)), '..', 'plugin-designs', 'my-upg-100-list', 'data')
const readJson = (f) => JSON.parse(readFileSync(join(DATA, f), 'utf8'))
let fails = 0
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
const TREE = [{
id: 'asia', label: 'Asia', depth: 0,
children: [
{ id: 'z1', label: 'Zone 1', zoneKey: 'Z1' },
{ id: 'sub', label: 'Sub', depth: 1, children: [{ id: 'z2' }, { id: 'z3' }] },
],
}]
// The real gap, named as it appears in the data.
const MISSING = [
'asia-china-guizhou-province',
'asia-china-xinjiang-uyghur-autonomous-region',
'asia-india-bihar',
'asia-india-madhya-pradesh',
]
ok(leavesOf(TREE[0]).map((l) => l.id).join(',') === 'z1,z2,z3', 'leavesOf walks NESTED children, not one level')
ok([...zoneIdsIn(TREE)].sort().join(',') === 'z1,z2,z3', 'zoneIdsIn collects every leaf id in the tree')
// ── the emit contract ───────────────────────────────────────────────────────────────────────────
ok(selectedId({ id: 'z1' }) === 'z1', 'a LEAF emits its zone id')
ok(selectedId({ id: 'asia' }) === 'asia', 'a REGION row emits the region id — the consumer expands it, where the join lives')
ok(selectedId(null) === null, 'and a CLEARED selection emits null, so a filter can be dropped')
ok(typeof selectedId({ id: 'z1' }) === 'string',
'the value is a STRING, so it survives ?zone=, profile-config and a deep link')
// ── never drop a zone ───────────────────────────────────────────────────────────────────────────
const all = ['z1', 'z2', 'z3', ...MISSING]
const withBucket = withUnassigned(TREE, all, 'Unassigned')
ok(withBucket.length === TREE.length + 1, 'zones absent from the tree get an explicit bucket')
const bucket = withBucket[withBucket.length - 1]
ok(bucket.id === UNASSIGNED_ID && bucket.children.length === 4, 'and all four missing zones are in it')
ok(bucket.children.map((c) => c.id).includes('asia-china-xinjiang-uyghur-autonomous-region'),
'including xinjiang — the consumer\'s largest zone at 100 groups, which a region tree loses silently')
ok(bucket.children.every((c) => c.id && c.zoneKey),
'each is a SELECTABLE leaf, not a lump — a reader wants xinjiang, not "the four nobody filed"')
ok(withUnassigned(TREE, ['z1', 'z2', 'z3']).length === TREE.length,
'CONTROL: no bucket appears when nothing is missing')
ok(withUnassigned(TREE, null).length === TREE.length && withUnassigned(TREE, []).length === TREE.length,
'CONTROL: omitting the zone set leaves the tree untouched, so this is additive')
// NEG CONTROL: the drop this exists to prevent must be detectable.
const naive = TREE // what a region-grouped tree gives you, unaugmented
ok(!zoneIdsIn(naive).has('asia-china-xinjiang-uyghur-autonomous-region'),
'NEG CONTROL: the unaugmented tree really does omit xinjiang, so the bucket is not decoration')
// ── a region sweep is NOT a zone sweep (gospel, END-GOS-VLG2) ───────────────────────────────────
// ★ The consequence of the bucket that neither of us should leave implicit: ticking every REGION
// still does not select every ZONE, because the four unfiled zones have no region BY DEFINITION.
// Nothing on screen would say so — the reader just gets a smaller total, which is the exact
// silent-wrong-answer shape the bucket exists to end. So if a "select all" is ever added it must
// sweep ZONES, not regions, or it inherits the bug. Pinned here so the wrong one cannot ship
// quietly; gospel pins the mirror of it on the expansion side.
const regionRows = withBucket.filter((n) => n.id !== UNASSIGNED_ID)
const byRegionSweep = zoneIdsIn(regionRows)
ok(byRegionSweep.size < all.length,
'selecting every REGION reaches fewer zones than exist — a region sweep is not a zone sweep')
ok(!byRegionSweep.has('asia-china-xinjiang-uyghur-autonomous-region'),
'and the 100 groups it misses are xinjiang\'s, so a "select all" over regions must be over zones')
ok(zoneIdsIn(withBucket).size === all.length,
'sweeping the WHOLE tree, bucket included, does reach all of them — what the bucket buys')
// ── a zone with no members stays SELECTABLE (gospel: 16 of 33 resolve today) ────────────────────
// Of the 33 zones, members resolve for 16; 5 await a wagf field and 9 have no selector anywhere.
// Those zones must render as EMPTY, not vanish — same reason as the bucket. A tree that hides what
// it cannot resolve tells the reader the zone does not exist, when the truth is that we cannot
// currently join it. Nothing here filters on member count, and this fails if anything starts to.
const EMPTYZONE = [{ id: 'asia', label: 'Asia', depth: 0, children: [{ id: 'z9', label: 'Zone 9', zoneKey: 'Z9', members: [] }] }]
ok(leavesOf(EMPTYZONE[0]).map((l) => l.id).join(',') === 'z9',
'a zero-member zone is still a LEAF, so a reader can select it and see an empty result')
ok(selectedId(leavesOf(EMPTYZONE[0])[0]) === 'z9', 'and it still emits its own id')
ok(withUnassigned(EMPTYZONE, ['z9']).length === EMPTYZONE.length,
'presence is by ID, not by member count, so an unresolvable zone is not ALSO dumped in the bucket')
// ── the gap is REAL TODAY, not just real in my fixture (gospel, END-GOS-VLG3) ───────────────────
// ★ EVERY ASSERTION ABOVE RUNS AGAINST A FIXTURE. That proves the MODULE behaves, and it proves the
// fixture models a gap — it does not prove the gap still exists in the data we ship. So the negative
// control above could stay green forever while the bucket quietly became dead code, or while a FIFTH
// zone went missing and nobody heard. That is the same failure in a new costume: evidence that lives
// somewhere other than where the thing it describes lives. gospel named it — find out where your
// evidence lives — after catching the identical shape in their own work.
//
// This does NOT resolve the three disagreeing lists (33 in zone-regions, 12 reachable via
// admin-to-list, 14 in groupings, disagreeing in BOTH directions). That is a data-ownership ruling
// for upg100 and the agent, and picking a winner here would be exactly the silent join we have both
// refused. It only compares the region tree's own source against the joinable set and RECORDS the
// difference, so the disagreement cannot drift without someone being told.
const regionOf = readJson('zone-regions.json') // the region tree's source: zone id -> region
const adminToList = readJson('admin-to-list.json') // admin name -> zone id; its VALUES are joinable
const filed = new Set(Object.keys(regionOf))
const joinable = new Set(Object.values(adminToList).map(String))
const realGap = [...joinable].filter((id) => !filed.has(id)).sort()
ok(realGap.length > 0,
realGap.length > 0
? `the region tree really does omit ${realGap.length} joinable zone(s) TODAY — the bucket is load-bearing in the shipped data, not only in the fixture`
: 'INERT ARM: every joinable zone is now filed under a region, so this check no longer measures anything. That is not a pass. Re-derive the bucket against the current data or DELETE it and its component — do not leave a green arm guarding nothing.')
ok(realGap.join(',') === MISSING.join(','),
realGap.join(',') === MISSING.join(',')
? 'and it is exactly the four this check names, so the fixture above models the shipped data rather than a story about it'
: `THE GAP MOVED — re-derive. shipped now: [${realGap.join(', ')}] but this check asserts: [${MISSING.join(', ')}]. A zone ADDED here is groups that just became unreachable; a zone REMOVED got filed and the fixture is stale. Either way the numbers in the commit body are no longer true.`)
ok(joinable.has('asia-china-xinjiang-uyghur-autonomous-region') && !filed.has('asia-china-xinjiang-uyghur-autonomous-region'),
'xinjiang specifically: joinable in admin-to-list, absent from zone-regions — its 100 groups are reachable ONLY through the bucket')
console.log(fails ? `\nLIST-SELECTION: ${fails} FAIL(s)` : '\nLIST-SELECTION: serialisable ids out, and no joinable zone can be dropped')
process.exit(fails ? 1 : 0)