Skip to content

Diário — 26/09/2026 ​

Presentes: Luiza (@LuizaMaluf)

Pauta: issue #1 — subir o Postgres com docker compose.


O que foi feito ​

docker-compose.yml com PostgreSQL 16.15 e pgAdmin 9.18.0. Volume nomeado pgdata, healthcheck com pg_isready, e o pgAdmin só sobe depois de o banco estar healthy. Teste de fumaça em tests/infra/test_compose.sh cobre os critérios de aceite 1 e 2: sobe, grava uma linha, faz down + up e confere que a linha continua lá.

O que foi medido ​

MedidaValorCondição
Teste de fumaça completo, primeira vez (inclui download das imagens)44,6 sMac M-series, Docker Desktop 29.6.2
Teste de fumaça completo, imagens em cache (2 ciclos de up)8,5 sidem
up --wait → db healthy, volume vazio6,4 sidem
Tamanho da imagem postgres:16.15663 MB
Tamanho da imagem adminer:5.4.2 (primeira versão, substituída)165 MB
Tamanho da imagem dpage/pgadmin4:9.18.0805 MB
pgAdmin: up → responde HTTP 200~8 sidem
Teste em clone limpo, versão com adminerpassouclone local da branch numa pasta nova, antes do push
Teste em clone limpo do main no GitHub (PR #7, com pgAdmin)passou; make setup em 6,4 sgit clone do GitHub + make setup + make test, após o merge
Antes da correção: pgAdmin pelo IP da rede (192.168.68.x:5050)HTTP 200main do PR #7, portas em 0.0.0.0
Antes da correção: psql -h 192.168.68.x com a senha do READMEentrouidem
Depois da correção: pgAdmin e Postgres pelo IP da rederecusadosportas em 127.0.0.1

O que surpreendeu ​

Já havia um Postgres 17 do Homebrew no Mac escutando em 127.0.0.1:5432. Com ele ligado, o up falharia, ou o psql -h localhost conectaria no banco errado sem avisar. Resolvido parando o serviço (brew services stop postgresql@17), mas qualquer pessoa da squad com Postgres local cai nisso. Para conferir em qual banco você está: select version(); tem que dizer linux, não darwin.

O que foi decidido ​

  • Porta configurável (${POSTGRES_PORT:-5432}): o padrão continua 5432 para a máquina limpa, e quem tem conflito troca só no próprio .env.
  • pgAdmin em vez de adminer (revisto no mesmo dia). Começamos com adminer, mais leve. Na primeira tentativa de uso, o login falhou com Connection refused porque o campo servidor foi preenchido com localhost — dentro do container do adminer, localhost é ele mesmo, e o banco é db. Trocamos para pgAdmin em modo desktop (sem tela de login) com servers.json e pgpass gerados a partir do .env via configs: do Compose: abre já conectado, sem ninguém digitar host. Ganhamos EXPLAIN visual e gerência de roles. Perdemos leveza (805 MB contra 165 MB do adminer, ~8 s para responder) e o modo desktop não tem autenticação — só é aceitável com a porta presa em 127.0.0.1 (ver revisão abaixo).
  • Imagens com tag exata (16.15, 9.18.0) em vez de 16/latest: o que a monitoria sobe é o mesmo que nós testamos. O custo é atualizar à mão.
  • make setup como porta de entrada: um comando cria o .env (sem sobrescrever um existente) e sobe tudo. O Makefile só embrulha docker compose, então quem não tem make roda os comandos equivalentes do README.
  • Sem script de init de esquema aqui: o esquema e as migrações são contrato da #2.

Revisão depois do merge (PR #7) ​

A revisão de código do PR #7, feita depois do merge, achou dois problemas de segurança introduzidos por nós:

  • O pgAdmin sem login estava publicado em 0.0.0.0:5050. O diário dizia que a porta era local, e não era: qualquer máquina da mesma rede abria o pgAdmin já conectado ao banco, com acesso total. Medido: HTTP 200 no pgAdmin e login no psql pelo IP da rede.
  • O Postgres também estava em 0.0.0.0:5432, com a senha conselhos que está no README público.

Corrigido publicando as duas portas só em 127.0.0.1. O teste de fumaça agora falha se alguma porta voltar a escutar fora de 127.0.0.1 — a regra deixa de depender de alguém lembrar.

Outros achados corrigidos na mesma revisão:

  • O teste fazia source .env, e uma senha do INLABS com espaço ou ; quebrava o bash (ou executava comando). Agora as credenciais são lidas dentro do container.
  • O servers.json do pgAdmin só é importado na primeira subida do volume. Mudar POSTGRES_USER ou POSTGRES_DB depois deixava o pgAdmin pedindo senha. Criado make pgadmin-reset.
  • A mensagem de falha de persistência do teste nunca aparecia: o script abortava antes, no SELECT de uma tabela inexistente.

Lição: "local" precisa ser verificado, não declarado. Afirmamos no diário algo que um curl no IP da rede desmentia.


Revisão da #2 (esquema do OLTP) ​

Revisão da branch feature/issue-2-oltp-schema (do Moura, até 2b621c2) e correções aplicadas na própria branch.

O que foi medido ​

CasoAntesDepois
Teste do esquema reescrito (16 casos), banco local16 falhas: 0 tabelas no banco16 ok
Idem, banco zerado (worktree isolada)13 falhas16 ok
Passo "comentários" do teste antigocontava 5.266 entradas do pg_description, só 14 nossasconta só o schema public
Ato de 2003 da planilha (sem id_dou)recusadoaceito como origem = 'PLANILHA'
Pesquisadora reclassifica DEF → GESTduplicate key2 linhas; vigente = GEST
make migrate pela 2ª vez—não reaplica nada
make setup em clone limpo (inclui 1ª subida do pgAdmin)—38,2 s
Linhas deixadas pelo teste no banco—0

O que surpreendeu ​

  • O banco local da squad não tinha nenhuma tabela. O esquema era montado em docker-entrypoint-initdb.d, que só roda com volume vazio. Como todo mundo já tinha o volume da #1, a branch subia sem tabela e sem erro. Um 003_ futuro também nunca chegaria.
  • As definições de AUTO, GEST e IP no seed não batiam com a tipologia da pesquisadora. AUTO virou "Autorização", quando é autorregulação. O teste antigo só contava as siglas, então não pegava.
  • A branch tinha sido criada antes do PR #8 e ainda publicava as portas em 0.0.0.0. Trazer o main resolveu sem conflito.

O que foi decidido (a confirmar no ADR da E1, #5) ​

  • D1 — classificação insert-only. Reclassificar é uma nova linha, a view classificacao_vigente devolve a última de cada revisor, e um trigger bloqueia UPDATE/DELETE. Ganhamos o histórico do julgamento humano, que é o dado que nasce aqui, e a divergência humano × pipeline fica mensurável ao longo do tempo. Perdemos a simplicidade do UPDATE: corrigir exige nova linha, e toda leitura passa pela view.
  • D2 — classificamos a versão do ato. Uma retificação nasce sem classificação e precisa ser reclassificada.
  • D3 — ref_legal + órgão como chave dos atos da planilha, a confirmar com quem pegar a #3.
  • Migrações por make migrate com schema_migrations, e não por initdb. Arquivo mergeado não se edita.
  • ON DELETE RESTRICT em classificacao.ato_id: apagar um ato não pode apagar julgamento humano.

Issue #3 (normalizar as 5 abas da pesquisadora) ​

Presentes: Bruno (@BrunoBReis), Márcio Henrique

Branch feature/issue-3-normalizar-planilhas. Antes de escrever código, medimos qual ferramenta faz a extração e como ela roda.

O que foi medido ​

MedidaValorComo
Linhas com dado nas 5 abas215 (CNAS 53, CONAMA 98, CONCIDADES 28, CONDRAF 24, CNPIR 12)três leitores independentes: XML do .xlsx lido direto, openpyxl e pandas.read_excel
Linhas formatadas nas 5 abas (max_row − 1 do openpyxl)2.638 (366 + 99 + 848 + 951 + 374)openpyxl 3.1.5
Planilha de data/raw × planilha do repositório antigo (commit 5e6b516, 17/09)idênticas byte a byte (sha256 635b3f0a…)cmp e sha256sum
Anos dos atos2003–2020 (CNAS 2003–2004, CONAMA 2003–2006, CONCIDADES 2004–2005, CONDRAF 2003–2009, CNPIR 2005–2020)coluna de data de cada aba
ID_VERSAO repetido dentro de uma aba0 nas 5 abascontagem

O que surpreendeu ​

  • A carga inicial é 215 atos, não 2.638. O número de 25/09 contou as linhas que têm formatação de célula, e a maioria delas está vazia. O erro se espalhou para o README, os ADRs 0001 e 0002 e a issue #3, e foi corrigido nos arquivos do repositório (o diário de 25/09 ganhou uma errata). As medições dos ADRs com n = 2.638 continuam valendo como volume sintético: são ~12× a carga real. Pesa na E1, que pede volume relevante ("não bastam 100 linhas").
  • O Excel corrompeu o DIS_LEG (ATO/ANO) do CNPIR. "1/2005" foi lido como data e virou 01/01/2005, em 11 das 12 linhas. Dá para recuperar: o mês é o número do ato, e o resultado bate com o texto do TEMA nas 11.
  • A chave D3 da #2 (ref_legal + órgão) não se sustenta. O CNAS não tem coluna de referência legal. O DISP_LEG do CONAMA traz só o tipo ("Resolução", "Moção"), sem número. No CONDRAF, "Resolução Nº 61" aparece em dois atos diferentes.
  • Tipologia 99 em 6 linhas, fora das 5 categorias. O cabeçalho do CNAS diz TIPOL_DECIS (M e J), então quem classificou pode não ter sido a pesquisadora. O cabeçalho do CONAMA está na linha 2, não na 1.

Qual ferramenta lê a planilha ​

Escrevemos três leitores com o mesmo contrato de saída: aba, linha do Excel, id, data, referência legal, tipologia e texto. Cada um foi medido contra as armadilhas acima e contra duas mutações da planilha: uma data virando o texto s/d e a linha 10 do CNAS esvaziada. Os resultados brutos estão em docs/diario/medicoes/2026-09-26-extracao/resultados.json; o código do benchmark e dos três leitores ficou no commit e4f87eb e saiu do repositório no PR, porque os leitores descartados não seriam mantidos. Condições: Linux x86_64 (AMD Ryzen 7 2700), Docker 29.7.2, mediana de 5 execuções, três rodadas com resultados estáveis. O polars saiu da lista antes da medição.

Python stdlibpandas 3.0.6 + openpyxl 3.1.5DuckDB 1.5.5
Armadilhas e mutações (215 atos, CONAMA na linha 2, datas, CNPIR, 99, linha do Excel, determinismo; s/d falha e para; linha vazia não some)todastodastodas
Os 215 registros, campo a campoiguais nos trêsiguais nos trêsiguais nos três
Linhas de código613435
Imagem a mais sobre python:3.12-slim / tempo de build0 MB / 0,7 s45 MB / 23 s30 MB / 10 s
Execução no container0,7 s1,7 s0,6 s
Chamada mais direta da ferramenta, sem contornonão existe: toda conversão é escrita à mãoo max_row conta 2.638 linhas; o 1/2005 do CNPIR vira uma data que parece válidapara na primeira linha vazia: com a linha 10 esvaziada, devolve 8 dos 53 atos do CNAS, sem erro

Escritos com cuidado, os três acertam. O que os separa é o comportamento padrão, que pega quem mexer no script depois, e o custo.

O que foi decidido ​

  • A extração usa só a biblioteca padrão do Python (zipfile + xml.etree) e roda num container do compose (python:3.12-slim).
    • O que ganhamos: nenhuma dependência e nada baixado da internet; toda conversão (data serial, CNPIR, 99) fica no nosso código e coberta por teste; a linha do Excel vem do próprio XML.
    • O que perdemos: quase o dobro de código (61 linhas, contra 34 e 35), e um leitor de .xlsx que a squad passa a manter. Ele cobre só o que esta planilha usa: strings compartilhadas, números e datas no sistema 1900. Ele também não fala com o Postgres: quem grava no banco é o psql do container db, como no scripts/migrate.sh.
    • Por que o container e não uv run no host: Docker e make continuam sendo os únicos pré-requisitos do README.
  • Não abrimos ADR para isso, a princípio. A leitura do .xlsx roda uma vez, na carga inicial, e trocar de leitor é trocar um arquivo: é uma decisão reversível e isolada. Revisto na revisão do PR: a leitura, a chave e a regra da classificação ficaram registradas juntas no ADR 0003. A escolha entre container e uv no host volta na #6, onde ela vira padrão para o resto do pipeline.
  • Chave dos atos da planilha: conselho + ID_VERSAO, com a migração sql/003 e um índice único. Substitui a D3.

A carga (make carga) ​

O pacote src/planilha/ usa só a biblioteca padrão e roda no serviço carga do compose, sem rede e com as montagens só de leitura. Ele escreve no stdout: o SQL, que o psql do container db executa (como no migrate.sh), o relatório em docs/carga/relatorio-planilha.md e o CSV normalizado em data/interim/. O make setup e o make reset passam a carregar a planilha.

O que foi decidido na implementação:

  • Os 5 conselhos entram por migração (sql/004), porque todo ato exige orgao_id e a #4 ainda não saiu. Entram só com o nome vigente e a data do ato legal que os instituiu, conferida no planalto.gov.br. Hierarquia e aliases ficam para a #4, que reaproveita estes 5 órgãos.
  • Resumo em ementa, texto em conteudo. O TEMA muda de natureza entre as abas:
    • CNAS e CONDRAF: resumo, com ~100 caracteres de mediana;
    • CONCIDADES: a parte "RESOLVE:", com o resumo à parte em TEMA_RESUM;
    • CONAMA e CNPIR: texto transcrito, com ~1.900 caracteres de mediana e 41.941 no máximo.
  • Tipologia 99 vira uma sigla da tabela tipologia (sql/005, "Fora da tipologia"). São 6 atos: moções, uma menção honrosa e duas recomendações do ConCidades. O "99 MOÇÃO" do CNPIR entra como 99, e a anotação fica no CSV.
  • Revisora de todas as classificações: a "Pesquisadora Principal" do seed, embora o cabeçalho do CNAS diga (M e J). Fica em aberto na issue.
  • A classificação da planilha entra uma vez por ato e nunca sobrepõe a curadoria. Se a pesquisadora já classificou o ato, rodar a carga de novo não faz nada. Consequência aceita: se uma versão nova da planilha corrigir um rótulo, a correção não chega ao banco pela carga, só pela curadoria.
  • Linhas com problema viram descarte, com o motivo no relatório. Se a estrutura da planilha mudar (aba ou coluna faltando, célula de erro, datas de 1904), a carga para. As colunas são achadas pelo texto do cabeçalho, não pela letra.

O que foi medido:

MedidaValor
Atos carregados / descartados / com classificação215 / 0 / 215
make setup num clone limpo da branch (imagens em cache)11,9 s, com "215 atos novos e 215 classificações novas"
make test no clone limpo4 suítes passando: infra, esquema (20 casos, eram 16), leitura da planilha (21), carga (10)
git status no clone depois de make setup e de make testvazio: o relatório gerado do zero é idêntico ao versionado
Segunda carga seguida0 atos novos e 0 classificações novas; relatório com o mesmo sha256
Testes da leitura com o código sabotado (5 defeitos plantados)os 5 pegos
Caso "a carga não sobrepõe a curadoria" com o NOT EXISTS removidopega: a vigente vira AUTO e entrariam 215 classificações duplicadas (tudo desfeito)

O que surpreendeu:

  • As linhas abaixo do cabeçalho somam 2.637, não 2.638. O cabeçalho do CONAMA está na linha 2, e o max_row − 1 de 25/09 contou esse cabeçalho como se fosse uma linha de dado.
  • A Lei 10.683/2003 renomeou o "Conselho Nacional de Desenvolvimento Urbano" para "Conselho das Cidades". É exatamente o caso que o orgao_nome com vigência existe para guardar; fica para a #4.
  • A planilha estava aberta no LibreOffice durante o trabalho (havia um arquivo .~lock em data/raw/). Salvar por lá mudaria o arquivo e o sha256 do relatório. Conferimos antes de versionar que o arquivo continuava idêntico ao do repositório antigo.
  • A planilha é versionada em data/raw/. A professora tornou a planilha pública, e isso remove o passo manual de copiar o arquivo num clone limpo.

Issue #4 (identidade dos conselhos) ​

Branch feature/issue-4-identidade-conselhos, a partir da branch da #3, que já tem o leitor de .xlsx e os 5 conselhos do sql/004. Antes de qualquer código, escrevemos e validamos o ADR 0004: a identidade sai de uma chave normalizada mais decisões registradas, nunca de similaridade de texto.

O que foi medido ​

MedidaValorComo
Linhas da aba "Conselhos mapeados no DOU"125leitor da #3
Chaves distintas (caixa, acento, espaços, sigla no fim)95 (a normalização de 25/09 dava 96: faltava a sigla com hífen)src/conselhos/identidade.py
Conselhos distintos82, e até 85 conforme as 3 ambiguidades; 2 linhas indefinidasdocs/carga/relatorio-conselhos.md
Órgãos com código SIORG68 de 82recorte do SIORG consultado em 26/09
Similaridade do mesmo conselho renomeado × de dois conselhos diferentes0,889 ("Pessoa Idosa" × "do Idoso") × 0,887 ("Pessoa Idosa" × "LGBTQIA+")docs/adr/medicoes/0004-identidade/bench.py
Partição da carga × gabarito do ADRidênticas, linha a linhateste test_particao_igual_ao_gabarito_do_adr
make setup num diretório limpo (volume novo)13,9 s, com 77 órgãos novos, os 5 da #3 reaproveitados e 125 aliasescópia dos arquivos versionados, sem .env
make test no diretório limpo45,6 s, 4 suítes passando: esquema (27 casos), leitura da planilha (21), conselhos (27) e carga (15)idem
Segunda carga seguida0 órgãos, nomes, aliases, atos e classificações novos; os dois relatórios com o mesmo sha256tests/db/test_carga.sh
Testes da identidade com o código sabotado (5 defeitos plantados)os 5 pegoscópia de src/ com um defeito por vez

O que foi decidido na implementação ​

Cada ponto com opções apresentadas e escolhidas antes do código:

  • As decisões ficam num CSV versionado (data/referencia/conselhos-decisoes.csv), que a pesquisadora abre numa planilha. A carga confere cada linha contra a planilha: se a linha não existe, o nome não bate ou há vírgula sem aspas, ela para.
  • O SIORG entra por um recorte versionado (data/referencia/siorg-conselhos.csv, uma linha por chave da planilha). Quem gera é o make siorg, o único comando com rede, e o make setup não o chama: a carga continua sem rede e reproduzível.
  • A carga dos conselhos roda dentro do make carga, antes dos atos, no mesmo serviço carga.
  • orgao_alias com status (RESOLVIDO, INDEFINIDO, AMBIGUO): só o resolvido tem órgão, e o pendente exige motivo, garantido por CHECK.
  • Nome vigente: vem do SIORG quando a chave casa com uma unidade só; senão, da decisão "nome" do CSV. Uma decisão "nome" contra o SIORG para a carga.
  • orgao.codigo_siorg e orgao.sigla como colunas. A sigla não é única, porque CNE e CNT se repetem.
  • Os 3 ambíguos não viram órgão: ficam em orgao_alias sem órgão, esperando a resposta.
  • "do Idoso" e "da Mulher" entram como renomeação, com a data do primeiro ato conferido com o nome novo (Decreto 9.893/2019 e Decreto 11.351/2023), e o motivo diz que o ato não fala em "passa a denominar-se". O nome antigo fecha na véspera.

O que surpreendeu ​

  • O CSV de decisões tinha uma vírgula sem aspas, e o csv.DictReader joga a sobra numa coluna sem nome, sem erro. Os testes pegaram na primeira execução, e a carga agora recusa linha com número de colunas errado.
  • No primeiro teste em diretório limpo, o Compose reaproveitou o volume do projeto, porque o diretório tinha o mesmo nome. A carga respondeu "0 órgãos novos", o que parecia sucesso. Refeito num diretório com outro nome.
  • Os 5 conselhos da #3 casam exatamente com o nome vigente do SIORG, então a carga os reaproveita sem decisão nenhuma, e eles só ganham código SIORG e sigla.
  • O cruzamento com o artCategory do DOU, pedido na issue, fica para a #6: ainda não temos o DOU. O orgao_alias já aceita fonte = 'DOU', e a chave é a mesma.

Nota sobre uso de IA ​

Plano, compose, teste de fumaça e textos de documentação desta issue foram produzidos com auxílio de Claude Code. As tags das imagens foram conferidas no Docker Hub, o conflito de porta foi detectado com lsof, e todas as medições acima foram executadas na máquina, não estimadas. Ver AI-USAGE.md.

Projeto Integrado de Banco de Dados 2 — FCTE/UnB, 2026/2.