{"record":{"id":"21ba11c6e25a15f7","repo":"koala73/worldmonitor","slug":"company-monitoring-evidence-batch-invalid","errorCode":null,"errorMessage":"COMPANY_MONITORING_EVIDENCE_BATCH_INVALID","messagePattern":"COMPANY_MONITORING_EVIDENCE_BATCH_INVALID","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/companyMonitoring/evidence.ts","lineNumber":364,"sourceCode":"    referenceEvidenceFingerprints: [],\n    referenceCount: 0,\n    referencesTruncated: false,\n    evidenceRevision: existing.evidenceRevision + 1,\n    evidenceSnapshotDigest: emptyEvidenceSnapshotDigest,\n    updatedAt: now,\n  });\n}\n\nexport async function ingestCompanyEvidenceForCompanyIds(\n  ctx: MutationCtx,\n  input: {\n    ownerAccountId: string;\n    companyIds: string[];\n    evidence: ProviderEvidence[];\n  },\n) {\n  if (input.evidence.length === 0 || input.evidence.length > MAX_INGESTION_ROWS) {\n    throw new ConvexError(\"COMPANY_MONITORING_EVIDENCE_BATCH_INVALID\");\n  }\n  const account = await ctx.db\n    .query(\"companyMonitoringAccounts\")\n    .withIndex(\"by_logicalAccountId\", (q) => q.eq(\"logicalAccountId\", input.ownerAccountId))\n    .unique();\n  if (!account || account.lifecycle !== \"entitled\" || account.terminalReason) {\n    throw new ConvexError(\"COMPANY_MONITORING_ACCOUNT_INACTIVE\");\n  }\n  const subjects = await canonicalSubjects(ctx, input.ownerAccountId, input.companyIds);\n  const normalized = await normalizeCompanyEvidence({\n    ownerAccountId: input.ownerAccountId,\n    subjects,\n    evidence: input.evidence,\n    now: Date.now(),\n  });\n  if (normalized.evidence.length > MAX_EXPANDED_EVIDENCE_ROWS) {\n    throw new ConvexError(\"COMPANY_MONITORING_EVIDENCE_EXPANSION_INVALID\");\n  }","sourceCodeStart":346,"sourceCodeEnd":382,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/companyMonitoring/evidence.ts#L346-L382","documentation":"Thrown by ingestCompanyEvidenceForCompanyIds when the input.evidence array is empty or larger than MAX_INGESTION_ROWS (100). This bounds the per-call ingestion size so each mutation stays within its scheduler budget; an empty batch is rejected because there is nothing to ingest.","triggerScenarios":"Calling ingestion with evidence.length === 0 or evidence.length > 100 (MAX_INGESTION_ROWS).","commonSituations":"Worker batching a full Exa/X receipt that exceeds 100 rows without chunking; a no-op call with an empty evidence list; a normalization bug collapsing all rows away.","solutions":["Chunk provider evidence into batches of at most MAX_INGESTION_ROWS before calling ingest.","Skip the call entirely when the evidence list is empty.","Re-read MAX_INGESTION_ROWS from the module rather than hard-coding 100."],"exampleFix":"// before\nawait ingest({ ownerAccountId, companyIds, evidence: allEvidence });\n\n// after\nconst BATCH = 100;\nfor (let i = 0; i < allEvidence.length; i += BATCH) {\n  await ingest({ ownerAccountId, companyIds, evidence: allEvidence.slice(i, i + BATCH) });\n}","handlingStrategy":"validation","validationCode":"if (evidence.length === 0) return { status: \"noop\" };\nif (evidence.length > 100) throw new Error(\"evidence batch exceeds 100 rows\");\nawait ingest(...);","typeGuard":"function batchWithinLimit(rows: unknown[], limit = 100): boolean {\n  return rows.length > 0 && rows.length <= limit;\n}","tryCatchPattern":null,"preventionTips":["Chunk provider receipts into batches of MAX_INGESTION_ROWS.","Skip ingestion when there is nothing to ingest."],"tags":["convex","validation","limits","evidence","company-monitoring"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}