doxa-maps/internal/check-selection-kinds.mjs
Doxa Maps 35dfd82b83 Drop quotes and transcript references from the packet comments and gates
Per the 17:25Z rule: no quotes, no citations and no transcript references
in strings, commits or code. Studio had to strip these from two of my
commit messages during L64, which is a sign I should be doing it at the
source rather than leaving it to whoever lands the work.

Nine sites across five files, all of them lines I added. The engineering
content is unchanged -- what goes is the attribution, not the reasoning.
"A disabled button is visually indistinguishable from an unresponsive one"
is the durable fact; who observed it is provenance a later reader cannot
verify and that ties shipped code to a private transcript.

Where a decision is genuinely still open I keep the tracking reference
("still open, tracked as row 89") but not the speaker, because a reader
hitting that comment needs to know the value may change and where the
answer will come from.

Scope is deliberately my own unlanded additions. packet-render.js,
PosterWall.vue and upg-100-list.vue carry many more such comments from
before the rule; those are not mine and a fleet-wide sweep is a separate
decision. Flagged rather than silently rewritten.

All seven gates green after the edit, including the two whose own header
text changed.
2026-09-28 17:53:38 +00:00

99 lines
7.3 KiB
JavaScript

#!/usr/bin/env node
/**
* check-selection-kinds — a territory selected on the MAP must reach the packet.
*
* WHY THIS EXISTS: selecting countries on the map left the HUD showing a correct non-zero UUPG
* total while Prayer list reported zero and refused to generate. Reported from mobile, 2026-09-28.
* `selectedTerritories` holds THREE kinds — 'zone' (a legend row), 'adm0' (a country, key
* 'adm0:<ISO3>') and 'adm1' (an India state / China province, key 'adm1:<name>') — and
* `selectedLists()` kept only 'zone', so a map selection resolved to an empty list.
*
* ★ THE SHAPE WORTH PINNING IS THE DISAGREEMENT, not the zero. The HUD sums `t.count` straight off
* the selection while the packet path goes through `selectedLists()`. TWO SOURCES OF TRUTH FOR ONE
* SELECTION is why this read as a nonsense error rather than an empty state: the app said 87 and the
* generator said 0. A gate that only asserted "a country selection yields slugs" would miss the
* class; this one asserts every kind the resolver can EMIT is handled by the consumer.
*
* Headless, no network, no DOM. Exit 0 pass · 1 fail.
*/
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { readFileSync } from 'node:fs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const read = (p) => readFileSync(resolve(ROOT, p), 'utf8')
const { evaluate } = await import(resolve(ROOT, 'plugin-designs/my-upg-100-list/data/selector-evaluator.js'))
const VUE = read('plugin-designs/my-upg-100-list/upg-100-list.vue')
const TERR = read('plugin-designs/my-upg-100-list/data/territory-counts.js')
const fails = []
const ok = (c, m) => { if (!c) fails.push(m) }
// ── 1. EVERY KIND THE RESOLVER EMITS IS HANDLED DOWNSTREAM ─────────────────────────────────────
// ★ Read the kinds OUT OF resolveTerritory rather than listing them here. Listing them is how this
// bug survived: 'adm0' existed in the resolver and nowhere in the consumer, and no test noticed
// because the test would have been written from the same mental list as the consumer.
const kinds = [...new Set([...TERR.matchAll(/kind:\s*'([a-z0-9]+)'/g)].map((m) => m[1]))]
ok(kinds.includes('adm0') && kinds.includes('adm1'),
`resolveTerritory no longer emits the kinds this gate knows about (found: ${kinds.join(', ')}) — re-read it before trusting the rest of this file`)
for (const k of kinds) {
// Each kind must be named SOMEWHERE in the consumer — either resolved or explicitly refused.
ok(new RegExp(`'${k}'`).test(VUE),
`resolveTerritory emits kind '${k}' but upg-100-list.vue never mentions it — a territory of that kind will be silently dropped from the selection, which is exactly the "zero UPG" defect`)
}
// ── 2. A COUNTRY SELECTION RESOLVES TO ITS PEOPLE GROUPS ───────────────────────────────────────
// The adm0 key carries the ISO3, which IS `country_code` on a people-group row.
const rows = [
{ slug: 'ind-a', country_code: 'IND' },
{ slug: 'ind-b', country_code: 'IND' },
{ slug: 'npl-a', country_code: 'NPL' },
{ slug: 'mys-a', country_code: 'MYS' },
]
const codesFromKeys = (keys) => keys.filter((k) => k.startsWith('adm0:')).map((k) => k.slice(5))
const got = evaluate({ type: 'country_codes', values: codesFromKeys(['adm0:IND', 'adm0:NPL']) }, rows)
ok(got.size === 3, `two tapped countries resolved to ${got.size} people groups, expected 3`)
ok(!got.has('mys-a'), 'the selection leaked a group from a country that was not selected')
// A single country must work too — the common case on a phone, and the one that was reported.
ok(evaluate({ type: 'country_codes', values: ['IND'] }, rows).size === 2,
'one tapped country did not resolve to its two people groups')
// ── 3. THE CONSUMER ACTUALLY WIRES IT ──────────────────────────────────────────────────────────
// The evaluator supporting country_codes proves nothing if selectionAsList never calls it.
ok(/selectedCountryCodes\s*\(/.test(VUE), 'selectedCountryCodes() is gone — map-tapped countries are unresolved again')
ok(/type:\s*'country_codes'/.test(VUE),
'selectionAsList no longer builds a country_codes selector — a tapped country will resolve to nothing')
// ⚠ AND IT MUST NOT THROW ON A COUNTRY-ONLY SELECTION. The old guard was `if (!lists.length) throw`,
// which is TRUE for every country selection. The zero came from the guard, not from the data.
ok(/!lists\.length\s*&&\s*!codes\.length/.test(VUE),
'the "nothing selected" guard tests lists alone again — a country-only selection will throw "nothing selected" while the reader looks at their own highlighted selection')
// ── 4. ADM1 IS REFUSED BY NAME, NOT ABSORBED ───────────────────────────────────────────────────
// ★ A people-group row has country_code and NO province field, so an India state cannot resolve —
// the evaluator has no adm1 case at all. Folding it into the country union would print a packet for
// the whole COUNTRY while the reader selected one STATE: a plausible packet that is quietly wrong,
// which is worse than a refusal. So it must be named in the error.
ok(/selectedAdm1Names\s*\(/.test(VUE), 'adm1 selections are no longer distinguished — they will be silently dropped or silently widened')
ok(/no people-group data for/.test(VUE),
'the adm1 refusal no longer names the territory — "nothing selected" is a lie when the reader can see their selection on the map')
const adm1Cases = (read('plugin-designs/my-upg-100-list/data/selector-evaluator.js').match(/case 'adm1/g) || []).length
ok(adm1Cases === 0,
'the evaluator now HAS an adm1 case — if provinces can resolve, stop refusing them and wire them in instead')
// ── 5. THE BUSY STATE SAYS WHICH ACTION IS RUNNING ─────────────────────────────────────────────
// A slow print gave no feedback at all. A disabled button is visually indistinguishable
// from an unresponsive one, so the label has to change — and it must be CLEARED in `finally`, or an
// error leaves the button reading "Working…" forever.
ok(/busyAction/.test(VUE), 'the busy indicator is gone — a slow packet build will look like a dead button again')
const finallys = (VUE.match(/finally\s*\{[^}]*busyAction\.value\s*=\s*''/g) || []).length
const setters = (VUE.match(/busyAction\.value\s*=\s*'(packet|poster)'/g) || []).length
ok(finallys >= setters && setters >= 2,
`busyAction is set ${setters} times but cleared in only ${finallys} finally blocks — a failed print would leave the button stuck on "Working…"`)
if (fails.length) {
console.error('SELECTION-KINDS: FAIL')
for (const f of fails) console.error(` ✗ ${f}`)
process.exit(1)
}
console.log('SELECTION-KINDS: PASS — every kind resolveTerritory emits is handled by the consumer, a tapped country resolves to exactly its own people groups, the guard no longer throws on a country-only selection, a province is refused BY NAME rather than silently widened to its country, and the busy label is cleared on every exit path.')