# frozen_string_literal: true # tools/adn/triggers.rb — Sistema de triggers para sincronización automática del ADN # ============================================================================= # Este módulo implementa el sistema de triggers definido en adn/triggers.yml. # Los triggers implementan el Principio de Armonía Integral al propagar # automáticamente cambios entre diferentes componentes del sistema ADN. # # 📅 Última actualización: 2026-03-07 (Fase 2: Automatización Inteligente) # 📚 Referencia: adn/triggers.yml (configuración) # 📚 Plan: docs/plan/260307-1400_Plan_Mejoras_ADN.md # 🔗 Integración: tools/adn/eventos.rb (bus de eventos) require 'yaml' require 'singleton' require_relative 'eventos' module ADN class Triggers include Singleton # Inicializa el sistema de triggers def initialize @config_path = File.join(ADN::PROJECT_ROOT, 'adn', 'triggers.yml') @config = nil @suscriptores_registrados = {} @logger = ADN.logger @mutex = Mutex.new @logger.debug('Sistema de triggers ADN inicializado') end # Carga la configuración de triggers desde el archivo YAML # # @return [Hash] Configuración cargada # @raise [Errno::ENOENT] Si el archivo no existe # @raise [Psych::SyntaxError] Si el YAML es inválido def cargar_configuracion unless File.exist?(@config_path) @logger.advertencia("Archivo de configuración de triggers no encontrado: #{@config_path}") return {} end begin @config = YAML.load_file(@config_path, permitted_classes: [Date, Time, Symbol]) @logger.info("Configuración de triggers cargada desde #{@config_path}") @config rescue Psych::SyntaxError => e @logger.error("Error de sintaxis YAML en #{@config_path}: #{e.message}") raise rescue => e @logger.error("Error cargando configuración de triggers: #{e.message}") raise end end # Obtiene la configuración actual (carga si es necesario) # # @return [Hash] Configuración actual def configuracion @config || cargar_configuracion end # Registra todos los triggers configurados en el bus de eventos # # @return [Integer] Número de triggers registrados def registrar_triggers config = configuracion return 0 unless config['configuracion'] && config['configuracion']['sistema_habilitado'] @mutex.synchronize do limpiar_triggers_registrados total_registrados = 0 # Registrar triggers de cada categoría %w[bitacoras nodos proyectos adn].each do |categoria| next unless config[categoria] config[categoria].each do |trigger_def| next unless trigger_def['habilitado'] registrar_trigger_individual(trigger_def, categoria) total_registrados += 1 end end @logger.exito("#{total_registrados} trigger(s) registrado(s) en el bus de eventos") total_registrados end end # Ejecuta todos los triggers registrados (para modo batch/cron) # # @param opciones [Hash] Opciones de ejecución # @option opciones [Boolean] :forzar Ejecutar incluso si sistema deshabilitado # @option opciones [Array] :categorias Categorías específicas a ejecutar # @return [Hash] Resultados de la ejecución def ejecutar_triggers(opciones = {}) resultados = { timestamp: Time.now.utc, total_ejecutados: 0, exitosos: 0, fallidos: 0, detalles: [] } config = configuracion # Verificar si el sistema está habilitado unless config['configuracion'] && (config['configuracion']['sistema_habilitado'] || opciones[:forzar]) @logger.advertencia('Sistema de triggers deshabilitado (usar --forzar para ejecutar de todos modos)') return resultados end categorias = opciones[:categorias] || %w[bitacoras nodos proyectos adn] categorias.each do |categoria| next unless config[categoria] config[categoria].each do |trigger_def| next unless trigger_def['habilitado'] resultado = ejecutar_trigger_individual(trigger_def, categoria, opciones) resultados[:detalles] << resultado resultados[:total_ejecutados] += 1 resultados[resultado[:exito] ? :exitosos : :fallidos] += 1 end end resultados end # Prueba un trigger específico sin afectar el sistema # # @param identificador [String] Identificador del trigger a probar # @param datos_simulados [Hash] Datos para simular el evento # @return [Hash] Resultado de la prueba def probar_trigger(identificador, datos_simulados = {}) trigger = buscar_trigger_por_identificador(identificador) unless trigger @logger.error("Trigger no encontrado: #{identificador}") return { exito: false, error: "Trigger no encontrado: #{identificador}" } end @logger.info("Probando trigger: #{identificador}") resultado = { trigger: identificador, timestamp: Time.now.utc, datos_simulados: datos_simulados } begin # Simular la ejecución sin efectos reales accion_resultado = simular_accion(trigger['accion'], trigger['categoria'], datos_simulados) resultado.merge!( exito: true, accion_simulada: accion_resultado, mensaje: "Trigger probado exitosamente (modo simulación)" ) rescue => e resultado.merge!( exito: false, error: e.message, backtrace: e.backtrace.take(5) ) @logger.error("Error probando trigger #{identificador}: #{e.message}") end resultado end # Obtiene información sobre todos los triggers configurados # # @return [Array] Lista de triggers con información detallada def lista_triggers config = configuracion triggers = [] %w[bitacoras nodos proyectos adn].each do |categoria| next unless config[categoria] config[categoria].each_with_index do |trigger_def, index| triggers << { id: "#{categoria}_#{index}", categoria: categoria, evento: trigger_def['evento'], condicion: trigger_def['condicion'], accion: trigger_def['accion'], descripcion: trigger_def['descripcion'], habilitado: trigger_def['habilitado'], suscriptores_registrados: @suscriptores_registrados["#{categoria}_#{index}"] || 0 } end end triggers end # Limpia todos los triggers registrados del bus de eventos # # @return [Integer] Número de triggers limpiados def limpiar_triggers_registrados total = 0 @suscriptores_registrados.each do |trigger_id, _| ADN.eventos.eliminar_suscriptor(trigger_id, "trigger_#{trigger_id}") total += 1 end @suscriptores_registrados.clear @logger.debug("#{total} trigger(s) limpiados del bus de eventos") total end private # Registra un trigger individual en el bus de eventos # # @param trigger_def [Hash] Definición del trigger # @param categoria [String] Categoría del trigger # @param index [Integer] Índice dentro de la categoría def registrar_trigger_individual(trigger_def, categoria, index = nil) evento = trigger_def['evento'] condicion = trigger_def['condicion'] accion = trigger_def['accion'] descripcion = trigger_def['descripcion'] trigger_id = index ? "#{categoria}_#{index}" : "#{categoria}_#{Time.now.to_i}" # Registrar en el bus de eventos suscriptor_id = ADN.eventos.suscriptor(evento, "trigger_#{trigger_id}") do |datos| ejecutar_accion_si_cumple_condicion(accion, condicion, datos, trigger_id) end @suscriptores_registrados[trigger_id] = { evento: evento, condicion: condicion, accion: accion, descripcion: descripcion, suscriptor_id: suscriptor_id } @logger.debug("Trigger registrado: #{trigger_id} (#{descripcion})") end # Ejecuta una acción si se cumple la condición # # @param accion [String] Acción a ejecutar # @param condicion [String] Condición a evaluar # @param datos [Hash] Datos del evento # @param trigger_id [String] Identificador del trigger def ejecutar_accion_si_cumple_condicion(accion, condicion, datos, trigger_id) # Evaluar condición si existe if condicion && !condicion.empty? unless evaluar_condicion(condicion, datos) @logger.debug("Condición no cumplida para trigger #{trigger_id}") return end end # Ejecutar acción ejecutar_accion(accion, datos, trigger_id) end # Evalúa una condición contra los datos del evento # # @param condicion [String] Expresión de condición # @param datos [Hash] Datos del evento # @return [Boolean] true si la condición se cumple def evaluar_condicion(condicion, datos) # Implementación básica - en una versión futura podría usar un parser # Para ahora, solo verifica condiciones simples case condicion when /estado_anterior == '([^']+)' and estado_nuevo == '([^']+)'/ estado_anterior_esperado = $1 estado_nuevo_esperado = $2 datos[:estado_anterior] == estado_anterior_esperado && datos[:estado_nuevo] == estado_nuevo_esperado when /estado_nuevo == '([^']+)'/ datos[:estado_nuevo] == $1 when /estado_tema == '([^']+)'/ datos[:estado_tema] == $1 else # Si no reconocemos la condición, asumimos que se cumple # (para triggers sin condición explícita) true end end # Ejecuta una acción específica # # @param accion [String] Nombre de la acción # @param datos [Hash] Datos del evento # @param trigger_id [String] Identificador del trigger def ejecutar_accion(accion, datos, trigger_id) inicio = Time.now @logger.info("Ejecutando acción #{accion} (trigger: #{trigger_id})") begin resultado = case accion when 'actualizar_detalle_en_proceso' accion_actualizar_detalle_en_proceso(datos) when 'actualizar_resumen_integral' accion_actualizar_resumen_integral(datos) when 'ejecutar_rollover' accion_ejecutar_rollover(datos) when 'extraer_metricas_telemetria' accion_extraer_metricas_telemetria(datos) when 'validar_formato_adn' accion_validar_formato_adn(datos) when 'actualizar_ontologia' accion_actualizar_ontologia(datos) when 'vincular_nodo_a_proyectos' accion_vincular_nodo_a_proyectos(datos) when 'actualizar_dashboard_estados' accion_actualizar_dashboard_estados(datos) when 'registrar_en_bitacora' accion_registrar_en_bitacora(datos) when 'actualizar_referencias_cruzadas' accion_actualizar_referencias_cruzadas(datos) when 'generar_reporte_cierre' accion_generar_reporte_cierre(datos) when 'verificar_impacto_bitacoras' accion_verificar_impacto_bitacoras(datos) when 'actualizar_iconos_obsoletos' accion_actualizar_iconos_obsoletos(datos) when 'notificar_y_aplicar_directivas' accion_notificar_y_aplicar_directivas(datos) else @logger.advertencia("Acción desconocida: #{accion}") { accion: accion, estado: 'desconocida', mensaje: 'Acción no implementada' } end duracion = Time.now - inicio @logger.exito("Acción #{accion} completada en #{duracion.round(3)}s") resultado rescue => e duracion = Time.now - inicio @logger.error("Error ejecutando acción #{accion}: #{e.message}", { trigger_id: trigger_id, datos: datos, backtrace: e.backtrace.take(5) }) { accion: accion, estado: 'error', error: e.message, duracion: duracion } end end # Simula una acción sin efectos reales (para pruebas) # # @param accion [String] Nombre de la acción # @param categoria [String] Categoría del trigger # @param datos [Hash] Datos simulados # @return [Hash] Resultado de la simulación def simular_accion(accion, categoria, datos) { accion: accion, categoria: categoria, modo: 'simulacion', mensaje: "Esta acción se habría ejecutado con los datos: #{datos.keys.join(', ')}", timestamp: Time.now.utc } end # Busca un trigger por identificador # # @param identificador [String] Identificador del trigger # @return [Hash, nil] Definición del trigger encontrado o nil def buscar_trigger_por_identificador(identificador) config = configuracion %w[bitacoras nodos proyectos adn].each do |categoria| next unless config[categoria] config[categoria].each_with_index do |trigger_def, index| trigger_id = "#{categoria}_#{index}" return trigger_def.merge('categoria' => categoria, 'id' => trigger_id) if trigger_id == identificador end end nil end # Ejecuta un trigger individual # # @param trigger_def [Hash] Definición del trigger # @param categoria [String] Categoría del trigger # @param opciones [Hash] Opciones de ejecución # @return [Hash] Resultado de la ejecución def ejecutar_trigger_individual(trigger_def, categoria, opciones) inicio = Time.now trigger_id = "#{categoria}_#{Time.now.to_i}_#{rand(1000)}" resultado = { trigger_id: trigger_id, evento: trigger_def['evento'], categoria: categoria, descripcion: trigger_def['descripcion'], inicio: inicio, exito: false } begin # Simular datos para ejecución manual datos_simulados = { modo: 'manual', trigger_id: trigger_id, timestamp: inicio } accion_resultado = ejecutar_accion(trigger_def['accion'], datos_simulados, trigger_id) resultado.merge!( exito: true, fin: Time.now, duracion: Time.now - inicio, accion_resultado: accion_resultado ) rescue => e resultado.merge!( fin: Time.now, duracion: Time.now - inicio, error: e.message, backtrace: e.backtrace.take(3) ) end resultado end # Implementaciones de acciones específicas (stubs para Fase 2) def accion_actualizar_detalle_en_proceso(datos) { accion: 'actualizar_detalle_en_proceso', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_actualizar_resumen_integral(datos) { accion: 'actualizar_resumen_integral', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_ejecutar_rollover(datos) { accion: 'ejecutar_rollover', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_extraer_metricas_telemetria(datos) { accion: 'extraer_metricas_telemetria', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_validar_formato_adn(datos) { accion: 'validar_formato_adn', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_actualizar_ontologia(datos) { accion: 'actualizar_ontologia', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_vincular_nodo_a_proyectos(datos) { accion: 'vincular_nodo_a_proyectos', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_actualizar_dashboard_estados(datos) { accion: 'actualizar_dashboard_estados', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_registrar_en_bitacora(datos) { accion: 'registrar_en_bitacora', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_actualizar_referencias_cruzadas(datos) { accion: 'actualizar_referencias_cruzadas', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_generar_reporte_cierre(datos) { accion: 'generar_reporte_cierre', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_verificar_impacto_bitacoras(datos) { accion: 'verificar_impacto_bitacoras', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_actualizar_iconos_obsoletos(datos) { accion: 'actualizar_iconos_obsoletos', estado: 'stub', mensaje: 'Implementado en Fase 2' } end def accion_notificar_y_aplicar_directivas(datos) { accion: 'notificar_y_aplicar_directivas', estado: 'stub', mensaje: 'Implementado en Fase 2' } end end # Acceso global al sistema de triggers # # @return [ADN::Triggers] Instancia singleton del sistema de triggers def self.triggers Triggers.instance end end # ─── Notas de uso ────────────────────────────────────────────────────── # # Ejemplo de registro y uso: # # Cargar configuración y registrar triggers # ADN.triggers.cargar_configuracion # ADN.triggers.registrar_triggers # # Ejemplo de ejecución manual: # resultados = ADN.triggers.ejecutar_triggers(forzar: true) # # Ejemplo de prueba de trigger: # resultado = ADN.triggers.probar_trigger('bitacoras_0', { estado_tema: '⏳' }) # # El sistema de triggers se integra automáticamente con el bus de eventos. # Cuando ocurre un evento (ej: bitácora modificada), los triggers registrados # que escuchan ese evento se ejecutarán automáticamente si se cumplen las condiciones. # # ⚠️ ADVERTENCIA: Este módulo está en desarrollo (Fase 2). # Las acciones específicas se implementarán en iteraciones posteriores.