koala73/worldmonitor · error · Error

Select exactly one scorecard bloc preset or custom member li

Error message

Select exactly one scorecard bloc preset or custom member list.

What it means

resolveBlocSelection in server/worldmonitor/scorecard/v1/_bloc-presets.ts enforces XOR selection: a caller must provide exactly one of a named bloc preset OR a custom member list. If both are provided (preset plus non-empty members), or neither is provided (no preset and empty members), it throws this error. Presets come from SCORECARD_BLOC_PRESETS (USMCA, EU27, ASEAN, BRICS, etc.); custom lists bypass the preset table entirely.

Source

Thrown at server/worldmonitor/scorecard/v1/_bloc-presets.ts:30

    id: 'EU27',
    label: 'European Union',
    members: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'],
  },
  BRICS: { id: 'BRICS', label: 'BRICS', members: ['BR', 'RU', 'IN', 'CN', 'ZA', 'SA', 'EG', 'AE', 'ET', 'ID', 'IR'] },
  GCC: { id: 'GCC', label: 'Gulf Cooperation Council', members: ['AE', 'BH', 'KW', 'OM', 'QA', 'SA'] },
  ASEAN: { id: 'ASEAN', label: 'ASEAN', members: ['BN', 'KH', 'ID', 'LA', 'MY', 'MM', 'PH', 'SG', 'TH', 'TL', 'VN'] },
  NATO: {
    id: 'NATO',
    label: 'NATO',
    members: ['AL', 'BE', 'BG', 'CA', 'HR', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IS', 'IT', 'LV', 'LT', 'LU', 'ME', 'NL', 'MK', 'NO', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'TR', 'GB', 'US'],
  },
};

export function resolveBlocSelection(input: { preset?: string; members?: string[] }): ScorecardBlocSelection {
  const preset = String(input.preset || '').trim();
  const members = Array.isArray(input.members) ? input.members : [];
  if (Boolean(preset) === (members.length > 0)) {
    throw new Error('Select exactly one scorecard bloc preset or custom member list.');
  }
  if (preset) {
    if (!(preset in SCORECARD_BLOC_PRESETS)) throw new Error(`Unknown scorecard bloc preset: ${preset}`);
    return SCORECARD_BLOC_PRESETS[preset as ScorecardBlocPreset];
  }
  if (members.length < 2 || members.length > 30) throw new Error('Custom scorecard blocs require 2-30 members.');
  if (members.some((member) => !/^[A-Z]{2}$/.test(member))) {
    throw new Error('Custom scorecard bloc members must be uppercase ISO-2 codes.');
  }
  if (new Set(members).size !== members.length) throw new Error('Custom scorecard bloc members must be unique.');
  if (members.some((member) => !isInRankableUniverse(member))) {
    throw new Error('Custom scorecard bloc members must belong to the public rankable country universe.');
  }
  const sorted = [...members].sort();
  return { id: `custom:${sorted.join('-')}`, label: sorted.join(' + '), members: sorted };
}

View on GitHub (pinned to 9361220cc0)

Solutions

  1. Send exactly one field: either { preset: '<name>' } or { members: [...] }, never both.
  2. If both are set client-side, clear the preset when the user picks custom members (and vice versa) before submitting.
  3. If no selection is intended, don't call the bloc scorecard at all rather than passing empty input.
  4. Guard with a pre-check: Boolean(preset) !== (members.length > 0) must hold before calling.

Example fix

// before
resolveBlocSelection({ preset: 'EU27', members: ['DE', 'FR'] }); // throws
// after
const input = members.length > 0 ? { members } : { preset };
resolveBlocSelection(input);
Defensive patterns

Strategy: validation

Validate before calling

function canSubmitBlocSelection(sel) {
  const preset = String(sel.preset || '').trim();
  const count = Array.isArray(sel.members) ? sel.members.length : 0;
  return Boolean(preset) !== (count > 0); // exactly one of the two
}

Try / catch

try {
  const bloc = await getBlocScorecard(selection);
} catch (err) {
  if (err.message.includes('Select exactly one scorecard bloc preset or custom member list')) {
    resetSelectionUi(); return badRequest('choose either a preset or custom members, not both/neither');
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling getBlocScorecardWithReader / resolveBlocSelection with: (a) { preset: 'EU27', members: ['DE','FR'] } — both supplied; (b) { } or { preset: '', members: [] } — nothing selected; (c) { preset: ' ' } (whitespace-only trims to empty) with no members; (d) { members: [] } only.

Common situations: 1) A UI form keeps a previously selected preset in hidden state while the user also builds a custom list, sending both; 2) a script passes an empty object because its variables were unset; 3) whitespace-only preset strings that look set but trim to ''; 4) API clients send members: [] explicitly, which is treated as 'not selected' and combined with a preset — that is fine — but sending members plus preset is not.

Related errors


AI-assisted analysis of koala73/worldmonitor@9361220cc0 (2026-09-01). Data as JSON: /api/errors/4c1cd738e7bccb90. Report an issue: GitHub.