45 KiB
CONTINUE — tier blocks (handoff, act. 2026-08-17)
Estado: F1 CERRADA (8/8) · F2 CERRADA EN UNIDADES (15/15 — la página compuesta que su cierre exigía NO se construyó; vive ahora en F2b tramo D) · saneamiento CERRADO ENTERO · fase 5 (guard endurecido) HECHA · fase 7 (contraste doctrinal) CERRADA 15/15 · renombrado del eje de tamaño HECHO · A-99/A-98/A-93/A-68 ARREGLADAS · F2b (ola de variedad, 4 tramos) FIRMADA 2026-08-17, SIN EJECUTAR.
Por dónde entrar mañana: §Qué queda, por orden. Dos frentes: la ola F2b (grupo d — se empieza por V4) y las 19 filas CONFIRMADO de 96, ninguna bloqueante salvo las tres que dependen de decisión tuya o de una pieza sin construir. Los 15: site-header · hero · feature-grid · feature-split · pricing · testimonials · faq · stats-band · cta · newsletter · site-footer · banner · team · contact · content-section.
⚠️ LO PRIMERO: la fuente viva es el LEDGER, no el documento del 2026-08-01
docs/process/AUDIT-blocks-ledger.md — filas con id estable desde A-01.
El documento del 2026-08-01 se conserva (sus reclamaciones y evidencias son
válidas y el ledger las indexa) pero su clasificación NO vale: tenía los
veredictos desemparejados de sus hallazgos por un join por POSICIÓN, y el journal
del workflow era de sesión. Se reprodujo en vivo al re-verificar — el journal
devuelve los resultados en un orden distinto al de entrada.
Estado del ledger (2026-08-17): 64 ARREGLADO · 19 CONFIRMADO · 13 REFUTADO (96 filas: las 90 originales + A-91…A-99, nacidas del contraste doctrinal). Cero pendientes: todas están medidas. La tasa real de refutación fue del 13%, no del 28%.
La regla que hay que respetar al escribir en él: unión por id, SIEMPRE.
Ningún proceso vuelve a casar dos listas por posición. Un verificador que no
devuelve su id no escribe fila.
El hallazgo grande de la re-verificación
De las 12 ALTA de percepción, 8 tenían la causa raíz en el CANON, no en los blocks. Los blocks componen bien; lo que estaba roto era el arco perceptivo del canon. Todos arreglados ya (ver abajo), pero la lección se queda: cuando una auditoría de un tier acusa al tier, comprueba en qué capa vive el defecto.
Lo hecho en la sesión del 2026-08-06 (commit 866089a40)
Canon — Link (la prop color era inerte en subtle/plain, el hover
clavaba primary, el :active quedaba tapado) · Card (prometía BoxProps sin
aplicarlos: height="100%" era un atributo inerte; ahora el tipo dice la verdad y
el eje de tamaño va por buildSizeStyle, helper compartido) · Form.Submit y
Form.Reset (componen el Button del canon, así que la acción principal de un
formulario recupera el contact-activate en el gesto; y el aria-label genérico
deja de pisar el texto propio — WCAG 2.5.3 para todo consumidor, verificado con el
AX tree de Chrome) · Field (dejaba de duplicar commit-submit dentro de un
Form) · NavigationMenu (el commit-select se mueve al Link, el despliegue
habla como emerge-open/close en vez de fingir una selección por hover, y el pack
casa por fin con la parte que recibe el estampado — antes sonaba a la ganancia
base, 10× lo diseñado) · Badge (el ✕ compone IconButton; la altura del chip
pasa a ser la del control) · la fundación: [data-on] arrastra la propiedad
color, no solo las variables.
Blocks — hero 1.61:1 → 6.61:1 · contact separa incomplete de invalid (el
camino de error por campo era inalcanzable por construcción) y su frase llega a la
AT · site-header ya no congela la página al cruzar el breakpoint · site-footer
emite su commit · pricing no se vacía · cta llega a sangre de verdad · newsletter
COORDINA.
Doctrina — la frontera dura 1 nombra $libs/forms; el contrato B admite un
segundo servicio, el anunciador (uix.announce): un block que posee las
palabras de sus estados tiene que poder decirlas.
Fase 7 — contraste doctrinal de los 15 — CERRADA 15/15
Encargo del usuario: contrastar cada block con la documentación del sistema a nivel de cada componente que compone y en conjunto, siguiendo la guía de creación de blocks y con la composición como estrategia válida, comprobando las directrices de eidos, sema, morfo y soma — más motion, que faltaba en la rejilla y detectó él preguntando si había leído su doctrina. No la había leído; el primer veredicto sobre coreografía salió sin ella y hubo que retirarlo.
Detalle completo, rejilla de siete ejes y fichas: AUDIT-blocks-ledger.md
§Fase 7. Aquí sólo el estado y cómo seguir.
⚠️ Lo primero: el instrumento mentía
Las mediciones en navegador del 2026-08-09 se hicieron contra un dev server con caché de Vite anterior a arreglos ya commiteados. Al re-medir con servidor limpio cayeron A-36, A-69 y A-85. El delator fue aritmético: las duraciones medidas (7867/3567 ms, ratio 2.21) eran EXACTAMENTE las que el comentario del arreglo cita como estado anterior. Servidor recién arrancado antes de medir, y si un número reproduce el «antes» documentado con demasiada exactitud, sospechar del instrumento antes que del código.
Estado: los 15 con la rejilla completa
Ninguno de los 15 tiene un defecto de composición. Componen canon y lo componen bien; todo lo que salió vive una capa más abajo (canon) o en las FICHAS del propio ledger. Ese es el resultado de la fase, y conviene no perderlo: la auditoría original acusaba al tier.
⚠️ Cómo se llegó aquí importa, porque me equivoqué en el camino: durante días
declaré «conformes» siete blocks de los que sólo había medido UN eje (dónde cae
el {...rest} y quién estampa data-stagger). Un barrido no es la rejilla.
Se corrigió en el ledger y en este handoff cuando él preguntó «¿ya has
terminado?». Lo que el contraste produjo:
- Cinco disposiciones corregidas — A-55 (mecanismo falso: el desvío lo causa
el ancho que resta el
Close, no unos márgenes auto que miden0px), A-09 (su arreglo apagaría la detección live), A-68 (no es arreglable desde el block sin romper B-6), A-60 (correcta, y con el camino que nombra el propioAutoGrid), A-61 (su segunda mitad ya la cerróuix.announce). - Tres filas nuevas — A-91 (
bannerhereda el ejeintent/colorsin registrarlo), A-92 (una restricción del modelo de cascada que vive en un comentario depricingy falta en §D.13 de la doctrina de motion), A-93 (Motionno expone su momento «visto», que es lo que desbloquearía A-68).
⚠️ Al retomar: la composición es estrategia válida (norma del usuario). Un
block que sólo coloca canon no es defectuoso por serlo; sólo lo es si al componer
se salta un contrato o reinventa un primitivo. Y el eje motion se contrasta con
§D.11 (la cascada ES [data-stagger] + preset + regla de fundación — modelo
CERRADO, no un atajo), §D.13(1) hijos directos, §D.13(2) ENTER-ONLY, y §2
KNOWN-FRAGILE (firma y stagger sobre UN mismo nodo se pisan).
Lo hecho en las sesiones del 2026-08-16/17 (commits 4caf1100a, d02021aee)
1 · El eje de tamaño dice de QUIÉN es (4caf1100a)
size significaba dos cosas en el tier: el aire de la Section en trece blocks
y la altura de la tira en banner — misma prop, distinta escala (BannerSize
no tiene xl). Renombrado: size→**sectionSize** (13), container→
containerSize (14), minColumnWidth→**minChildWidth** en site-footer.
size queda libre y sólo lo usa banner, donde SÍ es el tamaño del block.
La regla vive ya en architecture/blocks.md §Conventions: un block que
reenvía el eje de tamaño de una pieza interna lo nombra {pieza}Size, y un eje
que el canon ya nombra se reenvía CON SU NOMBRE.
Método que funcionó y conviene repetir: renombrar los TIPOS primero y dejar
que svelte-check señale cada consumidor. Cazó los cinco call sites que
quedaban. ⚠️ Y cazó un error mío: el primer patrón era demasiado ancho y renombró
size en siete sub-partes donde ese size es el del Card o el Text que
envuelven. Revertidas antes de seguir.
2 · Dos animadores que no se hablaban y un cluster que no envolvía (d02021aee)
A-99 + A-98 — Group no aplicaba NINGUNO de los dos defaults que su README
promete (align: center, wrap: wrap): nunca se los pasaba a Flex, que cae
a nowrap+stretch. De ahí que las acciones de hero no apilaran a 375px.
Arreglado en group.svelte con el mecanismo que ya usaba para direction, y con
attached invirtiendo a nowrap (el recipe cuadra radios y solapa bordes
asumiendo UNA línea). El censo decidió la forma: ButtonGroup es el único
Group en columna de todo el ecosistema, así que su align="stretch" vive en
su call site y no como default condicional. Medido: hero pasa a 2 líneas y la
acción secundaria de 124px truncados a 156,9px.
A-68 + A-93 — los contadores corrían detrás de opacity: 0. CountUp trae
su propio IO (threshold 0) y Motion usa otro (0.1 / −10%); un IO no mira la
opacidad. Motion publica ya su momento visto por contexto
(motion/context.ts, calcado de cascade/context.ts) y stats-band-value lo
pasa a startWhen. Medido en la ventana que discrimina (banda a 6px dentro del
viewport): contadores a 0/0/0 en la llegada y tras 2,6 s, donde antes acababan
en 12.121/330/47. Al revelarse, la primera cifra al 0% — antes 97,8%. A-69
intacta: aterrizaje a 1781 ms, dispersión 0.
⚠️ Residuo documentado: las cifras que el stagger revela después llegan al 16,8% y 35,4%. Estimé «≤10%» y me equivoqué — el muelle es sobreamortiguado y cubre mucho recorrido al principio. Es el precio del aterrizaje conjunto que A-69 firmó; no baja sin reabrirla.
3 · Tres errores de método de estas sesiones (no los repitas)
- Medir un default en una demo que lo pisa no es medir un default. Escribí
en la ficha de A-99 que «el
alignsí se cumple, luego el recipe carga». Elcentersalía del control de la propia demo, que lo pasa explícito. - El panel del navegador oculto suspende el IntersectionObserver, no sólo
el rAF. Una primera medición de A-68 dio «contadores congelados» y era la
suspensión, no el arreglo: con el panel oculto el IO de
CountUptampoco dispara, así que el resultado no distinguía una cosa de la otra. La vía fiable es Playwright headless (como las fichas originales), resolviendoplaywrightdesde elpackage.jsondel repo si la sonda vive en el scratchpad. Motionesonce: true. Empujar la banda bajo el pliegue DESPUÉS de cargar no sirve: su observador ya disparó. El espaciador tiene que existir en el primer pintado (addInitScriptcon una hoja de estilo).
Lo hecho en la sesión del 2026-08-09 — fase 5, el guard endurecido
blocks:check deja de vigilar sólo la forma del tier y pasa a vigilar su
frontera, su documentación y su catálogo. Tres reglas, cada una con fixture
negativo en el selfTest():
- Lista blanca de importaciones (B-4) — la frontera dura 1 era prosa; ahora
es código. Permitido:
$uix, los arts públicos,$libs/forms,svelte/svelte/elementsy los relativos del propio block. Los arts se DERIVAN desrc/arts/*, no se listan a mano — una lista escrita se queda atrás el día que aterriza un art (y este proyecto ya pagó dos veces esa lección). El tier ya cumplía: cero violaciones al encenderla. Cierra la disposición de A-05. - README completo (B-9) + declaración de landmark (B-8) — las cuatro
secciones de la plantilla y un párrafo
**Landmark + headings**. Al encenderla salieron 7 de 15 sin declaración: los tres de A-14 másbanner,content-section,cta,newsletterysite-footer, que nombraban «landmark» en una celda de tabla pero no declaraban nada. Escritas leyendo la fuente de cada uno. A-14 → ARREGLADO. - Ficha en
_lib/catalog.ts(B-9) — el árbol y el catálogo tienen que coincidir en los DOS sentidos: un block que el catálogo no publica es invisible para el raíl, y un slug publicado sin block detrás es un enlace muerto.
De paso, el escáner deja de leer los comentarios como código: los index.ts
documentan su uso con un // import { Cta } from '$blocks/cta', y una lista
blanca que lee prosa habría empezado a acusar a los ejemplos. (No era teórico:
esos 15 comentarios ya entraban en el escaneo de B-10 y sólo se salvaban porque
cada ejemplo cita su propio block.)
Verificado en ROJO sobre el árbol real, no sólo contra los fixtures — que es
la única prueba de que una regla nueva no es decorativa: un zod y un
$libs/days metidos en hero/types.ts salen con su línea exacta mientras el
mismo import dentro de un comentario NO salta; el catálogo falla en las dos
direcciones (shipped: false sobre un block vivo, slug fantasma); y al romper
isAllowedSpec a propósito el self-test aborta el guard en vez de dar verde.
Qué queda, por orden
Las 19 filas CONFIRMADO vivas, agrupadas por lo que hace falta para cerrarlas. Cada una lleva mecanismo y evidencia en su ficha del ledger — léela antes de re-medirla (perdí una sesión re-midiendo A-67 sin leer la suya, que ya tenía el análisis completo).
a) Bloqueadas por decisión TUYA — no las toco sin tu firma
| Fila | Qué hay que decidir |
|---|---|
A-67 faq |
Dos earcons simultáneos al cambiar de pregunta. Puerta ya elegida en la ficha: un nuance emerge-collapse-swap (morfo + provider + pack). Cae en el eje de sema, así que se ejecuta en una sesión suya, no aquí. |
A-47 cta (ALTA) |
Anillo de foco. Por doctrina es un eje de configuración: se endurece con un VALOR en color.focus.ring, «never a per-component CSS change» — que es justo lo que hacía el intento revertido en e468e764b. |
A-09 stats-band |
Su disposición APAGARÍA la detección live: el contrato exige value para detectarla. El defecto real es el anuncio crudo en metrics-value.svelte:40, que es canon. |
b) Bloqueada por una pieza que no existe
- A-95
banner— conaffix="top"la tira tapa la cabecera pegada. No se arregla en ninguno de los dos blocks: falta la pieza que POSEE las alturas de página. Mantine lo resuelve en suAppShell(header={{ height }}); nuestro equivalente es el blockapp-shell, F3.1, sin construir. El plan ordena ya F3 conapp-shellPRIMERO por esto (y porque F4.1 lo especializa); la página compuesta (F2b tramo D) es donde A-95 se re-mide.
c) Las 15 restantes — trabajo normal
A-11 site-header · A-29 pricing+testimonials · A-48 banner · A-53 hero ·
A-55 banner · A-60 site-footer · A-61 contact · A-62 team · A-65
pricing · A-71 site-footer · A-74 feature-split · A-75 pricing · A-79
site-footer (SOSP) · A-92 pricing · A-96 banner.
⚠️ Dos avisos sobre este grupo: A-55 no es un defecto visible (20,5px = 1,6% del ancho; él lo ve centrado y tiene razón), y A-65/A-75 son el mismo patrón — re-emitir sobre un control que ya emite.
d) La ola F2b — FIRMADA 2026-08-17, sin ejecutar
La fuente es PLAN-blocks.md §F2b — no dupliques aquí sus tablas. Cuatro
tramos, en orden: A variantes de suelo (V4 hero ✅ · formV8
single-price ✅ · V5 cta split ✅ · V2 testimonials spotlight ✅ ·
V3 faq-lista ✅ · V1 ✅ — TRAMO A CERRADO 6/6 el
2026-08-17; entra por el tramo B) · B ✅ CERRADO 2026-08-17 (15 matrices + guard encendido y probado en rojo) — la matriz de
equivalencia variante-a-variante en el README de cada block, con recetas
demostradas en demo (el entregable central: responde a la brecha de
cardinalidad sin competir en dumps) · C ✅ CERRADO 2026-08-18 (Pricing.Compare ✅ con el eje logo-clouddata-ink en la fundación · ✅ como blogarticle-grid — el nombre se decidió en su fase 0 — · ✅ con su fase 0 legal: las 5 reglas son el TIPO y 8 tests, y una sola llamada requerida cookie-consentonDecision(consent, via) en vez de las dos de la ficha — es MÁS fuerte, porque el camino de rechazo lo renderiza el block y no el consumidor, que con dos callbacks podía pasar un no-op) C tres blocks nuevos — logo-cloud
(reabierto: las refs lo shippean estático), blog, cookie-consent (fase 0
legal profunda; la ley vive en el TIPO) — y onboarding NO entra (receta de
wizard) · D la página compuesta como puerta de cierre.
Las 4 filas que la sección «Deuda a decisión tuya» de este handoff listaba
llevan ahí su firma (V1 parte, no hermano · V2/V3 layout · V4 slot). V6/V7
(stats-band split-with-image y timeline) salieron a F5: el dossier declara ese
block «Paridad OK».
⚠️ Lo que dejó V4 y sirve para las que vienen:
- Dos gaps de canon (README del hero §«Found while composing»):
Form.Submitno reenvía alButtonniintentni eje de anchura — el botón no llena su celda cuando la fila colapsa, ynewslettermide lo mismo, así que es del canon, no del block. EljustifydeGridesjustify-content, nojustify-items: no se arregla desde fuera. La demo usastyle, que sí se reenvía. - El contexto de inversión mordió por segunda vez: bajo
background, unTextsincolorexplícito mide 1.62:1; conon-solid, 10:1. Bajobackgroundla tinta se pone explícita SIEMPRE. - La sonda de contraste: mide sobre PÍXELES PINTADOS, nunca parseando
colores. Dos sondas dieron cifras seguras y falsas antes (un parser rgb sobre
un sistema que emite
oklch, ycanvas.fillStyle, que tampoco normaliza oklch aquí → medía NEGRO contra todo: 20.46:1 en claro y 1.11:1 en oscuro, ficción las dos). Control de sanidad: la etiqueta del submit tiene que salir a 5.18:1, la cifra que el repo ya documenta paraprimary. Vive enscratchpad/v4-final.mjs. - El preview cambia de tema por
?mode=dark, no porprefers-color-schemeni tocandodata-themea mano (el boot es el dueño; forzar el atributo NO repinta y da dos medidas idénticas que parecen «no hay diferencia»).
⚠️ Y lo que dejó V8, que vale para TODA la ola: una fila «diferida» puede
ser una medición equivocada, no un aplazamiento. El README de pricing decía
desde julio que el plan único «ya lo cubre el layout» y era falso — la rejilla
es auto-fill y RESERVA las pistas que caben aunque nadie las ocupe (un plan
solo: card de 315px con 677px de vacío al lado). Nadie había mirado ese block
con menos de tres planes. Antes de dar por buena una disposición vieja de
cualquier README, mídela.
⚠️ Y dos cosas del árbol COMPARTIDO, medidas el 2026-08-17: otra sesión está
a medias con un componente background (toca morfo/, langs/ y
eidos/generated), así que svelte-check da 73 con cosas que no son del tier —
la base real sin nadie es 60. Y no hagas git stash --include-untracked
aquí: arrastra su trabajo sin commitear. Si necesitas una base, mide por
fichero.
e) Cabos y fases
- Tres cabos concretos:
- Un desbordamiento de texto en las cards del card-group que el usuario ve y que NO se reprodujo a 1280 en claro ni en oscuro. Falta el ancho de ventana.
- Los hermanos del
Field(password-field,search-field,textarea) declaran su propiocommit-submitcopiando el patrón: pueden duplicar igual dentro de unForm. Se comprueba en un comando con la sonda (abajo). - La página compuesta dejó de ser un cabo suelto: es el tramo D de F2b.
- F3 del plan original (10 blocks de aplicación), ya con orden recomendado
en el plan:
app-shellPRIMERO (desbloquea A-95 y F4.1);data-tableykanbanal final por su fase 0 obligada.
⚠️ El sonido CAMBIÓ DE MODELO el mismo día — léelo antes de tocar un pack
Mientras esta sesión trabajaba, la de sema rehízo el sonido dos veces. Modelo
vigente (CONTINUE-sema-audit.md §3.0-bis):
nombre = per-emit ?? cascada ?? pack ?? morfo ?? familia[verbo] ?? familia.default
sonido = pack[`${nombre}.${intent}`] ?? pack[nombre] ?? nada
soundTuning(),SOUND_TUNINGSysound()están RETIRADOS, con guard propio (sounds-grammar.test.ts). El sonido ya no se modula: se elige. Un intent selecciona OTRO sonido entero, no doblega el mismo.- El sonido lo decide el mapa, por VERBO (
SEMA_MAP.families[f].sounds), así que un pack escribe una regla sólo cuando DIFIERE del default: de ~165 reglas a 30 en 71 packs. Un pack migrado es casi sólo selectores. allowedTargetsen el morfo declara a qué partes puede viajar el estampado.- Guard de distinguibilidad: dos nombres han de diferir en ≥2 ejes.
Qué significa para la cola de este handoff: nada se invalida — lo pendiente es QUÉ evento se emite y SOBRE QUÉ parte, no cómo suena. Lo que cambia es el método: si al arreglar un confirmado hiciera falta tocar el sonido, ya no se afina, se NOMBRA; y si el nombre no existe, se registra en el catálogo antes de usarlo.
El pack de navigation-menu de esta sesión ilustra el encaje: la otra sesión lo
migró encima y las tres reglas sobrevivieron sin su sound: — la aportación
era a qué parte apunta cada una, y eso es ortogonal al modelo de sonido.
La sonda de sema — G:/tmp/sonda-sema.mjs
Herramienta nueva, y la forma de contestar «¿esto suena?» sin discutirlo:
node G:/tmp/sonda-sema.mjs <url> <enter|click|hover> [selector]
Dice, por gesto, cuántos nodos de audio se crean y qué eventos se estampan con su
familia e intent. Con ella se midió el doble commit-submit del Field (2
eventos a 11 ms y 4 nodos de audio → 1 y 2) y el mudo del NavigationMenu.Link.
⚠️ Sesgo conocido: asocia los metadatos por ventana temporal de 30 ms, así que
puede atribuir el intent de un evento vecino. El contact-activate NO lleva
intent (morfo de Button, cap. 22 §11).
⚠️⚠️ Y su límite, que el handoff de sema documenta como lección cara: contar
nodos de audio no es medir la salida. Sirve para «¿emite o no emite?», que es
para lo que se usó aquí. Para «¿suena como debe?» hay que renderizar el grafo
offline (OfflineAudioContext) y medir el pico de muestra — ahí se descubrió que
una firma «silenciada» salía a −13.9 dBFS.
⚠️ CAMBIO DE DOCTRINA (decisión del usuario, 2026-07-31). Lee la sección «La doctrina cambió» antes de tocar nada: un block ya no es solo colocación. Ya está ESCRITA en el contrato (
architecture/blocks.md§«Coordination»).
(El plan fijó 14; feature-split se añadió como block hermano con
scope-approval, así que el tier tiene 15 y el denominador honesto es 15.)
Commiteado en alpha-0.1-dir-prefs (866089a40 + este handoff). SIN PUSH.
Dónde vive cada cosa:
- Plan y bitácora por block:
docs/process/PLAN-blocks.md(fichas §F2.x + registro cronológico al final, con el detalle de cada cierre). - Contrato del tier:
docs/architecture/blocks.md(B-1..B-11, D-BLK). - Suelo de paridad:
docs/process/RESEARCH-blocks-references.md(el dossier). - Deuda de canon congelada:
docs/process/PLAN-blocks-quality.md§6. - Decisiones por block: el README de cada uno (
src/uix/blocks/{kebab}/).
La doctrina cambió: un block coordina, tiene estado y trae datos
Decisión del usuario, textual: «si al final es una lista de componentes… el
bloque es coordinación, estado, y data también». El motivo, con el contact
delante: cuando el estado vive fuera, cada disabled es una expresión distinta
montada en el punto de uso y aparecen huecos — un envío que no se puede hacer
y nadie dice por qué.
Lo que cambia respecto de lo firmado en docs/architecture/blocks.md (B-5/B-7 y
D-BLK: «todo el contenido por snippets, cero cadenas propias, sin servicios»):
- El block posee el estado de su sección. Una máquina única y EXHAUSTIVA,
derivada, en contexto. Sus partes la leen; ninguna la recalcula ni inventa un
disabled. - El block trae la forma de sus datos (el esquema por defecto). El app lo sustituye pasando el suyo.
- El block habla, con idlangrefs por el traductor
(
#?blocks.<block>.<clave>|fallback), la misma puerta que usa el canon para sustexts:. Fallback en inglés; el app traduce registrandoblocks.*.
ESCRITO (2026-07-31): architecture/blocks.md gana la sección
«Coordination», B-5 admite que el block traiga la FORMA de sus datos, B-7 queda
enmendado (posee las palabras de SUS estados, como idlangref con fallback
inglés) y la convención de servicios se acota. La sección dice también a quién NO
aplica.
Enmendado el 2026-08-06: los servicios sancionados son DOS — el traductor,
que resuelve las palabras, y el anunciador (uix.announce), que las entrega a la
tecnología asistiva. Entró por la misma razón que el primero: un block que posee
las palabras de sus estados tiene que poder decirlas, y montar una región viva
propia re-implementa Announce.
Alcance acordado: contact primero como prueba de la forma — HECHO.
Revisión documental de los 15 HECHA (2026-08-01): cada README declara ahora
su posición bajo la doctrina, y el mapa real salió así —
- Coordinan:
contact(máquina + esquema + palabras) ·pricing(el periodo por contexto, pero nada se puede BLOQUEAR ahí, así que no necesita máquina ni palabras: coordinar no es siempre una máquina). - Comparten configuración, no estado:
team(align) ·content-section(la medida). - Costura bindable hacia el canon:
faq(value→Accordion) ·site-header(mobileOpen→Drawer). Reenviar no es poseer. - No poseen nada: banner · cta · feature-grid · feature-split · hero · site-footer · stats-band · testimonials.
- ✅
newsletterCOORDINA desde 2026-08-06 — era el candidato abierto de esta lista y ya está cerrado. Máquina de cinco estados (incomplete · invalid · ready · sending · sent), dosRecordexhaustivos de razón y acción, yNewsletter.Submit/Newsletter.Reasoncomo PARTES que leen el contexto: la regla de forma del tier aplicada, no una excepción — compound se gana cuando las partes repiten o coordinan.state.test.tscon 8 casos.
⚠️ Al revisarlos, el listón es el que dejó contact: el estado se deriva de
una fuente y las partes lo leen; si un estado puede bloquear algo, tiene frase
por Record exhaustivo. Y no inventes estado donde no lo hay — un block de
layout que no coordina nada se queda como está.
F2.13 contact — CERRADO (2026-07-31)
Los seis puntos que quedaban están hechos: arco verificado en navegador · README
rehecho · pestañas de doc al día · namespace blocks registrado en el arnés ·
ficha §F2.13 + bitácora en PLAN-blocks.md · contrato B actualizado.
Un defecto real, encontrado al verificar: la máquina leía form.isValid,
que con progressive significa «aún no se ha encontrado nada mal» — un
formulario vacío e intacto no tiene errores, así que se declaraba válido y
incomplete era INALCANZABLE al cargar: la sección pedía resolver la
verificación con los tres campos vacíos. Justo el hueco que la doctrina existe
para cerrar. Se arregla preguntando al ESQUEMA (validateSync de SIUM: síncrono
y sin escribir errores; form.validate() habría encendido los tres campos en
rojo al cargar). Fijado con state.test.ts — 9 casos, primer test unitario
del tier, porque state.ts es su primera lógica pura.
Añadido de paso el control vivo que faltaba para un prop público:
verification con reto / sin reto. Omitir el prop NO es pasar idle: sin
él la sección no tiene paso de verificación; con un estado que nunca llega a
verified tienes una verificación que no pasa, que es otra cosa.
⚠️ Ojo con el dev server de otra sesión: el de :5173 sirvió un módulo VACÍO
para blocks/contact/index.ts (500 «does not provide an export named Contact»)
porque tenía el grafo caducado tras crear ficheros nuevos. Arrancado uno propio en
otro puerto, la página va perfecta. Si mañana ves ese 500, es eso: no persigas el
código.
Dos hilos de canon abiertos (medidos, sin causa raíz)
Form/SIUM: conprogressive/onSubmit, un envío inválido bloquea y mueve el foco pero no expone mensajes (form.errorsvacío). CononChangesí aparecen —y traducidos—, pero saltan en los tres campos al escribir en uno; cononBluraparecen desde la carga. La máquina de estado del block TAPA el agujero de cara al usuario (el botón dice qué falta), pero el hueco sigue ahí.ProofOfHuman: el provider de soma escribestatusél mismo (writableActive), así que el veredicto que devuelve el app tras consultar a su servidor no se sostiene, y los slotsstage*no renderizaron en ningún estado. El camino «rechazado» no es contable hoy desde el app. Consecuencia medida encontact: sus estadosverifyingyrejectedquedan cubiertos por test unitario de la máquina, no por el reto en vivo.
La regla de forma del tier (no la re-decidas)
Compound solo si las partes:
- SE REPITEN — el app mapea sobre N (
feature-grid.Item,testimonials.Item,pricing.Plan,faq.Item,stats-band.Stat,site-footer.Column), o - COORDINAN — se hablan por contexto (
pricing.Switch↔PlanPrice).
Si no hacen ninguna de las dos: slots de snippet en la raíz (hero, cta,
newsletter, y la marca / alta / social / legal / extra de site-footer). El
plan original dibujaba varias de esas como partes; la desviación está registrada
en cada README.
Los 8 hallazgos de canon que dejó el tier (congelados)
Restricción del usuario en vigor: no tocar componentes ni librerías fuera de
src/uix/blocks/ y web/routes/blocks/. Están todos medidos en navegador
y listados en PLAN-blocks-quality.md §6 (F15–F22) — más los de motion (A1, A2,
A4, C7, C8, C10, D12, E14) de la pasada de calidad.
| # | Qué |
|---|---|
| F15 | Surface variant='soft' no acota un panel: su track queda a 0.002 de luminancia del fondo de página en claro, y Surface no tiene borde. Card outline sí acota pero no acepta gradient → «panel sosegado con borde» no tiene primitivo. |
| F16 | La ranura contrast de la paleta es blanca en todo escalón sólido, así que un lienzo de luminancia media deja el cuerpo bajo AA: primary 5.18 · indigo 5.21 · plum 4.75 pasan; neutral 3.32 · secondary/slate 3.30 · teal 3.07 fallan en claro. |
| F17 | Text align es inerte por defecto: renderiza un span y text-align no hace nada sobre caja inline. Hay que pedir as="p". |
| F18 | La fundación de eidos no trae reset de modelo de caja y lo asume del app. Bajo content-box, [data-field-control] (inline-size:100% + padding) mide 30px más que su contenedor. Resuelto en app-land: web/routes/blocks/_lib/reset.css. |
| F19 | onValidSubmit/onInvalidSubmit son no-op silenciosos si se pasa un form ya construido: el componente solo los reenvía al createForm que hace él mismo. El handler va SIEMPRE en createForm. |
| F20 | Los mensajes de SIUM son idlangref (#?sium.errors.email|…): resolver con uix.langs.t(issue.message, issue.params). La demo de docs del Form los parte a mano y por eso siempre salen en inglés. |
| F21 | Los primitivos de layout no pueden cambiar de elemento: Text/Heading aceptan as, pero Box —y Stack/Flex/Grid/Group/Wrap/Container/Section— es un <div> fijo. Una columna de enlaces no puede ser ul/li. |
| F22 | Un Select controlado muestra el VALOR crudo hasta abrirse una vez: las etiquetas las registran los Select.Item al montarse y el Content portaleado está cerrado. Rodeo: el child de Select.Value. |
Y dos huecos anteriores que siguen abiertos: Box/Surface flex/grow no
hacen crecer a un hijo flex (se rodea con tracks 1fr de Grid) y Group no
apila (usa Flex direction={{ base: 'column', sm: 'row' }}; hero todavía
compone sus acciones con Group).
La plantilla ya existe — cópiala, no la reinventes
src/uix/blocks/{kebab}/
├── README.md # Función · Mapa de composición · Decisiones · Gaps
├── index.ts # export del compound + tipos
├── types.ts # props (todo contenido entra por snippets, B-5/B-7)
└── {kebab}.svelte # composición: solo componentes del canon, sin CSS
web/routes/blocks/{kebab}/
├── +page.svelte # BlockDemo + controles vivos + pestañas de doc
├── {Name}Site.svelte # el block dentro de contenido REAL de producto
└── preview/
├── +layout@.svelte # `@` resetea el layout: la vista previa es su página
└── +page.svelte # sirve {Name}Site leyendo la URL
Y luego: marcar shipped: true en web/routes/blocks/_lib/catalog.ts (raíl y
galería leen esa única fuente) y npm run blocks:check.
Reglas que ya costaron sangre (no las re-aprendas)
- El block se enseña A SANGRE en la página. Nada entre el block y el borde:
ni marco con relleno, ni caja con scroll, ni cromo pegajoso encima. Medido: un
Carddesplazaba 21px un header conoffset: 0(su recipe pinta con--card-padding-*, quepadding={0}de la capa Box no alcanza), y unposition: stickydentro de un div con scroll es un comportamiento que nadie vive. Si un block se ancla a algo, se mide contra lo que se anclará en producción. - Verifica esperando la CONDICIÓN, nunca un timeout fijo. Con
Form+Field+ SIUM el dev server tarda ~2,5s en hidratar y un screenshot temprano fotografía el panel todavía invisible (data-animation-pendingpuesto). Esperar a que ese atributo desaparezca es la diferencia entre verificar y reportar un bug que no existe. - Y la condición tiene que ser algo que escriba la RUNTIME del morfo, no el
render. Los
data-variant/data-sizelos pone el componente de eidos al renderizar, así que están desde el primer frame;type,aria-labely los handlers los aplica la runtime en un efecto POSTERIOR. Esperar a que exista el nodo (o a que se vea) no basta: a ~1s medítypeausente en TODOS losButtondel árbol y ningúnonclickdisparando, y a ~6s los mismos botones teníantype="button",aria-label="Descartar"y el clic funcionando. Reporté tres «defectos del canon» que no existían. - MATIZ CARO (2026-07-31): que lo escriba la runtime NO basta — tiene que no
existir en el SSR. Esperé
button[type="submit"]con atributotypecomo puerta de hidratación encontact, y ese atributo viene ya en el HTML del servidor: la espera se cumplía sobre el documento estático y yo tecleaba antes de que Svelte tomara los inputs. Resultado: los valores entraban en el DOM,form.valuesseguía vacío, y pasé varias rondas persiguiendo un fallo del block que no existía (llegué a «arreglar» un$derivedque estaba bien; lo revertí al medirlo). La puerta honesta: algo que solo pueda haber escrito el cliente — aquí<style id="uix-blocks-display">, que pone un$effectdeBootUix— más un ida y vuelta reactivo real antes de dar por hidratado. - Si instrumentas con
console.logy no lo ves en el navegador, míralo en el SERVIDOR. Unconsole.logdentro de un$derivedsale por el stdout del dev server durante el SSR. Ver el log SOLO ahí es la prueba de que el componente no está corriendo en cliente — fue lo que delató el diagnóstico anterior. - Playwright headless SÍ sirve para foco y teclado. El pane suspendido no
(congela rAF y pierde
activeElement), pero unpage.keyboard.type()real dispara:placeholder-showny:focus-within: así se verificó que la etiqueta flotante delFieldsube al borde.page.fill()usa el setter nativo y NO los dispara. Un script del scratchpad debe importarfile:///G:/dev/svelte/vicen/node_modules/playwright/index.mjs(no resuelveplaywrightpor ruta relativa). - Los anchos de dispositivo (375/768) van por la ruta
previewen iframe, y es opt-in: en dev, dos documentos sin empaquetar a la vez agotan las conexiones del navegador (ERR_INSUFFICIENT_RESOURCESmata las DOS páginas). - Cada prop público, un control vivo en la demo; los ejes de sección (tema, idioma, dirección, densidad) ya los da el shell, no los repitas.
- Un slot que se apaga se pasa como
undefined, no se renderiza vacío. Declara el snippet arriba y pásalo por prop (signup={activo ? banda : undefined}): así el{#if}del block quita también su hueco. Y cuidado con el sombreado:{#snippet signup()}pisa un prop llamadosignupdentro del componente. - Nada de backticks de markdown dentro de
<Text>: se ven literales. Lo que es código va en<Code>. - Ojo con las variables de layout que heredan.
justifydeGroupse hereda a los clusters anidados (ponjustifyexplícito) y las deBoxYA no heredan desde 2026-07-23 (docs/theming/changelog.md§46). - Un hueco del canon se registra, no se falsea. Si al componer falta algo, va a los Gaps del block como candidato a canon y la demo usa lo que hay.
gapen unGridsepara también las COLUMNAS, y una parte que las cruza se lleva esos huecos encima (content-section, medido): congap={8}y cinco pistas, la parte que abarca 2/5 medía 1088 en vez de los 1024 del contenedor con el que debía alinearse, y la de 1/-1 llegaba a 532 dentro de una rejilla de 404 y hacía scrollear la página a 420px. Si las columnas son un instrumento de medida y no cosas puestas al lado:rowGap+columnGap={0}.- Y
100%dentro de una pista no es100%de la rejilla. Una pistamin(medida, 100%)se come sus propios canalones (a 420px la central se quedaba los 404 enteros y la rejilla se iba a 436), y algo anidado dentro de una parte ve el ancho de SU pista, no el de la rejilla — capar igual en los dos casos descuadra uno de los dos. Los dos se cazaron midiendo, no leyendo. - Los números de layout se miden. Tres defaults de
site-footer(container, la razón de la rejilla, el gap de columnas) salieron mal a la primera y solo el navegador lo dijo. Si un default decide cuántas cosas caben en una fila, se comprueba con el contenido real.
Deuda del arnés de demos (mía, sin tocar)
- La galería (
web/routes/blocks/+layout@.svelte) arranca UIX en línea en vez de usar_lib/BootUix.svelte, que es lo que usan los previews: la misma cadena escrita dos veces, y el motivo de quereset.csshaya que importarlo en los dos sitios. - En
BlockDemo, a ~1400px, el conmutador de anchos de dispositivo se solapa con el párrafo que lo precede. Idéntico en los 11 blocks → es del arnés, no de un block.
Deuda a decisión tuya — SALDADA 2026-08-17
Las cuatro se firmaron en PLAN-blocks.md §F2b (tramo A) y ya no bloquean:
- Tabla de comparación de features×planes →
Pricing.Compare, PARTE del block (el toggle de periodo vive en su contexto y B-10 impide que un hermano lo lea). NO es el hermanopricing-tableque esta lista sugería. - Cita en spotlight →
layout="spotlight"entestimonials. - Lista estática 2/3 columnas →
layout="list"enfaq, con unión discriminada en los tipos (A-94). form-in-hero→ slotformdel hero.
Resueltas ya: feature-split/alternante (F2.3b, con Mockup) · hero con fondo
cover (layout background) · CTA que navega y parece botón (el child de
Button entrega un snippet content; Button sigue sin href y Link sigue
poseyendo la navegación — doctrina en el README de eidos Button §«CTA que
navega»).
Estado de gates al parar (2026-08-17 — TODO COMMITEADO Y PUSHEADO)
npm run blocks:check— verde, 0 errores sobre 15 blocks / 115 ficheros.npm run docs:check— 0 errores, 0 avisos (629 docs).svelte-check: 72 errores / 59 avisos, medido justo antes y justo después de cada cambio — misma cifra, cero regresión, ninguno en lo tocado. ⚠️ Esa cifra SE MUEVE: con otras sesiones vivas en el mismo árbol se midió 72, 74, 75, 76, 77 y 80 en días distintos. Mídela justo antes y justo después de tu cambio, nunca contra un número recordado.- Suite completa: 8 rojos en 3 ficheros, todos AJENOS —
contracts.test.ts(6),eidos/lint.test.tsysoma-attr-audit.test.tsnombranwaveform/skin-media-player/radio-group, que entran porbf12b8a57y68a48a440(sesión del media-player, componentes sin morfo), más un timeout deengine-orcaque huele a flaky. Ninguno cita nada del tier. prettier: los avisos que quedan son de ficheros CRLF preexistentes (motion.svelte,motion.css,motion/types.ts,motion/README.md,AUDIT-blocks-ledger.md) — ya fallaban en HEAD y formatearlos produciría un diff del fichero entero. Se dejan como están, a propósito.
Estado de gates al parar (2026-08-09, fase 5 — histórico)
npm run blocks:checkverde con las tres reglas nuevas, 15 blocks / 114 ficheros ·vitest src/uix/blocks/20/20 ·vitest src/uix/eidos src/uix/blocks410/410 ·docs:check0/0 (618 docs) ·svelte-check74/54.
⚠️ El árbol está COMPARTIDO — cuidado al commitear
El commit 866089a40 incluye ficheros que ya venían modificados cuando
empezó la sesión: había trabajo sin commitear de otras sesiones sobre blocks/ y
eidos/, y al editar encima no hay forma de separarlo por fichero. Lo que sí se
respetó: morfo/{calendar,pagination,rating-group,toolbar,tree-view,schema,types},
sema/components/textarea.ts y soma/tree-view quedaron FUERA por ser de otra
sesión.
Antes de tocar sema o morfo, mira si hay otra sesión viva ahí. En este mismo día,
una arregló el emparejamiento de las reglas (closest()) y el renombrado que mató
el prefijo form. en las claves de sonido, y otra (ésta) cambió a qué parte
apuntan los eventos. Se complementaron por suerte, no por diseño.
Sesión 2026-08-01 — lo que cambió
content-sectionHECHO → F2 cerrada 15/15. Rejilla de escape de 5 pistas (measurecol 3 ·widecol 2/5 ·fullcol 1/-1), los tres ejes RESPONSIVE poreidos.resolve(), y.Mediacon defaultmeasure: la figura sigue al texto y salirse se PIDE, como en todas las referencias.- CANON tocado por orden del usuario: un componente no es librería de otro. Las
cinco escalas tipográficas (
TextTracking,TextLeading,TextWrap,TextNumeric,TextMeasure) salieron decomponents/text/types.tsaeidos/lib/types.ts.Headingya las importaba deTextdesde antes. - La galería EMITE: arrancaba sin packs de sema desde F0 — el motor estampaba
data-event-*y nada era audible. Ahoraevents: { sound, haptic, components }con la lista DERIVADA del barrel (Object.values), no escrita a mano: la lista a mano del otro arnés se quedó atrás dos veces. - Los 15 READMEs declaran su posición bajo la doctrina de coordinación. El mapa no es uniforme y está en la sección de arriba.
Lecciones de verificación que costaron esta sesión
- La puerta de hidratación no puede ser un atributo que el SSR ya manda. Usé
el
typedel submit y la espera se cumplía sobre el documento estático: tecleé antes de hidratar, los valores entraron en el DOM,form.valuessiguió vacío, y perseguí un fallo inexistente varias rondas. La puerta honesta es algo que solo escriba el cliente (style#uix-blocks-display). - Medir el block contra SÍ MISMO no basta. El defecto de los 16px de
content-sectionsolo apareció al poner una sección normal debajo y comparar bordes. Los 15 blocks están verificados cada uno contra sí mismo. - Inspeccionar en una pestaña de fondo miente:
visibilityState: 'hidden'suspende el rAF y dejadata-animation-pendingconopacity: 0, que parece un fallo de visibilidad grave y no lo es. - Screenshot Y MIRAR. Generé la captura en oscuro y nunca la abrí.