Skip to main content
Glama
robertogecia

Jurisprudência TCE-RO

by robertogecia
README.md
# Jurisprudência do TCE-RO no Claude — instalador

Este é o **instalador** (`Jurisprudencia-TCERO.mcpb`) que dá ao Claude Desktop a capacidade de
pesquisar a jurisprudência do Tribunal de Contas do Estado de Rondônia dentro da conversa, sem
login e sem mexer em código. Feito para advogados **sem conhecimento nenhum de informática**.

## Como instalar (3 passos, uns 2 minutos)

1. **Baixe o arquivo:** ⬇️ [CLIQUE AQUI PARA BAIXAR (`Jurisprudencia-TCERO.mcpb`)](https://github.com/robertogecia/tcero-jurisprudencia-mcp/releases/latest/download/Jurisprudencia-TCERO.mcpb).
   Ele vai para a pasta **Downloads** do seu computador. *(Não use o botão verde "Code → Download
   ZIP" desta página: ele baixa o código-fonte, que não serve para instalar.)*
2. **Dê dois cliques** no arquivo baixado. O Claude Desktop abre perguntando se quer instalar a
   extensão "Jurisprudência TCE-RO" — clique em **Instalar**. Se nada abrir: Claude Desktop →
   Configurações (engrenagem) → Extensões → arraste o arquivo para essa janela.
3. **Abra uma conversa nova** e peça, por exemplo: *"pesquise no TCE-RO acórdãos sobre dispensa de
   licitação por emergência"*. O Claude pede autorização para usar a ferramenta — autorize.

Precisa ter o **Claude Desktop** (o programa instalado no computador, Mac ou Windows —
[claude.com/download](https://claude.com/download)). Pelo site ou pelo celular não funciona.

## Como vai funcionar, o que ela não faz, erros comuns

Está tudo explicado, em linguagem para advogado, na página principal do projeto:
**[tcero-jurisprudencia-mcp](https://github.com/robertogecia/tcero-jurisprudencia-mcp#como-vai-funcionar-no-dia-a-dia)** —
o que você recebe em cada busca (decisões mais pertinentes primeiro, citação pronta, link do PDF,
inteiro teor, conferência de citação, recibo), o que ela não substitui, e a tabela "algo deu
errado".

---

# Jurisprudência TCE-RO (.mcpb)

Porte Node do `~/MCP/tcero-jurisprudencia` (Python, fonte de verdade congelada no commit
`d8c024b`, v1.2.0 — porte inicial foi sobre `6dcd631`/v1.1.0, atualizado com o delta de
ranking/panorama) para instalar com dois cliques no Claude Desktop. Mesmas 4 ferramentas, mesmas
strings de saída, mesmo contrato de recibo de custódia (mesmos campos e mesma serialização; o hash de um
recibo gravado por este pacote NÃO é igual ao de um gravado pelo Python para o mesmo acórdão,
porque a extração de texto do PDF — pdfjs-dist × PyMuPDF — difere no espaçamento; cada recibo é
internamente consistente e o lint confere o hash contra o próprio arquivo).

## v1.2.0 (delta d8c024b) — `ordenar`, Panorama, `_orgao_do_fecho` com múltiplos fechos

- `ordenar` (novo parâmetro de `buscar_jurisprudencia_tcero`): `"relevancia"` é o PADRÃO quando há
  `texto_livre`/`grupos` — pontua cada decisão offline (núcleo ementa+dispositivo pesa 2 por termo
  distinto casado, informações adicionais de IA pesam 1) e ordena por isso, desempatando por data;
  sem termo nenhum, comporta-se como `"data"`. Cada item ganha a linha `termos casados: N/M`;
  `ordenar` inválido é recusado ANTES de qualquer requisição. Portado em `lib.js`
  (`termosDaConsulta`, `pontuarRelevancia`, `ordenarPorRelevancia`) e ligado em `index.js`.
- **Panorama**: bloco offline no fim da página 1 quando há 3+ decisões (`lib.js`:
  `blocoPanorama`) — órgão, ano, sigla, natureza, top-5 relatores.
- `orgaoDoFecho` agora devolve `null` quando o PDF tem fechos de **órgãos diferentes** (típico de
  embargos que transcrevem o acórdão embargado) — antes só pegava o primeiro match.
- `VERSAO` → `1.2.0` em `lib.js`, `manifest.json`, `package.json`.

### Red team 22/09/2026-b (commit `e7d8592`, mesma `VERSAO` 1.2.0)

- `"frase exata"` entre aspas no `texto_livre` vira UM termo do ranking (antes virava pedaços com
  aspas grudadas que nunca casavam); palavras vazias (`de`, `do`, `da`, `ao`, `à`, `e`, `o`, `a`...
  — lista exata em `PALAVRAS_VAZIAS_RELEVANCIA`, `lib.js`) e termos de 1 letra não contam mais —
  `\bde` casava o núcleo de 5.047/5.052 decisões do snapshot do Python e só inflava o denominador.
- Linha `termos casados: N/M` ganha `· +K só em informações adicionais (IA)` quando algum termo só
  bateu no campo de IA (não no núcleo ementa+dispositivo).
- Cabeçalho da busca não diz mais "por relevância" quando não há termo nenhum para pontuar (só
  relator/órgão/número) — nesse caso diz "por data... sem termo de texto para pontuar relevância".
- `blocoPanorama` tolera `orgaoJulgador`/`sigla`/etc. em lista ou `None` (`campoPanorama`), conta
  o mesmo relator uma vez mesmo com caixa diferente, e o cabeçalho diz quando cobre só o que casou
  todos os `grupos` ("que casaram todos os grupos" em vez de "desta busca").
- `orgaoDoFecho` reconhece "Tribunal Pleno"/"Primeira Câmara"/"Segunda Câmara" como sinônimos de
  "Pleno"/"1ª Câmara"/"2ª Câmara" (`SINONIMOS_ORGAO_FECHO`) — sem isso, duas grafias do MESMO
  órgão no mesmo PDF (comum: acórdão + embargos) davam falso conflito (`null`).

- `server/lib.js` — lógica pura: normalização (`_fold`, `_padronizar_numero`, `_html_para_texto`,
  `_ementa_limpa`, `_corrigir_link_pdf`), montagem de resumo/detalhe e orçamentos de saída,
  `grupos` (E entre grupos, OU dentro do grupo, filtrado no cliente), conferência literal por
  PALAVRA INTEIRA (`_verificar_trecho`/`_achar_palavras`) com alertas de atribuição (PARECER DO
  MPC/CORPO TÉCNICO, ALEGAÇÃO DA PARTE, TRANSCRIÇÃO, NEGAÇÃO, ENTRE ASPAS), recibo de custódia
  (sha256 do texto + sha256 dos campos, gravação atômica 0600/0700), corte do PDF preservando
  começo e fim, e o disjuntor de ritmo em arquivo (compartilhado entre processos via lockfile).
  Organizado para receber a v1.2.0 (ranking/panorama) sem mexer em `index.js`.
- `server/index.js` — protocolo MCP por stdio, rede (`fetch` nativo, timeout 90s na busca,
  redirect seguido + allowlist de host para o PDF, teto de 20 MB), extração de PDF com
  `pdfjs-dist` (ver escolha da lib abaixo), resolução de relator/órgão, checagem de versão nova
  em segundo plano.
- Estado do disjuntor em `server/.disjuntor_estado_tcero.json` (ao lado do módulo, como o
  Python); recibos em `~/.tcero-jurisprudencia-recibos/` (ou `TCERO_MCP_DIR_RECIBOS`).

## Por que `pdfjs-dist` (não `pdf-parse`, não binário nativo)

O Python usa PyMuPDF (`fitz`), que não existe em Node. Entre as alternativas puramente-JS:

- **`pdf-parse`** é mais simples (uma chamada, todo o texto), mas concatena o documento inteiro
  antes de devolver — não dá acesso página a página. O servidor precisa de contagem de páginas e
  de checagem de "caracteres não-espaço por página" página por página (`LIMIAR_CHARS_POR_PAGINA`,
  a mesma heurística que decide "PDF sem texto extraível") e de um corte cooperativo por página
  (`TETO_PAGINAS_PDF`, checado a cada página, não só no fim). `pdf-parse` também empacota
  `pdfjs-dist` por baixo — usar `pdfjs-dist` direto evita uma camada e dá o controle por página
  que o porte fiel do Python exige.
- **`pdfjs-dist` (legacy build)** é puro JavaScript (sem binário nativo, sem compilação, nada de
  rede durante `npm install` além do registry do npm), dá acesso a `getPage(i).getTextContent()`
  por página, e é o motor de referência para leitura de PDF em Node/browser (mesmo projeto do
  Firefox). É a escolha deste porte.

Divergência conhecida e aceita: PyMuPDF e pdfjs-dist não extraem espaçamento/quebra de linha
byte-a-byte igual (ordem de glifos, hifenização, colunas). A comparação de citação
(`_verificar_trecho`/`normalizarCasamento`) já normaliza espaço em branco antes de comparar, então
uma citação válida nos dois motores continua válida nos dois; o que pode variar é a contagem exata
de caracteres extraídos e o texto bruto guardado no recibo (`texto_pdf_completo`) — quando isso
importar (comparação byte a byte via `test/paridade.py`), normalize os dois lados por espaço antes
de comparar, como o próprio lint da peticao-rg já faz para a citação.

## Empacotar e instalar

```bash
npm install && npm test
npx -y @anthropic-ai/mcpb@latest pack . Jurisprudencia-TCERO.mcpb
```

Instalar: Claude Desktop → Configurações → Extensões → arrastar `Jurisprudencia-TCERO.mcpb`
(cópia em `~/MCP/`).

## Smoke test (22/09/2026, via stdio, rede real)

`tools/list` → 4 ferramentas. `buscar_jurisprudencia_tcero(numero_processo="02603/22",
por_pagina=3)` → 4 decisões reais, citação `(TCE-RO - APL-TC 00055/26, Rel. JOSÉ EULER POTYGUARA
PEREIRA DE MELLO, Pleno, j. 22/06/2026, DOe 07/07/2026)`. `obter_acordao_tcero(id_decisao=98114,
ler_inteiro_teor=true)` → PDF real baixado e extraído, linha `Verificação: "inteiro teor lido
(PDF)"` presente, recibo gravado em `~/.tcero-jurisprudencia-recibos/98114.json` com permissão
0600 (diretório 0700). `verificar_citacao_tcero(id_decisao=98114, trecho="DESCUMPRIMENTO DE
DETERMINAÇÃO DO TRIBUNAL DE CONTAS")` → ✅ VÁLIDO, **zero requisição** (via recibo local).

Achado corrigido durante o smoke test: `pdfjs-dist` escreve avisos (`Warning: ...`) em
`console.log` por padrão — no transporte MCP por stdio isso corrompe o JSON-RPC (stdout precisa
carregar só mensagens do protocolo). Corrigido com `verbosity: 0` em `getDocument()`
(`server/index.js`, função `extrairTextoPdf`).

## Duas implementações, uma fonte de verdade

Mudança de comportamento entra primeiro no Python (que tem os red teams de 13, 14 e 22/09/2026 —
ver `../tcero-jurisprudencia/references/`), depois aqui, com o teste correspondente.

## `test/paridade.py` — harness real, zero rede

```bash
~/MCP/tcero-jurisprudencia/.venv/bin/python test/paridade.py
```

Carrega o Python congelado (`/tmp/servidor_tcero_v121.py`, gerado sozinho a partir do commit
`e7d8592` se não existir) como módulo, com `_consultar_api` e `_baixar_pdf` monkey-patchados para
responder com as fixtures reais em `test/fixtures/` (zero requisição) — e roda o mesmo mock em
Node via `_setDepsParaTeste`, exportado por `server/index.js` só para teste (`buscar`,
`obterAcordao`, `verificarCitacao` também são exportados por essa mesma razão; o transporte MCP
por stdio só conecta quando o arquivo é executado diretamente, não quando importado). Compara 13
casos byte a byte: 8 páginas de busca (ordenar data/relevância, com/sem grupos, detalhar, ordenar
inválido, relevância sem termo de texto), obter com/sem inteiro teor, verificar via recibo e via
portal, e o recibo (JSON + os dois hashes).

**Resultado (22/09/2026, revalidado contra o red team 22/09/2026-b/e7d8592): 11 de 13 casos byte a
byte idênticos.** Os 2 restantes têm todos a MESMA causa raiz, já esperada e descrita na seção
anterior — pdfjs-dist e PyMuPDF não extraem espaço em branco de um PDF de forma idêntica:
- `obter_com_pdf` — divergência residual depois de normalizar espaço: o total de caracteres NÃO
  bate por causa da diferença de espaçamento (a contagem de caracteres NÃO-espaço,
  `chars_nao_espaco`, bate exatamente: 57.475 dos dois lados), e como o documento (36 páginas)
  está perto do teto de 45.000 caracteres, um lado corta (`TRECHO DO MEIO OMITIDO`) e o outro não
  — efeito cascata do mesmo espaçamento, não um bug novo.
- `recibo_texto`/`recibo_hashes` — o campo `texto` do recibo carrega o texto do PDF: idêntico
  depois de normalizar espaço, mas os hashes (`sha256`/`sha256_campos`) são calculados sobre o
  texto BRUTO de propósito (para detectar adulteração) — então divergem em cascata, por desenho,
  não por bug.
- `recibo_campos` (`texto_parecer_mpc`) — os excertos brutos (`_excertos_raw`) são recortados por
  posição de caractere no texto bruto; com o texto bruto ligeiramente diferente em comprimento, a
  janela do excerto pode incluir/excluir um caractere na borda. Mesma causa raiz.

`recibo_texto`/`recibo_hashes` continuam divergindo por desenho (mesmo motivo), mas não somam ao
total de "divergentes reais" — só o texto bruto do PDF (e o hash que depende dele) muda; nenhuma
das 13 comparações revelou um bug de LÓGICA (grupos, ordenar — incluindo frase exata/palavras
vazias/rótulo IA/cabeçalho sem termo do red team 22/09/2026-b —, panorama, citação, recibo
estrutural, sinônimo de órgão do fecho) — só o efeito conhecido e documentado da extração de PDF. Corrigido nesta rodada, a
partir do harness: a junção dos itens de texto do pdfjs-dist usava `.join(" ")` entre TODO item,
duplicando/triplicando espaços que o item já carregava (`"Secretaria de   Processamento"` em vez
de `"Secretaria de Processamento"`) e não distinguia fim de linha; trocado por concatenação direta
com quebra de linha em `item.hasEOL`, que aproximou MUITO o resultado do PyMuPDF (a saída bruta
antes desse ajuste divergia já no cabeçalho da 1ª página; depois, só no meio de um documento de 36
páginas). Também corrigido: `ordenar` inválido usava `JSON.stringify` (aspas duplas) onde o
Python usa `repr()` (aspas simples) — criada `pyRepr()` em `lib.js` e usada em toda mensagem de
erro que ecoa um valor do usuário (`relator`, `orgao_julgador`, `id`/`numero` não encontrado).

## O que NÃO foi portado nesta rodada

- **Testes de PDF sintético** (`--selftest` do Python gera PDFs em memória com `fitz.new_page()` +
  `insert_text()` para testar página em branco, PDF "misto", PDF digitalizado com carimbo de
  assinatura, e 60 páginas sintéticas com teto de 10): `pdfjs-dist` só LÊ PDF, não escreve, então
  esses casos não têm porte direto. O teste Node usa o PDF real `test/fixtures/pdf/98114.pdf` para
  confirmar que a extração funciona ponta a ponta; a lógica de corte/teto/limiar em si
  (`LIMIAR_CHARS_POR_PAGINA`, `TETO_PAGINAS_PDF`, `TETO_SEGUNDOS_PDF`) foi portada linha a linha
  para `extrairTextoPdf` em `server/index.js`, mas não tem teste automatizado equivalente ao dos
  PDFs sintéticos do Python.
- **Fuso horário do `diagnostico_ritmo_tcero`**: o Python formata os incidentes com
  `time.localtime` (hora local da máquina); o Node formata em UTC (`toISOString`). Cosmético (só
  aparece no histórico de incidentes do diagnóstico), mas é uma divergência real de texto — não
  corrigido nesta rodada.
- **Espaçamento/quebra de linha do texto extraído do PDF**: ver a seção do harness — reduzido
  muito nesta rodada (troca de `.join(" ")` por concatenação com `hasEOL`), mas não é garantia de
  byte a byte contra PyMuPDF em todo PDF; documentos perto do teto de orçamento do PDF
  (`ORCAMENTO_PDF`) podem cortar num ponto ligeiramente diferente entre os dois motores.

## Apoie o projeto

O pacote é gratuito e de código aberto, e é mantido no tempo livre de um advogado: cada mudança
do portal do TCE-RO exige diagnóstico, correção, testes e versão nova. Se ele economiza o seu
tempo, você pode apoiar a continuidade do trabalho com qualquer valor, por **Pix**:

> **Chave Pix (e-mail):** `robertogrecia@hotmail.com`

O apoio é voluntário e não muda nada no uso: o pacote continua igual para todos.

## Autor

**Roberto Grécia Bessa** — OAB/RO 7865-A
Instagram: [@robertogrecia](https://instagram.com/robertogrecia)

Irmão dos pacotes de jurisprudência do [TJRO](https://github.com/robertogecia/tjro-jurisprudencia-mcp) e dos servidores do [TRF1](https://github.com/robertogecia/trf1-jurisprudencia-mcp) e do [TJSE](https://github.com/robertogecia/mcp-tjse-jurisprudencia), do mesmo autor.

## Licença

MIT — veja [LICENSE](LICENSE).