# Gemini — Regras e Portabilidade

> Regras de negócio não óbvias, acoplamentos e armadilhas reais encontradas no código da v5.

---

## 1. Regras de negócio não óbvias

| # | Regra | Onde |
|---|---|---|
| R1 | A chave tem **duas fontes de verdade** independentes: `configurations.value` (sempre) e `env('GEMINI_API_KEY')` (só no caminho B). Se a linha do banco tiver valor **vazio**, o caminho B usa o `env`; o caminho A **não** usa o `env` e devolve 500. | `DataMaskTrait.php:219-220`, `GeminiController.php:22-29` |
| R2 | O caminho A **usa um modelo diferente** do caminho B: `gemini-2.0-flash` vs `gemini-2.5-flash`. Não há constante compartilhada. | `GeminiController.php:31`, `DataMaskTrait.php:240` |
| R3 | O título é limitado a **150** pelo *prompt* ("O titulo deve ter no máximo 150 caracteres") e o código **confia** nessa instrução: se o modelo devolver mais, o corte é em 150 **bytes**. | `GeminiController.php:18, 68-73` |
| R4 | A partição título/corpo depende de o modelo **obedecer** ao "separe com uma linha em branco". Não há JSON estruturado nem `responseMimeType`. | `GeminiController.php:58-75` |
| R5 | No caminho B, a IA **nunca** vê o texto original: ela recebe o texto **já mascarado** pelo baseline determinístico (`maskTextDeterministic`) e só pode **remover/compactar** asteriscos. O modelo **não pode** desanonimizar. | `DataMaskTrait.php:215, 233` |
| R6 | O pós-processamento do caminho B **colapsa** os espaços entre asteriscos (`/\*\s+\*/` → `**`), remove espaços antes de pontuação e normaliza espaços duplos — o resultado **não** preserva exatamente o espaçamento do original, apesar de a instrução pedir preservação. | `DataMaskTrait.php:266-274` |
| R7 | O mascaramento do e-SIC/Ouvidoria é **síncrono**, dentro do `store` do pedido: o cidadão espera a chamada à API antes de receber a confirmação. | `InformationController.php:112, 320`, `OmbudsmanController.php:180` |
| R8 | O `mask_request`/`mask_description` é gravado **além** do texto original (`request`/`description` continuam no banco em claro). O mascaramento é de **exibição**, não de armazenamento. | `informations_requests`, `ombudsman_requests` |
| R9 | Se a chave não estiver configurada, o caminho B **não falha**: registra `Log::warning` e devolve a máscara determinística — o fluxo do cidadão nunca quebra por causa da IA. | `DataMaskTrait.php:236-239` |

---

## 2. Acoplamentos

### 2.1 Dependências *de* outros módulos

| Módulo de origem | Ponto de acoplamento |
|---|---|
| **e-SIC (Portal)** | `v5/app/Http/Controllers/Portal/InformationController.php:112, 320` — grava `mask_request` |
| **Ouvidoria (Portal)** | `v5/app/Http/Controllers/Portal/OmbudsmanController.php:180` — grava `mask_description` |
| **Configurações (CMS)** | `v5/app/Http/Controllers/Prefecture/ConfigurationController.php` — CRUD de `configurations` |
| **Notícias / Publicações (CMS)** | `v5/resources/views/prefecture/contents/noticias/form.blade.php`, `.../publicacoes/form.blade.php` |
| **Model `Configuration`** | `v5/app/Http/Model/Prefecture/Configuration.php` — única fonte da credencial |

### 2.2 Dependências *para* outros módulos

| Módulo consumidor | O que recebe |
|---|---|
| **Relatório Estatístico e-SIC** | o texto já mascarado em `informations_requests.mask_request` |
| **Relatório Estatístico Ouvidoria** | o texto já mascarado in `ombudsman_requests.mask_description` |
| **`activity_log`** | o **valor da chave da API** em claro, a cada edição da configuração |

---

## 3. Armadilhas reais

### 3.1 Segurança

| # | Armadilha | Evidência | Impacto |
|---|---|---|---|
| A1 | **Chave da API em texto claro no banco** (`configurations.value`) | `v5/app/Http/Model/Prefecture/Configuration.php` (sem `$hidden`, sem cast de criptografia) | Qualquer dump do banco expõe a credencial |
| A2 | **Chave da API hardcoded no repositório** | `v5/database/seeds/AddToConfigurationsGeminiApiKeyValue.php` grava um literal `AIza…` | A credencial está no histórico do Git; **deve ser rotacionada** |
| A3 | **Seeder sobrescreve a configuração do operador** | `AddToConfigurationsGeminiApiKeyValue::run()` faz `UPDATE` incondicional; chamado por `ExtrasSeeder.php:35` | Rodar o seeder apaga a chave cadastrada na tela |
| A4 | **Chave exibida em claro na tela de Configurações** (`type="text"`) | `v5/resources/views/prefecture/configurations/configuracoes/loop_config.blade.php` | Shoulder-surfing / print / screenshot |
| A5 | **Chave gravada em claro no log de atividades** | `v5/app/Http/Controllers/Prefecture/ConfigurationController.php:200` e `:315` (`'Valor:' . $configurationModel->value`) | `activity_log.properties` passa a ser um cofre de segredos |
| A6 | **Chave na query string** (`?key=`) | `GeminiController.php:31`, `DataMaskTrait.php:242` | Chave exposta em log de acesso, `Referer`, proxy e histórico |
| A7 | **Rota pública, sem auth e sem throttle** | `v5/routes/web.php:203` (sem `check.logged`, sem `throttle`) | Proxy gratuito para a API paga; DoS e esgotamento de cota |
| A8 | **Erro do Guzzle devolvido ao cliente** | `GeminiController.php:83-86` (`$e->getMessage()`) | A mensagem pode conter a URL com a chave |
| A9 | **Corpo bruto da resposta da IA no log** | `DataMaskTrait.php:260` (`'body' => $body`) e `:278` (`'data' => $data`) | Log pode conter conteúdo derivado de dado pessoal |
| A10 | **Prompt do usuário sem sanitização** | `GeminiController.php:13, 20` | *Prompt injection* por qualquer visitante anônimo |

### 3.2 Correção funcional

| # | Armadilha | Evidência | Impacto |
|---|---|---|---|
| B1 | **`GEMINI_API_MODEL` é lido e nunca usado** | `DataMaskTrait.php:218` — a variável não aparece mais no método; a URL é fixa em `:240` | Trocar o modelo no `.env` **não tem efeito** |
| B2 | **`GEMINI_API_URL` não existe no `.env`** | `v5/.env` e `v5/.env.example` **não contêm** nenhuma variável `GEMINI_*` | `env('GEMINI_API_URL')` retorna `null` → sempre cai na URL fixa; `env('GEMINI_API_KEY')` retorna `null` → o banco é a única fonte |
| B3 | **`env()` lido em runtime** | `DataMaskTrait.php:217-220` | `env()` não deve ser usado fora de `config/`; quebra com `config:cache` em Laravel moderno e não é testável |
| B4 | **`GEMINI_API_URL` com `?` na URL impede a chave de ser anexada** | `DataMaskTrait.php:241-243` — `if (strpos($url, '?') === false)` | Se a URL já tiver query string, a chave **não** é enviada e a API responde 400 |
| B5 | **Corte em bytes quebra UTF-8** | `GeminiController.php:64-71` (`substr`, `strlen`, `strpos`) | Título pode terminar com caractere multibyte partido |
| B6 | **Corpo começa com `\n\n`** | `GeminiController.php:62` (`substr($fullText, $separatorPos)` + `trim` só nas pontas) | Parágrafo vazio inicial no Summernote |
| B7 | **`promptFeedback.blockReason` ignorado** | `GeminiController.php:46` e `DataMaskTrait.php:263` só checam `candidates[0].content.parts[0].text` | Bloqueio de segurança do Gemini vira "Resposta inesperada da API Gemini." (500) |
| B8 | **Sem `maxOutputTokens` / `temperature` / `safetySettings`** | nenhum dos dois payloads | Sem controle de custo; sem ajuste de segurança; sem limite de tamanho |
| B9 | **Sem timeout no caminho A** | `GeminiController.php:34` — `new \GuzzleHttp\Client()` sem opções (Guzzle 5.3.4: `timeout = 0` = infinito) | Requisição pode pendurar para sempre |
| B10 | **Ramo `.done()` com `data.success === false` é código morto** | `ia-modal.js` | O controller nunca devolve `success` |
| B11 | **Mensagem de erro do servidor é descartada** | `ia-modal.js` `.fail()` ignora `responseJSON.title`/`text` | O operador nunca vê "Erro de Configuração" |
| B12 | **Mensagem fixa diz "mínimo 5 caracteres"**, a regra é `min:4` | `ia-modal.js` vs `GeminiPromptRequest::rules()` | Informação incorreta ao operador |
| B13 | **Nenhum `timeout` no `$.ajax`** | `ia-modal.js` | Botão travado em "Gerando..." |
| B14 | **`id="modal-banner"` duplicado** | `ia-modal/index.blade.php:17` e `banner-modal/index.blade.php:11` | Colisão de ID se os dois componentes coexistirem |
| B15 | **`#title` / `#text` são IDs genéricos** | `ia-modal.js` | Risco de escrever no campo errado em outros formulários |
| B16 | **Imports não utilizados** | `v5/app/Http/Controllers/Prefecture/GeminiController.php:4` (`use Illuminate\Http\Request;`) | Ruído no código; sinaliza que a action não usa `Request` diretamente |
| B17 | **`GeminiController` estende `Admin\Controller` mas é rota de portal** | `v5/app/Http/Controllers/Prefecture/GeminiController.php:9` | Inconsistência que contribuiu para a rota ficar no arquivo errado |
| B18 | **`Configuration::where(...)->value('value')` sem `SoftDeletes` explícito** | `v5/app/Http/Model/Prefecture/Configuration.php` usa `SoftDeletes` | Linhas com `deleted_at` preenchido são ignoradas — comportamento correto, mas implícito |
| B19 | **Duas queries ao banco por requisição** no caminho B (`->first()`) e uma no caminho A (`->value()`) | `DataMaskTrait.php:219`, `GeminiController.php:22` | Consulta por chave **não indexada** em `configurations.key` a cada pedido de e-SIC/Ouvidoria |

### 3.3 Custo / limite de requisições

| Aspecto | Situação na v5 |
|---|---|
| Modelo | `gemini-2.0-flash` (caminho A) e `gemini-2.5-flash` (caminho B) |
| Cota | Não monitorada pelo sistema; nenhum contador, alerta ou limite |
| Cache de resposta | **Nenhum** — cada clique/pedido gera uma chamada paga |
| Fila | **Nenhuma** — chamadas síncronas no ciclo da requisição |
| Retry | **Nenhum** — `RequestException` vira erro imediato |
| Rate limit da aplicação | **Nenhum** nas rotas envolvidas |
| Contabilidade/auditoria | Nenhum registro de consumo (nem de quantas chamadas foram feitas) |
| Chave na URL | Sim → **cada chamada gera uma linha de log contendo a credencial** |

---

## 4. Código duplicado / copiado entre v5 e v6

| Artefato | v5 | v6 | Observação |
|---|---|---|---|
| `DataMaskTrait` | `v5/app/Http/Traits/DataMaskTrait.php` | `v6/app/Http/Traits/DataMaskTrait.php` | **Cópia quase idêntica** — o `diff` mostra apenas 2 regex diferentes (matrícula funcional e a lista de termos de gênero/sexualidade). O bloco Gemini é **byte a byte igual**. |
| `InformationController::MaskRequestWithGeminiAi` | `v5/app/Http/Controllers/Portal/InformationController.php:388` | `v6/app/Http/Controllers/Portal/InformationController.php:389` | Idêntico (`return $this->maskTextWithAi($requestText);`) |
| `AddToConfigurationsGeminiApiKeyValue` | v5 | v6 | **Corrigido** na v6: só grava se `empty($config->value)` |
| `AddToConfigurationsSeederGeminiKey` | **não existe** | `v6/database/seeds/AddToConfigurationsSeederGeminiKey.php` | Novo na v6 |
| `ConfigurationsTableSeeder` | — | `v6/database/seeds/ConfigurationsTableSeeder.php:1108-1120` | `updateOrCreate` da chave na v6 |
| Tela de Configurações | `v5/resources/views/prefecture/configurations/configuracoes/form.blade.php:274` | `v6/resources/views/prefecture/configurations/configuracoes/form.blade.php:157` | Filtro da aba "Opções" equivalente |
| **`GeminiController`** | `v5/app/Http/Controllers/Prefecture/GeminiController.php` | **não existe** | Removido |
| **`GeminiPromptRequest`** | `v5/app/Http/Requests/GeminiPromptRequest.php` | **não existe** | Removido |
| **Rota `gemini.ia`** | `v5/routes/web.php:203` | **não existe** | Removida |
| **Modal da IA** | `v5/resources/views/prefecture/contents/components/ia-modal/index.blade.php` | **não existe** | Removido |
| **JS da IA** | `v5/public/assets/js/custom/utilities/helpers/ia-modal.js` | **não existe** | Removido |

---

## 5. Onde a v6 divergiu da v5 (e por quê, se discoverível)

1. **Remoção completa do caminho A (geração de conteúdo).** A v6 não tem controller,
   request, rota, componente Blade nem JS. **Motivo não documentado no repositório**
   (`[NÃO VERIFICADO]`) — plausivelmente por custo/quota da API ou por decisão de produto.
   O **efeito colateral** é que a chave `gemini_api_key` continua aparecendo na tela de
   Configurações da v6 **sem nenhum consumidor**.
2. **Correção do seeder.** `v6/database/seeds/AddToConfigurationsGeminiApiKeyValue.php`
   passou a preservar a chave já cadastrada.
3. **Novo seeder dedicado** (`AddToConfigurationsSeederGeminiKey`) com `order = 71`
   (a v5 usa `order = 70`).
4. **`ConfigurationsTableSeeder` da v6** passou a declarar a chave por `updateOrCreate`.
5. **Nada mudou no `DataMaskTrait`** — o caminho B foi copiado como está, **com os mesmos
   bugs** (B1, B2, B3, B4, B9, A9).

---

## 6. O que **não pode** mudar na migração (equivalência funcional)

| Comportamento | Regra |
|---|---|
| Formato do texto mascarado | O relatório público deve continuar exibindo o **mesmo** padrão de asteriscos; o formatador pós-IA (`/\*\s+\*/` → `**`) faz parte do contrato visual. |
| Nunca quebrar o atendimento | Se a IA falhar, o mascaramento determinístico **precisa** continuar disponível. Não pode haver erro 500 para o cidadão por causa da API. |
| Título ≤ 150 caracteres | O contrato com o Summernote e com o CMS assume título curto. |
| Separador linha em branco | Título e corpo são gravados em campos diferentes (`#title` e `#text`). |
| Prompt concatenado | O texto do operador é **somado** à instrução; o resultado final é sempre "texto pronto para publicar". |

---

## 7. Referências

- `v5/app/Http/Controllers/Prefecture/GeminiController.php:9-89`
- `v5/app/Http/Requests/GeminiPromptRequest.php`
- `v5/app/Http/Traits/DataMaskTrait.php:213-287`
- `v5/app/Http/Controllers/Portal/InformationController.php:112, 320, 388-391`
- `v5/app/Http/Controllers/Portal/OmbudsmanController.php:180`
- `v5/app/Http/Controllers/Prefecture/ConfigurationController.php:200, 315`
- `v5/app/Http/Model/Prefecture/Configuration.php`
- `v5/database/seeds/AddToConfigurationsGeminiApiKeyValue.php`
- `v5/database/seeds/ExtrasSeeder.php:35`
- `v5/public/assets/js/custom/utilities/helpers/ia-modal.js`
- `v5/resources/views/prefecture/configurations/configuracoes/loop_config.blade.php`
- `v6/app/Http/Traits/DataMaskTrait.php`
- `v6/database/seeds/AddToConfigurationsGeminiApiKeyValue.php`
- `v6/database/seeds/AddToConfigurationsSeederGeminiKey.php`
