Calidad y fiabilidad del código
App: Python Course · Fecha: 1 de septiembre de 2026
Alcance: el intérprete de Python integrado, el contenido del curso (más de 2194 ejercicios, 48 lecciones, 17 módulos, una nueva sección «Lifehacks» — 15 tarjetas, 6 idiomas), el algoritmo de repetición espaciada y — nuevo en este informe — si el propio conjunto de pruebas realmente detecta regresiones (pruebas de mutación) en los 5 módulos de lógica más críticos
En resumen: dos preguntas independientes, dos respuestas. ¿Es correcto el código? 9710 candidatos verificados contra CPython y otros oráculos — 0 discrepancias. ¿Lo protegen realmente las pruebas? 3694 defectos inyectados deliberadamente (pruebas de mutación — la cobertura creció junto con el código) — los 3694 detectados. La segunda pregunta es nueva en este informe — es lo que hace que la primera sea digna de confianza y no solo una afirmación.
Pruebas de mutación — si las pruebas realmente detectan errores
Un porcentaje de cobertura solo responde "¿se ejecutó esta línea al menos una vez?", no "¿una prueba notaría si esta línea se rompiera?". Las pruebas de mutación responden directamente a la segunda pregunta: una herramienta inyecta deliberadamente miles de defectos pequeños e individuales en el código (cambia < por <=, && por ||, elimina una comprobación de límites, etc.) y verifica si al menos una prueba falla después. Si ninguna prueba detecta el defecto inyectado, es una brecha real — incluso si la cobertura de líneas por sí sola marca 100%.
| Módulo | Mutantes | Detectados | Score |
|---|---|---|---|
Lógica de desbloqueo de lecciones (gate-engine) |
208 | 208 | 100.00% |
Logros, XP, racha, corazones (gamification-engine) |
267 | 267 | 100.00% |
Planificador de repetición espaciada (srs-engine) |
47 | 47 | 100.00% |
| Intérprete de Python (tokenizador, parser, evaluador — incluyendo el código de import/archivos del módulo 17) | 3123 | 3123 | 100.00% |
Sección «Lifehacks» (lifehacks-engine + manage-lifehacks) |
49 | 49 | 100.00% |
| Total, 5 módulos más críticos | 3694 | 3694 | 100.00% |
"Detectado" significa aquí: el mutante hizo fallar al menos una prueba, o provocó un bucle infinito/bloqueo (también se cuenta — el defecto quedó igualmente señalado, solo que por otra vía; la fila del intérprete de Python de arriba cuenta exactamente un caso así). Cada caso individual en el que un mutante sobrevivía inicialmente se revisó a mano: o se escribió una prueba nueva que documenta el comportamiento real, o se demostró matemáticamente que ese cambio de código concreto nunca puede afectar al resultado (y en ese caso la propia línea lleva un comentario explicándolo, no un silencio), o bien — cuando esa resultó ser la respuesta más certera — el propio código se simplificó de modo que la rama de lógica no admitida dejó sencillamente de existir. Uno de esos casos se explica en detalle más abajo.
Metodología — corrección del código (comparación con CPython)
Tres líneas de validación independientes, sin código compartido y sin dependencia entre resultados. Intérprete: la app ejecuta el código a través de su propio intérprete de Python, escrito desde cero (no CPython, ni una biblioteca de terceros) — cada candidato (una solución de referencia, una instancia generada por plantilla o un programa generado al azar) se ejecutó simultáneamente en el intérprete de la app y en un CPython 3.12.3 instalado en el sistema, comparando luego el resultado exacto o la consistencia de errores. Estructura del curso: un recorrido completo, sin muestreo, por todos los archivos de contenido. Planificador de repetición espaciada: su salida se comparó con una implementación escrita por separado (no copiada del propio planificador) de la misma fórmula matemática. La validación se ejecutó mediante una herramienta temporal y aislada, fuera del código propio de la app, en varias rondas independientes que no se apoyaron en los resultados de las anteriores.
Resultados frente a un oráculo
| Línea de validación | Verificados | Coincidieron | Discrepancias |
|---|---|---|---|
Intérprete — contenido del curso y modo de práctica extendido (soluciones de referencia y alternativas, ejercicios de completar en modo valor, plantillas estáticas y paramétricas del modo de práctica extendido, la ejecución real de import/open() del módulo 17) y la sección «Lifehacks» (snapshots de código antes/después) |
393 | 393 | 0 |
| Intérprete — pruebas aleatorias (fuzz) (20 categorías de construcciones) | 400 | 400 | 0 |
| Planificador de repetición espaciada (1020 escenarios, 8917 pasos de repaso) | 8917 | 8917 | 0 |
| Total combinado | 9710 | 9710 | 0 |
Verificaciones estructurales y de contenido
| Verificación | Cobertura | Problemas encontrados |
|---|---|---|
| Estructura del curso (validez de soluciones, integridad del grafo de prerrequisitos, unicidad de id) | 2194 archivos de ejercicios, 48 lecciones, 17 módulos | 0 |
| Estructura del banco del modo de práctica extendido | 64 entradas (53 estáticas + 11 plantillas) | 0 |
| Sección «Lifehacks» (cada tarjeta referencia un concepto real del curso; el código antes/después produce la misma salida en el intérprete real) | 15 tarjetas, 30 snapshots de código | 0 |
| Paridad de localización (claves de contenido, sin valores sin traducir) | 12 214 claves × EN, ES, FR, DE, PT (61 070 comparaciones) | 0 |
Herramientas
strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes.Cómo se llegó realmente a estas cifras — sin adornos
Estas cifras no son con las que se empezó. La primera ejecución de pruebas de mutación sobre el módulo más grande y difícil (el intérprete de Python, ~2500 líneas) dio 75.65% — es decir, casi una cuarta parte de los defectos inyectados deliberadamente pasaban completamente desapercibidos. Cerrar esa brecha hasta el 100% llevó siete rondas consecutivas, cada una cerrando parte de lo que quedaba y sacando a la luz nuevos casos límite — incluidos errores de metodología cometidos durante el propio proceso de verificación.
Errores reales de la aplicación encontrados y corregidos (no solo brechas de pruebas) — descubiertos precisamente gracias a este proceso riguroso: la puntuación de una lección se rebajaba incorrectamente tras un fallo corregido (un solo error limitaba la puntuación máxima posible, incluso si el alumno acertaba de inmediato al repetir); la prueba de nivel otorgaba incorrectamente los logros "Sin Errores" y "Sin Piedad" por lecciones omitidas, no realmente completadas; y tres errores puntuales del propio intérprete (división entera, redondeo del operador de resto, comparación de bool con int).
Un error de metodología se detectó y corrigió en pleno proceso: una verificación intermedia (sustituir el texto de una línea entera en lugar de mutar en las coordenadas exactas de Stryker) sobrestimó cuántos mutantes eran realmente "equivalentes" (es decir, irreparables) — de 35 supuestos de ese tipo, solo 1 se confirmó. Se corrigió pasando a una verificación precisa, a nivel de coordenadas, para cada caso individual.
Los últimos 5 mutantes (comparación de cadenas usada al ordenar listas) no cedieron ni a una demostración de equivalencia ni a una prueba escrita a través de la interfaz pública normal — el propio algoritmo de ordenación de JavaScript hace comparaciones redundantes que enmascaran un resultado erróneo en un único par. Se resolvió probando directamente la función de comparación interna, evitando por completo el comportamiento impredecible del algoritmo de ordenación de terceros.
En resumen: ninguna cifra de este informe está pintada de antemano. Cada una es el resultado de un trabajo iterativo y verificado — incluido reconocer y corregir los propios errores por el camino. Por eso mismo se puede confiar en estas cifras: la app no solo se comporta correctamente en los ejemplos verificados — la red de seguridad de pruebas a su alrededor realmente detecta regresiones futuras, en lugar de existir solo formalmente.
Por qué "2797 de 2797, 100%" pasó brevemente a "3373 de 3374, 99.97%" antes de volver al 100%: tras alcanzar el 100% por primera vez (arriba), el intérprete incorporó nuevos temas del currículo — iteración directa (for x in iterable: sin range()), __str__/__eq__, @staticmethod/@classmethod, diccionarios/conjuntos/comprensiones/generadores — cientos de líneas nuevas que nunca habían pasado por pruebas de mutación. En vez de dejar ese crecimiento sin verificar (y seguir mostrando en silencio un "100%" ya obsoleto), se sometió a Stryker con el mismo rigor que el resto: 32 pruebas de regresión nuevas, cada una escrita contra un mutante superviviente concreto (primero leyendo el código alrededor, confirmando lógicamente por qué las pruebas existentes lo pasaban por alto, y solo entonces escribiendo la prueba) — no relleno de cobertura. También se revisó todo lo demás que había cambiado desde la ronda anterior de pruebas de mutación (17 de agosto): el parser (nueva gramática para esos mismos temas nuevos) y el planificador de repetición espaciada (un nuevo umbral previo al intercalado) — ambos ya al 100% sin que hiciera falta más trabajo. Eso dejó exactamente un mutante sin resolver, y aquí es donde el informe decía brevemente "3373 de 3374, 99.97%" con una explicación de por qué eso es más honesto que un "100%" fabricado: una función de búsqueda de variables locales (Scope.tryGet) tenía una comprobación que, en teoría, podía importar en dos escenarios distintos, pero uno resultó matemáticamente imposible dado cada punto de llamada real de la app — y el otro escenario, genuinamente alcanzable, ya estaba cubierto por una prueba dedicada. Marcar ambos mutantes de esa línea como "equivalentes" con un solo comentario habría sido el camino más rápido al "100%" — precisamente por eso no se hizo en ese momento: la herramienta solo puede desactivar la comprobación de mutación para una línea entera, no para un mutante concreto, así que ese atajo habría barrido también la cobertura del escenario que sí importa.
Qué cambió después — y por qué esto es un 100% genuino, no un parche: en lugar de conformarse con el 99.97% o suprimir en silencio lo que quedaba, la siguiente pregunta fue si esa línea era, de entrada, código muerto. Un rastreo más detallado de cada llamada a tryGet() en la app (hay exactamente una) y de cada lugar del que el valor que protegía podía llegar a originarse mostró que sí: la línea era genuinamente inalcanzable en cualquier estado real del programa, no simplemente difícil de probar. Se eliminó por completo. Esto no es un atajo: borrar código realmente muerto no pierde ninguna cobertura real (a diferencia de suprimir el mutante con un comentario, que también habría descartado la cobertura real, ya verificada, del mutante hermano) — el mutante que esa línea generaba simplemente dejó de existir junto con la línea. La suite de pruebas completa (1035 pruebas en ese momento) confirmó que el comportamiento no cambió ni un ápice. La siguiente ejecución de Stryker dio un genuino 100.00%. La lección de este pequeño desvío del propio informe: estar dispuestos a mostrar una cifra honesta justo por debajo del 100% resultó ser el camino más rápido hacia un 100% real, no uno más lento que disimular la brecha desde el principio.
De dónde viene el crecimiento de 3371 a 3694 y de 9655 a 9710 (1 de septiembre de 2026): el curso creció — un nuevo módulo 17, «Import, módulos y manejo de archivos», añadió ejecución real (no solo reconocimiento de sintaxis) de import/open()/with contra un sistema de archivos virtual simulado, y una nueva sección «Lifehacks» (15 tarjetas «antes → después» que explican por qué la forma más corta funciona igual). Ambas incorporaciones pasaron por la misma disciplina que el resto de la app: interpreter.ts y parser.ts se sometieron de nuevo a pruebas de mutación (+238 y +36 mutantes respectivamente, todos detectados), el nuevo lifehacks-engine y manage-lifehacks se llevaron desde cero hasta el 100% (2 brechas reales de pruebas cerradas, no disimuladas), las 15 tarjetas (30 snapshots de código) se verificaron contra CPython, y los 2 nuevos ejercicios del módulo 17 basados en bucles también. Por separado, ese mismo día se cerró el último rezago de traducción: la sección «Lifehacks» y las 434 claves nuevas del módulo 17 se tradujeron de inmediato al español, francés, alemán y portugués, así que los 6 idiomas del curso están ahora al 100% de paridad de contenido (12 214 claves cada uno) — de ahí que el número de comparaciones de localización creciera de 58 635 a 61 070.
Notas
Durante la validación iterativa se encontraron y corrigieron tres errores puntuales del intérprete — ninguno afectó jamás a contenido real del curso, y los tres quedan ahora asegurados por pruebas permanentes que vigilan que no vuelvan a aparecer:
- la división entera se mostraba como número entero en lugar del resultado decimal real de Python (
11en vez de11.0); - un artefacto poco frecuente de redondeo de punto flotante en el operador de resto (
%) con operandos fraccionarios; boolno se comparaba como numéricamente igual ainten==/!=/in/list.remove().
El límite máximo de intervalo del planificador de repetición espaciada se activó correctamente en varios cientos de pasos diseñados para alcanzarlo, sin ninguna excepción. Los resultados anteriores corresponden a la ronda de validación final y más reciente, sin problemas abiertos conocidos.