US07-06: Validate Performance and Resource Bounds (#93)

This commit was merged in pull request #93.
This commit is contained in:
2026-08-17 21:52:02 +02:00
parent 9851e112a9
commit 05f34cdda2
14 changed files with 1341 additions and 121 deletions

View File

@@ -18,7 +18,6 @@ from __future__ import annotations
import json
import os
import uuid
from datetime import datetime, timezone
from typing import Protocol
@@ -26,9 +25,9 @@ from sqlalchemy import func, select
from sqlalchemy.orm import sessionmaker
from photo_pipeline import path_policy
from photo_pipeline.models import AnalysisResult, Asset, SafetyReview
from photo_pipeline.models import AnalysisResult, Asset
from photo_pipeline.services import exif_checkpoint
from photo_pipeline.services.safety import SFW
from photo_pipeline.services.safety import SFW, latest_reviews
MODEL = "gemini-2.5-flash"
PROMPT_VERSION = "1"
@@ -72,11 +71,26 @@ class AnalysisService:
def _sfw_asset_ids(self, session) -> set[str]:
"""Asset ids whose latest safety decision is ``sfw`` — the ONLY assets that
may reach the provider."""
latest: dict[str, str | None] = {}
for review in session.scalars(select(SafetyReview).order_by(SafetyReview.created_at)):
latest[review.asset_id] = review.decision
return {aid for aid, decision in latest.items() if decision == SFW}
may reach the provider.
The "latest row wins" rule is applied in SQL (US07-06); loading every review
to fold it in Python made the gate cost grow with the review history rather
than with the work being gated.
"""
latest = latest_reviews().subquery()
return set(
session.scalars(select(latest.c.asset_id).where(latest.c.decision == SFW))
)
def _sfw_count(self, session) -> int:
"""How many assets the gate currently allows, without listing them."""
latest = latest_reviews().subquery()
return int(
session.scalar(
select(func.count()).select_from(latest).where(latest.c.decision == SFW)
)
or 0
)
def _is_still_sfw(self, asset_id: str) -> bool:
"""Re-read the current safety decision straight from the database."""
@@ -104,9 +118,12 @@ class AnalysisService:
)
return [a.id for a in assets if a.id not in done]
def counts(self) -> dict[str, int]:
def counts(self, *, eligible: int | None = None) -> dict[str, int]:
"""Analysis progress. ``eligible`` may be passed by a caller that has just
counted confirmed-SFW assets, so the workflow home does not resolve the
latest decision of every asset twice on one page load (US07-06)."""
with self._session_factory() as session:
sfw = self._sfw_asset_ids(session)
eligible = self._sfw_count(session) if eligible is None else eligible
rows = dict(
session.execute(
select(AnalysisResult.status, func.count()).group_by(AnalysisResult.status)
@@ -115,10 +132,10 @@ class AnalysisService:
analyzed = int(rows.get("analyzed", 0))
errored = int(rows.get("error", 0))
return {
"eligible": len(sfw),
"eligible": eligible,
"analyzed": analyzed,
"error": errored,
"pending": max(len(sfw) - analyzed - errored, 0),
"pending": max(eligible - analyzed - errored, 0),
}
def run(self, asset_ids: list[str] | None = None) -> dict: