bug_bunny 4.18.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 030517babaaafccb4ac6b067e1c11ac1cf6d4a6a711919c8455c3dcfe6a79637
4
- data.tar.gz: fb9a551e05882e05efc3cd23215773507ae6d5d2baab70681360c04100724d29
3
+ metadata.gz: e1076566b6a3b8c25f6f00fc624c92681e8db881ffc3b8d6640da3bb7158333b
4
+ data.tar.gz: 2510fe871ec6f32804dba3faa7f1110408656e8cff30d2c0d537c7d0b30e1686
5
5
  SHA512:
6
- metadata.gz: a5c4fbaade4883c361a20e705155d2b60dc6b3e6e64905afe34b02a7a7016cd7913bfd9f1f2de09979d357f72b026dbdc3129ad871d128a1b9ee901628a096b6
7
- data.tar.gz: fb7b8dd6f7b74dee3c6d13b789dd07ff3abaeda535680a1ab7ea4276269e40a238588403e5fd7c8d2cdb3188acd3b7889d867d4dffed6a19af45d91eefa68591
6
+ metadata.gz: 13525491f89b99f1e1be12663f64990109e17b9da22e034ecea463a3fda5ff68abfb3ab10fdf274d43df5460b3c74eabd14e1699e2af7f8c38e608a78e22cb69
7
+ data.tar.gz: 9f31c6ed9fbfff650e2770f970ab94e49979f8eec8b254c212817ca410106b4f4293cc9b6cde87f382145a2483f639aabe0998134c5656b7ec6b053aebcb7c73
data/AGENTS.md ADDED
@@ -0,0 +1,43 @@
1
+ # AGENTS.md — bug_bunny
2
+
3
+ Entrada de harness para agentes que trabajan en este repo. Para el contrato de
4
+ proyecto y convenciones de equipo, ver `CLAUDE.md`. Para la entrada humana, ver
5
+ `README.md`; para la entrada agente version-locked, `skill/SKILL.md`.
6
+
7
+ ## Mapa de conocimiento
8
+
9
+ > Stanza RFC-008 r2 — **indexa, no duplica**. Cada fila apunta al artefacto de
10
+ > detalle que es la fuente de verdad de esa capa. Leé el artefacto antes de
11
+ > responder sobre su capa.
12
+
13
+ | capa | artefacto | estado | qué responde |
14
+ |---|---|---|---|
15
+ | Comportamiento | `docs/behavior/behavior.md` | completo (6 flujos) | secuencias de publish/RPC/consume/confirms, contrato de error-wrapping |
16
+ | Glosario | `docs/glossary/glossary.md` | parcial (acreta por PR) | término de negocio → binding físico en `lib/` |
17
+ | Errores | `docs/errors/errors.md` | completo (§a/b/d estructura + §c política inferida) | jerarquía de excepciones públicas, mapeo `status→excepción`, shape del payload, política retry |
18
+ | Configuración | `docs/config/configuracion.md` | completo (estructura + enrich §f/g/h) | opciones de `Configuration`, defaults, failure-mode/threading, inyecciones del `Railtie`, ENV sugeridas |
19
+ | Dependencias consumidas | `docs/consumed/rabbitmq.md` | completo (estructura + enrich §c/e) | qué consume del broker RabbitMQ vía `bunny`, mapeo error-Bunny→excepción, retry/degradación |
20
+ | Test | `docs/test/test.md` | completo (estructura + enrich §e-h) | suites RSpec/Minitest, CI, contract-assessment, link a incidentes (#49/#52) |
21
+ | Release | `docs/release/release.md` | completo | patrón gema-tag, versionado, publish a RubyGems |
22
+ | Datos | — | n/a | gema sin DB |
23
+ | Operaciones / Interfaz / Topología | — | dev-structure F2 no implementado | contrato embebido en `README.md`/`skill/SKILL.md` (interim RFC-008 §2) |
24
+ | Eventos (RFC-005) | — | n/a | la gema es el transporte de eventos, no declara un catálogo de eventos de dominio propio |
25
+ | Seguridad (RFC-017) | — | n/a | sin authn/authz propias; el único control es el guard anti-RCE (clase enrutada debe heredar de `BugBunny::Controller`, si no → 403), cubierto en `docs/errors/` |
26
+ | Multi-tenancy (RFC-023) | — | n/a | sin modelo de tenant propio; el aislamiento es por `vhost` de RabbitMQ (config) |
27
+ | Data-lifecycle (RFC-026) | — | n/a | gema sin DB ni datos persistidos propios |
28
+
29
+ ## Enriquecimiento
30
+
31
+ Completo en errors (§c), config (§f/g/h), consumed (§c/e) y test (§e-h), anclado
32
+ a YARD/specs/CHANGELOG. Pendiente de **verificación humana** (inferencias):
33
+
34
+ - `docs/errors/errors.md` §c — política retry inferida de HTTP/AMQP (`confidence:medium`); confirmar idempotencia de re-publish y caso `RemoteError` con consumidores reales.
35
+ - `docs/glossary/glossary.md` — parcial por diseño, acreta por PR.
36
+
37
+ ## Convenciones operativas
38
+
39
+ - Ruby: `.ruby-version` · gemas: `Gemfile.lock` · manager: chruby + Bundler.
40
+ - Antes de commitear: `bundle exec rubocop -a` (base `rubocop-rails-omakase`),
41
+ `bundle exec rspec`, YARD incremental (`bundle exec yard stats --list-undoc`).
42
+ - Releases: `/gem-release` (el GitHub Action publica a RubyGems al pushear tag `v*`).
43
+ - Doc por-PR: si tocás una capa, mové su artefacto en el mismo PR (régimen RFC-001).
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.0.0] - 2026-07-01
4
+
5
+ > **BREAKING.** Se elimina la constante pública `BugBunny::SecurityError`. Aunque la excepción nunca se *levantaba*, su **ausencia rompe en evaluación**: un `rescue BugBunny::SecurityError` en un consumidor resuelve la constante cuando *cualquier* excepción entra a ese bloque → `NameError` que enmascara la excepción original. Por eso es breaking real, no inerte.
6
+
7
+ ### Removed
8
+ - **`BugBunny::SecurityError` eliminada:** introducida en `4f27bea` ("security controller") como la excepción prevista del control anti-RCE, pero **nunca se cableó** — ninguna ruta de código la levanta. La protección real (la clase enrutada debe heredar de `BugBunny::Controller`) vive en `Consumer` (`consumer.rb:222-228`) y responde **403 Forbidden** + reject + log `consumer.security_violation`, no una excepción. Eliminar la clase **no debilita** la protección (el guard 403 queda intacto). Doc ajustada (`docs/errors/`, README, skill). — @Gabriel
9
+
10
+ **Migración para consumidores:** quitar cualquier `rescue BugBunny::SecurityError`; el caso de controlador inválido llega como respuesta **403** en el envelope RPC (mapeable a `BugBunny::ClientError`/manejo de status), no como excepción local.
11
+
12
+ **Alcance verificado:** el grep que confirmó "nunca levantada" fue **in-repo** (lib + spec + test de esta gema). No se auditaron apps/gemas consumidoras externas — el bump MAJOR es la salvaguarda para ellas.
13
+
14
+ ### Correcciones
15
+ - **CI: el workflow disparaba en `push` a `master`, pero la rama por default es `main` (#56):** el job de tests (Ruby 3.4.4) nunca corría en push a `main`, solo entraba por `pull_request`. Apuntado a `main`. — @Gabriel
16
+
17
+ ### Documentación
18
+ - **Cobertura de arquitectura completa (dev-* / RFC-artefacto):** nuevas capas `docs/errors/` (RFC-020), `docs/config/` (RFC-012), `docs/consumed/` (RFC-018) y `docs/test/` (RFC-013), con enriquecimiento semántico (política de errores, retry/degradación, threading, contract-assessment); `docs/release/` re-anclado a RFC-014 `accepted`; `AGENTS.md` con stanza "Mapa de conocimiento". Empaquetado version-locked en la gema (#54, #55, #57). — @Gabriel
19
+
20
+ ## [4.19.0] - 2026-06-25
21
+
22
+ > Capa de errores transport-agnostic (#52). Cambio **aditivo y retrocompatible**: `.message` no se degrada y se suman dos accesores. Bump minor.
23
+
24
+ ### Nuevas funcionalidades
25
+ - **`status` + `raw_response` uniformes en toda la jerarquía (#52):** subidos a la base `BugBunny::Error` (`attr_accessor`) y poblados por `RaiseError.on_complete` en **todas** las clases de error, no solo `UnprocessableEntity`. El consumidor accede a `e.status` / `e.raw_response` de forma uniforme; la gema queda agnóstica al payload y el boundary del servicio interpreta el envelope de dominio. — @Gabriel
26
+
27
+ ### Correcciones
28
+ - **`.message` ya no vuelca un `Hash#inspect` con el envelope anidado (#52):** ante `{ error: { code, message, details } }`, `format_error_message` extrae `error.message` best-effort en vez de interpolar el Hash; mantiene el shape plano histórico (incluido `error: ""`) y cae a JSON. Es solo el string humano para logs/Sentry, no contrato. — @Gabriel
29
+
30
+ ### Documentación
31
+ - `skill/references/errores.md` y `skill/SKILL.md`: nueva sección "Materia prima del error" (`status`/`raw_response`), formato de `.message` endurecido y advertencia de sanitización (no loguear `raw_response` crudo). — @Gabriel
32
+ - Piloto RFC-014: `docs/release/release.md` (suplemento de release de la gema) (#51) — @Gabriel
33
+
3
34
  ## [4.18.0] - 2026-05-26
4
35
 
5
36
  > Behavior change menor: cualquier `Bunny::Exception` que escape en la frontera del gem (TCP fail, conn rota, canal cerrado, auth fail) ahora se envuelve como `BugBunny::CommunicationError`. La excepción original queda accesible vía `.cause`. Callers que rescatan `Bunny::TCPConnectionFailed` directo deben migrar a `BugBunny::CommunicationError`. Callers que ya rescatan `BugBunny::Error` / `CommunicationError`: sin cambios.
data/CLAUDE.md CHANGED
@@ -13,10 +13,20 @@ BugBunny es una gema Ruby que implementa una capa de enrutamiento RESTful sobre
13
13
  `skill/SKILL.md` agente version-locked) **indexan, no duplican**.
14
14
  Artefactos generados por `dev-structure` / `dev-enrich`; compuestos por
15
15
  `dev-compose`. Verificación humana antes de commitear.
16
- - **Estado actual:** `docs/data` = n/a (gema sin DB, declarado solo en índice);
17
- `docs/glossary` parcial (acreta por PR); `docs/behavior` completo (6 flujos,
18
- backfill on-demand); operaciones/interfaz/topología = dev-structure F2 no
19
- implementado.
16
+ - **Estado actual:**
17
+ - `docs/data` = n/a (gema sin DB, declarado solo en índice).
18
+ - `docs/behavior` completo (6 flujos, backfill on-demand).
19
+ - `docs/glossary` parcial (acreta por PR).
20
+ - `docs/errors` (RFC-020) completo: §a/§b/§d (estructura) + §c política
21
+ (enrich, **inferida** de HTTP/AMQP, verificación humana pendiente).
22
+ - `docs/config` (RFC-012) completo: inventario §a-§e/§i + enrich §f/§g/§h
23
+ (failure-mode/threading, anclado a YARD); §j n/a.
24
+ - `docs/consumed` (RFC-018) RabbitMQ-vía-`bunny` completo: §a/§b/§d + enrich
25
+ §c/§e (retry/backoff/degradación, anclado a `consumer.rb`).
26
+ - `docs/test` (RFC-013) completo: estructura §a-§d + enrich §e-§h
27
+ (contract-assessment, link a incidentes #49/#52, gaps de CI).
28
+ - operaciones/interfaz/topología (RFC-003/004/006) = dev-structure F2 no
29
+ implementado (interim RFC-008 §2).
20
30
  - **Para agentes AI**: `skill/SKILL.md` (empaquetada en el `.gem`) +
21
31
  `skill/references/`.
22
32
  - **Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de
data/README.md CHANGED
@@ -379,7 +379,6 @@ BugBunny maps RabbitMQ responses to a semantic exception hierarchy, similar to h
379
379
  BugBunny::Error
380
380
  ├── CommunicationError (wraps any Bunny::Exception — TCP/auth/channel — at the gem boundary; .cause preserves the original)
381
381
  ├── ConfigurationError (invalid config attribute)
382
- ├── SecurityError (unauthorized controller resolution)
383
382
  ├── PublishNacked (broker basic.nack on :confirmed publish)
384
383
  ├── PublishUnroutable (broker basic.return on mandatory + :confirmed)
385
384
  ├── ClientError (4xx)
@@ -0,0 +1,172 @@
1
+ # Configuración — bug_bunny
2
+
3
+ > meta: artefacto configuración · RFC-012 · generado `arch-structure` (inventario
4
+ > §a-§e/§i) + `arch-enrich` (§f/§g/§h/§j) · anclado a `24ea397`,
5
+ > `lib/bug_bunny/configuration.rb`, `lib/bug_bunny.rb`, `lib/bug_bunny/railtie.rb`,
6
+ > `lib/bug_bunny/consumer.rb`,
7
+ > `lib/generators/bug_bunny/install/templates/initializer.rb` · fecha 2026-06-30
8
+ > · cobertura: §a-§e/§i (estructura) + §f/§g/§h (enrich, anclado a YARD) completas;
9
+ > §j n/a.
10
+
11
+ ## 1. Resumen
12
+
13
+ Gema configurable vía `BugBunny.configure { |c| ... }` (`bug_bunny.rb:59`) sobre
14
+ la clase `Configuration` (`configuration.rb`): atributos con **code-defaults**
15
+ seguros, validados por `validate!` al cierre del bloque. La gema **no lee `ENV[`
16
+ en `lib/`**; el `ENV.fetch` vive en el **template** que sugiere al consumidor
17
+ (`initializer.rb`). Inyecta al host vía `Railtie`.
18
+
19
+ ## 2. Cuerpo
20
+
21
+ ### a. Hecho verificable
22
+
23
+ - **Total opciones (`Configuration`):** 29 (attr_accessor/reader).
24
+ - **Validadas requeridas (`VALIDATIONS`, `configuration.rb:25-37`):** 5 (`host`,
25
+ `port`, `username`, `password`, `vhost` — `required: true`; igual tienen
26
+ default, `validate!` exige no-vacío).
27
+ - **Con default:** 29 (todas; defaults en `initialize`/`init_callback_defaults`).
28
+ - **Con rango validado:** 7 (`port`, `heartbeat`, `connection_timeout`,
29
+ `read_timeout`, `write_timeout`, `rpc_timeout`, `channel_prefetch`).
30
+ - **Secretas (por nombre):** 1 (`password`).
31
+ - **ENV leídas por la gema en `lib/`:** 0 (config 100% por code-default + bloque).
32
+
33
+ ### b. Inventario base
34
+
35
+ Origen `code-default` salvo nota. Consumidor = `configuration.rb` (default en
36
+ `initialize`) salvo indicado.
37
+
38
+ | nombre | tipo | requerida | default | origen | consumidor | secret? |
39
+ |---|---|---|---|---|---|---|
40
+ | `host` | String | sí (validate!) | `'127.0.0.1'` | code-default | `configuration.rb:182` | no |
41
+ | `port` | Integer | sí (validate!, 1..65535) | `5672` | code-default | `:183` | no |
42
+ | `username` | String | sí (validate!) | `'guest'` | code-default | `:184` | no |
43
+ | `password` | String | sí (validate!) | `'guest'` | code-default | `:185` | **sí** |
44
+ | `vhost` | String | sí (validate!) | `'/'` | code-default | `:186` | no |
45
+ | `logger` | Logger | no | `Logger.new($stdout)` INFO | code-default | `:188` | no |
46
+ | `bunny_logger` | Logger | no | `Logger.new($stdout)` WARN | code-default | `:191` | no |
47
+ | `automatically_recover` | Boolean | no | `true` | code-default | `:193` | no |
48
+ | `network_recovery_interval` | Integer | no | `5` | code-default | `:194` | no |
49
+ | `max_reconnect_attempts` | Integer/nil | no | `nil` (reintenta ∞) | code-default | `:195` | no |
50
+ | `max_reconnect_interval` | Integer | no | `60` | code-default | `:196` | no |
51
+ | `connection_timeout` | Integer | no (1..300) | `10` | code-default | `:197` | no |
52
+ | `read_timeout` | Integer | no (1..300) | `30` | code-default | `:198` | no |
53
+ | `write_timeout` | Integer | no (1..300) | `30` | code-default | `:199` | no |
54
+ | `heartbeat` | Integer | no (0..3600) | `15` | code-default | `:200` | no |
55
+ | `continuation_timeout` | Integer (ms) | no | `15000` | code-default | `:201` | no |
56
+ | `channel_prefetch` | Integer | no (1..10000) | `1` | code-default | `:202` | no |
57
+ | `rpc_timeout` | Integer | no (1..3600) | `10` | code-default | `:203` | no |
58
+ | `health_check_interval` | Integer | no | `60` | code-default | `:204` | no |
59
+ | `health_check_file` | String/nil | no | `nil` (desactivado) | code-default | `:207` | no |
60
+ | `controller_namespace` | String | no | `'BugBunny::Controllers'` | code-default | `:210` | no |
61
+ | `log_tags` | Array | no | `[:uuid]` | code-default | `:212` | no |
62
+ | `exchange_options` | Hash | no | `{}` | code-default | `:215` | no |
63
+ | `queue_options` | Hash | no | `{}` | code-default | `:216` | no |
64
+ | `consumer_middlewares` | Stack (attr_reader) | no | `Stack.new` | code-default | `:218` | no |
65
+ | `rpc_reply_headers` | Proc/nil | no | `nil` | code-default | `:251` | no |
66
+ | `on_rpc_reply` | Proc/nil | no | `nil` | code-default | `:252` | no |
67
+ | `on_return` | Proc/nil | no | `nil` | code-default | `:253` | no |
68
+ | `nack_raise` | Boolean | no | `true` | code-default | `:254` | no |
69
+ | `return_raise` | Boolean | no | `true` | code-default | `:255` | no |
70
+
71
+ > **Override por request:** `nack_raise` y `return_raise` se sobreescriben por
72
+ > llamada con `nack_raise:` / `return_raise:` en `Client#publish` (scope-override
73
+ > → detalle a `arch-enrich`).
74
+
75
+ ### c. Meta-templates
76
+
77
+ El install template (`initializer.rb`) **sugiere al consumidor** wirear 4 ENV
78
+ (la gema no las lee — el consumidor las pasa al bloque `configure`):
79
+
80
+ | plantilla | template | instancias |
81
+ |---|---|---|
82
+ | `RABBITMQ_{X}` → `config.{y}` | `ENV.fetch('RABBITMQ_{X}', '{default}')` | `RABBITMQ_HOST`→host (`'localhost'`), `RABBITMQ_USERNAME`→username (`'guest'`), `RABBITMQ_PASSWORD`→password (`'guest'`), `RABBITMQ_VHOST`→vhost (`'/'`) |
83
+
84
+ > `spec/spec_helper.rb` usa `RABBITMQ_HOST` / `RABBITMQ_USER` / `RABBITMQ_PASS`
85
+ > (nombres distintos al template) — convención de test, no contrato.
86
+
87
+ ### d. Derivaciones simples
88
+
89
+ - `url` ← `"amqp://#{username}:#{password}@#{host}:#{port}/#{vhost}"`
90
+ (`configuration.rb:225`).
91
+ - `create_connection(**options)` ← `merge_connection_options(options)` sobre la
92
+ config global; las options explícitas pisan los defaults (`bug_bunny.rb:88`).
93
+
94
+ ### e. Scheduling
95
+
96
+ **n/a** — la gema no trae `sidekiq.yml`/`queue.yml`/`recurring.yml` ni cron.
97
+ `health_check_interval` es polling interno de salud, no un scheduler.
98
+
99
+ ### i. Inyecciones al host (`Railtie`, `railtie.rb`)
100
+
101
+ | inyección | qué hace | ancla |
102
+ |---|---|---|
103
+ | `initializer 'bug_bunny.add_autoload_paths'` | registra `app/rabbit` en el autoloader (Zeitwerk, `eager_load: true`) si existe | `:15-18` |
104
+ | `config.after_initialize` → `ForkTracker.after_fork` | cierra la conexión heredada en cada fork (Rails 7.1+) | `:24-26` |
105
+ | `config.after_initialize` → `Puma.events.on_worker_boot` | mismo cierre, hook legacy Puma <5 | `:30-34` |
106
+ | `rake_tasks` | carga `tasks/bug_bunny.rake` (`bug_bunny:sync`) | `:38-40` |
107
+ | `Spring.after_fork` | cierra conexión en preloader Spring | `:43-47` |
108
+
109
+ ### f. Enriquecimiento semántico
110
+
111
+ Agrupado por familia. Anclado a los YARD de `configuration.rb` y al comportamiento
112
+ del código.
113
+
114
+ | familia | categoría | failure-mode | side-effect | business-reason |
115
+ |---|---|---|---|---|
116
+ | Conexión (`host`/`port`/`username`/`password`/`vhost`) | conectividad | valor inválido/vacío → `ConfigurationError` en `validate!`; credencial/host errados → `CommunicationError` al conectar (`bug_bunny.rb:95`) | abre socket TCP al broker | identidad y destino del broker; `vhost` aísla ambientes |
117
+ | Timeouts (`connection_timeout`/`read_timeout`/`write_timeout`/`heartbeat`/`continuation_timeout`) | resiliencia/latencia | muy bajo → cortes espurios bajo carga; muy alto → detección de fallo lenta | — | tuning de la conexión Bunny; `heartbeat` detecta conexiones zombi |
118
+ | `rpc_timeout` | latencia | el worker remoto no responde a tiempo → `RequestTimeout` (`producer.rb:124,214`) | bloquea el hilo llamante hasta el timeout | techo de espera de un RPC síncrono |
119
+ | Resiliencia (`automatically_recover`/`network_recovery_interval`/`max_reconnect_attempts`/`max_reconnect_interval`) | resiliencia | `max_reconnect_attempts` agotado → el Consumer re-levanta y muere (`consumer.rb:110-112`) | reintentos con **backoff exponencial** `network_recovery_interval * 2^(n-1)` cap `max_reconnect_interval` (`consumer.rb:115-118`) | sobrevivir caídas transitorias del broker sin perder el worker |
120
+ | QoS (`channel_prefetch`) | rendimiento | alto → un worker lento acapara mensajes; `1` → menor throughput | controla unacked in-flight (backpressure) | balancea fairness vs throughput (default `1` = fair round-robin) |
121
+ | Health (`health_check_interval`/`health_check_file`) | observabilidad | `health_check_file` no escribible → el touch falla (degradación de visibilidad, no del flujo) | **escribe (touch) un archivo** en cada health check OK; `nil` desactiva | probe para orquestadores (K8s/Swarm) |
122
+ | Callbacks (`on_return`/`on_rpc_reply`/`rpc_reply_headers`) | extensibilidad | una excepción en `on_return` se captura pero **degrada visibilidad** (YARD `configuration.rb:147`) | corren en hilos sensibles (ver §h) | propagar trace-context / alertar unroutable |
123
+ | Confirms (`nack_raise`/`return_raise`) | integridad de entrega | `false` → NACK/return solo se logea, la llamada retorna `202` (modo legacy, posible pérdida silenciosa) | habilitan el raise de `PublishNacked`/`PublishUnroutable` | elegir entre fail-fast vs best-effort en publish confirmado |
124
+ | Routing (`controller_namespace`) | seguridad | clase resuelta no subclase de `BugBunny::Controller` → el worker responde **403** + reject (guard anti-RCE, `consumer.rb:222-228`) | acota qué clases son enrutables | superficie de control de RCE |
125
+ | Logging (`logger`/`bunny_logger`/`log_tags`) | observabilidad | — | salida a `$stdout` por default | trazabilidad estructurada |
126
+ | Infra (`exchange_options`/`queue_options`) | infraestructura | options incompatibles con el broker → `PreconditionFailed` (vía `CommunicationError`) | defaults globales mergeados por recurso | declaración AMQP por default |
127
+
128
+ ### g. Ramificadores intra-config
129
+
130
+ - `health_check_file = nil` (default) **desactiva** el touchfile aunque
131
+ `health_check_interval` siga corriendo (`configuration.rb:99-101,207`).
132
+ - `return_raise` es **inerte cuando `mandatory: false`** — sin `mandatory` el
133
+ broker nunca retorna, así que el flag no tiene efecto (`configuration.rb:171`).
134
+ - `nack_raise`/`return_raise` se sobreescriben **por request** (`Client#publish`),
135
+ ganando sobre el valor global (scope-override).
136
+
137
+ ### h. Threading
138
+
139
+ | opción | hilo de ejecución | restricción |
140
+ |---|---|---|
141
+ | `on_return` | **hilo interno del consumidor de Bunny** (`configuration.rb:147`) | debe ser rápido y no lanzar; BugBunny captura, pero degrada visibilidad |
142
+ | `on_rpc_reply` | **hilo llamante** tras recibir el reply RPC (`configuration.rb:132`) | hidrata trace-context en el publisher |
143
+ | `rpc_reply_headers` | hilo del consumer, justo antes del `basic_publish` del reply (`configuration.rb:125`) | debe retornar un Hash de headers |
144
+ | reconexión del Consumer | hilo del `subscribe` loop (`consumer.rb:90,122`) | `sleep wait` bloquea ese hilo durante el backoff |
145
+
146
+ ### j. Inyección a gemas configuradas
147
+
148
+ **n/a** — la gema **no** configura otras gemas vía bloque `Gema.configure` en
149
+ initializers; expone su propio `BugBunny.configure`. El `Railtie` inyecta al
150
+ host (§i), no a gemas terceras.
151
+
152
+ ## 3. Inferencias
153
+
154
+ - **`requerida` de host/port/username/password/vhost:** `VALIDATIONS` las marca
155
+ `required: true`, pero `initialize` les da default → nunca son `nil` salvo que
156
+ el consumidor las setee a `''`/`nil`. Marcadas `sí (validate!)`: el contrato es
157
+ "no-vacías al cierre del bloque", no "sin default". `confidence: high`.
158
+ - **`password` secret?=sí** por nombre (regla RFC-012). El default `'guest'` es
159
+ el usuario default de RabbitMQ (placeholder), **no** un secreto real → ver §4.
160
+
161
+ ## 4. Cobertura y fronteras
162
+
163
+ - §a-§e/§i (estructura) + §f/§g/§h (enrich) **completas** al commit ancla; §j n/a.
164
+ - **Linter de secretos (advisory):** `password` default `'guest'` y el template
165
+ `RABBITMQ_PASSWORD` default `'guest'` matchean el patrón de secreto, pero el
166
+ valor es el placeholder default de RabbitMQ, **no** un secreto hardcodeado. El
167
+ consumidor debe inyectar la credencial real vía ENV en prod (lo dice el header
168
+ del template). No es hallazgo de fuga.
169
+ - **Enriquecimiento (§f/§g/§h):** anclado a los YARD de `configuration.rb` y al
170
+ backoff real de `consumer.rb` — no inventado. La elección de valores concretos
171
+ (tuning de timeouts/prefetch por ambiente) es decisión operativa del consumidor,
172
+ no del artefacto.
@@ -0,0 +1,102 @@
1
+ # Dependencia consumida: RabbitMQ (vía `bunny`) — bug_bunny
2
+
3
+ > meta: artefacto consumed · RFC-018 · generado `arch-structure` (§a/§b/§d) +
4
+ > `arch-enrich` (§c/§e) · anclado a `24ea397`, `bug_bunny.gemspec`,
5
+ > `lib/bug_bunny.rb`, `lib/bug_bunny/session.rb`, `lib/bug_bunny/producer.rb`,
6
+ > `lib/bug_bunny/consumer.rb`, `lib/bug_bunny/middleware/raise_error.rb` · fecha
7
+ > 2026-06-30 · cobertura: §a/§b/§d (estructura) + §c/§e (enrich, anclado a
8
+ > `consumer.rb`) completas.
9
+
10
+ ## 1. Resumen
11
+
12
+ La gema consume **un único sistema externo: el broker RabbitMQ**, vía el SDK
13
+ `bunny ~> 2.24` (AMQP 0-9-1). Es el corazón del gem: toda publicación, consumo,
14
+ RPC y declaración de infraestructura pasa por `Bunny`. El pooling de conexiones
15
+ lo da `connection_pool` (infra, ver §4).
16
+
17
+ ## 2. Cuerpo
18
+
19
+ ### a. Identidad
20
+
21
+ | campo | valor |
22
+ |---|---|
23
+ | proveedor / sistema | **RabbitMQ broker** |
24
+ | sub-tipo | **externo** (sistema de mensajería, no un repo del fleet) |
25
+ | transporte | AMQP 0-9-1 (TCP) |
26
+ | cliente nuestro | gem `bunny ~> 2.24` (`bug_bunny.gemspec:36`), envuelto por `BugBunny.create_connection` (`bug_bunny.rb:87`) + `Session` |
27
+ | auth | SASL user/pass (`username`/`password` de `Configuration`); URL `amqp://user:pass@host:port/vhost` |
28
+ | ancla (doc proveedor) | Bunny: https://github.com/ruby-amqp/bunny · AMQP 0-9-1: https://www.rabbitmq.com/amqp-0-9-1-reference.html |
29
+
30
+ ### b. Operaciones consumidas (subset usado)
31
+
32
+ | operación Bunny | dónde | qué mandamos / esperamos |
33
+ |---|---|---|
34
+ | `Bunny.new(opts).start` | `bug_bunny.rb:89,93` | abre `Bunny::Session` (conexión TCP + handshake); espera sesión iniciada |
35
+ | `after_recovery_completed` | `bug_bunny.rb:90` | callback de recuperación de conexión → log `bug_bunny.connection_recovered` |
36
+ | `session.create_channel` | `session.rb:177` (rescata fallo) | abre canal AMQP |
37
+ | reconnect / recovery | `session.rb:285` | reconexión; en fallo → `CommunicationError` |
38
+ | publish confirmado | `producer.rb:90` (`Producer#confirmed`) | `basic.publish` + publisher confirms; espera ACK/NACK |
39
+ | publisher confirms (`nacked_set`) | `producer.rb:235` | lee NACKs del canal → `PublishNacked` |
40
+ | `mandatory: true` + `basic.return` | `producer.rb:325` | mensaje no ruteable → `PublishUnroutable` (reply_code 312 NO_ROUTE) |
41
+ | RPC (publish + reply-to consume) | `producer.rb:124,214` | request/response; timeout → `RequestTimeout` |
42
+ | AMQP publish/consume genérico | `client.rb:168` | cualquier `Bunny::Exception` en la frontera → `CommunicationError` |
43
+
44
+ > El mapa completo de operaciones que la gema **expone** (no las que consume)
45
+ > es la capa operaciones (RFC-003, `docs/api/`, pendiente F2).
46
+
47
+ ### d. Errores del proveedor → excepción nuestra
48
+
49
+ `bunny` levanta `Bunny::Exception` (y subclases). La gema las **envuelve** en la
50
+ frontera de abstracción. La columna "excepción nuestra" **referencia** el
51
+ catálogo `docs/errors/errors.md` (RFC-020), no lo redefine.
52
+
53
+ | error del proveedor (Bunny) | excepción nuestra | dónde se mapea |
54
+ |---|---|---|
55
+ | `Bunny::Exception` (TCP fail, auth fail, vhost inválido) al conectar | `CommunicationError` | `bug_bunny.rb:95-98` |
56
+ | `Bunny::Exception` al crear canal | `CommunicationError` | `session.rb:177` |
57
+ | `Bunny::Exception` en reconexión | `CommunicationError` | `session.rb:285` |
58
+ | `Bunny::Exception` en publish confirmado | `CommunicationError` | `producer.rb:90` |
59
+ | `Bunny::Exception` / `ConnectionClosedError` en publish/consume | `CommunicationError` | `client.rb:168` |
60
+ | NACK del broker (publisher confirms) | `PublishNacked` | `producer.rb:235` |
61
+ | `basic.return` (mandatory, no ruteable) | `PublishUnroutable` | `producer.rb:325` |
62
+ | timeout esperando reply RPC | `RequestTimeout` | `producer.rb:124,214` |
63
+
64
+ > Regla: los callers no rescatan `Bunny::*` directo — `rescue
65
+ > BugBunny::CommunicationError` cubre cualquier fallo de transporte/broker. La
66
+ > original queda en `.cause`.
67
+
68
+ ### c. Retry / idempotencia
69
+
70
+ | aspecto | comportamiento (anclado) |
71
+ |---|---|
72
+ | recuperación de conexión | `automatically_recover = true` (default): Bunny recupera canales/suscripciones tras corte TCP (`configuration.rb:60-61`). |
73
+ | retry del Consumer | `Consumer#subscribe` reintenta en cualquier `StandardError` con **backoff exponencial** `network_recovery_interval * 2^(n-1)` cap `max_reconnect_interval`, hasta `max_reconnect_attempts` (`nil` = ∞) (`consumer.rb:106-124`). |
74
+ | ack | `manual_ack: true` (`consumer.rb:90`) → entrega **at-least-once**: si el worker cae tras procesar pero antes del ack, el mensaje se re-entrega. **El handler debe ser idempotente.** |
75
+ | retry de publish | **no automático** — un publish fallido levanta `CommunicationError`/`PublishNacked`; reintentarlo puede **duplicar** salvo dedup aguas abajo. |
76
+ | RPC | `request` espera reply hasta `rpc_timeout`; el retry de un RPC mutante requiere idempotencia (correlación por `correlation_id`). |
77
+
78
+ ### e. Degradación (si RabbitMQ cae)
79
+
80
+ - **Al conectar:** `create_connection` levanta `CommunicationError` (`bug_bunny.rb:95`); no hay fallback ni cola local — el caller decide (circuit-break/alertar).
81
+ - **Consumer:** entra al loop de reconexión con backoff; por default (`max_reconnect_attempts = nil`) **reintenta indefinidamente**, logueando `consumer.connection_error` con `retry_in_s` (`consumer.rb:120-122`). Si se fija un máximo y se agota → `consumer.reconnect_exhausted` y re-raise (el worker muere).
82
+ - **Publisher:** cada publish/RPC sobre conexión caída levanta `CommunicationError` (envuelto en `client.rb:168`); **sin buffering** — el mensaje no sale.
83
+ - **Sin circuit-breaker propio:** la gema no trae breaker ni outbox; la resiliencia aguas arriba (reintentar el comando, encolar, alertar) es responsabilidad del consumidor.
84
+
85
+ ## 3. Inferencias
86
+
87
+ - El gem declara **dependencia directa** solo de RabbitMQ-vía-bunny. Las demás
88
+ gemas del gemspec (`activemodel`, `activesupport`, `rack`, `json`,
89
+ `concurrent-ruby`, `ostruct`) son librerías de soporte, no sistemas externos
90
+ consumidos → van a topología (RFC-006), no acá.
91
+
92
+ ## 4. Cobertura y fronteras
93
+
94
+ - §a/§b/§d (estructura) + §c/§e (enrich, anclado a `consumer.rb`) **completas**.
95
+ - **`connection_pool` (`>= 2.4`):** utilidad de pooling de las conexiones Bunny
96
+ (`Resource.connection_pool`), **no** un sistema externo con su propio
97
+ contrato de error de red → no es entrada consumed; el timeout de pool
98
+ (`ConnectionPool::TimedStack`) nace dentro de `@pool.with` y termina envuelto
99
+ como `CommunicationError` (ver `CHANGELOG` #49). Pertenece a topología.
100
+ - **Subset, no la API completa de Bunny:** solo se documentan las operaciones
101
+ que el gem invoca. Parámetros de tuning (heartbeat, prefetch, timeouts) viven
102
+ en `docs/config/`.
@@ -0,0 +1,196 @@
1
+ # Errores — bug_bunny
2
+
3
+ > meta: artefacto errores · RFC-020 · generado `arch-structure` (§a/§b/§d) +
4
+ > `arch-enrich` (§c) · anclado a `d0533bf`, `lib/bug_bunny/exception.rb`,
5
+ > `remote_error.rb`, `middleware/raise_error.rb`, `controller.rb`, `consumer.rb`
6
+ > · fecha 2026-06-30 · cobertura: §a/§b/§d completas (estructura); §c política
7
+ > completa (enrich, **inferida** de HTTP/AMQP — verificación humana pendiente).
8
+
9
+ ## 1. Resumen
10
+
11
+ Catálogo del unhappy path que la gema **emite** a sus consumidores: jerarquía de
12
+ excepciones bajo `BugBunny::Error`, el mapeo `status RPC → excepción` que aplica
13
+ `Middleware::RaiseError` en el lado cliente, y el shape del envelope de error que
14
+ emite el lado worker (`Controller`). La gema es **agnóstica al payload**: expone
15
+ la materia prima (`status` + `raw_response`) y no interpreta la estructura de
16
+ dominio del cuerpo (#52).
17
+
18
+ ## 2. Cuerpo
19
+
20
+ ### a. Inventario de excepciones públicas
21
+
22
+ Jerarquía (todas bajo `BugBunny::Error < ::StandardError`):
23
+
24
+ ```
25
+ ::StandardError
26
+ └── BugBunny::Error base · attr_accessor :status, :raw_response (#52)
27
+ ├── CommunicationError fallo de red/conexión/protocolo AMQP
28
+ ├── ConfigurationError configuración inválida de la gema
29
+ ├── PublishNacked broker NACK en publish :confirmed
30
+ ├── PublishUnroutable broker return (mandatory) sin binding
31
+ ├── ClientError base 4xx
32
+ │ ├── BadRequest 400
33
+ │ ├── NotFound 404
34
+ │ │ └── RoutingError 404 · ruta RPC inexistente en el remoto
35
+ │ ├── NotAcceptable 406
36
+ │ ├── RequestTimeout 408 · y timeout local de RPC
37
+ │ ├── Conflict 409
38
+ │ └── UnprocessableEntity 422 · parsea body, expone error_messages
39
+ └── ServerError base 5xx
40
+ ├── InternalServerError 500 genérico
41
+ └── RemoteError 500 · propaga excepción serializada del worker
42
+ ```
43
+
44
+ | excepción | jerarquía base | qué la levanta (ancla) |
45
+ |---|---|---|
46
+ | `Error` | `::StandardError` | base — no se levanta directa salvo `resource.rb:122` (pool ausente) |
47
+ | `CommunicationError` | `Error` | `bug_bunny.rb:96`, `session.rb:177,285`, `producer.rb:90`, `client.rb:168` — envuelve cualquier `Bunny::Exception` en la frontera del gem; original en `.cause` |
48
+ | `ConfigurationError` | `Error` | `configuration.rb:262,269,277` — validación al final de `BugBunny.configure` |
49
+ | `PublishNacked` | `Error` | `producer.rb:235` — NACK del broker en modo `:confirmed`. Attrs: `path`, `nacked_count`. Opt-out `nack_raise: false` |
50
+ | `PublishUnroutable` | `Error` | `producer.rb:325` — `basic.return` con `mandatory: true`. Attrs: `path`, `exchange`, `routing_key`, `reply_code`, `reply_text`, `correlation_id`. Opt-out `return_raise: false` |
51
+ | `ClientError` | `Error` | `raise_error.rb:170` — 4xx no mapeado explícito |
52
+ | `BadRequest` | `ClientError` | `raise_error.rb:38` — status 400 |
53
+ | `NotFound` | `ClientError` | `raise_error.rb:155` — status 404 genérico |
54
+ | `RoutingError` | `NotFound` | `raise_error.rb:152` — status 404 con `body['error_type'] == 'routing_error'` |
55
+ | `NotAcceptable` | `ClientError` | `raise_error.rb:40` — status 406 |
56
+ | `RequestTimeout` | `ClientError` | `raise_error.rb:41` (status 408) · `producer.rb:124,214` (timeout local de RPC) |
57
+ | `Conflict` | `ClientError` | `raise_error.rb:42` — status 409 |
58
+ | `UnprocessableEntity` | `ClientError` | `raise_error.rb:44` — status 422. Parsea body, expone `error_messages` (busca clave `errors`); setea `raw_response` en el ctor |
59
+ | `ServerError` | `Error` | `raise_error.rb:168` — 5xx no mapeado explícito |
60
+ | `InternalServerError` | `ServerError` | `raise_error.rb:67`, `producer.rb:396` (JSON inválido) — status 500 genérico |
61
+ | `RemoteError` | `ServerError` | `raise_error.rb:63` — status 5xx con `body['bug_bunny_exception']`. Attrs: `original_class`, `original_message`, `original_backtrace` |
62
+
63
+ > `ArgumentError`/`NameError` que levantan `client.rb:51`, `controller.rb:101,171`,
64
+ > `routing/*` son de la API de configuración/programación (uso incorrecto del
65
+ > dev), no del contrato runtime RPC — no se listan como contrato de error público.
66
+
67
+ ### b. Códigos de estado por superficie
68
+
69
+ Mapeo `status → excepción` que aplica `Middleware::RaiseError#on_complete`
70
+ (`raise_error.rb:32-48`) en el **lado cliente** del RPC. Referencia las
71
+ operaciones de RFC-003 (`docs/api/`, hoy pendiente — ver §4), no las redefine.
72
+
73
+ | superficie | status | excepción levantada | cuándo |
74
+ |---|---|---|---|
75
+ | Cliente RPC (`RaiseError`) | 200..299 | — (none) | éxito, flujo normal |
76
+ | Cliente RPC | 400 | `BadRequest` | bad request |
77
+ | Cliente RPC | 404 | `NotFound` / `RoutingError` | recurso / ruta RPC inexistente (`error_type: routing_error`) |
78
+ | Cliente RPC | 406 | `NotAcceptable` | content negotiation |
79
+ | Cliente RPC | 408 | `RequestTimeout` | timeout reportado por el remoto |
80
+ | Cliente RPC | 409 | `Conflict` | conflicto de estado/negocio |
81
+ | Cliente RPC | 422 | `UnprocessableEntity` | validación semántica del modelo remoto |
82
+ | Cliente RPC | 500..599 | `RemoteError` / `InternalServerError` | error del worker (RemoteError si trae `bug_bunny_exception`) |
83
+ | Cliente RPC | otro ≥500 | `ServerError` | 5xx no mapeado |
84
+ | Cliente RPC | otro ≥400 | `ClientError` | 4xx no mapeado |
85
+ | Productor (publish `:confirmed`) | n/a (AMQP) | `PublishNacked` / `PublishUnroutable` | NACK / return del broker — no es status HTTP |
86
+ | Transporte (frontera gem) | n/a (AMQP) | `CommunicationError` | fallo de red/broker, envuelve `Bunny::Exception` |
87
+ | **Worker (dispatch)** | **403** | — (no excepción; responde 403 + reject) | guard anti-RCE: clase enrutada no subclase de `BugBunny::Controller` (`consumer.rb:222-228`) |
88
+
89
+ ### c. Política por error
90
+
91
+ Enriquecimiento `arch-enrich` (RFC-020 §c). Política **inferida** de semántica
92
+ HTTP (RFC 9110) + AMQP — verificación humana pendiente (ver §3).
93
+
94
+ | excepción | retriable | backoff | idempotencia | acción sugerida |
95
+ |---|---|---|---|---|
96
+ | `CommunicationError` | sí (fallo transitorio de red/broker) | sí (`network_recovery_interval`; Bunny auto-recover) | el retry de un **publish** puede duplicar salvo consumidor idempotente; un **read** RPC es seguro | reintentar con backoff; si persiste, circuit-break y alertar |
97
+ | `ConfigurationError` | **no** (bug de config) | — | n/a | fail-fast al boot; corregir el bloque `configure` |
98
+ | `PublishNacked` | sí, con cautela (NACK puede ser transitorio: disk full, replicación) | sí | el re-publish puede duplicar → requiere dedup aguas abajo | reintentar acotado; si persiste, alertar (problema del broker) |
99
+ | `PublishUnroutable` | **no** (no hay binding para la routing key) | — | n/a | corregir routing key / topología de bindings; alertar (mensaje perdido) |
100
+ | `BadRequest` (400) | **no** | — | n/a | corregir el request del cliente |
101
+ | `NotFound` (404) | **no** | — | n/a | verificar recurso/identificador |
102
+ | `RoutingError` (404) | **no** | — | n/a | corregir verbo/path; el servicio remoto no tiene esa ruta registrada |
103
+ | `NotAcceptable` (406) | **no** | — | n/a | corregir content negotiation |
104
+ | `RequestTimeout` (408) | sí (transitorio) | sí | el retry de un publish/RPC mutante puede duplicar → idempotencia requerida | reintentar con backoff; subir `rpc_timeout` si es crónico |
105
+ | `Conflict` (409) | **no** sin resolver el conflicto | — | depende del flujo | resolver el estado en conflicto antes de reintentar |
106
+ | `UnprocessableEntity` (422) | **no** (error semántico/validación) | — | n/a | corregir el payload; leer `error_messages` / `raw_response` |
107
+ | `ClientError` (4xx genérico) | **no** (default 4xx) | — | n/a | inspeccionar status concreto |
108
+ | `ServerError` (5xx genérico) | sí (transitorio) | sí | idempotencia requerida si la op muta estado | reintentar con backoff |
109
+ | `InternalServerError` (500) | sí (transitorio) | sí | idem | reintentar con backoff; si persiste, escalar al dueño del worker |
110
+ | `RemoteError` (500) | depende de `original_class` | depende | depende | inspeccionar `original_class`/`original_backtrace`; tratar como bug del worker remoto, no reintentar a ciegas |
111
+
112
+ ### d. Shape del payload de error
113
+
114
+ **Lado worker → wire (lo que emite `Controller#handle_exception`, `controller.rb:224-233`)**
115
+ ante una excepción no mapeada por `rescue_from`:
116
+
117
+ ```json
118
+ {
119
+ "status": 500,
120
+ "headers": { },
121
+ "body": {
122
+ "error": "Internal Server Error",
123
+ "detail": "<exception.message>",
124
+ "type": "<exception.class.name>",
125
+ "bug_bunny_exception": {
126
+ "class": "<clase original>",
127
+ "message": "<mensaje original>",
128
+ "backtrace": ["<hasta 25 líneas>"]
129
+ }
130
+ }
131
+ }
132
+ ```
133
+
134
+ - El envelope `bug_bunny_exception` lo arma `RemoteError.serialize` (`remote_error.rb:29`);
135
+ también lo agrega `Consumer` cuando `status == 500 && exception` (`consumer.rb:325`).
136
+ Es lo que el cliente reconstruye como `RemoteError` (`raise_error.rb:61-64`).
137
+ - `render status:, json:` (`controller.rb:242`) deja el shape del body de error de
138
+ dominio a criterio del worker — la gema no lo impone.
139
+
140
+ **Lado cliente — materia prima expuesta (#52).** Toda excepción derivada de una
141
+ respuesta RPC trae, vía `RaiseError#raise_typed` (`raise_error.rb:82-86`):
142
+
143
+ - `e.status` → `Integer` (el código de la respuesta).
144
+ - `e.raw_response` → `Hash | String | nil` (el cuerpo crudo, **sin interpretar**).
145
+ `nil` para errores no-RPC (`CommunicationError`, `ConfigurationError`).
146
+
147
+ **Shapes que `format_error_message` reconoce para el `.message` humano**
148
+ (`raise_error.rb:110-141`, best-effort, **NO contrato**):
149
+
150
+ 1. Envelope anidado: `{ "error": { "message": "..." } }` → extrae `error.message`.
151
+ 2. Shape plano histórico: `{ "error": "texto", "detail": "..." }` → `"texto - detail"`.
152
+ 3. Fallback: `body.to_json` (nunca `Hash#inspect`).
153
+
154
+ `UnprocessableEntity` además expone `error_messages` (`exception.rb:219-263`):
155
+ parsea el body, devuelve `parsed['errors']` por convención o el cuerpo completo.
156
+
157
+ > **Seguridad (cruza RFC-017):** `raw_response` puede contener datos sensibles en
158
+ > `details`. La gema lo entrega crudo a propósito; **sanitizar antes de cualquier
159
+ > sink** (Sentry/logs) filtrando `password|pass|passwd|secret|token|api_key|auth`
160
+ > → `[FILTERED]` es responsabilidad del consumidor (`exception.rb:25-30`).
161
+
162
+ ## 3. Inferencias
163
+
164
+ - **Guard anti-RCE = 403, no excepción.** El control de seguridad que valida la
165
+ herencia de la clase enrutada vive en `consumer.rb:222-228`: si la clase
166
+ resuelta no es subclase de `BugBunny::Controller`, el worker loguea
167
+ `consumer.security_violation`, responde **403 'Forbidden'** (`handle_fatal_error`)
168
+ y rechaza el mensaje sin requeue — **no levanta una excepción dedicada**. La ex
169
+ `BugBunny::SecurityError` (nunca levantada en ninguna ruta de código) fue
170
+ **eliminada como dead code**; el contrato real del unhappy path de RCE es el 403
171
+ (ver §b). `confidence: high` (verificado por grep exhaustivo + lectura del guard).
172
+ - **§b superficies AMQP** (`PublishNacked`/`PublishUnroutable`/`CommunicationError`)
173
+ no tienen status HTTP: son señales del protocolo AMQP, no del envelope RPC. Se
174
+ listan como `n/a (AMQP)` para no forzar un código HTTP inventado.
175
+ - **§c política (enrich):** la columna retriable/backoff/idempotencia/acción está
176
+ **inferida** de la semántica estándar (HTTP RFC 9110 para 4xx/5xx, comportamiento
177
+ AMQP para NACK/return/transporte), **no** de una política declarada en el código
178
+ del gem. `confidence: medium`. A confirmar con el dueño del código /
179
+ consumidores reales (sobre todo idempotencia de re-publish y el caso
180
+ `RemoteError`, que depende del `original_class` del worker). El gem **no impone**
181
+ retry: la decisión es del consumidor en su boundary.
182
+
183
+ ## 4. Cobertura y fronteras
184
+
185
+ - **§a/§b/§d (estructura) + §c (enrich, política inferida) completas** al commit
186
+ ancla. §c marcada `confidence: medium` (ver §3).
187
+ - **RFC-003 (`docs/api/`) pendiente:** §b referencia "superficie" de operaciones
188
+ pero la capa operaciones no está generada (dev-structure F2, ver `CLAUDE.md`).
189
+ Cuando se genere, §b debe cruzar las operaciones reales.
190
+ - **Frontera con `consumed` (RFC-018):** este artefacto = errores que la gema
191
+ **emite**. Los errores de `Bunny::*` que la gema **consume** y envuelve en
192
+ `CommunicationError` son su mapeo error-proveedor→excepción; viven en
193
+ `docs/consumed/rabbitmq.md` §d (que referencia este catálogo, no lo redefine).
194
+ - **Errores internos no-públicos** (rescatados adentro, no cruzan la frontera) y
195
+ los `ArgumentError`/`NameError` de mal-uso de la API de config quedan fuera:
196
+ no son contrato runtime.