From bfd3b4020e6aebb60993e7b5e4c70c8d5ce749c1 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 16 May 2026 21:46:28 +0200 Subject: [PATCH] Validate persisted Eidos configs --- continue.md | 21 +++++++++++++++ src/uix/eidos/README.md | 7 +++++ src/uix/eidos/active-eidos-config.test.ts | 31 +++++++++++++++++++++++ src/uix/eidos/lib/config.ts | 8 ++++-- 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/continue.md b/continue.md index 5f7eb0f2d..731e89fa9 100644 --- a/continue.md +++ b/continue.md @@ -2,6 +2,27 @@ Fecha de corte: 2026-05-16. Rama: `active-uix`. +Actualizacion Eidos ecosystem 2026-05-16: + +- Correccion de alcance: no seguir tocando demos/rutas ni + `src/uix/eidos/components/*` sin orden explicita. El foco vigente es el + ecosistema Eidos fuera de componentes: `ActiveEidos`, `eidos/lib`, + contrato/generacion/persistencia, tests de runtime y documentacion de capa. +- Persistencia reforzada: `readEidosConfigFromDocument(...)` y + `parseEidosConfigFromJson(...)` ya no aceptan solamente un envelope valido; + al convertirlo en runtime validan tambien las `options` contra el contrato + completo de `EidosConfig` y lanzan `EidosConfigValidationError` si el tema + persistido no es usable. +- `parseEidosConfigDocument(...)` queda definido como parser de envelope + versionado (`kind/version/options`), no como validador semantico del tema. +- Guardia anadida: el contrato CSS de Eidos debe estar alineado con los + renderers generados; cada token `static` debe aparecer en `renderStaticCss()` + y cada token `theme` en `renderThemeCss('base-light')`. +- Validado: + - `npx vitest run src/uix/eidos/active-eidos-config.test.ts` -> 26 tests OK. + - `npx vitest run src/uix/eidos` -> 7 archivos, 65 tests OK. + - `npm run check` -> 0 errores, 0 warnings. + Actualizacion permutation runner 2026-05-16: - Segunda tanda de demos `/uix/components/*` instrumentada con diff --git a/src/uix/eidos/README.md b/src/uix/eidos/README.md index a62de3245..dc21f931d 100644 --- a/src/uix/eidos/README.md +++ b/src/uix/eidos/README.md @@ -349,6 +349,13 @@ con versiones no soportadas. La app decide dónde guardar ese documento (prefs, backend, archivo, etc.) y qué metadata de producto lo envuelve (`name`, owner, timestamps). +Al hidratar un documento como runtime (`config: document`, +`readEidosConfigFromDocument(...)` o `parseEidosConfigFromJson(...)`), Eidos +valida también las `options` contra el contrato completo de `EidosConfig`. +`parseEidosConfigDocument(...)` queda como parser de envelope: confirma +`kind/version/options`, pero no convierte ese envelope en configuracion +visual usable por si solo. + Integracion normal dentro de un arbol con `ActiveUix`: ```ts diff --git a/src/uix/eidos/active-eidos-config.test.ts b/src/uix/eidos/active-eidos-config.test.ts index c63c28495..eba54c7bf 100644 --- a/src/uix/eidos/active-eidos-config.test.ts +++ b/src/uix/eidos/active-eidos-config.test.ts @@ -12,6 +12,7 @@ import { EidosThemeNotFoundError, EidosConfigValidationError, extendEidosConfig, + parseEidosConfigFromJson, parseEidosConfigDocument, STATIC_CONTROL_HEIGHT, STATIC_DENSITY, @@ -58,6 +59,10 @@ function createEidosFromJson(json: string) { return createEidosFromDocument(parseEidosConfigDocument(json)) } +function cssVariableDeclarations(css: string): Set { + return new Set([...css.matchAll(/--[a-z0-9_-]+(?=\s*:)/g)].map(([name]) => name)) +} + function colorScale(seed: string): ColorScale { return Object.fromEntries( COLOR_SCALE_STEPS.map((step) => [step, `${seed}-${step}`]) @@ -299,6 +304,18 @@ describe('ActiveEidos config', () => { expect(() => parseEidosConfigDocument('{')).toThrow(EidosConfigDocumentError) }) + it('rejects persisted documents with invalid Eidos config options', () => { + const invalid = structuredClone(createThemeBaseEidos().toDocument()) as EidosConfigDocument + + ;(invalid.options.semantics.color.roles as Record).primary = + 'missing-scale' + + expect(() => createEidosFromDocument(invalid)).toThrow(EidosConfigValidationError) + expect(() => parseEidosConfigFromJson(JSON.stringify(invalid))).toThrow( + EidosConfigValidationError + ) + }) + it('ships typography families separately from typography styles', () => { const typography = createThemeBaseEidos().snapshot().primitives.typography! @@ -620,6 +637,20 @@ describe('ActiveEidos config', () => { expect(css).not.toContain('--size-full-control-height') }) + it('keeps the generated CSS renderers aligned with the CSS contract', () => { + const eidos = createThemeBaseEidos() + const contract = eidos.getCssContract() + const staticDeclarations = cssVariableDeclarations(eidos.renderStaticCss()) + const themeDeclarations = cssVariableDeclarations(eidos.renderThemeCss('base-light')) + + for (const token of contract.static) { + expect(staticDeclarations.has(token.cssVar), token.cssVar).toBe(true) + } + for (const token of contract.theme) { + expect(themeDeclarations.has(token.cssVar), token.cssVar).toBe(true) + } + }) + it('renders contract-aware runtime CSS variables', () => { const eidos = createThemeBaseEidos() const css = eidos.renderCssVariables( diff --git a/src/uix/eidos/lib/config.ts b/src/uix/eidos/lib/config.ts index f23df1f1b..e566ce65d 100644 --- a/src/uix/eidos/lib/config.ts +++ b/src/uix/eidos/lib/config.ts @@ -57,11 +57,15 @@ export function snapshotEidosConfig(options: EidosConfig): EidosConfigSnapshot { } export function readEidosConfigFromDocument(document: unknown): EidosConfig { - return snapshotEidosConfig(readEidosConfigDocumentEnvelope(document).options) + const config = snapshotEidosConfig(readEidosConfigDocumentEnvelope(document).options) + assertValidEidosConfig(config) + return config } export function parseEidosConfigFromJson(json: string): EidosConfig { - return snapshotEidosConfig(parseEidosConfigDocumentEnvelope(json).options) + const config = snapshotEidosConfig(parseEidosConfigDocumentEnvelope(json).options) + assertValidEidosConfig(config) + return config } export function createEidosConfigDocumentFromConfig(options: EidosConfig): EidosConfigDocument {