# Changelog Historial de cambios del proyecto. Formato basado en [Keep a Changelog](https://keepachangelog.com/es/). Los cambios se describen desde la perspectiva del código: qué archivos cambiaron, por qué, y qué decisiones de diseño están implícitas — para que futuras modificaciones profundas tengan contexto suficiente. --- ## [Unreleased] — Sprint 1: ROI y Escenarios de Automatización ### Estado En construcción. Etapas 1–4 completas; Etapas 5–6 pendientes. ### Etapa 1 — Cimientos del dominio (completada) **Nuevos campos en el modelo de dominio** (`src/domain/types.ts`): - `Activity` gana: `automatable: boolean`, `automatedCostFixed: number`, `automatedTimeMinutes: number` - `Process` gana: `annualFrequency: number`, `analysisHorizonYears: number`, `automationInvestment: number` - Nuevo tipo `Scenario = 'actual' | 'automated'` - `SimulationInput` acepta `scenario?: Scenario` (opcional, default `'actual'` para retrocompatibilidad) - `Simulation` gana `resultAutomated?: SimulationResult` (opcional, para retrocompatibilidad con snapshots viejos) **Migración Dexie v1 → v2** (`src/persistence/db.ts`): - `version(2).upgrade()` popula defaults en registros existentes: - Activities: `automatable=false`, `automatedCostFixed=0`, `automatedTimeMinutes=0` - Processes: `annualFrequency=1000`, `analysisHorizonYears=3`, `automationInvestment=0` - Sin cambio de índices — solo nuevos campos con defaults seguros. **Nuevo módulo de ROI** (`src/domain/roi.ts`): - Función pura `calculateRoi(input: RoiInput): RoiResult` - Fórmulas nominales simples (sin VPN ni TIR — decisión de diseño ADR-014) - Edge cases explicitados: inversión cero, ahorro negativo, costo actual cero (evitar div/0) - `paybackMonths = Infinity` cuando no hay ahorro, `roiAnnualPercent = Infinity` cuando inversión=0 y ahorro>0 **Motor de simulación dual** (`src/domain/simulation.ts`): - Acepta `scenario` en `SimulationInput` - En `'automated'`: actividades con `automatable=true` usan `automatedCostFixed`/`automatedTimeMinutes` - Las no-automatable mantienen sus costos originales en ambos escenarios - La probabilidad de ejecución NO cambia entre escenarios — solo costos y tiempos **Simulation store refactorizado** (`src/store/simulation-store.ts`): - `result` (single) → `resultActual + resultAutomated + lastSimulatedAt` - Nueva acción `persistResults(processId, actual, automated)`: guarda en Dexie + actualiza memoria en un solo paso atómico - Nueva acción `invalidateResults()`: pone ambos a null - Suscripción reactiva a `process-store`: cualquier cambio en `activities` o `currentProcess` invalida ambos resultados automáticamente - **Dependencia intencional documentada**: `simulation-store` importa `process-store` en una dirección (sin inversa → sin ciclo) **useSimulate refactorizado** (`src/features/simulation/useSimulate.ts`): - Corre las dos simulaciones en el mismo ciclo y llama `persistResults` del store (eliminando el import directo de `simulationRepo` desde un hook de feature) - Retorna `{ resultActual, resultAutomated }` en lugar de un único result **Infraestructura ESLint** (`eslint.config.js` — archivo nuevo): - Primer config de ESLint del proyecto (antes no existía) - Regla `no-restricted-imports`: prohíbe importar `persistence/repositories` desde fuera de `store/` - Overrides para `store/**`, `features/import/**`, `features/report/useReportData.ts`, `tests/**` - Regla `react-hooks/set-state-in-effect: 'off'`: patrón de sync local state con useEffect es válido en este codebase - **Por qué importa**: si alguien llama `activityRepo.save()` directamente desde una feature sin pasar por el store, los resultados de simulación quedan stale (la suscripción reactiva no dispara) **Tests nuevos** (32 tests): - `tests/domain/roi.test.ts`: 16 casos cubriendo happy path, inversión cero, ahorro negativo, NaN - `tests/integration/dexie-migration.test.ts`: 6 casos de persistencia de los nuevos campos con valores default y no-default - `tests/store/simulation-store-invalidation.test.ts`: 7 casos de la invariante de invalidación dual - `tests/integration/sprint1-flow.test.ts`: 11 casos del flujo integrado BPMN → sim actual → sim automated → calculateRoi → KPIs - `tests/domain/simulation.test.ts`: +4 casos de `scenario=automated` con costos en cero, sin NaN, probabilidad invariante - `tests/integration/bpmn-collaboration.test.ts`: 3 casos para BPMNs de colaboración con múltiples pools **Archivos de test actualizados** (fixtures con nuevos campos): - `tests/integration/simulation-engine.test.ts` - `tests/integration/gateway-validation.test.ts` - `tests/integration/indexeddb-persistence.test.ts` - `tests/features/report/components.test.tsx` - `tests/features/report/derivations.test.ts` - `tests/lib/export/measure-sizes.test.ts` - `tests/lib/export/csv-export.test.ts` - `tests/lib/export/pdf-export.test.ts` - `tests/setup.ts`: mocks globales `ResizeObserver` y `PointerEvent` para jsdom (necesario desde Sprint 1 para tests con Slider de Radix) ### Etapa 2 — UI de Configuración (completada) **Switch component** (`src/components/ui/switch.tsx` — nuevo): - Implementación Tailwind pura sin `@radix-ui/react-switch` - `role="switch"` + `aria-checked` + `aria-label` para accesibilidad - Transiciones CSS: `transition-colors duration-200` en el fondo, `transition-transform duration-200` en el knob - **Decisión**: no instalar `@radix-ui/react-switch` para no agregar dependencia — el comportamiento visual es idéntico **ActivityPanel ampliado** (`src/features/workspace/ActivityPanel.tsx`): - Nueva sección "Automatización" al pie del panel, separada con `` - Toggle Switch (`automatable`) oculta/muestra los campos con `transition-all duration-200 overflow-hidden max-h-{0|80}` - **Limitación conocida**: el max-height trick no anima a la altura real del contenido. Ver TODO.md para mejora con `useRef + scrollHeight` - Inputs numéricos `automatedCostFixed` y `automatedTimeMinutes` con mismo patrón que los existentes (placeholder cuando valor=0) - Helper ámbar de transparencia metodológica: - Aparece cuando `automatable=true && cost===0 && time===0` - **Siempre está en el DOM** (no conditional render) — la visibilidad se controla con `opacity-0/100 + max-h-0/20 + transition-all duration-150` - `aria-hidden` refleja el estado de visibilidad - **Por qué importa**: con conditional render (`&&`) el helper desaparece instantáneamente al tipear — sin transición. Al mantenerlo en el DOM, el fade de 150ms se aplica correctamente **GlobalSettingsPanel ampliado** (`src/features/workspace/GlobalSettingsPanel.tsx`): - Nueva sección "Análisis de Impacto" al pie del panel - `annualFrequency`: input numérico, min=1, sugerencias de referencia en texto helper - `analysisHorizonYears`: Slider de 1 a 10 años con valor en texto ("3 años") - `automationInvestment`: input numérico, default placeholder vacío cuando valor=0 - La validación de rango de `analysisHorizonYears` (clamp 1-10) ocurre en `handleSave`, no en `onChange` **BpmnCanvas ampliado** (`src/features/workspace/BpmnCanvas.tsx`): - Nueva prop `automatableElementIds?: string[]` - `syncAutomationOverlays()`: callback memoizado con `useCallback([automatableElementIds])` - Limpia overlays anteriores con `overlays.remove({ type: 'automation-indicator' })` - Re-agrega un badge por cada elementId en la lista - Se llama después de `importXML` (en el `.then()`) y en un `useEffect` separado cuando cambia la lista - **Posicionamiento del badge**: `{ top: -8, left: element.width - 12 }` - Obtenido via `elementRegistry.get(elementId)` con fallback `width ?? 100` - **No usar** `position.right` de diagram-js: con `right: -8`, la fórmula interna `left = right * -1 + width` da `left = 8 + width`, colocando el badge enteramente fuera del nodo sin overlap de esquina - Con `left = width - 12`: badge left está 12px antes del borde derecho, su borde derecho queda 8px fuera → overlap visible en la esquina - **Badge HTML**: círculo 20×20px, fondo `#fffbeb` (amber-50), borde `#e2e8f0`, sombra `0 1px 3px rgba(0,0,0,0.12)`, ícono Bot Lucide en `#f59e0b` como SVG inline - `pointer-events: none` para no interferir con clicks en el nodo - `title` nativo del browser como tooltip - **Por qué `#fffbeb` y no `#ffffff`**: el fondo de los nodos BPMN también es blanco, el fondo ámbar le da identidad cromática propia y hace visible el círculo por contraste **WorkspacePage** (`src/features/workspace/WorkspacePage.tsx`): - Destructura `activities` del process-store - Calcula `automatableElementIds` con `useMemo` - Pasa `useDeferredValue(automatableElementIds)` al BpmnCanvas (evita cascada de re-renders al editar el panel) **AutomationScenarioNote** (`src/features/report/AutomationScenarioNote.tsx` — nuevo): - Componente de leyenda para el tab "Automatizado" del reporte - Preparado y listo para ser colocado por Etapa 3 — no tiene lógica propia ### Etapa 3 — Motor dual y reporte con tabs (completada) **ReportPage ampliada** (`src/features/report/ReportPage.tsx`): - La página ahora tiene 3 tabs: **Actual** | **Automatizado** | **Comparación + ROI** - El contenido existente (KpiBar, heatmap, 3 gráficos, tabla de actividades) pasa intacto al tab "Actual" - Los tabs "Automatizado" y "ROI" se deshabilitan si la simulación no tiene `resultAutomated` (snapshot viejo) — en ese caso se muestra `NeedsResimulate` que guía al usuario a re-simular - `useMemo` para `automatableIds` y `roi` se declaran antes de los early returns (cumple reglas de hooks) - Los refs de heatmap son independientes por tab (`heatmapRef` / `heatmapRefAuto`) — el tab "Actual" sigue siendo el fuente de imagen para el export PDF - Export PDF/CSV sin cambios — Etapa 4 los ampliará **Tab "Automatizado"**: - Mismo layout que "Actual": KpiBar → heatmap con cross-linking → 3 gráficos → tabla - Usa `simulation.resultAutomated` como fuente de datos - `AutomationScenarioNote` aparece al pie del heatmap (componente preparado en Etapa 2) - Heatmap y tabla tienen su propio estado `selectedIdAuto` — cross-linking funciona independientemente del tab "Actual" **Tab "Comparación + ROI"**: - `RoiKpiBar` (`src/features/report/RoiKpiBar.tsx` — nuevo): 5 KPI cards en grid 2×5 (lg) - Ahorro por ejecución (con % sobre costo actual) - Ahorro anual - Payback (meses o años según magnitud; "Inmediato" si inversión=0; "No se recupera" si Infinity) - Ahorro acumulado neto a N años - ROI anual % ("∞%" si inversión=0 y ahorro>0) - Los valores positivos se muestran en `text-emerald-700`, negativos en `text-red-600` - `ComparisonTable` (`src/features/report/ComparisonTable.tsx` — nuevo): - Tabla actividad por actividad: costo actual | costo automatizado | ahorro | % ahorro - Ícono Bot ámbar en la primera columna para actividades marcadas `automatable` - Ordenable por ahorro (↕) — default descendente - Usa `Set` para el lookup de `automatable` en O(1) - `CumulativeSavingsChart` (`src/features/report/CumulativeSavingsChart.tsx` — nuevo): - Gráfico de línea + área (ECharts): X=años 0..N, Y=ahorro neto acumulado - Línea de break-even en y=0 siempre visible - Marker vertical dashed verde en `x=paybackYears` (solo si finite y dentro del horizonte) - Tooltip muestra valor del año exacto con color según signo - `TopSavingsChart` (`src/features/report/TopSavingsChart.tsx` — nuevo): - Top 5 actividades por ahorro positivo — barras horizontales verdes - Excluye actividades con ahorro ≤ 0 - Altura dinámica: `max(180, n * 48)` px — no hay espacio vacío si hay menos de 5 actividades - `TransparencySection` (`src/features/report/TransparencySection.tsx` — nuevo): - Condicional — solo se renderiza si hay issues metodológicos detectados - **Issue 1**: actividades con `automatable=true` pero `automatedCostFixed=0` y `automatedTimeMinutes=0` → el ahorro puede estar inflado. Lista los nombres de cada actividad - **Issue 2**: `automationInvestment=0` → payback "inmediato" e ROI "infinito" no reflejan la realidad - Fondo amber-50, borde amber-200 — consistente con el helper del ActivityPanel **Por qué ROI inline y no en `useReportData`**: El `calculateRoi` toma `simulation.result.totalCost` y `simulation.resultAutomated.totalCost` — valores ya disponibles en el componente. Moverlo al hook agregaría un `useEffect` extra sin beneficio. Se mantiene en el componente como `useMemo`. **Tests nuevos** (17 tests): - `tests/features/workspace/ActivityPanel.test.tsx`: 6 casos — toggle, helper ámbar con `aria-hidden`, dispatch de `updateActivity` - `tests/features/workspace/GlobalSettingsPanel.test.tsx`: 7 casos — defaults, edición de campos, dispatch de `updateProcess` - `tests/features/workspace/BpmnCanvas.test.tsx`: 4 casos — overlay add/remove, posición `left=width-12`, HTML con `#fffbeb` y `#f59e0b` **Patrón de mocks para tests de componentes** (documentado para referencia futura): - Los mocks del store con `vi.hoisted()` devuelven referencias estables - Sin `vi.hoisted`: cada llamada a `useProcessStore()` devuelve un nuevo objeto → el `useEffect([activities])` de ActivityPanel dispara en loop infinito → "Maximum update depth exceeded" - `simulation-store.ts` importa `process-store.ts` al nivel de módulo: los mocks deben ser compatibles o el store no carga --- ## [0.1.0-mvp] — MVP Fase 0 — 2026-05-13 ### Contexto Primera entrega funcional. Construida en una sesión de trabajo. Objetivo: demostrar en ≤15 minutos que se puede transformar un BPMN en un reporte de costos. ### Funcionalidades - Importar BPMN 2.0 por drag & drop o desde procesos de ejemplo - Renderizar diagrama en canvas read-only (bpmn-js Viewer) - Configurar actividades: costo fijo, tiempo, recursos asignados con utilización - CRUD de recursos (rol/persona/sistema/equipo/insumo + costo/hora) - Configuración global: moneda, overhead %, nombre proceso, cliente - Motor de simulación determinístico con propagación de probabilidades por gateways XOR, AND, inclusivo - Detección de loops (back-edges via DFS): se advierte pero no bloquea - Reporte visual: mapa de calor en dos modos (relativo/absoluto), KPIs, top actividades, composición, costo por recurso - Export PDF: 3 páginas (header+KPIs+heatmap, análisis, tabla detalle), con jsPDF-autotable - Export CSV: con BOM UTF-8 y metadatos del proceso - Persistencia en IndexedDB (Dexie v1) - Deploy estático en Cloudflare Pages ### Decisiones de diseño relevantes para el futuro **Parser BPMN multi-pool** (`bpmn-parser.ts`): - Itera todos los `` hijos directos de `` (no solo el primero) - Fix crítico: el parser original usaba `querySelector` que retorna solo el primero → todos los pools de una colaboración excepto el primero eran ignorados - Pools de soporte (SAP, bandeja manual) pueden no tener StartEvent propio — la validación exige al menos uno entre todos los pools **Heatmap sobre bpmn-js** (`HeatmapCanvas.tsx`): - bpmn-js rechaza la promesa de `importXML` pero el diagrama se renderiza parcialmente → usar `eventBus.on('import.done', ...)` en lugar de `.then()` - `element.style.fill = color` (NO `setAttribute('fill', color)`): bpmn-js aplica CSS que tiene mayor especificidad que atributos SVG - `captureImage()` espera 500ms después de importar para que la transición CSS `fill 0.35s ease` complete antes de la captura con html2canvas **Validación XOR en WorkspacePage** (`WorkspacePage.tsx`): - `hasLoadedOnce` ref: evita que el redirect "proceso no encontrado" dispare en el render inicial donde `isLoading=false` y `currentProcess=null` (antes del primer efecto de carga) ### Tests 268 tests unitarios + integración al cierre de MVP. 5 tests E2E con Playwright (import → simulate → export PDF/CSV × 3 BPMNs + acentos + gradiente de color). El test de gradiente verifica con `sharp` que el heatmap del PDF contiene píxeles verdes, ámbar y rojos — evita regresiones en la lógica de coloreo. --- ## Convenciones de este changelog - **Decisiones de diseño**: se documenta el *por qué*, no solo el *qué* - **Gotchas**: comportamientos contrarios a la intuición se marcan explícitamente - **Edge cases**: si un edge case motivó un test específico, se menciona - **Retrocompatibilidad**: cuando un campo es opcional por retrocompat, se indica - Las entradas son para humanos que modificarán el código, no para usuarios finales