Hold the packet's ring names to the screen's, and adopt its exact words
The agreed ring vocabulary now lives in two catalogues in eleven locales each -- gospelInternet.json for the screen legend, packet-strings.json for the printed sheet -- and nothing held them together. They legitimately want different SHAPES (the screen renders the axes as standalone labels, the packet composes them into one string per ring), so neither can import the other; but the WORDS must match. The failure that makes this worth a gate is quiet: one cell is provisional on the Driver's row 89, and it appears in rings 1, 4 and 7. Answer it in one file and not the other and the sheet and the screen state different things about the same ring, each correct in isolation, with no error anywhere. Only a reader holding both would notice. Measuring first showed 36 of 77 cells already differed, in two classes. Most were grammar -- the packet's culture terms follow a comma mid-sentence, so they are lowercase -- but some were genuinely different words: "Same family" against "Same language family", German "verwandte" against "Nahe", Hindi "वही जन" against "वही जनसमूह". Those are drift, and the screen's wording wins because it is what readers already see. The packet now adopts every term verbatim, lowercasing only the first character of the culture half, which is a no-op in zh/ar/hi and leaves German common nouns capitalised. That adoption caught a bug in my own assertion: I had hardcoded ring 7-9 as "Same family" from CenteredSetTarget.vue's header, while the shipping string is "Same language family". A summary in a comment is a description of the data, never a second source of it. The gate also refuses to pass when it compared nothing -- rename a key on either side and every lookup returns undefined, every pair is skipped, and a silent check reports success. Mutation-tested both ways: answering row 89 on the screen only, and renaming a packet key.
This commit is contained in:
parent
fccf6ee2fe
commit
4b45285b7d
3 changed files with 107 additions and 14 deletions
|
|
@ -115,9 +115,15 @@ ok(count(full, /class="sheet/g) - count(light, /class="sheet/g) === 3,
|
|||
}
|
||||
const all = only([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
||||
ok(all.length === 10, `all ten rings populated should list ten names, got ${all.length}`)
|
||||
// ⚠ THE CATALOGUE IS AUTHORITATIVE OVER THE COMMENT. CenteredSetTarget.vue's header abbreviates
|
||||
// ring 7-9 as "Same family"; the shipping string in gospelInternet.json is "Same language family",
|
||||
// and that is the word the SCREEN renders. I hardcoded the header's shorthand here and this
|
||||
// assertion caught it the moment the packet adopted the screen's wording verbatim. A summary in a
|
||||
// comment is a description of the data, never a second source of it.
|
||||
const want = ['Same dialect, same people', 'Same dialect, near culture', 'Same dialect, different culture',
|
||||
'Same language, same people', 'Same language, near culture', 'Same language, different culture',
|
||||
'Same family, same people', 'Same family, near culture', 'Same family, different culture', 'Nearby only']
|
||||
'Same language family, same people', 'Same language family, near culture',
|
||||
'Same language family, different culture', 'Nearby only']
|
||||
want.forEach((w, i) => ok((all[i] || '').endsWith(w),
|
||||
`ring ${i + 1} should read ${JSON.stringify(w)}, got ${JSON.stringify(all[i])}`))
|
||||
// ⚠ ONLY THE POPULATED RINGS. Ten rows of vocabulary beside a figure is a glossary, not a legend,
|
||||
|
|
|
|||
87
internal/check-ring-vocabulary.mjs
Normal file
87
internal/check-ring-vocabulary.mjs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-ring-vocabulary — the centered set's ring names say the same thing on the PRINTED sheet as
|
||||
* they do on the SCREEN.
|
||||
*
|
||||
* WHY THIS EXISTS: the same agreed vocabulary now lives in TWO catalogues, in eleven locales each.
|
||||
* screen library/i18n/locales/<lc>/gospelInternet.json rings.band.* / rings.culture.*
|
||||
* packet library/scripts/posters/packet-strings.json ring_band_* / ring_culture_*
|
||||
* They legitimately want different SHAPES — the screen renders the axes as standalone labels, the
|
||||
* packet composes them into one string per ring ("Same dialect, same people") — so neither can
|
||||
* simply import the other. But the WORDS must match, and nothing structural holds them together.
|
||||
*
|
||||
* ★ THE FAILURE THIS CATCHES IS QUIET AND WELL-SHAPED. One cell is provisional on the Driver's row
|
||||
* 89 ("same people" vs "same culture"), and it appears in rings 1, 4 AND 7. Answer it in one file
|
||||
* and not the other and the printed packet and the screen legend state DIFFERENT THINGS about the
|
||||
* same ring — each looking perfectly correct in isolation, with no error anywhere. A reader holding
|
||||
* the sheet beside the app is the only thing that would notice.
|
||||
*
|
||||
* Headless, no network, no DOM. Exit 0 pass · 1 fail.
|
||||
*/
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const read = (p) => JSON.parse(readFileSync(resolve(ROOT, p), 'utf8'))
|
||||
const PACKET = read('library/scripts/posters/packet-strings.json')
|
||||
|
||||
const fails = []
|
||||
const ok = (c, m) => { if (!c) fails.push(m) }
|
||||
|
||||
// The two halves, paired. Read from ONE list so the gate cannot drift from itself.
|
||||
const AXES = [
|
||||
['ring_band_dialect', 'band', 'dialect'],
|
||||
['ring_band_language', 'band', 'language'],
|
||||
['ring_band_family', 'band', 'family'],
|
||||
['ring_band_geography', 'band', 'geography'],
|
||||
['ring_culture_same', 'culture', 'same'],
|
||||
['ring_culture_near', 'culture', 'near'],
|
||||
['ring_culture_different', 'culture', 'different'],
|
||||
]
|
||||
|
||||
// ⚠ THE ONE SANCTIONED DIFFERENCE, and it is a difference of GRAMMAR, not of vocabulary. On screen a
|
||||
// culture term is a standalone label ("Near culture"); in the packet it follows a comma mid-sentence
|
||||
// ("Same dialect, near culture"). So the packet lowercases the FIRST CHARACTER of the culture half
|
||||
// and nothing else — German common nouns stay capitalised ("gleiches Volk"), and zh/ar/hi have no
|
||||
// case at all, where this is a no-op. Comparing case-insensitively instead would be the wrong fix:
|
||||
// it would stop the gate seeing a real capitalisation divergence.
|
||||
const lowerFirst = (s) => (s ? s[0].toLowerCase() + s.slice(1) : s)
|
||||
|
||||
const LOCALES = Object.keys(PACKET).filter((k) => k !== '_meta')
|
||||
ok(LOCALES.length >= 11, `expected the packet catalogue to carry 11 locales, found ${LOCALES.length}`)
|
||||
|
||||
let compared = 0
|
||||
for (const lc of LOCALES) {
|
||||
const p = `library/i18n/locales/${lc}/gospelInternet.json`
|
||||
if (!existsSync(resolve(ROOT, p))) {
|
||||
fails.push(`no screen catalogue for '${lc}' (${p}) — the packet names rings in a locale the screen cannot`)
|
||||
continue
|
||||
}
|
||||
const rings = read(p).rings
|
||||
if (!rings) { fails.push(`'${lc}' gospelInternet.json has no rings block`); continue }
|
||||
for (const [pk, group, leaf] of AXES) {
|
||||
const mine = PACKET[lc] && PACKET[lc][pk]
|
||||
const theirs = rings[group] && rings[group][leaf]
|
||||
// A missing key on either side is its own failure — "both absent" must not read as "they agree".
|
||||
if (mine === undefined) { fails.push(`packet-strings '${lc}' is missing ${pk}`); continue }
|
||||
if (theirs === undefined) { fails.push(`gospelInternet '${lc}' is missing rings.${group}.${leaf}`); continue }
|
||||
const want = group === 'culture' ? lowerFirst(theirs) : theirs
|
||||
compared++
|
||||
ok(mine === want,
|
||||
`'${lc}' ring vocabulary DIVERGED — the printed packet and the screen legend would name this ring differently:\n` +
|
||||
` packet ${pk} = ${JSON.stringify(mine)}\n` +
|
||||
` screen rings.${group}.${leaf} = ${JSON.stringify(theirs)}`)
|
||||
}
|
||||
}
|
||||
// ★ A COMPARISON THAT COMPARED NOTHING IS NOT A PASS. If the key names are ever renamed on either
|
||||
// side, every lookup returns undefined, every pair is skipped, and a silent gate reports success.
|
||||
ok(compared === LOCALES.length * AXES.length,
|
||||
`only ${compared} of ${LOCALES.length * AXES.length} pairs were actually compared — the gate is measuring less than it claims`)
|
||||
|
||||
if (fails.length) {
|
||||
console.error('RING-VOCABULARY: FAIL')
|
||||
for (const f of fails) console.error(` ✗ ${f}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`RING-VOCABULARY: PASS — the packet and the screen name every ring band and culture identically across ${LOCALES.length} locales (${compared} pairs compared), so answering row 89 in one catalogue alone cannot go unnoticed.`)
|
||||
|
|
@ -221,7 +221,7 @@
|
|||
"sets_centered_tally": "{placed} placed · {centre} at the centre · {unplaced} unplaced",
|
||||
"ring_band_dialect": "Same dialect",
|
||||
"ring_band_language": "Same language",
|
||||
"ring_band_family": "Same family",
|
||||
"ring_band_family": "Same language family",
|
||||
"ring_band_geography": "Nearby only",
|
||||
"ring_culture_same": "same people",
|
||||
"ring_culture_near": "near culture",
|
||||
|
|
@ -442,7 +442,7 @@
|
|||
"sets_centered_tally": "{placed} ubicados · {centre} en el centro · {unplaced} sin ubicar",
|
||||
"ring_band_dialect": "Mismo dialecto",
|
||||
"ring_band_language": "Misma lengua",
|
||||
"ring_band_family": "Misma familia",
|
||||
"ring_band_family": "Misma familia lingüística",
|
||||
"ring_band_geography": "Solo cercanía",
|
||||
"ring_culture_same": "mismo pueblo",
|
||||
"ring_culture_near": "cultura cercana",
|
||||
|
|
@ -663,8 +663,8 @@
|
|||
"sets_centered_tally": "{placed} placés · {centre} au centre · {unplaced} non placés",
|
||||
"ring_band_dialect": "Même dialecte",
|
||||
"ring_band_language": "Même langue",
|
||||
"ring_band_family": "Même famille",
|
||||
"ring_band_geography": "Proximité seule",
|
||||
"ring_band_family": "Même famille de langues",
|
||||
"ring_band_geography": "Proximité seulement",
|
||||
"ring_culture_same": "même peuple",
|
||||
"ring_culture_near": "culture proche",
|
||||
"ring_culture_different": "culture différente",
|
||||
|
|
@ -882,7 +882,7 @@
|
|||
"sets_centered_tally": "{placed} posicionados · {centre} no centro · {unplaced} sem posição",
|
||||
"ring_band_dialect": "Mesmo dialeto",
|
||||
"ring_band_language": "Mesma língua",
|
||||
"ring_band_family": "Mesma família",
|
||||
"ring_band_family": "Mesma família linguística",
|
||||
"ring_band_geography": "Apenas proximidade",
|
||||
"ring_culture_same": "mesmo povo",
|
||||
"ring_culture_near": "cultura próxima",
|
||||
|
|
@ -1106,7 +1106,7 @@
|
|||
"ring_band_family": "Gleiche Sprachfamilie",
|
||||
"ring_band_geography": "Nur benachbart",
|
||||
"ring_culture_same": "gleiches Volk",
|
||||
"ring_culture_near": "verwandte Kultur",
|
||||
"ring_culture_near": "nahe Kultur",
|
||||
"ring_culture_different": "andere Kultur",
|
||||
"ring_join": "{band}, {culture}"
|
||||
},
|
||||
|
|
@ -1322,7 +1322,7 @@
|
|||
"sets_centered_tally": "{placed} collocati · {centre} al centro · {unplaced} non collocati",
|
||||
"ring_band_dialect": "Stesso dialetto",
|
||||
"ring_band_language": "Stessa lingua",
|
||||
"ring_band_family": "Stessa famiglia",
|
||||
"ring_band_family": "Stessa famiglia linguistica",
|
||||
"ring_band_geography": "Solo vicinanza",
|
||||
"ring_culture_same": "stesso popolo",
|
||||
"ring_culture_near": "cultura vicina",
|
||||
|
|
@ -1541,8 +1541,8 @@
|
|||
"sets_centered_tally": "{placed} plasate · {centre} în centru · {unplaced} neplasate",
|
||||
"ring_band_dialect": "Același dialect",
|
||||
"ring_band_language": "Aceeași limbă",
|
||||
"ring_band_family": "Aceeași familie",
|
||||
"ring_band_geography": "Doar în apropiere",
|
||||
"ring_band_family": "Aceeași familie de limbi",
|
||||
"ring_band_geography": "Doar apropiere",
|
||||
"ring_culture_same": "același popor",
|
||||
"ring_culture_near": "cultură apropiată",
|
||||
"ring_culture_different": "cultură diferită",
|
||||
|
|
@ -1762,7 +1762,7 @@
|
|||
"sets_centered_tally": "{placed} размещено · {centre} в центре · {unplaced} без места",
|
||||
"ring_band_dialect": "Тот же диалект",
|
||||
"ring_band_language": "Тот же язык",
|
||||
"ring_band_family": "Та же семья",
|
||||
"ring_band_family": "Та же языковая семья",
|
||||
"ring_band_geography": "Только соседство",
|
||||
"ring_culture_same": "тот же народ",
|
||||
"ring_culture_near": "близкая культура",
|
||||
|
|
@ -2204,7 +2204,7 @@
|
|||
"ring_band_language": "वही भाषा",
|
||||
"ring_band_family": "वही भाषा-परिवार",
|
||||
"ring_band_geography": "केवल निकटता",
|
||||
"ring_culture_same": "वही जन",
|
||||
"ring_culture_same": "वही जनसमूह",
|
||||
"ring_culture_near": "निकट संस्कृति",
|
||||
"ring_culture_different": "भिन्न संस्कृति",
|
||||
"ring_join": "{band}, {culture}"
|
||||
|
|
@ -2424,8 +2424,8 @@
|
|||
"ring_band_family": "同一语系",
|
||||
"ring_band_geography": "仅地理相邻",
|
||||
"ring_culture_same": "同一族群",
|
||||
"ring_culture_near": "相近文化",
|
||||
"ring_culture_different": "不同文化",
|
||||
"ring_culture_near": "文化相近",
|
||||
"ring_culture_different": "文化不同",
|
||||
"ring_join": "{band},{culture}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue