docs(color): amplía el cierre de Stage 2 — RFC §15 + README + stub

Documenta el resultado (el solver de contraste era innecesario: el morph HEREDA
el contraste de donantes §40-compliant) en los docs de diseño/código:

- docs/rfcs/rfc-color-engine.md: §15 nueva (resolución Stage 2 con las 3 bancas
  de evidencia + decisiones D1–D6), anotación de fases §11 (Phase 0 = tipos
  diferidos D5; Phase 3 = base→seeds NO se hace, D2), footer + §14.
- src/arts/color/README.md: CONTRAST_PAIRS en la API + nota "contraste heredado,
  no resuelto" con puntero a RFC §15 y reference §40.
- src/uix/eidos/COLOR_ENGINE_RFC.md (stub): deja de sobreafirmar que el tipo
  ColorScaleSeed existe (está diferido, D5) + puntero a §15.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alpha-0.1-sec-dom
dev 3 months ago
parent 7cfacba633
commit 73411f1440

@ -562,6 +562,10 @@ bump if the authored `scales` shape changes; old 12-hex scales read the same.
## 11. Phase plan (each verifiable and mergeable alone)
### Phase 0 — Types + generator (build-time lib), no behavior change
> **Status (2026-07-20):** the generator + math shipped as `$color` (an
> isomorphic art). The config-authoring types (`ColorScaleSeed` /
> `ColorScaleSource`) stay **deferred** — decision D5 (§15): runtime seed paths
> were the validation banks; the types land when a config authors seeds.
- Add `Oklch`, `ColorScaleSeed`, `ColorScaleSource`, widen `ThemeColorSet.scales`.
- `lib/themes/generate-scale.ts` (template morph) + OKLCH↔sRGB↔P3 conversion +
gamut-mapping. Not consumed yet — the existing scales stay verbatim.
@ -579,6 +583,9 @@ bump if the authored `scales` shape changes; old 12-hex scales read the same.
the color saturates. Test that the fallback always exists.
### Phase 3 — Migrate base + grafito to seeds
> **Status (2026-07-20): not done, not needed** — decision D2 (§15) keeps the
> base **verbatim ground-truth**; the contrast audit guards it. The §9 cleanup
> rows it bundled (primary≡loss etc.) were already fixed in code separately.
- Re-author `themes/base.ts` and `_lib/grafito.ts` with `seed`. Fix
`primary≡loss`.
- **Verify**: per-step ΔE2000 vs the current hex within tolerance; visual
@ -678,9 +685,68 @@ sacrifices.
The RFC puts eidos **on par with Radix/M3 in color fidelity**
(OKLCH+P3+APCA+generator) while keeping what it already had **exclusively**
(TSC + the sema layer). That is the "next level".
(TSC + the sema layer). That is the "next level". One addition since: the
tonal-ramp text contrast is now **by construction + CI-guarded** (§15) — a
guarantee Radix (curation) and Tailwind/Chakra (no contrast logic) don't make,
though the mechanism is inheritance, not a solver.
---
**Last revision**: 2026-06-04. If anything here contradicts the code after
implementation, the code wins — open an issue to sync.
## 15. Stage 2 — tonal-ramp text contrast: inherited, not solved (2026-07-20)
The "contrast parity" initiative ([`next-features.md §1`](../next-features.md))
had two stages. Stage 1 (2026-07-19) measured the slot-pair promises and ratified
the doctrine ([`theming/reference.md §40`](../theming/reference.md) ·
[`changelog.md §44`](../theming/changelog.md)). Stage 2 was planned as a
**by-construction generator that SOLVES each step's luminance** to satisfy the
pair table for any seed ([execution plan](../process/contrast-stage2-plan-2026-07.md),
verified by a 7-agent workflow; 6 user decisions locked 2026-07-20 — D1 `text·11`
soft band ≈APCA 60 · D2 measured-pass · D3 flip-only on-solid · D4 opt-in post-pass
· D5 runtime-first · D6 pair-table-as-data).
**Execution disproved the premise — no solver was needed.** The template morph
(§6) does not *compute* text contrast; it **inherits** it. Steps 11/12 (the text
inks) copy the donor's L-curve **verbatim**, and text contrast is dominated by
lightness — the sRGB gamut-map reduces chroma at **fixed L**, so contrast is
~**chroma-invariant**. The exact-anchor (§6 step 5) only moves step 9. Therefore
**every scale generated from a §40-compliant donor library inherits the ratified
text floors by construction.** The base itself is §40-compliant (D2 keeps it
verbatim ground-truth), so `applyColorScheme` / `generatePalette` — which morph
from the active theme's scales — inherit compliance too.
Measured across three banks (`scripts/contrast-audit.ts` + the CI guard):
| Bank | Hard gate `text-strong·12` ≥ 4.5 WCAG |
|---|---|
| Authored base (33×2) | ✅ 198/198 · min 9.82:1 |
| Morph-generated, leave-one-out (33×2) | ✅ 198/198 · min 9.87:1 |
| Out-of-distribution seeds (L 0.30–0.88, C ≤ 0.24) | ✅ 0 failures · min 9.7:1 |
The luminance solver would have had nothing to resolve for realistic inputs, so
it was **dropped as speculative**. What shipped instead:
- **The ratified pair table as shared data** — `$color` → `CONTRAST_PAIRS`
(`arts/color/contrast-contract.ts`): the hard `text-strong·12` floor, the
`text·11` soft band (cap relational to `text-strong`, no magic number), and the
`border·7` exemption. One source consumed by the audit and any future checker.
- **The audit consumes it + a morph-generated regression bank**
(`scripts/contrast-audit.ts`; its pre-verdict `PAIRS` — which gated `text·11` at
a hard 4.5, contradicting the §40 verdict — is gone).
- **A CI guard that locks the inheritance** —
`eidos/lib/contrast-invariant.test.ts` (the `palette-invariant.test.ts` pattern):
fails if a future authored family or a generator change breaks the property. The
by-construction guarantee is now a test, not a claim.
**Consequences for this RFC's phase plan (§11):** Phase 3 (migrate base→seeds) is
**not done and not needed** — D2 keeps the base verbatim (the audit guards it).
Phase 0's config-authoring types (`ColorScaleSeed` / `ColorScaleSource`) stay
**deferred** (D5) — the runtime seed paths that already exist were the validation
banks; the types land when a config authors seeds. The only scenario where a
luminance solver would earn its keep is a **custom, NON-§40-compliant donor
library** — which the framework does not ship, and which the CI guard would flag.
---
**Last revision**: 2026-07-20 (§15 Stage 2 resolution — contrast inherited, not
solved). If anything here contradicts the code after implementation, the code
wins — open an issue to sync.

@ -47,6 +47,7 @@ shifts the hue). The `oklch(...)` string keeps the full gamut for the browser.
| `oklchToHex` / `oklchToCss` / `oklchToGammaRgb` | OKLCH → sRGB fallback / `oklch()` string / gamma sRGB |
| `isInSrgbGamut` | does an OKLCH fit sRGB without chroma reduction? |
| `apcaLc(text, bg)` / `wcagContrastRatio(a, b)` | contrast (APCA Lc · WCAG2 ratio) — gamma sRGB inputs |
| `CONTRAST_PAIRS` (`ContrastPair[]`) | ratified §40 slot-pair floors as data (audit + CI guard read) |
| `scaleToTemplate(hex[12])` | hand-authored scale → OKLCH template (curve donor) |
| `pickNearestTemplate(seed, templates)` | choose the donor whose solid is closest (L-weighted) |
| `generateScale(seed, template, { solidStep? })` | seed → 12 OKLCH steps (morph; solid anchored to seed) |
@ -64,6 +65,17 @@ so the solid step lands on the seed **exactly**. Beats a naive `l − k` linear
(constant chroma → muddy mids) because the donor's curve is already perceptually
placed. The 33 scales eidos already ships become the **template library**.
**Tonal-ramp text contrast is inherited, not solved.** Because the text steps
(11/12) copy the donor's L-curve **verbatim** and text contrast is L-dominated
(the gamut-map reduces chroma at fixed L → contrast is ~chroma-invariant), a scale
generated from a **§40-compliant donor library inherits the ratified text floors
by construction** — no luminance solver needed. The doctrine lives as data
(`CONTRAST_PAIRS`) and the property is CI-locked by
`eidos/lib/contrast-invariant.test.ts` (three banks: authored base, leave-one-out
regeneration, out-of-distribution seeds). Rationale + evidence:
[`rfc-color-engine.md`](../../../docs/rfcs/rfc-color-engine.md) §15 · doctrine
[`theming/reference.md`](../../../docs/theming/reference.md) §40.
## Theme builder (scheme derivation)
`deriveScheme(seed, variant?)` ports **Material 3's HCT `CorePalette` to OKLCH**: one

@ -1,7 +1,8 @@
# RFC — Next-generation color engine (moved)
✅ Implemented through phase 4-bis. The **physical layer** of color: OKLCH
authoring (`ColorScaleSeed`), the isomorphic 1-seed → 12-step generator
authoring (runtime seed → scale; the config-authoring `ColorScaleSeed` type is
**deferred** — D5, RFC §15), the isomorphic 1-seed → 12-step generator
(template morph, promoted to `uix.color`), wide-gamut output (hex +
`oklch()` sibling, default-on), APCA contrast with a WCAG2 floor,
`deriveScheme` (M3 formula) + `buildScheme` + runtime
@ -9,6 +10,10 @@ authoring (`ColorScaleSeed`), the isomorphic 1-seed → 12-step generator
stays closed in the theming reference §25 — this RFC changes how the vars
are produced, never their names.
**Stage 2 (tonal-ramp text contrast) closed 2026-07-20 — inherited, not solved**
(the morph inherits contrast from §40-compliant donors; no solver). See the moved
RFC §15.
**The RFC moved to the docs corpus:**
[`docs/rfcs/rfc-color-engine.md`](../../../docs/rfcs/rfc-color-engine.md)
— TL;DR, principles, generator algorithm, isomorphism (§6.1), scheme

Loading…
Cancel
Save

Powered by TurnKey Linux.