Skip to content
  1. Aug 08, 2026
    • Igor Matheus's avatar
      Remove o dio: o app não fala HTTP com ninguém · 8070d005
      Igor Matheus authored
      
      
      Nenhum arquivo do app importava dio nem citava http://. A dependência declarada
      dava a impressão contrária a quem lesse o pubspec — dependência não usada mas
      declarada é documentação errada.
      
      257 testes de widget e analyze limpos depois da remoção.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      8070d005
    • Igor Matheus's avatar
      AC-4: o envio da recuperação de senha, e o caso que decide a story · ab6b156a
      Igor Matheus authored
      
      
      257 testes de widget, 332 no Firebase, 5 em device. 9 dos 12 casos do plano
      executados; os três que faltam estão nomeados, não escondidos.
      
      O caso que decide não é o caminho feliz
      
      É o AC-4-02: e-mail sem conta responde igual a e-mail com conta. Ele é difícil
      porque os dois ambientes discordam — em produção, com a proteção de enumeração
      ligada, o Auth devolve sucesso para e-mail inexistente; no emulador devolve
      user-not-found. Repassar o erro faria a tela dizer "não achei esse e-mail" em
      desenvolvimento sempre e em produção nunca: o primeiro é vazamento, o segundo é
      comportamento que ninguém testou. O repositório engole user-not-found e
      invalid-email, e o caso em device existe para proteger esse engolir.
      
      Dois casos inverteram, e os dois eram bons testes
      
      AC-2-09 ("não oferece esqueci minha senha") e AC-1-11 ("avisa que não há
      recuperação") protegiam a decisão de 03/08 de não prometer o que não existia.
      Quando a funcionalidade chegou, o que eles protegiam virou mentira e eles
      falharam — que é exatamente o trabalho deles. Reescritos com o motivo da inversão
      no comentário, como o MG-2-07.
      
      Um defeito de tela: o botão ficava girando enquanto o diálogo estava aberto,
      porque o estado de "enviando" só caía no finally. A tela nunca assentava e o
      indicador mentia — o envio já tinha terminado.
      
      Três tasks ficam abertas de propósito. BE-1 e BE-2 são clique de console, e marcar
      como feito o que não cliquei seria mentir no controle. E a FE-4 mudou de natureza:
      ela supunha tela de troca de senha nossa, mas o link abre página hospedada pelo
      Firebase — a decisão que resta é de produto, aceitar a página dele ou hospedar a
      nossa, e um checkbox esconderia essa escolha.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      ab6b156a
    • Igor Matheus's avatar
      AC-4 volta ao escopo: a decisão de cancelar dependia de um preço que mudou · 5fae0c5b
      Igor Matheus authored
      
      
      A story foi cancelada em 03/08 porque não havia serviço de e-mail, e a decisão
      estava certa naquele dia — montar entrega de e-mail para uma POC era caro. O que
      mudou não foi a prioridade: foi o preço. Com o Firebase Auth e o Identity Platform
      já ligados, o envio, o link com expiração, a tela de troca e a invalidação da senha
      antiga vêm prontos.
      
      O que a story protege
      
      Recuperação é o caminho mais fácil de vazar quem tem conta: a tela de login foi
      fechada contra enumeração, e se esta responder diferente para e-mail conhecido o
      problema volta pela porta ao lado. Por isso o critério é resposta idêntica com e
      sem conta.
      
      E ela muda uma premissa, não só adiciona tela. AC-1, AC-2 e AC-3 foram escritas
      assumindo que perder a senha custa a conta — AC-3 inteira se justificava assim
      ("sessão que cai sozinha custa uma conta perdida"). A sessão persistente continua
      valendo; deixa de ser a única rede. O aviso de "não há recuperação de senha" sai
      junto com a entrega: aviso que sobrevive à funcionalidade é pior que não ter tido.
      
      O PRD de identificação e acesso reabre: 3/4 stories, 26/35 tasks. Um PRD concluído
      que volta a aberto é o comportamento certo do controle — fechar é sobre as stories
      que existem, e passou a existir mais uma.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      5fae0c5b
    • Igor Matheus's avatar
      PP-8 no ar: lembrete, push e deep link — e a task mais antiga do projeto fecha · 23a88fb6
      Igor Matheus authored
      
      
      251 testes de widget, 332 no Firebase, 4 em device. O lembrete de prazo saiu do
      papel quase dois meses depois de a story ser escrita: o que a travava era a
      credencial do FCM, e ela chegou com o projeto real.
      
      O deep link anota, não navega
      
      Com o app fechado, o toque na notificação chega antes de existir tela, sessão ou
      rota — navegar ali é navegar para o nada. O push guarda o poolId e a tela inicial
      consome quando já pode abrir. É o mesmo desenho do convite pendente, e pelo mesmo
      motivo. Bolão que não está mais na lista (saí, fui removido) não abre: push antigo
      levaria a uma tela vazia, e ficar na home é a resposta certa.
      
      Permissão negada não trava nada, e o teste força isso com o serviço de push
      explodindo — promessa que depende de o outro lado se comportar não é promessa.
      
      PP-10 OPS-1 fecha, e ela era a mais antiga
      
      "Hospedagem que não hiberne o processo de polling" foi o requisito que motivou a
      migração inteira. O Cloud Scheduler não dorme. Uma task de infraestrutura que
      sobreviveu a duas fases e fechou trocando de arquitetura, não de fornecedor.
      
      Nenhuma credencial no repositório (MG-6 OPS-3): o token da fonte vive em
      functions/.env.local para o emulador e no Secret Manager para produção; as
      credenciais do app ficam em .env.firebase — não por serem secretas, a chave de API
      viaja em todo APK, mas para o código não amarrar o app a um projeto só.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      23a88fb6
    • Igor Matheus's avatar
      Push entregue e backend publicado em produção · 95022ea5
      Igor Matheus authored
      
      
      App Android registrado em bolao-raro-4382d, 31 Functions no ar em
      southamerica-east1, rules e índices publicados. O sincronizarComAFonte e o
      lembrarPrazoFechando estão no Cloud Scheduler — o que fecha a PP-10 OPS-1, aberta
      desde o começo do projeto.
      
      O push entrega, não cria
      
      O gatilho escuta usuarios/{uid}/avisos e manda o que já está lá. Se a entrega
      falhar, ninguém perde informação: o aviso continua no app. É o que mantém PP-8-08 e
      PR-7-09 verdadeiros com o push desligado, e é por isso que permissão negada é
      resposta e não erro. Token que o FCM recusa em definitivo é apagado na hora — app
      desinstalado deixa token que faz toda entrega futura gastar tentativa e voltar erro.
      
      Um defeito grave que o teste de push encontrou de lado
      
      Ao escrever o teste do registro, o cold start apareceu quebrado: restaurar() ainda
      perguntava "há accessToken guardado?", e esse campo era a credencial do backend
      antigo. Com o Firebase Auth ninguém escreve mais nele — então a pergunta tinha uma
      resposta só, null, e o app mandava para o login todo mundo, sempre, logado ou não.
      
      O teste que deveria pegar isso passava: ele injetava um token no armazenamento,
      cenário que não existe mais em produção. É a mesma armadilha de MG-7-64 — cenário
      impossível de alcançar pelo produto — agora do lado do app. Quem sabe se há sessão
      passou a ser a camada de autenticação.
      
      O .env das Functions virou .env.local
      
      O deploy publica o .env como variável de ambiente comum, e o token colidiu com o
      Secret Manager: "Secret environment variable overlaps non secret". O .env.local é
      lido só pelo emulador e nunca é publicado — que é exatamente o que se quer de um
      segredo local. Documentado no README com a tabela das duas casas.
      
      E a rule souAdmin saiu: ela ficou sem uso quando toda ação de admin virou callable,
      e rule morta é armadilha — alguém lê e acha que existe checagem ali.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      95022ea5
    • Igor Matheus's avatar
      MG-9: o NestJS sai, e o mapa de onde cada regra foi morar fica · 305deaf4
      Igor Matheus authored
      
      
      O bolao-backend/ foi removido. Nenhuma regra mudou de conteúdo — mudou quem a
      aplica, e a tabela completa está em docs/prd/migracao-firebase.md, porque o
      histórico do git responde "o que saiu" e não responde "para onde foi".
      
      O resumo: prazo e sigilo viraram security rule; pontuação, classificação e
      sincronização viraram Function; a lista da tela inicial virou índice mantido por
      gatilho; e o palpite continua escrita direta, porque é o caminho quente e Function
      no meio somaria cold start ao momento em que todo mundo palpita junto.
      
      Por que só agora
      
      Enquanto o NestJS existia, ele era o oráculo: dava para comparar comportamento em
      vez de supor. Foi assim que a única divergência real da migração (CV-4-01) apareceu
      como divergência, e não como "sempre foi assim". Remover cedo teria sido mais
      rápido e teria custado essa referência.
      
      Três testes passavam fazendo HTTP de verdade
      
      Este é o achado da story, e não estava previsto. Os dublês de teste herdavam da
      implementação HTTP e sobrescreviam só o que o caso precisava — método não
      sobrescrito ia para a rede. CC-1-09, CC-2-03 e CC-10-12 navegavam para a tela da
      rodada, que chama rodada() ao montar; a chamada falhava por rede, a exceção morria
      no catch da tela, e a asserção era sobre a tela anterior. Havia HTTP num teste de
      widget, e ele dependia de o servidor estar fora do ar para passar.
      
      Com o ApiClient removido, os falsos passaram a herdar de um dublê base que lança
      UnimplementedError com o nome do método faltando. Os três caíram na hora e ganharam
      o método que a tela realmente chama. Herança conveniente esconde dependência.
      
      O ApiClient saiu junto (MG-7 FE-6), com as cinco implementações HTTP, o API_URL e a
      chave --dart-define=BACKEND. Verificado em device com o NestJS já removido: 332
      casos no Firebase, 244 de widget, 4 em device contra a fonte oficial, analyze limpo.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      305deaf4
    • Igor Matheus's avatar
      MG-8 fechada: zero casos a verificar em 420 · a8716a37
      Igor Matheus authored
      
      
      332 casos verdes no Firebase (22 arquivos), 244 de widget sem nenhuma asserção
      alterada pela migração. As 38 stories da fase 1 em 100%, conferível por
      python3 docs/cobertura.py.
      
      397 verificados com o id na stack nova, 22 reescritos com justificativa, 1
      cancelado, 0 a verificar.
      
      Três formas de trabalho, e só uma é "escrever teste"
      
      Reexecutar a regra mantendo o id de origem foram 130 casos novos em sete arquivos
      de paridade. Etiquetar teste que já existia e passava foi a maior parte do resto —
      e não é fachada: sem o id ninguém consegue dizer se o caso do plano está coberto ou
      se o teste é sobre outra coisa, e é essa resposta que autoriza desligar o NestJS.
      Registrar o que deixou de existir foram os 22 do grupo B, cada um com o que passou
      a valer no lugar; a justificativa mora no documento e o script a lê de lá, porque
      "reescrito e registrado" só é honesto com a justificativa ao lado.
      
      Uma divergência real de comportamento em toda a migração
      
      CV-4-01, achada e corrigida. Todo o resto foi mudança de mecanismo — registrada — ou
      defeito do próprio teste.
      
      E os defeitos de teste foram mais numerosos que os de produto, o que é um dado
      sobre o tipo de sistema que isto virou. Cinco padrões, todos documentados nos
      arquivos onde apareceram: espera pela categoria em vez do efeito; espera por um
      passo anterior ao que se quer medir; cenário impossível de alcançar pelo produto;
      contagem global onde o caso controla só um pedaço; e timestamp recalculado a cada
      chamada, que faz o produto parecer ter mudado o que ninguém mudou.
      
      Nenhum deles é sobre o Firebase. São sobre escrever teste de sistema assíncrono e
      compartilhado.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      a8716a37
    • Igor Matheus's avatar
      PP-8 sai do zero: o lembrete de prazo não precisava esperar o push · 434606b1
      Igor Matheus authored
      
      
      312 casos verdes no Firebase. Cobertura com o id em 335 de 420 (80%). Oito stories
      em 100%: CC-3, CC-5, CC-6, CC-8, CC-10, PP-6, PP-7 e PR-1.
      
      PP-8 estava em zero por um motivo que não era o dela
      
      O mapa de cobertura dizia 0/12, e a nota dizia "bloqueado por MG-6 BE-3/BE-4". O
      que bloqueava de verdade era só o BE-4: push é entrega, e decidir quem lembrar não
      depende de entregar. Escrito o job (BE-3), PP-8 foi a 8/12 — sobra permissão de
      notificação, deep link e iOS, que são device e credencial.
      
      Isso é o mapa fazendo o trabalho dele: ele não só conta, ele mostra onde a conta
      estava errada.
      
      O aviso vai para o Firestore, que é o canal que sempre existe. É literalmente o
      critério de PP-8-08 — sem push, o lembrete continua legível no app.
      
      O job decide na hora, não agenda: olha quem ainda não palpitou nos jogos que fecham
      nas próximas 2 h. É isso que faz "palpitou no meio do caminho" cancelar o lembrete
      sozinho, sem precisar de cancelamento. E aviso e marca vão na mesma escrita: se só
      a marca gravasse, a pessoa nunca seria lembrada; se só o aviso, seria lembrada a
      cada meia hora até o apito.
      
      Mais 30 casos de tela ganharam o id: regras, avisos, home, participantes, ranking e
      extrato. E um teste meu contava lembretes do mundo inteiro, incluindo os bolões que
      outros arquivos deixaram com jogo prestes a começar — passou a contar os do bolão
      que ele mesmo montou.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      434606b1
    • Igor Matheus's avatar
      MG-8 QA-3: paridade de palpite e sincronização (70%) · a5923b28
      Igor Matheus authored
      
      
      304 casos verdes no Firebase. Cobertura com o id em 297 de 420. CC-5, CC-8, PR-1 e
      PR-6 fecharam em 100%.
      
      Entrou o que acontece com o palpite quando o mundo muda embaixo dele: adiamento
      reabrindo o prazo sem apagar o palpite, antecipação fechando mais cedo, adiamento
      para o passado que não reabre, mesmo horário na fonte que não conta como
      adiamento, job parado recuperando o atraso e fonte fora do ar que não corrompe o
      que está guardado.
      
      Dois testes meus mentiram de formas novas
      
      PP-10-01 acusava o produto de alterar horário que ninguém alterou: o ajudante
      gerava Date.now() + deslocamento a cada chamada, então "o mesmo horário" eram
      instantes diferentes por alguns milissegundos. Base de tempo fixa por arquivo.
      
      MG-7-45 e os cenários de CV-5 plantavam a classificação à mão e passavam por
      acidente — nada recalculava o agregado fora da apuração. Com a correção de
      CV-4-01, a entrada de participante passou a recalcular, e a linha plantada virou
      zero. MG-7-45 passou a conquistar os pontos (palpite, jogo terminado, apuração),
      que é o cenário que o produto alcança; CV-5, que verifica leitura, espera o
      recálculo assentar antes de plantar.
      
      A lição é a mesma nas duas: teste que passa por acidente é dívida com juros — ele
      cobra no dia em que o produto fica mais correto.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      a5923b28
    • Igor Matheus's avatar
      MG-8: a primeira divergência real da migração · ec49f639
      Igor Matheus authored
      
      
      290 casos verdes no Firebase. Cobertura com o id em 281 de 420 (66%). Entram 30
      casos de paridade de fluxo — criação, regras, convite, papel, saída e ciclo de
      vida — com o id de origem preservado.
      
      CV-4-01: quem saía do bolão continuava no ranking
      
      No NestJS o ranking era somado na leitura a partir de quem estava ativo, então
      sair tirava a pessoa da lista na hora. No Firestore ele é agregado materializado,
      e a classificação só era reescrita na apuração — quem saía continuava aparecendo
      até o próximo jogo terminar, que pode ser semana que vem ou nunca, porque bolão
      encerrado não tem próximo jogo.
      
      Eram dois defeitos empilhados, e o segundo é o que dá o susto: a mudança de
      vínculo não disparava recálculo, e o recálculo não apagava a linha de quem saiu —
      ele reescrevia a de cada ativo, e o que não é reescrito fica. Agregado
      materializado só está correto quando alguém cuida do que sobra.
      
      É o tipo de coisa que a migração introduz sem avisar: a regra não mudou, o lugar
      mudou, e o lugar novo tem uma propriedade que o antigo não tinha — memória.
      
      Duas armadilhas de cenário viraram regra do arquivo
      
      Convidar depois do apito é recusado, corretamente. Então bolão com jogo já
      começado e convidado dentro precisa ser montado na ordem da vida real: entra todo
      mundo, e depois o jogo começa. E cada caso ganhou o seu próprio campeonato, porque
      o calendário é global e sobrevive ao bolão — a mesma lição de 07/08, agora
      estrutural em vez de anotada.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      ec49f639
    • Igor Matheus's avatar
      MG-8 QA-3: de 188 para 241 casos verificados com o id · b595b538
      Igor Matheus authored
      
      
      260 casos verdes no Firebase, 244 de widget. Cobertura em 57% de 420.
      
      CV-5 saiu de 1/10 e ganhou um caso que não existia
      
      O histórico não guarda campeão em lugar nenhum: ele compõe o índice do usuário com
      a classificação de cada bolão encerrado, na leitura. A consequência apareceu ao
      portar e virou CV-5-11 — correção de placar depois do encerramento muda quem
      aparece como campeão na lista de bolões passados. É CV-1-07 vivendo no histórico.
      
      Boa parte do avanço foi etiquetar teste que já existia e passava
      
      PP-3 (contagem de prazo), PP-4 (rascunho), AC-1 (tela de cadastro) e AC-3 (cold
      start) tinham caso verde sem o id no nome. Não é trabalho de fachada: sem o id
      ninguém consegue dizer se o caso do plano está coberto ou se o teste que existe é
      sobre outra coisa — e a resposta muda a decisão de desligar o NestJS.
      
      O que sobreviveu à troca do Auth virou caso de verdade: e-mail normalizado para
      minúsculas (senão "Ana@x" e "ana@x" viram duas contas e a segunda parece ter
      perdido o histórico), e-mail malformado recusado, conta apagada perdendo acesso.
      O resto do grupo AC é mecanismo que deixou de existir, já registrado no mapa.
      
      QA-4 continua sem nenhuma divergência de comportamento.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      b595b538
  2. Aug 07, 2026
    • Igor Matheus's avatar
      MG-8: CV-2 e PR-6 saem do zero, e as duas decisões do dono entram · 9dd5ec3c
      Igor Matheus authored
      
      
      247 casos verdes no Firebase, 244 de widget. A cobertura com o id vai de 188 para
      211 de 420.
      
      As duas stories que o mapa acusou em zero não eram difíceis
      
      CV-2 (co-campeões) e PR-6 (recálculo por placar corrigido) estavam em 0/9 e 1/10 —
      e a maior parte do que faltava é regra pura, que se testa sem banco. Elas eram as
      que ninguém tinha olhado, que é exatamente o que o mapa existe para revelar. CV-2
      ficou 7/9 (sobraram os dois casos de tela) e PR-6 fechou 10/10.
      
      Os casos mantêm o id de origem: CV-2-03 continua sendo CV-2-03. Dois deles cobrem
      o que mais dói na migração — correção de placar antes e depois do encerramento
      trocando o campeão, porque ele é derivado na leitura e nunca congelado.
      
      As duas decisões do dono, respondidas
      
      A email enumeration protection já estava ligada (é o padrão dos projetos novos),
      então o login nunca foi oráculo de enumeração neste projeto e AC-2-02 sai de
      bloqueado. O app já tratava os dois casos com a mesma mensagem. O Identity
      Platform foi ligado, e o mínimo de 8 caracteres de senha passa a ser aplicado pelo
      servidor.
      
      Nenhuma das duas é verificável no emulador: ele não aplica política de senha nem
      proteção de enumeração, de propósito, para o teste não depender de nuvem. Ficam
      registradas por configuração, com data, e conferidas em device.
      
      Um teste meu contava correções num jogo compartilhado entre casos e via as do
      vizinho — a trilha vive no jogo, que é global e sobrevive ao bolão.
      
      QA-4 segue sem nenhuma divergência registrada: o que apareceu até aqui foi
      mudança de mecanismo (rule em vez de service) ou defeito do próprio teste, nunca
      comportamento diferente do produto.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      9dd5ec3c
    • Igor Matheus's avatar
      MG-8 (parcial): o mapa que impede esta story de ser fingida · a83d5e14
      Igor Matheus authored
      
      
      228 casos verdes no Firebase (16 arquivos). Entram 34 casos de paridade com o id
      de origem preservado e a medição de custo.
      
      O mapa é script, não tabela
      
      docs/cobertura.py varre os planos da fase 1 e diz onde cada caso é verificado
      hoje. Existe como script porque o risco escrito na própria story é "é a story
      mais fácil de fingir que foi feita", e tabela escrita à mão envelhece em silêncio.
      O critério é rigoroso de propósito: só conta como verificado quando o id aparece
      num teste da stack nova. Resultado honesto: 188 de 420 (44%).
      
      Os 231 restantes não são um bloco só, e tratá-los como um seria o erro que a
      story avisa: casos de tela cujo teste existe e passa mas não cita o id; casos cujo
      mecanismo deixou de existir (rotação de refresh, rate limit de login, "não há
      recuperação de senha") — reescritos e registrados um a um no mapa, nunca
      descartados em silêncio; e o grupo que falta reexecutar, onde CV-2 e PP-8 estão em
      zero.
      
      34 casos reexecutados com o id original
      
      PP-7-05 continua sendo PP-7-05. Foi escolhido primeiro o grupo de sigilo, prazo,
      papel e autorização — o que sumiria primeiro se "os testes passam" bastasse como
      prova. Onde o meio mudou, o caso diz qual é: no NestJS recusava um if num service,
      aqui recusa o banco ou a callable. Para quem chama a API direto a diferença é
      grande — não existe mais resposta para filtrar.
      
      Custo medido, não estimado
      
      Contador em volta do Firestore, rodando os fluxos de verdade. O ranking geral
      custa 50 leituras para uma página de 50 num bolão de 60: uma por linha, não por
      participante nem por palpite. Somar na leitura seria 60 x 10 x 38 documentos por
      abertura de tela. Um bolão de 60 pessoas gasta ~5.150 leituras por rodada, com
      folga de 10x no free tier.
      
      Dois testes meus dependiam de banco recém-criado: contavam avisos e participantes
      em bolão de id fixo, e o emulador que fica de pé para o teste em device acumula a
      execução anterior. Passaram a criar bolão e participante novos a cada rodada.
      
      Seguem travando a paridade de AC-2 as duas decisões do dono: email enumeration
      protection e Identity Platform.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      a83d5e14
    • Igor Matheus's avatar
      MG-7: o app sobre o Firebase, verificado em device · ec68ccd5
      Igor Matheus authored
      
      
      244 testes de widget passando sem mudança de asserção, mais 4 casos em device
      contra os emuladores e a fonte oficial de verdade. Falta só FE-6 (remover o
      ApiClient), que espera MG-8.
      
      A troca não vazou para a apresentação, e dá para provar
      
      O que mudou nos 235 testes de widget foi uma palavra por arquivo: o falso passou
      a estender XRepositoryHttp em vez de XRepository, porque o nome de sempre virou
      contrato com duas implementações. Nenhuma tela, nenhum estado, nenhuma mensagem.
      O que sustentou isso foi manter os modelos intactos — eles são o formato que a
      tela conhece, e é por isso que MG-2 gravou os campos em inglês.
      
      Um dono só para o offline
      
      A story avisava que o cache muda de dono. O Firestore tem persistência local, mas
      responde dado antigo sem dizer que é antigo; o CacheLocal grava a idade e a
      mostra. Persistência do Firestore desligada: ter os dois seria ter duas respostas
      para a mesma pergunta, uma sem rótulo.
      
      O relógio virou desvio, não instante: a hora do servidor vem de uma callable uma
      vez por sessão, descontando meia viagem de rede. Falhar não derruba nada — sem
      medida a contagem cai no aparelho, e quem recusa continua sendo a rule.
      
      Quatro coisas que só o device mostrou
      
      Os 188 casos do servidor e os 244 de widget passavam com tudo isto quebrado:
      cleartext HTTP bloqueado pelo Android desde a API 28 (liberado só na variante de
      debug, e só para o emulador — abrir isso em release trocaria um problema de
      desenvolvimento por um furo de verdade); o SDK Android validando o formato da
      chave de API antes de qualquer chamada, com 40 caracteres em vez de 39; quem se
      cadastrava como "Bruno" virando "bruno1786151672" no bolão, porque o gatilho de
      perfil dispara antes de o displayName existir; e a tela do extrato quebrando em
      vez de mostrar erro quando a falha não era ApiError.
      
      E dois defeitos do meu próprio teste em device: ele herdava a sessão da execução
      anterior (o Firebase persiste o login), e o caso do apito se auto-pulava quando
      não sobrava jogo começado — caso que pula sozinho não protege nada.
      
      A cota da fonte aparece ao rodar em série: repetir o arquivo no mesmo minuto faz
      a criação de bolão falhar, e isso é o adaptador recusando antes de gastar as 10
      requisições por minuto, que é o comportamento de PP-10.
      
      Registrado para MG-8: o Firestore não tem offset, então a paginação do ranking lê
      do começo e descarta — aceitável com dezenas de participantes, e trocar o
      contrato da tela por cursor é outra story.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      ec68ccd5
    • Igor Matheus's avatar
      MG-7: o resto do backend que o app chama, e um furo do CC-3 · fa413194
      Igor Matheus authored
      
      
      188 casos verdes em 14 arquivos. Entraram regras, ciclo de vida, correção manual,
      extrato e ranking da rodada — que fecham junto as tasks BE-4, BE-5 e BE-6 de MG-5.
      No app entrou só a configuração: SDKs do Firebase e apontamento por --dart-define,
      com o emulador em opt-in (esquecer o define num build de release tem de cair no
      ambiente de verdade, não num emulador que não existe no aparelho de ninguém).
      
      Um furo do CC-3 que só apareceu ao ligar o app
      
      A rule de MG-2 deixava o admin escrever direto no documento do bolão, e o caso
      MG-2-07 afirmava isso como correto. Rule não consegue olhar se já existe palpite
      no bolão — ou seja, dava para trocar a tabela de pontos depois do primeiro
      palpite, que é exatamente o que a trava existe para impedir. Bastaria esperar os
      palpites e ajustar a pontuação para o que o palpite do admin acerta; o sigilo de
      PP-7 não cobre isso. A escrita direta no bolão passou a ser negada por inteiro e o
      MG-2-07 inverteu. Vale igual para closedAt e archivedAt: encerrar exige não haver
      jogo pendente, e encerramento antecipado precisa ficar marcado como tal.
      
      O que não foi portado, e por quê
      
      O encerramento antecipado do NestJS empurrava o kickoffAt dos jogos abertos para
      fechar o prazo. Aqui isso seria defeito: o jogo é global e o horário dele vale
      para todos os bolões do campeonato. A rule do palpite já recusa bolão encerrado.
      
      A consolidação do aviso virou fusão
      
      PR-7 pede aviso consolidado, e no NestJS isso era um coletor em memória da
      sincronização. Esse coletor não existe em Functions: cada jogo corrigido é uma
      invocação separada, possivelmente em outra máquina. Aviso de correção não lido e
      recente do mesmo bolão passa a receber o jogo novo em vez de virar outro.
      
      A correção manual também deixou de devolver quantos avisos saíram — quem apura é
      gatilho, e esperar por ele seria segurar o admin numa tela por um número. Ela
      devolve quantos palpites serão reapurados.
      
      Três testes meus, três defeitos meus, nenhum do produto: MG-7-62 esperava pela
      categoria do efeito (existe aviso) e não pelo efeito, e Ana já tinha um aviso
      antigo; MG-7-63 esperava pelo ponto para depois exigir fusão, mas o ponto é
      escrito antes do aviso, então o teste criava a corrida que denunciava; MG-7-64
      testava cenário impossível, porque em bolão pequeno toda correção mexe na posição
      de todo mundo. E um quarto, de vizinhança: o calendário é global, então dois
      arquivos de teste na mesma rodada de BSA disputavam os mesmos jogos — cada um
      passou a ter o seu código de campeonato.
      
      Falta a camada de dados do app (FE-1, FE-2, FE-4 a FE-6) e a verificação em
      device.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      fa413194
    • Igor Matheus's avatar
      MG-7 (parcial): quem escreve no Firestore · 9e870e7b
      Igor Matheus authored
      
      
      158 casos verdes em 12 arquivos. 26 novos, cobrindo criação, convite, entrada,
      saída e papel.
      
      A lacuna apareceu ao ligar o app
      
      MG-2 modelou as coleções e escreveu as rules que protegem cada uma. MG-3 fechou o
      palpite, MG-5 apura, MG-6 sincroniza. Faltava o óbvio: ninguém tinha escrito quem
      escreve. Não havia como criar bolão, gerar convite, entrar por ele, sair ou trocar
      de papel. Não é falha de nenhuma story — é a costura entre elas, e apareceu na
      primeira vez que alguém tentou usar o modelo para alguma coisa. As tasks entraram
      como BE-1 a BE-4 desta story.
      
      Callable para participação, escrita direta para palpite
      
      Cada operação de participação escreve em mais de um lugar de uma vez (bolão +
      vínculo + índice) e depende de dado que o cliente não pode ver: o hash do convite,
      os jogos da fonte oficial. Rule não faz transação nem busca na internet. O palpite
      ficou de fora de propósito: é o caminho quente, e Function no meio somaria cold
      start ao momento em que todo mundo palpita junto.
      
      O índice da tela inicial virou gatilho
      
      Entrada, saída, papel, encerramento e arquivamento são cinco portas para o mesmo
      estado. Se cada callable tivesse de lembrar de atualizar o índice, a primeira que
      esquecesse sumiria com o bolão da tela inicial de alguém — sem erro, e só
      reclamando é que se descobre. A contagem de ativos é recontada, não incrementada:
      increment erra em silêncio quando o gatilho roda duas vezes, e o Firestore garante
      at-least-once.
      
      MG-7-21 mudou de alvo ao ser escrito
      
      Ele ia verificar "o último admin não sai antes de promover outro" e não há como
      montar o cenário: o criador é admin permanente, não sai, não é removido e não é
      rebaixado, então sempre sobra um. O caso ficou registrando que a guarda é rede, e
      não caminho.
      
      Integridade que o Postgres dava de graça: o calendário é global, então criar bolão
      escreve só o jogo que falta (sobrescrever apagaria o placar da sincronização e a
      apuração de outro bolão); e a entrada fecha no início do primeiro jogo recalculado
      a cada checagem, porque jogo adiado muda o horário e um valor congelado manteria a
      entrada fechada por um jogo que não vai mais acontecer.
      
      FE-1 a FE-6 e QA-2 seguem abertas: a camada de dados do app.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      9e870e7b
    • Igor Matheus's avatar
      MG-6 (parcial): sincronização agendada com lock em documento · f783a224
      Igor Matheus authored
      
      
      132 casos verdes em 11 arquivos. O adaptador da fonte real veio portado com os
      seus 13 casos, sem alterar nenhum: cota de 10 req/min, cache de 5 min, payload
      malformado, timeout, 403 e 429.
      
      A trava em processo deixou de existir
      
      O defeito de 07/08 (PP-10-11) foi resolvido no NestJS com um Map em memória. Em
      Functions isso não vale: cada invocação é um processo novo e duas podem estar em
      máquinas diferentes. A trava virou documento tomado em transação — o único jeito
      de duas máquinas concordarem sobre quem chegou primeiro — com validade, porque
      Function que morre no meio deixaria o lock preso e a sincronização pararia em
      silêncio, que é o modo de falha que esta story existe para eliminar.
      
      Meu teste afirmava o mecanismo quando o que importa é o invariante
      
      Ele exigia "uma das duas é ignorada" e falhou: com a fonte instantânea, a primeira
      termina e solta o lock antes de a segunda tentar de novo, então as duas rodam em
      sequência — e isso é legítimo, a segunda lê o estado novo e não acha nada para
      mudar. Ficaram os dois casos: o invariante (uma alteração de placar no total) e o
      mecanismo (com fonte lenta, sobreposição real, a segunda é ignorada).
      
      Acoplamento que o teste expôs: sincronizar() dependia do app global do Admin SDK.
      A instância do Firestore passou a entrar por parâmetro.
      
      Decisão registrada: a sincronização não apura. Escreve o jogo, e os gatilhos de
      MG-5 e MG-3 reagem. Se ela apurasse, existiriam dois caminhos para a mesma regra e
      um envelheceria — e a correção manual do admin, que entra pela mesma porta, teria
      comportamento diferente.
      
      BE-3 (lembrete) e BE-4 (push) seguem abertas, e com elas FE-* e OPS-*: push
      depende de app registrado no projeto real, que é MG-7.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      f783a224
    • Igor Matheus's avatar
      MG-5 (parcial): apuração e classificação materializada em Functions · 46275510
      Igor Matheus authored
      
      
      110 casos verdes em 9 arquivos. A regra de pontuação e a de classificação foram
      PORTADAS do NestJS, não reescritas — as duas já eram funções puras, com 33 casos
      cobrindo faixa de saldo, critério exibido e posição compartilhada, e vieram com os
      mesmos IDs (PR-1-*, PR-5-*).
      
      Reescrever "melhor" durante a migração seria trocar regra verificada por regra
      nova sem ninguém pedir, e a forma de descobrir o erro seria o primeiro bolão real.
      Com o porte, caso que muda de resultado é defeito de migração.
      
      Preservadas com o comentário que explica por quê: empate sem faixa de saldo (1x1
      num 4x4 vale 3), critério de desempate olhado para os dois lados (quem está na
      frente também precisa da explicação) e campeão precisa ter pontuado.
      
      Um defeito no produto e um no teste, na mesma falha
      
      - Produto: o gatilho dispara em toda escrita do jogo, inclusive o sourceUpdatedAt
        que a sincronização grava sem mudança — e reapurava o bolão inteiro a cada uma.
        Não errava o resultado; jogava leitura e escrita fora, no serviço em que custo é
        por operação e o free tier é requisito. Agora compara resultado antes/depois e
        sai fora se nada mudou, o que também é a proteção de at-least-once.
      - Teste: minha condição de espera ("existem 4 linhas de classificação") já era
        satisfeita pela passada anterior à apuração, então o teste lia zero e acusava o
        produto. Segunda vez nesta migração que espera mal escolhida faz teste de
        integração parecer defeito de regra.
      
      Decisão registrada: classificação recalculada do zero, não por delta. Mais
      escrita, e mantém o agregado derivável — delta acumulado erra em silêncio até
      alguém comparar com o extrato.
      
      BE-4 (extrato), BE-5 (correção manual) e BE-6 (aviso consolidado) seguem abertas:
      dependem de rota chamável pelo app, assunto de MG-7.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      46275510
    • Igor Matheus's avatar
      MG-4 (servidor): Auth com perfil por gatilho, e duas garantias que o Firebase não dá · db275af7
      Igor Matheus authored
      Perfil criado por gatilho do Auth, não pelo app: se dependesse do app, uma falha
      de rede logo depois do cadastro deixaria a pessoa sem nome no ranking. Conta sem
      displayName cai para o trecho antes do @ — gravar vazio deixaria uma linha de
      ranking que ninguém identifica.
      
      Sondei o Auth em vez de assumir, e o resultado muda o que falta fazer
      
      - 🔴
      
       O Auth DISTINGUE senha errada de e-mail inexistente
        (auth/wrong-password vs auth/user-not-found). Isso quebra AC-2-02, que existe
        para o login não virar oráculo de enumeração: com respostas diferentes dá para
        descobrir quem tem conta testando e-mails. A correção é configuração (email
        enumeration protection) e NÃO é verificável em emulador — o emulador não
        implementa. Virou a primeira coisa da lista de MG-8 e pergunta aberta no PRD.
      - O mínimo de senha do Auth é 6, não 8. O app valida 8 antes de chamar, mas quem
        falar direto com a API cria com 6. Fechar exige Identity Platform ou blocking
        function — upgrade de plano, decisão do dono. Registrado com o número real.
      - Rate limit não é verificável aqui: o emulador não aplica. AC-2-03/AC-2-04
        passam a depender do comportamento em produção, para ser medido.
      
      Dois defeitos meus, nos testes
      
      - Teste que dependia de outro teste: MG-4-04 usava a conta de MG-4-01 e passava
        só com a suíte inteira, na ordem. Rodado isolado passava por engano — e
        isolado é justamente como se investiga falha. Virou auto-contido.
      - Asserção por mensagem em inglês (/already exists/ contra "already in use").
        Trocada pelo código do erro: texto do fornecedor muda sem avisar, código não.
      
      FE-1 a FE-3 e BE-3 seguem abertas: trocar o login para o SDK mexe na camada de
      dados do app (assunto de MG-7) e o e-mail de recuperação exige o projeto real.
      Marcar agora seria dizer que o participante já entra pelo Firebase.
      
      Premissa registrada: AC-4 volta do cancelamento, porque o Auth dá recuperação de
      senha de graça. Se a decisão for o contrário, é texto de tela e estado de story.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      db275af7
    • Igor Matheus's avatar
      MG-3: sigilo do palpite e prazo aplicados pelo banco · dd4ed868
      Igor Matheus authored
      
      
      34 casos verdes (26 de security rule, 8 de integração com a Function), suíte de 66.
      São as duas regras que, quebradas, corrompem o bolão — e as duas deixaram de ser
      código de aplicação.
      
      Descoberta que muda o app: rules não são filtros
      
      MG-3-07 falhou com "Property userId is undefined on object. for 'list'", que
      parecia bug de rule e não era. Numa consulta, a rule é avaliada contra a consulta:
      o `where` precisa provar o acesso. Filtrar palpites só por matchId deixa userId e
      matchFinished indefinidos para o avaliador, e ele recusa — mesmo que todos os
      documentos devolvidos fossem legíveis.
      
      Consequência concreta para MG-7: a tela de "palpites de todos" precisa consultar
      matchId == X E matchFinished == true. O caso ficou com as duas metades (sem o
      filtro falha, com ele passa) para a exigência não se perder.
      
      O risco do sigilo mudou de lugar, e os testes seguem ele
      
      No NestJS era filtro em consulta: código certo, regra válida sempre. Aqui é um
      campo que alguém precisa virar, e esse alguém é uma Function. Então os casos
      testam a transição: o apito não libera, FINISHED sem placar não libera, e
      MG-3-30 verifica a Function não rodando — palpite fica secreto a mais, nunca
      exposto antes. O inverso não tem como acontecer: só a Function escreve o campo.
      
      Defeito no teste, não no produto: os dois casos negativos falharam por timeout do
      Jest (esperar 5s com limite de 5s). Caso negativo com orçamento de tempo errado
      parece defeito de regra, e o reflexo de mexer na regra é forte.
      
      Limite registrado: o índice de grupo em palpites.matchId está declarado e não
      verificado — o emulador não exige índice e a Function de sigilo depende dele.
      Verificação obrigatória em MG-8.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      dd4ed868
    • Igor Matheus's avatar
      MG-2: modelo no Firestore e participação aplicada pelo banco · d2e25ef9
      Igor Matheus authored
      
      
      28 casos de security rule verdes. A autorização deixa de ser código de aplicação
      e passa a ser regra do banco: quem não participa não lê, quem saiu perde o acesso
      na hora, participante não se promove, e `createdBy` é imutável — que é o que
      sustenta "criador é admin permanente" (CC-8) por um caminho que nenhum teste de
      CC-8 cobria.
      
      O schema vive em functions/src/modelo.ts: o Firestore não tem schema, então esse
      arquivo é o contrato que as rules assumem. Rule que consulta `role` e documento
      que grava `papel` é furo de autorização silencioso.
      
      Duas correções de desenho, as duas achadas por teste que falhou
      
      - "Meus bolões" não pode ser consulta de grupo em `membros`. Falhou duas vezes e
        cada falha ensinou: primeiro que consulta de grupo não é avaliada pelo match
        aninhado (caiu no deny genérico), depois que no `list` o {uid} do caminho não é
        vinculado e a rule não inspeciona o `where` — não há como provar que a pessoa
        pediu só o que é dela. Virou índice sob o próprio usuário, com o resumo dentro:
        uma leitura, rule sem get(). O spike foi corrigido.
      - O displayName mudou de lugar por causa do sigilo: ler o perfil de cada
        participante seria leitura extra e furaria a regra de não ler perfil alheio.
      
      18 dos 28 casos verificam recusa, de propósito: no NestJS negar era uma linha de
      service com e2e; aqui é a ausência de uma cláusula em rule — e ausência não
      aparece lendo o arquivo, só falhando um teste.
      
      Limite registrado: o emulador não exige índice composto, então consulta que
      precisaria de um passa aqui e falha em produção. Entra como verificação
      obrigatória de MG-8 contra o projeto real.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      d2e25ef9
    • Igor Matheus's avatar
      Blaze ativo: deploy de fumaça, região corrigida e limpeza de imagens · 03fa1292
      Igor Matheus authored
      
      
      O dono habilitou o Blaze e autorizou publicar a função trivial `saude` da MG-1
      para exercitar o caminho inteiro antes de a MG-6 depender dele. Funcionou — o que
      prova o Blaze, já que Functions não sobem no Spark — e respondeu 200 em 0,5s.
      
      O deploy pegou dois problemas que nenhum teste local pegaria:
      
      - A Function foi para us-central1 com o Firestore em southamerica-east1. É o
        padrão do Firebase e ninguém notaria: no emulador tudo é localhost. Em
        produção, cada leitura da apuração e da sincronização cruzaria o continente,
        nas duas operações que fazem mais leituras. Fixada a região em
        setGlobalOptions; a função de Iowa foi removida (confirma 404).
      - Nenhuma política de limpeza no Artifact Registry: cada deploy acumula uma
        imagem, para sempre. Fatura crescendo em silêncio é o oposto da restrição de
        custo. Configurada limpeza de imagens com mais de 3 dias, nas duas regiões.
      
      Também ligados delete protection e point-in-time recovery no Firestore (retenção
      de 1h para 7 dias), com o banco ainda vazio — o momento mais barato.
      
      Custo residual honesto: centavos de dólar por mês de armazenamento das imagens
      dentro da janela de 3 dias. Não é zero.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      03fa1292
    • Igor Matheus's avatar
      Projeto definitivo: bolao-raro-4382d, Firestore em São Paulo · 913ee142
      Igor Matheus authored
      
      
      O dono trocou a conta do Firebase (igorengcomp12@gmail.com) e criou o projeto
      definitivo. O bolao-raro-ad0fc da conta corporativa foi abandonado antes de
      receber qualquer dado.
      
      Estado verificado: Firestore já provisionado em modo nativo na região
      southamerica-east1 — irreversível, e é a certa para um bolão do Brasileirão,
      onde o momento crítico é o minuto antes do apito.
      
      Registrado no spike o que precisa de atenção antes de existir dado que importe:
      delete protection e point-in-time recovery vêm desligados, e o projeto está numa
      conta pessoal em vez da corporativa — titularidade fica com a pessoa, não com a
      empresa.
      
      O default do .firebaserc continua demo-bolao; o real entra como alias `raro`.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      913ee142
    • Igor Matheus's avatar
      MG-1: projeto real da Raro como alias, nunca como default · a85f4448
      Igor Matheus authored
      
      
      bolao-raro-ad0fc entra no .firebaserc como alias `raro`. O `default` continua
      sendo demo-bolao, para comando esquecido sem --project cair no emulador em vez
      de produção.
      
      Estado verificado do projeto real: nenhum app registrado, Firestore não
      provisionado (API desabilitada), Auth sem configuração. Projeto limpo — e
      provisionar o Firestore exige escolher região, que é irreversível, então não foi
      feito sem confirmação.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      a85f4448
    • Igor Matheus's avatar
      MG-1: Firebase com emuladores rodando local, sem custo · b965f25a
      Igor Matheus authored
      
      
      bolao-firebase/ com firebase-tools 15.26.0 pinado como devDependency — global
      não é reprodutível para quem clona. Firestore, Auth e Functions em emulador,
      portas fixas, e 4 testes de security rule provando que as rules começam
      fechadas.
      
      Duas travas de propósito
      - Projeto `demo-bolao`: o Firebase trata prefixo `demo-` como demonstração e o
        SDK nunca fala com serviço real. Apontar para projeto de verdade exige
        PERMITIR_PROJETO_REAL=true.
      - Rules `if false` na largada. Participação entra em MG-2, sigilo e prazo em
        MG-3. Abrir primeiro e fechar depois é como vazamento chega em produção.
      
      Dois defeitos encontrados no próprio script, os dois no caminho de erro
      - `command -v java` mente no macOS: existe um stub em /usr/bin/java que satisfaz
        a checagem e falha ao executar. O teste passou a ser rodar `java -version` —
        presença de binário não é funcionamento.
      - Sem JDK algum, o script morria com "unbound variable" (array vazio sob set -u)
        em vez de mostrar a instrução. Foi para poder testar esse caminho que a lista
        de candidatos virou JDK_CANDIDATOS sobrescrevível.
      
      Confirmado: nada aqui exigiu plano Blaze. O cartão só entra no deploy.
      
      MG-1 fechada, 8/8 tasks, plano e execução em docs/testes/MG-1-*.md.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      b965f25a
    • Igor Matheus's avatar
      Fase 2: arquitetura e backlog da migração para Firebase · d018cf55
      Igor Matheus authored
      
      
      Decisão do dono em 07/08: Firestore + Cloud Functions, migrando antes de
      desligar — as duas implementações coexistem até os 234 casos e2e passarem na
      nova.
      
      Spike de arquitetura (docs/spikes/backend-firebase.md)
      - Dimensiona o que sai: 38 rotas, 17 services, 12 tabelas, 3 jobs, 5.139 linhas.
        O caro não é o código, são os 234 e2e que provam que prazo, sigilo e apuração
        valem.
      - Resolve o problema central: security rules não filtram resultado de query,
        então o sigilo do palpite passa a ser flag desnormalizada virada por Function
        no fim do jogo. O risco muda de "a consulta vaza" para "a Function não rodou".
      - Descoberta que muda o sequenciamento: o Emulator Suite roda local sem cartão,
        então MG-1 a MG-8 não dependem de decisão financeira. Blaze só para publicar.
      - Mapa das 12 tabelas para coleções, com o ranking materializado — somar pontos
        na leitura estouraria o free tier.
      
      PRD e 9 stories (73 tasks), na ordem do risco e não da facilidade: emuladores,
      modelo e rules, sigilo e prazo, Auth com recuperação de senha, apuração,
      jobs no Scheduler, app, paridade verificada, e só então o corte do NestJS.
      
      Efeitos colaterais registrados: MG-4 faz AC-4 voltar do cancelamento (o Auth dá
      recuperação de senha de graça) e MG-6 fecha PP-10 OPS-1 e as 4 tasks de PP-8
      presas ao FCM.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      d018cf55
    • Igor Matheus's avatar
      README da raiz: o que é o projeto, e as regras que não se negocia · 938ee639
      Igor Matheus authored
      
      
      O arquivo era o template do GitLab explicando como subir arquivos. Trocado por
      uma porta de entrada real: as três partes do monorepo, as seis regras de domínio
      que quebram a competição se forem violadas (prazo pelo servidor, sigilo até o fim
      do jogo, empate sem faixa de saldo, campeão nunca congelado, saída definitiva,
      tudo adiado) e onde ler o processo.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      938ee639
    • Igor Matheus's avatar
      Bolão: backend NestJS, app Flutter e o processo que os produziu · e51807ea
      Igor Matheus authored
      
      
      Produto completo de bolão de futebol, construído do PRD ao device: 35 das 38
      stories fechadas, 312 de 318 tasks, três PRDs concluídos (Identificação e
      acesso, Criação e convite, Ciclo de vida do bolão).
      
      Backend (NestJS 11 + Prisma 7 + Postgres)
      - Palpite com prazo decidido pelo relógio do servidor, sigilo até o fim do jogo,
        apuração 10/5/3 lida da configuração do bolão, ranking com desempate por
        critério configurável, extrato explicando cada ponto.
      - Ciclo de vida: encerramento normal e antecipado, campeão derivado na leitura
        (nunca congelado), arquivamento reversível, recriação de temporada herdando
        regras e galera.
      - Fonte oficial de calendário e placar via porta `FonteDeDados`: adaptador falso
        para desenvolvimento e `FootballDataAdapter` (football-data.org v4) para
        produção. O limite de 10 req/min desenhou o adaptador — uma requisição por
        temporada, cache de 5 min, cota recusada antes de ser gasta.
      - 71 testes de unidade e 216 e2e, em banco próprio (`bolao_test`).
      
      App (Flutter + Riverpod sem codegen)
      - Palpite offline com rascunho local e fila de envio, contagem de prazo pelo
        horário do servidor, ranking e extrato, convite por link e por código,
        administração de participantes.
      - 235 testes de widget e unidade; identidade visual da Raro (#615FF6) com os
        contrastes medidos e travados por teste.
      
      Documentação (docs/)
      - 5 PRDs, 38 stories com tasks por disciplina, 27 planos de caso de teste com
        execução registrada, e `CONTROLE.md` como fonte única de progresso —
        verificado por `docs/validar.sh`.
      
      As 6 tasks abertas dependem de credencial do FCM e de hospedagem que não
      hiberne, não de código.
      
      Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      e51807ea
    • Igor Matheus's avatar
      Initial commit · 5a0abfed
      Igor Matheus authored
      5a0abfed