{"record":{"id":"8c5bd2d17376bc9a","repo":"koala73/worldmonitor","slug":"company-monitoring-admission-evidence-missing","errorCode":"COMPANY_MONITORING_ADMISSION_EVIDENCE_MISSING","errorMessage":"COMPANY_MONITORING_ADMISSION_EVIDENCE_MISSING","messagePattern":"COMPANY_MONITORING_ADMISSION_EVIDENCE_MISSING","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/companyMonitoring/admission.ts","lineNumber":86,"sourceCode":"    new Set(candidate.referenceEvidenceFingerprints).size !==\n      candidate.referenceEvidenceFingerprints.length ||\n    candidate.referenceCount < candidate.referenceEvidenceFingerprints.length\n  ) {\n    throw new ConvexError(\"COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID\");\n  }\n  const rows = await Promise.all(candidate.referenceEvidenceFingerprints.map((evidenceFingerprint) =>\n    ctx.db\n      .query(\"companyMonitoringEvidence\")\n      .withIndex(\"by_account_company_fingerprint\", (q) =>\n        q\n          .eq(\"ownerAccountId\", candidate.ownerAccountId)\n          .eq(\"companyId\", candidate.companyId)\n          .eq(\"evidenceFingerprint\", evidenceFingerprint),\n      )\n      .unique()\n  ));\n  if (rows.some((row) => !row)) {\n    throw new ConvexError(\"COMPANY_MONITORING_ADMISSION_EVIDENCE_MISSING\");\n  }\n  const evidence = rows.map((row) => row!);\n  for (const row of evidence) {\n    if (\n      row.occurrenceDedupeKey !== candidate.occurrenceDedupeKey ||\n      (!allowExpired && (\n        row.state !== \"active\" ||\n        (row.expiresAt !== undefined && row.expiresAt <= now)\n      )) ||\n      (row.sourceAuthority === \"verified_first_party\" &&\n        (\n          row.independence !== \"first_party\" ||\n          (row.provider === \"exa\" && row.matchedClaimIds.length === 0)\n        ))\n    ) {\n      throw new ConvexError(\"COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID\");\n    }\n    if (!row.queryVersion || !ADMISSION_ID.test(row.queryVersion)) {","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/convex/companyMonitoring/admission.ts#L68-L104","documentation":"Thrown by referencedEvidence() (convex/companyMonitoring/admission.ts:86) when at least one fingerprint in candidate.referenceEvidenceFingerprints has no row in companyMonitoringEvidence under the by_account_company_fingerprint index (ownerAccountId + companyId + evidenceFingerprint). The admission flow requires every referenced evidence document to still exist at classification time.","triggerScenarios":"A purge job (purgeCompanyEvidenceBatch) deleted evidence rows between candidate creation and classification submission; evidence was written with a different fingerprint canonicalization than the candidate references; evidence belonged to a different companyId or ownerAccountId due to an id mix-up; manual deletion of evidence docs.","commonSituations":"Company removal/pause racing an in-flight classification worker; a deployment changing the fingerprint algorithm so old candidates reference fingerprints that no new rows carry; retention/TTL sweeps deleting expired evidence while a slow model call is in flight.","solutions":["Treat it as a terminal signal for that run: abandon the candidate and let the next scan cycle build a new candidate against existing evidence","Check whether a purge ran concurrently (companyMonitoringEvidence deletions, purgeGeneration/purgePhase on the company) and re-run the scan after purge completes","If fingerprints never resolve even after re-scan, verify fingerprint() inputs are canonicalized identically at write and reference time","Make retention sweeps skip evidence still referenced by pending candidates (check candidate existence before delete)"],"exampleFix":"// worker: handle missing evidence without crashing the loop\n// before\nconst result = await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);\n\n// after\ntry {\n  const result = await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);\n} catch (err) {\n  if (err instanceof ConvexError && err.data === \"COMPANY_MONITORING_ADMISSION_EVIDENCE_MISSING\") {\n    logger.warn(\"evidence purged before admission; dropping run\", { runId: args.classificationRunId });\n    return; // next scan rebuilds the candidate\n  }\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"// If you control the purge path, skip evidence still referenced by a pending candidate:\nconst pending = await ctx.db\n  .query(\"companyMonitoringCandidates\")\n  .withIndex(\"by_account_company_occurrence\", (q) =>\n    q.eq(\"ownerAccountId\", row.ownerAccountId).eq(\"companyId\", row.companyId).eq(\"occurrenceDedupeKey\", row.occurrenceDedupeKey),\n  )\n  .unique();\nif (pending?.state === \"pending_classification\") {\n  return; // evidence still needed for admission; defer purge\n}","typeGuard":null,"tryCatchPattern":"try {\n  const result = await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);\n} catch (err) {\n  if (err instanceof ConvexError && err.data === \"COMPANY_MONITORING_ADMISSION_EVIDENCE_MISSING\") {\n    logger.warn(\"evidence gone before admission; discarding run\", { runId: args.classificationRunId });\n    return; // terminal for this run; next scan rebuilds\n  }\n  throw err;\n}","preventionTips":["Make retention/purge jobs check for pending candidates before deleting evidence","Keep model latency and retry budgets short relative to evidence retention windows","Use identical canonicalization when computing fingerprints at write and reference time","Alert when this code fires repeatedly — it indicates a purge/admission race worth fixing"],"tags":["convex","evidence","purge","race-condition","internal-mutation"],"backgroundTag":"referenced-record-not-found","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}