44 KiB
Plan de ataque — fixes de la auditoría clean-room (2026-07-10)
Handoff turnkey + plan aprobable. Creado: 2026-07-11. Rama alpha-0.1-sec-dom.
Fuente: la tabla §9 de
docs/audit-new-10-july/audit-active-uix.md— leerla ENTERA antes de tocar nada. El programa anterior está COMPLETO (cierre endocs/audit/components/_cierre.md). Este plan cubre todas las IDs de la §9 (P1/P2/P3 + GAP-*); nada queda sin fase asignada — la tabla de cobertura del final es el checklist.
Reconciliación de estado (la clean-room es snapshot del 2026-07-10)
- SOM-5 (P1) ✅ CERRADO post-snapshot: field
commit-submital Enter (33f346ed) + float-panel kb-gestos por handle-events (bdd17f04). - SOM-3 (P2) ◐: float-panel ya en
uix.timers(bdd17f04); media-provider es PUERTO Svelte-agnóstico exento por diseño. Quedan ~6setTimeout(image-provider, safe-polygon, dismissal, datetime/announcer, tags-input, announce/global). - Badge R-4.3 (única violación R-4.x viva en su §5.3) ✅ cerrada en el censo
R-1.x (
ee9786bc). Su matriz (102/32/0) es previa: hoy 134/0/0.
F2 ✅ EJECUTADA 2026-07-11 (lote mecánico completo)
Gates: matriz 135/0/0 · contracts 38/38 · eidos 310/310 (+pin SEC-1) · sema 178 · morfo 94/94 (+9 selectors) · runtime 43 (+3 pins) · props 8/8 (suite nueva) · baseline 57 · vocab exit 0 · accordion verificado en vivo (outline con foco de teclado real, box-shadow none). Los 13 ítems: DEP-2 (ogl fuera) · DEP-1 (clsx →
toClassStringpropio + suite; soma.md §12 cerrado) · THM-7 (los CINCO selectores manuales de sema.md al builder — y los ejemplos[data-toast-root]apuntaban a un part inexistente: la deriva que el builder previene, demostrada en el propio doc) · THM-6 · EID-4 ×2 · MOR-1 (escape isomorfo + validación de attr-names + 9 tests, incluida la prueba matches() real) · MOR-2 (partMarkerAttrúnica fuente + test de paridad) · MOR-3 (no-op borrado, 0 usos) · SOM-2 (keydowncontinue+ fixture) · SOM-1 (censo async = 0 → no-await, contrato V1 cumplido + pin) · SEM-3 (fallback muerto de applyDominance → skip defensivo; timer tope de awaitExpression cancelado al ganar las animaciones) · SEM-2 (trigger pre-attacha catch con logger →void triggersin unhandled rejection, throw intacto para awaiters + pin) · accordion→outline (patrón tabs, tokens de componente).Dos adjudicaciones de auditoría:
- SEC-1 = YA IMPLEMENTADO desde 2026-05-11 (
assertCssVariableValuerechaza;{}en valores — el auditor buscó escape y no vio el reject; ahora hay pin de test del path de VALOR).- SEM-2(a) = el composition root YA inyectaba su logger al engine (
active-uix.svelte.tseventsOpts.logger ?? logger) — los fallos de canal nunca fueron silenciosos en apps reales. Mi primer intento (fallback a console en el engine) lo TUMBÓ el guard de pureza de sema ("no console en producción") — el guard defendió la arquitectura contra el propio fix: revertido y documentada la garantía real en la opciónlogger.
F3 ✅ EJECUTADA 2026-07-11 (censos con guard)
Gates: matriz 135/0/0 (con R-4.7 viva) · contracts 38/38 · eidos 314/314 · attr-audit verde · baseline propio 57 (el árbol marca 58: +1 AJENO en
src/packs/ambient/ambient.svelte, sesión paralela Scene).
- SOM-3 CERRADO: censo real = cropper/tags-input/float-panel YA migrados; dismissal/safe-polygon/announce-global YA con forma sancionada (scheduler-preferred + fallback sin-timers). Migrados los 2 restantes:
datetime/announcer(clear del live-region) eimage-provider(delay de fallback) — consumidores cableados (date/time-field víasoma.uix.timers; avatar/image víaeidos.timers, verificado en vivo: 20 avatares, 0 shells muertos). Guard ENSANCHADO desoma/componentsa TODO soma (layers/ y datetime/ escapaban) y pasado a EVIDENCIA en vez de allowlist: fichero con setTimeout debe contener también.schedule(.- THM-5 CERRADO: censo real = 15 declaraciones (no 17; tabs/select ya no tienen). R-4.7 nueva (error):
!importantexige válvula same-line/* important: <razón> */— escaneo comment-blanked como R-2.1. Las 15 anotadas con su razón verdadera (inline de soma ×3 · fusión de radios ×8 · opt-out de presencia · cursor del grip · kills de reduced-motion ×2). Válvula + regla asentadas endocs/canon/recipe-contract.md§3/§4.- SOM-4 ADJUDICADO (otra sobre-estimación de la clean-room: "sin censo ni guard" — el script
soma-attr-audit.ts+ el guard con 49 componentes pinneados ya existían): backlog real = knob(1)/ mask-field(1)/timeline(2) — overrides de valor conscientes documentados en el call-site, PINNEADOS — y media-player (35 hits / 10 parts, cero renderProps) = el ÚNICO batch de migración restante ("Batch 4"), dimensionado y pendiente de programación (decisión de usuario).- THM-4 DOCTRINADO: criterio en
architecture/eidos.md§unused — "declarado y jamás estilizado" NO es error (attrs de comportamiento, artefactos de composición: css-field→spin-field, collapsible headless, selectores cross-component), la deuda es el attr declarado PARA un eje visual que nadie consume → consumir o podar. Quema de hotspots (media-player 47 · stepper 30 · color-picker 29 · avatar 28 · trp 26) = lotes con dossier, registrados abajo.
DECISIONES F4 TOMADAS (usuario, 2026-07-12)
- THM-2 = MECANISMO UNIVERSAL: una cascada genérica
--palette-*de 33 bloques (~3-4 KB total) que todas las recetas consumen con fallback propio, en vez del rollout per-component (medido: 21.2 KB button / 20.7 KB toggle → ×15 ≈ +315 KB sobre 345 KB). Sesión de diseño TSC propia (migrar button/toggle al slot genérico + convención + guards). - SEM-4 = AHORA, tras F4-C (2 sesiones: close polimórfico ×5 pickers
patrón Dialog + triggers de los 6 menús/árboles con el caveat
branch-vs-item + flag
emissionD.2). - DOC-1 hogares APROBADOS: build-contract §13 → component-guide; intent-assignment → CANON (ejecutado abajo).
- Orden: F4-C primero (ejecutada abajo) → SEM-4 → resto.
F4-C ✅ EJECUTADA 2026-07-12 (corpus documental)
Gates: docs:check 0 err / 0 warn con I6-links subido a ERROR (contramedida: nunca más 15 enlaces muertos tolerados) · matriz 135/0/0 · vocab exit 0.
- DOC-3: los 15 enlaces muertos, resueltos — repoint (CANON + docs/README → edición FINAL trackeada; el puntero HOMOGENEIZADO ya no existía) o des-link histórico (THEMING_AUDIT ×4, sema-findings, 7 rutas del MOTION_RFC que es doc-RETIRADA).
- DOC-2: cerrado — binarios FINAL ya trackeados por
4addfd7d; el único puntero vivo (CANON §Sources → v2_3.md) reapuntado a FINAL.pdf + book-map.- DOC-1: tabla «Build contract» MIGRADA a component-guide.md con estados modernizados (A3–A5 → LIVE con los guards de hoy: R-4.3, §32, size-bundle, §37…); banners de component-guide + eidos/components/README reapuntados; citas de CANON §3 y sema.md §expression historificadas ("THIS section is the canonical..."); lápida-redirect en el §13 del fósil.
- DOC-4: cadena de hold de sema.md → holds.ts/
SEMA_HOLDS_BY_INTENT(el campo SEMA_MAP eliminado 2026-07-06) · FAQevent:*de notes.md marcada SUPERSEDED por las motion signatures ·gradientañadido a los DOS capstones (reference §10 + channels §5, ahora "the sextet" de verdad) · nota de paleta de demo-authoring corregida (universalPaletteDecls + decisión THM-2 como sucesor del tracker).- DOC-5: recuentos fósiles → forma anti-frágil con fecha (≈140 componentes 2026-07; tabla de purge anotada como medición 2026-06; "25 soma"/"66 morfos" datados).
- DOC-6: §4.11 duplicado → §4.12 (0 citas externas verificadas) · "Known gaps" de Layout Batch 1 anotado HISTÓRICO/superseded · TODO(reconcile) ×2 RESUELTOS: N-6/N-7 recuperadas de git (
d68d2c45^) y canonizadas en eidos.md §pickers; tracker de paleta → sucesor F4-A · authoring.md E2 → canon/tsc.md · air-old des-linkado.- EID-3: excepción de placement físico asentada en la fila RTL de la tabla Build contract. AUX-2:
disabledDommixto documentado en la regla 2 de active-uix.md (writes no-op / reads-that-must-return THROW).
SEM-4 · SESIÓN 1 ✅ EJECUTADA 2026-07-12 — pickers (D.11)
Hallazgo de reconciliación: D.11 era snapshot de mayo — los morfos de los 5 pickers YA NO declaran
closepropio (la reconciliación de-dialoged de 2026-06 los pasó aexpression: 'delegated': su firma de apertura/cierre pertenece al POPOVER que componen). El agujero real HOY: los cierres programáticos (commit(),cancel(), auto-close al seleccionar) flipabanopen.current = falsea pelo, bypaseando eldismissWithdel Popover → save/cancel/select eran perceptualmente SILENCIOSOS (solo sonaban Escape/outside/botón-close, los caminos propios del popover).Fix (upgrade coherente con delegated): delegado de cierre por causa.
PickerShellHandleganasetPopoverDismiss?+ tiposPickerCloseCause('save'|'cancel'|'dismiss') /PickerDismissDelegate(bridge soma picker-shell).- UN inyector (norma N-8) en el eidos PickerShell root — monta dentro del subtree del Popover y entrega
popover.dismissWithal provider vía el handle;nullal desmontar.- Los 5 providers (date, date-range, time, time-range, color):
closeWith(cause)con fallback raw (composiciones headless); 14 sitios migrados — select/commit → 'save' (commit.save+fulfill, "seleccionar ES el compromiso"), cancel → 'cancel' (emerge).- El Picker GENÉRICO queda FUERA a propósito: ya dispara su propia firma (
commit-setfulfill /commit-cancel, diseño S9 "celebrate the new state") — añadir emerge encima doblaría señales (doctrina D.6 de repartir intensidad).- Comentario fósil de picker-shell-close.svelte (citaba
close-commitinexistente) corregido.Verificado en vivo (date-picker; los 5 comparten inyector+patrón): Done → stamp
close · commit · fulfill · active· Cancel →close · emerge · (sin intent) · active, en el content del popover, con cierre real (data-state='closed'). Gates: matriz 141/141 (la sesión paralela sumó 6 morfos de texto — todos PASS) · suites pickers/popover verdes · baseline propio 57 (+1 ajenotext-blur.tsde la sesión paralela).
SEM-4 · SESIÓN 2 ✅ CERRADA POR VERIFICACIÓN 2026-07-12 — D.6 ya estaba vivo
D.6 era otro falso positivo por registro stale (como SEC-1 y parte de SOM-4): los 6 providers YA emiten vía
runtime.trigger— dropdown/context (open/close/commit-select×2), menubar/nav-menu (commit-select), trees (emerge-expand/collapse+commit-select) — y el caveat branch-vs-item estaba resuelto de punta a punta (emisión confallbackTarget: branchEl/rowEl+ packs cononBranch/onRow). Verificado en vivo:open · emerge · activeycommit-select · commit · affirmen dropdown-menu. El registro D.6/D.11 de book-deviations quedó RECONCILIADO (notas RESUELTO con fecha y lección: el registro se actualiza en el mismo pase que el cableado).D.2 (flag
emission) — ADJUDICADO: DESCARTAR. Su propósito era hacer auditable la dormancia de D.6, que ya no existe; un check estático de emisión sería frágil (nombres dinámicostrigger(eventName)) y el flag sería feature especulativa sin consumidor (Simplicity First). Si una dormancia real reaparece, la lección anti-stale de arriba es la contramedida barata.Con esto SEM-4 COMPLETO (sesión 1 = pickers
b55ca6ee; sesión 2 = verificación + reconciliación del registro).
THM-2 ✅ MECANISMO + 5 SUPERFICIES 2026-07-12 (paleta universal)
El mecanismo genérico YA existía (
universalPaletteDeclsauto-completaba las 33 escalas para cualquier receta con tokenspalette-*); el hueco real era el prefijo de componente (--button-palette-*) → cada superficie pagaba ~21 KB, por eso solo button/toggle. Colapso ejecutado:
- Capa compartida (
renderSharedPaletteLayer): UNA cascada[data-color='{role|scale}'] { --palette-{slot} }(8 roles + 33 escalas, ~19 KB una vez) reemplaza las copias per-component.- Forward de presencia (
renderRecipePaletteForward): toda receta conpalette-*(público) o_palette-*(privado) recibe[data-{c}][data-color] { --{c}-palette-{slot}: var(--palette-{slot}, host-default) }. La guarda de PRESENCIA es la seguridad de anidamiento (un hijo sin color dentro de un ancestro teal se queda en su default — verificado en vivo).- Generalización a paleta privada: checkbox/radio-group/switch ganan las 33 escalas con CERO cambio de CSS (ya consumen
--_{c}-palette-*).universalPaletteDeclseliminado; 2 guards migrados al nuevo modelo (recipe-css-contract "shared layer + forward"; active-eidos-config contrast en la capa compartida).Verificado en vivo (button + checkbox):
color="teal"→ escala teal exacta ·affirm→ rol exacto ·primary→ primary · nested-nocolor en ancestro teal → primary (sin contaminación). Peso 345K → 329K (−16K) Y las 17 superficies ya PUEDEN usar la capa. Gates: matriz 141/141 · eidos 314 · recipe-css-contract 98/98 · contracts 38/38.
Tracker de rollout THM-2 (sucesor de pendiente_color_demos.md)
El rollout tiene DOS ejes (descubrimiento 2026-07-12 por crítica del usuario
"usa componentes reales, no divs"): (1) el token en la capa compartida
(colapso de peso + resolución CSS); (2) el tipo de la prop color del
componente, que GATEA qué colores son alcanzables. El rename del token hace que
la cascada resuelva CUALQUIER data-color que el componente emita — pero si la
prop está tipada ColorRole, el componente NUNCA emite una escala, así que
color="teal" es error de tipo y las 33 escalas quedan LATENTES. Verificación
correcta = componente REAL + control REAL del demo (el picker de 33 escalas),
nunca <div data-color> fabricado (feedback_verify_with_real_components_not_synthetic_dom).
Capa compartida (eje 1) — DONE (13): button, toggle, checkbox, radio-group,
switch · tags-input, stepper, badge, card, select, file-upload, editable ·
tag-group (2026-07-12, variant-aware: solid/outline→slots canónicos,
soft→slots ALPHA surface(a2)/surface-hover(a3) que AÑADÍ a
PALETTE_SLOT_STEP + capa compartida + forward — extensión aditiva del
vocabulario, aprobada por el usuario; merge outline-fg→text + poda de 8
intermedios huérfanos).
Prop color ampliada a ComponentColor (eje 2) — DONE + verificado real
(3): badge, tag-group, tags-input (2026-07-12, ColorRole→ComponentColor
- picker de 33 escalas en el demo + verificado con el componente real: click
en el chip "teal" del demo → el componente emite
data-color="teal"→ resuelve escala teal exacta, incl. surface alpha translúcido). button/toggle/card ya la tenían. Semánticos, se quedan restringidos (decisión usuario): checkbox/ radio-group (AffirmativeColorRole) · stepper (ProgressiveColorRole) · editable (EditableColorRole) · select/file-upload (ColorRole, accents de form-control). Para éstos, THM-2 = colapso de peso behavior-preserving (roles).
toggle-group ✅ HECHO + verificado real (2026-07-12): colcoreaba vía
composición cross-recipe en la paleta pública de Toggle (4 roles). Migrado a
ruta a la capa compartida: (1) añadí el slot hover(4) a PALETTE_SLOT_STEP
— behavior-preserving para Toggle (--toggle-{role}-hover=--color-{role}-hover)
y de paso arregla el hover-en-escalas de Toggle (estaba latente-roto); (2)
quité el bloque composition del recipe (era su ÚNICO consumidor — el DSL de
composición no puede presence-scope [data-color] sin valor) y hand-authoré
UNA regla en toggle-group.css: [data-toggle-group][data-color] [data-toggle-group-item] { --toggle-palette-{slot}: var(--palette-{slot}) }
(presence-guard = anidamiento seguro; cubre roles + 33 escalas); (3) amplié
ToggleGroupColor a ComponentColor (+ sentinels absent/unknown) + picker.
Test active-eidos-config que pinneaba la composición → actualizado (el DSL
emitComposition queda como infra sin consumidor actual). Verificado real:
chip "teal" → grupo emite data-color="teal" → item (un Toggle) obtiene
--toggle-palette-solid=teal-9, -hover=teal-4, -border=teal-7, -text=teal-11;
Toggle sin regresión (affirm-hover preservado, teal-hover ahora funciona).
Pendiente (1) — avatar: NO es migración mecánica, tiene 2 arrugas reales:
- El
matrix()resuelve color×variante juntos, y un avatar sin color con variant solid/soft tiene default sutil (neutral-soft, no neutral-solid, porque las decls del matrix exigen color): un split a_palette-*variant-only cambiaría ese default silenciosamente (regresión). - La sub-parte
badgetiene su PROPIO default (affirm-solid) y sigue el color del avatar por herencia — no encaja en el modelo presence-guard del forward (que rutea por eldata-colorPROPIO del elemento, y el badge no tiene el suyo). Migrar el root dejando el badge per-component es posible pero parcial. → Es una reescritura real de un componente visual shipped, con riesgo de regresión difícil de verificar sin screenshots. Pase enfocado con verificación visual real, no exprimido al final de sesión.
Rename limpio (patrón, para los que tenían slots canónicos): renombrar la clave
del token en recipes/base.ts a _palette-{slot} + los consumidores
--_{c}-{slot}/--_{c}-accent-{slot} en el .css; el forward auto-emite
[data-{c}][data-color]{--_{c}-palette-slot: var(--palette-slot, host)}.
Baseline estable = 9 errores AJENOS en src/ (4 connection + 1 nav-menu
sideOffset + 4 file-upload-soma props['aria-label'], todos pre-existentes/
track paralelo, probado con git stash). Registrado en docs/next-features.md §6.
media-player Batch-4 ✅ EJECUTADA 2026-07-13 — SOM-4 CERRADO (attr-dedup completo)
Gates: soma-attr-audit media-player = 0 hits (35→0) · provider test 9/9 · guard soma-attr-audit con
media-player: 0verde · contracts 38/38 · baseline propio 57 restaurado · navegador (dev real): DOM de cada botón con sus attrs de morfo emitidos por renderProps + labels LOCALIZADOS (Reproducir/Silenciar/Avanzar/Pantalla completa), y el camino reactivo verificado en vivo (mute click →data-stateoff→on +aria-pressedfalse→true +aria-label"Silenciar"→"Activar sonido"; play click → paused→playing + "Reproducir"→"Pausar").Hallazgo de reconciliación: el ceiling REAL era 0, no un residuo. El handoff asumía "dejar el
aria-labelcomo override" (patrón carousel), pero carousel guarda su label porque su morfo declara untranslationRefDEFAULT y soma añade la lógica prop-or-translation encima. El morfo de media-player declaraaria-label: propRef('label')a secas — el label ES un prop, así que lo correcto es ALIMENTARLO como source (props: { label: () => this.label }) y dejar que renderProps lo emita. Cero override.Patrón aplicado a las 10 sub-partes (
media-player-provider.svelte.ts): por parte, registrar enruntime.part(...)los sources que el morfo lee —props: { label }(buttons),states: { on }(mute/fullscreen/pip/caption, para el pardata-state+aria-pressedvíastateRef('on')),props: { direction }(seek),props: { type }(time) — y en el.assert()cambiar...runtimePart.propspor...runtimePart.renderProps(), dejando SOLO lo contract-only que soma posee (play-buttondata-statesinvalue;data-disabledde presencia;data-mediade media/aria-hiddende poster/buffering salen de renderProps). Eldata-mediade la partemedialo resuelve renderProps contra el sourcemediade nivel-componente.Ajuste de test: en
media-player-provider.svelte.test.tslos accesos indexadosprops['aria-label']/['aria-pressed']/['data-state']dejaron de tipar (elas constsobre el bag ya no expone esas claves — vienen del spreadrenderProps():Record<string,unknown>) → convertidos atoMatchObject(mismo aserto de runtime, type-safe; estilo del test de carousel). Valores en runtime idénticos byte-a-byte (los 9 tests siguen verdes).Cierre en el guard:
soma-attr-audit.test.tsgana'media-player': 0con dossier de por qué es 0; el comentario "Still UNMIGRATED" retirado.
Cola pendiente (sin orden fijo — algunas requieren decisión de usuario)
- THM-2 rollout — 12 superficies restantes (tracker en §THM-2 arriba): privadas-forward-ready [toggle-group/tag-group/tags-input/stepper] vs rol-directo-mini-diseño [badge/card/avatar/…]. Lote estilo S4.
- THM-4 hotspots — quema de selectores sin consumidor (media-player 47 · stepper 30 · color-picker 29 · avatar 28 · trp 26), dossier por componente.
- F4-D GAP-* — menú de personalización (§7): pendiente de SCOPING del usuario (qué se arranca vs qué se registra como límite deliberado).
- cascada-vs-firmas (motion) — cola lateral: las reglas
[data-stagger]sombrean firmas de evento de igual/menor especificidad.
(histórico) ARRANQUE F4 (iniciativas, cada una con decisión)
F0–F3 hechas. F4 espera scoping del usuario por iniciativa (ver §F4 abajo):
F4-A THM-2 paleta-33 (medir peso primero) · F4-B SEM-4 firma dormida
(close ×5 + packs ×6 + flag emission) · F4-C corpus documental
(DOC-1/2 decisiones + barrido DOC-3/4/5/6 + EID-3 + AUX-2 + docs:check a
error) · F4-D menú GAP-*. Lotes ya dimensionados que pueden entrar en
cualquier hueco: media-player Batch-4 (renderProps, 35 hits) · quema
THM-4 de hotspots · cascada-vs-firmas (motion, en cola lateral).
(histórico) ARRANQUE F3 (censos con guard)
F2 hecha (arriba). F3 según la sección de abajo, orden sugerido: SOM-3
(los ~6 setTimeout restantes a uix.timers, patrón float-panel, y retirar
exenciones del guard) → THM-5 (17 !important: anotación
/* important: <razón> */ + regla de máquina para no anotados) → SOM-4
(censo providers re-declarando attrs morfo → tabla + guard WARN + mecánicos;
si el volumen es grande, plan por lotes para aprobación) → THM-4
(doctrina para selectores sin consumidor + quemar hotspots). Tras F3 → F4
(iniciativas, cada una con decisión de usuario).
(histórico) ARRANQUE F2 (lote mecánico)
Estado al cierre 2026-07-11: F0 ✅ + F1 ✅ (commiteadas — 4addfd7d +
el handoff posterior). Gates vigentes que deben MANTENERSE: matriz
135/0/0 · contracts 38/38 · eidos 309/309 · sema 178 · baseline
57 · vocab exit 0. Leer los bloques «EJECUTADA» de F0/F1 (abajo) para
el contexto y arrancar F2 directo — es el lote SIN decisiones de usuario.
Orden sugerido (barato → delicado, no-cascada: ítem → gates → siguiente):
- DEP-2 ogl fuera de package.json (+lockfile) · DEP-1 clsx inlineado (cerrar el "pending" de soma.md §12).
- One-liners: THM-7 (ejemplo sema.md → semaSelector) · THM-6
(menu-dial
--radius-full, 1px→ 9999px) · EID-4 ("31-scale" → 33 ×2). - MOR-1 escape/validación en semaSelector (isomorfo, sin CSS.escape
del DOM) + tests con
"y]· MOR-2 des-duplicar convención de marker · MOR-3_resetCompileCacheimplementar-o-borrar. - SOM-2 keydown
continueen match sin handler + test · SOM-1 censo de handlers async → si 0, quitar el await (si hay, DECISIÓN #3). - SEM-2 logging por defecto (dev) en canales sin logger + vía
void triggersin unhandled rejection (throw intacto para awaiters) · SEM-3 fallback muerto applyDominance + cancelar timer de awaitExpression · SEC-1 serializador renderCssVariables rechaza;/}embebidos + test. - accordion → outline (hallazgo F1:
outline:none+ box-shadow = cero anillo en HCM) — con verificación visual, patrón tabs.css:102.
Después de F2 → F3 (censos SOM-3◐/SOM-4/THM-5/THM-4). Las iniciativas F4 (THM-2 paleta · SEM-4 firma dormida · DOC-1/2 · GAP-*) esperan decisión.
Principios de secuencia
- La máquina primero. Los fixes que amplían el enforcement (THM-1, MOR-4, guard de enumeración) van ANTES que el resto: destapan trabajo que las fases posteriores deben ver en el tablero.
- Rojo = parar la línea. Una suite/guard en rojo (hoy: THM-3) se arregla antes de cualquier otra cosa de la sesión. Ninguna sesión cierra en rojo.
- No-cascada: un fix → gates → el siguiente. Fix sistémico ⇒ guard que lo fije (doctrina del programa 2026-06-26).
- Iniciativas ≠ fixes. THM-2, SEM-4, DOC-1/2 y los GAP-* requieren decisión de alcance del usuario ANTES de ejecutar — este plan las dimensiona y propone, no las arranca en frío.
F0 — La máquina, en verde y completa ✅ EJECUTADA 2026-07-11
Resultado: matriz 135/0/0 (card-group dentro) · contracts 38/38 (guard MOR-4 nuevo) · suite eidos 306/306 · baseline 57 · vocab exit 0. Verificación en navegador: shake
commit-blockvivo (block-shake,--duration-moderate, stampcommit · risk) + chevrons a--opacity-muted(0.65 computado). Extras que destapó la ejecución:
- El spectrum de color-picker había sido creado para evadir los escáneres (su header lo admitía; reescrito con doctrina honesta) y
recipe-css-contract.test.tstenía el MISMO agujero de enumeración — cerrado también (concatena hermanos;color-pickerenFIXED_TONE_COMPONENTS); R-2.1 ahora caza hsl/hwb/lab/lch/oklab/oklch y escanea texto sin comentarios.commit-blockpromovido a firma global deEidosConfig.motion(block-shake+ reduce) — completaba un peldaño ausente (el ladder tenía fulfill/affirm/threat): ahora TODO rechazo de ToggleGroup lleva la firma de denegación en vez del settle de éxito.- Hallazgo sistémico (pre-existente, registrado abajo): las reglas de cascada de entrada (
[data-stagger][data-state='open'] > [data-animation-style], (0,3,0)) retienenanimationpara SIEMPRE y sombrean cualquier firma de evento de igual/menor especificidad — el shake shipped de card-group YA estaba muerto en grupos conmotion. Card-group lo resuelve re-afirmando a (0,4,0); el arreglo sistémico queda en cola.morfo/internal/existe (picker-shell.ts vive ahí); el guard MOR-4 acepta ambas raíces.icon-button(alias puro de Button, 0 css) ysvg(glifos) exentos con auto-vigilancia (si les aparece css, el guard exige morfo);spin-fieldqueda como única capa sin contrato (F1). Gap real registrado en el dossier de card-group:disabledno llega al Collapsible (la tabla de props promete selection+toggle).
| Ítem | Qué | Tamaño |
|---|---|---|
| THM-3 P2 | Voltear el canario vencido: toggle ya lee 0 class-hooks → lint.test.ts:110 a toBe(0). Suite eidos 305/306 → 306/306. |
S |
| THM-1 P1 | Extender component-audit.ts:698,756 a todos los *.css del directorio del componente (hoy solo {kebab}.css). Pagar lo que destape: hsl() ×8 en spectrum (anotar /* literal: physically-fixed */), opacity: 0.6 (calendar-select.css:27), fallback mágico var(--calendar-month-gap, 36px). Mismo espíritu que la válvula R-2.1: máquina primero, luego los fixes, no-cascada. |
M |
| MOR-4 P1 | Morfo para card-group (doctrina morfo-first, scope:['eidos'] si procede): leer CSS+wrapper existentes para derivar parts/attrs, recordar la decisión de cardinalidad 2026-06-27 (ToggleGroup OWNS cardinality) en el docblock. Entra en la matriz (→135) con dossier F-1.x y R-* limpias (incluida su opacidad literal, TOK-3). |
M |
| Guard de enumeración (contramedida) | La matriz enumera por MORFO_DIR — un dir de eidos sin morfo es invisible (así se escondió card-group). Guard nuevo: eidos/components/* ↔ morfos, con exenciones explícitas (picker-shell, spin-field = capas compartidas). |
S |
Salida de F0: matriz 135/0/0 · contracts 38/38 · suite eidos 306/306.
F1 — Coherencia P1: foco y voz ✅ EJECUTADA 2026-07-11
Resultado: matriz 135/0/0 · contracts 38/38 · eidos 309/309 (+3 guards de fundación) · sema 178/178 · soma 1041 verde (2 rojos ajenos: chronos = pista rota pre-existente · attr-audit = timeout de contención, PASA aislado) · baseline 57. Hallazgos de la ejecución:
- EID-1: el
outline: none !importantglobal que justificaba el box-shadow no existe — la única mención en TODO el árbol era el propio comentario de archetypes.css (fósil de la era Tailwind). Y el anillo de fundación pintaba EN PARALELO con los de receta (propiedades distintas, mismos tokens → duplicación invisible, verificado en vivo). Migrado a outline +--focus-ring-*envuelto en:where()(la fundación pierde ante cualquier receta al compartir ya la propiedad);--focus-ring-bgera hook muerto (0 consumidores) — retirado. Verificación en vivo: Close de dialog = UN anillo (el de Button, box-shadow none) · sonda desnudadata-archetype= anillo de fundación · light+dark. Asientos en reference §32 + changelog §32; bloque forced-colors queda como suelo defensivo.- EID-2: disabled
0.5→var(--opacity-disabled)(=0.4 — las recetas YA usaban el token: disabled se veía distinto según la capa) ×3
- close hover
0.85→var(--opacity-hover)(=0.9, token por rol). Verificado en vivo (0.4 computado).- F1.3: 3 guards nuevos en recipe-css-contract ("Foundation CSS discipline": color/typo/opacity sobre
eidos/*.css+lib/*.css, enumeración dinámica) + fallback fantasma#fffpagado en menu-indicator. picker-shell/spin-field ya entraban por dir.- SEM-1: runtime alineado al canal — prioridad = INTENT solo (threat/loss → assertive); pinneado con
liveRegionFixtureMorfo(commit+threat = el caso divergente) en runtime.svelte.test. AUX-1: doctrina "who owns what" en sema.md §Announce (1 sinkuix.announce· 2 emisores ·<Announce>app-level).
| Ítem | Qué | Tamaño |
|---|---|---|
| EID-1 P1 | Anillo de foco de la fundación (archetypes.css:162-167) en box-shadow vs §32 que canoniza outline. Propuesta: retirar el condicionante (el outline: none global de web/routes/layout.css — inversión de dependencia app→fundación), migrar el anillo a outline con --focus-ring-*, conservar la vía forced-colors. Verificación visual obligatoria (focus visible en varios componentes, light/dark; 57/130 recetas caen a este anillo). Alternativa (si el usuario prefiere): anotar la excepción en §32. DECISIÓN #2. |
M |
| EID-2 P2 | Misma pasada, mismo fichero: opacity: 0.5/0.85 literales en la fundación → var(--opacity-disabled) / token o anotación. |
S |
| Fundación bajo R-* (contramedida) | La fundación violaba su propia disciplina porque nadie la escanea. Añadir archetypes.css + capas compartidas sin morfo (picker-shell/*.css, spin-field.css) al alcance del escáner R-2.x/R-4.x o test dedicado. Nota THM-1 (2026-07-11): recipe-css-contract.test.ts ya cubre esas capas para color/typo/phantoms (enumera por directorio y ahora concatena hermanos); lo que falta aquí es la vía component-audit (enumera por morfo). |
S |
| SEM-1 P1 | Unificar la prioridad de live-region: runtime (family==='signal' ∧ intent∈{threat,loss} → assertive, runtime.svelte.ts:725) vs AnnounceChannel (solo intent, announce.ts:55). Propuesta: canónica = intent (lo que sugiere el libro); alinear el runtime + test que fije la política. DECISIÓN #1. |
S–M |
| AUX-1 P3 | Mismo territorio: párrafo de doctrina que ordene los TRES mecanismos de live-region (uix.announce zwsp-toggle · fallback del canal · <Announce> soma) — quién es para qué capa. |
S |
F2 — Lote mecánico P2/P3 (≈1 sesión, sin decisiones de usuario)
Batch de fixes pequeños, cada uno con su test donde aplique; gates al final:
- DEP-2 P2 — eliminar
ogldepackage.json(0 imports; doctrina zero-dependence en su forma más barata). - DEP-1 P2 —
clsxfantasma (soma/props/props.ts:1): inlinear implementación propia mínima (doctrina zero-dependence) y cerrar el "pending decision" dearchitecture/soma.md§12. - MOR-1 P2 —
semaSelector: escape propio de valores (isomorfo, sinCSS.escapedel DOM) + validación de nombres de attr en matchers string (selectors.ts:141-155); tests con"y]. - MOR-2 P3 — des-duplicar la convención de marker (
selectors.ts:121re-implementa lo que el compilador ya posee): exportarla de un solo sitio. - MOR-3 P3 —
_resetCompileCache()no-op: implementar (WeakMap reasignable) o eliminar según uso real. - SOM-1 P2 —
await handler()vs contrato "handler síncrono": censo de handlers async registrados; si 0 → quitar el await (contrato cumplido, timing más limpio incluso para síncronos); si hay async → DECISIÓN #3. - SOM-2 P2 —
keydowncorta en el primer match sin handler (runtime.svelte.ts:562):continueen vez dereturn false+ test con dos planes de la misma tecla. - SEM-2 P2 — política de errores: logging por defecto (dev) en fallos de
canales sin
loggerinyectado; el caminovoid trigger(...)no puede producir unhandled rejection en producción (catch+log en la vía void, throw intacto para llamadores que awaitean). - SEM-3 P3 — motor: fallback muerto de
applyDominance(engine.ts:518)- cancelar el timer tope de
awaitExpressioncuandofinishedgana (callback fantasma ≤1500 ms).
- cancelar el timer tope de
- SEC-1 P2 — endurecer el serializador de
renderCssVariables: rechazar;/}embebidos en valores + test. - THM-7 P2 — ejemplo de
sema.md§packs con selector manual → reescribir consemaSelector(el código real ya está limpio). - THM-6 P3 —
var(--radius-full, 1px)en menu-dial.css:293 →9999px. - EID-4 P3 — comentarios "31-scale" → 33 (
active-eidos.svelte.ts:653,arts/color/README.md:65).
F3 — Censos con guard (≈2 sesiones; SOM-4 puede parir lotes)
| Ítem | Qué | Tamaño |
|---|---|---|
| SOM-3 P2 ◐ | Migrar los ~6 setTimeout restantes a uix.timers.schedule (key+meta, patrón float-panel) y retirar sus exenciones del guard de contracts. |
S–M |
| THM-5 P2 | Doctrina para los 17 !important: anotación en línea (/* important: <razón> */) + regla de máquina R-* nueva que marque los no anotados. color-picker (4) y group (8) ya tienen razón — anotarla; drawer/float-panel/tabs/select: revisar (fix o anotar). |
S–M |
| SOM-4 P2 | Censo de providers que re-declaran role/aria-* en props getters en vez de renderProps() ("most existing providers", guía §Part props). Entregable: tabla censo + guard (WARN) + migración de los mecánicos; si el volumen es grande → plan por lotes tipo S4 con sizing para aprobación. |
M (censo) |
| THM-4 P2 | Doctrina para los 1150 selectores de contrato sin consumidor CSS: criterio en eidos.md (attr de comportamiento puro = legítimo y declarado; resto = consumir o podar del contrato) + quemar hotspots (media-player 47 · stepper 30 · color-picker 29 · avatar 28 · trp 26; words 149 excluido). |
M |
F4 — Iniciativas con decisión de usuario (cada una, su sesión)
F4-A · THM-2 P1 — paleta 33 por instancia (2/17 superficies)
Es un ROLLOUT, no un fix. Triple acción de la clean-room + sizing previo:
- Medir el delta de CSS generado por superficie (33 escalas × 15
componentes en
generated/base.css— el peso decide el alcance). - DECISIÓN #4: rollout completo (15), subconjunto razonado, o
re-redactar la promesa de demo-authoring §6 ("acepta las 33") al estado
real. Mecanismo real:
universalPaletteDecls(render-css.ts:1766) — el doc cita un helper inexistente (paletteScaleDecls): corregirlo. - Tracker con hogar: el registro de rollout borrado renace en
docs/theming/(contramedida: borrar un tracker exige puntero sucesor).
F4-B · SEM-4 P2 — firma perceptiva declarada y no emitida
Deuda "sin urgencia" desde 2026-05/06 — ponerle fecha (DECISIÓN #5):
- close polimórfico inerte (5 pickers togglean
opensin disparar el evento): cablearruntime.trigger('close-*')con el patrón dialog/popover — el intent per-evento ya está declarado. - packs dormant (6 morfos menú/árbol sin emitir): cablear triggers en providers (D.6).
- flag
emission(D.2) en el morfo para que esto sea auditable por la máquina (cumple la regla 2-de-3: sema + auditoría).
F4-C · Corpus documental (DOC-1/2 P1 · DOC-3/4 P2 · DOC-5/6, EID-3, AUX-2 P3)
- DOC-1 — migrar el contenido canonizado FUERA de audit/deprecated:
registro de intent-assignment (CANON §3 ←
_veredictos.md) y build-contract (component-guide ← ARCHETYPE_COHERENCE §13) a E1/E2; los audits quedan como historia. DECISIÓN #6 (hogares exactos). - DOC-2 — ancla editorial: existe edición FINAL sin trackear
(
docs/Disenando_lo_que_ocurre_FINAL.{pdf,docx}, 426 págs.) y los punteros apuntan a ficheros inexistentes (README.md:138 · CANON.md:295). DECISIÓN #7: ¿commitear los binarios (hoy excluidos por norma) o punteros a edición externa? Reconciliar en cualquier caso. - Barrido editorial (una sesión): DOC-3 enlaces muertos ×15 · DOC-4
secciones stale (cadena de hold sin
SEMA_MAP.families[*].hold,event:*obsoleto en notes.md,gradientomitido en reference §10 + channels §5,paletteScaleDecls) · DOC-5 recuentos fósiles (95/62/25/66/31 → reales) · DOC-6 menudencias (§4.11 duplicado, TODO(reconcile) huérfanos, stub TSC, enlace air-old) · EID-3 párrafo de excepción de placement físico en el mandato logical-RTL · AUX-2 documentardisabledDommixto en la tabla de contratos. - Contramedida: tras limpiar,
docs:checkenlaces muertos WARN→error (que nunca vuelva a tolerarse ×15).
F4-D · GAP-* — menú de personalización (§7; el usuario elige)
| Iniciativa | Contenido | Tamaño |
|---|---|---|
| Selection/caret/cursor | La fundación no emite ::selection, caret-color ni tokens de cursor (cableados por arquetipo) — los 3 huecos sistémicos visibles. |
M |
| Scrollbar global | scrollbar-color/webkit sistémico fuera de scroll-area. |
S–M |
| Layer 4 | --{c}-{role}-{slot} sin consumidores; la FAQ la marca candidata a colapso "si en 6 meses nadie la usa" → decidir: consumir o colapsar. |
decisión |
| Jaulas abiertas | scrim sin regla consumidora · --depth-{plane}-z 0 consumidores · container queries 0 consumidores — fecha de consume-o-poda para que no fosilicen. |
decisión |
| Assets variables | Motor de fuentes variables listo, assets aún TTF estáticos (rfc-typography §5). | M |
| Límites a registrar (sin código) | Sonido por tema = app/dev (D.7) · icon-set no temable · touch-target constante (§37) · ThemeDefinition sin recipes (§19). Registrarlos como decisiones en el gap-log y cerrar. |
S |
Tabla de cobertura (toda ID de §9 → fase)
| Fase | IDs |
|---|---|
| ✅ hecho | SOM-5 (33f346ed+bdd17f04) · badge R-4.3 (ee9786bc) |
| F0 | THM-3 · THM-1 · MOR-4 (+guard enumeración) |
| F1 | EID-1 · EID-2 (+fundación bajo R-*) · SEM-1 · AUX-1 |
| F2 | DEP-1 · DEP-2 · MOR-1 · MOR-2 · MOR-3 · SOM-1 · SOM-2 · SEM-2 · SEM-3 · SEC-1 · THM-7 · THM-6 · EID-4 |
| F3 | SOM-3◐ · SOM-4 · THM-5 · THM-4 |
| F4-A | THM-2 |
| F4-B | SEM-4 |
| F4-C | DOC-1 · DOC-2 · DOC-3 · DOC-4 · DOC-5 · DOC-6 · EID-3 · AUX-2 |
| F4-D | GAP-* (§7) |
Decisiones que solo puede tomar el usuario
- SEM-1: política canónica de assertive — propuesta: intent ∈ {threat, loss} (el libro), alineando el runtime.
- EID-1: migrar fundación a outline + retirar el reset del app (propuesta) — o anotar excepción en §32.
- SOM-1: solo si el censo encuentra handlers async reales (si 0, se quita el await sin preguntar).
- THM-2: alcance del rollout de paleta (15 / subconjunto / re-redactar promesa), tras la medición de peso.
- SEM-4: prioridad y fecha (close ×5 · packs ×6 · flag
emission). - DOC-1: hogares E1/E2 exactos del contenido migrado.
- DOC-2: destino de los binarios del libro (commit vs puntero externo).
- F4-D: qué iniciativas GAP se arrancan y cuáles se registran como límite deliberado.
- Contramedidas: adoptar rojo=parar · fundación bajo R-* · guard de enumeración · trackers con sucesor · deuda con fecha (book-deviations) · docs:check en error · clean-room periódica al cierre de cada programa.
Protocolo de verificación (invariantes)
npx tsx scripts/component-audit.ts→ 134/0/0 debe MANTENERSE (→135 al entrar card-group; nunca hacia abajo).npx vitest run src/uix/contracts.test.ts→ 37/37 (→38 con el guard de enumeración).npm run checkfuera dealpha|words|palabras|chronos→ 57.npx tsx scripts/morfo-vocabulary-check.ts→ exit 0 (WARN cropper conocido).- Suite eidos → 306/306 tras F0 y se mantiene.
- Cambios observables → verificación en navegador (gesto/tecla/inspección
real). Gotchas de entorno en la memoria del proyecto (rAF congelado en tab
desenfocado, dev server flaky, HMR de
.svelte.ts).
Exclusiones DURAS (sin cambios)
words/**·palabras/**·chronos(ACTIVE_DEV_TRACK) ·web/routes/alpha/**.- Demos: NO son fix — frame nuevo de sistema pendiente de diseño
(decisión 2026-07-10,
e53bf0e8); las D-* son verdict-neutral. - Nada a git sin petición; commits con paths explícitos (excluir
.claude/,rt.css, README stub, binarios del libro — salvo que la DECISIÓN #7 los incorpore).
Cola lateral (no bloqueante, registrada)
- accordion: anillo box-shadow +
outline: none(§32 stray) — el censo S5 no lo vio (accordion.css:165outline: none+ anillo box-shadow con tokens propios). Consecuencia real: en forced-colors/HCM suoutline: none(0,2,0) gana al suelo defensivo generado (0,1,0) → cero anillo en HCM. Migrar a outline en el lote F2 (mecánico, con verificación visual). - Cascada de entrada vs firmas de evento (sistémico, shipped): las
reglas generadas
[data-stagger][data-state='open'] > [data-animation-style='…'](0,3,0) siguen reclamandoanimationtras terminar la entrada (data-state persiste) y sombrean las firmas de evento (0,2,0) en cualquier ítem escalonado — commit-settle/announce-* incluidos. Card-group re-afirma la suya a (0,4,0) como parche local; el arreglo de sistema (¿liberaranimationdurantedata-event-phase='active'? riesgo: re-disparo de la entrada al soltar) es diseño de motion — adjudicar en F2/F4-B. spin-field: única capa compartida sin morfo/contrato (guard MOR-4 la exime con nota) — darle contrato en F1 junto a la fundación.card-group.disabledno alcanza el Collapsible compuesto (promesa de la tabla de props a medias) — dossier## Gapscon «implementar»; toca wrapper eidos de collapsible (reenvío) o corregir el doc.- class-hooks preexistentes: color-picker(2) · time-picker(8) ·
time-range-picker(12) — extensión del codemod S1 cuando cuadre; nota:
.range-time/.picker-time-row(CSS secundarios, ahora escaneados) pertenecen al mismo censo. task_d66356de(trpdata-sizea endpoints) · Task #31 (pickers →PickerProvider<TValue>) ·FieldLangsSnippetPropssin uso.- Hallazgos trascendentes del programa anterior (en
_cierre.md): holiday/event de range-calendar · naming gemelos tree · invalid de rango.