Coleta, classifica e rankeia licitações públicas de software — foco inicial em Sergipe, arquitetura preparada para o Brasil inteiro.
Fonte primária: PNCP (Portal Nacional de Contratações Públicas). Pela Lei 14.133/2021 a divulgação no PNCP é condição de eficácia do ato, então ele é um espelho legalmente obrigatório de tudo — não é preciso raspar portal nenhum.
O estudo que fundamenta cada decisão de arquitetura está em ESTUDO.md,
com os números medidos contra a API de produção e os scripts que os produziram em
prototipo/.
cp .env.example .env # ajuste UFS_MONITORADAS e USER_AGENT
docker compose up --buildAbra http://localhost:8000. Na aba Configuração → Operação, clique em carga inicial (12 meses) para a primeira coleta.
python -m venv .venv
.venv/Scripts/pip install -e ".[dev]" # Linux/macOS: .venv/bin/pip
cd frontend && npm install && npm run build && cd ..
.venv/Scripts/python -m backend.mainO SQLite é o padrão: sem Docker, sem Postgres, sem serviço externo. O schema é criado na primeira execução.
Para desenvolver o frontend com hot reload, rode npm run dev em frontend/
(porta 5173, com proxy do /api para o backend na 8000).
Pela interface (Configuração → Operação) ou pela API:
curl -X POST localhost:8000/api/coletar -H 'Content-Type: application/json' \
-d '{"job":"carga_inicial"}'A carga inicial leva alguns minutos. O PNCP falha com frequência (500 de
banco, timeouts); a coleta isola a falha por segmento e o job termina com status
parcial, listando exatamente quais segmentos ficaram de fora. Rodar de novo
costuma resolver — e parcial nunca é reportado como ok, justamente para que a
lacuna não passe por "o PNCP não publicou".
Depois disso o agendador assume:
| Job | Frequência | Por quê |
|---|---|---|
| Propostas abertas | 1 hora | A janela mediana de uma dispensa de software em SE é de 5 dias; 6% têm 3 dias ou menos |
| Publicações recentes | 3 horas | Órgão atrasa publicação e errata republica o edital |
| Detalhe e resultados | diário | Itens, PDFs e vencedores |
| Dossiê do edital | 6 horas | Baixa o PDF e extrai as exigências de habilitação |
| Contratos e atas | semanal | Prospecção antes do edital existir; atas nossas viram adesão |
| Impedimentos CEIS/CNEP | diário | O Portal da Transparência só publica o arquivo do dia corrente |
| PCA | mensal | Intenção de compra do ano |
| Resumo por e-mail | 7h | Para quem prefere e-mail a notificação |
Classificação em duas camadas. Medi 389 itens sergipanos: 0% têm
CATMAT/CATSER preenchido, então não existe filtro estruturado — é texto ou nada.
A primeira camada é regex ponderado (determinístico, auditável, reprocessável); só
a zona de dúvida (~6% dos editais) vai para o LLM. Sem ANTHROPIC_API_KEY o
sistema roda apenas com as regras e a dúvida fica para revisão humana.
Dossiê do edital. O PNCP entrega o edital em /arquivos — PDF nativo, PDF
escaneado ou ZIP com os anexos. O sistema baixa, extrai o texto (testado em
editais reais: ~200 mil caracteres, inclusive de dentro de ZIP) e, com
ANTHROPIC_API_KEY configurada, estrutura as exigências: atestado de capacidade
técnica, capital social mínimo, sede local, prova de conceito, garantia. Essas
exigências entram no componente "ausência de barreiras" do score — é o que evita
montar proposta e descobrir cláusula eliminatória na página 60. Quando o PDF é
escaneado, o sistema diz isso explicitamente em vez de deixar o campo vazio e
parecer falha de coleta.
Atas e adesão ("carona"). Ganhar uma ata não garante faturamento — o órgão compra por demanda. O valor está no art. 86 da Lei 14.133: outro órgão pode aderir a uma ata existente e comprar sem nova licitação. Configure o CNPJ da empresa e o sistema identifica em quais atas vencemos (prospecção de adesão) e quais são de concorrentes (preço praticado e posicionamento).
Impedimentos de licitar (CEIS/CNEP). Serve a dois lados. Sobre a nossa empresa: sanção vigente inabilita em qualquer sessão, e descobrir isso no dia é perder a licitação por algo que se sabia com meses de antecedência — daí o alerta crítico. Sobre concorrentes: impedido declarado vencedor é fundamento de impugnação. Os dois cadastros vêm do download em massa do Portal da Transparência (23.364 + 1.716 registros, sem precisar de chave de API).
O sistema separa impedimento de sanção registrada: multa e publicação extraordinária da condenação constam no cadastro mas não barram a disputa. São 1.655 dos 1.716 registros do CNEP — tratar tudo como impedimento marcaria 96% do cadastro como impedido. No sentido oposto, "declaração de inidoneidade sem prazo determinado" (1.078 registros do CEIS sem data final) é impedimento por tempo indeterminado: ali a ausência de data significa vigente, não ausência de sanção.
Funil de acompanhamento. Analisando → vamos participar → proposta enviada → em disputa → encerrada. Só entra o que a equipe colocou lá: a grade tem centenas de linhas e o funil, dezenas. Cada coluna soma o valor em jogo, e mover para uma etapa marca a oportunidade como interesse — a estrela da grade e o funil contam a mesma história.
Ranking explicável. Seis componentes com pesos configuráveis pela interface: aderência técnica, baixa concorrência, janela de tempo, adequação de valor, ausência de barreiras e proximidade. Cada linha da grade mostra por que está naquela posição.
O sistema mede a si mesmo. A aba Qualidade calcula precisão e recall do classificador contra as correções feitas na grade (metas do estudo: 85% e 95%), a taxa de participação e de vitória, e a correlação entre score e vitória real — se licitação de score alto não é ganha com mais frequência, o ranking está errado e os pesos precisam mudar. Nenhum número aparece sem o tamanho da amostra: com 3 rótulos, 100% de precisão não é informação. Para isso o veredito automático é guardado à parte da correção humana; sem esse par, a correção apagaria justamente a evidência de que a máquina errou.
"Menos concorrentes" é estimativa declarada, não medição. Nenhuma API pública expõe número de participantes — verifiquei nos schemas do PNCP e nos 77 endpoints do Compras.gov.br. Os sinais usados são proxies (exclusividade ME/EPP, dispensa, item deserto/fracassado, praça pouco disputada, portal de nicho). A aba Participações existe para registrar quantos concorrentes realmente apareceram; com algumas dezenas de registros os pesos passam a ser calibrados por evidência em vez de opinião.
backend/
coletores/ cliente PNCP resiliente, mapeamento, persistência, jobs
classificacao/ regras determinísticas + triagem por LLM
enriquecimento/ download do edital, extração de PDF/ZIP e dossiê
api/exportacao.py CSV (BOM + `;` para o Excel pt-BR) e XLSX
ranking/ motor de score explicável
api/ rotas e schemas
qualidade.py precisão/recall do classificador, correlação score x vitória
alertas.py Telegram e e-mail
agendador.py APScheduler
frontend/src/ React + TanStack Table + Tailwind (7 abas: Radar, Funil,
Concorrentes, Prospecção, Participações, Certidões, Qualidade)
alembic/ migrações
prototipo/ scripts que produziram os números do estudo
tests/ unitários, pipeline e API (offline) + testes de rede
.venv/Scripts/python -m pytest # 842 testes, rápido, offline
.venv/Scripts/python -m pytest -m rede # bate na API real do PNCP
.venv/Scripts/python -m ruff check backend/ tests/Os testes de pipeline e de API rodam offline, com payloads reais do PNCP
guardados em tests/fixtures/. Isso foi uma decisão tomada na marra: durante o
desenvolvimento o PNCP caiu no meio de um teste ponta a ponta. Um pipeline que só
pode ser verificado quando um serviço de terceiro está saudável não é verificável.
Os testes do dossiê geram PDFs em memória com a própria PyMuPDF, cobrindo PDF, ZIP de PDFs e PDF escaneado sem binário no repositório.
Todo falso positivo encontrado em coleta real virou teste de regressão — mão
de obra de R$ 47 milhões, "Sistema Único de Saúde", papel A4 para o almoxarifado,
aluguel de impressora térmica, R$ 13 milhões de livros de espanhol que vinham com
"plataforma digital", o SAEB confundido com sistema, e aplicação de provas
confundida com plataforma de provas. O ESTUDO.md (Anexo E) explica cada caso.
Há um teste que percorre todos os padrões do classificador e reprova qualquer
um que contenha caractere de controle ou que não case nem com um texto derivado de
si mesmo. Ele existe porque \b já virou o byte 0x08 duas vezes ao editar as
regras por script: o padrão continua compilando, nunca mais casa com nada, e não
há sintoma — um grupo negativo inteiro ficou inativo em silêncio.
Os testes do CEIS/CNEP carregam os cabeçalhos reais dos dois arquivos, porque
o CEIS tem 24 colunas e o CNEP tem 25: o CNEP insere VALOR DA MULTA na posição
10 e desloca todo o resto. Ler por posição colocaria a data de início do CNEP no
campo da categoria — sem erro nenhum, com dado errado no banco.
Tudo em .env (veja .env.example). O que mais importa:
| Variável | Padrão | Observação |
|---|---|---|
BANCO_URL |
SQLite local | Postgres em produção |
UFS_MONITORADAS |
SE |
Vazio = Brasil inteiro |
USER_AGENT |
— | Coloque um contato real: é API pública, identificar-se é o mínimo |
ANTHROPIC_API_KEY |
vazio | Sem ela, só as regras determinísticas |
TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID |
vazio | Alertas urgentes |
SMTP_* |
vazio | Alertas por e-mail |
Pesos do ranking, faixa de valor, capacidades técnicas e portais credenciados ficam no banco, editáveis pela interface — a equipe muda conforme a estratégia do trimestre e vê o efeito na hora.
- Credenciar no Licitanet — dominante nos municípios de Sergipe (141 dos 640 editais de software medidos).
- SICAF atualizado e certificado digital e-CNPJ A1.
- Certidões em dia com calendário de renovação (federal, FGTS, CNDT, estadual, municipal, falência).
- Emitir atestado de capacidade técnica de todo cliente atual — é o ativo mais escasso de empresa nova e o que mais elimina pequenas na habilitação.
- Enquadramento ME/EPP formalizado: destrava os 30% de itens exclusivos.
O sistema avisa quando a disputa vai acontecer em portal onde a empresa não está credenciada — score 95 em portal inacessível vale zero.
Os dados são públicos por determinação legal e coletá-los é lícito. O sistema
não raspa portais privados: o robots.txt do Licitanet bloqueia /api/ e o
BLL declara sinais contra coleta automatizada — e não é necessário, porque o PNCP
cobre. As chamadas se identificam via USER_AGENT, respeitam intervalo entre
requisições e nunca são retentadas em erro 4xx (que é bug nosso, não instabilidade
deles).