koala73/worldmonitor · error

Use at most 30 deliveries per list.

Error message

Use at most 30 deliveries per list.

What it means

The inner deliveries() validator in parseOperationalInput caps each delivery list at MAX_OPERATIONAL_DELIVERIES (30) rows. A longer array is rejected rather than silently truncated, so users get an explicit message instead of a quietly incomplete plan.

Solutions

  1. Reduce the deliveries array to 30 or fewer entries before calling the parser.
  2. Split long schedules across multiple worksheets or aggregate deliveries that share a date.
  3. If the data legitimately needs more rows, request a MAX_OPERATIONAL_DELIVERIES bump upstream instead of bypassing validation.
  4. Validate array length in the import UI and surface a user-facing warning before parse.

Example fix

// before
calculateOperationalBalance({ ...input, deliveries: allDeliveries });
// after
const deliveries = allDeliveries.slice(0, 30);
if (allDeliveries.length > 30) console.warn('Truncated deliveries to the 30-row worksheet limit.');
calculateOperationalBalance({ ...input, deliveries });
Defensive patterns

Strategy: validation

Validate before calling

for (const key of ['deliveries', 'alternativeDeliveries'] as const) {
  if (Array.isArray(input[key]) && input[key].length > 30) {
    throw new Error(`${key} has ${input[key].length} rows; the worksheet allows at most 30.`);
  }
}

Type guard

const withinDeliveryCap = (v: unknown): v is unknown[] =>
  Array.isArray(v) && v.length <= 30;

Try / catch

try {
  const snapshot = calculateOperationalBalance(input);
} catch (err) {
  if (err instanceof Error && err.message === 'Use at most 30 deliveries per list.') {
    input.deliveries = input.deliveries.slice(0, 30);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling parseOperationalInput/calculateOperationalBalance with input.deliveries or input.alternativeDeliveries as an array of 31+ delivery objects; bulk-importing a CSV/log of deliveries without chunking.

Common situations: Importing a season-long supply schedule; a scripted generator producing hundreds of rows; merging multiple worksheets' delivery arrays into one.

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


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/431eef6875b06d82. Report an issue: GitHub.

Appendix: source

Thrown at src/utils/operational-balance.ts:38

}
function date(value: unknown): string {
  if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value || value < '1900-01-01' || value > '9998-12-31') {
    throw new Error('Use a valid date from 1900 through 9998.');
  }
  return value;
}

export function parseOperationalInput(value: unknown): OperationalInput {
  const input = record(value);
  if (typeof input.operation !== 'string' || !input.operation.trim() || input.operation.length > 100) throw new Error('Enter an operation name of 1-100 characters.');
  if (typeof input.unit !== 'string' || !/^[\p{L}\p{N} %./-]{1,24}$/u.test(input.unit) || !input.unit.trim()) throw new Error('Enter one quantity unit of 1-24 characters.');
  const unit = input.unit.trim();
  const startDate = date(input.startDate);
  if (!Number.isInteger(input.horizonDays) || (input.horizonDays as number) < 1 || (input.horizonDays as number) > MAX_OPERATIONAL_DAYS) throw new Error('Horizon must be 1-90 whole days.');
  const horizonDays = input.horizonDays as number;
  if (input.basis !== 'example' && input.basis !== 'user') throw new Error('Input basis must be example or user.');
  const deliveries = (value: unknown): OperationalDelivery[] => {
    if (!Array.isArray(value) || value.length > MAX_OPERATIONAL_DELIVERIES) throw new Error('Use at most 30 deliveries per list.');
    return value.map(value => {
      const row = record(value);
      const deliveryDate = date(row.date);
      if (deliveryDate < startDate || deliveryDate > dateAt(startDate, horizonDays - 1)) throw new Error('Delivery dates must be inside the selected horizon.');
      if (row.unit !== unit) throw new Error(`Every delivery must use ${unit}; mixed units are not converted.`);
      return { date: deliveryDate, unit, quantity: quantity(row.quantity, 'Delivery quantity'), costUsd: row.costUsd === null || row.costUsd === undefined ? null : quantity(row.costUsd, 'Delivery cost') };
    });
  };
  return { operation: input.operation.trim(), basis: input.basis, unit, startDate, horizonDays,
    startingStock: quantity(input.startingStock, 'Starting stock'), dailyDemand: quantity(input.dailyDemand, 'Daily demand'),
    deliveries: deliveries(input.deliveries), alternativeDeliveries: deliveries(input.alternativeDeliveries),
    alternativeDailyDemand: input.alternativeDailyDemand === null ? null : quantity(input.alternativeDailyDemand, 'Alternative daily demand') };
}

export function calculateOperationalBalance(value: unknown): OperationalSnapshot {
  const input = parseOperationalInput(value);
  const balance = (deliveries: OperationalDelivery[], demand: number): OperationalBalance => {
    let stock = input.startingStock;

View on GitHub (pinned to 7d06c8633d)