An FP lane sitting at annotated can gain missed-smoke — and therefore start needing localization — without changing stage, so neither the localization guard nor the FP→smoke demotion fires. The lane ends up at annotated, needing localization, with zero boxes: on /localize/done and in /export/alerts.
This is the state #346 set out to prevent. PR #352 closed every path into annotated; this one never crosses that boundary.
Reproduced
SEEDED: [('annotated', {'annotation': []})] # FP lane's empty placeholder
PATCH {"has_missed_smoke": true}
PATCH status: 200
RESULT stage: annotated
RESULT has_missed_smoke: True
IN /localize/done: True
IN /export/alerts: True
Why both guards miss it
- The localization guard (
sequence_annotations.py, assert_localization_complete) requires the stage to be changing into annotated. Here target_processing_stage == existing.processing_stage == ANNOTATED, so it is skipped. Widening it would not help either: the FP exit already wrote empty annotated-stage detection annotations via auto_create_detection_annotations, so assert_localization_complete would pass on those rows.
- The FP→smoke promotion (
is_fp_promotion) requires target_processing_stage == SEQ_ANNOTATION_DONE. A flags-only PATCH leaves the stage alone, so the demotion — which would delete those empty rows and re-arm auto-annotate — never runs.
Reachable from the UI, not just the API
frontend/src/pages/LocalizeAlertPage.tsx sends exactly this PATCH from the missed-smoke row:
apiClient.updateSequenceAnnotation(annotationId, { has_missed_smoke: value })
and missedSmokeAnnotationId falls back to "the first lane that has an annotation at all", which can be the FP lane. The row renders in done mode.
Suggested fix
Key is_fp_promotion on the flag transition rather than the target stage: a lane at annotated that did not previously need localization but does after this edit should demote to seq_annotation_done, whatever stage the payload names. That is the intent issue #275 already established — the current predicate just happens to detect it only when the client also sends the new stage.
Worth confirming as part of that work: a flags-only PATCH would then change processing_stage without the client asking, which callers should expect.
Found by review of #352; not a regression (the same gap existed before it, via the existing == SEQ_ANNOTATION_DONE form of the guard).
An FP lane sitting at
annotatedcan gain missed-smoke — and therefore start needing localization — without changing stage, so neither the localization guard nor the FP→smoke demotion fires. The lane ends up atannotated, needing localization, with zero boxes: on/localize/doneand in/export/alerts.This is the state #346 set out to prevent. PR #352 closed every path into
annotated; this one never crosses that boundary.Reproduced
Why both guards miss it
sequence_annotations.py,assert_localization_complete) requires the stage to be changing intoannotated. Heretarget_processing_stage == existing.processing_stage == ANNOTATED, so it is skipped. Widening it would not help either: the FP exit already wrote empty annotated-stage detection annotations viaauto_create_detection_annotations, soassert_localization_completewould pass on those rows.is_fp_promotion) requirestarget_processing_stage == SEQ_ANNOTATION_DONE. A flags-only PATCH leaves the stage alone, so the demotion — which would delete those empty rows and re-arm auto-annotate — never runs.Reachable from the UI, not just the API
frontend/src/pages/LocalizeAlertPage.tsxsends exactly this PATCH from the missed-smoke row:and
missedSmokeAnnotationIdfalls back to "the first lane that has an annotation at all", which can be the FP lane. The row renders in done mode.Suggested fix
Key
is_fp_promotionon the flag transition rather than the target stage: a lane atannotatedthat did not previously need localization but does after this edit should demote toseq_annotation_done, whatever stage the payload names. That is the intent issue #275 already established — the current predicate just happens to detect it only when the client also sends the new stage.Worth confirming as part of that work: a flags-only PATCH would then change
processing_stagewithout the client asking, which callers should expect.Found by review of #352; not a regression (the same gap existed before it, via the
existing == SEQ_ANNOTATION_DONEform of the guard).