Both consumers read the zones of the list that is showing, not UPG-100's by name
The behaviour-visible half of the zones seam, kept in its own commit so a regression bisects to one
change. Every list still resolves to UPG-100's zones today, so this is a no-op until a second zone set is
registered — the point is that the seam now exists for the second list to be built against.
★ allLists()'s OWN NOTE PROMISED "a one-line change instead of an audit". IT WAS FOUR SITES, and I found
them by grepping the IDENTIFIER rather than trusting the note:
allLists() the documented seam
bootstrapLegend() builds the tree AND attaches selectors from the buckets directly
two legend rebuilds buildLegendNodes(clusters, {records}) — these pass the WHOLE OBJECT, so a
`clusters.` grep misses them entirely
Left on the static import, bootstrapLegend would have built the legend from UPG-100's zones while the list
showed another list's, and the two would have disagreed with nothing reporting it. A convention comment
saying "do not read this directly" is a statement of intent, not a guarantee that nobody did.
⚠ AND poster-wall WAS ALREADY HALF LIST-AWARE, which is the worst of the two states. It filtered its
RECORDS by list (applyPeopleGroupList) while taking its ZONE NAMES from clusters.json — so another list's
people groups appeared arranged under UPG-100's zone names, which LOOKS right. Now both follow the list.
RESOLVED PER CALL, NOT ONCE AT SETUP, and that ordering is forced rather than chosen: profileConfig is
injected at upg-100-list.vue:126, AFTER the import block, so a module-level
`const clusters = zonesForList(...)` throws on the temporal dead zone — my first attempt did exactly that.
It is also more correct: profileConfig is a ref a host can change, and the list can arrive later than
setup. Cheap, because zonesForList is a registry lookup returning a shared object.
⚠ check:no-free-identifiers CAUGHT A REAL BUG HERE: I used resolvePeopleGroupList in poster-wall without
importing it. That is a name that compiles and throws at runtime in exactly the path nobody clicked —
which is the gate's own stated reason for existing, doing its job on me.
Also corrected a stale comment: poster-wall said "the same 27 the map paints". The legend has 28 leaves;
this spread yields 27 because it reads the three geographic buckets and the deaf row is synthesised by
buildLegendNodes. The number was right and the sentence was describing the wrong thing.
Verified: check:sfc-parse PASS, check:no-free-identifiers PASS, Vite builds (833.06 kB, +1.0 kB for the
resolver — not duplicated data), check:zoning-declared still PASS, and all four zone labels plus the zone
ids are still inlined exactly once in the built bundle, so the data path is intact.
This commit is contained in:
parent
04630e1571
commit
278bdde885
2 changed files with 60 additions and 19 deletions
|
|
@ -41,11 +41,11 @@
|
|||
import { computed, inject, ref } from 'vue'
|
||||
import { useOptionalT } from '@map/composables/useOptionalT.js'
|
||||
// The page's people-group LIST (library/lists/): every people-group fetch below keeps only its groups.
|
||||
import { applyPeopleGroupList } from '@map/lists/peopleGroupList.js'
|
||||
import { applyPeopleGroupList, resolvePeopleGroupList } from '@map/lists/peopleGroupList.js'
|
||||
import PosterWall from '@map/components/poster/PosterWall.vue'
|
||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||
// Named imports: only the list arrays ship in the bundle; the file's bookkeeping fields stay out.
|
||||
import { lists as clusterLists, asia_lists as clusterAsiaLists, uncovered_lists as clusterUncoveredLists } from './data/clusters.json'
|
||||
import { zonesForList } from './data/zones-for-list.js'
|
||||
// The SAME normalize+evaluate pipeline the sibling map profile uses to turn the live
|
||||
// /people-groups/list payload + a list's `selector` into the exact rows that belong to it.
|
||||
import { normalizeRecords, extractRecords } from './data/normalize.js'
|
||||
|
|
@ -115,12 +115,27 @@ const regionFor = (id) => {
|
|||
if (id.startsWith('asia-china-')) return 'China + Mongolia'
|
||||
return 'Other'
|
||||
}
|
||||
// One flat list from the three cluster groups — the same 27 the map paints.
|
||||
const items = computed(() => [
|
||||
...(clusterLists || []),
|
||||
...(clusterAsiaLists || []),
|
||||
...(clusterUncoveredLists || []),
|
||||
].filter(l => l && l.id))
|
||||
// One flat list of the zones the map paints — for WHICHEVER list is showing.
|
||||
//
|
||||
// ★ THROUGH zonesForList, not a static clusters.json import (pattern work, 2026-09-27). This wall already
|
||||
// filtered its RECORDS by list (applyPeopleGroupList, below), so before this it showed another list's
|
||||
// people groups arranged under UPG-100's zone names — half list-aware, which is the worst of the two
|
||||
// states because it looks right. Now the zones follow the list too.
|
||||
// Safe inside a computed: profileConfig is injected at :54 and this evaluates lazily, after setup —
|
||||
// a module-level call would hit the temporal dead zone (it does in upg-100-list.vue, which is why that
|
||||
// file resolves per call as well).
|
||||
// ⚠ AND THE OLD COMMENT SAID "the same 27", which was already stale: the legend has 28 leaves, because
|
||||
// buildLegendNodes collapses the two deaf_lists into one unified row. This spread still yields 27 because
|
||||
// it reads the three geographic buckets and the deaf row is synthesised — so the number is right for what
|
||||
// this list IS, and the old comment was describing the wrong thing.
|
||||
const items = computed(() => {
|
||||
const z = zonesForList(resolvePeopleGroupList(profileConfig))
|
||||
return [
|
||||
...(z.lists || []),
|
||||
...(z.asia_lists || []),
|
||||
...(z.uncovered_lists || []),
|
||||
].filter(l => l && l.id)
|
||||
})
|
||||
|
||||
// ─── THE RECORDS PROVIDER — the one thing that cannot be shared ──────────────
|
||||
async function fetchListRecords(list) {
|
||||
|
|
|
|||
|
|
@ -83,9 +83,15 @@ import { ensureEngineScript, engineGlobal, engineStylesheetUrl } from '@map/comp
|
|||
// registration only ever worked by accident.
|
||||
import { installRtlTextPlugin } from '@map/maps/rtl-text.js'
|
||||
import { resolveNamedStyle } from '@map/styles/registry.js'
|
||||
// Named imports: only the list arrays ship in the bundle; the file's bookkeeping fields stay out.
|
||||
import { lists as clusterLists, asia_lists as clusterAsiaLists, uncovered_lists as clusterUncoveredLists } from './data/clusters.json'
|
||||
const clusters = { lists: clusterLists, asia_lists: clusterAsiaLists, uncovered_lists: clusterUncoveredLists }
|
||||
// ★ NO DIRECT clusters.json IMPORT ANY MORE — every zone read goes through listZones() so the legend, the
|
||||
// list, the selectors and the packet all see the SAME zones for whatever list is showing. The named-import
|
||||
// tree-shaking this line used to protect now lives in zones-for-list.js, which keeps a static named import
|
||||
// for exactly that reason.
|
||||
// ⚠ FOUR SITES READ THE ZONES, not one. allLists()'s own note promised the `list` swap would be "a one-line
|
||||
// change instead of an audit"; it was four, found by grepping the IDENTIFIER rather than trusting the note:
|
||||
// allLists, bootstrapLegend (which also attaches selectors), and two legend rebuilds that pass the whole
|
||||
// object so a `clusters.` grep misses them. Recorded so the next reader greps too.
|
||||
import { zonesForList } from './data/zones-for-list.js'
|
||||
import { normalizeRecords, extractRecords } from './data/normalize.js'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
// The in-place print helper: a hidden same-page iframe, NOT window.open(). A popup
|
||||
|
|
@ -1424,9 +1430,22 @@ async function printPrayerPacket(zoneId) {
|
|||
// universal `list` prop lands (exp-shepherd Tier A: profile-config.list / ?list= / window.MAP_LIST,
|
||||
// default 'doxa') it replaces the body here and nothing else moves. Do not read `clusters` directly
|
||||
// anywhere else; that is what makes the swap a one-line change instead of an audit.
|
||||
// ★ AND THIS IS THAT SWAP, taken 2026-09-27 — the body, exactly as the note above predicted, with the
|
||||
// static `clusters` import left untouched so its tree-shaking survives (zones-for-list.js keeps the named
|
||||
// import for that reason). resolvePeopleGroupList reads config.list / ?list= / window.MAP_LIST, so a
|
||||
// partner already choosing a LIST now also gets that list's ZONES.
|
||||
// ⚠ RESOLVED PER CALL, NOT ONCE AT SETUP. `profileConfig` is injected at :126 — after the import block —
|
||||
// so a module-level `const clusters = zonesForList(...)` throws on the temporal dead zone. It is also more
|
||||
// correct per call: profileConfig is a ref a host can change, and the list can arrive later than setup.
|
||||
// Cheap, because zonesForList is a registry lookup returning a shared object, not a build.
|
||||
function listZones() {
|
||||
return zonesForList(resolvePeopleGroupList(profileConfig));
|
||||
}
|
||||
|
||||
function allLists() {
|
||||
return [...(clusters.lists || []), ...(clusters.asia_lists || []),
|
||||
...(clusters.uncovered_lists || [])];
|
||||
const z = listZones();
|
||||
return [...(z.lists || []), ...(z.asia_lists || []),
|
||||
...(z.uncovered_lists || [])];
|
||||
}
|
||||
|
||||
// ── THE PAGE-2 FUNNEL CONTEXT ───────────────────────────────────────────────
|
||||
|
|
@ -1893,14 +1912,21 @@ function findLegendNode(nodes, key) {
|
|||
// (both are plain module-scope `let`s, so building them early is free).
|
||||
// Idempotent: re-running it just rebuilds the same tree from the same static input.
|
||||
function bootstrapLegend() {
|
||||
const nodes = buildLegendNodes(clusters, { deafCount: DEAF_COUNT })
|
||||
// ⚠ THE SAME ZONES allLists() USES, via listZones(), and this is the SECOND site — the note on
|
||||
// allLists promised the `list` swap would be "a one-line change instead of an audit" and that was not
|
||||
// quite true: this function reads the cluster buckets DIRECTLY to attach selectors. Left on the static
|
||||
// import it would have built the legend from UPG-100's zones while the list showed another list's, and
|
||||
// the two would disagree with nothing reporting it. Found by grepping for `clusters.` rather than by
|
||||
// trusting the convention comment.
|
||||
const z = listZones()
|
||||
const nodes = buildLegendNodes(z, { deafCount: DEAF_COUNT })
|
||||
legendNodes.value = nodes
|
||||
leafZoneList = leafZones(nodes)
|
||||
const byId = {}
|
||||
for (const l of clusters.lists || []) byId[l.id] = l.selector
|
||||
for (const a of clusters.asia_lists || []) byId[a.id] = a.selector
|
||||
for (const l of z.lists || []) byId[l.id] = l.selector
|
||||
for (const a of z.asia_lists || []) byId[a.id] = a.selector
|
||||
// slice 09b-6 §1a: the 7 Frontier-Asia uncovered zones (country_codes → ADM0).
|
||||
for (const u of clusters.uncovered_lists || []) byId[u.id] = u.selector
|
||||
for (const u of z.uncovered_lists || []) byId[u.id] = u.selector
|
||||
byId['deaf'] = { type: 'affinity_bloc', rop1: 'A017' }
|
||||
for (const z of leafZoneList) z.selector = byId[z.id]
|
||||
matchColorExpr = buildMatchColorExpr(leafZoneList)
|
||||
|
|
@ -1920,7 +1946,7 @@ async function enrichLegendFromRecords() {
|
|||
if (!resp.ok) throw new Error(`list ${resp.status}`)
|
||||
const records = normalizeRecords(await applyPeopleGroupList(extractRecords(await resp.json()), profileConfig))
|
||||
countryNameMap = buildCountryNameMap(records)
|
||||
legendNodes.value = buildLegendNodes(clusters, { deafCount: DEAF_COUNT, records })
|
||||
legendNodes.value = buildLegendNodes(listZones(), { deafCount: DEAF_COUNT, records })
|
||||
} catch (_) { /* counts-only legend is the honest fallback; keep it rendered */ }
|
||||
}
|
||||
|
||||
|
|
@ -2240,7 +2266,7 @@ onMounted(async () => {
|
|||
|
||||
// Re-shape the legend tree WITH records so every node carries `pop` (slice
|
||||
// 04u §1b). Selectors/colors/counts are unchanged; only `pop` is added.
|
||||
legendNodes.value = buildLegendNodes(clusters, { deafCount: DEAF_COUNT, records })
|
||||
legendNodes.value = buildLegendNodes(listZones(), { deafCount: DEAF_COUNT, records })
|
||||
|
||||
|
||||
// ── pins layer (kept from 04b, now stamped with zoneKey for isolate dim) ──
|
||||
|
|
|
|||
Loading…
Reference in a new issue