Pular para o conteúdo principal

Os gates, e como eles são medidos

Um gate é um ponto do fluxo em que o trabalho para até que alguém — pessoa, agente ou script — diga que ele pode seguir. O produto tem treze, e até a FASE 15a nenhum deles existia como lista: estavam espalhados por regras puras, casos de uso, testes e workflows de CI. Responder "quais gates existem" exigia ler seis arquivos.

docs/gates.yml é essa lista. Este documento explica o que ela é e — mais importante — o que ela não é.

O registro descreve; ele não executa​

Nenhum gate passa a ser cobrado por causa do arquivo. Mudar severidade nele não muda comportamento nenhum: quem bloqueia um merge continua sendo o decide.ts, quem julga uma PR continua sendo o agente de QA, quem reprova um backmerge continua sendo o workflow de CI.

É a suposição mais fácil de errar, então vale repetir: o registro é um índice, não política. A política de branches continua morando em branching-policy.md; os campos entrada e entregavel são uma frase em português, e onde aponta para o código que governa de verdade.

O que o registro compra é outra coisa: tornar os gates enumeráveis. A lição das Fases 10 e 13 — métrica extraída por script, nunca anotada à mão — não se aplica ao que não se consegue listar.

Os dois campos que envelhecem se ficarem indefinidos​

verificacao responde como se prova que este gate passou, não quem o julga. script quer dizer que há um artefato mecânico — um evento, um teste, um job — que serve de prova. É por isso que qa-verificada é script mesmo com um LLM emitindo o veredito: o artefato é o pr.gate_changed com veredito. Quem julga mora em dono.

aprovacao_humana: true quer dizer que a decisão é do usuário — diretamente, por um clique, ou delegada por uma política que o próprio usuário escreveu. É isso que deixa acao-aprovada conviver com status: auto_approved: a política decidindo sozinha é o usuário tendo decidido antes. merge-protegida é o caso em que nem a delegação existe, porque o teto rebaixa auto_approve para require_approval mesmo com a autonomia ligada.

Nos quatro gates que a constituição declara manuais, esse campo é invariante e travado por teste (RN-071).

Três formas de evidência, porque nem toda prova mora no log​

tipoquandoo que o localizador traz
event_logo desfecho vira evento de domíniotipos de evento + filtro de payload
testea garantia é uma asserçãocaminho do spec
cio gate mora no repositórioworkflow + spec

Os dois casos que obrigaram o campo a existir:

  • merge-protegida não emite evento próprio. É um teto aplicado por último sobre o veredito já calculado, que rebaixa auto_approve para require_approval; o desfecho aparece como um proposed_action.created pendente, indistinguível de qualquer outra pendência. O que garante o bloqueio é o teste.
  • backmerge mora fora da aplicação: é um check obrigatório, com estado versionado em .release/gate.json na main.

Sem evidencia, a regra "gate block sem prova reprova" deixaria as duas travas mais duras do produto vermelhas para sempre.

Duas validações, e só uma delas roda em produção​

O registro é validado em duas camadas, e a divisão não é cosmética:

camadao que afirmaquem a roda
validarRegistroafirmações sobre o conteúdo — block exige script, os quatro gates humanos continuam humanos, active exige evidência, nenhum id duplicado, nenhuma entrada órfãquem lê o registro, inclusive a api em runtime
validarLocalizadoreso arquivo-alvo de teste/ci existeo teste contra o arquivo real e a fase 2 do validacao-gates.ts — nunca a api

A primeira vale onde quer que o registro seja lido. A segunda é uma afirmação sobre o repositório, e só faz sentido dentro de um checkout.

As duas já foram uma função só, e o loader da api a chamava. A consequência foi medida na v6.1.0 instalada: GET /gates respondia 500 com RegistroDeGatesInvalido listando os onze alvos como "não existe" — doze respostas 5xx em cerca de 55 minutos. Não havia nada de errado com o registro. A imagem de produção leva /app/docs/gates.yml e nada mais de docs/; apps/api/test/, scripts/ci/ e .github/ nunca entram nela, então uma afirmação sobre o repositório avaliada contra essa árvore reprova todo gate, em toda instalação.

Nada foi afrouxado. A regra continua existindo, continua cobrindo os mesmos alvos, e um alvo que some continua reprovando — ela só roda onde a pergunta tem sentido. Duas guardas mantêm isso: o teste que sobe até a raiz do repositório e confere todo localizador, e um teste do loader que reconstrói em disco a árvore da imagem (uma raiz com docs/gates.yml e nada mais) e afirma que o registro carrega. Devolver a checagem ao loader deixa o segundo vermelho com a mensagem exata que a instalação produziu.

E a api servindo o registro agora é exercitada contra a imagem de produção: docker/smoke.sh chama GET /gates e GET /internal/gates (service token) e exige o registro de volta nas duas. A checagem do corpo é uma função de shell, checar_registro_de_gates, que scripts/ci/smoke-gates.spec.ts roda contra os três corpos sem subir a stack. Nenhuma suíte de unidade poderia ter pego a falha original — vitest e ExUnit rodam de um checkout, onde os alvos existem.

A armadilha do tipo compartilhado​

qa-verificada e secops-segura não são dois tipos de evento: os dois gravam pr.gate_changed, discriminados por payload.gate. Pior — o mesmo tipo é gravado quando o gate abre, sem veredito.

Um registro que guardasse só o nome do tipo contaria abertura como passagem e mediria o dobro do que de fato aconteceu. Daí o filtro, e daí a sentinela veredito: presente. O mesmo vale para os dois gates de PR de infra, que compartilham infra.gate_changed.

O teste afirma que nenhum par (event_types + filtro) se repete no registro. É essa unicidade que faz a medição significar alguma coisa.

O filtro só alcança o payload, de propósito: aceitar uma coluna arbitrária no YAML abriria a consulta inteira. Quem promoveu uma história — uma pessoa ou o PO — mora na coluna actor_kind e fica fora do vocabulário declarativo.

Medindo​

pnpm --filter api validacao:gates # full report
pnpm --filter api validacao:gates -- --sem-banco # registry and locators only
pnpm --filter api validacao:gates -- --projeto <uuid>

Três fases, nesta ordem: registro (carrega e valida), localizadores (o alvo de teste/ci existe?) e event log (última passagem, com o id do evento). As duas primeiras nunca tocam o banco — é isso que torna o script útil em CI sem Postgres.

A fase 2 relata todo localizador do registro, não só os dos gates active+block. Ela já teve um laço próprio, mais estreito, enquanto o loader da api tinha um mais largo, e rag-acertivo — active, warn, citando um workflow e um teste — nunca aparecia na tabela. Agora é uma regra só, validarLocalizadores, compartilhada com o teste.

código de saídaquando
0registro válido e localizadores no lugar
1invariante violado, alvo ausente, ou gate sem passagem quando --projeto foi passado
2uso inválido

A assimetria é deliberada. Registro e localizadores são afirmações sobre o repositório: valem sempre. A evidência do event log é uma afirmação sobre uma execução, e só existe se uma aconteceu — sem --projeto, a fase 3 é só um relatório. Exigir passagem num banco recém-criado faria o script sair 1 sempre, e script que sempre reprova vira ruído que ninguém lê.

O que enumerar já pagou​

Antes de medir qualquer coisa, escrever a lista achou duas coisas que ninguém estava vendo:

  • o julgamento de QA e SecOps sobre PRs de infra, que tem caminho próprio (infra.gate_changed, sem tarefa de backlog por trás) e não estava na spec;
  • promotion-check era um check obrigatório sem spec próprio, ao contrário de pr-police e approval-ladder. A evidência apontava para o script, com a lacuna escrita ali mesmo, e o item foi para a triagem — a fase declara e mede, não corrige. A triagem o fechou: scripts/ci/promotion-check.spec.ts existe, e a evidência do gate agora aponta para ele. É o ciclo funcionando como foi desenhado — enumerar achou, a triagem priorizou, outra fase corrigiu.

O terceiro achado veio da primeira execução do medidor: o filtro de story-promovida apontava para actor_kind, que é coluna e não payload, e o gate aparecia como "nunca passou" num banco onde tinha passado horas antes. Registro que ninguém roda é registro que mente.

Ativar um gate planned é o mesmo trabalho que qualquer outro​

implementavel (dono dev-lead) nasceu planned na FASE 14d, junto com o resto do organograma-alvo que docs/gates.yml/docs/fluxo.yml já descreviam sem código por trás. O ADR 0090 o ativou — e o exercício vale como exemplo porque nada na forma do registro mudou: status: planned → active mais o bloco evidencia (apontando para proposed_action.created filtrado por actionType: assess_implementability) são as ÚNICAS duas linhas que um gate warn precisa ganhar para deixar de ser aspiração. severidade continua warn — o comentário no arquivo já dizia "nasce warn mesmo quando ativado", e ativar não é a mesma decisão que promover a block (isso exigiria medir passagens reais antes, a mesma disciplina da FASE 15a).

O trabalho de verdade fica fora do arquivo: o Dev Lead precisou de uma ferramenta nova (assess_implementability) que propõe a avaliação como proposed_action, e QA-estratégia — até então um papel proposto em docs/fluxo.yml, com o critério de separação já escrito ali ("pode ser o próprio qa-lead num segundo momento") — precisou existir para alimentar a avaliação com um plano de teste real. O registro só passou a descrever o que o código agora faz.

Essa entrada não sobreviveu ao uso real. O ADR 0192 (RN-674) moveu o plano de teste para DEPOIS da entrega do dev — escrito ANTES do dev, ele não tinha código para ler e esgotava as iterações — e a entrada do gate mudou de plano-de-teste para module_map: o Dev Lead julga a história e o mapa de módulos que ele já tem. O plano agora alimenta qa-verificada, como entrada do único veredito que aquele gate já dá. A mesma lição no outro sentido: a linha do registro mudou porque mudou QUEM ENTREGA O QUÊ A QUEM, não para fazer um gate passar.

Um registro pode envelhecer na direção errada — desatualizado, não inativo​

implementavel é o exemplo de gate que precisou de código novo para sair de planned. paralelismo-autorizado é o oposto: o mecanismo (RequestParallelizationUseCase, RN-083) está em produção desde a FASE 14d — foi o registro que ficou para trás, declarando planned sobre algo que já era active no irmão docs/fluxo.yml e no código. A auditoria fluxo.yml × código (achado A1/B5, auditoria-fluxo-vs-codigo.md) achou a divergência; a correção foram só as mesmas duas linhas que implementavel também ganhou — status: active mais evidencia apontando para proposed_action.created filtrado por actionType: parallelize.

Vale notar por que isso não mudou nada na trilha de PR do painel do time: paralelismo-autorizado é fluxo: execucao, e a tela só deriva etapas de gates fluxo: pr (ver "Consumo" abaixo) — um gate pode virar active no registro sem aparecer em lugar nenhum da interface, se o fluxo dele for outro.

Um gate pode nascer active direto — quando o mecanismo e o registro chegam juntos​

implementavel e paralelismo-autorizado são os dois exemplos de gate que já existia (declarado planned ou desatualizado) e ganhou a entrada no registro DEPOIS. necessidade-validada (RN-406, ADR 0095) é o terceiro padrão: mecanismo e registro nasceram na MESMA mudança, porque o próprio gate não existia em lugar nenhum — nem planned no arquivo, nem código por trás.

A escolha de severidade: warn segue o mesmo raciocínio de implementavel, mas por outro motivo. implementavel é warn porque promover a block exigiria medir passagens reais antes (FASE 15a). necessidade-validada é warn porque NADA no produto hoje confere a passagem dele antes de deixar o PO continuar — o handoff Criativo→PO já acontece dentro de confirm_readiness, antes mesmo de este gate existir. block prometeria uma trava que não existe; warn descreve exatamente o que o gate é: uma medição de que um humano validou o mérito da necessidade, não um gate que bloqueia o passo seguinte.

workspace-verificado (RN-423, ADR 0104) é o quarto exemplo, e o mais claro dos quatro sobre por que warn não é uma "trava fraca": o que trava de verdade AQUI não é o gate, é a recusa explícita em Engine.Actions.TerminalExecutor.decisao_de_execucao/1 — sem workspace verificado ou runner conectado, o comando é recusado incondicionalmente, no fundo do código, sem passar pelo registro. O gate existe só para dar EVIDÊNCIA de quando a confirmação aconteceu (project.workspace_verified no event log); promovê-lo a block descreveria uma trava que já existe em outro lugar, do mesmo jeito que necessidade-validada teria mentido sobre uma que não existe em lugar nenhum.

rag-acertivo (RN-490/RN-498, ADR 0132/ADR 0138) é o quinto exemplo, e o motivo de ele continuar warn mudou de forma uma vez, o que vale contar inteiro. Ele nasceu warn pelo motivo mais simples dos cinco: não havia CI que rodasse um LLM real — o golden-set que ele mede (17 perguntas compostas a partir de RNs/ADRs reais, conferidas contra um corpus real curado pelo HybridSearchUseCase de verdade) só rodava à mão (mix golden_set.rag), a mesma postura que o golden-set de QA Automation (ADR 0123) ainda tem hoje. Isso deixou de ser verdade: .github/workflows/golden-set-rag.yml agora roda mix golden_set.rag de verdade, contra um serviço Ollama real — ele só chama o modelo de embedding, em CPU, determinístico, nunca um modelo de chat fazendo julgamento. (A afirmação de que o golden-set de QA não era tratável do mesmo jeito foi medida depois, e o relógio não se mostrou o obstáculo — ver a próxima seção.) O que impediu severidade: warn de ficar desatualizada não foi o gate continuar manual — foi o workflow ser agendado (cron noturno mais workflow_dispatch), nunca disparado por pull_request. block ainda prometeria uma checagem que segura um merge, e uma execução agendada não segura nada: uma regressão fica visível em até um dia, nunca na hora da PR. O motivo passou de "nenhum CI consegue rodar isso" para "este CI não está no caminho do merge" — warn continua descrevendo exatamente o que é verdade, só que não a mesma verdade de antes.

O golden-set de QA em CI: o relógio cabe, o instrumento mede, e roda toda semana​

O golden-set do julgamento semântico do agente de QA Automation (ADR 0123) é o que daria à etapa semântica de qa-verificada um sinal de tendência. O ADR 0123 e o ADR 0138 afirmavam, sem medir, que rodá-lo em CI exigia um segredo de LLM de API ou infraestrutura nova (um runner com GPU). Em 2026-09-13 (AT-067) isso foi medido, no molde exato do golden-set-rag.yml: ubuntu-latest, sem GPU, as mesmas imagens de serviço pgvector e ollama pinadas, qwen2.5-coder:latest baixado no Ollama do próprio job (com o OLLAMA_KEEP_ALIVE=1h/OLLAMA_CONTEXT_LENGTH=16384 do compose de dev), a api e o engine de pé como servidores — o engine é a mais em relação ao job do RAG, porque é o engine, chamado pela api via POST /internal/actions/execute, que roda o npm test que o QA pede.

execuçãopull do modelopreparação até a task do mixmix golden_set.qajob inteiroplacar
3476944740515 s~3 min19 min 15 s22 min 09 s0/6
3477086942913 s~3 min22 min 53 s25 min 45 s0/6

Um turno de chat do modelo 7B na CPU desse runner levou mediana de ~20 s (10–34 s nos 23 turnos que o log da api guardou). O relógio cabe: um job noturno ou semanal de menos de meia hora, sem segredo, sem GPU. A metade "infraestrutura nova" da frase antiga é refutada pela medição.

O que não cabe é o instrumento. As duas execuções fizeram 0/6, abaixo do piso gravado de 1/6 — e não por causa do julgamento do modelo. O event log da segunda execução mostra o porquê: todo npm test que o QA propôs foi auto-aprovado (permissions.json allow: npm test) e depois recusado pelo engine, porque desde a RN-502 (ADR 0143, 2026-09-04) um projeto container sem container running não roda comando de terminal em lugar nenhum. O script de seed é de 2026-08-30 e nunca registra container. Então nenhum caso consegue ver exit 0, e emit_qa_verdict recusa approved sem ele — três dos seis vereditos esperados ficaram impossíveis. A mensagem de recusa diz "suba o container do projeto (a Infra propõe container_start)", e o modelo fez o que leu: em três casos pediu ao terminal container_start ou docker-compose up (ficou pending, logo awaiting_approval), e em um repetiu npm test sessenta vezes até toolloop.limit_reached (blocked(modelo)). Os dois restantes travaram na primeira chamada com command vazio — a falta de confiabilidade que o ADR 0123 já registrava para esse modelo, independente da RN-502. Nada aqui é específico de CI: a mesma recusa vale para uma execução local.

É por isso que nenhum golden-set-qa.yml foi commitado. Um workflow agendado que fica vermelho em toda execução por um motivo alheio ao que ele mede é pior que nenhum — vermelho de fundo ensina as pessoas a ignorar o vermelho. O desenho que foi medido fica no histórico do repositório (o workflow como estava no commit e7d7d1b16, incluindo um passo de diagnóstico temporário). Fazê-lo medir de novo significava decidir como o golden-set roda a suíte sob a RN-502, e essa era uma decisão para um humano. Afrouxar a recusa para o harness passar nunca foi uma das opções: enfraqueceria o isolamento para o qual a RN-502 existe, para fazer um teste passar.

A decisão, e a segunda medição (AT-076)​

Em 2026-09-27 o mantenedor escolheu o primeiro caminho: o seed sobe um container real pelo broker e o registra running, pelo caminho de produção. A RN-502 fica intacta; o que mudou é que o seed agora satisfaz a pré-condição dela. Para cada caso, depois de clonar o esqueleto na pasta gerenciada do projeto, apps/api/scripts/golden-set-qa-container.ts percorre os mesmos casos de uso que um projeto real percorre, numa sessão consultiva própria: CreateModuleMapUseCase (um módulo), RouteModulesToInfraUseCase (uma imagem candidata), ProposeActionUseCase para container_start com ator agent/infra, e ApproveActionUseCase como dono do projeto. A aprovação roda ExecuteContainerStartUseCase, que grava a eleição como um artifact.project_image novo por DecidirImagemDoProjetoUseCase (então validarDecisaoDeImagem julga a imagem), chama ContainerBrokerPort.start e registra provisioning → running pela máquina de estados. O seed propõe e aprova em vez de chamar o caso de uso de execução direto porque duas regras moram do lado da proposta — o 409 nomeado sem_broker_na_instalacao (RN-591) e a política do decide() para container_start — e um harness que as pulasse deixaria de seguir a produção no dia em que qualquer uma mudasse.

A imagem é node:24.11.1-bookworm-slim, pinada pelo digest do índice multiplataforma no seed (a regra do ADR 0159, embora esse arquivo fique fora das árvores que o imagens-pinadas.ts varre). Debian e não Alpine porque o broker mantém o container vivo com sleep infinity. A geometria que faz os três lados verem o mesmo código: o worktree é a pasta gerenciada do projeto (projectScopeRoot), o broker monta <PROJECT_WORKSPACES_HOST_ROOT>/<workspace_dir_name> em /work, e o engine traduz o cwd do QA para /work antes do exec. No job de CI a api, o engine e o broker rodam todos nativos no runner, então PROJECT_WORKSPACES_ROOT (api, engine) e PROJECT_WORKSPACES_HOST_ROOT (broker) são o mesmo caminho; o broker é o único processo que fala com o Docker.

Um caso cujo container não sobe agora reprova o seed com ContainerDoGoldenSetNaoSubiuError, nomeando o passo e o motivo que o produto deu, e o módulo ExUnit reprova quando a api está de pé mas o seed falha. Antes, ele devolvia {:skip, _} do corpo do teste — o que o ExUnit não trata como skip: o teste passava, verde, sem ter medido nada.

Medido com um workflow temporário disparado por push na branch (removido antes da PR, como a AT-067 fez):

execuçãopull do modelopull da imagemcontainer de pé (proposta → executada)mix golden_set.qajob inteiroplacar
3629144010815 s6 s0,20–0,28 s por caso48 min 29 s52 min 38 s3/6
3629403729715 s6 s0,22–0,30 s por caso83 min 16 s86 min 16 s3/6

Contra os três critérios, nas duas execuções: o event log tem zero recusas da RN-502 (nenhuma ação de terminal e nenhum evento de sessão a menciona); npm test rodou dentro do container com exit 0 em cinco dos seis casos na primeira execução e em todos os seis na segunda (as lacunas são commands vazios que o broker recusou nomeando — a falta de confiabilidade do modelo que o ADR 0123 já registrava); e o placar é relatado como veio, 3/6 duas vezes, contra o piso de 1/6.

casoesperado3629144010836294037297
rf-coveredapproved✓ approved✓ approved
rf-uncoveredchanges_requested✓ changes_requested✓ changes_requested
rf-single-cleanapproved✗ changes_requested✗ blocked(modelo)
rf-mismatched-filenameapproved✗ changes_requested✓ approved
rf-partial-coveragechanges_requested✗ blocked(modelo)✗ blocked(modelo)
rf-skipped-testchanges_requested✓ changes_requested✗ approved

Mesmo total, casos diferentes: os erros agora são o julgamento do modelo e a variância dele, que é o que o golden-set existe para medir — rf-skipped-test aprovou uma vez, contando um test.skip como cobertura, e rf-single-clean rodou npm test 30 vezes na segunda execução antes de terminar blocked(modelo).

As execuções ficaram mais longas (48 e 83 min contra 19–23 min) porque os casos agora fazem o trabalho: leem arquivos e raciocinam sobre cobertura em vez de parar na recusa, e um caso que se repete agora repete execuções reais. Um job agendado precisa de timeout-minutes acima dos 86 min vistos aqui. Duas coisas foram vistas e não mudadas aqui: a chamada api→broker tem teto de 5 s para toda operação, start incluído, então um container_start cuja imagem ainda não está no daemon estouraria o tempo enquanto o pull continua (o job baixa a imagem antes; sem isso não foi medido); e o heartbeat do engine fechou as sessões ociosas (heartbeat_timeout) cerca de 30 s depois do seed, enquanto o QA seguia propondo e rodando comandos nelas (o QA roda no processo do mix, fora de qualquer processo de sessão), sem efeito visível na rodada.

Agendado, semanal (AT-149)​

Em 2026-09-27 o mantenedor escolheu a cadência: semanal (ADR 0168). .github/workflows/golden-set-qa.yml é o desenho da AT-076 tornado permanente — api, engine e broker nativos no runner, só o broker falando com o Docker, a mesma pasta gerenciada nos três lados — em schedule mais workflow_dispatch, nunca pull_request, a mesma postura do golden-set-rag.yml.

Semanal e não noturno como o job do RAG, por dois motivos medidos. A execução do RAG custa ~20 minutos e é determinística, então a queda de ontem é uma queda real. Esta custa de 52 a 86 minutos e varia: as duas execuções da AT-076 chegaram aos mesmos 3/6 por casos diferentes. Uma execução noturna gastaria horas de runner por semana para ler a mesma tendência com mais ruído; uma execução por semana é o que o piso catraca precisa para mostrar uma queda, e workflow_dispatch responde na hora depois de uma mudança no prompt do QA ou no seed. Ela roda no sábado à 01:17 UTC: longe das 06:00 diárias do RAG, das 04:00 de domingo das provas de propriedade e das 06:00 de segunda da checagem de links externos; no sábado para que uma execução vermelha espere a semana começar; no minuto 17 porque o Actions atrasa, e pode descartar, agendamentos na hora cheia. timeout-minutes é 150: os 86 min medidos mais folga para uma execução em que mais de um caso bate no teto de iterações. Isso é a variância do modelo, e o job não mede o relógio.

O veredito é o do próprio teste: abaixo do passRate que o floor.json grava para o modelo, o ExUnit reprova. O workflow só acrescenta duas guardas de instrumento — a linha do placar precisa existir (o teste pula quando a api está fora, e um skip numa execução sem supervisão é um verde que não mediu nada) e o event log não pode ter recusa da RN-502 (a assinatura da AT-067) — e, na falha, imprime os logs, como o job do RAG faz. A imagem a pré-baixar é lida da própria constante do seed (IMAGEM_DO_GOLDEN_SET_QA), não de um segundo literal: o docker run do broker roda sob o teto de controle de 30 s da porta do Docker (RN-604), e um job que baixasse uma imagem enquanto o seed pedisse outra poria o pull dentro desse teto.

A execução de prova, com um gatilho push temporário na branch, removido antes da PR:

execuçãopull do modelopull da imagemmix golden_set.qajob inteiroplacar
3632728177415 s5 s10 min 13 s13 min 41 s3/6

Zero recusas da RN-502 no event log, os seis containers running antes do primeiro caso. Os mesmos 3/6 mais uma vez, por mais um conjunto diferente de casos — rf-covered, rf-uncovered e rf-skipped-test acertaram; rf-single-clean e rf-mismatched-filename receberam changes_requested onde se esperava approved, e rf-partial-coverage foi aprovado com cobertura parcial. A execução levou um sexto das da AT-076 (10 contra 48–83 minutos de mix golden_set.qa): nenhum caso desta vez se repetiu até o teto de iterações. O timeout continua dimensionado pela execução mais lenta medida, não pela mais rápida.

Um gate pode faltar no registro enquanto o fluxo.yml já o atribui​

deployavel é o quarto padrão, e o menos lisonjeiro: ele estava declarado como gate_saida da area-infra em docs/fluxo.yml e estava ausente de docs/gates.yml por completo — nem planned, nem desatualizado, simplesmente não estava lá. O modelo de time tem três peças (catálogo, fluxo, registro) e nada ainda as cruza, então um gate pode ser entregável numa e não existir em outra. Este é o segundo achado dessa forma; o primeiro foi paralelismo-autorizado envelhecendo na direção oposta.

Ele entra como planned/warn, sem bloco evidencia e sem script de verificação, porque a RN-070/ RN-071 proíbem block sem um — e porque o que o bloqueia não é código. A infraestrutura existe (overlays, imagens no GHCR por digest, scripts de smoke/rollout/restore); o que falta é um ambiente de produção com tráfego real e alguém que o mantenha. Ver o ADR 0153, que também faz dez documentos pararem de citar DEPLOY_ENABLED como se fosse uma variável que existe no código.

Consumo: a tela deriva, não repete​

A trilha de PR no painel — Dev → QA → SecOps → Você — era uma lista escrita dentro do componente. Desde a FASE 15b ela vem do registro, via GET /gates.

O que muda na prática é uma propriedade, não a aparência: um gate que sai do registro sai da tela sozinho. Antes, desativar o SecOps deixava uma etapa morta na trilha até alguém lembrar de editar o código — exatamente o tipo de envelhecimento que motivou o registro a existir.

Três decisões que valem explicar:

A rota é separada da interna. /internal/gates é de serviço para serviço, com service token, e serve o script de medição; /gates é do usuário logado e serve a tela. Mesmo registro, públicos diferentes.

Sem projectId. O registro é um fato sobre o PRODUTO: os mesmos gates valem para todo projeto. Atrelá-lo a um projeto sugeriria que é possível ter gates diferentes por projeto, que é exatamente o que o ADR 0054 deixou em aberto de propósito.

Só gates active. Um gate planned descreve um papel futuro (dev-lead, platform). Numa tela que diz o que está acontecendo agora, ele apareceria como se estivesse acontecendo.

O que a tela ainda decide sozinha​

O rótulo de cada etapa (QA, Você) e qual etapa cada gate representa ficam no componente. Não é incoerência: o registro descreve política, e como chamar as coisas para o usuário é decisão de tela. O que saiu dela foi a LISTA — quais etapas existem.

Um gate de PR que o registro traz e que a tela ainda não sabe desenhar é ignorado, com um teste afirmando isso: uma etapa undefined no meio da trilha seria pior que a ausência dela.

Referências​