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/contrast-stage2-plan-2026-0...

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)

  1. Lee este doc ENTERO + reference.md §40 (la doctrina ratificada) + rfc-color-engine.md §6/§8/§9/§10/§11.
  2. 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.
  3. 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:line que 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 ✅) → buildScheme regenera 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/paleta y /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:4 SOBREAFIRMA que existen — ignóralo, el RFC real es docs/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: alphaColorOverBackground devuelve 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·9 NO 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/paleta y /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.css byte-exacto → regenerar con npm run generate:eidos-css es 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 · chronicle changelog.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.ts
    • color-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í).

Powered by TurnKey Linux.