{"record":{"id":"56b9eb672be0e80b","repo":"koala73/worldmonitor","slug":"invalid-scorecard-bloc-selection","errorCode":null,"errorMessage":"Invalid scorecard bloc selection.","messagePattern":"Invalid scorecard bloc selection\\.","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/scorecard/v1/get-bloc-scorecard.ts","lineNumber":26,"sourceCode":"// @ts-expect-error — JS module, no declaration file\nimport { captureSilentError } from '../../../../api/_sentry-edge.js';\nimport { markNoStoreFallbackResponse } from '../../../_shared/response-headers';\nimport { resolveBlocSelection } from './_bloc-presets';\nimport { asFiveFactorSnapshot, readFiveFactorSnapshot, type ScorecardSnapshotReader } from './_read-snapshot';\nimport { toPublicBlocScorecard } from './_response';\nimport { scoreBloc } from './_score-bloc';\nimport type { CountryScorecardEvidence } from './_types';\n\nexport async function getBlocScorecardWithReader(\n  ctx: ServerContext,\n  req: GetBlocScorecardRequest,\n  reader: ScorecardSnapshotReader,\n): Promise<GetBlocScorecardResponse> {\n  let selection;\n  try {\n    selection = resolveBlocSelection(req);\n  } catch (error) {\n    throw new ValidationError([{\n      field: req.preset ? 'preset' : 'members',\n      description: error instanceof Error ? error.message : 'Invalid scorecard bloc selection.',\n    }]);\n  }\n  let snapshotValue: unknown;\n  try {\n    snapshotValue = await reader(selection.members);\n  } catch (error) {\n    console.warn('[scorecard] snapshot read failed operation=get-bloc-scorecard', error instanceof Error ? error.message : 'unknown');\n    void captureSilentError(error, { tags: { route: 'scorecard/get-bloc-scorecard', step: 'snapshot-read' } });\n    return markNoStoreFallbackResponse(ctx.request, {\n      unavailable: true,\n      unavailableReason: 'scorecard-snapshot-unavailable',\n    });\n  }\n  const snapshot = asFiveFactorSnapshot(snapshotValue);\n  if (!snapshot) {\n    return markNoStoreFallbackResponse(ctx.request, {","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/server/worldmonitor/scorecard/v1/get-bloc-scorecard.ts#L8-L44","documentation":"getBlocScorecard validates the bloc request via resolveBlocSelection, which enforces: exactly one of preset or members, preset must be one of USMCA/EU27/BRICS/GCC/ASEAN/NATO, custom members must be 2-30 unique uppercase ISO-2 codes from the rankable universe. Any resolveBlocSelection throw is wrapped in a ValidationError naming the offending field ('preset' or 'members').","triggerScenarios":"Passing both preset and members; passing neither; unknown preset string (e.g. 'EU' or 'brics' lowercase); custom members with <2 or >30 entries; non-uppercase or non-2-letter member codes; duplicate members; countries outside the rankable universe.","commonSituations":"Client UI sending both a preset and a stale members array; hardcoded preset names that drift from SCORECARD_BLOC_PRESETS; lowercase country codes from user input; including countries like 'HK' or 'TW' that are not in the rankable universe.","solutions":["Send exactly one of preset or members, not both","Use a preset from SCORECARD_BLOC_PRESETS: USMCA, EU27, BRICS, GCC, ASEAN, NATO (case-sensitive)","For custom blocs, send 2-30 unique uppercase ISO-2 codes that are in the rankable universe","Normalize client input: trim, uppercase, dedupe, and filter members before calling"],"exampleFix":"// before\ngetBlocScorecard({ preset: 'EU', members: ['DE'] })\n// after\ngetBlocScorecard({ preset: 'EU27' })\n// or custom:\ngetBlocScorecard({ members: ['DE', 'FR'] })","handlingStrategy":"validation","validationCode":"const PRESETS = ['USMCA','EU27','BRICS','GCC','ASEAN','NATO'];\nfunction validBlocInput(req) {\n  const hasPreset = Boolean(String(req.preset || '').trim());\n  const members = Array.isArray(req.members) ? req.members : [];\n  if (hasPreset === (members.length > 0)) return false;\n  if (hasPreset) return PRESETS.includes(req.preset.trim());\n  const uniq = [...new Set(members.map((m) => String(m).trim().toUpperCase()))];\n  return uniq.length >= 2 && uniq.length <= 30 && uniq.every((m) => /^[A-Z]{2}$/.test(m));\n}","typeGuard":"function isBlocRequest(req) {\n  return typeof req === 'object' && req !== null &&\n    (typeof req.preset === 'string' || Array.isArray(req.members));\n}","tryCatchPattern":"try {\n  const res = await getBlocScorecard(req);\n} catch (e) {\n  if (e.name === 'ValidationError' && (e.field === 'preset' || e.field === 'members')) {\n    // surface e.description, correct the bloc selection\n  } else throw e;\n}","preventionTips":["Always send exactly one of preset or members","Use preset names copied from SCORECARD_BLOC_PRESETS, not ad-hoc abbreviations","Uppercase, dedupe, and rankable-universe-filter custom members client-side","Keep member lists between 2 and 30 entries"],"tags":["validation","input-validation","api"],"backgroundTag":"invalid-request-parameter","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"}