Every zone a reader can pick resolves to its groups, Deaf included
The legend offers 28 zones; every place that turned a picked id back
into a zone spread clusters.json's three geographic buckets, which is
27. The missing one is Deaf, whose two deaf lists the legend collapses
into a single affinity-bloc row. So in the browser, silently:
- the Prayer packet for Deaf returned on "if (!zone) return";
- the Deaf poster titled itself by its slug;
- a selection containing Deaf dropped it in .filter(Boolean);
- the packet count read 27 while 28 packets exist, and that count
prints on page 1 and at the tip of page 2's triangle.
The packet fetch also lacked rop1, the field the Deaf selector reads,
so even a found Deaf zone would have resolved to nobody.
The fix is one enumeration. legend-tree.js now exports DEAF_SELECTOR
(the unified row's selector, which exists nowhere in clusters.json) and
zonesWithSelectors(clusters): the legend's leaves with their selectors
attached. The plugin's printableZones() keeps the 27 catalogue objects
exactly as they were and adds only what the catalogue lacks, so the
packet, poster, selection and count all see the same 28. rop1 joins
both packet fetches.
The three hand-typed copies of the Deaf literal (the map, the poster
generator, the offline packet generator) now import the one
definition; the offline generator's own id-to-selector table is
replaced by zonesWithSelectors. The copies left are frozen snapshots and
proof scripts, which keep independent literals on purpose.
The offline sweep gate's coverage had the same 27: it never required a
Deaf packet. It now counts through the legend.
Verified: the live API carries exactly 123 A017 rows, matching the
legend's Deaf count; the default 3x4 posters for sudan, kerala, deaf
and west-africa are byte-identical before and after.
This commit is contained in:
parent
f3e5556875
commit
502d7107e3
5 changed files with 71 additions and 25 deletions
|
|
@ -83,7 +83,10 @@ if (!files.length) {
|
|||
const CLUSTERS = resolve(REPO, 'plugin-designs/my-upg-100-list/data/clusters.json')
|
||||
if (existsSync(CLUSTERS)) {
|
||||
const z = JSON.parse(readFileSync(CLUSTERS, 'utf8'))
|
||||
const zones = [...(z.lists || []), ...(z.asia_lists || []), ...(z.uncovered_lists || [])].map((l) => l.id)
|
||||
// ★ THE LEGEND'S 28, NOT THE THREE BUCKETS' 27: spreading lists/asia_lists/uncovered_lists drops
|
||||
// `deaf`, the one zone the legend shows as a single row, so this never required a Deaf packet.
|
||||
const { buildLegendNodes, leafZones } = await import(pathToFileURL(resolve(REPO, 'plugin-designs/my-upg-100-list/data/legend-tree.js')).href)
|
||||
const zones = leafZones(buildLegendNodes(z, { records: null })).map((l) => l.id)
|
||||
const have = new Set(all.map((f) => f.replace(/\.html$/, '')))
|
||||
for (const id of zones) if (!have.has(id)) failures.push(`zone '${id}' has NO generated packet at all`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const SERIES_DEF = SERIES_DEF_ALL[SERIES_ID];
|
|||
function readJSON_early(p) { return JSON.parse(readFileSync(p, 'utf8')); }
|
||||
|
||||
// ── the map's own logic, reused read-only ────────────────────────────────────
|
||||
const { buildLegendNodes, leafZones } = await import(join(DATA, 'legend-tree.js'));
|
||||
const { buildLegendNodes, leafZones, DEAF_SELECTOR } = await import(join(DATA, 'legend-tree.js'));
|
||||
const { resolveZoneGeometry } = await import(join(DATA, 'zone-geometry.js'));
|
||||
const { evaluate } = await import(join(DATA, 'selector-evaluator.js'));
|
||||
const { normalizeRecords, extractRecords } = await import(join(DATA, 'normalize.js'));
|
||||
|
|
@ -2863,7 +2863,7 @@ 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 u of clusters.uncovered_lists || []) byId[u.id] = u.selector;
|
||||
byId['deaf'] = { type: 'affinity_bloc', rop1: 'A017' };
|
||||
byId['deaf'] = DEAF_SELECTOR; // defined once in legend-tree.js
|
||||
// Country zones already carry their own country_code selector — reassigning from
|
||||
// byId (which only knows 100-list ids) would blank every one of them.
|
||||
if (!COUNTRIES && !CITIES) for (const z of zones) z.selector = byId[z.id];
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ const { evaluate } = await import(join(DATA, 'selector-evaluator.js'));
|
|||
const { normalizeRecords, extractRecords } = await import(join(DATA, 'normalize.js'));
|
||||
// The zone enumeration itself, so this generator, the poster generator and upg-100-list.vue cannot
|
||||
// disagree about which zones exist (see allZones below).
|
||||
const { buildLegendNodes, leafZones } = await import(join(DATA, 'legend-tree.js'));
|
||||
const { buildLegendNodes, leafZones, zonesWithSelectors } = await import(join(DATA, 'legend-tree.js'));
|
||||
|
||||
async function loadRecords() {
|
||||
// ★ A COMMITTED FIXTURE, so CI can build a packet at all. out/ is gitignored — both the
|
||||
|
|
@ -353,17 +353,10 @@ function allZones() {
|
|||
selector: { type: 'country_codes', values: [c.iso3] },
|
||||
}));
|
||||
}
|
||||
const tops = buildLegendNodes(clusters, { records: null });
|
||||
const byId = {};
|
||||
for (const k of ['lists', 'asia_lists', 'uncovered_lists']) {
|
||||
for (const z of clusters[k] || []) byId[z.id] = z.selector;
|
||||
}
|
||||
// The unified deaf leaf's selector. DUPLICATED from legend-tree.js:99's DEAF_SELECTOR, which is
|
||||
// function-scoped rather than exported, and from generate.mjs:2805 which hardcodes the same literal.
|
||||
// Kept as a literal here rather than exporting a third consumer's dependency mid-hold; if a fourth
|
||||
// caller ever needs it, export it once from legend-tree.js and delete all three copies.
|
||||
byId.deaf = { type: 'affinity_bloc', rop1: 'A017' };
|
||||
return leafZones(tops).map((z) => ({ ...z, zoneKey: z.zoneKey || z.id, selector: byId[z.id] }));
|
||||
// The legend's leaves with their selectors, the unified deaf leaf included — ONE enumeration
|
||||
// (legend-tree.js zonesWithSelectors) shared with the browser packet and the map, replacing the
|
||||
// id->selector table and deaf literal this function used to type out itself.
|
||||
return zonesWithSelectors(clusters, { records: null });
|
||||
}
|
||||
|
||||
// ── PHOTOS: parallel fetch, inlined as data URIs ────────────────────────────
|
||||
|
|
|
|||
|
|
@ -82,6 +82,13 @@ function leafPopulation(row, records) {
|
|||
* Omit to keep the prior count-only behaviour (pop undefined).
|
||||
* @returns {Array} nested TreeNode[] (3 top-level groups, 26 leaves total)
|
||||
*/
|
||||
// ★ THE UNIFIED DEAF LEAF'S SELECTOR, DEFINED ONCE. The legend collapses the two deaf_lists into one
|
||||
// leaf (affinity bloc A017) whose selector exists nowhere in clusters.json, so every consumer that
|
||||
// RESOLVES zones had to know it — and each typed its own copy of the literal (the map in
|
||||
// upg-100-list.vue, generate.mjs, packet.mjs). The browser packet path had no copy at all, which is why
|
||||
// the Deaf packet could not be printed from the UI: the zone lookup found nothing and returned silently.
|
||||
export const DEAF_SELECTOR = { type: 'affinity_bloc', rop1: 'A017' };
|
||||
|
||||
export function buildLegendNodes(clusters, opts = {}) {
|
||||
const deafCount = opts.deafCount ?? 123;
|
||||
const records = Array.isArray(opts.records) ? opts.records : null;
|
||||
|
|
@ -105,8 +112,7 @@ export function buildLegendNodes(clusters, opts = {}) {
|
|||
const uncovered = clusters.uncovered_lists || [];
|
||||
|
||||
// Deaf row's selector is implicit (affinity bloc A017), so population can be
|
||||
// summed the same way as the regional rows.
|
||||
const DEAF_SELECTOR = { type: 'affinity_bloc', rop1: 'A017' };
|
||||
// summed the same way as the regional rows. Module-level and exported — see DEAF_SELECTOR below.
|
||||
|
||||
// ── ordered leaf list (drives color index space) ───────────────────────────
|
||||
// order: 13 regional, 1 deaf, asia (10 India + 2 China), then the Frontier-Asia
|
||||
|
|
@ -379,4 +385,28 @@ export function leafZones(nodes) {
|
|||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every printable zone — the legend's leaves — each carrying the selector that resolves its groups.
|
||||
*
|
||||
* ★ THE ENUMERATION IS THE LEGEND'S, NOT clusters.json's BUCKETS. Spreading lists/asia_lists/
|
||||
* uncovered_lists gives 27 zones and silently drops `deaf`, which the legend shows as one row; a
|
||||
* packet lookup built that way cannot print Deaf, and a count built that way prints "one of 27" on
|
||||
* page 1 and at the triangle's tip while 28 packets exist. Leaves do not carry selectors, so they are
|
||||
* attached here by id, the unified deaf leaf taking DEAF_SELECTOR.
|
||||
* Returns COPIES; the legend's own node objects are left untouched.
|
||||
*
|
||||
* @param {object} clusters the zone catalogue (clusters.json shape)
|
||||
* @param {object} [opts] passed through to buildLegendNodes
|
||||
* @returns {Array<{id:string, zoneKey:string, label:string, selector:object}>}
|
||||
*/
|
||||
export function zonesWithSelectors(clusters, opts = {}) {
|
||||
const byId = {};
|
||||
for (const k of ['lists', 'asia_lists', 'uncovered_lists']) {
|
||||
for (const z of (clusters && clusters[k]) || []) byId[z.id] = z.selector;
|
||||
}
|
||||
byId.deaf = DEAF_SELECTOR;
|
||||
return leafZones(buildLegendNodes(clusters, opts))
|
||||
.map((z) => ({ ...z, zoneKey: z.zoneKey || z.id, selector: byId[z.id] }));
|
||||
}
|
||||
|
||||
export default buildLegendNodes;
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ import { useI18n } from 'vue-i18n'
|
|||
// is blockable and drops the user out of the map; the iframe opens the print dialog
|
||||
// right here. Shared so the packet, the poster and the card deck all print the same way.
|
||||
import { usePosterPrints } from '@map/composables/usePosterPrints.js'
|
||||
import { buildLegendNodes, leafZones } from './data/legend-tree.js'
|
||||
import { buildLegendNodes, leafZones, zonesWithSelectors, DEAF_SELECTOR } from './data/legend-tree.js'
|
||||
import { resolveAllZones } from './data/zone-geometry.js'
|
||||
// slice 09b-9 §3 (L18/L29 FIX): the per-zone color OVERRIDE map. zone-colors.json
|
||||
// holds the region-inherited tapestry (purple ME, red Africa, green
|
||||
|
|
@ -1336,7 +1336,7 @@ async function onLegendExport(kind) {
|
|||
// The legend selects by KEY, while the print actions label by item — hand over the zone
|
||||
// object so the print dialog's title says the list's name rather than its slug.
|
||||
// Same resolution printPrayerPacket uses, so both print actions name the list identically.
|
||||
const zone = allLists().find((z) => z.id === zoneId) || { id: zoneId };
|
||||
const zone = printableZones().find((z) => z.id === zoneId) || { id: zoneId };
|
||||
await packetPrints.printPoster(zone);
|
||||
} finally {
|
||||
// Cleared on EVERY exit path including the early `return` inside the try — a throw would
|
||||
|
|
@ -1410,7 +1410,7 @@ async function packetRecords(zone) {
|
|||
// fetch (:1988) has always carried both. Measured: a wagf_block selector returns 0 members on
|
||||
// packet rows and 1 on map rows. oceania-indonesia was the worst of the six because its union
|
||||
// still matched IDN, so it printed a PLAUSIBLE packet that was silently missing all of Oceania.
|
||||
const fields = 'slug,name,country_code,wagf_region,wagf_block,population,religion,image_url,primary_language,imb_gsec';
|
||||
const fields = 'slug,name,country_code,wagf_region,wagf_block,rop1,population,religion,image_url,primary_language,imb_gsec';
|
||||
const resp = await fetch(`${API_BASE}/api/people-groups/list?fields=${fields}&lang=${packetLang()}`,
|
||||
{ cache: 'no-store' });
|
||||
if (!resp.ok) throw new Error(`people-groups fetch failed (${resp.status})`);
|
||||
|
|
@ -1456,7 +1456,7 @@ const flashStatus = (text, ms) => flashRef(status, text, ms);
|
|||
// profiles, sets, points, wheel) or 'lightweight' (map, triangle, profiles, points, wheel).
|
||||
// Defaults to 'full', so every caller that does not pass one prints exactly what it printed before.
|
||||
async function printPrayerPacket(zoneId, variant = 'full') {
|
||||
const zone = allLists().find((z) => z.id === zoneId);
|
||||
const zone = printableZones().find((z) => z.id === zoneId);
|
||||
if (!zone) return;
|
||||
// The map svg needs the renderer's own mapCss/W, so it is resolved here and
|
||||
// handed in rather than fetched inside the shared action, which knows nothing
|
||||
|
|
@ -1500,6 +1500,24 @@ function allLists() {
|
|||
...(z.uncovered_lists || [])];
|
||||
}
|
||||
|
||||
// Every zone a reader can PICK — the legend's leaves — as the object the print actions need.
|
||||
//
|
||||
// ★ allLists() IS 27 AND THE LEGEND IS 28. It spreads the three geographic buckets and cannot see
|
||||
// `deaf`, which the legend shows as one row (the two deaf_lists collapse into a single affinity-bloc
|
||||
// leaf). Every place that turned a picked id back into a zone went through allLists(), so Deaf was
|
||||
// unreachable from all of them, silently: the Prayer packet returned on `if (!zone)`, the poster
|
||||
// titled itself by its slug, a selection containing Deaf dropped it in `.filter(Boolean)`, and the
|
||||
// packet count read 27 while 28 packets exist.
|
||||
// The 27 keep EXACTLY the catalogue objects they had; only a leaf the catalogue lacks is added, as the
|
||||
// legend's own leaf carrying its selector (legend-tree.js zonesWithSelectors). One enumeration for
|
||||
// every pick, so the four sites cannot disagree about which zones exist.
|
||||
function printableZones() {
|
||||
const base = allLists();
|
||||
const have = new Set(base.map((z) => z.id));
|
||||
return [...base, ...zonesWithSelectors(listZones(), { deafCount: DEAF_COUNT })
|
||||
.filter((z) => !have.has(z.id))];
|
||||
}
|
||||
|
||||
// ── THE PAGE-2 FUNNEL CONTEXT ───────────────────────────────────────────────
|
||||
// What the packet's funnel page needs that the packet itself cannot know: the nested IMB/network
|
||||
// steps, how many lists the catalogue holds, and how many groups are in no list at all.
|
||||
|
|
@ -1561,7 +1579,9 @@ async function packetFunnel() {
|
|||
// Carded for the Driver: 27 geographic / 29 counting deaf, and whether "Central Asia + Pakistan"
|
||||
// (55 groups, contiguous, poster-walled as a real region) is a prayer list rather than a residual.
|
||||
if (listId === LIST_DEFAULT) {
|
||||
return { ...raw, listCount: allLists().length, listTotal: doxaTotal };
|
||||
// printableZones, not allLists: the count prints on page 1 ("one of N") and at the triangle's
|
||||
// tip, and allLists() is 27 while the legend offers — and the generator builds — 28 packets.
|
||||
return { ...raw, listCount: printableZones().length, listTotal: doxaTotal };
|
||||
}
|
||||
|
||||
// A NON-DEFAULT LIST: read its own DOXA-side total from the membership file the build writes.
|
||||
|
|
@ -1613,7 +1633,7 @@ function selectedLists() {
|
|||
const ids = selectedTerritories.value
|
||||
.filter((t) => t.kind === 'zone')
|
||||
.map((t) => String(t.key).replace(/^zone:/, ''));
|
||||
const byId = new Map(allLists().map((l) => [l.id, l]));
|
||||
const byId = new Map(printableZones().map((l) => [l.id, l]));
|
||||
return ids.map((id) => byId.get(id)).filter(Boolean);
|
||||
}
|
||||
|
||||
|
|
@ -1692,7 +1712,7 @@ async function selectionAsList() {
|
|||
// fetch (:1988) has always carried both. Measured: a wagf_block selector returns 0 members on
|
||||
// packet rows and 1 on map rows. oceania-indonesia was the worst of the six because its union
|
||||
// still matched IDN, so it printed a PLAUSIBLE packet that was silently missing all of Oceania.
|
||||
const fields = 'slug,name,country_code,wagf_region,wagf_block,population,religion,image_url,primary_language,imb_gsec';
|
||||
const fields = 'slug,name,country_code,wagf_region,wagf_block,rop1,population,religion,image_url,primary_language,imb_gsec';
|
||||
const resp = await fetch(`${API_BASE}/api/people-groups/list?fields=${fields}&lang=${packetLang()}`,
|
||||
{ cache: 'no-store' });
|
||||
if (!resp.ok) throw new Error(`people-groups fetch failed (${resp.status})`);
|
||||
|
|
@ -2088,7 +2108,7 @@ function bootstrapLegend() {
|
|||
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 z.uncovered_lists || []) byId[u.id] = u.selector
|
||||
byId['deaf'] = { type: 'affinity_bloc', rop1: 'A017' }
|
||||
byId['deaf'] = DEAF_SELECTOR
|
||||
for (const z of leafZoneList) z.selector = byId[z.id]
|
||||
matchColorExpr = buildMatchColorExpr(leafZoneList)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue