Toda segunda-feira, 05:00 de Brasília, o repositório se pergunta se o catálogo
ainda corresponde à lei. Baixa os textos compilados dos diplomas de
data/fontes.json, estrutura cada dispositivo, lê as molduras penais, compara
com static/data/crimes.json e olha o DOU da semana em busca de lei penal nova.
O resultado sai em dois canais: uma issue com tudo que precisa de gente e, quando há correção inequívoca, um PR aberto pelo próprio robô.
É determinístico de ponta a ponta — sem IA, sem inferência. Onde não há leitura segura, o achado vira pergunta na issue em vez de virar dado.
| Arquivo | O que faz |
|---|---|
baixar.py |
Fetch dos compilados. Detecta codificação (BOM → meta → UTF-8 → cp1252) e valida a sentinela de cada fonte: a página é gravada em .html.part e só é promovida a <data>.html quando a sentinela passa — snapshot no caminho canônico significa "conferido", por construção, e o differ nunca compara com texto velho ou truncado. |
parsear.py |
HTML → dispositivos (caput, §§, pena, situação, anotação). Fatia o HTML cru: a árvore do Word malformado desloca as fronteiras de parágrafo. |
../pena_parser.py |
Lê a moldura. Compartilhado com transform_data.py, para que catálogo e conferidor leiam a mesma pena do mesmo jeito. |
conferir.py |
O differ. Classifica cada achado e escreve DOIS relatórios: crawler/relatorios/AAAA-MM-DD.md, completo, id por id, que vai no artifact; e -resumo.md, que vai no corpo da issue — lá os limites declarados de cobertura saem como contagem, porque o corpo tem teto de 65.536 caracteres e o despejo empurrava para fora justamente o que pede ação. Ambos abrem dizendo contra que branch e commit a rodada correu. Sai com 3 quando há achados. |
derivadas.py |
Desde 28/09/2026, recalcula as linhas de pena derivada (aumento, diminuição, dobro, metade de outra moldura): a base é o registro que o c/c declara, ou o caput do mesmo artigo — e, se a base não é registro, a moldura que a lei escreve nela, seguindo "na mesma pena" de um salto; a fração é a do texto da lei (nucleo/fracao.py, compartilhado com o Proponente). Confere → conta como conferido; diverge → achado DIVERGENTE-derivada, com a moldura esperada, que o Proponente corrige como qualquer outra; o que a regra não decide sai como pede juízo, com o motivo, e fica no balde pena_derivada. Não escreve no catálogo. |
vigencia.py |
Vacatio legis e produção de efeitos diferida: nunca propor mudança que ainda não vigora. |
revogacao.py |
Revogação total de diploma (banner no topo da página). |
corrigir.py |
Correção mecânica de linha existente: moldura e espécie de pena. |
propor.py (sentinelas) |
Desde 28/09/2026, troca a sentinela de uma fonte quando o compilado já anota lei mais nova que ela ("Incluído pela Lei nº 15.517, de 2026"): o número novo está na própria página. Só sentinela que é número de lei ordinária; a de conteúdo (nomen juris, para diploma sem emenda) fica. "Vide" não conta. |
propor.py (modificadores) |
Desde 28/09/2026, propõe modificador para o MODIFICADOR-AUSENTE cujo texto declara o alcance — "na hipótese do § 10", "a pena prevista no § 2º-A", ou "neste artigo" em artigo de linha única no catálogo. Fração e direção vêm do texto; o alvo tem de existir no catálogo; no CP, marca ignora_embutida quando não há linha derivada. O que não declara alcance continua pergunta na issue. Não escreve nota. |
criar.py |
Linha nova. Não roda no automático — ver "o que o robô não faz". |
propor.py |
Escolhe o diploma da rodada, aplica e monta o corpo do PR. Quando a mudança vem de lei recente (redação dada, ou dispositivo incluído, por lei deste ano ou do anterior, lido da anotação do compilado), escreve a nota — declarando o alcance (tipo, atributo ou os dois) e dizendo em português a direção da mudança — e sobe o patch da versão; correção de dado não vira nota. |
auditar.py |
Audita os campos que a conferência de penas não alcança: hediondez (contra data/hediondos.json), ação penal, causas de aumento ausentes e nomes — inclusive o nome que descreve MELHOR outro artigo do mesmo diploma, ponto cego da conferência de molduras. |
conferir_violencia.py, conferir_acao_penal.py |
Os derivadores de violencia/grave_ameaca e de acao, por regra escrita (scripts/violencia.py, scripts/acao_penal.py). Toda resposta cita o trecho da lei que a decidiu e carrega fundamento_verificado: sem lastro, a derivação não vale — trecho que o texto não tem zera a resposta, que vai para "pede juízo", e o relatório conta quantas foram zeradas. Não escrevem no catálogo. |
dou_watcher.py |
Filtro semanal da Seção 1 do DOU, para achar lei penal nova e autônoma. Só lei ordinária e lei complementar, pela reserva legal — MP, lei delegada, decreto-lei e emenda não criam tipo penal. Baixa o texto INTEGRAL de cada uma — não há mais pré-filtro por vocabulário — e tria em três níveis pelo preceito secundário, que inclui a pena por remissão ("nas mesmas penas", "metade da pena"). O descartado sai nomeado, não some, e o integral fica no JSON da rodada para permitir retriagem quando o critério mudar. |
leis_do_ano.py |
A defesa contra o falso NEGATIVO. Uma vez por mês, confronta a lista de leis sancionadas do ano (no Planalto, que é a FONTE) com o que o watcher examinou. O que sobra é a lista de leis que ninguém olhou. Não classifica: só aponta. Se a lista não puder ser lida, não afirma nada — reportar zero sem ter lido seria o silêncio que ele existe para quebrar. |
excecoes.json |
O que já foi julgado e decidido na conferência de PENAS. Sem isso o relatório repetiria para sempre os mesmos achados. |
excecoes-auditoria.json |
O mesmo, para a auditoria de classificação. Arquivo próprio porque as chaves são outras: ora o id do registro, ora o dispositivo do compilado. |
cobertura-limites.json |
Quantos registros o conferidor aceita não garantir, por motivo. Crescer é regressão e vira achado; encolher vira convite a apertar. É a trava contra a falha que mais dói aqui — a silenciosa: registro que sai da conferência não aparece como divergente, aparece como nada. |
../verificar_documentacao.py |
Saúde da prosa: documento vence por prazo ou porque algo de que ele fala mudou depois da última conferência. |
tempo.py |
A data de Brasília. O runner roda em UTC, e sem isso a rodada das 21h se datava de amanhã. |
python scripts/robos/nucleo/baixar.py --todas # ou --fonte cp
python scripts/robos/vigia/conferir.py # relatório de tudo
python scripts/robos/vigia/conferir.py --carimbar # + trilha em data/conferencia.json
python scripts/robos/vigia/conferir.py --atualizar-limites # regrava a trava de cobertura
python scripts/robos/proponente/propor.py # o PR que sairia (não escreve)
python scripts/robos/sentinela/dou_watcher.py --dias 8
python scripts/robos/recenseador/leis_do_ano.py # o que o watcher nao viu
python -m pytest scripts/robos/tests # sem rede, contra fixturesO workflow também roda sob demanda: aba Actions → "Conferidor semanal" → Run workflow, com opção de não abrir PR e de incluir linhas novas.
Faz, sozinho, direto na main: carimba a trilha de auditoria
(data/conferencia.json → fonte, conferido_em e conferido_resultado no derivado).
É registro do que a máquina fez, não proposta de mudança de dado.
Faz, sozinho, em PR: corrigir moldura ou espécie de pena de linha que já
existe, quando o compilado diz outra coisa; trocar a sentinela de uma fonte pela lei
mais nova que a página anota; e cadastrar o modificador cujo dispositivo declara
fração e alcance ("na hipótese do § 10") — as duas últimas desde 28/09/2026, depois
que a rodada da Lei 15.517 mostrou que eram leitura de texto e ficavam para gente.
Um diploma por rodada (as sentinelas vão todas juntas, de qualquer fonte), um PR
aberto por vez, evidência por mudança. Se alguma mudança vem de lei recente, o PR escreve a nota
(uma por lei e por direção da mudança, que é como ele as agrupa) e fecha uma versão,
em 0.0.x até o lançamento; se tudo é correção de dado, não escreve nota nem sobe versão,
e o corpo do PR diz por quê.
Faz, em PR que PEDE JUÍZO: as mudanças de hediondez e ação penal, com o fundamento ao lado, e a entrada de data/fontes.json para lei nova que pareça criar tipo penal. Só sai quando não há correção de pena pendente — uma coisa por rodada.
Não faz: criar nem remover linha. Criar exige decidir se o dispositivo é
crime autônomo, causa de aumento ou nada; remover exige decidir o destino da URL
pública. criar.py existe e é útil, mas rodado por gente — a primeira leva
automática dele (v1.3.0) trouxe 29 registros que não eram tipos penais vigentes,
e foi preciso retirá-los na v1.4.0.
Cada uma tem teste e fixture; nenhuma volta em silêncio.
- Texto citado. O compilado transcreve, embaixo do artigo alterador, a redação dada à lei alterada — e congelada na data da alteração. A Lei 8.137 "contém" o art. 172 do CP; a Lei 12.850 "contém" o art. 288. Duas formas de detectar: o texto do caput ("passa a vigorar…") e o número fora de sequência ("Art. 24, Art. 288, Art. 25").
- Espécie que não é pena. Preceito que começa por "Pena - multa" é multa, mesmo que fale em suspensão ou fechamento depois do ponto e vírgula.
- Revogação que mantém o texto. A LCP não apaga o corpo do artigo revogado: anota a revogação ao lado.
- Espécie alternativa. "reclusão ou detenção, de um a três anos" é uma moldura com duas espécies, não duas molduras.
- Rótulo pela lei criadora. Artigos do CP aparecem com
lei="Lei 14.811/24". O vínculo diploma → rótulos vem defontes.json, nunca do campoleicru. - Comparar com o DERIVADO. A moldura que a aplicação usa é
pena_*_meses, do derivado — os campos crus da fonte podem estar velhos. - Codificação. O acervo do Planalto mistura cp1252 e UTF-16 com BOM. "Página parece desatualizada" quase sempre é erro de decodificação.
Art . 190, com espaço antes do ponto, é como a Lei 6.766 escreve do art. 37 em diante.- Sufixo de letra. Vem colado ao número, pode repetir-se (
359-M-B) e pode vir depois do ordinal (2º-A). Sem as duas formas, o2º-Ada Lei 7.716 (injúria racial) virava corpo do art. 2º, que está vetado, e o359-M-Bdo CP virava corpo do golpe de Estado. Em contrapartida,Art. 13 - O resultadonão tem sufixo: ali o hífen é pontuação e oOé artigo definido — foi assim que nasceram artigos inexistentes comoArt. 13-OeArt. 100-A. A regra que separa os dois casos é o espaço em volta do hífen, e ela precisa valer igual emparsear.pye nachave()deconferir.py: os dois lados têm de reduzir ao MESMO identificador, senão o registro existe e não é conferido.
Uma entrada em data/fontes.json (id, rotulos, url do compilado,
sentinela — uma string que prova que a página está fresca). O conferidor passa
a vigiá-lo na rodada seguinte, e o watcher do DOU passa a reconhecer citações a
ele. Todo rótulo distinto de data/crimes.json precisa constar de exatamente um
diploma.