685 lines
23 KiB
Ruby
685 lines
23 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# adn/tools/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
|