{"record":{"id":"b85649a76a08dedd","repo":"koala73/worldmonitor","slug":"company-monitoring-evidence-expansion-invalid","errorCode":null,"errorMessage":"COMPANY_MONITORING_EVIDENCE_EXPANSION_INVALID","messagePattern":"COMPANY_MONITORING_EVIDENCE_EXPANSION_INVALID","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/companyMonitoring/evidence.ts","lineNumber":381,"sourceCode":"  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  }\n  const now = Date.now();\n  const affected = new Map<string, Map<string, \"evidence_unavailable\" | undefined>>();\n  const rememberOccurrence = (\n    companyId: string,\n    occurrenceDedupeKey: string,\n    fallbackLossReason?: \"evidence_unavailable\",\n  ) => {\n    const occurrences = affected.get(companyId) ?? new Map();\n    if (!occurrences.has(occurrenceDedupeKey) || fallbackLossReason === undefined) {\n      occurrences.set(occurrenceDedupeKey, fallbackLossReason);\n    }\n    affected.set(companyId, occurrences);\n  };\n  for (const evidence of normalized.evidence) {\n    const existing = await ctx.db\n      .query(\"companyMonitoringEvidence\")\n      .withIndex(\"by_account_company_locator\", (q) =>","sourceCodeStart":363,"sourceCodeEnd":399,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/companyMonitoring/evidence.ts#L363-L399","documentation":"Thrown by ingestCompanyEvidenceForCompanyIds when the normalized (expanded) evidence list exceeds MAX_EXPANDED_EVIDENCE_ROWS (625, i.e. 25 companies * 25 rows). normalizeCompanyEvidence can multiply the input because a single provider row may be routed to multiple subject companies; this guard rejects an over-broad expansion before any write so the receipt stays atomic within the scheduler budget.","triggerScenarios":"Provider evidence that, after normalization and per-company routing, expands to more than 625 rows in a single ingestion call.","commonSituations":"A provider returned rows touching the full cohort (25 companies) with 25+ routed matches each; a query that over-matches claims; the cohort size or claim set grew since the cap was set.","solutions":["Tighten the provider query/claim matching so fewer rows route to each company.","Split the ingestion so each call covers fewer subjects or fewer evidence rows.","If the expansion is legitimately large, raise MAX_EXPANDED_EVIDENCE_ROWS only after confirming the scheduler budget holds."],"exampleFix":"// before\nawait ingest({ ownerAccountId, companyIds: allSubjects, evidence });\n\n// after\n// split subjects so expansion stays under the cap\nfor (const subjectBatch of chunk(allSubjects, 5)) {\n  await ingest({ ownerAccountId, companyIds: subjectBatch, evidence });\n}","handlingStrategy":"validation","validationCode":"// estimate worst-case expansion = subjects * rows; keep under 625\nconst MAX_EXPANSION = 625;\nconst estimated = companyIds.length * evidence.length;\nif (estimated > MAX_EXPANSION) {\n  // split ingestion\n}","typeGuard":"function expansionSafe(subjects: number, rows: number, cap = 625): boolean {\n  return subjects * rows <= cap;\n}","tryCatchPattern":null,"preventionTips":["Split ingestion so subject-count * row-count stays under MAX_EXPANDED_EVIDENCE_ROWS.","Tighten provider query/claim matching to reduce routing expansion."],"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"}