# 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](#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 miden `0px`), 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 propio `AutoGrid`), A-61 (su segunda mitad ya la cerró `uix.announce`). - **Tres filas nuevas** — A-91 (`banner` hereda el eje `intent`/`color` sin registrarlo), A-92 (una restricción del modelo de cascada que vive en un comentario de `pricing` y falta en §D.13 de la doctrina de motion), A-93 (`Motion` no 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) 1. **Medir un default en una demo que lo pisa no es medir un default.** Escribí en la ficha de A-99 que «el `align` sí se cumple, luego el recipe carga». El `center` salía del control de la propia demo, que lo pasa explícito. 2. **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 `CountUp` tampoco dispara, así que el resultado no distinguía una cosa de la otra. La vía fiable es **Playwright headless** (como las fichas originales), resolviendo `playwright` desde el `package.json` del repo si la sonda vive en el scratchpad. 3. **`Motion` es `once: 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 (`addInitScript` con 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()`: 1. **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/elements` y los relativos del propio block. **Los arts se DERIVAN de `src/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. 2. **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ás `banner`, `content-section`, `cta`, `newsletter` y `site-footer`, que nombraban «landmark» en una celda de tabla pero no declaraban nada. Escritas leyendo la fuente de cada uno. **A-14 → ARREGLADO.** 3. **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` — con `affix="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 su `AppShell` (`header={{ height }}`); nuestro equivalente es el block **`app-shell`, F3.1, sin construir**. El plan ordena ya F3 con `app-shell` PRIMERO 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 `form`~~ ✅ **HECHA 2026-08-17** → **entra por V8 single-price**, que SE MIDE antes de declararse gratis → V5 cta split → V2 spotlight → V3 faq-lista → V1 `Pricing.Compare` en tanda propia) · **B** 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** 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.Submit` no reenvía al `Button` ni `intent` ni eje de anchura — el botón no llena su celda cuando la fila colapsa, y `newsletter` mide lo mismo, así que es del canon, no del block. El `justify` de `Grid` es `justify-content`, no `justify-items`: no se arregla desde fuera. La demo usa `style`, que sí se reenvía. - **El contexto de inversión mordió por segunda vez**: bajo `background`, un `Text` sin `color` explícito mide **1.62:1**; con `on-solid`, 10:1. Bajo `background` la 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`, y `canvas.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 para `primary`. Vive en `scratchpad/v4-final.mjs`. - **El preview cambia de tema por `?mode=dark`**, no por `prefers-color-scheme` ni tocando `data-theme` a mano (el boot es el dueño; forzar el atributo NO repinta y da dos medidas idénticas que parecen «no hay diferencia»). ### e) Cabos y fases 1. **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 propio `commit-submit` copiando el patrón: pueden duplicar igual dentro de un `Form`. 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. 2. **F3 del plan original** (10 blocks de aplicación), ya con orden recomendado en el plan: **`app-shell` PRIMERO** (desbloquea A-95 y F4.1); `data-table` y `kanban` al 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_TUNINGS` y `sound()` 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. - **`allowedTargets`** en 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: ```bash node G:/tmp/sonda-sema.mjs [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»): 1. **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`. 2. **El block trae la forma de sus datos** (el esquema por defecto). El app lo sustituye pasando el suyo. 3. **El block habla**, con idlangrefs por el traductor (`#?blocks..|fallback`), la misma puerta que usa el canon para sus `texts:`. Fallback en inglés; el app traduce registrando `blocks.*`. **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. - ✅ **`newsletter` COORDINA 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`), dos `Record` exhaustivos de razón y acción, y `Newsletter.Submit` / `Newsletter.Reason` como 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.ts` con 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) 1. **`Form`/SIUM**: con `progressive`/`onSubmit`, un envío inválido bloquea y mueve el foco pero **no expone mensajes** (`form.errors` vacío). Con `onChange` sí aparecen —y traducidos—, pero saltan en los tres campos al escribir en uno; con `onBlur` aparecen 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í. 2. **`ProofOfHuman`**: el provider de soma escribe `status` él mismo (`writableActive`), así que el veredicto que devuelve el app tras consultar a su servidor no se sostiene, y los slots `stage*` no renderizaron en ningún estado. El camino «rechazado» no es contable hoy desde el app. **Consecuencia medida en `contact`**: sus estados `verifying` y `rejected` quedan 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 `
` 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 ```text 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 `Card` desplazaba 21px un header con `offset: 0` (su recipe pinta con `--card-padding-*`, que `padding={0}` de la capa Box no alcanza), y un `position: sticky` dentro 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-pending` puesto). 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-size` los pone el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y 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í `type` ausente en TODOS los `Button` del árbol y ningún `onclick` disparando, y a ~6s los mismos botones tenían `type="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 atributo `type` como puerta de hidratación en `contact`, 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.values` seguía vacío, y pasé varias rondas persiguiendo un fallo del block que no existía (llegué a «arreglar» un `$derived` que estaba bien; lo revertí al medirlo). **La puerta honesta**: algo que solo pueda haber escrito el cliente — aquí `