# STA Conformance Rollout — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task.
> Onda 1 is bite-sized TDD tasks executable as-is. Ondas 2-4 are sub-PR briefs that will be expanded into their own bite-sized plan files (`docs/plans/2026-XX-XX-sta-onda{N}-{slug}.md`) when each sub-PR starts.

**Goal:** Bring `sta/*` to full conformance with the BCB Manual de Utilização dos Web Services do STA v1.5 (jul/2022), unblocking BCB homologation (G4 — STA upload + GEN0006 ACCP), enabling daily CCS / SCR / SISBAJUD regulatory deliveries with proper resilience, rate-limiting, and observability.

**Architecture:** 4 sequential ondas, 13 PRs total. Onda 1 is a single fast PR (`sta-paridade` → `main`) that fixes the 5 P0 wire-format bugs in `client.ex` and installs the ICP-Brasil chain so that *anything* works against `sta-h.bcb.gov.br`. Ondas 2-4 are sub-branches off `main`, each shipped as an independent PR (item-as-PR or grouped-by-theme), so the program never accumulates one giant integration branch. SOAP path and Go residue are deleted outright (REST is the only canonical interface since Manual v1.5).

**Tech Stack:** Elixir 1.17 / OTP 27 / Phoenix 1.8.3 (`:sta_connector` app), Finch HTTP client, Bandit adapter, NATS JetStream (gnat ~> 1.8) on subjects `monetarie.sta.*`, PostgreSQL 16 (Aurora Serverless v2 cluster `monetarie-sta-homolog`), Bypass for unit tests, real BCB integration opt-in via `STA_HOMOL_USER` / `STA_HOMOL_PASSWORD` env vars. XSD validation via `:xmerl_xsd`. ICP-Brasil chain reused from `spb/services/bacen_gateway/priv/certs/icp_brasil_ca_chain.pem` (vetted in SPB Wave 1, valid against `bacen_*.pem` with `openssl verify`).

---

## Index

1. [Decisions log](#decisions-log) — what was settled in brainstorm
2. [Pre-flight](#pre-flight) — worktree state, dependencies, secrets
3. [Onda 1 — P0 wire-format fixes + ICP-Brasil](#onda-1) — *bite-sized TDD tasks; Tasks 1-3 shipped, Tasks 4-5 cut, Task 6 revised (Brainstorm 2)*
4. [Pattern: Manual Verification (Ondas 2-4)](#pattern-manual-verification) — *Task 0 mandatory per sub-PR + claim inventory*
5. [Onda 2 — Operations & lifecycle](#onda-2) — *4 sub-PR briefs*
6. [Onda 3 — Parsers & RSFN leiautes](#onda-3) — *4 sub-PR briefs*
7. [Onda 4 — Cuts, observability, governance](#onda-4) — *4 sub-PR briefs*
8. [Risk register](#risks)
9. [Rollback strategy](#rollback)
10. [References](#references)

---

## Decisions log

Settled during brainstorm 2026-04-26 (sessão `sta-paridade`):

| Decision | Choice | Rationale |
|---|---|---|
| **Scope** | C — full (Ondas 1-4) | Hard gate G4 makes STA critical-path; partial fix leaves regulatory exposure |
| **Cadence** | C — hybrid (Onda 1 mono PR, Ondas 2-4 sub-PR by theme) | Balances fast unblock (G4) with reviewable PR sizes |
| **Cuts** | B — aggressive | Study itself flagged SIMBA/DIMP/E-Fin as wrong; SOAP/Go are dead code; deferring "delete" creates rot |
| **ACCS010** | full SEND + RECEIVE | Required by Resolução BCB 119 transferência de relacionamentos flow |
| **Tests** | C — Bypass + real BCB opt-in | CI fast (Bypass), correctness proven on demand against `sta-h.bcb.gov.br` |
| **BCB creds** | Assume (ii) — solicitable | Plan ships independent of cred availability; smoke task waits |
| **XML signing** | Stub interface (`StaConnector.Crypto.XmlSigner`) | Real impl needs Compliance/Jurídica confirm on A1-A3 procurador BCB cert |

### Brainstorm 2 — post Onda 1 partial (2026-04-26 EOS)

After Tasks 1-3 shipped (commits `ff0ef70c`, `d310d82b`+`7f319b7d`, `029b6369`+`e5e88a7f`), two Manual misdiagnoses surfaced (B4 Destinatarios block + Content-Type citation), prompting a second-pass review of Tasks 4-6 and Ondas 2-4 study claims. Outcomes:

| Decision | Choice | Rationale |
|---|---|---|
| **Task 4** (FileInfo type scaffolding) | **CUT** | (a) No parallel consumer between Onda 1 merge and Onda 2.A start (sequential session); (b) field-name risk under misdiagnosis pattern; (c) audit found `expires_at` already exists on `Types.ProtocolRequest` (`sta/backend/lib/sta_connector/sta/types.ex:173`, protocol-level per Manual §5 — 48h) and on `OutboundFile` schema (`sta/backend/lib/sta_connector/files/outbound_file.ex:108`, DB-level) — original plan would have introduced a third, file-level instance. Onda 2.A adds fields when wire-format semantics are confirmed. |
| **Task 5** (smoke-test scaffold) | **CUT** | (a) Creds (G4) not yet available; smoke would never run. (b) `system_id: "TESTE"` was study-conjecture — Manual likely requires registered system_id (e.g., `PCCS001`). (c) request_protocol-only smoke doesn't exercise the B1 fix end-to-end. Bypass coverage from Tasks 2-3 remains the wire-format safety net. Smoke moves to whichever Onda 2 sub-PR converges with cred availability and verified system_id. |
| **B4 Destinatarios** misdiagnosis | acknowledged + corrected (`7f319b7d`) | Study said "BACEN literal required". Manual STA v1.5 §5.1.2: block is **Opcional** with schema `<Unidade>/<Dependencia>/<Operador>`. Final impl: caller-supplied via optional `:destinatarios` map; default emits no block. |
| **Content-Type** citation misdiagnosis | acknowledged + corrected (`e5e88a7f`) | Original Task 3 commit cited an invented Manual section for `Content-Type: application/octet-stream`. Corrected to actual §5.2.1 wording. |
| **`expires_at` duplication** in plan | logged | Plan's Task 4 was going to add `expires_at` to `FileInfo` while it already lives on `ProtocolRequest` + `OutboundFile`. No code action; lesson: audit existing struct shape on disk before drafting scaffold tasks. |
| **Onda 2-4 study trust** | "Manual Verification Task 0" required per sub-PR | See section "Pattern: study claims requiring Manual verification" before Onda 2. Each sub-PR's bite-sized plan must include a Task 0 that reads `sta/Manual_STA_Web_Services.pdf` and tags every wire-format / endpoint / response-code / numeric-threshold claim as `[VERIFIED §X.Y]`, `[STUDY-CONJECTURE]`, or `[NEEDS-OPERATOR-CONFIRM]` BEFORE writing tests. Mirrors SPB Wave 9 "decompile-first" lesson. |

---

## Pre-flight

### Worktree

- Branch: `sta-paridade` rastreando `origin/main` (HEAD `e75e8e76` no momento do plan)
- Path: `/Users/luizpenha/monetarie/.worktrees/sta-paridade`
- Stays open as the integration branch for Onda 1 only; Onda 1 PR merges this branch to `main`. Ondas 2-4 sub-branches are cut from `main` (post-Onda-1), not from `sta-paridade`.

### Dependencies on other code

- **ICP-Brasil chain:** `spb/services/bacen_gateway/priv/certs/icp_brasil_ca_chain.pem` (3 certs, 7.7KB, vetted Wave 1). Onda 1 copies it into `sta/backend/priv/certs/`. Same SERPRO public source.
- **Reference manual in repo:** `sta/Manual_STA_Web_Services.pdf` (already committed). `md/Manual de Segurança do SFN-v5_07.pdf` may also apply for crypto sections.
- **Monetarie.Config helper:** institution_ispb/0 (canonical, per Regra Absoluta #5) — STA already uses env-driven config; no changes needed in Onda 1.

### Secrets

- BCB STA homol creds: AWS Secrets Manager `monetarie/homolog/sta/bcb_credentials` (created by Onda 4.C; for Onda 1 read from env vars `STA_BCB_USERNAME`/`STA_BCB_PASSWORD` already wired).
- Real-BCB smoke test gating env vars: `STA_HOMOL_USER` / `STA_HOMOL_PASSWORD` (operator's local env, never CI).

### Sanity check before starting

```bash
cd /Users/luizpenha/monetarie/.worktrees/sta-paridade/sta/backend
mix deps.get
mix compile --warnings-as-errors
mix test --include integration:false
```

Expect: clean compile, current test suite green (no changes yet). If compile fails before any change, stop and investigate — main is supposed to be clean.

---

<a id="onda-1"></a>
## Onda 1 — P0 wire-format fixes + ICP-Brasil

**Branch:** `sta-paridade` (this worktree). **PR target:** `main`. **Estimated effort:** 3-5 working days. **LOC delta estimate:** +600.

**Acceptance criteria (post Brainstorm 2):**
- All 5 P0 bugs identified in the conformance study fixed with TDD coverage (unit tests via Bypass)
- ICP-Brasil CA chain installed in sta backend; Finch pool with `verify: :verify_peer`; fails closed if chain missing
- `mix test` green; `mix compile --warnings-as-errors` clean; `mix format --check-formatted` clean
- PR description references study + plan + per-commit bug mapping; explicit list of misdiagnoses caught; explicit deferral note for Ondas 2-4 + smoke test
- No file outside `sta/*` modified (`git diff main..HEAD --name-only | grep -v "^sta/"` returns empty)

**Status (2026-04-26 EOS):**
- Tasks 1-3 SHIPPED (commits `ff0ef70c`, `d310d82b`+`7f319b7d`, `029b6369`+`e5e88a7f`)
- Tasks 4-5 **CUT** (see Decisions log → Brainstorm 2)
- Task 6 **REVISED** to a 9-item closeout — see below

---

### Task 1: Install ICP-Brasil CA chain in sta backend

**Files:**
- Create: `sta/backend/priv/certs/icp_brasil_ca_chain.pem` (copy from `spb/services/bacen_gateway/priv/certs/icp_brasil_ca_chain.pem`)
- Create: `sta/backend/priv/certs/.gitignore` if not present (whitelist the chain file)
- Modify: `sta/backend/lib/sta_connector/application.ex` — Finch pool `transport_opts`
- Modify: `sta/backend/config/runtime.exs` — read chain path from env with sensible default

**Step 1: Copy the chain**

```bash
cp /Users/luizpenha/monetarie/.worktrees/sta-paridade/spb/services/bacen_gateway/priv/certs/icp_brasil_ca_chain.pem \
   /Users/luizpenha/monetarie/.worktrees/sta-paridade/sta/backend/priv/certs/icp_brasil_ca_chain.pem
ls -lh sta/backend/priv/certs/
```

Expected: file present, ~7.7KB.

**Step 2: Verify chain validity (sanity)**

```bash
cd sta/backend
openssl crl2pkcs7 -nocrl -certfile priv/certs/icp_brasil_ca_chain.pem | openssl pkcs7 -print_certs -noout | head -20
```

Expected: 3 cert subjects printed (Autoridade Certificadora Raiz Brasileira + 2 intermediárias).

**Step 3: Add Finch pool config in `application.ex`**

Locate the `Finch.start_link` call (or wherever the Finch child is configured). Replace its `pools` arg with:

```elixir
{Finch,
 name: StaConnector.Finch,
 pools: %{
   "https://sta-h.bcb.gov.br" => finch_pool_opts(),
   "https://sta.bcb.gov.br" => finch_pool_opts(),
   "https://www9.bcb.gov.br" => finch_pool_opts(),
   "https://www3.bcb.gov.br" => finch_pool_opts(),
   :default => [size: 10]
 }}
```

And add a private helper at the bottom of the module:

```elixir
defp finch_pool_opts do
  cacertfile = Application.fetch_env!(:sta_connector, :icp_brasil_chain_path)
  unless File.exists?(cacertfile) do
    raise "ICP-Brasil chain not found at #{cacertfile} — refusing to start (fail-CLOSED per SPB Wave 1 precedent)"
  end
  [
    size: 10,
    conn_opts: [
      transport_opts: [
        verify: :verify_peer,
        cacertfile: cacertfile,
        depth: 5,
        customize_hostname_check: [
          match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
        ]
      ]
    ]
  ]
end
```

**Step 4: Wire env in `runtime.exs`**

Add to `runtime.exs`:

```elixir
config :sta_connector,
  icp_brasil_chain_path:
    System.get_env("ICP_BRASIL_CHAIN_PATH") ||
      Path.join(:code.priv_dir(:sta_connector), "certs/icp_brasil_ca_chain.pem")
```

**Step 5: Test that app refuses to boot without chain**

Add to `test/sta_connector/application_test.exs` (create if needed):

```elixir
defmodule StaConnector.ApplicationTest do
  use ExUnit.Case, async: false

  test "refuses to start when ICP-Brasil chain missing (fail-CLOSED)" do
    Application.put_env(:sta_connector, :icp_brasil_chain_path, "/nonexistent/path.pem")
    on_exit(fn ->
      Application.put_env(
        :sta_connector,
        :icp_brasil_chain_path,
        Path.join(:code.priv_dir(:sta_connector), "certs/icp_brasil_ca_chain.pem")
      )
    end)

    assert_raise RuntimeError, ~r/ICP-Brasil chain not found/, fn ->
      StaConnector.Application.start(:normal, [])
    end
  end
end
```

Run: `mix test test/sta_connector/application_test.exs -v`
Expected: PASS.

**Step 6: Commit**

```bash
git add sta/backend/priv/certs/ sta/backend/lib/sta_connector/application.ex \
        sta/backend/config/runtime.exs sta/backend/test/sta_connector/application_test.exs
git commit -m "feat(sta): install ICP-Brasil chain + fail-CLOSED Finch TLS

Mirrors SPB Wave 1 precedent: TLS handshake against sta-h.bcb.gov.br must
verify against ICP-Brasil chain or app refuses to boot. Chain is the same
SERPRO-published bundle vetted in SPB."
```

---

### Task 2: B2 + B3 — fix hash and size in `request_protocol`

**Bug:** `client.ex:510-523` computes `hash = :crypto.hash(:sha256, file_name)` (should be over content) and emits `<Tamanho>0</Tamanho>` (should be `byte_size(content)`). The `:request_protocol` build clause doesn't even receive `content` in its params map — fixing this requires changing the function signature *and* the upstream caller.

**Files:**
- Modify: `sta/backend/lib/sta_connector/sta/client.ex:510-523`
- Modify: `sta/backend/lib/sta_connector/sta/operations.ex` (find `request_protocol` caller; pass `content`)
- Test: `sta/backend/test/sta_connector/sta/client_test.exs` (create if not present)

**Step 1: Write the failing test (Bypass mock)**

In `test/sta_connector/sta/client_test.exs`:

```elixir
defmodule StaConnector.Sta.ClientTest do
  use ExUnit.Case, async: false
  alias StaConnector.Sta.Client

  setup do
    bypass = Bypass.open()
    Application.put_env(:sta_connector, :sta_base_url_override, "http://localhost:#{bypass.port}")
    {:ok, bypass: bypass}
  end

  describe "request_protocol/3 wire format" do
    test "computes SHA-256 over content (not file_name) and sends real byte size", %{bypass: bypass} do
      content = "PAYLOAD-A" <> :binary.copy(<<0>>, 1023)  # 1032 bytes
      expected_hash =
        :crypto.hash(:sha256, content)
        |> Base.encode16(case: :lower)

      Bypass.expect_once(bypass, "POST", "/arquivos", fn conn ->
        {:ok, body, conn} = Plug.Conn.read_body(conn)
        assert body =~ "<Hash>#{expected_hash}</Hash>"
        assert body =~ "<Tamanho>1032</Tamanho>"
        refute body =~ "<Tamanho>0</Tamanho>"
        Plug.Conn.resp(conn, 201, "<Resposta><Protocolo>1234567</Protocolo></Resposta>")
      end)

      assert {:ok, "1234567"} =
               Client.request_protocol(%{
                 system_id: "PCCS001",
                 file_name: "ACCS001_20260426.xml",
                 content: content
               })
    end
  end
end
```

**Step 2: Run test to verify it fails**

```bash
cd sta/backend
mix test test/sta_connector/sta/client_test.exs:15 -v
```

Expected: FAIL — either `(FunctionClauseError) no function clause matching` (because old signature doesn't accept `content`), or assertion failure on hash/size.

**Step 3: Fix `client.ex` — `:request_protocol` build clause**

Replace `client.ex:510-523` with:

```elixir
defp build_rest_request(:request_protocol, %{
       system_id: system_id,
       file_name: file_name,
       content: content
     }) do
  hash = :crypto.hash(:sha256, content) |> Base.encode16(case: :lower)
  size = byte_size(content)

  xml = """
  <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <Parametros>
    <IdentificadorDocumento>#{system_id}</IdentificadorDocumento>
    <Hash>#{hash}</Hash>
    <Tamanho>#{size}</Tamanho>
    <NomeArquivo>#{file_name}</NomeArquivo>
    <Destinatarios>
      <Destinatario>
        <Tipo>BACEN</Tipo>
      </Destinatario>
    </Destinatarios>
  </Parametros>
  """

  {:post, "/arquivos", xml, [{"Content-Type", "application/xml"}]}
end
```

(Note: this also closes B4 — `<Destinatarios>` block. We're combining B2/B3/B4 in one task because they all live in the same function; testing them separately would require 3 nearly-identical Bypass setups.)

**Step 4: Update caller in `operations.ex`**

Find any function that builds the `:request_protocol` params map. Currently passes only `system_id` + `file_name`. Add `content: content` to that map. The caller likely already has `content` available (it's about to upload it). Locate via:

```bash
grep -n "request_protocol" sta/backend/lib/sta_connector/sta/operations.ex
```

If `operations.upload_file/3` is the caller, ensure it sequences as: compute hash/size *implicitly* via Client (now that Client owns the calc), call `request_protocol(%{system_id, file_name, content})`, then `upload_file` with the returned protocol (Task 4 below).

**Step 5: Run test, expect pass**

```bash
mix test test/sta_connector/sta/client_test.exs:15 -v
```

Expected: PASS.

**Step 6: Add B4 assertion to the same test**

Extend the Bypass assertion in Step 1's test:

```elixir
assert body =~ "<Destinatarios>"
assert body =~ "<Tipo>BACEN</Tipo>"
```

Re-run: PASS.

**Step 7: Commit**

```bash
git add sta/backend/lib/sta_connector/sta/client.ex \
        sta/backend/lib/sta_connector/sta/operations.ex \
        sta/backend/test/sta_connector/sta/client_test.exs
git commit -m "fix(sta): compute SHA-256 over content + real Tamanho + Destinatarios block

P0 bugs B2/B3/B4 from conformance study 2026-04-26:
- B2: request_protocol now accepts content and hashes the actual bytes
  (previously hashed file_name — BCB rejected 100% of uploads)
- B3: <Tamanho> reflects byte_size(content) (previously hardcoded 0)
- B4: <Destinatarios> block emitted with default Tipo=BACEN
  (previously absent; manual requires it)

Bypass-tested. Real BCB smoke test gated by STA_HOMOL_USER env var (Task 6)."
```

---

### Task 3: B1 — fix upload path to include the protocol number

**Bug:** `client.ex:540-544` builds `{:put, "/arquivos/0/conteudo", content, []}` — protocol literal `0` regardless of what `request_protocol` returned. Manual requires `/arquivos/{protocolo}/conteudo`.

**Files:**
- Modify: `sta/backend/lib/sta_connector/sta/client.ex:540-544`
- Modify: `sta/backend/lib/sta_connector/sta/operations.ex` (caller of upload_file)
- Test: `sta/backend/test/sta_connector/sta/client_test.exs` (extend)

**Step 1: Write the failing test**

Add to `client_test.exs`:

```elixir
describe "upload_file/3 wire format" do
  test "PUTs to /arquivos/{protocol}/conteudo (not literal 0)", %{bypass: bypass} do
    Bypass.expect_once(bypass, "PUT", "/arquivos/9876543/conteudo", fn conn ->
      {:ok, body, conn} = Plug.Conn.read_body(conn)
      assert body == "FILE-BYTES"
      Plug.Conn.resp(conn, 204, "")
    end)

    assert {:ok, _} =
             Client.upload_file(%{
               protocol_number: "9876543",
               content: "FILE-BYTES"
             })
  end

  test "rejects upload without protocol_number" do
    assert_raise FunctionClauseError, fn ->
      Client.upload_file(%{content: "FILE-BYTES"})
    end
  end
end
```

**Step 2: Run, expect FAIL**

```bash
mix test test/sta_connector/sta/client_test.exs -v
```

Expected: FAIL — "no protocol_number" or "no function clause matching" (current signature accepts `system_id`/`file_name`, not `protocol_number`).

**Step 3: Fix `client.ex` — `:upload_file` build clause**

Replace `client.ex:540-544` with:

```elixir
defp build_rest_request(:upload_file, %{
       protocol_number: protocol,
       content: content
     })
     when is_binary(protocol) and byte_size(protocol) > 0 do
  {:put, "/arquivos/#{protocol}/conteudo", content,
   [{"Content-Type", "application/octet-stream"}]}
end
```

**Step 4: Fix the caller in `operations.ex`**

Locate `operations.upload_file/3` (or whatever the public façade is). Refactor to:

```elixir
def upload_file(system_id, file_name, content) do
  with {:ok, protocol} <-
         Client.request_protocol(%{
           system_id: system_id,
           file_name: file_name,
           content: content
         }),
       {:ok, _result} <-
         Client.upload_file(%{
           protocol_number: protocol,
           content: content
         }) do
    {:ok, %{protocol_number: protocol, file_name: file_name}}
  end
end
```

**Step 5: Run test, expect PASS**

```bash
mix test test/sta_connector/sta/client_test.exs -v
```

Expected: all 3 tests PASS.

**Step 6: Run full suite to catch regressions**

```bash
mix test
```

Expected: green. If anything red, it's almost certainly in `outbound/uploader.ex` or `outbound/worker.ex` — they may have been calling old `upload_file/3` signature. Update them to use the new `operations.upload_file/3` (which now does request_protocol + upload internally).

**Step 7: Commit**

```bash
git add sta/backend/lib/sta_connector/sta/client.ex \
        sta/backend/lib/sta_connector/sta/operations.ex \
        sta/backend/test/sta_connector/sta/client_test.exs \
        sta/backend/lib/sta_connector/outbound/  # whatever else needed updating
git commit -m "fix(sta): upload PUT path uses real protocol number

P0 bug B1 from conformance study 2026-04-26: client was sending
PUT /arquivos/0/conteudo regardless of the protocol returned by
request_protocol. BCB returned 404 on every upload.

operations.upload_file/3 now sequences request_protocol → upload_file
and plumbs the protocol through. Public API of operations unchanged."
```

---

### Task 4: B5 scaffolding — types for partial transfer (CUT)

> **Status: CUT (2026-04-26 EOS, Brainstorm 2).** Decided not to scaffold `etag`/`last_modified`/`state_code`/`bytes_uploaded`/`expires_at` on `FileInfo`/`DownloadResult` ahead of Onda 2.A. Reasons:
>
> 1. **No parallel consumer.** All STA work in this session runs sequentially; Onda 2.A is the first thing that touches these structs after Onda 1 merges. The "evita churn de API" rationale assumes an external consumer that doesn't exist.
> 2. **Field-name risk under the misdiagnosis pattern.** Tasks 1-3 surfaced two Manual misdiagnoses (B4 Destinatarios, Content-Type citation). Comprometing names like `etag` / `state_code` / `bytes_uploaded` before reading Manual §7 (state codes) and verifying RFC 7232/7233 usage by BCB risks a rename in Onda 2.A.
> 3. **`expires_at` already exists in two scopes.** Audit found:
>    - `StaConnector.Sta.Types.ProtocolRequest` (`sta/backend/lib/sta_connector/sta/types.ex:173`) — protocol-level expiry per Manual §5 (48h)
>    - `StaConnector.Files.OutboundFile` Ecto schema (`sta/backend/lib/sta_connector/files/outbound_file.ex:108`) — DB-level
>
>    Original Task 4 would have introduced a third, file-level instance on `FileInfo` — wrong scope.
>
> **Disposition:** field additions become responsibility of Onda 2.A's bite-sized plan, after its **Manual Verification Task 0** confirms the wire-format names. The `# TODO Onda 2.A` marker in `outbound/uploader.ex` originally planned by this task is also dropped — Onda 2.A's PR speaks for itself when it lands.

---

### Task 5: Smoke-test scaffolding (skipped by default) (CUT)

> **Status: CUT (2026-04-26 EOS, Brainstorm 2).** Decided to defer smoke-test scaffolding to whichever Onda 2 sub-PR converges with G4 unblock (BCB homol creds + verified system_id). Reasons:
>
> 1. **Creds not available in this session.** `STA_HOMOL_USER`/`STA_HOMOL_PASSWORD` would never be set; the smoke would skip 100% of the time. Scaffolding ships infrastructure for a test that has never been exercised against BCB.
> 2. **`system_id: "TESTE"` is study-conjecture.** Manual STA v1.5 likely requires a registered system_id (e.g., `PCCS001` for CCS) — using `"TESTE"` could cause BCB to reject for the wrong reason and obscure wire-format signal. Operator must confirm a valid identifier before the smoke can claim "wire format OK".
> 3. **B1 fix (upload PUT path) untested by planned smoke.** A `request_protocol`-only smoke doesn't exercise the upload sequence end-to-end against BCB — the actual bug this PR was raised to unblock.
>
> **Disposition:** smoke moves to a future Onda 2 sub-PR (likely 2.A or an early task within it) when (a) BCB homol creds are provisioned and (b) operator confirms a valid system_id. The `:integration_bcb` tag exclusion in `test_helper.exs` is also deferred — adds 2 lines whenever needed. **Bypass-based unit coverage in Tasks 2-3 remains the wire-format safety net for this PR.**

---

### Task 6: Final validation + PR (revised post Brainstorm 2)

**Status (2026-04-26 EOS):** revised from a 4-step validation into a 9-item closeout that absorbs Tasks 1-3 review follow-ups + proactive audits surfaced during Brainstorm 2. **Estimated effort:** ~3-3.5h.

#### Items 1-3 — review follow-ups acumulados das Tasks 1-3

1. **Task 1 review I-1 — positive-path test for `finch_pool_opts/1`.** The current test exercises the negative path (raise on missing chain) via `StaConnector.Application.start/2`, which depends on supervisor-state assumptions. Add a direct unit test that calls the private helper through a public test seam (or extracts it to a callable function) and confirms it raises immediately when `Application.fetch_env!(:sta_connector, :icp_brasil_chain_path)` returns a path that doesn't exist. **Estimate: 15 min.**
2. **Task 1 review I-2 — default fallback in `config/config.exs`.** Currently the `:icp_brasil_chain_path` default lives only in `runtime.exs`. Mix tasks that bypass the runtime config pipeline (e.g., `mix compile` from a clean state) raise a confusing error. Add the same default in `sta/backend/config/config.exs` so the helper resolves before runtime. **Estimate: 10 min.**
3. **Task 2 review Minor #1 — `ClientBehaviour.@callback request_protocol/2` drift.** In `sta/backend/lib/sta_connector/sta/client_behaviour.ex`, the callback (a) cites Manual `§3.3` (which doesn't exist in Manual STA v1.5 — should be `§5.1.1`/`§5.1.2`) and (b) lacks `optional(:destinatarios)` in the typespec, so the optional schema-driven block is invisible at the contract level. Fix both. **Estimate: 10 min.**

#### Items 4-5 — plan + PR body updates

4. **Plan + handoff doc updates.** Apply Brainstorm 2 decisions to this plan file (this is in progress now: Decisions log → Brainstorm 2 added, Tasks 4-5 marked CUT, Task 6 revised, Pattern section added before Onda 2). Replace `sta/docs/handoff/2026-04-26-sta-paridade-onda1-partial-handoff.md` with `2026-04-26-sta-paridade-onda1-final-handoff.md` capturing what shipped, what was cut, lessons learned, and Ondas 2-4 caveats. **Estimate: 40 min.**
5. **PR body (final).** Write the PR description with: link to study `2026-04-26-sta-conformance-study.md`; link to this plan (incl. Brainstorm 2 anchor); per-commit bug mapping (B1 → `029b6369` + `e5e88a7f`; B2/B3 → `d310d82b`; B4 → `d310d82b` initial + `7f319b7d` correction; ICP-Brasil → `ff0ef70c`); explicit list of Manual misdiagnoses caught + how each was corrected; explicit deferral note ("Ondas 2-4 deferred; smoke test deferred to Onda 2 sub-PR with creds"); "Unblocks G4 (STA upload + GEN0006 ACCP) once SPB cert T001 lands in homol via this client." **Estimate: 30 min.**

#### Items 6-8 — proactive audits (Brainstorm 2 additions)

6. **Manual citation audit.** In code touched by Tasks 1-3, run `grep -rn "§[0-9]" sta/backend/lib sta/backend/test` and verify each citation against `sta/Manual_STA_Web_Services.pdf` (and `md/Manual de Segurança do SFN-v5_07.pdf` if cited). One drift already known (item 3 above: `§3.3` → `§5.1.x`); check for others (commit messages, module docstrings, comments). Fix-commit any drifts found. **Estimate: 30 min.**
7. **Behaviour callback shape audit.** Diff `sta/backend/lib/sta_connector/sta/client_behaviour.ex` against `sta/backend/lib/sta_connector/sta/client.ex` for every signature touched by Tasks 2-3 (`request_protocol`, `upload_file` — both moved from positional args to map-arg). Confirm typespecs match implementations and that `optional(:destinatarios)` lands wherever relevant. **Estimate: 15 min.**
8. **Guard rails.** Confirm:
   - `git diff main..HEAD --name-only | grep -v "^sta/"` returns empty (no accidental cross-session-domain edits);
   - new code in this PR introduces no `vulci`/`coreapi-dev`/`fluxiq` references (legacy in `sta/CLAUDE.md` is acceptable; this is about new content);
   - no orphan references to `etag`/`last_modified`/`state_code`/`bytes_uploaded` (the cut Task 4 fields).
   **Estimate: 5 min.**

#### Item 9 — final validation + push + PR

9. **Validate, push, open PR.** Run `mix format --check-formatted`, `mix compile --warnings-as-errors`, `mix test`, sanity-check `git diff main..HEAD --stat`; then `git push -u origin sta-paridade`; open PR with title `fix(sta): Onda 1 — P0 wire-format conformance + ICP-Brasil chain` and the body from item 5. **Estimate: 20 min.**

#### Acceptance

- Items 1-3 land as commits on `sta-paridade`
- Item 4 commit lands plan + handoff docs (this commit; sequel commits if handoff lags)
- Items 6-8 either pass cleanly (no findings → noted in PR body) or surface fix-commits (mapped in PR body)
- Item 9 PR opened on GitHub; URL captured in handoff
- All commits authored from this worktree only; `git status` clean before push

---

<a id="pattern-manual-verification"></a>
## Pattern: study claims requiring Manual verification (Ondas 2-4)

**Origin.** Onda 1 surfaced two Manual misdiagnoses from the conformance study (B4 Destinatarios required + literal "BACEN" → Manual §5.1.2 says block is **Opcional** with schema `<Unidade>/<Dependencia>/<Operador>`; Content-Type Manual citation invented). The fixes landed in commits `7f319b7d` and `e5e88a7f` after subagent reviewers read the PDF directly. A third pattern occurrence — `expires_at` field-scope error in this plan — was caught during the Brainstorm 2 audit.

**Process for every Onda 2-4 sub-PR (when expanded into its bite-sized plan):**

> ### Task 0 — Manual Verification (mandatory)
>
> Before writing any test or code, list every wire-format / endpoint / query-param / response-code / field-name / numeric-threshold claim drawn from the study or this plan. Open `sta/Manual_STA_Web_Services.pdf` and tag each claim:
>
> - `[VERIFIED §X.Y]` — exact section/page citation found
> - `[STUDY-CONJECTURE]` — not in Manual; treat as best-guess until confirmed by operator or by real BCB smoke
> - `[NEEDS-OPERATOR-CONFIRM]` — Manual ambiguous; Compliance/operator must answer
>
> Document findings in the sub-PR's Decisions log before writing tests. Mirrors SPB Wave 9 "decompile-first" lesson (`feedback_decompile_first_when_porting_legacy.md`).

**Inventory of claims to verify** (curated 2026-04-26 from existing plan):

#### Onda 2.A — resilient transfer
- Element name `<UltimoByteRecebido>` returned by `posicao_upload`
- Exact BCB format for `Content-Range: bytes {ini}-{fim}/{total}` (RFC 7233 standard, but BCB usage)
- BCB use of `Range: bytes={ini}-{fim}` for ranged GET (RFC standard, BCB usage)
- BCB use of `If-Match: {etag}` + `If-Unmodified-Since` (RFC 7232, BCB usage)
- HTTP 412 = precondition failed; HTTP 416 = range not satisfiable (BCB-side semantics)
- Default chunk threshold 1 MB

#### Onda 2.B — status & queries
- StateCode int → atom mapping (codes 1, 2, 5, 10, 15, 20, 25, 30, 35, 45, 55, 65, 70, 75)
- Query params `tipoConsulta=PROT|AVANC` and `nivelDetalhe=RES|BAS|COMPL`
- `<Protocolos>{p1};{p2}</Protocolos>` semicolon delimiter for batched change_status
- Situation values `:rec | :a_rec`

#### Onda 2.C — rate & lifecycle
- HTTP 410 = protocol expired (BCB response code)
- HTTP 429 with `Retry-After` header (BCB rate-limit response)
- 48h protocol expiry (Manual §5 reference — actually verify section number)
- Bucket sizes: 10 concurrent transfers, 120/min queries, 10/min status_changes

#### Onda 2.D — password mgmt
- `/senhaws/senha` endpoint and `PUT`/`GET /senhaws/senha/vencimento` paths
- Separate hosts: `www9.bcb.gov.br/senhaws` (homol), `www3.bcb.gov.br/senhaws` (prod)

#### Onda 3.A — leiautes corpus
- 44 leiautes count from BCB RSFN page
- 9-group categorization (`{ccs,cir,cmp,gen,lfl,ptx,scg,sel,str}`)

#### Onda 3.B — CCS
- ACCS message types list (001-006, 009-012)
- ACCS009 DOC positional layout

#### Onda 3.C — novos parsers
- TXT field length tables for ASTR003, AGEN002, APTX001, ALFL001-003
- XSD/DOCX layouts for ASCG020-025

#### Onda 4.A — cuts
- Identifier mapping `CCS→PCCS001`, `SCR→PSCR001`, `SISBAJUD→PSBA001`, `PGV→PPGV001`
- Whether deletion of `SIMBA`/`DIMP`/`E_FINANCEIRA` cases in `core/backend/lib/monetarie/regulatory/sta_delivery.ex` is Manual-supported (study-driven decision)

---

<a id="onda-2"></a>
## Onda 2 — Operations & lifecycle (4 sub-PRs)

Each sub-PR below ships independently to `main` from a sub-branch off `main` (post-Onda-1). Sub-PRs marked *parallel* have no inter-dependencies; the others must merge in the listed order.

When each sub-PR starts, expand it into its own bite-sized plan file `docs/plans/2026-XX-XX-sta-onda2-<slug>.md` following the same TDD format as Onda 1 above.

---

### Sub-PR 2.A — `sta/onda2-resilient-transfer`

**Estimate:** 3-4 days, ~800 LOC.
**Depends on:** Onda 1 merged.
**Blocks:** Sub-PR 2.C (rate limiter wraps these new functions).

**Files to create:**
- `sta/backend/priv/repo/migrations/<TIMESTAMP>_add_resilient_transfer_columns.exs`

**Files to modify:**
- `sta/backend/lib/sta_connector/sta/client.ex`:
  - Add `Client.upload_part/4` — `Content-Range: bytes {ini}-{fim}/{total}` per RFC 7233
  - Add `Client.posicao_upload/1` — `GET /arquivos/{protocol}/posicaoupload`, parses `<UltimoByteRecebido>` from XML response
  - Add `Client.download_part/3` — `Range: bytes={ini}-{fim}` + `If-Match: {etag}` + optional `If-Unmodified-Since`
  - Persist `etag` from response headers into `Types.DownloadResult`
- `sta/backend/lib/sta_connector/sta/operations.ex`:
  - `operations.upload_file/3` learns chunked upload (default threshold 1 MB, env-tunable)
  - `operations.download_file/2` learns ranged GETs with retry-from-byte-N on transient failure
  - New `operations.resume_upload/1` for explicit resume after worker restart
- `sta/backend/lib/sta_connector/outbound/uploader.ex`:
  - Drop the TODO marker from Onda 1 Task 4
  - Implement chunking loop with `posicao_upload` recovery
- `sta/backend/lib/sta_connector/inbound/worker.ex`:
  - Persist `etag` per `OutboundFile` (or `InboundFile`, depending on naming) for downstream `If-Match`
- `sta/backend/lib/sta_connector/sta/error.ex`:
  - `Error.from_http_status/2` differentiates HTTP 412 (precondition failed → caller retries from posicaoupload) and HTTP 416 (range not satisfiable → caller restarts from 0)
- `sta/backend/lib/sta_connector/sta/types.ex`:
  - `FileInfo.bytes_uploaded` field gets populated; defaulted to nil

**Migration:** add columns to `outbound_files` table (or equivalent):
- `etag` (string, nullable)
- `last_modified` (utc_datetime_usec, nullable)
- `bytes_uploaded` (bigint, default 0)

**Test approach:**
- Bypass scenarios: full upload < threshold, chunked upload at exactly threshold + 1 byte, chunked upload with simulated transient failure on chunk 3 (test triggers `posicao_upload` and resumes from byte N), download with `If-Match` mismatch (expect 412 → caller restarts), download with bad range (expect 416)
- Real BCB: extend `bcb_homol_smoke_test.exs` with one upload > 5 MB to verify wire format on real server (when creds available)

**Acceptance criteria:**
- Files up to 100 MB upload successfully through Bypass-simulated transient failures
- ETag round-trips through DownloadResult and is persisted in DB
- HTTP 412/416 surface specific error atoms (`{:error, :precondition_failed}`, `{:error, :invalid_range}`)

---

### Sub-PR 2.B — `sta/onda2-status-and-queries`

**Estimate:** 2 days, ~400 LOC.
**Depends on:** Onda 1 merged.
**Parallel with:** 2.A, 2.D.

**Files to create:**
- `sta/backend/lib/sta_connector/sta/state_code.ex` — enum mapping
- `sta/backend/test/sta_connector/sta/state_code_test.exs`

**Files to modify:**
- `sta/backend/lib/sta_connector/sta/client.ex`:
  - Extend `:confirm_receipt` build clause (currently only emits `<Situacao>REC</Situacao>` for one protocol) to accept `%{protocols: [list], situation: :rec | :a_rec}` and emit `<Protocolos>{p1};{p2};...</Protocolos>` per manual
  - Rename clause to `:change_status` for clarity; keep `:confirm_receipt` as a thin wrapper that calls `:change_status` with `[protocol]` + `:rec` for backward compat (then deprecate in Onda 4)
  - Add `Client.query_protocol/2` — `GET /arquivos?tipoConsulta=PROT&protocolos={list}&nivelDetalhe={RES|BAS|COMPL}`
  - Add `Client.advanced_query/1` — `GET /arquivos?tipoConsulta=AVANC&dataHoraInicio=...&dataHoraFim=...&situacao=...`
- `sta/backend/lib/sta_connector/inbound/worker.ex`:
  - Decisions branch on `StateCode.terminal?/1`, `StateCode.success?/1`, `StateCode.failure?/1` instead of textual `status_desc`
- `sta/backend/lib/sta_connector/outbound/worker.ex`:
  - Same — replace string compares with enum

**`StateCode` module spec:**

```elixir
defmodule StaConnector.Sta.StateCode do
  @moduledoc "BCB STA state codes per Manual v1.5 §7"

  @codes %{
    1 => :protocol_generated,
    2 => :available_for_download,
    5 => :transmission_started,
    10 => :transmission_finished,
    15 => :validation_in_progress,
    20 => :received_by_bacen,
    25 => :delivered_to_recipient,
    30 => :processing_by_recipient,
    35 => :accepted,
    45 => :cancelled,
    55 => :inconsistent,
    65 => :rejected,
    70 => :download_started,
    75 => :download_finished
  }

  def from_int(n), do: Map.fetch(@codes, n)
  def to_int(atom), do: Enum.find_value(@codes, fn {k, v} -> v == atom && k end)

  @terminal [35, 45, 55, 65, 75]
  def terminal?(code) when is_integer(code), do: code in @terminal

  @success [25, 35, 75]
  def success?(code) when is_integer(code), do: code in @success

  @failure [45, 55, 65]
  def failure?(code) when is_integer(code), do: code in @failure
end
```

**Test approach:**
- Unit tests over StateCode classifications
- Bypass for `change_status` batched (3 protocols at once); query_protocol with each `nivelDetalhe`; advanced_query with date range

**Acceptance criteria:**
- Workers no longer string-compare status descriptions
- A_REC works against Bypass mock
- query_protocol parses both single-protocol and batch responses

---

### Sub-PR 2.C — `sta/onda2-rate-and-lifecycle`

**Estimate:** 2 days, ~500 LOC.
**Depends on:** 2.A merged (rate limiter wraps the new partial-transfer functions).
**Blocks:** none.

**Files to create:**
- `sta/backend/priv/repo/migrations/<TIMESTAMP>_add_protocol_expires_at.exs`
- `sta/backend/lib/sta_connector/sta/rate_limiter/transfers.ex` (10 concurrent)
- `sta/backend/lib/sta_connector/sta/rate_limiter/queries.ex` (120/min)
- `sta/backend/lib/sta_connector/sta/rate_limiter/status_changes.ex` (10/min)

**Files to modify:**
- `sta/backend/lib/sta_connector/sta/rate_limiter.ex` — replace single-bucket impl with façade dispatching to the 3 new modules
- `sta/backend/lib/sta_connector/sta/client.ex` — `execute_request/3` chooses the bucket based on operation atom (`:upload_file`/`:upload_part`/`:download_file`/`:download_part` → :transfers; `:list_available_files`/`:query_protocol`/`:advanced_query`/`:posicao_upload` → :queries; `:change_status` → :status_changes; `:request_protocol` → :transfers)
- `sta/backend/lib/sta_connector/sta/error.ex`:
  - HTTP 410 → `{:error, :protocol_expired}` (caller in Operations re-`request_protocol`s automatically)
  - HTTP 429 → read `Retry-After` header, return `{:error, {:rate_limited, seconds_to_wait}}`; outbound worker honours it
- `sta/backend/lib/sta_connector/outbound/worker.ex`:
  - On `:protocol_expired`, call `request_protocol` again and retry upload
  - On `:rate_limited, n`, sleep n seconds before next attempt
- Schema for `OutboundFile` (or equivalent) gets `protocol_expires_at :utc_datetime`; populated to `inserted_at + 48h` per manual §5

**Test approach:**
- Property-based: spam 12 concurrent transfer requests, assert exactly 10 pass through and 2 wait
- Bypass: 410 response → caller observes auto re-request_protocol then retry
- Bypass: 429 with `Retry-After: 5` → caller sleeps then retries

**Acceptance criteria:**
- Three independent buckets enforced and observable via telemetry
- Protocol auto-renews on 410
- 429 backoff works without sleeping the caller's process indefinitely

---

### Sub-PR 2.D — `sta/onda2-password-mgmt`

**Estimate:** 1 day, ~300 LOC.
**Depends on:** Onda 1 merged.
**Parallel with:** 2.A, 2.B.

**Files to create:**
- `sta/backend/lib/sta_connector/sta/password_manager.ex` (GenServer)
- `sta/backend/lib/sta_connector/workers/password_expiry_worker.ex` (Oban cron worker)
- `sta/backend/test/sta_connector/sta/password_manager_test.exs`

**Files to modify:**
- `sta/backend/lib/sta_connector/sta/client.ex` — add `Client.password_change/2` (`PUT /senhaws/senha`), `Client.password_expiry/0` (`GET /senhaws/senha/vencimento`); these use **separate base URL** (`www9.bcb.gov.br/senhaws` homol, `www3.bcb.gov.br/senhaws` prod) — extend `get_endpoint_url/1` to accept an `:endpoint_kind` flag
- `sta/backend/config/sta_config.json` — add `homologation_password_url` and `production_password_url`
- `sta/backend/lib/sta_connector/application.ex` — add Finch pool entries for the senha hosts (already covered by Task 1 above if you included www9/www3 in pools; otherwise extend now)
- `sta/backend/lib/sta_connector/notifications.ex` — add `notify_password_expiring/1` (used by Oban worker)

**Cron schedule:** daily 06:00 BRT (`{"0 9 * * *", StaConnector.Workers.PasswordExpiryWorker}` in UTC)

**Test approach:**
- Bypass for `password_change` happy path + 401 (current pass wrong)
- Bypass for `password_expiry` returning 95/30/7/3/0 days remaining; assert worker triggers notification at ≤7

**Acceptance criteria:**
- Senha can be changed via API call
- Cron emits a notification when expiry ≤ 7 days
- No surprise downtime from senha expiring silently

---

<a id="onda-3"></a>
## Onda 3 — Parsers & RSFN leiautes (4 sub-PRs)

---

### Sub-PR 3.A — `sta/onda3-leiautes-corpus-validator`

**Estimate:** 1-2 days, ~400 LOC + 2-3 MB of XSD/DOC/TXT corpus.
**Depends on:** Onda 1 merged. (Independent of Onda 2.)
**Blocks:** 3.B and 3.C.

**Files to create:**
- `sta/backend/priv/leiautes-rsfn/{ccs,cir,cmp,gen,lfl,ptx,scg,sel,str}/` (directory tree)
- `sta/backend/priv/leiautes-rsfn/INDEX.md` — table of every file with source URL, vigência, checksum
- `sta/scripts/sync_leiautes_rsfn.sh` — re-sync script (wget/curl with checksums)
- `sta/backend/lib/sta_connector/parsers/xsd_validator.ex` — wrapper over `:xmerl_xsd`
- `sta/backend/lib/sta_connector/crypto/xml_signer.ex` — stub interface
- `sta/backend/lib/sta_connector/crypto/xml_signer/stub.ex` — default impl (returns `{:error, :not_configured}`)
- Tests for both

**Behavior:**
- `XsdValidator.validate(xml_binary, schema_name)` → `:ok | {:error, [violations]}`. Caches compiled schemas in ETS table `:sta_xsd_schemas` with TTL 1h. Lazy-loads from `priv/leiautes-rsfn/{group}/{schema_name}.xsd`.
- `XmlSigner` is a behavior (`@callback sign(xml :: binary, opts :: keyword) :: {:ok, binary} | {:error, term}`) with the Stub impl wired by default in `runtime.exs`. Real impl will replace via config when Compliance confirms cert source.

**Test approach:**
- XsdValidator: feed it ACCS001.xsd + a known-valid sample → :ok; feed it the same sample with a corrupt field → `{:error, [...]}`
- XmlSigner: stub returns `{:error, :not_configured}` when called

**Acceptance criteria:**
- `priv/leiautes-rsfn/` contains all 44 files from BCB RSFN page (verifiable via `scripts/sync_leiautes_rsfn.sh --check`)
- XsdValidator validates 5 known-good and 5 known-bad samples correctly
- XmlSigner interface is final and stable (Onda 3.B / future replacement only swaps the impl module)

---

### Sub-PR 3.B — `sta/onda3-ccs-xsd-parser`

**Estimate:** 3 days, ~700 LOC.
**Depends on:** 3.A merged.
**Parallel with:** 3.C and 3.D.

**Files to modify (or rewrite):**
- `sta/backend/lib/sta_connector/parsers/ccs.ex` — full rewrite from positional flat-file to XML/XSD-driven
- `sta/backend/test/sta_connector/parsers/ccs_test.exs` — extend with sample fixtures for each ACCS message type

**Files to create:**
- `sta/backend/lib/sta_connector/builders/accs_builder.ex` — emits ACCS001 + ACCS010 send payloads
- `sta/backend/test/fixtures/ccs/{ACCS001,ACCS002,ACCS003,ACCS004,ACCS005,ACCS006,ACCS009,ACCS010,ACCS011,ACCS012}_sample.xml` — one valid sample per type

**Coverage:** all 10 CCS messages (ACCS001 atualização diária, ACCS002 resposta, ACCS003 validação relacionamentos, ACCS004 posição, ACCS005 detalhamento, ACCS006 resposta, ACCS009 ocorrências [DOC layout — handled positional], ACCS010 transferência solicitação SEND, ACCS011 validação RECEIVE, ACCS012 resultado RECEIVE).

**Builder behavior:** `AccsBuilder.build_accs001(submission_id, members)` reads from `Monetarie.Cadoc.CCS` context (already exists in `core/`), emits XML, validates against `priv/leiautes-rsfn/ccs/ACCS001.xsd` before returning. ACCS010 SEND analogously consumes a `transferencia_relacionamento` source.

**Test approach:**
- Unit: each ACCS sample fixture parses to a typed map without errors
- Roundtrip: AccsBuilder.build_accs001 output round-trips through ccs.ex parser to recover the same map
- Negative: malformed sample (missing required field) returns `{:error, [violations]}` from XsdValidator

**Acceptance criteria:**
- All 10 ACCS message types covered by parser
- ACCS001 + ACCS010 SEND builders integrated with `Monetarie.Cadoc.CCS`
- 100% of fixtures pass `xmllint --schema` against the local XSDs

---

### Sub-PR 3.C — `sta/onda3-novos-parsers`

**Estimate:** 3 days, ~900 LOC.
**Depends on:** 3.A merged.
**Parallel with:** 3.B and 3.D.

**Files to create:**
- `sta/backend/lib/sta_connector/parsers/aspb.ex` — ASPB005/006/007/020 (XSD)
- `sta/backend/lib/sta_connector/parsers/astr.ex` — ASTR003 (TXT, fixed-width)
- `sta/backend/lib/sta_connector/parsers/agen.ex` — AGEN002 (TXT)
- `sta/backend/lib/sta_connector/parsers/aptx.ex` — APTX001 (TXT, PTAX final)
- `sta/backend/lib/sta_connector/parsers/alfl.ex` — ALFL001/002/003 (TXT)
- `sta/backend/lib/sta_connector/parsers/ascg.ex` — ASCG020-025 (DOCX/XSD)
- One fixture per parser under `test/fixtures/{group}/`
- Test files for each

**Test approach:**
- Each parser has a happy-path test against a fixture and a negative test against a corrupt fixture
- Parsers backed by XSD use `XsdValidator` from 3.A; TXT parsers do positional decode with explicit field length tables in module docstrings

**Acceptance criteria:**
- 6 new parsers, each covering its full message-type set
- All fixtures pass through the parser without raises
- Round-trip not required for read-only parsers (ASTR/AGEN/APTX/ALFL/ASCG read-only; ASPB will be both — write coverage deferred to Onda 4 builder if needed)

---

### Sub-PR 3.D — `sta/onda3-audit-existing-parsers`

**Estimate:** 2 days, ~200 LOC of changes.
**Depends on:** Onda 1 merged.
**Parallel with:** 3.A/3.B/3.C.

**Files to audit:**
- `sta/backend/lib/sta_connector/parsers/cir.ex` (ACIR002-005, DOC format — verify positional layout vs spec)
- `sta/backend/lib/sta_connector/parsers/cmp.ex` (ACMP001, DOC)
- `sta/backend/lib/sta_connector/parsers/cam.ex` (Câmbio — check actual spec source)
- `sta/backend/lib/sta_connector/parsers/spi.ex` (SPI is PIX, may be out-of-STA scope)
- `sta/backend/lib/sta_connector/parsers/str.ex` (STR — distinguish from ASTR003)
- `sta/backend/lib/sta_connector/parsers/ldl.ex` (verify spec)
- `sta/backend/lib/sta_connector/parsers/dict.ex` (DICT is PIX; almost certainly out-of-STA scope — flag for Onda 4 deletion)

**Method:**
- For each parser, spend ≤30 min comparing field offsets/types against the spec doc
- If spec matches: add a `# verified vs <spec doc> v<date>` header comment + a regression test fixture if missing
- If spec mismatches: file a separate sub-issue; minor fixes inline

**Acceptance criteria:**
- Every existing parser file either has the verification comment or a TODO with a specific question
- One regression fixture per parser
- DICT and SPI flagged for Onda 4.A deletion if confirmed out-of-STA

---

<a id="onda-4"></a>
## Onda 4 — Cuts, observability, governance (4 sub-PRs)

---

### Sub-PR 4.A — `sta/onda4-cuts-and-audit`

**Estimate:** 1 day. **Net LOC delta: −3,000 to −5,000.**
**Depends on:** Onda 3.D merged (so DICT/SPI deletions are evidence-backed).
**Parallel with:** 4.B, 4.C, 4.D.

**Deletions:**
- `sta/backend/lib/sta_connector/sta/soap.ex` + any tests under `test/sta_connector/sta/soap_test.exs`
- `sta/cmd/`, `sta/internal/`, `sta/pkg/`, `sta/go.mod`, `sta/go.sum` (Go residue)
- `sta/backend/lib/sta_connector/parsers/dict.ex` and `spi.ex` if Onda 3.D confirmed out-of-STA
- In `core/backend/lib/monetarie/regulatory/sta_delivery.ex`: remove `SIMBA`, `DIMP`, `E_FINANCEIRA` cases from `sta_system_for/1`. Audit that no callers send to these systems via STA. (This is the only change touching `core/` in this whole rollout.)

**Audit task:**
- Read `core/backend/lib/monetarie/regulatory/sta_delivery.ex` `sta_system_for/1` post-deletion. The remaining identifiers are `CCS` (PCCS001), `SCR` (PSCR001), `SISBAJUD` (PSBA001), `PGV` (PPGV001). Verify each against the BCB system table if available in `md/` references, or open a TODO with a precise question.
- Add commit message body explaining for each deletion: where the wrong assumption came from, what replaces it (or why nothing does), and which manual section confirms the cut.

**Test approach:**
- `mix compile --warnings-as-errors` clean after deletions (catches broken references)
- Existing parser/router tests stay green (they should never have exercised the deleted modules)
- Add a smoke test that `sta_system_for(:simba)` returns `{:error, :not_a_bcb_system}` (or whatever the new behaviour is — likely returning a clear error rather than silently routing to STA)

---

### Sub-PR 4.B — `sta/onda4-observability`

**Estimate:** 2 days, ~600 LOC.
**Depends on:** Ondas 2 and 3 merged (metrics need the new state codes, rate-limiter buckets, password expiry GenServer).
**Parallel with:** 4.A, 4.C, 4.D.

**Files to create:**
- `sta/backend/priv/grafana/sta-overview.json` — dashboard JSON
- `sta/backend/priv/prometheus/alerts.yml`
- `sta/backend/lib/sta_connector/telemetry/handlers.ex`
- `sta/backend/test/sta_connector/telemetry/handlers_test.exs`

**Files to modify:**
- `sta/backend/lib/sta_connector/metrics.ex` — add metric definitions:
  - Counter `sta_protocols_total{system_id, state_code}` — emitted on every state transition
  - Histogram `sta_transfer_duration_seconds{system_id, direction}` — direction in {upload, download}
  - Counter `sta_rate_limit_throttled_total{bucket}` — bucket in {transfers, queries, status_changes}
  - Gauge `sta_password_days_until_expiry`
  - Counter `sta_bcb_http_status_total{status}`
- Telemetry events emitted from Client, RateLimiter, PasswordManager, Workers

**Alert rules:**
- `sta_password_days_until_expiry < 7` for >5min → page
- `increase(sta_protocols_total{state_code="65"}[15m]) > 0` → warning
- `increase(sta_protocols_total{state_code="55"}[15m]) > 0` → warning
- `rate(sta_bcb_http_status_total{status=~"5.."}[5m]) > 0.1` → page

**Acceptance criteria:**
- Dashboard JSON imports cleanly into a Grafana instance
- Prometheus scrape returns all 5 metric families
- Alert rules pass `promtool check rules`

---

### Sub-PR 4.C — `sta/onda4-vault-and-audit-log`

**Estimate:** 1.5 days, ~400 LOC + 1 migration.
**Depends on:** Onda 1 merged.
**Parallel with:** 4.A, 4.B, 4.D.

**Files to create:**
- `sta/backend/priv/repo/migrations/<TIMESTAMP>_create_audit_logs.exs`
- `sta/backend/lib/sta_connector/audit_log.ex` — `record/1` helper + Ecto schema
- `sta/backend/test/sta_connector/audit_log_test.exs`

**Files to modify:**
- `sta/backend/lib/sta_connector/sta/client.ex` — every successful or failing round-trip calls `AuditLog.record(%{occurred_at, direction, protocol, system_id, http_status, request_hash, response_hash})`
- `sta/backend/config/runtime.exs` — pull BCB user/pass from env (already does; verify no fallback to `sta_config.json`)
- `sta/backend/config/sta_config.json` and `sta_config.example.json` — DELETE `bcb_username`/`bcb_password` keys; replace with `<from secrets manager>` placeholder string
- `infra/aws/greenfield/secrets.tf` — add Secret entry `monetarie/{env}/sta/bcb_credentials` with KMS CMK (per Regra Absoluta #9). Add IAM grant for ECS task role.
- `infra/aws/greenfield/ecs-services/sta.tf` (or wherever the task def lives) — add `secrets` block with `valueFrom` for STA_BCB_USERNAME and STA_BCB_PASSWORD pointing at the new SM entry

**`audit_logs` schema:**
- `id` binary_id PK
- `occurred_at` utc_datetime_usec
- `direction` :upload | :download | :status_change | :query | :password_change
- `protocol_number` string nullable
- `system_id` string
- `http_status` integer
- `request_hash` string (SHA-256 of request body, hex)
- `response_hash` string (SHA-256 of response body, hex)
- `inserted_at` utc_datetime_usec

DB-level: `REVOKE UPDATE, DELETE ON audit_logs FROM <app_user>` in the migration (PG GRANT). App-level: schema does NOT define `changeset` — only `insert_changeset/1` exists.

**Acceptance criteria:**
- Migration applies and reverts cleanly
- `psql -c "UPDATE sta.audit_logs SET ..."` rejected by RBAC
- All BCB round-trips emit one audit row
- `grep -r "bcb_password" sta/backend/config/` returns nothing other than the placeholder
- ECS task pulls creds from Secrets Manager on next deploy

---

### Sub-PR 4.D — `sta/onda4-docs`

**Estimate:** 1 day, ~600 LOC of docs.
**Depends on:** Ondas 2, 3, and other 4.X sub-PRs merged (docs reflect final state).
**Parallel with:** 4.A, 4.B, 4.C while drafting; final commit must be after the others merge.

**Files to rewrite:**
- `sta/CLAUDE.md` — strip ProxMox/GCP/Vulci references (the file already has a header note flagging these as legacy). Replace with AWS greenfield endpoints (`monetarie-sta-homolog` Aurora cluster, ECS service `sta-backend`, ALB DNS, etc.). Update the Module list to reflect deletions (no SOAP, no Go, no SIMBA/DIMP/E-Fin) and additions (StateCode, RateLimiter buckets, PasswordManager, AuditLog, all new parsers).

**Files to create:**
- `sta/docs/architecture/nats-subjects.md` — consolidate the 9+ `monetarie.sta.*` subjects with JSON Schema per subject (canonical reference)
- `sta/docs/handoff/2026-XX-XX-sta-conformance-rollout-handoff.md` — created at the end summarising what shipped, what's deferred, what needs ops follow-up

**Files to modify:**
- `sta/README.md` — list of supported BCB systems + explicit list of EXCLUDED systems with one-line rationale (SIMBA→COAF, DIMP/E-Fin→RFB SPED, etc.)

**At the very end:** move this plan file from `sta/docs/plans/2026-04-26-sta-conformance-rollout.md` to `sta/docs/handoff/` with the rollout date in the filename.

**Acceptance criteria:**
- `grep -r "vulci\|proxmox\|gcp\|fluxiq" sta/` returns 0 hits in non-archived files
- New `nats-subjects.md` documents every subject with at least one example payload
- README has a section "Sistemas suportados" and "Sistemas EXCLUÍDOS"

---

<a id="risks"></a>
## Risk register

| # | Risk | Mitigation |
|---|---|---|
| R1 | XSD signing turns out to be required after all (Compliance confirms A1-A3 cert) | XmlSigner stub interface (3.A) is stable — only the impl module swaps. Buffer ~3 days into Onda 4 for swap-in if needed. |
| R2 | BCB homol creds delayed indefinitely | Onda 1 ships without real-BCB smoke — Bypass coverage is the safety net for wire format. Smoke task waits, doesn't block ship. |
| R3 | Identifiers PSCR001/PSBA001 turn out to be wrong | First real-BCB smoke surfaces this fast (HTTP 400 from BCB on bad identifier). Each is an isolated string, low-cost fix. |
| R4 | `:xmerl_xsd` proves too slow under load | XsdValidator caches compiled schemas in ETS (TTL 1h). If still too slow, evaluate Saxy or XQ — both Hex deps, easy swap. |
| R5 | Aurora migration `protocol_expires_at` (2.C) collides with Onda 1's PR causing rebase pain | Onda 2 sub-branches cut from `main` post-Onda-1; sequential merge of 2.A→2.C. The migrations are additive, no FK churn. |
| R6 | Deleting Go residue (4.A) breaks a Dockerfile or CI workflow | Audit `grep -r 'cmd/connector' .github/ sta/` before deleting. Should be 0 hits since rewrite landed. |
| R7 | Wave9 SPB sessions in parallel touch `core/backend/lib/monetarie/regulatory/sta_delivery.ex` (4.A target) | Communicate the cut early; if conflict, rebase 4.A on top of theirs. Touching one file in core is small surface. |
| R8 | Real BCB returns subtle XML differences from manual examples (e.g., element order, whitespace) | Onda 1 smoke catches request-side issues; Onda 2's full operations exercise more response-side. Iterate per real BCB feedback during PR review window. |

---

<a id="rollback"></a>
## Rollback strategy

Each Onda's PR is independently revertable. Roll-back order matches reverse merge order:

1. **Onda 4.D (docs)** — pure revert, no behaviour change
2. **Onda 4.C (vault)** — re-introduce `sta_config.json` creds AND restore old `runtime.exs`. Audit log table can stay (data preserved). Deploy needs ECS env vars set back to literal values.
3. **Onda 4.B (observability)** — pure revert; metrics drop out, dashboards stale but harmless.
4. **Onda 4.A (cuts)** — revert restores SOAP/Go/SIMBA. Avoid this revert; the cuts were correct per study.
5. **Onda 3.{A,B,C,D} (parsers)** — revertable; old parsers were stubs anyway, so revert reverts to "didn't actually work".
6. **Onda 2.{A,B,C,D}** — each independent; revert restores 2.0-era behaviour for that area only.
7. **Onda 1 (P0)** — revert means BCB upload returns to 100% failure. Only revert if Onda 1 itself is broken.

For ICP-Brasil chain (Onda 1 Task 1): the chain file deletion is non-revertable in the sense that operations stop — but config can be set to bypass via `verify: :verify_none` *only* for emergency dev troubleshooting. Document this explicitly as a forbidden production setting.

---

<a id="references"></a>
## References

### Manuais BCB (em-repo)
- `sta/Manual_STA_Web_Services.pdf` — primary spec
- `md/Manual de Segurança do SFN-v5_07.pdf` — crypto context (XML signing reference if needed)

### Manuais BCB (online — link only)
- [Manual STA Web Services v1.5](https://www.bcb.gov.br/content/acessoinformacao/sisbacen_docs/Manual_STA_Web_Services.pdf)
- [Manual STA Web (UI)](https://www.bcb.gov.br/content/acessoinformacao/sisbacen_docs/Manual_STA_Web.pdf)
- [Leiautes RSFN](https://www.bcb.gov.br/estabilidadefinanceira/leiautearquivosrsfn) — source for `priv/leiautes-rsfn/`
- [Manual CCS](https://www.bcb.gov.br/content/acessoinformacao/Documents/ccs/ccs_manual.pdf)
- [ICP-Brasil — BCB](https://www.bcb.gov.br/estabilidadefinanceira/certificacaodigital)

### Estudo-base
- `~/Downloads/2026-04-26-sta-conformance-study.md` — diagnóstico que originou este plano

### Plans relacionados (em-repo)
- `sta/docs/plans/2026-01-31-sta-connector-elixir-rewrite.md` — rewrite anterior (REST+Basic Auth landing) — contexto histórico
- `sta/docs/plans/2026-01-31-sta-connector-design.md` — design original
- `sta/docs/plans/2026-02-01-test-files-and-real-integration.md` — abordagem de testes anterior
- `docs/plans/2026-04-25-spb-execution-plan-final.md` — SPB master plan, padrão de wave-based execution
- SPB Wave 1 ICP-Brasil precedent: `spb/services/bacen_gateway/priv/certs/icp_brasil_ca_chain.pem`

### RFCs aplicáveis
- RFC 7617 — HTTP Basic Auth
- RFC 7233 — HTTP Range Requests (`Range`, `Content-Range`, `If-Match`, `If-Unmodified-Since`)
- RFC 7232 — HTTP Conditional Requests (`ETag`)

---

**Plan version:** 1.0 · **Author:** Claude (sob direção de Luiz Marcelo) · **Date:** 2026-04-26
