20 KiB
Auditoría del sistema de theming — Fase 1 (clean-room)
- Estado: CERRADA (2026-07-05 → 2026-07-07). Todos los ítems del checkpoint ejecutados bajo veredicto de usuario, con guard y verificación por ítem.
- Reemplaza: el informe de Fase 1 anterior, invalidado por contaminación metodológica (ancló en auditorías previas, invirtió la jerarquía doc↔código, decidió valores unilateralmente). Este informe se derivó de cero.
- Método: clean-room — entradas prohibidas
docs/old-deprecated/**, informes previos dedocs/audit/**,tmp/component-audit.md; sin agentes; evidencia primaria (lectura directa + greps reproducibles + sondas de navegador). La norma es la decisión de diseño documentada y fechada (changelog/RFC/canon); el código discrepante es deriva, y las fechas arbitran. Ningún valor se decidió sin veredicto explícito del usuario. - Desviación sancionada del plan: el plan original era solo-informe. En el checkpoint (F) el usuario redirigió a resolver ítem a ítem — análisis (implicaciones + referencias de frameworks + coherencia del ecosistema) → veredicto → ejecución con nota fechada + guard + verificación. Este informe documenta hallazgos y su resolución.
- Detalle canónico: cada ejecución dejó su crónica en
docs/theming/changelog.md§39–§41 y notas fechadas en secciones/RFCs previos. Este informe enlaza, no copia.
1. Resumen ejecutivo
El sistema quedó, tras la ejecución, en el estado que su propia doctrina promete: una fuente por concepto, todo eje como dato del config, contrato ≡ emisión, y cada arreglo sistémico con su guard. Los cinco hallazgos de mayor calado, todos ejecutados:
- El contrato CSS era un segundo censo a mano y había derivado 211 tokens (familias enteras — depth, named styles, scaling, breakpoints, banda overlay-z — invisibles para
setCssVariablesestricto y para el esqueleto de temas CSS-only). Hoy el contrato se deriva de la propia emisión (census-as-data) con muro bidireccional en suite. §2-A.9. - Dieciséis alias token-level (puentes de la migración air→eidos, 2026-05-14) mantenían una segunda gramática — incluida una cadena de TRES nombres para la tipografía de UI y un par duplicado de focus con ambos nombres adoptados (36 vs 13 refs). Doctrina de usuario: "no quiero alias" → migración value-preserving (~207 refs) y muerte de todos. §2-A.9.
- El pick de contraste on-solid vivía dos veces: computado en el bucle de roles y como lista curada a mano en la cascada per-instance — y la lista había derivado:
color="orange"embarcaba tinta blanca a 2.97:1 (sub-AA). Hoy hay un criterio único computado (pickOnSolid, blanco-preferente salvo fallo de AMBOS suelos APCA≥60 y WCAG≥3) consumido por ambos caminos + test de paridad. §2-A.7. - Knobs canónicos cocidos fuera del sistema de datos (state-layer en
archetypes.css; radius-factor/default, inset-ring, floating-gaps como constantes de emisor). Doctrina de usuario: "todo tiene que ser tematizable" → promovidos aprimitives.{state,floating,radiusFactor,radiusDefault}+border.insetRingWidth, valores verbatim. §2-A.10. - La clase eager-freeze: tokens de
:rootreferenciando privados--_*de CSS de componente se congelaban guaranteed-invalid — las marcas de festivo/evento del calendario nunca pintaron, las alturas de segmento computaronautodesde su nacimiento, y un slot inexistente (--color-content-tertiary) heredó color un mes. Resueltos con veredictos + dos guards (G1 scope-para-privados, G2 referencias-fantasma contra el contrato derivado). §2-B.
Métricas finales: contrato 4.540 nombres (3.383 static + 1.157 theme), emisión ≡ contrato en ambas direcciones (por construcción + guard), solo 31 paths (derived) (el resto con procedencia real de config). Suites eidos 299/299 · check 59 (baseline intocada) · component:audit 86/47/1 (baseline) · docs:check 0 errores.
2. Hallazgos y resolución (checkpoint F, con veredictos)
Formato: clasificación · severidad · evidencia · norma · veredicto → ejecución → guard. Severidades: RC rompe contrato prometido · SE salida errónea silenciosa · DR deriva doc↔código · OL olor/carencia.
A.1 — El eje scaling no estaba definido como dato (carencia · OL→RC)
- Evidencia: la participación por familia (space/controlHeight/fontSize/iconSize/blur escalan; radius/borderWidth/shadow/motionDistance nítidos) vivía implícita en los emisores;
rfc-scalingademás contenía la falsedad "as in Radix" (Radix SÍ escala su radius). - Veredicto: el modelo es canónico y se define por elemento como dato (
ScalingParticipationMap), default general nitidez-del-chrome. - Ejecución:
STATIC_SCALING_PARTICIPATION+PrimitiveSet.scaling(mapa parcial mergeable); emisores consumen el mapa; RFC corregido con nota. Verificado en vivo: radius 6px→6px bajo zoom 110 mientras font 16→17.6. - Guard: test de flip (radius:true opta al zoom) + asserts del default. Changelog §23 nota fechada.
A.2 — Container widths: colisión de namespace + fantasma (SE)
- Evidencia:
--container-width-xlre-declarado con valor fantasma (--layout-container-width-xl, 80rem— la clave sombreada no existía); breakpoints y container desconectados. - Veredicto: interrelación canónica —
--container-width-{k}referenciavar(--breakpoint-{k})(480/768/1024/1280/1536); ancho de página canónico = xl 1280. - Ejecución + guard: STATIC_LAYOUT re-anclado; fantasma muerto; test de referencia; verificado en vivo los 5 valores. Changelog §35 nota.
A.3 — El tracking óptico no llegaba a los named styles (DR)
- Evidencia: 12
letterSpacing: '0'redundantes en styles pisaban el tracking por-tamaño; 3 lineHeights duplicados. - Veredicto: los styles heredan la óptica del tamaño salvo divergencia deliberada anotada.
- Ejecución + guard: 12+3 eliminados, 9 divergencias anotadas
/* diverges … on purpose */; guard styles-never-redeclare; verificado hero −0.02em→−1.6px@80px. rfc-typography pauta + changelog §35.
A.4/A.5 — Canon temporal de sema: cita falsa + doble tabla de holds (DR·SE)
- Evidencia:
holds.tscitaba "cap. 24 §6.2 verbatim" — esa sección es otra cosa; el libro da regiones cualitativas, no ms (c4 §13 · c12 §4-9 · TABLA 32.1/32.2). Dos tablas de holds (holds.ts y sema-map.ts) podían discrepar. El hold actuaba de tijera sobre la expresión visual. - Veredicto: el libro es el canon; los ms son materialización del framework; peldaño intermedio
settled: 400; "el hold es suelo, no tijera". - Ejecución: citas reales;
SEMA_HOLDS_BY_INTENT= única tabla (resolver + canal visual la consumen; la columna de sema-map murió);commit.fulfill → settled,signal.loss → brief;awaitExpressionespera el fin de la animación tras el hold con techo absolutoMAX_EXPRESSION_WAIT_MS = 1500(nunca derivado del hold — el "2×hold" propuesto se rechazó por re-acoplar presupuestos); firmas announce retuneadas a regiones del libro. - Guard: triple (granularidad fulfill 400 · suelo-no-tijera · cap con timers por fases) + design-lint: toda firma ≤ cap. Sema 178/178.
book-deviations.mdD.12 (candidata editorial ApD).
A.6 — Depth Decisión 8 (ratificación, docs-only)
El plano pinta el bundle de APARIENCIA (surface·border·shadow+halo·tipografía on-surface); z JAMÁS se pinta — "el plano pinta, el posicionador posiciona". rfc-depth §5 + changelog §29 Adopción-v2. (La cobertura Tier-A de estampado data-depth → auditoría de componentes.)
A.7 — On-solid: lista curada vs cómputo (SE — accesibilidad)
- Evidencia (medición mecánica): set computado {cyan, yellow, amber, orange, sky, mint, lime, gold} vs lista
LIGHT_SOLID_SCALESde 6 — orange blanco a 2.97:1 sub-AA y cyan Lc 59.5 embarcados; 0 desacuerdos light↔dark en las 33 escalas. - Veredicto: P1 — cómputo en generación + criterio único canonizado como el shipped: blanco-preferente salvo fallo de AMBOS suelos (APCA |Lc|≥60 Y WCAG≥3). El "elige |Lc| mayor" del borrador del RFC quedó rechazado (volcaría media paleta: teal 60.5, grass 60.2, blue 62.6…).
- Ejecución + guard:
lib/on-solid.ts(lapickOnSolidque rfc-ce §6.1 nombraba) consumida por AMBOS caminos; test de paridad rol↔instancia + pin del set; instancias orange/cyan → tinta oscura (verificado + captura). rfc-ce §8 corregido; changelog §24.1 actualización.
A.8 — Dos idiomas dimensionales: px/py vs logical properties (RC)
- Evidencia: 198 claves
p[xy](+3my) evadían R-4.4; 20 shorthands físicos. - Veredicto (literal): "normalizar el ecosistema de una puta vez… aunque haya que reescribir todo"; exclusión words/palabras/chronos LEVANTADA para el codemod.
- Ejecución + guard: codemod atómico 57 ficheros (~570 nombres) + longhands lógicos; R-4.4 ampliado a segmentos abreviados (error, sin allowlist); muro de tipos
defineRecipes(clave física no compila). Before/after idéntico en navegador. recipe-contract §1 nota.
A.9 — Alias token-level + contrato-censo derivado (RC·DR) — el mayor
- Evidencia: (a) 211 tokens emitidos fuera de
getCssContract()(sonda reproducible: parse de declaraciones--x:de la emisión vs el census a mano) —setCssVariablesestricto LANZABA sobre vocabulario legítimo; (b) 16 alias (función literalappendTransitionAliasDeclarations, nacidos6e8ced2e2026-05-14):--font-ui(cadena de 3 nombres, 94 refs),--font-mono(64),--color-focus-ringvs--focus-ring-color(13 vs 36 — ambos vivos),--text-{1..6}-*(que además evadía la regla §5 del bundle, como px/py evadía R-4.4),--radius-xs(step inexistente), etc. - Veredictos: P1 directa (contrato derivado); "no quiero alias"; font-ui muere →
--style-label-font-family+ validación:styles.label.familyes requisito; los knobs(derived)se promueven en lote con A.10. - Ejecución: codemod ~207 refs (con reversión selectiva del vocabulario local del dev-site —
layout.cssusaba--font-sans/--font-monocomo hook propio: renombrarlos habría re-tipografiado los componentes del site);lib/contract.tsreescrito — parsea la emisión real (static + temasscales:'all'; tema sintético para configs CSS-only), metadatos por tabla de reglas (una familia sin regla entra igual: degrada metadatos, nunca cobertura), WeakMap cache; 4 tokensfield-*compartidos: par adoptado → recipefield; par hover superseded por el state-layer con 0 consumidores → eliminado con veredicto explícito. - Guards: muro bidireccional (emisión-pública ≡ contrato, ambas direcciones) + pins de familias nuevas + pins de los 16 alias muertos. Changelog §39 (crónica completa).
A.10 — Knobs cocidos en emisor → config (RC)
- Evidencia:
--state-{hover,press,selected}hardcoded enarchetypes.css :root(8%/12%/12% — los números de M3, sobrecurrentColor);--radius-factor: '1',--radius-default: md,--ring-inset-width: medium,--floating-gap-{menu,panel}como constantes de render-css. Ninguno expresable en config, todos fuera de validación/serialización. - Veredicto (literal): "todo tiene que ser tematizable" — a la capa primitives, defaults verbatim.
- Ejecución + guard:
primitives.state+radiusFactor+radiusDefault+border.insetRingWidth+primitives.floating; el bloque de archetypes.css murió (las reglas quedan); validación (porcentajes, membresías); test de retune (hover 4%, factor 1.25…); +5 pins al muro. Cero cambio visual (verificado). Changelog §40.
B — Fantasmas value-changing: la clase eager-freeze (SE ×3)
Mecánica común: custom property de :root referenciando --_* de CSS de componente → congela guaranteed-invalid → herencia congelada (punto ciego del TSC: su inferencia solo ve deps públicas).
| Fantasma | Realidad shipped | Veredicto → ejecución |
|---|---|---|
--calendar-{day-holiday,event}-shadow |
nunca pintaron | "las features deben existir" → scope:'host' (dato TSC): pintan por primera vez (verificado + captura). Préstamos cross-component (range-calendar 79 tokens de calendar, month/year-grid 65, drp 38) → veredicto de diseño: la familia calendar se formaliza como capa compartida/arquetipo (chronos beberá de ella) — registrado. |
--{date,time,color}-field-segment-height |
auto desde su nacimiento |
Veredicto de diseño: la altura sale del eje size a nivel familia field (--field-control-height-{k}, hoy re-duplicada por cada x-field) — mandatado: los componentes deben incorporar el wrapper Field (sondado: los segmentos NO viven bajo [data-field]). Mientras: 3 tokens rotos + 4 consumos eliminados (cero cambio visual, verificado 20px→20px). |
image-adjustments value-color: var(--color-content-tertiary) |
slot inexistente — heredó color desde 7da7285d (2026-06-11) |
Corrección de usuario: "tertiary era un color de ACENTO" (mezcló namespace content con nombre de ROL) → var(--color-tertiary-text); el read-out pinta el acento (verificado). |
- Guards: G1 — referencia a
--_*en valor de recipe exige scope que la cubra (nunca:root); G2 — toda referencia pública sin fallback debe existir en el contrato derivado (--color-content-tertiaryhabría roto el build el día que se escribió). Changelog §41.
C — Lote doc-sync: 18 contradicciones doc↔doc (DR ×18)
Arbitradas por fechas/código y corregidas al doc con nota fechada. Las de más peso: la tabla de gramática §6 de reference ganó 16 familias ausentes; "6 sizes"→7; "two builders"→seis + capstone applyTheme (existía; el "pendiente" del changelog llevaba semanas stale); escala tipográfica corregida (xxl 32→48 · xxxl 40→80); stubs §32/§36 al día; los "13 slots" anotados como crónica (12 vigentes, border-hover retirado); arch-eidos ya no enseña como ejemplo el anti-patrón R-2.3; smoothing = exponente (el "factor 0..1" del RFC nunca aterrizó). El detalle ítem a ítem quedó en las notas de cada doc (todas fechadas 2026-07-07). El nº 17 no era prosa: es la decisión D-1 de §5.2.
3. Censo de tokens — estado final
| Clase | Antes | Después |
|---|---|---|
| Duplicados (dos nombres, un concepto) | 16 alias + par focus duplicado + doble censo (contrato) + doble tabla de holds | 0 — un nombre por concepto; contrato y holds con fuente única |
| Repetitivos (patrón que la gramática debería factorizar) | height-{xs..xl} re-declarada por cada x-field; alturas de segmento ×3; vocabulario calendar prestado ×5 componentes (~250 refs) |
Registrados como diseño de familia para la auditoría de componentes (field-family + calendar-surface/arquetipo) |
| Contradictorios | on-solid lista vs cómputo; holds ×2; §36 gap 0 vs space-1; 13 vs 12 slots; … | 1 vivo → decisión D-1 (0.96 vs 0.97) |
| Fuera de gramática | --state-*/--floating-gap* sin casa en config; p[xy] ×198; text-N numérica paralela |
0 sistémicos |
4. Matriz de ejes ortogonales — post-ejecución
Todos los ejes cumplen ahora las cinco columnas — definido como dato de config · emitido · contratado (derivación) · con guard · con pauta documentada: color (escalas/roles/slots + on-solid computado) · tipografía (escala + styles + óptica heredada) · espacio/density · size-bundle (7 tamaños, coordenadas completas incl. letter-spacing) · radius (+factor/default) · border (+inset-ring) · sombra/depth (planos+cues) · motion (duraciones 9 + métricas + loops) · state-layer · scaling (mapa de participación) · shape · blur · gradient · floating-gaps · focus (dos anillos, inner-width) · z (banda overlay) · opacity · breakpoints (fuente de container).
Carencias restantes conocidas (registradas, no defectos silenciosos): per-theme-id (themes.{id}) solo retunea color+shadow — los demás ejes son config-level (suficiente hoy; evolución registrada si un tema-id real lo pide); la adopción por componentes de varios ejes es la deuda que cobra la auditoría de componentes (§5.3).
5. Plan de normalización
5.1 Ejecutado durante la auditoría (nada que aprobar)
Todo §2, con crónica en changelog §23–§41 (notas fechadas), RFCs corregidos (scaling · typography · depth · shape · structure · color-model · color-engine §6.1/§8) y docs/CANON.md/book-deviations.md D.12. Verificación por ítem: suites (eidos 299/299 · sema 178/178) + check 59 + component:audit 86/47/1 + docs:check 0 + navegador con sondas y capturas (before/after donde hubo riesgo visual).
5.2 Decisiones de diseño requeridas (NO ejecutadas — tu aprobación)
- D-1 · press 0.96 vs 0.97: el token
--motion-scale-press: 0.97no tiene consumidor; la firma press-squeeze usa el literal0.96(ychannels.mddocumenta 0.96). Opciones: (a) la firma consume el token y el token pasa a 0.96 (canoniza el shipped); (b) firma a 0.97 (canoniza el token; cambio perceptible mínimo); (c) anotar el literal como deliberado y podar el token. Referencia: M3 usa un único pressed-scale de sistema. - D-2 · huérfanos deliberados con decisión pendiente documentada:
--focus-ring/--focus-ring-errorcompuestos (0 consumidores CSS — changelog §32 los deja "podar es decisión de API aparte") y la poda de los--{x}-bg-hoverhuérfanos post-state-layer (changelog §38 ◇). Podar/mantener = decisión de API. - D-3 · Meter y CVD: hueco G3 documentado (zona solo-color, WCAG 1.4.1) — ¿cue no-cromático obligatorio o dev-warning?
- D-4 · capa 4 (component-color): la FAQ la deja "candidata a deprecación si en ~6 meses no hay consumidor" (vence ~2026-11) — confirmar o colapsar entonces.
5.3 Iniciativas registradas (docs/next-features.md)
- Paridad de rampa tonal con M3 — etapa 1: tabla de pares slot↔slot con suelos + medición en generación (generaliza
on-solid.ts); etapa 2: garantía por construcción engenerateScale, gated en la migración base→seeds (rfc-ce §9). - Emisión per-theme de la cascada palette-contrast — trigger documentado: un tema real que discrepe de polaridad.
- Auditoría de componentes (la deuda de adopción, con mandatos): Field composition (segmentos bajo
[data-field][data-size]+ token de familia + des-duplicación de alturas — MANDATADO); calendar-surface/arquetipo (formalizar la familia; chronos); adopción del bundle letter-spacing (~34 recipes) + ~7 literales; named-styles en controles; cobertura Tier-Adata-depth; los borrowers de B.2; los duplicados de matemática de los pickers. - sema.md hold-sync (bloqueado: fichero WIP del autor) y nota editorial del libro (D.12, "el hold es suelo, no tijera").
5.4 Guardas añadidas (para que nada reaparezca)
Participación de scaling (flip test) · container↔breakpoint refs · styles-never-redeclare óptica · triple guard temporal sema + design-lint firmas≤cap · paridad on-solid rol↔instancia + pin del set computado · R-4.4 ampliado (segmentos físicos, error) + muro de tipos defineRecipes · muro bidireccional emisión≡contrato + pins (familias nuevas, alias muertos, state/floating) · validación styles.label.family + porcentajes state + membresías radiusDefault/insetRingWidth · test de retune de knobs · G1 (privados exigen scope) · G2 (referencias fantasma vs contrato). Doctrina cumplida: cada fix sistémico se empareja con un guard — y tres de ellos pararon errores míos durante esta misma auditoría.
Auditoría Fase 1 cerrada 2026-07-07. Siguiente fase natural: la auditoría de componentes (§5.3-3), que cobra la adopción de los ejes que este pass dejó canónicos, contratados y guardados.