# Auditoria de fidelidade do simulador SPB vs BACEN real

> Data: 2026-07-10
> Escopo: `spb/simulator`, `spb/simulator-frontend` vs cabine `spb/services/bacen_gateway` e consumidor Core `core/backend`
> Instituição: Monetarie SCD, ISPB 46026562, prefixo de controle MON
> Método: leitura arquivo a arquivo, geração empírica de XML, `xmllint`, execução da suíte do simulador. Zero inferência sem prova; toda afirmação traz evidência arquivo:linha.
>
> **ATUALIZAÇÃO 2026-07-10 (sessão de correção, mesma data): os quatro P0 foram CORRIGIDOS no simulador.** A cabine (`spb/services/bacen_gateway`) e o Core não foram tocados. Decisão de arquitetura aplicada: o simulador injeta a mensagem na FRONTEIRA DO FIO da cabine (webhook do sidecar MQ, `POST /api/mq/webhook/SPB01`), de modo que TODO o pipeline real roda (MQBridge -> MQConsumer -> MessageProcessor -> LifecycleEngine -> outbox -> `monetarie.spb.credits.inbound` -> Core) e o simulador nunca mais fabrica evento de Core na mão. Detalhe e evidências na seção 7 (P0 marcados como corrigidos); passo a passo novo na seção 3. Suíte do simulador: 38 testes, 0 falhas (15 pré-existentes + 23 novos).
>
> **ATUALIZAÇÃO 2026-07-10 (trilha "mensagens de entrada por família"): cobertura de ENTRADA estendida a TODAS as famílias que a cabine processa.** 34 tipos de mensagem de entrada gerados por 6 geradores de família (`spb/simulator/lib/simulator/generators/`), todos modelados pelo PARSER REAL da cabine (os testes chamam `parse/1` e `validate/1` dos módulos reais via a dependência de leitura `{:bacen_gateway, path: ..., runtime: false}`), entregues pela fronteira real (`CabinWebhook`). LPI COMPLETO conforme pedido do dono (LPI0006 + todas as pernas R que chegam). Cenários multi-passo iniciados por entrada (`deliver: true` por passo), incluindo o dia completo (abertura -> TED-in -> LPI0006 -> fechamento). Tela Inbound refeita com catálogo dinâmico por família. Detalhe na seção 9. Suíte: 96 testes, 0 falhas; xmllint: 34/34 bem formados.

## 1. Sumário executivo

O simulador SPB é um servidor Phoenix separado (`Simulator.Application`) que replica o BACEN para testes sem a RSFN. Ele cobre bem o lado sintático (envelope DOC/BCMSG/SISMSG, 47 handlers de mensagem, 14 templates de cenário, 34 cenários abrangentes, ciclo COA/COD/R1/R2/R3, abertura/fechamento de dia GEN0014/0015/0017/0018, broadcast de saldo STR0016, header RSFN de 588 bytes com bypass de cripto reconhecido pela cabine). Os XML que ele gera são bem formados (validado por `xmllint`).

Onde ele DIVERGE do real provado é justamente no caminho do dinheiro (crédito de TED entrando):

- O evento NATS que o simulador emite para o Core usa o subject e o payload ERRADOS. O crédito real dos R$ 50.000 (STR0008R2) chegou ao Core pelo subject `monetarie.spb.credits.inbound` com payload `credit_received` (`SpbInboundCreditHandler`), enquanto o simulador publica em `monetarie.spb.transactions.settled` com um JSON de forma diferente que cai no catch-all `SpbHandler.handle_transaction_updated`. São handlers diferentes, contratos diferentes.
- A entrada de TED (`InboundGenerator.generate/2`) é um beco sem saída: gera o XML e o devolve na resposta HTTP, sem publicar em NATS nem enfileirar na cabine. Nada chega ao Core.
- O caminho que de fato publica em NATS (`ScenarioRunner`) só tem templates iniciados por saída, então o ramo de crédito de entrada (`direction: "inbound"`) é código morto, nunca disparado.
- O simulador nunca gera um STR0008R2/STR0004R2 de crédito com os campos que o `LifecycleEngine` da cabine exige (`ISPBIFCredtd`, `CtCredtd`, `NumCtrlSTR`, blocos de cliente creditado). Ele gera STR0010 com `ISPBOrig`/`ISPBDest` e sem bloco de cliente.

Resultado prático (na data da auditoria): o time NÃO conseguia simular ponta a ponta uma TED entrando que creditasse um cliente no Core pelo mesmo caminho que creditou os R$ 50.000 reais. **Estado atual: os quatro P0 foram corrigidos nesta mesma data (ver seção 7); o passo a passo que funciona está na seção 3.**

## 2. Inventário de cobertura

### 2.1 Onde vive e o que sobe no boot

`spb/simulator/lib/simulator/application.ex:13-55` inicia a árvore de supervisão:

| Componente | Arquivo | Papel |
|---|---|---|
| `Simulator.Core.ScenarioEngine` | `core/scenario_engine.ex` | modo de ciclo de vida (success/partial/timeout/r1_reject/r1_error) |
| `Simulator.Core.MessageRouter` | `core/message_router.ex` | roteia mensagem para handler por categoria |
| `Simulator.Core.MQSimulator` | `core/mq_simulator.ex` | filas em memória `QL.BACEN.ENVIO`/`QL.BACEN.RETORNO` (`mq_simulator.ex:17-18`) |
| `Simulator.Core.ScenarioRunner` | `core/scenario_runner.ex` | máquina de estados de cenário multi-passo; único que publica em NATS |
| `Simulator.Handlers.DayLifecycleHandler` | `handlers/day_lifecycle_handler.ex` | abertura/fechamento de dia GEN0014/0015/0017/0018 |
| `Simulator.Generators.InboundGenerator` | `generators/inbound_generator.ex` | despacho de 34 tipos de entrada para os geradores por família (`generators/{str_inbound,lpi,gen_inbound,ldl,dda,post_integration}_generator.ex`, seção 9) |
| `Simulator.MQ.MQPoller` | `mq/mq_poller.ex` | poll de IBM MQ REST real, injeta COA/COD/R1/R2/R3 |
| `Simulator.Schedule.GradeSimulator`, `HolidayCalendar` | `schedule/*` | grade horária e feriados |
| `Simulator.Persistence.StateStore` | `persistence/state_store.ex` | ETS + PostgreSQL |
| `SimulatorWeb.Endpoint` | Phoenix + canais WebSocket | API REST + dashboard |

Handlers de mensagem: 47 arquivos em `lib/simulator/messages/*_handler.ex` (STR, LPI, SEL, CAM, PAG, LDL, DDA, CIR, GEN, TED, DOC etc.). Templates de cenário: 14 (`core/scenario_templates.ex`). Cenários abrangentes: 34 (`core/comprehensive_scenarios.ex`).

### 2.2 O que simula (resposta às perguntas do mandato)

- MQ? Sim, em duas camadas. `MQSimulator` (filas ETS em memória, nomes legados `QL.BACEN.ENVIO`/`QL.BACEN.RETORNO`, `mq_simulator.ex:17-18`) usado pela API `/api/mq/send`. E `MQPoller` (`mq/mq_poller.ex`), que fala com a IBM MQ REST real nos nomes de fila corretos `QR.REQ.46026562.00038166.0X` / `QL.RSP.00038166.46026562.0X` (`mq/queue_config.ex:51-53`). O `MQPoller` está DESLIGADO por padrão: `polling_enabled` vem de `MQ_POLLING_ENABLED` default `false` (`config/config.exs:46`, checado em `mq_poller.ex:32,296`).
- Respostas R1? Sim, `ResponseGenerator.generate_typed_r1/3` (`core/response_generator.ex:187`), com campos por categoria SFN (`response_generator.ex:506-572`).
- Chegada de STR0008R2? Como resposta a um envio nosso, sim (`response_generator.ex:238-272`). Como crédito de entrada avulso, NÃO (ver P0-1).
- GEN de abertura/fechamento? Sim, GEN0014 -> GEN0015 e GEN0017 -> GEN0018, com estado de dia (`day_lifecycle_handler.ex:101-168`).
- LDL? Sim, handler dedicado (`messages/ldl_handler.ex`) e campos R1 (`response_generator.ex:542-549`).
- DDA? Sim, handler dedicado (`messages/dda_handler.ex`) e R1 (`response_generator.ex:562-564`).
- Como se pluga na cabine? Não substitui o sidecar MQ. Dois modos: (a) a cabine roda com `SIMULATOR_MODE=true` e o simulador entrega XML cru no webhook do sidecar (`POST /api/mq/webhook/:domain`) — desde 2026-07-10 esse é o plugue canônico, usado por `generate_inbound` e pelo cenário `ted_inbound_credit` (`Simulator.Delivery.CabinWebhook`); a cabine pula a cripto para corpo iniciado em `<` (`mq/mq_consumer.ex:113-135`, `mq/domain_manager.ex:1211-1213`); (b) o `MQPoller` do simulador escreve direto nas filas MQ REST que a cabine lê. Não existe flag `IBM_MQ_ENABLED=false` no simulador; essa flag é da cabine (`services/bacen_gateway/config/runtime.exs:75`). O terceiro plugue antigo (ScenarioRunner publicando `settled` sintético direto em NATS para o Core) foi REMOVIDO em 2026-07-10 (P0-2).

### 2.3 XMLs que gera (validados)

Geração empírica em 2026-07-10 (app do simulador iniciado, `mix run`), depois `xmllint --noout`:

| XML gerado | Origem | `xmllint` |
|---|---|---|
| STR0010 (entrada) | `InboundGenerator.do_generate("STR0010")` | BEM FORMADO |
| STR0016 (saldo) | `InboundGenerator.do_generate("STR0016")` | BEM FORMADO |
| STR0008R2 | `ResponseGenerator.generate_typed_r2` | BEM FORMADO |
| STR0008R1 | `ResponseGenerator.generate_typed_r1` | BEM FORMADO |
| COA | `ResponseGenerator.generate_coa` | BEM FORMADO |
| STR0001 request (template) | `config/message_templates/str0001_request.xml` | BEM FORMADO |

Ressalva de validação de esquema: o repositório NÃO contém os XSD reais do catálogo SPB (STR/GEN/LDL etc.). Em `spb/services/bacen_gateway/priv/xsd/` e em `mwbank/md/XSDDOCV511/` os arquivos são documentação `.md` (resumos), não `.xsd`. Os únicos `.xsd` reais no repo são do PIX SPI (pacs/pain/camt/reda em `mwbank/md/v5.11.1/xsd/`), que não se aplicam ao SPB. Logo a validação estrita contra XSD do SPB NÃO é possível aqui; a comparação de campos abaixo é feita contra a especificação documentada no catálogo `.md` e contra o parser real da cabine (`str0008r2.ex`).

## 3. Como o time simula uma TED entrando (passo a passo que FUNCIONA, pós-correção de 2026-07-10)

O simulador agora injeta a STR0008R2 de crédito na FRONTEIRA DO FIO da cabine: o webhook do sidecar MQ (`POST /api/mq/webhook/SPB01` do bacen_gateway). A partir daí roda o MESMO pipeline que creditou os R$ 50.000 reais: `MQBridge.handle_webhook` -> `MQConsumer.process_message` -> `MessageProcessor` -> `LifecycleEngine.dispatch_r2` (reconhece crédito receiver-side, materializa `spb_operations` inbound) -> outbox `spb_inbound_credit` -> NATS `monetarie.spb.credits.inbound` -> Core `SpbInboundCreditHandler` (AccountResolver fail-closed).

### Pré-requisitos
1. Cabine `bacen_gateway` de pé com `SIMULATOR_MODE=true`. É essa flag que ativa o bypass de cripto para XML cru vindo do simulador (`mq/mq_consumer.ex:113-135`: corpo começando com `<` vira `%{simulator: true}` e pula decifra e assinatura). Sem a flag, a cabine exige header criptografado real e rejeita (fail-closed, correto).
2. Se a cabine exigir assinatura de webhook: ou exporte o MESMO `MQ_WEBHOOK_SECRET` no simulador (ele passa a assinar o corpo com HMAC-SHA256, header `x-webhook-signature`, igual ao sidecar), ou rode a cabine com `WEBHOOK_ALLOW_UNSIGNED_INTERNAL=true` (origem precisa ser rede interna, `webhook_auth_plug.ex:54-64,112-120`).
3. Aponte o simulador para a cabine: `CABIN_BASE_URL` (default `http://localhost:4010`) ou `CABIN_WEBHOOK_URL` (URL completa). Lidos em RUNTIME (`delivery/cabin_webhook.ex`).
4. Para o crédito chegar ao Core: NATS de pé, cabine com outbox worker rodando e Core com o `SpbConsumer` durável no stream `MONETARIE_SPB`.
5. A conta creditada deve existir no Core (`creditor_account`); conta desconhecida cai no caminho fail-closed do `SpbInboundCreditHandler` (suspense/pendência), que também é comportamento real.

### Caminho A (recomendado): tela Inbound ou `POST /api/simulator/generate_inbound`
1. Tela `simulator-frontend/src/views/InboundView.vue` -> tipo "STR0008 - TED recebida p/ cliente (STR0008R2)", preenchendo valor, conta creditada, nome e CPF/CNPJ do cliente. Ou por API:

```bash
curl -X POST http://localhost:4001/api/simulator/generate_inbound \
  -H 'Content-Type: application/json' \
  -d '{"type":"STR0008","opts":{"amount":"50000.00","creditor_account":"1176749","creditor_name":"CLIENTE HML","creditor_document":"39053344705"}}'
```

2. O `InboundController` gera o STR0008R2 real (bloco de cliente creditado completo) e ENTREGA no webhook da cabine por padrão (`deliver: true`). Resposta de sucesso traz `delivered: true`, `cabin_status: 200`, `cabin_url` e o XML.
3. Se a cabine não estiver de pé, a resposta é HTTP 502 com `stage: "delivery"`, o motivo real (`econnrefused` etc.) e o XML gerado — fallback claro, nada falha em silêncio (`inbound_controller.ex`, ramo `{:error, {:delivery_failed, ...}}`).
4. Para só inspecionar o XML sem entregar: `"deliver": false`.
5. `"type":"STR0004"` gera o STR0004R2 (TED IF->IF, sem bloco de cliente, vira `credit_kind=institutional` na ponte).

### Caminho B: cenário `POST /api/scenarios/start` (ScenarioRunner)
1. `POST /api/scenarios/start {"template_id":"ted_inbound_credit"}` — template novo iniciado por ENTRADA (`scenario_templates.ex`, primeiro passo `direction: :inbound`, `message_type: "STR0008R2"`).
2. Ao iniciar, o runner gera o STR0008R2 e entrega no webhook da cabine em Task assíncrona; o passo avança com `DELIVERED-HTTP200` e o run completa. Cabine fora do ar marca o run como `failed` com o erro real no passo (provado por teste, `scenario_runner_inbound_test.exs`).
3. O runner NÃO publica mais NENHUM evento sintético `monetarie.spb.transactions.settled` para o Core (P0-2/P0-3): quem publica para o Core é a própria cabine, pelo outbox real.

### Verificação ponta a ponta (o que conferir depois do POST)
1. Cabine: `spb_operations` com `direction='inbound'`, `message_type='STR0008'`, `control_number_clearing` = NumCtrlSTR enviado, estado `r2_confirmed`.
2. NATS: evento `credit_received` em `monetarie.spb.credits.inbound`.
3. Core: linha em `spb_inbound_credits` (chave `num_ctrl_str`), depósito TigerBeetle e extrato com referência `SPBCR<NumCtrlSTR>`.
4. Reentrega do MESMO NumCtrlSTR e idempotente na cabine (indice parcial unico + ON CONFLICT, `lifecycle_engine.ex:1327-1359`) — repetir o POST com o mesmo `num_ctrl_str` NAO duplica a operação.

### Atenção: verificação de crédito duplo TED-in em aberto
`docs/reports/2026-07-10-ted-in-duplo-credito-verificacao.md` descreve o risco de a cabine emitir DOIS eventos por STR0008R2 (hook legado `monetarie.spb.transactions.settled` via `CoreNotifier.notify_inbound_settled` + ponte `monetarie.spb.credits.inbound`), com caminhos de ledger distintos no Core e sem dedup cruzado. O simulador agora injeta exatamente no ponto de entrada recomendado pelo procedimento de verificação daquele relatório (item 4.2). Ao rodar a primeira TED-in simulada em HML, conferir se o cliente foi creditado UMA vez ou duas — se duas, é o defeito da cabine descrito lá (a correção é decisão do time; o simulador não mascara nem corrige isso).

### Nota técnica sobre o header de 588 bytes (provada em código)
O formato default de entrega é XML CRU (`wire_format=raw_xml`), porque é o ÚNICO que a cabine de hoje aceita em modo simulador: com um header 588 presente, `unpack_inbound` cai em `parse_security_header` -> `MessagePacker.unpack`, que executa RSA/3DES REAIS sobre os buffers (`message_packer.ex:270-290,364-390`) — os buffers zerados do `HeaderSimulator` falham a decifra, mesmo com serial `SIMULATOR` e `SIMULATOR_MODE=true` (o branch de `simulator_header?` em `mq_consumer.ex:124-135` só aceita corpo que começa com `<`). O empacotamento 588 com serial SIMULATOR está implementado e disponível (`SIMULATOR_WIRE_FORMAT=wrapped_588` ou `wire_format: :wrapped_588`), para transportes que desempacotam o header antes (ex.: fila IBM MQ REST via `MQPoller`) e para quando a cabine reconhecer o serial SIMULATOR no próprio unpack.

### Caminho C: `MQPoller` (IBM MQ REST real) — inalterado
Confirma envios nossos com COA/COD/R1/R2/R3 nas filas MQ reais (`mq_poller.ex:118-175`); não origina crédito de entrada. Para TED entrando, usar os caminhos A ou B.

## 4. Matriz de fidelidade

Legenda: FIEL = equivale ao real provado; DIVERGE = existe mas diferente; AUSENTE = não existe.

| # | Item | Real provado (evidência) | Simulador (evidência) | Veredito |
|---|---|---|---|---|
| 1 | Subject NATS do crédito de entrada | `monetarie.spb.credits.inbound`, handler dedicado `SpbInboundCreditHandler` (`spb_consumer.ex:66-69`); crédito dos R$ 50k chegou por aqui via outbox (`spb_consumer.ex:11-17`) | ~~publicava só `monetarie.spb.transactions.settled` sintético~~ **CORRIGIDO 2026-07-10**: o simulador NÃO publica evento de crédito nenhum; injeta a STR0008R2 no fio e quem publica `credits.inbound` é a PRÓPRIA cabine (outbox real) | FIEL (pós-fix) |
| 2 | Payload do crédito de entrada | `event=credit_received`, `num_ctrl_str`, `amount_brl` (reais string), `creditor_account`, `credit_kind`, blocos creditado/devedor (`lifecycle_engine.ex:1693-1720`; `core_notifier.ex:69-110`) | ~~payload sintético incompatível (`amount:250_000`, `account_id:201`)~~ **CORRIGIDO 2026-07-10**: payload gerado pela cabine a partir do XML real do simulador (`build_inbound_credit_event_payload`) — zero drift de contrato por construção | FIEL (pós-fix) |
| 3 | Mensagem que traz o crédito | STR0008R2/STR0004R2 onde SOMOS creditados (`lifecycle_engine.ex:1096-1130`); parser exige `ISPBIFCredtd`, `CtCredtd`/`CtPgtoCredtd`, `NumCtrlSTR`, cliente creditado (`str0008r2.ex:61-93`) | ~~gerava só STR0010 sem esses campos~~ **CORRIGIDO 2026-07-10**: `do_generate("STR0008R2"/"STR0004R2")` com todos os campos exigidos (`inbound_generator.ex`; testes verdes) | FIEL (pós-fix) |
| 4 | Entrega da entrada ao Core | pela cabine: MQ -> `MQBridge` -> `MessageProcessor` -> `LifecycleEngine.dispatch_r2` -> outbox `spb_inbound_credit` (`lifecycle_engine.ex:1519-1524,1660-1685`) | ~~devolvia XML na resposta HTTP, sem entrega~~ **CORRIGIDO 2026-07-10**: entrega no webhook MQ da cabine (`delivery/cabin_webhook.ex`; `inbound_controller.ex` deliver default) — o pipeline real da cabine publica ao Core | FIEL (pós-fix) |
| 5 | Cenário de crédito de entrada | STR0008R2 avulso do parceiro credita o cliente | ~~ramo `inbound` era código morto~~ **CORRIGIDO 2026-07-10**: template `ted_inbound_credit` iniciado por entrada + `start_inbound_delivery` (`scenario_templates.ex`; `scenario_runner.ex`) | FIEL (pós-fix) |
| 6 | Confirmação R1 após envio (semântica) | R1 do parent por operação (`lifecycle_engine.ex:handle_r1`) | `generate_typed_r1` + sequência COA/COD/R1 (`response_generator.ex:187-357`; `mq_poller.ex:132-154`) | FIEL |
| 7 | Namespace do R1/R2 | R2 é xs:choice dentro do XSD pai; namespace = `.../SPB/STR0008.xsd` (`str0008r2.ex:18-22,42`) | ~~usava `.../SPB/STR0008R2.xsd`~~ **CORRIGIDO 2026-07-10 (trilha respostas)**: TODA resposta (R1/R2/R3) sai com o namespace do módulo PAI real da cabine (`CabinCatalog.parent_namespace/1`) | FIEL (pós-fix) |
| 8 | Campos obrigatórios do STR0008R2 | `NumCtrlSTR, ISPBIFDebtd, TpCtDebtd, CtDebtd, ISPBIFCredtd, CNPJ_CPFCliCredtd, VlrLanc, FinlddCli, DtMovto`; SEM `SitMsg` (`str0008r2.ex:12-16,61-93`) | ~~só `CodMsg, NumCtrlSTR, DtHrBC, SitMsg, DescSitMsg`~~ **CORRIGIDO 2026-07-10**: corpo do leg gerado do layout REAL do acervo (`legacy_tag_definitions.csv`, 707 legs R), com ECO dos campos do pedido; R2 sem `SitMsg` e com a sequência completa — validado pelo parser real `STR0008R2.parse/1` | FIEL (pós-fix) |
| 9 | Número de controle com prefixo MON | `MON` + data + sequência (`scheduled_query_catalog.ex:491-497`; `BACEN_CONTROL_PREFIX=MON`) | **CORRIGIDO 2026-07-10 nos fluxos de RESPOSTA**: `NumCtrl*` no formato real (prefixo + AAAAMMDDhhmmss + 3 dígitos), determinístico por NUOp (R1/R2/R3 da mesma operação levam o MESMO número), `NumCtrlIF` ECOADO do pedido, zero UUID; o `str_handler` sintético virou delegação pura. Pendente fora desta trilha: run ids do `scenario_runner.ex` | FIEL nas respostas |
| 10 | Timestamp DtHrBC | `AAAA-MM-DDThh:mm:ss` sem fração, horário de Brasília (`lifecycle_engine.ex` parse `parse_str_credit_bc_datetime`) | **CORRIGIDO 2026-07-10 nas respostas e COA/COD**: todo `DtHr*` sai sem fração de segundo, horário de Brasília (`response_generator.ex`, `bc_datetime/0`); geradores de entrada são da trilha de inbound | FIEL nas respostas |
| 11 | Agrupamento operação por NUMORIGEM | operação = COALESCE(NUMORIGEMOR, NUMORIGEM); envio + R1 + R2 na mesma operação (modelo AutBank canônico) | correlação por `correlation_id` de fila (`mq_poller.ex:257-262`); não há conceito de agrupamento por NUMORIGEM/NUMORIGEMOR | DIVERGE (P2) |
| 12 | STR0010 semântica | STR0010 não é confirmação R2 de TED de cliente no catálogo | docstring o descreve como "Transfer confirmation (R2 for STR transfers)" (`inbound_generator.ex:10`) | DIVERGE (P2) |
| 13 | Abertura/fechamento de dia | GEN0014->GEN0015, GEN0017->GEN0018 | idêntico, com estado e rejeição de duplicata (`day_lifecycle_handler.ex:101-168`) | FIEL |
| 14 | Broadcast de saldo STR0016 | saldo em conta de reservas com quebras por câmara | `SldRSV/SldDisp/SldBloq` + `GrpCmpe` por câmara (`inbound_generator.ex:219-281`) | FIEL (estrutura) |
| 15 | Header RSFN 588 bytes | `HeaderBacen` real, cripto + assinatura | 588 bytes com buffers zerados, serial `...-SIMULATOR-CERT` (`header_simulator.ex:91-116`) | FIEL como stub (ver #16) |
| 16 | Dispensa de HSM/assinatura | fail-closed: assinatura inválida rejeita (`mq_consumer.ex:189-215`) | bypass só quando `simulator:true`/`version:0`/serial `SIMULATOR` (`mq_consumer.ex:113-151,191-195`; `domain_manager.ex:627-635,1211-1213`) | FIEL (gating correto) |
| 17 | Nomes de fila MQ | `QR.REQ.{inst}.{bacen}.0X` / `QL.RSP.{bacen}.{inst}.0X` (`domain_manager.ex:21-22`; `mq_bridge.ex:27-31`) | **CORRIGIDO 2026-07-10**: `MQSimulator` aposentou `QL.BACEN.ENVIO/RETORNO` e usa a convenção real do `QueueConfig` (`QR.REQ.46026562.00038166.01` / `QL.RSP.00038166.46026562.01`), com teste (`mq_simulator_test.exs`) | FIEL (pós-fix) |
| 18 | URL NATS de destino | `nats://nats-{1,2,3}.monetarie.internal:4222` | ~~default morto `nats://172.69.77.10:4222` em compile-time~~ **CORRIGIDO 2026-07-10**: `NATS_URL` lido em runtime, default `nats://localhost:4222`, falha com `Logger.error` explícito (`nats/publisher.ex`) | FIEL (pós-fix) |
| 19 | DomSist nos templates | `SPB01` (domínio real) | template STR0001 usa `<DomSist>STR</DomSist>` (`config/message_templates/str0001_request.xml`) | DIVERGE (P2) |

## 5. Validação empírica de XML

`xmllint` disponível (`/usr/bin/xmllint`). Todos os exemplares gerados pelo simulador em 2026-07-10 passaram como bem formados (seção 2.3). Exemplo STR0010 de entrada gerado:

```
<DOC xmlns="http://www.bcb.gov.br/SPB/STR0010.xsd">
  <BCMSG>
    <IdentdEmissor>00038166</IdentdEmissor>
    <IdentdDestinatario>46026562</IdentdDestinatario>
    <IdentdEmissorDes662>00038166</IdentdEmissorDes662>
    <IdentdDes662>46026562</IdentdDes662>
    <DomSist>SPB01</DomSist>
    <NUOp>0fa82cab-351c-4b07-abbd-83a68d8d1cc2</NUOp>
    <DtHrMsg>2026-07-10T23:01:47.964324</DtHrMsg>
  </BCMSG>
  <SISMSG>
    <STR0010>
      <CodMsg>STR0010</CodMsg>
      <NumCtrlBCB>0fa82cab-351c-4b07-abbd-83a68d8d1cc2</NumCtrlBCB>
      <ISPBOrig>00000000</ISPBOrig><ISPBDest>46026562</ISPBDest>
      <DtHrBC>2026-07-10T23:01:47.958884</DtHrBC>
      <VlrLanc>50000.00</VlrLanc>
      <SitLancSTR>ACCP</SitLancSTR>
      <NUOp>0fa82cab-351c-4b07-abbd-83a68d8d1cc2</NUOp>
    </STR0010>
  </SISMSG>
</DOC>
```

Observações contra a especificação real (não há XSD do SPB no repo para validação estrita; comparação feita contra `str0008r2.ex` e o catálogo `.md`):
- `NUOp` e `NumCtrlBCB` são UUID; o real usa controle `MON`+data+sequência.
- `DtHrBC`/`DtHrMsg` trazem microssegundos; o real é `AAAA-MM-DDThh:mm:ss` sem fração.
- Campos de crédito de cliente ausentes (`ISPBIFCredtd`, `CtCredtd`, `NomCliCredtd`), sem os quais `receiver_side_str_credit?/1` da cabine (`lifecycle_engine.ex:1262-1268`) retorna falso e nenhum crédito é materializado.

Não é possível validação estrita contra XSD do SPB porque o repo só tem os XSD reais do PIX SPI (`mwbank/md/v5.11.1/xsd/`), e o catálogo SPB está como resumos `.md`.

## 6. Testes do simulador

Executado apenas o que pertence ao simulador:

```
cd spb/simulator && MIX_ENV=test mix test
```

Resultado na auditoria original: `15 tests, 0 failures` (seed 603042). Arquivos: `core/message_catalog_contract_test.exs`, `core/response_generator_contract_test.exs`, `core/comprehensive_scenarios_contract_test.exs`, `core/scenario_engine_test.exs`, `mq/queue_config_test.exs`.

**Pós-correção (2026-07-10): `38 tests, 0 failures`** — os 15 pré-existentes seguem verdes e foram adicionados 23 testes novos (TDD):

- `test/simulator/generators/inbound_generator_test.exs` (10): campos obrigatórios do STR0008R2/STR0004R2 contra o contrato do parser da cabine, `NumCtrlSTR` na faixa real, `DtHrBC` sem fração, namespace do XSD pai, overrides, conta de pagamento (`CtPgtoCredtd`), aliases e erro claro para tipo sem gerador.
- `test/simulator/delivery/cabin_webhook_test.exs` (7): URL em runtime, entrega raw no webhook com headers estilo sidecar, HMAC com `MQ_WEBHOOK_SECRET`, formato `wrapped_588` (header 588 com serial SIMULATOR), cabine fora do ar e rejeição HTTP.
- `test/simulator/core/scenario_runner_inbound_test.exs` (3): template `ted_inbound_credit` iniciado por entrada, entrega real do STR0008R2 em servidor HTTP local (verificando o XML recebido no path `/api/mq/webhook/SPB01`) com run completando, e run `failed` com erro real quando a cabine está fora.
- `test/simulator/nats/publisher_test.exs` (2): `NATS_URL` em runtime e default `nats://localhost:4222`.

Suporte de teste: `test/support/webhook_test_server.ex` + `webhook_capture_plug.ex` (servidor Bandit local simulando o webhook da cabine). `mix deps.get` acusa mismatch de lock em `phoenix_template 1.0.4`, mas a suíte compila e roda com o `_build` existente.

**Pós-trilha de entradas por família (2026-07-10, mais tarde): `96 tests, 0 failures`** — inclui os testes das outras trilhas do dia e os 43 novos da trilha de entradas (seção 9.5), que validam cada gerador contra o `parse/1`/`validate/1` REAIS dos módulos da cabine.

## 7. Prioridades para fidelidade rígida

### P0 (bloqueavam simular corretamente uma TED entrando) — TODOS CORRIGIDOS em 2026-07-10

- P0-1 **CORRIGIDO**: gerador de crédito de entrada real implementado. `InboundGenerator.do_generate/2` agora emite STR0008R2 (aliases `STR0008`) e STR0004R2 (alias `STR0004`) modelados pelo parser real da cabine (`bacen_gateway/messages/str/str0008r2.ex`) e pelo discriminador `receiver_side_str_credit?` (`lifecycle_engine.ex:1262-1268`): `ISPBIFCredtd` = 46026562, `VlrLanc`, `NumCtrlSTR` na faixa real observada (`STR` + AAAAMMDDhhmmss + 3 dígitos, mesmo formato do `STR20260706033056377` dos R$ 50.000), bloco de cliente creditado (`AgCredtd`, `TpCtCredtd`, `CtCredtd` ou `CtPgtoCredtd` quando `TpCt=PG`, `TpPessoaCredtd`, `CNPJ_CPFCliCredtd`, `NomCliCredtd`), espelho do debitado, `FinlddCli`, `DtMovto` (data de Brasília), `DtHrBC`/`DtHrMsg` sem fração de segundo e namespace do XSD PAI (`.../SPB/STR0008.xsd`, sem `SitMsg`). XML validado com `xmllint` (bem formado). A tela Inbound foi alinhada: STR0004/STR0008 agora funcionam e os tipos sem gerador (LDL0001/LDL0004/SEL0001/SEL0002/CAM0001/CTP0001) foram removidos da oferta (`InboundView.vue`). Evidência: `inbound_generator.ex` (novos `do_generate` STR0008R2/STR0004R2), testes `test/simulator/generators/inbound_generator_test.exs` (10 testes).
- P0-1b **CORRIGIDO**: `generate_inbound` deixou de ser beco sem saída. Novo módulo `Simulator.Delivery.CabinWebhook` entrega a mensagem no webhook da cabine (`POST {CABIN_BASE_URL:-http://localhost:4010}/api/mq/webhook/{domain}`), com URL configurável em RUNTIME (`CABIN_WEBHOOK_URL`/`CABIN_BASE_URL`), assinatura HMAC opcional (`MQ_WEBHOOK_SECRET`), headers de origem estilo sidecar (`x-mq-message-id`/`x-mq-source-queue`/`x-mq-queue-manager`) e fallback claro de erro (`{:error, {:cabin_unreachable, ...}}` com log explícito; controller devolve 502 com o motivo e o XML). O empacotamento 588 reutiliza o `HeaderSimulator` existente via `wire_format: :wrapped_588`; o default é `raw_xml` porque, PROVADO em código, a cabine só aceita XML cru em `SIMULATOR_MODE` (com header presente ela roda cripto real em `MessagePacker.unpack`, `message_packer.ex:270-290` — ver nota técnica na seção 3). `InboundController.generate` entrega por padrão (`deliver: true`, opt-out `"deliver": false`), inclusive em batch. Evidência: `delivery/cabin_webhook.ex`, `inbound_controller.ex`, testes `test/simulator/delivery/cabin_webhook_test.exs` (7 testes, incluindo entrega real em servidor HTTP local, HMAC e os dois formatos de fio).
- P0-2 **CORRIGIDO**: o `ScenarioRunner` NÃO publica mais `monetarie.spb.transactions.settled` sintético para o Core — `maybe_publish_scenario_result/1` foi removido por inteiro (payload `account_id: 201`, `amount: 100_000` etc. eliminado). Cenário de entrada usa o mesmo caminho do P0-1b: entrega no webhook da cabine, e os eventos ao Core saem do pipeline real dela (outbox -> `monetarie.spb.credits.inbound`). Evidência: `scenario_runner.ex` (comentário P0-2 no lugar do publish; `start_inbound_delivery/2` + `handle_cast {:inbound_step_result, ...}`).
- P0-3 **CORRIGIDO**: ramo `inbound` destravado com o template novo `ted_inbound_credit` (`scenario_templates.ex`), iniciado por ENTRADA (passo 1 `direction: :inbound`, `message_type: "STR0008R2"`). Ao iniciar o run, o runner gera e entrega o STR0008R2 na cabine em Task assíncrona; sucesso completa o run (`DELIVERED-HTTP200`), cabine fora do ar marca `failed` com o erro real. Evidência: `test/simulator/core/scenario_runner_inbound_test.exs` (3 testes, incluindo prova de entrega no webhook com verificação do XML recebido e prova do caminho de falha).
- P0-4 **CORRIGIDO**: `Simulator.Nats.Publisher` não tem mais IP morto compilado. `nats_url/0` lê `NATS_URL` em RUNTIME com default `nats://localhost:4222`; falha de conexão/publicação agora loga `Logger.error` explícito com a URL e o motivo (antes era `Logger.debug` silencioso). Evidência: `nats/publisher.ex`, testes `test/simulator/nats/publisher_test.exs` (2 testes). Observação: com P0-2, o runner não depende mais desse publisher; o módulo permanece para uso utilitário.

Execução da suíte pós-correção (2026-07-10):

```
cd spb/simulator && MIX_ENV=test mix test
38 tests, 0 failures
```

XML gerados na sessão de correção e validados com `xmllint --noout` (bem formados): STR0008R2 com bloco de cliente e STR0004R2 IF->IF (exemplo STR0004R2 abaixo, gerado empiricamente):

```
<DOC xmlns="http://www.bcb.gov.br/SPB/STR0004.xsd">
  <BCMSG>...<DomSist>SPB01</DomSist><NUOp>99999999202607107554699</NUOp><DtHrMsg>2026-07-10T21:55:17</DtHrMsg></BCMSG>
  <SISMSG><STR0004R2>
    <CodMsg>STR0004R2</CodMsg>
    <NumCtrlSTR>STR20260710215517720</NumCtrlSTR>
    <DtHrBC>2026-07-10T21:55:17</DtHrBC>
    <ISPBIFDebtd>99999999</ISPBIFDebtd>
    <ISPBIFCredtd>46026562</ISPBIFCredtd>
    <VlrLanc>1000.00</VlrLanc>
    <FinlddIF>01</FinlddIF>
    <DtMovto>2026-07-10</DtMovto>
  </STR0004R2></SISMSG>
</DOC>
```

### P1 (fidelidade de protocolo)

- P1-1 **CORRIGIDO em 2026-07-10 (completo)**: além dos geradores do `InboundGenerator`, o `ResponseGenerator` foi reescrito catalog-driven e TODA resposta (R1/R2/R3) usa o namespace do XSD PAI vindo do módulo real da cabine (`CabinCatalog.parent_namespace/1`, que delega para `module.namespace/0` do módulo-base gerado dos XSDs). Não existe mais `.../SPB/<CODIGO>R<N>.xsd` em nenhum fluxo de resposta (teste: `response_generator_contract_test.exs`).
- P1-2 **CORRIGIDO em 2026-07-10**: o corpo de cada leg de resposta agora vem do layout REAL do acervo (`legacy_tag_definitions.csv` da cabine, 707 legs R com sequência/obrigatoriedade/ou-exclusivo/grupos aninhados), com ECO dos campos de identidade do pedido; `SitMsg` só aparece quando o layout real o tem (o R1 do STR0008, por exemplo, sai com `NumCtrlIF` ecoado + `ISPBIFDebtd` + `NumCtrlSTR` + `SitLancSTR` + `DtHrSit` + `DtMovto`, exatamente o layout do acervo, e SEM `SitMsg`). Validação nos testes com o parser REAL da cabine (`parse/1` do módulo R correspondente).
- P1-3 **CORRIGIDO em 2026-07-10 nos fluxos de resposta**: `NumCtrl*` atribuído pelo BACEN sai no formato real (prefixo + AAAAMMDDhhmmss + 3 dígitos) e é determinístico por operação (R1/R2/R3 do mesmo NUOp levam o MESMO número, como no fluxo real); `NumCtrlIF` (que é da IF, prefixo MON) é ECOADO da mensagem original; o gerador sintético de 9 dígitos do `str_handler.ex` foi removido (o handler virou delegação pura). PENDENTE (fora desta trilha): run ids do `scenario_runner.ex`.

### P2 (higiene e semântica)

- P2-1 (AVANÇOU em 2026-07-10): além dos novos STR0008R2/STR0004R2, o `ResponseGenerator` inteiro (todos os legs de resposta + COA/COD + GEN9901) emite `DtHr*` sem fração de segundo, horário de Brasília. Restam os geradores antigos de ENTRADA (STR0016/GEN0001/STR0010), que são da trilha de inbound.
- P2-2 (PENDENTE): introduzir agrupamento de operação por NUMORIGEM/NUMORIGEMOR (modelo operação != mensagem) para espelhar o acervo real; hoje só há `correlation_id` de fila.
- P2-3 (PARCIAL em 2026-07-10): docstring do STR0010 corrigida no `InboundGenerator` (é aviso de situação de lançamento, não "R2 for STR transfers"). PENDENTE: `DomSist` do template STR0001 (`STR` -> `SPB01`, `config/message_templates/str0001_request.xml`).
- P2-4 **CORRIGIDO em 2026-07-10**: o `MQSimulator` aposentou `QL.BACEN.ENVIO/RETORNO` e passou a usar a convenção real do `QueueConfig` (`QR.REQ.{inst}.{bacen}.01` / `QL.RSP.{bacen}.{inst}.01`, domínio SPB01), com roundtrip provado por teste (`test/simulator/core/mq_simulator_test.exs`).
- P2-5 (CORRIGIDO em 2026-07-10): tela Inbound alinhada ao backend — STR0004/STR0008 implementados (geram os R2 de crédito e entregam na cabine); LDL0001/LDL0004/SEL0001/SEL0002/CAM0001/CTP0001 removidos da oferta; campos de valor/conta/cliente creditado adicionados; STR0010 exposto (`InboundView.vue`).

### Frontend do simulador (item extra do mandato de correção)

- CORRIGIDO em 2026-07-10: `spb/simulator-frontend` não tinha `package.json` (só o lock) e o proxy do Vite apontava para a porta errada (4100; o backend do simulador roda em 4001, `simulator/config/dev.exs`). Criado `package.json` mínimo coerente com o `package-lock.json` existente (`npm ci` roda sem alterar o lock) e proxy corrigido para `http://localhost:4001`. Um erro de tipo pré-existente em `stores/simulator.ts` (Object.values sem cast) foi corrigido para o `npm run build` passar. Prova: `npm ci` + `npm run build` verdes (vue-tsc + vite build). O `generateInbound` do store agora mostra o status de entrega na cabine e o motivo real quando a entrega falha (502).

## 8. Anexo: caminho real do crédito provado (referência de fidelidade)

1. BACEN entrega STR0008R2 (nós creditados) na fila MQ de resposta.
2. Sidecar/`MQResponsePoller` -> `MQBridge` -> `MQConsumer.process_message` desempacota header 588 e verifica assinatura (`mq_consumer.ex:39-70`).
3. `MessageProcessor.handle_receive_success` publica em `BACEN_INBOUND` e chama `LifecycleEngine`/`CoreNotifier` (`message_processor.ex:711-739`).
4. `LifecycleEngine.dispatch_r2` reconhece crédito receiver-side, materializa `spb_operations` (direction inbound) e enfileira o evento outbox `spb_inbound_credit` na MESMA transação (`lifecycle_engine.ex:1096-1130,1519-1524,1660-1685`).
5. Outbox worker publica em `monetarie.spb.credits.inbound`.
6. Core `SpbConsumer` roteia para `SpbInboundCreditHandler` (`spb_consumer.ex:66-69`), que resolve a conta fail-closed e credita o cliente. Foi por aqui, via replay do outbox, que os R$ 50.000 creditaram (`spb_consumer.ex:11-17`).

O simulador precisava reproduzir os passos 1-2 (STR0008R2 com campos de crédito entregue no fio). **Desde 2026-07-10 ele faz exatamente isso**: gera o STR0008R2 real e o entrega no webhook do sidecar MQ da cabine (o mesmo ponto de entrada do passo 2), deixando os passos 3-6 por conta do pipeline real. O atalho de fabricar o passo 5 (publicar `credit_received` direto) foi deliberadamente NÃO implementado, para eliminar drift de contrato para sempre.

## 9. Mensagens de ENTRADA por família (trilha de 2026-07-10)

Escopo: todas as famílias que a cabine processa de entrada, entregues pela fronteira real (webhook MQ, `Simulator.Delivery.CabinWebhook`). Cada gerador foi modelado pelo PARSER real da cabine para a mensagem, e os testes chamam o `parse/1`/`validate/1` REAIS (a dependência `{:bacen_gateway, path: "../services/bacen_gateway", runtime: false}` no `mix.exs` do simulador é somente-biblioteca; a árvore de supervisão da cabine não sobe).

Arquitetura: `Simulator.Generators.InboundGenerator` despacha por tipo para os geradores de família (contrato `Simulator.Generators.FamilyGenerator`): `StrInboundGenerator`, `LpiGenerator`, `GenInboundGenerator`, `LdlGenerator`, `DdaGenerator`, `PostIntegrationGenerator` (+ helpers comuns em `generators/helpers.ex`: timestamps BACEN sem fração, `NumCtrlSTR` na faixa real, `NUOp` ISPB+data+sequência, `NumCtrlIF` com prefixo `MON`). Catálogo por tipo/família exposto em `GET /api/simulator/inbound_catalog` e consumido pela tela Inbound.

### 9.1 Tabela de cobertura (família, mensagem, perna simulada, consumidor real, status)

| Família | Mensagem | Perna simulada | Consumidor real na cabine (arquivo:linha) | Status |
|---|---|---|---|---|
| **LPI** | LPI0006 | base (única LPI recebida como notificação; espelho da Conta PI, provada real em produção) | `inbound_consumer.ex:360,716` (`:inbound_notification`) -> `LpiHandler.mirror_conta_pi/1` (`specific_handlers/lpi_handler.ex:94`): credita grupo de saldo 43 se `SitLancSTR` 1-3, valor por `TpCtBC` (RL->`VlrRB_CL`, ME->`VlrCCME`), idempotente por `NumCtrlSTR` | SIMULADA |
| LPI | LPI0001R1, LPI0002R1, LPI0003R1, LPI0004R1 | R1 (resposta a envio nosso) | `:r1` -> `run_post_integration` -> `Dispatcher` -> `LpiHandler.handle/4` (`lpi_handler.ex:64-76`) | SIMULADAS |
| LPI | LPI0001R2, LPI0003R2 | R2 (confirmação) | `:r2` -> `LpiHandler.handle/4` | SIMULADAS |
| LPI | LPI0005R1 | R1 da configuração da Conta PI | `:r1` -> `LpiHandler` | SIMULADA |
| LPI | LPI0007R1, LPI0008R1 | R1 (operações do TESOURO; não somos o destinatário natural como PSPI — geradas para exercitar o R1 genérico) | `LpiHandler.handle_r1` (`lpi_handler.ex:71,244`) | SIMULADAS |
| LPI | LPI0001..LPI0005 base | — | envios NOSSOS (`direction_flag='E'`); LPI0007/0008 base = Tesouro. Não são pernas de entrada | NÃO SIMULADAS (não chegam; evidência `lpi_handler.ex:16-42`) |
| **STR** | STR0008R2 / STR0004R2 | R2 de crédito receiver-side (cliente / IF-IF) | `LifecycleEngine.receiver_side_str_credit?` + ponte `credits.inbound` (seções 3-8) | SIMULADAS (P0 de 2026-07-10) |
| STR | STR0010 | base — ordem de devolução real (campos do parser: `NumCtrlIF`, `ISPBIFDebtd/Credtd`, `VlrLanc`, `CodDevTransf`, `NumCtrlSTROr`, `DtMovto`) | catch-all `InboundProcessor.process_base_inbound` (`str/inbound_processor.ex:445-451`, registro) | SIMULADA (antes gerava campos irreais `ISPBOrig/SitLancSTR`) |
| STR | STR0010R1 | R1 da devolução comandada por nós | `StrHandler.handle_devolution_r1` (`str_handler.ex:139-159`) -> `DevolutionEngine.handle_response` (`devolution_engine.ex:138-226`, lê `SitMsg`/`SitLancSTR`) | SIMULADA |
| STR | STR0010R2 | devolução RECEBIDA (outra IF nos devolve TED) | `StrHandler.handle_devolution_r2` (`str_handler.ex:161-176`) + alerta `devolucao_recebida` (`alert_engine.ex:141-199`, lê `NumCtrlSTR/NumCtrlSTROr/CodDevTransf/VlrLanc` do XML — por isso o gerador emite `VlrLanc` mesmo fora do parser do módulo) | SIMULADA |
| STR | STR0015 | aviso de fechamento (clearing STR 'F') | `ClearingLifecycle` `@fecha` (`clearing_lifecycle.ex:61,117-147`) | SIMULADA |
| STR | STR0016 | saldo de fechamento do dia (`TpSld=2`) | `InboundProcessor.process_broadcast` (`inbound_processor.ex:326-336`) -> `DayOpenClose.handle_str0016` (`day_open_close.ex:36-110`); sinal do `DayAutoClose` (`day_auto_close.ex:282-310`) | SIMULADA (CORRIGIDA: o gerador antigo emitia `ISPB/SldDisp/GrpCmpe`, campos que o parser real não lê; agora `ISPBPart/TpSld/SldRB_CL/DtHrBC/DtMovto`) |
| STR | STR0017 | aviso de abertura do dia | `InboundProcessor` (`:338-342`) -> `DayOpenClose.handle_day_open` (`day_open_close.ex:122-162`, abre a data operacional) + `ClearingLifecycle` 'A' | SIMULADA |
| STR | STR0020 | — | NÃO processada de entrada de forma dedicada: é transferência de SAÍDA (repasse de tributos; builder `handlers/str_handler.ex:861-862`); base inbound cairia só no registro catch-all | NÃO SIMULADA (decisão com evidência) |
| **GEN** | GEN0021 | broadcast de grade horária | `:grade` -> `GradeLifecycle.apply` (`grade_lifecycle.ex:43-49,63`) -> `GradeScheduleIngestor` (`grade_schedule_ingestor.ex:77-247`): `TpHrio=P` -> `bacen_operating_schedule`; outro -> `grade_schedule_overrides`; pode RETROAGIR D+1 | SIMULADA (suporta N grades via `:grades`) |
| GEN | GEN0015 | aviso de arquivo disponível (direto) — evento TIPO_E=A no legado | `:file_notice` (`inbound_consumer.ex:343,541-553`) -> `route_file_notice` (`:686-710`) -> `FileTransfer.Orchestrator.handle_file_available` (exige `NomArq`) | SIMULADA |
| GEN | GEN0017 | aviso de arquivo via prestador | `:file_notice` -> `handle_file_available_via_provider` (`inbound_consumer.ex:704`) | SIMULADA |
| GEN | GEN0018 | aviso de data de ativação de certificado | `:cert_activation` (`inbound_consumer.ex:350,555-565`): apenas REGISTRADO (não muta certificados) | SIMULADA (`CodCertifrAtv=6` VALID, serial 32 hex) |
| GEN | GEN0014 / GEN0016 | — | REQUISIÇÕES de arquivo enviadas POR NÓS (outbound, `coa_cod_handler.ex:67-128`); não são pernas de entrada. A reclassificação GEN0014-18 = transferência de arquivo/cert (não abertura de dia) está em `coa_cod_handler.ex:15-37` | NÃO SIMULADAS (não chegam) |
| GEN | GEN0001 | teste de conectividade (gerador legado mantido) | `mq/gen0001_test.ex` (conectividade MQ) | SIMULADA (legado) |
| **LDL** | LDL0023R1 | broadcast de grade (interceptada como `:grade` ANTES do teste R1) | `GradeLifecycle` (`grade_lifecycle.ex:47`) -> `GradeScheduleIngestor` (grupo `Grupo_LDL0023R1_HrioCamr`) | SIMULADA |
| LDL | LDL0024 | broadcast de grade de liquidação diferida | idem, grupo `Grupo_LDL0024_HrioCamr` | SIMULADA |
| LDL | LDL0028 / LDL0029 | abertura/fechamento da câmara LDL | `ClearingLifecycle` `@abre`/`@fecha` (`clearing_lifecycle.ex:60-61`, lê `ISPBLDL`) -> `bacen_clearings_mqs.st_funcionamento` | SIMULADAS |
| LDL | LDL0030 / LDL0031 | — | mesmo caminho clearing dos 0028/0029 | NÃO SIMULADAS (cobertura representativa) |
| LDL | LDL0005R2 / LDL0009R2 | — | reconciliação R2 correlata a envio nosso (`ldl_handler.ex:42,119-146`, `Posting.post` grupo 7) | NÃO SIMULADAS (exigem operação enviada correlata; pendência) |
| **DDA** | DDA0400 | abertura de dia DDA | `DdaHandler.handle_inbound_notification` (`dda_handler.ex:74-79`): lê `DtHrAbert` e aplica `SystemDate.open`; não toca saldo | SIMULADA |
| DDA | DDA0110 | aviso a terceiros (representativo) | `dda_handler.ex:80-83` (apenas reconhecido; persistência via `record_received_bacen_message`) — MESMO caminho de DDA0101/0102/0104/0108/0115/0116/0121/0122/0127 e grades DDA0402-0404 | SIMULADA (representativa) |
| **BMC** | BMC0205R1 | R1 real do catálogo (operação interbancária de câmbio) | `BmcHandler` `{_, "R1"}` -> `handle_r1` lê `SitLancBMC` (`bmc_handler.ex:146`) — a tag vive DENTRO de `Grupo_BMC0205R1_OpInterbanc` e o handler a lê via regex no XML cru | SIMULADA |
| **CTP** | CTP0001R1 | R1 (SitOpCTP no topo) | `CtpHandler` lê `SitLancCTP`/`SitOpCTP` (`ctp_handler.ex:158-163`) | SIMULADA |
| **CCR** | CCR0001R1 | R1 (SitOpCCR dentro de `Grupo_CCR0001R1_OpComercExtr`; namespace espelho MES) | `CcrHandler` lê `SitOpCCR` (`ccr_handler.ex:135-137`) via regex no XML cru | SIMULADA |
| **SLB** | SLB0002R1 | R1 (SitLancSLB no topo) | `SlbHandler` lê `SitLancSLB` (`slb_handler.ex:170-173`) | SIMULADA |
| **CAM** | CAM0059R1 | R1 (SitMigr no topo; namespace próprio MES) | `CamHandler` lê `SitMigr` (`cam_handler.ex:146-152`); validate real exige `SitMsg` ACCP/RJCT | SIMULADA |
| **PAG** | PAG0101 | base (situação de operação de IF na câmara) | `PagHandler` `{"PAG0101", _}` lê `ISPBIF`/`SitOperacIFCamr` do grupo (`pag_handler.ex:62-63`) | SIMULADA |

**Veredito LPI (pedido explícito do dono): COMPLETO.** Todas as pernas LPI que a cabine parseia como entrada são geradas: a base LPI0006 (a única LPI recebida como notificação não-solicitada, com os campos exatos que o `mirror_conta_pi` lê, inclusive a semântica RL/ME do `TpCtBC` e o `SitLancSTR` numérico do `StatusDePara`) e as 9 pernas R (LPI0001R1/R2, LPI0002R1, LPI0003R1/R2, LPI0004R1, LPI0005R1, LPI0007R1, LPI0008R1), cada uma com a tag de controle certa (`NumCtrlIF`/`NumCtrlIEME`/`NumCtrlPSPI`/`NumCtrlTES`) e namespace do XSD pai. As bases LPI0001..LPI0005 (envios nossos) e LPI0007/LPI0008 (Tesouro) não chegam para nós e por isso não têm gerador de entrada.

### 9.2 Sobre os "avisos TIPO_E=A"

`TIPO_E` não é campo XML: é a classificação do legado AB_MGI que no código novo corresponde a `message_type_config.grid_type='A'` (Alertas/Avisos, `spb/message_grid.ex:9,28-33`). Avisos são mensagens normais recebidas e PERSISTIDAS, nunca auto-respondidas (Liquidante desligado por default; gates em `message_processor.ex:744-753` e `inbound_consumer.ex:497-499`). A cobertura de avisos desta trilha = GEN0015/GEN0017/GEN0018 + STR0015/STR0016/STR0017 + DDA0110 (e o LPI0006, que também é notificação não-solicitada).

### 9.3 Cenários multi-passo iniciados por entrada

O `ScenarioRunner` agora entrega passos inbound em QUALQUER posição do template (flag `deliver: true` + `gen_opts` por passo; o primeiro passo inbound mantém a entrega automática por compatibilidade). Templates novos em `scenario_templates.ex`:

- `lpi0006_conta_pi` — LPI0006 avulsa confirmada (espelho da Conta PI).
- `full_day_cycle` — dia completo pela fronteira real: STR0017 (abre) -> STR0008R2 (TED-in, R$ 1.500,00) -> LPI0006 (espelho Conta PI) -> STR0016 `TpSld=2` (fecha). Provado por teste com servidor webhook local: as 4 mensagens chegam NA ORDEM, com os `gen_opts` fluindo para o XML.
- `grade_broadcast` — GEN0021 (grade efetiva) -> LDL0024.
- `devolucao_recebida` — STR0010R2 avulsa (alerta `devolucao_recebida`).

### 9.4 Tela Inbound (simulator-frontend)

Refeita para o catálogo dinâmico: `GET /api/simulator/inbound_catalog` (tipo, família, nome, campos por tipo), seletor agrupado por família (LPI, STR, GEN, LDL, DDA, BMC, CTP, CCR, SLB, CAM, PAG), formulário com os campos certos do tipo selecionado (limpa opções alheias ao trocar de tipo), badge de status de entrega real (HTTP da cabine ou o motivo da falha) e fallback estático quando o backend está fora. `npm run build` (vue-tsc + vite) verde.

### 9.5 Provas

- Suíte completa do simulador: `MIX_ENV=test mix test` -> **96 testes, 0 falhas** (inclui os 43 novos desta trilha: 35 de geradores por família + 4 de catálogo + 4 de cenários multi-passo, além dos pré-existentes e dos das outras trilhas do dia). Os testes de gerador chamam `parse/1` e, onde o módulo define obrigatórios, `validate/1` DOS MÓDULOS REAIS da cabine (ex.: `BacenGateway.Messages.LPI.LPI0006.validate/1` retorna `{:ok, _}` sobre o XML gerado).
- `xmllint --noout` sobre 1 exemplar de cada um dos **34 tipos** gerados: **34/34 bem formados** (0 falhas).
- Entrega real provada por teste com servidor HTTP local simulando o webhook da cabine (ordem, corpo e `gen_opts` verificados).

### 9.6 Pendências honestas da trilha de entradas

1. Variantes de erro `*E` (LPI0001E, STR0004E...) não são geradas por esta trilha: são RESPOSTAS de erro a envios nossos, território do `ResponseGenerator` (trilha do catálogo de respostas). A classificação `:error` da cabine (`inbound_consumer.ex:325,582-604`) segue sem gerador avulso.
2. LDL0005R2/LDL0009R2 (reconciliação com `Posting.post`) e LDL0030/LDL0031 não geradas — a reconciliação exige operação enviada correlata; os clearing 0030/0031 usam o mesmo caminho dos 0028/0029 simulados.
3. GEN0007 (distribuição de certificado, `:cert_update`) não gerada: muta o registro de certificados da cabine; fora do escopo desta trilha.
4. Valores de domínio que a cabine NÃO interpreta no caminho vivo (`TpTransm`, `CodProdt`, `CodDevTransf`, `SitOperacIFCamr`) usam placeholders plausíveis e são todos configuráveis por opção; não há validação de domínio desses campos no pipeline inbound da cabine.
5. Correlação com operação real enviada exige passar `:nu_op` e o número de controle do tipo (`:num_ctrl_if`/`:num_ctrl_pspi`/...) — os defaults são sintéticos (prefixo `MON`, faixa real).
6. DDA canonicamente trafegaria por SPB02 (catálogo legado `dom_spb=SPB02`), mas o runtime atual da cabine não provisiona SPB02 por default; a entrega default é no webhook `SPB01` (configurável via `:domain`).
7. A entrega default de TODAS as famílias é no domínio `SPB01` — o único polled em produção (`DOMAINS_TO_POLL=SPB01`); o webhook da cabine aceita qualquer `:domain` no path.

## 10. Respostas do BACEN para TODO o catálogo de envio (trilha respostas, 2026-07-10)

O `ResponseGenerator` do simulador foi reescrito para ser CATALOG-DRIVEN, reusando o catálogo REAL da cabine em vez de respostas escritas à mão.

### 10.1 Decisão de arquitetura: cabine como dependência por path

`spb/simulator/mix.exs` ganhou `{:bacen_gateway, path: "../services/bacen_gateway", runtime: false}`. A dependência é VIÁVEL e está funcionando: compila (deps resolvidas; o mismatch pré-existente do lock em `phoenix_template 1.0.4` foi corrigido regenerando a entrada do lock), e a árvore de supervisão da cabine NÃO sobe junto (`runtime: false` — só os módulos ficam disponíveis). A cabine não depende do simulador; não há ciclo. O acesso é centralizado em `Simulator.Core.CabinCatalog` (`spb/simulator/lib/simulator/core/cabin_catalog.ex`):

- `BacenGateway.Messages.Registry` para resolver módulo-base e módulos R1/R2/R3;
- `response_types/0` + existência do módulo R gerado = quais respostas o fluxo real tem. Achado provado em 2026-07-10: 246 bases do catálogo de envio têm módulo R gerado do XSD sem o leg declarado em `response_types/0` (o inverso é zero) — o conjunto esperado é a UNIÃO das duas evidências, ambas nascidas dos XSDs;
- `module.namespace/0` do PAI = namespace de toda resposta (R é `xs:choice` do XSD pai);
- `BacenGateway.MessagePipeline.resolve_domain/1` = DomSist por família quando o pedido não traz `DomSist` (eco tem precedência);
- `priv/repo/seeds/data/legacy_tag_definitions.csv` (cabine) = layout REAL de cada leg (707 legs R: sequência, obrigatoriedade, ou-exclusivo, grupos aninhados `Grupo_*`/`/Grupo_*`);
- `priv/repo/seeds/bacen_reference_data/group_status_map.csv.gz` (cabine) = códigos de situação REAIS por grupo (spb_tb_gen_status_de_para, 26 grupos/495 regras): sucesso = menor código confirmado (STR=1, PAG=COM, SEL=ATU, DDA=2...), rejeição = menor finalizador não confirmado com status interno 9 (STR=5, PAG=REJ, SEL=RIT...).

### 10.2 Comportamento novo (regras)

1. Mensagem enviada recebe COA + COD + APENAS os legs R1/R2/R3 que o catálogo real define. Mensagem sem resposta no fluxo real (155 do catálogo de envio, ex.: BMC0004) recebe só COA/COD — nenhuma resposta inventada.
2. O corpo de cada leg segue o layout do acervo; os campos de identidade da operação (NumCtrlIF, ISPBs, contas, clientes, valores, DtMovto) são ECOADOS da mensagem original (`raw_xml` fluindo pelo `MessageRouter`), como o BACEN real faz.
3. Modos de cenário preservados e aplicáveis a qualquer tipo: success (legs do catálogo), r1_reject (código de situação de rejeição REAL do grupo; tipos sem R1 caem num corpo genérico RJCT documentado), timeout (COA+COD+R1 atrasado 30s; sem R1 no catálogo, sem R1), partial (só COA), r1_error (GEN9901).
4. `MQSimulator` nas filas reais `QR.REQ.46026562.00038166.01` / `QL.RSP.00038166.46026562.01` (P2-4 fechado).

### 10.3 Matriz de cobertura (medida por teste, 2026-07-10)

Catálogo de envio real (mesmo recorte do `send_catalog` da cabine: módulos-base com `build/1`, sem R/E): **587 mensagens, 33 grupos**.

| Métrica | Valor |
|---|---|
| Mensagens com R1 automático | 432 |
| Mensagens com R2 automático | 116 |
| Mensagens com R3 automático | 9 |
| Mensagens sem resposta no fluxo real (só COA/COD, nada inventado) | 155 |
| Legs de resposta totais | 557 |
| Legs com layout REAL do acervo | 532 |
| Legs no corpo genérico de protocolo (fallback documentado) | 25 |

Amostra paramétrica (1 mensagem por grupo, os 33 grupos, incluindo STR, LPI, LDL, GEN, PAG, CAM, SEL, CTP e DDA): o envio é construído pelo BUILDER REAL da cabine (dados preenchidos por um laço guiado pelos erros do `validate/1` real — `test/support/send_data_factory.ex`; 558/587 builders convergem com preenchimento genérico e TODOS os 33 grupos têm ao menos uma mensagem convergente), passa pelo `MessageRouter` do simulador e cada resposta é validada com `xmllint` (bem formado) e com o PARSER REAL da cabine (`parse/1` do módulo R correspondente: `cod_msg` correto, `NUOp` ecoado, `DomSist` ecoado). Resultado da execução: 33/33 grupos, 22 amostras com R-leg validado pelo parser real e 11 amostras de mensagens que no fluxo real não têm resposta (só COA/COD):

```
APT APT0001 -> R1          BMC BMC0004 -> so COA/COD   CAM CAM0005 -> R1+R2+R3
CCR CCR0001 -> R1          CCS CCS0001 -> so COA/COD   CDE CDE0001 -> R1
CIR CIR0003 -> R1          CMP CMP0001 -> R1           COR COR0001 -> R1
CQL CQL0001 -> so COA/COD  CSD CSD1001 -> so COA/COD   CTP CTP0001 -> R1
DDA DDA0001 -> R1          DPO DPO0001 -> R1           ECR ECR0001 -> R1
GEN GEN0001 -> R1          LDL LDL0001 -> so COA/COD   LEI LEI0001 -> so COA/COD
LFL LFL0001 -> R1          LPI LPI0001 -> R1+R2        LTR LTR0001 -> so COA/COD
PAG PAG0101 -> so COA/COD  PTX PTX0001 -> so COA/COD   RCO RCO0001 -> R1
RDC RDC0001 -> R1          SEL SEL1003 -> R1           SLB SLB0001 -> so COA/COD
SLC SLC0001 -> so COA/COD  SME SME0001 -> R1+R2        SML SML0002 -> R1
SRC SRC0001 -> R1          STR STR0001 -> R1           TES TES0001 -> R1+R2
```

Nota sobre "só COA/COD": vale para a MENSAGEM AMOSTRADA do grupo, não para o grupo inteiro (ex.: LDL0001 não tem leg R no catálogo, mas dezenas de outras LDL têm R1). Os totais por leg estão na tabela acima.

### 10.4 Testes (trilha respostas)

- `test/simulator/core/catalog_response_coverage_test.exs`: os 2 testes acima (matriz do catálogo inteiro + varredura paramétrica dos 33 grupos com builder real -> simulador -> parser real + xmllint).
- `test/simulator/core/response_generator_contract_test.exs` (10): namespace do pai em todo leg, layout real do R1 (sem SitMsg, com SitLancSTR/DtHrSit), códigos do `group_status_map` (sucesso e rejeição), DomSist por `resolve_domain` na ausência de eco, nenhum leg inventado, número de controle único por operação, eco de campos, timeout, COA/COD sem fração de segundo.
- `test/simulator/core/message_catalog_contract_test.exs`: contrato reescrito — para TODAS as 587 mensagens do catálogo de envio, os legs gerados são EXATAMENTE os do catálogo real (o teste antigo exigia R1 para tudo, o que era a infidelidade que esta trilha eliminou).
- `test/simulator/core/mq_simulator_test.exs` (2): nomes de fila reais + roundtrip envio->resposta pela fila.

Execução em 2026-07-10 (suíte completa do simulador, incluindo os testes das outras trilhas presentes no worktree): `98 tests, 0 failures`. Os testes desta trilha isolados: `29 tests, 0 failures` (3 execuções consecutivas).

