# WORKLOG — Assistente RH (módulo Fichas)

## 2026-09-24 (Porte de funcoes/ocr_docs.php — frente do CC + acentos do nome, continuação) — Claude (Sonnet)
- Resultado: continuação da entrada anterior (mesma tarefa "porte
  ocr_docs.php"), itens 1 e 2 da lista pendente — ver
  `app/docs/PORTE-OCR-DOCS.md` "Estado da 3ª entrega" para o detalhe.
  (1) `OcrDocs::parseCcFrente` integrado em `CartaoCidadao::extrairFrente`:
  ncc/nascimento/validade_cc continuam a sair da leitura própria sempre que
  encontra algo (é a única com o Luhn do ncc e tem as correções dos
  processos reais 773/788/850/854); o porte só preenche quando a própria
  não encontra nada. O nome nunca é substituído pelo porte (só confirmado —
  o fixture `$digital` do teste mostrou o porte a ler "M M M" para o
  layout app.gov.pt e a marcar isso como fiável, teria corrompido o nome
  digital se fosse tratado como substituição). (2) Acentos do nome:
  `LerCaso::confirmarNomePorSegundaFonte` reescrito para usar
  `OcrDocs::escolherNome` sobre os candidatos já recolhidos em
  `$nomesPorOrigem` (cc_frente/cc_digital/rc) — nunca troca as letras, só
  melhora a grafia quando uma fonte acentuada (tipicamente o RC, que já lê
  o nome com acentos do PDF) bate com o nome já lido do CC físico
  (maiúsculas sem acentos); reparte a melhoria por apelido/nome_proprio.
  Confirmado que o `mapeado` do portal nunca traz `nome` (wrapper-ct107) —
  nada a fazer aí, o loop genérico já respeitaria a regra se um dia trouxer.
- Alterado: `app/Services/Fichas/Extratores/CartaoCidadao.php`
  (`extrairFrente`), `app/Jobs/LerCaso.php`
  (`confirmarNomePorSegundaFonte` reescrito + `resplitarNomeCompleto`
  novo), `app/docs/PORTE-OCR-DOCS.md`, `docs/DEPLOY.md`,
  `tests/Feature/Fichas/AcentosNomeTest.php` (novo, 4 testes).
- Testado: os 25 testes de `CartaoCidadaoTest.php` e os 15 de
  `OcrDocsParidadeTest.php` continuam verdes (inalterados); 4 testes novos
  em `AcentosNomeTest.php`; os 3 grupos completos do projeto —
  (a) Unit+Contratos+Leitor = 242, (b) Auth+Fiabilidade+FilaProcessamento+
  Feature soltos = 20, (c) Fichas/* exceto os 2 de integração com o Qwen
  real = 167 — 429 testes, 0 falhas, 24/09/2026, no CT 117.
- Pendente: `ocr_parse_cartao_especialidade` (330 linhas, cartões PSP
  continuam com `Extratores\CartaoPsp` próprio); `ocr_parse_morada`/
  `ocr_parse_iban`/`ocr_parse_passaporte`/`ocr_parse_habilitacoes`;
  `ocr_merge_campos` vs. a política de confiança atual da app. Nada disto
  leva a prod (CT 121/107/Proxmox nunca tocados) — só dev (CT 117).

## 2026-09-24 (Porte de funcoes/ocr_docs.php — verso e frente do CC, continuação) — Claude (Sonnet)
- Resultado: continuação da entrada anterior (mesma tarefa "porte
  ocr_docs.php"). Item 1 completo (porte + fixtures + integração): toda a
  família de `ocr_parse_cc_verso` (NISS/filiação/NCC/morada pela MRZ) —
  ver `app/docs/PORTE-OCR-DOCS.md` "Estado da 2ª entrega" para a lista
  completa das ~28 funções. Item 2 parcial: `ocr_parse_cc_frente` +
  `ocr_pontuar_nome_fonte`/`ocr_escolher_nome` portados e com paridade, mas
  **não integrados** em `extrairFrente` (ver "Pendente" abaixo) — a ligação
  de `recuperarAcentos` ao nome do CC via RC/portal precisa de uma fonte de
  texto que a classe do extrator não tem (fica para a próxima entrega,
  depois de localizar a camada certa).
- Alterado: `app/Support/OcrDocs.php` ganhou ~28 métodos novos (verso:
  `nissBlocoFiscal`, `nissPorAncora`, `nissCandidatos`,
  `nissProvaDocumento`, `nissConsensoDocumento`, `filiacaoQualidade`,
  `filiacaoMelhor`, `mrzDatas`, `mrzNome`, `dataAposLabel`, `nccDeMrz`,
  `mrzNccCandidatos`, `nccConsenso`, `nccTrDeMrz`, `regexArruamento`,
  `ruaPlausivel`, `limparLocalidade`, `cpLocalidadePlausivel`,
  `montarMoradaDeLinhas`, `moradaAposNome`, `limparParteNome`,
  `validarDatasResultado`, `trNumPlausivel`, `numTituloResidencia`,
  `parseCcVerso`; frente: `parseCcFrente`, `pontuarNomeFonte`,
  `escolherNome`) — porte literal, mesma lógica, comentários de
  proveniência mantidos onde explicam uma regra não óbvia.
  `app/Services/Fichas/Extratores/CartaoCidadao.php::extrairVerso` passou a
  chamar `OcrDocs::parseCcVerso` como 1ª leitura para nif (campo novo),
  niss, ncc (campo novo) e filho_de (com `tituloComParticulas` por cima); o
  método próprio da app continua como 2ª leitura/fallback, inalterado
  (cobre os separadores `•`/`·`/`●` que o porte não reconhece).
  `extrairFrente` não foi tocado.
- Testado: 9 testes novos em `OcrDocsParidadeTest.php` (`parseCcVerso`,
  `parseCcFrente`, `nissBlocoFiscal`, `mrzNome`, `mrzDatas`,
  `filiacaoQualidade`, `montarMoradaDeLinhas`, `moradaAposNome`,
  `pontuarNomeFonte`, `escolherNome` — na prática 10 testes, contando
  `escolherNome`), fixtures geradas no CT 100 (`sudo -n cat`, sha256 do
  `funcoes/ocr_docs.php` original inalterado desde a 1ª entrega) com casos
  sintéticos (MRZ, NIF/NISS com checksum calculado — nunca reais). Os 25
  testes de `CartaoCidadaoTest.php` (todos pré-existentes) continuam
  verdes. Os 3 grupos completos: (a) Unit+Contratos+Leitor = 242 passed;
  (b) Auth+Fiabilidade+FilaProcessamento+Feature soltos = 20 passed; (c)
  Fichas/* exceto FichasLerCommandTest/RetryExtracaoTest = 163 passed.
  Total 425 testes, 0 falhas.
- Pendente (ver `app/docs/PORTE-OCR-DOCS.md`, "Estado da 2ª entrega", por
  ordem de prioridade): integrar `OcrDocs::parseCcFrente` em
  `extrairFrente`; localizar a camada que combina CC+RC+portal para ligar
  `recuperarAcentos` ao nome do CC; `ocr_parse_cartao_especialidade`;
  `ocr_parse_morada`/`ocr_parse_iban`/`ocr_parse_passaporte`/
  `ocr_parse_habilitacoes`; `ocr_merge_campos` vs. a política de confiança
  atual. Nada disto foi levado a prod (só dev). Ver `docs/DEPLOY.md`
  §"2026-09-24 (2ª entrega) — Porte de ocr_docs.php: verso e frente do CC".

## 2026-09-24 (Porte de funcoes/ocr_docs.php — camada de classificação) — Claude (Sonnet)
- Resultado: mapa completo das 73 funções de `funcoes/ocr_docs.php` (Sabichão,
  CT 100 dev) em `app/docs/PORTE-OCR-DOCS.md` — função, o que faz,
  dependências, equivalente atual na app, estado (portada/pendente), e a
  lista justificada do que fica de fora (chama binários/ficheiros/BD do
  Sabichão — a app usa o Vision do mini). Dado o volume, esta entrega porta
  e integra só o núcleo de CLASSIFICAÇÃO, onde estava o bug mais concreto
  reportado (verso do CC a ser lido como frente).
- Alterado: `app/Support/OcrDocs.php` ganhou `recuperarAcentos`,
  `classificarPorNome`, `tipoPorTokens`, `classificarPorConteudo`,
  `cartaoEspecialidadeRegex` — porte literal, comentários de proveniência
  mantidos. `app/Services/Fichas/Classificador::classificar` passou a
  chamar `OcrDocs::classificarPorConteudo` como primeira leitura (RC antes
  do CC, habilitações antes do RC, verso do CC antes da frente pela MRZ,
  etc.), mapeando vig/spr/ard/are/coordenador/diretor para `cartao_psp`;
  as regras próprias da app que já lá estavam ficam como fallback (para
  tr_ambos/passaporte e quando o porte devolve null).
- Testado: `tests/Unit/Fichas/OcrDocsParidadeTest.php` compara as 5 funções
  portadas com o PHP ORIGINAL — fixtures em
  `tests/Unit/Fichas/fixtures/ocr_docs_paridade.json` (~65 casos
  sintéticos), geradas correndo `tests/Unit/Fichas/fixtures/gerar_paridade.php`
  no PRÓPRIO ficheiro original no CT 100 (via `sudo -n cat` para o utilizador
  `claude`, sem permissão de leitura direta) — o "out" de cada caso é a
  verdade do Sabichão, não uma expectativa escrita à mão. Corrigido um erro
  de tipo no porte inicial de `classificarPorNome` (devolvia `''` em vez de
  `null` quando o nome não diz nada) — apanhado pelo próprio teste de
  paridade. Os 3 grupos de teste pedidos correram verdes no CT 117: (a)
  Unit+Contratos+Leitor = 232 passed; (b) Auth+Fiabilidade+
  FilaProcessamento+Feature soltos = 20 passed; (c) Fichas/* exceto
  FichasLerCommandTest/RetryExtracaoTest = 163 passed. Nenhum teste antigo
  precisou de ajuste.
- Pendente (ver `app/docs/PORTE-OCR-DOCS.md`, "Estado desta entrega", por
  ordem de prioridade): `ocr_parse_cc_verso` + família NISS/filiação (onde
  está o "NISS e filiação vazios" reportado); `ocr_parse_cc_frente` +
  integrar `recuperarAcentos` nos extratores (onde está o "nomes sem
  acentos"); `ocr_parse_cartao_especialidade`; `ocr_parse_morada`/
  `ocr_parse_iban`/`ocr_parse_passaporte`/`ocr_parse_habilitacoes`;
  comparação de `ocr_merge_campos` com a política de confiança atual da
  app (`docs/PLANO.md`). `classificarPorNome`/`tipoPorTokens` ficaram
  portadas mas não integradas — o pipeline atual não classifica por nome
  de ficheiro. Nada disto foi levado a prod (fora do âmbito — só dev).
  Ver `docs/DEPLOY.md` §"2026-09-24 — Porte de ocr_docs.php".

## 2026-09-24 (Contratos: paginação verificada com o LibreOffice) — Claude (Opus)
- Resultado: implementada a "paginação verificada" do módulo Contratos —
  `App\Services\Contratos\PaginacaoVerificada`, chamada pelo `Gerador` depois
  das regras estáticas (`Paginacao::arrumarEstatico`), usa o LibreOffice como
  oráculo de páginas (`Conversor::converter` + `pdftotext -layout`) para
  corrigir o que sobrar: bloco duplicado+data+assinaturas partido, títulos de
  cláusula órfãos, páginas a começar com parágrafos vazios, e o Anexo I com
  página em branco real quando calha em página par (nunca a secção
  `oddPage`) — porte do passo dinâmico "com o Word" de
  `~/.claude/skills/fazer-contrato/paginacao.py`, mas sem Word (não existe no
  CT). Até 4 iterações (converter→verificar→corrigir). Se sobrar problema, o
  caso fica com aviso "Paginação por rever: ..." e desce a
  `gerado_com_falta` em vez de publicar calado. `DocxEngine` passou a gerar o
  Anexo I com `pageBreakBefore` simples (modo `quebra`) em vez da secção
  `seccao_impar` — decisão do utilizador que substitui
  `docs/CONTRATOS-DECISOES.md` #2 nesta parte, agora que há paginação
  dinâmica no CT.
  Validado a sério com o processo 15 de dev (app, sem publicar): 15 páginas
  (igual aos 12 originais da skill), bloco de assinatura junto, Anexo I em
  página ímpar com a página em branco inserida — confirmado por `pdftotext`.
  O processo 11 já estava `publicado` (sessão anterior) — estado terminal
  respeitado, não contornado só para o teste; cobertura equivalente ficou no
  teste de integração com LibreOffice real (abaixo). `SELECT` final no
  Sabichão dev confirma `processo.contrato`: 15 = `Fazer` (não publicado),
  11 = `Nao` (já assim antes desta entrega). Achado antes do código: as
  fontes Aptos/Aptos Display não estavam instaladas no CT 117 — instaladas
  (`/usr/local/share/fonts/aptos` + `fc-cache`), o que por si só corrigiu o
  contrato do processo 15 de 17 para 14 páginas; licença por decidir pelo
  utilizador antes de replicar em prod.
- Alterado: `app/app/Services/Contratos/PaginacaoVerificada.php` (novo),
  `app/app/Services/Contratos/Paginacao.php` (helpers `texto`/`comPpr`/
  `semPagebreakVazio`/`PAR_RE`/`TITULO_RE` passaram a `public`, reaproveitados
  pela classe nova), `app/app/Services/Contratos/DocxEngine.php` (modo do
  Anexo I: `seccao_impar` → `quebra`), `app/app/Services/Contratos/Gerador.php`
  (injeta `PaginacaoVerificada`, corre depois de `DocxEngine::preencherDocx`
  e antes do PDF final, aviso no manifesto + estado `gerado_com_falta` quando
  não resolve, `paginacao_verificada` no evento `gerado`/`regenerado`),
  `app/config/services.php` (`CONTRATOS_PDFTOTEXT_BIN`), `docs/DEPLOY.md`
  (secção 14: fontes Aptos + armadilha de permissões do perfil do soffice),
  `docs/CONTRATOS-FASE3-RESULTADOS.md` (secção "Paginação verificada com o
  LibreOffice").
- Testado: `tests/Unit/Contratos/PaginacaoVerificadaAnaliseTest.php` (9
  testes, lógica pura, sem soffice — corre sempre), `tests/Feature/Contratos/
  PaginacaoVerificadaTest.php` (2 testes, `.docx` sintético longo, LibreOffice
  real — confirma que sem correção o bloco de assinatura pode partir e com
  `PaginacaoVerificada` fica sempre numa página; salta automaticamente sem
  `soffice`/`pdftotext`), `tests/Feature/Contratos/GeradorTest.php`
  (`PaginacaoVerificada` mockada nos testes de motor). Suite completa no CT
  117 (PHP 8.4): 309 testes, 860 asserções, tudo verde (298 antes + 11
  novos). Achado de infraestrutura durante os testes: o perfil do soffice em
  `storage/app/private/contratos/.soffice-home` tinha ficado sem acesso de
  grupo (`drwx--S---`), o que fazia o `soffice --headless` esgotar o timeout
  de 90s quando corrido como `claude` (SSH) em vez de `www-data` (fila) —
  corrigido com `chmod -R 2770` (ver `docs/DEPLOY.md` secção 14).
- Pendente: `pdftotext` só localiza marcadores conhecidos (não cada
  parágrafo, ao contrário do Word/AppleScript da skill) — suficiente para os
  problemas documentados, mas não é uma cobertura total; fontes Aptos só em
  dev (CT 117), replicar em prod fica para quando o módulo lá for instalado.

## 2026-09-24 (leitor-mini: endpoint de paginação/exportação PDF de contratos) — Claude (Opus)
- Resultado: acrescentado ao serviço leitor-mini (mac mini, 10.0.30.231) o
  endpoint `POST /v1/contrato/paginar-pdf`, que reaproveita a paginação
  dinâmica (Word/AppleScript) da skill `fazer-contrato` do Mac principal para
  arrumar um `.docx` de contrato já gerado e exportar o PDF final. Só a parte
  do mini — `app/` (Conversor/Gerador do CT) não foi tocado, por estar a ser
  mexido noutra sessão; a integração no CT fica para depois. Teste real
  (24/09) com o modelo `Contrato_Termo_Certo_v2.docx` (texto de exemplo do
  modelo, sem dados pessoais), enviado do CT `assistente-dev`
  (192.168.69.150): `HTTP 200`, 14 páginas, ~92,6s, fontes AptosDisplay
  embutidas (`pdffonts`), bloco de assinaturas numa só página (`pdftotext`).
  Achado importante: a automação do Word por AppleScript já funciona sob o
  `launchd` (`GET /v1/saude` devolve `"word":"disponivel"` mesmo corrido pelo
  LaunchAgent), mas o acesso do próprio Python ao contentor do Word
  (`~/Library/Containers/com.microsoft.Word/...`) falha sob o `launchd` com
  `PermissionError` — falta conceder "Acesso Total ao Disco" ao binário
  Python do venv em Definições > Privacidade e Segurança (passo manual, não
  scriptável por SSH; documentado com o caminho exato do binário em
  `leitor-mini/README.md`). O teste real foi por isso feito com um `uvicorn`
  manual por SSH numa porta separada (8091), sem tocar no serviço `launchd`
  real (8090), que ficou intocado e continua a responder normalmente.
- Alterado: `leitor-mini/app.py` (novo endpoint, `WORD_LOCK`, helpers
  `_word_ativo`/`_garantir_word_aberto`/`_paginar_pdf_sync`, campo `word` em
  `/v1/saude`), `leitor-mini/contratos/__init__.py` (novo),
  `leitor-mini/contratos/paginacao.py` (novo — cópia anotada de
  `~/.claude/skills/fazer-contrato/paginacao.py`, só `WORD_TMP` muda),
  `leitor-mini/README.md` (nova secção + registo do teste),
  `docs/06-CONTRATO-LEITOR-MINI.md` (secção 8, novo endpoint). Deploy feito
  por `rsync` para `~/leitor` no mini e `launchctl kickstart -k` do
  LaunchAgent `pt.segunor.leitor` (serviço real corrido e confirmado depois
  do deploy, só sem o passo de TCC).
- Testado: sintaxe (`ast.parse`) e import do módulo (`import app`) no venv do
  mini; `GET /v1/saude` real via `launchd` (word disponível); `POST
  /v1/contrato/paginar-pdf` real via `uvicorn` manual (porta 8091) a partir
  do CT `assistente-dev`, com verificação de páginas/fontes/assinaturas por
  `pdfinfo`/`pdffonts`/`pdftotext`. Todos os temporários (no CT, no mini e o
  LaunchAgent de depuração usado para reproduzir o `PermissionError`) foram
  limpos no fim.
- Pendente: passo manual de TCC (Acesso Total ao Disco) no mini para o
  serviço `launchd` real conseguir usar o endpoint sem precisar do `uvicorn`
  manual; integração no CT (Conversor/Gerador a chamar este endpoint, com
  fallback para LibreOffice) — fora do âmbito desta sessão.

## 2026-09-23 (teste ponta a ponta: contrato/condições/App RH) — Claude (Sonnet)
- Resultado: confirmado em dev, por SELECT direto ao Sabichão (não só pela conferência
  da própria app), que o contrato (modalidade termo certo/duração/renovação/motivo),
  as condições (horas noturnas, duodécimos férias+Natal, dias Seg-Sex) e o App RH
  escolhidos no "Novo caso" ficam gravados corretamente em `processo_contrato`,
  `funcionarios_condicoes` e `status_plataforma_rh`. Caso sintético 4 (MAI 999950, NIF
  fictício 290999120), sessão HTTP real (login+CSRF+cookies) até `inserido_verificado`;
  conferência pós-inserção 33/33 "ok". Nenhum bug encontrado — o fix já commitado em
  `15e3759`/`57c9aba` estava correto; único achado foi uma fixture com nº de cartão PSP
  mal escolhido (sufixo `03x`, fora dos prefixos reconhecidos), sem impacto no âmbito
  testado. Suite Pest: 267 testes, sem alterações. Sabichão dev revertido e confirmado a
  zero; `FICHAS_PROCESSOS_PATH_TESTE` e a pasta de fixtures removidas do CT 117. Ver
  docs/FASE4-RESULTADOS.md, secção "Contrato, condições e App RH — teste de ponta a
  ponta (23/09)".
- Alterado: `docs/FASE4-RESULTADOS.md` (nova secção). Nenhum ficheiro de código.
- Testado: `php artisan test` no CT 117 — 267 passed, 0 falhas.
- Pendente: nada deste teste; Fase 5 (prod) continua por fazer (ver secção final de
  FASE4-RESULTADOS.md).

## 2026-09-22/23 (lacunas do E2E + revisão do Codex) — Claude (Sonnet)
- Resultado: fechadas as duas lacunas menores do teste E2E (docs/TESTE-E2E-2026-09-22.md,
  cenários E e H) e corrigidos 6 problemas reais (P1/P2) apontados pela revisão do Codex
  ao código desde `5f7edc8` (Fase 4 + correções do E2E) e ao módulo Contratos. Suite Pest:
  203 → **224 testes a passar** (21 novos), 0 falhas.
  - Cenário E: linha "Especialidades resultantes: VIG/ARD/..." acrescentada à página do
    caso, calculada dos cartões com `conta_como_especialidade` marcado pela ordem canónica
    VIG/SPR/ARD/ARE (`CartaoPsp::ordenar`, nova), com atualização ao vivo em `app.js`;
    `GeradorManifesto` usava a ordem de gravação dos cartões para o campo `especialidades`
    — corrigido para a mesma ordem (a `Conferencia` já compara como conjunto, sem
    regressão do lado da inserção).
  - Cenário H: confirmado por teste real contra o CT que o botão "Novo utilizador" em
    `/utilizadores` já existe (commit anterior ao E2E) e está ligado à rota certa — sem
    alteração de código, só teste de regressão.
  - Revisão do Codex (Fichas, `codex review --base 5f7edc8`) — 2 achados reais: P1
    `aprovacaoInvalida()` comparava `FichaDocumento::sha256` (a própria BD) contra o hash
    da aprovação, os dois vindos da mesma leitura, nunca detetando um documento
    substituído na NAS depois da aprovação — nova `hashAtualDoDocumento()` recalcula o
    sha256 a partir do ficheiro atual, com a mesma validação de caminho/symlink da
    `DocumentoController`. P2 o mutex `email_enviando_em` ficava preso para sempre se o
    PHP morresse a meio do envio, sem recuperação possível — `reservarEnvioEmail()` passa
    a reivindicar um mutex mais velho que 5 minutos.
  - Revisão do Codex (Contratos, `codex exec` reasoning=high sobre
    `docs/CONTRATOS-PLANO.md`+decisões+wrapper+`app/Services/Contratos/`) — 4 achados
    reais corrigidos: `fundirAmarelos()` fundia runs amarelos adjacentes sem comparar o
    resto do `rPr` (perdia formatação distinta, ex. negrito); `runText()` não decodificava
    entidades XML do `w:t`, duplicando `&amp;` → `&amp;amp;` em qualquer "&" não tocado;
    `nFalta` contava qualquer `w:highlight` sobrevivente (incluindo um run opaco fora dos
    slots) em vez dos marcadores `[FALTA: chave]` reais; um modelo `.docx` sem
    `word/document.xml` rebentava com `TypeError` em vez de um problema estruturado. Os
    restantes achados (bloqueio de fila/overwrite, afetação ativa dupla, validação de
    flags, gate de paridade ARD "pendente" não "concluído", `TEXTOS_EXEMPLO` como
    substring) ficaram registados como decisões de plano na secção "Revisão do Codex
    (23/09)" de `docs/CONTRATOS-PLANO.md`, por pertencerem a uma fase (Fase 3 de
    Contratos) ainda não construída ou por exigirem mudança de arquitetura fora do âmbito
    desta sessão.
- Alterado: `app/app/Services/Fichas/Extratores/CartaoPsp.php` (`ordenar`),
  `app/app/Services/Fichas/GeradorManifesto.php`, `app/public/js/app.js`,
  `app/resources/views/fichas/mostrar.blade.php`,
  `app/app/Http/Controllers/FichasController.php` (`hashAtualDoDocumento`,
  `reservarEnvioEmail`), `app/app/Services/Contratos/{Paragrafo,DocxEngine}.php`,
  testes correspondentes (`tests/Feature/Fichas/{MostrarTest,GeradorManifestoTest}.php`,
  `tests/Feature/{UsersTest,Fichas/AplicarDevTest}.php`,
  `tests/Unit/Fichas/CartaoPspTest.php`, `tests/Unit/Contratos/{ParagrafoTest,DocxEngineTest}.php`),
  `docs/CONTRATOS-PLANO.md`. Sincronizado para o CT 117 por rsync + reinício do
  php-fpm a cada bloco. Nada em produção.
- Testado: suite Pest completa no CT a cada commit (218 depois do bloco 1, 224 no fim);
  confirmado por HTTP real (login curl) a linha "Especialidades resultantes" nos 5 casos
  da fila de dev (157459 → `—`, 156614 → `VIG/ARD/ARE`, 125457 → `VIG/SPR/ARD/ARE`, 900010
  → `VIG`, 900020 → `—`) e o botão "Novo utilizador" em `/utilizadores`.
- Pendente: nenhum achado real por corrigir desta revisão. Itens de plano de Contratos
  (bloqueio de fila/overwrite, pergunta bloqueante para afetação ativa dupla, validação de
  flags, `TEXTOS_EXEMPLO` por posição em vez de substring) ficam como requisitos da Fase 3
  do módulo Contratos, documentados em `docs/CONTRATOS-PLANO.md`.

## 2026-09-22 (teste E2E independente, testador ≠ autor) — Claude (Sonnet)
- Resultado: teste ponta a ponta por HTTP real (não artisan) de todos os cenários pedidos
  (A a I) contra DEV (192.168.69.150), com o motor/worker/wrapper/Sabichão dev a sério.
  7 bugs reais encontrados e corrigidos (relatório completo com tabela por cenário em
  `docs/TESTE-E2E-2026-09-22.md`): permissões `rwx------` em `index.blade.php` (500 na fila
  logo após login); mensagens de validação nativas do Laravel em inglês por falta de
  `lang/pt`/`lang/en` no projeto; MAI+NIF+admissão duplicado dentro da mesma lista de lote
  não detetado (a pré-visualização mentia "2 linhas OK", só 1 caso era criado, em silêncio);
  leituras alternativas Vision/Qwen sem botão "usar" (regredidas para `<span>` de texto,
  apesar do CSS já as esperar como botões); **sessão de um utilizador desativado continuava
  com acesso total até expirar sozinha** (falha de segurança — corrigida com middleware
  `GarantirUtilizadorAtivo`); **um campo REVER confirmado sem mudar o texto (ex. clicar
  "usar" num valor que já lá estava) ficava bloqueado para sempre**, o bug mais grave do
  teste, só descoberto ao correr o cenário F a sério com um caso sintético completo (registar
  → ler → rever → aprovar → editar depois de aprovado, anula → aprovar de novo → dry-run →
  aplicar em DEV → conferência → PDF → email → idempotência → histórico); e a página
  Fiabilidade sem o link "Voltar à fila". Suite Pest 179 → 203 testes (24 novos, um por bug
  real). Documentada em `docs/DEPLOY.md` §10 uma armadilha de deploy nova: alterar um
  ficheiro PHP já carregado não basta com `optimize:clear`, é preciso reiniciar o `php-fpm`
  (o OPcache do pool fica preso ao bytecode antigo, `tinker` via CLI mascara o problema por
  não partilhar esse OPcache).
- Alterado: `/Volumes/www/assistente/app/` (`lang/en/` e `lang/pt/` novos, `FichasController.php`
  — duplicado no lote + confirmação de campo rever, `mostrar.blade.php`/`app.js` — botões
  "usar" + `confirmados[]`, `fiabilidade/index.blade.php` — link em falta, middleware
  `GarantirUtilizadorAtivo.php` novo + registado em `bootstrap/app.php`, testes novos em
  `NovoCasoTest`/`MostrarTest`/`LoginTest`/`FiabilidadeTest` novo); `docs/DEPLOY.md` §10
  (armadilha do OPcache) e `docs/TESTE-E2E-2026-09-22.md` novo. CT 117 sincronizado a cada
  correção, `php-fpm` reiniciado via `pct exec` quando necessário (sem sudo no `claude`).
  Sabichão dev (CT 100): reserva sintética criada por SQL para o caso do cenário F
  (id_processo 279, NIF 290005230, série `2900052xx` nunca usada em prod) e revertida no
  fim, confirmado por `SELECT` (tudo a 0). `FICHAS_PROCESSOS_PATH_TESTE` e a pasta
  `storage/app/e2e-fixtures/` do CT 117 removidas outra vez no fim. Utilizador de teste
  `teste_e2e` apagado.
- Testado: os 203 testes Pest a passar depois de cada correção; todos os cenários A-I por
  HTTP real com sessão/cookies/CSRF (não vistos num browser real por falta de um disponível
  neste ambiente — ver secção "Não testado" do relatório). Fila de dev deixada com os 3
  casos reais (157459/156614/125457, `por_rever`, nenhum aprovado nem aplicado) mais os 2
  casos sintéticos históricos (900010 da Fase 4, 900020 deste teste, ambos
  `inserido_verificado`) como evidência, seguindo o precedente já existente.
- Pendente: 2 gaps menores encontrados mas não corrigidos (fora do que foi pedido como
  bug bloqueante) — a linha "Especialidades resultantes" do mockup não aparece na página do
  caso (só as checkboxes, que funcionam); `/utilizadores` não tem link visível para criar um
  novo utilizador (a rota existe e funciona por URL direto). Interação visual real em browser
  (zoom, painel de imagem, troca de tema a olho) por confirmar por um humano.

## 2026-09-22/23 (Fase 4 — dry-run e apply em dev) — Claude (Sonnet)
- Resultado: Fase 4 implementada de ponta a ponta — `Aplicador`/`Conferencia` novos, rotas `aplicar-dev`/`reconciliar`/`pdf`/`email`/`email-lote`, wrapper com `ficha_pdf` implementado de facto, `consulta.alvo`, idempotência do apply revista (lock por PID, marca `INCERTO`), email não-idempotente protegido por mutex (`email_enviando_em`) e por `confirmar_reenvio` explícito. Blade dos botões/tabelas novas pedido ao Qwen (`delegar-mini`, Pedido 4, docs/FASE3-QWEN.md), 1 bug real corrigido na revisão (form fora dos limites). 12 rondas completas de `codex review --uncommitted` (13ª interrompida por limite de uso da conta OpenAI, ~00:30 de 23/09) — todos os P1 corrigidos (ver docs/FASE4-RESULTADOS.md para a lista completa), incluindo 2 rondas sobre a própria correção de uma ronda anterior (idempotência do apply refeita 3x até fechar as janelas de corrida). 179 testes Pest a passar no CT (145 + 34 novos). Worker instalado como serviço systemd (`deploy/assistente-queue.service`, `User=www-data`) via root do Proxmox. `wkhtmltopdf` instalado em dev (CT 100) a partir do `.deb` estático oficial (não existe no apt do Debian 13). **Teste ponta a ponta real** com um caso 100% sintético (MAI 900010, NIF fictício com checksum válido 290005124, fixtures geradas com Pillow no mini): registar → ler → rever/editar → aprovar → dry-run → aplicar em DEV → conferência (25/25 campos `ok`, incluindo zona/especialidades/horas) → PDF → email (redirecionado para a caixa de teste, nunca para o destinatário real) — tudo pela webapp a sério (sessão HTTP, login, CSRF), não por comandos artisan. 3 bugs reais só descobertos por este teste, todos corrigidos: `SegundaLeituraQwen` rebentava com um campo não-escalar do Qwen; as chaves SSH do wrapper ficaram inacessíveis ao `www-data` assim que o worker deixou de correr manualmente como `claude` (ownership corrigido, `chown www-data:www-data` + `600` — uma tentativa intermédia com grupo `640` funcionava para a app mas era recusada pelo próprio OpenSSH quando corrido como `claude`, o que só a suite Pest expôs); `WRAPPER_CHAVE_APPLY`/`WRAPPER_APPLY_ATIVO` estavam documentados mas nunca escritos no `.env` real.
- Alterado: `/Volumes/www/assistente/app/` (`app/Services/Fichas/Aplicador.php` e `Conferencia.php` novos, `FichasController.php`, `GeradorManifesto.php` — fix zona1/local_servico, `SegundaLeituraQwen.php` — fix campo não-escalar, `LocalizadorPasta.php`/`AppServiceProvider.php` — segundo caminho de teste, `Cruzamentos.php` — `alvo()`, `FichaCaso.php`, migrações `2026_09_23_*`, blades, testes, `config/services.php`); `wrapper-ct107/` (`wrapper.php` reescrito em várias secções, `ficha_pdf.php` novo, `install.sh`, `README.md`); `docs/` (`03-CONTRATO-WRAPPER-CT107.md` revisto, `FASE4-RESULTADOS.md` novo, `DEPLOY.md` com as secções 10-12 novas, `FASE3-QWEN.md` +Pedido 4). CT 117: código sincronizado, `.env` com `WRAPPER_CHAVE_APPLY`/`WRAPPER_APPLY_ATIVO=dev`, serviço systemd instalado, ownership das chaves SSH corrigido, `claude` acrescentado ao grupo `www-data` (sem efeito prático depois da correção final de ownership, mas inofensivo). CT 100 (Sabichão dev): wrapper reinstalado 6x ao longo das correções (release mais recente `20260922221402`... e seguintes até à versão final), `wkhtmltopdf` instalado. Sabichão dev BD: só o caso sintético do teste ponta a ponta (revertido no fim, ver abaixo) — nada de dados reais tocado.
- Testado: 179 testes Pest a cada bloco de alteração (0 falhas na versão final); teste ponta a ponta manual completo descrito acima, com resultados reais de cada passo (dry-run mostrou as 9 operações que o apply ia fazer; conferência com as 25 linhas e estados reais; PDF confirmado por `file`; email confirmado pelo texto de saída real do wrapper).
- Pendente (Fase 5/prod): chave de apply de prod nunca gerada nem instalada; `wkhtmltopdf` não instalado no CT 107; decidir em prod se o worker/php-fpm corre como `www-data` desde o início (evita a armadilha de ownership descoberta aqui); "Aplicar em PROD" continua sem rota nenhuma.

## 2026-09-22 (Qwen como segunda leitura) — Claude (Sonnet)
- Resultado: terminado o WIP "Qwen como segunda leitura" encontrado por commitar (`SegundaLeituraQwen.php` novo + alterações em `LerCaso`/`FichasLer`/`FichasValidar`/`FichasController`). rc/morada/iban vão sempre ao Qwen (leitura independente sobre a imagem); recurso automático a campos do manifesto vazios/"rever" com documento candidato (CC, cartão PSP), uma chamada por documento+tipo (cache na execução); regra de confiança Vision=Qwen→alta, só Qwen→rever "Só lido pelo Qwen: confirmar", divergem→rever "Vision e Qwen divergem" com os dois valores para os botões "usar"; comparador semântico em morada/nome/naturalidade quando a igualdade literal falha; falha do Qwen nunca bloqueia (campo rever "Qwen indisponível" + evento no caso); modelo do Qwen registado em `fichas_extracoes` a partir de `/v1/saude` (quantização fica `null` — o `/v1/saude` real não a devolve separada do modelo, só `modelo`); tempos por documento no log sem conteúdo. Corrigido no WIP: `concelho` do RC (lido pelo Vision, presente no schema do Qwen) não estava mapeado — ficava sempre sem 2.ª leitura.
- Alterado: `/Volumes/www/assistente/app/` (`app/Services/Fichas/SegundaLeituraQwen.php` novo, `app/Jobs/LerCaso.php`, `app/Console/Commands/FichasLer.php`, `app/Console/Commands/FichasValidar.php`, `app/Http/Controllers/FichasController.php`, `tests/Feature/Fichas/SegundaLeituraQwenTest.php` novo com 12 testes), commit único no repo do Mac. Sincronizado para o CT 117 (`/var/www/assistente`) por rsync. `docs/FASE3-RESULTADOS.md` (secção nova com a releitura antes/depois). Nada escrito no Sabichão.
- Testado: 145 testes Pest a passar no CT (0 falhas), incluindo os 12 novos com `Http::fake` (nunca contra o mini real) para recurso automático, cache por documento+tipo, campo já em alta nunca reaberto, falha do Qwen não bloqueia, cartão PSP completado. Releitura real dos 3 casos (MAI 157459/156614/125457, `fichas:ler --sem-cruzamentos`) contra o mini real, comparada com uma releitura de controlo do código anterior (commit `5f1c2f8`, sem Qwen) feita e revertida na mesma sessão (confirmado por `diff` contra a fonte + suite completa a passar depois de repor): sobe a confiança de vários campos confirmados por duas fontes, recupera `morada`/`niss` que o Vision não tinha lido, e desce corretamente dois campos que o Vision tinha marcado "alta" por engano (`naturalidade` e `iban` do caso 125457) — a política de confiança a funcionar como pretendido. Tempos: ~33–40 s por caso (um pico de 2m18s numa corrida, atribuído a carga do mini).
- Pendente: `fichas_fiabilidade.leitor` continua a gravar só `"vision"`/`"qwen"` (não `qwen:<modelo>:<quantização>` como o PLANO.md descreve) — pré-existente da Fase 3, fora do âmbito desta sessão; quantização do Qwen sem fonte real (ver acima); resto igual às entradas anteriores (systemd do worker, firewall pf, Fase 4 — dry-run/apply/PDF/email).


## 2026-09-22 (Fase 3, sessão autónoma) — Claude (Sonnet)
- Resultado: Fase 3 (webapp) implementada de ponta a ponta — autenticação própria (username+password, Argon2id, rate limit 5/min, comando `utilizadores:criar`), layout com tema claro/escuro por utilizador, fila com filtros/contadores do leitor, novo caso individual e em lote, página do caso (tabela de campos editável, checklist, cartões PSP), aprovação imutável (manifesto+hashes, alimenta `fichas_fiabilidade`), dry-run, rota de documentos protegida (path traversal/symlink/MIME por conteúdo, PDF rasterizado por `pdftoppm`), utilizadores, fiabilidade. Blade/CSS pedidos ao Qwen (1 pedido, `delegar-mini`, sem repetir — ver docs/FASE3-QWEN.md); todo o resto (rotas, controladores, form requests, segurança, testes) escrito pelo Claude. 110 testes Pest a passar (76 da Fase 2 + 34 novos). Teste manual real: 3 casos da NAS (MAI 157459/156614/125457) registados pela webapp a sério, lidos pelo worker, ficaram `bloqueado`/`sem_reserva` (comportamento correto — a Sabichão de dev não tem as reservas desses processos reais de prod); rota de documentos confirmada com JPEG/PNG/PDF reais.
- Alterado: `/Volumes/www/assistente/app/` (fonte no Mac, 7 commits: auth+controladores+testes, views Qwen+correções, fix binding LocalizadorPasta, fix colisão fichas_extracoes num retry, fix redespacho após falha técnica, resultados finais), `/var/www/assistente` no CT 117 (rsync + migração `2026_09_22_000011` + permissões `storage`/`bootstrap/cache` abertas a `www-data`, sem sudo). Nada escrito no Sabichão (nem dev nem prod) além do `consulta.reserva`/`consulta.funcionario`/`consulta.zonas` já existentes (só leitura).
- Testado: suite Pest completa a cada bloco (108→110, 0 falhas). 3 bugs reais só descobertos pelo teste manual com o worker/wrapper a sério (nenhum aparecia nos testes síncronos da Fase 2): binding em falta do `LocalizadorPasta` na fila, colisão de `fichas_extracoes` num retry, e nenhum redespacho automático depois de uma falha técnica recuperável — todos corrigidos com teste de regressão. Também descoberto e corrigido: `storage/`/`bootstrap/cache` sem escrita para `www-data` (qualquer view nova dava 500 mudo, sem log).
- Pendente: Fase 4 (dry-run/apply real, PDF, email) — botões já na UI mas desativados; `systemd` do worker por instalar (sem sudo; unit file pronto em `deploy/assistente-queue.service`, worker corrido à mão em background durante os testes); os 3 casos reais ficam em `bloqueado` na fila de dev, por decisão da RH (criar reserva em dev ou aceitar o bloqueio) — nunca aprovados nem aplicados.

## 2026-09-22 — Claude
- Resultado: plano aprovado pelo utilizador (docs/PLANO.md), revisto com segunda opinião do Codex; teste de OCR nativo concluído (65/65 cartões PSP, 38/38 CC) — ferramentas e resultados em ~/.claude/skills/inserir-funcionario/ocr-mini/.
- Alterado: só documentação. Nada construído. Mac mini preparado (Claude Code, Codex, tmux, VS Code, chaves; LM Studio com Qwen 3.8 27B; binário ~/ocr/ocr compilado).
- Testado: OCR Vision vs Sabichão em 40 processos (só leitura).
- Pendente: Fase 0 (contratos em docs/ + golden dataset adjudicado pelo utilizador). Modelo de trabalho: Sonnet para o mecânico e testes, Fable para decisões/revisão.

## 2026-09-22 (tarde) — Claude (Sonnet para os docs, Fable na revisão)
- Resultado: Fase 0 escrita: docs 00 a 07 em docs/ (threat model, schema+guia do manifesto, máquina de estados, contrato do wrapper CT 107, política de confiança com casos de teste, conjunto de validação, contrato do leitor do mini, decisões de fecho).
- Alterado: só docs/. Decisão do utilizador integrada: sem adjudicação manual do conjunto de validação; a verdade vem das correções na app.
- Testado: schema JSON válido (json.load). Conteúdo dos docs revisto por amostragem, não linha a linha.
- Pendente: revisão do utilizador aos docs (critério de aceitação da Fase 0); Fase 1 precisa de CT de dev novo no Proxmox (autorização do utilizador).

## 2026-09-22 (fim do dia) — Claude (Fable a coordenar, Sonnet nos blocos)
- Resultado: Fase 1 quase fechada. CT 117 `assistente-dev` (192.168.69.150, Debian 13, PHP 8.4, MariaDB, nginx, esqueleto Laravel 12 em /var/www/assistente com .env MySQL e fila em BD). Serviço leitor no mini a correr por launchd (HTTPS+token, /v1/ocr, /v1/analisar-documento, /v1/comparar, /v1/saude; fonte em leitor-mini/). Wrapper SSH restrito instalado no Sabichão DEV (utilizador `assistente`, 2 chaves geradas no CT 117, release root-owned; fonte em wrapper-ct107/). Sistema visual aprovado por mockup (tema claro/escuro, datas dd/mm/aaaa); PLANO.md atualizado.
- Alterado: Proxmox dev (CT 117 novo; fstab do host com montagem CIFS que FALHA por permissão), mini (~/leitor, LaunchAgent), Sabichão dev (utilizador/sudoers/opt/assistente). Prod: só o email às RH. Nada no CT 107.
- Testado: leitor a partir do CT 117 (saude 200, sem token 401); wrapper a partir do CT 117 (consulta.zonas OK, shell recusada, apply pela chave de consulta recusado); nginx 200 no CT 117. Não testado: leitura de documentos reais pelo leitor (só sintéticos), montagem da NAS no CT.
- Pendente: conta NAS só de leitura para Processos (bloqueado: conta proxmox sem permissão; criação por API travada pelo modo automático); utilizador técnico no CT 107 (autorização do utilizador); firewall pf no mini (sudo); contexto do LM Studio a 65k; Fase 2 (motor).

## 2026-09-22 (sessão autónoma) — Claude (Sonnet)
- Resultado: Fase 2 (motor) implementada de ponta a ponta — migrações+modelos das 9 tabelas fichas_*, máquina de estados em `FichaCaso`, `App\Support\Datas` e `App\Support\OcrDocs` (cópia versionada de `funcoes/ocr_docs.php`, sha256 documentado), `LeitorClient`, extratores (CartaoPsp/CartaoCidadao/RegistoCriminal/Morada/Iban), `PoliticaConfianca`, `Cruzamentos` (wrapper SSH, só consulta.*/dry_run/verify), `GeradorManifesto` (validado com opis/json-schema), job `LerCaso`, comandos `fichas:ler` e `fichas:validar`. 50 testes Pest (49 passam, 1 skip automático — leitor sem token real). Detalhe completo, bugs reais apanhados e corrigidos, e o que falta em `docs/FASE2-RESULTADOS.md`.
- Alterado: `/Volumes/www/assistente/app/` (fonte no Mac, repo git criado, 7 commits); `/var/www/assistente/` no CT 117 (rsync de volta, composer install, migrate --force, docs/ copiado para o GeradorManifesto validar o schema). Nada escrito no Sabichão (nem dev nem prod) — só leitura testada no wrapper (não exercitada nesta sessão, ver §4.6 do FASE2-RESULTADOS.md).
- Testado: `composer update` (subiu Pest 3→4 por incompatibilidade com PHPUnit 12 já fixado no esqueleto), `php artisan migrate --force` (10 migrações OK), `./vendor/bin/pest` (1 skipped, 49 passed, 72 assertions, 1.73s) — números reais, não inventados.
- Pendente: Fase 1 por fechar (NAS não montada, token real do leitor não copiado para o CT); `pdo_sqlite` não instalado no CT 117 (sem sudo) — testes a correr contra MySQL de dev em vez de sqlite em memória; `fichas:validar` não corrido sobre os 40 processos reais (precisa de NAS + wrapper); Fase 3 (webapp) por começar.

## 2026-09-22 (noite) — Claude
- Resultado: Fase 2 (motor) a funcionar de ponta a ponta contra o leitor real do mini com fixtures sintéticas: cartão PSP (número/sufixo/validade, alta), CC (ncc 12 chars + Luhn OK = alta; nascimento, validade, apelido/nome = rever), sem duplicados ao reler, datas dd/mm/aaaa na UI. 57 testes Pest a passar no CT. Commits até 3be2f15.
- Alterado: app/ (LerCaso, LeitorClient uso, Classificador, extratores CC/PSP/IBAN, comandos, testes, fixtures), docs/FASE2-RESULTADOS.md. CT 117 sincronizado. Wrapper de CONSULTA instalado no CT 107 (prod) com from= 192.168.69.150,192.168.4.2 (NAT dev→prod).
- Testado: fichas:ler --sem-cruzamentos com leitor real; suite Pest (nota: os testes limpam a BD de dev — RefreshDatabase). NÃO testado: 40 processos reais (NAS não montada), RC/morada/IBAN com imagens (só testes unitários com texto), cruzamentos contra prod a partir do motor.
- Bugs apanhados na verificação: chaves pages/lines vs paginas/linhas (leitura vazia silenciosa), falhas do leitor engolidas, regex sem /u, classificador CC a exigir "republica portuguesa", validade CC atrás do nº do documento, rsync de pastas com espaços a falhar em silêncio, `._*/` a fechar um docblock.
- Pendente: conta NAS `assistente` (utilizador, no DSM); firewall pf no mini e contexto LM Studio 65k (sudo/UI do utilizador); Fase 3 (webapp) sobre o sistema visual aprovado.

## 2026-09-22 (correções pós-validação) — Claude (Sonnet)
- Resultado: corrigidos os problemas concretos da 1.ª validação com 40 processos reais (docs/validacao-fase2.md): validade_cc (regras de sanidade dia/mês/ano + carimbo de assinatura digital do CC digital), val_rc (achado o bug de fundo — o RC batia na regra do CC por "IDENTITY CARD NUMBER" e nunca chegava a ser lido), iban (âncora PT50 + tolerância a quebra de linha entre prefixo e resto do número + máscara com dígitos antes das estrelas), nacionalidade (limpeza generalizada do resto da etiqueta "(NATIONALITY)"), nome no CC físico (limite de varrimento corrigido + confirmação cruzada por 2.ª fonte). Bugs adicionais encontrados SÓ pela re-validação real (não estavam na lista original): Classificador classificava RC como CC (causa raiz da maioria dos "não lidos"), pasta "Proc" sem número ficava invisível ao motor (MAI 175801, todos os campos por ler de uma vez). Nova ação `consulta.ficha` no wrapper (colunas do manifesto + id_processo/admissao/estado/email), instalada em DEV (release 20260922153544 no CT 100), `fichas:validar` já a usá-la e a gravar o id_processo do Sabichão em vez do id interno do caso.
- Alterado: `app/` (CartaoCidadao, Classificador, Iban, OcrDocs, LocalizadorPasta, LerCaso, Cruzamentos, FichasValidar + testes), `wrapper-ct107/wrapper.php` (+README), `docs/03-CONTRATO-WRAPPER-CT107.md`, `docs/validacao-fase2.md` (novo). Sincronizado para o CT 117 por rsync. Wrapper novo só em DEV (CT 100) — prod intocado.
- Testado: 76 testes Pest a passar (sem avisos) a cada bloco de alteração; validação completa recorrida 4x contra prod (gabarito.tsv) até fechar todas as divergências (explicadas) e zerar os "não lidos"/lixo — números finais: validade_cc 39/40 iguais (1 diferente explicado, 0 não lidos, antes 34/3/3); val_rc 33/40 iguais (4 diferentes explicados, 3 não lidos — doc genuinamente ausente, antes 30/2/8); cartões PSP 67/68 lidos iguais (antes 65/67); iban e nacionalidade sem lixo nenhum (antes ~15 casos de nacionalidade e 2 de iban).
- Pendente: naturalidade/concelho têm o mesmo padrão de etiqueta residual da nacionalidade, não corrigido (nomes de lugar são multi-palavra, mais arriscado); `Cruzamentos::funcionario()` lê a chave errada do wrapper (bug pré-existente encontrado por inspeção, não corrigido, fora do âmbito); `consulta.ficha` só em dev, prod por autorizar.

## 2026-09-22 (tarde/noite, 2ª parte) — Claude
- Resultado: NAS montada no CT 117 (conta `assistente` ro, bind-mount do host Proxmox dev). Validação REAL nos 40 processos (770-851) contra prod: cartões PSP 67/68 iguais, validade CC 39/40, val_rc 33/40 (4 diferentes = motor certo/prod desatualizado, 3 sem documento); iban/nacionalidade/nome limpos. 76 testes Pest. Wrapper com `consulta.ficha` instalado em dev e em PROD (CT 107, release 20260922160526, só chave de consulta). docs/validacao-fase2.md.
- Alterado: app/ (extratores, classificador, LocalizadorPasta "Proc" sem número, Cruzamentos, fichas:validar), wrapper-ct107/ (consulta.ficha), CT 117 sync, CT 100 dev e CT 107 prod (nova release do wrapper). Prod BD: só SELECT.
- Testado: suite Pest no CT; fichas:validar nos 40 processos com leitor real; consulta.ficha em prod a partir do CT 117.
- Pendente: firewall pf no mini (utilizador); contexto LM Studio 65k; prod tem valores por omissão "2026-01-01" em val_rc (138294, 117245, 163305) e datas desatualizadas em 6 casos (lista no docs/validacao-fase2.md §2) — comunicar aos RH; Fase 3 (webapp).

## 2026-09-22 (fim) — Claude
- Resultado: Fase 3 (webapp) no ar em dev: http://192.168.69.150 (login rh, password em storage/app/.rh-pass no CT). 110 testes Pest. 3 casos reais (157459, 156614, 125457) lidos pelo motor e visíveis na fila em "por rever". Qwen escreveu Blade+CSS (1 pedido, 12 min, 11k tokens; 5 correções mecânicas, 0 de segurança) — docs/FASE3-QWEN.md.
- Alterado: app/ (auth, rotas, controladores, views, rota de documentos, utilizadores, fiabilidade, testes), phpunit.xml → BD assistente_test (a suite limpava a BD de dev; BD criada no CT), deploy/assistente-queue.service, docs/FASE3-RESULTADOS.md, docs/DEPLOY.md. Worker a correr à mão no CT (systemd por instalar, precisa de root).
- Testado: login HTTP real + fila 200 com os 3 casos; suite completa; rota de documentos com JPEG/PNG/PDF reais (pelo agente). NÃO testado por um humano no browser.
- Pendente: teste do utilizador no browser; firewall pf no mini; systemd do worker; Fase 4 (dry-run/apply em dev, PDF da ficha, email).

## 2026-09-22 (correções pós-teste em browser) — Claude (Sonnet)
- Resultado: 6 correções do primeiro teste real do utilizador na página do caso (docs/FASE3-RESULTADOS.md, secção própria): botão `[img]` em cada linha de campo (documento_id/pagina novos em fichas_campos) + secção "Documentos da pasta" (um `[img]` por página, contagem via pdfinfo); apelido/nome_proprio escondidos da tabela; só campos do manifesto na tabela, pela ordem da skill, criados vazios/"rever" quando em falta; datas dd/mm/aaaa em toda a UI (App\Support\Datas); ncc só com os 8 dígitos do documento (Luhn continua sobre os 12 chars, texto "Luhn OK/falhou" sempre visível); filho_de/nome/naturalidade/concelho/nacionalidade normalizados em Title Case com partículas (OcrDocs::tituloComParticulas, partilhado). Corrigido também um bug lateral no `fichas:ler` (CLI) que passou a mostrar sempre "Luhn falhou" por recalcular sobre os 8 dígitos truncados.
- Alterado: app/ (migração `2026_09_22_000012`, FichaCampo, FichasController, LerCaso, CartaoCidadao, OcrDocs, FichasLer, resources/views/fichas/mostrar.blade.php, testes). Blade feito diretamente pelo Claude (não delegado ao Qwen — justificação em docs/FASE3-QWEN.md, Pedido 2). CT 117 sincronizado + `migrate --force`.
- Testado: 121 testes Pest a passar (110 + 11); os 3 casos reais (157459/156614/125457) relidos com `fichas:ler --sem-cruzamentos`; confirmado por HTTP real (login curl) no caso 157459 — tabela na ordem certa, datas dd/mm/aaaa, ncc 8 dígitos + Luhn visível, `[img]` por linha a devolver `200 image/jpeg`, secção "Documentos da pasta" com um botão por página.
- Pendente: teste do utilizador no browser a esta 2ª ronda; resto igual à entrada anterior (systemd do worker, firewall pf, Fase 4).

## 2026-09-22 (Contratos, Fase 0 + Fase 2) — Claude (Opus)
- Resultado: módulo Contratos — Fase 0 (docs/08-CONTRATOS-WRAPPER-ACOES.md, docs/09-CONTRATOS-CAMPOS-MAPA.md, docs/10-CONTRATOS-ESTADOS.md, docs/CONTRATOS-DECISOES.md) e Fase 2 (motor `App\Services\Contratos`: Extenso, Regras, Calculadora, Paragrafo, DocxEngine, Paginacao — porte fiel de `~/.claude/skills/fazer-contrato/{gerar_contrato.py,extenso_pt.py,paginacao.py}`; comandos `contratos:gerar` e `contratos:paridade`). Teste de paridade obrigatório contra os 48 contratos já publicados pela skill (SELECT só leitura ao Sabichão PROD, script standalone no Mac): 45/48 comparáveis, **45/45 com paridade de texto de 100%** (33 SPR, 5 incerto, 7 termo_certo, incluindo o grupo "Festas de Vizela" em dias); 3/48 (807, 510, 514) com perguntas pendentes sem flags — mesmo comportamento documentado da skill, não bugs. Modelo ARD sem caso real publicado; verificado à parte por completude de âncoras contra o modelo real (0 problemas). 18 testes Pest novos com modelo `.docx` sintético (sem dados pessoais). Suite completa no CT 117: 199 testes, 504 assertions, sem regressão em Fichas.
- Alterado: `app/app/Services/Contratos/*.php` (6 classes), `app/app/Console/Commands/{ContratosGerar,ContratosParidade}.php`, `app/tests/Unit/Contratos/*` (5 ficheiros), `docs/08-*` a `docs/10-*`, `docs/CONTRATOS-DECISOES.md`, `docs/CONTRATOS-FASE2-RESULTADOS.md`. Sincronizado para o CT 117 por rsync (mesma receita do módulo Fichas).
- Testado: smoke standalone no Mac (PHP 8.2, sem Laravel) antes do sync; suite Pest completa no CT 117 (PHP 8.4); teste de paridade com dados reais de prod (só leitura, nunca escrita); `codex review --uncommitted` tentado antes do sync — sem quota (renovação 23/09 00:30), confirmado com um pedido de teste; revisão do Codex fica pendente.
- Pendente: paridade de texto contra um caso ARD real (nenhum existe ainda na fila); Fase 1 (infraestrutura: automount CIFS de escrita, qpdf, libreoffice) e Fase 3 (webapp) do módulo Contratos, ainda por começar; revisão do Codex (repetir depois de 23/09 00:30). Decisões assumidas por omissão em docs/CONTRATOS-DECISOES.md (nunca escreve no Sabichão, sem paginação dinâmica no MVP, Qwen fora do MVP, escrita na NAS só na Fase 1) — a confirmar pelo utilizador.

## 2026-09-23 (Contratos, Fase 3 — webapp) — Claude (Sonnet)
- Resultado: módulo Contratos na webapp (fila com seleção múltipla, "Redigir selecionados" despacha um job por caso, página de revisão com pré-visualização do PDF autenticada, perguntas/`[FALTA]` com "Regerar", "Aprovar e publicar"/"Publicar em lote"/"Tentar marcar outra vez"/"Descartar", histórico). Infraestrutura de dev (qpdf + libreoffice-writer-nogui no CT 117, pastas `/srv/assistente-dev/{modelos,contratos-feitos}`) instalada e testada a sério (conversão .docx→PDF + encriptação AES-256 reais, com uma correção de string de verificação e uma armadilha de cwd documentadas). Wrapper com 3 ações novas (`consulta.fila_contratos`, `consulta.dados_contrato`, `contrato.marcar_redigido`) testadas de verdade contra o Sabichão dev (2 bugs de SQL/guarda corrigidos a testar), incluindo uma escrita real revertida (processo 11: `Fazer`→`Nao`→`Fazer`, log gravado e apagado, confirmado por SELECT). 3 rondas de `codex review` (antes da quota acabar a meio do trabalho) com 8 achados corrigidos (2 P1 de segurança/correção real: guarda de NIF com espaços no wrapper, zona não sanitizada no caminho de publicação). 12 testes Pest novos; suite completa 280/280.
- Alterado: `wrapper-ct107/wrapper.php` (3 ações + correções), `app/app/{Models,Services/Contratos,Jobs,Http/Controllers}` novos, `database/migrations/2026_09_23_{4,5,6}*`, `resources/views/contratos/*` (escritas pelo Claude, não pelo Qwen — mini desligado a meio deste trabalho), `config/services.php`, `.env.example`, `routes/web.php`, `tests/Feature/Contratos/*`, `docs/{CONTRATOS-FASE3-RESULTADOS,DEPLOY,FASE3-QWEN}.md`. Sincronizado para o CT 117 por rsync (`--chmod` forçado depois de um incidente de permissões 700 vindo do Mac que parou o worker, corrigido a meio pelo coordenador). Wrapper reinstalado só em DEV (CT 100), 3x, para corrigir bugs reais encontrados a testar.
- Testado: suite Pest completa no CT 117 (280 testes, 755 assertions) a cada bloco de alteração; as 3 ações do wrapper a sério contra o CT 100 (incluindo escrita real revertida); pipeline soffice/qpdf a sério contra o modelo `Contrato_Termo_Certo_v2.docx` real. NÃO testado: browser real (Playwright) — faltou tempo; ponta a ponta completo até "publicado" com um dos 3 processos reais da fila de dev — os 3 têm dados incomuns (sem `processo_contrato`, sem `cliente_equipa`, e 2 deles sem cartão válido para a especialidade), o motor recusou corretamente gerar (proteção "texto de exemplo sobreviveu" / pergunta "horas=0" sem flag equivalente), sem publicar nada malformado; os 3 processos e a BD ficaram confirmados exatamente como antes (SELECT final).
- Pendente: verificação em browser (Playwright); fechar o ponta a ponta até "publicado" com um processo real (precisa de completar dados de teste em dev ou esperar o primeiro caso real do utilizador); revisão do Codex da 4ª ronda de achados (regeneração de auditoria pós-publicação, item 9 de `CONTRATOS-FASE3-RESULTADOS.md`) — quota esgotada a meio deste trabalho, repetir depois do reset; conta NAS de escrita dedicada (utilizador cria no DSM); instalação em prod (CT 107) — nada tocado.

## 2026-09-23 (Contratos, teste de ponta a ponta em browser) — Claude (Sonnet)
- Resultado: ciclo completo redigir → rever → publicar → marcar 'Nao' no Sabichão provado pela interface (HTTP real + Playwright), com dois processos fictícios (999960 termo certo, 999961 termo incerto) criados e depois apagados do Sabichão dev. 4 bugs reais encontrados e corrigidos: (1) diretórios de rascunho sem permissão de grupo para `www-data` (infra, sem código a mudar); (2) `Conversor` (soffice) sem `$HOME` gravável para o job da fila — perfil dedicado passado por `Process->env()`; (3) [P1] `ContratosController::ficheiro()` com tipo de retorno errado (`Response` em vez de `BinaryFileResponse`) — quebrava toda a pré-visualização de PDF/DOCX, nunca detetado antes por falta de teste HTTP desta rota; (4) estados técnicos em cru na UI (`gerado_com_falta` etc.) — `ContratoCaso::estadoLegivel()` novo. Paridade de texto confirmada byte-a-byte entre o `.docx` publicado pela webapp e o gerado em paralelo por `contratos:gerar` com os mesmos dados. PDF encriptado (AES-256, sem password de leitura, sem permissão de edição) confirmado por `qpdf --show-encryption`. Publicar duas vezes o mesmo caso dá erro claro sem tocar nos ficheiros. Suite Pest final: 284/284 (755→765 assertions, +4 testes novos de regressão em `FicheiroTest.php`).
- Alterado: `app/Services/Contratos/Conversor.php`, `app/Http/Controllers/ContratosController.php`, `app/Models/ContratoCaso.php`, `resources/views/contratos/{index,mostrar}.blade.php`, `tests/Feature/Contratos/FicheiroTest.php` (novo). Sincronizado para o CT 117 por rsync + restart de `php8.4-fpm`/`assistente-queue`. Permissões de `storage/app/private/contratos/` corrigidas no CT (infra, fora do git).
- Testado: fluxo completo por curl (login+CSRF reais) e por Playwright (login, fila, abrir caso, voltar, tema escuro — capturas em `docs/capturas/`, fora do git); suite Pest completa a cada correção; comparação de paridade `contratos:gerar` vs publicado; `qpdf --show-encryption`; tentativa de publicar 2x (erro claro, sem overwrite, mtime confirmado igual).
- Pendente: limitação de ambiente (não da app) — o `<iframe>` do PDF fica visualmente em branco no Chromium headless do Playwright, mas o pedido HTTP ao mesmo URL feito pelo próprio browser devolve os bytes certos (confirmado); fechar o "publicado" com um processo REAL da fila (os 3 atuais têm dados incompletos no Sabichão dev — sem `processo_contrato`/`cliente_equipa` ou sem cartão válido); `codex review` desta ronda (quota indicada como esgotada, não usado por instrução explícita desta tarefa); item 9 da lista de achados anterior (regeneração de auditoria pós-publicação) continua por fazer; instalação em prod (CT 107) — nada tocado.

## 2026-09-24 (perguntas pendentes visíveis + fila de processamento) — Claude (Sonnet)
- Resultado: duas melhorias pedidas pelo utilizador depois de testar no browser (selecionou os processos 11 e 15, carregou "Redigir", os jobs correram em ~250ms e ficaram `gerado_com_perguntas` sem nada visível — "pareceu que não saíram"). (1) Fila e página do caso mostram "À espera de respostas: Modalidade, Especialidade, Horas" com botão "Responder"; formulário com o controlo certo por pergunta (selects para modalidade/especialidade/documento/categoria, número+período para horas, data para data_fim, pesquisa de cliente reaproveitando `consulta.clientes`); nova pergunta respondível "horas" (antes só dizia "corrigir no Sabichão"); `Calculadora::calcular()` devolve `opcoes` estruturadas. (2) Página nova "Fila de processamento" (menu lateral, visível a todos): em espera/a correr (tabela `jobs`), concluídos recentes 24h, falhados com "Tentar outra vez" (`queue:retry` pelo UUID), aviso de trabalhador parado, estado do leitor do mini, atualização automática 10s, filtro Fichas/Contratos/Todos; ligação "Ver na fila de processamento" depois de redigir/registar. 2 bugs reais encontrados e corrigidos a testar no browser contra os processos reais (não pelos testes mockados, que nunca reproduziam isto): `Gerador::gerar()` só gravava o manifesto/flags novos quando o estado técnico mudava — respondido a uma pergunta que revelava uma pergunta NOVA em cascata (estado continuava `gerado_com_perguntas` → `gerado_com_perguntas`) ficava preso num ciclo sem fim, nunca avançando (achado grave, teste de regressão feito); um candidato a cliente sem a coluna `estado` (dados reais) rebentava com `Undefined array key`.
- Alterado: `app/Services/Contratos/{Calculadora,Gerador}.php`, `app/Models/{ContratoCaso,FichaCaso}.php`, `app/Http/Controllers/{ContratosController,FichasController,FilaProcessamentoController(novo)}.php`, `resources/views/{contratos/{index,mostrar},layouts/app,fila-processamento/index(nova)}.blade.php`, `routes/web.php`, testes novos (`GeradorTest`, `CalculadoraTest`, `PerguntasPendentesTest`, `FilaProcessamento/IndexTest`), `docs/CONTRATOS-FASE3-RESULTADOS.md`. Sincronizado para o CT 117 por rsync + restart de `php8.4-fpm`/`assistente-queue` a cada bloco.
- Testado: suite Pest completa no CT 117 a cada correção — final 298 testes, 834 assertions, sem regressão (284→298). Browser real (Playwright) contra os processos REAIS 11, 15, 16 (confirmados `contrato='Fazer'`/`estado='Ativo'` no Sabichão dev antes e depois, por leitura direta): processo 11 → "Gerado — com campos em falta" (.docx servido, 200, sem PDF por documento de identificação caducado); processo 15 → "Gerado — completo" (PDF servido, 200, `application/pdf`); processo 16 → ficou `gerado_com_perguntas` com um erro real do `DocxEngine` no modelo `.docx` real ("texto de exemplo sobreviveu"), não investigado (fora do âmbito desta tarefa). Nada publicado; os três confirmados por leitura direta ao Sabichão dev no fim, continuam `contrato='Fazer'`. Capturas em `docs/capturas/fase3b-*.png` (fora do git).
- Pendente: bug do processo 16 (texto de exemplo sobrevivente no `.docx` real, mesma família dos bugs de âncora do `DocxEngine` já documentados na skill) por investigar; `codex review` (quota esgotada, instrução explícita para não usar Codex nesta tarefa); primeiro "publicado" com um processo real genuíno continua por fazer (dados incompletos no Sabichão dev, por natureza).

## 24/09/2026 07:05 — Contratos: Word do mini como conversor principal (Claude)
- Resultado: o CT gera o PDF dos contratos com o Word real do mini; se o mini falhar, cai para LibreOffice + paginação verificada.
- Alterado: Gerador, LeitorClient::paginarPdfContrato, RedigirContrato (timeout 300), Protetor (aceita exit 3 do qpdf), config/services.php, phpunit.xml, tests/Pest.php (Storage::fake nos testes de Contratos), mostrar.blade.php (motor usado), deploy/assistente-queue.service (--timeout=300), .env do CT (CONTRATOS_CONVERSOR=word, DB_QUEUE_RETRY_AFTER=360), DEPLOY.md §15.
- Testado: Unit/Contratos + Feature/Contratos + Feature/Leitor no CT 117 (71 passam); processo 15 real pela fila: Word, 15 páginas, 65s, PDF protegido com fontes do Word. O recurso LibreOffice correu a sério uma vez (antes da correção do Protetor) e deu 15 páginas.
- Pendente: suite completa por partes; [FALTA] editáveis; realce das alterações na pré-visualização; bloquear publicação sem PDF; horas em Europe/Lisbon; repor processo 11.

## 24/09/2026 (campos [FALTA] editáveis + realce de revisão + bloqueio de publicação + Europe/Lisbon) — Claude (Sonnet)
- Resultado: 4 blocos pedidos, um commit por bloco. (1) `valores_manuais` (JSON, migração nova) por caso: cartão "Campos por preencher" na página do caso, um campo de texto por chave [FALTA] (rótulos PT em `ContratoCaso::ROTULOS_CAMPOS`), rota `POST contratos/{caso}/preencher` valida/grava/regenera, evento de auditoria só com as chaves (nunca os valores); `Gerador::gerar` sobrepõe esses valores em cima dos campos calculados e guarda a lista de chaves em `meta.manuais`; lista de campos ainda `[FALTA]` calculada por regex sobre `DocxEngine::extrairTexto` do `.docx` gerado, guardada em `manifesto.falta`. (2) Realce das alterações: `DocxEngine::preencherDocx` ganha `$revisao` opcional e colore os slots preenchidos por origem (verde=Sabichão, ciano=calculado, cinzento=manual; `[FALTA]` continua amarelo); `Gerador` gera sempre com esse realce, produz um "PDF de revisão" (nunca protegido, nunca publicado) e retira as cores do `.docx` antes de gravar/publicar (`DocxEngine::retirarRealceRevisao`); caminho Word ganha o campo `revisao=1` e o contrato de resposta `pdf_revisao_base64`; caminho LibreOffice converte duas vezes (revisão colorida, depois final sem cores); página do caso mostra o PDF de revisão com legenda de cores e link para o PDF final sem cores; rota `ficheiro` ganha o tipo `revisao`. (3) Publicação bloqueada quando incompleta: só `gerado_completo` com 0 `[FALTA]` e PDF em disco pode publicar (antes `gerado_com_falta` também podia) — `Publicador::publicar` (defesa em profundidade), `ContratosController::publicar`/`publicarLote`, índice "prontos para publicar" e botão desativado com o motivo na página do caso. (4) `config/app.php` timezone passa a `env('APP_TIMEZONE', 'Europe/Lisbon')` (antes fixo `UTC`), `.env.example` atualizado.
- Alterado: `database/migrations/2026_09_24_000001_*`, `app/Models/ContratoCaso.php`, `app/Services/Contratos/{Gerador,DocxEngine,Paragrafo,Publicador}.php`, `app/Services/Leitor/LeitorClient.php`, `app/Http/Controllers/ContratosController.php`, `routes/web.php`, `resources/views/contratos/mostrar.blade.php`, `public/css/app.css` (`.swatch*`), `config/app.php`, `.env.example`, `docs/DEPLOY.md` (§16). Sincronizado para o CT 117 por rsync + `migrate --force` + restart `php8.4-fpm`/`assistente-queue`.
- Testado: suite Pest completa no CT 117, por partes tal como pedido — `tests/Unit` + `tests/Feature/Contratos` + `tests/Feature/Leitor`: 199 passam (544 assertions, 0 falhas), incluindo testes novos/atualizados (`DocxEngineTest` realce+`retirarRealceRevisao`, `GeradorTest`/`GeradorConversorWordTest` com as duas conversões do caminho LibreOffice e `pdf_revisao_base64` do caminho Word, `PreencherTest` novo, `PublicadorTest`/`PublicarBloqueioTest` com os 3 bloqueios); `tests/Feature/Auth` + `tests/Feature/Fiabilidade` + `tests/Feature/FilaProcessamento` + `tests/Feature/*.php`: 20 passam; `tests/Feature/Fichas` (exceto `FichasLerCommandTest`/`RetryExtracaoTest`, integração com o Qwen real): 118 passam — sem regressão em nenhuma das três rondas (337 testes no total desta sessão). `php artisan route:list | grep contratos` confirma as rotas novas (`preencher`, `ficheiro/{tipo}` com `revisao`). Não testado por curl autenticado (classificador do modo auto bloqueou leitura do `.env`/login por curl como "credential materialization/exploration" — route:list já satisfaz a alternativa pedida); não regerado nenhum caso real (Word do mini em alteração em paralelo).
- Pendente: verificação em browser (Playwright) deste bloco específico; repor o processo 11 no Sabichão dev (ainda pendente de entradas anteriores); primeiro "publicado" com um processo real genuíno.

## 24/09/2026 08:55 — Contratos: revisão do trabalho do agente + validação real (Claude)
- Resultado: realce validado ponta a ponta com o Word; corrigido o caso com [FALTA] (o .docx ficava com as cores de revisão; passa a ter PDF de revisão pelo LibreOffice); vista sem link partido para o PDF final.
- Alterado: Gerador (ramo nFalta>0), contratos/mostrar.blade.php, GeradorTest (+1 teste), DEPLOY.md §16. Mini: app.py com modo revisão (commit 4873609), instalado.
- Testado: Unit + Contratos + Leitor 200/200; integração com o Qwen 3/3; processo 15 real pelo Word (15 páginas, cores só na revisão); páginas 200 autenticadas. Processo 11 reposto em dev (Sabichão 'Fazer', log 345 apagado, .docx apagado, caso sem_gerar).
- Pendente: ver no browser pelo utilizador; revisões Codex quando houver quota; Gemma 4 vs Qwen; bug do processo 16 no DocxEngine.

## 24/09/2026 10:45 — Contratos: bug do processo 16 (Claude)
- Resultado: era um falso positivo da verificação dos textos de exemplo (data real 01/01/2026 contém "01/01/202"); corrigido sem mudar o texto gerado.
- Alterado: DocxEngine::preencherDocx (verificação desconta os valores inseridos), DocxEngineTest (+2), docs/CONTRATOS-FASE3-RESULTADOS.md.
- Testado: Unit + Contratos + Leitor 202/202 no CT 117; processo 16 real em dev: com falta → PDF de revisão; validade escrita à mão → Word, completo, 15 páginas, cinzento no PDF de revisão. Paridade 45/45 não repetida (dados apagados; o texto gerado não muda).
- Pendente: a skill fazer-contrato tem o mesmo falso positivo (não alterada); o aviso "a validade do cartao fica por preencher" continua a aparecer depois de o valor ser escrito à mão; caso 16 em dev ficou gerado_completo com flags e valores de teste (não publicado).

## 24/09/2026 11:30 — Revisão do Codex de tudo desde 3e7a40d (Claude)
- Resultado: 3 rondas de `codex review` (82 ficheiros, 30 commits). Ronda 1: 2 achados reais (P1 trinco do Word libertado no timeout com a thread ainda a trabalhar; P2 publicação deixava .docx órfão se o PDF falhasse). Ronda 2 (sobre a correção): P2 a limpeza podia apagar o .docx de outra publicação concorrente. Ronda 3: sem achados.
- Alterado: leitor-mini/app.py (trinco só sai quando a thread do Word acaba, também se o pedido for cancelado; instalado no mini), Publicador (flock de publicação no CT, rollback do .docx só se o inode for desta chamada, copy/rename com @ porque no Laravel o aviso virava exceção e saltava a limpeza), PublicadorTest (+1).
- Testado: Unit + Contratos + Leitor 203/203 no CT 117; simulação asyncio do trinco (2.º pedido só entra quando a thread do 1.º acaba); /v1/saude do mini depois de reinstalar.
- Pendente: nada da revisão. Revisões Codex pendentes antigas ficam cobertas por esta (base 3e7a40d).

## 24/09/2026 — Ambiente de escrita dev/prod (preparação para o CT de produção) (Claude Opus 5.5)
- Resultado: `WRAPPER_APPLY_ATIVO` passa a aceitar `off`/`dev`/`prod` (antes só ligava/desligava um "modo dev" fixo). `Aplicador::ambiente()` (`'dev'`/`'prod'`/`null`) e `Aplicador::ativo()` substituem `ativoEmDev()` em toda a app (renomeado, sem alias). `Aplicador::confirmarAlvoRemoto(Cruzamentos)` centraliza a confirmação contra o wrapper remoto (antes comparava sempre contra `'dev'` fixo no `FichasController`) — usada em todas as ações de apply/reconciliar/pdf/email E, agora, também antes de `contrato.marcar_redigido` (`Publicador::marcar`), que não tinha nenhuma confirmação de alvo até agora. Os registos de execução (`fichas_execucoes.alvo`) passam a gravar o ambiente real em vez de `'dev'` fixo (migração nova alarga o enum para incluir `'off'`, usado pelo dry-run quando não há nenhum ambiente de escrita configurado — dry-run continua a não precisar de `ativo()`, só a chave de consulta). Vistas: um único botão "Aplicar em DEV"/"Aplicar em PROD" (o botão morto "Aplicar em PROD" sempre desativado foi removido); em produção, a caixa "Confirmo que revi a ficha contra os documentos" (obrigatória no servidor só quando `ambiente=prod`) mais um `confirm()` em JS com o MAI e o nome, antes do apply de fichas; pill discreta "DEV"/"PROD"/"OFF" na barra lateral (`layouts/app.blade.php`, classes `.pill`/`.p-run`/`.p-blk`/`.p-rev` já existentes). Cada instância continua a escrever só no ambiente do seu próprio `.env` — decisão do utilizador de 24/09/2026, preparação para o CT de produção que ainda não existe.
- Alterado: `app/Services/Fichas/Aplicador.php` (ambiente/ativo/confirmarAlvoRemoto), `app/Services/Contratos/Publicador.php` (injeta Cruzamentos, chama confirmarAlvoRemoto antes de marcarRedigido), `app/Http/Controllers/FichasController.php` (todas as ações da Fase 4 + dryRun), `resources/views/fichas/{mostrar,index}.blade.php`, `resources/views/layouts/app.blade.php` (pill), `config/services.php` (comentário dos 3 valores), `.env.example`, `database/migrations/2026_09_24_000002_add_off_to_fichas_execucoes_alvo_enum.php` (novo), `docs/DEPLOY.md` (§17 "Ambiente de escrita (dev/prod)"), testes: `tests/Feature/Fichas/AplicarDevTest.php` (renomeado ativoEmDev→ativo/confirmarAlvoRemoto em todos os mocks existentes + 3 testes novos de prod/dry-run), `tests/Feature/Fichas/AplicadorAmbienteTest.php` (novo, 11 testes unitários de ambiente()/ativo()/confirmarAlvoRemoto), `tests/Feature/Fichas/MostrarTest.php` (teste do botão único atualizado), `tests/Feature/Contratos/PublicadorTest.php` (Cruzamentos mockado + 1 teste novo de marcar_redigido recusado por alvo trocado). Sincronizado para o CT 117 por rsync + `migrate --force` + restart `php8.4-fpm`/`assistente-queue`.
- Testado: os 3 grupos pedidos, todos verdes no CT 117 — (a) `tests/Unit` + `tests/Feature/Contratos` + `tests/Feature/Leitor`: 204/204; (b) `tests/Feature/Auth` + `tests/Feature/Fiabilidade` + `tests/Feature/FilaProcessamento` + `tests/Feature/*.php`: 20/20; (c) `tests/Feature/Fichas/*.php` exceto `FichasLerCommandTest`/`RetryExtracaoTest`: 132/132. Confirmado por HTTP real (login curl com o utilizador `rh`) que a página de um caso continua a mostrar "Aplicar em DEV" e a pill "DEV" na barra lateral — o `.env` do CT 117 não foi tocado (`WRAPPER_APPLY_ATIVO=dev`). Um bug real dos meus próprios testes apanhado a correr a sério (não do código da app): dois `shouldReceive('ambiente')` conflituosos no mesmo mock faziam o Mockery devolver sempre o primeiro ('dev'), mascarando o teste de produção — corrigido.
- Pendente: nada por testar desta tarefa. O CT de produção em si (host, `.env` real, chave de apply própria, wrapper no CT 107 com `ALVO=prod`) continua por criar — `docs/DEPLOY.md` §17 já documenta o que muda quando existir, marcado "a preencher na instalação". `wrapper-ct107/` e `leitor-mini/` não foram tocados (fora do âmbito).

## 24/09/2026 12:15 — Produção: CT 121 + CT 107 (Claude)
- Resultado: Assistente RH em produção em http://10.0.30.248 (CT 121), ligado ao Sabichão prod (CT 107) e às pastas de prod; escrita ativa (WRAPPER_APPLY_ATIVO=prod). Utilizador único ru1p3dro.
- Alterado: Proxmox prod (fstab com Processos ro + Assistente rw, CT 121 novo, backups com o 121), CT 107 (wrapper release 20260924110956 ALVO=prod, authorized_keys só com as chaves do CT 121), CT 117 (chave de prod removida), Aptos oficial nos dois CTs, docs/DEPLOY-PROD.md.
- Testado: login 200; leitor + Word a partir do CT 121; NAS (705 processos, 4 modelos, escrita em Contratos Feitos com ficheiro temporário apagado); restauro do backup (BD vazia); consulta.alvo=prod, zonas, fila (95); contrato do processo 465 gerado pelo Word em prod (não publicado).
- Pendente: primeira ficha real em prod acompanhada pelo utilizador (dry-run → apply com confirmação dupla); primeiro contrato publicado em prod comparado com o que a skill faria; repetir o teste de restauro com dados; HTTPS na LAN (proposta).

## 24/09/2026 — Pedidos do utilizador durante os primeiros testes em prod (por fazer, NÃO instalar enquanto ele testa)
1. Novo caso (Fichas): a data de início do contrato é sempre a da admissão. Hoje há uma caixa "Data de início" (placeholder "=admissão") que fica vazia se não for preenchida; passar a usar a admissão sem pedir (tirar a caixa ou preenchê-la automaticamente), e confirmar o que o manifesto grava em processo_contrato.data_inicio quando vem vazia.
2. Campos de data: escrever só os números. O servidor já aceita ddmmaaaa (App\Support\Datas); falta a caixa pôr as barras sozinha ao escrever (01102026 -> 01/10/2026) em todas as caixas de data (Fichas e Contratos), mantendo dd/mm/aaaa. Alternativa type=date descartada por mostrar o formato do browser.

## 24/09/2026 (tarde) — Portal dos dados + correções do primeiro caso real de prod — Claude (Sonnet)
- Resultado: as 5 tarefas pedidas + 2 achados reais em prod endereçadas, todas SÓ EM DEV (CT 117), nada instalado em prod/CT 107. (1) `Cruzamentos::portalLookup()` existia mas nunca era chamado — agora `LerCaso` chama-o no passo 3 (via novo serviço `PortalDadosAplicador`, partilhado com o botão novo "Consultar portal outra vez" na página do caso); preenche email/contacto/estado civil/dependentes/habilitações/emergência com origem 'portal' e confiança alta, nunca sobrepõe edição do utilizador, nunca preenche a morada (só conferência), e nunca deixa not_found/indisponível bloquear a aprovação (achado durante a tarefa: mostrar() cria automaticamente esses 8 campos como 'rever' se não existirem — corrigido com `garantirCamposPortal()`). (2) Classificador: verso do CC (MRZ/NISS/filiação) testado antes da regra da frente — corrige o bug real do primeiro caso (verso classificado como cc_frente, NISS/filho_de vazios). (3) Concelho/distrito do RC deixam de herdar a confiança uniforme do bloco: sem concelho → ambos vazios e alta (limitação do documento); com concelho sem distrito no mapa → 'rever' com motivo "distrito por confirmar" (antes ficava 'rever' sem motivo nenhum). (4) contrato_data_inicio passa a ser sempre a admissão — caixa tirada do formulário "Novo caso". (5) Máscara de data (public/js/datas-mascara.js) nas caixas `.dt` de Fichas/Contratos. Mais 2 achados durante os testes: um campo 'rever' vazio sem leitura alternativa (caso do distrito) não tinha forma de sair de 'rever' — novo botão "Confirmar em branco" (mesmo mecanismo dos "[usar]", sem mudar o servidor). E um caso real com o NIF errado no portal (número do verso do CC, falha o checksum): novo fallback por nome+nascimento — ação de leitura nova `consulta.portal_por_nome` (só no repo do wrapper, NÃO instalada em lado nenhum), 1 candidato preenche com confiança 'rever' e motivo explícito, >1 fica 'ambíguo' sem preencher nada.
- Alterado: `app/app/Jobs/LerCaso.php`, `app/app/Services/Fichas/{PortalDadosAplicador.php (novo),Classificador.php,Cruzamentos.php}`, `app/app/Http/Controllers/FichasController.php`, `app/app/Http/Requests/Fichas/NovoCasoRequest.php`, `app/app/Models/FichaCaso.php`, `app/routes/web.php`, `app/database/migrations/2026_09_24_000003_*`, `app/resources/views/fichas/{mostrar,novo}.blade.php`, `app/resources/views/layouts/app.blade.php`, `app/public/js/datas-mascara.js` (novo), `wrapper-ct107/wrapper.php` (ação nova, não instalada), `docs/03-CONTRATO-WRAPPER-CT107.md`, `docs/DEPLOY.md` (§18). Sincronizado para o CT 117 por rsync + `migrate --force` + `view:clear`.
- Testado: (a) `tests/Unit`+`tests/Feature/Contratos`+`tests/Feature/Leitor`: 205/205; (b) `tests/Feature/Auth`+`tests/Feature/Fiabilidade`+`tests/Feature/FilaProcessamento`+`tests/Feature/*.php`: 20/20; (c) `tests/Feature/Fichas/*.php` exceto `FichasLerCommandTest`/`RetryExtracaoTest`: 158/158 (inclui os 4 ficheiros de teste novos: `PortalDadosTest` 17, `ConcelhoDistritoTest` 3, `DatasMascaraTest` 2, `ConfirmarEmBrancoTest` 3). `wrapper-ct107/` só recebeu `php -l` local (sem instalação — fora do âmbito desta sessão) e a query SQL foi verificada contra o schema real de `portal_submissions` em dev (`DESCRIBE`, só leitura).
- Pendente: nada por testar do que foi pedido. Fora do âmbito desta sessão (nunca tocado): `fichas:validar` contra os 40 processos reais de `docs/validacao-fase2.md` exigiria `WRAPPER_HOST=10.0.30.253` (CT 107, produção) — não corrido, âmbito era estritamente dev. Instalar `consulta.portal_por_nome` no wrapper (dev e/ou prod) fica para quando o utilizador pedir (docs/DEPLOY.md §18). `FichasLerCommandTest`/`RetryExtracaoTest` não foram corridos (fora do pedido) mas `php -l` não mostrou erro em nenhum ficheiro tocado que referenciassem.

## 24/09/2026 14:25 — Prod atualizado + comparação Qwen vs Gemma 4 (Claude)
- Resultado: CT 121 com as correções do primeiro caso real (portal ligado com procura por nome, verso do CC, concelho/distrito, data de início = admissão, máscara de datas, "Confirmar em branco") e revisão do Codex (3 rondas, sem achados na última). CT 107 com wrapper release 20260924141729 (consulta.portal_por_nome). Leitor do mini com escolha de modelo, trinco por modelo, /v1/classificar-documento e campos vazios retirados antes do schema. Gemma 4 26B A4B QAT carregado ao lado do Qwen.
- Testado em prod (só leitura): consulta.alvo=prod; caso do Cristóvão: por NIF not_found, por nome 1 candidato (submissão 153, NIF inválido, 7 campos). Comparação com 1 processo e verdade do Sabichão: a funcionar.
- A correr: fichas:comparar-modelos --verdade=sabichao sobre os 40 processos, no CT 121 (nohup, log em storage/logs/comparacao-modelos.log, resumo em storage/app/COMPARACAO-MODELOS.md). Não escreve na BD da app.
- Pendente: submissão 153 por marcar como aplicada (escrita bloqueada pelas permissões; utilizador faz no Sabichão ou desbloqueia); decidir modelo com base na comparação; classificação por modelo na leitura (proposta).

## 2026-09-24 (4ª entrega) — Porte de ocr_docs.php: cartão PSP, IBAN, morada, habilitações, passaporte (Claude)
- Resultado: porte literal (com paridade testada no CT 100 contra o PHP original, sha256 e15bfb5... inalterado) de ordenarSiglas/cartaoInfosPorLinha/parseCartaoEspecialidade/ibanValidado/parseIban/parseMorada/habilitacoesDeNome/parseHabilitacoes/paisIso3/parsePassaporte + extensão de extrairDatas (ramo de meses por nome). Integrados como 1ª leitura: cartão PSP (só a validade, por bloco — caso 773 protegido por 2 testes novos), IBAN (só cobertura adicional quando a lógica própria não acha nada, para não reabrir os bugs reais de mascaramento/coluna), morada (guardas institucionais + rótulos), habilitações (capacidade nova, LerCaso nunca teve extração própria). Passaporte portado e testado mas NÃO integrado (Classificador ainda não distingue 'passaporte' como tipo).
- Alterado: app/app/Support/OcrDocs.php, app/app/Services/Fichas/Extratores/CartaoPsp.php, app/app/Services/Fichas/Extratores/Iban.php, app/app/Jobs/LerCaso.php, app/tests/Unit/Fichas/{OcrDocsParidadeTest.php,CartaoPspTest.php,IbanTest.php,fixtures/gerar_paridade.php,fixtures/ocr_docs_paridade.json}, app/tests/Feature/Fichas/HabilitacoesMoradaPorteTest.php, app/docs/PORTE-OCR-DOCS.md, docs/DEPLOY.md.
- Testado: 26 testes de paridade + 9 testes novos de integração (773 x2, Iban x2, Habilitações/Morada x7). 3 grupos completos do projeto no CT 117: (a) Unit+Contratos+Leitor = 260, (b) Auth+Fiabilidade+FilaProcessamento+Feature soltos = 20, (c) Fichas/* exceto FichasLerCommandTest/RetryExtracaoTest = 174. Total 454, 0 falhas.
- Pendente: integrar parsePassaporte quando o Classificador distinguir 'passaporte'; ocr_merge_campos vs. política de confiança atual (comparação campo a campo por fazer); ocr_campos_uteis/ocr_classificar_por_nome/ocr_tipo_por_tokens já portadas, ainda sem ponto de integração decidido. Só dev — não leva a prod.

## 2026-09-25 — Correções do módulo Fichas depois do uso em prod (Claude)
- Resultado: 6 pontos pedidos pelo utilizador depois de usar a app em prod, todos SÓ EM DEV (CT 117), nada instalado em CT 121/107/Proxmox prod. (1) Classificador: verso do CC sem MRZ legível (imagem cortada) deixa de cair em cc_frente pelo cabeçalho — marcadores do verso (filiação/NISS/nº utente/identificação fiscal) sem nenhum marcador da frente (apelido/data de nascimento/nome(s)) forçam cc_verso antes de aceitar o cc_frente do porte. (2) Morada::normalizar: 3 bugs reais corrigidos — abreviaturas (R./Av./Tv./Trav./Estr./Lg./Pç.) deixavam o ponto solto ("Rua. das Flores") porque o \b final não fechava depois do ponto (trocado por lookahead de espaço/vírgula/fim); designadores de andar (Dto/Esq/Direito/Frente/Fte/Tras) passam a reconhecer-se insensíveis a maiúsculas e a normalizar a grafia ("2 DTO" -> "2º Dto"); a vírgula solta antes de um "nº" só inserido depois (documento sem rótulo, morada em 2 linhas) deixa de duplicar ("Rua X, nº12, CP" -> "Rua X nº12, CP"). Todos os testes antigos do MoradaTest continuam verdes, 8 testes novos (3+5) cobrem os casos reais. (3) As leituras alternativas Vision/"Qwen" na página do caso (botões "[usar]" de um campo morada em revisão) passam a normalizar a morada também, mostrando sempre "original -> normalizada" — o valor gravado ao clicar "usar" já é o normalizado. (4) UI deixa de dizer "Qwen" fixo: rótulo do modelo real (fichas_extracoes.modelo_qwen, de /v1/saude; "Modelo local" se vazio) nos botões "[usar]" e nos motivos "Só lido pelo <modelo>: confirmar"/"Vision e <modelo> divergem"/"<modelo> indisponível" (SegundaLeituraQwen::nomeModelo, memoizado — nunca mais de 1 pedido a /v1/saude por execução do job). Nomes de classes/colunas (SegundaLeituraQwen, valor_qwen, modelo_qwen, leitor="qwen:<modelo>") não mudaram. (5) Página do caso mostra e permite editar a Zona 1 (coluna `zona`) ao lado da Zona 2 — antes só a zona 2 aparecia; select com a lista de zonas ativas (igual ao Novo caso) quando disponível, texto livre como reserva; zona2 continua opcional, zona1 em branco no guardar mantém o valor já gravado (é obrigatória desde o registo). (6) Visualizador de documentos: painel passa de `position:absolute` a `position:fixed` (deixava de acompanhar o scroll da página — achado real, o utilizador via o painel desaparecer ao descer), cabeçalho com o botão Fechar em `position:sticky`, fecha com Esc (quando não há ecrã inteiro aberto, que já trata o seu próprio Esc) e com clique fora do painel; PDFs rasterizados a 300 DPI/qualidade 90 em vez de 200/85 (texto pequeno dos cartões/CC ilegível no zoom — achado real); zoom e "Ajustar à largura" já existiam no viewer.js e ficam sem alteração de comportamento.
- Alterado: app/app/Services/Fichas/Classificador.php, app/app/Services/Fichas/Extratores/Morada.php, app/app/Services/Fichas/SegundaLeituraQwen.php (nomeModelo + saude() memoizado), app/app/Jobs/LerCaso.php (marcarQwenIndisponivel ganha o parâmetro SegundaLeituraQwen), app/app/Http/Controllers/FichasController.php (modeloLabel na view, zona no guardar), app/app/Http/Requests/Fichas/EditarCamposRequest.php (regra zona), app/app/Http/Controllers/DocumentoController.php (pdftoppm -r 300 qualidade 90), app/resources/views/fichas/mostrar.blade.php (zona1, leituras alternativas normalizadas, rótulo do modelo), app/public/css/app.css (.panel fixed + sticky), app/public/js/app.js (Esc + clique fora do painel); testes: app/tests/Unit/Fichas/ClassificadorTest.php (+3), app/tests/Unit/Fichas/MoradaTest.php (+5), app/tests/Feature/Fichas/SegundaLeituraQwenTest.php (motivos/assinatura atualizados, +2 fakes de /v1/saude).
- Testado: (a) tests/Unit + tests/Feature/Contratos + tests/Feature/Leitor: 314/314; (b) tests/Feature/Fichas/*.php exceto FichasLerCommandTest/RetryExtracaoTest: 176/176 (inclui SegundaLeituraQwenTest com o motivo do modelo real do mini, "google/gemma-4-26b-a4b-qat", confirmado ao correr contra o leitor real do CT 117 num teste sem fake de /v1/saude). Sincronizado por rsync + view:clear + restart php8.4-fpm/assistente-queue no CT 117; sem migração.
- Pendente: ponto 6 (visualizador/painel fixo, zoom, resolução) só verificado por leitura do código e pela suite automatizada — sem teste manual num browser real (o pedido limitava os testes aos grupos pytest, sem Playwright/screenshot); confirmar visualmente no CT 117 antes de considerar fechado. Fora do âmbito: qualquer coisa em prod (CT 121/107).

## 2026-09-25 (2ª entrega) — Botão "Fazer contrato" + pesquisa na lista de contratos (Claude Opus 5.5)
- Resultado: 2 pedidos do utilizador, só em dev (CT 117), nada tocado em prod/CT 107. (1) Página da ficha (`mostrar.blade.php`): quando `estado='inserido_verificado'`, novo botão "Fazer contrato" — `FichasController::fazerContrato` obtém o `id_processo` (primeiro `sabichao_id_processo` gravado pela conferência pós-apply, com `Cruzamentos::ficha()` como reserva), confirma que o processo está mesmo na fila de contratos (`consulta.fila_contratos`), cria ou reaproveita o `ContratoCaso` desse processo (`ContratoCaso::paraProcesso()`, extraído de `ContratosController::index` para não duplicar a criação) e despacha `RedigirContrato` se o caso não estiver já publicado (`ContratoCaso::podeRedigir()`, mesma regra de `redigirSelecionados`, agora central), redirecionando para a página do contrato. Sem processo na fila, mensagem "Este processo não está na fila de contratos." (2) Lista de contratos (`contratos/index.blade.php`): caixa de pesquisa no topo (`public/js/contratos-pesquisa.js`, novo, só JS no cliente) filtra as linhas de todas as tabelas da página ao escrever (nome, MAI, nº de processo, zona), ignorando acentos/maiúsculas (`normalize('NFD')`); o "selecionar todos" (`chk-todos-fila`) passa a marcar só as checkboxes das linhas visíveis, sem alterar o resto da seleção múltipla.
- Alterado: `app/app/Http/Controllers/FichasController.php` (+`fazerContrato`), `app/app/Http/Controllers/ContratosController.php` (reusa `ContratoCaso::paraProcesso`), `app/app/Models/ContratoCaso.php` (+`paraProcesso`, +`podeRedigir`), `app/routes/web.php` (rota `fichas.fazer-contrato`), `app/resources/views/fichas/mostrar.blade.php` (botão), `app/resources/views/contratos/index.blade.php` (caixa de pesquisa + script), `app/public/js/contratos-pesquisa.js` (novo); testes novos `app/tests/Feature/Fichas/FazerContratoTest.php` (6) e `app/tests/Feature/Contratos/PesquisaListaTest.php` (3, smoke test sem runner de JS — mesma limitação de `DatasMascaraTest`).
- Testado: `tests/Unit` + `tests/Feature/Contratos` + `tests/Feature/Leitor` + `tests/Feature/Fichas/*.php` exceto `FichasLerCommandTest`/`RetryExtracaoTest`: 499/499 no CT 117. Sincronizado por rsync + `view:clear` + restart `php8.4-fpm`/`assistente-queue`; sem migração.
- Pendente: teste manual num browser real da caixa de pesquisa e do botão "Fazer contrato" (fora do âmbito pedido, que limitava os testes aos grupos Pest). Nada tocado em prod (CT 121/107).
