koala73/worldmonitor · error · ConvexError

COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID

COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID

Error message

COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID

What it means

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.

Solutions

  1. Inspect the candidate document in the Convex dashboard (companyMonitoringCandidates, by_account_company_occurrence) and compare referenceCount with the fingerprint array
  2. Let the candidate expire (it terminalizes as hold_expired) and re-run the company scan so a fresh, consistent candidate is built
  3. Fix the selection-stage writer so it validates these three invariants before insert/patch (non-empty, deduped, referenceCount >= fingerprints.length)
  4. If it recurs across many candidates, check for concurrent selection runs and serialize them per occurrenceDedupeKey

Example fix

// selection writer: enforce invariants before saving the candidate (prevents the error)
// before
await ctx.db.patch(candidateId, {
  referenceEvidenceFingerprints: fingerprints,
  referenceCount: count,
});

// after
const unique = [...new Set(fingerprints)];
if (unique.length === 0 || count < unique.length) {
  throw new Error("evidence selection produced an invalid reference set");
}
await ctx.db.patch(candidateId, {
  referenceEvidenceFingerprints: unique,
  referenceCount: Math.max(count, unique.length),
});
Defensive patterns

Strategy: try-catch

Validate before calling

// Only feasible for code that writes candidates — enforce the invariants at write time:
function assertCandidateReferenceSet(fingerprints: string[], referenceCount: number): void {
  const unique = new Set(fingerprints);
  if (fingerprints.length === 0 || unique.size !== fingerprints.length || referenceCount < fingerprints.length) {
    throw new Error("candidate reference set invalid: empty, duplicated, or count below fingerprint count");
  }
}

Type guard

function hasValidReferenceSet(candidate: { referenceEvidenceFingerprints: string[]; referenceCount: number }): boolean {
  return (
    candidate.referenceEvidenceFingerprints.length > 0 &&
    new Set(candidate.referenceEvidenceFingerprints).size === candidate.referenceEvidenceFingerprints.length &&
    candidate.referenceCount >= candidate.referenceEvidenceFingerprints.length
  );
}

Try / catch

try {
  await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);
} catch (err) {
  if (err instanceof ConvexError && err.data === "COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID") {
    logger.error("candidate reference set corrupt", { occurrenceDedupeKey: args.occurrenceDedupeKey });
    await abandonRun(args); // let the candidate expire; next scan rebuilds it
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/88e561edb5f203d0. Report an issue: GitHub.

Appendix: source

Thrown at convex/companyMonitoring/admission.ts:72

function admissionTerminalReason(decision: string) {
  if (decision === "publish") return "admitted" as const;
  if (decision === "reject") return "rejected" as const;
  return "hold_expired" as const;
}

async function referencedEvidence(
  ctx: MutationCtx,
  candidate: Doc<"companyMonitoringCandidates">,
  now: number,
  allowExpired = false,
) {
  if (
    candidate.referenceEvidenceFingerprints.length === 0 ||
    new Set(candidate.referenceEvidenceFingerprints).size !==
      candidate.referenceEvidenceFingerprints.length ||
    candidate.referenceCount < candidate.referenceEvidenceFingerprints.length
  ) {
    throw new ConvexError("COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID");
  }
  const rows = await Promise.all(candidate.referenceEvidenceFingerprints.map((evidenceFingerprint) =>
    ctx.db
      .query("companyMonitoringEvidence")
      .withIndex("by_account_company_fingerprint", (q) =>
        q
          .eq("ownerAccountId", candidate.ownerAccountId)
          .eq("companyId", candidate.companyId)
          .eq("evidenceFingerprint", evidenceFingerprint),
      )
      .unique()
  ));
  if (rows.some((row) => !row)) {
    throw new ConvexError("COMPANY_MONITORING_ADMISSION_EVIDENCE_MISSING");
  }
  const evidence = rows.map((row) => row!);
  for (const row of evidence) {
    if (

View on GitHub (pinned to eeab0a219f)