{"record":{"id":"995f6554f4776d12","repo":"koala73/worldmonitor","slug":"batch-ordinals-must-be-contiguous-from-0","errorCode":null,"errorMessage":"batch ordinals must be contiguous from 0","messagePattern":"batch ordinals must be contiguous from 0","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"shared/company-monitoring-contract.ts","lineNumber":319,"sourceCode":"  if (utf8Bytes(JSON.stringify(normalized)) > COMPANY_MONITORING_LIMITS.maxImportRowBytes) {\n    throw new Error(`row exceeds ${COMPANY_MONITORING_LIMITS.maxImportRowBytes} bytes`);\n  }\n  return normalized;\n}\n\nexport function normalizeCompanyImportBatch(inputs: CompanyImportRowInput[]): NormalizedCompanyImportRow[] {\n  if (!Array.isArray(inputs)) throw new Error('import batch must be a list');\n  if (inputs.length === 0) throw new Error('import batch requires at least one row');\n  if (inputs.length > COMPANY_MONITORING_LIMITS.maxImportRows) {\n    throw new Error(`batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportRows} rows`);\n  }\n\n  const normalized = inputs.map(normalizeCompanyImportRow).sort((left, right) => left.ordinal - right.ordinal);\n  const importId = normalized[0]?.clientImportId;\n  for (let index = 0; index < normalized.length; index += 1) {\n    const row = normalized[index]!;\n    if (row.clientImportId !== importId) throw new Error('batch rows must share one clientImportId');\n    if (row.ordinal !== index) throw new Error('batch ordinals must be contiguous from 0');\n  }\n  if (utf8Bytes(JSON.stringify(normalized)) > COMPANY_MONITORING_LIMITS.maxImportBatchBytes) {\n    throw new Error(`batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportBatchBytes} bytes`);\n  }\n  return normalized;\n}\n\nexport function assertCompanyMonitoringPayloadSize(byteLength: number): void {\n  if (!Number.isSafeInteger(byteLength) || byteLength < 0) throw new Error('request byte length is invalid');\n  if (byteLength > COMPANY_MONITORING_LIMITS.maxRequestBytes) {\n    throw new Error(`request exceeds ${COMPANY_MONITORING_LIMITS.maxRequestBytes} bytes`);\n  }\n}\n\nexport function assertCompanyMonitoringAccountContext(\n  context: { ownerAccountId?: string } | null | undefined,\n): string {\n  const ownerAccountId = context?.ownerAccountId;","sourceCodeStart":301,"sourceCodeEnd":337,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/shared/company-monitoring-contract.ts#L301-L337","documentation":"Thrown by normalizeCompanyImportBatch() when the sorted rows' ordinal values do not form an exact 0-based sequence. After sorting by ordinal, each row at index i must have ordinal === i, which rejects gaps, duplicates, and 1-based numbering alike.","triggerScenarios":"Sending ordinals 1..N from user-facing or spreadsheet data that is 1-based; filtering out invalid rows client-side but not renumbering the survivors, leaving gaps; duplicate ordinals from a mapping bug; an ordinal >= maxImportRows (100) surviving the earlier per-row check.","commonSituations":"CSV importers that pass through the file's row number as ordinal; UI code that deletes a row from a draft batch without resequencing; two async workers appending rows with overlapping counters.","solutions":["Renumber the batch 0..n-1 immediately before calling normalizeCompanyImportBatch(), after any filtering or deletion","If ordinals arrive 1-based from user data, subtract 1 during row normalization","Pre-validate: rows.every((r, i) => r.ordinal === i) after sorting by ordinal, and fail fast in the UI with the offending index"],"exampleFix":"// before\nconst rows = csvLines.map((line, i) => ({ ...line, ordinal: i + 1 })); // 1-based from spreadsheet\nconst normalized = normalizeCompanyImportBatch(rows); // throws: ordinals must be contiguous from 0\n\n// after\nconst rows = csvLines.map((line, i) => ({ ...line, ordinal: i })); // 0-based, contiguous\nconst normalized = normalizeCompanyImportBatch(rows);","handlingStrategy":"validation","validationCode":"const rows = rawRows\n  .filter(isValidRow)\n  .sort((a, b) => a.ordinal - b.ordinal)\n  .map((r, i) => ({ ...r, ordinal: i })); // resequence 0..n-1 after filter+sort\nif (!rows.every((r, i) => r.ordinal === i)) throw new Error('ordinal resequencing failed');","typeGuard":"function hasContiguousOrdinals(rows: { ordinal: number }[]): boolean {\n  const sorted = [...rows].sort((a, b) => a.ordinal - b.ordinal);\n  return sorted.every((r, i) => r.ordinal === i);\n}","tryCatchPattern":"catch (e) { if (e instanceof Error && e.message === 'batch ordinals must be contiguous from 0') resequenceAndRetry(); else throw e; }","preventionTips":["Treat ordinals as array position, not as user data — always renumber after edits","Subtract 1 from 1-based spreadsheet row numbers during ingestion","Beware duplicate ordinals from parallel workers appending rows — build the batch in one pass"],"tags":["validation","import","data-normalization","company-monitoring"],"backgroundTag":"schema-validation-failed","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}