doxa-maps/plugin-designs/gospel-resources/scripts/build-deaf-gap-table.mjs
doxa-maps-88655c70 337a1a863b Tell the curated tables that GRN and Scripture Earth exist
The legacy reconciler reported "nothing to change" and was confidently
wrong: its hasAny predicate never learned the two newest pools, so 122
curated rows — 8.9 million people — still carried heart_has_any=false
for groups the pin index can now reach. A reconciler that cannot see a
source is not reconciling, it is confirming.

Corrected exactly the 122 predicted, and the derived columns moved
with them: status 122, opportunity 93, lbc_carries_resource 69,
aligned 59, gap 29. Gap moves least because it means "nothing in the
heart language AND no in-country bridge either" — most of these have a
bridge, so they were already opportunities rather than gaps, and that
documented meaning is intact.

This was the fifth occurrence of one bug, and the gate written for it
had never looked at scripts — only .vue. Extending it to the build
scripts immediately found a sixth, in the Deaf gap table, which is
latent today because no sign language carries either field: GRN is
deliberately skipped for them and Scripture Earth has none. Fixed
while it is still cheap.

The rule now differs by file because the truth does: a surface must
READ the tier, while a build script COMPUTES it and cannot, so a
script must instead name every pool. Both halves verified against the
real pre-fix lines rather than synthetic ones.
2026-09-24 22:15:53 +00:00

120 lines
6.3 KiB
JavaScript

#!/usr/bin/env node
/**
* build-deaf-gap-table.mjs — the Deaf gap, rebuilt from the PINS.
*
* WHY IT IS BEING REBUILT. The table this replaces was derived from the curated
* 2,106-record set and reported 73 sign languages, 120 people groups and 19.35M people.
* The map draws 150 sign languages across 234 groups and 84.2M people. Two surfaces of one
* dataset describing different universes, and the smaller one was the "Deaf Gap" tab — the
* table a reader would export to decide where Deaf work is needed. It was missing more than
* half of it.
*
* MEASURED NOW: 150 sign languages · 234 groups · 84.2M people, of which 136 languages /
* 183 groups / 55.2M people have nothing found in any pool checked.
*
* THE BRIDGE RULE IS KEPT VERBATIM because it is the most important sentence here: a sign
* language is bridged ONLY by another sign language. Text and audio in a spoken language do
* not reach a Deaf community, so the "related variety" reasoning that applies to spoken
* macrolanguages must never be applied to these rows.
*
* PRIVATE-USE CODES ARE COUNTED, NOT HIDDEN. 40 of these languages carry qaa-qtz codes,
* which are private use in ISO 639-3 and are indexed by no resource pool on earth. A group
* on such a code cannot be matched to a resource even if one exists, so "nothing found" for
* them is a statement about the JOIN as much as about the world, and the row says so.
*/
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '../../..')
const PINS = resolve(REPO, 'data/tables/people-group-pins.json')
const BY_COUNTRY = resolve(REPO, 'data/tables/gospel-by-country.json')
const OUT = resolve(REPO, 'data/tables/deaf-gap.json')
const pins = JSON.parse(readFileSync(PINS, 'utf8'))
const { codes, pins: rows } = pins
let countryName = {}
try {
const c = JSON.parse(readFileSync(BY_COUNTRY, 'utf8'))
for (const r of c.rows || []) if (r.iso3) countryName[r.iso3] = r.country || r.iso3
} catch { countryName = {} }
const isPrivate = (c) => !!c && c.length === 3 && c >= 'qaa' && c <= 'qtz'
const out = []
const langs = new Set()
const nothingLangs = new Set()
let people = 0, nothingGroups = 0, nothingPeople = 0, privateCodes = new Set()
for (const [name, , , iso3, code, pop] of rows) {
const v = codes[code]
if (!(v && v.sg === 1)) continue
langs.add(code)
people += pop || 0
if (isPrivate(code)) privateCodes.add(code)
const hasFilm = !!v.f
// Every pool, including Scripture Earth and GRN. No sign language carries either today —
// GRN is deliberately skipped for them and Scripture Earth happens to have none — so this
// is latent rather than live, which is exactly when it is cheap to fix.
const hasAny = !!(v.s || v.f || v.au || v.bv || v.se || v.ga || (v.d && v.d.length))
if (!hasAny) { nothingLangs.add(code); nothingGroups++; nothingPeople += pop || 0 }
out.push({
people_group: name,
sign_language: v.l || '',
iso639_3: code || '',
countries: countryName[iso3] || iso3 || '',
population: pop || 0,
// A Deaf group is reached by SIGNED Scripture. Text is recorded because a Deaf reader
// may use it, but it is never what makes the row "reached".
signed_film: hasFilm ? v.f : '',
has_signed_film: hasFilm,
has_any_resource: hasAny,
code_unmatchable: isPrivate(code),
status: hasAny ? 'a resource exists in this sign language' : 'nothing found in the pools checked',
note: isPrivate(code)
? 'This code is PRIVATE USE in ISO 639-3 (qaa-qtz). No resource pool indexes it, so a resource could exist and still never join to this row.'
: '',
})
}
out.sort((a, b) => Number(a.has_any_resource) - Number(b.has_any_resource) || b.population - a.population)
const fmt = (n) => n.toLocaleString('en-US')
// DataTable renders a { meta, columns, rows } document and `columns` IS the Tabulator
// definition — omit it and the table draws its header, its row count and its pagination
// over an empty body, which looks like a data problem and is not one.
const columns = [
{ title: 'People Group', field: 'people_group', type: 'string' },
{ title: 'Sign language', field: 'sign_language', type: 'string' },
{ title: 'ISO 639-3', field: 'iso639_3', type: 'string' },
{ title: 'Countries', field: 'countries', type: 'string' },
{ title: 'Population', field: 'population', type: 'number' },
{ title: 'Signed film', field: 'has_signed_film', type: 'boolean' },
{ title: 'Signed film link', field: 'signed_film', type: 'url' },
{ title: 'Has anything', field: 'has_any_resource', type: 'boolean' },
{ title: 'Code unlookupable', field: 'code_unmatchable', type: 'boolean' },
{ title: 'Status', field: 'status', type: 'string' },
{ title: 'Note', field: 'note', type: 'string' },
]
mkdirSync(dirname(OUT), { recursive: true })
writeFileSync(OUT, `${JSON.stringify({
columns,
meta: {
title: 'The Deaf Gap',
grain: 'one row per Deaf people group',
generated: new Date().toISOString().slice(0, 10),
measured: `${langs.size} sign languages · ${out.length} people groups · ${fmt(people)} people. ` +
`${nothingLangs.size} languages / ${nothingGroups} groups / ${fmt(nothingPeople)} people with NOTHING found.`,
bridge_rule: 'A sign language is bridged ONLY by another sign language. Text and audio in a spoken language do not reach a Deaf community.',
compounding_problem: `${privateCodes.size} of these languages carry PRIVATE-USE (qaa-qtz) codes outside ISO 639-3. No resource pool indexes a private-use code, so those groups cannot be matched to a resource even if one exists.`,
what_is_unchecked: 'Deaf Bible Society and DOOR International, both key-gated. The open catalogues are exhausted: the JESUS Film lists 14 sign-language entries across 13 ISO codes and every one is already here, and Faith Comes By Hearing carries none.',
source: 'data/tables/people-group-pins.json — the same index the map draws, so the two cannot disagree.',
},
rows: out,
}, null, 1)}\n`)
console.log(`deaf-gap: ${out.length} groups · ${langs.size} sign languages · ${fmt(people)} people`)
console.log(` nothing found: ${nothingLangs.size} languages / ${nothingGroups} groups / ${fmt(nothingPeople)} people`)
console.log(` private-use codes: ${privateCodes.size}`)
console.log(`wrote ${OUT}`)