En esta páginaDecisión

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, LearningExperience sustituye la frontera específica y los bloques pedagógicos legacy salen de cursos nuevos— se define en learning-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.

Frontera ejecutable actual

La disponibilidad completa y la exposición al modelo son dos conjuntos distintos:

  • didact_snapshot.json y export_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:

  1. disponibilidad y madurez permitida;
  2. misión cognitiva y función de la fuente;
  3. requisitos presentes en el knowledge pack;
  4. capacidades obligatorias de accesibilidad;
  5. productor disponible (content, assessment, media, simulation o deterministic);
  6. preferencias declaradas de presentación, como sesgo y no como obligación;
  7. 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 main mutable 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 solo correct es final (ofrecer reintento ahí falsificaría evidencia ya ganada); partial y unscored ofrecen 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 usaba QuizItemBlock.
  • didact.quiz.fill-in-the-blank tiene 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, en src/services/activity_definitions.py): cancion acepta canción y pinguino acepta pingüino, pero ano NO acepta añ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 es case_sensitive: true. Debajo había además un error duro: hmac.compare_digest lanza TypeError con 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 sin ImplementationBinding, así que el cliente no postea a /activities/{id}/attempts sino a POST /activities/{id}/evaluate — que no persistía nada: ni fila de intento, ni maestría, ni commit. 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 por MasteryEvidenceService, acotado a la familia assessment para 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_id es opcional y aditivo: cuando viaja, un doble clic replica el veredicto en vez de calificar dos veces, y reusarlo con un contenido distinto es 409, 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 un sort de 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 de v2-dynamic-courses.md, que por eso dejó de exigir hints_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.expected de un didact.matching es {"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 mandar expected al navegador sería filtrar la clave. La respuesta trae state, mastery, show_worked_solution y una solution ya redactada (src/services/activity_solution.py), revelada bajo passed or show_worked_solution — la misma puerta que POST /nodes/{id}/answer pone delante de correct_answer, y deliberadamente sin mirar hints_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

  1. scheduler real antes de emitir retrieval-practice-session;
  2. composición de hijos evaluables antes de emitir practice-set;
  3. transiciones deterministas de branching-scenario y simulation-lab sobre el estado concreto del componente;
  4. sandbox de code-exercise que cumpla ArtifactExecutionResponse;
  5. 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-lab y code-exercise.
  • El runtime usa por defecto top5 sobre 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 unsupported declina 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.