diff --git a/data/tables/doxa-people-groups.json b/data/tables/doxa-people-groups.json index c674d3bc8..62b8b1cc1 100644 --- a/data/tables/doxa-people-groups.json +++ b/data/tables/doxa-people-groups.json @@ -4991,7 +4991,7 @@ "religion": "Islam - Sunni", "population": 5500, "people_praying": 0, - "people_committed": 0, + "people_committed": 1, "adopted_by_churches": 0, "mapped": true, "matched_by": "doxa detail", @@ -16751,7 +16751,7 @@ "religion": "Islam - Sunni", "population": 57500, "people_praying": 2, - "people_committed": 0, + "people_committed": 1, "adopted_by_churches": 0, "mapped": true, "matched_by": "doxa detail", @@ -34055,7 +34055,7 @@ "religion": "Hinduism", "population": 700, "people_praying": 1, - "people_committed": 2, + "people_committed": 3, "adopted_by_churches": 0, "mapped": true, "matched_by": "doxa detail", @@ -34319,7 +34319,7 @@ "religion": "Islam - Folk", "population": 12000, "people_praying": 1, - "people_committed": 4, + "people_committed": 3, "adopted_by_churches": 0, "mapped": true, "matched_by": "doxa detail", @@ -43463,7 +43463,7 @@ "religion": "Islam - Sunni", "population": 23000, "people_praying": 0, - "people_committed": 1, + "people_committed": 2, "adopted_by_churches": 0, "mapped": true, "matched_by": "doxa detail", diff --git a/data/tables/gospel-link-verification.json b/data/tables/gospel-link-verification.json index 6750f6f69..da049f9d1 100644 --- a/data/tables/gospel-link-verification.json +++ b/data/tables/gospel-link-verification.json @@ -1,7 +1,7 @@ { "meta": { "title": "Gospel resource link verification", - "generated": "2026-09-25T03:43:08.467Z", + "generated": "2026-09-25T09:56:59.860Z", "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": 8516, - "checkedThisPass": 3, + "records": 8524, + "checkedThisPass": 8, "carriedForward": 74, "pruned": 0, "broken": 74, - "verified": 8437, + "verified": 8445, "unknown": 5 }, "byHost": { @@ -142,7 +142,7 @@ "verified": 921 }, "fivefish.org": { - "verified": 1022 + "verified": 1030 } } }, @@ -42726,6 +42726,46 @@ "state": "verified", "detail": "HTTP 200 (via https://www.jesusfilm.org/watch/the-savior.html/arabic-najdi.html)", "at": "2026-09-25T03:43:07.944Z" + }, + "https://fivefish.org/lma": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/12954?language=Limba%2C%20East (the site answers 200 for any path)", + "at": "2026-09-25T09:55:49.572Z" + }, + "https://fivefish.org/nev": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/23092?language=Nyaheun (the site answers 200 for any path)", + "at": "2026-09-25T09:55:59.632Z" + }, + "https://fivefish.org/ngt": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/23045?language=Kriang (the site answers 200 for any path)", + "at": "2026-09-25T09:56:09.574Z" + }, + "https://fivefish.org/alk": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/23050?language=Alak (the site answers 200 for any path)", + "at": "2026-09-25T09:56:19.687Z" + }, + "https://fivefish.org/nos": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/4976?language=Nisu%2C%20Eastern (the site answers 200 for any path)", + "at": "2026-09-25T09:56:29.456Z" + }, + "https://fivefish.org/lbo": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/12797?language=Laven (the site answers 200 for any path)", + "at": "2026-09-25T09:56:39.531Z" + }, + "https://fivefish.org/jle": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/14912?language=Ngile (the site answers 200 for any path)", + "at": "2026-09-25T09:56:49.486Z" + }, + "https://fivefish.org/psi": { + "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" } } } diff --git a/internal/check-gospel-all.mjs b/internal/check-gospel-all.mjs new file mode 100644 index 000000000..a22777ac8 --- /dev/null +++ b/internal/check-gospel-all.mjs @@ -0,0 +1,86 @@ +/** + * check-gospel-all.mjs — everything this bundle owns, in one command. + * + * WHY THIS EXISTS. gospel-resources has ten gates of its own and every one of them was + * hand-run. That is how a dead bundle shipped: a duplicate module killed all five leaves, + * and the one gate I happened to run that day was pointed at the map. `check:no-orphan-gates` + * was right that they were all reachable — reachable is not the same as run. + * + * It is NOT wired into gates:pre/post on purpose. Three of these need a browser, the shared + * wave holds browser gates under load, and a bundle owner should not be able to slow every + * other agent's build by adding to a list they do not own. This is the bundle's own front + * door instead: one command, honest about what it could not measure. + * + * STATIC FIRST, THEN BROWSER. The static gates are sub-second and catch most drift, so they + * run first and a failure there stops the run before a browser is launched — there is no + * point spending a scarce browser slot to confirm a table is inconsistent. + * + * NOT MEASURED IS NOT PASS. A browser gate that cannot get a slot exits 75 and is reported + * as NOT MEASURED, never folded into the pass. Anything else would turn a busy machine into + * a green tick. + */ +import { spawnSync } from 'node:child_process' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const HERE = dirname(fileURLToPath(import.meta.url)) +const REPO = resolve(HERE, '..') +const SLOT = resolve(HERE, 'browser-slot.sh') + +const STATIC = [ + ['check:no-conflict-markers', 'nothing tracked is mid-merge'], + ['check:no-conflict-markers:poison', '...and that check can fail'], + ['check:coverage-consistent', 'the surfaces agree about who has what'], + ['check:coverage-consistent:poison', '...and that check can fail'], + ['check:surfaces-read-the-tier', 'one place decides "this group has nothing"'], + ['check:stale-sweep-claim', 'no caveat outliving its truth'], + ['check:click-sheet', 'the country sheet resolves, links are absolute'], + ['check:snapshot-guard', 'no fetcher writes around the guard'], +] +const BROWSER = [ + ['check:leaves-mount', 'all five leaves actually mount'], + ['check:leaves-mount:poison', '...and that check can fail'], + ['check:composed-filters', 'the composite filters instead of navigating'], +] + +const run = (script, viaSlot) => { + const cmd = viaSlot ? SLOT : 'bun' + const args = viaSlot ? ['bun', 'run', script] : ['run', script] + const r = spawnSync(cmd, args, { cwd: REPO, encoding: 'utf8' }) + return { code: r.status ?? 1, out: `${r.stdout || ''}${r.stderr || ''}` } +} + +const failed = [] +const notMeasured = [] +let passed = 0 + +for (const [script, what] of STATIC) { + const r = run(script, false) + if (r.code === 0) { passed++; console.log(` ok ${script.padEnd(34)} ${what}`) } + else { failed.push([script, r.out.trim().split('\n').slice(-3).join(' | ')]); console.log(` FAIL ${script.padEnd(34)} ${what}`) } +} +if (failed.length) { + console.error(`\nGOSPEL-ALL: FAIL — ${failed.length} static gate(s); not launching a browser to confirm it.`) + for (const [s, tail] of failed) console.error(` ${s}: ${tail}`) + process.exit(1) +} + +for (const [script, what] of BROWSER) { + const r = run(script, true) + if (r.code === 0) { passed++; console.log(` ok ${script.padEnd(34)} ${what}`) } + else if (r.code === 75) { notMeasured.push(script); console.log(` -- ${script.padEnd(34)} NOT MEASURED (no browser slot)`) } + else { failed.push([script, r.out.trim().split('\n').slice(-3).join(' | ')]); console.log(` FAIL ${script.padEnd(34)} ${what}`) } +} + +console.log('') +if (failed.length) { + console.error(`GOSPEL-ALL: FAIL — ${failed.length} gate(s) red, ${passed} green, ${notMeasured.length} not measured.`) + for (const [s, tail] of failed) console.error(` ${s}: ${tail}`) + process.exit(1) +} +if (notMeasured.length) { + console.log(`GOSPEL-ALL: INCOMPLETE — ${passed} green, ${notMeasured.length} NOT MEASURED (${notMeasured.join(', ')}).`) + console.log(' Not a pass. Re-run when a browser slot is free.') + process.exit(75) +} +console.log(`GOSPEL-ALL: PASS — ${passed} gate(s), static and browser.`) diff --git a/internal/check-no-conflict-markers.mjs b/internal/check-no-conflict-markers.mjs new file mode 100644 index 000000000..f267da80e --- /dev/null +++ b/internal/check-no-conflict-markers.mjs @@ -0,0 +1,68 @@ +/** + * check-no-conflict-markers.mjs — nothing tracked may carry an unresolved merge. + * + * THE INCIDENT. A merge commit from this worktree landed the BUILT bundle + * public/doxa-maps/gospel-resources/gospel-resources.js with 21 conflict markers in it — + * both sides concatenated, 1.13 MB. The browser threw "Identifier Ge has already been + * declared" and every gospel leaf rendered dead, while the SRI hash still matched, every + * request still returned 200, and no probe could see an error. + * + * THE CAUSE WAS A LANDING SCRIPT, NOT A BAD MERGE. The loop resolved the one file it + * expected to conflict and then ran `git add -A public`, which stages EVERY file under + * public/ — including the ones still holding markers. A one-line convenience that turns a + * conflict git correctly refused to guess at into a committed artefact. + * + * AND THE FIRST DIAGNOSIS WAS WRONG. I attributed the duplicate identifier to a module + * imported twice and re-routed an import to "fix" it. That change was harmless but it was + * not the cause; the rebuild that came with it is what repaired the file. A plausible cause + * that a fix appears to confirm is the easiest kind of wrong answer, and the only defence is + * a check that names the real condition. + * + * THE RULE, for a BUILT file especially: never hand-merge, never take both. Take either + * side, REBUILD, commit the rebuild. Then run this before pushing. + */ +import { execSync } from 'node:child_process' +import { readFileSync } 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 POISON = process.argv.includes('--poison') + +/* Built from char codes so this file never contains a marker and never trips itself. */ +const OPEN = String.fromCharCode(60).repeat(7) +const MID = String.fromCharCode(61).repeat(7) +const CLOSE = String.fromCharCode(62).repeat(7) +const MARKER = new RegExp('^(' + OPEN + '|' + MID + '$|' + CLOSE + ')', 'm') +const scan = (text) => MARKER.test(text) + +if (POISON) { + const fake = 'a\n' + OPEN + ' HEAD\nmine\n' + MID + '\ntheirs\n' + CLOSE + ' other\n' + if (!scan(fake)) { + console.error('NO-CONFLICT-MARKERS: SELF-TEST FAIL — a textbook conflict was not detected.') + process.exit(1) + } + console.log('NO-CONFLICT-MARKERS: SELF-TEST PASS — a textbook conflict is detected.') + process.exit(0) +} + +const tracked = execSync('git ls-files -z', { cwd: REPO, maxBuffer: 1 << 28 }) + .toString('utf8').split(String.fromCharCode(0)).filter(Boolean) + +const hits = [] +for (const f of tracked) { + let text + try { text = readFileSync(resolve(REPO, f), 'utf8') } catch { continue } + if (text.indexOf(String.fromCharCode(0)) >= 0) continue + if (scan(text)) hits.push(f) +} + +if (hits.length) { + console.error('NO-CONFLICT-MARKERS: FAIL — ' + hits.length + ' tracked file(s) carry an unresolved merge:') + for (const f of hits.slice(0, 10)) console.error(' ' + f) + console.error(' For a BUILT file: take either side, REBUILD the bundle, commit the rebuild.') + console.error(' Never "add -A" a directory mid-merge — it stages the markers with everything else.') + process.exit(1) +} +console.log('NO-CONFLICT-MARKERS: PASS — ' + tracked.length + ' tracked file(s), none mid-merge.') diff --git a/package.json b/package.json index 71d3d658c..7691abd9e 100644 --- a/package.json +++ b/package.json @@ -197,7 +197,10 @@ "app:start-page": "node internal/set-app-start-page.mjs", "check:app-start-page": "node internal/set-app-start-page.mjs --check", "check:leaves-mount": "node internal/check-gospel-leaves-mount.mjs", - "check:leaves-mount:poison": "node internal/check-gospel-leaves-mount.mjs --poison" + "check:leaves-mount:poison": "node internal/check-gospel-leaves-mount.mjs --poison", + "check:gospel-all": "node internal/check-gospel-all.mjs", + "check:no-conflict-markers": "node internal/check-no-conflict-markers.mjs", + "check:no-conflict-markers:poison": "node internal/check-no-conflict-markers.mjs --poison" }, "//scripts": { "_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.", @@ -297,7 +300,9 @@ "check:vision-card-numbers": "the DOXA Vision Card must show the numbers the DATA gives: independently recomputes all four funnel steps (and DOXA-in-frontier) from people_groups.csv + the saved DOXA list, compares with vision-funnel.json, and with --url also with the numbers painted in the card. A typed-in number, a stale funnel file or a rendering bug fails it.", "check:map-embed-cells": "PRODUCTION-MATRIX cells B (iframe) and C (script embed) for the seven Research Gallery maps on a genuinely foreign origin (tree on 127.0.0.1, partner page on localhost). C passes the HOST a distinct token and fails if any Mapbox request uses another. --only bundle/profile,… --cell B|C.", "check:gallery-embed:live-cors": "the Research Gallery on a foreign page under the LIVE static layer's CORS rule (Access-Control-Allow-Origin on files inside the tree's folders, NOT on the root manifest.json, measured on nuxtbase): it must still boot every tab, by reading the CORS-readable manifest copy generate-tree writes beside its bundle.", - "check:leaves-mount": "DID THE ELEMENT ACTUALLY MOUNT, on all five gospel leaves. Exists because a ReferenceError thrown during setup dies INSIDE the custom-element boundary: the page loads, every asset returns 200, pageerror fires zero times, and the element sits there with an empty shadow root. A healthy-looking dead page. This asks the cheap unambiguous question the rich gates are too slow to ask on every leaf — is anything there — in five steps that each name their own failure: tag defined, host present, shadowRoot attached, shadow root non-empty, visible text rendered. It does NOT trust the absence of console errors; zero errors is not the same as working. --poison for the negative test." + "check:leaves-mount": "DID THE ELEMENT ACTUALLY MOUNT, on all five gospel leaves. Exists because a ReferenceError thrown during setup dies INSIDE the custom-element boundary: the page loads, every asset returns 200, pageerror fires zero times, and the element sits there with an empty shadow root. A healthy-looking dead page. This asks the cheap unambiguous question the rich gates are too slow to ask on every leaf — is anything there — in five steps that each name their own failure: tag defined, host present, shadowRoot attached, shadow root non-empty, visible text rendered. It does NOT trust the absence of console errors; zero errors is not the same as working. --poison for the negative test.", + "check:gospel-all": "EVERYTHING gospel-resources owns, in one command. Its ten gates were all hand-run, which is how a dead bundle shipped: a duplicate module killed all five leaves and the one gate run that day was pointed at the map. check:no-orphan-gates was right that they were reachable — reachable is not run. Deliberately NOT in gates:pre/post: three need a browser, the shared wave holds browser gates under load, and a bundle owner should not be able to slow everyone else's build by adding to a list they do not own. Static gates run first and stop the run on failure, so a scarce browser slot is never spent confirming a table is inconsistent. Exit 75 = NOT MEASURED, never folded into a pass.", + "check:no-conflict-markers": "Nothing tracked may carry an unresolved merge. Exists because a merge from this worktree committed a BUILT bundle with 21 conflict markers - both sides concatenated, 1.13MB - and every gospel leaf rendered dead while the SRI hash matched and every request returned 200. The cause was a landing script that resolved one expected file then ran \"git add -A public\", staging every other file including the ones still holding markers. For a built file: take either side, REBUILD, commit the rebuild, then run this before pushing. --poison for the negative test." }, "devDependencies": { "@capacitor/android": "^7.6.9", diff --git a/public/doxa-maps/assets/tables/doxa-people-groups.json b/public/doxa-maps/assets/tables/doxa-people-groups.json index c674d3bc8..62b8b1cc1 100644 --- a/public/doxa-maps/assets/tables/doxa-people-groups.json +++ b/public/doxa-maps/assets/tables/doxa-people-groups.json @@ -4991,7 +4991,7 @@ "religion": "Islam - Sunni", "population": 5500, "people_praying": 0, - "people_committed": 0, + "people_committed": 1, "adopted_by_churches": 0, "mapped": true, "matched_by": "doxa detail", @@ -16751,7 +16751,7 @@ "religion": "Islam - Sunni", "population": 57500, "people_praying": 2, - "people_committed": 0, + "people_committed": 1, "adopted_by_churches": 0, "mapped": true, "matched_by": "doxa detail", @@ -34055,7 +34055,7 @@ "religion": "Hinduism", "population": 700, "people_praying": 1, - "people_committed": 2, + "people_committed": 3, "adopted_by_churches": 0, "mapped": true, "matched_by": "doxa detail", @@ -34319,7 +34319,7 @@ "religion": "Islam - Folk", "population": 12000, "people_praying": 1, - "people_committed": 4, + "people_committed": 3, "adopted_by_churches": 0, "mapped": true, "matched_by": "doxa detail", @@ -43463,7 +43463,7 @@ "religion": "Islam - Sunni", "population": 23000, "people_praying": 0, - "people_committed": 1, + "people_committed": 2, "adopted_by_churches": 0, "mapped": true, "matched_by": "doxa detail", diff --git a/public/doxa-maps/assets/tables/gospel-link-verification.json b/public/doxa-maps/assets/tables/gospel-link-verification.json index 6750f6f69..da049f9d1 100644 --- a/public/doxa-maps/assets/tables/gospel-link-verification.json +++ b/public/doxa-maps/assets/tables/gospel-link-verification.json @@ -1,7 +1,7 @@ { "meta": { "title": "Gospel resource link verification", - "generated": "2026-09-25T03:43:08.467Z", + "generated": "2026-09-25T09:56:59.860Z", "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": 8516, - "checkedThisPass": 3, + "records": 8524, + "checkedThisPass": 8, "carriedForward": 74, "pruned": 0, "broken": 74, - "verified": 8437, + "verified": 8445, "unknown": 5 }, "byHost": { @@ -142,7 +142,7 @@ "verified": 921 }, "fivefish.org": { - "verified": 1022 + "verified": 1030 } } }, @@ -42726,6 +42726,46 @@ "state": "verified", "detail": "HTTP 200 (via https://www.jesusfilm.org/watch/the-savior.html/arabic-najdi.html)", "at": "2026-09-25T03:43:07.944Z" + }, + "https://fivefish.org/lma": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/12954?language=Limba%2C%20East (the site answers 200 for any path)", + "at": "2026-09-25T09:55:49.572Z" + }, + "https://fivefish.org/nev": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/23092?language=Nyaheun (the site answers 200 for any path)", + "at": "2026-09-25T09:55:59.632Z" + }, + "https://fivefish.org/ngt": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/23045?language=Kriang (the site answers 200 for any path)", + "at": "2026-09-25T09:56:09.574Z" + }, + "https://fivefish.org/alk": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/23050?language=Alak (the site answers 200 for any path)", + "at": "2026-09-25T09:56:19.687Z" + }, + "https://fivefish.org/nos": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/4976?language=Nisu%2C%20Eastern (the site answers 200 for any path)", + "at": "2026-09-25T09:56:29.456Z" + }, + "https://fivefish.org/lbo": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/12797?language=Laven (the site answers 200 for any path)", + "at": "2026-09-25T09:56:39.531Z" + }, + "https://fivefish.org/jle": { + "state": "verified", + "detail": "5fish redirected to its language page https://fivefish.org/14912?language=Ngile (the site answers 200 for any path)", + "at": "2026-09-25T09:56:49.486Z" + }, + "https://fivefish.org/psi": { + "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" } } }