merge main (gospel) into packet/vertical
This commit is contained in:
commit
9df086e5f0
27 changed files with 25169 additions and 8865 deletions
File diff suppressed because one or more lines are too long
|
|
@ -59,7 +59,7 @@
|
|||
"meta": {
|
||||
"title": "The Deaf Gap",
|
||||
"grain": "one row per Deaf people group",
|
||||
"generated": "2026-09-25",
|
||||
"generated": "2026-09-27",
|
||||
"measured": "150 sign languages · 234 people groups · 84,159,050 people. 136 languages / 183 groups / 55,199,840 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": "40 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.",
|
||||
|
|
@ -2621,7 +2621,7 @@
|
|||
"iso639_3": "jos",
|
||||
"countries": "SYR",
|
||||
"population": 221000,
|
||||
"signed_film": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"signed_film": "https://arc.gt/g8wmw",
|
||||
"has_signed_film": true,
|
||||
"has_any_resource": true,
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -2660,7 +2660,7 @@
|
|||
"iso639_3": "jos",
|
||||
"countries": "JOR",
|
||||
"population": 116000,
|
||||
"signed_film": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"signed_film": "https://arc.gt/g8wmw",
|
||||
"has_signed_film": true,
|
||||
"has_any_resource": true,
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -2738,7 +2738,7 @@
|
|||
"iso639_3": "jos",
|
||||
"countries": "LBN",
|
||||
"population": 51000,
|
||||
"signed_film": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"signed_film": "https://arc.gt/g8wmw",
|
||||
"has_signed_film": true,
|
||||
"has_any_resource": true,
|
||||
"code_unmatchable": false,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"title": "Doxa people groups — what the gospel is available in, and what is missing",
|
||||
"grain": "one row per people group on Doxa's roster",
|
||||
"source": "pray.doxa.life roster joined to the resource index by ISO 639-3",
|
||||
"generated": "2026-09-26",
|
||||
"generated": "2026-09-27",
|
||||
"groups": 2107,
|
||||
"by_access": {
|
||||
"nothing found": 369,
|
||||
|
|
@ -19768,7 +19768,7 @@
|
|||
"translated_by": "",
|
||||
"translated_year": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"film_url": "https://arc.gt/g8wmw",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
|
|
@ -20048,7 +20048,7 @@
|
|||
"translated_by": "",
|
||||
"translated_year": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"film_url": "https://arc.gt/g8wmw",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
|
|
@ -21928,7 +21928,7 @@
|
|||
"translated_by": "",
|
||||
"translated_year": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"film_url": "https://arc.gt/g8wmw",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"bridge_strength_note": "MEASURED: 24% of aligned rows rest on a bridge shared by only 2-3 groups. bridge_strength grades this (strong >=8 groups, moderate >=4, weak <4). A WEAK bridge is a hypothesis, not a route — e.g. \"Gawri\" was offered as the bridge for 10.3M Pakistanis where the real lingua franca is Urdu, which no unengaged group here claims as primary and which this method therefore cannot see. Filter on bridge_strength before acting.",
|
||||
"bridge_caveat": "A bridge candidate is measured from THIS corpus (a language primary to >1 DX group in the country). It is NOT proof the group speaks it, and it is NOT a national trade language — a lingua franca no unengaged group claims as primary is invisible here.",
|
||||
"pools_caveat": "Resource status is from eBible + JESUS Film. GRN and FCBH audio are key-blocked and unchecked — absence is never proof.",
|
||||
"resource_flags_refreshed": "2026-09-25",
|
||||
"resource_flags_refreshed": "2026-09-27",
|
||||
"resource_flags_source": "data/tables/people-group-pins.json — the same index the map draws. Curated bridge, internet and channel columns are untouched."
|
||||
},
|
||||
"columns": [
|
||||
|
|
|
|||
10095
data/tables/gospel-by-province-groups.json
Normal file
10095
data/tables/gospel-by-province-groups.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,11 +2,12 @@
|
|||
"meta": {
|
||||
"title": "Gospel access by province — India and China",
|
||||
"why": "These two countries hold 47.9% of Doxa's roster (855 + 154 of 2,107). No other country is close; the third is Sudan at 105. A national fill cannot answer \"where would a team go\" for half the map.",
|
||||
"generated": "2026-09-26",
|
||||
"generated": "2026-09-27",
|
||||
"provinces_with_groups": 46,
|
||||
"provinces_available": 48,
|
||||
"groups_assigned": 1005,
|
||||
"groups_unassigned": 4,
|
||||
"per_group_file": "gospel-by-province-groups.json — one row per placed group, reconciled with these counts at build time",
|
||||
"india_note": "India is read from the COMPLETE ADM1 (36 states, geoBoundaries via DataMeet India, CC BY 2.5 IN). The first build of this table read gb-IND-ADM1.geojson, which is the UPG-100 list's curated 34-unit partition, and eleven groups came out with no province because they stood in states that partition does not draw. Switching the file moved 998 assigned to 1,005 and 44 province rows to 46, and turned the province names into India's own states.",
|
||||
"unassigned_note": "Four remain, each one legible rather than lost. Great Andamanese and the Sentinelese sit on small islands whose simplified coastline the point falls just outside — Andaman and Nicobar IS in the file, so this is geometry precision, not a missing province. Jing and Pana are on China's Vietnam and Laos borders, outside the twelve-province subset. They are listed in `unassigned` with their coordinates.",
|
||||
"china_note": "China's ADM1 file carries 12 provinces, not 31+. Measured rather than assumed: 152 of 154 Chinese groups fall inside them and all twelve are used, so the file is cut to the provinces where the unreached peoples are.",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"meta": {
|
||||
"title": "Gospel resource link verification",
|
||||
"generated": "2026-09-25T09:56:59.860Z",
|
||||
"generated": "2026-09-27T01:21:18.374Z",
|
||||
"method": "HEAD, redirects followed. verified = HTTP 200. broken = 404/410. unknown = unreachable, an ambiguous status, or a URL shape that cannot be checked by fetch.",
|
||||
"scope": "Every URL in data/tables/people-group-pins.json, deduplicated.",
|
||||
"states": {
|
||||
|
|
@ -96,12 +96,12 @@
|
|||
]
|
||||
},
|
||||
"summary": {
|
||||
"records": 8524,
|
||||
"checkedThisPass": 8,
|
||||
"records": 8525,
|
||||
"checkedThisPass": 1,
|
||||
"carriedForward": 74,
|
||||
"pruned": 0,
|
||||
"broken": 74,
|
||||
"verified": 8445,
|
||||
"verified": 8446,
|
||||
"unknown": 5
|
||||
},
|
||||
"byHost": {
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
"broken": 1
|
||||
},
|
||||
"arc.gt": {
|
||||
"verified": 33,
|
||||
"verified": 34,
|
||||
"broken": 1
|
||||
},
|
||||
"zume.training": {
|
||||
|
|
@ -42766,6 +42766,11 @@
|
|||
"state": "verified",
|
||||
"detail": "5fish redirected to its language page https://fivefish.org/15537?language=Pashai%2C%20Southeast (the site answers 200 for any path)",
|
||||
"at": "2026-09-25T09:56:59.503Z"
|
||||
},
|
||||
"https://arc.gt/g8wmw": {
|
||||
"state": "verified",
|
||||
"detail": "HTTP 200 (via https://www.jesusfilm.org/watch/jesus.html/jordanian-sign-language.html)",
|
||||
"at": "2026-09-27T01:21:17.988Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -11,7 +11,7 @@
|
|||
"honest_limit": "This is NOT the same as a national trade language. It finds languages widely used AMONG THE UNENGAGED GROUPS TRACKED HERE, and will MISS a lingua franca that no unengaged group claims as primary (e.g. Hindi in India). Named bridge_candidates, not trade_languages, for that reason.",
|
||||
"why_not_a_list": "No open machine-readable list of national trade languages was available; bridge candidates are derived from this corpus."
|
||||
},
|
||||
"resource_flags_refreshed": "2026-09-25",
|
||||
"resource_flags_refreshed": "2026-09-27",
|
||||
"resource_flags_source": "data/tables/people-group-pins.json. prevalence_rank, bridge_candidate, groups_speaking and full_bible are curated and untouched."
|
||||
},
|
||||
"columns": [
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
201
internal/check-link-verification-coverage.mjs
Normal file
201
internal/check-link-verification-coverage.mjs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* check-link-verification-coverage.mjs — rule 4 of the brief, as a gate instead of a good habit.
|
||||
*
|
||||
* "Verify a URL resolves before shipping it." verify-gospel-links.mjs does the verifying and writes
|
||||
* the record; nothing asserted that the record COVERS what the build actually ships. So the honest
|
||||
* state of the corpus was a number somebody had to go and measure, and a number nobody re-measures
|
||||
* is a number that was true once.
|
||||
*
|
||||
* ⚠ THE URLS ARE DISCOVERED, NOT LISTED. Every other place in this bundle that walks the pin index
|
||||
* names the fields it reads — and that list has gone stale twice: `ga` (Words of Life) and `se`
|
||||
* (Scripture Earth) shipped unscreened because a field list written before those pools existed
|
||||
* could not see them, and the SUN block and the access table each had to be added after the fact.
|
||||
* This one recurses over the whole structure and treats ANY string that starts with http as a
|
||||
* shipped link. A new pool is then covered on the day it lands, without anyone remembering to come
|
||||
* here — which is the only kind of coverage check worth having.
|
||||
*
|
||||
* WHAT COUNTS AS A PASS:
|
||||
* every shipped URL has an entry — an unrecorded link has never been looked at
|
||||
* no shipped URL is recorded BROKEN — the builder drops dead links; if one survives, it
|
||||
* is shipping against its own evidence
|
||||
* `unknown` only on hosts that cannot be — live.bible.is and knowgod.com answer 200 with
|
||||
* checked by status, and always counted identical bytes for any path, so a status check
|
||||
* there is a false green, and the verifier says so
|
||||
*
|
||||
* Self-test: `--poison` adds a fabricated URL to the corpus, marks a shipped one broken, and moves
|
||||
* an `unknown` onto a host that IS checkable. All three arms must fire.
|
||||
*/
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = resolve(HERE, '..')
|
||||
const POISON = process.argv.includes('--poison')
|
||||
|
||||
/* What cannot be proved by a status code, kept here as literals: a check that reads its own
|
||||
exemptions out of the file it is checking exempts whatever that file says.
|
||||
⚠ THE LAST ENTRY IS A URL SHAPE, NOT A HOST, and that distinction is the whole point of the
|
||||
migration that preceded it: jesusfilm.org/watch/jesus/<slug> DOES discriminate (a bogus slug
|
||||
404s) and 2,049 film links became checkable because of it, while the older
|
||||
/watch/?languageId=<n> form renders byte-identical pages for any id, valid or bogus. Exempting
|
||||
the host would have re-blinded every one of those 2,049. */
|
||||
const STATUS_BLIND = [
|
||||
{ re: /^(https?:\/\/)?([a-z0-9-]+\.)*(live\.bible\.is|bible\.is)(\/|$)/i, why: '200 with identical bytes for any fileset' },
|
||||
{ re: /^(https?:\/\/)?([a-z0-9-]+\.)*knowgod\.com(\/|$)/i, why: 'page bytes identical; proved through Cru\'s API instead' },
|
||||
{ re: /^(https?:\/\/)?([a-z0-9-]+\.)*sun\.bible(\/|$)/i, why: '200 for /sun/<anything>; proved by content' },
|
||||
{ re: /jesusfilm\.org\/watch\/\?languageId=/i, why: 'the pre-shareUrl form renders the same bytes for any id' },
|
||||
]
|
||||
const statusBlind = (u) => STATUS_BLIND.find((x) => x.re.test(u)) || null
|
||||
|
||||
/* ⚠ A CONTACT IS VERIFIED SOMEWHERE ELSE, and the recursive walk found that out the honest way:
|
||||
it reported 18 unrecorded links, all of them `cr.u` — the translator organisations' own
|
||||
homepages. They were fetched on the run that wrote them, but by verify-translator-org-links.mjs
|
||||
into its OWN record, so gospel-link-verification.json has never heard of them. Resolving them
|
||||
against that second record is right; declaring them exempt would not be. */
|
||||
let orgVerified = new Set()
|
||||
try {
|
||||
const orgs = JSON.parse(readFileSync(resolve(REPO, 'data/tables/translator-org-links.json'), 'utf8')).orgs || {}
|
||||
orgVerified = new Set(Object.values(orgs).filter((o) => o.state === 'verified').map((o) => o.url))
|
||||
} catch { /* no org record: those URLs then have no evidence anywhere, and must fail below */ }
|
||||
|
||||
/* SUN IS THE THIRD RECORD, and its evidence is STRONGER than a status code rather than weaker:
|
||||
sun.bible answers 200 for /sun/<anything>, so fetch-sun-deaf-index.mjs proves each page by
|
||||
CONTENT against a catch-all control (a bogus path returns 904 characters; the New Testament page
|
||||
returns 22,043). The recursive walk found these three the same way it found the contacts — by not
|
||||
knowing which fields were supposed to hold links. */
|
||||
/* PRAYER PAGES ARE PROVED BY A LISTING, NOT A FETCH — the fourth record, and the reason is in
|
||||
fetch-pray-index.mjs: pray.doxa.life answers 200 with identical bytes for ANY slug, so a status
|
||||
check there is a false green. Existence comes from doxa.life's own /api/people-groups/list, and
|
||||
every URL this build ships is built from a slug that listing returned. A pray.doxa.life URL that
|
||||
is NOT in that index is therefore a CONSTRUCTED one, and must fail here. */
|
||||
let prayListed = new Set()
|
||||
try {
|
||||
const pray = JSON.parse(readFileSync(resolve(REPO, 'data/tables/pray-index.json'), 'utf8')).byKey || {}
|
||||
for (const e of Object.values(pray)) if (e && e.u) prayListed.add(e.u.split('?')[0])
|
||||
} catch { /* no prayer index: those URLs then have no evidence, and must fail below */ }
|
||||
/* ⚠ THE INDEX IS A SUBSET AND THE ROSTER IS THE LISTING. pray-index.json holds only the 1,723
|
||||
groups whose (name, country) matched a pin; the access table ships a prayer link for all 2,107,
|
||||
so 809 of them were reported unrecorded here. They are not unproved — doxa-people-groups.json IS
|
||||
the API listing, one row per group with the slug doxa.life returned, and the index is that
|
||||
listing narrowed by a join this check has no business requiring. */
|
||||
try {
|
||||
const roster = JSON.parse(readFileSync(resolve(REPO, 'data/tables/doxa-people-groups.json'), 'utf8')).rows || []
|
||||
for (const r of roster) if (r && r.pray_url) prayListed.add(String(r.pray_url).split('?')[0])
|
||||
} catch { /* no roster: the subset above still covers what it covers */ }
|
||||
|
||||
let sunVerified = new Set()
|
||||
try {
|
||||
const sun = JSON.parse(readFileSync(resolve(REPO, 'data/tables/sun-deaf-index.json'), 'utf8'))
|
||||
if ((sun.meta || {}).catch_all_control) sunVerified = new Set((sun.resources || []).map((r) => r.url))
|
||||
} catch { /* no SUN record: its links then have no evidence, and must fail below */ }
|
||||
|
||||
const fails = []
|
||||
const idx = JSON.parse(readFileSync(resolve(REPO, 'data/tables/people-group-pins.json'), 'utf8'))
|
||||
const verif = JSON.parse(readFileSync(resolve(REPO, 'data/tables/gospel-link-verification.json'), 'utf8')).links || {}
|
||||
|
||||
/** Every http string anywhere in the structure, with a breadcrumb of where it was found. */
|
||||
const found = new Map()
|
||||
const walk = (node, path) => {
|
||||
if (node == null) return
|
||||
if (typeof node === 'string') {
|
||||
/* A citation is not an offer. `source`, `_note` and everything under `meta` record WHERE the
|
||||
data came from — an API endpoint, a licence page — and no surface renders them as links. */
|
||||
if (/(^|\.)(meta|_note|source|sources|site|fetchedFrom|licenceSource|boundarySource|why|note)(\.|$|\[)/.test(path)) return
|
||||
if (/^https?:\/\//i.test(node) && !found.has(node)) found.set(node, path)
|
||||
return
|
||||
}
|
||||
if (Array.isArray(node)) { node.forEach((v, i) => walk(v, `${path}[${i}]`)); return }
|
||||
if (typeof node === 'object') for (const [k, v] of Object.entries(node)) walk(v, path ? `${path}.${k}` : k)
|
||||
}
|
||||
walk(idx.codes, 'codes')
|
||||
walk(idx.sun, 'sun')
|
||||
walk(idx.bridges, 'bridges')
|
||||
walk(idx.bridgeCandidates, 'bridgeCandidates')
|
||||
|
||||
/* ⚠ THE PIN INDEX IS NOT THE ONLY THING SHIPPED, and scoping this check to it repeated the very
|
||||
blind spot it was written to close: people-group-resources.json — the "Resources by People
|
||||
Group" tab — was serving 2,294 URL cells of which 1,412 were the retired jesusfilm
|
||||
?languageId= form, links the site ignores, while the map beside it had been migrated onto
|
||||
Arclight shareUrls. Nothing saw it, because every check looked at one file.
|
||||
*
|
||||
* ⚠ AND THE SCOPE IS "WHAT A READER CAN CLICK", which is narrower than "what is staged". Reading
|
||||
* every staged file reported 6,115 unrecorded links and 53 broken ones, and NONE of them is
|
||||
* offered to anybody: they are API endpoints in `source` fields, and the raw upstream catalogues
|
||||
* (arclight-share-urls and friends) which are build CACHES that happen to sit in the same folder —
|
||||
* they still hold the 53 share URLs the verifier proved dead, which is exactly why the pin builder
|
||||
* drops them. Judging those as shipped would make this check red on correct behaviour, and a gate
|
||||
* that is red on correct behaviour gets switched off. So the scope is the tables the SURFACES
|
||||
* read, taken from the surfaces, plus provenance fields excluded by path. */
|
||||
const SURFACES = [
|
||||
resolve(REPO, 'plugin-designs/gospel-resources/data-tables.vue'),
|
||||
resolve(REPO, 'plugin-designs/gospel-resources/resources-map.vue'),
|
||||
resolve(REPO, 'plugin-designs/gospel-resources/resources-list.vue'),
|
||||
resolve(REPO, 'plugin-designs/gospel-resources/charts.vue'),
|
||||
]
|
||||
const wanted = new Set()
|
||||
for (const f of SURFACES) {
|
||||
let src = ''
|
||||
try { src = readFileSync(f, 'utf8') } catch { continue }
|
||||
for (const m of src.matchAll(/file:\s*'([a-z0-9-]+)'/gi)) wanted.add(`${m[1]}.json`)
|
||||
for (const m of src.matchAll(/tables\/([a-z0-9-]+)\.json/gi)) wanted.add(`${m[1]}.json`)
|
||||
}
|
||||
if (wanted.size < 5) fails.push(`only ${wanted.size} reader-facing table(s) found in the surfaces — the scope is not being derived, so this check would under-measure`)
|
||||
const POOL = resolve(REPO, 'public', TREE_NAME, 'assets/tables')
|
||||
for (const f of wanted) {
|
||||
/* The verification record is a LEDGER, deliberately keeping URLs proved dead; walking it would
|
||||
ask the record to vouch for itself. */
|
||||
if (f === 'gospel-link-verification.json') continue
|
||||
try { walk(JSON.parse(readFileSync(resolve(POOL, f), 'utf8')), `tables/${f}`) } catch (e) { fails.push(`${f} is read by a surface but could not be read here: ${e.message}`) }
|
||||
}
|
||||
|
||||
if (POISON) found.set('https://ebible.org/this-was-never-verified/', 'codes.__poison.s')
|
||||
|
||||
const unrecorded = [], broken = [], unknownOnCheckable = []
|
||||
let unknownBlind = 0, verified = 0, contacts = 0, byContent = 0, listed = 0
|
||||
let poisonedBroken = false, poisonedUnknown = false
|
||||
for (const [url, where] of found) {
|
||||
let e = verif[url]
|
||||
if (POISON && !poisonedBroken && e && e.state === 'verified') { e = { ...e, state: 'broken' }; poisonedBroken = true }
|
||||
else if (POISON && !poisonedUnknown && e && e.state === 'verified' && !statusBlind(url)) { e = { ...e, state: 'unknown' }; poisonedUnknown = true }
|
||||
if (!e && orgVerified.has(url)) { contacts++; continue }
|
||||
if (!e && sunVerified.has(url)) { byContent++; continue }
|
||||
/* The shipped URL may carry a tracking suffix the index does not (or vice versa); the SLUG is
|
||||
what the listing proved, so compare on origin+path. */
|
||||
if (!e && /pray\.doxa\.life/i.test(url)) {
|
||||
if (prayListed.has(url.split('?')[0])) { listed++; continue }
|
||||
}
|
||||
if (!e) { unrecorded.push(`${url} (${where})`); continue }
|
||||
if (e.state === 'broken') { broken.push(`${url} (${where})`); continue }
|
||||
if (e.state === 'unknown') {
|
||||
if (statusBlind(url)) unknownBlind++
|
||||
else unknownOnCheckable.push(`${url} — ${e.detail || 'no reason recorded'}`)
|
||||
continue
|
||||
}
|
||||
verified++
|
||||
}
|
||||
|
||||
if (!found.size) fails.push('no URL found in the pin index at all — this check would pass vacuously')
|
||||
if (unrecorded.length) fails.push(`${unrecorded.length} shipped link(s) have NO verification entry (e.g. ${unrecorded[0]})`)
|
||||
if (broken.length) fails.push(`${broken.length} shipped link(s) are recorded BROKEN (e.g. ${broken[0]})`)
|
||||
if (unknownOnCheckable.length) fails.push(`${unknownOnCheckable.length} link(s) are 'unknown' on a host that CAN be checked by status (e.g. ${unknownOnCheckable[0]})`)
|
||||
|
||||
if (POISON) {
|
||||
const want = ['have NO verification entry', 'recorded BROKEN', "'unknown' on a host that CAN be checked"]
|
||||
const missed = want.filter((w) => !fails.some((f) => f.includes(w)))
|
||||
if (missed.length) { console.error(`LINK-COVERAGE --poison: these arms did NOT fire: ${missed.join(' | ')}`); process.exit(1) }
|
||||
console.log(`LINK-COVERAGE --poison: PASS — all 3 arms fired (${fails.length} failure(s)).`)
|
||||
process.exit(0)
|
||||
}
|
||||
if (fails.length) {
|
||||
console.error(`LINK-COVERAGE: FAIL — ${fails.length} problem(s):`)
|
||||
for (const f of fails) console.error(` ${f}`)
|
||||
console.error(' Fix: node plugin-designs/gospel-resources/scripts/verify-gospel-links.mjs')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`LINK-COVERAGE: PASS — ${found.size} distinct shipped link(s): ${verified} verified by fetch, ` +
|
||||
`${contacts} translator contact(s) and ${byContent} SUN page(s) verified in their own records, ` +
|
||||
`${listed} prayer page(s) proved by doxa.life's own listing, ` +
|
||||
`${unknownBlind} on targets no status code can prove; ` +
|
||||
'0 unrecorded, 0 broken.')
|
||||
|
|
@ -92,6 +92,8 @@
|
|||
"check:gospel-tables-staged": "node internal/check-gospel-tables-staged.mjs",
|
||||
"check:embed-contract": "node internal/check-gospel-embed-contract.mjs",
|
||||
"check:engine-contract": "node internal/check-gospel-engine-contract.mjs",
|
||||
"check:link-coverage": "node internal/check-link-verification-coverage.mjs",
|
||||
"check:link-coverage:poison": "node internal/check-link-verification-coverage.mjs --poison",
|
||||
"check:engine-contract:poison": "node internal/check-gospel-engine-contract.mjs --poison",
|
||||
"check:embed-contract:poison": "node internal/check-gospel-embed-contract.mjs --poison",
|
||||
"check:map-attribution": "node internal/check-map-attribution.mjs",
|
||||
|
|
@ -397,6 +399,7 @@
|
|||
"check:translator-contacts": "the credit line LINKS the translating organisation now, and a contact URL is a link this map stands behind. Reads ALLOWED_CONTACT out of internal/verify-gospel-map.mjs rather than restating it (two copies of an allowlist drift, and the drifting copy is the one nobody reads), then asserts every cr.u is on that list, was recorded `verified` by a real fetch, is never also serving a RESOURCE (wycliffe.org is somewhere to write to, not a publisher this map reads text from), and is actually RENDERED as an anchor \u2014 the data can carry 838 URLs while the template shows none. Static, sub-second. Self-test: `bun run check:translator-contacts:poison`.",
|
||||
"check:embed-contract": "the map's URL is a PUBLIC INTERFACE: a partner embeds ?codes=<iso3 list>, ?country=<ISO3> or ?view=zero in an iframe and never touches it again. A refactor breaks those links SILENTLY \u2014 the leaf still mounts, renders and logs no errors, it just shows the whole world to a church that asked for its forty groups. Asserts through the reader's eyes (the partner-list notice's two numbers), with both numbers and the probe codes derived from the roster the leaf itself reads, never typed in. Browser gate; needs no server of its own. Self-test: `bun run check:embed-contract:poison`.",
|
||||
"check:engine-contract": "the map must use the engine it was ASKED for or fail VISIBLY \u2014 never a substitute. resources-map read `engineGlobal(provider) || window.mapboxgl`; engineGlobal already defaults to mapbox, so that could only fire for provider 'maplibre' and then returned MAPBOX \u2014 a keyless page silently handed the engine that needs a token, which the profile then does not give it, so its tiles 401. Invisible to every other gate: the leaf mounts, renders and logs nothing. Six runtime measurements (2 providers x engine present / blocked / both blocked, CDN blocked by request routing the way a proxy or an offline kiosk does) plus a static arm on the source, comments stripped so the fix's own quotation of the deleted line is not read as the defect. Mapbox arms inject a dummy MAP_TOKEN, because without one the element never mounts on a static tree and that is the environment, not the product. Engine attribution is read off the canvas class, not a debug handle. Browser gate. Self-test: `bun run check:engine-contract:poison`.",
|
||||
"check:link-coverage": "rule 4 of the brief — 'verify a URL resolves before shipping it' — as a gate instead of a habit. verify-gospel-links.mjs does the verifying; nothing asserted the record COVERS what is shipped, so the honest state of the corpus was a number someone had to go and measure. DISCOVERS the URLs by recursing the pin index for any http string rather than naming fields: every field list in this bundle has gone stale (ga and se shipped unscreened, the SUN block and the access table were each added after the fact), and the recursion found 16 links my own hand-rolled measurement had missed. Resolves each against THREE records — the verification table, translator-org-links.json (contacts, fetched by their own script) and sun-deaf-index.json (proved by content against a catch-all control, which is stronger than a status code) — and fails on any unrecorded link, any shipped link recorded BROKEN, and any 'unknown' on a target a status code CAN prove. The status-blind list holds one URL SHAPE, not a host: jesusfilm.org/watch/jesus/<slug> discriminates and 2,049 links depend on that, while /watch/?languageId= does not. Static, sub-second. Self-test: `bun run check:link-coverage:poison`.",
|
||||
"check:snapshot-guard": "every gospel-resources catalogue fetcher must write through scripts/_snapshot-guard.mjs, which refuses a write that loses more than a tenth of the previous snapshot. Written after a rate-limited FCBH run silently replaced 1,666 video scopes with 1,103. Static, sub-second. Self-test: `bun run check:snapshot-guard:poison` injects a raw write and the check must fail on it.",
|
||||
"check:spa-tab-order": "the cap-5 SPA must open on Big Picture and keep the Driver's order (Big Picture, UPG-100, Prayer Progress, Gospel Resources) — asserted as PRESENCE + tab-1 + RELATIVE order on the RENDERED rail, because the rail gained three sections and now renders 12 rows with others between them. ⚠ NOT WAVE-WIRED: it FAILS rather than skips when the SPA is unreachable, deliberately (\"a gate that cannot reach its subject has not passed, it has not run\") — correct standalone, but gates:post runs on checkouts with no server. Needs `bun run serve` on 4173.",
|
||||
"check:stale-sweep-claim": "a caveat that has stopped being true is a wrong number. The gospel surfaces carry a paragraph saying which pools were searched; while the GRN sweep ran they truthfully said part of it was unread, the sweep finished, and the sentence stayed for four more commits. The DATA decides here: undecided gap codes mean the claim is allowed, all-decided means it must go, so it re-arms itself for the next slow pool. Does NOT flag a claim rendered under a v-if on the pending count — that form expires on its own. Self-test: `--poison`. Static, sub-second.",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { writeSnapshot } from './_snapshot-guard.mjs'
|
|||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = resolve(HERE, '../../..')
|
||||
const OUT = resolve(REPO, 'data/tables/gospel-by-province.json')
|
||||
const OUT_GROUPS = resolve(REPO, 'data/tables/gospel-by-province-groups.json')
|
||||
const T = (f) => JSON.parse(readFileSync(resolve(REPO, 'data/tables', f), 'utf8'))
|
||||
const BOUND = (f) => JSON.parse(readFileSync(resolve(REPO, 'public/dx-maps/assets/boundaries', f), 'utf8'))
|
||||
|
||||
|
|
@ -110,6 +111,7 @@ const COUNTRIES = [
|
|||
|
||||
const rows = []
|
||||
const unassigned = []
|
||||
const assignments = []
|
||||
let provincesAvailable = 0
|
||||
for (const c of COUNTRIES) {
|
||||
const feats = BOUND(c.file).features.map((f) => ({ f, b: bboxOf(f), name: f.properties.shapeName }))
|
||||
|
|
@ -124,6 +126,20 @@ for (const c of COUNTRIES) {
|
|||
if (featHas(cand.f, x, y)) { hit = cand.name; break }
|
||||
}
|
||||
if (!hit) { unassigned.push({ country: c.country, iso3: c.iso3, people_group: r.name, lat: r.lat, lng: r.lng }); continue }
|
||||
/* THE PER-GROUP ROW, published beside the aggregate so no other build has to repeat this
|
||||
point-in-polygon. gospel-internet was running the identical ray-casting over the identical
|
||||
two geometry files and refusing to write unless its counts matched these; a second
|
||||
implementation of the same geometry is a second place for it to drift. */
|
||||
assignments.push({
|
||||
people_group: r.name,
|
||||
country: c.country,
|
||||
iso3: c.iso3,
|
||||
province: hit,
|
||||
iso639_3: r.iso639_3 || '',
|
||||
population: r.population || 0,
|
||||
tier: tiers[r.iso639_3] ?? r.tier ?? 0,
|
||||
approximate: true,
|
||||
})
|
||||
if (!by.has(hit)) by.set(hit, [])
|
||||
by.get(hit).push(r)
|
||||
}
|
||||
|
|
@ -212,6 +228,49 @@ if (!assigned || assigned < 900) {
|
|||
console.error(' geometry or the coordinates changed shape, not that the answer moved.')
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
/* ═══ THE TWO FILES MUST AGREE, ASSERTED HERE ════════════════════════════════════════════
|
||||
* The aggregate says a province holds N groups; the per-group file lists them. Published
|
||||
* separately (the map fetches only the aggregate, lazily, and has no use for 1,005 rows), which
|
||||
* means they CAN disagree — so they are reconciled before either is written. gospel-internet
|
||||
* built its own copy of this point-in-polygon and refused to write unless its counts matched
|
||||
* ours; it should not have to, and it should not have to trust us either.
|
||||
*/
|
||||
const perProvince = new Map()
|
||||
for (const a of assignments) perProvince.set(`${a.iso3}|${a.province}`, (perProvince.get(`${a.iso3}|${a.province}`) || 0) + 1)
|
||||
const disagree = rows.filter((r) => (perProvince.get(`${r.iso3}|${r.province}`) || 0) !== r.groups)
|
||||
if (disagree.length || assignments.length !== assigned) {
|
||||
console.error(`GOSPEL-BY-PROVINCE: REFUSING TO WRITE — the per-group file and the aggregate disagree on ${disagree.length} province(s); ${assignments.length} assignments vs ${assigned} counted.`)
|
||||
process.exit(1)
|
||||
}
|
||||
writeSnapshot(
|
||||
OUT_GROUPS,
|
||||
`${JSON.stringify({
|
||||
meta: {
|
||||
title: 'Which province each people group falls in — India and China',
|
||||
why: 'Published so no other build has to repeat this point-in-polygon. gospel-internet ran ' +
|
||||
'the identical ray-casting over the identical geometry and could only check itself against ' +
|
||||
'our aggregates; a second implementation of the same geometry is a second place for it to drift.',
|
||||
generated: new Date().toISOString().slice(0, 10),
|
||||
grain: 'one row per PLACED people group; the four that fall outside every polygon are in the aggregate file\'s `unassigned`',
|
||||
rows: assignments.length,
|
||||
agrees_with: 'gospel-by-province.json — per-province counts and the assigned total are reconciled at build time, and neither file is written if they differ',
|
||||
approximate: 'Every row is approximate. The coordinate marks where a PEOPLE is found, not a ' +
|
||||
'settlement, and near a border it can fall on the wrong side. No surface may present a ' +
|
||||
'province as a fact about a group.',
|
||||
},
|
||||
columns: [
|
||||
{ title: 'People group', field: 'people_group', type: 'string' },
|
||||
{ title: 'Country', field: 'country', type: 'string' },
|
||||
{ title: 'Province', field: 'province', type: 'string' },
|
||||
{ title: 'Language code', field: 'iso639_3', type: 'string' },
|
||||
{ title: 'People', field: 'population', type: 'number' },
|
||||
{ title: 'Tier', field: 'tier', type: 'number' },
|
||||
],
|
||||
rows: assignments,
|
||||
}, null, 1)}\n`,
|
||||
(j) => (j.rows || []).length,
|
||||
'province assignment(s)',
|
||||
)
|
||||
writeSnapshot(
|
||||
OUT,
|
||||
`${JSON.stringify({
|
||||
|
|
@ -225,6 +284,7 @@ if (!assigned || assigned < 900) {
|
|||
provinces_available: provincesAvailable,
|
||||
groups_assigned: assigned,
|
||||
groups_unassigned: unassigned.length,
|
||||
per_group_file: 'gospel-by-province-groups.json — one row per placed group, reconciled with these counts at build time',
|
||||
india_note: 'India is read from the COMPLETE ADM1 (36 states, geoBoundaries via DataMeet ' +
|
||||
'India, CC BY 2.5 IN). The first build of this table read gb-IND-ADM1.geojson, which is ' +
|
||||
'the UPG-100 list\'s curated 34-unit partition, and eleven groups came out with no ' +
|
||||
|
|
|
|||
|
|
@ -31,19 +31,32 @@ const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Geck
|
|||
const JF = '1_jf-0-0'
|
||||
|
||||
/**
|
||||
* `jos` IS SKIPPED, AND THE REASON IS UPSTREAM. ISO 639-3 jos is Jordanian Sign Language.
|
||||
* Arclight's roster files iso3 "jos" under languageId 139080, whose own name field reads
|
||||
* "Lebanese Sign Language" — a different language (lbs). Meanwhile the JESUS Film DOES exist
|
||||
* at languageId 7138, named Jordanian Sign Language, which is the id the pin index carries.
|
||||
* ═══ TWO ARCLIGHT LANGUAGES, ONE ISO CODE — RESOLVED, WITH THE EVIDENCE ═════════════════════
|
||||
*
|
||||
* So the ISO join — the thing this whole map is built on — disagrees with the publisher's own
|
||||
* label for this one code, and the fallback below resolved 139080 to Magdalena: the wrong film
|
||||
* AND the wrong language, shown to three people groups. Left exactly as it was rather than
|
||||
* guessed at. Resolving it means deciding whether the code or the name is authoritative here,
|
||||
* and that is a data question for someone who can check it against Ethnologue, not something
|
||||
* to settle inside a fetcher.
|
||||
* This code was SKIPPED, and the note here said resolving it meant deciding whether the ISO code
|
||||
* or the publisher's name was authoritative — "a data question for someone who can check it".
|
||||
* It has now been checked, against Arclight's own API rather than against an opinion:
|
||||
*
|
||||
* media-languages?iso3=jos returns TWO ids
|
||||
* 7138 name "Jordanian Sign Language" JESUS film -> arc.gt/g8wmw
|
||||
* 139080 name "Lebanese Sign Language" (the id the roster file lists under jos)
|
||||
* arc.gt/g8wmw redirects to .../watch/jesus.html/jordanian-sign-language.html — the publisher's
|
||||
* own slug names the language, and a bogus arc.gt code 404s, so it discriminates.
|
||||
*
|
||||
* ⚠ AND THE OLD NOTE WAS WRONG ON A FACT. It called Lebanese Sign Language "a different language
|
||||
* (lbs)". ISO 639-3 lbs is LIBYAN Sign Language; Lebanese Sign Language has no code of its own.
|
||||
* So this is not a mis-filing to be corrected upstream — it is two Levantine varieties sharing one
|
||||
* code, which ISO does not separate. Nothing here can be "given back" to a Lebanese code, because
|
||||
* there is none, and joining 139080 to our lbs would be a NAME match on the wrong country.
|
||||
*
|
||||
* THE RULE, therefore: where several Arclight ids share an ISO code, take the one whose own NAME
|
||||
* agrees with the language this map names for that code, and record the other. The code stays
|
||||
* authoritative for the JOIN; the name only breaks a tie the code cannot. Our three jos groups are
|
||||
* Jordanian, so they get the Jordanian film and not the Lebanese variety.
|
||||
*/
|
||||
const SKIP = new Set(['jos'])
|
||||
const OVERRIDE = {
|
||||
jos: { id: 7138, why: 'two Arclight ids carry iso3 jos (7138 Jordanian, 139080 Lebanese); 7138 is the one whose name agrees with this map\'s language for the code, and its shareUrl slug says jordanian-sign-language' },
|
||||
}
|
||||
const DELAY = Number(process.env.DELAY_MS || 250)
|
||||
|
||||
const arc = JSON.parse(readFileSync(ARC, 'utf8')).codes
|
||||
|
|
@ -54,10 +67,12 @@ let fetched = 0, cached = 0, missing = 0
|
|||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const [code, v] = entries[i]
|
||||
if (SKIP.has(code)) { cached++; continue }
|
||||
if (out[code] !== undefined) { cached++; continue }
|
||||
/* An override replaces the ROSTER's id, not the result: the shareUrl is still fetched from the
|
||||
API for that id, so a wrong override cannot invent a link. */
|
||||
const id = OVERRIDE[code] ? OVERRIDE[code].id : v.id
|
||||
try {
|
||||
const r = await fetch(`https://api.arclight.org/v2/media-components/${JF}/languages/${v.id}`, { headers: { 'User-Agent': UA } })
|
||||
const r = await fetch(`https://api.arclight.org/v2/media-components/${JF}/languages/${id}`, { headers: { 'User-Agent': UA } })
|
||||
if (r.ok) {
|
||||
const d = await r.json()
|
||||
// https, always: the API returns http:// and a plain-http link in a field context
|
||||
|
|
@ -98,12 +113,12 @@ for (let i = 0; i < entries.length; i++) {
|
|||
* than falling back to a URL nobody can check. */
|
||||
let alt = null
|
||||
try {
|
||||
const c = await fetch(`https://api.arclight.org/v2/media-components?languageIds=${v.id}&limit=25`, { headers: { 'User-Agent': UA } })
|
||||
const c = await fetch(`https://api.arclight.org/v2/media-components?languageIds=${id}&limit=25`, { headers: { 'User-Agent': UA } })
|
||||
if (c.ok) {
|
||||
const cj = await c.json()
|
||||
const feature = (cj._embedded?.mediaComponents || []).find((m) => m.subType === 'featureFilm')
|
||||
if (feature) {
|
||||
const fr = await fetch(`https://api.arclight.org/v2/media-components/${feature.mediaComponentId}/languages/${v.id}`, { headers: { 'User-Agent': UA } })
|
||||
const fr = await fetch(`https://api.arclight.org/v2/media-components/${feature.mediaComponentId}/languages/${id}`, { headers: { 'User-Agent': UA } })
|
||||
if (fr.ok) {
|
||||
const fd = await fr.json()
|
||||
const fu = String(fd.shareUrl || '').replace(/^http:\/\//, 'https://')
|
||||
|
|
@ -130,7 +145,18 @@ for (let i = 0; i < entries.length; i++) {
|
|||
writeSnapshot(OUT, JSON.stringify({
|
||||
_note: 'Per-language JESUS Film Project deep links (shareUrl) from Arclight media-components. A STRING is the JESUS Film (component 1_jf-0-0). An OBJECT {u,t} is a different JESUS Film Project feature in that language — the JESUS component has no entry for it — and t is the real title, which the surfaces must show instead of "JESUS Film". null means no feature exists and the film is dropped rather than linked to the retired ?languageId= URL, which nothing can verify.',
|
||||
generated: new Date().toISOString().slice(0, 10),
|
||||
stats: { languages: entries.length, withShareUrl: Object.keys(out).length, missing },
|
||||
/* COUNTED, NOT NAMED. withShareUrl used to be Object.keys(out).length, which counts the nulls
|
||||
and the alternative-feature objects too — so a file holding one null reported every language
|
||||
as having a shareUrl, and the previous run reported 2,087 of them while jos was absent
|
||||
entirely. Each state is now counted as what it is. */
|
||||
stats: {
|
||||
languages: entries.length,
|
||||
recorded: Object.keys(out).length,
|
||||
jesusFilm: Object.values(out).filter((v) => typeof v === 'string').length,
|
||||
otherFeature: Object.values(out).filter((v) => v && typeof v === 'object').length,
|
||||
noFeature: Object.values(out).filter((v) => v === null).length,
|
||||
notAnswered: missing,
|
||||
},
|
||||
codes: out,
|
||||
}), (j) => Object.keys(j.codes || {}).length, 'Arclight shareUrl codes')
|
||||
console.log(`DONE languages ${entries.length} shareUrl ${Object.keys(out).length} missing ${missing}`)
|
||||
console.log(`DONE languages ${entries.length} JESUS film ${Object.values(out).filter((v) => typeof v === 'string').length} other feature ${Object.values(out).filter((v) => v && typeof v === 'object').length} no feature ${Object.values(out).filter((v) => v === null).length} not answered ${missing}`)
|
||||
|
|
|
|||
|
|
@ -118,6 +118,28 @@ const bump = (k) => { changed[k] = (changed[k] || 0) + 1 }
|
|||
const any = hasAny(c)
|
||||
if (r.any_resource_found !== any) { r.any_resource_found = any; bump('any_resource_found') }
|
||||
if (r.no_resource_found !== !any) { r.no_resource_found = !any; bump('no_resource_found') }
|
||||
|
||||
/* ═══ THE URLS, AND THIS TABLE IS SHIPPED ═════════════════════════════════════════════
|
||||
* The booleans were being refreshed while the LINKS beside them were not, and this table
|
||||
* is the map's "Resources by People Group" tab — a reader clicks these. Measured before
|
||||
* this pass: 2,294 URL cells, of which 1,412 were the retired
|
||||
* jesusfilm.org/watch/?languageId=<id> form. That form is IGNORED by the site — a real id
|
||||
* and a bogus one render the same generic library page — which is why the map migrated off
|
||||
* it onto Arclight shareUrls. The migration never reached here, so the tables page kept
|
||||
* offering links that go nowhere in particular, for a year's worth of builds.
|
||||
*
|
||||
* The index is authoritative for these two fields and its URLs are the verified ones, so
|
||||
* they are copied across. Only where the index HAS one: a curated URL is never blanked by
|
||||
* a pool that happens to have no entry. */
|
||||
const sUrl = (codes[c] || {}).s, fUrl = (codes[c] || {}).f
|
||||
if ('scripture_url' in r && sUrl && r.scripture_url !== sUrl) {
|
||||
if (/[?&]languageId=/.test(r.scripture_url || '')) bump('retired_form_replaced')
|
||||
r.scripture_url = sUrl; bump('scripture_url')
|
||||
}
|
||||
if ('film_url' in r && fUrl && r.film_url !== fUrl) {
|
||||
if (/[?&]languageId=/.test(r.film_url || '')) bump('retired_form_replaced')
|
||||
r.film_url = fUrl; bump('film_url')
|
||||
}
|
||||
}
|
||||
// THE POOL LIST IS A CLAIM TOO. This table still told readers GRN was "not included:
|
||||
// requires an API key" — false on both halves. GRN is included, 583 codes of it, and it
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -59,7 +59,7 @@
|
|||
"meta": {
|
||||
"title": "The Deaf Gap",
|
||||
"grain": "one row per Deaf people group",
|
||||
"generated": "2026-09-25",
|
||||
"generated": "2026-09-27",
|
||||
"measured": "150 sign languages · 234 people groups · 84,159,050 people. 136 languages / 183 groups / 55,199,840 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": "40 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.",
|
||||
|
|
@ -2621,7 +2621,7 @@
|
|||
"iso639_3": "jos",
|
||||
"countries": "SYR",
|
||||
"population": 221000,
|
||||
"signed_film": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"signed_film": "https://arc.gt/g8wmw",
|
||||
"has_signed_film": true,
|
||||
"has_any_resource": true,
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -2660,7 +2660,7 @@
|
|||
"iso639_3": "jos",
|
||||
"countries": "JOR",
|
||||
"population": 116000,
|
||||
"signed_film": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"signed_film": "https://arc.gt/g8wmw",
|
||||
"has_signed_film": true,
|
||||
"has_any_resource": true,
|
||||
"code_unmatchable": false,
|
||||
|
|
@ -2738,7 +2738,7 @@
|
|||
"iso639_3": "jos",
|
||||
"countries": "LBN",
|
||||
"population": 51000,
|
||||
"signed_film": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"signed_film": "https://arc.gt/g8wmw",
|
||||
"has_signed_film": true,
|
||||
"has_any_resource": true,
|
||||
"code_unmatchable": false,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"title": "Doxa people groups — what the gospel is available in, and what is missing",
|
||||
"grain": "one row per people group on Doxa's roster",
|
||||
"source": "pray.doxa.life roster joined to the resource index by ISO 639-3",
|
||||
"generated": "2026-09-26",
|
||||
"generated": "2026-09-27",
|
||||
"groups": 2107,
|
||||
"by_access": {
|
||||
"nothing found": 369,
|
||||
|
|
@ -19768,7 +19768,7 @@
|
|||
"translated_by": "",
|
||||
"translated_year": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"film_url": "https://arc.gt/g8wmw",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
|
|
@ -20048,7 +20048,7 @@
|
|||
"translated_by": "",
|
||||
"translated_year": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"film_url": "https://arc.gt/g8wmw",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
|
|
@ -21928,7 +21928,7 @@
|
|||
"translated_by": "",
|
||||
"translated_year": "",
|
||||
"scripture_url": "",
|
||||
"film_url": "https://www.jesusfilm.org/watch/?languageId=7138",
|
||||
"film_url": "https://arc.gt/g8wmw",
|
||||
"audio_url": "",
|
||||
"bible_video_url": "",
|
||||
"stories_url": "",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"bridge_strength_note": "MEASURED: 24% of aligned rows rest on a bridge shared by only 2-3 groups. bridge_strength grades this (strong >=8 groups, moderate >=4, weak <4). A WEAK bridge is a hypothesis, not a route — e.g. \"Gawri\" was offered as the bridge for 10.3M Pakistanis where the real lingua franca is Urdu, which no unengaged group here claims as primary and which this method therefore cannot see. Filter on bridge_strength before acting.",
|
||||
"bridge_caveat": "A bridge candidate is measured from THIS corpus (a language primary to >1 DX group in the country). It is NOT proof the group speaks it, and it is NOT a national trade language — a lingua franca no unengaged group claims as primary is invisible here.",
|
||||
"pools_caveat": "Resource status is from eBible + JESUS Film. GRN and FCBH audio are key-blocked and unchecked — absence is never proof.",
|
||||
"resource_flags_refreshed": "2026-09-25",
|
||||
"resource_flags_refreshed": "2026-09-27",
|
||||
"resource_flags_source": "data/tables/people-group-pins.json — the same index the map draws. Curated bridge, internet and channel columns are untouched."
|
||||
},
|
||||
"columns": [
|
||||
|
|
|
|||
10095
public/dx-maps/assets/tables/gospel-by-province-groups.json
Normal file
10095
public/dx-maps/assets/tables/gospel-by-province-groups.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,11 +2,12 @@
|
|||
"meta": {
|
||||
"title": "Gospel access by province — India and China",
|
||||
"why": "These two countries hold 47.9% of Doxa's roster (855 + 154 of 2,107). No other country is close; the third is Sudan at 105. A national fill cannot answer \"where would a team go\" for half the map.",
|
||||
"generated": "2026-09-26",
|
||||
"generated": "2026-09-27",
|
||||
"provinces_with_groups": 46,
|
||||
"provinces_available": 48,
|
||||
"groups_assigned": 1005,
|
||||
"groups_unassigned": 4,
|
||||
"per_group_file": "gospel-by-province-groups.json — one row per placed group, reconciled with these counts at build time",
|
||||
"india_note": "India is read from the COMPLETE ADM1 (36 states, geoBoundaries via DataMeet India, CC BY 2.5 IN). The first build of this table read gb-IND-ADM1.geojson, which is the UPG-100 list's curated 34-unit partition, and eleven groups came out with no province because they stood in states that partition does not draw. Switching the file moved 998 assigned to 1,005 and 44 province rows to 46, and turned the province names into India's own states.",
|
||||
"unassigned_note": "Four remain, each one legible rather than lost. Great Andamanese and the Sentinelese sit on small islands whose simplified coastline the point falls just outside — Andaman and Nicobar IS in the file, so this is geometry precision, not a missing province. Jing and Pana are on China's Vietnam and Laos borders, outside the twelve-province subset. They are listed in `unassigned` with their coordinates.",
|
||||
"china_note": "China's ADM1 file carries 12 provinces, not 31+. Measured rather than assumed: 152 of 154 Chinese groups fall inside them and all twelve are used, so the file is cut to the provinces where the unreached peoples are.",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"meta": {
|
||||
"title": "Gospel resource link verification",
|
||||
"generated": "2026-09-25T09:56:59.860Z",
|
||||
"generated": "2026-09-27T01:21:18.374Z",
|
||||
"method": "HEAD, redirects followed. verified = HTTP 200. broken = 404/410. unknown = unreachable, an ambiguous status, or a URL shape that cannot be checked by fetch.",
|
||||
"scope": "Every URL in data/tables/people-group-pins.json, deduplicated.",
|
||||
"states": {
|
||||
|
|
@ -96,12 +96,12 @@
|
|||
]
|
||||
},
|
||||
"summary": {
|
||||
"records": 8524,
|
||||
"checkedThisPass": 8,
|
||||
"records": 8525,
|
||||
"checkedThisPass": 1,
|
||||
"carriedForward": 74,
|
||||
"pruned": 0,
|
||||
"broken": 74,
|
||||
"verified": 8445,
|
||||
"verified": 8446,
|
||||
"unknown": 5
|
||||
},
|
||||
"byHost": {
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
"broken": 1
|
||||
},
|
||||
"arc.gt": {
|
||||
"verified": 33,
|
||||
"verified": 34,
|
||||
"broken": 1
|
||||
},
|
||||
"zume.training": {
|
||||
|
|
@ -42766,6 +42766,11 @@
|
|||
"state": "verified",
|
||||
"detail": "5fish redirected to its language page https://fivefish.org/15537?language=Pashai%2C%20Southeast (the site answers 200 for any path)",
|
||||
"at": "2026-09-25T09:56:59.503Z"
|
||||
},
|
||||
"https://arc.gt/g8wmw": {
|
||||
"state": "verified",
|
||||
"detail": "HTTP 200 (via https://www.jesusfilm.org/watch/jesus.html/jordanian-sign-language.html)",
|
||||
"at": "2026-09-27T01:21:17.988Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -11,7 +11,7 @@
|
|||
"honest_limit": "This is NOT the same as a national trade language. It finds languages widely used AMONG THE UNENGAGED GROUPS TRACKED HERE, and will MISS a lingua franca that no unengaged group claims as primary (e.g. Hindi in India). Named bridge_candidates, not trade_languages, for that reason.",
|
||||
"why_not_a_list": "No open machine-readable list of national trade languages was available; bridge candidates are derived from this corpus."
|
||||
},
|
||||
"resource_flags_refreshed": "2026-09-25",
|
||||
"resource_flags_refreshed": "2026-09-27",
|
||||
"resource_flags_source": "data/tables/people-group-pins.json. prevalence_rank, bridge_candidate, groups_speaking and full_bible are curated and untouched."
|
||||
},
|
||||
"columns": [
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue