From 11f0f4f5fc40e3ee82c9997a18e9653d50fe7880 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 05:10:15 +0000 Subject: [PATCH] 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. --- library/scripts/posters/packet.mjs | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/library/scripts/posters/packet.mjs b/library/scripts/posters/packet.mjs index c1a120575..b0fac7926 100644 --- a/library/scripts/posters/packet.mjs +++ b/library/scripts/posters/packet.mjs @@ -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']) {