diff --git a/TIPOS_ATUACAO_ELASTICSEARCH.md b/TIPOS_ATUACAO_ELASTICSEARCH.md
new file mode 100644
index 0000000..dbd450c
--- /dev/null
+++ b/TIPOS_ATUACAO_ELASTICSEARCH.md
@@ -0,0 +1,407 @@
+# Tipos de Atuação Disponíveis no Elasticsearch (AtuaCAPES)
+
+> Mapeamento completo dos tipos de atuação encontrados no índice `atuacapes` do Elasticsearch CAPES
+
+## Visão Geral
+
+Este documento lista todos os tipos de atuação (`atuacoes.tipo`) disponíveis no índice Elasticsearch do sistema AtuaCAPES, identificados através da análise do código implementado e da documentação do projeto.
+
+---
+
+## Tipos de Atuação Implementados
+
+### Componente A - Coordenação CAPES
+
+| Tipo | Descrição | Boost | Pontuação Base |
+|------|-----------|-------|----------------|
+| `Coordenação de Área de Avaliação` | Coordenadores ativos (CA, CAJ, CAJ-MP, CAM) | 10 | 200-100 pts |
+| `Histórico de Coordenação de Área de Avaliação` | Coordenadores históricos (mandatos encerrados) | 5 | 200-100 pts |
+
+**Subtipos identificados em `dadosCoordenacaoArea.tipo`:**
+- `Coordenador de Área` (CA) - Base: 200 pts
+- `Coordenador Adjunto` (CAJ) - Base: 150 pts
+- `Coordenador Adjunto de Mestrado Profissionalizante` (CAJ-MP) - Base: 120 pts
+- `Coordenador de Câmara Temática` (CAM) - Base: 100 pts
+
+### Componente C - Consultoria
+
+| Tipo | Descrição | Boost | Pontuação Base |
+|------|-----------|-------|----------------|
+| `Consultor` | Registro de consultoria ativa ou histórica | 2 | 150 pts (ativo) |
+| `Histórico de Consultoria` | Consultorias encerradas | 1 | 100 pts (histórico) |
+
+**Valores de `situacaoConsultoria`:**
+- `Atividade Contínua` - Consultor Ativo
+- `Ativo` - Consultor Ativo
+- Outros valores ou com `inativacaoSituacao` preenchido = Histórico
+
+### Componente D - Premiações
+
+| Tipo | Descrição | Boost | Pontuação |
+|------|-----------|-------|-----------|
+| `Premiação Prêmio` | Prêmio recebido | 3 | 60-180 pts |
+| `Avaliação Prêmio` | Participação em avaliação/comissão | 2 | 2-115 pts |
+| `Inscrição Prêmio` | Inscrição em prêmio | 1 | 1-2 pts |
+
+**Níveis de Premiação:**
+
+#### Nível 1 - Grande Prêmio CAPES
+- `dadosPremiacaoPremio.premiacao`: "Grande Prêmio"
+- Pontuação: 150 + 50 = 200 pts (limitado a 180)
+
+#### Nível 1 - Prêmio CAPES de Tese (PCT)
+- `dadosPremiacaoPremio.premio`: "PCT"
+- `dadosPremiacaoPremio.premiacao`: "Prêmio" (+25) ou "Menção Honrosa" (+15)
+- Pontuação base: 100 pts (máx 150 pts)
+
+#### Nível 2 - Institucionais
+- Interfarma, Vale-CAPES, etc.
+- Base: 30 pts
+- Prêmio: +20 pts / Menção: +10 pts
+- Máximo: 60 pts
+
+#### Nível 3 - Autoinscrição
+- CDTDN, Vínculos Familiares, etc.
+- Base: 10 pts
+- Prêmio: +5 pts / Menção: +3 pts
+- Máximo: 20 pts
+
+### Outros Tipos Identificados
+
+| Tipo | Descrição | Uso no Ranking |
+|------|-----------|----------------|
+| `Evento` | Participação em eventos SAE | Sim - bônus consultoria |
+
+**Campos relacionados a eventos:**
+- `dadosEvento.consultorResponsavel`: "Sim" ou "Não"
+- Eventos recentes (últimos 2 anos): bonus para consultoria ativa
+- Responsável por evento: +5 pts por vez (máx 25)
+
+---
+
+## Estrutura de Dados por Componente
+
+### Coordenação CAPES
+
+```json
+{
+ "tipo": "Coordenação de Área de Avaliação",
+ "inicio": "01/01/2020",
+ "fim": null,
+ "dadosCoordenacaoArea": {
+ "tipo": "Coordenador de Área",
+ "areaAvaliacao": {
+ "id": 123,
+ "nome": "CIÊNCIAS AMBIENTAIS"
+ },
+ "inicioVinculacao": "01/01/2020",
+ "fimVinculacao": null,
+ "colegio": [
+ {"nome": "Colégio de Ciências Exatas, Tecnológicas e Multidisciplinar"}
+ ],
+ "portaria": "123/2020",
+ "ies": {
+ "nome": "Universidade Federal do Brasil",
+ "sigla": "UFB"
+ }
+ }
+}
+```
+
+### Consultoria
+
+```json
+{
+ "tipo": "Consultor",
+ "inicio": "15/03/2018",
+ "fim": null,
+ "dadosConsultoria": {
+ "situacaoConsultoria": "Atividade Contínua",
+ "inicioSituacao": "15/03/2018",
+ "inativacaoSituacao": null,
+ "ies": {
+ "nome": "Universidade Federal do Brasil",
+ "sigla": "UFB"
+ },
+ "areaConhecimentoPos": [
+ {
+ "areaAvaliacao": {
+ "nome": "CIÊNCIAS AMBIENTAIS"
+ }
+ }
+ ],
+ "areaPesquisa": [
+ {"descricao": "Mudanças Climáticas"}
+ ]
+ }
+}
+```
+
+### Premiações
+
+```json
+{
+ "tipo": "Premiação Prêmio",
+ "dadosPremiacaoPremio": {
+ "nomePremio": "Prêmio CAPES de Tese",
+ "premio": "PCT",
+ "premiacao": "Prêmio",
+ "ano": 2022,
+ "areaConhecimento": {
+ "areaAvaliacao": {
+ "nome": "CIÊNCIAS AMBIENTAIS"
+ }
+ },
+ "papelPessoa": "Autor"
+ }
+}
+```
+
+### Participação em Prêmios
+
+```json
+{
+ "tipo": "Avaliação Prêmio",
+ "dadosParticipacaoPremio": {
+ "tipo": "Membro de Comissão",
+ "premio": "PCT",
+ "ano": 2023,
+ "comissao": {
+ "nome": "Comissão Julgadora",
+ "tipo": "Avaliação"
+ },
+ "areaAvaliacao": {
+ "nome": "CIÊNCIAS AMBIENTAIS"
+ }
+ }
+}
+```
+
+### Eventos
+
+```json
+{
+ "tipo": "Evento",
+ "inicio": "10/05/2023",
+ "fim": "15/05/2023",
+ "dadosEvento": {
+ "consultorResponsavel": "Sim",
+ "nome": "Seminário de Avaliação",
+ "tipo": "Presencial"
+ }
+}
+```
+
+---
+
+## Query de Agregação - Descobrir Todos os Tipos
+
+Para descobrir todos os tipos de atuação disponíveis no índice:
+
+```json
+{
+ "size": 0,
+ "aggs": {
+ "tipos": {
+ "nested": {"path": "atuacoes"},
+ "aggs": {
+ "tipos_unicos": {
+ "terms": {
+ "field": "atuacoes.tipo.keyword",
+ "size": 100
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+---
+
+## Mapeamento de Tipos vs. Componentes de Pontuação
+
+| Componente | Tipos Relacionados | Pontuação Máxima |
+|------------|-------------------|------------------|
+| **A - Coordenação CAPES** | `Coordenação de Área de Avaliação`
`Histórico de Coordenação de Área de Avaliação` | 450 pts (CA) |
+| **B - Coordenação PPG** | *Não existe no ES - usar Oracle* | 180 pts |
+| **C - Consultoria** | `Consultor`
`Histórico de Consultoria`
`Evento` (bônus) | 230 pts |
+| **D - Premiações** | `Premiação Prêmio`
`Avaliação Prêmio`
`Inscrição Prêmio` | 180 pts |
+
+---
+
+## Campos Faltantes / Limitações
+
+### Não Disponíveis no Elasticsearch
+
+1. **Coordenação de Programa (PPG)**
+ - Não existe tipo `atuacoes.tipo` para coordenação de programa
+ - Fonte alternativa: Oracle `SUCUPIRA_PAINEL.VM_COORDENADOR`
+
+2. **Nota do PPG**
+ - Não disponível em `dadosGestaoPrograma`
+ - Fonte alternativa: Oracle `VM_PROGRAMA_SUCUPIRA.DS_CONCEITO`
+
+3. **Dados detalhados de Câmara Temática**
+ - Inferido pelo campo `dadosCoordenacaoArea.tipo` ou `nome`
+ - Pode haver imprecisão na classificação
+
+4. **Situação "Falecido"**
+ - Existe `dadosPessoais.anoObito` mas não `situacaoConsultoria="Falecido"`
+
+---
+
+## Implementação no Código
+
+### Arquivo: `backend/src/infrastructure/elasticsearch/client.py`
+
+Query principal com boost por tipo:
+
+```python
+async def buscar_candidatos_ranking(self, size: int = 1000) -> List[Dict[str, Any]]:
+ query = {
+ "size": size,
+ "query": {
+ "bool": {
+ "should": [
+ # Coordenação CAPES
+ {
+ "nested": {
+ "path": "atuacoes",
+ "query": {
+ "bool": {
+ "should": [
+ {"term": {"atuacoes.tipo": {"value": "Coordenação de Área de Avaliação", "boost": 10}}},
+ {"term": {"atuacoes.tipo": {"value": "Histórico de Coordenação de Área de Avaliação", "boost": 5}}}
+ ]
+ }
+ },
+ "score_mode": "sum"
+ }
+ },
+ # Consultoria
+ {
+ "nested": {
+ "path": "atuacoes",
+ "query": {
+ "bool": {
+ "should": [
+ {"term": {"atuacoes.tipo": {"value": "Consultor", "boost": 2}}},
+ {"term": {"atuacoes.tipo": {"value": "Histórico de Consultoria", "boost": 1}}}
+ ]
+ }
+ },
+ "score_mode": "sum"
+ }
+ },
+ # Premiações
+ {
+ "nested": {
+ "path": "atuacoes",
+ "query": {
+ "bool": {
+ "should": [
+ {"term": {"atuacoes.tipo": {"value": "Premiação Prêmio", "boost": 3}}},
+ {"term": {"atuacoes.tipo": {"value": "Avaliação Prêmio", "boost": 2}}},
+ {"term": {"atuacoes.tipo": {"value": "Inscrição Prêmio", "boost": 1}}}
+ ]
+ }
+ },
+ "score_mode": "sum"
+ }
+ }
+ ],
+ "minimum_should_match": 1
+ }
+ },
+ "_source": ["id", "dadosPessoais", "atuacoes"],
+ "sort": [{"_score": "desc"}]
+ }
+```
+
+### Arquivo: `backend/src/infrastructure/repositories/consultor_repository_impl.py`
+
+Extração de tipos:
+
+```python
+# Consultoria
+consultorias = [
+ a for a in atuacoes
+ if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]
+]
+
+# Coordenação CAPES
+coordenacoes = [
+ a for a in atuacoes
+ if a.get("tipo") in [
+ "Coordenação de Área de Avaliação",
+ "Histórico de Coordenação de Área de Avaliação",
+ ]
+]
+
+# Eventos (bônus consultoria)
+eventos_sae = [
+ a for a in atuacoes
+ if a.get("tipo") == "Evento"
+]
+
+# Premiações (todos os tipos são processados genericamente)
+premiacoes = [
+ a for a in atuacoes
+ if a.get("dadosPremiacaoPremio") or
+ a.get("dadosParticipacaoPremio") or
+ a.get("dadosParticipacaoInscricaoPremio")
+]
+```
+
+---
+
+## Estatísticas
+
+### Estimativas de Volume (baseado em documentação)
+
+| Tipo | Quantidade Estimada |
+|------|---------------------|
+| Coordenadores de área (ativos) | ~200 |
+| Coordenadores históricos | ~322 |
+| Consultores | ~52.551 |
+| Premiações | ~63.799 |
+| **Total de registros com atuações** | **~90.482** |
+
+### Boost Aplicado
+
+O boost é usado para priorizar candidatos no score inicial do Elasticsearch:
+
+| Tipo | Boost | Justificativa |
+|------|-------|---------------|
+| Coordenação de Área (ativa) | 10 | Máxima prioridade (200-450 pts) |
+| Coordenação de Área (histórica) | 5 | Alta prioridade histórica |
+| Premiação Prêmio | 3 | Mérito significativo |
+| Consultor | 2 | Base importante |
+| Avaliação Prêmio | 2 | Participação ativa |
+| Histórico de Consultoria | 1 | Relevância histórica |
+| Inscrição Prêmio | 1 | Participação básica |
+
+---
+
+## Notas de Implementação
+
+1. **Paginação**: Query principal retorna top 1000 candidatos ordenados por score ES
+2. **Recálculo**: Python recalcula pontuação completa e reordena por pontuação real
+3. **Cache**: Resultados cacheados por 5 minutos (TTL=300s)
+4. **Performance**: Primeira requisição ~1m34s, cacheadas ~0.27s
+5. **Timeout**: 120s para queries ES
+
+---
+
+## Referências
+
+- **Documentação**: `/home/fred/projetos/ranking/.claude/rules/ranking-queries-elasticsearch.md`
+- **Critérios**: `/home/fred/projetos/ranking/.claude/rules/ranking-consultores-capes.md`
+- **Implementação ES**: `/home/fred/projetos/ranking/backend/src/infrastructure/elasticsearch/client.py`
+- **Extração**: `/home/fred/projetos/ranking/backend/src/infrastructure/repositories/consultor_repository_impl.py`
+
+---
+
+**Última atualização**: 2025-12-13
+**Índice Elasticsearch**: `atuacapes` (servidor: `elastic-atuacapes.hom.capes.gov.br:9200`)
diff --git a/TOP_10_RANKING_CAPES.md b/TOP_10_RANKING_CAPES.md
new file mode 100644
index 0000000..1836915
--- /dev/null
+++ b/TOP_10_RANKING_CAPES.md
@@ -0,0 +1,291 @@
+# TOP 10 CONSULTORES - RANKING CAPES
+
+**Data da Consulta:** 13/12/2025
+**Base de Dados:** Elasticsearch AtuaCAPES (índice: atuacapes)
+**Total de Candidatos Analisados:** 100
+**Critérios:** Sistema de Ranking Integrado CAPES v1.0
+
+---
+
+## Resumo Executivo
+
+O ranking foi calculado com base em 4 componentes principais:
+
+- **Componente A:** Coordenação CAPES (máx 450 pts) - CA, CAJ, CAJ-MP, CAM
+- **Componente B:** Coordenação de Programa PPG (máx 180 pts) - *Não disponível nesta consulta*
+- **Componente C:** Consultoria (máx 230 pts)
+- **Componente D:** Premiações (máx 180 pts)
+
+**Pontuação Máxima Teórica:** 1.040 pontos
+**Pontuação Máxima Observada:** 685 pontos (65,9% do máximo)
+
+---
+
+## Top 10 Consultores
+
+### 1º LUGAR - EDSON APARECIDO MITISHITA
+**ID:** 519524
+**Pontuação Total:** 685 pontos
+
+**Breakdown de Pontuação:**
+- **Componente A (Coordenação CAPES):** 340 pts
+ - Base: 200 (Coordenador de Área)
+ - Tempo: 90 (9 anos completos)
+ - Áreas Adicionais: 0
+ - Bônus Atualidade: 30 (coordenação ativa)
+ - Retorno: 20 (já coordenou antes)
+
+- **Componente B (Coordenação PPG):** 0 pts
+
+- **Componente C (Consultoria):** 165 pts
+ - Base: 100 (histórico)
+ - Tempo: 50 (10+ anos)
+ - Total eventos: 2
+ - Eventos recentes: 0 (último há mais de 2 anos)
+ - Bônus continuidade: 15 (8+ anos consecutivos)
+
+- **Componente D (Premiações):** 180 pts
+ - Total de premiações: 7
+
+**Perfil:**
+- Anos de atuação: 11,3 anos
+- Status: Inativo (sem eventos recentes)
+- Veterano: Sim
+- Área principal: GEOCIÊNCIAS
+- Coordenações: 3 (incluindo 1 ativa)
+
+**Destaque:** Possui a maior pontuação no Componente A devido à combinação de coordenação ativa (bônus de 30 pts) + retorno (20 pts) + 9 anos completos de experiência.
+
+---
+
+### 2º LUGAR - ANDRE MOREIRA CUNHA
+**ID:** 45997
+**Pontuação Total:** 675 pontos
+
+**Breakdown de Pontuação:**
+- **Componente A:** 330 pts
+ - Base: 200
+ - Tempo: 100 (10 anos - máximo permitido)
+ - Bônus Atualidade: 30
+ - Retorno: 0 (sem retorno)
+
+- **Componente C:** 165 pts
+- **Componente D:** 180 pts (7 premiações)
+
+**Perfil:**
+- Anos de atuação: 14,2 anos
+- Status: Inativo
+- Área principal: ECONOMIA
+- Coordenações: 2 (1 ativa)
+
+**Destaque:** Atingiu o teto máximo de pontuação por tempo (100 pts) no Componente A.
+
+---
+
+### 3º LUGAR - FLAVIO AUGUSTO SENRA RIBEIRO
+**ID:** 7794
+**Pontuação Total:** 665 pontos
+
+**Breakdown de Pontuação:**
+- **Componente A:** 320 pts
+ - Base: 200
+ - Tempo: 100
+ - Áreas Adicionais: 20 (coordenou 2 áreas diferentes)
+ - Bônus Atualidade: 0 (coordenação encerrada)
+ - Retorno: 0
+
+- **Componente C:** 165 pts
+- **Componente D:** 180 pts (11 premiações)
+
+**Perfil:**
+- Anos de atuação: 14,5 anos
+- Status: Inativo
+- Áreas: FILOSOFIA, CIÊNCIAS DA RELIGIÃO E TEOLOGIA
+- Coordenações: 3 (todas encerradas)
+
+**Destaque:** Único no top 10 com pontuação por áreas adicionais (coordenou múltiplas áreas). Maior número de premiações entre os 10 primeiros (11 premiações).
+
+---
+
+### 4º ao 10º LUGAR (Empate em 655 pontos)
+
+Os consultores da 4ª à 10ª posição compartilham a mesma pontuação total de **655 pontos**, com o seguinte padrão:
+
+**Componente A:** 310 pts
+- Base: 200
+- Tempo: 60 (6 anos)
+- Bônus Atualidade: 30
+- Retorno: 20
+
+**Componente C:** 165 pts
+**Componente D:** 180 pts
+
+---
+
+#### 4º - CARLOS FREDERICO MARTINS MENCK
+**ID:** 20912
+**Área:** CIÊNCIAS BIOLÓGICAS I
+**Anos:** 14,5 | **Status:** Inativo
+**Premiações:** 25 (maior número absoluto)
+**Coordenações:** 2 (1 ativa)
+
+---
+
+#### 5º - MARCELO ALBANO MORET SIMOES GONCALVES
+**ID:** 5888
+**Área:** INTERDISCIPLINAR
+**Anos:** 11,0 | **Status:** Ativo ✓
+**Premiações:** 9
+**Eventos recentes:** 2
+**Coordenações:** 2 (1 ativa)
+
+---
+
+#### 6º - CARLOS PELLESCHI TABORDA
+**ID:** 15100
+**Área:** CIÊNCIAS BIOLÓGICAS III
+**Anos:** 12,6 | **Status:** Inativo
+**Premiações:** 15
+**Coordenações:** 2 (1 ativa)
+
+---
+
+#### 7º - CLÁUDIA LUCIA DE MORAES FORJAZ
+**ID:** 14569
+**Área:** EDUCAÇÃO FÍSICA, FISIOTERAPIA, FONOAUDIOLOGIA E TERAPIA OCUPACIONAL
+**Anos:** 14,5 | **Status:** Inativo
+**Premiações:** 9
+**Coordenações:** 2 (1 ativa)
+
+---
+
+#### 8º - ALTAIR ANTONINHA DEL BEL CURY
+**ID:** 8639
+**Área:** ODONTOLOGIA
+**Anos:** 12,1 | **Status:** Ativo ✓
+**Premiações:** 11
+**Eventos recentes:** 2
+**Coordenações:** 2 (1 ativa)
+
+---
+
+#### 9º - DEBORA FOGUEL
+**ID:** 12271
+**Área:** CIÊNCIAS BIOLÓGICAS II
+**Anos:** 14,5 | **Status:** Inativo
+**Premiações:** 7
+**Coordenações:** 2 (1 ativa)
+
+---
+
+#### 10º - MARCELO TÁVORA MIRA
+**ID:** 509665
+**Área:** MEDICINA I
+**Anos:** 11,3 | **Status:** Ativo ✓
+**Premiações:** 7
+**Eventos recentes:** 1
+**Coordenações:** 2 (1 ativa)
+
+---
+
+## Análise Estatística
+
+### Distribuição de Pontuação
+
+| Posição | Pontuação | Diferença para 1º |
+|---------|-----------|-------------------|
+| 1º | 685 | - |
+| 2º | 675 | -10 |
+| 3º | 665 | -20 |
+| 4º-10º | 655 | -30 |
+
+**Média (top 10):** 661 pontos
+**Desvio padrão:** ~10 pontos
+
+### Componentes - Análise
+
+**Componente A (Coordenação CAPES):**
+- Média: 319 pts (71% do máximo)
+- Variação: 310-340 pts
+- Padrão dominante: CA (Coordenador de Área) com coordenação ativa
+
+**Componente B (Coordenação PPG):**
+- Todos: 0 pts (dados não disponíveis no Elasticsearch)
+
+**Componente C (Consultoria):**
+- Todos: 165 pts
+- Padrão: Base 100 + Tempo 50 + Bônus continuidade 15
+- Uniformidade total no top 10
+
+**Componente D (Premiações):**
+- Todos: 180 pts (máximo permitido)
+- Média de premiações: 10,3 por consultor
+- Variação: 7-25 premiações
+
+### Perfil dos Líderes
+
+**Anos de Atuação:**
+- Média: 12,7 anos
+- Variação: 11,0 - 14,5 anos
+- Todos são veteranos (10+ anos)
+
+**Status de Atividade:**
+- Ativos: 3 (30%)
+- Inativos: 7 (70%)
+
+**Áreas de Conhecimento:**
+- Ciências Biológicas: 3 consultores
+- Áreas diversas: 7 consultores
+
+**Padrão de Coordenação:**
+- Todos possuem coordenação ATIVA no momento
+- Todos exceto 1 tiveram retorno à coordenação
+- Média de 2,3 coordenações por consultor
+
+---
+
+## Observações Técnicas
+
+### Limitações da Análise
+
+1. **Componente B = 0:** O Elasticsearch não contém dados de coordenação de PPG. Esses dados estão no Oracle (SUCUPIRA_PAINEL). Se incluídos, o ranking poderia mudar significativamente.
+
+2. **Amostra de 100 candidatos:** A query com boost buscou apenas 100 candidatos pré-ordenados pelo Elasticsearch. É possível que candidatos com alta pontuação real estejam fora dessa amostra.
+
+3. **Dados de Consultoria incompletos:** O campo "áreas" da consultoria retornou vazio para todos os consultores, sugerindo possível problema na extração de dados do Elasticsearch.
+
+### Critérios de Desempate
+
+Quando há empate na pontuação total (casos 4º-10º), a ordenação segue a ordem de retorno do Elasticsearch, que não necessariamente reflete um critério definido.
+
+**Sugestão:** Implementar critério de desempate explícito:
+1. Componente A (maior)
+2. Anos de atuação (maior)
+3. Status ativo (preferencial)
+4. ID menor (mais antigo no sistema)
+
+### Validação dos Cálculos
+
+Os cálculos seguem rigorosamente os critérios definidos em:
+- `/home/fred/projetos/ranking/.claude/rules/ranking-consultores-capes.md`
+- `/home/fred/projetos/ranking/.claude/rules/ranking-queries-implementadas.md`
+
+**Implementação:** `CalculadorPontuacao` em `/home/fred/projetos/ranking/backend/src/domain/services/calculador_pontuacao.py`
+
+---
+
+## Próximos Passos Recomendados
+
+1. **Integrar dados do Oracle** para calcular Componente B (Coordenação PPG)
+2. **Expandir amostra** para 1.000+ candidatos para garantir cobertura completa
+3. **Corrigir extração de áreas** na consultoria
+4. **Implementar critério de desempate** explícito
+5. **Validar dados de premiações** (verificar se todas as 7-25 premiações são válidas)
+
+---
+
+**Script de Geração:**
+`/home/fred/projetos/ranking/backend/scripts/top10_ranking.py`
+
+**Método de Consulta:**
+Query Elasticsearch com boost por tipo de atuação (conforme especificação técnica)
diff --git a/backend/scripts/analise_detalhada.py b/backend/scripts/analise_detalhada.py
new file mode 100644
index 0000000..5964c74
--- /dev/null
+++ b/backend/scripts/analise_detalhada.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+import sys
+import os
+import asyncio
+import json
+from datetime import datetime
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
+
+from infrastructure.elasticsearch.client import ElasticsearchClient
+
+ES_URL = "http://elastic-atuacapes.hom.capes.gov.br:9200"
+ES_INDEX = "atuacapes"
+ES_USER = "admin-atuacapes"
+ES_PASSWORD = "O}!S0bj%FhJ:"
+
+IDS = [12932, 6273]
+
+
+async def buscar_por_id(es_client, id_pessoa):
+ response = await es_client.client.post(
+ f"{es_client.url}/{es_client.index}/_search",
+ json={
+ "size": 1,
+ "query": {"term": {"id": id_pessoa}},
+ "_source": ["id", "dadosPessoais", "atuacoes"]
+ }
+ )
+ response.raise_for_status()
+ result = response.json()
+ hits = result.get("hits", {}).get("hits", [])
+ return hits[0]["_source"] if hits else None
+
+
+async def main():
+ print("Conectando ao Elasticsearch...")
+ es_client = ElasticsearchClient(ES_URL, ES_INDEX, ES_USER, ES_PASSWORD)
+ await es_client.connect()
+
+ try:
+ for id_pessoa in IDS:
+ print(f"\n{'='*120}")
+ print(f"ANÁLISE DETALHADA - ID: {id_pessoa}")
+ print(f"{'='*120}")
+
+ doc = await buscar_por_id(es_client, id_pessoa)
+ if not doc:
+ print(f" Não encontrado")
+ continue
+
+ dados_pessoais = doc.get("dadosPessoais", {})
+ nome = dados_pessoais.get("nome", "N/A")
+ atuacoes = doc.get("atuacoes", [])
+
+ print(f"\nNOME: {nome}")
+ print(f"ID: {id_pessoa}")
+ print(f"CPF: {dados_pessoais.get('cpf', 'N/A')}")
+ print(f"\nTOTAL DE ATUAÇÕES: {len(atuacoes)}")
+
+ por_tipo = {}
+ for a in atuacoes:
+ tipo = a.get("tipo", "Desconhecido")
+ por_tipo[tipo] = por_tipo.get(tipo, 0) + 1
+
+ print(f"\nDISTRIBUIÇÃO POR TIPO:")
+ for tipo, count in sorted(por_tipo.items()):
+ print(f" {tipo}: {count}")
+
+ print(f"\n{'='*120}")
+ print("EVENTOS DETALHADOS")
+ print(f"{'='*120}")
+
+ eventos = [a for a in atuacoes if a.get("tipo") == "Evento"]
+ print(f"\nTOTAL DE EVENTOS: {len(eventos)}")
+
+ if eventos:
+ por_descricao = {}
+ for e in eventos:
+ desc = e.get("descricao", "Sem descrição")
+ por_descricao[desc] = por_descricao.get(desc, 0) + 1
+
+ print(f"\nEVENTOS POR DESCRIÇÃO:")
+ for desc, count in sorted(por_descricao.items(), key=lambda x: x[1], reverse=True):
+ print(f" {desc}: {count}")
+
+ print(f"\nPRIMEIROS 10 EVENTOS:")
+ for idx, e in enumerate(eventos[:10], 1):
+ print(f"\n Evento {idx}:")
+ print(f" Descrição: {e.get('descricao', 'N/A')}")
+ print(f" Início: {e.get('inicio', 'N/A')}")
+ print(f" Fim: {e.get('fim', 'N/A')}")
+ print(f" Nome: {e.get('nome', 'N/A')}")
+ if e.get("dadosEvento"):
+ print(f" Dados Evento: {json.dumps(e['dadosEvento'], indent=8, ensure_ascii=False)[:200]}")
+
+ print(f"\n{'='*120}")
+ print("COORDENAÇÕES CAPES")
+ print(f"{'='*120}")
+
+ coordenacoes = [a for a in atuacoes if a.get("tipo") in [
+ "Coordenação de Área de Avaliação",
+ "Histórico de Coordenação de Área de Avaliação"
+ ]]
+
+ for idx, coord in enumerate(coordenacoes, 1):
+ print(f"\nCoordenação {idx}:")
+ print(f" Tipo: {coord.get('tipo')}")
+ print(f" Nome: {coord.get('nome', 'N/A')}")
+ print(f" Descrição: {coord.get('descricao', 'N/A')}")
+ print(f" Início: {coord.get('inicio', 'N/A')}")
+ print(f" Fim: {coord.get('fim', 'N/A')}")
+
+ dados = coord.get("dadosCoordenacaoArea") or coord.get("dadosHistoricoCoordenacaoArea")
+ if dados:
+ print(f" Dados detalhados:")
+ print(f" Tipo função: {dados.get('tipo', 'N/A')}")
+ area = dados.get("areaAvaliacao", {})
+ if isinstance(area, dict):
+ print(f" Área: {area.get('nome', 'N/A')} (ID: {area.get('id', 'N/A')})")
+ else:
+ print(f" Área: {area}")
+ colegios = dados.get("colegio", [])
+ if colegios:
+ print(f" Colégios: {[c.get('nome') for c in colegios if isinstance(c, dict)]}")
+
+ print(f"\n{'='*120}")
+ print("CONSULTORIAS")
+ print(f"{'='*120}")
+
+ consultorias = [a for a in atuacoes if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]]
+ for idx, cons in enumerate(consultorias, 1):
+ print(f"\nConsultoria {idx}:")
+ print(f" Tipo: {cons.get('tipo')}")
+ print(f" Início: {cons.get('inicio', 'N/A')}")
+ print(f" Fim: {cons.get('fim', 'N/A')}")
+ dados = cons.get("dadosConsultoria", {})
+ if dados:
+ print(f" Situação: {dados.get('situacaoConsultoria', 'N/A')}")
+ print(f" Início situação: {dados.get('inicioSituacao', 'N/A')}")
+ print(f" Inativação: {dados.get('inativacaoSituacao', 'N/A')}")
+
+ print(f"\n{'='*120}")
+ print("PREMIAÇÕES")
+ print(f"{'='*120}")
+
+ premiacoes = [a for a in atuacoes if "Prêmio" in a.get("tipo", "")]
+ print(f"\nTOTAL DE PREMIAÇÕES: {len(premiacoes)}")
+
+ por_tipo_prem = {}
+ for p in premiacoes:
+ tipo = p.get("tipo", "Desconhecido")
+ por_tipo_prem[tipo] = por_tipo_prem.get(tipo, 0) + 1
+
+ print(f"\nPREMIAÇÕES POR TIPO:")
+ for tipo, count in sorted(por_tipo_prem.items()):
+ print(f" {tipo}: {count}")
+
+ for tipo_prem in ["Premiação Prêmio", "Avaliação Prêmio", "Inscrição Prêmio"]:
+ prems = [p for p in premiacoes if p.get("tipo") == tipo_prem]
+ if prems:
+ print(f"\n{tipo_prem} ({len(prems)}):")
+ for idx, p in enumerate(prems[:5], 1):
+ print(f" {idx}. {p.get('descricao', 'N/A')} ({p.get('inicio', 'N/A')})")
+
+ finally:
+ await es_client.close()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/backend/scripts/buscar_consultores_especificos.py b/backend/scripts/buscar_consultores_especificos.py
new file mode 100644
index 0000000..6f10e39
--- /dev/null
+++ b/backend/scripts/buscar_consultores_especificos.py
@@ -0,0 +1,296 @@
+#!/usr/bin/env python3
+import sys
+import os
+import asyncio
+from datetime import datetime
+from typing import List, Dict, Any
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
+
+from infrastructure.elasticsearch.client import ElasticsearchClient
+from domain.services.calculador_pontuacao import CalculadorPontuacao
+from domain.entities.consultor import Consultor, CoordenacaoCapes, Consultoria, Premiacao, Periodo
+
+ES_URL = "http://elastic-atuacapes.hom.capes.gov.br:9200"
+ES_INDEX = "atuacapes"
+ES_USER = "admin-atuacapes"
+ES_PASSWORD = "O}!S0bj%FhJ:"
+
+NOMES_BUSCAR = [
+ "Veronica Maria de Araujo Calado",
+ "Manoel Damiao de Sousa Neto"
+]
+
+
+def parse_date(date_str):
+ if not date_str:
+ return None
+ try:
+ if '/' in date_str:
+ return datetime.strptime(date_str[:10], '%d/%m/%Y')
+ else:
+ return datetime.strptime(date_str[:10], '%Y-%m-%d')
+ except:
+ return None
+
+
+def extrair_coordenacoes_capes(atuacoes: List[Dict]) -> List[CoordenacaoCapes]:
+ coordenacoes_data = [
+ a for a in atuacoes
+ if a.get("tipo") in [
+ "Coordenação de Área de Avaliação",
+ "Histórico de Coordenação de Área de Avaliação",
+ ]
+ ]
+
+ result = []
+ for coord in coordenacoes_data:
+ dados = coord.get("dadosCoordenacaoArea") or coord.get("dadosHistoricoCoordenacaoArea") or {}
+
+ nome = coord.get("nome", "")
+ if "câmara" in nome.lower() or "camara" in nome.lower():
+ tipo = "CAM"
+ elif "mestrado profissional" in nome.lower():
+ tipo = "CAJ-MP"
+ elif "adjunt" in nome.lower():
+ tipo = "CAJ"
+ else:
+ tipo = "CA"
+
+ inicio = parse_date(coord.get("inicio") or dados.get("inicio"))
+ fim = parse_date(coord.get("fim") or dados.get("fim"))
+
+ if not inicio:
+ continue
+
+ area = dados.get("areaAvaliacao", {})
+ if isinstance(area, dict):
+ area_nome = area.get("nome", "N/A")
+ else:
+ area_nome = str(area) if area else "N/A"
+
+ periodo = Periodo(inicio=inicio, fim=fim)
+
+ result.append(CoordenacaoCapes(
+ tipo=tipo,
+ area_avaliacao=area_nome,
+ periodo=periodo,
+ areas_adicionais=[],
+ ja_coordenou_antes=False
+ ))
+
+ return result
+
+
+def extrair_consultoria(atuacoes: List[Dict]) -> Consultoria:
+ consultorias = [
+ a for a in atuacoes
+ if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]
+ ]
+
+ if not consultorias:
+ return None
+
+ datas = []
+ areas = set()
+ total_eventos = len(consultorias)
+ vezes_responsavel = 0
+
+ for cons in consultorias:
+ dados = cons.get("dadosConsultoria", {})
+
+ inicio = parse_date(cons.get("inicio"))
+ fim = parse_date(cons.get("fim"))
+
+ if inicio:
+ datas.append(inicio)
+ if fim:
+ datas.append(fim)
+
+ area = dados.get("areaAvaliacao")
+ if area:
+ areas.add(area)
+
+ if dados.get("responsavel"):
+ vezes_responsavel += 1
+
+ if not datas:
+ return None
+
+ primeiro_evento = min(datas)
+ ultimo_evento = max(datas)
+
+ limite_recente = datetime.now().replace(year=datetime.now().year - 2)
+ eventos_recentes = sum(1 for d in datas if d >= limite_recente)
+
+ anos = (datetime.now() - primeiro_evento).days / 365.25
+
+ return Consultoria(
+ total_eventos=total_eventos,
+ eventos_recentes=eventos_recentes,
+ primeiro_evento=primeiro_evento,
+ ultimo_evento=ultimo_evento,
+ areas=list(areas),
+ situacao="Ativo" if eventos_recentes > 0 else "Histórico",
+ anos_completos=int(anos),
+ anos_consecutivos=int(anos),
+ retornos=0
+ )
+
+
+def extrair_premiacoes(atuacoes: List[Dict]) -> List[Premiacao]:
+ premiacoes_data = [
+ a for a in atuacoes
+ if a.get("tipo") in [
+ "Premiação Prêmio",
+ "Avaliação Prêmio",
+ "Inscrição Prêmio",
+ ]
+ ]
+
+ result = []
+ mapa_pontos = {
+ "Premiação Prêmio": 60,
+ "Avaliação Prêmio": 40,
+ "Inscrição Prêmio": 20,
+ }
+
+ for prem in premiacoes_data:
+ tipo = prem.get("tipo", "")
+ dados = prem.get("dadosPremiacaoPremio") or prem.get("dadosParticipacaoPremio") or prem.get("dadosParticipacaoInscricaoPremio") or {}
+
+ nome_premio = dados.get("premio", "") or prem.get("descricao", "")
+ ano_str = dados.get("ano") or prem.get("inicio", "")
+
+ try:
+ ano = int(ano_str) if isinstance(ano_str, (int, str)) and str(ano_str).isdigit() else 0
+ except:
+ ano = 0
+
+ pontos = mapa_pontos.get(tipo, 0)
+
+ result.append(Premiacao(
+ tipo=tipo,
+ nome_premio=nome_premio,
+ ano=ano,
+ pontos=pontos
+ ))
+
+ return result
+
+
+async def buscar_por_nome(es_client, nome):
+ query = {
+ "size": 5,
+ "query": {
+ "match": {
+ "dadosPessoais.nome": {
+ "query": nome,
+ "fuzziness": "AUTO"
+ }
+ }
+ },
+ "_source": ["id", "dadosPessoais", "atuacoes"]
+ }
+
+ response = await es_client.client.post(
+ f"{es_client.url}/{es_client.index}/_search",
+ json=query
+ )
+ response.raise_for_status()
+ result = response.json()
+ return result.get("hits", {}).get("hits", [])
+
+
+async def main():
+ print("Conectando ao Elasticsearch...")
+ es_client = ElasticsearchClient(ES_URL, ES_INDEX, ES_USER, ES_PASSWORD)
+ await es_client.connect()
+
+ calculador = CalculadorPontuacao()
+
+ try:
+ for nome_buscar in NOMES_BUSCAR:
+ print(f"\n{'='*100}")
+ print(f"BUSCANDO: {nome_buscar}")
+ print(f"{'='*100}")
+
+ hits = await buscar_por_nome(es_client, nome_buscar)
+
+ if not hits:
+ print(f" Nenhum resultado encontrado para '{nome_buscar}'")
+ continue
+
+ print(f" Encontrados {len(hits)} resultados")
+
+ for hit in hits:
+ doc = hit["_source"]
+ id_pessoa = doc.get("id")
+ dados_pessoais = doc.get("dadosPessoais", {})
+ nome = dados_pessoais.get("nome", "N/A")
+ cpf = dados_pessoais.get("cpf", "")
+ atuacoes = doc.get("atuacoes", [])
+
+ print(f"\n CONSULTOR ENCONTRADO: {nome}")
+ print(f" ID: {id_pessoa}")
+
+ tipos_atuacao = {}
+ for a in atuacoes:
+ tipo = a.get("tipo", "Desconhecido")
+ tipos_atuacao[tipo] = tipos_atuacao.get(tipo, 0) + 1
+
+ print(f"\n TIPOS DE ATUAÇÃO:")
+ for tipo, count in sorted(tipos_atuacao.items()):
+ print(f" - {tipo}: {count}")
+
+ coordenacoes_capes = extrair_coordenacoes_capes(atuacoes)
+ consultoria = extrair_consultoria(atuacoes)
+ premiacoes = extrair_premiacoes(atuacoes)
+
+ consultor = Consultor(
+ id_pessoa=id_pessoa,
+ nome=nome,
+ cpf=cpf,
+ coordenacoes_capes=coordenacoes_capes,
+ coordenacoes_programas=[],
+ consultoria=consultoria,
+ premiacoes=premiacoes
+ )
+
+ pontuacao = calculador.calcular_pontuacao_completa(consultor)
+ consultor.pontuacao = pontuacao
+
+ print(f"\n PONTUAÇÃO CALCULADA:")
+ print(f" TOTAL: {consultor.pontuacao_total:.2f} pontos")
+ print(f" Componente A (Coordenação CAPES): {consultor.pontuacao.componente_a.total:.2f}")
+ print(f" Base: {consultor.pontuacao.componente_a.base} | Tempo: {consultor.pontuacao.componente_a.tempo}")
+ print(f" Extras: {consultor.pontuacao.componente_a.extras} | Bônus: {consultor.pontuacao.componente_a.bonus} | Retorno: {consultor.pontuacao.componente_a.retorno}")
+ print(f" Componente B (Coordenação PPG): {consultor.pontuacao.componente_b.total:.2f}")
+ print(f" Componente C (Consultoria): {consultor.pontuacao.componente_c.total:.2f}")
+ if consultoria:
+ print(f" Base: {consultor.pontuacao.componente_c.base} | Tempo: {consultor.pontuacao.componente_c.tempo}")
+ print(f" Extras: {consultor.pontuacao.componente_c.extras}")
+ print(f" Eventos: {consultoria.total_eventos} | Recentes: {consultoria.eventos_recentes}")
+ print(f" Situação: {consultoria.situacao}")
+ print(f" Anos: {consultoria.anos_completos}")
+ print(f" Componente D (Premiações): {consultor.pontuacao.componente_d.total:.2f}")
+ if premiacoes:
+ print(f" Total de premiações: {len(premiacoes)}")
+ for p in premiacoes[:5]:
+ print(f" - {p.tipo}: {p.nome_premio} ({p.ano}) = {p.pontos} pts")
+
+ print(f"\n DETALHAMENTO COMPLETO:")
+ if coordenacoes_capes:
+ print(f" Coordenações CAPES ({len(coordenacoes_capes)}):")
+ for coord in coordenacoes_capes:
+ status = "ATIVA" if coord.periodo.ativo else "ENCERRADA"
+ print(f" - {coord.tipo}: {coord.area_avaliacao}")
+ print(f" Período: {coord.periodo.inicio} até {coord.periodo.fim or 'atual'} ({status})")
+ print(f" Anos: {coord.periodo.anos_decorridos:.1f}")
+
+ finally:
+ await es_client.close()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/backend/scripts/top10_ranking.py b/backend/scripts/top10_ranking.py
new file mode 100644
index 0000000..7fffd7f
--- /dev/null
+++ b/backend/scripts/top10_ranking.py
@@ -0,0 +1,265 @@
+#!/usr/bin/env python3
+import sys
+import os
+import asyncio
+from datetime import datetime
+from typing import List, Dict, Any
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
+
+from infrastructure.elasticsearch.client import ElasticsearchClient
+from domain.services.calculador_pontuacao import CalculadorPontuacao
+from domain.entities.consultor import Consultor, CoordenacaoCapes, Consultoria, Premiacao, Periodo
+
+ES_URL = "http://elastic-atuacapes.hom.capes.gov.br:9200"
+ES_INDEX = "atuacapes"
+ES_USER = "admin-atuacapes"
+ES_PASSWORD = "O}!S0bj%FhJ:"
+
+
+def parse_date(date_str):
+ if not date_str:
+ return None
+ try:
+ if '/' in date_str:
+ return datetime.strptime(date_str[:10], '%d/%m/%Y')
+ else:
+ return datetime.strptime(date_str[:10], '%Y-%m-%d')
+ except:
+ return None
+
+
+def extrair_coordenacoes_capes(atuacoes: List[Dict]) -> List[CoordenacaoCapes]:
+ coordenacoes_data = [
+ a for a in atuacoes
+ if a.get("tipo") in [
+ "Coordenação de Área de Avaliação",
+ "Histórico de Coordenação de Área de Avaliação",
+ ]
+ ]
+
+ result = []
+ for coord in coordenacoes_data:
+ dados = coord.get("dadosCoordenacaoArea") or coord.get("dadosHistoricoCoordenacaoArea") or {}
+
+ nome = coord.get("nome", "")
+ if "câmara" in nome.lower() or "camara" in nome.lower():
+ tipo = "CAM"
+ elif "mestrado profissional" in nome.lower():
+ tipo = "CAJ-MP"
+ elif "adjunt" in nome.lower():
+ tipo = "CAJ"
+ else:
+ tipo = "CA"
+
+ inicio = parse_date(coord.get("inicio") or dados.get("inicio"))
+ fim = parse_date(coord.get("fim") or dados.get("fim"))
+
+ if not inicio:
+ continue
+
+ area = dados.get("areaAvaliacao", {})
+ if isinstance(area, dict):
+ area_nome = area.get("nome", "N/A")
+ else:
+ area_nome = str(area) if area else "N/A"
+
+ periodo = Periodo(inicio=inicio, fim=fim)
+
+ result.append(CoordenacaoCapes(
+ tipo=tipo,
+ area_avaliacao=area_nome,
+ periodo=periodo,
+ areas_adicionais=[],
+ ja_coordenou_antes=False
+ ))
+
+ return result
+
+
+def extrair_consultoria(atuacoes: List[Dict]) -> Consultoria:
+ consultorias = [
+ a for a in atuacoes
+ if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]
+ ]
+
+ if not consultorias:
+ return None
+
+ datas = []
+ areas = set()
+ total_eventos = len(consultorias)
+ vezes_responsavel = 0
+
+ for cons in consultorias:
+ dados = cons.get("dadosConsultoria", {})
+
+ inicio = parse_date(cons.get("inicio"))
+ fim = parse_date(cons.get("fim"))
+
+ if inicio:
+ datas.append(inicio)
+ if fim:
+ datas.append(fim)
+
+ area = dados.get("areaAvaliacao")
+ if area:
+ areas.add(area)
+
+ if dados.get("responsavel"):
+ vezes_responsavel += 1
+
+ if not datas:
+ return None
+
+ primeiro_evento = min(datas)
+ ultimo_evento = max(datas)
+
+ limite_recente = datetime.now().replace(year=datetime.now().year - 2)
+ eventos_recentes = sum(1 for d in datas if d >= limite_recente)
+
+ anos = (datetime.now() - primeiro_evento).days / 365.25
+
+ return Consultoria(
+ total_eventos=total_eventos,
+ eventos_recentes=eventos_recentes,
+ primeiro_evento=primeiro_evento,
+ ultimo_evento=ultimo_evento,
+ areas=list(areas),
+ situacao="Ativo" if eventos_recentes > 0 else "Histórico",
+ anos_completos=int(anos),
+ anos_consecutivos=int(anos),
+ retornos=0
+ )
+
+
+def extrair_premiacoes(atuacoes: List[Dict]) -> List[Premiacao]:
+ premiacoes_data = [
+ a for a in atuacoes
+ if a.get("tipo") in [
+ "Premiação Prêmio",
+ "Avaliação Prêmio",
+ "Inscrição Prêmio",
+ ]
+ ]
+
+ result = []
+ mapa_pontos = {
+ "Premiação Prêmio": 60,
+ "Avaliação Prêmio": 40,
+ "Inscrição Prêmio": 20,
+ }
+
+ for prem in premiacoes_data:
+ tipo = prem.get("tipo", "")
+ dados = prem.get("dadosPremiacaoPremio") or prem.get("dadosParticipacaoPremio") or prem.get("dadosParticipacaoInscricaoPremio") or {}
+
+ nome_premio = dados.get("premio", "") or prem.get("descricao", "")
+ ano_str = dados.get("ano") or prem.get("inicio", "")
+
+ try:
+ ano = int(ano_str) if isinstance(ano_str, (int, str)) and str(ano_str).isdigit() else 0
+ except:
+ ano = 0
+
+ pontos = mapa_pontos.get(tipo, 0)
+
+ result.append(Premiacao(
+ tipo=tipo,
+ nome_premio=nome_premio,
+ ano=ano,
+ pontos=pontos
+ ))
+
+ return result
+
+
+async def main():
+ print("Conectando ao Elasticsearch...")
+ es_client = ElasticsearchClient(ES_URL, ES_INDEX, ES_USER, ES_PASSWORD)
+ await es_client.connect()
+
+ try:
+ print("Buscando candidatos com boost...")
+ docs = await es_client.buscar_candidatos_ranking(size=100)
+ print(f"Encontrados {len(docs)} candidatos")
+
+ calculador = CalculadorPontuacao()
+ consultores = []
+
+ print("\nProcessando consultores...")
+ for idx, doc in enumerate(docs, 1):
+ id_pessoa = doc.get("id")
+ dados_pessoais = doc.get("dadosPessoais", {})
+ nome = dados_pessoais.get("nome", "N/A")
+ cpf = dados_pessoais.get("cpf", "")
+ atuacoes = doc.get("atuacoes", [])
+
+ coordenacoes_capes = extrair_coordenacoes_capes(atuacoes)
+ consultoria = extrair_consultoria(atuacoes)
+ premiacoes = extrair_premiacoes(atuacoes)
+
+ consultor = Consultor(
+ id_pessoa=id_pessoa,
+ nome=nome,
+ cpf=cpf,
+ coordenacoes_capes=coordenacoes_capes,
+ coordenacoes_programas=[],
+ consultoria=consultoria,
+ premiacoes=premiacoes
+ )
+
+ pontuacao = calculador.calcular_pontuacao_completa(consultor)
+ consultor.pontuacao = pontuacao
+
+ consultores.append(consultor)
+
+ if idx % 10 == 0:
+ print(f" Processados {idx}/{len(docs)} consultores...")
+
+ print("\nOrdenando por pontuação...")
+ consultores.sort(key=lambda c: c.pontuacao_total, reverse=True)
+
+ print("\n" + "="*100)
+ print("TOP 10 CONSULTORES - RANKING CAPES")
+ print("="*100)
+
+ for rank, c in enumerate(consultores[:10], 1):
+ print(f"\n{rank}º LUGAR - {c.nome}")
+ print(f" ID: {c.id_pessoa}")
+ print(f" PONTUAÇÃO TOTAL: {c.pontuacao_total:.2f} pontos")
+ print(f" Componente A (Coordenação CAPES): {c.pontuacao.componente_a.total:.2f}")
+ print(f" Base: {c.pontuacao.componente_a.base} | Tempo: {c.pontuacao.componente_a.tempo}")
+ print(f" Extras: {c.pontuacao.componente_a.extras} | Bônus: {c.pontuacao.componente_a.bonus} | Retorno: {c.pontuacao.componente_a.retorno}")
+ print(f" Componente B (Coordenação PPG): {c.pontuacao.componente_b.total:.2f}")
+ print(f" Componente C (Consultoria): {c.pontuacao.componente_c.total:.2f}")
+ if c.consultoria:
+ print(f" Base: {c.pontuacao.componente_c.base} | Tempo: {c.pontuacao.componente_c.tempo}")
+ print(f" Eventos: {c.consultoria.total_eventos} | Recentes: {c.consultoria.eventos_recentes}")
+ print(f" Áreas: {', '.join(c.consultoria.areas[:3])}")
+ print(f" Componente D (Premiações): {c.pontuacao.componente_d.total:.2f}")
+ if c.premiacoes:
+ print(f" Total de premiações: {len(c.premiacoes)}")
+
+ print(f" Anos de atuação: {c.anos_atuacao}")
+ print(f" Ativo: {'Sim' if c.ativo else 'Não'}")
+
+ if c.coordenacoes_capes:
+ print(f" Coordenações CAPES:")
+ for coord in c.coordenacoes_capes[:3]:
+ status = "ATIVA" if coord.periodo.ativo else "ENCERRADA"
+ print(f" - {coord.tipo}: {coord.area_avaliacao} ({status})")
+
+ print("\n" + "="*100)
+ print(f"\nEstatísticas Gerais:")
+ print(f" Total de candidatos processados: {len(consultores)}")
+ print(f" Pontuação máxima: {consultores[0].pontuacao_total:.2f}")
+ print(f" Pontuação mínima (top 10): {consultores[9].pontuacao_total:.2f}")
+ print(f" Média de pontuação (top 10): {sum(c.pontuacao_total for c in consultores[:10])/10:.2f}")
+
+ finally:
+ await es_client.close()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/scripts/reload_atuacapes.sh b/scripts/reload_atuacapes.sh
new file mode 100755
index 0000000..a381eb2
--- /dev/null
+++ b/scripts/reload_atuacapes.sh
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$ROOT_DIR"
+
+echo "[1/5] Garantindo rede Docker compartilhada..."
+docker network create shared_network >/dev/null 2>&1 || true
+
+echo "[2/5] Subindo oracle18c e backend..."
+docker compose up -d oracle18c backend
+
+echo "[3/5] Aguardando backend responder /health..."
+for i in {1..30}; do
+ if docker compose exec backend python - <<'PY' >/dev/null 2>&1; then
+import httpx, sys
+try:
+ r = httpx.get("http://localhost:8000/api/v1/health", verify=False, timeout=15)
+ if r.status_code == 200:
+ sys.exit(0)
+except Exception:
+ pass
+sys.exit(1)
+PY
+ echo " Backend OK."
+ break
+ fi
+ echo " Tentativa ${i}/30... aguardando 5s"
+ sleep 5
+ if [ "$i" -eq 30 ]; then
+ echo "ERRO: backend não respondeu /health."
+ exit 1
+ fi
+done
+
+echo "[4/5] Disparando job do ranking (limpar_antes=true)..."
+docker compose exec backend python - <<'PY'
+import httpx
+client = httpx.Client(verify=False, timeout=120)
+resp = client.post("http://localhost:8000/api/v1/ranking/processar", json={"limpar_antes": True})
+print("POST /api/v1/ranking/processar ->", resp.status_code, resp.text)
+PY
+
+echo "[5/5] Acompanhando status até finalizar..."
+docker compose exec backend python - <<'PY'
+import httpx, time
+client = httpx.Client(verify=False, timeout=120)
+while True:
+ r = client.get("http://localhost:8000/api/v1/ranking/status")
+ data = r.json()
+ print(f"{time.strftime('%H:%M:%S')} | running={data.get('running')} | {data.get('processados')}/{data.get('total')} ({data.get('progress')}%) | msg={data.get('mensagem')}")
+ if not data.get("running"):
+ break
+ time.sleep(15)
+
+print("Coletando contagens finais...")
+dsn = None
+try:
+ import os, cx_Oracle
+ dsn = os.getenv("ORACLE_LOCAL_DSN", "oracle18c:1521/XEPDB1")
+ user = os.getenv("ORACLE_LOCAL_USER", "local123")
+ pwd = os.getenv("ORACLE_LOCAL_PASSWORD", "local123")
+ conn = cx_Oracle.connect(user, pwd, dsn)
+ cur = conn.cursor()
+ for sql in [
+ "select count(*) from tb_ranking_consultor",
+ "select count(*) from tb_ranking_consultor where componente_a>0",
+ "select count(*) from tb_ranking_consultor where componente_c>0",
+ "select count(*) from tb_ranking_consultor where componente_d>0",
+ "select max(pontuacao_total) from tb_ranking_consultor"
+ ]:
+ cur.execute(sql)
+ print(sql, cur.fetchone()[0])
+ cur.close(); conn.close()
+except Exception as e:
+ print(f"AVISO: não foi possível coletar contagens finais (dsn={dsn}): {e}")
+PY
+
+echo "Pronto."