From 3c115ce093f63fb5ff17c9b5053343266bdcf5e0 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 16 May 2026 21:31:21 +0200 Subject: [PATCH] Guard Eidos namespace assignments --- continue.md | 11 +++++ src/uix/eidos/component-api-contract.test.ts | 46 ++++++++++++++++++++ src/uix/eidos/components/README.md | 5 +++ 3 files changed, 62 insertions(+) diff --git a/continue.md b/continue.md index 53d668231..5f7eb0f2d 100644 --- a/continue.md +++ b/continue.md @@ -50,6 +50,17 @@ Actualizacion permutation axes 2026-05-16: http://127.0.0.1:5192` -> 12 demos instrumentadas OK, 54 pasos, 28 variantes URL. +Actualizacion Eidos component API guard 2026-05-16: + +- `src/uix/eidos/component-api-contract.test.ts` ahora tambien valida que los + miembros declarados en cada `XxxNamespace` coincidan con las asignaciones + explicitas del barrel (`Tabs.Trigger = Trigger`, etc.). +- Esto cierra el hueco que dejaba el cast `const Tabs = TabsRoot as + TabsNamespace`: TypeScript no detecta una asignacion olvidada despues del + cast, pero la guardia estatica si. +- Validacion: `npx vitest run src/uix/eidos/component-api-contract.test.ts` + -> 3 tests OK; `npm run check` -> 0 errores, 0 warnings. + Arranque rapido para mañana: - Rama `active-uix` queda por delante de `gita/active-uix` con los commits de diff --git a/src/uix/eidos/component-api-contract.test.ts b/src/uix/eidos/component-api-contract.test.ts index 5b4a1a4df..fa4949c70 100644 --- a/src/uix/eidos/component-api-contract.test.ts +++ b/src/uix/eidos/component-api-contract.test.ts @@ -5,6 +5,12 @@ import { describe, expect, it } from 'vitest' const COMPONENTS_DIR = 'src/uix/eidos/components' const ROOT_FILE_EXCEPTIONS = new Set(['svg']) +interface NamespaceContract { + component: string + properties: readonly string[] + assignments: readonly string[] +} + function stripComments(source: string): string { return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1') } @@ -20,6 +26,22 @@ function readIndexSource(component: string): string { return stripComments(readFileSync(join(COMPONENTS_DIR, component, 'index.ts'), 'utf8')) } +function readNamespaceContract(source: string): NamespaceContract | undefined { + const match = /type\s+([A-Z][A-Za-z0-9]+)Namespace\s*=\s*typeof\s+\w+Root\s*&\s*\{([\s\S]*?)\n\}/.exec( + source + ) + if (!match) return undefined + + const component = match[1] + const body = match[2] + const properties = [...body.matchAll(/^\s*([A-Z][A-Za-z0-9]+)\s*:/gm)].map((m) => m[1]) + const assignments = [ + ...source.matchAll(new RegExp(`\\b${component}\\.([A-Z][A-Za-z0-9]+)\\s*=`, 'g')) + ].map((m) => m[1]) + + return { component, properties, assignments } +} + describe('Eidos component API contract', () => { it('keeps component barrels on the disciplined option C surface', () => { const violations: string[] = [] @@ -48,4 +70,28 @@ describe('Eidos component API contract', () => { expect(missingRoots).toEqual([]) }) + + it('keeps namespace type members and explicit assignments in sync', () => { + const violations: string[] = [] + + for (const component of readComponentDirs()) { + const contract = readNamespaceContract(readIndexSource(component)) + if (!contract) continue + + const declared = new Set(contract.properties) + const assigned = new Set(contract.assignments) + for (const property of declared) { + if (!assigned.has(property)) { + violations.push(`${component}: ${contract.component}.${property} declared but not assigned`) + } + } + for (const property of assigned) { + if (!declared.has(property)) { + violations.push(`${component}: ${contract.component}.${property} assigned but not declared`) + } + } + } + + expect(violations).toEqual([]) + }) }) diff --git a/src/uix/eidos/components/README.md b/src/uix/eidos/components/README.md index 7b3b06033..f638bc995 100644 --- a/src/uix/eidos/components/README.md +++ b/src/uix/eidos/components/README.md @@ -221,6 +221,11 @@ estos attrs/parts visuales. El lint base sigue siendo estricto con valores inválidos de enums morfo; la allowlist solo evita que el reporte de drift se llene de partes visuales intencionales. +`src/uix/eidos/component-api-contract.test.ts` protege la forma pública del +barrel: sin `Object.assign`, sin `Provider` público, root en +`{component}.svelte` y miembros del `XxxNamespace` sincronizados con sus +asignaciones explícitas (`Drawer.Trigger = Trigger`, etc.). + La guardia `src/uix/eidos/component-visual-attrs.test.ts` fija el cableado mínimo entre wrapper y recipe: si un wrapper visual declara una prop que se consume como `data-*` (`size`, `variant`, `color`, `position`, `columns`,