# Resultados da Fase 2 — motor de geração de contratos (PHP)

Data: 22/09/2026. Motor: `App\Services\Contratos` (Extenso, Regras, Calculadora,
Paragrafo, DocxEngine, Paginacao), comandos `php artisan contratos:gerar` e
`php artisan contratos:paridade`. Porte fiel de
`~/.claude/skills/fazer-contrato/{gerar_contrato.py,extenso_pt.py,paginacao.py}`
conforme `docs/09-CONTRATOS-CAMPOS-MAPA.md`.

## Testes Pest (dados sintéticos, sem dados pessoais)

`tests/Unit/Contratos/` — 18 testes, correm na suite completa do CT 117 (PHP 8.4):
`ExtensoTest`, `RegrasTest`, `CalculadoraTest` (escolha de modelo: termo_certo,
incerto, SPR, ARD/ARE; vencimento FT vigilante e por valor_hora; duração em dias) e
`DocxEngineTest` (substituição de âncoras sem realce, `[FALTA: chave]` a amarelo,
termo certo em dias com troca "meses→dias" e "termo previsto→termo", remoção do
parágrafo de renovação) contra um modelo `.docx` sintético mínimo construído no
próprio teste (`DocxBuilder`, sem dados reais). Suite completa (Fichas + Contratos):
**199 testes, 504 assertions, todos a passar**, sem regressão no módulo Fichas.

## Teste de paridade (obrigatório) contra os contratos já publicados

Corrido no Mac (PHP 8.2 CLI, classes do motor sem dependência da framework Laravel),
script standalone fora do repositório
(`/private/tmp/.../scratchpad/paridade/paridade_run.php`, apagado no fim). Para cada um
dos **48 contratos publicados** em `/Volumes/Assistente/Contratos Feitos/<zona>/`:
reconstruído o `id_processo` a partir do nome+admissão do ficheiro (SELECT só leitura
ao Sabichão PROD, via SSH, réplica exata das 9 queries de `carregar()` na skill),
gerados os mesmos dados que `consulta.dados_contrato` devolveria
(`docs/08-CONTRATOS-WRAPPER-ACOES.md`), corrido `Calculadora::calcular()` +
`DocxEngine::preencherDocx()` com os 4 modelos reais de `/Volumes/Assistente/Modelos
contratos`, e comparado o texto normalizado (`DocxEngine::extrairTexto`, mesmo método
usado pelo comando `contratos:paridade`) contra o `.docx` publicado pela skill.

### Resultado por modelo

| Modelo | Casos reais comparados | Iguais (paridade de texto = 100%) | Paridade |
|---|---|---|---|
| SPR | 33 | 33 | 100% |
| incerto | 5 | 5 | 100% |
| termo_certo | 7 | 7 | 100% |
| ARD | 0 (nenhum dos 48 publicados usa este modelo) | — | ver nota |

**45/45 casos comparáveis com paridade de texto de 100%** (não 95% — os textos
normalizados ficaram byte a byte idênticos, sem nenhuma diferença a explicar). Muito
acima do critério de aceitação do plano (≥95% em pelo menos 10 casos cobrindo os 4
modelos).

### Casos não comparáveis (3/48) — perguntas pendentes, não bugs do motor

Ao correr sem flags (o script de paridade não reproduziu as respostas que o operador
deu na altura), 3 dos 48 processos ficaram com perguntas pendentes, exatamente como a
skill original também teria ficado sem essas mesmas flags:

- **Processo 807**: pergunta "cliente" (cliente inexistente em `clientes`, sem
  afetação em `cliente_equipa`) — é o mesmo caso já documentado no histórico da skill
  (`SKILL.md`, entrada 2026-09-22: "com cliente inexistente em `clientes` ... o script
  listava sempre a pergunta pendente"), resolvido na altura com
  `--cliente-nome`/`--cliente-morada`. Confirma que o motor PHP reproduz a mesma regra
  de negócio (pergunta quando não há dados suficientes), não uma regressão.
- **Processos 510 e 514**: pergunta "especialidade" (ambígua entre os cartões do
  funcionário) — parte do grupo "Festas de Vizela 2026" (processos 508-514,
  `SKILL.md` entrada 2026-09-03(5)), onde nem todos os casos tinham a especialidade
  óbvia; resolvido na altura com `--especialidade`.

Nenhum destes 3 casos é um bug do porte — é o comportamento correto e esperado do
motor sem as respostas às perguntas pendentes que o operador deu originalmente. Os
outros 4 casos do mesmo grupo "Festas de Vizela" (508, 509, 511, 512) **geraram e
compararam com sucesso a 100%**, confirmando também a regra especial de termo certo em
dias (duração de 8 dias, "termo em" sem "previsto", parágrafo de renovação removido).

### Modelo ARD — sem caso real disponível, verificado por completude de âncoras

Nenhum dos 48 contratos já publicados usa o modelo ARD (só SPR, incerto e
termo_certo). Não há, portanto, um `.docx` de referência real para comparar. Em vez
disso, verificou-se que **as 4 âncoras usadas pelo modelo ARD batem todas, sem
problema nenhum**, correndo `DocxEngine::preencherDocx()` diretamente contra
`Contrato_2026_ARD.docx` (o modelo real da NAS) com dados sintéticos completos —
`n_falta=0`, nenhum "Paragrafo com amarelo sem ancora", nenhum texto de exemplo
sobrevivente. Isto cobre o risco específico documentado no plano (bug histórico do
`<w:lastRenderedPageBreak/>` no modelo ARD, corrigido em `Paragrafo::__construct()`
tal como na skill) mas **não é um teste de paridade de texto contra um caso real** —
falta em relação ao critério de aceitação "cobrindo os 4 modelos". Fica registado como
pendente: assim que existir um contrato ARD real na fila, repetir o teste de paridade
completo para esse modelo.

## Bugs corrigidos durante o porte

Nenhum bug de negócio foi encontrado nos 45 casos comparados (paridade 100%, não
95%) — o porte reproduziu exatamente o comportamento da skill, incluindo os casos que
a própria skill só acertou depois de correções históricas (fusão de runs por `rPr`
igual, remoção de `<w:lastRenderedPageBreak/>` do modelo ARD, duração em dias). Dois
problemas foram encontrados e corrigidos **durante o desenvolvimento dos testes**
(não no teste de paridade em si, portanto sem impacto nos 45 casos):

1. `especialidades_n` no porte inicial de `Calculadora::calcular()` ficava a zero
   quando havia uma segunda especialidade, por um erro de "pop" na lista antes de
   contar o tamanho — corrigido para contar a lista original, tal como o Python faz
   (`len(lista_esp)` antes de qualquer fatiagem).
2. Dados de teste sintéticos (Pest e o script de paridade) com números de documento
   como `12345678`/`123456789` colidiam com a lista `TEXTOS_EXEMPLO` (`'123456'` é uma
   das strings proibidas) e faziam o motor recusar-se a gerar — não é um bug do motor,
   é a validação a funcionar corretamente contra dados de teste mal escolhidos;
   corrigido trocando os números de teste.

## O que falta (âmbito não coberto nesta entrega, ver docs/CONTRATOS-DECISOES.md)

- Paridade de texto contra um caso ARD real (nenhum existe ainda na fila).
- Paginação dinâmica (mapa de páginas via Word/UNO) — decisão assumida #2, fora do
  MVP.
- PDF protegido (`qpdf`) e conversão para PDF (LibreOffice) — Fase 4 do plano, não
  desta entrega.
- Publicação na NAS real e wrapper SSH (`consulta.fila_contratos`/
  `consulta.dados_contrato`) — Fase 1/3 do plano; o comando `contratos:gerar` recebe
  os dados diretamente em JSON, sem passar pelo wrapper (decisão assumida #4).
- Revisão do Codex: não foi possível (quota esgotada; ver nota abaixo).

## Revisão do Codex

Tentado `codex review --uncommitted` antes do sync para o CT 117 (gate
`/review-pre-deploy`); sem quota disponível nesta sessão (mesma limitação já registada
em `CONTRATOS-PLANO.md`, renovação prevista 23/09/2026 00:30). **Revisão do Codex
pendente** — repetir assim que a quota renovar, antes de qualquer avanço para a
Fase 3.
