Skip to content

Adiciona agregador falcao (Jurisprudencia Nacional da JT / CSJT)#325

Open
biabrajal wants to merge 2 commits into
jtrecenti:mainfrom
biabrajal:feat/aggregator-falcao
Open

Adiciona agregador falcao (Jurisprudencia Nacional da JT / CSJT)#325
biabrajal wants to merge 2 commits into
jtrecenti:mainfrom
biabrajal:feat/aggregator-falcao

Conversation

@biabrajal

Copy link
Copy Markdown

O que é

Novo agregador falcao (FalcaoScraper, jus.scraper("falcao")) para a Jurisprudencia Nacional da Justica do Trabalho — sistema hospedado pelo CSJT em jurisprudencia.jt.jus.br (endpoint publico /no-auth/pesquisa). Uma unica busca cobre o TST e os 24 TRTs sobre cinco colecoes de documentos.

API

cjsg(pesquisa, paginas=None, **kwargs) + par cjsg_download / cjsg_parse, devolvendo pd.DataFrame.

  • Colunas canonicas garantidas: processo, colecao, tribunal (+ relator, classe, ementa, data_julgamento, data_juntada conforme a colecao). Campos brutos preservados via extra="allow" no Output.
  • Filtros validados por pydantic com extra="forbid" (kwargs desconhecidos -> TypeError): colecao (acordaos default, sentencas, decisoesmonocraticas, precedentes, recursorevista), tribunais, relator, orgao_julgador, classe, fase_processual, prioridade, tem_ementa, somente_ementa, ordenacao (mais_relevante/mais_recente/menos_recente) e intervalo data_juntada_inicio/data_juntada_fim (aceita DD/MM/AAAA, AAAA-MM-DD ou date; filtra dataJuntada).

Engenharia reversa do backend (SPA Angular)

Codigo final usa so requests. Descobertas que viraram comportamento do scraper:

  • sessionId obrigatorio (sem ele: 403 "Tentativa invalida de acesso ao sistema").
  • Origin/Referer do site e User-Agent de navegador exigidos pelo WAF (CloudFront) — o UA padrao do juscraper toma 403, entao DEFAULT_HEADERS sobrescreve com UA de navegador (mesma abordagem do ComunicaCNJ).
  • page 0-based no wire (a API publica do juscraper continua 1-based).
  • tamanho_pagina restrito a {5, 10} para usuario nao autenticado; resultados limitados a 10000 por consulta.

Testes

  • Offline (responses): tests/falcao/test_falcao_contract.py (schema do DataFrame, querystring com as guardas obrigatorias, TypeError/ValidationError) e test_falcao_granular.py (helpers de build/parse/paginacao). Samples por colecao em tests/falcao/samples/pesquisa/.
  • Integracao (@pytest.mark.integration): test_falcao_integration.py — busca real, 5 colecoes, filtros, download+parse. Todos passando contra o site ao vivo.
  • Suite offline completa: 1778 passed / 32 skipped, sem regressao. Notebook executa end-to-end (nbmake). flake8 limpo, pylint 10/10.

Docs

Notebook de exemplo (docs/notebooks/falcao.ipynb, em ingles), entradas em _quarto.yml e index.qmd, e uma entrada consolidada no CHANGELOG.md.

Notas para revisao

  • Nome falcao = nome interno do sistema (backend br.jus.trt9.falcao, client_id falcao-trt9).
  • Colecao precedentes nao tem numero CNJ de processo — processo recebe o numero do precedente (numero, ex.: numero da sumula/OJ). Documentado no schema.

🤖 Generated with Claude Code

biabrajal and others added 2 commits July 6, 2026 22:33
Novo scraper FalcaoScraper para a busca publica de jurisprudencia da
Justica do Trabalho (jurisprudencia.jt.jus.br, endpoint /no-auth/pesquisa),
cobrindo TST e os 24 TRTs sobre cinco colecoes.

- cjsg / cjsg_download / cjsg_parse com filtros validados por pydantic
  (extra="forbid"): colecao, tribunais, relator, orgao_julgador, classe,
  fase_processual, prioridade, tem_ementa, somente_ementa, ordenacao e
  intervalo data_juntada_*.
- Reverse engineering do backend Angular/SPA: sessionId obrigatorio,
  Origin/Referer e User-Agent de navegador exigidos pelo WAF, page 0-based
  no wire, tamanho_pagina restrito a {5,10}, teto de 10000 resultados.
- Registrado na factory como "falcao".
- Testes: contrato + granular (offline, via responses) e integracao
  (@pytest.mark.integration), com samples por colecao.
- Docs: notebook de exemplo, entradas em _quarto.yml/index.qmd e CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@bdcdo bdcdo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisão (ultrareview) — 15 achados verificados

Revisão multi-agente em modo recall máximo (11 ângulos independentes → ~40 candidatos → verificação adversarial → sweep final), toda em modo leitura sobre o head 0b5c032. O PR está bem construído — arquitetura consistente com os agregadores irmãos, docstrings no template, CHANGELOG correto e samples sem PII (anonimização da própria API). Os achados abaixo estão ordenados por gravidade.

Bugs de corretude

1. Buscas diferentes se misturam no mesmo diretório de downloadclient.py:286-290

_preparar_destino cria destino fixo por coleção (falcao_cjsg_{colecao}) com nomes de arquivo fixos, sem limpeza. cjsg_download('dano moral') grava 100 páginas; cjsg_download('assédio', paginas=1) depois sobrescreve só acordaos_0001.json, e cjsg_parse devolve 1 página de "assédio" + 99 páginas obsoletas de "dano moral", sem erro nem aviso. A docstring promete "subdiretório da busca", que não existe. Os irmãos que persistem arquivos isolam por timestamp (courts/_esaj/download.py, courts/tjsp/cjpg_download.py:131-134) — o fix é trivial no padrão da casa. (O fluxo cjsg() escapa por usar TemporaryDirectory fresco.)

2. range com step baixa o dobro de páginas (ou nenhuma)client.py:301-305

_resolver_paginas reconstrói range(max(1, start), min(stop, total+1)) descartando o step: paginas=range(1, 101, 2) baixa as 100 páginas 1–100 em vez das 50 ímpares (2× requests num backend com rate limit ~50/janela, incluindo páginas que o usuário excluiu); range(10, 0, -1) vira range vazio e baixa 0 páginas em silêncio. paginas=list(range(1, 101, 2)) funciona correto, e PDPJ (client.py:341), TJSP cjpg (cjpg_download.py:142-147) e TJRN honram o step — o mesmo input dá resultados diferentes conforme o scraper. Correção mais profunda possível: normalizar/materializar step≠1 em normalize_paginas, que é a fonte única do contrato de paginas.

3. processo=None em precedentes reaisparse.py:44-48

Documento de precedentes sem a chave numero produz processo=None, violando o shape declarado (OutputCJSGBase.processo: str) — há linhas TRT9 assim no output commitado do próprio notebook (docs/notebooks/falcao.ipynb). O contract test assere df['processo'].notna().all() sobre um sample TST que sempre tem numero, dando falsa segurança.

4. Data inválida com um só limite vaza crua para o backendclient.py:176-183

coerce_brazilian_date faz passthrough do que não parseia e validate_intervalo_datas retorna cedo quando um dos lados é None (utils/params.py:353). cjsg('x', data_juntada_inicio='31/02/2024') (ou 'abc') não levanta o ValueError prometido na docstring e envia dataInicio=31/02/2024 cru na querystring — se o backend ignorar o filtro malformado, a busca devolve resultados sem recorte de data, silenciosamente. Com ambos os limites preenchidos, o mesmo valor levanta o erro amigável.

5. Fallback download_path or "." é ramo morto e a docstring enganaclient.py:287

BaseScraper.set_download_path(None) sempre cria tempfile.mkdtemp() (core/base.py:32-33), nunca deixa download_path como None. Quem chamar cjsg_download sem diretorio esperando os JSONs no diretório atual (como diz a docstring) encontra os arquivos em /tmp/tmpXXXX/falcao_cjsg_acordaos — e os perde na limpeza do /tmp se não guardar o path retornado.

6. cjsg(diretorio=...) colide e o rótulo do TypeError aponta o método erradoclient.py:139-143 e client.py:281

cjsg repassa **kwargs junto de diretorio=tmp fixo, então diretorio= passado a cjsg"got multiple values for keyword argument 'diretorio'" críptico em vez da mensagem canônica. E raise_on_extra_kwargs usa rótulo fixo "FalcaoScraper.cjsg()" mesmo quando o kwarg inválido veio de cjsg_download (a família eSAJ passa method_label por endpoint, _esaj/base.py:182).

Fiscalização e testes

7. O falcao é invisível a toda a rede anti-drift de tests/schemas/tests/schemas/test_schema_coverage.py:219

Por ser o primeiro agregador com endpoint cjsg, ele cai num ponto cego: a tupla candidates dos meta-testes de agregadores não contém "cjsg", então nada falha cobrando o registro em EXPECTED_AGGREGATOR_SCHEMAS/EXPECTED_AGGREGATOR_OUTPUT_SCHEMAS (CONTRIBUTING, checklist de schemas item 4) nem em CASES/DOWNLOAD_REFERENCE_CASES do docstring parity (CLAUDE.md, docstrings regra 7). Resultado: extra="forbid", aceitação das 4 formas de paginas, nomes canônicos e paridade docstring↔schema nunca rodam para o falcao — é exatamente o schema "órfão silencioso" que a suíte existe para impedir, e explica o "1778 passed" ser verdadeiro e ainda assim enganoso. O fix precisa das duas pontas: registrar os mappings e adicionar "cjsg" às duas tuplas candidates (senão test_no_stale_entries acusa a entrada nova como órfã).

8. Nenhum teste offline exercita 2+ páginastest_falcao_contract.py:27

O parâmetro total de _register é código morto; o caminho "página 2 envia page=1, dorme e grava _0002.json" nunca roda em teste — um off-by-one na querystring das páginas 2+ passaria a suíte inteira. Os asserts de querystring também re-parseiam responses.calls com parse_qs inline em vez do query_param_subset_matcher de tests/_helpers.py:173 (CONTRIBUTING, item 3: matcher de payload sempre que possível).

9. Falta o capture scripttests/fixtures/capture/falcao.py

Item 1 do checklist do CONTRIBUTING; todos os agregadores existentes têm o seu (datajud.py, jusbr.py, comunica_cnj.py). Sem ele, os 6 samples não têm procedência reproduzível ("nunca sintetizar samples à mão") e não há como regenerá-los nem detectar drift quando o backend mudar de shape. Nota positiva: os samples estão limpos de PII.

Documentação e tratamento de erro

10. Docstring promete "colunas canônicas garantidas" que o shape real não sustentaclient.py:116-120

Três frentes: (a) busca vazia devolve DataFrame sem coluna alguma — é a convenção de toda a base (verificado em comunica_cnj/datajud/eSAJ), então o defeito é a promessa, única no repo; (b) ementa/data_julgamento não existem nas coleções sem a chave bruta — df['ementa'] em sentencas dá KeyError apesar do "None nas demais" em schemas.py:148; (c) decisoesmonocraticas traz o dado em nomeRedator, que o parser não mapeia — a coluna relator nunca existe nessa coleção e o filtro relator= tende a ser inócuo nela. Sugestão: relativizar a promessa ao shape real (regra do projeto: "Output reflete shape real do parser") ou materializar o núcleo no parser.

11. 403 do WAF vira retry cego + erro opacoclient.py:208

403 está em RETRYABLE_STATUSES (core/http.py:34) e as chamadas não passam on_response: um bloqueio do CloudFront (determinístico — o próprio download.py documenta os gatilhos) paga 3 tentativas com backoff e morre em RetryExhaustedError genérica, enquanto a base tem BotChallengeBlockedError + marker anti_bot exatamente para isso (5 call sites em courts/_trf/download.py). Sem isso, os testes de integração não se beneficiam do xfail ambiental e o recipe -m "integration and not anti_bot" não exclui o falcao. Atenuante: o comunica_cnj também depende do retry genérico.

Reuso e simplificação

12. 3ª cópia da mecânica de paginaçãoclient.py:292-306: _total_paginas/_resolver_paginas repetem comunica_cnj (client.py:146-158) e _esaj/download.py:132-153, e _validar_input é a 2ª cópia do pipeline de validação de agregador com data canônica própria. Pela Regra 1 do #84 já há massa crítica para promover a utils/params.py — as cópias já divergem entre si (eSAJ exclui a página 1, PDPJ honra o step) e compartilham o bug do step.

13. Hooks anunciados e nunca consumidosclient.py:43-44 e schemas.py:85: INPUT_CJSG e COLECOES não são lidos por nada (o código hardcoda InputCJSGFalcao nas linhas 277/283 e a constante de módulo no validator), e BACKEND_DATE_FORMAT nunca é lido — o client hardcoda "%Y-%m-%d" em 2 lugares (183 e 275). Ou consumir via self/cls (como a eSAJ consome self.INPUT_CJSG), ou remover os atributos.

14. field_validator reinventando Literalschemas.py:106-132: colecao: Literal[...], tamanho_pagina: Literal[5, 10] e ordenacao: Literal[...] | None eliminam ~27 linhas com a mesma validação, no padrão que tjto/tjpa/tjpe/tjsc/_esaj já usam (e que os TODOs #212 do repo apontam como direção).

15. Probe incondicional da página 1client.py:208-212: com paginas explícito que não inclui a página 1, paga-se um request extra (payload de até ~1MB, descartado) só para obter um total que a primeira página pedida também traria — o backend é REST stateless e toda resposta carrega quantidadeTotal.

Menores (fora do corte de 15)

  • _as_csv sem escape de vírgula — confirmado ao vivo que o backend splita o parâmetro como OR, mas nenhum valor legítimo do domínio contém vírgula (formato canônico é "NOME SOBRENOME"), então o gatilho é atípico.
  • model_config redeclarado idêntico ao pai nas duas classes de schema; max(0, pagina-1) defensivo para caso impossível; logger de módulo morto em download.py e parse.py.
  • Notebook: célula com linha de exibição morta (df_filtrado[[...]].head() sobrescrita pelo head() seguinte); json.load(open(...)) sem with dentro de TemporaryDirectory (risco de PermissionError no Windows).
  • Integração: assert len(df) == 10 exato acoplado ao corpus vivo do backend; nenhum teste passa datetime.date nas datas apesar de docstring/schema prometerem.
  • relator='' é copiado como string vazia (o guard usa is not None, diferente da truthiness das _CLASSE_KEYS), e o teste granular correspondente passa vacuosamente ('' == '').
  • README sem a linha do falcao na tabela de agregadores (lacuna pré-existente — comunica_cnj também falta).

🤖 Revisão gerada com Claude Code (/code-review max)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants