From 51524b78970f71df6bbca065e29a72c7baf55024 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Wed, 30 Sep 2026 22:29:06 +0000 Subject: [PATCH] A poster series for India's states and union territories; series-isolation sees shared wording and poster text correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit series.json: a new `indiaState` series, modelled on `country`, so a state poster names itself honestly ("India State Maps", "What is a State Prayer Map?", one sheet for each state and union territory of India, "which state God has put on their heart"). The country series' text would have called Gujarāt one of the ~170 Country Maps. Approved as the default wording; shown as editable. check:series-isolation, two blind spots found by adding a third series: - Shared wording was dropped from only the EARLIER series (sets were mutated while still being compared), so the later one kept it as a fingerprint and the earlier failed for carrying it. The shared set is now found on the original sets. Self-test: three series in any order; the old loop fails it. - The contamination scan never read a series' `poster` block, where a country poster once printed the UPG-100 side box. It does now. The phrases that NAME country and indiaState are hand-listed, so a verbatim copy of one's framing into the other is caught even though symmetric dropping treats it as shared. Self-test 9/9, including both directions of a country/state copy. countrySheet.js: the seriesFor comment no longer says "map 1 of 0" was printed; it was latent (the map-only packet page prints no series line, and the India poster is gated), as a rendered check showed. --- internal/check-series-isolation.mjs | 45 ++++++++++++++++++++++++----- library/packet/countrySheet.js | 5 ++-- library/scripts/posters/series.json | 44 ++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/internal/check-series-isolation.mjs b/internal/check-series-isolation.mjs index 878cebb8f..af3f2bd1c 100644 --- a/internal/check-series-isolation.mjs +++ b/internal/check-series-isolation.mjs @@ -63,8 +63,19 @@ const HAND_VOCAB = { 'Project 700', 'How this list was found', ], + // The phrases that NAME a series. Derived vocabulary drops any sentence two series share, so a verbatim + // copy of one series' framing into another would go unseen without these (the copy becomes "shared"). country: [ 'PRAY FOR THIS COUNTRY', + 'Country Prayer Map', + 'one sheet for each country', + 'which country God has put', + ], + indiaState: [ + 'PRAY FOR THIS STATE', + 'State Prayer Map', + 'one sheet for each state', + 'which state God has put', ], } @@ -100,13 +111,13 @@ function deriveVocab(series) { ...strings(e.poster || {}), ].map((s) => s.trim()).filter((s) => s.length >= 12)) } - // Drop anything two series share — it cannot identify either of them. - for (const a of ids) { - for (const b of ids) { - if (a === b) continue - for (const s of own[b]) own[a].delete(s) - } - } + // Drop anything two series share — it cannot identify either of them. The shared set is found on the + // ORIGINAL sets first: deleting while still comparing pairs removed a shared string from the earlier + // series only, so the later one kept it as a "fingerprint" and the earlier one failed for carrying it. + // Invisible with two series; a third (indiaState, sharing country's networks) exposed it. + const shared = new Set() + for (const a of ids) for (const b of ids) if (a !== b) for (const s of own[a]) if (own[b].has(s)) shared.add(s) + for (const id of ids) for (const s of shared) own[id].delete(s) const out = {} for (const id of ids) out[id] = [...new Set([...(HAND_VOCAB[id] || []), ...own[id]])] return out @@ -120,8 +131,10 @@ function* strings(v) { } /** The text a series OWNS: its overrides and its poster furniture. Not its prose documentation. */ +// The poster header block too: that is where the country poster once printed the UPG-100 side box, and a scan +// of strings + networks alone could never see a series' framing copied into another's poster text. function seriesText(entry) { - return [...strings(entry.strings || {}), ...strings(entry.networks || [])] + return [...strings(entry.strings || {}), ...strings(entry.networks || []), ...strings(entry.poster || {})] } function checkDeclarations(series) { @@ -208,6 +221,9 @@ function selfTest() { ['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' }], + ['indiaState keeps the country side box', (s) => { s.indiaState.poster.sideBoxHeading = 'What is a Country Prayer Map?' }], + ['country keeps the state network line', (s) => { s.country.networks[0][1] = s.indiaState.networks[0][1] }], + ['indiaState keeps PRAY FOR THIS COUNTRY', (s) => { s.indiaState.strings.map_page_kicker = 'PRAY FOR THIS COUNTRY' }], ] let pass = 0 for (const [name, mutate] of cases) { @@ -224,6 +240,19 @@ function selfTest() { 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`) + // Shared wording is nobody's fingerprint, in EITHER order: three series where the first and last share a + // sentence must neither flag it nor lose their own phrases. + const SHARED = 'A sentence two series both print, word for word.' + const tri = { + a: { strings: { k: 'Only series a says this phrase.' }, networks: [['h', SHARED]] }, + b: { strings: { k: 'Only series b says this phrase.' }, networks: [] }, + c: { strings: { k: 'Only series c says this phrase.' }, networks: [['h', SHARED]] }, + } + const v = deriveVocab(tri) + const leaked = Object.entries(v).filter(([, p]) => p.includes(SHARED)).map(([id]) => id) + const lost = ['a', 'b', 'c'].filter((id) => !v[id].includes(`Only series ${id} says this phrase.`)) + if (leaked.length || lost.length) fail(`self-test: shared wording kept as vocabulary of [${leaked}] / own phrases lost for [${lost}]`) + else ok('self-test: shared wording is dropped from every series, whatever their order') } // ── run ────────────────────────────────────────────────────────────────────── diff --git a/library/packet/countrySheet.js b/library/packet/countrySheet.js index ea4dbbc87..dacab7b5f 100644 --- a/library/packet/countrySheet.js +++ b/library/packet/countrySheet.js @@ -58,8 +58,9 @@ * keeps the renderer's own default rather than a fabricated position. * * A null sheet is a tier too: India (sheet null, too many groups for any paper size) is its own - * 1-of-1 series. Skipping it printed "map 1 of 0" on India's packet page 1 from the Regions tab - * while the wall's own copy printed "map 1 of 1" -- the two paths disagreeing again. + * 1-of-1 series. Skipping it gave India's sheet "map 1 of 0" on the Regions path and "map 1 of 1" on the + * wall's old copy. Neither was visible then (the map-only packet page prints no series line, and India's + * poster is gated), but any India sheet with a header would have shown it. * Every null-sheet country shares that ONE oversized series: a second would make India "map 1 of 2". * check:country-sheet-one-path fails when that happens, so it is decided, not drifted into. */ diff --git a/library/scripts/posters/series.json b/library/scripts/posters/series.json index d7a903aba..c309f765e 100644 --- a/library/scripts/posters/series.json +++ b/library/scripts/posters/series.json @@ -143,5 +143,49 @@ "emit-countries.mjs), so the SHAPE was replaced: not 'split the set' but 'ask which one,", "then give them that poster'. The numbers are gone from the headings for the same reason." ] + }, + "indiaState": { + "label": "India states and union territories — one poster for each of the 34 with people groups listed", + "dataDir": "plugin-designs/my-upg-100-list/data", + "countryMode": true, + "funnel": false, + "poster": { + "kicker": "Unreached People Groups", + "titleQualifier": "Pray for", + "seriesName": "India State Maps", + "sideBoxHeading": "What is a State Prayer Map?", + "sideBoxBody": "This map is one of a {approxLists}-map set called the {seriesName} — one sheet for each state and union territory of India, naming the unreached people groups who live there so a reader can pray for them by name." + }, + "_poster_why": [ + "A state poster is a country poster one level down: the Country Posters wall prints one for each of India's", + "states and union territories that has people groups listed (34 of 36; data/people-groups/india-states.json).", + "The country series' text cannot be reused: it would call Gujarāt's poster one of the ~170 'Country Maps',", + "one sheet for each country. The term is 'state and union territory', never 'province'. Approved as the", + "default wording 2026-09-30 and shown as editable; a change is a strings-only follow-up." + ], + "strings": { + "map_page_kicker": "PRAY FOR THIS STATE", + "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 state.", + "christian_lede": "This is the same grid — one human life in weeks. Now each box is a share of the people in this state. The shaded boxes are the evangelical share of their population; the rest are waiting." + }, + "_strings_why": [ + "The country series' four keys with 'country' read as 'state'. Used by the Node generator; the browser", + "packet reads the localized packet-strings.json, which is series-invariant." + ], + "networks": [ + [ + "1 · Disciples.", + "Ask a disciple which state God has put on their heart, then give them that state'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 states, and give them their own state's map to pray for it specifically." + ] + ] } }