Módulo Depurador
Proyecto: Kmila-9s
Este módulo es un componente central de Kmila-9s, un proyecto titulado oficialmente "Aplicación Multiplataforma para el Aprendizaje y Depuración de Código VHDL sin Requerimiento de Hardware Físico" (Multiplatform Application for Learning and Debugging VHDL Code without Requiring Physical Hardware).
Autor: Ulrich Tamayo Daniel Correo: [email protected] Correo 2: [email protected] Versión: 1.14.1 Última actualización: 3 de agosto de 2026
1. Panorama del proyecto
El objetivo principal de este proyecto es desarrollar una aplicación de escritorio libre, de código abierto y multiplataforma para la depuración interactiva de código VHDL. La aplicación busca facilitar el aprendizaje del diseño digital al proporcionar un entorno unificado para la simulación y la visualización en tiempo real de señales digitales, ciclos de reloj y estados de puertos, eliminando por completo la necesidad de hardware físico como FPGAs.
Motivaciones clave
- Accesibilidad: Superar la barrera económica de las costosas tarjetas FPGA y las licencias de software propietario.
- Mejora pedagógica: Ofrecer una interfaz gráfica integrada e intuitiva que simplifica el proceso de depuración, a diferencia de los flujos de trabajo fragmentados basados en línea de comandos de herramientas como GHDL y GTKWave.
- Flexibilidad: Proporcionar una aplicación autocontenida que se ejecuta localmente en múltiples sistemas operativos (Windows, macOS, Linux, Android, iOS) sin requerir conexión a internet.
Este proyecto está construido con .NET y MAUI, lo que garantiza una base de código moderna y mantenible con amplio soporte de plataformas.
2. El módulo Depurador
Este módulo específico, el Depurador, es una fachada ligera que enlaza los archivos fuente crudos de VHDL con los modelos de datos del Interpreter. En el estado actual de la base de código realiza dos tareas:
- Ejecutar una pasada estructural rápida para extraer los bloques
entity/architecturede nivel superior y, a partir de ellos, poblar los objetosInterpreter.Services.Entity/Interpreter.Services.Architecture. - Ejecutar una pasada de lint ligera a nivel de fuente (
RunDiagnostics) que emite entradas estructuradas deParser.Models.Diagnostic(falta de;, desbalance debegin/end) para que la superficie del editor las renderice como marcadores de Monaco — antes de que se ejecute el análisis estructural, de modo que las advertencias sigan apareciendo en fuentes con errores.
No orquesta la simulación en vivo ni gestiona puntos de interrupción; esas responsabilidades residen en Kmila.Shared.Services (SimulationParameters, SimulationCoordinator, etc.) y en el ejecutor de simulación del Interpreter. Debugger.csproj aún hace ProjectReference a Interpreter y a TimeMachine porque la pasada estructural devuelve los resultados a tipos de Interpreter y los consumidores posteriores (Kmila.Shared) dependen de que las instantáneas de TimeMachine sean alcanzables desde el mismo grafo de ensamblados — pero nada dentro del propio Debugger invoca a TimeMachine. Revisiones anteriores del README describían un rol más amplio de "orquestador central" que nunca se materializó aquí; este módulo se mantuvo exclusivamente como preprocesamiento.
Responsabilidades centrales
- Procesamiento de archivos VHDL: Lectura de archivos fuente
.vhdlcrudos desde el sistema de archivos (File.ReadAllTexten el constructor). - Análisis estructural: Recorrido del archivo línea por línea con una máquina de estados de 3 estados (default → entity → architecture) para identificar los bloques de nivel superior.
- Poblado de modelos de datos: Construcción de objetos
EntityyArchitecture(del móduloInterpreter) y asociación de la arquitectura a su entidad declarante medianteentity.SetArchitecture(arch). - Diagnósticos a nivel de fuente: Recolección de advertencias no fatales sobre punto y coma faltantes y balance de
begin/endenDiagnostics, apto para el mapeo directo asetModelMarkersde Monaco. - Análisis resiliente: La pasada de lint se ejecuta antes de la pasada estructural, de modo que aunque la pasada estructural lance una excepción, el editor siga mostrando diagnósticos sobre los cuales el usuario pueda actuar.
3. Arquitectura del módulo Depurador
El módulo Depurador está diseñado como un coordinador de alto nivel que prepara los datos para los demás componentes centrales del proyecto Kmila-9s.
Componentes clave
Program.cs: Un pequeño arnés de prueba de consola que explora el directorioCodes/en busca de archivos.vhdly analiza cada uno, imprimiendo el número de entidades que encontró por archivo. Útil para pruebas de humo del puente del analizador de forma aislada.Repositories/Parser.cs: La única clase real del módulo. Lee un archivo.vhdlen el constructor y expone:string FileContent { get; private set; }— texto crudo del archivo.List<Entity> Entities { get; private set; }— entidades descubiertas (con sus arquitecturas ya asociadas tras el análisis).List<Parser.Models.Diagnostic> Diagnostics { get; private set; }— advertencias estructuradas producidas por la pasada de lint.void ParseFile()— ejecuta primero la pasada de lint y luego recorre el archivo con la máquina de estados de 3 estados para poblarEntities. Reenvía aParseFile(CancellationToken.None).void ParseFile(CancellationToken cancellationToken)— sobrecarga cancelable agregada en la sesión 22 pasada 4 (F-G-08). Propaga el token tanto aentity.Syntetize(rawEntity, ct)como aarch.Syntetize(rawEntity, ct)para que el gancho de tiempo límite de pared (KMILA_SIM_WALL_TIMEOUT_MSenHeadlessSimulator) pueda realmente interrumpir un descenso de síntesis atascado en lugar de esperar a que el cliente HTTP se desconecte. Antes de la pasada 4, el analizador no tenía superficie de cancelación.private void RunDiagnostics(string source)— la pasada de lint en sí.private static bool LooksLikeStatement(string lower)— heurística que decide qué líneas deberían requerir un;final.
Modelos de datos centrales
Este módulo usa modelos de datos definidos en dos módulos anteriores en la cadena:
Interpreter.Services.Entity— representa una entidad VHDL, incluidos sus puertos y genéricos.Interpreter.Services.Architecture— representa una arquitectura VHDL;Syntetize(string[] lines)se invoca aquí una vez que se ha recolectado el cuerpo de la arquitectura.Parser.Models.Diagnostic+Parser.Models.DiagnosticSeverity— registro de diagnóstico estructurado usado para exponer los hallazgos del lint a la página del Editor. El Depurador nunca instanciaParser.Models.ParserExceptiondirectamente; esa es tarea del proyecto anteriorParser, pero el Editor mapea ambos al mismo flujo de diagnósticos.
Diagnósticos de lint emitidos
| Código | Severidad | Disparador |
|---|---|---|
VHD-MISSING-SEMICOLON |
Advertencia | La línea parece una sentencia (<=, :=, =>, o comienza con library/use/signal/variable/constant) pero no termina con ;, ,, is, then, else, loop, begin, generate, ( ni ). |
VHD-BLOCK-BALANCE |
Advertencia | El número total de palabras clave begin no coincide con el número total de palabras clave end en todo el archivo. Se reporta una sola vez en la última línea. |
Los errores lanzados por el proyecto anterior Parser se propagan como Parser.Models.ParserException; los llamadores (p. ej. la página del Editor) típicamente los convierten con Diagnostic.FromException(...) para que aparezcan en el mismo flujo de marcadores de Monaco.
4. Cómo usar este módulo
Para usar la funcionalidad de análisis del depurador, se proporciona la ruta de un archivo VHDL a Debugger.Repositories.Parser. El método ParseFile() entonces procesa el archivo, ejecuta la pasada de lint y puebla las listas Entities y Diagnostics.
using Debugger.Repositories;
using System;
// This example demonstrates the current file processing capability.
var files = Directory.GetFiles("Codes");
foreach (var file in files)
{
try
{
// 1. Initialize the Debugger's Parser with a file path.
Parser vhdlFileParser = new(file);
// 2. Process the file to identify entities and architectures.
// The lint pass runs first, even if the structural pass later throws.
vhdlFileParser.ParseFile();
// 3. The parsed entities are now available for the Interpreter.
Console.WriteLine($"File: {file}, Entities found: {vhdlFileParser.Entities.Count}");
// 4. Surface non-fatal lint warnings (mirrors what the Editor page does
// when wiring Monaco's setModelMarkers).
foreach (var d in vhdlFileParser.Diagnostics)
{
Console.WriteLine($" [{d.Severity}] L{d.Line}:{d.StartColumn} {d.Code} — {d.Message}");
}
}
catch (Exception ex)
{
// Structural failures (e.g. "Entity not found") propagate. The editor
// surface typically catches these and maps them to error-level
// diagnostics via Diagnostic.FromException(...).
Console.WriteLine($"An error occurred while parsing {file}: {ex.Message}");
}
}
5. Dependencias
Este módulo integra otras partes clave del proyecto Kmila-9s y depende de:
- Módulo
Interpreter: Para sus modelos de datos (Entity,Architecture) y el punto de entrada de síntesis de arquitectura (Architecture.Syntetize). - Módulo
Parser: Para dos cosas — elTokenizer(actualmente instanciado pero reservado para futuras reescrituras estructurales) y los tiposParser.Models.Diagnostic/Parser.Models.DiagnosticSeverityque usa la pasada de lint. - Solo la BCL de .NET 9 por lo demás — sin
Microsoft.Extensions.Logging, sin dependencia deTimeMachine. El Depurador es una capa de preprocesamiento; la simulación en vivo se dirige desde la pila del Editor deKmila.Shared, no desde este proyecto.
6. Flujo del Depurador y relaciones entre clases
Los siguientes diagramas ilustran el flujo de datos y las relaciones entre clases reales del módulo tal como está hoy. Revisiones anteriores de este README representaban un subsistema de "State Manager / Time Travel Logic / Simulation Orchestrator" dentro del Depurador que nunca existió en el código fuente — se ha eliminado.
6.1. Flujo de datos de alto nivel
graph TD
A["VHDL Files (.vhdl)"] --> B["Debugger.Repositories.Parser"]
B -->|RunDiagnostics first| D["List<Parser.Models.Diagnostic><br/>(warnings)"]
B -->|Structural state machine| C["Interpreter.Services.Entity<br/>+ Architecture (with body synthesised)"]
D --> E["Editor (Monaco markers)<br/>[External — Kmila.Shared]"]
C --> F["Interpreter Simulation<br/>[External]"]
style B fill:#ccf,stroke:#333,stroke-width:2px
style D fill:#fec,stroke:#333,stroke-width:1px
style F fill:#bbf,stroke:#333,stroke-width:2px
6.2. Diagrama de clases detallado
classDiagram
direction LR
class Program {
+static Main(args : string[])
}
class DbgParser["Debugger.Repositories.Parser"] {
+FileContent : string
+Entities : List~Entity~
+Diagnostics : List~Diagnostic~
-_excludedEnds : List~string~
+Parser(filePath : string)
+ParseFile() void
-RunDiagnostics(source : string) void
-LooksLikeStatement(lower : string)$ bool
}
class IntEntity["Interpreter.Services.Entity"] {
<<External>>
+Name : string
+Ports
+Generics
+SetArchitecture(arch)
}
class IntArch["Interpreter.Services.Architecture"] {
<<External>>
+Syntetize(lines : string[])
}
class Tok["Parser.Repositories.Tokenizer"] {
<<External, reserved>>
}
class Diag["Parser.Models.Diagnostic"] {
<<External>>
+Severity : DiagnosticSeverity
+Line : int
+StartColumn : int
+EndColumn : int
+Code : string
+Message : string
}
Program --> DbgParser : creates and uses
DbgParser --> IntEntity : instantiates
DbgParser --> IntArch : instantiates and Syntetize()
DbgParser ..> Tok : instantiates (currently unused)
DbgParser --> Diag : appends warnings
6.3. Diagrama de secuencia: análisis de un archivo
sequenceDiagram
participant Main
participant DbgParser as "Debugger.Repositories.Parser"
participant FileIO as "File IO"
participant Arch as "Interpreter.Services.Architecture"
Main->>DbgParser: new(filePath)
activate DbgParser
DbgParser->>FileIO: ReadAllText(filePath)
FileIO-->>DbgParser: file content
deactivate DbgParser
Main->>DbgParser: ParseFile()
activate DbgParser
DbgParser->>DbgParser: RunDiagnostics(FileContent)
Note right of DbgParser: emits VHD-MISSING-SEMICOLON,<br/>VHD-BLOCK-BALANCE warnings
loop For each non-empty line
DbgParser->>DbgParser: Detect entity / architecture starts
DbgParser->>DbgParser: Collect lines until "end ...;"
end
DbgParser->>DbgParser: new Entity(rawLines)
DbgParser->>Arch: new Architecture(ports, generics)
DbgParser->>Arch: Syntetize(rawLines)
DbgParser->>DbgParser: entity.SetArchitecture(arch)
deactivate DbgParser
DbgParser-->>Main: Entities + Diagnostics populated
6.4. Diagrama de módulos
graph TD
vhdl_code["VHDL Code Files<br>/Codes/*.vhdl"]
subgraph debugger_boundary["Debugger Project"]
program["Program.cs<br>(test harness)"]
dbg_parser["Repositories/Parser.cs<br>(structural pass + lint pass)"]
program -->|invokes| dbg_parser
end
subgraph parser_project["Parser Project [External]"]
diagnostic["Parser.Models.Diagnostic<br>+ DiagnosticSeverity"]
tokenizer["Parser.Repositories.Tokenizer<br>(instantiated, reserved)"]
end
subgraph interpreter_project["Interpreter Project [External]"]
entity["Interpreter.Services.Entity"]
architecture["Interpreter.Services.Architecture<br>.Syntetize(lines)"]
end
subgraph kmila_shared["Kmila.Shared [External — actual UI consumer]"]
editor_page["Pages/Editor.razor<br>(Monaco markers)"]
sim_coord["Services/SimulationParameters,<br>SimulationCoordinator"]
end
dbg_parser -->|reads| vhdl_code
dbg_parser -->|emits warnings into| diagnostic
dbg_parser -.->|references| tokenizer
dbg_parser -->|hydrates| entity
dbg_parser -->|hydrates and synthesises| architecture
editor_page -->|consumes Diagnostics + maps via Diagnostic.FromException| dbg_parser
sim_coord -->|drives the actual simulation, NOT the Debugger| architecture
7. Limitaciones actuales
Las siguientes limitaciones existen en la implementación actual:
- El Tokenizer no se usa activamente: La clase
Tokenizerdel módulo Parser se instancia dentro deParseFile()pero sus tokens no se consumen. La pasada estructural sigue usando coincidencia de cadenas a nivel de línea. Reservado para una futura reescritura. - Comentarios de VHDL: La pasada de lint elimina los comentarios de línea
--antes del análisis, pero las formas de comentarios multilínea / de bloque no se tratan de forma especial (VHDL en sí no tiene/* ... */). - Enfoque de análisis simple: La detección de estructura se apoya en
comparer.StartsWith("entity")/"architecture"— no se construye un AST real en este proyecto. El análisis estilo AST ocurre en el proyecto anteriorParser. - Suposición de una sola pareja entidad-arquitectura: Las arquitecturas se emparejan por nombre con entidades declaradas antes en el mismo archivo. Las declaraciones fuera de orden y las búsquedas entre archivos lanzan
Exception("Entity not found"). - Una sola pasada de diagnóstico:
RunDiagnosticssolo emite hoy dos códigos (VHD-MISSING-SEMICOLON,VHD-BLOCK-BALANCE). Reglas más ricas (dirección de puertos, discrepancias de tipos, señales sin usar) residen en la canalización de simulación, no aquí. - Sin simulación, sin TimeMachine: A pesar del nombre del proyecto, este módulo no ejecuta una simulación ni se comunica con
TimeMachine. La depuración en vivo se dirige desdeKmila.Shared.Servicescontra elInterpreter. test_output.vcdextraviado en la raíz del módulo: Un archivoDebugger/test_output.vcdse encuentra en la raíz del módulo. Es un artefacto residual, no producido por este módulo (que no ejecuta simulación alguna ni emite VCD — véase arriba). Trátelo como seguro de ignorar/eliminar durante la limpieza; no juega ningún papel en la pasada de preprocesamiento del Depurador.
7.bis. Diagramas de auditoría (2026-05-09)
Estos diagramas complementan las secciones 6.1–6.4. Se agregaron durante la auditoría
entre módulos porque los diagramas TT1 existentes o bien tergiversaban este módulo
(fig_5_2_c4_contenedores afirmaba que el Depurador delega la tokenización a Parser y
coordina con TimeMachine — ninguna de las dos cosas es cierta) o bien no representaban en absoluto
la canalización con lint primero. Las fuentes también residen en Documentacion/TT1/diagrams/.
fig_5_5_12_clases_debugger — Detalle de clases del Depurador
classDiagram
direction TB
class DbgParser {
+string FileContent
+List~Entity~ Entities
+List~Diagnostic~ Diagnostics
-List _excludedEnds
-int funcProcDepth
+ParseFile() void
+RunDiagnostics(content) void
-LooksLikeStatement(line) bool
}
class Tokenizer {
<<from Parser>>
+ReadFile(path) void
+Evaluate(token) void
}
class Diagnostic {
<<from Parser.Models>>
+Severity Severity
+int Line
+int StartColumn
+int EndColumn
+string Code
+string Message
+FromException(ex)$ Diagnostic
}
class Entity {
<<from Interpreter.Services>>
+Syntetize(lines: List~tuple~) void
+SetArchitecture(arch) void
}
class Architecture {
<<from Interpreter.Services>>
+Syntetize(rawEntity: List~tuple~) void
}
DbgParser ..> Tokenizer : instancia (no consume tokens)
DbgParser "1" *-- "*" Entity : produce
DbgParser "1" *-- "*" Diagnostic : emite
Entity "1" *-- "1" Architecture : compone
fig_5_11_12_act_debugger_lint_resilience — Canalización con lint primero
flowchart TD
A([DbgParser.ParseFile]) --> B[Cargar FileContent desde disco]
B --> C[**Lint pass**: RunDiagnostics FileContent]
C --> D[Por cada linea]
D --> E{LooksLikeStatement?}
E -->|Si y falta `;`| F[Diagnostic VHD-MISSING-SEMICOLON]
E -->|No| G[Skip]
D --> H[Contar begin / end por bloque]
H --> I{Balance?}
I -->|No| J[Diagnostic VHD-BLOCK-BALANCE]
F --> K[Diagnostics.Add]
J --> K
G --> L
K --> L[Continuar siguiente linea]
L --> M{Quedan lineas?}
M -->|Si| D
M -->|No| N[Lint pass completo: Diagnostics persistentes]
N --> O[**Structural pass**: maquina line-based<br/>default -> in-entity -> in-architecture]
O --> P{Excepcion en parsing?}
P -->|Si| Q[Capturar y agregar a Diagnostics]
Q --> R([Retornar: Entities parciales + Diagnostics completos])
P -->|No| S[Crear Entity / Architecture<br/>con tuplas List int string]
S --> T[Entity.Syntetize / Architecture.Syntetize]
T --> U[Entity.SetArchitecture]
U --> V([Retornar: Entities completas + Diagnostics])
8. Registro de cambios
Versión 1.14.1 (2026-05-01)
Realineación de la documentación
- Sección 2 / "Responsabilidades centrales": Se eliminó la antigua afirmación de que el módulo Depurador orquesta
Parser+Interpreter+TimeMachine. El código actual es solo de preprocesamiento (extracción estructural + diagnósticos de lint). La simulación en vivo siempre ha sido dirigida porKmila.Shared.Services. - Sección 3 / Componentes clave: Se agregaron la propiedad
Diagnostics, la pasada de lintRunDiagnostics(string)y el ayudanteLooksLikeStatement(string). Se agregó una subsección explícita "Diagnósticos de lint emitidos" enumerando los dos códigos (VHD-MISSING-SEMICOLON,VHD-BLOCK-BALANCE). - Sección 4 / Ejemplo de uso: Se actualizó el fragmento para exponer
Diagnostics(la propiedad que el editor realmente consume) y se aclaró queParser.Models.ParserExceptionse mapea medianteDiagnostic.FromException(...)aguas arriba. - Sección 5 / Dependencias: Se eliminaron las dependencias espurias de
TimeMachineyMicrosoft.Extensions.Logging— el Depurador solo referencia aInterpreteryParser, sobre .NET 9 puro. - Sección 6 / Diagramas — los cuatro regenerados:
- §6.1 ahora muestra la pasada de lint ejecutándose antes de la pasada estructural y alimentando al editor de forma independiente.
- §6.2 lista la superficie pública real (
Diagnostics,_excludedEnds, los ayudantesRunDiagnostics/LooksLikeStatement) y agrega la clase externaParser.Models.Diagnostic. - §6.3 inserta el paso
RunDiagnosticsy la llamada aArchitecture.Syntetize. - §6.4 eliminó las fronteras ficticias "State Manager" y "Time Travel Logic" y las reemplazó con la superficie de consumo real (
Kmila.Shared.Pages.EditorySimulationCoordinator).
- Sección 7 / Limitaciones: Se refinó la nota sobre el manejo de comentarios (los comentarios de línea se eliminan, y de todas formas no existe una forma de comentario de bloque en VHDL), se agregaron "Una sola pasada de diagnóstico" + "Sin simulación, sin TimeMachine" para fijar expectativas correctas.
Versión 0.6.1 (2026-02-21)
Correcciones de documentación
- README.md: Se corrigió el diagrama de módulos para referenciar correctamente
Parser.Repositories.Tokenizeren lugar deInterpreter.Services.Tokenizer - README.md: Se actualizó el nodo VHDL Models para referenciar ambos espacios de nombres
Interpreter.ModelseInterpreter.Services(Entity está en Services, los modelos en Models)
Versión 0.6.0 (2025-12-13)
Mejoras de documentación
Program.cs: Se agregó documentación XML exhaustiva que incluye:
- Resumen a nivel de clase que explica el propósito del arnés de prueba
- Documentación de métodos con pasos de procesamiento
- Diagrama de la estructura de directorios esperada
- Documentación de excepciones
Repositories/Parser.cs: Se agregó documentación XML completa que incluye:
- Documentación a nivel de clase que explica el algoritmo de la máquina de estados
- Comentarios detallados sobre las limitaciones del análisis
- Documentación de propiedades para
FileContentyEntities - Documentación del campo privado
_excludedEnds - Documentación del constructor y de los métodos con detalles de procesamiento paso a paso
- Comentarios en línea que explican las transiciones de estado y la asociación entidad/arquitectura
README.md: Se agregó información de versión, fecha de última actualización, sección de limitaciones actuales y este registro de cambios
Versión 1.14.0 (2026-04-19)
Repositories/Parser.cs— recolección de diagnósticos. Se agregó una propiedadDiagnostics(tipada comoglobal::Parser.Models.Diagnosticpara esquivar la colisión de espacios de nombres entre la claseParserde este archivo y el proyectoParser) más una pasada de lint ligeraRunDiagnostics(source)que detecta;faltantes al final de sentencias y desbalance debegin/endantes de pasar al analizador completo. Alimenta directamente asetModelMarkersde Monaco desde el editor.- Las instancias de
ParserExceptiontambién se coaccionan medianteDiagnostic.FromException(...)a la misma lista, de modo que su exposición en el editor sea uniforme independientemente de si el error provino de la pasada de lint o de que el analizador lanzara una excepción. - Higiene de compilación — se agregó
<NoWarn>para el efecto en cadena de la documentación XML, de modo que el proyecto Depurador compile limpiamente junto con el resto de la solución.
Novedades en la v1.15 (2 de mayo de 2026)
- Diagnósticos precisos a nivel de línea de fuente (#21).
ParseFileahora recolecta cada línea no vacía como(lineIndex, text)y alimenta esas tuplas a las nuevas sobrecargasEntity.Syntetize(IReadOnlyList<(int, string)>)/Architecture.Syntetize(IReadOnlyList<(int, string)>), que llaman aTokenizer.TokenizeLineAt. Los mensajes de error referencian las líneas reales del archivo del usuario en lugar del contador interno de llamadas del tokenizer. - Los diagnósticos de lint se exponen incluso ante un fallo de análisis.
Program.MainenvuelveParseFile()en un try/catch e imprime cadaDiagnostic(p. ej.VHD-MISSING-SEMICOLON,VHD-BLOCK-BALANCE) antes de reemitir el error estructural. Anteriormente, una excepción ocultaba toda la salida de lint aguas arriba. - Corrección del error de seccionamiento de arquitectura. El respaldo por coincidencia de subcadenas
comparer.Contains(currentArchName.ToLower())ya no confundeend maxv;con el fin de la arquitectura dearchitecture A of …. Solo coincidencia de nombre exacto, más una lista de exclusión de palabras clave de control de flujo (end if/loop/case/for/while/generate/record). - GC de estación de trabajo habilitado a nivel de proyecto (#7).
Notas de versión completas: ../Documentacion/CHANGELOG_v1.15.md.
Última actualización: 2026-08-03