Every reference to the Driver in internal/ is reworded the same way as in library/: the reason stays, the speaker and the quote go, an open decision reads "provisional, pending a product decision", and agent names and decision- sheet rows are dropped. Where a gate printed the word, its message now names the reviewer or the ruling instead. Three spots were code, not comments, and changed with their callers: - the element census's allowlist field `driver` is now `ruling`, in the gate and both presets (same validation: match, ruling, date and card required); - check:i18n-never-translated's DRIVER-GATED marker is DECISION-PENDING, in the entries and in the regex that counts them (its output is otherwise byte-identical); - the package.json description of the census says "ruling". public-hygiene-check keeps its two patterns for the word: they are what keep it out of the served folder.
194 lines
15 KiB
JavaScript
194 lines
15 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 _qa-cycle/. It began as a _qa-cycle 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'
|
|
// ★ TWO ROOTS, DELIBERATELY, BECAUSE ONLY TWO OF THE THREE FILES MOVED. zone-regions.json and
|
|
// groupings.json now live in data/tables/ — the shared asset pool the tree copies into
|
|
// public/dx-maps/assets/tables/ — because gospel's legend renders IN THE BROWSER and needs them at
|
|
// runtime; a build-time import path would not reach the bundle. admin-to-list.json stays in the
|
|
// plugin, provisional, pending a product decision on the "zones fold" regeneration, so it is read
|
|
// from the old root. Collapsing these to one constant silently breaks whichever file is not where you pointed.
|
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const TABLES = join(REPO, 'data', 'tables')
|
|
const PLUGIN_DATA = join(REPO, 'plugin-designs', 'my-upg-100-list', 'data')
|
|
const readJson = (f) => JSON.parse(readFileSync(join(f === 'admin-to-list.json' ? PLUGIN_DATA : TABLES, 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 WAS here and is now FILED. upg100 hit this arm while filing it, and
|
|
// did the right thing with it: they reported the red instead of editing my expectation to make
|
|
// their change green — "editing someone else's pinned expectation to make my own change green is
|
|
// precisely the move that hollows out a guard". dx-shepherd then ruled that their data and this
|
|
// fixture land TOGETHER, which is the only ordering where neither of us is red in between.
|
|
|
|
|
|
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]
|
|
// Counted from MISSING, never written as a literal: the gap CHANGES (upg100 filed one while I was
|
|
// writing this), and a hardcoded 4 here turns every legitimate data fix into a puzzling red in an
|
|
// arm that is not even about the data.
|
|
ok(bucket.id === UNASSIGNED_ID && bucket.children.length === MISSING.length,
|
|
`and all ${MISSING.length} 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 ones 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 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 question,
|
|
// provisional, pending a product decision, and picking a winner here would be exactly the silent join
|
|
// this repo refuses. 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()
|
|
// ★ WHAT THIS GAP COUNTS, AND WHAT IT DOES NOT. `joinable` is admin-to-list's VALUES — zones
|
|
// reachable by an admin-area join. That is NOT the same as every printable zone, so this arm can
|
|
// only ever report a LOWER BOUND on unfiled zones. upg100 proved the difference costs something:
|
|
// checking the property rather than the four names they were handed, they found deaf-old-world,
|
|
// deaf-new-world-asia-pacific, asia-india-uttarakhand and asia-india-west-bengal also unfiled,
|
|
// and none of the four is an admin-to-list value, so none of them appears in realGap. Two were
|
|
// then filed; the deaf pair look deliberate (there is a "deaf": "exempt" umbrella key). Stated
|
|
// here because a gap number that is quietly a lower bound reads exactly like a complete one.
|
|
|
|
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.`)
|
|
// ── the zones that got FILED stay filed (dx-shepherd RG12) ─────────────────────────────────────
|
|
// The gap arms watch the BAD direction. Filing is the good one, and it is exactly as revertible:
|
|
// drop a key from zone-regions and those groups leave the region tree again, silently, with the
|
|
// bucket the only thing still carrying them. Two of these three were never visible to the gap arms
|
|
// at all (see the coverage note below), so without this they could be unfiled with nothing to say so.
|
|
const FILED = ['asia-india-madhya-pradesh', 'asia-india-uttarakhand', 'asia-india-west-bengal']
|
|
for (const z of FILED) {
|
|
ok(filed.has(z), filed.has(z)
|
|
? `${z} is filed under a region, so it reaches readers through the tree and not only the bucket`
|
|
: `${z} IS NO LONGER FILED — it was given a region and has lost it, so its groups are back to being reachable only through the unassigned bucket. That is the silent direction: no error, just a shorter region tree.`)
|
|
}
|
|
|
|
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')
|
|
|
|
// ── the id ambiguity that is masked by a data coincidence (gospel, END-GOS-VLGC) ────────────────
|
|
// ★ REGION IDS AND ZONE IDS SHARE ONE NAMESPACE. selectedId emits one bare string with no kind, and
|
|
// the two sets OVERLAP in the shipped data. gospel found it while wiring and I re-measured it here
|
|
// rather than taking the report: 33 zone ids, 7 region ids, intersection exactly {europe}, whose
|
|
// region is itself. So `europe` cannot tell a consumer whether the Europe REGION row was clicked
|
|
// (expand to every European zone) or the Europe ZONE leaf (filter to that one).
|
|
//
|
|
// It is not live today ONLY because region `europe` contains exactly one zone, so both readings give
|
|
// the same set. That is a coincidence in the data, not a property of the design, and it is the same
|
|
// shape as the inert arm above: something green because reality currently happens to agree with it.
|
|
// The arm below is therefore the alarm, not the fix — the consumer builds the tree, so only the
|
|
// consumer can namespace its region rows, and gospel is doing that.
|
|
const zonesAll = new Set(Object.keys(regionOf))
|
|
const regionsAll = new Set(Object.values(regionOf).map(String))
|
|
const collide = [...zonesAll].filter((id) => regionsAll.has(id)).sort()
|
|
ok(collide.join(',') === 'europe',
|
|
collide.join(',') === 'europe'
|
|
? 'region ids and zone ids share one namespace and collide on exactly {europe} — measured, not assumed'
|
|
: `THE COLLIDING IDS CHANGED — re-derive. shipped now: [${collide.join(', ') || 'none'}], this check asserts: [europe]. Every id in both sets is one a consumer cannot disambiguate from the emitted string alone.`)
|
|
for (const id of collide) {
|
|
const members = Object.keys(regionOf).filter((z) => String(regionOf[z]) === id)
|
|
ok(members.length === 1,
|
|
members.length === 1
|
|
? `and region '${id}' still contains exactly one zone, so both readings of '${id}' give the same set — the ambiguity is MASKED, not absent`
|
|
: `THE '${id}' AMBIGUITY IS NOW LIVE: region '${id}' contains ${members.length} zones (${members.join(', ')}), so emitting the bare id '${id}' means either that whole region or the single zone of the same name, and the two now differ. Nothing on screen would say which — the reader just gets a smaller set. The consumer that BUILDS the tree must namespace its region rows (region:${id}) and leave leaves bare; this component cannot know which rows are regions.`)
|
|
}
|
|
|
|
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)
|