from __future__ import annotations from datetime import datetime from typing import Literal from uuid import UUID from pydantic import BaseModel, Field DetectionEvidenceRole = Literal["false_positive", "false_negative"] DetectionReviewDecision = Literal[ "confirmed_model_false_positive", "confirmed_model_false_negative", "reference_gap_or_change", "qa_alignment_mismatch", "imagery_obscured_or_uncertain", "uncertain", "unreviewed", ] class DetectionReviewUpsert(BaseModel): evidence_role: DetectionEvidenceRole evidence_feature_id: str = Field(min_length=1, max_length=255) decision: DetectionReviewDecision notes: str | None = Field(default=None, max_length=2000) reviewed_by: str = Field(default="operator", min_length=1, max_length=120) class DetectionReviewRead(BaseModel): id: UUID | None = None project_id: UUID quality_check_id: UUID analysis_run_id: UUID | None = None evidence_role: DetectionEvidenceRole evidence_feature_id: str detection_id: UUID | None = None reference_feature_id: UUID | None = None decision: DetectionReviewDecision = "unreviewed" notes: str | None = None reviewed_by: str | None = None confidence: float | None = None class_name: str | None = None source_tile_path: str | None = None created_at: datetime | None = None updated_at: datetime | None = None class DetectionReviewSummary(BaseModel): total: int reviewed: int remaining: int false_positive_total: int false_negative_total: int decision_counts: dict[str, int] # The score with the operator's verdicts applied, next to the raw one. A # finding adjudicated as a reference gap is not the model's error, and an # interval covers what the unreviewed remainder could still turn out to be. reviewed_metrics: dict | None = None class DetectionReviewList(BaseModel): items: list[DetectionReviewRead] total: int limit: int offset: int summary: DetectionReviewSummary