{"record":{"id":"3969540e8e12e271","repo":"koala73/worldmonitor","slug":"unknown-scenario-scenarioid-worker","errorCode":null,"errorMessage":"Unknown scenario: ${scenarioId}","messagePattern":"Unknown scenario: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/scenario-worker.mjs","lineNumber":230,"sourceCode":"}\n\n// ────────────────────────────────────────────────────────────────────────────\n// 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","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/scenario-worker.mjs#L212-L248","documentation":"computeScenario(scenarioId, iso2, disruptionPct) in scripts/scenario-worker.mjs resolves the scenario by looking up scenarioId in the SCENARIO_TEMPLATES list. If no template has a matching id, it throws `Unknown scenario: ${scenarioId}` because only template-defined scenarios can be computed.","triggerScenarios":"Calling computeScenario('tariff-shok', ...) or any id not present in SCENARIO_TEMPLATES — via direct import, a worker message, or an API/CLI path that forwards a raw scenario id from client input.","commonSituations":"Typo or outdated scenario id sent from a UI/form; ids renamed in SCENARIO_TEMPLATES while cached clients still send the old id; case-sensitivity mismatch ('Tariff-Shock' vs 'tariff-shock'); building an id dynamically by string concatenation.","solutions":["Log/inspect SCENARIO_TEMPLATES and use an exact existing id (they are case-sensitive)","Validate the scenario id against the template list before calling computeScenario (or return a 400 to the caller)","Check for renames: if an id changed in SCENARIO_TEMPLATES, update clients/caches still sending the old id"],"exampleFix":"// before\nawait computeScenario('tariff shock', 'CN');\n// after\nconst id = 'tariff-shock';\nif (!SCENARIO_TEMPLATES.some(t => t.id === id)) throw new Error(`Unsupported scenario: ${id}`);\nawait computeScenario(id, 'CN');","handlingStrategy":"validation","validationCode":"export const SCENARIO_IDS = new Set(SCENARIO_TEMPLATES.map(t => t.id));\nfunction assertValidScenarioId(id) {\n  if (typeof id !== 'string' || !SCENARIO_IDS.has(id)) {\n    throw new Error(`Unknown scenario: ${id}; valid ids: ${[...SCENARIO_IDS].join(', ')}`);\n  }\n}","typeGuard":"const isScenarioId = (id) =>\n  typeof id === 'string' && SCENARIO_TEMPLATES.some(t => t.id === id);","tryCatchPattern":"try {\n  const result = await computeScenario(scenarioId, iso2, disruptionPct);\n  return result;\n} catch (err) {\n  if (/^Unknown scenario: /.test(err.message)) {\n    return { error: 'unknown_scenario', scenarioId, valid: SCENARIO_TEMPLATES.map(t => t.id) };\n  }\n  throw err;\n}","preventionTips":["Expose the list of valid scenario ids to clients (e.g. via an endpoint or shared constant) instead of free-form strings","Validate ids at the boundary (API/worker message) before reaching computeScenario","Treat scenario ids as an enum: rename centrally and update all callers in one change","Watch for case/spacing differences in user-supplied ids; normalize (lowercase, trim) before lookup only if the template ids are lowercase"],"tags":["validation","unknown-identifier","scenario"],"backgroundTag":"invalid-enum-value","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"}