From 1e925c4d715572ee8416cf441b15ffeb22e939c6 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Thu, 1 Oct 2026 08:37:17 +0000 Subject: [PATCH] Gospel checks: the field-card shards are expected from the roster, not read as one file, and German 'Ring' is the same word on purpose --- internal/check-gospel-tables-staged.mjs | 39 +++++++++++++++++++ .../scripts/check-gospel-internet-i18n.mjs | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/internal/check-gospel-tables-staged.mjs b/internal/check-gospel-tables-staged.mjs index d5625685d..146b488b2 100644 --- a/internal/check-gospel-tables-staged.mjs +++ b/internal/check-gospel-tables-staged.mjs @@ -64,6 +64,44 @@ for (const prefix of dirLike) { for (const file of named.filter((f) => f.endsWith(ext))) wanted.push(prefix + file) } +/** + * ⚠ AN INTERPOLATED FILE NAME IS NOT A FILE EITHER. The field card fetches + * `${assetsBase.value}/tables/card-${cardShard(cardSlug)}.json`, so the parse above captures "tables/card-" and + * stops at the interpolation; read as a file it failed as "not staged" while all its shards were there. Its + * expected set comes from the SOURCE, as the rule above says, never from the tree: every roster row the card + * builder keeps (data/tables/doxa-people-groups.json, the same filter as build-field-cards.mjs) hashed with the + * leaf's own cardShard. This file carries a copy of that function and fails if the leaf's copy changes, so the + * two cannot drift apart; any other interpolated name is reported as unresolved, never passed. + */ +const SHARD_FN = 'let h = 5381 for (let i = 0; i < slug.length; i++) h = (((h << 5) + h) ^ slug.charCodeAt(i)) & 0xffffffff return ((h >>> 0) & 0xff).toString(16).padStart(2, \'0\')' +const cardShard = (slug) => { + let h = 5381 + for (let i = 0; i < slug.length; i++) h = (((h << 5) + h) ^ slug.charCodeAt(i)) & 0xffffffff + return ((h >>> 0) & 0xff).toString(16).padStart(2, '0') +} +function checkInterpolatedName(rel, missing) { + if (rel !== 'tables/card-') { missing.push(`${rel} — an interpolated file name this check cannot resolve`); return } + const leafFn = (src.match(/const cardShard = \(slug\) => \{([\s\S]*?)\n\}/) || [])[1] + if (!leafFn || leafFn.replace(/\s+/g, ' ').trim() !== SHARD_FN) { + missing.push('tables/card-*.json — the leaf\'s cardShard changed; update this check\'s copy (and build-field-cards.mjs) to match') + return + } + let rows = [] + try { rows = JSON.parse(readFileSync(resolve(ROOT, 'data/tables/doxa-people-groups.json'), 'utf8')).rows || [] } + catch (e) { missing.push(`tables/card-*.json — could not read the roster: ${e.message}`); return } + const keys = [...new Set(rows.filter((r) => r && r.slug && r.mapped && r.lat != null && r.lng != null).map((r) => cardShard(r.slug)))].sort() + if (keys.length < 20) { missing.push(`tables/card-*.json — the roster gave only ${keys.length} shard key(s); the roster changed shape`); return } + let ok = 0, bytes = 0 + for (const k of keys) { + const p = resolve(POOL, `tables/card-${k}.json`) + if (!existsSync(p)) { missing.push(`tables/card-${k}.json — not staged`); continue } + const b = statSync(p).size + if (b < 200) { missing.push(`tables/card-${k}.json — staged but only ${b} bytes`); continue } + ok++; bytes += b + } + console.log(` ${ok === keys.length ? 'ok ' : 'FAIL'} ${'tables/card-*.json'.padEnd(42)} ${ok}/${keys.length} shard(s) the roster needs, ${bytes} bytes`) +} + const missing = [] for (const rel of wanted) { if (rel.endsWith('/')) { @@ -73,6 +111,7 @@ for (const rel of wanted) { if (!found.length) missing.push(`${rel} — a dynamic fetch this check could not resolve to a file`) continue } + if (!/\.(?:geojson|json)$/.test(rel)) { checkInterpolatedName(rel, missing); continue } const p = resolve(POOL, rel) if (!existsSync(p)) { missing.push(`${rel} — not staged`); continue } const bytes = statSync(p).size diff --git a/plugin-designs/gospel-internet/scripts/check-gospel-internet-i18n.mjs b/plugin-designs/gospel-internet/scripts/check-gospel-internet-i18n.mjs index 9b78b79cd..cffd277a5 100644 --- a/plugin-designs/gospel-internet/scripts/check-gospel-internet-i18n.mjs +++ b/plugin-designs/gospel-internet/scripts/check-gospel-internet-i18n.mjs @@ -48,7 +48,7 @@ const get = (lc, key) => { // SETTLED, not deferred: the correct word in that language IS the English spelling. Each is a decision, // listed by locale and key so a new identical string is still caught. const SAME_ON_PURPOSE = new Set(['fr:res.source', 'fr:res.type', 'ro:tree.dialect', 'fr:facet.religion', - 'de:facet.region', 'de:facet.religion', 'de:tree.poster', 'it:tree.poster', 'ro:tree.poster', 'de:col.openWiki', 'es:col.openWiki', 'it:col.openWiki', 'ro:col.openWiki']) + 'de:facet.region', 'de:facet.religion', 'de:tree.poster', 'it:tree.poster', 'ro:tree.poster', 'de:col.openWiki', 'es:col.openWiki', 'it:col.openWiki', 'ro:col.openWiki', 'de:rings.ring']) let fails = 0 if (keys.size < 100) { console.log(`NOT MEASURED: read only ${keys.size} keys (expected 100+); the parser is broken, not the bundle`); process.exitCode = 75 }