16 KiB
Changelog
Historial de cambios del proyecto. Formato basado en Keep a Changelog. 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):
Activitygana:automatable: boolean,automatedCostFixed: number,automatedTimeMinutes: numberProcessgana:annualFrequency: number,analysisHorizonYears: number,automationInvestment: number- Nuevo tipo
Scenario = 'actual' | 'automated' SimulationInputaceptascenario?: Scenario(opcional, default'actual'para retrocompatibilidad)SimulationganaresultAutomated?: 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
- Activities:
- 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 = Infinitycuando no hay ahorro,roiAnnualPercent = Infinitycuando inversión=0 y ahorro>0
Motor de simulación dual (src/domain/simulation.ts):
- Acepta
scenarioenSimulationInput - En
'automated': actividades conautomatable=trueusanautomatedCostFixed/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 enactivitiesocurrentProcessinvalida ambos resultados automáticamente - Dependencia intencional documentada:
simulation-storeimportaprocess-storeen una dirección (sin inversa → sin ciclo)
useSimulate refactorizado (src/features/simulation/useSimulate.ts):
- Corre las dos simulaciones en el mismo ciclo y llama
persistResultsdel store (eliminando el import directo desimulationRepodesde 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 importarpersistence/repositoriesdesde fuera destore/- Overrides para
store/**,features/import/**,features/report/useReportData.ts,tests/**
- Overrides para
- 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, NaNtests/integration/dexie-migration.test.ts: 6 casos de persistencia de los nuevos campos con valores default y no-defaulttests/store/simulation-store-invalidation.test.ts: 7 casos de la invariante de invalidación dualtests/integration/sprint1-flow.test.ts: 11 casos del flujo integrado BPMN → sim actual → sim automated → calculateRoi → KPIstests/domain/simulation.test.ts: +4 casos descenario=automatedcon costos en cero, sin NaN, probabilidad invariantetests/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.tstests/integration/gateway-validation.test.tstests/integration/indexeddb-persistence.test.tstests/features/report/components.test.tsxtests/features/report/derivations.test.tstests/lib/export/measure-sizes.test.tstests/lib/export/csv-export.test.tstests/lib/export/pdf-export.test.tstests/setup.ts: mocks globalesResizeObserveryPointerEventpara 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-labelpara accesibilidad- Transiciones CSS:
transition-colors duration-200en el fondo,transition-transform duration-200en el knob - Decisión: no instalar
@radix-ui/react-switchpara 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 contransition-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
- Limitación conocida: el max-height trick no anima a la altura real del contenido. Ver TODO.md para mejora con
- Inputs numéricos
automatedCostFixedyautomatedTimeMinutescon 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-hiddenrefleja 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
- Aparece cuando
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 helperanalysisHorizonYears: 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 enhandleSave, no enonChange
BpmnCanvas ampliado (src/features/workspace/BpmnCanvas.tsx):
- Nueva prop
automatableElementIds?: string[] syncAutomationOverlays(): callback memoizado conuseCallback([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 unuseEffectseparado cuando cambia la lista
- Limpia overlays anteriores con
- Posicionamiento del badge:
{ top: -8, left: element.width - 12 }- Obtenido via
elementRegistry.get(elementId)con fallbackwidth ?? 100 - No usar
position.rightde diagram-js: conright: -8, la fórmula internaleft = right * -1 + widthdaleft = 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
- Obtenido via
- Badge HTML: círculo 20×20px, fondo
#fffbeb(amber-50), borde#e2e8f0, sombra0 1px 3px rgba(0,0,0,0.12), ícono Bot Lucide en#f59e0bcomo SVG inlinepointer-events: nonepara no interferir con clicks en el nodotitlenativo del browser como tooltip- Por qué
#fffbeby 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
activitiesdel process-store - Calcula
automatableElementIdsconuseMemo - 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 muestraNeedsResimulateque guía al usuario a re-simular useMemoparaautomatableIdsyroise 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.resultAutomatedcomo fuente de datos AutomationScenarioNoteaparece 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 entext-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 deautomatableen 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=trueperoautomatedCostFixed=0yautomatedTimeMinutes=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 conaria-hidden, dispatch deupdateActivitytests/features/workspace/GlobalSettingsPanel.test.tsx: 7 casos — defaults, edición de campos, dispatch deupdateProcesstests/features/workspace/BpmnCanvas.test.tsx: 4 casos — overlay add/remove, posiciónleft=width-12, HTML con#fffbeby#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 auseProcessStore()devuelve un nuevo objeto → eluseEffect([activities])de ActivityPanel dispara en loop infinito → "Maximum update depth exceeded" simulation-store.tsimportaprocess-store.tsal 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
querySelectorque 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
importXMLpero el diagrama se renderiza parcialmente → usareventBus.on('import.done', ...)en lugar de.then() element.style.fill = color(NOsetAttribute('fill', color)): bpmn-js aplica CSS que tiene mayor especificidad que atributos SVGcaptureImage()espera 500ms después de importar para que la transición CSSfill 0.35s easecomplete antes de la captura con html2canvas
Validación XOR en WorkspacePage (WorkspacePage.tsx):
hasLoadedOnceref: evita que el redirect "proceso no encontrado" dispare en el render inicial dondeisLoading=falseycurrentProcess=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