feat: foto vira conversa (sub-projeto 3) - #6
Merged
Merged
Conversation
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
Substitui o loadDetailsFromLocalDB da câmera, que fazia três queries e juntava as tabelas à mão em JavaScript. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
Com compensação em vez de transação: o driver não tem uma, e abrir uma enquanto o sync escreve na mesma conexão convida database is locked. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
FK ligada (PRAGMA foreign_keys = ON) sem ON DELETE, e sem transação: quando appendMessage falha, chat_sessions já está commitada, e apagar só fila_diagnosticos era recusado em silêncio pelo próprio SQLite. Agora a limpeza desfaz chat_messages, depois chat_sessions, depois fila_diagnosticos — e a falha da própria limpeza vai para console.warn em vez de sumir. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
…opinião Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
…câmera Sobrevive ao app morrer no meio: reabrir a conversa volta a tentar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
O card sai da tela inteira e vira componente puro de props, com a bula recolhida para caber numa conversa. getCrossValidationPriority passa a receber só o status, que é tudo que ela olhava; o único call site em produção, em app/camera.tsx, foi ajustado para o novo formato. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
…vergência Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
De 1.995 linhas para uma tela que roda a inferência em memória e só grava quando o produtor confirma. O card, o feedback, a persistência e a orquestração da segunda opinião já mudaram de casa. O código de web sai junto: o target morreu no sub-projeto 1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
useCameraDevice enumera o hardware sem depender de permissão, então num aparelho recém-instalado `device` já existe com `hasPermission` ainda false: o obturador ficava tocável sob a própria tela de permissão e disparava capturePhoto num output sem nenhum <Camera> montado, caindo num "erro de captura" genérico em vez de pedir a permissão que falta. O obturador continua visível — esconder a affordance principal seria pior do que mostrá-la apagada — mas trava atrás de `podeCapturar` (permissão E sensor), o mesmo botão de flash ao lado já usava essa regra e agora reaproveita a derivada em vez de repetir a condição. De brinde, devolve à aba "Câmera" o accessibilityLabel que a reescrita anterior tinha perdido. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
A segunda opinião passa a ser disparada no mount da conversa, que é quem fica viva durante a chamada. O aviso legal aparece na tela de conversa pela primeira vez. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
diagnosticContext, buildDiagnosticChatContext e diagnosticDraftService existiam só para ligar a câmera ao chat e para limpar o que a câmera gravava antes da hora. Nenhum dos dois problemas existe mais. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
A revisão da branch inteira achou dois furos que o teste de dedupe concorrente não cobria. 1. diagnosticImageUploadService dedupava uploads por um mapa em voo que esvazia assim que a promessa termina — cobre só chamadas concorrentes. No mount da conversa, se a segunda opinião sobe a imagem com sucesso e grava a chave em fila_diagnosticos, mas a resposta do motor (disparada em paralelo) falha depois de já ter resolvido a mesma chave compartilhada, o CloudEngine nunca grava a chave no anexo da mensagem (só grava em onDone, numa resposta completa). Na próxima abertura da conversa o disparo automático roda de novo, resolveImageKey não acha nada em attachment.imageS3Key, e a foto sobe pela segunda vez — um PUT pago a mais no S3, com uma chave diferente da que o servidor já registrou no cross-validate. Agora uploadDiagnosticImage lê fila_diagnosticos.image_s3_key antes de mintar uma URL nova, cobrindo os três chamadores (cloudEngine, crossValidationService, syncService) sem tocar em nenhum deles. 2. Numa divergência com doença fora do catálogo local, o backend devolve llm_doenca_nome mas llm_doenca_id nulo (FK para doencas(id) não comporta doença fora do catálogo). Quando o produtor escolhia a hipótese da LLM nesses casos, o feedback chegava ao servidor sem nenhuma identificação da doença — justamente os casos mais valiosos para melhorar os modelos, porque são doenças que a visão local nem consegue representar. A nota automática do feedback agora carrega o nome da doença quando a doença escolhida foi a da LLM. 174 testes existentes + 1 novo cobrindo o item 1 = 175 verdes. Typecheck limpo. Backend não tocado. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sub-projeto 3 de 7 da reformulação: a foto deixa de ser uma tela de resultado e vira a primeira mensagem de uma conversa.
Spec:
CropAI-docs/especificacoes/2026-09-12-foto-vira-conversa-design.mdPlano:
CropAI-docs/planos/2026-09-12-foto-vira-conversa.mdRegistro de execução, com as 12 decisões tomadas e o custo de cada uma se estiver errada:
CropAI-docs/historico/execucoes/2026-09-13-foto-vira-conversa-ledger.mdO que muda
O obturador roda a inferência em memória e mostra o veredito para confirmação. Nada é gravado até o produtor tocar em "Analisar" — 3 a 5 tentativas até um bom quadro é o normal em campo, e cada tentativa persistida custaria uma sessão no histórico, um PUT no S3 e uma chamada paga ao modelo. Confirmada a foto, nascem diagnóstico, sessão e primeira mensagem numa operação só, e a tela navega para a conversa.
Na conversa, o diagnóstico é renderizado como card dentro da bolha da foto — a partir do banco, sem depender de modelo de linguagem nenhum. É o que mantém RF01 e RF02 vivos quando o modelo local não está instalado e não há sinal. A segunda opinião do modelo de nuvem roda em paralelo e atualiza o card quando chega; os botões de feedback (RF06) passam a nomear as duas hipóteses quando há divergência. O aviso legal da Lei 7.802 passa a existir na tela de conversa, que até aqui não avisava nada.
app/camera.tsx: 1.995 → 510 linhas. A ponte provisória que o sub-projeto 2 deixou (diagnosticContext), obuildDiagnosticChatContexte odiagnosticDraftServiceinteiro deixam de existir — os problemas que eles resolviam não existem mais.Nenhuma migração de banco. O schema do sub-projeto 2 já bastava.
Três defeitos que a execução encontrou
1. A câmera não funcionava em aparelho — e isso é anterior a este PR.
O código chamava
nativeCameraRef.current.takePhoto(...), método que não existe nareact-native-vision-camera@5.0.11instalada. Passou pelo typecheck porque o arquivo declaravalet NativeCamera: anyeuseRef<any>, e orequirecondicional (que existia só para o bundler de web) apagava a checagem daquele caminho. Mesma classe do bugloadModel/loadTensorflowModelregistrado no sub-projeto 2. A reescrita corrige: import estático e tipado, captura porusePhotoOutput()+capturePhoto()+saveToTemporaryFileAsync().2. A compensação não compensava.
chat_sessions.origin_diagnostic_local_idé FK parafila_diagnosticossemON DELETE, o banco roda comPRAGMA foreign_keys = ON, e cadaexecuteé autocommit. Apagar o diagnóstico primeiro era recusado pelo SQLite, e o.catchmudo engolia a recusa — sobrava o órfão mais uma conversa vazia apontando para ele, que o JOIN do mapa exibiria como análise real. A limpeza agora vai de filho para pai: mensagens, sessão, diagnóstico.3. "Um PUT por foto" só valia no caminho concorrente.
Se a resposta do motor falhasse depois de a segunda opinião já ter subido a imagem, a chave nunca chegava ao anexo da mensagem — só o
onDonea grava ali. Na abertura seguinte o disparo automático subia a foto de novo: segundo PUT pago, com chave diferente da que o servidor já registrou. O upload passa a lerfila_diagnosticos.image_s3_keyantes de mintar URL nova.Verificação
db/sqlite.tssem diff)camera.tsxCada uma das 11 tarefas passou por revisão independente própria; duas tiveram rodadas de correção com re-revisão escopada. A revisão da branch inteira saiu "com correções", e as correções foram aplicadas e re-revisadas.
O que este PR não prova
Nada aqui tocou um aparelho. Como o defeito nº 1 mostra, a captura nunca funcionou em dispositivo, então nenhum dos três sub-projetos foi homologado de fato. Quatro itens ficam para a homologação de campo:
.ggufausente e o aparelho offline, o card aparece completo — doença, severidade, sintomas, defensivo, dosagem, carência e aviso legal — e só a prosa falta. É a afirmação central do desenho.fila_diagnosticosnem emchat_sessions.__DEV__leva a imagem mock até o card, com a segunda opinião emSKIPPED.Decisões que valem sua revisão
SKIPPED. Deixá-la pendente para rodar quando o sinal voltasse seria melhor para o produtor, mas o cliente só sincroniza linhasPENDINGe o servidor protege o veredito porTERMINAL_CV_STATUSES— o veredito tardio nunca chegaria lá, e cliente e servidor divergiriam em silêncio. Fica para o sub-projeto 4, que constrói o caminho de re-sync.cv.llmDoencaNomeé lido dentro deenviarnoFeedbackPanelmas não está nas dependências daqueleuseCallback. Não é bug vivo —onSubmittedé recriado a cada render e força o recompute —, mas acorda se alguém estabilizar aquele prop.🤖 Generated with Claude Code
https://claude.ai/code/session_015dTz4Z51NvTgHWGrisq2oy