# frozen_string_literal: true # adn/tools/eventos.rb — Bus de eventos interno para el sistema ADN # ================================================================== # Este módulo implementa un bus de eventos que permite la comunicación # desacoplada entre los diferentes componentes del sistema ADN. # Facilita la implementación del Principio de Armonía Integral al # permitir que cambios en un componente disparen acciones automáticas # en otros componentes dependientes. # # 📅 Última actualización: 2026-03-07 (Fase 2: Automatización Inteligente) # 📚 Referencia: docs/plan/260307-1400_Plan_Mejoras_ADN.md # 🔗 Integración: adn/triggers.yml (configuración de triggers) require 'singleton' module ADN class Eventos include Singleton # Eventos predefinidos del sistema ADN # Estos eventos corresponden a los triggers definidos en adn/triggers.yml EVENTOS = { # Bitácoras bitacora_creada: :bitacora_creada, bitacora_modificada: :bitacora_modificada, bitacora_guardada: :bitacora_guardada, nueva_vinyeta_cronologia: :nueva_vinyeta_cronologia, estado_cambiado: :estado_cambiado, metrica_hibrida_registrada: :metrica_hibrida_registrada, error_formato_detectado: :error_formato_detectado, # Nodos nodo_creado: :nodo_creado, nodo_modificado: :nodo_modificado, estado_nodo_cambiado: :estado_nodo_cambiado, # Proyectos proyecto_creado: :proyecto_creado, proyecto_modificado: :proyecto_modificado, hito_proyecto_actualizado: :hito_proyecto_actualizado, estado_proyecto_cambiado: :estado_proyecto_cambiado, # ADN y configuración hebra_adn_modificada: :hebra_adn_modificada, iconografia_actualizada: :iconografia_actualizada, directivas_ia_actualizadas: :directivas_ia_actualizadas, configuracion_actualizada: :configuracion_actualizada, # Sistema sistema_iniciado: :sistema_iniciado, validacion_completada: :validacion_completada, error_sistema: :error_sistema, advertencia_sistema: :advertencia_sistema }.freeze # Inicializa el bus de eventos def initialize @suscriptores = Hash.new { |hash, key| hash[key] = [] } @logger = ADN.logger @mutex = Mutex.new @logger.debug('Bus de eventos ADN inicializado') end # Registra un suscriptor para un tipo de evento # # @param evento [Symbol, String] Tipo de evento al que suscribirse # @param bloque [Proc] Bloque a ejecutar cuando se publique el evento # @param identificador [String] Identificador opcional para el suscriptor # @return [String] Identificador del suscriptor def suscriptor(evento, identificador = nil, &bloque) evento_normalizado = normalizar_evento(evento) @mutex.synchronize do id = identificador || "suscriptor_#{@suscriptores[evento_normalizado].length + 1}" @suscriptores[evento_normalizado] << { id: id, bloque: bloque } @logger.debug("Suscriptor registrado: #{id} para evento #{evento_normalizado}") id end end # Publica un evento a todos los suscriptores registrados # # @param evento [Symbol, String] Tipo de evento a publicar # @param datos [Hash] Datos asociados al evento # @param opciones [Hash] Opciones de publicación # @option opciones [Boolean] :async Ejecutar suscriptores en hilos separados # @option opciones [Boolean] :silencioso No loggear la publicación # @return [Array] Resultados de la ejecución de suscriptores def publicar(evento, datos = {}, opciones = {}) evento_normalizado = normalizar_evento(evento) suscriptores = @suscriptores[evento_normalizado].dup return [] if suscriptores.empty? @logger.debug("Publicando evento #{evento_normalizado} a #{suscriptores.length} suscriptor(es)", datos) unless opciones[:silencioso] resultados = [] suscriptores.each do |suscriptor| resultado = ejecutar_suscriptor(suscriptor, evento_normalizado, datos, opciones) resultados << resultado if resultado end resultados end # Elimina un suscriptor específico # # @param evento [Symbol, String] Tipo de evento # @param identificador [String] Identificador del suscriptor a eliminar # @return [Boolean] true si se eliminó, false si no se encontró def eliminar_suscriptor(evento, identificador) evento_normalizado = normalizar_evento(evento) @mutex.synchronize do inicial = @suscriptores[evento_normalizado].length @suscriptores[evento_normalizado].reject! { |s| s[:id] == identificador } eliminados = inicial - @suscriptores[evento_normalizado].length if eliminados > 0 @logger.debug("Suscriptor eliminado: #{identificador} del evento #{evento_normalizado}") true else @logger.advertencia("Suscriptor no encontrado: #{identificador} para evento #{evento_normalizado}") false end end end # Elimina todos los suscriptores de un evento # # @param evento [Symbol, String] Tipo de evento # @return [Integer] Número de suscriptores eliminados def limpiar_suscriptores(evento = nil) @mutex.synchronize do if evento.nil? total = @suscriptores.values.flatten.length @suscriptores.clear @logger.debug("Todos los suscriptores eliminados (#{total} total)") total else evento_normalizado = normalizar_evento(evento) eliminados = @suscriptores[evento_normalizado].length @suscriptores.delete(evento_normalizado) @logger.debug("#{eliminados} suscriptor(es) eliminados del evento #{evento_normalizado}") eliminados end end end # Lista todos los eventos con suscriptores # # @return [Hash] Mapa de evento -> número de suscriptores def lista_suscriptores resultado = {} @mutex.synchronize do @suscriptores.each do |evento, suscriptores| resultado[evento] = suscriptores.length end end resultado end # Verifica si hay suscriptores para un evento # # @param evento [Symbol, String] Tipo de evento # @return [Boolean] true si hay suscriptores def tiene_suscriptores?(evento) evento_normalizado = normalizar_evento(evento) !@suscriptores[evento_normalizado].empty? end # Métodos de conveniencia para eventos comunes # Publica un evento de archivo modificado # # @param ruta [String] Ruta del archivo modificado # @param tipo [String] Tipo de archivo (bitacora, nodo, proyecto, hebra) # @param accion [String] Acción (creado, modificado, eliminado) # @param datos_extra [Hash] Datos adicionales def archivo_modificado(ruta, tipo, accion, datos_extra = {}) datos = { ruta: ruta, tipo: tipo, accion: accion, timestamp: Time.now.utc }.merge(datos_extra) evento = case tipo when 'bitacora' then :bitacora_modificada when 'nodo' then :nodo_modificado when 'proyecto' then :proyecto_modificado when 'hebra' then :hebra_adn_modificada else :archivo_modificado end publicar(evento, datos) end # Publica un evento de estado cambiado # # @param entidad [String] Tipo de entidad (nodo, proyecto, tarea) # @param nombre [String] Nombre de la entidad # @param estado_anterior [String] Estado anterior # @param estado_nuevo [String] Estado nuevo # @param datos_extra [Hash] Datos adicionales def estado_cambiado(entidad, nombre, estado_anterior, estado_nuevo, datos_extra = {}) datos = { entidad: entidad, nombre: nombre, estado_anterior: estado_anterior, estado_nuevo: estado_nuevo, timestamp: Time.now.utc }.merge(datos_extra) publicar(:estado_cambiado, datos) end # Publica un evento de error detectado # # @param tipo [String] Tipo de error (formato, validacion, sistema) # @param ubicacion [String] Ubicación del error (archivo, módulo) # @param mensaje [String] Mensaje descriptivo del error # @param severidad [String] Severidad (baja, media, alta, critica) # @param datos_extra [Hash] Datos adicionales def error_detectado(tipo, ubicacion, mensaje, severidad = 'media', datos_extra = {}) datos = { tipo: tipo, ubicacion: ubicacion, mensaje: mensaje, severidad: severidad, timestamp: Time.now.utc }.merge(datos_extra) publicar(:error_formato_detectado, datos) end # Publica un evento de validación completada # # @param tipo [String] Tipo de validación (bitacoras, nodos, proyectos, completa) # @param resultados [Hash] Resultados de la validación def validacion_completada(tipo, resultados) datos = { tipo: tipo, resultados: resultados, timestamp: Time.now.utc } publicar(:validacion_completada, datos) end private # Normaliza un evento a símbolo # # @param evento [Symbol, String] Evento a normalizar # @return [Symbol] Evento normalizado def normalizar_evento(evento) evento.to_s.to_sym end # Ejecuta un suscriptor individual con manejo de errores # # @param suscriptor [Hash] Información del suscriptor # @param evento [Symbol] Evento que se está publicando # @param datos [Hash] Datos del evento # @param opciones [Hash] Opciones de publicación # @return [Hash] Resultado de la ejecución def ejecutar_suscriptor(suscriptor, evento, datos, opciones) id = suscriptor[:id] bloque = suscriptor[:bloque] inicio = Time.now begin resultado = bloque.call(datos.dup.freeze) duracion = Time.now - inicio { suscriptor: id, evento: evento, exito: true, duracion: duracion, resultado: resultado, timestamp: inicio } rescue => e duracion = Time.now - inicio @logger.error("Error en suscriptor #{id} para evento #{evento}: #{e.message}", { backtrace: e.backtrace.take(5), evento: evento, datos: datos }) { suscriptor: id, evento: evento, exito: false, duracion: duracion, error: e.message, backtrace: e.backtrace.take(5), timestamp: inicio } end end end # Acceso global al bus de eventos # # @return [ADN::Eventos] Instancia singleton del bus de eventos def self.eventos Eventos.instance end end # ─── Notas de uso ────────────────────────────────────────────────────── # # Ejemplo de suscripción: # ADN.eventos.suscriptor(:bitacora_creada) do |datos| # puts "Nueva bitácora creada: #{datos[:ruta]}" # end # # Ejemplo de publicación: # ADN.eventos.publicar(:bitacora_creada, { # ruta: 'bitacoras/2026-03-07.md', # fecha: Date.today # }) # # Ejemplo de publicación con método de conveniencia: # ADN.eventos.archivo_modificado( # 'bitacoras/2026-03-07.md', # 'bitacora', # 'creado' # ) # # El bus de eventos es especialmente útil para: # - Implementar triggers basados en eventos # - Desacoplar componentes del sistema # - Facilitar la extensibilidad mediante plugins # - Loggear actividades del sistema de forma centralizada #