Fecha: 2026-02-24
Decisión: Usar Next.js 16 (última versión) con App Router y directorio src/.
Razón: Estándar moderno, soporte nativo de Server Components, compatible con Vercel.
Fecha: 2026-02-24
Decisión: Tailwind CSS 4 con @tailwindcss/postcss plugin.
Razón: v4 simplifica configuración, no requiere tailwind.config.js.
Fecha: 2026-02-24
Decisión: ESLint 9 con flat config (eslint.config.mjs) y eslint-config-next.
Razón: Next.js 16 eliminó el comando next lint; usamos eslint . directamente.
Fecha: 2026-02-24
Decisión: Usar @supabase/ssr con middleware para manejo de sesiones.
Razón: Patrón oficial de Supabase para Next.js App Router. Tres clientes: browser, server, middleware.
Fecha: 2026-02-24 Decisión: Zod como librería única de validación (inputs, schemas IA, API responses). Razón: Type-safe, composable, excelente DX con TypeScript.
Fecha: 2026-02-25 Decisión: Usar email+password como método principal de autenticación. Razón: Más simple para MVP, no requiere configurar SMTP. Magic link se puede agregar después.
Fecha: 2026-02-25
Decisión: Trigger en auth.users que crea automáticamente un registro en profiles.
Razón: Garantiza que todo usuario tiene profile. El rol default es municipio_user; admins asignan roles manualmente.
Fecha: 2026-02-25
Decisión: Funciones auth_user_role(), auth_user_tenant_id(), auth_user_municipio_id() con SECURITY DEFINER.
Razón: Simplifica policies RLS. SECURITY DEFINER permite acceder a profiles desde policies sin recursión.
Fecha: 2026-02-25
Decisión: Guardar la plantilla MGA como etapas_json JSONB en mga_templates (una fila por convocatoria).
Razón: La estructura de etapas/campos varía por convocatoria. JSONB permite flexibilidad total. Validamos con Zod en la app. Se puede migrar a tablas normalizadas si hay queries complejas.
Fecha: 2026-02-25
Decisión: El editor de plantilla MGA es un client component ("use client") con estado local y save explícito.
Razón: Manipulación interactiva de arrays anidados (etapas > campos) requiere estado mutable. Server actions no son prácticas para edición in-place. El save invoca un server action para persistir.
Fecha: 2026-02-25
Decisión: Al crear una convocatoria, se inserta automáticamente un mga_templates con etapas_json: [].
Razón: Simplifica la relación 1:1 (convocatoria siempre tiene template). El entidad_admin lo llena después.
Fecha: 2026-02-25
Decisión: data_json en submissions es un objeto plano { campo_id: string_value } sin estructura por etapa.
Razón: Simplifica lectura/escritura. Los campos tienen IDs únicos globales. El progreso se calcula cruzando con la plantilla MGA.
Fecha: 2026-02-25
Decisión: Autosave usa estado React (pendingFields) en vez de refs, con debounce de 1.5 segundos vía useEffect.
Razón: ESLint React Compiler no permite acceso a refs durante render. El patrón state-based es igual de efectivo y pasa lint.
Fecha: 2026-02-25
Decisión: Un trigger PostgreSQL sincroniza submissions.progress a convocatoria_municipios.progress automáticamente.
Razón: La entidad consulta convocatoria_municipios para ver avance. Sincronizar vía trigger evita queries adicionales y mantiene consistencia.
Fecha: 2026-02-25
Decisión: Usar patrón adapter con interfaz LlmAdapter y factory createLlmAdapter() que selecciona OpenAI o Anthropic según env var LLM_PROVIDER.
Razón: Permite cambiar de proveedor LLM sin modificar código de negocio. OpenAI usa SDK oficial; Anthropic usa fetch directo para evitar dependencia adicional.
Fecha: 2026-02-25
Decisión: Rate limiting simple: contar registros en audit_logs del usuario en el último minuto (máx 10).
Razón: No requiere infraestructura adicional (Redis, etc.). Suficiente para MVP. La tabla audit_logs ya existe para trazabilidad.
Fecha: 2026-02-25
Decisión: Si el LLM devuelve JSON que no pasa Zod, intentar extraer campos individuales. Si devuelve texto plano, envolverlo como suggested_text.
Razón: Los LLMs no siempre respetan el schema exacto. Es mejor mostrar algo útil que fallar completamente.
Fecha: 2026-02-25
Decisión: El botón "Asistente IA" solo aparece en campos de tipo textarea y text, no en number o date.
Razón: Solo los campos de texto libre se benefician de sugerencias narrativas del LLM.
Fecha: 2026-02-25
Decisión: Usar pgvector con HNSW index (vector_cosine_ops, m=16, ef_construction=64) para búsqueda de similaridad en embeddings.
Razón: HNSW ofrece buen balance entre velocidad y recall. Mejor que IVFFlat para datasets que crecen sin necesidad de re-training.
Fecha: 2026-02-25
Decisión: Tanto documents como embeddings llevan convocatoria_id. La función match_embeddings filtra por convocatoria_id antes de hacer similarity search.
Razón: Multi-tenancy a nivel de datos. Un municipio no debe ver documentos de otra convocatoria. RLS adicional por rol.
Fecha: 2026-02-25
Decisión: Usar modelo text-embedding-3-small de OpenAI para generar embeddings de 1536 dimensiones.
Razón: Buen balance costo/calidad para MVP. Compatible con pgvector. Se puede migrar a modelos más grandes si se necesita mejor recall.
Fecha: 2026-02-25 Decisión: Chunks de ~500 tokens (~2000 chars) con overlap de ~50 tokens (~200 chars). Breakpoints preferidos en fin de oración o salto de línea. Razón: Simple y efectivo para MVP. El overlap evita perder contexto entre chunks. Los breakpoints semánticos mejoran la coherencia de cada chunk.
Fecha: 2026-02-25
Decisión: Usar pdf-parse v1.1.1 (no v2) con await import("pdf-parse") dinámico.
Razón: pdf-parse v2 tiene API completamente diferente (clase PDFParse). v1 tiene la API simple pdfParse(buffer). Se importa dinámicamente porque v1 intenta cargar un archivo test al importar estáticamente.
Fecha: 2026-02-25 Decisión: Si el RAG falla (no hay embeddings, error de API), el asistente IA continúa sin contexto de documentos. Razón: El asistente debe funcionar siempre, con o sin documentos. RAG enriquece pero no es requisito.
Fecha: 2026-02-25
Decisión: Las server actions de documentos hacen redirect(url?error=msg) en vez de retornar { error }.
Razón: Next.js form action espera void | Promise<void>. Retornar objetos no es compatible con el tipo action. Redirect con query param es el patrón compatible.
Fecha: 2026-02-25 Decisión: Una rúbrica por convocatoria (UNIQUE), criterios como JSONB array. Cada criterio tiene campo_id, peso, descripción y niveles de evaluación (score 1-4). Razón: Mismo patrón que mga_templates. Flexible, se valida en la app. La relación campo_id → campo MGA permite evaluar campos específicos.
Fecha: 2026-02-25 Decisión: Evaluaciones se generan por etapa (no por submission completa). Por cada criterio de la etapa, el LLM evalúa la respuesta del municipio y asigna un score según los niveles de la rúbrica. Razón: Granularidad por etapa permite evaluaciones parciales. El municipio puede ver feedback incremental. El score final es un promedio ponderado normalizado a 100 puntos.
Fecha: 2026-02-25
Decisión: Un constraint UNIQUE en evaluations(submission_id, etapa_id) permite re-evaluar una etapa sobreescribiendo la evaluación anterior.
Razón: Las evaluaciones se pueden repetir a medida que el municipio mejora sus respuestas. Solo se mantiene la evaluación más reciente.