doxa-maps/library/scripts/research/verify-resource-join.mjs
doxa-maps d5ba21fa76 doxa-maps — portable bundler for embeddable JAMstack maps
Standalone repo that builds embeddable JAMstack map bundles (web components),
composes them into a lazy-loaded SPA via layouts/, and optionally wraps a
Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs.

History reset to a single clean commit; no PII, IPs, or internal terms.
2026-09-10 01:53:07 +00:00

71 lines
4.1 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.

// verify-resource-join.mjs — prove the gospel-resource join against the RAW pools.
//
// node verify-resource-join.mjs # exits 1 on any failure
//
// ═══ WHY THIS EXISTS ════════════════════════════════════════════════════════
// `no_resource_found` is the most consequential claim in this corpus: it is what
// says a people group has NOTHING in their language. A false positive sends effort
// toward a group that already has a resource; a false negative hides a group that
// has nothing. Neither is visible by reading the table.
//
// Checking the table against language-resources.json only proves the table is
// FAITHFUL TO ITS OWN INPUT. It cannot catch an error in that input. So this checks
// BOTH DIRECTIONS against the RAW pool caches:
// * has_x=true but the ISO is NOT in the raw pool -> false positive
// * has_x=false but the ISO IS in the raw pool -> false negative (the worse one)
//
// TWO PARSING TRAPS, both of which I hit and which made a hand-check report 306
// fake mismatches before I read the schemas:
// * arclight-languages.json is a PAGINATED ENVELOPE — the records are at
// _embedded.mediaLanguages, not the top level. Reading the top level yields 0.
// * ebible-translations.csv carries a UTF-8 BOM, so row['languageCode'] is
// undefined unless the file is read as utf-8-sig / the BOM is stripped.
// A cross-check that silently parses 0 records "passes" everything, which is the
// same fail-open shape this repo keeps catching. Hence the guard asserts the raw
// pools are NON-EMPTY and near their known sizes before comparing anything.
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const OUT = join(__dirname, 'out');
let failed = 0;
const fail = (m) => { console.error(` ✗ ${m}`); failed++; };
const ok = (m) => console.log(` ✓ ${m}`);
const arc = JSON.parse(readFileSync(join(OUT, 'arclight-languages.json'), 'utf8'));
const arcIso = new Set((arc?._embedded?.mediaLanguages || []).map((r) => r.iso3).filter(Boolean));
const csv = readFileSync(join(OUT, 'ebible-translations.csv'), 'utf8').replace(/^/, '');
const [head, ...lines] = csv.split(/\r?\n/);
const col = head.split(',').findIndex((h) => h.replace(/"/g, '') === 'languageCode');
const ebIso = new Set(lines.map((l) => (l.split(',')[col] || '').replace(/"/g, '').trim()).filter(Boolean));
console.log('GOSPEL-RESOURCE JOIN VERIFICATION (against the RAW pools)');
// FAIL-CLOSED on an empty parse — an empty pool would clear every comparison below.
if (arcIso.size < 1500) fail(`arclight parsed only ${arcIso.size} ISO codes — expected ~2085. Parser broken, comparisons below are meaningless.`);
else ok(`arclight raw: ${arcIso.size} ISO codes`);
if (ebIso.size < 900) fail(`ebible parsed only ${ebIso.size} ISO codes — expected ~1241. Parser broken.`);
else ok(`ebible raw: ${ebIso.size} ISO codes`);
if (failed) { console.log('\nFAILED — raw pools did not parse; not comparing.'); process.exit(1); }
const langs = JSON.parse(readFileSync(join(OUT, 'language-resources.json'), 'utf8')).languages;
const fpFilm = langs.filter((l) => l.has_film && !arcIso.has(l.iso639_3));
const fnFilm = langs.filter((l) => !l.has_film && arcIso.has(l.iso639_3));
const fpScr = langs.filter((l) => l.has_scripture && !ebIso.has(l.iso639_3));
const fnScr = langs.filter((l) => !l.has_scripture && ebIso.has(l.iso639_3));
const rep = (label, arr) => arr.length
? fail(`${arr.length} ${label}: e.g. ${arr.slice(0, 5).map((l) => l.iso639_3).join(', ')}`)
: ok(`0 ${label}`);
rep('FALSE POSITIVE has_film (claims film, absent from raw pool)', fpFilm);
rep('FALSE NEGATIVE has_film (in raw pool, claims none) — the dangerous direction', fnFilm);
rep('FALSE POSITIVE has_scripture', fpScr);
rep('FALSE NEGATIVE has_scripture — the dangerous direction', fnScr);
console.log(`\n${failed ? `FAILED — ${failed} problem(s)` : 'JOIN VALID — both directions agree with the raw pools'}`);
process.exit(failed ? 1 : 0);