koala73/worldmonitor · error · Error

Custom scorecard bloc members must be unique.

Error message

Custom scorecard bloc members must be unique.

What it means

resolveBlocSelection rejects custom member lists containing duplicate country codes, comparing new Set(members).size against members.length. Duplicates would double-weight a country in the scorecard aggregation and inflate the bloc, so they are rejected rather than silently deduplicated. This check runs after the count (2–30) and format (uppercase ISO-2) checks.

Source

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

    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. Deduplicate before sending: [...new Set(members)].
  2. Fix the picker UI to toggle selection instead of appending duplicates.
  3. After dedup, re-check the 2–30 count — heavy dedup can drop you below the minimum.
  4. Normalize first (trim/uppercase) so 'US' and 'us ' dedupe to one entry.

Example fix

// before
resolveBlocSelection({ members: ['US', 'CA', 'US'] }); // throws
// after
const members = [...new Set(raw.map((m) => m.trim().toUpperCase()))];
resolveBlocSelection({ members });
Defensive patterns

Strategy: validation

Validate before calling

const members = [...new Set(raw.map((m) => String(m).trim().toUpperCase()))];
if (new Set(members).size !== members.length) throw new Error('duplicate members'); // unreachable after Set, defensive

Try / catch

try {
  const bloc = await getBlocScorecard({ members });
} catch (err) {
  if (err.message.includes('must be unique')) {
    return badRequest({ duplicates: findDuplicates(members) });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling getBlocScorecardWithReader with { members: ['US','CA','US','MX'] } — any repeated code, even from merging two lists or a UI that appends to an existing selection.

Common situations: 1) A multi-select UI appends to the array each time a tag is re-clicked; 2) server-side code merges overlapping blocs (USMCA + custom US list) without deduping; 3) scripted concatenation of two member arrays.

Related errors


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