You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
13 KiB
185 lines
13 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ó)? 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.
|
|
|
|
> **Anotación 2026-08-26 — el renombre ocurrió, y el tropiezo seguía teniendo
|
|
> razón.** El codemod de los 14 canales de valor renombró las cuatro a
|
|
> `--_knob-{progress,angle,start-angle,sweep}` (más otras nueve de otros seis
|
|
> componentes). No rompió nada porque el flip fue ATÓMICO y guiado por un
|
|
> barrido `git ls-files` de cada nombre por todo el árbol trackeado — es decir,
|
|
> **por disciplina de procedimiento, no porque un guard lo cubriera**. Sigue sin
|
|
> haber defensa automática: el `recipe-css-contract` ve el nombre PÚBLICO fuera
|
|
> de contrato (ley del espacio cerrado, 2026-08-26) pero no cruza escrituras del
|
|
> provider contra lecturas de la receta, así que un `--_knob-angle` mal escrito
|
|
> en un lado sigue siendo silencio. El diseño abierto S5 queda igual de abierto.
|
|
> 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).
|
|
>
|
|
> **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 por `var()` 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**: declarar
|
|
> `cssVars: [...]` en el morfo (fuente de verdad de qué vars SON el contrato) +
|
|
> guard "el provider escribe cada `cssVar` declarada" (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 documenta `state<T>(initial) → State<T>` pero los ejemplos de provider usan `$state` a secas para campos internos. ¿Cuándo cada uno? Deduje que `State<T>` 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.
|