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.
99 lines
7.3 KiB
JavaScript
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.')
|