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/docs/process/CONTINUE-opts-canonicas.md

16 KiB

CONTINUE — opts canónicas: la forma de las opts deja de ser artesanía

✅ EJE EJECUTADO Y CERRADO — 2026-08-05

Este documento es HISTÓRICO. No lo ejecutes: ya está hecho. Se conserva por el porqué de cada decisión, no como plan pendiente. Las seis decisiones de §4 están tomadas y los cuatro movimientos, ejecutados en seis commits (337dde857 · 0ea2c22f3 · 26cf2a22f · aea2c9f90 · 46ce55783 · 095841ca4).

lo que decía el plan lo que pasó
M1 — «93 re-declaraciones» 49 interfaces (el 93 contaba líneas de grep) fundidas en WithRefOpts/ProviderOpts, + guard provider/opts-census.test.ts
D1 — ¿inferencia en bindProps v2? Ni inferencia ni retirada: TARGET-TYPED. bindProps<O>(config: ConfigFor<O>): O — la interfaz declarada computa el tipo del config. El spike probó que las variantes por inferencia dejan el cuerpo del setter en any
D2 · D3 id/ref fuera del helper genérico (los cubre partOpts); dir explícito en la init del wrapper y dentro de la bolsa por pass-through de Active
D4 — convención de callbacks Dos conceptos, no dos convenciones: bindable→setter · evento puro→aparte · y una tercera nombrada, el debounce (search-field). 16 parejas migradas; 6 doble-disparaban
D5 OptsFromProps SOBREVIVE con un 4º parámetro Preserve (el undefined significativo)
D6 Adopción total: partOpts en 288 wrappers triviales; casts de bolsa 3 → 0

Lo que quedó como doctrina viva está en guides/component-guide.md §Wrapper Pattern

  • §Callback conventions (incl. el invariante de catálogo: ninguna escritura interna en silencio).

Los números de §1 son del estado ANTERIOR al eje. No los uses como censo.

Handoff original (histórico). El eje era transversal (596 wrappers, 524 interfaces de opts) y la primera tarea no era código — eran seis decisiones de diseño 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:

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

Powered by TurnKey Linux.