FASE 5 DEL SANEAMIENTO — `blocks:check` deja de vigilar solo la forma del tier
y pasa a vigilar su frontera, su documentacion y su catalogo. Tres reglas, cada
una con su fixture negativo en el selfTest():
1. Lista blanca de importaciones (B-4). La frontera dura 1 era prosa; ahora es
codigo: $uix, los arts publicos, $libs/forms, svelte y los relativos del
propio block. Los arts se DERIVAN de src/arts/*, no se listan a mano — una
lista escrita se queda atras el dia que aterriza un art. El tier ya cumplia:
cero violaciones al encenderla.
2. README completo (B-9) + declaracion de landmark (B-8). Salieron 7 de 15 sin
ella; escritas leyendo la fuente de cada block, no la plantilla.
3. Ficha en el catalogo de demos (B-9), en los DOS sentidos: un block que el
catalogo no publica es invisible para el rail, y un slug publicado sin block
detras es un enlace muerto.
De paso el escaner deja de leer los comentarios como codigo: los index.ts
documentan su uso con un `// import { Cta } from '$blocks/cta'`, y una lista
blanca que lee prosa habria empezado a acusar a los ejemplos.
Verificado EN ROJO sobre el arbol real, no solo contra los fixtures: un zod y un
$libs/days metidos en hero/types.ts salen con su linea exacta mientras el mismo
import dentro de un comentario NO salta; el catalogo falla en las dos
direcciones; y al romper isAllowedSpec a proposito el self-test aborta el guard
en vez de dar verde.
ARREGLOS DEL CONTRASTE DOCTRINAL — siete filas, cada una con medicion o gate:
- A-88 team: `height="100%"` en el Stack del Member. El div que pinta Motion es
un bloque pelado, asi que el Stack se quedaba a altura de contenido y el
`margin-top:auto` de los enlaces repartia CERO. Medido: la desviacion entre
filas de iconos pasa de 20px a 0, y el margen reparte 20,297px.
- A-94/A-28 (feature-grid, team, testimonials): los tres declaraban su tipo como
`= AutoGridProps` y ponian {...rest} ANTES de las props que fijan, asi que un
consumidor podia escribir align="start" y no obtener nada. Cerrados con
Pick<AutoGridProps, ...>, que conserva los tipos exactos del canon.
- A-84 feature-grid: el eje `align` llega a las partes por contexto, como en
team, en vez de que el typedoc instruya al app a repetirlo. Medido: con
align=center el align-items computado es center en cabecera Y celdas.
- A-19 faq: `marginX="auto"` en el Header — Box no centra solo. Medido: el Box
de 768px resuelve margin-inline 248px/248px donde antes daba 0.
- A-27 testimonials y A-86 pricing: el typedoc decia `soft` donde el codigo hace
`outline`, y el ejemplo de la puerta de entrada no compilaba.
- A-91 banner (fila nueva): el block hereda el eje intent/color que el canon
declara fuera del sistema abierto en su propio typedoc, sin registrarlo.
LEDGER — dos lecciones de instrumento escritas dentro, porque ambas produjeron
hallazgos falsos publicados:
1. El dev server sirve codigo ANTERIOR a HEAD si reusa cache de Vite. Tres filas
(A-36, A-69, A-85) se dieron por vivas estando arregladas. El delator fue
aritmetico: las duraciones medidas eran EXACTAMENTE las que el comentario del
arreglo cita como estado anterior.
2. Contar nodos WebAudio no es medir sonido: la sintesis crea dos osciladores
por earcon, un sound pack de muestras cambia la aritmetica entera, y crear
nodos no es sonar. El contador responde "hubo actividad" y nada mas.
Y el error de metodo del que ambas son sintoma, tambien registrado: se fue a
re-medir A-67 sin leer su ficha, que ya traia los gains del catalogo, el empate
de applyDominance y la regla de CANON.md incumplida — mejor razonado que la nota
que lo sustituia.
Gates: blocks:check verde (15 blocks / 115 ficheros) · svelte-check 69 errores /
57 avisos = linea base exacta, ninguno en lo tocado · docs:check 0/0 · prettier
limpio en todo lo del commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| B-1 | No morfo, no sema pack, no audit row. Behavior with contract surface is promoted to canon BEFORE the block composes it. |
| B-2 | Every interactive element is a canon eidos component (`Button`, `Link`, `Field`, …). Raw interactive natives (`button/input/select/textarea/a`) are an error. (Single exception: the already-rendered HTML that `Prose` receives — that content belongs to the app.) |
| B-3 | Composed components are consumed AS-IS through their public props (`variant/size/color/…`). Re-styling their internals from the block (CSS or `style=`) is forbidden. Colors are always roles/tokens via props. |
| B-4 | One-way imports (hard boundary 1). Deleting the tier leaves `check` green. Blocks do not import each other (see B-10). |
| B-5 | Content enters by composition (children/snippets) — never `root={tree}` data-tree props (`items={...}` only where the composed canon component is already data-driven). A coordinating block MAY ship the default **shape** of the data it coordinates (a schema), which the app replaces by passing its own — a shape is not content. |
| B-6 | Responsive via the framework's mechanisms (responsive props of the layout components, canonical breakpoints). No `matchMedia`/listeners of its own — needing to observe something is the admission rule firing. |
| B-7 | Everything the APP says arrives as children/props — a block writes none of it. **Amended 2026-07-31**: a block DOES own the words of the states it coordinates (and only those), as idlangrefs with an English fallback resolved through the translator (`#?blocks.<block>.<key>\|…`). The reason a state names itself is the same reason it exists: the record that blocks the action writes the sentence, so it cannot go silent. |
| B-8 | Correct landmarks: sectioning element + `aria-label`/`aria-labelledby` where landmarks repeat; heading hierarchy coherent and documented in the block README (which level it emits, how to adjust). |
| B-9 | Every block ships `README.md` (**Function · Composition map** — which canon components, which props — **· Decisions · Gaps-with-disposition**) and a live demo page under `web/routes/blocks/{kebab}/`, built on the shared demo shell (`web/routes/blocks/_lib/BlockDemo.svelte`): the block is shown FULL-BLEED on the page — never inside a padded frame or a scroll box, which would change what it does — with the device widths served by its own `preview` route. Anatomy in [`src/uix/blocks/README.md`](../../src/uix/blocks/README.md). |
| B-10 | A block does not import another block. Shared structure is either a canon component or a conscious duplication (recorded in Gaps). Declared exception: the shells (`app-shell`, `docs-shell`) compose F1 pieces and blocks by design — allow-listed in `blocks:check`. |
| B-11 | Motion only through the composed components' `motion` props/presets or `Cascade` for entrance choreography. No `@keyframes`/transitions of its own. |
| B-1 | No morfo, no sema pack, no audit row. Behavior with contract surface is promoted to canon BEFORE the block composes it. |
| B-2 | Every interactive element is a canon eidos component (`Button`, `Link`, `Field`, …). Raw interactive natives (`button/input/select/textarea/a`) are an error. (Single exception: the already-rendered HTML that `Prose` receives — that content belongs to the app.) |
| B-3 | Composed components are consumed AS-IS through their public props (`variant/size/color/…`). Re-styling their internals from the block (CSS or `style=`) is forbidden. Colors are always roles/tokens via props. |
| B-4 | One-way imports (hard boundary 1). Deleting the tier leaves `check` green. Blocks do not import each other (see B-10). The allowlist is mechanical since 2026-08-09: `$uix`, the public arts (**derived from `src/arts/*`**, never a hand list), `$libs/forms`, `svelte` / `svelte/elements` and the block's own relatives. Anything else is either promoted to canon or the app's, and the guard says so by name. |
| B-5 | Content enters by composition (children/snippets) — never `root={tree}` data-tree props (`items={...}` only where the composed canon component is already data-driven). A coordinating block MAY ship the default **shape** of the data it coordinates (a schema), which the app replaces by passing its own — a shape is not content. |
| B-6 | Responsive via the framework's mechanisms (responsive props of the layout components, canonical breakpoints). No `matchMedia`/listeners of its own — needing to observe something is the admission rule firing. |
| B-7 | Everything the APP says arrives as children/props — a block writes none of it. **Amended 2026-07-31**: a block DOES own the words of the states it coordinates (and only those), as idlangrefs with an English fallback resolved through the translator (`#?blocks.<block>.<key>\|…`). The reason a state names itself is the same reason it exists: the record that blocks the action writes the sentence, so it cannot go silent. |
| B-8 | Correct landmarks: sectioning element + `aria-label`/`aria-labelledby` where landmarks repeat; heading hierarchy coherent and documented in the block README (which level it emits, how to adjust). The declaration is a `**Landmark + headings**` paragraph and `blocks:check` requires it — a block with no landmark of its own says THAT, which is the answer the app needs to name the region itself. |
| B-9 | Every block ships `README.md` (**Function · Composition map** — which canon components, which props — **· Decisions · Gaps-with-disposition**) and a live demo page under `web/routes/blocks/{kebab}/`, built on the shared demo shell (`web/routes/blocks/_lib/BlockDemo.svelte`): the block is shown FULL-BLEED on the page — never inside a padded frame or a scroll box, which would change what it does — with the device widths served by its own `preview` route. Anatomy in [`src/uix/blocks/README.md`](../../src/uix/blocks/README.md). `blocks:check` requires the README's four sections (Function · Composition map · Decisions · Gaps) and a `shipped` entry in the demo catalog (`web/routes/blocks/_lib/catalog.ts`), both ways: a block the catalog does not ship is invisible to the rail, and a slug it ships without a block behind it is a dead link. |
| B-10 | A block does not import another block. Shared structure is either a canon component or a conscious duplication (recorded in Gaps). Declared exception: the shells (`app-shell`, `docs-shell`) compose F1 pieces and blocks by design — allow-listed in `blocks:check`. |
| B-11 | Motion only through the composed components' `motion` props/presets or `Cascade` for entrance choreography. No `@keyframes`/transitions of its own. |
@ -50,19 +50,19 @@ Columna **or.** = de qué sección del documento viejo salió la fila (`C` confi
| A-04 | `contact` | doctrina | ALTA | C | `CONTACT_ACTION` («la acción nombra lo que está pasando», state.ts:86) solo se cumple a la vista: el nombre accesible del botón es siempre «Enviar»,… | `src/uix/blocks/contact/state.ts` | ARREGLADO |
| A-05 | `contact` | doctrina | ALTA | C | El block importa de `$libs/forms`, que no está en la lista de importaciones permitidas de la frontera dura 1 («may import `$uix`, `$adom` and the… | `src/uix/blocks/contact/contact.svelte` | ARREGLADO |
| A-06 | `site-footer` | percepcion | ALTA | C | El éxito del alta en la banda de sign-up del footer es PERCEPTIVAMENTE NULO: no suena, no estampa y no emite — el único momento que merecía… | `web/routes/blocks/site-footer/SiteFooterSite.svelte:105` | ARREGLADO |
| A-07 | `faq` | doctrina | MEDIA | C | La sección «Coordination» declara UNA sola costura (`value`) pero el block también expone `disabled`: una pregunta que no se puede abrir, sin nada… | `src/uix/blocks/faq/types.ts` | CONFIRMADO |
| A-07 | `faq` | doctrina | MEDIA | C | La sección «Coordination» declara UNA sola costura (`value`) pero el block también expone `disabled`: una pregunta que no se puede abrir, sin nada… | `src/uix/blocks/faq/types.ts` | ARREGLADO |
| A-08 | `cta` | doctrina | MEDIA | C | La medida de la descripción se construye a mano con un valor fuera de escala (`60ch`) en vez de usar el vocabulario de medida que el canon ya expone… | `src/uix/blocks/cta/cta.svelte` | ARREGLADO |
| A-09 | `stats-band` | doctrina | MEDIA | C | En modo `live` el anuncio a lectores de pantalla puede llevar la cifra CRUDA mientras en pantalla se ve la formateada por locale: el block pasa… | `src/uix/blocks/stats-band/stats-band-value.svelte` | CONFIRMADO |
| A-10 | `site-header` | doctrina | MEDIA | C | Si el cajón está abierto y el viewport cruza el `breakpoint`, el propio block esconde cajón + overlay + trigger con `display:none`, pero el Drawer… | `src/uix/blocks/site-header/site-header.svelte` | ARREGLADO |
| A-11 | `site-header` | doctrina | MEDIA | C | Las demos de site-header y hero redeclaran a mano uniones que ya existen en el canon, y ya han derivado: el `xxl` de `ContainerSize` es inalcanzable… | `web/routes/blocks/site-header/SiteHeaderSite.svelte` | CONFIRMADO |
| A-12 | `feature-grid` | doctrina | MEDIA | C | El tipo y la demo describen el chip del icono como `Surface` soft; el código lo pinta solid por defecto, y el README dice solid. Dos de las cuatro… | `src/uix/blocks/feature-grid/types.ts` | ARREGLADO |
| A-13 | `testimonials` | percepcion | MEDIA | C | Mismo defecto: `height="100%"` sobre `Card` es inerte, así que la tarjeta no rellena la altura de fila y las caras NO se alinean cuando las citas… | `src/uix/blocks/testimonials/testimonials-item.svelte` | ARREGLADO |
| A-14 | `feature-split · pricing · testimonials` | doctrina | MEDIA | C | Ninguno de los tres README declara el landmark ni la jerarquía de encabezados, que es la segunda mitad literal de B-8 y el único apartado a11y propio… | `src/uix/blocks/pricing/README.md` | CONFIRMADO |
| A-14 | `feature-split · pricing · testimonials` | doctrina | MEDIA | C | Ninguno de los tres README declara el landmark ni la jerarquía de encabezados, que es la segunda mitad literal de B-8 y el único apartado a11y propio… | `src/uix/blocks/pricing/README.md` | ARREGLADO |
| A-15 | `contact` | doctrina | MEDIA | C | La frase del estado no llega a la tecnología de apoyo: el submit va `disabled` nativo (no es alcanzable con teclado, así que un lector de pantalla… | `src/uix/blocks/contact/contact-reason.svelte` | ARREGLADO |
| A-16 | `contact` | doctrina | MEDIA | C | Deriva README↔código: el README afirma en negrita que el block «no valida y no pinta los errores de los campos», y `Contact.Fields` hace exactamente… | `src/uix/blocks/contact/README.md` | ARREGLADO |
| A-17 | `content-section` | doctrina | MEDIA | C | La pestaña API de la demo (superficie obligatoria por B-9) publica un default falso para `ContentSection.Media`: dice `width` «def. wide» cuando el… | `web/routes/blocks/content-section/+page.svelte` | ARREGLADO |
| A-18 | `team` | doctrina | MEDIA | C | Comentario nuevo en castellano dentro de la fuente de un block, contra la regla del proyecto (comentarios en inglés; los castellanos que quedan son… | `src/uix/blocks/team/team-member-links.svelte` | ARREGLADO |
| A-19 | `faq` | percepcion | BAJA | C | El typedoc de `Faq.Header` promete un Box «centered» que el propio root desmiente: la raíz apila con `align="stretch"`, así que la cabecera queda… | `src/uix/blocks/faq/types.ts` | CONFIRMADO |
| A-19 | `faq` | percepcion | BAJA | C | El typedoc de `Faq.Header` promete un Box «centered» que el propio root desmiente: la raíz apila con `align="stretch"`, así que la cabecera queda… | `src/uix/blocks/faq/types.ts` | ARREGLADO |
| A-20 | `faq` | doctrina | BAJA | C | B-8 incumplido: el block emite un encabezado por pregunta con `aria-level` FIJO en 3, no ofrece forma de ajustarlo y el README no documenta ni el… | `src/uix/blocks/faq/README.md` | ARREGLADO |
| A-21 | `stats-band` | doctrina | BAJA | C | El typedoc de `countOptions` anuncia una opción `style` que `CountUp` NO acepta — pasarla es error de tipos. | `src/uix/blocks/stats-band/types.ts` | ARREGLADO |
| A-22 | `stats-band` | doctrina | BAJA | C | El README no documenta landmark ni jerarquía de encabezados (B-8); el dato solo vive en la pestaña A11y de la demo. | `src/uix/blocks/stats-band/README.md` | ARREGLADO |
@ -70,20 +70,20 @@ Columna **or.** = de qué sección del documento viejo salió la fila (`C` confi
| A-24 | `pricing` | percepcion | BAJA | C | Las tarjetas de plan NO son de igual alto y los CTA NO se alinean: `height="100%"` sobre `Card` es un atributo HTML inerte, no una prop. El README y… | `src/uix/blocks/pricing/pricing-plan.svelte` | ARREGLADO |
| A-25 | `pricing` | doctrina | BAJA | C | La demo documenta una elevación que el código se niega explícitamente a hacer y que el propio README lista como brecha abierta del canon. | `web/routes/blocks/pricing/+page.svelte` | CONFIRMADO |
| A-26 | `pricing` | doctrina | BAJA | C | El block exige a la app tres cadenas obligatorias para nombrar el ÚNICO estado que coordina, y justifica esa exigencia citando una lectura de B-7 que… | `src/uix/blocks/pricing/pricing-switch.svelte` | REFUTADO |
| A-27 | `testimonials` | doctrina | BAJA | C | El tipo público documenta un default de `variant` que el componente no usa. | `src/uix/blocks/testimonials/types.ts` | CONFIRMADO |
| A-28 | `testimonials` | doctrina | BAJA | C | `.Items` tipa toda la superficie de `AutoGrid` pero descarta en silencio parte de ella: el spread del consumidor va ANTES de props fijadas a mano. | `src/uix/blocks/testimonials/testimonials-items.svelte` | CONFIRMADO |
| A-27 | `testimonials` | doctrina | BAJA | C | El tipo público documenta un default de `variant` que el componente no usa. | `src/uix/blocks/testimonials/types.ts` | ARREGLADO |
| A-28 | `testimonials` | doctrina | BAJA | C | `.Items` tipa toda la superficie de `AutoGrid` pero descarta en silencio parte de ella: el spread del consumidor va ANTES de props fijadas a mano. | `src/uix/blocks/testimonials/testimonials-items.svelte` | ARREGLADO |
| A-29 | `pricing · testimonials` | percepcion | BAJA | C | Las vistas de 375/768 de estas dos demos no heredan NINGUNO de los ejes de la sección: siempre salen en claro, español y ltr — justo la rama estrecha… | `web/routes/blocks/pricing/+page.svelte` | CONFIRMADO |
| A-30 | `newsletter` | doctrina | ALTA | SV | UNA sola pulsación de Enter dentro del campo de correo emite DOS `commit-submit` con intents contradictorios (neutral y fulfill) y suenan DOS earcons… | `src/uix/blocks/newsletter/newsletter.svelte:105` | CONFIRMADO |
| A-30 | `newsletter` | doctrina | ALTA | SV | UNA sola pulsación de Enter dentro del campo de correo emite DOS `commit-submit` con intents contradictorios (neutral y fulfill) y suenan DOS earcons… | `src/uix/blocks/newsletter/newsletter.svelte:105` | ARREGLADO |
| A-31 | `newsletter` | doctrina | ALTA | SV | `signal-warn-invalid` está declarado `persistence:'untilFix'` pero NUNCA se limpia al arreglar: corregido el correo, el form sigue estampado en… | `src/uix/soma/components/form/form-provider.svelte.ts:129` | REFUTADO |
| A-32 | `site-header` | percepcion | ALTA | SV | El pack de sema de NavigationMenu NUNCA casa: su regla de cascada apunta a la parte `item` y el estampado cae siempre en el `trigger`, asi que el nav… | `src/uix/sema/components/navigation-menu.ts` | ARREGLADO |
| A-33 | `site-header` | percepcion | ALTA | SV | El nav del header emite `commit-select` + `affirm` — con earcon audible — por el simple HOVER del puntero, sin un solo clic: un barrido del raton… | `src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts` | ARREGLADO |
| A-34 | `site-header` | percepcion | ALTA | SV | Inversion de dominancia atencional: pasar el raton por encima del nav suena mas largo y mas fuerte que pulsar el CTA principal del block, y con… | `web/routes/blocks/site-header/SiteHeaderSite.svelte` | ARREGLADO |
| A-35 | `site-header` | percepcion | ALTA | SV | MUDOS: 4 de los 7 elementos interactivos visibles del header no emiten NADA, y son precisamente los que navegan — el brand «Acme», los… | `web/routes/blocks/site-header/SiteHeaderSite.svelte` | ARREGLADO |
| A-36 | `site-header` | percepcion | ALTA | SV | Carrera de estampado en el trigger del drawer: `contact-activate` vive 3 ms antes de que `emerge.open` lo pise en el MISMO nodo, asi que el feedback… | `src/uix/blocks/site-header/site-header.svelte` | CONFIRMADO |
| A-36 | `site-header` | percepcion | ALTA | SV | Carrera de estampado en el trigger del drawer: `contact-activate` vive 3 ms antes de que `emerge.open` lo pise en el MISMO nodo, asi que el feedback… | `src/uix/blocks/site-header/site-header.svelte` | ARREGLADO |
| A-37 | `banner` | doctrina | ALTA | SV | El block cablea el descarte con `variant="ghost"` y sin tinta, así que la ✕ se pinta SIEMPRE con el ink `primary` del Button sobre el lienzo que el… | `src/uix/blocks/banner/banner.svelte` | ARREGLADO |
| A-38 | `banner` | percepcion | ALTA | SV | La pestaña «Notas» afirma que la acción en línea HEREDA la tinta de contraste de la tira «en todos los roles y en los dos modos». La medición lo… | `web/routes/blocks/banner/+page.svelte` | ARREGLADO |
| A-39 | `newsletter` | doctrina | ALTA | SV | El ejemplo de uso de la puerta de entrada del block anuncia `onValidSubmit` — exactamente el prop que el block decidió NO exponer porque es un no-op… | `src/uix/blocks/newsletter/index.ts` | ARREGLADO |
| A-40 | `contact` | percepcion | ALTA | SV | El clic sobre «Enviar mensaje» —la accion principal de la seccion— no estampa NINGUN evento sema en el momento del gesto; la unica emision llega 915… | `src/uix/blocks/contact/contact-submit.svelte` | CONFIRMADO |
| A-40 | `contact` | percepcion | ALTA | SV | El clic sobre «Enviar mensaje» —la accion principal de la seccion— no estampa NINGUN evento sema en el momento del gesto; la unica emision llega 915… | `src/uix/blocks/contact/contact-submit.svelte` | ARREGLADO |
| A-41 | `contact` | percepcion | ALTA | SV | Un solo Enter dentro de un campo estampa DOS commits con intents distintos; y cuando el envio esta bloqueado estampa un commit que no commitea nada. | `src/uix/morfo/components/field.ts` | ARREGLADO |
| A-42 | `contact` | percepcion | ALTA | SV | El nombre accesible del boton de envio queda congelado en «Enviar» en los cinco estados: las palabras de estado que el block posee por B-7 nunca… | `src/uix/blocks/contact/contact-submit.svelte` | ARREGLADO |
| A-43 | `contact` | percepcion | ALTA | SV | `Contact.Reason` —la frase que el block existe para que ningun estado bloqueado quede mudo— no es region viva ni esta enlazada al control: cambia sin… | `src/uix/blocks/contact/contact-reason.svelte` | ARREGLADO |
@ -94,7 +94,7 @@ Columna **or.** = de qué sección del documento viejo salió la fila (`C` confi
| A-48 | `banner` | percepcion | MEDIA | SV | El descarte —el ÚNICO gesto que el block cablea— sólo expresa `contact-activate` (una pulsación); la desaparición de la tira no la expresa nadie, y… | `src/uix/blocks/banner/banner.svelte:59` | CONFIRMADO |
| A-49 | `banner` | percepcion | MEDIA | SV | La única llamada a la acción del banner («Ver qué cambia →») es completamente muda: ni estampa ni suena, en clic, en Enter ni en hover. | `web/routes/blocks/banner/BannerSite.svelte:56` | REFUTADO |
| A-50 | `site-footer` | percepcion | MEDIA | SV | 18 de los 24 elementos interactivos del footer son `Link` y TODOS son mudos: el block cuya función entera es navegar no tiene canal perceptivo para… | `src/uix/blocks/site-footer/site-footer.svelte:77` | REFUTADO |
| A-51 | `newsletter` | doctrina | MEDIA | SV | La acción principal del block no tiene `contact` propio: pulsar «Suscribirme» no estampa NADA sobre el control — el único estampado es el `commit`… | `src/uix/morfo/components/form.ts:90` | CONFIRMADO |
| A-51 | `newsletter` | doctrina | MEDIA | SV | La acción principal del block no tiene `contact` propio: pulsar «Suscribirme» no estampa NADA sobre el control — el único estampado es el `commit`… | `src/uix/morfo/components/form.ts:90` | ARREGLADO |
| A-52 | `site-header` | percepcion | MEDIA | SV | La costura `mobileOpen` cierra el drawer en SILENCIO: el unico camino de cierre que el block documenta como suyo es el unico que no se anuncia,… | `src/uix/blocks/site-header/site-header.svelte` | REFUTADO |
| A-53 | `hero` | percepcion | MEDIA | SV | Las dos acciones del hero viven en el mismo `Group`, con la misma jerarquia de par primario/secundario, y responden de forma opuesta: el CTA emite… | `web/routes/blocks/hero/HeroSite.svelte` | CONFIRMADO |
| A-54 | `site-header` | doctrina | MEDIA | SV | El morfo de `NavigationMenu` carga el intent evaluable en el MARCO: declara un unico evento, `commit-select` + `affirm`, para lo que solo despliega… | `src/uix/morfo/components/navigation-menu.ts` | ARREGLADO |
@ -112,7 +112,7 @@ Columna **or.** = de qué sección del documento viejo salió la fila (`C` confi
| A-66 | `pricing` | percepcion | MEDIA | SV | El plan destacado se grada `fulfill` en lo visual pero su consecuencia suena `affirm`, exactamente igual que los otros dos planes: la lectura… | `web/routes/blocks/pricing/PricingSite.svelte` | REFUTADO |
| A-67 | `faq` | percepcion | MEDIA | SV | Un solo clic que cambia de pregunta dispara DOS earcons emerge simultaneos y sin atenuar (collapse descendente + expand ascendente): un gesto, dos… | `src/uix/blocks/faq/faq-list.svelte:14` | CONFIRMADO |
| A-68 | `stats-band` | percepcion | MEDIA | SV | La coreografia de la banda esta desincronizada: la ENTRADA escalona, pero los contadores arrancan todos en el mismo frame, asi que una cifra ya va… | `src/uix/blocks/stats-band/stats-band-value.svelte:20` | CONFIRMADO |
| A-69 | `stats-band` | percepcion | MEDIA | SV | Las cuatro cifras no aterrizan nunca juntas y `duration` no significa segundos: con el valor por defecto (2), la cifra grande tarda 7,9 s en… | `src/uix/eidos/components/count-up/count-up.svelte:169` | CONFIRMADO |
| A-69 | `stats-band` | percepcion | MEDIA | SV | Las cuatro cifras no aterrizan nunca juntas y `duration` no significa segundos: con el valor por defecto (2), la cifra grande tarda 7,9 s en… | `src/uix/eidos/components/count-up/count-up.svelte:169` | ARREGLADO |
| A-70 | `feature-grid` | percepcion | BAJA | SV | DATO, NO DEFECTO: `feature-grid` no tiene ningun elemento interactivo — nada que emitir y nada que auditar perceptivamente en el. | `src/uix/blocks/feature-grid/feature-grid.svelte` | REFUTADO |
| A-71 | `site-footer` | doctrina | BAJA | SV | La página de demo pinta el block ANTES de su propio `h1`, así que el documento arranca en h2/h3 y el h1 llega el último — jerarquía de encabezados… | `web/routes/blocks/_lib/BlockDemo.svelte` | CONFIRMADO |
| A-72 | `contact` | percepcion | BAJA | SV | La ventana de vuelo (900 ms) no tiene canal perceptivo propio: ningun `sustain.*`, ningun spinner — solo el cambio de etiqueta visible. | `src/uix/blocks/contact/contact-submit.svelte` | REFUTADO |
@ -121,19 +121,23 @@ Columna **or.** = de qué sección del documento viejo salió la fila (`C` confi
| A-75 | `pricing` | percepcion | BAJA | SV | Volver a pulsar el CTA de un plan YA elegido re-emite un `commit-select`+`affirm` identico: no hay diferencia perceptiva entre «acabas de elegirlo» y… | `web/routes/blocks/pricing/PricingSite.svelte` | CONFIRMADO |
| A-76 | `testimonials` | percepcion | BAJA | SV | DATO, no defecto: el block no pone NINGUN elemento interactivo — no hay nada que pulsar y, por tanto, nada mudo; ademas sus Card no fingen afordancia. | `src/uix/blocks/testimonials/testimonials.svelte` | DATO |
| A-77 | `stats-band` | percepcion | BAJA | SV | DATO, no defecto: el block no pone NINGUN elemento interactivo — no hay nada que pulsar y por tanto nada que estampar. Toda su superficie perceptiva… | `src/uix/blocks/stats-band/stats-band.svelte` | DATO |
| A-78 | `stats-band` | doctrina | BAJA | SV | El <section> del block sale sin nombre accesible Y sin ningun encabezado dentro, asi que no es una region navegable ni por landmarks ni por… | `src/uix/blocks/stats-band/stats-band.svelte:26` | CONFIRMADO |
| A-78 | `stats-band` | doctrina | BAJA | SV | El <section> del block sale sin nombre accesible Y sin ningun encabezado dentro, asi que no es una region navegable ni por landmarks ni por… | `src/uix/blocks/stats-band/stats-band.svelte:26` | ARREGLADO |
| A-79 | `site-footer` | percepcion | SOSP | SV | Cambiar de idioma en el selector del footer cambia el régimen de toda la página (langs, formatos, dirección) pero suena y se estampa exactamente… | `web/routes/blocks/site-footer/SiteFooterSite.svelte:164` | CONFIRMADO |
| A-80 | `faq` | — | — | R | El block reimplementa la autogeneración del `value` que el canon ya hace, y README+index la venden como «la única conveniencia» de `.Item`. | — | ARREGLADO |
| A-81 | `cta` | — | — | R | El ejemplo del `index.ts` anuncia `variant="soft"`, una prop que se CORTÓ a propósito: copiarlo es un error de tipos. | — | ARREGLADO |
| A-82 | `cta` | — | — | R | La demo redeclara en app-land la unión que el block EXPORTA (`CtaLayout`), duplicando vocabulario. | — | CONFIRMADO |
| A-82 | `cta` | — | — | R | La demo redeclara en app-land la unión que el block EXPORTA (`CtaLayout`), duplicando vocabulario. | — | ARREGLADO |
| A-83 | `hero` | — | — | R | El block deriva el estado «la copia va sobre sólido» y lo aplica solo a dos de sus piezas, sin publicarlo: cada consumidor lo re-deriva en el punto… | — | ARREGLADO |
| A-84 | `feature-grid` | — | — | R | `align` está aplicado a medias: el block declara la prop y solo mueve la caja de la cabecera; el propio tipo instruye al app a repetir la alineación… | — | CONFIRMADO |
| A-85 | `site-header` | — | — | R | El `aria-label` que la demo escribe para el trigger del cajón nunca llega al DOM: el lector de pantalla anuncia la etiqueta genérica del canon. | — | CONFIRMADO |
| A-86 | `pricing` | — | — | R | El ejemplo de uso de la superficie pública del block no compila: omite dos props obligatorias. | — | CONFIRMADO |
| A-84 | `feature-grid` | — | — | R | `align` está aplicado a medias: el block declara la prop y solo mueve la caja de la cabecera; el propio tipo instruye al app a repetir la alineación… | — | ARREGLADO |
| A-85 | `site-header` | — | — | R | El `aria-label` que la demo escribe para el trigger del cajón nunca llega al DOM: el lector de pantalla anuncia la etiqueta genérica del canon. | — | ARREGLADO |
| A-86 | `pricing` | — | — | R | El ejemplo de uso de la superficie pública del block no compila: omite dos props obligatorias. | — | ARREGLADO |
| A-87 | `contact` | — | — | R | El propio gating del block deja INALCANZABLE el error por campo: con `progressive` los errores solo se escriben tras un envío inválido, y el envío… | — | ARREGLADO |
| A-88 | `team` | — | — | R | `marginTop: 'auto'` no ancla nada: el ítem de la retícula es el `<div>` del `Motion` (block, estirado), y el `Stack` de dentro mide su contenido, así… | — | CONFIRMADO |
| A-88 | `team` | — | — | R | `marginTop: 'auto'` no ancla nada: el ítem de la retícula es el `<div>` del `Motion` (block, estirado), y el `Stack` de dentro mide su contenido, así… | — | ARREGLADO |
| A-89 | `contact` | — | — | R | `sent` es un estado TERMINAL del que el block no sabe salir, y lo entra él solo: tras un envío correcto el formulario sigue editable pero la sección… | — | REFUTADO |
| A-90 | `contact` | — | — | R | Vocabulario paralelo: `ContactVerification` re-declara palabra por palabra el `ProofOfHumanStatus` del canon, y lo hace señalando a ese mismo… | — | ARREGLADO |
| A-91 | `banner` | doctrina | BAJA | F7 | El block re-expone el eje `intent` de `Banner` —que el canon declara fuera del sistema de color abierto, en su propio typedoc— sin registrarlo en sus Gaps… | `src/uix/blocks/banner/README.md` | ARREGLADO |
| A-92 | `pricing` | doctrina | BAJA | F7 | Una restricción estructural del modelo de cascada (sólo los presets CSS participan del stagger) vive en un comentario del block y falta en §D.13 de la doctrina… | `docs/theming/motion.md` | CONFIRMADO |
| A-93 | `stats-band` | doctrina | MEDIA | F7 | `Motion` no expone su momento «visto» (privado + `data-animation-pending`), así que un block no puede sincronizarse con el revelado sin violar B-6… | `src/uix/eidos/components/motion/motion.svelte` | CONFIRMADO |
| A-94 | `feature-grid · team · testimonials` | doctrina | MEDIA | F7 | Los tres declaran `= AutoGridProps` y ponen `{...rest}` antes de props fijadas: el consumidor tipa `align`/`width` y se descartan en silencio (A-28 ×3)… | `src/uix/blocks/testimonials/types.ts` | ARREGLADO |
## Reparto
@ -187,6 +191,194 @@ y 30 de las confirmadas ya ARREGLADAS.
el CANON** (`NavigationMenu`, `Form.Submit`, morfo de `Field`, `Link`), no en los blocks. Los
blocks componen bien; lo que está roto es el arco perceptivo del canon.
## Fase 6 — re-medición de los confirmados (2026-08-10)
Las 33 filas que seguían CONFIRMADAS, medidas otra vez ANTES de tocar código. El
motivo: de las 12 ALTA de percepción, 8 tenían la causa raíz en el CANON, y el
canon se arregló el 2026-08-06 — cada fila de percepción viva era sospechosa de
estar ya cerrada. **Seis lo estaban** (A-07 · A-30 · A-40 · A-51 · A-78 · A-82),
y sus fichas llevan la evidencia. Quedan **27**.
Método: lectura dirigida para doctrina; para percepción, Playwright headless
contra un dev server propio (`:5201`), con la puerta de hidratación honesta
(`#uix-blocks-display` + ida y vuelta reactiva) y dos sondas — contador de nodos
WebAudio parcheado antes de cargar y `MutationObserver` de `data-event*` sobre
todo el documento.
**Lo que la re-medición corrige del método anterior**, y que vale para la
siguiente: el cero de la escala tiene que ser el GESTO. Anclado a la carga, un
`contact-activate` a +270 ms parece llegar tarde; con un CONTROL en el mismo
entorno (el `<Button>` canónico del `cta`, +299 ms) se ve que esos 270 ms son
latencia del montaje. **Sin control en el mismo entorno, una latencia no es un
veredicto.** Y para el anillo de foco, `.focus()` programático NO enciende
`:focus-visible` en Chromium: hay que TABULAR, o se mide el caso que no importa.
> ⚠️ **LECCIÓN DE INSTRUMENTO (2026-08-10), y la más cara de esta pasada.** Las
> mediciones en navegador del 2026-08-09 se hicieron contra un dev server cuyo
> grafo era ANTERIOR a los arreglos ya commiteados: `npm run dev` reutilizó una
> caché de Vite de una ejecución previa. El delator fue aritmético — las
> duraciones medidas (7867 / 3567 ms, ratio 2.21) son EXACTAMENTE las que el
> comentario del arreglo cita como estado anterior («12 500 took 7.9 s while 48
> took 3.6 s, a 2.21 ratio»). **Que el commit esté en HEAD no significa que el
> servidor lo sirva.** Antes de medir percepción: servidor recién arrancado, y
> si un número reproduce con demasiada exactitud el «antes» documentado,
> sospechar del instrumento antes que del código.
> ⚠️ **SEGUNDA LECCIÓN DE INSTRUMENTO (2026-08-10) — el contador de audio no mide
> lo que yo decía.** La sonda parchea `createOscillator` y `createBufferSource` y
> los suma en UN contador. Con eso escribí «N earcons», que es una etiqueta que no
> medí: (a) un earcon sintetizado del catálogo por defecto crea DOS osciladores
> (sine + el intervalo de la voz) y TRES si `roughness` supera el umbral, así que
> el número de nodos no divide por earcon; (b) si un **sound pack** sustituye
> nombres por muestras (`applySoundPack` / `sounds:` en el boot), lo que se crea es
> un `createBufferSource` por reproducción y la aritmética cambia entera; (c) crear
> nodos **no es sonar** — un earcon puede salir a gain 0, y en este proyecto
> «silenciar con gain 0 no es silencio» ya está escrito. Verificado para las rutas
> medidas: `SOUND_CATALOGUE` es síntesis y la galería de blocks **no aplica ningún
> sound pack** (lo que registra `_lib/sema-packs.ts` son packs de sema de
> COMPONENTE — reglas de cascada—, que es otra cosa). Regla: el contador responde
> «¿hubo actividad de audio?» y nada más. Toda fila sobre NIVEL o simultaneidad
> audible se mide offline o se deja sin verificar.
>
> ⚠️ Y el error mayor del que éste es síntoma: **fui a re-medir A-67 sin leer su
> ficha**, que ya traía los gains del catálogo, el empate de `applyDominance` y la
> regla del canon incumplida — todo mejor razonado que mi nota. Presenté como
> hallazgo lo que el registro ya tenía escrito. La primera regla de este fichero
> es unión por `id`: eso incluye LEER la fila antes de tocarla, no sólo escribir
> en la correcta.
Lo medido, por `id`:
| id | qué se midió | resultado |
| --- | --- | --- |
| A-36 | trigger del cajón a 375px | ⚠️ RETIRADA — medida contra código caducado (ver la nota de instrumento abajo). Re-medida con servidor limpio: **ARREGLADA**. |
| A-47 | anillo de foco del `cta`, tabulando de verdad | REPRODUCIDA y cuantificada: el anillo existe (2px, `:focus-visible`) pero el navegador lo compone en `rgb(165,110,212)` sobre `rgb(142,78,198)` = **1.43:1**, contra el mínimo 3:1 de WCAG 1.4.11. En los DOS elementos del block. |
| A-48 | descarte del `banner` | REPRODUCIDA: sólo `contact-activate`; la desaparición de la tira no la expresa nadie. |
| A-53 | las dos acciones del `hero` | REPRODUCIDA: la primaria estampa `contact-activate` (+2 nodos de audio), la secundaria **0 eventos y 0 audio**, en el mismo `Group` y con la misma jerarquía. |
| A-60 | `AutoGrid` del `site-footer` | CONFIRMADA por lectura: `auto-grid.svelte:26` emite `repeat(auto-fill, …)` y el README del canon documenta la elección — las pistas vacías son la consecuencia declarada. |
| A-61 | submit bloqueado de `contact` | REPRODUCIDA: `disabled=true`, `aria-disabled=true`, **no enfocable** (`tabIndex` 0 pero `focus()` no lo alcanza), con `aria-describedby` apuntando a una frase que quien navega con teclado nunca llega a oír. |
| A-62 · A-74 · A-29 | demos de `team`, `feature-split`, `pricing` | VIVAS por lectura: los `IconButton` siguen sin `onclick`/`href`; los `href` siguen apuntando a fragmentos inexistentes; `previewSrc` sigue sin propagar ningún eje. |
| A-65 | CTA de un plan | REPRODUCIDA: un gesto, DOS eventos sobre el mismo nodo — `contact-activate` +293 ms y `commit-select`+`affirm` **+296 ms**, 4 nodos de audio. |
| A-67 | cambiar de pregunta en `faq` | **SIGUE REPRODUCIÉNDOSE** tras los cambios del framework: el segundo gesto emite `emerge-collapse` y `emerge-expand` en el MISMO milisegundo (con un solo gesto la fila sale falsa por construcción — hay que encadenar dos). ⚠️ Lo único que aporta esta re-medición es ESO. El análisis está en la ficha desde la fase 2 y es más completo que esta nota: gains del catálogo, el empate de `applyDominance`, la regla `CANON.md:253-254` que no se aplica, y la precisión de que «sin atenuar» es impreciso (0.08 + 0.06 <0.15deun`emerge.strong`:nohaypico).**Leer la ficha, no esta fila.**|
| A-68 · A-69 | contadores de `stats-band` | ⚠️ RETIRADA — las duraciones 7867/5083/3567 ms son las del código ANTERIOR al arreglo. Re-medidas con servidor limpio: 2005 ms cada una, desfase 0 → **A-69 ARREGLADA**. A-68 queda como tensión doctrinal, no como defecto: el canon decidió en A-69 que la banda **aterrice junta**, y escalonar los arranques rompería esa decisión. Necesita firma. |
| A-75 | re-elegir el mismo plan | REPRODUCIDA: el segundo gesto sobre el plan YA elegido re-emite lo idéntico (`contact-activate` + `commit-select`/`affirm`, +4 nodos). |
| A-85 | `aria-label` del trigger del cajón | ARREGLADA 2026-08-11 — no con el `prop-truthy` prescrito sino con la regla de VOCABULARIO (dos clases de precedencia): `aria-label` es NAMING (`ARIA_NAMING_ATTRS`), el compilador marca el plan `consumerWins`, viaja en el bag (nunca `dom.apply`) y `mergeProps` resuelve consumer-first. El espejo manual de `drawer-provider` retirado. |
| A-09 · A-11 · A-19 · A-25 · A-27 · A-28 · A-55 · A-71 · A-84 · A-86 · A-88 | doctrina, por lectura de la fuente que cada una ancla | TODAS VIVAS, sin cambios desde su ficha. |
| A-79 | idioma del `site-footer` | VIVA por lectura (sigue SOSP): el `Select` cambia el régimen de toda la página y no expresa nada distinto de cualquier otro select. No se midió con sonda — el gesto pasa por un `Select` portaleado y la fila es de sospecha, no de defecto medido. |
**Cabo cerrado de paso — los hermanos del `Field`.** El handoff sospechaba que
`password-field`, `search-field` y `textarea` duplicaban el `commit-submit`
dentro de un `Form` copiando el patrón del `Field`. **No lo duplican, y por una
razón que la sospecha no contemplaba**: `search-field`, `number-field` y
`combobox` no declaran el evento en su provider, y `password-field`
(`password-field-provider.svelte.ts:353-356`) y `textarea`
(`textarea-provider.svelte.ts:353-356`) llaman a `e.preventDefault()` ANTES de
estampar, así que el envío implícito del navegador no ocurre y el form nunca
emite su segundo commit.
⚠️ Eso destapa una pregunta distinta, que se anota sin veredicto porque **no se
ha medido**: si `password-field` cancela el envío implícito, un Enter sobre la
contraseña dentro de un `Form` estampa `commit-submit` (intent `neutral`) y el
formulario **no se envía** — que es el segundo defecto descrito en el comentario
del `Field`, no el primero. Necesita un `Form` con `password-field` y una
medición antes de convertirse en fila.
## Fase 7 — contraste doctrinal de los 15 (2026-08-10, 8 de 15 con la rejilla completa)
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 **estableciendo la composición como estrategia válida**, y
comprobar que respeta las directrices de eidos, sema, morfo y soma — más
**motion**, que faltaba en la rejilla y detectó el usuario preguntando si había
leído su doctrina. No la había leído: el primer veredicto sobre coreografía se
emitió sin ella y hubo que retirarlo (abajo).
La vara es la documentación vigente, no la lista de hallazgos de 2026-08-01. Los
ids nuevos que salgan de esta pasada continúan la serie desde **A-91**; los
existentes se corrigen en su ficha, nunca se renumeran.
**Rejilla, siete ejes**: (1) componente a componente contra su propio contrato ·
(2) el conjunto contra B-1…B-11 + regla de forma + doctrina de coordinación ·
(3) morfo · (4) sema — incluido **no re-emitir sobre un control que ya emite** ·
(5) soma · (6) eidos · (7) **motion**: §D.11 (la cascada ES `[data-stagger]` +
| `pricing` | **conforme** | A-86 vive (ejemplo que no compila) · **A-92** nueva |
| `faq` | **conforme** | A-19 vive (typedoc «centered») · A-67 explicada: rango idéntico, el `>` estricto de la dominancia es deliberado |
| `site-header` | **conforme** | sólo A-11, y es de la demo. B-6 ejemplar: `dom.isAtLeast`, nunca `matchMedia` propio |
| `site-footer` | **conforme** | A-60 aclarada: el arreglo es composición (`Grid` con `auto-fit`), camino que nombra el README del propio `AutoGrid` |
| `contact` | **conforme** — el listón del tier | A-61 se reduce: su segunda mitad ya está cerrada por `uix.announce`, y su disposición está medio obsoleta |
| `newsletter` | **conforme** (rejilla, sin medición en navegador) | gemelo de `contact`; sin filas vivas. Su comentario ya recoge el cambio del canon del 06 |
| `hero` · `cta` · `content-section` · `feature-split` | ⚠️ **BARRIDO PARCIAL, no conforme-verificado** | sólo se midió el eje del `{...rest}` y quién estampa `data-stagger`. Limpios EN ESE EJE (`content-section-body` cierra su tipo con `Omit`). Falta la rejilla: contratos de lo que componen · README · motion · sus filas vivas (A-25 · A-53 · A-74) |
| `team` | **conforme** (rejilla completa) | A-88 vive y es ADEMÁS deriva README↔código (el README ancla la fila de enlaces dos veces y el DOM no lo cumple) · A-94 le toca · A-62 es de la demo |
| `testimonials` · `feature-grid` | ⚠️ **BARRIDO PARCIAL** | **A-94** salió de ese barrido. Falta la rejilla y sus filas vivas: A-27 · A-84 |
| `stats-band` | **conforme** | A-09 y A-68 corregidas en su disposición · **A-93** nueva (la costura de `Motion`) |
Los trece restantes, por filas vivas primero: `pricing` · `faq` · `site-header` ·
### Precisiones de esta fase sobre fichas existentes (2026-08-10)
- **A-67** — la disposición «canon, no block» se confirma, con el mecanismo por fin nombrado: `applyDominance` (`engine.ts:885-892`) mutea una señal SÓLO si hay otra activa de rango **estrictamente mayor**, y `emerge-collapse` / `emerge-expand` tienen rango IDÉNTICO (ninguno evaluable, ninguno con intent). El comentario de `occurrenceRank` declara que el `>` estricto es deliberado, «so an equal-ranked newcomer stays audible». Que suenen los dos ES la conducta que la regla quiere. La pregunta abierta no es por qué no atenúa, sino si un `collapse`+`expand` del MISMO gesto debe contar como una ocurrencia — decisión de doctrina de sema, no defecto del tier.
- **A-88** — se confirma sin re-medir, y con un agravante de documentación: el README del block afirma DOS VECES el anclaje que no ocurre (mapa de composición, fila `MemberLinks`; y §Decisiones, «The links row is pinned to the bottom»). Las dos piezas del mecanismo verificadas por lectura el 2026-08-10: `team-member.svelte` no pasa `height` al `Stack`, y `motion.svelte:119-129` renderiza un div pelado sin `display` ni `height`, así que el `margin-top: auto` reparte cero. El contrafactual medido ya vive en la ficha.
- **A-62** — la atribución de la ficha (es de la DEMO, no del block) se confirma, y el contraste del conjunto añade un dato: la mini-página de `team` es la ÚNICA del corpus con cero anclas; sus ocho hermanas resuelven las navegaciones con `<Link href>`. El README del block justifica con «no stock faces», que no cubre el defecto: lo que falla no son las fotos, son dos navegaciones expresadas como `<button>` sin destino.
- **A-61** — la fila dice que quien navega con teclado «nunca llega al control NI SE TOPA CON LA FRASE que lo explica». La primera mitad sigue siendo cierta (medido: `disabled`, `aria-disabled=true`, no enfocable); **la segunda ya no**: `contact-reason.svelte` anuncia la frase por `uix.announce` en cada cambio de estado, así que la explicación SÍ llega a la tecnología asistiva aunque el control no se alcance. Esa costura entró el 2026-08-06 y la fila se midió el 05. Su disposición además está medio obsoleta: pedía dar `id` al `Text` y pasar `aria-describedby`, y ambas están ya en el código. Lo que queda es sólo la alcanzabilidad del control (`aria-disabled` en vez de `disabled` nativo, a cambio de bloquear el envío en el handler — el esquema no cubre `unverified`).
- **A-60** — la disposición «se arregla en el BLOCK» es correcta y ahora tiene camino: el README del canon `AutoGrid` documenta `auto-fill` como elección deliberada y nombra la salida — `<Grid templateColumns="repeat(auto-fit, minmax(…, 1fr))">`. Es composición con otro primitivo de layout del canon, sin tocar canon y sin CSS propio.
### A-94 — ARREGLADO · `feature-grid · team · testimonials` · doctrina · MEDIA
**Mecanismo** — A-28 no es de un block: es de TRES, y el barrido del tier lo mide. `feature-grid-items.svelte`, `team-members.svelte` y `testimonials-items.svelte` declaran su tipo público como `= AutoGridProps` —toda la superficie del primitivo— y colocan `{...rest}` ANTES de props fijadas a mano (`align`, `width`, y el propio `data-stagger`), así que un consumidor puede escribir `<Testimonials.Items align="start" width="60ch">`, tipar perfecto y no obtener nada. Prometer y descartar. El resto del tier está limpio: `content-section-body` cierra con `Omit<ProseProps,'measure'>` y los dos `Submit` con `Omit<…,'disabled'>`; en esos dos quedan `aria-label`/`aria-describedby` pisados, pero eso es deliberado (el block posee las palabras de sus estados, B-7) aunque el tipo siga exponiéndolos.
**Evidencia** — Barrido de los 114 ficheros del tier cruzando, por componente, el destructuring con los atributos escritos tras el `{...rest}` del tag raíz: 6 candidatos, 3 descartes reales. `feature-grid/types.ts:35` · `team/types.ts:36` · `testimonials/types.ts:24`, los tres `= AutoGridProps`.
**Disposición** — La solución ya está DENTRO del tier: `PricingPlansProps` (`pricing/types.ts:44-52`) declara un tipo cerrado sin rest, precisamente por esto. Cerrar los tres igual, o mover `{...rest}` al final y aceptar que `align`/`width` sean sobreescribibles. Lo único que no vale es prometer y descartar.
**Arreglo (2026-08-10)** — ARREGLADO 2026-08-10 — con una decisión de forma que NO tiene doctrina escrita detrás y se declara como tal. Los tres tipos quedan `Pick<AutoGridProps, 'minChildWidth' | 'columns' | 'gap' | 'children'>`, no un tipo cerrado escrito a mano como `PricingPlansProps`. Motivo: el `Pick` conserva los tipos EXACTOS del canon (incluido `ResponsiveProp`) y no puede quedarse atrás si el primitivo cambia. ⚠️ Y no es teórico: el primer intento los cerró a mano con `columns?: number` y rompió la demo de `team`, que pasa `columns={{ base: 2, md: N }}` — `svelte-check` lo cazó como +1 error sobre la línea base. Queda una tercera forma de tipar sub-parts en el tier (cerrado a mano · `Pick` · `= XProps` entero) sin regla que diga cuál; candidato a doctrina.
### A-91 — ARREGLADO · `banner` · doctrina · BAJA
**Mecanismo** — El canon avisa EN SU PROPIO TIPO (`eidos/components/banner/types.ts`, typedoc de `BannerIntent`) de que su eje `intent` «conflates two axes the doctrine separates»: `intent` es el eje EVALUATIVO (los 6 intents) mientras `primary`/`secondary`/`tertiary` son roles de jerarquía que pertenecen a `color`; y de que Banner, al no tener prop `color`, «is the one painted component outside the open colour system — resolving this is a pending decision, not an oversight». El block re-expone ese eje entero vía `Omit<BannerProps,'children'>` y su mapa de composición lo describe como «`intent` (the canonical color roles)», sin registrarlo. Su README registra CINCO huecos del canon (el `role="banner"` no anulable, la tinta de la ✕ con sus medidas, la persistencia del descarte, la animación diferida, el sticky) — la doctrina del tier es que «un hueco del canon se registra, no se falsea», y a éste le falta la fila.
**Disposición** — Una fila en los Gaps del block, como candidato a canon: el eje de pintura de `Banner` está fuera del sistema de color abierto y el block lo hereda sin poder corregirlo. Cero código.
### A-92 — CONFIRMADO · `pricing` · doctrina · BAJA
**Mecanismo** — `pricing-plan.svelte` documenta en un comentario una restricción estructural del modelo de cascada que la doctrina de motion NO recoge: «el preset debe ser CSS: el retardo del stagger vive en el `animation-delay` del preset (índice × each), y un driver JS (`spring`) no lo lee — con `spring-pop` aquí todos los planes entraban a la vez». Es decir, **sólo los presets CSS participan del stagger**; un preset de driver JS ignora el índice estructural y colapsa la cascada en una entrada simultánea. `docs/theming/motion.md` §D.13 se titula «Structural constraints of the CSS model» y enumera TRES (estructura de hijos directos · salida ENTER-ONLY · depuración); ésta falta, y es la que más fácilmente se incumple al elegir un preset por su nombre.
**Evidencia** — `src/uix/blocks/pricing/pricing-plan.svelte:19-27` (la restricción, aprendida midiendo) · `docs/theming/motion.md` §2 RFC §D.13 (las tres que sí están).
**Disposición** — Un cuarto punto en §D.13 de `theming/motion.md`. Cero código: la restricción ya se respeta en los seis blocks que estampan `data-stagger`.
### A-93 — CONFIRMADO · `stats-band` · doctrina · BAJA
**Mecanismo** — Reservado: ver la ficha de A-68, cuya disposición corregida nombra la costura que falta en `Motion`. Se registra aquí para que el gap de canon tenga id propio y no viaje escondido dentro de una fila de percepción: **`Motion` no expone su momento «visto»**. Lo guarda en `$state` privado (`motion.svelte:42`) y sólo lo materializa como `data-animation-pending` en el DOM, así que un consumidor que necesite sincronizarse con el revelado tendría que montar un observador propio — exactamente lo que B-6 prohíbe. `Cascade` ya expone el equivalente por contexto (`cascade.svelte`, `effectiveOpen`), así que la pieza existe en el canon; le falta a `Motion`.
**Disposición** — CANON: dar a `Motion` una costura para su momento visto (contexto o callback), espejando `Cascade`. Desbloquea A-68 sin que el block viole B-6.
## Veredictos re-medidos
_Una ficha por `id` verificado. La reclamación completa vive en el fichero del 2026-08-01._
@ -215,7 +407,7 @@ _Una ficha por `id` verificado. La reclamación completa vive en el fichero del
**Evidencia** — Hecho en 6 de los 8 READMEs sin declaración: feature-grid · feature-split · stats-band · team · faq · contact. Pendientes: pricing · testimonials.
**Disposición** — El resto se cierra con A-14; la regla mecánica llega en la fase 5.
**Disposición** — El resto se cierra con A-14; la regla mecánica llega en la fase 5.**Llegó** (2026-08-09): `blocks:check` exige `**Landmark + headings**` en todo README de block.
### A-04 — ARREGLADO · `contact` · doctrina · ALTA
@ -233,7 +425,7 @@ _Una ficha por `id` verificado. La reclamación completa vive en el fichero del
**Disposición** — La lista blanca mecánica entra en la fase 5 del guard.
**Disposición** — La lista blanca mecánica entra en la fase 5 del guard.**Entró** (2026-08-09): `blocks:check` rechaza todo especificador que la frontera dura 1 no nombre, y los arts públicos los DERIVA de `src/arts/*` en vez de listarlos a mano. El tier ya cumplía — cero violaciones al encenderla.
### A-06 — ARREGLADO · `site-footer` · percepcion · ALTA
@ -245,7 +437,7 @@ _Una ficha por `id` verificado. La reclamación completa vive en el fichero del
**Disposición** — Doctrina que merece escribirse: un `sequence:'post'` no sobrevive al desmontaje de su propio provider.
### A-07 — CONFIRMADO · `faq` · doctrina · MEDIA
### A-07 — ARREGLADO · `faq` · doctrina · MEDIA
**Mecanismo** — El README §Coordination enumera UNA costura («`value` is a bindable SEAM… A seam is not ownership») y el código expone DOS: `types.ts:28` declara `disabled` y `faq-item.svelte:16` lo reenvía a `Accordion.Item`. Y no es un passthrough genérico: `.Item` destructura exactamente `value, disabled, question, children` sin `...rest`, así que exponer `disabled` fue una elección deliberada que la doctrina del block no recoge.
@ -253,6 +445,9 @@ _Una ficha por `id` verificado. La reclamación completa vive en el fichero del
**Disposición** — Nombrar `disabled` en §Coordination (quién decide bloquear y por qué no necesita frase: la app pone el motivo en el texto de la pregunta). Fase 3, documental.
**Re-medición (2026-08-10)** — La costura que faltaba ya está escrita: `faq/README.md:43-45` nombra `disabled` como segunda costura reenviada y explica por qué no necesita frase propia (la app pone el motivo en el texto de la pregunta) — exactamente la disposición. Cerrada por lectura.
### A-08 — ARREGLADO · `cta` · doctrina · MEDIA
@ -267,7 +462,7 @@ _Una ficha por `id` verificado. La reclamación completa vive en el fichero del
**Evidencia** — `src/uix/blocks/stats-band/stats-band-value.svelte:20-22`, `src/uix/eidos/components/metrics/metrics-value.svelte:15-30`, `metrics.svelte:57-58` (`notifyUpdate` solo con `live`).
**Disposición** — Block-side: no pasar `value` cuando se compone `CountUp`. Fase 3.
**Disposición** — ⚠️ **CORREGIDA (2026-08-10, contraste doctrinal).** No pasar `value`**apagaría la función en vez de arreglarla**: el contrato de `Metrics.Value` dice que pasar `value` Y `children` es el uso previsto («when both are present, `children` renders and `value` still drives live detection») y que **`live` REQUIERE `value` para detectar el cambio**. Quitarlo deja la banda sin detección live. El defecto está en el CANON: `metrics-value.svelte:40` anuncia `String(next)` —la cifra cruda— teniendo el runtime de formato disponible, que es el mismo que `CountUp` usa para pintar. Arreglo: que el anuncio use el texto formateado.
### A-10 — ARREGLADO · `site-header` · doctrina · MEDIA
@ -302,13 +497,13 @@ _Una ficha por `id` verificado. La reclamación completa vive en el fichero del
**Disposición** — Cierra también A-24.
### A-14 — CONFIRMADO · `feature-split · pricing · testimonials` · doctrina · MEDIA
### A-14 — ARREGLADO · `feature-split · pricing · testimonials` · doctrina · MEDIA
**Mecanismo** — Ninguno de los tres README declara landmark ni jerarquía de encabezados. Es la misma falta que A-03, medida sobre el tier entero: 8 de los 15 READMEs no mencionan «landmark» ni una vez.
**Evidencia** — Grep sobre los 15 `src/uix/blocks/*/README.md`: 0 menciones en contact · faq · feature-grid · feature-split · pricing · stats-band · team · testimonials.
**Disposición** — Se arregla junto a A-03: párrafo «Landmark + headings» en los 8, y regla en `blocks:check` (fase 5).
**Disposición** — CERRADO en la fase 5 (2026-08-09), y con más alcance del que la fila pedía: la regla mecánica de `blocks:check` exige la declaración en los QUINCE, y al encenderla salieron **7** READMEs sin ella — los tres de esta fila más `banner`, `content-section`, `cta`, `newsletter` y `site-footer`, que mencionaban «landmark» de pasada en una celda de tabla pero no declaraban nada. Escritas las 7 leyendo la fuente de cada block, no la plantilla: `pricing` y `testimonials` son `<section>` pelados sin nombre (la app los nombra por `...rest`); `cta`, `newsletter` y `content-section` se nombran con su propio título por `aria-labelledby`; `site-footer` es un `<footer>` real (`contentinfo` mientras siga colgando de `<body>`); `banner` no puede ni poner ni quitar su landmark porque el canon estampa `role="banner"` después del rest — de ahí los dos banner de la página, que ya estaban en sus Gaps.
### A-15 — ARREGLADO · `contact` · doctrina · MEDIA
@ -341,7 +536,7 @@ _Una ficha por `id` verificado. La reclamación completa vive en el fichero del
**Evidencia** — `team/team-member-links.svelte`.
### A-19 — CONFIRMADO · `faq` · percepcion · BAJA
### A-19 — ARREGLADO · `faq` · percepcion · BAJA
**Mecanismo** — La promesa existe y el código no la cumple, pero SOLO cuando el cap muerde — la reclamación acierta en el mecanismo y exagera el alcance.
(b) o retirar la promesa de `src/uix/blocks/faq/types.ts:17` («measure-cap, centered» → «measure-cap»), y entonces conviene decir además que con `container='md'` el tope de 48rem es inerte.
Preferible (a): la asimetría con `container='xl'` es fea y el hermano ya la resuelve así. Severidad BAJA confirmada — nada visible en el block tal como se envía y se demuestra.
**Arreglo (2026-08-10)** — ARREGLADO 2026-08-10 y MEDIDO. `faq-header.svelte:12` pasa `marginX="auto"` — el mismo idioma que esta ficha ya nombraba en el block hermano (`content-section-media.svelte`). Medido en `/blocks/faq/preview`: el `Box` de `max-width: 768px` resuelve `margin-inline: 248px / 248px`, donde antes daba `0px / 0px` y la caja se anclaba en cross-start. Con los defaults del block el cap no muerde, así que nada se mueve hasta que un consumidor ensancha la sección — que es el caso para el que la promesa estaba escrita. El typedoc «centered» pasa a ser cierto en vez de tener que corregirse.
### A-20 — ARREGLADO · `faq` · doctrina · BAJA
@ -426,7 +624,7 @@ Preferible (a): la asimetría con `container='xl'` es fea y el hermano ya la res
**Disposición** — No se toca. Lo único cierto y menor es que el comentario cita B-7 en su forma anterior a la enmienda; si se roza el fichero por otro motivo, se acota la redacción.
### A-27 — CONFIRMADO · `testimonials` · doctrina · BAJA
### A-27 — ARREGLADO · `testimonials` · doctrina · BAJA
**Mecanismo** — El tipo público documenta un default que el componente no usa: `types.ts:26` dice «`variant` defaults to `soft`» y `testimonials-item.svelte:13` destructura `variant = 'outline'` (que además es la decisión razonada del README: el soft neutral es casi invisible en claro).
@ -435,7 +633,7 @@ Preferible (a): la asimetría con `container='xl'` es fea y el hermano ya la res
**Disposición** — Corregir el typedoc a `outline`. Fase 3, trivial.
### A-28 — CONFIRMADO · `testimonials` · doctrina · BAJA
### A-28 — ARREGLADO · `testimonials` · doctrina · BAJA
**Mecanismo** — `TestimonialsItemsProps = AutoGridProps`, así que el tipo promete toda la superficie de `AutoGrid`; pero el componente vuelca `{...rest}` PRIMERO y después fija `align="stretch"` y `width="100%"` a mano, y en Svelte el último gana. Un consumidor que pase `align` o `width` los ve descartados en silencio. `gap`, `columns` y `minChildWidth` sí funcionan porque están destructurados.
@ -494,7 +692,7 @@ No encaja en ningún falso positivo conocido (F15–F22), ni en «medir antes de
5. Candidato a guarda en `blocks:check`: que todo `previewSrc` de un demo pase por el constructor de URL del shell en vez de ser literal.
### A-30 — CONFIRMADO · `newsletter` · doctrina · ALTA
### A-30 — ARREGLADO · `newsletter` · doctrina · ALTA
**Mecanismo** — La duplicación del verbo es ESTRUCTURAL y está en el morfo, no en el block: `morfo/components/field.ts:29-34` declara `commit-submit` sobre `partRef('input')` con intent **neutral**, y `morfo/components/form.ts:17-22` declara `commit-submit` sobre `partRef('provider')` con intent **fulfill**. Un Enter dentro del input envía el form, así que la composición `Form > Field` dispara los dos: el mismo verbo, dos veces, con intents contradictorios.
@ -502,6 +700,9 @@ No encaja en ningún falso positivo conocido (F15–F22), ni en «medir antes de
**Disposición** — Deuda de canon (no es a11y): §6 de `PLAN-blocks-quality.md`. El block no puede evitarlo — no declara ninguno de los dos eventos.
**Re-medición (2026-08-10)** — La duplicación estructural está cortada en el origen: `soma/components/field/field-provider.svelte.ts:377` sale con `if (this.provider.form) return;` antes de estampar, y el comentario que lo precede documenta la medición que la fila reclamaba (dos commits a 11 ms con intents contradictorios). Un Enter dentro de un `Form` deja hoy UN solo `commit-submit`, el del form.
### A-31 — REFUTADO · `newsletter` · doctrina · ALTA
@ -540,7 +741,7 @@ No encaja en ningún falso positivo conocido (F15–F22), ni en «medir antes de
**Evidencia** — Sonda `G:/tmp/sonda-sema.mjs`, clic sobre `[data-navigation-menu-link]`: antes 0 eventos y 0 nodos de audio; ahora `commit-select` · familia=commit · intent=affirm sobre el propio link, 2 nodos de audio (un earcon).
### A-36 — CONFIRMADO · `site-header` · percepcion · ALTA
### A-36 — ARREGLADO · `site-header` · percepcion · ALTA
**Mecanismo** — La carrera existe y es peor que lo reclamado: el feedback de pulsación del hamburguesa no se trunca, NUNCA se pinta.
@ -585,6 +786,9 @@ Montaje del Content: antes del primer open `[data-drawer-content]` NO existe; ab
Colateral a arreglar en el mismo paso: `awaitExpression` (`src/uix/sema/chans/visual.ts:108-131`) mide animaciones ajenas del nodo compartido y alarga el hold de una señal por culpa de otra.
**Re-medición (2026-08-10, servidor limpio)** — YA NO SE REPRODUCE contra el código actual. Traza sobre el mismo nodo con servidor limpio: `contact-activate` estampado a +207,8 ms del clic y borrado a +410,2 — **202 ms de vida, por encima de su hold `glimpse` de 120 ms**, y el `emerge/open` NO aparece sobre el trigger. La carrera desapareció porque el evento ya no viaja a esa parte (`allowedTargets`, M1(i) `1c4303b10`). Queda vivo, pero como riesgo estructural sin manifestación conocida, que `unstampEventAttrs` borre ignorando la señal: si dos señales vuelven a coincidir en un nodo, el cleanup de una apagará a la otra.
### A-37 — ARREGLADO · `banner` · doctrina · ALTA
@ -611,7 +815,7 @@ Colateral a arreglar en el mismo paso: `awaitExpression` (`src/uix/sema/chans/vi
**Evidencia** — `newsletter/index.ts`.
### A-40 — CONFIRMADO · `contact` · percepcion · ALTA
### A-40 — ARREGLADO · `contact` · percepcion · ALTA
**Mecanismo** — El hecho es exacto y el defecto se sostiene, pero el origen es CANON, no el block. Cadena: `src/uix/blocks/contact/contact-submit.svelte:21` compone el canon `<Form.Submit>` de eidos. `src/uix/eidos/components/form/form-submit.svelte:27,38` es un wrapper PASSTHROUGH: no pasa el snippet `child` de soma, así que `src/uix/soma/components/form/components/form-submit.svelte:36` cae en su `<button>` nativo y pinta el cromo a mano en `form.css:85-190` en vez de componer el `<Button>` del framework — exactamente el bug de passthrough documentado (dialog-trigger 2026-06-19, card-group-title 2026-06-27) y contrario a la Regla 3 del patrón consumidor de Button. Consecuencia sema: el ÚNICO morfo que declara la familia `contact` para una acción es Button (`src/uix/morfo/components/button.ts:53-66`, `contact-activate` con `sequence:'pre'`, precisamente «para que la señal perceptual aterrice en el momento del clic, antes de cualquier trabajo asíncrono»). El morfo de Form (`src/uix/morfo/components/form.ts:16-25`) solo declara `commit-submit` con `sequence:'post'`, y el proveedor lo dispara DESPUÉS del await: `src/uix/soma/components/form/form-provider.svelte.ts:121-151` hace `await this.form.submit(e)` y solo entonces `runtime.trigger('commit-submit')`. Ese await encadena `onValidSubmit` del block (`src/uix/blocks/contact/contact.svelte:60`) con el `send()` de la demo (`web/routes/blocks/contact/ContactSite.svelte:37`, 900 ms). Resultado: el gesto sobre la acción principal no tiene tick de contacto en NINGÚN canal sema, ni micro-compresión `:active` (form.css solo trae `:hover`, `:focus-visible`, `:disabled`), mientras cualquier otro CTA de la misma galería —con sound/haptic ON por diseño, `web/routes/blocks/_lib/BootUix.svelte:62`— sí suena al pulsarse. Matiz honesto que la reclamación no dice: el botón NO queda mudo del todo — a ~45 ms del clic pasa a `disabled` + `data-pending` y reetiqueta a «Enviando…», o sea hay acuse de recibo en el canal de ESTADO; el agujero es el momento de contacto de sema (doctrina cap. 22 §8: «Contact inicia. Commit resuelve.»), y la asimetría con el resto de acciones de la página es perceptible.
@ -619,6 +823,9 @@ Colateral a arreglar en el mismo paso: `awaitExpression` (`src/uix/sema/chans/vi
**Disposición** — CANON, no el block. Arreglo en `src/uix/eidos/components/form/form-submit.svelte`: componer el `<Button>` del framework a través del snippet `child` de soma (Regla 3 del patrón consumidor de Button; `<Form.Submit>{#snippet child({props})}<Button {...props} {variant} {size} {color}>…</Button>{/snippet}</Form.Submit>`), con lo que el submit hereda `contact-activate` (sequence 'pre') en el gesto, el anillo de foco y la micro-compresión de press, y `form.css:85-190` suelta el cromo de acción duplicado. Mismo tratamiento para `form-reset.svelte` y los botones add/remove de auto-fields, que tienen el mismo passthrough. `src/uix/blocks/contact/contact-submit.svelte` no debe tocarse: compone el canon como toca.
**Re-medición (2026-08-10)** — MEDIDO de nuevo (Playwright headless, dev server propio en :5201, puerta `#uix-blocks-display` + ida y vuelta reactiva real tecleando los tres campos hasta que el submit se habilita). El submit estampa `data-event=contact-activate`, `data-event-family=contact`**sobre su propio nodo** (`[data-form-submit][data-archetype]`) a +270 ms del `mousedown`; el DOM confirma que compone el canon (`data-button`, `data-archetype=trigger`). CONTROL en el mismo entorno y misma sonda: el `<Button>` canónico de `/blocks/cta/preview` estampa a +299 ms. Es decir, el submit va incluso ANTES que el control — los 270 ms son latencia de mi montaje, no del block, y la asimetría que la fila describía ha desaparecido. Causa: `eidos/components/form/form-submit.svelte:30` compone `<Button>` por el snippet `child`, que es la disposición escrita en esta misma ficha.
### A-41 — ARREGLADO · `contact` · percepcion · ALTA
@ -844,7 +1051,7 @@ VISUALES del Button social, para comparar: reposo bg rgba(0,0,0,0) → hover bg
### A-51 — CONFIRMADO · `newsletter` · doctrina · MEDIA
### A-51 — ARREGLADO · `newsletter` · doctrina · MEDIA
**Mecanismo** — El morfo de `Form` declara la parte `Submit` (archetype `action`, `type=submit`) pero sus TRES eventos —`commit-submit`, `signal-warn-invalid`, `commit-reset`— apuntan todos a `partRef('provider')`: ninguno estampa sobre el control. Como `Form.Submit` no es el `Button` del canon, tampoco hereda su `contact-activate`. Pulsar la acción principal no estampa contact propio y el arco perceptivo empieza directamente por el resultado.
**Disposición** — Deuda de canon (morfo de `Form`), no del block y no a11y: §6 de `PLAN-blocks-quality.md`. Emparentado con A-30 y A-51 — el arco de `Form` es un hilo, no tres defectos sueltos.
**Re-medición (2026-08-10)** — Cerrada con A-40 y por la misma medición: la acción principal del formulario SÍ tiene contact propio en el gesto, porque `Form.Submit` compone el `Button` del canon y hereda su `contact-activate` (`sequence:'pre'`). Los tres eventos del morfo de `Form` siguen apuntando al provider —eso no ha cambiado ni tiene por qué—, pero la consecuencia que hacía de esto un defecto ya no se da.
### A-52 — REFUTADO · `site-header` · percepcion · MEDIA
**Disposición** — Block-side: `align="left"` en el Container (o `paddingX={0}` con márgenes explícitos) cuando hay descarte. Fase 3.
**Disposición** — ⚠️ **CORREGIDA (2026-08-10, contraste doctrinal).** El hecho es cierto y ahora está medido: a 1280px, con `onDismiss` el centro del contenedor cae en 619,5 frente al centro de la tira en 640 — **20,5 px de desvío**; sin descarte, 0. Pero **el mecanismo escrito es falso**: el `margin-inline` computado del Container es `0px / 0px`, así que los «tres márgenes automáticos» no existen. La causa es aritmética: el `Banner.Close` es un hermano flex que consume 41,1 px del ancho (1232 → 1190,9), y 41,1/2 = 20,55 = el desvío exacto. Por tanto `align="left"`**no arreglaría nada** — el contenedor ya no se centra a sí mismo. El arreglo real es que el descarte no compita por el ancho (fuera del flujo) o compensarlo con un hueco simétrico al inicio.
**Determinación (2026-08-10)** — **NO es arreglable desde el block con props públicas.** El canon pinta `[data-banner] [data-banner-close] { margin-inline-start: auto }` (`banner.css:150`) sobre una tira que es `display: flex` (`banner.css:20`), así que el `Close` es un hermano EN EL FLUJO y le resta su ancho al `Container` (medido: 41,1 px de 1232 → el mensaje se descentra 20,5 px, la mitad exacta). Sacarlo del flujo es CSS, y un `<style>` propio aquí no tendría justificación bajo D-BLK.2.
Dos salidas, y la primera es una DECISIÓN VISUAL que el block no debe tomar solo:
1. **Mover el `Close` dentro del `Container`** (composición pura, sin tocar canon): el `Container` recupera el ancho completo y el mensaje se centra respecto a la tira, pero el ✕ pasa a alinearse con la COLUMNA de contenido en vez de con el borde de la tira. En pantallas anchas se mete visiblemente hacia dentro. Es coherente con el «en columna por dentro» que el block promete, pero cambia el aspecto.
2. **Canon**: que `Banner` permita sacar el descarte del flujo, o que centre su contenido con independencia del `Close`.
Mientras no se decida, lo que hay que corregir es la PROMESA: el README del block afirma «full-bleed por fuera, en columna por dentro», y con `onDismiss` eso deja de ser cierto.
### A-56 — REFUTADO · `banner` · doctrina · MEDIA
@ -986,6 +1206,9 @@ Block (`src/uix/blocks/site-footer/site-footer.svelte:77`): la región de column
Canon (`src/uix/eidos/components/auto-grid/auto-grid.svelte:26`): `auto-fill` está cableado; no hay forma de pedir `auto-fit` desde fuera. Para una rejilla de tarjetas `auto-fill` es el default correcto (dos tarjetas no deben inflarse a media región), así que no lo cambiaría por defecto — pero le falta la palanca. Un `fit?: 'fill' | 'fit'` en `AutoGridProps` es la extensión mínima, y es lo que dejaría al site-footer resolverlo sin recetas propias (contrato B).
**Determinación (2026-08-10)** — el camino existe y es el que nombra el README del canon (`<GridtemplateColumns="repeat(auto-fit, minmax(…, 1fr))">`, y `GridProps.templateColumns` es `ResponsiveProp<string>`), pero NO es la sustitución trivial que parecía: el prop del block es `minColumnWidth?: AutoGridProps['minChildWidth']`, o sea `ResponsiveProp<LayoutLengthValue>`, así que construir la pista exige resolverlo antes (precedente en el tier: `content-section` resuelve sus tres ejes con `eidos.resolve()`). Coste real: resolver el valor + reescribir el track + comprobar que no se pierde el comportamiento responsive. No se toca a medias.
### A-61 — CONFIRMADO · `contact` · percepcion · MEDIA
@ -1119,8 +1342,10 @@ Emisión sema con LAS DOS sondas. (a) MutationObserver ligado a CADA nodo botón
**Disposición** — La costura es del block (`src/uix/blocks/stats-band/stats-band-value.svelte:22` compone `CountUp` sin gate; `stats-band-stat.svelte:18` pone el `Motion`), pero el arreglo necesita canon: que `src/uix/eidos/components/motion/motion.svelte` exponga su momento de revelado (un `onEnter` o el `seen` como parámetro del snippet, hoy privado en la línea 42) y, si se quiere sincronía exacta, el retardo de stagger efectivo — que hoy sólo vive en CSS (`motion.css:41-43` + `render-css.ts:1118`). Con eso, `StatsBand.Value` alimenta el `startWhen`/`delay` que `CountUp` ya acepta (`count-up.svelte:31,34`) y la cifra empieza a contar cuando aterriza, no antes. Alternativa mínima en canon sin tocar el block: alinear el observador de `CountUp` con el de `Motion` (mismo threshold/rootMargin) — corrige el caso del scroll lento pero NO el desfase de 210 ms del stagger.
**Disposición corregida (2026-08-10, contraste doctrinal)** — **No es arreglable desde el block sin romper el contrato B.**`theming/motion.md` §D.11 dice que «the children's timing is per-component realization», así que sincronizar contador y revelado SÍ es trabajo del block; pero `Motion` guarda su `seen` en `$state` privado y lo único observable es el `data-animation-pending` del DOM, y leerlo exigiría que el block monte un observador propio — **exactamente lo que B-6 prohíbe** («needing to observe something is the admission rule firing»). La pieza que falta tiene nombre: `Motion` necesita exponer el momento visto, como `Cascade` ya lo expone por contexto. ⚠️ Y hay un conflicto que hay que FIRMAR antes de tocar nada: A-69 acaba de fijar en el canon que la banda **aterrice junta** («by construction», medido: 2005 ms × 3, desfase 0), y arrancar cada cifra con su retardo de entrada rompería ese aterrizaje. Escalonar el arranque y aterrizar juntas son incompatibles.
### A-69 — CONFIRMADO · `stats-band` · percepcion · MEDIA
### A-69 — ARREGLADO · `stats-band` · percepcion · MEDIA
**Mecanismo** — El muelle de `count-up` está sobreamortiguado y su parada depende del TAMAÑO de la cifra, no de `duration`. En `src/uix/eidos/components/count-up/count-up.svelte:180-181` los parámetros son `damping = 20 + 40/duration` y `stiffness = 100/duration`; con el valor por defecto `duration = 2` → stiffness 50, damping 40 → w0 = 7.0711, zeta = 2.8284 (>1, sobreamortiguado). La forma cerrada de `runSpring` (líneas 108-133) se reduce, pasado el transitorio, a |y(t)| = 1.0345·|d0|·e^(−1.2917·t), con d0 = from − to. La condición de parada es `restThreshold = 0.5 * 10^-decimals` (línea 169), es decir 0.5 absoluto para enteros, luego el tiempo de asentamiento es t = ln(2.069·|d0|)/1.2917: crece con el LOGARITMO de la magnitud. `duration` sólo desplaza λ, nunca cancela el término log|d0| — medido, la razón settle(12500)/settle(48) = 2.21 constante para duration de 0.3 a 2, así que NINGÚN valor global de `duration` hace aterrizar juntas las cifras. `web/routes/blocks/stats-band/StatsBandSite.svelte:25-30` pone 12500 / 340 / 48 y `stats-band-value.svelte:22` monta `<CountUp to={count} />` sin `countOptions`, o sea con el default. Consecuencias reales, las tres verificadas en navegador: (1) los tres contadores arrancan en el MISMO frame y terminan a 3,57 / 5,09 / 7,88 s — 4,31 s de dispersión; (2) `duration = 2`, documentado en `types.ts:21` como «approximate count duration in seconds», da 3,9× para la cifra de cabecera; (3) `onEnd` se dispara en `delay + duration` (línea 189), 5,88 s ANTES de que la cifra grande deje de moverse. La cola no es imperceptible: el último dígito de 12.500 salta a 6,05 / 6,17 / 6,37 / 6,65 / 7,05 / 7,88 s, seis cambios visibles en los dos segundos finales.
@ -1128,6 +1353,9 @@ Emisión sema con LAS DOS sondas. (a) MutationObserver ligado a CADA nodo botón
**Disposición** — Se arregla en CANON, no en el block: `src/uix/eidos/components/count-up/count-up.svelte:169` (+180-181, 189). El umbral de reposo debe ser relativo al recorrido (p. ej. `restThreshold = max(0.5*10^-decimals, |to-from| * eps)`) o, mejor, `duration` debe cumplir su contrato: derivar λ de la magnitud para que el asentamiento real sea ≈ `duration` con cualquier cifra — y entonces `onEnd` (línea 189, hoy un temporizador ciego a `delay+duration`) debe dispararse desde la parada real del muelle, no desde un timer. Mientras el canon no cambie, `stats-band` sólo puede paliarlo con `countOptions.duration` por estadística, y eso serían números mágicos que ni siquiera igualan los aterrizajes (la razón 2,21 no depende de `duration`); mal parche. Documentar además en `types.ts:21` / `count-up/README.md:38` que `duration` NO es la duración observable mientras siga siendo un parámetro del muelle.
**Re-medición (2026-08-10, servidor limpio)** — ARREGLADA en `acda70f1e`/`89e42735e` y CONFIRMADO por medición contra código actual: las tres cifras de la banda (12.500 · 340 · 48) aterrizan en **2005 ms cada una, con 0 ms de desfase**. El contrato de `duration` («duración REAL del asentamiento para cualquier magnitud, λ derivada del recorrido») se cumple.
@ -1343,7 +1571,7 @@ EMISIÓN DE SEMA, las DOS sondas exigidas: (a) contador WebAudio parcheado en `A
**Evidencia** — Sonda Playwright headless (scratchpad/probe-A-77.mjs) contra http://127.0.0.1:5201/blocks/stats-band/preview. Puerta de hidratación: esperado `#uix-blocks-display` (nodo cliente) y luego 0 `[data-animation-pending]`. Ida y vuelta real (no hay inputs en la página, así que usé el comportamiento sólo-cliente): los 3 `[data-count-up-value]` pasan de la semilla SSR "0","0","0" a "12.050","328","46" a los 2,6 s — hidratado y rAF vivo en primer plano. Mapa de la banda (25 nodos bajo el `<section>` raíz): elementos interactivos = 0 (selector a[href],button,input,select,textarea,summary,details,[tabindex],[role=button|link|tab|checkbox|menuitem],[onclick],[contenteditable]); focusables = 0; `cursor:pointer` = 0; `[data-metrics][data-live]` = 0 de 4 `[data-metrics]`. Atributos data presentes: sólo estructurales/visuales (data-section, data-container, data-box, data-auto-grid, data-grid, data-stagger, data-metrics(-value|-label), data-count-up(-value), data-animation-style, data-animation-trigger, data-state, data-align, data-layout, data-size) — ni un data-event. Interacción: hover + click + dblclick sobre la banda y sobre los 4 stats, sus values y sus labels; 12 pulsaciones Tab (document.activeElement se quedó en BODY las 12 veces, `inBand:false`), más Enter y Space. Resultado con las DOS sondas: (a) contador WebAudio parcheado en addInitScript ANTES de la carga → createOscillator = 0, createBufferSource = 0 (1 AudioContext construido, sin nodos); (b) MutationObserver atado AL NODO de la banda (+ uno al documento) → 0 mutaciones `data-event*`, y 0 elementos con `[data-event*]` al final. Contra-prueba de que la sonda no es ciega: el mismo observer en /blocks/stats-band, al pulsar un Button real, capturó 10 mutaciones — `data-event=commit-toggle`, `data-event-family=commit`, `data-event-intent=neutral`, `data-event-phase=active`, `data-event-id=sig-0` y su retirada. 0 errores de consola.
### A-78 — CONFIRMADO · `stats-band` · doctrina · BAJA
### A-78 — ARREGLADO · `stats-band` · doctrina · BAJA
**Mecanismo** — `stats-band.svelte:26` emite `<section {...rest}>` sin nombre accesible propio y el block no pone ningún encabezado dentro (no importa `Heading`). Un `<section>` sin nombre no se expone como landmark, así que la banda no es navegable ni por landmarks ni por titulares, y el README no dice nada de ninguna de las dos cosas.
@ -1351,6 +1579,9 @@ EMISIÓN DE SEMA, las DOS sondas exigidas: (a) contador WebAudio parcheado en `A
**Disposición** — Documentar la posición (una banda de cifras puede legítimamente no ser landmark) y, si se quiere región, que el app pueda nombrarla por `...rest`. Fase 3, junto a A-22.
**Re-medición (2026-08-10)** — Las dos mitades de la disposición están cumplidas: el README declara la posición en su párrafo `**Landmark + headings**` (una banda de cifras es una tira de apoyo, deliberadamente ausente del árbol de landmarks y del outline) y `stats-band.svelte:26` derrama `...rest` sobre el `<section>`, así que una app que quiera región la nombra con `aria-label`. Cerrada junto a A-22.
@ -1375,7 +1606,7 @@ EMISIÓN DE SEMA, las DOS sondas exigidas: (a) contador WebAudio parcheado en `A
**Evidencia** — `cta/index.ts`.
### A-82 — CONFIRMADO · `cta` · sin dim · —
### A-82 — ARREGLADO · `cta` · sin dim · —
**Mecanismo** — El block EXPORTA `CtaLayout` (`index.ts:21`) y la demo lo redeclara a mano en tres sitios en vez de importarlo. Mismo patrón que A-11, agravado porque aquí el tipo canónico es del propio block.
@ -1383,6 +1614,9 @@ EMISIÓN DE SEMA, las DOS sondas exigidas: (a) contador WebAudio parcheado en `A
**Disposición** — Importar `CtaLayout`. Fase 3, junto a A-11.
**Re-medición (2026-08-10)** — La demo ya no redeclara nada: `web/routes/blocks/cta/+page.svelte:14` y `CtaSite.svelte:18` importan `CtaLayout` de `$blocks/cta`.
### A-83 — ARREGLADO · `hero` · sin dim · —
@ -1391,7 +1625,7 @@ EMISIÓN DE SEMA, las DOS sondas exigidas: (a) contador WebAudio parcheado en `A
**Evidencia** — Ver A-02.
### A-84 — CONFIRMADO · `feature-grid` · sin dim · —
### A-84 — ARREGLADO · `feature-grid` · sin dim · —
**Mecanismo** — El block declara un `align` de sección y solo lo aplica al `Stack` raíz (`feature-grid.svelte:30`); su propio tipo instruye al app a repetirlo: «Set the matching alignment on the header's own `Heading`/`Text` and on each `Item`» (`types.ts:22-25`), y la demo lo repite. No es «inventarle estado a un block de layout» —el eje YA está declarado—: es un eje declarado a medias, con cada consumidor re-derivándolo en el punto de uso. El propio tier tiene el precedente resuelto: `team` pasa su `align` a las partes por contexto.
@ -1399,8 +1633,11 @@ EMISIÓN DE SEMA, las DOS sondas exigidas: (a) contador WebAudio parcheado en `A
**Disposición** — Que `.Header` y `.Item` lean el `align` por contexto, como `team`. Fase 3.
**Arreglo (2026-08-10)** — ARREGLADO 2026-08-10 y MEDIDO. `feature-grid/context.ts` nuevo (mismo patrón y misma regla que `team`: es un DEFAULT, un `align` explícito en una parte gana), el root lo publica con un getter, y `.Header` y `.Item` lo leen. El typedoc que instruía al app a repetir el eje —«set the matching alignment on the header's own Heading/Text and on each Item»— se sustituye por la verdad: se pone una vez en la raíz. Medido en `/blocks/feature-grid/preview`: con `align=center` el `align-items` computado es `center` en la cabecera Y en las celdas; con `align=start`, `start` en ambas. `FeatureGridHeaderProps` gana el `align` (era `= BoxProps` y `svelte-check` cazó la falta como +1 error sobre base).
### A-85 — CONFIRMADO · `site-header` · sin dim · —
### A-85 — ARREGLADO · `site-header` · sin dim · —
**Mecanismo** — El `aria-label="Abrir navegación"` que la demo escribe en `web/routes/blocks/site-header/SiteHeaderSite.svelte:161` (dentro del `child` de `Drawer.Trigger`) SÍ llega al Button y SÍ se pinta en el DOM — y acto seguido lo pisa el canon.
@ -1428,7 +1665,26 @@ Resultado: el nombre accesible final es «Abrir cajón», la etiqueta genérica
**Disposición** — Se arregla en el CANON, no en el block (la demo compone correctamente y el block `site-header` sólo renderiza el snippet `mobileTrigger`). Dos puntos posibles, por orden de limpieza:
**RESOLUCIÓN (2026-08-11)** — Arreglado en el CANON, y no con el mecanismo que
esta disposición prescribía: replicar el `prop-truthy` de button era anotar 132
morfos y hacer dar al texto del consumidor una vuelta entera por el grafo
reactivo. La declaración del drawer era CORRECTA; la precedencia del runtime era
el defecto. Regla de VOCABULARIO (dos clases de precedencia, `morfo.md` Step 4):
`aria-label` ∈ `ARIA_NAMING_ATTRS` → el compilador marca el plan `consumerWins`,
el atributo viaja en el render bag de la parte (nunca por `dom.apply`, que era
la escritura post-render que pisaba) y `mergeProps` lo resuelve consumer-first.
Los espejos manuales del provider del drawer (líneas 440/1266) retirados.
Re-medido en `/blocks/site-header/preview` (2026-08-11): el SSR crudo YA trae
`aria-label="Abrir navegación"` (antes: sin atributo — los naming ships in the
bag también arreglan el hueco de SSR), y tras hidratación + click round-trip
(`data-state` closed→open→…) el label del consumidor sobrevive — la segunda y
tercera escritura del MutationObserver de la evidencia ya no existen. El caso
default (sin label del consumidor, `/uix/components/drawer`): «Abrir cajón» del
catálogo. Guard: `runtime.svelte.test.ts` («ships naming defaults in the bag»),
**Disposición ORIGINAL (superada)** — Se arregla en el CANON, no en el block (la demo compone correctamente y el block `site-header` sólo renderiza el snippet `mobileTrigger`). Dos puntos posibles, por orden de limpieza:
1. `src/uix/morfo/components/drawer.ts:181-185` + `src/uix/soma/components/drawer/drawer-provider.svelte.ts:444`: el `aria-label` del trigger debe ser un DEFAULT, no una imposición — condicionarlo a que el consumidor no haya aportado uno (patrón `prop-truthy` sobre un `aria-label` propio del trigger, como ya hace `src/uix/morfo/components/button.ts:164-169`), y en `drawer-trigger.svelte:26` dejar que el valor del consumidor gane sobre el del canon.
Paliativo si no se toca el canon: la demo no puede arreglarlo por sí sola (ninguna de las dos vías de llamada gana), así que documentar la limitación en `src/uix/blocks/site-header/README.md` sería lo único disponible.
**Re-medición (2026-08-10, servidor limpio)** — ARREGLADA. El trigger del cajón expone hoy `aria-label="Abrir navegación"` — el valor del CONSUMIDOR — donde antes el canon imponía «Abrir cajón». Medido en el DOM servido.
### A-86 — CONFIRMADO · `pricing` · sin dim · —
### A-86 — ARREGLADO · `pricing` · sin dim · —
**Mecanismo** — `PricingSwitchProps` declara las tres cadenas como REQUERIDAS (`monthlyLabel`, `annualLabel`, `'aria-label'`, sin `?`) y el ejemplo de la puerta de entrada monta `<Pricing.Switch annualLabel="Anual (−20%)" />`: faltan dos props obligatorias, no compila.
@ -1453,7 +1712,7 @@ Paliativo si no se toca el canon: la demo no puede arreglarlo por sí sola (ning
**Evidencia** — Ver A-44.
### A-88 — CONFIRMADO · `team` · sin dim · —
### A-88 — ARREGLADO · `team` · sin dim · —
**Mecanismo** — La cadena de cajas es: `AutoGrid` (grid, `align-items: stretch` por defecto) → el `<div>` que pinta `Motion` (`src/uix/eidos/components/motion/motion.svelte:119-129`, un div PELADO, sin `display` ni `height` propios) → el `Stack` del miembro (`src/uix/blocks/team/team-member.svelte:27-31`) → el `Group` de enlaces con `marginTop='auto'` (`src/uix/blocks/team/team-member-links.svelte:18`). El `stretch` de la retícula sí estira el div del `Motion` hasta la altura de la fila, pero ese div es `display: block`, así que el `Stack` de dentro —que es quien tiene el `display:flex; flex-direction:column` -- se queda en altura de contenido (`height: auto`). En una columna flex de altura automática el espacio libre es CERO, y `margin-top: auto` reparte cero. El token llega bien (`--box-margin-top: auto` vía `Box`, `box.css:242` + `formatLayoutSpace` que deja pasar la cadena `auto`), pero su valor usado se resuelve a `0px`. Resultado: la fila de enlaces cuelga justo detrás de la biografía y, con biografías de distinto largo —el caso EXACTO que el README declara como razón de ser de la regla (`src/uix/blocks/team/README.md:19` y `:69-71`)—, las filas de iconos quedan a alturas distintas dentro de la misma fila de la retícula. La reclamación acierta en el mecanismo Y en la consecuencia; también deja al README afirmando algo que el DOM no cumple.
@ -1465,6 +1724,9 @@ LÍNEA BASE sin biografía: todas las tarjetas 195px, desviación 0px — es dec
**Disposición** — Se arregla en el BLOCK, no en el canon: el defecto nace de la propia composición de `src/uix/blocks/team/team-member.svelte` (un `Stack` de altura automática dentro del div estirado del `Motion`). El arreglo mínimo verificado es que el `Stack` del miembro llene la celda —`height="100%"` en la línea 28 de `team-member.svelte`— o, equivalente, que el envoltorio del `Motion` sea columna flex. Nota de canon: `Motion` (`motion.svelte:119`) no expone ni `display` ni `height`, sólo derrama `...rest`, así que si se prefiere arreglarlo en el envoltorio hay que darle ese asa en el canon. Y corregir de paso el README del block (`README.md:19` y `:69-71`), que hoy documenta un anclaje que no ocurre.
**Arreglo (2026-08-10)** — ARREGLADO 2026-08-10 y MEDIDO. `team-member.svelte:28` pasa `height="100%"` al `Stack` (prop pública heredada de `BoxProps`, sobrevive el `Omit` de `StackProps` — comprobado en el contrato antes de dar el cambio por bueno). Medido en `/blocks/team/preview?align=center&columns=4&bio=true`: `linksTop` = [274,274,274,274], **desviación 0 px** (era 20), y `margin-top` reparte **20,297 px** donde antes daba `0px` en las seis tarjetas — el mismo valor que el contrafactual A de esta ficha, medido en su día por otra vía. El README del block, que afirmaba el anclaje dos veces, pasa a decir la verdad en vez de tener que corregirse.
detail:`block "${ownBlock}" imports block "${targetBlock}" — share via canon or duplicate consciously`
});
continue;
}
}
if(!isAllowedSpec(spec)){
out.push({
rule:'B-4',
file: rel,
line,
detail:`import outside hard boundary 1 (${spec}) — a block imports $uix, the public arts, $libs/forms and svelte; anything else is promoted or the app's`
});
}
}
returnout;
}
/** Scan a block README against B-9's template and B-8's landmark duty. Pure. */
detail:`README without the "${section}" section of the B-9 template`
});
}
}
if(!/^\*\*Landmark\b/m.test(src)){
out.push({
rule:'B-8',
file: rel,
line: 1,
detail:
'README without a `**Landmark + headings**` declaration — which sectioning element and which heading levels the block emits, and how the app adjusts them'
});
}
returnout;
}
/** slug → shipped, read from the demo catalog. Pure. */