{"record":{"id":"6bf7b0fb64ac90f2","repo":"koala73/worldmonitor","slug":"custom-scorecard-bloc-members-must-belong-to-the-p","errorCode":null,"errorMessage":"Custom scorecard bloc members must belong to the public rankable country universe.","messagePattern":"Custom scorecard bloc members must belong to the public rankable country universe\\.","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/scorecard/v1/_bloc-presets.ts","lineNumber":42,"sourceCode":"};\n\nexport function resolveBlocSelection(input: { preset?: string; members?: string[] }): ScorecardBlocSelection {\n  const preset = String(input.preset || '').trim();\n  const members = Array.isArray(input.members) ? input.members : [];\n  if (Boolean(preset) === (members.length > 0)) {\n    throw new Error('Select exactly one scorecard bloc preset or custom member list.');\n  }\n  if (preset) {\n    if (!(preset in SCORECARD_BLOC_PRESETS)) throw new Error(`Unknown scorecard bloc preset: ${preset}`);\n    return SCORECARD_BLOC_PRESETS[preset as ScorecardBlocPreset];\n  }\n  if (members.length < 2 || members.length > 30) throw new Error('Custom scorecard blocs require 2-30 members.');\n  if (members.some((member) => !/^[A-Z]{2}$/.test(member))) {\n    throw new Error('Custom scorecard bloc members must be uppercase ISO-2 codes.');\n  }\n  if (new Set(members).size !== members.length) throw new Error('Custom scorecard bloc members must be unique.');\n  if (members.some((member) => !isInRankableUniverse(member))) {\n    throw new Error('Custom scorecard bloc members must belong to the public rankable country universe.');\n  }\n  const sorted = [...members].sort();\n  return { id: `custom:${sorted.join('-')}`, label: sorted.join(' + '), members: sorted };\n}\n","sourceCodeStart":24,"sourceCodeEnd":47,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/server/worldmonitor/scorecard/v1/_bloc-presets.ts#L24-L47","documentation":"Even well-formed, unique, uppercase ISO-2 codes are only accepted if each belongs to the public rankable country universe (checked via isInRankableUniverse(member)). The scorecard ranks countries against a curated dataset universe; microstates, territories, or codes without underlying indicator data are excluded so blocs stay comparable. Codes outside the universe (e.g., 'VA', 'HK' depending on the current universe, or 'AQ') throw this error.","triggerScenarios":"Calling getBlocScorecardWithReader with a custom list containing any non-rankable code: territories (e.g., 'GL', 'PR'), microstates ('MC', 'SM'), or any ISO-2 code absent from the published rankable universe, even if the rest of the list is valid.","commonSituations":"1) A world country-picker lists all ISO-3166 entries including dependencies and microstates that the dataset doesn't rank; 2) universe changes between versions — a code previously rankable is dropped after a data-source revision; 3) users deliberately test edge codes ('AQ' Antarctica).","solutions":["Filter members through the published rankable-universe list (or isInRankableUniverse if you can import it) before sending.","Use the API's country listing (listScorableCountries / static index) to populate the picker so only rankable codes are offered.","After filtering, confirm ≥2 members remain and meet all other rules; otherwise switch to a preset.","Pin your client to the API version whose universe your lists were built against."],"exampleFix":"// before\nresolveBlocSelection({ members: ['US', 'VA', 'MC'] }); // throws: VA/MC not rankable\n// after\nconst members = raw.filter((m) => isInRankableUniverse(m));\nif (members.length >= 2) resolveBlocSelection({ members });","handlingStrategy":"validation","validationCode":"const rankable = await fetchRankableUniverse(); // from listScorableCountries or static index\nconst members = raw.filter((m) => rankable.includes(m));\nif (members.length < 2) throw new Error('not enough rankable members for a bloc');","typeGuard":"function isRankable(code: string, universe: ReadonlySet<string>): code is string {\n  return universe.has(code);\n}","tryCatchPattern":"try {\n  const bloc = await getBlocScorecard({ members });\n} catch (err) {\n  if (err.message.includes('rankable country universe')) {\n    const excluded = members.filter((m) => !universe.has(m));\n    return badRequest({ excluded, hint: 'choose from listScorableCountries' });\n  }\n  throw err;\n}","preventionTips":["Populate pickers from listScorableCountries, not raw ISO-3166 lists","Filter territories/microstates out of custom selections","Re-filter on API version upgrades (universe can change)","Keep a cached copy of the rankable universe client-side for pre-validation"],"tags":["validation","scorecard","data-universe","bad-request"],"backgroundTag":"invalid-bloc-selection","analyzedSha":"9361220cc013571781071f0206e4d80fd14b2f7f","analyzedAt":"2026-09-01T10:32:37.851Z","contentChangedAt":"2026-09-01T10:32:37.851Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}