diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index a3643970b..d2347fa11 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -16,9 +16,19 @@ # today: every real build is serialized only by the local flock in # internal/build-lock.sh. Do not rely on this file to prevent a concurrent-build # race until a runner is registered AND a run is observed in the Actions tab. -# (The actions/runners endpoints 404 on this Forgejo version, so runner state was -# not directly measurable — the 0-runs figure is the load-bearing fact, and it -# holds regardless: 138 pushes with a working runner would have produced runs.) +# RE-VERIFIED 2026-09-24: still total_count 0. The 0-runs figure is the +# load-bearing fact and it holds — pushes with a working runner would have +# produced runs. +# ⚠ CORRECTION to what this header used to say. It claimed "the actions/runners +# endpoints 404 on this Forgejo version, so runner state was not directly +# measurable". Only the LISTING endpoint 404s. Both registration-token endpoints +# return 200 and issue a 40-char token on demand (measured 2026-09-24): +# /api/v1/repos/doxa-life/doxa-maps/actions/runners/registration-token +# /api/v1/admin/runners/registration-token +# So registering a runner is API-reachable, not a blocked unknown — which matters, +# because "not measurable" reads as "nothing to try" and that is why this sat. +# The step-by-step is in the handoff doc cited below, which EXISTS as of +# 2026-09-24: it was referenced twice here and had never been written. # # THE CONTRACT for a swarm agent: # edit files -> git commit -> git push (DONE. never run `bun run build`.) @@ -83,6 +93,21 @@ jobs: - name: Install dependencies (locked) run: bun install --frozen-lockfile + # ★ BUILD ONE PACKET BEFORE THE GATES, so check:packets-all has something to check. + # library/scripts/posters/out/ is gitignored — both the records cache and every + # generated packet — so a clean CI checkout had NONE, that sweep SKIPped on every + # single build, and every packet assertion in this tree (the 12 prayer points, the + # wheel's 12 labels, page 1's geography, the print layer) was never once evaluated + # here. A SKIP that is always taken is indistinguishable from an unwired gate. + # It builds from a 20KB COMMITTED fixture: no API, no network, no photo fetch. The + # packet stamps itself "fixture nomap" (CI has no poster sources either), which the + # gate reads and accepts as declared — while still asserting everything that does + # not depend on live data. It runs BEFORE build:unlocked because gates:post is + # inside that script. + - name: Build one packet from the committed fixture + run: bun run packet:ci + + # THE AUTHORITATIVE BUILD. build:unlocked is the real chain (clean -> token # -> per-bundle vite -> tree -> drop/parse checks -> verify -> gallery). We # call :unlocked, NOT `bun run build`, because CI IS the single builder now — diff --git a/documentation/README-SECTION-3-cli-commands.md b/documentation/README-SECTION-3-cli-commands.md index d144255e3..d49c35161 100644 --- a/documentation/README-SECTION-3-cli-commands.md +++ b/documentation/README-SECTION-3-cli-commands.md @@ -49,7 +49,7 @@ The scripts these commands are grounded in (from this branch's `package.json`): "build": "bash internal/build-lock.sh", "build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery", "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:wp-embed check:doc-scripts", -"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars", +"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source", "build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time "preview": "bun x vite preview", "serve": "node internal/serve-strict.mjs", // 404s a directory, like production diff --git a/internal/check-legend-matches-source.mjs b/internal/check-legend-matches-source.mjs index 90e639c55..c556baf78 100644 --- a/internal/check-legend-matches-source.mjs +++ b/internal/check-legend-matches-source.mjs @@ -46,9 +46,25 @@ const UA = 'Mozilla/5.0 (compatible; doxa-maps-gate/1.0)' // the number ONLY inside explanatory comments — no shipping code hardcodes a corpus total. Every // legend and KPI computes from the rows it loaded. The corpus moving 2107 -> 2108 therefore changes // what the maps DISPLAY automatically, and only this baseline lagged. +// Re-measured 2026-09-24 (THIRD reading). The corpus has now moved 2107 -> 2108 -> 2107, and one +// group crossed from Unengaged to Engaged: 2081/27 -> 2079/28. Sums check (2079 + 28 = 2107). +// +// ★ THE SAME SAFETY CHECK WAS REPEATED, not assumed from last time. The gate's own failure text +// forbids updating this baseline alone, because that silences the check while leaving the maps +// wrong. So the maps were checked FIRST: grepping 2107/2108/2,107/2,108/2081/2079 across +// plugin-designs/ and library/ finds them ONLY inside explanatory comments — no shipping code +// hardcodes a corpus total, every legend and KPI computes from the rows it loaded. The corpus +// figure was also confirmed INDEPENDENTLY against the live endpoint (2107 rows) rather than +// taken from this gate's own reading. +// +// ⚠ THIS DRIFTS, AND THAT IS THE POINT. Three readings in two days, moving in both directions. +// A failure here is normally UPSTREAM DATA MOVING, not a regression — but it must still be seen, +// because the one thing it would also catch is a legend that stopped computing and started +// asserting. Do not widen this into a range to stop the noise: the noise IS the signal that the +// numbers on the page are live. const EXPECT = { - total: 2108, - engagement: { Unengaged: 2081, Engaged: 27 }, + total: 2107, + engagement: { Unengaged: 2079, Engaged: 28 }, adoptedPct: 1, } diff --git a/internal/check-packet-browser.mjs b/internal/check-packet-browser.mjs index 1d8eb4912..85042baf8 100644 --- a/internal/check-packet-browser.mjs +++ b/internal/check-packet-browser.mjs @@ -37,10 +37,26 @@ const STRINGS = `${ORIGIN}/library/scripts/posters/packet-strings.json` // page actually loaded rather than a second copy. const STRINGS_PATH = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'library/scripts/posters/packet-strings.json') +// The POSTER zone whose map page 1 shows. ISO3 selects the people groups; the poster is +// keyed by ZONE id, and they are not the same string (ISO3 'LAO' vs zone 'laos'). +const ZONE = process.env.PACKET_ZONE || 'laos' const LANG = process.argv[2] || 'en' -await pg.goto(ORIGIN + '/') -const html = await pg.evaluate(async ({RENDER, STRINGS, LANG, ORIGIN, API, ISO3}) => { +// ⚠ SKIP CLEANLY WHEN THE PRECONDITION IS ABSENT. This needs a static server over the +// REPO ROOT and a reachable records API; without either it threw an unhandled navigation +// error and dumped a stack trace, which is why it could never join a wave — a gate that +// goes red with a stack on a machine that simply has no server running only teaches +// people to disable it. Absence of a server is not a product failure; say so and exit 0. +try { + await pg.goto(ORIGIN + '/', { timeout: 8000 }) +} catch { + await b.close() + console.log(`PACKET-BROWSER: SKIP — nothing served at ${ORIGIN}.`) + console.log(' This gate exercises the BROWSER export path, so it needs the repo root served:') + console.log(' python3 -m http.server 4898 (or set PACKET_ORIGIN)') + process.exit(0) +} +const html = await pg.evaluate(async ({RENDER, STRINGS, LANG, ORIGIN, API, ISO3, ZONE}) => { const [{ createPacketRenderer }, strings, norm] = await Promise.all([ import(RENDER), fetch(STRINGS).then(r=>r.json()), @@ -55,8 +71,24 @@ const html = await pg.evaluate(async ({RENDER, STRINGS, LANG, ORIGIN, API, ISO3} const groups = recs.filter(r=>String(r.country_code)===ISO3) .sort((a,b)=>String(a.name).localeCompare(String(b.name), LANG)) const photos = new Map(groups.filter(g=>g.image_url).map(g=>[g.slug,g.image_url])) - return R.buildPacket({id:ISO3.toLowerCase(),label:ISO3}, groups, photos, '') -}, {RENDER, STRINGS, LANG, ORIGIN, API, ISO3}) + // ⚠ FETCH THE MAP THE WAY THE PRODUCT DOES. This passed '' for mapSvg, so the + // browser packet had NO page-1 map and any geography assertion would have been + // measuring the HARNESS's shortcut rather than the product. The real export path + // (upg-100-list.vue:packetMapSvg) fetches posters/.html and lifts its , + // returning '' on failure — a legitimate degraded state, the browser twin of the + // build-time `nomap`. Exercise that code shape here instead of skipping it. + let mapSvg = '' + try { + const html = await (await fetch(`${ORIGIN}/library/scripts/posters/out/posters/${ZONE}.html`)).text() + const m = html.match(//) + if (m) { + mapSvg = `` + + `
${m[0]}
` + } + } catch { /* no poster generated — page 1 renders without its map, as in the product */ } + return R.buildPacket({id:ISO3.toLowerCase(),label:ISO3}, groups, photos, mapSvg) +}, {RENDER, STRINGS, LANG, ORIGIN, API, ISO3, ZONE}) await pg.setContent(html, { waitUntil:'load' }) await pg.waitForTimeout(3000) @@ -76,6 +108,40 @@ const r = await pg.evaluate(()=>({ // on a CORRECTLY translated packet (the same trap already fixed in // check-packet-render). mark5Len: (document.querySelector('.p4-passage')||{textContent:''}).textContent.trim().length, + // ★ THE SAME PACKET, EXPORTED THROUGH A DIFFERENT PATH, WAS HELD TO A WEAKER STANDARD. + // check-packet-render gained these over several ticks — the 12 points, the wheel's 12 + // LABELS (not just its 12 segments), page 1's geography — each found by mutation, each + // catching a real "looks fine, is empty" shape. None of them existed here, and THIS is + // the packet a reader actually gets: the legend's Export > Prayer packet action. The + // build-time file is the one nobody clicks. + // Counted the same way as the build-time gate, deliberately, so the two cannot disagree. + points: (() => { + const t = document.querySelector('.points-title') + const host = t && t.parentElement && t.parentElement.parentElement + if (!host) return -1 + return (host.innerText || '').split('\n').filter((l) => /^\s*\d{1,2}\.\s/.test(l)).length + })(), + // A segment without its word is a colour. `order` renders 12 from 12 keys, but `praise` + // and `praise_end` collapse to one WORD in English — so count ELEMENTS, never distinct + // words, or the expected number changes per language. + wheelLabels: (() => { + const t = document.querySelector('.wheel-title') + const sheet = t && t.closest('.sheet') + const svg = sheet && sheet.querySelector('svg') + if (!svg) return -1 + return [...svg.querySelectorAll('text')] + .map((e) => (e.textContent || '').trim()) + .filter((s) => s && !/^\d+$/.test(s) && !/^\d[\s\S]*×\s*12$/.test(s)).length + })(), + // Page 1 is the map one-pager. Palette-resolves and not-black are BOTH satisfied by a + // map with zero country shapes, so count the coastlines: paths whose `d` is long enough + // to be geography rather than an icon. + mapGeoPaths: (() => { + const sheet0 = document.querySelector('.sheet') + const svg = sheet0 && sheet0.querySelector('svg') + if (!svg) return -1 + return [...svg.querySelectorAll('path')].filter((e) => (e.getAttribute('d') || '').length > 200).length + })(), metacamp: (document.body.innerText.match(/metacamp\.org/g)||[]).length, firstCard: (document.querySelector('.card')||{}).innerText||'', wheelTitle: (document.querySelector('.wheel-title')||{textContent:''}).textContent.trim(), @@ -85,12 +151,23 @@ const pdf = await pg.pdf({ preferCSSPageSize:true }) const t = pdf.toString('latin1') const pages=(t.match(/\/Type\s*\/Page[^s]/g)||[]).length const boxes=[...t.matchAll(/MediaBox\s*\[\s*0\s+0\s+([\d.]+)\s+([\d.]+)\s*\]/g)].map(m=>[+m[1],+m[2]]) +// THE PRINT LAYER. This gate already made a PDF and read only its STRUCTURE — page count +// and MediaBox — which eight blank sheets of the right size satisfy perfectly. The packet +// exists to be PRINTED, and a print-only CSS rule can strip content every on-screen check +// still sees. FontFile2 = a face actually embedded, so the text travels with the file. +const pdfFonts=(t.match(/\/FontFile2/g)||[]).length +const pdfBytes=pdf.length await b.close() console.log(` lang : ${LANG}`) console.log(` sheets/PDF pages : ${r.sheets} / ${pages}`) console.log(` people cards : ${r.cards}`) -console.log(` wheel segments : ${r.wheel}`) -console.log(` Mark 5 in full : ${r.mark5}`) +console.log(` prayer points : ${r.points} / 12`) +console.log(` wheel segments : ${r.wheel} (${r.wheelLabels}/12 labelled)`) +console.log(` map (page 1) : ${r.mapGeoPaths} geography paths`) +console.log(` printed artefact : ${(pdfBytes/1024/1024).toFixed(1)}MB, ${pdfFonts} embedded font(s)`) +console.log(` Mark 5 in full : ${r.mark5Len} chars`) // ⚠ mark5Len, not mark5 — the latter is not a key on r and printed "undefined" + // beside a PASS for as long as this gate has existed. The ASSERTION below always read + // the right key, so the requirement was protected; only the display was blind. console.log(` metacamp.org : ${r.metacamp}`) console.log(` page size : ${boxes[0]?boxes[0].join(' x '):'?'} pt`) console.log(` points heading : ${r.pointsTitle.trim()}`) @@ -107,6 +184,37 @@ if (!r.sheets) fails.push('no .sheet elements — the browser packet did not ren if (pages !== r.sheets) fails.push(`${pages} PDF pages for ${r.sheets} sheets — one sheet must be one page`) if (!r.cards) fails.push('no people-group cards') if (r.wheel !== 12) fails.push(`prayer wheel has ${r.wheel} segments, expected 12`) +// THE PRODUCT IS NAMED FOR THIS NUMBER: the page header reads "12 Prayer Points" in every +// locale, so 12 is a contract. The failure it guards is silent — a partial translation once +// rendered THREE under that heading, with nothing blank and no error. +if (r.points !== 12) { + fails.push(r.points === -1 + ? 'no .points-title block — the 12 prayer points page did not render at all' + : `${r.points} prayer point(s) rendered, expected 12`) +} +// A wheel with 12 coloured segments and no words on it is a pie chart. The document-wide +// rhythm vocabulary cannot see this: the SAME words are rendered twice, once per segment +// and once per prayer-point heading, so a whole-document search passes on the points page. +if (r.wheelLabels !== 12) { + fails.push(r.wheelLabels === -1 + ? 'no prayer-wheel svg found — the wheel page did not render' + : `the prayer wheel paints ${r.wheelLabels} segment label(s), expected 12`) +} +// Page 1 is the map one-pager — the first line of the packet spec. +if (r.mapGeoPaths !== -1 && r.mapGeoPaths < 5) { + fails.push(`only ${r.mapGeoPaths} geography path(s) on page 1 — the map one-pager is blank ` + + 'or drew nothing but furniture') +} +// A missing map is a DECLARED degradation here, exactly as PACKET_EXPECT_MAP=0 is for the +// build-time gate: out/posters/ is gitignored, so a checkout that never ran `bun run posters` +// legitimately has none, and the product itself returns '' in that case. +if (r.mapGeoPaths === -1) { + console.log(` map (page 1) : ABSENT — no out/posters/${ZONE}.html (run \`bun run posters\`)`) +} +// The packet is a standalone file meant to print offline, so its text must travel with it. +if (pdfFonts === 0) { + fails.push('the PDF embeds NO fonts (0 /FontFile2) — the text depends on the printer\'s font set') +} if (r.metacamp !== 2) fails.push(`${r.metacamp} metacamp.org attributions, expected exactly 2`) // Mark 5:1-20 in full, by LENGTH rather than an English proper noun. if (r.mark5Len < 1200) { diff --git a/internal/check-packet-render.mjs b/internal/check-packet-render.mjs index 1c450e7b2..f2139d203 100644 --- a/internal/check-packet-render.mjs +++ b/internal/check-packet-render.mjs @@ -346,7 +346,9 @@ if (r.cards && r.imgs + r.nophoto !== r.cards) { } // The packet's own stamp is authoritative; the env var stays as an override for // artefacts generated before data-packet-build existed. -const expectPhotos = process.env.PACKET_EXPECT_PHOTOS !== '0' && !/\bnophotos\b/.test(build) +// `fixture` implies no photos: a fixture packet is built from a committed records subset +// with image_url stripped precisely so CI never fetches ~43 images from someone else's CDN. +const expectPhotos = process.env.PACKET_EXPECT_PHOTOS !== '0' && !/\b(nophotos|fixture)\b/.test(build) if (r.cards && expectPhotos && r.imgs === 0) { fails.push(`${r.cards} people cards and NOT ONE photo — the packet spec is "20 per page ` + 'WITH IMAGES", and every source record carries an image_url, so this is a skipped or ' diff --git a/internal/check-provider-switch.mjs b/internal/check-provider-switch.mjs index 4f4f6cb78..4bd46a4fd 100644 --- a/internal/check-provider-switch.mjs +++ b/internal/check-provider-switch.mjs @@ -18,8 +18,28 @@ import { chromium } from 'playwright' // `new URL(request.url())` below throws and the host tally stays EMPTY while the // gate happily reports 0 mapbox requests and PASSES. Measured: it did exactly // that, hiding 4 real requests to events.mapbox.com. -const LEAF = process.argv[2] -if (!LEAF) { console.error('usage: check-provider-switch.mjs '); process.exit(2) } +// ⚠ SKIP CLEANLY, IN THE FORM THE WAVE RUNNER RECOGNISES. This exited 2 on a missing +// argument, so it could never join a wave: run-parallel grades on the exit code and a +// bare `usage:` line would have turned every server-less build RED. It also had no +// default, so it only ever ran when somebody typed a URL — the same present-but-never- +// executed shape check:packets-all had. Default to the upg-100 leaf (the profile this +// gate was written for) and SKIP with a named verdict when nothing is served there. +const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173') + + '/doxa-maps/my-upg-100-list/upg-100-list/index.html' +const LEAF = process.argv[2] || DEFAULT_LEAF +{ + let reachable = false + try { + const res = await fetch(LEAF, { signal: AbortSignal.timeout(4000) }) + reachable = res.ok + } catch { /* nothing served — that is a precondition, not a product failure */ } + if (!reachable) { + console.log(`PROVIDER-SWITCH: SKIP — nothing served at ${LEAF}.`) + console.log(' Serve the built tree and re-run, e.g.:') + console.log(' python3 -m http.server 4173 --directory public (or set BASE_URL)') + process.exit(0) + } +} async function run(provider) { const b = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] }) diff --git a/internal/generate-packet-fixture.mjs b/internal/generate-packet-fixture.mjs new file mode 100644 index 000000000..b29a0fd1c --- /dev/null +++ b/internal/generate-packet-fixture.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +// generate-packet-fixture — a small COMMITTED records subset, so CI can build a packet. +// +// ★ WHY THIS EXISTS. library/scripts/posters/out/ is gitignored — both the records cache +// and every generated packet — so on a clean checkout check:packets-all finds nothing and +// SKIPs. MEASURED 2026-09-24: CI runs that sweep on every build through gates:post, and it +// has SKIPped every time. Every packet assertion in this tree — the 12 prayer points, the +// wheel's 12 labels, page 1's geography paths, the print layer, the build stamp — has never +// once been evaluated in CI. A SKIP that is always taken is indistinguishable from an +// unwired gate, and the wave-membership census cannot tell them apart because membership +// measures INVOCATION, not EVALUATION. +// +// ★ WHY A FIXTURE RATHER THAN A LIVE FETCH. Building a real packet needs 2,107 records +// (887KB, live data that goes stale) and, with photos, ~95 CDN requests. Neither belongs in +// every CI run. This carries ONE zone's records — the smallest, Tibet at 43 slugs, 20KB — +// with image_url stripped so it can never reach for a photo. +// +// ★ AND IT IS DERIVED, NOT HAND-MAINTAINED. Membership comes from clusters.json, which IS +// committed, so this can be regenerated and content-compared (check:packet-fixture) exactly +// like the capability-exemplar table. A fixture nobody can verify is a fixture that rots. +// +// ⚠ IT IS NOT A SUBSTITUTE FOR REAL PACKETS. PACKET_RECORDS is opt-in and never the default: +// a fixture packet proves the RENDERER and the GATE work end to end, not that today's live +// data is sound. The 11-locale sweep against real records stays the real verification. +// +// Usage: node internal/generate-packet-fixture.mjs [--check] +// (reads the live records cache; run after a normal `node library/scripts/posters/packet.mjs`) +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const CACHE = join(ROOT, 'library/scripts/posters/out/_records.json') +const CLUSTERS = join(ROOT, 'plugin-designs/my-upg-100-list/data/clusters.json') +const OUT_DIR = join(ROOT, 'library/scripts/posters/fixtures') +const OUT = join(OUT_DIR, 'records-tibet.json') +// The smallest zone by slug count, so the fixture stays small. Its membership lives in +// clusters.json; if that zone is ever renamed this script fails loudly rather than +// silently emitting an empty fixture. +const ZONE = 'asia-china-tibet-autonomous-region' + +const CHECK = process.argv.includes('--check') + +if (!existsSync(CLUSTERS)) { + console.log('PACKET-FIXTURE: SKIP — no clusters.json.') + process.exit(0) +} +const clusters = JSON.parse(readFileSync(CLUSTERS, 'utf8')) +const zones = [...(clusters.lists || []), ...(clusters.asia_lists || []), ...(clusters.uncovered_lists || [])] +const zone = zones.find((z) => z && z.id === ZONE) +if (!zone || !zone.selector || zone.selector.type !== 'pg_slugs') { + console.error(`PACKET-FIXTURE: FAIL — ${ZONE} is not a pg_slugs zone in clusters.json.`) + console.error(' The fixture is keyed to that zone; pick another and update ZONE here.') + process.exit(1) +} +const want = new Set(zone.selector.values || []) + +// ── CHECK MODE: the committed fixture must still match clusters.json ──────── +if (CHECK) { + if (!existsSync(OUT)) { + console.error(`PACKET-FIXTURE: FAIL — ${OUT.slice(ROOT.length + 1)} does not exist.`) + console.error(' Fix: node internal/generate-packet-fixture.mjs (then commit it)') + process.exit(1) + } + const have = JSON.parse(readFileSync(OUT, 'utf8')) + const haveSlugs = new Set(have.map((r) => r.slug)) + const missing = [...want].filter((s) => !haveSlugs.has(s)) + const extra = [...haveSlugs].filter((s) => !want.has(s)) + const withPhotos = have.filter((r) => r.image_url).length + console.log(`PACKET-FIXTURE: ${have.length} record(s) for ${ZONE}.`) + const fails = [] + if (missing.length) fails.push(`${missing.length} slug(s) in clusters.json are absent: ${missing.slice(0, 4).join(', ')}`) + if (extra.length) fails.push(`${extra.length} record(s) are no longer in the zone: ${extra.slice(0, 4).join(', ')}`) + // image_url must stay stripped, or CI starts fetching ~43 photos from someone else's CDN. + if (withPhotos) fails.push(`${withPhotos} record(s) still carry image_url — the fixture must never fetch photos`) + if (fails.length) { + console.error(`\nPACKET-FIXTURE: FAIL — ${fails.length} problem(s)`) + for (const f of fails) console.error(' - ' + f) + console.error('\n Fix: node internal/generate-packet-fixture.mjs (then commit it)') + process.exit(1) + } + console.log('PACKET-FIXTURE: PASS — the committed fixture matches clusters.json and fetches no photos.') + process.exit(0) +} + +// ── GENERATE ──────────────────────────────────────────────────────────────── +if (!existsSync(CACHE)) { + console.error('PACKET-FIXTURE: FAIL — no records cache to derive from.') + console.error(` Expected ${CACHE.slice(ROOT.length + 1)}.`) + console.error(' Run a normal packet build first: node library/scripts/posters/packet.mjs') + process.exit(1) +} +const recs = JSON.parse(readFileSync(CACHE, 'utf8')) +const subset = (Array.isArray(recs) ? recs : recs.records || []).filter((r) => r && want.has(r.slug)) +if (subset.length !== want.size) { + console.error(`PACKET-FIXTURE: FAIL — matched ${subset.length} of ${want.size} slug(s) in the cache.`) + console.error(' The cache is stale or partial; re-run a full packet build and try again.') + process.exit(1) +} +// STRIP image_url. A fixture that carries photo URLs would make CI fetch ~43 images from +// Joshua Project's CDN on every build, which is both slow and rude; the packet degrades to +// typographic cards, which is exactly what a fixture should exercise. +const lean = subset.map((r) => { + const c = { ...r } + delete c.image_url + c.has_photo = false + return c +}) +mkdirSync(OUT_DIR, { recursive: true }) +writeFileSync(OUT, JSON.stringify(lean)) +const kb = (JSON.stringify(lean).length / 1024).toFixed(0) +console.log(`[packet-fixture] wrote ${OUT.slice(ROOT.length + 1)} — ${lean.length} records for ${ZONE}, ${kb}KB`) diff --git a/internal/run-parallel.mjs b/internal/run-parallel.mjs index 8af6f2c5f..dff3275d5 100644 --- a/internal/run-parallel.mjs +++ b/internal/run-parallel.mjs @@ -133,10 +133,20 @@ async function main() { if (!step) return const r = await runStep(step) results.push(r) + // ★ A SKIP IS NOT A PASS, AND THIS PRINTED THEM IDENTICALLY. The grade is the exit + // code, and a gate that declines to evaluate exits 0 — so "35/35 passed" read the + // same whether 35 gates graded something or 17 did. MEASURED 2026-09-24: 18 of the + // 35 steps in gates:post CAN skip, every one of them on a precondition (a served + // tree, generated posters, a built manifest) that a fresh checkout does not have. + // check:packets-all SKIPped on every CI build for exactly this reason and nobody + // could see it, because the wave reported it green. An always-taken SKIP is + // indistinguishable from an unwired gate unless the runner says so. + const skipped = /(^|\n)\s*[A-Z][A-Z0-9-]*:\s*SKIP\b/.test(r.out) console.log( - ` ${r.code === 0 ? '✓' : '✗'} ${r.name.padEnd(26)} ${String(r.ms).padStart(7)}ms` + + ` ${r.code !== 0 ? '✗' : skipped ? '⊘' : '✓'} ${r.name.padEnd(26)} ${String(r.ms).padStart(7)}ms` + (r.code === 0 ? '' : ` exit ${r.code}`) ) + r.skipped = skipped } } await Promise.all(Array.from({ length: Math.min(CONCURRENCY, steps.length) }, worker)) @@ -150,10 +160,20 @@ async function main() { // a truncated explanation is how a real defect gets re-diagnosed as a flake. else for (const r of failed) console.log(`\n───── FAILED: ${r.name} (exit ${r.code}) ─────\n${r.out}`) + // COUNT THE SKIPS IN THE SUMMARY, because the summary is the line a reader quotes and + // the one that lands in a report. "35/35 passed" was true of a wave where a third of the + // gates declined to look at anything. + const skips = results.filter((r) => r.skipped) console.log( - `\n▶ ${LABEL}: ${results.length - failed.length}/${results.length} passed in ${(wall / 1000).toFixed(1)}s wall ` + + `\n▶ ${LABEL}: ${results.length - failed.length - skips.length}/${results.length} passed` + + (skips.length ? `, ${skips.length} SKIPPED` : '') + + ` in ${(wall / 1000).toFixed(1)}s wall ` + `(${(serial / 1000).toFixed(1)}s of work — ${(serial / Math.max(wall, 1)).toFixed(1)}x)` ) + if (skips.length) { + console.log(` ⊘ evaluated nothing: ${skips.map((s) => s.name).join(', ')}`) + console.log(' Each declined on a precondition it names in its own output (run it alone to see).') + } if (failed.length) { console.error(`✗ ${LABEL}: ${failed.length} step(s) FAILED — ${failed.map((f) => f.name).join(', ')}`) process.exit(1) diff --git a/library/scripts/posters/fixtures/records-tibet.json b/library/scripts/posters/fixtures/records-tibet.json new file mode 100644 index 000000000..c780d503f --- /dev/null +++ b/library/scripts/posters/fixtures/records-tibet.json @@ -0,0 +1 @@ +[{"slug":"adi","name":"Adi","country_code":"CHN","population":"1400","religion":"E","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://flickr.com/photos/ramesh_lalwani/8169468855/","text":"Ramesh Lalwani - Flickr"}],"latitude":"29.24961200","longitude":"95.17182400","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion"},{"slug":"ainu-china","name":"Ainu","country_code":"CHN","population":"11000","religion":"MSN","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"38.93020500","longitude":"76.16959600","country_code_label":"People's Republic of China","religion_label":"Islam - Sunni"},{"slug":"alxa-mongols","name":"Alxa Mongols","country_code":"CHN","population":"35500","religion":"MSN","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"39.20798700","longitude":"101.67522000","country_code_label":"People's Republic of China","religion_label":"Islam - Sunni"},{"slug":"bogar-lhoba","name":"Bogar Lhoba","country_code":"CHN","population":"5300","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By bojum"}],"latitude":"28.74470000","longitude":"93.64580000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"bogol","name":"Bogol","country_code":"CHN","population":"1400","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"48.46892600","longitude":"124.48821300","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"boyu-tibetans","name":"Boyu Tibetans","country_code":"CHN","population":"5400","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"33.36236100","longitude":"104.58044400","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"bunan","name":"Bunan","country_code":"CHN","population":"2500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"31.79607000","longitude":"78.90344000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"buryat","name":"Buryat","country_code":"CHN","population":"170000","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://commons.wikimedia.org/wiki/File:Me_and_a_Buryat.jpg","text":"Telaneo - Wikimedia"}],"latitude":"48.75926000","longitude":"117.78630000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"butuo-nosu","name":"Butuo Nosu","country_code":"CHN","population":"337000","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Anonymous"}],"latitude":"27.60452900","longitude":"102.87518600","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"cona-monba","name":"Cona Monba","country_code":"CHN","population":"58500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of Joshua Project by Anonymous"}],"latitude":"27.89404900","longitude":"92.20691400","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"darang-deng","name":"Darang Deng","country_code":"CHN","population":"12500","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"28.37980000","longitude":"97.02930000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"enger-yugur","name":"Enger Yugur","country_code":"CHN","population":"5500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://flickr.com/photos/mke1963/154213183/","text":"Mark mke1963 - Flickr"}],"latitude":"37.98815000","longitude":"101.76214000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"ersu","name":"Ersu","country_code":"CHN","population":"48000","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"29.23558500","longitude":"102.35591600","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"geman-deng","name":"Geman Deng","country_code":"CHN","population":"16500","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"28.47324000","longitude":"97.04437000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"heishui","name":"Heishui","country_code":"CHN","population":"48500","religion":"BLA","has_photo":false,"picture_credit":null,"latitude":"32.19164300","longitude":"103.03860100","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"henan-mongols","name":"Henan Mongols","country_code":"CHN","population":"43000","religion":"BLA","has_photo":false,"picture_credit":null,"latitude":"34.73230000","longitude":"101.60860000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"keji","name":"Keji","country_code":"CHN","population":"2600","religion":"MSN","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.paulnoll.com/China/Minorities/China-Nationalities.html","text":"Paul Noll"}],"latitude":"29.65355100","longitude":"91.12095900","country_code_label":"People's Republic of China","religion_label":"Islam - Sunni"},{"slug":"kyrgyz","name":"Kyrgyz","country_code":"CHN","population":"195000","religion":"MOF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://commons.wikimedia.org/wiki/File:Kyrgyz_women_offering_butter_and_salt.jpg","text":"Theklan - Wikimedia"}],"latitude":"38.01774000","longitude":"76.09630000","country_code_label":"People's Republic of China","religion_label":"Islam - Folk"},{"slug":"luzu","name":"Luzu","country_code":"CHN","population":"1600","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"28.33754000","longitude":"101.35823900","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"manyak","name":"Manyak","country_code":"CHN","population":"3000","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"29.67090000","longitude":"101.59510000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"medog-monba","name":"Medog Monba","country_code":"CHN","population":"11000","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"29.46300000","longitude":"95.74060000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"miguba","name":"Miguba","country_code":"CHN","population":"100","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"29.29621200","longitude":"95.31208900","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"ming","name":"Ming","country_code":"CHN","population":"15500","religion":"NAT","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"31.53976000","longitude":"103.68679500","country_code_label":"People's Republic of China","religion_label":"Unaffiliated - Atheism"},{"slug":"minyak","name":"Minyak","country_code":"CHN","population":"35000","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.pesquisas.org.br/","text":"COMIBAM / Sepal"}],"latitude":"29.65400600","longitude":"101.52975300","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"naju","name":"Naju","country_code":"CHN","population":"2500","religion":"EAN","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"27.67805600","longitude":"101.46272000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Animism"},{"slug":"namuyi","name":"Namuyi","country_code":"CHN","population":"11500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"28.59085000","longitude":"101.89760000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"nghari-tibetans","name":"Nghari Tibetans","country_code":"CHN","population":"82500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Peoples of the Buddhist World, Asia Harvest"}],"latitude":"30.97696000","longitude":"81.21100500","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"olot","name":"Olot","country_code":"CHN","population":"2400","religion":"ESM","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"47.87897700","longitude":"124.33623400","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Shamanism"},{"slug":"ongkor","name":"Ongkor","country_code":"CHN","population":"25","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://english.cri.cn/811/2005/08/24/48@13403.htm","text":"CRIENGLISH"}],"latitude":"43.98811800","longitude":"81.23009100","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"puroik","name":"Puroik","country_code":"CHN","population":"450","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Anonymous"}],"latitude":"28.42033000","longitude":"93.10505600","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"saman","name":"Saman","country_code":"CHN","population":"2600","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"47.46389900","longitude":"124.11668900","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"saragh-yugur","name":"Saragh Yugur","country_code":"CHN","population":"10500","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"38.97160000","longitude":"99.50310000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"sarikoli-tajiks","name":"Sarikoli Tajiks","country_code":"CHN","population":"20500","religion":"MIS","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Anonymous"}],"latitude":"37.90501000","longitude":"75.14486000","country_code_label":"People's Republic of China","religion_label":"Islam - Shia Imami Ismaili"},{"slug":"shixing","name":"Shixing","country_code":"CHN","population":"4100","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Christopher Timmons"}],"latitude":"28.28800000","longitude":"100.65700000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"suodi-nosu","name":"Suodi Nosu","country_code":"CHN","population":"305000","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"26.86570200","longitude":"102.37629200","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"tongren-bonan","name":"Tongren Bonan","country_code":"CHN","population":"8900","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://flickr.com/photos/ding_zhou/15612978268/","text":"Joe Tym - Flickr"}],"latitude":"35.14500000","longitude":"102.18300000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"uzbeks","name":"Uzbeks","country_code":"CHN","population":"12500","religion":"MSN","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://commons.wikimedia.org/wiki/File:Uzbek_people_(4934182297).jpg","text":"Fulvio Spada - Wikimedia"}],"latitude":"44.14145000","longitude":"90.29497000","country_code_label":"People's Republic of China","religion_label":"Islam - Sunni"},{"slug":"wakhi-china","name":"Wakhi","country_code":"CHN","population":"9600","religion":"MSH","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://flickr.com/photos/100415065@N06/50312622062","text":"Robertino Radovix - Flickr"}],"latitude":"37.38000000","longitude":"75.40200000","country_code_label":"People's Republic of China","religion_label":"Islam - Shia"},{"slug":"western-xibe","name":"Western Xibe","country_code":"CHN","population":"35500","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Epel - Shutterstock"}],"latitude":"43.80236300","longitude":"80.79151000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"wutun","name":"Wutun","country_code":"CHN","population":"3800","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"35.57300000","longitude":"102.05400000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"},{"slug":"yidu-lhoba","name":"Yidu Lhoba","country_code":"CHN","population":"12000","religion":"ESM","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Mow Sunil"}],"latitude":"28.97230000","longitude":"96.63810000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Shamanism"},{"slug":"yinuo-nosu","name":"Yinuo Nosu","country_code":"CHN","population":"810000","religion":"ECF","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By Anonymous"}],"latitude":"28.04378200","longitude":"102.44427000","country_code_label":"People's Republic of China","religion_label":"Ethnoreligion - Chinese Folk Religion"},{"slug":"za","name":"Za","country_code":"CHN","population":"3400","religion":"BLA","has_photo":false,"picture_credit":[{"link":null,"text":"Photo courtesy of "},{"link":"https://www.joshuaproject.net","text":"Joshua Project"},{"link":null,"text":". By "},{"link":"https://www.asiaharvest.org","text":"Operation China, Asia Harvest"}],"latitude":"28.47990000","longitude":"97.07920000","country_code_label":"People's Republic of China","religion_label":"Buddhism - Tibetan"}] \ No newline at end of file diff --git a/library/scripts/posters/packet.mjs b/library/scripts/posters/packet.mjs index 1699f5a57..f51825f17 100644 --- a/library/scripts/posters/packet.mjs +++ b/library/scripts/posters/packet.mjs @@ -52,8 +52,14 @@ const LANG = process.env.PACKET_LANG || 'en'; const ONLY = process.env.ONLY || ''; const NOPHOTOS = process.env.NOPHOTOS === '1'; const API = process.env.VITE_API_BASE_URL || process.env.MAP_APP_API_URL; -if (!API) { - console.error('[packet] No API base — set VITE_API_BASE_URL. Failing closed.'); +// FAIL CLOSED without an API — EXCEPT when records come from a committed fixture, which +// is the one case where no API is needed at all. Measured 2026-09-24: this guard runs +// BEFORE loadRecords() ever looks at PACKET_RECORDS, so `bun run packet:ci` exited 1 on a +// tree that had everything it needed. It only passed in hand-testing because I had been +// supplying a dummy host — the real invocation path is what exposed it. +if (!API && !process.env.PACKET_RECORDS) { + console.error('[packet] No API base — set VITE_API_BASE_URL, or PACKET_RECORDS for a fixture build.'); + console.error(' Failing closed.'); process.exit(1); } @@ -109,6 +115,21 @@ const { evaluate } = await import(join(DATA, 'selector-evaluator.js')); const { normalizeRecords, extractRecords } = await import(join(DATA, 'normalize.js')); async function loadRecords() { + // ★ A COMMITTED FIXTURE, so CI can build a packet at all. out/ is gitignored — both the + // records cache AND every generated packet — so on a clean checkout check:packets-all + // finds nothing and SKIPs. MEASURED 2026-09-24: CI runs the sweep on EVERY build via + // gates:post and it has SKIPped every single time, which means every packet assertion in + // this tree (the 12 points, the wheel labels, page 1's geography, the print layer, the + // build stamp) has never once been evaluated there. A SKIP that is always taken is + // indistinguishable from an unwired gate, and the wave-membership census cannot see the + // difference because membership measures INVOCATION, not EVALUATION. + // PACKET_RECORDS points at a small committed subset instead of the live API or the + // gitignored cache. It is deliberately NOT the default: real packets must use real data. + const fixture = process.env.PACKET_RECORDS; + if (fixture) { + if (!existsSync(fixture)) throw new Error(`PACKET_RECORDS set but missing: ${fixture}`); + return JSON.parse(readFileSync(fixture, 'utf8')); + } const cache = join(__dirname, 'out', '_records.json'); if (process.env.OFFLINE && existsSync(cache)) return JSON.parse(readFileSync(cache, 'utf8')); const fields = 'slug,name,country_code,population,religion,image_url,has_photo,picture_credit,latitude,longitude'; @@ -232,7 +253,13 @@ for (const z of zones) { // session, packet-strings.json was 303s newer than the packets while being byte-identical // to HEAD, because a probe restored it from backup. Record the INTENT, which is a fact // about the build, not the clock. - const provenance = [NOPHOTOS ? 'nophotos' : null, mapMiss ? 'nomap' : null] + // `fixture` is a build fact like the others: a fixture packet is built from a committed + // records subset with image_url stripped, so it legitimately has no photos and its + // records are not today's live data. Without this the gate reports "every source record + // carries an image_url, so this is a skipped or failed photo fetch" — a true sentence + // about real records and a false one about this packet. + const provenance = [process.env.PACKET_RECORDS ? 'fixture' : null, + NOPHOTOS ? 'nophotos' : null, mapMiss ? 'nomap' : null] .filter(Boolean).join(' ') || 'full'; const stamped = html.replace('.html for page 1's map, which `bun run posters` writes — without it the packet still builds and page 1 is simply BLANK, so the script now warns per-zone AND in its summary. PACKET_LANG= for one language, ONLY= for one zone, NOPHOTOS=1 to skip the photo fetch.", + "packet-fixture": "node internal/generate-packet-fixture.mjs # regenerate the committed records subset (smallest zone, image_url stripped) that lets CI build a packet at all — out/ is gitignored, so a clean checkout has neither the records cache nor any packet.", + "check:packet-fixture": "node internal/generate-packet-fixture.mjs --check # the committed fixture must still match clusters.json and must never carry image_url (CI would fetch 43 photos from someone else's CDN).", + "packet:ci": "sh -c 'F=\"$PWD/library/scripts/posters/fixtures/records-tibet.json\"; PACKET_RECORDS=\"$F\" ONLY=asia-china-tibet-autonomous-region node library/scripts/posters/packet.mjs' # build ONE packet from the committed fixture, no network. This is what makes check:packets-all evaluate something in CI instead of SKIPping, as it had on every build.", "token": "node internal/write-token.mjs", "smoke": "node internal/smoke.mjs", "smoke:embeds": "node internal/embed-smoke.mjs", @@ -46,7 +49,7 @@ "deploy:dev": "bun run build && node internal/sync-dropin.mjs --from-config --prune # destination + allowlist come from vite.doxa-maps-build-config.json (dropin.dest / dropin.include); DOXA_DROPIN_DEST= overrides the destination", "check:public-hygiene": "node internal/public-hygiene-check.mjs", "check:label-joins": "node internal/check-label-joins.mjs", - "check:dxlife": "node internal/check-dxlife-untouched.mjs", + "check:dxlife": "node internal/check-dxlife-untouched.mjs # ⚠ NOT WAVE-SAFE, deliberately unwired: exit codes 0/1/2 are a DOCUMENTED contract (0 unchanged · 1 drift · 2 clone not found), and run-parallel grades any non-zero as FAILURE — so a checkout without the sibling /root-dx-life clone goes red on a precondition. Changing the contract could break another consumer; hand-run it, or give it a recognised SKIP first.", "check:facet-cascade": "node internal/check-facet-cascade.mjs # a child facet selection must not outlive its parent: clearing a parent facet must cascade-clear its descendants on EVERY clear path. Asserts on RESULT COUNTS, not DOM — an orphaned child silently ANDs the map to empty. run check:facet-cascade:self-test for the NEGATIVE test (reverts the cascade and requires this gate to FAIL).", "check:facet-cascade:self-test": "node internal/check-facet-cascade.mjs --self-test", "check:facet-one-field": "node internal/check-facet-one-field.mjs # ONE AXIS = ONE CONTROL-REACHABLE FACET. Asserts on the PARTITION each facet induces over the rows, NOT on which r. it reads: three fields (religion, primary_language, rop1) are legitimately read by two facets each as tier-1/tier-2 pairs, so a gate on duplicate field READS would fail correct code. Scoped to facets a user can actually select (rail + dropdowns + charts), because a search-only projection sharing a partition is fine until it becomes a control. run check:facet-one-field:self-test for the NEGATIVE test.", @@ -62,12 +65,12 @@ "check:i18n-parity": "node internal/check-i18n-parity.mjs", "check:locale-reachable": "node internal/check-locale-reachable.mjs", "check:poster-prints": "node internal/check-poster-prints.mjs", - "check:provider-switch": "node internal/check-provider-switch.mjs", + "check:provider-switch": "node internal/check-provider-switch.mjs # the provider switch must actually SWITCH: counts requests BY HOST (not window globals, which stay defined after a swap) and asserts the maplibre run stays within the leaf HTML's own declared api.mapbox.com preloads. Defaults to the upg-100 leaf; pass a URL or set BASE_URL for another. CONDITIONAL: SKIPs when nothing is served.", "check:mfe-two-tab": "node internal/check-mfe-two-tab.mjs", "check:share-policy": "node internal/check-share-policy.mjs", - "check:zone-census": "node internal/check-zone-census.mjs", + "check:zone-census": "node internal/check-zone-census.mjs # every zone a USER CAN SELECT must have a poster (asks the LEGEND, not clusters.json — two collapsed deaf lists are unreachable and not a gap). CONDITIONAL: SKIPs without out/posters/, which is gitignored — run `bun run posters` first. It cannot be fixtured like check:packets-all, because it asserts COVERAGE of real generated output and a fixture would only assert the fixture is complete.", "check:print-geometry": "node internal/check-print-geometry.mjs", - "check:packet-browser": "node internal/check-packet-browser.mjs", + "check:packet-browser": "node internal/check-packet-browser.mjs # the packet the READER gets — the legend's Export > Prayer packet path, proving the shared renderer runs in a BROWSER. Asserts the same 12 points / 12 wheel labels / page-1 geography / print layer as check:packet-render, which it did not until 2026-09-24. CONDITIONAL: SKIPs without a server over the repo root (python3 -m http.server 4898) and a reachable records API.", "check:packet-render": "node internal/check-packet-render.mjs", "check:template-literal": "node internal/template-literal-check.mjs", "verify:map-paint": "node internal/verify-map-paint.mjs", @@ -154,7 +157,7 @@ "check:doc-scripts:self-test": "node internal/check-doc-quotes-scripts.mjs --self-test", "check:legend-source": "node internal/check-legend-matches-source.mjs # the numbers the maps SHOW must match the numbers the live SOURCE has. Every other gate checks INTERNAL consistency — a page can agree with itself perfectly while its legend has stopped describing reality. Offline SKIPs (exit 0); only a reachable endpoint that disagrees fails.", "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:wp-embed check:doc-scripts", - "gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars", + "gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source", "build:bundles": "node internal/run-parallel.mjs --bundles", "check:maplibre-drops": "node internal/check-maplibre-layer-drops.mjs", "check:tabs-localized": "node internal/check-tabs-localized.mjs # a chosen locale must CHANGE the rendered tab text. check:i18n-parity compares catalogs to each OTHER, so it cannot tell a shipped translation from a dead one; tabs.json is reached only via a DYNAMIC key (SemanticTreeLegend.vue:40), invisible to any grep. Renders it and reads the result.", diff --git a/plugin-designs/regions-dashboard/region-dashboards.vue b/plugin-designs/regions-dashboard/region-dashboards.vue index 4b3006e12..1dcf51650 100644 --- a/plugin-designs/regions-dashboard/region-dashboards.vue +++ b/plugin-designs/regions-dashboard/region-dashboards.vue @@ -489,6 +489,22 @@ function writeUrl(push) { if (!URL_SYNC) return const slug = currentSlug() const url = window.location.pathname + window.location.search + (slug ? '#' + encodeURIComponent(slug) : '') + // WRITE ONLY WHEN THE URL WOULD ACTUALLY CHANGE. This is what makes Back and Forward work, and + // it replaces a flag that could not do the job. MEASURED before the fix, drilling + // All Regions -> Africa -> Angola and then pressing Back three times and Forward twice: + // back 1 -> Africa (correct) + // back 2 -> Africa (WRONG, should be All Regions) + // back 3 -> Africa + // forward 1 -> Africa (WRONG, should be Angola) + // history.length stayed 4 throughout: every Back pushed a REPLACEMENT entry, which discards the + // forward stack and pins the position — the classic loop the note below warns about. + // The old guard set applyingHistory=true, assigned sel, then cleared it SYNCHRONOUSLY, while + // the watcher that calls this is { flush: 'post' } and therefore runs after the DOM update, + // by which time the flag was already false. A scheduling-dependent flag cannot guarantee this; + // comparing the URL can. After a popstate the address bar already holds the target, so there is + // nothing to write and no entry is created. + const current = window.location.pathname + window.location.search + window.location.hash + if (url === current) { syncTitle(); return } try { push ? history.pushState({ slug }, '', url) : history.replaceState({ slug }, '', url) } catch (_) {} syncTitle() } @@ -508,20 +524,23 @@ function syncTitle() { try { document.title = t && t !== allLabel ? t + ' — ' + suffix : suffix } catch (_) {} } -// popstate WRITES the selection, so the watcher below must not turn around and push a new -// entry for it — that is the classic history loop where Back appears to do nothing. -let applyingHistory = false +// popstate WRITES the selection, and the watcher below then runs — but it can no longer create an +// entry for it, because writeUrl() refuses to write a URL the address bar already holds. The flag +// that used to guard this is GONE rather than patched: it was cleared synchronously while the +// watcher it guarded is { flush: 'post' }, so it was never in effect when it mattered. function onPopState() { - applyingHistory = true sel.value = selectionForSlug(slugFromHash()) syncTitle() - applyingHistory = false } // One watcher rather than a writeUrl() call at each of the five places a selection is made // (tab-1 event, table row, region dropdown, country dropdown, back-link) — those would drift. watch(() => [sel.value.regionSlug, sel.value.countrySlug], () => { - if (applyingHistory) return + // No guard needed: writeUrl() is idempotent, so the call this watcher makes after a popstate + // writes nothing. (Deleting the declaration while leaving `if (applyingHistory) return` here + // threw a ReferenceError inside the watcher, which silently stopped the URL being written at + // all — the hash never changed and Back left the page. Caught by re-running the measurement + // rather than by trusting the fix.) writeUrl(true) }, { flush: 'post' }) diff --git a/public/doxa-maps/index.html b/public/doxa-maps/index.html index b2dcad309..297e0d19b 100644 --- a/public/doxa-maps/index.html +++ b/public/doxa-maps/index.html @@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut) + window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":22,"profileCount":35,"bundles":[{"bundle":"all-people-groups","scriptUrl":"./all-people-groups/all-people-groups.js","output":"doxa-maps/all-people-groups/all-people-groups.js","integrity":"sha384-3kskcN7GLnCNNOrJINJt4n5ERXa0qpwyRNcuVuw4++2KzU0LwGYnLW3aepEu8jDg","elements":["all-people-groups"],"profiles":[{"profile":"all-people-groups","file":"all-people-groups.vue","description":"All 12,379 people groups (peoplegroups.org) in four colors — reached,","element":"all-people-groups","type":"single","isMap":true,"iframeUrl":"./all-people-groups/all-people-groups/index.html","scriptUrl":"./all-people-groups/all-people-groups.js","embed":{"iframe":"\u003ciframe src=\"./all-people-groups/all-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-3kskcN7GLnCNNOrJINJt4n5ERXa0qpwyRNcuVuw4++2KzU0LwGYnLW3aepEu8jDg\" crossorigin=\"anonymous\">\u003c/script>\n\u003call-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/all-people-groups>"},"bundle":"all-people-groups"}]},{"bundle":"big-picture","scriptUrl":"./big-picture/big-picture.js","output":"doxa-maps/big-picture/big-picture.js","integrity":"sha384-3I1mBK8C0GOMsEJyYTyztMqFnvn4EGg3F2KFhYSVie+o2YKplcSFcOxWI4L2+cmr","elements":["bp-big-picture","bp-research-header","people-groups-grid"],"composes":["bp-research-header","people-groups-grid"],"profiles":[{"profile":"big-picture","file":"big-picture.vue","description":"The Cat-4 composite that PAIRS the two real capability elements of the","element":"bp-big-picture","type":"nested-elements-mfe","isMap":false,"iframeUrl":"./big-picture/big-picture/index.html","scriptUrl":"./big-picture/big-picture.js","embed":{"iframe":"\u003ciframe src=\"./big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./big-picture/big-picture.js\" integrity=\"sha384-3I1mBK8C0GOMsEJyYTyztMqFnvn4EGg3F2KFhYSVie+o2YKplcSFcOxWI4L2+cmr\" crossorigin=\"anonymous\">\u003c/script>\n\u003cbp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/bp-big-picture>"},"instances":[{"id":"big-picture","title":"Big Picture — research header ↔ people-groups grid (composite)","apiBaseUrl":"","props":{"profile":"big-picture","provider":"maplibre","instanceId":"bp-big-picture"},"element":"bp-big-picture","iframeUrl":"./big-picture/big-picture/big-picture/index.html","embed":{"iframe":"\u003ciframe src=\"./big-picture/big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"frontier","title":"Big Picture — frontier accent","apiBaseUrl":"","props":{"profile":"big-picture","provider":"maplibre","instanceId":"bp-big-picture-frontier","accent":"#22d3ee","title":"Big Picture — Frontier Focus"},"element":"bp-big-picture","iframeUrl":"./big-picture/big-picture/frontier/index.html","embed":{"iframe":"\u003ciframe src=\"./big-picture/big-picture/frontier/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}],"bundle":"big-picture"}]},{"bundle":"cities-internet","scriptUrl":"./cities-internet/cities-internet.js","output":"doxa-maps/cities-internet/cities-internet.js","integrity":"sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr","elements":["cities-internet-map"],"profiles":[{"profile":"connectivity-list","file":"connectivity-list.vue","description":"Countries where DX people groups live, grouped by region, showing measured internet penetration","element":"cities-internet-map","type":"single","isMap":false,"iframeUrl":"./cities-internet/connectivity-list/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/connectivity-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/cities-internet-map>"}},{"profile":"connectivity-map","file":"connectivity-map.vue","description":"Every country shaded by measured internet penetration, with its named carriers and dominant","element":"cities-internet-map","type":"single","isMap":true,"iframeUrl":"./cities-internet/connectivity-map/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/connectivity-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/cities-internet-map>"}},{"profile":"data-tables","file":"data-tables.vue","description":"Four validated research tables, one tab each, with sortable and filterable columns, CSV and JSON","element":"cities-internet-map","type":"single","isMap":false,"iframeUrl":"./cities-internet/data-tables/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/cities-internet-map>"}}]},{"bundle":"country-posters","scriptUrl":"./country-posters/country-posters.js","output":"doxa-maps/country-posters/country-posters.js","integrity":"sha384-+uSJg/LdhuPzecJG34oeevbtghdX7Pqaop+MyNzs4tyqd3/1frmrwQimIJgcv/yo","elements":["country-posters-map"],"composes":["country-posters/country-poster-wall","country-posters/countries-map"],"profiles":[{"profile":"countries-map","file":"countries-map.vue","description":"The researcher's workbench retargeted from people groups to COUNTRIES — region","element":"country-posters-map","type":"single","isMap":true,"iframeUrl":"./country-posters/countries-map/index.html","scriptUrl":"./country-posters/country-posters.js","embed":{"iframe":"\u003ciframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-+uSJg/LdhuPzecJG34oeevbtghdX7Pqaop+MyNzs4tyqd3/1frmrwQimIJgcv/yo\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/country-posters-map>"}},{"profile":"country-poster-wall","file":"country-poster-wall.vue","description":"One printable card per country, grouped into COLLAPSIBLE regional sections so the","element":"country-posters-map","type":"single","isMap":false,"iframeUrl":"./country-posters/country-poster-wall/index.html","scriptUrl":"./country-posters/country-posters.js","embed":{"iframe":"\u003ciframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-+uSJg/LdhuPzecJG34oeevbtghdX7Pqaop+MyNzs4tyqd3/1frmrwQimIJgcv/yo\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/country-posters-map>"}},{"profile":"country-posters-mfe","file":"country-posters-mfe.vue","description":"Country Posters as ONE two-tab element — the countries map and the printable","element":"country-posters-mfe","type":"nested-elements-mfe","isMap":true,"iframeUrl":"./country-posters/country-posters-mfe/index.html","scriptUrl":"./country-posters/country-posters.js","embed":{"iframe":"\u003ciframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-+uSJg/LdhuPzecJG34oeevbtghdX7Pqaop+MyNzs4tyqd3/1frmrwQimIJgcv/yo\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/country-posters-map>"}}]},{"bundle":"dashboard-plus-grid","scriptUrl":"./dashboard-plus-grid/dashboard-plus-grid.js","output":"doxa-maps/dashboard-plus-grid/dashboard-plus-grid.js","integrity":"sha384-VOkoTUw4VURBoPSbk3YnU3Wc/JuN+ywf1eF9D6RHBI2Be/8N8FlY3WX/2CkNk5NS","elements":["dashboard-plus-grid"],"composes":["research-dashboard","people-groups-grid"],"profiles":[{"profile":"dashboard-plus-grid","file":"dashboard-plus-grid.vue","description":"Both children are IMPORTED DIRECTLY (compiled into this ONE bundle — no runtime bundle","element":"dashboard-plus-grid","type":"nested-elements-mfe","isMap":true,"iframeUrl":"./dashboard-plus-grid/dashboard-plus-grid/index.html","scriptUrl":"./dashboard-plus-grid/dashboard-plus-grid.js","embed":{"iframe":"\u003ciframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-VOkoTUw4VURBoPSbk3YnU3Wc/JuN+ywf1eF9D6RHBI2Be/8N8FlY3WX/2CkNk5NS\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/dashboard-plus-grid>"},"instances":[{"id":"default","title":"default","apiBaseUrl":"","props":{"profile":"dashboard-plus-grid"},"element":"dashboard-plus-grid","iframeUrl":"./dashboard-plus-grid/dashboard-plus-grid/default/index.html","embed":{"iframe":"\u003ciframe src=\"./dashboard-plus-grid/dashboard-plus-grid/default/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]},{"bundle":"demo-map","scriptUrl":"./demo-map/demo-map.js","output":"doxa-maps/demo-map/demo-map.js","integrity":"sha384-WLB+/GTY8u9CWAKt72vKJroKi26oGzzjlGdA0iA3DSSP9oWHnUb8r6jtjAWSw0ee","elements":["demo-map"],"profiles":[{"profile":"demo-map","file":"demo-map.vue","description":"Simple two-tab demo map. Tab 1 colors pins by religion (shared","element":"demo-map","type":"parameterized","isMap":true,"iframeUrl":"./demo-map/demo-map/index.html","scriptUrl":"./demo-map/demo-map.js","embed":{"iframe":"\u003ciframe src=\"./demo-map/demo-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./demo-map/demo-map.js\" integrity=\"sha384-WLB+/GTY8u9CWAKt72vKJroKi26oGzzjlGdA0iA3DSSP9oWHnUb8r6jtjAWSw0ee\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdemo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/demo-map>"},"instances":[{"id":"maplibre","title":"Demo map — MapLibre (token-free)","apiBaseUrl":"","props":{"profile":"demo-map","provider":"maplibre","instanceId":"demo-maplibre"},"element":"demo-map","iframeUrl":"./demo-map/demo-map/maplibre/index.html","embed":{"iframe":"\u003ciframe src=\"./demo-map/demo-map/maplibre/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}],"bundle":"demo-map"}]},{"bundle":"doxa-maps-spa","scriptUrl":"./doxa-maps-spa/doxa-maps-spa.js","output":"doxa-maps/doxa-maps-spa/doxa-maps-spa.js","integrity":"sha384-1f+/rBSGo/1jyUmRmGg34CIU/PcGkB7M3jah1DO0ocb5O0zj81wpUemXcz4WAFz8","elements":["doxa-maps-spa"],"composes":["bp-big-picture","doxa-research-map","doxa-simple-map","cities-internet-map","gospel-resources-map","my-upg-100-list-map"],"profiles":[{"profile":"doxa-maps-spa","file":"doxa-maps-spa.vue","description":"The DX-maps app itself: one element whose vertical-tab rail nests every other","element":"doxa-maps-spa","type":"nested-mfe-spa","isMap":false,"iframeUrl":"./doxa-maps-spa/doxa-maps-spa/index.html","scriptUrl":"./doxa-maps-spa/doxa-maps-spa.js","embed":{"iframe":"\u003ciframe src=\"./doxa-maps-spa/doxa-maps-spa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-1f+/rBSGo/1jyUmRmGg34CIU/PcGkB7M3jah1DO0ocb5O0zj81wpUemXcz4WAFz8\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/doxa-maps-spa>"},"instances":[{"id":"app","title":"DX-Maps SPA — Big Picture + every research map (app-shell)","apiBaseUrl":"","props":{"profile":"doxa-maps-spa","provider":"maplibre","instanceId":"doxa-maps-spa-app","title":"DX-Maps"},"element":"doxa-maps-spa","iframeUrl":"./doxa-maps-spa/doxa-maps-spa/app/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-maps-spa/doxa-maps-spa/app/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]},{"bundle":"doxa-research-map","scriptUrl":"./doxa-research-map/doxa-research-map.js","output":"doxa-maps/doxa-research-map/doxa-research-map.js","integrity":"sha384-LyIs0sqdkyJcj3DESqefXc7jsD96o+H+YVcOIq2kCdwaS+ZeBLqcn//06n1W1blE","elements":["doxa-research-map"],"profiles":[{"profile":"research-map","file":"research-map.vue","description":"The researcher's workbench: every piece a missiologist, demographer or","element":"doxa-research-map","type":"monolithic","isMap":true,"iframeUrl":"./doxa-research-map/research-map/index.html","scriptUrl":"./doxa-research-map/doxa-research-map.js","embed":{"iframe":"\u003ciframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-LyIs0sqdkyJcj3DESqefXc7jsD96o+H+YVcOIq2kCdwaS+ZeBLqcn//06n1W1blE\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/doxa-research-map>"},"instances":[{"id":"maplibre","title":"Research map — MapLibre (token-free, no search bar)","apiBaseUrl":"","props":{"profile":"research-map","provider":"maplibre","instanceId":"research-map-maplibre"},"element":"doxa-research-map","iframeUrl":"./doxa-research-map/research-map/maplibre/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-research-map/research-map/maplibre/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]},{"bundle":"doxa-simple-map","scriptUrl":"./doxa-simple-map/doxa-simple-map.js","output":"doxa-maps/doxa-simple-map/doxa-simple-map.js","integrity":"sha384-FtEGLeSRNfBCdmK26IURIxCldSnfKphxRT74XgjftFGzNpvube+ov/EVBFi2gXjo","elements":["doxa-map","doxa-simple-map"],"profiles":[{"profile":"doxa-simple-map","file":"doxa-simple-map.vue","description":"The simple world map, shipped as three presets off ONE prop table —","element":"doxa-map","type":"parameterized","isMap":true,"iframeUrl":"./doxa-simple-map/doxa-simple-map/index.html","scriptUrl":"./doxa-simple-map/doxa-simple-map.js","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-FtEGLeSRNfBCdmK26IURIxCldSnfKphxRT74XgjftFGzNpvube+ov/EVBFi2gXjo\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/doxa-map>"},"instances":[{"id":"prayer","title":"Prayer Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-prayer","dataSource":"pray-tools","tabs":[{"id":"prayer","colorStrategy":"prayer","legend":"prayer","popup":"prayer"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/prayer/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/prayer/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"engagement","title":"Engagement Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-engagement","dataSource":"pray-tools","tabs":[{"id":"engagement","colorStrategy":"engagement","legend":"engagement","popup":"engagement"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/engagement/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/engagement/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"adoption","title":"Adoption Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-adoption","dataSource":"pray-tools","tabs":[{"id":"adoption","colorStrategy":"adoption","legend":"adoption","popup":"adoption"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/adoption/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/adoption/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]},{"bundle":"doxa-vision-card","scriptUrl":"./doxa-vision-card/doxa-vision-card.js","output":"doxa-maps/doxa-vision-card/doxa-vision-card.js","integrity":"sha384-2mza3cltN5LQQIrlENG/IVL6Icd9zzcFve3E8R8VWuCdYOGW9uGKFvxcbu6eFfwn","elements":["doxa-vision-card"],"profiles":[{"profile":"doxa-vision-card","file":"doxa-vision-card.vue","description":"The Capability-1 example: ONE folder -> ONE bundle -> ONE element.","element":"doxa-vision-card","type":"parameterized","isMap":false,"iframeUrl":"./doxa-vision-card/doxa-vision-card/index.html","scriptUrl":"./doxa-vision-card/doxa-vision-card.js","embed":{"iframe":"\u003ciframe src=\"./doxa-vision-card/doxa-vision-card/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./doxa-vision-card/doxa-vision-card.js\" integrity=\"sha384-2mza3cltN5LQQIrlENG/IVL6Icd9zzcFve3E8R8VWuCdYOGW9uGKFvxcbu6eFfwn\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-vision-card profile-config='{\"profile\":\"doxa-vision-card\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/doxa-vision-card>"},"instances":[{"id":"default","title":"Vision card — reach at a glance","apiBaseUrl":"","props":{"profile":"doxa-vision-card","provider":"maplibre","instanceId":"vision-card-default"},"element":"doxa-vision-card","iframeUrl":"./doxa-vision-card/doxa-vision-card/default/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-vision-card/doxa-vision-card/default/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"frontier","title":"Vision card — frontier accent","apiBaseUrl":"","props":{"profile":"doxa-vision-card","provider":"maplibre","instanceId":"vision-card-frontier","accent":"#22d3ee","title":"The last frontier of the gospel.","tagline":"Frontier people groups — where fewer than one in a thousand follow Jesus.","stats":[{"value":"1,910","label":"Frontier groups","note":"under 0.1% believers"},{"value":"2.1B","label":"People","note":"living in frontier settings"},{"value":"0.1%","label":"Christian","note":"or fewer, in each group"}]},"element":"doxa-vision-card","iframeUrl":"./doxa-vision-card/doxa-vision-card/frontier/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-vision-card/doxa-vision-card/frontier/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}],"bundle":"doxa-vision-card"}]},{"bundle":"golden-path-check","scriptUrl":"./golden-path-check/golden-path-check.js","output":"doxa-maps/golden-path-check/golden-path-check.js","integrity":"sha384-1V/sT5SkV0U9yTCK9ELIOFfU1ZIJkInIF6nG3UllJ4dxax3RsEAupSGLJEFK6e14","elements":["golden-path-check"],"profiles":[{"profile":"golden-path-check","file":"golden-path-check.vue","description":"A live self-test of the develop-a-plugin-and-see-it-live path: it reads the built","element":"golden-path-check","type":"single","isMap":false,"iframeUrl":"./golden-path-check/golden-path-check/index.html","scriptUrl":"./golden-path-check/golden-path-check.js","embed":{"iframe":"\u003ciframe src=\"./golden-path-check/golden-path-check/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./golden-path-check/golden-path-check.js\" integrity=\"sha384-1V/sT5SkV0U9yTCK9ELIOFfU1ZIJkInIF6nG3UllJ4dxax3RsEAupSGLJEFK6e14\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgolden-path-check profile-config='{\"profile\":\"golden-path-check\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/golden-path-check>"}}]},{"bundle":"gospel-resources","scriptUrl":"./gospel-resources/gospel-resources.js","output":"doxa-maps/gospel-resources/gospel-resources.js","integrity":"sha384-NfnelqhIUAYNfRgFUORa0o69lAWqvF2gI0LGY0VPbMDx0ENlU9H4BYPnIcUPV75m","elements":["gospel-resources-map"],"composes":["resources-map","data-tables"],"profiles":[{"profile":"charts","file":"charts.vue","description":"The gap measured by LANGUAGE (ISO 639-3) rather than by people group, because one translation serves every group that speaks it, so counting by group overstates how much work remains.","element":"gospel-resources-map","type":"single","isMap":false,"iframeUrl":"./gospel-resources/charts/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-NfnelqhIUAYNfRgFUORa0o69lAWqvF2gI0LGY0VPbMDx0ENlU9H4BYPnIcUPV75m\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/gospel-resources-map>"}},{"profile":"data-tables","file":"data-tables.vue","description":"Eight tables covering both halves of one question: every gospel resource with its real URL and how much Scripture it is, and the 3,457 people groups where nothing was found with the pools searched recorded per row, plus bridge languages, city bridges, the deaf gap and access against population growth.","element":"gospel-resources-map","type":"single","isMap":false,"iframeUrl":"./gospel-resources/data-tables/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-NfnelqhIUAYNfRgFUORa0o69lAWqvF2gI0LGY0VPbMDx0ENlU9H4BYPnIcUPV75m\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/gospel-resources-map>"}},{"profile":"gospel-dashboard","file":"gospel-dashboard.vue","description":"The gospel map and its tables as one scrolling element: 12,379 people-group pins with the real Scripture, film and Bible-story links for each group's own language, above every resource with its URL and the 3,457 groups where nothing was found, exportable as CSV.","element":"gospel-resources-dashboard","type":"nested-elements-mfe","isMap":true,"iframeUrl":"./gospel-resources/gospel-dashboard/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/gospel-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-NfnelqhIUAYNfRgFUORa0o69lAWqvF2gI0LGY0VPbMDx0ENlU9H4BYPnIcUPV75m\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/gospel-resources-map>"}},{"profile":"resources-list","file":"resources-list.vue","description":"Which people groups have Scripture in their own language and which have none, zero-resource languages first, with every link the verification pass proved and Open Bible Stories labelled as stories rather than as Scripture.","element":"gospel-resources-map","type":"single","isMap":false,"iframeUrl":"./gospel-resources/resources-list/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-NfnelqhIUAYNfRgFUORa0o69lAWqvF2gI0LGY0VPbMDx0ENlU9H4BYPnIcUPV75m\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/gospel-resources-map>"}},{"profile":"resources-map","file":"resources-map.vue","description":"12,379 people-group pins over a country choropleth: tap a pin for the Scripture, audio, film, Bible-story and training links that exist in that group's own language with real URLs, tap a country for every group in it, search by group, language or ISO 639-3 code, and filter to the groups where nothing was found.","element":"gospel-resources-map","type":"single","isMap":true,"iframeUrl":"./gospel-resources/resources-map/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-NfnelqhIUAYNfRgFUORa0o69lAWqvF2gI0LGY0VPbMDx0ENlU9H4BYPnIcUPV75m\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/gospel-resources-map>"}}]},{"bundle":"my-upg-100-list","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","output":"doxa-maps/my-upg-100-list/my-upg-100-list.js","integrity":"sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p","elements":["my-upg-100-list-map"],"composes":["my-upg-100-list/upg-100-list","my-upg-100-list/poster-wall"],"profiles":[{"profile":"poster-wall","file":"poster-wall.vue","description":"A wall of printable poster cards, one per 100-list entry, each with a \"Show on map\"","element":"my-upg-100-list-map","type":"single","isMap":false,"iframeUrl":"./my-upg-100-list/poster-wall/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/my-upg-100-list-map>"}},{"profile":"upg-100-list","file":"upg-100-list.vue","description":"A personal unreached-people-groups list painted as true boundary polygons —","element":"my-upg-100-list-map","type":"single","isMap":true,"iframeUrl":"./my-upg-100-list/upg-100-list/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/my-upg-100-list-map>"}},{"profile":"upg-100-mfe","file":"upg-100-mfe.vue","description":"The standalone, shareable and embeddable composite of the my-upg-100-list family — it nests the map","element":"my-upg-100-list-map","type":"nested-elements-mfe","isMap":true,"iframeUrl":"./my-upg-100-list/upg-100-mfe/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/my-upg-100-list-map>"}}]},{"bundle":"people-groups-grid","scriptUrl":"./people-groups-grid/people-groups-grid.js","output":"doxa-maps/people-groups-grid/people-groups-grid.js","integrity":"sha384-8B2T2ckH+v8p2hEnkGruUg4Do5ZFsh31QSxYJYMj4/PN/ccHdgaIyK4dyW2GlUAR","elements":["people-groups-grid"],"profiles":[{"profile":"people-groups-grid","file":"people-groups-grid.vue","description":"The people-groups grid as an owned custom element: card list, search and four","element":"people-groups-grid","type":"parameterized","isMap":false,"iframeUrl":"./people-groups-grid/people-groups-grid/index.html","scriptUrl":"./people-groups-grid/people-groups-grid.js","embed":{"iframe":"\u003ciframe src=\"./people-groups-grid/people-groups-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-8B2T2ckH+v8p2hEnkGruUg4Do5ZFsh31QSxYJYMj4/PN/ccHdgaIyK4dyW2GlUAR\" crossorigin=\"anonymous\">\u003c/script>\n\u003cpeople-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/people-groups-grid>"},"instances":[{"id":"people-groups-grid","title":"People Groups Grid — standalone (own search + 4 dropdowns)","apiBaseUrl":"","props":{"profile":"people-groups-grid","provider":"maplibre","instanceId":"people-groups-grid","pray-base-url":"https://pray.doxa.life"},"element":"people-groups-grid","iframeUrl":"./people-groups-grid/people-groups-grid/people-groups-grid/index.html","embed":{"iframe":"\u003ciframe src=\"./people-groups-grid/people-groups-grid/people-groups-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"driven","title":"People Groups Grid — externally driven (controls off, props/events)","apiBaseUrl":"","props":{"profile":"people-groups-grid","provider":"maplibre","instanceId":"people-groups-grid-driven","pray-base-url":"https://pray.doxa.life","show-search":"false","show-filters":"false"},"element":"people-groups-grid","iframeUrl":"./people-groups-grid/people-groups-grid/driven/index.html","embed":{"iframe":"\u003ciframe src=\"./people-groups-grid/people-groups-grid/driven/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]},{"bundle":"plugin-studio","scriptUrl":"./plugin-studio/plugin-studio.js","output":"doxa-maps/plugin-studio/plugin-studio.js","integrity":"sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH","elements":["plugin-studio"],"composes":["bp-big-picture","people-groups-grid","doxa-research-map","doxa-simple-map","cities-internet-map","gospel-resources-map","my-upg-100-list-map","doxa-vision-card","all-people-groups","research-dashboard"],"profiles":[{"profile":"plugin-studio","file":"plugin-studio.vue","description":"The developer's viewing surface: a sidebar that is the composability ladder","element":"plugin-studio","type":"nested-mfe-spa","isMap":false,"iframeUrl":"./plugin-studio/plugin-studio/index.html","scriptUrl":"./plugin-studio/plugin-studio.js","embed":{"iframe":"\u003ciframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH\" crossorigin=\"anonymous\">\u003c/script>\n\u003cplugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/plugin-studio>"},"instances":[{"id":"studio","title":"Plugin Studio — every element by capability 5 -> 1 (developer studio)","apiBaseUrl":"","props":{"profile":"plugin-studio","provider":"maplibre","instanceId":"plugin-studio","title":"Plugin Studio"},"element":"plugin-studio","iframeUrl":"./plugin-studio/plugin-studio/studio/index.html","embed":{"iframe":"\u003ciframe src=\"./plugin-studio/plugin-studio/studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]},{"bundle":"prayer-walk-planner","scriptUrl":"./prayer-walk-planner/prayer-walk-planner.js","output":"doxa-maps/prayer-walk-planner/prayer-walk-planner.js","integrity":"sha384-VZxxlIqmR6mKdB1Dx6143LxEyI3POl9ks045yApFu4M4ZlLxsRH3+dCdEMpJb2Vw","elements":["prayer-walk-planner"],"profiles":[{"profile":"prayer-walk-planner","file":"prayer-walk-planner.vue","description":"A simple polygon-drawing tool for planning prayer-walking routes.","element":"prayer-walk-planner","type":"single","isMap":true,"iframeUrl":"./prayer-walk-planner/prayer-walk-planner/index.html","scriptUrl":"./prayer-walk-planner/prayer-walk-planner.js","embed":{"iframe":"\u003ciframe src=\"./prayer-walk-planner/prayer-walk-planner/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-VZxxlIqmR6mKdB1Dx6143LxEyI3POl9ks045yApFu4M4ZlLxsRH3+dCdEMpJb2Vw\" crossorigin=\"anonymous\">\u003c/script>\n\u003cprayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/prayer-walk-planner>"}}]},{"bundle":"regions-dashboard","scriptUrl":"./regions-dashboard/regions-dashboard.js","output":"doxa-maps/regions-dashboard/regions-dashboard.js","integrity":"sha384-oXKY7GNXVKcivqGSmhcCZeEPtmSfjR+UNiGyvnqBrJ/gZtJD61rOA8T+SA0SlHQj","elements":["regions-dashboard"],"composes":["regions-dashboard/regions-list","regions-dashboard/region-dashboards"],"profiles":[{"profile":"region-dashboards","file":"region-dashboards.vue","description":"The dashboard half of the Driver's 2-tab Regions element: given a region (or a","element":"regions-dashboard","type":"single","isMap":true,"iframeUrl":"./regions-dashboard/region-dashboards/index.html","scriptUrl":"./regions-dashboard/regions-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-oXKY7GNXVKcivqGSmhcCZeEPtmSfjR+UNiGyvnqBrJ/gZtJD61rOA8T+SA0SlHQj\" crossorigin=\"anonymous\">\u003c/script>\n\u003cregions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/regions-dashboard>"}},{"profile":"regions-list","file":"regions-list.vue","description":"A refactor of DX-life's /regions index page (app/pages/regions/index.vue) into","element":"regions-dashboard","type":"single","isMap":true,"iframeUrl":"./regions-dashboard/regions-list/index.html","scriptUrl":"./regions-dashboard/regions-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-oXKY7GNXVKcivqGSmhcCZeEPtmSfjR+UNiGyvnqBrJ/gZtJD61rOA8T+SA0SlHQj\" crossorigin=\"anonymous\">\u003c/script>\n\u003cregions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/regions-dashboard>"}},{"profile":"regions-mfe","file":"regions-mfe.vue","description":"The composite the Driver asked for: ONE element carrying the SELECT VIEW switcher","element":"regions-dashboard","type":"nested-elements-mfe","isMap":true,"iframeUrl":"./regions-dashboard/regions-mfe/index.html","scriptUrl":"./regions-dashboard/regions-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-oXKY7GNXVKcivqGSmhcCZeEPtmSfjR+UNiGyvnqBrJ/gZtJD61rOA8T+SA0SlHQj\" crossorigin=\"anonymous\">\u003c/script>\n\u003cregions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/regions-dashboard>"}}]},{"bundle":"research-dashboard","scriptUrl":"./research-dashboard/research-dashboard.js","output":"doxa-maps/research-dashboard/research-dashboard.js","integrity":"sha384-tAUIe2wERKg/ZRvINilfjrgfNYZeix7iSXYTJI1iSvf3eRtafkVt1a8cHIGyyRTF","elements":["research-dashboard-map"],"profiles":[{"profile":"research-dashboard","file":"research-dashboard.vue","description":"Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store.","element":"research-dashboard-map","type":"monolithic","isMap":true,"iframeUrl":"./research-dashboard/research-dashboard/index.html","scriptUrl":"./research-dashboard/research-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-tAUIe2wERKg/ZRvINilfjrgfNYZeix7iSXYTJI1iSvf3eRtafkVt1a8cHIGyyRTF\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/research-dashboard-map>"},"bundle":"research-dashboard"}]},{"bundle":"research-map-plus-grid","scriptUrl":"./research-map-plus-grid/research-map-plus-grid.js","output":"doxa-maps/research-map-plus-grid/research-map-plus-grid.js","integrity":"sha384-sOkaMV0UFr2uNXfoJpRvaD+gHD6xFlkmpR7MzEwwFdarYdid8aN9Nj1Y8TqQJuFo","elements":["research-map-plus-grid"],"composes":["doxa-research-map","people-groups-grid"],"profiles":[{"profile":"research-map-plus-grid","file":"research-map-plus-grid.vue","description":"The monolithic 7-tab research map (cap-3) on top, the People Groups grid beneath,","element":"research-map-plus-grid","type":"nested-elements-mfe","isMap":false,"iframeUrl":"./research-map-plus-grid/research-map-plus-grid/index.html","scriptUrl":"./research-map-plus-grid/research-map-plus-grid.js","embed":{"iframe":"\u003ciframe src=\"./research-map-plus-grid/research-map-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./research-map-plus-grid/research-map-plus-grid.js\" integrity=\"sha384-sOkaMV0UFr2uNXfoJpRvaD+gHD6xFlkmpR7MzEwwFdarYdid8aN9Nj1Y8TqQJuFo\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-map-plus-grid profile-config='{\"profile\":\"research-map-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/research-map-plus-grid>"},"instances":[{"id":"default","title":"default","apiBaseUrl":"","props":{"profile":"research-map-plus-grid"},"element":"research-map-plus-grid","iframeUrl":"./research-map-plus-grid/research-map-plus-grid/default/index.html","embed":{"iframe":"\u003ciframe src=\"./research-map-plus-grid/research-map-plus-grid/default/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]},{"bundle":"stack-demo","scriptUrl":"./stack-demo/stack-demo.js","output":"doxa-maps/stack-demo/stack-demo.js","integrity":"sha384-gnwM6s+puf1K60Nl7crQcC2QC+9BxwpkaTx4OepLZ8VUCraZc+aRlWpsb6nj6Gyf","elements":["stack-demo"],"composes":["research-dashboard-map","people-groups-grid"],"profiles":[{"profile":"stack-demo","file":"stack-demo.vue","description":"Groups TWO independent elements TOP-TO-BOTTOM so you can embed the pair on a page and","element":"stack-demo","type":"nested-elements-mfe","isMap":false,"iframeUrl":"./stack-demo/stack-demo/index.html","scriptUrl":"./stack-demo/stack-demo.js","embed":{"iframe":"\u003ciframe src=\"./stack-demo/stack-demo/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./stack-demo/stack-demo.js\" integrity=\"sha384-gnwM6s+puf1K60Nl7crQcC2QC+9BxwpkaTx4OepLZ8VUCraZc+aRlWpsb6nj6Gyf\" crossorigin=\"anonymous\">\u003c/script>\n\u003cstack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/stack-demo>"},"instances":[{"id":"pair","title":"pair","apiBaseUrl":"","props":{"profile":"stack-demo"},"element":"stack-demo","iframeUrl":"./stack-demo/stack-demo/pair/index.html","embed":{"iframe":"\u003ciframe src=\"./stack-demo/stack-demo/pair/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]},{"bundle":"template-bundle","scriptUrl":"./template-bundle/template-bundle.js","output":"doxa-maps/template-bundle/template-bundle.js","integrity":"sha384-Arx3XL5Mhdv0VcU7RX9gwggLBjx3SH4WFltOaoEJ6mW/jw/E6wgGQHIOkFyRtBRd","elements":["template-bundle"],"profiles":[{"profile":"template-map-a","file":"template-map-a.vue","description":"Minimal example profile A — a colored placeholder, no Mapbox or data.","element":"template-bundle","type":"single","isMap":false,"iframeUrl":"./template-bundle/template-map-a/index.html","scriptUrl":"./template-bundle/template-bundle.js","embed":{"iframe":"\u003ciframe src=\"./template-bundle/template-map-a/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-Arx3XL5Mhdv0VcU7RX9gwggLBjx3SH4WFltOaoEJ6mW/jw/E6wgGQHIOkFyRtBRd\" crossorigin=\"anonymous\">\u003c/script>\n\u003ctemplate-bundle profile-config='{\"profile\":\"template-map-a\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/template-bundle>"},"bundle":"template-bundle"},{"profile":"template-map-b","file":"template-map-b.vue","description":"Minimal example profile B — a second profile in the same bundle.","element":"template-bundle","type":"single","isMap":false,"iframeUrl":"./template-bundle/template-map-b/index.html","scriptUrl":"./template-bundle/template-bundle.js","embed":{"iframe":"\u003ciframe src=\"./template-bundle/template-map-b/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-Arx3XL5Mhdv0VcU7RX9gwggLBjx3SH4WFltOaoEJ6mW/jw/E6wgGQHIOkFyRtBRd\" crossorigin=\"anonymous\">\u003c/script>\n\u003ctemplate-bundle profile-config='{\"profile\":\"template-map-b\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/template-bundle>"},"bundle":"template-bundle"}]},{"bundle":"upg-neighbor","scriptUrl":"./upg-neighbor/upg-neighbor.js","output":"doxa-maps/upg-neighbor/upg-neighbor.js","integrity":"sha384-JDJr4qoVZuiSi/rvgdzeuUNrzf7vjPdyh/g8dg7X8RHkZk/Y+tYXIQ3gpEzrU07q","elements":["upg-neighbor"],"profiles":[{"profile":"upg-neighbor","file":"upg-neighbor.vue","description":"Rescues the five pre-bundler experimental maps that live inside the legacy","element":"upg-neighbor","type":"parameterized","isMap":false,"iframeUrl":"./upg-neighbor/upg-neighbor/index.html","scriptUrl":"./upg-neighbor/upg-neighbor.js","embed":{"iframe":"\u003ciframe src=\"./upg-neighbor/upg-neighbor/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-JDJr4qoVZuiSi/rvgdzeuUNrzf7vjPdyh/g8dg7X8RHkZk/Y+tYXIQ3gpEzrU07q\" crossorigin=\"anonymous\">\u003c/script>\n\u003cupg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/upg-neighbor>"},"instances":[{"id":"send-map","title":"Send Map (legacy)","apiBaseUrl":"","props":{"profile":"upg-neighbor","legacyElement":"send-map","legacyTitle":"Send Map"},"element":"upg-neighbor","iframeUrl":"./upg-neighbor/upg-neighbor/send-map/index.html","embed":{"iframe":"\u003ciframe src=\"./upg-neighbor/upg-neighbor/send-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"upg-neighbors","title":"UPG Neighbors (legacy)","apiBaseUrl":"","props":{"profile":"upg-neighbor","legacyElement":"upg-neighbors","legacyTitle":"UPG Neighbors"},"element":"upg-neighbor","iframeUrl":"./upg-neighbor/upg-neighbor/upg-neighbors/index.html","embed":{"iframe":"\u003ciframe src=\"./upg-neighbor/upg-neighbor/upg-neighbors/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"lang-cluster","title":"Language Clusters (legacy)","apiBaseUrl":"","props":{"profile":"upg-neighbor","legacyElement":"lang-cluster","legacyTitle":"Language Clusters"},"element":"upg-neighbor","iframeUrl":"./upg-neighbor/upg-neighbor/lang-cluster/index.html","embed":{"iframe":"\u003ciframe src=\"./upg-neighbor/upg-neighbor/lang-cluster/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"bloom-cockpit","title":"Bloom Cockpit (legacy)","apiBaseUrl":"","props":{"profile":"upg-neighbor","legacyElement":"bloom-cockpit","legacyTitle":"Bloom Cockpit"},"element":"upg-neighbor","iframeUrl":"./upg-neighbor/upg-neighbor/bloom-cockpit/index.html","embed":{"iframe":"\u003ciframe src=\"./upg-neighbor/upg-neighbor/bloom-cockpit/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"flower-mesh","title":"Flower Mesh (legacy)","apiBaseUrl":"","props":{"profile":"upg-neighbor","legacyElement":"flower-mesh","legacyTitle":"Flower Mesh"},"element":"upg-neighbor","iframeUrl":"./upg-neighbor/upg-neighbor/flower-mesh/index.html","embed":{"iframe":"\u003ciframe src=\"./upg-neighbor/upg-neighbor/flower-mesh/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]}]}; \n\n" + "script": "\n\n" } }, { @@ -908,7 +908,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n" + "script": "\n\n" } }, { @@ -922,7 +922,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n" + "script": "\n\n" } } ] diff --git a/public/doxa-maps/regions-dashboard/region-dashboards/index.html b/public/doxa-maps/regions-dashboard/region-dashboards/index.html index 23c89d1d4..4600e1440 100644 --- a/public/doxa-maps/regions-dashboard/region-dashboards/index.html +++ b/public/doxa-maps/regions-dashboard/region-dashboards/index.html @@ -29,7 +29,7 @@ - + - + - +