doxa-maps/internal/check-vision-card-numbers.mjs
Doxa Maps 28a0083310
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source a59db6c4a
2026-09-28 08:59:18 +00:00

69 lines
4.6 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* check-vision-card-numbers — the DOXA Vision Card shows the numbers the DATA gives, nothing typed.
*
* USAGE: node internal/check-vision-card-numbers.mjs (data only, no browser, ~2 s)
* node internal/check-vision-card-numbers.mjs --url <card> (also read the RENDERED card)
* Exit 0 = every number matches · 1 = a mismatch.
*
* INDEPENDENT RECOMPUTATION. It does not import build-vision-funnel.mjs: it re-reads the raw CSV with
* its own column lookup and re-applies the published rules (HOW-THE-NUMBERS-ARE-COUNTED.md) to the
* saved DOXA list, then compares with (1) data/people-groups/vision-funnel.json, which the card
* compiles in, and (2) with --url, the numbers actually painted in the card's bars. A typed-in
* number, a stale funnel file or a rendering bug each makes it fail.
*/
import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const text = readFileSync(resolve(ROOT, 'data/people-groups/people_groups.csv'), 'utf8')
// Own CSV reader: split records on newlines outside quotes, fields on commas outside quotes.
const records = []; { let cur = ''; let q = false; for (const ch of text) { if (ch === '"') q = !q; if (ch === '\n' && !q) { records.push(cur); cur = '' } else cur += ch } if (cur.trim()) records.push(cur) }
const split = (line) => { const out = []; let f = ''; let q = false; for (let i = 0; i < line.length; i++) { const c = line[i]; if (c === '"') { if (q && line[i + 1] === '"') { f += '"'; i++ } else q = !q } else if (c === ',' && !q) { out.push(f); f = '' } else if (c !== '\r') f += c } out.push(f); return out }
const head = split(records[0]).map((h) => h.replace(/^/, ''))
const col = (n) => head.indexOf(n)
const L = col('LostnessPriority'), R3 = col('ROP3Code'), ISO = col('ISOAlpha3')
const data = records.slice(1).map(split).filter((f) => f.length === head.length)
const expect = {
all: data.length,
unreached: data.filter((f) => ['0', '1', '2'].includes(f[L])).length,
frontier: data.filter((f) => f[L] === '0').length,
}
const snap = JSON.parse(readFileSync(resolve(ROOT, 'data/people-groups/doxa-groups-snapshot.json'), 'utf8'))
const lost = new Map(data.map((f) => [f[R3] + '|' + f[ISO], f[L]]))
expect.doxa = snap.groups.length
expect.doxaFrontier = snap.groups.filter((g) => lost.get(g.rop3 + '|' + g.country) === '0').length
const funnel = JSON.parse(readFileSync(resolve(ROOT, 'data/people-groups/vision-funnel.json'), 'utf8'))
const got = Object.fromEntries(funnel.steps.map((s) => [s.id, s.groups]))
got.doxaFrontier = funnel.steps.find((s) => s.id === 'doxa').frontier
const bad = []
for (const k of Object.keys(expect)) {
const ok = expect[k] === got[k]
console.log((ok ? 'ok ' : 'FAIL ') + k.padEnd(13) + ' recomputed ' + String(expect[k]).padStart(6) + ' vision-funnel.json ' + got[k])
if (!ok) bad.push(k)
}
const i = process.argv.indexOf('--url')
if (i > -1) {
// Measure THIS build, or measure nothing (exit 75 = NOT MEASURED; see a9550858 / _serves-this-tree.mjs).
const { requireThisTree } = await import('./_serves-this-tree.mjs')
await requireThisTree(process.argv[i + 1], ROOT, 'VISION-CARD-NUMBERS')
const { chromium } = await import('./lib/playwright-guarded.mjs')
const b = await chromium.launch()
const p = await b.newPage({ viewport: { width: 900, height: 1000 } })
await p.goto(process.argv[i + 1], { waitUntil: 'networkidle' })
await p.waitForTimeout(1500)
const shown = await p.evaluate(() => { const el = document.querySelector('doxa-vision-card'); const r = el && el.shadowRoot; return r ? [...r.querySelectorAll('.vc-n')].map((n) => n.textContent.trim()) : [] })
const note = await p.evaluate(() => { const r = document.querySelector('doxa-vision-card')?.shadowRoot; return r ? r.querySelector('.vc-step-doxa .vc-note')?.textContent || '' : '' })
await b.close()
const want = [expect.all, expect.unreached, expect.frontier, expect.doxa].map((n) => new Intl.NumberFormat('en').format(n))
const okBars = JSON.stringify(shown) === JSON.stringify(want)
console.log((okBars ? 'ok ' : 'FAIL ') + 'rendered bars ' + JSON.stringify(shown) + (okBars ? '' : ' expected ' + JSON.stringify(want)))
const okNote = note.includes(new Intl.NumberFormat('en').format(expect.doxaFrontier))
console.log((okNote ? 'ok ' : 'FAIL ') + 'rendered DOXA note "' + note.trim() + '"')
if (!okBars) bad.push('rendered bars'); if (!okNote) bad.push('rendered note')
}
console.log(bad.length ? 'VISION-CARD-NUMBERS: FAIL — ' + bad.join(', ') : 'VISION-CARD-NUMBERS: PASS — every number matches the data')
process.exit(bad.length ? 1 : 0)