Number({value:"1"}) is NaN, so the whole reachedness sentence vanished
The live API sends imb_gsec as { value, label }, not a number. describeGroup read it
with Number(), got NaN, matched no GSEC level, and dropped the level sentence — not an
error, not a fallback, simply absent, so the row described a people group nobody has a
reachedness figure for. upg100 hit it and fixed their own normalizer; that fixes their
caller and nobody else's, and every other consumer handing this function a RAW API
record lost the sentence the same silent way.
The unwrap belongs at the READ, because this function's contract is "an API or
normalized row" and both spellings are inside that contract. pick() now flattens
{ value, label } to its value, exactly as normalize.js:22 does.
THE LANGUAGE CLAUSE WAS GOING THE SAME WAY, FOR THE SAME REASON, and fixing only GSEC
would have left it. On a raw record the language NAME is at primary_language.label; the
field itself holds an ISO code. The existing read deliberately refuses that code -- it
would print "and speak abf" -- so a raw record simply had no language and fell to the
country-only sentence. labelOf() takes the display half of the same pair, and ONLY for
a nested object: consulting a flattened primary_language would print the code, which is
the defect the refusal exists to prevent. Same for country_code, whose nested label is
also the English fall-back when no resolver is injected.
A raw record now produces a string identical to the normalized one, which is the
assertion that makes the rest of it hard to regress.
Negative-tested: removing the unwrap, dropping either nested-label read, or letting
labelOf read bare strings (so the code prints) each turn it red; restored, green.
This commit is contained in:
parent
42b4c8f16b
commit
18bcaec446
2 changed files with 62 additions and 4 deletions
|
|
@ -92,6 +92,38 @@ eq('no resolver, label present -> the label', describeGroup({ ...G, country_code
|
|||
'The Abai Sungai live in Malaysia and speak Abai Sungai. Fewer than 2% are evangelical, and no one has been planting churches.')
|
||||
has('resolver returning nothing falls through to the code', describeGroup(G, 'en', { strings: S, countryName: () => '' }), 'live in MYS')
|
||||
|
||||
// 3e. ★ A RAW API RECORD MUST DESCRIBE THE SAME PEOPLE AS A NORMALIZED ONE. The live API sends several
|
||||
// fields as { value, label } pairs, and Number({value:'1'}) is NaN — so on a raw record the GSEC sentence
|
||||
// did not fall back, it silently VANISHED, and the row read as a people group with no reachedness figure
|
||||
// at all. upg100 fixed their own normalizer; every other caller passing the raw record lost it the same
|
||||
// way, which is why the unwrap belongs at the read. The language clause went the same way for the same
|
||||
// reason: the NAME is at primary_language.label on a raw record.
|
||||
const RAW = {
|
||||
name: 'Abai Sungai',
|
||||
country_code: { value: 'MYS', label: 'Malaysia' },
|
||||
primary_language: { value: 'abf', label: 'Abai Sungai' },
|
||||
imb_gsec: { value: '1', label: 'Fewer than 2% evangelical' },
|
||||
}
|
||||
eq('a raw record reads exactly like the normalized one', describeGroup(RAW, 'en', O), describeGroup(G, 'en', O))
|
||||
has('the level sentence survives a nested imb_gsec', describeGroup(RAW, 'en', O), 'Fewer than 2% are evangelical')
|
||||
has('the language NAME comes from the nested label, not the code', describeGroup(RAW, 'en', O), 'speak Abai Sungai.')
|
||||
hasNot('and the nested code is never printed', describeGroup(RAW, 'en', O), 'abf')
|
||||
has('a nested country_code still localizes from its value', describeGroup(RAW, 'ar', O), 'ماليزيا')
|
||||
// The nested label is also the English fall-back when no resolver is injected — the same ladder, reached
|
||||
// through the raw shape rather than through country_code_label.
|
||||
has('no resolver -> the nested country label, not the bare code',
|
||||
describeGroup(RAW, 'en', { strings: S }), 'live in Malaysia')
|
||||
// An inverted name arrives inverted in the nested shape too, and must still be un-inverted.
|
||||
has('a nested inverted label is un-inverted',
|
||||
describeGroup({ ...RAW, primary_language: { value: 'ssh', label: 'Arabic, Shihhi' } }, 'en', O), 'speak Shihhi Arabic.')
|
||||
// ...and the rule that a MISSING field drops its clause is unchanged by any of this.
|
||||
eq('a nested pair with an empty label drops the clause rather than printing nothing',
|
||||
describeGroup({ ...RAW, primary_language: { value: 'abf', label: '' } }, 'en', O),
|
||||
'The Abai Sungai live in Malaysia. Fewer than 2% are evangelical, and no one has been planting churches.')
|
||||
eq('a nested imb_gsec with no value drops the level, not the row',
|
||||
describeGroup({ ...RAW, imb_gsec: { value: '', label: '' } }, 'en', O),
|
||||
'The Abai Sungai live in Malaysia and speak Abai Sungai.')
|
||||
|
||||
// 4. missing or nonsense GSEC omits the level rather than inventing one or throwing
|
||||
hasNot('no gsec -> no level sentence', describeGroup({ ...G, imb_gsec: undefined }, 'en', O), 'evangelical')
|
||||
hasNot('gsec 9 is not a level', describeGroup({ ...G, imb_gsec: 9 }, 'en', O), 'evangelical')
|
||||
|
|
@ -149,4 +181,4 @@ if (fails.length) {
|
|||
for (const f of fails) console.error(` ✗ ${f}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('DESCRIBE-GROUP: PASS — every GSEC level states its own fact, an index-ordered language label is un-inverted without shredding a parenthetical, a missing clause is dropped rather than blanked, the country is localized from its code, and an unknown locale falls back per key.')
|
||||
console.log('DESCRIBE-GROUP: PASS — every GSEC level states its own fact, a raw { value, label } record describes the same people as a normalized one, an index-ordered language label is un-inverted without shredding a parenthetical, a missing clause is dropped rather than blanked, the country is localized from its code, and an unknown locale falls back per key.')
|
||||
|
|
|
|||
|
|
@ -40,15 +40,37 @@ import { naturalLanguageLabel } from '../utils/languageLabel.js'
|
|||
|
||||
const LEVELS = [0, 1, 2, 3, 4, 5, 6]
|
||||
|
||||
/**
|
||||
* Unwrap the API's { value, label } pair to its VALUE — the same flattening normalize.js:22 applies, done
|
||||
* here so a caller that hands us a RAW record gets the same answer as one that hands us a normalized one.
|
||||
*
|
||||
* ★ WITHOUT THIS, Number({value:'1'}) IS NaN AND THE WHOLE GSEC SENTENCE DISAPPEARS. Not an error, not a
|
||||
* fallback: the level clause is simply absent, and the row reads as a people group nobody has a reached-
|
||||
* ness figure for. upg100 hit it and fixed their own normalizer (3b644893), which fixes their caller and
|
||||
* nobody else's — every other consumer passing the raw record loses the sentence the same silent way. The
|
||||
* unwrap belongs here, at the read, because this function's contract is "an API/normalized row" and both
|
||||
* spellings are in that contract.
|
||||
*/
|
||||
const unwrap = (v) => (v && typeof v === 'object' && !Array.isArray(v) && 'value' in v ? v.value : v)
|
||||
|
||||
/** The first field present, trimmed; '' when none is. Accepts both the API and normalized spellings. */
|
||||
const pick = (group, ...names) => {
|
||||
for (const n of names) {
|
||||
const v = group && group[n]
|
||||
const v = unwrap(group && group[n])
|
||||
if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* The DISPLAY half of the same pair, and only when the field really is one. A raw record carries the
|
||||
* human name at `<field>.label`; a normalized one moved it to `<field>_label` and left `<field>` holding a
|
||||
* CODE. So this returns a label ONLY for a nested object and '' for a bare string — consulting a
|
||||
* flattened `primary_language` would print "and speak abf", which is the defect the field comment below
|
||||
* exists to prevent. Same rule, opposite half: pick() takes the value, labelOf() takes the name.
|
||||
*/
|
||||
const labelOf = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? String(v.label ?? '').trim() : '')
|
||||
|
||||
const fill = (tpl, params) =>
|
||||
String(tpl).replace(/\{(\w+)\}/g, (m, k) => (params[k] !== undefined ? params[k] : m))
|
||||
|
||||
|
|
@ -98,7 +120,9 @@ export function describeGroup(group, locale, opts = {}) {
|
|||
// always renders. A missing injection is an English country name, never an empty row.
|
||||
const iso = pick(group, 'country_code', 'countryIso', 'ISOAlpha3', 'countryCode')
|
||||
const country = (iso && opts.countryName ? opts.countryName(iso, lc) : '')
|
||||
|| pick(group, 'country_code_label', 'country', 'CountryName', 'countryName')
|
||||
|| pick(group, 'country_code_label')
|
||||
|| labelOf(group && group.country_code)
|
||||
|| pick(group, 'country', 'CountryName', 'countryName')
|
||||
|| iso
|
||||
// A language NAME is a proper noun; it is not translated, and there is no source that would.
|
||||
//
|
||||
|
|
@ -112,7 +136,9 @@ export function describeGroup(group, locale, opts = {}) {
|
|||
// 725 of 12,379 rows carry one. naturalLanguageLabel un-inverts it, sharing its split with the
|
||||
// language-family legend so the two readings of the same field cannot drift apart.
|
||||
const language = naturalLanguageLabel(
|
||||
pick(group, 'primary_language_label', 'LanguageName', 'languageName'),
|
||||
pick(group, 'primary_language_label')
|
||||
|| labelOf(group && group.primary_language)
|
||||
|| pick(group, 'LanguageName', 'languageName'),
|
||||
)
|
||||
|
||||
const whereKey = language && country ? 'desc_where_both'
|
||||
|
|
|
|||
Loading…
Reference in a new issue