Refactor(ADN): Retructuración del ecosistema ADN para unificar herramientas independientes. Fusión de w-zombi como adn/sondas y ns8-candados como adn/seguridad. Renombrado de comando/ a cli/ para consistencia y plan/parser.rb a parsers/plan.rb
This commit is contained in:
@@ -0,0 +1,684 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# tools/adn/parsers/plan.rb — Parser para seguimiento de planes ADN
|
||||
# ============================================================================
|
||||
# Este módulo proporciona funcionalidad para parsear, actualizar y generar
|
||||
# reportes del plan de trabajo ADN BITACORAs.
|
||||
#
|
||||
# Características:
|
||||
# • Parseo de archivos Markdown con estructura de planes
|
||||
# • Detección automática de fases, semanas y tareas
|
||||
# • Actualización de estado de tareas ([ ] → [x])
|
||||
# • Generación de reportes de progreso
|
||||
# • Persistencia de cambios en el archivo original
|
||||
#
|
||||
# Uso:
|
||||
# parser = PlanParser.new('docs/plan/260308-1200_Plan_Integral_ADN_BITACORAs.md')
|
||||
# parser.parse_file
|
||||
# parser.update_task_status('1.1', true)
|
||||
# parser.save_changes
|
||||
# puts parser.progress_report
|
||||
|
||||
require 'yaml'
|
||||
require 'json'
|
||||
require 'fileutils'
|
||||
require 'time'
|
||||
|
||||
module ADN
|
||||
module Plan
|
||||
class PlanParser
|
||||
# Patrones regex para parsear el archivo de plan
|
||||
PHASE_PATTERN = /^###\s*([✅🟡🟠🔴🟣⚫️⬛️◼️▪️]+)?\s*\*\*FASE\s+(\d+):\s*(.+?)\*\*\s*(?:\((.+?)\))?$/i
|
||||
WEEK_PATTERN = /^####\s*Semana\s+(\d+):\s*(.+)$/i
|
||||
TASK_PATTERN = /^(\s*)[-*]\s*\[([ x]?)\]\s*\*\*([A-Za-z]*\d+(?:\.\d+)?):\*\*\s*(.+?)(?:\s*\((.+?)\))?$/
|
||||
SECTION_PATTERN = /^\#{1,6}\s*(.+)$/
|
||||
KPI_TABLE_PATTERN = /^\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|$/
|
||||
|
||||
# Estados posibles de las tareas
|
||||
TASK_STATES = {
|
||||
' ' => :pending,
|
||||
'x' => :completed,
|
||||
'X' => :completed
|
||||
}.freeze
|
||||
|
||||
# Fases del plan con sus emojis representativos
|
||||
PHASE_EMOJIS = {
|
||||
0 => '✅', # Preparación (completado)
|
||||
1 => '🟡', # Núcleo de Conexión BD
|
||||
2 => '🟠', # Herramientas CLI Avanzadas
|
||||
3 => '🔴', # Sistema Integrado
|
||||
4 => '🟣', # Madurez y Autonomía
|
||||
5 => '⚫️' # Mantenimiento
|
||||
}.freeze
|
||||
|
||||
attr_reader :file_path, :phases, :tasks, :kpis, :metadata, :logger
|
||||
|
||||
# Inicializa el parser con la ruta del archivo
|
||||
#
|
||||
# @param file_path [String] Ruta al archivo markdown del plan
|
||||
# @param logger [Logger] Instancia de logger (opcional)
|
||||
def initialize(file_path, logger = nil)
|
||||
@file_path = File.expand_path(file_path)
|
||||
@logger = logger || default_logger
|
||||
@phases = []
|
||||
@tasks = []
|
||||
@kpis = []
|
||||
@metadata = {}
|
||||
@lines = []
|
||||
@original_content = nil
|
||||
@changes_made = false
|
||||
end
|
||||
|
||||
# Parsea el archivo completo y extrae estructura
|
||||
#
|
||||
# @return [PlanParser] self para encadenamiento
|
||||
def parse_file
|
||||
unless File.exist?(@file_path)
|
||||
@logger.error("Archivo de plan no encontrado: #{@file_path}")
|
||||
raise "Archivo no encontrado: #{@file_path}"
|
||||
end
|
||||
|
||||
@logger.info("Parseando archivo de plan: #{@file_path}")
|
||||
@original_content = File.read(@file_path, encoding: 'UTF-8')
|
||||
@lines = @original_content.lines.map(&:chomp)
|
||||
|
||||
debug_log("Archivo leído, #{@lines.size} líneas")
|
||||
debug_log("Primeras 5 líneas: #{@lines.first(5).inspect}")
|
||||
|
||||
extract_metadata
|
||||
parse_structure
|
||||
|
||||
@logger.info("Parseo completado: #{@phases.size} fases, #{@tasks.size} tareas encontradas")
|
||||
|
||||
if @phases.empty? || @tasks.empty?
|
||||
debug_log("ADVERTENCIA: No se encontraron fases o tareas. Posibles causas:")
|
||||
debug_log(" - Patrones regex no coinciden con el formato del archivo")
|
||||
debug_log(" - El archivo no sigue la estructura esperada")
|
||||
debug_log(" - Los encabezados de fase no coinciden con PHASE_PATTERN")
|
||||
debug_log(" PHASE_PATTERN: #{PHASE_PATTERN.inspect}")
|
||||
debug_log(" TASK_PATTERN: #{TASK_PATTERN.inspect}")
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Actualiza el estado de una tarea específica
|
||||
#
|
||||
# @param task_id [String] ID de la tarea (ej: "1.1", "A1")
|
||||
# @param completed [Boolean] true para marcar como completada
|
||||
# @param notes [String] Notas adicionales (opcional)
|
||||
# @return [Boolean] true si la tarea fue encontrada y actualizada
|
||||
def update_task_status(task_id, completed, notes = nil)
|
||||
task = find_task_by_id(task_id)
|
||||
|
||||
unless task
|
||||
@logger.warn("Tarea no encontrada: #{task_id}")
|
||||
return false
|
||||
end
|
||||
|
||||
new_state = completed ? 'x' : ' '
|
||||
old_state = task[:state]
|
||||
|
||||
if new_state != old_state
|
||||
# Actualizar línea en el archivo
|
||||
line_index = task[:line_number] - 1
|
||||
original_line = @lines[line_index]
|
||||
|
||||
if original_line && original_line.match(TASK_PATTERN)
|
||||
updated_line = original_line.sub(/\[#{old_state}\]/, "[#{new_state}]")
|
||||
|
||||
# Agregar notas si se proporcionan
|
||||
if notes && !notes.empty?
|
||||
# Si ya hay notas, actualizarlas, sino agregar
|
||||
if original_line.match(/\((.+?)\)$/)
|
||||
updated_line = updated_line.sub(/\((.+?)\)$/, "(#{notes})")
|
||||
else
|
||||
updated_line = "#{updated_line} (#{notes})"
|
||||
end
|
||||
end
|
||||
|
||||
@lines[line_index] = updated_line
|
||||
task[:state] = new_state
|
||||
task[:completed] = completed
|
||||
task[:notes] = notes if notes
|
||||
task[:updated_at] = Time.now.iso8601
|
||||
|
||||
@changes_made = true
|
||||
@logger.info("Tarea #{task_id} actualizada: #{old_state} → #{new_state}")
|
||||
|
||||
# Actualizar emoji de fase si corresponde
|
||||
update_phase_emoji_if_needed(task[:phase_number])
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# Marca múltiples tareas como completadas
|
||||
#
|
||||
# @param task_ids [Array<String>] IDs de las tareas
|
||||
# @param notes [String] Notas para todas las tareas (opcional)
|
||||
# @return [Hash] Resultado con tareas actualizadas y fallidas
|
||||
def complete_tasks(task_ids, notes = nil)
|
||||
results = { updated: [], failed: [] }
|
||||
|
||||
task_ids.each do |task_id|
|
||||
if update_task_status(task_id, true, notes)
|
||||
results[:updated] << task_id
|
||||
else
|
||||
results[:failed] << task_id
|
||||
end
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
# Guarda los cambios en el archivo original
|
||||
#
|
||||
# @param backup [Boolean] Crear backup antes de guardar
|
||||
# @return [Boolean] true si se guardaron cambios
|
||||
def save_changes(backup: true)
|
||||
return false unless @changes_made
|
||||
|
||||
if backup
|
||||
create_backup
|
||||
end
|
||||
|
||||
File.write(@file_path, @lines.join("\n") + "\n", encoding: 'UTF-8')
|
||||
@changes_made = false
|
||||
|
||||
@logger.info("Cambios guardados en #{@file_path}")
|
||||
true
|
||||
end
|
||||
|
||||
# Genera un reporte de progreso
|
||||
#
|
||||
# @param format [Symbol] Formato del reporte (:text, :json, :markdown)
|
||||
# @return [String] Reporte formateado
|
||||
def progress_report(format: :text)
|
||||
case format
|
||||
when :json
|
||||
generate_json_report
|
||||
when :markdown
|
||||
generate_markdown_report
|
||||
else
|
||||
generate_text_report
|
||||
end
|
||||
end
|
||||
|
||||
# Encuentra una tarea por su ID
|
||||
#
|
||||
# @param task_id [String] ID de la tarea
|
||||
# @return [Hash, nil] Información de la tarea o nil
|
||||
def find_task_by_id(task_id)
|
||||
@tasks.find { |t| t[:id] == task_id }
|
||||
end
|
||||
|
||||
# Encuentra tareas por fase
|
||||
#
|
||||
# @param phase_number [Integer] Número de fase
|
||||
# @return [Array<Hash>] Tareas de la fase
|
||||
def tasks_by_phase(phase_number)
|
||||
@tasks.select { |t| t[:phase_number] == phase_number }
|
||||
end
|
||||
|
||||
# Encuentra tareas por semana
|
||||
#
|
||||
# @param phase_number [Integer] Número de fase
|
||||
# @param week_number [Integer] Número de semana
|
||||
# @return [Array<Hash>] Tareas de la semana
|
||||
def tasks_by_week(phase_number, week_number)
|
||||
@tasks.select { |t| t[:phase_number] == phase_number && t[:week_number] == week_number }
|
||||
end
|
||||
|
||||
# Calcula estadísticas de progreso
|
||||
#
|
||||
# @return [Hash] Estadísticas detalladas
|
||||
def progress_stats
|
||||
total_tasks = @tasks.size
|
||||
completed_tasks = @tasks.count { |t| t[:completed] }
|
||||
|
||||
phase_stats = @phases.map do |phase|
|
||||
phase_tasks = tasks_by_phase(phase[:number])
|
||||
phase_completed = phase_tasks.count { |t| t[:completed] }
|
||||
|
||||
{
|
||||
number: phase[:number],
|
||||
name: phase[:name],
|
||||
total: phase_tasks.size,
|
||||
completed: phase_completed,
|
||||
percentage: phase_tasks.empty? ? 0 : (phase_completed * 100 / phase_tasks.size).round(1)
|
||||
}
|
||||
end
|
||||
|
||||
week_stats = []
|
||||
@phases.each do |phase|
|
||||
(1..4).each do |week|
|
||||
week_tasks = tasks_by_week(phase[:number], week)
|
||||
next if week_tasks.empty?
|
||||
|
||||
week_completed = week_tasks.count { |t| t[:completed] }
|
||||
week_stats << {
|
||||
phase: phase[:number],
|
||||
week: week,
|
||||
total: week_tasks.size,
|
||||
completed: week_completed,
|
||||
percentage: (week_completed * 100 / week_tasks.size).round(1)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
total_tasks: total_tasks,
|
||||
completed_tasks: completed_tasks,
|
||||
pending_tasks: total_tasks - completed_tasks,
|
||||
overall_percentage: total_tasks.zero? ? 0 : (completed_tasks * 100 / total_tasks).round(1),
|
||||
phases: phase_stats,
|
||||
weeks: week_stats,
|
||||
last_updated: Time.now.iso8601,
|
||||
file_path: @file_path
|
||||
}
|
||||
end
|
||||
|
||||
# Exporta el progreso a un archivo JSON
|
||||
#
|
||||
# @param output_path [String] Ruta de salida (opcional)
|
||||
# @return [String] JSON generado
|
||||
def export_progress(output_path = nil)
|
||||
export_data = {
|
||||
metadata: @metadata.merge(
|
||||
exported_at: Time.now.iso8601,
|
||||
version: '1.0'
|
||||
),
|
||||
progress: progress_stats,
|
||||
tasks: @tasks.map { |t| t.reject { |k| k == :line_number } },
|
||||
phases: @phases
|
||||
}
|
||||
|
||||
json_data = JSON.pretty_generate(export_data)
|
||||
|
||||
if output_path
|
||||
File.write(output_path, json_data, encoding: 'UTF-8')
|
||||
@logger.info("Progreso exportado a #{output_path}")
|
||||
end
|
||||
|
||||
json_data
|
||||
end
|
||||
|
||||
# Valida la integridad del plan
|
||||
#
|
||||
# @return [Hash] Resultados de validación
|
||||
def validate_plan
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Verificar que todas las tareas tengan IDs únicos
|
||||
task_ids = @tasks.map { |t| t[:id] }
|
||||
duplicates = task_ids.group_by { |id| id }.select { |_, ids| ids.size > 1 }.keys
|
||||
|
||||
duplicates.each do |duplicate_id|
|
||||
errors << "ID duplicado: #{duplicate_id}"
|
||||
end
|
||||
|
||||
# Verificar referencias a fases inexistentes
|
||||
@tasks.each do |task|
|
||||
unless @phases.any? { |p| p[:number] == task[:phase_number] }
|
||||
warnings << "Tarea #{task[:id]} referencia fase #{task[:phase_number]} que no existe"
|
||||
end
|
||||
end
|
||||
|
||||
# Verificar tareas sin descripción
|
||||
@tasks.each do |task|
|
||||
if task[:description].to_s.strip.empty?
|
||||
warnings << "Tarea #{task[:id]} sin descripción"
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
valid: errors.empty?,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
tasks_count: @tasks.size,
|
||||
phases_count: @phases.size
|
||||
}
|
||||
end
|
||||
|
||||
# Genera un resumen ejecutivo para dashboard
|
||||
#
|
||||
# @return [Hash] Resumen ejecutivo
|
||||
def executive_summary
|
||||
stats = progress_stats
|
||||
|
||||
next_phase = @phases.find { |p| stats[:phases].find { |ps| ps[:number] == p[:number] }[:percentage] < 100 }
|
||||
next_tasks = @tasks.select { |t| !t[:completed] }.sort_by { |t| [t[:phase_number], t[:week_number]] }.first(5)
|
||||
|
||||
{
|
||||
overall_progress: stats[:overall_percentage],
|
||||
completed_tasks: stats[:completed_tasks],
|
||||
pending_tasks: stats[:pending_tasks],
|
||||
next_phase: next_phase ? {
|
||||
number: next_phase[:number],
|
||||
name: next_phase[:name],
|
||||
progress: stats[:phases].find { |p| p[:number] == next_phase[:number] }[:percentage]
|
||||
} : nil,
|
||||
next_tasks: next_tasks.map { |t| { id: t[:id], description: t[:description] } },
|
||||
estimated_completion: estimate_completion_date,
|
||||
last_updated: @tasks.map { |t| t[:updated_at] }.compact.max || @metadata[:created_at]
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Extrae metadatos del archivo
|
||||
def extract_metadata
|
||||
@metadata = {
|
||||
file_path: @file_path,
|
||||
file_size: File.size(@file_path),
|
||||
created_at: File.ctime(@file_path).iso8601,
|
||||
modified_at: File.mtime(@file_path).iso8601
|
||||
}
|
||||
|
||||
# Buscar metadatos en las primeras líneas
|
||||
@lines.each_with_index do |line, index|
|
||||
break if index > 20 # Solo primeras 20 líneas
|
||||
|
||||
case line
|
||||
when /^#\s*(.+)$/
|
||||
@metadata[:title] = $1.strip
|
||||
when /^\*\*Fecha:\*\*\s*(.+)$/
|
||||
@metadata[:date] = $1.strip
|
||||
when /^\*\*Autor:\*\*\s*(.+)$/
|
||||
@metadata[:author] = $1.strip
|
||||
when /^\*\*Versión:\*\*\s*(.+)$/
|
||||
@metadata[:version] = $1.strip
|
||||
when /^\*\*Estado:\*\*\s*(.+)$/
|
||||
@metadata[:status] = $1.strip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Parsea la estructura completa del plan
|
||||
def parse_structure
|
||||
current_phase = nil
|
||||
current_week = nil
|
||||
current_section = nil
|
||||
|
||||
@lines.each_with_index do |line, line_number|
|
||||
debug_log("Línea #{line_number + 1}: #{line.inspect}")
|
||||
|
||||
# Buscar fases
|
||||
if match = line.match(PHASE_PATTERN)
|
||||
debug_log(" ✓ Coincide con PHASE_PATTERN: #{match.inspect}")
|
||||
current_phase = parse_phase(match, line_number + 1)
|
||||
@phases << current_phase
|
||||
current_week = nil
|
||||
current_section = "FASE #{current_phase[:number]}"
|
||||
|
||||
# Buscar semanas
|
||||
elsif match = line.match(WEEK_PATTERN)
|
||||
debug_log(" ✓ Coincide con WEEK_PATTERN: #{match.inspect}")
|
||||
current_week = match[1].to_i
|
||||
current_section = "Semana #{current_week}"
|
||||
|
||||
# Buscar tareas
|
||||
elsif match = line.match(TASK_PATTERN)
|
||||
debug_log(" ✓ Coincide con TASK_PATTERN: #{match.inspect}")
|
||||
if current_phase
|
||||
task = parse_task(match, line_number + 1, current_phase[:number], current_week)
|
||||
@tasks << task
|
||||
else
|
||||
debug_log(" ✗ Tarea ignorada: no hay fase actual")
|
||||
end
|
||||
|
||||
# Buscar KPIs en tablas
|
||||
elsif line.match?(KPI_TABLE_PATTERN) && !line.match?(/^[-|]/)
|
||||
# Solo procesar líneas de datos de tabla (no encabezados)
|
||||
if line.match?(/^\|/) && !line.match?(/^\|[-:|]/)
|
||||
parse_kpi_table_line(line, line_number + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
debug_log("Estructura parseada: #{@phases.size} fases, #{@tasks.size} tareas")
|
||||
end
|
||||
|
||||
# Parsea información de una fase
|
||||
def parse_phase(match, line_number)
|
||||
emoji = match[1] || PHASE_EMOJIS[match[2].to_i] || '⬜️'
|
||||
number = match[2].to_i
|
||||
name = match[3].strip
|
||||
|
||||
{
|
||||
number: number,
|
||||
name: name,
|
||||
emoji: emoji,
|
||||
line_number: line_number,
|
||||
original_line: match[0]
|
||||
}
|
||||
end
|
||||
|
||||
# Parsea información de una tarea
|
||||
def parse_task(match, line_number, phase_number, week_number)
|
||||
indent = match[1].to_s.length
|
||||
state_char = match[2].to_s
|
||||
task_id = match[3].strip
|
||||
description = match[4].strip
|
||||
notes = match[5]
|
||||
|
||||
{
|
||||
id: task_id,
|
||||
description: description,
|
||||
notes: notes,
|
||||
state: state_char,
|
||||
completed: TASK_STATES[state_char] == :completed,
|
||||
phase_number: phase_number,
|
||||
week_number: week_number,
|
||||
indent_level: indent / 2, # Asume indentación de 2 espacios
|
||||
line_number: line_number,
|
||||
original_line: match[0],
|
||||
updated_at: nil
|
||||
}
|
||||
end
|
||||
|
||||
# Parsea línea de tabla KPI
|
||||
def parse_kpi_table_line(line, line_number)
|
||||
return unless line.match?(KPI_TABLE_PATTERN)
|
||||
|
||||
parts = line.split('|').map(&:strip).reject(&:empty?)
|
||||
return unless parts.size >= 5 # Necesita al menos 5 columnas para ser KPI
|
||||
|
||||
kpi = {
|
||||
name: parts[0],
|
||||
baseline: parts[1],
|
||||
target_week_4: parts[2],
|
||||
target_week_8: parts[3],
|
||||
measurement: parts[4],
|
||||
line_number: line_number
|
||||
}
|
||||
|
||||
@kpis << kpi
|
||||
end
|
||||
|
||||
# Actualiza el emoji de fase si todas sus tareas están completadas
|
||||
def update_phase_emoji_if_needed(phase_number)
|
||||
phase = @phases.find { |p| p[:number] == phase_number }
|
||||
return unless phase
|
||||
|
||||
phase_tasks = tasks_by_phase(phase_number)
|
||||
return if phase_tasks.empty?
|
||||
|
||||
all_completed = phase_tasks.all? { |t| t[:completed] }
|
||||
new_emoji = all_completed ? '✅' : PHASE_EMOJIS[phase_number] || '⬜️'
|
||||
|
||||
# Actualizar línea de fase si el emoji cambió
|
||||
line_index = phase[:line_number] - 1
|
||||
original_line = @lines[line_index]
|
||||
|
||||
if original_line && original_line.match(PHASE_PATTERN)
|
||||
current_emoji = $1
|
||||
if current_emoji != new_emoji
|
||||
updated_line = original_line.sub(/#{Regexp.escape(current_emoji)}/, new_emoji)
|
||||
@lines[line_index] = updated_line
|
||||
phase[:emoji] = new_emoji
|
||||
@changes_made = true
|
||||
@logger.info("Fase #{phase_number} actualizada: #{current_emoji} → #{new_emoji}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Estima fecha de completitud basada en progreso
|
||||
def estimate_completion_date
|
||||
stats = progress_stats
|
||||
return nil if stats[:overall_percentage] == 100 || stats[:overall_percentage] == 0
|
||||
|
||||
start_date = Time.parse(@metadata[:date] || @metadata[:created_at])
|
||||
elapsed_days = (Time.now - start_date).to_i / 86400
|
||||
|
||||
if elapsed_days > 0
|
||||
completion_rate = stats[:overall_percentage] / elapsed_days
|
||||
remaining_percentage = 100 - stats[:overall_percentage]
|
||||
estimated_days_remaining = (remaining_percentage / completion_rate).ceil
|
||||
(Time.now + (estimated_days_remaining * 86400)).strftime("%Y-%m-%d")
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Crea backup del archivo original
|
||||
def create_backup
|
||||
backup_dir = File.join(File.dirname(@file_path), 'backups')
|
||||
FileUtils.mkdir_p(backup_dir)
|
||||
|
||||
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
||||
backup_file = File.join(backup_dir, "#{File.basename(@file_path, '.md')}_#{timestamp}.md")
|
||||
|
||||
FileUtils.cp(@file_path, backup_file)
|
||||
@logger.info("Backup creado: #{backup_file}")
|
||||
end
|
||||
|
||||
# Genera reporte en formato texto
|
||||
def generate_text_report
|
||||
stats = progress_stats
|
||||
|
||||
report = []
|
||||
report << "=" * 80
|
||||
report << "REPORTE DE PROGRESO - #{@metadata[:title]}"
|
||||
report << "=" * 80
|
||||
report << ""
|
||||
report << "📊 ESTADÍSTICAS GENERALES"
|
||||
report << " • Tareas totales: #{stats[:total_tasks]}"
|
||||
report << " • Completadas: #{stats[:completed_tasks]}"
|
||||
report << " • Pendientes: #{stats[:pending_tasks]}"
|
||||
report << " • Progreso total: #{stats[:overall_percentage]}%"
|
||||
report << ""
|
||||
|
||||
report << "📈 PROGRESO POR FASES"
|
||||
stats[:phases].each do |phase|
|
||||
emoji = phase[:percentage] == 100 ? '✅' : PHASE_EMOJIS[phase[:number]] || '⬜️'
|
||||
report << " #{emoji} FASE #{phase[:number]}: #{phase[:name]}"
|
||||
report << " • Completadas: #{phase[:completed]}/#{phase[:total]} (#{phase[:percentage]}%)"
|
||||
end
|
||||
|
||||
report << ""
|
||||
report << "🚀 PRÓXIMAS TAREAS"
|
||||
next_tasks = @tasks.select { |t| !t[:completed] }.first(5)
|
||||
if next_tasks.any?
|
||||
next_tasks.each do |task|
|
||||
report << " • #{task[:id]}: #{task[:description]}"
|
||||
end
|
||||
else
|
||||
report << " ¡Todas las tareas están completadas! 🎉"
|
||||
end
|
||||
|
||||
report << ""
|
||||
report << "📅 ESTIMACIÓN DE COMPLETITUD"
|
||||
if completion_date = estimate_completion_date
|
||||
report << " • Fecha estimada: #{completion_date}"
|
||||
else
|
||||
report << " • Insuficientes datos para estimación"
|
||||
end
|
||||
|
||||
report << ""
|
||||
report << "🔄 ÚLTIMA ACTUALIZACIÓN: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
report << "=" * 80
|
||||
|
||||
report.join("\n")
|
||||
end
|
||||
|
||||
# Genera reporte en formato markdown
|
||||
def generate_markdown_report
|
||||
stats = progress_stats
|
||||
|
||||
report = []
|
||||
report << "# Reporte de Progreso - #{@metadata[:title]}"
|
||||
report << ""
|
||||
report << "**Fecha de generación:** #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
report << ""
|
||||
|
||||
report << "## 📊 Estadísticas Generales"
|
||||
report << ""
|
||||
report << "| Métrica | Valor |"
|
||||
report << "|---------|-------|"
|
||||
report << "| Tareas totales | #{stats[:total_tasks]} |"
|
||||
report << "| Tareas completadas | #{stats[:completed_tasks]} |"
|
||||
report << "| Tareas pendientes | #{stats[:pending_tasks]} |"
|
||||
report << "| **Progreso total** | **#{stats[:overall_percentage]}%** |"
|
||||
report << ""
|
||||
|
||||
report << "## 📈 Progreso por Fases"
|
||||
report << ""
|
||||
stats[:phases].each do |phase|
|
||||
emoji = phase[:percentage] == 100 ? '✅' : PHASE_EMOJIS[phase[:number]] || '⬜️'
|
||||
progress_bar = "[#{'█' * (phase[:percentage] / 5)}#{'░' * (20 - phase[:percentage] / 5)}]"
|
||||
report << "### #{emoji} FASE #{phase[:number]}: #{phase[:name]}"
|
||||
report << ""
|
||||
report << "- **Progreso:** #{progress_bar} #{phase[:percentage]}%"
|
||||
report << "- **Completadas:** #{phase[:completed]}/#{phase[:total]}"
|
||||
report << ""
|
||||
end
|
||||
|
||||
report << "## 🚀 Próximas Tareas Prioritarias"
|
||||
report << ""
|
||||
next_tasks = @tasks.select { |t| !t[:completed] }.first(10)
|
||||
if next_tasks.any?
|
||||
next_tasks.each do |task|
|
||||
report << "- [ ] **#{task[:id]}:** #{task[:description]}"
|
||||
end
|
||||
else
|
||||
report << "🎉 **¡Todas las tareas están completadas!**"
|
||||
end
|
||||
|
||||
report.join("\n")
|
||||
end
|
||||
|
||||
# Genera reporte en formato JSON
|
||||
def generate_json_report
|
||||
JSON.pretty_generate(
|
||||
metadata: @metadata.merge(report_generated_at: Time.now.iso8601),
|
||||
progress: progress_stats,
|
||||
executive_summary: executive_summary,
|
||||
validation: validate_plan
|
||||
)
|
||||
end
|
||||
|
||||
# Logger por defecto
|
||||
def default_logger
|
||||
Class.new do
|
||||
def info(msg); puts "[INFO] #{msg}"; end
|
||||
def warn(msg); puts "[WARN] #{msg}"; end
|
||||
def error(msg); puts "[ERROR] #{msg}"; end
|
||||
def debug(msg); puts "[DEBUG] #{msg}"; end
|
||||
end.new
|
||||
end
|
||||
|
||||
# Log de depuración (solo se muestra si ADN_DEBUG está activado)
|
||||
def debug_log(message)
|
||||
if ENV['ADN_DEBUG'] == '1' || ENV['ADN_DEBUG'] == 'true'
|
||||
@logger.debug(message) if @logger.respond_to?(:debug)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user