packet: country mode needs the country zone source, not just the wording

exp-shepherd measured on main: SERIES=country ONLY=country-yem made a POSTER,
and the matching packet.mjs run died with 'no zone matches ONLY=country-yem'.
The packet half of SERIES=country could not run for any country.

Cause: allZones() built zones from clusters.json -- the UPG-100 zoning -- where a
country id never appears. generate.mjs got its country zone source from
countryMode in the previous commit; packet.mjs did not. The same 'one concept,
two switches' defect I had just removed from one file survived in the other.

MY OWN TEST MISSED IT BECAUSE IT WAS NOT END TO END. I ran the country packet
against a UPG-100 zone id (ONLY=west-africa), which exercised the WORDING and
never the zone source, then reported the parameter verified end to end. Running
the actual pairing is what found it, and exp-shepherd did that, not me.

allZones() now returns the country zones under countryMode, built to match
generate.mjs's country branch exactly -- same ids, labels, adm0 and the PLURAL
country_codes selector shape -- so a poster and its packet cannot describe
different zones. Same two-path country-list lookup and the same error naming the
command that writes the file.

Verified: country-yem builds 8 groups / 6 pages, SERIES-ISOLATION PASS with 9
country markers and zero UPG-100 framing; the UPG-100 default is unchanged at 12
pages and 13 markers; all four packet gates PASS.
This commit is contained in:
Doxa Maps 2026-09-28 05:10:15 +00:00
parent 11129455e4
commit 11f0f4f5fc

View file

@ -289,6 +289,43 @@ async function loadRecords() {
// leafZones() carries id/zoneKey/label/count but NOT selector (leaves do not), so the selector is
// re-attached by id exactly as generate.mjs:2801-2808 does it, deaf included.
function allZones() {
// ★ COUNTRY MODE HAS ITS OWN ZONE SOURCE, and leaving it out broke the packet half of
// SERIES=country for every country. Measured by exp-shepherd on main, 2026-09-28:
// `SERIES=country ONLY=country-yem node generate.mjs` made a poster, and the matching
// packet.mjs run died with "no zone matches ONLY=country-yem" — because zones came from
// clusters.json (the UPG-100 zoning), where a country id never appears.
// ⚠ MY OWN TEST MISSED IT BECAUSE IT WAS NOT END TO END. I only ever ran the country packet
// against a UPG-100 zone id (ONLY=west-africa), which exercised the WORDING and never the zone
// source. The same "one concept, two switches" defect I had just removed from generate.mjs
// survived here, in the other file.
// Built to MATCH generate.mjs's country branch exactly (same ids, labels and selector shape), so
// a poster and its packet cannot describe different zones.
if (SERIES.countryMode) {
const cpath = [
join(__dirname, '..', '..', 'data', 'country-posters.json'),
join(REPO, 'plugin-designs', 'country-posters', 'data', 'countries.json'),
].find((p) => existsSync(p));
if (!cpath) {
console.error('[packet] COUNTRY MODE needs the country list, and neither location has it:');
console.error(' library/data/country-posters.json (written by: node emit-countries.mjs)');
process.exit(1);
}
const cdata = JSON.parse(readFileSync(cpath, 'utf8'));
const want = process.env.SHEET || '3x3';
return (cdata.countries || [])
.filter((c) => c.sheet === want)
.map((c) => ({
id: c.id,
zoneKey: c.id,
label: c.label,
count: c.count,
adm0: [c.iso3],
// PLURAL type + `values`. The evaluator's SINGULAR form is
// { type:'country_code', value:'SDN' }; passing `values` to the singular type matches
// nothing and yields a silently empty packet. Same note as generate.mjs:2841.
selector: { type: 'country_codes', values: [c.iso3] },
}));
}
const tops = buildLegendNodes(clusters, { records: null });
const byId = {};
for (const k of ['lists', 'asia_lists', 'uncovered_lists']) {