526 lines
18 KiB
Ruby
526 lines
18 KiB
Ruby
# 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 '../adn'
|
|
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<String>] :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<Hash>] 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.
|