"""Rejected candidate storage for audit, learning, and debugging. Stores candidates rejected during evidence verification with structured reason codes. Currently in-memory; designed for future persistence to v3_rejected_candidates table. """ from __future__ import annotations from collections import defaultdict from services.intelligence_pipeline_v3.verification.models import ( RejectedCandidate, RejectionReason, ) class RejectedCandidateStore: """In-memory store for rejected candidates, queryable by pipeline run and reason. Designed to be replaced with a database-backed implementation once the v3_rejected_candidates table is deployed. The interface is stable. """ def __init__(self) -> None: self._by_run: dict[str, list[RejectedCandidate]] = defaultdict(list) self._by_reason: dict[RejectionReason, list[RejectedCandidate]] = defaultdict(list) self._all: list[RejectedCandidate] = [] def store(self, rejected: RejectedCandidate, run_id: str = "default") -> None: """Store a rejected candidate. Args: rejected: The rejected candidate to store. run_id: Pipeline run identifier for grouping. """ self._all.append(rejected) self._by_run[run_id].append(rejected) self._by_reason[rejected.rejection_reason].append(rejected) def store_batch(self, rejected_list: list[RejectedCandidate], run_id: str = "default") -> None: """Store multiple rejected candidates in one call. Args: rejected_list: List of rejected candidates to store. run_id: Pipeline run identifier for grouping. """ for r in rejected_list: self.store(r, run_id) def get_by_pipeline_run(self, run_id: str) -> list[RejectedCandidate]: """Retrieve all rejected candidates for a given pipeline run. Args: run_id: Pipeline run identifier. Returns: List of rejected candidates for that run (empty if none). """ return list(self._by_run.get(run_id, [])) def get_by_reason(self, reason: RejectionReason) -> list[RejectedCandidate]: """Retrieve all rejected candidates with a specific rejection reason. Args: reason: The rejection reason code to filter by. Returns: List of rejected candidates with that reason (empty if none). """ return list(self._by_reason.get(reason, [])) def get_all(self) -> list[RejectedCandidate]: """Retrieve all stored rejected candidates. Returns: List of all rejected candidates. """ return list(self._all) def count(self) -> int: """Total number of rejected candidates stored.""" return len(self._all) def count_by_reason(self) -> dict[str, int]: """Count of rejected candidates grouped by reason code. Returns: Mapping from reason code string to count. """ return {reason.value: len(items) for reason, items in self._by_reason.items()} def clear(self) -> None: """Remove all stored rejected candidates.""" self._by_run.clear() self._by_reason.clear() self._all.clear()