{"record":{"id":"07f0d2fda5c6d203","repo":"koala73/worldmonitor","slug":"invalid-country-scenario-worker","errorCode":null,"errorMessage":"Invalid country","messagePattern":"Invalid country","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/scenario-worker.mjs","lineNumber":233,"sourceCode":"// Scenario computation\n// ────────────────────────────────────────────────────────────────────────────\n\n/** @param {number} score @param {number} severity @param {number} multiplier */\nexport function physicalImpact(score, severity, multiplier) {\n  return score * (severity / 100) * multiplier;\n}\n\n/**\n * @param {string} scenarioId\n * @param {string | null} iso2\n * @param {number | undefined} [disruptionPct]\n */\nexport async function computeScenario(scenarioId, iso2, disruptionPct) {\n  const template = SCENARIO_TEMPLATES.find(t => t.id === scenarioId);\n  if (!template) throw new Error(`Unknown scenario: ${scenarioId}`);\n  const isTariffShock = template.affectedChokepointIds.length === 0;\n  if (iso2 !== null && (typeof iso2 !== 'string' || !/^[A-Z]{2}$/.test(iso2))) {\n    throw new Error('Invalid country');\n  }\n  if (disruptionPct !== undefined && (isTariffShock || !Number.isInteger(disruptionPct) || disruptionPct < 0 || disruptionPct > 100)) {\n    throw new Error('Invalid disruption override');\n  }\n  const severity = disruptionPct ?? template.disruptionPct;\n  const manifest = await redisGet('seed-meta:supply_chain:chokepoint-exposure').catch(() => null);\n  const validIds = (values, pattern, limit) => Array.isArray(values) && values.length > 0\n    && values.length <= limit && values.every(v => typeof v === 'string' && pattern.test(v))\n    && new Set(values).size === values.length;\n  // Deliberately NOT gated on `manifest.status === 'ok'`. The manifest's country/sector\n  // arrays describe the seeder's static universe, not the outcome of its last run — a\n  // failed run leaves them true while invalidating per-key freshness, which the per-record\n  // `missing` state already reports. Requiring 'ok' here turned any single seeder failure\n  // into a total feature blackout even though the exposure keys stay TTL-extended.\n  const manifestKnown = manifest?.manifestVersion === 1\n    && validIds(manifest.countryIds, /^[A-Z]{2}$/, 250)\n    && validIds(manifest.hs2Codes, /^(0[1-9]|[1-9][0-9])$/, 99);\n  const countryIds = iso2 ? [iso2] : manifestKnown ? manifest.countryIds : [];","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/scenario-worker.mjs#L215-L251","documentation":"computeScenario() validates the iso2 country parameter before running a supply-chain scenario. The parameter must be either exactly null (meaning 'global, all countries') or a 2-letter uppercase ISO-3166 alpha-2 code. Any other value — lowercase, 3 letters, a full country name, a number, or a non-string — throws this error rather than silently producing wrong scenario output.","triggerScenarios":"Calling computeScenario(scenarioId, iso2, disruptionPct) where iso2 is not null and fails /^[A-Z]{2}$/: e.g. 'usa', 'us', 'USA', '', 'US ', 840, undefined (undefined !== null), or a full name like 'United States'.","commonSituations":"Passing a user-supplied country picker value that is a country name or numeric code; forgetting to convert lowercase input with .toUpperCase(); defaulting the parameter to undefined instead of null; UI sending 3-letter alpha-3 codes.","solutions":["Normalize caller input: uppercase and trim before passing, e.g. computeScenario(id, raw?.trim().toUpperCase() ?? null, pct).","Ensure 'no country selected' is encoded as null, not undefined, '' or 0.","Map full names or alpha-3 codes to alpha-2 before the call (lookup table or lib such as i18n-iso-countries).","Add a pre-call validation with the same regex so the failure surfaces at the boundary with context."],"exampleFix":"// before\nawait computeScenario('chokepoint-closure', 'usa', 30);\n// after\nconst iso2 = rawCountry ? rawCountry.trim().toUpperCase() : null; // 'usa' -> 'USA', absent -> null\nif (iso2 !== null && !/^[A-Z]{2}$/.test(iso2)) throw new Error(`Bad country code: ${rawCountry}`);\nawait computeScenario('chokepoint-closure', iso2, 30);","handlingStrategy":"validation","validationCode":"function toIso2(raw) {\n  if (raw === null || raw === undefined || raw === '') return null;\n  const v = String(raw).trim().toUpperCase();\n  if (!/^[A-Z]{2}$/.test(v)) throw new Error(`Not an ISO-3166 alpha-2 code: ${raw}`);\n  return v;\n}\nconst iso2 = toIso2(userCountry);\nawait computeScenario(scenarioId, iso2, pct);","typeGuard":"const isIso2 = (v) => v === null || (typeof v === 'string' && /^[A-Z]{2}$/.test(v));","tryCatchPattern":"try {\n  await computeScenario(id, iso2, pct);\n} catch (e) {\n  if (e.message === 'Invalid country') {\n    notifyUser(`'${iso2}' is not a valid 2-letter country code`);\n    return;\n  }\n  throw e;\n}","preventionTips":["Normalize with trim().toUpperCase() at the input boundary, once.","Represent 'no country' as null everywhere, never undefined.","Keep an alpha-2 whitelist sourced from your chokepoint-exposure data.","Unit-test the validator against lowercase, alpha-3, and empty inputs."],"tags":["validation","arguments","country-code","scenario-worker"],"backgroundTag":"invalid-argument-format","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}