# Handoff — STA Onda 2.C (B trim) complete, awaiting push/PR auth

**Data:** 2026-04-27
**Worktree:** `/Users/luizpenha/monetarie/.worktrees/sta-paridade`
**Branch:** `sta/onda2-rate-and-lifecycle` (cut from `origin/main` HEAD `67924840` post Onda 2.B merge)
**Status:** All 4 Onda 2.C (B) implementation tasks complete + plan + handoff. Touchstones green. Ready for push/PR after explicit user authorization.

---

## TL;DR (one paragraph)

Implemented Onda 2.C per option **(B) trim** decided at session start: 3-bucket RateLimiter façade per Manual STA Web Services v1.5 §2.6 + conservative status-changes cap, HTTP 410 per-operation mapping (uploads → `:protocol_expired`, downloads unchanged → `:file_unavailable`), HTTP 429 `Retry-After` parsing per RFC 7231 §7.1.3, and `Outbound.Worker.dispatch_upload_error/2` routing the new error atoms onto auto-renewal + delayed-retry actions. **Deferred**: proactive `protocol_expires_at` migration to a future 2.C-2 follow-up when an admin-UI consumer exists (same lesson learned as Onda 2.B (B): no orphan fields without callers). 6 commits, ~750 LOC delta. **27 new tests** across 4 files + extended client_test.exs; combined 262/256 PASS (vs. 235/229 post-Onda 2.B baseline; the 6 `Soap.parse_response` baseline failures preserved). Compile preserves 5 baseline warnings exactly. Zero out-of-scope file changes.

---

## What's in this branch (chronologically)

| # | SHA | Type | Subject |
|---|---|---|---|
| 1 | (plan) | docs | Onda 2.C (B trim) plan + Task 0 Manual Verification |
| 2 | (T1) | feat | 3-bucket RateLimiter façade per Manual §2.6 (Task 1) |
| 3 | (T2) | feat | Per-operation HTTP 410 mapping per Manual §5.6 vs §6.1/§6.4 (Task 2) |
| 4 | (T3) | feat | HTTP 429 Retry-After parsing per RFC 7231 (Task 3) |
| 5 | (T4) | feat | Outbound.Worker dispatch_upload_error for protocol_expired + rate_limited (Task 4) |
| 6 | (this) | docs | EOS handoff |

Use `git log origin/main..HEAD --oneline` for exact SHAs.

---

## Acceptance criteria — all satisfied

From rollout plan §Sub-PR 2.C (verbatim):

- ✅ **"Three independent buckets enforced and observable via telemetry"** — Task 1 added `:transfers` / `:queries` / `:status_changes` buckets, each independently consumable. `[:sta, :rate_limiter, :acquire]` telemetry event emitted on every acquire (status `:ok` / `:wait`) with `bucket` metadata.
- ✅ **"Protocol auto-renews on 410"** — Task 2 split 410 mapping per-operation; Task 4 wired `dispatch_upload_error/2` → `{:renew_protocol, msg}` → `Outbound.clear_protocol/1` + restart from `:start_upload`.
- ✅ **"429 backoff works without sleeping the caller's process indefinitely"** — Task 3 + Task 4: 429 produces `{:rate_limited, n}`; worker handles via `Process.send_after(self(), {:retry_upload, attempt+1}, n*1000)` — non-blocking, GenServer waits on the message.

Plus (B) trim extras:
- ✅ Manual Verification gate satisfied (Task 0 inventory in plan: 6 [VERIFIED §X.Y] + 5 [STUDY-CONJECTURE] + 2 [NEEDS-OPERATOR-CONFIRM]; HTTP 410 semantic split was the Manual-validated central insight).
- ✅ No out-of-scope file changes (`git diff base..HEAD --name-only | grep -v "^sta/"` empty).
- ✅ Backward-compat preserved: existing `RateLimiter.acquire(name, :query | :concurrent)` calls (operations_test.exs uses both heavily) continue to work as silent aliases.

---

## Test surface delta

### Touchstone files (post-Onda 2.B baseline)

| File | Pre-2.C | Post-2.C | Δ | Notes |
|---|---|---|---|---|
| `client_test.exs` | 86 / 6 | **94 / 6** | +8 | 4 410-per-op + 4 429-Retry-After |
| `operations_test.exs` | 58 / 0 | **58 / 0** | 0 | Backward-compat: `:query`/`:concurrent` aliases work unchanged |
| `outbound_file_test.exs` | 7 / 0 | **7 / 0** | 0 | unchanged |
| `application_test.exs` | 3 / 0 | **3 / 0** | 0 | unchanged |
| `outbound_test.exs` | 38 / 0 | **38 / 0** | 0 | `clear_protocol/1` added but tested via worker_test indirectly |
| `outbound_files_controller_test.exs` | 7 / 0 | **7 / 0** | 0 | unchanged |
| **Touchstone total** | **199 / 6** | **207 / 6** | **+8** | |

### New / extended test files (NOT in original 6 touchstones)

| File | Pre-2.C | Post-2.C | Δ | Source |
|---|---|---|---|---|
| `state_code_test.exs` (Onda 2.B) | 14 / 0 | **14 / 0** | 0 | unchanged |
| `inbound/worker_test.exs` (Onda 2.B) | 3 / 0 | **3 / 0** | 0 | unchanged |
| `outbound/worker_test.exs` (Onda 2.B + 2.C) | 3 / 0 | **8 / 0** | +5 | dispatch_upload_error/2 dispatch tests (Task 4) |
| `sta_query_controller_test.exs` (Onda 2.B) | 16 / 0 | **16 / 0** | 0 | unchanged |
| `rate_limiter_three_bucket_test.exs` (Onda 2.C) | — | **14 / 0** | +14 | NEW (Task 1) |

### Combined Onda 2.C test surface

- **27 new tests across 1 new file + extended 2 prior files**:
  - Task 1 (rate_limiter_three_bucket_test.exs): 14 — bucket constants + canonical-atom + alias + bucket-independence + 12-concurrent property + status shape + telemetry.
  - Task 2 (client_test.exs HTTP 410 describe): 4 — request_protocol/upload_file/upload_part/posicao_upload all → :protocol_expired.
  - Task 3 (client_test.exs HTTP 429 describe): 4 — explicit Retry-After + missing default + HTTP-date warning + negative defensive.
  - Task 4 (outbound/worker_test.exs dispatch describe): 5 — rate_limited delay multiplier + zero edge + protocol_expired renewal + protocol_expired post-renewal fail + catch-all.
- **Combined run** (11 files batch): **262 / 256** — 6 baseline `Soap.parse_response/2` failures preserved (pre-Onda 1).
- **Compile**: 5 baseline warnings preserved across all 4 task commits. The 4 transient warnings introduced in Task 1 (unused defaults after `Queries.max_per_minute()` etc. helpers + `@refill_interval_ms` removed but referenced) were caught and fixed before commit.

---

## Manual Verification gate — execution log

The plan §Task 0 inventory listed:

- **6 `[VERIFIED §X.Y]`** claims — all confirmed via `/tmp/manual_sta.txt` extract:
  - 48h protocol expiry (line 267 of extract).
  - HTTP 410 in §5.6 (line 564) = "protocolo cancelado".
  - HTTP 410 in §6.1 (line 653) and §6.4 (line 738) = "arquivo não disponível para download".
  - §2.6 (line 190) = 10 simultaneous transfers.
  - §2.6 (line 192) = 120 queries/minute.

- **5 `[STUDY-CONJECTURE]`** items — conservative reads applied:
  - #1 RFC 7231 §7.1.3 Retry-After form fallback to 60s default (Task 3).
  - #2 status_changes 10/min conservative invention (Task 1 `RateLimiter.StatusChanges`).
  - #3 `:request_protocol` joins `:transfers` bucket (over-restrict if Manual writers intended otherwise).
  - #4 inner-Client check kept as pre-flight (smaller blast radius — implemented as planned).
  - #5 `:protocol_expired` fan-out across all 4 upload-side operations (Task 2 4 clauses).

- **2 `[NEEDS-OPERATOR-CONFIRM]`** items — both addressed:
  - #1 `:status_changes` 10/min cap → conservative invention; document as `[STUDY-CONJECTURE]` in module + plan; expose env-tunable hook for operators if data emerges.
  - #2 Worker protocol-renewal cap = 1 → implemented; `protocol_renewed: false → true` flag in State struct.

**Misdiagnosis caught**: the central insight from the Manual extract — HTTP 410 carries DIFFERENT meanings on upload vs. download paths (§5.6 line 564 vs. §6.1 line 653 / §6.4 line 738) — was Manual-validated, not invented. Per-operation 410 mapping is the correct architecture, not a workaround.

---

## Public API additions / changes (cheat sheet)

### `StaConnector.Sta.RateLimiter`

```elixir
# New canonical bucket atoms:
RateLimiter.acquire(name, :transfers)       # was :concurrent
RateLimiter.acquire(name, :queries)         # was :query
RateLimiter.acquire(name, :status_changes)  # NEW

# Backward-compat aliases (silent — no deprecation warning):
RateLimiter.acquire(name, :query)
RateLimiter.acquire(name, :concurrent)

# status/1 returns extended map with both flat fields AND nested bucket sub-maps:
%{
  query_tokens: int, query_max: int,            # backward-compat flat
  concurrent_slots: int, concurrent_max: int,    # backward-compat flat
  waiting_count: int, config: map,
  transfers: %{slots: int, max: int},            # canonical
  queries: %{tokens: int, max: int},             # canonical
  status_changes: %{tokens: int, max: int}       # canonical
}
```

### `StaConnector.Sta.Client` error atoms

| HTTP Status | Operation | Error |
|---|---|---|
| 410 | `:request_protocol` | **NEW** `:protocol_expired` |
| 410 | `:upload_file` | **NEW** `:protocol_expired` |
| 410 | `:upload_part` | **NEW** `:protocol_expired` |
| 410 | `:posicao_upload` | **NEW** `:protocol_expired` |
| 410 | `:download_part` | `:file_unavailable` (unchanged) |
| 410 | `:download_file_streamed` | `:file_unavailable` (unchanged) |
| 429 | (any operation) | **NEW** `{:rate_limited, seconds}` |

### `StaConnector.Outbound`

```elixir
@spec clear_protocol(String.t()) :: {:ok, OutboundFile.t()} | {:error, term()}
def clear_protocol(file_id)
# Resets protocol_number=nil + bytes_uploaded=0 + status="pending".
# Called by Outbound.Worker when handling :protocol_expired auto-renewal.
```

### `StaConnector.Outbound.Worker`

```elixir
@spec dispatch_upload_error(error :: term(), state :: %State{}) ::
        {:retry_after, non_neg_integer()}
        | {:renew_protocol, String.t()}
        | {:fail, String.t()}
def dispatch_upload_error(error, state)
# Pure function. Routes Client errors onto Worker actions.
```

State struct grew `protocol_renewed: false` (cap = 1 renewal per worker lifetime).

---

## Files touched (10 total — all in `sta/`)

```
sta/backend/lib/sta_connector/sta/rate_limiter.ex                          (modified, ~+90)
sta/backend/lib/sta_connector/sta/rate_limiter/transfers.ex                (NEW, 18 LOC)
sta/backend/lib/sta_connector/sta/rate_limiter/queries.ex                  (NEW, 24 LOC)
sta/backend/lib/sta_connector/sta/rate_limiter/status_changes.ex           (NEW, 28 LOC)
sta/backend/lib/sta_connector/sta/client.ex                                (modified, ~+50)
sta/backend/lib/sta_connector/sta/client_behaviour.ex                      (modified, ~+10)
sta/backend/lib/sta_connector/outbound.ex                                  (modified, ~+20 clear_protocol)
sta/backend/lib/sta_connector/outbound/worker.ex                           (modified, ~+90)
sta/backend/test/sta_connector/sta/client_test.exs                         (modified, ~+200)
sta/backend/test/sta_connector/sta/rate_limiter_three_bucket_test.exs      (NEW, ~150 LOC)
sta/backend/test/sta_connector/outbound/worker_test.exs                    (modified, ~+60)
sta/docs/plans/2026-04-27-sta-onda2c-rate-and-lifecycle-trim.md            (NEW plan, 496 LOC)
sta/docs/handoff/2026-04-27-sta-onda2c-trim-complete.md                    (NEW handoff)
```

Use `git diff $(git merge-base origin/main HEAD)..HEAD --stat` for exact line counts.

---

## Out of scope (deferred per (B) trim + plan)

1. **`OutboundFile.protocol_expires_at` field + migration** — proactive 48h tracking. Reactive 410 handling in this PR already covers BCB-side correctness. Defer until an admin-UI consumer actually exists.
2. **Inbound.Worker `:rate_limited` handling** — inbound has its own RetryEntry-based retry budget; layering 429 sleep is a separate concern.
3. **Operations.upload_with_retry/4 retry-loop refactor** — today the loop has its own retry budget per attempt, blind to `:protocol_expired`/`:rate_limited`. Worker test covers the dispatch. Operations refactor is its own hygiene PR.
4. **Removing the inner-Client rate check** ([STUDY-CONJECTURE] #4) — kept as pre-flight; removing is a separate hygiene PR.
5. **GenServer integration tests for Outbound.Worker** — pure-helper coverage only per plan §Out of scope #11.
6. **Onda 2.D** — `/senhaws/senha` password management (next sub-PR).
7. **Onda 3** — leiautes corpus.
8. **Onda 4** — identifier mapping in `core/`.
9. **Real BCB smoke test** — operator post-merge.
10. **Telemetry metric definitions** — Wave 11 P2 metrics framework will compose them from the events emitted in Task 1.
11. **Admin auth enforcement** — `:admin_api` pipeline still has the `# Future: Add admin authentication here` TODO.
12. **`:status_changes` env-tunable cap** — module constant for now; expose `Application.get_env` if operator data emerges.
13. **`download_file/410 → :file_unavailable` explicit clause** — today falls through to `Error.from_http_status/2`. Standardizing is a behavior change for callers expecting `%Error{}` shape; deferred.

---

## Hard rules respected (carry-over)

| # | Rule | Verified |
|---|---|---|
| 1 | STA scope only | ✅ `git diff base..HEAD --name-only` empty when filtered for `^sta/` |
| 2 | Never modify `core/`, `pix/`, `spb/`, `npc/`, `clst/`, `backoffice/`, `mobile/`, `infra/`, root `docs/`, root `MEMORY.md`, root `CLAUDE.md` | ✅ |
| 3 | `mix format` ONLY on touched files | ✅ |
| 4 | Run tests per-file, never full suite | ✅ |
| 5 | Pattern: Manual Verification mandatory | ✅ Task 0 in plan; Manual extract validated all wire-format claims |
| 6 | Push + PR só com auth explícita do usuário | ⏳ pending — this handoff serves as the request |
| 7 | Worktree branch policy: cut new branch off `origin/main` | ✅ branch cut from `67924840` |
| 8 | Decision policy when ambiguous | ✅ Plan §Component 1 documents the inner-Client-check decision; §Component 4 documents the renewal cap |

---

## Resumption checklist (for next session)

```bash
cd /Users/luizpenha/monetarie/.worktrees/sta-paridade
pwd                                                 # → /Users/luizpenha/monetarie/.worktrees/sta-paridade
git fetch origin --prune
git status                                          # working tree should be clean

# After this PR merges:
git checkout --detach origin/main
git branch -d sta/onda2-rate-and-lifecycle          # cleanup local
git log --oneline -1                                # → merge commit

cd sta/backend
mix compile 2>&1 | grep -c "^[[:space:]]*warning:"  # → 5

# Touchstones — combined batch (fastest):
mix test \
  test/sta_connector/sta/client_test.exs \
  test/sta_connector/sta/operations_test.exs \
  test/sta_connector/files/outbound_file_test.exs \
  test/sta_connector/application_test.exs \
  test/sta_connector/outbound_test.exs \
  test/sta_connector_web/controllers/api/admin/outbound_files_controller_test.exs \
  test/sta_connector/sta/state_code_test.exs \
  test/sta_connector/inbound/worker_test.exs \
  test/sta_connector/outbound/worker_test.exs \
  test/sta_connector_web/controllers/api/admin/sta_query_controller_test.exs \
  test/sta_connector/sta/rate_limiter_three_bucket_test.exs
# Expected post-merge: 262 tests, 6 failures (the 6 pre-existing Soap.parse_response baseline failures)
```

---

## Decision tree for next session

### State A — This PR not yet merged

Branch is `sta/onda2-rate-and-lifecycle`. Either wait for merge OR work in parallel on a non-overlapping sub-PR (e.g., **Onda 2.D password mgmt** — touches `senhaws/*` URL-host config + new `Client.password_*` methods, not the rate-limiter or worker code added here).

### State B — This PR merged + worktree on detached HEAD

Cut new branch off `origin/main` for whichever sub-PR is next. Default suggested: **Onda 2.D `sta/onda2-password-mgmt`** per parent rollout plan §Sub-PR 2.D (lines 774-799). 1 day, ~300 LOC. Independent of 2.C — uses different host (www9/www3 BCB).

### State C — Other sub-PR started in another session / partial

Unlikely but check `gh pr list --state open --search "sta/"` first.

---

## Suggested next sub-PR (post-merge)

**Onda 2.D `sta/onda2-password-mgmt`** per parent rollout plan §Sub-PR 2.D (lines 774-799). 1 day, ~300 LOC. Adds `Client.password_change/2` (`PUT /senhaws/senha`), `Client.password_expiry/0` (`GET /senhaws/senha/vencimento`), and an Oban cron worker to notify on ≤7 days remaining. Uses **separate base URL** (`www9.bcb.gov.br/senhaws` homol / `www3.bcb.gov.br/senhaws` prod). Operator deadline-driven if BCB password rotation is impending.

Alternative: **Onda 3.A leiautes corpus + XSD validator** if Compliance has the cert source confirmed. Independent of 2.C.

Alternative deferred from 2.C: a **2.C-2 follow-up** wiring proactive `protocol_expires_at` field + admin UI surface, when an admin-UI consumer is ready to read it.

---

## References

### This PR
- **This handoff:** `sta/docs/handoff/2026-04-27-sta-onda2c-trim-complete.md`
- **This plan:** `sta/docs/plans/2026-04-27-sta-onda2c-rate-and-lifecycle-trim.md`
- **Branch:** `sta/onda2-rate-and-lifecycle`
- **Base SHA:** `67924840` (origin/main HEAD at session start, post Onda 2.B merge PR #62)

### Predecessor handoff
- `sta/docs/handoff/2026-04-27-sta-onda2b-full-admin-complete.md` — Onda 2.B (C) status & queries.

### Plans
- **Parent rollout plan:** `sta/docs/plans/2026-04-26-sta-conformance-rollout.md` §Sub-PR 2.C (lines 739-770), §Pattern: Manual Verification.
- **Onda 2.B plan (template):** `sta/docs/plans/2026-04-27-sta-onda2b-status-and-queries-full-admin.md`.

### Manual STA references
- `sta/Manual_STA_Web_Services.pdf` — v1.5 jul/2022.
- §2.6 Limites de conexões (lines 183+ in extract).
- §5 intro (line 267) — 48h protocol expiry.
- §5.6 errors table (line 564) — 410 = "protocolo cancelado".
- §6.1 errors table (line 653), §6.4 errors table (line 738) — 410 = "arquivo não disponível".

### RFCs
- RFC 6585 §4 — HTTP 429 status code.
- RFC 7231 §7.1.3 — Retry-After header (delta-seconds OR HTTP-date).

### Onda 2.A + 2.B + 2.C merged PRs (cross-reference)
| PR | SHA | Title |
|---|---|---|
| #24 | `301503ec` | Onda 1 wire-format + ICP-Brasil chain |
| #30 | `3af17964` | Onda 2.A core |
| #34 | `ed03ce39` | Worker chunked-upload wiring |
| #36 | `100e5154` | Operations.resume_upload/3 |
| #43 | `99e6e57c` | Streaming download + ranged-resume mid-download |
| #46 | `1c10b7fa` | Caller-wiring bundle |
| #52 | `fbf4dac2` | upload_attempts retry-cap fix |
| #62 | `67924840` | Onda 2.B status & queries (C full+admin) |

---

## Awaiting auth

Per Hard rule #6, no `git push` or `gh pr create` without explicit user authorization. When ready:

```bash
cd /Users/luizpenha/monetarie/.worktrees/sta-paridade
git push -u origin sta/onda2-rate-and-lifecycle
gh pr create --title "feat(sta): Onda 2.C rate & lifecycle (B trim)" --body "..."
```

---

**End of handoff.** All Onda 2.C (B) deliverables complete; awaiting push + PR authorization.
