evidence_geojson emitted one feature per false positive, one per false negative and two per match, with no limit. A regional check of 40k detections against 45k reference footprints produced well over a hundred thousand features in a single response, plus one warning string per unresolvable identifier. The endpoint the entire review workflow depends on therefore failed exactly where review matters most. What to draw is now decided before any geometry is fetched, so the query work is proportional to the result rather than to the size of the check — previously 130k geometries were resolved through an IN clause holding every identifier in the check, to then discard most of them. The budget is split between misses and false positives in proportion to their populations with at least one of each, rather than by strict priority, which would mean a check with 50.000 misses and three false positives never showed one. Confirmations fill what remains, and a match is kept or dropped as a pair because half a match is not reviewable evidence. limit_evidence and evidence_role_counts are removed: plan_evidence supersedes them, and helpers kept alive only by their own tests read like a contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
3.3 KiB
Python
107 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.db.session import get_db
|
|
from app.schemas import Envelope, QualityEvidenceResponse
|
|
from app.schemas.detection_review import DetectionReviewList, DetectionReviewRead, DetectionReviewUpsert
|
|
from app.schemas.qa import QualityCheckList
|
|
from app.services.detection_review_service import DetectionReviewService
|
|
from app.services.quality_evidence_service import QualityEvidenceService
|
|
from app.services.quality_check_service import QualityCheckService
|
|
from app.utils.response import envelope
|
|
|
|
router = APIRouter(prefix="/projects/{project_id}", tags=["quality-checks"])
|
|
|
|
|
|
@router.get("/quality-checks", response_model=Envelope[QualityCheckList])
|
|
def list_quality_checks(
|
|
project_id: UUID,
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
items, total = QualityCheckService.list_quality_checks(
|
|
db,
|
|
project_id=project_id,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
return envelope(QualityCheckList(items=items, total=total, limit=limit, offset=offset).model_dump())
|
|
|
|
|
|
@router.get(
|
|
"/quality-checks/{quality_check_id}/evidence/geojson",
|
|
response_model=Envelope[QualityEvidenceResponse],
|
|
)
|
|
def get_quality_check_evidence_geojson(
|
|
project_id: UUID,
|
|
quality_check_id: UUID,
|
|
limit: int = Query(
|
|
default=QualityEvidenceService.DEFAULT_EVIDENCE_LIMIT,
|
|
ge=0,
|
|
le=100_000,
|
|
description="Maximum evidence features to draw; 0 returns everything. Misses and false positives first.",
|
|
),
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
return envelope(
|
|
QualityEvidenceService.evidence_geojson(
|
|
db,
|
|
project_id=project_id,
|
|
quality_check_id=quality_check_id,
|
|
limit=limit,
|
|
)
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/quality-checks/{quality_check_id}/reviews",
|
|
response_model=Envelope[DetectionReviewList],
|
|
)
|
|
def list_detection_reviews(
|
|
project_id: UUID,
|
|
quality_check_id: UUID,
|
|
evidence_role: str | None = Query(default=None, pattern="^(false_positive|false_negative)$"),
|
|
decision: str | None = Query(default=None, max_length=64),
|
|
reviewed: bool | None = Query(default=None),
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
return envelope(
|
|
DetectionReviewService.list_reviews(
|
|
db,
|
|
project_id=project_id,
|
|
quality_check_id=quality_check_id,
|
|
evidence_role=evidence_role,
|
|
decision=decision,
|
|
reviewed=reviewed,
|
|
limit=limit,
|
|
offset=offset,
|
|
).model_dump()
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/quality-checks/{quality_check_id}/reviews",
|
|
response_model=Envelope[DetectionReviewRead],
|
|
)
|
|
def upsert_detection_review(
|
|
project_id: UUID,
|
|
quality_check_id: UUID,
|
|
payload: DetectionReviewUpsert,
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
return envelope(
|
|
DetectionReviewService.upsert_review(
|
|
db,
|
|
project_id=project_id,
|
|
quality_check_id=quality_check_id,
|
|
payload=payload,
|
|
).model_dump()
|
|
)
|