# STUMBLES — dónde tropieza un agente construyendo el Knob solo con docs Este es el entregable real del ejercicio. Cada punto es un lugar donde tuve que adivinar, donde dos docs se contradicen, o donde falta la pieza que un agente necesitaría para producir código correcto al primer intento. Ordenado por impacto. ## Estado de resolución (actualizado 2026-07-03) El Knob del ejercicio **ya está construido de verdad** en las 5 capas (morfo/langs/sema/soma/eidos, `c13cad74`) con demo (`8dfd3505`) — lo que valida el ejercicio: 8 de 9 cerrados; de **#7** (contrato CSS-vars soma→eidos) se cerró la dirección recipe→tema con un guard (opción C, cazó 3 fantasmas reales), y queda solo el sub-contrato provider→recipe (opción A, opcional). | # | Tropiezo | Estado | Cómo | |---|---|---|---| | 1 | Vocabularios invisibles | ✅ RESUELTO | `npm run docs:vocabularies` → `docs/canon/vocabularies.md` desde los consts + guard docs-check I7 (`116594e0`) | | 2 | Drift `kind` public/virtual vs internal | ✅ RESUELTO | `MorfoPartKind = 'public' \| 'private' \| 'virtual'` (types.ts) y checklist A-2.2 alineado | | 3 | No hay capa de gesto radial | ✅ RESUELTO | `Gesture.rotate` 4ª especialización en `soma/layers/gesture` (`18b0d44e`); el Knob lo consume | | 4 | Doctrina `trigger()` continuo sin cerrar | ✅ RESUELTO | sección `## Continuous components` en `sema.md` (`1da36ca6`), fact-check adversarial | | 5 | Falta condición `part-absent` | ✅ RESUELTO | `{ when: 'part-absent', part }` en morfo (`11504043`); el morfo del Knob la usa | | 6 | Formato de `langs/components/{kebab}.ts` | ✅ RESUELTO | forma `LangNode` (`{ key: { es, en } }`, anidable, `satisfies`) documentada en morfo/soma/checklist | | 7 | CSS-vars del provider sin contrato | 🟡 CASI | dirección **recipe→tema** guardada (opción C: guard de fantasmas de tema en `recipe-css-contract` — pilló 3 bugs reales); queda solo el sub-contrato **provider→recipe** (opción A, opcional) | | 8 | Docs imprescindibles fuera del paquete | ✅ RESUELTO | `component-audit.md §0` lista el paquete mínimo como archivos exactos | | 9 | Fricciones menores (docs) | ✅ RESUELTO | `### Authoring notes` en `soma.md` §6: `state()` vs `$state`, `role` opcional en Provider, `Without<>`/`PrimitiveDivAttributes`, ownership de pointermove/up del gesture | ## 1. Vocabularios canónicos invisibles desde los docs `ARCHETYPE_VOCABULARY` vive solo en `types.ts` y los docs prohíben explícitamente copiar la lista ("a copied list survived at 24 entries while the code grew to 26"). Correcto contra el drift humano — pero un agente que trabaja desde el docs-book **no puede asignar archetypes**: no sé si existe `control`, `handle` o `indicator` para las partes del Knob, y A-2.2 los pide en *todas* las partes (error-level en el audit). Lo mismo pasa con `SEMA_MAP.families[*].hold` (¿cuánto dura el hold de `handle`?), los `kind` categóricos del HapticChannel, y el contenido de `commonLangs`. **Fix sugerido**: apéndice *generado desde código* en el docs-book (`npm run docs:vocabularies`). Generado = sin objeción de drift. Para un agente, ese apéndice es la diferencia entre declarar y adivinar. ## 2. Drift entre docs: `kind: 'public' | 'virtual'` vs `'public' | 'internal'` `morfo.md` define `kind` como `'public' / 'virtual'`; el completion-checklist A-2.2 dice `kind: 'public' | 'internal'`. Uno de los dos miente. Un agente elegirá según cuál doc leyó último. También: A-2.1 exige que el Provider lleve `archetype: 'provider'`, pero morfo.md dice que cuando el Provider ES el elemento interactivo (Toggle, Switch) su archetype es `'trigger'` — tal como está escrita, la regla del audit marcaría error en Toggle. ## 3. No existe capa de gesto radial — gap de framework, flaggeado `layers/` tiene `Gesture.base/drag/resize`, `AxialDrag`, `ZoomPan` — nada angular. Siguiendo guide §4 ("flag the gap, never inline bespoke"), lo correcto es proponer `Gesture.rotate` (centro + sweep + wrap + detents + velocity angular). Lo reutilizarían: AngleSlider, color wheel (hue ring), un time-picker de esfera, y este Knob. De momento la matemática vive en el provider con un comentario FLAGGED GAP. ## 4. La doctrina de `trigger()` para gestos continuos está sin cerrar El guide clasifica la forma "Continuous" (start/commit boundaries) pero no hay ejemplo obrado de `handle` + gesture + `trigger()`. Preguntas que no pude resolver desde los docs: - Con `sequence: 'post'`, ¿`trigger()` sigue reteniendo el turno hasta que el hold del canal visual termina (aunque el handler ya corrió)? Para `handle-pick` en un pointerdown eso importa: si el `await` interno vive en el mismo turno del gesto, hereda el problema del checkbox-lag. - ¿O el camino santo para pick/drop es `provider.emitEvent()` (fire-and-forget) en vez de `trigger()`? El guide dice "the provider must route semantic actions through runtime.trigger(...)" — que parece prohibir emitEvent para acciones perceptuales. Slider ya resolvió esto en código; el doc no lo cuenta. - ¿`commit-set` en cada paso de teclado satura el SoundChannel si el usuario mantiene pulsada la flecha? ¿Hay throttling doctrinal o es responsabilidad del pack sema? Ningún doc lo dice. **Fix**: un párrafo "Continuous components" en sema.md con Slider como ejemplo obrado, igual que overlays tienen su callout de `sequence: 'post'`. ## 5. Falta la condición `part-absent` en morfo El patrón ARIA más común del mundo — "aria-label solo cuando no hay Label" — no es declarable: las conditions son `always / part-present / state-equals / prop-truthy / prop-falsy`. Tuve que declarar `aria-label` como optional y dejar la exclusividad en lógica del provider, que es exactamente el tipo de contrato-fuera-del-morfo que el framework existe para eliminar. ## 6. El formato del catálogo `langs/components/{kebab}.ts` nunca se muestra Todos los docs referencian su *ubicación* (morfo.md, soma.md, guide A3, checklist A-1.3) pero ninguno enseña su *forma*: ¿export nombrado?, ¿record `{ key: { en, es } }` o `{ en: { key } }`?, ¿claves anidadas o planas con puntos? Mi `knob.ts` de langs es una conjetura. A-1.3 es error-level: un agente fallará el audit por un archivo cuyo formato no está documentado en el paquete que le diste. ## 7. Las CSS custom properties del provider no tienen contrato `--knob-progress` / `--knob-angle` siguen el patrón de `--drawer-progress`, pero ese patrón vive solo en prosa (SOMA_ARCHITECTURE §9). El morfo no modela CSS vars, eidos-lint no las verifica, y el recipe-css-contract solo cubre los alias de `EidosConfig.recipes`. Es la única superficie soma→eidos sin defensa contra drift: renombrar `--knob-angle` en el provider rompe el recipe en silencio. Candidato a extensión del morfo — y pasa la regla 2-de-3 (soma escribe, eidos consume, sema podría leerlas en cascade selectors vía `style` no, pero soma+eidos ya son 2). > **Confirmado al construir el Knob (2026-07-03).** El recipe del Knob consume > `--knob-progress`/`--knob-angle` (que el provider publica) y además heredó de > la plantilla de Fable **tokens de tema fantasma que no existen** — > `--color-neutral-content`, `--state-hover`, `--color-neutral-bg` — que el CSS > tragaba en silencio (fallback vacío / pointer transparente) y solo se > detectaron **mirando el render**, no por ningún guard. Es exactamente el > "drift silencioso" que este tropiezo predijo, en dos direcciones: vars > provider→recipe **y** recipe→tema. Diseño abierto (S5): declaración `cssVars` > en el morfo validada contra escrituras del provider + lecturas del recipe, o > un cross-check de `eidos-lint` de vars escritas-por-provider vs consumidas. > Decidir antes de implementar. > > **Cerrada la dirección recipe→tema (opción C, 2026-07-03).** Guard nuevo en > `recipe-css-contract.test.ts`: toda `var(--x)` **sin fallback** de un recipe > debe resolver a un token declarado en algún sitio del árbol CSS de eidos > (foundation + capas + auto-declaraciones del componente); las `var(--x, > default)` son runtime-opcionales (vars provider/floating con default) y quedan > exentas por construcción. El guard cazó **3 fantasmas reales** además de los > del Knob — `card-group --color-content-default`→`-primary`, `link-preview > --leading-body`→`-normal`, `textarea --font-family-body`→`-primary` — > arreglados. Queda solo la dirección **provider→recipe** (opción A): declarar > las vars del provider en el morfo (`cssVars`) o un grep-cross-check. ## 8. Dos docs imprescindibles no estaban en el paquete `demo-authoring.md` (el template de 9 tabs está "locked" y es error-level en D-1.x — imposible producir un demo conforme sin él) y `CANON.md` (sema.md se declara subordinado a él: "if they ever disagree, the code + canon win"). Un agente con este paquete produce las 4 capas pero no el demo, y no puede verificar que el vocabulario que usó es el vigente. Para briefs de agentes, el "paquete mínimo" debería estar definido en el propio component-audit.md §0 como lista de archivos, no de títulos. ## 9. Fricciones menores - **`state()` vs `$state`**: el sistema reactivo documenta `state(initial) → State` pero los ejemplos de provider usan `$state` a secas para campos internos. ¿Cuándo cada uno? Deduje que `State` es para pasar por referencia (labelId) y `$state` para campos locales, pero es deducción, no doc. - **`role` en el Provider raíz con DOM**: el ejemplo de Dialog tiene Provider virtual sin role; para un Provider-contenedor con DOM no hay guía de si omitir `role` es válido o si el campo es requerido por el schema. Lo omití. - **`Without<...>` / `PrimitiveDivAttributes`**: nombres citados en ejemplos pero sin definición visible; los usé por imitación. - **El wrapper de gesture**: A15 dice que `.props` del gesture es "only `onpointerdown`" — pero ¿quién registra pointermove/up globales, el layer vía `dom.listen` o el provider? Asumí el layer. ## Qué funcionó notablemente bien Para ser justos, la mayor parte del camino estuvo pavimentada: el morfo fue casi mecánico de escribir (los builders `v.*` + las conditions cubren el 90% del contrato), `SEMA_VERBS` tenía exactamente los verbos que necesitaba (`handle.rotate` existe, `commit.set` existe — el vocabulario del libro aguanta un componente que no existía), la política de intent forzó una decisión de diseño buena (¿qué intent lleva el commit de un knob? → prop `intent`, y de ahí salió el caso de uso del gain a 0dB), y las reglas A30/A31/A35/A36 me previnieron de tres bugs de reactividad que habría escrito con toda seguridad. El framework corrige al agente. Ese era el punto.