exis_ray 0.9.0 → 0.10.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: c87f049dd7216e8ac33a9844b77cd1c863f9248130b329994002aa654c0c47ca
4
- data.tar.gz: 1752d432a8437178121b36a0b28293f52f96b99d13a5c40589f312fb8f9b6236
3
+ metadata.gz: 78d2b6cbc5e3da104fee7f3d2f481311acd2da7971d80295edf0bbad6eaab178
4
+ data.tar.gz: c54ebfc75e0c5abf640506b97b41b4b86bfe1f05a8d17a2d484941dae0fb21d4
5
5
  SHA512:
6
- metadata.gz: 72f092befb65f8eb03fe851a302ec49ac47fb155a288ed1103923766f0322da6e5704e760d8c6fa7e4865d78666ddc29b7f7c5e486f22120de1c0ff4a072777e
7
- data.tar.gz: 57e552a85bc42d9cb659d49a429c85131175350542856378d432063b9558f264a636c93d4a25ee57b51717718c5bcb03a2f86fbff3676ce85fedfde4562f5509
6
+ metadata.gz: e6642e744b2b9fc0d798e419664d76ebe2a0beba38722b1db09ac501d408938695990d4d752a0b0bf9cad78aa1a62a3939113e1444a623df890cabb8a4f0ad31
7
+ data.tar.gz: e96e7936add04a739ac1f325d1b768437c82381f5252420f2ab0eb72d6ad51e50bc272c7b136e4f1c0096d59f50b4b9fdc227ec05086e374e385ad543d5d47e8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [0.10.0] - 2026-05-25
2
+
3
+ ### Documentación
4
+ - **`SKILL.md`/`README` describían la auto-inyección como incondicional** (#11): la prosa de "Flujo runtime" en `SKILL.md` y "Flujo de propagación" en `README.md` listaba los campos auto-inyectados (`root_id`, `trace_id`, `source`, `request_id`, ...) sin desglosar los guards. `JsonFormatter#inject_tracer_context` corta su bloque con `return unless Tracer.root_id`, y `request_id` se emite **fuera** de ese guard (distinto ciclo de vida — fix de #9). Sin esa precisión, un implementador en `box_radius_manager` concluyó (apoyándose en la doc) que "`root_id` ya cubre la trazabilidad, `request_id` es redundante" y removió un workaround necesario; en su entrypoint sin trace header `root_id` era nil → logs sin `request_id`/`source`/`root_id`. Agregada subsección "Condiciones de emisión por campo" en `SKILL.md` con tabla "campo → condición → entrypoint que la garantiza", callout en el README arriba de "Campos auto-inyectados", y enlace cruzado a `docs/behavior/behavior.md` + `docs/glossary/glossary.md` (que ya tenían el detalle).
5
+
6
+ ### Correcciones
7
+ - **Clave `task` duplicada en JSON cuando `TaskMonitor.run` loguea lifecycle** (#12): `TaskMonitor` emitía `task=#{task_name}` en los KV strings de `task_started`/`task_finished` (3 sites: `task_monitor.rb:32,41,48`); `JsonFormatter#inject_tracer_context` ya inyecta la misma clave desde `Tracer.task` con Symbol — al colapsar Symbol/String en `JSON.generate`, `json` emitía `warning: detected duplicate key "task"` y la línea entera va a romper en `json` 3.0. **Fix B** (origen): los KV de lifecycle ya no emiten `task=...`; la clave viene exclusivamente del formatter vía `Tracer.task`. **Fix A** (defensa en profundidad): `JsonFormatter` normaliza todas las claves del payload a String antes de `JSON.generate`, deduplicando con precedencia "última inserción gana" (developer payload pisa contexto canónico inyectado por Tracer, igual que `log_fields`). Cubre futuros casos análogos Tracer ↔ developer KV sobre la misma clave.
8
+
1
9
  ## [0.9.0] - 2026-05-19
2
10
 
3
11
  ### Correcciones
data/README.md CHANGED
@@ -52,7 +52,7 @@ ExisRay opera en tres capas que se combinan automáticamente:
52
52
  **Flujo de propagación:**
53
53
 
54
54
  1. Un request/job/mensaje llega al servicio. El middleware correspondiente hidrata el `Tracer` con el header entrante. **Todo entrypoint garantiza un `root_id`**: si no llega trace header (servicio que es punto de entrada, no eslabón intermedio), genera uno fresco. En HTTP además captura el `request_id` de Rails.
55
- 2. `JsonFormatter` inyecta automáticamente `root_id`, `trace_id`, `source`, `request_id` y el contexto de negocio (`user_id`, `isp_id`, `correlation_id`) en cada línea de log. Como el entrypoint siempre asegura `root_id`, `source` (campo mandatorio) nunca falta.
55
+ 2. `JsonFormatter` inyecta `root_id`, `trace_id`, `source`, `request_id` y el contexto de negocio (`user_id`, `isp_id`, `correlation_id`) en cada línea de log. **Cada campo tiene un guard:** `root_id`/`trace_id`/`source`/`task`/`sidekiq_job` salen solo si `Tracer.root_id` está presente; `request_id` se emite fuera de ese guard (distinto ciclo de vida). Como todo entrypoint asegura `root_id` (genera uno fresco si no llega header), `source` (mandatorio) nunca falta. Detalle por campo: tabla [Campos auto-inyectados](#campos-auto-inyectados) más abajo.
56
56
  3. Cuando el servicio llama a otro servicio (HTTP, Sidekiq, RabbitMQ), el middleware de salida genera un nuevo header con `Tracer.generate_trace_header`, que incluye el `root_id` original, el `self_id` del servicio actual, el `CalledFrom` y el tiempo acumulado.
57
57
  4. El servicio destino repite desde el paso 1. El `root_id` se mantiene constante a lo largo de toda la cadena.
58
58
 
@@ -339,7 +339,9 @@ config.logger.formatter = ExisRay::JsonFormatter
339
339
 
340
340
  ### Campos auto-inyectados
341
341
 
342
- `JsonFormatter` inyecta estos campos automáticamente en cada línea. **Nunca** los incluyas manualmente en tus logs:
342
+ `JsonFormatter` inyecta estos campos automáticamente en cada línea. **Nunca** los incluyas manualmente en tus logs.
343
+
344
+ > **No es incondicional.** `root_id`/`trace_id`/`source`/`task`/`sidekiq_job` están gateados por `inject_tracer_context`'s `return unless Tracer.root_id`. Los 4 entrypoints (HTTP, Sidekiq server, BugBunny consumer, TaskMonitor) garantizan el `root_id` (fresco si no llega header), por eso `source` (mandatorio) nunca falta — pero si el código loguea fuera de un entrypoint (boot, código inicializador, hilos sueltos), esos campos pueden faltar. `request_id` se emite fuera de ese guard y tiene distinto ciclo de vida.
343
345
 
344
346
  | Campo | Condición |
345
347
  |:------|:----------|
@@ -61,13 +61,29 @@ module ExisRay
61
61
  inject_current_tags(payload)
62
62
  process_message(payload, msg)
63
63
 
64
- "#{JSON.generate(payload.compact, { ascii_only: false })}\n"
64
+ "#{JSON.generate(normalize_keys(payload), { ascii_only: false })}\n"
65
65
  rescue StandardError
66
66
  fallback_message(severity, timestamp, msg)
67
67
  end
68
68
 
69
69
  private
70
70
 
71
+ # Normaliza todas las claves del payload a strings y deduplica con precedencia
72
+ # "última inserción gana" (developer payload pisa contexto canónico inyectado
73
+ # antes, igual que `log_fields`). Previene el warning `duplicate key` que `json`
74
+ # emite cuando coexisten `:task` (Symbol, vía Tracer) y `"task"` (String, vía
75
+ # KV string developer) — `json` 3.0 lo va a transformar en error.
76
+ #
77
+ # @param payload [Hash]
78
+ # @return [Hash{String => Object}]
79
+ def normalize_keys(payload)
80
+ payload.each_with_object({}) do |(key, value), result|
81
+ next if value.nil?
82
+
83
+ result[key.to_s] = value
84
+ end
85
+ end
86
+
71
87
  # @return [String, nil]
72
88
  def service_version
73
89
  ExisRay.configuration.service_version
@@ -29,7 +29,7 @@ module ExisRay
29
29
  curr.correlation_id = ExisRay::Tracer.correlation_id
30
30
  end
31
31
 
32
- log_event(:info, "component=exis_ray event=task_started task=#{task_name} outcome=started")
32
+ log_event(:info, "component=exis_ray event=task_started outcome=started")
33
33
 
34
34
  # Bloque de ejecución con o sin tags dependiendo de la configuración
35
35
  execute_with_optional_tags(&block)
@@ -38,14 +38,14 @@ module ExisRay
38
38
  human_time = ExisRay::Tracer.format_duration(duration_s)
39
39
 
40
40
  log_event(:info,
41
- "component=exis_ray event=task_finished task=#{task_name} " \
41
+ "component=exis_ray event=task_finished " \
42
42
  "outcome=success duration_s=#{duration_s} duration_human=\"#{human_time}\"")
43
43
  rescue StandardError => e
44
44
  duration_s = ExisRay::Tracer.current_duration_s
45
45
  human_time = ExisRay::Tracer.format_duration(duration_s)
46
46
 
47
47
  log_event(:error,
48
- "component=exis_ray event=task_finished task=#{task_name} " \
48
+ "component=exis_ray event=task_finished " \
49
49
  "outcome=failed duration_s=#{duration_s} duration_human=\"#{human_time}\" " \
50
50
  "error_class=#{e.class} error_message=#{e.message.inspect} " \
51
51
  "exception.type=#{e.class} exception.message=#{e.message.inspect} " \
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ExisRay
4
4
  # Versión actual de la gema.
5
- VERSION = "0.9.0"
5
+ VERSION = "0.10.0"
6
6
  end
data/skill/SKILL.md CHANGED
@@ -11,7 +11,7 @@ Para el complemento del estándar de logging Wispro (regla Data First, mapeo Ope
11
11
 
12
12
  ### Artefactos de detalle (RFC-008)
13
13
 
14
- Este SKILL.md **resume e indexa**; el contrato y el significado de detalle viven en `docs/<capa>/`. **Version-lock por construcción:** `gemspec.files` empaqueta `docs/**` en el mismo tag que este `SKILL.md`; los links son rutas relativas dentro del paquete del release (nunca rama/`HEAD`/URL flotante). Contrato resumido anclado a **v0.8.0 + fix issue #9** (commit `00cf803`):
14
+ Este SKILL.md **resume e indexa**; el contrato y el significado de detalle viven en `docs/<capa>/`. **Version-lock por construcción:** `gemspec.files` empaqueta `docs/**` en el mismo tag que este `SKILL.md`; los links son rutas relativas dentro del paquete del release (nunca rama/`HEAD`/URL flotante). Contrato resumido anclado a **v0.10.0** (post fix issues #9, #11, #12):
15
15
 
16
16
  - [`docs/behavior/behavior.md`](../docs/behavior/behavior.md) — secuencias de hidratación de trace por entrypoint + emisión en logs (parcial, incremental).
17
17
  - [`docs/glossary/glossary.md`](../docs/glossary/glossary.md) — lenguaje ubicuo del bounded context (`root_id`, `trace_id`, `source`, `request_id`, `entrypoint`, ...).
@@ -79,7 +79,7 @@ ExisRay unifica trazabilidad distribuida, logging estructurado JSON, contexto de
79
79
  2. `Tracer.parse_trace_id` extrae `root_id`, `self_id`, `called_from`, `total_time_so_far`
80
80
  3. `ExisRay.sync_correlation_id` asigna `Tracer.correlation_id` a `Current.correlation_id`
81
81
  4. Controller ejecuta `before_action` para setear `Current.user_id`, `Current.isp_id`
82
- 5. `JsonFormatter` intercepta cada `Rails.logger.*` e inyecta automaticamente: `time`, `level`, `severity_number`, `service`, `service_version`, `deployment_environment`, `request_id` (fuera del guard de `root_id` distinto ciclo de vida), `root_id`, `trace_id`, `source`, `user_id`, `isp_id`, `correlation_id`. Como el entrypoint siempre garantiza `root_id`, `source` (mandatorio) nunca falta. El developer aporta `component` (modulo de negocio) y `event` (que paso); estos NO son auto-inyectados porque dependen del call site, no del contexto de ejecucion.
82
+ 5. `JsonFormatter` intercepta cada `Rails.logger.*` e inyecta el contexto de ejecucion en cada linea. **No es incondicional:** cada campo tiene un guard especifico (ver tabla "Condiciones de emision" mas abajo). En particular `inject_tracer_context` corta el bloque `root_id`/`trace_id`/`source`/`task`/`sidekiq_job` con `return unless Tracer.root_id` la invariante "todo entrypoint garantiza `root_id`" es lo que hace que `source` (mandatorio) nunca falte. `request_id` se emite **fuera** de ese guard (distinto ciclo de vida que `root_id`). El developer aporta `component` (modulo de negocio) y `event` (que paso); estos NO son auto-inyectados porque dependen del call site, no del contexto de ejecucion.
83
83
  6. `LogSubscriber` emite un unico Hash al finalizar el request con campos default (`component`, `event`, `method`, `path`, `http_route`, `format`, `controller`, `action`, `http_status`, `duration_s`, `duration_human`, `view_runtime_s`, `db_runtime_s`, `user_agent_original`, `server_address`, y en error `error_class`/`error_message`/`exception.*`).
84
84
  7. En llamadas salientes, `FaradayMiddleware`/`ActiveResourceInstrumentation` inyectan `propagation_trace_header` con `Tracer.generate_trace_header`
85
85
  8. Al finalizar, `ActiveSupport::CurrentAttributes` hace reset automatico
@@ -295,6 +295,26 @@ Casteo automatico: integers, floats, objetos JSON (`{...}`, `[...]`). Filtra cla
295
295
 
296
296
  Por eso `component` y `event` jamas se auto-inyectan, aunque el estandar Wispro los exija.
297
297
 
298
+ #### Condiciones de emision por campo
299
+
300
+ La auto-inyeccion **no es incondicional**: cada campo tiene un guard. `inject_tracer_context` corta su bloque con `return unless Tracer.root_id`, asi que `root_id`/`trace_id`/`source`/`task`/`sidekiq_job` solo se emiten cuando hay trace context activo. Los 4 entrypoints (HTTP, Sidekiq server, BugBunny consumer, TaskMonitor) garantizan ese `root_id` (fresco si no llega header), por eso `source` (mandatorio) nunca falta en una linea originada por un entrypoint. `request_id` se emite **fuera** del guard de `root_id` — distinto ciclo de vida.
301
+
302
+ | Campo | Condicion de emision | Entrypoint que la garantiza |
303
+ |:------|:---------------------|:----------------------------|
304
+ | `time`, `level`, `severity_number`, `service`, `service_version`, `deployment_environment` | Siempre (no depende de Tracer/Current) | — |
305
+ | `request_id` | `Tracer.request_id` presente. **Fuera del guard de `root_id`** (issue #9 Gap C): distinto ciclo de vida (UUID v4 de Rails vs formato X-Ray). | HTTP (via `ActionDispatch::RequestId`). Otros entrypoints solo si la app lo setea explicitamente. |
306
+ | `root_id` | `Tracer.root_id` presente. **Gatea todo el bloque de tracer context.** | Los 4 entrypoints garantizan `root_id` fresco si no llega trace header (issue #9 Gap A). |
307
+ | `source` | `Tracer.source` presente **y** `root_id` presente (esta dentro del bloque gateado). | Idem `root_id`. Como `source` es mandatorio del estandar Wispro, la invariante "todo entrypoint garantiza `root_id`" es lo que evita que falte. |
308
+ | `trace_id` | `Tracer.trace_id` presente **y** `root_id` presente. Solo cuando el servicio es **eslabon intermedio** (recibio trace header upstream). | — (entrypoint que no recibe header tiene `root_id` fresco pero `trace_id` nil). |
309
+ | `sidekiq_job` | `Tracer.sidekiq_job` presente **y** `root_id` presente. | Sidekiq `ServerMiddleware`. |
310
+ | `task` | `Tracer.task` presente **y** `root_id` presente. | `TaskMonitor.run`. |
311
+ | `correlation_id` | `Current.correlation_id` presente. | `ExisRay.sync_correlation_id` (HTTP middleware lo llama; otros entrypoints solo si la app lo invoca). |
312
+ | `user_id`, `isp_id` | `Current.<attr>` no nil. | Lo setea la app (login, before_actions, etc.). |
313
+ | `Current.log_fields` (cualquier key) | La subclass de `Current` overrideo el hook y retorno un Hash no vacio. | — |
314
+ | `tags` | Rails tagged logging activo. **Antipatron con JSON** (rompe el formato) — ver FAQ. | — |
315
+
316
+ Para el detalle por entrypoint (que setea que y cuando), ver [`docs/behavior/behavior.md`](../docs/behavior/behavior.md). Para el significado de cada campo, [`docs/glossary/glossary.md`](../docs/glossary/glossary.md).
317
+
298
318
  #### Ejemplos: KV vs Hash producen output equivalente
299
319
 
300
320
  ```ruby
data/skills.yml CHANGED
@@ -2,48 +2,34 @@ mcps:
2
2
  - github
3
3
  - clickup
4
4
  skills:
5
- skill-manager:
6
- repo: sequre/ai_knowledge
7
- scope: local
8
5
  yard:
9
6
  repo: sequre/ai_knowledge
10
- scope: local
11
7
  quality-code:
12
8
  repo: sequre/ai_knowledge
13
- scope: local
14
9
  gem-release:
15
10
  repo: sequre/ai_knowledge
16
- scope: local
17
- skill-builder:
11
+ dev-structure:
18
12
  repo: sequre/ai_knowledge
19
- scope: local
20
- ai-reports:
13
+ dev-compose:
21
14
  repo: sequre/ai_knowledge
22
- scope: local
23
- environment:
24
- space_id: "${AI_REPORTS_SPACE_ID}"
25
- bug_reports_list_id: "${AI_REPORTS_BUG_REPORTS_LIST_ID}"
26
- improvements_list_id: "${AI_REPORTS_IMPROVEMENTS_LIST_ID}"
27
- agent-review:
15
+ dev-enrich:
28
16
  repo: sequre/ai_knowledge
29
- scope: local
30
- environment:
31
- space_id: "${AGENT_REVIEW_SAPCE_ID}"
32
- list_id: "${AGENT_LIST_ID}"
33
- action-plan:
17
+ skill-feedback:
18
+ repo: sequre/ai_knowledge
19
+ agent-issue:
20
+ repo: sequre/ai_knowledge
21
+ dev-flow:
22
+ repo: sequre/ai_knowledge
23
+ matrix-element:
34
24
  repo: sequre/ai_knowledge
35
25
  environment:
36
- space_id: "${AGENT_REVIEW_SAPCE_ID}"
37
- list_id: "${AGENT_ACTION_PLAN_LIST_ID}"
26
+ homeserver: "https://matrix.cloud.wispro.co"
27
+ auth_token: "${MATRIX_AUTH_TOKEN}"
28
+ rooms:
29
+ agents: "!VCHwQXgmXdyhhhPhoz:matrix.cloud.wispro.co"
38
30
  documentation-writer:
39
31
  repo: github/awesome-copilot
40
32
  path: skills/documentation-writer
41
- scope: local
42
- find-skills:
43
- repo: vercel-labs/skills
44
- path: skills/find-skills
45
- scope: local
46
33
  opentelemetry:
47
34
  repo: bobmatnyc/claude-mpm-skills
48
35
  path: universal/observability/opentelemetry
49
- scope: local
@@ -463,6 +463,72 @@ RSpec.describe ExisRay::JsonFormatter do
463
463
  end
464
464
  end
465
465
 
466
+ describe "dedup de claves Symbol/String (issue #12)" do
467
+ def stub_tracer_with_task(task_value)
468
+ stub_const("ExisRay::Tracer", Module.new do
469
+ define_singleton_method(:service_name) { "test-service" }
470
+ define_singleton_method(:root_id) { "1-abc-def" }
471
+ define_singleton_method(:trace_id) { nil }
472
+ define_singleton_method(:request_id) { nil }
473
+ define_singleton_method(:source) { "task" }
474
+ define_singleton_method(:sidekiq_job) { nil }
475
+ define_singleton_method(:task) { task_value }
476
+ end)
477
+ end
478
+
479
+ it "no emite warning de `duplicate key` cuando KV string y Tracer setean la misma key" do
480
+ stub_tracer_with_task("billing:invoices")
481
+
482
+ warning = nil
483
+ original_warn = Warning.method(:warn)
484
+ Warning.singleton_class.define_method(:warn) { |msg, **| warning = msg }
485
+
486
+ begin
487
+ call("event=task_started task=billing:invoices outcome=started")
488
+ ensure
489
+ Warning.singleton_class.define_method(:warn, original_warn)
490
+ end
491
+
492
+ expect(warning).to be_nil
493
+ end
494
+
495
+ it "developer payload (KV string) pisa al contexto canónico inyectado por Tracer" do
496
+ stub_tracer_with_task("billing:invoices")
497
+
498
+ result = call("event=foo task=override outcome=success")
499
+
500
+ expect(result["task"]).to eq("override")
501
+ end
502
+
503
+ it "emite la key como String aunque internamente fuese Symbol" do
504
+ stub_tracer_with_task("billing:invoices")
505
+
506
+ json_str = formatter.call(severity, timestamp, progname, "event=task_started outcome=started")
507
+
508
+ # Si quedaran ambas (`:task` y `"task"`) el JSON tendría dos veces la key.
509
+ expect(json_str.scan(/"task"\s*:/).size).to eq(1)
510
+ expect(JSON.parse(json_str)["task"]).to eq("billing:invoices")
511
+ end
512
+
513
+ it "no incluye keys con valor nil en el output" do
514
+ stub_const("ExisRay::Tracer", Module.new do
515
+ define_singleton_method(:service_name) { "test-service" }
516
+ define_singleton_method(:root_id) { nil }
517
+ define_singleton_method(:trace_id) { nil }
518
+ define_singleton_method(:request_id) { nil }
519
+ define_singleton_method(:source) { nil }
520
+ define_singleton_method(:sidekiq_job) { nil }
521
+ define_singleton_method(:task) { nil }
522
+ end)
523
+
524
+ result = call("event=foo")
525
+
526
+ expect(result).not_to have_key("root_id")
527
+ expect(result).not_to have_key("task")
528
+ expect(result).not_to have_key("source")
529
+ end
530
+ end
531
+
466
532
  describe "#filter_sensitive_hash (privado)" do
467
533
  it "filtra claves sensibles en el nivel raíz" do
468
534
  result = formatter.send(:filter_sensitive_hash, { user: "gabriel", password: "secret" })
@@ -49,7 +49,22 @@ RSpec.describe ExisRay::TaskMonitor do
49
49
  expect(start_log[0]).to eq(:info)
50
50
  expect(start_log[1]).to include("outcome=started")
51
51
  expect(start_log[1]).to include("event=task_started")
52
- expect(start_log[1]).to include("task=billing:invoices")
52
+ end
53
+
54
+ it "no emite `task=` en el KV (la clave viene del formatter vía Tracer.task)" do
55
+ captured_task = nil
56
+ described_class.run("billing:invoices") { captured_task = ExisRay::Tracer.task }
57
+
58
+ # task_name vive en el Tracer durante la ejecución del bloque
59
+ expect(captured_task).to eq("billing:invoices")
60
+
61
+ # Los KV de lifecycle NO incluyen `task=...` — evita la duplicación con
62
+ # `JsonFormatter#inject_tracer_context` (issue #12).
63
+ lifecycle = captured_logs.map { |_, msg| msg }.select { |m| m.include?("event=task_") }
64
+ expect(lifecycle).not_to be_empty
65
+ lifecycle.each do |msg|
66
+ expect(msg).not_to match(/\btask=/)
67
+ end
53
68
  end
54
69
 
55
70
  it "loguea task_finished con outcome=success (breaking rename v0.6.0)" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exis_ray
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Edera
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-19 00:00:00.000000000 Z
11
+ date: 2026-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -90,8 +90,8 @@ licenses:
90
90
  metadata:
91
91
  homepage_uri: https://github.com/gedera/exis_ray
92
92
  source_code_uri: https://github.com/gedera/exis_ray
93
- changelog_uri: https://github.com/gedera/exis_ray/blob/v0.9.0/CHANGELOG.md
94
- documentation_uri: https://github.com/gedera/exis_ray/blob/v0.9.0/skill
93
+ changelog_uri: https://github.com/gedera/exis_ray/blob/v0.10.0/CHANGELOG.md
94
+ documentation_uri: https://github.com/gedera/exis_ray/blob/v0.10.0/skill
95
95
  post_install_message:
96
96
  rdoc_options: []
97
97
  require_paths: