11 KiB
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<T>() 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ó)? Parahandle-picken un pointerdown eso importa: si elawaitinterno 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 detrigger()? 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-seten 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óncssVarsen el morfo validada contra escrituras del provider + lecturas del recipe, o un cross-check deeidos-lintde 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: todavar(--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); lasvar(--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).A2 (grep) descartado — probado ambiguo (2026-07-03). Un
var(--{c}-x)que el recipe lee es indistinguible entre (1) var funcional escrita-por-provider y (2) alias override-por-consumidor: idénticas en el CSS. Y muchos providers publican vars como hook de API que el recipe por defecto NO consume (--drawer-progress,--dialog-depth,--toast-swipe-*,--scroll-area-*— 17, ninguna leída porvar()en componentes). Así que ni "provider escribe → recipe lee" ni el inverso distinguen un rename de un hook legítimo; un grep cross-check da ~17 falsos positivos. Solo A1 lo cierra: declararcssVars: [...]en el morfo (fuente de verdad de qué vars SON el contrato) + guard "el provider escribe cadacssVardeclarada" (un rename deja de escribir la declarada → se detecta). Coste: morfo types+schema+compile + declarar en cada morfo con vars publicadas + guard. Pendiente de decisión.
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<T>()vs$state: el sistema reactivo documentastate<T>(initial) → State<T>pero los ejemplos de provider usan$statea secas para campos internos. ¿Cuándo cada uno? Deduje queState<T>es para pasar por referencia (labelId) y$statepara campos locales, pero es deducción, no doc.roleen 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 omitirrolees 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
.propsdel gesture es "onlyonpointerdown" — pero ¿quién registra pointermove/up globales, el layer víadom.listeno 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.