diff --git a/STUMBLES.md b/STUMBLES.md new file mode 100644 index 000000000..2322f3195 --- /dev/null +++ b/STUMBLES.md @@ -0,0 +1,148 @@ +# 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: 7 de los 9 tropiezos se cerraron; quedan 2. + +| # | 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 | ⏳ PENDIENTE | **diseño abierto** — ver abajo | +| 8 | Docs imprescindibles fuera del paquete | ✅ RESUELTO | `component-audit.md §0` lista el paquete mínimo como archivos exactos | +| 9 | Fricciones menores (docs) | ⏳ PENDIENTE | 4 notas doc por escribir — ver abajo | + +## 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. + +## 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.