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

Revisto a 22/09/2026 depois da segunda opinião do Codex (aprovação condicionada da arquitetura base; correções integradas abaixo).

## Contexto

Hoje as fichas de funcionários novos são feitas pela skill `/inserir-funcionario` com o Claude: lê os documentos digitalizados da NAS, valida, apresenta uma tabela para OK e insere no Sabichão. Isso envia dados pessoais (CC, registo criminal, cartões PSP) para fora da empresa. O utilizador tem um mac mini M5 Pro 64 GB (10.0.10.231) com LM Studio + Qwen 3.8 27B (modelo de visão) e quer as fichas feitas **dentro da rede**, com aprovação numa **webapp** usada pelos RH (1 a 2 utilizadores). A skill do Claude mantém-se como via manual.

Teste de 22/09/2026 (só leitura): o OCR nativo do macOS (Apple Vision) leu 65/65 validades de cartões PSP e 38/38 de CC iguais ao Sabichão quando prod estava certo (os 2 desacordos eram erros em prod), em 35 s para 40 processos. Regras validadas em `~/.claude/skills/inserir-funcionario/ocr-mini/RESULTADOS-2026-09-22.md`.

A app é pensada como plataforma para mais módulos no futuro (Contratos, Arquivador), mas este plano cobre só Fichas. Modelos: Sonnet para testes e trabalho mecânico das fases; Fable só para decisões e revisão (pedido do utilizador).

Decisões do utilizador:
- Webapp num **CT** (Proxmox); o mini é só "leitor" (Vision + Qwen).
- Utilizadores: RH, 1 a 2 pessoas, login local.
- Imagens **não se guardam**; servem-se da pasta do processo na NAS, atrás de um botão pequeno, fechadas por omissão.
- Sem Telegram. Aprovação só na webapp.
- **Especialidades = todos os cartões PSP na raiz da pasta do processo** (`/Arquivo` e `old` ignorados); aparecem já marcados e o utilizador desmarca um a mais antes do OK.
- **Fila assíncrona:** registar um caso é imediato; o motor lê por trás, um de cada vez; o utilizador continua a registar enquanto os anteriores são lidos. Aprovação e inserção independentes por caso.

## Princípios de engenharia

1. **Nenhum dado pessoal sai da rede.** Documentos circulam só NAS → CT → mini. O Claude nunca recebe imagens nem texto de documentos deste fluxo, nem para "comparar" casos.
2. **O código decide, os modelos sugerem.** Confiança definida **por campo** (política abaixo). Vision é a fonte principal de leitura; o Qwen tem dois papéis: (a) leitor independente sobre a imagem original nos documentos difíceis (nunca sobre o texto do Vision, senão não é segunda fonte) e (b) **comparador semântico**: dizer se dois valores escritos de forma diferente são a mesma coisa (morada do CC vs comprovativo vs portal; nome com abreviaturas; naturalidade), e conferir depois da inserção que o que ficou no Sabichão corresponde ao que os documentos dizem. O Qwen nunca decide sozinho: uma equivalência afirmada por ele com a comparação literal a falhar aparece na tabela como "equivalente segundo o modelo" e entra na estatística de fiabilidade.
3. **Aprovação imutável:** ligada ao hash de cada documento e ao hash do manifesto. Edição de campo, nova leitura ou mudança de um ficheiro na NAS anula aprovação e dry-run.
4. **Reutilizar:** `inserir.php` (PHP 7.3, corre no CT 107), `portal_lookup.php`, `fichas_email.php`, `funcoes/ocr_docs.php` (cópia versionada com hash e testes de paridade). O SKILL.md do inserir-funcionario é a especificação funcional.
5. **Password de prod nunca sai do CT 107** (resolvida lá, dentro do wrapper).
6. **"Passar por dev"** significa validar a versão do sistema em dev, não copiar cada funcionário real para a BD de dev antes de prod.

## Arquitetura

```
NAS //10.0.30.79/Processos ──CIFS read-only──▶ CT "assistente" (Debian 13, nginx+php-fpm 8.3, Laravel 12, MySQL, fila em BD)
                                                 ├─ módulo Fichas: fila, caso, tabela de aprovação, auditoria
                                                 ├─ motor (job LerCaso): classificar → extrair → validar → cruzar → manifesto
                                                 │     ├─ POST https://10.0.10.231:8090/v1/ocr                (Vision)
                                                 │     ├─ POST https://10.0.10.231:8090/v1/analisar-documento  (Qwen, schema fechado por tipo)
                                                 │     ├─ OcrDocs.php (cópia versionada de funcoes/ocr_docs.php)
                                                 │     └─ SSH restrito ao CT 107: consulta (portal_lookup, SELECTs)
                                                 └─ SSH restrito ao CT 107: apply / ficha_pdf / email (wrapper fixo → inserir.php, render da ficha, fichas_email.php instalados lá)

mac mini 10.0.10.231 — serviço leitor (Python/FastAPI, launchd): recebe bytes + id opaco, nunca caminhos;
   firewall só ao IP do CT; HTTPS (certificado próprio) + token em Authorization; 1 análise de cada vez;
   limites de tamanho/páginas/MIME; temporários 0700 apagados em finally; sem montagem da NAS; LM Studio só localhost;
   logs do LM Studio/FastAPI sem prompts nem imagens (verificado, não assumido). /v1/saude sem sudo (sem powermetrics no MVP).
```

Porque Laravel: autenticação, fila, validação, migrações prontas; mesma família do Rondista/Supervisor/BreedLedger. Estilo herdado do centralista: tokens de cor (`--brand: #fdc500`), Inter self-hosted, PT-PT, sem emojis, CSS próprio curto. **Sistema visual decidido a 22/09/2026** (mockup aprovado pelo utilizador): uma direção só, com tema claro e escuro por tokens CSS e botão "Tema" na barra lateral, escolha guardada por utilizador (`users.tema`); barra lateral clara no tema claro, marca amarela como âncora; Manrope + IBM Plex Mono nos números; estados em badges pastel. **Datas sempre dd/mm/aaaa na interface e na escrita** (aceitar também ddmmaaaa e dd-mm-aaaa), convertidas para aaaa-mm-dd no manifesto e na BD. O desenho visual é do Claude (Fable); o Qwen só produz código mecânico sobre o sistema já fechado.

## Política de confiança por campo (substitui a regra global)

| Campo | Confiança alta exige | Fonte 2 (quando existe) | Qwen |
|---|---|---|---|
| NIF, NISS, IBAN, nº CC | formato + checksum + rótulo/posição certos no documento + titular coerente | verso do CC / MRZ | não |
| Validade CC | data a seguir a "Data de validade"/"Expiry" | MRZ do verso | não |
| Cartão PSP | número = MAI 6 dígitos + sufixo coerente com categoria impressa + validade no mesmo bloco + titular = nome do CC | — | só se número ilegível |
| RC (val_rc, finalidade) | data associada a "CÓDIGO VIGENTE ATÉ" + finalidade "exercício da atividade de segurança privada" + titular | — | sim (documento difícil), independente |
| Nome, nascimento, naturalidade | CC frente/digital com rótulo | RC | sim, se divergência |
| Morada | fonte identificada (prioridade CC/certidão AT > fatura/carta recente > portal) + **normalização ao padrão da casa** em código (`<Artéria> <Nome> nº<n>[ <compl>], <CP> <Localidade>`, abreviaturas expandidas R→Rua, Av→Avenida, Tv/Trav→Travessa, Pç→Praça, Lg→Largo, Estr→Estrada; regra do SKILL.md linhas 40 e 86); mostrada sempre `original → normalizada` | CC digital vs comprovativo vs portal | sim: **comparador semântico** (é a mesma morada escrita de outra forma? duas moradas oficiais diferentes → revisão) |
| Zona, local, horas, alínea, modalidade | lista do utilizador | — | não |
| Contacto, email, emergência | portal dos dados | — | não |
| **Conferência pós-inserção** (todos os campos) | SELECT ao Sabichão depois do apply comparado com o manifesto e com os documentos | — | sim: confirma equivalência semântica onde a comparação literal falha (morada, nome com/sem acentos, naturalidade) e assinala o que não bate |

Tudo o que não cumpre a política do campo vai para revisão humana com a evidência (imagem via `[img]`). Critério global mantido: **zero valores errados com confiança alta** no conjunto de validação.

**Como se resolve um campo em revisão (resposta ao utilizador):** sim, cada linha da tabela tem o valor numa caixa de texto editável (os campos de data com seletor, zona/alínea/modalidade com lista). Numa linha marcada REVER a app mostra as leituras disponíveis lado a lado (Vision, Qwen, portal) como botões "usar este valor", e uma caixa livre para escrever o valor correto à mão; ao lado, o `[img]` abre o documento para confirmar. Ao guardar, o campo passa a origem `utilizador`, fica registado em `fichas_campos.editado_por` e em `fichas_eventos`, e alimenta a estatística de fiabilidade (o valor que escreves é a verdade contra a qual os leitores são medidos). Um campo REVER por resolver bloqueia o botão "Aprovar": a aprovação só fica ativa quando todas as linhas estão em alta ou resolvidas por ti. Campos com confiança alta também se podem editar (mesma caixa), e a edição anula qualquer aprovação anterior.

## Modelo de dados (MySQL no CT)

- `users`: id, nome, username, password_hash (Argon2id), ativo. Sem tabela de módulos por agora (constante de código).
- `fichas_casos`: id, mai (char 6), nif (char 9), nome_lido, admissao, zona, local_servico, nhoras, alinea, modalidade, **estado**, versao, pasta_nas, proc_num, bloqueio_motivo, created_by, timestamps. Estados: `registado → em_leitura → por_rever | bloqueado → aprovado → dry_run_ok → a_aplicar → inserido_verificado`; terminais: `cancelado`, `ja_inserido`, `erro_tecnico` (falha técnica ≠ bloqueio funcional).
- `fichas_extracoes`: caso_id, versao, versao_vision (macOS), modelo_qwen, quantizacao, versao_regras, versao_prompts, iniciado_em, terminado_em.
- `fichas_documentos`: caso_id, extracao_id, ficheiro (relativo), sha256, tamanho, mtime, tipo, paginas, texto_vision, json_qwen, confianca. `texto_vision`/`json_qwen` purgados após conclusão (retenção configurável, proposta 30 dias); ficam valores finais + evidência mínima.
- `fichas_cartoes`: caso_id, documento_id, numero, categoria_sufixo, categoria_texto, validade, titular, confianca, conta_como_especialidade (default 1).
- `fichas_campos`: caso_id, campo, valor_vision, valor_qwen, valor_final, origem, confianca, motivo_revisao, editado_por.
- `fichas_aprovacoes`: caso_id, versao, manifesto_json, manifesto_hash, hashes_documentos, aprovado_por, aprovado_em, invalidada_em, motivo.
- `fichas_execucoes`: caso_id, aprovacao_id, alvo (dev/prod), acao (dry_run/apply/verify/ficha_pdf/email), chave_idempotencia, hash_manifesto, versao_inserir_php, exit_code, resultado_sanitizado, timestamps. **Apply sem retry automático**: timeout SSH → reconciliar por SELECT.
- `fichas_eventos`: auditoria.
- `fichas_fiabilidade`: por campo e por caso, o valor de cada leitor (`vision`, `qwen:<modelo>:<quantização>`, `regras`), o valor final confirmado pelo utilizador, e o resultado (`certo`, `errado`, `sem_leitura`, `equivalente_semantico_confirmado`, `equivalente_semantico_rejeitado`). Alimentada automaticamente na aprovação (o valor aprovado é a verdade; um valor escrito ou corrigido à mão pelo utilizador, antes da aprovação ou antes do apply, conta como erro do leitor que o tinha diferente) e na conferência pós-inserção. Concordância entre leitores + checksum = presumido correto, sem confirmação do utilizador. Serve a página **Fiabilidade**: acerto por leitor, por campo e por tipo de documento, por período e por versão de modelo, para decidir se o Qwen fica em prod, muda de quantização ou é trocado por outro modelo (o nome/versão do modelo já é registado em `fichas_extracoes`).

Fila: exclusão por caso, prevenção de duplicados (MAI+NIF+admissão), lease/heartbeat para recuperar `em_leitura` presos, limite de backlog configurável (proposta 200).

## Fluxo de um caso

1. **Registo** (individual ou lote colado no formato da skill). Validação imediata: NIF checksum, MAI 6 dígitos, zona na lista (cache diária das zonas ativas por consulta restrita). Entra em `registado`; o formulário fica livre para o próximo.
2. **Job LerCaso**: localiza `<MAI> <Nome>/Proc <n>` mais alto (ignora `Arquivo`, `old`, `._*`, `Thumbs.db`, `.msg`); 0 ou >1 → `bloqueado`. Por ficheiro: hash → `/v1/ocr` → classificar → extrair por regras → `/v1/analisar-documento` só nos tipos/campos da política → validar → guardar.
3. **Cruzamentos** por SSH de consulta ao CT 107: `portal_lookup.php`, reserva `Reservado`, NIF existente → re-admissão ou bloqueio.
4. **Tabela de aprovação** com checklist da Fase A do SKILL.md, cartões já marcados, campos editáveis (edição anula aprovação anterior).
5. **Aprovar** → snapshot (manifesto + hashes) → **dry-run** via wrapper → resultado → **Aplicar** (dev; prod só na fase 5) → **conferência pós-inserção**: SELECT às tabelas escritas, comparação literal com o manifesto, e Qwen como comparador semântico nos campos onde a literal falha; discrepâncias reais ficam visíveis no caso → `inserido_verificado`.
6. **Ficha em PDF**: no caso inserido, botão **Descarregar ficha (PDF)**: o wrapper gera o PDF no CT 107 com o render que já existe (`funcoes/ficha_funcionario_render.php` + wkhtmltopdf, o mesmo do `fichas_email.php`) e devolve os bytes; a app serve-o com `no-store`, sem guardar cópia.
7. **Enviar por email**: botão por caso e ação por lote (seleção na fila) → wrapper corre `fichas_email.php --ids=... --enviar --ignora-modo-teste` (um email, todos os PDFs, limites de 30 fichas/15 MB já impostos pelo script); resultado registado em `fichas_execucoes` (ação `email`) e marca visível na fila `(email enviado dd-mm)`.

## Segurança

- **CT 107:** utilizador técnico `assistente`, duas chaves (consulta e apply), `authorized_keys` com `from=<IP do CT>`, `command=<wrapper>`, sem PTY/forwarding; `sudoers` só para o wrapper correr como `www-data`; `inserir.php`/`portal_lookup.php` instalados como release versionada, root-owned; o wrapper valida o manifesto, resolve a password localmente, devolve JSON. O CT de dev **nunca** tem a chave de apply de prod.
- **Rota de documentos:** só `documento_id`; autorização sobre o caso; caminho da BD + `realpath` dentro da pasta autorizada; sem symlinks; MIME por conteúdo; PDFs rasterizados por página (nunca o PDF inline); `Cache-Control: private, no-store`, `nosniff`, CSP sem origens externas.
- **Webapp:** só LAN, sem túnel; Argon2id; rate limiting no login; sessões curtas; CSRF; logs e exceções sanitizados (sem NIF/NISS/morada).
- **Dados:** disco do CT e backups cifrados; backup diário do MySQL para a NAS com **restauro testado antes do go-live**; retenção/purga; dev sem cópias permanentes de funcionários reais (dados sintéticos ou purga no fim de cada teste).
- **Mini:** ver arquitetura; verificar na prática que LM Studio/FastAPI/crash reports/Time Machine não retêm prompts nem imagens.

## Divisão do trabalho de código

| Quem | O quê |
|---|---|
| Claude (Sonnet para o mecânico, Fable para decisões/revisão) | modelo de dados e migrações, máquina de estados, auth/autorização, fila, contratos da API do leitor, FastAPI endurecido, rota de documentos, integração SSH e wrapper, motor de extração/validação, manifesto e apply, documentação de deploy escrita a partir do que foi realmente executado |
| Qwen (skill `delegar-mini`, sem ferramentas) | Blade e CSS sem lógica sensível, componentes visuais, fixtures sintéticas, testes unitários para contratos já definidos |

Regras: nada do Qwen entra em dev sem revisão e testes; testes de segurança/aceitação não são escritos pelo mesmo modelo que escreveu o código; gate `/review-pre-deploy` antes de cada upload.

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

### Fase 0 — Contratos (1 dia, sem código de aplicação)
- Threat model e fluxo de dados; schema JSON do manifesto (igual ao que `inserir.php` aceita); máquina de estados; política de confiança por campo (tabela acima) com casos de teste; **contrato do wrapper SSH** (entrada, saída JSON, exit codes); **conjunto de validação**: os 40 processos (770–851) comparados automaticamente com o Sabichão; divergências listadas como "a confirmar", sem adjudicação manual prévia (decisão do utilizador, 22/09/2026: não quer trabalho de verificação um a um; a verdade constrói-se com o uso, ver Fiabilidade). Mais casos negativos sintéticos (documento errado, RC caducado, morada divergente, re-admissão).
- Aceitação: documentos em `docs/` revistos pelo utilizador.

### Fase 1 — Infraestrutura (1 dia)
- CT de dev `assistente-dev` (.69.x): Debian 13, nginx, php-fpm 8.3, composer, MySQL, CIFS read-only por systemd automount, disco cifrado, user `claude` sem sudo. Serviço leitor no mini (`~/leitor/`, launchd, HTTPS, token, firewall ao IP do CT). Wrapper e utilizador técnico no CT 107 **só com a chave de consulta** (apply de prod fica para a fase 5).
- Aceitação: do CT, `/v1/saude` responde; `ls /mnt/processos` após reinício; SSH de consulta devolve JSON; token inválido e tamanho excessivo rejeitados.

### Fase 2 — Motor (2 a 3 dias)
- `php artisan fichas:ler <mai>` + job LerCaso; classificação; extração Vision + regras; Qwen só nos campos da política; OcrDocs versionado com testes de paridade; cruzamentos; manifesto.
- Aceitação contra o conjunto de validação: precisão e cobertura **por campo** face ao Sabichão; **zero valores com confiança alta que divirjam do Sabichão sem explicação** (cada divergência é analisada: ou o Sabichão está errado, como nos casos 806/843, ou o motor baixa a confiança); taxa de campos enviados a revisão; todos os casos negativos bloqueados.

### Fase 3 — Webapp (2 a 3 dias)
- Login, fila com refresh automático, novo caso/lote, página do caso (mockups abaixo), preview seguro de documentos, edição com auditoria, aprovação imutável, página **Fiabilidade** (tabela simples: leitor × campo → acertos/erros/percentagem, filtro por período e modelo).
- Aceitação: 5 casos reais lidos e aprovados pelos RH sem inserir; testes de duplo clique, pedidos concorrentes, documento alterado após aprovação, worker caído a meio, NAS desmontada, mini indisponível, path traversal/symlink/MIME falso.

### Fase 4 — Dry-run e apply em dev (1 dia)
- Wrapper no CT dev; dry-run e apply a partir da aprovação; verificação por SELECT; reconciliação após timeout; reversão documentada (e o que não é reversível).
- **Ficha em PDF** (download) e **email** por caso e por lote, pelo wrapper, testados em dev com o modo de teste de email do Sabichão (redireciona para a caixa de teste).
- Aceitação: 3 a 4 casos aplicados em dev e verificados; uma reversão real; PDF descarregado e email de lote recebido na caixa de teste; purga dos dados de teste; **restauro do backup testado**.

### Fase 5 — Prod (½ dia + acompanhamento)
- CT `assistente` em prod; chave de apply no CT 107; botão "Aplicar em prod" com confirmação dupla; primeiros casos validados **por humano contra os documentos e o dry-run** (nunca reenviando documentos ao Claude).
- Aceitação: 10 casos reais em prod sem correção posterior; fichas do primeiro lote enviadas aos RH pela app. `DEPLOY.md` + `DEPLOY-UPDATE-<data>.md` escritos a partir dos passos executados.

### Fase 6 — Complementos (dentro do plano, depois de prod estável)
Porque estavam "fora": o Codex recomendou cortar tudo o que não é essencial para chegar a uma inserção segura mais cedo. Resposta ao utilizador: ficam **no plano**, só que depois da fase 5, para não atrasar nem arriscar o núcleo. Conteúdo e ordem:
1. **"Perguntar ao Qwen"** na página do caso e página de chat livre (texto já extraído + pergunta; a imagem nunca sai do CT/mini).
3. **Página de estado rica**: temperatura e potência do mini (exige sudoers `powermetrics`, decisão consciente), histórico de tempos por caso.
4. **Certificado de cliente** entre CT e mini (hoje: firewall + HTTPS + token).
5. **Módulos futuros** na mesma plataforma: Contratos (a partir da skill fazer-contrato) e Arquivador (a partir do Arquivador-IA). Cada um terá o seu plano; a plataforma já fica preparada (menu por módulo, permissões por utilizador e módulo, wrapper do CT 107 com ações por módulo).

## Mockups (ASCII)

**Fila**
```
┌──────────────┬──────────────────────────────────────────────────────────────────────┐
│ ASSISTENTE   │  Fichas                                   [ Novo caso ] [ Lote ]     │
│ ▸ Fichas     │  Leitor: Qwen carregado · NAS montada · a ler: 1 · em fila: 3        │
│ Utilizadores │  Filtro: [ Por rever ▾ ]  Pesquisa: [ MAI, NIF ou nome      ]        │
│ Sair         │  ┌─────────┬─────────────────────┬─────────┬─────────────┬─────────┐ │
│              │  │ MAI     │ Nome                │ Admissão│ Estado      │         │ │
│              │  │ 157459  │ Ana Sofia Martins   │ 01-10   │ POR REVER   │ Abrir   │ │
│              │  │ 156614  │ Mariana S. Teixeira │ 01-10   │ EM LEITURA  │ ...     │ │
│              │  │ 175801  │ —                   │ 25-09   │ BLOQUEADO   │ Abrir   │ │
│              │  │ 151669  │ João Pedro Costa    │ 15-09   │ INSERIDO    │ Ver PDF │ │
│              │  └─────────┴─────────────────────┴─────────┴─────────────┴─────────┘ │
│              │  [x] selecionar inseridos   [ Enviar fichas selecionadas por email ]   │
└──────────────┴──────────────────────────────────────────────────────────────────────┘
```

**Novo caso / Lote**
```
│  MAI [157459 ]  NIF [123456789]  Admissão [2026-10-01]  Zona [Porto ▾]              │
│  Local [Theatro Club     ]  Horas [40]  Alínea [b) ▾]  Modalidade [Termo certo ▾]   │
│                                                            [ Registar e ler ]        │
│  Lote (uma linha por caso, formato da skill)                                         │
│  │ 157459, 123456789, 2026-10-01, Porto, Theatro Club, 40, b), termo certo │        │
│  Validação: 2 linhas OK · 0 erros                          [ Registar lote ]         │
```

**Caso — tabela de aprovação** (imagens fechadas; `[img]` abre a página do documento num painel)
```
│  Caso 157459 · Ana Sofia Martins · Proc 851 · POR REVER · versão 1                   │
│  Cartões PSP na raiz (todos contam; desmarque só se houver um a mais)                 │
│  │[x]│ 157459021 │ Vigilante   │ VIG │ 2028-04-26 │ alta │ [img] │                    │
│  │[x]│ 157459051 │ Ass.R.Desp. │ ARD │ 2028-06-16 │ alta │ [img] │                    │
│  │[x]│ 157459061 │ Ass.R.Esp.  │ ARE │ 2028-06-16 │ alta │ [img] │                    │
│  Especialidades resultantes: VIG/ARD/ARE                                              │
│  Campos                                                                               │
│  │ Nome         │ Ana Sofia Martins         │ cc frente.jpg │ alta  │ [img] │          │
│  │ Nº CC        │ 12763582 3 ZV1 (Luhn OK)  │ cc frente.jpg │ alta  │ [img] │          │
│  │ Validade CC  │ 2035-01-22 (MRZ confirma) │ cc frente.jpg │ alta  │ [img] │          │
│  │ NISS         │ 11913587096 (checksum OK) │ cc trás.jpg   │ alta  │ [img] │          │
│  │ Val. RC      │ [2026-12-10       ] REVER │ Certificado.. │ [img] │ Vision: 2026-12-10 [usar] · Qwen: 2026-12-16 [usar] · ou escrever │
│  │ Morada       │ [Rua Dr. Nuno Pinheiro Torres nº223 Casa 22, 4150-543 Porto] │ CC digital │ alta │ [img] │ original: R Dr Nuno Pinheiro Torres BL 5 223 - Casa 22 │
│  │ IBAN         │ PT50 ... (mod-97 OK)      │ iban.pdf      │ alta  │ [img] │          │
│  │ Reserva      │ encontrada (01-10)        │ Sabichão      │ —     │       │          │
│  │ Portal dados │ contacto, email, emerg.   │ portal        │ alta  │       │          │
│  Checklist Fase A: [x] checksums [x] Luhn CC [x] RC vigente+finalidade [x] morada [x] varrimento │
│  1 campo por resolver (Val. RC) — Aprovar fica ativo quando todos estiverem resolvidos                      │
│  [ Guardar edições ]  [ Aprovar (fixa versão) ] (inativo)  [ Dry-run ]  [ Aplicar em DEV ]  [ Aplicar em PROD ] (inativo) │
│  (depois de inserido)  [ Descarregar ficha PDF ]  [ Enviar ficha por email ]  email enviado: —      │
```

## Riscos

| Risco | Mitigação |
|---|---|
| Dois motores erram igual / Qwen inventa | política por campo; Qwen independente e só onde acrescenta; revisão humana com evidência; **estatística contínua por leitor** (`fichas_fiabilidade`, página Fiabilidade) para trocar de modelo com base em dados |
| Falha no Laravel vira execução em prod | chave restrita a wrapper, utilizador técnico, sem shell, apply só a partir do CT de prod |
| Documento muda entre leitura e apply | hashes por documento; aprovação invalidada automaticamente |
| Apply repetido após timeout | sem retry; chave de idempotência; reconciliação por SELECT |
| Mini desligado | app e apply não dependem dele; casos ficam `registado` com aviso |
| Regras divergem entre skill e app | SKILL.md é a especificação; OcrDocs versionado com testes de paridade |
| Gabarito (Sabichão) com erros | divergências analisadas uma a uma na Fase 2; em operação, a verdade vem das correções do utilizador na app (cada correção = flag + entrada em `fichas_fiabilidade`) |

## Primeiros passos ao sair do plano

1. Copiar este plano para `/Volumes/www/assistente/docs/PLANO.md` (apagar o rascunho em `/Volumes/www/fichas-local/`), criar `WORKLOG.md`, guardar na memória a decisão "Sonnet para testes e trabalho mecânico".
2. Fase 0: escrever os contratos em `docs/` e preparar o golden dataset com o utilizador.
