|
|
|
|
@ -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.»_
|