diff --git a/assets/schemas/course.schema.json b/assets/schemas/course.schema.json index 5b7db9f..baafc17 100644 --- a/assets/schemas/course.schema.json +++ b/assets/schemas/course.schema.json @@ -188,7 +188,12 @@ }, "lesson": { "type": "object", - "required": ["id", "order", "slug", "title", "context", "concept", "build", "ship", "reflect"], + "description": "A lesson supports TWO authoring shapes (relaxed under DOJ-4007 to unblock retrofit of legacy free-form content for staging seed):\n\n 1. **IDT 5-bucket (recommended)** — `context` + `concept` + `build` + `ship` + `reflect`. Enforces Builder's Bloom pedagogy + Ship-First Design. Use for new lessons authored via the instructional-design-toolkit.\n 2. **Free-form `body`** — single markdown string. Use for legacy / open-ended content that doesn't fit the 5-bucket frame, or for lessons that ship before IDT-style structuring (DemoLab/Articulate Rise parity).\n\n `anyOf` requires AT LEAST one of the two shapes (cannot ship an empty lesson). A lesson MAY provide both: the IDT tooling will prefer the structured fields and treat `body` as supplemental commentary. Course validators downstream (dojo-os ingestion) MUST accept both shapes without branching on which one is present — render the structured fields when available, fall back to `body` otherwise.", + "required": ["id", "order", "slug", "title"], + "anyOf": [ + { "required": ["body"] }, + { "required": ["context", "concept", "build", "ship"] } + ], "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^lesson:[a-z0-9-]+$", "description": "IMMUTABLE" }, @@ -197,31 +202,36 @@ "slug": { "type": "string" }, "title": { "type": "string" }, "estimated_minutes": { "type": "integer", "minimum": 5 }, + "body": { + "type": "string", + "minLength": 50, + "description": "Free-form markdown body — alternative to the 5-bucket structured fields. Use for legacy / open-ended content or content imported from external authoring tools (Articulate Rise, Notion exports, etc.). IDT tooling emits a WARNING (not an error) when a lesson uses `body` instead of the structured fields, recommending migration to the 5-bucket frame when the content allows." + }, "context": { "type": "string", "minLength": 100, - "description": "100-200 word hook that makes the student FEEL the urgency" + "description": "(Recommended) 100-200 word hook that makes the student FEEL the urgency. Required when not using the free-form `body` field." }, "concept": { "type": "string", "minLength": 200, - "description": "300-500 word mental model. Use \\n---\\n to split across Marp slides." + "description": "(Recommended) 300-500 word mental model. Use \\n---\\n to split across Marp slides. Required when not using the free-form `body` field." }, "build": { "type": "string", "minLength": 200, - "description": "50-60% of lesson content. Apply the concept with Claude as partner (NOT teacher)." + "description": "(Recommended) 50-60% of lesson content. Apply the concept with Claude as partner (NOT teacher). Required when not using the free-form `body` field." }, "ship": { "type": "string", "minLength": 30, - "description": "50-100 word deliverable — what the student saves/creates/deploys" + "description": "(Recommended) 50-100 word deliverable — what the student saves/creates/deploys. Required when not using the free-form `body` field." }, "reflect": { "type": "array", "items": { "type": "string" }, "minItems": 1, - "description": "1-2 provocative questions specific to THIS lesson" + "description": "(Recommended) 1-2 provocative questions specific to THIS lesson. Optional but encouraged for both authoring shapes." }, "marp_slide_count": { "type": "integer", "minimum": 3 } } diff --git a/assets/schemas/path.schema.json b/assets/schemas/path.schema.json new file mode 100644 index 0000000..3c72757 --- /dev/null +++ b/assets/schemas/path.schema.json @@ -0,0 +1,170 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://dojocodinglabs.github.io/instructional-design-toolkit/path.schema.json", + "title": "Path", + "description": "A Dojo Path authoring contract produced by instructional-design-toolkit. A PATH is a SUPER-STRUCTURE *above* the cmi5-course layer (course.schema.json): it references existing courses by id or slug, groups them into ordered MILESTONES, and wraps them in a credential. The per-course cmi5/xAPI contract is UNTOUCHED — a path adds NO AU and NO block; it is purely additive. xAPI identity (additive, no collision with course IRIs): the path activity IRI is `path:`, each milestone activity IRI is `path:/milestone:`. The credential is an OpenBadge 3.0 achievement (credential.cert_name / skills_demonstrated / earning_criteria seed the OpenBadge template — NOT a new cmi5 AU). IDs (meta.id, milestones[].id) are IMMUTABLE once created — changing them breaks xAPI history and OpenBadge issuance for learners. Slugs and titles are mutable. Field names align with the dojo-academy content source-of-truth at schemas/path.schema.yml (content/paths//path-overview.md frontmatter); this JSON schema is the toolkit authoring contract and additionally carries the meta.id / version cmi5-identity fields the YAML omits.", + "type": "object", + "required": ["meta", "milestones"], + "additionalProperties": false, + "properties": { + "meta": { + "type": "object", + "required": ["id", "version", "slug", "title", "language", "status"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^path:[a-z0-9-]+$", + "description": "IMMUTABLE path identifier. Doubles as the path-level xAPI activity IRI (path:). Changing it breaks xAPI history and OpenBadge issuance." + }, + "version": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "description": "Semantic version. MAJOR=breaking (milestone removed/reordered, course removed from a milestone, credential criteria hardened), MINOR=additive (course or milestone added), PATCH=cosmetic (title/description/presentation tweaks)." + }, + "slug": { + "type": "string", + "pattern": "^[a-z0-9-]+$", + "description": "URL + xAPI IRI key. Must be stable once published. Mirrors dojo-academy path.slug." + }, + "title": { + "type": "string", + "minLength": 5, + "description": "Mutable display title." + }, + "language": { + "type": "string", + "enum": ["es", "en"] + }, + "description": { + "type": "string", + "description": "Mutable path summary." + }, + "status": { + "type": "string", + "enum": ["draft", "published", "archived"], + "description": "Lifecycle status. Mirrors dojo-academy path.status (default draft)." + }, + "level": { + "type": "string", + "enum": ["beginner", "intermediate", "advanced"], + "description": "Aggregate difficulty of the path (independent of Dojo Score rank)." + }, + "emblem": { + "type": "string", + "description": "Emoji/glyph for the credential badge when no image URL is set." + }, + "faculty": { + "type": "string", + "description": "Owning faculty (e.g. 'Dojo Academy')." + }, + "created": { "type": "string", "format": "date" }, + "updated": { "type": "string", "format": "date" }, + "version_timeline": { + "type": "array", + "description": "Append-only log of versions.", + "items": { + "type": "object", + "required": ["version", "date", "type", "summary"], + "additionalProperties": false, + "properties": { + "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" }, + "date": { "type": "string", "format": "date" }, + "type": { "type": "string", "enum": ["major", "minor", "patch"] }, + "summary": { "type": "string" } + } + } + } + } + }, + "credential": { + "type": "object", + "description": "OpenBadge 3.0 achievement metadata. This is NOT a new cmi5 AU — it is the path-level credential definition that seeds the OpenBadge template (and dojo-academy paths.{cert_name,skills_demonstrated,earning_criteria} columns).", + "additionalProperties": false, + "properties": { + "cert_name": { + "type": "string", + "description": "Path certificate / credential name (e.g. 'Agentic AI Engineer')." + }, + "skills_demonstrated": { + "type": "array", + "items": { "type": "string" }, + "description": "Curated path-level skills (distinct from per-course tags). Maps to OpenBadge 3.0 achievement skills." + }, + "earning_criteria": { + "type": "array", + "items": { "type": "string" }, + "description": "How a learner earns the credential (bullet list). Maps to OpenBadge 3.0 achievement criteria." + } + } + }, + "presentation": { + "type": "object", + "description": "Path-card and credential presentation metadata (no pedagogical weight).", + "additionalProperties": false, + "properties": { + "tech_pills": { + "type": "array", + "items": { "type": "string" }, + "description": "3-4 most-recognisable technologies the path teaches (path card)." + }, + "reward_points": { + "type": "integer", + "minimum": 0, + "description": "Dojo Score reward on completion (reconciled with score-engine)." + }, + "credential_badge_url": { + "type": "string", + "format": "uri", + "description": "Optional optimized badge image; overrides emblem when set." + }, + "banner_image_url": { + "type": "string", + "format": "uri" + } + } + }, + "milestones": { + "type": "array", + "minItems": 1, + "description": "Ordered named phases. Each groups ordered course references. A milestone adds NO cmi5 AU — its xAPI identity is path:/milestone:.", + "items": { "$ref": "#/$defs/milestone" } + } + }, + "$defs": { + "milestone": { + "type": "object", + "required": ["id", "ordinal", "title", "courses"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^(milestone:[a-z0-9-]+|path:[a-z0-9-]+/milestone:\\d+)$", + "description": "IMMUTABLE milestone identifier. Either a bare `milestone:` or the fully-qualified `path:/milestone:` form (the latter is the xAPI activity IRI). Changing it breaks xAPI history." + }, + "ordinal": { + "type": "integer", + "minimum": 1, + "description": "1-based position of this milestone within the path (teaching order)." + }, + "title": { + "type": "string", + "description": "Mutable milestone title." + }, + "description": { + "type": "string", + "description": "Mutable milestone summary." + }, + "courses": { + "type": "array", + "minItems": 1, + "description": "Member courses in teaching order. Each entry is either a cmi5 course id (`course:`) or a bare course slug. The path REFERENCES these courses — it never modifies their course.json.", + "items": { + "type": "string", + "pattern": "^(course:[a-z0-9-]+|[a-z0-9-]+)$" + } + } + } + } + } +} diff --git a/commands/new-path.md b/commands/new-path.md new file mode 100644 index 0000000..94c35eb --- /dev/null +++ b/commands/new-path.md @@ -0,0 +1,68 @@ +--- +description: "Diseño guiado de Path — super-estructura sobre la capa cmi5: agrupa cursos existentes en milestones ordenados y los envuelve en una credential OpenBadge 3.0. No modifica ningún course.json." +argument-hint: "[optional: path-slug si querés revisar/continuar uno existente]" +--- + +# /new-path — Diseño Guiado de Path + +Dispatcher que invoca el skill `instructional-design-toolkit:new-path`. + +Un **Path** es una **super-estructura ABOVE the cmi5-course layer**: referencia +cursos existentes por id/slug, los agrupa en **milestones** ordenados, y carga +metadata de credential + presentation. El contrato cmi5/xAPI por-curso queda +**INTACTO** — un path NO agrega ningún AU ni block; es puramente aditivo. + +Si `$ARGUMENTS` matchea un path existente en +`content/paths/{slug}/path-overview.md`, el skill detecta el modo revise (sugiere +bumpear versión + changelog). Si vacío o slug nuevo, arranca diálogo guiado desde +cero. + +## Qué hace (y qué NO hace) + +- **Hace**: pickear cursos miembros (por slug), agruparlos en milestones nombrados + y ordenados, autorear la credential (cert name, skills demonstrated, earning + criteria = metadata OpenBadge 3.0), setear level / emblem / reward. +- **NO hace**: NO modifica ningún `course.json`. Los cursos se referencian por + id/slug; sus AUs cmi5, masteryScore, moveOn y xAPI IRIs quedan tal cual. El path + vive en una capa de identidad propia (`path:` + `path:/milestone:`) + que no colisiona con los IRIs de curso. + +## Output esperado + +``` +content/paths/{slug}/ +├── path.json ← fuente de verdad (validado contra path.schema.json) +└── path-overview.md ← frontmatter alineado con el contrato dojo-academy + (schemas/path.schema.yml) +``` + +`path.json` lleva los campos de identidad cmi5 (`meta.id` = `path:`, +`meta.version` semver) que el YAML de dojo-academy omite. El frontmatter de +`path-overview.md` usa exactamente el shape del contrato dojo-academy +(`slug` / `title` / `description` / `status` / `level` / `emblem` / `faculty` / +`cert_name` / `skills_demonstrated` / `earning_criteria` / `tech_pills` / +`reward_points` / `milestones[{title, description, courses[]}]`) para que +`bun run path:import` lo consuma sin branching. + +## Próximos comandos sugeridos + +- `/new-track {category}` — mapear el catálogo de cursos + prerequisite graph +- `/course-audit {slug}` — validar un curso miembro contra el framework +- `/new-course {slug}` — crear un curso miembro que todavía no existe + +## Overlay invocation (post-base-draft) + +Después de producir el base draft (Layer 1 cmi5/xAPI-shaped + Layer 2) de este +comando, seguir `${CLAUDE_PLUGIN_ROOT}/assets/runtime/overlay-protocol.md` para +descubrir y aplicar consumer overlays. El runtime camina +`/.claude-plugin/plugin.json`, encuentra skills que declaran +`overlay_target: ["new-path"]` en su frontmatter, los ordena por +`overlay_priority`, y los aplica en orden. + +Layer 1 invariants (`meta.id` = `path:`, `milestones[].id`, semver, la +identidad OpenBadge 3.0 de la credential) permanecen inmutables — outputs de +overlay que los muten abortan el run con un error que apunta al `SKILL.md` +ofensor. Layer 2 contradictions (milestones sin cursos, ordinal gaps, credential +sin earning criteria) loguean un warning visible pero no abortan. Discovery +devuelve cero overlays en un consumer sin `.claude-plugin/plugin.json` — el base +draft del path se escribe directo, voice-neutral, sin warning. diff --git a/skills/new-path/SKILL.md b/skills/new-path/SKILL.md new file mode 100644 index 0000000..2242261 --- /dev/null +++ b/skills/new-path/SKILL.md @@ -0,0 +1,287 @@ +--- +name: new-path +version: 1.0.0 +description: > + Diálogo guiado para diseñar un Path — una super-estructura SOBRE la capa + cmi5-curso que agrupa cursos existentes en milestones ordenados y los envuelve + en una credential OpenBadge 3.0. NO modifica ningún course.json (aditivo). + Produce path.json (fuente de verdad, validado contra path.schema.json) + + content/paths//path-overview.md (frontmatter alineado con el contrato + dojo-academy). Use when user asks to "create a path", "design a path", + "new path", "crear un path", "diseñar un path", "nuevo path", "ruta de + aprendizaje", "learning path", "armar un path", "/new-path". +--- + +# Taller de Diseño de Path + +Diálogo guiado interactivo para diseñar un **Path**: una super-estructura +**ABOVE the cmi5-course layer**. Un path referencia cursos existentes por id/slug, +los agrupa en **milestones** ordenados, y carga metadata de credential +(OpenBadge 3.0) + presentation. + +**Invariante central — un path es puramente aditivo:** NO toca ningún +`course.json`. Los AUs cmi5, masteryScore, moveOn y xAPI IRIs de cada curso quedan +intactos. El path vive en su propia capa de identidad: + +- path activity IRI = `path:` +- milestone activity IRI = `path:/milestone:` +- credential = OpenBadge 3.0 (seeded desde `credential.*`), NO un AU. + +Produce `path.json` (fuente de verdad) + `path-overview.md` (frontmatter para +humanos + ingestion dojo-academy). + +## Regla de idioma + +Todo el contenido generado en **español**. Términos técnicos en formato +**"español (English)"** la primera vez (*"Hito (Milestone)"*, +*"Credencial (Credential)"*). Después solo español. Nombres propios de +frameworks mantienen su idioma (cmi5, xAPI, OpenBadge, Dojo Score). + +## Directorio de salida + +`content/paths/{slug-del-path}/` — contiene `path.json`, `path-overview.md`, +eventualmente `CHANGELOG.md` (en revise). + +## Estilo de preguntas + +Una pregunta a la vez. Opción múltiple cuando sea posible. Esperar respuesta +antes de avanzar. + +## Puerta obligatoria + +NO generar ningún artefacto hasta que cada paso esté completo Y aprobado por el +usuario. Si el usuario pide saltar un paso, advertir el riesgo (milestone sin +cursos, credential sin earning criteria, ordinal gaps) antes de proceder. + +--- + +## Paso 0 — Detectar Modo Revise (Optional) + +Si `$ARGUMENTS` no está vacío: + +1. Buscar `content/paths/{$ARGUMENTS}/path-overview.md` (o `path.json`). +2. Si existe: sugerir + > "Ya existe un path `{$ARGUMENTS}`. Para iterar bumpeá versión + changelog. + > Si querés crear uno nuevo, dame un slug distinto." + ABORT. +3. Si no existe: continuar al Paso 1 con el slug pre-poblado. + +--- + +## Paso 1 — Identidad del Path + +Preguntar (una a la vez): + +1. "¿Cuál es el **slug** del path? (`^[a-z0-9-]+$`, estable una vez publicado — + se vuelve la xAPI IRI `path:`)." +2. "**Título** del path:" +3. "**Descripción** en 1-2 frases — ¿qué transforma este path en el alumno?" +4. "**Idioma** (es / en):" +5. "**Level** agregado del path (beginner / intermediate / advanced):" +6. "**Faculty** dueño (ej. 'Dojo Academy'):" +7. "**Emblem** — emoji/glyph para la credential cuando no hay imagen:" + +Asignar `meta.id = path:` (IMMUTABLE) y `meta.version = 0.1.0` inicial. +`meta.status` default `draft`. + +**PUERTA DE APROBACIÓN**: confirmar identidad antes de continuar. + +--- + +## Paso 2 — Pick de Cursos Miembros + +Explicar: + +> "Un path NO crea cursos — referencia cursos que ya existen, por slug o por +> `course:` id. No vamos a tocar ningún `course.json`." + +Preguntar: + +1. "Listame los cursos miembros de este path, en orden de enseñanza. Cada uno por + slug (ej. `flutter-fundamentals`) o por id (`course:flutter-fundamentals`)." + +Si el repo consumidor tiene `content/courses/`, verificar que cada slug resuelva +a un directorio existente. Si alguno falta, advertir: + +> "El curso `{slug}` no existe todavía en `content/courses/`. Podés crearlo con +> `/new-course {slug}` antes de publicar el path, o dejarlo referenciado como +> placeholder (el path queda en `draft`)." + +NO crear ni modificar ningún curso. + +**PUERTA DE APROBACIÓN**: confirmar la lista de cursos miembros. + +--- + +## Paso 3 — Agrupar en Milestones + +Explicar: + +> "Los milestones son fases nombradas y ordenadas que agrupan los cursos. Cada +> milestone tiene su propia identidad xAPI (`path:/milestone:`) +> pero NO es un AU cmi5 — es agrupación + presentación." + +Para cada milestone (uno a la vez), preguntar: + +a. "**Título** del milestone:" +b. "**Descripción** (opcional) — qué desbloquea esta fase:" +c. "¿Qué cursos (de la lista del Paso 2) entran en este milestone, en orden?" + +Asignar `ordinal` 1-based incremental y `id`: +- forma corta `milestone:`, o +- forma calificada `path:/milestone:` (= la xAPI IRI). IMMUTABLE. + +**Validación**: +- Cada curso del Paso 2 debe caer en exactamente un milestone (warn si un curso + queda huérfano o duplicado). +- `ordinal` debe ser contiguo desde 1 (warn si hay gaps). +- Ningún milestone vacío (cada uno ≥ 1 curso). + +**PUERTA DE APROBACIÓN**: confirmar el mapa de milestones. + +--- + +## Paso 4 — Credential (OpenBadge 3.0) + +Explicar: + +> "La credential es la metadata OpenBadge 3.0 del path. NO es un AU cmi5 nuevo — +> es el achievement que el alumno gana al completar el path. Estos tres campos +> seedean el template OpenBadge." + +Preguntar (una a la vez): + +1. "**Cert name** — nombre de la credencial (ej. 'Agentic AI Engineer'):" +2. "**Skills demonstrated** — 3-6 skills nivel-path que el alumno demuestra + (distintas de los tags por-curso):" +3. "**Earning criteria** — cómo se gana la credencial (bullet list verificable):" + +**Validación**: +- `cert_name` presente si `status` será `published` (warn si vacío). +- Al menos 1 earning criterion (warn si vacío). + +**PUERTA DE APROBACIÓN**: confirmar la credential. + +--- + +## Paso 5 — Presentation + +Preguntar: + +1. "**Tech pills** — 3-4 tecnologías más reconocibles que enseña el path (para la + path card):" +2. "**Reward points** — Dojo Score reward al completar (int ≥ 0):" +3. "**Credential badge URL** (opcional — overridea el emblem si está):" +4. "**Banner image URL** (opcional):" + +**PUERTA DE APROBACIÓN**: confirmar presentation. + +--- + +## Paso 6 — Generar JSON + MD + Validar + +1. Construir el objeto `path.json` siguiendo + `${CLAUDE_PLUGIN_ROOT}/assets/schemas/path.schema.json`. + + El objeto resultante es el **base draft (Layer 1 + Layer 2)** — cmi5/xAPI-safe + (capa de identidad path aditiva, contrato de curso intacto), voice-neutral. + +2. **Aplicar overlays (Base + Overlay protocol).** Antes de tocar disco, leer + `${CLAUDE_PLUGIN_ROOT}/assets/runtime/overlay-protocol.md` y ejecutar el + procedimiento completo (Discovery + Invocation + Layer 1 validator) sobre el + base draft: + + - `command` = `"new-path"` + - `cwd` = directorio donde se invocó `/new-path` + - `baseDraft` = objeto `path.json` + - `context.locale` = `path.meta.language` (`es` | `en`) + + Layer 1 invariants (`meta.id` = `path:`, `milestones[].id`, semver, + identidad OpenBadge de la credential) se revalidan después de cada overlay. Si + un overlay los muta: ABORT con error apuntando al `SKILL.md` ofensor. NO se + escribe nada. + +3. Crear directorio `content/paths/{slug}/`. + +4. Escribir `path.json` (el draft final post-overlays). + +5. Generar `path-overview.md` con frontmatter YAML que matchea el contrato + dojo-academy `schemas/path.schema.yml`: + + ```yaml + --- + slug: { meta.slug } + title: { meta.title } + description: { meta.description } + status: { meta.status } + level: { meta.level } + emblem: { meta.emblem } + faculty: { meta.faculty } + cert_name: { credential.cert_name } + skills_demonstrated: { credential.skills_demonstrated } + earning_criteria: { credential.earning_criteria } + tech_pills: { presentation.tech_pills } + reward_points: { presentation.reward_points } + credential_badge_url: { presentation.credential_badge_url } + banner_image_url: { presentation.banner_image_url } + milestones: + - title: { ... } + description: { ... } + courses: [ ... ] + --- + ``` + + El cuerpo del MD resume el path para humanos (milestones + cursos en orden). + NO incluir `meta.id` / `meta.version` en el frontmatter del MD — esos son + campos de identidad cmi5 que solo viven en `path.json` (el YAML de dojo-academy + los omite por contrato). + +6. Validar `path.json` con `ajv` si está disponible: + + ```bash + ajv validate -s ${CLAUDE_PLUGIN_ROOT}/assets/schemas/path.schema.json -d content/paths/{slug}/path.json --spec=draft2020 -c ajv-formats + ``` + + Si falla: report al usuario, NO marcar como done. + +7. Presentar resumen: + + > "Path generado en `content/paths/{slug}/`: + > + > - **{N} milestones**, **{M} cursos** referenciados (sin modificar) + > - **Credential**: {credential.cert_name} (OpenBadge 3.0) + > - **Level**: {meta.level} · **Reward**: {presentation.reward_points} pts + > - **xAPI identity**: `path:{slug}` + milestones `path:{slug}/milestone:` + > + > ✅ JSON válido contra path.schema.json + > ✅ IDs estables asignados (meta.id, milestones[].id) + > ✅ Contrato cmi5 por-curso INTACTO (ningún course.json tocado) + > {Si overlays aplicados: bullet list de SKILL.md path que corrieron + warnings} + > + > Próximos pasos sugeridos: + > - `/new-track {category}` — ver el catálogo + prerequisite graph + > - `/new-course {slug}` — crear un curso miembro faltante" + +**PUERTA DE APROBACIÓN**: confirmar output con el usuario antes de cerrar el flow. + +--- + +## Recursos + +### Esquemas +- `${CLAUDE_PLUGIN_ROOT}/assets/schemas/path.schema.json` — contrato de authoring del path +- `${CLAUDE_PLUGIN_ROOT}/assets/schemas/course.schema.json` — contrato cmi5 por-curso (NO se modifica) +- `${CLAUDE_PLUGIN_ROOT}/assets/schemas/overlay-protocol.schema.json` — contrato `OverlayInput`/`OverlayOutput` para el Paso 2 (Base + Overlay) + +### Runtime +- `${CLAUDE_PLUGIN_ROOT}/assets/runtime/overlay-protocol.md` — discovery + invocation + Layer 1 invariant validator + +## Principios clave + +- **Aditivo, no destructivo** — un path NUNCA modifica un `course.json`. +- **IDs son forever** — `meta.id` (`path:`) y `milestones[].id` son inmutables. +- **Identidad xAPI propia** — `path:` + `path:/milestone:`, sin colisión con IRIs de curso. +- **Credential = OpenBadge 3.0** — `credential.*` seedea el achievement template, NO un AU cmi5. +- **Consistencia con dojo-academy** — el frontmatter del MD matchea `schemas/path.schema.yml`; `path.json` agrega solo los campos de identidad cmi5. +- **Datos antes que visualización** — `path.json` es fuente de verdad, el MD es derivado. +- **No fabricar** — si un curso miembro no existe, marcarlo placeholder y mantener `draft`, no inventar.