{"record":{"id":"88e561edb5f203d0","repo":"koala73/worldmonitor","slug":"company-monitoring-admission-evidence-invalid","errorCode":"COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID","errorMessage":"COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID","messagePattern":"COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/companyMonitoring/admission.ts","lineNumber":72,"sourceCode":"function admissionTerminalReason(decision: string) {\n  if (decision === \"publish\") return \"admitted\" as const;\n  if (decision === \"reject\") return \"rejected\" as const;\n  return \"hold_expired\" as const;\n}\n\nasync function referencedEvidence(\n  ctx: MutationCtx,\n  candidate: Doc<\"companyMonitoringCandidates\">,\n  now: number,\n  allowExpired = false,\n) {\n  if (\n    candidate.referenceEvidenceFingerprints.length === 0 ||\n    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 (","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/convex/companyMonitoring/admission.ts#L54-L90","documentation":"Thrown by referencedEvidence() (convex/companyMonitoring/admission.ts:72) when the candidate document itself is structurally inconsistent: referenceEvidenceFingerprints is empty, contains duplicate fingerprints, or candidate.referenceCount is smaller than the fingerprint list length. This is a stored-data invariant violation written by the evidence-selection stage, not an argument error from the admission caller.","triggerScenarios":"A candidate row was created with referenceEvidenceFingerprints: [] (selection found zero evidence but still enqueued classification); the selection writer appended the same fingerprint twice (dedupe bug); referenceCount was decremented on a partial update while fingerprints stayed; concurrent selection writes produced a torn candidate document.","commonSituations":"A bug or rollback in the evidence-selection job; a migration that rewrote candidate docs inconsistently; two selection runs racing on the same owner+company+occurrence key; manual dashboard edits to candidate fields.","solutions":["Inspect the candidate document in the Convex dashboard (companyMonitoringCandidates, by_account_company_occurrence) and compare referenceCount with the fingerprint array","Let the candidate expire (it terminalizes as hold_expired) and re-run the company scan so a fresh, consistent candidate is built","Fix the selection-stage writer so it validates these three invariants before insert/patch (non-empty, deduped, referenceCount >= fingerprints.length)","If it recurs across many candidates, check for concurrent selection runs and serialize them per occurrenceDedupeKey"],"exampleFix":"// selection writer: enforce invariants before saving the candidate (prevents the error)\n// before\nawait ctx.db.patch(candidateId, {\n  referenceEvidenceFingerprints: fingerprints,\n  referenceCount: count,\n});\n\n// after\nconst unique = [...new Set(fingerprints)];\nif (unique.length === 0 || count < unique.length) {\n  throw new Error(\"evidence selection produced an invalid reference set\");\n}\nawait ctx.db.patch(candidateId, {\n  referenceEvidenceFingerprints: unique,\n  referenceCount: Math.max(count, unique.length),\n});","handlingStrategy":"try-catch","validationCode":"// Only feasible for code that writes candidates — enforce the invariants at write time:\nfunction assertCandidateReferenceSet(fingerprints: string[], referenceCount: number): void {\n  const unique = new Set(fingerprints);\n  if (fingerprints.length === 0 || unique.size !== fingerprints.length || referenceCount < fingerprints.length) {\n    throw new Error(\"candidate reference set invalid: empty, duplicated, or count below fingerprint count\");\n  }\n}","typeGuard":"function hasValidReferenceSet(candidate: { referenceEvidenceFingerprints: string[]; referenceCount: number }): boolean {\n  return (\n    candidate.referenceEvidenceFingerprints.length > 0 &&\n    new Set(candidate.referenceEvidenceFingerprints).size === candidate.referenceEvidenceFingerprints.length &&\n    candidate.referenceCount >= candidate.referenceEvidenceFingerprints.length\n  );\n}","tryCatchPattern":"try {\n  await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);\n} catch (err) {\n  if (err instanceof ConvexError && err.data === \"COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID\") {\n    logger.error(\"candidate reference set corrupt\", { occurrenceDedupeKey: args.occurrenceDedupeKey });\n    await abandonRun(args); // let the candidate expire; next scan rebuilds it\n    return;\n  }\n  throw err;\n}","preventionTips":["Validate the three reference-set invariants in the selection writer before persisting a candidate","Serialize selection runs per occurrenceDedupeKey to avoid torn candidate writes","Add a periodic audit query for candidates failing hasValidReferenceSet","Never hand-edit candidate documents in the dashboard"],"tags":["convex","data-integrity","evidence","invariant","internal-mutation"],"backgroundTag":"referential-integrity-violation","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"}