Integración de Didact en SkillNet
Estado: inventario completo integrado; adopción funcional por familias
Didact: https://github.com/JoseEstevez520/Didact (MIT)
Revisión examinada: 06c80e8
Relacionado: openui-adoption.md, personalization-architecture.md,
learning-experience-architecture.md, v2-dynamic-courses.md
Alcance de este documento: describe el inventario y la integración ejecutable actual de Didact. La arquitectura objetivo neutral —donde Didact es un proveedor reemplazable,
LearningExperiencesustituye la frontera específica y los bloques pedagógicos legacy salen de cursos nuevos— se define enlearning-experience-architecture.md. Cuando una decisión histórica de adopción incremental de esta página contradiga ese objetivo, gana el documento neutral; esta página sigue ganando sobre qué tipos y puertos funcionan hoy.
Decisión
SkillNet conserva la autoría pedagógica, la personalización, el RAG, la seguridad de evaluación y la composición OpenUI generada en el momento. Didact aporta contratos y componentes educativos accesibles. No se integra como un segundo motor de cursos ni como una lista de widgets que el LLM deba conocer completa.
objetivo + knowledge pack + perfil cerrado
│
▼
plan de experiencia SkillNet
│
▼
resolver de capacidades de Didact
│ 2–5 candidatos compatibles
▼
generación OpenUI on-the-fly
│
▼
validación → render Didact adaptado → eventos
Didact ofrece las facetas necesarias para seleccionar sin inferir por nombres: propósito, representación, acción del alumno, contexto, accesibilidad, madurez, esquema de autoría, capacidades y dependencias opcionales. SkillNet las adapta a ComponentDescriptor; el catálogo no decide por sí solo qué debe aprender la persona.
Base OpenUI directa
La integración comenzó con dos experiencias del catálogo real skillnet-ui/1:
| Componente | Para qué entra | Estado que conserva |
|---|---|---|
Flashcard(front, back) |
Intento de recuerdo antes de revelar; útil para reconocer o reconstruir | revelado y autoevaluación local |
HintReveal(title, hints, solution) |
Pistas progresivas y solución bajo petición; especialmente útil con más apoyo | número de pistas y solución visible |
Ambos cumplen el dialecto estático: propiedades literales, estado React local, sin Query, Mutation, código ejecutable ni identidad. Después se sumaron Glossary, Timeline y WorkedExample como bloques directos, y DidactActivity como referencia opaca a definiciones server-owned. Sus esquemas existen en frontend y backend; el test de deriva comprueba nombres, orden de propiedades y artefacto del prompt. La versión del prompt invalida renders producidos con catálogos anteriores.
En la primera adopción no se sustituyó StepSequence por Timeline: representaban la misma
capacidad y se evitó duplicarlas en el catálogo. Esta fue una decisión incremental, no el estado
objetivo. Para cursos nuevos, la migración aprobada retira StepSequence y los demás componentes
pedagógicos legacy del catálogo de autoría; Didact entra a través de la frontera neutral
LearningExperience. Los renderers legacy permanecen sólo para reproducir cursos publicados.
Tampoco se añadió repetición espaciada; Didact separa correctamente la tarjeta de la planificación de
repasos y SkillNet no necesita todavía ese scheduler.
Selección cuando crezca el catálogo
Frontera ejecutable actual
La disponibilidad completa y la exposición al modelo son dos conjuntos distintos:
didact_snapshot.jsonyexport_didact_descriptors()proyectan los 34 tipos al resolver. Conservan identidad, facetas, acciones, representaciones, accesibilidad, productor y requisitos de puertos; un tipo bloqueado sigue siendo descubrible para experimentos y para explicar qué capacidad falta.openui_names_for_shortlist()es la puerta fail-closed. Solo traduce un tipo a su nombre de schema cuando renderer, permiso de emisión y puertos están listos.build_didact_prompt_slice()serializa exclusivamente esos schemas aceptados junto con el shell seguro de pantalla. Un tipo instalado pero bloqueado produce error explícito; nunca desaparece silenciosamente ni llega al LLM sin contrato.
Hoy los 34 tipos están inventariados y se cargan de forma lazy en el frontend. Veintinueve tienen una ruta de emisión honesta: cinco bloques OpenUI directos, once evaluaciones server-side, tres actividades con assets revisados, dos lecturas de progreso del host y ocho actividades con definición, estado y puertos. Los otros cinco permanecen disponibles para el resolver, pero bloqueados hasta que existan scheduler, simulación adaptada o sandbox. La tabla de runtime al final de este documento es la autoridad sobre cada familia.
La frontera ya está conectada al generador: el runtime forma una shortlist de 3-5 tipos,
aplica gates de renderer, puertos y datos, y entrega al modelo solo el slice permitido. Para
actividades ricas, una fase de autoría crea una ActivityDefinition server-owned y la
valida antes de persistir. Si no puede construirla con datos respaldados, hace Decline y
vuelve a una representación segura.
El modelo no debe recibir todos los componentes de Didact. Antes de cada generación se aplican filtros deterministas:
- disponibilidad y madurez permitida;
- misión cognitiva y función de la fuente;
- requisitos presentes en el knowledge pack;
- capacidades obligatorias de accesibilidad;
- productor disponible (
content,assessment,media,simulationodeterministic); - preferencias declaradas de presentación, como sesgo y no como obligación;
- presupuesto de complejidad de la pantalla.
El resultado es una colección pequeña y versionada, no una elección final rígida. El LLM puede componer entre esos candidatos y Decline si ninguno representa honestamente la misión. component_id@version, capacidades y versión de selección entrarán en traza y caché cuando el filtro pase de sombra a producción.
Niveles de adopción
Nivel A — estático y seguro
Props planas o listas, estado efímero, sin servicios externos. Puede entrar directamente en OpenUI: Flashcard, HintReveal, Glossary y algunas representaciones visuales.
Nivel B — respuesta y evaluación del host
El componente recoge una respuesta serializable, pero SkillNet conserva la respuesta correcta y evalúa por API. Matching, rúbricas con evidencia, anotación y preguntas avanzadas necesitan mapearse al endpoint y al sobre de eventos antes de entrar.
Nivel C — motor o medio inyectado
CodeExercise, InteractiveMedia, BranchingScenario y SimulationLab necesitan un puerto explícito de ejecución, reproducción o transición de estados. Una simulación es datos + estado + transiciones deterministas + renderer; nunca código inventado por el LLM dentro del programa OpenUI.
Invariantes
- Más componentes aportan riqueza cuando añaden acciones, estados, feedback o representaciones útiles.
- Los hechos críticos y las reglas de seguridad proceden del knowledge pack, no del componente.
- Una preferencia visual no fuerza una imagen sin valor ni permite inventar un asset.
- El answer key nunca llega en las props del navegador.
- Arrastrar nunca es la única vía de interacción.
- Una capacidad ausente produce fallback explícito o
Decline, no una simulación fingida. - La copia de un componente Didact vive en SkillNet y se actualiza deliberadamente; no se consume
mainmutable en producción.
Matriz de runtime del frontend (2026-08-13)
Los 34 tipos estan instalados, tienen loader lazy y pueden referenciarse mediante
DidactActivity(activity_id, component_id). OpenUI nunca recibe la definicion publica,
respuestas correctas ni configuracion de evaluacion. El porcentaje de didact.progress y
didact.mastery-badge lo inyecta el host desde LearnerNodeState; el cliente no puede
escribirlo.
| Estado | Tipos | Motivo |
|---|---|---|
| Usable local/estatico | flashcard, glossary-term, hint-reveal, rubric, timeline-steps, worked-example, data-explorer | No afirman correccion; puertos opcionales ausentes degradan |
| Persistencia host | self-explanation-prompt, concept-map, drawing-response, evidence-annotation | Estado por /activities/{id}/state; dibujo y anotacion aceptan evaluacion async |
| Evaluacion host compatible | equation-workbench, measurement-lab | Callback async con resultado de /activities/{id}/evaluate |
| Evaluacion server-side | matching, sort, categorize, cinco quiz, completion-problem, numeric-question, word-bank | Adaptador SecureEvaluatedActivity; la clave no llega a props, DOM ni eventos |
| Assets revisados | hotspot, label-diagram, interactive-media | Refs opacas skasset_; geometria/transcript verificados en servidor |
| Progreso de solo lectura | progress, mastery-badge | GET /activities/{id}/progress proyecta mastery del nodo; progress.write esta prohibido |
| Bloqueado: composicion/agenda | practice-set, retrieval-practice-session | Compone hijos evaluables o exige scheduler; no se finge |
| Bloqueado: runtime | branching-scenario, simulation-lab | Falta adaptar transiciones remotas al estado concreto del componente |
| Bloqueado: ejecucion | code-exercise | La respuesta generica aun no satisface ArtifactExecutionResponse |
Los endpoints de definicion, estado, evaluacion, transicion, ejecucion, assets y progreso
estan conectados como puertos genericos. Un puerto solo se expone cuando el contrato
concreto es compatible. La mera existencia de /evaluate no desbloquea un quiz que se
autocorrige en el navegador, ni /progress habilita practice-set.
Fallar una actividad evaluada (corregido el 2026-08-27)
Tres arreglos en la misma superficie, SecureEvaluatedActivity, que es el adaptador de todo lo
que se evalúa en el servidor:
- Una respuesta incorrecta lo dice y deja volver a intentarlo. Antes se imprimía “La
respuesta necesita revisión”, que se lee como “el sistema no ha podido corregir esto” y no
como “te has equivocado” — y encima congelaba los controles y quitaba el botón de enviar, así
que no había forma de reintentar.
unscored, el resultado para el que esa frase se escribió, nunca lo produce el corrector: el único camino real de un fallo era la frase engañosa. Ahora solocorrectes final (ofrecer reintento ahí falsificaría evidencia ya ganada);partialyunscoredofrecen uno. Limpiar el resultado no basta: un radio marcado y deshabilitado no se resetea de forma fiable, así que un nonce de intento indexa el subárbol y se reconstruye — el mismo patrón que ya usabaQuizItemBlock. didact.quiz.fill-in-the-blanktiene render propio. Estaba en la lista blanca sin rama suya, caía en el campo genérico “Tu respuesta” y la frase con el hueco no se enseñaba nunca: el aprendiz no veía qué estaba rellenando. Ahora el hueco se parte por el marcador que se le pide al generador (____,{{blank}},[blank]) y el campo se pinta en su sitio dentro de la frase. Sin marcador escrito, la frase se queda como encabezado y el campo lleva la etiqueta.- Los modos de texto plegan diacríticos (
normalized_any,keyed_text, ensrc/services/activity_definitions.py):cancionaceptacanciónypinguinoaceptapingüino, peroanoNO aceptaaño— la ñ es una letra del alfabeto y los pares que separa son palabras distintas (año/ano, caña/cana, seña/sena). La diéresis se pliega porque solo registra que la u se pronuncia y el español no tiene ningún par distinguido por ella; el acento agudo se pliega por una razón más débil pero explícita: pares como esta/está existen, pero la pregunta ya fija de qué palabra se habla, y suspender por una tilde que falta califica el teclado. Coste aceptado: estos modos ya no pueden evaluar acentuación, y la vía de escape escase_sensitive: true. Debajo había además un error duro:hmac.compare_digestlanzaTypeErrorcon cadenas no ASCII, así que cualquier respuesta esperada con tilde no puntuaba mal, reventaba; se comparan bytes UTF-8, que mantiene el tiempo constante.
Los ejemplos del contrato mostraban al modelo una lista expected de un solo elemento, así que
emitía una única respuesta aceptada aunque el corrector siempre haya aceptado cualquier miembro.
Ahora muestran variantes reales: es el único sitio donde el modelo lo aprende.
/activities/{id}/evaluate guarda evidencia, y la actividad tiene salida (corregido el 2026-08-28)
Dos agujeros que eran el mismo: el cierre por defecto de un nodo no dejaba rastro y no tenía salida.
- Se corregía y se tiraba. El cierre por defecto es la familia Didact
(
DIDACT_CLOSER_ROTATION); esas actividades se autoran en runtime sinImplementationBinding, así que el cliente no postea a/activities/{id}/attemptssino aPOST /activities/{id}/evaluate— que no persistía nada: ni fila de intento, ni maestría, nicommit. La prueba principal de la lección se calificaba y se descartaba, así que no contaba fallos, no alimentaba la personalización y ninguna regla de salida podía dispararse porque no había contador. Ahora pasa porMasteryEvidenceService, acotado a la familiaassessmentpara que una actividad de artefacto, simulación o medios siga sin tocar maestría — poner un número en un certificado que nadie se ganó es exactamente lo que ese límite impide. Todo lo demás conserva el comportamiento sin estado byte a byte.attempt_ides opcional y aditivo: cuando viaja, un doble clic replica el veredicto en vez de calificar dos veces, y reusarlo con un contenido distinto es409, igual que en/attempts. - Y no había salida. La escalera de pistas existe sólo para
QuizItem, que es el fallback;SecureEvaluatedActivity—el componente que pinta el camino normal— sólo sabía reintentar. Quien no daba con el orden de unsortde cinco elementos se quedaba ahí. Ahora la solución cierra la actividad y abre el paso, igual que en el quiz. Es la regla 8 de §7.3 dev2-dynamic-courses.md, que por eso dejó de exigirhints_used >= HINT_LIMIT: en la familia Didact no hay escalera que agotar, así que la condición era inalcanzable por construcción. - La solución la redacta el servidor.
evaluation.expectedde undidact.matchinges{"source-1": "target-1"}: ids de máquina que no significan nada en pantalla. Cruzarlos con las etiquetas públicas para escribir “Concepto A → Definición A” sólo puede hacerlo quien tiene las dos mitades, y mandarexpectedal navegador sería filtrar la clave. La respuesta traestate,mastery,show_worked_solutiony unasolutionya redactada (src/services/activity_solution.py), revelada bajopassed or show_worked_solution— la misma puerta quePOST /nodes/{id}/answerpone delante decorrect_answer, y deliberadamente sin mirarhints_used, que en esta ruta es un contador de nodo entero y abriría la respuesta de todas las actividades restantes en cuanto se gastaran tres pistas en cualquier sitio. - Tercer callejón: una actividad que no se puede evaluar nunca llega a tener un intento que contar, así que el aprendiz veía “No se pudo evaluar la respuesta, inténtalo de nuevo” para siempre. Ahora el paso se abre al primer envío y queda un warning en el log.
Siguiente ola propuesta
- scheduler real antes de emitir
retrieval-practice-session; - composición de hijos evaluables antes de emitir
practice-set; - transiciones deterministas de
branching-scenarioysimulation-labsobre el estado concreto del componente; - sandbox de
code-exerciseque cumplaArtifactExecutionResponse; - medir las 7 estrategias de selección con el banco offline y, si hay clave, un piloto LLM pequeño.
Cada ola se mide con el mismo nodo y knowledge pack: cobertura de hechos críticos, evidencia obtenible, variedad de acciones, accesibilidad, tasa de reparación, tokens, latencia y estabilidad. No se promueve un componente solo porque su story aislada sea atractiva.
Estado de cierre del 13 de agosto de 2026
- Los 34 tipos de Didact están fijados por commit, inventariados y disponibles mediante loaders lazy; el catálogo completo no aumenta el bundle inicial.
- 29 tipos son emitibles. Cinco siguen bloqueados con honestidad:
practice-set,retrieval-practice-session,branching-scenario,simulation-labycode-exercise. - El runtime usa por defecto
top5sobre una shortlist de 3-5 candidatos. Dual-agent y specialist permanecen en sombra. El catálogo completo permanece consultable por el resolver. - La llamada opcional de autoría registra tokens, modelo y duración; si falla, la lección
continúa con una representación segura. Un tipo
unsupporteddeclina antes del LLM. - El experimento fixture favorece intención + shortlist + esquema específico: 89,8 puntos y 100% de gates, frente a 27,8 del brazo legacy. Es evidencia de arquitectura, no prueba definitiva de calidad LLM.
- La personalización causal sigue siendo débil (15,4% en el fixture). La próxima ronda debe aislar apoyo, presentación y profundidad con modelos reales y evaluación ciega.