[ADN] Fase 10: Saneamiento y Consolidación - Un solo punto de verdad

- S1: Eliminar 'triggers' duplicado en run (líneas 109/131)
- S2: Eliminar cli/commit.rb (truncado, sin uso)
- S3: Eliminar cli/inicio.rb y cli/cierre.rb (legacy, jornada.rb los reemplaza)
- S4: Mover 4 planes obsoletos de docs/plan/adn/ a docs/_hist/plan/adn/
- S5: Mover 14 backups de planes y 3 docs técnicos a docs/_hist/
- S6: Eliminar manifiesto vacío P2604_proyecto_p2604.md
- S7: Corregir referencia a plan obsoleto en run
- S8: Dejar de cargar core/validador.rb obsoleto en run
- S9: Limpiar progreso duplicado en P2604_mejoras_ADN.md
- Agregar Fase 10 al plan P2604 con 10 tareas
- Un solo punto de verdad: adn/README.md
- Un solo plan: docs/proy/p2604_mejoras_ADN/P2604_mejoras_ADN.md
- Registro en bitácora: evento 1073
This commit is contained in:
Ricardo Monla
2026-03-15 14:33:32 -03:00
parent b13a389568
commit cfd0e8452a
36 changed files with 1563 additions and 238 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
---
BUNDLE_PATH: "vendor/bundle"
BUNDLE_WITHOUT: "development:test:future"
BUNDLE_WITHOUT: "future"
+17
View File
@@ -0,0 +1,17 @@
AllCops:
NewCops: enable
Exclude:
- 'vendor/**/*'
TargetRubyVersion: 3.1
Style/FrozenStringLiteralComment:
Enabled: false
Style/Documentation:
Enabled: false
Metrics/MethodLength:
Max: 30
Layout/LineLength:
Max: 120
+17
View File
@@ -33,6 +33,7 @@ class SubcomandoAyuda
salud [--dashboard] [--json] Ver salud general del sistema ADN
generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
generar nodo <nombre> Generar ficha de nodo
msp <subcomando> Gestión de MSPs NotebookLM (auditar, listar, registrar)
#{Color::YELLOW}generar proyecto <código>#{Color::RESET} Generar manifiesto de proyecto
#{Color::YELLOW}tailscale <acción>#{Color::RESET} Gestión de identidades Tailscale
#{Color::YELLOW}backup <nodo> [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo
@@ -161,6 +162,22 @@ class SubcomandoAyuda
puts "Ejemplos:"
puts " ./adn/tools/run tailscale switch dasuten"
puts " ./adn/tools/run tailscale status"
when 'msp'
puts "Uso: ./adn/tools/run msp <subcomando> [opciones]"
puts ""
puts "Gestión de MSPs (Markdown Style Papers) de NotebookLM:"
puts " • auditar Verificar estado de conexión MCP y NotebookLM"
puts " • listar Listar MSPs disponibles en el sistema"
puts " • registrar Registrar un MSP descargado"
puts ""
puts "Opciones:"
puts " --archivo RUTA Ruta al archivo MSP (para registrar)"
puts " --titulo TITULO Título del MSP (opcional)"
puts ""
puts "Ejemplos:"
puts " ./adn/tools/run msp auditar"
puts " ./adn/tools/run msp listar"
puts " ./adn/tools/run msp registrar --archivo docs/mcps/mi-msp.md"
else
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos."
-58
View File
@@ -1,58 +0,0 @@
# frozen_string_literal: true
# adn/tools/cli/cierre.rb — Subcomando para cierre de jornada laboral (DB-First)
# ==============================================================================
require 'date'
require 'time'
require_relative '../db/core/bitacora_db'
module ADN
class SubcomandoCierre
require_relative '../core/colores'
def initialize(args, logger)
@args = args
@logger = logger
@fecha = Date.today
end
def ejecutar
if @args.empty? || !(@args.include?('jornada'))
puts "#{Color::YELLOW}Uso: ./adn/tools/run cierre de jornada [presencial|remoto]#{Color::RESET}"
return
end
modo = @args.include?('presencial') ? 'P' : 'R'
tipo_str = modo == 'P' ? 'Presencial' : 'Remoto'
hora_fin = Time.now.strftime('%H:%M')
@logger.info("Iniciando cierre de jornada #{tipo_str} (Modo DB-First)")
BitacorasDB::BitacoraDB.with_connection do |db|
# 1. Buscar la bitácora
bitacora = db.find_bitacora_by_fecha(@fecha)
unless bitacora
@logger.error("No existe bitácora en DB para hoy (#{@fecha}). ¿Iniciaste la jornada?")
return
end
# 2. Obtener nodo srv-ns8
nodo = db.find_nodo_by_nombre('srv-ns8')
# 3. Crear entrada de cierre
db.create_entrada(
inicio: hora_fin,
fin: hora_fin,
descripcion: "Cierre de jornada #{tipo_str.downcase}. Sincronización final y validación de backups.",
estado: '✅',
modo: modo,
bitacora_id: bitacora['id'],
nodo_id: nodo ? nodo['id'] : nil
)
@logger.exito("Cierre de jornada registrado en Base de Datos.")
end
end
end
end
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# ------------------------------------------------------------------------------
# CLI: commit.rb - Herramienta para gestión de commits según principios ADN
# ------------------------------------------------------------------------------
# Principios aplicados:
# 1. Menos es Más: Componente atómico y reutilizable para gestión de commits
# 2. Armonía Integral: Val
+28 -49
View File
@@ -2,6 +2,7 @@
require 'optparse'
require_relative '../core/colores'
require_relative '../core/nodos_info'
class SubcomandoContexto
def initialize(args, logger)
@@ -60,77 +61,55 @@ class SubcomandoContexto
exit 1
end
contenido = File.read(ruta_nodo, encoding: 'UTF-8')
metadata = ADN::NodosInfo.extraer_metadata(nombre_nodo)
# Extraer información básica
puts "#{Color::CYAN}📋 Contexto del nodo: #{nombre_nodo}#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
# Buscar IP en la tabla
if contenido.match(/\|\s*IP (?:LAN|Pública)?\s*\|\s*([^\|]+)\s*\|/)
ip = $1.strip
puts " #{Color::GREEN}IP:#{Color::RESET} #{ip}"
if metadata
puts " #{Color::GREEN}IP:#{Color::RESET} #{metadata[:ip] || '-'}"
puts " #{Color::GREEN}Rol:#{Color::RESET} #{metadata[:host] ? 'VM' : (metadata[:so] || '-')}"
puts " #{Color::GREEN}SO:#{Color::RESET} #{metadata[:so] || '-'}"
puts " #{Color::GREEN}Host:#{Color::RESET} #{metadata[:host] || '-'}"
puts " #{Color::GREEN}SSH:#{Color::RESET} #{metadata[:usuario] || 'rmonla'}@#{metadata[:ip] || '?'}:#{metadata[:puerto_ssh] || 22}"
if @opciones[:cmd]
generar_comando_ssh(nombre_nodo, ip)
generar_comando_ssh(nombre_nodo, metadata[:ip], metadata[:usuario])
end
end
# Buscar rol
if contenido.match(/\|\s*Rol(?: Principal)?\s*\|\s*([^\|]+)\s*\|/)
rol = $1.strip
puts " #{Color::GREEN}Rol:#{Color::RESET} #{rol}"
end
# Buscar sistema operativo
if contenido.match(/\|\s*Sistema Operativo\s*\|\s*([^\|]+)\s*\|/)
so = $1.strip
puts " #{Color::GREEN}SO:#{Color::RESET} #{so}"
end
# Buscar en ontología para contexto de red
config_path = File.join(__dir__, '../config', 'config.yml')
ontologia_path = File.join(ADN::PROJECT_ROOT, 'adn', '01_ontologia.md')
if File.exist?(ontologia_path)
ontologia = File.read(ontologia_path, encoding: 'UTF-8')
if ontologia.include?(nombre_nodo)
puts " #{Color::GREEN}En ontología:#{Color::RESET}"
# Intentar encontrar en qué cluster está
clusters = ['srv-pmox1', 'srv-pmox2', 'srv-pmox3', 'srv-xen1', 'srv-dasu']
cluster = clusters.find { |c| ontologia.include?("**#{c}**") && ontologia.include?(nombre_nodo) }
if cluster
puts " #{Color::GREEN}Cluster:#{Color::RESET} #{cluster}"
# Determinar si necesita ProxyJump
if cluster != 'srv-ns8' && !nombre_nodo.start_with?('srv-')
# Buscar en ontología para contexto de red
ontologia_path = File.join(ADN::PROJECT_ROOT, 'adn', '01_ontologia.md')
if File.exist?(ontologia_path)
ontologia = File.read(ontologia_path, encoding: 'UTF-8')
if ontologia.include?(nombre_nodo)
clusters = ['srv-pmox1', 'srv-pmox2', 'srv-pmox3', 'srv-xen1', 'srv-dasu']
cluster = clusters.find { |c| ontologia.include?("**#{c}**") && ontologia.include?(nombre_nodo) }
if cluster && cluster != 'srv-ns8'
puts " #{Color::YELLOW}🔧 Acceso probable via ProxyJump:#{Color::RESET}"
puts " ssh -J root@#{cluster} rmonla@#{nombre_nodo}"
end
end
end
else
puts "#{Color::YELLOW}⚠ No se pudo extraer metadata del nodo#{Color::RESET}"
end
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
@logger.exito("Contexto obtenido para nodo: #{nombre_nodo}")
end
def generar_comando_ssh(nodo, ip)
def generar_comando_ssh(nodo, ip, usuario = nil)
usuario ||= if nodo.start_with?('srvv-') || nodo.start_with?('pcv-')
'rmonla'
elsif nodo.start_with?('srv-') && nodo != 'srv-ns8'
'root'
else
'rmonla'
end
puts "\n#{Color::CYAN}🔧 Comando SSH sugerido:#{Color::RESET}"
# Determinar usuario basado en el nodo
usuario = if nodo.start_with?('srvv-') || nodo.start_with?('pcv-')
'rmonla'
elsif nodo.start_with?('srv-') && nodo != 'srv-ns8'
'root'
else
'rmonla'
end
puts " ssh #{usuario}@#{ip}"
# Si no es srv-ns8 y no es un hypervisor, sugerir usando nombre
unless nodo == 'srv-ns8' || nodo.start_with?('srv-pmox') || nodo.start_with?('srv-xen')
puts " #{Color::DIM}O usando nombre (si está en ~/.ssh/config):#{Color::RESET}"
puts " ssh #{nodo}"
-61
View File
@@ -1,61 +0,0 @@
# frozen_string_literal: true
# adn/tools/cli/inicio.rb — Subcomando para inicio de jornada laboral (DB-First)
# ==============================================================================
require 'date'
require 'time'
require_relative '../db/core/bitacora_db'
module ADN
class SubcomandoInicio
require_relative '../core/colores'
def initialize(args, logger)
@args = args
@logger = logger
@fecha = Date.today
end
def ejecutar
if @args.empty? || !(@args.include?('jornada'))
puts "#{Color::YELLOW}Uso: ./adn/tools/run inicio de jornada [presencial|remoto]#{Color::RESET}"
return
end
modo = @args.include?('presencial') ? 'P' : 'R'
tipo_str = modo == 'P' ? 'Presencial' : 'Remoto'
hora_ini = Time.now.strftime('%H:%M')
@logger.info("Iniciando jornada #{tipo_str} (Modo DB-First)")
BitacorasDB::BitacoraDB.with_connection do |db|
# 1. Asegurar que existe la bitácora
bitacora = db.find_bitacora_by_fecha(@fecha)
unless bitacora
@logger.info("Creando bitácora en DB para hoy...")
bitacora = db.create_bitacora(fecha: @fecha)
end
# 2. Obtener nodo srv-ns8
nodo = db.find_nodo_by_nombre('srv-ns8')
unless nodo
@logger.warn("Nodo 'srv-ns8' no encontrado, usando nodo general...")
# Podríamos crearlo o buscar uno por defecto
end
# 3. Crear entrada de inicio
db.create_entrada(
inicio: hora_ini,
descripcion: "Inicio de jornada #{tipo_str.downcase}. Sincronización de contexto y revisión de tareas.",
estado: '👁️',
modo: modo,
bitacora_id: bitacora['id'],
nodo_id: nodo ? nodo['id'] : nil
)
@logger.exito("Entrada de inicio creada en Base de Datos.")
end
end
end
end
+261
View File
@@ -0,0 +1,261 @@
# frozen_string_literal: true
require 'optparse'
require 'json'
require 'open3'
require_relative '../core/colores'
require_relative '../core/help_formatter'
class SubcomandoMsp
def initialize(args, logger)
@args = args
@logger = logger
@opciones = {}
@subcommand = nil
end
def ejecutar
@subcommand = @args.shift || 'auditar'
case @subcommand
when 'auditar'
auditar_msp
when 'registrar'
registrar_msp
when 'listar'
listar_msp
when 'help', '--help', '-h'
mostrar_ayuda
else
puts "#{Color::RED}✗ Subcomando desconocido: #{@subcommand}#{Color::RESET}"
puts "Usa: ./adn/tools/run msp --help"
exit 1
end
end
private
def mostrar_ayuda
puts ADN::HelpFormatter.generar_help(
titulo: "Gestión de MSPs - NotebookLM",
descripcion: "Herramientas para auditar, registrar y listar MSPs (Markdown Style Papers)",
uso: ADN::HelpFormatter.format_usage("./adn/tools/run msp", "<subcomando> [opciones]"),
opciones: [
{ names: %w[auditar], desc: "Verificar estado de conexión MCP y NotebookLM" },
{ names: %w[registrar], desc: "Registrar un MSP descargado en la base de conocimiento", default: "--archivo <ruta>" },
{ names: %w[listar], desc: "Listar MSPs disponibles en el sistema" }
],
ejemplos: [
{ cmd: "./adn/tools/run msp auditar", desc: "Verificar estado de MCP y NotebookLM" },
{ cmd: "./adn/tools/run msp listar", desc: "Listar MSPs disponibles" },
{ cmd: "./adn/tools/run msp registrar --archivo /ruta/msp.md", desc: "Registrar un MSP" }
]
)
end
def auditar_msp
parser = OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run msp auditar"
opts.on("-h", "--help", "Mostrar ayuda") do
puts opts
exit
end
end
parser.parse!(@args)
@logger.info("Auditando estado de MSPs y conexión MCP")
puts "#{Color::CYAN}🔍 Auditoría de MSPs - NotebookLM#{Color::RESET}"
puts "#{Color::DIM}#{'=' * 60}#{Color::RESET}"
resultados = {
mcp_config: verificar_mcp_config,
ejecutable: verificar_ejecutable,
autenticacion: verificar_autenticacion,
msp_disponibles: verificar_msps_locales
}
puts ""
puts "#{Color::CYAN}📋 Resultados:#{Color::RESET}"
puts " #{Color::CYAN}MCPs configurados:#{Color::RESET} #{resultados[:mcp_config][:mcps].join(', ')}"
if resultados[:mcp_config][:estado] == 'ok'
puts " #{Color::GREEN}✅ Archivo de configuración#{Color::RESET}"
else
puts " #{Color::RED}❌ Error: #{resultados[:mcp_config][:error]}#{Color::RESET}"
end
puts ""
if resultados[:ejecutable][:existe]
puts " #{Color::CYAN}Ejecutable NotebookLM:#{Color::RESET}"
puts " #{Color::GREEN}✅ Existe#{Color::RESET}"
puts " #{Color::DIM}#{resultados[:ejecutable][:ruta]}#{Color::RESET}"
else
puts " #{Color::CYAN}Ejecutable NotebookLM:#{Color::RESET}"
puts " #{Color::RED}❌ No encontrado#{Color::RESET}"
end
puts ""
puts " #{Color::CYAN}Autenticación:#{Color::RESET}"
if resultados[:autenticacion][:completa]
puts " #{Color::GREEN}✅ Configurada#{Color::RESET}"
else
puts " #{Color::YELLOW}⚠️ Incompleta#{Color::RESET}"
puts " #{Color::DIM}Faltan: #{resultados[:autenticacion][:faltantes].join(', ')}#{Color::RESET}"
end
puts ""
puts " #{Color::CYAN}MSPs locales:#{Color::RESET}"
puts " #{resultados[:msp_disponibles][:total]} archivos encontrados"
puts ""
puts "#{Color::DIM}#{'=' * 60}#{Color::RESET}"
puts "#{Color::CYAN}#{Color::RESET} Los MSPs se generan desde la UI web de NotebookLM"
puts "#{Color::CYAN}#{Color::RESET} MCP NO incluye herramienta de generación MSP"
@logger.exito("Auditoría completada")
end
def verificar_mcp_config
config_path = File.expand_path('~/.gemini/antigravity/mcp_config.json')
unless File.exist?(config_path)
return { estado: 'error', error: 'Archivo no encontrado', mcps: [] }
end
begin
config = JSON.parse(File.read(config_path))
mcps = config['mcpServers']&.keys || []
{ estado: 'ok', mcps: mcps }
rescue JSON::ParserError => e
{ estado: 'error', error: e.message, mcps: [] }
end
end
def verificar_ejecutable
ruta = File.expand_path('~/.local/share/notebooklm-mcp/bin/notebooklm-mcp')
{ existe: File.exist?(ruta), ruta: ruta }
end
def verificar_autenticacion
auth_path = File.expand_path('~/.notebooklm-mcp/auth.json')
unless File.exist?(auth_path)
return { completa: false, faltantes: ['cookies', 'csrf_token', 'session_id'] }
end
begin
auth = JSON.parse(File.read(auth_path))
faltantes = []
faltantes << 'cookies' unless auth['cookies']
faltantes << 'csrf_token' unless auth['csrf_token']
faltantes << 'session_id' unless auth['session_id']
{ completa: faltantes.empty?, faltantes: faltantes }
rescue JSON::ParserError
{ completa: false, faltantes: ['archivo corrupto'] }
end
end
def verificar_msps_locales
patrones = [
File.join(ADN::PROJECT_ROOT, '**/*MSP*.md'),
File.join(ADN::PROJECT_ROOT, 'docs/mcps/*.md')
]
archivos = []
patrones.each do |patron|
archivos |= Dir.glob(patron).select { |f| File.file?(f) }
end
{ total: archivos.count, archivos: archivos }
end
def registrar_msp
parser = OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run msp registrar --archivo <ruta>"
opts.on("-a", "--archivo RUTA", "Ruta al archivo MSP") do |ruta|
@opciones[:archivo] = ruta
end
opts.on("-t", "--titulo TITULO", "Título del MSP") do |titulo|
@opciones[:titulo] = titulo
end
opts.on("-h", "--help", "Mostrar ayuda") do
puts opts
exit
end
end
parser.parse!(@args)
unless @opciones[:archivo]
puts "#{Color::RED}✗ Error: Se requiere --archivo <ruta>#{Color::RESET}"
puts "Usa: ./adn/tools/run msp registrar --help"
exit 1
end
@logger.info("Registrando MSP: #{@opciones[:archivo]}")
unless File.exist?(@opciones[:archivo])
puts "#{Color::RED}✗ Error: Archivo no encontrado: #{@opciones[:archivo]}#{Color::RESET}"
exit 1
end
titulo = @opciones[:titulo] || File.basename(@opciones[:archivo], '.md')
contenido = File.read(@opciones[:archivo])
puts "#{Color::CYAN}📝 Registrando MSP:#{Color::RESET}"
puts " #{Color::CYAN}Título:#{Color::RESET} #{titulo}"
puts " #{Color::CYAN}Archivo:#{Color::RESET} #{@opciones[:archivo]}"
puts " #{Color::CYAN}Tamaño:#{Color::RESET} #{contenido.lines.count} líneas"
puts ""
puts "#{Color::YELLOW}⚠️ Registro en base de conocimiento no implementado#{Color::RESET}"
puts "#{Color::DIM}Para registrar, usar: ./adn/tools/run conocimiento asimilar <archivo>#{Color::RESET}"
@logger.exito("MSP registrado: #{titulo}")
end
def listar_msp
parser = OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run msp listar"
opts.on("-h", "--help", "Mostrar ayuda") do
puts opts
exit
end
end
parser.parse!(@args)
@logger.info("Listando MSPs disponibles")
patrones = [
File.join(ADN::PROJECT_ROOT, '**/*MSP*.md'),
File.join(ADN::PROJECT_ROOT, 'docs/mcps/*.md')
]
archivos = []
patrones.each do |patron|
archivos |= Dir.glob(patron).select { |f| File.file?(f) }
end
puts "#{Color::CYAN}📚 MSPs disponibles#{Color::RESET}"
puts "#{Color::DIM}#{'=' * 60}#{Color::RESET}"
if archivos.empty?
puts "#{Color::YELLOW}⚠️ No se encontraron MSPs#{Color::RESET}"
puts ""
puts "#{Color::CYAN}#{Color::RESET} Los MSPs se generan desde:"
puts " 1. Ir a https://notebooklm.google.com"
puts " 2. Crear/abrir un cuaderno con fuentes"
puts " 3. Click en 'More' → 'Generate MSP'"
puts " 4. Descargar y guardar en el proyecto"
else
archivos.each do |archivo|
puts " #{Color::GREEN}#{Color::RESET} #{archivo}"
end
end
puts "#{Color::DIM}#{'=' * 60}#{Color::RESET}"
puts "Total: #{archivos.count} MSP(s)"
@logger.exito("Listado completado: #{archivos.count} MSPs")
end
end
+6 -32
View File
@@ -9,6 +9,8 @@
require 'optparse'
require_relative '../core/colores'
require_relative '../core/nodos_info'
require_relative '../core/help_formatter'
module ADN
class SubcomandoNodos
@@ -89,7 +91,8 @@ module ADN
nodo_nombre = File.basename(archivo, '.md')
next if nodo_nombre.start_with?('_')
host = detectar_host(archivo)
metadata = ADN::NodosInfo.extraer_metadata(nodo_nombre)
host = metadata ? metadata[:host] : nil
if host
servidores[host] << nodo_nombre
@@ -127,28 +130,6 @@ module ADN
puts ""
end
def detectar_host(archivo)
contenido = File.read(archivo, encoding: 'UTF-8')
host = nil
contenido.split("\n").each do |linea|
# Formato lista o tabla: **Padre/Host**: `srv-dasu`
if linea.match(/\*\*(?:Padre\/Host|Host Anfitrión|Host|Servidor(?: Anfitrión)?|Hipervisor|IP \/ Ubicación)\*\*\s*[:\|]?\s*`?([a-zA-Z0-9\-]+)`?/) ||
linea.match(/\*\*(?:Padre\/Host|Host Anfitrión|Host|Servidor(?: Anfitrión)?|Hipervisor|IP \/ Ubicación)\*\*\s*[:\|]?\s*([^`\s|]+)/)
candidato = ($1 || $2).strip.gsub(/`/, '')
return candidato if candidato.start_with?('srv-')
end
# Texto: "dependiente del host físico `srv-dasu`"
if linea.match(/host(?: físico)?\s+`?([a-zA-Z0-9\-]+)`?/)
candidato = $1.strip
return candidato if candidato.start_with?('srv-')
end
end
host
end
def es_nodo_fisico?(nombre)
nombre.start_with?('srv-') || nombre.start_with?('pc-') ||
nombre == 'dtic-bitacoras' || nombre == 'cam-hikvision-24'
@@ -180,15 +161,8 @@ module ADN
end
nodos.each do |nodo|
archivo = File.join(ADN::NODOS_DIR, "#{nodo}.md")
contenido = File.read(archivo, encoding: 'UTF-8')
# Extraer rol si existe
rol = '-'
if contenido.match(/\*\*Rol(?:\s*Principal)?\*\*\s*[:\|]\s*([^\|\n]+)/)
rol = $1.strip[0..40]
end
metadata = ADN::NodosInfo.extraer_metadata(nodo)
rol = metadata && metadata[:host] ? 'VM' : (metadata && metadata[:so] ? metadata[:so].to_s : '-')
puts " 💻 #{nodo.ljust(22)} #{Color::DIM}#{rol}#{Color::RESET}"
end
+4
View File
@@ -1,5 +1,9 @@
# frozen_string_literal: true
# ⚠️ [DEPRECATED] Este módulo está en desuso.
# Filosofia actual: DB-First. Ver ./adn/tools/run db
# Más información: adn/README.md
require 'optparse'
require_relative '../core/colores'
require_relative '../core/validador'
+43 -4
View File
@@ -2,9 +2,12 @@
# adn/tools/core/constants.rb — Definición centralizada de rutas y constantes del ADN
# ==============================================================================
# Este módulo centraliza todas las constantes reutilizables del sistema ADN.
# Following: Menos es Más (una fuente de verdad)
module ADN
# El script 'run' está en adn/tools/run, por lo que la raíz es 2 niveles arriba
# === RUTAS DEL PROYECTO ===
PROJECT_ROOT = File.expand_path('../../..', __dir__)
NODOS_DIR = File.join(PROJECT_ROOT, 'nodos')
@@ -12,10 +15,46 @@ module ADN
BITACORAS_DIR = File.join(PROJECT_ROOT, 'docs', 'bitacoras')
LOGS_DIR = File.join(BITACORAS_DIR, '_hist')
# Directorios de herramientas
TOOLS_DIR = File.join(PROJECT_ROOT, 'adn', 'tools')
SEGURIDAD_DIR = File.join(TOOLS_DIR, 'seguridad')
# Bóveda
CANDADOS_PATH = File.join(SEGURIDAD_DIR, 'ns8-candados.rb')
# === ESTADOS DE BITÁCORA ===
ESTADOS = {
pendiente: '⏳',
completado: '✅',
fallido: '❌',
advertencia: '⚠️',
pausado: '⏸️',
en_proceso: '📍'
}.freeze
# === MODOS DE TRABAJO ===
MODOS = {
presencial: 'P',
remoto: 'R'
}.freeze
# === CONFIGURACIÓN DE BASE DE DATOS ===
DB_CONFIG = {
host: 'localhost',
puerto: 5433,
nombre: 'dtic_bitacoras',
schema: 'bitacoras'
}.freeze
# === EXCEPCIONES PERSONALIZADAS ===
class Error < StandardError; end
class ConfigError < Error; end
class DBError < Error; end
class ValidationError < Error; end
class NetworkError < Error; end
# === VERSIÓN ===
VERSION = '1.0.0'.freeze
end
+36
View File
@@ -0,0 +1,36 @@
# frozen_string_literal: true
# adn/tools/core/error_handler.rb — Helper para mensajes de error estandarizados
# ==============================================================================
# módulo reutilizable para formateo consistente de errores en CLI
module ADN
module ErrorHandler
module_function
def error(msg, code: 1)
puts "#{Color::RED}#{msg}#{Color::RESET}"
exit(code)
end
def warning(msg)
puts "#{Color::YELLOW}#{msg}#{Color::RESET}"
end
def info(msg)
puts "#{Color::CYAN} #{msg}#{Color::RESET}"
end
def success(msg)
puts "#{Color::GREEN}#{msg}#{Color::RESET}"
end
def debug(msg)
puts "#{Color::DIM}🔍 #{msg}#{Color::RESET}" if ENV['ADN_DEBUG']
end
def die(msg, code: 1)
error(msg, code: code)
end
end
end
+64
View File
@@ -0,0 +1,64 @@
# frozen_string_literal: true
# adn/tools/core/help_formatter.rb - Formateador de ayuda estandarizado
# ======================================================================
# Helper reutilizable para formateo consistente de --help en CLI
require_relative 'colores'
module ADN
module HelpFormatter
module_function
def format_header(titulo, descripcion = nil)
lines = []
lines << "#{Color::BOLD}#{titulo}#{Color::RESET}"
lines << "#{Color::DIM}#{'=' * titulo.length}#{Color::RESET}" if descripcion
lines << ""
lines << "#{Color::CYAN}#{descripcion}#{Color::RESET}" if descripcion
lines << ""
lines.join("\n")
end
def format_usage(comando, formato)
"Uso: #{Color::YELLOW}#{comando}#{Color::RESET} #{formato}"
end
def format_option(opciones, descripcion, default = nil)
opt_str = opciones.map { |o| o.length > 1 ? "--#{o}" : "-#{o}" }.join(', ')
default_str = default ? " (default: #{default})" : ""
" #{Color::GREEN}#{opt_str.ljust(20)}#{Color::RESET} #{descripcion}#{default_str}"
end
def format_example(comando, descripcion)
"#{Color::DIM} $ #{comando}#{Color::RESET}\n #{Color::CYAN}#{descripcion}#{Color::RESET}"
end
def format_section(titulo)
"\n#{Color::BOLD}#{titulo}#{Color::RESET}\n"
end
def generar_help(titulo:, descripcion:, uso:, opciones:, ejemplos: [])
lines = []
lines << format_header(titulo, descripcion)
lines << uso
lines << ""
if opciones.any?
lines << format_section("Opciones:")
opciones.each do |opt|
lines << format_option(opt[:names], opt[:desc], opt[:default])
end
end
if ejemplos.any?
lines << format_section("Ejemplos:")
ejemplos.each do |ej|
lines << format_example(ej[:cmd], ej[:desc])
end
end
lines.join("\n")
end
end
end
+50 -13
View File
@@ -4,7 +4,7 @@
# ====================================================================
# Este módulo proporciona logging estructurado en JSON para todas las
# herramientas del sistema ADN. Los logs se almacenan en `logs/adn/`
# con rotación diaria automática.
# con rotación diaria automática y por tamaño.
require 'json'
require 'fileutils'
@@ -13,32 +13,30 @@ require 'time'
module ADN
class Logger
# Niveles de log soportados
NIVELES = %w[DEBUG INFO EXITO ADVERTENCIA ERROR].freeze
# Configuración de rotación
MAX_TAMANO_MB = 10
MAX_ARCHIVOS = 7
require_relative 'colores'
# Inicializa el logger
#
# @param opts [Hash] Opciones de configuración
# @option opts [String] :directorio Directorio para logs (default: logs/adn/)
# @option opts [Boolean] :terminal Mostrar logs en terminal (default: true)
# @option opts [Boolean] :colores Usar colores en terminal (default: true)
# @option opts [String] :archivo Nombre base del archivo de log (default: fecha actual)
def initialize(opts = {})
@directorio = opts[:directorio] || File.join(File.expand_path('../../..', __dir__), 'logs', 'adn')
@mostrar_terminal = opts.fetch(:terminal, true)
@usar_colores = opts.fetch(:colores, true)
@archivo_base = opts[:archivo] || Date.today.iso8601
# Nivel mínimo de log (configurable)
@nivel_minimo = opts[:nivel] || 'DEBUG'
# Crear directorio si no existe
FileUtils.mkdir_p(@directorio) unless Dir.exist?(@directorio)
# Ruta completa del archivo de log
@ruta_log = File.join(@directorio, "#{@archivo_base}.log")
# Buffer para logs (útil para pruebas o modo batch)
@buffer = []
# Verificar rotación al iniciar
verificar_rotacion
end
# Registra un mensaje de nivel DEBUG
@@ -209,6 +207,45 @@ module ADN
'adn.desconocido'
end
# Verifica y ejecuta rotación de logs si es necesario
def verificar_rotacion
return unless File.exist?(@ruta_log)
tamano_mb = File.size(@ruta_log) / (1024.0 * 1024.0)
if tamano_mb > MAX_TAMANO_MB
rotar_por_tamano
end
end
# Rota el log por tamaño (archivo muy grande)
def rotar_por_tamano
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
archivo_rotado = "#{@ruta_log}.#{timestamp}"
FileUtils.mv(@ruta_log, archivo_rotado)
limpiar_logs_antiguos
puts "#{Color::YELLOW}⚠ Log rotado por tamaño: #{archivo_rotado}#{Color::RESET}"
end
# Limpia logs antiguos (más de MAX_ARCHIVOS)
def limpiar_logs_antiguos
patron = File.join(@directorio, '*.log.*')
archivos = Dir.glob(patron).sort_by { |f| File.mtime(f) }
archivos[0...-MAX_ARCHIVOS].each do |archivo|
File.delete(archivo) if File.exist?(archivo)
end
end
# Verifica si el nivel está habilitado
def nivel_habilitado?(nivel)
idx_actual = NIVELES.index(@nivel_minimo) || 0
idx_nivel = NIVELES.index(nivel) || 0
idx_nivel >= idx_actual
end
end
# Logger global preconfigurado para uso rápido
+4
View File
@@ -1,5 +1,9 @@
# frozen_string_literal: true
# ⚠️ [DEPRECATED] Este módulo está en desuso.
# Filosofia actual: DB-First. La base de datos es la fuente de verdad.
# Uso: API REST en puerto 3002 o herramienta ./adn/tools/run db
require 'date'
module BitacorasDB
+16 -5
View File
@@ -22,9 +22,10 @@ ENV['TZ'] = 'America/Argentina/Buenos_Aires'
# tailscale <acción> [opciones] Gestión de identidades Tailscale
# conocimiento asimilar <archivo> Asimilar documentación al ADN
# evolucion <propuesta.md> Gestionar propuesta de evolución del ADN
# msp <subcomando> Gestión de MSPs (auditar, registrar, listar)
# ayuda [subcomando] Mostrar ayuda detallada
#
# Documentación completa: docs/plan/260307-1400_Plan_Mejoras_ADN.md
# Documentación completa: docs/proy/p2604_mejoras_ADN/P2604_mejoras_ADN.md
require 'optparse'
require 'json'
@@ -43,7 +44,6 @@ end
require_relative 'core/logger'
require_relative 'core/eventos'
require_relative 'core/triggers'
require_relative 'core/validador'
require_relative 'cli/db'
require_relative 'cli/plan'
require_relative 'cli/backup'
@@ -67,6 +67,7 @@ require_relative 'cli/generar'
require_relative 'cli/salud'
require_relative 'cli/ayuda'
require_relative 'cli/nodos'
require_relative 'cli/msp'
# ─── CLI Principal ──────────────────────────────────────────────────────
class ADNCLI
@@ -126,10 +127,10 @@ class ADNCLI
@logger.info("Subcomando 'evolucion' será implementado en Fase 2")
puts "#{Color::YELLOW}⚠ El subcomando 'evolucion' será implementado en Fase 2#{Color::RESET}"
puts " Ver plan: docs/plan/260307-1400_Plan_Mejoras_ADN.md"
when 'triggers'
ADN::SubcomandoTriggers.new(args, @logger).ejecutar
when 'plan'
ADN::SubcomandoPlan.new(args, @logger).ejecutar
when 'msp'
SubcomandoMsp.new(args, @logger).ejecutar
else
@logger.error("Subcomando desconocido: #{subcomando}")
puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}"
@@ -141,10 +142,20 @@ class ADNCLI
puts "\n#{Color::YELLOW}⚠ Interrumpido por el usuario#{Color::RESET}"
@logger.info("Ejecución interrumpida por usuario")
exit 130
rescue OptionParser::InvalidOption => e
@logger.error("Opción inválida: #{e.message}")
puts "#{Color::RED}✗ Error: opción inválida #{e.message}#{Color::RESET}"
puts "Usa './adn/tools/run ayuda' para ver las opciones disponibles"
exit 1
rescue ADN::Error => e
@logger.error("Error de aplicación: #{e.message}")
puts "#{Color::RED}✗ #{e.message}#{Color::RESET}"
exit 1
rescue => e
@logger.error("Error inesperado: #{e.message}", { backtrace: e.backtrace })
puts "#{Color::RED}✗ Error inesperado: #{e.message}#{Color::RESET}"
puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG']
puts "#{Color::DIM}#{e.backtrace.first(3).join("\n")}#{Color::RESET}" if ENV['ADN_DEBUG']
puts "\n#{Color::YELLOW}💡 Ejecuta con ADN_DEBUG=1 para ver el trace completo#{Color::RESET}"
exit 1
end
private
+33
View File
@@ -0,0 +1,33 @@
# encoding: utf-8
# colores_spec.rb - Tests para módulo de colores
require_relative '../spec_helper'
$LOAD_PATH.unshift File.expand_path('../../core', __dir__)
require 'colores'
describe 'ADN::Color' do
describe 'Constantes de color' do
it 'define RESET' do
expect(Color::RESET).to eq("\e[0m")
end
it 'define BOLD' do
expect(Color::BOLD).to eq("\e[1m")
end
it 'define GREEN' do
expect(Color::GREEN).to eq("\e[32m")
end
it 'define CYAN' do
expect(Color::CYAN).to eq("\e[36m")
end
it 'define YELLOW' do
expect(Color::YELLOW).to eq("\e[33m")
end
it 'define RED' do
expect(Color::RED).to eq("\e[31m")
end
end
end
+40
View File
@@ -0,0 +1,40 @@
# encoding: utf-8
# validador_spec.rb - Tests para módulo validador core
require_relative '../spec_helper'
describe 'ADN::Validador Patterns' do
describe 'Validación de tiempo HH:MM' do
it 'valida formato correcto' do
expect('10:30').to match(/^\d{2}:\d{2}$/)
end
it 'rechaza formato con hs' do
expect('10:30 hs').to match(/\d{1,2}:\d{2}\s*hs/)
end
it 'rechaza hora fuera de rango' do
expect('25:00').not_to match(/^([01]?\d|2[0-3]):([0-5]?\d)$/)
end
end
describe 'Validación de iconografía' do
it 'reconoce iconos válidos' do
iconos_validos = %w[✅ ⏳ ⚠️ ❌ 📍 ➡️]
iconos_validos.each do |icono|
expect(['✅', '⏳', '⚠️', '❌', '📍', '➡️']).to include(icono)
end
end
end
describe 'Validación de bitácora' do
it 'valida nombre de archivo YYYY-MM-DD.md' do
expect('2026-03-14.md').to match(/^\d{4}-\d{2}-\d{2}\.md$/)
end
end
describe 'Validación de proyecto' do
it 'valida código de proyecto PXXXX' do
expect('P2601_dasuten.md').to match(/^P\d+_[a-z0-9_]+\.md$/)
end
end
end
+41
View File
@@ -0,0 +1,41 @@
# encoding: utf-8
# evento_spec.rb - Tests atómicos para módulo evento
require_relative '../spec_helper'
describe 'ADN::SubcomandoDBEvento' do
describe '.validar_hora' do
it 'valida formato HH:MM válido' do
horas_validas = ['08:30', '00:00', '23:59', '12:00']
horas_validas.each do |hora|
expect(validar_formato_hora(hora)).to be true
end
end
it 'rechaza formato inválido' do
horas_invalidas = ['8:30', '25:00', '12:60', 'abc', '']
horas_invalidas.each do |hora|
expect(validar_formato_hora(hora)).to be false
end
end
end
describe '.validar_estado' do
it 'acepta estados válidos' do
expect(validar_estado('⏳')).to be true
expect(validar_estado('✅')).to be true
expect(validar_estado('❌')).to be true
end
end
end
def validar_formato_hora(hora)
return false if hora.nil? || hora.empty?
match = hora.match(/^(\d{2}):(\d{2})$/)
return false unless match
hora_i, min = match[1].to_i, match[2].to_i
hora_i >= 0 && hora_i <= 23 && min >= 0 && min <= 59
end
def validar_estado(estado)
%w[⏳ ✅ ❌ ⚠️ ⏸️].include?(estado)
end
+11
View File
@@ -0,0 +1,11 @@
# spec_helper.rb - Configuración básica de tests
require 'bundler/setup'
require 'rspec'
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
RSpec.configure do |config|
config.expect_with :rspec do |c|
c.syntax = [:should, :expect]
end
end