fix(vision): il gate del fuori tolleranza vale per l'acquisizione, non per la quota

Un'acquisizione camera produce N quote nello stesso istante: non c'e "andare
avanti" fra l'una e l'altra, quindi il gate sequenziale di save_measurement
non deve scattare dentro il lotto. execute_task valuta pending_authorisation
una sola volta per l'intera acquisizione (bloccando solo se il fail pendente
non appartiene alle quote di questo task) e passa enforce_tolerance_gate=False
a ogni save_measurement del lotto. Query delle quote ordinata per
marker_number, cosi l'esito non dipende dall'ordine di ritorno di SQLite/MySQL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
This commit is contained in:
2026-08-16 18:58:46 +02:00
parent ddf7788d77
commit b86eeada86
3 changed files with 186 additions and 11 deletions
+28
View File
@@ -52,10 +52,15 @@ async def execute_task(
detail="this task carries no vision graph",
)
# Ordered by marker_number - the number the operator sees on the annotated
# image and in reports (RecipeSubtask.__repr__, statistics.py, report_service.py
# all key off it). Which quote is saved first must never be an accident of
# how SQLite/MySQL happens to return rows.
quotes = (await db.execute(
select(RecipeSubtask)
.where(RecipeSubtask.task_id == task.id)
.where(RecipeSubtask.vision_output.is_not(None))
.order_by(RecipeSubtask.marker_number)
)).scalars().all()
if not quotes:
raise HTTPException(
@@ -79,6 +84,28 @@ async def execute_task(
),
)
# Ruling R10: the gate is evaluated once per acquisition, not once per
# quote. `save_measurement`'s own gate models "moving on" between
# measurements taken one after another - a real later moment where the
# operator could leave a bad reading behind. A camera acquisition produces
# every quote of this task at the same instant: there is no "moving on"
# between them, so applying the sequential rule inside the batch would
# block on a fail this very request just saved. Check it once here,
# against the whole set this acquisition is about to produce, and pass
# enforce_tolerance_gate=False to every save_measurement call below.
blocking = await measurement_service.pending_authorisation(
db, version_id, measured_by, production_run_id,
)
quote_ids = {q.id for q in quotes}
if blocking is not None and blocking.subtask_id not in quote_ids:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=(
"A measurement is out of tolerance and awaiting the supervisor: "
"authorise it or measure that quote again before going on"
),
)
result = VisionResult(
task_id=task.id,
engine_version=report["engine_version"],
@@ -102,6 +129,7 @@ async def execute_task(
input_method="camera",
production_run_id=production_run_id,
vision_result_id=result.id,
enforce_tolerance_gate=False,
))
return result, saved