koala73/worldmonitor · error · Error
Invalid disruption override
Error message
Invalid disruption override
What it means
computeScenario() rejects a disruptionPct override that cannot legally apply to the chosen scenario. An override must be an integer in [0,100], and for 'tariff shock' scenarios (those with no affected chokepoints) overrides are forbidden entirely because severity is driven by tariff logic, not a chokepoint disruption percentage.
Solutions
- For tariff-shock scenarios, do not pass disruptionPct at all — leave it undefined to use the template default.
- Clamp and round overrides before the call: Math.min(100, Math.max(0, Math.round(pct))).
- Coerce form/query input to a number first (Number(input)) and check Number.isInteger.
- If a percentage override is genuinely needed for tariff scenarios, extend the template/validation logic rather than bypassing the check.
Example fix
// before
await computeScenario('tariff-shock-2025', null, 40); // tariff scenario rejects overrides
await computeScenario('chokepoint-closure', null, 12.5);
// after
const pct = scenarioId.startsWith('tariff') ? undefined : Math.min(100, Math.max(0, Math.round(Number(rawPct))));
await computeScenario(scenarioId, null, pct); Defensive patterns
Strategy: validation
Validate before calling
const isTariffShock = template.affectedChokepointIds.length === 0;
function sanitizeOverride(rawPct, isTariff) {
if (rawPct === undefined || rawPct === null || isTariff) return undefined;
const n = Number(rawPct);
if (!Number.isInteger(n) || n < 0 || n > 100) throw new Error(`Override must be an integer 0-100, got ${rawPct}`);
return n;
} Type guard
const isValidOverride = (v) => v === undefined || (typeof v === 'number' && Number.isInteger(v) && v >= 0 && v <= 100);
Try / catch
try {
await computeScenario(id, iso2, pct);
} catch (e) {
if (e.message === 'Invalid disruption override') {
log.warn(`Override ${pct} rejected for ${id}; falling back to template default`);
return computeScenario(id, iso2, undefined);
}
throw e;
} Prevention
- Clamp and round slider/form values (Math.round, min 0, max 100) before sending.
- Coerce strings from query params/forms with Number() and validate Number.isInteger.
- Know which scenario templates are tariff-shock and disable the override control in the UI for them.
- Add a shared validator used by both UI and worker so rules never drift.
When it happens
Trigger: Calling computeScenario for a tariff-shock template (affectedChokepointIds empty) with any disruptionPct other than undefined; or for any scenario with a non-integer (e.g. 12.5), out-of-range (<0 or >100), or non-number (string '50') override.
Common situations: A slider UI producing fractional values; passing a form string instead of a number; assuming overrides apply to all scenario types; sending 0 or 150 from unbounded input; API callers replaying overrides against a tariff scenario id.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- Invalid country
- digestHour must be an integer 0–23
- quietHoursStart must be an integer 0–23
- quietHoursEnd must be an integer 0–23
- alertThreshold must be between 0 and 100
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/b52b886229eb702b.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/scenario-worker.mjs:236
/** @param {number} score @param {number} severity @param {number} multiplier */
export function physicalImpact(score, severity, multiplier) {
return score * (severity / 100) * multiplier;
}
/**
* @param {string} scenarioId
* @param {string | null} iso2
* @param {number | undefined} [disruptionPct]
*/
export async function computeScenario(scenarioId, iso2, disruptionPct) {
const template = SCENARIO_TEMPLATES.find(t => t.id === scenarioId);
if (!template) throw new Error(`Unknown scenario: ${scenarioId}`);
const isTariffShock = template.affectedChokepointIds.length === 0;
if (iso2 !== null && (typeof iso2 !== 'string' || !/^[A-Z]{2}$/.test(iso2))) {
throw new Error('Invalid country');
}
if (disruptionPct !== undefined && (isTariffShock || !Number.isInteger(disruptionPct) || disruptionPct < 0 || disruptionPct > 100)) {
throw new Error('Invalid disruption override');
}
const severity = disruptionPct ?? template.disruptionPct;
const manifest = await redisGet('seed-meta:supply_chain:chokepoint-exposure').catch(() => null);
const validIds = (values, pattern, limit) => Array.isArray(values) && values.length > 0
&& values.length <= limit && values.every(v => typeof v === 'string' && pattern.test(v))
&& new Set(values).size === values.length;
// Deliberately NOT gated on `manifest.status === 'ok'`. The manifest's country/sector
// arrays describe the seeder's static universe, not the outcome of its last run — a
// failed run leaves them true while invalidating per-key freshness, which the per-record
// `missing` state already reports. Requiring 'ok' here turned any single seeder failure
// into a total feature blackout even though the exposure keys stay TTL-extended.
const manifestKnown = manifest?.manifestVersion === 1
&& validIds(manifest.countryIds, /^[A-Z]{2}$/, 250)
&& validIds(manifest.hs2Codes, /^(0[1-9]|[1-9][0-9])$/, 99);
const countryIds = iso2 ? [iso2] : manifestKnown ? manifest.countryIds : [];
const hs2Codes = template.affectedHs2 ?? (manifestKnown ? manifest.hs2Codes : []);
const records = [];
const pending = [];View on GitHub (pinned to 7d06c8633d)