CalificaYa es una aplicación web para configurar, corregir y exportar resultados de exámenes, con OCR y sugerencia de calificación asistida por IA.
- Arquitectura del sistema
- UX de importación de respuestas
- Modelo de datos
- Seguridad
- ADRs (Architecture Decision Records)
- Roadmap de mejoras
- Reporte de accesibilidad del workflow
Consulta la guía de contribución en CONTRIBUTING.md.
- Node.js 20+
- npm 9+
npm installinstala dependencias.npm run devejecuta el frontend (Vite).npm run dev:apiejecuta el backend local (backend/server.js).npm run buildconstruye la versión de producción.npm run db:migrateaplica migraciones de base de datos (backend/db/runMigrations.js).npm run db:seedcarga un dataset mínimo determinístico e idempotente para desarrollo (backend/db/seed.js).npm run previewprevisualiza el build.npm run lintvalida reglas de ESLint para React + Vite.npm run testejecuta la suite de pruebas con Vitest (sin cobertura, útil para iteración local).npm run test:ciejecuta Vitest con cobertura (--coverage) y aplica umbrales globales y por archivo.npm run test:watchejecuta pruebas en modo observación local.npm run formataplica formateo con Prettier.npm run test:e2eejecuta pruebas end-to-end con Playwright sobre build local.npm run test:e2e:uiabre el runner UI de Playwright para depuración local.
Para preparar rápidamente un entorno con datos de ejemplo reproducibles:
npm run db:migratenpm run db:seed
El seed carga un dataset mínimo sobre schools, groups, exams, students, submissions, grades, reports y audit_logs, usando IDs seed_* y validación por conteos esperados para detectar cargas incompletas.
Flujo recomendado antes de abrir PR:
npm installnpm run lintnpm run test:ci(incluye quality gate de cobertura para módulos críticos)npm run buildnpm run test:e2e
backend/
├── server.js # API interna /api/calificacion/sugerir
└── server.test.js # Pruebas Vitest del contrato HTTP y errores de proveedor
src/
├── App.jsx # Orquestador del flujo y composición de UI
├── components/
│ ├── StepIndicator.jsx
│ └── TopBar.jsx
├── features/
│ └── exam-workflow/
│ ├── StepConfiguracion.jsx
│ ├── StepIngresoRespuestas.jsx
│ ├── StepReporteFinal.jsx
│ └── StepRevision.jsx
├── hooks/
│ ├── useExamWorkflow.js
│ └── useReportes.js
├── services/
│ ├── aiService.js # Cliente del endpoint interno /api/calificacion/sugerir
│ ├── exportService.js # Exportación PDF/CSV
│ ├── importService.js # Importación y validación CSV/Excel
│ ├── ocrService.js # OCR con Tesseract
│ └── storageService.js # localStorage (decisión final, reportes)
├── utils/
│ └── examUtils.js
├── main.jsx
└── styles.css
- Inicio: landing page con acceso rápido al flujo de corrección.
- Corrector: mantiene el workflow guiado de 4 pasos (configuración, ingreso, revisión, reporte/exportación).
- Organización: explica y visualiza la agrupación de reportes en “carpetas lógicas” por materia (
materiaFolderId), útil para separar historial y exportaciones por asignatura.
- Cada reporte guarda metadatos de organización (
organizacion.materiaNormalizadayorganizacion.materiaFolderId). - La agrupación es tolerante a variaciones de escritura (mayúsculas/acentos) gracias a la normalización de materia.
- Recomendación operativa: definir un catálogo estable de nombres de materias para evitar carpetas duplicadas por variantes de texto.
- Configuración del examen (materia, grupo, fecha, total de preguntas, clave).
- Ingreso de respuestas por transcripción manual, carga de lote CSV/Excel o foto/escaneo.
- Procesamiento OCR en cliente con Tesseract.js y parser por número de pregunta con tolerancia a formatos manuscritos/ruidosos, segunda pasada por ventanas de tokens y resolución de duplicados por confianza.
- Revisión editable de respuestas en tabla antes de calificar.
- Revisión de calificaciones (aciertos, errores, porcentaje y puntaje) con desglose por pregunta basado en evidencia (clave, respuesta, estado, confianza OCR y fuente).
- Sugerencia de calificación con proveedor de IA configurable (
anthropicuopenai) usando prompt interno en español con criterios contables. - Reporte final con decisión final editable de la profesora y exportación simulada.
- Campos obligatorios por fila:
estudianteNombre,estudianteMatricula,respuestas. - Normalización de respuestas: mayúsculas y tolerancia OCR (
4→A,8→B,(→C,0/O/Q→D). - Política de duplicados: para matrícula repetida en el mismo archivo se conserva la última fila válida.
- Límite de archivo: 2MB.
- Máximo de filas por importación: configurable con
VITE_IMPORT_MAX_ROWSowindow.__APP_CONFIG__.import.maxRows(fallback seguro: 200). - Formatos soportados: CSV UTF-8 (
.csv) y Excel (.xlsxOpenXML y.xlsSpreadsheetML/XML 2003). - Validaciones homogéneas entre CSV y Excel: encabezados con alias (
nombre/matrícula/respuesta), contrato por fila (REQUIRED_FIELDS+validarSchema), deduplicación por matrícula y límites de tamaño/filas. - El paso de ingreso permite previsualizar filas válidas, revisar errores por registro y cargar una fila al formulario para corrección manual antes de guardar.
El sistema usa un contrato explícito y reutilizable para cada ítem del desglose (puntuacionPorPregunta[*].desglose), tanto en UI, persistencia y exportación:
criterioAplicado: regla de evaluación usada en la comparación.evidencia: datos trazables de la corrección:claverespuestaEstudianteestado(correcta|incorrecta)confianzaOCRfuente
resultado: conclusión del ítem (incluye impacto en puntaje).recomendacion: acción pedagógica o de verificación sugerida.
En Revisión de calificaciones, el razonamiento se visualiza en un panel expandible por fila. En exportación PDF/CSV, estos mismos campos se incluyen por pregunta.
- El frontend siempre llama
POST /api/calificacion/sugerir. - El backend usa patrón provider/strategy con selector
AI_PROVIDER:anthropic→https://api.anthropic.com/v1/messagesopenai→https://api.openai.com/v1/chat/completions
- Contrato normalizado de respuesta al frontend (estable):
puntuacionjustificacionproveedormodelo
- La UI también consulta
GET /api/calificacion/proveedorpara mostrar proveedor/modelo activos.
- Backend primario: la persistencia de reportes usa
GET/POST /api/reportescomo fuente de verdad en ejecución. - Fallback controlado:
localStoragese usa solo en modo degradado (offline/desarrollo o caída del backend), no como ruta primaria. - Decisión final editable: la puntuación/justificación docente se conserva para edición y forma parte del objeto de reporte persistido.
- Fuente de comportamiento en runtime: el flujo está implementado en
src/hooks/useReportes.js(estrategia backend-first + fallback) ysrc/services/storageService.js(cliente API y almacenamiento local de respaldo).
Para QA y soporte: si /api/reportes no está disponible o responde error de conectividad, la app activa automáticamente persistencia local de respaldo (localStorage). En ese estado, el historial refleja datos locales del navegador actual; al restablecer backend/sesión, el flujo vuelve a priorizar API.
La UI muestra errores claros en español para:
- API key inválida o sin permisos (
401/authentication_error). - Límite de cuota/tasa (
429/rate_limit_error). - Timeout de red (cancelación tras 20 segundos).
- Respuestas de IA mal formadas o con puntuación fuera de rango.
- Configuración del examen.
- Ingreso de respuestas (manual u OCR).
- Scoring automático.
- Sugerencia IA vía backend propio.
- Ajuste docente final con guardado explícito del reporte.
- Exportación individual (PDF/CSV) desde reporte guardado o snapshot actual, sin duplicar historial.
- Historial de reportes con filtros (hidratado desde backend cuando está disponible).
Para mejorar la precisión del OCR cuando se corrige por imagen:
- Iluminación uniforme: usa luz frontal, evita contraluces y sombras sobre la hoja.
- Enfoque nítido: espera que la cámara enfoque antes de disparar; si queda borrosa, repite la captura.
- Encuadre completo: incluye la hoja entera en formato vertical, sin cortar márgenes ni números de pregunta.
- Resolución suficiente: prefiere fotos de al menos 900x1200 píxeles para lectura estable.
- Evita panorámicas o recortes extremos: el OCR funciona mejor cuando la hoja ocupa la mayor parte del encuadre.
- Preprocesado configurable:
src/services/ocrService.jsaplica escala de grises, contraste y umbral local antes de Tesseract; puedes ajustar constantes de preprocesado al inicio del archivo según tipo de hoja/cámara.
La cobertura se genera con provider v8 y reportes text, lcov y html en la carpeta coverage/.
Umbrales vigentes:
- Globales: Statements 70%, Branches 60%, Functions 70%, Lines 70%.
- Módulos críticos (por archivo, quality gate):
backend/server.js,backend/ai/providerOrchestrator.js,src/hooks/useReportes.js,src/services/aiService.jscon mínimo 80% en Statements, Functions y Lines (Branches mantiene el umbral global del proyecto).
Si cualquier umbral no se cumple, npm run test:ci falla y el workflow de CI marca el job como fallido.
Cómo interpretar los reportes:
- text: resumen inmediato en consola para feedback rápido.
- lcov (
coverage/lcov.info): formato estándar para integraciones con herramientas de calidad. - html (
coverage/index.html): vista navegable para identificar archivos/líneas sin cubrir.
AI_PROVIDER(opcional, defaultanthropic): proveedor activo (anthropicuopenai).ANTHROPIC_API_KEY(obligatoria siAI_PROVIDER=anthropic).ANTHROPIC_MODEL(opcional, defaultclaude-sonnet-4-20250514).OPENAI_API_KEY(obligatoria siAI_PROVIDER=openai).OPENAI_MODEL(opcional, defaultgpt-4o-mini).AI_REQUEST_TIMEOUT_MS(opcional, default20000).INTERNAL_AUTH_TOKEN(obligatoria): token compartido esperado en header interno para protegerPOST /api/calificacion/sugerir.INTERNAL_AUTH_HEADER(opcional, defaultx-internal-token): nombre del header donde se envía el token interno.RATE_LIMIT_WINDOW_MS(opcional, default60000): ventana temporal de rate limit en milisegundos.RATE_LIMIT_MAX_REQUESTS(opcional, default20): máximo de solicitudes permitidas por ventana.RATE_LIMIT_KEY_STRATEGY(opcional, defaultauthenticated_or_token_or_ip): estrategia de partición para rate limit (authenticated_or_token_or_ip,authenticated,token,ip).- Semántica: si existe identidad autenticada se usa
tenantId + userId; si no, se hace fallback a token interno y luego IP.
- Semántica: si existe identidad autenticada se usa
PORT(opcional, por defecto8787).
- Inicia backend en una terminal:
AI_PROVIDER=anthropic ANTHROPIC_API_KEY=tu_key INTERNAL_AUTH_TOKEN=token_interno_seguro npm run dev:api- Inicia frontend en otra terminal:
npm run devVite proxy redirige
/api/*ahttp://localhost:8787.
Ejemplo con OpenAI:
AI_PROVIDER=openai OPENAI_API_KEY=tu_key OPENAI_MODEL=gpt-4o-mini INTERNAL_AUTH_TOKEN=token_interno_seguro npm run dev:apiLa carpeta e2e/ contiene el caso feliz completo del flujo:
- Configuración del examen.
- Ingreso de respuestas.
- Solicitud de sugerencia IA (mock backend).
- Ajuste de decisión final.
- Guardado de reporte.
- Verificación de aparición en historial.
Para evitar dependencia de proveedores reales, las pruebas interceptan:
GET /api/calificacion/proveedorPOST /api/calificacion/sugerirGET/POST /api/reportes
con fixtures determinísticos ubicados en e2e/fixtures/mockData.js.
npm install
npm run build
npm run test:e2eEl workflow .github/workflows/ci.yml ejecuta en orden:
lint- tests unit/integration con cobertura (
npm run test:ci) - E2E (
npm run test:e2e)
El job E2E instala Chromium vía Playwright y publica el reporte HTML como artifact.