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.
svelte-kit-vice/STUMBLES.md

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.

Powered by TurnKey Linux.