# TECH_DEBT — InQ ROI > Deuda técnica, refactors postergados y decisiones arquitectónicas pendientes. > Requiere consulta entre director y técnico para resolver. > No incluye features de producto (ver BACKLOG.md) ni hallazgos de sprint actual (ver OBSERVATIONS.md). --- ## Convención de etiquetas - `[SPRINT-2]` — Comprometido para resolución en el próximo sprint - `[BACKLOG]` — Disponible para asignar - `[DEFERRED]` — Pospuesto, requiere decisión arquitectónica - `[INVESTIGATING]` — Necesita análisis técnico antes de planear Cada ítem incluye: - Estado actual del problema - Causa raíz hipotetizada - Impacto si no se resuelve - Solución propuesta - Archivos afectados (cuando aplica) --- ## Deuda crítica — afecta calidad del entregable ### [RESOLVED] Truncamiento de líneas largas en notas metodológicas **Estado actual:** los caracteres Unicode rotos (Σ → £, − → ") fueron resueltos en Etapa 8 mediante reescritura a ASCII puro (SUM, x, -). Decisión arquitectónica: la notación ASCII se mantiene como solución permanente porque alinea con terminología financiera/Excel. **Pendiente:** verificar si todavía hay líneas largas que se truncan en margen derecho cuando una fórmula excede el ancho de página portrait. **Acción:** test E2E que valide ancho de cada línea de notas metodológicas en los 3 PDFs. Si hay truncamiento, evaluar: - Opción A: usar `doc.splitTextToSize()` para wrap automático - Opción B: reformular fórmulas problemáticas en múltiples líneas en el texto fuente **Resultado:** auditado en Sprint 2 Etapa 5-6. Dos líneas variables corregidas con `splitTextToSize` (`topResource.resourceName` en `drawAnalysisSection` y top3 en análisis ROI). Resto sin riesgo confirmado. 7 tests nuevos cubren el comportamiento. **Sprint cerrado en:** Sprint 2 Etapas 5-6. --- ## Deuda media — afecta DX o evolución del producto ### [RESOLVED] Clave de localStorage del auth E2E vs producción **Resultado:** la "contradicción" era un bug real, no solo una discrepancia de documentación. `tests/e2e/auth-localStorage-key.spec.ts` (Sprint 6 Etapa 3) confirmó empíricamente que la única clave presente en `localStorage` con sesión real es `sb-${projectRef}-auth-token` (`sb-xgqbkxcliyjzisoccsur-auth-token` en este proyecto) — `'supabase.auth.token'` (STORAGE_KEY default de `@supabase/auth-js`'s `GoTrueClient` en aislamiento) **no existe**, porque `@supabase/supabase-js`'s `createClient()` sobrescribe ese default calculando la clave `sb-`-prefijada desde la URL del proyecto. `ProfileSheet.getAccessToken()` leía `'supabase.auth.token'` — la clave vieja, inexistente. Esto significaba que **el guardado de perfil (nombre, empresa) nunca llegaba a Supabase**: `getAccessToken()` retornaba `null`, el `if (!token) return` silenciaba el error, y el usuario veía el cambio solo en memoria (`updateUser()`) sin persistir. Bug real, no solo deuda de documentación. **Fix:** `getAccessToken()` ahora calcula `sb-${projectRef}-auth-token` igual que `auth-setup.ts`. Verificado end-to-end: PATCH a `/rest/v1/users` devuelve 204, y tras un reload completo de página el valor guardado se relee correctamente desde la DB. **Sprint cerrado en:** Sprint 6 Etapa 3. ### [BACKLOG] Guards de rol via `useEffect` en lugar de `beforeLoad` (3 componentes) **Estado actual:** `AdminLayout`, `WorkspacePage`, e `ImportPage` usan `useEffect` para detectar el rol y redirigir. El componente monta brevemente antes del redirect — posible flash de contenido. **Causa raíz:** el pattern surgió de prompts que describieron el guard en términos de comportamiento de componente ("si el rol no es X, redirigir") en lugar de especificar la sección de router. **Impacto:** no es bug de seguridad (RLS protege los datos en Supabase), pero es anti-patrón para TanStack Router y puede causar flash visual breve en conexiones lentas. **Solución propuesta:** migrar a `beforeLoad` de TanStack Router. Ejemplo: `beforeLoad: ({ context }) => { if (context.auth.user?.platformRole !== 'platform_admin') throw redirect({ to: '/library' }) }`. **Archivos afectados:** `src/features/admin/AdminLayout.tsx`, `src/features/workspace/WorkspacePage.tsx`, `src/features/import/ImportPage.tsx` (aprox. línea 65-70 en WorkspacePage). **Aprendizaje de método:** en prompts futuros que requieran guards de rol, especificar siempre el código exacto de `beforeLoad` en la sección de router — no solo el comportamiento esperado del componente. **Origen:** Sprint 7 Etapa 5 — patrón recurrente detectado en 3 componentes. ### [BACKLOG] Doble suscripción `onAuthStateChange` en AuthContext **Estado actual:** `AuthContext.tsx` tiene dos listeners de `onAuthStateChange`: uno principal (SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, INITIAL_SESSION) y uno secundario para `PASSWORD_RECOVERY`. El secundario fue implementado para evitar importación circular entre `AuthContext` y `SupabaseAuthService`. **Causa raíz:** `PASSWORD_RECOVERY` necesita llamar a `supabase.auth.updateUser()` directamente desde el contexto — si se delega a `SupabaseAuthService`, este importa desde `AuthContext` creando un ciclo. **Impacto:** ambos listeners se registran en cada mount del `AuthProvider`. En la mayoría de los casos no genera bugs, pero el patrón es frágil y puede causar doble-dispatch de eventos en casos edge de remount. **Solución propuesta:** refactorizar para un único switch/case en el listener principal. Resolver la importación circular moviendo `supabase` client a un módulo compartido sin dependencia de AuthContext. **Archivos afectados:** `src/auth/AuthContext.tsx`. **Origen:** Sprint 7 Etapa 3 — documentado en OBSERVATIONS.md. ### [BACKLOG] `getCurrentUser()` en SupabaseAuthService — sin callers activos **Estado actual:** `SupabaseAuthService.getCurrentUser()` usa `getSession()` directamente (sin timeout, sin processLock workaround). Es la única excepción documentada a la regla — justificada porque necesita el objeto sesión completo, no solo una barrera. Sin callers activos hoy. **Impacto si no se resuelve:** ninguno mientras no tenga callers. Si en el futuro algún flujo llama `getCurrentUser()`, podría colgar el processLock si el servidor no responde. **Solución propuesta:** cuando adquiera callers reales, evaluar si `supabase.auth.getUser()` (token de localStorage, sin lock) es suficiente para ese caso. **Archivos afectados:** `src/auth/SupabaseAuthService.ts` (~línea 27, comentario explicativo presente). **Origen:** OBSERVATIONS Sprint 5 Etapa 1 — ítem prometido que faltaba registrar acá. ### [RESOLVED] `setTimeout(350)` en ProfileSheet — candidato a limpieza **Resultado:** `setTimeout` eliminado — `updateUser()` se llama directamente tras `onOpenChange(false)`. Verificado end-to-end (Sprint 6 Etapa 3): el fix definitivo en `src/components/ui/sheet.tsx` (`useEffect` sobre el prop `open`, limpieza defensiva de `pointer-events`) es suficiente por sí solo — `document.body.style.pointerEvents` queda vacío tras guardar, y la navegación posterior (click en "Importar BPMN") funciona sin bloqueos. **Sprint cerrado en:** Sprint 6 Etapa 3. ### [RESOLVED] E2E specs con Dexie directo — reescribir para Supabase **Resultado:** `export-pdf.spec.ts` (test "Heatmap con gradiente real") y `validate-dod-etapa4.spec.ts` reescritos para inyectar datos vía `@supabase/supabase-js` con `SUPABASE_SERVICE_ROLE_KEY` (bypasa RLS), en lugar de IndexedDB directo. `afterAll` borra el proceso creado por cada test (cascada en `activities`/`simulations`). Si `SUPABASE_SERVICE_ROLE_KEY` no está configurada en `.env.test`, ambos tests hacen `test.skip()` con mensaje descriptivo en vez de fallar — ver `.env.example`. Diagnóstico previo de `auth-setup.ts` confirmó que la storageKey (`sb-${projectRef}-auth-token`) ya era correcta (no era parte del problema): `@supabase/supabase-js`'s `createClient()` sobrescribe el `STORAGE_KEY` default de `auth-js` con esa clave calculada desde la URL. **Sprint cerrado en:** Sprint 5 Etapa 6. ### [RESOLVED] E2E flakiness — "Guardar cambios" button timing en ActivityPanel **Resultado:** agregado `data-testid="save-activity-btn"` al botón en `ActivityPanel.tsx`. Specs afectados (`payback-etapa-2.spec.ts`, `export-roi.spec.ts`) reemplazan el click directo por `waitForSelector(':not([disabled])')` → click → `waitForSelector('[disabled]')` (confirma que el guardado se procesó). Confirmado estable en 3 corridas consecutivas sin fallos. `GlobalSettingsPanel.tsx` tiene el mismo patrón `isDirty`/disabled en su botón "Guardar configuración" pero no se tocó — fuera del scope de Etapa 6; queda como ítem nuevo abajo. **Sprint cerrado en:** Sprint 5 Etapa 6. ### [BACKLOG] GlobalSettingsPanel — mismo patrón de flakiness potencial que ActivityPanel **Estado actual:** `GlobalSettingsPanel.tsx` tiene un botón "Guardar configuración" con el mismo guard `disabled={!isDirty}` que `ActivityPanel.tsx` tenía antes de Etapa 6. La función `configureGlobal()` en `payback-etapa-2.spec.ts` y `export-roi.spec.ts` sigue usando `getByRole('button', { name: /Guardar/i }).click()` sin esperar a que se habilite — el mismo riesgo de flakiness, no confirmado como activo todavía. **Impacto:** potencial, no confirmado. Estos specs pasaron 3/3 en Etapa 6 sin tocar esta función. **Solución propuesta:** agregar `data-testid="save-global-btn"` a `GlobalSettingsPanel.tsx` y aplicar el mismo patrón de `waitForSelector` que se usó para `save-activity-btn`. **Costo:** bajo. Mismo patrón ya validado. **Origen:** Sprint 5 Etapa 6 — detectado durante el diagnóstico, explícitamente fuera del scope del prompt (solo `ActivityPanel.tsx` listado en archivos a modificar). ### [BACKLOG] 9 specs adicionales con el mismo patrón flaky de "Guardar" sin data-testid **Estado actual:** además de los 2 specs corregidos en Etapa 6, otros 9 archivos (`sprint-1-5-final.spec.ts`, `screenshots-etapa-3.spec.ts`, `etapa-4-pdfs.spec.ts` a `etapa-9-badge.spec.ts`, `etapa-11-polish.spec.ts`) usan el mismo `getByRole('button', { name: /Guardar/i }).click()` sin esperar a `isDirty`. No confirmados como flaky activamente — fuera del scope explícito de Etapa 6 (que listaba solo 2 archivos a modificar). **Impacto:** riesgo de flakiness latente, no confirmado. **Solución propuesta:** aplicar el mismo patrón `waitForSelector('[data-testid="save-activity-btn"]:not([disabled])')` ahora que el testid existe en producción. **Costo:** bajo, mecánico — mismo cambio repetido 9 veces. **Origen:** Sprint 5 Etapa 6 — identificado durante el grep de diagnóstico, no aplicado por respeto al scope explícito del prompt. ### [BACKLOG] CumulativeSavingsChart — derivación de inversión base desde cumulativeSavingsHorizon **Estado actual:** cuando `roiBase` está presente (modo sensibilidad activo), el componente recupera la inversión original algebraicamente: `investment_base = roiBase.annualSavings * horizonYears - roiBase.cumulativeSavingsHorizon`. Es correcto mientras `horizonYears` del chart coincida con el usado en `calculateRoi` (hoy siempre es el mismo). **Riesgo:** si en el futuro el chart recibe un horizonte diferente al del cálculo de ROI, el valor de referencia del eje Y para la línea base será incorrecto, sin error visible. **Solución propuesta:** agregar prop `investmentBase?: number` a `CumulativeSavingsChartProps` y pasar `process.automationInvestment` directamente desde `ReportPage`. **Costo:** bajo. **Origen:** Sprint 6 Etapa 2 — detectado en auditoría post-implementación. ### [INVESTIGATING] Dexie live queries (sincronización store ↔ db) **Estado actual:** la invalidación del resultado de simulación se garantiza vía ESLint rule (`no-restricted-imports`) + convención: todas las mutaciones pasan por `useProcessStore`. **Riesgo a futuro:** si en Sprint 2 la grilla de edición masiva requiere escrituras de alta frecuencia directas a IndexedDB (por performance), la convención actual no escala. **Solución a evaluar:** migración a live queries de Dexie que invaliden el store automáticamente. **Acción pendiente:** análisis técnico de costo/beneficio cuando se planee la grilla de edición masiva. **Origen:** auditoría de Mayo 2026. ### [BACKLOG] Indicador visual en reporte de actividades automatable **Estado:** Sprint 1 usa solo leyenda de texto para diferenciar actividades automatables. **Acción:** evaluar si el cliente necesita más claridad visual (borde punteado o ícono diferenciador) cuando compara Actual vs Automatizado en reuniones. Decidir con feedback de uso real. ### [BACKLOG] Paleta del heatmap — saturado vs pastel **Estado:** los colores saturados actuales (`#10B981`, `#F59E0B`, `#EF4444`) funcionan bien en pantallas grandes de talleres InQuality. El brief de Sprint 1.5 propuso pasteles "más consultivos" (`#BBF7D0`, `#FED7AA`, `#FECACA`). **Decisión actual:** mantener saturados (Sprint 1.5 Etapa 1). **Impacto del cambio futuro:** requiere recalibrar tests E2E de análisis de píxeles (umbrales de verde/ámbar/rojo). **Acción:** decidir con feedback real de uso en talleres InQuality. Sprint 3-4. --- ## Refinamientos UX ### [BACKLOG] Mejorar animación de expansión de sección "Automatización" en ActivityPanel **Estado:** actualmente usa max-h trick (funciona, pero la aceleración no es lineal en los últimos píxeles). **Solución sofisticada:** `useRef + scrollHeight` para animar a la altura real del contenido. **Costo:** medio. **Beneficio:** micro-pulido visual. ### [RESOLVED] Label "Payback" superpuesto con eje Y en CumulativeSavingsChart (web) **Resultado:** offset ajustado con `padding: [0,0,0,8]` y `offset: [4,0]`. Sin colisión con eje Y. **Sprint cerrado en:** Sprint 2 Etapa 3. ### [RESOLVED] CumulativeSavingsChart — empezar en `-inversión`, no en 0 (web) **Resultado:** verificado en Sprint 2 Etapa 3 — la línea ya arrancaba en `-investment` (`roi.annualSavings * y - investment`, y=0 → `-investment`). Ítem cerrado sin cambio de código. **Sprint cerrado en:** Sprint 2 Etapa 3. --- ## Decisiones arquitectónicas tomadas en Sprint 1.5 (cerradas, archivadas) > Sección informativa. Estas decisiones ya están aplicadas en el código. Se mantienen acá como referencia. ### Subtítulo "Análisis de retorno de inversión" solo en página 1 ROI Decidido: página 1 tiene subtítulo descriptivo, páginas 2-4 no. Patrón estándar de reportes financieros (página 1 con título extendido). ### KPI cards en página 1 de PDFs Actual/Automatizado — color naranja Decidido: el KPI "Costo total" se migró a naranja durante refactor de Sprint 1.5. Antes era azul. Decisión retroactiva del audit. ### ComparisonTable — actividades no automatizables muestran em dash Decidido: en filas donde `isAutomatable === false`, mostrar "—" (em dash) en columnas Ahorro y % Ahorro en lugar de "USD 0,00" y "0,0%". Aplicado en Sprint 1.5. ### Versión "v0.1.0" en footer — fuera de PDFs, presente en web Decidido: la versión es útil para usuarios internos en web. En PDFs entregados a clientes genera dudas de estabilidad. Aplicado en Sprint 1.5. --- ### Anotaciones rápidas antes de pasar a Etapa 10 ### [RESOLVED] Tamaño del badge ⚡ — evaluar aumentar **Resultado:** evaluado en Sprint 2 Etapa 3 (16px) y ajustado a 20×20px en fix post-validación visual. Sin colisión en nodos de evento. Director aprobó visualmente. **Sprint cerrado en:** Sprint 2 Etapa 3 + fix. ### [INVESTIGATING] Toggle automatable + Guardar requerido para badge **Estado:** comportamiento intencional. Switch del ActivityPanel cambia visualmente, pero el badge en BpmnCanvas solo aparece después de "Guardar". **Decisión actual (Sprint 1.5):** mantener como está. Es UX deliberada. **Acción:** revisar en Sprint 2 con feedback real de uso en talleres. Si genera confusión recurrente, evaluar Opción B (indicador visual de "pendiente de guardar") o Opción C (auto-save). ### [RESOLVED] Badge ⚡ naranja en BpmnCanvas — Etapa 9 **Resultado:** badge rediseñado con identidad InQ, hex hardcodeados eliminados, 526 tests verdes. **Sprint cerrado en:** Sprint 1.5 Etapa 9. ### [RESOLVED] Refinamientos página 1 PDF ROI — Etapa 11 **Resultado:** KPI Hero 26pt, banner con bold naranja oscuro, grid 2×2 uniforme 9/18/9pt, Top 3 con título 11pt. Esquinas redondeadas via canvas clip con fondo blanco opaco. **Sprint cerrado en:** Sprint 1.5 Etapa 11.