16 KiB
| title | type | audience | status | date | related |
|---|---|---|---|---|---|
| Plan — Contraste Stage 2 (generador by-construction) · verificado para ejecutar en sesión aparte | process | human + agent | EJECUTADO Y CERRADO 2026-07-20 — 6 decisiones cerradas; el solver resultó INNECESARIO (el morph HEREDA el contraste, no lo computa). Entregado F0 + guard CI; solver descartado. Ver §OUTCOME abajo + changelog §45. | 2026-07-19 | docs/next-features.md §1 · docs/theming/reference.md §40 · docs/theming/changelog.md §44 · docs/rfcs/rfc-color-engine.md §6/§8/§9/§10/§11 |
Plan — Contraste Stage 2: el generador by-construction
✅ OUTCOME (2026-07-20) — ejecutado; el solver era innecesario
Las 6 decisiones se cerraron (D1 banda text·11 blanda ≈APCA 60 · D2
medido-pasa · D3 flip-only · D4 post-pass opt-in · D5 runtime-first · D6
tabla-como-datos ya). Al ejecutar F0 se refutó la premisa del solver: el
morph de plantilla hereda el contraste del texto (steps 11/12 = curva-L del
donante verbatim; contraste ~invariante al croma bajo gamut-mapping), así que
toda escala desde donantes §40-compliant cumple por construcción. Medido en
3 bancos (base, leave-one-out, 45 semillas OOD): 0 fallos del gate duro
text-strong·12, min WCAG 9.7:1. El solver de luminancia (F1) no tenía nada
que resolver → descartado por especulativo. F2/F3 no aplican (F3 ya estaba
gated-off por D2).
Entregado: $color CONTRAST_PAIRS (arts/color/contrast-contract.ts) ·
scripts/contrast-audit.ts refactorizado (muere la PAIRS pre-veredicto +
banco generado) · guard de CI eidos/lib/contrast-invariant.test.ts que bloquea
la herencia. Doctrina: reference.md §40; crónica: changelog.md §45.
Lo de abajo es el plan ORIGINAL tal como se verificó el 2026-07-19 — se conserva como registro de razonamiento (útil si aparece un donante custom NO-compliant, el único escenario donde un solver aportaría).
Objetivo en una frase
Que las escalas generadas desde semilla cumplan el contrato de contraste ratificado (§40) por construcción — resolviendo la luminancia de los steps de texto — en vez de heredarlo del donante por morph; y, solo si el usuario lo aprueba (gate), extenderlo al tema base migrándolo a semillas.
⚡ Empieza aquí (sesión nueva)
- Lee este doc ENTERO +
reference.md §40(la doctrina ratificada) +rfc-color-engine.md §6/§8/§9/§10/§11. - NO toques código hasta resolver las 6 decisiones del usuario (§Decisiones).
La primera (target de
text·11) define qué significa "resolver" — nada se puede especificar antes. - El terreno está VERIFICADO (2026-07-19, workflow de 7 agentes contra el
código real) — los anclajes de §Terreno son fiables; aun así, re-verifica
los
file:lineque uses (el árbol es compartido y se mueve).
Decisiones del usuario (ABIERTAS — por orden de consecuencia)
D1 · Target de text·11 para escalas GENERADAS. (a) blando ≈APCA 60 —
paridad visual con las 33 escalas enviadas (el contrato Radix que Stage 1
ratificó); (b) duro WCAG 4.5 — más accesible pero visiblemente más oscuro en
tonos turbios, rompe la paridad recién ratificada. Si (a): ¿solo suelo, o
banda (tope superior para que text·11 siga leyéndose "secundario" vs
text-strong)? Define qué resuelve el solver; bloquea todo lo demás.
D2 · Semántica de cobertura del BASE + tolerancia ΔE. (a) medido-pasa: el
base queda VERBATIM como ground-truth (rfc §12: "the 33 hex are ground-truth,
not deleted") y su garantía es por MEDICIÓN (hoy pasa; el harness la vigila);
(b) construido-pasa: regenerar el base desde semillas aceptando drift ΔE
(por-step ΔE2000, rfc §11 Fase 3 — fijar el número de tolerancia, el RFC
no lo da) + re-ratificar TODOS los pins de valor (§Guards). Esto ES la
decisión del gate; (b) cambia los píxeles del look.
D3 · Modo on-solid. (a) flip-only (default actual: light-ink-first, cae a
tinta oscura solo si falla AMBOS floors — on-solid.ts; el RFC RECHAZÓ en
canonización la alternativa que cambiaba el look, rfc §8:487-494); (b) añadir
un modo opt-in "oscurece el solid hasta que la tinta blanca pase" — sacrifica
la fidelidad de marca en semillas claras (clase amber/yellow/lime) porque
solid·9 está anclado EXACTO a la semilla (generate.ts:89). ¿Debe existir
siquiera? Recomendación: (a); el flip YA es el mecanismo by-construction.
D4 · Colocación y alcance del solver. (a) dentro de generateScale — lo
heredan TODOS los consumidores, incluido el output vivo de
applyColorScheme (Fase 4-bis SHIPPED → los temas white-label existentes
cambian de píxeles el día que aterrice: cambio visible, no refactor
interno); (b) post-pass opt-in compuesto por buildScheme + generatePalette
(+ la futura expansión config). Y alcance: ¿solo la capa de 9 roles de
buildScheme, o también el full-33 de generatePalette? Ojo además al
mandato de calibración del RFC (§6:257-261): el generador debe reproducir las
escalas curadas dentro de tolerancia — reproducir-ground-truth y
resolver-contraste son objetivos OPUESTOS; un opt-in evita el choque.
D5 · Tipos de Fase 0 ahora o después. ColorScaleSeed/ColorScaleSource
NO existen (solo prosa del RFC §5/§11; ThemeColorSet.scales sigue siendo
12-hex — config-types.ts:1015). (a) Stage 2 runtime-first contra los dos
caminos de semilla que EXISTEN (applyColorScheme + generatePalette), tipos
después; (b) aterrizar Fase 0 primero (autorar semillas en config) — arrastra
la decisión de persistencia: persistence.ts:48-52 RECHAZA versión ≠ 1 sin
lector de migración; ¿unión-mantiene-v1 o bump+lector-v1 (que no existe)?
D6 · Normalización de la tabla de pares. scripts/contrast-audit.ts:38-46
aún codifica la propuesta PRE-veredicto (4.5 duro en text·11) — un solver que
la consuma tal cual CONTRADICE los veredictos del 2026-07-19. ¿Re-codificar §40
como MÓDULO DE DATOS compartido (solver + auditoría consumen UNA fuente) ahora,
o junto con Stage 2? Recomendación: ahora — es F0 del plan y no depende de D1.
El terreno (VERIFICADO 2026-07-19 — qué existe de verdad)
El generador (src/arts/color/generate.ts): template morph puro. pickNearestTemplate
elige donante por proximidad del solid (L ponderada ×2 + hue; ignora chroma);
generateScale copia la curva-L del donante VERBATIM en 11 de 12 steps,
re-matiza el hue conservando el drift relativo, rescala chroma
multiplicativamente (seedC/tplSolidC), y devuelve la semilla EXACTA en el
solid (:89 — "exact brand anchor"). Cero contraste en la generación; el
contraste entra solo en el pick post-hoc pickOnSolid (APCA≥60 ∧ WCAG≥3,
light-ink-first) + onSolidWcagRatio (cross-check) + alphaOverBackground
(inversa de compositing en sRGB gamma). scheme.ts:11-12 lo dice explícito:
"HCT's tone→contrast is NOT ported".
Caminos de semilla HOY (los bancos de validación):
ActiveEidos.applyColorScheme(seed)(§26; Fase 4-bis ✅) →buildSchemeregenera SOLO la capa de binding de roles (--primitive-{role}-*, ≤9 roles) como bloque override gestionado; las 33--scale-*NO se re-emiten (actúan de donantes).generatePalette(full 33 desde anclas+carácter) → demos/temas/paletay/temas/estudio. Este es el banco del solver full-palette.- Config-authored seeds: IMPOSIBLE hoy (tipos de Fase 0 sin implementar; el
stub
src/uix/eidos/COLOR_ENGINE_RFC.md:4SOBREAFIRMA que existen — ignóralo, el RFC real esdocs/rfcs/rfc-color-engine.md). - El path build/estático NO ejecuta el generador (emite hex verbatim; rfc §6.1:280 lo admite). "Isomorfo" es propiedad del módulo, no comportamiento actual del build.
El base (792 hex = 33 escalas × 12 × 2 modos, en DOS ficheros):
themes/base.ts:47-217/219-389 (12 escalas/modo, 288 hex) +
themes/color-scales.ts:33-81 (RADIX_EXTRA_*, 21 escalas/modo, 504 hex,
spread en base.ts:216/388). La paleta es NUESTRA (sembrada de Radix v3;
gold/bronze/steel/fuchsia ya se autoraron DESDE SEMILLAS offline con
generateScale — sus semillas están documentadas en color-scales.ts:13-15;
las Radix-seeded no tienen semilla registrada → ¿derivar-inversa, pin como hex,
o re-sembrar? — sub-decisión de D2). Los semantic tokens (surface/content/
border/focus) referencian --primitive-* → CABALGAN la migración solos, salvo
3 literales autorados: content.onSolid (#ffffff), onSolidContrast (#1c1917)
(anclas deliberadas del flip APCA) y surface.backdrop.
El contrato downstream: NOMBRES congelados (rfc §10; contract.ts parsea
solo nombres) → recipes/TSC/CSS de componente/purge/API color intactos. PERO
hay pins de VALOR diseñados para dispararse conscientemente si los valores
driftan (§Guards). La emisión foundation solo lleva las ~9 escalas
referenciadas por roles; el full-33 es opt-in (generated/palette.css).
La matemática (restricciones del solver):
- La inversión "Y objetivo → L OKLCH" NO es cerrada: bisección iterativa
contra el sRGB GAMUT-MAPEADO (todo apcaLc/wcag pasa por
oklchToGammaRgb; el mapping reduce chroma a L fija,convert.ts:91-103). Sondeo empírico: Y post-map es no-monótona en L cerca de la frontera de gamut (~2% de dip); monótona en el rango útil de texto — la bisección converge (~40 evals/step). - El solver debe emitir HEX:
alphaColorOverBackgrounddevuelve null con no-hex y la rampa alfa degrada EN SILENCIO al fallback color-mix (render-css.ts:2608-2656). Mover la L de un step cambia todas las alfas derivadas — se re-ejecutan, no se pinnean. - Color de medición (decisión técnica a fijar): ¿contraste sobre el sRGB
mapeado (lo que mide el código hoy) o sobre el
oklch()wide que renderiza un display P3? Divergen tras la reducción de chroma.
El spec REAL del solver (derivado de la doctrina ratificada, NO de la propuesta pre-veredicto):
- ÚNICO floor duro ratificado:
text-strong·12≥ 4.5 WCAG sobre track·1 / bg·2 / element·3, por modo (formular como "resolver contra el extremo vinculante"; la auditoría midió 1 y 3 — 2 queda acotado entre ambos; ambos modos resuelven independiente, curvas-L distintas). text·11: lo que salga de D1 (blando ≈APCA 60 vs duro 4.5).- on-solid: el flip ES el mecanismo (D3);
solid·9NO se mueve (ancla de marca). - Steps 1–8/10: SIN floors ratificados — no se resuelven (el carácter del donante se conserva; "¿reemplazar la curva-L o desviación mínima?" — restringir a desviación mínima respecto al donante para no tirar el carácter perceptual que el morph existe para conservar).
- Bordes decorativos y foco: FUERA del solver (exentos §40 / eje de config §32).
Nota §9 (para no repetir trabajo): 3 de las 4 filas de limpieza del RFC §9
YA están arregladas en código (primary purple + loss plum base.ts:23,44;
tertiary indigo :32; border 6→7 render-css.ts:102) y el doc-drift alineado.
Lo que queda de §9 = la re-autoría a semillas en sí (rfc §11 Fase 3). Al pasar
por el RFC, marcar la tabla §9 como resuelta-en-código.
Plan por fases (cada una verificable y commiteable sola)
F0 — La tabla ratificada como datos (no depende de D1; recomendado hacerla ya).
Módulo de datos con la tabla de pares POST-veredicto (§40): floor duro
text-strong·12, tier blando text·11 (parametrizado por D1), floors on-solid,
exenciones. scripts/contrast-audit.ts pasa a consumirla (muere su constante
PAIRS pre-veredicto) y el futuro solver consume LA MISMA fuente. Extender la
auditoría para correr también sobre output GENERADO (hoy solo importa las
escalas estáticas del base) — ese es el harness de regresión de todo Stage 2.
F1 — El solver (tras D1/D3/D4). En $color (junto a generateScale),
como post-pass componible (o integrado, según D4): para cada modo, bisección de
la L de los steps 11/12 contra el sRGB gamut-mapeado hasta cumplir la tabla de
F0, con desviación mínima respecto a la curva del donante; salida hex.
Tests puros en color.test.ts (converge, respeta ancla del solid, hex out,
casos frontera de gamut: semillas alta-chroma clase amber/yellow).
F2 — Validación en los bancos de semilla (SIN tocar el base).
generatePalette→/temas/paletay/temas/estudio: full-33 generado con solver, auditoría F0 en verde sobre el output, y verificación EN NAVEGADOR (lección dura del track: la verificación de cascada/percepción de color exige navegador, no lectura de código).applyColorScheme: si D4=(a), documentar el cambio visible en white-label ANTES de aterrizar; si (b), demo con el opt-in activado.
F3 — (GATED por D2=(b)) Migración base→seeds. Re-autorar los DOS ficheros
(base.ts + color-scales.ts) como semillas (las 4 ya-sembradas primero — sus
semillas están documentadas); tolerancia ΔE fijada en D2; los 3 literales
autorados (onSolid/onSolidContrast/backdrop) se quedan a mano salvo decisión
contraria. Protocolo de re-ratificación de pins (§Guards) + pase visual en
navegador side-by-side (light+dark, /uix/components/* + /temas/grafito)
con reporte ΔE — el vehículo del "look-approval" del usuario, no un diff de texto.
Guards y verificación (qué se dispara y qué no)
NO se disparan con cambio de valores (estructurales): contract.ts (solo
nombres) · recipe-css-contract.test.ts (30/30, 0 hex) · checks de morfo · el
parity guard §8 (active-eidos-config.test.ts:1558-1581 — auto-consistente,
recomputa del config vivo).
SÍ se disparan — POR DISEÑO, exigen re-ratificación consciente (no mecánica):
- Pins de hex literal:
active-eidos-config.test.ts:1481,1485,1588-1589·active-eidos.test.ts:73,78. - El flip-set de polaridad pinneado:
active-eidos-config.test.ts:1525-1542(['amber','cyan','gold','lime','mint','orange','sky','yellow']) — su comentario MANDA actualización consciente si un step-9 cruza el floor. generated-css.test.ts:8-15:generated/base.cssbyte-exacto → regenerar connpm run generate:eidos-csses paso OBLIGATORIO de cualquier cambio de valor (un CI rojo aquí es "falta regenerar", no "contrato roto").
Operativa (heredada del track de color, sigue vigente): árbol COMPARTIDO
(chat/palabras editan base.ts de recipes en vivo — ojo: ese es
lib/recipes/base.ts, distinto de lib/themes/base.ts, pero el stage se hace
igual con rutas explícitas y git reset -q antes) · npm run check baseline
~76-77 inestable → verificar CERO errores en tocados · contract test como gate.
Posicionamiento honesto (corregido tras verificación)
Para no repetir sobreafirmaciones: el propio RFC (§14, 2026-06-04) registra a
Radix con APCA y P3 — la posición del framework es "on par with Radix/M3
en fidelidad de color"; los exclusivos que el repo reivindica son TSC +
sema, no el output de color. Nuestra paleta ENVIADA es sRGB-equivalente (los
oklch() son siblings de hex sRGB); el wide-gamut real vive solo en esquemas
generados. Lo que Stage 2 añade de verdad: garantía por construcción
(mecanismo clase-M3) decidida con APCA en vez de deltas de tono HCT — con D2
también en el base, algo que ni Radix (curación) ni Tailwind/Chakra (sin lógica
de contraste) hacen. Las caracterizaciones de competidores citan el RFC §14
(afirmación propia del repo, fechada) — si el plan las publica fuera, re-verificar
externamente primero.
Referencias
- Doctrina:
docs/theming/reference.md §40· chroniclechangelog.md §44. - RFC:
docs/rfcs/rfc-color-engine.md§5 (tipos) · §6 (generador+calibración) · §8 (on-solid) · §9 (limpieza, mayormente resuelta) · §10 (contrato congelado) · §11 (fases) · §12 (riesgos: "los 33 hex son ground-truth"). - Código:
src/arts/color/generate.ts·src/uix/eidos/lib/build-scheme.ts·src/uix/eidos/lib/generate-palette.ts·src/uix/eidos/lib/themes/base.tscolor-scales.ts·src/uix/eidos/lib/on-solid.ts·scripts/contrast-audit.ts.
- Registro:
docs/next-features.md §1(Stage 1 DONE; Stage 2 = este plan). - Verificación de este plan: workflow 7 agentes 2026-07-19 (transcript en la sesión de origen; los anclajes file:line de arriba salen de ahí).