Skip to content

fix(zones): projeto pre-gravado adotava arena e ROIs de estudo anterior - #556

Merged
MarkSant merged 1 commit into
mainfrom
claude/arena-rois-source-investigation-cd8be2
Sep 7, 2026
Merged

MarkSant merged 1 commit into
mainfrom
claude/arena-rois-source-investigation-cd8be2

Conversation

@MarkSant

@MarkSant MarkSant commented Sep 7, 2026

Copy link
Copy Markdown
Owner

O problema

Um projeto pré-gravado criado sobre uma pasta que já continha 1_ProcessingArea_* / 2_AreasOfInterest_* de um estudo anterior carregava aqueles polígonos no primeiro duplo-clique — apesar de a importação ter sido recusada no wizard.

Nada levantava exceção e nada degradava visivelmente. O relatório saía completo, com todas as distâncias, ocupações de ROI e latências medidas contra a arena do experimento anterior.

Perícia do caso real que originou o PR:

Hora Evento
16:04 Wizard cria o projeto. config_snapshot.yaml → detection_zones.polygon: [], e import_config com todos os flags false
16:10 Usuário abre o 1º vídeo → arena de 16 pontos + 4 ROIs (A1–A4) aparecem sozinhas
16:11–16:13 Trajetórias e relatórios gerados com essa geometria

Os polígonos eram byte a byte idênticos ao 1_ProcessingArea_CECT_4.parquet de 13/12/2025 que estava ao lado do .mp4.

Causa raiz — duas, independentes

1. DialogManager.offer_zone_reuse importava sob uma única condição: "este vídeo não tem zonas em project_data?". Nunca consultava a decisão do wizard. O método foi escrito para gravações live (o recorder grava os parquets na pasta da sessão enquanto as zonas em memória ficam sob a chave do reference-frame) e a regra passou a valer para todo mundo.

Agravante: scan_input_paths registra parquet_files na entrada do vídeo mesmo com a recusa, e _resolve_source_zone_parquets varre Path(video).parent de qualquer forma — limpar um não fecha o outro.

2. ZoneManager.save_zone_data escrevia project_data["detection_zones"] incondicionalmente, mesmo com video_path explícito. Esse é o default global que get_zone_data(fallback_to_global=True) serve a qualquer vídeo sem zonas próprias — então a arena do vídeo A virava a arena do vídeo B.

A correção

Nova regra única: core/services/zone_autoimport_policy.decide_zone_autoimport(), que decide por procedência, não por "achou arquivo":

Situação Decisão
Parquets dentro do projeto (sessão live, reprocessamento) AUTO — silencioso, como sempre foi
Fora do projeto + recusa registrada em _wizard_metadata["import_config"] SKIP
Fora sem registro (projeto anterior ao wizard atual) ASK
Sem projeto (vídeo único, live ad-hoc) AUTO — legado intacto

import_zone_data_from_video_parquets() passa a importar incondicionalmente: a permissão é de quem chama. Extraí resolve_zone_parquet_candidates() para a política julgar exatamente os arquivos que serão lidos, sem janela entre a pergunta e a resposta.

Fim do silêncio: o ramo ASK abre um diálogo que nomeia a pasta de origem — era justamente a informação ausente que fazia o import parecer configuração escondida — e avisa, depois de importar, para conferir os contornos.

Espelho global: vira ZoneManager.should_mirror_to_global(). Mantido quando não há vídeo alvo (o save é o global), quando project_type == "live" e quando não há project_type declarado. Só um projeto que positivamente se declara não-live é excluído.

Segurança contra regressão nos outros fluxos

A restrição é sempre pelo caso que foi provado errado, nunca pela negação do caso conhecido:

  • Live com projeto — o espelho global é o armazenamento legítimo (uma câmera, uma arena; os .mp4 gravados não têm chave própria em zones_by_video). ZoneContextService, ValidationManager (placeholders de sessões planejadas) e MultiAquariumCoordinator.save_manual_arena leem o global. Preservado por teste.
  • Vídeo único e live ad-hoc — rodam sem project_type. Caem no comportamento legado, sem alteração.
  • Pré-gravado — o único caso alterado, e o que apresentava o bug.

Bônus: crash latente pré-existente

save_project() levanta ProjectInvalidError sem project_path, e o fluxo de vídeo único chegava lá depois de um import bem-sucedido, derrubando o duplo-clique. Blindado, com teste.

Verificação

pytest -q                                     6695 passed, 5 skipped, 0 failed
pytest -m gui -n0                             1206 passed, 0 failed
mypy .                                        902 files, clean
ruff check / ruff format                      clean
tests/i18n/                                   79 passed
mutation_check --module zone_autoimport_policy  5/5 killed (100%)

Testes novos cobrem cada decisão da política, cada fluxo do espelho global, e a regressão direta: "abrir o vídeo B não devolve a arena do vídeo A".

Notas de revisão

i18n — divergência deliberada do processo. As 4 strings novas foram anexadas ao .pot/.po e o .mo recompilado, em vez de rodar scripts/update_translations.py. Motivo: o catálogo em main está completo (0 pendências) mas defasado do fonte — pelo menos 8 msgids com _() nunca foram extraídos ("Detector models not found", "Aquarium width (cm):", o aviso de escala px/cm…). O teste de integridade compara o .pot commitado com o .po commitado, então a defasagem é invisível no CI. Regenerar arrastaria ~2600 linhas de churn alheio para dentro deste fix e obrigaria a traduzir 8 strings sem relação com ele. O par ficou registrado em locales/_pairs/zone-autoimport-gate.json para a próxima regeneração completa. Fechar a defasagem merece um PR próprio.

Catálogo de mutação estendido (53 → 58, todas mortas), seguindo a regra do MUTATION_BASELINE.md: "estenda o catálogo quando adicionar um contrato que degrada em silêncio". Este degrada — um AUTO errado adota geometria alheia sem sinal algum.

⚠️ Este PR não desfaz dados já gravados. Projetos afetados continuam com a arena importada em detection_zones, em zones_by_video e nos parquets de resultado, e os relatórios já gerados foram medidos com ela.

🤖 Generated with Claude Code

Um projeto pre-gravado criado sobre uma pasta que ja continha
1_ProcessingArea_*/2_AreasOfInterest_* de um estudo anterior carregava
aqueles poligonos no primeiro duplo-clique, apesar de a importacao ter
sido RECUSADA no wizard. Nada levantava, nada degradava visivelmente: o
relatorio saia completo, medido contra a arena do experimento anterior.

Duas causas independentes, ambas silenciosas.

1. DialogManager.offer_zone_reuse chamava import_zone_data_from_video_parquets
   sob uma unica condicao -- "este video nao tem zonas em project_data?" -- e
   nunca consultava a decisao do wizard. O metodo foi escrito para gravacoes
   LIVE (o recorder grava os parquets na pasta da sessao enquanto as zonas em
   memoria ficam sob a chave do reference-frame) e a regra valia para todos.
   Agravante: scan_input_paths registra parquet_files na entrada do video
   mesmo com a recusa, e _resolve_source_zone_parquets varre
   Path(video).parent de qualquer forma -- limpar um nao fecha o outro.

2. ZoneManager.save_zone_data escrevia project_data["detection_zones"]
   incondicionalmente, mesmo com video_path explicito. O default global e o
   que get_zone_data(fallback_to_global=True) serve a qualquer video sem
   zonas proprias, entao a arena do video A virava a arena do video B.

A decisao agora e de core/services/zone_autoimport_policy, por PROCEDENCIA:
dentro do projeto => AUTO (sessao live, reprocessamento); fora do projeto com
recusa registrada em _wizard_metadata["import_config"] => SKIP; fora sem
registro => ASK. Sem projeto (video unico, live ad-hoc) => AUTO, legado
intacto. import_zone_data_from_video_parquets passa a importar
incondicionalmente: a permissao e de quem chama.

O ramo ASK abre um dialogo que NOMEIA a pasta de origem -- era justamente a
informacao ausente que fazia o import parecer configuracao escondida -- e
avisa, depois de importar, para conferir os contornos.

O espelho global vira ZoneManager.should_mirror_to_global(). Ele e mantido
quando nao ha video alvo (o save E o global), quando project_type == "live"
(uma camera, uma arena, e os .mp4 gravados nao tem chave propria: ZoneContext
Service, ValidationManager e a grade de sessoes planejadas leem o global) e
quando nao ha project_type declarado (video unico e live ad-hoc). So um
projeto que se declara nao-live e excluido -- a restricao e pelo caso provado
errado, nunca pela negacao do caso conhecido.

Corrige tambem um crash latente pre-existente no fluxo de video unico:
save_project() levanta ProjectInvalidError sem project_path, e o import
chegava la depois de ter importado com sucesso.

Verificacao: pytest -q 6695 passed / 0 failed; pytest -m gui -n0 1206 passed;
mypy . limpo em 902 arquivos; ruff limpo; tests/i18n 79 passed;
mutation_check --module zone_autoimport_policy 5/5 killed.

i18n: as 4 strings novas foram anexadas ao .pot/.po e o .mo recompilado, em
vez de rodar update_translations.py -- o catalogo em main esta completo mas
defasado do fonte (8 msgids nunca extraidos), e regenerar arrastaria ~2600
linhas de churn alheio para dentro deste fix. O par ficou registrado em
locales/_pairs/ para a proxima regeneracao completa.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.57143% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/zebtrack/ui/components/dialog_manager.py 89.58% 5 Missing ⚠️
...c/zebtrack/core/services/zone_autoimport_policy.py 95.77% 3 Missing ⚠️
src/zebtrack/core/project/project_manager.py 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@MarkSant
MarkSant merged commit c1a5fd8 into main Sep 7, 2026
6 checks passed
@MarkSant
MarkSant deleted the claude/arena-rois-source-investigation-cd8be2 branch September 7, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant