{"record":{"id":"94747c4ab355a858","repo":"koala73/worldmonitor","slug":"company-monitoring-evidence-subjects-invalid","errorCode":null,"errorMessage":"COMPANY_MONITORING_EVIDENCE_SUBJECTS_INVALID","messagePattern":"COMPANY_MONITORING_EVIDENCE_SUBJECTS_INVALID","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/companyMonitoring/evidence.ts","lineNumber":47,"sourceCode":"// companies. Reject a wider internal expansion before the first write so the\n// receipt remains atomic and the mutation cannot exceed its scheduler budget.\nconst MAX_EXPANDED_EVIDENCE_ROWS = 25 * 25;\ntype EvidenceDoc = Doc<\"companyMonitoringEvidence\">;\n\nfunction nextUpdatedAt(row: { updatedAt: number } | null | undefined, now: number) {\n  return Math.max(now, (row?.updatedAt ?? now - 1) + 1);\n}\n\nasync function canonicalSubjects(\n  ctx: MutationCtx,\n  ownerAccountId: string,\n  requestedCompanyIds: string[],\n) {\n  if (\n    requestedCompanyIds.length === 0 ||\n    requestedCompanyIds.length > COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount\n  ) {\n    throw new ConvexError(\"COMPANY_MONITORING_EVIDENCE_SUBJECTS_INVALID\");\n  }\n  const subjectIds = [...new Set(requestedCompanyIds)].sort();\n  if (subjectIds.length !== requestedCompanyIds.length) {\n    throw new ConvexError(\"COMPANY_MONITORING_EVIDENCE_SUBJECTS_INVALID\");\n  }\n  const canonical = await Promise.all(subjectIds.map(async (companyId): Promise<EvidenceSubject> => {\n    const [company, claims] = await Promise.all([\n      ctx.db\n        .query(\"companyMonitoringCompanies\")\n        .withIndex(\"by_account_companyId\", (q) =>\n          q.eq(\"ownerAccountId\", ownerAccountId).eq(\"companyId\", companyId),\n        )\n        .unique(),\n      ctx.db\n        .query(\"companyMonitoringClaims\")\n        .withIndex(\"by_account_company\", (q) =>\n          q.eq(\"ownerAccountId\", ownerAccountId).eq(\"companyId\", companyId),\n        )","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/companyMonitoring/evidence.ts#L29-L65","documentation":"Thrown by canonicalSubjects in evidence.ts when the requestedCompanyIds array is empty or exceeds COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount. This is the first of three guards in canonicalSubjects that share the same error code; this one bounds the size of the evidence-ingestion subject set before any deduplication or lookup.","triggerScenarios":"Calling ingestCompanyEvidenceForCompanyIds (or the ingestEvidenceForTest mutation) with an empty companyIds array or one longer than COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount.","commonSituations":"Worker code forwarding an unbatched provider result that touched more companies than the account cap allows; a bug producing an empty subject list; exceeding the per-account company cap because the limit was lowered after companies were added.","solutions":["Ensure the companyIds list is non-empty and at most COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount before calling ingest.","If the provider returned more subjects than the cap, split the ingestion into multiple calls each within the limit, or drop out-of-cap subjects deliberately.","Re-read the current maxCompaniesPerAccount value from the shared contract; do not hard-code it."],"exampleFix":"// before\nawait ingest({ ownerAccountId, companyIds: allIds, evidence });\n\n// after\nconst CAP = COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount;\nconst safeIds = allIds.slice(0, CAP);\nif (safeIds.length === 0) throw new Error(\"no subjects\");\nawait ingest({ ownerAccountId, companyIds: safeIds, evidence });","handlingStrategy":"validation","validationCode":"const CAP = COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount;\nif (companyIds.length === 0 || companyIds.length > CAP) {\n  throw new Error(\"subjects out of range\");\n}","typeGuard":"function subjectsInRange(ids: string[], cap: number): boolean {\n  return ids.length > 0 && ids.length <= cap;\n}","tryCatchPattern":null,"preventionTips":["Bound the subject list to maxCompaniesPerAccount before ingesting.","Re-read the cap from the shared contract; do not hard-code it."],"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"}