diff --git a/internal/check-list-selection.mjs b/internal/check-list-selection.mjs index be60492f7..c64ea78c8 100644 --- a/internal/check-list-selection.mjs +++ b/internal/check-list-selection.mjs @@ -126,5 +126,32 @@ ok(realGap.join(',') === MISSING.join(','), 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) diff --git a/library/components/legends/listSelection.js b/library/components/legends/listSelection.js index 14a79e795..5758dc3e5 100644 --- a/library/components/legends/listSelection.js +++ b/library/components/legends/listSelection.js @@ -64,6 +64,22 @@ export function withUnassigned(nodes, allZoneIds, label = 'Unassigned') { * the union of its zones). The consumer expands a region id to its zones using the zone data it * already holds — which is also where the diacritic-folding join lives, so there is no second copy * of it here to drift. + * + * ★ REGION IDS AND ZONE IDS SHARE ONE NAMESPACE, SO NAMESPACE YOUR REGION ROWS (e.g. `region:europe`, + * leaves left bare). One bare string on one channel carries no kind, and the two id sets overlap in + * the shipped data: measured against zone-regions.json there are 33 zone ids and 7 region ids, and + * the intersection is exactly {europe}, whose region is itself. So `europe` alone cannot tell a + * consumer whether the Europe REGION 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` currently contains exactly one zone, so both + * readings produce the same set — the bug is masked by a data coincidence, not absent. It goes live + * the day a second European zone is filed, and then it fails the quiet way: a smaller result, no + * error, no empty state. check:list-selection FAILS ON PURPOSE at that moment. + * + * This component cannot fix it: the CONSUMER builds the tree, so only the consumer knows which rows + * are regions. Found by gospel-plugin while wiring, who is namespacing their region rows; written + * here because the next consumer to build a tree from the same zone data inherits the same collision. */ export function selectedId(node) { return node && node.id != null ? String(node.id) : null