| Estudiante | Edad | País | Escuela | Fecha | Estado | Acciones |
|---|---|---|---|---|---|---|
| Sin estudiantes | ||||||
| Nombre | Rol | Plan | Institución | Solicitado | Estado | Acciones | |
|---|---|---|---|---|---|---|---|
| Cargando... | |||||||
Parte A (secciones 1–7): cómo funciona el informe, cómo leerlo con un estudiante, cómo mantener el catálogo de carreras y cómo verificar el algoritmo antes de un piloto. Parte B (secciones 8–14): arquitectura, mapa del código, modelo de datos, flujos, convenciones y seguridad, para que un programador pueda seguir haciendo cambios. Caso de referencia: piloto de Bolivia (liceo de Potosí, 56 estudiantes).
Parte A — Funcionamiento y algoritmo
1. Qué mide el test y qué devuelve
El test es un tamizaje multidimensional. Mide cinco cosas: intereses (RIASEC/Holland, seis tipos: Realista, Investigador, Artístico, Social, Emprendedor, Convencional, con percentiles normados “pc”), fortalezas y confianza (ocho inteligencias múltiples de Gardner, autoreportadas), razonamiento (ejercicios de comprensión lectora, razonamiento, vocabulario y matemática, más matrices tipo Raven), personalidad (Big Five) y valores (qué busca el estudiante en un trabajo). Se completan además el torneo de carreras (elegir entre dos) y la “isla ideal”.
El informe devuelve un ranking de carreras (cuatro recomendadas y dos para investigar), un perfil narrativo, las gráficas de cada dimensión ordenadas de mayor a menor, el plan de 30 días, preguntas de reflexión, una sección para padres y una nota para el orientador. Todo el cálculo se hace en el navegador, de forma determinística: dos estudiantes con las mismas respuestas obtienen el mismo resultado. La función de IA está en el código pero no se usa hoy.
2. Cómo se calcula el ranking (versión v3, vigente desde el 08/09/2026)
Cada carrera del catálogo tiene dos vectores: un perfil RIASEC (seis valores de 1 a 5) y un perfil de inteligencias (ocho valores de 1 a 5). Para cada estudiante, el algoritmo compara su perfil con el de cada carrera y calcula un puntaje. Los pesos vigentes son:
| Componente | Peso | Qué hace |
|---|---|---|
| Intereses (RIASEC) | 55% | Normalizados dentro del propio estudiante (su tipo más alto vale 1, el más bajo 0), ponderados por el perfil RIASEC de la carrera. Es el predictor principal. |
| Fortalezas (inteligencias) | 20% | Igual lógica, con el perfil de inteligencias de la carrera. Si el estudiante marcó todo igual, este bloque se anula. |
| Personalidad (Big Five) | 10% | Responsabilidad, Apertura y Estabilidad emocional como moderador global. |
| Valores | 10% | Si los valores elegidos coinciden con los dos tipos RIASEC dominantes de la carrera, suma. |
| Razonamiento | ajuste fino ±4 pts | Ya no forma parte de la base. Suma o resta hasta 4 puntos al match de cada carrera según el desempeño cognitivo. |
Sobre esa base se aplican tres ajustes: un bono por el torneo de carreras (hasta +10 por carreras elegidas repetidamente), un bono por la isla ideal (+4 si el tipo dominante de la carrera coincide) y la regla de umbral matemático: solo las carreras de muy alta exigencia matemática (valor 5 en Lógico-matemática: ingenierías, ciencias exactas, informática, finanzas) se multiplican por 0,85 si el estudiante tuvo menos de 40% en matemática y por 0,72 si tuvo menos de 25%, con una advertencia visible en el informe. Hoy son 28 de las 105 carreras bolivianas; antes de la v3 eran 63.
El puntaje crudo se reescala a una escala de 35 a 96 y se comprime hacia el centro cuando los seis intereses quedaron muy parejos (menos de 15 puntos entre el más alto y el más bajo). Las etiquetas son: Máxima (82 o más), Alta (68 a 81), Media (54 a 67) y Explorable (menos de 54).
Dos reglas nuevas de la v3 sobre la lista final: en las cuatro carreras recomendadas hay como máximo dos de la misma área (evita que un perfil convencional vea cuatro variantes de Contaduría), y si la diferencia entre el interés más alto y el más bajo es menor a 12 puntos, el informe se marca como perfil plano: el resumen deja de afirmar una vocación, presenta las carreras como puntos de partida para explorar, y la nota al orientador lo dice explícitamente con el rango en puntos.
Qué esperar en un grupo: en una simulación de 60 perfiles aleatorios realistas, la primera carrera salió Alta en el 65% de los casos, Media en el 30% y Máxima en el 5%; en el conjunto de las cuatro recomendadas hubo también algunas Explorable. Si un grupo entero sale Alta o entero sale Media, algo está mal en los datos de entrada.
3. Catálogo de carreras por país
Si el estudiante tiene país Bolivia en su ficha, el ranking usa el catálogo boliviano (CATALOGO_BO): 105 carreras en tres niveles (74 licenciaturas universitarias, 20 técnico superior, 11 de las Escuelas Superiores de Formación de Maestros). Fuentes: oferta de UMSA, UAGRM y UPSA, Guía de Institutos Técnicos y Tecnológicos y convocatoria ESFM del Ministerio de Educación. Para cualquier otro país se usa el catálogo general. El campo país se carga en “Nuevo estudiante” y también lo completa el estudiante al registrarse; si queda vacío o “no especificado”, cae al catálogo general, así que conviene revisarlo antes de enviar los links.
Cada carrera boliviana se define en una fila [nombre, código RIASEC de 3 letras, área, nivel, descripción, entorno laboral]. El código RIASEC genera el vector de intereses (5/4/3 para las tres letras, 1 para el resto) y el área genera el vector de inteligencias base (doce áreas: salud, agro, ciencias, ing, info, empresa, admin, social, docencia, arte, turismo, oficio). Desde la v3 existe además la tabla AJUSTES_BO, que corrige por carrera cualquier valor de esos dos vectores. Ejemplo: 'Agropecuaria': {g:{LO:3,CN:5}} baja la exigencia matemática a 3 y sube lo corporal a 5; 'Ingeniería Electrónica': {r:{I:5,R:4}, g:{ES:3,CN:3}} la vuelve más investigadora que realista. Gracias a esa tabla, las 105 carreras tienen hoy perfiles distintos (antes solo había 56 perfiles para 105 carreras, con un grupo de 10 ingenierías idénticas).
Para agregar una carrera: sumar una fila a CARRERAS_BO_ROWS() con el código RIASEC y el área correctos y, si comparte código y área con otra ya existente, agregar una entrada en AJUSTES_BO que la diferencie. Para cambiar cómo se comporta una carrera (por ejemplo, si un orientador nota que Enfermería sale demasiado poco), tocar solo su entrada en AJUSTES_BO; no hace falta tocar la lógica. Las claves válidas son r con letras R, I, A, S, E, C y g con LI (lingüística), LO (lógico-matemática), ES (espacial), MU (musical), CN (corporal-kinestésica), IN (interpersonal), IP (intrapersonal), NA (naturalista), siempre con valores de 1 a 5. Recordar que LO=5 activa la regla matemática: asignarlo solo cuando la carrera realmente exige matemática avanzada.
4. Cómo leer el informe con el estudiante (para el orientador)
Primero mirar la nota para el orientador al final del informe: dice el tipo dominante con su porcentaje y el rango en puntos entre el tipo más alto y el más bajo. Un rango de 25 o más es un perfil nítido; entre 12 y 25 es moderado; menos de 12 es perfil plano y el informe ya lo marca. En ese caso no se conversa sobre “cuál es tu carrera” sino sobre qué actividades de exploración hacer antes (entrevistar a un profesional, visitar una universidad o instituto, probar el torneo de carreras de nuevo en unas semanas).
Segundo, las cuatro carreras y sus etiquetas. Alta o Máxima significa que el perfil completo es consistente con esa carrera; Media significa que hay afinidad parcial y vale la pena mirar qué componente falta; Explorable significa que es la mejor opción disponible pero con poca evidencia, cosa que pasa sobre todo en perfiles planos. Desde la v3 las cuatro carreras vienen de al menos dos áreas distintas, así que la conversación puede contrastar caminos y no solo variantes de uno.
Tercero, la advertencia matemática cuando aparece. No es una descalificación: dice que esa carrera exige matemática avanzada y que hoy el estudiante tuvo un resultado bajo en esa prueba. Se convierte en plan de refuerzo, no en descarte.
Cuarto, el nivel de cada carrera (Licenciatura, Técnico Superior o Normal · ESFM). El ranking mezcla los tres niveles con el mismo peso; corresponde al orientador conversar sobre qué nivel es realista para cada familia (duración, costo, ciudad donde se dicta). Para Potosí, la Universidad Autónoma Tomás Frías y los institutos técnicos del departamento cubren parte del catálogo; el resto implica mudarse a Sucre, La Paz, Cochabamba o Santa Cruz.
Preguntas que abren la conversación: ¿con cuál te sentís identificado? ¿cuál te sorprende? ¿cuál habrías puesto vos que no está? Esta última es la más valiosa: si el estudiante nombra una carrera que el informe dejó abajo, se puede buscar por qué (casi siempre es un interés bajo en Realista o un componente de inteligencia que el estudiante subestimó al responder).
5. Cómo verificar el algoritmo antes de un piloto
Todo el motor es accesible desde la consola del navegador con la sesión de orientador abierta en la app. La función generarResultadoLocal(e, riasec, gardner, cognitivo, valores, extra, islaTop, islaSeleccion, learning, bigfive, pares) devuelve el informe completo. Un perfil de prueba se arma así: e = {nombre, edad, pais:'Bolivia'}; riasec = {R,I,A,S,E,C} en porcentajes; gardner con las ocho inteligencias por nombre en porcentajes; cognitivo = {'Comprensión lectora','Razonamiento','Vocabulario','Matemática', total}; valores como lista de textos de la pantalla de valores; bigfive con los cinco rasgos por nombre; pares como objeto carrera → cantidad de victorias en el torneo. El resultado trae carreras (nombre, match, afinidad, advertencia, nivel), carreras_investigar, perfil_resumen, nota_metodologica y recomendacion_orientador.
La batería mínima antes de cada cambio son seis perfiles: realista-agro con matemática baja, investigador STEM, artístico visual-musical, perfil plano, emprendedor-social y convencional. Para cada uno se controla que las cuatro carreras sean coherentes con el tipo dominante, que las etiquetas no sean todas iguales entre perfiles, que el perfil plano se marque como tal, y que en el realista con matemática baja aparezcan carreras agropecuarias y técnicas (es el caso que fallaba antes de la v3). Conviene además controlar que CATALOGO_BO() siga teniendo 105 perfiles distintos: si dos carreras comparten vector, empatan y el orden lo decide la posición en el archivo.
Resultado de la batería con la v3 en producción: realista-agro → Construcción Civil, Topografía, Ingeniería Forestal, Agropecuaria (Media); STEM → Matemática, Ingeniería Informática, Sistemas Informáticos, Física (Alta); artístico → Lingüística e Idiomas, Comunicación Social, Comunicación Estratégica, Literatura (Alta); plano → Enfermería, Nutrición, Ciencias de la Educación, Ciencias Ambientales (Media, con texto de exploración); emprendedor-social → Derecho, Turismo, Turismo técnico, Relaciones Internacionales (Alta); convencional → Contaduría Pública, Banca y Seguros, Ingeniería Comercial, Administración (Alta).
6. Cómo se publica un cambio
El código vive en el repositorio privado paulavelazco25-hue/gps-vocacional, archivo index.html; Netlify redespliega automáticamente cada commit a main en uno o dos minutos. Se puede editar desde el editor web de GitHub (con la sesión de Pau) o desde la copia local en Downloads/gps-vocacional-fix con commit y push. Los informes ya generados no cambian con un deploy: quedan guardados en Firestore tal como se calcularon; solo los tests que se completen después usan la lógica nueva. Si se quiere regenerar el de un estudiante, existe la opción “regenerar” en el panel.
Historial reciente: 08/09/2026 — barras ordenadas de mayor a menor en informe y PDF; catálogo Bolivia con 105 vectores únicos vía AJUSTES_BO; umbral matemático solo para LO=5; razonamiento como ajuste fino ±4; detección de perfil plano; máximo dos carreras por área en el top 4; textos de “¿Cómo calculamos?” y nota metodológica actualizados. 15/07/2026 — ponderación v2, mensajes de aliento, A/B del orden de secciones.
7. Limitaciones conocidas y próximos pasos
El interés Realista pesa mucho en las carreras de diseño y arquitectura por cómo están codificadas (A-R-I), así que un artístico con Realista muy bajo recibe más carreras de comunicación y letras que de diseño; corregirlo implica revisar los códigos RIASEC del área arte. El nivel (universidad / técnico / ESFM) no se pregunta al estudiante; una preferencia previa ordenaría mejor. El catálogo es nacional y no distingue qué se dicta en Potosí. Si una sección cognitiva queda sin responder, el informe muestra 0% en lugar de “sin datos”. Y la función de IA (llamarIA) está definida pero sin uso; conectarla permitiría una narrativa más rica, con el costo y la variabilidad que eso implica.
Parte B — Manual para desarrolladores
Esta parte describe la app tal como está construida hoy, para que cualquier programador pueda ubicarse en el código y hacer cambios sin romper lo que funciona. Todo lo que sigue se relevó del código en producción (septiembre 2026).
8. Arquitectura en una página
GPS Vocacional es una single-page application en un único archivo index.html (~6.100 líneas, ~400 KB) sin framework, sin build y sin bundler: HTML, CSS y JavaScript plano en el mismo archivo. Se sirve como sitio estático desde Netlify (gpsvocacionalapp.netlify.app), con deploy automático en cada commit a main del repo privado paulavelazco25-hue/gps-vocacional.
El backend es Firebase: Authentication (email + contraseña) y Cloud Firestore como base de datos, accedidos directamente desde el navegador con el SDK compat 10.12.2 (firebase-app-compat, firebase-auth-compat, firebase-firestore-compat). No hay servidor propio; la única lógica fuera del navegador son dos Cloudflare Workers (worker-gps-v2.js y worker_registro.js en el repo) que hablan con Firestore por REST, con Mailchimp y con Resend/Stripe. El PDF se genera en el navegador con jsPDF 2.5.1 (cdnjs). No hay otras dependencias.
Flujo de datos, de punta a punta: el orientador crea un estudiante (documento en estudiantes) y obtiene un link con ?test=<sid>&token=<token>; el estudiante abre el link sin cuenta, la app cierra cualquier sesión y carga el test; al terminar, el navegador calcula el informe con generarResultadoLocal y guarda resultado en el mismo documento; el orientador lo ve en su panel y puede exportarlo a PDF.
9. Mapa del archivo index.html
El archivo tiene cuatro bloques <script> y el HTML de las vistas al final. Los números de línea son aproximados (versión del 08/09/2026) y sirven para orientarse; buscar por nombre de función es más seguro.
| Zona | Líneas aprox. | Qué contiene |
|---|---|---|
| Script 1 (cabecera) | 5–26 | window.onerror de seguridad (muestra el login si el JS falla), metas PWA. |
| CDN | 27–30 | jsPDF y Firebase compat. |
| Script 2 (config) | 31–61 | firebaseConfig, auth, db, y wrappers con nombres del SDK modular (getDoc, setDoc, updateDoc, deleteDoc, getDocs, query, collection, doc, onAuthStateChanged, etc.) sobre la API compat. Usar siempre estos wrappers, no db.collection() directo. |
| Script 3 (app) | 62–~5.400 | Toda la lógica: planes, auth, panel, test, algoritmo, informe, PDF, admin, institución, comparar, recursos. |
| Script 4 (arranque) | ~5.400–5.520 | Lectura de ?test=&token=, onAuthStateChanged, repairAdminDoc, gestión de planes admin. |
| HTML | ~5.520–6.121 | #view-auth (login/registro), #view-test (test del estudiante), #view-app (panel con sidebar y las páginas #page-*). |
Dentro del Script 3, en orden:
| Bloque | Funciones clave | Notas |
|---|---|---|
| Planes y permisos | PLANES (l.67), getPlan, canUse, esInstitucion, esPlanInstitucional, showUpgradeModal | Planes: free, starter, profesional, premium, inst_small, inst_medium, inst_large. Cada plan define límites (maxEstudiantes, maxPDFs, carrerasVisibles, verInformeCompleto, exportPDF, notaOrientador, marcaBlanca, dashboardAvanzado). |
| Utilidades | escapeHTML, toast, showModal, closeModal, showLoading, formatDate, initials | escapeHTML existe pero casi no se usa (ver Seguridad). |
| Auth | doLogin, doRegister (orientador), doRegisterFree (estudiante autónomo), doLogout, doForgotPassword | Al registrarse se crea el doc usuarios/{uid} con rol, estado, plan. |
| Sesión y panel | initApp (l.553), showPage (l.686), updateDashboard, renderEstudiantes, crearEstudiante, buildTestLink, resetearTest, deleteEstudiante | session es el objeto global con uid + campos del doc de usuario; showPage(id) activa #page-<id> y marca el nav (navMap). El bloque #admin-nav solo se muestra si session.rol==='admin'. |
| Test | TEST_STEPS (l.~990), TEST_STEPS_CORTO, pasos(), abVariante(), initTestView, renderTestStep, testNext/testPrev, guardarProgreso/cargarProgreso | Los pasos son objetos {title, instruccion, items, type}; tipos: mixed (Likert), cognitivo, raven, pares (torneo), más valores e isla ideal. Progreso en localStorage['gps_progress_'+sid] (expira a los 7 días). A/B: la variante B mueve el torneo al inicio. |
| Cierre del test | testSubmit → doSubmitFinal (l.~2199) | Calcula calcRIASEC, calcGardner, calcLearningStyle, calcBigFive, calcCognitivo, calcRaven, calcularPercentiles, calcCalidad; llama a generarResultadoLocal; guarda {tokenUsado:true, resultado, abVariante}; avisa al worker /tag-complete; muestra showTestSuccess. llamarIA y buildPrompt existen pero no se llaman. |
| Catálogo y algoritmo | CARRERAS_BO_ROWS, G_AREA_BO, TAGS_AREA_BO, AJUSTES_BO, CATALOGO_BO, generarResultadoLocal (con CARRERAS_GEN adentro), score, afinidadLabel | Ver Parte A, secciones 2 y 3. |
| Informe | openInforme (l.~3059), abrirEdicion/guardarEdicion, downloadPDF (l.~3397) | openInforme(sid) arma el HTML del informe del orientador; la vista del estudiante es renderMiResultado/showTestSuccess. Cualquier cambio visual del informe suele tocar tres lugares: vista estudiante, vista orientador y PDF. |
| Admin e institución | aprobarOrientador, cambiarPlanOrientador, loadAdminConfig/saveAdminConfig, renderInstDashboard, exportarCohorteCSV, renderInstOrientadores | config/plataforma guarda nombre, web y mensaje de bienvenida (marca blanca). |
| Comparar y recursos | initComparar, mostrarComparacion, MATERIALES_DEF, renderMateriales, descargarMaterial | Materiales del método (pilares/sesiones) embebidos en el código. |
| Manual | #page-admin-manual (HTML) | Este documento. showPage redirige al panel si el rol no es admin. |
10. Modelo de datos (Firestore)
usuarios/{uid} — una cuenta por orientador, institución, estudiante autónomo o admin. Campos: nombre, titulo, inst, contacto, email, pais, rol (orientador | estudiante | institucion | admin), estado (aprobado | pendiente | rechazado), plan (clave de PLANES), fechaReg, pdfCount, onboardingVisto; para estudiantes autónomos además autoSid y autoToken (apuntan a su propio doc en estudiantes); para orientadores de una institución, institucionId.
estudiantes/{sid} — un test. sid es 's' + uuid sin guiones. Campos: nombre, edad, escuela, anio, pais, obs, emailAlumno, orientadorId, orientadorNombre, fecha, token (64 hex, se compara con el de la URL), tokenUsado, resultado (null hasta completar), abVariante, testIniciadoAt, banderas esAutoTest / esDemo, y notaOrientador cuando el orientador edita. resultado contiene: carreras[] (nombre, match, afinidad, descripcion, entorno_laboral, tags, nivel, advertencia), carreras_investigar[], riasec, gardner, bigfive, cognitivo, percentiles, valores, learning, perfil_resumen, fortalezas, areas_desarrollo, entornos_laborales, plan_30_dias, preguntas_reflexion, para_padres, proximos_pasos, recomendacion_orientador, nota_metodologica, calidad.
config/plataforma — nombre, web, welcomeMsg (marca blanca, lo edita el admin).
baremos/global — histograma acumulado de RIASEC y Big Five (N, hist, actualizado) para calcular percentiles “pc”; cada test completado lo actualiza.
waitlist/{uid} — interés en formación/comunidad.
reporteTokens — creada por el worker v2 para reportes pagos por Stripe (flujo aparte del panel).
Regla práctica: el país del estudiante (estudiantes.pais) es lo que decide el catálogo; si está vacío o “no especificado” se usa el general.
11. Roles, permisos y navegación
El rol vive en usuarios/{uid}.rol y se copia a session al iniciar sesión. initApp muestra u oculta bloques del sidebar según rol (#admin-nav para admin, #inst-nav para institución/admin) y según plan (canUse('feature') y showUpgradeModal). Un orientador nuevo queda estado:'aprobado' y plan:'free' de entrada; el admin cambia planes desde “Usuarios & Planes” (cambiarPlanOrientador). El estudiante que se registra solo (doRegisterFree) obtiene un usuario con rol:'estudiante' y un doc en estudiantes propio.
Importante: hoy toda esta autorización es del lado del cliente. Lo que realmente protege los datos son las reglas de Firestore, que hay que mantener alineadas con estos roles (ver sección 14).
12. Flujos que conviene conocer antes de tocar código
Arranque. El Script 4 lee la URL. Si hay ?test=&token= cierra sesión (salvo &self=1, vista previa del orientador) y llama a initTestView(sid, token), que lee el doc, compara el token, y si tokenUsado muestra “test ya realizado”. Si no hay parámetros, onAuthStateChanged decide entre login y initApp.
Test. renderTestStep dibuja el paso actual según pasos(); las respuestas van a testState.answers (Likert), testState.cog, testState.raven, testState.pares, testState.valores, testState.islaTop. Cada cambio llama a guardarProgreso. Los mensajes de aliento se muestran entre secciones (mostrarTransicion). Para cambiar preguntas, editar TEST_STEPS: los ids de ítems (R1, I3, LI2, B5…) son los que usan calcRIASEC, calcGardner, calcBigFive; si se agrega un ítem hay que respetar el prefijo.
Informe. Tres renderizados distintos comparten los datos de resultado: showTestSuccess (estudiante al terminar), renderMiResultado (estudiante autónomo en su panel), openInforme (orientador) y downloadPDF (jsPDF, dibuja a mano). Un cambio de contenido normalmente se hace en generarResultadoLocal (datos) y se refleja solo; un cambio visual hay que replicarlo.
Regenerar un informe. resetearTest deja tokenUsado:false, resultado:null y el estudiante vuelve a hacer el test; no hay recálculo a partir de respuestas guardadas porque las respuestas crudas no se persisten en Firestore (solo en localStorage mientras dura el test). Si se quisiera “recalcular con el algoritmo nuevo”, primero habría que guardar answers en el doc al completar.
PDF. downloadPDF(sid) descuenta pdfCount y está detrás del plan (exportPDF). El texto se dibuja con doc2.text y checkY para saltos de página; no usa HTML.
Workers. worker-gps-v2.js: POST /register (alta + Mailchimp), POST /tag-complete (tag “Test GPS completado” + email de resultados), POST /webhook-stripe (verifica firma, crea reporteTokens, envía email con Resend), GET /reporte/:token (HTML del reporte pago). Usa env.* para secretos. worker_registro.js es la versión anterior con claves incrustadas (ver Seguridad). El desplegado en Cloudflare debería ser el v2; conviene confirmarlo en el dashboard de Cloudflare.
13. Convenciones para hacer cambios
Editar en el editor web de GitHub o en local con push a main; Netlify publica en 1–2 minutos. Antes de commitear un cambio de JS, verificar la sintaxis (por ejemplo, abrir la app en local con un servidor estático o pegar el bloque en la consola); un error de sintaxis en el Script 3 deja la app en el login por el window.onerror.
Usar los wrappers de Firestore del Script 2 y escapeHTML() para cualquier texto que venga de un usuario y se inserte con innerHTML. Toda función que se llame desde onclick en el HTML debe exponerse en window (window.miFuncion = miFuncion) porque el Script 3 no es un módulo pero varias funciones se declaran dentro de bloques. Para agregar una página al panel: un <div id="page-x" class="page">, un botón nav-x con onclick="showPage('x')", la entrada en navMap y, si carga datos, un if (id==='x') renderX(); dentro de showPage.
Para cambios de algoritmo, correr la batería de la sección 5 antes y después. Para cambios de catálogo, controlar que CATALOGO_BO() mantenga vectores únicos. Documentar cada deploy en el proyecto (estado-implementacion).
14. Seguridad — estado y prioridades
Resumen de la revisión de septiembre 2026 (detalle en el documento auditoria-seguridad).
- Reglas de Firestore (crítico). El estudiante hace el test sin autenticarse y lee y escribe
estudiantes/{sid}directamente; los workers acceden por REST solo con la API key pública. Eso implica que las reglas permiten lectura y escritura anónima, al menos sobreestudiantes: cualquiera que conozca la URL del proyecto puede listar nombres, edades, emails y resultados de menores, modificarlos o borrarlos. Arreglo recomendado: autenticación anónima para el estudiante (signInAnonymously) y reglas que impidan listar, permitan leer un doc solo por id, y permitan actualizar únicamenteresultado,tokenUsado,abVariante,testIniciadoAtcuandotokenUsadoerafalse;usuarioslegible/escribible solo por el propio uid (sin poder cambiarrol,plan,estado) y por admin;config,baremosywaitlistcon reglas explícitas. - Secretos en el repo (alto).
worker_registro.jstiene la API key de Mailchimp incrustada. Rotarla en Mailchimp, borrar el archivo del repo (queda en el historial: considerar limpiar) y usar solo variables de entorno del Worker, como ya hace v2. - Endpoints abiertos (alto).
/registery/tag-completeaceptan cualquier origen (Access-Control-Allow-Origin: *) sin autenticación ni límite: permiten inscribir emails ajenos en la audiencia, disparar emails y marcar tests como completados por email. Agregar verificación de origen, un secreto compartido o Turnstile, y rate limiting de Cloudflare. - XSS almacenado (medio). Hay ~40 inserciones con
innerHTMLy una sola llamada aescapeHTML. Nombres, escuela, observaciones y notas escritas por un usuario se renderizan sin escapar en paneles de otros usuarios (incluido el admin). Escapar sistemáticamente. - Autorización solo en cliente (medio).
rol,planyestadose leen del doc del usuario y el panel permiteupdateDoc(usuarios/{uid})para el perfil; si las reglas no restringen campos, un usuario puede auto-asignarseadminopremium. Se resuelve con las reglas del punto 1. - Cabeceras de seguridad (medio). No hay
_headersninetlify.toml: agregarContent-Security-Policy,X-Frame-Options,Referrer-Policy,Strict-Transport-Security,Permissions-Policy. - Endurecimiento de Firebase (bajo). Restringir la API key por referrer HTTP en Google Cloud, activar App Check, y revisar los dominios autorizados de Auth.
- Datos de menores. Existe consentimiento en el test y borrado por el orientador; falta política de privacidad visible, plazo de retención y un procedimiento de baja/exportación. Conviene resolverlo antes del piloto en un colegio.
| Orientador | Institución/Consultorio | Estado | Acciones | |
|---|---|---|---|---|
| Cargando... | ||||