Files
inq-roi-simulador-web/CHANGELOG.md

263 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 14 completas; Etapas 56 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 `<Separator>`
- 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<bpmnElementId>` 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 `<bpmn:process>` hijos directos de `<definitions>` (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