bug_bunny 4.19.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: 8c751d42f963af2dc145005dba7b6245b0dc9fa549cacecb824e6a5cc2c3232d
4
- data.tar.gz: c7c602f3f96a47a85080b287fd87435bb161abcab6b5358f51b45ed05fd1428a
3
+ metadata.gz: e1076566b6a3b8c25f6f00fc624c92681e8db881ffc3b8d6640da3bb7158333b
4
+ data.tar.gz: 2510fe871ec6f32804dba3faa7f1110408656e8cff30d2c0d537c7d0b30e1686
5
5
  SHA512:
6
- metadata.gz: e8e1e583486fec890b5ccdedb77f99f1954f37e9a4217afe1aee0085ca8483f242a966643bcce5da5a7a180d0223850039a883ae9ce92b8be91a02c7beaac7b7
7
- data.tar.gz: 71305eaf4dec9b4bf3fae7bf19bbb3fd997d75c409c6d5239d52030c1c7b5833f48f675f5bbcf187ef16334c0286f7a780675add36980c5b927ca592c68ab9d7
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,22 @@
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
+
3
20
  ## [4.19.0] - 2026-06-25
4
21
 
5
22
  > Capa de errores transport-agnostic (#52). Cambio **aditivo y retrocompatible**: `.message` no se degrada y se suman dos accesores. Bump minor.
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.
@@ -1,9 +1,9 @@
1
1
  # Release — bug_bunny
2
2
 
3
- > meta: artefacto release · RFC-014 (shape v2.0, `proposed`) · generado
4
- > manualmente (**piloto RFC-014** suplemento gema, valida patrón 1 +
5
- > per-repo-visible) · anclado a `.github/workflows/release.yml`, `*.gemspec`,
6
- > `lib/bug_bunny/version.rb`, `CHANGELOG.md`, git · fecha 2026-06-01 · cobertura:
3
+ > meta: artefacto release · RFC-014 (`accepted`) · generado por `arch-structure`
4
+ > + `arch-enrich` (híbrido RFC-014 §2; nació como piloto manual #51, re-anclado
5
+ > a la RFC vigente) · anclado a `.github/workflows/release.yml`, `*.gemspec`,
6
+ > `lib/bug_bunny/version.rb`, `CHANGELOG.md`, git · fecha 2026-06-30 · cobertura:
7
7
  > completa (régimen gema: build→publish, §e/f/g n/a).
8
8
 
9
9
  ## 1. Resumen
@@ -20,9 +20,9 @@ out-of-repo).
20
20
 
21
21
  ### a. Hecho verificable
22
22
 
23
- - **Convención de versión:** SemVer `vX.X.X`. Actual: **4.18.0**.
24
- - **Source of truth:** tag remoto (`v4.18.0`) + **triple mirror**
25
- `lib/bug_bunny/version.rb` (`VERSION = '4.18.0'`) ← `bug_bunny.gemspec:7`
23
+ - **Convención de versión:** SemVer `vX.X.X`. Actual: **4.19.0**.
24
+ - **Source of truth:** tag remoto (`v4.19.0`) + **triple mirror**
25
+ `lib/bug_bunny/version.rb` (`VERSION = '4.19.0'`) ← `bug_bunny.gemspec:7`
26
26
  (`spec.version = BugBunny::VERSION`).
27
27
  - **Changelog canónico:** `CHANGELOG.md` único.
28
28
  - **Patrón de trigger:** `gema-tag` (patrón 1).
@@ -33,7 +33,7 @@ out-of-repo).
33
33
 
34
34
  - **Convención:** SemVer `vX.X.X` (**con `v`** — distinto al servicio).
35
35
  - **Source of truth:** tag remoto canónico (`git tag --sort=-v:refname` →
36
- `v4.18.0`).
36
+ `v4.19.0`).
37
37
  - **Mirror:** `lib/bug_bunny/version.rb` (`VERSION`), leído por
38
38
  `bug_bunny.gemspec:7` (`spec.version = BugBunny::VERSION`).
39
39
  `required_ruby_version >= 2.6.0` (`bug_bunny.gemspec:17`).
@@ -58,7 +58,7 @@ out-of-repo).
58
58
  `on: push: tags: ['v*']` → `ruby/setup-ruby@v1` → `gem build *.gemspec` +
59
59
  `gem push *.gem` (auth `secrets.RUBYGEMS_API_KEY`). Auditable y versionado con
60
60
  el código; se ancla a `file:line`, no se referencia como caja negra.
61
- - **Consumo:** los servicios la pinnean por versión (`gem "bug_bunny", "~> 4.18.0"`)
61
+ - **Consumo:** los servicios la pinnean por versión (`gem "bug_bunny", "~> 4.19.0"`)
62
62
  desde RubyGems — **no** git-source.
63
63
 
64
64
  ### e. Deploy / publish
@@ -80,7 +80,7 @@ procedimiento per-repo porque no vive acá. Una versión yankeada se anotaría e
80
80
  ### h. Dependencias de deploy inter-servicio
81
81
 
82
82
  - **Consumidores** (cruza RFC-018): servicios del fleet la pinnean
83
- `~> 4.18.0` (semántica minor-compatible). Un cambio de contrato del gem
83
+ `~> 4.19.0` (semántica minor-compatible). Un cambio de contrato del gem
84
84
  (ej. el behavior-change de `4.18.0` — `Bunny::Exception` → `CommunicationError`)
85
85
  obliga a los consumidores a migrar; el `CHANGELOG.md` lo documenta como
86
86
  breaking note. **Orden de deploy:** los consumidores adoptan al hacer `bundle
data/docs/test/test.md ADDED
@@ -0,0 +1,110 @@
1
+ # Test — bug_bunny
2
+
3
+ > meta: artefacto test · RFC-013 · generado `arch-structure` (§a-§d) +
4
+ > `arch-enrich` (§e-§h) · anclado a `24ea397`, `Rakefile`, `bug_bunny.gemspec`,
5
+ > `spec/spec_helper.rb`, `spec/support/integration_helper.rb`,
6
+ > `.github/workflows/main.yml`, `CHANGELOG.md` · fecha 2026-06-30 · cobertura:
7
+ > §a-§d (estructura) + §e-§h (enrich, anclado a specs/CHANGELOG) completas.
8
+
9
+ ## 1. Resumen
10
+
11
+ Suite principal **RSpec** (`spec/`, 22 specs: 15 unit + 7 integration). Tarea
12
+ `:test` legacy de **Minitest** (`test/`, 2 archivos) fuera del default y del CI.
13
+ CI corre `bundle exec rake` (= `:spec`) en Ruby 3.4.4. Sin coverage tool
14
+ configurado.
15
+
16
+ ## 2. Cuerpo
17
+
18
+ ### a. Suites, frameworks y niveles
19
+
20
+ | framework | dir | nivel | nº | propósito |
21
+ |---|---|---|---|---|
22
+ | **RSpec** `~> 3.0` | `spec/unit/` | unit | 15 | client/session pool, configuration, consumer, producer, controller, raise_error, remote_error, request, route, observability, otel, resource, middleware |
23
+ | **RSpec** | `spec/integration/` | integration | 7 | client, consumer_middleware, controller, error_handling, infrastructure, publisher_confirms, resource — **requieren RabbitMQ real** (usan `BugBunny.create_connection` + pool) |
24
+ | **Minitest** `~> 5.0` (+ `mocha`, `minitest-reporters`) | `test/integration/` | integration (legacy) | 2 | `manual_client_test.rb`, `infrastructure_test.rb` — tarea `:test`, **no** en default ni CI |
25
+
26
+ Sin tags declarados (`:slow`/`:js`) en la config de RSpec.
27
+
28
+ ### b. Comando de corrida
29
+
30
+ | objetivo | comando | qué corre |
31
+ |---|---|---|
32
+ | default / CI | `bundle exec rake` | tarea `:spec` (toda la suite RSpec) — `Rakefile:` `task default: :spec` |
33
+ | solo unit | `bundle exec rake spec:unit` | `spec/unit/**/*_spec.rb` |
34
+ | solo integration | `bundle exec rake spec:integration` | `spec/integration/**/*_spec.rb` (requiere broker) |
35
+ | Minitest legacy | `bundle exec rake test` | `test/**/*_test.rb` |
36
+ | CI | `.github/workflows/main.yml` → `bundle exec rake` | matrix Ruby `3.4.4`, `ruby/setup-ruby@v1` + `bundler-cache`, on push `main` + PR |
37
+
38
+ > Todas las tareas RSpec corren con `--require spec_helper` (`Rakefile`).
39
+
40
+ ### c. Fixtures / Factories
41
+
42
+ - **Sin FactoryBot ni fixtures YAML.** El setup vive en `spec/spec_helper.rb`:
43
+ configura `BugBunny` (host/user/pass desde ENV con default `guest`), arma un
44
+ `ConnectionPool` de test (`TEST_POOL`, size 5) y declara rutas globales para
45
+ todos los specs (`resources :ping/:node/:user`, `get 'around'/'rescue'/'boom'/
46
+ 'echo'`, `post 'events'`).
47
+ - **Carga `.env`** si existe (parse manual, `spec_helper.rb`).
48
+ - **Soporte:** `spec/support/integration_helper.rb` (helpers + `TEST_WORKER_QUEUE_OPTS`
49
+ para colas efímeras), `spec/support/bunny_mocks.rb` (mocks de Bunny para unit).
50
+ - `exchange_options = { durable: false, auto_delete: true }` → exchanges efímeros
51
+ en test.
52
+
53
+ ### d. Configuración de coverage
54
+
55
+ **n/a** — sin SimpleCov ni `.simplecov` ni `SimpleCov.start` en el repo. No hay
56
+ umbral de coverage declarado.
57
+
58
+ ### e. Gaps de cobertura
59
+
60
+ - **Integration specs no corren en CI:** `main.yml` no declara servicio RabbitMQ;
61
+ las 7 integration specs **se skipean** vía `rabbitmq_available?`
62
+ (`spec/support/integration_helper.rb:14`, ver `publisher_confirms_spec.rb:10`).
63
+ En CI solo se ejercitan las **15 unit specs** → el contrato AMQP real (publish/
64
+ consume/confirms contra broker) **no se valida en pipeline**, solo localmente
65
+ con broker. Gap relevante.
66
+ - **Sin medición de cobertura:** no hay SimpleCov ni umbral → la cobertura no está
67
+ cuantificada (no se sabe el % de líneas ejercidas).
68
+
69
+ ### f. Contract-assessment
70
+
71
+ ¿Los tests cubren los contratos públicos? (RFC-020/018/012/003)
72
+
73
+ | contrato | specs que lo cubren | veredicto |
74
+ |---|---|---|
75
+ | **Errores RFC-020** (status→excepción, materia prima) | `raise_error_spec`, `remote_error_spec`, `communication_error_wrapping_spec`, `error_handling_spec` (integration) | **bien cubierto** (unit) |
76
+ | **Consumed RFC-018** (Bunny::Exception→`CommunicationError`) | `communication_error_wrapping_spec`, `client_session_pool_spec` | cubierto (unit) |
77
+ | **Config RFC-012** (validaciones de `Configuration`) | `configuration_spec` | cubierto |
78
+ | **Confirms** (`PublishNacked`/`PublishUnroutable`) | `producer_spec`, `publisher_confirms_spec` (integration) | parcial en CI (la parte integration skipea sin broker) |
79
+ | **Operaciones/routing** (RFC-003, capa F2) | `route_spec`, `request_spec`, `controller_spec`, `controller_after_action_spec`, `resource_spec` | cubierto (unit) |
80
+
81
+ ### g. Link a incidente → test de regresión
82
+
83
+ | incidente | test de regresión | ancla |
84
+ |---|---|---|
85
+ | **#52** (`status`/`raw_response` en toda la jerarquía + hardening de `format_error_message`) | `raise_error_spec` — comentarios explícitos "Alcance issue #52" | `spec/unit/raise_error_spec.rb:59,120,169` |
86
+ | **#49** (leak de `Bunny::TCPConnectionFailedForAllHosts` en `try_create`) | `communication_error_wrapping_spec` — "Client#publish — TCP fail en try_create (issue #49 caso original)" | `spec/unit/communication_error_wrapping_spec.rb:41` |
87
+
88
+ ### h. PII en fixtures / factories
89
+
90
+ - **Sin PII.** No hay fixtures con datos personales: `spec_helper` usa
91
+ placeholders (`guest`/`localhost`), las rutas de test son `ping`/`node`/`user`
92
+ **sin payloads de datos reales**, y `bunny_mocks.rb` mockea el driver. Cruza
93
+ RFC-026: nada que clasificar/anonimizar. `confidence: high`.
94
+
95
+ ## 3. Inferencias
96
+
97
+ - **Doble framework:** RSpec es el primario (default + CI); Minitest (`test/`,
98
+ `mocha`, `minitest-reporters` en gemspec) es **legacy** — el comentario del
99
+ `Rakefile` lo declara explícito ("tests de integración legacy de Minitest").
100
+ `confidence: high`.
101
+ - ~~CI on `master` pero la rama principal es `main` (drift de naming)~~ →
102
+ **resuelto** (`main.yml` ahora dispara `push: branches: [main]`). El trigger
103
+ `push` corre en la rama por default; `pull_request` cubre los PRs.
104
+
105
+ ## 4. Cobertura y fronteras
106
+
107
+ - §a-§d (estructura) + §e-§h (enrich, anclado a specs/CHANGELOG) **completas**.
108
+ - **Integration specs requieren un RabbitMQ vivo** y se skipean sin broker (§e) —
109
+ por eso no se ejercitan en CI.
110
+ - **Contenido de cada test case** queda en el código, no se inventaria acá.
@@ -60,10 +60,6 @@ module BugBunny
60
60
  # Se levanta al final de {BugBunny.configure} si algún atributo no pasa las validaciones.
61
61
  class ConfigurationError < Error; end
62
62
 
63
- # Error lanzado cuando ocurren un acceso no permitido a controladores.
64
- # Protege contra vulnerabilidades de RCE validando la herencia de las clases enrutadas.
65
- class SecurityError < Error; end
66
-
67
63
  # Error lanzado cuando el broker responde NACK a una publicación en modo `:confirmed`.
68
64
  #
69
65
  # Un NACK significa que el broker rechazó explícitamente el mensaje (ej: política de
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = '4.19.0'
4
+ VERSION = '5.0.0'
5
5
  end
data/skill/SKILL.md CHANGED
@@ -415,8 +415,9 @@ Limitación de RSpec: `instance_double` valida que el método exista pero **no**
415
415
  **Causa:** No hubo respuesta en `config.rpc_timeout` segundos.
416
416
  **Resolución:** Verificar que el worker esté activo y que el controlador remoto no lance excepciones silenciosas.
417
417
 
418
- ### BugBunny::SecurityError
418
+ ### Guard anti-RCE (403, no es excepción)
419
419
  **Causa:** El mensaje intenta ejecutar un controlador que no hereda de `BugBunny::Controller`.
420
+ **Comportamiento:** El worker responde **403 Forbidden** + reject + log `event=consumer.security_violation` (`consumer.rb:222-228`); no levanta una excepción dedicada.
420
421
  **Resolución:** Verificar la jerarquía de controladores y que `config.controller_namespace` coincida.
421
422
 
422
423
  ### BugBunny::RouteNotFoundError (404)
@@ -119,5 +119,5 @@ livenessProbe:
119
119
  |-----------|-----------|
120
120
  | Ruta no encontrada | 404 + log `event=consumer.route_not_found` |
121
121
  | Controller no encontrado (namespace) | 404 + log `event=consumer.controller_not_found` |
122
- | Controller no hereda de BugBunny::Controller | `SecurityError` |
122
+ | Controller no hereda de BugBunny::Controller | 403 Forbidden + reject + log `event=consumer.security_violation` (guard anti-RCE) |
123
123
  | Excepción no capturada en controller | 500 + log `event=controller.unhandled_exception` con backtrace |
@@ -7,7 +7,6 @@ StandardError
7
7
  └── BugBunny::Error
8
8
  ├── BugBunny::CommunicationError
9
9
  ├── BugBunny::ConfigurationError
10
- ├── BugBunny::SecurityError
11
10
  ├── BugBunny::PublishNacked
12
11
  ├── BugBunny::PublishUnroutable
13
12
  ├── BugBunny::ClientError (4xx)
@@ -58,9 +57,10 @@ message, details } }`), parsealo en el boundary del servicio desde
58
57
  **Validaciones:** host (String no vacío), port (1-65535), username/password (no nil), heartbeat (0-3600), rpc_timeout (>0), channel_prefetch (1-10000).
59
58
  **Resolución:** Revisar el bloque `BugBunny.configure` y corregir valores.
60
59
 
61
- ### BugBunny::SecurityError
60
+ ### Guard anti-RCE (403, no es excepción)
62
61
  **Causa:** Un mensaje intenta ejecutar un controlador que no hereda de `BugBunny::Controller`.
63
- **Cuándo:** El consumer resuelve la clase pero falla la validación `is_a?(BugBunny::Controller)`.
62
+ **Cuándo:** El consumer resuelve la clase (`constantize`) pero falla `controller_class < BugBunny::Controller` (`consumer.rb:222-228`).
63
+ **Comportamiento:** El worker **no levanta una excepción** — loguea `event=consumer.security_violation`, responde **403 Forbidden** al caller RPC y rechaza el mensaje sin requeue.
64
64
  **Resolución:** Verificar que el controlador herede de `BugBunny::Controller` y que `config.controller_namespace` sea correcto.
65
65
 
66
66
  ### BugBunny::PublishNacked
@@ -69,7 +69,7 @@ El consumer resuelve el controlador concatenando:
69
69
 
70
70
  Ejemplo: namespace `:admin`, controller `:reports` → `BugBunny::Controllers::Admin::ReportsController`
71
71
 
72
- Valida que el controlador sea subclase de `BugBunny::Controller`. Si no, lanza `SecurityError`.
72
+ Valida que el controlador sea subclase de `BugBunny::Controller` (guard anti-RCE). Si no, el worker loguea `consumer.security_violation`, responde **403 Forbidden** y rechaza el mensaje sin requeue (`consumer.rb:222-228`) — no levanta una excepción dedicada.
73
73
 
74
74
  ## Route Object
75
75
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.19.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-25 00:00:00.000000000 Z
11
+ date: 2026-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -229,13 +229,18 @@ executables: []
229
229
  extensions: []
230
230
  extra_rdoc_files: []
231
231
  files:
232
+ - AGENTS.md
232
233
  - CHANGELOG.md
233
234
  - CLAUDE.md
234
235
  - README.md
235
236
  - Rakefile
236
237
  - docs/behavior/behavior.md
238
+ - docs/config/configuracion.md
239
+ - docs/consumed/rabbitmq.md
240
+ - docs/errors/errors.md
237
241
  - docs/glossary/glossary.md
238
242
  - docs/release/release.md
243
+ - docs/test/test.md
239
244
  - initializer_example.rb
240
245
  - lib/bug_bunny.rb
241
246
  - lib/bug_bunny/client.rb
@@ -308,7 +313,7 @@ metadata:
308
313
  homepage_uri: https://github.com/gedera/bug_bunny
309
314
  source_code_uri: https://github.com/gedera/bug_bunny
310
315
  changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
311
- documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.19.0/skill
316
+ documentation_uri: https://github.com/gedera/bug_bunny/blob/v5.0.0/skill
312
317
  post_install_message:
313
318
  rdoc_options: []
314
319
  require_paths: