diff --git a/internal/check-batch-selection-export.mjs b/internal/check-batch-selection-export.mjs index 42ab25233..6d154b09f 100644 --- a/internal/check-batch-selection-export.mjs +++ b/internal/check-batch-selection-export.mjs @@ -25,10 +25,11 @@ const fails = [] const notes = [] const ok = (cond, msg, note) => { if (!cond) fails.push(msg); else if (note) notes.push(` ok ${note}`) } -let X, parsePayload +let X, parsePayload, runApply, formatClipboard try { X = await import(pathToFileURL(P('plugin-designs/my-upg-100-list/data/selection-export.js')).href) - ;({ parsePayload } = await import(pathToFileURL(P('plugin-designs/my-upg-100-list/data/apply-grouping.mjs')).href)) + ;({ parsePayload, runApply } = await import(pathToFileURL(P('plugin-designs/my-upg-100-list/data/apply-grouping.mjs')).href)) + ;({ formatClipboard } = await import(pathToFileURL(P('plugin-designs/my-upg-100-list/data/territory-counts.js')).href)) } catch (e) { console.error(`BATCH-SELECTION-EXPORT: NOT MEASURED (exit 75) — an input did not load: ${e.message}`) process.exit(75) @@ -54,16 +55,37 @@ const text = X.selectionClipboard(selection, { color }, csv) let parsed = null try { parsed = parsePayload(text) } catch (e) { fails.push(`apply-grouping's parsePayload REFUSED the Copy text: ${e.message}`) } if (parsed) { - ok(JSON.stringify(parsed.names) === JSON.stringify(selection.map((s) => s.name)), - `apply-grouping reads the names as ${JSON.stringify(parsed.names)}, expected ${JSON.stringify(selection.map((s) => s.name))}`, - 'Copy\'s first line parses in apply-grouping: the 3 territory names, in selection order') + const machine = ['Mahārāshtra', 'Goa', 'NPL'] + ok(JSON.stringify(parsed.names) === JSON.stringify(machine), + `apply-grouping reads the names as ${JSON.stringify(parsed.names)}, expected ${JSON.stringify(machine)}`, + 'Copy\'s first line parses in apply-grouping: the 3 territories in machine names (a country as its ISO3), in order') ok(parsed.total === 93, `apply-grouping reads the total as ${parsed.total}, expected 93`, 'the UUPG total (93) and the colour survive the CSV after them') ok(parsed.hex === color, `apply-grouping reads the colour as ${parsed.hex}, expected ${color}`) } -ok(text.split('\n')[0] === 'Mahārāshtra, Goa, Nepal — 93 UUPG · #e69f00', `Copy's first line is "${text.split('\n')[0]}"`) +ok(text.split('\n')[0] === 'Mahārāshtra, Goa, NPL — 93 UUPG · #e69f00', `Copy's first line is "${text.split('\n')[0]}"`) +ok(text.split('\n')[1] === 'Mahārāshtra 54, Goa 5, Nepal 34', `Copy's second line (the reader's names) is "${text.split('\n')[1]}"`) ok(text.includes('\n\n' + csv), 'Copy does not carry the CSV after a blank line') ok(X.selectionClipboard(selection, {}, '').split('\n').length === 2, 'Copy without people groups is not exactly the two territory lines') +// ── A2. apply-grouping RESOLVES line 1 in ANY UI language ─────────────────────────────────────────────── +// parsePayload only splits the names; resolution happens later (upsertPayloadZone), so this runs the tool's own +// dry run. In Arabic the HUD shows "نيبال", and a line 1 carrying it was rejected ("unresolved territory name(s)"). +// The NEGATIVE CONTROL proves this check can fail: the same selection formatted with the reader's names must be +// refused by the same dry run. +const arabic = [ + { key: 'adm0:NPL', name: 'نيبال', count: 34, kind: 'adm0' }, + { key: 'adm0:BTN', name: 'بوتان', count: 13, kind: 'adm0' }, +] +const dry = (payloadText) => { try { const r = runApply({ payloadText, dry: true, quiet: true }); return r && r.ok !== false ? 'ok' : 'not ok' } catch (e) { return 'threw: ' + String(e.message || e).slice(0, 120) } } +const arText = X.selectionClipboard(arabic, { color: '#0072b2' }) +ok(dry(arText) === 'ok', `apply-grouping's dry run REFUSES the Arabic Copy: ${dry(arText)} (line 1 "${arText.split('\n')[0]}")`, + 'an ARABIC selection\'s Copy resolves in apply-grouping\'s own dry run (line 1 "NPL, BTN — 47 UUPG · #0072b2")') +ok(arText.split('\n')[1] === 'نيبال 34, بوتان 13', `the Arabic Copy's second line is "${arText.split('\n')[1]}", expected the reader's names`) +ok(dry(formatClipboard(arabic, { color: '#0072b2' })) !== 'ok', 'NEGATIVE CONTROL: a line 1 in the reader\'s Arabic names should be refused by the dry run, and was not: this check cannot fail', + 'negative control: the same selection in the reader\'s names IS refused (the check can fail)') +const indiaText = X.selectionClipboard([{ key: 'adm1:Mahārāshtra', name: 'Mahārāshtra', count: 54, kind: 'adm1' }, { key: 'adm1:Goa', name: 'Goa', count: 5, kind: 'adm1' }], {}) +ok(dry(indiaText) === 'ok', `apply-grouping's dry run refuses an India state selection: ${dry(indiaText)}`, 'an India state selection resolves too (shape names are canonical)') + // ── B. the CSV ─────────────────────────────────────────────────────────────────────────────────── function parseCsv(s) { // RFC 4180: quoted fields, doubled quotes, CRLF records const rows = []; let row = [], cell = '', q = false diff --git a/plugin-designs/my-upg-100-list/data/selection-export.js b/plugin-designs/my-upg-100-list/data/selection-export.js index 212248cd4..c163b66f9 100644 --- a/plugin-designs/my-upg-100-list/data/selection-export.js +++ b/plugin-designs/my-upg-100-list/data/selection-export.js @@ -6,9 +6,9 @@ // the posters. Without the export the maps cannot be reproduced. // // THE CLIPBOARD TEXT, in this order: -// line 1 "Name1, Name2, … — {total} UUPG · #hex" formatClipboard's line: data/apply-grouping.mjs parses -// exactly this (names, total, colour), so it MUST stay first -// line 2 "Name1 c1, Name2 c2, …" the per-territory breakdown +// line 1 "NPL, BTN, … — {total} UUPG · #hex" formatClipboard's line in MACHINE names (payloadName): +// data/apply-grouping.mjs parses AND resolves it, so it MUST stay first +// line 2 "Nepal 34, Bhutan 13, …" the per-territory breakdown, in the reader's names // blank line, then the SAME CSV that Export CSV downloads (when the people groups could be detected). // // THE CSV: one row per people group, header first, CRLF line ends. The cells follow the library's rules @@ -40,9 +40,23 @@ export function selectionCsv(records, territoriesBySlug = new Map(), opts = {}) return [SELECTION_CSV_COLUMNS.join(','), ...rows].join('\r\n'); } -/** The clipboard text: apply-grouping's two lines, then (when present) the CSV after a blank line. */ +// ★ LINE 1 IS A MACHINE CONTRACT: data/apply-grouping.mjs resolves every name in it, so it carries the names that +// tool resolves in EVERY UI language, never the reader's translation (an Arabic "نيبال, بوتان" was rejected: +// "unresolved territory name(s)"). A country is its ISO3 code, the only country form apply-grouping resolves for +// every country (its friendly-name table has ~15); a state or province is its shape name, already canonical; a +// zone keeps its name. The reader's own names stay on line 2, in the HUD and in the CSV. +export function payloadName(t) { + if (t && t.kind === 'adm0') return String(t.key || '').replace(/^adm0:/, '') || t.name; + return t ? t.name : ''; +} + +/** The clipboard text: apply-grouping's line (machine names), the reader's breakdown, then the CSV after a blank line. */ export function selectionClipboard(selection, opts = {}, csv = '') { - const head = formatClipboard(selection, { color: opts && opts.color ? opts.color : '' }); + const color = opts && opts.color ? opts.color : ''; + const sel = Array.isArray(selection) ? selection : []; + const line1 = formatClipboard(sel.map((t) => ({ ...t, name: payloadName(t) })), { color }).split('\n')[0]; + const line2 = formatClipboard(sel, { color }).split('\n')[1]; + const head = line1 + '\n' + line2; return csv ? head + '\n\n' + csv : head; } @@ -53,4 +67,4 @@ export function selectionCsvFileName(label) { return `upg-100-${base}.csv`; } -export default { SELECTION_CSV_COLUMNS, csvCell, selectionCsv, selectionClipboard, selectionCsvFileName }; +export default { SELECTION_CSV_COLUMNS, csvCell, payloadName, selectionCsv, selectionClipboard, selectionCsvFileName };