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
- 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
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
- 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
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
- COMPANY_MONITORING_ADMISSION_EVIDENCE_MISSING
- COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE
- COMPANY_MONITORING_ADMISSION_QUERY_VERSION_INVALID
- COMPANY_MONITORING_EVIDENCE_REVISION_INVALID
- COMPANY_MONITORING_ _INVALID
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)