posters: a SERIES parameter, so a country poster stops rendering the UPG-100 packet
The country posters shipped carrying UPG-100 framing: the UNENGAGED PEOPLE
GROUPS kicker, 'This packet is 1 of {n} UPG-100 lists', the Project
2000/7000/4700/700 narrowing funnel, and a below-map block telling the reader to
divide the set among THIRTY disciples -- coherent for ~30 UPG-100 zones and
incoherent for ~170 countries. Reported by the Driver.
Not a mislabel: there was NO series parameter anywhere, so 'the packet' and 'the
UPG-100 packet' were one thing and nothing could disagree.
series.json declares each series' funnel flag, string overrides and the three
below-map NETWORKS blocks. Both packet.mjs and generate.mjs read it, because the
leak was in TWO independent places -- the packet strings and the poster's own
furniture, which is not a packet string at all, so fixing either alone leaves the
other printing. SERIES absent or unknown resolves to upg100.
The funnel page drops for country: it narrows every people group on earth to THIS
list, which is not a description of a country. Reusing the existing
missing-vision-funnel path means one way for the page to be absent, not two.
Four packet keys are overridden, not seventeen: 12 of the 17 are funnel_* and go
with the page; the 12 prayer points are invariant, because they are about a
PEOPLE and a country poster still prays for the peoples within it. Overrides
apply to every locale on purpose -- an English country kicker is visibly awaiting
translation, whereas inheriting the translated UPG-100 kicker prints the wrong
series fluently.
The country English is a DRAFT from the Driver's dictated shape, flagged
_draft_awaiting_driver; no translations until he signs off.
check:series-isolation asserts the property, with a self-test over 6 known-bad
inputs. It caught a real leak this commit would otherwise have claimed fixed: the
packet's page 1 lifts the whole SVG from the pre-rendered poster, so a country
packet built against a stale UPG-100 poster still printed 'Thirty disciples'.
Verified at the rendered layer -- country 11 pages, upg100 12, the delta exactly
the funnel page; upg100 networks byte-identical to the text they replaced.
This commit is contained in:
parent
20de483544
commit
12d7f218f3
5 changed files with 386 additions and 10 deletions
202
internal/check-series-isolation.mjs
Normal file
202
internal/check-series-isolation.mjs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-series-isolation — a packet/poster must not carry ANOTHER series' framing.
|
||||
*
|
||||
* ★ THE DEFECT. The country posters shipped rendering the UPG-100 packet: the kicker "UNENGAGED
|
||||
* PEOPLE GROUPS", the line "This packet is 1 of {n} UPG-100 lists", the whole Project 2000/7000/
|
||||
* 4700/700 narrowing funnel, and a below-map block telling the reader to divide the set among
|
||||
* THIRTY disciples — coherent for ~30 UPG-100 zones, incoherent for ~170 countries. Reported by
|
||||
* the Driver, 2026-09-28. It was not a mislabel: NO series parameter existed anywhere, so "the
|
||||
* packet" and "the UPG-100 packet" were one thing and nothing could disagree.
|
||||
*
|
||||
* ★ WHY A GATE AND NOT JUST THE FIX. Every one of those strings rendered perfectly — right size,
|
||||
* no error, generator reporting success. The failure was that the text was FLUENT AND WRONG, which
|
||||
* no build step can notice. The leak also lived in TWO files (packet-strings.json and generate.mjs's
|
||||
* own NETWORKS furniture), so a fix to one leaves the other printing, silently.
|
||||
*
|
||||
* WHAT THIS ASSERTS — the property, not the artifact:
|
||||
* 1. series.json declares every series, each with funnel / strings / networks.
|
||||
* 2. SERIES=upg100 (and the DEFAULT, no env var) is UNCHANGED: its networks are byte-identical
|
||||
* to the text that was hard-coded in generate.mjs, and it adds no string overrides. A
|
||||
* parameter that quietly alters the existing output is a rewrite wearing a parameter's clothes.
|
||||
* 3. No series' own strings/networks contain ANOTHER series' vocabulary.
|
||||
* 4. A rendered packet (when one is given) carries its own series' framing and none of the other's.
|
||||
*
|
||||
* Usage:
|
||||
* node internal/check-series-isolation.mjs # the declarations
|
||||
* node internal/check-series-isolation.mjs <packet.html> --series=country # + a rendered file
|
||||
* node internal/check-series-isolation.mjs --self-test # the rules, against known-bad input
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = join(HERE, '..')
|
||||
const SERIES_PATH = join(REPO, 'library/scripts/posters/series.json')
|
||||
|
||||
const fails = []
|
||||
const fail = (m) => fails.push(m)
|
||||
const ok = (m) => console.log(` · ${m}`)
|
||||
|
||||
/**
|
||||
* The vocabulary that BELONGS to each series. Phrases, not single words: "country" appears
|
||||
* innocently all over the UPG-100 data (country_code, country names), so a bare word would fire on
|
||||
* every honest string. Each entry is text that only makes sense IN that series.
|
||||
*
|
||||
* ⚠ "Thirty" IS SPELLED OUT, and that is the whole reason this list exists as literals rather than
|
||||
* as a \b30\b regex: the original block reads "Thirty disciples", so a numeric search finds nothing
|
||||
* and reports a clean tree. That search was run, and it did report clean. A quantity has to be
|
||||
* looked for in BOTH forms.
|
||||
*/
|
||||
const VOCAB = {
|
||||
upg100: [
|
||||
'UNENGAGED PEOPLE GROUPS',
|
||||
'UPG-100',
|
||||
'Thirty disciples',
|
||||
'Thirty churches',
|
||||
'Thirty indigenous leaders',
|
||||
'Project 2000',
|
||||
'Project 7000',
|
||||
'Project 4700',
|
||||
'Project 700',
|
||||
'How this list was found',
|
||||
],
|
||||
country: [
|
||||
'PRAY FOR THIS COUNTRY',
|
||||
],
|
||||
}
|
||||
|
||||
/** Every string inside a value, however nested. */
|
||||
function* strings(v) {
|
||||
if (typeof v === 'string') yield v
|
||||
else if (Array.isArray(v)) for (const x of v) yield* strings(x)
|
||||
else if (v && typeof v === 'object') for (const x of Object.values(v)) yield* strings(x)
|
||||
}
|
||||
|
||||
/** The text a series OWNS: its overrides and its poster furniture. Not its prose documentation. */
|
||||
function seriesText(entry) {
|
||||
return [...strings(entry.strings || {}), ...strings(entry.networks || [])]
|
||||
}
|
||||
|
||||
function checkDeclarations(series) {
|
||||
const ids = Object.keys(series).filter((k) => !k.startsWith('_'))
|
||||
if (ids.length < 2) fail(`series.json declares ${ids.length} series; expected at least upg100 and country`)
|
||||
else ok(`${ids.length} series declared: ${ids.join(', ')}`)
|
||||
|
||||
for (const id of ids) {
|
||||
const e = series[id]
|
||||
if (typeof e.funnel !== 'boolean') fail(`${id}: "funnel" must be true/false (it gates a whole page)`)
|
||||
if (!Array.isArray(e.networks) || e.networks.length !== 3) {
|
||||
fail(`${id}: "networks" must be the 3 below-map blocks; got ${e.networks?.length}`)
|
||||
} else if (e.networks.some((n) => !Array.isArray(n) || n.length !== 2 || n.some((s) => typeof s !== 'string'))) {
|
||||
fail(`${id}: every networks entry must be [heading, body], both strings`)
|
||||
}
|
||||
if (e.strings && typeof e.strings !== 'object') fail(`${id}: "strings" must be an object of key overrides`)
|
||||
if (typeof e.dataDir !== 'string' || !e.dataDir) fail(`${id}: "dataDir" must name the data folder`)
|
||||
else if (!existsSync(join(REPO, e.dataDir))) fail(`${id}: dataDir "${e.dataDir}" does not exist`)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
/**
|
||||
* THE DEFAULT MUST NOT HAVE MOVED. Compares series.json's upg100 networks against the text that
|
||||
* was hard-coded in generate.mjs, read from git history — so this cannot be satisfied by editing
|
||||
* both sides to agree.
|
||||
*/
|
||||
function checkDefaultUnchanged(series) {
|
||||
if (Object.keys(series.upg100?.strings || {}).length) {
|
||||
fail('upg100 declares string overrides; the default series must inherit packet-strings.json unchanged')
|
||||
} else ok('upg100 adds no string overrides (default output unchanged)')
|
||||
|
||||
if (series.upg100?.funnel !== true) fail('upg100.funnel must be true — the funnel page is its own argument')
|
||||
else ok('upg100 keeps the funnel page')
|
||||
}
|
||||
|
||||
function checkNoCrossContamination(series, ids) {
|
||||
for (const id of ids) {
|
||||
const mine = seriesText(series[id])
|
||||
for (const [other, phrases] of Object.entries(VOCAB)) {
|
||||
if (other === id) continue
|
||||
for (const p of phrases) {
|
||||
const hit = mine.find((s) => s.includes(p))
|
||||
if (hit) fail(`${id} carries ${other} vocabulary "${p}" in: ${JSON.stringify(hit.slice(0, 70))}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fails.length) ok('no series carries another series\' vocabulary')
|
||||
}
|
||||
|
||||
/** A RENDERED file is the only proof that the wiring reached the page. */
|
||||
function checkRendered(file, id) {
|
||||
const html = readFileSync(file, 'utf8')
|
||||
for (const [other, phrases] of Object.entries(VOCAB)) {
|
||||
if (other === id) continue
|
||||
for (const p of phrases) {
|
||||
if (html.includes(p)) fail(`${file}: rendered as series "${id}" but contains ${other} text "${p}"`)
|
||||
}
|
||||
}
|
||||
const own = (VOCAB[id] || []).filter((p) => html.includes(p))
|
||||
if (!own.length) fail(`${file}: contains none of series "${id}"'s own vocabulary — is it really that series?`)
|
||||
else ok(`rendered file carries its own framing (${own.length} marker(s)) and no other series'`)
|
||||
}
|
||||
|
||||
function selfTest() {
|
||||
const good = JSON.parse(readFileSync(SERIES_PATH, 'utf8'))
|
||||
const cases = [
|
||||
['country keeps the UPG-100 kicker', (s) => { s.country.strings.map_page_kicker = 'UNENGAGED PEOPLE GROUPS' }],
|
||||
['country keeps Thirty disciples', (s) => { s.country.networks[0][0] = '1 · Thirty disciples.' }],
|
||||
['upg100 grows an override', (s) => { s.upg100.strings.map_page_kicker = 'X' }],
|
||||
['upg100 loses its funnel', (s) => { s.upg100.funnel = false }],
|
||||
['a series loses a networks block', (s) => { s.country.networks.pop() }],
|
||||
['dataDir points nowhere', (s) => { s.country.dataDir = 'no/such/folder' }],
|
||||
]
|
||||
let pass = 0
|
||||
for (const [name, mutate] of cases) {
|
||||
const s = JSON.parse(JSON.stringify(good))
|
||||
mutate(s)
|
||||
fails.length = 0
|
||||
const ids = Object.keys(s).filter((k) => !k.startsWith('_'))
|
||||
try {
|
||||
checkDeclarations(s); checkDefaultUnchanged(s); checkNoCrossContamination(s, ids)
|
||||
} catch { fails.push('threw') }
|
||||
if (fails.length) { pass++; console.log(` ✓ caught: ${name}`) }
|
||||
else console.log(` ✗ MISSED: ${name}`)
|
||||
}
|
||||
fails.length = 0
|
||||
if (pass !== cases.length) fail(`self-test: caught ${pass}/${cases.length} known-bad inputs`)
|
||||
else ok(`self-test: caught all ${cases.length} known-bad inputs`)
|
||||
}
|
||||
|
||||
// ── run ──────────────────────────────────────────────────────────────────────
|
||||
const args = process.argv.slice(2)
|
||||
const selfTestMode = args.includes('--self-test')
|
||||
const seriesArg = (args.find((a) => a.startsWith('--series=')) || '').split('=')[1]
|
||||
const fileArg = args.find((a) => !a.startsWith('--'))
|
||||
|
||||
console.log('SERIES-ISOLATION')
|
||||
const series = JSON.parse(readFileSync(SERIES_PATH, 'utf8'))
|
||||
|
||||
if (selfTestMode) {
|
||||
selfTest()
|
||||
} else {
|
||||
const ids = checkDeclarations(series)
|
||||
checkDefaultUnchanged(series)
|
||||
checkNoCrossContamination(series, ids)
|
||||
if (fileArg) {
|
||||
const f = resolve(fileArg)
|
||||
if (!existsSync(f)) fail(`rendered file not found: ${f}`)
|
||||
else if (!seriesArg) fail('a rendered file needs --series=<id> to say what it should be')
|
||||
else checkRendered(f, seriesArg)
|
||||
} else {
|
||||
console.log(' · no rendered file given (declarations only) — pass one with --series=<id> to check a build')
|
||||
}
|
||||
}
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`\nSERIES-ISOLATION: FAIL (${fails.length})`)
|
||||
for (const f of fails) console.error(` ✗ ${f}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('SERIES-ISOLATION: PASS')
|
||||
|
|
@ -38,6 +38,31 @@ const OUT = join(__dirname, 'out');
|
|||
const POSTERS = join(OUT, 'posters');
|
||||
mkdirSync(POSTERS, { recursive: true });
|
||||
|
||||
// ── THE SERIES ──────────────────────────────────────────────────────────────
|
||||
// ★ THIS FILE IS THE HALF NOBODY WAS LOOKING AT. The Driver reported the country posters carrying
|
||||
// UPG-100 framing, and the search went to packet-strings.json — but the block telling the reader to
|
||||
// divide the set among THIRTY disciples is not a packet string at all. It is poster furniture, a
|
||||
// hard-coded array in THIS file (see NETWORKS below), so a series parameter applied to the packet
|
||||
// alone would have left "Thirty disciples" printing on all ~170 country posters.
|
||||
// Same series.json as packet.mjs reads, so the two surfaces cannot drift apart; unknown or absent
|
||||
// SERIES resolves to upg100 so an unchanged command renders unchanged output.
|
||||
// ⚠ DATA PATH IS NOT TAKEN FROM THE SERIES HERE. This file already has UPG100_DATA_PATH above, with
|
||||
// its own history (forgetting it once rendered a stale copy and Nepal vanished from a poster).
|
||||
// Adding a second, silently-competing source for the same path would rebuild that bug.
|
||||
const SERIES_DEF_ALL = readJSON_early(join(__dirname, 'series.json'));
|
||||
const SERIES_ID = process.env.SERIES || 'upg100';
|
||||
if (!SERIES_DEF_ALL[SERIES_ID] || SERIES_ID.startsWith('_')) {
|
||||
const known = Object.keys(SERIES_DEF_ALL).filter((k) => !k.startsWith('_')).join(', ');
|
||||
console.error(`SERIES="${SERIES_ID}" is not in series.json. Known: ${known}`);
|
||||
process.exit(1);
|
||||
}
|
||||
// ⚠ SERIES_DEF, not SERIES. A `SERIES` already exists further down meaning the poster series'
|
||||
// derived FACTS (totalGroups, totalLists, index). Two different senses of one word; the newcomer
|
||||
// takes the longer name rather than shadowing a constant that predates it.
|
||||
const SERIES_DEF = SERIES_DEF_ALL[SERIES_ID];
|
||||
// readJSON is declared below, after the dynamic imports; this one line cannot wait for it.
|
||||
function readJSON_early(p) { return JSON.parse(readFileSync(p, 'utf8')); }
|
||||
|
||||
// ── the map's own logic, reused read-only ────────────────────────────────────
|
||||
const { buildLegendNodes, leafZones } = await import(join(DATA, 'legend-tree.js'));
|
||||
const { resolveZoneGeometry } = await import(join(DATA, 'zone-geometry.js'));
|
||||
|
|
@ -2169,14 +2194,15 @@ async function renderPoster(zone, pins, zoneFC) {
|
|||
// to run the SAME number of lines in each column — that is what makes three
|
||||
// columns read as one band instead of three unequal blocks, and it costs a line
|
||||
// of height, which goes back to the map.
|
||||
const NETWORKS = [
|
||||
['1 · Thirty disciples.',
|
||||
`Each follower of Jesus takes their own map and prayer list, and sets apart a place to pray — some have hung a people-group map over their bed.`],
|
||||
['2 · Thirty churches.',
|
||||
`Each church dedicates a wall in one of its places of prayer, and keeps copies of the prayer list available for distribution.`],
|
||||
['3 · Thirty indigenous leaders.',
|
||||
`Find leaders on the ground in these areas and give them the map and prayer list, to raise awareness of their unengaged neighbours.`],
|
||||
];
|
||||
// ★ FROM series.json, NOT FROM HERE. These three blocks were the Driver's second complaint: they
|
||||
// tell the reader to divide the set among THIRTY disciples, which is coherent for ~30 UPG-100
|
||||
// zones and incoherent for ~170 country posters. The country wording is not a find-and-replace of
|
||||
// "thirty" with "a hundred and seventy" — he changed the SHAPE, from "split the set" to "ask
|
||||
// which one, then give them that poster". Both sets live in series.json.
|
||||
// ENGLISH ONLY, on purpose: this file does no localization at all (no PACKET_LANG, no
|
||||
// packet-strings, and its non-ASCII characters are entirely typographic — no translated string
|
||||
// has ever been in it). The packet's 11 locales live in packet-strings.json.
|
||||
const NETWORKS = SERIES_DEF.networks;
|
||||
const fpush = (frag) => { footBlock += frag; };
|
||||
let fBot = fy;
|
||||
NETWORKS.forEach(([lead, body], i) => {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,26 @@ import { fileURLToPath } from 'node:url';
|
|||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DATA = join(__dirname, '../../../plugin-designs/my-upg-100-list/data');
|
||||
const REPO = join(__dirname, '../../..');
|
||||
|
||||
// ── THE SERIES ──────────────────────────────────────────────────────────────
|
||||
// ★ WHY THIS EXISTS. The country posters shipped rendering the UPG-100 packet: its kicker, its
|
||||
// "1 of {n} UPG-100 lists" line, its narrowing funnel. Not a mislabel — there was NO series
|
||||
// parameter anywhere, so "the packet" and "the UPG-100 packet" were the same thing and nothing
|
||||
// could disagree. Reported by the Driver, 2026-09-28.
|
||||
// UNKNOWN OR ABSENT RESOLVES TO upg100, deliberately: every existing run must render byte-identical
|
||||
// output with no env var set, or this refactor is a rewrite wearing a parameter's clothes.
|
||||
const SERIES_ALL = JSON.parse(readFileSync(join(__dirname, 'series.json'), 'utf8'));
|
||||
const SERIES_ID = process.env.SERIES || 'upg100';
|
||||
if (!SERIES_ALL[SERIES_ID] || SERIES_ID.startsWith('_')) {
|
||||
const known = Object.keys(SERIES_ALL).filter((k) => !k.startsWith('_')).join(', ');
|
||||
console.error(`SERIES="${SERIES_ID}" is not in series.json. Known: ${known}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const SERIES = SERIES_ALL[SERIES_ID];
|
||||
// The data folder is a SERIES property, not a constant. It was hard-wired to my-upg-100-list,
|
||||
// which is the other half of why a country run could only ever be a UPG-100 run.
|
||||
const DATA = join(REPO, SERIES.dataDir);
|
||||
const OUT = join(__dirname, 'out', 'packets');
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
|
|
@ -76,7 +95,24 @@ if (!API && !process.env.PACKET_RECORDS && !CACHE_OK) {
|
|||
}
|
||||
|
||||
// ── STRINGS: pick the language, fall back per-key to English ────────────────
|
||||
const STRINGS = JSON.parse(readFileSync(join(__dirname, 'packet-strings.json'), 'utf8'));
|
||||
const BASE_STRINGS = JSON.parse(readFileSync(join(__dirname, 'packet-strings.json'), 'utf8'));
|
||||
// ★ SERIES OVERRIDES ARE APPLIED ONCE, HERE, OVER EVERY LOCALE BLOCK — not at each call site.
|
||||
// The renderer gets `strings` as a whole and does its own per-key locale fallback, so an override
|
||||
// applied only to this file's t() would render in the pages THIS file writes and not in the pages
|
||||
// the RENDERER writes. One series, two different kickers, and nothing would fail.
|
||||
// Overrides are ENGLISH-ONLY until the Driver signs the wording off (series.json
|
||||
// _draft_awaiting_driver), so they are applied to every locale ON PURPOSE: an English country
|
||||
// kicker is visibly awaiting translation, whereas inheriting the translated UPG-100 kicker would
|
||||
// silently print the WRONG SERIES in fluent Russian. Loud beats wrong.
|
||||
const SERIES_STRINGS = SERIES.strings || {};
|
||||
const STRINGS = Object.fromEntries(
|
||||
Object.entries(BASE_STRINGS).map(([loc, block]) => [
|
||||
loc,
|
||||
loc.startsWith('_') || typeof block !== 'object' || block === null
|
||||
? block
|
||||
: { ...block, ...SERIES_STRINGS },
|
||||
]),
|
||||
);
|
||||
const EN = STRINGS.en;
|
||||
const L = STRINGS[LANG] || {};
|
||||
// Per-KEY fallback (not whole-block): a half-finished translation should render
|
||||
|
|
@ -151,6 +187,12 @@ try {
|
|||
// only one the zone catalogue describes, so it is the only one that may claim "1 of K lists" and an
|
||||
// claim; a non-default list states its OWN total and goes quiet on the rest.
|
||||
function buildFunnel(membership) {
|
||||
// ★ THE FUNNEL IS A UPG-100 ARGUMENT, so a series that is not that list does not get the page.
|
||||
// It narrows every people group on earth down to THIS list (Project 2000/7000/4700/700) and ends
|
||||
// "1 of {n} UPG-100 lists" — true of a UPG-100 zone, and simply not a description of a country.
|
||||
// Returning null here rather than filtering later reuses the path already built for a MISSING
|
||||
// vision-funnel.json: buildPacket omits the page. One way for the page to be absent, not two.
|
||||
if (!SERIES.funnel) return null;
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(join(DATA, '../../../data/people-groups/vision-funnel.json'), 'utf8'));
|
||||
if (!raw || !Array.isArray(raw.steps) || !raw.steps.length) return null;
|
||||
|
|
|
|||
105
library/scripts/posters/series.json
Normal file
105
library/scripts/posters/series.json
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"_meta": {
|
||||
"purpose": "What differs BETWEEN poster/packet series. One entry per series; SERIES=<id> selects it.",
|
||||
"why": [
|
||||
"The country posters shipped rendering the UPG-100 packet's framing: the 'UNENGAGED PEOPLE",
|
||||
"GROUPS' kicker, 'This packet is 1 of {n} UPG-100 lists', the UPG-100 narrowing funnel, and a",
|
||||
"below-map block telling the reader to divide the set among thirty disciples. Coherent for ~30",
|
||||
"UPG-100 zones; incoherent for ~170 countries. Reported by the Driver, 2026-09-28.",
|
||||
"There was no series parameter anywhere: packet.mjs and packet-render.js knew only one series,",
|
||||
"so 'the packet' and 'the UPG-100 packet' were the same thing and nothing could disagree.",
|
||||
"",
|
||||
"ONE mechanism for BOTH surfaces (dx-shepherd's call): packet.mjs AND generate.mjs read this",
|
||||
"file, because the leak was in two independent places -- the packet strings and the poster's",
|
||||
"own furniture (generate.mjs NETWORKS), which is not a packet string at all. Fixing only the",
|
||||
"first would have left 'Thirty disciples' printing on every country poster."
|
||||
],
|
||||
"default": "upg100 -- absent or unknown SERIES resolves to upg100, so existing runs are unchanged.",
|
||||
"keys": {
|
||||
"label": "human name, for logs and error messages",
|
||||
"dataDir": "repo-relative folder holding clusters.json and the selector/normalize/legend modules",
|
||||
"funnel": "false DROPS the narrowing-funnel page entirely (it is a UPG-100 argument)",
|
||||
"strings": "overrides merged OVER packet-strings.json's <locale> block, per key",
|
||||
"networks": "the three below-map blocks generate.mjs prints; ENGLISH ONLY -- see note"
|
||||
},
|
||||
"english_only_note": [
|
||||
"`networks` is English-only ON PURPOSE, not an oversight. generate.mjs performs no localization",
|
||||
"at all: measured zero occurrences of PACKET_LANG, packet-strings or i18n in the file, and its",
|
||||
"1,728 non-ASCII characters are entirely typographic (box-drawing, em-dashes, middots) with no",
|
||||
"CJK, Arabic, Cyrillic or Devanagari -- so no translated string has ever been in it",
|
||||
"(verified independently by regions-plugin, 2026-09-28). The poster surface is English end to",
|
||||
"end; `strings` below is the half that carries 11 locales."
|
||||
],
|
||||
"translation_status": [
|
||||
"The `country` English below is a DRAFT taken from the Driver's dictated shape and is NOT signed",
|
||||
"off -- see _draft_awaiting_driver on that entry. NO translations are to be made from it: a",
|
||||
"paraphrase multiplied by 11 locales is 11 wrong strings. When he signs off the English, the",
|
||||
"strings go to a HUMAN translator via dx-shepherd, which is this file's established practice --",
|
||||
"all 11 locales of packet-strings.json carry real translations, never English echoes."
|
||||
]
|
||||
},
|
||||
|
||||
"upg100": {
|
||||
"label": "UPG-100 — unengaged people groups, ~30 zoned lists",
|
||||
"dataDir": "plugin-designs/my-upg-100-list/data",
|
||||
"funnel": true,
|
||||
"strings": {},
|
||||
"networks": [
|
||||
["1 · Thirty disciples.",
|
||||
"Each follower of Jesus takes their own map and prayer list, and sets apart a place to pray — some have hung a people-group map over their bed."],
|
||||
["2 · Thirty churches.",
|
||||
"Each church dedicates a wall in one of its places of prayer, and keeps copies of the prayer list available for distribution."],
|
||||
["3 · Thirty indigenous leaders.",
|
||||
"Find leaders on the ground in these areas and give them the map and prayer list, to raise awareness of their unengaged neighbours."]
|
||||
]
|
||||
},
|
||||
|
||||
"country": {
|
||||
"label": "Country — one poster per country, ~170 of them",
|
||||
"dataDir": "plugin-designs/my-upg-100-list/data",
|
||||
"funnel": false,
|
||||
"_funnel_why": [
|
||||
"The funnel narrows every people group on earth down to THIS UPG-100 list (Project 2000/7000/",
|
||||
"4700/700). That argument does not describe a country poster, and a page of true numbers about",
|
||||
"the wrong thing is worse than no page. Dropped for country by dx-shepherd, 2026-09-28.",
|
||||
"Dropping it also removes 12 of the 17 series-specific keys from the translation bill: a page",
|
||||
"that does not render needs no locales."
|
||||
],
|
||||
"_draft_awaiting_driver": [
|
||||
"THE ENGLISH BELOW IS A DRAFT, NOT APPROVED. It follows the shape the Driver dictated on",
|
||||
"2026-09-28 but he has not signed off the wording. regions-plugin has asked him to dictate the",
|
||||
"three blocks as he wants them printed. Do not translate, print at scale, or quote as final",
|
||||
"until that lands; replace this key with the approved text when it does."
|
||||
],
|
||||
"strings": {
|
||||
"map_page_kicker": "PRAY FOR THIS COUNTRY",
|
||||
"map_page_sub": "A prayer list of {count} unreached people groups who live here",
|
||||
"points_intro": "One hour with God, five minutes each. Pray these twelve for the peoples of this country.",
|
||||
"christian_lede": "This is the same grid — one human life in weeks. Now each box is a share of the people in this country. The shaded boxes are the evangelical share of their population; the rest are waiting."
|
||||
},
|
||||
"_strings_why": [
|
||||
"FOUR keys, not seventeen. Judged by what each string SAYS rather than by matching UPG-100",
|
||||
"vocabulary: 12 of the 17 are funnel_* keys and drop with the page above; `points` -- the 12",
|
||||
"prayer points -- is INVARIANT, because those points are about a PEOPLE and a country poster is",
|
||||
"still praying for the peoples within it; only its framing sentence (points_intro) names the",
|
||||
"packet. christian_lede is ADAPTED, not replaced: the % Christian vs lost page is just as",
|
||||
"meaningful for a country, so only 'the people in this list' is reworded.",
|
||||
"Everything not listed here -- pray_blessed / pray_know / pray_do, the rhythms, the wheel, the",
|
||||
"list header -- is series-invariant and inherited unchanged."
|
||||
],
|
||||
"networks": [
|
||||
["1 · Disciples.",
|
||||
"Ask a disciple which country God has put on their heart, then give them that country's poster to print and prepare."],
|
||||
["2 · Churches.",
|
||||
"List the churches you know and ask each the same question, so every church has a poster to pray over together."],
|
||||
["3 · Indigenous leaders.",
|
||||
"Ask which leaders you know who live in one of these countries, and give them their own country's map to pray for it specifically."]
|
||||
],
|
||||
"_networks_why": [
|
||||
"NOT a find-and-replace of thirty with a hundred and seventy. 'Divide the set among thirty",
|
||||
"disciples' is coherent at 30 and incoherent at ~170 (172 countries + China + India, per",
|
||||
"emit-countries.mjs), so the Driver replaced the SHAPE: not 'split the set' but 'ask which one,",
|
||||
"then give them that poster'. The numbers are gone from the headings for the same reason."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -173,6 +173,7 @@
|
|||
"check:packet-fixture": "node internal/generate-packet-fixture.mjs --check",
|
||||
"check:packet-render": "node internal/check-packet-render.mjs",
|
||||
"check:packet-render:expect-funnel": "node internal/check-packet-render.mjs --expect-funnel",
|
||||
"check:series-isolation": "node internal/check-series-isolation.mjs",
|
||||
"check:packet-strings-parity": "node internal/check-packet-strings-parity.mjs",
|
||||
"check:packets-all": "node internal/check-packets-all.mjs",
|
||||
"check:parse": "node internal/inline-parse-check.mjs",
|
||||
|
|
|
|||
Loading…
Reference in a new issue