# Assistente RH — plataforma interna de automação (módulo 2: Contratos)

## Contexto

Hoje os contratos de trabalho são redigidos pela skill `/fazer-contrato` (Claude Code, no Mac): lê a fila "Contratos a Redigir" do Sabichão prod (`contrato='Fazer'` AND `estado='Ativo'`) só por SELECT, calcula os campos (vencimento, extenso, datas, especialidade), preenche um modelo Word manipulando o XML do `.docx` diretamente (runs a amarelo como âncoras — sem `python-docx`, sem `{{token}}`), gera um PDF protegido contra edição via Microsoft Word (AppleScript) + `pypdf`, e publica em `//10.0.30.79/Assistente/Contratos Feitos/<zona>/`. É a segunda funcionalidade a entrar na plataforma Assistente RH, depois do módulo Fichas (`docs/PLANO.md`), reaproveitando a mesma arquitetura: CT Laravel, wrapper SSH restrito ao Sabichão, mini como leitor quando fizer falta.

Diferença central face a Fichas: **este módulo não lê documentos digitalizados nem tira dados de imagens.** Os dados de entrada vêm todos da própria base de dados do Sabichão (processo, processo_contrato, clientes, processamento). Não há OCR, não há confiança por campo, não há aprovação de leituras — há **geração determinística** de um documento a partir de dados estruturados. O papel do Qwen aqui é opcional e muito mais pequeno do que em Fichas (ver secção própria).

Fonte funcional: skill `~/.claude/skills/fazer-contrato/` (`SKILL.md`, `gerar_contrato.py` 1050 linhas, `extenso_pt.py`, `paginacao.py` 400 linhas, `campos.md`) — é a especificação de negócio deste módulo, ~45 contratos reais publicados desde 02/09/2026, com histórico de bugs corrigidos que este plano tem de herdar, não repetir. Memória: `project_skill_fazer_contratos.md`.

## O que a skill faz hoje, passo a passo

1. **Localizar o caso**: `--id`/`--mai`/`--nome` → SELECT a `processo` (prod, `contrato='Fazer' AND estado='Ativo'`; em dev há hoje 3 processos nesse estado). Vários candidatos → lista e pede `--id`.
2. **Carregar dados** (`carregar()`): `processo`, `processo_contrato` (modalidade, datas, duração, renovação, motivo, alínea, nhoras), `clientes_servicos`/`cliente_equipa` (afetação, para o nome/morada do cliente — escolhida pela mais recente `data_inicio DESC`, com aviso se houver mais de uma ativa), cartões PSP do funcionário (especialidade e categoria), `processamento_categorias`/`processamento_config` (valor_hora, ordenado_mensal, `contrato_ft_horas_semana`), tabela `feriados`.
3. **Calcular** (`calcular()`): modelo a usar (termo certo/incerto/SPR/ARD conforme modalidade + especialidade), especialidade do contrato (a que o cliente contratou **e** o funcionário tem cartão — ambígua → pergunta), vencimento (mesma fórmula do `js/contrato_minuta.js`: full-time Vigilante = `ordenado_mensal`; resto = `valor_hora × horas mensais`), data de assinatura (hoje ou dia útil anterior à admissão, com feriados), tipo de documento de identificação, artigos concordados ("instalações da/do X", "sitas na/no Y", heurística por palavras-chave), motivo do termo por alínea, duração em dias vs. meses (regra especial: dias muda "termo previsto em" → "termo em" e remove o parágrafo de renovação se não houver renovação no Sabichão).
4. **Perguntas pendentes** (exit 2, sem `.docx`): especialidade ambígua, sem afetação de cliente, renovação vazia, `data_fim` vazia, documento não reconhecido, categoria sem `valor_hora`, sem modalidade/termo, artigo errado, horas = 0 (sem flag — corrige-se no Sabichão). Todas numa só interação; o utilizador responde por flags e repete a corrida.
5. **Preencher o `.docx`** (`preencher_docx`, manipulação de XML crua com `zipfile`): identifica cada parágrafo por uma âncora de texto fixo (ex. `SEGUNDO:`, `detentor do cartão de`, `celebrado a Termo Certo`), funde runs amarelos contíguos (o Word parte datas em vários runs), substitui pelos valores calculados **sem realce**; campos sem valor ficam `[FALTA: chave]` a amarelo. Se uma âncora não bate (modelo alterado), recusa gerar com o parágrafo problemático no erro.
6. **Validar**: nenhum dos `TEXTOS_EXEMPLO` (dados de exemplo dos modelos) pode sobreviver no resultado.
7. **Paginação** (`paginacao.py`): regras estáticas (keepNext em títulos, parágrafo vazio antes de "elaborado em duplicado", quebra de página antes do ANEXO I) **mais** um passo dinâmico que pergunta ao **Microsoft Word por AppleScript** em que página cai cada parágrafo, remove vazios que ficam no topo de uma página (>= 2) e insere uma página em branco real se o ANEXO I cair em página par — itera até 3x. Sem Word (`--sem-word`), há um modo `seccao_impar` que imprime certo mas não mostra a página em branco no ecrã do Word e salta a numeração.
8. **PDF protegido** (só se 0 `[FALTA]`): exporta o `.docx` a PDF pelo **Word** (AppleScript, dentro do contentor do Word — fora dele dá erro -1708), depois `pypdf.PdfWriter().encrypt()` (AES-256, password vazia para abrir/imprimir, password de dono para bloquear edição/anotação/cópia, guardada em `~/.config/segunder/fazer-contrato-pdf.pw`, 600).
9. **Publicar**: copia `.docx` (+ `.pdf` se gerado) para `Assistente/Contratos Feitos/<zona1 do Sabichão>/<Nome> <dd-mm-aaaa>.docx`; recusa se já existir (nunca sobrescreve); cria a pasta da zona se faltar.
10. **Nunca escreve no Sabichão** (não marca "feito"); isso fica sempre para o utilizador.

## O que muda ao correr no CT (sem Claude)

### Geração do `.docx`: mesma técnica, portada, não reescrita à volta de outra biblioteca

A skill não usa `python-docx` — manipula o XML do `.docx` diretamente porque os modelos usam **runs com `w:highlight yellow`** como âncoras, alguns partidos em vários runs pelo próprio Word (datas, números), e a lógica de fusão de runs (`is_simple_run`, remoção de `w:proofErr`/`w:lastRenderedPageBreak` — o bug do modelo ARD, corrigido 04/09) é código específico a este formato de modelo, já testado contra 3+ contratos reais em regressão.

**Recomendação: portar a mesma abordagem para PHP, não adotar `PhpWord\TemplateProcessor`.** O `TemplateProcessor` do PhpWord assume placeholders `${variavel}` — os modelos da Assistente não têm isso, têm texto fixo (a âncora) seguido de runs amarelos posicionais. Fazer isso caber no `TemplateProcessor` obrigaria a reescrever os 4 modelos Word (risco: o utilizador teria de os re-marcar, e qualquer nova versão de modelo teria de repetir o trabalho) ou a contornar a API com acesso de baixo nível, o que anula a vantagem de usar a biblioteca. Em vez disso: `ZipArchive` + `DOMDocument` (ou `SimpleXML`, PHP tem as duas nativamente) para abrir `word/document.xml`, percorrer parágrafos, localizar as mesmas âncoras de `campos.md`, fundir runs amarelos contíguos com o mesmo critério (`rPr` igual) e substituir texto — é essencialmente o mesmo algoritmo do Python, trocado de sintaxe. `PhpWord` pode ainda servir de biblioteca auxiliar só para reabrir/gravar o zip e validar XML, mas a lógica de âncoras/runs é código próprio, tal como hoje.

Isto é trabalho de porte mecânico e bem definido (função a função, com o `campos.md` como mapa) — candidato a `delegar-mini`/`delegar-codex` depois de o esqueleto e os testes de paridade estarem definidos por um humano/Claude.

### Conversão para PDF: Word (Mac) → LibreOffice headless (CT); risco real na paginação dinâmica

Hoje: Word por AppleScript exporta `.docx → .pdf`, e o mesmo Word (por AppleScript) dá o **mapa de páginas** usado pela paginação dinâmica (parágrafo → página, para tirar vazios do topo e decidir se o Anexo I cai em página par). Não há Word no CT (Debian).

- **Conversão para PDF**: `libreoffice --headless --convert-to pdf` é a alternativa padrão em Linux, já usada noutros CTs da casa (nenhum ainda para `.docx`, mas o `wkhtmltopdf` do módulo Fichas mostra que instalar conversores fora do apt já é procedimento conhecido — `.deb` estático ou `flatpak`/`snap` conforme o que o Debian 13 tiver disponível). Testar com os 4 modelos reais antes de confiar na fidelidade (tabelas, quebras de secção, cabeçalhos/rodapés têm de sair iguais ao que o Word produz).
- **Paginação dinâmica (o passo que pergunta "em que página caiu este parágrafo")**: isto é o ponto mais arriscado do porte. O LibreOffice **tem** uma API (UNO, via `soffice --accept="socket,host=localhost,port=2002;..."` + um cliente Python/PHP ou uma macro Basic) que permite perguntar a página de um parágrafo através do `ViewCursor`/`TextCursor`, mas é mais lenta a montar, mais frágil entre versões, e ninguém na casa a usou ainda. **Recomendação para o MVP: não portar o passo dinâmico.** Usar só as regras estáticas já existentes no modo `--sem-word` da skill (`keepNext`, parágrafo vazio fixo antes de "elaborado em duplicado", `pageBreakBefore` no Anexo I) — que já imprimem corretamente, com a limitação conhecida (o Anexo I em secção "página ímpar" não se vê em branco no ecrã do Word e a numeração salta, mas o PDF fica certo, que é o que interessa para imprimir e assinar). Se a fidelidade do modo estático não for suficiente na prática, uma fase posterior explora UNO — é um risco a decidir com o utilizador, não a esconder (ver "Decisões em aberto").
- **Proteção do PDF**: hoje `pypdf` (Python) faz `encrypt()` AES-256 com password de dono. Equivalente em Linux/CT: **`qpdf --encrypt "" <owner-pw> 256 --print=full --modify=none --extract=n -- in.pdf out.pdf`** — mesmas garantias (abre e imprime sem password, bloqueia edição/anotação/cópia), `qpdf` é um pacote apt normal no Debian (ao contrário do `wkhtmltox`), sem dependências exóticas. A password de dono deixa de estar num ficheiro `~/.config` do Mac de um utilizador e passa a viver no CT, gerida como qualquer segredo de aplicação (`.env` ou ficheiro `0600` do utilizador do serviço, nunca em código nem em log).

### Onde entra o Qwen — e o que fica sempre determinístico

Ao contrário de Fichas, aqui **não há leitura de documentos nem incerteza de origem dos dados**: tudo vem de tabelas do Sabichão, já estruturadas. Isso significa que quase todo o módulo é determinístico — o mesmo tipo de cálculo que hoje já corre em `js/contrato_minuta.js` no próprio Sabichão (extenso, vencimento, regime), só que replicado em PHP no CT (tal como a skill já replica em Python). Não há "confiança alta/rever" por campo: um valor vem da BD ou não vem (e se não vier, é `[FALTA]`, ou pergunta, exatamente como hoje).

Papel proposto para o Qwen (opcional, fase tardia, não bloqueia nenhuma fase anterior):
1. **Revisão de coerência do texto final** — antes de publicar, mandar ao Qwen o texto extraído do `.docx` gerado (não a imagem) com uma pergunta fechada: "este contrato tem alguma frase com concordância estranha (artigo, plural/singular, número por extenso a não bater com o algarismo)?" — um detetor de regressão adicional, nunca uma fonte de dados nem uma decisão. Equivalente ao papel de "comparador semântico" que já tem em Fichas, mas aplicado a texto gerado em vez de a duas leituras divergentes.
2. **Nada mais.** Não há imagens neste módulo, não há campos ambíguos de OCR, não há necessidade de segunda fonte — os "avisos" que a skill já produz hoje (artigo por heurística, documento caducado, `data_fim` calculada) continuam a ser código determinístico com aviso ao utilizador, tal como agora.

Se o utilizador não quiser sequer esse papel do Qwen, o módulo funciona 100% sem ele — decisão em aberto (ver secção final).

## Dados necessários e de onde vêm

Confirmado por leitura ao schema de dev (só leitura, 22/09/2026): `processo` (chave da fila, `contrato`/`estado`), `processo_contrato` (modalidade, datas, duração+unidade, renovação+unidade, `motivo_termo`, `nhoras`/`periodo_horas`, `regime`), `clientes_servicos` (`numero_contrato`, `especialidade`), mais `cliente_equipa`/`clientes` (nome/morada do cliente, pelo mesmo critério de "afetação mais recente" já usado pela skill), cartões PSP do funcionário, `processamento_categorias`/`processamento_config`, `feriados`. Fila hoje em dev: 3 processos com `contrato='Fazer' AND estado='Ativo'`.

Todas as leituras já existem como **wrapper de consulta** para Fichas (`docs/03-CONTRATO-WRAPPER-CT107.md`) — este módulo acrescenta ações novas à mesma família, todas na **chave de consulta** (nunca escreve no Sabichão, herdando a regra "nunca escreve" da própria skill):

| Ação nova | Efeito | Idempotente |
|---|---|---|
| `consulta.fila_contratos` | `SELECT` a `processo` com `contrato='Fazer' AND estado='Ativo'`, junta nome/mai/zona para a lista da fila | Sim |
| `consulta.dados_contrato` | Réplica de `carregar()`+`calcular()` da skill: processo, processo_contrato, afetação de cliente (com aviso se houver mais de uma ativa), cartões, categorias/config, feriados — devolve o mesmo bloco de campos calculados que a skill hoje mostra no relatório | Sim |
| `consulta.zonas` | Já existe para Fichas; reaproveitada para a lista de zonas ativas | Sim |

Nenhuma ação de escrita ao Sabichão é necessária neste módulo — nem para marcar "feito" (fica ao critério do utilizador, tal como hoje, ver "Decisões em aberto" nº1).

## Montagem da partilha Assistente no CT — primeira escrita na NAS

Até agora (Fichas) o CT só **lê** a NAS (`//10.0.30.79/Processos`, CIFS read-only). Este módulo precisa de:
- **Ler** `//10.0.30.79/Assistente/Modelos contratos` (os 4 `.docx` fixos: `Contrato_Termo_Certo_v2.docx`, `Contrato_2026_incerto.docx`, `Contrato_2026_SPR.docx`, `Contrato_2026_ARD.docx`) — confirmado montado e acessível no Mac (`/Volumes/Assistente`), mas **não montado hoje no CT 117** (nota do utilizador); precisa de automount CIFS próprio, igual em espírito ao de `Processos`, mas para a share `Assistente` (share diferente, credenciais a confirmar — pode ser a mesma conta de serviço da NAS ou uma dedicada, ver "Decisões em aberto" nº3).
- **Escrever** em `//10.0.30.79/Assistente/Contratos Feitos/<zona>/` — a **primeira escrita da plataforma na NAS**. Implicações de segurança que Fichas nunca teve:
  - Conta de serviço dedicada (não a mesma conta read-only da leitura de `Processos`), com permissões CIFS restritas exatamente a esta subpasta (a NAS tem de impedir escrita fora de `Assistente/Contratos Feitos`, mesmo que a conta seja comprometida do lado do CT).
  - `realpath()`/normalização do caminho de destino antes de qualquer `fwrite` — nunca compor o caminho da zona a partir de texto livre sem validar contra a lista de zonas conhecida (mesma regra de "nunca sair da pasta autorizada" já aplicada à leitura de documentos em Fichas, `docs/PLANO.md` secção Segurança).
  - Mesma regra de nunca sobrescrever: se `.docx` ou `.pdf` já existirem no destino, recusar e devolver o caminho existente — replicar tal-e-qual o comportamento da skill.
  - Auditoria de escrita: cada publicação fica registada (quem, quando, caminho, hash do ficheiro) — não existe hoje na skill (que só mostra o relatório ao utilizador); é uma melhoria natural de ter isto numa app com login por utilizador em vez de uma sessão de Claude.

## Modelo de dados (MySQL no CT, tabelas novas ao lado das de `fichas_*`)

- `contratos_casos`: id, id_processo (Sabichão), nif, mai, nome, zona, modalidade, estado, versao, bloqueio_motivo, created_by, timestamps. Prevenção de duplicados por `id_processo` (só um caso "em curso" por processo de cada vez — ao contrário de Fichas, aqui o `id_processo` já existe e é a chave natural, não há registo livre de MAI/NIF).
- `contratos_versoes`: caso_id, versao, dados_calculados_json (o que a skill hoje mostra no relatório: modelo escolhido, especialidade, vencimento com a conta, datas, avisos), respostas_perguntas_json (as flags equivalentes: especialidade escolhida, cliente, renovação, `data_fim`, documento, categoria, artigo), campos_falta (lista de `[FALTA: chave]` ainda por resolver), gerado_em.
- `contratos_documentos`: caso_id, versao, ficheiro (`.docx`/`.pdf`), sha256, tamanho, publicado_em, caminho_nas.
- `contratos_eventos`: auditoria (quem gerou, quem respondeu a uma pergunta pendente, quem publicou, quem descarregou).
- Reaproveita `users` de Fichas (mesmo login, mesma tabela — módulo é só mais uma permissão).

Sem `fichas_fiabilidade`, `fichas_aprovacoes` nem hashes de documentos de entrada — não há leitura de imagens aqui para medir acerto de leitor, nem aprovação imutável contra ficheiros da NAS (os "ficheiros de entrada" são os modelos, fixos e versionados por hash de outra forma, ver abaixo).

**Hash dos modelos**: tal como a aprovação de Fichas invalida se um documento mudar, este módulo deve guardar o hash de cada modelo Word usado em cada `contratos_versoes` — se o utilizador substituir um modelo na NAS (nova redação, nova cláusula) depois de gerar mas antes de publicar, a app deve avisar e oferecer regenerar, não publicar com o modelo antigo. Mecanismo mais simples que Fichas (não há hash de "documentos de entrada" pessoais, só 4 ficheiros fixos partilhados por todos os casos).

## UI

- **Fila de contratos a redigir**: lista dos processos com `contrato='Fazer' AND estado='Ativo'` (via `consulta.fila_contratos`), com estado do caso na app ao lado (ainda não gerado / gerado com perguntas / gerado com `[FALTA]` / publicado). Um processo pode ter zero ou um caso "em curso" — abrir cria o caso se não existir.
- **Página do contrato**: mostra o mesmo relatório que a skill hoje dá ao utilizador em texto — modelo escolhido, especialidade e origem, cliente e morada (com o artigo escolhido e a heurística, editável), vencimento com a conta, datas, data de assinatura, avisos (documento caducado, `data_fim` calculada, motivo trocado, renovação removida). Perguntas pendentes viram campos do formulário em vez de flags de linha de comandos (especialidade → lista dos cartões do funcionário; cliente → lista de candidatos ou texto livre; renovação/`data_fim`/documento/categoria/artigo → os mesmos equivalentes). Botão "Recalcular" depois de responder.
- **Pré-visualização**: antes de publicar, mostrar a lista de `[FALTA: chave]` ainda pendentes (se houver) e os avisos; sem visualização do `.docx` em si no MVP (não há um leitor de Word embutido barato e seguro; descarregar o `.docx` gerado para conferir no Word/LibreOffice do utilizador é suficiente, mesma limitação que Fichas tem para o `.docx` gerado).
- **Publicar**: gera (se ainda não gerado ou se algo mudou) → copia para a NAS → mostra o caminho gravado, tal como a skill já faz. Sem "aprovação" formal em duas fases como Fichas (o próprio ato de publicar é a decisão, replicando a regra já validada pelo utilizador em 03/09 "publica sem pedir OK quando não há perguntas pendentes").
- **Marcar como feito no Sabichão**: **não** faz parte do MVP nem da skill atual (a skill explicitamente nunca escreve no Sabichão) — fica ação manual do utilizador, salvo decisão em contrário (ver "Decisões em aberto" nº1).

## Estados de `contratos_casos`

`sem_gerar → gerado_com_perguntas | gerado_completo | gerado_com_falta → (resposta às perguntas → recalcula) → publicado`. Terminal: `publicado`. Sem "aprovado"/"rejeitado" — não há aprovação de leitura, há geração e publicação. Regenerar depois de publicado (modelo mudou, dados do Sabichão mudaram) cria nova `contratos_versoes` mas não sobrescreve o ficheiro já publicado (mesma regra de "nunca sobrescrever" da skill) — fica como aviso "já publicado em `<caminho>`, gerar de novo cria conflito a resolver à mão".

## Segurança

- Mesmos princípios do módulo Fichas (`docs/PLANO.md`): só LAN, Argon2id, sessões curtas, CSRF, logs sanitizados.
- **Menos superfície de dados pessoais do que Fichas** — não há CC, RC, NISS, imagens; os dados que passam por aqui (nome, morada, NIF do funcionário) já estão no Sabichão e são os mesmos que a skill hoje já manipula. Ainda assim, mesma regra de nunca sair do CT: nenhum destes dados vai a um serviço fora da rede (nem ao Qwen tirando o texto do contrato já gerado, se o papel de revisão de coerência for aprovado).
- **Escrita na NAS é a superfície nova**: conta de serviço com permissão restrita à subpasta, caminho validado contra lista de zonas conhecida, nunca concatenação livre de texto vindo da BD para compor o caminho final.
- **Password de PDF** (owner) gerida como segredo do CT, não copiada do `~/.config` do Mac; rotação e local exatos a decidir na Fase de infraestrutura.
- Wrapper: novas ações (`consulta.fila_contratos`, `consulta.dados_contrato`) só na chave de consulta já existente — nenhuma chave nova, nenhuma ação de escrita ao Sabichão.

## Fases e critérios de aceitação

### Fase 0 — Contratos: contratos e mapa (meio dia)
- Confirmar schema exato de `processo_contrato`/`clientes_servicos`/afetação (feito nesta sessão, ver acima); mapa de âncoras (`campos.md`) revisto linha a linha contra os 4 modelos reais; schema JSON do "manifesto de cálculo" (equivalente ao relatório que a skill produz hoje); ações novas do wrapper especificadas (entrada/saída/exit codes, mesma forma do `03-CONTRATO-WRAPPER-CT107.md`).
- Aceitação: documento revisto pelo utilizador; nenhuma âncora do mapa por confirmar contra os modelos reais.

### Fase 1 — Infraestrutura (½ a 1 dia)
- Automount CIFS da share `Assistente` no CT de dev (leitura dos modelos; escrita ainda desligada ou apontada a uma subpasta de teste, nunca `Contratos Feitos` real em dev); `qpdf` e `libreoffice` (ou equivalente) instalados; ações novas do wrapper de consulta.
- Aceitação: CT lê os 4 modelos por hash; `soffice --headless --convert-to pdf` produz um PDF válido a partir de um `.docx` de teste; `qpdf --encrypt` produz um PDF que abre sem password e recusa edição (mesmo teste que `proteger_pdf` já faz hoje: reabrir e confirmar sem `MODIFY`).

### Fase 2 — Motor de geração (2 a 3 dias)
- Porte do algoritmo de âncoras/runs (PHP, `ZipArchive`+`DOMDocument`), da fusão de runs, dos cálculos (`calcular()`, `extenso_pt.py` portado), das perguntas pendentes, da validação de `TEXTOS_EXEMPLO`.
- **Teste de paridade** (crítico, pedido explicitamente pelo utilizador): gerar em dev os mesmos contratos que a skill já gerou em produção (usando os mesmos processos, cujos dados não mudaram) e comparar **texto extraído** (`docx → texto`, equivalente ao `textutil -convert txt` que a skill já usa para verificar) — byte a byte não é objetivo (formatação pode diferir ligeiramente entre motor Python/Word e motor PHP/LibreOffice), mas o texto normalizado (sem espaços/quebras) tem de ser idêntico campo a campo. Lista de candidatos: os ~45 processos já publicados (dados históricos, sem gerar nada novo em prod).
- Aceitação: paridade de texto em pelo menos 10 casos reais cobrindo os 4 modelos (termo certo, incerto, SPR, ARD) e o caso de termo em dias (Festas de Vizela); nenhuma âncora falha nos 4 modelos; `TEXTOS_EXEMPLO` nunca sobrevive.

### Fase 3 — Webapp (1 a 2 dias)
- Fila, página do contrato, formulário de perguntas pendentes, publicar, descarregar.
- Aceitação: 3 a 5 processos de dev gerados e publicados (pasta de teste) pelo utilizador sem intervenção técnica; teste de path traversal/zona inválida no caminho de escrita; teste de "não sobrescrever" (gerar duas vezes o mesmo processo).

### Fase 4 — PDF e paginação em dev (½ a 1 dia)
- Pipeline completo `.docx → PDF` (LibreOffice) + proteção (`qpdf`) integrado na publicação; paginação estática aplicada; validação de que o Anexo I sai sempre em página ímpar mesmo no modo estático.
- Aceitação: PDF final abre sem password, imprime, recusa edição/cópia; comparação visual (não só texto) de 3 a 5 casos entre o PDF da skill (Word) e o PDF do motor novo (LibreOffice) — divergências de layout documentadas e aceites ou corrigidas antes de prod.

### Fase 5 — Prod (½ dia + acompanhamento)
- Automount de escrita na NAS ligado à conta de serviço restrita real; primeiros casos reais publicados pela app, revistos pelo utilizador contra o que a skill teria produzido; skill do Claude mantém-se disponível como via manual/plano B durante um período de sobreposição (decisão do utilizador quando desligar de vez, ver módulo Fichas para o mesmo padrão).
- Aceitação: 5 contratos reais publicados pela app sem correção posterior; utilizador confirma que pode continuar a operar mesmo se o motor novo falhar (skill manual como rede de segurança).

## Riscos

| Risco | Mitigação |
|---|---|
| Porte de PHP do algoritmo de runs/âncoras introduz uma regressão subtil (ex. o bug do `lastRenderedPageBreak` do modelo ARD) | Teste de paridade de texto contra os ~45 casos reais já publicados antes de qualquer publicação nova em prod; `campos.md` como especificação linha a linha |
| LibreOffice não reproduz o layout do Word com fidelidade suficiente (quebras de secção, cabeçalhos, a página em branco real do Anexo I) | Fase 4 dedicada só a isto, com comparação visual explícita; se falhar, ficar com o `.docx` como entregável principal e o PDF como "melhor esforço" — decisão do utilizador se isso for aceitável |
| Paginação dinâmica (mapa de páginas do Word) não é portável para LibreOffice sem UNO, que ninguém testou | MVP sem esse passo (modo estático, já testado e a funcionar hoje via `--sem-word`); UNO fica como exploração futura, não bloqueia as fases 1-5 |
| Primeira escrita da plataforma na NAS — superfície nova de erro (caminho errado, permissões demasiado abertas) | Conta de serviço dedicada restrita à subpasta; validação de caminho contra lista de zonas; nunca sobrescrever; testado primeiro numa subpasta de teste em dev, nunca `Contratos Feitos` real antes da Fase 5 |
| Dois motores (skill manual + app) publicam o mesmo contrato em paralelo durante a transição | Checar sempre se já existe `.docx`/`.pdf` no destino antes de publicar (herdado); acordar com o utilizador um único "modo ativo" por período, não os dois em simultâneo para o mesmo processo |

## Decisões do utilizador em aberto

1. **Marcar "feito" no Sabichão.** A skill nunca escreve no Sabichão; a app podia, com um botão dedicado e auditoria própria, poupar o passo manual. Recomendação: manter a regra atual (nunca escrever) no MVP — é uma decisão de negócio fácil de mudar depois, e evita dar à app uma permissão de escrita ao Sabichão que hoje nem a skill tem.
2. **Papel do Qwen (revisão de coerência do texto final).** Descrito acima como opcional. Recomendação: incluir só depois da Fase 5 estar estável (paralelo ao "Fase 6" de Fichas), como verificação adicional e não bloqueante — o ganho é pequeno face ao risco de acrescentar complexidade a um motor que já é determinístico e testado.
3. **Conta de serviço para a share `Assistente`.** Reaproveitar a mesma conta CIFS já usada para `Processos` (read-only) exigiria dar-lhe também escrita restrita a uma subpasta — mistura permissões de dois módulos na mesma credencial. Recomendação: conta nova, dedicada só a este módulo, com escrita restrita a `Assistente/Contratos Feitos` e leitura a `Assistente/Modelos contratos`, nada mais.
4. **Paginação dinâmica (mapa de páginas via Word/UNO).** Recomendação acima é não a portar no MVP e viver com a limitação conhecida do modo estático (imprime certo, não se vê em branco no ecrã do Word, numeração salta). Só vale a pena investir em UNO se, na prática, isso incomodar RH ou clientes ao ler o documento no ecrã antes de imprimir — pergunta a decidir depois de ver os primeiros casos reais, não antes.
5. **Sobreposição com a skill manual durante a transição.** Como no módulo Fichas, recomendação: manter a skill disponível como via manual até o utilizador confiar na app com um volume razoável de casos reais (mesmo critério qualitativo já usado para Fichas, sem número fixo imposto aqui).

## Revisão do Codex

Não foi possível pedir a segunda opinião ao Codex na sessão anterior: `codex exec` falhou por limite de uso da conta OpenAI ("You've hit your usage limit", renovação indicada para 23/09/2026 00:30). Repetida com sucesso depois da renovação — ver secção seguinte.

## Revisão do Codex (23/09)

Pedido feito depois da renovação da quota (`codex exec` com `model_reasoning_effort=high`), revendo `CONTRATOS-PLANO.md`, `CONTRATOS-DECISOES.md`, `08-CONTRATOS-WRAPPER-ACOES.md` e o código em `app/app/Services/Contratos/`. Achados por prioridade, com o que mudou e o que ficou como decisão de plano (não código).

### Corrigido no código (achados reais, com teste de regressão — commit `9641872`)

1. **`fundirAmarelos()` não comparava o rPr** ao fundir runs amarelos adjacentes, só o highlight — a correção que o próprio comentário da classe já dizia existir ("mesmo critério: rPr igual") não estava no código, a mesma classe de bug já vista antes na skill Python. Corrigido: `rprSemRealce()` compara o rPr normalizado (ignora só `w:highlight`) antes de fundir dois runs.
2. **Entidades XML duplicadas** (`&amp;` → `&amp;amp;`) em qualquer run com "&" não tocado por nenhuma substituição — `runText()` não decodificava as entidades do `w:t`, e o render voltava a escapá-las. Corrigido: `runText()` decodifica `&amp; &lt; &gt; &apos; &quot;`.
3. **`nFalta` contava qualquer `w:highlight` sobrevivente**, incluindo um run "opaco" (fora dos slots reconhecidos) já amarelo no modelo original e nunca tocado — um realce editorial futuro seria interpretado como campo por preencher. Corrigido: conta os marcadores `[FALTA: chave]` reais.
4. **Modelo `.docx` sem `word/document.xml`** (corrompido/incompleto) rebentava com `TypeError` em vez de devolver o resultado estruturado `[null, [], $problemas]` que o `ContratosGerar` já sabe tratar. Corrigido com uma verificação explícita.

Os quatro modelos atuais não disparavam nenhum destes bugs (25 pares de runs amarelos inspecionados sem `rPr` divergente, sem "&" nos textos, sem realce opaco a mais, ficheiros válidos) — são regressões latentes para a próxima edição de modelo ou o próximo caso real, não falhas já vistas em produção.

### Aceite como decisão de plano (não código nesta sessão)

- **Bloquear geração fora da fila / não sobrescrever ficheiro existente** (achado de prioridade alta do Codex). O comando `contratos:gerar` atual é só o motor da Fase 2 — "nunca escreve no Sabichão nem na NAS" por decisão já assumida (`CONTRATOS-DECISOES.md` #1/#4), sem rota de publicação real ligada ainda (essa é a Fase 3). A recomendação do Codex (bloquear `meta.na_fila=false` salvo ensaio explícito, destino restrito a uma raiz validada por `realpath()`, criar sem `ZipArchive::OVERWRITE`) é válida e **fica registada como requisito da Fase 3** (publicar), não implementada agora — implementá-la já no comando CLI de teste da Fase 2 não protege nada, porque a fase que escreve a sério ainda não existe.
- **Mais de uma afetação ativa escolhe silenciosamente a mais recente** (`equipa[0]` + aviso). Fiel ao Python, herda o mesmo risco já materializado nos processos 747/750. Recomendação do Codex: transformar em pergunta bloqueante (a Fase 3 já tem o mecanismo de "perguntas pendentes"). **Aceite para a Fase 3** — não implementado agora porque a Calculadora é motor puro (sem UI de pergunta ainda ligada); registar aqui evita que se perca.
- **Falta de validação/enums nas flags do comando** (`documento`, `especialidade`, modalidade, artigos aceitam qualquer string). Real, mas o próprio Codex nota que "a futura validação HTTP não deve ser a única defesa" — o comando CLI é uma ferramenta de teste da Fase 2, chamada só por quem já tem acesso ao CT; a validação séria (DTO/enum) fica como requisito da Fase 3 (endpoint HTTP/webapp), onde os dados vêm de fora.
- **Gate de paridade "45/45" sobrestima a cobertura** (0 casos ARD testados apesar do critério pedir os 4 modelos; comparação só de texto, não formatação/secções/realces; corpus e script apagados, não repetível; `Calculadora` usa "hoje" interno sem relógio injetável). **Aceite integralmente** — `CONTRATOS-FASE2-RESULTADOS.md` mantém-se com o resultado obtido, mas o critério de aceitação da Fase 2 no plano passa a explicitar: ARD continua **pendente** (não "concluído"), e a Fase 4 (comparação visual) não pode reutilizar o mesmo corpus apagado — tem de gerar um novo, guardado desta vez.
- **`TEXTOS_EXEMPLO` com `123456` como substring genérica pode rejeitar dados reais legítimos** (NIF/NISS/MAI real que contenha essa sequência). Real — já aconteceu nos próprios testes (contornado a mudar os dados de teste, não a corrigir o falso positivo). **Não corrigido nesta sessão**: a correção própria (validar só nos slots/âncoras originais, não como substring global do texto final) é uma mudança de arquitetura da validação, não um bug isolado — fica registada como item da Fase 2 antes de fechar essa fase, para não arriscar quebrar a proteção real (impedir dados de exemplo do modelo de sobreviverem) sem tempo para testar bem.

### Inconsistências documentais corrigidas/anotadas

- `CONTRATOS-PLANO.md` dizia que `consulta.dados_contrato` replica `carregar()+calcular()`; `08-CONTRATOS-WRAPPER-ACOES.md` já estava correto (devolve só dados brutos, cálculo fica no CT) — este ficheiro é a fonte a seguir.
- A secção de segurança deste plano dizia que o módulo "não trata CC ou NISS"; o wrapper devolve `ncc`, NIF e NISS — a classificação/proteção de dados pessoais deste módulo está subavaliada no plano e precisa de ser revista antes da Fase 5 (mesmo nível de cuidado que o módulo Fichas já tem, docs/00-THREAT-MODEL.md).
- O caminho da password do PDF protegido tinha um erro de escrita (`segunder`) — é `segunor`.
- Duas notas menores do `08-CONTRATOS-WRAPPER-ACOES.md` (contagem de queries "9" vs. 10 enumeradas; título "processo não existe ou não está na fila" a contradizer a nota seguinte) ficam para correção editorial na próxima revisão desse ficheiro, sem urgência.
