docs(opts): handoff del eje de opts canonicas — el plan de manana

La forma de las opts es la ultima artesania masiva del catalogo: 2286
readableActive + 721 writableActive a mano en 593 wrappers, 524 interfaces con
93 re-declaraciones de id/ref, y dos helpers cuya adopcion es 3/101 CON cast en
el 100% de los adoptantes. El handoff lleva el censo re-medido HOY (no el del
agente de ayer), los 4 defectos de opts.ts con linea, las dos convenciones en
guerra, las SEIS decisiones que abren el eje (con recomendacion y sin firmar),
los movimientos M1-M4 con el orden (M1 primero: no depende de ninguna decision),
la linea base exacta post-endgame (check=75) y las trampas pagadas del eje
hermano. Deja claro lo que NO se toca: el mecanismo de dir ya es del morfo y la
clase base Provider se retiro a proposito.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent 6c65caf3f8
commit 30e2a027fe

@ -0,0 +1,275 @@
# CONTINUE — opts canónicas: la forma de las opts deja de ser artesanía
Handoff para una sesión nueva. **Leer entero antes de tocar nada**: el eje es
transversal (596 wrappers, 524 interfaces de opts) y la primera tarea NO es
código — son seis decisiones de diseño que están sin tomar (§4).
Contexto previo: este eje nació dentro del endgame de dirección
(`CONTINUE-direction-runtime.md` §11) y quedó **deliberadamente fuera** de aquel
plan con el porqué medido. La pregunta original del usuario: «el objetivo del
framework es que todos los componentes tengan las mismas mecánicas en todos los
aspectos» — la dirección ya lo cumple; la FORMA de las opts todavía no.
---
## 1. Por qué — los números (censo re-medido 2026-08-05, HEAD `6c65caf3f`)
| | veces |
| ------------------------------------------------------------------ | ---------- |
| Wrappers `.svelte` que llaman `Provider.create(` | **596** |
| …que construyen la bolsa A MANO | **593** |
| `readableActive(` escritos a mano en wrappers | **2 286** |
| `writableActive(` escritos a mano en wrappers | **721** |
| Interfaces `*Opts` en soma (raíces + partes) | **524** |
| …que re-declaran `id: Active<string>` a mano | **49** |
| …que re-declaran `ref: State<HTMLElement \| null>` a mano | **44** |
| Adopción de los helpers existentes (`OptsFromProps` + `bindProps`) | **3/101** proveedores (~3%) |
| Casts de escape en los 3 adoptantes | **3/3** (100%) |
Tres componentes usan los helpers (`button`, `switch`, `toggle`) y **los tres
necesitan un cast** para compilar (`as Parameters<typeof XProvider.create>[0]`;
`switch` además `as Omit<…, 'dir'>` + spread manual). Un helper cuyo 100% de
adoptantes lo esquiva con casts no es una abstracción — es una promesa rota.
La forma manual es invariante en los 593 restantes: un `readableActive(() =>
prop)` por clave no-bindable, un `writableActive(getter, setter)` por bindable
**con el callback disparado dentro del setter**, y `dir: activeDir(() => dir)`
para la dirección. Uniforme por disciplina, no por construcción — el mismo
estado en que estaba el estampado de `dir` antes del endgame (49 copias, 20
olvidos).
---
## 2. Lo que hay HOY, verificado con el código delante
### 2.1 · `src/uix/soma/provider/opts.ts` (115 líneas) — los helpers y sus 4 defectos
- **`OptsFromProps<P, Managed, StateKey>`** (líneas ~43-50):
1. **Clava `id`/`ref` obligatorios** — las raíces con `ProviderOpts` (ref
opcional/ausente: dialog:78, drawer:129, float-panel:119, popover:85,
toast:23 declaran `ref?:` inline) no pueden adoptarlo.
2. **`Exclude<P[K], undefined>` borra la opcionalidad** — la trampa está
DOCUMENTADA en su propio docblock (líneas 36-42: «intersect manually») y
es la que obligó a `switch` a declarar `dir` aparte. Cualquier prop cuya
ausencia signifique algo pierde esa semántica.
- **`bindProps(config): Record<string, Active<unknown>>`** (~101-114):
3. **Retorno deliberadamente perdido** — por eso el cast en 3/3 call sites.
El comentario de `PropsConfigEntry<T = any>` (~61-66) explica el `any`
por bivarianza en bolsas heterogéneas; ESA decisión es la que hay que
revisitar con mapped types.
4. **No expresa `dir`** — `activeDir` publica contexto (efecto), no es un
getter plano.
- **`WritableSpec<T>`** = `{ get, set }` estructural, sin marca nominal (para
no chocar con `$bindable`). Reutilizable en v2.
- Test existente: `src/uix/soma/provider/opts.svelte.test.ts` (4 casos:
getter→Active, spec→State, bolsa mixta, claves kebab). Base para ampliar.
### 2.2 · Las bases de interfaz — sanas pero no exigidas
`ProviderOpts { id; ref? }` y `WithRefOpts { id; ref }` en
`src/uix/soma/provider/provider.svelte.ts`. Importadas por 86/101 y 21/101
ficheros de provider. El reparto real de las 524 interfaces:
| forma | ~cuántas |
| ------------------------------------------------ | -------- |
| `extends WithRefOpts {}` (parte a pelo) | 212 |
| `extends WithRefOpts, ActiveProps<{…}>` | 166 |
| `extends WithRefOpts, StateProps … (± Active)` | ~58 |
| `extends ProviderOpts, …` | ~24 |
| sólo `ActiveProps`/`StateProps` + id/ref A MANO | ~22 |
| `interface XOpts {` sin extends | 32 |
Los 49 `id:` + 44 `ref:` re-declarados a mano son las 32 interfaces sin extends
más overrides sueltos — repiten VERBATIM lo que `WithRefOpts` ya dice.
⚠️ **Dos desviaciones de tipo REALES, investigar antes de tocar**:
`drag-drop-provider.svelte.ts:383` (`id: number`) y
`splitter-provider.svelte.ts:26` (`id: string`). Probablemente ids de DOMINIO
(no DOM) legítimos — si lo son, el veredicto es renombrarlos, no convertirlos.
### 2.3 · Las dos convenciones en guerra (el mismo concepto, dos formas)
- **Forma mano (593 wrappers)**: el setter del `writableActive` dispara el
callback — `(v) => { value = v; onValueChange(v); }`.
- **Forma bindProps (3 wrappers)**: el callback viaja APARTE como Active
readable (`onPressedChange: () => onPressedChange`).
Una de las dos sobra. La regla del proyecto es un nombre por concepto.
### 2.4 · El alias kebab→camel, sin puente
`'aria-label'` llega del consumidor en kebab; ~60 wrappers lo renombran a mano
(`'aria-label': ariaLabel`) y los proveedores NO coinciden en qué grafía
aterriza en opts (slider usa `ariaLabel` camel; otros pasan la kebab). Sin
convención declarada, cada componente decide.
### 2.5 · Lo que NO existe (verificado por búsqueda)
- Ningún helper que construya la bolsa entera desde `$props`
(`buildOpts|toOpts|makeOpts|createOpts|propsToOpts|optsFrom|asOpts` → 0).
- Ninguna clase base `Provider` (retirada a propósito — comentarios
«runtime-direct» en `toggle-provider.svelte.ts:39-52` y `dialog-provider`).
**No resucitarla**: la decisión fue explícita.
### 2.6 · Lo que el endgame de dirección YA resolvió — este eje NO lo toca
- El estampado y su gate viven en el MORFO (`direction: { parts }`) + tipo
condicional de `soma.runtime<M>`. **La presencia de `dir` en opts ya no es
la declaración de nada** — ese camino se evaluó y se descartó con evidencia
(el bag de opts perdía contra el morfo). No reabrir.
- `dir: activeDir(() => dir)` es UNA línea del wrapper con un EFECTO (publica
DirectionContext). Cualquier v2 tiene que decidir si la absorbe o la deja
explícita (§4 D3).
---
## 3. Qué está mal hoy, en una frase
**La bolsa de opts es la última artesanía masiva del catálogo**: 3 000+ líneas
idénticas a mano, dos helpers que nadie puede adoptar sin casts, dos
convenciones para el mismo concepto y 93 re-declaraciones de lo que la base ya
dice — uniforme por disciplina, que es exactamente lo que el eje de dirección
demostró que no dura.
---
## 4. Los movimientos — y las SEIS decisiones que los abren
⚠️ **P0 es decidir, no codificar.** Recomendaciones marcadas; ninguna está
firmada.
### D1 · ¿Inferencia total en `bindProps` v2?
La causa del cast es el retorno `Record<string, Active<unknown>>`. Un mapped
type lo arregla de raíz:
```ts
bindProps<C extends Record<string, PropsConfigEntry>>(config: C): {
[K in keyof C]: C[K] extends WritableSpec<infer T> ? State<T>
: C[K] extends () => infer T ? Active<T>
: never
}
```
El comentario de bivarianza (`opts.ts` ~61-66) explica por qué el v1 no lo
hizo — hay que probar que el mapped type no rompe las bolsas heterogéneas que
aquel `any` protegía. **Recomendación: sí, y es la pieza que decide si el eje
merece existir** — sin inferencia, v2 es v1 con otro nombre.
### D2 · ¿`id`/`ref` dentro o fuera del helper?
`OptsFromProps` los clava y por eso 15+ raíces no pueden usarlo.
**Recomendación: fuera** — el helper construye SOLO las claves del config; el
wrapper spreadea `id`/`ref` como hoy. Componer, no clavar.
### D3 · ¿`dir` como slot nativo o línea explícita?
`activeDir` publica contexto — no es un getter puro. Esconder un efecto dentro
de un helper genérico viola la regla de colocación del contrato de dirección
(«lee y publica en la init del wrapper»), y el morfo-gate ya hace imposible
olvidarla. **Recomendación: explícita, fuera del helper** (el precedente de
`switch` deja de necesitar el `Omit` en cuanto D1 dé inferencia).
### D4 · ¿Qué convención de callbacks gana?
**Recomendación: la forma mano** (setter dispara el callback) — es la de 593
wrappers contra 3, y la que los proveedores ya asumen (escriben en el State y
el consumidor se entera). v2 la expresa con `WritableSpec` tal cual; la forma
bindProps-v1 (callback aparte como Active) se retira en la migración de los 3.
### D5 · ¿Se deriva el TIPO de opts del config del wrapper, o sigue habiendo interfaz a mano?
La opción ambiciosa: `XOpts = ReturnType<typeof buildXOpts>` — el wrapper es
la fuente y `OptsFromProps` (con su trampa del `Exclude`) MUERE. La opción
conservadora: la interfaz sigue siendo la fuente y v2 sólo garantiza que el
config la satisface. **Recomendación: decidir con el piloto delante** — la
ambiciosa borra una clase de deriva (interfaz ≠ config) pero invierte la
dirección de lectura del código; hay que verla en un componente real antes de
firmarla.
### D6 · Estrategia de adopción
(a) sólo componentes nuevos + guía · (b) piloto medido y decidir · (c) barrido
de 593. **Recomendación: (b)** — migrar los 3 adoptantes rotos (quitándoles
los casts) + 3 representativos: uno con bindables y callbacks densos
(`slider`), uno con alias kebab (`toolbar` o `slider` mismo), una raíz
`ProviderOpts` sin ref (`dialog` o `popover`). Medir líneas antes/después y
legibilidad, y SÓLO entonces decidir si (c) merece sus riesgos. Un barrido de
593 wrappers con regex es exactamente la clase de operación que este eje ya
vio fallar cuatro veces.
### Los movimientos, si las decisiones acompañan
- **M1 — La base exigida (S-M)**: regla «ninguna interfaz de opts re-declara
`id`/`ref`; toda parte extiende `WithRefOpts`/`ProviderOpts`» + guard estilo
censo (fs-scan, como `direction-census.test.ts`) que cace las 93
re-declaraciones y las funda. Independiente de D1-D6; se puede hacer primero
y solo.
- **M2 — `bindProps` v2 (M)**: D1+D2+D3+D4 hechos código, con el test de
opts.svelte.test.ts ampliado (inferencia, WritableSpec, alias si D5 lo trae,
y el NEGATIVO: una prop cuya ausencia significa algo conserva su
`undefined`).
- **M3 — Piloto (M)**: los 6 de D6(b), con medidas. Cast count 3→0 es el
criterio duro de éxito.
- **M4 — Veredicto y doctrina (S)**: decidir (a)/(c) con los números del
piloto; escribir la regla en `component-guide.md`; retirar `OptsFromProps`
si D5-ambiciosa ganó (sin shim — actualizar los 3 consumidores y borrar).
---
## 5. Orden, y por qué
**M1 primero** — no depende de ninguna decisión, borra 93 duplicaciones y da
un guard. Después **P0 (las seis decisiones) → M2 → M3 → M4**. Si las
decisiones se atascan, M1 ya habrá pagado la sesión.
---
## 6. Verificación — la línea base exacta (medida 2026-08-05 tras el endgame)
| | valor esperado |
| ------------------------------------ | --------------------------------------------------------------------- |
| `npx svelte-check --threshold error` | **75** (bajó de 77 al cerrar chat-log; medir DOS veces — hay carreras con la otra sesión) |
| `npm run rtl:check` | **1 error** — `palabras-chrome.css:446`, preexistente y excluido |
| `npm run docs:check` | 0 errores sobre 567 docs |
| `npm run test` (pasada completa) | los fallos conocidos: 6 de `contracts.test.ts` (preexistentes, A/B'd) + `soma-attr-audit` flaky bajo carga (pasa aislado) |
| Casts de escape en adoptantes | **3** hoy → **0** es el criterio de éxito de M3 |
Los tests de los 3 adoptantes (`toggle`, `switch`, `button`) y
`opts.svelte.test.ts` son la red del piloto — correrlos por ámbito tras CADA
fichero migrado, no al final.
---
## 7. Trampas ya pagadas — no volver a pisarlas
- **`git commit` SIN pathspec en esta rama se llevó el índice de la otra
sesión una vez.** SIEMPRE `git commit -- <rutas>`, y verificar «ajenos: 0»
en el commit antes de seguir.
- **Los barridos con regex mintieron 4/4 en el eje hermano** y el de 4.1 dejó
12 componentes mal por no cubrir las DOS formas sintácticas de un mismo
call site. Para 593 wrappers: mapa por fichero + tests de ámbito tras cada
grupo, o no hacerlo.
- **Jamás `prettier --write` masivo** — reformatea líneas preexistentes y
esconde tu diff (medido: un README pasó de +7/−4 a +49/−46). Formatear sólo
lo ensuciado y mirar el TAMAÑO de la diff después.
- **Detectores de identificadores**: excluir las líneas de import antes de
contar usos (la ruta `core/soma.svelte` contiene «soma» y envenena el grep).
- **Un censo de handoff se re-mide antes de usarlo** — dos veces en este eje
la lista escrita estaba rancia (§2.3 del handoff de dirección, y el censo D4
inflado). Los números de §1 son de HOY; mañana vuelven a medirse.
- **`svelte-check` con la otra sesión viva da falsos deltas** — medir dos
veces antes de perseguir un ±1.
---
## 8. Estado del árbol al escribir esto
Rama `alpha-0.1-dir-prefs`, remoto `gita` (no hay `origin`), **pusheada hasta
`6c65caf3f`** — el endgame de dirección entero (P1–P5 + censo D4) está dentro.
La sesión de media-player sigue VIVA en esta rama: verificar `HEAD` al
arrancar. Sin tocar y sin commitear nunca: `.claude/settings.local.json`,
`src/arts/adom/__scratch-verify.ts`, `web/routes/alpha/`.
**Kickoff sugerido**: _«Lee `docs/process/CONTINUE-opts-canonicas.md` entero y
empieza por M1; las decisiones de §4 me las presentas juntas antes de M2.»_
Loading…
Cancel
Save

Powered by TurnKey Linux.