You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/continue-cleanroom-fixes-20...

935 lines
73 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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`.
## ⏭️ ESTADO AL CIERRE 2026-07-12 + PRÓXIMO ARRANQUE
**La tabla §9 de la clean-room está prácticamente entera.** Fases F0–F4-C +
SEM-4 pusheadas antes de esta sesión (`4addfd7d`·`29e621b1`·`b55ca6ee`·
`082b6623`·`0e75890f`). Esta sesión (2026-07-12) commiteó (rama
`alpha-0.1-sec-dom`, **push al cierre**):
| Commit | Qué |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `05f0031c` | **media-player Batch-4** (SOM-4 cerrado, renderProps 35→0) + **THM-2** rollout: 14 superficies a la capa compartida + vocab alpha (`surface`/`surface-hover`/`hover`) + 4 props ampliadas a `ComponentColor` |
| `cc7e9a16` | **THM-2 avatar ROOT** → paleta a la capa compartida + 33 escalas |
| `0a5aa387` | **THM-2 CERRADO** — `Avatar.Badge` fuera de la paleta-por-escala **por diseño** (alineado con el ecosistema) |
| `d9c3d03f`·`75fa9400`·`34d1d46b` | **THM-4 CERRADO** — 5 hotspots adjudicados (4 = 0 deuda; stepper = 1 redundancia de API consolidada). Columna `unused` = informativa, sin regla |
**Método clave de esta sesión** (corrección del usuario): verificar SIEMPRE con
el **componente real + control real del demo**, nunca DOM fabricado
(`feedback_verify_with_real_components_not_synthetic_dom`).
**Queda de la §9 / cola lateral (arranque próxima sesión):**
- **F4-D · GAP-\*** (§7 del audit + §F4-D abajo): **ADJUDICADO con referencias +
ejecutado en su mayoría 07-12** (bloque «F4-D — adjudicación profesional»
abajo). ✅ selection/caret/cursor + scrollbar · ✅ **assets-variables** (fuentes
variable + API de ejes, `7d4f9500`) · **KEEP** layer-4 / depth-z / container-
queries (ejes reservados deliberados; corregido 1 comentario falso de depth-z)
· **4 límites deliberados registrados** (LIM-SND/ICON/TOUCH/RECIPES). **Queda:**
scrim (⏳ decisión de diseño) · gap-3 auto-métricas (opcional).
- **cascada-vs-firmas** (motion, cola lateral): ✅ **RESUELTO 07-12** — la
dirección estaba zanjada por doctrina (B.2: gana la entrada, la firma se
silencia); ejecutado **B** (test de regresión + comentario load-bearing + nota
en `motion.md`) como monitor de hazard. Endpoint «una firma por evento» /
`@layer` diferido y trackeado. Detalle en la tabla F4-D abajo.
- **THM-2 · rol-directo restantes** (cola, opcional): las superficies semánticas
ya colapsaron sus roles a la capa; ampliar su `color` a escalas NO tiene
sentido semántico (decisión tomada). No queda rollout pendiente salvo que se
quiera ampliar alguna más.
- Cola menor registrada al final (§Cola lateral): SOM-3 (~6 setTimeout), class-hooks
preexistentes, Task #31 pickers, etc.
Detalle por iniciativa: bloques «✅ EJECUTADA/ADJUDICADO» abajo (buscar el ID).
> **Fuente**: la tabla §9 de
> [`docs/audit-new-10-july/audit-active-uix.md`](../audit-new-10-july/audit-active-uix.md)
> — leerla ENTERA antes de tocar nada. El programa anterior está COMPLETO
> (cierre en [`docs/audit/components/_cierre.md`](../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-submit` al 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 ~6
`setTimeout` (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 → `toClassString` propio +
> 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 (keydown `continue` + 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 trigger` sin 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** (`assertCssVariableValue`
> rechaza `;{}` 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.ts` `eventsOpts.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ón `logger`.
## 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) e `image-provider` (delay de
> fallback) — consumidores cableados (date/time-field vía `soma.uix.timers`;
> avatar/image vía `eidos.timers`, verificado en vivo: 20 avatares, 0
> shells muertos). **Guard ENSANCHADO** de `soma/components` a 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): `!important` exige 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 en `docs/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)
1. **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).
2. **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 `emission` D.2).
3. **DOC-1 hogares APROBADOS**: build-contract §13 → component-guide;
intent-assignment → CANON (ejecutado abajo).
4. **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) · FAQ `event:*` de notes.md
> marcada SUPERSEDED por las motion signatures · `gradient` añ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**: `disabledDom` mixto 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 `close` propio (la reconciliación
> de-dialoged de 2026-06 los pasó a `expression: '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) flipaban `open.current = false` a pelo, **bypaseando** el
> `dismissWith` del 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.
>
> - `PickerShellHandle` gana `setPopoverDismiss?` + tipos `PickerCloseCause`
> ('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.dismissWith` al provider vía
> el handle; `null` al 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-set` fulfill / `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-commit`
> inexistente) 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 ajeno `text-blur.ts` de 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 con
> `fallbackTarget: branchEl/rowEl` + packs con `onBranch`/`onRow`).
> **Verificado en vivo**: `open · emerge · active` y `commit-select ·
commit · affirm` en 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ámicos `trigger(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 (`universalPaletteDecls` auto-completaba
> las 33 escalas para cualquier receta con tokens `palette-*`); 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 con
> `palette-*` (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-*`).
> - `universalPaletteDecls` eliminado; 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).
**avatar ROOT ✅ HECHO + verificado real (2026-07-12):** la arruga #1 (default
sutil sin-color) resultó **inalcanzable** — `avatar.svelte` hace
`color ?? 'neutral'`, así que el avatar SIEMPRE emite `data-color` canónico y el
matrix siempre casa (host = fallback muerto). Migración limpia sin borrar
intermedios:
- `avatar.svelte`: `isCanonicalColor` ahora incluye `PALETTE_SCALES` (una escala
va a `data-color`, no a la vía custom-CSS — clave: `teal` es TAMBIÉN un color
CSS, antes caía a `--avatar-color-custom`).
- `recipes/base.ts`: `_palette-{solid,surface,contrast,text,border}` (solo host
fallback; el forward rutea roles + 33 escalas) + `_bg/_fg/_border` pasan de
matrix (variant×color) a **variant-only** leyendo `--_avatar-palette-*`. El
matrix helper + los 48 intermedios SE QUEDAN (el badge sigue usándolos → 0
orphans). avatar.css SIN CAMBIOS (lee `--_avatar-bg/fg/border` igual).
- **Verificado real** (control del demo + picker de escalas añadido): chip "teal"
→ el avatar emite `data-color="teal"`; affirm+soft behavior-preserving
(`--_avatar-bg`=`--color-affirm-surface`); teal+soft=`--scale-teal-a2`
(surface translúcido), teal+solid=`--scale-teal-9`. `AvatarColor` ya era
`ColorRole | (string & {})` → acepta escalas sin cambio de tipo.
**Sub-parte `Avatar.Badge` — FUERA de la paleta-por-escala A PROPÓSITO
(decisión de diseño cerrada 2026-07-12, alineada con el ecosistema):** es un
chip de estado DECORATIVO (`aria-hidden`) con su PROPIO `color`/default
(affirm-solid), independiente del avatar. Acepta **roles/intents** (estado:
online→affirm, alerta→threat) **+ custom-CSS** (cualquier string). Eso es
EXACTAMENTE lo que hace la referencia: MUI/Spectrum/Bootstrap → color de badge
= semántico (rol/estado); Chakra `bg`/Mantine Indicator/Ant `color` → color
directo/CSS. **Ningún framework mete el badge en un sistema de escala de paleta
por instancia** — un badge de estado no necesita `teal`/`plum` como escala de
marca. Migrarlo (forward hand-authored por-parte + ~36 podas de intermedios)
sería sobre-ingeniería que se aparta de la convención. Por tanto **NO es deuda:
es el diseño correcto.** THM-2 CERRADO.
> **REVERSIÓN 2026-07-18 (decisión de diseño del usuario).** Los SUBCONJUNTOS
> por componente que THM-2 dejó (`AffirmativeColorRole`, `ProgressiveColorRole`,
> narrows por `Extract<>`) se revierten: `color` = sistema completo
> (`ComponentColorProp`: rol / intent / 33 escalas / valor crudo) en **todos**
> los componentes, sin excepciones — incluida la tinta de contenido (unión
> aditiva que conserva su eje) y los que THM-2 había dejado fuera "por diseño"
> (Avatar.Badge incluido, abierto en Fase 1 de la iniciativa). Proceso completo
>
> - guard estructural: [`open-color-cage-2026-07.md`](open-color-cage-2026-07.md)
> y el test "keeps every component `*Color` prop open" en
> `recipe-css-contract.test.ts`. La doctrina queda registrada en
> [`theming/reference.md §25`](../theming/reference.md).
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: 0` verde · 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-state` off→on +
> `aria-pressed` false→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-label` como override" (patrón carousel), pero
> carousel guarda su label porque su morfo declara un `translationRef` DEFAULT y
> soma añade la lógica prop-or-translation encima. El morfo de media-player
> declara `aria-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 en `runtime.part(...)` los sources que el morfo lee —
> `props: { label }` (buttons), `states: { on }` (mute/fullscreen/pip/caption,
> para el par `data-state`+`aria-pressed` vía `stateRef('on')`),
> `props: { direction }` (seek), `props: { type }` (time) — y en el `.assert()`
> cambiar `...runtimePart.props` por `...runtimePart.renderProps()`, dejando SOLO
> lo contract-only que soma posee (play-button `data-state` sin `value`;
> `data-disabled` de presencia; `data-media` de media/`aria-hidden` de
> poster/buffering salen de renderProps). El `data-media` de la parte `media` lo
> resuelve renderProps contra el source `media` de nivel-componente.
>
> **Ajuste de test**: en `media-player-provider.svelte.test.ts` los accesos
> indexados `props['aria-label']`/`['aria-pressed']`/`['data-state']` dejaron de
> tipar (el `as const` sobre el bag ya no expone esas claves — vienen del spread
> `renderProps()`: `Record<string,unknown>`) → convertidos a `toMatchObject`
> (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.ts` gana `'media-player': 0`
> con dossier de por qué es 0; el comentario "Still UNMIGRATED" retirado.
### THM-4 · media-player ✅ ADJUDICADO 2026-07-12 — 0 deuda real
Dossier de los **47 `unused`** (columna de `eidos-lint media-player`) contra la
doctrina `eidos.md §unused` (comportamiento/composición = legítimo; "declarado
para un eje visual que nadie consume, sin razón" = deuda). Clasificación
completa — **cero deuda silenciosa**:
- **21 · estados de botón** (play/mute/fullscreen/pip/caption/seek/settings ×
`data-state`/`data-disabled`/`data-direction`) → **composición legítima**: el
cromo del botón (state-layer, disabled, focus) vive en el `<IconButton>`
compuesto; `data-state` conduce el swap de glifo en Svelte (JS), no en CSS.
- **7 · sliders** (time/volume × `data-orientation`/`data-disabled`/`data-seeking`)
→ **composición**: el visual es el `[data-slider]` compuesto.
- **11 · flags de estado del root** (`data-paused`/`muted`/`seeking`/`ended`/
`error`/`fullscreen`/`pip`/`live`/`can-play`/`captions` + `controls='visible'`)
→ **espejos de comportamiento** del elemento media (el morfo declara toda la
superficie behavioral; `controls='visible'` es el estado por defecto — solo
`hidden` se estiliza).
- **6 · resto** (media-part `data-media` [lo estiliza el padre], time
`data-type='current'/'remaining'` [default; solo `duration` distingue],
captions/buffering/controls `data-*` propios [driven por el padre / superficie
opcional]) → legítimo.
- **2 (nota) · superficies v2 documentadas**: `settings-button` (data-state
open/closed + disabled) y el custom `captions` NO tienen provider/CSS/render —
son **roadmap v2 declarado a propósito** (ambos READMEs lo marcan v2). No es
deuda silenciosa; es superficie futura documentada. Candidatas a poda SOLO si
se abandona el v2.
**Conclusión**: el count alto es porque media-player es un composite grande (10
partes + 10 espejos de engine), no porque tenga deuda. Nada que consumir/podar
sin decisión de feature (p.ej. un badge LIVE o un overlay de error serían
diseño nuevo, no THM-4). **media-player adjudicado, 0 deuda.**
### THM-4 · stepper ✅ ADJUDICADO 2026-07-12 — mayoría legítima + 1 redundancia de API
De los **30 `unused`**: ~la mitad legítimos (`data-state='incomplete'` = estado
base; `data-orientation='horizontal'` = default; estados de content). El
hallazgo: el morfo declara en CADA parte (item/trigger/indicator/separator)
**DOS representaciones del mismo estado** — `data-state` (enum complete/current/
incomplete) **Y** los booleanos `data-current`/`data-complete`/`data-incomplete`
(`propRef`, severity optional). El propio CSS las usa **inconsistentemente**:
enum en trigger/indicator, booleano `data-complete` en el separator. Así que NO
es deuda-sin-consumidor (la forma booleana SÍ se consume en el separator) —
es **redundancia**: cada parte declara ambas formas y usa una.
**CONSOLIDADO ✅ (decisión de usuario 2026-07-12)** en `data-state` (canónico),
API pública unificada. 5 sitios: morfo (podados 12 declaraciones booleanas de
item/trigger/indicator/separator) · provider (podados los sources
current/complete/incomplete de item/indicator/separator + los writes explícitos
del trigger + los getters `isComplete`/`isIncomplete` — `isCurrent` se queda,
lo usan aria-current/tabindex) · provider-test (assertions booleanas fuera) ·
README (10 filas booleanas fuera) · separator.css (`[data-complete]` →
`[data-state='complete']`). **Verificado real** (demo, botón Next): estados por
`data-state` (complete/current/incomplete), booleanos ausentes, separator
completo pinta `--_stepper-palette-solid` vía `data-state='complete']`
(behavior-preserving). Gates: provider-test + contracts verdes · baseline 9 ·
**stepper unused 30→18** (total catálogo 1152→1140).
### THM-4 — lectura emergente (2 lotes)
media-player (0 deuda) + stepper (0 deuda-real, 1 redundancia de API —
**consolidada**): los hotspots son **mayormente legítimos** (composites grandes
- superficie behavioral); la deuda-sin-consumidor genuina es **rara**. Lo
accionable son **decisiones de diseño** (como el dual-API de stepper que el
usuario decidió consolidar), no podas mecánicas. Esto
CONFIRMA la predicción de la doctrina ("la mayoría son legítimos") y sugiere que
**la tasa de deuda NO justifica una regla de máquina** — THM-4 es sobre todo
adjudicación documentada.
### THM-4 · color-picker (29) + avatar (28) + time-range-picker (26) ✅ ADJUDICADOS 2026-07-12 — 0 deuda
- **color-picker (29)** → 0 deuda: `data-state='open/closed'` (root/content/
trigger) = **composición** (de-dialoged, `color-picker-content.svelte` compone
`PopoverContent` — el estado lo pinta el Popover) · `data-last-action=*` =
**sema/dismissal** (escrito por prewrites close-commit/cancel/dismiss, lo
consumen sema packs) · `data-format`/`data-disabled/invalid/readonly/required`
- area/channel-slider/swatch = behavioral + composición (Sliders/Field
compuestos).
- **avatar (28)** → 0 deuda: `[data-avatar][data-color='ROLE']` ×8 = **artefacto
de capa compartida** — mi migración THM-2 movió el color al forward
`[data-avatar][data-color]` presence-guarded, así que los per-rol dejaron de
vivir en avatar.css (viven en la capa) → el count SUBIÓ por una MEJORA, no
deuda · `data-variant='soft/solid'` = lo consume el **recipe token** (generado,
verificado 2 hits) — punto ciego del lint per-fichero (solo ve avatar.css) ·
badge `[data-color='ROLE']` ×8 = el matrix consume la forma compuesta
`[variant][color]`, no la per-rol suelta · `data-status=*` = estado de carga
behavioral (loading/idle SÍ se usan; loaded/error = default).
- **time-range-picker (26)** → 0 deuda: `data-state='open/closed'` = composición
(Popover) · `data-last-action`/`data-starting-style`/`data-ending-style` =
sema + motion @starting-style · `*-slider[data-endpoint/disabled]` +
day-period-toggle = composición (Sliders/Toggle) + behavioral (endpoint) ·
`data-disabled/required` = form-state.
### THM-4 — CONCLUSIÓN (5 hotspots adjudicados)
Los 5 mayores hotspots (156 `unused` juntos) están adjudicados: **media-player,
color-picker, avatar, trp = 0 deuda** (composición + behavioral + capa
compartida); **stepper = 1 redundancia de API, consolidada** (`75fa9400`).
**Deuda real total en los 5 mayores = 1 ítem** (arreglado). Esto CONFIRMA la
doctrina ("la mayoría son legítimos") y sella la decisión: **la columna `unused`
se queda informativa, sin severidad/regla de máquina** — la tasa de deuda no la
justifica. El resto del catálogo (~1140) es la cola larga de las mismas
categorías legítimas. Hallazgo colateral: migrar un componente a la capa
compartida (avatar) SUBE su `unused` (los per-rol pasan a artefacto de capa) —
el count alto no es señal de deuda. **THM-4 CERRADO.**
### Cola pendiente (sin orden fijo — algunas requieren decisión de usuario)
- **THM-2 rollout** — CERRADO (ver §THM-2; `Avatar.Badge` fuera por diseño).
- **THM-4 hotspots** — **CERRADO**: 5 hotspots adjudicados (media-player/color-
picker/avatar/trp = 0 deuda · stepper = 1 redundancia consolidada `75fa9400`).
Columna `unused` = informativa, sin regla. Dossiers arriba.
- **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):
1. **DEP-2** ogl fuera de package.json (+lockfile) · **DEP-1** clsx
inlineado (cerrar el "pending" de soma.md §12).
2. One-liners: **THM-7** (ejemplo sema.md → semaSelector) · **THM-6**
(menu-dial `--radius-full, 1px` → 9999px) · **EID-4** ("31-scale" → 33 ×2).
3. **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** `_resetCompileCache` implementar-o-borrar.
4. **SOM-2** keydown `continue` en match sin handler + test · **SOM-1**
censo de handlers async → si 0, quitar el await (si hay, DECISIÓN #3).
5. **SEM-2** logging por defecto (dev) en canales sin logger + vía
`void trigger` sin unhandled rejection (throw intacto para awaiters) ·
**SEM-3** fallback muerto applyDominance + cancelar timer de
awaitExpression · **SEC-1** serializador renderCssVariables rechaza
`;`/`}` embebidos + test.
6. **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
1. **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.
2. **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.
3. **No-cascada**: un fix → gates → el siguiente. Fix sistémico ⇒ guard que
lo fije (doctrina del programa 2026-06-26).
4. **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-block` vivo (`block-shake`,
> `--duration-moderate`, stamp `commit · 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.ts` tenía el MISMO agujero de enumeración —
> cerrado también (concatena hermanos; `color-picker` en
> `FIXED_TONE_COMPONENTS`); R-2.1 ahora caza hsl/hwb/lab/lch/oklab/oklch
> y escanea texto sin comentarios.
> - `commit-block` promovido a firma global de `EidosConfig.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)) retienen `animation` para SIEMPRE y
> sombrean cualquier firma de evento de igual/menor especificidad — el
> shake shipped de card-group YA estaba muerto en grupos con `motion`.
> 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) y
> `svg` (glifos) exentos con auto-vigilancia (si les aparece css, el
> guard exige morfo); `spin-field` queda como única capa sin contrato
> (F1). Gap real registrado en el dossier de card-group: `disabled` no
> 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 !important` global 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-bg` era hook muerto (0 consumidores) — retirado.
> Verificación en vivo: Close de dialog = UN anillo (el de Button, box-shadow
> none) · sonda desnuda `data-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 `#fff` pagado 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 sink `uix.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 `ogl` de `package.json` (0 imports; doctrina
zero-dependence en su forma más barata).
- **DEP-1** P2 — `clsx` fantasma (`soma/props/props.ts:1`): inlinear
implementación propia mínima (doctrina zero-dependence) y cerrar el
"pending decision" de `architecture/soma.md` §12.
- **MOR-1** P2 — `semaSelector`: escape propio de valores (isomorfo, sin
`CSS.escape` del 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:121`
re-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 — `keydown` corta en el primer match sin handler
(`runtime.svelte.ts:562`): `continue` en vez de `return 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 `logger` inyectado; el camino `void 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 `awaitExpression` cuando `finished` gana
(callback fantasma ≤1500 ms).
- **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
con `semaSelector` (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:
1. **Medir** el delta de CSS generado por superficie (33 escalas × 15
componentes en `generated/base.css` — el peso decide el alcance).
2. **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.
3. **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 `open` sin disparar el
evento): cablear `runtime.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)
1. **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).
2. **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.
3. **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, `gradient` omitido 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** documentar `disabledDom`
mixto en la tabla de contratos.
4. **Contramedida**: tras limpiar, `docs:check` enlaces 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** | **EJECUTADO 07-12** (bloque abajo). Los cursores YA estaban cableados por arquetipo; los huecos reales eran `::selection` + `caret-color` (0 en catálogo). | M |
| ✅ **Scrollbar global** | **EJECUTADO 07-12** (mismo pase). `scrollbar-color`/`scrollbar-width` estándar en `:root`; webkit descartado (ScrollArea oculta la nativa inline). | 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 |
#### ✅ EJECUTADO 07-12 — selection/caret/cursor + scrollbar global
Sección **«Foundation globals»** en `src/uix/eidos/archetypes.css` (no
archetype-keyed: tematizan el DOCUMENTO, no una parte), a especificidad mínima
(bare `::selection`, `:root`, `:where(...)`) para que cualquier recipe gane:
```css
::selection {
background-color: color-mix(in srgb, var(--color-primary-solid) 24%, transparent);
}
:where(input, textarea, [contenteditable]) {
caret-color: var(--color-primary-solid);
}
:root {
scrollbar-color: var(--color-content-muted) transparent;
scrollbar-width: thin;
}
```
- **Selección** = lavado accent translúcido 24 %, solo `background-color` (el
texto conserva su color → legible en claro y oscuro).
- **Caret** = identidad accent; en `:where()` para que un recipe de campo lo
pueda cambiar (inválido → threat).
- **Scrollbars** = estándar (Firefox + Chromium ≥ 121). Descartado
`::-webkit-scrollbar`: ScrollArea dibuja overlay y oculta la nativa inline
(`scrollbar-width: none` en su viewport, gana). Thumb = `content-muted` para
casar con el thumb de ScrollArea.
- **Cursores** = NO era hueco: ya cableados (trigger/close/action/item/option →
pointer · thumb → grab/grabbing · disabled → not-allowed · text zones →
input nativo + `cursor: text` en date-field/editable · field-trigger →
pointer). Confirmado con grep; sin cambio.
**Verificado real** (demo tags-input, `data-theme=base-dark`): caret del
`<input>` = `oklch(0.5556 0.1829 305.86)` = `--color-primary-solid` exacto ·
`scrollbar-width: thin` aplicado · regla `::selection` presente y token
resuelve. Precedencia probada: el harness de docs sobre-escribe el default
`:root` con su `--uix-line` (app > fundación). `npm run check` src/ = 9 baseline,
0 nuevos.
#### F4-D — adjudicación profesional + ejecución (2026-07-12)
Investigado con referencias (MD3 · Primer · Radix · Tailwind v4 · DTCG ·
next/font · Fontsource) + pasada adversarial. Constante: **casi cero deuda real**
— la mayoría son ejes reservados deliberados o premisas de audit stale.
| Ítem | Veredicto | Estado |
| ----------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **assets-variables** | Ejecutar (era trabajo real) | ✅ **HECHO** `7d4f9500` — defaults (Instrument Sans/Lora/Azeret Mono, **identidad intacta**) → variable woff2 + API de ejes arbitrarios (wdth/slnt/custom + `variationSettings`). Gap-3 (auto-métricas anti-CLS) = ergonomía opcional pendiente. |
| **layer-4** (`--{c}-{role}-{slot}`) | **KEEP** | Tier deliberado del modelo de 7 capas (`reference.md:387-425`), config-backed. La premisa del audit («sin consumidores», §9:424) es **STALE**: lo consumen banner (06-01), toast (10 tokens `color-mix` sin equivalente en L3), avatar-ring, editable, words + el fallback THM-2. MD3 `comp.*`/SLDS/DTCG mantienen el tier 1:1. El ataque «slim» sobre-generalizó (lo confundió con la purga de font-size de accordion/Field). |
| **jaula depth-z** | **KEEP** (reserved) | Proyección de `--z-index-*` sobre planos; excluido del bundle a propósito (stacking va por `--{c}-content-z`). Deuda real = 1 comentario falso en `render-css.ts:1170` → ✅ **corregido** este pase. |
| **jaula container-queries** | **KEEP** (open cage) · dogfood **declinado** | `[data-container]{container-type}` + emisor `@container` por-recipe, ya en `tsc.md` como eje reservado (Tailwind v4 envía CQ en core). **Dogfood evaluado 07-12 y declinado**: el framework dimensiona por `data-size` explícito (79 componentes), 0 son container-aware; retrofitear CQ en un recipe shipped introduciría un paradigma que el framework no usa → especulativo/contrived (regla «nada especulativo»). El emisor ya está probado por el recipe sintético `widget` del test. Se dogfoodea cuando un componente/app tenga una necesidad real de layout relativo-al-contenedor. |
| **jaula scrim** | ✅ **PODADO** (07-12) | Analizado con workflow (mapa + 4 disposiciones + estrés): las 4 colapsan en podar vs promover-a-paridad-z; C (cablear) **se hunde** (contradice el precedente z test-lockeado + consenso MD3/Radix/Vaul), D-reubicar invierte el verbo a retener. **Decisión: podar** — scrim NO gana el open-cage de z (z varía por-plano + es impintable-para-siempre; scrim ni varía ni tiene valor que sembrar, su jaula solo duplicaría `--color-overlay` → alias). Quitado de `DepthPlane`/`DEPTH_CUES`/validación/emisor + `applyDepth` doc; RFC enmendado (contrato + Phase-4 + atmósfera). El veil queda en `--{component}-overlay-*` / `--color-overlay` (= referencia). Cambio observable: cero (nunca se emitió). |
| **cascada-vs-firmas** (motion) | ✅ **FIJADO + TEST** (07-12) | Workflow (mapa + 4 disposiciones + estrés) **resolvió la dirección**: gana la entrada/stagger, la firma se silencia = **doctrinalmente correcto** (RFC B.2). La lectura «firma gana» del 2º análisis estaba invertida; C (girar) contradice B.2 y su red `channels:[]` es ilusoria; D (`@layer`) se hunde como front-line (pregunta semántica con herramienta global + inversión `!important` en reduced-motion). **Cero bug visible** (select = sonido+háptico; ítem en fade-out). Ejecutado **B**: test de regresión (`motion.test.ts`) que fija el tie (0,3,0) + orden de emisión + ausencia de firma (0,4,0), como **monitor de hazard** (no «precedencia intencionada»); comentario load-bearing en `render-css.ts`; nota en `motion.md`. Endpoint diferido (trackeado): «una firma por evento» / `@layer`, gatillado por un canal visual de commit real. |
**Límites deliberados registrados** (gap-log §7 — sin código, todos confirmados NO-bug):
- **[LIM-SND]** Sonorización por tema NO es un eje de theming — los packs sema son TS (`SOUND_TUNINGS`), no config de `ThemeDefinition`; los samples = recursos de producto/tema/branding fuera del canon (book-deviations **D.7**). Personalización sonora = app/dev.
- **[LIM-ICON]** El set de glifos no es sustituible por config — `IconPrimitiveSet` expone solo `size` + `strokeWidth`; los glifos son componentes del framework. Un tema re-dimensiona/re-traza, no cambia el set.
- **[LIM-TOUCH]** `--touch-target: 44px` es constante ergonómica física (WCAG 2.5.5 AAA / Apple HIG), no valor de tema; token `:root` en `archetypes.css`, deliberadamente fuera del theming (§37). _(Matiz: retunable a nivel `:root`, pero no es un eje de theming.)_
- **[LIM-RECIPES]** `ThemeDefinition` expone solo `color`+`shadow`; el ajuste per-componente (`recipes`) vive en `EidosConfig` (boot de app), no en el tema — un tema distribuible no re-tunea componentes sin código de app (§19, portabilidad perceptual).
**Cola F4-D restante:** ✅ **VACÍA** — scrim (podado `a52f80ce`) · motion (fijado+test `d8b984bb`) · gap-3 anti-CLS métricas (`efc3f4c3`, capsize) · container-queries dogfood (declinado — paradigma explicit-size). F4-D completo.
---
## 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
1. **SEM-1**: política canónica de assertive — propuesta: **intent** ∈
{threat, loss} (el libro), alineando el runtime.
2. **EID-1**: migrar fundación a outline + retirar el reset del app
(propuesta) — o anotar excepción en §32.
3. **SOM-1**: solo si el censo encuentra handlers async reales (si 0, se
quita el await sin preguntar).
4. **THM-2**: alcance del rollout de paleta (15 / subconjunto / re-redactar
promesa), tras la medición de peso.
5. **SEM-4**: prioridad y fecha (close ×5 · packs ×6 · flag `emission`).
6. **DOC-1**: hogares E1/E2 exactos del contenido migrado.
7. **DOC-2**: destino de los binarios del libro (commit vs puntero externo).
8. **F4-D**: qué iniciativas GAP se arrancan y cuáles se registran como
límite deliberado.
9. **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 check` fuera de `alpha|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:165 `outline: none` + anillo box-shadow con
tokens propios). Consecuencia real: en forced-colors/HCM su
`outline: 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 reclamando `animation` tras
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 (¿liberar `animation` durante `data-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.disabled` no alcanza el Collapsible compuesto (promesa de la
tabla de props a medias) — dossier `## Gaps` con «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` (trp `data-size` a endpoints) · Task #31 (pickers →
`PickerProvider<TValue>`) · `FieldLangsSnippetProps` sin uso.
- Hallazgos trascendentes del programa anterior (en `_cierre.md`):
holiday/event de range-calendar · naming gemelos tree · invalid de rango.

Powered by TurnKey Linux.